NAME
Punk::Queue::Backend - the storage contract, and the shared implementation
SYNOPSIS
package My::Backend;
use parent 'Punk::Queue::Backend'; # optional
sub dequeue { ... } # the parts you do differently
DESCRIPTION
A backend is any class implementing the contract below. There is no base class you must inherit and no registry to enter: Punk::Queue->new(dsn => ..., backend => '+My::Backend') is the whole integration.
What inheriting from this class buys you is every shared method for free - enqueue, the terminal transitions, the migration runner, the clock probe - implemented in C, leaving only the genuinely divergent parts to write.
THE CONTRACT
Twelve required methods. A class answering to these is a working backend.
new(\%opts) -> $backend
dbh -> $dbh
migrate($to?) -> $version
enqueue($task, \@args, \%opts) -> $id
dequeue($worker_id, $timeout, \%opts) -> \%job | undef
finish_job($id, $retries, $result) -> 0|1
fail_job($id, $retries, $err) -> 0|1
retry_job($id, $retries, \%opts) -> 0|1
remove_job($id) -> 0|1
job_info($id) -> \%info | undef
register_worker($id?, \%opts) -> $worker_id
stats() -> \%stats
Optional groups are probed with can and defaulted where a portable default exists: listing (list_jobs, list_workers, list_locks, history, note), the worker registry (unregister_worker, broadcast, receive), locks (lock, unlock, renew_lock), cron (upsert_cron, disable_missing_crons, due_crons, advance_cron), the job log (log_job, job_log) and housekeeping (repair, reset, notify).
Two signature details are load-bearing.
dequeue's $timeout is "how long you may wait", and the backend owns how it waits. That is what lets PostgreSQL block on LISTEN and SQLite poll with a jittered backoff behind one signature, without the caller knowing which it got.
finish_job, fail_job and retry_job take $retries and use it as an optimistic guard. Repair requeues a vanished worker's job by bumping the retry count, and without the guard that worker - which may still be alive and about to report success - would clobber the new attempt.
METHODS
dbh
The pooled database handle for this backend's dsn, connected on first use in this process.
migrate / schema_version / latest_version
The version the database is at, and the version this build knows how to apply. latest_version above schema_version means a migration is pending.
now / clock_delta
now is the current time in the database's frame of reference: this host's clock plus a delta probed once per connection.
Every timestamp this distribution binds comes from now, never from the local clock directly. Two hosts a minute apart is enough to claim a delayed job early, and "the job ran an hour before its delay" is a baffling symptom to debug from the inside.
has_returning
Whether the connection supports RETURNING - PostgreSQL always, SQLite from 3.35. When it does, an insert reads back its own id without a second round trip.
enqueue / job_info / finish_job / fail_job / retry_job / remove_job
list_jobs / list_workers / stats / reset
As documented in Punk::Queue, which delegates to them.
register_worker
my $id = $backend->register_worker(0, { role => 'child',
queues => \@queues });
$backend->register_worker($id); # refresh, keeping the id
One row per claiming process. A refresh whose row has been swept (by repair) falls through to a fresh registration rather than failing.
unregister_worker
$backend->unregister_worker($id);
worker_heartbeat
$backend->worker_heartbeat($id, \%status);
Prove the worker is alive; %status is stored as JSON for the admin UI, and undef keeps whatever is there. From phase 6 this is also where the broadcast inbox is drained.
SEE ALSO
Punk::Queue, Punk::Queue::Backend::SQLite.
AUTHOR
LNATION <email@lnation.org>
LICENSE AND COPYRIGHT
This software is Copyright (c) 2026 by LNATION <email@lnation.org>.
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)