T2.3: Split domain.Job (durable) from domain.JobRuntime (transient)
Move all transient execution state off domain.Job into a new domain.JobRuntime, keyed by job ID: - domain: Job now holds only durable YAML fields; remove the yaml:"-" fields (LastRun/NextRun/LastState/Logs/Output) and NextDue. Add runtime.go with JobRuntime plus NewRuntime/NewRuntimes constructors, which now own the runtime-init logic moved out of normalizeJobs. - runner: RunJob no longer mutates the job; it is pure and returns the RunRecord for the caller to fold into the runtime. - scheduler: take a shared map[int]*JobRuntime and route status/next-run bookkeeping through runtimeFor(job); prepareNextRun writes a *JobRuntime. - storage: normalizeJobs touches only durable config. - gui: own the runtime map (NewRuntimes), share it with the scheduler, and read/write runtime state via runtimeFor; maintain the map by ID on add/edit/delete. - tests: update scheduler/storage tests to the split; tidy a pre-existing import-order nit in scheduler.go. This also satisfies T2.4 (storage load/save only Job, runtime init in domain.NewRuntime, round-trip tests), since removing the fields forced it. Runtime-map ownership remains GUI-side glue until T3.1. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+51
-28
@@ -8,8 +8,8 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.mixdep.ru/mix/gosentry/src/domain"
|
||||
"gitea.mixdep.ru/mix/gosentry/src/storage"
|
||||
"gitea.mixdep.ru/mix/gosentry/src/runner"
|
||||
"gitea.mixdep.ru/mix/gosentry/src/storage"
|
||||
)
|
||||
|
||||
// Scheduler owns the timing loop for jobs that are currently loaded in the GUI.
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
type Scheduler struct {
|
||||
store *storage.Store
|
||||
jobs *[]domain.Job
|
||||
runtimes map[int]*domain.JobRuntime
|
||||
onChange func(domain.RunRecord)
|
||||
|
||||
mu sync.Mutex
|
||||
@@ -28,11 +29,15 @@ type Scheduler struct {
|
||||
schedules map[int]domain.Schedule // parsed once per job on load/edit
|
||||
}
|
||||
|
||||
func NewScheduler(store *storage.Store, jobs *[]domain.Job, onChange func(domain.RunRecord)) *Scheduler {
|
||||
// NewScheduler shares the durable jobs slice and the transient runtime map with
|
||||
// the GUI. Both still point at the same in-memory state for now; Phase 3 moves
|
||||
// ownership behind an application service.
|
||||
func NewScheduler(store *storage.Store, jobs *[]domain.Job, runtimes map[int]*domain.JobRuntime, onChange func(domain.RunRecord)) *Scheduler {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s := &Scheduler{
|
||||
store: store,
|
||||
jobs: jobs,
|
||||
runtimes: runtimes,
|
||||
onChange: onChange,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
@@ -42,6 +47,18 @@ func NewScheduler(store *storage.Store, jobs *[]domain.Job, onChange func(domain
|
||||
return s
|
||||
}
|
||||
|
||||
// runtimeFor returns the runtime state for a job, lazily creating it if the map
|
||||
// has no entry yet. This keeps the scheduler robust if a job is added to the
|
||||
// shared slice without a matching runtime.
|
||||
func (s *Scheduler) runtimeFor(job *domain.Job) *domain.JobRuntime {
|
||||
runtime, ok := s.runtimes[job.ID]
|
||||
if !ok || runtime == nil {
|
||||
runtime = domain.NewRuntime(*job)
|
||||
s.runtimes[job.ID] = runtime
|
||||
}
|
||||
return runtime
|
||||
}
|
||||
|
||||
func (s *Scheduler) Start() {
|
||||
// A one-second ticker is accurate enough for cron-style desktop automation
|
||||
// and avoids the complexity of maintaining one timer per job. Five-field cron
|
||||
@@ -75,15 +92,16 @@ func (s *Scheduler) SetPaused(paused bool) {
|
||||
// understandable even before the next scheduler tick.
|
||||
for index := range *s.jobs {
|
||||
job := &(*s.jobs)[index]
|
||||
runtime := s.runtimeFor(job)
|
||||
if !job.Enabled {
|
||||
job.NextRun = "Paused"
|
||||
runtime.NextRun = "Paused"
|
||||
continue
|
||||
}
|
||||
if paused {
|
||||
job.NextRun = "Scheduler paused"
|
||||
runtime.NextRun = "Scheduler paused"
|
||||
continue
|
||||
}
|
||||
s.prepareNextRun(job, now)
|
||||
s.prepareNextRun(job, runtime, now)
|
||||
}
|
||||
_ = s.store.SaveJobs(*s.jobs)
|
||||
}
|
||||
@@ -109,16 +127,17 @@ func (s *Scheduler) RefreshSchedule(index int) {
|
||||
return
|
||||
}
|
||||
job := &(*s.jobs)[index]
|
||||
runtime := s.runtimeFor(job)
|
||||
s.parseJobSchedule(job) // re-parse in case the schedule string changed
|
||||
if !job.Enabled {
|
||||
job.NextRun = "Paused"
|
||||
runtime.NextRun = "Paused"
|
||||
return
|
||||
}
|
||||
if s.paused {
|
||||
job.NextRun = "Scheduler paused"
|
||||
runtime.NextRun = "Scheduler paused"
|
||||
return
|
||||
}
|
||||
s.prepareNextRun(job, time.Now())
|
||||
s.prepareNextRun(job, runtime, time.Now())
|
||||
}
|
||||
|
||||
func (s *Scheduler) tick(now time.Time) {
|
||||
@@ -128,7 +147,8 @@ func (s *Scheduler) tick(now time.Time) {
|
||||
if !s.paused {
|
||||
for index := range *s.jobs {
|
||||
job := &(*s.jobs)[index]
|
||||
if !job.Enabled || job.NextDue.IsZero() || now.Before(job.NextDue) {
|
||||
runtime := s.runtimeFor(job)
|
||||
if !job.Enabled || runtime.NextDue.IsZero() || now.Before(runtime.NextDue) {
|
||||
continue
|
||||
}
|
||||
// Run only one due job per tick for now. That avoids overlapping shell
|
||||
@@ -145,15 +165,16 @@ func (s *Scheduler) tick(now time.Time) {
|
||||
|
||||
func (s *Scheduler) startRunLocked(index int, trigger string) bool {
|
||||
job := &(*s.jobs)[index]
|
||||
if job.LastState == "Running" {
|
||||
runtime := s.runtimeFor(job)
|
||||
if runtime.LastState == "Running" {
|
||||
return false
|
||||
}
|
||||
|
||||
jobCopy := *job
|
||||
job.LastState = "Running"
|
||||
job.NextRun = "Running"
|
||||
job.Output = runningOutput(jobCopy, trigger, time.Now())
|
||||
job.NextDue = time.Time{}
|
||||
runtime.LastState = "Running"
|
||||
runtime.NextRun = "Running"
|
||||
runtime.Output = runningOutput(jobCopy, trigger, time.Now())
|
||||
runtime.NextDue = time.Time{}
|
||||
_ = s.store.SaveJobs(*s.jobs)
|
||||
|
||||
go func() {
|
||||
@@ -161,14 +182,15 @@ func (s *Scheduler) startRunLocked(index int, trigger string) bool {
|
||||
|
||||
s.mu.Lock()
|
||||
if current := s.findJobByIDLocked(jobCopy.ID); current != nil {
|
||||
current.LastRun = record.Time
|
||||
current.LastState = record.State
|
||||
current.Output = record.Output
|
||||
current.Logs = append([]domain.RunRecord{record}, current.Logs...)
|
||||
if len(current.Logs) > 50 {
|
||||
current.Logs = current.Logs[:50]
|
||||
currentRuntime := s.runtimeFor(current)
|
||||
currentRuntime.LastRun = record.Time
|
||||
currentRuntime.LastState = record.State
|
||||
currentRuntime.Output = record.Output
|
||||
currentRuntime.Logs = append([]domain.RunRecord{record}, currentRuntime.Logs...)
|
||||
if len(currentRuntime.Logs) > 50 {
|
||||
currentRuntime.Logs = currentRuntime.Logs[:50]
|
||||
}
|
||||
s.prepareNextRun(current, time.Now())
|
||||
s.prepareNextRun(current, currentRuntime, time.Now())
|
||||
_ = runner.CleanupLogs(s.store.Paths.LogsDir, s.store.Config.MaxLogFiles, s.store.Config.MaxLogAgeDays)
|
||||
_ = s.store.SaveJobs(*s.jobs)
|
||||
}
|
||||
@@ -210,12 +232,13 @@ func runningOutput(job domain.Job, trigger string, started time.Time) string {
|
||||
func (s *Scheduler) resetNextRuns(now time.Time) {
|
||||
for index := range *s.jobs {
|
||||
job := &(*s.jobs)[index]
|
||||
runtime := s.runtimeFor(job)
|
||||
s.parseJobSchedule(job) // parse once on load
|
||||
if !job.Enabled {
|
||||
job.NextRun = "Paused"
|
||||
runtime.NextRun = "Paused"
|
||||
continue
|
||||
}
|
||||
s.prepareNextRun(job, now)
|
||||
s.prepareNextRun(job, runtime, now)
|
||||
}
|
||||
_ = s.store.SaveJobs(*s.jobs)
|
||||
}
|
||||
@@ -232,13 +255,13 @@ func (s *Scheduler) parseJobSchedule(job *domain.Job) {
|
||||
s.schedules[job.ID] = sched
|
||||
}
|
||||
|
||||
func (s *Scheduler) prepareNextRun(job *domain.Job, from time.Time) {
|
||||
func (s *Scheduler) prepareNextRun(job *domain.Job, runtime *domain.JobRuntime, from time.Time) {
|
||||
sched, ok := s.schedules[job.ID]
|
||||
if !ok {
|
||||
job.NextRun = "Invalid schedule"
|
||||
job.NextDue = time.Time{}
|
||||
runtime.NextRun = "Invalid schedule"
|
||||
runtime.NextDue = time.Time{}
|
||||
return
|
||||
}
|
||||
job.NextDue = sched.Next(from)
|
||||
job.NextRun = job.NextDue.Format("2006-01-02 15:04:05")
|
||||
runtime.NextDue = sched.Next(from)
|
||||
runtime.NextRun = runtime.NextDue.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user