package tasks import ( "context" "database/sql" "testing" "atlas9.dev/c/core/dbi" "atlas9.dev/c/demo/boot/bootdb" _ "github.com/mattn/go-sqlite3" ) func newTestDB(t *testing.T) *sql.DB { t.Helper() db, err := bootdb.Database(":memory:") if err != nil { t.Fatal(err) } t.Cleanup(func() { db.Close() }) // Start from an empty schedule table; each test seeds its own rows rather // than depending on the migration's seeded system schedules. if _, err := db.Exec(`DELETE FROM system_schedules`); err != nil { t.Fatal(err) } return db } func systemStore(tx dbi.DBI) ScheduleStore { return NewSqliteScheduleStore(tx, "system_schedules") } func TestRunnerFire(t *testing.T) { db := newTestDB(t) ctx := context.Background() enqueued := 0 r := &Runner{ DB: db, Store: systemStore, Enqueue: func(ctx context.Context, tx dbi.DBI, id string) error { enqueued++ return nil }, } // Seed a row that is already due. if _, err := db.Exec(`INSERT INTO system_schedules (id, schedule, next_run_at) VALUES ('job', 'every 5m', '2000-01-01 00:00:00')`); err != nil { t.Fatal(err) } // One tick fires it exactly once and advances next_run_at into the future. r.tick(ctx) if enqueued != 1 { t.Fatalf("after first tick enqueued = %d, want 1", enqueued) } // A second tick finds nothing due. r.tick(ctx) if enqueued != 1 { t.Fatalf("after second tick enqueued = %d, want 1", enqueued) } } func TestRunnerBadScheduleIsSkipped(t *testing.T) { db := newTestDB(t) ctx := context.Background() enqueued := 0 r := &Runner{ DB: db, Store: systemStore, Enqueue: func(ctx context.Context, tx dbi.DBI, id string) error { enqueued++ return nil }, } // A due row with a bad schedule string, and a valid one alongside it. The // bad row is skipped without wedging the valid one. if _, err := db.Exec(`INSERT INTO system_schedules (id, schedule, next_run_at) VALUES ('bad', 'hourly', '2000-01-01 00:00:00'), ('good', 'every 5m', '2000-01-01 00:00:00')`); err != nil { t.Fatal(err) } r.tick(ctx) if enqueued != 1 { t.Fatalf("enqueued = %d, want 1 (only the valid schedule)", enqueued) } } func TestRunnerClaimIsExclusive(t *testing.T) { db := newTestDB(t) ctx := context.Background() if _, err := db.Exec(`INSERT INTO system_schedules (id, schedule, next_run_at) VALUES ('job', 'every 5m', '2000-01-01 00:00:00')`); err != nil { t.Fatal(err) } // The conditional UPDATE is the claim: the first advances next_run_at into // the future, so the second matches no rows. This is what makes two ticks // (or two instances) unable to double-enqueue a single occurrence. next := mustTime(t, "2999-01-01T00:00:00Z") claim := func() bool { var won bool err := dbi.ReadWrite(ctx, db, func(tx dbi.DBI) error { var err error won, err = systemStore(tx).Claim(ctx, "job", next) return err }) if err != nil { t.Fatal(err) } return won } if !claim() { t.Fatal("first claim should win") } if claim() { t.Fatal("second claim should lose") } }