3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2014 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
29 * The queue runner is a helper which runs all jobs in a queue.
31 * The queue runner is most useful for one-off queues (such as an upgrade);
32 * if the intention is to develop a dedicated, long-running worker thread,
33 * then one should consider writing a new queue consumer.
35 class CRM_Queue_Runner
{
38 * The failed task should be discarded, and queue processing should continue
40 CONST ERROR_CONTINUE
= 1;
43 * The failed task should be kept in the queue, and queue processing should abort
45 CONST ERROR_ABORT
= 2;
53 * @var CRM_Queue_Queue
61 // queue-runner id; used for persistence
65 * @var array whether to display buttons, eg ('retry' => TRUE, 'skip' => FALSE)
70 * @var CRM_Queue_TaskContext
75 * Locate a previously-created instance of the queue-runner
77 * @param $qrid string, the queue-runner ID
79 * @return CRM_Queue_Runner or NULL
81 static function instance($qrid) {
82 if (!empty($_SESSION['queueRunners'][$qrid])) {
83 return unserialize($_SESSION['queueRunners'][$qrid]);
92 * FIXME: parameter validation
93 * FIXME: document signature of onEnd callback
95 * @param $runnerSpec array with keys:
96 * - queue: CRM_Queue_Queue
97 * - errorMode: int, ERROR_CONTINUE or ERROR_ABORT
98 * - onEnd: mixed, a callback to update the UI after running; should be both callable and serializable
99 * - onEndUrl: string, the URL to which one redirects
100 * - pathPrefix: string, prepended to URLs for the web-runner; default: 'civicrm/queue'
102 public function __construct($runnerSpec) {
103 $this->title
= CRM_Utils_Array
::value('title', $runnerSpec, ts('Queue Runner'));
104 $this->queue
= $runnerSpec['queue'];
105 $this->errorMode
= CRM_Utils_Array
::value('errorMode', $runnerSpec, self
::ERROR_ABORT
);
106 $this->isMinimal
= CRM_Utils_Array
::value('isMinimal', $runnerSpec, FALSE);
107 $this->onEnd
= CRM_Utils_Array
::value('onEnd', $runnerSpec, NULL);
108 $this->onEndUrl
= CRM_Utils_Array
::value('onEndUrl', $runnerSpec, NULL);
109 $this->pathPrefix
= CRM_Utils_Array
::value('pathPrefix', $runnerSpec, 'civicrm/queue');
110 $this->buttons
= CRM_Utils_Array
::value('buttons', $runnerSpec, array('retry' => TRUE,'skip' => TRUE));
111 // perhaps this value should be randomized?
112 $this->qrid
= $this->queue
->getName();
120 return array('title', 'queue', 'errorMode', 'isMinimal', 'onEnd', 'onEndUrl', 'pathPrefix', 'qrid', 'buttons');
124 * Redirect to the web-based queue-runner and evaluate all tasks in a queue.
126 public function runAllViaWeb() {
127 $_SESSION['queueRunners'][$this->qrid
] = serialize($this);
128 $url = CRM_Utils_System
::url($this->pathPrefix
. '/runner', 'reset=1&qrid=' . urlencode($this->qrid
));
129 CRM_Utils_System
::redirect($url);
130 // TODO: evaluate items incrementally via AJAX polling, cleanup session, call
134 * Immediately run all tasks in a queue (until either reaching the end
135 * of the queue or encountering an error)
137 * If the runner has an onEndUrl, then this function will not return
139 * @return mixed, TRUE if all tasks complete normally; otherwise, an array describing the failed task
141 public function runAll() {
142 $taskResult = $this->formatTaskResult(TRUE);
143 while ($taskResult['is_continue']) {
144 // setRaiseException should't be necessary here, but there's a bug
145 // somewhere which causes this setting to be lost. Observed while
146 // upgrading 4.0=>4.2. This preference really shouldn't be a global
147 // setting -- it should be more of a contextual/stack-based setting.
148 // This should be appropriate because queue-runners are not used with
149 // basic web pages -- they're used with CLI/REST/AJAX.
150 $errorScope = CRM_Core_TemporaryErrorScope
::useException();
151 $taskResult = $this->runNext();
155 if ($taskResult['numberOfItems'] == 0) {
156 $result = $this->handleEnd();
157 if (!empty($result['redirect_url'])) {
158 CRM_Utils_System
::redirect($result['redirect_url']);
168 * Take the next item from the queue and attempt to run it.
170 * Individual tasks may also throw exceptions -- caller should watch for exceptions
172 * @param $useSteal bool, whether to steal active locks
174 * @return array(is_error => bool, is_continue => bool, numberOfItems => int, 'last_task_title' => $, 'exception' => $)
176 public function runNext($useSteal = FALSE) {
178 $item = $this->queue
->stealItem();
181 $item = $this->queue
->claimItem();
185 $this->lastTaskTitle
= $item->data
->title
;
189 $isOK = $item->data
->run($this->getTaskContext());
191 $exception = new Exception('Task returned false');
200 $this->queue
->deleteItem($item);
203 $this->releaseErrorItem($item);
206 return $this->formatTaskResult($isOK, $exception);
209 return $this->formatTaskResult(FALSE, new Exception('Failed to claim next task'));
214 * Take the next item from the queue and attempt to run it.
216 * Individual tasks may also throw exceptions -- caller should watch for exceptions
218 * @param $useSteal bool, whether to steal active locks
220 * @return array(is_error => bool, is_continue => bool, numberOfItems => int)
222 public function skipNext($useSteal = FALSE) {
224 $item = $this->queue
->stealItem();
227 $item = $this->queue
->claimItem();
231 $this->lastTaskTitle
= $item->data
->title
;
232 $this->queue
->deleteItem($item);
233 return $this->formatTaskResult(TRUE);
236 return $this->formatTaskResult(FALSE, new Exception('Failed to claim next task'));
243 protected function releaseErrorItem($item) {
244 switch ($this->errorMode
) {
245 case self
::ERROR_CONTINUE
:
246 $this->queue
->deleteItem($item);
247 case self
::ERROR_ABORT
:
249 $this->queue
->releaseItem($item);
254 * @return array(is_error => bool, is_continue => bool, numberOfItems => int, redirect_url => string)
256 public function handleEnd() {
257 if (is_callable($this->onEnd
)) {
258 call_user_func($this->onEnd
, $this->getTaskContext());
260 // Don't remove queueRunner until onEnd succeeds
261 if (!empty($_SESSION['queueRunners'][$this->qrid
])) {
262 unset($_SESSION['queueRunners'][$this->qrid
]);
265 // Fallback; web UI does redirect in Javascript
267 $result['is_error'] = 0;
268 $result['numberOfItems'] = 0;
269 $result['is_continue'] = 0;
270 if (!empty($this->onEndUrl
)) {
271 $result['redirect_url'] = $this->onEndUrl
;
278 * @param null $exception
280 * @return array(is_error => bool, is_continue => bool, numberOfItems => int)
282 function formatTaskResult($isOK, $exception = NULL) {
283 $numberOfItems = $this->queue
->numberOfItems();
286 $result['is_error'] = $isOK ?
0 : 1;
287 $result['exception'] = $exception;
288 $result['last_task_title'] = isset($this->lastTaskTitle
) ?
$this->lastTaskTitle
: '';
289 $result['numberOfItems'] = $this->queue
->numberOfItems();
290 if ($result['numberOfItems'] <= 0) {
292 $result['is_continue'] = 0;
295 // more tasks remain, and this task succeeded
296 $result['is_continue'] = 1;
298 elseif ($this->errorMode
== CRM_Queue_Runner
::ERROR_CONTINUE
) {
299 // more tasks remain, and we can disregard this error
300 $result['is_continue'] = 1;
303 // more tasks remain, but we can't disregard the error
304 $result['is_continue'] = 0;
311 * @return CRM_Queue_TaskContext
313 protected function getTaskContext() {
314 if (!is_object($this->taskCtx
)) {
315 $this->taskCtx
= new CRM_Queue_TaskContext();
316 $this->taskCtx
->queue
= $this->queue
;
317 // $this->taskCtx->log = CRM_Core_Config::getLog();
318 $this->taskCtx
->log
= CRM_Core_Error
::createDebugLogger();
320 return $this->taskCtx
;