docs: record distributed admission delivery
This commit is contained in:
parent
e54fc84a81
commit
c3b5b25597
@ -154,7 +154,7 @@ Redis Adapter 应通过单个 Lua 脚本、Redis Function 或等价的原子原
|
||||
- 两个并发成功响应的 Proxy ID 集合交集为空。
|
||||
- 原子操作失败时整个批次不返回,也不得留下部分移除结果。
|
||||
- `allOrNothing` 不足时零个条目退出活动池。
|
||||
- Redis 活动池不可用时返回 503,不以内存副本冒充成功。
|
||||
- Redis 活动池或分布式限流不可用时返回 503,不以内存副本冒充成功。
|
||||
- PostgreSQL 不可用不阻断提取;需要 PostgreSQL 的 Admin 管理写入单独降级。
|
||||
|
||||
## 7. 幂等
|
||||
@ -175,7 +175,9 @@ TTL 到期或 Redis 数据丢失后不再保证旧 Key 去重,系统不回退
|
||||
|
||||
- 直连请求使用来源 IP 形成匿名 Client。
|
||||
- 只有来源属于 `trustedProxies` 时才接受转发头。
|
||||
- 全局和每 Client 限流在查询库存前执行。
|
||||
- 全局和每 Client 限流在查询库存前执行;本地监听器先做早期拒绝,Redis 使用
|
||||
服务端时间和单个 Lua 原子操作执行跨 Controller 副本的权威额度。
|
||||
- Client 身份进入 Redis 前转换为定长摘要,窗口切换时原子删除上一窗口字段。
|
||||
- 过滤条件、数量、请求体和 Header 都有长度/数量上限。
|
||||
|
||||
## 9. 错误模型
|
||||
@ -190,7 +192,7 @@ TTL 到期或 Redis 数据丢失后不再保证旧 Key 去重,系统不回退
|
||||
- `415`:请求体不是 `application/json`。
|
||||
- `422`:数量、枚举或过滤组合违反业务约束。
|
||||
- `429`:全局或 Client 速率限制,响应 `Retry-After`。
|
||||
- `503`:Redis 活动池不可用、原子提取不可执行或服务正在排空。
|
||||
- `503`:Redis 活动池/分布式限流不可用、原子提取不可执行或服务正在排空。
|
||||
- `500`:未分类的内部错误;响应不包含底层错误文本。
|
||||
|
||||
错误响应不得包含 Provider Secret、Proxy 凭据、SQL 或内部拓扑。
|
||||
|
||||
@ -104,6 +104,10 @@ limits:
|
||||
`trustedProxies` 只决定何时接受 `Forwarded` 或 `X-Forwarded-For`,不能替代
|
||||
`allowCIDRs`。来自非可信代理的转发头必须忽略。
|
||||
|
||||
`requestsPerMinute` 和 `requestsPerMinutePerClient` 为非负整数,最大值为
|
||||
`2^53-1`;Distribution 的非零额度由 Redis Lua 计数,因此配置校验统一限制在
|
||||
Lua 可精确表示的整数范围内。
|
||||
|
||||
### 3.1 认证模式
|
||||
|
||||
- `none`:无身份认证,访问控制与限流仍生效。
|
||||
|
||||
@ -174,8 +174,9 @@ Upstream、`endBehavior` 默认 `stop`,并覆盖列表末端停止;disabled
|
||||
candidate eligibility, Gateway reserve, ownership, removal, and short-lived idempotency.
|
||||
- [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] Implement Redis Provider leader, distributed request quota, Client limit and
|
||||
automatic Provider inventory rebuild after Redis state loss.
|
||||
- [ ] Implement the Worker heartbeat receiving path and session lifecycle.
|
||||
- [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.
|
||||
- [x] Expose Distribution extraction/status and Admin status/enable/disable/switch/reload
|
||||
@ -236,9 +237,13 @@ Redis Provider Permit 现已把 requestInterval、maxInFlight 与 maxTotal 放
|
||||
启动后动态扩容。Supervisor 以 PostgreSQL 权威 HMAC 指纹和 revision 栅栏协调
|
||||
多副本 reload:管理库瞬断沿用 last-known 状态,本地共享源落后时停止旧 Provider,
|
||||
源匹配并预检后自动替换,迟到旧 revision 不覆盖新配置。
|
||||
Distribution 现通过公用 `admission.Admitter` 接入独立 `redisadmission` Adapter;
|
||||
全局和单 Client 分钟额度使用 Redis 服务端时间并在单个 Lua 原子边界内检查、递增,
|
||||
Controller 多副本共享同一计数。Client 身份只以 SHA-256 摘要进入 Redis,窗口切换
|
||||
原子回收历史字段;Redis 异常 fail-closed 并返回 503,真实额度耗尽返回 429。
|
||||
Gateway 请求热路径仍只使用本地准入,不增加 Redis/PostgreSQL 调用。
|
||||
WorkerControlPlane gRPC 接收端、session 签发/心跳、Snapshot ACK
|
||||
账本、Client 分布式限流和健康执行链仍待完成,因此本轮不勾选 Task 10 的组合
|
||||
验收项。
|
||||
账本和健康执行链仍待完成,因此 Task 10 尚未全部完成。
|
||||
|
||||
## Task 11: Checker and Health Reducer
|
||||
|
||||
|
||||
@ -240,7 +240,7 @@ Outbox 发布器必须以稳定 consumer ID 有界领取;发布成功后原子
|
||||
回查 PostgreSQL 或 Provider。
|
||||
2. Distribution 立即失败关闭并返回 503,禁止本地内存提取或 PostgreSQL 兜底。
|
||||
3. Controller 停止活动池写入和需要分布式互斥的工作,防止多个 Fetch Leader;
|
||||
本地限流不能声称满足全局额度。
|
||||
Distribution 的 Redis 权威限流同时失败关闭,本地早期限流不冒充跨副本额度。
|
||||
4. 恢复后确认 Leader 唯一和租约 epoch 单调,由 Provider 重新 Fetch 并构建 TTL
|
||||
活动池,再恢复 Distribution。Redis 整体丢失会终止原活动池代次的排他状态和
|
||||
短期幂等窗口;高可用、持久化、监控和告警必须明确并降低该风险。
|
||||
|
||||
@ -84,11 +84,11 @@ CI 已配置 Linux race job。PostgreSQL 18 和 Redis 8.2 的隔离 Adapter fixt
|
||||
4. Controller 的 PostgreSQL 连接池、迁移和 pgx Adapter 启动装配已完成;
|
||||
公用 bootstrap 已通过 PostgreSQL 18 + Redis 8.2 双存储集成,Controller
|
||||
三监听器与探针集成已完成;可选聚合指标和完整容器进程部署验证仍待实现。
|
||||
5. Redis Provider Leader、分布式速率与 Client 限制、Worker 心跳和自动重建;
|
||||
TTL 活动池、原子提取和 Worker ownership 已完成。
|
||||
5. Worker heartbeat gRPC 接收路径;Redis Provider Leader、分布式请求额度、
|
||||
Distribution Client 限制和 Provider 状态丢失重建已完成。
|
||||
6. Worker 网络快照流;Redis ownership drain/ACK/过期回收已完成。
|
||||
7. Checker 调度、探测器和健康 reducer。
|
||||
8. Admin/Distribution 细粒度授权、分布式限流和审计查询。
|
||||
8. Admin/Distribution 细粒度授权和审计查询;Distribution 分布式限流已完成。
|
||||
9. 真实 Compose/Kubernetes 集成、故障演练和代表性集群负载测试。
|
||||
10. 将五种 Routing 策略和 `onUnavailable` 接入 Gateway/Distribution 运行链,
|
||||
补齐 Sequential 持久化恢复、跨实例 CAS 和 disabled candidate 语义。
|
||||
|
||||
@ -72,7 +72,7 @@
|
||||
| DIST-005 | 返回 expiresAt 与 remainingTtlSeconds | 9334-9360 | `extraction/service_test.go` |
|
||||
| DIST-006 | 提取前校验 minRemainingTTL 与 maxHealthCheckAge | 9334-9369 | 过滤测试 |
|
||||
| DIST-007 | reserveForGateway 防止 Extract 清空共享池 | 9281-9333 | 共享池测试 |
|
||||
| DIST-008 | 提取认证可关闭,关闭后仍有来源识别与全局限制 | 8112-8441 | 来源身份准入与 `FixedWindow` 并发测试 |
|
||||
| DIST-008 | 提取认证可关闭,关闭后仍有来源识别与全局限制 | 8112-8441 | 来源身份准入、`FixedWindow` 单元测试及 `redisadmission` 双实例/并发集成测试 |
|
||||
|
||||
## 健康、安全、运维与测试
|
||||
|
||||
|
||||
@ -30,7 +30,8 @@
|
||||
原子操作;并发与主从切换下不得部分提交。
|
||||
- PostgreSQL 配置版本、Upstream/Routing 管理状态、Admin 审计与 outbox 的事务
|
||||
更新及幂等重放;测试库断言不包含 Proxy 明细或逐次提取记录。
|
||||
- Redis TTL 活动池、Leader 租约、限流、短期幂等窗口和失联恢复。
|
||||
- Redis TTL 活动池、Leader 租约、Provider 请求额度、Distribution Client
|
||||
跨副本限流、短期幂等窗口和失联恢复。
|
||||
- Snapshot/Delta/ACK/Report 的版本与校验和兼容性。
|
||||
- OpenAPI 错误模型、认证矩阵、批量 fulfillment。
|
||||
- OpenAPI 本地引用闭合、operationId 唯一、响应集合和 security scheme 引用。
|
||||
|
||||
@ -428,6 +428,13 @@ func TestValidateRejectsInvalidConfigurationMatrix(t *testing.T) {
|
||||
},
|
||||
want: "requestsPerMinute",
|
||||
},
|
||||
{
|
||||
name: "listener request limit exceeds exact counter range",
|
||||
mutate: func(cfg *Config) {
|
||||
cfg.Distribution.Limits.RequestsPerMinutePerClient = int(MaximumExactCounter) + 1
|
||||
},
|
||||
want: "requestsPerMinutePerClient",
|
||||
},
|
||||
{
|
||||
name: "invalid client identification mode",
|
||||
mutate: func(cfg *Config) {
|
||||
|
||||
@ -122,6 +122,9 @@ func validateListener(name string, listener Listener, security Security) error {
|
||||
if limit.value < 0 {
|
||||
return fmt.Errorf("validate %s limits.%s: must be non-negative", name, limit.name)
|
||||
}
|
||||
if int64(limit.value) > MaximumExactCounter {
|
||||
return fmt.Errorf("validate %s limits.%s: exceeds exact counter range", name, limit.name)
|
||||
}
|
||||
}
|
||||
host, err := validateListenAddress(name, listener.Listen)
|
||||
if err != nil {
|
||||
|
||||
@ -111,6 +111,9 @@ func (s *Service) Extract(ctx context.Context, request Request) (Response, error
|
||||
return response, ErrInvalidFulfillment
|
||||
}
|
||||
if err := s.admission.Admit(ctx, admissionKey(request)); err != nil {
|
||||
if errors.Is(err, admission.ErrUnavailable) {
|
||||
return response, errors.Join(ErrUnavailable, err)
|
||||
}
|
||||
return response, errors.Join(ErrAdmissionRejected, err)
|
||||
}
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"proxy-pool/internal/domain/activitypool"
|
||||
domain "proxy-pool/internal/domain/extraction"
|
||||
proxyDomain "proxy-pool/internal/domain/proxy"
|
||||
platformAdmission "proxy-pool/internal/platform/admission"
|
||||
)
|
||||
|
||||
func TestServiceAppliesPolicyAndBuildsResponse(t *testing.T) {
|
||||
@ -122,6 +123,30 @@ func TestServiceAppliesAdmissionBeforeStoreUsingStableIdentity(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceMapsUnavailableAdmissionToServiceUnavailable(t *testing.T) {
|
||||
store := &recordingStore{}
|
||||
admitter := &recordingAdmission{err: platformAdmission.ErrUnavailable}
|
||||
service, err := NewService(store, Policy{
|
||||
MaxCountPerRequest: 1,
|
||||
DefaultFulfillment: domain.Partial,
|
||||
}, admitter, time.Now)
|
||||
if err != nil {
|
||||
t.Fatalf("NewService(): %v", err)
|
||||
}
|
||||
|
||||
_, err = service.Extract(context.Background(), Request{
|
||||
RequestID: "req-1",
|
||||
ClientID: "client-1",
|
||||
Count: 1,
|
||||
})
|
||||
if !errors.Is(err, ErrUnavailable) || errors.Is(err, ErrAdmissionRejected) {
|
||||
t.Fatalf("Extract() error = %v, want only ErrUnavailable", err)
|
||||
}
|
||||
if store.calls != 0 {
|
||||
t.Fatalf("store calls = %d, want 0", store.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceUsesSourceIdentityForEphemeralIdempotency(t *testing.T) {
|
||||
store := &recordingStore{}
|
||||
service, err := NewService(store, Policy{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user