[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
1 <?php 2 3 /** 4 * @package Joomla.Administrator 5 * @subpackage com_scheduler 6 * 7 * @copyright (C) 2021 Open Source Matters, Inc. <https://www.joomla.org> 8 * @license GNU General Public License version 2 or later; see LICENSE.txt 9 */ 10 11 namespace Joomla\Component\Scheduler\Administrator\Task; 12 13 use Joomla\CMS\Application\CMSApplication; 14 use Joomla\CMS\Component\ComponentHelper; 15 use Joomla\CMS\Event\AbstractEvent; 16 use Joomla\CMS\Factory; 17 use Joomla\CMS\Language\Text; 18 use Joomla\CMS\Log\Log; 19 use Joomla\CMS\Plugin\PluginHelper; 20 use Joomla\Component\Scheduler\Administrator\Event\ExecuteTaskEvent; 21 use Joomla\Component\Scheduler\Administrator\Helper\ExecRuleHelper; 22 use Joomla\Component\Scheduler\Administrator\Helper\SchedulerHelper; 23 use Joomla\Component\Scheduler\Administrator\Scheduler\Scheduler; 24 use Joomla\Component\Scheduler\Administrator\Table\TaskTable; 25 use Joomla\Database\DatabaseDriver; 26 use Joomla\Database\DatabaseInterface; 27 use Joomla\Database\ParameterType; 28 use Joomla\Registry\Registry; 29 use Joomla\Utilities\ArrayHelper; 30 use Psr\Log\InvalidArgumentException; 31 use Psr\Log\LoggerAwareInterface; 32 use Psr\Log\LoggerAwareTrait; 33 34 // phpcs:disable PSR1.Files.SideEffects 35 \defined('_JEXEC') or die; 36 // phpcs:enable PSR1.Files.SideEffects 37 38 /** 39 * The Task class defines methods for the execution, logging and 40 * related properties of Tasks as supported by `com_scheduler`, 41 * a Task Scheduling component. 42 * 43 * @since 4.1.0 44 */ 45 class Task implements LoggerAwareInterface 46 { 47 use LoggerAwareTrait; 48 49 /** 50 * Enumerated state for enabled tasks. 51 * 52 * @since 4.1.0 53 */ 54 public const STATE_ENABLED = 1; 55 56 /** 57 * Enumerated state for disabled tasks. 58 * 59 * @since 4.1.0 60 */ 61 public const STATE_DISABLED = 0; 62 63 /** 64 * Enumerated state for trashed tasks. 65 * 66 * @since 4.1.0 67 */ 68 public const STATE_TRASHED = -2; 69 70 /** 71 * Map state enumerations to logical language adjectives. 72 * 73 * @since 4.1.0 74 */ 75 public const STATE_MAP = [ 76 self::STATE_TRASHED => 'trashed', 77 self::STATE_DISABLED => 'disabled', 78 self::STATE_ENABLED => 'enabled', 79 ]; 80 81 /** 82 * The task snapshot 83 * 84 * @var array 85 * @since 4.1.0 86 */ 87 protected $snapshot = []; 88 89 /** 90 * @var Registry 91 * @since 4.1.0 92 */ 93 protected $taskRegistry; 94 95 /** 96 * @var string 97 * @since 4.1.0 98 */ 99 public $logCategory; 100 101 /** 102 * @var CMSApplication 103 * @since 4.1.0 104 */ 105 protected $app; 106 107 /** 108 * @var DatabaseInterface 109 * @since 4.1.0 110 */ 111 protected $db; 112 113 /** 114 * Maps task exit codes to events which should be dispatched when the task finishes. 115 * 'NA' maps to the event for general task failures. 116 * 117 * @var string[] 118 * @since 4.1.0 119 */ 120 protected const EVENTS_MAP = [ 121 Status::OK => 'onTaskExecuteSuccess', 122 Status::NO_ROUTINE => 'onTaskRoutineNotFound', 123 Status::WILL_RESUME => 'onTaskRoutineWillResume', 124 'NA' => 'onTaskExecuteFailure', 125 ]; 126 127 /** 128 * Constructor for {@see Task}. 129 * 130 * @param object $record A task from {@see TaskTable}. 131 * 132 * @since 4.1.0 133 * @throws \Exception 134 */ 135 public function __construct(object $record) 136 { 137 // Workaround because Registry dumps private properties otherwise. 138 $taskOption = $record->taskOption; 139 $record->params = json_decode($record->params, true); 140 141 $this->taskRegistry = new Registry($record); 142 143 $this->set('taskOption', $taskOption); 144 $this->app = Factory::getApplication(); 145 $this->db = Factory::getContainer()->get(DatabaseDriver::class); 146 $this->setLogger(Log::createDelegatedLogger()); 147 $this->logCategory = 'task' . $this->get('id'); 148 149 if ($this->get('params.individual_log')) { 150 $logFile = $this->get('params.log_file') ?? 'task_' . $this->get('id') . '.log.php'; 151 152 $options['text_entry_format'] = '{DATE} {TIME} {PRIORITY} {MESSAGE}'; 153 $options['text_file'] = $logFile; 154 Log::addLogger($options, Log::ALL, [$this->logCategory]); 155 } 156 } 157 158 /** 159 * Get the task as a data object that can be stored back in the database. 160 * ! This method should be removed or changed as part of a better API implementation for the driver. 161 * 162 * @return object 163 * 164 * @since 4.1.0 165 */ 166 public function getRecord(): object 167 { 168 // ! Probably, an array instead 169 $recObject = $this->taskRegistry->toObject(); 170 171 $recObject->cron_rules = (array) $recObject->cron_rules; 172 173 return $recObject; 174 } 175 176 /** 177 * Execute the task. 178 * 179 * @return boolean True if success 180 * 181 * @since 4.1.0 182 * @throws \Exception 183 */ 184 public function run(): bool 185 { 186 /** 187 * We try to acquire the lock here, only if we don't already have one. 188 * We do this, so we can support two ways of running tasks: 189 * 1. Directly through {@see Scheduler}, which optimises acquiring a lock while fetching from the task queue. 190 * 2. Running a task without a pre-acquired lock. 191 * ! This needs some more thought, for whether it should be allowed or if the single-query optimisation 192 * should be used everywhere, although it doesn't make sense in the context of fetching 193 * a task when it doesn't need to be run. This might be solved if we force a re-fetch 194 * with the lock or do it here ourselves (using acquireLock as a proxy to the model's 195 * getter). 196 */ 197 if ($this->get('locked') === null) { 198 $this->acquireLock(); 199 } 200 201 // Exit early if task routine is not available 202 if (!SchedulerHelper::getTaskOptions()->findOption($this->get('type'))) { 203 $this->snapshot['status'] = Status::NO_ROUTINE; 204 $this->skipExecution(); 205 $this->dispatchExitEvent(); 206 207 return $this->isSuccess(); 208 } 209 210 $this->snapshot['status'] = Status::RUNNING; 211 $this->snapshot['taskStart'] = $this->snapshot['taskStart'] ?? microtime(true); 212 $this->snapshot['netDuration'] = 0; 213 214 /** @var ExecuteTaskEvent $event */ 215 $event = AbstractEvent::create( 216 'onExecuteTask', 217 [ 218 'eventClass' => ExecuteTaskEvent::class, 219 'subject' => $this, 220 'routineId' => $this->get('type'), 221 'langConstPrefix' => $this->get('taskOption')->langConstPrefix, 222 'params' => $this->get('params'), 223 ] 224 ); 225 226 PluginHelper::importPlugin('task'); 227 228 try { 229 $this->app->getDispatcher()->dispatch('onExecuteTask', $event); 230 } catch (\Exception $e) { 231 // Suppress the exception for now, we'll throw it again once it's safe 232 $this->log(Text::sprintf('COM_SCHEDULER_TASK_ROUTINE_EXCEPTION', $e->getMessage()), 'error'); 233 $this->snapshot['exception'] = $e; 234 $this->snapshot['status'] = Status::KNOCKOUT; 235 } 236 237 $resultSnapshot = $event->getResultSnapshot(); 238 239 $this->snapshot['taskEnd'] = microtime(true); 240 $this->snapshot['netDuration'] = $this->snapshot['taskEnd'] - $this->snapshot['taskStart']; 241 $this->snapshot = array_merge($this->snapshot, $resultSnapshot); 242 243 // @todo make the ExecRuleHelper usage less ugly, perhaps it should be composed into Task 244 // Update object state. 245 $this->set('last_execution', Factory::getDate('@' . (int) $this->snapshot['taskStart'])->toSql()); 246 $this->set('last_exit_code', $this->snapshot['status']); 247 248 if ($this->snapshot['status'] !== Status::WILL_RESUME) { 249 $this->set('next_execution', (new ExecRuleHelper($this->taskRegistry->toObject()))->nextExec()); 250 $this->set('times_executed', $this->get('times_executed') + 1); 251 } else { 252 /** 253 * Resumable tasks need special handling. 254 * 255 * They are rescheduled as soon as possible to let their next step to be executed without 256 * a very large temporal gap to the previous step. 257 * 258 * Moreover, the times executed does NOT increase for each step. It will increase once, 259 * after the last step, when they return Status::OK. 260 */ 261 $this->set('next_execution', Factory::getDate('now', 'UTC')->sub(new \DateInterval('PT1M'))->toSql()); 262 } 263 264 // The only acceptable "successful" statuses are either clean exit or resuming execution. 265 if (!in_array($this->snapshot['status'], [Status::WILL_RESUME, Status::OK])) { 266 $this->set('times_failed', $this->get('times_failed') + 1); 267 } 268 269 if (!$this->releaseLock()) { 270 $this->snapshot['status'] = Status::NO_RELEASE; 271 } 272 273 $this->dispatchExitEvent(); 274 275 if (!empty($this->snapshot['exception'])) { 276 throw $this->snapshot['exception']; 277 } 278 279 return $this->isSuccess(); 280 } 281 282 /** 283 * Get the task execution snapshot. 284 * ! Access locations will need updates once a more robust Snapshot container is implemented. 285 * 286 * @return array 287 * 288 * @since 4.1.0 289 */ 290 public function getContent(): array 291 { 292 return $this->snapshot; 293 } 294 295 /** 296 * Acquire a pseudo-lock on the task record. 297 * ! At the moment, this method is not used anywhere as task locks are already 298 * acquired when they're fetched. As such this method is not functional and should 299 * not be reviewed until it is updated. 300 * 301 * @return boolean 302 * 303 * @since 4.1.0 304 * @throws \Exception 305 */ 306 public function acquireLock(): bool 307 { 308 $db = $this->db; 309 $query = $db->getQuery(true); 310 $id = $this->get('id'); 311 $now = Factory::getDate('now', 'GMT'); 312 313 $timeout = ComponentHelper::getParams('com_scheduler')->get('timeout', 300); 314 $timeout = new \DateInterval(sprintf('PT%dS', $timeout)); 315 $timeoutThreshold = (clone $now)->sub($timeout)->toSql(); 316 $now = $now->toSql(); 317 318 // @todo update or remove this method 319 $query->update($db->qn('#__scheduler_tasks')) 320 ->set('locked = :now') 321 ->where($db->qn('id') . ' = :taskId') 322 ->extendWhere( 323 'AND', 324 [ 325 $db->qn('locked') . ' < :threshold', 326 $db->qn('locked') . 'IS NULL', 327 ], 328 'OR' 329 ) 330 ->bind(':taskId', $id, ParameterType::INTEGER) 331 ->bind(':now', $now) 332 ->bind(':threshold', $timeoutThreshold); 333 334 try { 335 $db->lockTable('#__scheduler_tasks'); 336 $db->setQuery($query)->execute(); 337 } catch (\RuntimeException $e) { 338 return false; 339 } finally { 340 $db->unlockTables(); 341 } 342 343 if ($db->getAffectedRows() === 0) { 344 return false; 345 } 346 347 $this->set('locked', $now); 348 349 return true; 350 } 351 352 /** 353 * Remove the pseudo-lock and optionally update the task record. 354 * 355 * @param bool $update If true, the record is updated with the snapshot 356 * 357 * @return boolean 358 * 359 * @since 4.1.0 360 * @throws \Exception 361 */ 362 public function releaseLock(bool $update = true): bool 363 { 364 $db = $this->db; 365 $query = $db->getQuery(true); 366 $id = $this->get('id'); 367 368 $query->update($db->qn('#__scheduler_tasks', 't')) 369 ->set('locked = NULL') 370 ->where($db->qn('id') . ' = :taskId') 371 ->where($db->qn('locked') . ' IS NOT NULL') 372 ->bind(':taskId', $id, ParameterType::INTEGER); 373 374 if ($update) { 375 $exitCode = $this->get('last_exit_code'); 376 $lastExec = $this->get('last_execution'); 377 $nextExec = $this->get('next_execution'); 378 $timesFailed = $this->get('times_failed'); 379 $timesExecuted = $this->get('times_executed'); 380 381 $query->set( 382 [ 383 'last_exit_code = :exitCode', 384 'last_execution = :lastExec', 385 'next_execution = :nextExec', 386 'times_executed = :times_executed', 387 'times_failed = :times_failed', 388 ] 389 ) 390 ->bind(':exitCode', $exitCode, ParameterType::INTEGER) 391 ->bind(':lastExec', $lastExec) 392 ->bind(':nextExec', $nextExec) 393 ->bind(':times_executed', $timesExecuted) 394 ->bind(':times_failed', $timesFailed); 395 } 396 397 try { 398 $db->setQuery($query)->execute(); 399 } catch (\RuntimeException $e) { 400 return false; 401 } 402 403 if (!$db->getAffectedRows()) { 404 return false; 405 } 406 407 $this->set('locked', null); 408 409 return true; 410 } 411 412 /** 413 * @param string $message Log message 414 * @param string $priority Log level, defaults to 'info' 415 * 416 * @return void 417 * 418 * @since 4.1.0 419 * @throws InvalidArgumentException 420 */ 421 public function log(string $message, string $priority = 'info'): void 422 { 423 $this->logger->log($priority, $message, ['category' => $this->logCategory]); 424 } 425 426 /** 427 * Advance the task entry's next calculated execution, effectively skipping the current execution. 428 * 429 * @return void 430 * 431 * @since 4.1.0 432 * @throws \Exception 433 */ 434 public function skipExecution(): void 435 { 436 $db = $this->db; 437 $query = $db->getQuery(true); 438 439 $id = $this->get('id'); 440 $nextExec = (new ExecRuleHelper($this->taskRegistry->toObject()))->nextExec(true, true); 441 442 $query->update($db->qn('#__scheduler_tasks', 't')) 443 ->set('t.next_execution = :nextExec') 444 ->where('t.id = :id') 445 ->bind(':nextExec', $nextExec) 446 ->bind(':id', $id); 447 448 try { 449 $db->setQuery($query)->execute(); 450 } catch (\RuntimeException $e) { 451 } 452 453 $this->set('next_execution', $nextExec); 454 } 455 456 /** 457 * Handles task exit (dispatch event). 458 * 459 * @return void 460 * 461 * @since 4.1.0 462 * 463 * @throws \UnexpectedValueException|\BadMethodCallException 464 */ 465 protected function dispatchExitEvent(): void 466 { 467 $exitCode = $this->snapshot['status'] ?? 'NA'; 468 $eventName = self::EVENTS_MAP[$exitCode] ?? self::EVENTS_MAP['NA']; 469 470 $event = AbstractEvent::create( 471 $eventName, 472 [ 473 'subject' => $this, 474 ] 475 ); 476 477 $this->app->getDispatcher()->dispatch($eventName, $event); 478 } 479 480 /** 481 * Was the task successful? 482 * 483 * @return boolean True if the task was successful. 484 * @since 4.1.0 485 */ 486 public function isSuccess(): bool 487 { 488 return in_array(($this->snapshot['status'] ?? null), [Status::OK, Status::WILL_RESUME]); 489 } 490 491 /** 492 * Set a task property. This method is a proxy to {@see Registry::set()}. 493 * 494 * @param string $path Registry path of the task property. 495 * @param mixed $value The value to set to the property. 496 * @param ?string $separator The key separator. 497 * 498 * @return mixed|null 499 * 500 * @since 4.1.0 501 */ 502 protected function set(string $path, $value, string $separator = null) 503 { 504 return $this->taskRegistry->set($path, $value, $separator); 505 } 506 507 /** 508 * Get a task property. This method is a proxy to {@see Registry::get()}. 509 * 510 * @param string $path Registry path of the task property. 511 * @param mixed $default Default property to return, if the actual value is null. 512 * 513 * @return mixed The task property. 514 * 515 * @since 4.1.0 516 */ 517 public function get(string $path, $default = null) 518 { 519 return $this->taskRegistry->get($path, $default); 520 } 521 522 /** 523 * Static method to determine whether an enumerated task state (as a string) is valid. 524 * 525 * @param string $state The task state (enumerated, as a string). 526 * 527 * @return boolean 528 * 529 * @since 4.1.0 530 */ 531 public static function isValidState(string $state): bool 532 { 533 if (!is_numeric($state)) { 534 return false; 535 } 536 537 // Takes care of interpreting as float/int 538 $state = $state + 0; 539 540 return ArrayHelper::getValue(self::STATE_MAP, $state) !== null; 541 } 542 543 /** 544 * Static method to determine whether a task id is valid. Note that this does not 545 * validate ids against the database, but only verifies that an id may exist. 546 * 547 * @param string $id The task id (as a string). 548 * 549 * @return boolean 550 * 551 * @since 4.1.0 552 */ 553 public static function isValidId(string $id): bool 554 { 555 $id = is_numeric($id) ? ($id + 0) : $id; 556 557 if (!\is_int($id) || $id <= 0) { 558 return false; 559 } 560 561 return true; 562 } 563 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Sep 7 05:41:13 2022 | Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer |