- 30 Aug, 2026 31 commits
-
-
luoqi committed
-
luoqi committed
-
生产 113 万患者全量差分要几小时,还会跟 2 小时批次抢 RDS;收窄到几十万后 几分钟就能在**生产真实数据**上拿到证据。抽样偏向"有 active 诊断/建议信号"的患者, 否则大多数抽中的人两版都返回空,验了个寂寞。
⚠ ️ 收窄降低的是**覆盖**不是可信度:差异一旦出现仍是真差异;但「子集零差异」 ≠「全量零差异」,所以报告行里带上患者域,逼自己写清跑的是多少人。 本地 3000 位子集实测:11/11 零差异。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
luoqi committed
-
luoqi committed
-
把 buildGapCore 的 resolvedTeethSql 从**逐 (患者×信号) 行相关子查询**改成 **集合式预聚合 + 反连接**。核心恒等式 ∃x∈G: t(x) ⋛ a ⟺ max{t(x)} ⋛ a, 分组 G=(患者,牙位)。13 个分支对 sig 的相关性只有三类: 时间门(10)/ 病历号等值(1)/ 无相关(2),外加「建议优先」一条的 sig.type 标量谓词。⚠ ️ 集合式是**独立重写一份**,刻意不与 legacy 共用片段 —— 共用则重构写错的地方两边 一起错、对拍互相抵消。等价性靠 verify-gap-equivalence 逐行差分来证。 全口码(K05/K07)不进集合式:其 lateral PG 本来就会摘掉(零收益),硬套还慢 2~3 倍。 **默认 legacy**,靠 PAC_GAP_VARIANT=setbased 显式开启。生产不设该变量 → 行为零变化。 验证(测试机 585K 患者 / 本地 30K): · SQL 层 11/11 子场景逐 (患者×信号×牙位) **零差异**,行数逐个相同 · 画像消费方 2000 位患者零差异,单患者 p95 47→27ms 不劣化 · 端到端 plan_reasons 行级 diff=0;测试机 32 万条差异仅时间漂移、**零删除** · 交互路径(详情页刷新)200 位 × 11 子场景零差异,一次刷新 89→75ms · 13 条分支源行全部非空 —— 零差异不是空转 收益(测试机相邻两轮,并发4):场景段 794s → 545s(×1.46)。**生产未验**。 新增 src/cli/verify-gap-equivalence.cli.ts:两版同一 REPEATABLE READ 快照双向 EXCEPT ALL 差分;--self 自对拍先证工具可信;--persona/--single/--conc 覆盖 画像、交互、并发标定四条路径。 新增 tests/gap-setbased-parity.spec.ts:结构对拍,守「两种形态的分支集合不许走散」 (加分支只改一边 = 静默错召,tsc 和现有 spec 都发现不了)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
luoqi committed
-
删掉四类冗余: · 「该计划共 N 条理由,另 X 条判得对」的旁注(#8/#10/#14/#15/#17) · 算法机制的重复解释(#9「算法逻辑没错,是事实没进结构化数据」等) · 「该修复上线晚于本单建单,故当时未生效」这类时序补注(#19/#22) · 举证细节的收尾展开(#8 总院回访那段收成一句) 个案表是给人看结论的,不是复述推理的地方。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luoqi committed -
luoqi committed
-
luoqi committed
-
生产全量实测(113 万患者 / 54.8 万命中,同一台 RDS,相邻两轮): 串行(=1): 场景段 7,200,096ms(2h00m) 整轮 8,654,105ms(2h24m) 并发(=4): 场景段 3,115,928ms(51m56s) 整轮 4,806,869ms(1h20m) → 场景段 ×2.31,整轮 ×1.80,省 64 分钟 先在生产一个 39,148 患者的诊所交替标定(×2.18),再由全量复现(×2.31)。
🔴 推翻原有的「⛔ 别指望靠这个旋钮提速」结论 —— 它让生产一年多没开这根杠杆。 原依据是「并发3=24.1分 vs 串行23.3分」,两处都错: ① 3% 差距落在 ±25% 的环境噪音里(用「两边跑同一条 SQL」的对照组量过),那次比较 什么也没证明; ② 机制归因也错:真瓶颈是**延迟**(逐次索引探查等 page,CPU 与磁盘都闲着)不是吞吐, 所以并发 2 就超线性(生产 ×1.50 / 测试机 ×2.88)。若真是吞吐受限,墙钟应约等于 各查询耗时之和;实测墙钟只有求和的 43%。 同时把「集合式重写是唯一出路」那段改成现状:并发已解决窗口问题,集合式转为可选。 .env.example 补文档,含「判据只能看阶段墙钟、不能看单条 sql=」的读数陷阱。 本提交只改注释与 .env.example,不改任何运行逻辑(生产开关已于 2026-08-30 09:16 生效)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
生产事故修复(2026-08-30):在 pac-service 容器里 docker exec 跑任何 CLI, 都会 createApplicationContext(AppModule) → 跑一遍 SyncIncrementalScheduler.onModuleInit → reapStaleRunningLocks 把 service 里**正在跑**的那轮同步当僵尸锁清掉。 实测 08:17:11 起 recompute-plans → 08:17:13 正常跑着的 08:15 那轮被标 failed (前 19 轮全 success)。数据未丢(cursor_after=null,下轮同水位 catchup),但白丢一轮。 两道防线:年龄阈值 REAP_MIN_AGE_MS=3h + 16 个 CLI 建上下文前设 PAC_SCHEDULER_DISABLED=1。 不带任何开关,上线即生效。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luoqi committed -
🔴 事故(2026-08-30 生产):在 pac-service 容器里 docker exec 跑 recompute-plans, 08:17:11 起进程 → 08:17:13 正在跑的 08:15 那轮同步被标 failed。前 19 轮全 success, 只死了撞上的这一轮。数据未丢(cursor_after=null,下轮同水位 catchup),但白丢一轮。 根因:每个 CLI 都 createApplicationContext(AppModule) → 跑一遍 SyncIncrementalScheduler.onModuleInit → reapStaleRunningLocks。 原判据是「startedAt < 本进程启动 = 僵尸锁」,注释里写着"两者的进程都不可能比本进程 启动得更早还活着"—— 但**长驻的 pac-service 恰恰就是那个更早启动还活着的进程**。 理由写反了方向,而且只在"回收逻辑跑在长驻服务里"时才成立。 两道防线: ① scheduler 加年龄阈值 REAP_MIN_AGE_MS=3h —— 真僵尸锁必然躺很久,正在跑的不会。 用年龄区分,不靠猜进程身份。(生产单轮摄入实测 28~52 分钟) ② 新增 src/cli/bootstrap-flags.ts,16 个 CLI 在建上下文**之前**设 PAC_SCHEDULER_DISABLED=1(该总闸本就会跳过回收,只是没人用)。 豁免 sync-incremental.cli(它就是要触发同步),由 ① 兜底。 回归测试 tests/cli-scheduler-guard.spec.ts:遍历所有会建上下文的 CLI, 断言调用存在**且位置早于** createApplicationContext;并锁住年龄阈值 ≥2h。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
🔴 事故(2026-08-30 生产):在 pac-service 容器里 docker exec 跑 recompute-plans, 08:17:11 起进程 → 08:17:13 正在跑的 08:15 那轮同步被标 failed。前 19 轮全 success, 只死了撞上的这一轮。数据未丢(cursor_after=null,下轮同水位 catchup),但白丢一轮。 根因:每个 CLI 都 createApplicationContext(AppModule) → 跑一遍 SyncIncrementalScheduler.onModuleInit → reapStaleRunningLocks。 原判据是「startedAt < 本进程启动 = 僵尸锁」,注释里写着"两者的进程都不可能比本进程 启动得更早还活着"—— 但**长驻的 pac-service 恰恰就是那个更早启动还活着的进程**。 理由写反了方向,而且只在"回收逻辑跑在长驻服务里"时才成立。 两道防线: ① scheduler 加年龄阈值 REAP_MIN_AGE_MS=3h —— 真僵尸锁必然躺很久,正在跑的不会。 用年龄区分,不靠猜进程身份。(生产单轮摄入实测 28~52 分钟) ② 新增 src/cli/bootstrap-flags.ts,16 个 CLI 在建上下文**之前**设 PAC_SCHEDULER_DISABLED=1(该总闸本就会跳过回收,只是没人用)。 豁免 sync-incremental.cli(它就是要触发同步),由 ① 兜底。 回归测试 tests/cli-scheduler-guard.spec.ts:遍历所有会建上下文的 CLI, 断言调用存在**且位置早于** createApplicationContext;并锁住年龄阈值 ≥2h。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
只读:每条子场景 SQL 外套 count(*),不写库。判据用**墙钟**,不看各查询耗时之和 (并发下单条会被争抢拉长,和不可比 —— 这正是 2026-08-29 那次误判的成因之一)。 背景:代码注释里「
⛔ 别指望靠并发提速」的依据是「并发3=24.1分 vs 串行23.3分」, 3% 的差距落在 ±25% 的环境噪音里,什么也没证明,却成了生产不开并发的理由。 今晚测试机 C 轮(legacy,并发4)各子场景耗时之和 1,736s 而场景段墙钟仅 794s —— 墙钟远小于求和 = 并发确实在重叠,与「瓶颈是共享I/O、并行无用」的旧归因矛盾。 本地(30K,8000 患者子集,两对交替): conc=4 4.8s / 5.2s conc=1 10.5s / 10.0s → 稳定 ×2.0,可复现 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
luoqi committed
-
plan.controller recomputeForPatient 是 HTTP 端点,走的就是这套召回 SQL 的 单患者路径(scope.patientId)。批量慢是运维问题,这条慢是用户当场感受得到的问题, 之前只测了批量和画像,漏了它。 按「一位患者跑完 11 个子场景」= 一次刷新的真实代价来计时,同时逐 signal×tooth 比结果。 本地(30K):零差异;单查询 p95 26→17ms;一次刷新 103→71ms。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luoqi committed -
luoqi committed
-
⛔ 我上一版据 B 轮单点写下「集合式内存打穿、溢盘、拖垮全口查询」——全错。 D 轮复现不出来(545s,四轮最快);实测每轮临时文件 C=4 个 / D=4 个、增量 62MB, 根本没溢盘;187GB 是统计累计值不是今晚产生的。B 轮整段压在 02:00 的 stale-scan cron 上,是外部负载。那套「内存打穿」的因果链是照着一个数字编的。 真实结论(C vs D,相邻两轮、两个同SQL全口对照组给出 ±25% 噪音基准): 场景段 794s → 545s(−31%) 整轮 1,181s → 923s(−22%) 超噪音的收益集中在 impacted ×2.25 / caries ×2.09 / hard ×1.59 / endo ×1.43 正确性三道门全过;端到端差异全是新增(时间漂移),零删除。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
luoqi committed
-
luoqi committed
-
luoqi committed
-
luoqi committed
-
luoqi committed
-
luoqi committed
-
luoqi committed
-
画像是**逐患者**调用(全量 54.7 万次),SQL 形态与召回不同(scope 恒 1 行), 单次开销会被放大 54.7 万倍 —— 必须单独验,而且要比耗时分布不只比结果。 只读不写库:直接调 selectForPatient 两次比 gap 列表。 selectForPatient 加可选 variant 入参(只给对拍用;生产路径不传,走环境开关)。 本地实测(2000 位有 active 信号的患者): 零差异(其中 1441 位有 gap) 耗时/患者 legacy p50=7ms p95=18ms 合计=16.1s setbased p50=6ms p95=16ms 合计=13.9s ← 不劣化 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
luoqi committed
-
luoqi committed
-
形态:cand → scope → resolved 预聚合 → 牙位反连接。核心恒等式 ∃x∈G: t(x) ⋛ a ⟺ max{t(x)} ⋛ a,分组 G=(患者,牙位)。 13 个分支对 sig 的相关性只有三类:时间门(10)/ 病历号等值(1)/ 无相关(2), 另有「建议优先」一条多个 sig.type 标量谓词(ndx 标志)。⚠ ️ 集合式是**独立重写一份**,刻意不与 legacy 共用片段 —— 共用则重构写错的地方 两边一起错、对拍互相抵消。等价性靠 verify-gap-equivalence 逐行差分来证。 全口码(K05/K07)不进集合式:它们的 lateral PG 本来就会摘掉(零收益), 实测硬套进 gap_cand 还慢 2~3 倍(1309→4439ms / 1388→3065ms)。 新增: · src/cli/verify-gap-equivalence.cli.ts —— 两版同一 REPEATABLE READ 快照, 双向 EXCEPT ALL 差分到 (患者,信号,牙位);--self 自对拍先证工具可信 · tests/gap-setbased-parity.spec.ts —— 结构对拍,守「分支集合不许走散」 (加分支只改一边 = 静默错召,tsc 和现有 spec 都发现不了) · scenario.buildScenarioSql() 抽成独立方法,让对拍拿到线上跑的那条 SQL 本身 · PAC_GAP_VARIANT=setbased 切换;默认 legacy 本地实测(30K 库):11 个子场景全部零差异,行数逐个相同; 牙位级 ×1.05~1.73(库小全热,不作为收益判据,以测试机 585K 为准)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
① 全口场景(K05/K07)的 lateral PG 已自动删除(useless-left-join removal), 本方案对它们零收益;测试机 K05 9m13s 的钱在患者级 NOT EXISTS,不在 resolvedTeeth。 → 全轮预期改善下修到 35~40%,不足以单独装进 2 小时窗口。 ② 本地 30K 库单分支对拍只快 1.26×,库太小全热不能外推 → 先建对拍与基准, 单子场景试点达标(≥3×)再铺开;不达标就停,只留对拍工具。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luoqi committed
-
- 29 Aug, 2026 9 commits
-
-
luoqi committed
-
luoqi committed
-
2026-08-29 生产六轮实测(endo_no_rct,72,769 行,全部在生产 RDS 上跑): 配置 耗时 相对基线 无索引 + 4MB (冷) 18:43 无索引 + 4MB (暖) 17:33 ← 基线;冷暖只差 6%,缓存不是主因 无索引 + 256MB (暖) 11:37 −34% 无索引 + 256MB (暖,复现) 12:12 −30% 有索引 + 4MB (暖) 16:42 −5% ← 噪声内,索引已从生产撤除 有索引 + 128MB (暖) 14:47 −16% ← 128MB 只吃到一半收益
⭐ 取 256MB:收益对取值很敏感,128→256 还差一倍。原本按「溢出只有 4 批、 内存用量 8MB」推断 128MB 够用,实测否定了这个推断。⛔ 事务级 SET LOCAL,不能全局调:work_mem 是每个排序/哈希节点的上限,不是每连接。 生产 RDS 约 7GB 内存,全局设大值遇上并发排序会吃穿。SET LOCAL 出事务自动还原, Web API 连接不受影响;场景查询串行,同一时刻只有一条,峰值可控。⛔ 别再加信号码部分索引:测试机与生产都测过,生产 −5% 落在噪声里 (同配置两次测量本身差 6%),不值得引入一个 Prisma 管不到的索引。 tests/recall-future-return-visit-gate.spec.ts 的 SQL 捕获桩补 $transaction/$executeRaw。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
补的是常态可观测性:2026-08-29 生产 plan 段从 12 分钟涨到 159 分钟仍未跑完, 而引擎中间什么都不打,只能临时起 pg_stat_activity 采样器反推。 现在每个子场景一行(SQL 与 Node 后处理分开计时)+ 每轮阶段耗时一行。 同时带上义齿按颌分支的预过滤(逻辑恒等,单患者路径实测 16.7×)—— 它针对的正是本次新增的两条 exam_findings 分支,是生产回归的头号嫌疑。
luoqi committed -
luoqi committed
-
luoqi committed
-
2026-08-29 生产 plan 段从 12 分钟涨到 159 分钟仍未跑完,而引擎从「
▶ Running engine」 到最后的统计块之间**什么都不打**,整轮是个黑盒 —— 只能临时起 pg_stat_activity 采样器 反推,才勉强定位到子场景粒度。这组日志就是为了不再重复那个过程。 ① 每个子场景一行(常态开启,每轮 11 行): [recall] sub=missing_tooth code=K08 sql=1234ms rows=567 post=89ms hits=42⭐ SQL 与 Node 后处理**分开计时** —— 今天最大的困难就是知道慢、却分不清 慢在 SQL 还是慢在 Node。 ② runAllForHost 三段(常态开启,每轮 1 行): [plan] 阶段耗时 场景=Xms 预取=Yms 写入=Zms 命中患者=N 总计=Wms 今天一度误以为瓶颈在预取,有这行就不会跑偏。 ③ PAC_RECALL_DUMP_SQL=1(默认关)保留,需要拿 SQL 原文做 EXPLAIN 时才开。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
luoqi committed
-
luoqi committed
-