chore: 归档 backend-architecture-hardening 变更并同步 delta spec 到主 spec
This commit is contained in:
@@ -17,7 +17,7 @@
|
||||
- **THEN** 该 target_id 在返回的 Map 中 SHALL 不存在对应的 key
|
||||
|
||||
### Requirement: 批量查询目标统计
|
||||
系统 SHALL 提供 `getAllTargetStats` 方法,通过单次 SQL GROUP BY 聚合查询获取所有 target 的拨测统计(totalChecks 和 availability)。
|
||||
系统 SHALL 提供 `getAllTargetStats` 方法,通过单次 SQL GROUP BY 聚合查询获取所有 target 的拨测统计(totalChecks 和 availability)。availability 计算精度 SHALL 与 `getTargetStats` 一致,统一使用 `Math.round(value * 100) / 100` 保留两位小数。
|
||||
|
||||
#### Scenario: 获取所有目标的聚合统计
|
||||
- **WHEN** 调用 `getAllTargetStats()`
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
#### Scenario: availability 精度
|
||||
- **WHEN** 计算 availability(upCount / totalChecks * 100)
|
||||
- **THEN** 结果 SHALL 四舍五入保留两位小数
|
||||
- **THEN** 结果 SHALL 使用 `Math.round(value * 100) / 100` 四舍五入保留两位小数,与 `getTargetStats` 方法一致
|
||||
|
||||
### Requirement: summary 查询使用批量方法
|
||||
`getSummary` 方法 SHALL 使用 `getLatestChecksMap` 一次性获取所有 target 的最新检查结果,而非对每个 target 逐条查询。
|
||||
@@ -39,11 +39,30 @@
|
||||
- **THEN** 系统 SHALL 调用 `getLatestChecksMap()` 一次获取所有最新结果,在内存中遍历统计 up/down 数量,而非循环 N 次调用 `getLatestCheck()`
|
||||
|
||||
### Requirement: targets 列表使用批量方法
|
||||
`createTargetsResponse`(app.ts 中生成 TargetStatus[] 的逻辑)SHALL 使用 `getLatestChecksMap` 和 `getAllTargetStats` 替代逐目标查询 latest checkout、stats 和 samples。
|
||||
`handleTargets`(routes/targets.ts 中生成 TargetStatus[] 的逻辑)SHALL 使用 `getLatestChecksMap`、`getAllTargetStats` 和 `getAllRecentSamples` 替代逐目标查询,消除 N+1 查询。
|
||||
|
||||
#### Scenario: 目标列表使用批量查询
|
||||
- **WHEN** 处理 `GET /api/targets` 请求
|
||||
- **THEN** 系统 SHALL 分别调用 `getLatestChecksMap()`、`getAllTargetStats()` 批量获取数据,在内存中组装 TargetStatus 数组,而非对每个 target 逐条查询数据库
|
||||
- **THEN** 系统 SHALL 分别调用 `getLatestChecksMap()`、`getAllTargetStats()`、`getAllRecentSamples(30)` 批量获取数据,在内存中组装 TargetStatus 数组,而非对每个 target 逐条查询数据库
|
||||
|
||||
#### Scenario: 目标无采样数据
|
||||
- **WHEN** 某 target 在 getAllRecentSamples 返回的 Map 中不存在
|
||||
- **THEN** 该 target 的 recentSamples SHALL 为空数组
|
||||
|
||||
### Requirement: 批量查询所有目标的最近采样数据
|
||||
系统 SHALL 提供 `getAllRecentSamples(limit: number)` 方法,通过单次 SQL 查询获取所有 target 的最近 N 条采样数据,返回 `Map<number, Array<{ timestamp: string; duration_ms: number | null; matched: number }>>` 结构。
|
||||
|
||||
#### Scenario: 获取所有目标的最近采样
|
||||
- **WHEN** 调用 `getAllRecentSamples(30)`
|
||||
- **THEN** 系统 SHALL 通过单次 SQL 查询获取每个 target 最近 30 条记录,返回按 target_id 索引的 Map
|
||||
|
||||
#### Scenario: 目标无历史记录
|
||||
- **WHEN** 某 target 在 check_results 表中无任何记录
|
||||
- **THEN** 该 target_id 在返回的 Map 中 SHALL 不存在对应的 key
|
||||
|
||||
#### Scenario: 采样数据排序
|
||||
- **WHEN** 获取采样数据
|
||||
- **THEN** 每个 target 的记录 SHALL 按 timestamp 降序排列(最新在前)
|
||||
|
||||
### Requirement: prepared statement 使用 query() 缓存
|
||||
ProbeStore 中不涉及事务内复用的单次读/写操作 SHALL 使用 `this.db.query()` 而非 `this.db.prepare()`,利用 bun:sqlite 内置的 statement 缓存机制。
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
- **THEN** checker SHALL 返回 `ConfigValidationIssue`,而不是直接抛出最终用户错误字符串
|
||||
|
||||
### Requirement: Checker 接口定义
|
||||
系统 SHALL 在 `src/server/checker/runner/types.ts` 中定义面向扩展的 `CheckerDefinition`,包含 `type`、`configKey`、TypeBox 配置契约、启动期语义校验、`resolve`、`execute`、`serialize` 成员。`CheckerContext` SHALL 包含引擎注入的 `AbortSignal`。接口方法的参数和返回值 SHALL 使用 base interface 类型(`RawTargetConfig`、`ResolvedTargetBase`),各 checker 实现内部自行 narrow 到具体类型。
|
||||
系统 SHALL 在 `src/server/checker/runner/types.ts` 中定义面向扩展的泛型 `CheckerDefinition<TResolved extends ResolvedTargetBase = ResolvedTargetBase>`,包含 `type`、`configKey`、TypeBox 配置契约、启动期语义校验、`resolve`、`execute`、`serialize` 成员。泛型参数 SHALL 约束 `execute` 和 `serialize` 方法的 target 参数类型,使 checker 实现内部获得编译期类型安全。默认泛型参数 `= ResolvedTargetBase` 保证中间层(registry、engine、config-loader)无需指定泛型。
|
||||
|
||||
#### Scenario: Checker 接口包含必要方法
|
||||
- **WHEN** 开发者实现一个新的 Checker
|
||||
@@ -68,20 +68,24 @@
|
||||
- **WHEN** checker 定义 `type: "tcp"`
|
||||
- **THEN** checker 的 `configKey` SHALL 默认使用 `"tcp"`,对应 target 的 `tcp` 分组和 defaults.tcp 分组
|
||||
|
||||
#### Scenario: 接口方法使用 base 类型
|
||||
- **WHEN** 开发者查看 `CheckerDefinition` 接口签名
|
||||
- **THEN** `resolve` 的参数 SHALL 为 `RawTargetConfig`,返回值 SHALL 为 `ResolvedTargetBase`;`execute` 的参数 SHALL 为 `ResolvedTargetBase`;`serialize` 的参数 SHALL 为 `ResolvedTargetBase`
|
||||
#### Scenario: 接口方法使用泛型约束
|
||||
- **WHEN** 开发者查看 `CheckerDefinition<TResolved>` 接口签名
|
||||
- **THEN** `resolve` 的返回值 SHALL 为 `TResolved`;`execute` 的参数 SHALL 为 `TResolved`;`serialize` 的参数 SHALL 为 `TResolved`
|
||||
|
||||
#### Scenario: checker 实现内部 narrow
|
||||
- **WHEN** HttpChecker 的 execute 方法接收 `ResolvedTargetBase` 参数
|
||||
- **THEN** 方法内部 SHALL 将参数 narrow 为 `ResolvedHttpTarget`(通过 type assertion),然后使用具体类型的字段
|
||||
#### Scenario: checker 实现无需手动断言
|
||||
- **WHEN** HttpChecker 实现 `CheckerDefinition<ResolvedHttpTarget>`
|
||||
- **THEN** `execute` 方法的 target 参数类型 SHALL 直接为 `ResolvedHttpTarget`,无需在方法内部使用 `as` 类型断言
|
||||
|
||||
#### Scenario: registry 使用默认泛型参数
|
||||
- **WHEN** CheckerRegistry 存储和返回 checker 实例
|
||||
- **THEN** registry 内部 SHALL 使用 `CheckerDefinition`(等价于 `CheckerDefinition<ResolvedTargetBase>`),实现类型擦除
|
||||
|
||||
### Requirement: CheckerRegistry 注册中心
|
||||
系统 SHALL 在 `src/server/checker/runner/registry.ts` 中提供 `CheckerRegistry` 类,支持 `register(checker)`、`get(type)` 和 `supportedTypes`。重复注册同一 type SHALL 抛出错误。
|
||||
系统 SHALL 在 `src/server/checker/runner/registry.ts` 中提供 `CheckerRegistry` 类,支持 `register(checker)`、`get(type)` 和 `supportedTypes`。重复注册同一 type SHALL 抛出错误。registry 内部 SHALL 存储 `CheckerDefinition`(使用默认泛型参数),对外提供类型擦除后的接口。
|
||||
|
||||
#### Scenario: 注册并获取 Checker
|
||||
- **WHEN** 调用 `registry.register(new HttpChecker())` 后再调用 `registry.get("http")`
|
||||
- **THEN** 返回的 SHALL 是之前注册的 HttpChecker 实例
|
||||
- **THEN** 返回的 SHALL 是之前注册的 HttpChecker 实例(类型为 `CheckerDefinition`)
|
||||
|
||||
#### Scenario: 获取未注册的 type
|
||||
- **WHEN** 调用 `registry.get("unknown")` 且未注册对应 type 的 checker
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
- **THEN** 系统 SHALL 返回与之前格式一致的健康检查响应
|
||||
|
||||
### Requirement: API 错误处理
|
||||
系统 SHALL 对不存在的目标 ID 和无效参数返回适当的 HTTP 错误响应。
|
||||
系统 SHALL 对不存在的目标 ID、无效参数和超出范围的分页参数返回适当的 HTTP 错误响应。
|
||||
|
||||
#### Scenario: 查询不存在的目标
|
||||
- **WHEN** 客户端请求 `GET /api/targets/999/history`
|
||||
@@ -104,6 +104,14 @@
|
||||
- **WHEN** 客户端请求 `GET /api/targets/1/history?from=ISO&to=ISO&page=abc`
|
||||
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
|
||||
|
||||
#### Scenario: pageSize 超过上限
|
||||
- **WHEN** 客户端请求 `GET /api/targets/1/history?from=ISO&to=ISO&pageSize=201`
|
||||
- **THEN** 系统 SHALL 返回 400 状态码和错误信息,提示 pageSize 不能超过 200
|
||||
|
||||
#### Scenario: pageSize 等于上限
|
||||
- **WHEN** 客户端请求 `GET /api/targets/1/history?from=ISO&to=ISO&pageSize=200`
|
||||
- **THEN** 系统 SHALL 正常返回数据
|
||||
|
||||
#### Scenario: from 或 to 参数缺失
|
||||
- **WHEN** 客户端请求 `GET /api/targets/1/trend` 未提供 from 或 to 参数
|
||||
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
|
||||
|
||||
@@ -291,3 +291,18 @@
|
||||
#### Scenario: retention 字段缺省
|
||||
- **WHEN** 配置文件中未指定 `runtime.retention`
|
||||
- **THEN** 系统 SHALL 使用默认值 `"7d"`
|
||||
|
||||
### Requirement: 数据目录路径解析
|
||||
配置加载流程 SHALL 将 `server.dataDir` 相对路径基于配置文件所在目录(configDir)解析为绝对路径。绝对路径 SHALL 保持不变。
|
||||
|
||||
#### Scenario: dataDir 为相对路径
|
||||
- **WHEN** 配置文件位于 `/opt/dial/probes.yaml`,且 `server.dataDir` 配置为 `./data`
|
||||
- **THEN** 系统 SHALL 将 dataDir 解析为 `/opt/dial/data`,而非依赖进程 cwd
|
||||
|
||||
#### Scenario: dataDir 为绝对路径
|
||||
- **WHEN** `server.dataDir` 配置为 `/var/lib/dial/data`
|
||||
- **THEN** 系统 SHALL 直接使用该绝对路径,不做额外解析
|
||||
|
||||
#### Scenario: dataDir 使用默认值
|
||||
- **WHEN** 未配置 `server.dataDir`(使用默认值 `./data`)
|
||||
- **THEN** 系统 SHALL 将默认值 `./data` 基于 configDir 解析为绝对路径
|
||||
|
||||
@@ -16,16 +16,24 @@
|
||||
- **THEN** 系统 SHALL 创建两个独立定时器,分别按各自频率调度
|
||||
|
||||
### Requirement: 组内并发拨测
|
||||
系统 SHALL 在每次调度 tick 时并发执行同组内目标的检查,但实际同时运行的检查数 MUST 受全局 `runtime.maxConcurrentChecks` 限制。
|
||||
系统 SHALL 在每次调度 tick 时并发执行同组内目标的检查,但实际同时运行的检查数 MUST 受全局 `runtime.maxConcurrentChecks` 限制。当某个目标的 checker 执行 rejected(非正常 CheckResult 返回,而是 Promise reject)时,系统 SHALL 将该异常记录为 `matched: false` 的 check_result,而非仅 console.warn。
|
||||
|
||||
#### Scenario: 同组目标并发执行
|
||||
- **WHEN** 调度器触发一次 tick,该组有 3 个目标,且全局并发余量至少为 3
|
||||
- **THEN** 系统 SHALL 同时执行 3 个 checker,而非顺序执行
|
||||
|
||||
#### Scenario: 单个目标失败不影响同组其他目标
|
||||
- **WHEN** 同组中某个目标的检查请求超时或失败
|
||||
- **WHEN** 同组中某个目标的检查请求超时或失败(checker 正常返回 CheckResult)
|
||||
- **THEN** 其他目标的检查 SHALL 正常完成并记录结果
|
||||
|
||||
#### Scenario: 同组中某个目标的 checker 执行 rejected
|
||||
- **WHEN** 同组中某个目标的 checker 执行抛出未捕获异常(Promise rejected)
|
||||
- **THEN** 系统 SHALL 为该目标写入一条 `matched: false` 的 check_result,failure 为 `{ kind: "error", phase: "internal", path: "engine", message: <rejected reason> }`,其他目标的检查 SHALL 不受影响
|
||||
|
||||
#### Scenario: rejected 结果通过索引关联 targetName
|
||||
- **WHEN** checker 执行 rejected
|
||||
- **THEN** 系统 SHALL 通过 Promise.allSettled 的索引关联回 target 数组,获取对应的 targetName 用于写入 check_result
|
||||
|
||||
#### Scenario: 全局并发限制生效
|
||||
- **WHEN** 调度器同时触发 10 个目标且 runtime.maxConcurrentChecks 为 3
|
||||
- **THEN** 系统 MUST 同时最多运行 3 个检查,其余检查等待并发槽位释放
|
||||
|
||||
42
openspec/specs/server-bootstrap/spec.md
Normal file
42
openspec/specs/server-bootstrap/spec.md
Normal file
@@ -0,0 +1,42 @@
|
||||
## Purpose
|
||||
|
||||
TBD - 统一服务启动引导函数,封装开发和生产模式的完整启动序列。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 统一启动引导函数
|
||||
系统 SHALL 提供 `src/server/bootstrap.ts` 导出 `bootstrap(options: BootstrapOptions)` 函数,封装完整的服务启动序列:加载配置、创建 store、同步 targets、创建并启动 engine、启动 HTTP server、注册 shutdown handler。
|
||||
|
||||
#### Scenario: 开发模式启动
|
||||
- **WHEN** `dev.ts` 调用 `bootstrap({ configPath, mode: "development" })`
|
||||
- **THEN** 系统 SHALL 完成完整启动序列,不传入 staticAssets
|
||||
|
||||
#### Scenario: 生产模式启动
|
||||
- **WHEN** build entry 调用 `bootstrap({ configPath, mode: "production", staticAssets })`
|
||||
- **THEN** 系统 SHALL 完成完整启动序列,并将 staticAssets 传递给 startServer
|
||||
|
||||
#### Scenario: 启动失败处理
|
||||
- **WHEN** 启动过程中任何步骤抛出异常
|
||||
- **THEN** 系统 SHALL 输出错误信息并以非零退出码退出进程
|
||||
|
||||
#### Scenario: 优雅关机
|
||||
- **WHEN** 进程收到 SIGINT 或 SIGTERM 信号
|
||||
- **THEN** bootstrap 注册的 shutdown handler SHALL 调用 engine.stop() 和 store.close() 后退出
|
||||
|
||||
### Requirement: BootstrapOptions 接口
|
||||
`bootstrap` 函数 SHALL 接受 `BootstrapOptions` 参数,包含 `configPath: string`、`mode: RuntimeMode`、`staticAssets?: StaticAssets`。
|
||||
|
||||
#### Scenario: 最小配置
|
||||
- **WHEN** 仅传入 configPath 和 mode
|
||||
- **THEN** 系统 SHALL 正常启动,staticAssets 为 undefined
|
||||
|
||||
### Requirement: dev.ts 和 build entry 使用 bootstrap
|
||||
`dev.ts` 和 `scripts/build.ts` 生成的 server entry SHALL 调用 `bootstrap()` 而非各自维护启动序列。
|
||||
|
||||
#### Scenario: dev.ts 调用 bootstrap
|
||||
- **WHEN** 开发者运行 `bun run dev:server`
|
||||
- **THEN** `dev.ts` SHALL 调用 `bootstrap` 完成启动
|
||||
|
||||
#### Scenario: build entry 调用 bootstrap
|
||||
- **WHEN** 生产可执行文件启动
|
||||
- **THEN** 生成的 entry SHALL 调用 `bootstrap` 完成启动
|
||||
Reference in New Issue
Block a user