Files
ARTEX-chixuweihu/db/task_runtime_test.go
T
artex d8bda59d7d fix: harden task runtime across restart and terminal mirror
Clear open run segments on process start without imputing downtime.
Mirror terminal/pause runtime fields into memory before hydration, make
SetPaused finalize segments, and add DB lifecycle tests.
2026-10-27 14:14:56 +08:00

173 lines
5.7 KiB
Go

package db
import (
"database/sql"
"testing"
)
func TestTaskRunSegmentLifecycle(t *testing.T) {
d, err := Open(testDSN(t))
if err != nil {
t.Skipf("postgres unavailable (%v) — skipping", err)
}
defer d.Close()
task, err := d.CreateTask("runtime lifecycle", "runtime lifecycle test", nil, 0, 0)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = d.DeleteTask(task.ID) })
active, started, err := d.StartTaskRunSegment(task.ID)
if err != nil {
t.Fatal(err)
}
if started == nil {
t.Fatal("StartTaskRunSegment did not open a segment")
}
if active != 0 {
t.Fatalf("new task active seconds=%d, want 0", active)
}
if _, err := d.Exec(`UPDATE tasks SET run_segment_started_at=now()-interval '3 seconds' WHERE id=$1`, task.ID); err != nil {
t.Fatal(err)
}
active, started, err = d.FinalizeTaskRunSegment(task.ID)
if err != nil {
t.Fatal(err)
}
if active <= 0 || started != nil {
t.Fatalf("first finalize active=%d started=%v, want positive and NULL", active, started)
}
firstActive := active
// Pause folds the open segment exactly once and closes it. Repeated
// finalization after wall-clock advancement remains a no-op.
if _, _, err := d.StartTaskRunSegment(task.ID); err != nil {
t.Fatal(err)
}
if _, err := d.Exec(`UPDATE tasks SET run_segment_started_at=now()-interval '3 seconds' WHERE id=$1`, task.ID); err != nil {
t.Fatal(err)
}
active, started, err = d.SetPausedRuntime(task.ID, true)
if err != nil {
t.Fatal(err)
}
if active <= firstActive || started != nil {
t.Fatalf("pause active=%d started=%v, want increase and NULL", active, started)
}
pausedActive := active
if active, started, err = d.FinalizeTaskRunSegment(task.ID); err != nil {
t.Fatal(err)
} else if active != pausedActive || started != nil {
t.Fatalf("repeated finalize after pause active=%d started=%v, want %d and NULL", active, started, pausedActive)
}
// Resuming opens a fresh segment, whose elapsed time is added on top of the
// paused total rather than replacing it.
if _, _, err := d.SetPausedRuntime(task.ID, false); err != nil {
t.Fatal(err)
}
if _, started, err = d.StartTaskRunSegment(task.ID); err != nil {
t.Fatal(err)
} else if started == nil {
t.Fatal("resume did not open a segment")
}
if _, err := d.Exec(`UPDATE tasks SET run_segment_started_at=now()-interval '3 seconds' WHERE id=$1`, task.ID); err != nil {
t.Fatal(err)
}
active, started, err = d.FinalizeTaskRunSegment(task.ID)
if err != nil {
t.Fatal(err)
}
if active <= pausedActive || started != nil {
t.Fatalf("resume finalize active=%d started=%v, want increase and NULL", active, started)
}
// Both terminal status paths fold an open segment and leave no live clock.
if _, started, err = d.StartTaskRunSegment(task.ID); err != nil {
t.Fatal(err)
} else if started == nil {
t.Fatal("terminal status setup did not open a segment")
}
if _, err := d.Exec(`UPDATE tasks SET run_segment_started_at=now()-interval '3 seconds' WHERE id=$1`, task.ID); err != nil {
t.Fatal(err)
}
beforeTerminal := active
active, started, err = d.SetStatusRuntime(task.ID, "done")
if err != nil {
t.Fatal(err)
}
if active <= beforeTerminal || started != nil {
t.Fatalf("SetStatusRuntime terminal active=%d started=%v, want increase and NULL", active, started)
}
if _, _, err = d.SetStatusRuntime(task.ID, "running"); err != nil {
t.Fatal(err)
}
if _, started, err = d.StartTaskRunSegment(task.ID); err != nil {
t.Fatal(err)
} else if started == nil {
t.Fatal("guarded terminal setup did not open a segment")
}
if _, err := d.Exec(`UPDATE tasks SET run_segment_started_at=now()-interval '3 seconds' WHERE id=$1`, task.ID); err != nil {
t.Fatal(err)
}
beforeGuarded := active
active, started, won, err := d.SetTerminalStatusGuardedRuntime(task.ID, "failed")
if err != nil || !won {
t.Fatalf("SetTerminalStatusGuardedRuntime: active=%d started=%v won=%v err=%v", active, started, won, err)
}
if active <= beforeGuarded || started != nil {
t.Fatalf("guarded terminal active=%d started=%v, want increase and NULL", active, started)
}
finalActive := active
active, started, won, err = d.SetTerminalStatusGuardedRuntime(task.ID, "timeout")
if err != nil || won {
t.Fatalf("second guarded terminal: active=%d started=%v won=%v err=%v", active, started, won, err)
}
if active != finalActive || started != nil {
t.Fatalf("second guarded terminal returned active=%d started=%v, want unchanged active=%d and NULL segment", active, started, finalActive)
}
got, err := d.GetTask(task.ID)
if err != nil {
t.Fatal(err)
}
if got == nil || got.ActiveRunSeconds != finalActive || got.RunSegmentStartedAt != nil {
t.Fatalf("persisted terminal runtime=%+v, want active=%d and NULL segment", got, finalActive)
}
}
func TestClearOpenTaskRunSegmentsPreservesAccumulatedRuntime(t *testing.T) {
d, err := Open(testDSN(t))
if err != nil {
t.Skipf("postgres unavailable (%v) — skipping", err)
}
defer d.Close()
task, err := d.CreateTask("runtime restart clear", "runtime restart clear test", nil, 0, 0)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = d.DeleteTask(task.ID) })
if _, err := d.Exec(`UPDATE tasks SET active_run_seconds=17, run_segment_started_at=now()-interval '1 hour' WHERE id=$1`, task.ID); err != nil {
t.Fatal(err)
}
cleared, err := d.ClearOpenTaskRunSegments()
if err != nil {
t.Fatal(err)
}
if cleared < 1 {
t.Fatalf("ClearOpenTaskRunSegments cleared %d rows, want at least 1", cleared)
}
var active int64
var started sql.NullTime
if err := d.QueryRow(`SELECT active_run_seconds, run_segment_started_at FROM tasks WHERE id=$1`, task.ID).Scan(&active, &started); err != nil {
t.Fatal(err)
}
if active != 17 || started.Valid {
t.Fatalf("restart clear changed runtime: active=%d started=%v, want 17 and NULL", active, started)
}
}