perf(acceptance): 使用多钱包身份分片模拟并发

单一验收钱包会把预留和结算串行化,掩盖双节点 Worker 的真实吞吐。验收现在幂等准备 32 个隔离身份和钱包,按请求轮询 API Key,并在 Run 配置中登记允许的 Key/User 身份对;密钥仅经 stdin 传给集群内压测进程。\n\n仍保留真实账务、候选权限、回调、重复扣费和强杀恢复校验。\n\n验证:Go 全量测试、临时 PostgreSQL 集成测试、go vet、OpenAPI 生成、迁移安全检查、bash -n、ShellCheck。
This commit is contained in:
2026-07-31 04:25:09 +08:00
parent 8ce120631f
commit 262090db7b
6 changed files with 263 additions and 42 deletions
+9 -9
View File
@@ -39,7 +39,7 @@ type options struct {
gateways []string
gatewayTLSName string
emulatorURL string
apiKey string
apiKeys []string
runID string
runToken string
geminiModel string
@@ -135,7 +135,7 @@ func parseOptions() (options, error) {
gateways: splitCSV(os.Getenv("AI_GATEWAY_ACCEPTANCE_GATEWAYS")),
gatewayTLSName: strings.TrimSpace(os.Getenv("AI_GATEWAY_ACCEPTANCE_GATEWAY_TLS_SERVER_NAME")),
emulatorURL: strings.TrimRight(strings.TrimSpace(os.Getenv("AI_GATEWAY_ACCEPTANCE_EMULATOR_URL")), "/"),
apiKey: strings.TrimSpace(os.Getenv("AI_GATEWAY_ACCEPTANCE_API_KEY")),
apiKeys: splitCSV(env("AI_GATEWAY_ACCEPTANCE_API_KEYS", os.Getenv("AI_GATEWAY_ACCEPTANCE_API_KEY"))),
runID: strings.TrimSpace(os.Getenv("AI_GATEWAY_ACCEPTANCE_RUN_ID")),
runToken: strings.TrimSpace(os.Getenv("AI_GATEWAY_ACCEPTANCE_RUN_TOKEN")),
geminiModel: strings.TrimSpace(os.Getenv("AI_GATEWAY_ACCEPTANCE_GEMINI_MODEL")),
@@ -158,7 +158,7 @@ func parseOptions() (options, error) {
(strings.ContainsAny(opts.gatewayTLSName, "/:@") || !strings.Contains(opts.gatewayTLSName, ".")) {
return options{}, errors.New("AI_GATEWAY_ACCEPTANCE_GATEWAY_TLS_SERVER_NAME must be a DNS hostname")
}
if opts.apiKey == "" || opts.runID == "" || opts.runToken == "" {
if len(opts.apiKeys) == 0 || opts.runID == "" || opts.runToken == "" {
return options{}, errors.New("acceptance API key, Run ID, and Run Token environment variables are required")
}
if opts.geminiModel == "" || opts.videoModel == "" {
@@ -289,7 +289,7 @@ func runGemini(
endpoint := opts.gateways[index%len(opts.gateways)] + "/v1beta/models/" + url.PathEscape(opts.geminiModel) + ":generateContent"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(requestBody))
if err == nil {
opts.setHeaders(req, realUpstream)
opts.setHeaders(req, index, realUpstream)
req.Header.Set("Content-Type", "application/json")
var response *http.Response
response, err = client.Do(req)
@@ -414,7 +414,7 @@ func runVideo(
endpoint := opts.gateways[index%len(opts.gateways)] + "/api/v1/videos/generations"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
if err == nil {
opts.setHeaders(req, realUpstream)
opts.setHeaders(req, index, realUpstream)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Async", "true")
var response *http.Response
@@ -519,7 +519,7 @@ func pollVideoTask(ctx context.Context, client *http.Client, opts options, taskI
if err != nil {
return err
}
opts.setHeaders(req, realUpstream)
opts.setHeaders(req, index, realUpstream)
response, err := client.Do(req)
if err == nil {
if response.StatusCode != http.StatusOK {
@@ -599,11 +599,11 @@ func validateVideoAsset(ctx context.Context, client *http.Client, mediaURL strin
return nil
}
func (o options) setHeaders(request *http.Request, realUpstream bool) {
func (o options) setHeaders(request *http.Request, requestIndex int, realUpstream bool) {
if o.gatewayTLSName != "" {
request.Host = o.gatewayTLSName
}
request.Header.Set("Authorization", "Bearer "+o.apiKey)
request.Header.Set("Authorization", "Bearer "+o.apiKeys[requestIndex%len(o.apiKeys)])
request.Header.Set(runHeader, o.runID)
request.Header.Set(tokenHeader, o.runToken)
if realUpstream {
@@ -799,7 +799,7 @@ func paddedPNG(size int) []byte {
}
func redactError(message string, opts options) string {
for _, secret := range []string{opts.apiKey, opts.runToken} {
for _, secret := range append(append([]string(nil), opts.apiKeys...), opts.runToken) {
if secret != "" {
message = strings.ReplaceAll(message, secret, "[REDACTED]")
}