You've already forked agentic-coding-workflow
6.5 KiB
6.5 KiB
后台 Job Runner 操作指南
ACW 使用一个 DB-backed 的 job 队列 + 周期 runner 处理后台任务:workspace fetch、worktree prune、附件清理、job reaper、job purge,以及 webhook 投递。本文档面向运维:如何启用、 如何排障、如何手动验收。
总览
- 队列存储:表
jobs(payload JSONB),SELECT FOR UPDATE SKIP LOCKED并发租约。 - Worker 池:
jobs.workers个 goroutine 拉取并执行;handler 失败按指数退避重试,超过max_attempts或 handler 返回Permanent错误进入dead状态。 - 周期 runner:每个 runner 由
time.Ticker触发;panic 由 scheduler 捕获不影响其他 runner。 - Webhook:
WebhookNotifier注册到通知 dispatcher 之上,按 topic 白名单 enqueuewebhook.deliverjob;handler 用 HMAC-SHA256 签名后 POST。 - 死信兜底:webhook 进入 dead 状态时会写一条 audit、并向所有 admin 用户 fan-out 一条 inApp 通知,避免外部投递失败被静默吞掉。
配置
配置在 config.yaml 的 jobs: 与 notify.webhook: 段,默认值见 config.example.yaml。
jobs:
enabled: true # 整体开关;false 时不启动 scheduler/worker
workers: 2 # worker 池大小
shutdown_grace: 30s # 关停时等待 worker 排空的最长时间
workspace_fetch: # A:每个 idle workspace 跑 git fetch
enabled: true
interval: 5m
worktree_prune: # B:两阶段清理 idle worktree
enabled: true
interval: 24h
idle_threshold: 720h # 30d:超过即可被 prune
warning_lead: 72h # 3d:在 prune 前 72h 发警告通知
attachment_cleanup: # C:删除软删会话或孤儿态附件
enabled: true
interval: 24h
retention: 720h # 30d
job_reaper: # D:将超过 lease_timeout 仍未完成的 running job 回退为 pending
enabled: true
interval: 1m
lease_timeout: 5m
job_purge: # 物理清理 completed/dead 旧记录
enabled: true
interval: 24h
completed_retention: 168h # 7d
notify:
webhook:
enabled: true
url: https://hooks.example.com/incoming
secret: ${WEBHOOK_SECRET}
timeout: 10s
topics: # 白名单:未列出的 topic 不会进 jobs 表
- workspace.sync_failed
- worktree.prune_pending
notify.webhook.enabled=false 时 WebhookNotifier.Send 直接 short-circuit,不会写
jobs 表。
Webhook 协议
handler 对每个 webhook job POST 一次:
| Header | 说明 |
|---|---|
Content-Type |
application/json |
X-ACW-Topic |
通知 topic(同 body 中 topic 字段) |
X-ACW-Event-Id |
job UUID,用于接收方去重 |
X-ACW-Timestamp |
Unix 秒,签名 payload 的一部分 |
X-ACW-Signature |
sha256=<hex>,HMAC-SHA256(<ts>. + body) |
响应处理:
- 2xx → 标记完成
- 4xx → 永久失败(
Permanent),直接 dead-letter - 5xx / 网络错误 → 退避重试,超过
max_attempts后 dead-letter
接收方验签示例(Python)
import hmac, hashlib, time
def verify(secret: str, headers: dict, body: bytes) -> bool:
ts = headers["X-ACW-Timestamp"]
sig = headers["X-ACW-Signature"]
if abs(time.time() - int(ts)) > 300:
return False
mac = hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256)
expected = f"sha256={mac.hexdigest()}"
return hmac.compare_digest(expected, sig)
手动验收清单
每次较大改动后按以下步骤过一遍。前置:本地起一个 nc -l 或 httpbin.org/status/500
作为 webhook 接收端。
- 关 webhook(
notify.webhook.enabled=false)→ 触发一次 fetch 失败 → 用户收到 inApp 通知,但SELECT count(*) FROM jobs WHERE type='webhook.deliver'应为 0。 - 开 webhook + 故意写错 URL(指向
localhost:1等不可达端口)→ 触发一条workspace.sync_failed→ 看到 5 次重试日志(job.fail_retry)+ 1 条status='dead'行 + 所有 admin 收到webhook.dead_letter兜底通知。 - prune 警告阶段:手工
UPDATE workspace_worktrees SET last_used_at = now() - INTERVAL '28 days' WHERE id = '<id>'→ 等下一轮 worktree_prune tick → 收到 inApp 警告 + 该行prune_warning_at被写入。 - prune 复位:接着对该 worktree acquire → release 一次 →
prune_warning_at被清空、last_used_at推到 now。 - prune 执行阶段:
UPDATE workspace_worktrees SET last_used_at = now() - INTERVAL '31 days' WHERE id = '<id>'→ 等下一轮 tick → 真删 worktree(验证目录 消失 + DB 行删)。 - reaper 复位:
kill -9进程模拟崩溃,重启后等lease_timeout + interval→SELECT * FROM jobs WHERE status='running'应为空(被 D2 reaper 复位为pending)。 - purge:手工把一行的
completed_at改成 8 天前 → 等下一轮 job_purge tick → 该行被物理删除。
常见排障
| 现象 | 排查思路 |
|---|---|
jobs 表只增不减 |
检查 job_purge.enabled 和 completed_retention;看 job_purge runner 日志 |
| webhook 永远收不到 | 检查 notify.webhook.enabled、URL 可达性、topic 是否在白名单 |
大量 status='dead' |
看 last_error;典型是接收端持续 5xx 或签名校验不过 |
| Worker 卡住不出 job | 看 lease_timeout,可能被 reaper 复位中;或 pool 已满(workers 配置过小) |
关停时 jobs.module.shutdown_timeout |
增大 shutdown_grace,或检查是否有 handler 不响应 ctx 取消 |
相关代码索引
internal/jobs/:核心包(domain / repository / scheduler / worker / module / notifier)internal/jobs/handlers/webhook.go:webhook 投递 handlerinternal/jobs/runners/:5 个周期 runnerinternal/app/app.go:runner 与 dispatcher 的 wiring + 关停顺序migrations/0005_jobs.up.sql:表结构