Files
restic-manager/internal/store/jobs_test.go
T
steve b8c9c50a93 store: LatestJobByKind includes in-flight jobs (avoid maintenance double-fire)
Widen the SQL query to consider all statuses (queued, running,
succeeded, failed, cancelled) rather than terminal-only. An in-flight
prune that outlasts the 60s tick interval previously produced
ErrNotFound, causing the ticker to anchor at now-24h and fire a second
prune concurrently with the first.

Update the doc comment and test: remove the "queued job filtered out"
case, add assertions that a running job and a queued job are each
returned as the latest.
2026-05-04 10:19:15 +01:00

137 lines
3.9 KiB
Go

package store
import (
"context"
"errors"
"testing"
"time"
)
func TestLatestJobByKind(t *testing.T) {
t.Parallel()
s := openTestStore(t)
ctx := context.Background()
hostID := makeSchedHost(t, s)
// No jobs yet → ErrNotFound.
if _, err := s.LatestJobByKind(ctx, hostID, "forget"); !errors.Is(err, ErrNotFound) {
t.Fatalf("expected ErrNotFound on empty, got %v", err)
}
// Insert two finished jobs of kind=forget; the newer one should win.
older := time.Now().UTC().Add(-2 * time.Hour)
newer := time.Now().UTC().Add(-1 * time.Hour)
if err := s.CreateJob(ctx, Job{
ID: "j-old", HostID: hostID, Kind: "forget",
ActorKind: "system", CreatedAt: older,
}); err != nil {
t.Fatalf("create older: %v", err)
}
if err := s.MarkJobFinished(ctx, "j-old", "succeeded", 0, nil, "", older.Add(time.Minute)); err != nil {
t.Fatalf("finish older: %v", err)
}
if err := s.CreateJob(ctx, Job{
ID: "j-new", HostID: hostID, Kind: "forget",
ActorKind: "system", CreatedAt: newer,
}); err != nil {
t.Fatalf("create newer: %v", err)
}
if err := s.MarkJobFinished(ctx, "j-new", "failed", 1, nil, "boom", newer.Add(time.Minute)); err != nil {
t.Fatalf("finish newer: %v", err)
}
got, err := s.LatestJobByKind(ctx, hostID, "forget")
if err != nil {
t.Fatalf("LatestJobByKind: %v", err)
}
if got.ID != "j-new" {
t.Errorf("want j-new, got %q", got.ID)
}
// An in-flight running job must be returned — long-prune-suppresses-tick
// scenario: if a prune runs >60s the next tick must not re-fire it.
runningAt := time.Now().UTC()
if err := s.CreateJob(ctx, Job{
ID: "j-running", HostID: hostID, Kind: "forget",
ActorKind: "system", CreatedAt: runningAt,
}); err != nil {
t.Fatalf("create running: %v", err)
}
if err := s.MarkJobStarted(ctx, "j-running", runningAt); err != nil {
t.Fatalf("mark started: %v", err)
}
got2, err := s.LatestJobByKind(ctx, hostID, "forget")
if err != nil {
t.Fatalf("LatestJobByKind 2: %v", err)
}
if got2.ID != "j-running" {
t.Errorf("in-flight running job must be returned; want j-running, got %q", got2.ID)
}
// A queued (not-yet-started) job is also returned (it is newer than
// j-running because CreatedAt is later).
queuedAt := runningAt.Add(time.Millisecond)
if err := s.CreateJob(ctx, Job{
ID: "j-queued", HostID: hostID, Kind: "forget",
ActorKind: "system", CreatedAt: queuedAt,
}); err != nil {
t.Fatalf("create queued: %v", err)
}
got3, err := s.LatestJobByKind(ctx, hostID, "forget")
if err != nil {
t.Fatalf("LatestJobByKind 3: %v", err)
}
if got3.ID != "j-queued" {
t.Errorf("queued job must be returned as newest; want j-queued, got %q", got3.ID)
}
// Different kind → ErrNotFound.
if _, err := s.LatestJobByKind(ctx, hostID, "prune"); !errors.Is(err, ErrNotFound) {
t.Fatalf("expected ErrNotFound for prune, got %v", err)
}
}
func TestListAllMaintenance(t *testing.T) {
t.Parallel()
s := openTestStore(t)
ctx := context.Background()
// Empty case.
rows, err := s.ListAllMaintenance(ctx)
if err != nil {
t.Fatalf("empty list: %v", err)
}
if len(rows) != 0 {
t.Errorf("want empty, got %+v", rows)
}
// Seed two hosts with maintenance rows.
h1 := "01HMAINTHOST00000000000A1"
h2 := "01HMAINTHOST00000000000A2"
for i, id := range []string{h1, h2} {
if err := s.CreateHost(ctx, Host{
ID: id, Name: "maint-host-" + string(rune('a'+i)),
OS: "linux", Arch: "amd64",
AgentVersion: "dev", ResticVersion: "0.16.0", ProtocolVersion: 1,
EnrolledAt: time.Now().UTC(),
}, "th-"+id, ""); err != nil {
t.Fatalf("create host %s: %v", id, err)
}
}
if err := s.CreateDefaultRepoMaintenance(ctx, h1); err != nil {
t.Fatalf("seed h1: %v", err)
}
if err := s.CreateDefaultRepoMaintenance(ctx, h2); err != nil {
t.Fatalf("seed h2: %v", err)
}
rows, err = s.ListAllMaintenance(ctx)
if err != nil {
t.Fatalf("list: %v", err)
}
if len(rows) != 2 {
t.Errorf("want 2 rows, got %d", len(rows))
}
}