start(); // Register shutdown function to stop the server when the process exits register_shutdown_function(static function() { self::$server?->stop(); }); } catch(Exception $e) { throw new ServerException('Failed to initialize the server.', 0, $e); } } if($mode === TamerMode::WORKER) { try { self::$worker_configuration = WorkerConfiguration::fromEnvironment(); self::$server_configuration = new ServerConfiguration(self::$worker_configuration->getHost(), self::$worker_configuration->getPort(), self::$worker_configuration->getPassword()); } catch(Exception $e) { throw new RuntimeException('Failed to initialize worker configuration. (is the process running as a worker?)', 0, $e); } } if($mode === TamerMode::CLIENT) { self::$supervisor = new WorkerSupervisor(self::$server_configuration); self::$return_channel = 'rch' . random_int(100000000, 999999999); } self::$job_manager = new JobManager(self::$server_configuration); } /** * Shuts down all workers * * @return void */ public static function shutdown(): void { if(self::$mode === null) { return; } if(self::$mode === TamerMode::CLIENT && self::$supervisor !== null) { self::$supervisor->stopAll(); } } /** * CLIENT FUNCTIONS */ /** * Spawns a worker process by their count, if the path is null then a generic sub process will be spawned * that will only be capable of executing closures. * * @param int $count * @param string|null $path * @param int $channel * @return void */ public static function createWorker(int $count=8, ?string $path=null, int $channel=0): void { if(self::$mode !== TamerMode::CLIENT) { throw new RuntimeException(sprintf('Attempting to spawn a worker in \'%s\' mode, only clients can spawn workers.', self::$mode)); } if($path === null) { self::$supervisor->spawnClosure($count, $channel); } else { self::$supervisor->spawnWorker($path, $count, $channel); } } /** * Preforms a job in the background, returns the Job ID to keep track of the job status. * * @param callable $function * @param array $arguments * @param int $channel * @return string */ public static function do(callable $function, array $arguments, int $channel=0): string { if(self::$mode !== TamerMode::CLIENT) { throw new RuntimeException(sprintf('Attempting to do() in \'%s\' mode, only clients can preform do().', self::$mode)); } $job_packet = new JobPacket(); $job_packet->setJobType(JobType::CLOSURE); $job_packet->setParameters(serialize($arguments)); $job_packet->setPayload(serialize(new SerializableClosure($function))); $job_packet->setChannel($channel); $job_packet->setReturnChannel(self::$return_channel); try { self::$job_manager->pushJob($job_packet); } catcH(Exception $e) { throw new RuntimeException('do() failed, failed to push job to the server', 0, $e); } self::addToWatchlist($job_packet->getId()); return $job_packet->getId(); } /** * Preforms a function call against a worker in the background, returns the Job ID to keep track of the job status. * * @param string $function * @param array $arguments * @param int $channel * @return mixed */ public static function call(string $function, array $arguments, int $channel=0): mixed { if(self::$mode !== TamerMode::CLIENT) { throw new RuntimeException(sprintf('Attempting to call() in \'%s\' mode, only clients can preform call().', self::$mode)); } $job_packet = new JobPacket(); $job_packet->setJobType(JobType::FUNCTION); $job_packet->setParameters(serialize($arguments)); $job_packet->setPayload($function); $job_packet->setChannel($channel); $job_packet->setReturnChannel(self::$return_channel); try { self::$job_manager->pushJob($job_packet); } catcH(Exception $e) { throw new RuntimeException('call() failed, failed to push job to the server', 0, $e); } self::addToWatchlist($job_packet->getId()); return $job_packet->getId(); } /** * Does a job in the background, but once the job is completed it will be forgotten and the result will not be * returned, this also means that the job will not be added to the watchlist. * * @param callable $function * @param array $arguments * @param int $channel * @return void */ public function dof(callable $function, array $arguments, int $channel=0): void { if(self::$mode !== TamerMode::CLIENT) { throw new RuntimeException(sprintf('Attempting to dof() in \'%s\' mode, only clients can preform dof().', self::$mode)); } $job_packet = new JobPacket(); $job_packet->setJobType(JobType::CLOSURE); $job_packet->setParameters(serialize($arguments)); $job_packet->setPayload(serialize(new SerializableClosure($function))); $job_packet->setChannel($channel); try { self::$job_manager->pushJob($job_packet); } catcH(Exception $e) { throw new RuntimeException('do() failed, failed to push job to the server', 0, $e); } self::addToWatchlist($job_packet->getId()); } /** * Sends a function call to a worker in the background, but once the job is completed it will be forgotten and * the result will not be returned, this also means that the job will not be added to the watchlist. * * @param string $function * @param array $arguments * @param int $channel * @return void */ public static function callf(string $function, array $arguments, int $channel=0): void { if(self::$mode !== TamerMode::CLIENT) { throw new RuntimeException(sprintf('Attempting to callf() in \'%s\' mode, only clients can preform callf().', self::$mode)); } $job_packet = new JobPacket(); $job_packet->setJobType(JobType::FUNCTION); $job_packet->setParameters(serialize($arguments)); $job_packet->setForget(true); $job_packet->setPayload($function); $job_packet->setChannel($channel); try { self::$job_manager->pushJob($job_packet); } catcH(Exception $e) { throw new RuntimeException('callf() failed, failed to push job to the server', 0, $e); } self::addToWatchlist($job_packet->getId()); } /** * Waits for all the dispatched jobs to complete, this is a blocking function and will not return until all the * jobs have completed. If a timeout is specified, the function will return after the timeout has been reached. * * @param callable $callback * @param int $timeout * @return void * @throws ServerException * @throws Throwable * @throws TimeoutException */ public static function wait(callable $callback, int $timeout=0): void { if(self::$mode !== TamerMode::CLIENT) { throw new RuntimeException(sprintf('Attempting to wait() in \'%s\' mode, only clients can preform wait().', self::$mode)); } $time_start = time(); while(true) { if(count(self::$watching_jobs) === 0) { Log::debug('net.nosial.tamerlib', 'No jobs to wait for, returning'); return; } Log::debug('net.nosial.tamerlib', 'Waiting for jobs to complete'); $job_packet = self::$job_manager->listenReturnChannel(self::$return_channel); if(in_array($job_packet->getId(), self::$watching_jobs)) { Log::debug('net.nosial.tamerlib', sprintf('Job \'%s\' has returned, removing from watchlist', $job_packet->getId())); self::removeFromWatchlist($job_packet->getId()); self::$job_manager->dropJob($job_packet->getId()); if($job_packet->getStatus() === JobStatus::FINISHED) { $return_value = $job_packet->getReturnValue(); if($return_value === null) { $return_value = null; } else { $return_value = unserialize($return_value, ['allowed_classes' => true]); if($return_value === false) { Log::error('net.nosial.tamerlib', 'Failed to unserialize return value, return value was dropped'); $return_value = null; } } $callback($job_packet->getId(), $return_value); } elseif($job_packet->getStatus() === JobStatus::FAILED) { try { $e = unserialize($job_packet->getException(), ['allowed_classes' => true]); } catch(Exception $e) { Log::error('net.nosial.tamerlib', 'Failed to unserialize exception, exception was dropped', $e); } finally { if(isset($e) && $e instanceof Throwable) { throw $e; } throw new ServerException('wait() failed, job returned with an exception'); } } else { Log::debug('net.nosial.tamerlib', sprintf('Job \'%s\' returned with an unexpected status of \'%s\'', $job_packet->getId(), $job_packet->getStatus())); throw new ServerException('wait() failed, job returned with an unexpected status of \'' . $job_packet->getStatus() . '\''); } } else { Log::debug('net.nosial.tamerlib', sprintf('Job \'%s\' has returned, but is not in the watchlist', $job_packet->getId())); } if ($timeout < 0) { throw new TimeoutException('wait() timed out'); } if($timeout > 0 && (time() - $time_start) >= $timeout) { throw new TimeoutException('wait() timed out'); } usleep(10); } } /** * Waits for a job to complete, returns the result of the job. * * @param JobPacket|int $job_id * @param int $timeout * @return mixed * @throws JobNotFoundException * @throws ServerException * @throws TimeoutException * @throws Throwable */ public static function waitFor(JobPacket|int $job_id, int $timeout=0): mixed { if(self::$mode !== TamerMode::CLIENT) { throw new RuntimeException(sprintf('Attempting to waitFor() in \'%s\' mode, only clients can preform waitFor().', self::$mode)); } if($job_id instanceof JobPacket) { $job_id = $job_id->getId(); } $time_start = time(); while(true) { self::$supervisor->monitor(-1); switch(self::$job_manager->getJobStatus($job_id)) { case JobStatus::FINISHED: $return = self::$job_manager->getJobResult($job_id); self::$job_manager->dropJob($job_id); return $return; case JobStatus::FAILED: $throwable = self::$job_manager->getJobException($job_id); self::$job_manager->dropJob($job_id); throw $throwable; } if($timeout < 0) { throw new TimeoutException('waitFor() timed out'); } if($timeout > 0 && (time() - $time_start) >= $timeout) { throw new TimeoutException(sprintf('waitFor() timed out after %d seconds', $timeout)); } usleep(10); } } /** * Clears the watchlist, this will remove all jobs from the watchlist. * * @return void */ public static function clear(): void { if(self::$mode !== TamerMode::CLIENT) { throw new RuntimeException(sprintf('Attempting to clear() in \'%s\' mode, only clients can preform clear().', self::$mode)); } self::$watching_jobs = []; } /** * Invokes the call() function, returns the Job ID. * * @param string $name * @param array $arguments * @return mixed */ public static function __callStatic(string $name, array $arguments) { return self::call($name, $arguments); } /** * WORKER FUNCTIONS */ /** * @param string $function * @param callable $callback * @return void */ public static function addFunction(string $function, callable $callback): void { if(self::$mode !== TamerMode::WORKER) { throw new RuntimeException(sprintf('Attempting to addFunction() in \'%s\' mode, only workers can preform addFunction().', self::$mode)); } self::$function_pointers[$function] = $callback; } /** * @param string $function * @return void */ public static function removeFunction(string $function): void { if(self::$mode !== TamerMode::WORKER) { throw new RuntimeException(sprintf('Attempting to removeFunction() in \'%s\' mode, only workers can preform removeFunction().', self::$mode)); } unset(self::$function_pointers[$function]); } /** * @return array */ public static function getFunctions(): array { if(self::$mode !== TamerMode::WORKER) { throw new RuntimeException(sprintf('Attempting to getFunctions() in \'%s\' mode, only workers can preform getFunctions().', self::$mode)); } return array_keys(self::$function_pointers); } /** * @param int|array $channel * @param int $timeout * @return void * @throws JobNotFoundException * @throws ServerException */ public static function run(int|array $channel=0, int $timeout=0): void { if(self::$mode !== TamerMode::WORKER) { throw new RuntimeException(sprintf('Attempting to run() in \'%s\' mode, only workers can preform run().', self::$mode)); } try { $job_packet = self::$job_manager->listenForJob(self::$worker_configuration->getWorkerId(), $channel, $timeout); } catch(TimeoutException $e) { unset($e); return; } Log::debug('net.nosial.tamerlib', sprintf('Worker %s received job %s', self::$worker_configuration->getWorkerId(), $job_packet->getId())); switch($job_packet->getJobType()) { case JobType::FUNCTION: if(!isset(self::$function_pointers[$job_packet->getPayload()])) { Log::warning('net.nosial.tamerlib', sprintf('Job %s requested function \'%s\' which does not exist, rejecting job.', $job_packet->getId(), $job_packet->getPayload())); self::$job_manager->rejectJob($job_packet); } try { $result = call_user_func_array(self::$function_pointers[$job_packet->getPayload()], unserialize($job_packet->getParameters(), ['allowed_classes'=>true])); self::$job_manager->returnJob($job_packet, $result); } catch(Exception $e) { var_dump($e); self::$job_manager->returnException($job_packet, $e); } break; case JobType::CLOSURE: try { $result = unserialize($job_packet->getPayload(), ['allowed_classes'=>true])( unserialize($job_packet->getParameters(), ['allowed_classes'=>true]) ); self::$job_manager->returnJob($job_packet, $result); } catch(Exception $e) { self::$job_manager->returnException($job_packet, $e); } break; } } }