| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103 |
- package service
- import (
- "context"
- "errors"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
- "go.uber.org/zap"
- )
- const (
- defaultBatchImageBillingRecoveryStaleAfter = 10 * time.Minute
- defaultBatchImageBillingRecoveryLimit = 100
- )
- type BatchImageBillingRecoveryService struct {
- Repo BatchImageRepository
- Billing UsageBillingRepository
- AuthCache APIKeyAuthCacheInvalidator
- Queue BatchImageQueue
- StaleAfter time.Duration
- Limit int
- }
- func (s *BatchImageBillingRecoveryService) ReleaseStaleUnsubmittedOnce(ctx context.Context) (int, error) {
- if s == nil || s.Repo == nil || s.Billing == nil {
- return 0, nil
- }
- staleAfter := s.StaleAfter
- if staleAfter <= 0 {
- staleAfter = defaultBatchImageBillingRecoveryStaleAfter
- }
- limit := s.Limit
- if limit <= 0 {
- limit = defaultBatchImageBillingRecoveryLimit
- }
- cutoff := time.Now().Add(-staleAfter)
- jobs, err := s.Repo.ListStaleUnsubmittedBatchImageJobs(ctx, cutoff, limit)
- if err != nil {
- return 0, err
- }
- released := 0
- var lastErr error
- for _, job := range jobs {
- if job == nil {
- continue
- }
- if err := ctx.Err(); err != nil {
- return released, err
- }
- msg := "batch image submission did not reach provider before recovery cutoff"
- // 原子转 failed 并复核 stale 条件:List 与转态之间 job 可能已被慢提交
- // 心跳续期或提交成功(provider_job_name 已写入),此时绝不能退款,
- // 否则上游任务照常产生成本而用户已拿回冻结余额。
- applied, err := s.Repo.FailStaleUnsubmittedBatchImageJob(ctx, job.BatchID, cutoff, "SUBMIT_STALE_BEFORE_PROVIDER", msg)
- if err != nil {
- // applied=true 时 UPDATE 已提交(仅审计事件写入失败):必须继续释放,
- // 否则 job 已转 failed、不再出现在 stale 列表,冻结余额会永久泄漏。
- if !applied {
- lastErr = err
- continue
- }
- logger.L().Warn("batch_image.recovery_fail_event_append_failed",
- zap.String("batch_id", job.BatchID),
- zap.Error(err),
- )
- }
- if !applied {
- continue
- }
- job.Status = BatchImageJobStatusFailed
- if err := releaseBatchImageBalanceHold(ctx, s.Billing, job, batchImageDerefString(job.RequestHash)); err != nil {
- // job 已转 failed、不会再进入 stale 列表:必须给释放失败留下
- // 自动重试路径(入队后由 worker 的 releaseTerminalHold 兜底),
- // 否则冻结余额永久泄漏。
- logger.L().Warn("batch_image.recovery_release_failed",
- zap.String("batch_id", job.BatchID),
- zap.Error(err),
- )
- s.enqueueReleaseRetry(ctx, job.BatchID)
- lastErr = err
- continue
- }
- if s.AuthCache != nil && job.UserID > 0 {
- s.AuthCache.InvalidateAuthCacheByUserID(ctx, job.UserID)
- }
- released++
- }
- return released, lastErr
- }
- func (s *BatchImageBillingRecoveryService) enqueueReleaseRetry(ctx context.Context, batchID string) {
- if s == nil || s.Queue == nil {
- return
- }
- if err := s.Queue.Enqueue(ctx, batchID); err != nil && !errors.Is(err, ErrBatchImageAlreadyQueued) {
- logger.L().Warn("batch_image.recovery_release_retry_enqueue_failed",
- zap.String("batch_id", batchID),
- zap.Error(err),
- )
- }
- }
|