1
0
Files
DiAL/openspec/changes/backend-architecture-hardening/design.md
lanyuanxiaoyao 147a2559ae refactor: 后端架构加固 — 泛型化、批量查询、bootstrap 统一、路径修复与 pageSize 上限
- CheckerDefinition 泛型化,HTTP/Command checker 移除 resolved target 断言
- 新增 ProbeStore.getAllRecentSamples 消除 targets 路由 N+1 查询
- 统一 getAllTargetStats 与 getTargetStats 的 availability 精度
- Engine rejected 结果写入 internal error 记录,提升可观测性
- 新增 bootstrap.ts 统一 dev/production 启动序列
- dataDir 相对路径改为基于配置文件目录解析
- validatePagination 增加 pageSize 上限 200 校验
- 修复 ErrorBoundary override 标记
- 更新 README/DEVELOPMENT 文档,新增完整测试覆盖
2026-05-13 18:15:46 +08:00

6.2 KiB
Raw Blame History

Context

DiAL 后端是基于 Bun 的拨测服务,当前有 2 个 checker 类型http、commandtarget 规模预计增长到 100checker 类型预计超过 5 种。

现状问题:

  1. GET /api/targets 对每个 target 单独查询 getRecentSamples,产生 N+3 次 SQL 查询
  2. ProbeEngine.probeGroup 中 rejected 结果仅 console.warn,前端无法感知异常
  3. dev.tsscripts/build.ts 生成的 entry 各自维护相同的启动序列
  4. config-loader.tsdataDir 未基于 configDir 解析,相对路径依赖进程 cwd
  5. validatePagination 无 pageSize 上限,可被滥用
  6. CheckerDefinition 接口方法参数为 ResolvedTargetBasechecker 内部需手动 as 断言

Goals / Non-Goals

Goals:

  • 消除 targets 路由的 N+1 查询,支撑 100 target 规模
  • Engine 异常可观测rejected 结果写入数据库,前端可见
  • 启动逻辑单一来源,降低维护成本
  • 修复 dataDir 路径解析 bug
  • API 防御性pageSize 上限
  • CheckerDefinition 泛型化checker 开发者获得编译期类型安全

Non-Goals:

  • 不做配置热更新
  • 不做 API 认证/鉴权
  • 不做通知/告警系统
  • 不改变 ResolvedTargetBase 的 index signatureregistry 层仍用类型擦除)
  • 不改变前端行为

Decisions

Decision 1: 批量查询 recentSamples 使用 window function

选择:在 ProbeStore 中新增 getAllRecentSamples(limit: number) 方法,使用 SQLite window function ROW_NUMBER() OVER (PARTITION BY target_id ORDER BY timestamp DESC) 一次查询所有 target 的最近 N 条采样。

替代方案

  • UNION ALL 拼接每个 target 的子查询 → SQL 长度随 target 数线性增长,不可控
  • 应用层批量(一次查全部再内存分组)→ 数据量大时内存开销高

理由window function 是 SQLite 3.25+ 原生支持的特性Bun 内置的 SQLite 版本满足要求。单次查询SQL 固定长度,性能最优。

Decision 2: Engine rejected 写入 internal error 记录

选择:在 probeGroup 中,对 rejected 的结果构造一条 matched: falsefailure: { kind: "error", phase: "internal", path: "engine", message: reason } 的 check_result 写入 store。

替代方案

  • 单独的错误日志表 → 增加 schema 复杂度,前端需要额外查询
  • 仅保留 console.warn → 现状,不可观测

理由:复用现有 check_results 表和 failure 结构,前端无需改动即可展示异常状态。phase: "internal" 区分于正常的 checker 执行失败。通过 Promise.allSettled 的索引关联回 target 数组,确保能获取 targetName。

Decision 3: 抽取 bootstrap.ts

选择:新增 src/server/bootstrap.ts,导出 bootstrap(options: BootstrapOptions) 函数封装完整启动序列loadConfig → ProbeStore → syncTargets → ProbeEngine → startServer → 注册 shutdown handler。

接口设计

interface BootstrapOptions {
  configPath: string;
  mode: RuntimeMode;
  staticAssets?: StaticAssets;
}

dev.ts 调用 bootstrap({ configPath, mode: "development" })build.ts 生成的 entry 调用 bootstrap({ configPath, mode: "production", staticAssets })

替代方案

  • 保持两处重复 → 维护负担随启动逻辑复杂化线性增长

Decision 4: dataDir 基于 configDir 解析

选择:在 config-loader.tsloadConfig 中,对 dataDir 使用 resolve(configDir, dataDir) 处理。如果 dataDir 是绝对路径,resolve 会直接返回绝对路径,不影响绝对路径用户。

影响:行为变更——之前相对路径基于 cwd现在基于配置文件目录。由于项目未上线无需向前兼容。

Decision 5: pageSize 上限 200

选择:在 middleware.tsvalidatePagination 中增加 pageSize > 200 的校验,返回 400。

常量定义MAX_PAGE_SIZE = 200,定义在 middleware.ts 中。

理由200 条/页对于拨测历史记录的展示场景足够。前端当前使用 20不受影响。

Decision 6: CheckerDefinition 泛型化

选择

interface CheckerDefinition<TResolved extends ResolvedTargetBase = ResolvedTargetBase> {
  execute(target: TResolved, ctx: CheckerContext): Promise<CheckResult>;
  resolve(target: RawTargetConfig, context: ResolveContext): TResolved;
  serialize(target: TResolved): { config: string; target: string };
  validate(input: CheckerValidationInput): ConfigValidationIssue[];
  readonly configKey: string;
  readonly schemas: CheckerSchemas;
  readonly type: string;
}
  • 默认泛型参数 = ResolvedTargetBase 保证 registry 等中间层无需指定泛型
  • CheckerRegistry 内部存储 CheckerDefinition<ResolvedTargetBase>(类型擦除)
  • 各 checker 实现 implements CheckerDefinition<ResolvedHttpTarget> 等具体类型
  • checker 内部 executeserialize 方法直接接收具体类型,无需 as 断言

替代方案

  • Discriminated union → 每加 checker 改 union违背插件化设计
  • 维持现状 → 5+ checker 时 as 断言散落各处

影响范围

  • runner/types.ts:接口加泛型参数
  • runner/registry.ts:内部 Map 类型为 CheckerDefinition(使用默认参数)
  • http/execute.tscommand/execute.tsimplements CheckerDefinition<具体类型>,移除方法内的 as 断言
  • engine.tsconfig-loader.tsstore.ts:不变(依赖 base interface

Risks / Trade-offs

  • window function 兼容性 → Bun 内置 SQLite >= 3.25,已验证支持。如果未来需要外部 SQLite需确认版本。
  • Engine rejected 写入依赖索引关联 → 通过 Promise.allSettled 的索引关联回 target 数组获取 targetName。前提是 probeGroup 的 targets 数组与 Promise.allSettled 结果数组保持一一对应,当前实现满足此条件。
  • bootstrap.ts 增加一层间接 → 启动流程从 2 处直接代码变为 1 处函数调用。复杂度不增加,只是位置移动。
  • 泛型擦除在 registry 层registry.get() 返回 CheckerDefinitionbase 类型engine 调用时仍是 base 类型。这是设计意图:中间层不感知具体 checker 类型。