Feature 20: Scheduler and Weekly Automation (#182)
CI / ci (push) Successful in 8m28s

Co-authored-by: dev_agent <dev_agent@cubecraftlabs.com>
This commit was merged in pull request #182.
This commit is contained in:
2026-10-10 19:30:23 -04:00
committed by approver_agent
parent 4dcfc3c27d
commit ecf7c6be89
39 changed files with 4457 additions and 228 deletions
+14 -5
View File
@@ -138,6 +138,9 @@ type Service struct {
grounder *recipes.Grounder
logger *slog.Logger
groundJob *jobs.Kind[GroundJob]
// wake is called after every committed change to a dinner's method or
// minutes, which can move its crock-pot start reminder.
wake func()
// groundingsMu guards groundings: the KindGroundDinner jobs running,
// by dinner id, so archiving or renaming a dinner can stop them.
@@ -157,10 +160,11 @@ type Service struct {
offerSearches map[string]map[*offerSearch]struct{}
}
// NewService returns a service over st that grounds dinners with grounder
// and registers the KindGroundDinner job on runner, which must not have
// started yet.
func NewService(st *store.Store, grounder *recipes.Grounder, runner *jobs.Runner, logger *slog.Logger) (*Service, error) {
// NewService returns a service over st that grounds dinners with grounder,
// calls wake (the scheduler's Wake) after every committed change to a
// dinner's method or minutes, and registers the KindGroundDinner job on
// runner, which must not have started yet.
func NewService(st *store.Store, grounder *recipes.Grounder, runner *jobs.Runner, wake func(), logger *slog.Logger) (*Service, error) {
switch {
case st == nil:
return nil, errors.New("dinners: nil store")
@@ -168,10 +172,12 @@ func NewService(st *store.Store, grounder *recipes.Grounder, runner *jobs.Runner
return nil, errors.New("dinners: nil grounder")
case runner == nil:
return nil, errors.New("dinners: nil job runner")
case wake == nil:
return nil, errors.New("dinners: nil wake")
case logger == nil:
return nil, errors.New("dinners: nil logger")
}
s := &Service{st: st, grounder: grounder, logger: logger, groundings: make(map[string]map[*runningGrounding]struct{}), offerKey: make([]byte, 32), offers: make(map[string]*offerSet), offerSearches: make(map[string]map[*offerSearch]struct{})}
s := &Service{st: st, grounder: grounder, wake: wake, logger: logger, groundings: make(map[string]map[*runningGrounding]struct{}), offerKey: make([]byte, 32), offers: make(map[string]*offerSet), offerSearches: make(map[string]map[*offerSearch]struct{})}
rand.Read(s.offerKey) // never fails: it crashes the program instead
kind, err := jobs.Register(runner, KindGroundDinner, s.runGroundJob, Reasons...)
if err != nil {
@@ -502,6 +508,9 @@ func (s *Service) Update(ctx context.Context, id string, p Patch, updatedBy stri
if err != nil {
return Dinner{}, err
}
if p.Method != nil || p.MinutesTotal != nil {
s.wake()
}
if renamed {
updateCommitted()
s.clearOffers(row.ID, row.GroundingRevision)
+3
View File
@@ -196,6 +196,7 @@ func (s *Service) runGroundJob(ctx context.Context, p GroundJob, _ func(groundRe
if n == 0 {
return s.currentGrounding(ctx, row.ID)
}
s.wake()
return groundResult{DinnerID: row.ID, GroundingStatus: GroundingGrounded}, nil
}
@@ -577,6 +578,7 @@ func (s *Service) Ground(ctx context.Context, id string, f Fix, updatedBy string
if err != nil {
return Dinner{}, fmt.Errorf("%w: dinner %s: record: %w", ErrGroundFailed, id, err)
}
s.wake()
s.clearOffers(cur.ID, math.MaxInt64)
return fromRow(row), nil
}
@@ -700,6 +702,7 @@ func (s *Service) groundManual(ctx context.Context, id string, m ManualRecipe, u
if err != nil {
return Dinner{}, fmt.Errorf("%w: dinner %s: %w", ErrGroundFailed, id, err)
}
s.wake()
s.clearOffers(row.ID, math.MaxInt64)
return fromRow(row), nil
}
+1 -1
View File
@@ -104,7 +104,7 @@ func newUnstartedEnvOn(t *testing.T, st *store.Store, dataDir string) *env {
if err != nil {
t.Fatal(err)
}
svc, err := dinners.NewService(st, g, runner, logger)
svc, err := dinners.NewService(st, g, runner, func() {}, logger)
if err != nil {
t.Fatal(err)
}