fix(deploy): 故障告警按 invocation 分会话 —— 崩溃循环时告警不再被护栏挡下

# 问题(实测)

服务反复崩溃时,故障告警**送不出去**。

链路:告警邮件带 `relay:"summary"`(走免预算通道),而服务端按
`AutoAliasFor(收件人, 主题)` 派生会话别名 —— **主题相同即同一条会话**。于是崩溃
循环下多封告警全落进同一条会话,而网关对会话内**连续**的中继邮件有
`maxRelayHops=5` 的硬上限(防两个 Agent 互相唤醒的正当护栏)→ 第 6 封起返回 403:

    POST /api/v1/mail/send ... - 403 245B        (网关 NRestarts=0,一直在正常服务)

报告只能落 spool,等下次 ExecStartPost `--flush` 才补投。实测 dsh 崩溃循环
(我自己的部署缺陷所致)时的六条告警全部 spooled —— 而**服务反复崩溃正是最需要
告警送达的时刻**。

# 修法

主题带上本次启动的 `INVOCATION_ID` 短号:`[dsh] 桥服务异常终止 #5ce6a845`。
主题变了,别名与会话随之分开,每次崩溃各自可投递。

**护栏不削弱**:它针对的是 agent↔agent 互相唤醒,不是同一个人收多条故障通知。
副作用是崩溃循环产生多条会话而非一条线程 —— 对"服务在崩"这件事,分开计数比合并成
一条更容易发现问题。

`INVOCATION_ID` 缺失时退回 `Date.now()+randomUUID()` 的哈希:宁可每次不同,
也不能因为拿不到标识而退回"主题相同 → 告警又被挡下"。

# 验证(三条,含两个反向对照)

  不同 invocation      → 主题各不相同(每次崩溃各自成会话)
  无 invocation(兜底) → 每次仍各不相同(不会退回同主题)
  同一 invocation 重报 → 主题相同(一次崩溃仍是一条会话,不刷屏)

注:第二次验证第一次跑时"失败",是因为**我的测试假设错了** —— 父环境里本来就继承了
`INVOCATION_ID`,两次跑用的是同一个 id。用 `env -u INVOCATION_ID` 才真正走到兜底分支。
This commit is contained in:
2026-09-12 11:56:48 +08:00
parent c9209e4adc
commit 9204f019a1

View File

@ -58,6 +58,21 @@ function redact(value, env) {
.slice(0, 1_500); .slice(0, 1_500);
} }
/**
* 本次服务启动的短标识,用于让每封故障通知各自成会话(见 subject 处的注释)。
*
* 优先用 systemd 的 `INVOCATION_ID`(每次启动唯一)。它缺失时退回随机值 ——
* 宁可每次不同,也不要因为拿不到标识而退回"主题相同 → 告警被护栏挡下"。
*/
function invocationTag(env) {
const invocation = String(env.INVOCATION_ID || '').trim();
if (invocation) return invocation.replace(/[^A-Za-z0-9]/g, '').slice(0, 8);
return createHash('sha256')
.update(`${Date.now()}:${randomUUID()}`)
.digest('hex')
.slice(0, 8);
}
function relayKeyFor(env, serviceName) { function relayKeyFor(env, serviceName) {
const invocation = String(env.INVOCATION_ID || '').trim(); const invocation = String(env.INVOCATION_ID || '').trim();
if (invocation) return `service-failure:${invocation}`; if (invocation) return `service-failure:${invocation}`;
@ -97,7 +112,23 @@ export function buildFailurePayload({
return { return {
to: String(env.AGENTMAIL_FAILURE_RECIPIENT || DEFAULT_RECIPIENT), to: String(env.AGENTMAIL_FAILURE_RECIPIENT || DEFAULT_RECIPIENT),
subject: `[${agentName}] 桥服务异常终止`, /*
* 主题带上本次启动的 invocation 短号。
*
* 服务端按 `AutoAliasFor(收件人, 主题)` 派生会话别名,所以**主题相同 = 同一条会话**。
* 崩溃循环下这一点会咬人:多封告警全落进同一条会话,而网关对会话内**连续**的中继
* 邮件有 `maxRelayHops=5` 的硬上限(防两个 Agent 互相唤醒的正当护栏)→ 第 6 封起
* 返回 403报告只能落 spool等下次 ExecStartPost `--flush` 才补投。
*
* 实测就是这样dsh 崩溃循环时的六条告警全部 spooled而**服务反复崩溃正是最需要
* 告警送达的时刻**。
*
* 带 invocation 让每次崩溃各自成会话,因而每次都能投出去 —— 护栏不受影响
* (它针对的是 agent↔agent 的互相唤醒,不是同一个人收多条故障通知)。
* 副作用是崩溃循环会产生多条会话而不是一条线程;对"服务在崩"这件事,
* 分开计数比合并成一条更容易发现问题。
*/
subject: `[${agentName}] 桥服务异常终止 #${invocationTag(env)}`,
body: lines.join('\n'), body: lines.join('\n'),
relay: 'summary', relay: 'summary',
relay_key: relayKeyFor(env, service), relay_key: relayKeyFor(env, service),