2026-06-26 08:37:04 +00:00
|
|
|
package usecase
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"haixun-backend/internal/library/clock"
|
|
|
|
|
"haixun-backend/internal/model/job/domain/entity"
|
|
|
|
|
"haixun-backend/internal/model/job/domain/enum"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestRunMaintenance_EnqueuePendingRetry(t *testing.T) {
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
template := demoTemplate()
|
|
|
|
|
queue := newMemoryQueueRepo()
|
|
|
|
|
runs := newMemoryRunRepo(nil)
|
|
|
|
|
scheduled := clock.Now().Add(-time.Second).UnixNano()
|
|
|
|
|
runs.run = &entity.Run{
|
|
|
|
|
TemplateType: template.Type,
|
|
|
|
|
Status: enum.RunStatusPending,
|
|
|
|
|
WorkerType: "go",
|
|
|
|
|
ScheduledAt: &scheduled,
|
|
|
|
|
}
|
|
|
|
|
uc := testUseCaseFull(template, runs, nil, queue)
|
|
|
|
|
|
|
|
|
|
result, err := uc.RunMaintenance(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("RunMaintenance() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if result.EnqueuedPending != 1 {
|
|
|
|
|
t.Fatalf("EnqueuedPending = %d, want 1", result.EnqueuedPending)
|
|
|
|
|
}
|
|
|
|
|
if runs.run.Status != enum.RunStatusQueued {
|
|
|
|
|
t.Fatalf("status = %s, want queued", runs.run.Status)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRunMaintenance_ReapCancelGrace(t *testing.T) {
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
template := demoTemplate()
|
|
|
|
|
template.CancelPolicy.GraceSeconds = 1
|
|
|
|
|
queue := newMemoryQueueRepo()
|
|
|
|
|
runs := newMemoryRunRepo(nil)
|
|
|
|
|
cancelAt := clock.Now().Add(-2 * time.Second).UnixNano()
|
|
|
|
|
runs.run = &entity.Run{
|
|
|
|
|
TemplateType: template.Type,
|
|
|
|
|
Status: enum.RunStatusCancelRequested,
|
|
|
|
|
CancelRequestedAt: &cancelAt,
|
|
|
|
|
DedupeKey: "dedupe-1",
|
|
|
|
|
}
|
|
|
|
|
uc := testUseCaseFull(template, runs, nil, queue)
|
|
|
|
|
|
|
|
|
|
result, err := uc.RunMaintenance(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("RunMaintenance() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if result.ReapedCancelGrace != 1 {
|
|
|
|
|
t.Fatalf("ReapedCancelGrace = %d, want 1", result.ReapedCancelGrace)
|
|
|
|
|
}
|
|
|
|
|
if runs.run.Status != enum.RunStatusCancelled {
|
|
|
|
|
t.Fatalf("status = %s, want cancelled", runs.run.Status)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 06:13:14 +00:00
|
|
|
func TestRunMaintenance_ReapCancelGrace_SkipsIfAlreadyResolved(t *testing.T) {
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
template := demoTemplate()
|
|
|
|
|
template.CancelPolicy.GraceSeconds = 1
|
|
|
|
|
queue := newMemoryQueueRepo()
|
|
|
|
|
runs := newMemoryRunRepo(nil)
|
|
|
|
|
cancelAt := clock.Now().Add(-2 * time.Second).UnixNano()
|
|
|
|
|
runs.run = &entity.Run{
|
|
|
|
|
TemplateType: template.Type,
|
|
|
|
|
Status: enum.RunStatusCancelRequested,
|
|
|
|
|
CancelRequestedAt: &cancelAt,
|
|
|
|
|
DedupeKey: "dedupe-1",
|
|
|
|
|
}
|
|
|
|
|
uc := testUseCaseFull(template, runs, nil, queue)
|
|
|
|
|
|
|
|
|
|
// Simulate the worker acknowledging cancel (or completing/failing the job)
|
|
|
|
|
// concurrently, right after the reaper reads it but before it writes back.
|
|
|
|
|
runs.raceAfterFind = func() { runs.run.Status = enum.RunStatusSucceeded }
|
|
|
|
|
|
|
|
|
|
result, err := uc.RunMaintenance(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("RunMaintenance() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if result.ReapedCancelGrace != 0 {
|
|
|
|
|
t.Fatalf("ReapedCancelGrace = %d, want 0 (must not clobber concurrently resolved status)", result.ReapedCancelGrace)
|
|
|
|
|
}
|
|
|
|
|
if runs.run.Status != enum.RunStatusSucceeded {
|
|
|
|
|
t.Fatalf("status = %s, want succeeded (unchanged)", runs.run.Status)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-26 08:37:04 +00:00
|
|
|
func TestRunMaintenance_ReapExpiredLock(t *testing.T) {
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
template := demoTemplate()
|
|
|
|
|
queue := newMemoryQueueRepo()
|
|
|
|
|
runs := newMemoryRunRepo(nil)
|
|
|
|
|
lockedUntil := clock.Now().Add(-time.Second).UnixNano()
|
|
|
|
|
runs.run = &entity.Run{
|
|
|
|
|
TemplateType: template.Type,
|
|
|
|
|
Status: enum.RunStatusRunning,
|
|
|
|
|
LockedUntil: &lockedUntil,
|
|
|
|
|
DedupeKey: "dedupe-2",
|
|
|
|
|
}
|
|
|
|
|
uc := testUseCaseFull(template, runs, nil, queue)
|
|
|
|
|
|
|
|
|
|
result, err := uc.RunMaintenance(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("RunMaintenance() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if result.ReapedExpiredLocks != 1 {
|
|
|
|
|
t.Fatalf("ReapedExpiredLocks = %d, want 1", result.ReapedExpiredLocks)
|
|
|
|
|
}
|
|
|
|
|
if runs.run.Status != enum.RunStatusExpired {
|
|
|
|
|
t.Fatalf("status = %s, want expired", runs.run.Status)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-06 06:13:14 +00:00
|
|
|
|
|
|
|
|
func TestRunMaintenance_ReapExpiredLock_SkipsIfAlreadyResolved(t *testing.T) {
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
template := demoTemplate()
|
|
|
|
|
queue := newMemoryQueueRepo()
|
|
|
|
|
runs := newMemoryRunRepo(nil)
|
|
|
|
|
lockedUntil := clock.Now().Add(-time.Second).UnixNano()
|
|
|
|
|
runs.run = &entity.Run{
|
|
|
|
|
TemplateType: template.Type,
|
|
|
|
|
Status: enum.RunStatusRunning,
|
|
|
|
|
LockedUntil: &lockedUntil,
|
|
|
|
|
DedupeKey: "dedupe-2",
|
|
|
|
|
}
|
|
|
|
|
uc := testUseCaseFull(template, runs, nil, queue)
|
|
|
|
|
|
|
|
|
|
// Simulate the worker completing the job right after the reaper reads it
|
|
|
|
|
// but before it writes back; the reaper must not overwrite it to expired.
|
|
|
|
|
runs.raceAfterFind = func() { runs.run.Status = enum.RunStatusSucceeded }
|
|
|
|
|
|
|
|
|
|
result, err := uc.RunMaintenance(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("RunMaintenance() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if result.ReapedExpiredLocks != 0 {
|
|
|
|
|
t.Fatalf("ReapedExpiredLocks = %d, want 0 (must not clobber concurrently resolved status)", result.ReapedExpiredLocks)
|
|
|
|
|
}
|
|
|
|
|
if runs.run.Status != enum.RunStatusSucceeded {
|
|
|
|
|
t.Fatalf("status = %s, want succeeded (unchanged)", runs.run.Status)
|
|
|
|
|
}
|
|
|
|
|
}
|