diff --git a/deploy/container/api-server.env.example b/deploy/container/api-server.env.example index 57e25f440..6dddc3e82 100644 --- a/deploy/container/api-server.env.example +++ b/deploy/container/api-server.env.example @@ -33,7 +33,7 @@ GENARRATIVE_WALLET_REFUND_OUTBOX_MAX_BYTES=67108864 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 +GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS=120 # BgFilter 失败后的中间兜底:阿里云通用抠图(SegmentCommonImage)。AccessKey 留空则跳过该层, # BgFilter 失败直接本地 editor_green_screen 去背;填入后恢复 BgFilter→阿里云→本地三级兜底。 # AccessKey 也可复用标准 SDK 命名 ALIBABA_CLOUD_ACCESS_KEY_ID / ALIBABA_CLOUD_ACCESS_KEY_SECRET。 diff --git a/deploy/env/bgfilter-worker.env.example b/deploy/env/bgfilter-worker.env.example index 0fd457251..376fc922d 100644 --- a/deploy/env/bgfilter-worker.env.example +++ b/deploy/env/bgfilter-worker.env.example @@ -7,9 +7,9 @@ GENARRATIVE_BGFILTER_WORKER_HOST=127.0.0.1 GENARRATIVE_BGFILTER_WORKER_PORT=8083 # Q:admission 保险丝,只防连接风暴;正常业务不应触达,显式配置时必须 >= N。 GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS=2048 -# flat 熔断只由本进程维护;complex 不读写熔断状态。 +# flat / complex 熔断只由本进程维护;两种模式共享参数,但状态互相独立。 GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD=3 -GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS=300 +GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS=120 GENARRATIVE_API_LOG=info,tower_http=info GENARRATIVE_OTEL_ENABLED=true diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 6d3cd5d20..5c6d18055 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -16,6 +16,15 @@ --- +## 2026-07-22 BgFilter flat 与 complex 使用独立熔断状态 + +- 背景:complex 请求在 provider 持续快速失败时仍会不断发起真实 provider attempt,并为每次已发出的失败生成异步审计;现有 flat 熔断不能约束 complex,且五分钟冷却会让短暂故障恢复后的等待过长。 +- 决策:把现有 flat 熔断行为按原语义复用到 complex。flat / complex 共享 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD=3` 与 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS=120`,但在唯一 `bgfilter-worker` 内分别维护独立的连续失败数和打开截止时间;真实 provider attempt 的失败或成功只更新当前模式。两种模式都在排队前及取得 provider permit 后、第一次真实 HTTP 前检查自身熔断;已经通过第二次检查的逻辑调用仍可完成自己的第二次顺序 attempt。complex 熔断仍直接使父流程失败,不获得 flat 的阿里云 / 本地 fallback;本次不修改失败审计的异步处理流程。 +- 部署边界:deploy / Provision 将 worker env 中历史模板默认 cooldown `300` 定向迁移到 `120`,其它显式自定义值保持不变;`bgfilter_circuit_state` 分别上报 `mode=flat` 与 `mode=complex`。 +- 影响范围:`api-server` BgFilter worker、熔断指标与测试、worker 环境模板、生产部署迁移门禁、BgFilter 架构和运维文档;不修改 SpacetimeDB schema、父业务 fallback、计费或失败审计流程。 +- 验证方式:覆盖两种模式状态隔离、阈值、成功重置、cooldown 到期、permit 前二次检查和部署默认值迁移;运行 api-server BgFilter 定向测试、生产部署脚本门禁、编码检查与 diff 检查。 +- 关联文档:`docs/technical/【后端架构】BgFilter受限资源调度方案-2026-07-21.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 + ## 2026-07-21 BgFilter 首版采用单实例同步内部 HTTP 与父流程原地等待 - 背景:角色动画在单个 `external_generation_job` 内通过 `buffer_unordered(frame_count)` 可并发发射最多 `48` 次 BgFilter 请求;限制父 worker 并发不能限制单个父 job 内的实际 BgFilter 并发。父 job checkpoint / continuation 和 SpacetimeDB 持久子任务都会扩大父状态机、attempt、计费、恢复和清理改动,而当前 BgFilter 成功结果本来就是 HTTP 图片二进制。 diff --git a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md index caf110d3d..36ab3af5f 100644 --- a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md +++ b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md @@ -21,8 +21,8 @@ - 生成资源右上角显示元数据按钮,点击打开独立元数据窗口。图片信息页不展示后端组装后的生图 Prompt,也不提供复制 Prompt;只展示该图片生成时用户在面板里提交的输入快照,包括普通生成提示词、规范表单字段、角色设定、图标素材描述、快速编辑提示词、重绘提示词,以及角色规范 / 常规参考图 / 图标规范 / 编辑参考图等参考图卡片,并提供“复制信息”复制当前可见字段。参考图输入快照只保存 `refType/refId` 行引用,其中 `refType="project-resource"` 指向 `editor_project_resource.resourceId`,`refType="asset"` 指向 `editor_asset.assetId`;不得把图片 Data URL、普通 URL 或 `objectKey` 写入 `generationInputs.references`。旧数据或上传图片没有输入快照时显示 `-`,禁止回退展示内部 Prompt。 - 对生成资源执行重绘时,在右侧创建新的生成结果图层,并自动调整视图显示原图和新图;重绘面板不因提交成功自动关闭,便于连续改提示词。重绘 / 改造输入框只允许从 `generationInputs.fields` 中恢复用户可见输入快照,例如普通生成提示词、视频描述、音效 `prompt`、背景音乐 `gpt_description_prompt`、角色设定、UI 用户输入、图标素材描述、规范表单和宣发素材字段;禁止回退展示资源 `prompt` / `actualPrompt` 中的后端拼接 Prompt、固定生成模板或模型默认提示词。没有用户输入快照的旧图层打开改造时保持空输入,等待用户重新填写。 - 图片生成 / 修改统一经 api-server BFF 接入 VectorEngine。普通生成、生成规范和重绘保留既有 `gpt-image-2` 路径;图片快速编辑统一打开框选区域 + 单提示词 + 模型选择面板,默认沿用原图模型,不展示参考图或比例 / 尺寸控件;其中生成规范类图片固定 `16:9`、`2K`、`gpt-image-2`,面板底部用与可编辑面板一致的比例 / 尺寸 / 模型胶囊按钮展示固定参数,但按钮为禁用态,不允许在该面板改比例、尺寸或模型。`生成角色形象` 与 `生成图标素材` 支持 `nanobanana2`(`gemini-3.1-flash-image-preview`)和 `gpt-image-2`,默认 `nanobanana2`,并在两类面板之间沿用用户上次选择的模型;两类面板不展示抠图背景色或抠图模型选择;前端用户路径固定提交 `screenColor=auto` 和 `segModel=birefnet`,由后端自动决策具体抠图背景色,`anime-seg` 作为内部保留能力不在用户界面暴露。`nanobanana2` 走 `/v1beta/models/{model}:generateContent`,请求体写入 `generationConfig.imageConfig.aspectRatio/imageSize`;`gpt-image-2` 走 `/v1/images/generations` 或 `/v1/images/edits`,请求体按 VectorEngine 文档映射 `size`。宣发素材三个工作流(游戏首图、详情五图、运营海报)固定使用 `gpt-image-2`,面板模型胶囊为禁用态,不提供 `nanobanana2` 入口;前端按 workflow 同时提交 `outputSize`、`aspectRatio` 和 `imageSize`,其中游戏首图为 `720x540 / 4:3`、详情单图为 `720x1280 / 9:16`、运营海报为 `1280x720 / 16:9`;后端收到 `kind: "publication-material"` 时也强制归一为 `gpt-image-2` 生成和计费,生成回填图层优先使用生成占位的 `originalWidth/originalHeight`,即使上游回包尺寸漂移也不得把宣发素材卡片变成随机 `1:1` 或 `4:3`。纯文本生成走 `/api/editor/images/generations`,重绘在前端优先复用当前图层 objectKey;尚未登记的本地图片先上传 OSS,再把 objectKey 交给同一图片生成 BFF,并在原图右侧生成一张新图;普通图层重绘作为 `quick-edit` 参考图提交,角色图层重绘必须按 `kind: "character"` 提交,继续套用角色生成器提示词限定、透明 PNG 后处理和角色资产持久化。`生成视频` 走 `/api/editor/videos/generations`,前端模型入口仅展示 Seedance 2.0 Fast / Seedance 2.0 / Kling 3.0 / Kling 3.0 Omni,不展示 Veo 入口,默认 Seedance 2.0 Fast;视频参数按当前正式面板支持的比例、时长、清晰度和声音开关提交,且 Seedance Fast 与 Seedance 标准版必须按各自真实模型 ID 独立映射,不得混用。生成结果以视频图层加入画布。纯文本生成入口采用 Lovart 式画布内占位图 + 锚定生成输入框:点击生成图片后以当前视口世界中心为目标,经统一 placement 避让后创建选中的灰色占位框,输入框跟随占位框显示;普通图片、角色、图标图集、UI 设计图及其重绘 / 改造入口必须在比例或清晰度恢复、切换时同步把占位框 `width/height/originalWidth/originalHeight` 更新为目标像素尺寸,生成中不得继续显示默认 1K 框;UI 素材提取的 1K / 2K 图集占位和旧图片修改入口也分别使用本次目标尺寸与源图真实尺寸。待生成、生成中和失败后保留的占位图都必须继续支持拖动,生成完成时真实生成图或视频落在最新占位框位置,输入框继续跟随新生成图层;占位图失焦时隐藏高亮边框、左上角生成器名称和右上角原始尺寸,重新聚焦时再显示,且名称 / 尺寸在画布缩小时按 viewport 反向缩放保持屏幕尺寸稳定;点击所有图片 / 视频生成入口并确认请求开始后,必须隐藏对应设置面板,只保留画布内占位图或原图预览,并在预览上显示 Lovart 式生成中遮罩,避免“面板仍占屏”或“预览一起消失”。图片快速编辑和重绘在调用图片 BFF 前必须把当前图层图片解析为已上传的 objectKey 或资源 ID;浏览器临时图片需先上传 OSS;视频素材快速编辑走视频生成 BFF,不允许走图片模型;角色动作的 `生成动画` 仍固定使用 `seedance2.0-fast` 动作 / 视频模型,角色动作素材的 `快速编辑` 按当前帧图片走图片编辑。前端不持有 provider 密钥;上游失败或配置缺失时恢复当前生成设置面板展示失败,不创建 mock 成功图。 -- 图片画布抠图统一通过唯一、只监听 loopback 的 `bgfilter-worker` 调用 BgFilter provider。手动去除背景面向用户任意图片,仍走登录态同源 BFF `POST /api/editor/images/background-removals` 和外部生成队列;API 在入队前拒绝 `data:` / `blob:` 内联媒体,父流程将稳定引用解析为当前账号已登记且归属已校验的私有 OSS object key,只通过一次内部 HTTP RPC 传递 object key、排队预算 `maxQueueWaitMs`、调用预算 `callBudgetMs` 和固定的 `background_mode=complex`、`seg_model=birefnet`、`cross_check=off`,不传源图字节、签名 URL、`file` 或 `screen_color`。子 worker 在每次真实 provider attempt 前签发 600 秒 URL,承担默认 `Q=2048` admission 保险丝、provider 并发 `N=16`、严格最多两次顺序 attempt、响应字节与图片尺寸校验,并把成功图片作为内部 HTTP 二进制 body 直接返回;父流程同步等待该响应且不重试整次内部 RPC。排队只消耗 `maxQueueWaitMs`,取得 provider permit 后才启动 `callBudgetMs`;attempt 按 `N × est × 2`、调用预算按 `2 × attempt + 1s` 派生,冻结 `est=5000ms` 时分别为 `160s / 321s`。complex 任意失败直接返回父流程失败,不接入阿里云 / 本地键色降级,也不读写 flat 熔断。provider 配置继续统一使用 `GENARRATIVE_EDITOR_BGFILTER_BASE_URL` 和 `GENARRATIVE_EDITOR_BGFILTER_TOKEN`,父子共同使用 `GENARRATIVE_BGFILTER_WORKER_CONCURRENCY` 与 `GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS` 派生预算;旧 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN` 只作为 provider token 兼容别名;内部调用另使用 `GENARRATIVE_BGFILTER_WORKER_BASE_URL` 和独立内部 Token。所有令牌只在服务端注入,前端不持有令牌。成功字节返回父流程后,仍由父流程完成最终处理、OSS / asset object 持久化、结果图层与最新项目快照写回;接口只向前端返回 `queueState`,有项目上下文时前端同时创建去背景生成占位并把 `canvasCompletion` 交给后端。 -- 编辑器自己生成的标准纯色背景抠图资产在保存源图后统一以 `background_mode=flat` 调用内部 `bgfilter-worker`。角色形象生成、图标 spritesheet 生成、UI 设计图素材提取和角色动作的前端用户路径都固定把 `screenColor=auto` 注入请求体,但用户可见 `generationInputs.fields` 不再记录 `抠图背景色` 或 `抠图模型`;api-server 在组装 prompt 前调用背景决策模块,从 12 个候选色中选择具体 hex,最多重试 3 次,失败后兜底 `#CFEFFF`。后端仍保留手动 hex 解析能力供内部兼容。最终生图 prompt、动作视频实色背景和子 worker 发往 provider 的 `screen_color` multipart 字段只接收解析后的具体 hex,不透传 `auto`。四条 flat 路径同时把默认 `segModel=birefnet` 传为 `seg_model`,并显式传 `cross_check`:角色形象生成和角色动作逐帧去背传 `on`,图标 spritesheet 和 UI 设计图素材提取传 `off`,不依赖 BgFilter 服务端默认值;后端仍保留识别 `anime-seg` 的内部兼容能力,但前端用户入口不展示也不提交 `seg_model`、`background_mode` 或 `cross_check`。子 worker 独占 flat 进程级熔断并对一次逻辑调用严格最多执行两次顺序 provider attempt;熔断继续使用 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD=3` 和 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS=300` 默认值,但只由子 worker 读写;父侧只发送一次内部 RPC。两次失败、熔断、overload、内部 deadline 或断连后,只要父业务预算仍有效,父流程才按同一 object key 进入“阿里云通用抠图 → 本地 `editor_green_screen` 键色”降级;阿里云 fallback 不属于 `bgfilter-worker`。角色动作生成的序列帧背景色已与生图统一:后端把源角色图合成到视觉决策出的具体 hex 后再图生视频;抽帧后逐帧进入同一条 `内部 bgfilter-worker(background_mode=flat,cross_check=on)→ 父侧阿里云 → 父侧本地键色` 链路。 +- 图片画布抠图统一通过唯一、只监听 loopback 的 `bgfilter-worker` 调用 BgFilter provider。手动去除背景面向用户任意图片,仍走登录态同源 BFF `POST /api/editor/images/background-removals` 和外部生成队列;API 在入队前拒绝 `data:` / `blob:` 内联媒体,父流程将稳定引用解析为当前账号已登记且归属已校验的私有 OSS object key,只通过一次内部 HTTP RPC 传递 object key、排队预算 `maxQueueWaitMs`、调用预算 `callBudgetMs` 和固定的 `background_mode=complex`、`seg_model=birefnet`、`cross_check=off`,不传源图字节、签名 URL、`file` 或 `screen_color`。子 worker 在每次真实 provider attempt 前签发 600 秒 URL,承担默认 `Q=2048` admission 保险丝、provider 并发 `N=16`、严格最多两次顺序 attempt、响应字节与图片尺寸校验,并把成功图片作为内部 HTTP 二进制 body 直接返回;父流程同步等待该响应且不重试整次内部 RPC。排队只消耗 `maxQueueWaitMs`,取得 provider permit 后才启动 `callBudgetMs`;attempt 按 `N × est × 2`、调用预算按 `2 × attempt + 1s` 派生,冻结 `est=5000ms` 时分别为 `160s / 321s`。complex 的真实 provider 失败会累计并打开自身熔断,但与 flat 状态隔离;complex 任意失败或熔断仍直接返回父流程失败,不接入阿里云 / 本地键色降级。provider 配置继续统一使用 `GENARRATIVE_EDITOR_BGFILTER_BASE_URL` 和 `GENARRATIVE_EDITOR_BGFILTER_TOKEN`,父子共同使用 `GENARRATIVE_BGFILTER_WORKER_CONCURRENCY` 与 `GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS` 派生预算;旧 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN` 只作为 provider token 兼容别名;内部调用另使用 `GENARRATIVE_BGFILTER_WORKER_BASE_URL` 和独立内部 Token。所有令牌只在服务端注入,前端不持有令牌。成功字节返回父流程后,仍由父流程完成最终处理、OSS / asset object 持久化、结果图层与最新项目快照写回;接口只向前端返回 `queueState`,有项目上下文时前端同时创建去背景生成占位并把 `canvasCompletion` 交给后端。 +- 编辑器自己生成的标准纯色背景抠图资产在保存源图后统一以 `background_mode=flat` 调用内部 `bgfilter-worker`。角色形象生成、图标 spritesheet 生成、UI 设计图素材提取和角色动作的前端用户路径都固定把 `screenColor=auto` 注入请求体,但用户可见 `generationInputs.fields` 不再记录 `抠图背景色` 或 `抠图模型`;api-server 在组装 prompt 前调用背景决策模块,从 12 个候选色中选择具体 hex,最多重试 3 次,失败后兜底 `#CFEFFF`。后端仍保留手动 hex 解析能力供内部兼容。最终生图 prompt、动作视频实色背景和子 worker 发往 provider 的 `screen_color` multipart 字段只接收解析后的具体 hex,不透传 `auto`。四条 flat 路径同时把默认 `segModel=birefnet` 传为 `seg_model`,并显式传 `cross_check`:角色形象生成和角色动作逐帧去背传 `on`,图标 spritesheet 和 UI 设计图素材提取传 `off`,不依赖 BgFilter 服务端默认值;后端仍保留识别 `anime-seg` 的内部兼容能力,但前端用户入口不展示也不提交 `seg_model`、`background_mode` 或 `cross_check`。子 worker 为 flat / complex 分别维护独立进程级熔断,并对一次逻辑调用严格最多执行两次顺序 provider attempt;两种模式共享 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD=3` 和 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS=120` 默认值,但失败和成功只更新当前模式;父侧只发送一次内部 RPC。flat 两次失败、熔断、overload、内部 deadline 或断连后,只要父业务预算仍有效,父流程才按同一 object key 进入“阿里云通用抠图 → 本地 `editor_green_screen` 键色”降级;阿里云 fallback 不属于 `bgfilter-worker`。角色动作生成的序列帧背景色已与生图统一:后端把源角色图合成到视觉决策出的具体 hex 后再图生视频;抽帧后逐帧进入同一条 `内部 bgfilter-worker(background_mode=flat,cross_check=on)→ 父侧阿里云 → 父侧本地键色` 链路。 - BgFilter 单次真实 provider attempt 不再使用独立固定 timeout,而由父子共同按 `attempt = N × est × 2` 运行时派生;一次逻辑调用的 `callBudgetMs = 2 × attempt + 1s`,从子 worker 取得 provider permit 后才开始计时。当前冻结 `N=16 / est=5000ms` 时为 `160s / 321s`。父侧继续管理父 job / request 总预算,为每个内部 RPC 单独派生 `maxQueueWaitMs`;排队只消耗该字段,不侵蚀 `callBudgetMs`,flat 还需预留阿里云和本地键色 fallback 时间。角色动作不再按本次实际帧数增加 attempt,`32 / 40 / 48` 帧使用同一公式。角色动画继续用 `buffer_unordered(frame_count.max(1))` 同时提交单帧逻辑调用,由唯一子 worker 保证健康进程内实际在飞的 provider 请求不超过 `N`、admission 不超过默认保险丝 `Q=2048`;父流程仍按“对应绿幕源图上传 OSS 并释放原帧字节 → 以 object key 调内部 worker / 按 object key 降级 → 父侧完成透明帧处理并落 OSS”连续组成无序在途流水线,允许响应乱序,并在收口时 collect / drain 全部已提交 frame future、按 `frameIndex` 恢复顺序。任一帧最终失败时仍先排空全部已启动请求,再使整个动作任务失败退款,不发布缺帧动画;最终图片处理、OSS、画布写回和计费始终属于父流程。 - 多产物生成以后端项目快照为唯一画布真相:同一任务实际产生的原始产物、抠图 / 透明化结果和拆分结果都要先登记为 `editor_project_resource`,再通过一次 `canvasCompletion` 原子写入画布。角色形象、图标 spritesheet 和 UI 素材提取的纯色背景原图不能只留在 OSS。透明后处理成功时,处理结果保持主图层和 `generatedLayerId` 锚点,三类任务同时把 provider 原图作为第二个图层放在透明主结果右侧,图标和 UI 的实际拆分素材从 provider 原图右侧开始放置。透明背景处理最终失败时,只把已保存的原图作为唯一主图完成占位,不放透明处理图,图标和 UI 不继续拆分。source-only fallback 的前端只消费后端返回的 `project` / `resource` 快照,不按缺失字段自行构造透明图、切片或图层;任务以 `completed + warning` 收口。该收口只捕获透明背景处理本身的最终失败;phase 上报、provider 原图持久化、透明处理图持久化和 `canvasCompletion` 写回错误仍正常传播,不能被原图降级吞掉。通用 `warning.reason` 是可直接展示的完整原因,并优先于 `sliceWarning`;既有 `sliceWarning.reason` 只表示透明图成功后的自动拆分失败,保留后端原始诊断,inline 前端仅在展示时补充“图集已生成,但自动拆分未完成:”提示,queue worker 则把它归一为 BFF `warning` 字符串后由前端直接展示。无项目上下文时不创建项目资源或画布图层。 - 图片快速编辑面板只保留一个提示词输入框和模型选择,不展示额外参考图或比例 / 尺寸控件;原图 / 原素材作为 `/api/editor/images/edits` 的 `sourceImageSrc` 直接提交,不作为 `referenceImageSrcs`。完整图标图集 `icon-spritesheet` 支持快速编辑,拆分后的单个 `icon` 不提供该入口,前后端必须使用同一素材类型规则。打开快速编辑时画布必须自动平移缩放,让原素材完整落在可视区上半部分,底部面板固定出现在素材下方且不遮挡内容,竖屏 UI 素材也必须完整展示。快速编辑右侧显示矩形、椭圆、画笔框选工具,但进入时不默认启用;点击工具后显示选中态,再点同一工具取消启用。完成框选后,画布红色细框显示连续序号,提示词可按这些编号填写每个区域怎么改。点击 `修改` 后仍停留在当前快速编辑面板显示修改中,不创建独立 `Quick Edit Generator` 画布占位;生成成功后直接用结果覆盖原图图层,失败时保留当前面板并在错误红框中显示具体错误文案。 diff --git a/docs/technical/【后端架构】BgFilter受限资源调度方案-2026-07-21.md b/docs/technical/【后端架构】BgFilter受限资源调度方案-2026-07-21.md index 674f88b51..2ed76b5e8 100644 --- a/docs/technical/【后端架构】BgFilter受限资源调度方案-2026-07-21.md +++ b/docs/technical/【后端架构】BgFilter受限资源调度方案-2026-07-21.md @@ -1,6 +1,6 @@ # BgFilter 受限资源调度方案(同步内部 HTTP 原地等待版) -更新时间:`2026-07-21` +更新时间:`2026-07-22` 状态:`已实施,待生产压测` @@ -19,7 +19,7 @@ | 并发 | 进程内 `Semaphore(N)`,并增加有界 admission 上限 `Q` | | 重试 | 子 worker 对一次逻辑调用最多做两次顺序 provider attempt;父侧不重试整次内部 RPC | | 超时 | 双预算:父侧派生排队预算 `maxQueueWaitMs` 与调用预算 `callBudgetMs`;attempt 上限由 `N × est × 2` 公式运行时派生(est 默认 `5s`),排队不侵蚀调用时间 | -| flat 熔断 | 迁到唯一子 worker 的进程内状态;连续失败达到阈值后暂时跳过 BgFilter,complex 完全不参与 | +| flat / complex 熔断 | 迁到唯一子 worker;两种模式共享阈值和 `120s` cooldown,但分别维护独立进程内状态 | | 业务语义 | 父流程继续负责 Alpha / 尺寸恢复、flat fallback、最终 OSS、画布写回、计费和父终态 | | 动画失败 | 首版保持当前“所有已提交帧都等待并排空”语义,不新增跨帧取消组 | | 崩溃恢复 | 不查询、不恢复 BgFilter 结果;父 job 沿用现有 lease、失败和退款语义 | @@ -89,14 +89,14 @@ flowchart LR 职责边界: - 父流程负责源对象已持久化、owner 校验、请求预算、flat fallback、Alpha / 尺寸恢复、动画 finalizer、最终 OSS / `asset_object` / 画布写回、计费和父终态。 -- `bgfilter-worker` 负责内部协议校验、OSS 签名、并发与排队上限、BgFilter 协议、两次顺序尝试、flat 熔断、provider 失败审计和结果图片校验。 +- `bgfilter-worker` 负责内部协议校验、OSS 签名、并发与排队上限、BgFilter 协议、两次顺序尝试、按模式隔离的熔断、provider 失败审计和结果图片校验。 - SpacetimeDB 不参与本次内部调度;不新增表、reducer、procedure、facade 或生成 bindings。 `bgfilter-worker` 从实现形态看是只监听内部地址的同步 worker service,不是队列 consumer。父 worker 调另一个 worker 在这里是允许的:父进程明确选择保留调用栈和槽位,因此同步内部 HTTP 正是首版的最小交接方式。 这里的“同步等待”是控制流上的 request / response `await`:不会阻塞 OS 执行线程或整个父进程,但父 job future 仍留在通用 worker 的并发集合中,占用一个父 worker 槽,并由现有 heartbeat 继续续租。 -首版进程角色仍复用现有完整 `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,可再缩小共享配置依赖,首版不能假设该拆分已经存在。 +首版进程角色仍复用现有完整 `AppState` 构造路径,以获得 OSS、BgFilter provider、SpacetimeDB 审计、HTTP client 和可观测性依赖;进程角色只阻止它挂载公共路由、claim 外部生成 job 或启动其它后台循环,并不等于它只需要几个调度环境变量。因此生产 unit 必须先加载 `/etc/genarrative/api-server.env` 中父子共享的 `N / est` 与基础配置,再加载 `/etc/genarrative/bgfilter-worker.env` 覆盖监听地址、`Q`、flat / complex 统一熔断参数和 worker 独占参数。后续若拆出轻量专用 state,可再缩小共享配置依赖,首版不能假设该拆分已经存在。 ### 3.1 图片数据流口径 @@ -151,7 +151,7 @@ Authorization: Bearer - 当前部署只有一个配置内私有 OSS bucket,因此请求只传 `sourceObjectKey`,子 worker 从自身 OSS 配置取 bucket 并生成短期签名 URL。 - 如果未来确实支持多个 bucket,新增字段也必须由服务端 allowlist 校验;不能接受调用方提供任意下载 URL。 - `backgroundMode` 只允许 `flat / complex`;`segModel` 继续沿用当前 `birefnet / anime-seg` allowlist;complex 固定使用当前参数组合。 -- `screenColor` 只对 flat 必填;complex 不得误接 flat 参数或熔断。 +- `screenColor` 只对 flat 必填;complex 不得误接 flat 参数,两种模式的熔断状态必须隔离。 - `maxQueueWaitMs` 与 `callBudgetMs` 都是相对预算,不是跨机器绝对时间。前者从 admission 起约束排队阶段(worker 还会用 §5.2 的动态估计对其取 min);后者从取得 provider permit 起计时,覆盖签名、两次 attempt、结果校验和响应构造。`callBudgetMs` 是父侧按 `N / est` 公式算出的“配置指纹”,仅作核对:worker 始终以自己按同一公式派生的值执行,不一致时不拒绝请求,而是记录 warn 日志并递增漂移指标。发布调优 N / est 时新旧进程共存的瞬态漂移因此不会误伤在途任务;持久性漂移的硬拦截由部署脚本的共享 env 对齐校验承担。 - JSON body 设置很小的固定上限;源图字节不进入该 JSON。 @@ -177,7 +177,7 @@ Content-Type: image/png 失败返回有界 JSON,稳定错误码只保留: - `provider_exhausted`:两次真实 provider attempt 都失败; -- `circuit_open`:flat 熔断已打开,未发送 provider 请求; +- `circuit_open`:当前请求模式的熔断已打开,未发送 provider 请求; - `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”实现映射; @@ -316,18 +316,18 @@ inline / External v1 当前没有显式 `RequestContext` deadline 时,内部 R ### 6.3 熔断 -熔断是故障保护:flat 的真实 provider attempt 连续失败达到阈值后,在 cooldown 内暂时不再请求 BgFilter,而是快速返回 `circuit_open`,由父流程进入“阿里云 → 本地”fallback,避免故障 provider 持续占满并发和超时。 +熔断是故障保护:flat 或 complex 的真实 provider attempt 连续失败达到阈值后,当前模式在 cooldown 内暂时不再请求 BgFilter,而是快速返回 `circuit_open`,避免故障 provider 持续占满并发和超时。flat 由父流程继续进入“阿里云 → 本地”fallback;complex 仍直接失败,不获得 flat fallback。 保持当前语义: -- 只有 flat 读取和更新熔断;complex 完全不读写。 -- flat 在取得 permit、即将发送第一次 provider HTTP 前重新检查熔断,避免 48 个排队请求在熔断打开前全部通过旧检查。 +- flat / complex 共享同一阈值与 cooldown 配置,但分别维护独立的 `consecutive_failures / open_until`;任一模式的失败或成功只更新自身状态,不影响另一模式。 +- 两种模式都在取得 permit、即将发送第一次 provider HTTP 前重新检查自身熔断,避免排队请求在熔断打开前全部通过旧检查。 - 已经获准执行的逻辑调用,即使第一次失败使熔断打开,也仍允许在预算内完成自己的第二次顺序 attempt;后续请求快速返回 `circuit_open`。 -- 每个真实失败 attempt 计一次失败,保持当前计数口径;flat 任一真实 attempt 成功后重置。 +- 每个真实失败 attempt 计一次失败,保持当前计数口径;任一真实 attempt 成功后只重置当前模式。 - 只有拿到完整公式 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 专属环境,避免运维人员在父侧修改了一个实际不生效的配置。 +flat / complex 统一使用的 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD=3` 与 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS=120` 属于 `bgfilter-worker` 运行参数;父 API / external-generation worker 不再读取或更新熔断。生产示例必须把这两个值放进 worker 专属环境;deploy / Provision 只把历史模板默认 cooldown `300` 定向迁移为 `120`,保留其它显式自定义值。 首版不增加 QPS 限制。若 provider 以后要求 QPS,需要另加 token bucket;不能把并发 semaphore 当作 QPS。 @@ -364,7 +364,7 @@ flat 熔断的 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD` 与 `GENA | flat | `cancelled`(保留码,首版子 worker 不产生),或父 job cancellation / 绝对 deadline 已生效 | 立即向上退出,不再启动阿里云或本地 fallback | | flat | `invalid_request / unauthorized` | 作为内部契约或部署配置错误失败,不 fallback、不计入 BgFilter 熔断 | | complex 手动去背景 | 图片二进制 | 父流程继续最终 OSS、资源和画布写回 | -| complex 手动去背景 | 任意非成功或断连 | 父流程直接失败;不得接 flat fallback,不得修改 flat 熔断 | +| complex 手动去背景 | 任意非成功或断连 | 父流程直接失败;provider 失败只累计 complex 熔断,不得接 flat fallback 或修改 flat 熔断 | | 角色 / 图标 / UI 后处理最终失败 | BgFilter 与 fallback 都未得到可用结果 | 保留已持久化 provider 原图,以现有 `completed + warning` 收口 | | 动画任一帧最终失败 | 该帧完整 fallback / finalizer / PUT 仍失败 | 排空其它已提交帧后,整项动画按现有语义失败退款 | @@ -411,11 +411,11 @@ BgFilter 成功二进制不是一份新的业务资产: - `bgfilter_provider_http_seconds{mode,attempt,outcome}` - `bgfilter_internal_request_total{mode,outcome}` - `bgfilter_internal_response_bytes` -- `bgfilter_circuit_state` +- `bgfilter_circuit_state{mode=flat|complex}` 日志只写 `requestId`、父 job / request correlation、mode、attempt、排队耗时、provider 耗时、结果码和安全 object key;不得记录请求/响应图片 body。 -当前 flat 的每次 provider 失败审计必须迁到子 worker,保留“第一次失败、第二次成功”也可观察的事实。审计口径以“该次 attempt 是否已发出 provider HTTP”为界:已发出的失败一律写入 `external_api_call_failure`,包括被剩余预算截短后发生的 timeout 与 response 阶段超时(它们不计入熔断,但必须可审计);未发出的失败(预算不足未启动、签名失败)以及内部 admission、鉴权和本地配置错误不伪装成 BgFilter provider 失败。子 worker 进程角色不共享 api / extgen 的落盘 tracking outbox,失败审计由异步任务直写 SpacetimeDB,并纳入 shutdown tracker,优雅退出前排空;进程被强杀时可能丢失,属首版接受行为。 +flat / complex 的每次 provider 失败审计都必须留在子 worker,保留“第一次失败、第二次成功”也可观察的事实。审计口径以“该次 attempt 是否已发出 provider HTTP”为界:已发出的失败一律写入 `external_api_call_failure`,包括被剩余预算截短后发生的 timeout 与 response 阶段超时(它们不计入熔断,但必须可审计);未发出的失败(预算不足未启动、签名失败)以及内部 admission、鉴权和本地配置错误不伪装成 BgFilter provider 失败。子 worker 进程角色不共享 api / extgen 的落盘 tracking outbox,失败审计由异步任务直写 SpacetimeDB,并纳入 shutdown tracker,优雅退出前排空;进程被强杀时可能丢失,属首版接受行为。 ## 10. 实施与部署计划 @@ -423,7 +423,7 @@ 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 进程级熔断。 +3. 增加 admission `Q`、`Semaphore(N)`、两次顺序 attempt、预算检查、结果限长 / 解码校验、response-body permit guard 和 flat / complex 独立进程级熔断。 4. 增加父侧共享内部 HTTP client。该 client 不自动重试;对 `2xx` 读取并返回受限图片字节,对非 `2xx` 只解析有界类型化 JSON 错误;把父剩余预算显式转换为 `maxQueueWaitMs` 与公式 `callBudgetMs`,client timeout 固定取两者之和加 `2s`。父绝对预算只在派生 `maxQueueWaitMs` 时扣除 callBudget 与父侧预留,不在发送阶段重新裁剪或挪用两笔相对预算。 5. 用内部 client 替换两个集中调用边界: - flat:`remove_editor_generated_screen_background_with_bgfilter`; @@ -467,8 +467,8 @@ BgFilter 成功二进制不是一份新的业务资产: - `callBudgetMs` 与 worker 本进程公式值不一致时不拒绝:worker 以自身公式值执行,记 warn 并递增漂移指标;发布调优 N / est 的新旧进程共存窗口内,在途 flat 任务仍能正常执行或走既有 fallback,不得因瞬态漂移触发 `invalid_request`(该码禁止 fallback)。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`。 +- 父业务预算仍有效时,flat 两次失败、熔断、overload、内部 RPC deadline 或断连仍走“阿里云 → 本地”;complex 任意失败或自身熔断都直接失败,不接 flat fallback。 +- flat / complex 分别按自身真实失败 attempt 计数且状态互不影响;由剩余业务预算截短的 timeout 不计入。两种模式都在 permit 前二次检查;已获准调用可完成第二次,后续同模式排队请求快速 `circuit_open`。 - `cancelled`(仅验证父侧映射,保留码首版不产生)、父 cancellation / 绝对 deadline、`invalid_request` 和 `unauthorized` 不启动 flat fallback;其它 flat 错误只在父业务预算仍有效时进入 fallback。 - 已发出的 provider attempt 失败(含预算截短 timeout 与 response 阶段超时)全部落 `external_api_call_failure`;未发出与纯内部失败不落。审计任务由 shutdown tracker 排空后进程才退出。 - 客户端断连时,等待 permit 的请求最终由 deadline 收口;已开始 provider attempt 持有 permit 并排空。明确 cancellation 已被观察到后不再开始第二次,单纯 TCP 断连只作 best-effort 测试,不作为硬保证。 @@ -480,7 +480,7 @@ BgFilter 成功二进制不是一份新的业务资产: - worker 重启 / RPC 丢失不查询、不恢复结果;父 job 的 heartbeat、lease、失败退款和 fencing 保持现状。 - External v1 / inline 不再直连 BgFilter;公共 router、BFF、账单和任务列表中没有内部 endpoint 或内部调用记录。 - 生产不存在两个同时运行的 `bgfilter-worker`,`N` 或 `est` 缺失、为 `0` 时 fail-closed;`Q` 显式配置时必须 `>= N`。 -- worker unit 先加载共享 API env、再加载 worker 专属 env;父子有效 `N` 与 `est` 完全一致(两者都在共享 API env),flat 熔断参数只由子 worker 配置和执行。 +- worker unit 先加载共享 API env、再加载 worker 专属 env;父子有效 `N` 与 `est` 完全一致(两者都在共享 API env),flat / complex 统一熔断参数只由子 worker 配置和执行,cooldown 默认 `120s`。 - `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:` endpoint;旧非空配置不能因为“无需补默认值”而绕过一致性检查。 - `genarrative-bgfilter-worker.service` 必须保持 `TimeoutStopSec=900`,覆盖 `callBudgetMs` 排空上界(约 `321s`)与停止收口余量;停止时排队请求立即类型化失败,不参与排空。 diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index 0777e0950..30931de2f 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -244,7 +244,7 @@ npm run check:server-rs-ddd - 抠图输入以私有 OSS 作为内存生命周期边界:生成原图和角色动作抽取帧上传时消费图片字节所有权,上传完成后不保留原图缓冲;手动去背景直接解析并校验已有 OSS object key,不下载原图。BgFilter 必须为 object key 签发 600 秒 GET URL 并通过 multipart `image_url` 提交,不用 `file` 重传;flat 链路进入阿里云 fallback 时由 `platform-matting` URL 接口单独下载并上传 `AuthorizeFileUpload` 临时对象,在推理前释放下载缓冲,继续 fallback 到本地键色时再单独下载一次原图,本地产出后释放本次原图下载缓冲。签名 URL 不得写入日志、审计或持久化。 - 角色动作抠图输入像素边界:仅图片画布角色动作链路在 FFmpeg 抽帧后、源帧上传 OSS 前,把帧解码为 RGB8,并按最终 `frameWidth × frameHeight` 的 contain 比例使用 `Triangle` 只缩放到内容尺寸;该阶段不得创建最终目标尺寸画布、不得引入 Alpha 通道,也不得插入任何 padding。BgFilter、阿里云通用抠图和本地键色降级共享这个无补边源帧 object key。抠图返回后才统一转为 RGBA8,按相同比例居中放入最终目标尺寸画布,并用 `RGBA(0,0,0,0)` 补齐透明 padding。以 `560×752 → 323×480` 为例,抠图输入固定为无 Alpha、无补边的 `323×434 RGB8 PNG`,最终输出为上下各 `23px` 透明补边的 `323×480 RGBA8 PNG`。旧 `/api/assets/character-animation/*` 动作发布链路继续保留原有帧 finalizer,不适用该输入规则。抽帧解码后若携带 Alpha 通道,必须先把像素按白底合成为不透明再转 RGB8,禁止直接丢弃 Alpha——全透明像素下未定义的 RGB 值会以杂色进入抠图输入,重新引入杂色边缘;共享 FFmpeg 抽帧命令保持不固定 `-pix_fmt`,白底合成只属于该链路的 BgFilter 输入准备阶段。 - 阿里云通用抠图的非上海地域输入不得使用 `viapiutils/GetOssStsToken`、固定 `viapi-customer-temp` 或 OSS V1 PUT。`platform-matting` 必须按官方新版 SDK Advance 协议调用 `AuthorizeFileUpload`,使用动态返回的单对象 Policy 执行 multipart POST,再把临时上海 OSS URL 交给 `SegmentCommonImage`;输入归一化、结果下载与原尺寸 Alpha 回贴继续留在同一适配器内。该协议仍上传图片字节,不等同于阿里云服务端直接抓取任意公网 URL,也不改变上层 BgFilter → 阿里云 → 本地降级顺序。 -- 编辑器抠图服务:手动 `POST /api/editor/images/background-removals` 与角色形象生成、图标 spritesheet 生成、UI 设计图素材提取、角色动作抽帧后的透明化统一通过唯一 loopback `bgfilter-worker` 调用 BgFilter provider。provider 配置继续使用 `GENARRATIVE_EDITOR_BGFILTER_BASE_URL` 与 `GENARRATIVE_EDITOR_BGFILTER_TOKEN`,默认 base URL 为 `http://58.87.105.82/bgfilter`;单次 provider attempt 上限不再独立配置,由公式 `N × est × 2` 运行时派生,其中 `est = GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS`(默认 `5000`,依据为服务端高并发单图处理约 1-3s、网络约 3-5s),旧 `GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS` 已删除;旧 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN` 只作为 provider token 的兼容回退别名,原手动去背景专用 base URL / timeout 配置已经删除。父流程先把候选 `objectKey`、`resourceId` 或 `assetId` 解析为当前 owner 已登记的私有 OSS object key;BFF 入队前统一拒绝 `data:` / `blob:`,底层 resolver 在解析引用前再次拒绝内联媒体并完成登记状态与 owner 校验。父流程只通过一次内部 HTTP RPC 传递 object key、排队预算 `maxQueueWaitMs`、调用预算 `callBudgetMs` 与模式参数,不传图片字节或签名 URL,并同步等待子 worker 返回的受限图片二进制 body。子 worker 在每次真实 provider attempt 前签发短期 OSS URL,承担 admission 保险丝 `Q`(默认 `2048`,仅防连接风暴)、provider 并发 `N`(生产 `16`);排队 deadline 从 `Q` admission 时刻起算,完成 JSON 校验并进入 provider permit 等待队列时再取得队长快照,按 `min((队长+5)×est×2, maxQueueWaitMs)` 约束排队等待。子 worker 还负责严格最多两次顺序 attempt、结果校验和 flat 进程级熔断;熔断继续使用 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD=3` 和 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS=300` 默认值,但只由子 worker 读写。手动去背景固定使用 `background_mode=complex`、`seg_model=birefnet`、`cross_check=off`,不传 `file` 或 `screen_color`;complex 任意失败直接返回父流程失败,不接 flat fallback,也不读写 flat 熔断。标准纯色背景四条链路固定使用 `background_mode=flat`、`screen_color=`、`seg_model=` 和 `cross_check=`,其中角色形象生成和角色动作逐帧去背传 `cross_check=on`,图标 spritesheet 生成和 UI 设计图素材提取传 `cross_check=off`。前端用户路径不展示抠图模型、模式或 cross-check,固定提交默认 `birefnet`,后端仍识别内部保留的 `anime-seg`;这些参数只属于后端内部供应商策略,不进入前端或外部 OpenAPI。父侧不重试整次内部 RPC;flat 两次 provider attempt 失败、熔断、overload、内部 deadline 或断连后,只要父业务预算仍有效,父流程才继续“阿里云通用抠图 → 本地 `editor_green_screen` 键色扣除”,熔断期不得直接退化到本地兜底。角色动作视频生成的背景色已与生图链路统一:`screenColor=auto` 时由视觉 LLM(`gpt-5-mini`,Responses 协议、low 推理档)读源角色图自动决策,并经硬过滤器剔除与前景 / 皮肤撞色的候选,手动 hex 则尊重用户选择;透明源角色图在提交 Ark 图生视频前先合成到选定背景色实色,使视频背景等于抠图键色;抽帧后每帧先上传私有 OSS 并释放原帧缓冲,再以 object key 固定使用 `seg_model=birefnet`、`cross_check=on` 进入上述三段式链路。阿里云通用抠图配置为 `GENARRATIVE_ALIYUN_MATTING_ENABLED`、`GENARRATIVE_ALIYUN_MATTING_ENDPOINT`、`GENARRATIVE_ALIYUN_MATTING_ACCESS_KEY_ID`、`GENARRATIVE_ALIYUN_MATTING_ACCESS_KEY_SECRET` 和 `GENARRATIVE_ALIYUN_MATTING_REQUEST_TIMEOUT_MS`;未配置专用 AK/SK 时可复用 `ALIBABA_CLOUD_ACCESS_KEY_ID` / `ALIBABA_CLOUD_ACCESS_KEY_SECRET`,默认 endpoint 为 `imageseg.cn-shanghai.aliyuncs.com`。标准纯色背景链路中,子 worker 已发出的 BgFilter provider 失败(含被剩余预算截短后发生的 timeout 与 response 阶段超时,这类失败不计入 flat 熔断但必须落审计)和父侧阿里云抠图链路已开始后的失败(包括源 OSS GET 成功后的解码、尺寸校验和归一化失败)都写入 `external_api_call_failure` 审计;真正开始外部调用前的本地预检不写该审计,并在 `failureStage` 中保留 `source_decode`、`source_validate` 等阶段。成功图片字节返回后,最终 Alpha / 尺寸恢复、OSS / asset object、画布写回、计费和父任务终态仍全部由父流程负责。 +- 编辑器抠图服务:手动 `POST /api/editor/images/background-removals` 与角色形象生成、图标 spritesheet 生成、UI 设计图素材提取、角色动作抽帧后的透明化统一通过唯一 loopback `bgfilter-worker` 调用 BgFilter provider。provider 配置继续使用 `GENARRATIVE_EDITOR_BGFILTER_BASE_URL` 与 `GENARRATIVE_EDITOR_BGFILTER_TOKEN`,默认 base URL 为 `http://58.87.105.82/bgfilter`;单次 provider attempt 上限不再独立配置,由公式 `N × est × 2` 运行时派生,其中 `est = GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS`(默认 `5000`,依据为服务端高并发单图处理约 1-3s、网络约 3-5s),旧 `GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS` 已删除;旧 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN` 只作为 provider token 的兼容回退别名,原手动去背景专用 base URL / timeout 配置已经删除。父流程先把候选 `objectKey`、`resourceId` 或 `assetId` 解析为当前 owner 已登记的私有 OSS object key;BFF 入队前统一拒绝 `data:` / `blob:`,底层 resolver 在解析引用前再次拒绝内联媒体并完成登记状态与 owner 校验。父流程只通过一次内部 HTTP RPC 传递 object key、排队预算 `maxQueueWaitMs`、调用预算 `callBudgetMs` 与模式参数,不传图片字节或签名 URL,并同步等待子 worker 返回的受限图片二进制 body。子 worker 在每次真实 provider attempt 前签发短期 OSS URL,承担 admission 保险丝 `Q`(默认 `2048`,仅防连接风暴)、provider 并发 `N`(生产 `16`);排队 deadline 从 `Q` admission 时刻起算,完成 JSON 校验并进入 provider permit 等待队列时再取得队长快照,按 `min((队长+5)×est×2, maxQueueWaitMs)` 约束排队等待。子 worker 还负责严格最多两次顺序 attempt、结果校验和按 flat / complex 隔离的进程级熔断;两种模式共享 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD=3` 和 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS=120` 默认值,但失败和成功只更新当前模式,且只由子 worker 读写。手动去背景固定使用 `background_mode=complex`、`seg_model=birefnet`、`cross_check=off`,不传 `file` 或 `screen_color`;complex provider 失败累计自身熔断,任意失败或自身熔断都直接返回父流程失败,不接 flat fallback,也不影响 flat 熔断。标准纯色背景四条链路固定使用 `background_mode=flat`、`screen_color=`、`seg_model=` 和 `cross_check=`,其中角色形象生成和角色动作逐帧去背传 `cross_check=on`,图标 spritesheet 生成和 UI 设计图素材提取传 `cross_check=off`。前端用户路径不展示抠图模型、模式或 cross-check,固定提交默认 `birefnet`,后端仍识别内部保留的 `anime-seg`;这些参数只属于后端内部供应商策略,不进入前端或外部 OpenAPI。父侧不重试整次内部 RPC;flat 两次 provider attempt 失败、熔断、overload、内部 deadline 或断连后,只要父业务预算仍有效,父流程才继续“阿里云通用抠图 → 本地 `editor_green_screen` 键色扣除”,熔断期不得直接退化到本地兜底。角色动作视频生成的背景色已与生图链路统一:`screenColor=auto` 时由视觉 LLM(`gpt-5-mini`,Responses 协议、low 推理档)读源角色图自动决策,并经硬过滤器剔除与前景 / 皮肤撞色的候选,手动 hex 则尊重用户选择;透明源角色图在提交 Ark 图生视频前先合成到选定背景色实色,使视频背景等于抠图键色;抽帧后每帧先上传私有 OSS 并释放原帧缓冲,再以 object key 固定使用 `seg_model=birefnet`、`cross_check=on` 进入上述三段式链路。阿里云通用抠图配置为 `GENARRATIVE_ALIYUN_MATTING_ENABLED`、`GENARRATIVE_ALIYUN_MATTING_ENDPOINT`、`GENARRATIVE_ALIYUN_MATTING_ACCESS_KEY_ID`、`GENARRATIVE_ALIYUN_MATTING_ACCESS_KEY_SECRET` 和 `GENARRATIVE_ALIYUN_MATTING_REQUEST_TIMEOUT_MS`;未配置专用 AK/SK 时可复用 `ALIBABA_CLOUD_ACCESS_KEY_ID` / `ALIBABA_CLOUD_ACCESS_KEY_SECRET`,默认 endpoint 为 `imageseg.cn-shanghai.aliyuncs.com`。标准纯色背景链路中,子 worker 已发出的 BgFilter provider 失败(含被剩余预算截短后发生的 timeout 与 response 阶段超时,这类失败不计入熔断但必须落审计)和父侧阿里云抠图链路已开始后的失败(包括源 OSS GET 成功后的解码、尺寸校验和归一化失败)都写入 `external_api_call_failure` 审计;真正开始外部调用前的本地预检不写该审计,并在 `failureStage` 中保留 `source_decode`、`source_validate` 等阶段。成功图片字节返回后,最终 Alpha / 尺寸恢复、OSS / asset object、画布写回、计费和父任务终态仍全部由父流程负责。 - BgFilter 连接复用、超时与动作帧流水线:`AppState` 分别复用父侧内部 worker HTTP Client 和子 worker 专用 BgFilter provider HTTP Client;父侧对一次逻辑调用只发送一次内部 RPC,不自动重试,子 worker 在同一个 `N` permit 内严格最多执行两次顺序 provider attempt。唯一子 worker 使用 `GENARRATIVE_BGFILTER_WORKER_CONCURRENCY=N`(生产 `16`)限制真实 provider 在途数;`GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS=Q` 降级为可选 admission 保险丝(默认 `2048`,仅防连接风暴,显式配置时必须 `>= N`)。超时全部由 `N` 与 `est` 运行时派生:单 attempt 上限 `N × est × 2`、调用预算 `callBudgetMs = 2 × attempt + 1s`(自取得 `N` permit 起算)、排队等待受 `min((provider 等待队列队长+5)×est×2, maxQueueWaitMs)` 双重上界(动态项充当自适应过载探测,超时带 `bound = estimate | parent` 标记),排队不侵蚀调用预算;`N` 与 `est` 必须同放共享 API 基础环境;请求携带的 `callBudgetMs` 只是父侧配置指纹,worker 比对后不一致只告警并计 `bgfilter_internal_call_budget_drift_total` 指标、始终以本进程公式值执行——发布调优 N / est 的新旧进程共存窗口不得误伤在途任务,持久漂移由部署脚本共享 env 对齐校验在启动前拦截。角色动作不再增加 `2000ms × 本次实际帧数`,`32 / 40 / 48` 帧使用相同公式。父侧按剩余绝对预算派生 `maxQueueWaitMs`(flat 扣除 `39s` 父侧预留(`37s` fallback + `2s` 传输窗),complex 只留 `2s` 传输窗;`<= 0` 时不发请求直接降级 / 失败),client timeout 取 `maxQueueWaitMs + callBudgetMs + 2s`;每次 attempt 前重新签发短期 OSS URL,剩余时间不足时不开始新的 attempt。父侧成功响应解码槽 `P = 8`。角色动作继续以 `buffer_unordered(frame_count.max(1))` 将全部单帧逻辑调用加入无序在途集合;返回结果携带原始帧序并在最终 collect / drain 全部已提交 Future 后排序,任一帧最终失败时必须先排空全部已启动 Future,再让整个动作任务失败退款,不能发布缺帧动画。单帧按“绿幕源图 owned 上传 OSS 并释放原帧 → 以 object key 调内部 worker / 按 object key 由父侧降级 → 父侧处理透明帧并落 OSS”流水化。角色动画源帧 PUT、透明帧 PUT 和最终帧 HEAD 仍统一复用 `AppState` 内初始化一次的 OSS HTTP Client(连接池参数为 connect 30 秒、request 60 秒、idle 300 秒、每 host 8 个 idle 连接、TCP keepalive 60 秒),并受进程级 8 路 OSS semaphore 限制;BgFilter provider 的 `N` 不占该 OSS permit,阿里云和本地处理既不占 OSS permit,也不受 `N / Q` 限制。每个 OSS 网络 attempt 单独获取 permit,退避期间释放;PUT/HEAD 动画帧请求最多 3 次(250ms、500ms 退避),只重试无 HTTP 响应的传输错误、timeout、OSS PutObject 的 `400 + RequestTimeout`、PUT `400` 错误体读取失败(未解析出 `Code`,按 timeout/transport 归类)、408、429 和 5xx。动作帧 PUT 只在 400 响应中有界读取最多 16 KiB OSS 错误 XML,并保留 `Code` 与响应头优先的 `x-oss-request-id`;错误体读取超时/断流时保留已读字节,已解析出的 `Code` 优先生效,未解析出 `Code` 则按 timeout/transport 归类重试;除 `RequestTimeout` 与该错误体读取失败情形外的其他 400、401/403/404、配置、URL/签名和空请求体错误不重试。最终帧 HEAD 失败只重试 HEAD,不重复 PUT。 - Match3D 物品 sheet:关卡整图完成后走 VectorEngine `/v1/images/edits` multipart `image`,模型为 `gpt-image-2`,`2K 1:1` 输出 `10*10` spritesheet;物品 sheet prompt 固定要求单一纯绿色 `#00FF00 / RGB(0,255,0)` 绿幕背景,后端上传 OSS 前必须把绿幕扣成透明 PNG,并把透明整图写入 `itemSpritesheetImageSrc/itemSpritesheetImageObjectKey`。后端优先按透明 alpha 连通域从该 sheet 识别真实素材矩形并持久化 20 个物品、每个 5 个形态;识别数量不足时才回退 `10*10` 固定网格。通用系列素材图集的行列索引按每行 2 个物品计算,必须落在 `1..=10`,难度只决定运行态加载 3 / 9 / 15 / 20 种。 - Match3D UI spritesheet 和背景派生图:关卡整图作为参考图并发生成 `1K 1:1` UI spritesheet 与 `1K 9:16` 背景图,模型均为 `gpt-image-2`。UI spritesheet prompt 固定要求单一纯绿色 `#00FF00 / RGB(0,255,0)` 绿幕背景,后端上传 OSS 前必须把绿幕扣成透明 PNG;背景图必须合成为全画幅不透明 PNG。 diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index 08c6c6b6b..de504eb51 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -74,9 +74,9 @@ HTTP 角色的 `GENARRATIVE_SPACETIME_POOL_SIZE` 只表示 procedure / reducer lease 过期后不代表任务一定再次执行:claim transaction 只有在 `attempt < max_attempts` 时才会递增 attempt 并返回 worker;如果过期的是最终 attempt,则直接把 job 收口为 `failed`、清理 lease,并按入队冻结价格为当前 attempt 原子退款或写 cancellation intent。该终态任务不会再次进入 provider executor,迟到 consume 会被 settlement intent 拒绝。 -图片画布角色图、图标素材、UI 素材提取和角色动作逐帧去背景使用 `background_mode=flat`;手动 `POST /api/editor/images/background-removals` 使用 `background_mode=complex`、`seg_model=birefnet`、`cross_check=off`。父流程不再直连 BgFilter,而是把已持久化的私有 OSS object key、模式参数、排队预算 `maxQueueWaitMs` 和调用预算 `callBudgetMs` 交给唯一的 loopback `bgfilter-worker`。子 worker 负责签发短期源 URL、全局 admission `Q`、provider 并发 `N`、最多两次顺序 attempt、结果校验和 flat 进程级熔断;成功时直接用内部 HTTP 二进制 body 把原始结果图片字节返回父流程,不写 raw OSS。当前冻结 `N=16`、单图估时 `est=5000ms`,`Q` 默认 `2048` 且仅作为连接风暴保险丝;角色动画仍可同时提交最多 `48` 个单帧逻辑调用,但健康 worker 中实际在飞的 BgFilter provider 请求不超过 `N`。 +图片画布角色图、图标素材、UI 素材提取和角色动作逐帧去背景使用 `background_mode=flat`;手动 `POST /api/editor/images/background-removals` 使用 `background_mode=complex`、`seg_model=birefnet`、`cross_check=off`。父流程不再直连 BgFilter,而是把已持久化的私有 OSS object key、模式参数、排队预算 `maxQueueWaitMs` 和调用预算 `callBudgetMs` 交给唯一的 loopback `bgfilter-worker`。子 worker 负责签发短期源 URL、全局 admission `Q`、provider 并发 `N`、最多两次顺序 attempt、结果校验和按 flat / complex 隔离的进程级熔断;成功时直接用内部 HTTP 二进制 body 把原始结果图片字节返回父流程,不写 raw OSS。当前冻结 `N=16`、单图估时 `est=5000ms`,`Q` 默认 `2048` 且仅作为连接风暴保险丝;角色动画仍可同时提交最多 `48` 个单帧逻辑调用,但健康 worker 中实际在飞的 BgFilter provider 请求不超过 `N`。 -BgFilter 不再配置独立的固定 attempt timeout。父子共同按 `attempt = N × est × 2` 派生单次真实 provider attempt 上限,并按 `callBudgetMs = 2 × attempt + 1s` 派生调用预算;当前 `N=16 / est=5000ms` 时分别为 `160s / 321s`。父侧仍管理父 job 总预算,按剩余绝对预算派生 `maxQueueWaitMs`;子 worker 从 admission 开始只用该字段等待 provider permit,取得 permit 后才启动 `callBudgetMs`,排队不侵蚀两次完整 attempt 窗口。flat 还由父侧预留阿里云 request timeout 和本地处理余量。flat 两次失败、熔断、overload 或内部 RPC 故障且父业务预算仍有效时,父流程才继续“阿里云通用抠图 → 本地键色”;阿里云 fallback 不属于 BgFilter worker。complex 任意失败直接使父流程失败,不接 flat fallback,也不读写 flat 熔断。父侧不会重试整次内部 HTTP,避免子侧两次乘成四次 provider attempt。 +BgFilter 不再配置独立的固定 attempt timeout。父子共同按 `attempt = N × est × 2` 派生单次真实 provider attempt 上限,并按 `callBudgetMs = 2 × attempt + 1s` 派生调用预算;当前 `N=16 / est=5000ms` 时分别为 `160s / 321s`。父侧仍管理父 job 总预算,按剩余绝对预算派生 `maxQueueWaitMs`;子 worker 从 admission 开始只用该字段等待 provider permit,取得 permit 后才启动 `callBudgetMs`,排队不侵蚀两次完整 attempt 窗口。flat 还由父侧预留阿里云 request timeout 和本地处理余量。flat 两次失败、熔断、overload 或内部 RPC 故障且父业务预算仍有效时,父流程才继续“阿里云通用抠图 → 本地键色”;阿里云 fallback 不属于 BgFilter worker。complex 的真实 provider 失败只累计自身熔断,任意失败或熔断仍直接使父流程失败,不接 flat fallback,也不影响 flat 状态。父侧不会重试整次内部 HTTP,避免子侧两次乘成四次 provider attempt。 阿里云通用抠图默认 `GENARRATIVE_ALIYUN_MATTING_ENABLED=true`,但必须在 `api-server.env` 填入 `GENARRATIVE_ALIYUN_MATTING_ACCESS_KEY_ID` / `GENARRATIVE_ALIYUN_MATTING_ACCESS_KEY_SECRET`(或标准 SDK 命名 `ALIBABA_CLOUD_ACCESS_KEY_ID` / `ALIBABA_CLOUD_ACCESS_KEY_SECRET`)才会真正启用;AccessKey 缺失时启动日志会打印「阿里云抠图 AccessKey 未配置,跳过抠图客户端初始化」,flat 失败后直接进入本地键色。两种模式的 provider 配置统一使用 `GENARRATIVE_EDITOR_BGFILTER_BASE_URL`、`GENARRATIVE_EDITOR_BGFILTER_TOKEN`,父子共同使用 `GENARRATIVE_BGFILTER_WORKER_CONCURRENCY` 与 `GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS` 派生预算;旧 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN` 只保留为 token 兼容别名。修改 provider、内部 worker 或 fallback 配置后,需要按角色重启对应进程。 @@ -605,9 +605,9 @@ Nginx 与 Pingora 在维护 marker 存在时对内网来源绕过整站维护闸 生产环境变量模板:`deploy/env/api-server.env.example`。真实密钥只放服务器,不提交 Git,不写入文档示例。 -BgFilter 受限资源调度使用非模板单实例 `genarrative-bgfilter-worker.service`,固定以 `GENARRATIVE_PROCESS_ROLE=bgfilter-worker` 监听 `127.0.0.1:8083`,不挂 Nginx 或公共路由。`api-server.env` 是父侧与子 worker 的共享基础,集中保存 provider、OSS、冻结的 `GENARRATIVE_BGFILTER_WORKER_CONCURRENCY=16`、`GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS=5000`、内部 worker 地址、Token 文件和连接超时;BgFilter unit 先加载它,再加载只含 `HOST / PORT / MAX_REQUESTS`、flat 熔断阈值 / cooldown 和可选日志覆盖的 `/etc/genarrative/bgfilter-worker.env`,其中 `MAX_REQUESTS` 默认 `2048` 且只作 admission 保险丝。`N / est` 是父侧派生 `callBudgetMs`、子侧派生 attempt 与队列估时的共同输入,专属 env 不得重复覆盖;同一 provider、OSS、Token 或预算基础配置也只保留一份。外部生成 worker unit 会在共享 API env 后加载 `/etc/genarrative/external-generation-worker.env`;该文件如重复定义内部 base URL、Token / Token 文件、connect timeout、`N / est`、OSS bucket 或 endpoint,最终有效值必须与共享 API env 完全一致,否则发布失败,避免父子预算或对象存储视图漂移。外部生成 worker 可以使用同一 bucket 下权限等价或更小的独立 AK,不要求凭据文本一致。Token 文件由 Provision 以 `root:genarrative 0440` 创建或保留,env 只引用路径,不保存内部 Token 明文;env 示例和仓库不得出现真实 provider / OSS secret。 +BgFilter 受限资源调度使用非模板单实例 `genarrative-bgfilter-worker.service`,固定以 `GENARRATIVE_PROCESS_ROLE=bgfilter-worker` 监听 `127.0.0.1:8083`,不挂 Nginx 或公共路由。`api-server.env` 是父侧与子 worker 的共享基础,集中保存 provider、OSS、冻结的 `GENARRATIVE_BGFILTER_WORKER_CONCURRENCY=16`、`GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS=5000`、内部 worker 地址、Token 文件和连接超时;BgFilter unit 先加载它,再加载只含 `HOST / PORT / MAX_REQUESTS`、flat / complex 统一熔断阈值 / cooldown 和可选日志覆盖的 `/etc/genarrative/bgfilter-worker.env`,其中两种模式共享参数但状态独立,阈值默认 `3`、cooldown 默认 `120s`,`MAX_REQUESTS` 默认 `2048` 且只作 admission 保险丝。`N / est` 是父侧派生 `callBudgetMs`、子侧派生 attempt 与队列估时的共同输入,专属 env 不得重复覆盖;同一 provider、OSS、Token 或预算基础配置也只保留一份。外部生成 worker unit 会在共享 API env 后加载 `/etc/genarrative/external-generation-worker.env`;该文件如重复定义内部 base URL、Token / Token 文件、connect timeout、`N / est`、OSS bucket 或 endpoint,最终有效值必须与共享 API env 完全一致,否则发布失败,避免父子预算或对象存储视图漂移。外部生成 worker 可以使用同一 bucket 下权限等价或更小的独立 AK,不要求凭据文本一致。Token 文件由 Provision 以 `root:genarrative 0440` 创建或保留,env 只引用路径,不保存内部 Token 明文;env 示例和仓库不得出现真实 provider / OSS secret。 -生产发布必须按 `共享配置 / endpoint / Token / N / est / Q 预检 → stop 旧 BgFilter worker → 等待 systemd 排空 → start 唯一实例 → 检查 http://127.0.0.1:8083/readyz → 重启 API → 重启 external-generation worker / controller` 的顺序执行。Token 预检要求 API env 指向非空、非符号链接的普通文件,权限固定为 `root:genarrative 0440`;endpoint 预检要求父进程 `GENARRATIVE_BGFILTER_WORKER_BASE_URL`、子 worker `HOST / PORT` 和 readiness URL 指向同一个 `127.0.0.1:`;预算预检要求共享 `N / est` 均为正整数、父子有效值一致,当前模板默认和压测前冻结值为 `N=16 / est=5000ms`,后续允许按真实压测校准 `est`,不由 deploy 脚本写死;`Q` 缺省为 `2048`,显式值不得小于 `N`,历史模板默认 `128` 会在 deploy / Provision 时定向迁移为 `2048`,其它显式定制值保留。任何预检失败都发生在 `current` 切换和停止现役 worker 之前。非模板 unit、固定 loopback 端口和显式 stop/start 共同避免新旧 BgFilter worker 重叠;第二实例会因固定端口绑定失败。内部请求分别携带 `maxQueueWaitMs` 与公式化 `callBudgetMs`;worker 停机时立即让尚未取得 provider permit 的排队请求失败,只排空已经取得 permit 的调用。默认 `callBudgetMs=321s`,unit 使用 `TimeoutStopSec=900` 给最多两次 attempt、响应发送和进程收口留足余量,禁止沿用约 `90s` 的默认停止窗口。默认 API deploy 会安装、enable、启动并验活该 unit;只有明确回滚或应急排障时才使用 `--no-bgfilter-worker` 跳过,且不得让父进程偷偷恢复为直连 BgFilter。 +生产发布必须按 `共享配置 / endpoint / Token / N / est / Q 预检 → stop 旧 BgFilter worker → 等待 systemd 排空 → start 唯一实例 → 检查 http://127.0.0.1:8083/readyz → 重启 API → 重启 external-generation worker / controller` 的顺序执行。Token 预检要求 API env 指向非空、非符号链接的普通文件,权限固定为 `root:genarrative 0440`;endpoint 预检要求父进程 `GENARRATIVE_BGFILTER_WORKER_BASE_URL`、子 worker `HOST / PORT` 和 readiness URL 指向同一个 `127.0.0.1:`;预算预检要求共享 `N / est` 均为正整数、父子有效值一致,当前模板默认和压测前冻结值为 `N=16 / est=5000ms`,后续允许按真实压测校准 `est`,不由 deploy 脚本写死;`Q` 缺省为 `2048`,显式值不得小于 `N`,历史模板默认 `128` 会在 deploy / Provision 时定向迁移为 `2048`;熔断 cooldown 的历史模板默认 `300s` 同样会定向迁移为 `120s`;其它显式定制值均保留。任何预检失败都发生在 `current` 切换和停止现役 worker 之前。非模板 unit、固定 loopback 端口和显式 stop/start 共同避免新旧 BgFilter worker 重叠;第二实例会因固定端口绑定失败。内部请求分别携带 `maxQueueWaitMs` 与公式化 `callBudgetMs`;worker 停机时立即让尚未取得 provider permit 的排队请求失败,只排空已经取得 permit 的调用。默认 `callBudgetMs=321s`,unit 使用 `TimeoutStopSec=900` 给最多两次 attempt、响应发送和进程收口留足余量,禁止沿用约 `90s` 的默认停止窗口。默认 API deploy 会安装、enable、启动并验活该 unit;只有明确回滚或应急排障时才使用 `--no-bgfilter-worker` 跳过,且不得让父进程偷偷恢复为直连 BgFilter。 `api-server` 进程角色由 `GENARRATIVE_PROCESS_ROLE` 控制:`api` 只监听 HTTP,`external-generation-worker` 只消费外部生成队列,`external-generation-controller` 只管理 worker systemd 实例,`all` 仅用于本地或临时 smoke,不隐式启动 controller。外部生成策略由 `GENARRATIVE_EXTERNAL_GENERATION_MODE` 控制;生产和容器压测默认保持 `queue`,本地 `npm run dev` / `npm run dev:api-server` 默认由 dev 脚本注入 `GENARRATIVE_PROCESS_ROLE=all`,如果外部生成策略为 `queue` 会由同一进程消费队列。`inline` 只用于本地或低并发同步排查,HTTP handler 会直接复用 worker executor,完成后返回 `completed`,但不会落 `external_generation_job`,也不能通过增加 worker 进程扩吞吐。外部生成 worker 使用同一发布包和同一套 SpacetimeDB 配置,按实例数和 `GENARRATIVE_EXTERNAL_GENERATION_WORKER_CONCURRENCY` 动态扩缩;生产默认由 `genarrative-external-generation-controller.service` 读取 `get_external_generation_queue_stats_and_return`,按 `claimable_pending + running_active + expired_running` 计算目标 worker 数,并对 `genarrative-external-generation-worker@N.service` 精确执行 `systemctl start/stop`。controller 参数模板是 `deploy/env/external-generation-controller.env.example`:默认保底 `MIN_WORKERS=1`、上限 `MAX_WORKERS=8`、每 worker 目标 `TARGET_JOBS_PER_WORKER=2`、`POLL_INTERVAL_MS=10000`、连续 `SCALE_DOWN_IDLE_ROUNDS=6` 轮完全空闲才缩容;缩容每轮只停止最高编号的一个实例,且不主动停止 `@1`。worker 收到 SIGINT/SIGTERM 后会停止 claim 新任务并等待当前任务完成;若进程被硬杀、机器断电或超过 systemd `TimeoutStopSec`,未完成任务才会在 lease 过期后由其它 worker 重领。每个 worker 实例应设置唯一 `GENARRATIVE_EXTERNAL_GENERATION_WORKER_ID`,默认会用主机名和 pid 兜底;systemd 生产模板 `deploy/systemd/genarrative-external-generation-worker@.service` 会用 `%H-%i` 生成实例 ID,并把 tracking outbox 隔离到 `/var/lib/genarrative/tracking-outbox/%H-%i`。`Genarrative-Server-Provision` 会安装 worker 模板、controller unit 和两份专属 env 模板,默认 enable 首个 `genarrative-external-generation-worker@1.service` 与 `genarrative-external-generation-controller.service`;首次 API deploy 会在默认 worker pattern 下自动 `enable --now genarrative-external-generation-worker@1.service` 并等待 worker active,同时重启并验活 controller。手动兜底扩容仍可用 `systemctl start genarrative-external-generation-worker@2.service` / `@3.service`,缩容用 `systemctl stop genarrative-external-generation-worker@N.service`;controller 下轮会按队列压力修正到目标实例数。worker 专属参数模板是 `deploy/env/external-generation-worker.env.example`,密钥与 SpacetimeDB 连接仍复用 `/etc/genarrative/api-server.env`。API 发布脚本默认会重启并验活 `genarrative-external-generation-worker@*.service` 和 `genarrative-external-generation-controller.service`;若本次只发 HTTP 且不希望滚动 worker,可传 `--no-worker-services`,若不希望重启 controller 可传 `--no-worker-controller`。`GENARRATIVE_EXTERNAL_GENERATION_WORKER_POLL_INTERVAL_MS` 控制空队列轮询间隔,`GENARRATIVE_EXTERNAL_GENERATION_WORKER_LEASE_SECONDS` 控制单次 lease,worker 会约每三分之一 lease、最长 30 秒续租;该值应覆盖一次心跳网络抖动窗口,不需要大于完整外部生成链路耗时。SpacetimeDB 使用自身事务时间计算 claim/renew/complete/fail,完成和失败回写还会校验 `lease_token` 与未过期 lease,避免同一 job 被过期 worker 覆盖。首版 worker 粒度是单动作单 job,不拆阶段 job;当前外部生成动作覆盖拼图、跳一跳、拼消消、敲木鱼和图片画布编辑器生成 / 抠图入口,纯元信息保存、发布、试玩启动、运行态动作和公开读取继续 inline。图片画布 worker 成功后由后端保存 `editor_project_resource` / `editor_asset` / `editor_canvas.layers_json`,前端只轮询 job 并重新读取项目快照。当前生成业务失败只做用户重新触发,不做自动业务重试,避免 worker 退款和重试成功之间产生钱包账本漂移。 diff --git a/scripts/check-production-api-deploy.mjs b/scripts/check-production-api-deploy.mjs index 9aa7777a5..84abfaffc 100644 --- a/scripts/check-production-api-deploy.mjs +++ b/scripts/check-production-api-deploy.mjs @@ -57,6 +57,7 @@ function main() { assertDeployRejectsNonLoopbackBgFilterListener(); assertDeployRejectsMissingBgFilterWorkerCapacity(); assertDeployMigratesOldDefaultBgFilterAdmissionLimit(); + assertDeployMigratesOldDefaultBgFilterCircuitCooldown(); assertDeployRejectsInvalidBgFilterWorkerCapacity(); assertDeployRejectsInlineBgFilterInternalToken(); assertDeployRejectsEmptyBgFilterInternalToken(); @@ -395,7 +396,12 @@ function assertDeployCopiesPingoraDirectReleaseDependencies() { assertIncludes( bgfilterEnvExample, 'GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD=3', - 'BgFilter 专属 env 必须提供 flat 熔断阈值。', + 'BgFilter 专属 env 必须提供 flat / complex 统一熔断阈值。', + ); + assertIncludes( + bgfilterEnvExample, + 'GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS=120', + 'BgFilter 专属 env 必须提供 flat / complex 统一的 120 秒熔断冷却时间。', ); for (const sharedKey of [ 'GENARRATIVE_BGFILTER_WORKER_CONCURRENCY=', @@ -1292,6 +1298,44 @@ function assertDeployMigratesOldDefaultBgFilterAdmissionLimit() { } } +function assertDeployMigratesOldDefaultBgFilterCircuitCooldown() { + const fixture = prepareFixture('bgfilter-worker-migrate-old-default-circuit-cooldown'); + writeFileSync( + fixture.bgfilterWorkerEnvFile, + `${readFileSync(fixture.bgfilterWorkerEnvFile, 'utf8')}GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS=300\n`, + 'utf8', + ); + + const result = runDeploy(fixture); + if (result.status !== 0) { + failures.push(`历史默认熔断 cooldown=300 迁移到 120 时部署不应失败:${result.stderr}`); + return; + } + const migrated = readFileSync(fixture.bgfilterWorkerEnvFile, 'utf8'); + if (!/^GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS=120$/mu.test(migrated)) { + failures.push('部署必须把历史模板默认熔断 cooldown=300 定向迁移为 120。'); + } + if (/^GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS=300$/mu.test(migrated)) { + failures.push('部署完成后不得继续保留历史模板默认熔断 cooldown=300。'); + } + + const custom = prepareFixture('bgfilter-worker-preserve-custom-circuit-cooldown'); + writeFileSync( + custom.bgfilterWorkerEnvFile, + `${readFileSync(custom.bgfilterWorkerEnvFile, 'utf8')}GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS=90\n`, + 'utf8', + ); + const customResult = runDeploy(custom); + if (customResult.status !== 0) { + failures.push(`自定义熔断 cooldown=90 时部署不应失败:${customResult.stderr}`); + return; + } + const preserved = readFileSync(custom.bgfilterWorkerEnvFile, 'utf8'); + if (!/^GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS=90$/mu.test(preserved)) { + failures.push('部署只能迁移历史默认熔断 cooldown=300,必须保留其它显式自定义值。'); + } +} + function assertMissingReleaseManifestFails() { const fixture = prepareFixture('missing-release-manifest'); rmSync(path.join(fixture.sourceDir, 'release-manifest.json')); diff --git a/scripts/check-production-ops-guardrails.mjs b/scripts/check-production-ops-guardrails.mjs index 9ed781480..15705c668 100644 --- a/scripts/check-production-ops-guardrails.mjs +++ b/scripts/check-production-ops-guardrails.mjs @@ -1691,6 +1691,13 @@ const checks = [ reason: 'Server-Provision 迁移历史默认值时必须读取最后一次有效赋值,不能覆盖后写的自定义运行态值。', }, + { + file: 'scripts/jenkins-server-provision.sh', + includes: + 'ensure_env_value_migrates_old_default "${BGFILTER_WORKER_ENV_FILE}" "GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS" "300" "120"', + reason: + 'Server-Provision 必须把 BgFilter 熔断 cooldown 历史模板默认 300 定向迁移为 120,并保留其它显式定制值。', + }, { file: 'scripts/jenkins-server-provision.sh', includes: "root:genarrative:440", diff --git a/scripts/deploy/production-api-deploy.sh b/scripts/deploy/production-api-deploy.sh index ee20dfefa..f83109596 100644 --- a/scripts/deploy/production-api-deploy.sh +++ b/scripts/deploy/production-api-deploy.sh @@ -497,7 +497,7 @@ ensure_bgfilter_worker_runtime_env_defaults() { # 其它显式定制值继续保留。 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" + ensure_env_value_migrates_old_default "${bgfilter_env_file}" "GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS" "300" "120" # N 已迁入共享 API env;worker 专属文件中的旧值会与共享值形成双写风险,直接移除。 remove_env_key_if_present "${bgfilter_env_file}" "GENARRATIVE_BGFILTER_WORKER_CONCURRENCY" } diff --git a/scripts/jenkins-server-provision.sh b/scripts/jenkins-server-provision.sh index 789cbaa79..894458fd2 100755 --- a/scripts/jenkins-server-provision.sh +++ b/scripts/jenkins-server-provision.sh @@ -806,7 +806,7 @@ ensure_bgfilter_worker_runtime_env_defaults() { # 其它显式定制值继续保留。 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" + ensure_env_value_migrates_old_default "${BGFILTER_WORKER_ENV_FILE}" "GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS" "300" "120" } bgfilter_internal_token_file_is_single_segment() { diff --git a/server-rs/crates/api-server/src/bgfilter_worker.rs b/server-rs/crates/api-server/src/bgfilter_worker.rs index 503da4bbf..905c46049 100644 --- a/server-rs/crates/api-server/src/bgfilter_worker.rs +++ b/server-rs/crates/api-server/src/bgfilter_worker.rs @@ -53,6 +53,7 @@ const BGFILTER_SOURCE_URL_EXPIRE_SECONDS: u64 = 600; const BGFILTER_PROVIDER_TOKEN_HEADER: &str = "X-Genarrative-Image-Token"; static BGFILTER_FLAT_CIRCUIT: OnceLock> = OnceLock::new(); +static BGFILTER_COMPLEX_CIRCUIT: OnceLock> = OnceLock::new(); struct BgfilterMetrics { _circuit_state: ObservableGauge, @@ -72,14 +73,22 @@ fn bgfilter_metrics() -> &'static BgfilterMetrics { let meter = global::meter("genarrative-bgfilter-worker"); let circuit_state = meter .i64_observable_gauge("bgfilter_circuit_state") - .with_description("Flat BgFilter circuit state: 0 closed, 1 open") + .with_description("BgFilter circuit state by mode: 0 closed, 1 open") .with_callback(|observer| { - let open = flat_circuit_state() - .lock() - .ok() - .and_then(|circuit| circuit.open_until) - .is_some_and(|open_until| open_until > Instant::now()); - observer.observe(i64::from(open), &[KeyValue::new("mode", "flat")]); + for mode in [ + BgfilterBackgroundMode::Flat, + BgfilterBackgroundMode::Complex, + ] { + let open = circuit_state(mode) + .lock() + .ok() + .and_then(|circuit| circuit.open_until) + .is_some_and(|open_until| open_until > Instant::now()); + observer.observe( + i64::from(open), + &[KeyValue::new("mode", mode.as_str())], + ); + } }) .build(); BgfilterMetrics { @@ -144,10 +153,6 @@ impl BgfilterBackgroundMode { Self::Complex => "complex", } } - - fn uses_flat_circuit(self) -> bool { - matches!(self, Self::Flat) - } } #[derive(Clone, Debug)] @@ -966,20 +971,19 @@ async fn execute_logical_request( request: BgfilterInternalRequest, admitted_at: Instant, ) -> WorkerOutcome { - if request.background_mode.uses_flat_circuit() { - if let Some(remaining) = flat_circuit_open_remaining(&runtime.app_state) { - return WorkerOutcome { - result: Err(WorkerFailure::new( - "circuit_open", - format!( - "BgFilter flat 熔断仍有 {}ms", - remaining.as_millis().min(u128::from(u64::MAX)) - ), - true, - )), - provider_permit: None, - }; - } + if let Some(remaining) = circuit_open_remaining(&runtime.app_state, request.background_mode) { + return WorkerOutcome { + result: Err(WorkerFailure::new( + "circuit_open", + format!( + "BgFilter {} 熔断仍有 {}ms", + request.background_mode.as_str(), + remaining.as_millis().min(u128::from(u64::MAX)) + ), + true, + )), + provider_permit: None, + }; } let (queue_len_ahead, queue_depth_guard) = QueueDepthGuard::enter(runtime.queue_depth.clone()); @@ -1056,20 +1060,19 @@ async fn execute_logical_request( // 中文注释:排队期间熔断可能刚被其它请求打开。必须在取得 provider permit 后、 // 第一次真实 HTTP 前再检查一次;本调用一旦通过该检查,自己的第二次 attempt 不再重查。 - if request.background_mode.uses_flat_circuit() { - if let Some(remaining) = flat_circuit_open_remaining(&runtime.app_state) { - return WorkerOutcome { - result: Err(WorkerFailure::new( - "circuit_open", - format!( - "BgFilter flat 熔断仍有 {}ms", - remaining.as_millis().min(u128::from(u64::MAX)) - ), - true, - )), - provider_permit: None, - }; - } + if let Some(remaining) = circuit_open_remaining(&runtime.app_state, request.background_mode) { + return WorkerOutcome { + result: Err(WorkerFailure::new( + "circuit_open", + format!( + "BgFilter {} 熔断仍有 {}ms", + request.background_mode.as_str(), + remaining.as_millis().min(u128::from(u64::MAX)) + ), + true, + )), + provider_permit: None, + }; } let attempt_state = &runtime.app_state; @@ -1127,8 +1130,8 @@ async fn execute_logical_request( attempt_started, error.clone(), ); - if request.background_mode.uses_flat_circuit() && error.counts_for_circuit { - record_flat_failure(&runtime.app_state); + if error.counts_for_circuit { + record_circuit_failure(&runtime.app_state, request.background_mode); } }, ) @@ -1136,9 +1139,7 @@ async fn execute_logical_request( match attempts { SequentialAttemptOutcome::Success { value, .. } => { - if request.background_mode.uses_flat_circuit() { - record_flat_success(); - } + record_circuit_success(request.background_mode); WorkerOutcome { result: Ok(value), provider_permit: Some(provider_permit), @@ -1889,41 +1890,66 @@ struct BgfilterCircuitState { open_until: Option, } -fn flat_circuit_state() -> &'static Mutex { - BGFILTER_FLAT_CIRCUIT.get_or_init(|| Mutex::new(BgfilterCircuitState::default())) +impl BgfilterCircuitState { + fn open_remaining(&mut self, now: Instant) -> Option { + let open_until = self.open_until?; + if open_until > now { + return Some(open_until.duration_since(now)); + } + *self = Self::default(); + None + } + + fn record_success(&mut self) { + *self = Self::default(); + } + + fn record_failure(&mut self, threshold: u32, cooldown: Duration, now: Instant) { + self.consecutive_failures = self.consecutive_failures.saturating_add(1); + if self.consecutive_failures >= threshold { + self.open_until = Some(now + cooldown); + } + } } -fn flat_circuit_open_remaining(state: &AppState) -> Option { +fn circuit_state(mode: BgfilterBackgroundMode) -> &'static Mutex { + match mode { + BgfilterBackgroundMode::Flat => { + BGFILTER_FLAT_CIRCUIT.get_or_init(|| Mutex::new(BgfilterCircuitState::default())) + } + BgfilterBackgroundMode::Complex => { + BGFILTER_COMPLEX_CIRCUIT.get_or_init(|| Mutex::new(BgfilterCircuitState::default())) + } + } +} + +fn circuit_open_remaining(state: &AppState, mode: BgfilterBackgroundMode) -> Option { if state.config.editor_bgfilter_circuit_failure_threshold == 0 { return None; } - let mut circuit = flat_circuit_state().lock().ok()?; - let open_until = circuit.open_until?; - let now = Instant::now(); - if open_until > now { - return Some(open_until.duration_since(now)); - } - *circuit = BgfilterCircuitState::default(); - None + circuit_state(mode) + .lock() + .ok()? + .open_remaining(Instant::now()) } -fn record_flat_success() { - if let Ok(mut circuit) = flat_circuit_state().lock() { - *circuit = BgfilterCircuitState::default(); +fn record_circuit_success(mode: BgfilterBackgroundMode) { + if let Ok(mut circuit) = circuit_state(mode).lock() { + circuit.record_success(); } } -fn record_flat_failure(state: &AppState) { +fn record_circuit_failure(state: &AppState, mode: BgfilterBackgroundMode) { let threshold = state.config.editor_bgfilter_circuit_failure_threshold; if threshold == 0 { return; } - if let Ok(mut circuit) = flat_circuit_state().lock() { - circuit.consecutive_failures = circuit.consecutive_failures.saturating_add(1); - if circuit.consecutive_failures >= threshold { - circuit.open_until = - Some(Instant::now() + state.config.editor_bgfilter_circuit_cooldown); - } + if let Ok(mut circuit) = circuit_state(mode).lock() { + circuit.record_failure( + threshold, + state.config.editor_bgfilter_circuit_cooldown, + Instant::now(), + ); } } @@ -2862,9 +2888,34 @@ mod tests { } #[test] - fn complex_mode_never_uses_flat_circuit_and_internal_errors_do_not_retry() { - assert!(BgfilterBackgroundMode::Flat.uses_flat_circuit()); - assert!(!BgfilterBackgroundMode::Complex.uses_flat_circuit()); + fn flat_and_complex_circuits_are_independent_and_internal_errors_do_not_retry() { + assert!(!std::ptr::eq( + circuit_state(BgfilterBackgroundMode::Flat), + circuit_state(BgfilterBackgroundMode::Complex), + )); + + let now = Instant::now(); + let cooldown = Duration::from_secs(120); + let mut flat = BgfilterCircuitState::default(); + let mut complex = BgfilterCircuitState::default(); + for _ in 0..2 { + flat.record_failure(3, cooldown, now); + } + assert_eq!(flat.open_remaining(now), None); + flat.record_failure(3, cooldown, now); + assert_eq!(flat.open_remaining(now), Some(cooldown)); + assert_eq!(complex.open_remaining(now), None); + + for _ in 0..3 { + complex.record_failure(3, cooldown, now); + } + assert_eq!(complex.open_remaining(now), Some(cooldown)); + complex.record_success(); + assert_eq!(complex.open_remaining(now), None); + assert_eq!(flat.open_remaining(now), Some(cooldown)); + assert_eq!(flat.open_remaining(now + cooldown), None); + assert_eq!(flat.consecutive_failures, 0); + assert!(!ProviderAttemptError::internal("config error").should_retry()); assert_eq!(BGFILTER_PROVIDER_MAX_ATTEMPTS, 2); } @@ -3051,7 +3102,7 @@ mod tests { } #[test] - fn worker_rechecks_flat_circuit_and_signs_each_attempt_while_parent_sends_once() { + fn worker_rechecks_mode_circuit_and_signs_each_attempt_while_parent_sends_once() { let source = include_str!("bgfilter_worker.rs"); let execute = source .split_once("async fn execute_logical_request") @@ -3061,7 +3112,7 @@ mod tests { .expect("attempt budget boundary") .0; let circuit_checks = execute - .match_indices("flat_circuit_open_remaining") + .match_indices("circuit_open_remaining") .map(|(index, _)| index) .collect::>(); assert_eq!(circuit_checks.len(), 2); diff --git a/server-rs/crates/api-server/src/config.rs b/server-rs/crates/api-server/src/config.rs index 1d78364b5..77eb6a15c 100644 --- a/server-rs/crates/api-server/src/config.rs +++ b/server-rs/crates/api-server/src/config.rs @@ -22,7 +22,7 @@ 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_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS: u64 = 120; const DEFAULT_ALIYUN_MATTING_ENDPOINT: &str = "imageseg.cn-shanghai.aliyuncs.com"; const DEFAULT_ALIYUN_MATTING_REQUEST_TIMEOUT_MS: u64 = 30_000; @@ -1642,6 +1642,7 @@ mod tests { config.editor_bgfilter_circuit_cooldown.as_secs(), DEFAULT_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS ); + assert_eq!(DEFAULT_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS, 120); assert!(config.editor_bgfilter_token.is_none()); assert_eq!( config.external_generation_worker_lease.as_secs(),