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