Files
gosentry/src/scheduler/scheduler.go
T
mixeme a4c93a5122 T3.4: Convert scheduler to drive app.Service; inject Clock
The scheduler no longer shares a *[]domain.Job with the GUI. It is now a
thin timing loop with an injected Clock that calls a tick callback; the
application service is the sole writer of job and runtime state.

- scheduler: add Clock interface + RealClock (clock.go); strip all job
  logic from scheduler.go (NewScheduler(clock, tick)); rewrite tests to
  cover the loop with a fake clock.
- app.Service: add RunDue(now) (pause + one-run-per-tick policy, records
  back through the service) and Start(Clock)/Stop() owning a cancelable
  run context; prime each job's first next-run at construction. Capture
  the run context under the lock for executeRun.
- gui: talk only to app.Service (no shared state) — Open() the service,
  keep a refreshed snapshot, route every mutation through the service,
  and react to changes via a single Subscribe listener.
- Tests: add RunDue (due/not-due/paused) and Start-drives-RunDue cases.

Verified with CGO + MSYS2 UCRT64: go vet ./... clean, go test -race
./... green (GUI included), full module builds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 08:22:35 +03:00

57 lines
1.5 KiB
Go

package scheduler
import (
"context"
"time"
)
// Scheduler is a thin timing loop. It owns no job or runtime state: on every
// clock tick it calls the injected tick function with the current time, and that
// function — the application service's RunDue — decides what, if anything, to
// run. Keeping all state and mutation in the service makes the service the sole
// writer (resolving the old shared-*[]Job data race) and reduces the scheduler
// to a loop that is trivially testable with a fake Clock.
type Scheduler struct {
clock Clock
tick func(now time.Time)
ctx context.Context
cancel context.CancelFunc
}
// NewScheduler builds a scheduler that calls tick on every Clock tick. The clock
// is injected so tests can drive the loop without the wall clock.
func NewScheduler(clock Clock, tick func(now time.Time)) *Scheduler {
ctx, cancel := context.WithCancel(context.Background())
return &Scheduler{
clock: clock,
tick: tick,
ctx: ctx,
cancel: cancel,
}
}
// Start launches the loop on its own goroutine and returns immediately.
func (s *Scheduler) Start() {
go func() {
ticks := s.clock.Ticks()
defer s.clock.Stop()
for {
select {
case <-s.ctx.Done():
return
case <-ticks:
// Pass the clock's notion of "now" rather than the tick value so a
// fake clock can control due-evaluation precisely.
s.tick(s.clock.Now())
}
}
}()
}
// Stop ends the loop. A tick already in progress finishes; no further ticks are
// delivered.
func (s *Scheduler) Stop() {
s.cancel()
}