fix(billing): 封住发布前计费竞态
ci / verify (pull_request) Failing after 8s
ci / verify (pull_request) Failing after 8s
阻止上游提交状态不明的任务被租约接管后重复执行,并为人工复核保留可操作的结算记录。 将生产提交绑定到当前估价签名,统一复用预处理快照,并补强规则形状、定点溢出与历史规则兼容校验。 已通过 PostgreSQL 16 集成测试、Go 全量测试与静态检查、前端测试与构建、OpenAPI、依赖审计、镜像、迁移、流水线和 SemVer 门禁。
This commit is contained in:
@@ -302,6 +302,7 @@ func (s *Service) executeWithToken(ctx context.Context, task store.GatewayTask,
|
||||
}
|
||||
}
|
||||
pricingByCandidate := map[string]resolvedPricing{}
|
||||
preprocessingByCandidate := map[string]parameterPreprocessResult{}
|
||||
reservationBillings := []any(nil)
|
||||
reservationPricingSnapshot := map[string]any(nil)
|
||||
if task.RunMode == "production" {
|
||||
@@ -316,9 +317,17 @@ func (s *Service) executeWithToken(ctx context.Context, task store.GatewayTask,
|
||||
}
|
||||
estimates := make([]candidateEstimate, 0, len(candidates))
|
||||
var pricingErr error
|
||||
var preprocessingErr error
|
||||
var preprocessingCandidate store.RuntimeModelCandidate
|
||||
for _, candidate := range candidates {
|
||||
pricingBody := preprocessRequest(task.Kind, cloneMap(body), candidate)
|
||||
estimate, estimateErr := s.estimateCandidateV2(ctx, user, task.Kind, pricingBody, candidate)
|
||||
preprocessing := s.preprocessRequestWithScripts(ctx, task.Kind, body, candidate)
|
||||
preprocessingByCandidate[pricingCandidateKey(candidate)] = preprocessing
|
||||
if preprocessing.Err != nil {
|
||||
preprocessingErr = parameterPreprocessClientError(preprocessing.Err)
|
||||
preprocessingCandidate = candidate
|
||||
break
|
||||
}
|
||||
estimate, estimateErr := s.estimateCandidateV2(ctx, user, task.Kind, preprocessing.Body, candidate)
|
||||
if estimateErr != nil {
|
||||
pricingErr = estimateErr
|
||||
if billingMode == "enforce" {
|
||||
@@ -329,8 +338,23 @@ func (s *Service) executeWithToken(ctx context.Context, task store.GatewayTask,
|
||||
estimates = append(estimates, estimate)
|
||||
pricingByCandidate[pricingCandidateKey(candidate)] = estimate.Pricing
|
||||
}
|
||||
if preprocessingErr != nil {
|
||||
preprocessing := preprocessingByCandidate[pricingCandidateKey(preprocessingCandidate)]
|
||||
s.recordFailedAttempt(ctx, failedAttemptRecord{
|
||||
Task: task, Body: preprocessing.Body, Candidate: &preprocessingCandidate,
|
||||
AttemptNo: task.AttemptCount + 1, Code: clients.ErrorCode(preprocessingErr), Cause: preprocessingErr,
|
||||
Simulated: false, Scope: "parameter_preprocessing", Reason: "parameter_preprocessing_failed",
|
||||
ExtraMetrics: []map[string]any{parameterPreprocessingMetrics(preprocessing.Log)}, Preprocessing: &preprocessing.Log,
|
||||
ModelType: preprocessingCandidate.ModelType,
|
||||
})
|
||||
failed, finishErr := s.failTask(ctx, task.ID, task.ExecutionToken, clients.ErrorCode(preprocessingErr), preprocessingErr.Error(), false, preprocessingErr, parameterPreprocessingMetrics(preprocessing.Log))
|
||||
if finishErr != nil {
|
||||
return Result{}, finishErr
|
||||
}
|
||||
return Result{Task: failed, Output: failed.Result}, preprocessingErr
|
||||
}
|
||||
if billingMode == "observe" {
|
||||
legacyItems, legacyAmount := s.maximumLegacyCandidateEstimate(ctx, user, task.Kind, body, candidates)
|
||||
legacyItems, legacyAmount := s.maximumLegacyCandidateEstimate(ctx, user, task.Kind, body, candidates, preprocessingByCandidate)
|
||||
reservationBillings = legacyItems
|
||||
candidateSnapshots := make([]any, 0, len(estimates))
|
||||
for _, estimate := range estimates {
|
||||
@@ -396,7 +420,10 @@ func (s *Service) executeWithToken(ctx context.Context, task store.GatewayTask,
|
||||
}
|
||||
}()
|
||||
if len(candidates) > 0 {
|
||||
preprocessing := s.preprocessRequestWithScripts(ctx, task.Kind, body, candidates[0])
|
||||
preprocessing, ok := preprocessingByCandidate[pricingCandidateKey(candidates[0])]
|
||||
if !ok {
|
||||
preprocessing = s.preprocessRequestWithScripts(ctx, task.Kind, body, candidates[0])
|
||||
}
|
||||
firstCandidateBody = preprocessing.Body
|
||||
firstPreprocessing = preprocessing.Log
|
||||
normalizedModelType = candidates[0].ModelType
|
||||
@@ -472,7 +499,10 @@ candidatesLoop:
|
||||
var candidateErr error
|
||||
for clientAttempt := 1; clientAttempt <= clientAttempts; clientAttempt++ {
|
||||
nextAttemptNo := attemptNo + 1
|
||||
preprocessing := s.preprocessRequestWithScripts(ctx, task.Kind, body, candidate)
|
||||
preprocessing, ok := preprocessingByCandidate[pricingCandidateKey(candidate)]
|
||||
if !ok {
|
||||
preprocessing = s.preprocessRequestWithScripts(ctx, task.Kind, body, candidate)
|
||||
}
|
||||
preprocessingLog := preprocessing.Log
|
||||
lastPreprocessing = &preprocessingLog
|
||||
if preprocessing.Err != nil {
|
||||
@@ -768,11 +798,14 @@ func normalizedBillingEngineMode(value string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) maximumLegacyCandidateEstimate(ctx context.Context, user *auth.User, kind string, body map[string]any, candidates []store.RuntimeModelCandidate) ([]any, fixedAmount) {
|
||||
func (s *Service) maximumLegacyCandidateEstimate(ctx context.Context, user *auth.User, kind string, body map[string]any, candidates []store.RuntimeModelCandidate, preprocessingByCandidate map[string]parameterPreprocessResult) ([]any, fixedAmount) {
|
||||
var maximumItems []any
|
||||
maximumAmount := fixedAmount(0)
|
||||
for index, candidate := range candidates {
|
||||
candidateBody := preprocessRequest(kind, cloneMap(body), candidate)
|
||||
if preprocessing, ok := preprocessingByCandidate[pricingCandidateKey(candidate)]; ok && preprocessing.Err == nil {
|
||||
candidateBody = preprocessing.Body
|
||||
}
|
||||
items := s.estimatedBillings(ctx, user, kind, candidateBody, candidate)
|
||||
amount := billingItemsFixedTotal(items)
|
||||
if index == 0 || amount > maximumAmount {
|
||||
@@ -792,7 +825,11 @@ func billingItemsFixedTotal(items []any) fixedAmount {
|
||||
}
|
||||
amount, err := fixedAmountFromAny(line["amount"])
|
||||
if err == nil && amount > 0 {
|
||||
total = total.Add(amount)
|
||||
next, addErr := addFixedAmounts(total, amount)
|
||||
if addErr != nil {
|
||||
return maxFixedAmount
|
||||
}
|
||||
total = next
|
||||
}
|
||||
}
|
||||
return total
|
||||
@@ -925,6 +962,11 @@ func (s *Service) runCandidate(ctx context.Context, task store.GatewayTask, user
|
||||
PreviousResponseTurns: responseExecution.PreviousTurns,
|
||||
})
|
||||
callFinishedAt := time.Now()
|
||||
if err == nil {
|
||||
if markErr := s.store.SetAttemptUpstreamSubmissionStatus(context.WithoutCancel(ctx), attemptID, "response_received"); markErr != nil {
|
||||
return clients.Response{}, &upstreamSubmissionUnknownError{AttemptID: attemptID, Cause: markErr}
|
||||
}
|
||||
}
|
||||
if response.ResponseStartedAt.IsZero() {
|
||||
response.ResponseStartedAt = callStartedAt
|
||||
}
|
||||
@@ -939,7 +981,9 @@ func (s *Service) runCandidate(ctx context.Context, task store.GatewayTask, user
|
||||
}
|
||||
if err != nil {
|
||||
if clients.ErrorResponseMetadata(err).StatusCode > 0 {
|
||||
_ = s.store.SetAttemptUpstreamSubmissionStatus(context.WithoutCancel(ctx), attemptID, "response_received")
|
||||
if markErr := s.store.SetAttemptUpstreamSubmissionStatus(context.WithoutCancel(ctx), attemptID, "response_received"); markErr != nil {
|
||||
return clients.Response{}, &upstreamSubmissionUnknownError{AttemptID: attemptID, Cause: markErr}
|
||||
}
|
||||
}
|
||||
retryable := clients.IsRetryable(err)
|
||||
requestID, metrics, responseStartedAt, responseFinishedAt, responseDurationMS := failureMetrics(err, simulated)
|
||||
|
||||
Reference in New Issue
Block a user