feat(11-02): run plugin schedules as River periodic jobs through bonfire.Call

- pact.HasSchedule with ScheduledCommand and Daily/DailyAt/Every cadences (no River import)
- bonfire.Call, Catalog and ErrUnknownCommand for in-process command runs
- conga Daily/Every wall-clock schedules in app.timezone, periodic jobs on every worker,
  scheduled queue (MaxAttempts 1, unique by args within the cadence period)
- scheduled worker runs only entries matching the compiled table; unregistered
  commands are skipped with a Warn log
- generated app main publishes bonfire.NewCatalog(commands); hello main regenerated
This commit is contained in:
Jakub Zych
2026-09-29 19:47:51 +02:00
parent 77b8ff177a
commit d9f939a1ea
15 changed files with 890 additions and 12 deletions

View File

@@ -55,6 +55,11 @@ func (w *Worker) Queues() []string {
// from database.dsn, so jobs committed by any process are picked up without
// waiting for the poll interval. The worker keeps running after ctx is
// cancelled; stop it with Worker.Stop.
//
// Every worker also carries the River periodic jobs of plugins'
// pact.HasSchedule entries (only the elected leader enqueues them) and runs
// the built-in scheduled command job on QueueScheduled when that queue is
// selected. An invalid schedule entry or app.timezone fails the start.
func StartWorker(ctx context.Context, app *backpack.App, plugins []party.Plugin, o WorkerOptions) (*Worker, error) {
m, err := From(app)
if err != nil {
@@ -77,13 +82,23 @@ func StartWorker(ctx context.Context, app *backpack.App, plugins []party.Plugin,
}
s := settingsFromApp(app)
log := loggerFromApp(app)
periodic, table, err := periodicJobs(app, plugins)
if err != nil {
return nil, err
}
m.mu.Lock()
defer m.mu.Unlock()
if m.worker != nil {
return nil, fmt.Errorf("conga: a worker is already running")
}
if err := m.registerLocked(m.scheduledJobLocked()); err != nil {
return nil, fmt.Errorf("conga: register scheduled command job: %w", err)
}
known := knownQueues(s, m.jobs)
if _, configured := s.queues[QueueScheduled]; !configured {
known[QueueScheduled] = scheduledMaxWorkers
}
queues, err := selectQueues(known, o.Queues)
if err != nil {
return nil, err
@@ -103,6 +118,9 @@ func StartWorker(ctx context.Context, app *backpack.App, plugins []party.Plugin,
}
cfg := baseConfig(s, log)
cfg.Workers = workers
// Every worker carries the periodic jobs whatever its queue filter:
// only the elected leader enqueues them.
cfg.PeriodicJobs = periodic
cfg.Queues = map[string]river.QueueConfig{}
for name, n := range queues {
cfg.Queues[name] = river.QueueConfig{MaxWorkers: n}
@@ -135,6 +153,7 @@ func StartWorker(ctx context.Context, app *backpack.App, plugins []party.Plugin,
return nil, fmt.Errorf("conga: start worker: %w", err)
}
m.worker = client
m.schedule = table
return &Worker{m: m, client: client, listener: listener, queues: sortedQueueNames(queues)}, nil
}