Skip to content
Back to skills

Swoole Expert

ASecurity

Deep reference for writing production Swoole PHP code. Covers the long-running process mental model (object lifetimes, what breaks vs php-fpm, per-request state via Coroutine::getContext), runtime hook flags (what's hookable year-to-year, what isn't), parent/child priority gotcha, Channel/WaitGroup/Barrier/defer patterns, server canonical skeleton (SWOOLE_BASE default since 5.0, dispatch modes, graceful reload rules), connection pooling (try/finally + defer, Channel-as-pool), the full pitfall...

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 29, 2026
databasesgophpshellbashsqlreactdockertestingapidatabase

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned September 29, 2026

npx -y skills add abnegate/claudes --skill swoole-expert --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Swoole Expert?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Swoole Expert
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/abnegate-swoole-expert/badge)](https://www.skillsdirectory.com/skills/abnegate-swoole-expert)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: swoole-expert
description: Deep reference for writing production Swoole PHP code. Covers the long-running process mental model (object lifetimes, what breaks vs php-fpm, per-request state via Coroutine::getContext), runtime hook flags (what's hookable year-to-year, what isn't), parent/child priority gotcha, Channel/WaitGroup/Barrier/defer patterns, server canonical skeleton (SWOOLE_BASE default since 5.0, dispatch modes, graceful reload rules), connection pooling (try/finally + defer, Channel-as-pool), the full pitfalls catalog (blocking in coroutines, shared connections, pcntl, Channel deadlocks, cooperative != concurrent-safe, exception isolation), production tuning tables, testing entry point with Co\run, Swoole 6.x version notes (removed coroutine clients, multi-threading, io_uring, Lock API changes), and build flags. Use when writing or reviewing Swoole code (swoole/swoole-src, NOT openswoole) on Swoole 5.x/6.x with PHP 8.2+.
---

# Swoole Expert

Reference for **swoole/swoole-src** (NOT openswoole). Target: **Swoole 5.x/6.x**, **PHP 8.2+**. Assumes baseline coroutine/async knowledge. This file covers only what's house-specific, load-bearing, or shifts year-to-year.

Non-negotiable defaults:
- Prefer idiomatic `Co\run` / `go()` / `Co::defer` patterns
- Always enable `SWOOLE_HOOK_ALL` so native PHP I/O becomes coroutine-aware
- Never share a connection across coroutines -- use a pool
- Workers are **resident** processes, not short-lived like php-fpm

---

## 1. The long-running process mental model

Swoole workers are **resident PHP processes**. Unlike php-fpm, memory is NOT torn down between requests. Everything allocated at class, file, or worker scope survives across every request.

### Four object lifetimes

| Lifetime | Where created | Destroyed by |
|---|---|---|
| **Program-global** | Before `$server->start()` | Full process shutdown (`reload` will NOT refresh) |
| **Process-global** | In `onWorkerStart` | `max_request` reached, worker crash, or reload |
| **Session** | `onConnect` / first `onReceive` | `onClose` |
| **Request** | Inside the request handler | End of request |

If you allocated it in a hot path, assume it's a leak unless you can point to where it's freed.

### What does NOT work (vs php-fpm)

- **Superglobals**: `$_GET`, `$_POST`, `$_COOKIE`, `$_FILES`, `$_SERVER`, `$_REQUEST`, `$_SESSION` are NOT populated. Use `$request->get`, `$request->post`, `$request->cookie`, `$request->files`, `$request->server`, `$request->header`, `$request->rawContent()`.
- **`session_start()`** -- implement sessions with Redis/DB keyed by a cookie from `$request->cookie`.
- **`echo` / `print_r` / `var_dump`** -- go to stdout/log_file, NOT the client. Use `$response->write()` / `$response->end()`.
- **`exit()` / `die()`** -- kills the worker. Swoole 4.1+ converts to `Swoole\ExitException`. Never use for control flow.
- **`header()` / `setcookie()`** -- silent no-ops. Use `$response->header()`, `$response->cookie()`, `$response->status()`. Must be called BEFORE `$response->end()`.
- **Static properties / singletons / `global`** -- leak request state across coroutines. Use `Coroutine::getContext()`.
- **`pcntl_*`** -- forbidden in coroutines. Use `Swoole\Process` and `Process::signal()`.
- **Xdebug, phptrace, aop, molten, xhprof, phalcon** -- incompatible with coroutines. Disable them.

### Per-request state with `Coroutine::getContext()`

```php
use Swoole\Coroutine;

// Context is a Swoole\Coroutine\Context (extends ArrayObject),
// auto-destroyed when coroutine exits -- no cleanup needed.
$ctx = Coroutine::getContext();
$ctx['user_id']   = 42;
$ctx['requestId'] = bin2hex(random_bytes(8));

function innerFunction(): void
{
    $ctx = Coroutine::getContext();
    $userId = $ctx['user_id'];  // works from anywhere in the same coroutine
}
```

Do NOT stash `$this` in the context -- it holds a strong reference and usually leaks the controller.

---

## 2. Coroutines

### Creating and running

```php
Swoole\Coroutine::create(callable $fn, mixed ...$args): int|false
go(callable $fn, mixed ...$args): int|false  // short alias

Swoole\Coroutine\run(callable $fn): bool    // top-level entry point
Co\run(callable $fn): bool                   // alias
```

All coroutine-creating APIs must run inside a **coroutine container** — either `Co\run()`, a server event callback, or a `Process`/`Process\Pool` worker with `enable_coroutine = true`. Nesting `run()` inside another `run()` is forbidden.

### Introspection

```php
Swoole\Coroutine::getCid(): int                   // -1 if outside coroutine
Swoole\Coroutine::getPcid(int $cid = 0): int|false
Swoole\Coroutine::exists(int $cid): bool
Swoole\Coroutine::list(): Swoole\Coroutine\Iterator
Swoole\Coroutine::stats(): array                  // coroutine_num, coroutine_peak_num, ...
Swoole\Coroutine::getBackTrace(int $cid = 0, int $options = DEBUG_BACKTRACE_PROVIDE_OBJECT, int $limit = 0): array
Swoole\Coroutine::printBackTrace(int $cid = 0): void
Swoole\Coroutine::getElapsed(int $cid = 0): int   // milliseconds alive
```

### Key non-obvious behaviors

### Parent/child priority gotcha

When you call `go()`, the **child starts immediately** and runs until its first yield. Only then does `go()` return to the parent:

```php
echo "a\n";
go(function () {
    echo "b\n";
    Co::sleep(0.1);   // yields here
    echo "d\n";
});
echo "c\n";
// Output: a, b, c, d
```

Coroutines have no parent/child lifecycle -- a parent exiting does not cancel or wait for children. Use `Barrier` or `WaitGroup` to wait.

### Scheduling yield points

A coroutine runs until: hooked I/O, `Co::sleep()`, `Channel::push()`/`pop()`, `Coroutine::yield()`/`suspend()`, `WaitGroup::wait()`/`Barrier::wait()`.

**A CPU-bound loop with no I/O monopolizes the worker.** Sprinkle `Coroutine::sleep(0)` to voluntarily yield, or dispatch CPU work to a task worker.

### Coroutine config

```php
Swoole\Coroutine::set(array $options): void  // call BEFORE run() / Server->start()
```

| Option | Default | Note |
|---|---|---|
| `max_coroutine` | 100000 | Global limit |
| `stack_size` / `c_stack_size` | 2 MB | C stack per coroutine |
| `hook_flags` | 0 | One-click hooks, e.g. `SWOOLE_HOOK_ALL` |
| `enable_preemptive_scheduler` | false | Force preemption at 10 ms |
| `socket_connect_timeout` | | Default connect timeout |
| `socket_timeout` | | Read/write timeout |
| `dns_cache_expire` / `dns_server` | | DNS defaults |
| `enable_deadlock_check` | true | |

### Cancellation

```php
Swoole\Coroutine::cancel(int $cid, bool $throwException = false): bool  // $throwException added 6.1
Swoole\Coroutine::isCanceled(): bool
Swoole\Coroutine::setTimeLimit(float $seconds): void  // 6.2+
```

**Caveat**: `cancel()` cannot cancel file I/O coroutines; may segfault. Since 6.2 it can cancel in-flight io_uring ops.

---

## 3. Runtime hooks and flag reference

Hooks patch PHP's blocking stdlib to yield on I/O. Set **once** at bootstrap:

```php
Co::set(['hook_flags' => SWOOLE_HOOK_ALL]);
// Or: $server->set(['hook_flags' => SWOOLE_HOOK_ALL]);
```

### Flag reference (ext-swoole 6.x)

| Flag | Covers |
|---|---|
| `SWOOLE_HOOK_TCP` | TCP streams, fsockopen, mysqlnd-based PDO_MYSQL/mysqli, predis, php-amqplib |
| `SWOOLE_HOOK_UNIX` | Unix-domain stream sockets |
| `SWOOLE_HOOK_UDP` / `SWOOLE_HOOK_UDG` | UDP / Unix datagram |
| `SWOOLE_HOOK_SSL` / `SWOOLE_HOOK_TLS` | TLS streams |
| `SWOOLE_HOOK_SLEEP` | `sleep`, `usleep` (>= 1ms), `time_nanosleep`, `time_sleep_until` |
| `SWOOLE_HOOK_FILE` | `fopen`, `fread`, `fwrite`, `file_get_contents`, `file_put_contents`, `unlink`, `mkdir`, `rmdir`. Uses AIO or **io_uring** in 6.0+ |
| `SWOOLE_HOOK_STREAM_FUNCTION` | `stream_select()` |
| `SWOOLE_HOOK_BLOCKING_FUNCTION` | `gethostbyname`, `shell_*` |
| `SWOOLE_HOOK_PROC` | `proc_open`, `proc_close`, `proc_get_status`, `proc_terminate` |
| `SWOOLE_HOOK_NATIVE_CURL` | Real libcurl coroutinized -- **use this**, not legacy `SWOOLE_HOOK_CURL`. Requires `--enable-swoole-curl`. Guzzle/Symfony HttpClient work transparently |
| `SWOOLE_HOOK_CURL` | **Legacy** partial reimplementation. Avoid |
| `SWOOLE_HOOK_SOCKETS` | ext-sockets. Auto-dropped in 6.1.6+ if ext-sockets not loaded |
| `SWOOLE_HOOK_STDIO` | STDIN/STDOUT/STDERR |
| `SWOOLE_HOOK_PDO_PGSQL` | `pdo_pgsql` (5.1+; 6.1.7 added timeout) |
| `SWOOLE_HOOK_PDO_ODBC` | `pdo_odbc` (5.1+) |
| `SWOOLE_HOOK_PDO_ORACLE` | `pdo_oci` (5.1+). **Constant is `_ORACLE`, not `_OCI`** |
| `SWOOLE_HOOK_PDO_SQLITE` | `pdo_sqlite` (5.1+) -- requires sqlite serialized/multi-thread |
| `SWOOLE_HOOK_PDO_FIREBIRD` | `pdo_firebird` (**new 6.2**) |
| `SWOOLE_HOOK_MONGODB` | MongoDB (**new 6.2** via `Swoole\RemoteObject\Server`) |
| `SWOOLE_HOOK_NET_FUNCTION` | coroutine `gethostbyname` (6.2+) |
| `SWOOLE_HOOK_ALL` | All of the above |

### Not hookable (block -- don't use in a coroutine)

- `mysql` extension (libmysqlclient)
- `mongo` / `mongodb` (mongo-c-client) -- use the 6.2+ MongoDB hook instead
- `php-amqp` (C AMQP ext -- but `php-amqplib` over streams works)
- Any extension bypassing PHP's streams layer

`pdo_mysql` and `mysqli` hookable **only in mysqlnd mode**. Check `php -m | grep mysqlnd`.

---

## 4. Concurrency primitives

### `Swoole\Coroutine\Channel`

```php
final class Swoole\Coroutine\Channel {
    public int $capacity;
    public int $errCode;  // SWOOLE_CHANNEL_OK | _TIMEOUT | _CLOSED | _CANCELED

    public function __construct(int $capacity = 1);
    public function push(mixed $data, float $timeout = -1): bool;
    public function pop(float $timeout = -1): mixed;
    public function close(): bool;
    public function length(): int;
    public function isEmpty(): bool;
    public function isFull(): bool;
    public function stats(): array;
}
```

**Gotchas**:
- Pushing `false`/`null`/`0` is ambiguous -- `pop()` also returns `false` on close/timeout. Always check `$chan->errCode`.
- `close()` wakes ALL waiting producers/consumers; they return `false`.
- Create channels in `onWorkerStart`, not before `start()`.

**Channel as semaphore** (capacity N, pre-filled):

```php
$sem = new Channel(5);
for ($i = 0; $i < 5; $i++) $sem->push(true);

go(function () use ($sem) {
    $sem->pop();
    try { doLimitedWork(); } finally { $sem->push(true); }
});
```

### `Swoole\Coroutine\WaitGroup`

```php
final class Swoole\Coroutine\WaitGroup {
    public function __construct(int $delta = 0);
    public function add(int $delta = 1): void;
    public function done(): void;
    public function wait(float $timeout = -1): bool;  // false on timeout
    public function count(): int;
}
```

Always call `done()` in `finally` -- if a worker throws and skips `done()`, `wait()` hangs.

### `Swoole\Coroutine\Barrier` (preferred over WaitGroup)

```php
use Swoole\Coroutine\Barrier;

$barrier = Barrier::make();
for ($i = 0; $i < 4; $i++) {
    go(function () use ($barrier) {  // capturing $barrier bumps refcount
        Co::sleep(0.5);
    });
}
Barrier::wait($barrier);  // by reference; nulled after wait
```

`wait()` takes the barrier **by reference**. If you forget `use ($barrier)` the child doesn't hold a ref and `wait()` returns immediately.

### `defer` -- Go-style cleanup

```php
go(function () {
    $db = new PDO(/* ... */);
    defer(fn() => $db = null);  // runs on coroutine exit, LIFO order, even on exception
});
```

### Timers

```php
Swoole\Timer::tick(int $msec, callable $cb, mixed ...$params): int
Swoole\Timer::after(int $msec, callable $cb, mixed ...$params): int
Swoole\Timer::clear(int $timer_id): bool
Swoole\Timer::clearAll(): bool
Swoole\Timer::info(int $timer_id): ?array
Swoole\Timer::list(): Swoole\Timer\Iterator
Swoole\Timer::stats(): array
```

Callback: `function(int $timerId, mixed ...$params): void`. In a coroutine container, timer callbacks run inside a new coroutine automatically.

### Batch primitives

```php
Swoole\Coroutine\batch(array $tasks, float $timeout = -1): array    // concurrent callables
Swoole\Coroutine\parallel(int $n, callable $fn): void               // N copies of $fn
Swoole\Coroutine\map(array $list, callable $fn, float $timeout = -1): array
Swoole\Coroutine::join(array $cids, float $timeout = -1): bool      // wait by cid
```

---

## 5. HTTP / WebSocket / TCP servers

### Canonical HTTP server skeleton

```php
<?php
declare(strict_types=1);

use Swoole\Http\{Server, Request, Response};

$server = new Server('0.0.0.0', 9501, SWOOLE_BASE);

$server->set([
    'worker_num'            => swoole_cpu_num() * 2,
    'task_worker_num'       => 4,
    'task_enable_coroutine' => true,
    'max_request'           => 10_000,
    'max_wait_time'         => 60,
    'reload_async'          => true,
    'hook_flags'            => SWOOLE_HOOK_ALL,
    'http_compression'      => true,
    'log_file'              => '/var/log/swoole.log',
    'log_level'             => SWOOLE_LOG_INFO,
    'package_max_length'    => 8 * 1024 * 1024,
]);

$server->on('request', function (Request $request, Response $response): void {
    $method = $request->server['request_method'] ?? 'GET';
    $uri    = $request->server['request_uri']    ?? '/';
    $body   = $request->rawContent();
    $response->status(200);
    $response->header('Content-Type', 'application/json');
    $response->end(json_encode(['method' => $method, 'uri' => $uri]));
});

$server->on('workerStart', function (Server $server, int $workerId): void {
    // Reset per-worker state, open DB/Redis pools here
});

$server->start();
```

### Server constructor

```php
new Swoole\Server(
    string $host = '0.0.0.0',
    int $port = 0,
    int $mode = SWOOLE_BASE,           // SWOOLE_BASE (default), SWOOLE_PROCESS, SWOOLE_THREAD (6.0+ ZTS)
    int $sock_type = SWOOLE_SOCK_TCP
)
```

`Http\Server` and `WebSocket\Server` extend `Swoole\Server` with the same constructor. SSL: OR sock type with `SWOOLE_SSL`.

### All server events

```
onStart(Server $server)                    // master start; NOT in SWOOLE_BASE mode
onShutdown(Server $server)
onManagerStart(Server $server)             // NOT in SWOOLE_BASE mode
onManagerStop(Server $server)
onBeforeReload(Server $server)             // 4.5+
onAfterReload(Server $server)              // 4.5+
onWorkerStart(Server $server, int $workerId)
onWorkerStop(Server $server, int $workerId)
onWorkerExit(Server $server, int $workerId)   // fires when reload_async and worker is draining
onWorkerError(Server $server, int $workerId, int $workerPid, int $exitCode, int $signal)
onBeforeShutdown(Server $server)           // 4.8+
onPipeMessage(Server $server, int $srcWorkerId, mixed $message)

// TCP
onConnect(Server $server, int $fd, int $reactorId)
onReceive(Server $server, int $fd, int $reactorId, string $data)
onPacket(Server $server, string $data, array $clientInfo)   // UDP
onClose(Server $server, int $fd, int $reactorId)

// Task workers
onTask(Server $server, Swoole\Server\Task $task)
onFinish(Server $server, int $taskId, mixed $data)

// HTTP
onRequest(Swoole\Http\Request $request, Swoole\Http\Response $response)

// WebSocket
onHandShake(Http\Request $request, Http\Response $response): bool
onOpen(WebSocket\Server $server, Http\Request $request)
onMessage(WebSocket\Server $server, WebSocket\Frame $frame)
```

### Core server methods

```php
public function set(array $settings): bool;
public function on(string $event, callable $callback): bool;
public function start(): bool;
public function stop(int $workerId = -1): bool;
public function shutdown(): bool;
public function reload(bool $onlyReloadTaskworker = false): bool;

public function send(int|string $fd, string $data, int $serverSocket = -1): bool;
public function close(int $fd, bool $reset = false): bool;
public function exists(int $fd): bool;
public function pause(int $fd): bool;
public function resume(int $fd): bool;

public function task(mixed $data, int $workerIdx = -1, ?callable $finishCb = null): int|false;
public function taskwait(mixed $data, float $timeout = 0.5, int $workerIdx = -1): mixed;
public function taskCo(array $tasks, float $timeout = 0.5): array|false;
public function finish(mixed $data): bool;

public function sendMessage(mixed $message, int $dstWorkerId): bool;
public function addProcess(Swoole\Process $process): int|false;

public function getClientInfo(int $fd, int $reactorId = -1): false|array;
public function getWorkerId(): int|false;
public function stats(): array;
```

### `Swoole\Http\Request` properties

```php
public int    $fd;
public int    $streamId;
public array  $header;     // lowercase keys
public array  $server;     // request_method, request_uri, query_string, request_time, remote_addr, ...
public ?array $cookie;
public array  $get;
public array  $post;
public array  $files;

public function rawContent(): string|false;   // alias getContent()
public function getMethod(): string|false;
```

### `Swoole\Http\Response` methods

```php
public function status(int $httpCode, string $reason = ''): bool;
public function header(string $key, string|array $value, bool $format = true): bool;
public function cookie(string $name, string $value = '', int $expires = 0,
    string $path = '/', string $domain = '', bool $secure = false, bool $httponly = false,
    string $samesite = '', string $priority = ''): bool;
public function trailer(string $key, string $value): bool;
public function write(string $content): bool;     // chunked; disables compression
public function end(?string $content = null): bool;
public function sendfile(string $filename, int $offset = 0, int $length = 0): bool;
public function redirect(string $location, int $httpCode = 302): bool;
public function detach(): bool;

// Upgrade to WebSocket from an HTTP server
public function upgrade(): bool;
public function push(Frame|string $data, int $opcode = WEBSOCKET_OPCODE_TEXT, int $flags = WEBSOCKET_FLAG_FIN): bool;
public function recv(float $timeout = 0): Frame|string|false;
public function close(): bool;
```

### Server modes

**Default mode changed from `SWOOLE_PROCESS` to `SWOOLE_BASE` in 5.0.** `SWOOLE_BASE` has no manager process -- workers accept directly. `SWOOLE_THREAD` (6.0+) requires ZTS PHP.

### Dispatch modes

| Mode | Constant | Use for |
|---|---|---|
| 1 | `SWOOLE_DISPATCH_ROUND` | Stateless, async only; `onConnect`/`onClose` suppressed |
| 2 | `SWOOLE_DISPATCH_FDMOD` | Stateful TCP (fd % worker_num) |
| 3 | `SWOOLE_DISPATCH_IDLE_WORKER` | Preemptive to idle worker -- recommended for HTTP |
| 4 | `SWOOLE_DISPATCH_IPMOD` | Sticky by client IP |
| 5 | `SWOOLE_DISPATCH_UIDMOD` | Sticky by `$server->bind($fd, $uid)` |
| 7 | `SWOOLE_DISPATCH_STREAM` | Workers `accept` themselves |
| 9 | `SWOOLE_DISPATCH_CO_REQ_LB` | Coroutine request LB (best for stateless HTTP) |

### Response gotchas

- `end()` can only be called **once**.
- `status()`/`header()`/`cookie()` must be called **before** the first `write()` or `end()`.
- `write()` switches to chunked transfer and disables compression.

### Task workers

```php
// Fire-and-forget
$server->task(['job' => 'resize', 'file' => $file]);

// Concurrent batch (requires task_enable_coroutine=true)
$results = $server->taskCo([['job' => 'a'], ['job' => 'b']], 5.0);
```

**Caveat**: `task()`/`taskwait()` only callable from event workers. Prefer `taskCo()` when `task_enable_coroutine=true`.

### WebSocket server

WS-specific methods:

```php
public function push(int $fd, Frame|string $data, int $opcode = WEBSOCKET_OPCODE_TEXT, int $flags = WEBSOCKET_FLAG_FIN): bool;
public function isEstablished(int $fd): bool;    // NOT exists() -- use this for WS
public function disconnect(int $fd, int $code = WEBSOCKET_CLOSE_NORMAL, string $reason = ''): bool;
public function ping(int $fd, string $data = ''): bool;
```

Use `$server->isEstablished($fd)` (NOT `exists()`) to check WS connection. `onHandShake` gives full control of the handshake; `onOpen` fires after the built-in one.

**Broadcast pattern**:

```php
foreach ($server->connections as $fd) {
    if ($server->isEstablished($fd)) $server->push($fd, $payload);
}
```

### TCP packet framing

`open_length_check` + `package_length_type` delivers complete packets to `onReceive`. EOF variant: `'open_eof_split' => true, 'package_eof' => "\r\n"`.

### Graceful reload

- `$server->reload()` -> workers finish in-flight, exit, get replaced.
- `reload_async=true`: old workers keep running current coroutines. `onWorkerExit` fires when done.
- **Smooth reload only picks up files `require`'d inside `onWorkerStart`**. Files required before `start()` stay cached forever.
- If `opcache.validate_timestamps=0`, add `opcache_reset()` at the top of `onWorkerStart`.

---

## 6. Process and Process\Pool

### `Swoole\Process`

```php
final class Swoole\Process {
    public int $pipe;
    public int $pid;
    public int $id;

    public function __construct(
        callable $callback,
        bool $redirectStdinStdout = false,
        int $pipeType = SOCK_DGRAM,    // 0=none, 1=SOCK_STREAM, 2=SOCK_DGRAM
        bool $enableCoroutine = false
    );

    public function start(): bool|int;
    public function write(string $data): int|false;
    public function read(int $size = 8192): string|false;

    // Sysv message queue IPC
    public function useQueue(int $key = 0, int $mode = 2, int $capacity = -1): bool;
    public function push(string $data): bool;
    public function pop(int $size = 65536): string|false;

    public function exportSocket(): Swoole\Coroutine\Socket|false;
    public function name(string $name): bool;
    public function exit(int $exitCode = 0): void;

    public static function wait(bool $blocking = true): array|false;
    public static function signal(int $signalNo, ?callable $callback = null): bool;
    public static function kill(int $pid, int $signalNo = SIGTERM): bool;
    public static function daemon(bool $nochdir = true, bool $noclose = true, array $pipes = []): bool;
}
```

`$enableCoroutine = true` makes the callback run inside a coroutine scheduler. Signal handling: use `Swoole\Process::signal()`, not `pcntl_signal()`.

```php
Swoole\Process::signal(SIGTERM, fn() => $server->shutdown());
Swoole\Process::signal(SIGINT,  fn() => $server->shutdown());
```

### `Swoole\Process\Pool`

```php
final class Swoole\Process\Pool {
    public function __construct(
        int $workerNum,
        int $ipcType = SWOOLE_IPC_NONE,   // 0=none, 1=UNIXSOCK, 2=MSGQUEUE, 3=SOCKET
        int $msgqueueKey = 0,
        bool $enableCoroutine = false
    );
    public function set(array $settings): void;
    public function on(string $event, callable $cb): bool;
    public function getProcess(int $workerId = -1): Process|false;
    public function listen(string $host, int $port = 0, int $backlog = 2048): bool;
    public function write(string $data): bool;
    public function sendMessage(string $data, int $dstWorkerId): bool;
    public function start();
    public function stop(): void;
    public function shutdown(): bool;
}
```

Minimal supervised worker pool -- bring your own protocol, no reactor. Good for "run my callable across N processes with shared TCP socket". Events: `WorkerStart`, `WorkerStop`, `Message`, `Start`, `Shutdown`.

**Pool vs Server**: `Pool` has no `onConnect`/`onReceive`/HTTP/WS. `Server` is the full reactor + protocol helpers.

---

## 7. Shared memory -- Table, Atomic, Lock

### `Swoole\Table`

Mmap'd shared hash table -- the only way to share state across workers. Per-row spinlocks + CAS.

```php
final class Swoole\Table implements Iterator, Countable {
    public const TYPE_INT    = 1;
    public const TYPE_FLOAT  = 2;
    public const TYPE_STRING = 3;

    public function __construct(int $tableSize, float $conflictProportion = 0.2);
    public function column(string $name, int $type, int $size = 0): bool;
    public function create(): bool;
    public function destroy(): bool;

    public function set(string $key, array $value): bool;
    public function get(string $key, ?string $field = null): mixed;
    public function exists(string $key): bool;
    public function del(string $key): bool;
    public function count(): int;
    public function incr(string $key, string $column, int|float $incrby = 1): int|float;
    public function decr(string $key, string $column, int|float $incrby = 1): int|float;
    public function getSize(): int;
    public function getMemorySize(): int;
    public function stats(): array|false;
}
```

```php
$table = new Swoole\Table(8192);
$table->column('name',  Swoole\Table::TYPE_STRING, 64);
$table->column('age',   Swoole\Table::TYPE_INT);
$table->column('score', Swoole\Table::TYPE_FLOAT);
$table->create();  // MUST create before $server->start()

$server->table = $table;  // attach so workers can reach it
```

**Gotchas**:
- `TYPE_STRING` columns have fixed byte length; overflow silently truncated.
- Size ~30% over peak for collision chains.
- In 5.0+, `Table` **no longer implements `ArrayAccess`** -- use `set()`/`get()`.

### Atomic / Lock

`Swoole\Atomic` -- shared-memory counters (32-bit unsigned; `Atomic\Long` for 64-bit signed). `wait()`/`wakeup()` are futex-style and **block the entire process** -- use `Channel` in event workers instead.

`Swoole\Lock` -- **process-level, NOT coroutine-safe**. If a coroutine holds the lock and yields, deadlock. Use `Channel(1)` for coroutine-safe mutex. In 6.1+: API unified to `lock($op = LOCK_EX, $timeout = -1)`, `unlock()`. `lockwait()`/`trylock()` removed.

---

## 8. Coroutine clients

### `Swoole\Coroutine\Http\Client`

```php
new Swoole\Coroutine\Http\Client(string $host, int $port, bool $ssl = false)
// $host = IP, domain (async DNS), or unix://tmp/foo.sock. Do NOT pass http:// prefix.
```

**Properties**: `$errCode`, `$errMsg`, `$statusCode` (negative = network issue), `$body`, `$headers`, `$cookies`, `$set_cookie_headers`.

Negative statusCode constants: `-1` CONNECT_FAILED, `-2` REQUEST_TIMEOUT, `-3` SERVER_RESET, `-4` SEND_FAILED.

```php
set(array $options): void
setMethod(string $method): void
setHeaders(array $headers): void
setCookies(array $cookies): void
setData(string|array $data): void
addFile(string $path, string $name, ?string $mime = null, ?string $filename = null, int $offset = 0, int $length = 0): void
addData(string $data, string $name, ?string $mime = null, ?string $filename = null): void
get(string $path): bool
post(string $path, mixed $data): bool
download(string $path, string $filename, int $offset = 0): bool
upgrade(string $path): bool        // websocket handshake
push(mixed $data, int $opcode = WEBSOCKET_OPCODE_TEXT, int $flags = WEBSOCKET_FLAG_FIN): bool
recv(float $timeout = 0): Frame|false
close(): bool
```

**Functional shortcuts** (`Swoole\Coroutine\Http` namespace, >= 4.6.4):

```php
use function Swoole\Coroutine\Http\{get, post, request};
$resp = get('https://httpbin.org/get?hello=world');
```

### `Swoole\Coroutine\Socket`

Low-level coroutine-native socket for custom protocols.

```php
new Swoole\Coroutine\Socket(int $domain, int $type, int $protocol);

bind(string $address, int $port = 0): bool
listen(int $backlog = 0): bool
accept(float $timeout = 0): Co\Socket|false
connect(string $host, int $port = 0, float $timeout = 0): bool
send(string $data, float $timeout = 0): int|false
sendAll(string $data, float $timeout = 0): int|false
recv(int $length = 65536, float $timeout = 0): string|false
recvAll(int $length, float $timeout = 0): string|false
recvPacket(float $timeout = 0): string|false   // uses framing from setProtocol()
recvLine(int $length = 65536, float $timeout = 0): string|false
setProtocol(array $settings): bool              // same framing options as Server->set()
checkLiveness(): bool
close(): bool
```

### Removed coroutine clients (6.0+)

**Removed entirely**: `Swoole\Coroutine\MySQL`, `Coroutine\Redis`, `Coroutine\PostgreSQL`. Use hooked PDO/ext-redis with `SWOOLE_HOOK_ALL`. Never generate code using these for 6.x.

---

## 9. Hooked PDO and curl

### Hooked PDO

With `SWOOLE_HOOK_ALL`, native `PDO` becomes coroutine-aware transparently.

**One-connection-per-coroutine rule** -- a single PDO handle is **not safe** across concurrent coroutines. The socket is bound to one coroutine; interleaved use corrupts the wire protocol. Use `PDOPool`.

**Persistent connections are NOT compatible** -- always `PDO::ATTR_PERSISTENT => false`.

**SQLite caveat**: `pdo_sqlite` hook requires SQLite in serialized/multi-threaded mode. Since 6.1.5, `PDO::sqliteCreateAggregate/Collation/Function` removed in coroutine mode.

### Hooked curl

**Use `SWOOLE_HOOK_NATIVE_CURL`** (included in `SWOOLE_HOOK_ALL`), not legacy `SWOOLE_HOOK_CURL`. **Guzzle and Symfony HttpClient work transparently**:

```php
Co\run(function () {
    $guzzle = new \GuzzleHttp\Client();
    $resp   = $guzzle->get('https://httpbin.org/get');  // coroutine-aware, no changes needed
});
```

---

## 10. Connection pooling

`swoole/library` ships with ext-swoole (auto-loaded via `swoole.enable_library=On`).

### Canonical pattern -- "put it back or leak"

**A pool connection that isn't returned is lost forever.** Always `try/finally`:

```php
use Swoole\Database\{PDOConfig, PDOPool};

$pool = new PDOPool(
    (new PDOConfig())
        ->withDriver('mysql')
        ->withHost('127.0.0.1')
        ->withPort(3306)
        ->withDbname('app')
        ->withCharset('utf8mb4')
        ->withUsername('app')
        ->withPassword('secret')
        ->withOptions([PDO::ATTR_EMULATE_PREPARES => false]),
    size: 32,
);

go(function () use ($pool) {
    $pdo = $pool->get();
    try {
        $stmt = $pdo->prepare('SELECT id, email FROM users WHERE id = :id');
        $stmt->execute(['id' => 1]);
    } finally {
        $pool->put($pdo);  // MUST return, even on exception
    }
});

// Or cleaner with defer:
go(function () use ($pool) {
    $conn = $pool->get();
    Co::defer(fn() => $pool->put($conn));
    // ... use $conn ...
});
```

**Pool notes**:
- `get(-1)` blocks forever; `get($timeout)` returns `false` on timeout.
- `put(null)` signals broken connection -- pool decrements count, rebuilds next `get()`.
- `PDOProxy` auto-reconnects on lost-connection exceptions outside transactions.
- **Transactions must begin and commit on the same connection.** Don't return mid-transaction.
- `RedisPool` / `MysqliPool` follow the same shape.
- **SQLite restriction**: `PDOPool` rejects `:memory:` and empty DB names.

### Channel-as-pool pattern

```php
final class GrpcPool
{
    private Channel $channel;

    public function __construct(
        private readonly \Closure $factory,
        private readonly int $size = 32,
    ) {
        $this->channel = new Channel($size);
        for ($i = 0; $i < $size; $i++) {
            $this->channel->push(($this->factory)());
        }
    }

    public function get(float $timeout = -1): object
    {
        $conn = $this->channel->pop($timeout);
        if ($conn === false) throw new \RuntimeException('pool exhausted or closed');
        return $conn;
    }

    public function put(object $conn): void { $this->channel->push($conn); }
}
```

---

## 11. Pitfalls catalog

### Never block inside a coroutine

These stall the whole worker (not just one coroutine):
- `mysql`/`mysqli`/`pdo_mysql` **without** `mysqlnd + hooks`
- `sleep()` without `SWOOLE_HOOK_SLEEP`
- `curl_*` without `SWOOLE_HOOK_NATIVE_CURL`
- `file_*`/`fread`/`fwrite` without `SWOOLE_HOOK_FILE`
- Any extension bypassing PHP streams

**Fix**: `SWOOLE_HOOK_ALL` at bootstrap. Exclude flags only when a library fights the scheduler (e.g. `SWOOLE_HOOK_ALL ^ SWOOLE_HOOK_TCP` for PHPMailer raw SMTP).

### Sharing a connection across coroutines

Swoole error: `"redis client has already been bound to another coroutine"`. PDO/Redis/socket handles are bound to the first coroutine. **Always pool, one checkout per coroutine.**

**Multiple processes must not share connections either.** Create per-worker pools in `onWorkerStart`, not program-global.

### `pcntl_*` is forbidden

`pcntl_fork`/`pcntl_signal`/`pcntl_waitpid` conflict with Swoole's `signalfd`. Use `Swoole\Process`, `Process::signal()`, `Coroutine\System::waitSignal()`.

### `go()` in `onStart` / `onManagerStart`

`onStart` runs in the master. Coroutine APIs there can conflict with `dispatch_func` and `package_length_func`. Put bootstrap in **`onWorkerStart`**.

### Channel deadlocks

`Channel::pop()` with default `timeout = -1` hangs forever if all producers exited and channel is empty. **Always pass a timeout**, or pair with `$channel->close()`.

### Cooperative != concurrent-safe

Read-modify-write across an I/O boundary is a data race:

```php
// WRONG
$count = $table->get('k', 'count');
$count++;  // yield possible if I/O runs
$table->set('k', ['count' => $count]);

// RIGHT
$table->incr('k', 'count');
```

### Exception handling across coroutines

Exceptions **cannot** cross coroutine boundaries. `go(fn() => throw new X)` inside an outer `try/catch` does nothing. Every top-level `go()` closure needs its own `try/catch`.

### Stashing `$this` in context

`Coroutine::getContext()['controller'] = $this` keeps the controller alive -- usually a leak. Stash only data.

### Framework compatibility

| Framework | Status |
|---|---|
| Hyperf | **Swoole-native**, production-ready |
| Webman | Fast, Swoole driver available |
| Imi | Swoole-native |
| Laravel Octane | Works, but use `app->scoped()` not `singleton()` for request-adjacent services |
| Slim / Mezzio | Work via PSR-15 bridges, not Swoole-aware |
| Phalcon | **Incompatible** -- C extension, globals-heavy |

---

## 12. Production tuning

### Worker sizing

| Setting | Guidance |
|---|---|
| `worker_num` | Default `swoole_cpu_num()`. Async/coroutine: 1-4x cores |
| `task_worker_num` | `(tasks/sec / tasks/worker/sec)`. Default 0 |
| `max_request` | Respawn after N requests. Mitigates leaks. **No effect in SWOOLE_BASE.** 10k-50k reasonable |
| `max_wait_time` | Drain timeout on reload/shutdown. 30-60s for HTTP |
| `reload_async` | **Set `true`** for graceful coroutine reload |
| `max_coroutine` | Per-worker cap, default 100000 |

### Buffers

| Setting | Default | Note |
|---|---|---|
| `package_max_length` | 2 MB | Max packet / HTTP POST body |
| `buffer_output_size` | 2 MB | Max `send()` payload. Memory = `worker_num x size` |
| `socket_buffer_size` | 2 MB | Per-connection send buffer |

### SSL / HTTP/2 / compression

- `ssl_cert_file` / `ssl_key_file`: PEM. Set `SWOOLE_SSL` socket flag.
- `open_http2_protocol`: requires `--enable-http2`.
- `http_compression`: gzip/brotli/zstd (6.0+ added zstd).

### Daemon / logs

- **`daemonize`**: `false` under systemd/Docker/k8s. Only `true` from interactive shell.
- **`log_file`**: use absolute paths (CWD changes after daemonize).
- `SIGRTMIN` reopens the log after logrotate.
- `log_rotation`: `SWOOLE_LOG_ROTATION_DAILY|HOURLY|MONTHLY`.

### Kernel tuning (Linux)

```
ulimit -n 262140
net.core.somaxconn = 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_max_syn_backlog = 81920
net.core.wmem_max = 16777216
net.core.rmem_max = 16777216
net.unix.max_dgram_qlen = 100
```

Do **not** set `net.ipv4.tcp_tw_recycle = 1` -- removed in Linux 4.12, unsafe with NAT.

---

## 13. Testing

### PHPUnit entry point

```php
public function testAsync(): void
{
    $result = null;
    \Swoole\Coroutine\run(function () use (&$result) {
        $client = new \Swoole\Coroutine\Http\Client('httpbin.org', 443, true);
        $client->get('/ip');
        $result = $client->body;
    });
    self::assertNotNull($result);
}
```

### Resetting state

Workers are resident -- static state persists. In `setUp()`: reset singletons, run each test in its own `Co\run` (fresh context), `Timer::clearAll()`. In `tearDown()` assert `Coroutine::stats()['coroutine_num'] === 0` -- non-zero means a leak.

### IDE stubs

```bash
composer require --dev swoole/ide-helper
```

PHPStan: add `vendor/swoole/ide-helper/src/swoole/constants.php` to `bootstrapFiles`.

---

## 14. Swoole 6.x version notes

### 6.0 (2024-12)

- **Removed**: `Coroutine\MySQL`, `Coroutine\Redis`, `Coroutine\PostgreSQL`. Use hooked PDO.
- Requires PHP 8.1+.
- **Multi-threading**: `SWOOLE_THREAD`, `Swoole\Thread`, `Thread\Map/ArrayList/Queue/Lock/Barrier/Atomic`. Requires ZTS + `--enable-swoole-thread`.
- **io_uring** for file AIO: `--enable-iouring`.
- `Swoole\Async\Client` (non-blocking TCP/UDP/Unix).
- zstd HTTP compression.
- Non-blocking reentrant coroutine mutex.

### 6.1 (2025-10)

- **llhttp** replaces `http_parser`.
- **Lock API unified**: `lock($op = LOCK_EX, $timeout = -1)`, `unlock()`. `lockwait()`/`trylock()` **removed**.
- Coroutine cancellation gains `$throwException` -> throws `CanceledException`.
- WebSocket: `disconnect()`, `ping()`, fragmented messages.
- Runtime hooks can **only** be set in main thread, before child threads.
- 6.1.5: `PDO::sqliteCreateAggregate/Collation/Function` removed from coroutine mode.
- 6.1.6: auto-strips `SWOOLE_HOOK_SOCKETS` if ext-sockets not loaded.
- 6.1.7: hooked `pdo_pgsql` gained timeout.

### 6.2 (2026-04)

- Requires **PHP 8.2+**. Supports up to PHP 8.5.
- Coroutine **FTP** (`--enable-swoole-ftp`) and **SSH** (`--with-swoole-ssh2`) clients.
- `SWOOLE_HOOK_PDO_FIREBIRD`, `SWOOLE_HOOK_MONGODB`, `SWOOLE_HOOK_NET_FUNCTION`.
- `Coroutine::setTimeLimit()` -- per-coroutine timeout.
- HTTP over io_uring sockets: `--enable-uring-socket`.
- `--enable-openssl` removed (always on). `liburing >= 2.8` required.
- `Coroutine::cancel()` cancels in-flight io_uring ops.

### Build flags cheatsheet

```bash
./configure --enable-swoole \
  --enable-sockets \
  --enable-swoole-thread \       # requires ZTS
  --enable-swoole-curl \          # SWOOLE_HOOK_NATIVE_CURL
  --enable-swoole-pgsql \
  --enable-swoole-sqlite \
  --with-swoole-firebird \        # 6.2+
  --with-swoole-ssh2 \            # 6.2+
  --enable-swoole-ftp \           # 6.2+
  --enable-iouring \              # Linux io_uring
  --enable-uring-socket \         # 6.2+
  --enable-brotli \
  --enable-zstd \                 # 6.0+
  --enable-cares                  # c-ares DNS
```

### swoole/library version alignment

`swoole/library` is bundled into ext-swoole and auto-loaded. Composer releases are tagged for 6.1 and 6.2 (latest v6.2.3 as of 2026-09), so `composer require swoole/library` resolves to 6.2.x. Treat the extension as source of truth for library classes in production.

---

## Quick reference -- canonical program skeleton

```php
<?php
declare(strict_types=1);

use Swoole\Coroutine;
use Swoole\Coroutine\{Barrier, Channel};
use function Swoole\Coroutine\run;

Coroutine::set([
    'hook_flags'            => SWOOLE_HOOK_ALL,
    'max_coroutine'         => 10_000,
    'socket_timeout'        => 5,
    'enable_deadlock_check' => true,
]);

run(function (): void {
    $jobs    = new Channel(32);
    $results = new Channel(32);
    $barrier = Barrier::make();

    for ($w = 0; $w < 4; $w++) {
        go(function () use ($jobs, $results, $barrier): void {
            while (($job = $jobs->pop()) !== false) {
                $results->push(['job' => $job, 'pid' => getmypid()]);
            }
        });
    }

    go(function () use ($jobs, $barrier): void {
        foreach (range(1, 20) as $i) $jobs->push($i);
        $jobs->close();
    });

    go(function () use ($results, $barrier): void {
        defer(fn() => $results->close());
        $collected = [];
        for ($i = 0; $i < 20; $i++) {
            $msg = $results->pop(5.0);
            if ($msg === false) break;
            $collected[] = $msg;
        }
        var_dump($collected);
    });

    Barrier::wait($barrier);
});
```

Files in this skill

  • README.md2.3 KB
  • SKILL.md38.6 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…