Skip to main content

Cross-worker ask

When one actor sends an ask() to a WorkerActorRef on a different thread, the request and response travel through the WorkerTransport — a Swoole Thread\Queue. This page covers the latency profile, the protocol, and the differences from a same-worker ask.

How it works

WorkerActorRef::ask() builds a WorkerAskRequest envelope and sends it to the target worker's Thread\Queue. The target worker's WorkerNode receives the request, delivers it to the local actor, and sends a WorkerAskReply back to the originating worker's queue. The originating worker's WorkerNode resolves the FutureSlot and unblocks the calling fiber.

The object is not serialized — Swoole's Thread\Queue transfers PHP objects between threads by copying their serialized representation internally. There is no explicit user-facing serializer call.

Latency comparison

Ask typePathApproximate overhead
Same-worker askDirect mailbox enqueue + fiber suspend< 0.1 ms
Cross-worker askThread\Queue send + remote mailbox + Thread\Queue reply + fiber resume0.5–2 ms (depends on queue depth and Swoole polling interval)

The cross-worker overhead comes from the adaptive-poll loop in ThreadQueueTransport: it backs off from 0 µs → 100 µs → 1 ms → 10 ms between polls when the queue is idle. Under load, the first message typically lands in the 0–100 µs window.

Usage

WorkerActorRef is obtained from WorkerNode by looking up an actor that lives on a different thread. The API is identical to a same-worker ask:

src/Handlers/CheckoutHandler.php
<?php

declare(strict_types=1);

use Monadial\Nexus\Runtime\Duration;
use Monadial\Nexus\WorkerPool\WorkerActorRef;

/** @var WorkerActorRef<InventoryCommand> $inventoryRef */
$inventoryRef = $workerNode->ref('inventory');

// ask() returns a Future — await it inside a Swoole coroutine
$result = $inventoryRef->ask(
new ReserveItems($orderId, $items),
Duration::seconds(5),
)->await();

Timeout and retry behaviour

WorkerNode retries the ask request up to 3 times with 50 ms between retries if the target worker does not acknowledge (WorkerAskAck) within the first attempt window. After 3 failed attempts, the Future is failed with AskTimeoutException.

The Duration passed to ask() is the total client-side timeout. Set it generously enough to absorb retries: a value below 150 ms risks spurious timeouts when the target worker is briefly busy.

Serialization in V1

In V1 (thread-mode only), objects travel across threads via Swoole's Thread\Queue, which performs an internal serialize/unserialize. This means:

  • Object identity is not preserved — a reference sent to another thread is a deep copy on arrival.
  • Non-serializable resources (database connections, file handles) must not be included in messages.
  • The #[MessageType] attribute is not required for cross-worker messages (unlike cluster-mode remote transport where explicit serialization is configured).
Resource handles in messages

Do not include PDO connections, Swoole coroutine channels, or other resource handles in cross-worker messages. Swoole's internal thread-queue serialization will fail or produce corrupt values.

When cross-worker ask adds up

A request that fans out to N workers and awaits all responses incurs N × cross-worker latency. For fan-out to all workers, prefer tell() with a coordinator actor collecting replies, rather than N sequential ask() calls.

See also