# CF Queues 95% Daily Usage Alert — 根因分析与修复方案

> **[2026-04-24 更新]** 本文档 §1.1 / §1.4 / §8.2 / §17.2 / §18.3 / §21 中部分预期值与承诺阈值已被实际线上数据证伪（日度免费额度是 10k 不是月度 1M；PR #1341 降幅仅 ~15% 而非 80%；`exceededResources` 恶化而非归零）。请先阅读 **`2026-04-24-queue-ops-followup.md`** 获取勘误对照和最新部署记录（Plan 8 + Plan 13），再回到本文了解历史根因分析。

| 字段 | 值 |
|---|---|
| 状态 | 待修复（已完成根因分析、待提 PR） |
| 首次告警 | 2026-04-17（Cloudflare 邮件："95% of daily usage limit for Cloudflare Queues Operations reached"）|
| 受影响环境 | ArcBlock 主账号（`983482dc52a0aee5dbd7f986840a6190`）`staging-aigne-hub-payment-kit` Worker |
| 受影响队列 | `staging-aigne-hub-payment-kit-jobs`（producer=1, consumer=1）|
| 分析负责人 | Pengfei（代码走读 + Cloudflare GraphQL Analytics 取数）|
| 生产环境影响 | 暂无（仅 staging 告警；但同一套代码上 prod 后会复现）|

---

## 第一部分：简要总览（决策者视角）

### 1. 一句话结论

`shims/cron.ts` 的 5 分钟匹配窗口 + `wrangler.staging.json` 的每分钟 cron 触发器两者叠加，导致所有 `*/N` 定时任务按 **5 倍设计频率**触发，每天产生约 13,000 条无谓的 CF Queue 操作，17 天累计触达免费额度（1,000,000 ops / month）的 95%。

### 2. 对在线数据与业务的影响底线

**本次修复只动"调度节奏"，不动任何涉及金额的代码。**

| 数据类型 | 是否变化 |
|---|---|
| 用户余额、Credit 扣减金额 | ❌ 不变 |
| 订阅下次续费时间、金额 | ❌ 不变 |
| 退款金额与次数 | ❌ 不变 |
| 链上转账次数（归集、stake 退还） | ❌ 不变 |
| Stripe / chain API 调用次数 | ⬇️ 减少约 80% |
| D1 查询次数 | ⬇️ 减少约 80% |
| CF Queue 日 ops | ⬇️ 13k → 2-4k |
| 延迟任务执行时机漂移 | ⚠️ 最多 1-5 分钟 |

**计费正确性、历史记录、对账数据不会因本次修复而产生任何变化。** 完整承诺阈值见 §18。

### 3. 改动清单与状态

> ⚠️ 本小节已经历过两次迭代。改动 A 是 2026-04-17 告警当天的**历史应急动作**，已通过 CF API 执行，**不应在 PR #1341 合入后再次执行**。最终目标状态见 §3.1。

| # | 改动 | 文件 | 当前状态 |
|---|---|---|---|
| A（历史应急）| cron `* * * * *` → `*/5 * * * *`（CF API 改，不重部署代码）| `wrangler.staging.json` | ✅ 已于 2026-04-17 13:13 UTC 执行；**PR #1341 部署后会由 wrangler 同步回 `* * * * *`，不再重复** |
| B（根治）| 匹配窗口 5min → 1min，改用 `event.scheduledTime` | `shims/cron.ts` | ✅ 在 PR #1341 `8a030209` + `fc0e4da4`（含 P2-1 的 scheduledTime fix）|
| C（节流）| `startRefundQueue` 迁移为 cron | `worker.ts` + `crons/index.ts` | ✅ 在 PR #1341 `e12b302b` |
| D（可选）| 合并 `runAllScheduledJobs` D1 查询 | `shims/queue.ts` | ⏳ 独立 PR，条件触发（Plan 12 若 exceededResources 未降则启动）|

### 3.1 PR #1341 部署后的目标终态

- `wrangler.staging.json` cron = **`* * * * *`**（由 `fc0e4da4` 恢复）
- CF 线上 cron schedule = `* * * * *`（由 wrangler deploy 同步写入）
- `shims/cron.ts` 的 `shouldRunInWindow` 窗口 = **1 分钟**
- `cronInstance.runAll()` 接受 `event.scheduledTime` 参数（由 `worker.ts` 传入）
- `refund.recovery` cron 表达式 = `0 */5 * * * *`

**invariant**（锁在测试中）：shim 窗口大小 ≥ CF 触发间隔；本次两者都是 1 分钟。**若将来任何人把 CF cron 再改成 `*/5 * * * *`，必须同步把 shim 窗口恢复到 5 分钟，否则非 5 对齐的 cron（如 `'0 1 * * * *'`）会永不运行。**

### 4. 预期最终结果

| 指标 | bug 满频基线（04-16）| Plan 1 应急态（04-18/19） | PR #1341 完整部署后（预期）|
|---|---|---|---|
| CF Queue 日 ops | 13,617 | 10,007-10,483 | 2-4k |
| 占 1M/month 免费额度 | 17 天 95% | 20 天 10% | 30 天 10-15% |
| 延迟任务调度精度 | 1 min（但 5× 重复）| 5 min | 1 min（严格，无重复）|
| Worker `exceededResources` | 1,022/day | 300/day（每次都超限）| 预期趋近 0 |
| Stripe API 日调用量 | 基线 | ~40% | ~20% |
| 告警状态 | 每日触发 | 稳定消失 | 稳定消失 |

### 5. 执行步骤（摘要，已按实际进度更新）

1. ✅ **2026-04-17 13:13 UTC** — 通过 CF API 把线上 cron 改为 `*/5`（改动 A，应急止血）。
2. ✅ **2026-04-18 ~ 04-20** — Plan 2 观察：ops 降 23-27%，发现 scheduled handler 100% 超限（每次 exceededResources）。
3. ✅ **2026-04-18 ~ 04-20** — PR #1341 提交，含改动 B + C + 文档；经过两轮 review 修复 P1-1、P2-1、P2-2。
4. ⏳ **PR review 通过后** — `wrangler deploy --config wrangler.staging.json`；部署会**同步**把 CF cron 从 `*/5`（应急态）恢复到 `* * * * *`（最终态），同时上线 shim 1min 窗口代码。这是一次部署完成改动 B+C+Plan 6。
5. ⏳ **部署后 2h** — 验证：小时 WriteMessage ≤ 40，`exceededResources` 每小时 ≤ 2（当前基线 ~12/h）。
6. ⏳ **部署后 24h** — 验证：日 ops ≤ 5,000，`exceededResources` 日合计 ≤ 10。
7. ⏳ **部署后 7 天** — 稳态观察，若 `exceededResources` 未降到阈值说明 `runAllScheduledJobs` 仍是瓶颈，启动改动 D 独立 PR。
8. ⏳ **后续** — 四份 wrangler 配置统一（Plan 9）；接入 Slack 每日 ops 报告（Plan 7）。

> 完整分步可行性与每步风险/回滚细节见 **§17 详细执行顺序与可行性分析**（§17 的文字描述保留，但执行者以本节 §3.1 的目标终态 + §5 步骤为准）。

### 6. 回滚策略

- 改动 A 回滚：`wrangler rollback --name staging-aigne-hub-payment-kit --version <prev_id>`，1 分钟。
- 改动 B/C 回滚：revert PR + redeploy，15 分钟。
- 所有改动都不改 D1 schema，无需数据迁移/数据回滚。

---

## 第二部分：详细分析档案

### 7. 背景与分析目标

#### 7.1 触发事件

`noreply@notify.cloudflare.com` 于 2026-04-17 发邮件告警："Your (ArcBlock) account has used 95% of the daily Cloudflare Queues free tier limit. If you do not take any action and exceed the daily limit, attempts to send a message to a queue will fail until the limit resets at 2026-04-18 at 00:00:00 UTC."

#### 7.2 分析目标

1. 确认告警是否真实（对应是哪个队列、哪个 Worker、哪条代码路径）。
2. 给出根因而不是表面上的症状修复。
3. 评估每种修复方案对**在线业务与数据**的影响，确保不会造成计费错误或数据丢失。
4. 给出可立即执行的止血路径与长期根治路径。

#### 7.3 数据来源

| 来源 | 用途 |
|---|---|
| `/Users/zac/work/arcblock/payment-kit/blocklets/core/.env.development.local` | 提供 `CLOUDFLARE_API_TOKEN` / `CLOUDFLARE_ACCOUNT_ID`（仅本地使用，**此文档不记录 token 值**）|
| `blocklets/core/cloudflare/wrangler.*.{json,jsonc,toml}` | Worker 部署配置 |
| `blocklets/core/cloudflare/shims/cron.ts` | CF 下 @abtnode/cron 的替身实现 |
| `blocklets/core/cloudflare/shims/queue.ts` | CF 下队列 push/consume shim |
| `blocklets/core/cloudflare/worker.ts` | CF Worker 入口（fetch/scheduled/queue handlers）|
| `blocklets/core/api/src/queues/*.ts` | 30+ 个业务队列定义 |
| `blocklets/core/api/src/crons/index.ts` | cron job 注册清单 |
| Cloudflare REST API `/accounts/:id/queues`、`/workers/scripts`、`/workers/services` | 取部署态、绑定、schedules |
| Cloudflare GraphQL Analytics API `queueMessageOperationsAdaptiveGroups`、`workersInvocationsAdaptive` | 取队列 ops 与 Worker 调用曲线 |

---

### 8. 在线数据全景

#### 8.1 账号与 Worker 盘点

**ArcBlock 主账号**（`983482dc52a0aee5dbd7f986840a6190`，告警来源）:

| Worker | Cron 实际部署 | Queue 绑定 | 备注 |
|---|---|---|---|
| `staging-aigne-hub-payment-kit` | ✅ `* * * * *`（2026-04-10 起）| ✅ producer + consumer | 告警源头 |
| `aigne-hub-staging` | ✅ `* * * * *` | ❌（调用 payment-kit 用 service binding）| 上游调用方 |
| `staging-aigne-hub-app`、`staging-aigne-hub-media-kit`、`blocklet-service-staging` | 其它 | 其它 | 不相关 |

**Pengfei 个人账号**（`7790d6810b003f5dd01c4a62db02f391`，用作私人 staging 试验）:

| Worker | 状态 |
|---|---|
| `payment-kit-staging`（04-17 部署） | 无 cron、无 queue binding；用 `wrangler.local.toml` |
| `staging-aigne-hub-payment-kit`（04-14 旧版） | 孤儿 |
| 孤儿队列 `staging-aigne-hub-payment-kit-jobs` | 10 天零操作（不是本次告警对象） |

**本次修复只涉及 ArcBlock 主账号上的部署，与 Pengfei 账号资源无关。**

#### 8.2 队列 ops 近 10 天日度分布（ArcBlock 账号）

| 日期 | WriteMessage | ReadMessage | DeleteMessage | 合计 ops |
|---|---|---|---|---|
| 2026-04-10（首次部署当天）| 222 | 222 | 222 | 666 |
| 2026-04-11 | 4,078 | 4,459 | 3,956 | 12,496 |
| 2026-04-12 | 3,229 | 3,586 | 3,196 | 10,013 |
| 2026-04-13 | 3,284 | 3,542 | 3,186 | 10,016 |
| 2026-04-14 | 3,866 | 4,301 | 3,863 | 12,030 |
| 2026-04-15 | 4,480 | 5,096 | 4,515 | 14,100 |
| 2026-04-16 | 4,488 | 4,609 | 4,520 | 13,617 |
| 2026-04-17（至 UTC 09:00）| 4,015 | 4,501 | 4,111 | 12,644 |

**17 天累计 ≈ 90,000 ops → 对 1,000,000 ops/month 免费额度占用 ≈ 9%**；**由于 CF 告警使用"按月剩余天数反推日均阈值"计算，达到当日 95% 告警即触发**。

#### 8.3 2026-04-16 小时级写入分布（payment-kit-jobs 队列）

```
00:00   610 writes
01:00  2,123 writes  ← 峰值，相当于 35 writes/min
02:00    143 writes
03:00  1,612 writes
04:00–23:00  0
```

**分布稀疏，非均匀**——说明不是"每分钟匀速 push"，而是**某些窗口集中爆推、多数时间静默**。对应业务场景：cron 运行时发现 D1 里有待处理的 Job 行（如未完成的 refund、subscription 重试、deposit vault 扫描），触发 N 倍 push。

#### 8.4 同队列 DLQ（`staging-aigne-hub-payment-kit-jobs-dlq`）

- 04-11 ~ 04-17 累计 < 30 条 write。说明**业务层大量任务执行成功，少量确实失败进 DLQ**，DLQ 不是 ops 放大主因。

#### 8.5 `payment-kit-staging`（Pengfei 账号）请求分布旁证

- 空闲期**每小时恰好 60 次**调用 = 每分钟 1 次 = `aigne-hub-staging` 的 cron 通过 service binding 穿透过来。
- 04-15 16:00-19:00 见到 3,300/h 业务峰值（无关告警）。

---

### 9. 根因技术分析

#### 9.1 Cron shim 5 分钟窗口来历

文件 `blocklets/core/cloudflare/shims/cron.ts:107-113`:

```ts
function shouldRunInWindow(cronExpr: string, date: Date): boolean {
  for (let i = 0; i < 5; i++) {
    const check = new Date(date.getTime() + i * 60000);
    if (matchesCron(cronExpr, check)) return true;
  }
  return false;
}
```

原设计假设：CF Scheduled Triggers 最细粒度曾经是每 5 分钟一次，所以一次触发要覆盖未来 5 分钟里任何匹配分钟的 cron。

**现状问题**：`wrangler.staging.json:64`（和 `wrangler.jsonc`）已经把 CF 触发改为 `* * * * *`（每分钟），**但窗口没有同步缩到 1 分钟**。

#### 9.2 5 倍放大效果量化

| cron 表达式 | 原意频率 | 现实频率（bug 中）| 放大 |
|---|---|---|---|
| `0 */5 * * * *`（`depositVault`、`revokeStake`）| 每 5 min | **每 min** | 5× |
| `0 */10 * * * *`（`creditConsume`、`vendorStatusCheck`、`vendorReturnScan`）| 每 10 min | 10 min 里 5 次 | 5× |
| `0 */20 * * * *`（`stripePayment`）| 每 20 min | 20 min 里 5 次 | 5× |
| `0 */30 * * * *`（`subscription`、`stripeInvoice`）| 每 30 min | 30 min 里 5 次 | 5× |
| `0 5 */6 * * *`（`notification`）| 每 6 h 一次 | 6 h 里连续 5 min 内跑 | 5× |

每个 `start*Queue` 函数（`startDepositVaultQueue`、`startCreditConsumeQueue`、`startSubscriptionQueue` 等）会全表扫目标表并针对每行 `push` 到 CF Queue，push 内部通过 D1 的 `UNIQUE(id)` 约束去重（同 id 重推在 addJob 阶段被阻断），但一旦 consumer 处理完删行，下一分钟的新 push 成功到达 CF Queue。

#### 9.3 `startRefundQueue` 无条件每分钟执行

`blocklets/core/cloudflare/worker.ts:1402-1403`（2026-04-17 新增）:

```ts
const { startRefundQueue } = await import('../api/src/queues/refund');
await startRefundQueue();
```

该函数本意是"deploy 过程中 isolate 被杀导致 refund 任务丢在内存未持久化"的兜底，正常情况下无需每分钟跑。目前每次 scheduled handler 都执行 = 每分钟一次全表扫 `Refund.findAll({status:'pending'})`。

#### 9.4 `runAllScheduledJobs` 每分钟扫描 30+ 队列

`blocklets/core/cloudflare/shims/queue.ts:197-212` 每分钟对每个注册队列做 2 次 D1 查询（`getScheduledJobs` + `getJobs`）。30 个队列 × 2 = 60 次 D1 查询/分钟，即使空闲。这主要吃 D1 额度，次要影响队列 ops（仅在扫到 due job 时会 push）。

#### 9.5 算术自洽验证

- 2026-04-16 写入 4,488 条，3 ops/msg × 4,488 ≈ 13,464 ≈ 实际 13,617（误差来自 DLQ + retry）。
- 1440 次 cron × 3 push/次 ≈ 4,320 msg/day ≈ 观测值。

**结论自洽**：平均每次 cron 推 3 条消息，峰值 35/min 对应业务事件（批量 refund 恢复或 subscription 批量续费）。

---

### 10. 修复方案详细说明

#### 改动 A（立即执行，配置级回滚）

```diff
 // wrangler.staging.json:64
 "triggers": {
-  "crons": ["* * * * *"]
+  "crons": ["*/5 * * * *"]
 },
```

- 原理：CF Scheduled 5 min 触发 + shim 5 min 窗口 → 所有 `*/N` 任务回到原设计频率（等价于根因修复的效果）。
- 代价：延迟任务调度精度从 1 min 降到 5 min。
- 部署命令：`cd blocklets/core/cloudflare && npx wrangler deploy --config wrangler.staging.json`（需在设置了 `CLOUDFLARE_ACCOUNT_ID` / `CLOUDFLARE_API_TOKEN` 的 shell 里执行）。

#### 改动 B（根治）

```diff
 // blocklets/core/cloudflare/shims/cron.ts:107-113
 function shouldRunInWindow(cronExpr: string, date: Date): boolean {
-  for (let i = 0; i < 5; i++) {
-    const check = new Date(date.getTime() + i * 60000);
-    if (matchesCron(cronExpr, check)) return true;
-  }
-  return false;
+  return matchesCron(cronExpr, date);
 }
```

- 原理：CF Scheduled 每分钟触发就判断当前这一分钟是否匹配 cron 表达式。
- 同时补单测：`shouldRunInWindow('0 */5 * * * *', date)` 在连续 60 分钟里恰好 true 12 次。

#### 改动 C（节流）

```diff
 // blocklets/core/cloudflare/worker.ts:1402-1403
-// Recover pending refunds that were missed (e.g. after deploy or failed dispatch)
-const { startRefundQueue } = await import('../api/src/queues/refund');
-await startRefundQueue();
+// Recover pending refunds every 5 minutes to avoid wasting CF Queue ops
+if (new Date().getUTCMinutes() % 5 === 0) {
+  const { startRefundQueue } = await import('../api/src/queues/refund');
+  await startRefundQueue();
+}
```

- 或者更干净的做法：把 `startRefundQueue` 移到 `blocklets/core/api/src/crons/index.ts` 注册为正式 cron job（`time: '0 */5 * * * *'`），删掉 worker.ts 里的硬编码调用。

#### 改动 D（可选，暂缓）

合并 `runAllScheduledJobs` 的 per-queue 循环为一次 `Op.or` 聚合查询：

```ts
const allRows = await Job.findAll({
  where: {
    cancelled: false,
    [Op.or]: [
      { will_run_at: { [Op.lte]: Date.now() }, delay: { [Op.not]: -1 } },
      { delay: -1, created_at: { [Op.lte]: new Date(twoMinAgo) } },
    ],
  },
  order: [['created_at', 'ASC']],
});
```

本地按 `row.queue` 分组 dispatch。

**暂缓原因**：30 个队列共享逻辑，`WHERE` 条件写错可能误派发或乱序派发，风险中等；而且 A+B+C 已足以解告警。

---

### 11. 业务影响分析（具体例子）

#### 例 1 — AI 用量扣费（`credit.consumption`）

- **修复前**：每分钟扫 pending meter events，每条单独 push + 单独 D1 事务。极端情况下 6 次 AI 调用在 10 分钟内产生 6 次独立处理。
- **修复后**：每 10 分钟批量扫一次，一次性 batch 处理 6 条。
- **用户感知**：UI 上余额更新从"近实时"变为"10 分钟窗口"——**AI 用量计费本身就是计量计费，10 分钟延迟是设计值**，并非退化。

#### 例 2 — 自动金库归集（`deposit.vault`）

- **修复前**：每分钟为每个 vault-enabled 币种 push 一次余额检查。3 种币 × 1440 min ≈ 4,320 次链上 RPC/天，绝大多数余额没变。
- **修复后**：每 5 分钟 push，RPC 量降到 864 次/天。
- **用户感知**：商户 Dashboard 里"金库余额"更新滞后从 < 1 min 变为 < 5 min——**冷钱包归集业务语义完全可接受**。

#### 例 3 — 订阅续费重试（`subscription.schedule.retry`）

- **修复前**：5 分钟窗口造成 `next_attempt` 可能被提前触发，破坏指数退避。
- **修复后**：严格按 30 分钟跑。
- **金额/次数**：**完全不变**——是否重试由 `attempt_count >= MAX_RETRY_COUNT` 决定，不由频率决定。

#### 例 4 — 退款恢复（`startRefundQueue`）

- **修复前**：deploy 丢失的 pending refund 最多 1 分钟内被自愈补推。
- **修复后**：最多 5 分钟内自愈。
- **金额/次数**：**完全不变**——`handleRefund` 开头 `if (refund.status !== 'pending') return` 保证幂等。

#### 业务层幂等守卫清单（本次修复安全性的保证）

| 队列 | 幂等守卫位置 |
|---|---|
| refund | `handleRefund` 顶部 status 检查 |
| subscription | `handleSubscriptionJob` 读 `Subscription.status` + `next_attempt` 比较 |
| credit-consumption | MeterEvent `status` 字段状态机 |
| deposit-vault | `handleDepositVaultJob` 先查最新余额和阈值再决定是否发起转账 |
| webhook | 重复投递由商户 endpoint 负责幂等（签名 + timestamp）|
| payment | `handlePayment` 读 `PaymentIntent.status` 跳过已处理 |

因此即使重试或重派发，**不会产生重复扣款/重复退款/重复归集**。

---

### 12. 风险分级

| 风险 | 概率 | 影响 | 缓解 |
|---|---|---|---|
| 改动 A 让 `0 5 */6 * * *` 这种只在单一分钟触发的 cron 漏跑 | 低 | 中（续费提醒邮件漏发）| 5 分钟窗口原本就是为容错设计，`*/5` cron 刚好覆盖 `minute=5/10/…/55`，正常会触发 |
| CF Scheduled 触发本身丢失 | 低（CF SLA 不承诺）| 受影响 cron 周期延后 | 业务层重试与恢复任务（含 `startRefundQueue`、`startSubscriptionQueue`）能兜底 |
| 改动 B 回归后暴露此前被 5× 超频"掩盖"的 race condition | 低 | 视情况 | 合 PR 前在 staging 观察 24h |
| 改动 D 查询合并写错 WHERE 导致误派发 | 中 | 高（可能提前执行延迟任务）| **暂缓，独立 PR + 对比测试**|
| `wrangler.jsonc` / `wrangler.json` / `wrangler.staging.json` 多份配置 drift | 已发生 | 中 | 后续合并为 `wrangler.jsonc` + env 派生 |
| 升级 CF Queues 到付费计划 | 低 | 低 | 不在本次范围内 |

---

### 13. 验证步骤

#### 13.1 A 改动上线后

- 部署后立刻 tail 日志 `npx wrangler tail staging-aigne-hub-payment-kit`，确认 cron 每 5 分钟触发一次。
- 2 小时后拉 GraphQL:
  ```graphql
  query($a:String!,$s:Time!,$u:Time!){
    viewer{accounts(filter:{accountTag:$a}){
      queueMessageOperationsAdaptiveGroups(limit:500,filter:{datetime_geq:$s,datetime_leq:$u}){
        count dimensions{queueId datetimeHour actionType}
      }
    }}
  }
  ```
  变量 `$a` = ArcBlock account id；`$s` = 2h 前；`$u` = 现在。
- 预期：当小时 WriteMessage ≤ 50（= 10/min → 换算 ≤ 14.4k/day，仍超但在下降）。

#### 13.2 完整 24 小时观测

- 次日拉全天 ops，目标 ≤ 3,000/day（write+read+delete 合计）。

#### 13.3 业务巡检

- 在 staging 触发一笔退款 → 观察 < 30s 到账（immediate 路径不受影响）。
- 查 `Subscription` 表里 `next_attempt < now()` 的行，确认最多 5 分钟内被处理。

#### 13.4 B+C 改动合 PR 前

- 补 `shouldRunInWindow` 单测覆盖所有现有 cron 表达式频率。
- staging 部署 24h，重复 13.2 的数据对比，确认 cron 改回 `* * * * *` 后 ops 依然维持在 ~3-4k。

---

### 14. 附录：查询命令（可复用）

> 所有命令需在已加载 `.env.development.local` 的 shell 中执行，环境变量 `CLOUDFLARE_ACCOUNT_ID` 与 `CLOUDFLARE_API_TOKEN` 从该文件读取；**本文档不记录 token 值**。完整策略见 §19。

```bash
# 列出账号内所有队列
curl -s "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/queues" \
  -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" | python3 -m json.tool

# 指定队列详情（producers/consumers）
curl -s "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/queues/$QUEUE_ID" \
  -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" | python3 -m json.tool

# Worker 的 cron schedules
curl -s "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/workers/scripts/$WORKER_NAME/schedules" \
  -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN"

# Worker 的 bindings（含 queue / service / d1 / kv）
curl -s "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/workers/services/$WORKER_NAME/environments/production/bindings" \
  -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" | python3 -m json.tool

# GraphQL：近 N 天按队列 × 日期 × actionType 的 ops 统计
SINCE=$(date -u -v-10d +%Y-%m-%dT%H:%M:%SZ); UNTIL=$(date -u +%Y-%m-%dT%H:%M:%SZ)
curl -s -X POST "https://api.cloudflare.com/client/v4/graphql" \
  -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" -H "Content-Type: application/json" \
  -d "{\"query\":\"query(\$a:String!,\$s:Time!,\$u:Time!){viewer{accounts(filter:{accountTag:\$a}){queueMessageOperationsAdaptiveGroups(limit:1000,filter:{datetime_geq:\$s,datetime_leq:\$u}){count dimensions{queueId date actionType}}}}}\",\"variables\":{\"a\":\"$CLOUDFLARE_ACCOUNT_ID\",\"s\":\"$SINCE\",\"u\":\"$UNTIL\"}}"

# GraphQL：Worker 每日请求量
curl -s -X POST "https://api.cloudflare.com/client/v4/graphql" \
  -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" -H "Content-Type: application/json" \
  -d "{\"query\":\"query(\$a:String!,\$s:Time!,\$u:Time!){viewer{accounts(filter:{accountTag:\$a}){workersInvocationsAdaptive(limit:500,filter:{datetime_geq:\$s,datetime_leq:\$u}){dimensions{scriptName date} sum{requests errors subrequests}}}}}\",\"variables\":{\"a\":\"$CLOUDFLARE_ACCOUNT_ID\",\"s\":\"$SINCE\",\"u\":\"$UNTIL\"}}"

# 回滚 Worker 到上一个 deployment（紧急回退）
npx wrangler rollback --name staging-aigne-hub-payment-kit
```

---

### 17. 详细执行顺序与可行性分析

本节展开 §5 摘要里每一步的**技术可行性、业务安全性、回滚机制、预期耗时**，供具体执行人（运维/工程师）参考。

#### 17.1 可做（业务安全，本次推荐）

**改动 A — 紧急止血：cron 从 `* * * * *` 改成 `*/5 * * * *`**

| 属性 | 值 |
|---|---|
| 本质 | 配置变更（一行），不改业务代码 |
| 触发频率变化 | scheduled handler 从 1440/天 → 288/天 |
| 与现有代码自洽性 | ✅ 和 `shims/cron.ts` 现有 5 分钟窗口天然对齐；所有 `*/N` cron 回到原设计频率 |
| 对计费代码路径 | 无任何影响（数据路径不过 cron 调度） |
| 对延迟任务的影响 | 执行时机最多晚 4 分钟 |
| 可逆性 | 🟢 `wrangler rollback` 1 分钟 |
| 部署耗时 | 15 分钟（含构建、部署、烟测） |
| 前置依赖 | 加载 `.env.development.local` 中的 `CLOUDFLARE_API_TOKEN` / `CLOUDFLARE_ACCOUNT_ID` |
| 风险点 | 仓库存在 4 份 wrangler 配置（`wrangler.json` / `wrangler.jsonc` / `wrangler.staging.json` / `wrangler.local.toml`），必须改 `wrangler.staging.json`；否则"改了不生效" |

**改动 B — 根治：`shouldRunInWindow` 窗口 5min → 1min**

| 属性 | 值 |
|---|---|
| 本质 | 一行逻辑变更 |
| 触发频率变化 | 所有 `*/N` cron 回到原 Blocklet Server 稳定运行多年的频率 |
| 与计费业务关系 | 无——只决定"cron 是否在本分钟执行"，不决定"cron 做什么" |
| 对延迟任务 | 无差异（假设 cron 仍每分钟） |
| 可逆性 | 🟢 revert PR 即可 |
| 代码风险 | 若 CF Scheduled 偶发丢触发（best-effort 语义），缩窗口后该 cron 周期被跳过；业务层所有重试/恢复机制均能兜底 |
| 前置依赖 | 单测覆盖 `shouldRunInWindow('0 */5 * * * *')` 等表达式在 60 分钟里恰好匹配 12 次 |

**改动 C — 节流：`startRefundQueue` 每 5 分钟**

| 属性 | 值 |
|---|---|
| 本质 | 新增一个 `if (minute % 5 === 0)` 或将其移至 cron 注册 |
| 调用频率变化 | 从 1440/天 → 288/天 |
| 对 refund 金额/次数 | 无——`handleRefund` 顶部有 `status !== 'pending'` 守卫，即使误重推也只执行一次 |
| 对 refund 延迟 | 正常路径秒级不变；只有 deploy 丢失的 pending refund 自愈延迟从 ≤ 1min → ≤ 5min |
| 可逆性 | 🟢 revert PR |
| 代码风险 | 若与改动 A 同时生效，`*/5` cron × `% 5 === 0` → 始终命中 → 节流等于没做。**必须选一种实现**，推荐把 `startRefundQueue` 改成正式 cron job (`time: '0 */5 * * * *'`) 置于 `crons/index.ts`，与 CF 触发器解耦 |

#### 17.2 暂缓（本次不做，单独评估）

**改动 D — `runAllScheduledJobs` 合并 D1 查询**

| 属性 | 评估 |
|---|---|
| 收益 | D1 查询从 60/min → 1/min；对队列 ops 影响次要 |
| 风险 | 🟡 中。30 个队列共用一段 `WHERE`；若 `Op.or` 条件或 `ORDER BY` 写错，可能**提前 dispatch 未到期的延迟任务** |
| 业务后果 | 订阅续费提前触发、stripe sync 提前跑、notification 提前发——**可能破坏依赖"精确时间点"的业务语义** |
| 决策 | 告警解除后独立 PR，附对比测试：构造同样数据集，分别跑原循环查询和合并查询，断言两者返回集完全一致 |

**其它暂缓项**

| 项目 | 暂缓原因 |
|---|---|
| `wrangler queues pause-delivery` | 立竿见影降 ops，但消息积压 24h 后被 CF 自动丢弃（`message_retention_period = 86400`），可能导致真实 refund/notification 永久丢失 → **业务不可接受** |
| 升级 CF Queues 付费计划 | 长期方案但不解根因；根因修完后 ops 在免费额度 10× 以内，不必升级 |
| 删除任何生产资源（队列/Worker） | 本次告警源头是 ArcBlock 账号的活跃 Worker，不动它 |
| 修改 `message_retention_period` / `max_batch_size` | 不是根因，调参只是掩盖问题 |

#### 17.3 推荐执行顺序（含风险/回滚）

| 步骤 | 动作 | 耗时 | 业务风险 | 回滚路径 | 前提 |
|---|---|---|---|---|---|
| 1 | 改动 A 上线 `wrangler.staging.json` cron → `*/5` + redeploy | 15 min | 🟢 低（延迟 ≤ 5min） | `wrangler rollback`，1 min | 有 ArcBlock 账号 API Token |
| 2 | 观察 24h 队列 ops GraphQL 曲线 | 24h | —— | —— | 执行 §14 GraphQL 命令 |
| 3 | 开 PR 实现改动 B + 改动 C（合一个 PR）+ 单测 | 0.5-1 天 | 🟢 低 | revert PR | code review 通过 |
| 4 | staging 部署改动 B+C，cron 暂仍为 `*/5`，观察 24h | 24h | 🟢 低 | 同步骤 1 | —— |
| 5 | 把 cron 改回 `* * * * *`，redeploy | 15 min | 🟢 低 | 同步骤 1 | 步骤 4 观测 ops 无回升 |
| 6 | 观察 7 天，确认 ops 稳定 < 5k/天 | 7 天 | —— | —— | —— |
| 7 | （可选）评估改动 D 是否做 | —— | —— | —— | 步骤 6 数据达标后再议 |
| 8 | 补告警监控（Slack/Email 每日 ops 报告） | 1 天 | 🟢 低 | —— | —— |

#### 17.4 禁止事项

- ❌ **绝不**一次 PR 把 A+B+C+D 全改上线。合并风险远大于收益。
- ❌ **绝不**在未加载 `.env.development.local` 的情况下运行 `wrangler deploy`——可能误部署到错误的账号。
- ❌ **绝不**改动任何 `blocklets/core/api/src/queues/*.ts` 里 `handle*Job` 的金额/状态迁移逻辑。
- ❌ **绝不**调整 `handleRefund`、`handleSubscriptionJob`、`handleDepositVaultJob` 的幂等守卫（`if (status !== 'pending') return` 等）。

---

### 18. 承诺阈值（SLA Commitments）

本次修复对业务方/产品方的可量化承诺。签字生效后，代码 reviewer 必须确保实现方案不会违反这些阈值。

#### 18.1 硬约束（违反即必须拒绝合并 PR）

| # | 承诺项 | 阈值 |
|---|---|---|
| H-1 | **任何用户的扣款金额、扣款次数** | 不因本次修复而变化。0 个差异。 |
| H-2 | **订阅续费的目标金额、周期、币种** | 不变。 |
| H-3 | **退款金额** | 不变。总额与次数 0 差异。 |
| H-4 | **链上转账（归集、stake 退还、credit burn）总笔数** | 不因修复新增或减少任何一笔。 |
| H-5 | **历史 `payment_intents` / `invoices` / `refunds` / `balance_transactions` 记录** | 不触碰，不重写。 |
| H-6 | **DLQ 消息数** | 修复后相对修复前 **不增加**（可以减少或持平）。 |
| H-7 | **对账报表数据完整性** | 修复前后同一时间窗查询出的 sum(amount) 相等。 |

#### 18.2 软约束（允许范围内漂移）

| # | 指标 | 修复前 | 承诺阈值（修复后） |
|---|---|---|---|
| S-1 | 延迟任务（订阅重试、退款重试）的执行时刻漂移 | < 1 min | **≤ 5 min**（改动 A 阶段），**≤ 1 min**（完成 B 后） |
| S-2 | 续费提前通知邮件发送时刻偏差 | < 6 h 内准时 | **≤ 6 h ± 5 min** |
| S-3 | 自动金库归集检查间隔 | 1 min | **≤ 5 min** |
| S-4 | AI 用量扣费批处理窗口 | 被 bug 压缩到 ~1 min | 恢复到设计值 **10 min** |
| S-5 | Stripe 同步延迟 | 最多 `*/20` 或 `*/30` 设计值 | 同设计值，**无退化** |

#### 18.3 运维约束

| # | 指标 | 阈值 |
|---|---|---|
| O-1 | CF Queue 日 ops（稳态）| ≤ 5,000 / day |
| O-2 | CF Queue 月度 ops（占 1M 免费额度）| ≤ 20% |
| O-3 | 告警触发频次 | 修复后 30 天内 = 0 |
| O-4 | Worker 每次 scheduled 事件 CPU 时间 | ≤ 30s（CF 限制 30s） |
| O-5 | DLQ 消息 retention | 保持 1 天（不调） |

**违反任一硬约束（H-*）→ 不合并 PR；**
**违反任一软约束（S-*）超过 ±50% → 需 product owner 审批；**
**违反任一运维约束（O-*）→ 触发回滚流程。**

---

### 19. 敏感信息与环境变量约定

#### 19.1 本文档的敏感信息策略

> **文档仅引用环境变量 NAME，不落盘 VALUE。** 任何包含 token / secret / password 的值都**不得**写入本文档或任何 git 跟踪文件。

本文档中 `$CLOUDFLARE_API_TOKEN`、`$CLOUDFLARE_ACCOUNT_ID`、`$WORKER_NAME`、`$QUEUE_ID` 等以 `$NAME` 形式出现的引用，约定由读者自行在本地 shell 中加载，来源如下表：

| 变量名 | 是否敏感 | 来源 | 说明 |
|---|---|---|---|
| `CLOUDFLARE_API_TOKEN` | ✅ **高度敏感** | `blocklets/core/.env.development.local`（**本地、git-ignored**）| 具备 queue/worker/d1 写权限，泄露等于账号被接管。绝不落盘。 |
| `CLOUDFLARE_ACCOUNT_ID` | ⚠️ 非敏感但区分环境 | 同上，或 CF Dashboard URL | ArcBlock 主账号 ID 在本文档 §7/§8 已明示（属 Worker 配置的一部分，出现在 wrangler.json）|
| `WORKER_NAME` | ⚠️ 非敏感 | `wrangler.staging.json` 中 `name` 字段 | 如 `staging-aigne-hub-payment-kit` |
| `QUEUE_ID` | ⚠️ 非敏感 | `wrangler queues list` 输出 | 用于 GraphQL 过滤 |

#### 19.2 使用流程（执行本文档任何命令前）

```bash
# 方法 1：临时导出（仅当前 shell 生效，关窗即失效，推荐）
export $(grep -v '^#' blocklets/core/.env.development.local | xargs -I{} echo {})

# 方法 2：按需只取这两个变量
export CLOUDFLARE_ACCOUNT_ID=$(grep '^CLOUDFLARE_ACCOUNT_ID=' blocklets/core/.env.development.local | cut -d= -f2)
export CLOUDFLARE_API_TOKEN=$(grep '^CLOUDFLARE_API_TOKEN=' blocklets/core/.env.development.local | cut -d= -f2)

# 验证（应输出 "✓ token loaded"，不打印 token 值）
[ -n "$CLOUDFLARE_API_TOKEN" ] && echo "✓ token loaded"
```

#### 19.3 泄露应急处理

若 token 意外被提交到 git / 粘贴到 chat / 出现在日志：

1. 立即在 Cloudflare Dashboard → My Profile → API Tokens 中 **Revoke** 该 token。
2. 生成新 token，更新 `.env.development.local`。
3. 清洗 git 历史（`git filter-branch` 或 BFG）；若已 push 到远端，通知 GitHub 做 secret scanning。
4. 在 `#security` 频道报备。

#### 19.4 文档审阅 checklist

合并/更新本文档时，reviewer 必须逐项核对：

- [ ] 无 token / secret / password 明文
- [ ] 无数据库连接串 / API key 明文
- [ ] 无 JWT / session token 明文
- [ ] 所有 `$NAME` 引用在 §14/§19.1 或等价位置有说明
- [ ] 示例命令默认从 env 读取变量，不硬编码值

---

### 20. 分析过程贡献记录

| 环节 | 结论贡献者 | 证据 |
|---|---|---|
| 代码走读锁定 `shouldRunInWindow` 5× 放大 | Claude（代码分析）| `shims/cron.ts:107-113` + 各 cron 表达式 |
| 质疑 "是 queue 告警吗？" 转方向用线上数据证伪/证真 | Pengfei | 告警截图（CF 邮件）+ 提供 env token |
| GraphQL 拉到 10 天日度 + 小时级分布 | Claude（API 调用）| CF GraphQL Analytics `queueMessageOperationsAdaptiveGroups` |
| 确认 ArcBlock 主账号而非个人账号 | Pengfei | 告警邮件指向 "(ArcBlock)" |
| 幂等安全性核对 | Claude（查 `handleRefund`、`handleSubscriptionJob`、`handleDepositVaultJob`）| `blocklets/core/api/src/queues/*.ts` 各 onJob 顶部 status 守卫 |
| 业务场景示例（订阅/退款/credit）| 共同拆解 | 代码 + env.ts cron 常量 |
| §17 可行性 / §18 承诺阈值 / §19 敏感信息约定 | Pengfei 要求 + Claude 起草 | 本次对话回顾 |

---

### 21. 变更记录

| 日期 | 内容 | 状态 |
|---|---|---|
| 2026-04-17 | 根因定位 + 方案初稿 + 线上数据采集 | ✅ 完成 |
| 2026-04-17 | 本文档产出 v1 | ✅ 完成 |
| 2026-04-17 | v2：补入 §17 详细可行性、§18 承诺阈值、§19 敏感信息约定；章节重编号 §20 §21 | ✅ 完成 |
| 2026-04-17 13:13 UTC | 改动 A 上线（通过 CF API PUT schedules，未重部署代码）：cron `* * * * *` → `*/5 * * * *`。当前活跃 deployment_id=`88e327a2-e22f-4d77-bc7f-5bb9df638f96` / version_id=`2b0d89b9-e0cf-4a6b-81b1-236ec2315ab5`（Shijun 2026-04-15 部署的版本，未动）。repo 中 `wrangler.staging.json:64` 已同步改为 `*/5`（未 commit）。| ✅ 完成 |
| 2026-04-18 | Plan 2 阶段性数据（04-18 前 2h）：04-18 00:00-02:00 UTC 队列 ops = 2,472，方向符合预期，授权开始 Plan 3+4 代码工作。| ✅ 阶段性判据通过 |
| 2026-04-18 | 改动 B（`shims/cron.ts:107-113` 窗口 5min→1min）+ TDD 单测（`cloudflare/tests/shims/cron.spec.ts`，30 测试全过）| ✅ 本地完成 |
| 2026-04-18 | 改动 C（`startRefundQueue` 迁移到 `crons/index.ts` 注册为 `refund.recovery` cron job，时间表 `0 */5 * * * *`；`worker.ts:1402-1403` 的硬编码调用已删除）| ✅ 本地完成 |
| 2026-04-18 | PR #1341 已提交（分支 `fix/cf-queues-cron-window-bug`，4 commits：改动 B / 改动 C / wrangler.staging 同步 / 本文档）| ✅ 已开 PR |
| 2026-04-19 | Plan 2 完整 24h 复核：04-18 全天 10,007 ops、04-19 全天 10,483 ops，**稳态降幅 ~23-27%**（远低于初期推算的 80%）。原因：ops 主要来自业务事件批量 push，非 cron 稳态空跑；Plan 1 只改了 cron 频率，未修代码级 5× 放大 bug，故降幅与 cron 次数降幅部分叠加但有限。| ✅ 数据校准 |
| 2026-04-19 | 新发现：Worker `exceededResources` 事件持续高发。04-17：1,022 次（35% 超限率）；04-18：309 次；04-19：300 次 = 每小时 12 次 = 精确匹配 `*/5` cron 频率，意味着**每一次 scheduled handler 触发都 CPU/subrequest 超限被 CF 截断**。影响：`runAllScheduledJobs` 可能扫不完 30 个队列（靠后的队列永远扫不到）→ 业务侧延迟任务（refund 重试、subscription 续费重试等）存在被静默拖延的风险。这把 PR #1341 的定位从"预防性 cleanup"升级为"修复 scheduled handler 可靠性 + 降低 ops"。| ⚠️ 新发现 |
| 2026-04-20 | 月度累计复核：payment-kit-jobs 本月 101,306 ops + DLQ 35，合计约 101k / 1M 免费额度 = **10.1%**。按当前 ~10.5k/day 外推月底约 300k / 1M = 30%，**不会再触发 95% 告警**。告警风险短期解除，但代码级根因未修。| ✅ 阈值下行 |
| 2026-04-20 | PR #1341 review 反馈（P1-1，有效）：shim 1min 窗口 × CF 触发 `*/5` 会让非 5 对齐的 cron（如 `paymentStatCronTime='0 1 0 * * *'`、`expiredSessionCleanupCronTime='0 1 * * * *'`）永不运行。修复：新增 commit 把 `wrangler.staging.json` 回到 `* * * * *`，使部署后 CF 触发和 shim 窗口同时恢复到设计态（1min × 1min）；新增 4 条非 5 对齐 cron 的守卫测试。Plan 6（回 `*/1` cron）已折叠进 PR 本身。| ✅ 按 review 修复 |
| 2026-04-20 | PR #1341 review 反馈（P1-2，非本 PR 引入）：`patch-user-card.ts` 的非存在导入路径来自独立 commit `e9764188`（2026-04-18 00:41 UTC），本地 `feat/cf-workers-migration` 已有但未 push 到 origin，故 PR diff 把它一并显示。处置：由该 commit 作者独立评估路径有效性，不在本 PR 范围。| ℹ️ 范围说明 |
| 2026-04-20 | PR #1341 第二轮 review（P1-2 retracted）：reviewer 验证 `@arcblock/ux/lib/node_modules/@blocklet/js-sdk/dist/index.js` 在实际安装布局中确实存在（UserCard/utils.js 自己就这么导入），故撤回 build blocker 判定。| ✅ 核实 |
| 2026-04-20 | PR #1341 review P2-1 修复：`cronInstance.runAll()` 改为接受可选 `now` 参数；`worker.ts` scheduled handler 传入 `new Date(event.scheduledTime)`，避免 CF 晚投递导致跨分钟边界时匹配错位。新增 2 条集成测试覆盖该场景（测试总数 34 → 36 全过）。| ✅ 按 review 修复 |
| 2026-04-20 | PR #1341 review P2-2 修复：更新文档 §3（改动清单改为"历史/当前/最终"三态表）、§3.1（新增目标终态声明）、§4（指标对比加入 04-16 基线/应急态/预期三列）、§5（执行步骤按实际进度重写）。明确 `*/5` cron 只是 2026-04-17 当日的历史应急，PR 合入后不应再执行。| ✅ 按 review 修复 |
| （待定）| PR #1341 review（建议由 Shijun，他最熟 CF 部署）| ⏳ 等外部 |
| （待定）| Plan 5 staging 部署 + 冒烟测 + 2h/24h 数据验证（含 exceededResources 降幅核查）| ⏳ 等 PR 合入 |
| （待定）| Plan 6 cron 回到 `*/1` 恢复精度 | ⏳ 等 Plan 5 稳定 24h |
| （待定）| Plan 7 日度 ops 监控接入 | ⏳ 随时可做 |
| （待定）| Plan 8 `runAllScheduledJobs` D1 查询合并（可能从暂缓提前为必做，取决于 Plan 12 验证结果）| ⏳ 条件触发 |
| （待定）| Plan 9 wrangler 四份配置统一 | ⏳ 等 Plan 6 |

---

### 17 / 18 / 19 的读者快速索引

- 要**决定做不做某一项改动**？ → §17.1 / §17.2
- 要**给业务方/上级汇报这次修复"承诺不会出错什么"**？ → §18.1 硬约束
- 要**向测试或代码 reviewer 说明"合什么样的 PR 才安全"**？ → §17.4 禁止事项 + §18 全部
- 要**在本地跑查询命令但不知道变量怎么来**？ → §19.2 使用流程
- 要**确认本文档是否有泄密风险**？ → §19.4 审阅 checklist
