fix(wle): 超时参数真正生效 —— 售后拦截 20s 不重试、表级探测走 querySlow #98

Merged
yanhui merged 1 commits from yanhui-wle-timeout-fix-1114 into main 2026-11-14 09:51:29 +08:00
Collaborator

背景

11-13 09:10 那轮「仓储物流转运异常」钉钉推送没发出去。查下来不是 webhook 问题,是业务库查询超时:

11-13 09:10:07  定时任务触发
11-13 09:30:07  Timeout: Request failed to complete in 300000ms
                → 老调度逻辑 HH:mm === sendTime 严格相等,当天再没有任何机会

system_config 记录:lastSentDate=2026-11-12、lastFailedAt=2026-11-13T01:30:07Z、lastError=Timeout ... 300000ms。

排查时发现代码里还有两笔纯粹的白等,跟「业务库变慢」叠加在一起把 300s 撑爆了。

改动

1. timeoutMs 是无效参数 —— 加告警,别再静默吞

cloudDatabase 只读 connectRequestTimeoutMs / requestTimeout。mssql 12.x 上请求级 request.timeout 不生效,超时只能靠池配置级 requestTimeout,而共享池(query())的池级超时在 pool 创建时就定死了 —— 调用方改不了。

所以服务层原来写的 { timeoutMs: 240000 }(想着「给足 4 分钟」)从来没生效过,实际一路按 60s(普通 query())/ 300s(querySlow)掐。

→ query() / queryWithFreshConnection() 入口加 warnLegacyTimeoutOption():再传 timeoutMs 就打日志,不再当没看见。

2. 售后拦截子查询:名义 240s → 真 20s,且不重试

两张售后表在 rpt 副本里是堆表(零索引),实测 125 万行 4.5s / 176 万行 8.5s。原来超时后 query() 还把它当可重试错误 —— resetPool() + 重查一次,白等 60s(11-13 日志里约 2 分钟)。

改成:

cloudDatabase.querySlow(sql, {}, {
  connectRequestTimeoutMs: 20000,  // 实测 8.5s,给 2 倍余量
  maxAttempts: 1,                  // 超时就降级「不剔除」,不重连重试
});

售后表跟不上时业务影响只是「少剔几单」(catch 里本来就有降级),不值得为它把整批核验拖进超时窗口。

3. 空窗口的表级 MAX(query_time) 探测也改走 querySlow

全表 MAX 实测已经从记忆里的 13.5s 退化到 60s 以上,用共享池等于永远探测不到 → sourceLastUpdate 恒空 → 又分不清「0 单是没发货还是没同步」(PR #94 那个场景)。

→ querySlow + connectRequestTimeoutMs=180000 + maxAttempts=1(探测失败只需退回「不知道」,重试只是把等待翻倍)。可用 CANGKU_WLE_SOURCE_LAST_UPDATE_TIMEOUT_MS 覆盖。

4. 顺手修一个时间炸弹测试

用例「窗口 0 行但数据源是新的」硬编码 sourceMaxQueryTime = '2026-11-02 09:58:00',而服务层算 ageMinutes 用的是真实时钟(registry.beijingNow()),不是测试传进来的 now —— 隔一天就变成「停更 24h」,该用例从 11-03 起在 main 上一直是红的。改成相对当前时间。

验证

node --test backend/test/warehouseLogisticsException.test.js
# tests 36 / pass 36 / fail 0

新增 2 条断言,直接锁住「超时参数必须真生效」:

  • 售后拦截查询:api === 'querySlow'、connectRequestTimeoutMs === 20000、maxAttempts === 1、timeoutMs === undefined
  • 表级 MAX 探测:api === 'querySlow'、connectRequestTimeoutMs >= 120000、maxAttempts === 1、timeoutMs === undefined

npm --prefix backend run check:505/507。两个失败都是环境原因,与本改动无关:

  • 负缓存带 TTL —— 本机没起 13333 → 218.93.9.21:1433 隧道,连不上业务库
  • secret backup ... owner-only permissions —— Windows 上 chmod 不生效(期望 0600 / 实际 0666)

不解决什么

业务库整体变慢(今天 SELECT 1 要 1.5s、TOP 1 订单表 3.6s)是数据库侧的事,代码改不了,需要 IT 看索引/统计信息/负载。本 PR 只保证「不那么容易被自己拖超时」,真正的超时兜底还是靠 #96 的重试窗口。

## 背景 11-13 09:10 那轮「仓储物流转运异常」钉钉推送**没发出去**。查下来不是 webhook 问题,是业务库查询超时: ``` 11-13 09:10:07 定时任务触发 11-13 09:30:07 Timeout: Request failed to complete in 300000ms → 老调度逻辑 HH:mm === sendTime 严格相等,当天再没有任何机会 ``` `system_config` 记录:`lastSentDate=2026-11-12`、`lastFailedAt=2026-11-13T01:30:07Z`、`lastError=Timeout ... 300000ms`。 排查时发现代码里还有**两笔纯粹的白等**,跟「业务库变慢」叠加在一起把 300s 撑爆了。 ## 改动 ### 1. `timeoutMs` 是无效参数 —— 加告警,别再静默吞 `cloudDatabase` 只读 `connectRequestTimeoutMs` / `requestTimeout`。mssql 12.x 上请求级 `request.timeout` 不生效,超时只能靠**池配置级** `requestTimeout`,而共享池(`query()`)的池级超时在 pool 创建时就定死了 —— 调用方改不了。 所以服务层原来写的 `{ timeoutMs: 240000 }`(想着「给足 4 分钟」)**从来没生效过**,实际一路按 60s(普通 `query()`)/ 300s(`querySlow`)掐。 → `query()` / `queryWithFreshConnection()` 入口加 `warnLegacyTimeoutOption()`:再传 `timeoutMs` 就打日志,不再当没看见。 ### 2. 售后拦截子查询:名义 240s → 真 20s,且不重试 两张售后表在 rpt 副本里是堆表(零索引),实测 125 万行 4.5s / 176 万行 8.5s。原来超时后 `query()` 还把它当可重试错误 —— `resetPool()` + 重查一次,**白等 60s**(11-13 日志里约 2 分钟)。 改成: ```js cloudDatabase.querySlow(sql, {}, { connectRequestTimeoutMs: 20000, // 实测 8.5s,给 2 倍余量 maxAttempts: 1, // 超时就降级「不剔除」,不重连重试 }); ``` 售后表跟不上时业务影响只是「少剔几单」(`catch` 里本来就有降级),不值得为它把整批核验拖进超时窗口。 ### 3. 空窗口的表级 `MAX(query_time)` 探测也改走 `querySlow` 全表 MAX 实测已经从记忆里的 13.5s 退化到 **60s 以上**,用共享池等于永远探测不到 → `sourceLastUpdate` 恒空 → 又分不清「0 单是没发货还是没同步」(PR #94 那个场景)。 → `querySlow` + `connectRequestTimeoutMs=180000` + `maxAttempts=1`(探测失败只需退回「不知道」,重试只是把等待翻倍)。可用 `CANGKU_WLE_SOURCE_LAST_UPDATE_TIMEOUT_MS` 覆盖。 ### 4. 顺手修一个时间炸弹测试 用例「窗口 0 行但数据源是新的」硬编码 `sourceMaxQueryTime = '2026-11-02 09:58:00'`,而服务层算 `ageMinutes` 用的是真实时钟(`registry.beijingNow()`),不是测试传进来的 `now` —— **隔一天就变成「停更 24h」,该用例从 11-03 起在 `main` 上一直是红的**。改成相对当前时间。 ## 验证 ``` node --test backend/test/warehouseLogisticsException.test.js # tests 36 / pass 36 / fail 0 ``` 新增 2 条断言,直接锁住「超时参数必须真生效」: - 售后拦截查询:`api === 'querySlow'`、`connectRequestTimeoutMs === 20000`、`maxAttempts === 1`、`timeoutMs === undefined` - 表级 MAX 探测:`api === 'querySlow'`、`connectRequestTimeoutMs >= 120000`、`maxAttempts === 1`、`timeoutMs === undefined` `npm --prefix backend run check`:505/507。两个失败都是环境原因,与本改动无关: - `负缓存带 TTL` —— 本机没起 `13333 → 218.93.9.21:1433` 隧道,连不上业务库 - `secret backup ... owner-only permissions` —— Windows 上 `chmod` 不生效(期望 0600 / 实际 0666) ## 不解决什么 业务库整体变慢(今天 `SELECT 1` 要 1.5s、`TOP 1` 订单表 3.6s)是数据库侧的事,代码改不了,需要 IT 看索引/统计信息/负载。本 PR 只保证「不那么容易被自己拖超时」,真正的超时兜底还是靠 #96 的重试窗口。
yanhui added 1 commit 2026-11-14 09:33:14 +08:00
11-13 09:10 那轮钉钉推送 300s 超时没发出去。除了业务库本身变慢,代码里还有两笔白等:

1. `{ timeoutMs: 240000 }` 是无效参数
   cloudDatabase 只读 connectRequestTimeoutMs / requestTimeout。mssql 12.x 上请求级
   request.timeout 不生效,超时只能靠池配置级 requestTimeout,而共享池(query())的
   池级超时在 pool 创建时就定死 —— 所以「给足 4 分钟」从来没生效过,实际一路按
   60s(普通 query)/ 300s(querySlow)掐断。
   → cloudDatabase 两个查询入口加 warnLegacyTimeoutOption():再传 timeoutMs 就告警,
     不再静默当没看见。

2. 售后拦截子查询:名义 240s → 真 20s,且不重试
   两张售后表在 rpt 副本里是堆表,实测 4.5s / 8.5s。原来超时后 query() 还把它当
   可重试错误 resetPool + 重查一次,白等 60s(11-13 日志里约 2 分钟)。
   改成 querySlow + connectRequestTimeoutMs=20000 + maxAttempts=1:超了就按既有
   catch 降级「不剔除」,不为一个补充判据把整批核验拖进超时窗口。

3. 空窗口的表级 MAX(query_time) 探测改走 querySlow(180s,maxAttempts=1)
   全表 MAX 实测已跑不进 60s,用共享池等于永远探测不到 → sourceLastUpdate 恒空,
   「0 单是没发货还是没同步」又分不清了。探测失败只需退回「不知道」,不值得重试。

顺带修一个时间炸弹测试:用例「窗口 0 行但数据源是新的」硬编码 sourceMaxQueryTime
为 2026-11-02 09:58,而服务层算 ageMinutes 用的是真实时钟(registry.beijingNow)——
隔一天就变成「停更 24h」,该用例在 origin/main 上从 11-03 起一直是红的。
改成相对当前时间。

测试:warehouseLogisticsException 36/36 通过(新增 2 条「超时参数必须真生效」断言)。
yanhui merged commit 9924c8d1c9 into main 2026-11-14 09:51:29 +08:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: lunanting/cangku#98
No description provided.