diff --git a/docs/PRE-RELEASE-TASKS.md b/docs/PRE-RELEASE-TASKS.md index 62e61d8..05fbb66 100644 --- a/docs/PRE-RELEASE-TASKS.md +++ b/docs/PRE-RELEASE-TASKS.md @@ -94,7 +94,7 @@ These land together because both edit `domain/job.go` and `storage/store.go`. - [x] P2.4 — `.gitignore` / `.dockerignore` ### 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.3 — Rework `RunDue`/`executeRun` for mode + overlap policy - [ ] P3.4 — Settings Queue selects diff --git a/src/app/operations.go b/src/app/operations.go index bd66547..b5c3c37 100644 --- a/src/app/operations.go +++ b/src/app/operations.go @@ -476,5 +476,11 @@ func validateConfig(config domain.Config) error { if config.MaxLogAgeDays <= 0 { 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 } diff --git a/src/app/operations_test.go b/src/app/operations_test.go index 755b34a..e407a69 100644 --- a/src/app/operations_test.go +++ b/src/app/operations_test.go @@ -25,7 +25,7 @@ func newTempService(t *testing.T, jobs []domain.Job) *Service { JobsPath: filepath.Join(dir, "jobs.json"), 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) } diff --git a/src/domain/config.go b/src/domain/config.go index 8e5c173..f0095ca 100644 --- a/src/domain/config.go +++ b/src/domain/config.go @@ -5,17 +5,41 @@ package domain // launches omit this flag and open the normal window. 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 // application-level choices: where to read jobs from, where to write logs, and // how the desktop shell should behave. type Config struct { - JobsDir string `json:"jobs_dir"` - LogsDir string `json:"logs_dir"` - MaxLogFiles int `json:"max_log_files"` - MaxLogAgeDays int `json:"max_log_age_days"` - StartOnLogin bool `json:"start_on_login,omitempty"` - KeepRunningInTray bool `json:"keep_running_in_tray,omitempty"` - NotifyOnFailure bool `json:"notify_on_failure,omitempty"` + JobsDir string `json:"jobs_dir"` + LogsDir string `json:"logs_dir"` + MaxLogFiles int `json:"max_log_files"` + MaxLogAgeDays int `json:"max_log_age_days"` + StartOnLogin bool `json:"start_on_login,omitempty"` + KeepRunningInTray bool `json:"keep_running_in_tray,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 diff --git a/src/domain/runtime.go b/src/domain/runtime.go index 35934be..e2ffd3b 100644 --- a/src/domain/runtime.go +++ b/src/domain/runtime.go @@ -18,6 +18,10 @@ type JobRuntime struct { // scheduler comparisons. NextRun above is its formatted display string and is // the only form shown in the GUI. 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 diff --git a/src/storage/store.go b/src/storage/store.go index 2eacb78..c088d5b 100644 --- a/src/storage/store.go +++ b/src/storage/store.go @@ -24,13 +24,15 @@ type Store struct { // domain struct so the value conversions in importYAMLConfig / importYAMLJobs // remain valid. type yamlConfig struct { - JobsDir string `yaml:"jobs_dir"` - LogsDir string `yaml:"logs_dir"` - MaxLogFiles int `yaml:"max_log_files"` - MaxLogAgeDays int `yaml:"max_log_age_days"` - StartOnLogin bool `yaml:"start_on_login,omitempty"` - KeepRunningInTray bool `yaml:"keep_running_in_tray,omitempty"` - NotifyOnFailure bool `yaml:"notify_on_failure,omitempty"` + JobsDir string `yaml:"jobs_dir"` + LogsDir string `yaml:"logs_dir"` + MaxLogFiles int `yaml:"max_log_files"` + MaxLogAgeDays int `yaml:"max_log_age_days"` + StartOnLogin bool `yaml:"start_on_login,omitempty"` + KeepRunningInTray bool `yaml:"keep_running_in_tray,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 { @@ -108,6 +110,8 @@ func loadOrCreateConfig(paths Paths) (domain.Config, error) { StartOnLogin: false, KeepRunningInTray: true, NotifyOnFailure: true, + ExecutionMode: domain.ExecutionModeParallel, + OverlapPolicy: domain.OverlapPolicySkip, } 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 { config.MaxLogAgeDays = 30 } + if config.ExecutionMode == "" { + config.ExecutionMode = domain.ExecutionModeParallel + } + if config.OverlapPolicy == "" { + config.OverlapPolicy = domain.OverlapPolicySkip + } return config, nil }