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

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.

Funky queue overview: stats bar, state breakdown by queue and task, 24-hour throughput chart
Funky - the queue overview, dark theme
Funky jobs table with the filter toolbar and bulk actions
Jobs - filters, search, bulk retry
Funky job detail: arguments, result, attempts, retry and remove actions
One job - args, result, retries
Cron - schedules and their next run
Locks - who holds what, and until when

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.