* @version 0.0.0 * @copyright Copyright (c) 2007, 2008 Roland Haeder, 2009 - 2023 Core Developer Team * @license GNU GPL 3.0 or any newer version * @link http://www.shipsimu.org * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ class TaskHandler extends BaseHandler implements Registerable, HandleableTask { // Load traits use IteratorTrait; use ListableTrait; use VisitorTrait; // Exception constants const EXCEPTION_TASK_IS_INVALID = 0xb00; /** * Protected constructor * * @return void */ private function __construct () { // Call parent constructor //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('TASK-HANDLER: CONSTRUCTWD!'); parent::__construct(__CLASS__); // Set handler name //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage('TASK-HANDLER: Setting handlerName=task ...'); $this->setHandlerName('task'); // Trace message //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('TASK-HANDLER: EXIT!'); } /** * Creates an instance of this class * * @return $handlerInstance An instance of a HandleableTask class */ public static final function createTaskHandler () { // Get new instance //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('TASK-HANDLER: CALLED!'); $handlerInstance = new TaskHandler(); // Output debug message self::createDebugInstance(__CLASS__, __LINE__)->debugMessage('TASK-HANDLER: Initializing task handler.'); // Init the task list $handlerInstance->setListInstance(ObjectFactory::createObjectByConfiguredName('task_list_class')); // Get default instance $handlerInstance->setIteratorInstance($handlerInstance->getListInstance()->getIterator()); // Init visitor instance for faster loop $handlerInstance->setVisitorInstance(ObjectFactory::createObjectByConfiguredName('active_task_visitor_class')); // Register the first (and generic) idle-loop task $taskInstance = ObjectFactory::createObjectByConfiguredName('idle_task_class'); $handlerInstance->registerTask('idle_loop', $taskInstance); // Output debug message self::createDebugInstance(__CLASS__, __LINE__)->debugMessage('TASK-HANDLER: Task handler initialized.'); // Return the prepared instance //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('TASK-HANDLER:handlerInstance=%s - EXIT!', $handlerInstance->__toString())); return $handlerInstance; } /** * Tries to execute the given task. If as task should not be started (yet) * or the interval time (see task_interval_delay) is not yet reached the * task is quietly skipped. * * @return void * @throws InvalidTaskException If the current task is invalid */ private function executeCurrentTask () { // Update no task by default //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('TASK-HANDLER: CALLED!'); $updateTask = false; // Is the current task valid? if (!$this->getListInstance()->getIterator()->valid()) { // Not valid! throw new InvalidTaskException($this, self::EXCEPTION_TASK_IS_INVALID); } // Get current task $currentTask = $this->getListInstance()->getIterator()->current(); // Is the task not yet started? //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('TASK-HANDLER: currentTask()=%d', count($currentTask))); if ($currentTask['task_started'] === false) { // Determine difference between current time and registration //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('TASK-HANDLER: currentTask[id]=%s has not started yet, checking ...', $currentTask['id'])); $diff = ($this->getMilliTime() - $currentTask['task_registered']) * 1000; // Should we start now? //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('TASK-HANDLER: diff=%d,currentTask[task_startup_delay]=%d', $diff, $currentTask['task_startup_delay'])); if ($diff < $currentTask['task_startup_delay']) { // Skip this silently //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('TASK-HANDLER: Task %s not started: diff=%d,task_startup_delay=%d', $currentTask['id'], $diff, $currentTask['task_startup_delay'])); return; } // Launch the task and mark it as updated $currentTask['task_started'] = true; $updateTask = true; // Debug message self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('TASK-HANDLER: Task %s started with startup_delay=%dms', $currentTask['id'], $currentTask['task_startup_delay'])); } // Get time difference from interval delay $diff = ($this->getMilliTime() - $currentTask['task_last_activity']) * 1000; // Is the interval delay reached? //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage('TASK-HANDLER: Task ' . $currentTask['id'] . ' diff=' . $diff . ',task_interval_delay=' . $currentTask['task_interval_delay'] . ',task_max_runs=' . $currentTask['task_max_runs'] . ',task_total_runs=' . $currentTask['task_total_runs']); if ((($diff < $currentTask['task_interval_delay']) && ($currentTask['task_max_runs'] == 0)) || (($currentTask['task_max_runs'] > 0) && ($currentTask['task_total_runs'] == $currentTask['task_max_runs']))) { // Should we update the task from startup? //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('TASK-HANDLER: currentTask[id]=%s has reached interval ... updateTask=%d', $currentTask['id'], intval($updateTask))); if ($updateTask === true) { // Update the task before leaving //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('TASK-HANDLER: Updating task %s ...', $currentTask['id'])); $this->updateTask($currentTask); } // Skip this silently //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('TASK-HANDLER: Task %s has been updated - EXIT!', $currentTask['id'])); return; } // Set last activity $currentTask['task_last_activity'] = $this->getMilliTime(); // Count this run $currentTask['task_total_runs']++; // Update the task //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('TASK-HANDLER: Updating task %s ...', $currentTask['id'])); $this->updateTask($currentTask); // And visit/run it // @TODO Messurement can be added around this call //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('TASK-HANDLER: Invoking currentTask[task_instance]->accept(%s), task_instance=%s ...', $this->getVisitorInstance()->__toString(), $currentTask['task_instance']->__toString())); $currentTask['task_instance']->accept($this->getVisitorInstance()); // Trace message //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('TASK-HANDLER: EXIT!'); } /** * Updates given task by updating the underlaying list * * @param $taskEntry An array with a task * @return void */ private function updateTask (array $taskEntry) { // Get the key from current iteration //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('TASK-HANDLER: taskEntry()=%d - CALLED!', count($taskEntry))); $key = $this->getListInstance()->getIterator()->key(); // Get the hash from key //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('TASK-HANDLER: key=%s', $key)); $hash = $this->getListInstance()->getHashByIndex($key); // Update the entry //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('TASK-HANDLER: Invoking this->listInstance->updateCurrentEntryByHash(%s,taskEntry()=%d) ...', $hash, count($taskEntry))); $this->getListInstance()->updateCurrentEntryByHash($hash, $taskEntry); // Trace message //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('TASK-HANDLER: EXIT!'); } /** * Unregisters the given task * * @param $taskData Data of the task * @return void */ private function unregisterTask (array $taskData) { // Remove the entry //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('TASK-HANDLER: Removing task %s from queue - CALLED!', $taskData['id'])); $this->getListInstance()->removeEntry('tasks', $taskData); // Trace message //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('TASK-HANDLER: Removing task %s from queue - EXIT!', $taskData['id'])); } /** * Searches a task by given instance * * @param $taskInstanc An instanceof a Taskable class * @return $taskName Name of the task as used while registration */ public function searchTask (Taskable $taskInstance) { // Default is an empty (not found) task name //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('TASK-HANDLER: taskInstance=%s - CALLED!', $taskInstance->__toString())); $taskName = ''; // Get whole list $taskList = $this->getListInstance()->getArrayFromList('tasks'); // Search all instances //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('TASK-HANDLER: taskList()=%d', count($taskList))); foreach ($taskList as $currentTask) { // Does it match given task instance? //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('TASK-HANDLER: currentTask[id=%s', $currentTask['id'])); if ($currentTask['task_instance']->equals($taskInstance)) { // Found it $taskName = $currentTask['id']; // Abort here //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('TASK-HANDLER: taskName=%s - BREAK!', $taskName)); break; } } // Return found name //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('TASK-HANDLER: taskName=%s - EXIT!', $taskName)); return $taskName; } /** * Registers a task with a task handler. * * @param $taskName A task name to register the task on * @param $taskInstance An instance of a Taskable class * @return void * @throws InvalidArgumentException If a parameter is not valid * @throws UnexpectedValueException If an unexpected value has been configured */ public function registerTask (string $taskName, Taskable $taskInstance) { // Is the parameter valid //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('TASK-HANDLER: taskName=%s,taskInstance - CALLED!', $taskName, $taskInstance->__toString())); if (empty($taskName)) { // Task name cannot be empty throw new InvalidArgumentException('Parameter "taskName" is empty', FrameworkInterface::EXCEPTION_INVALID_ARGUMENT); } // Get interval delay, startup delay and max runs $intervalDelay = FrameworkBootstrap::getConfigurationInstance()->getConfigEntry('task_' . $taskName . '_interval_delay'); $startupDelay = FrameworkBootstrap::getConfigurationInstance()->getConfigEntry('task_' . $taskName . '_startup_delay'); $maxRuns = FrameworkBootstrap::getConfigurationInstance()->getConfigEntry('task_' . $taskName . '_max_runs'); // If the task is 'idle_loop', a deplay of zero seconds is fine //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('TASK-HANDLER: intervalDelay=%d,startupDelay=%d,maxRuns=%d', $intervalDelay, $startupDelay, $maxRuns)); if ($intervalDelay < 0) { // Invalid configuration value throw new UnexpectedValueException(sprintf('taskName=%s has intervalDelay=%d below zero', $taskName, $intervalDelay), FrameworkInterface::EXCEPTION_UNEXPECTED_VALUE); } elseif ($startupDelay < 0) { // Invalid configuration value throw new UnexpectedValueException(sprintf('taskName=%s has startupDelay=%d below zero', $taskName, $startupDelay), FrameworkInterface::EXCEPTION_UNEXPECTED_VALUE); } elseif ($maxRuns < 0) { // Invalid configuration value throw new UnexpectedValueException(sprintf('taskName=%s has maxRuns=%d below zero', $taskName, $maxRuns), FrameworkInterface::EXCEPTION_UNEXPECTED_VALUE); } elseif ($taskName != 'idle_loop' && $intervalDelay == 0) { // Only idle_loop can have a zero interval delay throw new UnexpectedValueException(sprintf('taskName=%s has zero interval delay which is only valid for "idle_loop" task', $taskName), FrameworkInterface::EXCEPTION_UNEXPECTED_VALUE); } elseif ($taskName != 'idle_loop' && $startupDelay == 0) { // Only idle_loop can have a zero interval delay throw new UnexpectedValueException(sprintf('taskName=%s has zero startup delay which is only valid for "idle_loop" task', $taskName), FrameworkInterface::EXCEPTION_UNEXPECTED_VALUE); } // Create the entry $taskEntry = [ // Identifier for the generateHash() method 'id' => $taskName, // Whether the task is started 'task_started' => false, // Whether the task is paused (not yet implemented) 'task_paused' => false, // Whether the task can be paused (not yet implemented) 'task_pauseable' => true, // Timestamp of registration 'task_registered' => $this->getMilliTime(), // Last activity timestamp 'task_last_activity' => 0, // Total runs of this task 'task_total_runs' => 0, // Task instance itself 'task_instance' => $taskInstance, // Startup delay in milliseconds 'task_startup_delay' => $startupDelay, // Interval time (delay) in milliseconds before this task is executed again 'task_interval_delay' => $intervalDelay, // How often should this task run? 'task_max_runs' => $maxRuns, ]; // Add the entry //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('TASK-HANDLER: Invoking this->listInstance->addEntry(tasks,taskEntry()=%d) ...', count($taskEntry))); $this->getListInstance()->addEntry('tasks', $taskEntry); // Debug message self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('TASK-HANDLER: Task registered: taskName=%s (taskInstance=%s), startupDelay=%dms, intervalDelay=%dms, maxRuns=%d ...', $taskName, $taskInstance->__toString(), $taskEntry['task_startup_delay'], $taskEntry['task_interval_delay'], $taskEntry['task_max_runs'], )); // Trace message //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('TASK-HANDLER: EXIT!'); } /** * Checks whether tasks are left including idle task * * @return $tasksLeft Whether there are tasks left to handle */ public function hasTasksLeft () { // Do we have tasks there? //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('TASK-HANDLER: CALLED!'); $tasksLeft = ($this->getListInstance()->count() > 0); // Return result //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('TASK-HANDLER: tasksLeft=%d - EXIT!', intval($tasksLeft))); return $tasksLeft; } /** * Handles all tasks by checking if they should startup or if it is their * turn to run. You should use this method in a while() loop in conjuntion * with hasTasksLeft() so you can e.g. shutdown by adding a ShutdownTask * which will attempt to remove all tasks from the task handler. * * @return void */ public function handleTasks () { // Should we rewind? //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('TASK-HANDLER: CALLED!'); if (!$this->getListInstance()->getIterator()->valid()) { // Rewind to the beginning for next loop //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('TASK-HANDLER: Invoking this->listInstance->iterator->rewind() ...'); $this->getListInstance()->getIterator()->rewind(); } // Try to execute the task //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('TASK-HANDLER: Invoking this->executeCurrentTask() ...'); $this->executeCurrentTask(); // Go to next entry //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('TASK-HANDLER: Invoking this->listInstance->iterator->next() ...'); $this->getListInstance()->getIterator()->next(); // Trace message //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('TASK-HANDLER: EXIT!'); } /** * Shuts down all tasks and the task handler itself. This method should be * called from a corresponding filter class. * * @return void */ public function doShutdown () { // Always rewind to the beginning for next loop //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('TASK-HANDLER: Invoking this->listInstance->iterator->rewind() - CALLED!'); $this->getListInstance()->getIterator()->rewind(); // Remember all tasks that has been shutdown for removal self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('TASK-HANDLER: Shutting down all %d tasks...', $this->getListInstance()->count())); $tasks = []; // Instance a visitor //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage('TASK-HANDLER: Setting visitorInstance=shutdown_task_visitor_class from configuration ...'); $this->setVisitorInstance(ObjectFactory::createObjectByConfiguredName('shutdown_task_visitor_class')); // Shutdown all tasks in once go while ($this->getListInstance()->getIterator()->valid()) { // Get current entry $currentTask = $this->getListInstance()->getIterator()->current(); // Shutdown the task //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage('TASK-HANDLER: Shutting down task ' . $currentTask['id'] . ' (taskInstance=' . $currentTask['task_instance']->__toString() . ') ...'); $currentTask['task_instance']->accept($this->getVisitorInstance()); // Remember this task array_push($tasks, $currentTask); // Advance to next one //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('TASK-HANDLER: Invoking this->listInstance->iterator->next() ...'); $this->getListInstance()->getIterator()->next(); } // Remove all tasks //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('TASK-HANDLER: Shutdown of all tasks completed. tasks()=%d', count($tasks))); foreach ($tasks as $taskEntry) { // Unregister this task //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('TASK-HANDLER: Invokint this->unregisterTask(taskEntry()=%d) ...', count($taskEntry))); $this->unregisterTask($taskEntry); } // Trace message //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('TASK-HANDLER: EXIT!'); } }