From 63bc56be275eb8d1a728465506e849bba8d3bda8 Mon Sep 17 00:00:00 2001 From: youfak Date: Wed, 29 Jul 2026 18:01:37 +0800 Subject: [PATCH] docs: finalize redis activity pool delivery --- deploy/compose_test.go | 91 +++++++++++++++++++++++++ deploy/docker-compose.yml | 20 +++--- docs/adr/005-redis-activity-pool.md | 16 ++--- docs/development/implementation-plan.md | 25 ++++--- docs/operations/runbook.md | 9 ++- docs/requirements/traceability.md | 10 +-- docs/testing/strategy.md | 16 ++++- docs/testing/test-strategy.md | 12 ++++ progress.md | 14 ++-- task_plan.md | 6 +- 10 files changed, 179 insertions(+), 40 deletions(-) create mode 100644 deploy/compose_test.go diff --git a/deploy/compose_test.go b/deploy/compose_test.go new file mode 100644 index 0000000..41e061e --- /dev/null +++ b/deploy/compose_test.go @@ -0,0 +1,91 @@ +package deploy + +import ( + "os" + "testing" + + "go.yaml.in/yaml/v4" +) + +type composeDocument struct { + Services map[string]composeService `yaml:"services"` + Volumes map[string]any `yaml:"volumes"` +} + +type composeService struct { + Command []string `yaml:"command"` + Volumes []string `yaml:"volumes"` + DependsOn any `yaml:"depends_on"` +} + +func TestLocalRedisIsExplicitlyEphemeral(t *testing.T) { + document := loadComposeDocument(t) + redis, ok := document.Services["redis"] + if !ok { + t.Fatal("docker-compose.yml has no redis service") + } + if value, ok := commandFlag(redis.Command, "--appendonly"); !ok || value != "no" { + t.Fatalf("redis --appendonly = %q, %t; want no", value, ok) + } + if value, ok := commandFlag(redis.Command, "--save"); !ok || value != "" { + t.Fatalf("redis --save = %q, %t; want empty schedule", value, ok) + } + if len(redis.Volumes) != 0 { + t.Fatalf("redis volumes = %v; want no persistent mount", redis.Volumes) + } + if _, exists := document.Volumes["redis-data"]; exists { + t.Fatal("docker-compose.yml still declares redis-data") + } +} + +func TestLocalGatewaysDoNotDependOnControlPlaneStorage(t *testing.T) { + document := loadComposeDocument(t) + for _, name := range []string{"gateway-a", "gateway-b"} { + gateway, ok := document.Services[name] + if !ok { + t.Fatalf("docker-compose.yml has no %s service", name) + } + for _, storage := range []string{"postgres", "redis"} { + if composeDependsOn(gateway.DependsOn, storage) { + t.Errorf("%s depends on %s; gateway startup must be storage-independent", name, storage) + } + } + } +} + +func loadComposeDocument(t *testing.T) composeDocument { + t.Helper() + payload, err := os.ReadFile("docker-compose.yml") + if err != nil { + t.Fatalf("read docker-compose.yml: %v", err) + } + var document composeDocument + if err := yaml.Unmarshal(payload, &document); err != nil { + t.Fatalf("parse docker-compose.yml: %v", err) + } + return document +} + +func composeDependsOn(value any, service string) bool { + switch dependencies := value.(type) { + case map[string]any: + _, exists := dependencies[service] + return exists + case []any: + for _, dependency := range dependencies { + if dependency == service { + return true + } + } + } + return false +} + +func commandFlag(command []string, name string) (string, bool) { + for index := 0; index+1 < len(command); index++ { + if command[index] == name { + return command[index+1], true + } + } + return "", false +} diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 5d575dc..7171403 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -17,11 +17,6 @@ x-app: &app PROVIDER_A_TOKEN: ${PROVIDER_A_TOKEN:?set PROVIDER_A_TOKEN} PROVIDER_B_TOKEN: ${PROVIDER_B_TOKEN:?set PROVIDER_B_TOKEN} stop_grace_period: 45s - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy services: gateway-a: @@ -49,6 +44,11 @@ services: controller: <<: *app command: ["proxy-controller"] + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy expose: ["8081", "8082", "9090"] ports: - "127.0.0.1:8081:8081" @@ -63,6 +63,11 @@ services: checker: <<: *app command: ["proxy-checker"] + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy expose: ["9090"] healthcheck: test: ["CMD", "curl", "--fail", "--silent", "http://127.0.0.1:9090/readyz"] @@ -111,10 +116,8 @@ services: redis: image: redis:8.2-alpine restart: unless-stopped - command: ["redis-server", "--appendonly", "yes", "--save", "60", "1"] + command: ["redis-server", "--appendonly", "no", "--save", ""] networks: [backend] - volumes: - - redis-data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s @@ -160,6 +163,5 @@ networks: volumes: postgres-data: {} - redis-data: {} prometheus-data: {} grafana-data: {} diff --git a/docs/adr/005-redis-activity-pool.md b/docs/adr/005-redis-activity-pool.md index 0999e99..7d9089f 100644 --- a/docs/adr/005-redis-activity-pool.md +++ b/docs/adr/005-redis-activity-pool.md @@ -2,7 +2,7 @@ ## 状态 -接受,2026-07-29。 +接受并已实现,2026-07-29。 ## 背景 @@ -15,14 +15,14 @@ Redis 只承载控制面中的可重建短效状态:Proxy 活动池、健康 30 秒,因此数据结构必须支持高频刷新、有界清理和硬过期,不能把逐个 Proxy 或逐次提取记录写入 PostgreSQL。 -当前 `activitypool.MemoryPool` 已定义 Provider Upsert、Distribution Extract 和 -Worker Ownership 的参考语义,但生产 Redis 实现还存在以下缺口: +`activitypool.MemoryPool` 定义 Provider Upsert、Distribution Extract 和 Worker +Ownership 的参考语义,生产 Redis Adapter 已按同一套公用契约实现: -- `ownership.Repository` 缺少 `context.Context` 和存储错误返回值。 -- Provider 写入 `FETCHED` 后没有公用健康状态更新端口。 -- 本地 `FetchBudget` 不能作为多进程环境的最终库存权威。 -- 提取结果需要真实代理凭据,不能在 Redis 已提交消费后再执行可能失败的解析。 -- 短 TTL 条目不能依赖无上限全池扫描或长期数据库记录完成清理。 +- 所有远程存储端口接收 `context.Context` 并返回存储错误。 +- Provider Upsert、健康更新、提取、所有权和维护能力通过窄接口复用。 +- 原子 Lua 在提交前完成记录解码和候选校验,提取结果不依赖提交后的凭据解析。 +- 全局 ownership epoch 使用 Redis `INCR`,库存读取与过期清理均采用有界扫描。 +- 真实 Redis 8.2 fixture 覆盖并发提取、所有权竞争、幂等硬过期和键 TTL。 ## 决策 diff --git a/docs/development/implementation-plan.md b/docs/development/implementation-plan.md index 5ce4e30..2f680fd 100644 --- a/docs/development/implementation-plan.md +++ b/docs/development/implementation-plan.md @@ -127,7 +127,7 @@ test/{fixtures,integration,e2e,load}/ - [x] Enforce minRemainingTTL, maxHealthCheckAge, maxCount, client limits, and reserveForGateway. - [x] Atomically remove selected AVAILABLE entries from the allocatable set and return - the result; the current memory Store models the production Redis atomic boundary. + the result; MemoryPool and the production Redis Adapter run the same shared contract. - [x] Implement partial and allOrNothing without Lease, release, or renewal concepts. - [x] Run 1,000 concurrent claim attempts and prove every Proxy ID appears at most once. @@ -161,14 +161,18 @@ test/{fixtures,integration,e2e,load}/ - [ ] Define PostgreSQL ports for ConfigVersion, Upstream/Routing management state, AdminAudit, Outbox, and optional aggregate metrics; never persist Proxy details or per-extraction records. -- [ ] Implement the Redis TTL activity pool and one atomic extraction operation covering +- [x] Implement the Redis TTL activity pool and one atomic extraction operation covering candidate eligibility, Gateway reserve, ownership, removal, and short-lived idempotency. -- [ ] Implement Redis Provider leader, distributed rate, Client limit, and Worker - heartbeat/ownership; rebuild short-lived Proxy inventory from Providers after loss. -- [ ] Keep Provider output in Redis TTL activity state and node memory only; keep the +- [x] Implement Redis Worker ownership, drain/ACK, expiry reclaim, inventory and bounded + sweep primitives with a monotonic global epoch. +- [ ] Implement Redis Provider leader, distributed rate, Client limit and Worker + heartbeat; wire automatic Provider inventory rebuild after Redis loss. +- [x] Keep Provider output in Redis TTL activity state and node memory only; keep the Gateway request path on immutable local snapshots with no Redis/PostgreSQL calls. -- [ ] Expose Distribution extraction/status and Admin status/enable/disable/switch/reload. -- [ ] Add integration tests using Compose-backed PostgreSQL/Redis. +- [x] Expose Distribution extraction/status and Admin status/enable/disable/switch/reload + HTTP handlers and contracts. +- [x] Add Compose-backed Redis 8.2 integration and shared Adapter contract tests. +- [ ] Add PostgreSQL management Adapter and Compose-backed integration tests. 当前进度(2026-07-29):已实现共享 `platform/httpapi`、Distribution extract/live/ready Handler 与 Admin status/enable/disable/switch/reload Handler; @@ -177,14 +181,15 @@ extract/live/ready Handler 与 Admin status/enable/disable/switch/reload Handler Bearer/CIDR、可信代理、Client ID、本地准入和 API 401/Gateway 407 差异,并作为 Admin/Distribution 必需依赖。共享 `platform/httpserver` 与 `controller/runtime` 已完成 Distribution/Admin 独立监听器、首错联动关闭和 -有界优雅停机;端点正式勾选仍等待 Redis 活动池/原子提取 Adapter、PostgreSQL -管理面 Adapter、命令入口与 Compose 集成测试。 +有界优雅停机。生产命令入口、PostgreSQL 管理面 Adapter 及其 Compose 集成测试 +仍待实现。 已新增公用 `domain/activitypool` 契约及并发安全内存参考实现,Provider Reconciler 通过 `UpsertFetched` 写入带供应商 TTL 和分配安全余量的批次;已覆盖 `usableUntil` 向 Worker Snapshot 的传播与 Gateway 本地截止过滤、 重复刷新、过期淘汰、独占提取、短期幂等及 Worker ownership 互斥。生产 Redis -Lua/Function Adapter 和多节点集成测试仍待实现。 +Adapter 已通过真实 Redis 8.2 运行同一套公用契约;原子 Lua 覆盖提取、所有权和 +有界清理。Redis Sentinel/故障转移验证与代表性多节点压测仍待实施。 ## Task 11: Checker and Health Reducer diff --git a/docs/operations/runbook.md b/docs/operations/runbook.md index db4fba1..04a378b 100644 --- a/docs/operations/runbook.md +++ b/docs/operations/runbook.md @@ -48,6 +48,12 @@ docker compose -f deploy/docker-compose.yml config kubectl kustomize deploy/kubernetes/base > rendered.yaml ``` +本地 Compose 的 Redis 只作为可重建短效状态 fixture,固定使用 +`--appendonly no --save ""`,且不挂载 `/data` 或命名卷。真实 Redis 8.2 契约可 +通过 `.\scripts\test-redis.ps1` 执行;脚本使用唯一命名空间并在结束时定向清理, +不执行 `FLUSHDB`。生产环境的 Redis 高可用与持久化策略必须独立评审,不能照搬 +本地 fixture。 + 目标拓扑入口: - Gateway:`127.0.0.1:8080` @@ -243,7 +249,8 @@ Prometheus 标签禁止包含 Proxy IP、Client ID、Session、完整 URL、requ - PostgreSQL:每日全量、连续 WAL/PITR,保护配置版本、Upstream/Routing 管理 状态、Admin 审计与 outbox;至少每季度做恢复演练。 - Redis:保存可由 Provider 重建的 TTL 活动池、所有权/Leader 协调和短期幂等 - 结果;使用高可用与持久化降低窗口丢失风险,但不把它当长期业务档案。 + 结果;生产环境可使用高可用与受控持久化降低窗口丢失风险,但不把它当长期 + 业务档案或备份源。本地 Compose 刻意关闭持久化并且不挂载数据卷。 - 配置:版本化保存校验通过的不可变 Revision 与校验和。 - Secret:由密钥平台版本化,日志和备份中不得出现明文。 diff --git a/docs/requirements/traceability.md b/docs/requirements/traceability.md index aae1074..d1914f1 100644 --- a/docs/requirements/traceability.md +++ b/docs/requirements/traceability.md @@ -11,7 +11,7 @@ | ARCH-002 | 热路径只做认证、本地路由和网络转发 | 1-70, 380-430 | 依赖规则、测试、性能剖析 | | ARCH-003 | Gateway、Distribution、Admin、Metrics 独立入口 | 8904-8958 | 配置、监听装配、端口测试 | | ARCH-004 | Controller 集中 Provider 获取与切换 | 1403-1580 | Leader、singleflight、集成测试 | -| ARCH-005 | 100k QPS 峰值使用多 Worker 集群 | 当前会话 | 容量公式、负载场景、部署清单 | +| ARCH-005 | 100k QPS 峰值使用多 Worker 集群 | 当前会话 | 未验证设计目标;待代表性集群负载报告 | ## Routing 与 Upstream @@ -49,7 +49,7 @@ | CAP-002 | 补池依据 Available Slots,不只看 Proxy 数量 | 1203-1402, 8530-8597 | `Inventory.AvailableSlots` 与 Pool Reconciler 测试 | | CAP-003 | pool.maxSize 包括 FETCHED/CHECKING/AVAILABLE/SUSPECT/DRAINING 与 pending expected | 3001-3533, 6642-6680 | `FetchBudget` 100 并发额度预占测试 | | CAP-004 | TTL safety margin 内禁止新分配 | 173-220, 6728-6741 | 时钟测试 | -| CAP-005 | 多 Worker 不在热路径访问 Redis 计数 | 1403-1467 | 依赖审计与压测 | +| CAP-005 | 多 Worker 不在热路径访问 Redis 计数 | 1403-1467 | Gateway 包依赖审计、Snapshot/Dispatch 测试 | ## Gateway @@ -66,9 +66,9 @@ | ID | 最终需求 | 来源 | 验证证据 | |---|---|---|---| | DIST-001 | API 提取固定为一次性独占发放,不使用 Lease | 9083-9404 | Domain 状态机与 API 测试 | -| DIST-002 | AVAILABLE -> EXTRACTED 必须原子完成后才能返回 | 9083-9189 | 共享 Repository 所有权/提取 100 轮竞态测试 | +| DIST-002 | AVAILABLE -> EXTRACTED 必须原子完成后才能返回 | 9083-9189 | Memory/Redis 公用契约与真实 Redis 100 轮竞态测试 | | DIST-003 | 支持 partial 与 allOrNothing,默认 partial | 9190-9215 | API 契约测试 | -| DIST-004 | 保存审计记录,不提供释放接口 | 9216-9252 | 原子审计、幂等测试与 OpenAPI | +| DIST-004 | 不保存逐代理/逐次提取审计记录,不提供释放接口;仅保留短期幂等结果 | 9216-9252 | Redis 幂等 TTL 契约、OpenAPI 与 PostgreSQL 边界审计 | | DIST-005 | 返回 expiresAt 与 remainingTtlSeconds | 9334-9360 | `extraction/service_test.go` | | DIST-006 | 提取前校验 minRemainingTTL 与 maxHealthCheckAge | 9334-9369 | 过滤测试 | | DIST-007 | reserveForGateway 防止 Extract 清空共享池 | 9281-9333 | 共享池测试 | @@ -86,4 +86,4 @@ | OPS-001 | 配置校验后构建不可变快照并原子替换 | 8959-8999 | 100k 索引、版本/epoch 与并发 Apply/Acquire 测试 | | OPS-002 | 优雅停机停止新请求/Fetch,等待现有流量后超时关闭 | 8981-9000 | Provider Run 收敛与 `Handler.Shutdown` HTTP 排空、Hijacked CONNECT 超时关闭测试 | | OBS-001 | 指标禁止 Proxy IP、session、Client、完整 URL 高基数标签 | 9001-9029 | 指标描述符测试 | -| TEST-001 | 覆盖对话中列出的 11 个关键并发与故障场景 | 9030-9082 | CI 测试清单 | +| TEST-001 | 覆盖对话中列出的 11 个关键并发与故障场景 | 9030-9082 | 测试清单;Redis 活动池由 Memory/Redis 公用契约覆盖,跨进程故障场景仍按清单推进 | diff --git a/docs/testing/strategy.md b/docs/testing/strategy.md index 728084a..917064d 100644 --- a/docs/testing/strategy.md +++ b/docs/testing/strategy.md @@ -1,10 +1,15 @@ # 测试策略 +> 本文保留为早期领域与容量检查清单。存储边界和当前执行命令以 +> `test-strategy.md`、ADR-005 为准:Proxy 明细与逐次提取记录不写 PostgreSQL, +> 独占提取只在 Redis TTL 活动池保留短期幂等结果。 + ## 1. 分层 - **领域单测**:状态机、TTL、路由、容量、Fetch 分类和 Extraction 原子性。 - **契约测试**:配置、OpenAPI、Protobuf 和 Provider Adapter fixture。 -- **集成测试**:PostgreSQL 事务、Redis Leader/限流、Outbox 与重建。 +- **集成测试**:Redis 活动池原子契约、PostgreSQL 管理事务、Leader/限流、 + Outbox 与重建。 - **端到端测试**:HTTP、CONNECT、Admin、Distribution 和优雅停机。 - **负载测试**:Worker 调度微基准、50k 隧道 soak、集群 100k QPS 场景。 @@ -32,6 +37,15 @@ go test -race ./internal/... go build ./... ``` +真实 Redis 8.2 活动池契约使用独立 Compose fixture: + +```powershell +.\scripts\test-redis.ps1 +``` + +该 fixture 使用唯一命名空间,不执行 `FLUSHDB`,并关闭 AOF、RDB 与数据卷; +测试结束后按命名空间清理活动池、所有权和幂等键。 + 单条测试命令超时 60 秒。依赖真实等待的用例必须改为 fake clock;集成和 soak 测试单独标记,不混入快速单测。 diff --git a/docs/testing/test-strategy.md b/docs/testing/test-strategy.md index ef6daf8..75ee495 100644 --- a/docs/testing/test-strategy.md +++ b/docs/testing/test-strategy.md @@ -76,6 +76,18 @@ go vet ./... go build ./... ``` +Redis 活动池 Adapter 与内存参考实现共享同一套公用行为契约。真实 Redis 8.2 +fixture 的执行命令是: + +```powershell +.\scripts\test-redis.ps1 +``` + +契约覆盖 Upsert/去重/容量、健康更新、partial/allOrNothing 提取、Gateway 保留、 +零数量与非零数量幂等、幂等硬过期、Worker 所有权/Drain/ACK、库存、过期清理、 +100 轮并发提取和 100 轮所有权竞争。fixture 使用唯一命名空间,不执行 +`FLUSHDB`;本地 Redis 关闭 AOF、RDB 和数据卷,避免短效 Proxy 与凭据落盘。 + 需要 PostgreSQL/Redis 的测试使用独立实例和短生命周期容器,不复用开发数据。 测试结束后验证没有残留 Worker ownership、Leader 租约、活动池条目或幂等键, 并检查 PostgreSQL 中不存在 Proxy 明细和逐次提取记录。 diff --git a/progress.md b/progress.md index 4db3730..da140f8 100644 --- a/progress.md +++ b/progress.md @@ -10,12 +10,19 @@ `LoadResolved` 回归测试。 - 已新增公用 `activitypool` 契约和并发安全内存参考实现;Provider 按各供应商 TTL 与安全余量写入,独占提取、短期幂等和 Worker 所有权在同一原子边界内。 +- 已实现生产 Redis Activity Adapter:Upsert、健康更新、原子提取、所有权、 + Drain/ACK、库存读取与有界过期清理均封装为窄领域端口和 Lua 原子操作。 +- MemoryPool 与 Redis Adapter 运行同一套公用契约;真实 Redis 8.2 fixture 已覆盖 + 100 轮并发提取和 100 轮所有权竞争,本地 Compose 不持久化短效代理数据。 - Proxy 明细不写 PostgreSQL;PostgreSQL 仅保存配置版本、管理状态、Admin 审计、管理 outbox 和可选聚合指标。 - `usableUntil` 已进入 Worker Snapshot 契约,Gateway 在供应商硬过期前按安全 余量停止新分配。 -- 生产 Redis Adapter、PostgreSQL 管理面 Adapter、生产命令入口与代表性 - 100,000 QPS 集群压测仍待实现。 +- PostgreSQL 管理面 Adapter、Provider Leader/分布式限流与心跳装配、生产命令 + 入口、Redis 故障转移验证和代表性 100,000 QPS 集群压测仍待实现。 +- 本轮 `.\scripts\verify.ps1`、`.\scripts\test-redis.ps1`、Compose 静态展开和 + Compose 非持久化策略测试通过;Windows `CGO_ENABLED=0`,race 继续由 Linux + CI 执行。 ## 2026-07-28 @@ -36,8 +43,7 @@ - Windows 当前 `CGO_ENABLED=0` 且无 C 编译器,race 测试由 Linux CI 承担。 - 100,000 QPS 仍是未验证设计目标;运行进程、存储适配器、完整网络转发与 代表性集群压测尚未实施,已在完成审计中明确列出。 -- 已生成 `proxy-pool-docs-v1.0.zip`,包含 50 个条目,SHA-256 为 - `A6882B71196210CE3594A10992C2A0A73EA3A1CF31D8A570709CA999133CE104`。 +- 历史文档压缩包已由用户删除,当前交付以仓库内可追踪文档为准。 - 已实现 random、roundRobin、weighted、leastConnections,并将 Sequential 拆成共享 Upstream 空结果状态与每 Routing 版本化游标。 - 已实现 Provider 合并通知、完整 attempt 超时、永久错误契约、指数退避、 diff --git a/task_plan.md b/task_plan.md index b6ec75c..5753c7c 100644 --- a/task_plan.md +++ b/task_plan.md @@ -26,6 +26,8 @@ 8. [已完成] 按需求矩阵逐项审计并生成版本化文档包 9. [已完成] 将 Proxy 明细、独占提取、短期幂等和 Worker 所有权统一到 TTL 活动池契约;PostgreSQL 退出代理数据路径 +10. [已完成] 实现生产 Redis Activity Adapter、原子 Lua、公用行为契约和 + Redis 8.2 集成 fixture;本地 Redis 禁止短效代理数据持久化 ## 串并行关系 @@ -46,5 +48,5 @@ - Docker Compose 配置与 Kubernetes Kustomize 已完成静态渲染验证;未启动 目标运行拓扑。 -- `cmd/proxy-*`、生产 Redis 活动池 Adapter、PostgreSQL 管理面 Adapter 与 - Checker 运行时属于后续实施范围,见完成审计。 +- `cmd/proxy-*`、PostgreSQL 管理面 Adapter、Provider Leader/分布式限流、 + Checker 运行时、Redis 故障转移验证与代表性集群压测属于后续实施范围。