feat: make per-job timeout 0 mean "no timeout" instead of inherit

A per-job timeout now has three distinct states: unset inherits the global
default, an explicit 0 means no timeout and does not inherit, and a positive
value is the per-job limit. Job.TimeoutSeconds became *int so unset and 0 stay
distinguishable in jobs.json.

Also fixes the global default, which could not persist a 0. loadOrCreateConfig
normalized DefaultTimeoutSeconds <= 0 back to 30 on every read of an existing
gosentry.json, so "no timeout" only held until the next restart. The field is
now written unconditionally (no omitempty) and read back as-is.

Existing jobs and configs are unaffected: a job with no timeout_seconds still
inherits, and a saved global default of 30 stays 30.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mixeme
2026-07-26 22:02:46 +03:00
parent 7f4f63eb8e
commit e85cbc4eb1
14 changed files with 167 additions and 47 deletions
+12 -5
View File
@@ -617,23 +617,30 @@ func TestRunDueQueueDrainSkippedWhenPaused(t *testing.T) {
}
}
// TestEffectiveTimeout verifies the inherit-or-override resolution: a zero
// Job.TimeoutSeconds falls back to the global default, while a positive value
// overrides it.
// TestEffectiveTimeout verifies the three-state resolution: an unset (nil)
// Job.TimeoutSeconds falls back to the global default, a positive value
// overrides it, and an explicit 0 means "no timeout" without inheriting.
func TestEffectiveTimeout(t *testing.T) {
svc := newTempService(t, nil)
svc.store.Config.DefaultTimeoutSeconds = 30
inherit := &domain.Job{TimeoutSeconds: 0}
inherit := &domain.Job{TimeoutSeconds: nil}
if got, want := svc.effectiveTimeout(inherit), 30*time.Second; got != want {
t.Errorf("inherited timeout = %s, want %s", got, want)
}
own := &domain.Job{TimeoutSeconds: 5}
own := &domain.Job{TimeoutSeconds: domain.TimeoutSecondsPtr(5)}
if got, want := svc.effectiveTimeout(own), 5*time.Second; got != want {
t.Errorf("per-job timeout = %s, want %s", got, want)
}
// An explicit per-job 0 must beat a positive global default rather than be
// mistaken for "unset".
none := &domain.Job{TimeoutSeconds: domain.TimeoutSecondsPtr(0)}
if got, want := svc.effectiveTimeout(none), time.Duration(0); got != want {
t.Errorf("explicit per-job zero timeout = %s, want %s (no timeout)", got, want)
}
svc.store.Config.DefaultTimeoutSeconds = 0
if got, want := svc.effectiveTimeout(inherit), time.Duration(0); got != want {
t.Errorf("inherited timeout with no global default = %s, want %s (no timeout)", got, want)