feat(creation): 新增 patbond-creation 模块骨架——端点、租约队列 Worker、provider 适配层(T4-02)
CI / backend-test (push) Successful in 5m43s
CI / backend-test (push) Successful in 5m43s
M4 新模块,:8085,前缀 /api/v1/creation/**(ADR-023 §2,E3 拍板方案 A)。 它编排生成任务但不调用 AI 服务商——上游调用在独立的 patbond-ai(Python) 仓库,经 REST 到达。 落地内容: - 客户端端点 4 个(创建/查询/列表/取消)+ 目录端点。创建强制 Idempotency-Key (ADR-019:生成在上游要花钱,重试绝不能变成第二个计费任务) - 租约队列仓储:FOR UPDATE SKIP LOCKED 领取 + 租约续期 + 回收器。目标模型 只给了索引没给回收器代码,这里补上 - Worker 三条独立调度通道(提交/轮询/回收),任一卡住不饿死其余两条。 轮询与续租在同一语句内完成——分开做会让长任务在跑时租约过期被抢, 那正是重复扣费的经典路径 - provider 适配层接口按「两阶段 submit/poll」定形 + 两个实现位 - 服务身份 JWT 签发器(独立密钥对) - common 增 3 个错误码(40407/40408/42206);配额与限流的两个 429 码随 配额工单落地,此处不加死枚举 关键实现纪律(注释中已逐条说明原因): - provider_request_id 在上游产生任何计费动作之前落库。顺序反过来,每次 超时都会变成重复扣费 - 租约回收时刻意不清 provider_request_id:保留它,下次尝试才知道「已提交过, 该轮询而非重发」 - provider 按 provider_code_snapshot 而非当前目录解析,故目录改指向后 在途任务仍由原适配器轮询 - Worker 各 pass 捕获异常:Spring 会取消抛异常的 fixedDelay 任务, 不捕获等于一次偶发失败静默停掉整个队列 刻意未实现且会响亮失败(不装作能用):provider 两个实现位(待服务商文档 确认幂等键支持、查询接口与频率限制、单任务最长时长、产物返回形式、 限流与配额响应形态五项)、预签名上传目标获取、产物入 media.assets (后两者取决于挂起中的对象存储决策,ADR-023 §7)。 共享物按 ADR-023 §2 复制 3 件(JWT 校验栈、UuidV7、RequestHashes); **存储适配层不复制**——presignPut 已存在,产物走预签名上传由 patbond-user 签发,故无需第 4 份存储层,也使 patbond-ai 零对象存储凭证。 测试:新增 GenerationJobQueueIntegrationTest 11 项,全部针对「写错了也 能跑、只在生产表现为丢任务或重复扣费」的语义:SKIP LOCKED 不把同一行 交给两个 worker、轮询确实续了它依赖的租约、回收器保留 provider_request_id、 first_attempt_at 跨重排队存活、write-once 保护、取消被状态守卫。 全量 393 → 404,零失败零跳过。 过程中被测试抓出两个真问题:UPDATE...FROM 的 RETURNING 列表未限定别名 导致 id 歧义(列表已改为单一来源派生两种形式,避免漂移);以及夹具伪造 了物理上不可能的历史(租约过期但 started_at 是当下),被 ck_generation_jobs_lease 正确拒绝。 Readme.md 全文改中文并补齐 M2 之后的内容:原文停在 M2(pet 骨架阶段)、 完全没有 patbond-community、配置表缺 M2 之后全部条目。现覆盖六模块、 迁移链归属说明、两对密钥的区别与原因、以及各服务端点。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
server:
|
||||
port: ${PATBOND_CREATION_PORT:8085}
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: patbond-creation
|
||||
datasource:
|
||||
# 与 patbond-user 共库(MVP 单库多 schema);本服务只读写 creation schema,
|
||||
# 并只读 community.posts 的 generation_job_id 关联(建草稿由 community 端点完成)。
|
||||
# Flyway 迁移链(V1..V6,含 creation 基线)由 patbond-user 启动时统一执行,
|
||||
# 本服务不携带 Flyway —— 单一 flyway_schema_history 归属不拆(ADR-023 §2)。
|
||||
url: ${PATBOND_DB_URL:jdbc:postgresql://127.0.0.1:5432/patbond}
|
||||
username: ${PATBOND_DB_USER:patbond}
|
||||
password: ${PATBOND_DB_PASSWORD:patbond}
|
||||
|
||||
# /api/v1/** 业务端点自骨架起即接 RS256 校验,与 user/pet/community 同一约定。
|
||||
patbond:
|
||||
jwt:
|
||||
# RS256 公钥,用于本地校验 patbond-auth 签发的 access token。
|
||||
# 值可以是 PEM 文件路径,也可以是内联 PEM 内容(以 -----BEGIN 开头)。
|
||||
# 私钥只给 patbond-auth,绝不入库。
|
||||
public-key: ${PATBOND_JWT_PUBLIC_KEY:}
|
||||
# /internal/** 服务间共享密钥(Java↔Java 既有通道),需与 patbond-user 同值。
|
||||
# 注意:调用 patbond-ai 用的**不是**这个 token,而是下面的服务身份 JWT
|
||||
# ——两套机制并存的原因见 ADR-023 §5。
|
||||
internal-token: ${PATBOND_INTERNAL_TOKEN:dev-only-internal-token}
|
||||
user-service:
|
||||
# 预签名上传凭证来源:patbond-user 的 /internal/media/**(ADR-017 边界:
|
||||
# 媒体上传流程归 user,本服务不直连对象存储、也不持有其凭证)。
|
||||
url: ${PATBOND_USER_SERVICE_URL:http://127.0.0.1:8082}
|
||||
|
||||
creation:
|
||||
# ---- 上游生成服务(patbond-ai,Python)----
|
||||
ai-service:
|
||||
url: ${PATBOND_AI_SERVICE_URL:http://127.0.0.1:8000}
|
||||
# 单次 HTTP 调用超时。注意这是**提交/查询**的超时,不是生成耗时——
|
||||
# Java↔Python 边界一律异步(ADR-023 §4),故此值与出图出片时长无关,
|
||||
# 不得为了「等出图」而调大。
|
||||
connect-timeout: ${PATBOND_AI_CONNECT_TIMEOUT:2s}
|
||||
read-timeout: ${PATBOND_AI_READ_TIMEOUT:5s}
|
||||
|
||||
# ---- 服务身份 JWT(ADR-023 §5)----
|
||||
# **独立密钥对**,与用户 token 的密钥对不同。理由:现有 JwtVerifier 只验
|
||||
# 签名、不校验 aud/iss,用户 token 亦无 aud claim;若复用同一密钥对,
|
||||
# 四个模块都会把服务票当合法用户 token 接受。独立密钥对无需改动任何
|
||||
# 已发布的验证器。私钥只给本服务,公钥给 patbond-ai。
|
||||
service-token:
|
||||
private-key: ${PATBOND_SERVICE_JWT_PRIVATE_KEY:}
|
||||
issuer: ${PATBOND_SERVICE_JWT_ISSUER:patbond-api}
|
||||
audience: ${PATBOND_SERVICE_JWT_AUDIENCE:patbond-ai}
|
||||
subject: ${PATBOND_SERVICE_JWT_SUBJECT:service:creation-worker}
|
||||
# 短有效期:服务票每次调用现签,不缓存、不复用。
|
||||
ttl: ${PATBOND_SERVICE_JWT_TTL:5m}
|
||||
|
||||
# ---- Worker 与租约(ADR-023 §3/§4)----
|
||||
worker:
|
||||
# 关掉后本服务只提供读写端点、不消费队列(便于单实例调试与测试隔离)。
|
||||
enabled: ${PATBOND_CREATION_WORKER_ENABLED:true}
|
||||
# 调度器 tick 间隔。真正的轮询节律由 next_attempt_at 控制,
|
||||
# tick 只决定「最快多久发现一件该做的事」。
|
||||
tick-interval: ${PATBOND_CREATION_TICK_INTERVAL:3s}
|
||||
# 单 tick 最多领取多少条,防止一个实例吃光整个队列。
|
||||
batch-size: ${PATBOND_CREATION_BATCH_SIZE:5}
|
||||
# 租约时长。**必须大于最长的单次轮询间隔**(见下 poll-interval-late),
|
||||
# 否则会出现「任务仍在跑但租约过期被他人抢占」。每次轮询顺带续租。
|
||||
lease-ttl: ${PATBOND_CREATION_LEASE_TTL:60s}
|
||||
|
||||
# 服务端轮询 patbond-ai 的退避节律(ADR-023 §4)。视频任务绝对次数很多,
|
||||
# 固定 3s 会产生数百次无谓轮询与续租写入。
|
||||
poll-interval-early: ${PATBOND_CREATION_POLL_EARLY:3s} # 前 60 秒
|
||||
poll-interval-mid: ${PATBOND_CREATION_POLL_MID:10s} # 60s ~ 5min
|
||||
poll-interval-late: ${PATBOND_CREATION_POLL_LATE:30s} # 5min 以后
|
||||
poll-early-until: ${PATBOND_CREATION_POLL_EARLY_UNTIL:60s}
|
||||
poll-mid-until: ${PATBOND_CREATION_POLL_MID_UNTIL:5m}
|
||||
|
||||
# 失败重试的指数退避基数(实际间隔 = base * 2^(attempt-1))。
|
||||
retry-backoff-base: ${PATBOND_CREATION_RETRY_BACKOFF:30s}
|
||||
|
||||
# 客户端硬超时由服务端下发而非客户端硬编码(ADR-023 §10):图片与视频
|
||||
# 差一个量级,硬编码两个数会使换 provider 就要发版。
|
||||
client-timeout-image: ${PATBOND_CREATION_CLIENT_TIMEOUT_IMAGE:5m}
|
||||
client-timeout-video: ${PATBOND_CREATION_CLIENT_TIMEOUT_VIDEO:20m}
|
||||
# 任务被判定为僵死的阈值(running 超过此时长)。对应客户端的第六个 UI 态,
|
||||
# 它不是错误态(ADR-023 §10)。
|
||||
stale-after-image: ${PATBOND_CREATION_STALE_IMAGE:10m}
|
||||
stale-after-video: ${PATBOND_CREATION_STALE_VIDEO:40m}
|
||||
Reference in New Issue
Block a user