kamisama/php-resque-ex-scheduler
A PHP port of Resque's scheduler that queues delayed background jobs for later execution. This extended fork adds scheduling support to php-resque, letting you enqueue jobs to run at a future time and keep Redis-backed workers processing them.
Install the package alongside its worker companion (kamisama/php-resque-ex); it is archived, so add a vcs repository entry if Packagist resolution fails, and pin a tag:
composer require kamisama/php-resque-ex-scheduler kamisama/php-resque-ex
Look first at the bin/ directory (the scheduler entry point), lib/ResqueScheduler.php (the enqueueIn/enqueueAt API), and any bundled resque-scheduler-conf.php / sample schedule file. Run the entry point with --help to confirm the option names on this fork before wiring it into supervisor.
Create a single bootstrap that both your app and the scheduler/worker process include — it must register the autoloader, point Resque at Redis, and set the timezone:
require __DIR__ . '/vendor/autoload.php';
date_default_timezone_set('UTC');
Resque::setBackend('127.0.0.1:6379');
First use case — fire a one-off job 60 seconds from now from a web request, then run the two processes:
ResqueScheduler::enqueueIn(60, 'default', 'SendWelcomeEmail', ['user_id' => 42]);
# terminal 1 — the scheduler (polls Redis, enqueues due jobs)
php vendor/bin/resque-scheduler --verbose
# terminal 2 — a worker that actually executes the job class
php vendor/bin/resque-worker
Recurring jobs live in a schedule file (an array keyed by job name, each entry with cron, class, args, queue) that you pass to the scheduler; the job class itself is a normal Resque_Job-compatible class with a perform() method.
Bootstrap-once, include-everywhere. Put setBackend, autoloading, logger setup, and timezone in one bootstrap.php and require it from the scheduler process, the worker process, and any web controller that schedules jobs. Divergent bootstraps are the top cause of "job scheduled but never ran" (wrong Redis DB, wrong queue, wrong class path).
Two scheduling styles. Use enqueueAt($unixTimestamp, $queue, $class, $args) when the wall-clock moment matters (billing cutoffs, reminders at a user's local time) and enqueueIn($seconds, ...) for relative UX delays. Precompute timestamps in the app layer rather than letting the scheduler guess.
Recurring via a schedule definition file. Keep recurring entries in one file (keyed by a stable job name, with cron, class, queue, args, and an optional human description). Keep that file under version control and treat it as deployable config — it is the single source of truth for periodic work.
Separate the queues. Route delayed one-offs to delayed and cron jobs to cron, then run dedicated workers per queue. This lets you scale cron workers independently, throttle noisy periodic jobs, and keep user-facing delayed work from queueing behind a slow nightly task.
Wrap scheduling in a service. Don't call ResqueScheduler::enqueue* from controllers directly; put it behind something like Scheduler::scheduleAt(...) so you can add logging, validation, and test doubles. You can assert against Redis directly in tests: the pending timestamps are sorted sets under the resque:delayed: key family.
Make every job idempotent. Delivery is at-least-once: a scheduler restart, a slow tick, or a worker crash-and-retry can produce duplicate execution. Guard with a Redis SETNX/lock or a unique business key inside perform().
Process management. Run both the scheduler and workers under supervisor/systemd with autorestart. One supervisor program per queue for workers, exactly one program for the scheduler. Pair with resque-web (from the Resque family) or direct redis-cli queries for operational visibility.
Resque_* global class names can collide with newer Resque forks. Pin versions, run the full stack in staging, and budget for a fork or a migration if you depend on it long-term.* * * * * fires within seconds, not at :00.000. Don't design SLAs around sub-second precision.ZREM from the delayed-timestamp index and from the per-timestamp sorted set. Encapsulate that in your service layer so you don't hand-edit keys ad hoc.appendonly/save in production.enqueueAt timestamps use the process timezone. Set date_default_timezone_set() identically in the app, worker, and scheduler bootstraps, or jobs will fire hours off.enqueueAt() with a timestamp already in the past fires on the next poll, not immediately — fine for catch-up, but don't rely on it for "run now" semantics.perform(), or check for an in-flight marker before enqueuing.Resque_Log into your Monolog handler) and start the scheduler with --verbose. A misconfigured class name or queue name otherwise just leaves jobs sitting in a queue with no error surfaced.redis-cli. Check resque:queues, resque:queue:<name>, and the resque:delayed:* sorted sets to distinguish "never enqueued" (scheduler problem) from "enqueued but not processed" (worker problem).@daily or L/W modifiers may not be supported by the bundled parser. Test any non-trivial expression against the scheduler before relying on it.ResqueScheduler if you need custom logging or pre-enqueue validation — both are plain PHP classes, so this is the safest way to add behaviour without patching the archived source.How can I help you explore Laravel packages today?