--- url: https://true-async.github.io/en/architecture.md description: >- Internal design of TrueAsync components -- resource pool, PDO Pool, diagrams, and C API. --- ## Overview The architecture section describes the internal design of key TrueAsync components at the C-code level: data structures, algorithms, integration with Zend Engine, and interaction between the PHP core and the async extension. These materials are intended for developers who want to understand how TrueAsync works "under the hood" or plan to create their own extensions. ### [TrueAsync ABI](/en/architecture/zend-async-api.html) The heart of the asynchronous ABI: function pointers, extension registration system, global state (`zend_async_globals_t`), `ZEND_ASYNC_*` macros, and API versioning. ### [Coroutines, Scheduler, and Reactor](/en/architecture/scheduler-reactor.html) Internal design of the coroutine scheduler and event reactor: queues (circular buffers), context switching via fiber, microtasks, libuv event loop, fiber context pool, and graceful shutdown. ### [Events and the Event Model](/en/architecture/events.html) `zend_async_event_t` -- the base data structure from which all asynchronous primitives inherit. Callback system, ref-counting, event reference, flags, event type hierarchy. ### [Waker -- Wait and Wake-up Mechanism](/en/architecture/waker.html) Waker is the link between a coroutine and events. Statuses, `resume_when`, coroutine callbacks, error delivery, `zend_coroutine_t` structure, and switch handlers. ### [Garbage Collection in Asynchronous Context](/en/architecture/async-gc.html) How PHP GC works with coroutines, scope, and contexts: `get_gc` handlers, fiber stack traversal, zombie coroutines, hierarchical context, and protection against circular references. ## Components ### [Async\Pool](/en/architecture/pool.html) Universal resource pool. Covered topics: * Two-level data structure (ABI in the core + internal in the extension) * Acquire/release algorithms with a FIFO queue of waiting coroutines * Healthcheck via periodic timer * Circuit Breaker with three states * C API for extensions (`ZEND_ASYNC_POOL_*` macros) ### [PDO Pool](/en/architecture/pdo-pool.html) PDO-specific layer on top of `Async\Pool`. Covered topics: * Template connection and deferred creation of real connections * Binding connections to coroutines via HashTable * Pinning during active transactions and statements * Automatic rollback and cleanup on coroutine completion * Credentials management in the factory --- --- url: https://true-async.github.io/en/docs/reference/cpu-snapshot.md description: >- Async\CpuSnapshot — immutable snapshot of process and system CPU counters. Low-level source for custom delta calculations. --- # Async\CpuSnapshot (PHP 8.6+, True Async 1.0) `Async\CpuSnapshot` is an immutable point-in-time snapshot of process and system CPU counters. ## When to use it The high-level wrapper [`Async\cpu_usage()`](/en/docs/reference/cpu-usage.html) keeps a single internal snapshot per process and computes the delta automatically. That is enough for most telemetry tasks. `CpuSnapshot` is needed when: * multiple independent telemetry consumers want to compute their own deltas independently; * you need to keep the raw counters (for a log, dump, or transfer to another system); * you want to compute derived metrics beyond process/system. ## Class overview ```php namespace Async; final class CpuSnapshot { public readonly int $wallNs; public readonly int $processUserNs; public readonly int $processSystemNs; public readonly int $systemIdleNs; public readonly int $systemBusyNs; public readonly int $cpuCount; public static function now(): CpuSnapshot; } ``` All time-valued fields are monotonically increasing nanosecond counters with an implementation-defined origin. **A single value carries no meaning by itself** — compute deltas between two snapshots taken at different times. Cross-platform: identical fields and semantics on Linux and Windows. ## Fields | Field | Type | Description | |-------|------|-------------| | `wallNs` | `int` | Monotonic wall-clock time at capture. | | `processUserNs` | `int` | Aggregate user-mode CPU time across all process threads. | | `processSystemNs` | `int` | Aggregate kernel-mode CPU time across all process threads. | | `systemIdleNs` | `int` | Aggregate idle time across all logical CPUs on the host. | | `systemBusyNs` | `int` | Aggregate non-idle time across all logical CPUs on the host (`user + system + nice + irq + softirq + steal`). | | `cpuCount` | `int` | Number of logical CPUs visible to the OS at capture. | > **Inside containers** `systemIdleNs` / `systemBusyNs` reflect the **host**, not the cgroup. For > per-process backpressure prefer `process*` fields — they automatically honour affinity and > cgroup CPU throttling. ## Methods ### now (static) ```php public static CpuSnapshot::now(): CpuSnapshot ``` Takes a fresh snapshot. ## Examples ### Example #1 Manual delta computation ```php wallNs - $prev->wallNs; $user = $now->processUserNs - $prev->processUserNs; $sys = $now->processSystemNs - $prev->processSystemNs; // How many cores user + kernel time occupied over the interval. $processCores = ($user + $sys) / $wall; printf( "Process used on average %.3f cores over the last second\n", $processCores ); }); ``` ### Example #2 Two independent consumers ```php prev === null) { $this->prev = $now; return ['process_cores' => 0.0]; } $wall = $now->wallNs - $this->prev->wallNs; $cpu = ($now->processUserNs - $this->prev->processUserNs) + ($now->processSystemNs - $this->prev->processSystemNs); $this->prev = $now; return ['process_cores' => $wall > 0 ? $cpu / $wall : 0.0]; } } // Two instances — two independent measurement series. $apiMetrics = new TelemetryReporter(); $workerMetrics = new TelemetryReporter(); ``` ## Notes > The class is **immutable** and **not serializable** (`@strict-properties`, > `@not-serializable`). The constructor is private — instances are created only via > `CpuSnapshot::now()`. ## See also * [Async\cpu\_usage()](/en/docs/reference/cpu-usage.html) — ready-made delta with percentages * [Async\loadavg()](/en/docs/reference/loadavg.html) — 1/5/15-minute load average * [Async\available\_parallelism()](/en/docs/reference/available-parallelism.html) — number of available CPUs --- --- url: https://true-async.github.io/en/architecture/pool.md description: >- Internal design of the universal resource pool Async\Pool -- data structures, acquire/release algorithms, healthcheck, circuit breaker. --- # Async\Pool Architecture > This article describes the internal design of the universal resource pool. > If you are looking for a usage guide, see [Async\Pool](/en/docs/components/pool.html). > For the PDO-specific layer, see [PDO Pool Architecture](/en/architecture/pdo-pool.html). ## Data Structure The pool is implemented in two layers: a public ABI structure in the PHP core and an extended internal structure in the async extension. ![Pool Data Structures](/diagrams/en/architecture-pool/data-structures.svg) ## Two Creation Paths A pool can be created from PHP code (via the `Async\Pool` constructor) or from a C extension (via the internal API). | Path | Function | Callbacks | Used by | |-------|-------------------------------------|--------------------------------|------------------------| | PHP | `zend_async_pool_create()` | `zend_fcall_t*` (PHP callable) | User code | | C API | `zend_async_pool_create_internal()` | function pointers | PDO, other extensions | The difference is in `handler_flags`. When the flag is set, the pool calls the C function directly, bypassing the overhead of calling a PHP callable through `zend_call_function()`. ## Acquire: Obtaining a Resource ![acquire() -- Internal Algorithm](/diagrams/en/architecture-pool/acquire.svg) ### Waiting for a Resource When all resources are busy and `max_size` is reached, the coroutine suspends via `ZEND_ASYNC_SUSPEND()`. The waiting mechanism is similar to channels: 1. A `zend_async_pool_waiter_t` structure is created 2. The waiter is added to the FIFO `waiters` queue 3. A callback for wake-up is registered 4. If a timeout is set -- a timer is registered 5. `ZEND_ASYNC_SUSPEND()` -- the coroutine yields control Wake-up occurs when another coroutine calls `release()`. ## Release: Returning a Resource ![release() -- Internal Algorithm](/diagrams/en/architecture-pool/release.svg) ## Healthcheck: Background Monitoring If `healthcheckInterval > 0`, a periodic timer is started when the pool is created. The timer is integrated with the reactor via `ZEND_ASYNC_NEW_TIMER_EVENT`. ![Healthcheck -- Periodic Check](/diagrams/en/architecture-pool/healthcheck.svg) The healthcheck verifies **only** free resources. Busy resources are not affected. If, after removing dead resources, the total count drops below `min`, the pool creates replacements. ## Circular Buffer Free resources are stored in a circular buffer -- a ring buffer with fixed capacity. The initial capacity is 8 elements, expanded as needed. `push` and `pop` operations run in O(1). The buffer uses two pointers (`head` and `tail`), enabling efficient addition and extraction of resources without moving elements. ## Integration with the Event System The pool inherits from `zend_async_event_t` and implements a full set of event handlers: | Handler | Purpose | |----------------|------------------------------------------------------------| | `add_callback` | Register a callback (for waiters) | | `del_callback` | Remove a callback | | `start` | Start the event (NOP) | | `stop` | Stop the event (NOP) | | `dispose` | Full cleanup: free memory, destroy callbacks | This enables: * Suspending and resuming coroutines via event callbacks * Integrating the healthcheck timer with the reactor * Properly releasing resources through event disposal ## Garbage Collection The PHP pool wrapper (`async_pool_obj_t`) implements a custom `get_gc` that registers all resources from the idle buffer as GC roots. This prevents premature garbage collection of free resources that have no explicit references from PHP code. ## Circuit Breaker The pool implements the `CircuitBreaker` interface with three states: ![Circuit Breaker States](/diagrams/en/architecture-pool/circuit-breaker.svg) Transitions can be manual or automatic via `CircuitBreakerStrategy`: * `reportSuccess()` is called on a successful `release` (resource passed `beforeRelease`) * `reportFailure()` is called when `beforeRelease` returned `false` * The strategy decides when to switch states ## Close: Shutting Down the Pool When the pool is closed: 1. The pool event is marked as CLOSED 2. The healthcheck timer is stopped 3. All waiting coroutines are woken up with a `PoolException` 4. All free resources are destroyed via `destructor` 5. Busy resources continue to live -- they will be destroyed upon `release` ## C API for Extensions Extensions (PDO, Redis, etc.) use the pool through macros: | Macro | Function | |--------------------------------------------------|------------------------------| | `ZEND_ASYNC_NEW_POOL(...)` | Create pool with C callbacks | | `ZEND_ASYNC_NEW_POOL_OBJ(pool)` | Create PHP wrapper for pool | | `ZEND_ASYNC_POOL_ACQUIRE(pool, result, timeout)` | Acquire a resource | | `ZEND_ASYNC_POOL_RELEASE(pool, resource)` | Release a resource | | `ZEND_ASYNC_POOL_CLOSE(pool)` | Close the pool | All macros call function pointers registered by the async extension at load time. This ensures isolation: the PHP core does not depend on the specific pool implementation. ## Sequence: Full Acquire-Release Cycle ![Full acquire -> use -> release Cycle](/diagrams/en/architecture-pool/full-cycle.svg) ## What's Next? * [Async\Pool: Guide](/en/docs/components/pool.html) -- how to use the pool * [PDO Pool Architecture](/en/architecture/pdo-pool.html) -- PDO-specific layer * [Coroutines](/en/docs/components/coroutines.html) -- how coroutines work --- --- url: https://true-async.github.io/en/docs/components/pool.md description: >- Async\Pool -- universal resource pool for coroutines: creation, acquire/release, healthcheck, circuit breaker. --- # Async\Pool: Universal Resource Pool ## Why You Need a Pool When working with coroutines, the problem of sharing I/O descriptors arises. If the same socket is used by two coroutines that simultaneously write or read different packets from it, the data will get mixed up and the result will be unpredictable. Therefore, you cannot simply use the same `PDO` object in different coroutines! On the other hand, creating a separate connection for each coroutine over and over is a very wasteful strategy. It negates the advantages of concurrent I/O. Therefore, connection pools are typically used for interacting with external APIs, databases, and other resources. A pool solves this problem: resources are created in advance, given to coroutines on request, and returned for reuse. ```php use Async\Pool; // HTTP connection pool $pool = new Pool( factory: fn() => new HttpConnection('api.example.com'), destructor: fn($conn) => $conn->close(), min: 2, max: 10, ); // A coroutine takes a connection, uses it, and returns it $conn = $pool->acquire(); $response = $conn->request('GET', '/users'); $pool->release($conn); ``` ## Creating a Pool ```php $pool = new Pool( factory: fn() => createResource(), // How to create a resource destructor: fn($r) => $r->close(), // How to destroy a resource healthcheck: fn($r) => $r->ping(), // Is the resource alive? beforeAcquire: fn($r) => $r->isValid(), // Check before giving out beforeRelease: fn($r) => !$r->isBroken(), // Check before returning min: 2, // Pre-create 2 resources max: 10, // Maximum 10 resources healthcheckInterval: 30000, // Check every 30 sec ); ``` | Parameter | Purpose | Default | |------------------------|----------------------------------------------------------------|---------| | `factory` | Creates a new resource. **Required** | -- | | `destructor` | Destroys a resource when removed from the pool | `null` | | `healthcheck` | Periodic check: is the resource still alive? | `null` | | `beforeAcquire` | Check before giving out. `false` -- destroy and take the next | `null` | | `beforeRelease` | Check before returning. `false` -- destroy, don't return | `null` | | `min` | How many resources to create in advance (pre-warming) | `0` | | `max` | Maximum resources (free + in use) | `10` | | `healthcheckInterval` | Background health check interval (ms, 0 = disabled) | `0` | ## Acquire and Release ### Blocking Acquire ```php // Wait until a resource becomes available (indefinitely) $resource = $pool->acquire(); // Wait at most 5 seconds $resource = $pool->acquire(timeout: 5000); ``` If the pool is full (all resources are in use and `max` is reached), the coroutine **suspends** and waits until another coroutine returns a resource. Other coroutines continue running. On timeout, `Async\TimeoutException` is thrown, not `PoolException`. A timeout is not a pool failure: the pool is healthy, it is merely busy, and your own deadline expired. `PoolException` means the pool itself is unusable (closed, never initialised, factory failed). Note that `TimeoutException` extends `Exception`, not `PoolException`, so `catch (PoolException)` will **not** catch a timeout. ```php try { $resource = $pool->acquire(timeout: 5000); } catch (Async\TimeoutException $e) { // the resource did not free up in 5 seconds -- retry or back off } catch (Async\PoolException $e) { // the pool is closed -- retrying is pointless } ``` ### Non-blocking tryAcquire ```php $resource = $pool->tryAcquire(); if ($resource === null) { echo "All resources are busy, let's try later\n"; } else { // Use the resource $pool->release($resource); } ``` `tryAcquire()` returns `null` immediately if a resource is unavailable. The coroutine is not suspended. ### Release ```php $resource = $pool->acquire(); try { doWork($resource); } finally { // IMPORTANT: always return the resource to the pool! $pool->release($resource); } ``` If `beforeRelease` is set and returns `false`, the resource is considered damaged and is destroyed instead of being returned to the pool. ## Statistics ```php echo $pool->count(); // Total resources (free + in use) echo $pool->idleCount(); // Free, ready to be given out echo $pool->activeCount(); // Currently being used by coroutines ``` ## Closing the Pool ```php $pool->close(); ``` On closing: * All waiting coroutines receive a `PoolException` * All free resources are destroyed via `destructor` * Busy resources are destroyed upon subsequent `release` ## Healthcheck: Background Checking If `healthcheckInterval` is set, the pool periodically checks free resources. Dead resources are destroyed and replaced with new ones (if the count has dropped below `min`). ```php $pool = new Pool( factory: fn() => new DatabaseConnection($dsn), destructor: fn($conn) => $conn->close(), healthcheck: fn($conn) => $conn->ping(), // Check: is the connection alive? min: 3, max: 10, healthcheckInterval: 10000, // Every 10 seconds ); ``` Healthcheck works **only** for free resources. Busy resources are not checked. ## Circuit Breaker The pool implements the **Circuit Breaker** pattern for managing service availability. ### Three States | State | Behavior | |--------------|-------------------------------------------------------| | `ACTIVE` | Everything works, requests go through | | `INACTIVE` | Service unavailable, `acquire()` throws an exception | | `RECOVERING` | Test mode, limited requests | ```php use Async\CircuitBreakerState; // Check state $state = $pool->getState(); // CircuitBreakerState::ACTIVE // Manual control $pool->deactivate(); // Switch to INACTIVE $pool->recover(); // Switch to RECOVERING $pool->activate(); // Switch to ACTIVE ``` ### Automatic Management via Strategy ```php use Async\CircuitBreakerStrategy; class MyStrategy implements CircuitBreakerStrategy { private int $failures = 0; private int $openedAt = 0; public function reportSuccess(mixed $source): void { $this->failures = 0; $source->activate(); } public function reportFailure(mixed $source, \Throwable $error): void { $this->failures++; if ($this->failures >= 5) { $this->openedAt = time(); $source->deactivate(); } } public function shouldRecover(): bool { return time() - $this->openedAt >= 30; } } $pool->setCircuitBreakerStrategy(new MyStrategy()); ``` The strategy is called automatically: * `reportSuccess()` -- on successful resource return to the pool * `reportFailure()` -- when `beforeRelease` returns `false` (resource is damaged) ## Resource Lifecycle ![Resource Lifecycle](/diagrams/en/components-pool/resource-lifecycle.svg) ## Real-World Example: Redis Connection Pool ```php use Async\Pool; use function Async\spawn; use function Async\await; $redis = new Pool( factory: function() { $conn = new Redis(); $conn->connect('127.0.0.1', 6379); return $conn; }, destructor: fn($conn) => $conn->close(), healthcheck: fn($conn) => $conn->ping(), min: 2, max: 20, healthcheckInterval: 15000, ); // 100 coroutines concurrently read from Redis through 20 connections $coroutines = []; for ($i = 0; $i < 100; $i++) { $coroutines[] = spawn(function() use ($redis, $i) { $conn = $redis->acquire(timeout: 3000); try { return $conn->get("key:$i"); } finally { $redis->release($conn); } }); } $results = array_map(fn($c) => await($c), $coroutines); $redis->close(); ``` ## PDO Pool For PDO, there is a built-in integration with `Async\Pool` that makes pooling completely transparent. Instead of manual `acquire`/`release`, the pool is managed automatically behind the scenes. Learn more: [PDO Pool](/en/docs/components/pdo-pool.html) ## What's Next? * [Async\Pool Architecture](/en/architecture/pool.html) -- internals, diagrams, C API * [PDO Pool](/en/docs/components/pdo-pool.html) -- transparent pool for PDO * [Coroutines](/en/docs/components/coroutines.html) -- how coroutines work * [Channels](/en/docs/components/channels.html) -- data exchange between coroutines --- --- url: https://true-async.github.io/en/docs/components/threads.md description: >- Async\Thread — running code in a separate parallel thread: data transfer, WeakReference/WeakMap, ThreadChannel, Future between threads. --- # Async\Thread: running PHP in a separate thread ## Why threads are needed Coroutines solve the concurrency problem for **I/O-bound** workloads — a single process can handle thousands of concurrent network or disk waits. But coroutines have a limitation: they all run **in the same PHP process** and take turns receiving control from the scheduler. If a task is **CPU-bound** — compression, parsing, cryptography, heavy computation — a single such coroutine will block the scheduler, and all other coroutines will stall until it finishes. Threads solve this limitation. `Async\Thread` runs a closure in a **separate parallel thread** with its **own isolated PHP runtime**: its own set of variables, its own autoloader, its own classes and functions. Nothing is shared directly between threads — any data is passed **by value**, through deep copying. ```php compute()); // Waiting for the result. The calling coroutine waits; others continue running $result = await($thread); // Or a non-blocking check if ($thread->isCompleted()) { $result = $thread->getResult(); } ``` `Async\Thread` implements the `Completable` interface, so it can be passed to `await()`, `await_all()`, `await_any()`, and `Task\Group` — exactly like a regular coroutine. ### States | Method | What it checks | |-------------------|-------------------------------------------------------------| | `isRunning()` | The thread is still executing | | `isCompleted()` | The thread has finished (successfully or with an exception) | | `isCancelled()` | The thread was cancelled | | `getResult()` | The result if it finished successfully; otherwise `null` | | `getException()` | The exception if it finished with an error; otherwise `null`| ### Exception handling An exception thrown inside a thread is caught and delivered to the parent wrapped in `Async\RemoteException`: ```php getRemoteClass(), "\n"; $original = $e->getRemoteException(); if ($original !== null) { echo "original: ", $original->getMessage(), "\n"; } } }); ``` ``` remote class: RuntimeException original: boom ``` `getRemoteException()` may return `null` if the exception class could not be loaded in the parent thread (for example, it is a user-defined class that exists only in the receiving thread). ## Data transfer between threads This is the most important part of the model. **Everything is transferred by copying** — no shared references. ### What can be transferred | Type | Behavior | |---------------------------------------------------------|-----------------------------------------------------------------| | Scalars (`int`, `float`, `string`, `bool`, `null`) | Copied | | Arrays | Deep copy; nested objects preserve identity | | Objects with declared properties (`public $x`, etc.) | Deep copy; re-created from scratch on the receiving side | | `Closure` | The function body is transferred along with all `use(...)` vars | | `WeakReference` | Transferred together with the referent (see below) | | `WeakMap` | Transferred with all keys and values (see below) | | `Async\FutureState` | Once only, to write a result from the thread (see below) | ### What cannot be transferred | Type | Why | |--------------------------------------------------------|----------------------------------------------------------------------------------| | `stdClass` and any objects with dynamic properties | Dynamic properties have no class-level declaration and cannot be correctly recreated in the receiving thread | | PHP references (`&$var`) | A shared reference between threads contradicts the model | | Resources (`resource`) | File descriptors, curl handles, sockets are bound to a specific thread | Attempting to transfer any of these will immediately throw `Async\ThreadTransferException` in the source: ```php x = 1; try { $thread = spawn_thread(function() use ($obj) { return 'unreachable'; }); await($thread); } catch (Async\ThreadTransferException $e) { echo $e->getMessage(), "\n"; } }); ``` ``` Cannot transfer object with dynamic properties between threads (class stdClass). Use arrays instead ``` ### Object identity is preserved The same object referenced multiple times in a data graph is **created only once in the receiving thread**, and all references point to it. Within a single transfer operation (all variables from `use(...)` of one closure, one channel send, one thread result) identity is preserved: ```php $obj]; $thread = spawn_thread(function() use ($obj, $meta) { // The same instance in two different variables echo "same: ", ($obj === $meta['ref'] ? "yes" : "no"), "\n"; // A mutation via one reference is visible through the other $obj->name = 'staging'; echo "meta: ", $meta['ref']->name, "\n"; return 'ok'; }, bootloader: $boot); echo await($thread), "\n"; }); ``` ``` same: yes meta: staging ok ``` The same applies to linked objects within a single graph: an array with references to shared nested objects will preserve identity after transfer. ### Cycles A graph with a cycle through regular objects can be transferred. The limitation is that very deeply nested cycles may hit the internal transfer depth limit (hundreds of levels). In practice, this almost never occurs. Cycles of the form `$node->weakParent = WeakReference::create($node)` — that is, an object that references itself via a `WeakReference` — currently run into the same limit, so it is better not to use them within a single transferred graph. ## WeakReference across threads `WeakReference` has special transfer logic. The behavior depends on what else is transferred alongside it. ### Referent is also transferred — identity is preserved If the object itself is transferred together with the `WeakReference` (directly, inside an array, or as a property of another object), then on the receiving side `$wr->get()` returns **exactly that** instance that ended up in the other references: ```php get() === $obj ? "yes" : "no"), "\n"; return 'ok'; }, bootloader: $boot); await($thread); }); ``` ``` wr === obj: yes ``` ### Referent is not transferred — WeakReference becomes dead If only the `WeakReference` is transferred but not the object itself, then in the receiving thread no one holds a strong reference to that object. By PHP's rules this means the object is immediately destroyed and the `WeakReference` becomes **dead** (`$wr->get() === null`). This is exactly the same behavior as in single-threaded PHP: without a strong owner, the object is collected. ```php spawn(function() use ($boot) { $obj = new Config('prod'); $wr = WeakReference::create($obj); $thread = spawn_thread(function() use ($wr) { // $obj is NOT transferred echo "dead: ", ($wr->get() === null ? "yes" : "no"), "\n"; return 'ok'; }, bootloader: $boot); await($thread); }); ``` ``` dead: yes ``` ### Source is already dead If the `WeakReference` was already dead in the source at the time of transfer (`$wr->get() === null`), it will arrive in the receiving thread dead as well. ### Singleton `WeakReference::create($obj)` returns a singleton: two calls for the same object yield **the same** `WeakReference` instance. This property is preserved during transfer — in the receiving thread there will also be exactly one `WeakReference` instance per object. ## WeakMap across threads `WeakMap` is transferred with all its entries. But the same rule applies as in single-threaded PHP: **a `WeakMap` key lives only as long as someone holds a strong reference to it**. ### Keys are in the graph — entries survive If the keys are transferred separately (or are reachable through other transferred objects), the `WeakMap` in the receiving thread contains all entries: ```php complete($data); }); // The parent waits through its own Future — the event arrives here // when the thread calls $state->complete() $result = await($future); echo "got: ", $result, "\n"; await($thread); echo "thread done\n"; }); ``` ``` got: computed in thread thread done ``` **Important constraints:** 1. `FutureState` can be transferred to **only one** thread. A second transfer attempt will throw an exception. 2. Transferring the `Future` itself is not allowed — it belongs to the parent thread and can only wake its own owner. 3. After `FutureState` is transferred, the original object in the parent remains valid: when the thread calls `complete()`, that change becomes visible through the `Future` in the parent — `await($future)` unblocks. This is the only standard way to deliver a **single result** from a thread back to the caller, outside of the ordinary `return` from `spawn_thread()`. If you need to stream many values, use `ThreadChannel`. ## Bootloader: preparing the thread environment A thread has **its own environment** and does not inherit class, function, or constant definitions declared in the parent script. If a closure uses a user-defined class, that class must either be re-declared or loaded through autoload — for this there is the `bootloader` parameter: ```php $thread = spawn_thread( task: function() { $config = new Config('prod'); // Config must exist in the thread return $config->name; }, bootloader: function() { // Executed in the receiving thread BEFORE the main closure require_once __DIR__ . '/src/autoload.php'; }, ); ``` The bootloader is guaranteed to run in the receiving thread before the `use(...)` variables are loaded and before the main closure is called. Typical bootloader tasks: registering autoload, declaring classes via `eval`, setting ini options, loading libraries. ## Edge cases ### Superglobals `$_GET`, `$_POST`, `$_SERVER`, `$_ENV` are their own in the thread — they are initialized fresh, as in a new request. In the current version of TrueAsync, populating them in receiving threads is temporarily disabled (planned to be enabled later) — watch the CHANGELOG. ### Static function variables Each thread has its own set of static function and class variables. Changes in one thread are not visible to others — this is part of the general isolation. ### Opcache Opcache shares its compiled bytecode cache between threads as read-only: scripts are compiled once for the entire process, and each new thread reuses the ready bytecode. This makes thread startup faster. ## See also * [`spawn_thread()`](/en/docs/reference/spawn-thread.html) — running a closure in a thread * [`Async\ThreadChannel`](/en/docs/components/thread-channels.html) — channels between threads * [`await()`](/en/docs/reference/await.html) — waiting for a thread result * [`Async\RemoteException`](/en/docs/components/exceptions.html) — wrapper for receiving-thread errors --- --- url: https://true-async.github.io/en/docs/components/thread-channels.md description: >- Async\ThreadChannel — a thread-safe channel for passing data between OS threads in TrueAsync. --- # Async\ThreadChannel: channels between OS threads ## How it differs from a regular Channel `Async\Channel` works **within a single thread** — between coroutines of the same scheduler. Its data lives in **thread-local memory**, and safety is guaranteed by the fact that only one coroutine accesses the channel at a time. `Async\ThreadChannel` is designed for passing data **between OS threads**. The channel buffer lives in **shared memory** accessible to all threads, not in the memory of any single thread. Each sent value is deep-copied into that shared memory, and on the receiver side — back into the thread's local memory. Synchronization is via a thread-safe mutex, so `send()` and `recv()` can be called from different OS threads concurrently. | Property | `Async\Channel` | `Async\ThreadChannel` | |-----------------------------------|----------------------------------------|----------------------------------------------| | Scope | Single OS thread | Between OS threads | | Where buffered data lives | Thread-local memory | Shared memory visible to all threads | | Synchronization | Coroutine scheduler (cooperative) | Mutex (thread-safe) | | Rendezvous (capacity=0) | Supported | No — always buffered | | Minimum capacity | 0 | 1 | If everything runs in a single thread — use `Async\Channel`, it's lighter. `ThreadChannel` makes sense only when you genuinely need data exchange between OS threads. ## Creating a channel ```php use Async\ThreadChannel; $ch = new ThreadChannel(capacity: 16); ``` **`capacity`** — buffer size (minimum `1`). Larger values better absorb bursty producers, but consume more memory for the live queue. ## Basic example: producer + consumer ```php send("item-$i"); } $ch->close(); }); // Consumer — in the main thread (a coroutine) try { while (true) { $msg = $ch->recv(); echo "got: ", $msg, "\n"; } } catch (Async\ThreadChannelException $e) { echo "channel closed\n"; } await($producer); }); ``` ``` got: item-1 got: item-2 got: item-3 got: item-4 got: item-5 channel closed ``` The producer writes to the channel from a separate thread; the main thread reads via `recv()` — nothing special, it looks just like a regular `Channel`. ## send / recv ### `send($value[, $cancellation])` Sends a value into the channel. If the buffer is full — **suspends the current coroutine** (cooperative suspension — other coroutines in this scheduler keep running) until another thread frees space. The value is **deep-copied into the channel's shared memory** following the same rules as variables captured via `use(...)` in `spawn_thread()`. Objects with dynamic properties, PHP references, and resources are rejected with `Async\ThreadTransferException`. ```php $ch->send(['user' => 'alice', 'id' => 42]); // array $ch->send(new Point(3, 4)); // object with declared props $ch->send($futureState); // Async\FutureState (once!) ``` If the channel is already closed — `send()` throws `Async\ThreadChannelException`. ### `recv([$cancellation])` Reads a value from the channel. If the buffer is empty — suspends the current coroutine until data arrives **or** the channel is closed. * If data arrives — returns the value. * If the channel is closed and the buffer is empty — throws `Async\ThreadChannelException`. * If the channel is closed but the buffer still has items — **drains the remaining data first**, only throwing `ThreadChannelException` once the buffer is empty. This allows correctly draining a channel after it is closed. ## Channel state ```php capacity(), "\n"; echo "empty: ", ($ch->isEmpty() ? "yes" : "no"), "\n"; $ch->send('a'); $ch->send('b'); echo "count after 2 sends: ", count($ch), "\n"; echo "full: ", ($ch->isFull() ? "yes" : "no"), "\n"; $ch->send('c'); echo "full after 3: ", ($ch->isFull() ? "yes" : "no"), "\n"; $got = []; while (!$ch->isEmpty()) { $got[] = $ch->recv(); } echo "drained: ", implode(',', $got), "\n"; $ch->close(); echo "closed: ", ($ch->isClosed() ? "yes" : "no"), "\n"; }); ``` ``` capacity: 3 empty: yes count after 2 sends: 2 full: no full after 3: yes drained: a,b,c closed: yes ``` | Method | Returns | |----------------|-----------------------------------------------| | `capacity()` | Buffer size set in the constructor | | `count()` | Current number of messages in the buffer | | `isEmpty()` | `true` if the buffer is empty | | `isFull()` | `true` if the buffer is filled to capacity | | `isClosed()` | `true` if the channel has been closed | `ThreadChannel` implements `Countable`, so `count($ch)` works. ## close() ```php $ch->close(); ``` After closing: * `send()` immediately throws `Async\ThreadChannelException`. * `recv()` **drains remaining values**, then starts throwing `ThreadChannelException`. * All coroutines/threads suspended in `send()` or `recv()` are **woken** with `ThreadChannelException`. A channel can only be closed once. A repeated call is a safe no-op. ## Pattern: worker pool Two channels — one for jobs, one for results. Worker threads read jobs from the first and put results into the second. ```php recv(); // Simulate CPU load $x = 0; for ($k = 0; $k < 2_000_000; $k++) { $x += sqrt($k); } $results->send(['worker' => $i, 'n' => $n]); } } catch (Async\ThreadChannelException $e) { // jobs channel closed — worker exits } }); } // Dispatch 6 jobs for ($n = 1; $n <= 6; $n++) { $jobs->send($n); } $jobs->close(); // Wait for all worker threads to finish foreach ($workers as $w) { await($w); } $results->close(); // Drain results $by = []; while (!$results->isEmpty()) { $r = $results->recv(); $by[$r['worker']] = ($by[$r['worker']] ?? 0) + 1; } ksort($by); foreach ($by as $w => $n) { echo "worker-$w processed $n\n"; } }); ``` ``` worker-1 processed 2 worker-2 processed 2 worker-3 processed 2 ``` Each worker handled 2 jobs — the load was distributed across three threads. ### Note on distribution If the producer writes to the channel faster than the workers read (or if the workers spend almost no CPU time), **the first worker may grab all jobs** immediately, because its `recv()` wakes up first and picks up the next message before the other workers reach their `recv()`. This is normal behavior for a concurrent queue — fair scheduling is not guaranteed. If strict uniformity is required — partition tasks upfront (shard-by-hash), or give each worker its own dedicated channel. ## Passing complex data through the channel `ThreadChannel` can carry anything that cross-thread data transfer supports (see [Passing data between threads](/en/docs/components/threads.html#passing-data-between-threads)): * scalars, arrays, objects with declared properties * `Closure` (closures) * `WeakReference` and `WeakMap` (with the same strong-owner rules as in `spawn_thread`) * `Async\FutureState` (once) Each `send()` call is an independent operation with its own identity table. **Identity is preserved within a single message**, but not across separate `send()` calls. If you want two receivers to see "the same" object — send it once inside an array, not as two separate messages. ## Limitations * **Minimum capacity is 1.** Rendezvous (capacity=0) is not supported, unlike `Async\Channel`. * **`ThreadChannel` does not support serialization.** Channel objects cannot be saved to a file or sent over the network — a channel exists only within a live process. * **A channel handle can be passed** via `spawn_thread` or nested inside another channel — the object handle for `ThreadChannel` transfers correctly, and both sides see the same internal buffer. ## See also * [`Async\Thread`](/en/docs/components/threads.html) — OS threads in TrueAsync * [`spawn_thread()`](/en/docs/reference/spawn-thread.html) — start a closure in a new thread * [`Async\Channel`](/en/docs/components/channels.html) — channels between coroutines in the same thread --- --- url: https://true-async.github.io/en/docs/components/thread-pool.md description: >- Async\ThreadPool — a pool of worker threads for parallel CPU-bound task execution in TrueAsync. --- # Async\ThreadPool: worker thread pool ## Why ThreadPool [`spawn_thread()`](/en/docs/reference/spawn-thread.html) solves the "one task — one thread" problem: launch a heavy computation, wait for the result, thread exits. This is convenient, but comes at a cost: **every thread launch is a full system call**. Initializing a separate PHP environment, loading Opcache bytecode, allocating a stack — all of this happens from scratch. With hundreds or thousands of such tasks, the overhead becomes noticeable. `Async\ThreadPool` solves this problem: at startup, a fixed set of **worker threads** (OS threads with their own PHP environment) is created, living for the entire lifetime of the program and **reused repeatedly** to execute tasks. Each `submit()` places a task into the queue, a free worker picks it up, executes it, and returns the result via [`Async\Future`](/en/docs/components/future.html). ```php submit(function() use ($i) { $sum = 0; for ($k = 0; $k < 1_000_000; $k++) { $sum += sqrt($k); } return ['task' => $i, 'sum' => (int) $sum]; }); } foreach ($futures as $f) { $result = await($f); echo "task {$result['task']}: {$result['sum']}\n"; } $pool->close(); }); ``` Eight tasks run in parallel across four workers. While the workers compute — the main program (other coroutines) continues running: `await($f)` suspends only the waiting coroutine, not the entire process. ## When to use ThreadPool vs spawn\_thread or coroutines | Scenario | Tool | |----------------------------------------------------------|--------------------------| | One heavy task, launched rarely | `spawn_thread()` | | Many short CPU tasks in a loop | `ThreadPool` | | A fixed thread that lives for the entire program | `ThreadPool` | | I/O: network, database, filesystem | Coroutines | | Task needed immediately, without a queue | `spawn_thread()` | **Key rule:** if tasks are many and short — a pool amortizes the thread startup cost. If there is one task launched once every few seconds — `spawn_thread()` is sufficient. A typical pool size equals the number of physical CPU cores (`nproc` on Linux, `sysconf(_SC_NPROCESSORS_ONLN)` in C). More workers than cores does not speed up CPU-bound workloads and only adds context-switching overhead. ## Creating a pool ```php // autodetect by available CPUs $pool = new ThreadPool(); // explicit worker count $pool = new ThreadPool(workers: 4); // + queue size $pool = new ThreadPool(workers: 4, queueSize: 64); // + bootloader, executed once in each worker $pool = new ThreadPool( workers: 4, queueSize: 64, bootloader: function () { require __DIR__ . '/vendor/autoload.php'; Database::warmupPool(); }, ); // + coroutine-mode: the task starts as a coroutine inside the per-worker pool scope $pool = new ThreadPool(workers: 4, coroutine: true); ``` | Parameter | Type | Purpose | Default | |----------------|---------------|----------------------------------------------------------------------|--------------------| | `$workers` | `int` | Number of worker threads. `0` — autodetect via `available_parallelism()` | `0` | | `$queueSize` | `int` | Maximum length of the pending task queue | `workers × 4` | | `$bootloader` | `?\Closure` | Per-worker startup hook (see below) | `null` | | `$coroutine` | `bool` | Run each task as a coroutine (see below) | `false` | | `$concurrency` | `int` | Limit of concurrent coroutines per worker (only with `coroutine: true`) | `0` (unlimited) | All worker threads start **immediately on pool creation**. This is a small "upfront" investment, but subsequent `submit()` calls carry no thread-startup overhead. `$queueSize` limits the size of the internal task queue. If the queue is full (all workers are busy and there are already `$queueSize` tasks in the queue), the next `submit()` **suspends the calling coroutine** until a worker becomes available. A value of zero means `workers × 4`. ### Autodetecting the worker count When `workers: 0` (or the parameter is omitted), the pool takes its size from [`Async\available_parallelism()`](/en/docs/reference/available-parallelism.html). That function honours `cgroup` quotas, affinity, and container limits — on a Kubernetes pod with `cpu.max=2` you get 2, not the physical core count of the host. ### Bootloader The `$bootloader` closure is deep-copied once and runs in **every** worker before the main task loop. It is the ideal place for autoload, connection-pool warmup, and opcache pre-compile — everything that would otherwise execute inside each `submit()`. If the bootloader throws, the entire pool is considered failed: the worker does not start and the error is raised in the parent. ### Coroutine mode With `coroutine: true`, every task runs **as a coroutine** in its own child scope, which is nested inside the worker's shared pool scope. Inside the task you can `await`, use `Channel`s, do I/O, and `spawn` — all without blocking the worker itself. ```php $pool = new ThreadPool(workers: 4, coroutine: true); $pool->submit(function () { // ordinary blocking calls in this mode correctly park the coroutine // rather than blocking the OS thread $rows = (new PDO('mysql:...'))->query('SELECT ...')->fetchAll(); return $rows; }); ``` `$concurrency` limits how many coroutines can be alive concurrently inside a **single** worker. ## Submitting tasks ### submit() ```php $future = $pool->submit(callable $task, mixed ...$args): Async\Future; ``` Adds a task to the pool's queue. Returns an [`Async\Future`](/en/docs/components/future.html) that: * **resolves** with the `return` value of `$task` when the worker finishes execution; * **rejects** with an exception if `$task` threw an exception. ```php submit(function() { return strtoupper('hello from worker'); }); // Task with arguments — arguments are also passed by value (deep copy) $f2 = $pool->submit(function(int $n, string $prefix) { $sum = 0; for ($i = 0; $i < $n; $i++) { $sum += $i; } return "$prefix: $sum"; }, 1_000_000, 'result'); echo await($f1), "\n"; echo await($f2), "\n"; $pool->close(); }); ``` ``` HELLO FROM WORKER result: 499999500000 ``` #### Handling exceptions from a task If a task throws an exception, the `Future` is rejected, and `await()` rethrows it in the calling coroutine: ```php submit(function() { throw new RuntimeException('something went wrong in the worker'); }); try { await($f); } catch (RuntimeException $e) { echo "Caught: ", $e->getMessage(), "\n"; } $pool->close(); }); ``` ``` Caught: something went wrong in the worker ``` #### Data transfer rules The task (`$task`) and all `...$args` are **deep-copied** into the worker thread — the same rules as with `spawn_thread()`. You cannot pass `stdClass`, PHP references (`&$var`), or resources; attempting to do so will cause the source to throw `Async\ThreadTransferException`. More details: [«Data transfer between threads»](/en/docs/components/threads.html#data-transfer-between-threads). ### map() ```php $results = $pool->map(array $items, callable $task): array; ``` Applies `$task` to each element of `$items` in parallel using the pool's workers. **Blocks** the calling coroutine until all tasks complete. Returns an array of results in the same order as the input data. ```php map($files, function(string $path) { if (!file_exists($path)) { return 0; } $count = 0; $fh = fopen($path, 'r'); while (!feof($fh)) { fgets($fh); $count++; } fclose($fh); return $count; }); foreach ($files as $i => $path) { echo "$path: {$lineCounts[$i]} lines\n"; } $pool->close(); }); ``` If at least one task throws an exception, `map()` rethrows it in the calling coroutine. The result order always matches the input element order, regardless of the order in which workers finish. ## Monitoring pool state ```php submit(function() { // Simulate work $t = microtime(true); while (microtime(true) - $t < 0.1) {} return 'done'; }); } // Check counters while tasks are running delay(50); // give workers time to start echo "workers: ", $pool->getWorkerCount(), "\n"; echo "pending: ", $pool->getPendingCount(), "\n"; echo "running: ", $pool->getRunningCount(), "\n"; echo "completed: ", $pool->getCompletedCount(), "\n"; foreach ($futures as $f) { await($f); } echo "--- after all done ---\n"; echo "pending: ", $pool->getPendingCount(), "\n"; echo "running: ", $pool->getRunningCount(), "\n"; echo "completed: ", $pool->getCompletedCount(), "\n"; $pool->close(); }); ``` ``` workers: 3 pending: 3 running: 3 completed: 0 --- after all done --- pending: 0 running: 0 completed: 6 ``` | Method | What it returns | |-----------------------|-----------------------------------------------------------------------------------------| | `getWorkerCount()` | Number of worker threads running now; a closed pool reports 0 once its workers have exited | | `getPendingCount()` | Tasks in the queue, not yet picked up by a worker | | `getRunningCount()` | Tasks currently being executed by a worker | | `getCompletedCount()` | Total tasks completed since the pool was created (monotonically increasing) | | `isClosed()` | `true` if the pool has been closed via `close()` or `cancel()` | The counters are implemented as atomic variables — they are accurate at any point in time, even when workers are running in parallel threads. ## Shutting down the pool Worker threads live until the pool is explicitly stopped. Always call `close()` or `cancel()` when done — otherwise threads will continue running until the end of the process. ### close() — graceful shutdown ```php $pool->close(); ``` After calling `close()`: * New `submit()` calls immediately throw `Async\ThreadPoolException`. * Tasks already in the queue or being executed by workers **complete normally**. * The method returns only after all in-progress tasks have finished and all workers have stopped. ```php submit(function() { return 'finished'; }); $pool->close(); echo await($f), "\n"; // Guaranteed to get the result try { $pool->submit(fn() => 'too late'); } catch (ThreadPoolException $e) { echo "Error: ", $e->getMessage(), "\n"; } }); ``` ``` finished Error: Cannot submit task: thread pool is closed ``` ### cancel() — hard/forced shutdown ```php $pool->cancel(); ``` After calling `cancel()`: * New `submit()` calls throw `Async\ThreadPoolException`. * Tasks in the queue (not yet picked up by a worker) are **immediately rejected** — the corresponding `Future` objects transition to the "rejected" state. * Tasks already being executed by workers **run to completion** of the current iteration (forcibly interrupting PHP code inside a thread is not possible). * Workers stop immediately after finishing the current task and do not pick up new ones. ```php submit(function() use ($i) { $t = microtime(true); while (microtime(true) - $t < 0.2) {} return $i; }); } // Cancel immediately — tasks in the queue will be rejected $pool->cancel(); $done = 0; $cancelled = 0; foreach ($futures as $f) { try { await($f); $done++; } catch (ThreadPoolException $e) { $cancelled++; } } echo "done: $done\n"; echo "cancelled: $cancelled\n"; }); ``` ``` done: 2 cancelled: 6 ``` ### Comparing close() and cancel() | Aspect | `close()` | `cancel()` | |---------------------------------|------------------------------------|---------------------------------------| | New submit() calls | Throws `ThreadPoolException` | Throws `ThreadPoolException` | | Tasks in the queue | Execute normally | Rejected immediately | | Currently executing tasks | Complete normally | Complete normally (current iteration) | | When workers stop | After the queue is drained | After the current task completes | ## Passing a pool between threads The `ThreadPool` object is itself thread-safe: it can be passed into `spawn_thread()` via `use()`, and any thread can call `submit()` on the same pool. ```php submit(function() use ($i) { return $i * $i; }); } $results = []; foreach ($futures as $f) { $results[] = await($f); } return $results; }); $squares = await($producer); echo implode(', ', $squares), "\n"; $pool->close(); }); ``` ``` 0, 1, 4, 9, 16, 25, 36, 49, 64, 81 ``` This enables architectures where multiple OS threads or coroutines **share a single pool**, submitting tasks to it independently of each other. ## Full example: parallel image processing The pool is created once. Each worker receives a file path, opens the image via GD, scales it down to the specified dimensions, converts it to grayscale, and saves it to the output directory. The main thread collects results as they become ready. ```php imagecreatefromjpeg($src), IMAGETYPE_PNG => imagecreatefrompng($src), IMAGETYPE_WEBP => imagecreatefromwebp($src), default => throw new \RuntimeException("Unsupported format: $src"), }; // Resize while preserving aspect ratio [$origW, $origH] = [$info[0], $info[1]]; $scale = min(1.0, $maxWidth / $origW); $newW = (int) ($origW * $scale); $newH = (int) ($origH * $scale); $resized = imagescale($original, $newW, $newH, IMG_BICUBIC); imagedestroy($original); // Convert to grayscale imagefilter($resized, IMG_FILTER_GRAYSCALE); // Save to output directory $outPath = $outDir . '/' . basename($src, '.' . pathinfo($src, PATHINFO_EXTENSION)) . '_thumb.jpg'; imagejpeg($resized, $outPath, quality: 85); $outSize = filesize($outPath); imagedestroy($resized); return [ 'src' => $src, 'out' => $outPath, 'size_kb' => round($outSize / 1024, 1), 'width' => $newW, 'height' => $newH, ]; } spawn(function() { $srcDir = '/var/www/uploads/originals'; $outDir = '/var/www/uploads/thumbs'; $maxW = 800; // List of files to process $files = glob("$srcDir/*.{jpg,jpeg,png,webp}", GLOB_BRACE); if (empty($files)) { echo "No files to process\n"; return; } $pool = new ThreadPool(workers: (int) shell_exec('nproc') ?: 4); // map() preserves order — results[i] corresponds to files[i] $results = $pool->map($files, fn(string $path) => processImage($path, $outDir, $maxW)); $totalKb = 0; foreach ($results as $r) { echo sprintf("%-40s → %s (%dx%d, %.1f KB)\n", basename($r['src']), basename($r['out']), $r['width'], $r['height'], $r['size_kb'] ); $totalKb += $r['size_kb']; } echo sprintf("\nProcessed: %d files, total %.1f KB\n", count($results), $totalKb); $pool->close(); }); ``` ``` photo_001.jpg → photo_001_thumb.jpg (800x533, 42.3 KB) photo_002.png → photo_002_thumb.jpg (800x600, 38.7 KB) photo_003.jpg → photo_003_thumb.jpg (800x450, 51.2 KB) ... Processed: 20 files, total 876.4 KB ``` ## See also * [`spawn_thread()`](/en/docs/reference/spawn-thread.html) — launching a single task in a separate thread * [`Async\Thread`](/en/docs/components/threads.html) — OS threads and data transfer rules * [`Async\ThreadChannel`](/en/docs/components/thread-channels.html) — thread-safe channels * [`Async\Future`](/en/docs/components/future.html) — waiting for a task result --- --- url: https://true-async.github.io/en/docs/reference/available-parallelism.md description: >- Async\available_parallelism() — returns the number of CPUs available to the process. Honours cgroup quotas, affinity, and container limits. --- # available\_parallelism (PHP 8.6+, True Async 1.0) `Async\available_parallelism()` returns the number of CPUs available to the **current process**. ## Description ```php namespace Async; function available_parallelism(): int ``` Honours cgroup CPU quotas, `sched_setaffinity`, and similar constraints. This is the value libuv recommends for thread-pool / worker-pool sizing. Always `>= 1`. In a container with `cpu.max=2`, the function returns `2`, not the physical core count of the host. On bare metal — the number of logical cores minus affinity restrictions (if any). Backend: `uv_available_parallelism()` with a fallback to `uv_cpu_info`. ## Return value `int` — number of CPUs, guaranteed `>= 1`. ## Examples ### Example #1 Pool size matching available CPUs ```php addListener('0.0.0.0', 8080) ->setWorkers(available_parallelism()) ); $server->start(); ``` ### Example #3 Environment diagnostics ```php **Tip:** for `ThreadPool` and `HttpServer::setWorkers()` you do not have to call this function > by hand at all — both components use `available_parallelism()` automatically when the pool size > is set to `0`. > On most IO-bound workloads it makes sense to overcommit by `N + 1` or `N + 2`, because some > workers will be blocked on I/O. ## See also * [Async\ThreadPool](/en/docs/components/thread-pool.html) — where the value is used automatically * [Async\cpu\_usage()](/en/docs/reference/cpu-usage.html) — current process and system load * [Async\loadavg()](/en/docs/reference/loadavg.html) — average run-queue length --- --- url: https://true-async.github.io/en/docs/reference/await.md description: >- await() — waiting for a coroutine or Future to complete. Full documentation: parameters, exceptions, examples. --- # await (PHP 8.6+, True Async 1.0) `await()` — Waits for a coroutine, `Async\Future`, or any other `Async\Completable` to complete. Returns the result or throws an exception. ## Description ```php await(Async\Completable $awaitable, ?Async\Completable $cancellation = null): mixed ``` Suspends execution of the current coroutine until the specified `Async\Completable` `$awaitable` completes (or until `$cancellation` triggers, if provided) and returns the result. If the `awaitable` has already completed, the result is returned immediately. If the coroutine finished with an exception, it will be propagated to the calling code. ## Parameters **`awaitable`** An object implementing the `Async\Completable` interface (extends `Async\Awaitable`). Typically this is: * `Async\Coroutine` - the result of calling `spawn()` * `Async\TaskGroup` - a task group * `Async\Future` - a future value **`cancellation`** An optional `Async\Completable` object; when it completes, the waiting will be cancelled. ## Return Values Returns the value that the coroutine returned. The return type depends on the coroutine. ## Errors/Exceptions If the coroutine finished with an exception, `await()` will rethrow that exception. If the coroutine was cancelled, `Async\AsyncCancellation` will be thrown. If the cancellation token (`$cancellation`) triggered, `Async\OperationCanceledException` will be thrown. The original exception from the token is available via `$e->getPrevious()`. This allows you to distinguish a token trigger from an exception thrown by the awaitable object itself. ## How the exception is delivered When a coroutine finishes with an exception, **the result "settles" on its handle** until someone picks it up. The behaviour is symmetric to `Async\Future` and depends on whether anyone besides the Scheduler is holding the coroutine handle: * **The handle is held** (`$coro = spawn(...)`, the coroutine is in an array, passed into `await_all()`, etc.) — the exception stays on the handle and waits. Any `await($coro)` retrieves it, even long after the coroutine has finished. * **No one holds the handle** (fire-and-forget — `spawn(...)` without saving the result) — the exception surfaces when the handle is destroyed, through the fire-and-forget safety net. The key practical consequence — **`await` catches the exception even after a race**: ```php use function Async\spawn; use function Async\await; $coro = spawn(function () { throw new RuntimeException('boom'); }); // The coroutine may finish before we reach await — that's fine. // The exception will quietly wait for us here: try { await($coro); } catch (RuntimeException $e) { echo "caught: ", $e->getMessage(), "\n"; // caught: boom } ``` The same applies to `await_all()`, `await_any_or_fail()`, and other `await_*()` calls: you can collect coroutines into an array, let them run in parallel, and then await them. Exceptions are gathered through `await`. > When a parent scope dies before its coroutine, the child coroutines receive `AsyncCancellation` > per spec. That branch is handled separately and does not depend on who holds the handle. ## Examples ### Example #1 Basic usage of await() ```php ``` ### Example #2 Sequential waiting ```php ``` ### Example #3 Exception handling ```php getMessage() . "\n"; } ?> ``` ### Example #4 await with TaskGroup ```php spawn(function() { return "Result 1"; }); $taskGroup->spawn(function() { return "Result 2"; }); $taskGroup->spawn(function() { return "Result 3"; }); // Get an array of all results $results = await($taskGroup); print_r($results); // Array of results ?> ``` ### Example #5 Multiple await on the same coroutine ```php ``` ### Example #6 await inside a coroutine ```php ``` ## Changelog | Version | Description | |----------|---------------------------------| | 1.0.0 | Added the `await()` function | ## See Also * [spawn()](/en/docs/reference/spawn.html) - Launching a coroutine * [suspend()](/en/docs/reference/suspend.html) - Suspending execution --- --- url: https://true-async.github.io/en/tutors/03-await.md description: Why await() is needed and how it works with coroutines. --- # Await Imagine a classic distributed backend system. There's a dozen services tied together via `JWT` authorization. A user logs in through `UserDirectory` and lands in `ProfileService` to change some business data in their profile, for example their home address or a delivery address. The API's job is to update that data: ```php function profileExists(int $userId): bool { $result = $db->query('SELECT 1 FROM profiles WHERE user_id = ?', [$userId]); return $result->rowCount() > 0; } if (profileExists($userId)) { updateProfile($userId, $changes); } ``` If the operation is deemed risky, it might be necessary to additionally validate the JWT token through `UserDirectory`: ```php function validateToken(string $token): bool { $response = file_get_contents("https://userdirectory.example.com/api/validate?token=$token"); return json_decode($response)->valid; } if (profileExists($userId) && validateToken($token)) { updateProfile($userId, $changes); } ``` The `validateToken` and `profileExists` functions perform I/O and take time, especially `validateToken`. It would make sense to run `validateToken` in a coroutine to reduce the total wait time: ```php $isValid = spawn(validateToken(...), $token); if (profileExists($userId) && $isValid) { updateProfile($userId, $changes); } ``` However, this code won't work, since `spawn` can't return a result immediately. We need some way to wait for the coroutine. That's what the `await` function is for. ```php use function Async\await; $validation = spawn(validateToken(...), $token); if (profileExists($userId) && await($validation)) { updateProfile($userId, $changes); } ``` The `await` function pauses the flow of execution until the coroutine finishes, which lets you synchronize different coroutines with one another. --- --- url: https://true-async.github.io/en/docs/reference/await-all-or-fail.md description: >- await_all_or_fail() — wait for all tasks to complete; throws an exception on the first error. --- # await\_all\_or\_fail (PHP 8.6+, True Async 1.0) `await_all_or_fail()` — Waits for **all** tasks to complete successfully. On the first error, throws an exception and cancels the remaining tasks. ## Description ```php await_all_or_fail( iterable $triggers, ?Async\Awaitable $cancellation = null, bool $preserveKeyOrder = true ): array ``` ## Parameters **`triggers`** An iterable collection of `Async\Completable` objects (coroutines, Futures, etc.). **`cancellation`** An optional Awaitable to cancel the entire wait (e.g., `timeout()`). **`preserveKeyOrder`** If `true` (default), results are returned in the key order of the input array. If `false`, in completion order. ## Return Values An array of results from all tasks. Keys correspond to the input array keys. ## Errors/Exceptions Throws the exception from the first task that failed. ## Examples ### Example #1 Parallel data loading ```php spawn(file_get_contents(...), 'https://api/users'), 'orders' => spawn(file_get_contents(...), 'https://api/orders'), 'products' => spawn(file_get_contents(...), 'https://api/products'), ]); // $results['users'], $results['orders'], $results['products'] ?> ``` ### Example #2 With timeout ```php ``` ### Example #3 With Iterator instead of array All `await_*` family functions accept not only arrays but any `iterable`, including `Iterator` implementations. This allows generating coroutines dynamically: ```php urls = $urls; } public function current(): mixed { return spawn(file_get_contents(...), $this->urls[$this->pos]); } public function key(): int { return $this->pos; } public function next(): void { $this->pos++; } public function valid(): bool { return isset($this->urls[$this->pos]); } public function rewind(): void { $this->pos = 0; } } $iterator = new UrlIterator([ 'https://api.example.com/a', 'https://api.example.com/b', 'https://api.example.com/c', ]); $results = await_all_or_fail($iterator); ?> ``` ## See Also * [await\_all()](/en/docs/reference/await-all.html) — all tasks with error tolerance * [await()](/en/docs/reference/await.html) — waiting for a single task --- --- url: https://true-async.github.io/en/docs/reference/await-all.md description: await_all() — wait for all tasks with tolerance for partial failures. --- # await\_all (PHP 8.6+, True Async 1.0) `await_all()` — Waits for **all** tasks to complete, collecting results and errors separately. Does not throw an exception when individual tasks fail. ## Description ```php await_all( iterable $triggers, ?Async\Awaitable $cancellation = null, bool $preserveKeyOrder = true, bool $fillNull = false ): array ``` ## Parameters **`triggers`** An iterable collection of `Async\Completable` objects. **`cancellation`** An optional Awaitable to cancel the entire wait. **`preserveKeyOrder`** If `true` (default), results are in the key order of the input array. If `false`, in completion order. **`fillNull`** If `true`, `null` is placed in the results array for tasks that failed. If `false` (default), keys with errors are omitted. ## Return Values An array of two elements: `[$results, $errors]` * `$results` — array of successful results * `$errors` — array of exceptions (keys correspond to the input task keys) ## Examples ### Example #1 Tolerating partial failures ```php spawn(file_get_contents(...), 'https://api/fast'), 'slow' => spawn(file_get_contents(...), 'https://api/slow'), 'broken' => spawn(function() { throw new \Exception('Error'); }), ]; [$results, $errors] = await_all($coroutines); // $results contains 'fast' and 'slow' // $errors contains 'broken' => Exception foreach ($errors as $key => $error) { echo "Task '$key' failed: {$error->getMessage()}\n"; } ?> ``` ### Example #2 With fillNull ```php ``` ## Notes > **Note:** The `triggers` parameter accepts any `iterable`, including `Iterator` implementations. Coroutines can be created dynamically during iteration. See the [Iterator example](/en/docs/reference/await-all-or-fail.html#example-3-with-iterator-instead-of-array). ## See Also * [await\_all\_or\_fail()](/en/docs/reference/await-all-or-fail.html) — all tasks, error aborts * [await\_any\_or\_fail()](/en/docs/reference/await-any-or-fail.html) — first result --- --- url: https://true-async.github.io/en/docs/reference/await-any-of-or-fail.md description: await_any_of_or_fail() — wait for the first N successfully completed tasks. --- # await\_any\_of\_or\_fail (PHP 8.6+, True Async 1.0) `await_any_of_or_fail()` — Waits for the **first N** tasks to complete successfully. If one of the first N fails, throws an exception. ## Description ```php await_any_of_or_fail( int $count, iterable $triggers, ?Async\Awaitable $cancellation = null, bool $preserveKeyOrder = true ): array ``` ## Parameters **`count`** The number of successful results to wait for. If `0`, returns an empty array. **`triggers`** An iterable collection of `Async\Completable` objects. **`cancellation`** An optional Awaitable to cancel the wait. **`preserveKeyOrder`** If `true`, result keys correspond to the input array keys. If `false`, in completion order. ## Return Values An array of `$count` successful results. ## Errors/Exceptions If a task fails before reaching `$count` successes, the exception is thrown. ## Examples ### Example #1 Getting 2 out of 5 results ```php ``` ## Notes > **Note:** The `triggers` parameter accepts any `iterable`, including `Iterator` implementations. See the [Iterator example](/en/docs/reference/await-all-or-fail.html#example-3-with-iterator-instead-of-array). ## See Also * [await\_any\_of()](/en/docs/reference/await-any-of.html) — first N with error tolerance * [await\_all\_or\_fail()](/en/docs/reference/await-all-or-fail.html) — all tasks --- --- url: https://true-async.github.io/en/docs/reference/await-any-of.md description: >- await_any_of() — wait for the first N tasks with tolerance for partial failures. --- # await\_any\_of (PHP 8.6+, True Async 1.0) `await_any_of()` — Waits for the **first N** tasks to complete, collecting results and errors separately. Does not throw an exception when individual tasks fail. ## Description ```php await_any_of( int $count, iterable $triggers, ?Async\Awaitable $cancellation = null, bool $preserveKeyOrder = true, bool $fillNull = false ): array ``` ## Parameters **`count`** The number of successful results to wait for. **`triggers`** An iterable collection of `Async\Completable` objects. **`cancellation`** An optional Awaitable to cancel the wait. **`preserveKeyOrder`** If `true`, result keys correspond to the input array keys. **`fillNull`** If `true`, `null` is placed in the results array for tasks that failed. ## Return Values An array of two elements: `[$results, $errors]` * `$results` — array of successful results (up to `$count` items) * `$errors` — array of exceptions from tasks that failed ## Examples ### Example #1 Quorum with error tolerance ```php = 3) { echo "Quorum reached\n"; } else { echo "Quorum not reached, errors: " . count($errors) . "\n"; } ?> ``` ## Notes > **Note:** The `triggers` parameter accepts any `iterable`, including `Iterator` implementations. See the [Iterator example](/en/docs/reference/await-all-or-fail.html#example-3-with-iterator-instead-of-array). ## See Also * [await\_any\_of\_or\_fail()](/en/docs/reference/await-any-of-or-fail.html) — first N, error aborts * [await\_all()](/en/docs/reference/await-all.html) — all tasks with error tolerance --- --- url: https://true-async.github.io/en/docs/reference/await-any-or-fail.md description: await_any_or_fail() — wait for the first completed task. --- # await\_any\_or\_fail (PHP 8.6+, True Async 1.0) `await_any_or_fail()` — Waits for the **first** task to complete. If the first completed task threw an exception, it is propagated. ## Description ```php await_any_or_fail( iterable $triggers, ?Async\Awaitable $cancellation = null ): mixed ``` ## Parameters **`triggers`** An iterable collection of `Async\Completable` objects. **`cancellation`** An optional Awaitable to cancel the wait. ## Return Values The result of the first completed task. ## Errors/Exceptions If the first completed task threw an exception, it will be propagated. ## Examples ### Example #1 Request race ```php ``` ## Notes > **Note:** The `triggers` parameter accepts any `iterable`, including `Iterator` implementations. See the [Iterator example](/en/docs/reference/await-all-or-fail.html#example-3-with-iterator-instead-of-array). ## See Also * [await\_first\_success()](/en/docs/reference/await-first-success.html) — first success, ignoring errors * [await\_all\_or\_fail()](/en/docs/reference/await-all-or-fail.html) — all tasks --- --- url: https://true-async.github.io/en/docs/reference/await-first-success.md description: >- await_first_success() — wait for the first successfully completed task, ignoring errors from others. --- # await\_first\_success (PHP 8.6+, True Async 1.0) `await_first_success()` — Waits for the **first successfully** completed task. Errors from other tasks are collected separately and do not interrupt the wait. ## Description ```php await_first_success( iterable $triggers, ?Async\Awaitable $cancellation = null ): array ``` ## Parameters **`triggers`** An iterable collection of `Async\Completable` objects. **`cancellation`** An optional Awaitable to cancel the wait. ## Return Values An array of two elements: `[$result, $errors]` * `$result` — the result of the first successfully completed task (or `null` if all tasks failed) * `$errors` — array of exceptions from tasks that failed before the first success ## Examples ### Example #1 Fault-tolerant request ```php getMessage() . "\n"; } } ?> ``` ## Notes > **Note:** The `triggers` parameter accepts any `iterable`, including `Iterator` implementations. See the [Iterator example](/en/docs/reference/await-all-or-fail.html#example-3-with-iterator-instead-of-array). ## See Also * [await\_any\_or\_fail()](/en/docs/reference/await-any-or-fail.html) — first task, error aborts * [await\_all()](/en/docs/reference/await-all.html) — all tasks with error tolerance --- --- url: https://true-async.github.io/en/docs/components/interfaces.md description: >- Base TrueAsync interfaces -- Awaitable, Completable, Timeout, ScopeProvider and SpawnStrategy. --- # Base Interfaces ## Awaitable ```php interface Async\Awaitable {} ``` A marker interface for all objects that can be awaited. Contains no methods -- serves for type-checking. Awaitable objects can change states multiple times, meaning they are `multiple-shot` objects. Implemented by: `Coroutine`, `Future`, `Channel`, `Timeout`. ## Completable ```php interface Async\Completable extends Async\Awaitable { public function cancel(?AsyncCancellation $cancellation = null): void; public function isCompleted(): bool; public function isCancelled(): bool; } ``` Extends `Awaitable`. `Async\Completable` objects change state only once (`one-shot`). Implemented by: `Coroutine`, `Future`, `Timeout`. ### cancel() Cancels the object. The optional `$cancellation` parameter allows passing a specific cancellation error. ### isCompleted() Returns `true` if the object has already completed (successfully or with an error). ### isCancelled() Returns `true` if the object was cancelled. ## Timeout ```php final class Async\Timeout implements Async\Completable { public function cancel(?AsyncCancellation $cancellation = null): void; public function isCompleted(): bool; public function isCancelled(): bool; } ``` A timeout object. Created via the `timeout()` function: ```php cancel(); // Release the timer ``` ## ScopeProvider ```php interface Async\ScopeProvider { public function provideScope(): ?Scope; } ``` An interface that allows providing a `Scope` for creating coroutines. Used with `spawn_with()`: ```php scope = new Scope(); } public function provideScope(): Scope { return $this->scope; } } $provider = new RequestScope(); $coroutine = spawn_with($provider, function() { echo "Working in the provided Scope\n"; }); ?> ``` If `provideScope()` returns `null`, the coroutine is created in the current Scope. ## SpawnStrategy ```php interface Async\SpawnStrategy extends Async\ScopeProvider { public function beforeCoroutineEnqueue(Coroutine $coroutine, Scope $scope): array; public function afterCoroutineEnqueue(Coroutine $coroutine, Scope $scope): void; } ``` Extends `ScopeProvider` with lifecycle hooks -- allows executing code before and after a coroutine is enqueued. ### beforeCoroutineEnqueue() Called **before** the coroutine is added to the scheduler queue. Returns an array of parameters. ### afterCoroutineEnqueue() Called **after** the coroutine is added to the queue. ```php scope = new Scope(); } public function provideScope(): Scope { return $this->scope; } public function beforeCoroutineEnqueue(Coroutine $coroutine, Scope $scope): array { echo "Coroutine #{$coroutine->getId()} will be created\n"; return []; } public function afterCoroutineEnqueue(Coroutine $coroutine, Scope $scope): void { echo "Coroutine #{$coroutine->getId()} added to queue\n"; } } $strategy = new LoggingStrategy(); spawn_with($strategy, function() { echo "Executing\n"; }); ?> ``` ## CircuitBreaker and CircuitBreakerStrategy These interfaces are described in the [Async\Pool](/en/docs/components/pool.html) documentation. ## See Also * [Coroutines](/en/docs/components/coroutines.html) -- the basic unit of concurrency * [Scope](/en/docs/components/scope.html) -- managing coroutine lifetimes * [Future](/en/docs/components/future.html) -- a promise of a result * [spawn\_with()](/en/docs/reference/spawn-with.html) -- launching a coroutine with a provider --- --- url: https://true-async.github.io/en/tutors-server/04-streaming-uploads.md description: send() and sendable(), streaming the body, file uploads, and sendFile(). --- # Byte Streams Until now our responses have been small and whole, built with `setBody()` and `json()`. For an API that's exactly what you want. But `ProfileService` has picked up tasks of a different caliber. Accounting wants a year's export, and that's hundreds of megabytes of `CSV`. Users upload import files, and those aren't small either. Let's do the rough math: fifty concurrent requests, each holding a hundred megabytes in memory... no, let's not count further, it's already clear how that ends. We need to learn to work with bodies in parts. In both directions. ## The Response in Parts: send() ```php $server->addHttpHandler(function (HttpRequest $req, HttpResponse $res) { $res->setStatusCode(200) ->setHeader('Content-Type', 'text/csv'); foreach (exportRows() as $row) { $res->send(implode(',', $row) . "\n"); } }); ``` The first `send()` fixes the status and headers, after which chunks go to the client as they become ready: chunked in HTTP/1.1, DATA frames in HTTP/2. At any moment a single row lives in memory. A stream of bytes to the client takes shape! Now a question to test your wits. We generate rows from the database at, say, a gigabit per second. And the client is downloading over mobile internet on a train. Where do the extra rows go? If you read the chapter on channels, you already have the answer: backpressure. When the stream buffer fills up, `send()` suspends the handler coroutine. The client cleared the queue, the coroutine woke up, generation continued. The export tunes itself to the speed of the narrowest bottleneck, and note, we didn't write a single line for it! Waiting for a slow client, though, isn't always what you want: ```php if (!$res->sendable()) { // send() will block right now. Say that we won't wait. } ``` `send()` is always safe. `sendable()` only warns you: the next call will go to sleep. What to do next is up to you. > There's still a popular type of attack with slow clients: they open > a connection and don't read, until the server blocks. > For a coroutine server this is less frightening, since each coroutine > costs far less than a process or a thread. > Still, the overall coroutine limit is an important parameter, as are > the other extra security options! ## The Request Body in Parts: readBody() The reverse situation. A user uploads a gigabyte CSV, and by default the handler is called once the entire body has been read. The word "gigabyte" and the words "into memory" in one sentence are exactly what we agreed to avoid. We turn on streaming mode: ```php $config->setBodyStreamingEnabled(true); ``` ```php $server->addHttpHandler(function (HttpRequest $req, HttpResponse $res) { $file = fopen('/var/imports/' . bin2hex(random_bytes(8)) . '.csv', 'wb'); $total = 0; while (($chunk = $req->readBody()) !== null) { fwrite($file, $chunk); $total += strlen($chunk); } fclose($file); $res->json(['received' => $total]); }); ``` In the `TrueAsync` server the handler starts right after the headers, early enough. In fact, the coroutine's work happens precisely in step with receiving data from the socket. That's why working with the data stream here is a natural fit. If you run fifty concurrent 20 MiB uploads, peak memory would have been 1170 MiB and became 197. Throughput grew almost threefold, because processing begins without waiting for the upload to finish. Not bad for one line of configuration. ## Forms With Files: UploadedFile The classic HTML form with a file is an easier case, and for it you don't need to turn anything on. Multipart is always parsed as a stream, and the handler receives ready-made objects: ```php $server->addHttpHandler(function (HttpRequest $req, HttpResponse $res) { $csv = $req->getFile('import'); if ($csv === null || !$csv->isValid()) { throw new HttpException('File is required', 422); } $csv->moveTo('/var/imports/' . $csv->getClientFilename()); $res->json(['size' => $csv->getSize()]); }); ``` Anyone who's seen PSR-7 feels at home: `moveTo()`, `getSize()`, `getClientFilename()`. Several files in one field (`photos[]`) come as an array via `getFiles()`. ## Serving Files: sendFile() That leaves serving ready-made files. This is the one thing you should not assemble by hand from `fopen` and `send()`, and here's why: ```php use TrueAsync\SendFileOptions; use TrueAsync\SendFileDisposition; $server->addHttpHandler(function (HttpRequest $req, HttpResponse $res) { $res->sendFile('/var/reports/2026-06.csv', new SendFileOptions( disposition: SendFileDisposition::ATTACHMENT, downloadName: 'report-june.csv', cacheControl: 'private, max-age=3600', )); }); ``` Behind that single call hides some thirty years of HTTP history. `Content-Type` from the extension. `ETag` and `Last-Modified`, so a repeat request gets away with a short 304. Resumed downloads via `Range` when the client dropped in the middle. Serving a pre-compressed neighbor (`report.csv.gz`) if the client understands gzip. And all of it with asynchronous reads from disk, without holding up the event loop. Writing this by hand means forgetting at least half of it. One rule: after `sendFile()` the response is sealed, writing anything more into it won't work, you'll get an exception. We can now both accept and serve a gigabyte file. But note who we serve all this to through a PHP handler: reports gated by access rights, files behind one-time links. And CSS? And the logo? They're the same for everyone, and spinning up a coroutine for each of them feels a bit awkward. That's the next chapter, and it'll be short. --- --- url: https://true-async.github.io/en/docs/components/cancellation.md description: >- Coroutine cancellation in TrueAsync -- cooperative cancellation, critical sections with protect(), cascading cancellation via Scope, timeouts. --- # Cancellation A browser sent a request, but then the user closed the page. The server continues working on a request that is no longer needed. It would be good to abort the operation to avoid unnecessary costs. Or suppose there is a long-running data copy process that needs to be suddenly cancelled. There are many scenarios where you need to stop operations. Usually this problem is solved with flag variables or cancellation tokens, which is quite labor-intensive. The code must know that it might be cancelled, must plan cancellation checkpoints, and correctly handle these situations. ## Cancellable by Design Most of the time, an application is busy reading data from databases, files, or the network. Interrupting a read is safe. Therefore, in `TrueAsync` the following principle applies: **a coroutine can be cancelled at any moment from a waiting state**. This approach reduces the amount of code, since in most cases, the programmer doesn't need to worry about cancellation. ## How Cancellation Works A special exception -- `Cancellation` -- is used to cancel a coroutine. The `Cancellation` exception or a derived one is thrown at a suspension point (`suspend()`, `await()`, `delay()`). Execution can also be interrupted during I/O operations or any other blocking operation. ```php $coroutine = spawn(function() { echo "Starting work\n"; suspend(); // Here the coroutine will receive Cancellation echo "This won't happen\n"; }); $coroutine->cancel(); try { await($coroutine); } catch (\Cancellation $e) { echo "Coroutine cancelled\n"; throw $e; } ``` ## Cancellation Cannot Be Suppressed `Cancellation` is a base-level exception, on par with `Error` and `Exception`. The `catch (Exception $e)` construct won't catch it. Catching `Cancellation` and continuing work is an error. You can use `catch Async\AsyncCancellation` to handle special situations, but you must ensure that you correctly re-throw the exception. In general, it is recommended to use `finally` for guaranteed resource cleanup: ```php spawn(function() { $connection = connectToDatabase(); try { processData($connection); } finally { $connection->close(); } }); ``` ## Three Cancellation Scenarios The behavior of `cancel()` depends on the coroutine's state: **The coroutine hasn't started yet** -- it will never start. ```php $coroutine = spawn(function() { echo "Won't execute\n"; }); $coroutine->cancel(); ``` **The coroutine is in a waiting state** -- it will wake up with a `Cancellation` exception. ```php $coroutine = spawn(function() { echo "Started work\n"; suspend(); // Here it will receive Cancellation echo "Won't execute\n"; }); suspend(); $coroutine->cancel(); ``` **The coroutine has already completed** -- nothing happens. ```php $coroutine = spawn(function() { return 42; }); await($coroutine); $coroutine->cancel(); // Not an error, but has no effect ``` ## Critical Sections: protect() Not every operation can be safely interrupted. If a coroutine has debited money from one account but hasn't yet credited another -- cancellation at this point would lead to data loss. The `protect()` function defers cancellation until the critical section completes: ```php use Async\protect; use Async\spawn; $coroutine = spawn(function() { protect(function() { $db->query("UPDATE accounts SET balance = balance - 100 WHERE id = 1"); suspend(); $db->query("UPDATE accounts SET balance = balance + 100 WHERE id = 2"); }); // Cancellation will take effect here -- after exiting protect() }); suspend(); $coroutine->cancel(); ``` Inside `protect()`, the coroutine is marked as protected. If `cancel()` arrives at this moment, the cancellation is saved but not applied. As soon as `protect()` completes -- the deferred cancellation takes effect immediately. ## Cascading Cancellation via Scope When a `Scope` is cancelled, all its coroutines and all child scopes are cancelled. The cascade goes **only top-down** -- cancelling a child scope does not affect the parent or sibling scopes. ### Isolation: Cancelling a Child Doesn't Affect Others ```php $parent = new Async\Scope(); $child1 = Async\Scope::inherit($parent); $child2 = Async\Scope::inherit($parent); // Cancel only child1 $child1->cancel(); $parent->isCancelled(); // false -- parent is unaffected $child1->isCancelled(); // true $child2->isCancelled(); // false -- sibling scope is unaffected ``` ### Downward Cascade: Cancelling a Parent Cancels All Descendants ```php $parent = new Async\Scope(); $child1 = Async\Scope::inherit($parent); $child2 = Async\Scope::inherit($parent); $parent->cancel(); // Cascade: cancels both child1 and child2 $parent->isCancelled(); // true $child1->isCancelled(); // true $child2->isCancelled(); // true ``` ### A Coroutine Can Cancel Its Own Scope A coroutine can initiate cancellation of the scope it runs in. Code before the nearest suspension point will continue executing: ```php $scope = new Async\Scope(); $scope->spawn(function() use ($scope) { echo "Starting\n"; $scope->cancel(); echo "This will still execute\n"; suspend(); echo "But this won't\n"; }); ``` After cancellation, the scope is closed -- launching a new coroutine in it is no longer possible. ## Timeouts A special case of cancellation is a timeout. The `timeout()` function creates a time limit: ```php $coroutine = spawn(function() { return file_get_contents('https://slow-api.example.com/data'); }); try { $result = await($coroutine, timeout(5000)); } catch (Async\OperationCanceledException $e) { // $e->getPrevious() contains TimeoutException echo "API didn't respond within 5 seconds\n"; } ``` When a cancellation token triggers (including a timeout), `OperationCanceledException` is thrown. The original exception from the token is available via `$e->getPrevious()`. This allows you to distinguish a token trigger from an error in the awaitable object itself. ## Checking the State A coroutine provides two methods for checking cancellation: * `isCancellationRequested()` -- cancellation was requested but not yet applied * `isCancelled()` -- the coroutine has actually stopped ```php $coroutine = spawn(function() { suspend(); }); $coroutine->cancel(); $coroutine->isCancellationRequested(); // true $coroutine->isCancelled(); // false -- not yet processed suspend(); $coroutine->isCancelled(); // true ``` ## Example: Queue Worker with Graceful Shutdown ```php class QueueWorker { private Async\Scope $scope; public function __construct() { $this->scope = new Async\Scope(); $this->queue = new Async\Channel(); } public function start(): void { $this->scope->spawn(function() { while (true) { $job = $this->queue->receive(); try { $job->process(); } finally { $job->markDone(); } } }); } public function stop(): void { // All coroutines will be stopped here $this->scope->cancel(); } } ``` ## What's Next? * [Scope](/en/docs/components/scope.html) -- managing groups of coroutines * [Coroutines](/en/docs/components/coroutines.html) -- coroutine lifecycle * [Channels](/en/docs/components/channels.html) -- data exchange between coroutines --- --- url: https://true-async.github.io/en/tutors/02-cancellation.md description: How cancel() works, and cooperative coroutine cancellation. --- # Cancellation In the previous example there was an interesting call to the `cancel` function, ```php use function Async\spawn; use function Async\delay; $progress = spawn(function() use (&$counter, $total) { while (true) { printProgress($counter, $total); delay(1000); } }); processUsers('users.csv', $counter); $progress->cancel(); ``` What happens if we remove it? Try it yourself. The `$progress` coroutine spins in an infinite loop with a 1-second delay. When `processUsers` finishes, control moves on. The `$progress` coroutine keeps running. Forever. It will never stop. The PHP process will never stop (unless it's killed from the outside). `$progress->cancel()` stops the `$progress` coroutine. But how? ```php use function Async\spawn; use function Async\delay; $progress = spawn(function() use (&$counter, $total) { while (true) { printProgress($counter, $total); try { delay(1000); } catch (Throwable $e) { echo get_class($e). PHP_EOL; throw $e; } } }); processUsers('users.csv', $counter); $progress->cancel(); ``` Let's change the code around `delay(1000)` and see what happens: ```bash Async\AsyncCancellation ``` When the `$progress` coroutine was asleep inside `delay(1000)` and `cancel()` was then called, `delay` threw an `Async\AsyncCancellation` exception. The same happens with a plain `sleep(1)`: under TrueAsync `sleep()` becomes asynchronous too and is likewise a cancellation point — it throws `Async\AsyncCancellation` exactly like `delay`. You could say that using `delay` in your code effectively establishes a contract that lets other code interrupt the coroutine's execution. This is very convenient, since it once again separates concerns between different modules: 1. The coroutine doesn't know when its execution will be interrupted. 2. The code that cancels the coroutine doesn't know exactly how the coroutine will be interrupted. A coroutine doesn't stop by some kind of magic, it stops via an exception. A coroutine cannot be cancelled in the middle of some arbitrary operation, only at a point where it itself chooses to yield. This type of cancellation is called "cooperative". --- --- url: https://true-async.github.io/en/docs/reference/channel/construct.md description: Create a new channel for exchanging data between coroutines. --- # Channel::\_\_construct (PHP 8.6+, True Async 1.0) ```php public Channel::__construct(int $capacity = 0, int $noProducerTimeout = 0, int $noConsumerTimeout = 0, bool $hardTimeouts = false) ``` Creates a new channel for passing data between coroutines. A channel is a synchronization primitive that allows coroutines to safely exchange data. The channel's behavior depends on the `$capacity` parameter: * **`capacity = 0`** — rendezvous channel (unbuffered). The `send()` operation suspends the sender until another coroutine calls `recv()`. This ensures synchronous data transfer. * **`capacity > 0`** — buffered channel. The `send()` operation does not block as long as there is room in the buffer. When the buffer is full, the sender is suspended until space becomes available. ## Parameters **capacity** : The capacity of the channel's internal buffer. `0` — rendezvous channel (default), send blocks until receive. Positive number — buffer size. ## Examples ### Example #1 Rendezvous channel (unbuffered) ```php send('hello'); // suspends until someone calls recv() echo "Sent\n"; }); spawn(function() use ($channel) { $value = $channel->recv(); // receives 'hello', unblocks the sender echo "Received: $value\n"; }); ``` ### Example #2 Buffered channel ```php send(1); // does not block — buffer is empty $channel->send(2); // does not block — space available $channel->send(3); // does not block — last slot $channel->send(4); // suspends — buffer is full }); ``` ## See also * [Channel::send](/en/docs/reference/channel/send.html) — Send a value to the channel * [Channel::recv](/en/docs/reference/channel/recv.html) — Receive a value from the channel * [Channel::capacity](/en/docs/reference/channel/capacity.html) — Get the channel capacity * [Channel::close](/en/docs/reference/channel/close.html) — Close the channel --- --- url: https://true-async.github.io/en/docs/reference/channel/capacity.md description: Get the channel buffer capacity. --- # Channel::capacity (PHP 8.6+, True Async 1.0) ```php public Channel::capacity(): int ``` Returns the channel capacity set at creation time via the constructor. * `0` — rendezvous channel (unbuffered). * Positive number — maximum buffer size. The value does not change during the channel's lifetime. ## Return values The channel buffer capacity (`int`). ## Examples ### Example #1 Checking capacity ```php capacity(); // 0 $buffered = new Channel(100); echo $buffered->capacity(); // 100 ``` ### Example #2 Adaptive logic based on channel type ```php capacity() === 0) { echo "Rendezvous channel: each send waits for a receiver\n"; } else { echo "Buffered channel: capacity {$ch->capacity()}\n"; echo "Free: " . ($ch->capacity() - $ch->count()) . " slots\n"; } } ``` ## See also * [Channel::\_\_construct](/en/docs/reference/channel/construct.html) — Create a channel * [Channel::count](/en/docs/reference/channel/count.html) — Number of values in the buffer * [Channel::isFull](/en/docs/reference/channel/is-full.html) — Check if the buffer is full --- --- url: https://true-async.github.io/en/docs/reference/channel/close.md description: Close the channel for further data sending. --- # Channel::close (PHP 8.6+, True Async 1.0) ```php public Channel::close(): void ``` Closes the channel. After closing: * Calling `send()` throws a `ChannelException`. * Calling `recv()` continues to return values from the buffer until it is empty. After that, `recv()` throws a `ChannelException`. * All coroutines waiting in `send()` or `recv()` receive a `ChannelException`. * Iteration via `foreach` terminates when the buffer is empty. Calling `close()` again on an already closed channel does not cause errors. ## Examples ### Example #1 Closing a channel after sending data ```php send($i); } $channel->close(); // signal to the receiver that no more data will come }); spawn(function() use ($channel) { foreach ($channel as $value) { echo "Received: $value\n"; } // foreach terminates after closing and draining the buffer echo "Channel exhausted\n"; }); ``` ### Example #2 Handling closure by waiting coroutines ```php send('data'); // waiting for a receiver } catch (\Async\ChannelException $e) { echo "Channel closed: {$e->getMessage()}\n"; } }); spawn(function() use ($channel) { delay(100); // short delay $channel->close(); // unblocks the sender with an exception }); ``` ## See also * [Channel::isClosed](/en/docs/reference/channel/is-closed.html) — Check if the channel is closed * [Channel::recv](/en/docs/reference/channel/recv.html) — Receive a value (drains the buffer) * [Channel::getIterator](/en/docs/reference/channel/get-iterator.html) — Iterate until closed --- --- url: https://true-async.github.io/en/docs/reference/channel/count.md description: Get the number of values in the channel buffer. --- # Channel::count (PHP 8.6+, True Async 1.0) ```php public Channel::count(): int ``` Returns the current number of values in the channel buffer. Channel implements the `Countable` interface, so you can use `count($channel)`. For a rendezvous channel (`capacity = 0`), this always returns `0`. ## Return values The number of values in the buffer (`int`). ## Examples ### Example #1 Monitoring buffer fill level ```php send(1); $channel->send(2); $channel->send(3); echo count($channel); // 3 echo $channel->count(); // 3 $channel->recv(); echo count($channel); // 2 ``` ### Example #2 Logging channel load ```php isClosed()) { $usage = $tasks->count() / $tasks->capacity() * 100; echo "Buffer is " . round($usage) . "% full\n"; delay(1000); } }); ``` ## See also * [Channel::capacity](/en/docs/reference/channel/capacity.html) --- Channel capacity * [Channel::isEmpty](/en/docs/reference/channel/is-empty.html) --- Check if the buffer is empty * [Channel::isFull](/en/docs/reference/channel/is-full.html) --- Check if the buffer is full --- --- url: https://true-async.github.io/en/docs/reference/channel/get-iterator.md description: Get an iterator to traverse channel values using foreach. --- # Channel::getIterator (PHP 8.6+, True Async 1.0) ```php public Channel::getIterator(): \Iterator ``` Returns an iterator for traversing channel values. Channel implements the `IteratorAggregate` interface, so you can use `foreach` directly. The iterator suspends the current coroutine while waiting for the next value. Iteration terminates when the channel is closed **and** the buffer is empty. > **Important:** If the channel is never closed, `foreach` will wait for new values indefinitely. ## Return values An `\Iterator` object for traversing channel values. ## Examples ### Example #1 Reading a channel with foreach ```php send('one'); $channel->send('two'); $channel->send('three'); $channel->close(); // without this, foreach will never terminate }); spawn(function() use ($channel) { foreach ($channel as $value) { echo "Received: $value\n"; } echo "All values processed\n"; }); ``` ### Example #2 Producer-consumer pattern ```php send($url); } $jobs->close(); }); // Consumer spawn(function() use ($jobs) { foreach ($jobs as $url) { $response = httpGet($url); echo "Downloaded: $url ({$response->status})\n"; } }); ``` ## See also * [Channel::recv](/en/docs/reference/channel/recv.html) --- Receive a single value * [Channel::close](/en/docs/reference/channel/close.html) --- Close the channel (terminates iteration) * [Channel::isEmpty](/en/docs/reference/channel/is-empty.html) --- Check if the buffer is empty --- --- url: https://true-async.github.io/en/docs/reference/channel/is-closed.md description: Check if the channel is closed. --- # Channel::isClosed (PHP 8.6+, True Async 1.0) ```php public Channel::isClosed(): bool ``` Checks whether the channel has been closed by a `close()` call. A closed channel does not accept new values via `send()`, but allows reading remaining values from the buffer via `recv()`. ## Return values `true` — the channel is closed. `false` — the channel is open. ## Examples ### Example #1 Checking channel state ```php isClosed() ? "closed" : "open"; // "open" $channel->send('data'); $channel->close(); echo $channel->isClosed() ? "closed" : "open"; // "closed" // You can still read the buffer even after closing $value = $channel->recv(); // "data" ``` ### Example #2 Conditional sending ```php isClosed()) { $data = produceData(); $channel->send($data); delay(100); } echo "Channel closed, stopping sends\n"; }); ``` ## See also * [Channel::close](/en/docs/reference/channel/close.html) — Close the channel * [Channel::isEmpty](/en/docs/reference/channel/is-empty.html) — Check if the buffer is empty --- --- url: https://true-async.github.io/en/docs/reference/channel/is-empty.md description: Check if the channel buffer is empty. --- # Channel::isEmpty (PHP 8.6+, True Async 1.0) ```php public Channel::isEmpty(): bool ``` Checks whether the channel buffer is empty (no values available for receiving). For a rendezvous channel (`capacity = 0`), this always returns `true`, since data is transferred directly without buffering. ## Return values `true` — the buffer is empty. `false` — the buffer contains values. ## Examples ### Example #1 Checking for available data ```php isEmpty() ? "empty" : "has data"; // "empty" $channel->send(42); echo $channel->isEmpty() ? "empty" : "has data"; // "has data" ``` ### Example #2 Batch data processing ```php isClosed() || !$channel->isEmpty()) { if ($channel->isEmpty()) { delay(50); // wait for data to arrive continue; } $batch = []; while (!$channel->isEmpty() && count($batch) < 10) { $batch[] = $channel->recv(); } processBatch($batch); } }); ``` ## See also * [Channel::isFull](/en/docs/reference/channel/is-full.html) --- Check if the buffer is full * [Channel::count](/en/docs/reference/channel/count.html) --- Number of values in the buffer * [Channel::recv](/en/docs/reference/channel/recv.html) --- Receive a value --- --- url: https://true-async.github.io/en/docs/reference/channel/is-full.md description: Check if the channel buffer is full. --- # Channel::isFull (PHP 8.6+, True Async 1.0) ```php public Channel::isFull(): bool ``` Checks whether the channel buffer is filled to its maximum capacity. For a rendezvous channel (`capacity = 0`), this always returns `true`, since there is no buffer. ## Return values `true` — the buffer is full (or it is a rendezvous channel). `false` — the buffer has free space. ## Examples ### Example #1 Checking buffer fullness ```php isFull() ? "full" : "has space"; // "has space" $channel->send('a'); $channel->send('b'); echo $channel->isFull() ? "full" : "has space"; // "full" ``` ### Example #2 Adaptive send rate ```php isFull()) { echo "Buffer full, slowing down processing\n"; } $channel->send($line); // suspends if full } $channel->close(); }); ``` ## See also * [Channel::isEmpty](/en/docs/reference/channel/is-empty.html) --- Check if the buffer is empty * [Channel::capacity](/en/docs/reference/channel/capacity.html) --- Channel capacity * [Channel::count](/en/docs/reference/channel/count.html) --- Number of values in the buffer * [Channel::sendAsync](/en/docs/reference/channel/send-async.html) --- Non-blocking send --- --- url: https://true-async.github.io/en/docs/reference/channel/recv.md description: Receive a value from the channel (blocking operation). --- # Channel::recv (PHP 8.6+, True Async 1.0) ```php public Channel::recv(?Completable $cancellationToken = null): mixed ``` Receives the next value from the channel. This is a blocking operation — the current coroutine is suspended if no values are available in the channel. If the channel is closed and the buffer is empty, a `ChannelException` is thrown. If the channel is closed but values remain in the buffer, they will be returned. ## Parameters **cancellationToken** : A cancellation token (`Completable`) that allows cancelling the wait on any condition. `null` — wait indefinitely (default). When the token completes, the operation is cancelled and a `AsyncCancellation` is thrown. To limit by time, you can use `Async\timeout()`. ## Return values The next value from the channel (`mixed`). ## Errors * Throws `Async\ChannelException` if the channel is closed and the buffer is empty. * Throws `Async\AsyncCancellation` if the cancellation token has been completed. ## Examples ### Example #1 Receiving values from a channel ```php send($i); } $channel->close(); }); spawn(function() use ($channel) { try { while (true) { $value = $channel->recv(); echo "Received: $value\n"; } } catch (\Async\ChannelException) { echo "Channel closed and empty\n"; } }); ``` ### Example #2 Receiving with a timeout ```php recv(Async\timeout(2000)); echo "Received: $value\n"; } catch (\Async\AsyncCancellation) { echo "No data received within 2 seconds\n"; } }); ``` ### Example #3 Receiving with a custom cancellation token ```php recv($cancel); echo "Received: $value\n"; } catch (\Async\AsyncCancellation) { echo "Receive cancelled\n"; } }); // Cancel from another coroutine spawn(function() use ($cancel) { Async\delay(500); $cancel->complete(null); }); ``` ## See also * [Channel::recvAsync](/en/docs/reference/channel/recv-async.html) — Non-blocking receive * [Channel::send](/en/docs/reference/channel/send.html) — Send a value to the channel * [Channel::isEmpty](/en/docs/reference/channel/is-empty.html) — Check if the buffer is empty * [Channel::getIterator](/en/docs/reference/channel/get-iterator.html) — Iterate over the channel using foreach --- --- url: https://true-async.github.io/en/docs/reference/channel/recv-async.md description: Non-blocking receive of a value from the channel, returns a Future. --- # Channel::recvAsync (PHP 8.6+, True Async 1.0) ```php public Channel::recvAsync(): Future ``` Performs a non-blocking receive of a value from the channel and returns a `Future` object that can be awaited later. Unlike `recv()`, this method **does not suspend** the current coroutine immediately. Instead, a `Future` is returned that will be resolved when a value becomes available. ## Return values A `Future` object that will resolve with the received value from the channel. ## Examples ### Example #1 Non-blocking receive ```php send('data A'); $channel->send('data B'); $channel->close(); }); spawn(function() use ($channel) { $futureA = $channel->recvAsync(); $futureB = $channel->recvAsync(); // Can perform other work while data is not yet needed doSomeWork(); echo await($futureA) . "\n"; // "data A" echo await($futureB) . "\n"; // "data B" }); ``` ### Example #2 Parallel receive from multiple channels ```php recvAsync(); $notifFuture = $notifications->recvAsync(); // Wait for the first available value from any channel [$result, $index] = awaitAnyOf($orderFuture, $notifFuture); echo "Received from channel #$index: $result\n"; }); ``` ## See also * [Channel::recv](/en/docs/reference/channel/recv.html) — Blocking receive * [Channel::sendAsync](/en/docs/reference/channel/send-async.html) — Non-blocking send * [await](/en/docs/reference/await.html) — Await a Future --- --- url: https://true-async.github.io/en/docs/reference/channel/send.md description: Send a value to the channel (blocking operation). --- # Channel::send (PHP 8.6+, True Async 1.0) ```php public Channel::send(mixed $value, ?Completable $cancellationToken = null): void ``` Sends a value to the channel. This is a blocking operation — the current coroutine is suspended if the channel cannot accept the value immediately. For a **rendezvous channel** (`capacity = 0`), the sender waits until another coroutine calls `recv()`. For a **buffered channel**, the sender waits only when the buffer is full. ## Parameters **value** : The value to send. Can be of any type. **cancellationToken** : A cancellation token (`Completable`) that allows cancelling the wait on any condition. `null` — wait indefinitely (default). When the token completes, the operation is cancelled and a `AsyncCancellation` is thrown. To limit by time, you can use `Async\timeout()`. ## Errors * Throws `Async\ChannelException` if the channel is closed. * Throws `Async\AsyncCancellation` if the cancellation token has been completed. ## Examples ### Example #1 Sending values to a channel ```php send('first'); // placed in the buffer $channel->send('second'); // waits for space to free up $channel->close(); }); spawn(function() use ($channel) { echo $channel->recv() . "\n"; // "first" echo $channel->recv() . "\n"; // "second" }); ``` ### Example #2 Sending with a timeout ```php send('data', Async\timeout(1000)); } catch (\Async\AsyncCancellation $e) { echo "Timeout: no one accepted the value within 1 second\n"; } }); ``` ### Example #3 Sending with a custom cancellation token ```php send('data', $cancel); } catch (\Async\AsyncCancellation $e) { echo "Send cancelled\n"; } }); // Cancel the operation from another coroutine spawn(function() use ($cancel) { Async\delay(500); $cancel->complete(null); }); ``` ## See also * [Channel::sendAsync](/en/docs/reference/channel/send-async.html) — Non-blocking send * [Channel::recv](/en/docs/reference/channel/recv.html) — Receive a value from the channel * [Channel::isFull](/en/docs/reference/channel/is-full.html) — Check if the buffer is full * [Channel::close](/en/docs/reference/channel/close.html) — Close the channel --- --- url: https://true-async.github.io/en/docs/reference/channel/send-async.md description: Non-blocking send of a value to the channel. --- # Channel::sendAsync (PHP 8.6+, True Async 1.0) ```php public Channel::sendAsync(mixed $value): bool ``` Performs a non-blocking attempt to send a value to the channel. Unlike `send()`, this method **never suspends** the coroutine. Returns `true` if the value was successfully sent (placed in the buffer or delivered to a waiting receiver). Returns `false` if the buffer is full or the channel is closed. ## Parameters **value** : The value to send. Can be of any type. ## Return values `true` — the value was successfully sent. `false` — the channel is full or closed, the value was not sent. ## Examples ### Example #1 Attempting a non-blocking send ```php sendAsync('a'); // true — buffer is empty $channel->sendAsync('b'); // true — space available $result = $channel->sendAsync('c'); // false — buffer is full echo $result ? "Sent" : "Channel full"; // "Channel full" ``` ### Example #2 Sending with availability check ```php sendAsync($item)) { // Buffer is full — fall back to blocking send $channel->send($item); } } $channel->close(); }); ``` ## See also * [Channel::send](/en/docs/reference/channel/send.html) — Blocking send * [Channel::isFull](/en/docs/reference/channel/is-full.html) — Check if the buffer is full * [Channel::isClosed](/en/docs/reference/channel/is-closed.html) — Check if the channel is closed --- --- url: https://true-async.github.io/en/docs/components/channels.md description: >- Channels in TrueAsync -- safe data transfer between coroutines, task queues, and backpressure. --- # Channels Channels are more useful for communication in a multithreaded environment than in a single-threaded one. They serve for safe data transfer from one coroutine to another. If you need to modify shared data, in a single-threaded environment it's simpler to pass an object to different coroutines than to create a channel. However, channels are useful in the following scenarios: * organizing a task queue with limits * organizing object pools (it's recommended to use the dedicated `Async\Pool` primitive) * synchronization For example, there are many URLs to crawl, but no more than N connections simultaneously: ```php use Async\Channel; use Async\Scope; const MAX_CONNECTIONS = 10; const MAX_QUEUE = 100; $tasks = new Scope(); $channel = new Channel(MAX_QUEUE); for($i = 0; $i < MAX_CONNECTIONS; $i++) { $tasks->spawn(function() use ($channel) { while (!$channel->isClosed()) { $url = $channel->recv(); $content = file_get_contents($url); echo "Fetched page {$url}, length: " . strlen($content) . "\n"; } }); } // Fill the channel with values for($i = 0; $i < MAX_CONNECTIONS * 2; $i++) { $channel->send("https://example.com/{$i}"); } ``` The `MAX_QUEUE` constant in this example acts as a limiter for the producer, creating backpressure -- a situation where the producer cannot send data until the consumer frees up space in the channel. ## Unbuffered Channel (Rendezvous) A channel with buffer size `0` works in rendezvous mode: `send()` blocks until another coroutine calls `recv()`, and vice versa. This ensures strict synchronization: ```php use Async\Channel; $ch = new Channel(0); // Rendezvous channel spawn(function() use ($ch) { echo "Sender: before send\n"; $ch->send("hello"); echo "Sender: send completed\n"; // Only after recv() }); spawn(function() use ($ch) { echo "Receiver: before recv\n"; $value = $ch->recv(); echo "Receiver: got $value\n"; }); ``` ## Cancellation The `recv()` and `send()` methods accept an optional cancellation token (`Completable`) that allows cancelling the wait on any condition. This is more flexible than a fixed timeout -- you can cancel an operation from another coroutine, on a signal, on an event, or by time: ```php use Async\Channel; use Async\AsyncCancellation; $ch = new Channel(0); // Cancellation by timeout spawn(function() use ($ch) { try { $ch->recv(Async\timeout(50)); // Wait no more than 50 ms } catch (AsyncCancellation $e) { echo "Nobody sent data within 50 ms\n"; } }); // Cancellation by a custom condition spawn(function() use ($ch) { $cancel = new \Async\Future(); spawn(function() use ($cancel) { // Cancel after 50 ms Async\delay(50); $cancel->complete(null); }); try { $ch->send("data", $cancel); } catch (AsyncCancellation $e) { echo "Nobody received the data -- operation cancelled\n"; } }); ``` ## Competing Receivers If multiple coroutines are waiting on `recv()` on the same channel, each value is received by **only one** of them. Values are not duplicated: ```php use Async\Channel; $ch = new Channel(0); // Sender spawn(function() use ($ch) { for ($i = 1; $i <= 3; $i++) { $ch->send($i); } $ch->close(); }); // Receiver A spawn(function() use ($ch) { foreach ($ch as $v) { echo "A received: $v\n"; } }); // Receiver B spawn(function() use ($ch) { foreach ($ch as $v) { echo "B received: $v\n"; } }); // Each value (1, 2, 3) will be received by only A or B, but not both ``` This pattern is useful for implementing worker pools, where multiple coroutines compete for tasks from a shared queue. --- --- url: https://true-async.github.io/en/tutors/07-channels.md description: >- Channel: a stream of values between coroutines, a worker pool, and backpressure. --- # Channels `Future` promises exactly one value. But what if there are many values, and they arrive gradually? Recall the CSV file from the first chapter. Now the task is more serious: while importing, every user's address needs to be checked against `GeoDirectory`. We already know how to use coroutines, so let's try the direct approach: ```php while (($row = fgetcsv($handle)) !== false) { spawn(checkAddress(...), $row[$addressIndex]); } ``` The file has a hundred thousand rows. This code will create a hundred thousand coroutines, and all of them will concurrently open a connection to `GeoDirectory`. Coroutines are cheap, but connections aren't. The service will choke, and our import along with it. What we want instead is for, say, ten coroutines to handle the checking, while the rest of the addresses wait their turn. We need a queue that some coroutines pull work from and others add work to. Such a queue is called a channel. ## Channel A channel connects coroutines directly, like a pipe: ```php use Async\Channel; use function Async\spawn; $channel = new Channel(5); spawn(function () use ($channel) { $channel->send('hello'); }); echo $channel->recv(); // hello ``` * **`send`** — puts a value into the channel. * **`recv`** — takes a value out of the channel. Both operations block, and that's the whole point. If the channel is empty, `recv` suspends the coroutine until data shows up. If the channel is full, it's `send` that gets suspended instead. The `5` in the constructor sets the buffer size: how many values the channel is willing to hold before anyone picks them up. ## Rendezvous What happens if you create a channel with a buffer of `0`? It has nowhere to store values, so `send` won't complete until another coroutine calls `recv`: ```php $ch = new Channel(0); spawn(function () use ($ch) { echo "before send\n"; $ch->send('hello'); echo "after send\n"; // runs only after recv }); spawn(function () use ($ch) { echo "before recv\n"; echo $ch->recv() . "\n"; }); ``` Such a channel is called a rendezvous: the sender and the receiver are forced to meet at the same point in time, hence the name. The difference from a buffered channel is subtler than it looks. A buffer means "sent" but not "received": `send` drops off a value and moves on, and the sender has no idea whether, or when, it gets picked up. A rendezvous guarantees delivery: once `send` returns, the value is already in the receiver's hands. That matters when a task can't be left sitting in a queue: for example, handing work to a worker only if it's free right now. Data is almost beside the point here: a rendezvous is pure synchronization. Two coroutines are guaranteed to meet at the same moment, even if all they were passing was `null`. ## Worker pool Let's assemble a solution for the import. Ten worker coroutines pull addresses out of a channel, while the main flow reads the file and feeds the channel: ```php use Async\Channel; use Async\ChannelException; use function Async\spawn; $queue = new Channel(100); for ($i = 0; $i < 10; $i++) { spawn(function () use ($queue) { foreach ($queue as $address) { checkAddress($address); } }); } while (($row = fgetcsv($handle)) !== false) { $queue->send($row[$addressIndex]); } $queue->close(); ``` Each value from the channel goes to exactly one worker, even if all ten are waiting on `recv`. Values are never duplicated, so the workers don't step on each other: the channel distributes the work among whoever is free on its own. However many rows are in the file, there are never more than ten connections to `GeoDirectory`. The number of workers is exactly the concurrency limit, and it's set by a single constant in the loop. ## Backpressure Why a buffer of exactly `100`? Imagine the workers check addresses slower than the main flow reads the file. Without a limit, the queue would keep growing until the entire hundred-thousand-row file sat in memory. With a buffer of `100`, something different happens: as soon as a hundred addresses pile up in the channel, `send` suspends the main flow. Reading the file pauses and resumes once the workers free up space. The producer automatically adjusts to the consumers' speed. This mechanism is called backpressure. Notice that we didn't program it ourselves: it falls out of the blocking nature of `send`. Just pick a buffer size, and everything else balances itself. ## Closing a channel Once the file is read, the workers keep waiting on `recv`. A familiar situation: in the chapter on cancellation, the progress coroutine kept spinning forever too. But here we don't need `cancel()` on every worker; a channel has a more precise tool: ```php $queue->close(); ``` `close` announces: no new values are coming. The workers first finish reading whatever is left in the buffer, and then `recv` throws `ChannelException`, ending the `while (true)` loop. Notice the order: closing doesn't cut the work short, it lets it finish properly. And one more familiar detail: `recv` and `send` accept the same cancellation token as the `await` from the timeouts chapter: ```php $address = $queue->recv(timeout(5000)); ``` If nothing shows up in the channel within five seconds, the worker gets an `AsyncCancellation` and can, for example, log a warning. ## Passing data or synchronizing? Let's take a closer look at what the channel was actually doing in this chapter. `recv` on an empty channel put a worker to sleep until work showed up. `send` on a full channel put the file-reading to sleep until the workers cleared the queue. The rendezvous brought two coroutines together at the same moment in time. Every blocking operation turned out to be a point where coroutines adjust to each other. And here it's worth noting a subtlety of the single-threaded world. For passing data as such, a channel isn't required: coroutines live in shared memory, and an object can simply be handed to them via `use`. Data races don't happen within a single thread; two coroutines are never executing at the exact same moment. A channel is used for something else: a bounded queue, distributing work among free workers, signaling "no more work" through `close`. So it's more accurate to think of a channel this way: it's a way of synchronizing coroutines that happens to move data along, not the other way around. In multi-threaded code, though, where coroutines are spread across different threads, a channel becomes indispensable in both roles: there's no shared memory there, and it remains the one safe bridge. Now we have two ways to link coroutines together. `Future` promises one value. A channel carries a whole stream of values and, along the way, sets a shared rhythm for the work: the producer doesn't know who will pick up the data or when, the consumer doesn't know where it came from, but nobody floods anybody with work. There's one nagging thought left. We launched ten workers and moved on. What if one of them dies with an exception partway through the import? Who's actually watching over all these coroutines? Let's find out in the next chapter. --- --- url: https://true-async.github.io/en/tutors-server/03-concurrency-inside.md description: TaskGroup in a handler, the PDO Pool under load, and request_context(). --- # Concurrency Inside a Request In the previous chapter `showProfile` loaded the profile in a single line, and I honestly pretended there was nothing behind it. Time to come clean. Behind it are three sources: user data from the database, orders from the database, reviews from an external API. Three independent trips for data. A familiar problem? Of course. This is chapter ten of the first series word for word, and the solution carries over without a single change: ```php use Async\TaskGroup; function showProfile(int $userId, HttpRequest $req, HttpResponse $res): void { $group = new TaskGroup(); $group->spawnWithKey('user', fn() => fetchUser($userId)); $group->spawnWithKey('orders', fn() => fetchOrders($userId)); $group->spawnWithKey('reviews', fn() => fetchReviews($userId)); $res->json($group->all()->await(timeout(2000))); } ``` The three requests go out concurrently. The page is assembled in the time of the slowest source, not the sum of all three. There's a timeout, there's fail-together, there's `CompositeException`. Here it's important to feel one thing: the server introduced no concurrency rules of its own. None whatsoever. The handler is an ordinary coroutine, and inside it everything you already know works. The server just opened a door through which HTTP requests step into the world you already know. ## The Pool Under Real Load Remember chapter nine of the first series? Ten import workers, one `PDO` object, tangled transactions, chaos. Back then it was a teaching example with ten coroutines. Well, forget about ten. In a server, there are as many concurrent database users as there are requests currently in flight. Twenty. Five hundred. As many as the traffic drives in, and you don't control that number. The only thing that saves you, you already know: ```php $pdo = new PDO($dsn, $user, $password, [ PDO::ATTR_POOL_ENABLED => true, PDO::ATTR_POOL_MIN => 4, PDO::ATTR_POOL_MAX => 16, ]); ``` The mechanics are the same as before: each coroutine owns a connection exclusively at its moment, transactions are pinned, and a dropped connection is quietly replaced with a fresh one. But `POOL_MAX` has a new job now. It's now a fuse between the storm and the database: a thousand concurrent requests will line up in a queue for sixteen connections. Agreed, that's better than a thousand connections arriving at PostgreSQL at once. ## Whose Request Is This? Chapter fifteen of the first series ended with the question of where to store the "current": the user, the locale, the request identifier. Back then we built the answer on contexts. The server carries this story through to the end, and does it beautifully. Each handler runs in its own scope. And the request scope has a context shared across the whole coroutine tree of that request: ```php use function Async\request_context; use function Async\spawn; $server->addHttpHandler(function (HttpRequest $req, HttpResponse $res) { request_context() ->set('request_id', $req->getHeader('x-request-id') ?? bin2hex(random_bytes(8))) ->set('user_id', authenticate($req)); // ... even ten levels of calls deeper ... }); function logInfo(string $message): void { $requestId = request_context()->find('request_id'); error_log("[$requestId] $message"); } ``` `logInfo` may be called from the handler. From a coroutine it spawned. From a coroutine inside a `TaskGroup` inside a service inside a repository. It doesn't matter: `request_context()` is one and the same everywhere, as long as we're inside this request. And the neighboring request, being processed interleaved with ours right now? It has its own. Three hundred concurrent requests, three hundred independent `request_id`s, zero global variables. The difference from `current_context()` can now be phrased in one line: that one is about a single coroutine, this one is about the whole request. ## Scope Cleans Up After the Request And the last consequence, the most invisible and the most valuable. Because the handler lives in a scope, the whole of chapter eight of the first series applies to the request automatically, without a single action on your part. Spawned a coroutine and forgot to await it? The request finishes, the scope cleans up. The client dropped the connection halfway? The server cancels the request scope, cancellation cooperatively reaches every child coroutine, and every `finally` releases its own. Remember how much discipline structured concurrency demanded? Here it stopped being discipline. It's now a property of the platform: the life of any request coroutine is bounded by the request itself. Full stop. That's it for the invisible part of the server. Next come bytes, and plenty of them: the client uploads a gigabyte file, and we hand back a two-gigabyte report. Where do we put all of it so we don't wake the OOM killer? The next chapter is about exactly that. --- --- url: https://true-async.github.io/en/tutors/12-iterate.md description: >- iterate(): concurrent collection traversal in a single line, generators, and early exit. --- # Concurrent Iterator Let's count how many times we've built the same pattern. In the channels chapter: a channel, ten workers, a `recv` loop. In the TaskGroup chapter: `concurrency: 10`, a `spawn` loop, `close()`. In the TaskSet chapter: the same thing, but consuming results. Every time, the task sounded the same: walk a collection, do concurrent work on each element, and never exceed the limit. A pattern this common deserves a function of its own: ```php use function Async\iterate; iterate($addresses, checkAddress(...), concurrency: 10); ``` And that's the whole worker pool. `iterate` calls the function for each element of the collection in its own coroutine, makes sure no more than ten run concurrently, and returns control once everything has been processed. ## A generator instead of an array Wait, what about reading the file? Collecting a hundred thousand addresses into an array just to call `iterate` would be a step backwards: the back-pressure from the channels chapter was what saved us memory. No problem: `iterate` accepts not just an array but any `Traversable`, including generators: ```php function addresses(string $path): Generator { $handle = fopen($path, 'r'); $header = fgetcsv($handle); $addressIndex = array_search('address', $header); while (($row = fgetcsv($handle)) !== false) { yield $row[$addressIndex]; } fclose($handle); } iterate(addresses('users.csv'), checkAddress(...), concurrency: 10); ``` The generator reads the file lazily, one line at a time. `iterate` only pulls the next address once a slot frees up, so the file is never held in memory all at once. It's the same back-pressure a buffered channel gives you, except now it's invisible: all that's left is a single line expressing the intent. ## Early exit The handler function can stop the whole traversal by returning `false`: ```php $broken = 0; iterate(addresses('users.csv'), function (string $address) use (&$broken) { if (!checkAddress($address)) { $broken++; } if ($broken >= 100) { return false; // the file is corrupt, no point continuing } }, concurrency: 10); ``` A hundred invalid addresses in a row is a reliable sign we've been handed the wrong file. No new elements start after the `false`, the coroutines already running finish what they're doing, and `iterate` returns. Exceptions are stricter, and you already know the rule from Scope: an error in any handler stops the traversal, cancels the remaining coroutines, and propagates outward. Fail-together by default: ```php try { iterate(addresses('users.csv'), checkAddress(...), concurrency: 10); } catch (RemoteApiException $e) { echo "Import aborted: {$e->getMessage()}\n"; } ``` There's no magic behind it: inside `iterate` lives an ordinary child scope from chapter eight, and it's the one keeping things tidy. ## A ladder of abstractions Over six chapters a whole ladder has taken shape, and it's worth looking back over it once more, from the bottom up: * **A channel + Scope** — the primitives. Any topology: pools, pipelines, rendezvous, supervisors. Maximum control, maximum code. * **`TaskGroup` / `TaskSet`** — ready-made assemblies for a set of tasks, when you need results: all of them, the first, the first successful, or one by one as they become ready. * **`iterate()`** — a single line, when you don't need the result and just need to walk a collection concurrently. The higher up the ladder, the less code and the narrower the scenario. Start at the top: if `iterate` is enough, there's no reason to build a group; if a group isn't enough, drop down to channels. At the bottom you can assemble any construction you like; at the top, everything is already assembled for you. Notice what happened to our import: chapter after chapter it kept shrinking, until it finally fit on a single line. That's exactly how it should be. Concurrency stops being an event and becomes an ordinary tool, just like `foreach`. But one question from chapter nine is still hanging. For PDO, the connection pool is built into the core, while `checkAddress` talks to `GeoDirectory` over HTTP. Its connections are worth reusing too, and not just its: sockets, clients, heavy objects in general. Do we really have to hand-build a pool out of a channel every single time? Fortunately, no, and the next chapter will show you why. --- --- url: https://true-async.github.io/en/tutors-laravel/02-pool-transactions.md description: >- PDO Pool under Eloquent and CoroutineTransactions: why the nested transaction counter can't stay on a Connection property. --- # Connection Pool and Transactions `Auth`, `session`, `request`: we settled those in the previous chapter, context plus a proxy, and the framework stopped confusing requests. The database seems even simpler: chapter nine of the core series already solved it, the `PDO Pool` hands each coroutine its own connection and takes it back on its own. What could possibly go wrong for `Eloquent` here? It can. The pool transparently manages the connection. But it knows nothing about another piece of state that `Laravel` keeps right next to that connection: the transaction nesting counter. ## Where `transactionLevel()` Lives Inside `Illuminate\Database\Connection` there's an ordinary instance property: ```php protected $transactions = 0; ``` `beginTransaction()` increments it, `commit()` and `rollBack()` decrement it. As long as one `Connection` serves one process, this makes perfect sense: one property, one transaction. But `PDO Pool` operates a layer below `Connection`. It swaps the physical connection underneath the object, while the `Connection` object itself, the one `$this->transactions` is attached to, is still one single instance shared across the whole `DatabaseManager`. Let's replay the scenario from the previous chapter, this time with the database: ```php $server->addHttpHandler(function ($request, $response) { DB::transaction(function () use ($request) { Order::create(['user_id' => $request->getQueryParam('u')]); delay(30); // the coroutine falls asleep right inside the transaction }); $response->json(['ok' => true]); }); ``` Two requests enter `DB::transaction()` concurrently. Both physical connections are honestly handed out by the pool, each its own. But `$this->transactions` for both is the same number on the same `Connection` object. The first coroutine bumps the counter to `1`, then sleeps. The second bumps it too, but now to `2`, even though for it this should have been an outer transaction at level `1`. `commit()` in the first coroutine multiplies against the wrong nesting level, and Laravel silently issues a `SAVEPOINT` where it shouldn't, or commits a transaction early that a neighboring coroutine is still holding open. ## Same Recipe, Different Scope: Coroutine, Not the Request Tree In the previous chapter `auth` state lived in scope context because it's shared across every coroutine of one request. The transaction counter is built differently: `PDO Pool` hands out a physical connection per *coroutine*, not per whole request (a parallel `TaskGroup` inside a handler gets two separate connections from the pool). So the counter has to live in coroutine context, not scope context: ```php trait CoroutineTransactions { private const CTX_TRANSACTIONS = 'db.transactions'; public function transactionLevel() { if ($this->isAsyncMode()) { return coroutine_context()->find(self::CTX_TRANSACTIONS) ?? 0; } return $this->transactions; } private function setTransactionLevel(int $level): void { if ($this->isAsyncMode()) { coroutine_context()->set(self::CTX_TRANSACTIONS, $level, replace: true); } else { $this->transactions = $level; } } // beginTransaction(), commit(), rollBack(), and the error handlers // are overridden the same way: instead of reading and writing // $this->transactions, they go through setTransactionLevel()/transactionLevel(). } ``` `coroutine_context()` versus `current_context()`, that's exactly the boundary discussed back in the `Context` chapter of the core series: the first is private to a single coroutine, the second is shared across a request's whole coroutine tree. The choice here isn't a matter of style, it's mandatory: mix them up and two parallel trips to the database inside one request start sharing someone else's transaction counter again, the hole just moves up a floor. The trait doesn't rewrite `Connection` wholesale, it surgically intercepts the methods that touch `$this->transactions`, and it's attached to a specific connection class: ```php class AsyncPgsqlConnection extends PostgresConnection { use CoroutineTransactions; } ``` There's a separate class per `DBMS`, `AsyncPgsqlConnection`, `AsyncMySqlConnection`, `AsyncMariaDbConnection`, `AsyncSqliteConnection`, `AsyncSqlServerConnection`, because Laravel's parent classes already differ, while the counter-isolation trait is the same one for all of them. ## Why You Can't Just Scope the Whole `DatabaseManager` The temptation is there: since we already know how to hide a service behind context (`ScopedServiceProxy` from the previous chapter), why not do the same for `db`? The reason it doesn't happen is fairly unpleasant. `DatabaseServiceProvider::boot()` writes this once at startup: ```php Model::setConnectionResolver($app['db']); ``` That's a static property on the `Model` class itself, shared by every model and every request. If `db` resolved differently for each scope, this static reference would keep pointing at the `DatabaseManager` of whichever request created it first. Once that request's scope finishes and cleans up, the object `Model::$resolver` points to gets garbage collected, and the static property is left pointing at dead memory. The result isn't "wrong data", it's the whole process crashing. So `db` stays a singleton, as it always was. Physical connection isolation is `PDO Pool`'s job at the `C` level, not the dependency container's. Transaction counter isolation is the trait's job at the level of a single coroutine. Two narrow, precise tools instead of one big refactor that would additionally bring the server down. We've dealt with request state and transaction state. Laravel handles ordinary HTTP responses on its own, buffering them whole. But what if the response isn't a one-off, but a stream: a progress bar to the browser, or a call from a neighboring service over gRPC? That's where we head in the next chapter. --- --- url: https://true-async.github.io/en/tutors/15-context.md description: 'Context: where to keep "the current one" once global variables stop working.' --- # Context Classic PHP lived its whole life by one simple rule: one process, one request. A whole culture grew out of that rule: global variables, static properties, singletons. The current user? The static property `Auth::$user`. A request ID for logging? A global variable. It worked flawlessly, because "the current one" really was one single thing for the whole process. TrueAsync abolished that rule. Now hundreds of coroutines live mixed together in a single process, serving different users. Assign `Auth::$user` in one coroutine, and the next one to wake up will read it back, and you're lucky if that's even the same request. A familiar story: the same thing happened in chapter nine, when ten workers shared a single `PDO`. Races without threads, only now it's in global state. Pass everything as parameters instead? Honest, but merciless: you'd have to thread an authorization token through twenty function signatures, when only one of them actually needs it. What you really want is storage tied not to the process, but to the logical thread of execution. And we already have a structure fit for the job: coroutines and scopes already form a tree. ## Storage on a tree `Async\Context` is a key-value store attached to a scope or to a coroutine. A scope's context is available to all of its coroutines: ```php use function Async\current_context; // middleware, start of request handling current_context() ->set('request_id', bin2hex(random_bytes(8))) ->set('user_id', $userId); ``` And from there, anywhere, at any call depth, without a single extra parameter: ```php function logInfo(string $message): void { $requestId = current_context()->find('request_id'); error_log("[$requestId] $message"); } ``` Notice `find`: it looks for the key in the current context, and if it doesn't find it, climbs up the scope tree. Child scopes automatically see their parents' data, so a coroutine started deep inside a handler will find the `request_id` set at the very start of the request. It's the same mechanism as Go's `context.Context`, except you don't have to look it up and pass it around by hand: the tree is already there. Requests no longer get in each other's way: each one has its own scope, and therefore its own context. A thousand concurrent requests, a thousand independent `request_id`s, and not a single global variable. ## Three levels Context exists on three levels, from widest to narrowest: ```php use function Async\root_context; use function Async\current_context; use function Async\coroutine_context; root_context(); // the whole process: configuration, shared by everyone current_context(); // the current scope: request, user, locale coroutine_context(); // just this coroutine: private data ``` `root_context` is the legitimate home for whatever used to rightfully be a global variable: the application name, settings. You address it explicitly: `root_context()->find('app_name')`. `coroutine_context` is the opposite pole: its data is invisible to anyone but the coroutine itself, even its neighbors in the same scope. Between the two, `find` stitches the scope levels together: didn't find it locally, ask the parent. And one more pleasant detail: ```php current_context()->set('user_id', 42); current_context()->set('user_id', 7); // AsyncException: key already exists ``` Overwriting is forbidden unless you explicitly ask for it (`replace: true`). The context guards itself against the very disease of global variables this chapter started with: someone, somewhere, quietly overwriting a value. ## The end of the journey Fifteen chapters ago we launched two functions "at the same time" and were surprised to see their output interleave. Since then, a whole system has taken shape, and every level of it has its own concern: * **Coroutines, `spawn`, `await`** — the unit of concurrency and a promise of a result. * **Cancellation, timeouts, exceptions** — the interruption contract: nothing runs forever, and nothing dies silently. * **`Future` and channels** — connections: a single result, and a stream of values with synchronization. * **`Scope`, `TaskGroup`, `TaskSet`, `iterate`** — structure: every coroutine has an owner, and every group has a waiting strategy. * **Pools** — resource discipline: few connections, many coroutines. * **Threads** — parallelism for computation, without shared memory. * **`Context`** — data tied to execution, not to the process. Notice what we never had to learn: mutexes, semaphores, data races, callbacks, or coloring functions async versus sync. The code stayed ordinary, sequential PHP that simply stopped sitting idle. From here, two paths. For hands-on practice, head to the [documentation](/en/docs.html), where every component is taken apart down to the last screw. For understanding how it all works underneath, head to the [architecture](/en/architecture.html). Or best of all, just take your slowest script and see what a single `spawn` does to it. --- --- url: https://true-async.github.io/en/docs/components/context.md description: >- Context in TrueAsync -- storing data in scope hierarchy, local and inherited values, analogous to Go context.Context. --- # Context: Execution Contexts ## Why This is Needed There is an `API` with a service class that needs to perform actions tied to an authorization token. However, passing the token to every method of the service is a bad idea. In `PHP`, this problem is solved through global variables or static class properties. But in an asynchronous environment, where a single process can handle different requests, this approach won't work, because at the time of the call, it's unknown which request is being handled. `Async\Context` allows storing data associated with a coroutine or `Scope` and building application logic based on the execution context. ## What is Context `Async\Context` is a key-value store bound to a `Scope` or coroutine. Contexts form a hierarchy: when reading a value, the search goes up the scope tree. This is analogous to `context.Context` in `Go` or `CoroutineContext` in `Kotlin`. A mechanism for passing data through the hierarchy without explicitly passing parameters. ## Three Levels of Context `TrueAsync` provides three functions for accessing contexts: ```php ``` ### current\_context() Returns the context of the current `Scope`. If the context hasn't been created yet, it creates one automatically. Values set here are visible to all coroutines in this Scope. ### coroutine\_context() Returns the context of the current coroutine. This is a **private** context belonging only to this coroutine. Other coroutines cannot see data set here. A coroutine context has no Scope above it, so `find()` on it searches only its own keys: use `current_context()` or `root_context()` to look up the Scope tree. ### root\_context() Returns the global context, shared across the entire request. Values here are visible via `find()` from any context. ## Keys A key can be a **string** or an **object**: ```php set('request_id', 'abc-123'); // Object as key (useful for unique tokens) $key = new stdClass(); $ctx->set($key, 'value'); ?> ``` Object keys are stored by reference in the context, which guarantees their uniqueness. ## Reading: Local and Hierarchical ### find() / get() / has() -- Hierarchical Search Searches for a value first in the current context, then in the parent, and so on up to the root: ```php set('app_name', 'MyApp'); $scope = new Async\Scope(); spawn(function() { // find() searches up the hierarchy $name = current_context()->find('app_name'); echo $name; // "MyApp" -- found in root_context }); ?> ``` ### findLocal() / getLocal() / hasLocal() -- Current Context Only Searches for a value **only** in the current context, without going up the hierarchy: ```php set('app_name', 'MyApp'); $local = current_context()->findLocal('app_name'); // null -- this value is not set in the current Scope $inherited = current_context()->find('app_name'); // "MyApp" -- found in parent scope ?> ``` ## Writing and Deleting ### set() ```php set('key', 'value'); // Repeated set without replace -- error $ctx->set('key', 'new_value'); // Error: Context key already exists and replace is false // With explicit replace = true $ctx->set('key', 'new_value', replace: true); // OK ``` The `set()` method returns `$this`, allowing method chaining: ```php set('user_id', 42) ->set('request_id', 'abc-123') ->set('locale', 'en'); ?> ``` ### unset() ```php unset('key'); ``` The `unset()` method also returns `$this`. ## Practical Examples ### Passing a Request ID ```php set('request_id', bin2hex(random_bytes(8))); // Any coroutine in this scope can read it spawn(function() { $requestId = current_context()->find('request_id'); // Use in logging error_log("[$requestId] Processing request..."); }); ?> ``` ### Coroutine Context as Private Storage ```php set('step', 1); // ... perform work $step = coroutine_context()->getLocal('step'); }); $c2 = spawn(function() { // Cannot see 'step' from c1 $step = coroutine_context()->findLocal('step'); // null }); ?> ``` ### Configuration via root\_context ```php set('db_host', 'localhost') ->set('cache_ttl', 3600); // Available from any coroutine spawn(function() { $dbHost = current_context()->find('db_host'); // "localhost" }); ?> ``` ## See Also * [Scope](/en/docs/components/scope.html) -- managing coroutine lifetimes * [Coroutines](/en/docs/components/coroutines.html) -- the basic unit of concurrency * [current\_context()](/en/docs/reference/current-context.html) -- getting the current Scope's context --- --- url: https://true-async.github.io/en/docs/reference/context/find.md description: Find a value by key in the current or parent context. --- # Context::find (PHP 8.6+, True Async 1.0) ```php public Context::find(string|object $key): mixed ``` Searches for a value by key in the current context. If the key is not found, the search continues up the hierarchy of parent contexts. Returns `null` if the value is not found at any level. This is a safe search method: it never throws an exception when a key is missing. ## Parameters **key** : The key to search for. Can be a string or an object. When using an object as a key, the search is performed by object reference. ## Return Value The value associated with the key, or `null` if the key is not found in the current or any parent context. ## Examples ### Example #1 Searching for a value by string key ```php set('request_id', 'abc-123'); spawn(function() { // Child coroutine finds value from parent context $id = current_context()->find('request_id'); echo $id . "\n"; // "abc-123" // Searching for a non-existent key returns null $missing = current_context()->find('nonexistent'); var_dump($missing); // NULL }); ``` ### Example #2 Searching for a value by object key ```php set($loggerKey, new MyLogger()); spawn(function() use ($loggerKey) { // Search by object key reference $logger = current_context()->find($loggerKey); $logger->info('Message from child coroutine'); }); ``` ### Example #3 Hierarchical search ```php set('app_name', 'MyApp'); spawn(function() { // Level 1: add own value current_context()->set('user_id', 42); spawn(function() { // Level 2: search for values from all levels echo current_context()->find('user_id') . "\n"; // 42 echo current_context()->find('app_name') . "\n"; // "MyApp" }); }); ``` ## See Also * [Context::get](/en/docs/reference/context/get.html) --- Get value (throws exception if missing) * [Context::has](/en/docs/reference/context/has.html) --- Check if key exists * [Context::findLocal](/en/docs/reference/context/find-local.html) --- Search only in local context * [Context::set](/en/docs/reference/context/set.html) --- Set value in context --- --- url: https://true-async.github.io/en/docs/reference/context/find-local.md description: Find a value only in the local context (without searching parent contexts). --- # Context::findLocal (PHP 8.6+, True Async 1.0) ```php public Context::findLocal(string|object $key): mixed ``` Searches for a value by key **only** in the current (local) context. Unlike `find()`, this method does not search up the hierarchy of parent contexts. Returns `null` if the key is not found at the current level. ## Parameters **key** : The key to search for. Can be a string or an object. ## Return Value The value associated with the key in the local context, or `null` if the key is not found. ## Examples ### Example #1 Difference between find and findLocal ```php set('config', 'global_value'); spawn(function() { current_context()->set('local_data', 'local_value'); // find() searches up the hierarchy echo current_context()->find('config') . "\n"; // "global_value" // findLocal() searches only at the current level echo current_context()->findLocal('local_data') . "\n"; // "local_value" var_dump(current_context()->findLocal('config')); // NULL }); ``` ### Example #2 Using with an object key ```php set($parentKey, 'parent_value'); spawn(function() use ($parentKey, $localKey) { current_context()->set($localKey, 'child_value'); // Object key from parent is not visible through findLocal var_dump(current_context()->findLocal($parentKey)); // NULL var_dump(current_context()->findLocal($localKey)); // "child_value" }); ``` ### Example #3 Overriding a parent value ```php set('timeout', 5000); spawn(function() { // Check if the value is overridden locally if (current_context()->findLocal('timeout') === null) { // Use inherited value, but can override current_context()->set('timeout', 3000); } echo current_context()->findLocal('timeout') . "\n"; // 3000 }); ``` ## See Also * [Context::find](/en/docs/reference/context/find.html) --- Search with hierarchical traversal * [Context::getLocal](/en/docs/reference/context/get-local.html) --- Get local value (throws exception) * [Context::hasLocal](/en/docs/reference/context/has-local.html) --- Check key in local context --- --- url: https://true-async.github.io/en/docs/reference/context/get.md description: Get a value from context. Throws an exception if the key is not found. --- # Context::get (PHP 8.6+, True Async 1.0) ```php public Context::get(string|object $key): mixed ``` Gets a value by key from the current context. If the key is not found at the current level, the search continues up the hierarchy of parent contexts. Unlike `find()`, this method throws an exception if the key is not found at any level. Use `get()` when the presence of a value is a mandatory requirement. ## Parameters **key** : The key to search for. Can be a string or an object. When using an object as a key, the search is performed by object reference. ## Return Value The value associated with the key. ## Errors * Throws `Async\ContextException` if the key is not found in the current or any parent context. ## Examples ### Example #1 Getting a required value ```php set('db_connection', $pdo); spawn(function() { // Get a value that must exist $db = current_context()->get('db_connection'); $db->query('SELECT 1'); }); ``` ### Example #2 Handling a missing key ```php get('missing_key'); } catch (\Async\ContextException $e) { echo "Key not found: " . $e->getMessage() . "\n"; } ``` ### Example #3 Using an object key ```php set($dbKey, new PDO('sqlite::memory:')); spawn(function() use ($dbKey) { // Object key ensures uniqueness without name conflicts $pdo = current_context()->get($dbKey); $pdo->exec('CREATE TABLE test (id INTEGER)'); }); ``` ## See Also * [Context::find](/en/docs/reference/context/find.html) --- Safe search (returns null) * [Context::has](/en/docs/reference/context/has.html) --- Check if key exists * [Context::getLocal](/en/docs/reference/context/get-local.html) --- Get value only from local context * [Context::set](/en/docs/reference/context/set.html) --- Set value in context --- --- url: https://true-async.github.io/en/docs/reference/context/get-local.md description: Get a value only from the local context. Throws an exception if not found. --- # Context::getLocal (PHP 8.6+, True Async 1.0) ```php public Context::getLocal(string|object $key): mixed ``` Gets a value by key **only** from the current (local) context. Unlike `get()`, this method does not search in parent contexts. If the key is not found at the current level, it throws an exception. ## Parameters **key** : The key to search for. Can be a string or an object. ## Return Value The value associated with the key in the local context. ## Errors * Throws `Async\ContextException` if the key is not found in the local context. ## Examples ### Example #1 Getting a local value ```php set('task_id', 42); // Value is set locally — getLocal works $taskId = current_context()->getLocal('task_id'); echo "Task: {$taskId}\n"; // "Task: 42" }); ``` ### Example #2 Exception when accessing an inherited key ```php set('parent_value', 'hello'); spawn(function() { // find() would find the value in the parent echo current_context()->find('parent_value') . "\n"; // "hello" // getLocal() throws an exception — value is not in the local context try { current_context()->getLocal('parent_value'); } catch (\Async\ContextException $e) { echo "Not found locally: " . $e->getMessage() . "\n"; } }); ``` ### Example #3 Using with an object key ```php set($key, ['user' => 'admin', 'role' => 'superuser']); $session = current_context()->getLocal($key); echo "User: " . $session['user'] . "\n"; // "User: admin" }); ``` ## See Also * [Context::get](/en/docs/reference/context/get.html) --- Get value with hierarchical search * [Context::findLocal](/en/docs/reference/context/find-local.html) --- Safe search in local context * [Context::hasLocal](/en/docs/reference/context/has-local.html) --- Check key in local context --- --- url: https://true-async.github.io/en/docs/reference/context/has.md description: Check if a key exists in the current or parent context. --- # Context::has (PHP 8.6+, True Async 1.0) ```php public Context::has(string|object $key): bool ``` Checks whether a value with the specified key exists in the current context or in one of the parent contexts. The search is performed up the hierarchy. ## Parameters **key** : The key to check. Can be a string or an object. ## Return Value `true` if the key is found in the current or any parent context, `false` otherwise. ## Examples ### Example #1 Checking for a key before use ```php set('locale', 'ru_RU'); spawn(function() { if (current_context()->has('locale')) { $locale = current_context()->find('locale'); echo "Locale: {$locale}\n"; // "Locale: ru_RU" } else { echo "Locale not set, using default\n"; } }); ``` ### Example #2 Checking with an object key ```php set($cacheKey, new RedisCache()); if (current_context()->has($cacheKey)) { echo "Cache is available\n"; } $unknownKey = new stdClass(); var_dump(current_context()->has($unknownKey)); // false ``` ### Example #3 Hierarchical check ```php set('global_flag', true); spawn(function() { current_context()->set('local_flag', true); spawn(function() { var_dump(current_context()->has('global_flag')); // true (from root) var_dump(current_context()->has('local_flag')); // true (from parent) var_dump(current_context()->has('unknown')); // false }); }); ``` ## See Also * [Context::find](/en/docs/reference/context/find.html) --- Find value by key * [Context::get](/en/docs/reference/context/get.html) --- Get value (throws exception) * [Context::hasLocal](/en/docs/reference/context/has-local.html) --- Check only in local context --- --- url: https://true-async.github.io/en/docs/reference/context/has-local.md description: Check if a key exists only in the local context. --- # Context::hasLocal (PHP 8.6+, True Async 1.0) ```php public Context::hasLocal(string|object $key): bool ``` Checks whether a value with the specified key exists **only** in the current (local) context. Unlike `has()`, this method does not search in parent contexts. ## Parameters **key** : The key to check. Can be a string or an object. ## Return Value `true` if the key is found in the local context, `false` otherwise. ## Examples ### Example #1 Difference between has and hasLocal ```php set('inherited_key', 'value'); spawn(function() { current_context()->set('local_key', 'value'); // has() searches up the hierarchy var_dump(current_context()->has('inherited_key')); // true var_dump(current_context()->has('local_key')); // true // hasLocal() checks only the current level var_dump(current_context()->hasLocal('inherited_key')); // false var_dump(current_context()->hasLocal('local_key')); // true }); ``` ### Example #2 Checking with an object key ```php set($configKey, ['debug' => true]); spawn(function() use ($configKey) { $localKey = new stdClass(); current_context()->set($localKey, 'local'); var_dump(current_context()->hasLocal($configKey)); // false var_dump(current_context()->hasLocal($localKey)); // true }); ``` ### Example #3 Conditional initialization of a local value ```php hasLocal('request_count')) { current_context()->set('request_count', 0); } echo current_context()->getLocal('request_count') . "\n"; // 0 }); ``` ## See Also * [Context::has](/en/docs/reference/context/has.html) --- Check with hierarchical traversal * [Context::findLocal](/en/docs/reference/context/find-local.html) --- Find value in local context * [Context::getLocal](/en/docs/reference/context/get-local.html) --- Get local value (throws exception) --- --- url: https://true-async.github.io/en/docs/reference/context/set.md description: Set a value in the context by key. --- # Context::set (PHP 8.6+, True Async 1.0) ```php public Context::set(string|object $key, mixed $value, bool $replace = false): Context ``` Sets a value in the current context with the specified key. By default, if the key already exists, the value is **not overwritten**. To force overwriting, use the `replace = true` parameter. The method returns the `Context` object, allowing method chaining. ## Parameters **key** : The key to set the value for. Can be a string or an object. Object keys are useful for avoiding name conflicts between libraries. **value** : The value to store. Can be of any type. **replace** : If `false` (default) --- do not overwrite an existing value. If `true` --- overwrite the value even if the key already exists. ## Return Value The `Context` object for method chaining. ## Examples ### Example #1 Setting values with string keys ```php set('request_id', 'req-001') ->set('user_id', 42) ->set('locale', 'ru_RU'); echo current_context()->find('request_id') . "\n"; // "req-001" echo current_context()->find('user_id') . "\n"; // 42 ``` ### Example #2 Behavior without overwriting ```php set('mode', 'production'); // Setting again without replace — value does NOT change current_context()->set('mode', 'debug'); echo current_context()->find('mode') . "\n"; // "production" // With replace = true — value is overwritten current_context()->set('mode', 'debug', replace: true); echo current_context()->find('mode') . "\n"; // "debug" ``` ### Example #3 Object keys for library isolation ```php set(LoggerContext::$key, new FileLogger('/var/log/app.log')) ->set(CacheContext::$key, new RedisCache('localhost:6379')); spawn(function() { $logger = current_context()->find(LoggerContext::$key); $cache = current_context()->find(CacheContext::$key); $logger->info('Cache initialized'); }); ``` ### Example #4 Passing context to child coroutines ```php set('trace_id', bin2hex(random_bytes(8))) ->set('service', 'api-gateway'); // Child coroutines inherit values through find() spawn(function() { $traceId = current_context()->find('trace_id'); echo "Processing request: {$traceId}\n"; // Child coroutine adds its own value current_context()->set('handler', 'user_controller'); }); ``` ## See Also * [Context::unset](/en/docs/reference/context/unset.html) --- Remove value by key * [Context::find](/en/docs/reference/context/find.html) --- Find value by key * [Context::get](/en/docs/reference/context/get.html) --- Get value (throws exception) * [current\_context()](/en/docs/reference/current-context.html) --- Get the current Scope context --- --- url: https://true-async.github.io/en/docs/reference/context/unset.md description: Remove a value by key from the context. --- # Context::unset (PHP 8.6+, True Async 1.0) ```php public Context::unset(string|object $key): Context ``` Removes a value by key from the current context. The removal only affects the local context --- values in parent contexts are not changed. The method returns the `Context` object, allowing method chaining. ## Parameters **key** : The key to remove. Can be a string or an object. ## Return Value The `Context` object for method chaining. ## Examples ### Example #1 Removing a value from context ```php set('temp_data', 'value') ->set('keep_data', 'preserve'); echo current_context()->find('temp_data') . "\n"; // "value" // Remove temporary data current_context()->unset('temp_data'); var_dump(current_context()->find('temp_data')); // NULL echo current_context()->find('keep_data') . "\n"; // "preserve" ``` ### Example #2 Removing with an object key ```php set($tokenKey, 'secret-token-123'); echo current_context()->find($tokenKey) . "\n"; // "secret-token-123" // Remove sensitive data after use current_context()->unset($tokenKey); var_dump(current_context()->find($tokenKey)); // NULL ``` ### Example #3 Removal does not affect the parent context ```php set('shared', 'parent_value'); spawn(function() { // Child context sees value from parent echo current_context()->find('shared') . "\n"; // "parent_value" // Set a local value with the same key current_context()->set('shared', 'child_value', replace: true); echo current_context()->findLocal('shared') . "\n"; // "child_value" // Remove the local value current_context()->unset('shared'); // After removing local value — parent value is visible again through find() echo current_context()->find('shared') . "\n"; // "parent_value" var_dump(current_context()->findLocal('shared')); // NULL }); ``` ### Example #4 Method chaining with unset ```php set('a', 1) ->set('b', 2) ->set('c', 3); // Clear multiple keys with chaining current_context() ->unset('a') ->unset('b'); var_dump(current_context()->find('a')); // NULL var_dump(current_context()->find('b')); // NULL echo current_context()->find('c') . "\n"; // 3 ``` ## See Also * [Context::set](/en/docs/reference/context/set.html) --- Set value in context * [Context::find](/en/docs/reference/context/find.html) --- Find value by key * [Context::findLocal](/en/docs/reference/context/find-local.html) --- Find value in local context --- --- url: https://true-async.github.io/en/contributing.md description: How to help TrueAsync grow — code, documentation, testing and community --- ## Project Status `PHP TrueAsync` is an unofficial project to modify the `PHP` core! The `RFC` being proposed is currently in an uncertain situation, and it is unclear whether it will be accepted in the future. Nevertheless, as the author of the project, I believe that having a **choice** is an important condition for **progress**. The `PHP TrueAsync` project is open for ideas, suggestions and help. If you'd like to discuss something — write on the project forum or contact me personally: ## Ways to Contribute ### Code * **Bug fixes** — check [open issues](https://github.com/true-async/php-async/issues){:target="\_blank"} labeled `good first issue` to get started * **New features** — discuss your idea in [Discussions](https://github.com/orgs/true-async/discussions){:target="\_blank"} before implementing * **Code review** — help review pull requests, it's a valuable contribution ### Documentation * **Corrections** — found an inaccuracy? Click "Edit this page" at the bottom of any page * **Translations** — help translate the documentation into other languages * **Examples** — write API usage examples for real-world scenarios * **Tutorials** — create step-by-step guides for specific tasks ### Testing * **Build testing** — try [installing TrueAsync](/en/download.html) on your system and report any issues * **Writing tests** — increase test coverage for the existing API * **Load testing** — help find performance bottlenecks ### Community * **Answer questions** on [GitHub Discussions](https://github.com/orgs/true-async/discussions){:target="\_blank"} and [Discord](https://discord.gg/yqBQPBHKp5){:target="\_blank"} * **Spread the word** — talks, articles, blog posts * **Report bugs** — a detailed bug report saves hours of development time ## Getting Started ### 1. Fork the Repository ```bash git clone https://github.com/true-async/php-src.git cd php-src ``` ### 2. Set Up Your Environment Follow the [build instructions](/en/download.html) for your platform. For development, a debug build is recommended: ```bash ./buildconf ./configure --enable-async --enable-debug make -j$(nproc) ``` ### 3. Create a Branch ```bash git checkout -b feature/my-improvement ``` ### 4. Make Your Changes * Follow the project's code style * Add tests for new functionality * Make sure existing tests pass: `make test` ### 5. Submit a Pull Request * Describe **what** and **why** you changed * Reference related issues * Be prepared for discussion and revisions ## Repository Structure | Repository | Description | |------------|-------------| | [php-src](https://github.com/true-async/php-src){:target="\_blank"} | PHP core with Async API | | [php-async](https://github.com/true-async/php-async){:target="\_blank"} | Extension with implementation | | [true-async.github.io](https://github.com/true-async/true-async.github.io){:target="\_blank"} | This documentation site | ## Guidelines * **Small PRs are better than large ones** — one PR solves one task * **Discuss before implementing** — for major changes, create an issue or discussion first * **Write tests** — code without tests is harder to accept * **Document your work** — update docs when changing the API ## Get in Touch * **GitHub Discussions** — [questions and ideas](https://github.com/orgs/true-async/discussions){:target="\_blank"} * **Discord** — [live chat](https://discord.gg/yqBQPBHKp5){:target="\_blank"} * **Issues** — [bug reports](https://github.com/true-async/php-async/issues){:target="\_blank"} Thank you for contributing to the future of PHP! --- --- url: https://true-async.github.io/en/architecture/waker.md description: >- Internal design of the Waker -- the link between coroutines and events: statuses, resume_when, timeout, error delivery. --- # Coroutine Wait and Wake-up Mechanism To store the waiting context of a coroutine, `TrueAsync` uses the `Waker` structure. It serves as the link between a coroutine and the events it is subscribed to. Thanks to the `Waker`, a coroutine always knows exactly which events it is waiting for. ## Waker Structure For memory optimization purposes, the `waker` is integrated directly into the coroutine structure (`zend_coroutine_t`), which avoids additional allocations and simplifies memory management, although a `zend_async_waker_t *waker` pointer is used in the code for backward compatibility. The `Waker` holds a list of awaited events and aggregates the wait result or exception. ```c struct _zend_async_waker_s { ZEND_ASYNC_WAKER_STATUS status; // Events the coroutine is waiting for HashTable events; // Events that fired on the last iteration HashTable *triggered_events; // Wake-up result zval result; // Error (if wake-up was caused by an error) zend_object *error; // Creation point (for debugging) zend_string *filename; uint32_t lineno; // Destructor zend_async_waker_dtor dtor; }; ``` ## Waker Statuses At each stage of a coroutine's life, the `Waker` is in one of five states: ![Waker Statuses](/diagrams/en/architecture-waker/waker-states.svg) ```c typedef enum { ZEND_ASYNC_WAKER_NO_STATUS, // Waker is not active ZEND_ASYNC_WAKER_WAITING, // Coroutine is waiting for events ZEND_ASYNC_WAKER_QUEUED, // Coroutine is queued for execution ZEND_ASYNC_WAKER_IGNORED, // Coroutine was skipped ZEND_ASYNC_WAKER_RESULT // Result is available } ZEND_ASYNC_WAKER_STATUS; ``` A coroutine starts with `NO_STATUS` -- the `Waker` exists but is not active; the coroutine is executing. When the coroutine calls `SUSPEND()`, the `Waker` transitions to `WAITING` and begins monitoring events. When one of the events fires, the `Waker` transitions to `QUEUED`: the result is saved, and the coroutine is placed in the `Scheduler` queue awaiting a context switch. The `IGNORED` status is needed for cases when a coroutine is already in the queue but must be destroyed. In that case, the `Scheduler` does not launch the coroutine but immediately finalizes its state. When the coroutine wakes up, the `Waker` transitions to the `RESULT` state. At this point, `waker->error` is transferred to `EG(exception)`. If there are no errors, the coroutine can use `waker->result`. For example, `result` is what the `await()` function returns. ## Creating a Waker ```c // Get waker (create if it doesn't exist) zend_async_waker_t *waker = zend_async_waker_define(coroutine); // Reinitialize waker for a new wait zend_async_waker_t *waker = zend_async_waker_new(coroutine); // With timeout and cancellation zend_async_waker_t *waker = zend_async_waker_new_with_timeout( coroutine, timeout_ms, cancellation_event); ``` `zend_async_waker_new()` destructs the existing waker and resets it to its initial state. This allows reusing the waker without allocations. ## Subscribing to Events The zend\_async\_API.c module provides several ready-made functions to bind a coroutine to an event: ```c zend_async_resume_when( coroutine, // Which coroutine to wake event, // Which event to subscribe to trans_event, // Transfer event ownership callback, // Callback function event_callback // Coroutine callback (or NULL) ); ``` `resume_when` is the main subscription function. It creates a `zend_coroutine_event_callback_t`, binds it to the event and to the coroutine's waker. As the callback function, you can use one of three standard ones, depending on how you want to wake the coroutine: ```c // Successful result zend_async_waker_callback_resolve(event, callback, result, exception); // Cancellation zend_async_waker_callback_cancel(event, callback, result, exception); // Timeout zend_async_waker_callback_timeout(event, callback, result, exception); ``` --- --- url: https://true-async.github.io/en/docs/reference/coroutine-context.md description: coroutine_context() — get the private context of the current coroutine. --- # coroutine\_context (PHP 8.6+, True Async 1.0) `coroutine_context()` — Returns the `Async\Context` object bound to the current coroutine. ## Description ```php coroutine_context(): Async\Context ``` Returns the **private** context of the current coroutine. Data set here is not visible to other coroutines. If the context for the coroutine has not been created yet, it is created automatically. ## Return Values An `Async\Context` object. ## Examples ```php set('step', 1); // Later in the same coroutine $step = coroutine_context()->getLocal('step'); // 1 }); spawn(function() { // Cannot see 'step' from another coroutine $step = coroutine_context()->findLocal('step'); // null }); ?> ``` ## See Also * [current\_context()](/en/docs/reference/current-context.html) — Scope context * [root\_context()](/en/docs/reference/root-context.html) — global context * [Context](/en/docs/components/context.html) — the context concept --- --- url: https://true-async.github.io/en/docs/reference/coroutine/as-hi-priority.md description: Mark the coroutine as high-priority for the scheduler. --- # Coroutine::asHiPriority (PHP 8.6+, True Async 1.0) ```php public Coroutine::asHiPriority(): Coroutine ``` Marks the coroutine as high-priority. The scheduler will give preference to such coroutines when selecting the next task for execution. The method returns the same coroutine object, enabling a fluent interface. ## Return Value `Coroutine` -- the same coroutine object (fluent interface). ## Examples ### Example #1 Setting priority ```php asHiPriority(); ``` ### Example #2 Fluent interface ```php criticalOperation())->asHiPriority() ); ``` ## See Also * [spawn()](/en/docs/reference/spawn.html) -- Create a coroutine --- --- url: https://true-async.github.io/en/docs/reference/coroutine/cancel.md description: Cancel coroutine execution. --- # Coroutine::cancel (PHP 8.6+, True Async 1.0) ```php public Coroutine::cancel(?Async\AsyncCancellation $cancellation = null): void ``` Cancels the coroutine execution. The coroutine will receive an `AsyncCancellation` exception at the next suspension point (`suspend`, `await`, `delay`, etc.). Cancellation works cooperatively -- the coroutine is not interrupted instantly. If the coroutine is inside `protect()`, cancellation is deferred until the protected section completes. ## Parameters **cancellation** : The exception serving as the cancellation reason. If `null`, a default `AsyncCancellation` is created. ## Examples ### Example #1 Basic cancellation ```php getMessage() . "\n"; } }); suspend(); $coroutine->cancel(); await($coroutine); ``` ### Example #2 Cancellation with reason ```php cancel(new \Async\AsyncCancellation("Timeout exceeded")); try { await($coroutine); } catch (\Async\AsyncCancellation $e) { echo $e->getMessage() . "\n"; // "Timeout exceeded" } ``` ### Example #3 Cancellation before start ```php cancel(); try { await($coroutine); } catch (\Async\AsyncCancellation $e) { echo "Coroutine cancelled before start\n"; } ``` ## See Also * [Coroutine::isCancelled](/en/docs/reference/coroutine/is-cancelled.html) -- Check cancellation * [Coroutine::isCancellationRequested](/en/docs/reference/coroutine/is-cancellation-requested.html) -- Check cancellation request * [Cancellation](/en/docs/components/cancellation.html) -- Cancellation concept * [protect()](/en/docs/reference/protect.html) -- Protected section --- --- url: https://true-async.github.io/en/docs/reference/coroutine/on-finally.md description: Register a handler to be called when the coroutine completes. --- # Coroutine::finally (PHP 8.6+, True Async 1.0) ```php public Coroutine::finally(\Closure $callback): void ``` Registers a callback function that will be called when the coroutine completes, regardless of the outcome (success, error, or cancellation). If the coroutine has already completed at the time `finally()` is called, the callback will execute immediately. Multiple handlers can be registered -- they execute in the order they were added. ## Parameters **callback** : The handler function. Receives the coroutine object as an argument. ## Examples ### Example #1 Basic usage ```php finally(function() { echo "Coroutine completed\n"; }); await($coroutine); ``` ### Example #2 Resource cleanup ```php query('SELECT * FROM users'); }); $coroutine->finally(function() use ($connection) { $connection->close(); echo "Connection closed\n"; }); $result = await($coroutine); ``` ### Example #3 Multiple handlers ```php "done"); $coroutine->finally(fn() => echo "Handler 1\n"); $coroutine->finally(fn() => echo "Handler 2\n"); $coroutine->finally(fn() => echo "Handler 3\n"); await($coroutine); // Output: // Handler 1 // Handler 2 // Handler 3 ``` ### Example #4 Registration after completion ```php 42); await($coroutine); // Coroutine already completed -- callback executes immediately $coroutine->finally(function() { echo "Called immediately\n"; }); ``` ## See Also * [Coroutine::isCompleted](/en/docs/reference/coroutine/is-completed.html) -- Check completion * [Coroutine::getResult](/en/docs/reference/coroutine/get-result.html) -- Get the result --- --- url: https://true-async.github.io/en/docs/reference/coroutine/get-awaiting-info.md description: Get information about what the coroutine is awaiting. --- # Coroutine::getAwaitingInfo (PHP 8.6+, True Async 1.0) ```php public Coroutine::getAwaitingInfo(): array ``` Returns debug information about what the coroutine is currently awaiting. Useful for diagnosing stuck coroutines. ## Return Value `array` -- an array with awaiting information. In the current build the extension does not fill it, so the method always returns an empty array; use `getSuspendLocation()` and `getTrace()` to see where a coroutine is stopped. ## Examples ### Example #1 Diagnosing awaiting state ```php isSuspended()) { $info = $coro->getAwaitingInfo(); echo "Coroutine #{$coro->getId()} is awaiting:\n"; print_r($info); } } ``` ## See Also * [Coroutine::isSuspended](/en/docs/reference/coroutine/is-suspended.html) -- Check suspension * [Coroutine::getTrace](/en/docs/reference/coroutine/get-trace.html) -- Call stack * [Coroutine::getSuspendLocation](/en/docs/reference/coroutine/get-suspend-location.html) -- Suspension location --- --- url: https://true-async.github.io/en/docs/reference/coroutine/get-context.md description: Get the local context of a coroutine. --- # Coroutine::getContext (PHP 8.6+, True Async 1.0) ```php public Coroutine::getContext(): Async\Context ``` Returns the local context of the coroutine. The context is created lazily on first access. The context allows storing data bound to a specific coroutine and passing it to child coroutines. ## Return Value `Async\Context` -- the coroutine's context object. ## Examples ### Example #1 Accessing the context ```php getContext(); ``` ## See Also * [Context](/en/docs/components/context.html) -- Context concept * [current\_context()](/en/docs/reference/current-context.html) -- Get the current coroutine's context --- --- url: https://true-async.github.io/en/docs/reference/coroutine/get-exception.md description: Get the exception that occurred in a coroutine. --- # Coroutine::getException (PHP 8.6+, True Async 1.0) ```php public Coroutine::getException(): mixed ``` Returns the exception that occurred in the coroutine. If the coroutine completed successfully or has not yet completed, returns `null`. If the coroutine was cancelled, returns an `AsyncCancellation` object. ## Return Value `mixed` -- the exception or `null`. * `null` -- if the coroutine has not completed or completed successfully * `Throwable` -- if the coroutine completed with an error * `AsyncCancellation` -- if the coroutine was cancelled ## Errors Throws `RuntimeException` if the coroutine is currently running. ## Examples ### Example #1 Successful completion ```php getException()); // NULL ``` ### Example #2 Completion with error ```php getException(); var_dump($exception instanceof RuntimeException); // bool(true) var_dump($exception->getMessage()); // string(10) "test error" ``` ### Example #3 Cancelled coroutine ```php cancel(); suspend(); $exception = $coroutine->getException(); var_dump($exception instanceof \Async\AsyncCancellation); // bool(true) ``` ## See Also * [Coroutine::getResult](/en/docs/reference/coroutine/get-result.html) -- Get the result * [Coroutine::isCancelled](/en/docs/reference/coroutine/is-cancelled.html) -- Check cancellation * [Exceptions](/en/docs/components/exceptions.html) -- Error handling --- --- url: https://true-async.github.io/en/docs/reference/coroutine/get-id.md description: Get the unique identifier of a coroutine. --- # Coroutine::getId (PHP 8.6+, True Async 1.0) ```php public Coroutine::getId(): int ``` Returns the unique integer identifier of the coroutine. The identifier is unique within the current PHP process. ## Return Value `int` -- unique coroutine identifier. ## Examples ### Example #1 Basic usage ```php getId(); $id2 = $coroutine2->getId(); var_dump(is_int($id1)); // bool(true) var_dump($id1 !== $id2); // bool(true) ``` ### Example #2 Logging with identifier ```php getId(); echo "[coro:$id] Task '$name' started\n"; \Async\delay(1000); echo "[coro:$id] Task '$name' completed\n"; }); } ``` ## See Also * [Coroutine::getSpawnLocation](/en/docs/reference/coroutine/get-spawn-location.html) -- Coroutine creation location * [current\_coroutine()](/en/docs/reference/current-coroutine.html) -- Get the current coroutine --- --- url: https://true-async.github.io/en/docs/reference/coroutine/get-result.md description: Get the result of coroutine execution. --- # Coroutine::getResult (PHP 8.6+, True Async 1.0) ```php public Coroutine::getResult(): mixed ``` Returns the result of the coroutine execution. If the coroutine has not yet completed, returns `null`. **Important:** this method does not wait for the coroutine to complete. Use `await()` for waiting. ## Return Value `mixed` -- the coroutine result or `null` if the coroutine has not yet completed. ## Examples ### Example #1 Basic usage ```php getResult()); // NULL // Wait for completion await($coroutine); var_dump($coroutine->getResult()); // string(11) "test result" ``` ### Example #2 Checking with isCompleted() ```php 42); suspend(); // let the coroutine complete if ($coroutine->isCompleted()) { echo "Result: " . $coroutine->getResult() . "\n"; } ``` ## See Also * [Coroutine::getException](/en/docs/reference/coroutine/get-exception.html) -- Get the exception * [Coroutine::isCompleted](/en/docs/reference/coroutine/is-completed.html) -- Check completion * [await()](/en/docs/reference/await.html) -- Wait for the result --- --- url: >- https://true-async.github.io/en/docs/reference/coroutine/get-spawn-file-and-line.md description: Get the file and line where the coroutine was created. --- # Coroutine::getSpawnFileAndLine (PHP 8.6+, True Async 1.0) ```php public Coroutine::getSpawnFileAndLine(): array ``` Returns the file and line number where `spawn()` was called to create this coroutine. ## Return Value `array` -- an array of two elements: * `[0]` -- file name (`string` or `null`) * `[1]` -- line number (`int`) ## Examples ### Example #1 Basic usage ```php "test"); // line 5 [$file, $line] = $coroutine->getSpawnFileAndLine(); echo "File: $file\n"; // /app/script.php echo "Line: $line\n"; // 5 ``` ## See Also * [Coroutine::getSpawnLocation](/en/docs/reference/coroutine/get-spawn-location.html) -- Creation location as a string * [Coroutine::getSuspendFileAndLine](/en/docs/reference/coroutine/get-suspend-file-and-line.html) -- Suspension file and line --- --- url: https://true-async.github.io/en/docs/reference/coroutine/get-spawn-location.md description: Get the coroutine creation location as a string. --- # Coroutine::getSpawnLocation (PHP 8.6+, True Async 1.0) ```php public Coroutine::getSpawnLocation(): string ``` Returns the coroutine creation location in the format `"file:line"`. If the information is unavailable, returns `"unknown"`. ## Return Value `string` -- a string like `"/app/script.php:42"` or `"unknown"`. ## Examples ### Example #1 Debug output ```php "test"); echo "Created at: " . $coroutine->getSpawnLocation() . "\n"; // Output: "Created at: /app/script.php:5" ``` ### Example #2 Logging all coroutines ```php Async\delay(1000)); spawn(fn() => Async\delay(2000)); foreach (get_coroutines() as $coro) { echo "Coroutine #{$coro->getId()} created at {$coro->getSpawnLocation()}\n"; } ``` ## See Also * [Coroutine::getSpawnFileAndLine](/en/docs/reference/coroutine/get-spawn-file-and-line.html) -- File and line as an array * [Coroutine::getSuspendLocation](/en/docs/reference/coroutine/get-suspend-location.html) -- Suspension location * [get\_coroutines()](/en/docs/reference/get-coroutines.html) -- All active coroutines --- --- url: >- https://true-async.github.io/en/docs/reference/coroutine/get-suspend-file-and-line.md description: Get the file and line where the coroutine is suspended. --- # Coroutine::getSuspendFileAndLine (PHP 8.6+, True Async 1.0) ```php public Coroutine::getSuspendFileAndLine(): array ``` Returns the file and line number where the coroutine was suspended (or was last suspended). ## Return Value `array` -- an array of two elements: * `[0]` -- file name (`string` or `null`) * `[1]` -- line number (`int`) ## Examples ### Example #1 Basic usage ```php getSuspendFileAndLine(); echo "Suspended at: $file:$line\n"; // /app/script.php:7 ``` ## See Also * [Coroutine::getSuspendLocation](/en/docs/reference/coroutine/get-suspend-location.html) -- Suspension location as a string * [Coroutine::getSpawnFileAndLine](/en/docs/reference/coroutine/get-spawn-file-and-line.html) -- Creation file and line --- --- url: >- https://true-async.github.io/en/docs/reference/coroutine/get-suspend-location.md description: Get the coroutine suspension location as a string. --- # Coroutine::getSuspendLocation (PHP 8.6+, True Async 1.0) ```php public Coroutine::getSuspendLocation(): string ``` Returns the coroutine suspension location in the format `"file:line"`. If the information is unavailable, returns `"unknown"`. ## Return Value `string` -- a string like `"/app/script.php:42"` or `"unknown"`. ## Examples ### Example #1 Diagnosing a stuck coroutine ```php isSuspended()) { echo "Coroutine #{$coro->getId()} waiting at: {$coro->getSuspendLocation()}\n"; } } ``` ## See Also * [Coroutine::getSuspendFileAndLine](/en/docs/reference/coroutine/get-suspend-file-and-line.html) -- File and line as an array * [Coroutine::getSpawnLocation](/en/docs/reference/coroutine/get-spawn-location.html) -- Creation location * [Coroutine::getTrace](/en/docs/reference/coroutine/get-trace.html) -- Full call stack --- --- url: https://true-async.github.io/en/docs/reference/coroutine/get-trace.md description: Get the call stack of a suspended coroutine. --- # Coroutine::getTrace (PHP 8.6+, True Async 1.0) ```php public Coroutine::getTrace( int $options = DEBUG_BACKTRACE_PROVIDE_OBJECT, int $limit = 0 ): ?array ``` Returns the call stack (backtrace) of a suspended coroutine. If the coroutine is not suspended (not yet started, currently running, or completed), returns `null`. ## Parameters **options** : A bitmask of options, similar to `debug_backtrace()`: * `DEBUG_BACKTRACE_PROVIDE_OBJECT` -- include `$this` in the trace * `DEBUG_BACKTRACE_IGNORE_ARGS` -- do not include function arguments **limit** : Maximum number of stack frames. `0` -- no limit. ## Return Value `?array` -- an array of stack frames or `null` if the coroutine is not suspended. ## Examples ### Example #1 Getting the stack of a suspended coroutine ```php getTrace(); if ($trace !== null) { foreach ($trace as $frame) { echo ($frame['file'] ?? '?') . ':' . ($frame['line'] ?? '?'); echo ' ' . ($frame['function'] ?? '') . "\n"; } } ``` ### Example #2 Trace for a completed coroutine -- null ```php "test"); // Before start -- null var_dump($coroutine->getTrace()); // NULL await($coroutine); // After completion -- null var_dump($coroutine->getTrace()); // NULL ``` ## See Also * [Coroutine::isSuspended](/en/docs/reference/coroutine/is-suspended.html) -- Check suspension * [Coroutine::getSuspendLocation](/en/docs/reference/coroutine/get-suspend-location.html) -- Suspension location * [Coroutine::getSpawnLocation](/en/docs/reference/coroutine/get-spawn-location.html) -- Creation location --- --- url: >- https://true-async.github.io/en/docs/reference/coroutine/is-cancellation-requested.md description: Check whether cancellation has been requested for the coroutine. --- # Coroutine::isCancellationRequested (PHP 8.6+, True Async 1.0) ```php public Coroutine::isCancellationRequested(): bool ``` Checks whether cancellation has been requested for the coroutine. Unlike `isCancelled()`, returns `true` immediately after `cancel()` is called, even if the coroutine is still executing inside `protect()`. ## Return Value `bool` -- `true` if cancellation has been requested. ## Examples ### Example #1 Difference from isCancelled() ```php isCancellationRequested()); // bool(false) $coroutine->cancel(); // Immediately after cancel() var_dump($coroutine->isCancellationRequested()); // bool(true) var_dump($coroutine->isCancelled()); // bool(false) -- still in protect() ``` ## See Also * [Coroutine::isCancelled](/en/docs/reference/coroutine/is-cancelled.html) -- Check completed cancellation * [Coroutine::cancel](/en/docs/reference/coroutine/cancel.html) -- Cancel the coroutine * [protect()](/en/docs/reference/protect.html) -- Protected section --- --- url: https://true-async.github.io/en/docs/reference/coroutine/is-cancelled.md description: Check whether the coroutine has been cancelled. --- # Coroutine::isCancelled (PHP 8.6+, True Async 1.0) ```php public Coroutine::isCancelled(): bool ``` Checks whether the coroutine has been cancelled **and** completed. Returns `true` only when the cancellation has fully finished. If the coroutine is inside `protect()`, `isCancelled()` will return `false` until the protected section completes, even if `cancel()` has already been called. To check for a cancellation request, use `isCancellationRequested()`. ## Return Value `bool` -- `true` if the coroutine has been cancelled and completed. ## Examples ### Example #1 Basic cancellation ```php cancel(); suspend(); // let the cancellation complete var_dump($coroutine->isCancelled()); // bool(true) var_dump($coroutine->isCompleted()); // bool(true) ``` ### Example #2 Deferred cancellation with protect() ```php cancel(); // Cancellation requested but not yet completed var_dump($coroutine->isCancellationRequested()); // bool(true) var_dump($coroutine->isCancelled()); // bool(false) suspend(); // let protect() complete var_dump($coroutine->isCancelled()); // bool(true) ``` ## See Also * [Coroutine::isCancellationRequested](/en/docs/reference/coroutine/is-cancellation-requested.html) -- Check cancellation request * [Coroutine::cancel](/en/docs/reference/coroutine/cancel.html) -- Cancel the coroutine * [Cancellation](/en/docs/components/cancellation.html) -- Cancellation concept --- --- url: https://true-async.github.io/en/docs/reference/coroutine/is-completed.md description: Check whether the coroutine has completed. --- # Coroutine::isCompleted (PHP 8.6+, True Async 1.0) ```php public Coroutine::isCompleted(): bool ``` Checks whether the coroutine has finished execution. A coroutine is considered completed on successful completion, on completion with an error, or on cancellation. ## Return Value `bool` -- `true` if the coroutine has finished execution. ## Examples ### Example #1 Checking completion ```php isCompleted()); // bool(false) await($coroutine); var_dump($coroutine->isCompleted()); // bool(true) ``` ### Example #2 Non-blocking readiness check ```php file_get_contents('https://api1.example.com')), spawn(fn() => file_get_contents('https://api2.example.com')), ]; // Wait until all are completed while (true) { $allDone = true; foreach ($tasks as $task) { if (!$task->isCompleted()) { $allDone = false; break; } } if ($allDone) break; suspend(); } ``` ## See Also * [Coroutine::getResult](/en/docs/reference/coroutine/get-result.html) -- Get the result * [Coroutine::getException](/en/docs/reference/coroutine/get-exception.html) -- Get the exception * [Coroutine::isCancelled](/en/docs/reference/coroutine/is-cancelled.html) -- Check cancellation --- --- url: https://true-async.github.io/en/docs/reference/coroutine/is-queued.md description: Check whether the coroutine is in the scheduler queue. --- # Coroutine::isQueued (PHP 8.6+, True Async 1.0) ```php public Coroutine::isQueued(): bool ``` Checks whether the coroutine is in the scheduler queue for execution. ## Return Value `bool` -- `true` if the coroutine is in the queue. ## Examples ### Example #1 Queue state ```php isQueued()); // bool(true) -- waiting to start suspend(); // let the scheduler start the coroutine // Coroutine started but remains in queue after internal suspend() var_dump($coroutine->isStarted()); // bool(true) ``` ## See Also * [Coroutine::isStarted](/en/docs/reference/coroutine/is-started.html) -- Check if started * [Coroutine::isSuspended](/en/docs/reference/coroutine/is-suspended.html) -- Check suspension --- --- url: https://true-async.github.io/en/docs/reference/coroutine/is-running.md description: Check whether the coroutine is currently executing. --- # Coroutine::isRunning (PHP 8.6+, True Async 1.0) ```php public Coroutine::isRunning(): bool ``` Checks whether the coroutine is currently executing. A coroutine is considered running if it has been started and has not yet completed. ## Return Value `bool` -- `true` if the coroutine is running and not completed. ## Examples ### Example #1 Checking execution state ```php isRunning()); // bool(true) return "done"; }); // Outside -- coroutine is suspended or not yet started var_dump($coroutine->isRunning()); // bool(false) ``` ## See Also * [Coroutine::isStarted](/en/docs/reference/coroutine/is-started.html) -- Check if started * [Coroutine::isSuspended](/en/docs/reference/coroutine/is-suspended.html) -- Check suspension * [Coroutine::isCompleted](/en/docs/reference/coroutine/is-completed.html) -- Check completion --- --- url: https://true-async.github.io/en/docs/reference/coroutine/is-started.md description: Check whether the coroutine has been started by the scheduler. --- # Coroutine::isStarted (PHP 8.6+, True Async 1.0) ```php public Coroutine::isStarted(): bool ``` Checks whether the coroutine has been started by the scheduler. A coroutine is considered started after the scheduler begins its execution. ## Return Value `bool` -- `true` if the coroutine has been started. ## Examples ### Example #1 Checking before and after start ```php isStarted()); // bool(false) -- still in queue suspend(); // let the scheduler start the coroutine var_dump($coroutine->isStarted()); // bool(true) await($coroutine); var_dump($coroutine->isStarted()); // bool(true) -- still true after completion ``` ## See Also * [Coroutine::isQueued](/en/docs/reference/coroutine/is-queued.html) -- Check queue status * [Coroutine::isRunning](/en/docs/reference/coroutine/is-running.html) -- Check if currently running * [Coroutine::isCompleted](/en/docs/reference/coroutine/is-completed.html) -- Check completion --- --- url: https://true-async.github.io/en/docs/reference/coroutine/is-suspended.md description: Check whether the coroutine is suspended. --- # Coroutine::isSuspended (PHP 8.6+, True Async 1.0) ```php public Coroutine::isSuspended(): bool ``` Checks whether the coroutine is suspended. A coroutine becomes suspended when `suspend()` is called, during I/O operations, or while waiting with `await()`. ## Return Value `bool` -- `true` if the coroutine is suspended. ## Examples ### Example #1 Checking suspension ```php isSuspended()); // bool(true) var_dump($coroutine->isStarted()); // bool(true) var_dump($coroutine->isCompleted()); // bool(false) ``` ## See Also * [Coroutine::isRunning](/en/docs/reference/coroutine/is-running.html) -- Check execution * [Coroutine::getTrace](/en/docs/reference/coroutine/get-trace.html) -- Call stack of a suspended coroutine * [suspend()](/en/docs/reference/suspend.html) -- Suspend the current coroutine --- --- url: https://true-async.github.io/en/architecture/scheduler-reactor.md description: >- Internal design of the coroutine scheduler and event reactor -- queues, context switching, libuv, fiber pool. --- # Coroutines, Scheduler, and Reactor `Scheduler` and `Reactor` are the two main components of the runtime. `Scheduler` manages the coroutine queue and context switching, while `Reactor` handles `I/O` events through the `Event loop`. ![Scheduler and Reactor Interaction](/diagrams/en/architecture-scheduler-reactor/architecture.svg) ## Scheduler ### Scheduler Coroutine and Minimizing Context Switches In many coroutine implementations, the `scheduler` uses a separate thread or at least a separate execution context. A coroutine calls `yield`, control passes to the `scheduler`, which picks the next coroutine and switches to it. This results in **two** context switches per `suspend`/`resume`: coroutine -> scheduler -> coroutine. In `TrueAsync`, the `Scheduler` has **its own coroutine** (`ZEND_ASYNC_SCHEDULER`) with a dedicated context. When all user coroutines are sleeping and the queue is empty, control is passed to this coroutine, where the main loop runs: `reactor tick`, `microtasks`. Because coroutines use a full execution context (stack + registers), context switching takes roughly 10-20 ns on modern `x86`. Therefore, `TrueAsync` optimizes the number of switches by allowing some operations to execute directly in the current coroutine's context, without switching to the scheduler. When a coroutine calls a `SUSPEND()` operation, `scheduler_next_tick()` is called directly in the current coroutine's context -- a function that performs one scheduler tick: microtasks, reactor, queue check. If there is a ready coroutine in the queue, the `Scheduler` switches to it **directly**, bypassing its own coroutine. This is one `context switch` instead of two. Moreover, if the next coroutine in the queue hasn't started yet and the current one has already finished, no switch is needed at all -- the new coroutine receives the current context. Switching to the `Scheduler` coroutine (via `switch_to_scheduler()`) occurs **only** if: * The coroutine queue is empty and the reactor needs to wait for events * Switching to another coroutine failed * A deadlock is detected ### Main Loop ![Scheduler Main Loop](/diagrams/en/architecture-scheduler-reactor/scheduler-loop.svg) On each tick, the scheduler performs: 1. **Microtasks** -- processing the `microtasks` queue (small tasks without context switching) 2. **Coroutine queue** -- extracting the next coroutine from the `coroutine_queue` 3. **Context switching** -- `zend_fiber_switch_context()` to the selected coroutine 4. **Result handling** -- checking the coroutine's status after return 5. **Reactor** -- if the queue is empty, calling `ZEND_ASYNC_REACTOR_EXECUTE(no_wait)` ### Microtasks Not every action deserves a coroutine. Sometimes you need to do something quick between switches: update a counter, send a notification, release a resource. Creating a coroutine for this is excessive, yet the action needs to be performed as soon as possible. This is where microtasks come in handy -- lightweight handlers that execute directly in the current coroutine's context, without switching. Microtasks must be lightweight, fast handlers since they get direct access to the scheduler's loop. In early versions of `TrueAsync`, microtasks could reside in PHP-land, but due to strict rules and performance considerations, the decision was made to keep this mechanism for C code only. ```c struct _zend_async_microtask_s { zend_async_microtask_handler_t handler; zend_async_microtask_handler_t dtor; bool is_cancelled; uint32_t ref_count; }; ``` In `TrueAsync`, microtasks are processed via a FIFO queue before each coroutine switch. If a microtask throws an exception, processing is interrupted. After execution, the microtask is immediately removed from the queue, and its active reference count is decremented by one. Microtasks are used in scenarios such as the concurrent iterator, allowing iteration to automatically transfer to another coroutine if the previous one entered a waiting state. ### Coroutine Priorities Under the hood, `TrueAsync` uses the simplest type of queue: a circular buffer. This is probably the best solution in terms of the balance between simplicity, performance, and functionality. There is no guarantee that the queue algorithm won't change in the future. That said, there are rare occasions when coroutine priority matters. Currently, two priorities are used: ```c typedef enum { ZEND_COROUTINE_NORMAL = 0, ZEND_COROUTINE_HI_PRIORITY = 255 } zend_coroutine_priority; ``` High-priority coroutines are placed **at the head** of the queue during `enqueue`. Extraction always happens from the head. No complex scheduling, just insertion order. This is a deliberate simple approach: two levels cover real-world needs, while complex priority queues (as in `RTOS`) would add overhead unjustified in the context of PHP applications. ### Suspend and Resume ![Suspend and Resume Operations](/diagrams/en/architecture-scheduler-reactor/suspend-resume.svg) `Suspend` and `Resume` operations are the core tasks of the `Scheduler`. When a coroutine calls `suspend`, the following happens: 1. The coroutine's `waker` events are started (`start_waker_events`). Only at this moment do timers begin ticking and poll objects start listening on descriptors. Before calling `suspend`, events are not active -- this allows preparing all subscriptions first, then starting the wait with a single call. 2. **Without a context switch**, `scheduler_next_tick()` is called: * Microtasks are processed * A `reactor tick` is performed (if enough time has passed) * If there is a ready coroutine in the queue, `execute_next_coroutine()` switches to it * If the queue is empty, `switch_to_scheduler()` switches to the `scheduler` coroutine 3. When control returns, the coroutine wakes up with the `waker` object that holds the `suspend` result. **Fast return path**: if during `start_waker_events` an event has already fired (e.g., a `Future` is already completed), the coroutine **is not suspended at all** -- the result is available immediately. Therefore, `await` on a completed `Future` does not trigger `suspend` and does not cause a context switch, returning the result directly. ## Context Pool A context is a full `C stack` (`EG(fiber_stack_size)` by default). Since stack creation is an expensive operation, `TrueAsync` strives to optimize memory management. We account for the memory usage pattern: coroutines are constantly dying and being created. The pool pattern is ideal for this scenario! ```c struct _async_fiber_context_s { zend_fiber_context context; // Native C fiber (stack + registers) zend_vm_stack vm_stack; // Zend VM stack zend_execute_data *execute_data;// Current execute_data uint8_t flags; // Fiber state }; ``` Instead of constantly creating and destroying memory, the Scheduler returns contexts to the pool and reuses them over and over. The pool size follows the load. A minimum of 4 contexts is always kept. When a coroutine finishes and the pool holds fewer contexts than there are coroutines waiting in the queue, its context is returned to the pool; otherwise the context is destroyed. So the pool grows under pressure and shrinks back when the queue drains, which bounds both `mmap`/`mprotect` latency and memory footprint. ### Switch Handlers In `PHP`, many subsystems rely on a simple assumption: code executes from start to finish without interruption. The output buffer (`ob_start`), object destructors, global variables -- all of this works linearly: start -> end. Coroutines break this model. A coroutine can sleep in the middle of its work and wake up after thousands of other operations. Between `LEAVE` and `ENTER` on the same thread, dozens of other coroutines will have run. `Switch Handlers` are hooks bound to a **specific coroutine**. Unlike microtasks (which fire on any switch), a `switch handler` is called only on enter and exit of "its" coroutine: ```c typedef bool (*zend_coroutine_switch_handler_fn)( zend_coroutine_t *coroutine, bool is_enter, // true = enter, false = exit bool is_finishing // true = coroutine is finishing // return: true = keep handler, false = remove ); ``` The return value controls the handler's lifetime: * `true` -- the `handler` stays and will be called again. * `false` -- the `Scheduler` will remove it. The `Scheduler` calls handlers at three points: ```c ZEND_COROUTINE_ENTER(coroutine) // Coroutine received control ZEND_COROUTINE_LEAVE(coroutine) // Coroutine yielded control (suspend) ZEND_COROUTINE_FINISH(coroutine) // Coroutine is finishing permanently ``` #### Example: Output Buffer The `ob_start()` function uses a single handler stack. When a coroutine calls `ob_start()` and then goes to sleep, another coroutine may see the other's buffer if nothing is done. (By the way, **Fiber** does not handle `ob_start()` properly.) A one-shot `switch handler` solves this at coroutine startup: it moves the global `OG(handlers)` into the coroutine's context and clears the global state. After this, each coroutine works with its own buffer, and `echo` in one doesn't mix with another. #### Example: Destructors During Shutdown When `PHP` shuts down, `zend_objects_store_call_destructors()` is called -- traversing the object store and calling destructors. Normally this is a linear process. But a destructor may contain `await`. For example, a database connection object wants to properly close the connection -- which is a network operation. The coroutine calls `await` inside the destructor and goes to sleep. The remaining destructors need to continue. The `switch handler` catches the `LEAVE` moment and spawns a new high-priority coroutine that continues the traversal from the object where the previous one stopped. #### Registration ```c // Add handler to a specific coroutine ZEND_COROUTINE_ADD_SWITCH_HANDLER(coroutine, handler); // Add to the current coroutine (or to main if Scheduler hasn't started yet) ZEND_ASYNC_ADD_SWITCH_HANDLER(handler); // Add handler that fires when the main coroutine starts ZEND_ASYNC_ADD_MAIN_COROUTINE_START_HANDLER(handler); ``` The last macro is needed by subsystems that initialize before the `Scheduler` starts. They register a handler globally, and when the `Scheduler` creates the `main` coroutine, all global handlers are copied into it and fire as `ENTER`. ## Reactor ### Why libuv? `TrueAsync` uses `libuv`, the same library that powers `Node.js`. The choice is deliberate. `libuv` provides: * A unified `API` for `Linux` (`epoll`), macOS (`kqueue`), Windows (`IOCP`) * Built-in support for timers, signals, `DNS`, child processes, file I/O * A mature codebase tested by billions of requests in production Alternatives (`libev`, `libevent`, `io_uring`) were considered, but `libuv` wins on usability. ### Structure ```c // Reactor global data (in ASYNC_G) uv_loop_t uvloop; bool reactor_started; uint64_t last_reactor_tick; // Signal management HashTable *signal_handlers; // signum -> uv_signal_t* HashTable *signal_events; // signum -> HashTable* (events) HashTable *process_events; // SIGCHLD process events ``` ### Event Types and Wrappers Each event in `TrueAsync` has a dual nature: an `ABI` structure defined in the `PHP` core, and a `libuv handle` that actually interacts with the `OS`. The `Reactor` "glues" them together, creating wrappers where both worlds coexist: | Event Type | ABI Structure | libuv handle | |------------------|---------------------------------|-------------------------------| | Poll (fd/socket) | `zend_async_poll_event_t` | `uv_poll_t` | | Timer | `zend_async_timer_event_t` | `uv_timer_t` | | Signal | `zend_async_signal_event_t` | Global `uv_signal_t` | | Filesystem | `zend_async_filesystem_event_t` | `uv_fs_event_t` | | DNS | `zend_async_dns_addrinfo_t` | `uv_getaddrinfo_t` | | Process | `zend_async_process_event_t` | `HANDLE` (Win) / `waitpid` | | Thread | `zend_async_thread_event_t` | `uv_thread_t` | | Exec | `zend_async_exec_event_t` | `uv_process_t` + `uv_pipe_t` | | Trigger | `zend_async_trigger_event_t` | `uv_async_t` | For more details on event structure, see [Events and the Event Model](/en/architecture/events.html). ### Async IO For stream operations, a unified `async_io_t` is used: ```c struct _async_io_t { zend_async_io_t base; // ABI: event + fd/socket + type + state int crt_fd; // CRT file descriptor async_io_req_t *active_req; union { uv_stream_t stream; uv_pipe_t pipe; uv_tty_t tty; uv_tcp_t tcp; uv_udp_t udp; struct { zend_off_t offset; } file; } handle; }; ``` The same interface (`ZEND_ASYNC_IO_READ/WRITE/CLOSE`) works with `PIPE`, `FILE`, `TCP`, `UDP`, `TTY`. The specific implementation is selected at handle creation time based on `type`. ### Reactor Loop `reactor_execute(no_wait)` calls one tick of the `libuv` `event loop`: * `no_wait = true` -- non-blocking call, process only ready events * `no_wait = false` -- block until the next event The `Scheduler` uses both modes. Between coroutine switches -- a non-blocking tick to collect events that have already fired. When the coroutine queue is empty -- a blocking call to avoid wasting CPU in an idle loop. This is a classic strategy from the world of event-driven servers: `nginx`, `Node.js`, and `Tokio` use the same principle: poll without waiting while there's work to do, and sleep when there's no work. ## Switching Efficiency: TrueAsync in the Industry Context ### Stackful vs Stackless: Two Worlds There are two fundamentally different approaches to implementing coroutines: **Stackful** (Go, Erlang, Java Loom, PHP Fibers) -- each coroutine has its own C stack. Switching involves saving/restoring registers and the stack pointer. The main advantage: **transparency**. Any function at any call depth can invoke `suspend` without requiring special annotations. The programmer writes ordinary synchronous code. **Stackless** (Rust async/await, Kotlin, C# async) -- the compiler transforms an `async` function into a state machine. "Suspending" is just a `return` from the function, and "resuming" is a method call with a new state number. The stack is not switched at all. The cost: **"function coloring"** (`async` infects the entire call chain). | Property | Stackful | Stackless | |-------------------------------------------|-----------------------------------|-----------------------------------| | Suspension from nested calls | Yes | No -- only from `async` functions | | Switching cost | 15-200 ns (register save) | 10-50 ns (writing fields to object) | | Memory per coroutine | 4-64 KiB (separate stack) | Exact state machine size | | Compiler optimization through yield | Not possible (stack is opaque) | Possible (inline, HALO) | `PHP coroutines` are **stackful** coroutines based on `Boost.Context fcontext_t`. ### Architectural Trade-off `TrueAsync` chooses the **stackful single-threaded** model: * **Stackful** -- because the `PHP` ecosystem is huge, and "coloring" millions of lines of existing code with `async` is expensive. Stackful coroutines allow using regular C functions, which is a critical requirement for PHP. * **Single-threaded** -- PHP is historically single-threaded (no shared mutable state), and this property is easier to preserve than to deal with its consequences. Threads appear only in the `ThreadPool` for `CPU-bound` tasks. Since `TrueAsync` currently reuses the low-level `Fiber API`, the context switching cost is relatively high and may be improved in the future. ## Graceful Shutdown A `PHP` script can terminate at any moment: an unhandled exception, `exit()`, an OS signal. But in the async world, dozens of coroutines may hold open connections, unwritten buffers, and uncommitted transactions. `TrueAsync` handles this through a controlled shutdown: 1. `ZEND_ASYNC_SHUTDOWN()` -> `start_graceful_shutdown()` -- sets the flag 2. All coroutines receive a `CancellationException` 3. Coroutines get the opportunity to execute `finally` blocks -- close connections, flush buffers 4. `finally_shutdown()` -- final cleanup of remaining coroutines and microtasks 5. The Reactor stops ```c #define TRY_HANDLE_EXCEPTION() \ if (UNEXPECTED(EG(exception) != NULL)) { \ if (ZEND_ASYNC_GRACEFUL_SHUTDOWN) { \ finally_shutdown(); \ break; \ } \ start_graceful_shutdown(); \ } ``` --- --- url: https://true-async.github.io/en/docs/reference/cpu-usage.md description: >- Async\cpu_usage() — current process and system load with automatic delta computation between calls. Convenient for telemetry. --- # cpu\_usage (PHP 8.6+, True Async 1.0) `Async\cpu_usage()` returns CPU usage since the previous call, with percentages already computed. Convenient for telemetry loops. ## Description ```php namespace Async; function cpu_usage(): array ``` The function keeps a **per-process** internal "previous" snapshot of CPU counters. The first call stores the snapshot and returns zeros; every subsequent call returns the delta from the previous snapshot and replaces it. ## Return value An associative array: | Key | Type | Description | |-----|------|-------------| | `process_cores` | `float` | Average number of cores occupied by the process (`0..cpuCount`). Multi-core factor. | | `process_percent` | `float` | Share of the total machine capacity, `0..100`. | | `system_percent` | `float` | Overall host CPU utilisation, `0..100`. | | `cpu_count` | `int` | Number of logical CPUs visible to the OS. | | `interval_sec` | `float` | Wall-clock seconds between snapshots. | | `loadavg` | `array{0:float,1:float,2:float}\|null` | 1/5/15-minute load average, or `null` on Windows. | > Inside containers, `system_percent` reflects the **host**, not the cgroup. For per-process > backpressure prefer `process_cores` / `process_percent` — they correctly account for affinity > and cgroup CPU throttling. ## Examples ### Example #1 Logging load once per second ```php 90% of its capacity share. return $u['process_percent'] < 90.0; } ``` ### Example #3 Health endpoint ```php addListener('0.0.0.0', 8080)); $server->addHttpHandler(function ($req, $res) { if ($req->getPath() === '/healthz') { $u = cpu_usage(); $res->json([ 'cpu' => $u, 'load' => loadavg(), ]); return; } $res->setStatusCode(404); }); $server->start(); ``` ## Notes > **State is global to the process.** If you need several independent telemetry consumers (for > example, different subsystems computing their own deltas), take snapshots manually via > [`CpuSnapshot::now()`](/en/docs/reference/cpu-snapshot.html) and compute deltas yourself. ## See also * [Async\CpuSnapshot](/en/docs/reference/cpu-snapshot.html) — low-level CPU counter snapshot * [Async\loadavg()](/en/docs/reference/loadavg.html) — 1/5/15-minute load average * [Async\available\_parallelism()](/en/docs/reference/available-parallelism.html) — number of available CPUs --- --- url: https://true-async.github.io/en/tutors/01-coroutines.md description: 'A first look at coroutines: spawn() and concurrent execution.' --- # Creating Coroutines ```php function counter(string $name): void { for ($i = 1; $i <= 5; $i++) { echo "$name: $i\n"; sleep(1); } } counter('A'); ``` The `counter` function prints a counter to the screen with a one-second pause: ```bash A: 1 A: 2 A: 3 A: 4 A: 5 ``` Every time `sleep` is called, the PHP thread goes to sleep for the given duration. During that time it's literally doing nothing. The PHP script runs for 5 seconds, exactly as long as the function itself waits. What happens if we run the `counter` function inside a coroutine? ```php use function Async\spawn; spawn(counter(...), 'B'); counter('A'); ``` The output now alternates: ```bash A: 1 B: 1 A: 2 B: 2 A: 3 B: 3 A: 4 B: 4 A: 5 B: 5 ``` The script's total running time is still about 5 seconds, yet the script now behaves as though it's running two functions "concurrently". So what's actually going on? `spawn` creates a second logical thread of control, "B", inside which the `counter` function runs. When thread "A" hits `sleep`, instead of blocking all of PHP, it hands control over to the other logical thread, "B". And so on: ```text A sleep -> B B sleep -> A A sleep -> B ... ``` ## What's the Benefit? Imagine that the `counter` function is ordinary sequential code that performs I/O operations and timer work (`sleep`). I/O operations are handled by the operating system kernel, so PHP code has to wait for them. Meanwhile, PHP could be doing something else. Coroutines let you fill that waiting time with useful work, without creating separate processes, threads, synchronization, data races, and the many other horrors of parallel programming. Let's look at a practical example: ```php function processUsers(string $path, &$counter): void { $handle = fopen($path, 'r'); $header = fgetcsv($handle); $loginIndex = array_search('login', $header); $emailIndex = array_search('email', $header); while (($row = fgetcsv($handle)) !== false) { $login = $row[$loginIndex]; $email = $row[$emailIndex]; $counter++; } fclose($handle); } ``` The `processUsers` function reads a CSV file and processes each row. The file is large, and there's no need to print every row to the screen, but it would be nice to see progress. We could redraw the progress bar on every iteration, but that would hurt processing performance. We could redraw it every 100 iterations, but rows can take different amounts of time to process. How do we show progress smoothly, at roughly even intervals? ```php use function Async\spawn; use function Async\delay; function printProgress(int $current, int $total, int $width = 30): void { $ratio = $total > 0 ? $current / $total : 1; $filled = (int) round($ratio * $width); $bar = str_repeat('=', $filled) . str_repeat(' ', $width - $filled); echo "\r[$bar] " . round($ratio * 100) . "%"; } $counter = 0; $total = 100_000; $progress = spawn(function() use (&$counter, $total) { while (true) { printProgress($counter, $total); delay(1000); } }); processUsers('users.csv', $counter); $progress->cancel(); ``` This can be achieved with the `$progress` coroutine, which displays the current progress once a second. It relies on the `$counter` variable, which is incremented inside the `processUsers` function and passed into the coroutine by reference. ```bash [====> ] 15% ``` The beauty of this solution is that `printProgress` knows nothing about `processUsers`, and vice versa. They don't depend on each other, yet they work together. In other words, coroutines don't just create the illusion of concurrent execution, they also help separate concerns. Did you notice `$progress->cancel()`? What is it there for, exactly? --- --- url: https://true-async.github.io/en/docs/reference/current-context.md description: current_context() — get the context of the current Scope. --- # current\_context (PHP 8.6+, True Async 1.0) `current_context()` — Returns the `Async\Context` object bound to the current Scope. ## Description ```php current_context(): Async\Context ``` If the context for the current Scope has not been created yet, it is created automatically. Values set in this context are visible to all coroutines in the current Scope via `find()`. ## Return Values An `Async\Context` object. ## Examples ```php set('request_id', 'abc-123'); spawn(function() { // Sees the value from the parent scope $id = current_context()->find('request_id'); // "abc-123" }); ?> ``` ## See Also * [coroutine\_context()](/en/docs/reference/coroutine-context.html) — coroutine context * [root\_context()](/en/docs/reference/root-context.html) — global context * [Context](/en/docs/components/context.html) — the context concept --- --- url: https://true-async.github.io/en/docs/reference/current-coroutine.md description: current_coroutine() — get the object of the currently executing coroutine. --- # current\_coroutine (PHP 8.6+, True Async 1.0) `current_coroutine()` — Returns the object of the currently executing coroutine. ## Description ```php current_coroutine(): Async\Coroutine ``` ## Return Values An `Async\Coroutine` object representing the current coroutine. ## Errors/Exceptions `Async\AsyncException` — if called outside a coroutine. ## Examples ### Example #1 Getting the coroutine ID ```php getId() . "\n"; }); ?> ``` ### Example #2 Diagnostics ```php getSpawnLocation() . "\n"; echo "Status: " . ($coro->isRunning() ? 'running' : 'suspended') . "\n"; }); ?> ``` ## See Also * [get\_coroutines()](/en/docs/reference/get-coroutines.html) — list of all coroutines * [Coroutines](/en/docs/components/coroutines.html) — the coroutine concept --- --- url: https://true-async.github.io/en/docs/reference/delay.md description: delay() — suspend a coroutine for a given number of milliseconds. --- # delay (PHP 8.6+, True Async 1.0) `delay()` — Suspends execution of the current coroutine for the specified number of milliseconds. ## Description ```php delay(int $ms): void ``` Suspends the coroutine, yielding control to the scheduler. After `$ms` milliseconds, the coroutine will be resumed. Other coroutines continue to execute during the wait. ## Parameters **`ms`** Wait time in milliseconds. If `0`, the coroutine simply yields control to the scheduler (similar to `suspend()`, but with queueing). ## Return Values No return value. ## Examples ### Example #1 Basic usage ```php ``` ### Example #2 Periodic execution ```php ``` ## Notes > **Note:** `delay()` does not block the entire PHP process — only the current coroutine is blocked. > **Note:** `delay()` automatically starts the scheduler if it has not been started yet. ## See Also * [suspend()](/en/docs/reference/suspend.html) — yield control without delay * [timeout()](/en/docs/reference/timeout.html) — create a timeout to limit waiting --- --- url: https://true-async.github.io/en/docs.md description: >- TrueAsync Documentation. Learn how to install and use true asynchronous primitives for PHP. --- ## Introduction {#introduction} `PHP TrueAsync` is a project that implements true asynchrony in PHP by modifying the Zend core, the I/O library, database libraries, socket libraries, and other functions. `PHP TrueAsync` implements the transparent asynchrony paradigm without colored functions, which minimizes code changes and eliminates library segmentation. In other words, when using coroutines, you use the same functions without changes or with minimal changes. ## IDE support {#ide-support} For autocompletion, inline documentation, and static-analysis stubs, install the dev-only package [`true-async/ide-helper`](https://github.com/true-async/ide-helper). It covers the async core, HTTP server, and ClickHouse client, and works with PhpStorm, PHPStan, and Psalm. ```bash composer require --dev true-async/ide-helper ``` --- --- url: https://true-async.github.io/en/download.md description: Download and install TrueAsync — kernel-level asynchrony for PHP. --- --- --- url: https://true-async.github.io/en/docs/evidence/coroutines-evidence.md description: >- Empirical evidence: context switch cost measurements, memory comparison, the C10K problem, academic research. --- # Empirical Evidence: Why Single-Threaded Coroutines Work The claim that single-threaded cooperative concurrency is effective for IO-bound workloads is supported by measurements, academic research, and operational experience with large-scale systems. *** ## 1. Switching Cost: Coroutine vs OS Thread The main advantage of coroutines is that cooperative switching occurs in user space, without invoking the OS kernel. ### Measurements on Linux | Metric | OS Thread (Linux NPTL) | Coroutine / async task | |------------------------|------------------------------------------|----------------------------------------| | Context switch | 1.2–1.5 µs (pinned), ~2.2 µs (unpinned) | ~170 ns (Go), ~200 ns (Rust async) | | Task creation | ~17 µs | ~0.3 µs | | Memory per task | ~9.5 KiB (min), 8 MiB (default stack) | ~0.4 KiB (Rust), 2–4 KiB (Go) | | Scalability | ~80,000 threads (test) | 250,000+ async tasks (test) | **Sources:** * [Eli Bendersky, Measuring context switching and memory overheads for Linux threads (2018)](https://eli.thegreenplace.net/2018/measuring-context-switching-and-memory-overheads-for-linux-threads/) — direct measurements of Linux thread switching costs and comparison with goroutines * [Jim Blandy, context-switch (Rust benchmark)](https://github.com/jimblandy/context-switch) — async task switches in ~0.2 µs vs ~1.7 µs for a thread (**8.5x** faster), created in 0.3 µs vs 17 µs (**56x** faster), uses 0.4 KiB vs 9.5 KiB (**24x** less) ### What This Means in Practice Switching a coroutine costs **~200 nanoseconds** — an order of magnitude cheaper than switching an OS thread (~1.5 µs). But even more importantly, coroutine switching **does not incur indirect costs**: TLB cache flush, branch predictor invalidation, migration between cores — all of these are characteristic of threads, but not of coroutines within a single thread. For an event loop handling 80 coroutines per core, the total switching overhead is: ``` 80 × 200 ns = 16 µs for a full cycle through all coroutines ``` This is negligible compared to 80 ms of I/O wait time. *** ## 2. Memory: Scale of Differences OS threads allocate a fixed-size stack (8 MiB by default on Linux). Coroutines store only their state — local variables and the resumption point. | Implementation | Memory per unit of concurrency | |-------------------------------|-----------------------------------------------------------| | Linux thread (default stack) | 8 MiB virtual, ~10 KiB RSS minimum | | Go goroutine | 2–4 KiB (dynamic stack, grows as needed) | | Kotlin coroutine | tens of bytes on heap; thread:coroutine ratio ≈ 6:1 | | Rust async task | ~0.4 KiB | | C++ coroutine frame (Pigweed) | 88–408 bytes | | Python asyncio coroutine | ~2 KiB (vs ~5 KiB + 32 KiB stack for a thread) | **Sources:** * [Kotlin Coroutines vs Threads Memory Benchmark (TechYourChance)](https://www.techyourchance.com/kotlin-coroutines-vs-threads-memory-benchmark/) — 6:1 memory ratio * [Super Fast Python: Coroutines Use Less Memory Than Threads](https://superfastpython.com/coroutines-less-memory-threads/) — comparison in Python * [Go FAQ: goroutines](https://go.dev/doc/faq#goroutines) — dynamic goroutine stack ### Implications for Web Servers For 640 concurrent tasks (8 cores × 80 coroutines): * **OS threads**: 640 × 8 MiB = 5 GiB of virtual memory (actually less due to lazy allocation, but the pressure on the OS scheduler is significant) * **Coroutines**: 640 × 4 KiB = 2.5 MiB (a difference of **three orders of magnitude**) *** ## 3. The C10K Problem and Real Servers ### The Problem In 1999, Dan Kegel formulated the [C10K problem](https://www.kegel.com/c10k.html): servers using the "one thread per connection" model were unable to serve 10,000 simultaneous connections. The cause was not hardware limitations, but OS thread overhead. ### The Solution The problem was solved by transitioning to an event-driven architecture: instead of creating a thread for each connection, a single event loop serves thousands of connections in one thread. This is exactly the approach implemented by **nginx**, **Node.js**, **libuv**, and — in the PHP context — **True Async**. ### Benchmarks: nginx (event-driven) vs Apache (thread-per-request) | Metric (1000 concurrent connections) | nginx | Apache | |--------------------------------------|--------------|----------------------------------| | Requests per second (static) | 2,500–3,000 | 800–1,200 | | HTTP/2 throughput | >6,000 req/s | ~826 req/s | | Stability under load | Stable | Degradation at >150 connections | nginx serves **2–4x** more requests than Apache, while consuming significantly less memory. Apache with thread-per-request architecture accepts no more than 150 simultaneous connections (by default), after which new clients wait in a queue. **Sources:** * [Dan Kegel, The C10K problem (1999)](https://www.kegel.com/c10k.html) — problem statement * [Nginx vs Apache: Web Server Performance Comparison (2025)](https://wehaveservers.com/blog/linux-sysadmin/nginx-vs-apache-which-web-server-is-faster-in-2025/) — benchmarks * [Cloudflare: How we scaled nginx](https://blog.cloudflare.com/how-we-scaled-nginx-and-saved-the-world-54-years-every-day/) — industry experience *** ## 4. Academic Research ### SEDA: Staged Event-Driven Architecture (Welsh et al., 2001) Matt Welsh, David Culler, and Eric Brewer from UC Berkeley proposed SEDA — a server architecture based on events and queues between processing stages. **Key result**: The SEDA server in Java outperformed Apache (C, thread-per-connection) in throughput at 10,000+ simultaneous connections. Apache could not accept more than 150 simultaneous connections. > Welsh M., Culler D., Brewer E. *SEDA: An Architecture for Well-Conditioned, > Scalable Internet Services.* SOSP '01 (2001). > [PDF](https://www.sosp.org/2001/papers/welsh.pdf) ### Comparison of Web Server Architectures (Pariag et al., 2007) The most thorough comparison of architectures was conducted by Pariag et al. from the University of Waterloo. They compared three servers on the same codebase: * **µserver** — event-driven (SYMPED, single process) * **Knot** — thread-per-connection (Capriccio library) * **WatPipe** — hybrid (pipeline, similar to SEDA) **Key result**: The event-driven µserver and pipeline-based WatPipe delivered **~18% higher throughput** than the thread-based Knot. WatPipe required 25 writer threads to achieve the same performance as µserver with 10 processes. > Pariag D. et al. *Comparing the Performance of Web Server Architectures.* > EuroSys '07 (2007). > [PDF](https://people.eecs.berkeley.edu/~brewer/cs262/Pariag07.pdf) ### AEStream: Accelerating Event Processing with Coroutines (2022) A study published on arXiv conducted a direct comparison of coroutines and threads for stream data processing (event-based processing). **Key result**: Coroutines delivered **at least 2x throughput** compared to conventional threads for event stream processing. > Pedersen J.E. et al. *AEStream: Accelerated Event-Based Processing with Coroutines.* (2022). > [arXiv:2212.10719](https://arxiv.org/abs/2212.10719) *** ## 5. Scalability: 100,000 Tasks ### Kotlin: 100,000 Coroutines in 100 ms In the [TechYourChance](https://www.techyourchance.com/kotlin-coroutines-vs-threads-performance-benchmark/) benchmark, creating and launching 100,000 coroutines took ~100 ms of overhead. An equivalent number of threads would require ~1.7 seconds just for creation (100,000 × 17 µs) and ~950 MiB of memory for stacks. ### Rust: 250,000 Async Tasks In the [context-switch benchmark](https://github.com/jimblandy/context-switch), 250,000 async tasks were launched in a single process, while OS threads reached their limit at ~80,000. ### Go: Millions of Goroutines Go routinely launches hundreds of thousands and millions of goroutines in production systems. This is what enables servers like Caddy, Traefik, and CockroachDB to handle tens of thousands of simultaneous connections. *** ## 6. Evidence Summary | Claim | Confirmation | |----------------------------------------------------|-----------------------------------------------------------| | Coroutine switching is cheaper than threads | ~200 ns vs ~1500 ns — **7–8x** (Bendersky 2018, Blandy) | | Coroutines consume less memory | 0.4–4 KiB vs 9.5 KiB–8 MiB — **24x+** (Blandy, Go FAQ) | | Event-driven server scales better | nginx 2–4x throughput vs Apache (benchmarks) | | Event-driven > thread-per-connection (academically) | +18% throughput (Pariag 2007), C10K solved (Kegel 1999) | | Coroutines > threads for event processing | 2x throughput (AEStream 2022) | | Hundreds of thousands of coroutines in one process | 250K async tasks (Rust), 100K coroutines in 100ms (Kotlin)| | Formula N ≈ 1 + T\_io/T\_cpu is correct | Goetz 2006, Zalando, Little's Law | *** ## References ### Measurements and Benchmarks * [Eli Bendersky: Measuring context switching for Linux threads (2018)](https://eli.thegreenplace.net/2018/measuring-context-switching-and-memory-overheads-for-linux-threads/) * [Jim Blandy: context-switch benchmark (Rust)](https://github.com/jimblandy/context-switch) * [TechYourChance: Kotlin Coroutines vs Threads Performance](https://www.techyourchance.com/kotlin-coroutines-vs-threads-performance-benchmark/) * [TechYourChance: Kotlin Coroutines vs Threads Memory](https://www.techyourchance.com/kotlin-coroutines-vs-threads-memory-benchmark/) * [Super Fast Python: Coroutines Faster Than Threads](https://superfastpython.com/asyncio-coroutines-faster-than-threads/) ### Academic Papers * Welsh M. et al. *SEDA: An Architecture for Well-Conditioned, Scalable Internet Services.* SOSP '01. [PDF](https://www.sosp.org/2001/papers/welsh.pdf) * Pariag D. et al. *Comparing the Performance of Web Server Architectures.* EuroSys '07. [PDF](https://people.eecs.berkeley.edu/~brewer/cs262/Pariag07.pdf) * Pedersen J.E. et al. *AEStream: Accelerated Event-Based Processing with Coroutines.* [arXiv:2212.10719](https://arxiv.org/abs/2212.10719) ### Industry Experience * [Dan Kegel: The C10K problem (1999)](https://www.kegel.com/c10k.html) * [Cloudflare: How we scaled nginx](https://blog.cloudflare.com/how-we-scaled-nginx-and-saved-the-world-54-years-every-day/) * [High Scalability: The Secret to 10 Million Concurrent Connections](https://highscalability.com/the-secret-to-10-million-concurrent-connections-the-kernel-i/) ### See Also * [Python asyncio in Practice](/en/docs/evidence/python-evidence.html) — production cases (Duolingo, Super.com, Instagram), uvloop benchmarks, Cal Paterson's counter-arguments * [Swoole in Practice](/en/docs/evidence/swoole-evidence.html) — production cases and benchmarks for PHP coroutines --- --- url: https://true-async.github.io/en/architecture/events.md description: >- The base zend_async_event_t structure -- foundation of all asynchronous operations, callback system, flags, event hierarchy. --- # Events and the Event Model An event (`zend_async_event_t`) is a universal structure from which **all** asynchronous primitives inherit: coroutines, `future`, channels, timers, `poll` events, signals, and others. The unified event interface allows: * Subscribing to any event via callback * Combining heterogeneous events in a single wait * Managing the lifecycle through ref-counting ## Base Structure ```c struct _zend_async_event_s { uint32_t flags; uint32_t extra_offset; // Offset to additional data union { uint32_t ref_count; // For C objects uint32_t zend_object_offset; // For Zend objects }; uint32_t loop_ref_count; // Event loop reference count zend_async_callbacks_vector_t callbacks; // Methods zend_async_event_add_callback_t add_callback; zend_async_event_del_callback_t del_callback; zend_async_event_start_t start; zend_async_event_stop_t stop; zend_async_event_replay_t replay; // Nullable zend_async_event_dispose_t dispose; zend_async_event_info_t info; // Nullable zend_async_event_callbacks_notify_t notify_handler; // Nullable }; ``` ### Virtual Methods of an Event Each event has a small set of virtual methods. | Method | Purpose | |------------------|----------------------------------------------------| | `add_callback` | Subscribe a callback to the event | | `del_callback` | Unsubscribe a callback | | `start` | Activate the event in the reactor | | `stop` | Deactivate the event | | `replay` | Re-deliver the result (for futures, coroutines) | | `dispose` | Release resources | | `info` | Text description of the event (for debugging) | | `notify_handler` | Hook called before notifying callbacks | #### `add_callback` Adds a callback to the event's dynamic `callbacks` array. Calls `zend_async_callbacks_push()`, which increments the callback's `ref_count` and adds the pointer to the vector. #### `del_callback` Removes a callback from the vector (O(1) via swap with the last element) and calls `callback->dispose`. Typical scenario: during a `select` wait on multiple events, when one fires, the others are unsubscribed via `del_callback`. #### `start` The `start` and `stop` methods are intended for events that can be placed into the `EventLoop`. Therefore, not all primitives implement this method. For EventLoop events, `start` increments the `loop_ref_count`, which allows the event to remain in the EventLoop as long as it is needed by someone. | Type | What `start` does | |------------------------------------------------|--------------------------------------------------------------------------| | Coroutine, `Future`, `Channel`, `Pool`, `Scope` | Does nothing | | Timer | `uv_timer_start()` + increments `loop_ref_count` and `active_event_count` | | Poll | `uv_poll_start()` with event mask (READABLE/WRITABLE) | | Signal | Registers the event in the global signal table | | IO | Increments `loop_ref_count` -- libuv stream starts via read/write | #### `stop` The mirror method of `start`. Decrements the `loop_ref_count` for EventLoop-type events. The last `stop` call (when `loop_ref_count` reaches 0) actually stops the `handle`. #### `replay` Allows late subscribers to receive the result of an already completed event. Implemented only by types that store a result. | Type | What `replay` returns | |--------------|--------------------------------------------------| | **Coroutine** | `coroutine->result` and/or `coroutine->exception` | | **Future** | `future->result` and/or `future->exception` | If a `callback` is provided, it is called synchronously with the result. If `result`/`exception` is provided, values are copied to the pointers. Without `replay`, waiting on a closed event produces a warning. #### `dispose` This method attempts to release the event by decrementing its `ref_count`. If the count reaches zero, actual resource deallocation is triggered. #### `info` A human-readable string for debugging and logging. | Type | Example string | |----------------------|--------------------------------------------------------------------------| | **Coroutine** | `"Coroutine 42 spawned at foo.php:10, suspended at bar.php:20 (myFunc)"` | | **Scope** | `"Scope #5 created at foo.php:10"` | | **Future** | `"FutureState(completed)"` or `"FutureState(pending)"` | | **Iterator** | `"iterator-completion"` | #### `notify_handler` A hook that intercepts notification **before** callbacks receive the result. By default `NULL` for all events. Used in `Async\Timeout`: ## Event Lifecycle ![Event Lifecycle](/diagrams/en/architecture-events/lifecycle.svg) An event goes through several states: * **Created** -- memory allocated, `ref_count = 1`, callbacks can be subscribed * **Active** -- registered in the `EventLoop` (`start()`), increments `active_event_count` * **Fired** -- `libuv` called the callback. For periodic events (timer, poll) -- returns to **Active**. For one-shot events (DNS, exec, Future) -- transitions to **Closed** * **Stopped** -- temporarily removed from the `EventLoop` (`stop()`), can be reactivated * **Closed** -- `flags |= F_CLOSED`, subscription is not possible, when `ref_count = 0` is reached, `dispose` is called ## Interaction: Event, Callback, Coroutine ![Event -> Callback -> Coroutine](/diagrams/en/architecture-events/callback-flow.svg) ## Dual Life: C Object and Zend Object Events often live in two worlds simultaneously. A timer, `poll` handle, or `DNS` query is an internal `C` object managed by the `Reactor`. But a coroutine or `Future` is also a `PHP` object accessible from user code. C structures in the `EventLoop` may live longer than the `PHP` objects that reference them, and vice versa. C objects use `ref_count`, while `PHP` objects use `GC_ADDREF/GC_DELREF` with the garbage collector. Therefore, `TrueAsync` supports several types of bindings between PHP objects and C objects. ### C Object Internal events invisible from PHP code use the `ref_count` field. When the last owner releases the reference, `dispose` is called: ```c ZEND_ASYNC_EVENT_ADD_REF(ev) // ++ref_count ZEND_ASYNC_EVENT_DEL_REF(ev) // --ref_count ZEND_ASYNC_EVENT_RELEASE(ev) // DEL_REF + dispose when reaching 0 ``` ### Zend Object A coroutine is a `PHP` object implementing the `Awaitable` interface. Instead of `ref_count`, they use the `zend_object_offset` field, which points to the offset of the `zend_object` structure. The `ZEND_ASYNC_EVENT_ADD_REF`/`ZEND_ASYNC_EVENT_RELEASE` macros work correctly in all cases. ```c ZEND_ASYNC_EVENT_ADD_REF(ev) -> is_zend_obj ? GC_ADDREF(obj) : ++ref_count ZEND_ASYNC_EVENT_RELEASE(ev) -> is_zend_obj ? OBJ_RELEASE(obj) : dispose(ev) ``` The `zend_object` is part of the event's C structure and can be recovered using `ZEND_ASYNC_EVENT_TO_OBJECT`/`ZEND_ASYNC_OBJECT_TO_EVENT`. ```c // Get event from PHP object (accounting for event reference) zend_async_event_t *ev = ZEND_ASYNC_OBJECT_TO_EVENT(obj); // Get PHP object from event zend_object *obj = ZEND_ASYNC_EVENT_TO_OBJECT(ev); ``` ## Event Reference Some events face an architectural problem: they cannot be `Zend` objects directly. For example, a timer. `PHP GC` may decide to collect the object at any moment, but `libuv` requires asynchronous handle closure via `uv_close()` with a callback. If `GC` calls the destructor while `libuv` hasn't finished working with the handle, we get `use-after-free`. In this case, the **Event Reference** approach is used: the `PHP` object stores not the event itself, but a pointer to it: ```c typedef struct { uint32_t flags; // = ZEND_ASYNC_EVENT_REFERENCE_PREFIX uint32_t zend_object_offset; zend_async_event_t *event; // Pointer to the actual event } zend_async_event_ref_t; ``` With this approach, the lifetimes of the `PHP` object and the C event are **independent**. The `PHP` object can be collected by `GC` without affecting the `handle`, and the `handle` will close asynchronously when ready. The `ZEND_ASYNC_OBJECT_TO_EVENT()` macro automatically recognizes a reference by the `flags` prefix and follows the pointer. ## Callback System Subscribing to events is the primary mechanism of interaction between coroutines and the outside world. When a coroutine wants to wait for a timer, data from a socket, or completion of another coroutine, it registers a `callback` on the corresponding event. Each event stores a dynamic array of subscribers: ```c typedef struct { uint32_t length; uint32_t capacity; zend_async_event_callback_t **data; // Pointer to the active iterator index (or NULL) uint32_t *current_iterator; } zend_async_callbacks_vector_t; ``` `current_iterator` solves the problem of safely removing callbacks during iteration. ### Callback Structure ```c struct _zend_async_event_callback_s { uint32_t ref_count; zend_async_event_callback_fn callback; zend_async_event_callback_dispose_fn dispose; }; ``` A callback is also a ref-counted structure. This is necessary because a single `callback` can be referenced by both the event's vector and the coroutine's `waker` simultaneously. `ref_count` ensures that memory is freed only when both sides release their reference. ### Coroutine Callback Most callbacks in `TrueAsync` are used to wake up a coroutine. Therefore, they store information about the coroutine and the event they subscribed to: ```c struct _zend_coroutine_event_callback_s { zend_async_event_callback_t base; // Inheritance zend_coroutine_t *coroutine; // Who to wake zend_async_event_t *event; // Where it came from }; ``` This binding is the foundation for the [Waker](/en/architecture/waker.html) mechanism: ## Event Flags Bit flags in the `flags` field control the event's behavior at every stage of its lifecycle: | Flag | Purpose | |-----------------------|----------------------------------------------------------------------------------| | `F_CLOSED` | Event is complete. `start`/`stop` no longer work, subscription is not possible | | `F_RESULT_USED` | Someone is awaiting the result -- no unused result warning needed | | `F_EXC_CAUGHT` | The error will be caught -- suppress unhandled exception warning | | `F_ZVAL_RESULT` | The result in the callback is a pointer to `zval` (not `void*`) | | `F_ZEND_OBJ` | The event is a `Zend` object -- switches `ref_count` to `GC_ADDREF` | | `F_NO_FREE_MEMORY` | `dispose` should not free memory (object was not allocated via `emalloc`) | | `F_EXCEPTION_HANDLED` | Exception was handled -- no need to re-throw | | `F_REFERENCE` | The structure is an `Event Reference`, not an actual event | | `F_OBJ_REF` | At `extra_offset` there is a pointer to `zend_object` | | `F_CLOSE_FD` | Close the file descriptor upon destruction | | `F_HIDDEN` | Hidden event -- does not participate in `Deadlock Detection` | ### Deadlock Detection `TrueAsync` tracks the number of active events in the `EventLoop` via `active_event_count`. When all coroutines are suspended and there are no active events -- this is a `deadlock`: no event can wake any coroutine. But some events are always present in the `EventLoop` and are unrelated to user logic: background `healthcheck` timers, system handlers. If they are counted as "active", `deadlock detection` will never trigger. For such events, the `F_HIDDEN` flag is used: ```c ZEND_ASYNC_EVENT_SET_HIDDEN(ev) // Mark as hidden ZEND_ASYNC_INCREASE_EVENT_COUNT(ev) // +1, but only if NOT hidden ZEND_ASYNC_DECREASE_EVENT_COUNT(ev) // -1, but only if NOT hidden ``` ## Event Hierarchy In `C` there is no class inheritance, but there is a technique: if the first field of a structure is `zend_async_event_t`, then a pointer to the structure can be safely cast to a pointer to `zend_async_event_t`. This is exactly how all specialized events "inherit" from the base: ``` zend_async_event_t |-- zend_async_poll_event_t -- fd/socket polling | \-- zend_async_poll_proxy_t -- proxy for event filtering |-- zend_async_timer_event_t -- timers (one-shot and periodic) |-- zend_async_signal_event_t -- POSIX signals |-- zend_async_process_event_t -- waiting for process termination |-- zend_async_thread_event_t -- background threads |-- zend_async_filesystem_event_t -- filesystem changes |-- zend_async_dns_nameinfo_t -- reverse DNS |-- zend_async_dns_addrinfo_t -- DNS resolution |-- zend_async_exec_event_t -- exec/system/passthru/shell_exec |-- zend_async_listen_event_t -- TCP server socket |-- zend_async_trigger_event_t -- manual wake-up (cross-thread safe) |-- zend_async_task_t -- thread pool task |-- zend_async_io_t -- unified I/O |-- zend_coroutine_t -- coroutine |-- zend_future_t -- future |-- zend_async_channel_t -- channel |-- zend_async_group_t -- task group |-- zend_async_pool_t -- resource pool \-- zend_async_scope_t -- scope ``` Thanks to this, a `Waker` can subscribe to **any** of these events with the same `event->add_callback` call, without knowing the specific type. ### Examples of Specialized Structures Each structure adds to the base event only those fields that are specific to its type: **Timer** -- minimal extension: ```c struct _zend_async_timer_event_s { zend_async_event_t base; unsigned int timeout; // Milliseconds bool is_periodic; }; ``` **Poll** -- I/O tracking on a descriptor: ```c struct _zend_async_poll_event_s { zend_async_event_t base; bool is_socket; union { zend_file_descriptor_t file; zend_socket_t socket; }; async_poll_event events; // What to track: READABLE|WRITABLE|... async_poll_event triggered_events; // What actually happened }; ``` **Filesystem** -- filesystem monitoring: ```c struct _zend_async_filesystem_event_s { zend_async_event_t base; zend_string *path; unsigned int flags; // ZEND_ASYNC_FS_EVENT_RECURSIVE unsigned int triggered_events; // RENAME | CHANGE zend_string *triggered_filename; // Which file changed }; ``` **Exec** -- executing external commands: ```c struct _zend_async_exec_event_s { zend_async_event_t base; zend_async_exec_mode exec_mode; // exec/system/passthru/shell_exec bool terminated; char *cmd; zval *return_value; zend_long exit_code; int term_signal; }; ``` ## Poll Proxy Imagine a situation: two coroutines on a single TCP socket -- one reading, the other writing. They need different events (`READABLE` vs `WRITABLE`), but the socket is one. `Poll Proxy` solves this problem. Instead of creating two `uv_poll_t` handles for the same fd (which is impossible in `libuv`), a single `poll_event` is created along with several proxies with different masks: ```c struct _zend_async_poll_proxy_s { zend_async_event_t base; zend_async_poll_event_t *poll_event; // Parent poll async_poll_event events; // Event subset for this proxy async_poll_event triggered_events; // What fired }; ``` The `Reactor` aggregates masks from all active proxies and passes the combined mask to `uv_poll_start`. When `libuv` reports an event, the `Reactor` checks each proxy and notifies only those whose mask matched. ## Async IO For stream I/O operations (reading from a file, writing to a socket, working with pipes), `TrueAsync` provides a unified `handle`: ```c struct _zend_async_io_s { zend_async_event_t event; union { zend_file_descriptor_t fd; // For PIPE/FILE zend_socket_t socket; // For TCP/UDP } descriptor; zend_async_io_type type; // PIPE, FILE, TCP, UDP, TTY uint32_t state; // READABLE | WRITABLE | CLOSED | EOF | APPEND }; ``` The same `ZEND_ASYNC_IO_READ/WRITE/CLOSE` interface works with any type, and the specific implementation is selected at `handle` creation time based on `type`. All I/O operations are asynchronous and return a `zend_async_io_req_t` -- a one-shot request: ```c struct _zend_async_io_req_s { union { ssize_t result; ssize_t transferred; }; zend_object *exception; // Operation error (or NULL) char *buf; // Data buffer bool completed; // Operation complete? void (*dispose)(zend_async_io_req_t *req); }; ``` A coroutine calls `ZEND_ASYNC_IO_READ`, receives a `req`, subscribes to its completion via the `Waker`, and goes to sleep. When `libuv` completes the operation, `req->completed` becomes `true`, the callback wakes the coroutine, and it retrieves data from `req->buf`. --- --- url: https://true-async.github.io/en/docs/components/exceptions.md description: >- TrueAsync exception hierarchy -- AsyncCancellation, TimeoutException, DeadlockError and others. --- # Exceptions ## Hierarchy TrueAsync defines a specialized exception hierarchy for different types of errors: ``` \Cancellation -- base cancellation class (on par with \Error and \Exception) +-- Async\AsyncCancellation -- coroutine cancellation +-- Async\OperationCanceledException -- operation interrupted by cancellation token \Error +-- Async\DeadlockError -- deadlock detected \Exception +-- Async\AsyncException -- general async operation error | +-- Async\ServiceUnavailableException -- service unavailable (circuit breaker) +-- Async\InputOutputException -- I/O error +-- Async\DnsException -- DNS resolution error +-- Async\TimeoutException -- operation timeout +-- Async\PollException -- poll operation error +-- Async\ChannelException -- channel error +-- Async\PoolException -- resource pool error +-- Async\CompositeException -- container for multiple exceptions ``` ## AsyncCancellation ```php class Async\AsyncCancellation extends \Cancellation {} ``` Thrown when a coroutine is cancelled. `\Cancellation` is the third root `Throwable` class on par with `\Error` and `\Exception`, so regular `catch (\Exception $e)` and `catch (\Error $e)` blocks do **not** accidentally catch cancellation. ```php getMessage() . "\n"; } }); delay(100); $coroutine->cancel(); ?> ``` **Important:** Do not catch `AsyncCancellation` via `catch (\Throwable $e)` without re-throwing -- this violates the cooperative cancellation mechanism. ## OperationCanceledException ```php class Async\OperationCanceledException extends Async\AsyncCancellation {} ``` Thrown when an awaited operation is interrupted by a **cancellation token**. The original exception from the token is available via `$previous`. This allows you to distinguish a token trigger from an exception thrown by the awaitable object itself. Affects all operations with a cancellation token: `await()`, `await_*()`, `Future::await()`, `Channel::send()`/`recv()`, `Scope::awaitCompletion()`. ```php getPrevious()?->getMessage() . "\n"; } catch (\Exception $e) { // Error from the coroutine itself echo "Error: " . $e->getMessage() . "\n"; } ?> ``` ## DeadlockError ```php class Async\DeadlockError extends \Error {} ``` Thrown when the scheduler detects a deadlock -- a situation where coroutines are waiting for each other and none can proceed. ```php ``` Example where a coroutine awaits itself: ```php ``` ## AsyncException ```php class Async\AsyncException extends \Exception {} ``` Base exception for general async operation errors. Used for errors that don't fall into specialized categories. ## TimeoutException ```php class Async\TimeoutException extends \Exception {} ``` Thrown when a timeout is exceeded inside a coroutine. When `timeout()` is used as a **cancellation token** in `await()`, `OperationCanceledException` is thrown with `TimeoutException` in `$previous`: ```php getPrevious() is TimeoutException. echo "Operation didn't complete in time\n"; } ?> ``` ## InputOutputException ```php class Async\InputOutputException extends \Exception {} ``` General exception for I/O errors: sockets, files, pipes, and other I/O descriptors. ## DnsException ```php class Async\DnsException extends \Exception {} ``` Thrown on DNS resolution errors (`gethostbyname`, `gethostbyaddr`, `gethostbynamel`). ## PollException ```php class Async\PollException extends \Exception {} ``` Thrown on poll operation errors on descriptors. ## ServiceUnavailableException ```php class Async\ServiceUnavailableException extends Async\AsyncException {} ``` Thrown when the circuit breaker is in the `INACTIVE` state and a service request is rejected without an attempt to execute. ```php acquire(); } catch (ServiceUnavailableException $e) { echo "Service is temporarily unavailable\n"; } ?> ``` ## ChannelException ```php class Async\ChannelException extends Async\AsyncException {} ``` Thrown on channel operation errors: sending to a closed channel, receiving from a closed channel, etc. ## PoolException ```php class Async\PoolException extends Async\AsyncException {} ``` Thrown on resource pool operation errors. ## CompositeException ```php final class Async\CompositeException extends \Exception { public function addException(\Throwable $exception): void; public function getExceptions(): array; } ``` A container for multiple exceptions. Used when several handlers (e.g., `finally` in Scope) throw exceptions during completion: ```php finally(function() { throw new \Exception('Cleanup error 1'); }); $scope->finally(function() { throw new \RuntimeException('Cleanup error 2'); }); $scope->setExceptionHandler(function($scope, $coroutine, $exception) { if ($exception instanceof CompositeException) { echo "Errors: " . count($exception->getExceptions()) . "\n"; foreach ($exception->getExceptions() as $e) { echo " - " . $e->getMessage() . "\n"; } } }); $scope->dispose(); // Errors: 2 // - Cleanup error 1 // - Cleanup error 2 ?> ``` ## Recommendations ### Properly Handling AsyncCancellation ```php beginTransaction(); protect(function() use ($db) { $db->exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1"); $db->exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2"); $db->commit(); }); ``` ## See Also * [Cancellation](/en/docs/components/cancellation.html) -- the coroutine cancellation mechanism * [protect()](/en/docs/reference/protect.html) -- protection from cancellation * [Scope](/en/docs/components/scope.html) -- exception handling in scopes --- --- url: https://true-async.github.io/en/tutors/04-exceptions.md description: How exceptions from a coroutine propagate through await(). --- # Exceptions in Coroutines ```php function validateToken(string $token): bool { $response = file_get_contents("https://userdirectory.example.com/api/validate?token=$token"); return json_decode($response)->valid; } ``` The `validateToken` function from the previous chapter has a few problems. If `UserDirectory` doesn't respond (timeout, network down, DNS not resolving), `file_get_contents` returns `false`. `json_decode(false)` returns `null`, and `null->valid` is also `null`. The function returns something falsy, the same result as with an actually invalid token. `validateToken` has no way to tell "the token expired" apart from "`UserDirectory` didn't respond", even though these are two completely different situations. The right approach is to throw an exception when the service doesn't respond: ```php function validateToken(string $token): bool { $response = file_get_contents("https://userdirectory.example.com/api/validate?token=$token"); if ($response === false) { throw new RemoteApiException('UserDirectory did not respond'); } return json_decode($response)->valid; } ``` But what happens if an exception is thrown inside a coroutine? If an exception is thrown in regular `PHP` code and nobody catches it, `PHP` terminates with an Unhandled Exception message. What about a coroutine? ```php use function Async\spawn; spawn(function () { throw new Exception('Something went wrong'); }); echo "Hello, world!\n"; ``` ```text Hello, world! Fatal error: Uncaught Exception: Something went wrong in /path/to/script.php ``` At first glance it looks like there's no difference. But that's far from true. Coroutines belong to the `Scheduler` component, which is responsible for switching between them. Each coroutine runs in its own logical thread. If an unhandled exception reaches the coroutine's final handler, it gets stored on the coroutine's handle. ```php $coroutine = spawn(function () { throw new Exception('Something went wrong'); }); echo "Hello, world!\n"; sleep(1); echo "Goodbye, world!\n"; unset($coroutine); ``` In other words, the exception won't terminate the program as long as someone still holds a reference to `$coroutine`. This logic guarantees idempotency for the `await` operation. ```php $coroutine = spawn(function () { throw new Exception('Something went wrong'); }); try { await($coroutine); } catch (Exception $e) { echo "Caught the exception: {$e->getMessage()}\n"; } try { await($coroutine); } catch (Exception $e) { echo "Caught the exception a second time: {$e->getMessage()}\n"; } ``` Repeated calls to `await` on the same coroutine produce the same behavior, regardless of whether the coroutine is still running or not. This lets you treat a coroutine like a `Future`, a promise of a result that can be obtained at some point in the future. If the result already exists, `await` returns it immediately. Now we can improve the code that validates the token and updates the profile by adding exception handling: ```php $validation = spawn(validateToken(...), $token); try { if (profileExists($userId) && await($validation)) { updateProfile($userId, $changes); } } catch (RemoteApiException $e) { // Maybe worth retrying the operation later? } ``` > Important! > After calling `await` or any other `await_*` operation, > the coroutine is marked as handled, and its exception will no longer cause > the PHP program to terminate. --- --- url: https://true-async.github.io/en/architecture/fibers.md description: >- How TrueAsync changes Fiber behavior — coroutine mode, GC, refcount, parameters, exit/bailout, destructors. --- # Fibers in TrueAsync In standard `PHP`, a fiber (`Fiber`) is a cooperative thread with its own call stack. When the `TrueAsync` extension is loaded, the fiber switches to **coroutine mode**: instead of direct stack switching, the fiber gets its own coroutine managed by the scheduler (`Scheduler`). This article describes the key changes in fiber behavior when using `TrueAsync`. ## Fiber Coroutine Mode When creating `new Fiber(callable)`, if `TrueAsync` is active, instead of initializing a stack-switching context, a coroutine is created: ```c fiber->coroutine = ZEND_ASYNC_NEW_COROUTINE(...); ZEND_COROUTINE_SET_FIBER(fiber->coroutine); fiber->coroutine->extended_data = fiber; fiber->coroutine->internal_entry = coroutine_entry_point; ``` Calling `$fiber->start()` does not switch the stack directly but enqueues the coroutine into the scheduler via `ZEND_ASYNC_ENQUEUE_COROUTINE`, after which the calling code suspends in `zend_fiber_await()` until the fiber completes or suspends. ## Coroutine Refcount Lifecycle The fiber explicitly retains its coroutine via `ZEND_ASYNC_EVENT_ADD_REF`: ``` After constructor: coroutine refcount = 1 (scheduler) After start(): coroutine refcount = 2 (scheduler + fiber) ``` The additional `+1` from the fiber is necessary to keep the coroutine alive after completion; otherwise `getReturn()`, `isTerminated()`, and other methods would be unable to access the result. The `+1` is released in the fiber destructor (`zend_fiber_object_destroy`): ```c if (ZEND_COROUTINE_IS_FINISHED(coroutine) || !ZEND_COROUTINE_IS_STARTED(coroutine)) { ZEND_ASYNC_EVENT_RELEASE(&coroutine->event); } ``` ## Fiber::start() Parameters — Copying to the Heap The `Z_PARAM_VARIADIC_WITH_NAMED` macro, when parsing `Fiber::start()` arguments, sets `fcall->fci.params` as a pointer directly into the VM frame stack. In standard PHP this is safe — `zend_fiber_execute` is called immediately via a stack switch, and the `Fiber::start()` frame is still alive. In coroutine mode, `fcall->fci.params` can become a dangling pointer if the awaited coroutine is destroyed first. There is no way to guarantee this will never happen. Therefore, after parsing the parameters, we copy them to heap memory: ```c if (fiber->coroutine != NULL && fiber->fcall != NULL) { if (fiber->fcall->fci.param_count > 0) { uint32_t count = fiber->fcall->fci.param_count; zval *heap_params = emalloc(sizeof(zval) * count); for (uint32_t i = 0; i < count; i++) { ZVAL_COPY(&heap_params[i], &fiber->fcall->fci.params[i]); } fiber->fcall->fci.params = heap_params; } if (fiber->fcall->fci.named_params) { GC_ADDREF(fiber->fcall->fci.named_params); } } ``` Now `coroutine_entry_point` can safely use and release the parameters. ## GC for Coroutine Fibers Instead of adding the coroutine object to the GC buffer, `zend_fiber_object_gc` directly traverses the coroutine's execution stack and passes the found variables: ```c if (fiber->coroutine != NULL) { zend_execute_data *ex = ZEND_ASYNC_COROUTINE_GET_EXECUTE_DATA(fiber->coroutine); if (ex != NULL && ZEND_COROUTINE_IS_YIELD(fiber->coroutine)) { // Stack traversal — same as for a regular fiber for (; ex; ex = ex->prev_execute_data) { // ... add CVs to GC buffer ... } } } ``` This only works for the `YIELD` state (fiber suspended via `Fiber::suspend()`). For other states (running, awaiting child), the stack is active and cannot be traversed. ## Destructors from GC In standard PHP, destructors of objects found by `GC` are called synchronously in the same context. In `TrueAsync`, GC runs in a separate GC coroutine (see [Garbage Collection in an Asynchronous Context](async-gc.html)). This means: 1. **Execution order** — destructors run asynchronously, after returning from `gc_collect_cycles()`. 2. **`Fiber::suspend()` in a destructor** — not possible. The destructor runs in the GC coroutine, not in a fiber. Calling `Fiber::suspend()` will result in the error "Cannot suspend outside of a fiber". 3. **`Fiber::getCurrent()` in a destructor** — returns `NULL`, since the destructor runs outside a fiber context. For this reason, tests that expect synchronous execution of destructors from GC inside a fiber are marked as `skip` for `TrueAsync`. ## Generators During Shutdown In standard PHP, when a fiber is destroyed, the generator is marked with the `ZEND_GENERATOR_FORCED_CLOSE` flag. This prevents `yield from` in finally blocks — the generator is dying and should not create new dependencies. In `TrueAsync`, the coroutine receives graceful cancellation rather than forced closure. The generator is not marked as `FORCED_CLOSE`, and `yield from` in finally blocks may execute. This is a known behavioral difference. It is not yet clear whether this should be changed or not. --- --- url: https://true-async.github.io/en/docs/components/filesystem-watcher.md description: >- FileSystemWatcher in TrueAsync -- a persistent filesystem observer with foreach iteration support, event buffering, and two storage modes. --- # FileSystemWatcher: Filesystem Monitoring ## What is FileSystemWatcher `Async\FileSystemWatcher` is a persistent observer for changes in files and directories. Unlike one-shot approaches, FileSystemWatcher runs continuously and delivers events through standard `foreach` iteration: ```php filename}: renamed={$event->renamed}, changed={$event->changed}\n"; } ?> ``` Iteration automatically suspends the coroutine when the buffer is empty and resumes it when a new event arrives. ## FileSystemEvent Each event is an `Async\FileSystemEvent` object with four readonly properties: | Property | Type | Description | |------------|-----------|-------------------------------------------------------| | `path` | `string` | The path passed to the `FileSystemWatcher` constructor | | `filename` | `?string` | The name of the file that triggered the event (may be `null`) | | `renamed` | `bool` | `true` -- file was created, deleted, or renamed | | `changed` | `bool` | `true` -- file contents were modified | ## Two Buffering Modes ### Coalesce (Default) In coalesce mode, events are grouped by the `path/filename` key. If a file changed multiple times before the iterator processed it, only one event with merged flags remains in the buffer: ```php ``` This is optimal for typical scenarios: hot-reload, config change rebuilds, synchronization. ### Raw In raw mode, each event from the OS is stored as a separate element in a circular buffer: ```php ``` Suitable when exact order and count of events matters -- auditing, logging, replication. ## Constructor ```php new FileSystemWatcher( string $path, bool $recursive = false, bool $coalesce = true ) ``` **`path`** -- path to a file or directory. If the path doesn't exist, an `Error` is thrown. **`recursive`** -- if `true`, nested directories are also monitored. **`coalesce`** -- buffering mode: `true` -- event merging (HashTable), `false` -- all events (circular buffer). Monitoring starts immediately upon object creation. Events are buffered even before iteration begins. ## Lifecycle ### close() Stops monitoring. The current iteration finishes after processing remaining events in the buffer. Idempotent -- repeated calls are safe. ```php close(); ?> ``` ### isClosed() ```php isClosed(); // bool ?> ``` ### Automatic Closing If the `FileSystemWatcher` object is destroyed (goes out of scope), monitoring automatically stops. ## Examples ### Hot-Reload Configuration ```php filename ?? '', '.yml')) { echo "Config changed: {$event->filename}\n"; reloadConfig(); } } }); ?> ``` ### Time-Limited Monitoring ```php close(); }); foreach ($watcher as $event) { processUpload($event->filename); } echo "Monitoring finished\n"; ?> ``` ### Monitoring Multiple Directories ```php filename}\n"; } }); } ?> ``` ### Raw Mode for Auditing ```php renamed ? 'RENAME' : 'CHANGE'; auditLog("[{$type}] {$event->path}/{$event->filename}"); } }); ?> ``` ## Cancellation via Scope FileSystemWatcher terminates correctly when the scope is cancelled: ```php filename}\n"; } echo "Iteration finished\n"; }); delay(5000); $watcher->close(); }); ?> ``` ## See Also * [Coroutines](/en/docs/components/coroutines.html) -- the basic unit of concurrency * [Channel](/en/docs/components/channels.html) -- CSP channels for data transfer * [Cancellation](/en/docs/components/cancellation.html) -- the cancellation mechanism --- --- url: https://true-async.github.io/en/docs/reference/filesystem-watcher/construct.md description: Create a new FileSystemWatcher and start watching files or a directory. --- # FileSystemWatcher::\_\_construct (PHP 8.6+, True Async 1.0) ```php public FileSystemWatcher::__construct( string $path, bool $recursive = false, bool $coalesce = true, int $debounceMs = 0, int $maxHoldMs = 0, array $extensions = [] ) ``` Creates a watcher and immediately starts tracking changes. Events are buffered from the moment of creation, even if iteration has not yet begun. ## Parameters **path** : The path to a file or directory to watch. If the path does not exist or is inaccessible, an `Error` is thrown. **recursive** : If `true`, nested directories are also monitored. Default is `false`. **coalesce** : Event buffering mode. `true` (default) --- events are grouped by `path/filename` key. Repeated changes to the same file merge the `renamed`/`changed` flags via OR. `false` --- each OS event is stored as a separate element in a circular buffer. ## Errors/Exceptions * `Error` --- the path does not exist or is not available for watching. ## Examples ### Example #1 Watching a directory ```php filename}\n"; $watcher->close(); } ?> ``` ### Example #2 Recursive watching in raw mode ```php path}] {$event->filename}\n"; } ?> ``` ## See Also * [FileSystemWatcher::close](/en/docs/reference/filesystem-watcher/close.html) --- Stop watching * [FileSystemWatcher](/en/docs/components/filesystem-watcher.html) --- Concept overview --- --- url: https://true-async.github.io/en/docs/reference/filesystem-watcher/close.md description: Stop watching the file system and end iteration. --- # FileSystemWatcher::close (PHP 8.6+, True Async 1.0) ```php public FileSystemWatcher::close(): void ``` Stops watching the file system. Iteration via `foreach` ends after processing the remaining buffered events. Idempotent --- repeated calls are safe. ## Parameters No parameters. ## Examples ### Example #1 Closing after receiving the desired event ```php filename === 'ready.flag') { $watcher->close(); } } echo "Marker file detected\n"; ?> ``` ### Example #2 Closing from another coroutine ```php close(); }); foreach ($watcher as $event) { processEvent($event); } echo "Watching ended by timeout\n"; ?> ``` ## See Also * [FileSystemWatcher::isClosed](/en/docs/reference/filesystem-watcher/is-closed.html) --- Check state * [FileSystemWatcher::\_\_construct](/en/docs/reference/filesystem-watcher/construct.html) --- Create a watcher --- --- url: >- https://true-async.github.io/en/docs/reference/filesystem-watcher/get-iterator.md description: Get an asynchronous iterator for foreach traversal of file system events. --- # FileSystemWatcher::getIterator (PHP 8.6+, True Async 1.0) ```php public FileSystemWatcher::getIterator(): Iterator ``` Returns an iterator for use with `foreach`. Called automatically when using `foreach ($watcher as $event)`. The iterator yields `Async\FileSystemEvent` objects. When the buffer is empty, the coroutine suspends until a new event arrives. Iteration ends when the watcher is closed and the buffer is exhausted. ## Parameters No parameters. ## Return Value `Iterator` --- an iterator yielding `Async\FileSystemEvent` objects. ## Errors/Exceptions * `Error` --- if the iterator is used outside a coroutine. ## Examples ### Example #1 Standard usage with foreach ```php close(); }); foreach ($watcher as $event) { echo "Event: {$event->filename}"; echo " renamed={$event->renamed}"; echo " changed={$event->changed}\n"; } echo "Iteration completed\n"; }); ?> ``` ### Example #2 Interrupting with break ```php filename === 'stop.flag') { break; } processEvent($event); } $watcher->close(); ?> ``` ## See Also * [FileSystemWatcher](/en/docs/components/filesystem-watcher.html) --- Concept overview * [FileSystemWatcher::close](/en/docs/reference/filesystem-watcher/close.html) --- Stop watching --- --- url: https://true-async.github.io/en/docs/reference/filesystem-watcher/is-closed.md description: Check if file system watching has been stopped. --- # FileSystemWatcher::isClosed (PHP 8.6+, True Async 1.0) ```php public FileSystemWatcher::isClosed(): bool ``` Returns `true` if watching has been stopped --- `close()` was called, the scope was cancelled, or an error occurred. ## Parameters No parameters. ## Return Value `true` --- the watcher is closed, `false` --- it is active. ## Examples ### Example #1 ```php isClosed()); // false $watcher->close(); var_dump($watcher->isClosed()); // true ?> ``` ## See Also * [FileSystemWatcher::close](/en/docs/reference/filesystem-watcher/close.html) --- Stop watching --- --- url: https://true-async.github.io/en/docs/frankenphp.md description: >- Running TrueAsync PHP with FrankenPHP — Docker quick start, building from source, Caddyfile configuration, async worker entrypoint, graceful restart, and troubleshooting. --- # FrankenPHP + TrueAsync > **A native alternative is here.** Since 2026-05 TrueAsync ships its own server — > [**TrueAsync Server**](/en/docs/server/index.html). It is a PHP extension that runs > HTTP/1.1/2/3 directly inside the PHP process, with no Go runtime and no Caddy. FrankenPHP is > still relevant when you need Caddy features (automatic Let's Encrypt, Caddyfile routing) or > integration into an existing Caddy deployment. [FrankenPHP](https://frankenphp.dev) is a PHP application server built on top of [Caddy](https://caddyserver.com). It embeds the PHP runtime directly into a Go process, eliminating the overhead of a separate FastCGI proxy. In the TrueAsync fork of FrankenPHP, a single PHP thread handles **many requests concurrently** — each incoming HTTP request gets its own coroutine, and the TrueAsync scheduler switches between them while they are waiting for I/O. ``` Traditional FPM / regular FrankenPHP: 1 request → 1 thread (blocked during I/O) TrueAsync FrankenPHP: N requests → 1 thread (coroutines, non-blocking I/O) ``` ## Quick Start — Docker The fastest way to try the setup is with the pre-built Docker image: ```bash docker run --rm -p 8080:8080 trueasync/php-true-async:latest-frankenphp ``` Open — you will see the live dashboard showing PHP version, active coroutines, memory, and uptime. ### Available image tags | Tag | Description | |-----|-------------| | `latest-frankenphp` | Latest stable, latest PHP | | `latest-php8.6-frankenphp` | Latest stable, PHP 8.6 | | `0.6.4-php8.6-frankenphp` | Specific release | ### Running your own PHP application Mount your application directory and provide a custom `Caddyfile`: ```bash docker run --rm -p 8080:8080 \ -v $PWD/app:/app \ -v $PWD/Caddyfile:/etc/caddy/Caddyfile \ trueasync/php-true-async:latest-frankenphp ``` ## Install from Source Building from source gives you a native `frankenphp` binary alongside the `php` binary. ### Linux (Ubuntu / Debian) ```bash curl -fsSL https://raw.githubusercontent.com/true-async/releases/master/installer/build-linux.sh | \ BUILD_FRANKENPHP=true NO_INTERACTIVE=true bash ``` Or interactively — the wizard will ask about FrankenPHP as part of the extension preset selection. ### macOS ```bash curl -fsSL https://raw.githubusercontent.com/true-async/releases/master/installer/build-macos.sh | \ BUILD_FRANKENPHP=true NO_INTERACTIVE=true bash ``` ### What gets installed After a successful build both binaries are placed in `$INSTALL_DIR/bin/`: ``` ~/.php-trueasync/bin/php # PHP CLI ~/.php-trueasync/bin/frankenphp # FrankenPHP server binary ``` ## Caddyfile Configuration FrankenPHP is configured via a `Caddyfile`. The minimal configuration for an async TrueAsync worker: ```txt { admin off frankenphp { num_threads 4 # total PHP threads across all workers (default: 2× CPU cores) } } :8080 { root * /app php_server { index off file_server off worker { file /app/entrypoint.php num 1 async match /* } } } ``` ### Global `frankenphp` directives | Directive | Description | |-----------|-------------| | `num_threads N` | Total PHP thread pool size. Defaults to `2 × CPU cores`. All workers share this pool | ### Key worker directives | Directive | Description | |-----------|-------------| | `file` | Path to the PHP entrypoint script | | `num` | Number of PHP threads assigned to this worker. Start with `1` and tune based on CPU-bound work | | `async` | **Required** — enables TrueAsync coroutine mode | | `drain_timeout` | Grace period for in-flight requests during graceful restart (default `30s`) | | `match` | URL pattern handled by this worker | ### Multiple workers You can run different entrypoints for different routes: ```txt :8080 { root * /app php_server { worker { file /app/api.php num 2 async match /api/* } worker { file /app/web.php num 1 async match /* } } } ``` ## Writing the Entrypoint The entrypoint is a long-running PHP script. It registers a request handler callback and then hands control to `FrankenPHP`, which blocks until the server shuts down. ```php getUri(), PHP_URL_PATH); $response->setStatus(200); $response->setHeader('Content-Type', 'text/plain'); $response->write("Hello from TrueAsync! Path: $path"); $response->end(); }); ``` The handler receives a [`Request`](/en/docs/reference/frankenphp/request.html) and a [`Response`](/en/docs/reference/frankenphp/response.html) object. Each request runs in its own coroutine — there are no shared globals, so handlers are safe for concurrent execution. > **Important:** always call `response->end()` to send the response, even when the body is empty. > Omitting `end()` will hang the request. ### API Reference | Class | Description | |-------|-------------| | [`FrankenPHP\Request`](/en/docs/reference/frankenphp/request.html) | Read-only access to HTTP method, URI, headers, body, query params, cookies, and uploaded files | | [`FrankenPHP\Response`](/en/docs/reference/frankenphp/response.html) | Set status, headers, buffer body with `write()`, send with `end()`, redirect | | [`FrankenPHP\UploadedFile`](/en/docs/reference/frankenphp/uploaded-file.html) | Uploaded file metadata (name, type, size, error) and `moveTo()` | ### Async I/O inside the handler Because each request runs in its own coroutine, you can use blocking I/O calls freely — they will yield the coroutine instead of blocking the thread: ```php HttpServer::onRequest(function (Request $request, Response $response): void { $db = new PDO('pgsql:host=localhost;dbname=app', 'user', 'pass'); $rows = $db->query('SELECT * FROM users LIMIT 10')->fetchAll(); $response->setStatus(200); $response->setHeader('Content-Type', 'application/json'); $response->write(json_encode($rows)); $response->end(); }); ``` ### Spawning additional coroutines The handler itself is already a coroutine, so you can `spawn()` child work: ```php use function Async\spawn; use function Async\await; HttpServer::onRequest(function (Request $request, Response $response): void { // Fan-out: run two DB queries concurrently $users = spawn(fn() => fetchUsers()); $totals = spawn(fn() => fetchTotals()); $data = [ 'users' => await($users), 'totals' => await($totals), ]; $response->setStatus(200); $response->setHeader('Content-Type', 'application/json'); $response->write(json_encode($data)); $response->end(); }); ``` ## Tuning ### Worker thread count (`num`) Each PHP thread runs one TrueAsync scheduler loop. A single thread already handles thousands of concurrent I/O-bound requests via coroutines. Add more threads only when you have CPU-bound work that benefits from true parallelism (each thread runs on a separate OS thread thanks to ZTS). A good starting point: ``` I/O-heavy API: num 1–2 Mixed workload: num = number of CPU cores / 2 CPU-heavy: num = number of CPU cores ``` ## Graceful Restart Async workers support **green-blue restarts** — code is reloaded without dropping in-flight requests. When a restart is triggered (via admin API, file watcher, or config reload): 1. Old threads are **detached** — no new requests are routed to them. 2. In-flight requests get a grace period (`drain_timeout`, default `30s`) to finish. 3. Old threads shut down and release their resources. 4. Fresh threads boot with the updated PHP code. During the drain window new requests receive `HTTP 503`. Once the new threads are ready, traffic resumes normally. ### Trigger via Admin API ```bash curl -X POST http://localhost:2019/frankenphp/workers/restart ``` The Caddy admin API listens on `localhost:2019` by default. To enable it, remove `admin off` from your global block (or restrict it to localhost): ```txt { admin localhost:2019 frankenphp { num_threads 4 } } ``` ### Configuring the drain timeout ```txt worker { file entrypoint.php num 2 async drain_timeout 30s # grace period for in-flight requests (default 30s) match /* } ``` ## Checking the installation ```bash # Version frankenphp version # Start with a config frankenphp run --config /etc/caddy/Caddyfile # Validate the Caddyfile without starting frankenphp adapt --config /etc/caddy/Caddyfile ``` Check that TrueAsync is active from PHP: ```php var_dump(extension_loaded('true_async')); // bool(true) var_dump(ZEND_THREAD_SAFE); // bool(true) ``` ## Troubleshooting ### Requests never arrive at the PHP handler Make sure the worker has `async` enabled **and** that the Caddy matcher routes traffic to it. Without `match *` (or a specific pattern) no requests reach the async worker. ### Requests getting `HTTP 503` All PHP threads are busy and the thread queue is saturated, or a graceful restart is in progress. Increase `num` to add more threads, or reduce `drain_timeout` if deploys are taking too long. ## Source code | Repository | Description | |------------|-------------| | [true-async/frankenphp](https://github.com/true-async/frankenphp/tree/true-async) | TrueAsync fork of FrankenPHP (`true-async` branch) | | [true-async/releases](https://github.com/true-async/releases) | Docker images, installers, build configuration | For a deep dive into how the Go ↔ PHP integration works internally, see the [FrankenPHP Architecture](/en/architecture/frankenphp.html) page. --- --- url: https://true-async.github.io/en/docs/reference/frankenphp/request.md description: >- FrankenPHP\Request class — access HTTP request data (method, URI, headers, body, query parameters, cookies, uploaded files) inside an async worker. --- # FrankenPHP\Request (True Async 0.6+) The `Request` object is passed to your handler callback by `HttpServer::onRequest()`. It provides read-only access to all HTTP request data. Each request coroutine receives its own `Request` instance — there are no shared globals, so concurrent handlers are always safe. ## Class Synopsis ```php namespace FrankenPHP; class Request { public function getMethod(): string; public function getUri(): string; public function getHeader(string $name): ?string; public function getHeaders(): array; public function getBody(): string; public function getQueryParams(): array; public function getCookies(): array; public function getHost(): string; public function getRemoteAddr(): string; public function getScheme(): string; public function getProtocolVersion(): string; public function getParsedBody(): array; public function getUploadedFiles(): array; } ``` ## Methods ### getMethod ```php public Request::getMethod(): string ``` Returns the HTTP method: `GET`, `POST`, `PUT`, `DELETE`, etc. ### getUri ```php public Request::getUri(): string ``` Returns the full request URI including the query string (e.g. `/api/users?page=2`). ### getHeader ```php public Request::getHeader(string $name): ?string ``` Returns a single header value by name, or `null` if the header is not present. Header names are case-insensitive. ### getHeaders ```php public Request::getHeaders(): array ``` Returns all headers as an associative array `name => value`. When a header has multiple values they are joined with `, `. ### getBody ```php public Request::getBody(): string ``` Returns the full request body as a string. The body is read once and cached — subsequent calls return the same value. ### getQueryParams ```php public Request::getQueryParams(): array ``` Returns the parsed and URL-decoded query string as an associative array. ### getCookies ```php public Request::getCookies(): array ``` Returns cookies parsed from the `Cookie` header as an associative array `name => value`. ### getHost ```php public Request::getHost(): string ``` Returns the value of the `Host` header. ### getRemoteAddr ```php public Request::getRemoteAddr(): string ``` Returns the client address in `ip:port` format. ### getScheme ```php public Request::getScheme(): string ``` Returns `http` or `https`. ### getProtocolVersion ```php public Request::getProtocolVersion(): string ``` Returns the HTTP protocol version, e.g. `HTTP/1.1` or `HTTP/2.0`. ### getParsedBody ```php public Request::getParsedBody(): array ``` Returns form fields from `application/x-www-form-urlencoded` and `multipart/form-data` bodies as an associative array. ### getUploadedFiles ```php public Request::getUploadedFiles(): array ``` Returns uploaded files as an array of [`FrankenPHP\UploadedFile`](/en/docs/reference/frankenphp/uploaded-file.html) objects. Multiple files for the same field are returned as an array. ## Examples ### Example #1 Routing by method and path ```php getMethod(); $path = parse_url($request->getUri(), PHP_URL_PATH); if ($method === 'GET' && $path === '/health') { $response->setStatus(200); $response->write('OK'); $response->end(); return; } $response->setStatus(404); $response->write('Not Found'); $response->end(); }); ``` ### Example #2 Reading query parameters and cookies ```php getCookies(); $params = $request->getQueryParams(); $name = $params['name'] ?? $cookies['username'] ?? 'Guest'; $response->setStatus(200); $response->setHeader('Content-Type', 'text/plain'); $response->write("Hello, {$name}!"); $response->end(); }); ``` ### Example #3 Processing a JSON body ```php getMethod() !== 'POST') { $response->setStatus(405); $response->end(); return; } $data = json_decode($request->getBody(), true); $response->setStatus(200); $response->setHeader('Content-Type', 'application/json'); $response->write(json_encode(['received' => $data])); $response->end(); }); ``` ## See Also * [FrankenPHP\Response](/en/docs/reference/frankenphp/response.html) -- Building and sending responses * [FrankenPHP\UploadedFile](/en/docs/reference/frankenphp/uploaded-file.html) -- Working with uploaded files * [FrankenPHP Integration Guide](/en/docs/frankenphp.html) -- Installation, configuration, and deployment --- --- url: https://true-async.github.io/en/docs/reference/frankenphp/response.md description: >- FrankenPHP\Response class — set status codes, headers, write body content, redirect, and send the HTTP response from an async worker. --- # FrankenPHP\Response (True Async 0.6+) The `Response` object is passed to your handler callback by `HttpServer::onRequest()`. It provides methods to set the status code, headers, body, and send the response to the client. > **Important:** always call `end()` to send the response, even when the body is empty. > `write()` buffers data in the object; `end()` sends everything to the client. > Omitting `end()` will hang the request. ## Class Synopsis ```php namespace FrankenPHP; class Response { public function setStatus(int $code): void; public function getStatus(): int; public function setHeader(string $name, string $value): void; public function addHeader(string $name, string $value): void; public function removeHeader(string $name): void; public function getHeader(string $name): ?string; public function getHeaders(): array; public function isHeadersSent(): bool; public function redirect(string $url, int $code = 302): void; public function write(string $data): void; public function end(): void; } ``` ## Methods ### setStatus ```php public Response::setStatus(int $code): void ``` Set the HTTP status code. Default is `200`. ### getStatus ```php public Response::getStatus(): int ``` Returns the current status code. ### setHeader ```php public Response::setHeader(string $name, string $value): void ``` Set a header, replacing any existing values for that name. ### addHeader ```php public Response::addHeader(string $name, string $value): void ``` Append a header value. Use this for headers that can have multiple values, such as `Set-Cookie`. ### removeHeader ```php public Response::removeHeader(string $name): void ``` Remove a previously set header. ### getHeader ```php public Response::getHeader(string $name): ?string ``` Returns the first value of a header, or `null` if not set. ### getHeaders ```php public Response::getHeaders(): array ``` Returns all headers as an associative array `name => [values...]`. ### isHeadersSent ```php public Response::isHeadersSent(): bool ``` Returns `true` if `end()` has already been called. ### redirect ```php public Response::redirect(string $url, int $code = 302): void ``` Sets the `Location` header and the status code. You still need to call `end()` after. ### write ```php public Response::write(string $data): void ``` Buffers response body data. Can be called multiple times — chunks are concatenated. ### end ```php public Response::end(): void ``` Sends the status code, headers, and buffered body to the client. **Must be called** to complete the response. After `end()`, further calls to `write()` or header methods have no effect. ## Examples ### Example #1 Basic text response ```php setStatus(200); $response->setHeader('Content-Type', 'text/plain'); $response->write('Hello, World!'); $response->end(); }); ``` ### Example #2 JSON API response ```php 'ok', 'time' => time()]; $response->setStatus(200); $response->setHeader('Content-Type', 'application/json'); $response->write(json_encode($data)); $response->end(); }); ``` ### Example #3 Setting cookies and redirecting ```php getCookies(); if (!isset($cookies['session'])) { $response->addHeader('Set-Cookie', 'session=abc123; Path=/; HttpOnly'); $response->addHeader('Set-Cookie', 'theme=dark; Path=/'); $response->redirect('/welcome'); $response->end(); return; } $response->setStatus(200); $response->setHeader('Content-Type', 'text/plain'); $response->write('Welcome back!'); $response->end(); }); ``` ### Example #4 Streaming-style writes ```php setStatus(200); $response->setHeader('Content-Type', 'text/plain'); // Multiple write() calls — all data is buffered and sent at end() $response->write("Line 1\n"); $response->write("Line 2\n"); $response->write("Line 3\n"); $response->end(); }); ``` ## See Also * [FrankenPHP\Request](/en/docs/reference/frankenphp/request.html) -- Reading request data * [FrankenPHP\UploadedFile](/en/docs/reference/frankenphp/uploaded-file.html) -- Working with uploaded files * [FrankenPHP Integration Guide](/en/docs/frankenphp.html) -- Installation, configuration, and deployment --- --- url: https://true-async.github.io/en/docs/reference/frankenphp/uploaded-file.md description: >- FrankenPHP\UploadedFile class — access uploaded file metadata (name, type, size) and move files to a permanent location. --- # FrankenPHP\UploadedFile (True Async 0.6+) `UploadedFile` objects are returned by [`Request::getUploadedFiles()`](/en/docs/reference/frankenphp/request.html#getuploadedfiles). Each object represents one uploaded file and provides access to its metadata and a method to move it to a permanent location. Multiple files uploaded under the same field name are returned as an array of `UploadedFile` objects. ## Class Synopsis ```php namespace FrankenPHP; class UploadedFile { public function getName(): string; public function getType(): string; public function getSize(): int; public function getTmpName(): string; public function getError(): int; public function moveTo(string $path): bool; } ``` ## Methods ### getName ```php public UploadedFile::getName(): string ``` Returns the original filename as sent by the client. > **Note:** never trust the original filename for storage. Always sanitize or generate > a safe name before saving. ### getType ```php public UploadedFile::getType(): string ``` Returns the MIME type reported by the client (e.g. `image/png`). ### getSize ```php public UploadedFile::getSize(): int ``` Returns the file size in bytes. ### getTmpName ```php public UploadedFile::getTmpName(): string ``` Returns the path to the temporary file on disk. ### getError ```php public UploadedFile::getError(): int ``` Returns the upload error code. `UPLOAD_ERR_OK` (0) means success. See [PHP upload error constants](https://www.php.net/manual/en/features.file-upload.errors.php). ### moveTo ```php public UploadedFile::moveTo(string $path): bool ``` Moves the uploaded file to the given destination path. Returns `true` on success. ## Examples ### Example #1 Handling a single file upload ```php getUploadedFiles(); if (isset($files['avatar'])) { $file = $files['avatar']; if ($file->getError() === UPLOAD_ERR_OK) { $safeName = bin2hex(random_bytes(16)) . '.png'; $file->moveTo('/uploads/' . $safeName); $response->setStatus(200); $response->write("Uploaded: {$file->getName()} ({$file->getSize()} bytes)"); } else { $response->setStatus(400); $response->write("Upload error code: {$file->getError()}"); } } else { $response->setStatus(400); $response->write('No file uploaded'); } $response->end(); }); ``` ### Example #2 Handling multiple files ```php getUploadedFiles(); $saved = []; // Multiple files uploaded as photos[] $photos = $files['photos'] ?? []; if (!is_array($photos)) { $photos = [$photos]; } foreach ($photos as $file) { if ($file->getError() === UPLOAD_ERR_OK) { $dest = '/uploads/' . bin2hex(random_bytes(8)) . '_' . $file->getName(); $file->moveTo($dest); $saved[] = $file->getName(); } } $response->setStatus(200); $response->setHeader('Content-Type', 'application/json'); $response->write(json_encode(['saved' => $saved])); $response->end(); }); ``` ## See Also * [FrankenPHP\Request](/en/docs/reference/frankenphp/request.html) -- Reading request data * [FrankenPHP\Response](/en/docs/reference/frankenphp/response.html) -- Building and sending responses * [FrankenPHP Integration Guide](/en/docs/frankenphp.html) -- Installation, configuration, and deployment --- --- url: https://true-async.github.io/en/tutors/06-future.md description: 'Future and FutureState: a promise of a result that isn''t tied to a coroutine.' --- # Future In the previous chapters we found that a coroutine behaves like a promise of a result: `await` can be called as many times as you like, and it always returns the same thing. But a coroutine has a hard constraint: the result is always produced by the function that `spawn` started. One call, one function, one result. But what if the result doesn't come from a function at all? Let's go back to `ProfileService`. A user changes their delivery address, and before saving it, the address needs to be checked against a directory of regions served by the `GeoDirectory` service: ```php function loadRegions(): array { $response = file_get_contents('https://geodirectory.example.com/api/regions'); return json_decode($response, true); } ``` The directory is large, slow to load, and the same for everyone. At the same time, every profile-update request needs it, and requests are handled concurrently. Calling `spawn(loadRegions(...))` in each one means bombarding `GeoDirectory` with identical requests for the sake of an identical answer. Wasteful. What we'd like is for the first request to actually load the directory, while all the others wait for its result. For that we need a place where one piece of code puts the result and another picks it up. That place can be a `Future`. ## FutureState and Future `Future` is a promise and a container for a result. It isn't tied to a coroutine, but it works with the `await` we already know. `Future` consists of two objects: ```php use Async\Future; use Async\FutureState; $state = new FutureState(); $future = new Future($state); ``` * **`FutureState`** — writes the result. It stays with whoever produces the result. * **`Future`** — reads the result. It's handed to whoever is waiting for the result. The producer completes the operation, the consumer waits for it via the familiar `await`: ```php use function Async\await; $state->complete(42); echo await($future); // 42 ``` Why two objects instead of one? The split protects against mistakes. Whoever holds a `Future` physically cannot complete the operation: there's no such method on it. Only the owner of the `FutureState` is allowed to write the result, and only once: ```php $state->complete(1); $state->complete(2); // AsyncException: FutureState is already completed ``` If the operation fails, an exception is written instead of a result, and `await` will throw it for everyone who's waiting: ```php $state->error(new RemoteApiException('GeoDirectory did not respond')); await($future); // throws RemoteApiException ``` ## Solving the directory problem Now we can build directory loading that concurrent requests share between them: ```php use Async\Future; use Async\FutureState; use function Async\spawn; final class RegionsDirectory { private ?Future $future = null; public function regions(): Future { if ($this->future !== null) { return $this->future; } $state = new FutureState(); $this->future = new Future($state); spawn(function () use ($state) { try { $state->complete(loadRegions()); } catch (Exception $e) { $state->error($e); } }); return $this->future; } } ``` The first call to `regions` starts a coroutine that does the actual loading. Every subsequent call gets the same `Future` and waits on one shared result. Once the directory is loaded, `await` starts returning it instantly, no matter how many times it's asked: ```php $regions = await($directory->regions()); if (profileExists($userId) && isset($regions[$changes['region']])) { updateProfile($userId, $changes); } ``` However many requests are handled concurrently, `GeoDirectory` gets hit exactly once, while `RegionsDirectory` stays an ordinary service: it can be wired up in a DI container, swapped out in tests, and all its state lives in a single `$future` field. Note that `await` works the same way with a coroutine and with a `Future`, including the timeout from the previous chapter: ```php $regions = await($directory->regions(), timeout(2000)); ``` ## A result you already have Sometimes the result is known ahead of time. For example, the region directory gets warmed up when the service starts and is already sitting in memory. There's no point creating a `FutureState` and a coroutine just for an already-known value, so there are factory methods for that: ```php // the result is already there $future = Future::completed($regionsFromWarmup); // the error is already known $future = Future::failed(new RemoteApiException('GeoDirectory did not respond')); ``` The `regions` method can return such a `Future`, and the consumer won't notice the difference: it still calls `await` and gets the result immediately. ## A classic technique: memoization `RegionsDirectory` already implements a classic technique called memoization: compute the result once and hand it out to every repeated call. The technique is general enough to be wrapped around any function with arguments: ```php use Async\Future; use Async\FutureState; use function Async\spawn; function memoize(callable $fn): callable { $cache = []; return function (mixed ...$args) use ($fn, &$cache): Future { $key = serialize($args); if (isset($cache[$key])) { return $cache[$key]; } $state = new FutureState(); $cache[$key] = new Future($state); spawn(function () use ($state, $fn, $args) { try { $state->complete($fn(...$args)); } catch (Throwable $e) { $state->error($e); } }); return $cache[$key]; }; } ``` ```php $regionsOf = memoize(loadRegionsOf(...)); $de = await($regionsOf('DE')); // a request to GeoDirectory $fr = await($regionsOf('FR')); // a request to GeoDirectory $de = await($regionsOf('DE')); // instant, from the cache ``` Notice a subtlety: the cache holds the `Future` itself, not a plain value. A naive memoization in concurrent code would suffer from a race: while the first call is waiting for `GeoDirectory`'s answer, a second call checks the cache, sees it empty, and fires off a duplicate request. There's no such gap here. The `Future` lands in the cache immediately, before the computation even starts, so the second call finds it and simply waits. The technique has a boundary: memoization only works for data that doesn't change for the life of the process. Caching a token check or a currency rate this way would be a mistake: they depend on time, and such a cache needs a lifetime after which the result is fetched again. For the same reason, errors deserve separate thought: a failed `Future` would also stay in the cache forever, and it's usually better to remove it so the next call retries. The main thing to take from this chapter: a coroutine answers the question "how do I get the result", while a `Future` simply promises that a result will exist. Where it comes from, a coroutine, a cache, or another thread, is none of the consumer's business. The producer and the consumer have agreed on a result and know nothing else about each other. But what if there isn't just one result, but an entire stream of values that need to travel from coroutine to coroutine? There's a separate tool for that, and that's the subject of the next chapter. --- --- url: https://true-async.github.io/en/docs/components/future.md description: >- Future in TrueAsync -- a promise of a result, transformation chains map/catch/finally, FutureState and diagnostics. --- # Future: A Promise of a Result ## What is Future `Async\Future` is an object representing the result of an operation that may not be ready yet. Future allows you to: * Await the result via `await()` or `$future->await()` * Build transformation chains via `map()`, `catch()`, `finally()` * Cancel the operation via `cancel()` * Create already-completed Futures via static factories Future is similar to `Promise` in JavaScript, but integrated with TrueAsync coroutines. ## Future and FutureState Future is split into two classes with a clear separation of concerns: * **`FutureState`** -- a mutable container through which the result is written * **`Future`** -- a readonly wrapper through which the result is read and transformed ```php complete(42); // The consumer gets the result $result = $future->await(); // 42 ?> ``` This separation guarantees that the consumer cannot accidentally complete the Future -- only the holder of `FutureState` has that right. ## Creating a Future ### Via FutureState ```php complete(json_decode($data, true)); }); $result = $future->await(); ?> ``` ### Static Factories For creating already-completed Futures: ```php await(); // 42 // Future with an error $future = Future::failed(new \RuntimeException('Something went wrong')); $result = $future->await(); // throws RuntimeException ?> ``` ## Transformation Chains Future supports three transformation methods, working similarly to Promise in JavaScript: ### map() -- Transforming the Result Called only on successful completion. Returns a new Future with the transformed result: ```php map(fn($value) => $value * 2); $asString = $doubled->map(fn($value) => "Result: $value"); $state->complete(21); echo $asString->await(); // "Result: 42" ?> ``` ### catch() -- Handling Errors Called only on error. Allows recovery from an exception: ```php catch(function(\Throwable $e) { return 'Default value'; }); $state->error(new \RuntimeException('Error')); echo $safe->await(); // "Default value" ?> ``` ### finally() -- Execution on Any Outcome Always called -- both on success and on error. The parent Future's result is passed to the child unchanged: ```php finally(function($resultOrException) { // Release resources echo "Operation completed\n"; }); $state->complete('data'); echo $withCleanup->await(); // "data" (result is passed unchanged) ?> ``` ### Composite Chains ```php map(fn($data) => json_decode($data, true)) ->map(fn($parsed) => $parsed['name'] ?? 'Unknown') ->catch(fn(\Throwable $e) => 'Error: ' . $e->getMessage()) ->finally(function($value) { // Logging }); $state->complete('{"name": "PHP"}'); echo $result->await(); // "PHP" ?> ``` ### Independent Subscribers Each call to `map()` on the same Future creates an **independent** chain. Subscribers do not affect each other: ```php map(fn($x) => $x * 2); $tripled = $future->map(fn($x) => $x * 3); $state->complete(10); echo await($doubled) . "\n"; // 20 echo await($tripled) . "\n"; // 30 ?> ``` ### Error Propagation in Chains If the source Future completes with an error, `map()` is **skipped**, and the error is passed directly to `catch()`: ```php map(function($value) { echo "This code won't execute\n"; return $value; }) ->catch(function(\Throwable $e) { return 'Recovered: ' . $e->getMessage(); }); $state->error(new \RuntimeException('Source error')); echo await($result) . "\n"; // "Recovered: Source error" ?> ``` If an exception occurs **inside** `map()`, it is caught by the subsequent `catch()`: ```php map(function($x) { throw new \RuntimeException('Error in map'); }) ->catch(function(\Throwable $e) { return 'Caught: ' . $e->getMessage(); }); $state->complete(42); echo await($result) . "\n"; // "Caught: Error in map" ?> ``` ## Awaiting the Result ### Via the await() Function ```php await() Method ```php await(); // With cancellation timeout $result = $future->await(Async\timeout(5000)); ``` ## Cancelling a Future ```php cancel(); // Cancel with a custom error $future->cancel(new AsyncCancellation('Operation is no longer needed')); ``` ## Suppressing Warnings: ignore() If a Future is not used (neither `await()`, `map()`, `catch()` nor `finally()` was called), TrueAsync will issue a warning. To explicitly suppress this warning: ```php ignore(); ``` Also, if a Future completed with an error and that error was not handled, TrueAsync will warn about it. `ignore()` suppresses this warning as well. ## FutureState: Completing the Operation ### complete() -- Successful Completion ```php complete($result); ``` ### error() -- Completion with an Error ```php error(new \RuntimeException('Error')); ``` ### Constraints * `complete()` and `error()` can only be called **once**. A repeated call will throw `AsyncException`. * After calling `complete()` or `error()`, the Future's state is immutable. ```php complete(1); $state->complete(2); // AsyncException: FutureState is already completed ``` ## Diagnostics Both classes (`Future` and `FutureState`) provide diagnostic methods: ```php isCompleted(); // bool $future->isCancelled(); // bool // Where the Future was created $future->getCreatedFileAndLine(); // [string $file, int $line] $future->getCreatedLocation(); // "file.php:42" // Where the Future was completed $future->getCompletedFileAndLine(); // [string|null $file, int $line] $future->getCompletedLocation(); // "file.php:55" or "unknown" // Awaiting information $future->getAwaitingInfo(); // array ``` ## Practical Example: HTTP Client ```php complete($response); } catch (\Throwable $e) { $state->error($e); } }); return $future; } // Usage $userFuture = httpGet('https://api.example.com/user/1') ->map(fn($json) => json_decode($json, true)) ->catch(fn($e) => ['error' => $e->getMessage()]); $result = $userFuture->await(); ?> ``` ## See Also * [await()](/en/docs/reference/await.html) -- awaiting completion * [Coroutines](/en/docs/components/coroutines.html) -- the basic unit of concurrency * [Cancellation](/en/docs/components/cancellation.html) -- the cancellation mechanism --- --- url: https://true-async.github.io/en/docs/reference/future/construct.md description: Creates a Future bound to a FutureState. --- # Future::\_\_construct (PHP 8.6+, True Async 1.0) ```php public function __construct(FutureState $state) ``` Creates a new `Future` bound to a `FutureState` object. `FutureState` manages the Future's state and allows completing it externally with a result or error. ## Parameters `state` — the `FutureState` object that manages the state of this Future. ## Examples ### Example #1 Creating a Future via FutureState ```php complete($result); }); // Await the result $value = $future->await(); echo "Received: $value\n"; ``` ### Example #2 Creating a Future with a deferred result ```php await(); echo "Result: $result\n"; }); // Another coroutine provides the result \Async\spawn(function() use ($state) { \Async\delay(100); $state->complete("Done!"); }); ``` ## See also * [Future::completed](/en/docs/reference/future/completed.html) — Create an already completed Future * [Future::failed](/en/docs/reference/future/failed.html) — Create a Future with an error --- --- url: https://true-async.github.io/en/docs/reference/future/await.md description: Await the Future result. --- # Future::await (PHP 8.6+, True Async 1.0) ```php public function await(?Completable $cancellation = null): mixed ``` Awaits the completion of the `Future` and returns its result. Blocks the current coroutine until the Future is completed. If the Future completed with an error, the method throws that exception. You can pass a `Completable` to cancel the wait by timeout or external condition. ## Parameters `cancellation` — a wait cancellation object. If provided and triggered before the Future completes, a `AsyncCancellation` will be thrown. Defaults to `null`. ## Return value `mixed` — the Future result. ## Errors Throws an exception if the Future completed with an error or was cancelled. ## Examples ### Example #1 Basic result awaiting ```php complete(42); }); $result = $future->await(); echo "Result: $result\n"; // Result: 42 ``` ### Example #2 Handling errors during await ```php error(new \RuntimeException("Something went wrong")); }); try { $result = $future->await(); } catch (\RuntimeException $e) { echo "Error: " . $e->getMessage() . "\n"; // Error: Something went wrong } ``` ## See also * [Future::isCompleted](/en/docs/reference/future/is-completed.html) — Check if the Future is completed * [Future::cancel](/en/docs/reference/future/cancel.html) — Cancel the Future * [Future::map](/en/docs/reference/future/map.html) — Transform the result --- --- url: https://true-async.github.io/en/docs/reference/future/cancel.md description: Cancels the Future. --- # Future::cancel (PHP 8.6+, True Async 1.0) ```php public function cancel(?AsyncCancellation $cancellation = null): void ``` Cancels the `Future`. All coroutines awaiting this Future via `await()` will receive a `AsyncCancellation`. If the `$cancellation` parameter is provided, it will be used as the cancellation reason. ## Parameters `cancellation` — a custom cancellation exception. If `null`, the default `AsyncCancellation` is used. ## Return value The function does not return a value. ## Examples ### Example #1 Basic Future cancellation ```php await(); } catch (\Async\AsyncCancellation $e) { echo "Future cancelled\n"; } }); // Cancel the Future $future->cancel(); ``` ### Example #2 Cancellation with a custom reason ```php await(); } catch (\Async\AsyncCancellation $e) { echo "Reason: " . $e->getMessage() . "\n"; // Reason: Timeout exceeded } }); $future->cancel(new AsyncCancellation("Timeout exceeded")); ``` ## See also * [Future::isCancelled](/en/docs/reference/future/is-cancelled.html) — Check if the Future is cancelled * [Future::await](/en/docs/reference/future/await.html) — Await the result * [Future::catch](/en/docs/reference/future/catch.html) — Handle Future errors --- --- url: https://true-async.github.io/en/docs/reference/future/catch.md description: Handle a Future error. --- # Future::catch (PHP 8.6+, True Async 1.0) ```php public function catch(callable $catch): Future ``` Registers an error handler for the `Future`. The callback is invoked if the Future completed with an exception. If the callback returns a value, it becomes the result of the new Future. If the callback throws an exception, the new Future completes with that error. ## Parameters `catch` — the error handling function. Receives a `Throwable`, may return a value for recovery. Signature: `function(\Throwable $e): mixed`. ## Return value `Future` — a new Future with the error handling result, or with the original value if there was no error. ## Examples ### Example #1 Error handling with recovery ```php catch(function(\Throwable $e) { echo "Error: " . $e->getMessage() . "\n"; return "default value"; // Recovery }); $result = $future->await(); echo $result; // default value ``` ### Example #2 Catching errors in async operations ```php status !== 200) { throw new \RuntimeException("HTTP error: {$response->status}"); } $state->complete(json_decode($response->body, true)); } catch (\Throwable $e) { $state->error($e); } }); $future = $source ->catch(function(\Throwable $e) { // Log the error and return an empty array error_log("API error: " . $e->getMessage()); return []; }) ->map(function(array $users) { return count($users); }); $count = $future->await(); echo "Users found: $count\n"; ``` ## See also * [Future::map](/en/docs/reference/future/map.html) — Transform the Future result * [Future::finally](/en/docs/reference/future/finally.html) — Callback on Future completion * [Future::ignore](/en/docs/reference/future/ignore.html) — Ignore unhandled errors --- --- url: https://true-async.github.io/en/docs/reference/future/completed.md description: Creates an already completed Future with a result. --- # Future::completed (PHP 8.6+, True Async 1.0) ```php public static function completed(mixed $value = null): Future ``` Creates an already completed `Future` with the specified value. This is a factory method that returns a `Future` immediately containing a result. Useful for returning an already known value from functions that return a `Future`. ## Parameters `value` — the value with which the Future will be completed. Defaults to `null`. ## Return value `Future` — a completed Future with the specified value. ## Examples ### Example #1 Creating a Future with a ready value ```php isCompleted()); // bool(true) var_dump($future->await()); // int(42) ``` ### Example #2 Using in a function that returns a Future ```php complete(loadFromDatabase($key)); } catch (\Throwable $e) { $state->error($e); } }); return new Future($state); } $result = fetchData('user:1')->await(); echo "Result: $result\n"; ``` ## See also * [Future::failed](/en/docs/reference/future/failed.html) — Create a Future with an error * [Future::\_\_construct](/en/docs/reference/future/construct.html) — Create a Future via FutureState * [Future::await](/en/docs/reference/future/await.html) — Await the result --- --- url: https://true-async.github.io/en/docs/reference/future/failed.md description: Creates a Future completed with an error. --- # Future::failed (PHP 8.6+, True Async 1.0) ```php public static function failed(\Throwable $throwable): Future ``` Creates a `Future` that is immediately completed with the specified error. Calling `await()` on such a Future will throw the provided exception. ## Parameters `throwable` — the exception with which the Future will be completed. ## Return value `Future` — a completed Future with an error. ## Examples ### Example #1 Creating a Future with an error ```php isCompleted()); // bool(true) try { $future->await(); } catch (\RuntimeException $e) { echo "Caught: " . $e->getMessage() . "\n"; // Caught: Loading error } ``` ### Example #2 Using for early error return ```php complete(performConnection($host)); } catch (\Throwable $e) { $state->error($e); } }); return new Future($state); } $future = connectToService(''); $future ->catch(function(\Throwable $e) { echo "Error: " . $e->getMessage() . "\n"; }) ->ignore(); ``` ## See also * [Future::completed](/en/docs/reference/future/completed.html) — Create a Future with a result * [Future::catch](/en/docs/reference/future/catch.html) — Handle a Future error * [Future::await](/en/docs/reference/future/await.html) — Await the result --- --- url: https://true-async.github.io/en/docs/reference/future/finally.md description: Callback that always executes on Future completion. --- # Future::finally (PHP 8.6+, True Async 1.0) ```php public function finally(callable $finally): Future ``` Registers a callback that executes when the `Future` completes regardless of the outcome --- success, error, or cancellation. The Future resolves with the same value or error as the original. Useful for releasing resources. ## Parameters `finally` — the function to execute on completion. Takes no arguments. Signature: `function(): void`. ## Return value `Future` — a new Future that will complete with the same value or error as the original. ## Examples ### Example #1 Releasing resources ```php complete($connection->query("SELECT * FROM users")); }); $future = $source ->finally(function() use ($connection) { $connection->close(); echo "Connection closed\n"; }); $users = $future->await(); ``` ### Example #2 Chaining with map, catch, and finally ```php complete(fetchDataFromApi()); }); $future = $source ->map(fn($data) => processData($data)) ->catch(function(\Throwable $e) { error_log("Error: " . $e->getMessage()); return []; }) ->finally(function() { echo "Operation completed\n"; }); $result = $future->await(); ``` ## See also * [Future::map](/en/docs/reference/future/map.html) — Transform the Future result * [Future::catch](/en/docs/reference/future/catch.html) — Handle a Future error * [Future::ignore](/en/docs/reference/future/ignore.html) — Ignore unhandled errors --- --- url: https://true-async.github.io/en/docs/reference/future/get-awaiting-info.md description: Debug information about awaiting coroutines. --- # Future::getAwaitingInfo (PHP 8.6+, True Async 1.0) ```php public function getAwaitingInfo(): array ``` Returns debug information about coroutines that are currently awaiting the completion of this `Future`. Useful for diagnosing deadlocks and analyzing dependencies between coroutines. ## Return value `array` — an array with information about awaiting coroutines. ## Examples ### Example #1 Getting information about waiters ```php await(); }); \Async\spawn(function() use ($future) { $future->await(); }); // Give coroutines time to start waiting \Async\delay(10); $info = $future->getAwaitingInfo(); var_dump($info); // Array with information about awaiting coroutines $state->complete("done"); ``` ## See also * [Future::getCreatedFileAndLine](/en/docs/reference/future/get-created-file-and-line.html) — Future creation location * [Future::getCreatedLocation](/en/docs/reference/future/get-created-location.html) — Creation location as a string * [Future::await](/en/docs/reference/future/await.html) — Await the result --- --- url: >- https://true-async.github.io/en/docs/reference/future/get-completed-file-and-line.md description: Future completion location as an array. --- # Future::getCompletedFileAndLine (PHP 8.6+, True Async 1.0) ```php public function getCompletedFileAndLine(): array ``` Returns information about the location where the `Future` was completed (where `complete()` or `fail()` was called on the associated `FutureState`). Contains the file name and line number. Useful for debugging and tracing async chains. ## Return value `array` — an array with keys `file` (string, file path) and `line` (integer, line number). If the Future has not yet completed, returns an empty array. ## Examples ### Example #1 Getting the completion location ```php complete(42); // line 8 $location = $future->getCompletedFileAndLine(); echo "File: " . $location['file'] . "\n"; echo "Line: " . $location['line'] . "\n"; // File: /app/script.php // Line: 8 ``` ### Example #2 Comparing creation and completion locations ```php complete("result"); }); $future->await(); echo "Created at: " . $future->getCreatedLocation() . "\n"; $completed = $future->getCompletedFileAndLine(); echo "Completed at: " . $completed['file'] . ":" . $completed['line'] . "\n"; ``` ## See also * [Future::getCompletedLocation](/en/docs/reference/future/get-completed-location.html) — Completion location as a string * [Future::getCreatedFileAndLine](/en/docs/reference/future/get-created-file-and-line.html) — Future creation location * [Future::getAwaitingInfo](/en/docs/reference/future/get-awaiting-info.html) — Information about waiters --- --- url: >- https://true-async.github.io/en/docs/reference/future/get-completed-location.md description: Future completion location as a string. --- # Future::getCompletedLocation (PHP 8.6+, True Async 1.0) ```php public function getCompletedLocation(): string ``` Returns information about the `Future` completion location as a formatted string. Convenient for logging and debugging. ## Return value `string` — a string in the format `file:line`, for example `/app/worker.php:15`. If the Future has not yet completed, returns an empty string. ## Examples ### Example #1 Getting the completion location as a string ```php complete("result"); echo $future->getCompletedLocation(); // /app/script.php:9 ``` ### Example #2 Full Future lifecycle tracing ```php complete("done"); }); $result = $future->await(); echo "Future lifecycle:\n"; echo " Created at: " . $future->getCreatedLocation() . "\n"; echo " Completed at: " . $future->getCompletedLocation() . "\n"; echo " Result: " . $result . "\n"; ``` ## See also * [Future::getCompletedFileAndLine](/en/docs/reference/future/get-completed-file-and-line.html) — Completion location as an array * [Future::getCreatedLocation](/en/docs/reference/future/get-created-location.html) — Creation location as a string * [Future::getAwaitingInfo](/en/docs/reference/future/get-awaiting-info.html) — Information about waiters --- --- url: >- https://true-async.github.io/en/docs/reference/future/get-created-file-and-line.md description: Future creation location as an array. --- # Future::getCreatedFileAndLine (PHP 8.6+, True Async 1.0) ```php public function getCreatedFileAndLine(): array ``` Returns information about the `Future` creation location as an array. Contains the file name and line number where this Future was created. Useful for debugging and tracing. ## Return value `array` — an array with keys `file` (string, file path) and `line` (integer, line number). ## Examples ### Example #1 Getting the creation location ```php getCreatedFileAndLine(); echo "File: " . $location['file'] . "\n"; echo "Line: " . $location['line'] . "\n"; // File: /app/script.php // Line: 5 ``` ### Example #2 Logging Future information ```php getCreatedFileAndLine(); error_log(sprintf( "Future created at %s:%d", $info['file'], $info['line'] )); return $future; } ``` ## See also * [Future::getCreatedLocation](/en/docs/reference/future/get-created-location.html) — Creation location as a string * [Future::getCompletedFileAndLine](/en/docs/reference/future/get-completed-file-and-line.html) — Future completion location * [Future::getAwaitingInfo](/en/docs/reference/future/get-awaiting-info.html) — Information about waiters --- --- url: https://true-async.github.io/en/docs/reference/future/get-created-location.md description: Future creation location as a string. --- # Future::getCreatedLocation (PHP 8.6+, True Async 1.0) ```php public function getCreatedLocation(): string ``` Returns information about the `Future` creation location as a formatted string. Convenient for logging and debug output. ## Return value `string` — a string in the format `file:line`, for example `/app/script.php:42`. ## Examples ### Example #1 Getting the creation location as a string ```php getCreatedLocation(); // /app/script.php:5 ``` ### Example #2 Using in debug messages ```php isCompleted()) { echo "Warning: Future created at " . $future->getCreatedLocation() . " has not completed in over 5 seconds\n"; } }); ``` ## See also * [Future::getCreatedFileAndLine](/en/docs/reference/future/get-created-file-and-line.html) — Creation location as an array * [Future::getCompletedLocation](/en/docs/reference/future/get-completed-location.html) — Completion location as a string * [Future::getAwaitingInfo](/en/docs/reference/future/get-awaiting-info.html) — Information about waiters --- --- url: https://true-async.github.io/en/docs/reference/future/ignore.md description: Do not propagate unhandled errors to the event loop handler. --- # Future::ignore (PHP 8.6+, True Async 1.0) ```php public function ignore(): Future ``` Marks the `Future` as ignored. If the Future completes with an error and the error is not handled, it will not be passed to the event loop's unhandled exception handler. Useful for "fire-and-forget" tasks where the result does not matter. ## Return value `Future` — returns the same Future for method chaining. ## Examples ### Example #1 Ignoring Future errors ```php ignore(); \Async\spawn(function() use ($state) { // This operation may fail try { sendAnalytics(['event' => 'page_view']); $state->complete(null); } catch (\Throwable $e) { $state->error($e); } }); // The error will not be passed to the event loop handler ``` ### Example #2 Using ignore with method chaining ```php ignore(); // Cache errors are not critical \Async\spawn(function() use ($state, $key) { try { $data = loadFromDatabase($key); saveToCache($key, $data); $state->complete(null); } catch (\Throwable $e) { $state->error($e); } }); } } warmupCache(['user:1', 'user:2', 'user:3']); ``` ## See also * [Future::catch](/en/docs/reference/future/catch.html) — Handle a Future error * [Future::finally](/en/docs/reference/future/finally.html) — Callback on Future completion --- --- url: https://true-async.github.io/en/docs/reference/future/is-cancelled.md description: Checks whether the Future is cancelled. --- # Future::isCancelled (PHP 8.6+, True Async 1.0) ```php public function isCancelled(): bool ``` Checks whether the `Future` has been cancelled. A Future is considered cancelled after the `cancel()` method has been called. ## Return value `bool` — `true` if the Future has been cancelled, `false` otherwise. ## Examples ### Example #1 Checking Future cancellation ```php isCancelled()); // bool(false) $future->cancel(); var_dump($future->isCancelled()); // bool(true) var_dump($future->isCompleted()); // bool(true) ``` ### Example #2 Difference between completion and cancellation ```php isCancelled()); // bool(false) var_dump($completed->isCompleted()); // bool(true) $failed = Future::failed(new \RuntimeException("error")); var_dump($failed->isCancelled()); // bool(false) var_dump($failed->isCompleted()); // bool(true) ``` ## See also * [Future::cancel](/en/docs/reference/future/cancel.html) — Cancel the Future * [Future::isCompleted](/en/docs/reference/future/is-completed.html) — Check if the Future is completed --- --- url: https://true-async.github.io/en/docs/reference/future/is-completed.md description: Checks whether the Future is completed. --- # Future::isCompleted (PHP 8.6+, True Async 1.0) ```php public function isCompleted(): bool ``` Checks whether the `Future` is completed. A Future is considered completed if it contains a result, an error, or has been cancelled. ## Return value `bool` — `true` if the Future is completed (successfully, with an error, or cancelled), `false` otherwise. ## Examples ### Example #1 Checking Future completion ```php isCompleted()); // bool(false) $state->complete(42); var_dump($future->isCompleted()); // bool(true) ``` ### Example #2 Checking static factory methods ```php isCompleted()); // bool(true) $failed = Future::failed(new \RuntimeException("error")); var_dump($failed->isCompleted()); // bool(true) ``` ## See also * [Future::isCancelled](/en/docs/reference/future/is-cancelled.html) — Check if the Future is cancelled * [Future::await](/en/docs/reference/future/await.html) — Await the Future result --- --- url: https://true-async.github.io/en/docs/reference/future/map.md description: Transform the Future result. --- # Future::map (PHP 8.6+, True Async 1.0) ```php public function map(callable $map): Future ``` Transforms the `Future` result using a callback function. The callback receives the value of the completed Future and returns a new value. Analogous to `then()` in Promise-based APIs. If the original Future completed with an error, the callback is not invoked, and the error is passed through to the new Future. ## Parameters `map` — the transformation function. Receives the Future result, returns a new value. Signature: `function(mixed $value): mixed`. ## Return value `Future` — a new Future containing the transformed result. ## Examples ### Example #1 Transforming the result ```php map(fn(int $x) => $x * 2) ->map(fn(int $x) => "Result: $x"); echo $future->await(); // Result: 10 ``` ### Example #2 Chain of transformations for async loading ```php complete(file_get_contents('https://api.example.com/data')); }); $future = $source ->map(fn(string $json) => json_decode($json, true)) ->map(fn(array $data) => $data['users']) ->map(fn(array $users) => count($users)); $count = $future->await(); echo "Number of users: $count\n"; ``` ## See also * [Future::catch](/en/docs/reference/future/catch.html) — Handle a Future error * [Future::finally](/en/docs/reference/future/finally.html) — Callback on Future completion * [Future::await](/en/docs/reference/future/await.html) — Await the result --- --- url: https://true-async.github.io/en/architecture/async-gc.md description: >- How PHP GC works with coroutines, scope, and contexts -- get_gc handlers, zombie coroutines, circular references. --- # Garbage Collection in Asynchronous Context In `PHP`, the garbage collector normally works synchronously. When the possible roots buffer is full, `gc_collect_cycles()` is called in the current context. The `GC` computes circular references and calls object destructors in a loop for objects marked for deletion. In a concurrent environment, this model breaks down. An object's destructor may call `await` -- for example, to properly close a database connection. If `GC` is running inside a coroutine, `await` will suspend that coroutine, leaving the `GC` in an incomplete state. Other coroutines will see partially collected objects. For this reason, `TrueAsync` had to modify the garbage collection logic. ## GC Coroutine When the `gc_possible_root` buffer fills up and the threshold is triggered, `zend_gc_collect_cycles()` launches itself in a separate coroutine. ```c ZEND_API int zend_gc_collect_cycles(void) { if (UNEXPECTED(ZEND_ASYNC_IS_ACTIVE && ZEND_ASYNC_CURRENT_COROUTINE != GC_G(gc_coroutine))) { if (GC_G(gc_coroutine)) { return 0; // GC is already running in another coroutine } start_gc_in_coroutine(); return 0; } // ... actual garbage collection } ``` The coroutine that triggered `GC` is not blocked and continues its work, while garbage collection happens on the next `Scheduler` tick. The `GC` coroutine gets its own top-level `Scope` (`parent = NULL`). This isolates garbage collection from user code: canceling a user `Scope` will not affect the `GC`. ## Destructors in Coroutines The main problem arises specifically when calling destructors, because destructors can unexpectedly suspend a coroutine. Therefore, the `GC` uses a concurrent iterator algorithm based on microtasks. To launch the iteration, `GC` creates yet another iterator coroutine. This is done to create the illusion of sequential execution, which simplifies the `GC` considerably. ```c static bool gc_call_destructors_in_coroutine(void) { GC_G(dtor_idx) = GC_FIRST_ROOT; GC_G(dtor_end) = GC_G(first_unused); // Create child coroutine for destructors zend_coroutine_t *coroutine = gc_spawn_destructors_coroutine(); // GC coroutine suspends on dtor_scope zend_async_resume_when(GC_G(gc_coroutine), &scope->event, ...); ZEND_ASYNC_SUSPEND(); // GC sleeps while destructors run return true; } ``` The destructor uses the Scope mechanism not only to control the lifetime of coroutines, but also to await their completion. For this purpose, another child `Scope` is created to encapsulate all destructor coroutines: ``` gc_scope <- top-level `GC` \-- GC coroutine <- marking + coordination \-- dtor_scope <- child scope \-- dtor-coroutine[0] <- calling destructors (HI_PRIORITY) ``` The `GC` coroutine subscribes to the completion event of `dtor_scope`. It will wake up only when **all** destructors in `dtor_scope` have completed. ![Garbage Collection in a Separate Coroutine](/diagrams/en/architecture-async-gc/gc-coroutine.svg) ## What If a Destructor Calls await? Here the classic concurrent iterator algorithm based on microtasks is used: * A microtask is registered that will execute if a context switch occurs * If a switch happens, the microtask creates yet another coroutine for iteration The iterator checks whether it is still in the same coroutine: ```c static zend_result gc_call_destructors(uint32_t idx, uint32_t end, ...) { zend_coroutine_t *coroutine = ZEND_ASYNC_CURRENT_COROUTINE; while (idx != end) { obj->handlers->dtor_obj(obj); // call destructor // If the coroutine changed -- the destructor called await if (coroutine != NULL && coroutine != *current_coroutine_ptr) { return FAILURE; // abort traversal } idx++; } return SUCCESS; } ``` If `ZEND_ASYNC_CURRENT_COROUTINE` has changed, it means the destructor called `await` and the current coroutine went to sleep. In this case, the iterator simply exits, and the next iteration step will be launched in a new coroutine. --- --- url: https://true-async.github.io/en/docs/reference/get-coroutines.md description: get_coroutines() — get a list of all active coroutines for diagnostics. --- # get\_coroutines (PHP 8.6+, True Async 1.0) `get_coroutines()` — Returns an array of all active coroutines. Useful for diagnostics and monitoring. ## Description ```php get_coroutines(): array ``` ## Return Values An array of `Async\Coroutine` objects — all coroutines registered in the current request. ## Examples ### Example #1 Monitoring coroutines ```php getId(), $coro->isSuspended() ? 'suspended' : 'running', $coro->getSpawnLocation() ); } ?> ``` ### Example #2 Detecting leaks ```php 0) { foreach ($active as $coro) { error_log("Unfinished coroutine: " . $coro->getSpawnLocation()); } } ?> ``` ## See Also * [current\_coroutine()](/en/docs/reference/current-coroutine.html) — current coroutine * [Coroutines](/en/docs/components/coroutines.html) — the coroutine concept --- --- url: https://true-async.github.io/en/docs/reference/graceful-shutdown.md description: >- graceful_shutdown() — graceful scheduler shutdown with cancellation of all coroutines. --- # graceful\_shutdown (PHP 8.6+, True Async 1.0) `graceful_shutdown()` — Initiates a graceful scheduler shutdown. All coroutines receive a cancellation request. ## Description ```php graceful_shutdown(?Async\AsyncCancellation $cancellationError = null): void ``` Starts the graceful shutdown procedure: all active coroutines are cancelled, and the application continues running until they complete naturally. ## Parameters **`cancellationError`** An optional cancellation error to pass to the coroutines. If not specified, a default message is used. ## Return Values No return value. ## Examples ### Example #1 Handling a termination signal ```php ``` ## Notes > **Note:** Coroutines created **after** calling `graceful_shutdown()` will be immediately cancelled. > **Note:** `exit` and `die` automatically trigger a graceful shutdown. ## See Also * [Cancellation](/en/docs/components/cancellation.html) — cancellation mechanism * [Scope](/en/docs/components/scope.html) — lifecycle management --- --- url: https://true-async.github.io/en/tutors-server/10-grpc.md description: >- addGrpcHandler(): unary and streaming, readMessage/writeMessage, trailers, and deadlines. --- # gRPC So far only browsers have talked to our server. But `ProfileService` has other interlocutors too: `UserDirectory`, `GeoDirectory`, billing. Services have long talked among themselves not over REST but over gRPC: strict contracts, streaming in both directions, deadlines and error codes out of the box. Usually a separate server is spun up for gRPC on a separate port. Ask yourself: why, actually? gRPC is a protocol over HTTP/2 and HTTP/3. Over what's already listening for us. The server just needs to tell such requests apart by the content-type `application/grpc` and hand them to a separate handler. And that's exactly what it does. ## Contract First A gRPC conversation begins not with code. It begins with a contract, and that's perhaps the main cultural difference from REST. Let's describe the profile service in `profile.proto`: ```protobuf syntax = "proto3"; package profile; service ProfileService { rpc GetProfile (GetProfileRequest) returns (Profile); } message GetProfileRequest { int64 user_id = 1; } message Profile { int64 id = 1; string name = 2; string region = 3; } ``` The protobuf compiler generates PHP classes from this, and the runtime is installed with Composer: ```bash $ composer require google/protobuf $ protoc --php_out=src/Generated profile.proto ``` Now the project has `Profile\GetProfileRequest` and `Profile\Profile`: typed getters, setters, serialization. And protoc will generate exactly the same classes for a client in Go, Java, or Python. That's the whole point: one contract, any languages. ## Messages Instead of Bodies How does a gRPC handler differ from an HTTP handler? In the unit of communication. There we had a "request body" and a "response body," one of each. Here it's a stream of messages in each direction: `readMessage()` returns the bytes of the next incoming one or `null` at the end, and `writeMessage()` sends an outgoing one. The server is responsible for gRPC framing, lengths, and flags. The generated classes are responsible for the contents: ```php use Profile\GetProfileRequest; use Profile\Profile; $server->addGrpcHandler(function (HttpRequest $req, HttpResponse $res) { // the request path names the contract method if ($req->getPath() !== '/profile.ProfileService/GetProfile') { $res->setTrailer('grpc-status', '12'); // UNIMPLEMENTED return; } $getProfile = new GetProfileRequest(); $getProfile->mergeFromString($req->readMessage()); // bytes -> object $profile = (new Profile()) ->setId($getProfile->getUserId()) ->setName(fetchName($getProfile->getUserId())) ->setRegion(fetchRegion($getProfile->getUserId())); $res->writeMessage($profile->serializeToString()); // object -> bytes }); ``` Routing here is just the path: package, service, method. A client in any language, calling `GetProfile`, sends a POST to `/profile.ProfileService/GetProfile`. And the handler itself lives next to the REST routes and sees the same warmed-up pool and request context. All four forms of gRPC come out of this same API and differ only in the number of calls: ```php // Unary: one message there, one back $getProfile->mergeFromString($req->readMessage()); $res->writeMessage($reply->serializeToString()); // Server streaming: one there, many back $getHistory->mergeFromString($req->readMessage()); foreach (loadHistory($getHistory->getUserId()) as $event) { $res->writeMessage($event->serializeToString()); } // Full duplex: read and reply interleaved while (($bytes = $req->readMessage()) !== null) { $msg = new ChatMessage(); $msg->mergeFromString($bytes); $res->writeMessage(process($msg)->serializeToString()); } ``` `readMessage()` puts the coroutine to sleep until the next message, and `writeMessage()` replies right away, without waiting for the end of the incoming stream. The same duplex as WebSocket, only with a contract and by the standard. ## Passing Errors gRPC has its own system of error codes, and it lives in a place that's confusing at first. The response's HTTP status is always 200. Always. The real result travels in the trailers, headers that HTTP/2 sends after the body. We glimpsed them in the streaming chapter, and here's their main consumer: ```php $server->addGrpcHandler(function (HttpRequest $req, HttpResponse $res) { $data = $req->readMessage(); if (!authorized($req)) { $res->setTrailer('grpc-status', '7'); // PERMISSION_DENIED $res->setTrailer('grpc-message', 'access denied'); return; } $res->writeMessage(process($data)); // a clean return: the server itself appends grpc-status: 0 (OK) }); ``` Crashes are handled too: an exception that flies out of the handler turns into `grpc-status: 13` (INTERNAL), rather than a dropped connection. ## Deadlines Remember how many chapters of the first series we spent on the idea that "any wait must have a limit"? Well, in gRPC that idea is elevated to a protocol standard. The client passes its deadline right in the request, via the `grpc-timeout` header, and the server hands it to the handler: ```php $deadline = $req->getGrpcTimeout(); // milliseconds or null $result = $group->all()->await(timeout($deadline ?? 5000)); ``` Think about how right this mechanic is. The client has two hundred milliseconds of patience left? Then there's no point going to `GeoDirectory` with a two-second timeout. The deadline is threaded through all the internal waits, through all the services in the chain, and the whole system respects the patience of the very first caller. ## The End of the Second Series That's the whole route. Let's glance back once. The fifteen chapters of the first series were building a vocabulary: coroutines, cancellation, `Future`, channels, scope, groups, pools, threads, context. Honestly, in places it might have felt like the vocabulary was excessive. And then the second series came, and it turned out the server is just a sentence composed of those same words. Every request is a coroutine. The pool protects the database. Scope cleans up after the request. Backpressure holds off overload. Threads occupy the cores. And on top, static, SSE, WebSocket, and gRPC on one port. Note too what wasn't in either series: callbacks, `.then()` chains, the keywords `async` and `await` on every other line, manual event loop management. All the code is ordinary sequential PHP. It just stopped waiting for nothing. From here you're on your own. Take the service that's long been asking for a rework, and start with a single handler. The class reference is in the [server documentation](/en/docs/server/index.html), and the internals are in the [architecture](/en/architecture/server.html). And if something behaves differently from what these chapters promised, you now know enough to file a good bug report. --- --- url: https://true-async.github.io/en/interactive/coroutine-demo.md description: Interactive visualization of how coroutines work in TrueAsync --- --- --- url: https://true-async.github.io/en/docs/server/compression.md description: >- gzip, Brotli, and zstd in TrueAsync Server: Accept-Encoding negotiation, MIME filter, limits, BREACH mitigation, inbound body decoding. --- # HTTP compression (PHP 8.6+, true\_async\_server 0.6+) TrueAsync Server supports three codecs — **gzip**, **Brotli (br)**, and **zstd** — uniformly across all protocols: HTTP/1.1, HTTP/2, and HTTP/3. ## Backends * **gzip** — `zlib-ng` (preferred, roughly 2–4× faster at the same compression level) or system `zlib` as a fallback. The same code, switched through a `zng_*` ↔ `*` macro layer. * **Brotli** — `libbrotli`. Active only when `--enable-brotli` finds the library. * **zstd** — `libzstd`. Active only when `--enable-zstd` finds the library. The compiled-in set can be inspected at runtime: ```php TrueAsync\HttpServerConfig::getSupportedEncodings(); // → ["zstd", "br", "gzip", "identity"] ``` The list always contains `"identity"`; `"gzip"` shows up on successful `--enable-http-compression`; `"br"`/`"zstd"` show up when the corresponding library is present at configure time. ## Server-side preference The server preference order is **`zstd > gzip > brotli > identity`**. > **Why is gzip ahead of brotli?** The Brotli encoder cannot reuse its state > (`libbrotli` has no public reset API). Until an arena allocator lands (TODO Step 4), gzip's > `deflateReset` gives the better default. Clients that explicitly prefer brotli via q-values > (`br;q=1.0, gzip;q=0.5`) still get brotli. ## Negotiation (RFC 9110 §12.5.3) The server parses the client `Accept-Encoding`: q-values, `identity;q=0`, `*;q=0`. If the header is **absent**, the response is sent uncompressed (identity only). That matches nginx behaviour and is safer than a strict reading of the RFC. Compression is **skipped** when: * the status is `1xx`, `204`, or `304` * the method is `HEAD` * the response uses `Range` * the handler has already set `Content-Encoding` * the MIME is outside the whitelist * the body is smaller than the threshold ## Configuration ```php use TrueAsync\HttpServerConfig; $config ->setCompressionEnabled(true) // master switch (default: true) ->setCompressionLevel(6) // gzip 1..9, default 6 ->setBrotliLevel(4) // 0..11, default 4 ->setZstdLevel(3) // 1..22, default 3 ->setCompressionMinSize(1024) // do not compress bodies < 1 KiB ->setCompressionMimeTypes([ 'application/javascript', 'application/json', 'application/xml', 'image/svg+xml', 'text/css', 'text/html', 'text/javascript', 'text/plain', 'text/xml', ]) ->setRequestMaxDecompressedSize(10 * 1024 * 1024); // anti-zip-bomb cap ``` ### Compression levels | Codec | Range | Default | Notes | |-------|------:|--------:|-------| | gzip | 1..9 | 6 | classic zlib semantics | | brotli | 0..11 | 4 | quality 11 ≈ 50× slower than quality 4 with little real gain | | zstd | 1..22 | 3 | the zstd team's own default: better ratio than gzip-6 and faster | ### MIME whitelist `setCompressionMimeTypes()` **fully replaces** the list (nginx `gzip_types` semantics). Entries are normalised at setter time: parameters (`; charset=...`) are trimmed off, whitespace is stripped, everything is lowercased. The runtime comparison stays exact and zero-allocation. ### Anti-zip-bomb `setRequestMaxDecompressedSize($bytes)` caps the **decompressed** size of inbound bodies. The default is 10 MiB. Anything larger returns 413. `0` disables the cap, but it must be set explicitly — there is no implicit-unlimited path. ## Per-response opt-out `HttpResponse::setNoCompression()` overrides everything (Accept-Encoding, MIME, size). Use it for: * endpoints where secrets sit next to reflected user input (**BREACH mitigation**) * payloads where `Content-Encoding` is already set (the handler wrapped the body itself) * any response the server should not re-wrap ```php $server->addHttpHandler(function ($req, $res) { if ($req->getPath() === '/account') { // contains a CSRF token + reflected search query — BREACH-sensitive $res->setNoCompression(); } $res->json($payload); }); ``` The method is idempotent. ## Streaming When the handler calls `HttpResponse::send($chunk)`, the compressing wrapper transparently kicks in on the first call (if negotiation allowed it) and emits **one downstream chunk per source chunk**, preserving framing efficiency for chunked H1 and H2 DATA frames. ## Inbound decoding `Content-Encoding: gzip` / `br` / `zstd` (and the legacy `x-gzip`) on requests is decoded transparently. `identity` is a no-op. An unknown coding → 413/415 (see below). | Situation | Code | |-----------|-----:| | Unknown coding | 415 | | Anti-bomb cap exceeded | 413 | | Corrupt inflate | 400 | Inside the handler, the already-decoded body is visible through [`HttpRequest::getBody()`](/en/docs/reference/server/http-request.html#getbody). ## One-shot brotli Since 0.6.3, the server uses `BrotliEncoderCompress()` for bodies of known size (size-hint `BROTLI_PARAM_SIZE_HINT`): the encoder picks the correct ring-buffer and hash-table sizes up front, instead of falling back to the streaming mode that targets unknown lengths. The streaming path stays in use for chunked / unknown-length responses. ## Benchmarks The C-side defaults are tuned for production (gzip 6, brotli 4). The author's bench harnesses use `setCompressionLevel(1)` / `setBrotliLevel(1)` to match Swoole's `BrotliEncoderCompress` path. ## See also * [`HttpServerConfig::setCompressionEnabled()`](/en/docs/reference/server/http-server-config.html#setcompressionenabled) * [`HttpResponse::setNoCompression()`](/en/docs/reference/server/http-response.html#setnocompression) * [Static files](/en/docs/server/static-files.html): precompressed sidecars (`.br`, `.gz`, `.zst`) --- --- url: https://true-async.github.io/en/docs/reference/ini-settings.md description: php.ini configuration directives for the TrueAsync extension. --- # INI Settings The TrueAsync extension adds the following directives to `php.ini`. ## Directives List | Directive | Default Value | Scope | Description | |-----------|--------------|-------|-------------| | `async.debug_deadlock` | `1` | `PHP_INI_ALL` | Enables diagnostic report output on deadlock detection | ## async.debug\_deadlock **Type:** `bool` **Default:** `1` (enabled) **Scope:** `PHP_INI_ALL` — can be changed in `php.ini`, `.htaccess`, `.user.ini`, and via `ini_set()`. When enabled, this directive activates detailed diagnostic output when the scheduler detects a deadlock. If the scheduler finds that all coroutines are blocked and there are no active events, it prints a report before throwing `Async\DeadlockError`. ### Report Contents * Number of waiting coroutines and active events * List of all blocked coroutines showing: * Spawn and suspend locations * Events each coroutine is waiting for, with human-readable descriptions ### Example Output ``` === DEADLOCK REPORT START === Coroutines waiting: 2, active_events: 0 Coroutine 1 spawn: /app/server.php:15 suspend: /app/server.php:22 waiting for: - Channel recv (capacity: 0, senders: 0, receivers: 1) Coroutine 2 spawn: /app/server.php:28 suspend: /app/server.php:35 waiting for: - Channel recv (capacity: 0, senders: 0, receivers: 1) === DEADLOCK REPORT END === Fatal error: Uncaught Async\DeadlockError: ... ``` ### Examples #### Disabling via php.ini ```ini async.debug_deadlock = 0 ``` #### Disabling via ini\_set() ```php ``` #### Disabling for tests ```ini ; phpunit.xml or .phpt file async.debug_deadlock=0 ``` ## See Also * [Exceptions](/en/docs/components/exceptions.html) — `Async\DeadlockError` --- --- url: https://true-async.github.io/en/docs/evidence/concurrency-efficiency.md description: >- Concurrency efficiency analysis for IO-bound and CPU-bound tasks. Little's Law, Goetz's formula, calculating the optimal number of coroutines. --- # IO-Bound vs CPU-bound How much concurrency or parallelism provides a performance gain depends on the nature of the workload. In server applications, two main types of tasks are typically distinguished. * **IO-bound** — tasks where a significant portion of time is spent waiting for input/output operations: network requests, database queries, reading and writing files. During these moments, the CPU sits idle. * **CPU-bound** — tasks requiring intensive computation that keep the processor busy almost constantly: complex algorithms, data processing, cryptography. In recent years, most web applications have been shifting toward **IO-bound** workloads. This is driven by the growth of microservices, remote `API`s, and cloud services. Approaches like Frontend for Backend (`BFF`) and `API Gateway`, which aggregate data from multiple sources, amplify this effect. A modern server application is also hard to imagine without logging, telemetry, and real-time monitoring. All these operations are inherently IO-bound. ## Efficiency of IO-bound Tasks The efficiency of concurrent execution of `IO-bound` tasks is determined by what fraction of time the task actually uses the `CPU` versus how much it spends waiting for I/O operations to complete. ### Little's Law In queueing theory, one of the fundamental formulas is Little's Law ([Little's Law](https://en.wikipedia.org/wiki/Little%27s_law)): $$ L = \lambda \cdot W $$ Where: * `L` — the average number of tasks in the system * `λ` — the average rate of incoming requests * `W` — the average time a task spends in the system This law is universal and does not depend on the specific system implementation: it doesn't matter whether threads, coroutines, or asynchronous callbacks are used. It describes the fundamental relationship between load, latency, and the level of concurrency. When estimating concurrency for a server application, you are essentially solving the problem of how many tasks must be in the system simultaneously for resources to be used efficiently. For `IO-bound` workloads, the average request processing time is large compared to the time spent on active computation. Therefore, to keep the CPU from idling, there must be a sufficient number of concurrent tasks in the system. This is exactly the quantity that formal analysis allows us to estimate, by relating: * wait time, * throughput, * and the required level of concurrency. A similar approach is used in industry for calculating the optimal thread pool size (see Brian Goetz, *"Java Concurrency in Practice"*). > The actual statistical data for each element of these formulas > (number of SQL queries per HTTP request, DB latencies, PHP framework throughput) > is collected in a separate document: > [Statistical Data for Concurrency Calculation](/en/docs/evidence/real-world-statistics.html). ### Basic CPU Utilization To calculate what fraction of time the processor is actually doing useful work when executing a single task, the following formula can be used: $$ U = \frac{T\_{cpu}}{T\_{cpu} + T\_{io}} $$ * `T_cpu` — the time spent performing computations on the CPU * `T_io` — the time spent waiting for I/O operations The sum `T_cpu + T_io` represents the total lifetime of a task from start to completion. The value `U` ranges from 0 to 1 and indicates the degree of processor utilization: * `U → 1` characterizes a computationally heavy (`CPU-bound`) task * `U → 0` characterizes a task that spends most of its time waiting for I/O (`IO-bound`) Thus, the formula provides a quantitative assessment of how efficiently the `CPU` is being used and whether the workload in question is `IO-bound` or `CPU-bound`. ### Impact of Concurrency When executing multiple `IO-bound` tasks concurrently, the `CPU` can use the `I/O` wait time of one task to perform computations for **another**. CPU utilization with `N` concurrent tasks can be estimated as: $$ U\_N = \min\left(1,; N \cdot \frac{T\_{cpu}}{T\_{cpu} + T\_{io}}\right) $$ Increasing concurrency improves `CPU` utilization, but only up to a certain limit. ### Efficiency Limit The maximum gain from concurrency is bounded by the ratio of `I/O` wait time to computation time: $$ E(N) \approx \min\left(N,; 1 + \frac{T\_{io}}{T\_{cpu}}\right) $$ In practice, this means that the number of truly useful concurrent tasks is approximately equal to the ratio `T_io / T_cpu`. ### Optimal Concurrency $$ N\_{opt} \approx 1 + \frac{T\_{io}}{T\_{cpu}} $$ The one in the formula accounts for the task currently executing on the `CPU`. With a large `T_io / T_cpu` ratio (which is typical for `IO-bound` workloads), the contribution of one is negligible, and the formula is often simplified to `T_io / T_cpu`. This formula is a special case (for a single core) of the classic optimal thread pool size formula proposed by Brian Goetz in the book *"Java Concurrency in Practice"* (2006): $$ N\_{threads} = N\_{cores} \times \left(1 + \frac{T\_{wait}}{T\_{service}}\right) $$ The ratio `T_wait / T_service` is known as the **blocking coefficient**. The higher this coefficient, the more concurrent tasks can be effectively utilized by a single core. At this level of concurrency, the processor spends most of its time doing useful work, and further increasing the number of tasks no longer yields a noticeable gain. This is precisely why asynchronous execution models are most effective for `IO-bound` web workloads. ## Example Calculation for a Typical Web Application Let's consider a simplified but fairly realistic model of an average server-side web application. Assume that processing a single `HTTP` request primarily involves interacting with a database and does not contain computationally complex operations. ### Initial Assumptions * Approximately **20 SQL queries** are executed per HTTP request * Computation is limited to data mapping, response serialization, and logging * The database is outside the application process (remote I/O) > **Why 20 queries?** > This is the median estimate for ORM applications of moderate complexity. > For comparison: > > * WordPress generates ~17 queries per page, > * Drupal without caching — from 80 to 100, > * and a typical Laravel/Symfony application — from 10 to 30. > > The main source of growth is the N+1 pattern, where the ORM loads related entities > with separate queries. ### Execution Time Estimate For the estimate, we'll use averaged values: * One SQL query: * I/O wait time: `T_io ≈ 4 ms` * CPU computation time: `T_cpu ≈ 0.05 ms` Total per HTTP request: * `T_io = 20 × 4 ms = 80 ms` * `T_cpu = 20 × 0.05 ms = 1 ms` > **About the chosen latency values.** > The I/O time for a single `SQL` query consists of the network latency (`round-trip`) > and the query execution time on the DB server. > Network round-trip within a single data center is ~0.5 ms, > and for cloud environments (cross-AZ, managed RDS) — 1–5 ms. > Accounting for the execution time of a moderately complex query, > the resulting 4 ms per query is a realistic estimate for a cloud environment. > The CPU time (0.05 ms) covers ORM result mapping, entity hydration, > and basic processing logic. ### Workload Characteristics The ratio of wait time to computation time: $$ \frac{T\_{io}}{T\_{cpu}} = \frac{80}{1} = 80 $$ This means the task is predominantly **IO-bound**: the processor spends most of its time idle, waiting for I/O operations to complete. ### Estimating the Number of Coroutines The optimal number of concurrent coroutines per CPU core is approximately equal to the ratio of I/O wait time to computation time: $$ N\_{coroutines} \approx \frac{T\_{io}}{T\_{cpu}} \approx 80 $$ In other words, approximately **80 coroutines per core** allow virtually complete hiding of I/O latency while maintaining high CPU utilization. For comparison: [Zalando Engineering](https://engineering.zalando.com/posts/2019/04/how-to-set-an-ideal-thread-pool-size.html) provides an example with a microservice where response time is 50 ms and processing time is 5 ms on a dual-core machine: `2 × (1 + 50/5) = 22 threads` — the same principle, the same formula. ### Scaling by Number of Cores For a server with `C` cores: $$ N\_{total} \approx C \cdot \frac{T\_{io}}{T\_{cpu}} $$ For example, for an 8-core processor: $$ N\_{total} \approx 8 \times 80 = 640 \text{ coroutines} $$ This value reflects the **useful level of concurrency**, not a hard limit. ### Sensitivity to Environment The value of 80 coroutines per core is not a universal constant, but the result of specific assumptions about I/O latency. Depending on the network environment, the optimal number of concurrent tasks may differ significantly: | Environment | T\_io per SQL query | T\_io total (×20) | N per core | |---------------------------------|--------------------|-------------------|------------| | Localhost / Unix-socket | ~0.1 ms | 2 ms | ~2 | | LAN (single data center) | ~1 ms | 20 ms | ~20 | | Cloud (cross-AZ, RDS) | ~4 ms | 80 ms | ~80 | | Remote server / cross-region | ~10 ms | 200 ms | ~200 | The greater the latency, the more coroutines are needed to fully utilize the CPU with useful work. ### PHP-FPM vs Coroutines: Approximate Calculation To estimate the practical benefit of coroutines, let's compare two execution models on the same server with the same workload. #### Initial Data **Server:** 8 cores, cloud environment (cross-AZ RDS). **Workload:** typical Laravel API endpoint — authorization, Eloquent queries with eager loading, JSON serialization. Based on benchmark data from [Sevalla](https://sevalla.com/blog/laravel-benchmarks/) and [Kinsta](https://kinsta.com/blog/php-benchmarks/): | Parameter | Value | Source | |-------------------------------------------------|------------|-------------------| | Laravel API throughput (30 vCPU, localhost DB) | ~440 req/s | Sevalla, PHP 8.3 | | Number of PHP-FPM workers in benchmark | 15 | Sevalla | | Response time (W) in benchmark | ~34 ms | L/λ = 15/440 | | Memory per PHP-FPM worker | ~40 MB | Typical value | #### Step 1: Estimating T\_cpu and T\_io In the **Sevalla** benchmark, the database runs on localhost (latency <0.1 ms). With ~10 SQL queries per endpoint, total I/O is less than 1 ms. Given: * Throughput: λ ≈ 440 req/s * Number of concurrently served requests (PHP-FPM workers): L = 15 * Database on localhost, so T\_io ≈ 0 By Little's Law: $$ W = \frac{L}{\lambda} = \frac{15}{440} \approx 0.034 , \text{s} \approx 34 , \text{ms} $$ Since in this benchmark the database runs on `localhost` and total `I/O` is less than 1 ms, the resulting average response time almost entirely reflects the `CPU` processing time per request: $$ T\_{cpu} \approx W \approx 34 , \text{ms} $$ This means that under `localhost` conditions, nearly all the response time (~34 ms) is `CPU`: framework, `middleware`, `ORM`, serialization. Let's move the same endpoint to a **cloud environment** with 20 `SQL` queries: $$ T\_{cpu} = 34 \text{ ms (framework + logic)} $$ $$ T\_{io} = 20 \times 4 \text{ ms} = 80 \text{ ms (DB wait time)} $$ $$ W = T\_{cpu} + T\_{io} = 114 \text{ ms} $$ Blocking coefficient: $$ \frac{T\_{io}}{T\_{cpu}} = \frac{80}{34} \approx 2.4 $$ #### Step 2: PHP-FPM In the `PHP-FPM` model, each worker is a separate OS process. During `I/O` wait, the worker blocks and cannot process other requests. To fully utilize 8 cores, enough workers are needed so that at any given moment, 8 of them are performing `CPU` work: $$ N\_{workers} = 8 \times \left(1 + \frac{80}{34}\right) = 8 \times 3.4 = 27 $$ | Metric | Value | |-------------------------------------|---------------| | Workers | 27 | | Memory (27 × 40 MB) | **1.08 GB** | | Throughput (27 / 0.114) | **237 req/s** | | CPU utilization | ~100% | In practice, administrators often set `pm.max_children = 50–100`, which is above the optimum. Extra workers compete for CPU, increase the number of OS context switches, and consume memory without increasing throughput. #### Step 3: Coroutines (event loop) In the coroutine model, a single thread (per core) serves many requests. When a coroutine awaits I/O, the scheduler switches to another in ~200 nanoseconds (see [evidence base](/en/docs/evidence/coroutines-evidence.html)). The optimal number of coroutines is the same: $$ N\_{coroutines} = 8 \times 3.4 = 27 $$ | Metric | Value | |------------------------|---------------| | Coroutines | 27 | | Memory (27 × ~2 MiB) | **54 MiB** | | Throughput | **237 req/s** | | CPU utilization | ~100% | Throughput is **the same** — because the CPU is the bottleneck. But memory for concurrency: **54 MiB vs 1.08 GB** — a **~20x** difference. > **About coroutine stack size.** > The memory footprint of a coroutine in PHP is determined by the reserved C-stack size. > By default this is ~2 MiB, but it can be reduced to 128 KiB. > With a 128 KiB stack, memory for 27 coroutines would be just ~3.4 MiB. #### Step 4: What if CPU load is lower? The `Laravel` framework in `FPM` mode spends ~34 ms of `CPU` per request, which includes re-initialization of services on every request. In a stateful runtime (which `True Async` is), these costs are significantly reduced: routes are compiled, the dependency container is initialized, connection pools are reused. If `T_cpu` drops from 34 ms to 5 ms (which is realistic for stateful mode), the picture changes dramatically: | T\_cpu | Blocking coeff. | N (8 cores) | λ (req/s) | Memory (FPM) | Memory (coroutines) | |-------|-----------------|------------|-----------|--------------|---------------------| | 34 ms | 2.4 | 27 | 237 | 1.08 GB | 54 MiB | | 10 ms | 8 | 72 | 800 | 2.88 GB | 144 MiB | | 5 ms | 16 | 136 | 1 600 | 5.44 GB | 272 MiB | | 1 ms | 80 | 648 | 8 000 | **25.9 GB** | **1.27 GiB** | At `T_cpu = 1 ms` (lightweight handler, minimal overhead): * PHP-FPM would require **648 processes and 25.9 GB RAM** — unrealistic * Coroutines require the same 648 tasks and **1.27 GiB** — **~20x less** #### Step 5: Little's Law — verification through throughput Let's verify the result for `T_cpu = 5 ms`: $$ \lambda = \frac{L}{W} = \frac{136}{0.085} = 1,600 \text{ req/s} $$ To achieve the same throughput, PHP-FPM needs 136 workers. Each occupies ~40 MB: $$ 136 \times 40 \text{ MB} = 5.44 \text{ GB for workers alone} $$ Coroutines: $$ 136 \times 2 \text{ MiB} = 272 \text{ MiB} $$ The freed ~5.2 GB can be directed toward caches, DB connection pools, or handling more requests. #### Summary: When Coroutines Provide a Benefit | Condition | Benefit from coroutines | |-------------------------------------------------|--------------------------------------------------------------------------| | Heavy framework, localhost DB (T\_io ≈ 0) | Minimal — the workload is CPU-bound | | Heavy framework, cloud DB (T\_io = 80 ms) | Moderate — ~20x memory savings at the same throughput | | Lightweight handler, cloud DB | **Maximum** — throughput increase up to 13x, ~20x memory savings | | Microservice / API Gateway | **Maximum** — nearly pure I/O, tens of thousands of req/s on one server | **Conclusion:** the greater the share of I/O in total request time and the lighter the CPU processing, the greater the benefit from coroutines. For IO-bound applications (which is the majority of modern web services), coroutines allow utilizing the same CPU several times more efficiently, while consuming orders of magnitude less memory. ### Practical Notes * Increasing the number of coroutines above the optimal level rarely provides a benefit, but it is not a problem either: coroutines are lightweight, and the overhead from "extra" coroutines is incomparably small compared to the cost of OS threads * The real limitations become: * database connection pool * network latency * back-pressure mechanisms * open file descriptor limits (ulimit) * For such workloads, the *event loop + coroutines* model proves to be significantly more efficient than the classic blocking model ### Conclusion For a typical modern web application where I/O operations predominate, the asynchronous execution model allows you to: * effectively hide I/O latency * significantly improve CPU utilization * reduce the need for a large number of threads It is precisely in such scenarios that the advantages of asynchrony are most clearly demonstrated. *** ### Further Reading * [Swoole in Practice: Real-World Measurements](/en/docs/evidence/swoole-evidence.html) — production cases (Appwrite +91%, IdleMMO 35M req/day), independent benchmarks with and without DB, TechEmpower * [Python asyncio in Practice](/en/docs/evidence/python-evidence.html) — Duolingo +40%, Super.com −90% costs, uvloop benchmarks, counter-arguments * [Evidence Base: Why Single-Threaded Coroutines Work](/en/docs/evidence/coroutines-evidence.html) — context switch cost measurements, comparison with OS threads, academic research and industry benchmarks *** ### References and Literature * Brian Goetz, *Java Concurrency in Practice* (2006) — optimal thread pool size formula: `N = cores × (1 + W/S)` * [Zalando Engineering: How to set an ideal thread pool size](https://engineering.zalando.com/posts/2019/04/how-to-set-an-ideal-thread-pool-size.html) — practical application of Goetz's formula with examples and derivation through Little's Law * [Backendhance: The Optimal Thread-Pool Size in Java](https://backendhance.com/en/blog/2023/optimal-thread-pool-size/) — detailed analysis of the formula accounting for target CPU utilization * [CYBERTEC: PostgreSQL Network Latency](https://www.cybertec-postgresql.com/en/postgresql-network-latency-does-make-a-big-difference/) — measurements of network latency impact on PostgreSQL performance * [PostgresAI: What is a slow SQL query?](https://postgres.ai/blog/20210909-what-is-a-slow-sql-query) — guidelines for acceptable SQL query latencies in web applications --- --- url: https://true-async.github.io/en/docs/reference/iterate.md description: >- iterate() — concurrent iteration over an array or Traversable with concurrency control and lifecycle management of spawned coroutines. --- # iterate (PHP 8.6+, True Async 1.0.0) `iterate()` — Concurrently iterates over an array or `Traversable`, calling a `callback` for each element. ## Description ```php iterate(iterable $iterable, callable $callback, int $concurrency = 0, bool $cancelPending = true): void ``` Executes `callback` for each element of `iterable` in a separate coroutine. The `concurrency` parameter allows limiting the number of simultaneously running callbacks. The function blocks the current coroutine until all iterations complete. All coroutines spawned via `iterate()` run in an isolated child `Scope`. ## Parameters **`iterable`** An array or an object implementing `Traversable` (including generators and `ArrayIterator`). **`callback`** A function called for each element. Accepts two arguments: `(mixed $value, mixed $key)`. If the callback returns `false`, iteration stops. **`concurrency`** Maximum number of simultaneously running callbacks. Defaults to `0` — the default limit, all elements are processed concurrently. A value of `1` means execution in a single coroutine. **`cancelPending`** Controls the behavior of child coroutines spawned inside the callback (via `spawn()`) after iteration completes. * `true` (default) — all unfinished spawned coroutines are cancelled with `AsyncCancellation`. * `false` — `iterate()` waits for all spawned coroutines to complete before returning. ## Return Values The function does not return a value. ## Errors/Exceptions * `Error` — if called outside an async context or from the scheduler context. * `TypeError` — if `iterable` is not an array and does not implement `Traversable`. * If the callback throws an exception, iteration stops, remaining coroutines are cancelled, and the exception is propagated to the calling code. ## Examples ### Example #1 Basic array iteration ```php 'https://php.net', 'github' => 'https://github.com', 'google' => 'https://google.com', ]; iterate($urls, function(string $url, string $name) { $content = file_get_contents($url); echo "$name: " . strlen($content) . " bytes\n"; }); echo "All requests completed\n"; }); ?> ``` ### Example #2 Limiting concurrency ```php ``` ### Example #3 Stopping iteration by condition ```php ``` **Output:** ``` Processing: apple Processing: banana Processing: cherry Iteration finished ``` ### Example #4 Iterating over a generator ```php $i; } } spawn(function() { iterate(generateTasks(), function(int $value, string $key) { echo "$key: processing value $value\n"; }, concurrency: 2); echo "All tasks completed\n"; }); ?> ``` ### Example #5 Cancelling spawned coroutines (cancelPending = true) By default, coroutines spawned via `spawn()` inside the callback are cancelled after iteration completes: ```php ``` **Output:** ``` Background task 1 started Background task 2 started Background task 3 started Background task 1 cancelled Background task 2 cancelled Background task 3 cancelled Iteration finished ``` ### Example #6 Waiting for spawned coroutines (cancelPending = false) If you pass `cancelPending: false`, `iterate()` will wait for all spawned coroutines to complete: ```php ``` **Output:** ``` result-1, result-2, result-3 ``` ### Example #7 Error handling ```php getMessage() . "\n"; } }); ?> ``` ## Notes > **Note:** `iterate()` creates an isolated child Scope for all spawned coroutines. > **Note:** When an array is passed, `iterate()` creates a copy of it before iteration. > Modifying the original array inside the callback does not affect the iteration. > **Note:** If the `callback` returns `false`, iteration stops, > but already running coroutines continue until completion (or cancellation, if `cancelPending = true`). ## Changelog | Version | Description | |---------|------------------------------------| | 1.0.0 | Added the `iterate()` function. | ## See Also * [spawn()](/en/docs/reference/spawn.html) - Launching a coroutine * [await\_all()](/en/docs/reference/await-all.html) - Waiting for multiple coroutines * [Scope](/en/docs/components/scope.html) - The Scope concept * [Cancellation](/en/docs/components/cancellation.html) - Coroutine cancellation --- --- url: https://true-async.github.io/en/tutors/05-timeout.md description: Limiting how long await() waits, using timeout(). --- # Limiting How Long await Waits Often you need a guarantee that an operation won't take longer than some given time. For example, if `UserDirectory` doesn't respond for too long, it can make the `API` feel broken. There are two possible solutions here: 1. Set a timeout at the level of the `file_get_contents` operation and change the function's code. 2. Add a limit directly to `await`. ```php use function Async\timeout; use Async\OperationCanceledException; try { $isValid = await($validation, timeout(2000)); } catch (OperationCanceledException $e) { $validation->cancel(); } catch (RemoteApiException $e) { } ``` The benefit of limiting `await` is that there's no need to change the code of `validateToken`. At the same time, notice the `catch (OperationCanceledException $e)`, not `catch (TimeoutException $e)` as you might expect. ## OperationCanceledException `timeout()` throws nothing by itself. It returns a **cancellation token** — an `Async\Timeout` object. The code below will never throw, no matter how long the script runs: ```php use function Async\timeout; $token = timeout(2000); // just an object; nothing happens ``` The token only takes effect once it is handed to an operation as a cancellation argument: ```php use function Async\timeout; use Async\OperationCanceledException; try { $isValid = await($validation, timeout(2000)); } catch (OperationCanceledException $e) { $validation->cancel(); echo $e->getPrevious()->getMessage(); // message from TimeoutException } ``` Note that when the token trips you get an `OperationCanceledException`, not a `TimeoutException`. The timeout itself sits inside, in `getPrevious()`. This is intentional, to simplify the `try-catch` handling logic for `await` and to clearly distinguish a cancelled wait from an exception raised inside the coroutine. Coroutines normally should not throw `OperationCanceledException` themselves. The thing used to limit an `await` wait doesn't have to be `timeout()`; it can also be any other coroutine, or a `Future`, a logical contract representing the completion of some arbitrary operation. --- --- url: https://true-async.github.io/en/docs/reference/loadavg.md description: Async\loadavg() — 1/5/15-minute load average. POSIX; returns null on Windows. --- # loadavg (PHP 8.6+, True Async 1.0) `Async\loadavg()` returns the system load average over the last 1, 5, and 15 minutes, or `null` if the platform does not support load average (Windows). ## Description ```php namespace Async; function loadavg(): ?array ``` Load average is the average length of the kernel run queue. It is a **different metric** from CPU utilisation: on a 4-core machine a steady load of 4.0 means the run queue is, on average, fully populated. ## Return value `array{0: float, 1: float, 2: float}` — `[1min, 5min, 15min]`. On Windows the function returns `null`. ## Examples ### Example #1 Basic usage ```php $cpu) { error_log(sprintf( "[WARN] sustained load %.2f (5min) > %d CPUs", $load[1], $cpu )); } } }); ``` ## Notes > **Load average ≠ CPU usage.** A high load on a machine with light CPU usage usually means an > I/O-bound workload (processes stuck in `D`-state on disk/network). For CPU assessment prefer > [`cpu_usage()`](/en/docs/reference/cpu-usage.html). > **Windows.** There is no load-average concept in Windows (it is a BSD/Linux thing). The function > returns `null` — intentionally, with no emulation. ## See also * [Async\cpu\_usage()](/en/docs/reference/cpu-usage.html) — current process and system load * [Async\available\_parallelism()](/en/docs/reference/available-parallelism.html) — number of available CPUs * [Async\CpuSnapshot](/en/docs/reference/cpu-snapshot.html) — low-level CPU counters --- --- url: https://true-async.github.io/en/motivation.md description: Why PHP needs built-in asynchronous capabilities --- > *"The most dangerous phrase in the language is 'We've always done it this way.'"* > > — Grace Hopper ## Why does PHP need asynchrony? `PHP` is one of the last major languages that still lacks built-in support for concurrent execution **at the language level**. Python has `asyncio`, `JavaScript` is natively built on an event loop, `Go` has goroutines, `Kotlin` has coroutines. `PHP` remains in the "one request — one process" paradigm, even though most real-world applications spend the majority of their time waiting for `I/O` (`IO Bound`). ## The fragmentation problem Today, asynchrony in `PHP` is implemented through extensions: `Swoole`, `AMPHP`, `ReactPHP`. Each creates **its own ecosystem** with incompatible `APIs`, its own database drivers, `HTTP` clients and servers. This leads to critical problems: * **Code duplication** — each extension is forced to rewrite drivers for `MySQL`, `PostgreSQL`, `Redis` and other systems * **Incompatibility** — a library written for `Swoole` doesn't work with `AMPHP`, and vice versa * **Limitations** — extensions cannot make standard `PHP` functions (`file_get_contents`, `fread`, `curl_exec`) non-blocking, because they don't have access to the core * **Barrier to entry** — developers need to learn a separate ecosystem instead of using familiar tools ## The solution: core integration `TrueAsync` takes a different approach — **asynchrony at the PHP core level**. This means: ### Transparency Existing synchronous code works in coroutines without changes. `file_get_contents()`, `PDO::query()`, `curl_exec()` — all these functions automatically become non-blocking when executed inside a coroutine. ```php // This code already runs concurrently! spawn(function() { $data = file_get_contents('https://api.example.com/users'); // the coroutine suspends during the HTTP request, // other coroutines continue running }); ``` ### No colored functions Unlike Python (`async def` / `await`) and JavaScript (`async` / `await`), `TrueAsync` does not require marking functions as asynchronous. Any function can run inside a coroutine — there is no split between a "synchronous" and "asynchronous" world. ### A unified standard The standard `True Async ABI` as part of `Zend` allows **any** extension to support non-blocking `I/O`: `MySQL`, `PostgreSQL`, `Redis`, file operations, sockets — all through a single interface. No more duplicating drivers for each async framework. ### Backward compatibility Existing code continues to work, but now all PHP code is asynchronous by default. Everywhere. ## PHP workload: why this matters right now A typical PHP application (Laravel, Symfony, WordPress) spends **70–90% of its time waiting for I/O**: database queries, HTTP calls to external APIs, file reads. All that time, the CPU sits idle. With coroutines, this time is used efficiently: | Scenario | Without coroutines | With coroutines | |------------------------------|--------------------|------------------| | 3 DB queries at 20ms each | 60ms | ~22ms | | HTTP + DB + file | sequential | parallel | | 10 API calls | 10 × latency | ~1 × latency | Learn more: [IO-Bound vs CPU-Bound](/en/docs/evidence/concurrency-efficiency.html), [Concurrency Statistics](/en/docs/evidence/real-world-statistics.html). ## Practical scenarios * **Web servers** — handling many requests in a single process (`FrankenPHP`, `RoadRunner`) * **API Gateway** — parallel data aggregation from multiple microservices * **Background tasks** — concurrent queue processing * **Real-time** — WebSocket servers, chatbots, streaming ## See also: * [PHP RFC: True Async →](https://wiki.php.net/rfc/true_async){:target="\_blank"} * [RFC: Scope and Structured Concurrency](https://wiki.php.net/rfc/true_async_scope){:target="\_blank"} * [TrueAsync Documentation](/en/docs.html) * [Interactive Coroutine Demo](/en/interactive/coroutine-demo.html) --- --- url: https://true-async.github.io/en/docs/server/workers.md description: >- setWorkers(N): built-in thread pool on Async\ThreadPool. Bootloader, SO_REUSEPORT, per-request scope, request_context(). --- # Multi-worker (PHP 8.6+, true\_async\_server 0.6+) TrueAsync Server runs in **single-threaded** mode by default: one event loop, one thread, the entire pipeline (accept → parse → dispatch → respond) on a single CPU. This is the fastest model for typical IO-bound workloads, but it does not scale across cores. `setWorkers(N)` spins up the built-in pool of N OS threads via [`Async\ThreadPool`](/en/docs/components/thread-pool.html). Each worker re-binds the same listeners and the kernel (Linux/BSD) distributes accepts through `SO_REUSEPORT`. Each worker has its own independent event loop, its own opcache, and its own connection pools. ## Basic example ```php use TrueAsync\HttpServer; use TrueAsync\HttpServerConfig; $server = new HttpServer( (new HttpServerConfig()) ->addListener('0.0.0.0', 8080) ->setWorkers(4) ); $server->addHttpHandler(function ($req, $res) { $res->json(['pid' => getmypid()]); }); $server->start(); // blocks until all workers finish ``` `HttpServer::start()` in the parent: 1. Spawns an `Async\ThreadPool` of the desired size. 2. Uses `transfer_obj` to copy the config + handler set into each worker. 3. Starts the event loop inside the worker, which re-binds the listeners. 4. The parent `await`s the completion of all workers. ## Graceful shutdown `HttpServer::stop()` works on a pool parent. It retires the whole cohort and **suspends until the server is really down** — when it returns, the workers have drained, the pool is torn down and the listen sockets are closed. Call it from a coroutine; a signal handler is the usual place: ```php use function Async\spawn; use function Async\await; use function Async\signal; use Async\Signal; spawn(function () use ($server) { await(signal(Signal::SIGTERM)); $server->stop(); // returns once the pool is really down }); $server->start(); ``` On a **standalone** server (`setWorkers(1)`, the default) `stop()` does not suspend: it is normally called from a request handler, and the shutdown drain waits on that very handler — so a blocking `stop()` there would be waiting for itself. ## Hot reload `HttpServer::reload()` replaces the worker cohort without dropping a connection: the workers finish what they are holding, stop and exit, and fresh worker threads re-run the bootloader — picking up the changed code — and take over on the **same listen sockets**. It suspends until the old cohort has drained; `start()` keeps running throughout. Pool parent only. You rarely call it yourself. Wire a trigger instead: ```php $config ->setWorkers(4) ->setBootloader(function () { require __DIR__ . '/app/bootstrap.php'; // re-runs in every fresh worker }) // development: watch the tree and reload when it settles ->enableHotReload([__DIR__ . '/app'], ['php'], debounceMs: 300, maxHoldMs: 2000) // production: reload on SIGHUP, which is what a deploy script sends ->enableReloadOnSignal(); ``` `enableHotReload()` watches each path recursively. A settled burst of changes invalidates the watched trees in opcache and calls `reload()`. `debounceMs` is the quiet window before a burst fires one reload; `maxHoldMs` forces a reload at most that long after the first change, so a directory that never goes quiet still reloads. `enableReloadOnSignal()` arms a persistent SIGHUP handler (not supported on Windows). Both are pool-mode only. Whatever the trigger, the code the new workers pick up is whatever the bootloader loads — so anything you want reloaded must be loaded **there**, not at the top of the entry script, which runs once in the parent and never again. > If you call `reload()` by hand, invalidate the changed files first > (`opcache_invalidate()`) or rely on opcache timestamp validation — otherwise the fresh > workers compile the old code. ## Bootloader Heavy worker initialisation (autoload, pool warmup, JIT warmup) must run **once** at start, not per request. That is what `setBootloader(?\Closure $cb)` is for: ```php $config ->setWorkers(4) ->setBootloader(function () { // runs once in each worker before the task loop require __DIR__ . '/vendor/autoload.php'; // warm up the connection pool Database::initPool(min: 4, max: 16); // pre-compile critical routes Router::compile(); }); ``` The closure is deep-copied once and runs on every worker before it starts accepting tasks. **An exception thrown inside the bootloader fails the entire pool**: the worker does not start. The bootloader only applies when `setWorkers() > 1`. `null` removes it. > Requires TrueAsync ABI v0.15+. Test: `server/core/021-bootloader.phpt`. ## Per-request scope Since 0.6.5, each handler coroutine runs **in its own scope** that is a child of the server scope. This gives two important semantics: * [`Async\request_context()`](/en/docs/reference/request-context.html) — a shared context across the entire request coroutine tree (the handler and any child `spawn`s). * [`Async\current_context()`](/en/docs/reference/current-context.html) stays per-coroutine. ```php use function Async\spawn; use function Async\await; use function Async\request_context; $server->addHttpHandler(function ($req, $res) { // The context is visible to the entire coroutine branch of the request request_context()->set('request_id', $req->getHeader('X-Request-Id') ?? bin2hex(random_bytes(8))); request_context()->set('user_id', authUser($req)); // Fan-out [$user, $posts] = await(\Async\await_all([ spawn(fn() => fetchUser()), // request_id is visible here spawn(fn() => fetchPosts()), // and here ])); $res->json(['user' => $user, 'posts' => $posts]); }); ``` Compare: `current_context()` creates values visible **only** within the current coroutine; `request_context()` provides a shared subtree tied to the request scope. The child scope costs two allocations per request. `setRequestScope(false)` drops it and reuses the connection scope directly — but then `request_context()` returns `null`, so reach for `?->` if you turn it off. ## SO\_REUSEPORT and balancing On Linux/BSD the kernel distributes incoming connections evenly (but non-deterministically) across every socket opened with `SO_REUSEPORT` on the same `(host, port)`. Each worker opens its own; no userspace load balancer is needed, no locks. On Windows the `SO_REUSEPORT` equivalent is less predictable; lift the balancing one level up (into an LB) or use single-worker plus N processes on different ports. ## Cross-thread handler transfer If the configuration is built on one thread and the server runs on another, `HttpServer` supports the transfer. Since 0.2.0 the transfer path correctly preserves protocol masks (the "silently dropped every request" bug is fixed; see CHANGELOG and `core/007-server-transfer-handler-dispatch.phpt`). ## Debugging the multi-threaded mode Loud logging on an unexpected worker exit was added in 0.6.3. Uncaught `$server->start()` exceptions and clean returns while the await-loop is still waiting for workers are now visible in stderr (previously each case silently dropped 1/N of the accept capacity with no operator signal). Enable INFO logging: ```php use TrueAsync\LogSeverity; $config->setLogSinks([ ['type' => 'stderr', 'format' => 'pretty', 'level' => LogSeverity::INFO], ]); ``` > **Do not use `setLogStream()` under a worker pool.** A PHP stream resource opened by the > parent cannot cross into a worker thread: the sink stays active on the parent and is > skipped in the workers, with a notice at start-up. Use a sink each worker can open for > itself — `stderr`, `stdout`, or `file` (each worker reopens the path in append mode). > See [Observability](/en/docs/server/observability.html). ## How many workers? Rules of thumb: * **IO-bound** (standard web with DB/HTTP): start with `available_parallelism()`, watch CPU utilisation. * **CPU-bound** (rendering, compression-heavy, big JSON): `available_parallelism()` or fewer, watch p99 latency. * **Mixed**: overcommit by 1–2 workers (`N+1` or `N+2`) often yields better core utilisation under IO stalls. ```php $config->setWorkers(\Async\available_parallelism()); ``` > `Async\available_parallelism()` returns the number of CPUs available to the process (it takes > cgroup quotas and affinity into account). Backed by `uv_available_parallelism` with a fallback > to `uv_cpu_info`. ## See also * [`HttpServerConfig::setWorkers()`](/en/docs/reference/server/http-server-config.html#setworkers) * [`HttpServerConfig::setBootloader()`](/en/docs/reference/server/http-server-config.html#setbootloader) * [Observability](/en/docs/server/observability.html): cross-worker stats, logging under a pool * [`Async\ThreadPool`](/en/docs/components/thread-pool.html): pool internals * [`Async\request_context()`](/en/docs/reference/request-context.html) * [Backpressure / drain](/en/docs/server/configuration.html#graceful-drain-step-8) --- --- url: https://true-async.github.io/en/docs/server/observability.md description: >- Request statistics with getStats(), a Prometheus /metrics endpoint and Grafana, structured logging and an access log with setLogSinks(), and runtime counters. --- # Observability (PHP 8.6+, true\_async\_server 0.10+) The server can report request statistics, write structured logs, and emit one access-log record per request. Everything here is off by default. ## Request statistics: `getStats()` Turn statistics on with `setStatsEnabled(true)`, then read them with `getStats()`: ```php use TrueAsync\HttpServer; use TrueAsync\HttpServerConfig; $config = (new HttpServerConfig()) ->addListener('0.0.0.0', 8080) ->setWorkers(4) ->setStatsEnabled(true); $server = new HttpServer($config); $server->addHttpHandler(fn ($req, $res) => $res->json(['ok' => true])); $server->start(); ``` `getStats()` returns per-worker counters and a combined total. It throws if statistics were not enabled. ```php [ 'enabled' => true, 'workers' => [ 0 => [ /* one worker's counters */ ], 1 => [ … ] ], 'totals' => [ /* summed across workers */ ], ] ``` `totals` holds: | Counter | Meaning | |---------|---------| | `total_requests` | requests completed | | `responses_2xx_total` … `responses_5xx_total` | responses per status class; the four sum to `total_requests` | | `conns_active_h1` / `_h2` / `_h3` | open connections per protocol | Totals keep growing across a `reload()`; the connection counters track only live workers. ## Prometheus and Grafana The server does not expose a `/metrics` endpoint itself — `getStats()` hands you a plain PHP array, and you turn it into whatever your monitoring stack expects. For Prometheus that means one small handler that formats the array as the [text exposition format](https://prometheus.io/docs/instrumenting/exposition_formats/): ```php use TrueAsync\HttpServer; use TrueAsync\HttpServerConfig; $config = (new HttpServerConfig()) ->addListener('0.0.0.0', 8080) ->setWorkers(4) ->setStatsEnabled(true); $server = new HttpServer($config); $server->addHttpHandler(function ($req, $res) use ($server) { if ($req->getPath() === '/metrics') { $t = $server->getStats()['totals']; $body = "# HELP http_requests_total Requests completed.\n"; $body .= "# TYPE http_requests_total counter\n"; $body .= "http_requests_total {$t['total_requests']}\n"; $body .= "# HELP http_responses_total Responses by status class.\n"; $body .= "# TYPE http_responses_total counter\n"; foreach (['2xx', '3xx', '4xx', '5xx'] as $class) { $body .= "http_responses_total{class=\"{$class}\"} {$t["responses_{$class}_total"]}\n"; } $body .= "# HELP http_connections_active Open connections by protocol.\n"; $body .= "# TYPE http_connections_active gauge\n"; foreach (['h1', 'h2', 'h3'] as $proto) { $body .= "http_connections_active{protocol=\"{$proto}\"} {$t["conns_active_{$proto}"]}\n"; } $res->setHeader('Content-Type', 'text/plain; version=0.0.4')->end($body); return; } $res->json(['ok' => true]); }); $server->start(); ``` Point Prometheus at the endpoint: ```yaml scrape_configs: - job_name: 'true-async-server' static_configs: - targets: ['your-server:8080'] ``` The counters live in one process-wide table that every worker updates and `getStats()` reads, so a single scrape covers the whole pool: ![Metrics flow from workers to Grafana](/diagrams/en/server-observability/metrics-flow.svg) From there Grafana graphs the request rate, status classes and open connections like any other Prometheus source: ![Grafana dashboard over the server's metrics](/diagrams/en/server-observability/grafana-dashboard.png) ## Logging: `setLogSinks()` `setLogSinks()` sends each log record to one or more destinations, each with its own format and minimum level: ```php use TrueAsync\LogSeverity; $config->setLogSinks([ ['type' => 'file', 'path' => '/var/log/app/access.log', 'format' => 'json', 'category' => 'access', 'level' => LogSeverity::INFO], ['type' => 'stderr', 'format' => 'pretty', 'level' => LogSeverity::WARN], ]); ``` Up to 8 destinations. This replaces the single-stream `setLogSeverity()` / `setLogStream()`. **Where a record goes** — `type` is `file`, `stdout`, `stderr`, `syslog`, or `stream`. With a worker pool, use `file` (or `stdout` / `stderr`): a `stream` resource opened by the parent cannot be shared with worker threads, so it is used only on the parent. **How it looks** — `format` is `plain`, `logfmt`, `json`, `pretty` (a coloured console line), or `template`: ```php ['type' => 'stdout', 'format' => 'template', 'template' => '{ts:Y-m-d H:i:s.v} [{level}] {msg}{attrs}', 'level' => LogSeverity::INFO] ``` Placeholders: `{ts}` or `{ts:PATTERN}` (`date()`-style `Y y m d H i s v`), `{level}`, `{msg}`, `{attrs}`, `{trace}`, `{span}`. Anything else is printed as written. A `syslog` destination speaks RFC 5424 over TCP, UDP, or a unix socket: ```php ['type' => 'syslog', 'target' => 'udg:///dev/log', 'facility' => 'local0', 'level' => LogSeverity::INFO] ``` ### Access log Set `category` to choose what a destination receives: `app` (the default) gets server diagnostics, `access` gets one record per completed request, `all` gets both. So a JSON access log and a readable diagnostics console can run side by side. Access records follow the OpenTelemetry HTTP conventions. One `json` record: ```json { "Timestamp": "2026-07-15T07:03:37.740Z", "SeverityText": "INFO", "Body": "GET /x 200", "Attributes": { "http.request.method": "GET", "url.path": "/x", "http.response.status_code": 200, "network.protocol.version": "1.1", "http.response.body.size": 11, "http.server.request.duration": 9.266e-06, "client.address": "127.0.0.1", "client.port": 42336 } } ``` A record is written for every request over HTTP/1, HTTP/2 and HTTP/3, including under a worker pool. If the request carried a W3C trace context, it is included. ## Runtime counters: `getRuntimeStats()` `getRuntimeStats()` reports the server's own memory pools and cross-worker WebSocket topic traffic — useful for attributing memory growth to a subsystem. No opt-in needed. Keys include the connection arena (`conn_arena_*`), the request-body pool (`body_pool*`), and topic delivery (`ws_topic_posted` / `ws_topic_skipped` / `ws_topic_dropped`). ## HTTP/3 counters: `getHttp3Stats()` `getHttp3Stats()` returns one entry per HTTP/3 listener with its QUIC counters (`quic_packets_sent`, `quic_bytes_sent`, datagram counts, and so on). It returns an empty array on a build without `--enable-http3`. ## See also * [Multi-worker](/en/docs/server/workers.html): logging and shutdown under a pool * [Configuration](/en/docs/server/configuration.html) * [`HttpServer::getStats()`](/en/docs/reference/server/http-server.html) --- --- url: https://true-async.github.io/en/tutors/09-pdo-pool.md description: >- Why coroutines can't share a single PDO, and how the built-in connection pool solves that transparently. --- # PDO Pool The import is almost done: the workers check addresses via `GeoDirectory`. All that's left is saving the checked addresses to the database. Sounds trivial: ```php $pdo = new PDO('mysql:host=localhost;dbname=profiles', 'app', 'secret'); for ($i = 0; $i < 10; $i++) { $workers->spawn(function () use ($queue, $pdo) { foreach ($queue as $address) { checkAddress($address); saveAddress($pdo, $address); } }); } ``` One `PDO` object shared by ten coroutines. In the chapter on channels we said that data races don't happen within a single thread, so this should be fine, right? No. That's true for memory. But a database connection isn't memory, it's a protocol on top of a socket: request, response, request, response, strictly in order. Every database query is a wait point where a coroutine goes to sleep and control passes to another one. And that other one writes its own query into the very same socket: ```php // Worker 1 $pdo->beginTransaction(); $pdo->exec("INSERT INTO addresses ..."); // wait point: worker 1 goes to sleep, worker 2 wakes up // Worker 2 $pdo->beginTransaction(); // on the same connection! $pdo->exec("UPDATE ..."); $pdo->commit(); // commits both its own transaction and someone else's ``` The responses get scrambled, transactions commit each other's work. The races are back, only now they live in the connection instead of in memory. All right, so give each coroutine its own connection? ```php $workers->spawn(function () use ($queue) { $pdo = new PDO(/* ... */); // its own connection // ... }); ``` Fine for ten workers. But picture not an import job, but a server, where a coroutine gets created for every request. A thousand coroutines means a thousand TCP connections. MySQL allows 151 by default, PostgreSQL 100. And opening a connection for a few milliseconds of work is simply expensive: the handshake with the database can take longer than the query itself. Sound familiar? The chapter on channels had the same fork in the road: a hundred thousand connections to `GeoDirectory`, or a queue for ten. ## A pool: a queue in reverse The solution is called a connection pool: open N connections ahead of time and hand them to coroutines for the duration of their work. As it happens, we already know how to build one. A pool is a channel that holds connections: ```php $pool = new Channel(5); for ($i = 0; $i < 5; $i++) { $pool->send(new PDO(/* ... */)); } // Inside a coroutine: $pdo = $pool->recv(); // take a connection saveAddress($pdo, $address); $pool->send($pdo); // return it ``` Free connections sit in the buffer. If they're all busy, `recv` puts the coroutine to sleep until somebody returns a connection. The same synchronization from the previous chapter, only the queue holds resources instead of tasks. The scheme works, but it has a weak spot: you have to remember to return the connection, no matter what happens, including an exception or a cancellation. And then there are transactions, broken connections, reconnects. For PDO, all of that has already been taken care of, right in the core. ## PDO Pool The pool is built into `PDO` itself and is turned on via constructor attributes: ```php $pdo = new PDO('mysql:host=localhost;dbname=profiles', 'app', 'secret', [ PDO::ATTR_POOL_ENABLED => true, PDO::ATTR_POOL_MIN => 2, PDO::ATTR_POOL_MAX => 10, ]); ``` From the outside, nothing changed: that very first example, where a single `$pdo` goes out to ten workers, is now correct. The `$pdo` object is no longer a connection, it's a facade for the pool. When a coroutine runs its first query, the pool hands it a dedicated connection, and all of that coroutine's queries go through it. Once the coroutine finishes, the connection goes back to the pool, ready for the next coroutine. No manual `recv` and `send`: acquiring and returning happen on their own, at the right moments, no matter how things turn out. The code looks like ordinary synchronous PHP with an ordinary PDO, and that was the whole idea. ## Transactions A transaction is state that belongs to a connection, so the pool treats it specially: while a transaction is open, the connection is pinned to its coroutine and won't go back to the pool: ```php $workers->spawn(function () use ($pdo, $queue) { // ... $pdo->beginTransaction(); $pdo->exec("INSERT INTO addresses (user_id, region) VALUES (...)"); $pdo->exec("UPDATE users SET address_checked = 1 WHERE id = ..."); $pdo->commit(); // only now can the connection return to the pool }); ``` What if a coroutine finishes without calling `commit`? Recall the chapter on Scope: a worker could get cancelled right in the middle of a transaction, and that's a normal scenario, not a catastrophe. Before returning the connection, the pool automatically runs a `ROLLBACK`. An unfinished transaction won't leak into the next coroutine or hang around in the database. ## When a connection drops An import can run for an hour. Within an hour, the database might restart, the network might blip, or a DBA might kill a session. In classic PHP the script would simply crash, but a long-running application needs to be able to keep going. The pool checks connections when they're returned: a broken one gets destroyed instead of being handed to the next coroutine. And if a query fails because of a dropped connection, simply retrying it on the same `$pdo` is enough, the pool will hand the coroutine a fresh connection: ```php try { saveAddress($pdo, $address); } catch (PDOException $e) { saveAddress($pdo, $address); // the pool has already swapped in a new connection } ``` No need to write reconnect logic, no need to recreate the `PDO` object. Just retry the query, the pool takes care of everything else. In the end, the code works as if every coroutine had its own database connection. In reality there are only ten connections, and the pool constantly passes them from hand to hand, keeps an eye on transactions, and discards the dead ones. But none of that kitchen work shows from the outside, and that's the main benefit: we just write ordinary code with PDO. By the way, our workers are still working half-blind: they check addresses and save them, but nobody's counting how many addresses turned out to be bad, or which ones. How do we get results back from coroutines and collect them conveniently? That's what we'll tackle in the next chapter. --- --- url: https://true-async.github.io/en/architecture/pdo-pool.md description: >- Internal design of PDO Pool -- components, connection lifecycle, binding to coroutines, credentials management. --- # PDO Pool Architecture > This article describes the internal design of PDO Pool. > If you are looking for a usage guide, see [PDO Pool: Connection Pool](/en/docs/components/pdo-pool.html). ## Two-Level Architecture PDO Pool consists of two layers: **1. PDO Core (`pdo_pool.c`)** -- logic for binding connections to coroutines, transaction management, statement reference counting. **2. Async Pool (`zend_async_pool_t`)** -- the universal resource pool from the async extension. Manages the queue of free connections, limits, and healthchecks. It knows nothing about PDO -- it works with abstract `zval` values. This separation allows using the same pooling mechanism for any resources, not just databases. ## Component Diagram ![PDO Pool -- Components](/diagrams/en/architecture-pdo-pool/components.svg) ## Template Connection When creating a `PDO` with a pool, the core **does not open** a real TCP connection. Instead, a **template** is created -- a `pdo_dbh_t` object that stores the DSN, username, password, and a reference to the driver. All real connections are created later, on demand, based on this template. For the template, `db_handle_init_methods()` is called instead of `db_handle_factory()`. This method sets the driver's method table (`dbh->methods`) but does not create a TCP connection or allocate `driver_data`. ## Connection Lifecycle ![Connection Lifecycle in the Pool](/diagrams/en/architecture-pdo-pool/lifecycle.svg) ## Creating a Connection from the Pool (Sequence) ![Creating a Connection from the Pool](/diagrams/en/architecture-pdo-pool/connection-sequence.svg) ## Internal API ### pdo\_pool.c -- Public Functions | Function | Purpose | |----------------------------|----------------------------------------------------------------| | `pdo_pool_create()` | Creates a pool for `pdo_dbh_t` based on constructor attributes | | `pdo_pool_destroy()` | Releases all connections, closes the pool, clears the hash table | | `pdo_pool_acquire_conn()` | Returns a connection for the current coroutine (reuse or acquire) | | `pdo_pool_peek_conn()` | Returns the bound connection without acquire (NULL if none) | | `pdo_pool_maybe_release()` | Returns the connection to the pool if no transaction or statements | | `pdo_pool_get_wrapper()` | Returns the `Async\Pool` PHP object for the `getPool()` method | ### pdo\_pool.c -- Internal Callbacks | Callback | When Called | |-----------------------------|-----------------------------------------------------------| | `pdo_pool_factory()` | Pool needs a new connection (acquire when pool is empty) | | `pdo_pool_destructor()` | Pool destroys a connection (on close or eviction) | | `pdo_pool_healthcheck()` | Periodic check -- is the connection still alive? | | `pdo_pool_before_release()` | Before returning to pool -- rollback uncommitted transactions | | `pdo_pool_free_conn()` | Closes the driver connection, frees memory | ### Binding to a Coroutine Connections are bound to coroutines via a `pool_connections` hash table, where the key is the coroutine identifier and the value is a pointer to `pdo_dbh_t`. The coroutine identifier is computed by the `pdo_pool_coro_key()` function: * If the coroutine is a PHP object -- `zend_object.handle` (sequential uint32\_t) is used * For internal coroutines -- the pointer address shifted by `ZEND_MM_ALIGNMENT_LOG2` ### Cleanup on Coroutine Completion When a connection is bound to a coroutine, a `pdo_pool_cleanup_callback` is registered via `coro->event.add_callback()`. When the coroutine completes (normally or with an error), the callback automatically returns the connection to the pool. This guarantees no connection leaks even with unhandled exceptions. ### Pinning: Connection Locking A connection is pinned to a coroutine and will not return to the pool if at least one condition is met: * `conn->in_txn == true` -- an active transaction * `conn->pool_slot_refcount > 0` -- there are live statements (`PDOStatement`) using this connection The refcount is incremented when a statement is created and decremented when it is destroyed. When both conditions are cleared, `pdo_pool_maybe_release()` returns the connection to the pool. ## Credentials Management in the Factory When creating a new connection, `pdo_pool_factory()` **copies** the DSN, username, and password strings from the template via `estrdup()`. This is necessary because drivers may mutate these fields during `db_handle_factory()`: * **PostgreSQL** -- replaces `;` with spaces in `data_source` * **MySQL** -- allocates `username`/`password` from DSN if they were not passed * **ODBC** -- completely rebuilds `data_source`, embedding credentials After a successful `db_handle_factory()` call, the copies are freed via `efree()`. On error, freeing happens through `pdo_pool_free_conn()`, which is also used by the pool's destructor. ## Incompatibility with Persistent Connections Persistent connections (`PDO::ATTR_PERSISTENT`) are incompatible with the pool. A persistent connection is bound to the process and survives across requests, while the pool creates connections at the request level with automatic lifecycle management. Attempting to enable both attributes simultaneously will result in an error. ## What's Next? * [PDO Pool: Connection Pool](/en/docs/components/pdo-pool.html) -- usage guide * [Coroutines](/en/docs/components/coroutines.html) -- how coroutines work * [Scope](/en/docs/components/scope.html) -- managing coroutine groups --- --- url: https://true-async.github.io/en/docs/components/pdo-pool.md description: >- PDO Pool -- built-in database connection pool for coroutines: transparent pooling, transactions, automatic rollback. --- # PDO Pool: Database Connection Pool ## The Problem When working with coroutines, the problem of sharing I/O descriptors arises. If the same socket is used by two coroutines that simultaneously write or read different packets from it, the data will get mixed up and the result will be unpredictable. Therefore, you cannot simply use the same `PDO` object in different coroutines! On the other hand, creating a separate connection for each coroutine over and over is a very wasteful strategy. It negates the advantages of concurrent I/O. Therefore, connection pools are typically used for interacting with external APIs, databases, and other resources. ```php $pdo = new PDO('mysql:host=localhost;dbname=app', 'root', 'secret'); // Ten coroutines simultaneously use the same $pdo for ($i = 0; $i < 10; $i++) { spawn(function() use ($pdo, $i) { $pdo->beginTransaction(); $pdo->exec("INSERT INTO orders (user_id) VALUES ($i)"); // Another coroutine already called COMMIT on this same connection! $pdo->commit(); // Chaos }); } ``` You could create a separate connection in each coroutine, but then with a thousand coroutines you'd get a thousand TCP connections. MySQL allows 151 simultaneous connections by default. PostgreSQL -- 100. ## The Solution: PDO Pool **PDO Pool** -- a database connection pool built into the PHP core. It automatically gives each coroutine its own connection from a pre-prepared set and returns it back when the coroutine finishes working. ```php $pdo = new PDO('mysql:host=localhost;dbname=app', 'root', 'secret', [ PDO::ATTR_POOL_ENABLED => true, PDO::ATTR_POOL_MIN => 2, PDO::ATTR_POOL_MAX => 10, ]); // Ten coroutines -- each gets its own connection for ($i = 0; $i < 10; $i++) { spawn(function() use ($pdo, $i) { // Pool automatically allocates a connection for this coroutine $pdo->beginTransaction(); $pdo->exec("INSERT INTO orders (user_id) VALUES ($i)"); $pdo->commit(); // Connection is returned to the pool }); } ``` From the outside, the code looks as if you're working with a regular `PDO`. The pool is completely transparent. ## How to Enable The pool is enabled via `PDO` constructor attributes: ```php $pdo = new PDO($dsn, $user, $password, [ PDO::ATTR_POOL_ENABLED => true, // Enable pool PDO::ATTR_POOL_MIN => 0, // Minimum connections (default 0) PDO::ATTR_POOL_MAX => 10, // Maximum connections (default 10) PDO::ATTR_POOL_HEALTHCHECK_INTERVAL => 30000, // Health check interval (ms, 0 = disabled) ]); ``` | Attribute | Meaning | Default | |-----------------------------|----------------------------------------------------------------------|---------| | `POOL_ENABLED` | Enable the pool | `false` | | `POOL_MIN` | Minimum number of connections the pool keeps open | `0` | | `POOL_MAX` | Maximum number of simultaneous connections | `10` | | `POOL_HEALTHCHECK_INTERVAL` | How often to check that a connection is alive (in milliseconds) | `0` | | `POOL_STMT_CACHE_SIZE` | Prepared-statement cache size per physical connection | `0` (off) | ## Binding Connections to Coroutines Each coroutine gets **its own** connection from the pool. All calls to `query()`, `exec()`, `prepare()` within a single coroutine go through the same connection. ```php $pdo = new PDO($dsn, $user, $password, [ PDO::ATTR_POOL_ENABLED => true, PDO::ATTR_POOL_MAX => 5, ]); $coro1 = spawn(function() use ($pdo) { // All three queries go through connection #1 $pdo->query("SELECT 1"); $pdo->query("SELECT 2"); $pdo->query("SELECT 3"); // Coroutine finished -- connection #1 returns to pool }); $coro2 = spawn(function() use ($pdo) { // All queries go through connection #2 $pdo->query("SELECT 4"); // Coroutine finished -- connection #2 returns to pool }); ``` If a coroutine is no longer using the connection (no active transactions or statements), the pool may return it earlier -- without waiting for the coroutine to finish. ## Prepared-statement cache Enabled with the `PDO::ATTR_POOL_STMT_CACHE_SIZE => N` attribute when constructing the `PDO`. The pool keeps an LRU cache of the last `N` prepared statements **per physical connection**. When a coroutine re-runs `prepare()` with the same SQL, the pool returns the already-prepared **server-side** statement — no round trip to the database. ```php $pdo = new PDO($dsn, $user, $password, [ PDO::ATTR_POOL_ENABLED => true, PDO::ATTR_POOL_MAX => 10, PDO::ATTR_POOL_STMT_CACHE_SIZE => 64, // up to 64 stmts per connection ]); spawn(function () use ($pdo) { for ($i = 0; $i < 1000; $i++) { // First call: a real PREPARE on the server. // Every subsequent call on this connection: cache hit, zero wire traffic. $stmt = $pdo->prepare('SELECT name FROM users WHERE id = ?'); $stmt->execute([$i]); $row = $stmt->fetch(); } }); ``` On a tight `prepare → execute → fetch` loop this gives **~2.9×** speedup (depending on the driver and workload). ### Supported drivers `pdo_pgsql`, `pdo_mysql`, `pdo_sqlite`. ### When the cache is bypassed The cache is automatically skipped in the following cases to preserve semantics: * `PDO_CURSOR_SCROLL` — the server-side cursor of a scrollable result cannot be reused. * `PDO::ATTR_EMULATE_PREPARES = true` — emulated queries have no server-side statement. * `PGSQL_ATTR_DISABLE_PREPARES` — an explicit opt-out on the PG driver side. ### Cache invalidation on schema / plan changes If a table's schema changes (`ALTER TABLE`), the server-side plan of an old statement may stop being valid. The pool recognises such errors and **transparently re-runs** the query: the stale statement is evicted from the cache, a new `prepare` is issued, and the user code **gets a successful result on the first try**. | Driver | Error codes that trigger a retry | |--------|----------------------------------| | PostgreSQL | SQLSTATE `0A000` (feature not supported, cached plan must not change result type), `26000` (invalid SQL statement name) | | MySQL | `1243` (unknown prepared statement handler), `1615` (prepared statement needs to be re-prepared), `2057` (statement has wrong column count) | ### How big should it be? The LRU operates **independently per physical connection**, so the total prepared-statement memory on the DB server is roughly `POOL_MAX × POOL_STMT_CACHE_SIZE` statements at peak. Reasonable values: * web app with a couple dozen unique SQL strings — `16..32`; * service with a wide variety of queries — `64..256`; * if SQL is essentially unique every time — the cache buys nothing, leave it at `0`. ## Transactions Transactions work the same as in regular PDO. But the pool guarantees that while a transaction is active, the connection is **pinned** to the coroutine and won't return to the pool. ```php spawn(function() use ($pdo) { $pdo->beginTransaction(); $pdo->exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1"); $pdo->exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2"); $pdo->commit(); // Only after commit can the connection return to the pool }); ``` ### Automatic Rollback If a coroutine finishes without calling `commit()`, the pool automatically rolls back the transaction before returning the connection to the pool. This is a safeguard against accidental data loss. ```php spawn(function() use ($pdo) { $pdo->beginTransaction(); $pdo->exec("DELETE FROM users WHERE id = 1"); // Forgot commit() // Coroutine finished -- pool will call ROLLBACK automatically }); ``` ## Connection Lifecycle ![Connection lifecycle in the pool](/diagrams/en/components-pdo-pool/connection-lifecycle.svg) A detailed technical diagram with internal calls is in the [PDO Pool architecture](/en/architecture/pdo-pool.html). ## Accessing the Pool Object The `getPool()` method returns the `Async\Pool` object through which you can get statistics: ```php $pool = $pdo->getPool(); if ($pool !== null) { echo "Pool is active: " . get_class($pool) . "\n"; // Async\Pool } ``` If the pool is not enabled, `getPool()` returns `null`. ## When to Use **Use PDO Pool when:** * The application runs in asynchronous mode with TrueAsync * Multiple coroutines simultaneously access the database * You need to limit the number of connections to the database **Not needed when:** * The application is synchronous (classic PHP) * Only one coroutine works with the database * Persistent connections are used (they are incompatible with the pool) ## Supported Drivers | Driver | Pool Support | |--------------|--------------| | `pdo_mysql` | Yes | | `pdo_pgsql` | Yes | | `pdo_sqlite` | Yes | | `pdo_odbc` | No | ## Error Handling If the pool cannot create a connection (wrong credentials, unavailable server), the exception is propagated to the coroutine that requested the connection: ```php $pdo = new PDO('mysql:host=localhost;dbname=app', 'root', 'wrong_password', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_POOL_ENABLED => true, PDO::ATTR_POOL_MIN => 0, ]); spawn(function() use ($pdo) { try { $pdo->query("SELECT 1"); } catch (PDOException $e) { echo "Failed to connect: " . $e->getMessage() . "\n"; } }); ``` Note `POOL_MIN => 0`: if you set the minimum higher than zero, the pool will try to create connections in advance, and the error will occur when creating the PDO object. ## Broken Connection Recovery In a long-running async application connections can break at any moment: the database server restarts, a DBA kills a session, a network glitch drops the TCP link, or a coroutine is cancelled while a query is in flight. Without special handling the broken connection would silently return to the pool and the **next** coroutine to use it would get a confusing error like `MySQL server has gone away`. PDO Pool solves this automatically at two levels. ### Automatic detection on return When a connection is returned to the pool (after a coroutine finishes or releases it), the pool checks whether the connection is still healthy. If the connection is broken — it is **destroyed** instead of being placed back into the pool. The next coroutine gets a fresh, working connection. ```php $pdo = new PDO('mysql:host=localhost;dbname=app', 'root', 'secret', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_POOL_ENABLED => true, PDO::ATTR_POOL_MAX => 5, ]); spawn(function () use ($pdo) { $pdo->query("SELECT SLEEP(30)"); // Meanwhile the DBA runs: KILL // The query throws PDOException. // When this coroutine ends, the pool detects the broken connection // and destroys it instead of returning it to the pool. }); spawn(function () use ($pdo) { // This coroutine gets a brand-new healthy connection, // NOT the killed one from the previous coroutine. $rows = $pdo->query("SELECT * FROM users")->fetchAll(); }); ``` ### Transparent reconnect on retry When a coroutine catches an error from a broken connection and retries the query **on the same `$pdo` object**, the pool transparently discards the broken connection and acquires a fresh one. No manual reconnection code needed: ```php spawn(function () use ($pdo) { try { $pdo->query("SELECT 1"); } catch (PDOException $e) { // Connection was broken (server restart, network issue, etc.) // The pool has already discarded the broken connection internally. // Just retry — the pool gives this coroutine a new connection: $pdo->query("SELECT 1"); // works } }); ``` This means a simple `try/catch` with a retry is all you need for robust connection handling. You do **not** need to create a new `PDO` object or manually reconnect — the pool does it for you. ### Scenarios covered | Scenario | What happens | |----------|-------------| | Database server restarts | Broken connections are detected and destroyed on return to pool | | DBA kills a session | Same — the killed connection never reaches another coroutine | | Coroutine cancelled mid-query | Connection is detected as broken and destroyed | | Network timeout / TCP reset | Pool discards the connection, next acquire gets a fresh one | ## Error State Isolation In a pool each coroutine shares the same `$pdo` PHP object but uses a different underlying database connection. PDO Pool ensures that error state from one coroutine never leaks into another. ### `errorCode()` and `errorInfo()` Each coroutine sees **only its own** error state through `$pdo->errorCode()` and `$pdo->errorInfo()`. A failed query in one coroutine does not affect the error code seen by another: ```php $pdo = new PDO('mysql:host=localhost;dbname=app', 'root', 'secret', [ PDO::ATTR_POOL_ENABLED => true, ]); $coro1 = spawn(function () use ($pdo) { $pdo->query("SELECT * FROM nonexistent_table"); // fails echo $pdo->errorCode(); // e.g. "42S02" — only this coroutine's error }); $coro2 = spawn(function () use ($pdo) { $pdo->query("SELECT 1"); // succeeds echo $pdo->errorCode(); // "00000" — not affected by coro1's failure }); ``` ### Consistent initial state `$pdo->errorCode()` always returns `"00000"` before the first query in a coroutine, even when multiple coroutines start concurrently on fresh connections. ```php spawn(function () use ($pdo) { // Guaranteed "00000", never NULL — even on a fresh pool connection echo $pdo->errorCode(); // "00000" }); ``` ## Real-World Example: Parallel Order Processing ```php use function Async\spawn; use function Async\await; $pdo = new PDO('mysql:host=localhost;dbname=shop', 'app', 'secret', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_POOL_ENABLED => true, PDO::ATTR_POOL_MIN => 2, PDO::ATTR_POOL_MAX => 5, ]); // Get a list of orders to process $orders = [101, 102, 103, 104, 105, 106, 107, 108, 109, 110]; $coroutines = []; foreach ($orders as $orderId) { $coroutines[] = spawn(function() use ($pdo, $orderId) { // Each coroutine gets its own connection from the pool $pdo->beginTransaction(); $stmt = $pdo->prepare("SELECT * FROM orders WHERE id = ? FOR UPDATE"); $stmt->execute([$orderId]); $order = $stmt->fetch(); if ($order['status'] === 'pending') { $pdo->exec("UPDATE orders SET status = 'processing' WHERE id = $orderId"); $pdo->exec("INSERT INTO order_log (order_id, action) VALUES ($orderId, 'started')"); } $pdo->commit(); return $orderId; }); } // Wait for all coroutines to complete foreach ($coroutines as $coro) { $processedId = await($coro); echo "Order #$processedId processed\n"; } ``` Ten orders are processed concurrently, but through a maximum of five database connections. Each transaction is isolated. Connections are reused between coroutines. ## What's Next? * [Interactive PDO Pool Demo](/en/interactive/pdo-pool-demo.html) -- a visual demonstration of connection pool operation * [PDO Pool Architecture](/en/architecture/pdo-pool.html) -- pool internals, diagrams, connection lifecycle * [Coroutines](/en/docs/components/coroutines.html) -- how coroutines work * [Scope](/en/docs/components/scope.html) -- managing groups of coroutines * [spawn()](/en/docs/reference/spawn.html) -- launching coroutines * [await()](/en/docs/reference/await.html) -- awaiting results --- --- url: https://true-async.github.io/en/tutors/13-pool.md description: >- Async\Pool: a universal resource pool with health checks and a circuit breaker. --- # Pool In the PDO Pool chapter we sketched a channel-based pool in six lines and immediately admitted it was naive for real life: you have to remember to return the resource no matter how things turn out, recognize broken connections, and replace the dead ones. For PDO, the core did all of that for us. But `checkAddress` talks to `GeoDirectory` over HTTP, and opening a fresh connection to it on every request would be wasteful too. And tomorrow there will be Redis for caching and SMTP for email. Surely we're not going to hand-build a channel pool for every single resource. Right, there's a ready-made primitive for this, the very one working under the hood of PDO Pool: `Async\Pool`. ## Factory and destructor The pool doesn't know what kind of resource it's holding. You tell it two things: how to create a resource and how to destroy one: ```php use Async\Pool; $geo = new Pool( factory: fn() => new GeoConnection('geodirectory.example.com'), destructor: fn($conn) => $conn->close(), min: 2, max: 10, ); ``` `min: 2` means two connections are opened up front, so the first coroutines don't have to wait through a handshake. `max: 10` means the pool will never create more than ten connections, no matter how many coroutines ask for one. It's the familiar concurrency limit, except now it's tied to the resource itself rather than to a number of workers. Next comes the cycle we've already built by hand with a channel: ```php $conn = $geo->acquire(); try { $verdict = $conn->request("/check?address=$address"); } finally { $geo->release($conn); } ``` `acquire` hands out a free resource. If none are free and the limit hasn't been reached, the factory creates a new one. If the limit has been reached, the coroutine goes to sleep until someone else calls `release`: the same mechanics as `recv` on an empty channel. Notice the `finally`: the resource is returned no matter how things turn out, including an exception or a cancellation. That's exactly the point where our hand-rolled pool used to stumble. You can also wait with a limit, in the usual way: ```php $conn = $geo->acquire(timeout: 3000); // TimeoutException if it doesn't arrive within 3 seconds ``` ## Resources die A connection that has been sitting in the pool for half an hour may have died quietly: the server closed it after a timeout, the network blipped. We already saw in the PDO chapter what that leads to: the next coroutine gets a mysterious error out of nowhere. The pool fights this on three fronts: ```php $geo = new Pool( factory: fn() => new GeoConnection('geodirectory.example.com'), destructor: fn($conn) => $conn->close(), healthcheck: fn($conn) => $conn->ping(), beforeAcquire: fn($conn) => $conn->isAlive(), beforeRelease: fn($conn) => !$conn->isBroken(), min: 2, max: 10, healthcheckInterval: 30000, ); ``` * **`healthcheck`** — every thirty seconds the pool walks its free resources on its own and checks their pulse. Dead ones are destroyed and replaced with new ones. * **`beforeAcquire`** — a final check before handing a resource out. If it fails, the resource is destroyed and the coroutine gets the next one. * **`beforeRelease`** — a check on return. A coroutine may have broken the connection partway through a request; a resource like that doesn't go back into the pool. Coroutines know nothing about this behind-the-scenes work: they ask for a resource and get a working one. Health management is expressed declaratively, in the constructor, instead of smeared across the code as a pile of `if`s. ## Circuit breaker Now imagine `GeoDirectory` goes down entirely. All ten connections are dead, every coroutine dutifully waits out a timeout, creates a new connection, waits again. The application spends all its energy bombarding a service that's already down, and in doing so keeps it from getting back up. Electrical engineering invented a fix for exactly this: a circuit breaker that opens the circuit until the fault is cleared. The pattern is named after it, circuit breaker, and it's built into the pool. The pool has three states: `ACTIVE`, everything works; `INACTIVE`, the service has been declared unavailable and `acquire` fails immediately, without any timeouts; `RECOVERING`, a cautious check on whether the service has come back to life. You can switch states by hand, or hand the decision off to a strategy: ```php use Async\CircuitBreakerStrategy; final class FiveStrikes implements CircuitBreakerStrategy { private int $failures = 0; public function reportSuccess(mixed $pool): void { $this->failures = 0; $pool->activate(); } public function reportFailure(mixed $pool, Throwable $error): void { if (++$this->failures >= 5) { $pool->deactivate(); } } public function shouldRecover(): bool { return true; // attempt recovery at the first opportunity } } $geo->setCircuitBreakerStrategy(new FiveStrikes()); ``` The pool itself reports every success and every failure of a resource to the strategy, and asks through `shouldRecover` whether it's time to cautiously check if the service is back. Five failures in a row, and the circuit opens: coroutines get a fast failure instead of a slow torture of timeouts, and `GeoDirectory` stops taking pointless hits and gets to recover in peace. Looking back, `Pool` does exactly what we hand-built out of a channel in chapter nine, plus everything you really don't want to hand-build: guaranteed return, health checks, a circuit breaker. `PDO Pool` is the same primitive, just tucked behind the `PDO` facade. And for everything else, HTTP clients, Redis, sockets, and heavy objects in general, there's `Async\Pool`. By this point our arsenal looks impressive: coroutines, channels, scopes, task groups, pools. But all of it runs in a single operating system thread and shares a single CPU core. As long as tasks are waiting on I/O, that's nobody's problem. But what if the work isn't bound by waiting but by computation? There the whole picture changes, and that's what the next chapter is about. --- --- url: https://true-async.github.io/en/docs/reference/pool/construct.md description: Create a new resource pool. --- # Pool::\_\_construct (PHP 8.6+, True Async 1.0) ```php public Pool::__construct( callable $factory, ?callable $destructor = null, ?callable $healthcheck = null, ?callable $beforeAcquire = null, ?callable $beforeRelease = null, int $min = 0, int $max = 10, int $healthcheckInterval = 0 ) ``` Creates a new resource pool. The pool manages a set of reusable objects (connections, clients, file descriptors, etc.), automatically creating and destroying them as needed. ## Parameters **factory** : A factory function for creating a new resource. Called each time the pool needs a new resource and the current count is less than `max`. Must return a ready-to-use resource. **destructor** : A function for properly destroying a resource. Called when the pool is closed or when a resource is removed (e.g., after a failed health check). `null` --- the resource is simply removed from the pool without additional actions. **healthcheck** : A resource health check function. Takes a resource, returns `bool`. `true` --- the resource is healthy, `false` --- the resource will be destroyed and replaced. `null` --- no health check is performed. **beforeAcquire** : A hook called before a resource is handed out. Takes the resource. Can be used to prepare the resource (e.g., reset state). `null` --- no hook. **beforeRelease** : A hook called before a resource is returned to the pool. Takes the resource, returns `bool`. If it returns `false`, the resource is destroyed instead of being returned to the pool. `null` --- no hook. **min** : The minimum number of resources in the pool. When the pool is created, `min` resources are created immediately. Default is `0`. **max** : The maximum number of resources in the pool. When the limit is reached, `acquire()` calls block until a resource is released. Default is `10`. **healthcheckInterval** : The interval for background resource health checks in milliseconds. `0` --- background checking is disabled (check only on acquire). ## Examples ### Example #1 PDO connection pool ```php PDO::ERRMODE_EXCEPTION, ]); }, destructor: function(PDO $pdo): void { // PDO is closed automatically when removed }, healthcheck: function(PDO $pdo): bool { try { $pdo->query('SELECT 1'); return true; } catch (\Throwable) { return false; } }, min: 2, max: 20, healthcheckInterval: 30000 // check every 30 seconds ); $conn = $pool->acquire(); $result = $conn->query('SELECT * FROM users'); $pool->release($conn); ``` ### Example #2 Pool with hooks ```php new RedisClient('127.0.0.1', 6379), destructor: fn(RedisClient $r) => $r->close(), beforeAcquire: function(RedisClient $r): void { $r->select(0); // reset to default database }, beforeRelease: function(RedisClient $r): bool { // If the connection is broken — destroy the resource return $r->isConnected(); }, max: 5 ); ``` ## See Also * [Pool::acquire](/en/docs/reference/pool/acquire.html) --- Acquire a resource from the pool * [Pool::release](/en/docs/reference/pool/release.html) --- Release a resource back to the pool * [Pool::close](/en/docs/reference/pool/close.html) --- Close the pool --- --- url: https://true-async.github.io/en/docs/reference/pool/acquire.md description: Acquire a resource from the pool with waiting. --- # Pool::acquire (PHP 8.6+, True Async 1.0) ```php public Pool::acquire(int $timeout = 0): mixed ``` Acquires a resource from the pool. If no free resources are available and the maximum limit has been reached, the coroutine blocks until a resource becomes available. If the pool has a free resource, it is returned immediately. If there are no free resources but the `max` limit has not been reached, a new resource is created via `factory`. Otherwise, the call waits for a resource to be released. ## Parameters **timeout** : Maximum wait time in milliseconds. `0` --- wait indefinitely. If the timeout is exceeded, a `PoolException` is thrown. ## Return Value Returns a resource from the pool. ## Errors Throws `Async\PoolException` if: * The wait timeout is exceeded. * The pool is closed. ## Examples ### Example #1 Basic usage ```php new PDO('mysql:host=localhost;dbname=app', 'user', 'pass'), max: 5 ); // Get a connection (waits if necessary) $conn = $pool->acquire(); try { $stmt = $conn->prepare('SELECT * FROM users WHERE id = ?'); $stmt->execute([42]); $user = $stmt->fetch(); } finally { $pool->release($conn); } ``` ### Example #2 With timeout ```php new PDO('mysql:host=localhost;dbname=app', 'user', 'pass'), max: 2 ); try { $conn = $pool->acquire(timeout: 5000); // wait no more than 5 seconds // work with connection... $pool->release($conn); } catch (PoolException $e) { echo "Failed to acquire resource: {$e->getMessage()}\n"; } ``` ## See Also * [Pool::tryAcquire](/en/docs/reference/pool/try-acquire.html) --- Non-blocking resource acquisition * [Pool::release](/en/docs/reference/pool/release.html) --- Release a resource back to the pool * [Pool::\_\_construct](/en/docs/reference/pool/construct.html) --- Create a pool --- --- url: https://true-async.github.io/en/docs/reference/pool/activate.md description: Force the pool into the ACTIVE state. --- # Pool::activate (PHP 8.6+, True Async 1.0) ```php public Pool::activate(): void ``` Forcefully transitions the pool to the `ACTIVE` state. Resources become available for acquisition again. Used for manual Circuit Breaker management, for example, after confirming that the service has recovered. ## Parameters This method takes no parameters. ## Return Value No value is returned. ## Examples ### Example #1 Manual activation after verification ```php new HttpClient('https://api.example.com'), max: 5 ); // Suppose the pool was deactivated if ($pool->getState() === CircuitBreakerState::INACTIVE) { // Manually check service availability if (checkServiceHealth('https://api.example.com/health')) { $pool->activate(); echo "Pool activated\n"; } } ``` ### Example #2 Activation by external signal ```php activate(); echo "Service restored, pool activated\n"; } ``` ## See Also * [Pool::deactivate](/en/docs/reference/pool/deactivate.html) --- Transition to INACTIVE state * [Pool::recover](/en/docs/reference/pool/recover.html) --- Transition to RECOVERING state * [Pool::getState](/en/docs/reference/pool/get-state.html) --- Current state --- --- url: https://true-async.github.io/en/docs/reference/pool/active-count.md description: Number of active resources in the pool. --- # Pool::activeCount (PHP 8.6+, True Async 1.0) ```php public Pool::activeCount(): int ``` Returns the number of resources that are currently in use (acquired via `acquire()` or `tryAcquire()` and not yet returned via `release()`). ## Parameters This method takes no parameters. ## Return Value The number of active resources. ## Examples ### Example #1 Counting active resources ```php new \stdClass(), max: 5 ); echo $pool->activeCount() . "\n"; // 0 $r1 = $pool->acquire(); $r2 = $pool->acquire(); echo $pool->activeCount() . "\n"; // 2 $pool->release($r1); echo $pool->activeCount() . "\n"; // 1 ``` ### Example #2 Displaying pool statistics ```php count(), $pool->activeCount(), $pool->idleCount() ); } ``` ## See Also * [Pool::idleCount](/en/docs/reference/pool/idle-count.html) --- Number of idle resources * [Pool::count](/en/docs/reference/pool/count.html) --- Total number of resources --- --- url: https://true-async.github.io/en/docs/reference/pool/close.md description: Close the pool and destroy all resources. --- # Pool::close (PHP 8.6+, True Async 1.0) ```php public Pool::close(): void ``` Closes the resource pool. All idle resources are destroyed via the `destructor` (if one was provided). All coroutines waiting for a resource via `acquire()` receive a `PoolException`. After closing, any calls to `acquire()` and `tryAcquire()` throw an exception. ## Parameters This method takes no parameters. ## Return Value No value is returned. ## Examples ### Example #1 Graceful shutdown ```php new PDO('mysql:host=localhost;dbname=app', 'user', 'pass'), destructor: function(PDO $pdo): void { // Close all prepared statements and connection }, min: 2, max: 10 ); // ... work with the pool ... // Close the pool when the application shuts down $pool->close(); ``` ### Example #2 Waiting coroutines receive an exception ```php new \stdClass(), max: 1 ); $resource = $pool->acquire(); // took the only resource spawn(function() use ($pool) { try { $pool->acquire(); // waiting for release } catch (PoolException $e) { echo "Pool closed: {$e->getMessage()}\n"; } }); $pool->close(); // waiting coroutine will receive PoolException ``` ## See Also * [Pool::isClosed](/en/docs/reference/pool/is-closed.html) --- Check if the pool is closed * [Pool::\_\_construct](/en/docs/reference/pool/construct.html) --- Create a pool --- --- url: https://true-async.github.io/en/docs/reference/pool/count.md description: Total number of resources in the pool. --- # Pool::count (PHP 8.6+, True Async 1.0) ```php public Pool::count(): int ``` Returns the total number of resources in the pool, including both idle and active (in-use) resources. ## Parameters This method takes no parameters. ## Return Value The total number of resources in the pool. ## Examples ### Example #1 Monitoring the pool ```php new PDO('mysql:host=localhost;dbname=app', 'user', 'pass'), min: 2, max: 10 ); echo "Total resources: " . $pool->count() . "\n"; // 2 (min) echo "Idle: " . $pool->idleCount() . "\n"; // 2 echo "Active: " . $pool->activeCount() . "\n"; // 0 $conn1 = $pool->acquire(); $conn2 = $pool->acquire(); $conn3 = $pool->acquire(); // a new resource is created echo "Total resources: " . $pool->count() . "\n"; // 3 echo "Idle: " . $pool->idleCount() . "\n"; // 0 echo "Active: " . $pool->activeCount() . "\n"; // 3 ``` ## See Also * [Pool::idleCount](/en/docs/reference/pool/idle-count.html) --- Number of idle resources * [Pool::activeCount](/en/docs/reference/pool/active-count.html) --- Number of active resources --- --- url: https://true-async.github.io/en/docs/reference/pool/deactivate.md description: Force the pool into the INACTIVE state. --- # Pool::deactivate (PHP 8.6+, True Async 1.0) ```php public Pool::deactivate(): void ``` Forcefully transitions the pool to the `INACTIVE` state. In this state, the pool rejects all resource acquisition requests. Used for manual deactivation when problems with an external service are detected. Unlike `close()`, deactivation is reversible --- the pool can be returned to a working state via `activate()` or `recover()`. ## Parameters This method takes no parameters. ## Return Value No value is returned. ## Examples ### Example #1 Deactivation upon detecting a problem ```php new HttpClient('https://api.example.com'), max: 10 ); // Upon detecting a critical error try { $client = $pool->acquire(); $response = $client->get('/critical-endpoint'); $pool->release($client); } catch (ServiceUnavailableException $e) { $pool->deactivate(); echo "Service unavailable, pool deactivated\n"; } ``` ### Example #2 Planned maintenance ```php deactivate(); echo "Pool deactivated for maintenance\n"; } function endMaintenance(Pool $pool): void { $pool->activate(); echo "Maintenance complete, pool activated\n"; } ``` ## See Also * [Pool::activate](/en/docs/reference/pool/activate.html) --- Transition to ACTIVE state * [Pool::recover](/en/docs/reference/pool/recover.html) --- Transition to RECOVERING state * [Pool::getState](/en/docs/reference/pool/get-state.html) --- Current state * [Pool::close](/en/docs/reference/pool/close.html) --- Permanent pool closure (irreversible) --- --- url: https://true-async.github.io/en/docs/reference/pool/get-state.md description: Get the current Circuit Breaker state. --- # Pool::getState (PHP 8.6+, True Async 1.0) ```php public Pool::getState(): CircuitBreakerState ``` Returns the current Circuit Breaker state of the pool. ## Parameters This method takes no parameters. ## Return Value A `CircuitBreakerState` enum value: * `CircuitBreakerState::ACTIVE` --- the pool is operating normally, resources are being issued. * `CircuitBreakerState::INACTIVE` --- the pool is deactivated, requests are rejected. * `CircuitBreakerState::RECOVERING` --- the pool is in recovery mode, allowing a limited number of requests to check service availability. ## Examples ### Example #1 Checking pool state ```php new HttpClient('https://api.example.com'), max: 10 ); $state = $pool->getState(); match ($state) { CircuitBreakerState::ACTIVE => echo "Pool is active\n", CircuitBreakerState::INACTIVE => echo "Service unavailable\n", CircuitBreakerState::RECOVERING => echo "Recovering...\n", }; ``` ### Example #2 Conditional logic based on state ```php getState() === CircuitBreakerState::INACTIVE) { // Use cached data instead of calling the service return getCachedResponse($endpoint); } $client = $pool->acquire(timeout: 3000); try { return $client->get($endpoint); } finally { $pool->release($client); } } ``` ## See Also * [Pool::setCircuitBreakerStrategy](/en/docs/reference/pool/set-circuit-breaker-strategy.html) --- Set the strategy * [Pool::activate](/en/docs/reference/pool/activate.html) --- Force activation * [Pool::deactivate](/en/docs/reference/pool/deactivate.html) --- Force deactivation * [Pool::recover](/en/docs/reference/pool/recover.html) --- Transition to recovery mode --- --- url: https://true-async.github.io/en/docs/reference/pool/idle-count.md description: Number of idle resources in the pool. --- # Pool::idleCount (PHP 8.6+, True Async 1.0) ```php public Pool::idleCount(): int ``` Returns the number of idle (unused) resources that are ready to be acquired. ## Parameters This method takes no parameters. ## Return Value The number of idle resources in the pool. ## Examples ### Example #1 Tracking idle resources ```php new PDO('mysql:host=localhost;dbname=app', 'user', 'pass'), min: 3, max: 10 ); echo $pool->idleCount() . "\n"; // 3 $conn = $pool->acquire(); echo $pool->idleCount() . "\n"; // 2 $pool->release($conn); echo $pool->idleCount() . "\n"; // 3 ``` ### Example #2 Adaptive strategy ```php createExpensiveResource(), min: 1, max: 20 ); // If few idle resources remain — reduce load if ($pool->idleCount() < 2 && $pool->count() >= 18) { echo "Warning: pool is nearly exhausted\n"; } ``` ## See Also * [Pool::activeCount](/en/docs/reference/pool/active-count.html) --- Number of active resources * [Pool::count](/en/docs/reference/pool/count.html) --- Total number of resources --- --- url: https://true-async.github.io/en/docs/reference/pool/is-closed.md description: Check if the pool is closed. --- # Pool::isClosed (PHP 8.6+, True Async 1.0) ```php public Pool::isClosed(): bool ``` Checks whether the pool has been closed by a `close()` call. ## Parameters This method takes no parameters. ## Return Value Returns `true` if the pool is closed, `false` if the pool is active. ## Examples ### Example #1 Checking pool state ```php new \stdClass(), max: 5 ); var_dump($pool->isClosed()); // bool(false) $pool->close(); var_dump($pool->isClosed()); // bool(true) ``` ### Example #2 Conditional pool usage ```php isClosed()) { throw new \RuntimeException('Connection pool is closed'); } $conn = $pool->acquire(); try { return $conn->query($sql)->fetchAll(); } finally { $pool->release($conn); } } ``` ## See Also * [Pool::close](/en/docs/reference/pool/close.html) --- Close the pool * [Pool::getState](/en/docs/reference/pool/get-state.html) --- Circuit Breaker state --- --- url: https://true-async.github.io/en/docs/reference/pool/recover.md description: Transition the pool to the RECOVERING state. --- # Pool::recover (PHP 8.6+, True Async 1.0) ```php public Pool::recover(): void ``` Transitions the pool to the `RECOVERING` state. In this state, the pool allows a limited number of requests through to check service availability. If requests succeed, the Circuit Breaker automatically transitions the pool to the `ACTIVE` state. If requests continue to fail, the pool returns to `INACTIVE`. ## Parameters This method takes no parameters. ## Return Value No value is returned. ## Examples ### Example #1 Recovery attempt ```php new HttpClient('https://api.example.com'), max: 10 ); // Pool is deactivated, try to recover if ($pool->getState() === CircuitBreakerState::INACTIVE) { $pool->recover(); echo "Pool transitioned to recovery mode\n"; // Circuit Breaker will allow probe requests through } ``` ### Example #2 Periodic recovery attempts ```php isClosed()) { if ($pool->getState() === CircuitBreakerState::INACTIVE) { $pool->recover(); } suspend(delay: 10000); // check every 10 seconds } }); ``` ## See Also * [Pool::activate](/en/docs/reference/pool/activate.html) --- Force activation * [Pool::deactivate](/en/docs/reference/pool/deactivate.html) --- Force deactivation * [Pool::getState](/en/docs/reference/pool/get-state.html) --- Current state * [Pool::setCircuitBreakerStrategy](/en/docs/reference/pool/set-circuit-breaker-strategy.html) --- Configure strategy --- --- url: https://true-async.github.io/en/docs/reference/pool/release.md description: Release a resource back to the pool. --- # Pool::release (PHP 8.6+, True Async 1.0) ```php public Pool::release(mixed $resource): void ``` Returns a previously acquired resource back to the pool. If a `beforeRelease` hook was set when creating the pool, it is called before the return. If the hook returns `false`, the resource is destroyed instead of being returned to the pool. If there are coroutines waiting for a resource via `acquire()`, the resource is immediately handed to the first waiting coroutine. ## Parameters **resource** : A resource previously acquired via `acquire()` or `tryAcquire()`. ## Return Value No value is returned. ## Examples ### Example #1 Safe return via finally ```php new PDO('mysql:host=localhost;dbname=app', 'user', 'pass'), max: 10 ); $conn = $pool->acquire(); try { $conn->beginTransaction(); $conn->exec("INSERT INTO logs (message) VALUES ('event')"); $conn->commit(); } catch (\Throwable $e) { $conn->rollBack(); throw $e; } finally { $pool->release($conn); } ``` ### Example #2 Automatic destruction via beforeRelease ```php new TcpClient('api.example.com', 443), destructor: fn(TcpClient $c) => $c->disconnect(), beforeRelease: function(TcpClient $client): bool { // If the connection is broken — do not return to the pool return $client->isAlive(); }, max: 5 ); $client = $pool->acquire(); try { $client->send('PING'); } finally { // If isAlive() returns false, the client will be destroyed $pool->release($client); } ``` ## See Also * [Pool::acquire](/en/docs/reference/pool/acquire.html) --- Acquire a resource from the pool * [Pool::tryAcquire](/en/docs/reference/pool/try-acquire.html) --- Non-blocking acquisition * [Pool::close](/en/docs/reference/pool/close.html) --- Close the pool --- --- url: >- https://true-async.github.io/en/docs/reference/pool/set-circuit-breaker-strategy.md description: Set the Circuit Breaker strategy for the pool. --- # Pool::setCircuitBreakerStrategy (PHP 8.6+, True Async 1.0) ```php public Pool::setCircuitBreakerStrategy(?CircuitBreakerStrategy $strategy): void ``` Sets the Circuit Breaker strategy for the pool. The Circuit Breaker monitors the availability of an external service: upon detecting multiple failures, the pool automatically transitions to the `INACTIVE` state, preventing a cascade of errors. When the service recovers, the pool returns to an active state. ## Parameters **strategy** : A `CircuitBreakerStrategy` object defining the rules for transitioning between states. `null` --- disable Circuit Breaker. ## Return Value No value is returned. ## Examples ### Example #1 Setting a strategy ```php new HttpClient('https://api.example.com'), destructor: fn(HttpClient $c) => $c->close(), max: 10 ); $strategy = new CircuitBreakerStrategy( failureThreshold: 5, // after 5 errors — deactivate recoveryTimeout: 30000, // after 30 seconds — attempt recovery successThreshold: 3 // 3 successful requests — full activation ); $pool->setCircuitBreakerStrategy($strategy); ``` ### Example #2 Disabling Circuit Breaker ```php setCircuitBreakerStrategy(null); ``` ## See Also * [Pool::getState](/en/docs/reference/pool/get-state.html) --- Current Circuit Breaker state * [Pool::activate](/en/docs/reference/pool/activate.html) --- Force activation * [Pool::deactivate](/en/docs/reference/pool/deactivate.html) --- Force deactivation * [Pool::recover](/en/docs/reference/pool/recover.html) --- Transition to recovery mode --- --- url: https://true-async.github.io/en/docs/reference/pool/try-acquire.md description: Non-blocking resource acquisition from the pool. --- # Pool::tryAcquire (PHP 8.6+, True Async 1.0) ```php public Pool::tryAcquire(): mixed ``` Attempts to acquire a resource from the pool without blocking. If a free resource is available or the `max` limit has not been reached, returns the resource immediately. Otherwise, returns `null`. ## Parameters This method takes no parameters. ## Return Value Returns a resource from the pool or `null` if no free resources are available and the maximum limit has been reached. ## Examples ### Example #1 Attempting to acquire a resource ```php new PDO('mysql:host=localhost;dbname=app', 'user', 'pass'), max: 5 ); $conn = $pool->tryAcquire(); if ($conn === null) { echo "All connections are busy, try again later\n"; } else { try { $result = $conn->query('SELECT COUNT(*) FROM orders'); echo "Orders: " . $result->fetchColumn() . "\n"; } finally { $pool->release($conn); } } ``` ### Example #2 Fallback when pool is unavailable ```php new CacheClient('127.0.0.1', 11211), max: 3 ); function getData(Pool $pool, string $key): mixed { $client = $pool->tryAcquire(); if ($client === null) { // Cache unavailable — query database directly return fetchFromDatabase($key); } try { return $client->get($key) ?? fetchFromDatabase($key); } finally { $pool->release($client); } } ``` ## See Also * [Pool::acquire](/en/docs/reference/pool/acquire.html) --- Blocking resource acquisition * [Pool::release](/en/docs/reference/pool/release.html) --- Release a resource back to the pool --- --- url: https://true-async.github.io/en/tutors-server/09-production.md description: >- Timeouts, limits, backpressure on accept, compression, logs, and graceful shutdown. --- # Production A good server differs from a demo not in how it works. In how it fails. Everyone works when the mood is right; production begins where the bad day arrives: a traffic spike, slow clients, a deploy at the most inopportune moment. This chapter is entirely about bad days. ## Timeouts: Nobody Waits Forever From the first series we took an iron rule: any wait must have a limit. Let's see where a connection even has waits. The client sends a request, that's one. Takes the response, that's two. Hangs between requests on keep-alive, that's three. Each stage gets its own limit: ```php $config ->setReadTimeout(30) // request takes longer than 30 seconds to send? goodbye ->setWriteTimeout(60) // and the response takes longer than a minute to fetch? that too ->setKeepAliveTimeout(15); // an idle connection lives at most 15 seconds ``` The read timeout isn't only about slow internet. There's an attack with a wonderful name, slowloris: the client sends a request one byte a minute and occupies the connection forever. A hundred such clients and a server without timeouts is done. With a timeout it's just a hundred dropped connections. The one legitimate exception you already know from the SSE chapter: for streams the write timeout is turned off, `setWriteTimeout(0)`. ## Limits: Refuse Cheaply Now about overload. The question isn't "if," the question is "when." And at that moment the server has exactly two paths. Path one: accept everyone, pile up a queue, slow down, and in the end everyone gets timeouts, including those who could have been served. Path two: tell the extras "busy" right away, and serve the rest as if nothing happened. The second path is configured like this: ```php $config ->setMaxConnections(10_000) // hard connection limit ->setMaxInflightRequests(1_000) // requests processed concurrently ->setMaxBodySize(10 * 1024 * 1024); // body larger than 10 MiB? 413 ``` Look closely at `setMaxInflightRequests`. Recognize it? It's our old friend, the concurrency limit: `POOL_MAX`, `concurrency` on groups, now at the level of a whole server. An excess request gets an instant `503 Retry-After: 1`. Instant is the key word: the client learns the truth in a millisecond and will retry later, instead of hanging in a queue for a minute waiting for a timeout. There's a third line of defense too, the most elegant. The server itself measures how long requests wait in the queue. Is the wait systematically growing? Then we're not keeping up, and the server temporarily stops accepting new connections until it catches up. The algorithm is called CoDel, but you know it under a different name: it's the backpressure from the channels chapter, applied to `accept()`. The internet is the producer, the handlers are the consumer, and the producer is slowed down. There's one setting: ```php $config->setBackpressureTargetMs(20); // what wait time in the queue to consider normal ``` ## Compression Good news: response compression already works. The server negotiates with the client via `Accept-Encoding` and picks the best of the available codecs: zstd, brotli, gzip. Usually there's nothing to do here, but three levers are worth knowing: ```php $config ->setCompressionMinSize(1024) // don't compress small stuff: the overhead costs more ->setZstdLevel(3) // speed/ratio balance per codec ->setCompressionMimeTypes([...]); // whitelist: text is compressed, jpeg already isn't ``` Plus one lever on the response side: `$res->setNoCompression()` for endpoints where compression is harmful. The SSE helpers, by the way, call it themselves; a buffering gzip would kill all the instantaneity of delivery. ## Logs While all is well, the server is silent. Agreed, we'd rather hear about trouble not from the users: ```php use TrueAsync\LogSeverity; $config ->setLogSeverity(LogSeverity::WARN) ->setLogStream(STDERR); ``` `WARN` covers failed TLS handshakes, dropped clients, swallowed exceptions. `INFO` adds the lifecycle: start, ports, workers. The sink is an ordinary PHP stream: stderr for systemd and Docker, a file, whatever. ## A Graceful Stop And the last bad day, which is actually a good one: the deploy. A picture worthy of oils: the server has a thousand live connections, a hundred of them mid-write to the database, and here comes the new version. `kill -9`? Enjoy a hundred aborted transactions and a crowd of users with broken uploads. The graceful version looks like this: ```php $config->setShutdownTimeout(30); ``` On receiving `stop()` or SIGTERM, the server first stops accepting new ones, and lets the current ones live out their lives, up to thirty seconds. Whoever didn't make it will be cancelled. And here, for the last time in this series, scope discipline pays off: cancellation cooperatively passes through the tree of each request, the `finally` blocks release resources, the pool rolls back unclosed transactions. Graceful shutdown wasn't written as a feature. It fell out of the architecture. For long-lived connections behind a load balancer there's one more touch, `setMaxConnectionAgeMs()`: a connection older than the given age is gracefully closed (`Connection: close`, GOAWAY), so that clients redistribute across machines instead of hanging on one for years. Now this is production. Overload meets a fast refusal, slow clients run into timeouts, a deploy doesn't tear transactions apart, and the server reports its problems itself. One last frontier remains for the whole series: so far only browsers and curl have talked to our server. It's time to let in other interlocutors, the neighboring services, in their native tongue. --- --- url: https://true-async.github.io/en/docs/reference/protect.md description: >- protect() — execute code in a non-cancellable mode to protect critical sections. --- # protect (PHP 8.6+, True Async 1.0) `protect()` — Executes a closure in a non-cancellable mode. Coroutine cancellation is deferred until the closure completes. ## Description ```php protect(\Closure $closure): mixed ``` While `$closure` is executing, the coroutine is marked as protected. If a cancellation request arrives during this time, `AsyncCancellation` will be thrown only **after** the closure finishes. ## Parameters **`closure`** A closure to execute without interruption by cancellation. ## Return Values Returns the value returned by the closure. ## Examples ### Example #1 Protecting a transaction ```php beginTransaction(); $result = protect(function() use ($db) { $db->exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1"); $db->exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2"); $db->commit(); return true; }); // If the coroutine was cancelled during protect(), // AsyncCancellation will be thrown here — after commit() ?> ``` ### Example #2 Protecting file writes ```php ``` ### Example #3 Getting a result ```php set($key, $value); return $value; }); ?> ``` ### Example #4 Deferred cancellation and diagnostics During `protect()`, cancellation is saved but not applied. This can be checked via coroutine methods: ```php isCancellationRequested() ? "true" : "false"; // true echo "\n"; echo $me->isCancelled() ? "true" : "false"; // false echo "\n"; suspend(); echo "Protected operation completed\n"; }); // AsyncCancellation is thrown here — after protect() echo "This code will not execute\n"; }); suspend(); // Let the coroutine enter protect() $coroutine->cancel(); suspend(); // Let protect() finish echo $coroutine->isCancelled() ? "true" : "false"; // true ?> ``` * `isCancellationRequested()` — `true` immediately after `cancel()`, even inside `protect()` * `isCancelled()` — `false` while `protect()` is running, then `true` ## Notes > **Note:** If cancellation occurred during `protect()`, `AsyncCancellation` will be thrown immediately after the closure returns — the return value of `protect()` will be lost in this case. > **Note:** `protect()` does not make the closure atomic — other coroutines can execute during I/O operations inside it. `protect()` only guarantees that **cancellation** will not interrupt execution. ## See Also * [Cancellation](/en/docs/components/cancellation.html) — cooperative cancellation mechanism * [suspend()](/en/docs/reference/suspend.html) — suspending a coroutine --- --- url: https://true-async.github.io/en/docs/evidence/python-evidence.md description: >- Python asyncio in practice: Duolingo, Super.com, Instagram, uvloop benchmarks, counter-arguments. --- # Python asyncio in Practice: Real-World Measurements Python is the language most similar to PHP in terms of execution model: interpreted, single-threaded (GIL), with a dominance of synchronous frameworks. The transition from synchronous Python (Flask, Django + Gunicorn) to asynchronous (FastAPI, aiohttp, Starlette + Uvicorn) is a precise analogy to the transition from PHP-FPM to a coroutine-based runtime. Below is a collection of production cases, independent benchmarks, and measurements. *** ## 1. Production: Duolingo — Migration to Async Python (+40% Throughput) [Duolingo](https://blog.duolingo.com/async-python-migration/) is the largest language learning platform (500M+ users). The backend is written in Python. In 2025, the team began a systematic migration of services from synchronous Python to async. | Metric | Result | |-----------------------|-----------------------------------------| | Throughput per instance | **+40%** | | AWS EC2 cost savings | **~30%** per migrated service | The authors note that after building the async infrastructure, migrating individual services turned out to be "fairly straightforward." **Source:** [How We Started Our Async Python Migration (Duolingo Blog, 2025)](https://blog.duolingo.com/async-python-migration/) *** ## 2. Production: Super.com — 90% Cost Reduction [Super.com](https://www.super.com/) (formerly Snaptravel) is a hotel search and discount service. Their search engine handles 1,000+ req/s, ingests 1 TB+ of data per day, and processes $1M+ in sales daily. **Key workload characteristic:** each request makes **40+ network calls** to third-party APIs. This is a pure I/O-bound profile — an ideal candidate for coroutines. The team migrated from Flask (synchronous, AWS Lambda) to Quart (ASGI, EC2). | Metric | Flask (Lambda) | Quart (ASGI) | Change | |--------------------------|----------------|---------------|----------------| | Infrastructure costs | ~$1,000/day | ~$50/day | **−90%** | | Throughput | ~150 req/s | 300+ req/s | **2x** | | Errors during peak hours | Baseline | −95% | **−95%** | | Latency | Baseline | −50% | **2x faster** | Savings of $950/day × 365 = **~$350,000/year** on a single service. **Source:** [How we optimized service performance using Quart ASGI and reduced costs by 90% (Super.com, Medium)](https://medium.com/super/how-we-optimized-service-performance-using-the-python-quart-asgi-framework-and-reduced-costs-by-1362dc365a0) *** ## 3. Production: Instagram — asyncio at 500M DAU Scale Instagram serves 500+ million daily active users on a Django backend. Jimmy Lai (Instagram engineer) described the migration to asyncio in a talk at PyCon Taiwan 2018: * Replaced `requests` with `aiohttp` for HTTP calls * Migrated internal RPC to `asyncio` * Achieved API performance improvement and reduced CPU idle time **Challenges:** High CPU overhead of asyncio at Instagram's scale, the need for automated detection of blocking calls through static code analysis. **Source:** [The journey of asyncio adoption in Instagram (PyCon Taiwan 2018)](https://www.slideshare.net/jimmy_lai/the-journey-of-asyncio-adoption-in-instagram) *** ## 4. Production: Feature Store — From Threads to asyncio (−40% Latency) The Feature Store service migrated from Python multithreading to asyncio. | Metric | Threads | Asyncio | Change | |-----------------|-------------------------|----------------------|-------------------------| | Latency | Baseline | −40% | **−40%** | | RAM consumption | 18 GB (hundreds of threads) | Significantly less | Substantial reduction | The migration was carried out in three phases with 50/50 production traffic splitting for validation. **Source:** [How We Migrated from Python Multithreading to Asyncio (Medium)](https://medium.com/@DorIndivo/how-we-migrated-from-python-multithreading-to-asyncio-128b0c8e4ec5) *** ## 5. Production: Talk Python — Flask to Quart (−81% Latency) [Talk Python](https://talkpython.fm/) is one of the largest Python podcasts and learning platforms. The author (Michael Kennedy) rewrote the site from Flask (synchronous) to Quart (asynchronous Flask). | Metric | Flask | Quart | Change | |-----------------------|-------|-------|-------------| | Response time (example) | 42 ms | 8 ms | **−81%** | | Bugs after migration | — | 2 | Minimal | The author notes: during load testing, the maximum req/s differed insignificantly because MongoDB queries took <1 ms. The gain appears during **concurrent** request processing — when multiple clients access the server simultaneously. **Source:** [Talk Python rewritten in Quart (async Flask)](https://talkpython.fm/blog/posts/talk-python-rewritten-in-quart-async-flask/) *** ## 6. Microsoft Azure Functions — uvloop as Standard Microsoft included [uvloop](https://github.com/MagicStack/uvloop) — a fast event loop based on libuv — as the default for Azure Functions on Python 3.13+. | Test | Standard asyncio | uvloop | Improvement | |--------------------------------|------------------|-------------|-------------| | 10K requests, 50 VU (local) | 515 req/s | 565 req/s | **+10%** | | 5 min, 100 VU (Azure) | 1,898 req/s | 1,961 req/s | **+3%** | | 500 VU (local) | 720 req/s | 772 req/s | **+7%** | The standard event loop at 500 VU showed **~2% request losses**. uvloop — zero errors. **Source:** [Faster Python on Azure Functions with uvloop (Microsoft, 2025)](https://techcommunity.microsoft.com/blog/appsonazureblog/faster-python-on-azure-functions-with-uvloop/4455323) *** ## 7. Benchmark: I/O-bound Tasks — asyncio 130x Faster Direct comparison of concurrency models on a task of downloading 10,000 URLs: | Model | Time | Throughput | Errors | |--------------|----------|----------------|-----------| | Synchronous | ~1,800 s | ~11 KB/s | — | | Threads (100)| ~85 s | ~238 KB/s | Low | | **Asyncio** | **14 s** | **1,435 KB/s** | **0.06%** | Asyncio: **130x faster** than synchronous code, **6x faster** than threads. For CPU-bound tasks, asyncio provides no advantage (identical time, +44% memory consumption). **Source:** [Python Concurrency Model Comparison (Medium, 2025)](https://medium.com/@romualdoluwatobi/python-concurrency-model-comparison-for-cpu-and-io-bound-execution-asyncio-vs-threads-vs-sync-35c114fc0045) *** ## 8. Benchmark: uvloop — Faster Than Go and Node.js [uvloop](https://github.com/MagicStack/uvloop) is a drop-in replacement for the standard asyncio event loop, written in Cython on top of libuv (the same library underlying Node.js). TCP echo server: | Implementation | 1 KiB (req/s) | 100 KiB throughput | |---------------------|---------------|--------------------| | **uvloop** | **105,459** | **2.3 GiB/s** | | Go | 103,264 | — | | Standard asyncio | 41,420 | — | | Node.js | 44,055 | — | HTTP server (300 concurrent): | Implementation | 1 KiB (req/s) | |------------------------|---------------| | **uvloop + httptools** | **37,866** | | Node.js | Lower | uvloop: **2.5x faster** than standard asyncio, **2x faster** than Node.js, **on par with Go**. **Source:** [uvloop: Blazing fast Python networking (MagicStack)](https://magic.io/blog/uvloop-blazing-fast-python-networking/) *** ## 9. Benchmark: aiohttp vs requests — 10x on Concurrent Requests | Library | req/s (concurrent) | Type | |---------------|---------------------|-------| | **aiohttp** | **241+** | Async | | HTTPX (async) | ~160 | Async | | Requests | ~24 | Sync | aiohttp: **10x faster** than Requests for concurrent HTTP requests. **Source:** [HTTPX vs Requests vs AIOHTTP (Oxylabs)](https://oxylabs.io/blog/httpx-vs-requests-vs-aiohttp) *** ## 10. Counter-argument: Cal Paterson — "Async Python Is Not Faster" It is important to present counter-arguments as well. Cal Paterson conducted a thorough benchmark with a **real database** (PostgreSQL, random row selection + JSON): | Framework | Type | req/s | P99 Latency | |------------------------------|-------|-----------|-------------| | Gunicorn + Meinheld/Bottle | Sync | **5,780** | **32 ms** | | Gunicorn + Meinheld/Falcon | Sync | **5,589** | **31 ms** | | Uvicorn + Starlette | Async | 4,952 | 75 ms | | Sanic | Async | 4,687 | 85 ms | | AIOHTTP | Async | 4,501 | 76 ms | **Result:** synchronous frameworks with C servers showed **higher throughput** and **2–3x better tail latency** (P99). ### Why Did Async Lose? Reasons: 1. **A single SQL query** per HTTP request — too little I/O for coroutine concurrency to have an effect. 2. **Cooperative multitasking** with CPU work between requests creates "unfair" CPU time distribution — long computations block the event loop for everyone. 3. **asyncio overhead** (standard event loop in Python) is comparable to the gain from non-blocking I/O when I/O is minimal. ### When Async Actually Helps Paterson's benchmark tests the **simplest scenario** (1 SQL query). As the production cases above demonstrate, async provides a dramatic gain when: * There are **many** DB / external API queries (Super.com: 40+ calls per request) * Concurrency is **high** (thousands of simultaneous connections) * I/O **dominates** over CPU (Duolingo, Appwrite) This aligns with theory: the higher the blocking coefficient (T\_io/T\_cpu), the greater the benefit from coroutines. With 1 SQL query × 2 ms, the coefficient is too low. **Source:** [Async Python is not faster (Cal Paterson)](https://calpaterson.com/async-python-is-not-faster.html) *** ## 11. TechEmpower: Python Frameworks Approximate results from [TechEmpower Round 22](https://www.techempower.com/benchmarks/): | Framework | Type | req/s (JSON) | |-------------------|------------|-----------------------| | Uvicorn (raw) | Async ASGI | Highest among Python | | Starlette | Async ASGI | ~20,000–25,000 | | FastAPI | Async ASGI | ~15,000–22,000 | | Flask (Gunicorn) | Sync WSGI | ~4,000–6,000 | | Django (Gunicorn) | Sync WSGI | ~2,000–4,000 | Async frameworks: **3–5x** faster than synchronous ones in the JSON test. **Source:** [TechEmpower Framework Benchmarks](https://www.techempower.com/benchmarks/) *** ## Summary: What Python Data Shows | Case | Sync → Async | Condition | |----------------------------|----------------------------------------|----------------------------------------| | Duolingo (production) | **+40%** throughput, **−30%** cost | Microservices, I/O | | Super.com (production) | **2x** throughput, **−90%** cost | 40+ API calls per request | | Feature Store (production) | **−40%** latency | Migration from threads to asyncio | | Talk Python (production) | **−81%** latency | Flask → Quart | | I/O-bound (10K URLs) | **130x** faster | Pure I/O, massive concurrency | | aiohttp vs requests | **10x** faster | Concurrent HTTP requests | | uvloop vs standard | **2.5x** faster | TCP echo, HTTP | | TechEmpower JSON | **3–5x** | FastAPI/Starlette vs Flask/Django | | **Simple CRUD (1 SQL)** | **Sync is faster** | Cal Paterson: P99 2–3x worse for async | | **CPU-bound** | **No difference** | +44% memory, 0% gain | ### Key Takeaway Async Python provides maximum benefit with a **high blocking coefficient**: when I/O time significantly exceeds CPU time. With 40+ network calls (Super.com) — 90% cost savings. With 1 SQL query (Cal Paterson) — async is slower. This **confirms the formula** from [IO-bound Task Efficiency](/en/docs/evidence/concurrency-efficiency.html): gain ≈ 1 + T\_io/T\_cpu. When T\_io >> T\_cpu — tens to hundreds of times. When T\_io ≈ T\_cpu — minimal or zero. *** ## Connection to PHP and True Async Python and PHP are in a similar situation: | Characteristic | Python | PHP | |------------------------|----------------------|---------------------| | Interpreted | Yes | Yes | | GIL / single-threaded | GIL | Single-threaded | | Dominant model | Sync (Django, Flask) | Sync (FPM) | | Async runtime | asyncio + uvloop | Swoole / True Async | | Async framework | FastAPI, Starlette | Hyperf | Python data shows that transitioning to coroutines in a single-threaded interpreted language **works**. The scale of the gain is determined by the workload profile, not the language. *** ## References ### Production Cases * [Duolingo: How We Started Our Async Python Migration (2025)](https://blog.duolingo.com/async-python-migration/) * [Super.com: Quart ASGI, 90% cost reduction](https://medium.com/super/how-we-optimized-service-performance-using-the-python-quart-asgi-framework-and-reduced-costs-by-1362dc365a0) * [Instagram: asyncio adoption at scale (PyCon Taiwan 2018)](https://www.slideshare.net/jimmy_lai/the-journey-of-asyncio-adoption-in-instagram) * [Feature Store: Multithreading to Asyncio](https://medium.com/@DorIndivo/how-we-migrated-from-python-multithreading-to-asyncio-128b0c8e4ec5) * [Talk Python: Flask → Quart rewrite](https://talkpython.fm/blog/posts/talk-python-rewritten-in-quart-async-flask/) * [Microsoft Azure: uvloop as default (2025)](https://techcommunity.microsoft.com/blog/appsonazureblog/faster-python-on-azure-functions-with-uvloop/4455323) ### Benchmarks * [Cal Paterson: Async Python is not faster](https://calpaterson.com/async-python-is-not-faster.html) * [Python Concurrency Model Comparison (2025)](https://medium.com/@romualdoluwatobi/python-concurrency-model-comparison-for-cpu-and-io-bound-execution-asyncio-vs-threads-vs-sync-35c114fc0045) * [HTTPX vs Requests vs AIOHTTP (Oxylabs)](https://oxylabs.io/blog/httpx-vs-requests-vs-aiohttp) * [uvloop: Blazing fast Python networking (MagicStack)](https://magic.io/blog/uvloop-blazing-fast-python-networking/) * [TechEmpower Framework Benchmarks](https://www.techempower.com/benchmarks/) ### Coroutines vs Threads * [Super Fast Python: Coroutines Use Less Memory Than Threads](https://superfastpython.com/coroutines-less-memory-threads/) * [Super Fast Python: Asyncio Coroutines Faster Than Threads](https://superfastpython.com/asyncio-coroutines-faster-than-threads/) --- --- url: https://true-async.github.io/en/tutors-server/02-request-response.md description: 'HttpRequest and HttpResponse: routing, json(), and errors via HttpException.' --- # Request and Response The handler receives two objects. `HttpRequest` is everything the client sent, and it's read-only. `HttpResponse` is everything that goes back. Between them sits your code. Let's build a minimal profile `API` on this trio, and along the way see what these objects can do. ## Parsing the Request ```php use TrueAsync\HttpRequest; use TrueAsync\HttpResponse; $server->addHttpHandler(function (HttpRequest $req, HttpResponse $res) { $method = $req->getMethod(); // GET, POST, ... $path = $req->getPath(); // /profile/42, without query string if ($method === 'GET' && preg_match('#^/profile/(\d+)$#', $path, $m)) { showProfile((int) $m[1], $req, $res); return; } if ($method === 'POST' && preg_match('#^/profile/(\d+)/address$#', $path, $m)) { updateAddress((int) $m[1], $req, $res); return; } $res->setStatusCode(404)->setBody("Not found\n"); }); ``` Much of the request is already parsed and available in a convenient form: ```php // GET /profile/42?fields=name,address&debug=1 $req->getQuery(); // ['fields' => 'name,address', 'debug' => '1'] $req->getQueryParam('debug', '0'); // '1', with a default value // POST with a form: application/x-www-form-urlencoded or multipart $req->getPost(); // ['address' => ['city' => 'Berlin', ...]] // headers, case-insensitive $req->getHeader('authorization'); // 'Bearer eyJ...' or null // raw body, for example JSON $data = json_decode($req->getBody(), true); ``` `getQuery()` and `getPost()` understand the familiar PHP array notation: `address[city]`, `photos[]`. Moving over from the old `$_GET`/`$_POST` is almost word for word. ## Assembling the Response An `API` needs `JSON`, and the response has a ready-made helper for it: ```php function showProfile(int $userId, HttpRequest $req, HttpResponse $res): void { $profile = loadProfile($userId); $res->json($profile); // 200, Content-Type: application/json $res->json($errors, status: 422); // status as the second argument } ``` The other helpers follow the same spirit: ```php $res->html('

Profile updated

'); $res->redirect('/profile/42', 303); $res->setHeader('Cache-Control', 'no-store'); ``` Every method returns `$this`, so the response is assembled as a chain. You don't need to finish it explicitly: the handler returned, the response went out. ## Errors: HttpException Now `updateAddress`. The profile may not exist. The address may not be sent. Validation may fail. Do we write a `setStatusCode` with an early `return` for each case? We could. Or we could do this: ```php use TrueAsync\HttpException; function updateAddress(int $userId, HttpRequest $req, HttpResponse $res): void { if (!profileExists($userId)) { throw new HttpException('Profile not found', 404); } $address = $req->getPost()['address'] ?? null; if ($address === null) { throw new HttpException('Address is required', 422); } saveAddress($userId, $address); $res->json(['ok' => true]); } ``` The server understands `HttpException` without a translator: the exception code becomes the status, the message becomes the response body. No 500 with a stack trace leaking out. The class isn't final, so build your own hierarchy and forget about magic numbers: ```php final class NotFoundException extends HttpException { public function __construct(string $message = 'Not found') { parent::__construct($message, 404); } } ``` And here I'll ask for a minute of your attention, because what follows is my favorite detail of this chapter. Look at the class's ancestry: `HttpException extends Async\AsyncCancellation` That very same cancellation exception. From chapter two of the first series. A coincidence? No. Think about what the server should do when a client drops the connection mid-request: stop the handler coroutine. And how do we stop coroutines? Cooperative cancellation. An HTTP error and a coroutine cancellation turn out to be the same mechanism, just seen from different sides. From this kinship follows the old rule, familiar from the chapter on cancellation: if you catch `HttpException` by accident, together with `Throwable`, rethrow it. The server knows what to do with it. You don't have to. So the `API` responds by the rules and fails by the rules. But our handlers are still suspiciously simple: one check, one query, one response. What happens when they need to hit the database, the `GeoDirectory`, and the cache inside, and preferably concurrently? That's chapter three. There the first series finally fires on all cylinders. --- --- url: https://true-async.github.io/en/docs/server/streaming.md description: >- readBody(): pull-based request body streaming. send()/sendable(): chunked response streaming with backpressure. HTTP/2 trailers. --- # Request and response streaming (PHP 8.6+, true\_async\_server 0.6+) ## Request body streaming: `readBody()` By default the handler receives the fully-read body (`HttpRequest::getBody()`). With `HttpServerConfig::setBodyStreamingEnabled(true)` the H1/H2 parsers push DATA chunks into a per-request FIFO and the handler reads them one at a time through `HttpRequest::readBody()`. ```php use TrueAsync\HttpServer; use TrueAsync\HttpServerConfig; $server = new HttpServer( (new HttpServerConfig()) ->addListener('0.0.0.0', 8080) ->setBodyStreamingEnabled(true) ); $server->addHttpHandler(function ($req, $res) { $fp = fopen('/tmp/upload-' . bin2hex(random_bytes(8)), 'wb'); $total = 0; while (($chunk = $req->readBody()) !== null) { fwrite($fp, $chunk); $total += strlen($chunk); } fclose($fp); $res->json(['received' => $total]); }); $server->start(); ``` ### Semantics * One `readBody()` call returns **one** parser-supplied chunk: * an H2 DATA frame (up to 16 KiB by default), * an llhttp `on_body` slice (bounded by the H1 read buffer of 8 KiB). * When the queue is empty, the coroutine parks on a per-request trigger event. * EOF returns `null` (idempotent). * A stream error (peer reset, `max_body_size` exceeded) throws `\Exception`. * The `$maxLen` parameter is reserved for a future coalesce optimisation and is currently ignored. The signature stays binary-compatible with the upcoming polish (issue #26). ### When to enable * Large uploads (logs, media, backups) * Streaming parsers (NDJSON, MessagePack stream) * Services where tail latency degrades from holding the body in RAM * Multipart is **always** streamed, regardless of `setBodyStreamingEnabled()` When **not** to enable: REST endpoints where the body is small and `getBody()`/`getPost()`/ `getQuery()` is more convenient. There is no combined mode (stream only when the body exceeds X); `getBody()` in streaming mode throws `LogicException` (planned on the roadmap). ### Memory footprint For 50 parallel 20-MiB POSTs (h2load, WSL2): peak RSS drops 1170 MiB → **197 MiB** (×6). Throughput grows from 36 req/s → **100 req/s** (×2.7) because handler dispatch no longer waits for the full body. ## Response streaming: `send()` / `sendable()` The simplest response — via `setBody()` / `json()` / `html()` / `redirect()` — is sent as a single chunk. For a streamed response (chunked H1, HTTP/2 DATA frames), use `send($chunk)`: ```php $server->addHttpHandler(function ($req, $res) { $res ->setStatusCode(200) ->setHeader('Content-Type', 'text/event-stream') ->setHeader('Cache-Control', 'no-store') ->setNoCompression(); // SSE: events must reach the client immediately // The first send() commits status + headers (they cannot be changed afterwards) foreach (generateEvents() as $event) { $res->send("data: " . json_encode($event) . "\n\n"); } }); ``` ### Backpressure `send()` blocks the handler coroutine **only** under backpressure: the per-stream staging buffer is full. In the normal case it returns immediately. HTTP/2: backpressure kicks in when the ring slots are full **or** the `HttpServerConfig::setStreamWriteBufferBytes()` limit is exceeded (default 256 KiB). HTTP/1 chunked: uses the kernel send buffer. ### `sendable()` Advisory non-blocking check: returns `true` if `send()` will accept a chunk without suspending the coroutine. `false` means: `send()` will block, or the response was closed / sealed by `sendFile()`, or the response type is not streaming-capable. ```php foreach ($events as $event) { if (!$res->sendable()) { // we don't want to wait on a slow client — do other work $event->save(); // persist to the database continue; } $res->send($event->encode()); } ``` `send()` is **always** safe to call, regardless of `sendable()`. The latter just gives the handler a chance to do other work instead of blocking on a slow peer. ## HTTP/2 trailers HTTP/2 supports a HEADERS frame after the body (trailers). The canonical consumer is gRPC (`grpc-status` as a trailer). ```php $res->setStatusCode(200); $res->send($body); $res->setTrailer('grpc-status', '0'); $res->setTrailer('grpc-message', 'OK'); ``` Bulk set: ```php $res->setTrailers(['grpc-status' => '0', 'grpc-message' => 'OK']); $res->resetTrailers(); // clear all $res->getTrailers(); ``` On HTTP/1.1 the value is **silently ignored**: chunked-encoding trailer emission is out of scope for Step 5b. > Trailer names are written in lowercase (RFC 9113 §8.2.2); uppercase is automatically lowered. ## See also * [`HttpServerConfig::setBodyStreamingEnabled()`](/en/docs/reference/server/http-server-config.html#setbodystreamingenabled) * [`HttpServerConfig::setStreamWriteBufferBytes()`](/en/docs/reference/server/http-server-config.html#setstreamwritebufferbytes) * [`HttpRequest::readBody()`](/en/docs/reference/server/http-request.html#readbody) * [`HttpResponse::send()`](/en/docs/reference/server/http-response.html#send) * [`HttpResponse::sendable()`](/en/docs/reference/server/http-response.html#sendable) * [`HttpResponse::setTrailer()`](/en/docs/reference/server/http-response.html#settrailer) --- --- url: https://true-async.github.io/en/docs/reference/request-context.md description: >- Async\request_context() — shared request context visible to the entire handler coroutine tree. Bound to the request scope set by the embedding C code (HTTP server). --- # request\_context (PHP 8.6+, True Async 1.0) `Async\request_context()` returns the [`Context`](/en/docs/components/context.html) of the request scope inherited from the parent coroutine, or `null` if no request scope is set. ## Description ```php namespace Async; function request_context(): ?Context ``` The request scope is set by **embedding C code** (for example, the HTTP server) and is automatically propagated to all child coroutines. This gives the handler a single context visible to the entire request coroutine tree. | Function | What it returns | |----------|-----------------| | `current_context()` | The **current Scope's** context — shared by every coroutine in that Scope | | `coroutine_context()` | The **current coroutine's** private context — isolated per coroutine | | `request_context()` | The **request's** context — shared between the handler and all its child coroutines | | `root_context()` | The root-scope context | ## Return value `Async\Context` — the shared request-scope context, or `null` outside a request scope (for example, in a CLI script without an HTTP server). ## Examples ### Example #1 Propagating a request id through the entire coroutine tree ```php addListener('0.0.0.0', 8080)); $server->addHttpHandler(function ($req, $res) { $rid = $req->getHeader('X-Request-Id') ?? bin2hex(random_bytes(8)); request_context()->set('request_id', $rid); request_context()->set('user_id', authUser($req)); // Child coroutines automatically see the same context. [$user, $posts] = await_all([ spawn(fn() => fetchUser()), spawn(fn() => fetchPosts()), ]); $res->setHeader('X-Request-Id', $rid); $res->json(['user' => $user, 'posts' => $posts]); }); function fetchUser(): array { // request_id is visible here — useful for logging, for example $rid = request_context()?->get('request_id'); log_debug("[$rid] fetching user"); // ... return [/* ... */]; } $server->start(); ``` ### Example #2 Safe access outside a request scope ```php get('request_id') ?? 'no-request'; error_log("[$rid] $event"); } // Works both inside an HTTP handler (request_id is visible) // and in a background CLI task (request_context() === null, $rid === 'no-request'). ``` ### Example #3 Comparison with `coroutine_context()` ```php addHttpHandler(function ($req, $res) { request_context()->set('request_id', 'abc-123'); coroutine_context()->set('local', 'handler-only'); $child = spawn(function () { // request_context is visible because it is shared across the entire request scope. var_dump(request_context()->get('request_id')); // string(7) "abc-123" // The child coroutine has its own coroutine_context(), and it does not look up the tree. var_dump(coroutine_context()->find('local')); // NULL }); await($child); $res->setStatusCode(204); }); ``` ## Notes > **When `null` is returned.** The request scope is set by external code (HTTP server, queue, > gRPC). In a plain CLI script without such an environment, `request_context()` is always `null` > — that is normal. > **Not for arbitrary communication.** `request_context()` is intentionally limited to the request > scope. Use regular services / a DI container for system-wide values (config, registry); use > `current_context()` for per-coroutine values. ## See also * [Async\Context](/en/docs/components/context.html) — the context class itself and its methods * [Async\current\_context()](/en/docs/reference/current-context.html) — current coroutine's context * [Async\root\_context()](/en/docs/reference/root-context.html) — root-scope context * [TrueAsync Server: per-request scope](/en/docs/server/workers.html#per-request-scope) --- --- url: https://true-async.github.io/en/rfc.md description: Official proposals for adding asynchronous capabilities to PHP core --- --- --- url: https://true-async.github.io/en/roadmap.md description: Development plan for TrueAsync --- --- --- url: https://true-async.github.io/en/docs/reference/root-context.md description: root_context() — get the global root context visible from all scopes. --- # root\_context (PHP 8.6+, True Async 1.0) `root_context()` — Returns the global root `Async\Context` object, shared across the entire request. ## Description ```php root_context(): Async\Context ``` Returns the top-level context. Values set here are visible via `find()` from any context in the hierarchy. ## Return Values An `Async\Context` object. ## Examples ```php set('app_name', 'MyApp') ->set('environment', 'production'); spawn(function() { // Accessible from any coroutine via find() $env = current_context()->find('environment'); // "production" }); ?> ``` ## See Also * [current\_context()](/en/docs/reference/current-context.html) — Scope context * [coroutine\_context()](/en/docs/reference/coroutine-context.html) — coroutine context * [Context](/en/docs/components/context.html) — the context concept --- --- url: https://true-async.github.io/en/tutors-laravel/01-start.md description: >- Step by step: running Laravel under TrueAsync Server and your first API, from routes to the database. --- # Running Laravel Inside a Coroutine Server The `Laravel` framework wasn't originally built for coroutine servers, so you can't just pick it up and run it on `TrueAsync`. However, the [`laravel-spawn`](https://github.com/YanGusik/laravel-spawn) project solved this problem with a set of dedicated adapters. ## Step 1. Install the Package ```bash composer require yangusik/laravel-spawn ``` The service provider registers itself automatically. Publish the server config: ```bash php artisan vendor:publish --tag=async-config ``` This creates `config/async.php`, which holds the listeners, the number of workers, the database connection pool, and a list of services that need to be re-resolved on every request. ## Step 2. Start the Server ```bash php artisan async:serve --host=0.0.0.0 --port=8080 ``` This command starts `TrueAsync Server` and wires up Laravel's router. ```bash $ curl http://localhost:8080/ ``` If you see Laravel's default welcome page, everything came up fine. From here we build a real `API` on top of it. ## Step 3. Routes Nothing unusual, a plain `routes/api.php`: ```php use App\Http\Controllers\ProfileController; Route::get('/profile/{id}', [ProfileController::class, 'show']); Route::post('/profile/{id}/address', [ProfileController::class, 'updateAddress']); ``` `Route::get`, `Route::post`, `Route::apiResource` all work as usual. The router is built once when the worker starts and survives every request that follows. ## Step 4. Controller ```php class ProfileController extends Controller { public function show(int $id) { $user = User::with(['orders', 'reviews'])->findOrFail($id); return response()->json($user); } public function updateAddress(int $id, Request $request) { $request->validate(['address' => 'required|array']); $user = User::findOrFail($id); $user->update(['address' => $request->input('address')]); return response()->json(['ok' => true]); } } ``` Validation, `Eloquent`, `findOrFail`, an automatic `404` thrown as an exception, all exactly like synchronous `Laravel`. The difference is hidden deeper: while this handler waits for a response from the database, the worker is already serving the next request in a neighboring coroutine. ## Step 5. Authentication and Session ```php Route::middleware('auth:sanctum')->get('/me', function (Request $request) { return $request->user(); }); ``` `Auth::user()`, `$request->user()`, `session()->get(...)` all work unchanged. The package already makes sure every request sees its own user and its own session, even while hundreds of other requests are being handled concurrently right next to it. How exactly this works under the hood is the topic of the next chapter, but from the controller's point of view it just works. ## Step 6. Database ```env DB_CONNECTION=pgsql ``` ```php // config/async.php 'db_pool' => [ 'enabled' => true, 'min' => 2, 'max' => 10, ], ``` `Eloquent` queries, transactions, `DB::transaction(...)`, all written exactly as usual. Under the hood, each request gets its own physical connection from the built-in `PDO Pool` instead of sharing one across everyone. You've already seen the pool's mechanics in the core series. ## What's Next The `API` built across these six steps is already a real Laravel application running under `TrueAsync Server`: routes, controllers, authentication, a database. The next chapters explain exactly what happens under the hood when hundreds of such requests run in a single process concurrently: how the connection pool and transactions work, which code patterns are dangerous inside a coroutine, and how popular packages like `Telescope` and `spatie/laravel-permission` behave. --- --- url: https://true-async.github.io/en/tutors/08-scope.md description: >- Scope: who owns coroutines, waits for them to finish, and cancels the whole group. --- # Scope In the previous chapter we launched ten workers and closed the channel. The file is read, `close()` has been called, and the main flow has moved on. But wait: the workers are still working through the buffer. The import function has already returned control, and the work isn't done. If the PHP script were to end right now, some addresses would be left unchecked. And a second worry from that same chapter: what if `checkAddress` inside a worker throws an exception? From the chapter on exceptions we know it'll be stored in the coroutine's handle, waiting for an `await`. But nobody was going to `await` the workers. The error would surface right at the very end, when it's too late to fix anything. Both problems can be solved by hand: collect coroutines into an array and await each one. ```php $workers = []; for ($i = 0; $i < 10; $i++) { $workers[] = spawn(worker(...), $queue); } // ... reading the file ... foreach ($workers as $worker) { await($worker); } ``` It works. But it's bookkeeping: you have to remember to set up the array, carry it through the whole code path, and iterate over it at the end. And if a coroutine gets launched somewhere deep inside a called function, it never even makes it into that array. What we want is for coroutines to know on their own who they belong to. That's what `Scope` is for. ## A sandbox for coroutines `Scope` is a space in which coroutines live. It knows about every coroutine launched inside it and can treat them as a group: ```php use Async\Scope; use Async\Channel; use Async\ChannelException; $queue = new Channel(100); $workers = new Scope(); for ($i = 0; $i < 10; $i++) { $workers->spawn(function () use ($queue) { foreach ($queue as $address) { checkAddress($address); } }); } while (($row = fgetcsv($handle)) !== false) { $queue->send($row[$addressIndex]); } $queue->close(); $workers->awaitCompletion(timeout(60000)); echo "Import finished, all addresses checked\n"; ``` The difference from the previous chapter is two lines: `$workers->spawn()` instead of `spawn()`, and `awaitCompletion()` at the end. The `awaitCompletion` method waits until every coroutine in the scope finishes, however many there are. No array, no bookkeeping: the scope keeps track on its own. The cancellation token here isn't optional, it's a required argument: a scope deliberately won't let you wait on a group without a bound. The familiar `timeout` fits nicely, and if the import doesn't make it within a minute, the wait is interrupted with an `OperationCanceledException`, and it's up to you to decide: wait a bit longer, or cancel the group. ## Errors no longer get lost Let's go back to the worker that crashed. A coroutine inside a scope can't die silently: an unhandled exception bubbles up to the parent scope. By default, a scope reacts strictly: an error in one coroutine cancels all the others, and the exception is delivered to whoever is waiting in `awaitCompletion`. ```php try { $workers->awaitCompletion(timeout(60000)); } catch (RemoteApiException $e) { echo "Import aborted: {$e->getMessage()}\n"; } ``` This strategy is called fail-together: the group either finishes as a whole, or stops as a whole. For the import, that's reasonable: if `GeoDirectory` goes down, there's no point bombarding it with the remaining nine workers. But strictness isn't always what you want. One bad address in the file isn't a reason to abandon the other ninety-nine thousand. In that case, you assign the scope an error handler, and the coroutines become independent: the failed one gets logged, the rest keep working: ```php $workers->setExceptionHandler(function ($scope, $coroutine, Throwable $e) { error_log("Address not checked: {$e->getMessage()}"); }); ``` Besides the exception itself, the handler receives the scope and the failed coroutine: handy for figuring out exactly who died, or for restarting the work. The choice of strategy is up to you, and that's the main difference from a plain `spawn`: there, the only strategy is "fire and forget". ## Cancelling the whole group In the chapter on cancellation we stopped a single coroutine with `cancel()`. A scope does the same for an entire group at once: ```php $workers->cancel(); ``` Every coroutine inside receives the familiar `AsyncCancellation` at its own wait point: some in `recv`, some in `delay`. The mechanism is the same, cooperative, it's just that the signal goes out to everyone at once. A scope can contain child scopes, and cancellation flows down the hierarchy recursively: cancel the parent, and the whole branch is cancelled. Coroutines stop being a scattered pile of independent tasks and form a tree, where each one has a place and an owner. This approach is called structured concurrency, and it's already proven itself in Kotlin, Swift, and Java. TrueAsync brings it to PHP. ## A scope belongs to an object The most elegant use of a scope: hand ownership of it to an object. ```php use Async\Scope; final class ImportService { private Scope $scope; public function __construct() { $this->scope = new Scope(); } public function import(string $path): void { $this->scope->spawn(/* workers and file reading */); $this->scope->spawn(/* the progress coroutine from the first chapter */); } public function __destruct() { $this->scope->dispose(); } } ``` The lifetime of the coroutines now matches the lifetime of the service. As long as the object exists, its coroutines keep working. Destroy the object, and `dispose()` cancels everything it managed to launch. Remember `$progress->cancel()` from the first chapter? We manually caught the moment when the progress coroutine became unnecessary. With a scope, that question just goes away: progress is needed for as long as the import runs, and it runs for exactly as long as `ImportService` lives. Ownership is expressed directly in the code, and there's simply nowhere left to forget a coroutine. ## The whole import, end to end Let's put together everything we've accumulated over eight chapters into one working class: the coroutines and progress from the first chapter, the backpressure-aware channel from the seventh, the scope from this one. ```php use Async\Scope; use Async\Channel; use Async\ChannelException; use function Async\delay; use function Async\timeout; final class ImportService { private Scope $scope; public function __construct(private readonly int $workers = 10) { $this->scope = new Scope(); } public function import(string $path, int $total): void { $queue = new Channel(100); $counter = 0; // Workers: pull addresses from the channel, no more than $this->workers concurrently for ($i = 0; $i < $this->workers; $i++) { $this->scope->spawn(function () use ($queue, &$counter) { foreach ($queue as $address) { checkAddress($address); $counter++; } }); } // Progress: renders the state once a second until the import is done $this->scope->spawn(function () use (&$counter, $total) { while ($counter < $total) { printProgress($counter, $total); delay(1000); } printProgress($total, $total); }); // Producer: reads the file, the channel's backpressure protects memory $handle = fopen($path, 'r'); $header = fgetcsv($handle); $addressIndex = array_search('address', $header); while (($row = fgetcsv($handle)) !== false) { $queue->send($row[$addressIndex]); } fclose($handle); $queue->close(); // Wait for everything: both the workers and the progress coroutine $this->scope->awaitCompletion(timeout(600000)); } public function __destruct() { $this->scope->dispose(); } } $importer = new ImportService(); $importer->import('users.csv', 100_000); ``` A couple of details worth a closer look. Progress is no longer an infinite loop: the condition `while ($counter < $total)` ends it cooperatively once the last address has been processed, so `awaitCompletion` waits for everything without a single `cancel`. And `dispose()` in the destructor plays no part in normal operation at all: it's a safety net for when the import throws an exception or the object gets discarded partway through. That's the essentials of scope. `spawn` only answers the question "how do I start concurrent work". Scope handles the questions that come right after: who waits for that work, who finds out about an error, and who stops it. Without a scope, a coroutine is left to fend for itself; inside a scope, it has an owner and a place in the program's structure. So far the workers have only been reading from the outside world. But checked addresses still need to be saved to a database. Can we just hand ten coroutines a single `PDO` object? A good question to open the next chapter. --- --- url: https://true-async.github.io/en/docs/components/scope.md description: >- Scope in TrueAsync -- managing coroutine lifetimes, hierarchy, group cancellation, error handling and structured concurrency. --- # Scope: Managing Coroutine Lifetimes ## The Problem: Explicit Resource Control, Forgotten Coroutines ```php function processUser($userId) { spawn(sendEmail(...), $userId); spawn(updateCache(...), $userId); spawn(logActivity(...), $userId); return "OK"; } processUser(123); // The function returned, but three coroutines are still running! // Who is watching them? When will they finish? // Who will handle exceptions if they occur? ``` One of the common problems in asynchronous programming is coroutines accidentally "forgotten" by the developer. They are launched, perform work, but nobody monitors their lifecycle. This can lead to resource leaks, incomplete operations, and hard-to-find bugs. For `stateful` applications, this problem is significant. ## The Solution: Scope ![Scope Concept](../../../assets/docs/scope_concept.jpg) **Scope** -- a logical space for running coroutines, which can be compared to a sandbox. The following rules guarantee that coroutines are under control: * Code always knows which `Scope` it is executing in * The `spawn()` function creates a coroutine in the current `Scope` * A `Scope` knows about all coroutines that belong to it ```php function processUser($userId):string { spawn(sendEmail(...), $userId); spawn(updateCache(...), $userId); spawn(logActivity(...), $userId); // Wait until all coroutines in scope finish $scope->awaitCompletion(Async\timeout(1000)); return "OK"; } $scope = new Async\Scope(); $scope->spawn(processUser(...), 123); $scope->awaitCompletion(Async\timeout(5000)); // Now the function will only return when ALL coroutines have finished ``` ## Binding to an Object `Scope` is convenient to bind to an object to explicitly express ownership of a group of coroutines. Such semantics directly express the programmer's intent. ```php class UserService { // Only one unique object will own a unique Scope // Coroutines live as long as the UserService object private Scope $scope; public function __construct() { // Create a dome for all service coroutines $this->scope = new Async\Scope(); } public function sendNotification($userId) { // Launch a coroutine inside our dome $this->scope->spawn(function() use ($userId) { // This coroutine is bound to UserService sendEmail($userId); }); } public function __destruct() { // When the object is deleted, resources are guaranteed to be cleaned up // All coroutines inside are automatically cancelled $this->scope->dispose(); } } $service = new UserService(); $service->sendNotification(123); $service->sendNotification(456); // Delete the service - all its coroutines are automatically cancelled unset($service); ``` ## Scope Hierarchy A scope can contain other scopes. When a parent scope is cancelled, all child scopes and their coroutines are also cancelled. This approach is called **structured concurrency**. ```php $mainScope = new Async\Scope(); $mainScope->spawn(function() { echo "Main task\n"; // Create a child scope $childScope = Async\Scope::inherit(); $childScope->spawn(function() { echo "Subtask 1\n"; }); $childScope->spawn(function() { echo "Subtask 2\n"; }); // Wait for subtasks to complete $childScope->awaitCompletion(Async\timeout(5000)); echo "All subtasks done\n"; }); $mainScope->awaitCompletion(Async\timeout(5000)); ``` If you cancel `$mainScope`, all child scopes will also be cancelled. The entire hierarchy. ## Cancelling All Coroutines in a Scope ```php $scope = new Async\Scope(); $scope->spawn(function() { try { while (true) { echo "Working...\n"; Async\delay(1000); } } catch (Async\AsyncCancellation $e) { echo "I was cancelled!\n"; } }); $scope->spawn(function() { try { while (true) { echo "Also working...\n"; Async\delay(1000); } } catch (Async\AsyncCancellation $e) { echo "Me too!\n"; } }); // Works for 3 seconds Async\delay(3000); // Cancel ALL coroutines in scope $scope->cancel(); // Both coroutines will receive AsyncCancellation ``` ## Error Handling in Scope When a coroutine inside a scope fails with an error, the scope can catch it: ```php $scope = new Async\Scope(); // Set up an error handler $scope->setExceptionHandler(function (Async\Scope $scope, Async\Coroutine $coroutine, Throwable $e) { echo "Error in scope: " . $e->getMessage() . "\n"; // Can log it, send to Sentry, etc. }); $scope->spawn(function() { throw new Exception("Something broke!"); }); $scope->spawn(function() { echo "I'm working fine\n"; }); $scope->awaitCompletion(Async\timeout(5000)); // Output: // Error in scope: Something broke! // I'm working fine ``` ## Finally: Guaranteed Cleanup Even if a scope is cancelled, finally blocks will execute: ```php $scope = new Async\Scope(); $scope->spawn(function() { try { echo "Starting work\n"; Async\delay(10000); // Long operation echo "Finished\n"; // Won't execute } finally { // This is GUARANTEED to execute echo "Cleaning up resources\n"; closeConnection(); } }); Async\delay(1000); $scope->cancel(); // Cancel after one second // Output: // Starting work // Cleaning up resources ``` ## TaskGroup: Scope with Results `TaskGroup` -- a specialized scope for parallel task execution with result aggregation. It supports concurrency limits, named tasks, and three waiting strategies: ```php $group = new Async\TaskGroup(concurrency: 5); $group->spawn(fn() => fetchUser(1)); $group->spawn(fn() => fetchUser(2)); $group->spawn(fn() => fetchUser(3)); // Get all results (waits for all tasks to complete) $results = await($group->all()); // Or get the first completed result $first = await($group->race()); // Or the first successful one (ignoring errors) $any = await($group->any()); ``` Tasks can be added with keys and iterated as they complete: ```php $group = new Async\TaskGroup(); $group->spawnWithKey('user', fn() => fetchUser(1)); $group->spawnWithKey('orders', fn() => fetchOrders(1)); // Iterate over results as they become ready foreach ($group as $key => [$result, $error]) { if ($error) { echo "Task $key failed: {$error->getMessage()}\n"; } else { echo "Task $key: $result\n"; } } ``` ## Global Scope: There's Always a Parent If you don't specify a scope explicitly, the coroutine is created in the **current scope**. At the top level of a script the current scope is the **global scope** (also called the main scope): ```php // At the top level: the coroutine goes into the global scope spawn(function() { echo "I'm in global scope\n"; }); // Inside a coroutine: the coroutine goes into the scope of the caller $scope = new Async\Scope(); $scope->spawn(function() { spawn(function() { echo "I'm in \$scope, same as my parent\n"; }); }); ``` The global scope has no PHP object: there is no `Scope::global()` method. Its context is reachable through `Async\root_context()`, and `Async\Scope::inherit()` called at the top level creates a child of it. Global scope lives for the entire request. When PHP exits, all coroutines in global scope are cancelled gracefully. ## Real-World Example: HTTP Client ```php class HttpClient { private Scope $scope; public function __construct() { $this->scope = new Async\Scope(); } public function get(string $url): Async\Awaitable { return $this->scope->spawn(function() use ($url) { $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); try { return curl_exec($ch); } finally { curl_close($ch); } }); } public function cancelAll(): void { // Cancel all active requests $this->scope->cancel(); } public function __destruct() { // When the client is destroyed, all requests are automatically cancelled $this->scope->dispose(); } } $client = new HttpClient(); $req1 = $client->get('https://api1.com/data'); $req2 = $client->get('https://api2.com/data'); $req3 = $client->get('https://api3.com/data'); // Cancel all requests $client->cancelAll(); // Or just destroy the client - same effect unset($client); ``` ## Structured Concurrency `Scope` implements the **Structured Concurrency** principle -- a set of rules for managing concurrent tasks, proven in production runtimes of `Kotlin`, `Swift`, and `Java`. ### API for Lifetime Management `Scope` provides the ability to explicitly control the lifetime of a coroutine hierarchy using the following methods: | Method | What it does | |------------------------------------------|------------------------------------------------------------------| | `$scope->spawn(Closure, ...$args)` | Launches a coroutine inside the Scope | | `$scope->awaitCompletion($cancellation)` | Waits for all coroutines in the Scope to complete | | `$scope->cancel()` | Sends a cancellation signal to all coroutines | | `$scope->dispose()` | Closes the Scope; cancels coroutines, or marks them zombie under `DISPOSE_SAFELY` | | `$scope->disposeSafely()` | Closes the Scope; coroutines are not cancelled but marked zombie | | `$scope->awaitAfterCancellation()` | Waits for all coroutines to complete, including zombie ones | | `$scope->disposeAfterTimeout(int $ms)` | Cancels coroutines after a timeout | These methods allow implementing three key patterns: **1. Parent waits for all child tasks** ```php $scope = new Async\Scope(); $scope->spawn(function() { /* task 1 */ }); $scope->spawn(function() { /* task 2 */ }); // Control won't return until both tasks complete $scope->awaitCompletion(Async\timeout(5000)); ``` In Kotlin, the same is done with `coroutineScope { }`, in Swift -- with `withTaskGroup { }`. **2. Parent cancels all child tasks** ```php $scope->cancel(); // All coroutines in $scope will receive a cancellation signal. // Child Scopes will also be cancelled -- recursively, to any depth. ``` **3. Parent closes the Scope and releases resources** `dispose()` closes the Scope and cancels all its coroutines: ```php $scope->dispose(); // Scope is closed. All coroutines are cancelled. // New coroutines cannot be added to this Scope. ``` `dispose()` follows the `DISPOSE_SAFELY` flag of the Scope. A `new Scope()` does not have it, so its coroutines are cancelled. A Scope that has the flag (after `allowZombies()`, or inherited from a parent that has it, such as the global scope) marks its coroutines as zombies instead, exactly like `disposeSafely()`. See [Zombie Coroutines](zombie-coroutines.md). If you need to close the Scope but allow current coroutines to **finish their work**, use `disposeSafely()` -- coroutines are marked as zombie (not cancelled, they continue executing, but the Scope is considered finished by active tasks): ```php $scope->disposeSafely(); // Scope is closed. Coroutines continue working as zombies. // Scope tracks them but doesn't count them as active. ``` ### Error Handling: Two Strategies An unhandled exception in a coroutine is not lost -- it bubbles up to the parent Scope. Different runtimes offer different strategies: | Strategy | Kotlin | Swift | TrueAsync | |------------------------------------------------------------------|-------------------|-------------------------|------------------------------------| | **Fail-together**: one child's error cancels all others | `coroutineScope` | `withThrowingTaskGroup` | `Scope` (default) | | **Independent children**: one's error doesn't affect others | `supervisorScope` | separate `Task` | `$scope->setExceptionHandler(...)` | The ability to choose a strategy is the key difference from "fire and forget". ### Context Inheritance Child tasks automatically receive the parent's context: priority, deadlines, metadata -- without explicitly passing parameters. In Kotlin, child coroutines inherit the parent's `CoroutineContext` (dispatcher, name, `Job`). In Swift, child `Task` instances inherit priority and task-local values. ### Where This Already Works | Language | API | In production since | |------------|-----------------------------------------------------------------|---------------------| | **Kotlin** | `coroutineScope`, `supervisorScope` | 2018 | | **Swift** | `TaskGroup`, `withThrowingTaskGroup` | 2021 | | **Java** | `StructuredTaskScope` ([JEP 453](https://openjdk.org/jeps/453)) | 2023 (preview) | TrueAsync brings this approach to PHP through `Async\Scope`. ## What's Next? * [Coroutines](/en/docs/components/coroutines.html) -- how coroutines work * [Cancellation](/en/docs/components/cancellation.html) -- cancellation patterns * [Zombie Coroutines](/en/docs/components/zombie-coroutines.html) -- tolerance for third-party code --- --- url: https://true-async.github.io/en/docs/reference/scope/construct.md description: Creates a new root Scope. --- # Scope::\_\_construct (PHP 8.6+, True Async 1.0) ```php public function __construct() ``` Creates a new root `Scope`. A root scope has no parent scope and serves as an independent unit for managing the lifecycle of coroutines. ## Examples ### Example #1 Basic usage ```php spawn(function() { echo "Coroutine in a new scope\n"; }); $scope->awaitCompletion(Async\timeout(5000)); ``` ### Example #2 Creating multiple independent scopes ```php spawn(function() { echo "Task A\n"; }); $scopeB->spawn(function() { echo "Task B\n"; }); // Cancelling one scope does not affect the other $scopeA->cancel(); // $scopeB continues running $scopeB->awaitCompletion(Async\timeout(5000)); ``` ## See Also * [Scope::inherit](/en/docs/reference/scope/inherit.html) — Create a child Scope * [Scope::spawn](/en/docs/reference/scope/spawn.html) — Spawn a coroutine in the scope --- --- url: https://true-async.github.io/en/docs/reference/scope/as-not-safely.md description: >- Marks the scope as not safe — coroutines receive cancellation instead of becoming zombies. --- # Scope::asNotSafely (PHP 8.6+, True Async 1.0) ```php public function asNotSafely(): Scope ``` Marks the scope as "not safe". When `disposeSafely()` is called on such a scope, coroutines **do not** become zombies but instead receive a cancellation signal. This is useful for background tasks that do not require guaranteed completion. The method returns the same scope object, enabling method chaining (fluent interface). ## Return Value `Scope` — the same scope object (for method chaining). ## Examples ### Example #1 Scope for background tasks ```php asNotSafely(); $scope->spawn(function() { while (true) { // Background task: cache cleanup cleanExpiredCache(); \Async\delay(60_000); } }); // With disposeSafely(), coroutines will be cancelled instead of becoming zombies $scope->disposeSafely(); ``` ### Example #2 Using with inherit ```php asNotSafely(); $bgScope->spawn(function() { echo "Background process\n"; \Async\delay(10_000); }); // On close: coroutines will be cancelled, not turned into zombies $bgScope->disposeSafely(); ``` ## See Also * [Scope::disposeSafely](/en/docs/reference/scope/dispose-safely.html) — Safely close the scope * [Scope::dispose](/en/docs/reference/scope/dispose.html) — Forcefully close the scope * [Scope::cancel](/en/docs/reference/scope/cancel.html) — Cancel all coroutines --- --- url: >- https://true-async.github.io/en/docs/reference/scope/await-after-cancellation.md description: >- Waits for all coroutines including zombies to complete after scope cancellation. --- # Scope::awaitAfterCancellation (PHP 8.6+, True Async 1.0) ```php public function awaitAfterCancellation( ?callable $errorHandler = null, ?Awaitable $cancellation = null ): void ``` Waits for **all** coroutines in the scope to complete, including zombie coroutines. Requires a prior call to `cancel()`. This method is used for graceful scope termination when you need to wait until all coroutines (including zombies) finish their work. ## Parameters `errorHandler` — a callback function for handling zombie coroutine errors. Accepts a `\Throwable` as an argument. If `null`, errors are ignored. `cancellation` — an `Awaitable` object to interrupt the wait. If `null`, the wait is not time-limited. ## Return Value No value is returned. ## Examples ### Example #1 Graceful termination with error handling ```php spawn(function() { \Async\delay(1000); echo "Task completed\n"; }); $scope->spawn(function() { \Async\delay(5000); throw new \RuntimeException("Background task error"); }); // First, cancel $scope->cancel(); // Then wait for all coroutines to finish $scope->awaitAfterCancellation( errorHandler: function(\Throwable $e) { error_log("Zombie error: " . $e->getMessage()); } ); ``` ### Example #2 Waiting with a timeout ```php spawn(function() { // Zombie coroutine that takes a long time to finish try { \Async\delay(30_000); } catch (\Async\AsyncCancellation) { // Resource cleanup \Async\delay(2000); } }); $scope->cancel(); $scope->awaitAfterCancellation( errorHandler: function(\Throwable $e) { error_log($e->getMessage()); }, cancellation: timeout(5000) ); ``` ## See Also * [Scope::cancel](/en/docs/reference/scope/cancel.html) — Cancel all coroutines * [Scope::awaitCompletion](/en/docs/reference/scope/await-completion.html) — Wait for active coroutines * [Scope::dispose](/en/docs/reference/scope/dispose.html) — Cancel and close the scope --- --- url: https://true-async.github.io/en/docs/reference/scope/await-completion.md description: Waits for active coroutines in the scope to complete. --- # Scope::awaitCompletion (PHP 8.6+, True Async 1.0) ```php public function awaitCompletion(Awaitable $cancellation): void ``` Waits for all **active** coroutines in the scope to complete. Zombie coroutines are not considered when waiting. The `$cancellation` parameter allows the wait to be interrupted early. ## Parameters `cancellation` — an `Awaitable` object that, when triggered, will interrupt the wait. ## Return Value No value is returned. ## Examples ### Example #1 Waiting for all coroutines to complete ```php spawn(function() { \Async\delay(1000); echo "Task 1 completed\n"; }); $scope->spawn(function() { \Async\delay(2000); echo "Task 2 completed\n"; }); // Wait for completion with a 5-second timeout $scope->awaitCompletion(timeout(5000)); echo "All tasks done\n"; ``` ### Example #2 Interrupting the wait ```php spawn(function() { \Async\delay(60_000); // Very long task }); try { $scope->awaitCompletion(timeout(3000)); } catch (\Async\AsyncCancellation $e) { echo "Wait interrupted by timeout\n"; $scope->cancel(); } ``` ## See Also * [Scope::awaitAfterCancellation](/en/docs/reference/scope/await-after-cancellation.html) — Wait for all coroutines including zombies * [Scope::cancel](/en/docs/reference/scope/cancel.html) — Cancel all coroutines * [Scope::isFinished](/en/docs/reference/scope/is-finished.html) — Check if the scope is finished --- --- url: https://true-async.github.io/en/docs/reference/scope/cancel.md description: Cancels all coroutines in the scope. --- # Scope::cancel (PHP 8.6+, True Async 1.0) ```php public function cancel(?AsyncCancellation $cancellationError = null): void ``` Cancels all coroutines belonging to the given scope. Each active coroutine will receive a `AsyncCancellation`. If `$cancellationError` is specified, it will be used as the cancellation reason. ## Parameters `cancellationError` — a custom cancellation exception. If `null`, the standard `AsyncCancellation` is used. ## Return Value No value is returned. ## Examples ### Example #1 Basic cancellation ```php spawn(function() { try { \Async\delay(60_000); // Long operation } catch (\Async\AsyncCancellation $e) { echo "Coroutine cancelled\n"; } }); // Cancel all coroutines $scope->cancel(); ``` ### Example #2 Cancellation with a custom error ```php spawn(function() { try { \Async\delay(60_000); } catch (\Async\AsyncCancellation $e) { echo "Reason: " . $e->getMessage() . "\n"; } }); $error = new AsyncCancellation("Timeout exceeded"); $scope->cancel($error); ``` ## See Also * [Scope::dispose](/en/docs/reference/scope/dispose.html) — Cancel and close the scope * [Scope::isCancelled](/en/docs/reference/scope/is-cancelled.html) — Check if the scope is cancelled * [Scope::awaitAfterCancellation](/en/docs/reference/scope/await-after-cancellation.html) — Wait after cancellation --- --- url: https://true-async.github.io/en/docs/reference/scope/dispose.md description: Cancels all coroutines and closes the scope. --- # Scope::dispose (PHP 8.6+, True Async 1.0) ```php public function dispose(): void ``` Closes the scope and cancels all coroutines in it. After calling `dispose()`, the scope is marked as both closed and cancelled. New coroutines cannot be added to a closed scope. For a scope without the dispose-safely flag (`new Scope()` by default) this is equivalent to calling `cancel()` followed by closing the scope. A scope that carries the flag, set by `allowZombies()` or inherited from a parent that has it, such as the main scope, leaves its coroutines running as zombies instead, exactly like `disposeSafely()`. ## Return Value No value is returned. ## Examples ### Example #1 Forcefully closing a scope ```php spawn(function() { try { \Async\delay(60_000); } catch (\Async\AsyncCancellation) { echo "Coroutine cancelled on dispose\n"; } }); // All coroutines will be cancelled, scope closed $scope->dispose(); var_dump($scope->isClosed()); // bool(true) var_dump($scope->isCancelled()); // bool(true) ``` ### Example #2 Cleanup in a try/finally block ```php spawn(function() { // Business logic \Async\delay(5000); }); $scope->awaitCompletion(Async\timeout(5000)); } finally { $scope->dispose(); } ``` ## See Also * [Scope::disposeSafely](/en/docs/reference/scope/dispose-safely.html) — Safe close (with zombies) * [Scope::disposeAfterTimeout](/en/docs/reference/scope/dispose-after-timeout.html) — Close after a timeout * [Scope::cancel](/en/docs/reference/scope/cancel.html) — Cancel without closing the scope --- --- url: https://true-async.github.io/en/docs/reference/scope/dispose-after-timeout.md description: Closes the scope after a specified timeout. --- # Scope::disposeAfterTimeout (PHP 8.6+, True Async 1.0) ```php public function disposeAfterTimeout(int $timeout): void ``` Schedules the scope to be closed after a specified timeout. When the timeout expires, `dispose()` is called, cancelling all coroutines and closing the scope. This is convenient for setting a maximum scope lifetime. ## Parameters `timeout` — time in milliseconds before the scope is automatically closed. ## Return Value No value is returned. ## Examples ### Example #1 Limiting execution time ```php disposeAfterTimeout(10_000); $scope->spawn(function() { try { // Long operation \Async\delay(60_000); } catch (\Async\AsyncCancellation) { echo "Task cancelled by scope timeout\n"; } }); $scope->awaitCompletion(Async\timeout(5000)); ``` ### Example #2 Scope with a limited lifetime ```php disposeAfterTimeout(5000); // 5 seconds for all work $scope->spawn(function() { \Async\delay(1000); echo "Task 1: OK\n"; }); $scope->spawn(function() { \Async\delay(2000); echo "Task 2: OK\n"; }); $scope->spawn(function() { \Async\delay(30_000); // Won't finish in time echo "Task 3: OK\n"; // Will not be printed }); $scope->awaitCompletion(Async\timeout(5000)); ``` ## See Also * [Scope::dispose](/en/docs/reference/scope/dispose.html) — Immediate scope closure * [Scope::disposeSafely](/en/docs/reference/scope/dispose-safely.html) — Safe scope closure * [timeout()](/en/docs/reference/timeout.html) — Global timeout function --- --- url: https://true-async.github.io/en/docs/reference/scope/dispose-safely.md description: Safely closes the scope — coroutines become zombies. --- # Scope::disposeSafely (PHP 8.6+, True Async 1.0) ```php public function disposeSafely(): void ``` Safely closes the scope. Active coroutines **are not cancelled** but instead become zombie coroutines: they continue running, but the scope is considered closed. Zombie coroutines will finish on their own when they complete their work. If the scope is marked as "not safe" via `asNotSafely()`, coroutines will be cancelled instead of becoming zombies. ## Return Value No value is returned. ## Examples ### Example #1 Basic usage ```php spawn(function() { \Async\delay(5000); echo "Task completed as a zombie\n"; }); // Scope is closed, but the coroutine continues running $scope->disposeSafely(); var_dump($scope->isClosed()); // bool(true) // Coroutine continues executing in the background ``` ### Example #2 Graceful shutdown with zombie waiting ```php spawn(function() { \Async\delay(2000); echo "Background task completed\n"; }); $scope->disposeSafely(); // Wait for zombie coroutines to finish $scope->awaitAfterCancellation( errorHandler: function(\Throwable $e) { error_log("Zombie error: " . $e->getMessage()); } ); ``` ## See Also * [Scope::dispose](/en/docs/reference/scope/dispose.html) — Forcefully close the scope * [Scope::asNotSafely](/en/docs/reference/scope/as-not-safely.html) — Disable zombie behavior * [Scope::awaitAfterCancellation](/en/docs/reference/scope/await-after-cancellation.html) — Wait for zombie coroutines --- --- url: https://true-async.github.io/en/docs/reference/scope/on-finally.md description: Registers a callback to be invoked when the scope completes. --- # Scope::finally (PHP 8.6+, True Async 1.0) ```php public function finally(\Closure $callback): void ``` Registers a callback function that will be executed when the scope completes. This is the equivalent of a `finally` block for a scope, guaranteeing that cleanup code runs regardless of how the scope finished (normally, by cancellation, or with an error). ## Parameters `callback` — the closure that will be called when the scope completes. ## Return Value No value is returned. ## Examples ### Example #1 Resource cleanup ```php finally(function() { echo "Scope completed, cleaning up resources\n"; // Close connections, delete temporary files }); $scope->spawn(function() { echo "Executing task\n"; }); $scope->awaitCompletion(Async\timeout(5000)); // Output: "Executing task" // Output: "Scope completed, cleaning up resources" ``` ### Example #2 Multiple callbacks ```php finally(function() { echo "Closing database connection\n"; }); $scope->finally(function() { echo "Writing metrics\n"; }); $scope->spawn(function() { \Async\delay(1000); }); $scope->dispose(); // Both callbacks will be invoked when the scope completes ``` ## See Also * [Scope::dispose](/en/docs/reference/scope/dispose.html) — Close the scope * [Scope::isFinished](/en/docs/reference/scope/is-finished.html) — Check if the scope is finished * [Coroutine::finally](/en/docs/reference/coroutine/on-finally.html) — Callback on coroutine completion --- --- url: https://true-async.github.io/en/docs/reference/scope/get-child-scopes.md description: Returns an array of child scopes. --- # Scope::getChildScopes (PHP 8.6+, True Async 1.0) ```php public function getChildScopes(): array ``` Returns an array of all child scopes created via `Scope::inherit()` from the given scope. Useful for monitoring and debugging the scope hierarchy. ## Return Value `array` — an array of `Scope` objects that are children of the given scope. ## Examples ### Example #1 Getting child scopes ```php getChildScopes(); var_dump(count($children)); // int(2) ``` ### Example #2 Monitoring child scope state ```php spawn(function() { \Async\delay(1000); }); foreach ($appScope->getChildScopes() as $child) { $status = match(true) { $child->isCancelled() => 'cancelled', $child->isFinished() => 'finished', $child->isClosed() => 'closed', default => 'active', }; echo "Scope: $status\n"; } ``` ## See Also * [Scope::inherit](/en/docs/reference/scope/inherit.html) — Create a child scope * [Scope::setChildScopeExceptionHandler](/en/docs/reference/scope/set-child-scope-exception-handler.html) — Exception handler for child scopes --- --- url: https://true-async.github.io/en/docs/reference/scope/inherit.md description: Creates a new Scope that inherits from a specified or current scope. --- # Scope::inherit (PHP 8.6+, True Async 1.0) ```php public static function inherit(?Scope $parentScope = null): Scope ``` Creates a new `Scope` that inherits from the specified parent scope. If the `$parentScope` parameter is not provided (or is `null`), the new scope inherits from the current active scope. The child scope inherits exception handlers and cancellation policies from the parent. ## Parameters `parentScope` — the parent scope from which the new scope will inherit. If `null`, the current active scope is used. ## Return Value `Scope` — a new child scope. ## Examples ### Example #1 Creating a child scope from the current one ```php spawn(function() { // Inside the coroutine, the current scope is $parentScope $childScope = Scope::inherit(); $childScope->spawn(function() { echo "Running in child scope\n"; }); $childScope->awaitCompletion(Async\timeout(5000)); }); ``` ### Example #2 Explicitly specifying the parent scope ```php spawn(function() { echo "Coroutine in child scope\n"; }); // Cancelling the parent also cancels the child scope $rootScope->cancel(); ``` ## See Also * [Scope::\_\_construct](/en/docs/reference/scope/construct.html) — Create a root Scope * [Scope::getChildScopes](/en/docs/reference/scope/get-child-scopes.html) — Get child scopes * [Scope::dispose](/en/docs/reference/scope/dispose.html) — Cancel and close the scope --- --- url: https://true-async.github.io/en/docs/reference/scope/is-cancelled.md description: Checks whether the scope is cancelled. --- # Scope::isCancelled (PHP 8.6+, True Async 1.0) ```php public function isCancelled(): bool ``` Checks whether the scope has been cancelled. A scope is marked as cancelled after a call to `cancel()` or `dispose()`. ## Return Value `bool` — `true` if the scope has been cancelled, `false` otherwise. ## Examples ### Example #1 Checking scope cancellation ```php isCancelled()); // bool(false) $scope->cancel(); var_dump($scope->isCancelled()); // bool(true) ``` ## See Also * [Scope::cancel](/en/docs/reference/scope/cancel.html) — Cancel the scope * [Scope::isFinished](/en/docs/reference/scope/is-finished.html) — Check if the scope is finished * [Scope::isClosed](/en/docs/reference/scope/is-closed.html) — Check if the scope is closed --- --- url: https://true-async.github.io/en/docs/reference/scope/is-closed.md description: Checks whether the scope is closed. --- # Scope::isClosed (PHP 8.6+, True Async 1.0) ```php public function isClosed(): bool ``` Checks whether the scope is closed. A scope is considered closed after a call to `dispose()` or `disposeSafely()`. New coroutines cannot be added to a closed scope. ## Return Value `bool` — `true` if the scope is closed, `false` otherwise. ## Examples ### Example #1 Checking scope state ```php isClosed()); // bool(false) $scope->dispose(); var_dump($scope->isClosed()); // bool(true) ``` ### Example #2 Guarding against adding to a closed scope ```php dispose(); if (!$scope->isClosed()) { $scope->spawn(function() { echo "This coroutine will not be created\n"; }); } else { echo "Scope is already closed\n"; } ``` ## See Also * [Scope::isFinished](/en/docs/reference/scope/is-finished.html) — Check if the scope is finished * [Scope::isCancelled](/en/docs/reference/scope/is-cancelled.html) — Check if the scope is cancelled * [Scope::dispose](/en/docs/reference/scope/dispose.html) — Close the scope --- --- url: https://true-async.github.io/en/docs/reference/scope/is-finished.md description: Checks whether the scope is finished. --- # Scope::isFinished (PHP 8.6+, True Async 1.0) ```php public function isFinished(): bool ``` Checks whether all coroutines in the scope have finished. A scope is considered finished when all its coroutines (including child scopes) have completed execution. ## Return Value `bool` — `true` if all scope coroutines have finished, `false` otherwise. ## Examples ### Example #1 Checking scope completion ```php spawn(function() { \Async\delay(1000); }); var_dump($scope->isFinished()); // bool(false) $scope->awaitCompletion(Async\timeout(5000)); var_dump($scope->isFinished()); // bool(true) ``` ## See Also * [Scope::isClosed](/en/docs/reference/scope/is-closed.html) — Check if the scope is closed * [Scope::isCancelled](/en/docs/reference/scope/is-cancelled.html) — Check if the scope is cancelled * [Scope::awaitCompletion](/en/docs/reference/scope/await-completion.html) — Wait for coroutine completion --- --- url: https://true-async.github.io/en/docs/reference/scope/provide-scope.md description: ScopeProvider interface implementation — returns the current scope. --- # Scope::provideScope (PHP 8.6+, True Async 1.0) ```php public function provideScope(): Scope ``` Implementation of the `ScopeProvider` interface. Returns the scope object itself. This allows `Scope` to be used anywhere a `ScopeProvider` is expected. ## Return Value `Scope` — the current scope object. ## Examples ### Example #1 Using as a ScopeProvider ```php provideScope(); $scope->spawn(function() { echo "Running in the provided scope\n"; }); } $scope = new Scope(); // Scope itself implements ScopeProvider runInScope($scope); $scope->awaitCompletion(Async\timeout(5000)); ``` ### Example #2 Polymorphism with ScopeProvider ```php scope = new Scope(); } public function provideScope(): Scope { return $this->scope; } } function startWorkers(ScopeProvider $provider, int $count): void { $scope = $provider->provideScope(); for ($i = 0; $i < $count; $i++) { $scope->spawn(function() use ($i) { echo "Worker $i started\n"; }); } } // Works with both Scope and ServiceContainer $scope = new Scope(); startWorkers($scope, 3); $container = new ServiceContainer(); startWorkers($container, 3); ``` ## See Also * [Scope::inherit](/en/docs/reference/scope/inherit.html) — Create a child scope * [Scope::spawn](/en/docs/reference/scope/spawn.html) — Spawn a coroutine in the scope --- --- url: >- https://true-async.github.io/en/docs/reference/scope/set-child-scope-exception-handler.md description: Sets an exception handler for child Scopes. --- # Scope::setChildScopeExceptionHandler (PHP 8.6+, True Async 1.0) ```php public function setChildScopeExceptionHandler(callable $exceptionHandler): void ``` Sets an exception handler for exceptions thrown in child scopes. When a child scope finishes with an error, this handler is called, preventing the exception from propagating to the parent scope. ## Parameters `exceptionHandler` — the exception handling function for child scopes. Accepts a `\Throwable` as an argument. ## Return Value No value is returned. ## Examples ### Example #1 Catching child scope errors ```php setChildScopeExceptionHandler(function(\Throwable $e) { error_log("Error in child scope: " . $e->getMessage()); }); $childScope = Scope::inherit($parentScope); $childScope->spawn(function() { throw new \RuntimeException("Child scope error"); }); $childScope->awaitCompletion(Async\timeout(5000)); // Error handled, does not propagate to $parentScope ``` ### Example #2 Isolating errors between modules ```php setChildScopeExceptionHandler(function(\Throwable $e) { error_log("[App] Module error: " . $e->getMessage()); }); // Each module in its own scope $authScope = Scope::inherit($appScope); $cacheScope = Scope::inherit($appScope); $authScope->spawn(function() { // An error here will not affect $cacheScope throw new \RuntimeException("Auth failed"); }); $cacheScope->spawn(function() { echo "Cache is working fine\n"; }); $appScope->awaitCompletion(Async\timeout(5000)); ``` ## See Also * [Scope::setExceptionHandler](/en/docs/reference/scope/set-exception-handler.html) — Exception handler for coroutines * [Scope::inherit](/en/docs/reference/scope/inherit.html) — Create a child scope * [Scope::getChildScopes](/en/docs/reference/scope/get-child-scopes.html) — Get child scopes --- --- url: https://true-async.github.io/en/docs/reference/scope/set-exception-handler.md description: Sets an exception handler for child coroutines. --- # Scope::setExceptionHandler (PHP 8.6+, True Async 1.0) ```php public function setExceptionHandler(callable $exceptionHandler): void ``` Sets an exception handler for exceptions thrown in child coroutines of the scope. When a coroutine finishes with an unhandled exception, instead of the error propagating upward, the specified handler is called. ## Parameters `exceptionHandler` — the exception handling function. Accepts a `\Throwable` as an argument. ## Return Value No value is returned. ## Examples ### Example #1 Handling coroutine errors ```php setExceptionHandler(function(\Throwable $e) { error_log("Coroutine error: " . $e->getMessage()); }); $scope->spawn(function() { throw new \RuntimeException("Something went wrong"); }); $scope->awaitCompletion(Async\timeout(5000)); // Log will contain: "Coroutine error: Something went wrong" ``` ### Example #2 Centralized error logging ```php setExceptionHandler(function(\Throwable $e) use (&$errors) { $errors[] = $e; }); $scope->spawn(function() { throw new \RuntimeException("Error 1"); }); $scope->spawn(function() { throw new \LogicException("Error 2"); }); $scope->awaitCompletion(Async\timeout(5000)); echo "Total errors: " . count($errors) . "\n"; // Total errors: 2 ``` ## See Also * [Scope::setChildScopeExceptionHandler](/en/docs/reference/scope/set-child-scope-exception-handler.html) — Exception handler for child scopes * [Scope::finally](/en/docs/reference/scope/on-finally.html) — Callback on scope completion --- --- url: https://true-async.github.io/en/docs/reference/scope/spawn.md description: Spawns a coroutine in the given scope. --- # Scope::spawn (PHP 8.6+, True Async 1.0) ```php public function spawn(\Closure $callable, mixed ...$params): Coroutine ``` Spawns a new coroutine within the given scope. The coroutine will be bound to the scope and managed by its lifecycle: when the scope is cancelled or closed, all its coroutines will also be affected. ## Parameters `callable` — the closure to be executed as a coroutine. `params` — arguments to pass to the closure. ## Return Value `Coroutine` — the spawned coroutine object. ## Examples ### Example #1 Basic usage ```php spawn(function() { echo "Hello from a coroutine!\n"; return 42; }); echo $coroutine->getResult(); // 42 ``` ### Example #2 Passing parameters ```php spawn(function(string $url, int $timeout) { echo "Fetching $url with timeout {$timeout}ms\n"; // ... perform the request }, 'https://example.com', 5000); $scope->awaitCompletion(Async\timeout(5000)); ``` ## See Also * [spawn()](/en/docs/reference/spawn.html) — Global function for spawning coroutines * [Scope::cancel](/en/docs/reference/scope/cancel.html) — Cancel all scope coroutines * [Scope::awaitCompletion](/en/docs/reference/scope/await-completion.html) — Wait for coroutine completion --- --- url: https://true-async.github.io/en/docs/server/sse.md description: >- sseStart()/sseEvent()/sseComment()/sseRetry(): ready-made text/event-stream helpers over HTTP/1.1, HTTP/2, and HTTP/3. --- # Server-Sent Events (PHP 8.6+, true\_async\_server 0.8+) SSE (Server-Sent Events) is a simple way to stream text events to the browser over a regular HTTP connection, one direction only: from the server to the browser. Unlike WebSocket, it needs no separate protocol and no Upgrade handshake: the server just keeps the response open and appends new events as they become ready. The browser consumes them with the built-in `EventSource` API, no extra libraries required. `HttpResponse` gives you four methods for `text/event-stream`: `sseStart()`, `sseEvent()`, `sseComment()`, and `sseRetry()`. This is a thin formatting layer on top of the same [`send()` pipeline](/en/docs/server/streaming.html), so the same handler works unchanged over HTTP/1.1, HTTP/2, and HTTP/3, and the protocol is chosen by the client. ```php use TrueAsync\HttpServer; use TrueAsync\HttpServerConfig; use function Async\delay; $config = (new HttpServerConfig()) ->addListener('0.0.0.0', 8080) ->setWriteTimeout(0); // long-lived stream: no write deadline $server = new HttpServer($config); $server->addHttpHandler(function ($req, $res) { $res->sseStart(); // optional: the first sseEvent()/sseComment() starts the stream too $res->sseRetry(3000); // hint the browser to reconnect after 3s on drop $res->sseComment('stream open'); // heartbeat, keeps proxies from idling the connection out for ($i = 1; $i <= 10; $i++) { $res->sseEvent( data: json_encode(['n' => $i, 'at' => time()]), event: 'tick', id: (string) $i, ); if (!$res->sendable()) { // client is gone, no point waiting break; } delay(1000); } $res->sseEvent('bye'); $res->end(); }); $server->start(); ``` Browser side: ```js const es = new EventSource('/events'); es.onmessage = e => console.log('message', e.data); es.addEventListener('tick', e => console.log('tick', e.data, e.lastEventId)); ``` ## sseStart() Switches the response into SSE mode and locks in the headers: `Content-Type: text/event-stream`, `Cache-Control: no-cache, no-transform`, and `X-Accel-Buffering: no` (the last one tells nginx not to buffer the response; without it, events stall behind the proxy buffer until it fills up). The response is also marked non-compressible: a buffering gzip stream would defeat the point of real-time delivery. The call is optional: the first `sseEvent()`/`sseComment()` starts the stream on its own. But `sseStart()` by itself does **not** flush the status line and headers onto the wire, the commit is lazy and happens on the first real event. To open the stream right away (for example, to unblock the browser's `onopen` before any real event is ready), send an empty `sseComment()`: that both starts the stream and commits the headers immediately. Throws `HttpServerInvalidArgumentException` if the handler already set its own `Content-Type`, and `HttpServerRuntimeException` if the response is already streaming, closed, or busy with `sendFile()`. ## sseEvent() ```php $res->sseEvent( ?string $data = null, ?string $event = null, ?string $id = null, ?int $retry = null, ): static ``` Formats and sends one SSE event, starting the stream if needed. Multiline `$data` is split on `\n` / `\r\n` / `\r` and sent as multiple `data:` fields (WHATWG §9.2). `$event`, `$id`, and `$retry` are included only when not `null`. The record ends with a blank line so the browser dispatches the event right away. * `$event` and `$id` must not contain `\r`/`\n` (otherwise the parser would read them as a field/record separator), and `$id` must not contain NUL (per WHATWG, a NUL makes the parser ignore the whole id): violations throw `HttpServerInvalidArgumentException`. * `$retry` must be non-negative. * An empty string `$data === ''` is valid too, it dispatches an empty `MessageEvent`. * All four arguments set to `null` is a no-op. The `EventSource` parser silently skips an event with neither `data` nor `retry`. ## sseComment() ```php $res->sseComment(string $text = ''): static ``` Sends a comment line (a record starting with `:`). Browsers ignore comments, but they keep the connection alive through the idle timeouts of intermediate proxies (nginx's `proxy_read_timeout`, 60s by default). Call it periodically as a heartbeat. The canonical payload is an empty string, which becomes `:\n\n` on the wire. `$text` must not contain `\r`/`\n`. ## sseRetry() ```php $res->sseRetry(int $milliseconds): static ``` Sends a `retry:` directive telling the browser how many milliseconds to wait before reconnecting after the stream drops. Sugar for `sseEvent(retry: $milliseconds)` with no payload. ## Backpressure: `sendable()` Like `send()`, every SSE method suspends the handler coroutine only under real backpressure, that is, when the stream's intermediate buffer is full. The `sendable()` check is non-blocking and advisory: `false` means the next call would suspend, the response is already closed, or this response type does not support streaming at all. Handy so you don't have to wait on a slow client when there is other work to do. ## See also * [`HttpResponse::sseStart()`](/en/docs/reference/server/http-response.html#ssestart) and the other SSE methods in the reference * [Streaming](/en/docs/server/streaming.html): the low-level `send()`/`sendable()` that SSE is built on * [Examples](/en/docs/server/examples.html#sse-server-sent-events) --- --- url: https://true-async.github.io/en/tutors-server/06-sse.md description: >- SSE: an event stream to the browser, import progress, and heartbeat via sseComment(). --- # Server-Sent Events So the address import now starts with a button on the page. The user clicked, the file went off, processing began. It runs for about ten minutes. What does the user see all that time? Right, a spinning spinner. For ten minutes. Without a single sign of life. Around the third minute they'll decide it's all frozen, refresh the page, and start the import a second time. We need a progress bar. The classic solution is polling: the browser hits `/import/progress` once a second, the server answers with a number. Does it work? It works. But do the math: six hundred requests in ten minutes, with headers, with a full processing cycle, and in each one the payload is a single number. What we'd rather have is the opposite: the browser connects once, and from then on the server itself sends the number whenever it changes. That's exactly what `SSE`, `Server-Sent Events`, does. No new protocol. An ordinary `HTTP` response that the server doesn't close but keeps appending events to. On the browser side they're received by the built-in `EventSource`, without a single library. ## Progress to the Browser ```php use function Async\delay; $server->addHttpHandler(function (HttpRequest $req, HttpResponse $res) { if ($req->getPath() !== '/import/progress') { /* ... routing ... */ } $import = currentImport(); // that same ImportService from the first series $res->sseStart(); $res->sseRetry(3000); // if the stream breaks, the browser reconnects in 3 seconds while (!$import->isFinished()) { $res->sseEvent( data: json_encode(['done' => $import->counter(), 'total' => $import->total()]), event: 'progress', ); if (!$res->sendable()) { break; // the tab was closed, nobody to draw for } delay(1000); } $res->sseEvent(event: 'finished', data: 'ok'); $res->end(); }); ``` And in the browser: ```js const es = new EventSource('/import/progress'); es.addEventListener('progress', e => { const {done, total} = JSON.parse(e.data); bar.style.width = (100 * done / total) + '%'; }); es.addEventListener('finished', () => es.close()); ``` Look closely at the handler's structure. A loop. Taking a reading. `delay(1000)`. Ring a bell? It's the progress coroutine from the very first chapter, letter for letter. Only the last line changed: instead of `echo` to the terminal, `sseEvent()` into an open response. Fifteen chapters later the progress bar reached the browser, and the code barely changed. And, as back then, progress knows nothing about the import, and the import knows nothing about progress. There's no magic under the hood either: the SSE methods are thin formatting over `send()` from chapter four. From that, two free gifts. Backpressure: a slow tab won't eat the server's memory. And protocol independence: the same handler works over `HTTP/1.1`, `HTTP/2`, and `HTTP/3`, and the browser chooses. ## Long Silence and the Heartbeat An `SSE` connection lives for minutes and hours. Long life, as usual, has its own ailments. There are two here. The first: the write timeout. By default the server limits how long a response may take to send, and that's correct. But an SSE response is "being sent" forever, that's its nature. For a service with streams you turn the limit off: ```php $config->setWriteTimeout(0); ``` The second ailment is subtler. Suppose the import stalls for a long time: it's computing something heavy, no events are going out. For us it's a pause, but for some proxy between the server and the browser it's a dead connection that's due to be killed. And it will kill it, nginx defaults to sixty seconds for this. The cure is comical in its simplicity: ```php $res->sseComment(); // on the wire: ":\n\n" ``` A comment. An event the browser silently ignores, but which travels through the wire and convinces every intermediary that the connection is alive. Send it on a timer during pauses, and the stream will survive any silence. ## Breaks and Recovery The mobile network blinked, the stream broke. What to do? Nothing. Seriously: `EventSource` restores the connection itself, after waiting the interval from `sseRetry()`. The one thing worth helping it with is not starting from a blank slate. Give events numbers: ```php $res->sseEvent(data: $update, id: (string) $sequence); ``` On reconnect the browser sends a `Last-Event-ID` header with the last number it received, and the handler continues from exactly that point: ```php $since = (int) ($req->getHeader('last-event-id') ?? 0); foreach (updatesSince($since) as $sequence => $update) { $res->sseEvent(data: $update, id: (string) $sequence); } ``` Delivery, reconnection, catching up on what was missed. An honest notification channel, and all of it over the most ordinary HTTP. With one caveat: the channel is one-way. The server speaks, the browser listens. For a progress bar that's ideal. But for a support chat, where both sides write? For a chat you'll need something more serious. --- --- url: https://true-async.github.io/en/docs/reference/signal.md description: signal() — wait for an OS signal with cancellation support via Completable. --- # signal (PHP 8.6+, True Async 1.0) `signal()` — Waits for an OS signal. Returns a `Future` that resolves with a `Signal` value when the signal is received. ## Description ```php signal(Async\Signal $signal, ?Async\Completable $cancellation = null): Async\Future ``` Creates a one-shot OS signal handler. Each call to `signal()` creates a new `Future` that resolves upon the first receipt of the specified signal. If the `$cancellation` parameter is provided, the `Future` will be rejected when the cancellation triggers (e.g., on timeout). Multiple calls to `signal()` with the same signal work independently — each will receive a notification. ## Parameters **`signal`** An `Async\Signal` enum value specifying the expected signal. For example: `Signal::SIGINT`, `Signal::SIGTERM`, `Signal::SIGUSR1`. **`cancellation`** An optional object implementing `Async\Completable` (e.g., a result of calling `timeout()`). If the cancellation object triggers before the signal arrives, the `Future` will be rejected with the corresponding exception (e.g., `Async\TimeoutException`). If the cancellation object has already completed at the time of the call, `signal()` immediately returns a rejected `Future`. ## Return Values Returns `Async\Future`. When the signal is received, the `Future` resolves with the `Async\Signal` enum value corresponding to the received signal. ## Errors/Exceptions * `Async\OperationCanceledException` — if the cancellation token triggered (including timeout). The original exception from the token is available via `$e->getPrevious()` (e.g., `TimeoutException` when using `timeout()`). ## Examples ### Example #1 Waiting for a signal with timeout ```php name . "\n"; } catch (Async\OperationCanceledException $e) { echo "Signal not received within 5 seconds\n"; } ?> ``` ### Example #2 Receiving a signal from another coroutine ```php name . "\n"; var_dump($result === Signal::SIGUSR1); // bool(true) ?> ``` ### Example #3 Graceful shutdown on SIGTERM ```php ``` ### Example #4 Already expired timeout ```php getPrevious()) . "\n"; // Async\TimeoutException } ?> ``` ## Notes > **Note:** Each call to `signal()` creates a **one-shot** handler. To wait for the same signal again, call `signal()` again. > **Note:** `Signal::SIGINT` and `Signal::SIGBREAK` work on all platforms, including Windows. Signals `SIGUSR1`, `SIGUSR2`, and other POSIX signals are only available on Unix systems. > **Note:** `Signal::SIGKILL` and `Signal::SIGSEGV` cannot be caught — this is an operating system limitation. ## Signal The `Async\Signal` enum defines the available OS signals: | Value | Signal | Description | |-------|--------|-------------| | `Signal::SIGHUP` | 1 | Terminal connection lost | | `Signal::SIGINT` | 2 | Interrupt (Ctrl+C) | | `Signal::SIGQUIT` | 3 | Quit with core dump | | `Signal::SIGILL` | 4 | Illegal instruction | | `Signal::SIGABRT` | 6 | Abnormal termination | | `Signal::SIGFPE` | 8 | Floating-point arithmetic error | | `Signal::SIGKILL` | 9 | Unconditional termination | | `Signal::SIGUSR1` | 10 | User-defined signal 1 | | `Signal::SIGSEGV` | 11 | Memory access violation | | `Signal::SIGUSR2` | 12 | User-defined signal 2 | | `Signal::SIGTERM` | 15 | Termination request | | `Signal::SIGBREAK` | 21 | Break (Ctrl+Break, Windows) | | `Signal::SIGABRT2` | 22 | Abnormal termination (alternative) | | `Signal::SIGWINCH` | 28 | Terminal window size change | ## See Also * [timeout()](/en/docs/reference/timeout.html) — create a timeout to limit waiting * [await()](/en/docs/reference/await.html) — waiting for a Future result * [graceful\_shutdown()](/en/docs/reference/graceful-shutdown.html) — graceful scheduler shutdown * [Cancellation](/en/docs/components/cancellation.html) — cancellation mechanism --- --- url: https://true-async.github.io/en/docs/reference/spawn.md description: >- spawn() — launch a function in a new coroutine. Full documentation: parameters, return value, examples. --- # spawn (PHP 8.6+, True Async 1.0) `spawn()` — Launches a function for execution in a new coroutine. Creates a coroutine. ## Description ```php spawn(callable $callback, mixed ...$args): Async\Coroutine ``` Creates and starts a new coroutine. The coroutine will be executed asynchronously. ## Parameters **`callback`** A function or closure to execute in the coroutine. Can be any valid callable type. **`args`** Optional parameters passed to `callback`. Parameters are passed by value. ## Return Values Returns an `Async\Coroutine` object representing the launched coroutine. The object can be used to: * Obtain the result via `await()` * Cancel execution via `cancel()` * Check the coroutine's state ## Examples ### Example #1 Basic usage of spawn() ```php ``` ### Example #2 Multiple coroutines ```php ``` ### Example #3 Using with a closure ```php json_decode($userData), 'orders' => json_decode($userOrders) ]; }); $data = await($coroutine); print_r($data); ?> ``` ### Example #4 spawn with Scope ```php spawn(function() { echo "Coroutine 1\n"; }); $scope->spawn(function() { echo "Coroutine 2\n"; }); // Wait for all coroutines in the scope to complete $scope->awaitCompletion(Async\timeout(5000)); ?> ``` ### Example #5 Passing parameters ```php ``` ### Example #6 Error handling One way to handle an exception from a coroutine is to use the `await()` function: ```php getMessage(); } ?> ``` ## Notes > **Note:** Coroutines created via `spawn()` execute concurrently, but not in parallel. > PHP TrueAsync uses a single-threaded execution model. > **Note:** Parameters are passed to the coroutine by value. > To pass by reference, use a closure with `use (&$var)`. ## Changelog | Version | Description | |----------|---------------------------------| | 1.0.0 | Added the `spawn()` function | ## See Also * [await()](/en/docs/reference/await.html) - Waiting for a coroutine result * [suspend()](/en/docs/reference/suspend.html) - Suspending coroutine execution --- --- url: https://true-async.github.io/en/docs/reference/spawn-thread.md description: >- spawn_thread() — run a closure in a new OS thread. Full documentation: parameters, data transfer, exceptions, examples. --- # spawn\_thread (PHP 8.6+, True Async 1.0) `spawn_thread()` — runs a closure in a **separate parallel thread** with its own isolated PHP environment. Returns an `Async\Thread` that implements `Completable`, so the thread can be awaited via `await()`. ## Description ```php Async\spawn_thread( \Closure $task, bool $inherit = true, ?\Closure $bootloader = null ): Async\Thread ``` Creates a new OS thread, starts a separate PHP request inside it, optionally runs `$bootloader`, then executes `$task`. The value returned from `$task` becomes the thread's result and is accessible via `await()` or `Thread::getResult()`. ## Parameters **`task`** : The closure executed in the receiver thread. Can capture variables via `use (...)` — they are deep-copied through shared memory at thread creation and come alive in the receiver thread's memory. **`inherit`** : Reserved for future use. The parameter is accepted but does not currently affect thread behavior — the receiver thread always starts with a fresh, isolated environment. The flag will remain in the signature until inheritance of classes and functions from the parent is supported. **`bootloader`** : An optional closure executed **first** in the receiver thread, before the variables from `use(...)` of the main `$task` are loaded. Used to prepare the thread environment: registering autoloaders, declaring classes, initializing ini settings, loading libraries. The bootloader takes no parameters; its return value is ignored. ## Return Value An `Async\Thread` object representing the running thread. Implements `Completable`, so it can be used with `await()`, `await_all()`, `await_any()`, `Task\Group`, and so on. ## Exceptions * `Async\ThreadTransferException` — thrown **in the parent** if one of the variables captured via `use(...)` contains a non-transferable type (`stdClass` with dynamic properties, a PHP reference, a resource, etc.). * `Async\RemoteException` — thrown on `await()` if `$task` completed with an error. Wraps the original exception; `getRemoteClass()` and `getRemoteException()` provide access to the details. ## Examples ### Example #1. Heavy work in a separate thread ```php $obj]; $thread = spawn_thread( task: function() use ($obj, $meta) { // The same instance in two variables from use(...) echo "same: ", ($obj === $meta['ref'] ? "yes" : "no"), "\n"; // Mutation through one reference is visible through another $obj->name = 'staging'; echo "meta: ", $meta['ref']->name, "\n"; return $obj->name; }, bootloader: $boot, ); echo "result: ", await($thread), "\n"; }); ``` ``` same: yes meta: staging result: staging ``` Object identity is preserved across different variables captured by the same closure via `use(...)`. ### Example #3. Exception handling ```php getRemoteClass(), "\n"; $original = $e->getRemoteException(); if ($original !== null) { echo "original: ", $original->getMessage(), "\n"; } } }); ``` ``` remote class: RuntimeException original: boom ``` ### Example #4. Passing a non-transferable type ```php x = 1; try { $thread = spawn_thread(function() use ($obj) { return 'unreachable'; }); await($thread); } catch (Async\ThreadTransferException $e) { echo $e->getMessage(), "\n"; } }); ``` ``` Cannot transfer object with dynamic properties between threads (class stdClass). Use arrays instead ``` The exception is thrown **in the parent** during variable copying from `use(...)` — the receiver thread did not even start. ### Example #5. Returning a result via FutureState If you need to "wake up" a parent `Future` directly from a parallel thread (for example, so that the same event can be awaited from different places in the main coroutine) — pass a `FutureState`: ```php complete($data); }); // The event will arrive in the parent via $future when the thread calls $state->complete() $result = await($future); echo "got: ", $result, "\n"; await($thread); echo "thread done\n"; }); ``` ``` got: computed in thread thread done ``` A `FutureState` can be passed to `spawn_thread` **only once** — attempting to pass the same state to a second thread will throw an exception during transit. ## Notes * **Closure type** — `$task` must be a `\Closure`. Other callable types (`[object, 'method']`, string function names) are not accepted — the transfer mechanism can only transport `Closure`. * **`use` with `&` (by-reference)** — rejected. A shared reference between threads makes no sense. * **User-defined classes** are not inherited into the receiver thread automatically. If `$task` uses a class declared in the parent script, it must be made available in the thread via `bootloader` (load via autoload or declare via `eval`). * **Static properties of functions and classes** in the receiver thread are its own — any changes remain inside the thread and do not leak out. ## See Also * [`Async\Thread`](/en/docs/components/threads.html) — component documentation * [`Async\ThreadChannel`](/en/docs/components/thread-channels.html) — channels between threads * [`await()`](/en/docs/reference/await.html) — awaiting a result * [`spawn()`](/en/docs/reference/spawn.html) — launching a coroutine (not a thread) --- --- url: https://true-async.github.io/en/docs/reference/spawn-with.md description: spawn_with() — launch a coroutine in a specified Scope or via a ScopeProvider. --- # spawn\_with (PHP 8.6+, True Async 1.0) `spawn_with()` — Launches a function in a new coroutine bound to the specified `Scope` or `ScopeProvider`. ## Description ```php spawn_with(Async\ScopeProvider $provider, callable $task, mixed ...$args): Async\Coroutine ``` Creates and starts a new coroutine in the Scope provided by `$provider`. This allows explicit control over which Scope the coroutine will run in. ## Parameters **`provider`** An object implementing the `Async\ScopeProvider` interface. Typically this is: * `Async\Scope` — directly, since `Scope` implements `ScopeProvider` * A custom class implementing `ScopeProvider` * An object implementing `SpawnStrategy` for lifecycle management **`task`** A function or closure to execute in the coroutine. **`args`** Optional parameters passed to `task`. ## Return Values Returns an `Async\Coroutine` object representing the launched coroutine. ## Errors/Exceptions * `Async\AsyncException` — if the Scope is closed or cancelled * `TypeError` — if `$provider` does not implement `ScopeProvider` ## Examples ### Example #1 Launching in a specific Scope ```php awaitCompletion(Async\timeout(5000)); ?> ``` ### Example #2 Inherited Scope ```php cancel(); ?> ``` ### Example #3 Using with ScopeProvider ```php scope = new Scope(); $this->scope->setExceptionHandler(function(\Throwable $e) { error_log("Worker error: " . $e->getMessage()); }); } public function provideScope(): Scope { return $this->scope; } public function shutdown(): void { $this->scope->disposeSafely(); } } $worker = new WorkerScope(); spawn_with($worker, function() { // Working in a managed scope }); $worker->shutdown(); ?> ``` ### Example #4 Passing arguments ```php ``` ## Notes > **Note:** If `ScopeProvider::provideScope()` returns `null`, the coroutine is created in the current Scope. > **Note:** You cannot create a coroutine in a closed or cancelled Scope — an exception will be thrown. ## See Also * [spawn()](/en/docs/reference/spawn.html) — launch a coroutine in the current Scope * [Scope](/en/docs/components/scope.html) — managing coroutine lifetimes * [Interfaces](/en/docs/components/interfaces.html) — ScopeProvider and SpawnStrategy --- --- url: https://true-async.github.io/en/tutors-laravel/03-sse-grpc.md description: >- trueasync_response(), Sse, and grpc_handlers: reaching past the buffered Illuminate Response right from a controller. --- # SSE and gRPC with Laravel The `TrueAsyncServer` inside `laravel-spawn` is built simply: it accepts an `HttpRequest`, assembles an `Illuminate\Http\Request` from it, runs it through `Kernel::handle()`, gets an `Illuminate\Http\Response`, buffers it whole into an `HttpResponse`, and sends it. One request, one response, all the content at once. Exactly what Laravel has been used to throughout its entire history. But the second series on this site had a progress bar over `SSE` and `gRPC` on the same port, and both live not by the "one response" formula but by the "stream of messages" formula. A buffered `Illuminate\Response` isn't built for them: it has neither `sseEvent()` nor `writeMessage()`. So a controller needs a way to reach the real, raw `HttpResponse`, bypassing the buffer. ## A Raw Response On Demand `laravel-spawn` places the current request's `HttpRequest` and `HttpResponse` into `request_context()` before handing control over to Laravel's router, the same trick Laravel itself uses to put `auth` and `session` there, back in the first chapter. You can retrieve them with two functions: ```php trueasync_request(); // TrueAsync\HttpRequest trueasync_response(); // TrueAsync\HttpResponse ``` Once a controller has written to the raw response and closed it itself (`$res->end()`), the usual `Illuminate\Response` path is no longer needed: `TrueAsyncServer` checks `isClosed()` before buffering, and if the response has already been sent manually, it doesn't add anything on top. The controller is still required to return something, Laravel demands it, but nobody cares about the content of that return value anymore. ## SSE: A Progress Bar Inside a Laravel Route Reaching for `sseStart()`/`sseEvent()`/`sseComment()` through `trueasync_response()` every time is inconvenient, so the package ships a thin wrapper: ```php use Spawn\Laravel\Sse\Sse; use function Async\delay; Route::get('/import/progress', function () { Sse::start(retryMs: 3000); $import = currentImport(); while (!$import->isFinished()) { Sse::event( data: json_encode(['done' => $import->counter(), 'total' => $import->total()]), event: 'progress', ); if (!Sse::connected()) { break; // the tab was closed } delay(1000); } Sse::event(event: 'finished', data: 'ok'); Sse::end(); return response()->noContent(); }); ``` Recognize this code? It's the progress bar from chapter six of the server series, word for word, with `$res->sseEvent()` swapped for `Sse::event()`. Inside the route you still have `Auth::user()`, `session()`, `Eloquent`, all available, this is an ordinary Laravel handler, it just answers with a stream instead of once. Middleware, authorization through `Route::middleware('auth:sanctum')`, `current_context()` for per-request state from the first chapter, all of it works as usual, because `Kernel::handle()` around this code hasn't changed by a single line. One configuration detail isn't about Laravel at all, it's about the server itself: a long-lived stream shouldn't be cut off by the write timeout that ordinary responses actually need, so services with streams turn it off globally, `ASYNC_WRITE_TIMEOUT=0` in `.env`, the same lever used in the production chapter of the server series. ## gRPC: A Contract Instead Of A Route With `gRPC`, the compromise is harsher. The protocol talks in protobuf-encoded messages, which have no meaningful mapping onto either `Illuminate\Http\Request` or, even less so, `Response`. Fabricating a fake HTTP request just to push it through Laravel's routing and middleware would be pointless: a gRPC client sends neither cookies, nor a CSRF token, nor a form body. So `gRPC` in `laravel-spawn` bypasses the `Kernel` entirely, taking a separate path configured through the config file: ```php // config/async.php 'grpc_handlers' => [ '/profile.ProfileService/GetProfile' => [ \App\Grpc\ProfileServiceHandler::class, 'getProfile', ], ], ``` The handler itself is resolved through the container (so ordinary constructor DI still works), and the method signature is the same pair of raw objects seen back in chapter ten of the server series: ```php namespace App\Grpc; use Profile\GetProfileRequest; use Profile\Profile; use TrueAsync\HttpRequest; use TrueAsync\HttpResponse; class ProfileServiceHandler { public function __construct(private readonly UserRepository $users) {} public function getProfile(HttpRequest $req, HttpResponse $res): void { $getProfile = new GetProfileRequest(); $getProfile->mergeFromString($req->readMessage()); $user = $this->users->find($getProfile->getUserId()); $profile = (new Profile()) ->setId($user->id) ->setName($user->name) ->setRegion($user->region); $res->writeMessage($profile->serializeToString()); // a plain return: the server appends grpc-status: 0 (OK) itself } } ``` `$this->users` arrived through the constructor, like in any Laravel service: the container and DI are still fully in play, even though Laravel's routing never touched this path at all. Errors travel the same way they do in plain `TrueAsync Server`: through a `grpc-status` trailer, not an HTTP status code. ```php public function getProfile(HttpRequest $req, HttpResponse $res): void { if (!$this->authorized($req)) { $res->setTrailer('grpc-status', '7'); // PERMISSION_DENIED return; } // ... } ``` An exception thrown out of a handler is turned by `laravel-spawn` itself into `grpc-status: 13` (`INTERNAL`), the same behavior the bare server had, just now tucked away inside the adapter. ## What's Real Here And What Isn't Neither path is an emulation or a hack layered over Symfony's `StreamedResponse`, both are direct access to the very same `HttpRequest`/`HttpResponse` primitives from the server series, right from under a Laravel route. The cost is predictable: an SSE handler doesn't answer through the familiar `return response()->json(...)`, it writes itself and decides for itself when to close the connection; a gRPC handler doesn't see Laravel's middleware or routing at all, only the container for DI. There's no black magic here, but no pretending either, if you need a real stream, you have to step out of the cozy world of the buffered `Response` into the place where the server itself lives. We've covered what Laravel can do: ordinary requests, streams, gRPC. What's left is what Laravel can't do on its own, and what deserves close scrutiny in your own code and other people's before you hand it over to concurrent coroutines. That's where we head in the next chapter. --- --- url: https://true-async.github.io/en/tutors-server/05-static.md description: >- StaticHandler: serving files without a PHP coroutine, caching, and security policies. --- # Static Files `ProfileService` has grown a frontend. HTML, CSS, scripts, avatars, the usual. They used to be served by nginx, but we ceremonially saw nginx off in the first chapter. Who serves them now? The first impulse is understandable: write a handler that opens files by URL. Stop. Consider what that means: thousands of requests for the logo a day, and for each one a coroutine, entry into PHP, `fopen`, exit. PHP here does nothing that requires PHP. The server solves this radically. A static route is served entirely in C. A request to it never enters PHP at all: ```php use TrueAsync\StaticHandler; $server->addStaticHandler( new StaticHandler('/assets/', '/var/www/profile/public') ); ``` A URL prefix, a directory on disk, done. `GET /assets/css/app.css` turns into an asynchronous read of the file straight into the socket, through libuv, past PHP. The handlers from previous chapters keep receiving everything else. There can be several mounts, and matches are searched in registration order. All the HTTP etiquette from `sendFile` is here too: `Content-Type`, `ETag` with 304, resumed downloads via `Range`. ## Configuring a Mount `StaticHandler` is configured with a chain, before it's attached to the server: ```php $static = (new StaticHandler('/assets/', '/var/www/profile/public')) ->setCacheControl('public, max-age=86400') ->enablePrecompressed('br', 'zstd', 'gzip') ->hide('*.map', 'drafts/**') ->setOnMissing(StaticOnMissing::NEXT); $server->addStaticHandler($static); ``` Let's go line by line. **`setCacheControl`** — the caching header on every response. Paired with the `ETag` that's on by default, the browser re-downloads a file only when the file has actually changed. **`enablePrecompressed`** — my favorite item. If `app.css.br` sits next to `app.css`, a client with a suitable `Accept-Encoding` gets the ready-made compressed file. Think about the economics: you compress once at the frontend build stage, at the most expensive and highest quality level, and serve it a million times without spending a single cycle on compression. **`hide`** — globs that get a 404 regardless of whether the file exists. Source maps and drafts won't go out. **`setOnMissing(NEXT)`** — the fate of requests that miss a file. By default a miss answers 404 straight from C. `NEXT` instead passes the request on, to an ordinary PHP handler. Why? SPA. The file `/assets/app.js` is served from disk, while a nonexistent `/assets/whatever` falls through into the application, which answers with its own `index.html`. After `addStaticHandler` the object is locked: the server has already built its hot-path structures from it. An attempt to touch a setter after that is an exception. ## Secure by Default A small digression. Serving files by URL is historically one of the most bountiful holes in web servers. `../../etc/passwd` in the address bar is a trick older than many readers of this chapter. So the out-of-the-box policy is paranoid. Requests with `..` get a 404\. Paths through files with a leading dot get a 404: neither `.env` nor `.git` will leak, even if they accidentally end up in the directory. Symbolic links aren't dereferenced at all: the file must physically lie inside the mount root, and no symlink can drag it out. All of this can be relaxed deliberately (`setDotfilePolicy`, `setSymlinkPolicy`), but the defaults are chosen so that the "plug it in and forget" option is safe. ## When There Are a Lot of Files For hot mounts there's one more lever: ```php $static->setOpenFileCache(maxEntries: 1024, ttlSeconds: 60); ``` The cache remembers the resolved path, metadata, and headers of the most recent files and cuts the syscall walk on repeat requests. On a large directory or a network filesystem it's noticeable. On a small local site it's not, which is why it's off by default. That's the whole chapter, I promised a short one. Static flies past PHP, and PHP gets on with its work. And now that the service has a face, it's time to bring it to life. Remember the progress bar from the very first chapter of the first series, the one drawn in the terminal? It's about to move into the browser. And the server itself will draw it. --- --- url: https://true-async.github.io/en/docs/server/static-files.md description: >- StaticHandler: built-in static delivery without a PHP handler. sendFile(): handler-driven file delivery. Precompressed sidecars, ETag, Range, security policies. --- # Static files and sendFile (PHP 8.6+, true\_async\_server 0.6+) TrueAsync Server has two independent file-delivery mechanisms: 1. **`StaticHandler`** — a separate prefix mount served **entirely in C**, without spawning a coroutine or entering the PHP VM. 2. **`HttpResponse::sendFile()`** — handler-driven delivery. The PHP code makes the decision (auth, ACL, name generation), and the server picks up the file from disk and sends it. Both use the same engine FSM (MIME, ETag, IMF-date, Range, conditional GET, precompressed sidecars). ## StaticHandler ```php use TrueAsync\HttpServer; use TrueAsync\HttpServerConfig; use TrueAsync\StaticHandler; $server = new HttpServer( (new HttpServerConfig())->addListener('0.0.0.0', 8080) ); $static = (new StaticHandler('/static/', '/var/www/public')) ->setIndexFiles('index.html') ->enablePrecompressed('br', 'gzip') ->setCacheControl('public, max-age=31536000, immutable') ->setEtagEnabled(true); $server->addStaticHandler($static); $server->addHttpHandler(function ($req, $res) { $res->setStatusCode(200)->setBody('dynamic route'); }); $server->start(); ``` Requests to `/static/...` are served by `StaticHandler` (no PHP handler is invoked). Everything else flows through the regular `addHttpHandler`. Multiple mounts are matched **in registration order**. After attach, `StaticHandler` is locked; any setter on it throws `HttpServerRuntimeException`. ### Index and fallthrough ```php $static ->setIndexFiles('index.html', 'index.htm') // what to serve on a directory URL ->disableIndex() // or no index lookup at all ->setOnMissing(StaticOnMissing::NEXT); // → hand off to the PHP handler ``` **`StaticOnMissing`** controls what happens when a file is not found inside the root: | Value | Behaviour | |-------|-----------| | `NOT_FOUND` (default) | 404 in C, the request never reaches the PHP VM | | `NEXT` | Control is handed back to the dispatcher and a normal handler coroutine is spawned | > A request for a directory URL without a trailing slash, where all index files 404, returns 404. > The 301 redirect that nginx/Apache emit is **not** produced by this handler. If your deployment > relies on a catch-all on directory paths, disable index lookup: `setIndexFiles([])` / > `disableIndex()`. ### Precompressed sidecars ```php $static->enablePrecompressed('br', 'gzip', 'zstd'); ``` When the client sends `Accept-Encoding: br, gzip`, the handler looks for `main.css.br` next to `main.css` and serves the sidecar directly, with no CPU cost for encoding. Supported names: `"br"`, `"gzip"`, `"zstd"`. An unknown name throws `InvalidArgumentException` from the setter. ### Security policies ```php use TrueAsync\StaticDotfiles; use TrueAsync\StaticSymlinks; $static ->setDotfilePolicy(StaticDotfiles::DENY) ->setSymlinkPolicy(StaticSymlinks::REJECT) ->hide('*.bak', '*.tmp', 'private/**'); ``` **`StaticDotfiles`**: | | Behaviour | |---|-----------| | `DENY` (default) | 404 on any path containing a segment that starts with `.` (including `..`) | | `ALLOW` | dotfiles are served as regular files | | `IGNORE` | as if the file did not exist (passthrough governed by `StaticOnMissing`) | **`StaticSymlinks`**: | | Behaviour | |---|-----------| | `REJECT` (default) | 404 on any symlink in the path. `O_NOFOLLOW` + per-segment `lstat` — a symlink is never traversed | | `FOLLOW` | symlinks are followed; the post-`realpath()` target must stay inside the root | | `OWNER_MATCH` | follow only when the symlink and its target share the same uid | `hide($glob, ...)` defines glob patterns that return 404 regardless of whether the file exists. The comparison is **relative to the root** and uses `/` as the separator. ### Cache / headers ```php $static ->setEtagEnabled(true) // W/"…" from (mtime_ns, size, ino) ->setCacheControl('public, max-age=31536000, immutable') ->setHeader('Strict-Transport-Security', 'max-age=63072000') ->setOpenFileCache(maxEntries: 1024, ttlSeconds: 60); ``` **Open-file cache** (nginx style): caches resolved path, fstat metadata, MIME, ETag, and Last-Modified for the last N requests. Within `ttlSeconds`, repeat requests hit the cache and skip the realpath/stat/MIME walk. Disabled by default. It pays off on cold dentry caches / large docroots / network filesystems. On a warm-dentry local disk, syscalls already cost sub-microseconds, so the HashTable-lookup overhead eats the win. ### MIME override ```php $static->setMimeType('webmanifest', 'application/manifest+json'); ``` Extension without a leading dot, lowercased. ### Performance Since 0.4.0 in the engine: * **Inline `open(2)`/`fstat(2)`** (issue #13): no futex round trip through the libuv thread pool. Wins: H1 tiny 256B 19k → 35k req/s, H1 304 If-None-Match 24k → 123k req/s. * **Small-file fast path** (≤ 64 KiB): the file is slurped into a `zend_string` and sent with a single `writev(headers + body)`. Wins: H1 tiny → 103k req/s (×2.9), H2 tiny → 154k (×4.4). * Files > 64 KiB go through sendfile. ## sendFile from a handler ```php use TrueAsync\SendFileOptions; use TrueAsync\SendFileDisposition; $server->addHttpHandler(function ($req, $res) { $userId = (int) $req->getQueryParam('id'); if (!isAuthorized($userId)) { $res->setStatusCode(403); return; } $res->sendFile('/var/storage/reports/2026-Q1.pdf', new SendFileOptions( contentType: 'application/pdf', disposition: SendFileDisposition::ATTACHMENT, downloadName: 'Q1-report.pdf', cacheControl: 'private, no-store', acceptRanges: true, conditional: true, precompressed: false, )); }); ``` `sendFile()` **records** the path + options on the response and **returns immediately**. The file transfer happens in the dispose phase through the same FSM as `StaticHandler`. The compression middleware is bypassed for sendFile (it has its own delivery pipeline). After `sendFile()` the response is **sealed**: `setHeader` / `setStatus*` / `write` / `send` / `setBody` / `json` / `html` / `redirect` / `end` and a repeat `sendFile()` all throw `HttpServerRuntimeException`. The path is treated as **trusted**: the handler already made the access decision. open/fstat errors (`ENOENT`, `EACCES`, oversize, non-regular) produce a 500, because the headers are not on the wire yet. ### SendFileOptions A `final readonly class` with named arguments in the constructor: | Field | Type | Default | What it does | |-------|------|---------|--------------| | `contentType` | `?string` | `null` | override MIME; `null` means auto-derive from the extension | | `disposition` | `SendFileDisposition` | `INLINE` | `INLINE` or `ATTACHMENT` | | `downloadName` | `?string` | `null` | filename for `Content-Disposition: attachment; filename=...` | | `cacheControl` | `?string` | `null` | literally placed into `Cache-Control` | | `etag` | `bool` | `true` | emit weak ETag | | `lastModified` | `bool` | `true` | emit `Last-Modified` | | `acceptRanges` | `bool` | `true` | support `Range:` | | `precompressed` | `bool` | `true` | look for `.br`/`.gz`/`.zst` sidecars | | `conditional` | `bool` | `true` | If-Modified-Since / If-None-Match → 304 | | `deleteAfterSend` | `bool` | `false` | unlink after successful send (for one-shot downloads) | | `status` | `?int` | `null` | override the HTTP status (for example, to answer a CDN with 200) | > The HTTP/3 path for `sendFile()` is still in progress: the dispose hook returns 500 on H3. ## See also * [`TrueAsync\StaticHandler`](/en/docs/reference/server/static-handler.html) * [`TrueAsync\SendFileOptions`](/en/docs/reference/server/send-file-options.html) * [`HttpResponse::sendFile()`](/en/docs/reference/server/http-response.html#sendfile) * [Compression](/en/docs/server/compression.html) --- --- url: https://true-async.github.io/en/docs/evidence/real-world-statistics.md description: >- Real-world statistical data for concurrency calculation: SQL queries, DB latencies, PHP framework throughput. --- # Statistical Data for Concurrency Calculation The formulas from the [IO-bound Task Efficiency](/en/docs/evidence/concurrency-efficiency.html) section operate on several key quantities. Below is a collection of real-world measurements that allow you to plug concrete numbers into the formulas. *** ## Formula Elements Little's Law: $$ L = \lambda \cdot W $$ * `L` — the required level of concurrency (how many tasks simultaneously) * `λ` — throughput (requests per second) * `W` — average time to process one request Goetz's formula: $$ N = N\_{cores} \times \left(1 + \frac{T\_{io}}{T\_{cpu}}\right) $$ * `T_io` — I/O wait time per request * `T_cpu` — CPU computation time per request For practical calculation, you need to know: 1. **How many SQL queries are executed per HTTP request** 2. **How long one SQL query takes (I/O)** 3. **How long CPU processing takes** 4. **What the server throughput is** 5. **What the overall response time is** *** ## 1. SQL Queries per HTTP Request The number of database calls depends on the framework, ORM, and page complexity. | Application / Framework | Queries per page | Source | |-------------------------------------|------------------------|------------------------------------------------------------------------------------------------------------------| | WordPress (no plugins) | ~17 | [Drupal Groups: How many queries per page](https://groups.drupal.org/node/12431) | | Symfony (Doctrine, average page) | <30 (profiler threshold) | [Symfony Docs: Profiler testing](https://symfony.com/doc/current/testing/profiling.html) | | Laravel (simple CRUD) | 5–15 | Typical values from Laravel Debugbar | | Laravel (with N+1 problem) | 20–50+ | [Laravel Daily: Debug Slow Queries](https://laraveldaily.com/post/laravel-eloquent-tools-debug-slow-sql-queries) | | Drupal (no cache) | 80–100 | [Drupal Groups](https://groups.drupal.org/node/12431) | | Magento (catalog) | 50–200+ | Typical for complex e-commerce | **Median for a typical ORM application: 15–30 queries per HTTP request.** Symfony uses a threshold of 30 queries as the "normal" boundary — when exceeded, the profiler icon turns yellow. ## 2. Time per SQL Query (T\_io per query) ### Query Execution Time on the DB Server Data from Percona's sysbench OLTP benchmarks (MySQL): | Concurrency | Share of queries <0.1 ms | 0.1–1 ms | 1–10 ms | >10 ms | |---------------|--------------------------|----------|---------|--------| | 1 thread | 86% | 10% | 3% | 1% | | 32 threads | 68% | 30% | 2% | <1% | | 128 threads | 52% | 35% | 12% | 1% | LinkBench (Percona, approximating real Facebook workload): | Operation | p50 | p95 | p99 | |---------------|--------|-------|--------| | GET\_NODE | 0.4 ms | 39 ms | 77 ms | | UPDATE\_NODE | 0.7 ms | 47 ms | 100 ms | **Source:** [Percona: MySQL and Percona Server in LinkBench](https://percona.com/blog/2013/05/08/mysql-and-percona-server-in-linkbench-benchmark/), [Percona: Query Response Time Histogram](https://www.percona.com/blog/query-response-time-histogram-new-feature-in-percona-server/) ### Network Latency (round-trip) | Scenario | Round-trip | Source | |-------------------------|------------|--------| | Unix-socket / localhost | <0.1 ms | [CYBERTEC PostgreSQL](https://www.cybertec-postgresql.com/en/postgresql-network-latency-does-make-a-big-difference/) | | LAN, single data center | ~0.5 ms | CYBERTEC PostgreSQL | | Cloud, cross-AZ | 1–5 ms | CYBERTEC PostgreSQL | | Cross-region | 10–50 ms | Typical values | ### Total: Full Time per SQL Query Full time = server-side execution time + network round-trip. | Environment | Simple SELECT (p50) | Average query (p50) | |-------------------|---------------------|---------------------| | Localhost | 0.1–0.5 ms | 0.5–2 ms | | LAN (single DC) | 0.5–1.5 ms | 1–4 ms | | Cloud (cross-AZ) | 2–6 ms | 3–10 ms | For a cloud environment, **4 ms per average query** is a well-grounded estimate. ## 3. CPU Time per SQL Query (T\_cpu per query) CPU time covers: result parsing, ORM entity hydration, object mapping, serialization. Direct benchmarks of this specific value are scarce in public sources, but can be estimated from profiler data: * Blackfire.io separates wall time into **I/O time** and **CPU time** ([Blackfire: Time](https://blackfire.io/docs/reference-guide/time)) * In typical PHP applications, the database is the main bottleneck, and CPU time constitutes a small fraction of wall time ([Datadog: Monitor PHP Performance](https://www.datadoghq.com/blog/monitor-php-performance/)) **Indirect estimate via throughput:** Symfony with Doctrine (DB + Twig rendering) processes ~1000 req/s ([Kinsta PHP Benchmarks](https://kinsta.com/blog/php-benchmarks/)). This means CPU time per request ≈ 1 ms. With ~20 SQL queries per page → **~0.05 ms CPU per SQL query**. Laravel API endpoint (Sanctum + Eloquent + JSON) → ~440 req/s ([Sevalla: Laravel Benchmarks](https://sevalla.com/blog/laravel-benchmarks/)). CPU time per request ≈ 2.3 ms. With ~15 queries → **~0.15 ms CPU per SQL query**. ## 4. Throughput (λ) of PHP Applications Benchmarks run on 30 vCPU / 120 GB RAM, nginx + PHP-FPM, 15 concurrent connections ([Kinsta](https://kinsta.com/blog/php-benchmarks/), [Sevalla](https://sevalla.com/blog/laravel-benchmarks/)): | Application | Page type | req/s (PHP 8.4) | |-------------|------------------------|-----------------| | Laravel | Welcome (no DB) | ~700 | | Laravel | API + Eloquent + Auth | ~440 | | Symfony | Doctrine + Twig | ~1,000 | | WordPress | Homepage (no plugins) | ~148 | | Drupal 10 | — | ~1,400 | Note that WordPress is significantly slower because each request is heavier (more SQL queries, more complex rendering). *** ## 5. Overall Response Time (W) in Production Data from LittleData (2023, 2,800 e-commerce sites): | Platform | Average server response time | |-------------------------|------------------------------| | Shopify | 380 ms | | E-commerce average | 450 ms | | WooCommerce (WordPress) | 780 ms | | Magento | 820 ms | **Source:** [LittleData: Average Server Response Time](https://www.littledata.io/average/server-response-time) Industry benchmarks: | Category | API response time | |-----------------------|-------------------| | Excellent | 100–300 ms | | Acceptable | 300–600 ms | | Needs optimization | >600 ms | ## Practical Calculation Using Little's Law ### Scenario 1: Laravel API in the Cloud **Input data:** * λ = 440 req/s (target throughput) * W = 80 ms (calculated: 20 SQL × 4 ms I/O + 1 ms CPU) * Cores: 8 **Calculation:** $$ L = \lambda \cdot W = 440 \times 0.080 = 35 \text{ concurrent tasks} $$ On 8 cores, that's ~4.4 tasks per core. This matches the fact that Laravel with 15 concurrent PHP-FPM workers already achieves 440 req/s. There is headroom. ### Scenario 2: Laravel API in the Cloud, 2000 req/s (target) **Input data:** * λ = 2000 req/s (target throughput) * W = 80 ms * Cores: 8 **Calculation:** $$ L = 2000 \times 0.080 = 160 \text{ concurrent tasks} $$ PHP-FPM cannot handle 160 workers on 8 cores — each worker is a separate process with ~30–50 MB of memory. Total: ~6–8 GB for workers alone. With coroutines: 160 tasks × ~4 KiB ≈ **640 KiB**. A difference of **four orders of magnitude**. ### Scenario 3: Using Goetz's Formula **Input data:** * T\_io = 80 ms (20 queries × 4 ms) * T\_cpu = 1 ms * Cores: 8 **Calculation:** $$ N = 8 \times \left(1 + \frac{80}{1}\right) = 8 \times 81 = 648 \text{ coroutines} $$ **Throughput** (via Little's Law): $$ \lambda = \frac{L}{W} = \frac{648}{0.081} \approx 8,000 \text{ req/s} $$ This is the theoretical ceiling with full utilization of 8 cores. In practice, it will be lower due to scheduler overhead, GC, connection pool limits. But even 50% of this value (4,000 req/s) is **an order of magnitude greater** than 440 req/s from PHP-FPM on the same 8 cores. ## Summary: Where the Numbers Come From | Quantity | Value | Source | |------------------------------------|------------------|-------------------------------------------| | SQL queries per HTTP request | 15–30 | WordPress ~17, Symfony threshold <30 | | Time per SQL query (cloud) | 3–6 ms | Percona p50 + CYBERTEC round-trip | | CPU per SQL query | 0.05–0.15 ms | Reverse calculation from throughput benchmarks | | Laravel throughput | ~440 req/s (API) | Sevalla/Kinsta benchmarks, PHP 8.4 | | E-commerce response time (average) | 450 ms | LittleData, 2,800 sites | | API response time (norm) | 100–300 ms | Industry benchmark | *** ## References ### PHP Framework Benchmarks * [Kinsta: PHP 8.5 Benchmarks](https://kinsta.com/blog/php-benchmarks/) — throughput for WordPress, Laravel, Symfony, Drupal * [Sevalla: Laravel Performance Benchmarks](https://sevalla.com/blog/laravel-benchmarks/) — Laravel welcome + API endpoint ### Database Benchmarks * [Percona: MySQL and Percona Server in LinkBench](https://percona.com/blog/2013/05/08/mysql-and-percona-server-in-linkbench-benchmark/) — p50/p95/p99 per operation * [Percona: Query Response Time Histogram](https://www.percona.com/blog/query-response-time-histogram-new-feature-in-percona-server/) — latency distribution at varying concurrency * [CYBERTEC: PostgreSQL Network Latency](https://www.cybertec-postgresql.com/en/postgresql-network-latency-does-make-a-big-difference/) — network latencies by environment * [PostgresAI: What is a slow SQL query?](https://postgres.ai/blog/20210909-what-is-a-slow-sql-query) — thresholds <10ms / >100ms ### Production System Response Times * [LittleData: Average Server Response Time](https://www.littledata.io/average/server-response-time) — 2,800 e-commerce sites ### PHP Profiling * [Blackfire.io: Time](https://blackfire.io/docs/reference-guide/time) — wall time breakdown into I/O and CPU * [Datadog: Monitor PHP Performance](https://www.datadoghq.com/blog/monitor-php-performance/) — APM for PHP applications --- --- url: https://true-async.github.io/en/docs/reference/supported-functions.md description: >- Complete list of PHP functions adapted for coroutine-aware non-blocking operation in TrueAsync. --- # Supported Functions TrueAsync adapts **70+ standard PHP functions** for non-blocking operation within coroutines. All listed functions automatically become asynchronous when called inside a coroutine. Outside of a coroutine, they work as usual. *** ## DNS | Function | Description | |----------|-------------| | `gethostbyname()` | Resolve hostname to IP address | | `gethostbyaddr()` | Reverse resolve IP address to hostname | | `gethostbynamel()` | Get list of IP addresses for hostname | *** ## Databases ### PDO MySQL | Function | Description | |----------|-------------| | `PDO::__construct()` | Non-blocking connection | | `PDO::prepare()` | Prepare statement | | `PDO::exec()` | Execute query | | `PDOStatement::execute()` | Execute prepared statement | | `PDOStatement::fetch()` | Fetch results | ### PDO PgSQL | Function | Description | |----------|-------------| | `PDO::__construct()` | Non-blocking connection | | `PDO::prepare()` | Prepare statement | | `PDO::exec()` | Execute query | | `PDOStatement::execute()` | Execute prepared statement | | `PDOStatement::fetch()` | Fetch results | ### PDO Connection Pooling Transparent connection pooling for PDO via `Async\Pool` integration. Each coroutine receives its own connection from the pool with automatic lifecycle management. ### MySQLi | Function | Description | |----------|-------------| | `mysqli_connect()` | Non-blocking connection | | `mysqli_query()` | Execute query | | `mysqli_prepare()` | Prepare statement | | `mysqli_stmt_execute()` | Execute prepared statement | | `mysqli_fetch_*()` | Fetch results | ### PostgreSQL (native) | Function | Description | |----------|-------------| | `pg_connect()` | Non-blocking connection | | `pg_query()` | Execute query | | `pg_prepare()` | Prepare statement | | `pg_execute()` | Execute prepared statement | | `pg_fetch_*()` | Fetch results | Each async context uses a separate connection for safe concurrency. *** ## CURL | Function | Description | |----------|-------------| | `curl_exec()` | Execute request | | `curl_multi_exec()` | Execute multiple requests | | `curl_multi_select()` | Wait for activity | | `curl_multi_getcontent()` | Get content | | `curl_setopt()` | Set options | | `curl_getinfo()` | Get request info | | `curl_error()` | Get error | | `curl_close()` | Close handle | *** ## Sockets | Function | Description | |----------|-------------| | `socket_create()` | Create socket | | `socket_create_pair()` | Create socket pair | | `socket_connect()` | Connect | | `socket_accept()` | Accept connection | | `socket_read()` | Read data | | `socket_write()` | Write data | | `socket_send()` | Send data | | `socket_recv()` | Receive data | | `socket_sendto()` | Send to address | | `socket_recvfrom()` | Receive from address | | `socket_bind()` | Bind to address | | `socket_listen()` | Listen | | `socket_select()` | Monitor socket activity | *** ## File and Stream I/O | Function | Description | |----------|-------------| | `fopen()` | Open file | | `fclose()` | Close file | | `fread()` | Read from file | | `fwrite()` | Write to file | | `fgets()` | Read line | | `fgetc()` | Read character | | `fgetcsv()` | Read CSV line | | `fputcsv()` | Write CSV line | | `fseek()` | Set position | | `ftell()` | Get position | | `rewind()` | Reset position | | `ftruncate()` | Truncate file | | `fflush()` | Flush buffers | | `fscanf()` | Formatted read | | `file_get_contents()` | Read entire file | | `file_put_contents()` | Write entire file | | `file()` | Read file into array | | `copy()` | Copy file | | `tmpfile()` | Create temporary file | | `readfile()` | Output file | | `fpassthru()` | Output remaining file | | `stream_get_contents()` | Read remaining stream | | `stream_copy_to_stream()` | Copy between streams | | `flock()` | File locking (via thread pool) | > **Important: `flock()` uses the thread pool.** > The `flock()` function is a blocking system call that cannot be made non-blocking via libuv I/O events. > Inside a coroutine, blocking lock operations (`LOCK_SH`, `LOCK_EX`) are offloaded to the libuv thread pool, > allowing other coroutines to continue executing while waiting for the lock. > Non-blocking locks (`LOCK_NB`) and unlocks (`LOCK_UN`) execute directly without the thread pool. > **Important: Timeouts for files and pipe streams.** > PHP does not natively support timeouts for file and pipe stream read/write operations — standard `fread()`, `fwrite()` and other functions can block indefinitely. > TrueAsync solves this: if a timeout is set for a stream (via `stream_set_timeout()`), > the read operation registers both an IO event and a timer simultaneously. If the timer fires before the IO completes, > the read is cancelled and the coroutine receives a result of `-1` (indicating a timeout). > This only works inside coroutines — outside coroutines, behavior remains standard. *** ## Stream Sockets | Function | Description | |----------|-------------| | `stream_socket_client()` | Create client connection | | `stream_socket_server()` | Create server socket | | `stream_socket_accept()` | Accept connection | | `stream_select()` | Monitor stream activity | | `stream_context_create()` | Create async-aware context | > **Limitation:** `stream_select()` with pipe streams (e.g. from `proc_open()`) is not supported on Windows. On Linux/macOS it works natively through the event loop. *** ## Process Execution | Function | Description | |----------|-------------| | `proc_open()` | Open process with pipes | | `proc_close()` | Close process | | `exec()` | Execute external command | | `shell_exec()` | Execute shell command | | `system()` | Execute system command | | `passthru()` | Execute with direct output | > **Important: `proc_close()` and `pclose()` block the coroutine.** > Calling `proc_close()` or `pclose()` waits for the child process to exit. > Inside a coroutine, this **blocks the current coroutine** until the process exits — other coroutines continue running. > The same happens on implicit calls via destructors: if a variable holding a process resource goes out of scope > or is destroyed by the garbage collector, the destructor calls `pclose()`, which blocks the coroutine where garbage collection is running. > > Recommendation: always close processes explicitly via `proc_close()` instead of relying on destructors, > so you can control which coroutine will be blocked. *** ## Timers and Delays | Function | Description | |----------|-------------| | `sleep()` | Delay in seconds | | `usleep()` | Delay in microseconds | | `time_nanosleep()` | Nanosecond precision delay | | `time_sleep_until()` | Wait until timestamp | *** ## Output Buffering Each coroutine receives an **isolated** output buffer. | Function | Description | |----------|-------------| | `ob_start()` | Start buffering | | `ob_flush()` | Flush buffer | | `ob_clean()` | Clean buffer | | `ob_get_contents()` | Get buffer contents | | `ob_end_clean()` | End buffering | *** ## Not Yet Supported Functions planned for implementation or not yet adapted. ### DNS | Function | Description | |----------|-------------| | `dns_check_record()` / `checkdnsrr()` | Check DNS record | | `dns_get_mx()` / `getmxrr()` | Get MX records | | `dns_get_record()` | Get DNS resource records | ### Databases | Extension | Description | |-----------|-------------| | PDO ODBC | ODBC driver | | PDO Oracle | Oracle driver | | PDO SQLite | SQLite driver | | PDO Firebird | Firebird driver | | MongoDB | MongoDB client | ### File Operations (metadata) | Function | Description | |----------|-------------| | `opendir()` / `readdir()` / `closedir()` | Directory traversal | | `unlink()` / `rename()` | File deletion and renaming | | `mkdir()` / `rmdir()` | Directory creation and removal | | `stat()` / `lstat()` | File information | | `readlink()` | Read symbolic links | > **Note:** File metadata operations on local disk complete in microseconds. Making them async only makes sense for network file systems (NFS). *** ## What's Next? * [spawn()](/en/docs/reference/spawn.html) — creating coroutines * [await()](/en/docs/reference/await.html) — waiting for results * [Coroutines](/en/docs/components/coroutines.html) — concepts and examples --- --- url: https://true-async.github.io/en/docs/reference/suspend.md description: >- suspend() — suspend execution of the current coroutine. Full documentation: cooperative multitasking examples. --- # suspend (PHP 8.6+, True Async 1.0) `suspend()` — Suspends execution of the current coroutine ## Description ```php suspend: void ``` Suspends execution of the current coroutine and yields control to the scheduler. The coroutine's execution will be resumed later when the scheduler decides to run it. `suspend()` is a function provided by the True Async extension. ## Parameters This construct has no parameters. ## Return Values The function does not return a value. ## Examples ### Example #1 Basic usage of suspend ```php ``` **Output:** ``` Before suspend Main code After suspend ``` ### Example #2 Multiple suspends ```php ``` **Output:** ``` Iteration 1 Coroutine started Iteration 2 Iteration 3 ``` ### Example #3 Cooperative multitasking ```php ``` **Output:** ``` Coroutine A: 1 Coroutine B: 1 Coroutine A: 2 Coroutine B: 2 Coroutine A: 3 Coroutine B: 3 ... ``` ### Example #4 Explicit yielding of control ```php ``` ### Example #5 suspend from nested functions `suspend()` works from any call depth — it does not need to be called directly from the coroutine: ```php ``` **Output:** ``` Coroutine: before nested call Deep call: start Nested function: before suspend Other coroutine: working Nested function: after suspend Deep call: end Coroutine: after nested call ``` ### Example #6 suspend in a wait loop ```php ``` **Output:** ``` Preparing... Ready! Condition met! ``` ## Notes > **Note:** `suspend()` is a function. Calling it as `suspend` (without parentheses) is incorrect. > **Note:** In TrueAsync, all executing code is treated as a coroutine, > so `suspend()` can be called anywhere (including the main script). > **Note:** After calling `suspend()`, coroutine execution will not resume immediately, > but when the scheduler decides to run it. The order of coroutine resumption is not guaranteed. > **Note:** In most cases, explicit use of `suspend()` is not required. > Coroutines are automatically suspended when performing I/O operations > (file reads, network requests, etc.). > **Note:** Using `suspend()` > in infinite loops without I/O operations can lead to high CPU usage. > You can also use `Async\timeout()`. ## Changelog | Version | Description | |-----------|-----------------------------------| | 1.0.0 | Added the `suspend()` function | ## See Also * [spawn()](/en/docs/reference/spawn.html) - Launching a coroutine * [await()](/en/docs/reference/await.html) - Waiting for a coroutine result --- --- url: https://true-async.github.io/en/docs/evidence/swoole-evidence.md description: >- Swoole in practice: production cases from Appwrite and IdleMMO, independent benchmarks, TechEmpower, comparison with PHP-FPM. --- # Swoole in Practice: Real-World Measurements Swoole is a PHP extension written in C that provides an event loop, coroutines, and asynchronous I/O. It is the only mature implementation of the coroutine model in the PHP ecosystem with years of production experience. Below is a collection of real-world measurements: production cases, independent benchmarks, and TechEmpower data. ### Two Sources of Performance Gain Transitioning from PHP-FPM to Swoole provides **two independent** advantages: 1. **Stateful runtime** — the application loads once and stays in memory. The overhead of re-initialization (autoload, DI container, configuration) on every request disappears. This effect provides a gain even without I/O. 2. **Coroutine concurrency** — while one coroutine waits for a DB or external API response, others process requests on the same core. This effect manifests **only when I/O is present** and requires the use of asynchronous clients (coroutine-based MySQL, Redis, HTTP client). Most public benchmarks **do not separate** these two effects. Tests without a DB (Hello World, JSON) measure only the stateful effect. Tests with a DB measure the **sum of both**, but do not allow isolating the coroutine contribution. Each section below indicates which effect predominates. ## 1. Production: Appwrite — Migration from FPM to Swoole (+91%) > **What is measured:** stateful runtime **+** coroutine concurrency. > Appwrite is an I/O proxy with minimal CPU work. The gain comes from > both factors, but isolating the coroutine contribution from public data is not possible. [Appwrite](https://appwrite.io/) is an open-source Backend-as-a-Service (BaaS) written in PHP. Appwrite provides a ready-made server API for common mobile and web application tasks: user authentication, database management, file storage, cloud functions, push notifications. By its nature, Appwrite is a **pure I/O proxy**: almost every incoming HTTP request translates into one or more I/O operations (MariaDB query, Redis call, file read/write), with minimal CPU computation of its own. This workload profile extracts maximum benefit from transitioning to coroutines: while one coroutine waits for a DB response, others process new requests on the same core. In version 0.7, the team replaced Nginx + PHP-FPM with Swoole. **Test conditions:** 500 concurrent clients, 5 minutes of load (k6). All requests to endpoints with authorization and abuse control. | Metric | FPM (v0.6.2) | Swoole (v0.7) | Change | |------------------------------|--------------|---------------|-----------------| | Requests per second | 436 | 808 | **+85%** | | Total requests in 5 min | 131,117 | 242,336 | **+85%** | | Response time (normal) | 3.77 ms | 1.61 ms | **−57%** | | Response time (under load) | 550 ms | 297 ms | **−46%** | | Request success rate | 98% | 100% | No timeouts | Overall improvement reported by the team: **~91%** across combined metrics. **Source:** [Appwrite 0.7: 91% boost in API Performance (DEV.to)](https://dev.to/appwrite/appwrite-0-7-91-boost-in-api-performance-144n) ## 2. Production: IdleMMO — 35 Million Requests per Day on a Single Server > **What is measured:** predominantly **stateful runtime**. > Laravel Octane runs Swoole in "one request — one worker" mode, > without coroutine I/O multiplexing within a request. > The performance gain is due to Laravel not reloading on every request. [IdleMMO](https://www.galahadsixteen.com/blog/from-zero-to-35m-the-struggles-of-scaling-laravel-with-octane) is a PHP application (Laravel Octane + Swoole), an MMORPG with 160,000+ users. | Metric | Value | |----------------------------|-----------------------------------| | Requests per day | 35,000,000 (~405 req/s average) | | Potential (author estimate)| 50,000,000+ req/day | | Server | 1 × 32 vCPU | | Swoole workers | 64 (4 per core) | | p95 latency before tuning | 394 ms | | p95 latency after Octane | **172 ms (−56%)** | The author notes that for less CPU-intensive applications (not an MMORPG), the same server could handle **significantly more** requests. **Source:** [From Zero to 35M: The Struggles of Scaling Laravel with Octane](https://www.galahadsixteen.com/blog/from-zero-to-35m-the-struggles-of-scaling-laravel-with-octane) ## 3. Benchmark: PHP-FPM vs Swoole (BytePursuits) > **What is measured:** only **stateful runtime**. > The test returns JSON without accessing a DB or external services. > Coroutine concurrency is not involved here — there is no I/O that could > be performed in parallel. The 2.6–3x difference is due entirely to > Swoole not recreating the application on every request. Independent benchmark on the Mezzio microframework (JSON response, no DB). Intel i7-6700T (4 cores / 8 threads), 32 GB RAM, wrk, 10 seconds. | Concurrency | PHP-FPM (req/s) | Swoole BASE (req/s) | Difference | |-------------|-----------------|---------------------|------------| | 100 | 3,472 | 9,090 | **2.6x** | | 500 | 3,218 | 9,159 | **2.8x** | | 1,000 | 3,065 | 9,205 | **3.0x** | Average latency at 1000 concurrent: * FPM: **191 ms** * Swoole: **106 ms** **Critical point:** starting at 500 concurrent connections, PHP-FPM began losing requests (73,793 socket errors at 500, 176,652 at 700). Swoole had **zero errors** at all concurrency levels. **Source:** [BytePursuits: Benchmarking PHP-FPM vs Swoole](https://bytepursuits.com/benchmarking-of-php-application-with-php-fpm-vs-swoole-openswoole) ## 4. Benchmark: With Database (kenashkov) > **What is measured:** a set of tests with **different** effects. > > * Hello World, Autoload — pure **stateful runtime** (no I/O). > * SQL query, real-world scenario — **stateful + coroutines**. > * Swoole uses a coroutine-based MySQL client, which allows serving > * other requests while waiting for a DB response. A more realistic test suite: Swoole 4.4.10 vs Apache + mod\_php. ApacheBench, 100–1000 concurrent, 10,000 requests. | Scenario | Apache (100 conc.) | Swoole (100 conc.) | Difference | |---------------------------------------|--------------------|--------------------|------------| | Hello World | 25,706 req/s | 66,309 req/s | **2.6x** | | Autoload 100 classes | 2,074 req/s | 53,626 req/s | **25x** | | SQL query to DB | 2,327 req/s | 4,163 req/s | **1.8x** | | Real-world scenario (cache + files + DB) | 141 req/s | 286 req/s | **2.0x** | At 1000 concurrent: * Apache **crashed** (connection limit, failed requests) * Swoole — **zero errors** in all tests **Key observation:** with real I/O (DB + files), the difference drops from 25x to **1.8–2x**. This is expected: the database becomes the common bottleneck. But stability under load remains incomparable. **Source:** [kenashkov/swoole-performance-tests (GitHub)](https://github.com/kenashkov/swoole-performance-tests) ## 5. Benchmark: Symfony 7 — All Runtimes (2024) > **What is measured:** only **stateful runtime**. > Test without DB — coroutines are not involved. > The >10x difference at 1000 concurrent is explained by the fact that FPM creates > a process per request, while Swoole and FrankenPHP keep the application > in memory and serve connections through an event loop. Test of 9 PHP runtimes with Symfony 7 (k6, Docker, 1 CPU / 1 GB RAM, no DB). | Runtime | vs Nginx + PHP-FPM (at 1000 conc.) | |-----------------------------------|-------------------------------------| | Apache + mod\_php | ~0.5x (slower) | | Nginx + PHP-FPM | 1x (baseline) | | Nginx Unit | ~3x | | RoadRunner | >2x | | **Swoole / FrankenPHP (worker)** | **>10x** | At 1000 concurrent connections, Swoole and FrankenPHP in worker mode showed **an order of magnitude higher throughput** than classic Nginx + PHP-FPM. **Source:** [Performance benchmark of PHP runtimes (DEV.to)](https://dev.to/dimdev/performance-benchmark-of-php-runtimes-2lmc) ## 6. TechEmpower: Swoole — First Place Among PHP > **What is measured:** **stateful + coroutines** (in DB tests). > TechEmpower includes both a JSON test (stateful) and tests with multiple > SQL queries (multiple queries, Fortunes), where coroutine-based DB access > provides a real advantage. This is one of the few benchmarks > where the coroutine effect is most clearly visible. In [TechEmpower Framework Benchmarks](https://www.techempower.com/benchmarks/) (Round 22, 2023), Swoole took **first place** among all PHP frameworks in the MySQL test. TechEmpower tests real-world scenarios: JSON serialization, single DB queries, multiple queries, ORM, Fortunes (templating + DB + sorting + escaping). **Source:** [TechEmpower Round 22](https://www.techempower.com/blog/2023/11/15/framework-benchmarks-round-22/), [swoole-src README](https://github.com/swoole/swoole-src) ## 7. Hyperf: 96,000 req/s on a Swoole Framework > **What is measured:** **stateful runtime** (benchmark is Hello World). > Hyperf is entirely built on Swoole coroutines, and in production, > coroutine concurrency is utilized for DB, Redis, and gRPC calls. > However, the 96K req/s figure was obtained on Hello World without I/O, > meaning it reflects the stateful runtime effect. [Hyperf](https://hyperf.dev/) is a coroutine-based PHP framework built on Swoole. In the benchmark (4 threads, 100 connections): * **96,563 req/s** * Latency: 7.66 ms Hyperf is positioned for microservices and claims **5–10x** advantage over traditional PHP frameworks. **Source:** [Hyperf GitHub](https://github.com/hyperf/hyperf) ## Summary: What Real Data Shows | Test type | FPM → Swoole | Primary effect | Note | |----------------------------------|---------------------------------|---------------------|-----------------------------------------------| | Hello World / JSON | **2.6–3x** | Stateful | BytePursuits, kenashkov | | Autoload (stateful vs stateless) | **25x** | Stateful | No I/O — pure effect of state preservation | | With database | **1.8–2x** | Stateful + coroutines | kenashkov (coroutine MySQL) | | Production API (Appwrite) | **+91%** (1.85x) | Stateful + coroutines | I/O proxy, both factors | | Production (IdleMMO) | p95: **−56%** | Stateful | Octane workers, not coroutines | | High concurrency (1000+) | **Swoole stable, FPM crashes** | Event loop | All benchmarks | | Symfony runtimes (1000 conc.) | **>10x** | Stateful | No DB in test | | TechEmpower (DB tests) | **#1 among PHP** | Stateful + coroutines | Multiple SQL queries | ## Connection to Theory The results align well with calculations from [IO-bound Task Efficiency](/en/docs/evidence/concurrency-efficiency.html): **1. With a database, the difference is more modest (1.8–2x) than without one (3–10x).** This confirms: with real I/O, the bottleneck becomes the DB itself, not the concurrency model. The blocking coefficient in DB tests is lower because the framework's CPU work is comparable to I/O time. **2. At high concurrency (500–1000+), FPM degrades while Swoole does not.** PHP-FPM is limited by the number of workers. Each worker is an OS process (~40 MB). At 500+ concurrent connections, FPM reaches its limit and starts losing requests. Swoole serves thousands of connections in dozens of coroutines without increasing memory consumption. **3. Stateful runtime eliminates re-initialization overhead.** The 25x difference in the autoload test demonstrates the cost of recreating application state on every request in FPM. In production, this manifests as the difference between T\_cpu = 34 ms (FPM) and T\_cpu = 5–10 ms (stateful), which dramatically changes the blocking coefficient and consequently the gain from coroutines (see [table in IO-bound Task Efficiency](/en/docs/evidence/concurrency-efficiency.html)). **4. The formula is confirmed.** Appwrite: FPM 436 req/s → Swoole 808 req/s (1.85x). If T\_cpu dropped from ~30 ms to ~15 ms (stateful) and T\_io remained ~30 ms, then the blocking coefficient increased from 1.0 to 2.0, which predicts a throughput increase of approximately 1.5–2x. This matches. ## References ### Production Cases * [Appwrite: 91% boost in API Performance](https://dev.to/appwrite/appwrite-0-7-91-boost-in-api-performance-144n) * [IdleMMO: From Zero to 35M with Laravel Octane](https://www.galahadsixteen.com/blog/from-zero-to-35m-the-struggles-of-scaling-laravel-with-octane) ### Independent Benchmarks * [BytePursuits: PHP-FPM vs Swoole](https://bytepursuits.com/benchmarking-of-php-application-with-php-fpm-vs-swoole-openswoole) * [kenashkov: swoole-performance-tests (GitHub)](https://github.com/kenashkov/swoole-performance-tests) * [PHP runtimes benchmark — Symfony 7 (DEV.to)](https://dev.to/dimdev/performance-benchmark-of-php-runtimes-2lmc) ### Frameworks and Runtimes * [TechEmpower Framework Benchmarks](https://www.techempower.com/benchmarks/) * [Hyperf — coroutine-based PHP framework](https://github.com/hyperf/hyperf) * [OpenSwoole benchmark](https://openswoole.com/benchmark) * [Swoole source (GitHub)](https://github.com/swoole/swoole-src) --- --- url: https://true-async.github.io/en/tutors/10-task-group.md description: >- TaskGroup: a group of tasks with results, and the all, race, any waiting strategies. --- # TaskGroup Suppose a user profile page is assembled from three sources: user data from the database, orders from the database, and reviews from an external API. The sources don't depend on each other, so they should be requested concurrently. We already know how to do this: ```php $user = spawn(fetchUser(...), $userId); $orders = spawn(fetchOrders(...), $userId); $reviews = spawn(fetchReviews(...), $userId); $profile = new UserProfile(await($user), await($orders), await($reviews)); ``` Tolerable for three tasks. But look closely: this is once again the manual bookkeeping from the chapter on `Scope`, except now we also need the results. Every coroutine has to be remembered and awaited, and if `fetchUser` throws an exception, `fetchOrders` and `fetchReviews` will keep running for nothing: they'd have to be cancelled "by hand" in a `catch`. And there are tasks where the manual approach becomes genuinely painful. For example, taking the result of whichever coroutine finishes first and cancelling the rest. Try writing that with `await` in a loop and you'll end up with a tangle of checks and cancellations. `Scope` isn't much help here either: it manages the lifetime of coroutines, but knows nothing about their results. We need a higher-level primitive. ## A group of tasks `TaskGroup` bundles tasks into a single whole: it runs them in its own `Scope`, stores their results, and lets you wait for the group as one unit: ```php use Async\TaskGroup; $group = new TaskGroup(); $group->spawnWithKey('user', fn() => fetchUser($userId)); $group->spawnWithKey('orders', fn() => fetchOrders($userId)); $group->spawnWithKey('reviews', fn() => fetchReviews($userId)); $data = $group->all()->await(); $profile = new UserProfile($data['user'], $data['orders'], $data['reviews']); ``` The `all()` method returns a familiar `Future`, which resolves to an array of results once every task has finished. We assigned the keys ourselves via `spawnWithKey`, so the array holds named entries instead of plain indexes. And since it's a `Future`, a timeout comes for free, via the same token as always: ```php $data = $group->all()->await(timeout(5000)); ``` If even one task throws an exception, the group behaves like the Scope from chapter eight: the remaining tasks get cancelled, and `await` throws a `CompositeException` holding all the errors. The group either collects everything, or collects nothing: there's no in-between state. ## First to finish: race `all()` is just one of the waiting strategies. Think back to `GeoDirectory`: it has three replicas, and one of them is sometimes slow. The classic trick: send the request to all replicas and take the first answer: ```php $group = new TaskGroup(); foreach (['geo-1', 'geo-2', 'geo-3'] as $host) { $group->spawn(fn() => checkAddressAt($host, $address)); } $verdict = $group->race()->await(); ``` `race()` resolves with the result of whichever task finishes first, whether that's success or failure. Exactly the "take the first one and don't wait for the rest" scenario that's so painful to write by hand. ## First to succeed: any `race()` has a hard edge: if the first task to finish happens to be a failed one, you get its exception. Sometimes you need something gentler: try several providers and take the first successful answer, turning a blind eye to failures: ```php $group = new TaskGroup(); $group->spawn(fn() => geocodeViaGoogle($address)); $group->spawn(fn() => geocodeViaOsm($address)); $group->spawn(fn() => geocodeViaYandex($address)); $coords = $group->any()->await(); $group->suppressErrors(); ``` `any()` ignores the failures and returns the first winner. You only get an exception if every task fails, and it will be a `CompositeException` with the full list of causes. Notice the `suppressErrors()` call: nobody handled the errors from the losing providers, and the group wants explicit confirmation that this was intentional. A familiar principle from the chapter on exceptions: an error can't just quietly disappear. ## Concurrency limit And now for something unexpected. Remember the worker pool from the chapter on channels: a channel, ten coroutines, a `recv` loop? `TaskGroup` can do the same thing in a few lines: ```php $group = new TaskGroup(concurrency: 10); while (($row = fgetcsv($handle)) !== false) { $group->spawn(fn() => checkAddress($row[$addressIndex])); } $group->close(); foreach ($group as $key => [$result, $error]) { // results arrive as they become ready } ``` The `concurrency: 10` parameter caps how many tasks run at once: the rest wait in line and don't even spin up a coroutine until a slot frees up. `close()` plays the same role it does for a channel: it announces that no new tasks are coming. And `foreach` hands out results as they become ready, without waiting for the whole group to finish. Does that mean the channel wasn't needed after all? No. A channel is a synchronization primitive you can build anything out of. `TaskGroup` is a ready-made assembly for the most common case: "run a set of tasks and get the results". When a task fits that pattern, reach for `TaskGroup`; when you need a non-standard topology, channels and Scope are still there in your hands. Bottom line: `TaskGroup` is a Scope plus results. A group of tasks turns into a single value you can ask for everything at once, the first to finish, or the first to succeed. One last detail: `TaskGroup` carefully keeps all the results around. Call `race()` twice, and you'll get the same answer both times. Iterate the group with `foreach` again, and it hands out everything from the start once more. For a profile page, that's convenient. Now picture a pipeline that runs a hundred thousand tasks through a group. Everything the group remembers lives in memory. See the catch? That's what we'll talk about in the next chapter. --- --- url: https://true-async.github.io/en/docs/reference/task-group/construct.md description: Create a new TaskGroup with optional concurrency limit. --- # TaskGroup::\_\_construct (PHP 8.6+, True Async 1.0) ```php public TaskGroup::__construct(?int $concurrency = null, ?int $queueLimit = null, ?Async\Scope $scope = null) ``` Creates a new task group. ## Parameters **concurrency** : Maximum number of concurrently running coroutines. `null` --- no limit, all tasks are started immediately. When the limit is reached, new tasks are placed in a queue and started automatically when a slot becomes available. **scope** : Parent scope. TaskGroup creates a child scope for its coroutines. `null` --- the current scope is inherited. ## Examples ### Example #1 Without limits ```php spawn(fn() => "task 1"); // starts immediately $group->spawn(fn() => "task 2"); // starts immediately $group->spawn(fn() => "task 3"); // starts immediately ``` ### Example #2 With concurrency limit ```php spawn(fn() => "task 1"); // starts immediately $group->spawn(fn() => "task 2"); // starts immediately $group->spawn(fn() => "task 3"); // waits in queue ``` ## See Also * [TaskGroup::spawn](/en/docs/reference/task-group/spawn.html) --- Add a task * [Scope](/en/docs/components/scope.html) --- Coroutine lifecycle management --- --- url: https://true-async.github.io/en/docs/reference/task-group/all.md description: Create a Future that resolves with an array of all task results. --- # TaskGroup::all (PHP 8.6+, True Async 1.0) ```php public TaskGroup::all(bool $ignoreErrors = false): Async\Future ``` Returns a `Future` that resolves with an array of results when all tasks have completed. Array keys match the keys assigned via `spawn()` / `spawnWithKey()`. If tasks have already completed, the `Future` resolves immediately. The returned `Future` supports a cancellation token via `await(?Completable $cancellation)`, allowing you to set a timeout or other cancellation strategy. ## Parameters **ignoreErrors** : If `false` (default) and there are errors, the `Future` rejects with `CompositeException`. If `true`, errors are ignored and the `Future` resolves with only successful results. Errors can be retrieved via `getErrors()`. ## Return Value `Async\Future` --- a future result containing the array of task results. Call `->await()` to get the value. ## Errors The `Future` rejects with `Async\CompositeException` if `$ignoreErrors = false` and at least one task failed with an error. ## Examples ### Example #1 Basic usage ```php spawnWithKey('a', fn() => 10); $group->spawnWithKey('b', fn() => 20); $group->spawnWithKey('c', fn() => 30); $group->close(); $results = $group->all()->await(); var_dump($results['a']); // int(10) var_dump($results['b']); // int(20) var_dump($results['c']); // int(30) }); ``` ### Example #2 Error handling ```php spawn(fn() => "ok"); $group->spawn(fn() => throw new \RuntimeException("fail")); $group->close(); try { $group->all()->await(); } catch (\Async\CompositeException $e) { foreach ($e->getExceptions() as $ex) { echo $ex->getMessage() . "\n"; // "fail" } } }); ``` ### Example #3 Ignoring errors ```php spawn(fn() => "ok"); $group->spawn(fn() => throw new \RuntimeException("fail")); $group->close(); $results = $group->all(ignoreErrors: true)->await(); echo count($results) . "\n"; // 1 $errors = $group->getErrors(); echo count($errors) . "\n"; // 1 }); ``` ### Example #4 Waiting with a timeout ```php spawn(fn() => slowApi()->fetchReport()); $group->spawn(fn() => anotherApi()->fetchStats()); $group->close(); $timeout = Async\timeout(5.0); try { $results = $group->all()->await($timeout); } catch (Async\TimeoutException) { echo "Failed to get data within 5 seconds\n"; } }); ``` ## See Also * [TaskGroup::awaitCompletion](/en/docs/reference/task-group/await-completion.html) --- Wait for completion without exceptions * [TaskGroup::getResults](/en/docs/reference/task-group/get-results.html) --- Get results without waiting * [TaskGroup::getErrors](/en/docs/reference/task-group/get-errors.html) --- Get errors --- --- url: https://true-async.github.io/en/docs/reference/task-group/any.md description: Create a Future that resolves with the result of the first successful task. --- # TaskGroup::any (PHP 8.6+, True Async 1.0) ```php public TaskGroup::any(): Async\Future ``` Returns a `Future` that resolves with the result of the first *successfully* completed task. Tasks that failed with an error are skipped. The remaining tasks **continue running**. If all tasks fail with errors, the `Future` rejects with `CompositeException`. The returned `Future` supports a cancellation token via `await(?Completable $cancellation)`. ## Return Value `Async\Future` --- a future result of the first successful task. Call `->await()` to get the value. ## Errors * Throws `Async\AsyncException` if the group is empty. * The `Future` rejects with `Async\CompositeException` if all tasks fail with errors. ## Examples ### Example #1 First successful ```php spawn(fn() => throw new \RuntimeException("fail 1")); $group->spawn(fn() => throw new \RuntimeException("fail 2")); $group->spawn(fn() => "success!"); $result = $group->any()->await(); echo $result . "\n"; // "success!" // Errors from failed tasks must be explicitly suppressed $group->suppressErrors(); }); ``` ### Example #2 All failed ```php spawn(fn() => throw new \RuntimeException("err 1")); $group->spawn(fn() => throw new \RuntimeException("err 2")); $group->close(); try { $group->any()->await(); } catch (\Async\CompositeException $e) { echo count($e->getExceptions()) . " errors\n"; // "2 errors" } }); ``` ### Example #3 Resilient search with timeout ```php spawn(fn() => searchGoogle($query)); $group->spawn(fn() => searchBing($query)); $group->spawn(fn() => searchDuckDuckGo($query)); $timeout = Async\timeout(3.0); try { $result = $group->any()->await($timeout); } catch (Async\TimeoutException) { echo "No provider responded within 3 seconds\n"; } $group->suppressErrors(); }); ``` ## See Also * [TaskGroup::race](/en/docs/reference/task-group/race.html) --- First completed (success or error) * [TaskGroup::all](/en/docs/reference/task-group/all.html) --- All results --- --- url: https://true-async.github.io/en/docs/reference/task-group/await-completion.md description: Wait for all tasks to complete without throwing exceptions. --- # TaskGroup::awaitCompletion (PHP 8.6+, True Async 1.0) ```php public TaskGroup::awaitCompletion(): void ``` Waits until all tasks in the group have fully completed. Unlike `all()`, it does not return results and does not throw exceptions on task errors. The group must be closed before calling this method. A typical use case is waiting for coroutines to actually finish after `cancel()`. The `cancel()` method initiates cancellation, but coroutines may finish asynchronously. `awaitCompletion()` guarantees that all coroutines have stopped. ## Errors Throws `Async\AsyncException` if the group is not closed. ## Examples ### Example #1 Waiting after cancel ```php spawn(function() { suspend(); return "result"; }); $group->cancel(); $group->awaitCompletion(Async\timeout(5000)); echo "all coroutines finished\n"; var_dump($group->isFinished()); // bool(true) }); ``` ### Example #2 Getting results after waiting ```php spawn(fn() => "ok"); $group->spawn(fn() => throw new \RuntimeException("fail")); $group->close(); $group->awaitCompletion(Async\timeout(5000)); // No exceptions — check manually $results = $group->getResults(); $errors = $group->getErrors(); echo "Successful: " . count($results) . "\n"; // 1 echo "Errors: " . count($errors) . "\n"; // 1 }); ``` ## See Also * [TaskGroup::all](/en/docs/reference/task-group/all.html) --- Wait for all tasks and get results * [TaskGroup::cancel](/en/docs/reference/task-group/cancel.html) --- Cancel all tasks * [TaskGroup::close](/en/docs/reference/task-group/close.html) --- Close the group --- --- url: https://true-async.github.io/en/docs/reference/task-group/cancel.md description: Cancel all tasks in the group. --- # TaskGroup::cancel (PHP 8.6+, True Async 1.0) ```php public TaskGroup::cancel(?Async\AsyncCancellation $cancellation = null): void ``` Cancels all running coroutines and queued tasks. Implicitly calls `close()`. Queued tasks are never started. Coroutines receive an `AsyncCancellation` and terminate. Cancellation happens asynchronously --- use `awaitCompletion()` to guarantee completion. ## Parameters **cancellation** : The exception serving as the cancellation reason. If `null`, a standard `AsyncCancellation` with the message "TaskGroup cancelled" is used. ## Examples ### Example #1 Cancellation with waiting for completion ```php spawn(function() { Async\delay(10000); return "long task"; }); $group->cancel(); $group->awaitCompletion(Async\timeout(5000)); echo "all tasks cancelled\n"; }); ``` ### Example #2 Cancellation with a reason ```php spawn(fn() => Async\delay(10000)); $group->cancel(new \Async\AsyncCancellation("Timeout exceeded")); $group->awaitCompletion(Async\timeout(5000)); }); ``` ## See Also * [TaskGroup::close](/en/docs/reference/task-group/close.html) --- Close without cancellation * [TaskGroup::awaitCompletion](/en/docs/reference/task-group/await-completion.html) --- Wait for completion * [TaskGroup::dispose](/en/docs/reference/task-group/dispose.html) --- Dispose of the group scope --- --- url: https://true-async.github.io/en/docs/reference/task-group/close.md description: Close the group to prevent new tasks. --- # TaskGroup::close (PHP 8.6+, True Async 1.0) ```php public TaskGroup::close(): void ``` Closes the group. Any attempt to use `spawn()` or `spawnWithKey()` will throw an exception. Already running coroutines and queued tasks continue to execute. Repeated calls are a no-op. ## Examples ### Example #1 Basic usage ```php spawn(fn() => "task"); $group->close(); try { $group->spawn(fn() => "another task"); } catch (\Async\AsyncException $e) { echo $e->getMessage() . "\n"; // "Cannot spawn tasks on a closed TaskGroup" } }); ``` ## See Also * [TaskGroup::cancel](/en/docs/reference/task-group/cancel.html) --- Cancel all tasks (implicitly calls close) * [TaskGroup::isClosed](/en/docs/reference/task-group/is-closed.html) --- Check if the group is closed --- --- url: https://true-async.github.io/en/docs/reference/task-group/count.md description: Get the total number of tasks in the group. --- # TaskGroup::count (PHP 8.6+, True Async 1.0) ```php public TaskGroup::count(): int ``` Returns the total number of tasks in the group: queued, running, and completed. TaskGroup implements the `Countable` interface, so you can use `count($group)`. ## Return Value The total number of tasks (`int`). ## Examples ### Example #1 Counting tasks ```php spawn(fn() => "a"); $group->spawn(fn() => "b"); $group->spawn(fn() => "c"); echo count($group); // 3 $group->close(); $group->all(); echo count($group); // 3 }); ``` ## See Also * [TaskGroup::isFinished](/en/docs/reference/task-group/is-finished.html) --- Check if all tasks are finished * [TaskGroup::isClosed](/en/docs/reference/task-group/is-closed.html) --- Check if the group is closed --- --- url: https://true-async.github.io/en/docs/reference/task-group/dispose.md description: Dispose of the group scope. --- # TaskGroup::dispose (PHP 8.6+, True Async 1.0) ```php public TaskGroup::dispose(): void ``` Calls `dispose()` on the group's internal scope, which results in cancelling all coroutines. ## See Also * [TaskGroup::cancel](/en/docs/reference/task-group/cancel.html) --- Cancel all tasks * [Scope](/en/docs/components/scope.html) --- Coroutine lifecycle management --- --- url: https://true-async.github.io/en/docs/reference/task-group/finally.md description: Register a completion handler for the group. --- # TaskGroup::finally (PHP 8.6+, True Async 1.0) ```php public TaskGroup::finally(Closure $callback): void ``` Registers a callback that is invoked when the group is closed and all tasks have completed. The callback receives the TaskGroup as a parameter. Since `cancel()` implicitly calls `close()`, the handler also fires on cancellation. If the group is already finished, the callback is called synchronously immediately. ## Parameters **callback** : A Closure that takes `TaskGroup` as its only argument. ## Examples ### Example #1 Logging completion ```php finally(function(TaskGroup $g) { echo "Completed: " . $g->count() . " tasks\n"; }); $group->spawn(fn() => "a"); $group->spawn(fn() => "b"); $group->close(); $group->all(); }); // Output: // Completed: 2 tasks ``` ### Example #2 Calling on an already finished group ```php spawn(fn() => 1); $group->close(); $group->all(); // Group is already finished — callback is called synchronously $group->finally(function(TaskGroup $g) { echo "called immediately\n"; }); echo "after finally\n"; }); // Output: // called immediately // after finally ``` ## See Also * [TaskGroup::close](/en/docs/reference/task-group/close.html) --- Close the group * [TaskGroup::cancel](/en/docs/reference/task-group/cancel.html) --- Cancel tasks --- --- url: https://true-async.github.io/en/docs/reference/task-group/get-errors.md description: Get an array of errors from failed tasks. --- # TaskGroup::getErrors (PHP 8.6+, True Async 1.0) ```php public TaskGroup::getErrors(): array ``` Returns an array of exceptions (`Throwable`) from tasks that failed with an error. Array keys match the task keys from `spawn()` or `spawnWithKey()`. The method does not wait for tasks to complete --- it returns only the errors available at the time of the call. ## Return Value An `array` where the key is the task identifier and the value is the exception. ## Examples ### Example #1 Viewing errors ```php spawnWithKey('api', function() { throw new \RuntimeException("Connection timeout"); }); $group->spawn(fn() => "ok"); $group->close(); $group->all(ignoreErrors: true); foreach ($group->getErrors() as $key => $error) { echo "$key: {$error->getMessage()}\n"; } // api: Connection timeout $group->suppressErrors(); }); ``` ## Unhandled Errors If unhandled errors remain when a TaskGroup is destroyed, the destructor signals this. Errors are considered handled if: * `all()` is called with `ignoreErrors: false` (default) and throws a `CompositeException` * `suppressErrors()` is called * Errors are handled through the iterator (`foreach`) ## See Also * [TaskGroup::getResults](/en/docs/reference/task-group/get-results.html) --- Get an array of results * [TaskGroup::suppressErrors](/en/docs/reference/task-group/suppress-errors.html) --- Mark errors as handled * [TaskGroup::all](/en/docs/reference/task-group/all.html) --- Wait for all tasks --- --- url: https://true-async.github.io/en/docs/reference/task-group/get-iterator.md description: Get an iterator to traverse results as tasks complete. --- # TaskGroup::getIterator (PHP 8.6+, True Async 1.0) ```php public TaskGroup::getIterator(): Iterator ``` Returns an iterator that yields results **as tasks complete**. TaskGroup implements `IteratorAggregate`, so you can use `foreach` directly. ## Iterator Behavior * `foreach` suspends the current coroutine until the next result is available * The key is the same as assigned via `spawn()` or `spawnWithKey()` * The value is an array `[mixed $result, ?Throwable $error]`: * Success: `[$result, null]` * Error: `[null, $error]` * Iteration ends when the group is closed **and** all tasks have been processed * If the group is not closed, `foreach` suspends waiting for new tasks > **Important:** Without calling `close()`, iteration will wait indefinitely. ## Examples ### Example #1 Processing results as they become ready ```php spawn(fn() => fetchUrl($urls[$i])); } $group->close(); foreach ($group as $key => [$result, $error]) { if ($error !== null) { echo "Task $key failed: {$error->getMessage()}\n"; continue; } echo "Task $key done\n"; } }); ``` ### Example #2 Iteration with named keys ```php spawnWithKey('users', fn() => fetchUsers()); $group->spawnWithKey('orders', fn() => fetchOrders()); $group->close(); foreach ($group as $key => [$result, $error]) { if ($error === null) { echo "$key: received " . count($result) . " records\n"; } } }); ``` ## See Also * [TaskGroup::close](/en/docs/reference/task-group/close.html) --- Close the group * [TaskGroup::all](/en/docs/reference/task-group/all.html) --- Wait for all tasks * [TaskGroup::getResults](/en/docs/reference/task-group/get-results.html) --- Get an array of results --- --- url: https://true-async.github.io/en/docs/reference/task-group/get-results.md description: Get an array of results from completed tasks. --- # TaskGroup::getResults (PHP 8.6+, True Async 1.0) ```php public TaskGroup::getResults(): array ``` Returns an array of results from successfully completed tasks. Array keys match the keys assigned via `spawn()` (auto-increment) or `spawnWithKey()` (custom). The method does not wait for tasks to complete --- it returns only the results available at the time of the call. ## Return Value An `array` where the key is the task identifier and the value is the execution result. ## Examples ### Example #1 Getting results after all() ```php spawnWithKey('user', fn() => ['name' => 'Alice']); $group->spawnWithKey('orders', fn() => [101, 102]); $group->close(); $group->all(); $results = $group->getResults(); // ['user' => ['name' => 'Alice'], 'orders' => [101, 102]] }); ``` ### Example #2 Results do not contain errors ```php spawn(fn() => "ok"); $group->spawn(function() { throw new \RuntimeException("fail"); }); $group->spawn(fn() => "also ok"); $group->close(); $group->all(ignoreErrors: true); $results = $group->getResults(); // [0 => "ok", 2 => "also ok"] $errors = $group->getErrors(); // [1 => RuntimeException("fail")] $group->suppressErrors(); }); ``` ## See Also * [TaskGroup::getErrors](/en/docs/reference/task-group/get-errors.html) --- Get an array of errors * [TaskGroup::all](/en/docs/reference/task-group/all.html) --- Wait for all tasks * [TaskGroup::suppressErrors](/en/docs/reference/task-group/suppress-errors.html) --- Mark errors as handled --- --- url: https://true-async.github.io/en/docs/reference/task-group/is-closed.md description: Check if the group is closed. --- # TaskGroup::isClosed (PHP 8.6+, True Async 1.0) ```php public TaskGroup::isClosed(): bool ``` Returns `true` after `close()` or `cancel()` has been called. ## See Also * [TaskGroup::close](/en/docs/reference/task-group/close.html) --- Close the group * [TaskGroup::isFinished](/en/docs/reference/task-group/is-finished.html) --- Check if finished --- --- url: https://true-async.github.io/en/docs/reference/task-group/is-finished.md description: Check if all tasks are finished. --- # TaskGroup::isFinished (PHP 8.6+, True Async 1.0) ```php public TaskGroup::isFinished(): bool ``` Returns `true` if the queue is empty and there are no active coroutines. This state may be temporary: if the group is not closed, new tasks can still be added. ## See Also * [TaskGroup::isClosed](/en/docs/reference/task-group/is-closed.html) --- Check if the group is closed * [TaskGroup::awaitCompletion](/en/docs/reference/task-group/await-completion.html) --- Wait for completion --- --- url: https://true-async.github.io/en/docs/reference/task-group/race.md description: Create a Future that resolves with the result of the first completed task. --- # TaskGroup::race (PHP 8.6+, True Async 1.0) ```php public TaskGroup::race(): Async\Future ``` Returns a `Future` that resolves with the result of the first completed task --- whether successful or failed. If the task failed with an error, the `Future` rejects with that exception. The remaining tasks **continue running**. If a completed task already exists, the `Future` resolves immediately. The returned `Future` supports a cancellation token via `await(?Completable $cancellation)`. ## Return Value `Async\Future` --- a future result of the first completed task. Call `->await()` to get the value. ## Errors * Throws `Async\AsyncException` if the group is empty. * The `Future` rejects with the task's exception if the first completed task failed with an error. ## Examples ### Example #1 First response ```php spawn(function() { delay(100); return "slow"; }); $group->spawn(fn() => "fast"); $winner = $group->race()->await(); echo $winner . "\n"; // "fast" }); ``` ### Example #2 Hedged requests with timeout ```php spawn(fn() => pg_query($host, 'SELECT * FROM products WHERE id = 42')); } $timeout = Async\timeout(2.0); try { $product = $group->race()->await($timeout); } catch (Async\TimeoutException) { echo "No replica responded within 2 seconds\n"; } }); ``` ## See Also * [TaskGroup::any](/en/docs/reference/task-group/any.html) --- First successful result * [TaskGroup::all](/en/docs/reference/task-group/all.html) --- All results --- --- url: https://true-async.github.io/en/docs/reference/task-group/spawn.md description: Add a task to the group with an auto-incremented key. --- # TaskGroup::spawn (PHP 8.6+, True Async 1.0) ```php public TaskGroup::spawn(callable $task, mixed ...$args): void ``` Adds a callable to the group with an auto-incremented key (0, 1, 2, ...). If no concurrency limit is set or a slot is available, the coroutine is created immediately. Otherwise, the callable with its arguments is placed in a queue and started when a slot becomes available. ## Parameters **task** : The callable to execute. Accepts any callable: Closure, function, method. **args** : Arguments passed to the callable. ## Errors Throws `Async\AsyncException` if the group is closed (`close()`) or cancelled (`cancel()`). ## Examples ### Example #1 Basic usage ```php spawn(fn() => "first"); $group->spawn(fn() => "second"); $group->close(); $results = $group->all(); var_dump($results[0]); // string(5) "first" var_dump($results[1]); // string(6) "second" }); ``` ### Example #2 With arguments ```php spawn(function(int $id) { return "user:$id"; }, 42); $group->close(); $results = $group->all(); var_dump($results[0]); // string(7) "user:42" }); ``` ## See Also * [TaskGroup::spawnWithKey](/en/docs/reference/task-group/spawn-with-key.html) --- Add a task with an explicit key * [TaskGroup::all](/en/docs/reference/task-group/all.html) --- Wait for all tasks --- --- url: https://true-async.github.io/en/docs/reference/task-group/spawn-with-key.md description: Add a task to the group with an explicit key. --- # TaskGroup::spawnWithKey (PHP 8.6+, True Async 1.0) ```php public TaskGroup::spawnWithKey(string|int $key, callable $task, mixed ...$args): void ``` Adds a callable to the group with the specified key. The task result will be accessible by this key in `all()`, `getResults()`, and during iteration. ## Parameters **key** : The task key. A string or integer. Duplicates are not allowed. **task** : The callable to execute. **args** : Arguments passed to the callable. ## Errors Throws `Async\AsyncException` if the group is closed or the key already exists. ## Examples ### Example #1 Named tasks ```php spawnWithKey('profile', fn() => ['name' => 'John']); $group->spawnWithKey('orders', fn() => [101, 102, 103]); $group->close(); $results = $group->all(); var_dump($results['profile']); // array(1) { ["name"]=> string(4) "John" } var_dump($results['orders']); // array(3) { [0]=> int(101) ... } }); ``` ## See Also * [TaskGroup::spawn](/en/docs/reference/task-group/spawn.html) --- Add a task with an auto-incremented key * [TaskGroup::all](/en/docs/reference/task-group/all.html) --- Wait for all tasks --- --- url: https://true-async.github.io/en/docs/reference/task-group/suppress-errors.md description: Mark all current errors as handled. --- # TaskGroup::suppressErrors (PHP 8.6+, True Async 1.0) ```php public TaskGroup::suppressErrors(): void ``` Marks all current errors in the group as handled. When a TaskGroup is destroyed, it checks for unhandled errors. If errors were not handled (via `all()`, `foreach`, or `suppressErrors()`), the destructor signals lost errors. Calling `suppressErrors()` is an explicit confirmation that the errors have been handled. ## Examples ### Example #1 Suppressing errors after selective handling ```php spawn(fn() => "ok"); $group->spawn(function() { throw new \RuntimeException("fail 1"); }); $group->spawn(function() { throw new \LogicException("fail 2"); }); $group->close(); $group->all(ignoreErrors: true); // Handle errors manually foreach ($group->getErrors() as $key => $error) { log_error("Task $key: {$error->getMessage()}"); } // Mark errors as handled $group->suppressErrors(); }); ``` ## See Also * [TaskGroup::getErrors](/en/docs/reference/task-group/get-errors.html) --- Get an array of errors * [TaskGroup::all](/en/docs/reference/task-group/all.html) --- Wait for all tasks --- --- url: https://true-async.github.io/en/tutors/11-task-set.md description: >- TaskSet: a self-cleaning stream of tasks, joinNext/joinAny/joinAll, and a supervisor loop. --- # TaskSet The previous chapter revealed that `TaskGroup` remembers everything. That's not an accident, it's a property you can rely on: no matter how many times you ask the group for results, you get the same answer. This behavior is called idempotence, and we've already run into it before: a repeated `await` on a coroutine returns the same result every time. But memory has a price. Run a hundred thousand tasks through a group, and all hundred thousand results stay sitting inside it, even if each one was only ever needed once, the moment it became ready. For a pipeline that runs for hours, that's not storage, it's a leak. What's needed is a twin of `TaskGroup` with the opposite temperament: hand over the result and forget it. It's called `TaskSet`. ## Consuming instead of storing On the surface everything looks the same: `spawn`, `close`, a concurrency limit. The difference is in what happens to a result after it's delivered: ```php use Async\TaskSet; $set = new TaskSet(); $set->spawn(fn() => 'alpha'); $set->spawn(fn() => 'beta'); $set->spawn(fn() => 'gamma'); echo $set->joinNext()->await(); // alpha echo $set->joinNext()->await(); // beta, already a different one! echo $set->joinNext()->await(); // gamma echo $set->count(); // 0, the set is empty ``` Each call to `joinNext()` returns the next ready result and removes its entry from the set. Compare that to `TaskGroup`'s `race()`, which returns the same first winner no matter how many times you call it. `TaskSet` behaves not like storage but like a queue: read it, and it's gone. Yes, that's the same semantics as `recv` from the channels chapter, except now the queue holds not values but completing tasks. The twins' waiting methods rhyme with each other: * **`joinNext()`** — like `race()`: the first one to finish, its entry removed. * **`joinAny()`** — like `any()`: the first successful one, its entry removed. * **`joinAll()`** — like `all()`: all results at once, the set drained. The `join` prefix hints at the difference itself: a result isn't just read, it's withdrawn. ## A pipeline without a leak Let's rewrite the hundred-thousand-row import with what we now know: ```php $set = new TaskSet(concurrency: 10); spawn(function () use ($set, $handle, $addressIndex) { while (($row = fgetcsv($handle)) !== false) { $set->spawn(fn() => checkAddress($row[$addressIndex])); } $set->close(); }); foreach ($set as $key => [$result, $error]) { if ($error !== null) { error_log("Address not verified: {$error->getMessage()}"); continue; } saveAddress($pdo, $result); } ``` One coroutine reads the file and keeps feeding it tasks, while the main flow processes results as they become ready. Each processed entry is immediately removed from the set, so memory only holds the tasks in flight: ten running plus the queue. The file can be any size; memory usage doesn't depend on it. Notice how familiar details have come together into a new picture: a concurrency limit instead of a hand-rolled worker pool, `close()` as the signal that "no more tasks are coming," the `[$result, $error]` pair instead of silently swallowed exceptions, and the `PDO` from chapter nine, unbothered by concurrent calls to `saveAddress`. ## Supervisor There's a second scenario where this consuming semantics is indispensable: code that watches over long-lived tasks and reacts when they finish. ```php $set = new TaskSet(); $set->spawnWithKey('mailer', runMailer(...)); $set->spawnWithKey('metrics', runMetrics(...)); $set->spawnWithKey('cleaner', runCleaner(...)); foreach ($set as $key => [$result, $error]) { error_log("Service $key stopped" . ($error ? ": {$error->getMessage()}" : '')); // restart the service that went down $set->spawnWithKey($key, restartService($key)); } ``` The set is never closed, so the `foreach` never finishes, it just waits for the next event. Each processed entry is removed, the restarted service is added back in its place, and the loop lives forever. The supervisor doesn't need a history of every completion since the dawn of time; it needs exactly this: one of its charges stopped, go figure out why and restart it. You couldn't write this loop with `TaskGroup`: its `foreach` would start over from the first service that stopped, every single time. So that, essentially, is the whole difference between the twins: memory. A group stores results and answers any question about them repeatedly, which makes it good for cases where the set of tasks is fixed and the results matter as a whole. A set hands over each result once and immediately frees the memory, which is why it can handle an endless stream of tasks. There's a simple rule for choosing: if your question to the tasks is "what did you come up with?", reach for `TaskGroup`; if it's "what's next?", reach for `TaskSet`. Over the last four chapters we've built the same thing three times: walk a collection, doing concurrent work on each element under a limit. Once with a channel and workers, once with `TaskGroup`, once with `TaskSet`. Isn't it time this pattern got a name of its own and shrank down to a single line? --- --- url: https://true-async.github.io/en/docs/reference/task-set/construct.md description: Create a new TaskSet with optional concurrency limit. --- # TaskSet::\_\_construct (PHP 8.6+, True Async 1.0) ```php public TaskSet::__construct(?int $concurrency = null, ?int $queueLimit = null, ?Async\Scope $scope = null) ``` Creates a new task set with automatic cleanup of results after delivery. ## Parameters **concurrency** : Maximum number of concurrently running coroutines. `null` — no limit, all tasks start immediately. When the limit is reached, new tasks are placed in a queue and started automatically when a slot becomes available. **scope** : Parent scope. TaskSet creates a child scope for its coroutines. `null` — the current scope is inherited. ## Examples ### Example #1 Without limits ```php spawn(fn() => "task 1"); // starts immediately $set->spawn(fn() => "task 2"); // starts immediately $set->spawn(fn() => "task 3"); // starts immediately ``` ### Example #2 With concurrency limit ```php spawn(fn() => "task 1"); // starts immediately $set->spawn(fn() => "task 2"); // starts immediately $set->spawn(fn() => "task 3"); // waits in queue ``` ## See Also * [TaskSet::spawn](/en/docs/reference/task-set/spawn.html) — Add a task * [TaskGroup::\_\_construct](/en/docs/reference/task-group/construct.html) — TaskGroup constructor --- --- url: https://true-async.github.io/en/docs/reference/task-set/await-completion.md description: Wait for all tasks in the set to complete. --- # TaskSet::awaitCompletion (PHP 8.6+, True Async 1.0) ```php public TaskSet::awaitCompletion(): void ``` Suspends the current coroutine until all tasks in the set are completed. The set **must** be closed before calling this method. Unlike `joinAll()`, this method does not throw exceptions on task errors and does not return results. ## Errors Throws `Async\AsyncException` if the set is not closed. ## Examples ### Example #1 Waiting for completion ```php spawn(fn() => processFile("a.txt")); $set->spawn(fn() => processFile("b.txt")); $set->spawn(fn() => throw new \RuntimeException("error")); $set->close(); $set->awaitCompletion(Async\timeout(5000)); // Does not throw even if tasks failed echo "All tasks completed\n"; }); ``` ## See Also * [TaskSet::joinAll](/en/docs/reference/task-set/join-all.html) — Wait and get results * [TaskSet::finally](/en/docs/reference/task-set/finally.html) — Completion handler --- --- url: https://true-async.github.io/en/docs/reference/task-set/cancel.md description: Cancel all tasks in the set. --- # TaskSet::cancel (PHP 8.6+, True Async 1.0) ```php public TaskSet::cancel(?Async\AsyncCancellation $cancellation = null): void ``` Cancels all running coroutines and clears the task queue. Implicitly calls `close()`. ## Parameters **cancellation** : Cancellation reason. If `null`, a default `AsyncCancellation` is created. ## Examples ### Example #1 Conditional cancellation ```php spawn(fn() => longRunningTask1()); $set->spawn(fn() => longRunningTask2()); // Cancel all tasks $set->cancel(); echo $set->isClosed() ? "closed\n" : "no\n"; // "closed" }); ``` ## See Also * [TaskSet::close](/en/docs/reference/task-set/close.html) — Close the set * [TaskSet::dispose](/en/docs/reference/task-set/dispose.html) — Destroy the set scope --- --- url: https://true-async.github.io/en/docs/reference/task-set/close.md description: Close the set for new tasks. --- # TaskSet::close (PHP 8.6+, True Async 1.0) ```php public TaskSet::close(): void ``` Seals the set. After this, `spawn()` and `spawnWithKey()` throw an exception. Already running coroutines and queued tasks continue to work. Repeated calls are a noop. ## Examples ### Example #1 Basic usage ```php spawn(fn() => "task"); $set->close(); try { $set->spawn(fn() => "another task"); } catch (\Async\AsyncException $e) { echo $e->getMessage() . "\n"; // "Cannot spawn tasks on a closed TaskGroup" } }); ``` ## See Also * [TaskSet::cancel](/en/docs/reference/task-set/cancel.html) — Cancel all tasks (implicitly calls close) * [TaskSet::isClosed](/en/docs/reference/task-set/is-closed.html) — Check if the set is closed --- --- url: https://true-async.github.io/en/docs/reference/task-set/count.md description: Get the number of tasks not yet delivered to the consumer. --- # TaskSet::count (PHP 8.6+, True Async 1.0) ```php public TaskSet::count(): int ``` Returns the number of tasks that have not yet been delivered to the consumer. Unlike `TaskGroup::count()`, which returns the total number of tasks, `TaskSet::count()` decreases with each result delivery via `joinNext()`, `joinAny()`, `joinAll()`, or `foreach`. `TaskSet` implements `Countable`, so you can use `count($set)`. ## Return Value The number of tasks in the set. ## Examples ### Example #1 Tracking progress ```php spawn(fn() => "a"); $set->spawn(fn() => "b"); $set->spawn(fn() => "c"); echo $set->count() . "\n"; // 3 $set->joinNext()->await(); echo $set->count() . "\n"; // 2 $set->joinNext()->await(); echo $set->count() . "\n"; // 1 $set->joinNext()->await(); echo $set->count() . "\n"; // 0 }); ``` ## See Also * [TaskSet::isFinished](/en/docs/reference/task-set/is-finished.html) — Check if all tasks are finished * [TaskSet::joinNext](/en/docs/reference/task-set/join-next.html) — Get the next result --- --- url: https://true-async.github.io/en/docs/reference/task-set/dispose.md description: Destroy the task set scope. --- # TaskSet::dispose (PHP 8.6+, True Async 1.0) ```php public TaskSet::dispose(): void ``` Destroys the set scope, cancelling all coroutines. After calling this, the set is completely unusable. ## Examples ### Example #1 Destroying a set ```php spawn(fn() => longRunningTask()); $set->dispose(); }); ``` ## See Also * [TaskSet::cancel](/en/docs/reference/task-set/cancel.html) — Cancel tasks * [TaskSet::close](/en/docs/reference/task-set/close.html) — Close the set --- --- url: https://true-async.github.io/en/docs/reference/task-set/finally.md description: Register a completion handler for the set. --- # TaskSet::finally (PHP 8.6+, True Async 1.0) ```php public TaskSet::finally(Closure $callback): void ``` Registers a callback that is called when the set is closed and all tasks are completed. The callback receives the TaskSet as a parameter. Since `cancel()` implicitly calls `close()`, the handler also fires on cancellation. If the set is already finished, the callback is called synchronously immediately. ## Parameters **callback** : Closure accepting `TaskSet` as its only argument. ## Examples ### Example #1 Logging completion ```php finally(function(TaskSet $s) { echo "Set completed\n"; }); $set->spawn(fn() => "a"); $set->spawn(fn() => "b"); $set->close(); $set->joinAll()->await(); }); // Output: // Set completed ``` ### Example #2 Calling on an already finished set ```php spawn(fn() => 1); $set->close(); $set->joinAll()->await(); // Set is already finished — callback is called synchronously $set->finally(function(TaskSet $s) { echo "called immediately\n"; }); echo "after finally\n"; }); // Output: // called immediately // after finally ``` ## See Also * [TaskSet::close](/en/docs/reference/task-set/close.html) — Close the set * [TaskSet::awaitCompletion](/en/docs/reference/task-set/await-completion.html) — Wait for completion --- --- url: https://true-async.github.io/en/docs/reference/task-set/get-iterator.md description: Get an iterator for traversing results with automatic cleanup. --- # TaskSet::getIterator (PHP 8.6+, True Async 1.0) ```php public TaskSet::getIterator(): Iterator ``` Returns an iterator that yields results **as tasks complete**. TaskSet implements `IteratorAggregate`, so you can use `foreach` directly. **Each processed entry is automatically removed from the set**, freeing memory and decreasing `count()`. ## Iterator Behavior * `foreach` suspends the current coroutine until the next result is available * The key is the same one assigned during `spawn()` or `spawnWithKey()` * The value is an array `[mixed $result, ?Throwable $error]`: * Success: `[$result, null]` * Error: `[null, $error]` * Iteration ends when the set is closed **and** all tasks have been processed * If the set is not closed, `foreach` suspends waiting for new tasks > **Important:** Without calling `close()`, iteration will wait indefinitely. ## Examples ### Example #1 Streaming processing ```php spawn(fn() => processItem($items[$i])); } $set->close(); foreach ($set as $key => [$result, $error]) { if ($error !== null) { echo "Task $key: error — {$error->getMessage()}\n"; continue; } echo "Task $key: done\n"; // Entry removed, memory freed } echo $set->count() . "\n"; // 0 }); ``` ### Example #2 Named keys ```php spawnWithKey('users', fn() => fetchUsers()); $set->spawnWithKey('orders', fn() => fetchOrders()); $set->close(); foreach ($set as $key => [$result, $error]) { if ($error === null) { echo "$key: received " . count($result) . " records\n"; } } }); ``` ## See Also * [TaskSet::close](/en/docs/reference/task-set/close.html) — Close the set * [TaskSet::joinAll](/en/docs/reference/task-set/join-all.html) — Wait for all tasks * [TaskSet::joinNext](/en/docs/reference/task-set/join-next.html) — Next result --- --- url: https://true-async.github.io/en/docs/reference/task-set/is-closed.md description: Check if the set is closed. --- # TaskSet::isClosed (PHP 8.6+, True Async 1.0) ```php public TaskSet::isClosed(): bool ``` Returns `true` if the set is closed (`close()` or `cancel()` was called). ## Return Value `true` if the set is closed. `false` otherwise. ## Examples ### Example #1 Checking state ```php isClosed() ? "yes\n" : "no\n"; // "no" $set->close(); echo $set->isClosed() ? "yes\n" : "no\n"; // "yes" }); ``` ## See Also * [TaskSet::close](/en/docs/reference/task-set/close.html) — Close the set * [TaskSet::isFinished](/en/docs/reference/task-set/is-finished.html) — Check if tasks are finished --- --- url: https://true-async.github.io/en/docs/reference/task-set/is-finished.md description: Check if all tasks in the set are finished. --- # TaskSet::isFinished (PHP 8.6+, True Async 1.0) ```php public TaskSet::isFinished(): bool ``` Returns `true` if there are no active coroutines and the task queue is empty. If the set is not closed, this state may be temporary — new tasks can be added via `spawn()`. ## Return Value `true` if all tasks are finished. `false` otherwise. ## Examples ### Example #1 Checking state ```php isFinished() ? "yes\n" : "no\n"; // "yes" $set->spawn(fn() => "task"); echo $set->isFinished() ? "yes\n" : "no\n"; // "no" $set->close(); $set->joinAll()->await(); echo $set->isFinished() ? "yes\n" : "no\n"; // "yes" }); ``` ## See Also * [TaskSet::isClosed](/en/docs/reference/task-set/is-closed.html) — Check if the set is closed * [TaskSet::count](/en/docs/reference/task-set/count.html) — Number of tasks --- --- url: https://true-async.github.io/en/docs/reference/task-set/join-all.md description: Wait for all tasks and get an array of results with automatic set cleanup. --- # TaskSet::joinAll (PHP 8.6+, True Async 1.0) ```php public TaskSet::joinAll(bool $ignoreErrors = false): Async\Future ``` Returns a `Future` that resolves with an array of results when all tasks are completed. Array keys match the keys assigned during `spawn()` / `spawnWithKey()`. **After delivering the results, all entries are automatically removed from the set**, and `count()` becomes 0. If tasks are already completed, the `Future` resolves immediately. The returned `Future` supports a cancellation token via `await(?Completable $cancellation)`. ## Parameters **ignoreErrors** : If `false` (default) and there are errors, the `Future` rejects with `CompositeException`. If `true`, errors are ignored and the `Future` resolves with only successful results. ## Return Value `Async\Future` — a future result containing an array of task results. Call `->await()` to get the value. ## Errors The `Future` rejects with `Async\CompositeException` if `$ignoreErrors = false` and at least one task finished with an error. ## Examples ### Example #1 Basic usage ```php spawnWithKey('a', fn() => 10); $set->spawnWithKey('b', fn() => 20); $set->spawnWithKey('c', fn() => 30); $set->close(); $results = $set->joinAll()->await(); var_dump($results['a']); // int(10) var_dump($results['b']); // int(20) var_dump($results['c']); // int(30) echo $set->count() . "\n"; // 0 }); ``` ### Example #2 Error handling ```php spawn(fn() => "ok"); $set->spawn(fn() => throw new \RuntimeException("fail")); $set->close(); try { $set->joinAll()->await(); } catch (\Async\CompositeException $e) { foreach ($e->getExceptions() as $ex) { echo $ex->getMessage() . "\n"; // "fail" } } }); ``` ### Example #3 Ignoring errors ```php spawn(fn() => "ok"); $set->spawn(fn() => throw new \RuntimeException("fail")); $set->close(); $results = $set->joinAll(ignoreErrors: true)->await(); echo count($results) . "\n"; // 1 }); ``` ### Example #4 Waiting with timeout ```php spawn(fn() => slowApi()->fetchReport()); $set->spawn(fn() => anotherApi()->fetchStats()); $set->close(); try { $results = $set->joinAll()->await(Async\timeout(5.0)); } catch (Async\TimeoutException) { echo "Failed to get data within 5 seconds\n"; } }); ``` ## See Also * [TaskSet::joinNext](/en/docs/reference/task-set/join-next.html) — Result of the first completed task * [TaskSet::joinAny](/en/docs/reference/task-set/join-any.html) — Result of the first successful task * [TaskGroup::all](/en/docs/reference/task-group/all.html) — Equivalent without auto-cleanup --- --- url: https://true-async.github.io/en/docs/reference/task-set/join-any.md description: >- Get the result of the first successfully completed task with automatic removal from the set. --- # TaskSet::joinAny (PHP 8.6+, True Async 1.0) ```php public TaskSet::joinAny(): Async\Future ``` Returns a `Future` that resolves with the result of the first *successfully* completed task. Tasks that finished with an error are skipped. **After delivering the result, the entry is automatically removed from the set.** Remaining tasks continue running. If all tasks finished with errors, the `Future` rejects with `CompositeException`. The returned `Future` supports a cancellation token via `await(?Completable $cancellation)`. ## Return Value `Async\Future` — a future result of the first successful task. Call `->await()` to get the value. ## Errors * Throws `Async\AsyncException` if the set is empty. * The `Future` rejects with `Async\CompositeException` if all tasks finished with errors. ## Examples ### Example #1 First successful result ```php spawn(fn() => throw new \RuntimeException("fail 1")); $set->spawn(fn() => throw new \RuntimeException("fail 2")); $set->spawn(fn() => "success!"); $result = $set->joinAny()->await(); echo $result . "\n"; // "success!" echo $set->count() . "\n"; // 2 (failed tasks remain) }); ``` ### Example #2 All tasks failed ```php spawn(fn() => throw new \RuntimeException("err 1")); $set->spawn(fn() => throw new \RuntimeException("err 2")); $set->close(); try { $set->joinAny()->await(); } catch (\Async\CompositeException $e) { echo count($e->getExceptions()) . " errors\n"; // "2 errors" } }); ``` ### Example #3 Resilient search ```php spawn(fn() => searchGoogle($query)); $set->spawn(fn() => searchBing($query)); $set->spawn(fn() => searchDuckDuckGo($query)); $result = $set->joinAny()->await(Async\timeout(3.0)); echo "Found, active: {$set->count()}\n"; }); ``` ## See Also * [TaskSet::joinNext](/en/docs/reference/task-set/join-next.html) — First completed (success or error) * [TaskSet::joinAll](/en/docs/reference/task-set/join-all.html) — All results * [TaskGroup::any](/en/docs/reference/task-group/any.html) — Equivalent without auto-cleanup --- --- url: https://true-async.github.io/en/docs/reference/task-set/join-next.md description: >- Get the result of the first completed task with automatic removal from the set. --- # TaskSet::joinNext (PHP 8.6+, True Async 1.0) ```php public TaskSet::joinNext(): Async\Future ``` Returns a `Future` that resolves with the result of the first completed task — whether successful or failed. If the task finished with an error, the `Future` rejects with that exception. **After delivering the result, the entry is automatically removed from the set**, and `count()` decreases by 1. Remaining tasks continue running. If a completed task already exists, the `Future` resolves immediately. The returned `Future` supports a cancellation token via `await(?Completable $cancellation)`. ## Return Value `Async\Future` — a future result of the first completed task. Call `->await()` to get the value. ## Errors * Throws `Async\AsyncException` if the set is empty. * The `Future` rejects with the task's exception if the first completed task failed with an error. ## Examples ### Example #1 Sequential result processing ```php spawn(fn() => fetchUser(1)); $set->spawn(fn() => fetchUser(2)); $set->spawn(fn() => fetchUser(3)); echo "before: count=" . $set->count() . "\n"; // 3 $first = $set->joinNext()->await(); echo "after first: count=" . $set->count() . "\n"; // 2 $second = $set->joinNext()->await(); echo "after second: count=" . $set->count() . "\n"; // 1 }); ``` ### Example #2 Processing loop ```php spawn(fn() => httpClient()->get($url)->getBody()); } $set->close(); while ($set->count() > 0) { try { $body = $set->joinNext()->await(); processResponse($body); } catch (\Throwable $e) { log("Error: {$e->getMessage()}"); } } }); ``` ### Example #3 With timeout ```php spawn(fn() => slowApi()->fetchReport()); $set->spawn(fn() => anotherApi()->fetchStats()); try { $result = $set->joinNext()->await(Async\timeout(5.0)); } catch (Async\TimeoutException) { echo "No task completed within 5 seconds\n"; } }); ``` ## See Also * [TaskSet::joinAny](/en/docs/reference/task-set/join-any.html) — First successful result * [TaskSet::joinAll](/en/docs/reference/task-set/join-all.html) — All results * [TaskGroup::race](/en/docs/reference/task-group/race.html) — Equivalent without auto-cleanup --- --- url: https://true-async.github.io/en/docs/reference/task-set/spawn.md description: Add a task to the set with an auto-increment key. --- # TaskSet::spawn (PHP 8.6+, True Async 1.0) ```php public TaskSet::spawn(callable $task, mixed ...$args): void ``` Adds a callable to the set with an auto-increment key (0, 1, 2, ...). If no concurrency limit is set or a slot is available, the coroutine is created immediately. Otherwise, the callable with arguments is placed in a queue and started when a slot becomes available. ## Parameters **task** : Callable to execute. Accepts any callable: Closure, function, method. **args** : Arguments passed to the callable. ## Errors Throws `Async\AsyncException` if the set is closed (`close()`) or cancelled (`cancel()`). ## Examples ### Example #1 Basic usage ```php spawn(fn() => "first"); $set->spawn(fn() => "second"); $set->close(); $results = $set->joinAll()->await(); var_dump($results[0]); // string(5) "first" var_dump($results[1]); // string(6) "second" }); ``` ### Example #2 With arguments ```php spawn(function(int $a, int $b) { return $a + $b; }, 10, 20); $set->close(); $results = $set->joinAll()->await(); var_dump($results[0]); // int(30) }); ``` ## See Also * [TaskSet::spawnWithKey](/en/docs/reference/task-set/spawn-with-key.html) — Add a task with an explicit key * [TaskSet::joinAll](/en/docs/reference/task-set/join-all.html) — Wait for all tasks --- --- url: https://true-async.github.io/en/docs/reference/task-set/spawn-with-key.md description: Add a task to the set with an explicit key. --- # TaskSet::spawnWithKey (PHP 8.6+, True Async 1.0) ```php public TaskSet::spawnWithKey(string|int $key, callable $task, mixed ...$args): void ``` Adds a callable to the set with a specified key. The key is used in the results array and during iteration via `foreach`. ## Parameters **key** : Result key. Must be unique within the set. **task** : Callable to execute. **args** : Arguments passed to the callable. ## Errors * Throws `Async\AsyncException` if the set is closed or cancelled. * Throws `Async\AsyncException` if the key is already in use. ## Examples ### Example #1 Named tasks ```php spawnWithKey('user', fn() => fetchUser($id)); $set->spawnWithKey('orders', fn() => fetchOrders($id)); $set->close(); $data = $set->joinAll()->await(); echo $data['user']['name']; echo count($data['orders']); }); ``` ## See Also * [TaskSet::spawn](/en/docs/reference/task-set/spawn.html) — Add a task with an auto-key * [TaskSet::joinAll](/en/docs/reference/task-set/join-all.html) — Wait for all tasks --- --- url: https://true-async.github.io/en/docs/components/coroutines.md description: >- The Async\Coroutine class -- creation, lifecycle, states, cancellation, debugging and complete method reference. --- # The Async\Coroutine Class (PHP 8.6+, True Async 1.0) ## Coroutines in TrueAsync When a regular function calls an I/O operation like `fread` or `fwrite` (reading a file or making a network request), control is passed to the operating system kernel, and `PHP` blocks until the operation completes. But if a function is executed inside a coroutine and calls an I/O operation, only the coroutine blocks, not the entire `PHP` process. Meanwhile, control is passed to another coroutine, if one exists. In this sense, coroutines are very similar to operating system threads, but they are managed in user space rather than by the OS kernel. Another important difference is that coroutines share CPU time by taking turns, voluntarily yielding control, while threads can be preempted at any moment. TrueAsync coroutines execute within a single thread and are not parallel. This leads to several important consequences: * Variables can be freely read and modified from different coroutines without locks, since they don't execute simultaneously. * Coroutines cannot simultaneously use multiple CPU cores. * If one coroutine performs a long synchronous operation, it blocks the entire process, since it doesn't yield control to other coroutines. ## Creating a Coroutine A coroutine is created using the `spawn()` function: ```php use function Async\spawn; // Create a coroutine $coroutine = spawn(function() { echo "Hello from a coroutine!\n"; return 42; }); // $coroutine is an object of type Async\Coroutine // The coroutine is already scheduled for execution ``` Once `spawn` is called, the function will be executed asynchronously by the scheduler as soon as possible. ## Passing Parameters The `spawn` function accepts a `callable` and any parameters that will be passed to that function when it starts. ```php function fetchUser(int $userId) { return file_get_contents("https://api/users/$userId"); } // Pass the function and parameters $coroutine = spawn(fetchUser(...), 123); ``` ## Getting the Result To get the result of a coroutine, use `await()`: ```php $coroutine = spawn(function() { sleep(2); return "Done!"; }); echo "Coroutine started\n"; // Wait for the result $result = await($coroutine); echo "Result: $result\n"; ``` **Important:** `await()` blocks the execution of the **current coroutine**, but not the entire `PHP` process. Other coroutines continue running. ## Coroutine Lifecycle A coroutine goes through several states: 1. **Queued** -- created via `spawn()`, waiting to be started by the scheduler 2. **Running** -- currently executing 3. **Suspended** -- paused, waiting for I/O or `suspend()` 4. **Completed** -- finished execution (with a result or an exception) 5. **Cancelled** -- cancelled via `cancel()` ### Checking the State ```php $coro = spawn(longTask(...)); var_dump($coro->isQueued()); // true - waiting to start var_dump($coro->isStarted()); // false - hasn't started yet suspend(); // let the coroutine start var_dump($coro->isStarted()); // true - the coroutine has started var_dump($coro->isRunning()); // false - not currently executing var_dump($coro->isSuspended()); // true - suspended, waiting for something var_dump($coro->isCompleted()); // false - hasn't finished yet var_dump($coro->isCancelled()); // false - not cancelled ``` ## Suspension: suspend The `suspend` keyword stops the coroutine and passes control to the scheduler: ```php spawn(function() { echo "Before suspend\n"; suspend(); // We stop here echo "After suspend\n"; }); echo "Main code\n"; // Output: // Before suspend // Main code // After suspend ``` The coroutine stopped at `suspend`, control returned to the main code. Later, the scheduler resumed the coroutine. ### suspend with waiting Typically `suspend` is used to wait for some event: ```php spawn(function() { echo "Making an HTTP request\n"; $data = file_get_contents('https://api.example.com/data'); // Inside file_get_contents, suspend is implicitly called // While the network request is in progress, the coroutine is suspended echo "Got data: $data\n"; }); ``` PHP automatically suspends the coroutine on I/O operations. You don't need to manually write `suspend`. ## Cancelling a Coroutine ```php $coro = spawn(function() { try { echo "Starting long work\n"; for ($i = 0; $i < 100; $i++) { Async\delay(100); // Sleep 100ms echo "Iteration $i\n"; } echo "Finished\n"; } catch (Async\AsyncCancellation $e) { echo "I was cancelled during iteration\n"; } }); // Let the coroutine work for 1 second Async\delay(1000); // Cancel it $coro->cancel(); // The coroutine will receive AsyncCancellation at the next await/suspend ``` **Important:** Cancellation works cooperatively. The coroutine must check for cancellation (via `await`, `sleep`, or `suspend`). You cannot forcefully kill a coroutine. ## Multiple Coroutines Launch as many as you want: ```php $tasks = []; for ($i = 0; $i < 10; $i++) { $tasks[] = spawn(function() use ($i) { $result = file_get_contents("https://api/data/$i"); return $result; }); } // Wait for all coroutines $results = array_map(fn($t) => await($t), $tasks); echo "Loaded " . count($results) . " results\n"; ``` All 10 requests run concurrently. Instead of 10 seconds (one second each), it completes in ~1 second. ## Error Handling Errors in coroutines are handled with regular `try-catch`: ```php $coro = spawn(function() { throw new Exception("Oops!"); }); try { $result = await($coro); } catch (Exception $e) { echo "Caught error: " . $e->getMessage() . "\n"; } ``` If the error is not caught, it bubbles up to the parent scope: ```php $scope = new Async\Scope(); $scope->spawn(function() { throw new Exception("Error in coroutine!"); }); try { $scope->awaitCompletion(Async\timeout(5000)); } catch (Exception $e) { echo "Error bubbled up to scope: " . $e->getMessage() . "\n"; } ``` ## Coroutine = Object A coroutine is a full-fledged PHP object. You can pass it anywhere: ```php function startBackgroundTask(): Async\Coroutine { return spawn(function() { // Long work Async\delay(10000); return "Result"; }); } $task = startBackgroundTask(); // Pass to another function processTask($task); // Or store in an array $tasks[] = $task; // Or in an object property $this->backgroundTask = $task; ``` ## Nested Coroutines Coroutines can launch other coroutines: ```php spawn(function() { echo "Parent coroutine\n"; $child1 = spawn(function() { echo "Child coroutine 1\n"; return "Result 1"; }); $child2 = spawn(function() { echo "Child coroutine 2\n"; return "Result 2"; }); // Wait for both child coroutines $result1 = await($child1); $result2 = await($child2); echo "Parent received: $result1 and $result2\n"; }); ``` ## Finally: Guaranteed Cleanup Even if a coroutine is cancelled, `finally` will execute: ```php spawn(function() { $file = fopen('data.txt', 'r'); try { while ($line = fgets($file)) { processLine($line); suspend(); // May be cancelled here } } finally { // File will be closed no matter what fclose($file); echo "File closed\n"; } }); ``` ## Debugging Coroutines ### Get the Call Stack ```php $coro = spawn(function() { doSomething(); }); // Get the coroutine's call stack $trace = $coro->getTrace(); print_r($trace); ``` ### Find Out Where a Coroutine Was Created ```php $coro = spawn(someFunction(...)); // Where spawn() was called echo "Coroutine created at: " . $coro->getSpawnLocation() . "\n"; // Output: "Coroutine created at: /app/server.php:42" // Or as an array [filename, lineno] [$file, $line] = $coro->getSpawnFileAndLine(); ``` ### Find Out Where a Coroutine Is Suspended ```php $coro = spawn(function() { file_get_contents('https://api.example.com/data'); // suspends here }); suspend(); // let the coroutine start echo "Suspended at: " . $coro->getSuspendLocation() . "\n"; // Output: "Suspended at: /app/server.php:45" [$file, $line] = $coro->getSuspendFileAndLine(); ``` ### Awaiting Information ```php $coro = spawn(function() { Async\delay(5000); }); suspend(); // Find out what the coroutine is waiting for $info = $coro->getAwaitingInfo(); print_r($info); ``` The method is reserved for debugging. In the current build it always returns an empty array: the extension does not fill it yet. Use `getSpawnLocation()` and `getSuspendLocation()` to see where a coroutine came from and where it stopped. ## Coroutines vs Threads | Coroutines | Threads | |-------------------------------|-------------------------------| | Lightweight | Heavyweight | | Fast creation (<1us) | Slow creation (~1ms) | | Single OS thread | Multiple OS threads | | Cooperative multitasking | Preemptive multitasking | | No race conditions | Race conditions possible | | Requires await points | Can be preempted anywhere | | For I/O operations | For CPU-bound computations | ## Deferred Cancellation with protect() If a coroutine is inside a protected section via `protect()`, cancellation is deferred until the protected block completes: ```php $coro = spawn(function() { $result = protect(function() { // Critical operation -- cancellation is deferred $db->beginTransaction(); $db->execute('INSERT INTO logs ...'); $db->commit(); return "saved"; }); // Cancellation will happen here, after exiting protect() echo "Result: $result\n"; }); suspend(); $coro->cancel(); // Cancellation is deferred -- protect() will complete fully ``` The `isCancellationRequested()` flag becomes `true` immediately, while `isCancelled()` only becomes `true` after the coroutine actually terminates. ## Class Overview ```php final class Async\Coroutine implements Async\Completable { /* Identification */ public getId(): int /* Priority */ public asHiPriority(): Coroutine /* Context */ public getContext(): Async\Context /* Result and errors */ public getResult(): mixed public getException(): mixed /* State */ public isStarted(): bool public isQueued(): bool public isRunning(): bool public isSuspended(): bool public isCompleted(): bool public isCancelled(): bool public isCancellationRequested(): bool /* Control */ public cancel(?Async\AsyncCancellation $cancellation = null): void public finally(\Closure $callback): void /* Debugging */ public getTrace(int $options = DEBUG_BACKTRACE_PROVIDE_OBJECT, int $limit = 0): ?array public getSpawnFileAndLine(): array public getSpawnLocation(): string public getSuspendFileAndLine(): array public getSuspendLocation(): string public getAwaitingInfo(): array } ``` ## Contents * [Coroutine::getId](/en/docs/reference/coroutine/get-id.html) -- Get the unique coroutine identifier * [Coroutine::asHiPriority](/en/docs/reference/coroutine/as-hi-priority.html) -- Mark the coroutine as high-priority * [Coroutine::getContext](/en/docs/reference/coroutine/get-context.html) -- Get the coroutine's local context * [Coroutine::getResult](/en/docs/reference/coroutine/get-result.html) -- Get the execution result * [Coroutine::getException](/en/docs/reference/coroutine/get-exception.html) -- Get the coroutine's exception * [Coroutine::isStarted](/en/docs/reference/coroutine/is-started.html) -- Check if the coroutine has started * [Coroutine::isQueued](/en/docs/reference/coroutine/is-queued.html) -- Check if the coroutine is queued * [Coroutine::isRunning](/en/docs/reference/coroutine/is-running.html) -- Check if the coroutine is currently running * [Coroutine::isSuspended](/en/docs/reference/coroutine/is-suspended.html) -- Check if the coroutine is suspended * [Coroutine::isCompleted](/en/docs/reference/coroutine/is-completed.html) -- Check if the coroutine has completed * [Coroutine::isCancelled](/en/docs/reference/coroutine/is-cancelled.html) -- Check if the coroutine was cancelled * [Coroutine::isCancellationRequested](/en/docs/reference/coroutine/is-cancellation-requested.html) -- Check if cancellation was requested * [Coroutine::cancel](/en/docs/reference/coroutine/cancel.html) -- Cancel the coroutine * [Coroutine::finally](/en/docs/reference/coroutine/on-finally.html) -- Register a completion handler * [Coroutine::getTrace](/en/docs/reference/coroutine/get-trace.html) -- Get the call stack of a suspended coroutine * [Coroutine::getSpawnFileAndLine](/en/docs/reference/coroutine/get-spawn-file-and-line.html) -- Get the file and line where the coroutine was created * [Coroutine::getSpawnLocation](/en/docs/reference/coroutine/get-spawn-location.html) -- Get the creation location as a string * [Coroutine::getSuspendFileAndLine](/en/docs/reference/coroutine/get-suspend-file-and-line.html) -- Get the file and line where the coroutine was suspended * [Coroutine::getSuspendLocation](/en/docs/reference/coroutine/get-suspend-location.html) -- Get the suspension location as a string * [Coroutine::getAwaitingInfo](/en/docs/reference/coroutine/get-awaiting-info.html) -- Get awaiting information ## What's Next * [Scope](/en/docs/components/scope.html) -- managing groups of coroutines * [Cancellation](/en/docs/components/cancellation.html) -- details about cancellation and protect() * [spawn()](/en/docs/reference/spawn.html) -- complete documentation * [await()](/en/docs/reference/await.html) -- complete documentation --- --- url: https://true-async.github.io/en/docs/components/task-group.md description: >- Async\TaskGroup -- a high-level structured concurrency pattern for managing groups of tasks. --- # The Async\TaskGroup Class (PHP 8.6+, True Async 1.0) ## Introduction When working with coroutines, you often need to launch several tasks and wait for their results. Using `spawn()` and `await()` directly, the developer takes responsibility for ensuring that every coroutine is either awaited or cancelled. A forgotten coroutine keeps running, an unhandled error is lost, and cancelling a group of tasks requires manual code. The `await_all()` and `await_any()` functions don't account for logical relationships between different tasks. For example, when you need to make several requests, take the first result, and cancel the rest, `await_any()` requires additional code from the programmer to cancel the remaining tasks. Such code can be quite complex, so `await_all()` and `await_any()` should be considered anti-patterns in this situation. Using `Scope` for this purpose is not suitable, since task coroutines may create other child coroutines, which requires the programmer to maintain a list of task coroutines and track them separately. **TaskGroup** solves all these problems. It is a high-level structured concurrency pattern that guarantees: all tasks will be properly awaited or cancelled. It logically groups tasks and allows operating on them as a single unit. ## Waiting Strategies `TaskGroup` provides several strategies for waiting on results. Each returns a `Future`, which allows passing a timeout: `->await(Async\timeout(5.0))`. * **`all()`** -- returns a `Future` that resolves with an array of all task results, or rejects with `CompositeException` if at least one task threw an exception. With the `ignoreErrors: true` parameter, returns only successful results. * **`race()`** -- returns a `Future` that resolves with the result of the first completed task, regardless of whether it completed successfully or not. Other tasks continue running. * **`any()`** -- returns a `Future` that resolves with the result of the first *successfully* completed task, ignoring errors. If all tasks failed -- rejects with `CompositeException`. * **`awaitCompletion()`** -- waits for full completion of all tasks, as well as other coroutines in the `Scope`. ## Concurrency Limit When the `concurrency` parameter is specified, `TaskGroup` works as a coroutine pool: tasks exceeding the limit wait in a queue and don't create a coroutine until a free slot appears. This saves memory and controls load when processing a large number of tasks. ## TaskGroup and Scope `TaskGroup` uses `Scope` for managing the lifecycle of task coroutines. When creating a `TaskGroup`, you can pass an existing `Scope` or let `TaskGroup` create a child `Scope` from the current one. All tasks added to `TaskGroup` execute inside this `Scope`. This means that when `TaskGroup` is cancelled or destroyed, all coroutines will be automatically cancelled, ensuring safe resource management and preventing leaks. ## Closing and Iteration `TaskGroup` allows adding tasks dynamically, until it is closed using the `close()` method. The `all()` method returns a `Future` that triggers when all existing tasks in the queue are completed. This allows using `TaskGroup` in a loop, where tasks are added dynamically, and `all()` is called to get results of the current set of tasks. `TaskGroup` also supports `foreach` for iterating over results as they become ready. In this case, `close()` must be called after adding all tasks to signal that there will be no new tasks, and `foreach` can finish after processing all results. ## Class Overview ```php final class Async\TaskGroup implements Async\Awaitable, Countable, IteratorAggregate { /* Methods */ public __construct(?int $concurrency = null, ?Async\Scope $scope = null) /* Adding tasks */ public spawn(callable $task, mixed ...$args): void public spawnWithKey(string|int $key, callable $task, mixed ...$args): void /* Waiting for results */ public all(bool $ignoreErrors = false): Async\Future public race(): Async\Future public any(): Async\Future public awaitCompletion(): void /* Lifecycle */ public close(): void public cancel(?Async\AsyncCancellation $cancellation = null): void public dispose(): void public finally(Closure $callback): void /* State */ public isFinished(): bool public isClosed(): bool public count(): int /* Results and errors */ public getResults(): array public getErrors(): array public suppressErrors(): void /* Iteration */ public getIterator(): Iterator } ``` ## Examples ### all() -- Parallel Data Loading The most common scenario -- loading data from multiple sources simultaneously: ```php $group = new Async\TaskGroup(); $group->spawnWithKey('user', fn() => $db->query('SELECT * FROM users WHERE id = ?', [$id])); $group->spawnWithKey('orders', fn() => $db->query('SELECT * FROM orders WHERE user_id = ?', [$id])); $group->spawnWithKey('reviews', fn() => $api->get("/users/{$id}/reviews")); $data = $group->all()->await(); // ['user' => ..., 'orders' => [...], 'reviews' => [...]] return new UserProfile($data['user'], $data['orders'], $data['reviews']); ``` All three requests execute in parallel. If any of them throws an exception, `all()` returns a `Future` that rejects with `CompositeException`. ### race() -- Hedged Requests The "hedged request" pattern -- send the same request to multiple replicas and take the first response. This reduces latency with slow or overloaded servers: ```php $replicas = ['db-replica-1', 'db-replica-2', 'db-replica-3']; $group = new Async\TaskGroup(); foreach ($replicas as $host) { $group->spawn(fn() => pg_query($host, 'SELECT * FROM products WHERE id = 42')); } // First response is the result, other tasks continue running $product = $group->race()->await(); ``` ### any() -- Error-Tolerant Search Query multiple providers, take the first successful response, ignoring errors: ```php $group = new Async\TaskGroup(); $group->spawn(fn() => searchGoogle($query)); $group->spawn(fn() => searchBing($query)); $group->spawn(fn() => searchDuckDuckGo($query)); // any() ignores providers that failed and returns the first successful result $results = $group->any()->await(); // Errors from failed providers must be explicitly handled, otherwise the destructor will throw an exception $group->suppressErrors(); ``` If all providers failed, `any()` will throw `CompositeException` with all errors. ### Concurrency Limit -- Processing a Queue Process 10,000 tasks, but no more than 50 simultaneously: ```php $group = new Async\TaskGroup(concurrency: 50); foreach ($urls as $url) { $group->spawn(fn() => httpClient()->get($url)->getBody()); } $results = $group->all()->await(); ``` `TaskGroup` automatically queues tasks. A coroutine is created only when a free slot appears, saving memory with large volumes of tasks. ### Iterating Over Results as They Complete Process results without waiting for all tasks to finish: ```php $group = new Async\TaskGroup(); foreach ($imageFiles as $file) { $group->spawn(fn() => processImage($file)); } $group->close(); foreach ($group as $key => $result) { // Results arrive as they become ready, not in the order they were added saveToStorage($result); } ``` ### Timeout for a Task Group Limit the waiting time for results: ```php $group = new Async\TaskGroup(); $group->spawn(fn() => slowApi()->fetchReport()); $group->spawn(fn() => anotherApi()->fetchStats()); $group->close(); try { $results = $group->all()->await(Async\timeout(5.0)); } catch (Async\TimeoutException) { echo "Failed to get data within 5 seconds"; } ``` ## Analogues in Other Languages | Capability | PHP `TaskGroup` | Python `asyncio.TaskGroup` | Java `StructuredTaskScope` | Kotlin `coroutineScope` | |-------------------------|-------------------------------------|---------------------------------|------------------------------------------|---------------------------| | Structured concurrency | `close()` + `all()->await()` | `async with` block | `try-with-resources` + `join()` | Automatically via scope | | Waiting strategies | `all()`, `race()`, `any()` -> Future | Only all (via `async with`) | `ShutdownOnSuccess`, `ShutdownOnFailure` | `async`/`await`, `select` | | Concurrency limit | `concurrency: N` | No (need `Semaphore`) | No | No (need `Semaphore`) | | Result iteration | `foreach` as they complete | No | No | `Channel` | | Error handling | `CompositeException`, `getErrors()` | `ExceptionGroup` | `throwIfFailed()` | Exception cancels scope | PHP `TaskGroup` combines capabilities that in other languages are spread across multiple primitives: concurrency limiting without a semaphore, multiple waiting strategies in a single object, and result iteration as they complete. ## Contents * [TaskGroup::\_\_construct](/en/docs/reference/task-group/construct.html) -- Create a task group * [TaskGroup::spawn](/en/docs/reference/task-group/spawn.html) -- Add a task with an auto-increment key * [TaskGroup::spawnWithKey](/en/docs/reference/task-group/spawn-with-key.html) -- Add a task with an explicit key * [TaskGroup::all](/en/docs/reference/task-group/all.html) -- Wait for all tasks and get results * [TaskGroup::race](/en/docs/reference/task-group/race.html) -- Get the result of the first completed task * [TaskGroup::any](/en/docs/reference/task-group/any.html) -- Get the result of the first successful task * [TaskGroup::awaitCompletion](/en/docs/reference/task-group/await-completion.html) -- Wait for all tasks to complete * [TaskGroup::close](/en/docs/reference/task-group/close.html) -- Close the group for new tasks * [TaskGroup::cancel](/en/docs/reference/task-group/cancel.html) -- Cancel all tasks * [TaskGroup::dispose](/en/docs/reference/task-group/dispose.html) -- Destroy the group's scope * [TaskGroup::finally](/en/docs/reference/task-group/finally.html) -- Register a completion handler * [TaskGroup::isFinished](/en/docs/reference/task-group/is-finished.html) -- Check if all tasks are finished * [TaskGroup::isClosed](/en/docs/reference/task-group/is-closed.html) -- Check if the group is closed * [TaskGroup::count](/en/docs/reference/task-group/count.html) -- Get the number of tasks * [TaskGroup::getResults](/en/docs/reference/task-group/get-results.html) -- Get an array of successful results * [TaskGroup::getErrors](/en/docs/reference/task-group/get-errors.html) -- Get an array of errors * [TaskGroup::suppressErrors](/en/docs/reference/task-group/suppress-errors.html) -- Mark errors as handled * [TaskGroup::getIterator](/en/docs/reference/task-group/get-iterator.html) -- Iterate over results as they complete --- --- url: https://true-async.github.io/en/docs/components/task-set.md description: >- Async\TaskSet — a dynamic task set with automatic result cleanup after delivery. --- # The Async\TaskSet Class (PHP 8.6+, True Async 1.0) ## Introduction `TaskGroup` is perfect for scenarios where the goal is the results, not the tasks themselves. However, there are many situations where you need to control the number of tasks while results are consumed as a stream. Typical examples: * **Supervisor**: code that monitors tasks and reacts to their completion. * **Coroutine pool**: a fixed number of coroutines processing data. **TaskSet** is designed to solve these problems. It automatically removes completed tasks at the moment of result delivery via `joinNext()`, `joinAll()`, `joinAny()`, or `foreach`. ## Differences from TaskGroup | Property | TaskGroup | TaskSet | |---------------------------|------------------------------------|--------------------------------------------| | Result storage | All results until explicit request | Removed after delivery | | Repeated method calls | Idempotent — same result | Each call — next element | | `count()` | Total number of tasks | Number of undelivered tasks | | Waiting methods | `all()`, `race()`, `any()` | `joinAll()`, `joinNext()`, `joinAny()` | | Iteration | Entries remain | Entries removed after `foreach` | | Use case | Fixed set of tasks | Dynamic task stream | ## Idempotency vs Consumption **The key conceptual difference** between `TaskSet` and `TaskGroup`. **TaskGroup is idempotent.** Calls to `race()`, `any()`, `all()` always return the same result. Iteration via `foreach` always traverses all tasks. Results are stored in the group and available for repeated access: ```php $group = new Async\TaskGroup(); $group->spawn(fn() => "alpha"); $group->spawn(fn() => "beta"); $group->spawn(fn() => "gamma"); $group->close(); // race() always returns the same first completed task $first = $group->race()->await(); // "alpha" $same = $group->race()->await(); // "alpha" — same result! // all() always returns the full array $all1 = $group->all()->await(); // ["alpha", "beta", "gamma"] $all2 = $group->all()->await(); // ["alpha", "beta", "gamma"] — same array! // foreach always traverses all elements foreach ($group as $key => [$result, $error]) { /* 3 iterations */ } foreach ($group as $key => [$result, $error]) { /* again 3 iterations */ } echo $group->count(); // 3 — always 3 ``` **TaskSet is consuming.** Each call to `joinNext()` / `joinAny()` extracts the next element and removes it from the set. A repeated `foreach` won't find already delivered entries. This behavior is analogous to reading from a queue or channel: ```php $set = new Async\TaskSet(); $set->spawn(fn() => "alpha"); $set->spawn(fn() => "beta"); $set->spawn(fn() => "gamma"); // joinNext() returns the NEXT result each time $first = $set->joinNext()->await(); // "alpha" $second = $set->joinNext()->await(); // "beta" — different result! $third = $set->joinNext()->await(); // "gamma" echo $set->count(); // 0 — set is empty // joinAll() after full consumption — empty array $set->close(); $rest = $set->joinAll()->await(); // [] — nothing to return ``` The same logic applies to iteration: ```php $set = new Async\TaskSet(); $set->spawn(fn() => "alpha"); $set->spawn(fn() => "beta"); $set->spawn(fn() => "gamma"); $set->close(); // First foreach consumes all results foreach ($set as $key => [$result, $error]) { echo "$result\n"; // "alpha", "beta", "gamma" } echo $set->count(); // 0 // Second foreach — empty, nothing to iterate foreach ($set as $key => [$result, $error]) { echo "this won't execute\n"; } ``` > **Rule:** if you need to access results repeatedly — use `TaskGroup`. > If results are processed once and should free memory — use `TaskSet`. ## Join Method Semantics Unlike `TaskGroup`, where `race()` / `any()` / `all()` leave entries in the group, `TaskSet` uses methods with **join** semantics — result delivered, entry removed: * **`joinNext()`** — analogous to `race()`: result of the first completed task (success or error), entry is removed from the set. * **`joinAny()`** — analogous to `any()`: result of the first *successfully* completed task, entry is removed from the set. Errors are skipped. * **`joinAll()`** — analogous to `all()`: array of all results, all entries are removed from the set. ## Automatic Cleanup Auto-cleanup works at all result delivery points: ```php $set = new Async\TaskSet(); $set->spawn(fn() => "a"); $set->spawn(fn() => "b"); echo $set->count(); // 2 $set->joinNext()->await(); echo $set->count(); // 1 $set->joinNext()->await(); echo $set->count(); // 0 ``` When iterating via `foreach`, each processed entry is removed immediately: ```php $set = new Async\TaskSet(); foreach ($urls as $url) { $set->spawn(fn() => fetch($url)); } $set->close(); foreach ($set as $key => [$result, $error]) { // $set->count() decreases with each iteration process($result); } ``` ## Concurrency Limit Like `TaskGroup`, `TaskSet` supports concurrency limiting: ```php $set = new Async\TaskSet(concurrency: 10); foreach ($tasks as $task) { $set->spawn(fn() => processTask($task)); } ``` Tasks exceeding the limit are queued and started when a slot becomes available. ## Class Synopsis ```php final class Async\TaskSet implements Async\Awaitable, Countable, IteratorAggregate { /* Methods */ public __construct(?int $concurrency = null, ?Async\Scope $scope = null) /* Adding tasks */ public spawn(callable $task, mixed ...$args): void public spawnWithKey(string|int $key, callable $task, mixed ...$args): void /* Waiting for results (with auto-cleanup) */ public joinNext(): Async\Future public joinAny(): Async\Future public joinAll(bool $ignoreErrors = false): Async\Future /* Lifecycle */ public close(): void public cancel(?Async\AsyncCancellation $cancellation = null): void public dispose(): void public finally(Closure $callback): void /* State */ public isFinished(): bool public isClosed(): bool public count(): int /* Awaiting completion */ public awaitCompletion(): void /* Iteration (with auto-cleanup) */ public getIterator(): Iterator } ``` ## Examples ### joinAll() — parallel loading with auto-cleanup ```php $set = new Async\TaskSet(); $set->spawnWithKey('user', fn() => $db->query('SELECT * FROM users WHERE id = ?', [$id])); $set->spawnWithKey('orders', fn() => $db->query('SELECT * FROM orders WHERE user_id = ?', [$id])); $set->spawnWithKey('reviews', fn() => $api->get("/users/{$id}/reviews")); $set->close(); $data = $set->joinAll()->await(); // $set->count() === 0, all entries removed return new UserProfile($data['user'], $data['orders'], $data['reviews']); ``` ### joinNext() — processing tasks as they complete ```php $set = new Async\TaskSet(concurrency: 5); foreach ($urls as $url) { $set->spawn(fn() => httpClient()->get($url)->getBody()); } $set->close(); while ($set->count() > 0) { $result = $set->joinNext()->await(); echo "Got result, remaining: {$set->count()}\n"; } ``` ### joinAny() — fault-tolerant search ```php $set = new Async\TaskSet(); $set->spawn(fn() => searchProvider1($query)); $set->spawn(fn() => searchProvider2($query)); $set->spawn(fn() => searchProvider3($query)); // First successful result, entry removed $result = $set->joinAny()->await(); echo "Found, active tasks: {$set->count()}\n"; ``` ### foreach — streaming processing ```php $set = new Async\TaskSet(concurrency: 20); foreach ($imageFiles as $file) { $set->spawn(fn() => processImage($file)); } $set->close(); foreach ($set as $key => [$result, $error]) { if ($error !== null) { log("Error processing $key: {$error->getMessage()}"); continue; } saveToStorage($result); // Entry removed, memory freed } ``` ### Worker loop with dynamic task addition ```php $set = new Async\TaskSet(concurrency: 10); // One coroutine adds tasks spawn(function() use ($set, $queue) { while ($message = $queue->receive()) { $set->spawn(fn() => processMessage($message)); } $set->close(); }); // Another processes results spawn(function() use ($set) { foreach ($set as $key => [$result, $error]) { if ($error !== null) { log("Error: {$error->getMessage()}"); } } }); ``` ## Equivalents in Other Languages | Feature | PHP `TaskSet` | Python `asyncio` | Kotlin | Go | |----------------------|-----------------------------------|-------------------------------|---------------------------|------------------------| | Dynamic set | `spawn()` + `joinNext()` | `asyncio.as_completed()` | `Channel` + `select` | `errgroup` + `chan` | | Auto-cleanup | Automatic | Manual management | Manual management | Manual management | | Concurrency limit | `concurrency: N` | `Semaphore` | `Semaphore` | Buffered channel | | Streaming iteration | `foreach` | `async for` + `as_completed` | `for` + `Channel` | `for range` + `chan` | ## Contents * [TaskSet::\_\_construct](/en/docs/reference/task-set/construct.html) — Create a task set * [TaskSet::spawn](/en/docs/reference/task-set/spawn.html) — Add a task with an auto-increment key * [TaskSet::spawnWithKey](/en/docs/reference/task-set/spawn-with-key.html) — Add a task with an explicit key * [TaskSet::joinNext](/en/docs/reference/task-set/join-next.html) — Get the result of the first completed task * [TaskSet::joinAny](/en/docs/reference/task-set/join-any.html) — Get the result of the first successful task * [TaskSet::joinAll](/en/docs/reference/task-set/join-all.html) — Wait for all tasks and get results * [TaskSet::close](/en/docs/reference/task-set/close.html) — Close the set for new tasks * [TaskSet::cancel](/en/docs/reference/task-set/cancel.html) — Cancel all tasks * [TaskSet::dispose](/en/docs/reference/task-set/dispose.html) — Destroy the set's scope * [TaskSet::finally](/en/docs/reference/task-set/finally.html) — Register a completion handler * [TaskSet::isFinished](/en/docs/reference/task-set/is-finished.html) — Check if all tasks are finished * [TaskSet::isClosed](/en/docs/reference/task-set/is-closed.html) — Check if the set is closed * [TaskSet::count](/en/docs/reference/task-set/count.html) — Get the number of undelivered tasks * [TaskSet::awaitCompletion](/en/docs/reference/task-set/await-completion.html) — Wait for all tasks to complete * [TaskSet::getIterator](/en/docs/reference/task-set/get-iterator.html) — Iterate over results with auto-cleanup --- --- url: https://true-async.github.io/en/tutors-laravel/05-third-party.md description: >- Debugbar, Telescope, Inertia, spatie/permission, Socialite: what's already adapted for coroutines and what's worth disabling. --- # Third-Party Packages We've covered Laravel's core and your own code. But a real project's dependency list doesn't stop there: `Debugbar` in development, `Telescope` for logs, `Inertia` for the frontend, `spatie/laravel-permission` for roles. Every one of them was written long before anyone pictured hundreds of concurrent requests inside a single process, and every one of them has its own singleton carrying state. The good news: for the most common packages this work is already done. The bad news: not for all of them, and it matters to be able to tell one from the other. ## Already Adapted **`spatie/laravel-permission`.** `PermissionRegistrar` keeps the current `team ID` and a wildcard-permission index in its own properties, exactly the pattern covered in the chapter on unsafe patterns. `AsyncPermissionRegistrar` moves both into `current_context()`: ```php class AsyncPermissionRegistrar extends PermissionRegistrar { public function setPermissionsTeamId(int|string|Model|null $id): void { current_context()->set(self::CTX_TEAM_ID, $id, replace: true); } public function getPermissionsTeamId(): int|string|null { return current_context()->find(self::CTX_TEAM_ID); } // clearPermissionsCollection() becomes a no-op: the permission list // itself is read-only after loading and safely shared across requests. } ``` Nothing changes in your own code: `Auth::user()->can(...)`, `setPermissionsTeamId()` in middleware, all work exactly as the package's docs describe. **`inertiajs/inertia-laravel`.** `AsyncResponseFactory` moves `sharedProps`, `rootView`, `version`, and `encryptHistory`, everything that used to accumulate on the `ResponseFactory` singleton's properties and would have survived past the end of a request into the next one, into context. **`barryvdh/laravel-debugbar`.** The solution here is a bit more nuanced. `Debugbar` collects data over the whole request: SQL queries, messages, timings, a classic accumulating collector that writes into itself for the entire handling cycle, including pauses on `await`. `AsyncDebugbar` doesn't resolve a new instance per request (that would break one-time event subscriptions), it keeps one `Debugbar` per worker, but makes the collectors themselves context-aware: ```php class AsyncDebugbar extends LaravelDebugbar { public function __construct(Application $app, Request $request) { parent::__construct($app, $request); // These accumulate data across every I/O pause in the request, // so storage needs to be per-coroutine, not per-instance. $this->messagesCollector = new AsyncMessagesCollector(); $this->timeCollector = new AsyncTimeDataCollector(...); $this->exceptionsCollector = new AsyncExceptionsCollector(); } } ``` The difference from `ScopedServiceProxy` in the first chapter matters: there, the whole service was resolved fresh per request; here, the service stays a single instance (it's expensive to recreate: event subscriptions, configuration), and only the specific accumulating collectors inside it become context-aware. Same diagnosis as the chapter on unsafe patterns ("don't write into state shared between requests"), just a cure tailored to this package's particular anatomy. **`laravel/telescope`.** Similarly: `entriesQueue`, `updatesQueue`, and the `shouldRecord` flag move into context through a `CoroutineSafeRecording` trait, and the decision whether to log a given request is made per coroutine. **`laravel/socialite`.** Simpler here: `SocialiteManager` caches drivers together with the config of whichever request reached them first. The fix isn't an adapter, it's a `scopedSingleton`, the same "approach one" from the first chapter: a fresh manager per request, no context needed at all. ## Safe Without Adaptation `Cache`, `Queue`, `Mail`, `Log`, `Validation`, `Filesystem`, `HTTP Client`, `Notifications`, `Encryption`, `Hashing`, `Pagination`, `Sanctum`, `Passport`, `Scout`, `Cashier`, `Horizon`, these have singletons too, but what they hold is configuration and clients, not per-request data. `CacheManager` caches the `Redis` client object, not the values you put into it; `MailManager` caches the configured `Mailer`, not the emails. Same question from the chapter on unsafe patterns: does the state survive the end of a request while staying correct? Here the answer is yes, because the state is connection configuration, not one user's data. ## Incompatible: Turn Them Off **`livewire/livewire`.** This is where it's worth pausing and not carrying the previous optimism forward. `LivewireManager` accumulates per-request state deep inside itself, and `wire:stream` is built on assumptions about a buffered, one-shot response that the concurrent coroutine model itself breaks. Adapting it piecemeal hasn't worked, not for `laravel-spawn`, and not for `Laravel Octane` before it. The only recommendation that actually holds up is not to enable `Livewire` in async mode at all. For interactive interfaces on this stack, use `Inertia`, it's already adapted and covered above. `Filament`, built on top of `Livewire`, inherits the same limitation. ## Deciding On Your Own Package If the package you need isn't on either list, the question is the same one from the previous chapter, just aimed at someone else's code this time: does the package's singleton hold mutable state that's only supposed to be correct for one request? If so, there are three paths, and they aren't equivalent. The fastest is `scoped_services` in the config, "approach one": the package gets resolved fresh on every request, and its own code stays untouched. ```php // config/async.php 'scoped_services' => [ \SomePackage\Manager::class, ], ``` If recreating it is expensive (the package is costly to initialize, or caches something useful across requests on its own), write a targeted adapter following the pattern of `AsyncPermissionRegistrar` or `AsyncDebugbar` from this chapter: subclass it, add a `bootCompleted()`, move only what actually changes from request to request into `current_context()`, leave the rest as is. And if it's not a standalone service but a deeply ingrained pattern like `Livewire`, don't spend a week adapting it only to discover `wire:stream` broken for architectural reasons a week later. Run the `MutableStaticPropertyRule` from the previous chapter against the package's source: if the findings number in the dozens and they hit the package's very core rather than safe boot-time caches, that's a signal to exclude it from async mode, not to fix it. We've covered your own code, Laravel's core, and its surrounding ecosystem. What's left is to see the numbers: how much faster does an application actually get once all of this is in place. --- --- url: https://true-async.github.io/en/docs/reference/thread/cancel.md description: Request cancellation of a thread. --- # Thread::cancel (PHP 8.6+, True Async 1.0) ```php public Thread::cancel(?Async\AsyncCancellation $cancellation = null): void ``` Requests cancellation of the thread. Cancellation is **cooperative** — the thread is not interrupted immediately. It must react to the cancellation request on its own: for example, through coroutine suspension points inside the thread or by explicitly checking the cancellation state. Once the thread has actually stopped, `isCancelled()` will return `true`. ## Parameters **cancellation** : The cancellation reason object. If `null`, a default `AsyncCancellation` is created. ## Examples ### Example #1 Cancelling a long-running thread ```php cancel(); await($thread); echo $thread->isCancelled() ? "Thread successfully cancelled\n" : "Thread finished before cancellation\n"; }); ``` ### Example #2 Cancellation with a reason ```php cancel(new \Async\AsyncCancellation("Operation timeout exceeded")); await($thread); if ($thread->isCancelled()) { echo "Thread cancelled\n"; } }); ``` ### Example #3 Cancelling multiple threads ```php spawn_thread(fn() => sleep(30)), range(1, 5) ); // Cancel all at once foreach ($threads as $thread) { $thread->cancel(); } foreach ($threads as $i => $thread) { await($thread); echo "Thread $i cancelled: " . ($thread->isCancelled() ? 'yes' : 'no') . "\n"; } }); ``` ## See Also * [Thread::isCancelled()](/en/docs/reference/thread/is-cancelled.html) — Check cancellation * [Thread::isCompleted()](/en/docs/reference/thread/is-completed.html) — Check completion * [Async\Thread](/en/docs/components/threads.html) — Thread component --- --- url: https://true-async.github.io/en/docs/reference/thread/finally.md description: Register a callback on thread completion. --- # Thread::finally (PHP 8.6+, True Async 1.0) ```php public Thread::finally(\Closure $callback): void ``` Registers a callback function that will be executed when the thread finishes — regardless of whether it completed successfully, with an exception, or was cancelled. The callback is executed in the **coroutine scheduler of the parent thread**. Multiple callbacks may be registered; they are called in registration order. The callback takes no arguments — to obtain the result or exception, use `getResult()` / `getException()` inside it. ## Parameters **callback** : A function with no parameters, called when the thread finishes. ## Examples ### Example #1 Releasing a resource after thread completion ```php finally(function() use ($resource, $thread) { releaseResource($resource); echo "Resource released. Thread cancelled: " . ($thread->isCancelled() ? 'yes' : 'no') . "\n"; }); }); ``` ### Example #2 Logging thread result ```php finally(function() use ($thread) { if ($thread->isCancelled()) { echo "[log] Thread cancelled\n"; } elseif ($thread->getException() !== null) { echo "[log] Thread finished with error: " . $thread->getException()->getMessage() . "\n"; } else { echo "[log] Thread finished. Result: " . $thread->getResult() . "\n"; } }); await($thread); }); ``` ### Example #3 Multiple callbacks ```php "result"); $thread->finally(function() { echo "First callback\n"; }); $thread->finally(function() { echo "Second callback\n"; }); $thread->finally(function() { echo "Third callback\n"; }); await($thread); // Output: // First callback // Second callback // Third callback }); ``` ## See Also * [Thread::isCompleted()](/en/docs/reference/thread/is-completed.html) — Check completion * [Thread::getResult()](/en/docs/reference/thread/get-result.html) — Get result * [Thread::getException()](/en/docs/reference/thread/get-exception.html) — Get exception * [Thread::isCancelled()](/en/docs/reference/thread/is-cancelled.html) — Check cancellation * [Async\Thread](/en/docs/components/threads.html) — Thread component --- --- url: https://true-async.github.io/en/docs/reference/thread/get-exception.md description: Get the exception with which the thread finished. --- # Thread::getException (PHP 8.6+, True Async 1.0) ```php public Thread::getException(): mixed ``` Returns an `Async\RemoteException` if the thread finished with an exception. Returns `null` if the thread has not yet finished, finished successfully, or was cancelled. `RemoteException` is a wrapper around the original exception from the child thread. Use the `getRemoteException()` and `getRemoteClass()` methods of the `RemoteException` object to access the details of the original error. ## Return Value `Async\RemoteException|null` — a wrapper around the thread's exception, or `null`. ## Examples ### Example #1 Distinguishing successful completion from an error ```php isCompleted()) { $exception = $thread->getException(); if ($exception !== null) { echo "Thread finished with error: " . $exception->getMessage() . "\n"; echo "Original class: " . $exception->getRemoteClass() . "\n"; } else { echo "Result: " . $thread->getResult() . "\n"; } } }); ``` ### Example #2 Handling RemoteException without await() ```php isCompleted()) { suspend(); } $exc = $thread->getException(); if ($exc instanceof \Async\RemoteException) { echo "Original exception class: " . $exc->getRemoteClass() . "\n"; echo "Message: " . $exc->getMessage() . "\n"; } }); ``` ## See Also * [Thread::getResult()](/en/docs/reference/thread/get-result.html) — Get result * [Thread::isCompleted()](/en/docs/reference/thread/is-completed.html) — Check completion * [await()](/en/docs/reference/await.html) — Wait with exception propagation * [Async\Thread](/en/docs/components/threads.html) — Thread component --- --- url: https://true-async.github.io/en/docs/reference/thread/get-result.md description: Get the result of thread execution. --- # Thread::getResult (PHP 8.6+, True Async 1.0) ```php public Thread::getResult(): mixed ``` Returns the value returned by the thread function if the thread finished successfully. Returns `null` if the thread has not yet finished, finished with an exception, or was cancelled. **Important:** this method does not throw exceptions and does not wait for the thread to finish. For blocking wait with exception propagation, use `await()`. To obtain an error, use `getException()`. ## Return Value `mixed` — the result of the thread function, or `null`. ## Examples ### Example #1 Getting the result after isCompleted() ```php isCompleted() && $thread->getException() === null) { echo "Result: " . $thread->getResult() . "\n"; // 500500 } }); ``` ### Example #2 Comparison with await() ```php getMessage() . "\n"; } // Option 2: getResult() — does not wait, does not throw $thread2 = spawn_thread(fn() => "other data"); await($thread2); echo "getResult: " . $thread2->getResult() . "\n"; }); ``` ## See Also * [Thread::getException()](/en/docs/reference/thread/get-exception.html) — Get exception * [Thread::isCompleted()](/en/docs/reference/thread/is-completed.html) — Check completion * [await()](/en/docs/reference/await.html) — Wait for result with exceptions * [spawn\_thread()](/en/docs/reference/spawn-thread.html) — Start a thread * [Async\Thread](/en/docs/components/threads.html) — Thread component --- --- url: https://true-async.github.io/en/docs/reference/thread/is-cancelled.md description: Check whether the thread was cancelled. --- # Thread::isCancelled (PHP 8.6+, True Async 1.0) ```php public Thread::isCancelled(): bool ``` Returns `true` if the thread was cancelled via `cancel()` and has actually finished executing. A cancelled thread is also considered completed: `isCancelled() === true` implies `isCompleted() === true`. ## Return Value `bool` — `true` if the thread was cancelled; `false` otherwise. ## Examples ### Example #1 Distinguishing cancellation from normal completion ```php cancel(); await($thread); if ($thread->isCancelled()) { echo "Thread was cancelled\n"; } elseif ($thread->getException() !== null) { echo "Thread finished with an error\n"; } else { echo "Thread finished successfully: " . $thread->getResult() . "\n"; } }); ``` ### Example #2 Checking cancellation while polling state ```php cancel(); while (!$thread->isCompleted()) { suspend(); } echo $thread->isCancelled() ? "Thread stopped by cancellation request\n" : "Thread finished on its own\n"; }); ``` ## See Also * [Thread::cancel()](/en/docs/reference/thread/cancel.html) — Request cancellation * [Thread::isCompleted()](/en/docs/reference/thread/is-completed.html) — Check completion * [Async\Thread](/en/docs/components/threads.html) — Thread component --- --- url: https://true-async.github.io/en/docs/reference/thread/is-completed.md description: Check whether the thread has finished executing. --- # Thread::isCompleted (PHP 8.6+, True Async 1.0) ```php public Thread::isCompleted(): bool ``` Returns `true` if the thread has finished executing — regardless of the reason: successful return of a value, throwing an exception, or cancellation. Once it transitions to `true`, the state will not change again. ## Return Value `bool` — `true` if the thread is finished; `false` if it is still running. ## Examples ### Example #1 Non-blocking check before getResult() ```php isCompleted()) { echo "Result: " . $thread->getResult() . "\n"; } else { echo "Thread has not finished yet\n"; } }); ``` ### Example #2 Waiting for multiple threads to complete ```php heavyTask(1)), spawn_thread(fn() => heavyTask(2)), spawn_thread(fn() => heavyTask(3)), ]; // Wait until all are finished do { suspend(); $pending = array_filter($threads, fn($t) => !$t->isCompleted()); } while (!empty($pending)); foreach ($threads as $i => $thread) { echo "Thread $i: " . $thread->getResult() . "\n"; } }); ``` ## See Also * [Thread::isRunning()](/en/docs/reference/thread/is-running.html) — Check if running * [Thread::isCancelled()](/en/docs/reference/thread/is-cancelled.html) — Check cancellation * [Thread::getResult()](/en/docs/reference/thread/get-result.html) — Get result * [Thread::getException()](/en/docs/reference/thread/get-exception.html) — Get exception * [Async\Thread](/en/docs/components/threads.html) — Thread component --- --- url: https://true-async.github.io/en/docs/reference/thread/is-running.md description: Check whether the thread is currently running. --- # Thread::isRunning (PHP 8.6+, True Async 1.0) ```php public Thread::isRunning(): bool ``` Returns `true` if the thread has been started and has not yet finished executing. Returns `false` if the thread has already finished — successfully, with an exception, or cancelled. ## Return Value `bool` — `true` if the thread is running; `false` if it has finished. ## Examples ### Example #1 Checking state while waiting ```php isRunning()); // bool(true) await($thread); var_dump($thread->isRunning()); // bool(false) }); ``` ### Example #2 Polling state in a loop ```php isRunning()) { echo "Thread is still running...\n"; suspend(); // yield control to the scheduler } echo "Thread finished. Result: " . $thread->getResult() . "\n"; }); ``` ## See Also * [Thread::isCompleted()](/en/docs/reference/thread/is-completed.html) — Check completion * [Thread::isCancelled()](/en/docs/reference/thread/is-cancelled.html) — Check cancellation * [Async\Thread](/en/docs/components/threads.html) — Thread component --- --- url: https://true-async.github.io/en/docs/reference/thread-channel/__construct.md description: Create a new thread-safe channel for exchanging data between OS threads. --- # ThreadChannel::\_\_construct (PHP 8.6+, True Async 1.0) ```php public ThreadChannel::__construct(int $capacity = 0) ``` Creates a new thread-safe channel for passing data between OS threads. `ThreadChannel` is the cross-thread counterpart of [`Channel`](/en/docs/components/channels.html). While `Channel` is designed for communication between coroutines within a single thread, `ThreadChannel` allows data to flow safely between **separate OS threads** — for example, between the main thread and a worker started with `spawn_thread()` or submitted to a `ThreadPool`. The channel's behavior depends on the `$capacity` parameter: * **`capacity = 0`** — unbuffered (synchronous) channel. `send()` blocks the calling thread until another thread calls `recv()`. This guarantees that the receiver is ready before the sender continues. * **`capacity > 0`** — buffered channel. `send()` does not block as long as there is room in the buffer. When the buffer is full, the calling thread blocks until space becomes available. All values transferred through the channel are **deep-copied** — the same serialization rules apply as with `spawn_thread()`. Objects that cannot be serialized (e.g. closures, resources, `stdClass` with references) will cause a `ThreadTransferException`. ## Parameters **capacity** : The capacity of the channel's internal buffer. `0` — unbuffered channel (default), `send()` blocks until a receiver is ready. Positive number — buffer size; `send()` blocks only when the buffer is full. ## Examples ### Example #1 Unbuffered channel between threads ```php recv(); // blocks until main thread sends return "Worker received: $value"; }); $channel->send('hello'); // blocks until worker calls recv() echo await($thread), "\n"; }); ``` ### Example #2 Buffered channel between threads ```php send($i); // does not block until buffer is full } $channel->close(); }); $consumer = spawn_thread(function() use ($channel) { $results = []; while (!$channel->isClosed() || !$channel->isEmpty()) { try { $results[] = $channel->recv(); } catch (\Async\ThreadChannelException) { break; } } return $results; }); await($producer); $results = await($consumer); echo implode(', ', $results), "\n"; }); ``` ## See also * [ThreadChannel::send](/en/docs/reference/thread-channel/send.html) — Send a value to the channel * [ThreadChannel::recv](/en/docs/reference/thread-channel/recv.html) — Receive a value from the channel * [ThreadChannel::capacity](/en/docs/reference/thread-channel/capacity.html) — Get the channel capacity * [ThreadChannel::close](/en/docs/reference/thread-channel/close.html) — Close the channel * [ThreadChannel component overview](/en/docs/components/thread-channels.html) --- --- url: https://true-async.github.io/en/docs/reference/thread-channel/capacity.md description: Get the buffer capacity of the thread channel. --- # ThreadChannel::capacity (PHP 8.6+, True Async 1.0) ```php public ThreadChannel::capacity(): int ``` Returns the channel capacity set at construction time. * `0` — unbuffered (synchronous) channel: `send()` blocks until the receiver is ready. * Positive number — maximum number of values the buffer can hold simultaneously. The capacity is fixed for the lifetime of the channel and does not change. ## Return values The channel buffer capacity (`int`). ## Examples ### Example #1 Checking capacity ```php capacity(); // 0 $buffered = new ThreadChannel(64); echo $buffered->capacity(); // 64 ``` ### Example #2 Adaptive logic based on channel type ```php capacity() === 0) { echo "Unbuffered: each send() blocks until recv() is called\n"; } else { $free = $ch->capacity() - $ch->count(); echo "Buffered: capacity {$ch->capacity()}, {$free} slot(s) free\n"; } } $ch = new ThreadChannel(8); $ch->send('item'); describeChannel($ch); // "Buffered: capacity 8, 7 slot(s) free" ``` ## See also * [ThreadChannel::\_\_construct](/en/docs/reference/thread-channel/__construct.html) — Create a channel * [ThreadChannel::count](/en/docs/reference/thread-channel/count.html) — Number of values currently buffered * [ThreadChannel::isFull](/en/docs/reference/thread-channel/is-full.html) — Check if the buffer is full * [ThreadChannel component overview](/en/docs/components/thread-channels.html) --- --- url: https://true-async.github.io/en/docs/reference/thread-channel/close.md description: Close the thread channel, signalling that no further values will be sent. --- # ThreadChannel::close (PHP 8.6+, True Async 1.0) ```php public ThreadChannel::close(): void ``` Closes the channel. After closing: * Calling `send()` throws a `ThreadChannelException`. * Calling `recv()` continues to return values already in the buffer until it is drained. Once the buffer is empty, `recv()` throws a `ThreadChannelException`. * Any threads currently blocked in `send()` or `recv()` are unblocked and receive a `ThreadChannelException`. Calling `close()` on an already closed channel is a no-op — it does not throw. `close()` is the standard way to signal "end of stream" to the consuming side. The producer closes the channel after sending all items; the consumer reads until it catches `ThreadChannelException`. `close()` itself is thread-safe and can be called from any thread. ## Examples ### Example #1 Producer closes after sending all items ```php send($item); } $channel->close(); // signals: no more data }); $consumer = spawn_thread(function() use ($channel) { try { while (true) { echo $channel->recv(), "\n"; } } catch (\Async\ThreadChannelException) { echo "Stream ended\n"; } }); await($producer); await($consumer); }); ``` ### Example #2 Close unblocks a waiting receiver ```php recv(); // blocks } catch (\Async\ThreadChannelException) { return "Unblocked by close()"; } }); // Close the channel from another thread — unblocks the waiter spawn_thread(function() use ($channel) { $channel->close(); }); echo await($waiter), "\n"; }); ``` ### Example #3 Calling close() twice is safe ```php close(); $channel->close(); // no-op, no exception thrown echo $channel->isClosed() ? "closed" : "open"; // "closed" ``` ## See also * [ThreadChannel::isClosed](/en/docs/reference/thread-channel/is-closed.html) — Check if the channel is closed * [ThreadChannel::recv](/en/docs/reference/thread-channel/recv.html) — Receive remaining values after close * [ThreadChannel component overview](/en/docs/components/thread-channels.html) --- --- url: https://true-async.github.io/en/docs/reference/thread-channel/count.md description: Get the number of values currently buffered in the thread channel. --- # ThreadChannel::count (PHP 8.6+, True Async 1.0) ```php public ThreadChannel::count(): int ``` Returns the current number of values held in the channel's buffer. `ThreadChannel` implements the `Countable` interface, so you can use `count($channel)` as well. For an unbuffered channel (`capacity = 0`), this always returns `0` — values are transferred directly between threads without buffering. The count is read atomically and is accurate at the moment of the call, even when other threads are concurrently sending or receiving. ## Return values The number of values currently in the buffer (`int`). ## Examples ### Example #1 Monitoring buffer fill level ```php send('a'); $channel->send('b'); $channel->send('c'); echo $channel->count(); // 3 echo count($channel); // 3 — Countable interface $channel->recv(); echo $channel->count(); // 2 ``` ### Example #2 Logging channel load from a monitor thread ```php isClosed()) { $pct = $tasks->capacity() > 0 ? round($tasks->count() / $tasks->capacity() * 100) : 0; echo "Buffer: {$tasks->count()}/{$tasks->capacity()} ({$pct}%)\n"; // In a real thread you'd use sleep() or a semaphore here } }); // ... producer and consumer threads ... $tasks->close(); await($monitor); }); ``` ## See also * [ThreadChannel::capacity](/en/docs/reference/thread-channel/capacity.html) — Channel capacity * [ThreadChannel::isEmpty](/en/docs/reference/thread-channel/is-empty.html) — Check if the buffer is empty * [ThreadChannel::isFull](/en/docs/reference/thread-channel/is-full.html) — Check if the buffer is full * [ThreadChannel component overview](/en/docs/components/thread-channels.html) --- --- url: https://true-async.github.io/en/docs/reference/thread-channel/is-closed.md description: Check whether the thread channel has been closed. --- # ThreadChannel::isClosed (PHP 8.6+, True Async 1.0) ```php public ThreadChannel::isClosed(): bool ``` Returns `true` if the channel has been closed via `close()`. A closed channel does not accept new values through `send()`, but `recv()` continues to return any values remaining in the buffer until it is drained. `isClosed()` is thread-safe and can be called from any thread without synchronization. ## Return values `true` — the channel is closed. `false` — the channel is open. ## Examples ### Example #1 Checking channel state from the main thread ```php isClosed() ? "closed" : "open"; // "open" $channel->send('data'); $channel->close(); echo $channel->isClosed() ? "closed" : "open"; // "closed" // Values buffered before close are still readable echo $channel->recv(), "\n"; // "data" }); ``` ### Example #2 Consumer loop guarded by isClosed() ```php send($i); } $channel->close(); }); $consumer = spawn_thread(function() use ($channel) { // Keep reading until closed AND buffer is empty while (!$channel->isClosed() || !$channel->isEmpty()) { try { echo $channel->recv(), "\n"; } catch (\Async\ThreadChannelException) { break; } } }); await($producer); await($consumer); }); ``` ## See also * [ThreadChannel::close](/en/docs/reference/thread-channel/close.html) — Close the channel * [ThreadChannel::isEmpty](/en/docs/reference/thread-channel/is-empty.html) — Check if the buffer is empty * [ThreadChannel component overview](/en/docs/components/thread-channels.html) --- --- url: https://true-async.github.io/en/docs/reference/thread-channel/is-empty.md description: Check whether the thread channel buffer currently holds no values. --- # ThreadChannel::isEmpty (PHP 8.6+, True Async 1.0) ```php public ThreadChannel::isEmpty(): bool ``` Returns `true` if the channel buffer contains no values. For an unbuffered channel (`capacity = 0`), this always returns `true` because data is transferred directly between threads without buffering. `isEmpty()` is thread-safe. The result reflects the state at the moment of the call; another thread may place a value into the channel immediately afterward. ## Return values `true` — the buffer is empty (no values available). `false` — the buffer contains at least one value. ## Examples ### Example #1 Checking for buffered data before receiving ```php isEmpty() ? "empty" : "has data"; // "empty" $channel->send(42); echo $channel->isEmpty() ? "empty" : "has data"; // "has data" $channel->recv(); echo $channel->isEmpty() ? "empty" : "has data"; // "empty" ``` ### Example #2 Consumer that drains a closed channel ```php send($i); } $channel->close(); }); $consumer = spawn_thread(function() use ($channel) { // Wait until there is something to read, or the channel closes while (!$channel->isClosed() || !$channel->isEmpty()) { if ($channel->isEmpty()) { // Buffer momentarily empty — yield and retry continue; } try { echo $channel->recv(), "\n"; } catch (\Async\ThreadChannelException) { break; } } }); await($producer); await($consumer); }); ``` ## See also * [ThreadChannel::isFull](/en/docs/reference/thread-channel/is-full.html) — Check if the buffer is full * [ThreadChannel::count](/en/docs/reference/thread-channel/count.html) — Number of values in the buffer * [ThreadChannel::recv](/en/docs/reference/thread-channel/recv.html) — Receive a value * [ThreadChannel component overview](/en/docs/components/thread-channels.html) --- --- url: https://true-async.github.io/en/docs/reference/thread-channel/is-full.md description: Check whether the thread channel buffer is filled to its maximum capacity. --- # ThreadChannel::isFull (PHP 8.6+, True Async 1.0) ```php public ThreadChannel::isFull(): bool ``` Returns `true` if the channel buffer has reached its maximum capacity. For an unbuffered channel (`capacity = 0`), this always returns `true` because there is no buffer — every `send()` must wait for a matching `recv()`. `isFull()` is thread-safe. The result reflects the state at the moment of the call; another thread may drain a slot immediately afterward. ## Return values `true` — the buffer is at capacity (or it is an unbuffered channel). `false` — the buffer has at least one free slot. ## Examples ### Example #1 Checking buffer fullness before sending ```php isFull() ? "full" : "has space"; // "has space" $channel->send('x'); $channel->send('y'); $channel->send('z'); echo $channel->isFull() ? "full" : "has space"; // "full" ``` ### Example #2 Back-pressure monitoring in a producer thread ```php isFull()) { // Buffer is currently full — send() will block; // log back-pressure for observability error_log("ThreadChannel back-pressure: buffer full"); } $channel->send($item); // blocks until space is available } $channel->close(); }); $consumer = spawn_thread(function() use ($channel) { try { while (true) { // Simulate slow consumer $val = $channel->recv(); // process $val ... } } catch (\Async\ThreadChannelException) { echo "Done\n"; } }); await($producer); await($consumer); }); ``` ## See also * [ThreadChannel::isEmpty](/en/docs/reference/thread-channel/is-empty.html) — Check if the buffer is empty * [ThreadChannel::capacity](/en/docs/reference/thread-channel/capacity.html) — Channel capacity * [ThreadChannel::count](/en/docs/reference/thread-channel/count.html) — Number of values in the buffer * [ThreadChannel::send](/en/docs/reference/thread-channel/send.html) — Send a value (blocks when full) * [ThreadChannel component overview](/en/docs/components/thread-channels.html) --- --- url: https://true-async.github.io/en/docs/reference/thread-channel/recv.md description: >- Receive the next value from the thread channel, blocking the calling thread if no value is available. --- # ThreadChannel::recv (PHP 8.6+, True Async 1.0) ```php public ThreadChannel::recv(?Completable $cancellationToken = null): mixed ``` Receives the next value from the channel. This is a **blocking** operation — the calling thread is blocked if no values are currently available in the channel. * For a **buffered channel**, `recv()` returns immediately if the buffer contains at least one value. If the buffer is empty, the thread blocks until a sender places a value. * For an **unbuffered channel** (`capacity = 0`), `recv()` blocks until another thread calls `send()`. If the channel is closed and the buffer still contains values, those values are returned normally. Once the buffer is drained and the channel is closed, `recv()` throws `ThreadChannelException`. The received value is a **deep copy** of the original — modifications to the returned value do not affect the sender's copy. ## Return values The next value from the channel (`mixed`). ## Errors * Throws `Async\ThreadChannelException` if the channel is closed and the buffer is empty. ## Examples ### Example #1 Receiving values produced by a worker thread ```php send($i * 10); } $channel->close(); }); // Receive all values — blocks when buffer is empty try { while (true) { echo $channel->recv(), "\n"; } } catch (\Async\ThreadChannelException) { echo "All values received\n"; } await($worker); }); ``` ### Example #2 Consumer thread draining a shared channel ```php send($letter); } $channel->close(); }); // Consumer: drains the channel from another thread $consumer = spawn_thread(function() use ($channel) { $collected = []; try { while (true) { $collected[] = $channel->recv(); } } catch (\Async\ThreadChannelException) { // buffer drained and channel closed } return $collected; }); await($producer); $result = await($consumer); echo implode(', ', $result), "\n"; // "a, b, c, d, e" }); ``` ### Example #3 Receiving from an unbuffered channel ```php send(['task' => 'compress', 'file' => '/tmp/data.bin']); }); // Main coroutine (thread) calls recv() — unblocks the sender $task = $channel->recv(); echo "Got task: {$task['task']} on {$task['file']}\n"; await($sender); }); ``` ## See also * [ThreadChannel::send](/en/docs/reference/thread-channel/send.html) — Send a value to the channel * [ThreadChannel::isEmpty](/en/docs/reference/thread-channel/is-empty.html) — Check if the buffer is empty * [ThreadChannel::close](/en/docs/reference/thread-channel/close.html) — Close the channel * [ThreadChannel component overview](/en/docs/components/thread-channels.html) --- --- url: https://true-async.github.io/en/docs/reference/thread-channel/send.md description: >- Send a value to the thread channel, blocking the calling thread if the channel cannot accept it immediately. --- # ThreadChannel::send (PHP 8.6+, True Async 1.0) ```php public ThreadChannel::send(mixed $value, ?Completable $cancellationToken = null): void ``` Sends a value to the channel. This is a **blocking** operation — the calling thread is blocked if the channel cannot accept the value immediately. * For an **unbuffered channel** (`capacity = 0`), the thread blocks until another thread calls `recv()`. * For a **buffered channel**, the thread blocks only when the buffer is full, and unblocks as soon as a receiver drains a slot. Unlike `Channel::send()` (which suspends a coroutine), `ThreadChannel::send()` blocks the entire OS thread. Design your architecture accordingly — for example, keep the sending thread free to block, or use a buffered channel to reduce contention. The value is **deep-copied** before being placed into the channel. Closures, resources, and non-serializable objects will cause a `ThreadTransferException`. ## Parameters **value** : The value to send. Can be of any serializable type (scalar, array, or a serializable object). ## Errors * Throws `Async\ThreadChannelException` if the channel is already closed. * Throws `Async\ThreadTransferException` if the value cannot be serialized for cross-thread transfer. ## Examples ### Example #1 Sending results from a worker thread ```php send($result); } $channel->close(); }); await($worker); while (!$channel->isClosed() || !$channel->isEmpty()) { try { echo $channel->recv(), "\n"; } catch (\Async\ThreadChannelException) { break; } } }); ``` ### Example #2 Unbuffered handshake between threads ```php recv(); // blocks until request arrives $responses->send(strtoupper($req)); // blocks until response is accepted }); $requests->send('hello'); // blocks until server calls recv() $reply = $responses->recv(); // blocks until server calls send() await($server); echo $reply, "\n"; // "HELLO" }); ``` ### Example #3 Handling a closed channel ```php close(); $thread = spawn_thread(function() use ($channel) { try { $channel->send('too late'); } catch (\Async\ThreadChannelException $e) { return "Send failed: " . $e->getMessage(); } }); echo await($thread), "\n"; }); ``` ## See also * [ThreadChannel::recv](/en/docs/reference/thread-channel/recv.html) — Receive a value from the channel * [ThreadChannel::isFull](/en/docs/reference/thread-channel/is-full.html) — Check if the buffer is full * [ThreadChannel::close](/en/docs/reference/thread-channel/close.html) — Close the channel * [ThreadChannel component overview](/en/docs/components/thread-channels.html) --- --- url: https://true-async.github.io/en/docs/reference/thread-pool/__construct.md description: Create a new ThreadPool with a fixed number of worker threads. --- # ThreadPool::\_\_construct() (PHP 8.6+, True Async 1.0) ```php public ThreadPool::__construct( int $workers = 0, int $queueSize = 0, ?\Closure $bootloader = null, bool $coroutine = false, int $concurrency = 0, ) ``` Creates a new thread pool and starts all worker threads immediately. Workers remain alive for the lifetime of the pool, eliminating per-task thread-startup overhead. ## Parameters | Parameter | Type | Description | |----------------|---------------|------------------------------------------------------------------------------------------------------------------------------------------| | `$workers` | `int` | Number of worker threads. `0` (default) — autodetect via [`Async\available_parallelism()`](/en/docs/reference/available-parallelism.html). | | `$queueSize` | `int` | Maximum length of the pending task queue. `0` (default) means `workers × 4`. When the queue is full, `submit()` suspends the calling coroutine until a slot becomes available. | | `$bootloader` | `?\Closure` | Worker startup initialisation. The closure is deep-copied once and runs in every worker **before** the main task loop. Useful for autoload, connection-pool warmup, opcache pre-compile. If the bootloader throws, the entire pool is considered failed. | | `$coroutine` | `bool` | When `true`, every task runs **as a coroutine** in its own child scope nested inside the worker's shared pool scope. Inside the task you can `await`, use channels, do I/O, and `spawn` — all without blocking the OS thread. | | `$concurrency` | `int` | Concurrency limit of coroutines inside a single worker. Used only when `coroutine: true`. `0` (default) — unlimited. | ## Exceptions Throws `\ValueError` if `$workers < 0` or `$queueSize < 0`. ## Examples ### Example #1 Basic pool creation ```php submit(fn() => 'hello from worker'); echo await($future), "\n"; // hello from worker $pool->close(); }); ``` ### Example #2 Explicit queue size ```php close(); }); ``` ### Example #3 Bootloader — worker startup initialisation ```php close(); }); ``` ### Example #4 Coroutine mode — `await` inside a task ```php submit(function () { // a regular blocking call correctly parks the coroutine // rather than blocking the worker's OS thread $pdo = new PDO('mysql:host=localhost;dbname=app', 'user', 'pass'); $rows = $pdo->query('SELECT * FROM users LIMIT 10')->fetchAll(); return $rows; }); print_r(await($future)); $pool->close(); }); ``` ### Example #5 Autodetect worker count from available CPUs ```php submit(function() use ($i) { $t = microtime(true); while (microtime(true) - $t < 0.2) {} return $i; }); } // Cancel immediately — tasks in the queue are rejected $pool->cancel(); $done = 0; $cancelled = 0; foreach ($futures as $f) { try { await($f); $done++; } catch (ThreadPoolException $e) { $cancelled++; } } echo "done: $done\n"; // 2 (already running when cancel() was called) echo "cancelled: $cancelled\n"; // 6 (were still in the queue) }); ``` ## See Also * [ThreadPool::close()](/en/docs/reference/thread-pool/close.html) — graceful shutdown * [ThreadPool::isClosed()](/en/docs/reference/thread-pool/is-closed.html) — check if pool is closed * [Async\ThreadPool](/en/docs/components/thread-pool.html) — component overview and close() vs cancel() comparison --- --- url: https://true-async.github.io/en/docs/reference/thread-pool/close.md description: >- Gracefully shut down the thread pool, waiting for all queued and running tasks to finish. --- # ThreadPool::close() (PHP 8.6+, True Async 1.0) ```php public ThreadPool::close(): void ``` Initiates a graceful shutdown of the pool. After `close()` is called: * Any subsequent `submit()` call immediately throws `Async\ThreadPoolException`. * Tasks already in the queue continue and complete normally. * Tasks currently executing in worker threads complete normally. * The method blocks the calling coroutine until all in-progress tasks have finished and all workers have stopped. For an immediate, hard shutdown that discards queued tasks, use [`cancel()`](/en/docs/reference/thread-pool/cancel.html) instead. ## Return Value `void` ## Examples ### Example #1 Graceful shutdown after all tasks are submitted ```php submit(function() { return 'finished'; }); $pool->close(); // waits for the task above to complete echo await($future), "\n"; // finished $pool->close(); }); ``` ### Example #2 Submit after close throws an exception ```php close(); try { $pool->submit(fn() => 'too late'); } catch (ThreadPoolException $e) { echo "Error: ", $e->getMessage(), "\n"; // Error: Cannot submit task: thread pool is closed } }); ``` ## See Also * [ThreadPool::cancel()](/en/docs/reference/thread-pool/cancel.html) — hard/forced shutdown * [ThreadPool::isClosed()](/en/docs/reference/thread-pool/is-closed.html) — check if pool is closed * [Async\ThreadPool](/en/docs/components/thread-pool.html) — component overview --- --- url: >- https://true-async.github.io/en/docs/reference/thread-pool/get-completed-count.md description: Get the total number of tasks completed by the thread pool since creation. --- # ThreadPool::getCompletedCount() (PHP 8.6+, True Async 1.0) ```php public ThreadPool::getCompletedCount(): int ``` Returns the total number of tasks that have been executed to completion (successfully or with an exception) by any worker in this pool since the pool was created. This counter is monotonically increasing and never resets. It is backed by an atomic variable and is accurate at any point in time. A task is counted as completed when the worker finishes executing it — regardless of whether it returned a value or threw an exception. ## Return Value `int` — total completed task count since pool creation. ## Examples ### Example #1 Tracking throughput ```php submit(function() { $t = microtime(true); while (microtime(true) - $t < 0.1) {} return 'done'; }); } delay(10); echo "completed so far: ", $pool->getCompletedCount(), "\n"; // 0 or more foreach ($futures as $f) { await($f); } echo "completed total: ", $pool->getCompletedCount(), "\n"; // 6 $pool->close(); }); ``` ## See Also * [ThreadPool::getPendingCount()](/en/docs/reference/thread-pool/get-pending-count.html) — tasks waiting in the queue * [ThreadPool::getRunningCount()](/en/docs/reference/thread-pool/get-running-count.html) — tasks currently executing * [ThreadPool::getWorkerCount()](/en/docs/reference/thread-pool/get-worker-count.html) — number of workers * [Async\ThreadPool](/en/docs/components/thread-pool.html) — component overview --- --- url: >- https://true-async.github.io/en/docs/reference/thread-pool/get-pending-count.md description: Get the number of tasks waiting in the thread pool queue. --- # ThreadPool::getPendingCount() (PHP 8.6+, True Async 1.0) ```php public ThreadPool::getPendingCount(): int ``` Returns the number of tasks that have been submitted but not yet picked up by a worker thread. This counter is backed by an atomic variable and is accurate at any point in time, even while workers are running in parallel. ## Return Value `int` — number of tasks currently waiting in the queue. ## Examples ### Example #1 Observing the queue drain ```php submit(function() { $t = microtime(true); while (microtime(true) - $t < 0.1) {} return 'done'; }); } delay(10); // let workers start echo "pending: ", $pool->getPendingCount(), "\n"; // pending: 4 foreach ($futures as $f) { await($f); } echo "pending: ", $pool->getPendingCount(), "\n"; // pending: 0 $pool->close(); }); ``` ## See Also * [ThreadPool::getRunningCount()](/en/docs/reference/thread-pool/get-running-count.html) — tasks currently executing * [ThreadPool::getCompletedCount()](/en/docs/reference/thread-pool/get-completed-count.html) — total completed tasks * [ThreadPool::getWorkerCount()](/en/docs/reference/thread-pool/get-worker-count.html) — number of workers * [Async\ThreadPool](/en/docs/components/thread-pool.html) — component overview --- --- url: >- https://true-async.github.io/en/docs/reference/thread-pool/get-running-count.md description: Get the number of tasks currently executing in worker threads. --- # ThreadPool::getRunningCount() (PHP 8.6+, True Async 1.0) ```php public ThreadPool::getRunningCount(): int ``` Returns the number of tasks that are currently being executed by a worker thread (i.e. picked up from the queue and not yet finished). The maximum value is bounded by the number of workers. This counter is backed by an atomic variable and is accurate at any point in time. ## Return Value `int` — number of tasks currently executing across all worker threads. ## Examples ### Example #1 Watching running count while tasks execute ```php submit(function() { $t = microtime(true); while (microtime(true) - $t < 0.1) {} return 'done'; }); } delay(10); // give workers time to start echo "workers: ", $pool->getWorkerCount(), "\n"; // workers: 3 echo "running: ", $pool->getRunningCount(), "\n"; // running: 3 foreach ($futures as $f) { await($f); } echo "running: ", $pool->getRunningCount(), "\n"; // running: 0 $pool->close(); }); ``` ## See Also * [ThreadPool::getPendingCount()](/en/docs/reference/thread-pool/get-pending-count.html) — tasks waiting in the queue * [ThreadPool::getCompletedCount()](/en/docs/reference/thread-pool/get-completed-count.html) — total completed tasks * [ThreadPool::getWorkerCount()](/en/docs/reference/thread-pool/get-worker-count.html) — number of workers * [Async\ThreadPool](/en/docs/components/thread-pool.html) — component overview --- --- url: https://true-async.github.io/en/docs/reference/thread-pool/get-worker-count.md description: Get the number of worker threads in the thread pool. --- # ThreadPool::getWorkerCount() (PHP 8.6+, True Async 1.0) ```php public ThreadPool::getWorkerCount(): int ``` Returns the number of worker threads running in the pool right now. It starts at the `$workers` argument passed to [`new ThreadPool()`](/en/docs/reference/thread-pool/__construct.html) and drops as workers exit. ## Return Value `int` — number of worker threads running now. A worker leaves the count when its thread returns, so a closed pool reports 0 once its workers have drained, and a pool that lost a thread reports one less than it was constructed with. ## Examples ### Example #1 Confirming worker count ```php getWorkerCount(), "\n"; // 4 $pool->close(); }); ``` ### Example #2 Sizing the pool to available CPU cores ```php getWorkerCount(), " workers\n"; $futures = []; for ($i = 0; $i < $cores * 2; $i++) { $futures[] = $pool->submit(fn() => 'done'); } foreach ($futures as $f) { await($f); } $pool->close(); }); ``` ## See Also * [ThreadPool::getPendingCount()](/en/docs/reference/thread-pool/get-pending-count.html) — tasks waiting in the queue * [ThreadPool::getRunningCount()](/en/docs/reference/thread-pool/get-running-count.html) — tasks currently executing * [ThreadPool::getCompletedCount()](/en/docs/reference/thread-pool/get-completed-count.html) — total completed tasks * [Async\ThreadPool](/en/docs/components/thread-pool.html) — component overview --- --- url: https://true-async.github.io/en/docs/reference/thread-pool/is-closed.md description: Check whether the thread pool has been shut down. --- # ThreadPool::isClosed() (PHP 8.6+, True Async 1.0) ```php public ThreadPool::isClosed(): bool ``` Returns `true` if the pool has been shut down via [`close()`](/en/docs/reference/thread-pool/close.html) or [`cancel()`](/en/docs/reference/thread-pool/cancel.html). Returns `false` while the pool is still accepting tasks. ## Return Value `bool` — `true` if the pool is closed; `false` if it is still active. ## Examples ### Example #1 Checking state before submitting ```php submit(fn() => 'done'); var_dump($pool->isClosed()); // bool(false) $pool->close(); var_dump($pool->isClosed()); // bool(true) echo await($future), "\n"; // done }); ``` ### Example #2 Guarding submit in shared contexts ```php isClosed()) { return null; } return await($pool->submit($task)); } spawn(function() { $pool = new ThreadPool(workers: 2); echo trySubmit($pool, fn() => 'hello'), "\n"; // hello $pool->close(); var_dump(trySubmit($pool, fn() => 'missed')); // NULL }); ``` ## See Also * [ThreadPool::close()](/en/docs/reference/thread-pool/close.html) — graceful shutdown * [ThreadPool::cancel()](/en/docs/reference/thread-pool/cancel.html) — hard shutdown * [Async\ThreadPool](/en/docs/components/thread-pool.html) — component overview --- --- url: https://true-async.github.io/en/docs/reference/thread-pool/map.md description: Apply a callable to each array item in parallel using the thread pool. --- # ThreadPool::map() (PHP 8.6+, True Async 1.0) ```php public ThreadPool::map(array $items, callable $task): array ``` Submits `$task($item)` for every element of `$items` to the pool's workers concurrently, then blocks the calling coroutine until all tasks finish. Returns results in the same order as the input array, regardless of the order in which workers complete. If any task throws an exception, `map()` rethrows it in the calling coroutine. Other in-flight tasks are not cancelled. ## Parameters | Parameter | Type | Description | |-----------|------------|----------------------------------------------------------------------------------------------------------| | `$items` | `array` | The input items. Each element is passed as the first argument to `$task`. | | `$task` | `callable` | The callable to apply to each item. Executed in a worker thread; the same data-transfer rules as `submit()` apply. | ## Return Value `array` — results of `$task` for each input element, in the same order as `$items`. ## Exceptions * `Async\ThreadPoolException` — if the pool has been closed. * Re-throws the first exception thrown by any task. ## Examples ### Example #1 Count lines in multiple files in parallel ```php map($files, function(string $path) { if (!file_exists($path)) { return 0; } $count = 0; $fh = fopen($path, 'r'); while (!feof($fh)) { fgets($fh); $count++; } fclose($fh); return $count; }); foreach ($files as $i => $path) { echo "$path: {$lineCounts[$i]} lines\n"; } $pool->close(); }); ``` ### Example #2 Parallel number crunching ```php map($inputs, function(int $n) { $sum = 0.0; for ($i = 0; $i < $n; $i++) { $sum += sqrt($i); } return $sum; }); foreach ($inputs as $i => $n) { echo "$n iterations → {$results[$i]}\n"; } $pool->close(); }); ``` ## See Also * [ThreadPool::submit()](/en/docs/reference/thread-pool/submit.html) — submit a single task and get a Future * [Async\ThreadPool](/en/docs/components/thread-pool.html) — component overview --- --- url: https://true-async.github.io/en/docs/reference/thread-pool/submit.md description: Submit a task to the thread pool and receive a Future for its result. --- # ThreadPool::submit() (PHP 8.6+, True Async 1.0) ```php public ThreadPool::submit(callable $task, mixed ...$args): Async\Future ``` Adds a task to the pool's internal queue. A free worker picks it up, executes it, and resolves the returned `Future` with the return value. If the queue is full, the calling coroutine is suspended until a slot opens. ## Parameters | Parameter | Type | Description | |-----------|------------|---------------------------------------------------------------------------------------------------------------------| | `$task` | `callable` | The callable to execute in a worker thread. Deep-copied into the worker — closures capturing objects or resources will throw `Async\ThreadTransferException`. | | `...$args`| `mixed` | Additional arguments passed to `$task`. Also deep-copied. | ## Return Value `Async\Future` — resolves with the return value of `$task`, or rejects with any exception thrown by `$task`. ## Exceptions * `Async\ThreadPoolException` — thrown immediately if the pool has been closed via `close()` or `cancel()`. * `Async\ThreadTransferException` — thrown if `$task` or any argument cannot be serialized for transfer (e.g. `stdClass`, PHP references, resources). ## Examples ### Example #1 Basic submit and await ```php submit(function(int $n) { $sum = 0; for ($i = 0; $i < $n; $i++) { $sum += $i; } return $sum; }, 1_000_000); echo await($future), "\n"; // 499999500000 $pool->close(); }); ``` ### Example #2 Handling exceptions from a task ```php submit(function() { throw new \RuntimeException('something went wrong in the worker'); }); try { await($future); } catch (\RuntimeException $e) { echo "Caught: ", $e->getMessage(), "\n"; // Caught: something went wrong in the worker } $pool->close(); }); ``` ### Example #3 Submitting multiple tasks in parallel ```php submit(function() use ($i) { return $i * $i; }); } foreach ($futures as $f) { echo await($f), "\n"; } $pool->close(); }); ``` ## See Also * [ThreadPool::map()](/en/docs/reference/thread-pool/map.html) — parallel map over an array * [ThreadPool::close()](/en/docs/reference/thread-pool/close.html) — graceful shutdown * [Async\ThreadPool](/en/docs/components/thread-pool.html) — component overview and data transfer rules --- --- url: https://true-async.github.io/en/tutors/14-threads.md description: >- spawn_thread and ThreadPool: true parallelism for computation, isolation, and data copying. --- # Threads All our tools so far have lived inside a single operating system thread. Coroutines created the impression that things were happening at once, but at any given moment exactly one of them was actually executing. For the import job and the requests to `GeoDirectory`, that was more than enough: the tasks mostly waited, and waiting divides beautifully. Now here's a different kind of task. Once a day, `ProfileService` builds an annual report: a full minute of pure computation, without a single network or database call. Let's run it in a coroutine, alongside our familiar ticker: ```php spawn(function () { while (true) { echo "tick\n"; delay(1000); } }); spawn(function () { $report = buildYearlyReport($rows); // a minute of pure computation echo "report ready\n"; }); ``` The ticker falls silent for a whole minute. Remember chapter one: coroutines switch at await points, at `sleep`, `delay`, I/O. But `buildYearlyReport` doesn't have a single await point, just loops and arithmetic. The scheduler never gets control back, and the entire process, ticker, workers, request handling, all of it, just sits there watching one coroutine crunch numbers. Coroutines give you concurrency, not parallelism. When a task is bound by the CPU rather than by waiting, you need a second processor. Or more precisely, a second operating system thread. ## spawn\_thread ```php use function Async\spawn_thread; use function Async\await; $thread = spawn_thread(fn() => buildYearlyReport($rows)); $report = await($thread); ``` `spawn_thread` launches the closure in a separate operating system thread, on a different CPU core. The report is now computed truly in parallel: the ticker keeps ticking, the workers keep working, and the scheduler has no idea heavy work is happening right next door. From the outside, a thread looks almost like a coroutine: you can pass it to the familiar `await`, which suspends only the waiting coroutine. An exception from the thread reaches `await` too, wrapped in `Async\RemoteException`. ## Nothing in common Almost like a coroutine, but with one fundamental difference. In the channels chapter we said that coroutines live in shared memory, you can just hand over an object through `use`, and data races don't happen within a single thread. Well, that trick doesn't work with threads. Each thread has its own PHP environment: its own variables, its own classes, its own static properties. There's no shared memory at all. That's why everything passed into a thread is copied in full: ```php $rows = loadRows(); $thread = spawn_thread(function () use ($rows) { // this is a COPY of $rows: changes here aren't visible outside return buildYearlyReport($rows); }); ``` This rule comes with restrictions too: you cannot pass a PHP reference (`&$var`), a resource like an open file, or an object with dynamic properties into a thread. Attempting to do so throws `ThreadTransferException` immediately, at the start, instead of causing mysterious behavior later. The rules are strict, but they're honest: with no shared state, there are no races, locks, mutexes, or any of the other nightmares of classic multithreading. TrueAsync threads communicate the same way coroutines do: by passing values, not by sharing memory. By the way, one old acquaintance knows how to cross the thread boundary meaningfully. `FutureState` from chapter six can be passed into a thread while its `Future` stays behind: ```php $state = new FutureState(); $future = new Future($state); spawn_thread(function () use ($state) { $state->complete(buildYearlyReport(loadRows())); }); $report = await($future); ``` The producer is in one thread, the consumer in another, and the contract is exactly the same. That's why `Future` was split into two separate objects in the first place: the boundary between them turned out to be sturdy enough to run a thread boundary right along it. ## ThreadPool A thread is an expensive entity: its own PHP environment has to be created, initialized, and warmed up. One task a minute, sure, no problem, but spinning up a thread for every tiny task is just as wasteful as opening a database connection on every request. You already know what to do with expensive resources. Right, pool them: ```php use Async\ThreadPool; $pool = new ThreadPool(workers: 8); $futures = []; foreach ($uploads as $path) { $futures[] = $pool->submit(makeThumbnail(...), $path); } foreach ($futures as $future) { echo await($future), "\n"; } $pool->close(); ``` Eight worker threads are created once and live as long as the pool does. `submit` puts a task in the queue, a free worker picks it up and returns the result. And look at what `submit` returns: a `Future`, a promise of a result that someone else will produce. In chapter six that "someone else" was a coroutine; now it's an entire separate thread, and the contract hasn't changed one bit. The pool's queue has a limit, and once it fills up, `submit` suspends the calling coroutine. Recognize that? It's the back-pressure from the channels chapter: the task producer automatically paces itself to match the workers' speed. Keep the number of workers close to the number of CPU cores: unlike waiting, computation needs actual physical cores, and twenty threads on eight cores will just elbow each other out of the way. By default, without the `workers` parameter, the pool figures out the available parallelism on its own, even accounting for container quotas. And for the most common case, "process a list in parallel," there's a one-line form you already know from `iterate`: ```php $thumbs = $pool->map($uploads, makeThumbnail(...)); ``` `map` distributes the elements among the workers, waits for all of them, and returns the results in their original order. So it turns out concurrency has two dimensions. Coroutines compress waiting: thousands of tasks share a single thread and don't get in each other's way while they wait. Threads add true parallelism: computation spreads out across CPU cores. These tools don't compete with each other, they complement each other: where code waits, reach for a coroutine; where it computes, reach for a thread; and once you have a lot of either, a pool comes to the rescue. Finally, take a look at what our process has turned into: hundreds of coroutines, all mixed together, serving different users, tasks hopping between workers and threads. And in that crowd, a simple question turns out to be surprisingly hard: where do you keep "the current one"? The current user, the current language, the request ID? There's one global variable for everyone, and there are thousands of coroutines. That's what the final chapter is about. --- --- url: https://true-async.github.io/en/docs/reference/timeout.md description: timeout() — create a timeout object to limit waiting time. --- # timeout (PHP 8.6+, True Async 1.0) `timeout()` — Creates an `Async\Timeout` object that triggers after the specified number of milliseconds. ## Description ```php timeout(int $ms): Async\Awaitable ``` Creates a timer that throws `Async\TimeoutException` after `$ms` milliseconds. Used as a wait time limiter in `await()` and other functions. ## Parameters **`ms`** Time in milliseconds. Must be greater than 0. ## Return Values Returns an `Async\Timeout` object implementing `Async\Completable`. ## Errors/Exceptions * `ValueError` — if `$ms` <= 0. ## Examples ### Example #1 Timeout on await() ```php ``` ### Example #2 Timeout on a task group ```php ``` ### Example #3 Cancelling a timeout ```php cancel(); ?> ``` ## See Also * [delay()](/en/docs/reference/delay.html) — suspending a coroutine * [await()](/en/docs/reference/await.html) — waiting with cancellation --- --- url: https://true-async.github.io/en/architecture/frankenphp.md description: >- How TrueAsync turns FrankenPHP into a fully asynchronous server -- a coroutine per request, zero-copy responses, dual notification path. --- # TrueAsync + FrankenPHP: Many Requests, One Thread In this article, we examine the experience of integrating `FrankenPHP` with `TrueAsync`. `FrankenPHP` is a server based on `Caddy` that runs `PHP` code inside a `Go` process. We added `TrueAsync` support to `FrankenPHP`, allowing each `PHP` thread to handle multiple requests simultaneously, using `TrueAsync` coroutines for orchestration. ## How FrankenPHP Works `FrankenPHP` is a process that bundles the `Go` world (`Caddy`) and `PHP` together. `Go` owns the process, while `PHP` acts as a "plugin" that `Go` interacts with through `SAPI`. To make this work, the `PHP` virtual machine runs in a separate thread. `Go` creates these threads and calls `SAPI` functions to execute `PHP` code. For each request, `Caddy` creates a separate goroutine that handles the HTTP request. The goroutine selects a free `PHP` thread from the pool and sends the request data via a channel, then enters a waiting state. When `PHP` finishes forming the response, the goroutine receives it via the channel and passes it back to `Caddy`. We changed this approach so that goroutines now send multiple requests to the same `PHP` thread, and the `PHP` thread learns to handle such requests asynchronously. ### General Architecture ![General FrankenPHP + TrueAsync Architecture](/diagrams/en/architecture-frankenphp/architecture.svg) The diagram shows three layers. Let's examine each one. ### Integrating Go into the TrueAsync Scheduler For the application to work, the PHP `Reactor` and `Scheduler` must be integrated with `Caddy`. Therefore, we need some cross-thread communication mechanism that is compatible with both the `Go` and `PHP` worlds. `Go` channels are excellent for data transfer between threads and are accessible from `C-Go`. But they are not sufficient, since the `EventLoop` cycle may go to sleep. There is an old well-known approach that can be found in almost any web server: a combination of a transfer channel and an `fdevent` (on macOS/Windows a `pipe` is used). If the channel is not empty, `PHP` will be reading from it, so we just add another value. If the channel is empty, the `PHP` thread is sleeping and needs to be woken up. That's what `Notify()` is for. ```go func NewAsyncNotifier() (*AsyncNotifier, error) { if runtime.GOOS == "linux" { fd, err := createEventFD() // eventfd -- the fastest option // ... } // Fallback: pipe for macOS/BSD syscall.Pipe(fds[:]) } ``` On the `PHP` side, the `eventfd` descriptor is registered in the `Reactor`: ```c request_event = ZEND_ASYNC_NEW_POLL_EVENT_EX( (zend_file_descriptor_t) notifier_fd, 0, ASYNC_READABLE, sizeof(uintptr_t) ); request_event->base.start(&request_event->base); ``` The `Reactor` (based on `libuv`) starts monitoring the descriptor. As soon as `Go` writes to `eventfd`, the `Reactor` wakes up and calls the request handling callback. Now, when a goroutine packages request data into a `contextHolder` structure and passes it to the `Dispatcher` for delivery to the `PHP` thread. The `Dispatcher` cycles through `PHP` threads in round-robin fashion and attempts to send the request context to the buffered `Go` channel (`requestChan`) bound to a specific thread. If the buffer is full, the `Dispatcher` tries the next thread. If all are busy -- the client receives `HTTP 503`. ```go start := w.rrIndex.Add(1) % uint32(len(w.threads)) for i := 0; i < len(w.threads); i++ { idx := (start + uint32(i)) % uint32(len(w.threads)) select { case thread.requestChan <- ch: if len(thread.requestChan) == 1 { thread.asyncNotifier.Notify() } return nil default: continue } } return ErrAllBuffersFull // HTTP 503 ``` ### Integration with the Scheduler When `FrankenPHP` initializes and creates `PHP` threads, it integrates with the `Reactor`/`Scheduler` using the `True Async ABI` (`zend_async_API.h`). The `frankenphp_enter_async_mode()` function is responsible for this process and is called once when the `PHP` script registers a callback via `HttpServer::onRequest()`: ```c void frankenphp_enter_async_mode(void) { // 1. Get the notifier FD from Go notifier_fd = go_async_worker_get_notification_fd(thread_index); // 2. Register FD in the Reactor (slow path) frankenphp_register_request_notifier(notifier_fd, thread_index); // 3. Launch the Scheduler ZEND_ASYNC_SCHEDULER_LAUNCH(); // 4. Replace the heartbeat handler (fast path) old_heartbeat_handler = zend_async_set_heartbeat_handler( frankenphp_scheudler_tick_handler ); // 5. Suspend the main coroutine frankenphp_suspend_main_coroutine(); // --- we only reach here on shutdown --- // 6. Restore the heartbeat handler zend_async_set_heartbeat_handler(old_heartbeat_handler); // 7. Release resources close_request_event(); } ``` We use a `heartbeat handler`, a special callback from the `Scheduler`, to add our own handler for each `Scheduler` tick. This handler allows `FrankenPHP` to create new coroutines for request processing. ![Dual Notification System](/diagrams/en/architecture-frankenphp/notification.svg) Now the `Scheduler` calls the `heartbeat handler` on each tick. This handler checks the `Go` channel via `CGo`: ```c void frankenphp_scheudler_tick_handler(void) { uint64_t request_id; while ((request_id = go_async_worker_check_requests(thread_index)) != 0) { if (request_id == UINT64_MAX) { ZEND_ASYNC_SHUTDOWN(); return; } frankenphp_handle_request_async(request_id); } if (old_heartbeat_handler) old_heartbeat_handler(); } ``` No system calls, no `epoll_wait`, a direct call to a `Go` function via `CGo`. Instant return if the channel is empty. The cheapest possible operation, which is a mandatory requirement for the `heartbeat handler`. If all coroutines are asleep, the `Scheduler` passes control to the `Reactor`, and the `heartbeat` stops ticking. Then the `AsyncNotifier` kicks in: the `Reactor` waits on `epoll`/`kqueue` and wakes up when `Go` writes to the descriptor. ```c static void frankenphp_async_check_requests_callback( zend_async_event_t *event, ...) { go_async_worker_clear_notification(thread_idx); while ((request_id = go_async_worker_check_requests(thread_idx)) != 0) { frankenphp_handle_request_async(request_id); } } ``` The two systems complement each other: `heartbeat` provides minimal latency under load, while the `poll event` ensures zero `CPU` consumption during idle periods. ### Creating a Request Coroutine The `frankenphp_request_coroutine_entry()` function is responsible for creating the request handling coroutine: ![Request Lifecycle](/diagrams/en/architecture-frankenphp/request-lifecycle.svg) ```c void frankenphp_handle_request_async(uint64_t request_id) { zend_async_scope_t *request_scope = ZEND_ASYNC_NEW_SCOPE(ZEND_ASYNC_CURRENT_SCOPE); zend_coroutine_t *coroutine = ZEND_ASYNC_NEW_COROUTINE(request_scope); coroutine->internal_entry = frankenphp_request_coroutine_entry; coroutine->extended_data = (void *)(uintptr_t)request_id; ZEND_ASYNC_ENQUEUE_COROUTINE(coroutine); } ``` A **separate `Scope`** is created for each request. This is an isolated context that allows controlling the lifecycle of the coroutine and its resources. When a `Scope` completes, all coroutines within it are cancelled. ### Interaction with PHP Code To create coroutines, `FrankenPHP` needs to know the handler function. The handler function must be defined by the PHP programmer. This requires initialization code on the `PHP` side. The `HttpServer::onRequest()` function serves as this initializer, registering a `PHP` callback for handling `HTTP` requests. From the `PHP` side, everything looks simple: ```php use FrankenPHP\HttpServer; use FrankenPHP\Request; use FrankenPHP\Response; HttpServer::onRequest(function (Request $request, Response $response) { $uri = $request->getUri(); $body = $request->getBody(); $response->setStatus(200); $response->setHeader('Content-Type', 'application/json'); $response->write(json_encode(['uri' => $uri])); $response->end(); }); ``` Initialization happens in the main coroutine. The programmer must create an `HttpServer` object, call `onRequest()`, and explicitly "start" the server. After that, `FrankenPHP` takes over control and blocks the main coroutine until the server shuts down. ```c bool frankenphp_suspend_main_coroutine(void) { zend_async_event_t *event = ecalloc(1, sizeof(zend_async_event_t)); event->start = frankenphp_server_wait_event_start; event->replay = frankenphp_server_wait_event_replay; // always false zend_async_resume_when(coroutine, event, true, ...); ZEND_ASYNC_SUSPEND(); } ``` To send results back to `Caddy`, `PHP` code uses the `Response` object, which provides `write()` and `end()` methods. Under the hood, memory is copied and results are sent to the channel. ```go func go_async_response_write(...) { dataCopy := make([]byte, int(length)) copy(dataCopy, unsafe.Slice((*byte)(data), int(length))) thread.responseChan <- responseWrite{requestID, dataCopy} } ``` ## Source Code The integration repository is a fork of `FrankenPHP` with the `true-async` branch: * [**true-async/frankenphp**](https://github.com/true-async/frankenphp/tree/true-async) -- integration repository Key files: | File | Description | |-------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------| | [`frankenphp_trueasync.c`](https://github.com/true-async/frankenphp/blob/true-async/frankenphp_trueasync.c) | Integration with `Scheduler`/`Reactor`: heartbeat, poll event, coroutine creation | | [`frankenphp_extension.c`](https://github.com/true-async/frankenphp/blob/true-async/frankenphp_extension.c) | PHP classes `HttpServer`, `Request`, `Response` | | [`async_worker.go`](https://github.com/true-async/frankenphp/blob/true-async/async_worker.go) | Go side: `round-robin`, `requestChan`, `responseChan`, `CGo` exports | | [`async_notifier.go`](https://github.com/true-async/frankenphp/blob/true-async/async_notifier.go) | `AsyncNotifier`: `eventfd` (Linux) / `pipe` (macOS) | | [`TRUE_ASYNC.README.md`](https://github.com/true-async/frankenphp/blob/true-async/TRUE_ASYNC.README.md) | Integration documentation | TrueAsync ABI used by the integration: | File | Description | |----------------------------------------------------------------------------------------------------------|---------------------------------------------------| | [`Zend/zend_async_API.h`](https://github.com/true-async/php-src/blob/true-async/Zend/zend_async_API.h) | API definition: macros, function pointers, types | | [`Zend/zend_async_API.c`](https://github.com/true-async/php-src/blob/true-async/Zend/zend_async_API.c) | Infrastructure: registration, stub implementations | --- --- url: https://true-async.github.io/en/architecture/zend-async-api.md description: >- Architecture of the PHP core asynchronous ABI -- function pointers, extension registration, global state, and ZEND_ASYNC_* macros. --- # TrueAsync ABI The `TrueAsync` `ABI` is built on a clear separation of **definition** and **implementation**: | Layer | Location | Responsibility | |-----------------|-------------------------|-------------------------------------------------| | **Zend Engine** | `Zend/zend_async_API.h` | Definition of types, structures, function pointers | | **Extension** | `ext/async/` | Implementation of all functions, registration via API | The `PHP` core does not call extension functions directly. Instead, it uses `ZEND_ASYNC_*` macros that invoke `function pointers` registered by the extension at load time. This approach serves two goals: 1. The async engine can work with any number of extensions implementing the `ABI` 2. Macros reduce dependency on implementation details and minimize refactoring ## Global State The portion of global state related to asynchrony resides in the PHP core and is also accessible via the `ZEND_ASYNC_G(v)` macro, as well as other specialized ones, such as `ZEND_ASYNC_CURRENT_COROUTINE`. ```c typedef struct { zend_async_state_t state; // OFF -> READY -> ACTIVE zend_atomic_bool heartbeat; // Scheduler heartbeat flag bool in_scheduler_context; // TRUE if currently in the scheduler bool graceful_shutdown; // TRUE during shutdown unsigned int active_coroutine_count; unsigned int active_event_count; zend_coroutine_t *coroutine; // Current coroutine zend_async_scope_t *main_scope; // Root scope zend_coroutine_t *scheduler; // Scheduler coroutine zend_object *exit_exception; zend_async_heartbeat_handler_t heartbeat_handler; } zend_async_globals_t; ``` ### Startup Currently, `TrueAsync` does not start immediately but does so lazily at the "right" moment. (This approach will change in the future, since virtually any PHP I/O function activates the `Scheduler`.) When a `PHP` script begins execution, `TrueAsync` is in the `ZEND_ASYNC_READY` state. On the first call to a function that requires the `Scheduler` via the `ZEND_ASYNC_SCHEDULER_LAUNCH()` macro, the scheduler is initialized and transitions to the `ZEND_ASYNC_ACTIVE` state. At this point, the code that was executing ends up in the main coroutine, and a separate coroutine is created for the `Scheduler`. In addition to `ZEND_ASYNC_SCHEDULER_LAUNCH()`, which explicitly activates the `Scheduler`, `TrueAsync` also intercepts control in the `php_execute_script_ex` and `php_request_shutdown` functions. ```c // php_execute_script_ex if (prepend_file_p && result) { result = zend_execute_script(ZEND_REQUIRE, NULL, prepend_file_p) == SUCCESS; } if (result) { result = zend_execute_script(ZEND_REQUIRE, retval, primary_file) == SUCCESS; } if (append_file_p && result) { result = zend_execute_script(ZEND_REQUIRE, NULL, append_file_p) == SUCCESS; } ZEND_ASYNC_RUN_SCHEDULER_AFTER_MAIN(); ZEND_ASYNC_INITIALIZE; ``` This code allows control to be passed to the `Scheduler` after the main thread finishes execution. The `Scheduler` in turn can launch other coroutines if any exist. This approach ensures not only 100% transparency of TrueAsync for the PHP programmer, but also full `PHP SAPI` compatibility. Clients using `PHP SAPI` continue to treat `PHP` as synchronous, even though an `EventLoop` is running internally. In the `php_request_shutdown` function, the final interception occurs to execute coroutines in destructors, after which the `Scheduler` shuts down and releases resources. ## Extension Registration Since the `TrueAsync ABI` is part of the `PHP` core, it is available to all `PHP` extensions at the earliest stage. Therefore, extensions have the opportunity to properly initialize `TrueAsync` before the `PHP Engine` is launched to execute code. An extension registers its implementations through a set of `_register()` functions. Each function accepts a set of function pointers and writes them to the core's global `extern` variables. Depending on the extension's goals, `allow_override` permits legally re-registering function pointers. By default, `TrueAsync` prohibits two extensions from defining the same `API` groups. `TrueAsync` is divided into several categories, each with its own registration function: * `Scheduler` -- API related to core functionality. Contains the majority of different functions * `Reactor` -- API for working with the `Event loop` and events. Contains functions for creating different event types and managing the reactor lifecycle * `ThreadPool` -- API for managing the thread pool and task queue * `Async IO` -- API for asynchronous I/O, including file descriptors, sockets, and UDP * `Pool` -- API for managing universal resource pools, with healthcheck and circuit breaker support ```c zend_async_scheduler_register( char *module, // Module name bool allow_override, // Allow overwrite zend_async_scheduler_launch_t, // Launch scheduler zend_async_new_coroutine_t, // Create coroutine zend_async_new_scope_t, // Create scope zend_async_new_context_t, // Create context zend_async_spawn_t, // Spawn coroutine zend_async_suspend_t, // Suspend zend_async_enqueue_coroutine_t, // Enqueue zend_async_resume_t, // Resume zend_async_cancel_t, // Cancel // ... and others ); ``` ```c zend_async_reactor_register( char *module, bool allow_override, zend_async_reactor_startup_t, // Initialize event loop zend_async_reactor_shutdown_t, // Shut down event loop zend_async_reactor_execute_t, // One reactor tick zend_async_reactor_loop_alive_t, // Are there active events zend_async_new_socket_event_t, // Create poll event zend_async_new_timer_event_t, // Create timer zend_async_new_signal_event_t, // Subscribe to signal // ... and others ); ``` --- --- url: https://true-async.github.io/en/docs/mobile.md description: >- native-bridge: a persistent PHP runtime inside a native Android app over JNI. Architecture, event exchange, calling Kotlin from PHP, code generation. --- # TrueAsync Mobile (demo project, experimental, repository [native-bridge](https://github.com/true-async/native-bridge), Android) Asynchronous PHP is a great fit for UI applications: the interface shouldn't freeze while something is talking to the network, reading from disk, or waiting for the next user action. TrueAsync has a dedicated C API for this: the Trigger Event (`ZEND_ASYNC_NEW_TRIGGER_EVENT()` in `zend_async_API.h`). It's an object with a single method, `trigger()`, that any C or C++ code can call from another thread to thread-safely wake up the PHP reactor and hand it control to process the event. **native-bridge** implements exactly this kind of integration for Android: PHP embeds into the app as a persistent process, starting once on a background thread, running an event loop (the same TrueAsync reactor used across the rest of the ecosystem), and talking to Kotlin in both directions. ## Why a persistent process instead of request/response The usual PHP scenario is a web request: the process starts up, handles one request, and exits. That doesn't fit a mobile app: PHP needs to stay alive for as long as the app is open, and react to user events (taps, sensors, location) the same way a handler reacts to an HTTP request. That's exactly what native-bridge gives you: PHP starts once when the app launches and lives in its own thread until it's explicitly stopped, while TrueAsync coroutines inside that thread handle events and background work concurrently. ## Bridge architecture The bridge works in two directions: 1. **Android to PHP.** Kotlin pushes events (a tap, a sensor reading, location, an arbitrary custom event) into a queue, and PHP pulls them from its own loop. 2. **PHP to Kotlin.** PHP calls methods implemented on the Kotlin side (show a Toast, vibrate, copy text to the clipboard, and so on). Both directions go through **JNI (Java Native Interface)**, the standard Android mechanism that lets C code call Kotlin/Java code and vice versa. Neither direction passes data through JSON or any other text format: values cross the boundary already typed, with no extra conversions. PHP runs on its own OS thread and never blocks Android's UI thread. If PHP is waiting on data, the UI thread keeps responding, and vice versa. ## Direction 1: events from Android to PHP Kotlin sends events over JNI into a queue that PHP reads with `NativeBridge::poll()`. When the queue is empty, `poll()` returns `null` right away, and the PHP application decides for itself whether to wait for the next event or do something else in the meantime (in the demo app that's a short `usleep()` pause, during which TrueAsync gets to run background coroutines and timers). There are four event types: a screen touch, location data, sensor data (accelerometer and similar), and an arbitrary event with a name and a text payload. That last kind is what the demo app uses to mark button presses: ```php use TrueAsync\NativeBridge; while (!NativeBridge::shouldStop()) { $event = NativeBridge::poll(); if ($event === null) { usleep(1000); continue; } if ($event['type'] === NativeBridge::EVENT_GENERIC) { match ($event['event_name']) { 'count_a' => $counterA->toggle(), 'fetch' => spawn(fn() => fetchDemo()), default => null, }; } } ``` The first three event types (touch, location, sensors) need no string allocation, so they stay cheap even at a high call rate (for example, for a stream of accelerometer data). ## Direction 2: calls from PHP to Kotlin When PHP calls a module method, for example `Toast::show('Hello', true)`, there are two ways that call can reach Kotlin: ### The generic path By default, PHP packs the arguments into a compact typed buffer (no string format like JSON, so Kotlin reads it without parsing text and without extra allocations) and ships it through a single call to `NativeBridge::invoke()`. Adding a new module or method on this path never touches C: only Kotlin and the generated PHP wrapper change, so a Gradle rebuild of the Kotlin side is enough, no need to rebuild the native library. ### The fast path: `#[FastPath]` For "hot" methods called very often (for example, feeding sensor data on every frame), the PHP spec marks the method with the `#[FastPath]` attribute. For such a method, the generator emits a dedicated typed C function that calls Kotlin directly over JNI, with no intermediate buffer. This kind of method requires rebuilding the native library (the `.so` file) on every change, but runs faster and without extra allocations. The method's behavior doesn't change, only the way the call crosses the PHP/Kotlin boundary. ## Describing a module: `#[BridgeModule]` A module's contract is described on the PHP side as an interface with the `#[BridgeModule]` attribute: ```php namespace TrueAsync\Android\Spec; #[BridgeModule] interface ToastInterface { #[Ui] public function show(string $text, bool $long): void; public function batteryLevel(): int; } ``` * The module name is derived from the interface name (`ToastInterface` becomes module `Toast`), or set explicitly: `#[BridgeModule('Clipboard')]`. * `#[Ui]` on a method means the Kotlin implementation must run on Android's UI thread (the generator adds the thread switch for you). * `#[FastPath]` on a method enables the fast call path described above. ## What `tools/bridge/gen.php` generates From a PHP spec (a `#[BridgeModule]` interface), the generator rebuilds on every run: * a Kotlin class with abstract methods (`ToastSpec`); * the call-routing code (Kotlin); * a PHP wrapper (`Toast::show(...)`) that the rest of the PHP app code calls; * for methods marked `#[FastPath]`, typed C code that calls Kotlin directly. ## PHP application lifecycle 1. Kotlin starts PHP on a background thread and passes it the path to the entry PHP script. 2. The PHP script calls `NativeBridge::init()`; from that point on the bridge is ready to accept events and calls. 3. From there the application runs in a loop: pull events through `poll()`, handle them, and spawn background TrueAsync coroutines when needed (for network requests, for example). 4. Shutdown is graceful: Kotlin calls `NativeBridge.stop()`, the PHP loop sees this through `NativeBridge::shouldStop()`, finishes up, and releases its resources cleanly. ## Example: a counter on a button A simplified example based on the demo app: a button starts and stops an endless counter, and its value updates directly in the UI. Starting and stopping are implemented with a plain TrueAsync coroutine's `spawn()`/`cancel()`, without blocking the UI thread: ```php use TrueAsync\NativeBridge; use TrueAsync\Android\Ui; use function Async\spawn; use function Async\delay; NativeBridge::init(); $root = Ui::newLinearLayout(); $button = Ui::newButton(); Ui::setText($button, '▶ start counter'); Ui::setOnClickListener($button, 'toggle'); Ui::addView($root, $button); $label = Ui::newTextView(); Ui::setText($label, 'stopped'); Ui::addView($root, $label); Ui::setContentView($root); $counter = null; function tick(int $label): void { $n = 0; while (true) { Ui::setText($label, 'tick ' . ++$n); delay(400); } } while (!NativeBridge::shouldStop()) { $event = NativeBridge::poll(); if ($event === null) { usleep(1000); continue; } if ($event['type'] === NativeBridge::EVENT_GENERIC && $event['event_name'] === 'toggle') { if ($counter === null) { $counter = spawn(fn() => tick($label)); Ui::setText($button, '■ stop counter'); } else { $counter->cancel(); $counter = null; Ui::setText($button, '▶ start counter'); } } } ``` A second click cancels the `$counter` coroutine through `cancel()`, and the counter stops at whatever value it reached. The full example, with several independent counters, is in the repository's `android/app.php`. ## Status and limitations * Only Android is supported; iOS support is planned but not yet implemented. * The bridge currently carries simple types: strings, integers, floats, booleans. Passing compound objects (by field, still with no string format) is planned. * The PHP-to-Kotlin direction is synchronous: a method returns its result immediately; deferred (asynchronous) results are not yet supported on this side. * PHP's opcache is force-disabled on Android: the app sandbox doesn't let it use the lock file and executable memory it needs. * A thread-safe (ZTS) PHP build is required, since PHP runs on its own OS thread rather than the app's main thread. ## See also * [Roadmap: TrueAsync Mobile](/en/roadmap.html) * [native-bridge repository on GitHub](https://github.com/true-async/native-bridge) --- --- url: https://true-async.github.io/en/docs/server.md description: >- TrueAsync Server — a native PHP extension that turns PHP into a high-performance HTTP/1.1/2/3 server. Multi-protocol, TLS 1.2/1.3, compression, coroutines — all in one process. --- # TrueAsync Server (PHP 8.6+, true\_async\_server 0.6+) **TrueAsync Server** is a native PHP extension that runs a high-performance HTTP server **directly inside the PHP process**. No separate daemon, no reverse-proxy, no FastCGI bridge. Out of the box it supports **HTTP/1.1 and HTTP/2 on the same TCP port**. Protocol selection happens via ALPN negotiation (for TLS) or HTTP Upgrade. HTTP/3 runs on the same UDP port (QUIC) and is advertised to clients through the `Alt-Svc` header. WebSocket and SSE are already done and run on the same single-listener-with-protocol-detect model. gRPC over HTTP/2 is still in progress (see [Roadmap](#features)). ```php use TrueAsync\HttpServer; use TrueAsync\HttpServerConfig; $server = new HttpServer( (new HttpServerConfig()) ->addListener('0.0.0.0', 8080) ->setWorkers(4) ); $server->addHttpHandler(function ($request, $response) { $response->setStatusCode(200)->setBody('Hello, World!'); }); $server->start(); ``` ## Why **The goal of the server is to unlock the potential of concurrent PHP applications.** TrueAsync gave the language real coroutines, non-blocking I/O, and connection pools. To turn that potential into production throughput, you need a server that is designed for the model from the start: a long-running process with an event loop, where every request gets its own coroutine and the scheduler switches between them on every I/O wait. TrueAsync Server is that server. There is no layer between coroutines and the network: listener, protocol parser, request dispatcher, and handler all live in one process and one event loop. Database connections are reused through `Async\Pool`, opcache stays hot between requests, and the cold-start cost is paid once, at `start()`. ## Features | Status | Feature | Details | |--------|---------|---------| | ✅ | **HTTP/1.1** | Full RFC 9112 compliance, keep-alive, pipelining (via [llhttp](https://github.com/nodejs/llhttp) — the same parser Node.js uses) | | ✅ | **HTTP/2** | Multiplexing, server push (libnghttp2 ≥ 1.57, floor for CVE-2023-44487) | | ✅ | **HTTP/3 / QUIC** | UDP transport on libngtcp2 + libnghttp3, OpenSSL 3.5 QUIC TLS API | | ✅ | **TLS 1.2 / 1.3** | OpenSSL 3.x, ALPN negotiation, weak ciphers disabled | | ✅ | **Compression** | gzip (zlib-ng / zlib), Brotli, zstd: for responses and inbound body decoding across all protocols | | ✅ | **Multipart / file uploads** | Streaming zero-copy parser | | ✅ | **Backpressure** | CoDel (RFC 8289), adaptive accept pause under load | | ✅ | **Streaming request body** | Optional via [`HttpRequest::readBody()`](/en/docs/reference/server/http-request.html); uploads without keeping the body in RAM | | ✅ | **sendFile** | Efficient file delivery from disk directly out of the handler | | ✅ | **Built-in worker pool** | `setWorkers(N)`: N threads via `Async\ThreadPool` + `SO_REUSEPORT` | | ✅ | **Per-request scope** | Each handler in its own scope; `Async\request_context()` gives a shared context across the entire request coroutine tree | | ✅ | **Native coroutines** | Deep TrueAsync integration: any blocking I/O in the handler suspends the coroutine, not the thread | | ✅ | **Zero-copy** | Minimal allocations on the hot path | | ✅ | **WebSocket** | RFC 6455, Upgrade from HTTP/1.1 and HTTP/2 (RFC 8441 Extended CONNECT), `wss://`, permessage-deflate (RFC 7692), full-duplex, backpressure, all 246 Autobahn|Testsuite tests | | ✅ | **SSE** | `text/event-stream` over HTTP/1.1, HTTP/2, and HTTP/3, the same handler regardless of protocol | | 📋 | **gRPC** | over HTTP/2, unary and streaming | ## Architecture: single-threaded event loop The same model used by [NGINX](https://nginx.org), [Envoy](https://www.envoyproxy.io), [Node.js](https://nodejs.org), and Rust [Tokio](https://tokio.rs)/[hyper](https://hyper.rs). **One thread owns both the connection and the request from accept to send.** There is no handoff between an accept thread and a worker thread, no locks, no context switches between them. A single event loop accepts the connection, reads bytes from the socket, parses HTTP, dispatches the request to the handler, and writes the response — without leaving the thread. ``` ┌─────────────────────────────────────────┐ │ Event Loop Thread │ │ │ accept ─► parse ─► dispatch ─► respond │ │ ▲ │ │ │ └──── coroutine yield ◄──┘ │ └─────────────────────────────────────────┘ ``` Non-blocking I/O is provided by the **libuv reactor** (through TrueAsync). When a coroutine needs to wait on a file, database, or the next WebSocket frame, it yields control to the event loop, which immediately picks up the next ready event. The thread never sits idle in `read()`/`recv()`. To scale across cores, **multi-worker** mode is enabled via [`setWorkers(N)`](/en/docs/reference/server/http-server-config.html#setworkers): the built-in `Async\ThreadPool` spins up N OS threads, each with its own independent event loop, and `SO_REUSEPORT` (Linux/BSD) lets the kernel distribute incoming connections across them. No shared state, no global locks. ## Where to start * [Quick start](/en/docs/server/quickstart.html): install and a minimal example in 5 minutes * [Configuration](/en/docs/server/configuration.html): listeners, workers, TLS, timeouts, body streaming, bootloader * [Compression](/en/docs/server/compression.html): gzip / brotli / zstd, negotiation, BREACH * [Static files and sendFile](/en/docs/server/static-files.html): `StaticHandler`, precompressed sidecars, Range * [Streaming](/en/docs/server/streaming.html): request body and response streaming * [SSE](/en/docs/server/sse.html): Server-Sent Events, `sseEvent()`, reconnection, heartbeat * [WebSocket](/en/docs/server/websocket.html): full-duplex connections, cross-worker pub/sub topics, backpressure * [Multi-worker](/en/docs/server/workers.html): `setWorkers(N)`, bootloader, hot reload, per-request scope * [Observability](/en/docs/server/observability.html): cross-worker stats, multi-sink logging, OTel access log * [Examples](/en/docs/server/examples.html): JSON API, static, fan-out, multipart upload * [Architecture](/en/architecture/server.html): internals ### API reference * [`TrueAsync\HttpServer`](/en/docs/reference/server/http-server.html) * [`TrueAsync\HttpServerConfig`](/en/docs/reference/server/http-server-config.html) * [`TrueAsync\HttpRequest`](/en/docs/reference/server/http-request.html) * [`TrueAsync\HttpResponse`](/en/docs/reference/server/http-response.html) * [`TrueAsync\WebSocket`](/en/docs/reference/server/websocket.html) * [`TrueAsync\StaticHandler`](/en/docs/reference/server/static-handler.html) * [`TrueAsync\SendFileOptions`](/en/docs/reference/server/send-file-options.html) * [`TrueAsync\UploadedFile`](/en/docs/reference/server/uploaded-file.html) * [`TrueAsync\LogSeverity`](/en/docs/reference/server/log-severity.html) * [Exceptions](/en/docs/reference/server/exceptions.html) ## Alternatives [FrankenPHP](/en/docs/frankenphp.html) is a separate embeddable server built on Caddy/Go, where PHP acts as a worker. It is a good fit when you need Caddy features (automatic Let's Encrypt, configuration through a Caddyfile) or integration into an existing Caddy deployment. TrueAsync Server is the native alternative without a Go runtime: the server lives directly inside the PHP process. --- --- url: https://true-async.github.io/en/architecture/server.md description: >- TrueAsync Server internals: single-threaded event loop, zero-copy, CoDel, bailout firewall, multi-worker via SO_REUSEPORT. --- # TrueAsync Server architecture (PHP 8.6+, true\_async\_server 0.6+) TrueAsync Server is a native PHP extension (in C) that runs an HTTP server directly inside the PHP process's address space. Architecturally it is a **single-threaded event loop** with an optional **replicated worker pool** for horizontal scaling inside a single process. ## Big picture ``` ┌────────────────────────────────────────────────────────────┐ │ PHP process │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ Event-loop thread #0 │ │ │ │ │ │ │ │ libuv ──► accept ──► parse ──► dispatch ──► send │ │ │ │ ▲ ▼ │ │ │ │ │ ┌──── PHP handler (coroutine) ────┐ │ │ │ │ │ │ user code, DB, HTTP client, … │ │ │ │ │ │ └─────────────┬───────────────────┘ │ │ │ │ └──────── yield ────┘ │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ Event-loop threads #1 …N-1 │ │ │ │ (when setWorkers(N>1), SO_REUSEPORT) │ │ │ └──────────────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────────────┘ ``` One thread holds the connection and the request from accept to final send. There is no accept→worker handoff, no per-request fork/cleanup, and no global locks. When the handler needs to wait on I/O (DB, HTTP, file), the coroutine yields to the event loop, which immediately picks up the next ready event. ## Layers ### 1. Reactor: libuv The base I/O layer: libuv via the [TrueAsync ABI](/en/architecture/zend-async-api.html). TCP accepts, UDP recvmmsg, file operations, timers, sigwait — all go through the same `zend_async_event_t` interface. The reactor knows about epoll/kqueue/IOCP; the server does not. Critical extension API: * `zend_async_io_*` — non-blocking socket and file reads/writes. * `zend_async_io_sendfile_t` — `uv_fs_sendfile` (Linux/BSD `sendfile`, Windows `TransmitFile`). * `zend_async_fs_open_t` — async `open(2)` through the libuv thread pool. * `udp_bind` for HTTP/3 / QUIC. ### 2. Protocol parsers * **HTTP/1.1**: vendored [`llhttp`](https://github.com/nodejs/llhttp) 9.3.0 (the same parser Node.js uses). * **HTTP/2**: `libnghttp2` ≥ 1.57 (floor for CVE-2023-44487 rapid-reset). * **HTTP/3 / QUIC**: `libngtcp2` + `libnghttp3`, OpenSSL 3.5 QUIC TLS API (backend `libngtcp2_crypto_ossl`). Protocol detection on a single TCP socket: * plaintext: preface `PRI * HTTP/2.0\r\n...\r\n` → HTTP/2 (h2c), otherwise → llhttp. * TLS: ALPN negotiation on handshake. `HttpServer::addListener()` raises a multi-protocol listener. For protocol-restricted ports, use `addHttp1Listener` / `addHttp2Listener` / `addHttp3Listener`. ### 3. Connection arena `http_connection_t` — per-connection state (768 B). Stored in a slab pool: chunks of `CONN_ARENA_CHUNK_SLOTS` (256) slots each. Live/free is tracked via a bitmap; chunks never shrink, which gives a hot arena hit with no allocations. Visible through [`HttpServer::getRuntimeStats()`](/en/docs/reference/server/http-server.html#getruntimestats): `conn_arena_live`, `conn_arena_slots`, `conn_arena_chunks`, `conn_arena_bytes`. ### 4. Body pool A per-thread LIFO for large request-body buffers (≥ 1 MB). Bodies of this class are allocated through `zend_mm` but **returned** to the per-size-class LIFO instead of the allocator. The next request of the same size class reuses the slot — no `mmap`/`munmap` traffic and no `mmap_lock` contention that used to cap multi-worker scaling on upload-heavy workloads. Bench (W=8, c=128, 2 MiB POST body): 1500 RPS / 370% CPU → **3300 RPS / 720% CPU** (×2.2 throughput; CPU now actually scales with workers). Drained on `HttpServer::stop()` and RSHUTDOWN. In debug builds the zend\_mm leak detector sees a clean slate at module unload. ### 5. Coroutine integration Every accepted request spawns a new coroutine via `ZEND_ASYNC_NEW_COROUTINE`. The coroutine runs in a **per-request scope** that is a child of the server scope. This produces two effects: * `Async\request_context()` resolves to a context shared across the entire request coroutine subtree. * `Async\current_context()` stays per-coroutine. Request cancellation (handler coroutine cancelled → 4xx parser limit, peer reset on the stream, drain timeout) propagates through the normal `AsyncCancellation` chain. `TrueAsync\HttpException extends AsyncCancellation` carries the HTTP status so the dispatcher knows what to tell the client. ### 6. Multi-worker (optional) `HttpServerConfig::setWorkers(N > 1)`: 1. The parent spawns an `Async\ThreadPool` of size N. 2. The config + handler set are copied into each worker via `transfer_obj` (deep copy of the entire graph, including closure op\_arrays; see [Thread snapshot](/en/architecture/zend-async-api.html)). 3. Each worker re-binds the same listeners with `SO_REUSEPORT`. 4. The kernel (Linux/BSD) distributes accepts evenly across sockets in the same reuse-port group. 5. The parent `start()` waits for all workers to finish. Each worker has an independent event loop, opcache, and allocator. No shared state, no locks. The bootloader (if set) runs in each worker once before the task loop. ## CoDel backpressure The server implements [CoDel](https://datatracker.ietf.org/doc/html/rfc8289), adaptive backpressure by sojourn time: * Every request is timestamped at enqueue → dequeue. * If sojourn (queue-wait) stays above `setBackpressureTargetMs()` (default 5 ms) for **100 ms in a row**, the listen socket is paused. * As soon as sojourn drops back, the listener resumes. Unlike a rigid `max_connections`, CoDel **tracks real pipeline load**, not just the number of concurrent connections. This matters especially on HTTP/2, where a single connection carries an arbitrary number of streams. CoDel is opt-in by default — after 0.3.0, situations where CoDel triggered incorrectly on muxed H2 (short, fast streams pushed the connection into "overloaded" and parked unrelated long-lived streams) led to a conservative default. ## Bailout firewall PHP fatal errors from the user handler (E\_ERROR, OOM, uncaught on shutdown) **do not crash the server**. Every protocol entry point (H1, H2, H3) wraps the handler call in a bailout fence which: 1. Drains the failing coroutine. 2. Emits 500 to the client (if headers are not on the wire yet). 3. Returns control to the listener, which keeps accepting. Diagnostics: on the failure path the server logs the C stack (when `` is available; gated by `HAVE_EXECINFO_H`) and the PHP-level `zend_error`. On musl / Windows the C frame dump is silently skipped. See [`docs/118-tracing-jit-stale-fp-spill.md`](https://github.com/true-async/server/tree/main/docs) in the repository for one of the early bailout bugs under Tracing JIT. ## Connection draining (Step 8) The server implements two drain models: ### Proactive: `setMaxConnectionAgeMs()` After `(age ± 10% jitter)` lifetime the connection receives a signal: * H1: the next response carries `Connection: close`. * H2: a `GOAWAY` is emitted. Equivalent to gRPC `MAX_CONNECTION_AGE`. Protects against long-lived connections "stuck" to a single worker behind an L4 LB. ### Reactive: CoDel trip / hard-cap transition When the server enters overload (CoDel paused or `max_connections` hit), the per-connection drain effect is spread across the `setDrainSpreadMs()` window (equivalent to HAProxy `close-spread-time`) so clients do not reconnect in a thundering herd. The minimum gap between triggers is set by `setDrainCooldownMs()` (default 10 s). ## Zero-copy hot paths * **H2 over TLS hybrid emit** (0.6.2): small responses go through the DRAIN path (mem\_send + `BIO_write`, no gather allocation); bodies > 2 KiB or streaming go through GATHER (NO\_COPY refs + a single `SSL_write_ex`). Bench: best-of-three on the h2load matrix. * **Static small-file fast path** (≤ 64 KiB): the file is slurped into a `zend_string` and sent with a single `writev(headers + body)`. Files > 64 KiB go through sendfile. * **Inline `open`/`fstat`** for static: no futex round trip through the libuv thread pool on a warm dentry cache. ## Memory model The server deliberately minimises RAM footprint: * **Asymmetric TLS BIO ring sizes** (0.6.0): CT-in 17 KiB, PT-app back-channel 17 KiB, the rest unchanged; saves ~62 KiB per TLS connection. * **Body pool** (see above): reuse of large bodies. * **Streaming request body**: peak RSS on 50 parallel 20-MiB POSTs drops from 1170 MiB to **197 MiB**. * **Static TSRMLS cache** (ext/async 0.7.0): `-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1` turns `EG()` / `ASYNC_G()` into a single `__thread` load instead of `pthread_getspecific`. +32% RPS on a minimal HTTP handler. ## RFC compliance * HTTP/1.1: full RFC 9112 (`Connection: close` → reply mirror per §9.6 since 0.6.3). * HTTP/2: RFC 9113, rapid-reset mitigation for CVE-2023-44487. * HTTP/3: RFC 9114, QUIC RFC 9000 including connection ID rotation and amplification limits. * TLS: TLS 1.2/1.3 only, OpenSSL 3.x; HTTP/3 requires OpenSSL 3.5+. * WebSocket / SSE / gRPC: planned. ## See also * [TrueAsync ABI](/en/architecture/zend-async-api.html) * [Scheduler & Reactor](/en/architecture/scheduler-reactor.html) * [Server configuration](/en/docs/server/configuration.html) * [Multi-worker](/en/docs/server/workers.html) --- --- url: https://true-async.github.io/en/docs/server/configuration.md description: >- HttpServerConfig: listeners, TLS, timeouts, backpressure, body limits, body streaming, hot reload, WebSocket topics, multi-sink logging, statistics, HTTP/3. --- # TrueAsync Server configuration (PHP 8.6+, true\_async\_server 0.6+) All server configuration is set through the [`TrueAsync\HttpServerConfig`](/en/docs/reference/server/http-server-config.html) object before calling `new HttpServer($config)`. Once `HttpServer` is constructed, the config is **frozen**: any setter on it throws `HttpServerRuntimeException`. ```php use TrueAsync\HttpServer; use TrueAsync\HttpServerConfig; use TrueAsync\LogSeverity; $config = (new HttpServerConfig()) ->addListener('0.0.0.0', 8080) ->addListener('0.0.0.0', 8443, tls: true) ->addHttp3Listener('0.0.0.0', 8443) ->setCertificate('/etc/tls/server.crt') ->setPrivateKey('/etc/tls/server.key') ->setWorkers(4) ->setKeepAliveTimeout(60) ->setMaxBodySize(50 * 1024 * 1024) ->setCompressionEnabled(true) ->setLogSeverity(LogSeverity::INFO) ->setLogStream(STDERR); $server = new HttpServer($config); ``` Setters return `static`, so the config is built as a chain. ## Listeners The server can listen on any number of TCP/Unix sockets and UDP ports (for HTTP/3) at the same time. | Method | What it does | |--------|--------------| | `addListener($host, $port, $tls = false)` | TCP, HTTP/1.1 + HTTP/2 (h2c by preface on plaintext, h2 via ALPN on TLS) | | `addHttp1Listener($host, $port, $tls = false)` | TCP, HTTP/1.1 only. A client sending an HTTP/2 preface gets 400 | | `addHttp2Listener($host, $port, $tls = false)` | TCP, HTTP/2 only. Without TLS this is h2c and the preface is required | | `addHttp3Listener($host, $port)` | UDP, HTTP/3 / QUIC. TLS 1.3 is enabled automatically, the server certificate is used | | `addUnixListener($path)` | Unix socket, HTTP/1.1 + HTTP/2 (h2c-style) | ```php $config ->addListener('0.0.0.0', 80) // H1 + H2c ->addListener('0.0.0.0', 443, tls: true) // H1 + H2 over TLS ->addHttp3Listener('0.0.0.0', 443); // H3 / QUIC on the same port ``` For a phased HTTP/3 rollout you can temporarily disable the `Alt-Svc` advertisement: ```php $config->setHttp3AltSvcEnabled(false); ``` ## TLS ```php $config ->setCertificate('/etc/tls/server.crt') ->setPrivateKey('/etc/tls/server.key'); ``` The certificate and key are shared across all TLS listeners (including HTTP/3). TLS 1.2/1.3, ALPN, weak ciphers disabled, stateless session tickets, safe renegotiation off. ## Workers and bootloader `setWorkers(1)` (the default) enables single-threaded mode: `start()` runs the event loop on the calling thread. `setWorkers(N > 1)` spins up the built-in pool of N threads via `Async\ThreadPool`. Each worker re-binds the same listeners and the kernel (Linux/BSD) distributes accepts through `SO_REUSEPORT`. The parent `start()` waits for all workers to finish. ```php $config ->setWorkers(4) ->setBootloader(function () { // runs once in each worker before the task loop require __DIR__ . '/vendor/autoload.php'; Database::warmupPool(); OpcacheWarm::compile(); }) ->setRequestScope(true); // default; false saves 2 allocs/req but nulls request_context() ``` ### Hot reload Replace the worker cohort without dropping a connection (pool mode only). Both triggers call `HttpServer::reload()`, which re-runs the bootloader in fresh workers on the same sockets: ```php $config ->enableHotReload([__DIR__ . '/app'], ['php'], debounceMs: 300, maxHoldMs: 2000) // watch files (dev) ->enableReloadOnSignal(); // SIGHUP (prod) ``` Details: [Multi-worker](/en/docs/server/workers.html). ## Timeouts | Method | Default | What it times out | |--------|---------|-------------------| | `setReadTimeout($sec)` | — | receiving the full request | | `setWriteTimeout($sec)` | — | sending the response | | `setKeepAliveTimeout($sec)` | — | idle between requests; `0` disables keep-alive | | `setShutdownTimeout($sec)` | — | graceful shutdown: how long to wait for active requests | ## Limits and backpressure ```php $config ->setBacklog(1024) ->setMaxConnections(50_000) ->setMaxInflightRequests(10_000) ->setMaxBodySize(10 * 1024 * 1024) ->setBackpressureTargetMs(10); ``` * **`setMaxConnections($n)`** — hard limit on the number of TCP connections. `0` removes the cap. * **`setMaxInflightRequests($n)`** — admission control: once the in-flight handler count hits this limit, new requests get a fast rejection. H1 → 503 + `Retry-After: 1`; H2 → `RST_STREAM REFUSED_STREAM` (retry-safe per RFC 7540 §8.1.4). A hard connection cap does not help on H2, because new streams arrive over an already-accepted connection. `0` derives the limit as `max_connections × 10`. * **`setMaxBodySize($bytes)`** — maximum request body size. Default 10 MiB, range 1 KiB..16 GiB. H1 returns 413 and closes the connection; H2 sends `RST_STREAM(INTERNAL_ERROR)`. * **`setBackpressureTargetMs($ms)`** — CoDel sojourn threshold for accept-side backpressure. When the per-request queue-wait stays above the threshold for 100 ms in a row, the listen socket is paused. `0` disables CoDel. Default 5 ms; 10–20 ms is typical for web workloads; 50–100 ms for slow handlers (database, IO). ### Graceful drain (Step 8) Controls for migrating load behind an L4 balancer: | Method | Default | Purpose | |--------|---------|---------| | `setMaxConnectionAgeMs($ms)` | 0 (off) | After ±10% jitter on the limit, a connection gets Connection: close (H1) or GOAWAY (H2). Equivalent to gRPC `MAX_CONNECTION_AGE`. Production: 600\_000 (10 min). | | `setMaxConnectionAgeGraceMs($ms)` | 0 | Hard-close after `Connection: close`/GOAWAY. `0` disables the force-close timer. | | `setDrainSpreadMs($ms)` | 5000 | Window for evenly spreading per-connection drain on CoDel trip / hard-cap (anti-thundering-herd). | | `setDrainCooldownMs($ms)` | 10\_000 | Minimum gap between reactive drain triggers. | ## HTTP/2 streaming limits ```php $config ->setStreamWriteBufferBytes(256 * 1024) // 256 KiB per stream, 4 KiB .. 64 MiB ->setH2StaticBudgetMax(0); // 0 = auto (memory_limit / 8) ``` `HttpResponse::send($chunk)` blocks the handler coroutine **only** under backpressure, when the per-stream staging buffer is full. The default is 256 KiB (for comparison: gRPC-Go 64 KiB, Envoy 1 MiB, Node.js 16 KiB). ## HTTP/3 production knobs ```php $config ->setHttp3IdleTimeoutMs(30_000) // RFC 9000 §10.1 ->setHttp3StreamWindowBytes(256 * 1024) // per-stream flow control ->setHttp3MaxConcurrentStreams(100) // initial_max_streams_bidi ->setHttp3PeerConnectionBudget(16) // per-source-IP cap, slow-loris protection ->setHttp3SocketBufferBytes(8 << 20) // UDP rcv/snd buffer, absorbs inbound bursts ->setHttp3Pacing(false) // opt-in send pacing, for lossy/rate-limited paths ->setHttp3AltSvcEnabled(true); // RFC 7838 Alt-Svc advertisement ``` The connection-level `initial_max_data` is derived as `window × max_concurrent_streams` (the nginx pattern). * **`setHttp3SocketBufferBytes($bytes)`** — UDP socket receive/send buffer. Absorbs inbound bursts so they don't overflow into `RcvbufErrors`. Default 8 MiB; the kernel clamps to `net.core.{r,w}mem_max` unless privileged. `0` leaves the OS default. * **`setHttp3Pacing($bool)`** — cap each burst at the congestion controller's `send_quantum` and space packets on ngtcp2's pacing timer. Off by default: on a lossless path pacing only adds cost, so enable it for constrained paths only. ## WebSocket ```php $config ->setWsMaxMessageSize(1024 * 1024) // 1 MiB, 128 .. 256 MiB ->setWsMaxFrameSize(1024 * 1024) // 1 MiB, same range ->setWsPingIntervalMs(30_000) // keepalive PING on idle ->setWsPongTimeoutMs(60_000) // deadline for the PONG reply ->setWsPermessageDeflate(false) // RFC 7692, off by default ->setWsMaxSubscriptions(0) // topic-filter cap per connection; 0 = no limit ->setWsPublishRateLimit(0); // publish() token bucket; 0 = off ``` * **`setWsMaxMessageSize($bytes)`** — max size for a reassembled message. Going over it produces `1009 Message Too Big` and closes the connection (RFC 6455 §7.4.1). * **`setWsMaxFrameSize($bytes)`** — max size for a single frame. Guards against fragment-flood, where the client sends millions of tiny fragments. * **`setWsPingIntervalMs($ms)`** — how often the server pings idle connections on its own. `0` disables the automatic ping. * **`setWsPongTimeoutMs($ms)`** — how long to wait for PONG after a PING before treating the connection as dead and closing it with code `1001 GoingAway`. `0` disables the timeout. * **`setWsPermessageDeflate($bool)`** — RFC 7692, message-level compression. Off by default: it's a deliberate opt-in, because compression costs CPU and widens the decompression-bomb attack surface. Negotiated only when the client itself offers this extension; requires a build with zlib. * **`setWsMaxSubscriptions($count)`** — how many distinct topic filters one connection may hold. `0` (default) is no limit, as every self-hosted broker ships. Set it when client input reaches `subscribe()`; over the cap, `subscribe()` throws `WebSocketException`. * **`setWsPublishRateLimit($perSecond, $burst = 0)`** — per-connection token bucket over `publish()`, the one WS call that causes work on every worker. `0` (default) is off. Over the rate, `publish()` throws `WebSocketBackpressureException`. See the [WebSocket guide](/en/docs/server/websocket.html) for the topic pub/sub model and the [reference](/en/docs/reference/server/websocket.html) for the connection API itself. ## Body streaming Enables pull-based request-body streaming (issue #26): the H1/H2 parsers push chunks into a queue and the handler reads them through [`HttpRequest::readBody()`](/en/docs/reference/server/http-request.html#readbody) without holding the entire body in RAM. ```php $config->setBodyStreamingEnabled(true); $server->addHttpHandler(function ($req, $res) { while (($chunk = $req->readBody()) !== null) { // process chunk (e.g., stream write to disk, parse on the fly) } $res->setStatusCode(204); }); ``` Without `setBodyStreamingEnabled(true)`, the handler receives the already-read body through `getBody()`; `readBody()` is unavailable in that mode. A 50-parallel-20-MiB-POST comparison (h2load, WSL2): peak RSS drops 1170 MiB → **197 MiB** (×6), throughput climbs from 36 req/s → **100 req/s** (×2.7), because handler dispatch no longer waits for the full body. See also [Streaming](/en/docs/server/streaming.html). ## Auto-await body ```php $config->setAutoAwaitBody(true); // default: true ``` When enabled, non-multipart requests wait for the full body before calling the handler (multipart is always streamed). Useful for classic whole-body processing. ## JSON ```php $config->setJsonEncodeFlags(JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ``` These flags are applied to [`HttpResponse::json()`](/en/docs/reference/server/http-response.html#json) when the caller does not pass `$flags` explicitly. `JSON_THROW_ON_ERROR` is silently stripped: an encoding error produces a 500 with a JSON error body — the exception is not propagated into the handler. ## Logging and statistics For a single console stream, the `setLogSeverity()` / `setLogStream()` sugar is enough: ```php use TrueAsync\LogSeverity; $config ->setLogSeverity(LogSeverity::INFO) ->setLogStream(STDERR); // any php_stream: file, php://stderr, php://memory, user wrapper ``` The logger is disabled by default (`LogSeverity::OFF`). Levels (OpenTelemetry SeverityNumber): | Level | Contents | |-------|----------| | `OFF` (0) | nothing | | `DEBUG` (5) | H3 packet tracing and similar | | `INFO` (9) | server lifecycle (start/stop), bind retries | | `WARN` (13) | TLS handshake fail, peer reset, absorbed exceptions | | `ERROR` (17) | listener bind failed, hard protocol errors | `FATAL` is intentionally absent: it travels through `zend_error_noreturn(E_ERROR)`, which already terminates the process. > **Under a worker pool, do not use `setLogStream()`.** A parent-opened stream resource cannot > cross into worker threads. Use `setLogSinks()` with a `file` / `stdout` / `stderr` sink each > worker can open itself. For multiple destinations, a structured **access log**, syslog, or JSON output, use `setLogSinks()` — and opt into cross-worker `getStats()` with `setStatsEnabled(true)`: ```php use TrueAsync\LogSeverity; $config ->setStatsEnabled(true) ->setLogSinks([ ['type' => 'file', 'path' => '/var/log/app/access.log', 'format' => 'json', 'category' => 'access', 'level' => LogSeverity::INFO], ['type' => 'stderr', 'format' => 'pretty', 'level' => LogSeverity::WARN], ]); ``` Both are covered in full on the [Observability](/en/docs/server/observability.html) page. ## Telemetry (W3C Trace Context) ```php $config->setTelemetryEnabled(true); ``` When enabled, incoming `traceparent` / `tracestate` are parsed and attached to the request. The following are available inside the handler: ```php $req->getTraceParent(); // raw header $req->getTraceState(); $req->getTraceId(); // 32 lower-hex chars $req->getSpanId(); // 16 lower-hex chars $req->getTraceFlags(); // int (0x01 = sampled) ``` ## Full reference See [`TrueAsync\HttpServerConfig`](/en/docs/reference/server/http-server-config.html): all 60+ methods with detailed descriptions and valid value ranges. --- --- url: https://true-async.github.io/en/docs/server/examples.md description: >- Ready-made recipes: JSON API, fan-out, multipart upload, static, redirect, SSE, bailout firewall. --- # TrueAsync Server examples (PHP 8.6+, true\_async\_server 0.6+) ## JSON API with parallel fan-out ```php use TrueAsync\HttpServer; use TrueAsync\HttpServerConfig; use function Async\spawn; use function Async\await_all; $server = new HttpServer( (new HttpServerConfig()) ->addListener('0.0.0.0', 8080) ->setWorkers(\Async\available_parallelism()) ); $server->addHttpHandler(function ($req, $res) { if ($req->getPath() !== '/dashboard') { $res->setStatusCode(404)->json(['error' => 'not found']); return; } $userId = (int) $req->getQueryParam('user_id'); // Three independent database queries in parallel [$user, $posts, $followers] = await_all([ spawn(fn() => fetchUser($userId)), spawn(fn() => fetchPosts($userId)), spawn(fn() => fetchFollowers($userId)), ]); $res->json([ 'user' => $user, 'posts' => $posts, 'followers' => $followers, ]); }); $server->start(); ``` ## Static + dynamic ```php use TrueAsync\HttpServer; use TrueAsync\HttpServerConfig; use TrueAsync\StaticHandler; use TrueAsync\StaticOnMissing; $config = (new HttpServerConfig()) ->addListener('0.0.0.0', 8080) ->setCompressionEnabled(true); $server = new HttpServer($config); // /assets/* is served from public/, no PHP handler $server->addStaticHandler( (new StaticHandler('/assets/', __DIR__ . '/public')) ->setIndexFiles('index.html') ->enablePrecompressed('br', 'gzip') ->setCacheControl('public, max-age=31536000, immutable') ); // Everything else goes to PHP $server->addHttpHandler(function ($req, $res) { $res->setStatusCode(200)->html('

Dynamic route: ' . htmlspecialchars($req->getPath()) . '

'); }); $server->start(); ``` ## Multipart upload with file move ```php use TrueAsync\HttpServer; use TrueAsync\HttpServerConfig; $server = new HttpServer( (new HttpServerConfig()) ->addListener('0.0.0.0', 8080) ->setMaxBodySize(100 * 1024 * 1024) ); $server->addHttpHandler(function ($req, $res) { if ($req->getMethod() !== 'POST') { $res->setStatusCode(405); return; } $avatar = $req->getFile('avatar'); if ($avatar === null || !$avatar->isValid()) { $res->setStatusCode(400)->json(['error' => 'no valid avatar']); return; } if ($avatar->getSize() > 5 * 1024 * 1024) { $res->setStatusCode(413)->json(['error' => 'too big']); return; } $target = '/var/storage/avatars/' . bin2hex(random_bytes(8)) . '.bin'; $avatar->moveTo($target); $res->json([ 'saved' => $target, 'name' => $avatar->getClientFilename(), 'mime' => $avatar->getClientMediaType(), 'size' => $avatar->getSize(), ]); }); $server->start(); ``` ## Streaming a large upload without holding it in RAM ```php $server = new HttpServer( (new HttpServerConfig()) ->addListener('0.0.0.0', 8080) ->setBodyStreamingEnabled(true) ->setMaxBodySize(10 * 1024 * 1024 * 1024) // 10 GiB ); $server->addHttpHandler(function ($req, $res) { $id = bin2hex(random_bytes(8)); $fp = fopen("/var/storage/uploads/$id.bin", 'wb'); if ($fp === false) { $res->setStatusCode(500)->json(['error' => 'open failed']); return; } $total = 0; try { while (($chunk = $req->readBody()) !== null) { fwrite($fp, $chunk); $total += strlen($chunk); } } finally { fclose($fp); } $res->json(['id' => $id, 'size' => $total]); }); $server->start(); ``` ## SSE (Server-Sent Events) ```php $server->addHttpHandler(function ($req, $res) { if ($req->getPath() !== '/events') { $res->setStatusCode(404); return; } for ($i = 0; $i < 60; $i++) { $res->sseEvent(json_encode(['t' => time(), 'i' => $i])); if (!$res->sendable()) { break; // the client is gone, no point waiting } \Async\delay(1000); } $res->end(); }); ``` See the [SSE guide](/en/docs/server/sse.html) for the full walkthrough of every method. ## WebSocket (echo server) ```php use TrueAsync\HttpServer; use TrueAsync\HttpServerConfig; use TrueAsync\WebSocket; $server = new HttpServer( (new HttpServerConfig()) ->addListener('0.0.0.0', 8080) ); $server->addWebSocketHandler(function (WebSocket $ws) { foreach ($ws as $msg) { if ($msg->binary) { $ws->sendBinary($msg->data); } else { $ws->send('echo: ' . $msg->data); } } }); // Required: the server refuses to start without an HTTP handler, and it answers // the requests that are not upgrades. $server->addHttpHandler(fn ($req, $res) => $res->setStatusCode(404)->end()); $server->start(); ``` Broadcasting to every client — including those served by **other worker threads** — is what topics are for. Subscribe on connect, `publish()` to fan out; no shared array, no `setWorkers(1)`: ```php use TrueAsync\HttpRequest; $server = new HttpServer( (new HttpServerConfig()) ->addListener('0.0.0.0', 8080) ->setWorkers(4) // a real chat, across 4 threads ); $server->addWebSocketHandler(function (WebSocket $ws, HttpRequest $req) { $room = ltrim($req->getPath(), '/') ?: 'lobby'; $ws->subscribe("chat/$room"); foreach ($ws as $msg) { $ws->publish("chat/$room", $msg->data); // reaches subscribers on ALL workers } }); $server->addHttpHandler(fn ($req, $res) => $res->setStatusCode(404)->end()); $server->start(); ``` `publish()` never suspends: a peer whose socket is backed up drops the message rather than stalling the rest of the topic. See the [WebSocket guide](/en/docs/server/websocket.html#topics-publishsubscribe-across-every-worker) for the topic model, filters, and rate limits. ## File download with auth ```php use TrueAsync\SendFileOptions; use TrueAsync\SendFileDisposition; $server->addHttpHandler(function ($req, $res) { $token = $req->getHeader('Authorization'); if (!isValidToken($token)) { $res->setStatusCode(401); return; } $reportId = preg_replace('#[^a-z0-9-]#', '', $req->getQueryParam('id') ?? ''); if ($reportId === '') { $res->setStatusCode(400); return; } $res->sendFile("/var/storage/reports/$reportId.pdf", new SendFileOptions( contentType: 'application/pdf', disposition: SendFileDisposition::ATTACHMENT, downloadName: "report-$reportId.pdf", cacheControl: 'private, no-store', )); }); ``` ## Redirect ```php $server->addHttpHandler(function ($req, $res) { if ($req->getPath() === '/old-url') { $res->redirect('/new-url', 301); return; } // ... }); ``` ## Pre-encoded JSON (skip re-encoding) ```php $cache = new RedisCache(); $server->addHttpHandler(function ($req, $res) use ($cache) { $key = 'feed:' . $req->getQueryParam('uid'); $cached = $cache->get($key); if ($cached !== null) { // a string → sent as is, without re-packing $res->json($cached); return; } $payload = buildFeed($req); $cache->set($key, $payload = json_encode($payload), 60); $res->json($payload); }); ``` ## Bailout firewall: fatals do not bring the server down A deliberately broken handler: ```php $server->addHttpHandler(function ($req, $res) { if ($req->getPath() === '/boom') { throw new \Error('uncaught fatal'); } $res->setStatusCode(200)->setBody('ok'); }); ``` A request to `/boom` produces **500 Internal Server Error**, the handler coroutine drains, and the listener keeps accepting new connections. The same behaviour applies to E\_ERROR, OOM, and uncaught exceptions during shutdown. Works on H1, H2, and H3. ## Custom HTTP exception `TrueAsync\HttpException extends Async\AsyncCancellation`. Throw it anywhere inside the handler to send a specific HTTP status through the normal cancellation chain. ```php use TrueAsync\HttpException; class NotFoundException extends HttpException {} class ForbiddenException extends HttpException {} $server->addHttpHandler(function ($req, $res) { $user = User::find($req->getQueryParam('id')) ?? throw new NotFoundException('user not found', 404); if (!$user->canBeViewedBy(currentUser())) throw new ForbiddenException('access denied', 403); $res->json($user->toArray()); }); ``` The status is taken from `$code` (must be 4xx/5xx, otherwise 500) and the body from `$message`. ## See also * [`examples/` in the repository](https://github.com/true-async/server/tree/main/examples) (`minimal-server.php`, `demo-server.php`, `multi-worker.php`, `multi-worker-manual.php`) * [Configuration](/en/docs/server/configuration.html) * [Multi-worker](/en/docs/server/workers.html) --- --- url: https://true-async.github.io/en/docs/reference/server/exceptions.md description: >- Server exception hierarchy: HttpServerException and descendants, plus HttpException — a cancellation carrier of an HTTP status. --- # TrueAsync Server exceptions (PHP 8.6+, true\_async\_server 0.1+) ## Hierarchy ``` \Exception └── TrueAsync\HttpServerException // base ├── TrueAsync\HttpServerRuntimeException // final ├── TrueAsync\HttpServerInvalidArgumentException // final ├── TrueAsync\HttpServerConnectionException // final ├── TrueAsync\HttpServerProtocolException // final └── TrueAsync\HttpServerTimeoutException // final \Async\AsyncCancellation └── TrueAsync\HttpException // NOT final — subclass for domain exceptions ``` ## TrueAsync\HttpServerException ```php namespace TrueAsync; class HttpServerException extends \Exception {} ``` Base exception for all server errors. Catch-all for error handling when the error domain does not matter. ## TrueAsync\HttpServerRuntimeException ```php final class HttpServerRuntimeException extends HttpServerException {} ``` Runtime errors during server operation. Typical sources: * Mutating the config after `new HttpServer($config)` (`$config->setXxx()` after lock). * Mutating `StaticHandler` after attach (`$static->setXxx()` after `addStaticHandler()`). * Any attempt to modify `HttpResponse` after `sendFile()` (response sealed). * `end()`-after-`end()`, `write()` after `sendFile()`, and similar lifecycle violations. ## TrueAsync\HttpServerInvalidArgumentException ```php final class HttpServerInvalidArgumentException extends HttpServerException {} ``` Invalid argument. Thrown by `HttpServerConfig`/`StaticHandler`/`UploadedFile` setters when a value is out of the valid range (for example, `setBrotliLevel(99)`, `setMaxBodySize(0)`, an unknown content-coding in `enablePrecompressed()`). ## TrueAsync\HttpServerConnectionException ```php final class HttpServerConnectionException extends HttpServerException {} ``` Socket-level and network errors: bind failed, listener not up, peer reset on a critical protocol path. ## TrueAsync\HttpServerProtocolException ```php final class HttpServerProtocolException extends HttpServerException {} ``` Protocol-level errors: malformed HTTP, invalid headers, unrecoverable protocol violations. ## TrueAsync\HttpServerTimeoutException ```php final class HttpServerTimeoutException extends HttpServerException {} ``` Timeouts: read, write, keep-alive, graceful shutdown. ## TrueAsync\HttpException ```php namespace TrueAsync; class HttpException extends \Async\AsyncCancellation {} ``` **A special class**: extends `Async\AsyncCancellation` rather than `HttpServerException`. Use it to send a specific HTTP response from anywhere inside the handler — the server reads: * `$code` — HTTP status (must be 4xx/5xx, otherwise 500); * `$message` — response body. It is also thrown **internally** when the parser hits a limit after the handler has been dispatched: the server cancels the handler with `HttpException`, and the cancellation travels through the normal Async chain while carrying the exact HTTP status for the peer. **Not final** — subclass it per domain: ```php use TrueAsync\HttpException; class NotFoundException extends HttpException {} class ForbiddenException extends HttpException {} class PayloadTooLargeException extends HttpException {} $server->addHttpHandler(function ($req, $res) { $user = User::find($req->getQueryParam('id')) ?? throw new NotFoundException('user not found', 404); if (!$user->canBeViewedBy(currentUser())) throw new ForbiddenException('access denied', 403); $res->json($user->toArray()); }); ``` ## Bailout firewall Any **other** exception thrown from a handler (E\_ERROR, OOM, uncaught `\Throwable`) does **not crash the server**. The bailout firewall sits at the H1/H2/H3 request entry point: 1. Drains the failing coroutine. 2. Emits 500 to the client (if headers are not on the wire yet). 3. Returns control to the listener — it keeps accepting. This behaviour is uniform across HTTP/1.1, HTTP/2 streams, and HTTP/3 streams. ## See also * [`Async\AsyncCancellation`](/en/docs/reference/exceptions/async-cancellation.html) * [Bailout firewall](/en/architecture/server.html#bailout-firewall) * [`HttpServerConfig::isLocked()`](/en/docs/reference/server/http-server-config.html#islocked) --- --- url: https://true-async.github.io/en/docs/server/quickstart.md description: >- Install TrueAsync Server, a minimal Hello World example, and a sanity check. Linux and Windows. --- # TrueAsync Server quick start (PHP 8.6+, true\_async\_server 0.6+) Five minutes: install the extension, write a minimal handler, and verify the response. The server ships **together with TrueAsync PHP** in every pre-built distribution. If you already have TrueAsync PHP from the installer, a Docker image, or the Windows ZIP, all you need to do is enable the extension in `php.ini` — nothing to build. If you want to build it from source yourself (your own PHP, your own dependency chain), see [Building from source](#building-from-source). ## Docker The fastest way to try it. The pre-built image contains PHP with TrueAsync and the `true_async_server` extension: ```bash docker run --rm -p 8080:8080 \ -v $PWD/hello.php:/app/hello.php \ -w /app \ trueasync/php-true-async:latest \ php hello.php ``` Available tags: | Tag | Description | |-----|-------------| | `trueasync/php-true-async:latest` | Ubuntu 24.04 + CLI + FPM, latest stable release | | `trueasync/php-true-async:latest-alpine` | Alpine 3.20, lightweight | | `trueasync/php-true-async:0.6.7-php8.6` | Specific version | A full list of tags and old releases is on the [downloads page](/en/download.html#docker). ## Linux / macOS — install via script The script downloads the sources, builds TrueAsync PHP along with the server extension, and installs everything into `~/.php-trueasync/bin/`: ```bash # Linux (Ubuntu / Debian) curl -fsSL https://raw.githubusercontent.com/true-async/releases/master/installer/build-linux.sh | bash # macOS (Apple Silicon / Intel; requires Homebrew) curl -fsSL https://raw.githubusercontent.com/true-async/releases/master/installer/build-macos.sh | bash ``` After installation, `php --ri true_async_server` shows the supported protocols and library versions. Parameters (pass via environment variables before `bash`, or in non-interactive mode): ```bash curl -fsSL .../build-linux.sh | NO_INTERACTIVE=true bash ``` Available options are described on the [downloads page](/en/download.html). ## Windows — ZIP The pre-built TrueAsync PHP distribution for Windows x64 includes the server extension. Download the ZIP from [GitHub Releases](https://github.com/true-async/releases/releases) (file name like `php-trueasync-X.Y.Z-php8.6-windows-x64.zip`), extract it, add the directory to `PATH`, and enable the extension in `php.ini`: ```ini extension=true_async_server ``` Verify: ```cmd php --ri true_async_server ``` > HTTP/3 outbound batching uses `UDP_SEGMENT` (Linux GSO); there is no equivalent on Windows. > HTTP/3 throughput on Windows is lower. HTTP/1.1, HTTP/2, and TLS work without any loss. ## Enabling the extension For every install method, add this line to `php.ini`: ```ini extension=true_async_server ``` And verify: ```bash php --ri true_async_server ``` The output lists the supported protocols (HTTP/1.1, HTTP/2, HTTP/3, TLS 1.2/1.3) and the runtime versions of OpenSSL, nghttp2, ngtcp2, nghttp3, and libuv. *** ## Building from source If the pre-built distributions do not fit your needs, you can build the extension manually. ### Requirements | Component | Minimum | Why | Note | |-----------|--------:|-----|------| | PHP | 8.6 | base | build from [TrueAsync php-src](https://github.com/true-async/php-src) | | `ext-async` | latest `main` | event loop, `udp_bind` for HTTP/3 | | | OpenSSL | 3.0 (3.5 for HTTP/3) | TLS, HTTP/3 | HTTP/3 requires the QUIC TLS API from OpenSSL 3.5 | | `libnghttp2` | 1.57 | HTTP/2 | floor for CVE-2023-44487 | | `libngtcp2` + `libngtcp2_crypto_ossl` | 1.22 | HTTP/3 | crypto backend must be `_ossl` | | `libnghttp3` | 1.15 | HTTP/3 | | | `libuv` | via TrueAsync | base | not linked directly into the extension | | `llhttp` | 9.3.0 | HTTP/1.1 | vendored in `deps/llhttp/` | > Distribution packages of OpenSSL/ngtcp2/nghttp3 are usually too old. > The recommended approach is to build OpenSSL 3.5 + ngtcp2 + nghttp3 from source under a single > prefix (`/usr/local` or `/opt/h3`) and point `PKG_CONFIG_PATH` at it during `./configure`. ### Linux #### 1. Dependencies ```bash sudo apt-get install -y \ build-essential autoconf bison re2c pkg-config \ libcmocka-dev # for --enable-tests ``` OpenSSL 3.5, ngtcp2 1.22+, and nghttp3 1.15+ are not in the repositories of most distributions at the time of writing, so build them into `/usr/local`: ```bash # OpenSSL 3.5 with QUIC git clone --branch openssl-3.5 https://github.com/openssl/openssl cd openssl && ./Configure --prefix=/usr/local && make -j$(nproc) && sudo make install sudo ldconfig # ngtcp2 (OpenSSL crypto backend) git clone --recursive https://github.com/ngtcp2/ngtcp2 cd ngtcp2 && autoreconf -i \ && ./configure --prefix=/usr/local --with-openssl --with-libnghttp3 \ PKG_CONFIG_PATH=/usr/local/lib/pkgconfig \ && make -j$(nproc) && sudo make install # nghttp3 git clone --recursive https://github.com/ngtcp2/nghttp3 cd nghttp3 && autoreconf -i \ && ./configure --prefix=/usr/local && make -j$(nproc) && sudo make install ``` #### 2. Building the extension ```bash git clone https://github.com/true-async/server true-async-server cd true-async-server phpize ./configure \ --enable-http-server \ --with-php-config="$(which php-config)" \ PKG_CONFIG_PATH=/usr/local/lib/pkgconfig make -j$(nproc) sudo make install ``` HTTP/2 and HTTP/3 are enabled automatically when the dependencies are present (`libnghttp2 ≥ 1.57` for H2; `libngtcp2 ≥ 1.22`, `libnghttp3 ≥ 1.15`, OpenSSL ≥ 3.5 for H3). To disable them: `--disable-http2`, `--disable-http3`. Additional flags: | Flag | Effect | |------|--------| | `--enable-tests` | build unit tests with libcmocka | | `--enable-coverage` | gcov instrumentation | | `--without-openssl` | no TLS (also disables HTTP/3) | | `--enable-brotli` | enable Brotli (autodetect) | | `--enable-zstd` | enable zstd (autodetect) | After that, enable the extension in `php.ini` and verify it — see [Enabling the extension](#enabling-the-extension) above. ### Windows Build via the standard PHP SDK. Static `.lib` files for OpenSSL 3.5, nghttp2, ngtcp2, and nghttp3 must be available under `deps\` in the PHP SDK tree. ```cmd REM from a Visual Studio x64 Native Tools prompt phpsdk_buildtree phpdev git clone https://github.com/true-async/php-src.git cd php-src git clone https://github.com/true-async/server ext\true_async_server buildconf.bat configure.bat ^ --disable-all ^ --enable-cli ^ --enable-async=shared ^ --enable-http-server=shared ^ --with-openssl=shared nmake ``` The resulting `php_true_async_server.dll` appears in `x64\Release_TS\` (or `Release\` for NTS). Copy it to `ext\` and add `extension=true_async_server` to `php.ini`. ## Minimal server ```php addListener('0.0.0.0', 8080) ); $server->addHttpHandler(function ($request, $response) { $response ->setStatusCode(200) ->setHeader('Content-Type', 'text/plain') ->setBody('Hello, World!'); }); $server->start(); // blocks until stop() ``` Run: ```bash php hello.php ``` Verify: ```bash curl -i http://localhost:8080/ ``` ``` HTTP/1.1 200 OK Content-Type: text/plain Content-Length: 13 Hello, World! ``` ## Next * [Configuration](/en/docs/server/configuration.html): TLS, timeouts, body limits * [Multi-worker](/en/docs/server/workers.html): `setWorkers(N)` and bootloader * [Examples](/en/docs/server/examples.html): JSON API, static, multipart upload, fan-out * [`HttpServer` reference](/en/docs/reference/server/http-server.html) --- --- url: https://true-async.github.io/en/docs/reference/server/http-request.md description: >- TrueAsync\HttpRequest — read-only HTTP request representation: method, URI, headers, body, query, multipart, W3C Trace Context, body streaming. --- # TrueAsync\HttpRequest (PHP 8.6+, true\_async\_server 0.6+) Read-only object passed as the first argument to the handler. Created by the server — not constructed by user code. ```php namespace TrueAsync; final class HttpRequest { // --- general --- public function getMethod(): string; public function getUri(): string; public function getPath(): string; public function getHttpVersion(): string; public function isKeepAlive(): bool; // --- query --- public function getQuery(): array; public function getQueryParam(string $name, mixed $default = null): mixed; // --- headers --- public function hasHeader(string $name): bool; public function getHeader(string $name): ?string; public function getHeaderLine(string $name): string; public function getHeaders(): array; public function getContentType(): ?string; public function getContentLength(): ?int; // --- body --- public function getBody(): string; public function hasBody(): bool; public function awaitBody(): static; public function readBody(int $maxLen = 65536): ?string; // --- multipart / form --- public function getPost(): array; public function getFiles(): array; public function getFile(string $name): ?UploadedFile; // --- W3C Trace Context --- public function getTraceParent(): ?string; public function getTraceState(): ?string; public function getTraceId(): ?string; public function getSpanId(): ?string; public function getTraceFlags(): ?int; } ``` ## General ### getMethod ```php public HttpRequest::getMethod(): string ``` `"GET"`, `"POST"`, `"PUT"`, `"DELETE"`, etc. ### getUri ```php public HttpRequest::getUri(): string ``` The full request URI — path plus query string. ### getPath ```php public HttpRequest::getPath(): string ``` The path without the query string. For example, `/search` from `/search?q=hello`. Uniform across HTTP/1.1, HTTP/2 (`:path` pseudo-header), and HTTP/3. Together with `getQuery()` it uses a single lazy parse — the URI is split into path/query on first access and cached in the request struct. ### getHttpVersion ```php public HttpRequest::getHttpVersion(): string ``` `"1.1"`, `"2"`, `"3"`. ### isKeepAlive ```php public HttpRequest::isKeepAlive(): bool ``` ## Query ### getQuery ```php public HttpRequest::getQuery(): array ``` All query parameters as an associative array — the equivalent of `$_GET`. Supports percent-decoding, `+`-as-space, and PHP array notation (`foo[]`, `foo[bar]`). Parsing is delegated to `php_default_treat_data(PARSE_STRING, ...)` — the same function that populates `$_GET`. ### getQueryParam ```php public HttpRequest::getQueryParam(string $name, mixed $default = null): mixed ``` A single parameter by name, or `$default` (default `null`) when missing. ## Headers ### hasHeader ```php public HttpRequest::hasHeader(string $name): bool ``` Case-insensitive. ### getHeader ```php public HttpRequest::getHeader(string $name): ?string ``` A single value, case-insensitive. `null` when missing. ### getHeaderLine ```php public HttpRequest::getHeaderLine(string $name): string ``` All values, joined with commas. Empty string when missing. ### getHeaders ```php public HttpRequest::getHeaders(): array ``` All headers. Names are **lowercase**. ### getContentType ```php public HttpRequest::getContentType(): ?string ``` The value of `Content-Type`, or `null`. ### getContentLength ```php public HttpRequest::getContentLength(): ?int ``` `Content-Length` or `null` (missing or invalid). ## Body ### getBody ```php public HttpRequest::getBody(): string ``` The request body. Empty string when there is no body. > In streaming-body mode (`HttpServerConfig::setBodyStreamingEnabled(true)`) `getBody()` throws — > read through `readBody()`. ### hasBody ```php public HttpRequest::hasBody(): bool ``` ### awaitBody ```php public HttpRequest::awaitBody(): static ``` Wait for the full body. Since Phase 6 Step 3+, the handler may be invoked **immediately after the parsed headers**, before the body is fully received. `awaitBody()` suspends the coroutine until message-complete. When the body is already fully buffered (the current default), the call returns immediately without suspending. ### readBody ```php public HttpRequest::readBody(int $maxLen = 65536): ?string ``` Pull-based body streaming (issue #26). Returns **one** parser-supplied chunk per call: * an H2 DATA frame (≈ 16 KiB); * an llhttp `on_body` slice (bounded by the H1 read buffer of 8 KiB). Behaviour: * Empty queue → the coroutine parks on a per-request trigger event. * EOF → `null` (idempotent). * Stream error (peer reset, `max_body_size` exceeded) → `\Exception`. * `$maxLen` is reserved for a future coalesce optimisation and is currently ignored. The signature stays binary-compatible with the upcoming polish. Available **only** when `HttpServerConfig::setBodyStreamingEnabled(true)`. See [Streaming](/en/docs/server/streaming.html). ## Multipart / form ### getPost ```php public HttpRequest::getPost(): array ``` POST data from `multipart/form-data` or `application/x-www-form-urlencoded`. Supports PHP-style arrays: `name[]`, `user[name]`, `matrix[0][1]`. ### getFiles ```php public HttpRequest::getFiles(): array ``` All uploaded files. Multiple files with the same name: `['photos' => [UploadedFile, UploadedFile, ...]]`. ### getFile ```php public HttpRequest::getFile(string $name): ?UploadedFile ``` A single file by name. For `photos[]`, returns the first in the array. `null` when missing. See [`UploadedFile`](/en/docs/reference/server/uploaded-file.html). ## W3C Trace Context Requires `HttpServerConfig::setTelemetryEnabled(true)`. ### getTraceParent ```php public HttpRequest::getTraceParent(): ?string ``` Raw `traceparent` as received. `null` when missing / malformed / telemetry is disabled. ### getTraceState ```php public HttpRequest::getTraceState(): ?string ``` Raw `tracestate`. `null` when missing / telemetry is disabled. ### getTraceId ```php public HttpRequest::getTraceId(): ?string ``` The decoded 32-character lower-hex trace id, or `null`. ### getSpanId ```php public HttpRequest::getSpanId(): ?string ``` The decoded 16-character lower-hex parent span id, or `null`. ### getTraceFlags ```php public HttpRequest::getTraceFlags(): ?int ``` The decoded 8-bit flags byte (for example, `0x01` — sampled), or `null`. ## Example ```php $server->addHttpHandler(function (HttpRequest $req, HttpResponse $res) { error_log(sprintf( "[%s] %s %s (HTTP/%s, body=%s, traceid=%s)", $req->getMethod(), $req->getPath(), $req->getQuery() ? json_encode($req->getQuery()) : '-', $req->getHttpVersion(), $req->getContentLength() ?? 'n/a', $req->getTraceId() ?? '-' )); if ($req->getMethod() === 'POST' && $req->getContentType() === 'application/json') { $body = json_decode($req->getBody(), true); // ... } $res->json(['ok' => true]); }); ``` ## See also * [`TrueAsync\HttpResponse`](/en/docs/reference/server/http-response.html) * [`TrueAsync\UploadedFile`](/en/docs/reference/server/uploaded-file.html) * [Streaming](/en/docs/server/streaming.html) --- --- url: https://true-async.github.io/en/docs/reference/server/http-response.md description: >- TrueAsync\HttpResponse — status, headers, body, streaming via send()/sendable(), HTTP/2 trailers, sendFile(), json(), html(), redirect(). --- # TrueAsync\HttpResponse (PHP 8.6+, true\_async\_server 0.6+) Response object with a fluent interface. Passed as the second argument to the handler. Created by the server — not constructed by user code. ```php namespace TrueAsync; final class HttpResponse { // status public function setStatusCode(int $code): static; public function getStatusCode(): int; public function setReasonPhrase(string $phrase): static; public function getReasonPhrase(): string; // headers public function setHeader(string $name, string|array $value): static; public function addHeader(string $name, string|array $value): static; public function hasHeader(string $name): bool; public function getHeader(string $name): ?string; public function getHeaderLine(string $name): string; public function getHeaders(): array; public function resetHeaders(): static; // trailers (HTTP/2) public function setTrailer(string $name, string $value): static; public function setTrailers(array $trailers): static; public function resetTrailers(): static; public function getTrailers(): array; // protocol introspection public function getProtocolName(): string; public function getProtocolVersion(): string; // body public function write(string $data): static; public function send(string $chunk): static; public function sendable(): bool; public function setNoCompression(): static; public function getBody(): string; public function setBody(string $body): static; public function getBodyStream(): mixed; // TODO public function setBodyStream(mixed $stream): static; // TODO // helpers public function json(array|string|object|null|int|float|bool $data, int $status = 200, int $flags = 0): static; public function html(string $html): static; public function redirect(string $url, int $status = 302): static; // send / state public function end(?string $data = null): void; public function sendFile(string $path, ?SendFileOptions $options = null): void; // Server-Sent Events (text/event-stream) public function sseStart(): static; public function sseEvent(?string $data = null, ?string $event = null, ?string $id = null, ?int $retry = null): static; public function sseComment(string $text = ""): static; public function sseRetry(int $milliseconds): static; public function isHeadersSent(): bool; public function isClosed(): bool; } ``` ## Status ### setStatusCode ```php public HttpResponse::setStatusCode(int $code): static ``` HTTP code, 100..599. ### getStatusCode ```php public HttpResponse::getStatusCode(): int ``` ### setReasonPhrase / getReasonPhrase ```php public HttpResponse::setReasonPhrase(string $phrase): static public HttpResponse::getReasonPhrase(): string ``` `"OK"`, `"Not Found"`, etc. ## Headers ### setHeader ```php public HttpResponse::setHeader(string $name, string|array $value): static ``` Set a header, replacing previous values. ### addHeader ```php public HttpResponse::addHeader(string $name, string|array $value): static ``` Append a value to the existing ones (for example, `Set-Cookie`). ### hasHeader / getHeader / getHeaderLine / getHeaders ```php public HttpResponse::hasHeader(string $name): bool public HttpResponse::getHeader(string $name): ?string public HttpResponse::getHeaderLine(string $name): string public HttpResponse::getHeaders(): array ``` Case-insensitive read-back of what the handler set. ### resetHeaders ```php public HttpResponse::resetHeaders(): static ``` Clear all headers. ## Trailers (HTTP/2) A HEADERS frame sent after the body. The canonical consumer is gRPC (`grpc-status`). **On HTTP/1.1 the value is silently ignored** — chunked-encoding trailer emission is out of scope for Step 5b. ### setTrailer ```php public HttpResponse::setTrailer(string $name, string $value): static ``` The name is lowercase (RFC 9113 §8.2.2); uppercase is automatically lowered. ### setTrailers ```php public HttpResponse::setTrailers(array $trailers): static ``` Bulk set. Existing trailers are preserved — call `resetTrailers()` first for a clean slate. ### resetTrailers ```php public HttpResponse::resetTrailers(): static ``` ### getTrailers ```php public HttpResponse::getTrailers(): array ``` ## Protocol ### getProtocolName / getProtocolVersion ```php public HttpResponse::getProtocolName(): string // always "HTTP" public HttpResponse::getProtocolVersion(): string // "1.1", "2", "3" ``` ## Body ### write ```php public HttpResponse::write(string $data): static ``` Append to the internal body buffer. Send happens on `end()` / automatically when the handler returns. ### send ```php public HttpResponse::send(string $chunk): static ``` Send a chunk to the client (streaming). * The **first** `send()` commits status + headers — they can no longer be changed. * Subsequent calls append HTTP/2 DATA frames or HTTP/1 chunked segments. * Blocks the handler coroutine **only** under backpressure (per-stream staging buffer full). Default backpressure threshold: `setStreamWriteBufferBytes()` — 256 KiB. * In the normal case returns immediately. ### sendable ```php public HttpResponse::sendable(): bool ``` Advisory non-blocking check: * `true` — `send()` will accept a chunk without suspending the coroutine. * `false` — `send()` will block on backpressure, or the response is already sealed by `sendFile()` / closed, or the response type is not streaming-capable. `send()` is **always** safe to call — `sendable()` simply gives the handler a chance to do other work instead of blocking on a slow peer. ### setNoCompression ```php public HttpResponse::setNoCompression(): static ``` Disable compression for this response — overrides Accept-Encoding, the MIME whitelist, and the size threshold. Use it for: BREACH-sensitive endpoints (secrets + reflected user input), payloads where `Content-Encoding` is already set, and bodies the server must not re-wrap. Idempotent. ### getBody / setBody ```php public HttpResponse::getBody(): string public HttpResponse::setBody(string $body): static ``` Get/set the current buffer contents. ## Helpers ### json ```php public HttpResponse::json( array|string|object|null|int|float|bool $data, int $status = 200, int $flags = 0 ): static ``` JSON serialisation via `php_json_encode_ex` (the same path used by `json_encode()`): * `array` / `object` / scalar `$data` → encoded. * `string` `$data` → sent **as is** (cached JSON, pre-built bytes). Skip re-encoding. `Content-Type: application/json` is set **only** when the handler did not set one already — chain `setHeader('Content-Type', 'application/problem+json')->json($payload)` for a different media type. `$flags` — a `JSON_*` bitmask. `0` — the server defaults from [`HttpServerConfig::setJsonEncodeFlags()`](/en/docs/reference/server/http-server-config.html#setjsonencodeflags-getjsonencodeflags) (`JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES` out of the box). `JSON_THROW_ON_ERROR` is silently stripped: an encode error produces a `500` JSON error and the exception is not propagated. Handlers should never wrap `json()` in try/catch. ### html ```php public HttpResponse::html(string $html): static ``` Sets `Content-Type: text/html`. ### redirect ```php public HttpResponse::redirect(string $url, int $status = 302): static ``` ## Send ### end ```php public HttpResponse::end(?string $data = null): void ``` Finalise the response and send it to the client. After `end()` no more writes are allowed. ### sendFile ```php public HttpResponse::sendFile(string $path, ?SendFileOptions $options = null): void ``` Handler-driven file delivery. Records the path + options on the response and **returns immediately** — the transfer happens in the dispose phase through the same FSM as `StaticHandler` (MIME, ETag, IMF-date, Range, conditional GET, precompressed sidecars). **After `sendFile()` the response is sealed**: `setHeader` / `setStatus*` / `write` / `send` / `setBody` / `json` / `html` / `redirect` / `end` / a repeat `sendFile()` all throw `HttpServerRuntimeException`. The path is **trusted** (the handler made the access decision). open/fstat errors (`ENOENT`, `EACCES`, oversize, non-regular) produce a 500, because the headers are not on the wire yet. The compression middleware is bypassed for sendFile bodies (it has its own delivery pipeline). > The HTTP/3 path for `sendFile()` is still in progress; for now the H3 dispose hook rejects with > 500\. See [`SendFileOptions`](/en/docs/reference/server/send-file-options.html). ## Server-Sent Events (text/event-stream) (true\_async\_server 0.8+). Guide with examples: [SSE](/en/docs/server/sse.html). ### sseStart ```php public HttpResponse::sseStart(): static ``` Switches the response into SSE mode and locks in the headers: `Content-Type: text/event-stream`, `Cache-Control: no-cache, no-transform`, `X-Accel-Buffering: no`, and marks the response non-compressible. The response enters streaming mode the same way the first `send()` would: the status and headers are committed and can no longer change, but the event payload itself isn't on the wire yet. The call is optional: the first `sseEvent()`/`sseComment()` starts the stream on its own. `sseStart()` by itself does **not** flush the status line and headers: the commit is lazy and happens on the first `sseEvent()`/`sseComment()`/`sseRetry()` (if none is ever called, an empty `200 text/event-stream` is flushed when the response ends). To open the stream right away, for example to unblock the browser's `onopen` before any real event is ready, send an initial `sseComment()`. Throws `HttpServerInvalidArgumentException` if the handler already set a `Content-Type` other than `text/event-stream`, and `HttpServerRuntimeException` if the response is already streaming, closed, or busy with `sendFile()`. ### sseEvent ```php public HttpResponse::sseEvent( ?string $data = null, ?string $event = null, ?string $id = null, ?int $retry = null ): static ``` Formats and sends one SSE event, starting the stream if needed. Multiline `$data` is split on `\n`/`\r\n`/`\r` and sent as multiple `data:` fields (WHATWG §9.2). `$event`, `$id`, and `$retry` are included only when not `null`. The record ends with a blank line so the browser dispatches the event immediately. `$event` and `$id` must not contain `\r`/`\n` (otherwise the parser would read them as a field/record separator), and `$id` must not contain NUL: violations throw `HttpServerInvalidArgumentException`. `$retry` must be non-negative. `$data === ""` is a valid value too, it dispatches an empty `MessageEvent`. All four arguments set to `null` is a no-op; the `EventSource` parser skips an event with neither `data` nor `retry`. ### sseComment ```php public HttpResponse::sseComment(string $text = ""): static ``` Sends a comment line (a record starting with `:`). Browsers ignore comments, but they keep the connection alive through the idle timeouts of intermediate proxies (nginx's `proxy_read_timeout`, 60s by default). The canonical payload is an empty string (`:\n\n` on the wire). `$text` must not contain `\r`/`\n`. Starts the stream if it isn't running yet. ### sseRetry ```php public HttpResponse::sseRetry(int $milliseconds): static ``` Sends a bare `retry:` directive telling the browser how many milliseconds to wait before reconnecting after the stream drops. Sugar for `sseEvent(retry: $milliseconds)` with no payload. Starts the stream if it isn't running yet. ## State ### isHeadersSent ```php public HttpResponse::isHeadersSent(): bool ``` ### isClosed ```php public HttpResponse::isClosed(): bool ``` ## Example ```php use TrueAsync\HttpResponse; use TrueAsync\SendFileOptions; use TrueAsync\SendFileDisposition; $server->addHttpHandler(function ($req, HttpResponse $res) { // SSE if ($req->getPath() === '/events') { foreach (loadEvents() as $event) { $res->sseEvent(json_encode($event)); } $res->end(); return; } // sendFile if ($req->getPath() === '/report.pdf') { $res->sendFile('/var/reports/q1.pdf', new SendFileOptions( disposition: SendFileDisposition::ATTACHMENT, downloadName: 'Q1-Report.pdf', )); return; } // JSON $res->json(['ok' => true]); }); ``` ## See also * [`TrueAsync\HttpRequest`](/en/docs/reference/server/http-request.html) * [`TrueAsync\SendFileOptions`](/en/docs/reference/server/send-file-options.html) * [SSE](/en/docs/server/sse.html) * [Streaming](/en/docs/server/streaming.html) * [Compression](/en/docs/server/compression.html) --- --- url: https://true-async.github.io/en/docs/reference/server/http-server.md description: >- TrueAsync\HttpServer — the main class of the built-in HTTP server. Handler registration, start/stop, telemetry, runtime stats. --- # TrueAsync\HttpServer (PHP 8.6+, true\_async\_server 0.6+) The main class of the built-in server. Receives a config through its constructor, accepts protocol handlers, starts via `start()`, and blocks the thread until `stop()`. ```php namespace TrueAsync; final class HttpServer { public function __construct(HttpServerConfig $config); public function addHttpHandler(callable $handler): static; public function addStaticHandler(StaticHandler $handler): static; public function addWebSocketHandler(callable $handler): static; public function addHttp2Handler(callable $handler): static; // TODO public function addGrpcHandler(callable $handler): static; // TODO public function start(): bool; public function stop(): bool; public function isRunning(): bool; public function getConfig(): HttpServerConfig; public function getHttp3Stats(): array; public function getRuntimeStats(): array; public function getTelemetry(): array; // TODO public function resetTelemetry(): bool; // TODO } ``` ## Methods ### \_\_construct ```php public HttpServer::__construct(HttpServerConfig $config) ``` Creates the server with the given config. **The config is frozen** by this call — any subsequent setter throws `HttpServerRuntimeException`. ### addHttpHandler ```php public HttpServer::addHttpHandler(callable $handler): static ``` Registers a handler for HTTP/1.1 and HTTP/2 requests. Signature: ```php function (HttpRequest $request, HttpResponse $response): void ``` Each request runs in its **own coroutine** inside a [per-request scope](/en/docs/server/workers.html#per-request-scope). The handler returns `void`; the response is sent through `$response`. ### addStaticHandler ```php public HttpServer::addStaticHandler(StaticHandler $handler): static ``` Registers a static mount (issue #13). Requests under `$handler->getUrlPrefix()` are served **entirely in C** — without spawning a coroutine and without entering the PHP VM. Multiple mounts are matched in registration order. After attach, the handler is **locked** — any setter on it throws `HttpServerRuntimeException`. See [`StaticHandler`](/en/docs/reference/server/static-handler.html). ### addWebSocketHandler ```php public HttpServer::addWebSocketHandler(callable $handler): static ``` Registers a handler for full-duplex WebSocket connections (RFC 6455). Upgrade is accepted from HTTP/1.1 and from HTTP/2 (RFC 8441 Extended CONNECT), plus `wss://` over TLS and permessage-deflate (RFC 7692). Each connection is served by its own coroutine. Two signatures are supported; the server checks how many parameters the handler declares: ```php function (WebSocket $ws): void function (WebSocket $ws, HttpRequest $req, WebSocketUpgrade $upgrade): void ``` The two-parameter form (`$ws` only) accepts the upgrade with default settings. The three-parameter form gives access to `WebSocketUpgrade`: subprotocol negotiation and the ability to reject the upgrade before the `101` response goes out. See the [WebSocket guide](/en/docs/server/websocket.html) and the [`WebSocket` class reference](/en/docs/reference/server/websocket.html). ### addHttp2Handler ```php public HttpServer::addHttp2Handler(callable $handler): static ``` 📋 Planned. Today HTTP/2 requests go through `addHttpHandler` (the shared H1/H2 dispatcher). ### addGrpcHandler ```php public HttpServer::addGrpcHandler(callable $handler): static ``` 📋 Planned. Over HTTP/2, unary and streaming RPC. ### start ```php public HttpServer::start(): bool ``` Starts the server and blocks the calling thread until `stop()` or a fatal error. * With `setWorkers(1)` — runs the event loop on the calling thread. * With `setWorkers(N > 1)` — spawns an `Async\ThreadPool` of N workers and `await`s their completion. Returns `true` on a normal shutdown. Throws `HttpServerException` (and its descendants) on start errors (bind failed, missing build dependencies for HTTP/3 when `addHttp3Listener` was used, etc.). ### stop ```php public HttpServer::stop(): bool ``` Graceful shutdown: 1. Stops accepting new connections. 2. Waits for active requests to finish (up to `setShutdownTimeout()`). 3. Closes all connections. Returns `true` on a successful stop. > Cross-thread `stop()` is on the roadmap. Today shutdown is usually initiated through > SIGINT/SIGTERM. ### isRunning ```php public HttpServer::isRunning(): bool ``` ### getConfig ```php public HttpServer::getConfig(): HttpServerConfig ``` Returns the **same** config object that was passed into `__construct`. After the server starts, the config is locked (`isLocked() === true`). ### getHttp3Stats ```php public HttpServer::getHttp3Stats(): array ``` Per-listener observability for HTTP/3. One entry per `addHttp3Listener()` in registration order. Each entry contains: | Key | Value | |-----|-------| | `host` | bound host | | `port` | UDP port | | `datagrams_received` | datagrams received counter | | `bytes_received` | bytes received | | `datagrams_errored` | datagrams that errored | | `last_datagram_size` | size of the last datagram | | `last_peer` | last peer (string) | Returns an empty array when the extension is built **without** `--enable-http3`. ### getRuntimeStats ```php public HttpServer::getRuntimeStats(): array ``` Snapshot of the server's internal allocators. Helps attribute RSS growth to specific subsystems. | Key | Meaning | |-----|---------| | `conn_arena_live` | `http_connection_t` slots currently in use (one per live TCP connection) | | `conn_arena_slots` | total slots across chunks (live + free, never shrinks) | | `conn_arena_chunks` | committed chunks; each one is `CONN_ARENA_CHUNK_SLOTS` (256) structs of ~768 B | | `conn_arena_bytes` | `chunks × 256 × sizeof(http_connection_t)` — virtual commitment | | `body_pool` | per-size-class LIFO of large request bodies (1 MB..128 MB). Each entry: `slot_bytes`, `count`, `bytes` | | `body_pool_total_bytes` | sum of `bytes` across all classes | ### getTelemetry ```php public HttpServer::getTelemetry(): array ``` 📋 Planned. ### resetTelemetry ```php public HttpServer::resetTelemetry(): bool ``` 📋 Planned. ## Example ```php use TrueAsync\HttpServer; use TrueAsync\HttpServerConfig; use TrueAsync\StaticHandler; $server = new HttpServer( (new HttpServerConfig()) ->addListener('0.0.0.0', 8080) ->setWorkers(4) ); $server->addStaticHandler( (new StaticHandler('/assets/', __DIR__ . '/public')) ->enablePrecompressed('br', 'gzip') ); $server->addHttpHandler(function ($req, $res) { $res->json(['ok' => true, 'path' => $req->getPath()]); }); $server->start(); ``` ## See also * [`TrueAsync\HttpServerConfig`](/en/docs/reference/server/http-server-config.html) * [`TrueAsync\HttpRequest`](/en/docs/reference/server/http-request.html) * [`TrueAsync\HttpResponse`](/en/docs/reference/server/http-response.html) * [`TrueAsync\WebSocket`](/en/docs/reference/server/websocket.html) * [Quickstart](/en/docs/server/quickstart.html) --- --- url: https://true-async.github.io/en/docs/reference/server/http-server-config.md description: >- Full reference for HttpServerConfig: listeners, workers, TLS, timeouts, backpressure, drain, compression, HTTP/3 knobs, body streaming, logging. --- # TrueAsync\HttpServerConfig (PHP 8.6+, true\_async\_server 0.6+) Server configuration. All methods are fluent (returning `static`). Once the object is passed into `new HttpServer($config)`, the config is **frozen**: every setter throws `HttpServerRuntimeException`. Check it with `isLocked()`. See also [Configuration](/en/docs/server/configuration.html) — a step-by-step guide. ## Constructor ### \_\_construct ```php public HttpServerConfig::__construct(?string $host = null, int $port = 8080) ``` The optional parameters are a shortcut for a single-listener setup. More commonly the constructor is called with no arguments and `addListener()` is used instead. ## Listeners ### addListener ```php public HttpServerConfig::addListener(string $host, int $port, bool $tls = false): static ``` TCP listener accepting HTTP/1.1 and HTTP/2 (h2c via preface detection on plaintext, h2 via ALPN on TLS). ### addHttp1Listener ```php public HttpServerConfig::addHttp1Listener(string $host, int $port, bool $tls = false): static ``` HTTP/1.1-only TCP listener. A connection arriving with an HTTP/2 preface is handed to llhttp, which emits a compliant 400 Bad Request and closes. ### addHttp2Listener ```php public HttpServerConfig::addHttp2Listener(string $host, int $port, bool $tls = false): static ``` HTTP/2-only listener. * `$tls=false`: h2c (cleartext H2). The listener requires the RFC 7540 §3.5 preface; anything else goes into nghttp2's `BAD_CLIENT_MAGIC` and receives a compliant `GOAWAY(PROTOCOL_ERROR)`. * `$tls=true`: the server advertises only `h2` via ALPN. ### addUnixListener ```php public HttpServerConfig::addUnixListener(string $path): static ``` Unix-socket listener (H1 + H2, h2c style). ### addHttp3Listener ```php public HttpServerConfig::addHttp3Listener(string $host, int $port): static ``` HTTP/3 / QUIC over UDP. TLS 1.3 is mandatory — the server certificate is used; there is no separate `$tls` flag. The extension must be built with `--enable-http3`, otherwise `start()` throws. ### getListeners ```php public HttpServerConfig::getListeners(): array ``` Array of all registered listeners. ## Connection limits ### setBacklog / getBacklog ```php public HttpServerConfig::setBacklog(int $backlog): static public HttpServerConfig::getBacklog(): int ``` Socket backlog. Default 128. ### setWorkers / getWorkers ```php public HttpServerConfig::setWorkers(int $workers): static public HttpServerConfig::getWorkers(): int ``` Built-in worker pool size (issue #11). * `1` (default) — single-threaded. * `> 1` — `start()` spawns an `Async\ThreadPool` of the given size, the config + handler set are replicated via `transfer_obj`, and the parent waits for all workers to finish. Each worker re-binds the listeners; the kernel balances accepts via `SO_REUSEPORT` (Linux/BSD). ### setBootloader / getBootloader ```php public HttpServerConfig::setBootloader(?\Closure $bootloader): static public HttpServerConfig::getBootloader(): ?\Closure ``` Per-worker startup hook. The pool deep-copies the closure once and runs it on every worker before the task loop — the perfect place for autoload, connection-pool warmup, and opcache pre-compile. Applied only when `setWorkers() > 1`. An exception in the bootloader fails the entire pool. Requires TrueAsync ABI v0.15+. ### setMaxConnections / getMaxConnections ```php public HttpServerConfig::setMaxConnections(int $maxConnections): static public HttpServerConfig::getMaxConnections(): int ``` Hard cap on concurrent connections. `0` — no limit. ### setMaxInflightRequests / getMaxInflightRequests ```php public HttpServerConfig::setMaxInflightRequests(int $n): static public HttpServerConfig::getMaxInflightRequests(): int ``` Admission control: when the limit is hit, new requests receive a fast rejection — H1 → 503 + `Retry-After: 1`, H2 → `RST_STREAM REFUSED_STREAM` (retry-safe per RFC 7540 §8.1.4). `0` — disabled (default); if `0` remains at `start()`, the limit is derived as `max_connections × 10`. ## Timeouts | Method | What it times out | |--------|-------------------| | `setReadTimeout(int)` / `getReadTimeout(): int` | request receive | | `setWriteTimeout(int)` / `getWriteTimeout(): int` | response send | | `setKeepAliveTimeout(int)` / `getKeepAliveTimeout(): int` | idle between requests; `0` disables keep-alive | | `setShutdownTimeout(int)` / `getShutdownTimeout(): int` | how long to wait for active requests during graceful shutdown | Values are in seconds. `0` (where applicable) means disabled. ## Backpressure (CoDel) ### setBackpressureTargetMs / getBackpressureTargetMs ```php public HttpServerConfig::setBackpressureTargetMs(int $ms): static public HttpServerConfig::getBackpressureTargetMs(): int ``` CoDel target sojourn. When per-request queue-wait stays above the threshold for 100 ms in a row, the listen socket is paused. Range 0..10\_000, default 5. `0` disables CoDel. Guidance: * fast handlers (<5 ms) — the default of 5 * typical web — 10..20 * slow handlers (database, IO) — 50..100 ## Graceful drain (Step 8) ### setMaxConnectionAgeMs / getMaxConnectionAgeMs ```php public HttpServerConfig::setMaxConnectionAgeMs(int $ms): static public HttpServerConfig::getMaxConnectionAgeMs(): int ``` After `(age ± 10% jitter)` lifetime — H1 emits the next response with `Connection: close`, H2 emits a `GOAWAY`. Equivalent to gRPC `MAX_CONNECTION_AGE`. Default `0` (off); production recommendation is 600\_000 (10 min) behind an L4 LB. Must be `0` or ≥ 1000. ### setMaxConnectionAgeGraceMs / getMaxConnectionAgeGraceMs ```php public HttpServerConfig::setMaxConnectionAgeGraceMs(int $ms): static public HttpServerConfig::getMaxConnectionAgeGraceMs(): int ``` Hard-close after `Connection: close`/`GOAWAY`. `0` — no force-close timer; non-zero ≥ 1000. ### setDrainSpreadMs / getDrainSpreadMs ```php public HttpServerConfig::setDrainSpreadMs(int $ms): static public HttpServerConfig::getDrainSpreadMs(): int ``` Window for evenly spreading per-connection drain on CoDel trip / hard-cap (anti-thundering-herd). Equivalent to HAProxy `close-spread-time`. Default 5000, ≥ 100. ### setDrainCooldownMs / getDrainCooldownMs ```php public HttpServerConfig::setDrainCooldownMs(int $ms): static public HttpServerConfig::getDrainCooldownMs(): int ``` Minimum gap between reactive drain triggers. Triggers inside the cooldown increment a telemetry counter. Default 10\_000, ≥ 1000. ## HTTP/2 streaming ### setStreamWriteBufferBytes / getStreamWriteBufferBytes ```php public HttpServerConfig::setStreamWriteBufferBytes(int $bytes): static public HttpServerConfig::getStreamWriteBufferBytes(): int ``` Per-stream chunk-queue cap that backs `HttpResponse::send()` backpressure. HTTP/2 only; HTTP/1 chunked uses the kernel send buffer. Default 262\_144 (256 KiB). Range 4\_096..67\_108\_864 (64 MiB). Industry baselines: gRPC-Go 64 KiB, Envoy 1 MiB, Node.js 16 KiB. ### setH2StaticBudgetMax / getH2StaticBudgetMax ```php public HttpServerConfig::setH2StaticBudgetMax(int $bytes): static public HttpServerConfig::getH2StaticBudgetMax(): int ``` Per-worker cap for HTTP/2 static-file body buffers (read-ahead chunks + ring queues). `0` — auto (`memory_limit / 8`). Any explicit value is clamped so the static budget does not exceed `memory_limit` minus a small reserve. ## Body limits ### setMaxBodySize / getMaxBodySize ```php public HttpServerConfig::setMaxBodySize(int $bytes): static public HttpServerConfig::getMaxBodySize(): int ``` Maximum request body size (H1 and H2). H1 — 413 + close; H2 — `RST_STREAM(INTERNAL_ERROR)` (the connection stays open for other streams). Default 10\_485\_760 (10 MiB). Range 1\_024..17\_179\_869\_184 (16 GiB). ## WebSocket {#websocket} (true\_async\_server 0.9+). Guide: [WebSocket](/en/docs/server/websocket.html). ### setWsMaxMessageSize / getWsMaxMessageSize ```php public HttpServerConfig::setWsMaxMessageSize(int $bytes): static public HttpServerConfig::getWsMaxMessageSize(): int ``` Maximum reassembled WebSocket message size. A frame set whose combined payload exceeds the limit closes the connection with RFC 6455 §7.4.1 `1009 Message Too Big`. Default 1\_048\_576 (1 MiB). Range 128..268\_435\_456 (256 MiB). ### setWsMaxFrameSize / getWsMaxFrameSize ```php public HttpServerConfig::setWsMaxFrameSize(int $bytes): static public HttpServerConfig::getWsMaxFrameSize(): int ``` Maximum payload for a single frame. Guards against fragment-flood attacks, where the client sends millions of tiny fragments. Default 1\_048\_576 (1 MiB). Same range as `setWsMaxMessageSize`. ### setWsPingIntervalMs / getWsPingIntervalMs ```php public HttpServerConfig::setWsPingIntervalMs(int $ms): static public HttpServerConfig::getWsPingIntervalMs(): int ``` How often the server pings an otherwise idle connection. The peer must reply with PONG within `WsPongTimeoutMs`, or the connection is closed with code `1001 GoingAway`. Default 30\_000 (30 s). `0` disables the automatic ping. ### setWsPongTimeoutMs / getWsPongTimeoutMs ```php public HttpServerConfig::setWsPongTimeoutMs(int $ms): static public HttpServerConfig::getWsPongTimeoutMs(): int ``` The PONG deadline: how long the server waits after a PING before declaring the connection dead. Default 60\_000 (60 s). `0` disables the timeout. ### setWsPermessageDeflate / getWsPermessageDeflate ```php public HttpServerConfig::setWsPermessageDeflate(bool $enabled): static public HttpServerConfig::getWsPermessageDeflate(): bool ``` Enables RFC 7692 permessage-deflate (message-level compression). Off by default: it's an opt-in, because compression costs CPU and widens the decompression-bomb attack surface. Negotiated only when the client offers the extension; the reassembled-message cap is checked both before and after inflate. Requires a build with zlib (HTTP compression). ## HTTP/3 knobs ### setHttp3IdleTimeoutMs / getHttp3IdleTimeoutMs ```php public HttpServerConfig::setHttp3IdleTimeoutMs(int $ms): static public HttpServerConfig::getHttp3IdleTimeoutMs(): int ``` QUIC `max_idle_timeout` (RFC 9000 §10.1). Default 30\_000 (30 s). Range 0..UINT32\_MAX (~49 days); `0` advertises "no idle timeout". The legacy env `PHP_HTTP3_IDLE_TIMEOUT_MS` still works as an ops escape hatch. ### setHttp3StreamWindowBytes / getHttp3StreamWindowBytes ```php public HttpServerConfig::setHttp3StreamWindowBytes(int $bytes): static public HttpServerConfig::getHttp3StreamWindowBytes(): int ``` Per-stream QUIC flow-control window. Sets all three: `initial_max_stream_data_bidi_local`, `_bidi_remote`, and `_uni` (h2o `http3-input-window-size` style). The connection-level `initial_max_data` is derived as `window × max_concurrent_streams` (the nginx pattern). Default 262\_144 (256 KiB). Range 1\_024..1\_073\_741\_824 (1 GiB). ### setHttp3MaxConcurrentStreams / getHttp3MaxConcurrentStreams ```php public HttpServerConfig::setHttp3MaxConcurrentStreams(int $n): static public HttpServerConfig::getHttp3MaxConcurrentStreams(): int ``` QUIC `initial_max_streams_bidi`. Equivalent to nginx `http3_max_concurrent_streams`. Default 100, range 1..1\_000\_000. ### setHttp3PeerConnectionBudget / getHttp3PeerConnectionBudget ```php public HttpServerConfig::setHttp3PeerConnectionBudget(int $n): static public HttpServerConfig::getHttp3PeerConnectionBudget(): int ``` Per-source-IP cap on concurrent QUIC connections. Mitigates handshake slow-loris and amplification. Default 16, range 1..4\_096. The legacy env `PHP_HTTP3_PEER_BUDGET` still overrides at listener spawn. ### setHttp3AltSvcEnabled / isHttp3AltSvcEnabled ```php public HttpServerConfig::setHttp3AltSvcEnabled(bool $enable): static public HttpServerConfig::isHttp3AltSvcEnabled(): bool ``` RFC 7838 `Alt-Svc: h3=":"; ma=86400` on H1/H2 responses when an H3 listener is up. Default `true`. Disable for a phased H3 rollout. The legacy env `PHP_HTTP3_DISABLE_ALT_SVC` is honoured at `start()`. ## Compression ### setCompressionEnabled / isCompressionEnabled ```php public HttpServerConfig::setCompressionEnabled(bool $enable): static public HttpServerConfig::isCompressionEnabled(): bool ``` Master switch. Default `true`. If the extension was built without `--enable-http-compression`, only `false` is accepted — `true` throws. ### setCompressionLevel / getCompressionLevel ```php public HttpServerConfig::setCompressionLevel(int $level): static public HttpServerConfig::getCompressionLevel(): int ``` gzip level. zlib semantics: 1 — fastest/weakest, 9 — slowest/strongest. Default 6. ### setBrotliLevel / getBrotliLevel ```php public HttpServerConfig::setBrotliLevel(int $level): static public HttpServerConfig::getBrotliLevel(): int ``` Brotli quality. Range 0..11. Default 4 (production-typical; quality 11 ≈ 50× slower than quality 4 with marginal ratio gain). Inert if the extension was built without `--enable-brotli` — the response pipeline never picks Brotli without `HAVE_HTTP_BROTLI`, no matter what is passed here. ### setZstdLevel / getZstdLevel ```php public HttpServerConfig::setZstdLevel(int $level): static public HttpServerConfig::getZstdLevel(): int ``` zstd level. Range 1..22. Default 3 — the zstd team's production default (better ratio than gzip-6 with higher throughput). ### setCompressionMinSize / getCompressionMinSize ```php public HttpServerConfig::setCompressionMinSize(int $bytes): static public HttpServerConfig::getCompressionMinSize(): int ``` Body-size threshold — below it the response is not compressed. Default 1024 (1 KiB). Range 0..16 MiB. ### setCompressionMimeTypes / getCompressionMimeTypes ```php public HttpServerConfig::setCompressionMimeTypes(array $types): static public HttpServerConfig::getCompressionMimeTypes(): array ``` MIME whitelist for compression. **Fully replaces** the default (nginx `gzip_types` semantics). Entries are normalised at setter time: parameters (`; charset=...`) are stripped, whitespace is trimmed, everything is lowercased. Default: `["application/javascript", "application/json", "application/xml", "image/svg+xml", "text/css", "text/html", "text/javascript", "text/plain", "text/xml"]`. ### setRequestMaxDecompressedSize / getRequestMaxDecompressedSize ```php public HttpServerConfig::setRequestMaxDecompressedSize(int $bytes): static public HttpServerConfig::getRequestMaxDecompressedSize(): int ``` Anti-zip-bomb cap on decompressed bodies (`Content-Encoding: gzip/br/zstd` inbound). On overflow — 413\. `0` disables the cap (explicitly — there is no implicit-unlimited). Default 10\_485\_760 (10 MiB). ### getSupportedEncodings (static) ```php public static HttpServerConfig::getSupportedEncodings(): array ``` List of codecs compiled into this build, in server preference order. Always contains `"identity"`; `"gzip"` appears on successful `--enable-http-compression`; `"br"` / `"zstd"` appear when the corresponding library is present at configure time. ## Buffers ### setWriteBufferSize / getWriteBufferSize ```php public HttpServerConfig::setWriteBufferSize(int $size): static public HttpServerConfig::getWriteBufferSize(): int ``` Write-buffer size. ## Protocol options | Method | Purpose | |--------|---------| | `enableHttp2(bool)` / `isHttp2Enabled(): bool` | toggle HTTP/2 (TODO) | | `enableWebSocket(bool)` / `isWebSocketEnabled(): bool` | toggle WS (TODO) | | `enableProtocolDetection(bool)` / `isProtocolDetectionEnabled(): bool` | auto-detect protocol on the listener | > `enableWebSocket()` is a separate, not-yet-implemented toggle. WebSocket itself already works > fully through [`addWebSocketHandler()`](/en/docs/reference/server/http-server.html#addwebsockethandler) > and the settings in the [WebSocket section](#websocket) above; the two flags are unrelated. ## TLS | Method | Purpose | |--------|---------| | `enableTls(bool)` / `isTlsEnabled(): bool` | toggle TLS on the default listener | | `setCertificate(string)` / `getCertificate(): ?string` | path to the PEM certificate | | `setPrivateKey(string)` / `getPrivateKey(): ?string` | path to the PEM key | ## Body handling ### setAutoAwaitBody / isAutoAwaitBodyEnabled ```php public HttpServerConfig::setAutoAwaitBody(bool $enable): static public HttpServerConfig::isAutoAwaitBodyEnabled(): bool ``` When `true`, non-multipart requests wait for the full body before calling the handler. Multipart is always streamed. Default `true`. ### setBodyStreamingEnabled / isBodyStreamingEnabled ```php public HttpServerConfig::setBodyStreamingEnabled(bool $enabled): static public HttpServerConfig::isBodyStreamingEnabled(): bool ``` Stream request bodies into a per-request queue (issue #26) instead of accumulating them in `req->body`. Handlers must read through [`HttpRequest::readBody()`](/en/docs/reference/server/http-request.html#readbody); `getBody()` throws. ## JSON ### setJsonEncodeFlags / getJsonEncodeFlags ```php public HttpServerConfig::setJsonEncodeFlags(int $flags): static public HttpServerConfig::getJsonEncodeFlags(): int ``` Default `JSON_*` flags for [`HttpResponse::json()`](/en/docs/reference/server/http-response.html#json) when the per-call `$flags=0` (or omitted). Default: `JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES`. `JSON_THROW_ON_ERROR` is silently stripped — an encode error produces a 500 JSON error and the exception is not propagated. ## Logging / telemetry ### setLogSeverity / getLogSeverity ```php public HttpServerConfig::setLogSeverity(\TrueAsync\LogSeverity $level): static public HttpServerConfig::getLogSeverity(): \TrueAsync\LogSeverity ``` Logger severity. Default `OFF`. Severity is fixed at start — runtime changes are not supported (single-threaded lock-free model). See [`LogSeverity`](/en/docs/reference/server/log-severity.html). ### setLogStream / getLogStream ```php public HttpServerConfig::setLogStream(mixed $stream): static public HttpServerConfig::getLogStream(): mixed ``` Logger sink. Any `php_stream` (file, `php://stderr`, `php://memory`, user wrapper). The logger stays disabled until **both** are set: a non-OFF severity AND a stream. ### setTelemetryEnabled / isTelemetryEnabled ```php public HttpServerConfig::setTelemetryEnabled(bool $enabled): static public HttpServerConfig::isTelemetryEnabled(): bool ``` W3C Trace Context parsing — incoming `traceparent` / `tracestate` are attached to the request and exposed via [`HttpRequest::getTraceParent/getTraceId/...`](/en/docs/reference/server/http-request.html). ## State ### isLocked ```php public HttpServerConfig::isLocked(): bool ``` `true` after the config is passed into `new HttpServer()`. A locked config rejects every setter with `HttpServerRuntimeException`. ## See also * [Configuration](/en/docs/server/configuration.html) — step-by-step guide * [`TrueAsync\HttpServer`](/en/docs/reference/server/http-server.html) * [`TrueAsync\WebSocket`](/en/docs/reference/server/websocket.html) * [`TrueAsync\LogSeverity`](/en/docs/reference/server/log-severity.html) --- --- url: https://true-async.github.io/en/docs/reference/server/log-severity.md description: >- TrueAsync\LogSeverity — enum of server logging levels. Backed by OpenTelemetry SeverityNumber. --- # TrueAsync\LogSeverity (PHP 8.6+, true\_async\_server 0.6+) Enum of server logging levels. The backing values correspond to [OpenTelemetry Logs Data Model SeverityNumber](https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber) (1..24); a stable subset is exposed. ```php namespace TrueAsync; enum LogSeverity: int { case OFF = 0; case DEBUG = 5; case INFO = 9; case WARN = 13; case ERROR = 17; } ``` | Case | OTel value | Contents | |------|-----------:|----------| | `OFF` | 0 | nothing | | `DEBUG` | 5 | tracing, H3 packet trace, etc. | | `INFO` | 9 | server lifecycle (start/stop), bind retries | | `WARN` | 13 | TLS handshake fail, peer reset, absorbed exceptions | | `ERROR` | 17 | listener bind failed, hard protocol errors | > **`TRACE` and `FATAL` are intentionally absent.** `TRACE` is unused; `FATAL` is delivered via > `zend_error_noreturn(E_ERROR)`, which already terminates the process. ## Usage The logger is disabled by default. To enable it, you need **both**: 1. A severity other than `OFF`. 2. A sink stream via [`HttpServerConfig::setLogStream()`](/en/docs/reference/server/http-server-config.html#setlogstream-getlogstream). ```php use TrueAsync\HttpServerConfig; use TrueAsync\LogSeverity; $config ->setLogSeverity(LogSeverity::INFO) ->setLogStream(STDERR); ``` Severity is **fixed at start** — runtime changes are not supported (single-threaded lock-free model). ### What you hear at each level ```php // production $config->setLogSeverity(LogSeverity::WARN); // staging / debug instability $config->setLogSeverity(LogSeverity::INFO); // deep debug $config->setLogSeverity(LogSeverity::DEBUG); ``` `DEBUG` also enables verbose tracing of HTTP/3 packets and other internal flows — useful for diagnostics, but it adds CPU/IO overhead. ## See also * [`HttpServerConfig::setLogSeverity()`](/en/docs/reference/server/http-server-config.html#setlogseverity-getlogseverity) * [`HttpServerConfig::setLogStream()`](/en/docs/reference/server/http-server-config.html#setlogstream-getlogstream) * [Configuration — logging](/en/docs/server/configuration.html#logging) --- --- url: https://true-async.github.io/en/docs/reference/server/send-file-options.md description: >- TrueAsync\SendFileOptions — value object with HttpResponse::sendFile() settings. Disposition, downloadName, ETag, Range, precompressed sidecars, deleteAfterSend. --- # TrueAsync\SendFileOptions (PHP 8.6+, true\_async\_server 0.4+) Value object with settings for [`HttpResponse::sendFile()`](/en/docs/reference/server/http-response.html#sendfile). Immutable (`final readonly class`), created via named arguments in the constructor. ```php namespace TrueAsync; final readonly class SendFileOptions { public function __construct( public ?string $contentType = null, public SendFileDisposition $disposition = SendFileDisposition::INLINE, public ?string $downloadName = null, public ?string $cacheControl = null, public bool $etag = true, public bool $lastModified = true, public bool $acceptRanges = true, public bool $precompressed = true, public bool $conditional = true, public bool $deleteAfterSend = false, public ?int $status = null, ) {} } ``` ## Fields | Field | Type | Default | What it does | |-------|------|---------|--------------| | `contentType` | `?string` | `null` | Override MIME. `null` — derived automatically from the extension. | | `disposition` | `SendFileDisposition` | `INLINE` | `INLINE` — display in the browser; `ATTACHMENT` — download. Affects `Content-Disposition`. | | `downloadName` | `?string` | `null` | File name for `Content-Disposition: attachment; filename=...`. Meaningful with `ATTACHMENT`. | | `cacheControl` | `?string` | `null` | Literally placed into `Cache-Control`. `null` — no header. | | `etag` | `bool` | `true` | Emit weak `ETag` from `(mtime_ns, size, ino)`. | | `lastModified` | `bool` | `true` | Emit `Last-Modified` (IMF-fixdate). | | `acceptRanges` | `bool` | `true` | Support `Range:` (HTTP/1.1 partial content). | | `precompressed` | `bool` | `true` | Look for sidecars (`*.br`, `*.gz`, `*.zst`) when `Accept-Encoding` permits. | | `conditional` | `bool` | `true` | `If-Modified-Since` / `If-None-Match` → 304. | | `deleteAfterSend` | `bool` | `false` | `unlink($path)` after a successful send. Useful for one-shot downloads from temp files. | | `status` | `?int` | `null` | Override the HTTP status. Example: 200 on a specific staged response, even when the default would be 304. | ## SendFileDisposition ```php namespace TrueAsync; enum SendFileDisposition: string { case INLINE = 'inline'; case ATTACHMENT = 'attachment'; } ``` ## Examples ### Inline PDF ```php use TrueAsync\SendFileOptions; $res->sendFile('/var/storage/q1-report.pdf', new SendFileOptions( contentType: 'application/pdf', cacheControl: 'private, max-age=300', )); ``` ### Download with a user-friendly name ```php use TrueAsync\SendFileOptions; use TrueAsync\SendFileDisposition; $res->sendFile('/var/storage/abc123.bin', new SendFileOptions( disposition: SendFileDisposition::ATTACHMENT, downloadName: 'Q1 Report 2026.pdf', contentType: 'application/pdf', )); ``` ### One-shot temp file ```php $tmp = '/tmp/export-' . bin2hex(random_bytes(8)) . '.csv'; generateExport($tmp); $res->sendFile($tmp, new SendFileOptions( disposition: SendFileDisposition::ATTACHMENT, downloadName: 'export.csv', contentType: 'text/csv; charset=utf-8', deleteAfterSend: true, )); ``` ### Without conditional GET (always 200) ```php $res->sendFile('/var/storage/live.mp4', new SendFileOptions( conditional: false, acceptRanges: true, cacheControl: 'no-store', )); ``` ### Without precompressed sidecars (compress on the fly in the engine) ```php $res->sendFile('/var/storage/big.json', new SendFileOptions( precompressed: false, )); ``` ## See also * [`HttpResponse::sendFile()`](/en/docs/reference/server/http-response.html#sendfile) * [`TrueAsync\StaticHandler`](/en/docs/reference/server/static-handler.html) * [Static files and sendFile](/en/docs/server/static-files.html) --- --- url: https://true-async.github.io/en/docs/reference/server/static-handler.md description: >- TrueAsync\StaticHandler — prefix-mount static delivery without a PHP handler. Precompressed sidecars, ETag, Range, dotfile/symlink policies, open-file cache. --- # TrueAsync\StaticHandler (PHP 8.6+, true\_async\_server 0.6+) Built-in static file handler (issue #13). One instance = one prefix mount. Attached to the server via [`HttpServer::addStaticHandler()`](/en/docs/reference/server/http-server.html#addstatichandler). Entirely in C: requests do not spawn coroutines and do not enter the PHP VM — files are served through libuv async fs ops straight into the response stream. ```php namespace TrueAsync; final class StaticHandler { public function __construct(string $urlPrefix, string $rootDirectory); // index / fallthrough public function setIndexFiles(string ...$files): static; public function disableIndex(): static; public function setOnMissing(StaticOnMissing $mode): static; // precompressed sidecars public function enablePrecompressed(string ...$encodings): static; public function disablePrecompressed(): static; // security public function setDotfilePolicy(StaticDotfiles $policy): static; public function setSymlinkPolicy(StaticSymlinks $policy): static; public function hide(string ...$globs): static; // cache / headers public function setEtagEnabled(bool $enabled): static; public function setCacheControl(string $value): static; public function setOpenFileCache(int $maxEntries, int $ttlSeconds = 60): static; public function disableOpenFileCache(): static; public function setHeader(string $name, string $value): static; // directory listing public function setBrowseEnabled(bool $enabled): static; // MIME public function setMimeType(string $extension, string $contentType): static; // introspection public function getUrlPrefix(): string; public function getRootDirectory(): string; public function isLocked(): bool; } ``` ## Constructor ### \_\_construct ```php public StaticHandler::__construct(string $urlPrefix, string $rootDirectory) ``` | Parameter | Requirements | |-----------|--------------| | `$urlPrefix` | URL prefix. Must start and end with `/`. Example: `"/static/"`. | | `$rootDirectory` | Absolute path to a directory on disk; canonicalised at attach time. | ## Index / fallthrough ### setIndexFiles ```php public StaticHandler::setIndexFiles(string ...$files): static ``` File names served when a directory URL is requested. Default `["index.html"]`. An empty list disables index lookup. ### disableIndex ```php public StaticHandler::disableIndex(): static ``` Equivalent to `setIndexFiles()` with no arguments. ### setOnMissing ```php public StaticHandler::setOnMissing(StaticOnMissing $mode): static ``` What to do when the requested path does not resolve to a regular file inside the root: | Value | Behaviour | |-------|-----------| | `StaticOnMissing::NOT_FOUND` (default) | 404 in C, the request never reaches the PHP VM | | `StaticOnMissing::NEXT` | Control is handed back to the dispatcher and a normal handler coroutine is spawned — the request goes to [`addHttpHandler()`](/en/docs/reference/server/http-server.html#addhttphandler) | ## Precompressed sidecars ### enablePrecompressed ```php public StaticHandler::enablePrecompressed(string ...$encodings): static ``` Enables serving precompressed sidecars (`main.css.br`, `main.css.gz`, `main.css.zst`) when the client allows them through `Accept-Encoding`. Arguments are content-coding names: `"br"`, `"gzip"`, `"zstd"`. Unknown names — `InvalidArgumentException` from the setter. ### disablePrecompressed ```php public StaticHandler::disablePrecompressed(): static ``` ## Security ### setDotfilePolicy ```php public StaticHandler::setDotfilePolicy(StaticDotfiles $policy): static ``` A "dotfile" is any path segment that starts with `.`, including `..` (which is always rejected by the traversal guard regardless of policy). | | Behaviour | |---|-----------| | `StaticDotfiles::DENY` (default) | 404 on any path containing a dotfile component | | `StaticDotfiles::ALLOW` | dotfiles are served as regular files | | `StaticDotfiles::IGNORE` | as if the file did not exist (passthrough governed by `StaticOnMissing`) | ### setSymlinkPolicy ```php public StaticHandler::setSymlinkPolicy(StaticSymlinks $policy): static ``` | | Behaviour | |---|-----------| | `StaticSymlinks::REJECT` (default) | 404 on any symlink in the path. `O_NOFOLLOW` + per-segment `lstat` — a symlink is never traversed | | `StaticSymlinks::FOLLOW` | symlinks are followed; the post-`realpath()` target must stay inside the root | | `StaticSymlinks::OWNER_MATCH` | follow only when the symlink and its target share the same uid | ### hide ```php public StaticHandler::hide(string ...$globs): static ``` Glob patterns: matching paths return 404 regardless of whether they exist. Comparison is **relative to the root** and uses `/` as the separator. ## Cache / headers ### setEtagEnabled ```php public StaticHandler::setEtagEnabled(bool $enabled): static ``` Toggle weak ETag (default `true`). When enabled, every 200 carries an `ETag: W/"…"` derived from `(mtime_ns, size, ino)`; `If-None-Match` / `If-Modified-Since` produce 304. ### setCacheControl ```php public StaticHandler::setCacheControl(string $value): static ``` Literal `Cache-Control`. An empty string suppresses emission. ### setOpenFileCache ```php public StaticHandler::setOpenFileCache(int $maxEntries, int $ttlSeconds = 60): static ``` nginx-style open-file cache: caches resolved path, fstat metadata, MIME, ETag, and Last-Modified for the last N requests. Within `ttlSeconds`, repeat requests hit the cache and skip realpath/stat/MIME walks. Disabled by default. It pays off on cold dentry caches / large docroots / network filesystems. On a warm-dentry local disk, syscalls already cost sub-microseconds — the HashTable-lookup overhead eats the win. `$maxEntries == 0` — disable. ### disableOpenFileCache ```php public StaticHandler::disableOpenFileCache(): static ``` Sugar for `setOpenFileCache(0)`. ### setHeader ```php public StaticHandler::setHeader(string $name, string $value): static ``` Fixed header, evaluated once at attach time. Emitted on every 200 and 304 (except `Content-*` headers per RFC 9110 §15.4.5). ## Directory listing ### setBrowseEnabled ```php public StaticHandler::setBrowseEnabled(bool $enabled): static ``` Toggle HTML listing when a directory is requested without an index. Default `false`. > Reserved for PR #6 — currently a no-op; accepted by the setter without effect. ## MIME ### setMimeType ```php public StaticHandler::setMimeType(string $extension, string $contentType): static ``` Override `Content-Type` for files with the given extension. Extension — lowercased, no leading dot. ## Introspection ### getUrlPrefix / getRootDirectory ```php public StaticHandler::getUrlPrefix(): string public StaticHandler::getRootDirectory(): string ``` ### isLocked ```php public StaticHandler::isLocked(): bool ``` `true` after the handler is attached to the server via `addStaticHandler()`. A locked handler rejects every setter with a runtime exception. ## Enums See the individual pages: * [`StaticOnMissing`](/en/docs/reference/server/static-on-missing.html) * [`StaticDotfiles`](/en/docs/reference/server/static-dotfiles.html) * [`StaticSymlinks`](/en/docs/reference/server/static-symlinks.html) (All three are `enum: int` under the `TrueAsync` namespace.) ## Example ```php use TrueAsync\StaticHandler; use TrueAsync\StaticOnMissing; use TrueAsync\StaticDotfiles; $static = (new StaticHandler('/static/', '/var/www/public')) ->setIndexFiles('index.html', 'index.htm') ->enablePrecompressed('br', 'gzip') ->setOnMissing(StaticOnMissing::NEXT) ->setDotfilePolicy(StaticDotfiles::DENY) ->setCacheControl('public, max-age=31536000, immutable') ->setEtagEnabled(true) ->setOpenFileCache(maxEntries: 1024, ttlSeconds: 60) ->setHeader('Strict-Transport-Security', 'max-age=63072000') ->hide('*.bak', '*.tmp', 'private/**'); $server->addStaticHandler($static); ``` ## See also * [Static files and sendFile](/en/docs/server/static-files.html) * [`HttpServer::addStaticHandler()`](/en/docs/reference/server/http-server.html#addstatichandler) * [`HttpResponse::sendFile()`](/en/docs/reference/server/http-response.html#sendfile) --- --- url: https://true-async.github.io/en/docs/reference/server/uploaded-file.md description: >- TrueAsync\UploadedFile — PSR-7-compatible class for uploaded multipart files. moveTo(), getStream(), getSize(), getClientFilename(). --- # TrueAsync\UploadedFile (PHP 8.6+, true\_async\_server 0.1+) Representation of a single uploaded file from `multipart/form-data`. PSR-7 compatible. Obtained through [`HttpRequest::getFile()`](/en/docs/reference/server/http-request.html#getfile) / [`HttpRequest::getFiles()`](/en/docs/reference/server/http-request.html#getfiles). ```php namespace TrueAsync; final class UploadedFile { public function getStream(): mixed; public function moveTo(string $targetPath, int $mode = 0644): void; public function getSize(): ?int; public function getError(): int; public function getClientFilename(): ?string; public function getClientMediaType(): ?string; public function getClientCharset(): ?string; public function isReady(): bool; public function isValid(): bool; } ``` ## Methods ### getStream ```php public UploadedFile::getStream(): mixed ``` Stream resource for reading the file. You can read a **partially uploaded** file. | | | |---|---| | returns | `resource` or `null` if unavailable | | throws | `\RuntimeException` if the file was already moved via `moveTo()` | ### moveTo ```php public UploadedFile::moveTo(string $targetPath, int $mode = 0644): void ``` Moves the uploaded file. * Supports absolute and relative paths. * Creates parent directories automatically if they do not exist. * Cross-filesystem: automatic fallback to `copy() + unlink()`. | | | |---|---| | throws | `\RuntimeException` if already moved or on write error | ### getSize ```php public UploadedFile::getSize(): ?int ``` File size in bytes. `null` if unknown (for example, while streaming before the tail arrives). ### getError ```php public UploadedFile::getError(): int ``` Error code in PHP `UPLOAD_ERR_*` format. ### getClientFilename ```php public UploadedFile::getClientFilename(): ?string ``` Original name from the client, **as received** (with no modifications). 4 KB limit. > **Do not trust it.** The name may contain any bytes (including path separators). Sanitise it > before use or generate the name server-side. ### getClientMediaType ```php public UploadedFile::getClientMediaType(): ?string ``` MIME type from the client (taken verbatim from the browser's part Content-Type). Not verified by the server. ### getClientCharset ```php public UploadedFile::getClientCharset(): ?string ``` Charset from the part's `Content-Type` header (if specified). ### isReady ```php public UploadedFile::isReady(): bool ``` `true` after the file has fully uploaded and the temporary descriptor has been closed. ### isValid ```php public UploadedFile::isValid(): bool ``` Equivalent to `getError() === UPLOAD_ERR_OK`. ## Example ```php $server->addHttpHandler(function ($req, $res) { if ($req->getMethod() !== 'POST') { $res->setStatusCode(405); return; } $avatar = $req->getFile('avatar'); if ($avatar === null) { $res->setStatusCode(400)->json(['error' => 'no avatar field']); return; } if (!$avatar->isValid()) { $res->setStatusCode(400)->json([ 'error' => 'upload error', 'code' => $avatar->getError(), ]); return; } if ($avatar->getSize() > 5 * 1024 * 1024) { $res->setStatusCode(413)->json(['error' => 'too big']); return; } // Verify that the client sent an image (only a first-order trust signal) $declared = $avatar->getClientMediaType() ?? ''; if (!str_starts_with($declared, 'image/')) { $res->setStatusCode(415)->json(['error' => 'not an image']); return; } // Generate the name server-side; don't trust the client $name = bin2hex(random_bytes(16)) . '.bin'; $avatar->moveTo("/var/storage/avatars/$name", 0644); $res->json([ 'saved' => $name, 'original_name' => $avatar->getClientFilename(), 'declared_mime' => $declared, 'size' => $avatar->getSize(), ]); }); ``` ## Multiple files in one field HTML form: ```html ``` Reading: ```php $files = $req->getFiles(); // $files['photos'] === [UploadedFile, UploadedFile, ...] foreach ($files['photos'] as $file) { if (!$file->isValid()) continue; $file->moveTo('/var/storage/' . bin2hex(random_bytes(8))); } ``` `getFile('photos')` in this case returns the **first** file in the array — enough for a doc-first file; use `getFiles()` for all. ## See also * [`HttpRequest::getFile()`](/en/docs/reference/server/http-request.html#getfile) * [`HttpRequest::getFiles()`](/en/docs/reference/server/http-request.html#getfiles) * [Multipart upload example](/en/docs/server/examples.html#multipart-upload-with-file-move) --- --- url: https://true-async.github.io/en/docs/reference/server/websocket.md description: >- TrueAsync\WebSocket, WebSocketMessage, WebSocketUpgrade, WebSocketCloseCode, and the WebSocket exception hierarchy. --- # TrueAsync\WebSocket (PHP 8.6+, true\_async\_server 0.9+) The classes behind full-duplex connections over RFC 6455. Guide with examples: [WebSocket](/en/docs/server/websocket.html). ## TrueAsync\WebSocket One WebSocket connection. Created by the server right after the upgrade handshake commits and passed as the first argument to the handler registered through [`HttpServer::addWebSocketHandler()`](/en/docs/reference/server/http-server.html#addwebsockethandler). ```php namespace TrueAsync; final class WebSocket implements \Iterator { public function recv(): ?WebSocketMessage; public function send(string $text): void; public function sendBinary(string $data): void; public function trySend(string $text): bool; public function trySendBinary(string $data): bool; public function ping(string $payload = ''): void; public function close(WebSocketCloseCode|int $code = WebSocketCloseCode::NORMAL, string $reason = ''): void; public function isClosed(): bool; public function getSubprotocol(): ?string; public function getRemoteAddress(): string; // Iterator public function current(): ?WebSocketMessage; public function key(): int; public function next(): void; public function rewind(): void; public function valid(): bool; } ``` Instances are constructed only by the server; `new WebSocket` is not available to user code. ### Lifecycle The connection is bound to the handler coroutine. When the handler returns control for any reason, including `return` from a `recv()` loop on `null`, the server closes the connection with code `1000 Normal`. An explicit `close()` before `return` is only needed for a non-default code or reason text. ### Concurrency model * `send()`, `sendBinary()`, and `ping()` are safe to call from any coroutine on the same thread. Producers atomically enqueue serialized frames; a single cooperative flusher writes them to the socket one at a time, so frames from different callers never interleave. * `recv()` is single-reader: a second concurrent `recv()` call throws `WebSocketConcurrentReadException`, because the connection is one byte stream and there is no defined semantics for multiple readers. * `close()` is idempotent and can be called from any coroutine. ### recv ```php public WebSocket::recv(): ?WebSocketMessage ``` Receives the next text or binary message. Suspends the calling coroutine until a complete message arrives or the connection closes. Returns a [`WebSocketMessage`](#websocketmessage) or `null` when the client closed cleanly: a normal CLOSE code (`1000`/`1001`/`1005`) or a plain disconnect with no CLOSE frame. Typical loop: `while (($m = $ws->recv()) !== null) { ... }`. The method throws: * `WebSocketClosedException` on a protocol error or an explicit error close code; `$closeCode`/`$closeReason` carry the RFC 6455 code and reason. * `WebSocketConcurrentReadException` if another coroutine is already waiting inside `recv()` on this connection. ### send ```php public WebSocket::send(string $text): void ``` Sends a text frame. `$text` **must** be valid UTF-8: invalid data is rejected up front so the receiver never sees a frame that violates RFC 6455 §5.6. Returns control right away in the common case, while the send buffer isn't full. Suspends the calling coroutine once the buffer fills up, and resumes once the client has read enough to make room again. If the suspension outlasts `write_timeout_ms`, the method throws `WebSocketBackpressureException`, and the handler can then drop the message, close the connection, or retry. The method also throws `WebSocketClosedException` if the connection is already closed. ### sendBinary ```php public WebSocket::sendBinary(string $data): void ``` Sends a binary frame. Binary payloads have no UTF-8 constraint. Backpressure behavior is identical to `send()`. ### trySend ```php public WebSocket::trySend(string $text): bool ``` Non-blocking send. Queues a text frame and returns `true` when the send buffer isn't full; returns `false` without queueing anything when the buffer is full, so the caller can drop the message, slow down, or close the connection. Unlike `send()`, `trySend()` never suspends the calling coroutine, which makes it the right tool for a broadcast loop where one slow client must not stall delivery to the others. The buffer's size is set by [`HttpServerConfig::setStreamWriteBufferBytes()`](/en/docs/reference/server/http-server-config.html#setstreamwritebufferbytes) (`0` disables the limit: `trySend()` then always queues the frame and returns `true`). The function returns `true` if the message was accepted into the queue, and `false` if the send buffer is full and the client isn't keeping up. Throws `WebSocketClosedException` if the connection is already closed. ### trySendBinary ```php public WebSocket::trySendBinary(string $data): bool ``` Non-blocking binary send. Behaves the same as `trySend()`. ### ping ```php public WebSocket::ping(string $payload = ''): void ``` Sends a PING frame. Per RFC 6455 §5.5.2 the peer is required to reply with PONG. Application code rarely needs to call this by hand: the server's keepalive timer (`HttpServerConfig::setWsPingIntervalMs()`) sends pings automatically when configured. `$payload` accepts up to 125 bytes (RFC 6455 §5.5). ### close ```php public WebSocket::close(WebSocketCloseCode|int $code = WebSocketCloseCode::NORMAL, string $reason = ''): void ``` Starts the close handshake and tears the connection down. Idempotent: repeated calls are no-ops. * `$code` is a `WebSocketCloseCode` value, or a raw integer in `4000..4999` (reserved for application-specific codes, RFC 6455 §7.4.2). * `$reason` is UTF-8 text, up to 123 bytes. ### isClosed ```php public WebSocket::isClosed(): bool ``` `true` after `close()` has been called, or after the client's CLOSE frame has been processed. ### getSubprotocol ```php public WebSocket::getSubprotocol(): ?string ``` The subprotocol negotiated during the upgrade, or `null` if none was selected. ### getRemoteAddress ```php public WebSocket::getRemoteAddress(): string ``` The peer address in `host:port` form (IPv4) or `[host]:port` (IPv6) for TCP connections. An empty string for connections over a Unix socket. ### Iterator ```php public WebSocket::current(): ?WebSocketMessage public WebSocket::key(): int public WebSocket::next(): void public WebSocket::rewind(): void public WebSocket::valid(): bool ``` Lets you write `foreach ($ws as $msg)` instead of a manual `recv()` loop. On each step the loop pulls the next message; a graceful close simply ends the `foreach`, and a close with an error throws `WebSocketClosedException` straight out of the loop. ## TrueAsync\WebSocketMessage {#websocketmessage} ```php namespace TrueAsync; final class WebSocketMessage { public readonly string $data; public readonly bool $binary; } ``` One fully reassembled message, as delivered by `WebSocket::recv()`. Text messages have already been validated as UTF-8, so you can use `$data` as-is without checking it again. * **`$data`** — the message payload. For text messages this is a valid UTF-8 string. * **`$binary`** — `true` if the message was sent as a binary frame, `false` for a text frame. Instances are constructed only by the server. You get them through `WebSocket::recv()`; there is no way to construct `new WebSocketMessage` yourself. ## TrueAsync\WebSocketUpgrade ```php namespace TrueAsync; final class WebSocketUpgrade { public function reject(int $status, string $reason = ''): void; public function setSubprotocol(string $name): void; public function getOfferedSubprotocols(): array; public function getOfferedExtensions(): array; } ``` The handle on an in-progress upgrade negotiation. Exists from the moment the handler is invoked until either `reject()` is called or the handler returns successfully (in which case the server sends `101` with whatever subprotocol was chosen through `setSubprotocol()`). Available only to handlers registered with three parameters: ```php $server->addWebSocketHandler(function (WebSocket $ws, HttpRequest $req, WebSocketUpgrade $u) { // ... }); ``` The server checks how many parameters the handler declares; a two-parameter handler skips this object entirely and the upgrade is accepted with default settings. Once the handshake commits, any call on this object throws: `Sec-WebSocket-Protocol` is already on the wire and the subprotocol can no longer change. ### reject ```php public WebSocketUpgrade::reject(int $status, string $reason = ''): void ``` Rejects the upgrade with the given HTTP status. The `101` response is never sent; the client gets the chosen status instead, and the connection closes. After `reject()` the handler should return right away: no further I/O is allowed. * `$status` — the HTTP status code (must be 4xx or 5xx). * `$reason` — an optional response body. ### setSubprotocol ```php public WebSocketUpgrade::setSubprotocol(string $name): void ``` Picks a subprotocol from the list the client offered. The chosen value is echoed back in the `Sec-WebSocket-Protocol` response header. Must be called before the handler returns and before `reject()`. The server does not verify that the chosen value was actually in `getOfferedSubprotocols()`; that's on the handler. ### getOfferedSubprotocols ```php public WebSocketUpgrade::getOfferedSubprotocols(): array ``` Returns the subprotocols (`string[]`) the client sent in the `Sec-WebSocket-Protocol` header, in the client's preferred order. An empty array if the client didn't offer any. ### getOfferedExtensions ```php public WebSocketUpgrade::getOfferedExtensions(): array ``` Returns the extensions (`string[]`) from the `Sec-WebSocket-Extensions` header, in the client's preferred order. permessage-deflate (RFC 7692, message compression) is negotiated by the server itself through `HttpServerConfig::setWsPermessageDeflate()`; the rest of the offered values are informational only. An empty array if the client didn't offer any. ## TrueAsync\WebSocketCloseCode ```php namespace TrueAsync; enum WebSocketCloseCode: int { case NORMAL = 1000; case GOING_AWAY = 1001; case PROTOCOL_ERROR = 1002; case UNSUPPORTED_DATA = 1003; case NO_STATUS = 1005; // RESERVED case ABNORMAL_CLOSURE = 1006; // RESERVED case INVALID_FRAME_PAYLOAD = 1007; case POLICY_VIOLATION = 1008; case MESSAGE_TOO_BIG = 1009; case MANDATORY_EXTENSION = 1010; case INTERNAL_SERVER_ERROR = 1011; case TLS_HANDSHAKE = 1015; // RESERVED } ``` The RFC 6455 §7.4.1 close code registry. Application-specific codes (`4000..4999`, RFC 6455 §7.4.2) stay available too: `WebSocket::close()` accepts a raw `int` alongside this enum. ## Exceptions ``` \Exception └── TrueAsync\HttpServerException └── TrueAsync\WebSocketException ├── WebSocketClosedException // final ├── WebSocketBackpressureException // final └── WebSocketConcurrentReadException // final ``` ### TrueAsync\WebSocketException ```php class WebSocketException extends HttpServerException {} ``` The base exception for all WebSocket errors. Extends the project-wide `HttpServerException`, so existing catch-all handlers keep working. ### TrueAsync\WebSocketClosedException ```php final class WebSocketClosedException extends WebSocketException { public readonly int $closeCode; public readonly string $closeReason; } ``` The connection was closed for a reason other than a normal handshake initiated by the client: a protocol error, or an explicit error code from the client. `$closeCode` carries the RFC 6455 close code (or `1006 Abnormal Closure` if no CLOSE frame arrived at all, for example on a network drop). `$closeReason` carries the UTF-8 reason text from the client's CLOSE frame, or an empty string if none was given. A clean close by the client (code `1000`) does not throw: `WebSocket::recv()` simply returns `null` in that case. ### TrueAsync\WebSocketBackpressureException ```php final class WebSocketBackpressureException extends WebSocketException {} ``` Thrown from `send()`/`sendBinary()` when the send buffer stays full longer than `write_timeout_ms`. This is the application's signal that the client is reading too slowly: close the connection, or drop the message and continue. ### TrueAsync\WebSocketConcurrentReadException ```php final class WebSocketConcurrentReadException extends WebSocketException {} ``` A programmer error: a second coroutine called `recv()` while another was already waiting inside `recv()` on the same `WebSocket`. A single connection can only be read from one place at a time; if you need to distribute messages to several handlers, build one `recv()` loop and dispatch the messages yourself from there. ## See also * [Guide: WebSocket](/en/docs/server/websocket.html) * [`HttpServer::addWebSocketHandler()`](/en/docs/reference/server/http-server.html#addwebsockethandler) * [`HttpServerConfig`: WebSocket options](/en/docs/reference/server/http-server-config.html#websocket) * [TrueAsync Server exceptions](/en/docs/reference/server/exceptions.html) --- --- url: https://true-async.github.io/en/tutorial.md description: >- Step-by-step TrueAsync tutorials — from your first coroutine to pools, threads, and Context. --- # Tutorials A step-by-step introduction to TrueAsync: from your first coroutine to structured concurrency, resource pools, and real parallelism. The tutorials build on each other, so we recommend going through them in order, starting with the first one. --- --- url: https://true-async.github.io/en/docs/server/websocket.md description: >- addWebSocketHandler(): full-duplex connections over RFC 6455, cross-worker pub/sub topics, backpressure, keepalive, subprotocol negotiation, permessage-deflate. --- # WebSocket (PHP 8.6+, true\_async\_server 0.9+) `HttpServer::addWebSocketHandler()` registers a handler for full-duplex connections over RFC 6455. A connection starts as a plain HTTP request, and then the client asks the server to switch it to a different protocol on that same TCP connection: that's what an Upgrade is. The server replies with status `101 Switching Protocols`, and from that point on the same connection carries WebSocket, not HTTP. Supported: * Upgrade from HTTP/1.1 (the classic `Connection: Upgrade` header). * Upgrade from HTTP/2 (RFC 8441 Extended CONNECT). * `wss://` (WebSocket over TLS). * permessage-deflate (RFC 7692), message-level compression. * [Pub/sub topics](#topics-publishsubscribe-across-every-worker) that reach every worker of the process, so a chat does not need a single-worker server or an external broker. > The implementation is verified against the Autobahn|Testsuite conformance suite and passes > all 246 tests in the `behavior` category. ```php use TrueAsync\HttpServer; use TrueAsync\HttpServerConfig; use TrueAsync\WebSocket; $server = new HttpServer( (new HttpServerConfig()) ->addListener('0.0.0.0', 8080) ); $server->addWebSocketHandler(function (WebSocket $ws) { foreach ($ws as $msg) { $ws->send('echo: ' . $msg->data); } }); // Required: the server refuses to start without an HTTP handler, and this is // what answers the requests that are not upgrades. $server->addHttpHandler(function ($req, $res) { $res->setStatusCode(404)->end(); }); $server->start(); ``` Registering the handler is what turns WebSocket on — there is no separate switch to flip, exactly like HTTP/2 and `addHttp2Handler()`. > `HttpServerConfig::enableWebSocket()` is a legacy toggle, not that switch. Passing it > `true` throws `HttpServerRuntimeException` pointing you at `addWebSocketHandler()` — > register the handler instead. Each connection is served by its own coroutine, the same per-request model as HTTP. A handler that throws does not take the worker down with it: the exception is logged, and the peer is told in-protocol — an HTTP status if the throw beat the upgrade, a `CLOSE 1011` once the session was live. The handler is always called with three arguments, and PHP drops the ones you did not declare — so `function (WebSocket $ws)`, `function (WebSocket $ws, HttpRequest $req)` and the three-parameter form are all valid. Declare only what you use. ## Lifecycle A connection stays open until the handler coroutine returns. If the handler simply finishes (for example, the `recv()`/`foreach` loop got `null` at the end), the server closes the connection with code `1000 Normal` automatically. An explicit `close()` before `return` is only needed when you want a different code or your own reason text. ## Receiving messages: `recv()` and `foreach` ```php public WebSocket::recv(): ?WebSocketMessage ``` Suspends the coroutine until the next message arrives or the connection closes. Returns a [`WebSocketMessage`](/en/docs/reference/server/websocket.html#websocketmessage) or `null` when the client closed the connection cleanly (a normal close code, or a disconnect with no explicit CLOSE frame): ```php while (($msg = $ws->recv()) !== null) { handle($msg->data, $msg->binary); } ``` `WebSocket` implements `\Iterator`, so the same loop can be written more concisely as `foreach ($ws as $msg) { ... }`. A clean close simply ends the `foreach`; a close with an error throws `WebSocketClosedException` straight out of the loop. Read messages from one place only: if you call `recv()` from two coroutines in parallel on the same connection, the second call throws `WebSocketConcurrentReadException`. If you need to distribute messages to several handlers, keep one `recv()` loop and dispatch from it yourself. ## Sending messages: `send()`, `trySend()` `send()` and `sendBinary()` are safe to call from any coroutine, including several at once: the server makes sure data from different calls never gets mixed up on the wire. ```php $ws->send('text frame'); // text MUST be valid UTF-8 $ws->sendBinary($binaryData); // binary data has no encoding constraint ``` Usually these functions return right away. If the client is reading slowly and the send buffer fills up, the coroutine suspends and resumes once the client drains some of the buffer. If the wait drags on longer than `write_timeout_ms`, a `WebSocketBackpressureException` is thrown, and the handler decides what to do: drop the message, close the connection, or retry. For broadcasting a message to many clients, where one slow client should not hold up the others, there are non-blocking variants: ```php if (!$ws->trySend($text)) { // this client's buffer is full, the message was NOT sent, the client is falling behind } ``` `trySend()`/`trySendBinary()` never suspend the coroutine: they return `true` right away if the message was accepted, and `false` if the buffer is full (in which case the message is simply not sent). The buffer's size is set by [`HttpServerConfig::setStreamWriteBufferBytes()`](/en/docs/reference/server/http-server-config.html#setstreamwritebufferbytes) (`0` disables the limit: `trySend()` always sends and returns `true`). ## Topics: publish/subscribe across every worker A worker is a thread with its own PHP context. So the obvious way to build a chat — keep an array of connections and loop over it — can only ever reach the peers of *one* worker, which is why such a chat had to run on `setWorkers(1)`. Topics fix that. They live in the server, not in your handler: each worker indexes the connections it owns, and a `publish()` is handed to every worker, which then delivers to its own sockets. No Redis, no message broker, no single-worker server. ```php $server->addWebSocketHandler(function (WebSocket $ws, HttpRequest $req) { $room = ltrim($req->getPath(), '/') ?: 'lobby'; $ws->subscribe("chat/$room"); foreach ($ws as $msg) { $ws->publish("chat/$room", $msg->data); // reaches subscribers on ALL workers } }); ``` A topic is addressed by **name, at the call site**. There is no topic object to obtain, hold, or pass into a handler. ### Filters follow MQTT Levels are separated by `/`, `+` matches exactly one level, and a trailing `#` matches the rest: | Filter | Receives | |--------|----------| | `chat/general` | exactly that topic | | `chat/+/typing` | `chat/general/typing`, `chat/random/typing` — one level, any value | | `user/42/#` | `user/42`, `user/42/presence`, `user/42/dm/7` — the whole subtree | Wildcards belong to *subscriptions*. A **publish topic must be concrete**: a message fanned out to a pattern has no well-defined destination, so `publish('chat/+/typing', …)` throws `WebSocketException`. Filters may be up to 128 levels deep. ### The API ```php $ws->subscribe('chat/+/typing'); // idempotent $ws->unsubscribe('chat/+/typing'); // idempotent $ws->getTopics(); // string[] — this connection's filters $ws->publish('chat/general', $text); // text, to every worker $ws->publishBinary('chat/general', $bytes); // binary counterpart $ws->subscriberCount('chat/general'); // across all workers, wildcards included ``` `publish()` **never suspends**. A peer whose outbound queue is backed up drops the message rather than stalling delivery to the rest of the topic — the same semantics as `trySend()`. When you need a delivery guarantee, `send()` to the one connection instead. A subscriber matched by several of its own filters still receives exactly one copy. `$excludeSelf` defaults to `true` — the "everyone but the sender" case a chat wants: ```php $ws->publish('chat/general', $msg->data); // sender does not get it back $ws->publish('chat/general', $msg->data, excludeSelf: false); // sender gets it too ``` The return value is the number of subscribers served **on the calling worker only**. Delivery to the other workers is asynchronous and cannot be counted at the call site, so this is a local number, not a process-wide one. `subscriberCount()` is the process-wide one — but since each worker answers with its own count and the answers are summed, it is a snapshot rather than a live counter, and a worker that does not answer in time is left out. A closing connection unsubscribes from everything by itself. ### Limits Both are off by default, which is what every self-hosted broker ships (EMQX `max_subscriptions` / `messages_rate`, NATS `max_subs`): only the application knows how many topics it needs. ```php $config ->setWsMaxSubscriptions(32) // distinct filters one connection may hold ->setWsPublishRateLimit(50, burst: 100); ``` Set `setWsMaxSubscriptions()` whenever client input reaches `subscribe()` — say `$ws->subscribe($msg->data)` — so a peer cannot grow the worker's topic tree without end. Over the cap, `subscribe()` throws `WebSocketException` and the connection stays up. `setWsPublishRateLimit()` is a per-connection token bucket. `publish()` is the one WebSocket call an unprivileged peer can turn into work on *every* worker in the process — `send()` and `trySend()` only ever touch its own socket. Unmetered, one client looping on a relayed message fills every worker's inbox, and the drops that follow take out *other* topics' traffic too. Over the rate, `publish()` throws `WebSocketBackpressureException` and the connection stays up: the sender is told, rather than the message vanishing into a full mailbox where nobody can see it. `$burst` is the bucket depth in messages — how far a handler may run ahead of the sustained rate. `0` means one second's worth. ```php try { $ws->publish("chat/$room", $msg->data); } catch (WebSocketBackpressureException) { $ws->send('you are sending too fast'); } catch (WebSocketException $e) { $ws->send('bad topic: ' . $e->getMessage()); } ``` ### What it costs Each worker summarises its subscriptions in a counting Bloom filter of topic prefixes, and a publisher skips the workers that provably hold no subscriber instead of waking all of them. A publish to a topic nobody in the process listens to costs zero cross-worker wake-ups. `HttpServer::getRuntimeStats()` reports the outcome — `ws_topic_posted`, `ws_topic_skipped` (the filter earning its keep) and `ws_topic_dropped` (a worker's mailbox was full: that one is data loss). Topics work on every WebSocket transport, not just plaintext HTTP/1 — over TLS, over HTTP/2 Extended CONNECT, and with permessage-deflate, where one `publish()` serves a compressed peer and a plain one side by side, each with the framing it negotiated. ## The client's address ```php $ws->getRemoteAddress(); // "203.0.113.7" or "2001:db8::1" — bare IP, no port $ws->getRemotePort(); // 54321 ``` `getRemoteAddress()` returns the **bare IP**: no port, and no brackets around an IPv6 literal — the same shape as `$_SERVER['REMOTE_ADDR']`, so it feeds straight into `filter_var(…, FILTER_VALIDATE_IP)`, an ACL, or a rate limiter. Both return `null` on a Unix-socket listener, which has no IP peer. This is the peer of the TCP connection. It is **not** derived from `X-Forwarded-For` — behind a proxy, parse that header yourself, and only when you trust the proxy that set it. > **Breaking change.** `getRemoteAddress()` used to return `"host:port"` (and `""` when > there was no IP peer). It now returns the bare IP, and `null`. Use `getRemotePort()` > for the port. ## Closing a connection: `close()`, `isClosed()` ```php $ws->close(WebSocketCloseCode::NORMAL, 'bye'); ``` Starts closing the connection. Safe to call more than once: later calls are no-ops. The close code is a [`WebSocketCloseCode`](/en/docs/reference/server/websocket.html#websocketclosecode) value or an integer in the `4000..4999` range (reserved for application-specific codes). `$reason` takes UTF-8 text, up to 123 bytes. `isClosed()` returns `true` after `close()`, or after the client sends its own close signal. ## Ping and keepalive ```php $ws->ping('optional payload'); // up to 125 bytes, RFC 6455 §5.5 ``` Application code rarely needs to call this by hand: the server's keepalive timer (`HttpServerConfig::setWsPingIntervalMs()`) sends PINGs automatically. If the client doesn't reply in time (`setWsPongTimeoutMs()`), the server closes the connection on its own. See [Configuration](/en/docs/server/configuration.html#websocket) for the details. ## Subprotocol negotiation and rejection: `WebSocketUpgrade` By default the handler only receives `WebSocket $ws`. To decide for yourself whether to accept the connection and which subprotocol to pick, register the handler with three parameters: the server detects the parameter count and, in that case, passes a third object, `WebSocketUpgrade`: ```php use TrueAsync\WebSocket; use TrueAsync\HttpRequest; use TrueAsync\WebSocketUpgrade; $server->addWebSocketHandler(function (WebSocket $ws, HttpRequest $req, WebSocketUpgrade $u) { $offered = $u->getOfferedSubprotocols(); // from the Sec-WebSocket-Protocol header if (!in_array('chat.v2', $offered, true)) { $u->reject(400, 'unsupported subprotocol'); return; } $u->setSubprotocol('chat.v2'); // must be called before return or reject() foreach ($ws as $msg) { // ... } }); ``` `WebSocketUpgrade` lives from the moment the handler is called until `reject()` or a successful `return` (at which point the server finishes the handshake with the chosen subprotocol). After that, any call on this object throws: the reply is already on the wire and the subprotocol can no longer change. `getOfferedExtensions()` returns the list of extensions the client offered. permessage-deflate (RFC 7692, message compression) is negotiated by the server itself through `HttpServerConfig::setWsPermessageDeflate()`; the rest of the offered values are informational only. ## Close codes and exceptions `WebSocketCloseCode` is an enum with the standard RFC 6455 close codes (`NORMAL`, `GOING_AWAY`, `PROTOCOL_ERROR`, `MESSAGE_TOO_BIG`, and others). The exception hierarchy: ``` \Exception └── TrueAsync\HttpServerException └── TrueAsync\WebSocketException // also: bad topic filter, subscription cap ├── WebSocketClosedException // closeCode / closeReason ├── WebSocketBackpressureException // slow reader — or publish() over its rate limit └── WebSocketConcurrentReadException // second recv() in parallel ``` A clean close by the client shows up as `null` from `recv()`, not as an exception. An exception is only thrown on a protocol error or a close with an explicit error code; `$closeCode`/ `$closeReason` carry the reason. See the [reference](/en/docs/reference/server/websocket.html) for details. ## Configuration | Method | Default | Purpose | |--------|---------|---------| | `setWsMaxMessageSize($bytes)` | 1 MiB | max reassembled message size, otherwise `1009` | | `setWsMaxFrameSize($bytes)` | 1 MiB | max size of a single frame, guards against a flood of tiny fragments | | `setWsPingIntervalMs($ms)` | 30000 | how often the server pings an idle connection, `0` disables it | | `setWsPongTimeoutMs($ms)` | 60000 | how long to wait for PONG before closing (`1001`) | | `setWsPermessageDeflate($bool)` | `false` | RFC 7692, opt-in because of its CPU cost | | `setWsMaxSubscriptions($count)` | `0` (no limit) | distinct topic filters one connection may hold | | `setWsPublishRateLimit($perSecond, $burst)` | `0` (off) | per-connection token bucket over `publish()` | See [Configuration](/en/docs/server/configuration.html#websocket) for more detail. ## See also * [`TrueAsync\WebSocket` and related classes](/en/docs/reference/server/websocket.html): the full reference * [`HttpServer::addWebSocketHandler()`](/en/docs/reference/server/http-server.html#addwebsockethandler) * [Configuration: WebSocket](/en/docs/server/configuration.html#websocket) --- --- url: https://true-async.github.io/en/tutors-server/07-websocket.md description: >- WebSocket: the recv loop, a chat room, send from other coroutines, and trySend against slow clients. --- # WebSocket In a support chat, both sides write. The user asks a question, the operator answers, and neither waits for the other: messages go both ways whenever they're written. Can we assemble such a chat from tools we already know? Downward, from the server to the browser, SSE will carry it, we've been able to do that since yesterday's chapter. And upward? Upward we'd have to send a separate POST for each of the user's lines. A new request, headers, a response, a disconnect. And so on for every "thanks, that helped." It would work, but not particularly well. For a two-way conversation there's a separate protocol, WebSocket. It's built surprisingly modestly. The client sends an ordinary HTTP request with an `Upgrade` header: a proposal to switch to another protocol. The server agrees: `101 Switching Protocols`. That's it, HTTP is over. Over the same TCP connection, messages now travel in both directions, without requests and without responses. ```php use TrueAsync\WebSocket; $server->addWebSocketHandler(function (WebSocket $ws) { foreach ($ws as $msg) { $ws->send('echo: ' . $msg->data); } }); ``` The server takes the handshake entirely upon itself: the handler is called once the connection is already switched, and it receives a ready-made object. The model is familiar: one connection, one coroutine. `WebSocket` implements `Iterator`, and it's the iterator that puts the coroutine to sleep until the next message. The client left, the loop ended, the server closed the connection itself with code `1000 Normal`. And ordinary HTTP handlers keep working alongside, on the same port. ## A Chat Room Echo is a warm-up. In a real chat, one participant's message must reach all the others. Pause for a second on what that means technically: the coroutine serving one connection must write into others. Sounds like a source of problems? Look: ```php use TrueAsync\HttpRequest; /** @var SplObjectStorage $room */ $room = new SplObjectStorage(); $server->addWebSocketHandler(function (WebSocket $ws, HttpRequest $req) use ($room) { $name = $req->getQueryParam('name', 'guest'); $room->attach($ws, $name); $ws->send("Welcome, people in the room: {$room->count()}"); try { foreach ($ws as $msg) { foreach ($room as $peer) { if ($peer !== $ws) { $peer->trySend("$name: {$msg->data}"); } } } } finally { $room->detach($ws); } }); ``` Thirty lines, and inside them three threads from the first series come together at once. Don't rush past, each one deserves a pause. The first: the room is just an object. An ordinary `SplObjectStorage`, shared across all coroutines, without a single lock. The chapter on channels promised that within one thread this is allowed, and here it is in action. The second: `finally`. A participant may leave gracefully, may vanish along with the Wi-Fi, or the coroutine may be cancelled by the server itself on shutdown. Three scenarios, one `finally`, and a ghost is guaranteed not to linger in the room. Cooperative cancellation as it is. The third: writing into someone else's connection is allowed from any coroutine. `send()` and `trySend()` are safe for this; the server itself makes sure that frames from different senders don't get mixed on the wire. Reading, however, is only from one. A second concurrent `recv()` on the same connection gets an exception, and rightly so: a byte stream has no meaningful semantics for two readers. ## A Slow Client Won't Stop the Room Did you notice that the broadcast uses `trySend`, not `send`? That's not a typo, it's a decision, and it's worth talking through. What does `send()` do on a full buffer? Right, backpressure: it puts the coroutine to sleep until the client clears the backlog. For a personal reply, that's just what you want. Now picture it in a broadcast loop. One participant drove into a tunnel with their mobile internet, their buffer is full, and... the whole room waits. A hundred people get no messages because one has poor reception. `trySend()` never waits. Either the message is accepted into the buffer and `true`, or the buffer is full and `false`, the message dropped. The chat deliberately sacrifices the laggard's messages so as not to punish everyone. Cruel? For a chat, no: a missed line in a room is no tragedy. If losing is not allowed in your task, use `send()` and put up with the pauses, or build a queue per client. Both strategies are honest, the choice is yours. As a last resort there's a safeguard too: if `send()` has been hanging in wait longer than the write timeout, it throws `WebSocketBackpressureException`. This is about a client that holds the connection open but doesn't read at all. ## Rooms, Without the Bookkeeping The `SplObjectStorage` worked, but count what it cost us: a shared object, a broadcast loop, and a `finally` to sweep out the ghosts. All so that a line typed by one person reaches the others. It's such a common wish that the server grants it directly. A connection **subscribes** to a name; a message **published** to that name reaches everyone subscribed. The same chat, without the bookkeeping: ```php $server->addWebSocketHandler(function (WebSocket $ws, HttpRequest $req) { $room = $req->getQueryParam('room', 'lobby'); $name = $req->getQueryParam('name', 'guest'); $ws->subscribe("chat/$room"); foreach ($ws as $msg) { $ws->publish("chat/$room", "$name: {$msg->data}"); } }); ``` The storage is gone, and with it the loop and the `finally`. `subscribe()` puts this connection into the room, `publish()` sends a line to everyone in it, and a connection that closes leaves its rooms on its own. `publish()` keeps the same bargain `trySend()` just made: a peer whose buffer is backed up drops the line rather than stalling the room, and it never blocks the sender. The name is not just a label, it's an MQTT-style filter. Levels are separated by `/`, `+` matches one level and `#` the rest. Subscribe to `chat/+/typing` and you hear the typing signal from every room at once; `subscriberCount("chat/general")` tells you how many are listening. And one more thing, quietly important, that we'll cash in next chapter: a topic isn't tied to one connection or even one array in memory. Hold on to that. ## Who Let Them Into the Chat? Right now anyone can enter the room. If you want to check at the door, declare a third parameter on the handler, and the server will pass the handshake object: ```php use TrueAsync\WebSocketUpgrade; $server->addWebSocketHandler( function (WebSocket $ws, HttpRequest $req, WebSocketUpgrade $upgrade) use ($room) { if (authenticate($req) === null) { $upgrade->reject(401, 'auth required'); return; } // ... the room ... } ); ``` `reject()` answers with an ordinary HTTP error instead of switching the protocol. Through the same object a subprotocol is negotiated too, if the client offers any. The chat is ready: the room in memory, instant broadcast, laggards slow no one down. Remember the formula it all rests on: "shared state in the process's memory." Remember it well. In the next chapter we'll turn on several workers to occupy all the machine's cores, and that innocent formula will be the first to blow up. --- --- url: https://true-async.github.io/en/tutors-laravel/04-unsafe-patterns.md description: >- Mutable static properties, once() on a singleton, and Number::useLocale(): the usual ways state leaks between requests, and how to catch them with static analysis. --- # What You Can't Do Inside a Coroutine The earlier chapters fixed specific state leaks: `auth`/`session` through context, the transaction counter through a trait. `laravel-spawn` closed those holes for you. But the framework is large, third-party packages are even more numerous, and tomorrow someone on your team will write their own service. What you need is a rule you can apply on sight, to tell code that's safe for concurrent coroutines apart from code that will one day hand one user someone else's data. ## One Rule: Don't Write To static After Startup Every example below is a special case of the same mistake: something writes a value into memory shared by every coroutine in the process, and that value belongs to one specific request. **A mutable static property on a service.** ```php // Dangerous class PriceCalculator { private static array $cache = []; public function forProduct(int $id): float { return self::$cache[$id] ??= $this->computeExpensive($id); } } ``` It looks like a harmless memoization. In reality it's an array shared across every coroutine. If `computeExpensive()` depends on anything that changes between requests, a user's currency, a regional markup, the first request decides the cache's fate for every request that follows. The fix is simple: an instance property instead of `static`, with the service resolved per request (the "approach one" from the first chapter). **What if the service is your own, and you deliberately need per-request state?** You don't need to wait for `laravel-spawn` to provide a `ScopedService` and a proxy for it, it's the same trick that solved the transaction counter problem in the second chapter, just one level up: not `coroutine_context()` for a single coroutine, but `request_context()`, shared across a request's whole coroutine tree. ```php class PriceCalculator // stays a singleton { private const CTX_CACHE = 'price.cache'; public function forProduct(int $id): float { $cache = request_context()->find(self::CTX_CACHE) ?? []; return $cache[$id] ??= $this->computeExpensive($id, $cache); } } ``` The difference from the previous example looks small but is fundamental: the array no longer lives on a class-wide `static` property shared by the whole process, it lives in the scope context of one specific request. Two concurrent requests call the very same `PriceCalculator` instance, yet each gets its own `$cache`, because `request_context()` resolves to a different scope for each of them. A detailed look at `coroutine_context()` and `request_context()` is in the [second chapter](/en/tutors-laravel/02-pool-transactions.html). **`once()` on a singleton.** ```php // Dangerous class CurrentUserService // registered as a singleton { public function get() { return once(fn() => Auth::user()); // caches the FIRST user, forever } } ``` `once()` caches the closure's result in a `WeakMap` keyed by the object the call belongs to. For a singleton that object is one for the whole process, so the cache is one too. The first request computes the user, every following request gets the same one. On per-request objects (controllers, `Eloquent` models) `once()` is perfectly safe, because there the object itself is fresh on every request. **A global mutation like `Number::useLocale()`.** ```php // Dangerous Number::useLocale('de'); $price = Number::format(1234.5); // what if the coroutine sleeps before this line? // Safe $price = Number::format(1234.5, locale: 'de'); ``` `useLocale()` changes a static variable on the `Number` class. Between the call to `useLocale()` and `format()`, an `await`, a `delay()`, any trip to the database can happen, in other words a point where the scheduler hands control to another coroutine. If that coroutine also calls `Number::format()` without an explicit locale, it gets whatever locale someone else just set. The explicit `locale:` parameter removes the possibility of a race entirely: there's nothing to protect, because there's nothing to share. **Superglobals.** `$_GET`, `$_POST`, `$_SERVER`, `$_SESSION` were safe under PHP-FPM simply because they lived for one request. In a coroutine worker they're variables shared by the whole process, and the server updates them, not PHP on every new connection the way you're used to. Use the `Request` object, which `laravel-spawn` already isolates through `current_context()`, and leave the superglobals alone. ## When static Is Actually Safe It's just as important not to swing to the other extreme and declare every `static` a crime. These are safe: * **`readonly static`**, if a value never changes after initialization, sharing it between coroutines is no more dangerous than sharing a constant. * **Boot-time configuration**, a `static` set once when the worker starts and only read afterward (routes, compiled templates, registered macros). * **Deterministic caches**, if the result depends only on the input arguments and not on the "current" request (say, the `Str::camel()` cache for a given string is always the same string), a race to fill it doesn't corrupt data, at worst something gets computed twice. * **Monotonic counters without semantics**, an auto-increment alias like an internal counter for unique SQL aliases: even if two requests grab the same number, the collision doesn't produce wrong data, at most a less pretty alias. The difference is always the same: is it *a computed result that doesn't depend on the request* being shared, or *state that belongs to one specific request*? The first is an optimization. The second is a leak. ## Static Analysis Instead Of Careful Reading Manually scrolling through every third-party package looking for `private static` without `readonly` is exactly the kind of work you gladly hand off to a linter. The package ships a `PHPStan` rule built for this: ```php final class MutableStaticPropertyRule implements Rule { public function getNodeType(): string { return Property::class; } public function processNode(Node $node, Scope $scope): array { if (! $node->isStatic() || $node->isReadonly()) { return []; } // ... message "potential state leak between coroutines" } } ``` The rule is about as simple as it gets: it finds every `static` property without the `readonly` modifier and flags it. There will be plenty of false positives, exactly the "safe" cases listed above, but that's a deliberate trade-off: missing a real leak is more expensive than manually sorting through a list of candidates once. ```bash phpstan analyse app/ --configuration=phpstan.neon phpstan analyse vendor/some/package/src --configuration=phpstan.neon ``` Running the rule against the Laravel framework itself turns up more than three hundred findings. The overwhelming majority are exactly the safe `static` from the previous section: compiled `BladeCompiler` caches, configuration flags set once at `boot()`, resolvers that internally reach into an already-isolated `$app['request']`. Sorting through three hundred lines once is a quarter hour of work. Missing even one and hitting a leak in production is hours of debugging someone else's bug report saying "I'm seeing another user's profile." ## The Bottom Line Before leaving a `static` (or a singleton with a mutable property) in code that runs inside a request handler, ask one question: will this value survive the end of the current request and still be correct for the next one? If yes, it's safe. If it's supposed to expire together with the request, but physically keeps living in shared process memory, it's a candidate for `ScopedService`, `request_context()`/`coroutine_context()` (see the [second chapter](/en/tutors-laravel/02-pool-transactions.html)), or a plain instance property recreated per request. We've covered your own code and stock Laravel. But a real project usually has `spatie/laravel-permission`, `Telescope`, `Inertia`, and `Debugbar` living right alongside it, each with its own history of mutable state. Which of them is already adapted, and which is worth disabling in async mode, is the topic of the next chapter. --- --- url: https://true-async.github.io/en/docs/components/introduction.md description: What is asynchrony and why do you need it? --- ## How Traditional PHP (FPM) Works ![FPM Model](../../../assets/docs/fpm_model.jpg) If a PHP server application were a restaurant, it would probably be considered an elite establishment where each table is served by a dedicated waiter. Each new request to the server is handled by a separate PHP VM, process, or thread, after which the state is destroyed. This is equivalent to a waiter serving one table and then being fired or having their memory wiped. This model has an advantage: if a PHP error occurs, a memory leak, a forgotten database connection -- it doesn't affect other requests. Each request is isolated. This means development is simpler, debugging is simpler, and there is high fault tolerance. In recent years, the PHP community has been trying to introduce a stateful model, where a single PHP VM can serve multiple requests, preserving state between them. For example, the Laravel Octane project, which uses Swoole or RoadRunner, achieves better performance by preserving state between requests. But this is far from the limit of what's possible. Firing a waiter after each order is too expensive. Because dishes are prepared slowly in the kitchen, the waiter spends most of their time waiting. The same thing happens with PHP-FPM: the PHP VM sits idle. There are more context switches, more overhead for creating and destroying processes or threads, and more resource consumption. ```php // Traditional PHP-FPM $user = file_get_contents('https://api/user/123'); // standing and waiting 300ms $orders = $db->query('SELECT * FROM orders'); // standing and waiting 150ms $balance = file_get_contents('https://api/balance'); // standing and waiting 200ms // Spent: 650ms of pure waiting // CPU is idle. Memory is idle. Everything is waiting. ``` ## Concurrency ![Concurrency Model](../../../assets/docs/concurrency_model.jpg) Since the kitchen cannot prepare dishes instantly, and the waiter has idle time between preparations, there is an opportunity to handle orders from multiple customers. This scheme can work quite flexibly: Table 1 ordered three dishes. Table 2 ordered two dishes. The waiter brings the first dish to table 1, then the first dish to table 2. Or maybe they managed to bring two dishes to the first table and one to the second. Or the other way around! This is concurrency: sharing a single resource (`CPU`) between different logical execution threads, which are called coroutines. ```php use function Async\spawn; use function Async\await; // Launch all three requests "concurrently" $userTask = spawn(file_get_contents(...), 'https://api/user/123'); $ordersTask = spawn($db->query(...), 'SELECT * FROM orders'); $balanceTask = spawn(file_get_contents(...), 'https://api/balance'); // While one request is waiting for a response, we do others! $user = await($userTask); $orders = await($ordersTask); $balance = await($balanceTask); // Spent: 300ms (the time of the slowest request) ``` ## Concurrency is not Parallelism It's important to understand the difference. **Concurrency** -- as in `True Async`, `JavaScript`, `Python`: * One waiter quickly switches between tables * One PHP thread switches between tasks * Tasks are **interleaved**, but do not execute simultaneously * No race conditions -- only one coroutine runs at any given moment **Parallelism** -- this is multithreading (`Go`): * Multiple waiters work simultaneously * Multiple threads execute on different CPU cores * Tasks execute **truly simultaneously** * Mutexes, locks, all that pain is required ## What's Next? Now you understand the essence. You can dig deeper: * [Efficiency](../evidence/concurrency-efficiency.md) -- how many coroutines are needed for maximum performance * [Evidence Base](../evidence/coroutines-evidence.md) -- measurements, benchmarks and research confirming the effectiveness of coroutines * [Swoole in Practice](../evidence/swoole-evidence.md) -- real measurements: Appwrite +91%, IdleMMO 35M req/day, benchmarks with DB * [Python asyncio in Practice](../evidence/python-evidence.md) -- Duolingo +40%, Super.com -90% costs, Instagram, uvloop benchmarks * [Coroutines](coroutines.md) -- how they work under the hood * [Scope](scope.md) -- how to manage groups of coroutines * [Scheduler](scheduler.md) -- who decides which coroutine to run --- --- url: https://true-async.github.io/en/tutors-server/08-workers.md description: >- setWorkers(): the first series' threads under the server's hood, the bootloader, and HTTP/3 on the same port. --- # Workers and HTTP/3 Open some htop on the server under load. Our process is toiling away, thousands of requests in flight... and out of eight cores, one is busy. Seven idle. Annoying? Annoying. There's nothing new in this, we covered it in the chapter on threads: while tasks wait on I/O, one core is enough for everyone. But under real traffic the server doesn't only wait. HTTP parsing, TLS handshakes, JSON serialization, those are computations, and they run up against that one single core. The recipe from that same chapter: give computation threads. The server applies it in a single line: ```php $config = (new HttpServerConfig()) ->addListener('0.0.0.0', 8080) ->setWorkers(Async\available_parallelism()); ``` `setWorkers(N)` spins up N workers, and it's literally `Async\ThreadPool` from chapter fourteen. Not a "similar mechanism," but the very same one. Which means you already know the rules too: each worker is a separate operating system thread with its own PHP environment, its own event loop, its own pools. The configuration and handlers are copied into each worker by the usual rules for passing between threads. `start()` in the parent waits for all of them. One question remains: who hands incoming connections to the workers? And here's the nicest part. Nobody. Each worker opens the same port with the `SO_REUSEPORT` flag, and from there the Linux kernel itself distributes connections among them. No dispatcher, no queue, no locks. Eight independent servers hidden behind one port. ## Bootloader: Warming Up Each Worker In the first series ThreadPool had a bootloader, and there it looked like an optional convenience. Here it becomes the central figure. Here's why: everything we did in the first chapter "once, before `start()`" now has to happen in every worker. Each one has its own memory, after all. ```php $config ->setWorkers(8) ->setBootloader(function () { require __DIR__ . '/vendor/autoload.php'; Database::initPool(min: 4, max: 16); // its own PDO Pool in each worker Router::compile(); }); ``` The closure runs once per worker, before the first request. An exception inside it stops the whole pool. Harsh? Correct: a server with one under-warmed worker out of eight is a machine that fires errors at every eighth client. Better it not start at all. ## The Chat Meets Workers And now the promised explosion. In the previous chapter we built a chat on the formula "shared state in the process's memory." Reread the formula slowly. In the memory. Of the process. Which one of the eight? The kernel scatters connections however it pleases. Alice landed in worker 3, Bob in worker 5. Each worker has its own memory, and thus its own `$room`. Two rooms with the same name that will never know about each other. Alice writes into the void, Bob is silent in a different void. No races, no errors, the chat just quietly stopped being a chat. What to do? The standard ways out are these. For a small system, an honest `setWorkers(1)`: one worker holds thousands of WebSocket connections just fine, since they mostly wait. For a large one, move the shared state outside, usually into Redis pub/sub, and let the workers talk through it. A rule to remember: request state lives in the request scope, process state in the worker, shared state in external storage. ## HTTP/3: The Same Handlers, a Different Transport Since we're already scaling, let's bring the stack up to modern. About HTTP/3 it's enough to know three things. It works not over TCP but over QUIC on UDP. It establishes a connection faster and doesn't let one lost packet stall all the streams at once. And it's mandatory to learn, because browsers already prefer it. Sounds like a big construction project? Look: ```php $config = (new HttpServerConfig()) ->setWorkers(Async\available_parallelism()) ->setCertificate('/etc/tls/profile.crt') ->setPrivateKey('/etc/tls/profile.key') ->addListener('0.0.0.0', 443, tls: true) // TCP: HTTP/1.1 and HTTP/2 ->addHttp3Listener('0.0.0.0', 443); // UDP: HTTP/3 ``` One line, `addHttp3Listener`. The same port, and there's no conflict: 443/TCP listens for HTTP/1.1 and HTTP/2, while 443/UDP goes to QUIC. It has no separate TLS flag, because per the spec QUIC doesn't exist without TLS; the certificates are taken from the server. How do clients learn about the UDP entrance? On their own. To every response over TCP the server adds an `Alt-Svc: h3=":443"` header. The browser sees it and sends the next requests over HTTP/3. First visit over HTTP/2, then QUIC, and nobody configured anything. ```bash $ curl --http3 -I https://profile.example.com/ HTTP/3 200 alt-svc: h3=":443"; ma=86400 ``` Know what I like most about this chapter? What isn't in it. We turned on eight threads and the third version of HTTP, and not a single line changed in the handlers. The routing from chapter two, the SSE from chapter six, the chat from chapter seven, none of them is aware that the world around them became multithreaded and started speaking QUIC. Scaling moved off into the config, where it belongs. The server got fast. The next step is to make it unsinkable: what to do about overload, about slow clients, about deploying in the thick of traffic. One chapter about bad days. --- --- url: https://true-async.github.io/en/tutors-server/01-first-server.md description: >- An HTTP server inside PHP: HttpServer, HttpServerConfig, and your first handler. --- # Your First Server Let's recall how PHP usually serves a request. Nginx accepts the connection and hands it to PHP-FPM. FPM grabs a free process. The process wakes up, loads classes, opens a database connection, assembles the response, sends it. And dies. Everything it managed to build, every connection, every cache, all those beautifully warmed-up pools from the first series, goes in the trash. A millisecond later the next request arrives, and the whole story replays from scratch. A hundred times a second. A thousand. Meanwhile, in the first series we built a whole arsenal of things for which such a life is contraindicated: a connection pool is good when it lives a long time, and a memoized `Future` is meaningless if it dies together with the process. So the plan for this series is simple: remove the middlemen. `TrueAsync Server` is an extension that runs an `HTTP` server right inside the `PHP` process: ```php use TrueAsync\HttpServer; use TrueAsync\HttpServerConfig; $server = new HttpServer( new HttpServerConfig()->addListener('0.0.0.0', 8080) ); $server->addHttpHandler(function ($request, $response) { $response->setStatusCode(200)->setBody('Hello, World!'); }); $server->start(); ``` ```bash $ php server.php & $ curl -i http://localhost:8080/ HTTP/1.1 200 OK Content-Length: 13 Hello, World! ``` `addListener` opens a port, `addHttpHandler` registers a handler function, and `start()` launches the event loop and never returns. Every incoming request runs the handler. ## A Handler Is a Coroutine Every handler invocation runs in its own coroutine (`spawn`). What does that mean in practice? Let's run an experiment. We'll add a deliberately slow route to the server: ```php use function Async\delay; $server->addHttpHandler(function ($request, $response) { if ($request->getPath() === '/slow') { delay(5000); // five seconds of "heavy" I/O work $response->setBody("was slow\n"); return; } $response->setBody("fast\n"); }); ``` Now open two terminals. In the first, request `/slow`. It hangs, waiting out its five seconds. Without waiting for it, request `/` in the second terminal: ```bash $ curl http://localhost:8080/ fast ``` Instantly. If you read the first series, you already understand what happened. The `/slow` coroutine fell asleep in `delay`, the scheduler passed control on, and the server calmly served the second request. These are the same "A" and "B" counters from the very first chapter, only now they're called HTTP requests. One thread. One event loop. Thousands of concurrent clients. Now picture what that same `/slow` would do to classic FPM. Five seconds of sleep is five seconds during which an entire worker is out of commission. A dozen such requests and the worker pool is exhausted. The whole site stands and waits while someone finishes their nap. ## A Process That Doesn't Die The second consequence is more interesting than the first, even though it looks mundane. `start()` never returns. Which means everything created before it lives as long as the server does: ```php $pdo = new PDO($dsn, $user, $password, [ PDO::ATTR_POOL_ENABLED => true, PDO::ATTR_POOL_MAX => 10, ]); $directory = new RegionsDirectory(); // memoization from chapter six $server->addHttpHandler(function ($request, $response) use ($pdo, $directory) { // the pool is already warm, the directory is already loaded }); $server->start(); ``` The connection pool opens once. The regions directory loads once. The routes compile once. Remember how much effort the first series spent on reuse tooling? Here's the place where it all finally comes home. Cold start didn't get faster. It vanished. To be fair, long life has a price, and it's worth saying so upfront. A memory leak is no longer forgiven by the death of the process after the request. A global variable is no longer "for one request," it's forever and for everyone. No need to panic: scope, pools, and context from the first series were invented for exactly this, and we'll cover the server-specific details in a separate chapter. For now our server has a simpler problem: it answers the same thing to everything. `GET /profile/42`, `POST /profile/42/address`, a typo in the URL, it makes no difference. A real `ProfileService` will have to learn to read the request. That's what we'll tackle next. --- --- url: https://true-async.github.io/en/docs/components/zombie-coroutines.md description: >- Zombie coroutines in TrueAsync -- tolerance for third-party code, disposeSafely(), disposeAfterTimeout(), managing non-cancellable tasks. --- # Zombie Coroutines: Fault Tolerance ## The Problem: Code That Can't Be Cancelled Coroutine cancellation is a cooperative process. The coroutine receives a `Cancellation` exception at a suspension point and must terminate gracefully. But what if someone made a mistake and created a coroutine in the wrong `Scope`? Although `TrueAsync` follows the `Cancellation by design` principle, situations may arise where someone wrote code whose cancellation could lead to an unpleasant outcome. For example, someone created a background task to send an `email`. The coroutine was cancelled, the `email` was never sent. High fault tolerance allows significant savings in development time and minimizes the consequences of errors, if programmers use log analysis to improve application quality. ## The Solution: Zombie Coroutines To smooth over such situations, `TrueAsync` provides a special approach: tolerant handling of "stuck" coroutines -- zombie coroutines. A `zombie` coroutine is a coroutine that: * Continues execution as normal * Remains bound to its Scope * Is not considered active -- the Scope can formally complete without waiting for it * Does not block `awaitCompletion()`, but blocks `awaitAfterCancellation()` ```php $scope = new Async\Scope(); $scope->spawn(function() { thirdPartySync(); // Third-party code -- we don't know how it reacts to cancellation }); $scope->spawn(function() { return myOwnCode(); // Our code -- correctly handles cancellation }); // disposeSafely() does NOT cancel coroutines, but marks them as zombie $scope->disposeSafely(); // Scope is closed for new coroutines. // Existing coroutines continue working as zombies. ``` ## Three Strategies for Scope Termination `TrueAsync` provides three ways to close a `Scope`, designed for different levels of trust in the code: ### `dispose()` -- Forced Cancellation All coroutines receive `Cancellation`. The Scope closes immediately. Use when you control all code inside the Scope. ```php $scope->dispose(); // All coroutines are cancelled. Scope is closed. ``` ### `disposeSafely()` -- No Cancellation, Coroutines Become Zombies Coroutines **do not receive** `Cancellation`. They are marked as `zombie` and continue running. The `Scope` is considered closed -- new coroutines cannot be created. Use when the `Scope` contains "third-party" code and you are not confident about the correctness of cancellation. ```php $scope->disposeSafely(); // Coroutines continue working as zombies. // Scope is closed for new tasks. ``` ### `disposeAfterTimeout(int $timeout)` -- Cancellation with Timeout A combination of both approaches: first, coroutines are given time to finish, then the `Scope` is forcefully cancelled. ```php $scope->disposeAfterTimeout(5000); // After 5 seconds, the Scope will send Cancellation to all remaining coroutines. ``` ## Waiting for Zombie Coroutines `awaitCompletion()` waits only for **active** coroutines. Once all coroutines become zombies, `awaitCompletion()` considers the Scope finished and returns control. But sometimes you need to wait for **all** coroutines to complete, including zombies. For this, `awaitAfterCancellation()` exists: ```php $scope = new Async\Scope(); $scope->spawn(fn() => longRunningTask()); $scope->spawn(fn() => anotherTask()); // Cancel -- coroutines that can't be cancelled will become zombies $scope->cancel(); // awaitCompletion() will return immediately if only zombies remain $scope->awaitCompletion($cancellation); // awaitAfterCancellation() will wait for ALL, including zombies $scope->awaitAfterCancellation(function (\Throwable $error, Async\Scope $scope) { // Error handler for zombie coroutines echo "Zombie error: " . $error->getMessage() . "\n"; }); ``` | Method | Waits for active | Waits for zombies | Requires cancel() | |------------------------------|:----------------:|:-----------------:|:------------------:| | `awaitCompletion()` | Yes | No | No | | `awaitAfterCancellation()` | Yes | Yes | Yes | `awaitAfterCancellation()` can only be called after `cancel()` -- otherwise an error will occur. This makes sense: zombie coroutines appear precisely as a result of cancellation with the `DISPOSE_SAFELY` flag. ## How Zombies Work Internally When a coroutine is marked as `zombie`, the following happens: 1. The coroutine receives the `ZOMBIE` flag 2. The active coroutine counter in the `Scope` decreases by 1 3. The `zombie` coroutine counter increases by 1 4. The `Scope` checks whether any active coroutines remain and can notify waiters about completion ``` Scope +-- active_coroutines_count: 0 <-- decreases +-- zombie_coroutines_count: 2 <-- increases +-- coroutine A (zombie) <-- continues running +-- coroutine B (zombie) <-- continues running ``` A `zombie` coroutine is **not detached** from the `Scope`. It remains in its coroutine list, but is not counted as active. When a `zombie` coroutine finally completes, it is removed from the `Scope`, and the `Scope` checks whether it can fully release resources. ## How the Scheduler Handles Zombies The `Scheduler` maintains two independent coroutine counts: 1. **Global active coroutine counter** (`active_coroutine_count`) -- used for quick checks on whether anything needs to be scheduled 2. **Coroutine registry** (`coroutines` hash table) -- contains **all** coroutines that are still running, including `zombies` When a coroutine is marked as `zombie`: * The global active coroutine counter **decreases** -- the Scheduler considers there is less active work * The coroutine **remains** in the registry -- the `Scheduler` continues managing its execution The application continues running as long as the active coroutine counter is greater than zero. An important consequence follows: `Zombie` coroutines do not prevent the application from shutting down, since they are not considered active. If there are no more active coroutines, the application terminates and even `zombie` coroutines will be cancelled. ## Inheriting the Safely Flag Only the global scope has the `DISPOSE_SAFELY` flag by default. A `new Scope()` is created without it: when such a `Scope` is destroyed (e.g., in an object's destructor), its coroutines are cancelled. `allowZombies()` turns the flag on, and then coroutines become `zombies` rather than being cancelled. A child `Scope` inherits the flag from its parent: ```php $parent = (new Async\Scope())->allowZombies(); // parent has the DISPOSE_SAFELY flag $child = Async\Scope::inherit($parent); // child also has the DISPOSE_SAFELY flag $top = Async\Scope::inherit(); // at the top level the parent is the global scope, so $top has the flag too ``` To force cancellation on destruction for an inherited Scope, use `asNotSafely()`: ```php $scope = Async\Scope::inherit()->asNotSafely(); // Now when the Scope object is destroyed, // coroutines will be cancelled rather than marked as zombies ``` ## Example: HTTP Server with Middleware ```php class RequestHandler { private Async\Scope $scope; public function __construct() { $this->scope = new Async\Scope(); } public function handle(Request $request): Response { // Launch middleware -- this could be third-party code $this->scope->spawn(function() use ($request) { $this->runMiddleware($request); }); // Main processing -- our code $response = $this->scope->spawn(function() use ($request) { return $this->processRequest($request); }); return await($response); } public function __destruct() { // On destruction: middleware may not be ready for cancellation, // so we use disposeSafely() instead of dispose(). // Zombie coroutines will finish on their own. $this->scope->disposeSafely(); } } ``` ## Example: Handler with Time Limit ```php $scope = new Async\Scope(); // Launch tasks with third-party code $scope->spawn(fn() => thirdPartyAnalytics($data)); $scope->spawn(fn() => thirdPartyNotification($userId)); // Give 10 seconds to finish, then force cancellation $scope->disposeAfterTimeout(10000); ``` ## When Zombies Become a Problem `Zombie` coroutines are a compromise. They solve the third-party code problem but can lead to resource leaks. Therefore, `disposeAfterTimeout()` or a `Scope` with explicit coroutine cancellation is the best choice for production: it gives third-party code time to finish but guarantees cancellation in case of hanging. ## Summary | Method | Cancels coroutines | Coroutines finish | Scope closed | |---------------------------|:------------------:|:------------------:|:------------:| | `dispose()` | Yes | No | Yes | | `disposeSafely()` | No | Yes (as zombies) | Yes | | `disposeAfterTimeout(ms)` | After timeout | Until timeout | Yes | ## Logging Zombie Coroutines In future versions, `TrueAsync` intends to provide a mechanism for logging zombie coroutines, which will allow developers to troubleshoot issues related to stuck tasks. ## What's Next? * [Scope](/en/docs/components/scope.html) -- managing groups of coroutines * [Cancellation](/en/docs/components/cancellation.html) -- cancellation patterns * [Coroutines](/en/docs/components/coroutines.html) -- coroutine lifecycle