fix(push): PUSH_SUBSCRIPTION_SECONDS 300→WEEK (604800)
Root cause of push notifications failing while cron works:
- Stalwart sends the verification request (RFC 8620 §7.2) once at
subscription creation; event pushes (§7.3) arrive without
verificationCode and rely on dedup_exists("push:subscription:{id}")
to confirm prior verification.
- TTL was 300s (5 min): key expired → every event push 401'd.
- Cron path bypasses this key entirely (polls /changes directly),
which is why cron-delivered emails still arrived.
Fix: align with PUSH_REGISTRATION_SECONDS (1 week). Stalwart does not
re-verify; the key must survive restarts and any gap between
subscription creation and events.
Also fixes: removed fictional push:subscription-code:{code} from
opengaps TTL table (no such key exists in code); corrected
reference.md caller from register_push→jmap_push verification path;
fixed 前→EX typo in reference.zh-CN.md; design.md updated with
fifth 'why' bullet and historical context.
This commit is contained in:
parent
88ac8594cc
commit
d452f95485
|
|
@ -638,14 +638,17 @@ Not implemented, see `docs/retired.md`.
|
|||
The Push event is consumed by Streams and delivered to Telegram. Its key path delivery semantics are as follows:
|
||||
|
||||
**The only authoritative source for keys and TTL is `docs/reference.md` (section Redis Keys and TTL). **The tables will not be repeated in this section - repeated tables
|
||||
Will be modified: `push:subscription:{id}` was previously marked as 7d, and `push:registration:{...}` was marked as 360s registration solo lock.
|
||||
Both are wrong, and the real 360s solo lock `lock:push-register:{sha256(callback_url)}` was missing the entire line. has now converged to
|
||||
Single authority table, this section only retains four "whys" that affect design judgment:
|
||||
Historical note: `push:subscription:{id}` was previously marked as 7d in docs but the code used 300s — a bug that caused event pushes to fail with 401 after 5 minutes
|
||||
(because Stalwart only sends the verification request once, at subscription creation, and never re-verifies). Now corrected to 7d (`WEEK`), aligned with `PUSH_REGISTRATION_SECONDS`.
|
||||
The `push:registration:{...}` was marked as 360s registration solo lock, which was also wrong; the real 360s solo lock is `lock:push-register:{sha256(callback_url)}`.
|
||||
All values have now converged to the single authority table in `docs/reference.md`. This section retains five "whys" that affect design judgment:
|
||||
|
||||
- **Reconciliation lock TTL 60s, with heartbeat renewal every 10s to 30s during active processing**, ensuring a crashed process releases the lock within 60s instead of blocking reconciliation
|
||||
(`REQ-RECONCILE-IDEMPOTENCY`, `SAF-RECONCILE-LOCK`).
|
||||
- **push registration solo lock TTL 360s, deliberately larger than the upper limit of 300s for a single outbound request**, otherwise a slow registration will cause repeated requests
|
||||
(`SAF-AUTH-JMAP-PUSH`).
|
||||
- **Push subscription verification key TTL 7d (`WEEK`)**: Stalwart sends the verification request (RFC 8620 §7.2) once at subscription creation; event pushes (§7.3) arrive without
|
||||
`verificationCode` and rely on this key existing to confirm prior verification. The key must outlast any gap between subscription creation and events, and survive restarts — Stalwart does not re-verify.
|
||||
- **Reconciliation deduplication 24h is intentionally designed**: the same email will only be notified once within 24h (`MOD-DEDUP`, see 11.5 of this article for the scope).
|
||||
- **If the configuration key is missing, it will be regarded as failed (fail-closed)** and the business path will not be entered.
|
||||
|
||||
|
|
|
|||
|
|
@ -638,14 +638,17 @@ pub enum BotError {
|
|||
Push 事件经 Streams 消费并投递到 Telegram,其关键路径交付语义如下:
|
||||
|
||||
**键与 TTL 的唯一权威来源是 `docs/reference.md`(Redis 键与 TTL 各节)。**本节不再重复表格——重复的表
|
||||
会被改坏:此前曾把 `push:subscription:{id}` 标成 7d、把 `push:registration:{...}` 标成 360s 注册单飞锁,
|
||||
两处都错,而真正的 360s 单飞锁 `lock:push-register:{sha256(callback_url)}` 当时整行缺失。现已收敛到
|
||||
单一权威表,本节只保留影响设计判断的四条"为什么":
|
||||
历史说明:`push:subscription:{id}` 此前在文档中标为 7d 但代码实为 300s——这是一个 bug,导致事件 push 在 5 分钟后全部 401 失败
|
||||
(因为 Stalwart 仅在订阅创建时发送一次验证请求,之后不再重验)。现已修正为 7d(`WEEK`),与 `PUSH_REGISTRATION_SECONDS` 对齐。
|
||||
`push:registration:{...}` 曾被标为 360s 注册单飞锁,亦为误标;真正的 360s 单飞锁是 `lock:push-register:{sha256(callback_url)}`。
|
||||
所有值现已收敛到 `docs/reference.md` 的单一权威表。本节保留影响设计判断的五条"为什么":
|
||||
|
||||
- **对账锁 TTL 60s,活跃处理期间每 10s 心跳续期至 30s**,确保崩溃进程在 60s 内释放锁、不阻塞对账
|
||||
(`REQ-RECONCILE-IDEMPOTENCY`、`SAF-RECONCILE-LOCK`)。
|
||||
- **push 注册单飞锁 TTL 360s,刻意大于单条出站请求上限 300s**,否则一次慢注册会放进重复请求
|
||||
(`SAF-AUTH-JMAP-PUSH`)。
|
||||
- **push 订阅验证键 TTL 7d(`WEEK`)**:Stalwart 仅在订阅创建时发送验证请求(RFC 8620 §7.2),事件 push(§7.3)不带
|
||||
`verificationCode`,依赖此键存在来确认已验证过。TTL 必须长于订阅创建到首条事件之间的任何间隔,且能跨重启存活——Stalwart 不会重验。
|
||||
- **对账去重 24h 是有意设计**:同一封邮件 24h 内只通知一次(`MOD-DEDUP`,范围见本文 11.5)。
|
||||
- **配置键缺键即视为关闭(fail-closed)**,不进入业务路径。
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ Contract baseline (see `docs/reference.md` §1 for the complete semantics of key
|
|||
| `ttl::PUSH_DISABLED_SECONDS` | 86_400 | `push:subscription:{id}:status` (`disabled`) |
|
||||
| `ttl::PUSH_STATUS_PENDING_SECONDS` | 900 | `push:subscription:{id}:status` (`pending`) |
|
||||
| `ttl::PUSH_STATUS_VERIFIED_SECONDS` | 300 | `push:subscription:{id}:status` (`verified`) |
|
||||
| `ttl::PUSH_SUBSCRIPTION_SECONDS` | 300 | `push:subscription:{id}` / `push:subscription-code:{code}` |
|
||||
| `ttl::PUSH_SUBSCRIPTION_SECONDS` | 604_800 | `push:subscription:{id}` |
|
||||
| `ttl::PUSH_REGISTRATION_SECONDS` | 604_800 | `push:registration:{sha256(callback_url)}` |
|
||||
| `ttl::PUSH_ORPHAN_SECONDS` | 604_800 | `push:orphan:{subscription_id}` |
|
||||
| `ttl::ADMIN_SESSION_SECONDS` | 1,800 | `admin-session:{token}` (also the `expires_in` returned by `/admin/session`) |
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ TTL 那条真机绿灯已经用你给的 Upstash URL 跑过了(见 §1),
|
|||
| `ttl::PUSH_DISABLED_SECONDS` | 86_400 | `push:subscription:{id}:status`(`disabled`) |
|
||||
| `ttl::PUSH_STATUS_PENDING_SECONDS` | 900 | `push:subscription:{id}:status`(`pending`) |
|
||||
| `ttl::PUSH_STATUS_VERIFIED_SECONDS` | 300 | `push:subscription:{id}:status`(`verified`) |
|
||||
| `ttl::PUSH_SUBSCRIPTION_SECONDS` | 300 | `push:subscription:{id}` / `push:subscription-code:{code}` |
|
||||
| `ttl::PUSH_SUBSCRIPTION_SECONDS` | 604_800 | `push:subscription:{id}` |
|
||||
| `ttl::PUSH_REGISTRATION_SECONDS` | 604_800 | `push:registration:{sha256(callback_url)}` |
|
||||
| `ttl::PUSH_ORPHAN_SECONDS` | 604_800 | `push:orphan:{subscription_id}` |
|
||||
| `ttl::ADMIN_SESSION_SECONDS` | 1,800 | `admin-session:{token}`(同时是 `/admin/session` 返回的 `expires_in`) |
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ duplicate. Source comment:
|
|||
|
||||
| Key | TTL | Writer | Reader |
|
||||
|---|---|---|---|
|
||||
| `push:subscription:{id}` | EX 300 (caller-supplied, min 1) | `remember_push_subscription`, called from `register_push` | `push_subscription_verified` |
|
||||
| `push:subscription:{id}` | EX 604_800 (caller-supplied, min 1) | `remember_push_subscription`, called from `jmap_push` verification path | `push_subscription_verified` |
|
||||
| `push:subscription:id` | No EX | `remember_push_subscription_id` | current-subscription lookup |
|
||||
| `push:subscription:{id}:status` | 900 / 300 / 86_400 (min 1 enforced) | `set_push_subscription_status`; `pending` on verification request, `verified` on success, `disabled` on disable | **none** |
|
||||
| `push:registration:{sha256(callback_url)}` | EX 604_800 | `remember_push_subscription_for_callback`, called from `register_push` | `get_push_subscription_for_callback`; removed by `remove_push_subscription_for_callback` |
|
||||
|
|
|
|||
|
|
@ -138,12 +138,12 @@ DLQ 是 **append-and-ack**:附加到 `stalwart:jmap:dlq` 的相同 Lua 脚本
|
|||
|
||||
| 键 | TTL | 写入方 | 读取方 |
|
||||
|---|---|---|---|
|
||||
| `push:subscription:{id}` | EX 300(调用方提供,最小 1)| `remember_push_subscription`,从 `register_push` 调用 | `push_subscription_verified` |
|
||||
| `push:subscription:{id}` | EX 604_800(调用方提供,最小 1)| `remember_push_subscription`,从 `jmap_push` 验证路径调用 | `push_subscription_verified` |
|
||||
| `push:subscription:id` | 无 EX | `remember_push_subscription_id` | 当前订阅查询 |
|
||||
| `push:subscription:{id}:status` | 900 / 300 / 86_400(强制最小 1)| `set_push_subscription_status`;验证请求时为 `pending`,成功时为 `verified`,禁用时为 `disabled` | **无** |
|
||||
| `push:registration:{sha256(callback_url)}` | EX 604_800 | `remember_push_subscription_for_callback`,从 `register_push` 调用 | `get_push_subscription_for_callback`;通过 `remove_push_subscription_for_callback` 删除 |
|
||||
| `push:registration:current` |前 604_800 | `set_push_registration_current`,从 `register_push` 调用 | `get_push_registration_current`;通过 `clear_push_registration_current` 清除,`disable_push` 经 `clear_push_registration_current_if` 清除 |
|
||||
| `push:orphan:{subscription_id}` |前 604_800 | `record_push_orphan` |孤儿扫荡|
|
||||
| `push:registration:current` | EX 604_800 | `set_push_registration_current`,从 `register_push` 调用 | `get_push_registration_current`;通过 `clear_push_registration_current` 清除,`disable_push` 经 `clear_push_registration_current_if` 清除 |
|
||||
| `push:orphan:{subscription_id}` | EX 604_800 | `record_push_orphan` |孤儿扫荡|
|
||||
|
||||
**三个不同的键经常被混淆。在编辑之前请阅读本文。**
|
||||
|
||||
|
|
|
|||
13
src/state.rs
13
src/state.rs
|
|
@ -65,8 +65,15 @@ pub(crate) mod ttl {
|
|||
pub const PUSH_STATUS_PENDING_SECONDS: u64 = 900;
|
||||
/// How long a `verified` push subscription stays trusted before re-check.
|
||||
pub const PUSH_STATUS_VERIFIED_SECONDS: u64 = 300;
|
||||
/// Push subscription code and its link to the subscription id.
|
||||
pub const PUSH_SUBSCRIPTION_SECONDS: u64 = 300;
|
||||
/// Push subscription verification key: `push:subscription:{id}` =
|
||||
/// `session_digest(verification_code)`. Stalwart sends the verification
|
||||
/// request (RFC 8620 §7.2) once at subscription creation; event pushes
|
||||
/// (§7.3) arrive without `verificationCode` and rely on this key existing
|
||||
/// to confirm prior verification. TTL must outlast any realistic gap
|
||||
/// between subscription creation and the first event, and survive across
|
||||
/// restarts — Stalwart does not re-verify. Aligned with
|
||||
/// `PUSH_REGISTRATION_SECONDS` (1 week).
|
||||
pub const PUSH_SUBSCRIPTION_SECONDS: u64 = WEEK;
|
||||
/// Callback-URL -> subscription-id registration window.
|
||||
pub const PUSH_REGISTRATION_SECONDS: u64 = WEEK;
|
||||
/// Orphan callback attribution window: a callback with an unknown
|
||||
|
|
@ -1617,7 +1624,7 @@ mod tests {
|
|||
|
||||
assert_eq!(ttl::PUSH_STATUS_PENDING_SECONDS, 900);
|
||||
assert_eq!(ttl::PUSH_STATUS_VERIFIED_SECONDS, 300);
|
||||
assert_eq!(ttl::PUSH_SUBSCRIPTION_SECONDS, 300);
|
||||
assert_eq!(ttl::PUSH_SUBSCRIPTION_SECONDS, 604_800);
|
||||
assert_eq!(ttl::PUSH_REGISTRATION_SECONDS, 604_800);
|
||||
assert_eq!(ttl::PUSH_ORPHAN_SECONDS, 604_800);
|
||||
assert_eq!(ttl::ADMIN_SESSION_SECONDS, 1_800);
|
||||
|
|
|
|||
Loading…
Reference in New Issue