Punk · Punk::Queue
Jobs, out of the request.
background
Punk::Queue is a job queue as a Punk plugin. Named queues, retries with full-jitter backoff, cron, locks and broadcast - and its keywords sit beside Punk’s own, so enqueueing a job reads like every other line in the application.
use Punk::Plugin::Queue;
plugin 'Queue' => {
dsn => 'dbi:Pg:dbname=myapp',
admin => { prefix => '/queue', guard => 'Web::Auth#admin' },
};
task 'mail.send' => 'Job::Mail#send';
cron '0 3 * * *' => 'Reports#nightly';
post '/signup' => sub {
my ($c) = @_;
$c->json({ job => $c->enqueue('mail.send' => [ $c->param('to') ]) });
};
What you get
-
Tasks
task 'mail.send' => 'Job::Mail#send'- a named target resolved at boot, like a route.$c->enqueuefrom a handler, another task or the command line, withdelay,priorityand a per-taskqueue. -
Retries that back off
An attempt that dies is retried with full jitter, up to
attempts. The last failure parks the job rather than looping forever, so a receiver that is gone costs a known amount and nothing is silently dropped. -
Cron
cron '0 3 * * *' => 'Job::Reports#nightly'. The schedule lives in the application, next to the code it runs, and deploys with it - not in a crontab on one box that nobody edits. -
Locks and leader election
$q->lockwithrenew_lock: only the holder unlocks, and the winner re-reads under the lock it just won. One box in a pool does the nightly run and the others carry on serving. -
Broadcast
One enqueue, every worker runs it - cache invalidation and configuration reloads, without a second mechanism to keep working.
-
Funky, the admin UI
Mounts under a guard you control, never as an open path. Overview, jobs with filters and bulk retry, one job’s arguments, result and attempts - and it is a Punk mount like any other, so it inherits your auth.
A task is a name
Declaring a task is declaring a target and its policy: which queue it runs on, how many attempts it gets, how long one attempt may take. The queue owns the backoff, so a handler that dies is a retry rather than an incident.
Enqueueing is one call and returns the job id, which is what the
admin UI and punk-queue jobs both look things up by.
# A task is a named target, and the queue owns the retrying.
task 'invoice.render' => 'Job::Invoice#render', {
queue => 'billing',
attempts => 5, # full-jitter backoff between them
timeout => 30,
};
# Enqueue from anywhere - a handler, another task, the CLI.
$c->enqueue('invoice.render' => [ $id ], delay => 60, priority => 10);
Scheduled, and safe in a pool
Cron entries are declared beside the routes and the tasks. On more than one box the schedule fires on all of them, which is what the lock is for: the holder does the work, the losers move on, and only the holder unlocks.
Winning a contended lock proves nothing on its own - the holder re-reads the state it is about to act on, under the lock, before acting. The queue gives you the lock, but that discipline is still yours.
# Cron, with the schedule in the application and not in crontab.
cron '0 3 * * *' => 'Job::Reports#nightly';
cron '*/5 * * * *' => 'Job::Sync#poll', { queue => 'sync' };
# ...and the lock that makes it safe on more than one box: only the
# holder unlocks, and a re-read under the lock is what makes winning
# it mean anything.
if (my $lock = $q->lock('nightly', 300, owner => $host)) {
$q->renew_lock('nightly', 300, owner => $host);
}
Funky
The admin UI that ships with it. Mounted under your guard, themed light and dark, and built the same way the rest of this is: rendered from the queue’s own tables, no build step.
Get it
cpanm Punk::Queue
Punk::Queue on CPAN Read the guide
Artistic License 2.0, like the rest of it. Needs Punk and a database - Postgres, MySQL or SQLite - and nothing else.