Skip to main content

Worker pool bootstrap

Three entry points exist for starting a worker pool: WorkerPoolApp (recommended for most applications), WorkerPoolBootstrap (lower-level), and WorkerStartHandler (direct interface for maximum control).

WorkerPoolApp

Extend WorkerPoolApp, override configure(), and call run() on the subclass:

src/App/MyApp.php
<?php

declare(strict_types=1);

use Monadial\Nexus\WorkerPool\Swoole\WorkerPoolApp;
use Monadial\Nexus\WorkerPool\WorkerNode;
use Monadial\Nexus\WorkerPool\WorkerPoolConfig;
use Monadial\Nexus\Core\Actor\Props;
use Monadial\Nexus\Core\Actor\Behavior;

final class MyApp extends WorkerPoolApp
{
protected function configure(WorkerNode $node): void
{
$node->spawn(
Props::fromBehavior(
Behavior::receive(static fn ($ctx, $msg) => Behavior::same()),
),
'orders',
);
}
}

MyApp::run(WorkerPoolConfig::withThreads(swoole_cpu_num()));

configure() is called once per worker thread with a fresh WorkerNode. The class is re-instantiated in each thread, so closures inside configure() do not cross thread boundaries.

WorkerPoolBootstrap

Use WorkerPoolBootstrap when you need to wire up the handler class separately from the bootstrap entry point:

src/bootstrap.php
<?php

declare(strict_types=1);

use Monadial\Nexus\WorkerPool\Swoole\WorkerPoolBootstrap;
use Monadial\Nexus\WorkerPool\WorkerPoolConfig;

WorkerPoolBootstrap::create(WorkerPoolConfig::withThreads(4))
->withHandler(MyWorkerStartHandler::class)
->run();

withHandler() accepts a class-string<WorkerStartHandler>. The class is instantiated fresh in each thread — no shared state.

WorkerStartHandler

Implement WorkerStartHandler directly when you need the full interface without the WorkerPoolApp base class:

src/App/MyWorkerStartHandler.php
<?php

declare(strict_types=1);

use Monadial\Nexus\WorkerPool\WorkerStartHandler;
use Monadial\Nexus\WorkerPool\WorkerNode;
use Monadial\Nexus\Core\Actor\Props;
use Monadial\Nexus\Core\Actor\Behavior;

final class MyWorkerStartHandler implements WorkerStartHandler
{
public function onWorkerStart(WorkerNode $node): void
{
$node->spawn(Props::fromBehavior($behavior), 'orders');
}
}

Looking up actors

After spawning, retrieve an actor reference by path from within a handler:

src/App/MyWorkerStartHandler.php
$ref = $node->actorFor('/user/orders');  // returns null if not registered

Example: distributed counter

Eight named counters spread across four workers via the hash ring:

src/App/CounterApp.php
<?php

declare(strict_types=1);

use Monadial\Nexus\WorkerPool\Swoole\WorkerPoolApp;
use Monadial\Nexus\WorkerPool\WorkerNode;
use Monadial\Nexus\WorkerPool\WorkerPoolConfig;
use Monadial\Nexus\Core\Actor\Behavior;
use Monadial\Nexus\Core\Actor\BehaviorWithState;
use Monadial\Nexus\Core\Actor\Props;

readonly class Increment {}
readonly class GetCount
{
public function __construct(public \Monadial\Nexus\Core\Actor\ActorRef $replyTo) {}
}

final class CounterApp extends WorkerPoolApp
{
protected function configure(WorkerNode $node): void
{
for ($i = 0; $i < 8; $i++) {
$node->spawn(
Props::fromBehavior(
Behavior::withState(
0,
static function ($ctx, $msg, int $count) {
if ($msg instanceof Increment) {
return BehaviorWithState::next($count + 1);
}

if ($msg instanceof GetCount) {
$msg->replyTo->tell($count);
}

return BehaviorWithState::same();
},
),
),
"counter-{$i}",
);
}
}
}

CounterApp::run(WorkerPoolConfig::withThreads(4));

Each "counter-N" name hashes to one of the four workers. Workers that hash "counter-N" to themselves get a LocalActorRef; workers that hash it to another worker get a WorkerActorRef that routes transparently.

See also