├── 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 |
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 |
--------------------------------------------------------------------------------