Background work: queues and cron
Queues run work outside the request that created it, so a slow job does not block a page. Cron runs tasks on a schedule. Reach for a queue when a request starts work that can finish later; reach for cron when work recurs on its own.
Which one to use
- A queue when a request needs to trigger work that can complete afterwards. Sending an email is the standard case: the request records the email and returns; the queue delivers it.
- Cron when work recurs on a clock rather than in response to a request, such as expiring old sessions or refreshing a cache every minute.
Registering a lock-based queue
A module registers a queue in its index file with
QueueConfig::AddQueue. You give the module name, a queue name, a rate
(max processes per second) and a ceiling (max in flight), and two callbacks: one
that locks and returns the next item ids, and one that processes a single item:
QueueConfig::AddQueue(
'Messaging',
'email',
10, // max processes per second
100, // max in flight
'MessagingEmailQueueDatabase::LockNext',
'MessagingEmailQueueDatabase::Process'
);
The queue is lock based. LockNext claims rows with
SELECT ... FOR UPDATE SKIP LOCKED so two workers never take the same
item, and Process runs inside a transaction and must update the row so
it no longer matches the lock query. If Process throws, the
transaction rolls back and the item returns to the queue. The same callback that
locks the next ids is used both to count what is waiting (with a limit of 100) and
to claim one item to run (with a limit of 1). Enqueuing is a plain insert: the code
above is exactly how Messaging wires up its email queue, and
MessagingEmail::Send enqueues by inserting an unsent row.
Registration only records the queue; the work of draining it is exposed as HTTP
endpoints by a separate QueueApi module, and the framework polls the
registered set on its own cadence. Because the whole loop is PHP running inside the
web process, this queue suits ordinary application work: sending a message, resizing
an image, updating a counter.
The lease-based task queue
For heavier workloads that a PHP request should not run, the module also offers a
lease-based task queue, registered with QueueTaskConfig::AddQueue. Here
an external worker (for example a GPU fleet) leases an item, does the work, and
reports back. Instead of a lock-and-process pair, you supply callbacks for the
stages of a lease: one that lists pending candidates, one that builds the payload a
worker needs, and separate callbacks that apply a result, a failure, or progress:
QueueTaskConfig::AddQueue(
'Clip',
'thumbnail',
120, // reassign after this many seconds if a worker goes quiet
3600, // overall timeout for one item
'ClipThumbnailQueue::Pending',
'ClipThumbnailQueue::Payload',
'ClipThumbnailQueue::ApplyResult',
'ClipThumbnailQueue::ApplyFailure'
);
An item is leased for a fixed window. If the worker does not report back within the
reassign window it is handed to another worker, and the overall timeout caps how long
one item may take before it is treated as failed. The registration shape mirrors the
lock-based queue; the difference is that the worker lives outside the web process and
talks to the queue over the QueueApi task endpoints. Choose this form when
the work is too slow or too specialised to run inside a request.
Registering a cron task
A module registers a recurring task with CronConfig::Minute, giving
the module name, a task name, and a callback to run:
CronConfig::Minute(
'MyModule',
'refresh',
'MyModuleCrons::Refresh'
);
The callback is an ordinary static method:
final class MyModuleCrons {
static function Refresh(): void {
// recurring work goes here
}
}
Registered tasks are driven by Cron::Run, called from the cron entry
point once a minute. It walks every task registered with
CronConfig::Minute and runs each one through
Cron::RunCallback, which records a log row when a task starts and closes
it when the task finishes, capturing any output and any error. That log is how you see
when a task last fired and whether it threw, so a task that fails quietly is still
visible.
Next: Caching and locks.