mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-21 14:19:18 +08:00
修复 PR #3768 批量图像 MVP 合并后审计报告中的全部问题: 结算与计费(高危): - 所有 SETTLEMENT_* 失败(超冻结/计数非法/manifest 冲突/定价缺失/扣费失败) 统一计入 retry_count 并在耗尽时释放冻结转 failed,消灭 settling 无限 requeue 导致的冻结余额永久锁死 - 耗尽出口的释放指纹统一为 RequestHash,与 processor/Cancel/recovery 一致; release 遇同 request id 指纹冲突视为幂等成功,治愈历史毒消息 - 管理端校验 hold_multiplier >= discount_multiplier,定价快照对存量脏数据钳制 - 释放前校验 per-job hold claim(dedup+归档表),杜绝幻影释放 索引对账(高危): - provider 输出与提交 custom_id 集对账:未知条目丢弃并记事件, 漏项补 PROVIDER_RESULT_MISSING 失败行,保证 success+fail == item_count 提交与恢复(高危): - 提交前转 uploading 并在 provider.Submit 期间心跳刷新 updated_at; 恢复扫描改为原子复核(FailStaleUnsubmittedBatchImageJob), 消灭慢提交被误杀退款而上游任务照常计费的孤儿场景 - 上游任务创建成功但本地状态推进失败时,尽力取消上游并清理输入 - recovery 释放失败时入队交由 worker releaseTerminalHold 兜底重试 队列与并发(中危): - Enqueue(SetNX+LPush)与 Reserve(BRPop+ZAdd)均改为 Lua 原子脚本, 消灭崩溃窗口导致 job 脱离队列、被 7 天 inflight 键锁死 - 锁冲突按 LockConflictDelay 重新入队(原直接丢弃需等 10 分钟 stale 恢复) - 处理期间心跳:active zset 续期(ZAddXX 防幽灵成员)+ 锁 TTL 续期 - ReplaceBatchImageItemsForJob 增加 indexing 状态守卫,防掉队 worker 重写账目 存量回归(中危): - image-only 定价条目(仅图片价无 token 价)恢复 token 计费 fail-closed, 不再按 $0 计费;图片计费路径不受影响 - 鉴权余额门槛恢复 balance <= 0 语义,MinimumBalanceReserve 不再作硬 403 加固: - ZIP max_items 钳制到管理员上限;Submit 补齐 Platform==Gemini 校验; gemini downloadUri 跟随前做 host 白名单校验 - 批量客户端改用共享 httpclient(拨号/TLS/响应头超时有界) - 审计点名的忽略错误(MarkDownloaded/SettlementFailed/AppendEvent 等)改为记日志
144 lines
3.2 KiB
Go
144 lines
3.2 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
|
)
|
|
|
|
type BatchImageWorkerRuntime struct {
|
|
worker *BatchImageWorker
|
|
billingRecovery *BatchImageBillingRecoveryService
|
|
cfg *config.Config
|
|
|
|
mu sync.Mutex
|
|
cancel context.CancelFunc
|
|
done chan struct{}
|
|
}
|
|
|
|
func NewBatchImageWorkerRuntime(worker *BatchImageWorker, cfg *config.Config) *BatchImageWorkerRuntime {
|
|
return &BatchImageWorkerRuntime{worker: worker, cfg: cfg}
|
|
}
|
|
|
|
func ProvideBatchImageWorkerRuntime(
|
|
repo BatchImageRepository,
|
|
accountRepo AccountRepository,
|
|
queue BatchImageQueue,
|
|
billingRepo UsageBillingRepository,
|
|
usageLogRepo UsageLogRepository,
|
|
pricing *BatchImageModelPricingResolver,
|
|
authCache APIKeyAuthCacheInvalidator,
|
|
cfg *config.Config,
|
|
) *BatchImageWorkerRuntime {
|
|
processor := &BatchImagePipelineProcessor{
|
|
ProviderProcessor: &BatchImageProviderProcessor{
|
|
Repo: repo,
|
|
ProviderRegistry: NewBatchImageProviderRegistryFromConfig(cfg),
|
|
AccountResolver: &BatchImageAccountRepositoryResolver{Repo: accountRepo},
|
|
BillingRepo: billingRepo,
|
|
AuthCache: authCache,
|
|
},
|
|
SettlementService: &BatchImageSettlementService{
|
|
Repo: repo,
|
|
BillingRepo: billingRepo,
|
|
UsageLogRepo: usageLogRepo,
|
|
Pricing: pricing,
|
|
AuthCache: authCache,
|
|
Config: cfg,
|
|
},
|
|
}
|
|
runtime := NewBatchImageWorkerRuntime(NewBatchImageWorker(queue, processor, NewBatchImageWorkerOptionsFromConfig(cfg)), cfg)
|
|
runtime.billingRecovery = &BatchImageBillingRecoveryService{
|
|
Repo: repo,
|
|
Billing: billingRepo,
|
|
AuthCache: authCache,
|
|
Queue: queue,
|
|
StaleAfter: NewBatchImageWorkerOptionsFromConfig(cfg).StaleActiveAfter,
|
|
Limit: NewBatchImageWorkerOptionsFromConfig(cfg).RecoverLimit,
|
|
}
|
|
runtime.Start()
|
|
return runtime
|
|
}
|
|
|
|
func (r *BatchImageWorkerRuntime) Start() {
|
|
if r == nil || r.worker == nil || r.cfg == nil || !r.cfg.BatchImage.QueueEnabled {
|
|
return
|
|
}
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if r.cancel != nil {
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
done := make(chan struct{})
|
|
r.cancel = cancel
|
|
r.done = done
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(4)
|
|
go func() {
|
|
defer wg.Done()
|
|
r.worker.Run(ctx)
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
r.worker.RunDelayedMover(ctx)
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
r.worker.RunStaleActiveRecovery(ctx)
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
r.runBillingRecovery(ctx)
|
|
}()
|
|
go func() {
|
|
wg.Wait()
|
|
close(done)
|
|
}()
|
|
}
|
|
|
|
func (r *BatchImageWorkerRuntime) runBillingRecovery(ctx context.Context) {
|
|
if r == nil || r.worker == nil || r.billingRecovery == nil {
|
|
return
|
|
}
|
|
interval := r.worker.opts.RecoveryInterval
|
|
for {
|
|
if err := ctx.Err(); err != nil {
|
|
return
|
|
}
|
|
_, _ = r.billingRecovery.ReleaseStaleUnsubmittedOnce(ctx)
|
|
sleepOrDone(ctx, interval)
|
|
}
|
|
}
|
|
|
|
func (r *BatchImageWorkerRuntime) Stop() {
|
|
if r == nil {
|
|
return
|
|
}
|
|
r.mu.Lock()
|
|
cancel := r.cancel
|
|
done := r.done
|
|
r.cancel = nil
|
|
r.done = nil
|
|
r.mu.Unlock()
|
|
|
|
if cancel != nil {
|
|
cancel()
|
|
}
|
|
if done != nil {
|
|
<-done
|
|
}
|
|
}
|
|
|
|
func (r *BatchImageWorkerRuntime) Running() bool {
|
|
if r == nil {
|
|
return false
|
|
}
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
return r.cancel != nil
|
|
}
|