diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 80f3f6cc2..00c10cf92 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -197,6 +197,8 @@ ## 2026-07-13 角色动作 BgFilter 全帧流水线与单次重试 +> 后续更正(2026-07-23):本条「每次 BgFilter 调用失败后立即重试 1 次」与「不新增供应商进程锁或全局 Semaphore」已被 2026-07-21 起的唯一 `bgfilter-worker` 架构取代。重试所有权迁入子 worker:对一次逻辑调用最多两次顺序 provider attempt,provider 并发由 worker 进程内 `Semaphore(N)`(生产 `N=16`)约束;父侧不重试已被 worker 接收的内部 RPC,仅 TCP 连接从未建立的失败按调度方案 §5.1 有界重连(见 2026-07-23「BgFilter 父侧连接失败有界重连与冷启动宽限」条目)。全帧独立流水化、失败排空与整任务失败退款的语义保留。下文保留作历史记录。 + - 背景:角色动作抽帧后原先固定 `buffered(3)`,并在整批绿幕源帧串行落 OSS 后才开始抠图;每帧还单独创建 HTTP Client。公网 BgFilter 的网络等待会让服务端推理队列出现空档,且首个最终错误会通过 `try_collect` 提前取消 api-server 中其余已发 Future。 - 决策:BgFilter HTTP Client 在 `AppState` 中统一创建并复用 keep-alive 连接池;每次 BgFilter 调用失败后立即重试 `1` 次,两次都失败才进入既有“阿里云通用抠图 → 本地键色”降级链,每次已发失败调用都保留审计。角色动作全部 `32 / 40 / 48` 帧按“单帧绿幕源图落 OSS → BgFilter/降级 → 透明帧落 OSS”独立流水化,使用覆盖本次全部帧的 `buffer_unordered` 连续发射并携带原始帧序,完成后排序;不在 api-server 新增供应商进程锁或全局 Semaphore。任一帧最终失败时先排空全部已启动 Future,再让整个动作任务失败退款,不发布缺帧动画。 - 影响范围:`server-rs/crates/api-server/src/state.rs`、`server-rs/crates/api-server/src/editor_project.rs`、`server-rs/crates/api-server/src/character_animation_assets.rs`、后端架构文档和图片画布技术文档。 @@ -4433,7 +4435,7 @@ - 冷启动宽限:重连配额按「本进程是否已连通过 worker」(收到任意 HTTP 响应即算,`AppState` 级标记)分档——冷启动档 flat 22.5s / complex 约 62.5s(覆盖开机竞态与慢开机),常规档 flat ≤1.5s(不侵蚀 39s fallback 预留)/ complex 22.5s(覆盖 `RestartSec=5s`+ 启动窗);档位单次调用内锁定。新增 `bgfilter_internal_connect_retry_total{mode}` 指标。 - 平台差异(Windows 开发环境):连接已关闭的 loopback 端口不回 RST 而是挂到 connect timeout,错误呈现为 `deadline_exceeded` 且 `is_connect` 为真;重连判定只看 connect 分类,不看错误码。生产 Linux 即时拒绝,呈现 `internal_error`。 - 安全不变量测试:除配额 / 跨窗 / deadline 地板路径外,专项覆盖「TCP 已 accept、未回任何 HTTP 字节即断开 → 不得发起第二次连接」,以 mock listener 的 accept 计数证明父侧未重连。 -- 影响范围:`server-rs/crates/api-server/src/bgfilter_worker.rs`、`state.rs`、`editor_project.rs`、调度方案 §5.1/§7/§9.3/§11、数据契约 247-248 行、运维文档及各编辑器专题文档的旧禁令措辞统一改为「不重试已被 worker 接收的内部 RPC」。 +- 影响范围:`server-rs/crates/api-server/src/bgfilter_worker.rs`、`state.rs`、`editor_project.rs`、调度方案 §5.1/§7/§9.3/§11、数据契约「BgFilter 连接复用、超时与动作帧流水线」条目、运维文档及各编辑器专题文档的旧禁令措辞统一改为「不重试已被 worker 接收的内部 RPC」。 ## 2026-07-23 生产 API 发布按实际路径渲染 worker systemd unit diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index bb4e62b99..12d57bb99 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -607,7 +607,7 @@ Nginx 与 Pingora 在维护 marker 存在时对内网来源绕过整站维护闸 BgFilter 受限资源调度使用非模板单实例 `genarrative-bgfilter-worker.service`,固定以 `GENARRATIVE_PROCESS_ROLE=bgfilter-worker` 监听 `127.0.0.1:8083`,不挂 Nginx 或公共路由。`api-server.env` 是父侧与子 worker 的共享基础,集中保存 provider、OSS、冻结的 `GENARRATIVE_BGFILTER_WORKER_CONCURRENCY=16`、`GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS=5000`、内部 worker 地址、Token 文件和连接超时;BgFilter unit 先加载它,再加载只含 `HOST / PORT / MAX_REQUESTS`、flat / complex 统一熔断阈值 / cooldown 和可选日志覆盖的 `/etc/genarrative/bgfilter-worker.env`,其中两种模式共享参数但状态独立,阈值默认 `3`、cooldown 默认 `120s`,`MAX_REQUESTS` 默认 `2048` 且只作 admission 保险丝。`N / est` 是父侧派生 `callBudgetMs`、子侧派生 attempt 与队列估时的共同输入,专属 env 不得重复覆盖;同一 provider、OSS、Token 或预算基础配置也只保留一份。外部生成 worker unit 会在共享 API env 后加载 `/etc/genarrative/external-generation-worker.env`;该文件如重复定义内部 base URL、Token / Token 文件、connect timeout、`N / est`、OSS bucket 或 endpoint,最终有效值必须与共享 API env 完全一致,否则发布失败,避免父子预算或对象存储视图漂移。外部生成 worker 可以使用同一 bucket 下权限等价或更小的独立 AK,不要求凭据文本一致。Token 文件由 Provision 以 `root:genarrative 0440` 创建或保留,env 只引用路径,不保存内部 Token 明文;env 示例和仓库不得出现真实 provider / OSS secret。 -生产发布必须按 `共享配置 / endpoint / Token / N / est / Q 预检 → stop 旧 BgFilter worker → 等待 systemd 排空 → start 唯一实例 → 检查 http://127.0.0.1:8083/readyz → 重启 API → 重启 external-generation worker / controller` 的顺序执行。Token 预检要求 API env 指向非空、非符号链接的普通文件,权限固定为 `root:genarrative 0440`;endpoint 预检要求父进程 `GENARRATIVE_BGFILTER_WORKER_BASE_URL`、子 worker `HOST / PORT` 和 readiness URL 指向同一个 `127.0.0.1:`;预算预检要求共享 `N / est` 均为正整数、父子有效值一致,当前模板默认和压测前冻结值为 `N=16 / est=5000ms`,后续允许按真实压测校准 `est`,不由 deploy 脚本写死;`Q` 缺省为 `2048`,显式值不得小于 `N`,历史模板默认 `128` 会在 deploy / Provision 时定向迁移为 `2048`;熔断 cooldown 的历史模板默认 `300s` 同样会定向迁移为 `120s`;其它显式定制值均保留。任何预检失败都发生在 `current` 切换和停止现役 worker 之前。非模板 unit、固定 loopback 端口和显式 stop/start 共同避免新旧 BgFilter worker 重叠;第二实例会因固定端口绑定失败。内部请求分别携带 `maxQueueWaitMs` 与公式化 `callBudgetMs`;worker 停机时立即让尚未取得 provider permit 的排队请求失败,只排空已经取得 permit 的调用。默认 `callBudgetMs=321s`,unit 使用 `TimeoutStopSec=900` 给最多两次 attempt、响应发送和进程收口留足余量,禁止沿用约 `90s` 的默认停止窗口。默认 API deploy 会安装、enable、启动并验活该 unit;只有明确回滚或应急排障时才使用 `--no-bgfilter-worker` 跳过,且不得让父进程偷偷恢复为直连 BgFilter。 +生产发布必须按 `共享配置 / endpoint / Token / N / est / Q 预检 → stop 旧 BgFilter worker → 等待 systemd 排空 → start 唯一实例 → 检查 worker readyz → 重启 API → 重启 external-generation worker / controller` 的顺序执行。readyz URL 由部署脚本从已校验 env 的 `HOST/PORT` 派生(默认 `http://127.0.0.1:8083/readyz`):`--bgfilter-worker-health-url` 缺省即派生,Jenkins 流水线不传该参数;显式传入时必须与父进程 base URL 和子 worker listener 三方一致,否则预检失败。Token 预检要求 API env 指向非空、非符号链接的普通文件,权限固定为 `root:genarrative 0440`;endpoint 预检要求父进程 `GENARRATIVE_BGFILTER_WORKER_BASE_URL`、子 worker `HOST / PORT` 和 readiness URL 指向同一个 `127.0.0.1:`;预算预检要求共享 `N / est` 均为正整数、父子有效值一致,当前模板默认和压测前冻结值为 `N=16 / est=5000ms`,后续允许按真实压测校准 `est`,不由 deploy 脚本写死;`Q` 缺省为 `2048`,显式值不得小于 `N`,历史模板默认 `128` 会在 deploy / Provision 时定向迁移为 `2048`;熔断 cooldown 的历史模板默认 `300s` 同样会定向迁移为 `120s`;其它显式定制值均保留。任何预检失败都发生在 `current` 切换和停止现役 worker 之前。非模板 unit、固定 loopback 端口和显式 stop/start 共同避免新旧 BgFilter worker 重叠;第二实例会因固定端口绑定失败。内部请求分别携带 `maxQueueWaitMs` 与公式化 `callBudgetMs`;worker 停机时立即让尚未取得 provider permit 的排队请求失败,只排空已经取得 permit 的调用。默认 `callBudgetMs=321s`,unit 使用 `TimeoutStopSec=900` 给最多两次 attempt、响应发送和进程收口留足余量,禁止沿用约 `90s` 的默认停止窗口。默认 API deploy 会安装、enable、启动并验活该 unit;只有明确回滚或应急排障时才使用 `--no-bgfilter-worker` 跳过,且不得让父进程偷偷恢复为直连 BgFilter。 `api-server` 进程角色由 `GENARRATIVE_PROCESS_ROLE` 控制:`api` 只监听 HTTP,`external-generation-worker` 只消费外部生成队列,`external-generation-controller` 只管理 worker systemd 实例,`all` 仅用于本地或临时 smoke,不隐式启动 controller。外部生成策略由 `GENARRATIVE_EXTERNAL_GENERATION_MODE` 控制;生产和容器压测默认保持 `queue`,本地 `npm run dev` / `npm run dev:api-server` 默认由 dev 脚本注入 `GENARRATIVE_PROCESS_ROLE=all`,如果外部生成策略为 `queue` 会由同一进程消费队列。`inline` 只用于本地或低并发同步排查,HTTP handler 会直接复用 worker executor,完成后返回 `completed`,但不会落 `external_generation_job`,也不能通过增加 worker 进程扩吞吐。外部生成 worker 使用同一发布包和同一套 SpacetimeDB 配置,按实例数和 `GENARRATIVE_EXTERNAL_GENERATION_WORKER_CONCURRENCY` 动态扩缩;生产默认由 `genarrative-external-generation-controller.service` 读取 `get_external_generation_queue_stats_and_return`,按 `claimable_pending + running_active + expired_running` 计算目标 worker 数,并对 `genarrative-external-generation-worker@N.service` 精确执行 `systemctl start/stop`。controller 参数模板是 `deploy/env/external-generation-controller.env.example`:默认保底 `MIN_WORKERS=1`、上限 `MAX_WORKERS=8`、每 worker 目标 `TARGET_JOBS_PER_WORKER=2`、`POLL_INTERVAL_MS=10000`、连续 `SCALE_DOWN_IDLE_ROUNDS=6` 轮完全空闲才缩容;缩容每轮只停止最高编号的一个实例,且不主动停止 `@1`。worker 收到 SIGINT/SIGTERM 后会停止 claim 新任务并等待当前任务完成;若进程被硬杀、机器断电或超过 systemd `TimeoutStopSec`,未完成任务才会在 lease 过期后由其它 worker 重领。每个 worker 实例应设置唯一 `GENARRATIVE_EXTERNAL_GENERATION_WORKER_ID`,默认会用主机名和 pid 兜底;systemd 生产模板 `deploy/systemd/genarrative-external-generation-worker@.service` 会用 `%H-%i` 生成实例 ID,并把 tracking outbox 隔离到 `/var/lib/genarrative/tracking-outbox/%H-%i`。`Genarrative-Server-Provision` 会安装 worker 模板、controller unit 和两份专属 env 模板,默认 enable 首个 `genarrative-external-generation-worker@1.service` 与 `genarrative-external-generation-controller.service`;首次 API deploy 会在默认 worker pattern 下自动 `enable --now genarrative-external-generation-worker@1.service` 并等待 worker active,同时重启并验活 controller。手动兜底扩容仍可用 `systemctl start genarrative-external-generation-worker@2.service` / `@3.service`,缩容用 `systemctl stop genarrative-external-generation-worker@N.service`;controller 下轮会按队列压力修正到目标实例数。worker 专属参数模板是 `deploy/env/external-generation-worker.env.example`,密钥与 SpacetimeDB 连接默认复用 `/etc/genarrative/api-server.env`。API 发布脚本默认会重启并验活 `genarrative-external-generation-worker@*.service` 和 `genarrative-external-generation-controller.service`;安装随包 BgFilter、external-generation worker 和 controller unit 前,必须按本次 `--current-link`、`--api-env-file`、`--worker-env-file`、`--controller-env-file` 与 `--bgfilter-worker-env-file` 渲染路径,不能用原始模板覆盖 Server-Provision 已安装的自定义路径。`Genarrative-Api-Deploy` 和 `Genarrative-Full-Build-And-Deploy` 必须同时暴露并透传这三类角色 env 参数,避免独立发布脚本修复后又被流水线默认值截断。若本次只发 HTTP 且不希望滚动 worker,可传 `--no-worker-services`,若不希望重启 controller 可传 `--no-worker-controller`。`GENARRATIVE_EXTERNAL_GENERATION_WORKER_POLL_INTERVAL_MS` 控制空队列轮询间隔,`GENARRATIVE_EXTERNAL_GENERATION_WORKER_LEASE_SECONDS` 控制单次 lease,worker 会约每三分之一 lease、最长 30 秒续租;该值应覆盖一次心跳网络抖动窗口,不需要大于完整外部生成链路耗时。SpacetimeDB 使用自身事务时间计算 claim/renew/complete/fail,完成和失败回写还会校验 `lease_token` 与未过期 lease,避免同一 job 被过期 worker 覆盖。首版 worker 粒度是单动作单 job,不拆阶段 job;当前外部生成动作覆盖拼图、跳一跳、拼消消、敲木鱼和图片画布编辑器生成 / 抠图入口,纯元信息保存、发布、试玩启动、运行态动作和公开读取继续 inline。图片画布 worker 成功后由后端保存 `editor_project_resource` / `editor_asset` / `editor_canvas.layers_json`,前端只轮询 job 并重新读取项目快照。当前生成业务失败只做用户重新触发,不做自动业务重试,避免 worker 退款和重试成功之间产生钱包账本漂移。