feat(13-01): queue jobs whose worker ships later while a worker runs

- a kind no plugin registered always inserts through the insert-only River
  client, so Dispatch and Enqueue no longer fail River's unknown-kind check
  while the in-process worker runs
- while a worker runs, such a kind must name a queue no worker serves;
  an empty queue, default, scheduled, a configured queue or a registered
  job's queue is ErrUnregisteredKindQueue and nothing is written
- README and docs/services/jobs.md describe jobs whose worker ships later
This commit is contained in:
Jakub Zych
2026-10-03 06:28:40 +02:00
parent fbdeb20126
commit 55a4092019
4 changed files with 292 additions and 2 deletions

View File

@@ -17,6 +17,7 @@ The scheduler is the Go form of WinterCMS `registerSchedule`. Plugins declare re
- River-free job declarations: `conga.Job` turns `func(ctx context.Context, args T) error` into a `pact.Job`; `conga.OnQueue`, `conga.MaxAttempts` and `conga.Timeout` set per-job defaults. A `pact.Job` not built by `conga.Job` is rejected with `conga.ErrNotCongaJob`.
- Transactional dispatch: `conga.Manager.Dispatch` inserts the `summer_jobs` row with `conga.StatusInProgress`, the principal's user id and admin flag, `progress_max` from `conga.DispatchOpts.Count` and JSON metadata, then enqueues the River job in the same transaction. It opens a transaction itself when the caller has none.
- Plain enqueue: `conga.Manager.Enqueue` inserts a River job without a record row, inside the caller's transaction when there is one.
- Jobs whose worker ships later: a kind that no plugin registers is always inserted through the insert-only River client, so `conga.Manager.Dispatch` and `conga.Manager.Enqueue` succeed while a worker runs and the job waits on its queue. While a worker runs, such a kind must name a queue no worker serves; an empty queue, `default`, `conga.QueueScheduled`, a configured queue or a registered job's queue is `conga.ErrUnregisteredKindQueue`, because a worker would fetch the job, find no worker for its kind and discard it.
- The record row: `conga.Record` maps `summer_jobs`; `conga.Status` holds the WinterCMS status values (`conga.StatusInQueue`, `conga.StatusInProgress`, `conga.StatusComplete`, `conga.StatusError`, `conga.StatusStopped`). `conga.JobID` gives a running job its own row id.
- The WinterCMS job manager operations with the same semantics: `conga.Manager.StartJob`, `conga.Manager.UpdateJobState`, `conga.Manager.UpdateMetadata`, `conga.Manager.CompleteJob`, `conga.Manager.FailJob`, `conga.Manager.CheckIfCanceled` and `conga.Manager.GetMetadata`. Updates are raw column writes, so `updated_at` changes only on dispatch and `conga.Manager.StartJob`.
- Cancellation in two parts: `conga.Manager.CancelJob` is the outside cancel (sets `is_canceled` and `conga.StatusStopped`, then cancels the River job, so a queued job never starts and a running job's context is cancelled); `conga.Manager.StopJob` is what a job calls on its own row after `conga.Manager.CheckIfCanceled` reports true (status only, the WinterCMS `cancelJob`).
@@ -112,6 +113,21 @@ func (p *Plugin) Schedule() []pact.ScheduledCommand {
}
```
A job whose worker ships in a later release can already be queued from a request, on a queue that nothing serves yet. It waits there until a release registers its worker and the queue becomes served:
```go
// No plugin registers acme.reindex yet; its queue is served by nobody.
_, err := m.Dispatch(ctx, tx, ReindexArgs{SiteID: 7}, conga.DispatchOpts{
Label: "acme.reindex",
Queue: "acme_reindex",
})
if errors.Is(err, conga.ErrUnregisteredKindQueue) {
// the queue is empty or a worker serves it
}
```
Queue names follow River's rule: lowercase letters and digits, separated by `_` or `-`.
A worker runs in the same process or in a separate one:
```go
@@ -161,6 +177,7 @@ defer w.Stop(context.Background())
| `conga.ErrNotCongaJob` | A registered `pact.Job` was not built by `conga.Job`. |
| `conga.ErrRegistrationClosed` | Registration was attempted while a worker runs. |
| `conga.ErrUnknownQueue` | A worker was asked for a queue nothing names. |
| `conga.ErrUnregisteredKindQueue` | While a worker runs, a kind no plugin registered was dispatched or enqueued with no queue or onto a queue a worker serves. |
## Configuration