P3.1: Add ExecutionMode/OverlapPolicy to Config; add Pending to JobRuntime

Introduces ExecutionMode (parallel/sequential) and OverlapPolicy
(skip/queue) types and constants in domain/config.go, wires defaults
(parallel/skip) into loadOrCreateConfig and the normalization pass, and
adds validation in validateConfig. Adds Pending bool to JobRuntime as
the flag P3.3 will use to re-run a queued overlap. Marks P3.1 done.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mixeme
2026-06-23 07:32:19 +03:00
parent e8421fc3ff
commit 9b4e9ee311
6 changed files with 60 additions and 16 deletions
+1 -1
View File
@@ -94,7 +94,7 @@ These land together because both edit `domain/job.go` and `storage/store.go`.
- [x] P2.4 — `.gitignore` / `.dockerignore` - [x] P2.4 — `.gitignore` / `.dockerignore`
### Phase 3 — Task-queue model + settings ### Phase 3 — Task-queue model + settings
- [ ] P3.1 — Config/runtime fields + defaults - [x] P3.1 — Config/runtime fields + defaults
- [ ] P3.2 — Split dispatch into `app/run.go` - [ ] P3.2 — Split dispatch into `app/run.go`
- [ ] P3.3 — Rework `RunDue`/`executeRun` for mode + overlap policy - [ ] P3.3 — Rework `RunDue`/`executeRun` for mode + overlap policy
- [ ] P3.4 — Settings Queue selects - [ ] P3.4 — Settings Queue selects
+6
View File
@@ -476,5 +476,11 @@ func validateConfig(config domain.Config) error {
if config.MaxLogAgeDays <= 0 { if config.MaxLogAgeDays <= 0 {
return errors.New("max log age days must be a positive number") return errors.New("max log age days must be a positive number")
} }
if config.ExecutionMode != domain.ExecutionModeParallel && config.ExecutionMode != domain.ExecutionModeSequential {
return errors.New("execution mode must be 'parallel' or 'sequential'")
}
if config.OverlapPolicy != domain.OverlapPolicySkip && config.OverlapPolicy != domain.OverlapPolicyQueue {
return errors.New("overlap policy must be 'skip' or 'queue'")
}
return nil return nil
} }
+1 -1
View File
@@ -25,7 +25,7 @@ func newTempService(t *testing.T, jobs []domain.Job) *Service {
JobsPath: filepath.Join(dir, "jobs.json"), JobsPath: filepath.Join(dir, "jobs.json"),
LogsDir: filepath.Join(dir, "logs"), LogsDir: filepath.Join(dir, "logs"),
}, },
Config: domain.Config{JobsDir: ".", LogsDir: "logs", MaxLogFiles: 100, MaxLogAgeDays: 30}, Config: domain.Config{JobsDir: ".", LogsDir: "logs", MaxLogFiles: 100, MaxLogAgeDays: 30, ExecutionMode: domain.ExecutionModeParallel, OverlapPolicy: domain.OverlapPolicySkip},
} }
return NewService(store, jobs) return NewService(store, jobs)
} }
+31 -7
View File
@@ -5,17 +5,41 @@ package domain
// launches omit this flag and open the normal window. // launches omit this flag and open the normal window.
const StartInTrayArgument = "--start-in-tray" const StartInTrayArgument = "--start-in-tray"
// ExecutionMode controls whether due jobs run concurrently or one at a time.
type ExecutionMode string
const (
// ExecutionModeParallel allows all due jobs to start simultaneously.
ExecutionModeParallel ExecutionMode = "parallel"
// ExecutionModeSequential runs due jobs one after another, in order.
ExecutionModeSequential ExecutionMode = "sequential"
)
// OverlapPolicy decides what happens when a job's next run fires while the
// previous run is still active.
type OverlapPolicy string
const (
// OverlapPolicySkip discards the new run when the job is already running.
OverlapPolicySkip OverlapPolicy = "skip"
// OverlapPolicyQueue holds the new run and starts it as soon as the current
// run finishes.
OverlapPolicyQueue OverlapPolicy = "queue"
)
// Config is stored in gosentry.json next to the program. It contains only // Config is stored in gosentry.json next to the program. It contains only
// application-level choices: where to read jobs from, where to write logs, and // application-level choices: where to read jobs from, where to write logs, and
// how the desktop shell should behave. // how the desktop shell should behave.
type Config struct { type Config struct {
JobsDir string `json:"jobs_dir"` JobsDir string `json:"jobs_dir"`
LogsDir string `json:"logs_dir"` LogsDir string `json:"logs_dir"`
MaxLogFiles int `json:"max_log_files"` MaxLogFiles int `json:"max_log_files"`
MaxLogAgeDays int `json:"max_log_age_days"` MaxLogAgeDays int `json:"max_log_age_days"`
StartOnLogin bool `json:"start_on_login,omitempty"` StartOnLogin bool `json:"start_on_login,omitempty"`
KeepRunningInTray bool `json:"keep_running_in_tray,omitempty"` KeepRunningInTray bool `json:"keep_running_in_tray,omitempty"`
NotifyOnFailure bool `json:"notify_on_failure,omitempty"` NotifyOnFailure bool `json:"notify_on_failure,omitempty"`
ExecutionMode ExecutionMode `json:"execution_mode,omitempty"`
OverlapPolicy OverlapPolicy `json:"overlap_policy,omitempty"`
} }
// JobsFile is the on-disk shape of jobs.json. Wrapping the slice in a top-level // JobsFile is the on-disk shape of jobs.json. Wrapping the slice in a top-level
+4
View File
@@ -18,6 +18,10 @@ type JobRuntime struct {
// scheduler comparisons. NextRun above is its formatted display string and is // scheduler comparisons. NextRun above is its formatted display string and is
// the only form shown in the GUI. // the only form shown in the GUI.
NextDue time.Time NextDue time.Time
// Pending is set when a run was skipped due to the overlap policy being
// "queue". The scheduler will start this job as soon as the current run ends.
Pending bool
} }
// NewRuntime builds the initial runtime state for a freshly loaded or created // NewRuntime builds the initial runtime state for a freshly loaded or created
+17 -7
View File
@@ -24,13 +24,15 @@ type Store struct {
// domain struct so the value conversions in importYAMLConfig / importYAMLJobs // domain struct so the value conversions in importYAMLConfig / importYAMLJobs
// remain valid. // remain valid.
type yamlConfig struct { type yamlConfig struct {
JobsDir string `yaml:"jobs_dir"` JobsDir string `yaml:"jobs_dir"`
LogsDir string `yaml:"logs_dir"` LogsDir string `yaml:"logs_dir"`
MaxLogFiles int `yaml:"max_log_files"` MaxLogFiles int `yaml:"max_log_files"`
MaxLogAgeDays int `yaml:"max_log_age_days"` MaxLogAgeDays int `yaml:"max_log_age_days"`
StartOnLogin bool `yaml:"start_on_login,omitempty"` StartOnLogin bool `yaml:"start_on_login,omitempty"`
KeepRunningInTray bool `yaml:"keep_running_in_tray,omitempty"` KeepRunningInTray bool `yaml:"keep_running_in_tray,omitempty"`
NotifyOnFailure bool `yaml:"notify_on_failure,omitempty"` NotifyOnFailure bool `yaml:"notify_on_failure,omitempty"`
ExecutionMode domain.ExecutionMode `yaml:"execution_mode,omitempty"`
OverlapPolicy domain.OverlapPolicy `yaml:"overlap_policy,omitempty"`
} }
type yamlJob struct { type yamlJob struct {
@@ -108,6 +110,8 @@ func loadOrCreateConfig(paths Paths) (domain.Config, error) {
StartOnLogin: false, StartOnLogin: false,
KeepRunningInTray: true, KeepRunningInTray: true,
NotifyOnFailure: true, NotifyOnFailure: true,
ExecutionMode: domain.ExecutionModeParallel,
OverlapPolicy: domain.OverlapPolicySkip,
} }
if _, err := os.Stat(paths.ConfigPath); errors.Is(err, os.ErrNotExist) { if _, err := os.Stat(paths.ConfigPath); errors.Is(err, os.ErrNotExist) {
@@ -147,6 +151,12 @@ func loadOrCreateConfig(paths Paths) (domain.Config, error) {
if config.MaxLogAgeDays <= 0 { if config.MaxLogAgeDays <= 0 {
config.MaxLogAgeDays = 30 config.MaxLogAgeDays = 30
} }
if config.ExecutionMode == "" {
config.ExecutionMode = domain.ExecutionModeParallel
}
if config.OverlapPolicy == "" {
config.OverlapPolicy = domain.OverlapPolicySkip
}
return config, nil return config, nil
} }