size = $size; $this->workerMemoryLimit = $workerMemoryLimit; $this->classLoader = $classLoader; $this->logger = $logger; } /** * Returns the maximum size of the pool. Note that there may be less active workers than this number. * * @return int */ public function getSize() : int{ return $this->size; } /** * Increases the maximum size of the pool to the specified amount. This does not immediately start new workers. * * @param int $newSize */ public function increaseSize(int $newSize) : void{ if($newSize > $this->size){ $this->size = $newSize; } } /** * Registers a Closure callback to be fired whenever a new worker is started by the pool. * The signature should be `function(int $worker) : void` * * This function will call the hook for every already-running worker. * * @param \Closure $hook */ public function addWorkerStartHook(\Closure $hook) : void{ $this->workerStartHooks[spl_object_hash($hook)] = $hook; foreach($this->workers as $i => $worker){ $hook($i); } } /** * Removes a previously-registered callback listening for workers being started. * * @param \Closure $hook */ public function removeWorkerStartHook(\Closure $hook) : void{ unset($this->workerStartHooks[spl_object_hash($hook)]); } /** * Returns an array of IDs of currently running workers. * * @return int[] */ public function getRunningWorkers() : array{ return array_keys($this->workers); } /** * Fetches the worker with the specified ID, starting it if it does not exist, and firing any registered worker * start hooks. * * @param int $worker * * @return AsyncWorker */ private function getWorker(int $worker) : AsyncWorker{ if(!isset($this->workers[$worker])){ $this->workerUsage[$worker] = 0; $this->workers[$worker] = new AsyncWorker($this->logger, $worker, $this->workerMemoryLimit); $this->workers[$worker]->setClassLoader($this->classLoader); $this->workers[$worker]->start(self::WORKER_START_OPTIONS); foreach($this->workerStartHooks as $hook){ $hook($worker); } } return $this->workers[$worker]; } /** * Submits an AsyncTask to an arbitrary worker. * * @param AsyncTask $task * @param int $worker */ public function submitTaskToWorker(AsyncTask $task, int $worker) : void{ if($worker < 0 or $worker >= $this->size){ throw new \InvalidArgumentException("Invalid worker $worker"); } if($task->getTaskId() !== null){ throw new \InvalidArgumentException("Cannot submit the same AsyncTask instance more than once"); } $task->progressUpdates = new \Threaded; $task->setTaskId($this->nextTaskId++); $this->tasks[$task->getTaskId()] = $task; $this->getWorker($worker)->stack($task); $this->workerUsage[$worker]++; $this->taskWorkers[$task->getTaskId()] = $worker; } /** * Selects a worker ID to run a task. * * - if an idle worker is found, it will be selected * - else, if the worker pool is not full, a new worker will be selected * - else, the worker with the smallest backlog is chosen. * * @return int */ public function selectWorker() : int{ $worker = null; $minUsage = PHP_INT_MAX; foreach($this->workerUsage as $i => $usage){ if($usage < $minUsage){ $worker = $i; $minUsage = $usage; if($usage === 0){ break; } } } if($worker === null or ($minUsage > 0 and count($this->workers) < $this->size)){ //select a worker to start on the fly for($i = 0; $i < $this->size; ++$i){ if(!isset($this->workers[$i])){ $worker = $i; break; } } } assert($worker !== null); return $worker; } /** * Submits an AsyncTask to the worker with the least load. If all workers are busy and the pool is not full, a new * worker may be started. * * @param AsyncTask $task * * @return int */ public function submitTask(AsyncTask $task) : int{ if($task->getTaskId() !== null){ throw new \InvalidArgumentException("Cannot submit the same AsyncTask instance more than once"); } $worker = $this->selectWorker(); $this->submitTaskToWorker($task, $worker); return $worker; } /** * Removes a completed or crashed task from the pool. * * @param AsyncTask $task * @param bool $force */ private function removeTask(AsyncTask $task, bool $force = false) : void{ if(isset($this->taskWorkers[$task->getTaskId()])){ if(!$force and ($task->isRunning() or !$task->isGarbage())){ return; } $this->workerUsage[$this->taskWorkers[$task->getTaskId()]]--; } unset($this->tasks[$task->getTaskId()]); unset($this->taskWorkers[$task->getTaskId()]); } /** * Collects garbage from running workers. */ private function collectWorkers() : void{ foreach($this->workers as $worker){ $worker->collect(); } } /** * Collects finished and/or crashed tasks from the workers, firing their on-completion hooks where appropriate. * * @throws \ReflectionException */ public function collectTasks() : void{ foreach($this->tasks as $task){ if(!$task->isGarbage()){ $task->checkProgressUpdates(); } if($task->isGarbage() and !$task->isRunning() and !$task->isCrashed()){ if(!$task->hasCancelledRun()){ try{ $task->onCompletion(); if($task->removeDanglingStoredObjects()){ $this->logger->notice("AsyncTask " . get_class($task) . " stored local complex data but did not remove them after completion"); } }catch(\Throwable $e){ $this->logger->critical("Could not execute completion of asynchronous task " . (new \ReflectionClass($task))->getShortName() . ": " . $e->getMessage()); $this->logger->logException($e); $task->removeDanglingStoredObjects(); //silent } } $this->removeTask($task); }elseif($task->isCrashed()){ $this->logger->critical("Could not execute asynchronous task " . (new \ReflectionClass($task))->getShortName() . ": Task crashed"); $this->removeTask($task, true); } } $this->collectWorkers(); } public function shutdownUnusedWorkers() : int{ $ret = 0; foreach($this->workerUsage as $i => $usage){ if($usage === 0){ $this->workers[$i]->quit(); unset($this->workers[$i], $this->workerUsage[$i]); $ret++; } } return $ret; } /** * Cancels all pending tasks and shuts down all the workers in the pool. */ public function shutdown() : void{ $this->collectTasks(); foreach($this->workers as $worker){ /** @var AsyncTask $task */ while(($task = $worker->unstack()) !== null){ //cancelRun() is not strictly necessary here, but it might be used to inform plugins of the task state //(i.e. it never executed). $task->cancelRun(); $this->removeTask($task, true); } } foreach($this->tasks as $task){ $task->cancelRun(); $this->removeTask($task, true); } $this->taskWorkers = []; $this->tasks = []; foreach($this->workers as $worker){ $worker->quit(); } $this->workers = []; $this->workerUsage = []; } }