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`
### 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
+6
View File
@@ -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
}
+1 -1
View File
@@ -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)
}
+24
View File
@@ -5,6 +5,28 @@ 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.
@@ -16,6 +38,8 @@ type Config struct {
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
+4
View File
@@ -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
+10
View File
@@ -31,6 +31,8 @@ type yamlConfig struct {
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
}