Queue (SQL)

Note

Not part of core. Install it separately:

composer require kinetis/queue-sql

Adds MySQL/Postgres as a backend for Queue, riding an existing database instead of a separate service. Application code that already pushes and pops jobs through QueueInterface needs no changes at all to switch — only your configuration changes.

QUEUE_CONNECTION=sql
DB_CONNECTION=mysql   # or "pgsql"
DB_HOST=127.0.0.1
DB_NAME=app
DB_USER=app
DB_PASSWORD=secret
vendor/bin/kinetis queue:work --queue=high,default

pop() relies on SELECT ... FOR UPDATE SKIP LOCKED to guarantee two workers never receive the same job — that’s this backend’s actual version floor: MySQL 8.0+ or MariaDB 10.6+. An older server doesn’t support that clause at all, so pop() fails outright rather than degrade quietly.

Configuring

DB_* are the exact keys kinetis/persistence already reads — nothing new to set up beyond a working database connection.

The queue needs a table

kinetis/queue-sql ships two ready-to-copy Migrations files — one per dialect, since the auto-incrementing primary key syntax itself isn’t portable between MySQL and Postgres:

vendor/kinetis/queue-sql/resources/migrations/create_kinetis_queue_jobs_table.mysql.php.stub
vendor/kinetis/queue-sql/resources/migrations/create_kinetis_queue_jobs_table.pgsql.php.stub

Copy whichever matches your database into your own migrations/ directory with a timestamp prefix, then run vendor/bin/kinetis migrate.

The table includes a nullable metadata column — the instrumentation propagation channel (see Telemetry). A table created from an earlier stub needs it added:

ALTER TABLE kinetis_queue_jobs ADD COLUMN metadata TEXT NULL;

A crashed worker’s job: the visibility timeout

By default, a job that’s been popped but whose worker crashes before ack()/release() runs stays reserved forever — no other worker can ever pick it up again, since nothing ever clears its reserved_at.

SqlQueue’s second constructor argument, $visibilityTimeoutSeconds, closes it — the standard “visibility timeout” pattern SQS’s own VisibilityTimeout already uses:

use Kinetis\QueueSql\SqlQueue;

$queue = new SqlQueue($db, visibilityTimeoutSeconds: 300);

A row reserved longer than this becomes poppable again by any worker — attempts is incremented at that point (crediting the crashed attempt, the same as an explicit release() call would), so maxAttempts still eventually gives up on a job whose worker keeps crashing rather than retrying it forever. null (the default) preserves the original forever-stranded behavior exactly, unchanged.

A value below 10 or negative — is rejected at construction: it would make pop()’s own query treat a row reserved an instant ago (or one whose reservation timestamp is in the future relative to now) as already stale, letting a second worker reclaim an actively-held reservation immediately instead of after it genuinely goes stale.

kinetis queue:work reads this from the optional QUEUE_VISIBILITY_TIMEOUT_SECONDS environment variable (via Config::scopedKey(), so it respects QUEUE_CONNECTION_NAME the same as every other queue setting) — absent means null, the same as constructing SqlQueue directly with no second argument:

QUEUE_CONNECTION=sql
QUEUE_VISIBILITY_TIMEOUT_SECONDS=300

Pick a value comfortably longer than your slowest real job takes to run — too short reclaims a job that’s still being legitimately processed, producing exactly the duplicate-processing risk a visibility timeout is meant to bound, not eliminate outright.

Delayed jobs

$this->queue->push(new SendReminderEmail($userId), delaySeconds: 3600);

Checked on this backend’s own polling cycle rather than firing at the exact moment the delay ends, so a delayed job can run slightly later than its exact target time — typically by a few seconds, not less.

Retries and giving up

Everything Queue documents about maxAttempts, QUEUE_MAX_ATTEMPTS, and the log entry written when a job is finally given up on works identically here — nothing about retry behavior changes by switching to this backend.

Named connections

QUEUE_CONNECTION_NAME=reports
DB_REPORTS_CONNECTION=mysql
DB_REPORTS_HOST=127.0.0.1

Same convention as everywhere else in Kinetis (see Configuration): QUEUE_CONNECTION_NAME picks which named block of DB_* settings a worker reads, and 'default' (or simply not setting it) reads the plain keys shown earlier in this page.

If the package isn’t installed

Setting QUEUE_CONNECTION=sql without having run composer require kinetis/queue-sql produces a clear error telling you which package to install, rather than a confusing crash.

See also

  • Queue — writing jobs, pushing and popping, and everything about retries that applies to every backend equally.

  • Persistence — connecting to MySQL and Postgres directly.

  • Migrations — running this backend’s own required migration.

  • Configuration — the named-connection convention used above.