Skip to content

Commit ff3fc17

Browse files
author
Chris Boulton
committed
add initial version. test.php shows sample usage/useful for testing until tests are ready
0 parents  commit ff3fc17

File tree

8 files changed

+579
-0
lines changed

8 files changed

+579
-0
lines changed

LICENSE

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
(c) 2012 Chris Boulton <[email protected]>
2+
3+
Permission is hereby granted, free of charge, to any person obtaining
4+
a copy of this software and associated documentation files (the
5+
"Software"), to deal in the Software without restriction, including
6+
without limitation the rights to use, copy, modify, merge, publish,
7+
distribute, sublicense, and/or sell copies of the Software, and to
8+
permit persons to whom the Software is furnished to do so, subject to
9+
the following conditions:
10+
11+
The above copyright notice and this permission notice shall be
12+
included in all copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.markdown

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
php-resque-scheduler: PHP Resque Scheduler
2+
==========================================
3+
4+
php-resque-scheduler is a PHP port of [resque-scheduler](http://github.com/defunkt/resque),
5+
which adds support for scheduling items in the future to Resque.
6+
7+
The PHP port of resque-scheduler has been designed to be an almost direct-copy
8+
of the Ruby plugin, and is designed to work with the PHP port of resque,
9+
[php-resque](http://github.com/chrisboulton/php-resque).
10+
11+
At the moment, php-resque-scheduler only supports delayed jobs, which is the
12+
ability to push a job to the queue and have it run at a certain timestamp, or
13+
in a number of seconds. Support for recurring jobs (similar to CRON) is planned
14+
for a future release.
15+
16+
Because the PHP port is almost a direct API copy of the Ruby version, it is also
17+
compatible with the web interface of the Ruby version, which provides the
18+
ability to view and manage delayed jobs.
19+
20+
## Delayed Jobs
21+
22+
To quote the documentation for the Ruby resque-scheduler:
23+
24+
> Delayed jobs are one-off jobs that you want to be put into a queue at some
25+
point in the future. The classic example is sending an email:
26+
27+
require 'Resque/Resque.php';
28+
require 'ResqueScheduler/ResqueScheduler.php';
29+
30+
$in = strtotime('+5 days');
31+
$args = array('id' => $user->id);
32+
ResqueScheduler::enqueueIn($in, 'email', 'SendFollowUpEmail', $args);
33+
34+
The above will store the job for 5 days in the delayed queue, and then pull the
35+
job off and submit it to the `email` queue in Resque for processing as soon as
36+
a worker is available.
37+
38+
Instead of passing a relative time in seconds, you can also supply a timestamp
39+
as either a DateTime object or integer containing a UNIX timestamp to the
40+
`enqueueAt` method:
41+
42+
require 'Resque/Resque.php';
43+
require 'ResqueScheduler/ResqueScheduler.php';
44+
45+
$time = 1332067214;
46+
ResqueScheduler::enqueueAt($time, 'email', 'SendFollowUpEmail', $args);
47+
48+
$datetime = new DateTime('2012-03-18 13:21:49');
49+
ResqueScheduler::enqueueAt(datetime, 'email', 'SendFollowUpEmail', $args);
50+
51+
NOTE: resque-scheduler does not guarantee a job will fire at the time supplied.
52+
At the time supplied, resque-scheduler will take the job out of the delayed
53+
queue and push it to the appropriate queue in Resque. Your next available Resque
54+
worker will pick the job up. To keep processing as quick as possible, keep your
55+
queues as empty as possible.
56+
57+
## Worker
58+
59+
Like resque, resque-scheduler includes a worker that runs in the background. This
60+
worker is responsible for pulling items off the schedule/delayed queue and adding
61+
them to the queue for resque. This means that for delayed or scheduled jobs to be
62+
executed, the worker needs to be running.
63+
64+
A basic "up-and-running" resque-scheduler.php file is included that sets up a
65+
running worker environment is included in the root directory. It accepts many
66+
of the same environment variables as php-resque:
67+
68+
* `REDIS_BACKEND` - Redis server to connect to
69+
* `LOGGING` - Enable logging to STDOUT
70+
* `VERBOSE` - Enable verbose logging
71+
* `VVERBOSE` - Enable very verbose logging
72+
* `INTERVAL` - Sleep for this long before checking scheduled/delayed queues
73+
* `APP_INCLUDE` - Include this file when starting (to launch your app)
74+
* `PIDFILE` - Write the PID of the worker out to this file
75+
76+
The resque-scheduler worker requires resque to function. The demo
77+
resque-scheduler.php worker allows you to supply a `RESQUE_PHP` environment
78+
variable with the path to Resque.php. If not supplied and resque is not already
79+
loaded, resque-scheduler will attempt to load it from your include path
80+
(`require_once 'Resque/Resque.php';'`)
81+
82+
It's easy to start the resque-scheduler worker using resque-scheduler.php:
83+
$ RESQUE_PHP=../resque/lib/Resque/Resque.php php resque-scheduler.php
84+
85+
## Event/Hook System
86+
87+
php-resque-scheduler uses the same event system used by php-resque and exposes
88+
the following events.
89+
90+
### afterSchedule
91+
92+
Called after a job has been added to the schedule. Arguments passed are the
93+
timestamp, queue of the job, the class name of the job, and the job's arguments.
94+
95+
### beforeDelayedEnqueue
96+
97+
Called immediately after a job has been pulled off the delayed queue and right
98+
before the job is added to the queue in resque. Arguments passed are the queue
99+
of the job, the class name of the job, and the job's arguments.
100+
101+
## Contributors ##
102+
103+
* chrisboulton

extras/resque-scheduler.monit

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Replace these with your own:
2+
# [PATH/TO/RESQUE]
3+
# [PATH/TO/RESQUE-SCHEDULER]
4+
# [UID]
5+
# [GID]
6+
# [APP_INCLUDE]
7+
8+
check process resque-scheduler_worker
9+
with pidfile /var/run/resque/scheduler-worker.pid
10+
start program = "/bin/sh -c 'APP_INCLUDE=[APP_INCLUDE] RESQUE_PHP=[PATH/TO/RESQUE] PIDFILE=/var/run/resque/scheduler-worker.pid nohup php -f [PATH/TO/RESQUE-SCHEDULER]/resque-scheduler.php > /var/log/resque/scheduler-worker.log &'" as uid [UID] and gid [GID]
11+
stop program = "/bin/sh -c 'kill -s QUIT `cat /var/run/resque/scheduler-worker.pid` && rm -f /var/run/resque/scheduler-worker.pid; exit 0;'"
12+
if totalmem is greater than 300 MB for 10 cycles then restart # eating up memory?
13+
group resque-scheduler_workers

lib/ResqueScheduler.php

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
<?php
2+
/**
3+
* ResqueScheduler core class to handle scheduling of jobs in the future.
4+
*
5+
* @package ResqueScheduler
6+
* @author Chris Boulton <[email protected]>
7+
* @copyright (c) 2012 Chris Boulton
8+
* @license http://www.opensource.org/licenses/mit-license.php
9+
*/
10+
class ResqueScheduler
11+
{
12+
/**
13+
* Enqueue a job in a given number of seconds from now.
14+
*
15+
* Identical to Resque::enqueue, however the first argument is the number
16+
* of seconds before the job should be executed.
17+
*
18+
* @param int $in Number of seconds from now when the job should be executed.
19+
* @param string $queue The name of the queue to place the job in.
20+
* @param string $class The name of the class that contains the code to execute the job.
21+
* @param array $args Any optional arguments that should be passed when the job is executed.
22+
*/
23+
public static function enqueueIn($in, $queue, $class, array $args = array())
24+
{
25+
self::enqueueAt(time() + $in, $queue, $class, $args);
26+
}
27+
28+
/**
29+
* Enqueue a job for execution at a given timestamp.
30+
*
31+
* Identical to Resque::enqueue, however the first argument is a timestamp
32+
* (either UNIX timestamp in integer format or an instance of the DateTime
33+
* class in PHP).
34+
*
35+
* @param DateTime|int $at Instance of PHP DateTime object or int of UNIX timestamp.
36+
* @param string $queue The name of the queue to place the job in.
37+
* @param string $class The name of the class that contains the code to execute the job.
38+
* @param array $args Any optional arguments that should be passed when the job is executed.
39+
*/
40+
public static function enqueueAt($at, $queue, $class, $args = array())
41+
{
42+
self::validateJob($class, $queue);
43+
44+
$job = self::jobToHash($queue, $class, $args);
45+
self::delayedPush($at, $job);
46+
47+
Resque_Event::trigger('afterSchedule', array(
48+
'at' => $at,
49+
'queue' => $queue,
50+
'class' => $class,
51+
'args' => $args,
52+
));
53+
}
54+
55+
/**
56+
* Directly append an item to the delayed queue schedule.
57+
*
58+
* @param DateTime|int $timestamp Timestamp job is scheduled to be run at.
59+
* @param array $item Hash of item to be pushed to schedule.
60+
*/
61+
public static function delayedPush($timestamp, $item)
62+
{
63+
$timestamp = self::getTimestamp($timestamp);
64+
$redis = Resque::redis();
65+
$redis->rpush('delayed:' . $timestamp, json_encode($item));
66+
67+
$redis->zadd('delayed_queue_schedule', $timestamp, $timestamp);
68+
}
69+
70+
/**
71+
* Get the total number of jobs in the delayed schedule.
72+
*
73+
* @return int Number of scheduled jobs.
74+
*/
75+
public static function getDelayedQueueScheduleSize()
76+
{
77+
return (int)Resque::redis()->zcard('delayed_queue_schedule');
78+
}
79+
80+
/**
81+
* Get the number of jobs for a given timestamp in the delayed schedule.
82+
*
83+
* @param DateTime|int $timestamp Timestamp
84+
* @return int Number of scheduled jobs.
85+
*/
86+
public static function getDelayedTimestampSize($timestamp)
87+
{
88+
$timestamp = self::toTimestamp($timestamp);
89+
return Resque::redis()->llen('delayed:' . $timestamp, $timestamp);
90+
}
91+
92+
/**
93+
* Generate hash of all job properties to be saved in the scheduled queue.
94+
*
95+
* @param string $queue Name of the queue the job will be placed on.
96+
* @param string $class Name of the job class.
97+
* @param array $args Array of job arguments.
98+
*/
99+
100+
private static function jobToHash($queue, $class, $args)
101+
{
102+
return array(
103+
'class' => $class,
104+
'args' => $args,
105+
'queue' => $queue,
106+
);
107+
}
108+
109+
/**
110+
* If there are no jobs for a given key/timestamp, delete references to it.
111+
*
112+
* Used internally to remove empty delayed: items in Redis when there are
113+
* no more jobs left to run at that timestamp.
114+
*
115+
* @param string $key Key to count number of items at.
116+
* @param int $timestamp Matching timestamp for $key.
117+
*/
118+
private static function cleanupTimestamp($key, $timestamp)
119+
{
120+
$timestamp = self::getTimestamp($timestamp);
121+
$redis = Resque::redis();
122+
123+
if ($redis->llen($key) == 0) {
124+
$redis->del($key);
125+
$redis->zrem('delayed_queue_schedule', $timestamp);
126+
}
127+
}
128+
129+
/**
130+
* Convert a timestamp in some format in to a unix timestamp as an integer.
131+
*
132+
* @param DateTime|int $timestamp Instance of DateTime or UNIX timestamp.
133+
* @return int Timestamp
134+
* @throws ResqueScheduler_InvalidTimestampException
135+
*/
136+
private static function getTimestamp($timestamp)
137+
{
138+
if ($timestamp instanceof DateTime) {
139+
$timestamp = $timestamp->getTimestamp();
140+
}
141+
142+
if ((int)$timestamp != $timestamp) {
143+
throw new ResqueScheduler_InvalidTimestampExeption(
144+
'The supplied timestamp value could not be converted to an integer.'
145+
);
146+
}
147+
148+
return (int)$timestamp;
149+
}
150+
151+
/**
152+
* Find the first timestamp in the delayed schedule before/including the timestamp.
153+
*
154+
* Will find and return the first timestamp upto and including the given
155+
* timestamp. This is the heart of the ResqueScheduler that will make sure
156+
* that any jobs scheduled for the past when the worker wasn't running are
157+
* also queued up.
158+
*
159+
* @param DateTime|int $timestamp Instance of DateTime or UNIX timestamp.
160+
* Defaults to now.
161+
* @return int|false UNIX timestamp, or false if nothing to run.
162+
*/
163+
public function nextDelayedTimestamp($at = null)
164+
{
165+
if ($at === null) {
166+
$at = time();
167+
}
168+
else {
169+
$at = self::getTimestamp($at);
170+
}
171+
172+
$items = Resque::redis()->zrangebyscore('delayed_queue_schedule', '-inf', $at, 'LIMIT', 0, 1);
173+
if (!empty($items)) {
174+
return $items[0];
175+
}
176+
177+
return false;
178+
}
179+
180+
/**
181+
* Pop a job off the delayed queue for a given timestamp.
182+
*
183+
* @param DateTime|int $timestamp Instance of DateTime or UNIX timestamp.
184+
* @return array Matching job at timestamp.
185+
*/
186+
public function nextItemForTimestamp($timestamp)
187+
{
188+
$timestamp = self::getTimestamp($timestamp);
189+
$key = 'delayed:' . $timestamp;
190+
191+
$item = json_decode(Resque::redis()->lpop($key), true);
192+
193+
self::cleanupTimestamp($key, $timestamp);
194+
return $item;
195+
}
196+
197+
/**
198+
* Ensure that supplied job class/queue is valid.
199+
*
200+
* @param string $class Name of job class.
201+
* @param string $queue Name of queue.
202+
* @throws Resque_Exception
203+
*/
204+
private static function validateJob($class, $queue)
205+
{
206+
if (empty($class)) {
207+
throw new Resque_Exception('Jobs must be given a class.');
208+
}
209+
else if (empty($queue)) {
210+
throw new Resque_Exception('Jobs must be put in a queue.');
211+
}
212+
213+
return true;
214+
}
215+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
/**
3+
* Exception thrown whenever an invalid timestamp has been passed to a job.
4+
*
5+
* @package ResqueScheduler
6+
* @author Chris Boulton <[email protected]>
7+
* @copyright (c) 2012 Chris Boulton
8+
* @license http://www.opensource.org/licenses/mit-license.php
9+
*/
10+
class ResqueScheduler_InvalidTimestampException extends Resque_Exception
11+
{
12+
13+
}

0 commit comments

Comments
 (0)