Pewarta Proxy

πŸ“‹ Server Request & Proxy Logs

Loading active proxy settings...
Developed by Khairul Adha (adhexa.id)
Mengambil log server...

βš™οΈ Pengaturan Proxy Target

Skip to content
phasyncPublic

About

Micro framework for ultra high performance PHP websites with non-blocking IO.

Resources

Contributing

Security policy

Stars

244 stars

Watchers

7 watching

Forks

Latest commit

Β 

History

529 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

phasync

phasync: High-concurrency PHP

πŸ§ͺ CI Latest Stable Version GitHub PHP Version Require codecov

Async PHP that is still just PHP. phasync runs thousands of coroutines in one PHP process, on PHP's own fibers. No promises, no ->then(), no special runtime to install: a function that waits on the network reads top to bottom, returns a value and throws exceptions like any other. Use it in one function of an existing PHP-FPM application, or as the engine of a server that holds tens of thousands of connections.

// In any controller, under PHP-FPM or anywhere else: three HTTP calls at once, not one after another
[$user, $orders, $recommendations] = phasync::run(fn () => array_map(phasync::await(...), [
    phasync::go(fn () => fetch("https://api.example.com/users/$id")),
    phasync::go(fn () => fetch("https://api.example.com/users/$id/orders")),
    phasync::go(fn () => fetch("https://api.example.com/users/$id/recommendations")),
]));

The request takes as long as the slowest of the three calls, not their sum. fetch() is an ordinary function (below); nothing else in the application changes.

Why phasync

  • No colored functions. A coroutine is a plain closure. Code that waits looks exactly like code that doesn't, so async stays an implementation detail of the function that needs it, instead of spreading Promise return types through your codebase.
  • Structured, not fire-and-forget. phasync::run() returns only when every coroutine it started has finished. An exception in a coroutine surfaces where you wait for it. Cancellation and timeouts are exceptions thrown into the coroutine, so finally blocks run.
  • Starts small. One phasync::run() in one function is a complete phasync program. There is no application-wide event loop to adopt first, no framework to switch to.
  • Built for load. One event loop per process, with waiting built on PHP streams: stream_select() out of the box, epoll with the optional phasync extension. Waits on sockets cost no objects of their own, pools (phasync\Util\Pool) reuse database connections across coroutines, and hot paths leave nothing for the garbage collector.
  • Legacy code joins in. With the phasync extension loaded, the code you already have (MySQL through PDO or mysqli, curl and Guzzle, file_get_contents(), http:// streams, DNS lookups, sleep()) waits cooperatively inside coroutines instead of blocking the process. No rewrite.
  • Yours to own. MIT, the extension included, and no dependencies beyond PHP, PSR interfaces and fiber-interop/scope: small enough for you, or your coding agent, to read whole and maintain for a decade. See the Ennerd philosophy.

One library, two ways to run it

Under PHP-FPM or any SAPI: concurrency inside a request. FPM stays exactly as it is, one request per process. Inside the request, phasync::run() overlaps independent work: API calls, queries on separate connections, file and DNS work. The response goes out when the slowest part is done, not when the sum of them is.

Under swerve: an asynchronous server, end to end. Your PSR-15 application stays loaded, and every worker serves thousands of requests at once, each in a Scope of its own. The same code that overlapped three API calls under FPM now also overlaps requests, WebSocket connections and background work.

The path from PHP-FPM to real-time

Each step is useful on its own, and none requires the next.

Step Add What you get
1 phasync/phasync Concurrent I/O inside one request, on your existing FPM setup. Wait with phasync's APIs: phasync::readable(), CurlAwaiter, MySQLiAwaiter.
2 The extension, bundled in phasync's release packages Libraries you did not write (MySQL through PDO or mysqli, Guzzle, curl_exec(), files, http:// streams, DNS) cooperate inside coroutines, unchanged. Epoll instead of stream_select().
3 phasync/swerve A long-running PSR-15 server: the app boots once, each worker serves thousands of connections, streaming bodies, Server-Sent Events, WebSockets. Slim, mini and other PSR-15 frameworks run as they are.
4 phasync/tether Live server-side components over one WebSocket per tab, in the style of Blazor Server and Phoenix LiveView. PHP 8.3.

For your own protocols, phasync/net has TCP, UDP and Unix socket servers and clients on the same loop.

How far that goes

On one 56-thread server, each at its fastest worker count, a hello-world PSR-15 app on swerve served 157,000 to 285,000 requests per second from 64 to about 28,000 connections: in the same range as Go's net/http (162,000 to 317,000) and Node's http module (135,000 to 246,000), and ahead of both at 10,000 connections. A Slim app on swerve served 2 to 3 times what an Express app served on Node. Method and raw results.

Getting started

composer require phasync/phasync

phasync needs PHP 8.2 or later and nothing else. It runs under PHP-FPM, Apache's mod_php, the CLI, and long-running servers.

use phasync\Services\CurlAwaiter;

function fetch(string $url): string
{
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

    // Inside a coroutine this waits without blocking the others; outside, it just runs
    return CurlAwaiter::await($ch);
}

$pages = phasync::run(function () {
    $a = phasync::go(fn () => fetch('https://www.php.net/'));
    $b = phasync::go(fn () => fetch('https://getcomposer.org/'));

    return [phasync::await($a), phasync::await($b)];
});
  • phasync::run($fn) runs $fn as a coroutine, and returns its result once it and every coroutine it started have finished. Called inside a coroutine, it runs $fn in that coroutine and waits only for what $fn started.
  • phasync::go($fn) starts a coroutine and returns it at once; phasync::await($coroutine) waits for its result, or throws its exception.
  • phasync::sleep($seconds) pauses only the current coroutine; phasync::sleep() lets the others run for a moment.
  • phasync::cancel($coroutine) throws a CancelledException into it; every wait accepts a timeout and throws a TimeoutException when it runs out. Without one, a wait waits for as long as it takes.

Frames and scopes

Every run() creates a frame (phasync\Frame): the coroutines started inside it, which it waits for. A failure cancels nothing; once the rest have ended, run() throws the failure, or a phasync\AggregateException of all of them when there are several. The frame can be cancelled, asked to stop without an exception (requestCancellation(), read by isCancellationRequested()), given a deadline, or given a handler for the failures nobody awaited:

phasync::run(function () {
    phasync::frame()->setTimeout(2.0);   // run() throws TimeoutException after 2 seconds
    $a = phasync::go(fn () => fetch('https://www.php.net/'));
    $b = phasync::go(fn () => fetch('https://getcomposer.org/'));

    return [phasync::await($a), phasync::await($b)];
});

Which request or job the code belongs to is a separate thing: the ambient \Scope from fiber-interop/scope. A coroutine keeps the scope it was started in across every wait, so per-request state lives in a WeakMap keyed by Scope::getCurrent(), and phasync::onScopeChange() swaps global state as the loop moves between requests.

Making your own I/O cooperative

Without the extension, a coroutine waits on a stream by asking phasync first:

stream_set_blocking($socket, false);

$data  = fread(phasync::readable($socket), 65536);       // waits until there is something to read
$wrote = fwrite(phasync::writable($socket), $response);  // waits until the socket takes more

Both work outside coroutines too, so a library written this way runs in any PHP program. One coroutine at a time may wait to read a given stream, and one to write to it.

For HTTP, phasync\Services\CurlAwaiter::await($ch) runs a curl handle cooperatively. For MySQL, phasync\Services\MySQLiAwaiter does the same for mysqli's asynchronous queries.

The phasync extension: existing code joins in

The phasync extension is an optional PHP extension. Inside phasync::run(), blocking I/O in code that knows nothing about phasync suspends the coroutine instead of the process, and returns exactly what PHP would have returned, timeouts and warnings included. That covers sockets and TLS, MySQL through mysqli or PDO, curl and Guzzle, pipes and child processes, sleep(), DNS lookups, files and filesystem calls, PostgreSQL and SQLite, and more. Clients with their own network code, such as phpredis, still block.

There is nothing extra to install: release packages of phasync/phasync carry prebuilt binaries under ext/ (PHP 8.3 to 8.5; Linux x86-64 and ARM64 with glibc or musl, macOS). Load one:

  • From the command line: vendor/bin/phasync script.php instead of php script.php. It takes the same arguments as php, and PHP processes the script starts load the extension too.
  • In a CLI script, as the first line: phasync\ext\ensure_loaded(); restarts the process once with the extension.
  • For every SAPI, PHP-FPM included: pie install phasync/ext-phasync builds it and enables it in php.ini. phasync then uses that one, and vendor/bin/phasync and ensure_loaded() leave it be.

The extension is open source under the MIT licence, and versioned with phasync: phasync 2.0.7 needs extension 2.0.7 or a later 2.0.x, and throws a RuntimeException that says what to update when another version is loaded. It bundles ext-scope for the Scope class, and uses an ext-scope or a PHP that already has Scope instead.

The extension waits with select() and switches to epoll at 4096 watched descriptors, so a process can watch any number of sockets: without it, PHP's stream_select() cannot use file descriptors numbered 1024 and up.

phasync behaves the same with and without the extension. The extension only lets more code wait cooperatively and makes waiting cheaper.

Coordinating coroutines

Channels pass values between coroutines. A read waits for a writer, and a write into a full channel waits for a reader:

phasync::run(function () {
    phasync::channel($reader, $writer, 10);

    phasync::go(function () use ($writer) {
        foreach (['a.txt', 'b.txt', 'c.txt'] as $file) {
            $writer->write($file);
        }
        $writer->close();
    });

    foreach ($reader as $file) { // ends when the writer closes
        echo "processing $file\n";
    }
});

WaitGroup waits for a set of coroutines to finish:

use phasync\Util\WaitGroup;

phasync::run(function () {
    $group = new WaitGroup();
    foreach (range(1, 5) as $i) {
        $group->add();
        phasync::go(function () use ($group, $i) {
            try {
                phasync::sleep(0.1 * $i);
            } finally {
                $group->done();
            }
        });
    }
    $group->await();
});

Publishers deliver every message to every subscriber, in order:

phasync::run(function () {
    phasync::publisher($subscribers, $publisher);

    foreach (range(1, 3) as $i) {
        $subscription = $subscribers->subscribe();
        phasync::go(function () use ($subscription, $i) {
            foreach ($subscription as $event) {
                echo "subscriber $i got $event\n";
            }
        });
    }
    $publisher->write('deployed');
    $publisher->close();
});

Pool lends interchangeable resources, such as database connections, to one coroutine at a time, up to a limit:

use phasync\Util\Pool;

$db   = new Pool(fn () => new PDO($dsn, $user, $password), 10);
$rows = $db->use(fn (PDO $pdo) => $pdo->query('SELECT 1')->fetchAll());

For heavy instances, window shrinks the pool to the most instances that were lent at once within that many seconds, and dispose closes what the pool lets go of. The check runs when the pool is borrowed from or released to, not on a timer:

$apps = new Pool(fn () => bootApplication(), 8, window: 30, dispose: fn ($app) => $app->flush());

Also in phasync\Util: RateLimiter, Synchronized (a lock per coroutine) and StringBuffer (a fast byte buffer for protocol parsers).

Documentation

Compared with other async PHP

  • Swoole and OpenSwoole are PHP extensions with their own server and runtime model; many of their features require it. phasync is a Composer library on standard PHP; the extension is optional, and an application written for phasync runs the same with or without it.
  • ReactPHP and AMPHP are async-first ecosystems: code that waits uses their APIs (ReactPHP's promises; AMPHP's futures, which read sequentially on fibers) and their own libraries, such as an async HTTP client or MySQL driver in place of curl or PDO. phasync is closer to Go's model: plain functions, and with the extension the libraries you already use wait cooperatively.

Contributing

Upgrading from 1.x: UPGRADING.md. docs/SEMANTICS.md describes how every part is meant to behave, and the tests in tests/Characterization pin how it behaves today. Tests, documentation and bug reports are welcome.

License

phasync and its extension are open-sourced software licensed under the MIT license.

About

Micro framework for ultra high performance PHP websites with non-blocking IO.

Resources

Contributing

Security policy

Stars

244 stars

Watchers

7 watching

Forks

Releases

Packages

Used by

Contributors

Languages