Vai al contenuto
Vai al contenuto
Navigazione documentazione

Questa pagina non è ancora disponibile nella lingua scelta. È mostrata la versione EN.

Queues for k0smos

This is the canonical operator guide for the k0smos queue subsystem.

Shipped answer: one tenant normally needs two worker processes: one for the default logical queue and one for notifications. k0smos has one queue subsystem and two alternative transports. Changing from database to Redis does not create another logical queue or require another worker.

Queue workers capture the tenant theme, default locale and active modules at startup. After a tenant theme or default-locale change, gracefully restart its workers so subsequent email and jobs use the new effective snapshot. In-flight jobs finish with the snapshot they started with.

Run the read-only diagnostic before changing a deployment:

php bin/console queue:topology --host=k0smos.example.com
php bin/console queue:topology --host=k0smos.example.com --format=json

--host selects the tenant to inspect. --format=json only changes standard output from the human-readable table to a machine-readable document; it does not select or contact a server. Use it from deployment scripts, CI, monitoring, or configuration management to read the expected process count, systemd unit names, heartbeat state, and diagnostics without parsing terminal tables.

The command reads the selected tenant configuration, active modules, DB-backed notification settings, producer metadata, and worker heartbeats. It does not expose passwords, tokens, payloads, or other secrets.

Worker Count Rule

Run one process for every distinct effective logical queue that has an asynchronous producer:

worker processes per tenant = distinct logical queue names with async producers

Several job or message types sharing one queue still need only one process. Two slots consuming the same busy queue are two processes for capacity, not a third logical queue.

Shipped topology

Logical queue Current producers Why it is separate No-worker behavior Required process
default Contact mail configured as async, payment webhooks, advanced AI unless overridden, Deadline messages, Firefly III, Psapi, Wpapi and Data Exchange imports, optional Brainstorming discussions Shared application lane; the workloads use the same retry and capacity pool Contact, payment, Deadline, and imports run synchronously; advanced AI and Brainstorming reject new work One queue:work --queue=default
notifications Notification email Prevents slow generic/import work from delaying operational email Email runs synchronously and emits a throttled admin warning One queue:work --queue=notifications

The shipped total is therefore two processes per tenant. If notification email is disabled in DB settings, its producer is inactive and the topology command does not require that lane.

When another queue is worth its cost

Candidate Use a dedicated queue when Operational cost
ai Provider/Python work is long-running, needs independent concurrency, or must not compete with imports/payment work Add one environment file, systemd instance, heartbeat, alerts, and capacity decision per tenant
imports Connector batches are large enough to delay payment/contact work or need separate deploy windows Every producer must be configured to dispatch to it; the current Firefly III, Psapi, and Wpapi launchers intentionally use default
Custom notification lane Notification delivery has different availability/capacity requirements or a renamed lane is part of an existing convention Update the DB setting and run a worker for that exact name

Prefer default plus notifications until monitoring shows a concrete isolation need. A custom name without a producer is a configuration error, not useful capacity.

Do Not Confuse These Four Layers

Layer What it means Current examples
Queue subsystem API How application code submits work Typed MessageBusInterface (preferred); raw QueueInterface for compatibility/infrastructure
Transport Where jobs and heartbeats are stored Database/DBAL; Redis primary with DBAL failover
Logical queue The named lane one worker consumes default, notifications, or a configured custom name such as ai
Job/message type The payload category handled inside a lane queue.async_message, notification.email.send, deadline.*, typed message classes

Never pass a job type such as queue.async_message, notification.email.send, or deadline.check_reminders to queue:work --queue. The option expects a logical queue name.

Transport Decision

module_config.queue.driver Primary storage Failure behavior Queue names Worker count Choose it when
database (default) Tenant DB tables queue_jobs and queue_worker_heartbeats No secondary transport; producer-specific sync/guard behavior still applies Unchanged Unchanged Normal volume, simplest operations, no Redis dependency
redis Redis structures under the tenant queue prefix Dispatch, reserve, reclaim, and heartbeat fall back to DBAL when Redis resolution/operations fail Unchanged Unchanged Sustained volume or Redis-specific latency/throughput needs justify another service

Redis failover keeps new work operable during an outage; it does not migrate jobs that already exist only in Redis into SQL. Both modes require queue migrations because Redis mode uses the SQL fallback.

Redis provisioning — tenant configuration keys, systemd unit ordering, key namespace, instance requirements, and the transport-mismatch failure mode — is in the systemd worker guide.

Authoritative Producer Inventory

QueueTopologyRegistry is the runtime source for producer metadata used by queue:topology. The registry is filtered by active modules and mutable settings, so documentation is not the deployment source of truth.

Producer id Typed messages Effective queue source Fallback/guard
front.contact_mail SendContactMailMessage Fixed default Synchronous fallback
payment.webhook ProcessWebhookEvent Fixed default Inline processing fallback
notification.email SendNotificationEmailMessage DB notifications.channels.email.queue, default notifications Synchronous fallback
brainstorming.run BrainstormingRunMessage Fixed default, active module only Requires recent worker heartbeat; no inline fallback; fixed-code failure and explicit user retry
ai.advanced GenerateChatReplyMessage, ProcessChatAttachmentMessage ai.advanced.queue, then queue.name, then default Rejects when no worker exists; dispatch errors update failure state
deadline.scheduler GenerateRecurringTicketMessage, CheckDeadlineRemindersMessage Scheduler --queue, default default Synchronous fallback in the CLI process
fireflyiiiapi.import FireflyImportMessage Fixed default Same handler runs synchronously
psapi.import PsapiImportMessage Fixed default Same handler runs synchronously
wpapi.import WpapiImportMessage Fixed default Same handler runs synchronously
site_publish.build SiteBuildMessage Fixed default No synchronous HTTP fallback; frozen inputs wait for the worker or explicit site:build. Failed jobs remain visible and retryable.
data_exchange.import DataExchangeImportMessage Fixed default Same handler runs synchronously; staged payload and durable run survive browser reload

Typed messages are serialized into the transport job type queue.async_message, then QueuedMessageJobHandler resolves the typed handler. Legacy/raw-compatible job types remain consumable:

  • front.contact_form.send_mail
  • notification.email.send
  • deadline.generate_recurring_ticket
  • deadline.check_reminders

Those compatibility types do not add logical queues or processes. Low-level callers that bypass the typed producer guards can still leave raw jobs pending when no worker consumes the selected logical queue; use queue:topology and heartbeat monitoring to prevent that configuration.

Effective Queue-name Sources

Source Applies to Precedence/notes
queue:work --queue One worker process CLI override wins
QUEUE_NAME One worker process Used when CLI does not override it
module_config.queue.name Worker default and AI fallback Does not rename producers that explicitly use default; queue:topology warns when it has no producer
module_config.ai.advanced.queue Advanced AI Falls back to queue.name, then default
notifications.channels.email.queue Notification email DB-backed; defaults to notifications
k0smos:deadline:schedule --queue One scheduler invocation Defaults to default; pass the same value to queue:topology --scheduler-queue

Logical names remain configurable and must match [A-Za-z0-9._-]+. QueueName provides central validation, while QueueTopologyRegistry detects custom names with no consumers and divergences from the shipped AI/notification lanes.

Topology Diagnostics

Text output answers the operator question directly. JSON is stable for deployment automation and contains:

  • selected/primary/fallback transport;
  • effective logical queues and active producers;
  • expected process count;
  • expected worker id and systemd instance for each queue;
  • current active heartbeat identifiers without payloads/secrets;
  • missing workers;
  • duplicate worker ids used on different queues;
  • global worker-id overrides that would make a multi-queue deployment ambiguous;
  • custom worker-default names without discovered producers;
  • custom AI/notification queues and invalid configuration values.

Example worker tuple:

tenant:          k0smos.example.com
logical queue:   default
slot:            1
worker id:       k0smos.example.com.default.1
systemd unit:    k0smos-queue-worker@k0smos.example.com-default-1.service

The old tenant.slot generated id is retired because two queues using slot 1 produced ambiguous logs and heartbeat identifiers.

Start And Supervise Workers

Direct CLI use:

TENANT_ENV=k0smos.example.com php bin/console queue:work --queue=default --worker-slot=1
TENANT_ENV=k0smos.example.com php bin/console queue:work --queue=notifications --worker-slot=1

Copy-ready systemd units, environment files, a two-process setup, scaling with slot 2, deployment order, monitoring, and a per-tenant target are documented in the systemd worker guide.

SIGTERM/SIGINT stop new reservations and let the current handler return. Keep handlers idempotent: retries, stale reservation reclaim, and process termination provide at-least-once rather than exactly-once delivery.

Failure And Dead-letter Behavior

SQL stale reclaim selects only the worker's logical queue before applying its batch limit. Each update also matches the observed reservation timestamp and attempt number: a job reclaimed and reserved by another worker after the scan is skipped, without changing its new lease or increasing the recovered count. A worker's timeout therefore cannot shorten another queue's reservation policy. Reclaim preserves payloads, attempt budgets and errors; normal handler failure still decides retries/dead letters. It does not cancel an old running handler or provide exactly-once side effects.

  • WorkerRestartRequired stops immediately without ack/fail, notifications, heartbeat or another reservation. Payment raises this after an uncertain SQL boundary retires the shared handle. The current job remains reserved; restart in a fresh process and allow normal stale-reservation reclaim. A lost commit reply may already have persisted settlement, so recovery must be idempotent.
  • Worker exceptions retry while attempts remain.
  • Exhausted DBAL jobs stay as queue_jobs.status = 'failed'.
  • Exhausted Redis jobs stay in the Redis failed structures.
  • Definitive worker failures create in-app admin notifications.
  • Producer-side synchronous fallbacks create throttled in-app admin notifications.
  • Processed jobs remain log-only by default.
  • Retry/delete dead-letter administration is a separate operational feature; queue:topology is intentionally read-only.

Owned native diagnostics now exercise this Payment retirement contract through real SQL queues/heartbeats, QueueWorker and the lazy webhook handler: 12 cases per MySQL/PostgreSQL engine, the same private event and reserved job, a genuinely aged lease, fresh physical connection and successful second attempt without duplicate settlement/stock. Private diagnostic queues avoid default content dispatch; an additional older reservation in a different queue genuinely ages alongside it and must remain entirely unchanged during reclaim. Shutdown/notifier observers are local controls. This does not qualify production default routing/fallback, CLI supervision/signals, actual notifier delivery, Redis or provider/HTTP behavior. Missing post-commit events remain best effort. See Ecommerce for the bounded workflow.

Private Customer reset and Notification email producers enter content-exclusion scopes before enqueueing/fallback; their workers resolve mail/render dependencies lazily inside those scopes. Delivery errors reach queues as fixed codes without original SMTP/template exception chains. Sensitive dispatcher failures are replaced before persisted fallback alerts; ordinary producers keep their diagnostics. An inline handler failure escapes after one attempt in that call, while normal transport retry policy remains unchanged. This does not guarantee exactly-once email after uncertain transport or later job retries.

Reset jobs contain customer/locale/scheme only and issue their raw link in the worker. Notification jobs freeze recipient, subject, template context, locale and optional text; rendering happens later. Acknowledgement removes successful jobs, while failed DBAL/Redis storage retains private payloads with no bounded dead-letter purge delivered here. Review queue access and retention separately. These source protections do not rewrite historical logs or qualify external mail/provider/proxy diagnostics.

Glossary

Transport — storage/availability adapter behind the queue contracts.

Logical queue — named worker lane selected by queue:work --queue.

Job type — low-level payload category resolved by JobHandlerRegistry.

Typed message — serializable application message implementing QueueMessageInterface.

Handler — application service that performs the job/message side effect.

Heartbeat — last-seen record for a worker id on one logical queue.

Synchronous fallback — producer executes the same handler in its current process because no worker is active or async dispatch failed.

Retry — a failed reservation returned to pending work while attempts remain.

Dead letter — permanently failed job retained for diagnosis after retry exhaustion.

Environment-dependent Smoke Checks

These require real Redis, real workers, or an isolated container/tenant, so they are not part of the automated suite. Run them in an isolated environment only — never against a runtime tenant database. If a run diverges from the behavior documented here, update README.md, doc/public/en/complete.md, this guide, and the affected changelog before closing the check.

  • Redis queue round-trip integration (skippable when Redis is unavailable): dispatch, reserve, retry, delayed job, and dead-letter.
  • Deadline command on an isolated container/tenant: with no active worker both handlers run inline and log queue.sync_fallback; with a worker active the work is dispatched asynchronously.
  • queue.driver=database with an active worker: advanced AI chat and attachment upload work end to end.
  • queue.driver=database with no worker: advanced AI chat and attachment upload answer with a controlled 503, never a raw 500.
  • queue.driver=redis with Redis stopped mid-session: AI registries and the queue transport fail over to SQL with a logged warning and the chat continues.
  • Deadline CLI on an isolated tenant with no worker: TENANT_ENV=k0smos.example.com php bin/console k0smos:deadline:schedule processes reminders/recurrences inline and exits 0.

queue.async_message is not a queue: it is the job type shared by every typed message, whatever queue carries it. Imports, payment webhooks, contact mail, content changes and Data Exchange run on default; notification email and the shop password reset email run on the email queue (notifications by default). A permanent failure notifies administrators in the inbox with the name of the work, the queue, the attempts and the reason, and links to the work's own page when it has one. queue:topology lists each producer with its queue and page.

Rebuild The Source Inventory

The former static occurrence appendix was retired so it cannot drift into a second architecture document. Generate audit evidence on demand under reports/ instead of committing another operator guide:

mkdir -p reports
rg -n "MessageBusInterface|QueueMessageInterface|MessageDispatchOptions|QueueManagerInterface|QueueWorkerHeartbeatInterface|dispatchOrRun|queue:work|queue\\.driver|queue_jobs|queue_worker_heartbeats" \
  src modules config tests -g '*.php' -g '*.md' -g '*.json' -g '*.service' \
  > reports/queue-occurrences.txt

Stable architectural entry points:

  • src/Domain/Queue/
  • src/Application/Queue/QueueTopologyRegistry.php
  • src/Application/Queue/QueueWorker.php
  • src/Infrastructure/Queue/
  • src/Command/QueueWorkCommand.php
  • src/Command/QueueTopologyCommand.php
  • src/Container/Definition/QueueDefinitions.php

Committed content updates

Canonical Ecommerce/Blog/Page changes first write SQL intent in their source transaction. The content.changes topology entry is drained by the default worker, at most 100 deliveries per five-second poll, with independent consumer checkpoints. Worker absence leaves updates queued; it does not run search providers inside the source HTTP request. Apply maintenance and active-module migrations before enabling these producer changes.

TENANT_ENV=k0smos.example.com php bin/console content:changes
TENANT_ENV=k0smos.example.com php bin/console content:changes --dispatch --limit=100
TENANT_ENV=k0smos.example.com php bin/console content:changes --retry --subscriber=discovery.freshness --limit=100
TENANT_ENV=k0smos.example.com php bin/console content:changes --reconcile=blog:article --limit=100

Recovery advances one durable cursor per source (ecommerce:product, blog:article, page:page when active). Repeat the recovery command until its JSON reports complete: true, then drain delivery. Use it after missed events, source imports or consumer activation. Each pass replays current IDs, including unpublished records, then tombstones missing tracked IDs. It never changes source content. Five failures quarantine a delivery; explicit bounded retry preserves active worker leases. Discovery offers the same operations for its subscriber in the backoffice; canonical replay additionally checks the source owner's edit permission. The core contract and external indexing limitations are in src/Application/ContentChange/AI.ContentChange.md.

Discovery maintenance

Handler registration follows active owners: Ai reply/attachment consumers require Ai, native payment webhook handling requires Ecommerce, and Discovery consumers require Discovery. A tenant without Ecommerce can resolve the shared default worker without catalog/payment checkout dependencies. The isolated 20-combination Ai/Discovery/source matrix checks handler, job registry and worker resolution.

Discovery's delivery tests also execute registered bounded CLI actions and a message through the default SQL queue's serialization/reserve/handle/ack cycle. That queue case verifies an unavailable Media source fails safely; successful binary extraction is covered separately. Psapi/Wpapi import fixtures verify committed outbox delivery creates pending Discovery work while queue dispatch is unavailable and preserves local editorial ownership. Those checks do not measure production queue delay or a complete import-to-vector round trip.

When Discovery is active, discovery.maintenance queues identity-only coalesced wakeups on default. A handler advances one bounded SQL-owned phase and may schedule a continuation, up to 256 steps. Phase ownership and child work use conditional leases; duplicate wakeups cannot execute the same claim. External embedding/Qdrant calls and Office/PDF parsing never run inside an owner SQL transaction. Document jobs remain on the existing discovery.documents producer; maintenance redispatches one due current job after lost delivery, without parsing inside the maintenance step.

Schedule discovery:work --advance --steps=100 --seconds=45 periodically in addition to the default worker. It recovers lost wakeups, future retries, expiry/retention and recurring immutable-point deletion. The time bound prevents starting another step; a step already in progress retains its own provider timeout. Pausing stops new projection work but retains cleanup, and retired generations remain in the recovery keyset. Exhausted projections require reconciliation; expired final document attempts remain available for explicit retry. These operations never automatically switch a serving generation or approve AI relations.

Brainstorming worker requirements

Brainstorming uses a maximum 120-second provider budget and a renewable 30-second SQL run lease. Set queue:work --reclaim-after=180 or a longer value for the default lane; transport reservation expiry does not renew with the module lease. Identity-only duplicate deliveries cannot claim an already running or terminal attempt. No provider invocation is replayed automatically. A shared default worker runs addressed agents sequentially; adding a second default worker slot permits independent CLI types to run concurrently. Host locks serialize discussions for the same OS identity and CLI type across tenants; a conflicting attempt fails busy and can be retried explicitly.

Use the worker's OS identity, HOME and actual executable paths when checking native CLI authentication. Discussion execution requires Linux process groups and whole-service descendant cleanup after an uncatchable kill (systemd KillMode=control-group or an equivalent supervisor). Keep the host connector key and any selected official token in the worker environment as well as HTTP. After deployment, restart workers to load the new typed handler.

Polling or brainstorming:recover ends expired leases and queued attempts left unclaimed for 180 seconds in bounded batches. Planned workflow turns allow 1,140 seconds, reserving eight maximum 120-second provider budgets beyond that grace period for a single worker. Recovery never invokes a provider directly. The always-declared minimal handler can reject already queued work after the module is disabled without resolving the active module's application services. Private transcript reads and CLI execution run inside a scoped telemetry exclusion; queue failures contain fixed codes, never prompts or raw stderr.

Finite Brainstorming workflows use the same default lane and ID-only run jobs. A phase transition commits its turn and immutable context before dispatch; completion drives the next automatic step even with no connected browser/MCP client. Moderated phases wait for an explicit operator action. A failed or partial step blocks dependent work. brainstorming:recover also reconciles up to 100 workflow records and redispatches queued, never-claimed attempts; it never repeats an already claimed model call. Run recovery before the applicable stale threshold to recover lost dispatch; stale terminal attempts still need explicit retry.

Apply the new workflow/artifact migrations before deploying these services and restart workers. Result-plan retrieval and document-only writes are independent of worker availability. Plans are not queue payloads or expiring job results; they remain canonical tenant SQL documents after execution and transcript deletion.

On 2026-10-03 the selected Brainstorming development tenant's workflow/artifact migrations were confirmed read-only and its empty default lane was gracefully restarted under the existing Restart=always supervisor. A fresh process and active heartbeat were verified with reclaim-after 600 seconds and KillMode=control-group. This is tenant-specific readiness evidence, not a rollout to every tenant. Authenticated native workflow/MCP acceptance used disposable SQL and synthetic text; owner runtime UI/Hermes acceptance remains separate.