Files
agentic-coding-workflow/docs/jobs.md
T

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。
  • WebhookWebhookNotifier 注册到通知 dispatcher 之上,按 topic 白名单 enqueue webhook.deliver job;handler 用 HMAC-SHA256 签名后 POST。
  • 死信兜底:webhook 进入 dead 状态时会写一条 audit、并向所有 admin 用户 fan-out 一条 inApp 通知,避免外部投递失败被静默吞掉。

配置

配置在 config.yamljobs: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=falseWebhookNotifier.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 -lhttpbin.org/status/500 作为 webhook 接收端。

  • 关 webhooknotify.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 + intervalSELECT * FROM jobs WHERE status='running' 应为空(被 D2 reaper 复位为 pending)。
  • purge:手工把一行的 completed_at 改成 8 天前 → 等下一轮 job_purge tick → 该行被物理删除。

常见排障

现象 排查思路
jobs 表只增不减 检查 job_purge.enabledcompleted_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 投递 handler
  • internal/jobs/runners/:5 个周期 runner
  • internal/app/app.go:runner 与 dispatcher 的 wiring + 关停顺序
  • migrations/0005_jobs.up.sql:表结构