├── lib └── ResqueScheduler │ ├── InvalidTimestampException.php │ ├── Job │ └── Status.php │ ├── Stat.php │ ├── Worker.php │ └── ResqueScheduler.php ├── extras └── resque-scheduler.monit ├── CHANGELOG.md ├── composer.json ├── LICENSE ├── bin └── resque-scheduler.php └── README.md /lib/ResqueScheduler/InvalidTimestampException.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright (c) 2012 Chris Boulton 8 | * @license http://www.opensource.org/licenses/mit-license.php 9 | */ 10 | namespace ResqueScheduler; 11 | 12 | class InvalidTimestampException extends Resque_Exception 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /lib/ResqueScheduler/Job/Status.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright Copyright 2013, Wan Qi Chen 9 | * @license MIT License (http://www.opensource.org/licenses/mit-license.php) 10 | */ 11 | namespace ResqueScheduler\Job; 12 | 13 | class Status extends \Resque_Job_Status 14 | { 15 | const STATUS_SCHEDULED = 63; 16 | } 17 | -------------------------------------------------------------------------------- /extras/resque-scheduler.monit: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ##Changelog 2 | 3 | ###**v1.2.3** [2015-03-28] 4 | 5 | * [fix] Fix#4: Undefined index `s_time` 6 | 7 | ###**v1.2.2** [2013-11-29] 8 | 9 | * [fix] Fix `clear()` method 10 | 11 | ###**v1.1.1** [2013-02-27] 12 | 13 | * [fix] Fix namespace incoherence with Composer 14 | * [fix] ResqueScheduler class inside ResqueScheduler directory to reflect namespace 15 | 16 | ###**v1.1.0** [2013-02-13] 17 | 18 | * [new] Add actual waiting time passed in scheduled queue in MoveScheduledJob log 19 | * [new] Add expected waiting time in scheduled queue in MoveSheduledJob log 20 | 21 | 22 | ###**v1.0.2** [2013-02-01] 23 | 24 | * [new] Record total number of jobs ever scheduled 25 | * [new] Add Job ID in log 26 | * [fix] Load missing classes in ResqueScheduler 27 | 28 | ###**v1.0.1** [2013-01-31] 29 | 30 | * [change] Update composer file 31 | 32 | ###**v1.0.0** [2013-01-31] 33 | 34 | * Initial Release -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kamisama/php-resque-ex-scheduler", 3 | "type": "library", 4 | "description": "php-resque-ex-scheduler is a PHP port of resque-scheduler, which adds support for scheduling jobs to PHP-Resque.", 5 | "keywords": ["job", "background", "redis", "resque", "queue", "php", "schedule", "cron"], 6 | "homepage": "http://www.github.com/kamisama/php-resque-ex-scheduler/", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Chris Boulton" 11 | }, 12 | { 13 | "name": "Wan Qi chen", 14 | "email": "kami@kamisama.me", 15 | "homepage": "http://www.kamisama.me" 16 | } 17 | ], 18 | "autoload": { 19 | "psr-0": { 20 | "ResqueScheduler": "lib" 21 | } 22 | }, 23 | "support": { 24 | "email": "kami@kamisama.me", 25 | "issues": "https://github.com/kamisama/php-resque-ex-scheduler/issues", 26 | "source": "https://github.com/kamisama/php-resque-ex-scheduler" 27 | }, 28 | "require": { 29 | "kamisama/php-resque-ex": ">=1.2.1" 30 | } 31 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (c) 2012 Chris Boulton 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. -------------------------------------------------------------------------------- /lib/ResqueScheduler/Stat.php: -------------------------------------------------------------------------------- 1 | (Original) 8 | * @author Wan Qi Chen 9 | * @license http://www.opensource.org/licenses/mit-license.php 10 | */ 11 | namespace ResqueScheduler; 12 | 13 | class Stat extends \Resque_Stat 14 | { 15 | const KEYNAME = 'schdlr'; 16 | 17 | /** 18 | * Get the value of the supplied statistic counter for the specified statistic. 19 | * 20 | * @param string $stat The name of the statistic to get the stats for. 21 | * @return mixed Value of the statistic. 22 | */ 23 | public static function get($stat = self::KEYNAME) 24 | { 25 | return parent::get($stat); 26 | } 27 | 28 | /** 29 | * Increment the value of the specified statistic by a certain amount (default is 1) 30 | * 31 | * @param string $stat The name of the statistic to increment. 32 | * @param int $by The amount to increment the statistic by. 33 | * @return boolean True if successful, false if not. 34 | */ 35 | public static function incr($stat = self::KEYNAME, $by = 1) 36 | { 37 | return parent::incr($stat, $by); 38 | } 39 | 40 | /** 41 | * Decrement the value of the specified statistic by a certain amount (default is 1) 42 | * 43 | * @param string $stat The name of the statistic to decrement. 44 | * @param int $by The amount to decrement the statistic by. 45 | * @return boolean True if successful, false if not. 46 | */ 47 | public static function decr($stat = self::KEYNAME, $by = 1) 48 | { 49 | return parent::decr($stat, $by); 50 | } 51 | 52 | /** 53 | * Delete a statistic with the given name. 54 | * 55 | * @param string $stat The name of the statistic to delete. 56 | * @return boolean True if successful, false if not. 57 | */ 58 | public static function clear($stat = self::KEYNAME) 59 | { 60 | return parent::clear($stat); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /bin/resque-scheduler.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | registerLogger($logger); 59 | $worker->logLevel = $logLevel; 60 | 61 | $PIDFILE = getenv('PIDFILE'); 62 | if ($PIDFILE) { 63 | file_put_contents($PIDFILE, getmypid()) or 64 | die('Could not write PID information to ' . $PIDFILE); 65 | } 66 | 67 | logStart($logger, array('message' => '*** Starting scheduler worker ' . $worker, 'data' => array('type' => 'start', 'worker' => (string) $worker)), $logLevel); 68 | $worker->work($interval); 69 | 70 | function logStart($logger, $message, $logLevel) 71 | { 72 | if ($logger === null || $logger->getInstance() === null) { 73 | fwrite(STDOUT, (($logLevel == Resque_Worker::LOG_NORMAL) ? "" : "[" . strftime('%T %Y-%m-%d') . "] ") . $message['message'] . "\n"); 74 | } else { 75 | list($host, $pid, $queues) = explode(':', $message['data']['worker'], 3); 76 | $message['data']['worker'] = $host . ':' . $pid; 77 | $message['data']['queues'] = explode(',', $queues); 78 | 79 | $logger->getInstance()->addInfo($message['message'], $message['data']); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 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 = 3600; 31 | $args = array('id' => $user->id); 32 | ResqueScheduler::enqueueIn($in, 'email', 'SendFollowUpEmail', $args); 33 | 34 | The above will store the job for 1 hour 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 104 | * Wan Qi Chen (Kamisama) 105 | -------------------------------------------------------------------------------- /lib/ResqueScheduler/Worker.php: -------------------------------------------------------------------------------- 1 | (Original) 7 | * @author Wan Qi Chen 8 | * @copyright (c) 2012 Chris Boulton 9 | * @license http://www.opensource.org/licenses/mit-license.php 10 | */ 11 | 12 | namespace ResqueScheduler; 13 | 14 | class Worker extends \Resque_Worker 15 | { 16 | 17 | /** 18 | * @var int Interval to sleep for between checking schedules. 19 | */ 20 | protected $interval = 5; 21 | 22 | /** 23 | * The primary loop for a worker. 24 | * 25 | * Every $interval (seconds), the scheduled queue will be checked for jobs 26 | * that should be pushed to Resque. 27 | * 28 | * @param int $interval How often to check schedules. 29 | */ 30 | public function work($interval = null) 31 | { 32 | if ($interval !== null) { 33 | $this->interval = $interval; 34 | } 35 | 36 | $this->updateProcLine('Starting'); 37 | $this->startup(); 38 | 39 | while (true) { 40 | if ($this->shutdown) { 41 | break; 42 | } 43 | 44 | $this->handleDelayedItems(); 45 | $this->sleep(); 46 | } 47 | 48 | $this->unregisterWorker(); 49 | } 50 | 51 | /** 52 | * Handle delayed items for the next scheduled timestamp. 53 | * 54 | * Searches for any items that are due to be scheduled in Resque 55 | * and adds them to the appropriate job queue in Resque. 56 | * 57 | * @param DateTime|int $timestamp Search for any items up to this timestamp to schedule. 58 | */ 59 | public function handleDelayedItems($timestamp = null) 60 | { 61 | while (($timestamp = ResqueScheduler::nextDelayedTimestamp(null)) !== false) { 62 | $this->updateProcLine('Processing Delayed Items'); 63 | $this->enqueueDelayedItemsForTimestamp($timestamp); 64 | } 65 | } 66 | 67 | /** 68 | * Schedule all of the delayed jobs for a given timestamp. 69 | * 70 | * Searches for all items for a given timestamp, pulls them off the list of 71 | * delayed jobs and pushes them across to Resque. 72 | * 73 | * @param DateTime|int $timestamp Search for any items up to this timestamp to schedule. 74 | */ 75 | public function enqueueDelayedItemsForTimestamp($timestamp) 76 | { 77 | $item = null; 78 | while ($item = ResqueScheduler::nextItemForTimestamp($timestamp)) { 79 | $this->log( 80 | array( 81 | 'message' => 'Moving scheduled job ' . strtoupper($item['class']) . ' to ' . strtoupper($item['queue']), 82 | 'data' => array( 83 | 'type' => 'movescheduled', 84 | 'args' => array( 85 | 'timestamp' => (int)$timestamp, 86 | 'class' => $item['class'], 87 | 'queue' => $item['queue'], 88 | 'job_id' => $item['args'][0]['id'], 89 | 'wait' => round(microtime(true) - (isset($item['s_time']) ? $item['s_time'] : 0), 3), 90 | 's_wait' => $timestamp - floor(isset($item['s_time']) ? $item['s_time'] : 0) 91 | ) 92 | ) 93 | ), 94 | self::LOG_TYPE_INFO 95 | ); 96 | \Resque_Event::trigger( 97 | 'beforeDelayedEnqueue', 98 | array( 99 | 'queue' => $item['queue'], 100 | 'class' => $item['class'], 101 | 'args' => $item['args'], 102 | ) 103 | ); 104 | 105 | $payload = array_merge(array($item['queue'], $item['class']), $item['args'], array($item['track'])); 106 | call_user_func_array('\Resque::enqueue', $payload); 107 | } 108 | } 109 | 110 | /** 111 | * Sleep for the defined interval. 112 | */ 113 | protected function sleep() 114 | { 115 | $this->log( 116 | array( 117 | 'message' => 'Sleeping for ' . $this->interval, 118 | 'data' => array('type' => 'sleep', 'second' => $this->interval) 119 | ), 120 | self::LOG_TYPE_DEBUG 121 | ); 122 | sleep($this->interval); 123 | } 124 | 125 | /** 126 | * Update the status of the current worker process. 127 | * 128 | * On supported systems (with the PECL proctitle module installed), update 129 | * the name of the currently running process to indicate the current state 130 | * of a worker. 131 | * 132 | * @param string $status The updated process title. 133 | */ 134 | protected function updateProcLine($status) 135 | { 136 | if (function_exists('setproctitle')) { 137 | setproctitle('resque-scheduler: ' . $status); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /lib/ResqueScheduler/ResqueScheduler.php: -------------------------------------------------------------------------------- 1 | (Original) 7 | * @author Wan Qi Chen 8 | * @copyright (c) 2012 Chris Boulton 9 | * @license http://www.opensource.org/licenses/mit-license.php 10 | */ 11 | namespace ResqueScheduler; 12 | 13 | class ResqueScheduler 14 | { 15 | // Name of the scheduler queue 16 | // Should be as unique as possible 17 | const QUEUE_NAME = '_schdlr_'; 18 | 19 | /** 20 | * Enqueue a job in a given number of seconds from now. 21 | * 22 | * Identical to Resque::enqueue, however the first argument is the number 23 | * of seconds before the job should be executed. 24 | * 25 | * @param int $in Number of seconds from now when the job should be executed. 26 | * @param string $queue The name of the queue to place the job in. 27 | * @param string $class The name of the class that contains the code to execute the job. 28 | * @param array $args Any optional arguments that should be passed when the job is executed. 29 | * @param boolean $trackStatus Set to true to be able to monitor the status of a job. 30 | * @return string Job ID 31 | */ 32 | public static function enqueueIn($in, $queue, $class, array $args = array(), $trackStatus = false) 33 | { 34 | return self::enqueueAt(time() + $in, $queue, $class, $args, $trackStatus); 35 | } 36 | 37 | /** 38 | * Enqueue a job for execution at a given timestamp. 39 | * 40 | * Identical to Resque::enqueue, however the first argument is a timestamp 41 | * (either UNIX timestamp in integer format or an instance of the DateTime 42 | * class in PHP). 43 | * 44 | * @param DateTime|int $at Instance of PHP DateTime object or int of UNIX timestamp. 45 | * @param string $queue The name of the queue to place the job in. 46 | * @param string $class The name of the class that contains the code to execute the job. 47 | * @param array $args Any optional arguments that should be passed when the job is executed. 48 | * @param boolean $trackStatus Set to true to be able to monitor the status of a job. 49 | * @return string Job ID 50 | */ 51 | public static function enqueueAt($at, $queue, $class, $args = array(), $trackStatus = false) 52 | { 53 | self::validateJob($class, $queue); 54 | 55 | $args['id'] = md5(uniqid('', true)); 56 | $args['s_time'] = time(); 57 | $job = self::jobToHash($queue, $class, $args, $trackStatus); 58 | self::delayedPush($at, $job); 59 | 60 | if ($trackStatus) { 61 | \Resque_Job_Status::create($args['id'], Job\Status::STATUS_SCHEDULED); 62 | } 63 | 64 | \Resque_Event::trigger( 65 | 'afterSchedule', 66 | array( 67 | 'at' => $at, 68 | 'queue' => $queue, 69 | 'class' => $class, 70 | 'args' => $args, 71 | ) 72 | ); 73 | 74 | return $args['id']; 75 | } 76 | 77 | /** 78 | * Directly append an item to the delayed queue schedule. 79 | * 80 | * @param DateTime|int $timestamp Timestamp job is scheduled to be run at. 81 | * @param array $item Hash of item to be pushed to schedule. 82 | */ 83 | public static function delayedPush($timestamp, $item) 84 | { 85 | $timestamp = self::getTimestamp($timestamp); 86 | $redis = \Resque::redis(); 87 | $redis->rpush(self::QUEUE_NAME . ':' . $timestamp, json_encode($item)); 88 | 89 | $redis->zadd(self::QUEUE_NAME, $timestamp, $timestamp); 90 | } 91 | 92 | /** 93 | * Get the total number of jobs in the delayed schedule. 94 | * 95 | * @return int Number of scheduled jobs. 96 | */ 97 | public static function getDelayedQueueScheduleSize() 98 | { 99 | return (int) \Resque::redis()->zcard(self::QUEUE_NAME); 100 | } 101 | 102 | /** 103 | * Get the number of jobs for a given timestamp in the delayed schedule. 104 | * 105 | * @param DateTime|int $timestamp Timestamp 106 | * @return int Number of scheduled jobs. 107 | */ 108 | public static function getDelayedTimestampSize($timestamp) 109 | { 110 | $timestamp = self::toTimestamp($timestamp); 111 | 112 | return \Resque::redis()->llen(self::QUEUE_NAME . ':' . $timestamp, $timestamp); 113 | } 114 | 115 | /** 116 | * Remove a delayed job from the queue 117 | * 118 | * note: you must specify exactly the same 119 | * queue, class and arguments that you used when you added 120 | * to the delayed queue 121 | * 122 | * also, this is an expensive operation because all delayed keys have tobe 123 | * searched 124 | * 125 | * @param $queue 126 | * @param $class 127 | * @param $args 128 | * @return int number of jobs that were removed 129 | */ 130 | public static function removeDelayed($queue, $class, $args) 131 | { 132 | $destroyed=0; 133 | $item = json_encode(self::jobToHash($queue, $class, $args)); 134 | $redis = \Resque::redis(); 135 | 136 | foreach ($redis->keys(self::QUEUE_NAME . ':*') as $key) { 137 | $key = $redis->removePrefix($key); 138 | $destroyed += $redis->lrem($key, 0, $item); 139 | } 140 | 141 | return $destroyed; 142 | } 143 | 144 | /** 145 | * removed a delayed job queued for a specific timestamp 146 | * 147 | * note: you must specify exactly the same 148 | * queue, class and arguments that you used when you added 149 | * to the delayed queue 150 | * 151 | * @param $timestamp 152 | * @param $queue 153 | * @param $class 154 | * @param $args 155 | * @return mixed 156 | */ 157 | public static function removeDelayedJobFromTimestamp($timestamp, $queue, $class, $args) 158 | { 159 | $key = self::QUEUE_NAME . ':' . self::getTimestamp($timestamp); 160 | $item = json_encode(self::jobToHash($queue, $class, $args)); 161 | $redis = \Resque::redis(); 162 | $count = $redis->lrem($key, 0, $item); 163 | self::cleanupTimestamp($key, $timestamp); 164 | 165 | return $count; 166 | } 167 | 168 | /** 169 | * Generate hash of all job properties to be saved in the scheduled queue. 170 | * 171 | * @param string $queue Name of the queue the job will be placed on. 172 | * @param string $class Name of the job class. 173 | * @param array $args Array of job arguments. 174 | */ 175 | 176 | private static function jobToHash($queue, $class, $args, $trackStatus) 177 | { 178 | return array( 179 | 'class' => $class, 180 | 'args' => array($args), 181 | 'queue' => $queue, 182 | 'track' => $trackStatus 183 | ); 184 | } 185 | 186 | /** 187 | * If there are no jobs for a given key/timestamp, delete references to it. 188 | * 189 | * Used internally to remove empty delayed: items in Redis when there are 190 | * no more jobs left to run at that timestamp. 191 | * 192 | * @param string $key Key to count number of items at. 193 | * @param int $timestamp Matching timestamp for $key. 194 | */ 195 | private static function cleanupTimestamp($key, $timestamp) 196 | { 197 | $timestamp = self::getTimestamp($timestamp); 198 | $redis = \Resque::redis(); 199 | 200 | if ($redis->llen($key) == 0) { 201 | $redis->del($key); 202 | $redis->zrem(self::QUEUE_NAME, $timestamp); 203 | } 204 | } 205 | 206 | /** 207 | * Convert a timestamp in some format in to a unix timestamp as an integer. 208 | * 209 | * @param DateTime|int $timestamp Instance of DateTime or UNIX timestamp. 210 | * @return int Timestamp 211 | * @throws ResqueScheduler_InvalidTimestampException 212 | */ 213 | private static function getTimestamp($timestamp) 214 | { 215 | if ($timestamp instanceof \DateTime) { 216 | $timestamp = $timestamp->getTimestamp(); 217 | } 218 | 219 | if ((int) $timestamp != $timestamp) { 220 | throw new ResqueScheduler\InvalidTimestampException( 221 | 'The supplied timestamp value could not be converted to an integer.' 222 | ); 223 | } 224 | 225 | return (int) $timestamp; 226 | } 227 | 228 | /** 229 | * Find the first timestamp in the delayed schedule before/including the timestamp. 230 | * 231 | * Will find and return the first timestamp upto and including the given 232 | * timestamp. This is the heart of the ResqueScheduler that will make sure 233 | * that any jobs scheduled for the past when the worker wasn't running are 234 | * also queued up. 235 | * 236 | * @param DateTime|int $timestamp Instance of DateTime or UNIX timestamp. 237 | * Defaults to now. 238 | * @return int|false UNIX timestamp, or false if nothing to run. 239 | */ 240 | public static function nextDelayedTimestamp($at = null) 241 | { 242 | if ($at === null) { 243 | $at = time(); 244 | } else { 245 | $at = self::getTimestamp($at); 246 | } 247 | 248 | $items = \Resque::redis()->zrangebyscore(self::QUEUE_NAME, '-inf', $at, array('limit', 0, 1)); 249 | if (!empty($items)) { 250 | return $items[0]; 251 | } 252 | 253 | return false; 254 | } 255 | 256 | /** 257 | * Pop a job off the delayed queue for a given timestamp. 258 | * 259 | * @param DateTime|int $timestamp Instance of DateTime or UNIX timestamp. 260 | * @return array Matching job at timestamp. 261 | */ 262 | public static function nextItemForTimestamp($timestamp) 263 | { 264 | $timestamp = self::getTimestamp($timestamp); 265 | $key = self::QUEUE_NAME . ':' . $timestamp; 266 | 267 | $item = json_decode(\Resque::redis()->lpop($key), true); 268 | 269 | self::cleanupTimestamp($key, $timestamp); 270 | 271 | return $item; 272 | } 273 | 274 | /** 275 | * Ensure that supplied job class/queue is valid. 276 | * 277 | * @param string $class Name of job class. 278 | * @param string $queue Name of queue. 279 | * @throws Resque_Exception 280 | */ 281 | private static function validateJob($class, $queue) 282 | { 283 | if (empty($class)) { 284 | throw new \Resque_Exception('Jobs must be given a class.'); 285 | } elseif (empty($queue)) { 286 | throw new \Resque_Exception('Jobs must be put in a queue.'); 287 | } 288 | 289 | return true; 290 | } 291 | } 292 | --------------------------------------------------------------------------------