Asynchrony — never block the event loop¶
Flyokai is cooperative. Every HTTP handler, CLI command, request handler, and DI lifecycle hook runs inside a fiber managed by Revolt's event loop. Block one fiber and you stall everything.
The two rules¶
- Never use a blocking system call. No
sleep(),usleep(),file_get_contents()for remote URLs,mysqli_query()withoutMYSQLI_ASYNC,curl_exec()without curl-multi,socket_*without non-blocking flags. - Use Amp / Revolt primitives instead. They suspend the current fiber, let the loop run other work, and resume when the I/O is ready.
The conversion table¶
| Blocking | Async equivalent |
|---|---|
sleep(N) |
Amp\delay(N) |
usleep(N) |
Amp\delay(N / 1_000_000) |
file_get_contents('https://…') |
(new Amp\Http\Client\HttpClientBuilder())->build()->request(new Request('https://…'))->getBody()->buffer() |
file_get_contents('/path') |
Amp\File\read('/path') |
file_put_contents('/path', $data) |
Amp\File\write('/path', $data) |
flock($handle, LOCK_EX) |
Flyokai\AmpMate\ampFlock($handle, LOCK_EX) (cooperative retry) |
mysqli_query($link, $sql) |
the framework Adapter — uses MYSQLI_ASYNC + EventLoop::onMysqli() under the hood |
PDO::query() |
the framework Adapter — uses worker pools under the hood (AsyncPdoConnectionPool) |
curl_exec() |
(new HttpClientBuilder())->build()->request(...) |
socket_read() / socket_write() |
EventLoop::onReadable($fd, …) / EventLoop::onWritable($fd, …) |
Concurrency idioms¶
Run two operations in parallel¶
use function Amp\async;
use function Amp\Future\await;
[$a, $b] = await([
async(fn() => $repoA->load($id)),
async(fn() => $repoB->load($id)),
]);
Apply backpressure with a semaphore¶
use Amp\Sync\LocalSemaphore;
$sem = new LocalSemaphore(8); // max 8 concurrent
foreach ($urls as $url) {
$lock = $sem->acquire();
async(function () use ($url, $lock) {
try { processUrl($url); }
finally { $lock->release(); }
});
}
Time out an operation¶
Stream-process a large dataset¶
Use flyokai/amp-data-pipeline:
use Flyokai\AmpDataPipeline\{ArraySource, ProcessorComposition};
$pipeline = new ProcessorComposition([
new LoadProcessor(),
new TransformProcessor(),
new SaveProcessor(),
]);
$pipeline->setSource(new ArraySource($input));
$pipeline->run();
Each processor independently controls its fiber count (setConcurrency) and output buffer (setBufferSize).
Suspension API (low level)¶
When you need to integrate with a callback-based API:
use Revolt\EventLoop;
$suspension = EventLoop::getSuspension();
someAsyncApi(onComplete: fn ($result) => $suspension->resume($result),
onError: fn ($e) => $suspension->throw($e));
$result = $suspension->suspend(); // current fiber pauses; loop runs other work
The MYSQLI_ASYNC integration in flyokai/laminas-db-driver-async is built on this primitive (EventLoop::onMysqli() + suspension).
Things that look async but aren't¶
sleep()inside aClosurepassed toEventLoop::defer()— the closure still runs blockingly when its tick comes; it just runs later. The loop is still stuck while it sleeps.file_get_contents()against a slow disk — magnetic disks under contention can stall fibers for seconds. On hot paths, preferAmp\File.- Tight loops without yielding — even if you call
Amp\delay(0)periodically, busy CPU work blocks the loop. Move heavy CPU work toAmp\Parallel\Worker.
Checking your code¶
To find blocking calls, grep for the usual suspects:
grep -rn -E '\b(sleep|usleep|fread|fgets|file_get_contents|file_put_contents|fopen|fclose|curl_exec|socket_(read|write))\b' --include='*.php' src/
Whitelist legitimate cases (CLI bootstrap before the loop runs, etc.).
Fiber vs {main}¶
EventLoop::run()is callable only from{main}— the top-level script that started the loop.- From inside a fiber (any handler / callback), use the
SuspensionAPI instead. CallingEventLoop::run()from a fiber re-enters the loop and corrupts state.
Error handling inside a fiber¶
try / catchworks as you expect — exceptions propagate up the fiber's call stack.- Unhandled exceptions in deferred callbacks go to
EventLoop::setErrorHandler(). If that handler throws, the loop stops immediately. Amp\Future::await()re-throws the future's error in the calling fiber.
Diagnostics¶
- Profiler:
Flyokai\Misc\ProfilerFacade::start('label')/stop('label')— no-op when disabled. - Trace driver: set
REVOLT_DRIVER_DEBUG_TRACE=1to wrap the event-loop driver in a tracer that records callback registration sites. - Cluster IPC: cluster workers report via
WatcherMessageProcessor; pipe the log to the coordinator for correlated tracing.