fix: 对齐规格解析跨端超时预算 (#257)

This commit is contained in:
chengma
2026-08-18 08:56:56 +08:00
parent 29cf66ce96
commit 6345b00106
7 changed files with 97 additions and 4 deletions
+13 -1
View File
@@ -26,6 +26,7 @@ const (
MaxPurchaseSpecResolutionBodyBytes = 64 << 10
PurchaseSpecRulesVersion = "purchase_rules_v1"
purchaseSpecSchemaVersion = 1
purchaseSpecResolutionAITimeout = 30 * time.Second
)
var (
@@ -143,7 +144,7 @@ func resolvePurchaseSpecWithSnapshotLoader(ctx context.Context, db *sql.DB, secr
// 可以安全接管;最终 UPDATE 仍只允许 pending 完成一次。
decision, terminal := matchPurchaseSpecByRule(req.TargetSize, req.Candidates)
if !terminal {
decision = matchPurchaseSpecWithAI(ctx, db, secrets, policy, req, loadSnapshot)
decision = matchPurchaseSpecWithRuntimeAI(ctx, db, secrets, policy, req, loadSnapshot)
}
if purchaseSpecDiagnosticSource(db, req.PddGoodsID) == "shopee_backfill" {
decision.Reason = truncateRunes(decision.Reason+";诊断来源:shopee_backfill", 500)
@@ -151,6 +152,17 @@ func resolvePurchaseSpecWithSnapshotLoader(ctx context.Context, db *sql.DB, secr
return completePurchaseSpecResolution(db, idempotencyKey, requestHash, resolution, req.Candidates, decision)
}
// matchPurchaseSpecWithRuntimeAI 给采购运行时 AI 单独设置 30 秒上限。
// Client 会等待最多 60 秒,因此 Admin 有足够时间保存安全结论并返回;如果请求更早取消,
// 子上下文也会立即取消,不会让模型调用脱离原 HTTP 请求继续运行。
func matchPurchaseSpecWithRuntimeAI(ctx context.Context, db *sql.DB, secrets AISecretStore,
policy AIEndpointPolicy, req PurchaseSpecResolutionRequest,
loadSnapshot aiSnapshotLoader) purchaseSpecDecision {
aiContext, cancel := context.WithTimeout(ctx, purchaseSpecResolutionAITimeout)
defer cancel()
return matchPurchaseSpecWithAI(aiContext, db, secrets, policy, req, loadSnapshot)
}
func parsePurchaseSpecResolutionRequest(taskID, idempotencyKey string, rawBody []byte) (PurchaseSpecResolutionRequest, error) {
var req PurchaseSpecResolutionRequest
if len(rawBody) == 0 || len(rawBody) > MaxPurchaseSpecResolutionBodyBytes || !utf8.Valid(rawBody) {
@@ -99,6 +99,23 @@ type stubAIModelClient struct {
mu sync.Mutex
}
type runtimeDeadlineAIClient struct {
deadline time.Time
deadlineOK bool
contextErr error
}
func (client *runtimeDeadlineAIClient) Match(ctx context.Context, _ model.AIProviderConfig, _ string,
_ AIModelMatchRequest) (AIModelMatchResponse, error) {
client.deadline, client.deadlineOK = ctx.Deadline()
client.contextErr = ctx.Err()
if client.contextErr != nil {
return AIModelMatchResponse{}, client.contextErr
}
return AIModelMatchResponse{Conclusion: "uncertain", ConfidenceBPS: 7000,
Reason: "测试信息不足"}, nil
}
func (client *stubAIModelClient) Match(ctx context.Context, _ model.AIProviderConfig, _ string,
request AIModelMatchRequest) (AIModelMatchResponse, error) {
client.calls.Add(1)
@@ -115,6 +132,46 @@ func (client *stubAIModelClient) Match(ctx context.Context, _ model.AIProviderCo
return client.response, client.err
}
func TestMatchPurchaseSpecWithRuntimeAI_限制30秒并继承更早取消(t *testing.T) {
req, _, _ := validRuntimeSpecRequest(t, "cg257-timeout", "L码", "M码", "XL码")
client := &runtimeDeadlineAIClient{}
loader := func(context.Context, *sql.DB, AISecretStore, AIEndpointPolicy) (AIMatchSnapshot, error) {
return AIMatchSnapshot{Provider: model.AIProviderConfig{
ProviderID: "provider-timeout", Model: "model-timeout", TimeoutSeconds: 120,
ConfidenceThresholdBPS: 9000,
}, ConfigFingerprint: "fingerprint", Secret: "not-a-real-secret", Client: client}, nil
}
startedAt := time.Now()
decision := matchPurchaseSpecWithRuntimeAI(context.Background(), nil, nil,
AIEndpointPolicy{}, req, loader)
if decision.Outcome != model.PurchaseSpecResolutionUncertain {
t.Fatalf("outcome=%s reason=%s", decision.Outcome, decision.Reason)
}
if !client.deadlineOK {
t.Fatal("运行时 AI 上下文缺少截止时间")
}
budget := client.deadline.Sub(startedAt)
if budget < 29*time.Second || budget > purchaseSpecResolutionAITimeout {
t.Fatalf("运行时 AI 时间预算=%s,期望不超过 %s", budget, purchaseSpecResolutionAITimeout)
}
cancelledContext, cancel := context.WithCancel(context.Background())
cancel()
cancelledClient := &runtimeDeadlineAIClient{}
cancelledLoader := func(context.Context, *sql.DB, AISecretStore, AIEndpointPolicy) (AIMatchSnapshot, error) {
return AIMatchSnapshot{Provider: model.AIProviderConfig{
ProviderID: "provider-timeout", Model: "model-timeout", TimeoutSeconds: 120,
ConfidenceThresholdBPS: 9000,
}, ConfigFingerprint: "fingerprint", Secret: "not-a-real-secret", Client: cancelledClient}, nil
}
decision = matchPurchaseSpecWithRuntimeAI(cancelledContext, nil, nil,
AIEndpointPolicy{}, req, cancelledLoader)
if decision.Outcome != model.PurchaseSpecResolutionFailed ||
!errors.Is(cancelledClient.contextErr, context.Canceled) {
t.Fatalf("更早取消未继承: outcome=%s context_err=%v", decision.Outcome, cancelledClient.contextErr)
}
}
func TestMatchPurchaseSpecWithAI_只接受请求候选并执行安全门禁(t *testing.T) {
tests := []struct {
name string