thread-master/backend/internal/model/job/usecase/retry_test.go

89 lines
2.6 KiB
Go

package usecase
import (
"context"
"testing"
"haixun-backend/internal/library/clock"
"haixun-backend/internal/model/job/domain/entity"
"haixun-backend/internal/model/job/domain/enum"
"go.mongodb.org/mongo-driver/bson/primitive"
)
func TestRetryRun_SchedulesBackoff(t *testing.T) {
ctx := context.Background()
template := demoTemplate()
template.RetryPolicy = entity.RetryPolicy{
MaxAttempts: 3,
BackoffSeconds: []int{30, 60},
}
jobID := primitive.NewObjectID()
runs := newMemoryRunRepo(&entity.Run{
ID: jobID,
TemplateType: template.Type,
Status: enum.RunStatusFailed,
WorkerType: "go",
Attempt: 1,
MaxAttempts: 3,
Progress: entity.RunProgress{
Steps: []entity.StepProgress{{ID: "prepare", Status: enum.StepStatusFailed}},
},
})
queue := newMemoryQueueRepo()
uc := testUseCaseFull(template, runs, nil, queue)
updated, err := uc.RetryRun(ctx, jobID.Hex())
if err != nil {
t.Fatalf("RetryRun() error = %v", err)
}
if updated.Status != enum.RunStatusPending {
t.Fatalf("status = %s, want pending", updated.Status)
}
if updated.ScheduledAt == nil {
t.Fatal("expected scheduled_at for backoff retry")
}
if *updated.ScheduledAt <= clock.NowUnixNano() {
t.Fatal("scheduled_at should be in the future")
}
if len(queue.queued("go")) != 0 {
t.Fatal("backoff retry should not enqueue immediately")
}
}
func TestRetryRun_BackoffReschedule_DoesNotClobberConcurrentCancel(t *testing.T) {
ctx := context.Background()
template := demoTemplate()
template.RetryPolicy = entity.RetryPolicy{
MaxAttempts: 3,
BackoffSeconds: []int{30, 60},
}
jobID := primitive.NewObjectID()
runs := newMemoryRunRepo(&entity.Run{
ID: jobID,
TemplateType: template.Type,
Status: enum.RunStatusFailed,
WorkerType: "go",
Attempt: 1,
MaxAttempts: 3,
Progress: entity.RunProgress{
Steps: []entity.StepProgress{{ID: "prepare", Status: enum.StepStatusFailed}},
},
})
queue := newMemoryQueueRepo()
uc := testUseCaseFull(template, runs, nil, queue)
// Simulate a user cancelling the job right after RetryRun transitions it
// to "pending" but before the backoff reschedule writes ScheduledAt back.
// The reschedule write must not silently revive the cancelled job.
runs.raceAfterUpdateIfStatus = func() { runs.run.Status = enum.RunStatusCancelled }
_, err := uc.RetryRun(ctx, jobID.Hex())
if err == nil {
t.Fatal("expected error when backoff reschedule races with a concurrent cancel")
}
if runs.run.Status != enum.RunStatusCancelled {
t.Fatalf("status = %s, want cancelled (must not be revived to pending)", runs.run.Status)
}
}