feat: initialize parent-bff service with full core features

add complete parent-bff implementation including:
- GraphQL endpoint with depth/cost validation
- ChildGuard越权校验 with redis cache and singleflight
- parallel orchestration with partial failure fallback
- three-level cache fallback strategy (Redis + LRU + downstream)
- Kafka consumer for cache invalidation and notification push
- opossum circuit breaker for downstream services
- Prometheus metrics and SLO alerts
- Helm chart for k8s deployment with multi-environment configs
- Grafana dashboard for observability
- complete unit and integration tests
This commit is contained in:
SpecialX
2026-07-10 18:49:06 +08:00
parent a7d8f92227
commit 2229309a1e
88 changed files with 11221 additions and 0 deletions

View File

@@ -0,0 +1,163 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
// Mock safeRedis 以控制 Redis 行为(单元测试不依赖真实 Redis
vi.mock("../../src/shared/cache/redis.client.js", () => ({
safeRedis: vi.fn().mockResolvedValue(null),
getRedisClient: vi.fn(() => null),
closeRedisClient: vi.fn(),
}));
import {
withCacheFallback,
invalidateCache,
} from "../../src/aggregation/fallback-strategy.js";
import { safeRedis } from "../../src/shared/cache/redis.client.js";
const mockSafeRedis = vi.mocked(safeRedis);
describe("FallbackStrategy三级降级", () => {
beforeEach(() => {
vi.clearAllMocks();
mockSafeRedis.mockResolvedValue(null);
});
describe("Redis 缓存命中(第一级)", () => {
it("Redis 命中时返回缓存数据fromCache=true", async () => {
const cachedData = { id: "test-1", name: "测试" };
mockSafeRedis.mockResolvedValueOnce(JSON.stringify(cachedData));
const result = await withCacheFallback(
"test-redis-hit",
() => Promise.resolve({ id: "test-1", name: "不应该被调用" }),
30,
"test",
);
expect(result.data).toEqual(cachedData);
expect(result.fromCache).toBe(true);
expect(result.stale).toBe(false);
});
});
describe("Redis 未命中 → fn 成功(第三级直连)", () => {
it("Redis miss 后调 fn 获取数据并写入缓存", async () => {
const fnData = { id: "test-2", value: 42 };
mockSafeRedis
.mockResolvedValueOnce(null) // get → miss
.mockResolvedValueOnce(undefined); // set → ok
const result = await withCacheFallback(
"test-redis-miss-fn-success",
() => Promise.resolve(fnData),
30,
"test",
);
expect(result.data).toEqual(fnData);
expect(result.fromCache).toBe(false);
expect(result.stale).toBe(false);
// safeRedis 被调用 2 次get + set
expect(mockSafeRedis).toHaveBeenCalledTimes(2);
});
});
describe("LRU 缓存命中(第二级降级)", () => {
it("Redis 不可用时从 LRU 缓存返回数据", async () => {
// 第一次调用miss Redis + miss LRU → 调 fn → 写入 LRU
const fnData = { id: "test-3", label: "LRU测试" };
mockSafeRedis
.mockResolvedValueOnce(null) // get → miss
.mockResolvedValueOnce(undefined); // set → ok
await withCacheFallback(
"test-lru-hit-key",
() => Promise.resolve(fnData),
60,
"test",
);
// 第二次调用miss Redis → hit LRU
mockSafeRedis.mockResolvedValueOnce(null); // get → miss
const result = await withCacheFallback(
"test-lru-hit-key",
() => Promise.resolve({ id: "test-3", label: "不应该被调用" }),
60,
"test",
);
expect(result.data).toEqual(fnData);
expect(result.fromCache).toBe(true);
expect(result.stale).toBe(false);
});
});
describe("fn 失败降级", () => {
it("fn 抛错时返回 null", async () => {
mockSafeRedis.mockResolvedValueOnce(null); // get → miss
const result = await withCacheFallback(
"test-fn-fail",
() => Promise.reject(new Error("downstream error")),
30,
"test",
);
expect(result.data).toBeNull();
expect(result.fromCache).toBe(false);
expect(result.stale).toBe(false);
});
});
describe("Redis JSON 解析失败", () => {
it("Redis 返回非法 JSON 时降级到 fn", async () => {
mockSafeRedis
.mockResolvedValueOnce("not-valid-json") // get → 返回非法 JSON
.mockResolvedValueOnce(null) // LRU miss
.mockResolvedValueOnce(undefined); // set → ok
const result = await withCacheFallback(
"test-bad-json",
() => Promise.resolve({ data: "from-fn" }),
30,
"test",
);
expect(result.data).toEqual({ data: "from-fn" });
expect(result.fromCache).toBe(false);
});
});
describe("invalidateCache", () => {
it("调用 safeRedis del + LRU delete", async () => {
// 先写入 LRU
mockSafeRedis
.mockResolvedValueOnce(null) // get → miss
.mockResolvedValueOnce(undefined); // set → ok
await withCacheFallback(
"test-invalidate",
() => Promise.resolve({ val: 1 }),
30,
"test",
);
// 失效缓存
mockSafeRedis.mockResolvedValueOnce(undefined); // del → ok
await invalidateCache("test-invalidate");
// 再次查询,应该 miss LRU
mockSafeRedis
.mockResolvedValueOnce(null) // get → miss
.mockResolvedValueOnce(undefined); // set → ok
const result = await withCacheFallback(
"test-invalidate",
() => Promise.resolve({ val: 2 }),
30,
"test",
);
expect(result.data).toEqual({ val: 2 });
expect(result.fromCache).toBe(false);
});
});
});