From 8c387142960ca2f606049d1d849965f26b5932db Mon Sep 17 00:00:00 2001 From: chengcheng Date: Tue, 21 Jul 2026 11:29:56 +0800 Subject: [PATCH] =?UTF-8?q?fix(runner):=20=E7=A8=B3=E5=AE=9A=E5=BC=82?= =?UTF-8?q?=E6=AD=A5=E4=BB=BB=E5=8A=A1=E9=87=8D=E5=90=AF=E6=81=A2=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 避免恢复任务将已到期的 next_run_at 误插入 River scheduled 状态,并使用专用模型隔离重启集成测试。\n\n验证:Go 全量测试、govulncheck、pnpm lint/test/build/audit、Compose、镜像、迁移、流水线及 SemVer 门禁全部通过。 --- .../httpapi/core_flow_integration_test.go | 13 +++++++- apps/api/internal/runner/queue_worker.go | 8 +++-- apps/api/internal/runner/queue_worker_test.go | 33 +++++++++++++++++++ 3 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 apps/api/internal/runner/queue_worker_test.go diff --git a/apps/api/internal/httpapi/core_flow_integration_test.go b/apps/api/internal/httpapi/core_flow_integration_test.go index 351db89..a20d169 100644 --- a/apps/api/internal/httpapi/core_flow_integration_test.go +++ b/apps/api/internal/httpapi/core_flow_integration_test.go @@ -1654,6 +1654,17 @@ WHERE m.platform_id = $1::uuid t.Fatal("task progress callback outbox should receive events") } + restartModel := "worker-restart-" + suffixText + createSimulationTextPlatformModel( + t, + server.URL, + loginResponse.AccessToken, + "openai-worker-restart-"+suffixText, + "OpenAI Worker Restart", + restartModel, + 1, + nil, + ) var restartAsyncTask struct { TaskID string `json:"taskId"` Task struct { @@ -1663,7 +1674,7 @@ WHERE m.platform_id = $1::uuid } `json:"task"` } doJSONWithHeaders(t, server.URL, http.MethodPost, "/api/v1/responses", apiKeyResponse.Secret, map[string]any{ - "model": defaultTextModel, + "model": restartModel, "runMode": "simulation", "simulation": true, "simulationDurationMs": 2000, diff --git a/apps/api/internal/runner/queue_worker.go b/apps/api/internal/runner/queue_worker.go index 311821e..6809d7e 100644 --- a/apps/api/internal/runner/queue_worker.go +++ b/apps/api/internal/runner/queue_worker.go @@ -156,7 +156,7 @@ func (s *Service) recoverAsyncRiverJobs(ctx context.Context) error { return err } for _, item := range items { - result, err := s.riverClient.Insert(ctx, asyncTaskArgs{TaskID: item.ID}, asyncTaskRecoveryInsertOpts(item)) + result, err := s.riverClient.Insert(ctx, asyncTaskArgs{TaskID: item.ID}, asyncTaskRecoveryInsertOpts(item, time.Now())) if err != nil { return err } @@ -196,9 +196,11 @@ func asyncTaskInsertOpts(task store.GatewayTask) *river.InsertOpts { } } -func asyncTaskRecoveryInsertOpts(item store.AsyncTaskQueueItem) *river.InsertOpts { +func asyncTaskRecoveryInsertOpts(item store.AsyncTaskQueueItem, now time.Time) *river.InsertOpts { opts := asyncTaskInsertOpts(store.GatewayTask{ID: item.ID}) - opts.ScheduledAt = item.NextRunAt + if item.NextRunAt.After(now) { + opts.ScheduledAt = item.NextRunAt + } // A replacement process must not be blocked by a River row that the dead // process left in running state. PostgreSQL execution leases still ensure // that only one recovery job can call the upstream provider. diff --git a/apps/api/internal/runner/queue_worker_test.go b/apps/api/internal/runner/queue_worker_test.go new file mode 100644 index 0000000..64cd2ac --- /dev/null +++ b/apps/api/internal/runner/queue_worker_test.go @@ -0,0 +1,33 @@ +package runner + +import ( + "testing" + "time" + + "github.com/easyai/easyai-ai-gateway/apps/api/internal/store" +) + +func TestAsyncTaskRecoveryInsertOptsMakesDueTaskImmediatelyAvailable(t *testing.T) { + now := time.Date(2026, time.July, 21, 3, 30, 0, 0, time.UTC) + opts := asyncTaskRecoveryInsertOpts(store.AsyncTaskQueueItem{ + ID: "due-task", + NextRunAt: now.Add(-time.Second), + }, now) + + if !opts.ScheduledAt.IsZero() { + t.Fatalf("due recovery task should be immediately available, scheduled at %s", opts.ScheduledAt) + } +} + +func TestAsyncTaskRecoveryInsertOptsPreservesFutureDelay(t *testing.T) { + now := time.Date(2026, time.July, 21, 3, 30, 0, 0, time.UTC) + nextRunAt := now.Add(30 * time.Second) + opts := asyncTaskRecoveryInsertOpts(store.AsyncTaskQueueItem{ + ID: "delayed-task", + NextRunAt: nextRunAt, + }, now) + + if !opts.ScheduledAt.Equal(nextRunAt) { + t.Fatalf("future recovery task should stay delayed until %s, got %s", nextRunAt, opts.ScheduledAt) + } +}