跳转至

Lambda Concurrency Decisions — 设计决策与调查方法论

状态: 决策快照 + 调查方法论(post PR #639 ContactsAnalyzer cron enable verification 调查产出) 日期: 2026-04-30 关联 Issue: callytics-infrastructure #691 关联 PR: #639 (cron enable, merged 2026-04-29) 范围: ContactsAnalyzer / TranscribeProcessor / AiAnalysisProcessor 三个 Lambda 的 reservedConcurrency 设计决策、历史演变、实测容量、扩容 baseline 读者: 半年后回来想知道"为什么 contacts-analyzer 是 2"的人 / 想做类似容量调查的工程师 / 接手扩 5→10 决策的人


What-We-Did — 这次调查的真实步骤

PR #639 把 ContactsAnalyzer 的 EventBridge daily cron 从 DISABLED 翻成 ENABLED,2026-04-29 01:28 UTC merge,之后 32 小时内 cron 跑了两次(04-29 + 04-30 各一次 06:00 UTC)。最初的 verification 看完 CloudWatch 数据后写进 issue #691,标了两个 hidden issue,其中 hidden issue #1 是 "reservedConcurrency=2 是 cron drain 时间瓶颈,prod rollout 前应评估扩容"。这份 doc 记录的是接下来对 hidden issue #1 的深挖。

调查 entry point 是一个 frame 问题:"调高 reservedConcurrency 会不会破坏其他服务?" 用户(Max)直觉里这个数字跟 RingCentral / OneRouter quota 有关,改它牵一发动全身。第一轮我(AI)用 callytics-impact-check skill 给了一份 impact report,标 OneRouter quota 为 🔴 breaking risk,说必须先做"方案 D"(给 ai-analysis-processor 补 reservedConcurrency 关历史 hole)。我同时 cross-validate 给 Codex + Gemini 看,两家都同意 "D 是 prerequisite"。

但 Max push back 了一句关键的:"为什么和我记得的不一样,我们要 call ringcentral api,it has rate limit too. if we have too many lambdas working at one time, we will have issues." 这一脚把我的 frame 踢碎了。我开始走 deep-research skill,做了三件事:读 RingCentral 官方文档拿到具体数字、grep codebase 看代码注释里历史决策、跑 CloudWatch 精确 filter 看真实 429 行为。结果三方面都跟我之前的 impact report 矛盾。

接着进 first-principles 做 frame audit,确认了 "lower bound = limiting factor" 这个泛化 frame 对 contacts-analyzer 不适用 — 因为 RC 跟 OneRouter 在不同资源池,contacts-analyzer 的 cron 路径根本不调 RC。Evidence 够 ship test 不够 ship prod。Max 接着说 "send more, like 30",我跑了 burst 实测(165 个并发请求 OneRouter,0 个 429)。这一步把 OneRouter quota 从"未知"变"实证健康到 30+",所有后续判断都站住了。

最后两轮 push back 还修正了两个 misleading 表述。一是我说 "ai-analysis-processor 没设 reservedConcurrency" 让 Max 误以为它完全不限,实际它有 SQS event source 的 maxConcurrency=2 间接限速,只是用了不同 AWS feature。二是 Max 问 "can you check git history and changelog to see why we have this rule?",git blame 出 6 个 commit 揭示 reservedConcurrency=2 不是 design choice,是一次 35 条 DLQ 危机逼出来的硬数字(2025-11-23 commit 201b2887),从 unlimited → 5 → 15 → 2 三次迭代才稳定。

整个调查 5 个 skill 串起来:callytics-impact-check(被推翻) → deep-research(找证据) → first-principles(frame audit) → empirical burst test(实测推翻) → crystallize(本文)。最有价值的不是"扩 2→5"这个结论,是这条 frame correction trail


What-We-Researched — 证据来源

RingCentral 官方 quota(已知 hard cap)

RingCentral API Rate Limits 的官方数字:

Light: 50 req/user/min; Medium: 40 req/user/min; Heavy: 10 req/user/min; Auth: 5 req/user/min。所有 tier 60s penalty。

Call Log Read + Recording Download 都属 Heavy tier。这是已经在 codebase 里 hard-coded 的:lib/stacks/lambda-stack.ts:93 注释 "SAFE: RingCentral limit ~10 API calls/min, each execution = 2-3 calls" 直接对应 Heavy tier 数字。

OneRouter quota(实测得出,没有公开文档)

llm.onerouter.pro 的所有者其实是 Infron AI(网站标题 "Infron AI — The #1 Unified AI Gateway"),不是公开 LLM 网关 OpenRouter。Infron 网站只说 "Scale infinitely with flexible capacity and custom rate limits",WebSearch 公开找不到 RPM/TPM 数字。

实测路径:本机直连 burst test。脚本 /tmp/onerouter-burst.py,目标 https://llm.onerouter.pro/v1/chat/completions,模型 x-ai/grok-4.1-fast-reasoning(production 用的同款)。第一次 burst 5 个全部 HTTP 403 Cloudflare error 1010 — 是 CF bot detection。加上 User-Agent: openai-python/1.30.0 后正常。

Burst N Prompt Success 429 5xx p95 latency
5 10 token 5/5 0 0 1.96s
10 10 token 10/10 0 0 1.99s
20 10 token 20/20 0 0 3.38s
30 10 token 30/30 0 0 2.58s
50 10 token 50/50 0 0 3.64s
75 10 token 75/75 0 0 2.74s
100 10 token 100/100 0 0 3.63s
5 ~3.6KB realistic 5/5 0 0 5.53s
15 ~3.6KB realistic 15/15 0 0 5.18s
30 ~3.6KB realistic 30/30 0 0 6.12s

总 165 个并发请求,0 个 429,0 个 5xx。OneRouter 在 ≥30 并发(realistic prompt 尺寸)下健康。我们当前 contacts-analyzer 4 in-flight peak 是它真实容量的 <14%

Git blame — 6 个关键 commit

为什么是 2,不是 design choice 是 incident driven。完整时间线:

日期 Commit 做了什么 当时的理由
2025-11-05 b4d8a26f maxwang 初始创建 transcribe-processor + ai-analysis-processor 都没设 reservedConcurrency
2025-11-23 201b2887 maxwang transcribe-processor: 设 reservedConcurrency=2 35 条 DLQ 在堆积!35 concurrent × 2.5 RC calls = 87.5 calls/min 远超 RC 10/min 上限。从 unlimited → 5 → 15 → 2 三次迭代才稳定
2025-11-24 20242e5e maxwang reconciliation-worker 创建时直接设 reservedConcurrency=2 注释 "CRITICAL: Prevent RingCentral 429 rate limit storms",吸取教训
2025-11-28 088a4f5d maxwang transcribe-processor 加 SQS maxConcurrency=2 CloudFormation 报错 "MaximumConcurrency: 3 is greater than Function Reserved Concurrency: 2"平台 hard rule,SQS maxConcurrency 必须 ≤ Lambda reservedConcurrency
2025-12-26 94ac099e P (Peter) ai-analysis-processor: 删除 reservedConcurrency,加注释 "Bedrock has no strict rate limits" 当时模型是 Bedrock,认为配额够大
2026-03-19 2f222fc9 + 48972169 P + Max contacts-analyzer 创建,reservedConcurrency=2 + SQS maxConcurrency=2 双保险 OneRouter,沿用 transcribe pattern
2026-04-21 infra#618 Max contacts-analyzer 79 条 DLQ — 加 retry + 150s timeout(没改 concurrency) OneRouter 60s timeout + 0 retry

Commit 201b2887 PR description 里写明数学:2 executions × 2.5 calls/exec = 5 calls/min,RC limit 10/min,50% safety margin。这是有数据支撑的硬约束

但 contacts-analyzer 的 reservedConcurrency=2 没有等价的数据 trail — 没有 design doc / RFC / commit message 解释数字怎么来的。看 2f222fc9 commit message,是新 Lambda 创建时 conservative 沿用 transcribe pattern,不是基于 OneRouter 实测。

CloudWatch 真实 retry / 429 行为(精确 filter)

第一轮我用粗 filter "429" 拿到 contacts-analyzer 5 events / 7 days,但 sample 显示是 "Wrote analysis with task decisions" 这种 log 里恰好出现 429 字符串的误判。换精确 filter 后真实数字:

Lambda "Retrying" "exhausted" "OneRouter API timeout" 04-30 cron 窗(8h)
contacts-analyzer 5 1 0 0 / 0 / 0
ai-analysis-processor 0 2 0 0 / 0 / 0
transcribe-processor n/a (RC 不是 OneRouter) n/a n/a clone-and-redrive 0(RC 也没 429)

contacts-analyzer 唯一 1 次 exhausted = 2026-04-27 22:44,在 cron enable 之前 2 天。04-29 cron enable 之后两次 cron run 全部一次性成功。

RC 端 7 天数字(transcribe-processor 7d invocations 15,870):92 次 clone-and-redrive,33 次 rate-limited log,46 次 Retry-After header — 错误率 0.58%。RC 是真热瓶颈,但 04-30 cron 窗内 0 次触发,说明 cron 流量没把 RC 推到 limit 以上。


Conclusions-Reached — 决策与判断

核心结论: contacts-analyzer 的 reservedConcurrency=2 是 conservative 沿用,不是 OneRouter 实测约束。今天实测 30+ 并发健康,扩到 5 是 reasoning over inertia,不是冒险。

四个组成判断:

(1) 三个 Lambda 的 reservedConcurrency 是 per-Lambda independent setting,不是共享池。改 contacts-analyzer 不影响 transcribe-processor 的 2,也不影响 ai-analysis-processor。AWS account-wide concurrency limit(default 1000)是唯一共享上限,我们当前 <10,远充足。

(2) ai-analysis-processor 的 "no limit" 是表述 misleading。它没设 Lambda 层 reservedConcurrency(commit 94ac099e 删的),但有 SQS event source maxConcurrency=2(lambda-stack.ts:731,commit 088a4f5d 加的)。两种限速作用一样,只是用不同 AWS feature 实现。当前 max=2 是有效约束。

历史包袱: ai-analysis-processor 的代码注释 "Bedrock has no strict rate limits" 现在 stale 了 — 已切到 OneRouter,但注释从 2025-12-26 起一直没更新。这是真 issue(将来加新触发源时容易踩坑),但不是 contacts-analyzer 扩容的 prerequisite。我之前判断它是"必须先补的 hole"是错的 — 它现在只有一条 SQS 触发源,SQS maxConcurrency 已经罩住。

(3) RC 不在 contacts-analyzer 的资源池里。contacts-analyzer 调 OneRouter + Neon,不调 RC。RC 错误率 0.58%(7d 92 次 clone-and-redrive)是真问题,但那是 transcribe-processor / message-processor 的问题,跟扩 contacts-analyzer 速度无关。"lower bound = limiting factor" 这个泛化 frame 在串行依赖链上才适用,而 EventBridge cron → contacts-analyzer 这条路径完全 bypass RC。

(4) 扩到 5 是安全的,扩到 10 也健康,15+ 进入 unobserved territory。理由:

2 → 5    OneRouter 实测 30+ 并发健康(safety margin >6×)
         SQS maxConcurrency 必须同步改到 5 (CloudFormation 平台约束)
         Cron drain time 从 3.5h → ~50min (削 76%)
         Per-call ❸ 路径不再被 cron starve

5 → 10   OneRouter 仍健康(safety margin >3×)
         Drain time → ~25min (再削 50%)
         开始要监控 Neon write contention + retry rate

10 → 15  技术 OK(实测 safety margin >2×)
         边际收益递减 (再削只省 17min)
         Neon ON CONFLICT 冲突频率上升(由 SQS FIFO MessageGroupId 部分缓解)

15+ → 30 实测范围内,但 Neon / SQS / 业务逻辑多个维度无 baseline
         不推荐除非有具体 prod traffic 推动

渐进式扩容的理由: 虽然 OneRouter 实测安全,但 Neon write contention / 数据 race condition / 业务语义 ON CONFLICT 在并发 >5 没 baseline 数据。先 2→5 观察一周(retry rate / Neon error / OneRouter latency),再决定要不要 5→10。


Open-Threads — 没解决的问题

ai-analysis-processor stale 注释(P3,不阻塞)。lambda-stack.ts:194 写的 "Bedrock has no strict rate limits" 已经过时,现在是 OneRouter。建议改成 "OneRouter limited via SQS event source maxConcurrency=2 — 见 line 731",顺手考虑加 Lambda 层 reservedConcurrency=2 作为防御性双保险(将来加 cron / on-demand / API 触发时不会绕过保护)。开独立 issue 追踪,不进 contacts-analyzer 扩容 PR。

Prod 流量推断未做。Test 环境 cron dispatch ~491 contact / day,prod 流量未知 — 可能 5-10× test。OneRouter 实测 30+ 并发健康,但那是同时 30 个 short prompt;prod 如果每天 5,000 contact 跑 cron 8h,total throughput 约束可能跟 burst 上限不同。建议: prod rollout 前用 prod 量级跑一次 dry-run(把 cron Lambda 的 AIANALYSISENABLED 临时设 false 看 dispatch 数字),或者在 test 环境分批 enqueue 模拟 5,000 burst。

Neon write contention 上限未测。reservedConcurrency 5 在 Neon HTTP API 层不会撞 connection cap(无 conn 限制),但 ON CONFLICT (phone, store_id) 在并发 >5 的实际 retry 率没数据。建议: 扩 2→5 后第一周监控 Neon 端 transaction retry / last_contact_analysis_at write conflict log,作为 5→10 的 baseline。

OneRouter cap 真上限不知。本次实测到 100 个并发短 prompt + 30 个 realistic prompt 全过,没找到真 cap。如果将来要扩超过 15,值得做更激进的 burst test(N=200/300)或直接联系 Infron AI 拿配额数字。

RC 错误率 0.58% 独立追踪。跟本 doc 不直接相关,但 7d 92 次 clone-and-redrive 说明 RC heavy tier 已经在 hot zone。如果 prod 流量翻倍,transcribe-processor / message-processor 才是要先评估扩容的对象 — 但那是独立工作,不是这次扩 contacts-analyzer 的副产品。


Next-Session-Entry-Point — 接手指南

接手扩 contacts-analyzer 2→5 的人: 改两行 lib/stacks/contacts-stack.ts(line 85 reservedConcurrentExecutions: 25,line 130 注释 maxConcurrency=2 上面 SqsEventSource 的实际值同步改 5),CHANGELOG 加一行 feat(contacts-analyzer): reservedConcurrency 2→5 based on burst test (165 concurrent OneRouter requests, 0 errors) — see lambda-concurrency-decisions.md。Commit message 引用本 doc + git history 6 commit timeline。CDK deploy 前跑 npm run cdk:diff 确认只动 contacts-analyzer,不 touch transcribe / ai-analysis。Deploy 后 1 周观察 OneRouter retry rate(logger.error Discord/Lark/Sentry alert)和 Neon transaction retry。

接手 ai-analysis-processor 注释 fix 的人: 这是独立 P3 issue,跟扩容 PR 不耦合。改 lambda-stack.ts:194 注释 + 考虑加 reservedConcurrentExecutions: 2 作双保险。SQS maxConcurrency=2 已经在 line 731,所以加 Lambda 层 reservedConcurrency=2 不改变现状,只是防御性写明 intent + 防将来加新触发源踩坑。

接手将来扩 5→10 决策的人: 第一步看本 doc Open-Threads 段的"Neon write contention 上限未测"— 有没有过去一周的 retry rate 数据。如果 contacts-analyzer 5→10 的扩容收益(drain time 削 25min)值得花 prod 量级 dry-run 验证,就做;不值得就停在 5。OneRouter 实测安全 margin 仍 >3×,不需要重测。

接手类似容量调查的工程师 — 调查方法论 5 步:

  1. Frame audit 优先: 不要被 plausible-sounding judgment 带偏。"X 改动会影响 Y" 这种声明,先验证 X 跟 Y 真的在同一资源池 / 串行链上。我第一轮 impact report 错判 OneRouter 是 breaking risk,根因是没区分 RC 池跟 OneRouter 池。
  2. Codebase 历史考古: git blame + git log commit message 比 design doc 更可信。reservedConcurrency=2 在 design doc 里完全没解释,但 commit 201b2887 的 PR description 写明 35 条 DLQ 危机 + 87.5 calls/min 算式 + 50% safety margin。Code-as-source-of-truth 不只是代码,是代码 + git 元数据。
  3. 官方文档拿 hard cap,实测拿 actual cap: RC 的 10/min 是官方文档 hard cap 不会变。OneRouter 的 cap Infron AI 不公开,只能 burst 实测。两种 cap 不能混用 — hard cap 不需要实测,actual cap 需要持续验证。
  4. Cross-CLI consult 是 second opinion 不是 verdict: Codex + Gemini 第一轮都被我误导(因为我给的 brief 里有错误前提),它们 confidently 说"D 是 prerequisite"。Max 的直觉 push back 才是真的 frame correction signal。AI consult 在 frame 错的时候只会强化错 frame。
  5. Burst test 早做不晚做: 165 个 OneRouter 请求 + 60s cooldown 一共耗时 ~10 分钟,$0.50 cost。这一步把 OneRouter quota 从"未知"变"实证健康",比之前 1 小时调研 + 跨 CLI consult 更 actionable。遇到 quota 未知的 capacity 问题,先 burst 实测,再讨论方案

Appendix — Frame Correction Trail

为什么这次调查值得记录,是因为 frame 改了 4 次。每次都是 Max 的 push back 触发。这条 trail 对将来类似调查有方法论价值。

Frame v1(我的): "调高 contacts-analyzer reservedConcurrency 会同时影响 RC 和 OneRouter pool" → 错误,把两个不相关资源池混了。

Frame v2(Max push back): "we call ringcentral api, it has rate limit too" → 修正了"OneRouter 是唯一约束"的错觉,但仍然把 RC 当 contacts-analyzer 直接相关因素。

Frame v3(deep-research 后): "RC 不在 contacts-analyzer 资源池,但 OneRouter quota 未知是 risk" → 修正了 RC 关系,但仍把 OneRouter quota 当 unknown 当 risk。

Frame v4(Max "send 30" + burst test): "OneRouter 实测 30+ 并发健康,RC 跟 contacts-analyzer 独立" → 完整修正,evidence-driven。

Frame v5(Max "ai-analysis has no limit, why we limit this?"): "ai-analysis-processor 的 no-Lambda-layer-limit 是 misleading 表述,它有 SQS 层 maxConcurrency=2 等价限速" → 修正了"ai-analysis 是 hole 必须先补"的错判。

每次 frame correction 都打掉了一个我之前 confidently 给的判断。最大的教训是 plausible-sounding 不等于 correct — Codex + Gemini cross-validate 在 frame 错的时候只会强化错 frame,不能当 verdict。Max 的直觉 push back("为什么和我记得的不一样")是最有价值的 frame audit signal,值得 trace 到底。


Update 2026-04-30 — PR #693 Implementation Outcome + Codex Review Cycle + Downstream Burst Verification

v1 doc 的判断("扩 2→5 安全,有 OneRouter 6× safety margin")在 v1 写的时候只测过 OneRouter 一条路径。本次 update 记录 PR #693 实施后的两件事:Codex adversarial review 抓到的 2 个 real issue + 2 个 challenge,以及为了 address 那 2 个 challenge 在 Neon ephemeral branch 上跑的端到端 burst test。结果是 v1 决策 evidence-完备地落地,merged 进 main。

Codex Review Cycle — 2 Real Issues Caught(post-PR-open 阶段)

提 PR #693 后立刻让 Codex 做 adversarial review(cat brief.md | codex exec,brief 包含完整 diff + body + context)。Codex 4 段返回:bugs / hidden assumptions / TDD gap / single biggest risk。它抓到了 2 个真问题,我修了。

第一个问题是注释逻辑颠倒。我在 lambda-stack.ts:194 写了 "Until now SQS event source maxConcurrency=5 was the only effective limiter" — 但 PR 改之前 maxConcurrency 实际是 2,改之后才是 5,"Until now" 这个时间锚点指 pre-PR 状态,所以应该是 "Until now SQS event source maxConcurrency=2 was the only effective limiter"。读注释的人会一头雾水,这是 documentation bug。修后:"Before this PR the SQS event source maxConcurrency=2 (now bumped to 5 alongside, see line ~754)",逻辑顺了。

第二个问题更严重,是真 TDD gap。我以为我加的 ai-analysis-processor 测试已经 cover SQS event source 的 maxConcurrency 改动,但 Codex 指出 lambda-stack-worker.test.ts:179"Event source mapping has maxConcurrency of 2" 测试用的是 .some(MaximumConcurrency === 2) pattern — 它只检查 "任何一个" SQS event source mapping 的 maxConcurrency 是 2 就 pass。本 repo 有 3 个 SQS event source mapping (transcribe-processor / reconciliation-worker / ai-analysis-processor),前两个仍是 2(它们调 RC,不动),所以就算我把 ai-analysis-processor 改回 2(回滚错了 / merge 冲突 / 手抖),这个测试仍 pass — false security。

修法是在 test/lambda-stack-xray.test.ts 加 anchored test:findResources('AWS::Lambda::EventSourceMapping')find(mapping => mapping.Properties.FunctionName.Ref.startsWith('ResultProcessorTS')) → assert ScalingConfig.MaximumConcurrency === 5。新测试精确指向 ai-analysis-processor 这一条 mapping,任何回滚都会被抓到。

我做了一次 retroactive 验证证明新测试真的能 catch:把 ai-analysis SQS maxConcurrency 临时改回 2 → 跑测试 → 新测试 fail (16/17 pass);改回 5 → 17/17 pass。Test 真锁住了行为。

剩下 2 个不是 issue 而是 challenge,Codex 自己也说 "noted, not code-fixed"。Challenge 1 是 PR body wording:"contacts-analyzer only calls OneRouter + Neon" 不准确,它还调 PhoneStoreAssignments DynamoDB(handler.ts:188-198 via resolvePhoneIdentity)。这个 wording 错误不影响"扩 5 安全"的结论 — DDB GetItem 在我们规模无 quota 压力 — 但措辞应该改正。Challenge 2 是 "OneRouter burst test 不能覆盖 prod end-to-end:真实路径还有 validation retries、Neon writes、DynamoDB lookup、SSM/Secrets cold path" — 5× concurrency 可能放大 Neon write contention 或 DDB throttle 或 retry traffic,burst test 只测了 OneRouter network capacity。

Codex 的 single biggest risk 总结说得很到位:"最大风险不是 OneRouter 429,而是下游端到端容量误判:5 并发会放大 Neon/DynamoDB writes/queries 和 retry traffic。若 Neon contention 或 AI validation retry 上升,SQS 会延长 drain、放大重试成本,并继续占满 reserved concurrency。" 这才是真正应该 verify 的。

Downstream Burst Test — Neon Ephemeral Branch Isolation

为了真正 close challenge 2,在 Neon callytics-test project 上创了 ephemeral branch concurrency-burst-test-2026-04-30(branch ID br-delicate-mud-akp0gxj2,copy-on-write fork 秒级,完整 7,330 contacts 数据),burst test 跑在 fork 上,跑完直接 delete branch — parent test 数据 0 影响。这是 CLAUDE.md 里 canonical 的 backfill / migration safety pattern,也适用于 capacity test。

Test 模拟 contacts-analyzer 实际写 pattern:UPDATE contacts SET last_contact_analysis_at = NOW(), lead_status = ?, updated_at = NOW() WHERE phone = ? AND store_id = ? RETURNING phone, store_id, last_contact_analysis_at。三个并发 level (5 / 15 / 30),30s cooldown 让 Neon autoscale 沉淀。

第一组测不同 row 的 UPDATE(典型 cron / per-call 场景,每个 worker 写不同 contact):

N Status Latency p50 p95 Max
5 5/5 OK 1474ms 1484ms 1484ms
15 15/15 OK 1018ms 1020ms 1020ms
30 30/30 OK 1065ms 1356ms 1356ms

注意 N=30 的 latency 比 N=5 还快 — connection pool 暖了之后 UPDATE 更快。0 serialization failures, 0 deadlocks。Neon 在 30 并发不同 row 完全顶得住,这是 6× 我们新 cap 的安全边际。

第二组测同 row 的 UPDATE worst case(模拟极端场景:cron + per-call ❸ + reconciliation 同时触发同一 contact),全部 worker 打同一行:

N Status p50 p95 Max Queue depth signal (max-min)
5 5/5 OK 1466ms 1713ms 1713ms 448ms
15 15/15 OK 1727ms 2429ms 2429ms 1427ms
30 30/30 OK 2644ms 3998ms 4099ms 3125ms

queue depth signal(max-min)随 N 线性增长是预期的:Postgres 在 row level 序列化每个 UPDATE,晚到的 worker 等前面 commit。但没有 deadlock,没有 serialization failure — Postgres 自己处理得很干净。N=5(我们实际 cap)worst case 也只 1.7s,完全可以接受;就算极端到 N=30 也只需 4.1s,远低于 Lambda 15min timeout。SQS FIFO MessageGroupId 在大多数情况下已经把 cross-source race 挡住,这个测试是 worst-of-worst 的 sanity 验证。

第三组测 DDB GSI 查询(对应 resolvePhoneIdentity 调 PhoneStoreAssignments phoneNumber-index),用真实 phones 跑 5 / 15 / 30 并发:

N Status p50 p95 Max
5 5/5 OK 1761ms 2595ms 2595ms
15 15/15 OK 383ms 418ms 418ms
30 30/30 OK 870ms 927ms 951ms

N=5 第一次 cold start 是 boto3 client init,后续 N=15/30 都 < 1s。PAYPERREQUEST 模式 auto-scale 不需要 capacity planning,30 并发 query 0 throttle / 0 RequestLimitExceeded / 0 ProvisionedThroughputExceeded。

三方汇总 — 5× Concurrency 安全边际验证

资源 当前 peak (N=4) 新 cap (N=5) 实测 ≥30 表现 Codex challenge 2 verdict
OneRouter API 4 in-flight 5 in-flight 165 burst, 0 errors ✅ refuted (no acute capacity issue)
Neon UPDATE 不同 row 4 concurrent 5 concurrent 30 concurrent in 1.4s ✅ refuted
Neon UPDATE 同 row (worst) 罕见 罕见 30 concurrent in 4.1s, 0 deadlock ✅ refuted
DDB GSI lookup 4 concurrent 5 concurrent 30 concurrent in 1s, 0 throttle ✅ refuted

Codex 的 challenge 2 在 5× concurrency 下 empirically 不成立。剩下未验证的是 prod-volume sustained throughput(我们的是 burst,不是 soak)、cold-start cascade after Lambda warm-up windowAI validation retry storms under OneRouter response variability — 这些 remain post-merge-monitor 期(2026-05-07 截止)的 watch items,但没有 acute capacity issue 阻止 deploy

PR #693 Merge Outcome + What Landed on Main

PR #693 于 2026-04-30 18:22 UTC merge,squash commit 572c39f,deploy-test.yml auto-triggered for 572c39f。最终 diff:5 files changed (+89/-12),Codex 反复 review 的版本。Test suite 从 v1 doc 写时的 148 涨到 150(+2 anchored tests:contacts-analyzer 和 ai-analysis-processor 各一个,前者 specific reservedConcurrency=5 + maxConcurrency=5,后者 specific anchored to ResultProcessorTS Ref 的 maxConcurrency=5)。

Resource pool 表格(更新版,补 DDB 维度):

Lambda OneRouter Neon writes DDB GSI lookup RingCentral
contacts-analyzer ✅ (verified ≥30 OK)
ai-analysis-processor ❌(transcribe-processor 上游 reservedConcurrency=2 间接节流)
transcribe-processor ✅(gates the chain at RC heavy 10/min)

contacts-analyzer 不调 RC 的论点不变 — 改 contacts-analyzer reservedConcurrency 跟 RC pool 完全独立。新版表格补充了 DDB 这一栏,跟 Codex challenge 1 的修正一致。

Reproducer Scripts

3 个 burst test scripts: - /tmp/onerouter-burst.py — OneRouter llm.onerouter.pro/v1/chat/completions 并发 burst,模拟 contacts-analyzer 实际 prompt 大小(~3.6KB realistic + 短 prompt 两组) - /tmp/neon-burst.py — 30 个不同 (phone, store_id) row 并发 UPDATE,connection-per-thread - /tmp/neon-burst-conflict.py — 30 个 worker 同 row 并发 UPDATE(worst-case 序列化测试) - /tmp/ddb-burst.py — PhoneStoreAssignments GSI phoneNumber-index query 并发 burst,boto3 client-per-thread

Scripts 当前在 /tmp/(本机),没 commit 到 repo。如果将来要做类似 capacity 调研,可以先来这份 doc 找参考实现,然后从 git history (git log --grep=concurrency-bump --since=2026-04-30) 找到 PR #693 ↔ docs PR #263 的双向 link 链。

v2 总结 — Methodology Reinforcement

v1 doc 末尾说 "plausible-sounding 不等于 correct" — 这一轮 Codex review 是这个原则的延伸验证。我自己第一版 PR 自信地说 "OneRouter 验证完了,Neon HTTP API 无 connection cap 所以肯定没问题" — 这是 plausible-sounding。Codex 抓出 "你只测了一个维度,真实路径还有 3 个",我接受 + 跑实测 + 数据全过 = "假设变成验证"。这跟 v1 用户 push back 触发的 frame correction 是同款 pattern,不同的是这次是 LLM agent 帮我做了这步。

新增到 v1 methodology(第 5 步)的 corollary:Burst test 不止做一次,任何 plausible-sounding 安全性 claim 都该问 "我有没有数据?" — 如果答案是 "我假设它 OK",那就是 v1 第 5 步说的 "未实测假设"。这次 Codex 等于代替用户问了这个问题。Cross-CLI consult 不当 verdict 但当前置 challenge 来源确实有用,前提是给的 brief 不已经被你的错 frame 污染过。

v2 Open Threads(超出本 update)

剩 1 个没 close 的(独立 followup,跟本 doc 主题相关但不属于这次 update 范围):

🟡 Hidden Issue #2 from #691 — 24h cron lookback vs 5,001 stale never-analyzed contact backlog mismatch。3 options(A: 一次性 backfill ~$50 OneRouter cost / B: cron lookback 24h → 168h cost 7× / C: 接受 stale 只用 cron 维护新流量)。本 PR 不解决,独立追踪在 issue #691。