From 3fc503235c4204f24338accdb7f0483edfd050c6 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 21 Sep 2026 06:20:03 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5:=20=E3=80=8C=E8=A7=A6=E5=8F=91?= =?UTF-8?q?=E6=9D=A1=E4=BB=B6=20=E2=89=A0=20=E5=A4=8D=E5=8F=91=E5=8E=9F?= =?UTF-8?q?=E5=9B=A0=E3=80=8D=E4=B8=A4=E5=8D=8A=E6=8B=86=E5=BC=80=EF=BC=88?= =?UTF-8?q?pi=20=E8=A1=A5=EF=BC=8C=E6=88=91=E5=A4=8D=E7=8E=B0=E5=90=8E?= =?UTF-8?q?=E5=90=8C=E6=84=8F=EF=BC=89+=20=E8=AE=B0=E4=B8=8B=204=20vs=207?= =?UTF-8?q?=20=E7=9A=84=E5=8F=A3=E5=BE=84=E5=B7=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pi 指出我上一笔把一件事写成了一个成因。拆开后是**两条**,缺一不可: 触发条件 快照在**取件时刻**正确,投递与取件之间被读掉 ⇒ 让**这一批**投出已读的信 复发原因 deliveredMails 是**内存** BoundedSet,重启即失忆 ⇒ 让它**下一轮**又挑同一批 **关键结论**:B-7.7 要的落盘账本只治"复发原因"那一半; **快照失效是另一半,账本治不了**(账本挡得住"整封重投",但挡不住"取件后、投递前被读掉")。 ## 4 还是 7:我一开始数和 pi 冲突,查下来是**口径不同,两边都对** 口径A 误投前**有过任何**合法投递 ⇒ 7/7 ← 我第一版数的 口径B 前一轮**也是 catchup** 且间隔 >5h(排除 SSE 首投) ⇒ 4/7 ← pi 的 4 差异全在 7a350f9d / 57b0c703 / 18527c6b:前一次是 SSE 实时投递(catchup=False,仅早 1.6h)。 **对"复发原因"这条论证,口径 B 才是相关的那个** —— 要证"重启后整批重来", 得看"上一轮也是补投",而不是"之前投过"。已在 docs 里写明两个口径各自的读数。 ## 另一条口径边界(我量的,pi 没提) 那 7 例全在 09-13/09-14;pi 侧 catchup 投递 **09-15 09:45 之后再没出现过**(距 09-21 已 5.9 天), 且轮次时刻分布在 07/09/10/15/16/17/19/20/22/23 点,**不在 04:00**。 ⇒ "每晚重来"对 **dsh 侧成立**(crontab `0 4 * * * restart dsh.service`),对 **pi 侧不成立** (它自己的宿主重启/唤醒,另一个触发器)。那 7 例是**历史反例**,不是"现在每晚都在发生"。 --- docs/PLUGIN-CONTRACT.md | 44 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/docs/PLUGIN-CONTRACT.md b/docs/PLUGIN-CONTRACT.md index 995619e..b225ce3 100644 --- a/docs/PLUGIN-CONTRACT.md +++ b/docs/PLUGIN-CONTRACT.md @@ -448,8 +448,48 @@ SSE 只推连上之后的事件。插件重启前发来的邮件不会再推一 > **"判据没答 ≠ 判据答错了"**:这 0/108 没答错,它答的是另一个问题。 > > ⇒ 结论要分开写:`catchUp` 的**取件谓词**没有错(`unreadFor` 只看 `mail_reads`, -> 取的时候确实未读);**错的是取件与投递之间的时效假设**。这给了"每天重投" -> **第三条**独立成因(前两条见下)。也说明"修好 (b) 重投就会停"是新的假绿期待。 +> 取的时候确实未读);**错的是取件与投递之间的时效假设**。 +> +> ★ **这条还要再拆成两半(pi 2026-09-21 补,我复现后同意)—— 触发条件 ≠ 复发原因**: +> +> | | 机制 | 作用 | +> |---|---|---| +> | **触发条件** | 快照在**取件时刻**正确,投递与取件之间被读掉 | 让**这一批**投出已读的信 | +> | **复发原因** | `deliveredMails` 是**内存** `BoundedSet` ⇒ 进程重启即失忆 | 让它**下一轮**又从同一批里挑 | +> +> 两者缺一不可:没有"失忆",快照过期只造成一次误投;没有"快照过期", +> 失忆后重投的也是**真正未读**的那些(那本来就是 `catchUp` 该干的事)。 +> 我复核了 pi 给的分布:这 7 例里 **4 例是"第二轮"** —— 同一批在约 10 小时前 +> 已**合法**投过一轮(当时确实无读行),第二轮才是在读行已存在时投的。 +> 例:`1868127e` 09-13 12:36 投(无读行)→ 09-13 15:10 投(无读行)→ +> 09-14 01:25 投(**已有读行**,`read_at` 09-14 01:22:25)。 +> +> ⚠️ **"前有合法投递"有两种口径,数出来不一样(我第一次数成 7/7,与 pi 的 4 冲突)**: +> +> | 口径 | 判据 | 结果 | +> |---|---|---| +> | A | 误投那次**之前有过任何**合法投递 | **7/7** | +> | B | 之前那一轮**也是 catchup**(不是 SSE 首投)且间隔 > 5h | **4/7** | +> +> 差异全在那 3 例(`7a350f9d`/`57b0c703`/`18527c6b`):它们前一次是 **SSE 实时投递** +> (`catchup=False`,仅早 1.6h),不是"上一轮 catchup"。**pi 的 4 用的是口径 B,是对的**; +> 我的 7 用的是口径 A,也没错 —— **两个口径的基准不同,不是分歧。** +> 记数时必须写明用哪个(**"量到 ≠ 量对了;任何读数的第一句话应该是'我量的是哪个东西'"**)。 +> 对"复发原因"这条论证,**口径 B 才是相关的那个**:要证明"重启后整批重来", +> 得看"上一轮也是补投",而不是"之前投过"。 +> ⇒ **B-7.7 要的落盘账本治的是"复发原因"那一半;快照失效是另一半,账本治不了它。** +> (账本能挡住"整封重投",但同一批里"取件后、投递前被读掉"仍会发生 —— +> 除非把去重判据从"投过没有"扩展到"投的这一刻还算不算未读"。) +> +> ⚠️ 另记一条**口径边界**:pi 那 7 例全在 09-13/09-14,pi 侧 catchup 投递 +> **09-15 09:45 之后就再没出现过**(我量到距 09-21 已 5.9 天), +> 且 pi 侧的轮次时刻分布在 07/09/10/15/16/17/19/20/22/23 点 —— **不在 04:00**。 +> 所以"每晚重来"对 **dsh 侧成立**(crontab `0 4 * * * restart dsh.service`), +> **对 pi 侧不成立**(是它自己的宿主重启/唤醒,另一个触发器)。 +> 那 7 例是**历史反例**,不是"现在每晚都在发生"。 +> +> 这给了"每天重投"**第三条**独立成因(前两条见下)。也说明"修好 (b) 重投就会停" +> 是新的假绿期待。 > > "未读"的成因有两条,互相独立(**连同上面那条"快照过期"共三条**): > **(a)** `read_mail` 不标已读(契约 T-12 对已读状态沉默);