foldershare-8.x-1.2/src/Entity/FolderShareScheduledTask.php
src/Entity/FolderShareScheduledTask.php
<?php namespace Drupal\foldershare\Entity; use Drupal\Core\Database\Database; use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\foldershare\ManageLog; use Drupal\foldershare\Entity\Exception\ValidationException; use Drupal\foldershare\Utilities\LimitUtilities; /** * Describes an internal task to be performed at a time in the future. * * The module's internal scheduled tasks are used to perform background * activity to finish long operations, such as copying, moving, or deleting * a large folder tree. Each task has: * * - A callback used to run the task. * * - An array of parameters, such as lists of entity IDs to copy, move, * or delete. * * Each task has three dates/times: * - The date/time the task was first started. * - The date/time the current task object was created. * - The date/time the task is scheduled to run. * * When the task is first started, the creation date matches the start date. * The scheduled run date is a short time in the future. Each time the task * is run, it is removed from the database and its parameters passed to * the task callback. If a callback cannot finish the task within * execution and memory use limits, the callback creates a new task to continue * the work. The new task has the same original start date, a new creation * date, and a future scheduled date. * * For debugging and monitoring, each task also has: * - A brief text comment describing the task. * - A total execution time so far, measured in seconds. * * Like all entities, a task also has: * - An ID. * - A unique ID. * - A user ID for the user that started the task. * * The user ID is the current user when the task was first started. The task * will run by CRON or at the end of pages delivered to any user. * For those runs, the current user is whomever is running CRON or * whomever got the most recent page, but the user ID in the task remains the * ID of the original user that started the task. It is this original user * that owns any new content created by the task. * * <B>Task processing</B> * The list of scheduled tasks is processed in one of three ways: * - At the end of a request. * - When CRON runs. * - From a drush command. * * The module provides an event subscriber that listens for the "terminate" * event sent after a request has been finished. This is normally at the end * of every page delivered to a user, or after any REST or AJAX request. * The event subscriber calls this class's executeTasks() method. * * The module includes a CRON hook that is called each time CRON is invoked, * whether via an external CRON trigger or via a "terminate" event listened * to by the Automated Cron module. At this time, the hook calls this class's * executeTasks() method. * * The module includes drush commands to list tasks, delete tasks, and run * tasks. Drush can call this class's executeTasks() method. * * In any case, executeTasks() quickly checks if there are any tasks ready * to run, then runs them. A task is ready to run only if its scheduled time * is equal to the current time or in the past. If there are multiple tasks * ready to run, they are executed in order of their scheduled times. * * <B>Task visibility</B> * Tasks are strictly an internal implementation detail of this module. * They are not intended to be seen by users or administrators. For this * reason, the entity definition intentionally omits features: * - The entity type is marked as internal. * - None of the entity type's fields are viewable. * - None of the entity type's fields are editable. * - The entity type is not fieldable. * - The entity type has no "views_builder" to present view pages. * - The entity type has no "views_data" for creating views. * - The entity type has no "list_builder" to show lists. * - The entity type has no edit forms. * - The entity type has no access controller. * - The entity type has no admin permission. * - The entity type has no routes. * - The entity type has no caches. * * <B>Warning:</B> This class is strictly internal to the FolderShare * module. The class's existance, name, and content may change from * release to release without any promise of backwards compatability. * * @ingroup foldershare * * @see foldershare_cron() * @see \Drupal\foldershare\EventSubscriber\FolderShareScheduledTaskHandler * * @ContentEntityType( * id = "foldershare_scheduledtask", * label = @Translation("FolderShare internal scheduled task"), * base_table = "foldershare_scheduledtask", * internal = TRUE, * persistent_cache = FALSE, * render_cache = FALSE, * static_cache = FALSE, * fieldable = FALSE, * entity_keys = { * "id" = "id", * "uuid" = "uuid", * "label" = "operation", * }, * ) */ final class FolderShareScheduledTask extends ContentEntityBase { /*--------------------------------------------------------------------- * * Fields. * *---------------------------------------------------------------------*/ /** * Indicates if task execution is enabled. * * In normal use, this value is TRUE. However, if task execution needs * to be disabled FOR THE CURRENT PROCESS ONLY, this flag may be set * to FALSE. Future calls to executeTasks() will return doing nothing. * * This is primarily used by drush commands to disable task execution * during a command so that tasks don't interfer with whatever the command * is trying to do. * * @var bool * * @see ::executeTasks() * @see ::isTaskExecutionEnabled() * @see ::setTaskExecutionEnabled() */ private static $enabled = TRUE; /*--------------------------------------------------------------------- * * Constants - Entity type id. * *---------------------------------------------------------------------*/ /** * The entity type id for the FolderShare Scheduled Task entity. * * This is 'foldershare_scheduledtask' and it must match the entity type * declaration in this class's comment block. * * @var string */ const ENTITY_TYPE_ID = 'foldershare_scheduledtask' ; /*--------------------------------------------------------------------- * * Constants - Database tables. * *---------------------------------------------------------------------*/ /** * The base table for 'foldershare_scheduledtask' entities. * * This is 'foldershare_scheduledtask' and it must match the base table * declaration in this class's comment block. * * @var string */ const BASE_TABLE = 'foldershare_scheduledtask' ; /*--------------------------------------------------------------------- * * Entity definition. * *---------------------------------------------------------------------*/ /** * Defines the fields used by instances of this class. * * The following fields are defined, along with their intended * public or private access: * * | Field | Allow for view | Allow for edit | * | ---------------- | -------------- | -------------- | * | id | no | no | * | uuid | no | no | * | uid | no | no | * | created | no | no | * | operation | no | no | * | parameters | no | no | * | scheduled | no | no | * | started | no | no | * | comments | no | no | * | executiontime | no | no | * * Some fields are supported by parent class methods: * * | Field | Get method | * | ---------------- | ------------------------------------ | * | id | ContentEntityBase::id() | * | uuid | ContentEntityBase::uuid() | * | operation | ContentEntityBase::getName() | * * Some fields are supported by methods in this class: * * | Field | Get method | * | ---------------- | ------------------------------------ | * | uid | getRequester() | * | created | getCreatedTime() | * | operation | getCallback() | * | parameters | getParameters() | * | scheduled | getScheduledTime() | * | started | getStartedTime() | * | comments | getComments() | * | executiontime | getAccumulatedExecutionTime() | * * @param \Drupal\Core\Entity\EntityTypeInterface $entityType * The entity type for which we are returning base field definitions. * * @return array * An array of field definitions where keys are field names and * values are BaseFieldDefinition objects. */ public static function baseFieldDefinitions(EntityTypeInterface $entityType ) { // // Base class fields // ----------------- // The parent ContentEntityBase class supports several standard // entity fields: // // - id: the entity ID // - uuid: the entity unique ID // - langcode: the content language // - revision: the revision ID // - bundle: the entity bundle // // The parent class ONLY defines these fields if they exist in // THIS class's comment block declaring class fields. Of the // above fields, we only define these for this class: // // - id // - uuid // // By invoking the parent class, we don't have to define these // ourselves below. $fields = parent::baseFieldDefinitions( $entityType ); // Entity id. // This field was already defined by the parent class. $fields [ $entityType ->getKey( 'id' )] ->setDescription(t( 'The ID of the task.' )) ->setDisplayConfigurable( 'view' , FALSE); // Unique id (UUID). // This field was already defined by the parent class. $fields [ $entityType ->getKey( 'uuid' )] ->setDescription(t( 'The UUID of the task.' )) ->setDisplayConfigurable( 'view' , FALSE) ->setDisplayConfigurable( 'form' , FALSE); // // Common fields // ------------- // Tasks have several fields describing the task, when it was started, // and when it should next run. // // Operation (callback). $fields [ 'operation' ] = BaseFieldDefinition::create( 'string' ) ->setLabel(t( 'Callback' )) ->setDescription(t( 'The task callback.' )) ->setRequired(TRUE) ->setSettings([ 'default_value' => '' , 'max_length' => 256, 'text_processing' => FALSE, ]) ->setDisplayConfigurable( 'view' , FALSE) ->setDisplayConfigurable( 'form' , FALSE); // Requester (original) user id. $fields [ 'uid' ] = BaseFieldDefinition::create( 'entity_reference' ) ->setLabel(t( 'Requester' )) ->setDescription(t( "The user ID that requested the task." )) ->setRequired(TRUE) ->setSetting( 'target_type' , 'user' ) ->setSetting( 'handler' , 'default' ) ->setDefaultValueCallback( 'Drupal\foldershare\Entity\FolderShareScheduledTask::getCurrentUserId' ) ->setDisplayConfigurable( 'view' , FALSE) ->setDisplayConfigurable( 'form' , FALSE); // Creation date. $fields [ 'created' ] = BaseFieldDefinition::create( 'created' ) ->setLabel(t( 'Created date' )) ->setDescription(t( 'The date and time when this task entry was created.' )) ->setRequired(TRUE) ->setDisplayConfigurable( 'view' , FALSE) ->setDisplayConfigurable( 'form' , FALSE); // Scheduled time to run. $fields [ 'scheduled' ] = BaseFieldDefinition::create( 'timestamp' ) ->setLabel(t( 'Scheduled time' )) ->setDescription(t( 'The date and time when the task is scheduled to run.' )) ->setRequired(TRUE) ->setDisplayConfigurable( 'view' , FALSE) ->setDisplayConfigurable( 'form' , FALSE); // Task parameters. $fields [ 'parameters' ] = BaseFieldDefinition::create( 'string' ) ->setLabel(t( 'Task parameters' )) ->setDescription(t( 'The JSON parameters for the task.' )) ->setRequired(FALSE) ->setDisplayConfigurable( 'view' , FALSE) ->setDisplayConfigurable( 'form' , FALSE); // Original operation date. $fields [ 'started' ] = BaseFieldDefinition::create( 'timestamp' ) ->setLabel(t( 'Original start date' )) ->setDescription(t( 'The date and time when the operation started.' )) ->setRequired(FALSE) ->setDisplayConfigurable( 'view' , FALSE) ->setDisplayConfigurable( 'form' , FALSE); // Comments. $fields [ 'comments' ] = BaseFieldDefinition::create( 'string' ) ->setLabel(t( 'Comments' )) ->setDescription(t( "The task's comments." )) ->setRequired(FALSE) ->setDisplayConfigurable( 'view' , FALSE) ->setDisplayConfigurable( 'form' , FALSE); // Accumulated run time. $fields [ 'executiontime' ] = BaseFieldDefinition::create( 'integer' ) ->setLabel(t( 'Accumulated execution time' )) ->setDescription(t( "The task's total execution time to date." )) ->setRequired(FALSE) ->setDisplayConfigurable( 'view' , FALSE) ->setDisplayConfigurable( 'form' , FALSE); return $fields ; } /*--------------------------------------------------------------------- * * General utilities. * *---------------------------------------------------------------------*/ /** * Returns the current user ID. * * This function provides the deault value callback for the 'uid' * base field definition. * * @return array * An array of default values. In this case, the array only * contains the current user ID. * * @see ::baseFieldDefinitions() */ public static function getCurrentUserId() { return [\Drupal::currentUser()->id()]; } /** * Validates a class and method callback. * * A standard PHP callback has one of these forms: * - String: * - FUNCTIONNAME * - CLASSNAME::METHODNAME * - CLASSOBJECT::METHODNAME * - Array: * - [CLASSNAME, METHODNAME] * - [CLASSOBJECT, METHODNAME] * * FUNCTIONNAME, CLASSNAME, and METHODNAME must exist. The METHODNAME must * be a public static method. * * @param mixed $callback * A string naming a function or a class and public static method, or * an array with two values that include either the class name or an * object instance, and a public static method name. * * @return string * Returns a string of the form "FUNCTIONNAME" or "CLASSNAME::METHODNAME", * where the function, class, and method have all been verified. * * @throws \Drupal\foldershare\Entity\Exception\ValidationException * Throws an exception if the callback is malformed, if the function, * class, or method does not exist, or if the method is not public * and static. * * @see ::createTask() */ private static function verifyCallback( $callback ) { // PHP's is_callable() checks for all valid forms of a callback, // including verifying that a function, class, or method exists. // It returns TRUE/FALSE and sets the callable name to a string // of the form "FUNCTIONNAME" or "CLASSNAME::METHODNAME". $callableName = '' ; if ( is_callable ( $callback , FALSE, $callableName ) === FALSE) { throw new ValidationException(t( 'Malformed task callback is not in a recognized format or a class, method, or function does not exist.' )); } // PHP's is_callable() does not check that the method is static and // public. PHP's call_user_func() handles non-static and/or non-public // methods with errors: // - If the method is private, PHP issues a warning. // - If the method is not static, PHP issues a fatal error and aborts. // // We'd rather not have these happen during a task, so we need to // validate that a named method is public and static. This requires // using the PHP reflection API. // // Split the callable name into CLASSNAME and METHODNAME, or just // FUNCTIONNAME. For the FUNCTIONNAME case, there is no further // checking required. $pieces = explode ( '::' , $callableName ); if ( count ( $pieces ) !== 2) { return $callableName ; } $className = $pieces [0]; $methodName = $pieces [1]; // Get the reflection. Since is_callable() has already confirmed that // the class and method exist, this should not throw exceptions. try { $rClass = new \ReflectionClass( $className ); $rMethod = $rClass ->getMethod( $methodName ); if ( $rMethod ->isStatic() === FALSE) { throw new ValidationException(t( 'Invalid task callback method @method is not static.' , [ '@method' => $methodName , ])); } if ( $rMethod ->isPublic() === FALSE) { throw new ValidationException(t( 'Invalid task callback method @method is not public.' , [ '@method' => $methodName , ])); } } catch (\Exception $e ) { throw new ValidationException(t( 'Malformed task callback class or method does not exist.' )); } return $callableName ; } /*--------------------------------------------------------------------- * * Create. * *---------------------------------------------------------------------*/ /** * Creates a new task. * * Every task has: * - A "class::method" string for the task callback. * - A set of parameters for that task (this may be empty). * - A user ID for the user that requested the task. * - A timestamp for the future time at which to run the task. * - A timestamp for when the operation was first started. * - Comments to describe the task during debugging. * - An accumulated run time, in seconds. * * Parameters must be appropriate for the task. * * This method should be used in preference to create() in order to * insure that the callback is valid and to use JSON encoding to process * task parameters to be saved with the task. * * @param int $timestamp * The future time at which the task will be executed. * @param mixed $callback * The callback for the task following standard PHP callable formats * (e.g. "class::method" or [object, "method"], etc.). * @param int $requester * (optional, default = (-1) = current user) The user ID of the * individual causing the task to be created. * @param array $parameters * (optional, default = NULL) An array of parameters to save with the * task and pass to the task when it is executed. * @param int $started * (optional, default = 0) The timestamp of the start date & time for * an operation that causes a chain of tasks. * @param string $comments * (optional, default = '') A comment on the current task. * @param int $executionTime * (optional, default = 0) The accumulated total execution time of the * task chain, in seconds. * * @return \Drupal\foldershare\Entity\FolderShareScheduledTask * Returns the newly created task. The task will already have been saved * to the task table. * * @throws \Drupal\foldershare\Entity\Exception\ValidationException * Throws an exception if the callback is malformed or unrecognized, or * if the the parameters array cannot be encoded as JSON. */ public static function createTask( int $timestamp , $callback , int $requester = (-1), array $parameters = NULL, int $started = 0, string $comments = '' , int $executionTime = 0) { // Validate the callback. switch ( $callback ) { // Well-known callbacks. case 'changeowner' : $callback = '\Drupal\foldershare\Entity\FolderShare::processTaskChangeOwner' ; break ; case 'copy-to-folder' : $callback = '\Drupal\foldershare\Entity\FolderShare::processTaskCopyToFolder' ; break ; case 'copy-to-root' : $callback = '\Drupal\foldershare\Entity\FolderShare::processTaskCopyToRoot' ; break ; case 'delete-hide' : $callback = '\Drupal\foldershare\Entity\FolderShare::processTaskDelete1' ; break ; case 'delete-delete' : $callback = '\Drupal\foldershare\Entity\FolderShare::processTaskDelete2' ; break ; case 'move-to-folder' : $callback = '\Drupal\foldershare\Entity\FolderShare::processTaskMoveToFolder' ; break ; case 'move-to-root' : $callback = '\Drupal\foldershare\Entity\FolderShare::processTaskMoveToRoot' ; break ; case 'rebuildusage' : $callback = '\Drupal\foldershare\ManageUsageStatistics::taskUpdateUsage' ; break ; } // Throws an exception if the callback does not validate. $callback = self::verifyCallback( $callback ); // Insure we have a requester. if ( $requester < 0) { $requester = (int) \Drupal::currentUser()->id(); } // Convert parameters to a JSON encoding. if ( $parameters === NULL) { $json = '' ; } else { $json = json_encode( $parameters ); if ( $json === FALSE) { throw new ValidationException(t( 'Task parameters cannot be JSON encoded for "@callback".' , [ '@callback' => $callback , ])); } } // Create the task. Let the ID, UUID, and creation date be automatically // assigned. $task = self::create([ // Required fields. 'operation' => $callback , 'uid' => $requester , 'scheduled' => $timestamp , // Optional fields. 'parameters' => $json , 'started' => $started , 'comments' => $comments , 'executiontime' => $executionTime , ]); $task ->save(); return $task ; } /*--------------------------------------------------------------------- * * Delete. * *---------------------------------------------------------------------*/ /** * Deletes all tasks. * * Any task already executing will continue to execute until it finishes. * That execution may add new tasks, which will not be deleted. * * <B>Warning:</B> Deleting all tasks ends any pending operations, such as * those to delete, copy, or move content. This can leave these operations * in an indetermine state, with parts of the operation incomplete, locks * still locked, and entities marked hidden or disabled. This should only * be done as a last resort. * * @see ::deleteTask() * @see ::deleteTasks() * @see ::findNumberOfTasks() */ public static function deleteAllTasks() { // Truncate the task table to delete everything. // // Since this entity type does not support a persistent cache, a static // cache, or a render cache, we do not have to worry about caches getting // out of sync with the database. $connection = Database::getConnection(); $truncate = $connection ->truncate(self::BASE_TABLE); $truncate ->execute(); } /** * Deletes a task. * * The task is deleted. If it is already executing, it will continue to * execute until it finishes. That execution may add new tasks, which * will not be deleted. * * <B>Warning:</B> Deleting a task ends any pending operation, such as one * to delete, copy, or move content. This can leave an operation in an * indetermine state, with parts of the operation incomplete, locks * still locked, and entities marked hidden or disabled. This should only * be done as a last resort. * * @param \Drupal\foldershare\Entity\FolderShareScheduledTask $task * The task to delete. * * @see ::deleteAllTasks() * @see ::deleteTasks() * @see ::findNumberOfTasks() */ public static function deleteTask(FolderShareScheduledTask $task ) { if ( $task === NULL) { return ; } // It is possible that the task object has been loaded by more than one // process, then deleted by more than one process. The first delete // actually removes it from the entity table. The second delete does // nothing. try { $task -> delete (); } catch (\Exception $e ) { // Do nothing. } } /** * Deletes tasks for a specific callback. * * All tasks with the indicated callback are deleted. If a task is * already executing, it will continue to execute until it finishes. That * execution may add new tasks, which will not be deleted. * * <B>Warning:</B> Deleting a task ends any pending operation, such as one * to delete, copy, or move content. This can leave an operation in an * indetermine state, with parts of the operation incomplete, locks * still locked, and entities marked hidden or disabled. This should only * be done as a last resort. * * @param mixed $callback * (optional, default = '') The callback for the task following standard * PHP callable formats (e.g. "class::method" or [object, "method"], etc.). * If the callback is empty, all tasks are deleted. * * @see ::deleteAllTasks() * @see ::deleteTask() * @see ::findNumberOfTasks() */ public static function deleteTasks( $callback = '' ) { if ( empty ( $callback ) === TRUE) { self::deleteAllTasks(); } // Throws an exception if the callback does not validate. $callback = self::verifyCallback( $callback ); // Delete all entries with the indicated callback. $connection = Database::getConnection(); $query = $connection -> delete (self::BASE_TABLE); $query ->condition( 'operation' , $callback , '=' ); $query ->execute(); } /*--------------------------------------------------------------------- * * Fields access. * *---------------------------------------------------------------------*/ /** * Returns the task's approximate accumulated execution time in seconds. * * A task may keep track of its accumulated execution time through a * chain of tasks, starting with the initial run of the task, followed * by a series of continuation runs. * * The execution time is approximate. If an operation schedules a * safety net task, runs for awhile, and is interrupted before it can * swap the safety net task with a continuation task, then the accumulated * execution time of the interrupted task will not have had a chance to * be saved into a continuation task. * * @return int * Returns the approximate accumulated execution time in seconds. */ public function getAccumulatedExecutionTime() { return $this ->get( 'executiontime' )->value; } /** * Returns the task's callback in the form "class::method". * * @return string * Returns the name of the task callback. */ public function getCallback() { return $this ->get( 'operation' )->value; } /** * Returns the task's optional comments. * * Comments are optional and may be used by a task to annotate why the * task exists or how it is progressing. * * @return string * Returns the comments for the task. */ public function getComments() { return $this ->get( 'comments' )->value; } /** * Returns the task's creation timestamp. * * @return int * Returns the creation timestamp for this task. */ public function getCreatedTime() { return $this ->get( 'created' )->value; } /** * Returns the task's parameters. * * @return array * Returns an array of task parameters. */ public function getParameters() { return $this ->get( 'parameters' )->value; } /** * Returns the user ID of the user that initiated the task. * * @return int * Returns the requester's user ID. */ public function getRequester() { return (int) $this ->get( 'uid' )->target_id; } /** * Returns the task's scheduled run timestamp. * * @return int * Returns the scheduled run timestamp for this task. */ public function getScheduledTime() { return $this ->get( 'scheduled' )->value; } /** * Returns the task's operation start timestamp. * * The start time is the time when an operation began, such as * the request time for a copy, move, or delete. This operation led to * the creation of the task object, which has created and scheduled times. * If that task reschedules itself into a continuing series of tasks, * all of them should share the same operation started timestamp. * * @return int * Returns the original start timestamp for this task. */ public function getStartedTime() { return $this ->get( 'started' )->value; } /*--------------------------------------------------------------------- * * Find tasks. * *---------------------------------------------------------------------*/ /** * Returns the number of scheduled tasks ready to be executed. * * Ready tasks are those with a task timestamp that is equal to * the current time or in the past. * * @param int $timestamp * The timestamp used to select which tasks are ready. * * @see ::findNumberOfTasks() * @see ::findReadyTaskIds() * @see ::executeTasks() */ public static function findNumberOfReadyTasks(int $timestamp ) { $connection = Database::getConnection(); $select = $connection ->select(self::BASE_TABLE, "st" ); $select ->condition( 'scheduled' , $timestamp , '<=' ); return (int) $select ->countQuery()->execute()->fetchField(); } /** * Returns the number of scheduled tasks. * * If a callback name is provided, the returned number only counts * tasks with that name. If no name is given, all tasks are counted. * * @param string $callback * (optional, default = '' = any) When set, returns the number of * scheduled tasks with the given callback in the form "class::method". * Otherwise returns the number of all scheduled tasks. * * @return int * Returns the number of tasks. * * @see ::findNumberOfReadyTasks() * @see ::findReadyTaskIds() */ public static function findNumberOfTasks(string $callback = '' ) { $connection = Database::getConnection(); $select = $connection ->select(self::BASE_TABLE, "st" ); if ( empty ( $callback ) === FALSE) { $select ->condition( 'operation' , $callback , '=' ); } return (int) $select ->countQuery()->execute()->fetchField(); } /** * Returns an ordered array of scheduled and ready task IDs. * * Ready tasks are those with a task timestamp that is equal to * the current time or in the past. The returned array is ordered * from oldest to newest. * * @param int $timestamp * The timestamp used to select which tasks are ready. * * @return int[] * Returns an array of task IDs for ready tasks, ordered from * oldest to newest. * * @see ::findNumberOfReadyTasks() * @see ::findNumberOfTasks() * @see ::executeTasks() */ public static function findReadyTaskIds(int $timestamp ) { $connection = Database::getConnection(); $select = $connection ->select(self::BASE_TABLE, "st" ); $select ->addField( 'st' , 'id' , 'id' ); $select ->condition( 'scheduled' , $timestamp , '<=' ); $select ->orderBy( 'scheduled' ); return $select ->execute()->fetchCol(0); } /** * Returns an ordered array of all scheduled task IDs. * * If a callback name is provided, the returned list only includes * tasks with that callback. If no callback is given, all tasks are included. * * The returned array is ordered from oldest to newest. * * @param string $callback * (optional, default = '' = any) When set, returns the scheduled tasks * with the given callback in the form "class::method". Otherwise returns * all scheduled tasks. * * @return int[] * Returns an array of task IDs, ordered from oldest to newest. * * @see ::findReadyTaskIds() * @see ::findNumberOfTasks() */ public static function findTaskIds(string $callback = '' ) { $connection = Database::getConnection(); $select = $connection ->select(self::BASE_TABLE, "st" ); $select ->addField( 'st' , 'id' , 'id' ); if ( empty ( $callback ) === FALSE) { $select ->condition( 'operation' , $callback , '=' ); } $select ->orderBy( 'scheduled' ); return $select ->execute()->fetchCol(0); } /*--------------------------------------------------------------------- * * Execute tasks. * *---------------------------------------------------------------------*/ /** * Returns TRUE if task execution is enabled, and FALSE otherwise. * * At the start of every process, this is TRUE and ready tasks will execute * each time executeTasks() is called. * * Task execution can be disabled FOR THE CURRENT PROCESS ONLY by calling * setTaskExecutionEnabled() with a FALSE argument. * * @return bool * Returns TRUE if enabled. * * @see ::setTaskExecutionEnabled() * @see ::executeTasks() */ public static function isTaskExecutionEnabled() { return self:: $enabled ; } /** * Enables or disables task execution. * * At the start of every process, this is TRUE and ready tasks will execute * each time executeTasks() is called. * * When set to FALSE, executeTasks() will return immediately, doing nothing. * This may be used to temporarily disable task execution FOR THE CURRENT * PROCESS ONLY. This has no effect on other processes or future processes. * * A common use of execution disabling is by drush, which needs to execute * commands without necessarily running pending tasks. * * @param bool $enable * TRUE to enable, FALSE to disable. * * @see ::isTaskExecutionEnabled() * @see ::executeTasks() */ public static function setTaskExecutionEnabled(bool $enable ) { self:: $enabled = $enable ; } /** * Executes ready tasks. * * All tasks with scheduled times equal to or earlier than the given * time stamp will be considered ready to run and executed in oldest to * newest order. Tasks are deleted just before they are executed. Tasks * may add more tasks. * * Task execution will abort if execution has been disabled FOR THE * CURRENT PROCESS ONLY using setTaskExecutionEnabled(). * * @param int $timestamp * The time used to find all ready tasks. Any task scheduled to run at * this time, or earlier, is considered ready and will be executed. * This is typically set to the current time, or the time at which an * HTTP request was made. * * @see ::findNumberOfReadyTasks() * @see ::findReadyTaskIds() * @see ::isTaskExecutionEnabled() * @see ::setTaskExecutionEnabled() */ public static function executeTasks(int $timestamp ) { // If execution is disabled, return immediately. if (self:: $enabled === FALSE) { return ; } // If execution time is already above the soft execution limit, // return immediately. if (LimitUtilities::aboveExecutionTimeLimit() === TRUE) { return ; } // // Quick reject. // ------------- // This function is called after every page is sent to a user. It is // essential that it quickly decide if there is anything to do. try { if (self::findNumberOfReadyTasks( $timestamp ) === 0) { // Nothing to do. return ; } } catch (\Exception $e ) { // Query failed? return ; } // // Get ready tasks. // ---------------- // A task is ready if the current time is greater than or equal to // its scheduled time. try { $taskIds = self::findReadyTaskIds( $timestamp ); } catch (\Exception $e ) { // Query failed? return ; } // // Execute tasks. // -------------- // Loop through the tasks and execute them. // // If a task fails to load, it has already been deleted. It may have // been serviced by another execution of this same method running in // another process after delivering a page for another user. foreach ( $taskIds as $taskId ) { $task = self::load( $taskId ); if ( $task === NULL) { continue ; } // Delete the task to reduce the chance that another process will // try to service the same task at the same time. This can still // happen and tasks must be written to consider this. $task -> delete (); // Copy out the task's values. $callback = $task ->getCallback(); $json = $task ->getParameters(); $requester = $task ->getRequester(); $comments = $task ->getComments(); $started = $task ->getStartedTime(); $executionTime = $task ->getAccumulatedExecutionTime(); unset( $task ); // Decode JSON-encoded task parameters. if ( empty ( $json ) === TRUE) { $parameters = []; } else { $parameters = json_decode( $json , TRUE, 512, JSON_OBJECT_AS_ARRAY); if ( $parameters === NULL) { // The parameters could not be decoded! This should not happen // since they were encoded using json_encode() during task creation. // There is nothing we can do with the task. ManageLog::error( "Programmer error: Missing or malformed parameters for '@callback' task." , [ '@callback' => $callback , ]); continue ; } } // Dispatch to the callback. The class and method were previously // validated when the task was created, so this call should not fail // with a PHP error. It still may fail due to a callback error. try { @call_user_func( $callback , $requester , $parameters , $started , $comments , $executionTime ); } catch (\Exception $e ) { // Unexpected exception. ManageLog::exception( $e ); } // If execution time is already above the soft execution limit, // stop processing tasks. if (LimitUtilities::aboveExecutionTimeLimit() === TRUE) { break ; } } } } |