perf(storage): 拦截任务二进制并本地暂存结果
原因:任务标准结果中的 Base64、Data URI 和 Buffer 会进入 PostgreSQL JSON,导致 TOAST 与备份体积快速增长。 影响:新增统一 JSON 持久化关口;upload_none 将二进制原子写入本地结果目录,数据库仅保存带 SHA-256 的有界占位符;任务详情、同步响应、异步查询和兼容协议按需校验恢复。补充 24 小时清理、容量上限、历史小批量治理命令及管理端说明。 风险:本地结果超过 TTL、丢失或损坏时分别返回明确的 410/500;空间不足时返回 503 且不重试上游。未自动执行历史治理。 验证:三种真实图片模型同步/异步与幂等重放通过;Go vet/全量测试、前端 111 测试、lint/typecheck/build、OpenAPI、迁移安全、govulncheck、依赖审计、手工发布测试及 Linux amd64 构建通过。
This commit is contained in:
@@ -630,7 +630,11 @@ candidatesLoop:
|
||||
}
|
||||
walletReservationFinalized = true
|
||||
s.logger.Warn("task succeeded but billing requires manual review", "taskID", task.ID, "error_category", "billing_calculation_failed")
|
||||
return Result{Task: review, Output: response.Result, Wire: response.Wire}, nil
|
||||
output, hydrateErr := s.HydrateTaskResult(ctx, task.ID, response.Result)
|
||||
if hydrateErr != nil {
|
||||
return Result{Task: review}, hydrateErr
|
||||
}
|
||||
return Result{Task: review, Output: output, Wire: response.Wire}, nil
|
||||
}
|
||||
finalAmountText = finalAmount.String()
|
||||
} else {
|
||||
@@ -661,6 +665,38 @@ candidatesLoop:
|
||||
ResponseDurationMS: record.ResponseDurationMS,
|
||||
})
|
||||
if finishErr != nil {
|
||||
if errors.Is(finishErr, store.ErrTaskResultBinaryNotMaterialized) {
|
||||
s.logger.Error("task result rejected by binary persistence guard",
|
||||
"taskID", task.ID,
|
||||
"error_category", "result_binary_not_materialized",
|
||||
)
|
||||
failureCtx := context.WithoutCancel(ctx)
|
||||
_ = s.store.FinishTaskAttempt(failureCtx, store.FinishTaskAttemptInput{
|
||||
AttemptID: response.AttemptID,
|
||||
Status: "failed",
|
||||
Retryable: false,
|
||||
RequestID: response.RequestID,
|
||||
ResponseStartedAt: response.ResponseStartedAt,
|
||||
ResponseFinishedAt: response.ResponseFinishedAt,
|
||||
ResponseDurationMS: response.ResponseDurationMS,
|
||||
ErrorCode: clients.ErrorCode(finishErr),
|
||||
ErrorMessage: finishErr.Error(),
|
||||
})
|
||||
failed, failErr := s.failTask(
|
||||
failureCtx,
|
||||
task.ID,
|
||||
task.ExecutionToken,
|
||||
clients.ErrorCode(finishErr),
|
||||
finishErr.Error(),
|
||||
isSimulation(task, candidate),
|
||||
finishErr,
|
||||
)
|
||||
if failErr != nil {
|
||||
return Result{}, failErr
|
||||
}
|
||||
walletReservationFinalized = true
|
||||
return Result{Task: failed, Output: failed.Result}, finishErr
|
||||
}
|
||||
if errors.Is(finishErr, store.ErrTaskExecutionLeaseLost) {
|
||||
latest, latestErr := s.store.GetTask(ctx, task.ID)
|
||||
if latestErr == nil && latest.Status == "cancelled" {
|
||||
@@ -688,7 +724,11 @@ candidatesLoop:
|
||||
}, isSimulation(task, candidate)); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
return Result{Task: finished, Output: response.Result, Wire: response.Wire}, nil
|
||||
output, hydrateErr := s.HydrateTaskResult(ctx, task.ID, response.Result)
|
||||
if hydrateErr != nil {
|
||||
return Result{Task: finished}, hydrateErr
|
||||
}
|
||||
return Result{Task: finished, Output: output, Wire: response.Wire}, nil
|
||||
}
|
||||
var submissionUnknown *upstreamSubmissionUnknownError
|
||||
if errors.As(err, &submissionUnknown) {
|
||||
@@ -811,7 +851,7 @@ candidatesLoop:
|
||||
break
|
||||
}
|
||||
decision := failoverDecisionForCandidate(runnerPolicy, candidate, candidateErr)
|
||||
if !decision.Retry && hasLoadAvoidanceFallback(candidates, index, maxPlatforms) {
|
||||
if !decision.Retry && !isResultPersistenceFailure(candidateErr) && hasLoadAvoidanceFallback(candidates, index, maxPlatforms) {
|
||||
decision = loadAvoidanceFallbackDecision(candidateErr)
|
||||
}
|
||||
s.recordAttemptTrace(ctx, task.ID, attemptNo, failoverTraceEntry(decision, candidate))
|
||||
@@ -1216,6 +1256,12 @@ func (s *Service) runCandidate(ctx context.Context, task store.GatewayTask, user
|
||||
return clients.Response{}, err
|
||||
}
|
||||
response.Result = uploadedResult
|
||||
if localBinaryResultHasPlaceholders(response.Result) {
|
||||
// A provider wire response can still contain its original Base64 body.
|
||||
// Force protocol responses to be rebuilt from the verified standard
|
||||
// result instead of bypassing local result materialization.
|
||||
response.Wire = nil
|
||||
}
|
||||
if task.Kind == "responses" {
|
||||
response.UpstreamProtocol = candidate.ResponseProtocol
|
||||
response.ParentResponseID = responseExecution.PublicPreviousResponseID
|
||||
|
||||
Reference in New Issue
Block a user