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()
* @see https://www.php.net/manual/en/function.is-callable.php
*/
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;
}
}
}
}
