feat: per-job command timeout with global default
Add an optional per-job run timeout following the overlap_policy inherit pattern: Job.TimeoutSeconds (0 = inherit) resolves against a new Config.DefaultTimeoutSeconds (default 30s), replacing the hard-coded 30s guard in runner.RunJob. - domain/storage: new fields, default 30, load-time normalization - runner: RunJob takes an explicit timeout; StartOnly stays untimed so it keeps measuring launch latency only - app: effectiveTimeout resolves under mu into runEnv, threaded to runJob; seam signature and validation updated; DisplayTimeout helper - ui: Timeout entry in the job dialog, Default timeout in Settings, and a Timeout row in the details panel - tests + docs (ARCHITECTURE, STANDARDS, ROADMAP, CHANGELOG) updated; version bumped to 0.12.0 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+16
-1
@@ -112,6 +112,7 @@ type runEnv struct {
|
||||
logsDir string
|
||||
maxFiles int
|
||||
maxAge int
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// startRunLocked transitions a job to "Running", advances its NextDue to the next
|
||||
@@ -142,6 +143,7 @@ func (s *Service) startRunLocked(job *domain.Job, runtime *domain.JobRuntime, tr
|
||||
logsDir: s.store.Paths.LogsDir,
|
||||
maxFiles: s.store.Config.MaxLogFiles,
|
||||
maxAge: s.store.Config.MaxLogAgeDays,
|
||||
timeout: s.effectiveTimeout(job),
|
||||
}
|
||||
// Capture ctx under the lock so a concurrent Start/Stop cannot swap it out
|
||||
// from under the goroutine after we release mu.
|
||||
@@ -155,7 +157,7 @@ func (s *Service) startRunLocked(job *domain.Job, runtime *domain.JobRuntime, tr
|
||||
// is not paused, deferred runs are started one at a time until PendingRuns reaches
|
||||
// zero. Each deferred run runs on its own goroutine.
|
||||
func (s *Service) executeRun(ctx context.Context, jobCopy domain.Job, trigger string, env runEnv) {
|
||||
record, logErr := s.runJob(ctx, &jobCopy, trigger, env.logsDir)
|
||||
record, logErr := s.runJob(ctx, &jobCopy, trigger, env.logsDir, env.timeout)
|
||||
|
||||
s.mu.Lock()
|
||||
var cleanupErr, saveErr error
|
||||
@@ -208,6 +210,19 @@ func (s *Service) effectiveOverlapPolicy(job *domain.Job) domain.OverlapPolicy {
|
||||
return s.store.Config.OverlapPolicy
|
||||
}
|
||||
|
||||
// effectiveTimeout resolves the run timeout that actually governs a job: the
|
||||
// job's own TimeoutSeconds when positive, otherwise the global
|
||||
// Config.DefaultTimeoutSeconds. A non-positive Job.TimeoutSeconds means "inherit
|
||||
// the global default", which is why normalizeJob leaves 0 rather than
|
||||
// backfilling the configured value. The caller must hold mu.
|
||||
func (s *Service) effectiveTimeout(job *domain.Job) time.Duration {
|
||||
secs := job.TimeoutSeconds
|
||||
if secs <= 0 {
|
||||
secs = s.store.Config.DefaultTimeoutSeconds
|
||||
}
|
||||
return time.Duration(secs) * time.Second
|
||||
}
|
||||
|
||||
// anyRunningLocked reports whether any loaded job is currently in the "Running"
|
||||
// state. It backs the sequential-mode guards in RunNow and RunDue. The caller
|
||||
// must hold mu.
|
||||
|
||||
Reference in New Issue
Block a user