3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
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 |
9 +--------------------------------------------------------------------+
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:
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.
21 * This runner supports a few modes of operation, eg
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.
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.
31 * @see CRM_Queue_Autorunner
33 class CRM_Queue_Runner
{
36 * The failed task should be discarded, and queue processing should continue.
38 const ERROR_CONTINUE
= 1;
41 * The failed task should be kept in the queue, and queue processing should
44 const ERROR_ABORT
= 2;
52 * @var CRM_Queue_Queue
61 * queue-runner id; used for persistence
68 * Whether to display buttons, eg ('retry' => TRUE, 'skip' => FALSE)
73 * @var CRM_Queue_TaskContext
78 * Locate a previously-created instance of the queue-runner.
81 * The queue-runner ID.
83 * @return CRM_Queue_Runner|NULL
85 public static function instance($qrid) {
86 if (!empty($_SESSION['queueRunners'][$qrid])) {
87 return unserialize($_SESSION['queueRunners'][$qrid]);
96 * FIXME: parameter validation
97 * FIXME: document signature of onEnd callback
99 * @param array $runnerSpec
101 * - queue: CRM_Queue_Queue
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'.
109 public function __construct($runnerSpec) {
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);
114 $this->onEnd
= $runnerSpec['onEnd'] ??
NULL;
115 $this->onEndUrl
= $runnerSpec['onEndUrl'] ??
NULL;
116 $this->pathPrefix
= CRM_Utils_Array
::value('pathPrefix', $runnerSpec, 'civicrm/queue');
117 $this->buttons
= CRM_Utils_Array
::value('buttons', $runnerSpec, ['retry' => TRUE, 'skip' => TRUE]);
118 // perhaps this value should be randomized?
119 $this->qrid
= $this->queue
->getName();
125 public function __sleep() {
141 * Redirect to the web-based queue-runner and evaluate all tasks in a queue.
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);
147 // TODO: evaluate items incrementally via AJAX polling, cleanup session
151 * Immediately run all tasks in a queue (until either reaching the end
152 * of the queue or encountering an error)
154 * If the runner has an onEndUrl, then this function will not return
157 * TRUE if all tasks complete normally; otherwise, an array describing the
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
166 // setting -- it should be more of a contextual/stack-based setting.
167 // This should be appropriate because queue-runners are not used with
168 // basic web pages -- they're used with CLI/REST/AJAX.
169 $taskResult = $this->runNext();
173 if ($taskResult['numberOfItems'] == 0) {
174 $result = $this->handleEnd();
175 if (!empty($result['redirect_url'])) {
176 CRM_Utils_System
::redirect($result['redirect_url']);
186 * Take the next item from the queue and attempt to run it.
188 * Individual tasks may also throw exceptions -- caller should watch for
191 * @param bool $useSteal
192 * Whether to steal active locks.
195 * - is_error => bool,
196 * - is_continue => bool,
197 * - numberOfItems => int,
198 * - 'last_task_title' => $,
201 public function runNext($useSteal = FALSE) {
203 $item = $this->queue
->stealItem();
206 $item = $this->queue
->claimItem();
210 $this->lastTaskTitle
= $item->data
->title
;
214 CRM_Core_Error
::debug_log_message("Running task: " . $this->lastTaskTitle
);
215 $isOK = $item->data
->run($this->getTaskContext());
217 $exception = new Exception('Task returned false');
220 catch (Exception
$e) {
226 $this->queue
->deleteItem($item);
229 $this->releaseErrorItem($item);
232 return $this->formatTaskResult($isOK, $exception);
235 return $this->formatTaskResult(FALSE, new Exception('Failed to claim next task'));
240 * Take the next item from the queue and attempt to run it.
242 * Individual tasks may also throw exceptions -- caller should watch for
245 * @param bool $useSteal
246 * Whether to steal active locks.
249 * - is_error => bool,
250 * - is_continue => bool,
251 * - numberOfItems => int)
253 public function skipNext($useSteal = FALSE) {
255 $item = $this->queue
->stealItem();
258 $item = $this->queue
->claimItem();
262 $this->lastTaskTitle
= $item->data
->title
;
263 $this->queue
->deleteItem($item);
264 return $this->formatTaskResult(TRUE);
267 return $this->formatTaskResult(FALSE, new Exception('Failed to claim next task'));
272 * Release an item in keeping with the error mode.
274 * @param object $item
275 * The item previously produced by Queue::claimItem.
277 protected function releaseErrorItem($item) {
278 switch ($this->errorMode
) {
279 case self
::ERROR_CONTINUE
:
280 $this->queue
->deleteItem($item);
281 case self
::ERROR_ABORT
:
283 $this->queue
->releaseItem($item);
289 * - is_error => bool,
290 * - is_continue => bool,
291 * - numberOfItems => int,
292 * - redirect_url => string
294 public function handleEnd() {
295 if (is_callable($this->onEnd
)) {
296 call_user_func($this->onEnd
, $this->getTaskContext());
298 // Don't remove queueRunner until onEnd succeeds
299 if (!empty($_SESSION['queueRunners'][$this->qrid
])) {
300 unset($_SESSION['queueRunners'][$this->qrid
]);
303 // Fallback; web UI does redirect in Javascript
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
;
315 * Format a result record which describes whether the task completed.
318 * TRUE if the task completed successfully.
319 * @param Exception|null $exception
320 * If applicable, an unhandled exception that arose during execution.
323 * (is_error => bool, is_continue => bool, numberOfItems => int)
325 public function formatTaskResult($isOK, $exception = NULL) {
326 $numberOfItems = $this->queue
->numberOfItems();
329 $result['is_error'] = $isOK ?
0 : 1;
330 $result['exception'] = $exception;
331 $result['last_task_title'] = $this->lastTaskTitle ??
'';
332 $result['numberOfItems'] = $this->queue
->numberOfItems();
333 if ($result['numberOfItems'] <= 0) {
335 $result['is_continue'] = 0;
338 // more tasks remain, and this task succeeded
339 $result['is_continue'] = 1;
341 elseif ($this->errorMode
== CRM_Queue_Runner
::ERROR_CONTINUE
) {
342 // more tasks remain, and we can disregard this error
343 $result['is_continue'] = 1;
346 // more tasks remain, but we can't disregard the error
347 $result['is_continue'] = 0;
354 * @return CRM_Queue_TaskContext
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();
363 return $this->taskCtx
;