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; 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.
root/static/img/shots/funky-cron.png
1600 × 900
root/static/img/shots/funky-locks.png
1600 × 900
Get it
cpanm Punk::Queue
Artistic License 2.0, like the rest of it. Needs Punk and a database - Postgres, MySQL or SQLite - and nothing else.