PHPackages                             hiblaphp/parallel - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. hiblaphp/parallel

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

hiblaphp/parallel
=================

04PHPCI passing

Since Mar 12Pushed 1mo agoCompare

[ Source](https://github.com/hiblaphp/parallel)[ Packagist](https://packagist.org/packages/hiblaphp/parallel)[ RSS](/packages/hiblaphp-parallel/feed)WikiDiscussions main Synced 1mo ago

READMEChangelogDependenciesVersions (3)Used By (0)

Hibla Parallel
==============

[](#hibla-parallel)

**The high-performance, self-healing, and cross-platform parallel processing engine for PHP.**

Hibla Parallel brings **Erlang-style reliability** and **Node.js-level cluster pool performance** to the PHP ecosystem. Orchestrate worker clusters that are fast (proven **100,000+ RPS** on http socket server benchmarks), truly non-blocking on all platforms, and capable of healing themselves through a supervised "Let it Crash" architecture.

[![Latest Release](https://camo.githubusercontent.com/db9c2dbc954584d88142b4fa635f50b585df818c79f8b2b55fa6672c9ffb616b/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f72656c656173652f6869626c617068702f706172616c6c656c2e7376673f7374796c653d666c61742d737175617265)](https://github.com/hiblaphp/parallel/releases)[![MIT License](https://camo.githubusercontent.com/942e017bf0672002dd32a857c95d66f28c5900ab541838c6c664442516309c8a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c75652e7376673f7374796c653d666c61742d737175617265)](./LICENSE)

---

Contents
--------

[](#contents)

**Getting started**

- [Installation](#installation)
- [Quick Start](#quick-start)
- [Quick Reference](#quick-reference)

**Core concepts**

- [How Parallel Works](#how-parallel-works)
- [Worker Types](#worker-types)
- [Serialization: Crossing the Process Boundary](#serialization-crossing-the-process-boundary)
- [IPC: Inter-Process Communication](#ipc-inter-process-communication)

**Writing tasks**

- [Simple Primitives](#simple-primitives)
- [Rich Data &amp; Stateful Execution](#rich-data--stateful-execution)
- [Global Helper Functions](#global-helper-functions)
    - [`parallel()`](#parallelcallable-task-int-timeout--null)
    - [`parallelFn()`](#parallelFncallable-task-int-timeout--null)
    - [`spawn()`](#spawncallable-task-int-timeout--null)
    - [`spawnFn()`](#spawnFncallable-task-int-timeout--null)
    - [Using `runFn` and `spawnFn` on fluent executors](#using-runfn-and-spawnfn-on-fluent-executors)

**Persistent pools**

- [Persistent Worker Pools](#persistent-worker-pools)
    - [Automatic garbage collection between tasks](#automatic-garbage-collection-between-tasks)
    - [Why `withMaxExecutionsPerWorker`](#why-withmaxexecutionsperworker)
    - [Lazy vs. Eager Spawning](#lazy-vs-eager-spawning)
        - [Pre-warming with `boot()`](#pre-warming-with-boot)
        - [Lazy Spawning](#lazy-spawning)
- [Self-Healing &amp; Supervisor Pattern](#self-healing--supervisor-pattern)
    - [Complete example: chaos server](#complete-example-chaos-server)
- [Respawn Rate Limiting](#respawn-rate-limiting)
    - [Fail-fast, not slow-fail](#fail-fast-not-slow-fail)
    - [Choosing the right limit](#choosing-the-right-limit)
    - [Interaction with `onWorkerRespawn`](#interaction-with-onworkerrespawn)
- [Pool Monitoring](#pool-monitoring)
    - [PID accuracy and `boot()`](#pid-accuracy-and-boot)

**Real-time communication**

- [Real-time Output &amp; Messaging](#real-time-output--messaging)
    - [Console Streaming](#console-streaming)
    - [Structured Messaging with `emit()`](#structured-messaging-with-emit)
    - [Pool-Level vs. Per-Task Message Handlers](#pool-level-vs-per-task-message-handlers)
    - [Handler completion blocks task promise resolution](#handler-completion-blocks-task-promise-resolution)

**Advanced patterns**

- [Fractal Concurrency: The Async Hybrid](#fractal-concurrency-the-async-hybrid)
- [Nested Execution &amp; Safety](#nested-execution--safety)
    - [The nested closure problem](#the-nested-closure-problem)
    - [Closure safety levels](#closure-safety-levels)
    - [Always `await()` nested calls](#always-await-nested-calls)
    - [Configuring the nesting limit](#configuring-the-nesting-limit)

**Error handling &amp; safety**

- [Distributed Exception Teleportation](#distributed-exception-teleportation)
- [Abnormal Termination Detection](#abnormal-termination-detection)
- [Task Cancellation &amp; Management](#task-cancellation--management)
- [Exception Reference](#exception-reference)

**Project setup**

- [Framework Bootstrapping](#framework-bootstrapping)
- [Autoloading &amp; Code Availability](#autoloading--code-availability)
- [Global Configuration](#global-configuration)
- [Architecture &amp; Testability](#architecture--testability)
    - [Strategy Map](#strategy-map)
    - [Using the Facade](#using-the-facade-recommended)
    - [Direct Instantiation](#direct-instantiation-best-for-di)
    - [Dependency Injection &amp; Mocking](#dependency-injection--mocking)

**Meta**

- [Credits](#credits)
- [License](#license)

---

Installation
------------

[](#installation)

```
composer require hiblaphp/parallel
```

**Requirements:**

- PHP 8.3+
- `hiblaphp/event-loop`
- `hiblaphp/promise`
- `hiblaphp/stream`
- `hiblaphp/async`

---

Quick Start
-----------

[](#quick-start)

The fastest way to run code in a separate process and get the result back:

```
require __DIR__ . '/vendor/autoload.php';

use function Hibla\{parallel, await};

$result = await(parallel(function () {
    return 'Hello from a separate process!';
}));

echo $result; // Hello from a separate process!
```

That is the entire API for the common case. The rest of this document covers everything built on top of it.

---

Quick Reference
---------------

[](#quick-reference)

Choose the right tool for your use case:

Use CaseAPIReturnsRun a task and get its result`parallel(fn() => ...)` or `Parallel::task()->run(...)``PromiseInterface`Run many tasks, wrapping a callable`parallelFn(callable)` or `Parallel::task()->runFn(callable)``callable(mixed ...$args): PromiseInterface`Fire-and-forget background work`spawn(fn() => ...)` or `Parallel::background()->spawn(...)``PromiseInterface`Fire-and-forget, wrapping a callable`spawnFn(callable)` or `Parallel::background()->spawnFn(callable)``callable(mixed ...$args): PromiseInterface`High-throughput repeated work`Parallel::pool(n)->run(...)``PromiseInterface`High-throughput, wrapping a callable`Parallel::pool(n)->runFn(callable)``callable(mixed ...$args): PromiseInterface`**Fluent configuration** is available on all strategies:

MethodEffect`->withTimeout(int $seconds)`Reject with `TimeoutException` after N seconds, terminate the worker and its full process tree`->withoutTimeout()`Disable the timeout entirely`->withMemoryLimit(string $limit)`Set worker memory limit (e.g. `'256M'`, `'1G'`)`->withUnlimitedMemory()`Set memory limit to `-1``->withBootstrap(string $file, ?callable $cb)`Load a framework or legacy environment in the worker`->withMaxNestingLevel(int $level)`Override the fork-bomb safety limit (1–10)`->withLazySpawning()`*(Pool only)* Spawn workers on first `run()` call instead of at construction`->withMaxExecutionsPerWorker(int $n)`*(Pool only)* Retire and replace workers after N tasks`->withMaxRestartPerSecond(int $n)`*(Pool only)* Limit respawn rate to N workers per second`->onMessage(callable $handler)`*(Task/Pool)* Register a handler for `emit()` messages from workers`->onWorkerRespawn(callable $handler)`*(Pool only)* Called whenever a worker exits and a replacement is spawned`->boot()`*(Pool only)* Pre-warm all workers immediately; returns before workers are ready---

How Parallel Works
------------------

[](#how-parallel-works)

PHP is a single-threaded runtime. The event loop and fiber-based concurrency in `hiblaphp/async` let you interleave non-blocking I/O efficiently, but they do not escape this constraint: one thread, one CPU core. When you need to run genuinely CPU-bound work, call a blocking library that cannot be made async, or isolate a task so a fatal error in it cannot kill your main process, you need true parallelism.

There are two approaches to parallelism in PHP: multi-threading and multi-processing. Multi-threading via extensions like `ext-parallel` runs multiple threads inside the same process and shares memory between them, but it is CLI-only, requires complex synchronization primitives (mutexes, channels), and is inherently prone to race conditions when shared state is not carefully managed. Hibla Parallel does not use this model. Multi-processing spawns independent OS processes that share nothing and communicate explicitly. It is simpler to reason about, safer by default, and available in every PHP environment without extensions. This is the model Hibla Parallel is built on, and multi-threading support is not currently planned.

Hibla Parallel solves this by spawning real child processes via `proc_open()` and communicating with them over OS-level I/O channels. Each worker is a fresh PHP process with its own memory space, its own CPU scheduling, and its own crash domain. A segfault, out-of-memory kill, or unhandled fatal in a worker cannot corrupt or terminate the parent.

When you call `parallel(fn() => heavyWork())`:

1. **Spawn.** The parent calls `proc_open()` to start a new PHP process running one of Hibla's worker scripts. The worker receives its I/O channels (stdin, stdout, stderr) at the OS level.
2. **Serialize.** The parent serializes the closure (its code, bound variables, and `$this` if captured) into a JSON payload and writes it to the worker's stdin.
3. **Execute.** The worker reads the payload, deserializes the closure, runs it inside its own event loop, and captures the return value.
4. **Communicate.** The worker writes structured JSON frames back to the parent over stdout in real time: output frames as the task runs, a terminal frame when it finishes.
5. **Deserialize.** The parent reads the terminal frame, deserializes the result, and resolves the promise.

The parent never blocks during this. Communication happens over non-blocking streams managed by `hiblaphp/stream`, so the parent's event loop continues running other fibers, timers, and I/O while it waits for a worker to finish. A pool of four workers can handle four tasks simultaneously without the parent event loop stalling at all.

```
Parent process (event loop running)
│
├── parallel(fn() => taskA())  ──spawn──▶  Worker A (own PHP process)
│       └── Promise                          runs taskA(), writes frames
│
├── parallel(fn() => taskB())  ──spawn──▶  Worker B (own PHP process)
│       └── Promise                          runs taskB(), writes frames
│
└── await(Promise::all([A, B]))
        │
        │  ◀── stdout frames ── Worker A resolves → promise A resolved
        │  ◀── stdout frames ── Worker B resolves → promise B resolved
        │
        └── both results available, script continues

```

---

Worker Types
------------

[](#worker-types)

Hibla Parallel ships three worker scripts, each optimized for a different execution model. The parent selects the correct script automatically based on which API you use.

### Streamed worker (`worker.php`)

[](#streamed-worker-workerphp)

Used by `parallel()` and `Parallel::task()`. Spawned fresh for each task. Maintains full bidirectional communication with the parent: OUTPUT frames stream in real time, `emit()` sends MESSAGE frames, exceptions teleport back as ERROR frames, and results are transmitted as COMPLETED frames. The worker exits after handling one task.

### Background worker (`worker_background.php`)

[](#background-worker-worker_backgroundphp)

Used by `spawn()` and `Parallel::background()`. Spawned fresh for each task. stdout and stderr are redirected to `/dev/null` at spawn time, so the parent has no communication channel back from this worker. The worker runs the task and exits. This means `emit()` is a silent no-op inside background workers, and there is no result, no exception teleportation, and no real-time output. Use this only for true fire-and-forget work where you do not need to know what happened inside.

### Persistent worker (`worker_persistent.php`)

[](#persistent-worker-worker_persistentphp)

Used by `Parallel::pool()`. Spawned once per pool slot at pool construction (or on first use with lazy spawning) and kept alive to handle many tasks sequentially. Shares the same structured-frame communication protocol as the streamed worker but with task IDs added to every frame for routing, plus the READY/RETIRING/CRASHED lifecycle frames. Crashes trigger the pool's respawn logic. Retirement after `withMaxExecutionsPerWorker(n)` tasks triggers a clean RETIRING exit and an automatic replacement.

---

Serialization: Crossing the Process Boundary
--------------------------------------------

[](#serialization-crossing-the-process-boundary)

The central challenge in any multi-process system is that processes do not share memory. Everything that moves between the parent and a worker (the task to run, the result it produces, exceptions it throws, and messages it emits) must be converted to bytes, transmitted, and reconstructed on the other side.

### Serializing the task

[](#serializing-the-task)

PHP closures are not natively serializable. Hibla uses [opis/closure](https://github.com/opis/closure) to serialize the AST (Abstract Syntax Tree) of the closure's source code along with any variables it captures from its surrounding scope. The serialized representation is a compact string that the worker can deserialize back into a fully functional callable, including bound variables and the captured `$this` object if the closure was defined inside a class method.

```
$multiplier = 3;

$result = await(parallel(function () use ($multiplier) {
    // $multiplier = 3 is serialized with the closure and reconstructed
    // in the worker — the worker never shared memory with the parent,
    // it received a copy of the value
    return 6 * $multiplier; // 18
}));
```

The entire task payload is JSON-encoded before being written to the worker's stdin:

```
{
  "serialized_callback": "...",
  "autoload_path": "/var/www/vendor/autoload.php",
  "framework_bootstrap": null,
  "timeout_seconds": 60,
  "memory_limit": "512M"
}
```

### Serializing the result

[](#serializing-the-result)

Once the task finishes, the worker serializes the return value back to the parent. Scalar values (strings, integers, floats, booleans, null) and plain arrays are JSON-encoded directly. Objects and nested arrays containing objects are serialized with PHP's native `serialize()` and base64-encoded before being embedded in the JSON frame:

```
{
  "status": "COMPLETED",
  "result": "base64encodedSerializedObject==",
  "result_serialized": true
}
```

The parent checks the `result_serialized` flag, decodes and unserializes the value if needed, and resolves the promise with the reconstructed object. The object arrives in the parent with its type, properties, and values intact, as if it had never left the process.

### Serializing exceptions

[](#serializing-exceptions)

When a task throws, the worker captures the exception's class name, message, code, file, line, and full stack trace, encodes them as a structured ERROR frame, and sends it to the parent. The parent's `ExceptionHandler` re-instantiates the original exception class (falling back to `RuntimeException` if the class is not available in the parent) and merges the worker's stack trace into it, so the combined trace shows both where `parallel()` was called in the parent and where the exception was thrown in the worker.

### What cannot be serialized

[](#what-cannot-be-serialized)

Objects that hold active OS resources (open database connections, PDO handles, open file pointers, network sockets, cURL handles) cannot be serialized because the resource itself lives in the OS kernel and is tied to the specific process that opened it. Attempting to pass one will fail at serialization time with a `TaskPayloadException`.

The pattern for these cases is to initialize the resource inside the worker, not pass it in:

```
// Wrong — PDO handle cannot be serialized across processes
$db = new PDO('mysql:host=localhost;dbname=app', 'user', 'pass');
$result = await(parallel(function () use ($db) {
    return $db->query('SELECT ...');  // TaskPayloadException
}));

// Correct — create the connection inside the worker
$result = await(parallel(function () {
    $db = new PDO('mysql:host=localhost;dbname=app', 'user', 'pass');
    return $db->query('SELECT ...')->fetchAll();
}));
```

---

IPC: Inter-Process Communication
--------------------------------

[](#ipc-inter-process-communication)

Hibla Parallel communicates with workers over OS-level I/O channels created by `proc_open()`. Understanding this channel design explains why the library is non-blocking even on Windows and how real-time output streaming works.

### Structured frames

[](#structured-frames)

All communication uses a simple line-delimited JSON protocol, similar in spirit to [NDJSON](https://ndjson.org/) and [JSON Lines](https://jsonlines.org/), but implemented directly over `proc_open()` pipes rather than files or TCP sockets. The parent writes one JSON line to the worker's stdin to deliver the task. The worker writes JSON lines to its stdout as the task runs. Each line is a complete, self-contained JSON object terminated by a newline. The parent reads lines one at a time on a non-blocking stream and routes each frame by its `status` field.

Frame typeDirectionWhen it is sent`OUTPUT`Worker → ParentThe task called `echo` or `print``MESSAGE`Worker → ParentThe task called `emit()``COMPLETED`Worker → ParentThe task returned a value`ERROR`Worker → ParentThe task threw an exception`TIMEOUT`Worker → ParentThe task exceeded its time limit`READY`Worker → Parent*(Persistent workers only)* Worker booted and is ready for a task`RETIRING`Worker → Parent*(Persistent workers only)* Worker has hit its max executions limit and is exiting cleanly`CRASHED`Worker → Parent*(Persistent workers only)* Worker is dying due to an unrecoverable error### Pipes on Unix, sockets on Windows

[](#pipes-on-unix-sockets-on-windows)

On Linux and macOS, Hibla uses anonymous pipes (`['pipe', 'r']` and `['pipe', 'w']` in the `proc_open()` descriptor spec). Anonymous pipes support `stream_set_blocking(false)` correctly at the kernel level, making them fully compatible with the non-blocking stream layer. They are also approximately 15% faster than sockets for small messages.

On Windows, anonymous pipes do not support non-blocking mode at the kernel level. Calling `stream_set_blocking($pipe, false)` on a Windows anonymous pipe is silently ignored and the pipe remains blocking. A blocking read on a pipe that has no data stalls the entire PHP thread indefinitely, which would deadlock the event loop.

Hibla detects the platform at spawn time and switches to socket pairs (`['socket']`) on Windows. Socket pairs support true non-blocking mode on Windows and are the standard solution for this limitation. The rest of the code (stream reading, frame parsing, promise resolution) is identical on both platforms:

```
// From ProcessSpawnHandler::spawnStreamedTask()
$descriptorSpec = PHP_OS_FAMILY === 'Windows'
    ? [0 => ['socket'], 1 => ['socket'], 2 => ['socket']]
    : [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
```

### Stdin handshake and drain-and-wait

[](#stdin-handshake-and-drain-and-wait)

After the worker finishes and writes its terminal frame (COMPLETED, ERROR, or TIMEOUT), it does not exit immediately. On Windows, a process exiting instantly destroys its socket descriptors, which can wipe out bytes that are still in the OS transmit buffer before the parent's non-blocking stream reader has consumed them.

To prevent this, the worker enters a drain-and-wait loop after writing the terminal frame: it switches its own stdin to non-blocking mode and reads in a 5ms polling loop for up to 500ms. The parent signals it is done by closing its end of the stdin pipe after receiving the terminal frame, which causes `feof()` to return `true` in the worker's drain loop and lets the worker exit cleanly. This handshake guarantees the terminal frame is always fully received before the worker process disappears.

### Persistent worker IPC

[](#persistent-worker-ipc)

Persistent pool workers use the same channel design but stay alive across multiple tasks. Instead of a one-shot payload-and-exit model, the boot sequence is:

1. Parent spawns the worker and writes a one-time boot payload to stdin with the autoload path, bootstrap configuration, memory limit, and retirement threshold.
2. Worker loads the autoloader, runs any framework bootstrap, and writes `{"status":"READY","pid":12345}` to stdout.
3. Parent dispatches queued tasks by writing JSON lines to stdin, one per task, each identified by a unique `task_id`.
4. Worker reads task lines in a loop, executes each one, and writes COMPLETED or ERROR frames tagged with the same `task_id` back to stdout.
5. All frames from all tasks share the same stdout channel. The `task_id` tag allows the parent to route each frame to the correct task's promise and `onMessage` handler.

The worker reports its own PID (`getmypid()`) in the READY frame because `proc_get_status()['pid']` returns the shell wrapper's PID on some platforms rather than the actual PHP process PID. The self-reported PID is used for `SIGKILL` / `taskkill` on cancellation and is what `getWorkerPids()` returns once the READY frame has been received. See [Pool Monitoring](#pool-monitoring) for the full detail on PID accuracy.

---

Simple Primitives
-----------------

[](#simple-primitives)

Hibla provides global helper functions for the most common use cases.

### One-off tasks with results

[](#one-off-tasks-with-results)

```
use function Hibla\{parallel, await};

$result = await(parallel(function () {
    return strlen("Hello from the background!");
}));

echo $result; // 27
```

### Fire-and-forget

[](#fire-and-forget)

```
use function Hibla\{spawn, await};

// spawn() returns a Promise that resolves to a BackgroundProcess handle.
// You must await() the spawn call itself to get that handle.
$process = await(spawn(function () {
    file_put_contents('log.txt', "Task started at " . date('Y-m-d H:i:s'));
    sleep(5);
}));

if ($process->isRunning()) {
    echo "Task is running with PID: " . $process->getPid();
}

// $process->terminate(); // kill it early if needed
```

---

Rich Data &amp; Stateful Execution
----------------------------------

[](#rich-data--stateful-execution)

Hibla Parallel is not limited to scalar return values. Because it uses a full serialization engine, you can pass objects into tasks, return objects from tasks, and access class state from inside parallel closures, all of it crossing the process boundary transparently.

### Returning value objects

[](#returning-value-objects)

Any serializable object returned from a worker is reconstructed in the parent with its type, class name, and property values intact:

```
use App\ValueObjects\TaskResult;
use function Hibla\{parallel, await};

$result = await(parallel(function () {
    return new TaskResult(status: 'success', data: [1, 2, 3]);
}));

echo get_class($result); // "App\ValueObjects\TaskResult"
echo $result->status;    // "success"
```

### Accessing class properties (`$this`)

[](#accessing-class-properties-this)

When `parallel()` is called inside a class method, the closure can capture `$this`. The worker receives a serialized copy of the object and can read its properties, including private ones:

```
namespace App\Services;

class ReportBuilder
{
    public function __construct(
        private string $title = "Q4 Report",
        private int $year = 2025
    ) {}

    public function buildParallel(): string
    {
        return await(parallel(fn() => "{$this->title} ({$this->year})"));
    }
}

$builder = new ReportBuilder();
echo $builder->buildParallel(); // "Q4 Report (2025)"
```

### Two rules for object serialization

[](#two-rules-for-object-serialization)

1. **Autoloading:** The class definition must be available to the worker via the Composer autoloader or a bootstrap file. If the worker cannot find the class, deserialization will fail.
2. **No resources:** Objects holding active OS resources (PDO handles, open file pointers, network sockets) cannot cross the process boundary. See [Serialization: Crossing the Process Boundary](#serialization-crossing-the-process-boundary) for the correct pattern.

---

Global Helper Functions
-----------------------

[](#global-helper-functions)

Hibla Parallel exposes four global helpers in the `Hibla` namespace. They are thin wrappers around the facade and are the recommended API for most scripts. All four accept an optional `$timeout` argument. For anything beyond a simple timeout override (custom memory limits, bootstrap files, or message handlers) use the fluent executors directly (see [Using `runFn` and `spawnFn` on fluent executors](#using-runfn-and-spawnfn-on-fluent-executors)).

### `parallel(callable $task, ?int $timeout = null)`

[](#parallelcallable-task-int-timeout--null)

Runs a task in a new process and returns a `Promise` resolving to the task's return value.

```
use function Hibla\{parallel, await};

$result = await(parallel(function () {
    return expensiveComputation();
}, timeout: 30));
```

### `parallelFn(callable $task, ?int $timeout = null)`

[](#parallelfncallable-task-int-timeout--null)

Wraps a callable and returns a new callable that spawns a fresh parallel process on each invocation. Useful with higher-order functions like `Promise::all()` or event listeners where you need to pass a callable rather than invoke one immediately.

```
use function Hibla\{parallelFn, await};
use Hibla\Promise\Promise;

$processItem = parallelFn(function (int $id): array {
    return fetchFromDatabase($id);
});

$results = await(Promise::all([
    $processItem(1),
    $processItem(2),
    $processItem(3),
]));
```

### `spawn(callable $task, ?int $timeout = null)`

[](#spawncallable-task-int-timeout--null)

Spawns a fire-and-forget background process. The returned `Promise` resolves immediately with a `BackgroundProcess` handle. No result, output, or exception is ever returned to the parent.

```
use function Hibla\{spawn, await};

$process = await(spawn(function () {
    generateReport();
}));

echo $process->getPid();
echo $process->isRunning() ? 'still running' : 'done';

$process->terminate(); // kill it early if needed
```

### `spawnFn(callable $task, ?int $timeout = null)`

[](#spawnfncallable-task-int-timeout--null)

The fire-and-forget equivalent of `parallelFn`. Returns a callable that spawns a new detached background process on each invocation.

```
use function Hibla\spawnFn;

$sendEmail = spawnFn(function (string $to, string $subject): void {
    mailer()->send($to, $subject);
});

$sendEmail('alice@example.com', 'Welcome!');
$sendEmail('bob@example.com', 'Your invoice');
```

> **Note:** `emit()` called inside a `spawn()` or `spawnFn()` task is a silent no-op. Fire-and-forget workers redirect stdout to `/dev/null`, so structured message passing is unavailable. Use `parallel()` or a pool if you need `emit()`.

### Using `runFn` and `spawnFn` on fluent executors

[](#using-runfn-and-spawnfn-on-fluent-executors)

The global helpers use default configuration. When you need custom timeouts, memory limits, a bootstrap file, or message handlers applied consistently to every invocation of the factory, use the `->runFn()` and `->spawnFn()` methods on the fluent executors instead.

The returned callable behaves identically to calling `->run()` or `->spawn()` directly. The only difference is that you get a reusable callable rather than an immediate promise.

**`Parallel::task()->runFn()`**

Each invocation spawns a fresh worker with the configured settings:

```
use Hibla\Parallel\Parallel;
use Hibla\Parallel\ValueObjects\WorkerMessage;
use Hibla\Promise\Promise;
use function Hibla\{await, emit};

$processItem = Parallel::task()
    ->withTimeout(30)
    ->withMemoryLimit('256M')
    ->withBootstrap('/path/to/bootstrap.php')
    ->onMessage(function (WorkerMessage $msg) {
        printf("Worker %d: %s\n", $msg->pid, $msg->data);
    })
    ->runFn(function (int $id): array {
        emit("Processing $id");
        return fetchAndTransform($id);
    });

$results = await(Promise::all([
    $processItem(1),
    $processItem(2),
    $processItem(3),
]));
```

`runFn()` accepts a per-invocation message handler as its second argument. It fires before the executor-level `onMessage` handler, following the same ordering rules described in [Pool-Level vs. Per-Task Message Handlers](#pool-level-vs-per-task-message-handlers):

```
$processItem = Parallel::task()
    ->onMessage(fn($msg) => Log::info('executor-level', [$msg->data]))
    ->runFn(
        function (int $id): array {
            return fetchAndTransform($id);
        },
        onMessage: fn($msg) => Log::debug('per-invocation', [$msg->data])
    );
```

**`Parallel::pool()->runFn()`**

Tasks are dispatched to the pool's persistent workers rather than spawning a fresh process per invocation. Use this when you want both the reuse efficiency of a pool and a consistent callable interface for higher-order functions:

```
use Hibla\Parallel\Parallel;
use Hibla\Promise\Promise;
use function Hibla\await;

$pool = Parallel::pool(size: 4)
    ->withTimeout(30)
    ->withMemoryLimit('256M');

$processItem = $pool->runFn(function (int $id): array {
    return fetchAndTransform($id);
});

$results = await(Promise::all(array_map($processItem, $ids)));

$pool->shutdown();
```

**`Parallel::background()->spawnFn()`**

Each invocation spawns a detached fire-and-forget process with the configured settings. The returned promise resolves immediately with a `BackgroundProcess` handle, not a task result:

```
use Hibla\Parallel\Parallel;
use function Hibla\await;

$sendEmail = Parallel::background()
    ->withTimeout(120)
    ->withMemoryLimit('128M')
    ->withBootstrap('/path/to/bootstrap.php')
    ->spawnFn(function (string $to, string $subject): void {
        Mailer::send($to, $subject);
    });

await($sendEmail('alice@example.com', 'Welcome!'));
await($sendEmail('bob@example.com', 'Invoice'));
```

> **Note:** `emit()` called inside a `->spawnFn()` task is a silent no-op. Fire-and-forget workers redirect stdout to `/dev/null`, so structured message passing is unavailable. Use `->runFn()` or a pool if you need `emit()`.

**Choosing between global helpers and fluent factories**

Use `parallelFn()` and `spawnFn()` for quick scripts where the defaults are acceptable. Use `->runFn()` and `->spawnFn()` on the fluent executors whenever you need consistent configuration, message handlers, or bootstrap logic applied to every invocation, or when working with a pool and want to avoid constructing a new executor on each call.

---

Persistent Worker Pools
-----------------------

[](#persistent-worker-pools)

Worker pools maintain a fixed set of long-lived workers to eliminate the overhead of repeated process spawning and framework bootstrapping. Each worker handles multiple tasks sequentially, making pools ideal for high-throughput workloads.

```
use Hibla\Parallel\Parallel;
use function Hibla\await;

$pool = Parallel::pool(size: 4)
    ->withMaxExecutionsPerWorker(100)
    ->withMemoryLimit('128M');

$task = fn() => getmypid();

for ($i = 0; $i < 4; $i++) {
    $pool->run($task)->then(fn($pid) => print("Handled by worker: $pid\n"));
}

/**
 * CRITICAL: Always shut down the pool when done.
 * Persistent workers hold open IPC channels that keep the Event Loop alive.
 * Without an explicit shutdown the script will never exit.
 */

// Option A: Synchronous — blocks until all queued tasks finish and all workers exit
$pool->shutdown();

// Option B: Graceful — rejects all incoming tasks while still waiting for current task to finish, then shuts down
// $pool->drain();
```

### Automatic garbage collection between tasks

[](#automatic-garbage-collection-between-tasks)

After every completed task, before writing the next `READY` frame, each persistent worker automatically calls `gc_collect_cycles()` to collect any reference cycles left behind by the task. This runs unconditionally regardless of any other configuration and provides a baseline level of memory hygiene at no cost to the caller.

For most workloads this is sufficient. If a task leaves behind significant heap allocations, large static caches, or framework state that the garbage collector cannot reclaim (which `gc_collect_cycles()` alone cannot address) use `withMaxExecutionsPerWorker()` to retire and replace workers at a predictable cadence.

### Why `withMaxExecutionsPerWorker`?

[](#why-withmaxexecutionsperworker)

Long-running pools can accumulate memory over time even with cycle collection in place. PHP's OPcache grows, framework static caches fill up, and residual heap allocations may persist across tasks. By retiring a worker after N tasks and replacing it with a fresh process, you get a fully clean memory slate at a predictable cadence, which is important for pools that run for hours or days.

### Lazy vs. Eager Spawning

[](#lazy-vs-eager-spawning)

By default, pools use **eager spawning**: all workers are spawned together when the pool manager is initialized. However, the pool manager itself is initialized lazily on the first `run()` call. This means even with eager spawning (the default), the first task submitted to the pool will incur the full cost of booting every worker process unless `boot()` is called explicitly beforehand.

#### Pre-warming with `boot()`

[](#pre-warming-with-boot)

The `boot()` method pre-warms the pool and guarantees that every worker is genuinely ready to accept tasks before it returns. It is **context-aware**: if called inside an async fiber, it suspends the fiber without blocking the event loop; if called in a synchronous script, it blocks execution and pumps the event loop until the pool is ready.

Once `boot()` returns:

- **Zero Spawn Latency:** Every worker has completed its bootstrap phase (autoloader, framework initialization, etc.). The first `run()` call will dispatch to an idle worker instantly.
- **Accurate PIDs:** `getWorkerPids()` is guaranteed to return the real PHP process PIDs (from `getmypid()` inside the worker), not temporary shell wrapper PIDs.
- **Idempotency:** Calling `boot()` multiple times is a safe no-op.

**Synchronous Usage:**

```
$pool = Parallel::pool(size: 4)->boot();
// Blocks until all 4 workers send their READY frame.

$result = await($pool->run(fn() => heavyWork()));
```

**Asynchronous Usage:**

```
async(function() use ($pool) {
    $pool->boot(); // Suspends this fiber non-blocking
    $result = await($pool->run(fn() => heavyWork()));
});
```

#### Lazy Spawning

[](#lazy-spawning)

Use **lazy spawning** when the pool is conditional or short-lived and workers may never be needed. Workers are spawned one-by-one only as tasks are submitted.

```
$pool = Parallel::pool(size: 4)->withLazySpawning();
```

Calling `boot()` on a lazy pool forces the manager to initialize, but workers still spawn individually on each `run()` call. For lazy pools, `boot()` returns immediately since there is no pre-boot phase.

**The trade-off:** The first batch of tasks will incur worker boot latency (~50–100ms per worker), and bootstrap errors (like syntax errors in your bootstrap file) will surface during task submission rather than during pool setup.

---

Self-Healing &amp; Supervisor Pattern
-------------------------------------

[](#self-healing--supervisor-pattern)

Build Erlang-style supervised clusters. If a worker crashes for any reason (segfault, out-of-memory kill, or an explicit `exit()`) Hibla detects the death, immediately spawns a replacement worker to maintain pool capacity, and fires the `onWorkerRespawn` hook so you can re-submit whatever that worker was doing.

**`onWorkerRespawn` fires on both crashes and planned retirements.**

When a worker reaches its `withMaxExecutionsPerWorker()` limit it exits cleanly with a RETIRING frame rather than a crash signal, but the pool treats both events identically from the caller's perspective: a replacement worker is spawned and `onWorkerRespawn` is called. This means any long-running task that was running on a retiring worker needs to be re-submitted in the hook just as it would after a crash. If you use `withMaxExecutionsPerWorker()` without an `onWorkerRespawn` handler, long-running tasks submitted to that worker will not be re-submitted on retirement.

The pattern is three lines:

```
use Hibla\Parallel\Parallel;
use Hibla\Parallel\Interfaces\ProcessPoolInterface;

$pool = Parallel::pool(size: 4)
    ->withoutTimeout() // long-running workers must not have a timeout
    ->onWorkerRespawn(function (ProcessPoolInterface $pool) use ($serverTask) {
        // Fired every time a worker exits, whether from a crash or a planned
        // retirement via withMaxExecutionsPerWorker(). Re-submit your
        // long-running task to the replacement worker here.
        $pool->run($serverTask);
    });
```

### Complete example: chaos server

[](#complete-example-chaos-server)

The following example makes the supervisor behavior directly observable. Each worker binds to port 8080 via `SO_REUSEPORT` and serves HTTP requests. The `/crash` route lets you trigger a controlled worker crash from the browser and watch the master detect and recover from it in the terminal in real time.

```
