Queues and scheduling
The queue is Redis (QUEUE_CONNECTION=redis), run by Laravel Horizon. Start every lane with one
command:
ddev artisan horizonLanes and supervisors
Section titled “Lanes and supervisors”From config/horizon.php, the same in every environment:
| Supervisor | Queues, in order | Processes | Worker memory | Timeout | Tries |
|---|---|---|---|---|---|
imports-and-default |
imports, then default |
2 | 2048 MB | 3600 s | 1 |
media |
media |
3 | 1024 MB | 600 s | 1 |
The split exists so that work people are waiting on never queues behind photos. imports comes before
default on its supervisor, so an Add to catalog run is picked up first.
Things that must stay true when you change these numbers:
retry_aftermust outlast the longest timeout. Theredisconnection’s is 3700 seconds (config/queue.php,REDIS_QUEUE_RETRY_AFTER). If a job runs longer thanretry_after, a second worker starts it too.- A job’s own
$triesand$timeoutbeat the supervisor’s, but a timeout must still stay underretry_after. - Worker memory is a restart threshold. Horizon restarts a worker that passes it. An Add to catalog part on the largest feed peaks around 1.2 GB, which is why the first lane allows 2048 MB. The catalog’s photo pipeline uses ImageMagick, whose memory sits outside PHP’s limit, so check the server’s memory before adding media workers.
memory_limit(128 MB) is the Horizon master’s own. The master exits when it passes this limit, so a value below what it uses makes it restart forever.
The waits setting flags a queue as waiting too long: 300 seconds for imports and default, 900
for media.
The dashboard is at /horizon, behind Horizon::auth() and the admin.queues capability, which no
role lists: super admins only, in every environment. The panel doesn’t link to it.
The assistant’s own connection
Section titled “The assistant’s own connection”config/queue.php has a separate assistant connection with its own queue, a blocking pop and a
330-second retry_after. WebMoves\Assistant\Jobs\RunTurn runs there, so a chat turn starts at
once. Horizon does not work this connection, so it needs a worker of its own. DDEV starts one
(assistant-worker in .ddev/config.assistant.yaml), with one try: a failed turn is reported in
the chat, not re-run.
Everything goes on default unless listed here. Spatie media library conversions follow
MEDIA_QUEUE, set to media.
| Job | Queue | Timeout | Notes |
|---|---|---|---|
Catalog\Jobs\AddToCatalog |
imports |
3600 s | One try; reports to Background tasks |
Catalog\Jobs\DownloadSupplierMedia |
media |
600 s | Supplier photos and documents; unique |
Catalog\Jobs\RunSupplierSync |
default |
3600 s | One per feed at a time, one try; a failed run is recorded, not retried |
Catalog\Jobs\RefreshFromPrices |
default |
1800 s | One try; reports to Background tasks |
Catalog\Jobs\ApplyDecorationSheets |
default |
1800 s | One try; reports to Background tasks |
Catalog\Jobs\ClearSupplierStock |
default |
1800 s | One try |
Catalog\Jobs\RegenerateProductPhotos |
default |
900 s | Remakes photo sizes, in chunks |
Catalog\Jobs\RebuildProductSearch |
default |
1800 s | Unique |
Catalog\Jobs\SyncDecorationMethodLinks |
default |
600 s | Unique |
Catalog\Jobs\SyncProductSearch, SyncTermProductsSearch, PushProductSearchSettings |
default |
Supervisor’s | Unique |
ContentManagement\Sites\Jobs\CascadeReindexSubtreeJob, RebuildSiteDocumentsForTypeJob, IndexSiteDocumentJob |
default |
Supervisor’s | Unique |
ContentManagement\Sites\Jobs\ReindexSiteDocumentsForTargetsJob, RegenerateSiteMediaJob |
default |
Supervisor’s | |
ContentManagement\Bulk\BulkOperationJob |
default |
Supervisor’s | One try: an operation may not be idempotent |
ContentManagement\Widgets\Reviews\RefreshReviews |
default |
Supervisor’s | |
Automations\Jobs\RunAutomationRule |
default |
Supervisor’s | Three tries |
App\Jobs\UseUpscaledPhotos |
default |
3600 s | One try |
Class names are under WebMoves\, apart from the App\ one. A few listeners and notifications also
queue: SendQuoteRequestNotifications, NotifyRepOfProofDecision, ProofDecisionNotice and
RuleNotice.
In tests the queue is sync, so these run inline unless the test calls Queue::fake().
The scheduler
Section titled “The scheduler”There is no central schedule. Each module adds its entries in its service provider, with
callAfterResolving(Schedule::class, ...). Production runs schedule:run every minute. Under DDEV
nothing runs it unless you start ddev artisan schedule:work. ddev artisan schedule:list shows
the current list. Times are in the app’s timezone (APP_TIMEZONE, UTC by default).
| When | Task | Module | What it does |
|---|---|---|---|
| Every minute | automations:run-due |
automations | Starts delayed automation rules that are due. Without overlapping |
| Every 5 minutes | cms:publish-due |
content-management | Reindexes content whose publish time has arrived. Without overlapping |
| Every 5 minutes | catalog:run-due-syncs |
catalog | Starts the feed sync that is due, one at a time. Without overlapping |
| Every 5 minutes | horizon:snapshot |
admin | Feeds Horizon’s graphs |
| Hourly | cms:refresh-reviews |
content-management | Fetches the reviews widgets’ reviews. Without overlapping |
| Hourly | quote-cart:recover |
sales | Marks stale carts abandoned and emails recovery nudges |
| Hourly | inspire |
root | Laravel’s stock example in routes/console.php. Does nothing useful |
| Daily 00:00 | model:prune for BackgroundTask |
admin | By Settings > Housekeeping |
| Daily 00:00 | model:prune for ToolCall |
assistant | The assistant’s tool-call log |
| Daily 00:00 | model:prune for AutomationRun |
automations | By Settings > Housekeeping |
| Daily 00:00 | quote-cart:prune |
sales | Hard-deletes anonymous abandoned carts and unattached artwork uploads |
| Daily 01:00 | sales:fetch-exchange-rates |
sales | Rates for the currencies sites use |
| Daily 03:30 | catalog:prune-sync-inputs |
catalog | Deletes old sync inputs by Settings > Housekeeping |
| Daily 03:45 | activity log cleanup | admin | activitylog:clean by Settings > Housekeeping. Skipped when that setting is blank |
| Daily 06:00 | sales:mark-overdue-invoices |
sales | Stamps overdue invoices once and announces it |
catalog:run-due-syncs decides from each feed’s own settings, in the timezone CATALOG_SYNC_TIMEZONE
(Europe/London by default). See Syncing.
Background tasks
Section titled “Background tasks”WebMoves\Admin\Models\BackgroundTask is how long-running work that a member of staff started
reports back. It is what they see on System > Background tasks
(guide), and it isn’t tied to the queue system. A job holds the
task’s id and reports as it goes:
$task = BackgroundTask::start($user, 'catalog.add_to_catalog', 'Add 40 products to Pens', total: 40);SomeJob::dispatch($task->id, ...); // and then, in the job:
$task->running();$task->advance(); // one item done$task->itemFailed('QS0345 DUO', 'No name'); // one item failed, counted as processed$task->finish('38 products added, 2 failed'); // done$task->fail($exception); // or the whole job failedFinishing or failing sends whoever started it a database notification (the bell) with a link to the
task. A task can have no user, in which case nobody is notified: catalog:add-supplier works that
way. Done and failed tasks are pruned by age, queued and running ones never are. AddToCatalog,
ApplyDecorationSheets and RefreshFromPrices use it today.
Staff see every task, and nobody can create, edit or delete one by hand (BackgroundTaskPolicy).
Use a background task when a person starts something and wants to know how it went. Don’t use it for system work no one is waiting on.