重构 BgFilter 排队与调用双预算
按 N 与单图估时公式化派生 provider attempt、调用预算和队列等待上限 将内部协议拆分为 maxQueueWaitMs 与 callBudgetMs,并补齐停机、熔断和审计语义 同步部署配置、运维门禁、smoke 测试及权威文档
This commit is contained in:
+3
-2
@@ -151,8 +151,9 @@ GENARRATIVE_BGFILTER_WORKER_HOST="127.0.0.1"
|
||||
GENARRATIVE_BGFILTER_WORKER_PORT="8083"
|
||||
GENARRATIVE_BGFILTER_WORKER_BASE_URL="http://127.0.0.1:8083"
|
||||
GENARRATIVE_BGFILTER_INTERNAL_TOKEN="CHANGE_ME_FOR_LOCAL"
|
||||
GENARRATIVE_BGFILTER_WORKER_CONCURRENCY="4"
|
||||
GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS="128"
|
||||
GENARRATIVE_BGFILTER_WORKER_CONCURRENCY="16"
|
||||
GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS="5000"
|
||||
GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS="2048"
|
||||
GENARRATIVE_BGFILTER_WORKER_CONNECT_TIMEOUT_MS="2000"
|
||||
|
||||
# SpacetimeDB 数据目录备份到 OSS。备份 bucket 可与资源 bucket 分离;未设置时脚本回退使用 ALIYUN_OSS_BUCKET。
|
||||
|
||||
@@ -30,7 +30,8 @@ GENARRATIVE_WALLET_REFUND_OUTBOX_DIR=/var/lib/genarrative/wallet-refund-outbox
|
||||
GENARRATIVE_WALLET_REFUND_OUTBOX_BATCH_SIZE=100
|
||||
GENARRATIVE_WALLET_REFUND_OUTBOX_FLUSH_INTERVAL_MS=1000
|
||||
GENARRATIVE_WALLET_REFUND_OUTBOX_MAX_BYTES=67108864
|
||||
GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS=180000
|
||||
GENARRATIVE_BGFILTER_WORKER_CONCURRENCY=16
|
||||
GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS=5000
|
||||
GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD=3
|
||||
GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS=300
|
||||
# BgFilter 失败后的中间兜底:阿里云通用抠图(SegmentCommonImage)。AccessKey 留空则跳过该层,
|
||||
|
||||
Vendored
+4
-2
@@ -34,8 +34,10 @@ GENARRATIVE_WALLET_REFUND_OUTBOX_DIR=/var/lib/genarrative/wallet-refund-outbox
|
||||
GENARRATIVE_WALLET_REFUND_OUTBOX_BATCH_SIZE=100
|
||||
GENARRATIVE_WALLET_REFUND_OUTBOX_FLUSH_INTERVAL_MS=1000
|
||||
GENARRATIVE_WALLET_REFUND_OUTBOX_MAX_BYTES=67108864
|
||||
# 共享的单次 provider attempt 超时上限,父侧据此计算两次逻辑预算;专属 worker env 不重复定义。
|
||||
GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS=180000
|
||||
# N 与单图估时:父子共同派生 attempt = N×est×2、callBudget = 2×attempt+1s 的公式输入;
|
||||
# 单一来源放共享 env,专属 worker env 不重复定义。旧固定 REQUEST_TIMEOUT_MS 已删除。
|
||||
GENARRATIVE_BGFILTER_WORKER_CONCURRENCY=16
|
||||
GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS=5000
|
||||
GENARRATIVE_EDITOR_BGFILTER_BASE_URL=http://58.87.105.82/bgfilter
|
||||
GENARRATIVE_EDITOR_BGFILTER_TOKEN=
|
||||
# BgFilter 失败后的中间兜底:阿里云通用抠图(SegmentCommonImage)。AccessKey 留空则跳过该层,
|
||||
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
# 复制到 /etc/genarrative/bgfilter-worker.env;只放专用进程独占参数和可选日志覆盖。
|
||||
# provider、OSS、内部 Token 文件和请求 timeout 统一来自先加载的 api-server.env,禁止在此重复定义。
|
||||
# provider、OSS、内部 Token 文件、N(CONCURRENCY)与单图估时统一来自先加载的 api-server.env,禁止在此重复定义。
|
||||
# systemd unit 会强制设置 GENARRATIVE_PROCESS_ROLE=bgfilter-worker。
|
||||
|
||||
GENARRATIVE_ENV=production
|
||||
GENARRATIVE_BGFILTER_WORKER_HOST=127.0.0.1
|
||||
GENARRATIVE_BGFILTER_WORKER_PORT=8083
|
||||
GENARRATIVE_BGFILTER_WORKER_CONCURRENCY=4
|
||||
GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS=128
|
||||
# Q:admission 保险丝,只防连接风暴;正常业务不应触达,显式配置时必须 >= N。
|
||||
GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS=2048
|
||||
# flat 熔断只由本进程维护;complex 不读写熔断状态。
|
||||
GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD=3
|
||||
GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS=300
|
||||
|
||||
@@ -15,7 +15,7 @@ ExecStart=/usr/bin/env GENARRATIVE_PROCESS_ROLE=bgfilter-worker OTEL_SERVICE_NAM
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
KillSignal=SIGINT
|
||||
# 内部 requestBudgetMs 协议上限为 600s;额外窗口用于响应发送和进程收口。
|
||||
# shutdown 会立即拒绝仍在排队的请求,只排空已取得 provider permit 的调用;默认 N=16、est=5000ms 时 callBudgetMs=321s,额外窗口用于响应发送和进程收口。
|
||||
TimeoutStopSec=900
|
||||
LimitNOFILE=65535
|
||||
TasksMax=2048
|
||||
|
||||
@@ -19,12 +19,12 @@
|
||||
## 2026-07-21 BgFilter 首版采用单实例同步内部 HTTP 与父流程原地等待
|
||||
|
||||
- 背景:角色动画在单个 `external_generation_job` 内通过 `buffer_unordered(frame_count)` 可并发发射最多 `48` 次 BgFilter 请求;限制父 worker 并发不能限制单个父 job 内的实际 BgFilter 并发。父 job checkpoint / continuation 和 SpacetimeDB 持久子任务都会扩大父状态机、attempt、计费、恢复和清理改动,而当前 BgFilter 成功结果本来就是 HTTP 图片二进制。
|
||||
- 决策:父 future 保持原调用栈、lease 和 attempt,等待期间继续占用通用 worker 槽并由现有 heartbeat 续租;所有调用统一同步请求唯一 `bgfilter-worker` 的内部 loopback HTTP。输入只传 OSS object key、参数和剩余预算,成功直接返回经过校验的图片二进制。子 worker 使用有界 admission `Q` 和进程内 `Semaphore(N)`,负责最多两次顺序 provider attempt、flat 进程级熔断和失败审计;父流程继续负责 flat 降级、complex 失败、Alpha / 尺寸恢复、动画 finalizer、最终 OSS、业务写回、计费和父终态。
|
||||
- 超时边界:父 job 总预算仍为普通 `900s` / 长任务 `1800s`,并保留现有 `60s` 终态写回窗口;`180s` 仅是子 worker 单次真实 provider attempt 的默认上限,父侧据此派生 `2 × attempt timeout + 1s response window` 的逻辑调用上限,再按父绝对 deadline 和 flat fallback reserve 截短。内部 RPC 总预算包含 admission 后的 semaphore 等待、最多两次 attempt、结果校验和二进制返回。动画删除旧的 `2000ms × frame_count` 单次 timeout 增量,父侧不得在内部 timeout / 断连后重试整次 RPC。
|
||||
- 决策:父 future 保持原调用栈、lease 和 attempt,等待期间继续占用通用 worker 槽并由现有 heartbeat 续租;所有调用统一同步请求唯一 `bgfilter-worker` 的内部 loopback HTTP。输入只传 OSS object key、参数、`maxQueueWaitMs / callBudgetMs` 和有界审计关联,成功直接返回经过校验的图片二进制。子 worker 使用有界 admission `Q` 和进程内 `Semaphore(N)`,负责最多两次顺序 provider attempt、flat 进程级熔断和失败审计;父流程继续负责 flat 降级、complex 失败、Alpha / 尺寸恢复、动画 finalizer、最终 OSS、业务写回、计费和父终态。
|
||||
- 超时边界:父 job 总预算仍为普通 `900s` / 长任务 `1800s`,并保留现有 `60s` 终态写回窗口。内部协议拆成互不挪用的 `maxQueueWaitMs` 与 `callBudgetMs`:前者由父剩余绝对预算扣除调用预算和父侧预留后派生,只限制等待 provider permit;flat 的 `39s` 父侧预留由 `37s` fallback(阿里云 `30s` + 本地 `7s`)与 `2s` 传输窗组成,complex 只留 `2s` 传输窗。后者从取得 permit 后起算,覆盖签名、最多两次 attempt、结果校验和响应构造。provider attempt 上限按 `N × est × 2` 派生,调用预算按 `2 × attempt + 1s` 派生;额外 `1s` 吸收 attempt 间开销,只要剩余时间仍能容纳完整 attempt 就不得先扣响应预留。父内部 client timeout 精确取 `maxQueueWaitMs + callBudgetMs + 2s`,不在发送阶段重新裁剪两笔相对预算。冻结 `N=16`、`est=5000ms` 时 attempt / callBudget 分别为 `160s / 321s`。动画删除旧的按帧数 timeout 增量,父侧不得在内部 timeout / 断连后重试整次 RPC。
|
||||
- 故障边界:首版不新增 `bgfilter_task_group`、`bgfilter_request_task`、raw OSS、checkpoint、continuation、数据库 capacity slot、共享熔断或 QPS token bucket。父或子进程崩溃、RPC 丢失时不查询、不恢复结果;父 job 沿用现有 lease / `max_attempts=1` 失败退款语义。动画首版保持所有已提交帧 collect / drain,不增加跨帧取消组。
|
||||
- 部署边界:专用进程首版仍复用完整 `AppState`,因此 systemd unit 先加载共享 `/etc/genarrative/api-server.env`,再加载 `/etc/genarrative/bgfilter-worker.env` 覆盖 worker 独占参数;共享 `GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS` 必须在父子进程保持同一有效值,flat 熔断 threshold / cooldown 只归子 worker。发布切换前校验父子使用同一个非空、非符号链接、`root:genarrative 0440` 的内部 Token 文件;拒绝 `external-generation-worker.env` 覆盖出不同的内部 URL、Token 文件、connect timeout、provider timeout、OSS bucket 或 endpoint(同 bucket 的独立 AK 允许),并要求父 base URL、子 `HOST / PORT` 与 readiness URL 指向同一 loopback endpoint。worker unit 使用 `TimeoutStopSec=900` 覆盖最大 `600s` 请求预算的优雅排空,运行期巡检同时检查唯一 worker unit active 与 loopback readiness。本地 `npm run dev` 同样启动独立子进程并解析第五个 dev 端口,`ProcessRole::All` 不内嵌 listener。
|
||||
- 部署边界:专用进程首版仍复用完整 `AppState`,因此 systemd unit 先加载共享 `/etc/genarrative/api-server.env`,再加载 `/etc/genarrative/bgfilter-worker.env` 覆盖 worker 独占参数;父子共同依赖的 `N=16` 与 `est=5000ms` 必须来自共享基础环境,worker 专属环境只管理 flat 熔断参数和默认 `Q=2048` 保险丝。发布切换前校验父子使用同一个非空、非符号链接、`root:genarrative 0440` 的内部 Token 文件,并拒绝父子内部 URL、Token、连接参数、`N / est`、OSS bucket 或 endpoint 漂移(同 bucket 的独立 AK 允许)。worker 停机时立即拒绝仍在排队的请求,只排空已取得 provider permit 的调用;unit 使用 `TimeoutStopSec=900` 覆盖默认 `321s` 调用预算及响应收口。运行期巡检同时检查唯一 worker unit active 与 loopback readiness;本地 `npm run dev` 同样启动独立子进程并解析第五个 dev 端口,`ProcessRole::All` 不内嵌 listener。
|
||||
- 影响范围:已实施范围包括 `api-server` 内部 HTTP client、专用 `bgfilter-worker` listener / process role、并发与超时配置、部署和运维观测;未修改 SpacetimeDB schema、父 job schema、用户任务 DTO、任务列表或收费归属,当前待生产压测后启用。
|
||||
- 验证方式:`48` 帧并发进入父 future 时,健康唯一子 worker 进程持有的 BgFilter HTTP future 峰值不得超过生产显式配置的 `N`,且 `queued + running` 不超过 `Q`;覆盖 flat / complex 降级矩阵、内部 deadline、断连后已启动请求排空、动画全帧 drain、External v1 / inline 旁路扫描、二进制大小 / MIME / 尺寸门禁、单实例部署、DDD、编码和 diff 门禁。
|
||||
- 验证方式:`48` 帧并发进入父 future 时,健康唯一子 worker 进程持有的 BgFilter HTTP future 峰值不得超过生产显式配置的 `N`,且 `queued + running + egress` 不超过 `Q`(admission permit 持有到 response body 发送完成或 drop);覆盖 flat / complex 降级矩阵、内部 deadline、断连后已启动请求排空、动画全帧 drain、External v1 / inline 旁路扫描、二进制大小 / MIME / 尺寸门禁、单实例部署、DDD、编码和 diff 门禁。
|
||||
- 关联文档:`docs/technical/【后端架构】BgFilter受限资源调度方案-2026-07-21.md`、`docs/technical/【后端架构】外部生成Worker化方案-2026-06-03.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。
|
||||
|
||||
---
|
||||
@@ -142,7 +142,7 @@
|
||||
|
||||
## 2026-07-15 角色动作 BgFilter 请求超时按帧数扩展
|
||||
|
||||
> 后续更正(2026-07-21):本条按帧数增加 `2000ms × frame_count`、形成 `244000 / 260000 / 276000ms` 单 attempt timeout 的决策,已被 2026-07-21「BgFilter 首版采用单实例同步内部 HTTP 与父流程原地等待」取代。当前角色动作的每次真实 provider attempt 与其它 BgFilter 路径一样使用默认 `180000ms` 上限,不再按帧数扩展;排队、等待 `N`、最多两次顺序 attempt、校验与响应统一受父流程派生的内部 RPC 剩余预算约束。下文保留作历史记录。
|
||||
> 后续更正(2026-07-21):本条按帧数增加 timeout 的决策已被 2026-07-21「BgFilter 首版采用单实例同步内部 HTTP 与父流程原地等待」的公式化双预算取代。当前每帧分别携带 `maxQueueWaitMs` 与 `callBudgetMs`:排队预算只约束等待 provider permit,取得 permit 后才启动调用预算;provider attempt 按 `N × est × 2`、调用预算按 `2 × attempt + 1s` 运行时派生,冻结 `N=16`、`est=5000ms` 时为 `160s / 321s`,不再按 `32 / 40 / 48` 帧扩展。下文保留作历史记录。
|
||||
|
||||
- 背景:角色动作全部序列帧会并发进入 BgFilter,而服务端可能在自身进程内排队;固定 `180000ms` 会把排队时间和单帧推理共用同一预算,靠后的请求可能在服务仍正常处理时被 api-server 提前取消。
|
||||
- 决策:保留 `GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS` 作为统一基准值。只有角色动作逐帧 BgFilter 在共享 Client 的 RequestBuilder 上把每一次 HTTP attempt 覆盖为“基准值 + `2000ms × 本次实际帧数`”,默认 `32 / 40 / 48` 帧为 `244000 / 260000 / 276000ms`;角色形象单图、图标、UI 和手动去背景不增加帧预算。该 timeout 覆盖请求发起到响应体读取完成;首次失败后的重试重新获得同样的 request deadline,整批并发策略、失败排空语义和 worker long-job 总预算不变。
|
||||
@@ -366,7 +366,7 @@
|
||||
|
||||
## 2026-07-03 图片画布生成纯色背景资产接入 BgFilter
|
||||
|
||||
> 后续更正:本条关于 multipart `file`、手动去背景独立 BiRefNet 配置以及 BgFilter 失败后直接本地兜底的描述,已分别由 2026-07-14、2026-07-15 OSS 签名 URL 决策和 2026-07-17 内存生命周期决策取代。下文保留作历史记录。
|
||||
> 后续更正:本条关于 multipart `file`、手动去背景独立 BiRefNet 配置以及 BgFilter 失败后直接本地兜底的描述,已分别由 2026-07-14、2026-07-15 OSS 签名 URL 决策和 2026-07-17 内存生命周期决策取代;其中固定 provider timeout 配置也已被 2026-07-21 的公式化双预算取代,当前请求分别携带 `maxQueueWaitMs / callBudgetMs`,attempt 按 `N × est × 2` 派生,冻结 `N=16 / est=5000ms` 时为 `160s`,调用预算为 `321s`。下文保留作历史记录。
|
||||
|
||||
- 背景:独立 BgFilter 服务已部署在 image host,并提供 `POST /bgfilter/remove-background`,支持显式 `screen_color` 和 `seg_model`。手动去背景已有独立 BiRefNet BFF,不能把两个服务的配置或语义混在一起。
|
||||
- 决策:角色形象生成、图标 spritesheet 生成和 UI 设计图素材提取在保存带纯色背景源图后,统一调用 BgFilter 生成透明 PNG;请求 multipart 字段为 `file`、`screen_color=<screenColor>` 和内部固定的 `seg_model=birefnet`。`segModel` 虽是后端可识别的内部兼容字段(另保留 `anime-seg`),但不向用户或外部 OpenAPI 暴露:当前 BgFilter 的内存与并发容量不适合由调用方自由切换模型。BgFilter 使用独立配置 `GENARRATIVE_EDITOR_BGFILTER_BASE_URL`、`GENARRATIVE_EDITOR_BGFILTER_TOKEN`、`GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS`,默认 base URL 为 `http://58.87.105.82/bgfilter`,默认请求超时 `180000ms`,token 未配置时复用 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN`。手动 `POST /api/editor/images/background-removals` 继续使用独立 BiRefNet 配置 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_BASE_URL`,不受 BgFilter 影响。BgFilter 参数里的 `seg_model=birefnet` 只表示 BgFilter 内部分割后端,不等于手动去背景的独立 BiRefNet 服务。若 BgFilter 失败,api-server 对这些标准纯色背景生成图使用本地 `editor_green_screen` 兜底;连续失败达到 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD`(默认 `3`)后,`GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS`(默认 `300`)内直接本地兜底。角色动作背景色和抠帧口径已由 2026-07-09 决策取代:角色动作同样使用多色自动决策,抽帧后优先阿里云通用抠图,失败再按选定背景色本地兜底。更正(截至 2026-07-10 实现):BgFilter 失败与熔断期本条描述的「直接本地兜底」已过时——角色形象/图标/UI 三条静态生图链路同样先走阿里云通用抠图,仅阿里云也失败才本地 `editor_green_screen` 兜底。
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -13,12 +13,12 @@
|
||||
| 调度单位 | 一次逻辑 BgFilter 调用;角色动画为单帧 |
|
||||
| 父流程 | 保持原 future、调用栈、lease 和 `attempt`,同步等待内部 HTTP |
|
||||
| 通用 worker 槽 | 等待期间继续占用;父 heartbeat 继续运行 |
|
||||
| 请求输入(父 → 子) | 只传私有 OSS `objectKey`、BgFilter 参数、剩余预算和有界审计关联;不重复传源图字节,也不传签名 URL |
|
||||
| 请求输入(父 → 子) | 只传私有 OSS `objectKey`、BgFilter 参数、排队预算 `maxQueueWaitMs`、调用预算 `callBudgetMs` 和有界审计关联;不重复传源图字节,也不传签名 URL |
|
||||
| 成功输出(子 → 父) | 内部 HTTP body 直接传回 BgFilter 结果图片的原始字节;不使用 Base64、不返回结果 object key、不先写 raw OSS |
|
||||
| BgFilter worker | 首版只运行一个内部 HTTP worker 实例 |
|
||||
| 并发 | 进程内 `Semaphore(N)`,并增加有界 admission 上限 `Q` |
|
||||
| 重试 | 子 worker 对一次逻辑调用最多做两次顺序 provider attempt;父侧不重试整次内部 RPC |
|
||||
| 超时 | 父侧管理 job / request 总预算和内部 RPC 总预算;子 worker 管理排队及单次 provider `180s` 上限 |
|
||||
| 超时 | 双预算:父侧派生排队预算 `maxQueueWaitMs` 与调用预算 `callBudgetMs`;attempt 上限由 `N × est × 2` 公式运行时派生(est 默认 `5s`),排队不侵蚀调用时间 |
|
||||
| flat 熔断 | 迁到唯一子 worker 的进程内状态;连续失败达到阈值后暂时跳过 BgFilter,complex 完全不参与 |
|
||||
| 业务语义 | 父流程继续负责 Alpha / 尺寸恢复、flat fallback、最终 OSS、画布写回、计费和父终态 |
|
||||
| 动画失败 | 首版保持当前“所有已提交帧都等待并排空”语义,不新增跨帧取消组 |
|
||||
@@ -78,7 +78,7 @@ flowchart LR
|
||||
|
||||
P --> O
|
||||
P --> C
|
||||
C -->|"objectKey + 参数 + requestBudgetMs"| W
|
||||
C -->|"objectKey + 参数 + maxQueueWaitMs + callBudgetMs"| W
|
||||
W --> B
|
||||
B --> W
|
||||
W -->|"图片二进制或类型化 JSON 错误"| C
|
||||
@@ -96,7 +96,7 @@ flowchart LR
|
||||
|
||||
这里的“同步等待”是控制流上的 request / response `await`:不会阻塞 OS 执行线程或整个父进程,但父 job future 仍留在通用 worker 的并发集合中,占用一个父 worker 槽,并由现有 heartbeat 继续续租。
|
||||
|
||||
首版进程角色仍复用现有完整 `AppState` 构造路径,以获得 OSS、BgFilter provider、SpacetimeDB 审计、HTTP client 和可观测性依赖;进程角色只阻止它挂载公共路由、claim 外部生成 job 或启动其它后台循环,并不等于它只需要 `N / Q` 几个环境变量。因此生产 unit 必须先加载 `/etc/genarrative/api-server.env`,再加载 `/etc/genarrative/bgfilter-worker.env` 覆盖监听地址、`N / Q` 和 worker 独占参数。后续若拆出轻量专用 state,可再缩小共享配置依赖,首版不能假设该拆分已经存在。
|
||||
首版进程角色仍复用现有完整 `AppState` 构造路径,以获得 OSS、BgFilter provider、SpacetimeDB 审计、HTTP client 和可观测性依赖;进程角色只阻止它挂载公共路由、claim 外部生成 job 或启动其它后台循环,并不等于它只需要几个调度环境变量。因此生产 unit 必须先加载 `/etc/genarrative/api-server.env` 中父子共享的 `N / est` 与基础配置,再加载 `/etc/genarrative/bgfilter-worker.env` 覆盖监听地址、`Q`、flat 熔断和 worker 独占参数。后续若拆出轻量专用 state,可再缩小共享配置依赖,首版不能假设该拆分已经存在。
|
||||
|
||||
### 3.1 图片数据流口径
|
||||
|
||||
@@ -136,7 +136,8 @@ Authorization: Bearer <internal-token>
|
||||
"screenColor": "#00ff00",
|
||||
"segModel": "birefnet",
|
||||
"crossCheck": true,
|
||||
"requestBudgetMs": 300000,
|
||||
"maxQueueWaitMs": 540000,
|
||||
"callBudgetMs": 321000,
|
||||
"auditContext": {
|
||||
"userId": "bounded-internal-id",
|
||||
"profileId": null,
|
||||
@@ -151,7 +152,7 @@ Authorization: Bearer <internal-token>
|
||||
- 如果未来确实支持多个 bucket,新增字段也必须由服务端 allowlist 校验;不能接受调用方提供任意下载 URL。
|
||||
- `backgroundMode` 只允许 `flat / complex`;`segModel` 继续沿用当前 `birefnet / anime-seg` allowlist;complex 固定使用当前参数组合。
|
||||
- `screenColor` 只对 flat 必填;complex 不得误接 flat 参数或熔断。
|
||||
- `requestBudgetMs` 是相对预算,不是跨机器绝对时间。当前实现从内部鉴权通过并取得 `Q` admission permit 的时刻起算;`Q` 满时立即返回 `overloaded`,成功 admission 后的 JSON 解析、等待 `N` permit、provider attempt、结果校验和响应构造都消耗该预算。
|
||||
- `maxQueueWaitMs` 与 `callBudgetMs` 都是相对预算,不是跨机器绝对时间。前者从 admission 起约束排队阶段(worker 还会用 §5.2 的动态估计对其取 min);后者从取得 provider permit 起计时,覆盖签名、两次 attempt、结果校验和响应构造。`callBudgetMs` 必须等于 worker 本进程按 `N / est` 公式算出的值,不一致按 `invalid_request` 拒绝,用于快速暴露父子配置漂移。
|
||||
- JSON body 设置很小的固定上限;源图字节不进入该 JSON。
|
||||
|
||||
签名 URL 必须在取得 provider permit 后、每次 attempt 前生成,避免排队期间过期。签名 URL 只存在于子 worker 内存和发往 BgFilter 的请求中。
|
||||
@@ -177,8 +178,8 @@ Content-Type: image/png
|
||||
|
||||
- `provider_exhausted`:两次真实 provider attempt 都失败;
|
||||
- `circuit_open`:flat 熔断已打开,未发送 provider 请求;
|
||||
- `deadline_exceeded`:排队、provider 或响应阶段预算耗尽,使用 `phase = queue | provider | response`;
|
||||
- `overloaded`:`queued + running` 已达到 `Q`;
|
||||
- `deadline_exceeded`:排队、provider 或响应阶段预算耗尽,使用 `phase = queue | provider | response`;`phase = queue` 时附带触发边界 `bound = estimate | parent`,区分动态过载探测与父上限;
|
||||
- `overloaded`:admission 保险丝 `Q` 触达(默认 `2048`,正常业务不应出现);
|
||||
- `cancelled`:保留错误码,首版子 worker 不产生。首版没有显式取消信号通道,单纯 TCP 断连后 handler future 被 drop、也无法再返回响应;该码为第二阶段 group cancellation 预留,父侧已按“不启动 fallback”实现映射;
|
||||
- `invalid_request`:内部契约不合法;
|
||||
- `unauthorized`:内部 Token 缺失或不匹配;
|
||||
@@ -192,6 +193,7 @@ Content-Type: image/png
|
||||
"error": {
|
||||
"code": "deadline_exceeded",
|
||||
"phase": "queue",
|
||||
"bound": "parent",
|
||||
"attemptsStarted": 0,
|
||||
"message": "BgFilter 内部请求预算已耗尽"
|
||||
}
|
||||
@@ -202,47 +204,69 @@ HTTP status 只作粗粒度传输分类,父侧以稳定 `error.code` 映射业
|
||||
|
||||
## 5. 超时所有权
|
||||
|
||||
### 5.1 三层预算
|
||||
### 5.1 双预算分工
|
||||
|
||||
新版不是“父 worker 完全不再管理 BgFilter 超时”,而是三层分工:
|
||||
本版把一次内部 RPC 的时间拆成两笔互不挪用的预算,排队不再侵蚀调用时间:
|
||||
|
||||
1. 父 worker 继续管理父 job 总预算:普通 `900s`、长任务 `1800s`。
|
||||
2. 父侧为每次内部 RPC 计算 worker `requestBudgetMs` 和略长的 parent client timeout;前者覆盖等待 semaphore、最多两次 provider attempt、结果校验和响应构造,后者再覆盖 loopback 传输与父侧读取。
|
||||
3. 子 worker 管理单次真实 BgFilter attempt,默认上限仍为 `180s`。
|
||||
2. 排队预算 `maxQueueWaitMs`:父侧按自己的剩余绝对预算派生,只约束“在 worker 内等待 provider permit”的阶段。
|
||||
3. 调用预算 `callBudgetMs`:从取得 provider permit 时起算,覆盖签名、最多两次 provider attempt、结果校验和响应构造;排队时长不消耗它,每个真正开跑的请求都保证有完整的两次 attempt 窗口。
|
||||
|
||||
queue job 的总预算从父 job 开始执行时起算,不从开始申请 BgFilter 时重新计时。现有父 worker 还会把 provider deadline 设在 job deadline 前 `60s`,为最终写回和终态保留时间。同步 RPC 实现必须显式读取父侧剩余 provider budget 并传入 `requestBudgetMs`,不能像当前 BgFilter helper 一样忽略 `RequestContext` deadline。
|
||||
所有派生值都在运行时由 `N` 与单图估时 `est` 两个配置计算,代码不得硬编码计算结果:
|
||||
|
||||
本版没有 BgFilter 子任务等待 claim 的阶段。几个起算点必须区分:父 job 在数据库中尚未被 claim 的等待不消耗 job 执行预算;父 job 开始实际执行后,生图及 BgFilter 之前的耗时都会消耗父总预算;父内部 HTTP client timeout 从开始发送请求起覆盖 loopback 传输、worker admission、等待 `N`、provider 和回包;单次 `180s` attempt timer 只在子 worker 真正开始一次 BgFilter provider HTTP 时启动。父侧不是放弃超时,而是不再直接执行 provider 单次 attempt 的计时器。
|
||||
```text
|
||||
est = GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS(默认 5000ms)
|
||||
attempt = N × est × 2
|
||||
callBudgetMs = 2 × attempt + 1s worker 响应构造窗
|
||||
maxQueueWaitMs = 父剩余绝对预算 − callBudgetMs − 父侧预留
|
||||
flat:39s = 37s fallback(阿里云 30s + 本地 7s)+ 2s 传输窗
|
||||
complex:2s 传输窗
|
||||
queue timeout = min((进入 provider 等待队列时的队长 + 5) × est × 2, maxQueueWaitMs) ← worker 侧计算
|
||||
parent client timeout = maxQueueWaitMs + callBudgetMs + 2s 传输窗
|
||||
```
|
||||
|
||||
`maxQueueWaitMs <= 0` 时父侧不得发送请求:flat 直接进入既有“阿里云 → 本地”fallback,complex 直接失败;不允许把注定超时的请求塞进队列。flat 扣除的 `39s` 父侧预留由 `37s` fallback 窗口和 `2s` 内部响应传输窗组成;complex 没有 fallback,只保留 `2s` 传输窗。
|
||||
|
||||
queue job 的总预算从父 job 开始执行时起算,不从开始申请 BgFilter 时重新计时。现有父 worker 还会把 provider deadline 设在 job deadline 前 `60s`,为最终写回和终态保留时间。同步 RPC 实现必须显式读取父侧剩余 provider budget 派生 `maxQueueWaitMs`,不能忽略 `RequestContext` deadline。
|
||||
|
||||
本版没有 BgFilter 子任务等待 claim 的阶段。几个起算点必须区分:父 job 在数据库中尚未被 claim 的等待不消耗 job 执行预算;父 job 开始实际执行后,生图及 BgFilter 之前的耗时都会消耗父总预算;父内部 HTTP client timeout 从开始发送请求起覆盖 loopback 传输、worker admission、排队、provider 和回包;`callBudgetMs` 计时只在子 worker 取得 provider permit 后启动,attempt timer 只在真正开始一次 provider HTTP 时启动。父侧不是放弃超时,而是不再直接执行 provider 单次 attempt 的计时器。
|
||||
|
||||
父总 deadline 到达时,现有 worker 会停止续租、释放 JoinSet 槽并把 work 交给 lease fencing 仲裁,不是立刻杀死所有内部工作。正常情况下内部 RPC 自身应在更早的 provider deadline 内结束,避免进入这条脱管路径。
|
||||
|
||||
### 5.2 子 worker attempt timeout
|
||||
### 5.2 排队预算与自适应过载探测
|
||||
|
||||
父侧先按 `requestBudgetMs + 2s` 计算期望的内部 client timeout,再受父绝对 deadline 截断:
|
||||
worker 通过鉴权与 `Q` admission 后,在请求完成 JSON 校验并进入 provider permit 等待队列时取得队长快照;排队 deadline 仍从 `Q` admission 时刻起算。双重上界为:
|
||||
|
||||
```text
|
||||
parent client timeout
|
||||
= min(requestBudgetMs + 2s parent transport window,
|
||||
父侧当前剩余绝对预算)
|
||||
queue timeout = min((进入 provider 等待队列时的队长 + 5) × est × 2, maxQueueWaitMs)
|
||||
```
|
||||
|
||||
`requestBudgetMs` 派生时会预扣这段 transport reserve,因此正常路径仍为子 worker 保留约 `2s` 的 loopback 传输、调度抖动和父侧读取类型化错误时间;父绝对 deadline 始终是硬上限。额外 `2s` 不增加 provider 可执行时间,也不能在父预算已经不足时强行延长 client timeout。
|
||||
- 动态项 `(队长 + 5) × est × 2`:按进入 provider permit 等待队列时的当前队列深度估计合理等待时间。`+5` 覆盖已在 provider 执行中、尚未释放 permit 的在途请求;`×2` 是安全系数。队列短但等待仍超出该值,说明 provider 实际速度远低于估计,趁父预算仍够时尽早返回 `deadline_exceeded (phase = queue)` 让 flat 走 fallback——动态项因此充当自适应过载探测器,替代旧 `Q` 容量拒绝的快速降级职责。tokio semaphore 按进入 provider 等待队列的顺序提供 FIFO;快照之后的后来者不影响本请求的动态上界。
|
||||
- 父上限 `maxQueueWaitMs`:父侧愿意等多久由父剩余预算决定,队列再长也不能超过它。
|
||||
|
||||
子 worker 收到请求后使用本机单调时钟计算 RPC deadline。每次 attempt 的 timeout 为:
|
||||
排队超时的错误响应必须标注触发边界(动态估计或父上限),便于区分“provider 变慢”与“父预算太紧”。
|
||||
|
||||
### 5.3 子 worker attempt timeout
|
||||
|
||||
子 worker 取得 provider permit 后,以本机单调时钟起算 `callBudgetMs` 的 RPC deadline。每次 attempt 的 timeout 为:
|
||||
|
||||
```text
|
||||
min(180s, RPC 剩余时间 - 1s worker 响应构造窗口)
|
||||
attempt = N × est × 2
|
||||
attemptTimeout = attempt (RPC 剩余时间 > attempt)
|
||||
= RPC 剩余时间 - 1s 响应构造窗口 (已无法容纳完整 attempt 的防御分支)
|
||||
```
|
||||
|
||||
worker 内部 `1s` 和 parent transport `2s` 都只是固定的小型进程 / 传输窗口,不是 raw 结果持久化预留。剩余时间不足时不得开始新的 attempt;第一次失败后只有预算仍足够才开始第二次。
|
||||
`callBudgetMs = 2 × attempt + 1s` 的额外 `1s` 用于吸收两次 attempt 之间的签名与调度开销;只要 RPC 剩余时间仍大于一次完整 `attempt`,第二次就必须取得完整窗口,不能先机械扣掉 `1s`,否则满窗 timeout 会被误判为预算截短并漏计熔断。已无法容纳完整 attempt 时才进入截短防御分支;被截短的 timeout 返回 `deadline_exceeded` 且不计入熔断。
|
||||
|
||||
flat 调用还要由父侧从可分配给 BgFilter 的预算中保留当前阿里云 request timeout(默认 `30s`)和少量本地处理余量,避免 BgFilter 排队吃完全部 provider budget 后名义上有 fallback、实际上已无时间执行。complex 没有 flat fallback,不使用该预留。
|
||||
attempt 公式的依据:BgFilter 服务端高并发时单图处理约 `1-3s`、网络约 `3-5s`;`N` 个在途请求在服务端排队的最坏等待按 `N × est` 估计,`×2` 为安全系数。`N = 16`、`est = 5s` 时 attempt 为 `160s`,与旧固定 `180s` 接近,单请求行为基本持平。
|
||||
|
||||
`GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS` 是单次真实 provider attempt 的基础上限,默认 `180s`。父侧据此派生一次逻辑调用的上限:`2 × attempt timeout + 1s worker response window`,再按父绝对 deadline 和 flat fallback reserve 截短为 `requestBudgetMs`;parent client timeout 则取前述 `min(requestBudgetMs + 最多 2s transport window, 父绝对 deadline 剩余)`。该配置必须由父子进程使用同一个有效值:首版把它放在共享 API 基础环境中作为单一来源,worker 专属环境不得悄悄覆盖成另一个值。
|
||||
flat 调用还要由父侧从可分配预算中扣除 `39s`:阿里云 request timeout(默认 `30s`)与本地处理余量 `7s` 构成 `37s` fallback 窗口,另有 `2s` 内部响应传输窗,避免排队吃完全部预算后名义上有 fallback、实际上已无时间执行。
|
||||
|
||||
inline / External v1 没有 queue job deadline 时,内部 RPC 仍必须有界;默认总上限按“两次现有 BgFilter attempt 上限 + 内部响应窗口”计算,排队时间同样包含在内。
|
||||
`N` 与 `est` 必须由父子进程使用同一份有效值:父侧要用它们算 `callBudgetMs` 与 client timeout,worker 要用它们算 attempt 与队列估时。两者都放在共享 API 基础环境中作为单一来源,worker 专属环境不得悄悄覆盖;worker 侧还应校验请求携带的 `callBudgetMs` 与本进程公式值一致,配置漂移时返回 `invalid_request` 快速暴露。
|
||||
|
||||
### 5.3 动画旧增量
|
||||
inline / External v1 当前没有显式 `RequestContext` deadline 时,内部 RPC 仍必须有界:`callBudgetMs` 同公式,`maxQueueWaitMs` 默认取与 `callBudgetMs` 等长的额度,因此默认父 client timeout 为 `2 × callBudgetMs + 2s`;后续若同步请求显式注入 deadline,再按同一 `maxQueueWaitMs` 公式从剩余预算派生。
|
||||
|
||||
### 5.4 动画旧增量
|
||||
|
||||
旧实现曾把动画单次 BgFilter timeout 设为:
|
||||
|
||||
@@ -250,20 +274,20 @@ inline / External v1 没有 queue job deadline 时,内部 RPC 仍必须有界
|
||||
180s + 2000ms × frame_count
|
||||
```
|
||||
|
||||
该增量原本用于容纳 32 / 40 / 48 个请求直接进入 BgFilter 后的服务端排队。当前实现已把排队前移到内部 worker并删除该增量;所有真实 provider attempt 统一使用 `180s` 上限,动画尾部请求能等待多久由各自内部 RPC 剩余预算决定。
|
||||
该增量原本用于容纳 32 / 40 / 48 个请求直接进入 BgFilter 后的服务端排队。当前实现已把排队前移到内部 worker 并删除该增量;所有真实 provider attempt 统一使用 `N × est × 2` 公式上限,动画尾部请求能等待多久由各自的排队预算决定。
|
||||
|
||||
## 6. 并发、排队与熔断
|
||||
|
||||
### 6.1 `N` 与 `Q`
|
||||
|
||||
唯一子 worker 使用两个简单上限:
|
||||
唯一子 worker 使用一个真并发上限和一个保险丝:
|
||||
|
||||
- `Q`:内部请求 admission 上限,满足 `queued + running + egress <= Q`;超出立即返回 `overloaded`。
|
||||
- `N`:BgFilter provider 并发 permit;handler 取得 permit 后才允许开始第一次 provider HTTP。
|
||||
- `N`:BgFilter provider 并发 permit(生产 `16`);handler 取得 permit 后才允许开始第一次 provider HTTP。
|
||||
- `Q`:admission 保险丝(默认 `2048`),只防调用方 bug 造成的连接风暴耗尽 fd / 连接资源,正常业务永远打不到;触达时立即返回 `overloaded`。部署与 Provision 会把历史模板默认 `128` 定向迁移为 `2048`,其它显式定制值保留。`Q` 不再承担容量策略职责——排队请求的真正上界是各自的 queue timeout(见 §5.2),队列因 deadline 自排水,长度上界约为到达率 × 父预算时长,而到达率被父侧扇出锁死。
|
||||
|
||||
同一次逻辑调用的 `N` permit 从第一次 provider attempt 前一直持有到两次尝试结束、provider body 读完、完成结果校验,并随内部成功 response body 一直持有到发送完成或 body drop。第二次 attempt 不重新排队。保守延长 permit 生命周期可以防止父侧慢读时积累多份完整结果;loopback 传输很短,首版接受这点吞吐代价。
|
||||
同一次逻辑调用的 `N` permit 从第一次 provider attempt 前一直持有到两次尝试结束、provider body 读完、完成结果校验,并随内部成功 response body 一直持有到发送完成或 body drop。第二次 attempt 不重新排队。保守延长 permit 生命周期可以防止父侧慢读时积累多份完整结果;loopback 传输很短,接受这点吞吐代价。
|
||||
|
||||
仅使用 provider semaphore 会形成无界 waiter,因此 `Q` 不是可选优化。内部 listener 必须在解析 JSON body 前使用连接 / request concurrency limit 与 load shedding 完成 admission,设置固定 listen backlog 和小型 body limit;`Q` permit 同样跟随 response body 到发送完成或 drop。`Q` 不替代内核 listen backlog,也不宣称消除所有已 accept socket。首版接受 FIFO,不增加持久优先级队列或公平调度状态机。
|
||||
内部 listener 仍在解析 JSON body 前完成鉴权与 `Q` admission,设置固定 listen backlog 和小型 body limit;`Q` 不替代内核 listen backlog。请求完成 JSON 校验后才进入 provider semaphore 等待队列,该队列采用 tokio semaphore 的 FIFO 语义;不增加持久优先级队列或公平调度状态机。
|
||||
|
||||
当前父 worker 可能提交的最大帧请求数并不只有 `96`:
|
||||
|
||||
@@ -273,15 +297,9 @@ inline / External v1 没有 queue job deadline 时,内部 RPC 仍必须有界
|
||||
| 一个默认 external-generation-worker,父并发 `2` | `96` |
|
||||
| controller 最多 `8` 个父 worker、每个并发 `2` | `768` |
|
||||
|
||||
`Q` 是保护子 worker 的 overload 取舍,不要求覆盖理论最大 `768`。例如选择 `Q = 128` 可以容纳两个满帧动画并留少量余量,但更多父 worker 会收到 `overloaded`;flat 将进入 fallback,complex 将失败。生产必须明确是否接受该行为。
|
||||
理论最大 `768` 远低于保险丝 `2048`,正常业务不会触发 `overloaded`;过载时的降级路径改由 §5.2 的动态排队探测承担——排队超出合理预期的 flat 请求提早进入 fallback,complex 失败。
|
||||
|
||||
理论最坏时间必须纳入容量评估:
|
||||
|
||||
```text
|
||||
48 帧、每帧两次 180s:约 ceil(48 / N) × 360s
|
||||
```
|
||||
|
||||
例如 `N = 4` 时理论最坏为 `4320s`,超过长 job 的 `1800s`。上线值不能只按理论最大超时推断,必须用真实 BgFilter P95、动画帧数和主机内存压测冻结;容量不足时尾部 flat 请求会在内部 deadline 后进入父侧 fallback。
|
||||
容量评估:`N = 16`、单图真实耗时约 `5-8s` 时,`48` 帧的排队时间约为 `ceil(48 / 16) × 单图耗时`,远低于父 job 预算。上线值仍必须用真实 BgFilter P95、动画帧数和主机内存压测冻结;`est` 估计过小时动态探测会提早降级、attempt 超时会误伤慢图并触发熔断,因此 `est` 校准是压测的首要目标。
|
||||
|
||||
### 6.2 并发承诺边界
|
||||
|
||||
@@ -294,7 +312,7 @@ inline / External v1 没有 queue job deadline 时,内部 RPC 仍必须有界
|
||||
|
||||
它不能绝对保证 BgFilter 服务端实际计算始终 `<= N`:客户端 timeout、进程退出或网络断开后,远端可能继续计算,而本地 permit 已被释放。若必须严格限制服务端计算,只能把全局 semaphore 放到 BgFilter 服务本身,或让 BgFilter 支持 request id、取消和状态查询。
|
||||
|
||||
两个子 worker 实例会得到 `2N`,因此首版使用固定内部监听地址和非模板化 systemd unit;同机第二实例应因固定端口 bind 失败。发布必须 `stop old -> 等待排空/退出 -> start new`,不能让新旧进程重叠。生产 `N` 或 `Q` 缺失、为 `0` 时 fail-closed。
|
||||
两个子 worker 实例会得到 `2N`,因此首版使用固定内部监听地址和非模板化 systemd unit;同机第二实例应因固定端口 bind 失败。发布必须 `stop old -> 等待排空/退出 -> start new`,不能让新旧进程重叠。生产 `N` 或 `est` 缺失、为 `0` 时 fail-closed;`Q` 可缺省(默认 `2048`),显式配置时必须 `>= N`。
|
||||
|
||||
### 6.3 熔断
|
||||
|
||||
@@ -306,7 +324,7 @@ inline / External v1 没有 queue job deadline 时,内部 RPC 仍必须有界
|
||||
- flat 在取得 permit、即将发送第一次 provider HTTP 前重新检查熔断,避免 48 个排队请求在熔断打开前全部通过旧检查。
|
||||
- 已经获准执行的逻辑调用,即使第一次失败使熔断打开,也仍允许在预算内完成自己的第二次顺序 attempt;后续请求快速返回 `circuit_open`。
|
||||
- 每个真实失败 attempt 计一次失败,保持当前计数口径;flat 任一真实 attempt 成功后重置。
|
||||
- 只有拿到完整配置 attempt 上限(默认 `180s`)后发生的 provider timeout,以及真实传输失败、非 2xx 和无效 / 超限图片计入。因 `requestBudgetMs` 剩余不足而被截短的 timeout 返回 `deadline_exceeded`,不更新熔断;排队满、排队超时、客户端取消、鉴权和本地配置错误同样不计入。
|
||||
- 只有拿到完整公式 attempt 上限(`N × est × 2`)后发生的 provider timeout,以及真实传输失败、非 2xx 和无效 / 超限图片计入。因 `callBudgetMs` 剩余不足而被截短的 timeout 返回 `deadline_exceeded`,不更新熔断;保险丝拒绝、排队超时、客户端取消、鉴权和本地配置错误同样不计入。
|
||||
- 进程重启后熔断状态清零是首版接受行为。
|
||||
|
||||
flat 熔断的 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD` 与 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS` 属于 `bgfilter-worker` 运行参数;父 API / external-generation worker 不再读取或更新熔断。生产示例必须把这两个值放进 worker 专属环境,避免运维人员在父侧修改了一个实际不生效的配置。
|
||||
@@ -320,9 +338,9 @@ flat 熔断的 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD` 与 `GENA
|
||||
- 父侧不得自动重试整次内部 HTTP。断连时结果未知,重试可能让一次逻辑调用从最多两次 provider attempt 扩大为四次,并可能突破瞬时并发预期。
|
||||
- 尚在等待 permit 的请求到达自身 deadline 后必须取消,不再发送 provider 请求;运行时若能可靠观察客户端断连,也可提前取消,但正确性不能只依赖断连事件。
|
||||
- 已经开始的 provider attempt 必须继续读取到完成或本次 attempt timeout,并持有 permit;可观察到的 handler / client drop 只丢弃最终结果,不能让已启动请求变成无人管理的本地 future。
|
||||
- deadline 已被子 worker 观察到后,不再开始第二次 attempt。首版没有显式 cancellation signal 通道;单纯 TCP 断连只能 best-effort 阻止二试(handler future 被 drop 后自然不再开始新 attempt),Axum / Hyper 不保证立刻通知 handler,因此不能承诺所有断连都阻止二试,`requestBudgetMs` deadline 是最终可靠的停止条件。
|
||||
- deadline 已被子 worker 观察到后,不再开始第二次 attempt。首版没有显式 cancellation signal 通道;单纯 TCP 断连只能 best-effort 阻止二试(handler future 被 drop 后自然不再开始新 attempt),Axum / Hyper 不保证立刻通知 handler,因此不能承诺所有断连都阻止二试,排队阶段的 queue timeout 与 permit 后的 `callBudgetMs` deadline 是最终可靠的停止条件。
|
||||
|
||||
实现时,已启动 provider attempt 应由持有 permit 的独立 task 管理;request handler 可观察到的 Drop / cancellation signal 只影响“是否继续重试和是否返回结果”,不直接丢弃已经开始的 provider future。`requestBudgetMs` deadline 是最终可靠的停止条件。
|
||||
实现时,已启动 provider attempt 应由持有 permit 的独立 task 管理;request handler 可观察到的 Drop / cancellation signal 只影响“是否继续重试和是否返回结果”,不直接丢弃已经开始的 provider future。`callBudgetMs` deadline 是最终可靠的停止条件。
|
||||
|
||||
### 7.2 进程崩溃
|
||||
|
||||
@@ -377,7 +395,7 @@ BgFilter 成功二进制不是一份新的业务资产:
|
||||
- MIME、魔数和实际解码结果必须一致;
|
||||
- 空 body、截断 body 和超限图片按 `invalid_result` 处理。
|
||||
|
||||
二进制跨进程传输期间,子 worker 和父 worker 可能同时持有同一张图片。子侧 provider permit、成功 body guard 和图片校验槽均与 `N` 对齐;guard 持有到内部响应发送完成或 body drop,完整成功 body 不会积累到 `Q` 级。父侧另有固定 `P = 4` 个成功图片读取 / 解码槽,必须在开始读取 `2xx` body 前取得,并覆盖有界 body 读取与 `spawn_blocking` 校验。极端完整 body 内存按 `(N + P) × 32 MiB` 再加父子解码缓冲、provider 读取缓冲和运行时开销评估;生产 `N / Q` 必须结合主机内存压测,而不是只看 BgFilter 吞吐。
|
||||
二进制跨进程传输期间,子 worker 和父 worker 可能同时持有同一张图片。子侧 provider permit、成功 body guard 和图片校验槽均与 `N` 对齐;guard 持有到内部响应发送完成或 body drop,完整成功 body 不会积累到排队规模。父侧另有固定 `P = 8` 个成功图片读取 / 解码槽,必须在开始读取 `2xx` body 前取得,并覆盖有界 body 读取与 `spawn_blocking` 校验;`P` 低于 `N` 时会在出口串行化压低吞吐,`N = 16` 配 `P = 8` 是内存与吞吐的折中。极端完整 body 内存按 `(N + P) × 32 MiB` 评估——`N = 16`、`P = 8` 时约 `768 MiB`——再加父子解码缓冲、provider 读取缓冲和运行时开销;生产 `N` 必须结合主机内存压测,而不是只看 BgFilter 吞吐。
|
||||
|
||||
图片解码运行在不可强制取消的 blocking task 中。父子两侧等待校验结果都必须受各自 deadline 约束;deadline 到达后请求可按类型化超时收口。子 worker 已启动但尚未结束的校验 task 继续持有图片字节和校验槽,并由 shutdown tracker 等待真实结束;provider `N` 只覆盖真实 provider 调用及内部响应发送,不因后台 CPU 校验延长而虚假占用。父侧超时后的 blocking task 继续持有父侧校验槽直到真实结束,防止后续大图无界叠加,但它没有外部副作用,不纳入子 worker 的 shutdown tracker。`TimeoutStopSec=900` 覆盖的是子 worker 的排空边界。
|
||||
|
||||
@@ -385,7 +403,8 @@ BgFilter 成功二进制不是一份新的业务资产:
|
||||
|
||||
最小指标:
|
||||
|
||||
- `bgfilter_internal_waiting_requests`
|
||||
- `bgfilter_internal_waiting_requests`(即 admission 后等待 `N` permit 的队长)
|
||||
- `bgfilter_internal_queue_timeout_total{bound}`(排队超时按触发边界 `estimate | parent` 分维度)
|
||||
- `bgfilter_internal_in_flight`
|
||||
- `bgfilter_internal_request_seconds{mode,outcome}`
|
||||
- `bgfilter_provider_http_seconds{mode,attempt,outcome}`
|
||||
@@ -404,9 +423,9 @@ BgFilter 成功二进制不是一份新的业务资产:
|
||||
1. 在现有 Rust 后端增加 `bgfilter-worker` 进程角色和独立 loopback Axum listener;它不启动用户 HTTP router,也不 claim `external_generation_job`。
|
||||
2. 增加内部 request / binary response / typed error 契约、Token 校验、JSON body 上限、object key allowlist 和健康检查;listener 在 body 解析前接入连接 / request concurrency limit、固定 backlog 和 load shedding。
|
||||
3. 增加 admission `Q`、`Semaphore(N)`、两次顺序 attempt、预算检查、结果限长 / 解码校验、response-body permit guard 和 flat 进程级熔断。
|
||||
4. 增加父侧共享内部 HTTP client。该 client 不自动重试;对 `2xx` 读取并返回受限图片字节,对非 `2xx` 只解析有界类型化 JSON 错误;把父剩余预算显式转换为较短的 `requestBudgetMs` 和略长的 client timeout,并保证两者都早于父绝对 deadline。
|
||||
4. 增加父侧共享内部 HTTP client。该 client 不自动重试;对 `2xx` 读取并返回受限图片字节,对非 `2xx` 只解析有界类型化 JSON 错误;把父剩余预算显式转换为 `maxQueueWaitMs` 与公式 `callBudgetMs`,client timeout 固定取两者之和加 `2s`。父绝对预算只在派生 `maxQueueWaitMs` 时扣除 callBudget 与父侧预留,不在发送阶段重新裁剪或挪用两笔相对预算。
|
||||
5. 用内部 client 替换两个集中调用边界:
|
||||
- flat:`remove_editor_generated_screen_background_with_bgfilter_with_request_timeout`;
|
||||
- flat:`remove_editor_generated_screen_background_with_bgfilter`;
|
||||
- complex:`request_editor_background_removal_image_with_bgfilter_worker`。
|
||||
6. 从父侧移除 BgFilter provider retry 和 flat 熔断实现;保留 flat fallback、complex 失败、Alpha / 尺寸恢复和所有最终持久化。
|
||||
7. 删除动画 `2000ms × frame_count` 单 attempt timeout 增量;保留现有所有帧 collect / drain。
|
||||
@@ -426,7 +445,7 @@ BgFilter 成功二进制不是一份新的业务资产:
|
||||
|
||||
内部 worker 不可用时禁止自动 direct fallback。flat 仍可走业务已有阿里云 / 本地 fallback;complex 明确失败。需要整体回滚时回滚父、子进程版本和配置,不在运行中混用两种 BgFilter 调度方式。
|
||||
|
||||
`bgfilter-worker` 收到停止信号后先停止接收新请求,再等待已 admission 的 handler 和已启动 provider attempt 排空。内部协议允许的 `requestBudgetMs` 最大为 `600s`,因此 systemd unit 固定使用 `TimeoutStopSec=900`;部署脚本的同步 `systemctl stop` 必须允许该窗口完成,不能沿用 systemd 常见的约 `90s` 默认值强杀在途调用。
|
||||
`bgfilter-worker` 收到停止信号后先停止接收新请求,并让仍在排队等待 `N` permit 的请求立即以类型化错误收口(父侧 flat 走 fallback),只等待已取得 permit 的调用和已启动 provider attempt 排空。排空上界由 `callBudgetMs`(`N = 16`、`est = 5s` 时约 `321s`)决定,因此 systemd unit 固定使用 `TimeoutStopSec=900` 仍有充分余量;部署脚本的同步 `systemctl stop` 必须允许该窗口完成,不能沿用 systemd 常见的约 `90s` 默认值强杀在途调用。排队请求不参与排空等待——它们尚未发出任何外部请求,快速失败是安全的。
|
||||
|
||||
### 10.3 本地开发
|
||||
|
||||
@@ -439,10 +458,11 @@ BgFilter 成功二进制不是一份新的业务资产:
|
||||
必须覆盖:
|
||||
|
||||
- `48` 帧同时进入时,唯一子 worker 观测到的客户端 BgFilter HTTP future 峰值不超过 `N`。
|
||||
- listener 在 body 解析前执行 concurrency limit / load shedding;`queued + running + egress` 达到 `Q` 后新请求立即返回 `overloaded`,handler 数不超过 `Q`,内核 socket backlog 按独立固定值验证。
|
||||
- 排队时间计入 `requestBudgetMs`;deadline 到达后不开始新的 provider attempt。
|
||||
- 不存在 BgFilter 子任务 claim 状态;成功 admission 后等待 `N` 的时间同时计入子 `requestBudgetMs` 和父 client timeout。
|
||||
- parent client timeout 取 `min(requestBudgetMs + parent transport window, 父剩余绝对预算)`;预算派生正常预留响应窗口,结果校验等待也必须 deadline-aware,不能只在校验完成后事后判超时。
|
||||
- listener 在 body 解析前执行鉴权与 admission;保险丝 `Q`(默认 `2048`)触达后新请求立即返回 `overloaded`,内核 socket backlog 按独立固定值验证。
|
||||
- 排队时间只消耗 `min((队长+5)×est×2, maxQueueWaitMs)`,不侵蚀 `callBudgetMs`;排队超时返回 `deadline_exceeded (phase=queue)` 且带触发边界;`callBudgetMs` 自取得 `N` permit 起算,deadline 到达后不开始新的 provider attempt。
|
||||
- `maxQueueWaitMs <= 0` 时父侧不发送请求:flat 直接 fallback,complex 直接失败。
|
||||
- `callBudgetMs` 与 worker 本进程公式值不一致时按 `invalid_request` 拒绝;attempt、callBudget、client timeout 全部由 `N / est` 运行时派生,代码不存在硬编码结果值。
|
||||
- parent client timeout 精确取 `maxQueueWaitMs + callBudgetMs + 2s`,helper 保持 infallible;父绝对预算通过 `maxQueueWaitMs` 的派生公式预先约束,结果校验等待也必须 deadline-aware,不能只在校验完成后事后判超时。
|
||||
- 第一次失败后预算不足时不开始第二次;父侧从不重试整次内部 RPC。
|
||||
- 父业务预算仍有效时,flat 两次失败、熔断、overload、内部 RPC deadline 或断连仍走“阿里云 → 本地”;complex 任意失败直接失败且不读写熔断。
|
||||
- flat 熔断按真实失败 attempt 计数;由剩余业务预算截短的 timeout 不计入。已获准调用可完成第二次,后续排队请求快速 `circuit_open`。
|
||||
@@ -453,19 +473,19 @@ BgFilter 成功二进制不是一份新的业务资产:
|
||||
- 成功 body 为原始图片字节而非 Base64、JSON 或结果 object key;父侧 client 把该有界字节缓冲直接交给现有后处理,子 worker 不执行 raw OSS PUT。父、子两侧都拒绝空 body、MIME / 魔数不一致、chunked 超 `32 MiB` 和超过 `8192 × 8192` 的图片。
|
||||
- `N / Q` response-body guard 在发送完成或 body drop 前不释放,慢读 / 断连时完整成功 body 不积累到 `Q` 级。
|
||||
- 子侧图片校验槽与 `N` 对齐;校验等待受 child deadline 约束,超时后的 blocking 校验 task 继续持有图片字节与校验槽,并被 shutdown tracker 排空;真实 provider 调用已经结束后不继续占用 `N`。
|
||||
- 父侧固定最多 `4` 个成功图片读取 / 解码槽,permit 在读取 `2xx` body 前取得并覆盖 `spawn_blocking` 校验;内部响应并发再高也不能无界累积父侧完整 body 或解码任务。
|
||||
- 父侧固定最多 `8` 个成功图片读取 / 解码槽,permit 在读取 `2xx` body 前取得并覆盖 `spawn_blocking` 校验;内部响应并发再高也不能无界累积父侧完整 body 或解码任务。
|
||||
- worker 重启 / RPC 丢失不查询、不恢复结果;父 job 的 heartbeat、lease、失败退款和 fencing 保持现状。
|
||||
- External v1 / inline 不再直连 BgFilter;公共 router、BFF、账单和任务列表中没有内部 endpoint 或内部调用记录。
|
||||
- 生产不存在两个同时运行的 `bgfilter-worker`,配置缺失或 `N / Q = 0` 时 fail-closed。
|
||||
- worker unit 先加载共享 API env、再加载 worker 专属 env;父子有效 `GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS` 完全一致,flat 熔断参数只由子 worker 配置和执行。
|
||||
- `external-generation-worker.env` 后加载时不得把内部 base URL、Token / Token 文件、connect timeout、provider attempt timeout、OSS bucket 或 endpoint 覆盖为与共享 API env 不同的有效值;父侧必须把源对象写到子 worker 将要签名读取的同一 OSS 位置。外部生成 worker 可使用同 bucket 下权限等价或更小的独立 AK,不要求凭据文本相同。
|
||||
- 生产不存在两个同时运行的 `bgfilter-worker`,`N` 或 `est` 缺失、为 `0` 时 fail-closed;`Q` 显式配置时必须 `>= N`。
|
||||
- worker unit 先加载共享 API env、再加载 worker 专属 env;父子有效 `N` 与 `est` 完全一致(两者都在共享 API env),flat 熔断参数只由子 worker 配置和执行。
|
||||
- `external-generation-worker.env` 后加载时不得把内部 base URL、Token / Token 文件、connect timeout、`N`、`est`、OSS bucket 或 endpoint 覆盖为与共享 API env 不同的有效值;父侧必须把源对象写到子 worker 将要签名读取的同一 OSS 位置。外部生成 worker 可使用同 bucket 下权限等价或更小的独立 AK,不要求凭据文本相同。
|
||||
- 父进程 `GENARRATIVE_BGFILTER_WORKER_BASE_URL`、子 worker `HOST / PORT` 和部署 readiness URL 必须指向同一个 `127.0.0.1:<port>` endpoint;旧非空配置不能因为“无需补默认值”而绕过一致性检查。
|
||||
- `genarrative-bgfilter-worker.service` 必须保持 `TimeoutStopSec=900`,覆盖最大 `600s` 内部请求预算和停止收口余量。
|
||||
- `genarrative-bgfilter-worker.service` 必须保持 `TimeoutStopSec=900`,覆盖 `callBudgetMs` 排空上界(约 `321s`)与停止收口余量;停止时排队请求立即类型化失败,不参与排空。
|
||||
- 发布目录切换前拒绝缺失、空、符号链接或权限错误的内部 Token 文件;父、子有效 Token 文件路径必须相同。
|
||||
- 生产运行期巡检同时检查 `genarrative-bgfilter-worker.service` 为 active 且 `127.0.0.1:8083/readyz` 成功,不能只依赖 systemd 自动重启。
|
||||
- `npm run dev` 启动独立 BgFilter 子进程并使用解析后的第五个端口;`all` 角色不内嵌 listener,单模块入口、watch、状态文件和退出清理没有遗留进程或硬编码端口。
|
||||
|
||||
本地全进程调度门禁使用已构建的 `api-server` binary 和 loopback mock provider,不读取真实 OSS / BgFilter 密钥,也不访问真实外部服务。`load-smoke` 固定验证 `R = 32 / 40 / 48、N = 4、Q = 128`;`fault-smoke` 用独立 worker / mock 生命周期验证 `Q` 满快速拒绝、queue deadline、`503 → 200` 顺序重试、两次 `503` 后 provider exhausted,以及 provider 成功响应 body 中途 reset 后第二次 attempt 串行成功。默认读取 `server-rs/target/debug/api-server(.exe)`;在 WSL 或自定义 target 目录运行时,通过 `GENARRATIVE_BGFILTER_SMOKE_BINARY` 指定 binary:
|
||||
本地全进程调度门禁使用已构建的 `api-server` binary 和 loopback mock provider,不读取真实 OSS / BgFilter 密钥,也不访问真实外部服务。`load-smoke` 固定验证 `R = 32 / 40 / 48、N = 16`(`Q` 用小值场景单独验证保险丝行为);`fault-smoke` 用独立 worker / mock 生命周期验证保险丝触达快速拒绝、queue timeout 双边界、`503 → 200` 顺序重试、两次 `503` 后 provider exhausted,以及 provider 成功响应 body 中途 reset 后第二次 attempt 串行成功。默认读取 `server-rs/target/debug/api-server(.exe)`;在 WSL 或自定义 target 目录运行时,通过 `GENARRATIVE_BGFILTER_SMOKE_BINARY` 指定 binary:
|
||||
|
||||
```bash
|
||||
cargo build -p api-server --manifest-path server-rs/Cargo.toml
|
||||
@@ -499,11 +519,12 @@ git diff --check
|
||||
|
||||
架构边界已固定:首版单实例、无 QPS、无数据库任务表、无 checkpoint / continuation、输出直接走内部二进制响应。
|
||||
|
||||
代码与部署接线已经完成;生产压测后、正式启用前需冻结两个容量值:
|
||||
当前冻结值与校准要求:
|
||||
|
||||
- 生产 `GENARRATIVE_BGFILTER_WORKER_CONCURRENCY = N`。
|
||||
- 生产 `GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS = Q`。若要求一个满帧动画不因自身 admission 被拒绝,`Q` 至少为 `48`;候选 `128` 可容纳两个满帧动画并留少量余量,但明确不能覆盖 controller 理论最大 `768` 次同时提交,超出部分会按 mode fallback 或失败。最终值以可接受的 overload 行为和内存压测为准。
|
||||
- 生产 `GENARRATIVE_BGFILTER_WORKER_CONCURRENCY = N = 16`。
|
||||
- 生产 `GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS = est = 5000`。依据:BgFilter 服务端高并发单图处理约 `1-3s`、网络约 `3-5s`。est 是 attempt、callBudget 与队列估时三个公式的共同地基,估计过小会导致排队提早降级、attempt 误伤慢图并触发熔断;生产压测的首要目标是校准该值。
|
||||
- `GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS = Q` 为可选保险丝,默认 `2048`,仅防连接风暴;显式配置时必须 `>= N`。
|
||||
|
||||
其余首版固定边界:provider 单 attempt 默认 `180s`,内部响应最大 `32 MiB / 8192 × 8192`,内部 listener 只绑定 loopback 且必须鉴权。若要多实例、严格服务端全局并发、QPS 或动画 peer cancellation,应先升级本文,不在编码中临时扩 scope。
|
||||
其余固定边界:内部响应最大 `32 MiB / 8192 × 8192`,父侧解码槽 `P = 8`,内部 listener 只绑定 loopback 且必须鉴权。旧的独立 provider attempt timeout 配置已删除,attempt 上限不再独立配置。若要多实例、严格服务端全局并发、QPS 或动画 peer cancellation,应先升级本文,不在编码中临时扩 scope。
|
||||
|
||||
配置归属同时冻结:`GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS=180000` 来自父子共同加载的 API 基础环境;`GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD`、`GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS`、`N` 与 `Q` 由 worker 专属环境管理。若部署脚本发现共享值被 worker 环境重复定义且不同,必须在启动前失败。
|
||||
配置归属同时冻结:`N` 与 `est` 来自父子共同加载的 API 基础环境(父侧算 callBudget / client timeout、worker 算 attempt / 队列估时都依赖它们);`GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD`、`GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS` 与可选的 `Q` 由 worker 专属环境管理。若部署脚本发现共享值被 worker 环境重复定义且不同,必须在启动前失败。
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -61,7 +61,7 @@
|
||||
仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用后端自动决策出的抠图背景色。纯色背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光。
|
||||
```
|
||||
|
||||
- 父流程收到 spritesheet 后先把带解析后纯色背景的源图 owned 上传私有 OSS(消费图片字节所有权,上传完成后释放原图缓冲,不克隆保留),写入项目资源和账号素材库;随后只持 object key,并仅向同机唯一 loopback `bgfilter-worker` 发起一次内部 HTTP RPC,请求中的源图只以 object key 传递,并附带 BgFilter 参数、剩余预算和有界审计关联,父流程不签发 BgFilter URL、不直连 provider,也不重试整次内部 RPC。子 worker 在 `Q` admission 和 `Semaphore(N)` 约束下执行这次逻辑调用,每次 provider attempt 前重新签发 600 秒 GET URL,multipart 固定传 `image_url`、`screen_color=<screenColor>`、`seg_model=<segModel>`、`background_mode=flat` 和 `cross_check=off`,不包含 `file`,并在内部 RPC 总预算内最多执行两次顺序 attempt,默认 `segModel=birefnet`。成功时,子 worker 通过内部 HTTP 二进制 body 把经过校验的图片字节直接返回父流程,不持久化中间结果;BgFilter 最终失败且父业务预算仍有效时,由父流程进入“阿里云通用抠图(按签名 URL 单独下载)→ 本地键色(再按 object key 独立下载一次原图并在产出后释放)”降级链。透明背景处理正常成功时,父流程把透明 spritesheet 写入 OSS、项目资源和账号素材库,再复用图标素材的连通域拆分能力;调用方未指定素材文件夹时落默认“项目”文件夹。BgFilter 与父侧 fallback 最终均失败、但 provider 原图已经持久化时,任务以 `completed + warning` 收口,只把 provider 原图作为唯一主图放入画布,`generatedLayerId` 指向原图,不创建透明图集,也不继续拆分。该收口只捕获透明背景处理本身的最终失败;phase 上报、provider 原图持久化、透明处理图持久化和 `canvasCompletion` 写回错误仍正常传播,不能被原图降级吞掉。最终透明结果及拆分切片的 OSS / 资源 / 画布持久化仍全部由父流程负责。
|
||||
- 父流程收到 spritesheet 后先把带解析后纯色背景的源图 owned 上传私有 OSS(消费图片字节所有权,上传完成后释放原图缓冲,不克隆保留),写入项目资源和账号素材库;随后只持 object key,并仅向同机唯一 loopback `bgfilter-worker` 发起一次内部 HTTP RPC,请求中的源图只以 object key 传递,并附带 BgFilter 参数、排队预算 `maxQueueWaitMs`、调用预算 `callBudgetMs` 和有界审计关联,父流程不签发 BgFilter URL、不直连 provider,也不重试整次内部 RPC。子 worker 在 `Q` admission 和 `Semaphore(N)` 约束下执行这次逻辑调用;排队只消耗 `maxQueueWaitMs`,取得 provider permit 后才启动 `callBudgetMs`。每次 provider attempt 前重新签发 600 秒 GET URL,multipart 固定传 `image_url`、`screen_color=<screenColor>`、`seg_model=<segModel>`、`background_mode=flat` 和 `cross_check=off`,不包含 `file`,并在调用预算内最多执行两次顺序 attempt,默认 `segModel=birefnet`。成功时,子 worker 通过内部 HTTP 二进制 body 把经过校验的图片字节直接返回父流程,不持久化中间结果;BgFilter 最终失败且父业务预算仍有效时,由父流程进入“阿里云通用抠图(按签名 URL 单独下载)→ 本地键色(再按 object key 独立下载一次原图并在产出后释放)”降级链。透明背景处理正常成功时,父流程把透明 spritesheet 写入 OSS、项目资源和账号素材库,再复用图标素材的连通域拆分能力;调用方未指定素材文件夹时落默认“项目”文件夹。BgFilter 与父侧 fallback 最终均失败、但 provider 原图已经持久化时,任务以 `completed + warning` 收口,只把 provider 原图作为唯一主图放入画布,`generatedLayerId` 指向原图,不创建透明图集,也不继续拆分。该收口只捕获透明背景处理本身的最终失败;phase 上报、provider 原图持久化、透明处理图持久化和 `canvasCompletion` 写回错误仍正常传播,不能被原图降级吞掉。最终透明结果及拆分切片的 OSS / 资源 / 画布持久化仍全部由父流程负责。
|
||||
- UI 素材自动拆分只在透明图集成功后执行,与图标图集一致,属于 best-effort 附加动作。未知素材数量时按从上到下、从左到右自动命名为 `素材 1`、`素材 2`;识别或切片持久化失败仍返回整张透明图集和 `sliceWarning`,前端显示非阻断 warning toast,用户可手动重试。`sliceWarning` 与透明背景最终失败使用的通用 `warning` 互斥,前者只表示透明图集成功但自动拆分失败,`sliceWarning.reason` 原始契约保持不变。
|
||||
- 正常透明化成功时,前端先把透明 spritesheet 作为 `assetKind: "icon-spritesheet"` 图集图层放在 UI 设计图右侧,再把拆分成功的独立素材作为 `assetKind: "icon"` 图标图层继续放到画布;透明背景处理最终失败时只消费后端快照中的 provider 原图。透明图集图层提供 `拆分图集` 工具栏按钮,可使用相同连通域规则重新拆分。
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
|
||||
## 去背与保存
|
||||
|
||||
- 父流程收到 spritesheet 后先把带解析后纯色背景的源图写入私有 OSS,并在上传完成后释放原图缓冲;随后只持 object key,并仅向同机唯一 loopback `bgfilter-worker` 发起一次内部 HTTP RPC,请求中的源图只以 object key 传递,并附带 BgFilter 参数、剩余预算和有界审计关联,父流程不签发 BgFilter URL、不直连 provider,也不重试整次内部 RPC。子 worker 在 `Q` admission 和 `Semaphore(N)` 约束下执行这次逻辑调用,每次 provider attempt 前重新签发 600 秒 GET URL,multipart 固定传 `image_url`、`screen_color=<screenColor>`、`seg_model=<segModel>`、`background_mode=flat` 和 `cross_check=off`,不包含 `file`,并在内部 RPC 总预算内最多执行两次顺序 attempt。前端用户路径固定提交 `screenColor=auto` 与默认 `segModel=birefnet`,后端仍识别内部保留的 `anime-seg`,但这些内部参数不对用户可见。成功时,子 worker 通过内部 HTTP 二进制 body 把经过校验的图片字节直接返回父流程,不持久化中间结果;BgFilter 最终失败且父业务预算仍有效时,由父流程进入“阿里云通用抠图(按签名 URL 单独下载)→ 本地键色(再按 object key 独立下载一次原图并在产出后释放)”降级链。
|
||||
- 父流程收到 spritesheet 后先把带解析后纯色背景的源图写入私有 OSS,并在上传完成后释放原图缓冲;随后只持 object key,并仅向同机唯一 loopback `bgfilter-worker` 发起一次内部 HTTP RPC,请求中的源图只以 object key 传递,并附带 BgFilter 参数、排队预算 `maxQueueWaitMs`、调用预算 `callBudgetMs` 和有界审计关联,父流程不签发 BgFilter URL、不直连 provider,也不重试整次内部 RPC。子 worker 在 `Q` admission 和 `Semaphore(N)` 约束下执行这次逻辑调用;排队只消耗 `maxQueueWaitMs`,取得 provider permit 后才启动 `callBudgetMs`。每次 provider attempt 前重新签发 600 秒 GET URL,multipart 固定传 `image_url`、`screen_color=<screenColor>`、`seg_model=<segModel>`、`background_mode=flat` 和 `cross_check=off`,不包含 `file`,并在调用预算内最多执行两次顺序 attempt。前端用户路径固定提交 `screenColor=auto` 与默认 `segModel=birefnet`,后端仍识别内部保留的 `anime-seg`,但这些内部参数不对用户可见。成功时,子 worker 通过内部 HTTP 二进制 body 把经过校验的图片字节直接返回父流程,不持久化中间结果;BgFilter 最终失败且父业务预算仍有效时,由父流程进入“阿里云通用抠图(按签名 URL 单独下载)→ 本地键色(再按 object key 独立下载一次原图并在产出后释放)”降级链。
|
||||
- 透明背景处理正常成功时,父流程把带背景原图和去背后的透明 spritesheet 写入 OSS、项目资源和账号素材库,再按 alpha 连通域和素材描述顺序执行附加拆分;若 BgFilter 返回较小图集,只把 alpha 蒙版重采样到 provider 原图尺寸并应用回原始高分辨率 RGB,不放大低分辨率后处理成品。画布完成快照同时写入透明主图与右侧 provider 原图(二者均已登记为 project resource / 账号素材),`generatedLayerId` 仍锚定透明主图;成功拆出的切片从 provider 原图右侧继续排列。调用方未指定素材文件夹时统一落默认“项目”文件夹。每个成功切片单独写入 OSS、项目资源和账号素材库,`sourceResourceId` 指向透明图集资源。BgFilter 与父侧 fallback 最终均失败、但 provider 原图已经持久化时,任务以 `completed + warning` 收口,只把 provider 原图作为唯一主图放入画布,`generatedLayerId` 指向原图,不创建透明图集,也不继续拆分,`iconImageSrcs=[]`。该收口只捕获透明背景处理本身的最终失败;phase 上报、provider 原图持久化、透明处理图持久化和 `canvasCompletion` 写回错误仍正常传播,不能被原图降级吞掉。最终透明结果及切片的 OSS / 资源 / 画布持久化仍全部由父流程负责。
|
||||
- 自动拆分只在透明图集成功后执行,属于 best-effort 附加动作,不参与图集生成的成功判定。连通域识别或切片持久化失败时,接口仍返回并回填整张透明图集,`iconImageSrcs=[]`,并通过 `sliceWarning.code/reason` 暴露非阻断原因;`sliceWarning` 与透明背景最终失败使用的通用 `warning` 互斥,前者只表示透明图集成功但自动拆分失败,`sliceWarning.reason` 原始契约保持不变。前端在 inline、worker 队列完成和刷新恢复三条路径统一显示对应 warning toast,用户可在图集工具栏手动重试。
|
||||
- 响应通过 `iconImageSrcs` 返回成功切片素材;自动生成使用用户输入的素材描述命名,UI 设计提取和手动拆分按从上到下、从左到右自动命名为 `素材 N`。
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
角色设定:<用户输入的角色设定>
|
||||
```
|
||||
|
||||
- 角色图生成完成后,编辑器父流程必须先把带自动决策纯色背景的源图 owned 上传私有 OSS(消费图片字节所有权,上传完成后释放原图缓冲),随后只持 object key,并仅向同机唯一 loopback `bgfilter-worker` 发起一次内部 HTTP RPC;请求中的源图只以 object key 传递,并附带 BgFilter 参数、剩余预算和有界审计关联,父流程不签发 BgFilter URL、不直连 provider,也不重试整次内部 RPC。子 worker 在 `Q` admission 和 `Semaphore(N)` 约束下执行这次逻辑调用,每次 provider attempt 前重新签发 600 秒 GET URL,multipart 字段包含 `image_url`、`screen_color=<screenColor>`、`seg_model=<segModel>`、`background_mode=flat` 和 `cross_check=on`,不包含 `file`,并在内部 RPC 总预算内最多执行两次顺序 attempt;用户路径默认并只提交 `seg_model=birefnet`,`flat` 明确表示单一纯色背景抠图模式,`birefnet` 是 BgFilter 管线内部后端。成功时,子 worker 通过内部 HTTP 二进制 body 把经过校验的图片字节直接返回父流程,不持久化中间结果;BgFilter 最终失败且父业务预算仍有效时,由父流程进入“阿里云通用抠图(按签名 URL 单独下载)→ 本地键色(再按 object key 独立下载一次原图并在产出后释放)”降级链。角色图 prompt 按 `screenColor` 写入颜色名称、hex 和 RGB。该流程不再调用 RPG / 资产工坊的角色主图专用 `character_visual_assets` 后处理,也不复用手动去背景的 `background_mode=complex` 路径。透明背景处理正常成功时,父流程输出透明背景 PNG,随后写入 OSS 私有对象并确认 `asset_object`;接口回包返回 `imageSrc: "/<objectKey>"`、`objectKey`、`assetObjectId` 及资源快照,画布同时写入透明主结果和 provider 原图,生成器 `generatedLayerId` 锚定透明主结果,provider 原图作为第二个图层放在其右侧。BgFilter、阿里云与本地键色最终均失败、但 provider 原图已经持久化时,任务以 `completed + warning` 收口,只把 provider 原图作为唯一主图放入画布,`generatedLayerId` 指向原图,不创建不存在的透明处理图;通用 `warning.code/reason` 携带完整降级原因。该收口只捕获透明背景处理本身的最终失败;phase 上报、provider 原图持久化、透明处理图持久化和 `canvasCompletion` 写回错误仍正常传播,不能被原图降级吞掉。最终透明结果的 OSS / 资源 / 画布持久化仍全部由父流程负责;前端创建图层和画板资源记录时必须保存最终回包对应的媒体引用。
|
||||
- 角色图生成完成后,编辑器父流程必须先把带自动决策纯色背景的源图 owned 上传私有 OSS(消费图片字节所有权,上传完成后释放原图缓冲),随后只持 object key,并仅向同机唯一 loopback `bgfilter-worker` 发起一次内部 HTTP RPC;请求中的源图只以 object key 传递,并附带 BgFilter 参数、排队预算 `maxQueueWaitMs`、调用预算 `callBudgetMs` 和有界审计关联,父流程不签发 BgFilter URL、不直连 provider,也不重试整次内部 RPC。子 worker 在默认 `Q=2048` admission 保险丝和 `Semaphore(N=16)` 约束下执行这次逻辑调用,每次 provider attempt 前重新签发 600 秒 GET URL,multipart 字段包含 `image_url`、`screen_color=<screenColor>`、`seg_model=<segModel>`、`background_mode=flat` 和 `cross_check=on`,不包含 `file`。排队只消耗 `maxQueueWaitMs`;取得 provider permit 后才启动 `callBudgetMs`,attempt 按 `N × est × 2`、调用预算按 `2 × attempt + 1s` 派生,冻结 `est=5000ms` 时分别为 `160s / 321s`,同一次逻辑调用最多执行两次顺序 attempt。用户路径默认并只提交 `seg_model=birefnet`,`flat` 明确表示单一纯色背景抠图模式,`birefnet` 是 BgFilter 管线内部后端。成功时,子 worker 通过内部 HTTP 二进制 body 把经过校验的图片字节直接返回父流程,不持久化中间结果;BgFilter 最终失败且父业务预算仍有效时,由父流程进入“阿里云通用抠图(按签名 URL 单独下载)→ 本地键色(再按 object key 独立下载一次原图并在产出后释放)”降级链。角色图 prompt 按 `screenColor` 写入颜色名称、hex 和 RGB。该流程不再调用 RPG / 资产工坊的角色主图专用 `character_visual_assets` 后处理,也不复用手动去背景的 `background_mode=complex` 路径。透明背景处理正常成功时,父流程输出透明背景 PNG,随后写入 OSS 私有对象并确认 `asset_object`;接口回包返回 `imageSrc: "/<objectKey>"`、`objectKey`、`assetObjectId` 及资源快照,画布同时写入透明主结果和 provider 原图,生成器 `generatedLayerId` 锚定透明主结果,provider 原图作为第二个图层放在其右侧。BgFilter、阿里云与本地键色最终均失败、但 provider 原图已经持久化时,任务以 `completed + warning` 收口,只把 provider 原图作为唯一主图放入画布,`generatedLayerId` 指向原图,不创建不存在的透明处理图;通用 `warning.code/reason` 携带完整降级原因。该收口只捕获透明背景处理本身的最终失败;phase 上报、provider 原图持久化、透明处理图持久化和 `canvasCompletion` 写回错误仍正常传播,不能被原图降级吞掉。最终透明结果的 OSS / 资源 / 画布持久化仍全部由父流程负责;前端创建图层和画板资源记录时必须保存最终回包对应的媒体引用。
|
||||
- 对 `assetKind: "character"` 的角色图层执行 `重绘` 时,前端仍使用原图作为参考图,但请求 `kind` 必须传 `character`,让后端继续套用上述角色提示词限定、角色图后处理和角色资产持久化;透明背景正常成功与最终失败保留 provider 原图的收口规则和角色新生成一致。普通图片图层重绘仍保持 `kind: "quick-edit"`。
|
||||
|
||||
## 生成规范参考图
|
||||
@@ -166,7 +166,7 @@
|
||||
- 抽帧采样必须按目标帧数预留视频尾部安全步长,例如 `32帧·4秒` 最后一帧采 `3.875s`,避免 FFmpeg 在尾点附近返回成功但输出 `0` 帧。
|
||||
- 图片画布角色动作的 FFmpeg 原始帧在上传 OSS 前必须转为 RGB8,并按最终帧宽高的 contain 比例使用 `Triangle` 只缩放到内容尺寸;不得提前创建最终目标尺寸 RGBA 画布,不得引入 Alpha 通道或透明 padding。以 `560×752` 原始帧、`323×480` 最终目标为例,上传给抠图链路的源帧必须是 `323×434 RGB8 PNG`,没有上下补边。抽帧解码后若携带 Alpha 通道,必须先把像素按白底合成为不透明再转 RGB8,禁止直接丢弃 Alpha——全透明像素下未定义的 RGB 值会以杂色进入抠图输入,重新引入杂色边缘;共享 FFmpeg 抽帧命令保持不固定 `-pix_fmt`,白底合成只属于该链路的 BgFilter 输入准备阶段。
|
||||
- 后端先计算整批精确采样时刻,再用单个 FFmpeg filter graph 统一解码预览视频并输出 `32 / 40 / 48` 张源帧;不得为每帧重新启动 FFmpeg、重复解码同一视频,也不得用会改变现有尾帧安全时刻的粗粒度 `fps` 抽帧替代。批量命令成功后必须逐一确认全部目标帧文件存在,缺少任一帧都按整批失败处理并保留缺帧编号、目标时刻和输出路径诊断。
|
||||
- 每帧绿幕源图字节由上传 owned 消费(`frame.bytes` 移入 put,上传完成后释放原帧缓冲,不克隆保留);后续父流程只持 object key,并为每帧向唯一 loopback `bgfilter-worker` 发起一次内部 HTTP RPC,不直接签发 BgFilter URL、不直连 provider,也不重试整次内部 RPC。子 worker 在 `Q` admission 和 `Semaphore(N)` 约束下调度每帧逻辑调用,每次 provider attempt 前重新签发 600 秒 GET URL,multipart 仅传 `image_url`(加 `background_mode=flat`、`seg_model=birefnet`、`cross_check=on` 与同一次生成已选定的 `screenColor`),不传 `file`。每个真实 provider attempt 的上限统一为 `GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS=180000ms`,不再按帧数增加;`32 / 40 / 48` 帧使用相同的单 attempt 上限,排队、等待 `N`、最多两次顺序 attempt、结果校验和响应构造都由该帧内部 RPC 的总预算覆盖。成功图片由子 worker 以内部 HTTP 二进制 body 返回父流程,不在子侧落 OSS;BgFilter 最终失败且父业务预算仍有效时,由父流程进入 `阿里云通用抠图(按签名 URL 单独下载)→ 本地 editor_green_screen(再按 object key 独立下载一次并在产出后释放)`。
|
||||
- 每帧绿幕源图字节由上传 owned 消费(`frame.bytes` 移入 put,上传完成后释放原帧缓冲,不克隆保留);后续父流程只持 object key,并为每帧向唯一 loopback `bgfilter-worker` 发起一次内部 HTTP RPC,不直接签发 BgFilter URL、不直连 provider,也不重试整次内部 RPC。每帧请求分别携带按父剩余绝对预算派生的 `maxQueueWaitMs` 和公式化 `callBudgetMs`;子 worker 在默认 `Q=2048` admission 保险丝和 `Semaphore(N=16)` 约束下排队,取得 provider permit 后才启动调用预算,每次 provider attempt 前重新签发 600 秒 GET URL,multipart 仅传 `image_url`(加 `background_mode=flat`、`seg_model=birefnet`、`cross_check=on` 与同一次生成已选定的 `screenColor`),不传 `file`。真实 provider attempt 按 `N × est × 2` 派生,调用预算按 `2 × attempt + 1s` 派生;冻结 `est=5000ms` 时分别为 `160s / 321s`,排队不侵蚀最多两次顺序 attempt 的完整窗口,`32 / 40 / 48` 帧也不再增加单帧 attempt。成功图片由子 worker 以内部 HTTP 二进制 body 返回父流程,不在子侧落 OSS;BgFilter 最终失败且父业务预算仍有效时,由父流程进入 `阿里云通用抠图(按签名 URL 单独下载)→ 本地 editor_green_screen(再按 object key 独立下载一次并在产出后释放)`。
|
||||
- BgFilter、阿里云或本地键色返回透明结果后,父流程继续通过现有最终帧 finalizer 转为 RGBA8,按宽高比居中放入最终目标尺寸,并使用 `RGBA(0,0,0,0)` 补边;最终帧 OSS 与业务写回仍由父流程完成。上述样例最终输出必须为 `323×480 RGBA8 PNG`,顶部和底部各 `23px` 透明 padding,内容区域完整保留抠图结果。
|
||||
- 全部 `32 / 40 / 48` 帧以覆盖本次所有帧的无序在途集合向内部 worker 提交,允许乱序完成并最终按 `frameIndex` 排序;BgFilter provider 的实际在途请求受唯一 worker 的 `N / Q` 限制。任一帧最终失败时先排空全部已启动 Future,再让整项任务失败退款,不发布缺帧动画。
|
||||
- 抽帧结果写入 OSS,并返回帧路径、帧尺寸、帧数、fps、预览视频路径、模型、价格和实际 prompt。
|
||||
|
||||
@@ -22,16 +22,20 @@ const LOAD_SCENARIOS = Object.freeze(
|
||||
}),
|
||||
),
|
||||
);
|
||||
const DEFAULT_WORKER_CONCURRENCY = 4;
|
||||
const DEFAULT_WORKER_MAX_REQUESTS = 128;
|
||||
const DEFAULT_WORKER_CONCURRENCY = 16;
|
||||
const DEFAULT_WORKER_MAX_REQUESTS = 2048;
|
||||
const PROVIDER_DELAY_MS = 100;
|
||||
const REQUEST_BUDGET_MS = 30_000;
|
||||
// 单图估时(est):attempt = N×est×2、callBudget = 2×attempt+1s 全部由它派生,
|
||||
// 请求携带的 callBudgetMs 必须与 worker 端公式一致,否则被按 invalid_request 拒绝。
|
||||
// 取 2500 使 N=1 的 fault 场景 attempt = 5s,与旧固定值一致,容纳 provider gate 阻塞窗口。
|
||||
const SINGLE_IMAGE_ESTIMATE_MS = 2_500;
|
||||
const MAX_QUEUE_WAIT_MS = 30_000;
|
||||
const REQUEST_TIMEOUT_MS = 35_000;
|
||||
const WORKER_START_TIMEOUT_MS = 20_000;
|
||||
const LOAD_SMOKE_TIMEOUT_MS = 60_000;
|
||||
const FAULT_SCENARIO_TIMEOUT_MS = 15_000;
|
||||
const OVERLOAD_RESPONSE_MAX_MS = 750;
|
||||
const QUEUE_DEADLINE_BUDGET_MS = 1_200;
|
||||
const QUEUE_DEADLINE_MAX_WAIT_MS = 1_200;
|
||||
const MAX_CAPTURED_LOG_BYTES = 64 * 1024;
|
||||
const MAX_MULTIPART_BYTES = 256 * 1024;
|
||||
const FAKE_OSS_ACCESS_KEY_ID = 'bgfilter-load-smoke-access-key';
|
||||
@@ -75,7 +79,7 @@ export function buildIsolatedWorkerEnv({
|
||||
GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS: String(maxRequests),
|
||||
GENARRATIVE_BGFILTER_WORKER_PORT: String(workerPort),
|
||||
GENARRATIVE_EDITOR_BGFILTER_BASE_URL: providerBaseUrl,
|
||||
GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS: '5000',
|
||||
GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS: String(SINGLE_IMAGE_ESTIMATE_MS),
|
||||
GENARRATIVE_EDITOR_GENERATION_PRICING_OVERRIDE_PATH: path.join(
|
||||
tempRoot,
|
||||
'missing-pricing-override.json',
|
||||
@@ -438,7 +442,8 @@ async function runOverloadFaultScenario() {
|
||||
timeoutMs: FAULT_SCENARIO_TIMEOUT_MS,
|
||||
},
|
||||
async ({ provider, signal, token, workerBaseUrl }) => {
|
||||
const scenario = { mode: 'complex', name: 'fault-overload' };
|
||||
// callBudgetMs 必须按本场景 worker 的实际 N 派生,否则被 invalid_request 拒绝。
|
||||
const scenario = { concurrency: 2, mode: 'complex', name: 'fault-overload' };
|
||||
const admitted = Array.from({ length: 4 }, (_, index) =>
|
||||
requestWorker(workerBaseUrl, token, scenario, index, {
|
||||
signal,
|
||||
@@ -516,7 +521,7 @@ async function runQueueDeadlineFaultScenario() {
|
||||
timeoutMs: FAULT_SCENARIO_TIMEOUT_MS,
|
||||
},
|
||||
async ({ provider, signal, token, workerBaseUrl }) => {
|
||||
const scenario = { mode: 'complex', name: 'fault-queue-deadline' };
|
||||
const scenario = { concurrency: 1, mode: 'complex', name: 'fault-queue-deadline' };
|
||||
const firstRequest = requestWorker(
|
||||
workerBaseUrl,
|
||||
token,
|
||||
@@ -538,7 +543,7 @@ async function runQueueDeadlineFaultScenario() {
|
||||
scenario,
|
||||
1,
|
||||
{
|
||||
requestBudgetMs: QUEUE_DEADLINE_BUDGET_MS,
|
||||
maxQueueWaitMs: QUEUE_DEADLINE_MAX_WAIT_MS,
|
||||
signal,
|
||||
timeoutMs: 3_500,
|
||||
},
|
||||
@@ -603,7 +608,7 @@ async function runRetryThenSuccessFaultScenario() {
|
||||
timeoutMs: FAULT_SCENARIO_TIMEOUT_MS,
|
||||
},
|
||||
async ({ provider, signal, token, workerBaseUrl }) => {
|
||||
const scenario = { mode: 'complex', name: 'fault-retry-success' };
|
||||
const scenario = { concurrency: 2, mode: 'complex', name: 'fault-retry-success' };
|
||||
const response = await requestWorker(
|
||||
workerBaseUrl,
|
||||
token,
|
||||
@@ -643,7 +648,7 @@ async function runProviderExhaustedFaultScenario() {
|
||||
timeoutMs: FAULT_SCENARIO_TIMEOUT_MS,
|
||||
},
|
||||
async ({ provider, signal, token, workerBaseUrl }) => {
|
||||
const scenario = { mode: 'complex', name: 'fault-provider-exhausted' };
|
||||
const scenario = { concurrency: 2, mode: 'complex', name: 'fault-provider-exhausted' };
|
||||
const response = await requestWorker(
|
||||
workerBaseUrl,
|
||||
token,
|
||||
@@ -691,7 +696,7 @@ async function runMidBodyResetThenSuccessFaultScenario() {
|
||||
timeoutMs: FAULT_SCENARIO_TIMEOUT_MS,
|
||||
},
|
||||
async ({ provider, signal, token, workerBaseUrl }) => {
|
||||
const scenario = { mode: 'complex', name: 'fault-mid-body-reset' };
|
||||
const scenario = { concurrency: 2, mode: 'complex', name: 'fault-mid-body-reset' };
|
||||
const response = await requestWorker(
|
||||
workerBaseUrl,
|
||||
token,
|
||||
@@ -948,7 +953,7 @@ function requestWorker(baseUrl, token, scenario, index, options = {}) {
|
||||
scenario,
|
||||
requestId,
|
||||
index,
|
||||
options.requestBudgetMs,
|
||||
options.maxQueueWaitMs,
|
||||
),
|
||||
),
|
||||
'utf8',
|
||||
@@ -968,16 +973,25 @@ function requestWorker(baseUrl, token, scenario, index, options = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
export function computeCallBudgetMs(
|
||||
concurrency = DEFAULT_WORKER_CONCURRENCY,
|
||||
estimateMs = SINGLE_IMAGE_ESTIMATE_MS,
|
||||
) {
|
||||
// 与 Rust 侧 AppConfig::bgfilter_call_budget_ms 完全一致:2 × (N × est × 2) + 1s。
|
||||
return 2 * (Math.max(concurrency, 1) * Math.max(estimateMs, 1) * 2) + 1_000;
|
||||
}
|
||||
|
||||
function buildScenarioRequest(
|
||||
scenario,
|
||||
requestId,
|
||||
index,
|
||||
requestBudgetMs = scenario.requestBudgetMs ?? REQUEST_BUDGET_MS,
|
||||
maxQueueWaitMs = scenario.maxQueueWaitMs ?? MAX_QUEUE_WAIT_MS,
|
||||
) {
|
||||
const common = {
|
||||
backgroundMode: scenario.mode,
|
||||
callBudgetMs: computeCallBudgetMs(scenario.concurrency),
|
||||
crossCheck: false,
|
||||
requestBudgetMs,
|
||||
maxQueueWaitMs,
|
||||
requestId,
|
||||
segModel: 'birefnet',
|
||||
sourceObjectKey: `generated-character-drafts/bgfilter-load-smoke/${scenario.name}/frame-${String(index).padStart(3, '0')}.png`,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import {
|
||||
chmodSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
@@ -35,6 +36,7 @@ if (failures.length > 0) {
|
||||
console.log('[check:production-api-deploy] OK');
|
||||
|
||||
function main() {
|
||||
assertDeployScriptSupportsProtectedEnvCleanup();
|
||||
assertDeployCopiesPingoraDirectReleaseDependencies();
|
||||
assertSuccessfulDeployCanKeepMaintenance();
|
||||
assertDeployRestartsActivePingoraWhenArtifactIncluded();
|
||||
@@ -45,7 +47,8 @@ function main() {
|
||||
assertDeployRejectsPingoraArtifactMissingManifestEntry();
|
||||
assertDeployRejectsPingoraManifestEntryMissingArtifact();
|
||||
assertDeployRequiresPingoraWhenRequested();
|
||||
assertDeployRejectsInvalidSharedBgFilterRequestTimeout();
|
||||
assertDeployRejectsInvalidSharedBgFilterEstimate();
|
||||
assertDeployRemovesRetiredBgFilterEnvThroughSudo();
|
||||
assertDeployRejectsBgFilterWorkerSharedEnvDrift();
|
||||
assertDeployRejectsEmptyBgFilterWorkerSharedEnvOverride();
|
||||
assertDeployRejectsExternalGenerationWorkerBgFilterEnvDrift();
|
||||
@@ -53,6 +56,7 @@ function main() {
|
||||
assertDeployRejectsBgFilterHealthEndpointDrift();
|
||||
assertDeployRejectsNonLoopbackBgFilterListener();
|
||||
assertDeployRejectsMissingBgFilterWorkerCapacity();
|
||||
assertDeployMigratesOldDefaultBgFilterAdmissionLimit();
|
||||
assertDeployRejectsInvalidBgFilterWorkerCapacity();
|
||||
assertDeployRejectsInlineBgFilterInternalToken();
|
||||
assertDeployRejectsEmptyBgFilterInternalToken();
|
||||
@@ -94,6 +98,18 @@ function main() {
|
||||
assertMissingPingoraCanaryAccessLogParityFails();
|
||||
}
|
||||
|
||||
function assertDeployScriptSupportsProtectedEnvCleanup() {
|
||||
const deployScript = readFileSync(
|
||||
'scripts/deploy/production-api-deploy.sh',
|
||||
'utf8',
|
||||
);
|
||||
assertIncludes(
|
||||
deployScript,
|
||||
'removal_result="$(sudo -n python3 -c "${python_script}" "${file_path}" "${key}")"',
|
||||
'退役 BgFilter env 键清理必须保留 sudo 读写分支。',
|
||||
);
|
||||
}
|
||||
|
||||
function assertSuccessfulDeployCanKeepMaintenance() {
|
||||
const fixture = prepareFixture('keep-maintenance-after-success');
|
||||
const result = runDeploy(fixture, { keepMaintenance: true });
|
||||
@@ -349,7 +365,7 @@ function assertDeployCopiesPingoraDirectReleaseDependencies() {
|
||||
assertIncludes(
|
||||
bgfilterUnit,
|
||||
'TimeoutStopSec=900',
|
||||
'BgFilter unit 必须给最多 600s 的内部请求预算留足优雅排空时间。',
|
||||
'BgFilter unit 必须给取得 permit 后的公式化 callBudget 留足优雅排空时间。',
|
||||
);
|
||||
assertFileExists(
|
||||
path.join(releaseDir, 'deploy/pingora/pingora-gateway.env.example'),
|
||||
@@ -380,7 +396,8 @@ function assertDeployCopiesPingoraDirectReleaseDependencies() {
|
||||
'BgFilter 专属 env 必须提供 flat 熔断阈值。',
|
||||
);
|
||||
for (const sharedKey of [
|
||||
'GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS=',
|
||||
'GENARRATIVE_BGFILTER_WORKER_CONCURRENCY=',
|
||||
'GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS=',
|
||||
'GENARRATIVE_EDITOR_BGFILTER_BASE_URL=',
|
||||
'ALIYUN_OSS_ACCESS_KEY_ID=',
|
||||
]) {
|
||||
@@ -815,31 +832,76 @@ function assertReadinessFailureKeepsMaintenanceAfterCurrentSwitch() {
|
||||
}
|
||||
}
|
||||
|
||||
function assertDeployRejectsInvalidSharedBgFilterRequestTimeout() {
|
||||
const fixture = prepareFixture('invalid-shared-bgfilter-request-timeout');
|
||||
function assertDeployRejectsInvalidSharedBgFilterEstimate() {
|
||||
const fixture = prepareFixture('invalid-shared-bgfilter-estimate');
|
||||
writeFileSync(
|
||||
fixture.apiEnvFile,
|
||||
`${readFileSync(fixture.apiEnvFile, 'utf8')}GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS=invalid\n`,
|
||||
`${readFileSync(fixture.apiEnvFile, 'utf8')}GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS=invalid\n`,
|
||||
'utf8',
|
||||
);
|
||||
const result = runDeploy(fixture);
|
||||
|
||||
if (result.status === 0) {
|
||||
failures.push('共享 BgFilter provider attempt timeout 非法时部署必须失败。');
|
||||
failures.push('共享 BgFilter 单图估时非法时部署必须失败。');
|
||||
}
|
||||
assertIncludes(
|
||||
result.stderr,
|
||||
'GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS 必须在共享 API env 中配置为正整数毫秒',
|
||||
'共享 BgFilter timeout 预检必须给出明确错误。',
|
||||
'GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS 必须在共享 API env 中配置为正整数毫秒',
|
||||
'共享 BgFilter 单图估时预检必须给出明确错误。',
|
||||
);
|
||||
if (
|
||||
readOptionalCommandsLog(fixture).includes(
|
||||
'systemctl stop genarrative-bgfilter-worker.service',
|
||||
)
|
||||
) {
|
||||
failures.push('共享 BgFilter timeout 预检失败时不得停止当前 worker。');
|
||||
failures.push('共享 BgFilter 单图估时预检失败时不得停止当前 worker。');
|
||||
}
|
||||
assertMaintenanceCleared(fixture, '共享 BgFilter 单图估时预检失败');
|
||||
}
|
||||
|
||||
function assertDeployRemovesRetiredBgFilterEnvThroughSudo() {
|
||||
const fixture = prepareFixture('retired-bgfilter-env-sudo-removal');
|
||||
writeFileSync(
|
||||
fixture.apiEnvFile,
|
||||
`${readFileSync(fixture.apiEnvFile, 'utf8')}GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS=180000\n`,
|
||||
'utf8',
|
||||
);
|
||||
writeFileSync(
|
||||
fixture.bgfilterWorkerEnvFile,
|
||||
`${readFileSync(fixture.bgfilterWorkerEnvFile, 'utf8')}GENARRATIVE_BGFILTER_WORKER_CONCURRENCY=4\n`,
|
||||
'utf8',
|
||||
);
|
||||
|
||||
chmodSync(fixture.apiEnvFile, 0o000);
|
||||
chmodSync(fixture.bgfilterWorkerEnvFile, 0o000);
|
||||
let result;
|
||||
try {
|
||||
result = runDeploy(fixture, {
|
||||
sudoEnvFiles: [fixture.apiEnvFile, fixture.bgfilterWorkerEnvFile],
|
||||
});
|
||||
} finally {
|
||||
chmodSync(fixture.apiEnvFile, 0o600);
|
||||
chmodSync(fixture.bgfilterWorkerEnvFile, 0o600);
|
||||
}
|
||||
|
||||
assertStatus(result, 0, 'root-only BgFilter env 应通过 sudo 完成退役键清理。');
|
||||
if (result.status !== 0) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
readFileSync(fixture.apiEnvFile, 'utf8').includes(
|
||||
'GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS=',
|
||||
)
|
||||
) {
|
||||
failures.push('sudo 清理后共享 API env 不得保留旧固定 BgFilter timeout。');
|
||||
}
|
||||
if (
|
||||
readFileSync(fixture.bgfilterWorkerEnvFile, 'utf8').includes(
|
||||
'GENARRATIVE_BGFILTER_WORKER_CONCURRENCY=',
|
||||
)
|
||||
) {
|
||||
failures.push('sudo 清理后 BgFilter 专属 env 不得保留旧并发 N。');
|
||||
}
|
||||
assertMaintenanceCleared(fixture, '共享 BgFilter timeout 预检失败');
|
||||
}
|
||||
|
||||
function assertDeployRejectsEmptyBgFilterInternalToken() {
|
||||
@@ -915,17 +977,17 @@ function assertDeployRejectsBgFilterWorkerSharedEnvDrift() {
|
||||
const fixture = prepareFixture('bgfilter-worker-shared-env-drift');
|
||||
writeFileSync(
|
||||
fixture.bgfilterWorkerEnvFile,
|
||||
`${readFileSync(fixture.bgfilterWorkerEnvFile, 'utf8')}GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS=120000\n`,
|
||||
`${readFileSync(fixture.bgfilterWorkerEnvFile, 'utf8')}GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS=120000\n`,
|
||||
'utf8',
|
||||
);
|
||||
const result = runDeploy(fixture);
|
||||
|
||||
if (result.status === 0) {
|
||||
failures.push('BgFilter 专属 env 覆盖不同的共享 timeout 时部署必须失败。');
|
||||
failures.push('BgFilter 专属 env 覆盖不同的共享单图估时时部署必须失败。');
|
||||
}
|
||||
assertIncludes(
|
||||
result.stderr,
|
||||
'BgFilter 专属 env 中的共享配置与 API env 不一致: GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS',
|
||||
'BgFilter 专属 env 中的共享配置与 API env 不一致: GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS',
|
||||
'共享配置漂移预检必须给出具体变量名。',
|
||||
);
|
||||
if (
|
||||
@@ -968,9 +1030,9 @@ function assertDeployRejectsEmptyBgFilterWorkerSharedEnvOverride() {
|
||||
function assertDeployRejectsExternalGenerationWorkerBgFilterEnvDrift() {
|
||||
const cases = [
|
||||
[
|
||||
'request-timeout',
|
||||
'GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS=120000',
|
||||
'GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS',
|
||||
'single-image-estimate',
|
||||
'GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS=120000',
|
||||
'GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS',
|
||||
],
|
||||
[
|
||||
'base-url',
|
||||
@@ -1080,7 +1142,7 @@ function assertDeployRejectsInvalidBgFilterWorkerCapacity() {
|
||||
const fixture = prepareFixture('bgfilter-worker-invalid-capacity');
|
||||
writeFileSync(
|
||||
fixture.bgfilterWorkerEnvFile,
|
||||
`${readFileSync(fixture.bgfilterWorkerEnvFile, 'utf8')}GENARRATIVE_BGFILTER_WORKER_CONCURRENCY=8\nGENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS=4\n`,
|
||||
`${readFileSync(fixture.bgfilterWorkerEnvFile, 'utf8')}GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS=4\n`,
|
||||
'utf8',
|
||||
);
|
||||
const result = runDeploy(fixture);
|
||||
@@ -1104,31 +1166,89 @@ function assertDeployRejectsInvalidBgFilterWorkerCapacity() {
|
||||
}
|
||||
|
||||
function assertDeployRejectsMissingBgFilterWorkerCapacity() {
|
||||
for (const key of [
|
||||
'GENARRATIVE_BGFILTER_WORKER_CONCURRENCY',
|
||||
'GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS',
|
||||
]) {
|
||||
const fixture = prepareFixture(`bgfilter-worker-missing-${key.toLowerCase()}`);
|
||||
const current = readFileSync(fixture.bgfilterWorkerEnvFile, 'utf8');
|
||||
const next = current
|
||||
.split(/\r?\n/u)
|
||||
.filter((line) => !line.startsWith(`${key}=`))
|
||||
.join('\n');
|
||||
writeFileSync(fixture.bgfilterWorkerEnvFile, `${next}\n`, 'utf8');
|
||||
const result = runDeploy(fixture);
|
||||
// N 已迁入共享 API env 且不可缺失;Q 是可选保险丝,缺失不再导致失败。
|
||||
// deploy 脚本会用 ensure_env_value 自动补齐缺失的共享 N/est,因此这里注入
|
||||
// 非法值(而不是删除)来验证 fail-closed。
|
||||
const fixture = prepareFixture('bgfilter-worker-invalid-shared-concurrency');
|
||||
writeFileSync(
|
||||
fixture.apiEnvFile,
|
||||
`${readFileSync(fixture.apiEnvFile, 'utf8')}GENARRATIVE_BGFILTER_WORKER_CONCURRENCY=0\n`,
|
||||
'utf8',
|
||||
);
|
||||
const result = runDeploy(fixture);
|
||||
|
||||
if (result.status === 0) {
|
||||
failures.push(`BgFilter worker 缺少 ${key} 时部署必须失败。`);
|
||||
}
|
||||
assertIncludes(
|
||||
result.stderr,
|
||||
`${key} 必须是正整数`,
|
||||
`BgFilter worker 缺少 ${key} 时必须给出明确错误。`,
|
||||
);
|
||||
assertBgFilterPreflightFailedBeforeSwitch(
|
||||
fixture,
|
||||
`BgFilter worker 缺少 ${key} 的预检失败`,
|
||||
);
|
||||
if (result.status === 0) {
|
||||
failures.push('共享 GENARRATIVE_BGFILTER_WORKER_CONCURRENCY 非法时部署必须失败。');
|
||||
}
|
||||
assertIncludes(
|
||||
result.stderr,
|
||||
'GENARRATIVE_BGFILTER_WORKER_CONCURRENCY 必须在共享 API env 中配置为正整数',
|
||||
'共享 N 预检必须给出明确错误。',
|
||||
);
|
||||
assertBgFilterPreflightFailedBeforeSwitch(
|
||||
fixture,
|
||||
'共享 N 非法的预检失败',
|
||||
);
|
||||
|
||||
const missingQ = prepareFixture('bgfilter-worker-missing-optional-q');
|
||||
const current = readFileSync(missingQ.bgfilterWorkerEnvFile, 'utf8');
|
||||
const next = current
|
||||
.split(/\r?\n/u)
|
||||
.filter((line) => !line.startsWith('GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS='))
|
||||
.join('\n');
|
||||
writeFileSync(missingQ.bgfilterWorkerEnvFile, `${next}\n`, 'utf8');
|
||||
const missingQResult = runDeploy(missingQ);
|
||||
if (missingQResult.status !== 0) {
|
||||
failures.push('Q 缺失时应回退代码默认保险丝 2048,部署不得失败。');
|
||||
}
|
||||
}
|
||||
|
||||
function assertDeployMigratesOldDefaultBgFilterAdmissionLimit() {
|
||||
const fixture = prepareFixture('bgfilter-worker-migrate-old-default-q');
|
||||
const current = readFileSync(fixture.bgfilterWorkerEnvFile, 'utf8');
|
||||
const legacy = current.replace(
|
||||
'GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS=2048',
|
||||
'GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS=128',
|
||||
);
|
||||
if (legacy === current) {
|
||||
failures.push('BgFilter Q 迁移 fixture 缺少当前默认 2048。');
|
||||
return;
|
||||
}
|
||||
writeFileSync(fixture.bgfilterWorkerEnvFile, legacy, 'utf8');
|
||||
|
||||
const result = runDeploy(fixture);
|
||||
if (result.status !== 0) {
|
||||
failures.push(`历史默认 Q=128 迁移到 2048 时部署不应失败:${result.stderr}`);
|
||||
return;
|
||||
}
|
||||
const migrated = readFileSync(fixture.bgfilterWorkerEnvFile, 'utf8');
|
||||
if (!/^GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS=2048$/mu.test(migrated)) {
|
||||
failures.push('部署必须把历史模板默认 Q=128 定向迁移为 2048。');
|
||||
}
|
||||
if (/^GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS=128$/mu.test(migrated)) {
|
||||
failures.push('部署完成后不得继续保留历史模板默认 Q=128。');
|
||||
}
|
||||
|
||||
const custom = prepareFixture('bgfilter-worker-preserve-custom-q');
|
||||
const customCurrent = readFileSync(custom.bgfilterWorkerEnvFile, 'utf8');
|
||||
const customized = customCurrent.replace(
|
||||
'GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS=2048',
|
||||
'GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS=512',
|
||||
);
|
||||
if (customized === customCurrent) {
|
||||
failures.push('BgFilter 自定义 Q 保留 fixture 缺少当前默认 2048。');
|
||||
return;
|
||||
}
|
||||
writeFileSync(custom.bgfilterWorkerEnvFile, customized, 'utf8');
|
||||
|
||||
const customResult = runDeploy(custom);
|
||||
if (customResult.status !== 0) {
|
||||
failures.push(`自定义 Q=512 时部署不应失败:${customResult.stderr}`);
|
||||
return;
|
||||
}
|
||||
const preserved = readFileSync(custom.bgfilterWorkerEnvFile, 'utf8');
|
||||
if (!/^GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS=512$/mu.test(preserved)) {
|
||||
failures.push('部署只能迁移历史默认 Q=128,必须保留其它显式自定义值。');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1824,7 +1944,8 @@ function prepareFixture(name) {
|
||||
'GENARRATIVE_BGFILTER_WORKER_BASE_URL=http://127.0.0.1:18083',
|
||||
`GENARRATIVE_BGFILTER_INTERNAL_TOKEN_FILE=${bgfilterTokenFile}`,
|
||||
'GENARRATIVE_BGFILTER_WORKER_CONNECT_TIMEOUT_MS=2000',
|
||||
'GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS=180000',
|
||||
'GENARRATIVE_BGFILTER_WORKER_CONCURRENCY=16',
|
||||
'GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS=5000',
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf8',
|
||||
@@ -1839,8 +1960,7 @@ function prepareFixture(name) {
|
||||
[
|
||||
'GENARRATIVE_BGFILTER_WORKER_HOST=127.0.0.1',
|
||||
'GENARRATIVE_BGFILTER_WORKER_PORT=18083',
|
||||
'GENARRATIVE_BGFILTER_WORKER_CONCURRENCY=4',
|
||||
'GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS=128',
|
||||
'GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS=2048',
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf8',
|
||||
@@ -2155,6 +2275,22 @@ function prepareFixture(name) {
|
||||
' mkdir -p "${path}"',
|
||||
' exit 0',
|
||||
'fi',
|
||||
'if [[ -n "${FAKE_SUDO_ENV_FILES:-}" && "${1:-}" == "python3" ]]; then',
|
||||
' IFS="|" read -r -a env_files <<< "${FAKE_SUDO_ENV_FILES}"',
|
||||
' modes=()',
|
||||
' for env_file in "${env_files[@]}"; do',
|
||||
' modes+=("$(/usr/bin/stat -c %a -- "${env_file}")")',
|
||||
' /usr/bin/chmod u+rw -- "${env_file}"',
|
||||
' done',
|
||||
' set +e',
|
||||
' "$@"',
|
||||
' status=$?',
|
||||
' set -e',
|
||||
' for index in "${!env_files[@]}"; do',
|
||||
' /usr/bin/chmod "${modes[$index]}" -- "${env_files[$index]}"',
|
||||
' done',
|
||||
' exit "${status}"',
|
||||
'fi',
|
||||
'exec "$@"',
|
||||
'',
|
||||
].join('\n'),
|
||||
@@ -2293,6 +2429,7 @@ function runDeploy(fixture, options = {}) {
|
||||
FAKE_RELEASE_ROOT: fixture.releaseRoot,
|
||||
FAKE_RELEASE_VERSION: fixture.version,
|
||||
FAKE_WORKER_STATE_FILE: fixture.workerStateFile,
|
||||
FAKE_SUDO_ENV_FILES: (options.sudoEnvFiles ?? []).join('|'),
|
||||
GENARRATIVE_SYSTEMD_UNIT_DIR: fixture.systemdUnitDir,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -292,7 +292,7 @@ function assertApiReleaseContainsPingoraDirectDependencies() {
|
||||
assertIncludes(
|
||||
bgfilterUnit,
|
||||
'TimeoutStopSec=900',
|
||||
'API release 的 BgFilter unit 必须给最多 600s 的内部请求预算留足优雅排空时间。',
|
||||
'API release 的 BgFilter unit 必须给取得 permit 后的公式化 callBudget 留足优雅排空时间。',
|
||||
);
|
||||
assertFileExists(
|
||||
path.join(releaseDir, 'deploy/pingora/pingora-gateway.env.example'),
|
||||
|
||||
@@ -1651,7 +1651,7 @@ const checks = [
|
||||
file: 'deploy/systemd/genarrative-bgfilter-worker.service',
|
||||
includes: 'TimeoutStopSec=900',
|
||||
reason:
|
||||
'BgFilter worker 必须给最多 600s 的内部请求预算留足优雅排空时间,不能沿用 systemd 默认停止窗口。',
|
||||
'BgFilter worker 必须给取得 permit 后的公式化 callBudget 留足优雅排空时间,不能沿用 systemd 默认停止窗口。',
|
||||
},
|
||||
{
|
||||
file: 'scripts/jenkins-server-provision.sh',
|
||||
@@ -1685,6 +1685,12 @@ const checks = [
|
||||
reason:
|
||||
'Server-Provision 必须拒绝仅含空白字符的 BgFilter 内部 Token 文件。',
|
||||
},
|
||||
{
|
||||
file: 'scripts/jenkins-server-provision.sh',
|
||||
includes: 'current_value="$(read_effective_env_value "${file}" "${key}")"',
|
||||
reason:
|
||||
'Server-Provision 迁移历史默认值时必须读取最后一次有效赋值,不能覆盖后写的自定义运行态值。',
|
||||
},
|
||||
{
|
||||
file: 'scripts/jenkins-server-provision.sh',
|
||||
includes: "root:genarrative:440",
|
||||
|
||||
@@ -265,6 +265,55 @@ ensure_env_value() {
|
||||
write_env_value "${file_path}" "${key}" "${default_value}"
|
||||
}
|
||||
|
||||
remove_env_key_if_present() {
|
||||
local file_path="$1"
|
||||
local key="$2"
|
||||
local can_update_direct=0
|
||||
local removal_result
|
||||
local python_script='
|
||||
import sys
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
file_path = Path(sys.argv[1])
|
||||
key = sys.argv[2]
|
||||
assignment_pattern = re.compile(rf"^(?:export\s+)?{re.escape(key)}\s*=")
|
||||
lines = file_path.read_text(encoding="utf-8").splitlines()
|
||||
next_lines = []
|
||||
removed = False
|
||||
for raw_line in lines:
|
||||
if assignment_pattern.match(raw_line.strip()):
|
||||
removed = True
|
||||
continue
|
||||
next_lines.append(raw_line)
|
||||
|
||||
if removed:
|
||||
content = "\n".join(next_lines)
|
||||
file_path.write_text(f"{content}\n" if content else "", encoding="utf-8")
|
||||
print("removed")
|
||||
'
|
||||
|
||||
if [[ ! -f "${file_path}" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -r "${file_path}" && -w "${file_path}" ]]; then
|
||||
can_update_direct=1
|
||||
fi
|
||||
if [[ "$(id -u)" -eq 0 || "${can_update_direct}" -eq 1 ]]; then
|
||||
removal_result="$(python3 -c "${python_script}" "${file_path}" "${key}")"
|
||||
else
|
||||
if ! sudo -n true >/dev/null 2>&1; then
|
||||
echo "[production-api-deploy] 当前用户无权更新 ${file_path},且 sudo -n 不可用;无法移除已退役环境变量 ${key}。" >&2
|
||||
exit 1
|
||||
fi
|
||||
removal_result="$(sudo -n python3 -c "${python_script}" "${file_path}" "${key}")"
|
||||
fi
|
||||
if [[ "${removal_result}" == "removed" ]]; then
|
||||
echo "[production-api-deploy] 移除已退役的环境变量: ${key} <- ${file_path}"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_env_value_migrates_old_default() {
|
||||
local file_path="$1"
|
||||
local key="$2"
|
||||
@@ -382,7 +431,11 @@ ensure_runtime_env_and_dirs() {
|
||||
ensure_env_value "${api_env_file}" "GENARRATIVE_BGFILTER_INTERNAL_TOKEN_FILE" "/etc/genarrative/secrets/bgfilter-worker.token"
|
||||
ensure_env_value "${api_env_file}" "GENARRATIVE_BGFILTER_WORKER_CONNECT_TIMEOUT_MS" "2000"
|
||||
ensure_runtime_bootstrap_secret_file_env "${api_env_file}"
|
||||
ensure_env_value_migrates_old_default "${api_env_file}" "GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS" "45000" "180000"
|
||||
# N 与单图估时是父子共同派生 attempt/callBudget 公式的输入,必须放共享 API env 单一来源;
|
||||
# 旧固定 attempt timeout 已由公式取代。
|
||||
ensure_env_value "${api_env_file}" "GENARRATIVE_BGFILTER_WORKER_CONCURRENCY" "16"
|
||||
ensure_env_value "${api_env_file}" "GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS" "5000"
|
||||
remove_env_key_if_present "${api_env_file}" "GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS"
|
||||
|
||||
tracking_enabled="$(read_env_value "${api_env_file}" "GENARRATIVE_TRACKING_OUTBOX_ENABLED")"
|
||||
tracking_outbox_dir="$(read_env_value "${api_env_file}" "GENARRATIVE_TRACKING_OUTBOX_DIR")"
|
||||
@@ -440,17 +493,28 @@ ensure_bgfilter_worker_runtime_env_defaults() {
|
||||
|
||||
ensure_env_value "${bgfilter_env_file}" "GENARRATIVE_BGFILTER_WORKER_HOST" "127.0.0.1"
|
||||
ensure_env_value "${bgfilter_env_file}" "GENARRATIVE_BGFILTER_WORKER_PORT" "8083"
|
||||
# Q 仅是 admission 保险丝;把历史模板默认 128 定向迁到新默认 2048,
|
||||
# 其它显式定制值继续保留。
|
||||
ensure_env_value_migrates_old_default "${bgfilter_env_file}" "GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS" "128" "2048"
|
||||
ensure_env_value "${bgfilter_env_file}" "GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD" "3"
|
||||
ensure_env_value "${bgfilter_env_file}" "GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS" "300"
|
||||
# N 已迁入共享 API env;worker 专属文件中的旧值会与共享值形成双写风险,直接移除。
|
||||
remove_env_key_if_present "${bgfilter_env_file}" "GENARRATIVE_BGFILTER_WORKER_CONCURRENCY"
|
||||
}
|
||||
|
||||
validate_bgfilter_shared_runtime_env() {
|
||||
local api_env_file="$1"
|
||||
local request_timeout_ms connect_timeout_ms
|
||||
local shared_concurrency shared_estimate_ms connect_timeout_ms
|
||||
|
||||
request_timeout_ms="$(read_env_value "${api_env_file}" "GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS")"
|
||||
if [[ ! "${request_timeout_ms}" =~ ^[1-9][0-9]*$ ]]; then
|
||||
echo "[production-api-deploy] GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS 必须在共享 API env 中配置为正整数毫秒: ${api_env_file}" >&2
|
||||
shared_concurrency="$(read_env_value "${api_env_file}" "GENARRATIVE_BGFILTER_WORKER_CONCURRENCY")"
|
||||
if [[ ! "${shared_concurrency}" =~ ^[1-9][0-9]*$ ]]; then
|
||||
echo "[production-api-deploy] GENARRATIVE_BGFILTER_WORKER_CONCURRENCY 必须在共享 API env 中配置为正整数: ${api_env_file}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
shared_estimate_ms="$(read_env_value "${api_env_file}" "GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS")"
|
||||
if [[ ! "${shared_estimate_ms}" =~ ^[1-9][0-9]*$ ]]; then
|
||||
echo "[production-api-deploy] GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS 必须在共享 API env 中配置为正整数毫秒: ${api_env_file}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -462,15 +526,21 @@ validate_bgfilter_shared_runtime_env() {
|
||||
}
|
||||
|
||||
validate_bgfilter_worker_capacity() {
|
||||
local bgfilter_env_file="$1"
|
||||
local api_env_file="$1"
|
||||
local bgfilter_env_file="$2"
|
||||
local concurrency max_requests
|
||||
|
||||
concurrency="$(read_env_value "${bgfilter_env_file}" "GENARRATIVE_BGFILTER_WORKER_CONCURRENCY")"
|
||||
# N 已迁入共享 API env(worker unit 先加载它);Q 是可选保险丝(代码默认 2048),
|
||||
# 显式配置时必须为正且不小于 N。
|
||||
concurrency="$(read_env_value "${api_env_file}" "GENARRATIVE_BGFILTER_WORKER_CONCURRENCY")"
|
||||
max_requests="$(read_env_value "${bgfilter_env_file}" "GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS")"
|
||||
if [[ ! "${concurrency}" =~ ^[1-9][0-9]{0,8}$ ]]; then
|
||||
echo "[production-api-deploy] GENARRATIVE_BGFILTER_WORKER_CONCURRENCY 必须是正整数: ${bgfilter_env_file}" >&2
|
||||
echo "[production-api-deploy] GENARRATIVE_BGFILTER_WORKER_CONCURRENCY 必须在共享 API env 中配置为正整数: ${api_env_file}" >&2
|
||||
return 1
|
||||
fi
|
||||
if [[ -z "${max_requests}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ ! "${max_requests}" =~ ^[1-9][0-9]{0,8}$ ]]; then
|
||||
echo "[production-api-deploy] GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS 必须是正整数: ${bgfilter_env_file}" >&2
|
||||
return 1
|
||||
@@ -487,7 +557,8 @@ validate_bgfilter_worker_shared_env_alignment() {
|
||||
local key shared_value dedicated_value
|
||||
|
||||
for key in \
|
||||
GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS \
|
||||
GENARRATIVE_BGFILTER_WORKER_CONCURRENCY \
|
||||
GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS \
|
||||
GENARRATIVE_EDITOR_BGFILTER_BASE_URL \
|
||||
GENARRATIVE_EDITOR_BGFILTER_TOKEN \
|
||||
GENARRATIVE_BGFILTER_INTERNAL_TOKEN_FILE \
|
||||
@@ -518,7 +589,8 @@ validate_external_generation_worker_bgfilter_env_alignment() {
|
||||
fi
|
||||
|
||||
for key in \
|
||||
GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS \
|
||||
GENARRATIVE_BGFILTER_WORKER_CONCURRENCY \
|
||||
GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS \
|
||||
GENARRATIVE_BGFILTER_WORKER_BASE_URL \
|
||||
GENARRATIVE_BGFILTER_INTERNAL_TOKEN_FILE \
|
||||
GENARRATIVE_BGFILTER_WORKER_CONNECT_TIMEOUT_MS \
|
||||
@@ -1458,7 +1530,7 @@ if [[ -n "${BGFILTER_WORKER_SERVICE}" ]]; then
|
||||
validate_bgfilter_loopback_endpoint_alignment "${API_ENV_FILE}" "${BGFILTER_WORKER_ENV_FILE}" "${BGFILTER_WORKER_HEALTH_URL}"
|
||||
fi
|
||||
if [[ -n "${BGFILTER_WORKER_ENV_FILE}" ]]; then
|
||||
validate_bgfilter_worker_capacity "${BGFILTER_WORKER_ENV_FILE}"
|
||||
validate_bgfilter_worker_capacity "${API_ENV_FILE}" "${BGFILTER_WORKER_ENV_FILE}"
|
||||
validate_bgfilter_worker_shared_env_alignment "${API_ENV_FILE}" "${BGFILTER_WORKER_ENV_FILE}"
|
||||
fi
|
||||
migrate_legacy_editor_generation_pricing_override "${CURRENT_LINK}"
|
||||
|
||||
+6
-2
@@ -3222,11 +3222,15 @@ function buildBgfilterWorkerProcessEnv({
|
||||
GENARRATIVE_BGFILTER_WORKER_CONCURRENCY:
|
||||
String(
|
||||
baseEnv.GENARRATIVE_BGFILTER_WORKER_CONCURRENCY ?? '',
|
||||
).trim() || '4',
|
||||
).trim() || '16',
|
||||
GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS:
|
||||
String(
|
||||
baseEnv.GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS ?? '',
|
||||
).trim() || '5000',
|
||||
GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS:
|
||||
String(
|
||||
baseEnv.GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS ?? '',
|
||||
).trim() || '128',
|
||||
).trim() || '2048',
|
||||
GENARRATIVE_API_LOG: options.apiLog,
|
||||
GENARRATIVE_SPACETIME_SERVER_URL: state.spacetimeServer,
|
||||
GENARRATIVE_SPACETIME_DATABASE: options.database,
|
||||
|
||||
+3
-2
@@ -342,8 +342,9 @@ describe('dev scheduler api-server env', () => {
|
||||
expect(workerEnv.GENARRATIVE_BGFILTER_INTERNAL_TOKEN).toBe(internalToken);
|
||||
expect(workerEnv.GENARRATIVE_BGFILTER_WORKER_HOST).toBe('127.0.0.1');
|
||||
expect(workerEnv.GENARRATIVE_BGFILTER_WORKER_PORT).toBe('18083');
|
||||
expect(workerEnv.GENARRATIVE_BGFILTER_WORKER_CONCURRENCY).toBe('4');
|
||||
expect(workerEnv.GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS).toBe('128');
|
||||
expect(workerEnv.GENARRATIVE_BGFILTER_WORKER_CONCURRENCY).toBe('16');
|
||||
expect(workerEnv.GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS).toBe('5000');
|
||||
expect(workerEnv.GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS).toBe('2048');
|
||||
});
|
||||
|
||||
test('Windows 本地 dev 自动注入已安装的 FFmpeg 路径', () => {
|
||||
|
||||
@@ -549,6 +549,27 @@ ensure_env_value() {
|
||||
fi
|
||||
}
|
||||
|
||||
remove_env_key_if_present() {
|
||||
local file="$1"
|
||||
local key="$2"
|
||||
|
||||
if [[ ! -f "${file}" ]]; then
|
||||
return
|
||||
fi
|
||||
if ! grep -Eq "^[[:space:]]*(export[[:space:]]+)?${key}=" "${file}"; then
|
||||
return
|
||||
fi
|
||||
|
||||
echo "[server-provision] 移除已退役的环境变量: ${key} <- ${file}"
|
||||
if [[ "${DRY_RUN}" != "true" ]]; then
|
||||
local tmp_file
|
||||
tmp_file="$(mktemp)"
|
||||
grep -Ev "^[[:space:]]*(export[[:space:]]+)?${key}=" "${file}" >"${tmp_file}"
|
||||
cat "${tmp_file}" >"${file}"
|
||||
rm -f "${tmp_file}"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_env_value_migrates_old_default() {
|
||||
local file="$1"
|
||||
local key="$2"
|
||||
@@ -556,7 +577,9 @@ ensure_env_value_migrates_old_default() {
|
||||
local new_default="$4"
|
||||
local current_value
|
||||
|
||||
current_value="$(read_env_value "${file}" "${key}")"
|
||||
# 迁移判断必须读取最后一次有效赋值;生产 env 若暂时存在重复键,后写值才是
|
||||
# systemd EnvironmentFile 的实际语义,不能因前面的历史默认覆盖后面的自定义值。
|
||||
current_value="$(read_effective_env_value "${file}" "${key}")"
|
||||
if [[ -z "${current_value}" ]]; then
|
||||
ensure_env_value "${file}" "${key}" "${new_default}"
|
||||
return
|
||||
@@ -625,20 +648,29 @@ ensure_api_runtime_env_defaults() {
|
||||
ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_BGFILTER_INTERNAL_TOKEN_FILE" "/etc/genarrative/secrets/bgfilter-worker.token"
|
||||
ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_BGFILTER_WORKER_CONNECT_TIMEOUT_MS" "2000"
|
||||
ensure_runtime_bootstrap_secret_file_env "${API_ENV_FILE}"
|
||||
ensure_env_value_migrates_old_default "${API_ENV_FILE}" "GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS" "45000" "180000"
|
||||
# N 与单图估时是父子共同派生 attempt/callBudget 公式的输入,必须放共享 API env 单一来源。
|
||||
ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_BGFILTER_WORKER_CONCURRENCY" "16"
|
||||
ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS" "5000"
|
||||
remove_env_key_if_present "${API_ENV_FILE}" "GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS"
|
||||
}
|
||||
|
||||
validate_bgfilter_shared_runtime_env() {
|
||||
local request_timeout_ms connect_timeout_ms
|
||||
local shared_concurrency shared_estimate_ms connect_timeout_ms
|
||||
|
||||
if [[ "${DRY_RUN}" == "true" ]]; then
|
||||
echo "+ validate shared BgFilter provider attempt timeout in ${API_ENV_FILE}"
|
||||
echo "+ validate shared BgFilter concurrency/estimate inputs in ${API_ENV_FILE}"
|
||||
return
|
||||
fi
|
||||
|
||||
request_timeout_ms="$(read_effective_env_value "${API_ENV_FILE}" "GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS")"
|
||||
if [[ ! "${request_timeout_ms}" =~ ^[1-9][0-9]*$ ]]; then
|
||||
echo "[server-provision] GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS 必须在共享 API env 中配置为正整数毫秒: ${API_ENV_FILE}" >&2
|
||||
shared_concurrency="$(read_effective_env_value "${API_ENV_FILE}" "GENARRATIVE_BGFILTER_WORKER_CONCURRENCY")"
|
||||
if [[ ! "${shared_concurrency}" =~ ^[1-9][0-9]*$ ]]; then
|
||||
echo "[server-provision] GENARRATIVE_BGFILTER_WORKER_CONCURRENCY 必须在共享 API env 中配置为正整数: ${API_ENV_FILE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
shared_estimate_ms="$(read_effective_env_value "${API_ENV_FILE}" "GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS")"
|
||||
if [[ ! "${shared_estimate_ms}" =~ ^[1-9][0-9]*$ ]]; then
|
||||
echo "[server-provision] GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS 必须在共享 API env 中配置为正整数毫秒: ${API_ENV_FILE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -671,7 +703,8 @@ validate_bgfilter_env_file_alignment() {
|
||||
local include_provider_credentials="$3"
|
||||
local key shared_value dedicated_value
|
||||
local -a shared_keys=(
|
||||
GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS
|
||||
GENARRATIVE_BGFILTER_WORKER_CONCURRENCY
|
||||
GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS
|
||||
GENARRATIVE_BGFILTER_WORKER_BASE_URL
|
||||
GENARRATIVE_BGFILTER_INTERNAL_TOKEN_FILE
|
||||
GENARRATIVE_BGFILTER_WORKER_CONNECT_TIMEOUT_MS
|
||||
@@ -765,8 +798,10 @@ ensure_bgfilter_worker_runtime_env_defaults() {
|
||||
|
||||
ensure_env_value "${BGFILTER_WORKER_ENV_FILE}" "GENARRATIVE_BGFILTER_WORKER_HOST" "127.0.0.1"
|
||||
ensure_env_value "${BGFILTER_WORKER_ENV_FILE}" "GENARRATIVE_BGFILTER_WORKER_PORT" "8083"
|
||||
ensure_env_value "${BGFILTER_WORKER_ENV_FILE}" "GENARRATIVE_BGFILTER_WORKER_CONCURRENCY" "4"
|
||||
ensure_env_value "${BGFILTER_WORKER_ENV_FILE}" "GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS" "128"
|
||||
remove_env_key_if_present "${BGFILTER_WORKER_ENV_FILE}" "GENARRATIVE_BGFILTER_WORKER_CONCURRENCY"
|
||||
# Q 已降级为可选 admission 保险丝(代码默认 2048);只迁移历史模板默认 128,
|
||||
# 其它显式定制值继续保留。
|
||||
ensure_env_value_migrates_old_default "${BGFILTER_WORKER_ENV_FILE}" "GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS" "128" "2048"
|
||||
ensure_env_value "${BGFILTER_WORKER_ENV_FILE}" "GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD" "3"
|
||||
ensure_env_value "${BGFILTER_WORKER_ENV_FILE}" "GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS" "300"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -74,7 +74,7 @@ use crate::{
|
||||
apply_editor_screen_background_decision_to_generation_inputs,
|
||||
build_editor_canvas_generated_layer_item, complete_editor_canvas_generation_with_items,
|
||||
persist_editor_generated_media_asset,
|
||||
remove_editor_generated_screen_background_with_bgfilter_with_request_timeout,
|
||||
remove_editor_generated_screen_background_with_bgfilter,
|
||||
resolve_editor_reference_object_key_for_owner,
|
||||
},
|
||||
editor_screen_background_decision::{
|
||||
@@ -2326,7 +2326,6 @@ async fn extract_and_persist_editor_character_animation_frames(
|
||||
use futures_util::StreamExt as _;
|
||||
|
||||
let frame_count = finalized_frames.len();
|
||||
let bgfilter_request_timeout_ms = state.config.editor_bgfilter_request_timeout_ms;
|
||||
let frame_results = futures_util::stream::iter(finalized_frames.into_iter().enumerate().map(
|
||||
|(frame_index, frame)| async move {
|
||||
process_and_persist_editor_character_animation_frame(
|
||||
@@ -2339,7 +2338,6 @@ async fn extract_and_persist_editor_character_animation_frames(
|
||||
request.frame_width,
|
||||
request.frame_height,
|
||||
request.screen_color,
|
||||
bgfilter_request_timeout_ms,
|
||||
audit,
|
||||
)
|
||||
.await
|
||||
@@ -2403,7 +2401,6 @@ async fn process_and_persist_editor_character_animation_frame(
|
||||
frame_width: u32,
|
||||
frame_height: u32,
|
||||
screen_color: EditorScreenBackgroundColor,
|
||||
bgfilter_request_timeout_ms: u64,
|
||||
audit: &crate::external_api_audit::ExternalApiAuditContext,
|
||||
) -> Result<ProcessedEditorCharacterAnimationFrame, AppError> {
|
||||
// 中文注释:每一帧只要求自己的绿幕源图先落 OSS,不再等待整批源图全部上传完成。
|
||||
@@ -2435,13 +2432,14 @@ async fn process_and_persist_editor_character_animation_frame(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let removed = remove_editor_generated_screen_background_with_bgfilter_with_request_timeout(
|
||||
// 排队与调用预算由共享 helper 按 N/est 公式和父剩余预算派生,动画帧不再携带
|
||||
// 任何按帧数放大的 timeout。
|
||||
let removed = remove_editor_generated_screen_background_with_bgfilter(
|
||||
state,
|
||||
source_put.object_key.as_str(),
|
||||
screen_color,
|
||||
EDITOR_BGFILTER_DEFAULT_SEG_MODEL,
|
||||
EDITOR_BGFILTER_CROSS_CHECK_ENABLED,
|
||||
bgfilter_request_timeout_ms,
|
||||
audit,
|
||||
)
|
||||
.await?;
|
||||
@@ -6417,17 +6415,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_character_animation_bgfilter_timeout_uses_configured_single_request_limit() {
|
||||
fn editor_character_animation_bgfilter_frames_share_budget_helper_without_per_frame_timeout() {
|
||||
let source = include_str!("character_animation_assets.rs");
|
||||
// 动画帧不携带任何自定 timeout:预算全部由共享 helper 按 N/est 公式派生。
|
||||
assert_function_contains(
|
||||
source,
|
||||
"async fn extract_and_persist_editor_character_animation_frames",
|
||||
"async fn process_and_persist_editor_character_animation_frame",
|
||||
&[
|
||||
"let bgfilter_request_timeout_ms = state.config.editor_bgfilter_request_timeout_ms;",
|
||||
"process_and_persist_editor_character_animation_frame",
|
||||
"bgfilter_request_timeout_ms",
|
||||
],
|
||||
&["process_and_persist_editor_character_animation_frame"],
|
||||
);
|
||||
assert!(!source.contains(concat!(
|
||||
"EDITOR_CHARACTER_ANIMATION_BGFILTER_TIMEOUT_",
|
||||
@@ -6437,6 +6432,7 @@ mod tests {
|
||||
"editor_character_animation_bgfilter_",
|
||||
"request_timeout_ms"
|
||||
)));
|
||||
assert!(!source.contains(concat!("bgfilter_request_", "timeout_ms =")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -6552,9 +6548,7 @@ mod tests {
|
||||
&[
|
||||
"apply_chroma_key: false",
|
||||
"prepare_for_bgfilter_input: true",
|
||||
"state.config.editor_bgfilter_request_timeout_ms",
|
||||
"process_and_persist_editor_character_animation_frame",
|
||||
"bgfilter_request_timeout_ms",
|
||||
".buffer_unordered(frame_count.max(1))",
|
||||
".collect::<Vec<_>>()",
|
||||
"frame_payloads.sort_by_key",
|
||||
@@ -6584,9 +6578,8 @@ mod tests {
|
||||
&[
|
||||
"put_character_animation_frame_object",
|
||||
"green-screen-frame",
|
||||
"remove_editor_generated_screen_background_with_bgfilter_with_request_timeout",
|
||||
"remove_editor_generated_screen_background_with_bgfilter",
|
||||
"source_put.object_key.as_str()",
|
||||
"bgfilter_request_timeout_ms",
|
||||
"finalize_animation_frame_payload",
|
||||
"put_character_animation_frame_object",
|
||||
"animation_frame",
|
||||
@@ -6600,10 +6593,9 @@ mod tests {
|
||||
"EDITOR_CHARACTER_ANIMATION_ASSET_KIND",
|
||||
"EDITOR_CHARACTER_ANIMATION_PROVIDER_SOURCE_SLOT",
|
||||
"green-screen-frame",
|
||||
"remove_editor_generated_screen_background_with_bgfilter_with_request_timeout",
|
||||
"remove_editor_generated_screen_background_with_bgfilter",
|
||||
"EDITOR_BGFILTER_DEFAULT_SEG_MODEL",
|
||||
"EDITOR_BGFILTER_CROSS_CHECK_ENABLED",
|
||||
"bgfilter_request_timeout_ms",
|
||||
"finalize_animation_frame_payload",
|
||||
],
|
||||
);
|
||||
|
||||
@@ -18,7 +18,9 @@ const DEFAULT_EXTERNAL_GENERATION_WORKER_JOB_TIMEOUT_SECONDS: u64 = 900;
|
||||
const DEFAULT_EXTERNAL_GENERATION_WORKER_LONG_JOB_TIMEOUT_SECONDS: u64 = 1_800;
|
||||
pub(crate) const DEFAULT_VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS: u64 = 1_000_000;
|
||||
const DEFAULT_EDITOR_BGFILTER_BASE_URL: &str = "http://58.87.105.82/bgfilter";
|
||||
const DEFAULT_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS: u64 = 180_000;
|
||||
const DEFAULT_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS: u64 = 5_000;
|
||||
const BGFILTER_ATTEMPT_SAFETY_FACTOR: u64 = 2;
|
||||
const BGFILTER_WORKER_RESPONSE_WINDOW_MS: u64 = 1_000;
|
||||
const DEFAULT_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD: u32 = 3;
|
||||
const DEFAULT_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS: u64 = 300;
|
||||
const DEFAULT_ALIYUN_MATTING_ENDPOINT: &str = "imageseg.cn-shanghai.aliyuncs.com";
|
||||
@@ -69,7 +71,7 @@ pub struct AppConfig {
|
||||
pub editor_generation_pricing_override_path: PathBuf,
|
||||
pub editor_bgfilter_base_url: String,
|
||||
pub editor_bgfilter_token: Option<String>,
|
||||
pub editor_bgfilter_request_timeout_ms: u64,
|
||||
pub editor_bgfilter_single_image_estimate_ms: u64,
|
||||
pub aliyun_matting_enabled: bool,
|
||||
pub aliyun_matting_endpoint: String,
|
||||
pub aliyun_matting_access_key_id: Option<String>,
|
||||
@@ -278,8 +280,8 @@ impl Default for AppConfig {
|
||||
bgfilter_worker_port: 8083,
|
||||
bgfilter_worker_base_url: "http://127.0.0.1:8083".to_string(),
|
||||
bgfilter_internal_token: None,
|
||||
bgfilter_worker_concurrency: 4,
|
||||
bgfilter_worker_max_requests: 128,
|
||||
bgfilter_worker_concurrency: 16,
|
||||
bgfilter_worker_max_requests: 2048,
|
||||
bgfilter_worker_connect_timeout_ms: 2_000,
|
||||
external_generation_mode: ExternalGenerationMode::Queue,
|
||||
external_generation_worker_id: default_external_generation_worker_id(),
|
||||
@@ -319,7 +321,8 @@ impl Default for AppConfig {
|
||||
crate::editor_generation_config::default_editor_generation_pricing_override_path(),
|
||||
editor_bgfilter_base_url: DEFAULT_EDITOR_BGFILTER_BASE_URL.to_string(),
|
||||
editor_bgfilter_token: None,
|
||||
editor_bgfilter_request_timeout_ms: DEFAULT_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS,
|
||||
editor_bgfilter_single_image_estimate_ms:
|
||||
DEFAULT_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS,
|
||||
aliyun_matting_enabled: true,
|
||||
aliyun_matting_endpoint: DEFAULT_ALIYUN_MATTING_ENDPOINT.to_string(),
|
||||
aliyun_matting_access_key_id: None,
|
||||
@@ -474,6 +477,22 @@ impl Default for AppConfig {
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
/// 单次 provider attempt 上限:`N × est × 2`。运行时派生,禁止硬编码计算结果;
|
||||
/// worker 角色在 main 中覆盖 `bgfilter_worker_concurrency` 后自动生效。
|
||||
pub fn bgfilter_provider_attempt_timeout_ms(&self) -> u64 {
|
||||
(self.bgfilter_worker_concurrency.max(1) as u64)
|
||||
.saturating_mul(self.editor_bgfilter_single_image_estimate_ms.max(1))
|
||||
.saturating_mul(BGFILTER_ATTEMPT_SAFETY_FACTOR)
|
||||
}
|
||||
|
||||
/// 调用预算:自取得 provider permit 起算,覆盖签名、两次顺序 attempt、
|
||||
/// 结果校验与响应构造;排队时长不消耗它。
|
||||
pub fn bgfilter_call_budget_ms(&self) -> u64 {
|
||||
self.bgfilter_provider_attempt_timeout_ms()
|
||||
.saturating_mul(2)
|
||||
.saturating_add(BGFILTER_WORKER_RESPONSE_WINDOW_MS)
|
||||
}
|
||||
|
||||
pub fn from_env() -> Self {
|
||||
let mut config = Self::default();
|
||||
|
||||
@@ -541,10 +560,10 @@ impl AppConfig {
|
||||
"GENARRATIVE_EDITOR_BGFILTER_TOKEN",
|
||||
"GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN",
|
||||
]);
|
||||
if let Some(timeout_ms) =
|
||||
read_first_positive_u64_env(&["GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS"])
|
||||
if let Some(estimate_ms) =
|
||||
read_first_positive_u64_env(&["GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS"])
|
||||
{
|
||||
config.editor_bgfilter_request_timeout_ms = timeout_ms;
|
||||
config.editor_bgfilter_single_image_estimate_ms = estimate_ms;
|
||||
}
|
||||
if let Some(enabled) = read_first_bool_env(&["GENARRATIVE_ALIYUN_MATTING_ENABLED"]) {
|
||||
config.aliyun_matting_enabled = enabled;
|
||||
@@ -1577,7 +1596,7 @@ mod tests {
|
||||
AppConfig, DEFAULT_EDITOR_BGFILTER_BASE_URL,
|
||||
DEFAULT_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS,
|
||||
DEFAULT_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD,
|
||||
DEFAULT_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS,
|
||||
DEFAULT_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS,
|
||||
DEFAULT_EXTERNAL_GENERATION_WORKER_JOB_TIMEOUT_SECONDS,
|
||||
DEFAULT_EXTERNAL_GENERATION_WORKER_LEASE_SECONDS,
|
||||
DEFAULT_EXTERNAL_GENERATION_WORKER_LONG_JOB_TIMEOUT_SECONDS, ExternalGenerationMode,
|
||||
@@ -1598,8 +1617,8 @@ mod tests {
|
||||
assert_eq!(config.bgfilter_worker_port, 8083);
|
||||
assert_eq!(config.bgfilter_worker_base_url, "http://127.0.0.1:8083");
|
||||
assert!(config.bgfilter_internal_token.is_none());
|
||||
assert_eq!(config.bgfilter_worker_concurrency, 4);
|
||||
assert_eq!(config.bgfilter_worker_max_requests, 128);
|
||||
assert_eq!(config.bgfilter_worker_concurrency, 16);
|
||||
assert_eq!(config.bgfilter_worker_max_requests, 2048);
|
||||
assert_eq!(config.bgfilter_worker_connect_timeout_ms, 2_000);
|
||||
assert!(config.llm_model.is_empty());
|
||||
assert!(config.llm_base_url.is_empty());
|
||||
@@ -1610,9 +1629,11 @@ mod tests {
|
||||
DEFAULT_EDITOR_BGFILTER_BASE_URL
|
||||
);
|
||||
assert_eq!(
|
||||
config.editor_bgfilter_request_timeout_ms,
|
||||
DEFAULT_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS
|
||||
config.editor_bgfilter_single_image_estimate_ms,
|
||||
DEFAULT_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS
|
||||
);
|
||||
assert_eq!(config.bgfilter_provider_attempt_timeout_ms(), 160_000);
|
||||
assert_eq!(config.bgfilter_call_budget_ms(), 321_000);
|
||||
assert_eq!(
|
||||
config.editor_bgfilter_circuit_failure_threshold,
|
||||
DEFAULT_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD
|
||||
@@ -2355,7 +2376,7 @@ mod tests {
|
||||
unsafe {
|
||||
std::env::remove_var("GENARRATIVE_EDITOR_BGFILTER_BASE_URL");
|
||||
std::env::remove_var("GENARRATIVE_EDITOR_BGFILTER_TOKEN");
|
||||
std::env::remove_var("GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS");
|
||||
std::env::remove_var("GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS");
|
||||
std::env::remove_var("GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN");
|
||||
std::env::set_var(
|
||||
"GENARRATIVE_EDITOR_BGFILTER_BASE_URL",
|
||||
@@ -2365,7 +2386,10 @@ mod tests {
|
||||
"GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN",
|
||||
"shared-token",
|
||||
);
|
||||
std::env::set_var("GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS", "240000");
|
||||
std::env::set_var(
|
||||
"GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS",
|
||||
"8000",
|
||||
);
|
||||
}
|
||||
|
||||
let config = AppConfig::from_env();
|
||||
@@ -2374,7 +2398,16 @@ mod tests {
|
||||
config.editor_bgfilter_token.as_deref(),
|
||||
Some("shared-token")
|
||||
);
|
||||
assert_eq!(config.editor_bgfilter_request_timeout_ms, 240_000);
|
||||
assert_eq!(config.editor_bgfilter_single_image_estimate_ms, 8_000);
|
||||
// attempt / callBudget 必须随 N 与 est 联动派生,而不是固定值。
|
||||
assert_eq!(
|
||||
config.bgfilter_provider_attempt_timeout_ms(),
|
||||
config.bgfilter_worker_concurrency as u64 * 8_000 * 2
|
||||
);
|
||||
assert_eq!(
|
||||
config.bgfilter_call_budget_ms(),
|
||||
config.bgfilter_provider_attempt_timeout_ms() * 2 + 1_000
|
||||
);
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("GENARRATIVE_EDITOR_BGFILTER_TOKEN", "bgfilter-token");
|
||||
@@ -2389,7 +2422,7 @@ mod tests {
|
||||
unsafe {
|
||||
std::env::remove_var("GENARRATIVE_EDITOR_BGFILTER_BASE_URL");
|
||||
std::env::remove_var("GENARRATIVE_EDITOR_BGFILTER_TOKEN");
|
||||
std::env::remove_var("GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS");
|
||||
std::env::remove_var("GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS");
|
||||
std::env::remove_var("GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,8 +132,6 @@ const EDITOR_BGFILTER_SEG_MODEL_ANIME_SEG: &str = "anime-seg";
|
||||
const EDITOR_MATTING_SOURCE_URL_EXPIRE_SECONDS: u64 = 600;
|
||||
pub(crate) const EDITOR_BGFILTER_CROSS_CHECK_ENABLED: bool = true;
|
||||
pub(crate) const EDITOR_BGFILTER_CROSS_CHECK_DISABLED: bool = false;
|
||||
const EDITOR_BGFILTER_LOGICAL_ATTEMPT_COUNT: u64 = 2;
|
||||
const EDITOR_BGFILTER_WORKER_RESPONSE_WINDOW_MS: u64 = 1_000;
|
||||
const EDITOR_BGFILTER_PARENT_TRANSPORT_WINDOW: Duration = Duration::from_secs(2);
|
||||
const EDITOR_BGFILTER_FLAT_LOCAL_RESERVE: Duration = Duration::from_secs(7);
|
||||
const EDITOR_PUBLICATION_MATERIAL_ASSET_KIND: &str = "editor_publication_material";
|
||||
@@ -3377,13 +3375,6 @@ fn editor_background_removal_source_unsupported(message: &str) -> AppError {
|
||||
}))
|
||||
}
|
||||
|
||||
fn editor_bgfilter_worker_request_budget_limit_ms(single_attempt_timeout_ms: u64) -> u64 {
|
||||
single_attempt_timeout_ms
|
||||
.max(1)
|
||||
.saturating_mul(EDITOR_BGFILTER_LOGICAL_ATTEMPT_COUNT)
|
||||
.saturating_add(EDITOR_BGFILTER_WORKER_RESPONSE_WINDOW_MS)
|
||||
}
|
||||
|
||||
fn editor_bgfilter_flat_deadline_reserve(aliyun_timeout_ms: u64) -> Duration {
|
||||
Duration::from_millis(aliyun_timeout_ms)
|
||||
.saturating_add(EDITOR_BGFILTER_FLAT_LOCAL_RESERVE)
|
||||
@@ -3395,11 +3386,9 @@ async fn request_editor_background_removal_image_with_bgfilter_worker(
|
||||
source_object_key: &str,
|
||||
audit: &crate::external_api_audit::ExternalApiAuditContext,
|
||||
) -> Result<EditorBackgroundRemovalImage, AppError> {
|
||||
let request_budget_limit_ms = editor_bgfilter_worker_request_budget_limit_ms(
|
||||
state.config.editor_bgfilter_request_timeout_ms,
|
||||
);
|
||||
let request_budget_ms = crate::bgfilter_worker::request_budget_ms(
|
||||
request_budget_limit_ms,
|
||||
// complex 没有 flat fallback,排队上限只预留 2s 传输窗。
|
||||
let max_queue_wait_ms = crate::bgfilter_worker::max_queue_wait_ms(
|
||||
state.config.bgfilter_call_budget_ms(),
|
||||
audit.external_call_deadline,
|
||||
EDITOR_BGFILTER_PARENT_TRANSPORT_WINDOW,
|
||||
);
|
||||
@@ -3410,7 +3399,7 @@ async fn request_editor_background_removal_image_with_bgfilter_worker(
|
||||
None,
|
||||
EDITOR_BGFILTER_DEFAULT_SEG_MODEL,
|
||||
EDITOR_BGFILTER_CROSS_CHECK_DISABLED,
|
||||
request_budget_ms,
|
||||
max_queue_wait_ms,
|
||||
audit,
|
||||
)
|
||||
.await
|
||||
@@ -3440,31 +3429,11 @@ pub(crate) async fn remove_editor_generated_screen_background_with_bgfilter(
|
||||
cross_check: bool,
|
||||
audit: &crate::external_api_audit::ExternalApiAuditContext,
|
||||
) -> Result<EditorScreenBackgroundRemovalOutput, AppError> {
|
||||
remove_editor_generated_screen_background_with_bgfilter_with_request_timeout(
|
||||
state,
|
||||
source_object_key,
|
||||
screen_color,
|
||||
seg_model,
|
||||
cross_check,
|
||||
state.config.editor_bgfilter_request_timeout_ms,
|
||||
audit,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_editor_generated_screen_background_with_bgfilter_with_request_timeout(
|
||||
state: &AppState,
|
||||
source_object_key: &str,
|
||||
screen_color: EditorScreenBackgroundColor,
|
||||
seg_model: &str,
|
||||
cross_check: bool,
|
||||
request_timeout_ms: u64,
|
||||
audit: &crate::external_api_audit::ExternalApiAuditContext,
|
||||
) -> Result<EditorScreenBackgroundRemovalOutput, AppError> {
|
||||
let request_budget_limit_ms =
|
||||
editor_bgfilter_worker_request_budget_limit_ms(request_timeout_ms);
|
||||
let request_budget_ms = crate::bgfilter_worker::request_budget_ms(
|
||||
request_budget_limit_ms,
|
||||
// flat 的排队上限从父剩余预算里预扣「阿里云 + 本地 + 传输」的 fallback 时间,
|
||||
// 保证排队吃不完 fallback 的执行窗口;上限为 0 时 client 直接返回
|
||||
// deadline_exceeded,本函数随即进入 fallback 链。
|
||||
let max_queue_wait_ms = crate::bgfilter_worker::max_queue_wait_ms(
|
||||
state.config.bgfilter_call_budget_ms(),
|
||||
audit.external_call_deadline,
|
||||
editor_bgfilter_flat_deadline_reserve(state.config.aliyun_matting_request_timeout_ms),
|
||||
);
|
||||
@@ -3475,7 +3444,7 @@ pub(crate) async fn remove_editor_generated_screen_background_with_bgfilter_with
|
||||
Some(screen_color.hex),
|
||||
seg_model,
|
||||
cross_check,
|
||||
request_budget_ms,
|
||||
max_queue_wait_ms,
|
||||
audit,
|
||||
)
|
||||
.await
|
||||
@@ -10318,30 +10287,43 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn editor_bgfilter_worker_budget_covers_two_attempts_and_parent_reserves() {
|
||||
assert_eq!(
|
||||
editor_bgfilter_worker_request_budget_limit_ms(180_000),
|
||||
361_000
|
||||
);
|
||||
let config = AppConfig::default();
|
||||
// N=16、est=5s → attempt=160s、callBudget=321s,全部由公式运行时派生。
|
||||
assert_eq!(config.bgfilter_provider_attempt_timeout_ms(), 160_000);
|
||||
assert_eq!(config.bgfilter_call_budget_ms(), 321_000);
|
||||
assert_eq!(
|
||||
editor_bgfilter_flat_deadline_reserve(30_000),
|
||||
Duration::from_secs(39)
|
||||
);
|
||||
|
||||
let complex_deadline = Instant::now() + Duration::from_secs(10);
|
||||
let complex_budget = crate::bgfilter_worker::request_budget_ms(
|
||||
editor_bgfilter_worker_request_budget_limit_ms(180_000),
|
||||
// complex:父剩余 400s − 2s 传输窗 − 321s callBudget → 排队上限约 77s。
|
||||
let complex_deadline = Instant::now() + Duration::from_secs(400);
|
||||
let complex_wait = crate::bgfilter_worker::max_queue_wait_ms(
|
||||
config.bgfilter_call_budget_ms(),
|
||||
Some(complex_deadline),
|
||||
EDITOR_BGFILTER_PARENT_TRANSPORT_WINDOW,
|
||||
);
|
||||
assert!((7_500..=8_000).contains(&complex_budget));
|
||||
assert!((76_500..=77_000).contains(&complex_wait));
|
||||
|
||||
let flat_deadline = Instant::now() + Duration::from_secs(50);
|
||||
let flat_budget = crate::bgfilter_worker::request_budget_ms(
|
||||
editor_bgfilter_worker_request_budget_limit_ms(180_000),
|
||||
// flat:再扣 39s fallback 预留 → 约 40s,排队吃不掉 fallback 的执行窗口。
|
||||
let flat_deadline = Instant::now() + Duration::from_secs(400);
|
||||
let flat_wait = crate::bgfilter_worker::max_queue_wait_ms(
|
||||
config.bgfilter_call_budget_ms(),
|
||||
Some(flat_deadline),
|
||||
editor_bgfilter_flat_deadline_reserve(30_000),
|
||||
);
|
||||
assert!((10_500..=11_000).contains(&flat_budget));
|
||||
assert!((39_500..=40_000).contains(&flat_wait));
|
||||
|
||||
// 父剩余放不下一次完整调用(排队 + callBudget + 预留)时必须为 0:不发请求直接降级。
|
||||
let tight_deadline = Instant::now() + Duration::from_secs(100);
|
||||
assert_eq!(
|
||||
crate::bgfilter_worker::max_queue_wait_ms(
|
||||
config.bgfilter_call_budget_ms(),
|
||||
Some(tight_deadline),
|
||||
editor_bgfilter_flat_deadline_reserve(30_000),
|
||||
),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -10354,22 +10336,20 @@ mod tests {
|
||||
external_call_deadline: Instant::now().checked_sub(Duration::from_millis(1)),
|
||||
};
|
||||
|
||||
let error =
|
||||
match remove_editor_generated_screen_background_with_bgfilter_with_request_timeout(
|
||||
&state,
|
||||
"generated-character-drafts/editor/source.png",
|
||||
parse_editor_screen_background_color(Some("#CFEFFF"))
|
||||
.expect("screen color should parse"),
|
||||
EDITOR_BGFILTER_DEFAULT_SEG_MODEL,
|
||||
EDITOR_BGFILTER_CROSS_CHECK_ENABLED,
|
||||
180_000,
|
||||
&audit,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => panic!("expired parent deadline should stop before flat fallback"),
|
||||
Err(error) => error,
|
||||
};
|
||||
let error = match remove_editor_generated_screen_background_with_bgfilter(
|
||||
&state,
|
||||
"generated-character-drafts/editor/source.png",
|
||||
parse_editor_screen_background_color(Some("#CFEFFF"))
|
||||
.expect("screen color should parse"),
|
||||
EDITOR_BGFILTER_DEFAULT_SEG_MODEL,
|
||||
EDITOR_BGFILTER_CROSS_CHECK_ENABLED,
|
||||
&audit,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => panic!("expired parent deadline should stop before flat fallback"),
|
||||
Err(error) => error,
|
||||
};
|
||||
|
||||
assert_eq!(error.status_code(), StatusCode::GATEWAY_TIMEOUT);
|
||||
assert_eq!(
|
||||
@@ -10390,7 +10370,6 @@ mod tests {
|
||||
let state = AppState::new(AppConfig {
|
||||
bgfilter_worker_base_url: base_url,
|
||||
bgfilter_internal_token: Some("flat-internal-token".to_string()),
|
||||
editor_bgfilter_request_timeout_ms: 20,
|
||||
..AppConfig::default()
|
||||
})
|
||||
.expect("state should build");
|
||||
@@ -10401,14 +10380,13 @@ mod tests {
|
||||
external_call_deadline: None,
|
||||
};
|
||||
|
||||
let removed = remove_editor_generated_screen_background_with_bgfilter_with_request_timeout(
|
||||
let removed = remove_editor_generated_screen_background_with_bgfilter(
|
||||
&state,
|
||||
"generated-character-drafts/editor/source.png",
|
||||
parse_editor_screen_background_color(Some("#CFEFFF"))
|
||||
.expect("screen color should parse"),
|
||||
EDITOR_BGFILTER_DEFAULT_SEG_MODEL,
|
||||
EDITOR_BGFILTER_CROSS_CHECK_ENABLED,
|
||||
2_000,
|
||||
&audit,
|
||||
)
|
||||
.await
|
||||
@@ -10438,7 +10416,9 @@ mod tests {
|
||||
assert_eq!(payload["screenColor"], json!("#CFEFFF"));
|
||||
assert_eq!(payload["segModel"], json!("birefnet"));
|
||||
assert_eq!(payload["crossCheck"], json!(true));
|
||||
assert_eq!(payload["requestBudgetMs"], json!(5_000));
|
||||
// 无绝对 deadline 时排队额度取 callBudget 等长的有界默认值;两者都由 N/est 公式派生。
|
||||
assert_eq!(payload["maxQueueWaitMs"], json!(321_000));
|
||||
assert_eq!(payload["callBudgetMs"], json!(321_000));
|
||||
assert_eq!(payload["auditContext"]["userId"], json!("user-flat"));
|
||||
assert_eq!(payload["auditContext"]["requestId"], json!("request-flat"));
|
||||
|
||||
@@ -10456,7 +10436,6 @@ mod tests {
|
||||
let state = AppState::new(AppConfig {
|
||||
bgfilter_worker_base_url: base_url,
|
||||
bgfilter_internal_token: Some("complex-internal-token".to_string()),
|
||||
editor_bgfilter_request_timeout_ms: 5_000,
|
||||
..AppConfig::default()
|
||||
})
|
||||
.expect("state should build");
|
||||
@@ -10499,7 +10478,8 @@ mod tests {
|
||||
assert!(payload["screenColor"].is_null());
|
||||
assert_eq!(payload["segModel"], json!("birefnet"));
|
||||
assert_eq!(payload["crossCheck"], json!(false));
|
||||
assert_eq!(payload["requestBudgetMs"], json!(11_000));
|
||||
assert_eq!(payload["maxQueueWaitMs"], json!(321_000));
|
||||
assert_eq!(payload["callBudgetMs"], json!(321_000));
|
||||
assert_eq!(
|
||||
payload["auditContext"]["requestId"],
|
||||
json!("request-complex")
|
||||
@@ -10640,21 +10620,13 @@ mod tests {
|
||||
"EDITOR_BGFILTER_CROSS_CHECK_DISABLED",
|
||||
],
|
||||
);
|
||||
assert_function_contains(
|
||||
source,
|
||||
"pub(crate) async fn remove_editor_generated_screen_background_with_bgfilter(",
|
||||
"pub(crate) async fn remove_editor_generated_screen_background_with_bgfilter_with_request_timeout",
|
||||
&[
|
||||
"state.config.editor_bgfilter_request_timeout_ms",
|
||||
"remove_editor_generated_screen_background_with_bgfilter_with_request_timeout",
|
||||
],
|
||||
);
|
||||
assert_function_contains_in_order(
|
||||
source,
|
||||
"async fn remove_editor_generated_screen_background_with_bgfilter_with_request_timeout",
|
||||
"pub(crate) async fn remove_editor_generated_screen_background_with_bgfilter(",
|
||||
"async fn fallback_editor_screen_background_removal",
|
||||
&[
|
||||
"editor_bgfilter_worker_request_budget_limit_ms(request_timeout_ms)",
|
||||
"crate::bgfilter_worker::max_queue_wait_ms",
|
||||
"state.config.bgfilter_call_budget_ms()",
|
||||
"editor_bgfilter_flat_deadline_reserve",
|
||||
"crate::bgfilter_worker::request_bgfilter_worker",
|
||||
"crate::bgfilter_worker::BgfilterBackgroundMode::Flat",
|
||||
@@ -10664,7 +10636,7 @@ mod tests {
|
||||
);
|
||||
assert_function_not_contains(
|
||||
source,
|
||||
"async fn remove_editor_generated_screen_background_with_bgfilter_with_request_timeout",
|
||||
"pub(crate) async fn remove_editor_generated_screen_background_with_bgfilter(",
|
||||
"async fn fallback_editor_screen_background_removal",
|
||||
&[
|
||||
"for attempt in",
|
||||
@@ -10690,7 +10662,8 @@ mod tests {
|
||||
"async fn request_editor_background_removal_image_with_bgfilter_worker",
|
||||
"pub(crate) struct EditorScreenBackgroundRemovalOutput",
|
||||
&[
|
||||
"editor_bgfilter_worker_request_budget_limit_ms",
|
||||
"crate::bgfilter_worker::max_queue_wait_ms",
|
||||
"state.config.bgfilter_call_budget_ms()",
|
||||
"crate::bgfilter_worker::request_bgfilter_worker",
|
||||
"crate::bgfilter_worker::BgfilterBackgroundMode::Complex",
|
||||
"EDITOR_BGFILTER_DEFAULT_SEG_MODEL",
|
||||
@@ -10780,7 +10753,7 @@ mod tests {
|
||||
let source = include_str!("editor_project.rs");
|
||||
assert_function_contains(
|
||||
source,
|
||||
"async fn remove_editor_generated_screen_background_with_bgfilter_with_request_timeout",
|
||||
"pub(crate) async fn remove_editor_generated_screen_background_with_bgfilter(",
|
||||
"async fn fallback_editor_screen_background_removal",
|
||||
&[
|
||||
"crate::bgfilter_worker::request_bgfilter_worker",
|
||||
@@ -10789,7 +10762,7 @@ mod tests {
|
||||
);
|
||||
assert_function_not_contains(
|
||||
source,
|
||||
"async fn remove_editor_generated_screen_background_with_bgfilter_with_request_timeout",
|
||||
"pub(crate) async fn remove_editor_generated_screen_background_with_bgfilter(",
|
||||
"async fn fallback_editor_screen_background_removal",
|
||||
&[
|
||||
"for attempt in",
|
||||
@@ -11118,8 +11091,8 @@ mod tests {
|
||||
assert_function_not_contains(
|
||||
source,
|
||||
"pub(crate) async fn remove_editor_generated_screen_background_with_bgfilter(",
|
||||
"pub(crate) async fn remove_editor_generated_screen_background_with_bgfilter_with_request_timeout",
|
||||
&["fallback_image", "DownloadedOpenAiImage"],
|
||||
"async fn fallback_editor_screen_background_removal",
|
||||
&["fallback_image"],
|
||||
);
|
||||
assert_function_contains(
|
||||
source,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user