Merge pull request #15901 from eileenmcnaughton/matt
[civicrm-core.git] / CRM / Queue / Runner.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
bc77d7c0 4 | Copyright CiviCRM LLC. All rights reserved. |
6a488035 5 | |
bc77d7c0
TO
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
6a488035 9 +--------------------------------------------------------------------+
d25dd0ee 10 */
6a488035
TO
11
12/**
13 * The queue runner is a helper which runs all jobs in a queue.
14 *
15 * The queue runner is most useful for one-off queues (such as an upgrade);
16 * if the intention is to develop a dedicated, long-running worker thread,
17 * then one should consider writing a new queue consumer.
18 */
19class CRM_Queue_Runner {
20
21 /**
4523a2f5 22 * The failed task should be discarded, and queue processing should continue.
6a488035 23 */
4523a2f5 24 const ERROR_CONTINUE = 1;
6a488035
TO
25
26 /**
4523a2f5
TO
27 * The failed task should be kept in the queue, and queue processing should
28 * abort.
6a488035 29 */
4523a2f5 30 const ERROR_ABORT = 2;
6a488035
TO
31
32 /**
33 * @var string
34 */
4523a2f5 35 public $title;
6a488035
TO
36
37 /**
38 * @var CRM_Queue_Queue
39 */
4523a2f5
TO
40 public $queue;
41 public $errorMode;
42 public $isMinimal;
43 public $onEnd;
44 public $onEndUrl;
45 public $pathPrefix;
c86d4e7c
SL
46 /**
47 * queue-runner id; used for persistence
48 * @var int
49 */
4523a2f5 50 public $qrid;
6a488035
TO
51
52 /**
53 * @var array whether to display buttons, eg ('retry' => TRUE, 'skip' => FALSE)
54 */
4523a2f5 55 public $buttons;
6a488035
TO
56
57 /**
58 * @var CRM_Queue_TaskContext
59 */
4523a2f5 60 public $taskCtx;
6a488035
TO
61
62 /**
4523a2f5 63 * Locate a previously-created instance of the queue-runner.
6a488035 64 *
4523a2f5
TO
65 * @param string $qrid
66 * The queue-runner ID.
6a488035 67 *
4523a2f5 68 * @return CRM_Queue_Runner|NULL
6a488035 69 */
4523a2f5 70 public static function instance($qrid) {
6a488035
TO
71 if (!empty($_SESSION['queueRunners'][$qrid])) {
72 return unserialize($_SESSION['queueRunners'][$qrid]);
73 }
74 else {
75 return NULL;
76 }
77 }
78
79 /**
80 *
81 * FIXME: parameter validation
82 * FIXME: document signature of onEnd callback
83 *
4523a2f5
TO
84 * @param array $runnerSpec
85 * Array with keys:
6a488035 86 * - queue: CRM_Queue_Queue
4523a2f5
TO
87 * - errorMode: int, ERROR_CONTINUE or ERROR_ABORT.
88 * - onEnd: mixed, a callback to update the UI after running; should be
89 * both callable and serializable.
90 * - onEndUrl: string, the URL to which one redirects.
91 * - pathPrefix: string, prepended to URLs for the web-runner;
92 * default: 'civicrm/queue'.
6a488035
TO
93 */
94 public function __construct($runnerSpec) {
4523a2f5
TO
95 $this->title = CRM_Utils_Array::value('title', $runnerSpec, ts('Queue Runner'));
96 $this->queue = $runnerSpec['queue'];
97 $this->errorMode = CRM_Utils_Array::value('errorMode', $runnerSpec, self::ERROR_ABORT);
98 $this->isMinimal = CRM_Utils_Array::value('isMinimal', $runnerSpec, FALSE);
99 $this->onEnd = CRM_Utils_Array::value('onEnd', $runnerSpec, NULL);
100 $this->onEndUrl = CRM_Utils_Array::value('onEndUrl', $runnerSpec, NULL);
6a488035 101 $this->pathPrefix = CRM_Utils_Array::value('pathPrefix', $runnerSpec, 'civicrm/queue');
be2fb01f 102 $this->buttons = CRM_Utils_Array::value('buttons', $runnerSpec, ['retry' => TRUE, 'skip' => TRUE]);
6a488035
TO
103 // perhaps this value should be randomized?
104 $this->qrid = $this->queue->getName();
105 }
106
e0ef6999
EM
107 /**
108 * @return array
109 */
4523a2f5 110 public function __sleep() {
6a488035 111 // exclude taskCtx
be2fb01f 112 return [
4523a2f5
TO
113 'title',
114 'queue',
115 'errorMode',
116 'isMinimal',
117 'onEnd',
118 'onEndUrl',
119 'pathPrefix',
120 'qrid',
121 'buttons',
be2fb01f 122 ];
6a488035
TO
123 }
124
125 /**
126 * Redirect to the web-based queue-runner and evaluate all tasks in a queue.
127 */
128 public function runAllViaWeb() {
129 $_SESSION['queueRunners'][$this->qrid] = serialize($this);
130 $url = CRM_Utils_System::url($this->pathPrefix . '/runner', 'reset=1&qrid=' . urlencode($this->qrid));
131 CRM_Utils_System::redirect($url);
4523a2f5 132 // TODO: evaluate items incrementally via AJAX polling, cleanup session
6a488035
TO
133 }
134
135 /**
136 * Immediately run all tasks in a queue (until either reaching the end
137 * of the queue or encountering an error)
138 *
139 * If the runner has an onEndUrl, then this function will not return
140 *
4523a2f5
TO
141 * @return mixed
142 * TRUE if all tasks complete normally; otherwise, an array describing the
143 * failed task
6a488035
TO
144 */
145 public function runAll() {
146 $taskResult = $this->formatTaskResult(TRUE);
147 while ($taskResult['is_continue']) {
148 // setRaiseException should't be necessary here, but there's a bug
149 // somewhere which causes this setting to be lost. Observed while
150 // upgrading 4.0=>4.2. This preference really shouldn't be a global
03e04002 151 // setting -- it should be more of a contextual/stack-based setting.
6a488035
TO
152 // This should be appropriate because queue-runners are not used with
153 // basic web pages -- they're used with CLI/REST/AJAX.
154 $errorScope = CRM_Core_TemporaryErrorScope::useException();
155 $taskResult = $this->runNext();
156 $errorScope = NULL;
157 }
158
159 if ($taskResult['numberOfItems'] == 0) {
160 $result = $this->handleEnd();
161 if (!empty($result['redirect_url'])) {
162 CRM_Utils_System::redirect($result['redirect_url']);
163 }
164 return TRUE;
165 }
166 else {
167 return $taskResult;
168 }
169 }
170
171 /**
172 * Take the next item from the queue and attempt to run it.
173 *
4523a2f5
TO
174 * Individual tasks may also throw exceptions -- caller should watch for
175 * exceptions.
6a488035 176 *
4523a2f5
TO
177 * @param bool $useSteal
178 * Whether to steal active locks.
6a488035 179 *
4523a2f5
TO
180 * @return array
181 * - is_error => bool,
182 * - is_continue => bool,
183 * - numberOfItems => int,
184 * - 'last_task_title' => $,
185 * - 'exception' => $
6a488035
TO
186 */
187 public function runNext($useSteal = FALSE) {
188 if ($useSteal) {
189 $item = $this->queue->stealItem();
190 }
191 else {
192 $item = $this->queue->claimItem();
193 }
194
195 if ($item) {
196 $this->lastTaskTitle = $item->data->title;
197
198 $exception = NULL;
199 try {
9901c22f 200 CRM_Core_Error::debug_log_message("Running task: " . $this->lastTaskTitle);
6a488035
TO
201 $isOK = $item->data->run($this->getTaskContext());
202 if (!$isOK) {
203 $exception = new Exception('Task returned false');
4523a2f5 204 }
0db6c3e1
TO
205 }
206 catch (Exception$e) {
6a488035
TO
207 $isOK = FALSE;
208 $exception = $e;
4523a2f5 209 }
6a488035
TO
210
211 if ($isOK) {
212 $this->queue->deleteItem($item);
213 }
214 else {
215 $this->releaseErrorItem($item);
216 }
217
218 return $this->formatTaskResult($isOK, $exception);
219 }
220 else {
221 return $this->formatTaskResult(FALSE, new Exception('Failed to claim next task'));
222 }
223 }
224
225 /**
226 * Take the next item from the queue and attempt to run it.
227 *
4523a2f5
TO
228 * Individual tasks may also throw exceptions -- caller should watch for
229 * exceptions.
6a488035 230 *
4523a2f5
TO
231 * @param bool $useSteal
232 * Whether to steal active locks.
6a488035 233 *
4523a2f5
TO
234 * @return array
235 * - is_error => bool,
236 * - is_continue => bool,
237 * - numberOfItems => int)
6a488035
TO
238 */
239 public function skipNext($useSteal = FALSE) {
240 if ($useSteal) {
241 $item = $this->queue->stealItem();
242 }
243 else {
244 $item = $this->queue->claimItem();
245 }
246
247 if ($item) {
248 $this->lastTaskTitle = $item->data->title;
249 $this->queue->deleteItem($item);
250 return $this->formatTaskResult(TRUE);
251 }
252 else {
253 return $this->formatTaskResult(FALSE, new Exception('Failed to claim next task'));
254 }
255 }
256
e0ef6999 257 /**
4523a2f5
TO
258 * Release an item in keeping with the error mode.
259 *
260 * @param object $item
261 * The item previously produced by Queue::claimItem.
e0ef6999 262 */
6a488035
TO
263 protected function releaseErrorItem($item) {
264 switch ($this->errorMode) {
265 case self::ERROR_CONTINUE:
266 $this->queue->deleteItem($item);
267 case self::ERROR_ABORT:
268 default:
269 $this->queue->releaseItem($item);
270 }
271 }
272
273 /**
4523a2f5
TO
274 * @return array
275 * - is_error => bool,
276 * - is_continue => bool,
277 * - numberOfItems => int,
278 * - redirect_url => string
6a488035
TO
279 */
280 public function handleEnd() {
281 if (is_callable($this->onEnd)) {
282 call_user_func($this->onEnd, $this->getTaskContext());
283 }
284 // Don't remove queueRunner until onEnd succeeds
285 if (!empty($_SESSION['queueRunners'][$this->qrid])) {
286 unset($_SESSION['queueRunners'][$this->qrid]);
287 }
288
289 // Fallback; web UI does redirect in Javascript
be2fb01f 290 $result = [];
6a488035
TO
291 $result['is_error'] = 0;
292 $result['numberOfItems'] = 0;
293 $result['is_continue'] = 0;
294 if (!empty($this->onEndUrl)) {
295 $result['redirect_url'] = $this->onEndUrl;
296 }
297 return $result;
298 }
299
300 /**
4523a2f5 301 * Format a result record which describes whether the task completed.
6a488035 302 *
4523a2f5
TO
303 * @param bool $isOK
304 * TRUE if the task completed successfully.
305 * @param Exception|NULL $exception
306 * If applicable, an unhandled exception that arose during execution.
307 *
308 * @return array
309 * (is_error => bool, is_continue => bool, numberOfItems => int)
6a488035 310 */
4523a2f5 311 public function formatTaskResult($isOK, $exception = NULL) {
6a488035
TO
312 $numberOfItems = $this->queue->numberOfItems();
313
be2fb01f 314 $result = [];
6a488035
TO
315 $result['is_error'] = $isOK ? 0 : 1;
316 $result['exception'] = $exception;
317 $result['last_task_title'] = isset($this->lastTaskTitle) ? $this->lastTaskTitle : '';
318 $result['numberOfItems'] = $this->queue->numberOfItems();
319 if ($result['numberOfItems'] <= 0) {
320 // nothing to do
321 $result['is_continue'] = 0;
322 }
323 elseif ($isOK) {
324 // more tasks remain, and this task succeeded
325 $result['is_continue'] = 1;
326 }
327 elseif ($this->errorMode == CRM_Queue_Runner::ERROR_CONTINUE) {
328 // more tasks remain, and we can disregard this error
329 $result['is_continue'] = 1;
330 }
331 else {
332 // more tasks remain, but we can't disregard the error
333 $result['is_continue'] = 0;
334 }
335
336 return $result;
337 }
338
e0ef6999
EM
339 /**
340 * @return CRM_Queue_TaskContext
341 */
6a488035
TO
342 protected function getTaskContext() {
343 if (!is_object($this->taskCtx)) {
344 $this->taskCtx = new CRM_Queue_TaskContext();
345 $this->taskCtx->queue = $this->queue;
346 // $this->taskCtx->log = CRM_Core_Config::getLog();
347 $this->taskCtx->log = CRM_Core_Error::createDebugLogger();
348 }
349 return $this->taskCtx;
350 }
96025800 351
6a488035 352}