[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/administrator/components/com_scheduler/src/Task/ -> Task.php (source)

   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  }


Generated: Wed Sep 7 05:41:13 2022 Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer