├── LICENSE ├── README.md ├── composer.json └── src ├── Environment.php ├── Environment └── Mode.php ├── EnvironmentInterface.php ├── Exception └── RoadRunnerException.php ├── Informer ├── Worker.php └── Workers.php ├── Internal └── StdoutHandler.php ├── Logger.php ├── Message ├── Command │ ├── GetProcessId.php │ ├── Pong.php │ ├── StreamStop.php │ └── WorkerStop.php ├── ControlMessage.php └── SkipMessage.php ├── Payload.php ├── PayloadFactory.php ├── StreamWorkerInterface.php ├── Version.php ├── Worker.php ├── WorkerAwareInterface.php ├── WorkerInterface.php └── WorkerPool.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Spiral Scout 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

16 | 17 | RoadRunner is an open-source (MIT licensed) high-performance PHP application server, load balancer, and process manager. 18 | It supports running as a service with the ability to extend its functionality on a per-project basis. 19 | 20 | RoadRunner includes PSR-7/PSR-17 compatible HTTP and HTTP/2 server and can be used to replace classic Nginx+FPM setup with much greater performance and flexibility. 21 | 22 |

23 | Official Website | 24 | Documentation 25 |

26 | 27 | Repository: 28 | -------- 29 | 30 | This repository contains the common codebase for all binary roadrunner workers. 31 | Check [spiral/roadrunner](https://github.com/spiral/roadrunner) to access application 32 | server and [spiral/roadrunner-http](https://github.com/spiral/roadrunner-http) for PSR-7 compatible worker. 33 | 34 | You can use the convenient installer to download the latest available compatible version of RoadRunner assembly: 35 | 36 | ```bash 37 | $ composer require spiral/roadrunner-cli --dev 38 | ``` 39 | 40 | To download latest version of application server: 41 | 42 | ```bash 43 | $ vendor/bin/rr get 44 | ``` 45 | 46 | Example: 47 | ------- 48 | 49 | To init abstract RoadRunner worker: 50 | 51 | ```php 52 | waitPayload()) { 60 | // Received Payload 61 | var_dump($data); 62 | 63 | // Respond Answer 64 | $worker->respond(new \Spiral\RoadRunner\Payload('DONE')); 65 | } 66 | ``` 67 | 68 | 69 | try Spiral Framework 70 | 71 | 72 | Testing: 73 | -------- 74 | 75 | This codebase is automatically tested via host repository - [spiral/roadrunner](https://github.com/spiral/roadrunner). 76 | 77 | License: 78 | -------- 79 | 80 | The MIT License (MIT). Please see [`LICENSE`](./LICENSE) for more information. Maintained by [Spiral Scout](https://spiralscout.com). 81 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spiral/roadrunner-worker", 3 | "type": "library", 4 | "description": "RoadRunner: PHP worker", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Anton Titov (wolfy-j)", 9 | "email": "wolfy-j@spiralscout.com" 10 | }, 11 | { 12 | "name": "Valery Piashchynski", 13 | "homepage": "https://github.com/rustatian" 14 | }, 15 | { 16 | "name": "Aleksei Gagarin (roxblnfk)", 17 | "homepage": "https://github.com/roxblnfk" 18 | }, 19 | { 20 | "name": "Pavel Buchnev (butschster)", 21 | "email": "pavel.buchnev@spiralscout.com" 22 | }, 23 | { 24 | "name": "Maksim Smakouz (msmakouz)", 25 | "email": "maksim.smakouz@spiralscout.com" 26 | }, 27 | { 28 | "name": "RoadRunner Community", 29 | "homepage": "https://github.com/roadrunner-server/roadrunner/graphs/contributors" 30 | } 31 | ], 32 | "homepage": "https://spiral.dev/", 33 | "support": { 34 | "docs": "https://docs.roadrunner.dev", 35 | "issues": "https://github.com/roadrunner-server/roadrunner/issues", 36 | "chat": "https://discord.gg/V6EK4he" 37 | }, 38 | "require": { 39 | "php": ">=8.1", 40 | "ext-json": "*", 41 | "ext-sockets": "*", 42 | "psr/log": "^2.0 || ^3.0", 43 | "spiral/goridge": "^4.1.0", 44 | "spiral/roadrunner": "^2023.1 || ^2024.1 || ^2025.1", 45 | "composer-runtime-api": "^2.0" 46 | }, 47 | "require-dev": { 48 | "buggregator/trap": "^1.13", 49 | "jetbrains/phpstorm-attributes": "^1.0", 50 | "phpunit/phpunit": "^10.5.45", 51 | "spiral/code-style": "^2.2", 52 | "vimeo/psalm": "^6.0" 53 | }, 54 | "scripts": { 55 | "cs:diff": "php-cs-fixer fix --dry-run -v --diff --show-progress dots", 56 | "cs:fix": "php-cs-fixer fix -v", 57 | "test": "phpunit", 58 | "psalm": "psalm" 59 | }, 60 | "autoload": { 61 | "psr-4": { 62 | "Spiral\\RoadRunner\\": "src" 63 | } 64 | }, 65 | "autoload-dev": { 66 | "psr-4": { 67 | "Spiral\\RoadRunner\\Tests\\Worker\\": "tests" 68 | } 69 | }, 70 | "funding": [ 71 | { 72 | "type": "github", 73 | "url": "https://github.com/sponsors/roadrunner-server" 74 | } 75 | ], 76 | "suggest": { 77 | "spiral/roadrunner-cli": "Provides RoadRunner installation and management CLI tools" 78 | }, 79 | "config": { 80 | "sort-packages": true 81 | }, 82 | "minimum-stability": "dev", 83 | "prefer-stable": true 84 | } 85 | -------------------------------------------------------------------------------- /src/Environment.php: -------------------------------------------------------------------------------- 1 | 17 | * @see Mode 18 | */ 19 | class Environment implements EnvironmentInterface 20 | { 21 | /** 22 | * @param EnvironmentVariables $env 23 | */ 24 | public function __construct( 25 | private array $env = [], 26 | ) {} 27 | 28 | public static function fromGlobals(): self 29 | { 30 | /** @var array $env */ 31 | $env = [...$_ENV, ...$_SERVER]; 32 | 33 | return new self($env); 34 | } 35 | 36 | public function getMode(): string 37 | { 38 | return $this->get('RR_MODE'); 39 | } 40 | 41 | public function getRelayAddress(): string 42 | { 43 | return $this->get('RR_RELAY', 'pipes'); 44 | } 45 | 46 | public function getRPCAddress(): string 47 | { 48 | return $this->get('RR_RPC', 'tcp://127.0.0.1:6001'); 49 | } 50 | 51 | public function getVersion(): string 52 | { 53 | return $this->get('RR_VERSION'); 54 | } 55 | 56 | /** 57 | * @template TDefault of string 58 | * 59 | * @param non-empty-string $name 60 | * @param TDefault $default 61 | * @return string|TDefault 62 | */ 63 | private function get(string $name, string $default = ''): string 64 | { 65 | if (isset($this->env[$name]) || \array_key_exists($name, $this->env)) { 66 | /** @psalm-suppress RedundantCastGivenDocblockType */ 67 | return (string) $this->env[$name]; 68 | } 69 | 70 | return $default; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Environment/Mode.php: -------------------------------------------------------------------------------- 1 | $workers 11 | */ 12 | public function __construct( 13 | private readonly array $workers = [], 14 | ) {} 15 | 16 | /** 17 | * @return array 18 | */ 19 | public function getWorkers(): array 20 | { 21 | return $this->workers; 22 | } 23 | 24 | public function count(): int 25 | { 26 | return \count($this->workers); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Internal/StdoutHandler.php: -------------------------------------------------------------------------------- 1 | = 0, 'Invalid chunk size argument value'); 40 | 41 | self::restreamOutputBuffer($chunkSize); 42 | self::restreamHeaders(); 43 | 44 | // Vendor packages 45 | self::restreamSymfonyDumper(); 46 | } 47 | 48 | /** 49 | * Intercept all output headers writing. 50 | */ 51 | private static function restreamHeaders(): void 52 | { 53 | \header_register_callback(static function (): void { 54 | $headers = \headers_list(); 55 | 56 | if ($headers !== []) { 57 | \file_put_contents(self::PIPE_OUT, self::ERROR_WRITING_HEADER); 58 | } 59 | }); 60 | } 61 | 62 | /** 63 | * Intercept all output buffer write. 64 | * 65 | * @param int<0, max> $chunkSize 66 | */ 67 | private static function restreamOutputBuffer(int $chunkSize): void 68 | { 69 | \ob_start(static function (string $chunk, int $phase): void { 70 | $isWrite = ($phase & \PHP_OUTPUT_HANDLER_WRITE) === \PHP_OUTPUT_HANDLER_WRITE; 71 | 72 | if ($isWrite && $chunk !== '') { 73 | \file_put_contents(self::PIPE_OUT, $chunk); 74 | } 75 | }, $chunkSize); 76 | } 77 | 78 | private static function restreamSymfonyDumper(): void 79 | { 80 | if (\class_exists(AbstractDumper::class)) { 81 | AbstractDumper::$defaultOutput = self::PIPE_OUT; 82 | CliDumper::$defaultOutput = self::PIPE_OUT; 83 | HtmlDumper::$defaultOutput = self::PIPE_OUT; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Logger.php: -------------------------------------------------------------------------------- 1 | write($this->format((string) $level, $message, $context)); 23 | } 24 | 25 | protected function write(string $message): void 26 | { 27 | \file_put_contents('php://stderr', $message); 28 | } 29 | 30 | protected function format(string $level, string $message, array $context = []): string 31 | { 32 | return \sprintf('[php %s] %s %s', $level, $message, $this->formatContext($context)); 33 | } 34 | 35 | protected function formatContext(array $context): string 36 | { 37 | try { 38 | return \json_encode($context, \JSON_THROW_ON_ERROR); 39 | } catch (\JsonException) { 40 | return \print_r($context, true); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Message/Command/GetProcessId.php: -------------------------------------------------------------------------------- 1 | body = $body ?? ''; 42 | $this->header = $header ?? ''; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/PayloadFactory.php: -------------------------------------------------------------------------------- 1 | payload ?? ''; 21 | 22 | if ($frame->hasFlag(Frame::CONTROL)) { 23 | return self::makeControl($payload); 24 | } 25 | 26 | if (($frame->byte10 & Frame::BYTE10_STOP) !== 0) { 27 | return new StreamStop($payload); 28 | } 29 | 30 | if (($frame->byte10 & Frame::BYTE10_PONG) !== 0) { 31 | return new Pong($payload); 32 | } 33 | 34 | return new Payload( 35 | \substr($payload, $frame->options[0]), 36 | \substr($payload, 0, $frame->options[0]), 37 | ); 38 | } 39 | 40 | private static function makeControl(string $header): Payload 41 | { 42 | try { 43 | $command = self::decode($header); 44 | } catch (\JsonException $e) { 45 | throw new RoadRunnerException('Invalid task header, JSON payload is expected: ' . $e->getMessage()); 46 | } 47 | 48 | if (!empty($command['stop'])) { 49 | return new WorkerStop(null, $header); 50 | } 51 | 52 | if (!empty($command['pid'])) { 53 | return new GetProcessId(null, $header); 54 | } 55 | 56 | throw new RoadRunnerException('Invalid task header, undefined control package'); 57 | } 58 | 59 | /** 60 | * @throws \JsonException 61 | * @psalm-assert non-empty-string $json 62 | */ 63 | private static function decode(string $json): array 64 | { 65 | $result = \json_decode($json, true, 512, self::JSON_DECODE_FLAGS); 66 | 67 | if (! \is_array($result)) { 68 | throw new \JsonException('Json message must be an array or object'); 69 | } 70 | 71 | return $result; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/StreamWorkerInterface.php: -------------------------------------------------------------------------------- 1 | 25 | * $worker = Worker::create(); 26 | * 27 | * while ($receivedPayload = $worker->waitPayload()) { 28 | * $worker->respond(new Payload("DONE", json_encode($context))); 29 | * } 30 | * 31 | */ 32 | class Worker implements StreamWorkerInterface 33 | { 34 | private const JSON_ENCODE_FLAGS = \JSON_THROW_ON_ERROR | \JSON_PRESERVE_ZERO_FRACTION; 35 | 36 | /** @var array */ 37 | private array $payloads = []; 38 | 39 | /** @var int<0, max> Count of frames sent in stream mode */ 40 | private int $framesSent = 0; 41 | 42 | private bool $streamMode = false; 43 | private bool $shouldPing = false; 44 | private bool $waitingPong = false; 45 | 46 | public function __construct( 47 | private readonly RelayInterface $relay, 48 | bool $interceptSideEffects = true, 49 | private readonly LoggerInterface $logger = new Logger(), 50 | ) { 51 | if ($interceptSideEffects) { 52 | StdoutHandler::register(); 53 | } 54 | } 55 | 56 | /** 57 | * Create a new RoadRunner {@see Worker} using global 58 | * environment ({@see Environment}) configuration. 59 | */ 60 | public static function create(bool $interceptSideEffects = true, LoggerInterface $logger = new Logger()): self 61 | { 62 | return static::createFromEnvironment( 63 | env: Environment::fromGlobals(), 64 | interceptSideEffects: $interceptSideEffects, 65 | logger: $logger, 66 | ); 67 | } 68 | 69 | /** 70 | * Create a new RoadRunner {@see Worker} using passed environment 71 | * configuration. 72 | */ 73 | public static function createFromEnvironment( 74 | EnvironmentInterface $env, 75 | bool $interceptSideEffects = true, 76 | LoggerInterface $logger = new Logger(), 77 | ): self { 78 | $address = $env->getRelayAddress(); 79 | \assert($address !== '', 'Relay address must be specified in environment'); 80 | 81 | return new self( 82 | relay: Relay::create($address), 83 | interceptSideEffects: $interceptSideEffects, 84 | logger: $logger, 85 | ); 86 | } 87 | 88 | public function getLogger(): LoggerInterface 89 | { 90 | return $this->logger; 91 | } 92 | 93 | public function waitPayload(): ?Payload 94 | { 95 | while (true) { 96 | if ($this->payloads !== []) { 97 | $payload = \array_shift($this->payloads); 98 | } else { 99 | $frame = $this->relay->waitFrame(); 100 | $payload = PayloadFactory::fromFrame($frame); 101 | } 102 | 103 | switch (true) { 104 | case $payload::class === Payload::class: 105 | return $payload; 106 | case $payload instanceof WorkerStop: 107 | $this->waitingPong = false; 108 | return null; 109 | case $payload::class === GetProcessId::class: 110 | $this->sendProcessId(); 111 | continue 2; 112 | case $payload instanceof Pong: 113 | $this->waitingPong = false; 114 | continue 2; 115 | case $payload instanceof SkipMessage: 116 | continue 2; 117 | } 118 | } 119 | } 120 | 121 | public function withStreamMode(): static 122 | { 123 | $clone = clone $this; 124 | $clone->streamMode = true; 125 | $clone->framesSent = 0; 126 | $clone->shouldPing = false; 127 | $clone->waitingPong = false; 128 | return $clone; 129 | } 130 | 131 | /** 132 | * @param int|null $codec The codec used for encoding the payload header. 133 | * Can be {@see Frame::CODEC_PROTO} for Protocol Buffers or {@see Frame::CODEC_JSON} for JSON. 134 | * This parameter will be removed in v4.0 and {@see Frame::CODEC_PROTO} will be used by default. 135 | */ 136 | public function respond(Payload $payload, ?int $codec = null): void 137 | { 138 | $this->streamMode and ++$this->framesSent; 139 | $this->send($payload->body, $payload->header, $payload->eos, $codec); 140 | } 141 | 142 | public function error(string $error): void 143 | { 144 | $frame = new Frame($error, [], Frame::ERROR); 145 | 146 | $this->sendFrame($frame); 147 | } 148 | 149 | public function stop(): void 150 | { 151 | $this->send('', $this->encode(['stop' => true])); 152 | } 153 | 154 | public function hasPayload(?string $class = null): bool 155 | { 156 | return $this->findPayload($class) !== null; 157 | } 158 | 159 | public function getPayload(?string $class = null): ?Payload 160 | { 161 | $pos = $this->findPayload($class); 162 | if ($pos === null) { 163 | return null; 164 | } 165 | $result = $this->payloads[$pos]; 166 | unset($this->payloads[$pos]); 167 | 168 | return $result; 169 | } 170 | 171 | /** 172 | * @param class-string|null $class 173 | * 174 | * @return null|int Index in {@see $this->payloads} or null if not found 175 | */ 176 | private function findPayload(?string $class = null): ?int 177 | { 178 | // Find in existing payloads 179 | if ($this->payloads !== []) { 180 | if ($class === null) { 181 | return \array_key_first($this->payloads); 182 | } 183 | 184 | foreach ($this->payloads as $pos => $payload) { 185 | if ($payload::class === $class) { 186 | return $pos; 187 | } 188 | } 189 | } 190 | 191 | do { 192 | if ($class === null && $this->payloads !== []) { 193 | return \array_key_first($this->payloads); 194 | } 195 | 196 | $payload = $this->pullPayload(); 197 | if ($payload === null || $payload instanceof Pong) { 198 | break; 199 | } 200 | 201 | $this->payloads[] = $payload; 202 | if ($class !== null && $payload::class === $class) { 203 | return \array_key_last($this->payloads); 204 | } 205 | } while (true); 206 | 207 | return null; 208 | } 209 | 210 | /** 211 | * Pull {@see Payload} if it is available without blocking. 212 | */ 213 | private function pullPayload(): ?Payload 214 | { 215 | if (!$this->waitingPong && $this->relay instanceof BlockingRelayInterface) { 216 | if (!$this->streamMode) { 217 | return null; 218 | } 219 | 220 | $this->haveToPing(); 221 | return null; 222 | } 223 | 224 | if (!$this->relay->hasFrame()) { 225 | return null; 226 | } 227 | 228 | $frame = $this->relay->waitFrame(); 229 | $payload = PayloadFactory::fromFrame($frame); 230 | 231 | if ($payload instanceof Pong) { 232 | $this->waitingPong = false; 233 | return null; 234 | } 235 | 236 | return $payload; 237 | } 238 | 239 | private function send(string $body = '', string $header = '', bool $eos = true, ?int $codec = null): void 240 | { 241 | $frame = new Frame($header . $body, [\strlen($header)]); 242 | 243 | if (!$eos) { 244 | $frame->byte10 |= Frame::BYTE10_STREAM; 245 | } 246 | 247 | if ($this->shouldPing) { 248 | $frame->byte10 |= Frame::BYTE10_PING; 249 | } 250 | 251 | if ($codec !== null) { 252 | $frame->setFlag($codec); 253 | } 254 | 255 | $this->sendFrame($frame); 256 | } 257 | 258 | private function sendFrame(Frame $frame): void 259 | { 260 | try { 261 | if ($this->streamMode && ($frame->byte10 & Frame::BYTE10_STREAM) && $this->shouldPing) { 262 | $frame->byte10 |= Frame::BYTE10_PING; 263 | $this->shouldPing = false; 264 | $this->waitingPong = true; 265 | } 266 | 267 | $this->relay->send($frame); 268 | } catch (GoridgeException $e) { 269 | throw new TransportException($e->getMessage(), $e->getCode(), $e); 270 | } catch (\Throwable $e) { 271 | throw new RoadRunnerException($e->getMessage(), (int) $e->getCode(), $e); 272 | } 273 | } 274 | 275 | private function encode(array $payload): string 276 | { 277 | return \json_encode($payload, self::JSON_ENCODE_FLAGS); 278 | } 279 | 280 | private function sendProcessId(): void 281 | { 282 | $frame = new Frame($this->encode(['pid' => \getmypid()]), [], Frame::CONTROL); 283 | $this->sendFrame($frame); 284 | } 285 | 286 | private function haveToPing(): void 287 | { 288 | if ($this->waitingPong || $this->framesSent === 0) { 289 | return; 290 | } 291 | 292 | if ($this->framesSent % 5 === 0) { 293 | $this->shouldPing = true; 294 | } 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /src/WorkerAwareInterface.php: -------------------------------------------------------------------------------- 1 | 32 | * $worker->error('Something Went Wrong'); 33 | * 34 | * 35 | * @throws RoadRunnerException 36 | */ 37 | public function error(string $error): void; 38 | 39 | /** 40 | * Terminate the process. Server must automatically pass task to the next 41 | * available process. Worker will receive stop command after calling this 42 | * method. 43 | * 44 | * Attention, you MUST use continue; after invoking this method to let 45 | * RoadRunner to properly stop worker. 46 | */ 47 | public function stop(): void; 48 | 49 | /** 50 | * @param class-string|null $class 51 | * 52 | * @return bool Returns {@see true} if worker is ready to accept new payload. 53 | */ 54 | public function hasPayload(?string $class = null): bool; 55 | 56 | /** 57 | * @param class-string|null $class 58 | * 59 | * @return Payload|null Returns {@see null} if worker is not ready to accept new payload and has no cached payload 60 | * of the given type. 61 | */ 62 | public function getPayload(?string $class = null): ?Payload; 63 | } 64 | -------------------------------------------------------------------------------- /src/WorkerPool.php: -------------------------------------------------------------------------------- 1 | rpc = $rpc->withCodec(new JsonCodec()); 32 | } 33 | 34 | /** 35 | * Add worker to the pool. 36 | * 37 | * @param non-empty-string $plugin 38 | */ 39 | public function addWorker(string $plugin): void 40 | { 41 | $this->rpc->call('informer.AddWorker', $plugin); 42 | } 43 | 44 | /** 45 | * Get the number of workers for the pool. 46 | * 47 | * @param non-empty-string $plugin 48 | */ 49 | public function countWorkers(string $plugin): int 50 | { 51 | return \count($this->getWorkers($plugin)); 52 | } 53 | 54 | /** 55 | * Get the info about running workers for the pool. 56 | * 57 | * @param non-empty-string $plugin 58 | */ 59 | public function getWorkers(string $plugin): Workers 60 | { 61 | /** 62 | * @var array{workers: list} $data 63 | */ 64 | $data = $this->rpc->call('informer.Workers', $plugin); 65 | 66 | return new Workers(\array_map(static function (array $worker): InformerWorker { 67 | return new InformerWorker( 68 | pid: $worker['pid'], 69 | statusCode: $worker['status'], 70 | executions: $worker['numExecs'], 71 | createdAt: $worker['created'], 72 | memoryUsage: $worker['memoryUsage'], 73 | cpuUsage: $worker['CPUPercent'], 74 | command: $worker['command'], 75 | status: $worker['statusStr'], 76 | ); 77 | }, $data['workers'])); 78 | } 79 | 80 | /** 81 | * Remove worker from the pool. 82 | * 83 | * @param non-empty-string $plugin 84 | */ 85 | public function removeWorker(string $plugin): void 86 | { 87 | $this->rpc->call('informer.RemoveWorker', $plugin); 88 | } 89 | } 90 | --------------------------------------------------------------------------------