Revolt is a rock-solid event loop for concurrent PHP applications.

Related tags

Event event-loop
Overview

Revolt

Revolt is a rock-solid event loop for concurrent PHP applications.

Motivation

Traditionally, PHP has a synchronous execution flow, doing one thing at a time. If you query a database, you send the query and wait for the response from the database server in a blocking manner. Once you have the response, you can start doing the next thing.

Instead of sitting there and doing nothing while waiting, we could already send the next database query, or do an HTTP call to an API. Making use of the time we usually spend on waiting for I/O can speed up the total execution time.

A single scheduler – also called event loop – is required to allow for cooperative multitasking, which this package provides.

Installation

This package can be installed as a Composer dependency.

composer require revolt/event-loop

This installs the basic building block for building concurrent applications in PHP.

Documentation

Documentation can be found on revolt.run.

Requirements

This package requires at least PHP 8.0. To take advantage of Fibers, either ext-fiber or PHP 8.1+ is required.

Optional Extensions

Extensions are only needed if your application necessitates a high numbers of concurrent socket connections, usually this limit is configured up to 1024 file descriptors.

Examples

Examples can be found in the ./examples directory of this repository.

Versioning

revolt/event-loop follows the semver semantic versioning specification.

License

The MIT License (MIT). Please see LICENSE for more information.

Revolt is the result of combining years of experience of amphp's and ReactPHP's event loop implementations.

Comments
  • Memory leak if callbacks suspend

    Memory leak if callbacks suspend

    reproduce:

    1. create script.php with the following content:
    <?php
    
    declare(strict_types=1);
    
    use Revolt\EventLoop;
    
    require __DIR__ . '/../../vendor/autoload.php';
    
    $write_line = static fn(string $m, ...$args) => printf($m . "\n", ...$args);
    
    $write_line('Server is listening on http://localhost:3030');
    
    // Error reporting suppressed since stream_socket_server() emits an E_WARNING on failure (checked below).
    $server = @stream_socket_server('tcp://localhost:3030', $errno, $_, flags: STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, context: stream_context_create([
        'socket' => [
            'ipv6_v6only' => true,
            'so_reuseaddr' => false,
            'so_reuseport' => false,
            'so_broadcast' => false,
            'tcp_nodelay' => false,
        ]
    ]));
    if (!$server || $errno) {
        throw new RuntimeException('Failed to listen localhost 3030.', $errno);
    }
    
    $watcher = null;
    EventLoop::unreference(EventLoop::onSignal(SIGINT, static function () use ($server, &$watcher) {
        EventLoop::cancel((string) $watcher);
        fclose($server);
    }));
    
    $watcher = EventLoop::onReadable($server, function ($watcher, $resource) {
        $stream = @stream_socket_accept($resource, timeout: 0.0);
        if (false === $stream) {
            EventLoop::cancel($watcher);
    
            return;
        }
    
        stream_set_read_buffer($stream, 0);
        stream_set_blocking($stream, false);
        $suspension = EventLoop::createSuspension();
        $watcher = EventLoop::onReadable($stream, function () use ($suspension) {
            $suspension->resume();
        });
        $suspension->suspend();
        EventLoop::cancel($watcher);
        $request = stream_get_contents($stream);
        $suspension = EventLoop::createSuspension();
        $watcher = EventLoop::onWritable($stream, function () use ($suspension) {
            $suspension->resume();
        });
        $suspension->suspend();
        EventLoop::cancel($watcher);
        fwrite($stream, "HTTP/1.1 200 OK\n");
        $suspension = EventLoop::createSuspension();
        $watcher = EventLoop::onWritable($stream, function () use ($suspension) {
            $suspension->resume();
        });
        $suspension->suspend();
        EventLoop::cancel($watcher);
        fwrite($stream, "Server: TCP Server\n");
        $suspension = EventLoop::createSuspension();
        $watcher = EventLoop::onWritable($stream, function () use ($suspension) {
            $suspension->resume();
        });
        $suspension->suspend();
        EventLoop::cancel($watcher);
        fwrite($stream, "Connection: close\n");
        $suspension = EventLoop::createSuspension();
        $watcher = EventLoop::onWritable($stream, function () use ($suspension) {
            $suspension->resume();
        });
        $suspension->suspend();
        EventLoop::cancel($watcher);
        fwrite($stream, "Content-Type: text/html; charset=utf-8\n\n");
        $suspension = EventLoop::createSuspension();
        $watcher = EventLoop::onWritable($stream, function () use ($suspension) {
            $suspension->resume();
        });
        $suspension->suspend();
        EventLoop::cancel($watcher);
        fwrite($stream, "<h3>Hello, World!</h3>");
        $suspension = EventLoop::createSuspension();
        $watcher = EventLoop::onWritable($stream, function () use ($suspension) {
            $suspension->resume();
        });
        $suspension->suspend();
        EventLoop::cancel($watcher);
        fwrite($stream, "<pre><code>" . htmlentities($request) . "</code></pre>");
        $suspension = EventLoop::createSuspension();
        $watcher = EventLoop::onWritable($stream, function () use ($suspension) {
            $suspension->resume();
        });
        $suspension->suspend();
        EventLoop::cancel($watcher);
        fwrite($stream, sprintf('memory usage: %dMiB<br />', round(memory_get_usage() / 1024 / 1024, 1)));
        $suspension = EventLoop::createSuspension();
        $watcher = EventLoop::onWritable($stream, function () use ($suspension) {
            $suspension->resume();
        });
        $suspension->suspend();
        EventLoop::cancel($watcher);
        fwrite($stream, sprintf('peak memory usage: %dMiB<br />', round(\memory_get_peak_usage() / 1024 / 1024, 1)));
        @fclose($stream);
    });
    
    EventLoop::run();
    
    $write_line('');
    $write_line('Goodbye 👋');
    
    1. run php script.php

    memory keeps going up with each request ( noticeable with a request batch of 10k )

    It seems suspensions are not being destructed properly and taking space in memory?

    I'm not really sure what is happening, i first noticed the leak in https://github.com/azjezz/hack-php-async-io/blob/main/src/server.php, and went on simplifying the code to isolate it, until i reached this step, where no PSL code was involved, which means the leak is happening in revolt, or I'm doing something wrong.

    bug 
    opened by azjezz 16
  • fix(driver): use `mixed` as a return type for `EventDriver::getHandle()` and `EvDriver::getHandle()`

    fix(driver): use `mixed` as a return type for `EventDriver::getHandle()` and `EvDriver::getHandle()`

    when one of these extensions is not installed, and isSupported() gets called from the driver factory, you will receive the following error:

    > php test.php
    PHP Fatal error:  Could not check compatibility between Revolt\EventLoop\Driver\EventDriver::getHandle(): EventBase and Revolt\EventLoop\Driver::getHandle(): mixed, because class EventBase is not available in /home/azjezz/Projects/php-standard-library/vendor/revolt/event-loop/src/EventLoop/Driver/EventDriver.php on line 150
    
    Fatal error: Could not check compatibility between Revolt\EventLoop\Driver\EventDriver::getHandle(): EventBase and Revolt\EventLoop\Driver::getHandle(): mixed, because class EventBase is not available in /home/azjezz/Projects/php-standard-library/vendor/revolt/event-loop/src/EventLoop/Driver/EventDriver.php on line 150
    

    This is somehow PHP's fault as regardless if the type T exist or not, compatibility check for return type from mixed to T should pass, but fixing it here is easier :)

    opened by azjezz 10
  • Fiber stack protect failed

    Fiber stack protect failed

    I catch error in my production server in callback method set_exception_handler(***):

    Fiber stack protect failed: mprotect failed: Cannot allocate memory (12), /project/vendor/revolt/event-loop/src/EventLoop/Internal/AbstractDriver.php:495
    

    In php.ini:

    ; Maximum amount of memory a script may consume
    ; https://php.net/memory-limit
    memory_limit = -1
    

    Available memory in server 230Gb.

    question 
    opened by PNixx 8
  • Bad file descriptor with

    Bad file descriptor with "ev" driver when trying to listen on socket (Windows)

    This code:

    $socket = \Amp\Socket\listen('tcp://127.0.0.1:49000', null);
    $socket->accept();
    

    Throws this warning on Windows platform:

    Warning: EvLoop::run(): Libev error(9): Bad file descriptor in ...\vendor\revolt\event-loop\src\EventLoop\Driver\EvDriver.php on line 164
    

    php -v

    PHP 8.1.1 (cli) (built: Dec 15 2021 10:36:13) (NTS Visual C++ 2019 x64)
    Copyright (c) The PHP Group
    Zend Engine v4.1.1, Copyright (c) Zend Technologies
    

    php --ri ev

    ev
    
    Ev support => enabled
    Debug support => disabled
    Version => 1.1.6RC1
    

    Package versions:

    revolt/event-loop: v0.1.1
    amphp/socket: v2.x-dev
    
    opened by codercms 8
  • Exceptions for closed resources

    Exceptions for closed resources

    Drivers result in different exceptions/errors making it harder for end-user to handle.

    an example would be EventLoop::onReadable/EventLoop::onWritable throwing different exception/error depending on the driver in case the resource was closed at one point, instead, we should throw ClosedResourceException.

    currently, I'm handling this case by catching Throwable, and checking if the resource was closed again: https://github.com/azjezz/psl/blob/2.0.x/src/Psl/Async/await_readable.php#L42-L54, but this method is not sufficient.

    feature request help wanted 
    opened by azjezz 8
  • Mimic Octane::concurrently behavior

    Mimic Octane::concurrently behavior

    Hello,

    In Laravel Octane when using Swoole its possible to do something like the following.

    	[ $recs1, $recs2 ] = Octane::concurrently( [
    		fn() => $this->get_recs_a( $ids ),
    		fn() => $this->get_recs_b( $ids )
    	] );
    

    What this will do is execute both functions get_recs_a and get_recs_b concurrently yet not returning unless both got executed and set their return values to $recs1 and $recs2.

    How to achieve similar behavior?

    question 
    opened by cayolblake 7
  • [Feedback] Welcome to RevoltPhp 🥳

    [Feedback] Welcome to RevoltPhp 🥳

    Hi there 👋 😄 , I'm Benoit, an enthusiast Php/Async dev, and I just wanted to give my feedback (+ questions) as proposed in this tweet.

    First of all, thank you very much for all this work! Even if I have some comments about RevoltPhp design, I'm really convinced that it's an awesome opportunity for Php ecosystem to see ReactPhp & Amp teams (& friends) working together to build such library 👍 🤩

    Some context

    When I had to choose an EventLoop for my company internal project, I studied a lot ReactPhp and Amp developer experience (DX). I really like the EventLoop Interface in ReactPhp, but I was really concerned by the potential callback hell for my team. In the other hand, Amp use generators with a nice DX (more intuitive to read IMHO), but EventLoop is global and doesn't expose an interface (but driver does). That's why we created Tornado, a modest attempt to abstract both with the Generator approach… So, when I read the first tweet of RevoltPhp I was very curious 😄

    Fibers

    I'm really excited about Fibers, I think it's the perfect tool to provide a fluent DX for async processing. But surprisingly, RevoltPhp seems to use callbacks extensively… Do you consider RevoltPhp like a very low-level async layer on which we can build some Fiber-oriented libs/projects? Nevertheless, it seems possible to rely on Fiber with the Suspension concept (example), but it seems very verbose.

    $watcher = EventLoop::onWritable($stream, fn () => $suspension->resume(null));
    $suspension->suspend();
    EventLoop::cancel($watcher);
    

    I was maybe expecting something like this:

    EventLoop::waitWritable($stream);
    

    Or maybe keeping the concept of Promise/Future, with something like:

    $foo = EventLoop::wait(EventLoop::async(foo(...)));
    

    🤷 Anyway, all these functions can be created from Revolt core functions, it's just to give you my (humble) opinion 😃

    Combinators

    Do you intend to provide some basic functions to combine asynchronous functions? Like all, race… ? Once again, it's totally possible thanks to suspension (example), but it's not straightforward IMHO

    Conclusion

    That's all 😄 Of course, I would prefer to have a non-global EventLoop or an EventLoopInterface (instead of using Drivers), but I don't think it's the most important, just a matter of taste 😉 And once again, thank you for this library 👍 Even with this design, I'm pretty sure it will be very useful as a core component to other projects. If you are curious, I created a 1-file async-library for an incoming talk, to quickly show possibilities offered by Fibers: https://github.com/b-viguier/Slip . Just for demonstration purpose, but feel free to comment 🤷

    Benoit

    opened by b-viguier 7
  • Fix reference to Fiber in Suspension causing memory leak

    Fix reference to Fiber in Suspension causing memory leak

    This changes Suspension to hold only a weak reference to the associated Fiber object while not suspended. Without this, there is a circular reference between the weak map of suspensions and the fiber held within the suspension.

    This fixes the memory leak in https://github.com/revoltphp/event-loop/issues/42#issuecomment-1041600341.

    opened by trowski 5
  • Use \Closure instead of callable

    Use \Closure instead of callable

    callable is a problematic type, because it's scope dependent. It can therefore not be used for typed properties, but \Closure can be used as a safe replacement, especially with first-class syntax introduced in PHP 8.1: (...).

    Resolves #14.

    opened by kelunik 5
  • Add suspension listeners

    Add suspension listeners

    This PR allows user-definable hooks to be invoked when a fiber (or main) is suspended or resumed.

    The motivation is to allow libraries/apps to implement behavior such as that requested in amphp/amp#354 if so desired.

    This is designed to be lightweight so applications that do not use these hooks do not incur a large performance overhead.

    • [ ] Benchmarks to see what effect this has on performance.
    opened by trowski 5
  • How to measure an event loop overhead?

    How to measure an event loop overhead?

    I'm writing a pure PHP equivalent of libpq that uses RevoltPHP v0.2.1 and Amp v3 (latest beta). And after I implemented the basic things like sending queries/reading rows, I decided to profile the performance.

    And then I saw that the most time is spent on calling Fiber::suspend: image

    If I understand correctly, Fiber::suspend time includes I/O wait time. But is there any way to see only I/O wait time?

    When I tried to compare my performance results with the same bench, but using the pgsql PHP extension, it was almost twice as fast.

    question 
    opened by codercms 4
  • apr_socket_recv: Connection reset by peer (104)

    apr_socket_recv: Connection reset by peer (104)

    Hello maintainer, I try http-server example in https://github.com/revoltphp/event-loop/blob/main/examples/http-server.php,

    But when using ab tool to benchmark, it result in

    This is ApacheBench, Version 2.3 <$Revision: 1901567 $>
    Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
    Licensed to The Apache Software Foundation, http://www.apache.org/
    
    Benchmarking 127.0.0.1 (be patient)
    apr_socket_recv: Connection reset by peer (104)
    

    AFAIK, this is not the problem with ab tool it self, but with the webserver, I'm using default ubuntu server to test it.

    opened by kocoten1992 2
Releases(v1.0.0)
  • v1.0.0(Nov 3, 2022)

    We're proud to announce our initial stable release! The event loop is ready for production use, has been tested in various different applications and scenarios, and fully supports fibers.

    What's Changed

    • Added EventLoop::getIdentifiers (#62)
    • Added EventLoop::getType (#62)
    • Added EventLoop::isEnabled (#62)
    • Added EventLoop::isReferenced (#62)
    • Fixed EventLoop::getErrorHandler missing the static modifier
    • Fixed double wrapping in UncaughtThrowable if a decorating event loop driver throws an UncaughtThrowable (#61)
    • Removed EventLoop::getInfo, use EventLoop::getIdentifiers() in combination with EventLoop::isEnabled, EventLoop::isReferenced, and EventLoop::getType instead (#62)
    • Removed EventLoop::createSuspension, use EventLoop::getSuspension instead

    Full Changelog: https://github.com/revoltphp/event-loop/compare/v0.2.5...v1.0.0

    Source code(tar.gz)
    Source code(zip)
  • v0.2.5(Aug 1, 2022)

    What's Changed

    • PHP 8.1 is now required (#55)
    • Fix compatibility with 8.2 by fixing a deprecation notice (#58)
    • Fixed an integer overflow on timers if a large (e.g. PHP_INT_MAX) timeout is requested (#49)
    • Removed the reference kept to microtask (EventLoop::queue()) callback arguments so passed objects may be garbage collected if a resulting fiber unsets all references to the argument (#60)

    Full Changelog: https://github.com/revoltphp/event-loop/compare/v0.2.4...v0.2.5

    Source code(tar.gz)
    Source code(zip)
  • v0.2.4(Mar 13, 2022)

    What's Changed

    • Fixed the fiber reference in DriverSuspension being nulled early during shutdown, leading to an assertion error when attempting to resume the suspension.

    Full Changelog: https://github.com/revoltphp/event-loop/compare/v0.2.3...v0.2.4

    Source code(tar.gz)
    Source code(zip)
  • v0.2.3(Mar 4, 2022)

    What's Changed

    • Fixed Undefined property: Revolt\EventLoop\Internal\DriverSuspension::$fiber in an error path

    Full Changelog: https://github.com/revoltphp/event-loop/compare/v0.2.2...v0.2.3

    Source code(tar.gz)
    Source code(zip)
  • v0.2.2(Feb 20, 2022)

    What's Changed

    • Fixed memory leak with suspensions keeping a reference to fibers (https://github.com/revoltphp/event-loop/issues/42, https://github.com/revoltphp/event-loop/pull/52) Similar leaks might still happen if suspensions are never resumed, so ensure your suspensions are eventually resumed.

    New Contributors

    • @nhedger made their first contribution in https://github.com/revoltphp/event-loop/pull/48
    • @tnsoftbear made their first contribution in https://github.com/revoltphp/event-loop/pull/50

    Full Changelog: https://github.com/revoltphp/event-loop/compare/v0.2.1...v0.2.2

    Source code(tar.gz)
    Source code(zip)
  • v0.2.1(Feb 3, 2022)

    What's Changed

    • Added template type to Suspension by @trowski in https://github.com/revoltphp/event-loop/pull/44
    • Added FiberLocal::unset() by @kelunik in https://github.com/revoltphp/event-loop/pull/45
    • Added stacktrace to all current suspensions on early exit of the event loop by @kelunik in https://github.com/revoltphp/event-loop/pull/46

    Full Changelog: https://github.com/revoltphp/event-loop/compare/v0.2.0...v0.2.1

    Source code(tar.gz)
    Source code(zip)
  • v0.2.0(Jan 14, 2022)

    This release is mostly backwards compatible with v0.1.x.

    What's Changed

    • Add FiberLocal to store data specific to each fiber, e.g. logging context (#40)
    • Throw UnhandledThrowable if event loop stops due to an exception (#32)
    • Reduce fiber switches by queueing callbacks for each tick (#34)
    • Avoid creating unnecessary fibers if exceptions are thrown from callbacks
    • Add EventLoop::getErrorHandler() to get the currently set error handler
    • Remove return value of EventLoop::setErrorHandler(), use EventLoop::getErrorHandler() instead
    • Remove default value for first argument of EventLoop::setErrorHandler() (#30)
    • Cache suspensions and always return the same value for a specific fiber (#37)
      • EventLoop::getSuspension() has been added as replacement for EventLoop::createSuspension()
      • EventLoop::createSuspension() has been deprecated and will be removed in the next version
    • Fix multiple interrupts on double resumption leading to an assertion error instead of an exception (#41)
    • Fix suspensions keeping their pending state after the event loop exceptionally stopped

    Full Changelog: https://github.com/revoltphp/event-loop/compare/v0.1.1...v0.2.0

    Source code(tar.gz)
    Source code(zip)
  • v0.1.1(Dec 10, 2021)

    What's Changed

    • Fixed exceptions being hidden if the event loop stopped due to an uncaught exception by @bwoebi in https://github.com/revoltphp/event-loop/pull/31

    New Contributors

    • @bwoebi made their first contribution in https://github.com/revoltphp/event-loop/pull/31

    Full Changelog: https://github.com/revoltphp/event-loop/compare/v0.1.0...v0.1.1

    Source code(tar.gz)
    Source code(zip)
  • v0.1.0(Dec 1, 2021)

Owner
Revolt PHP
Revolt is a rock-solid event loop for concurrent PHP applications.
Revolt PHP
An asynchronous event driven PHP socket framework. Supports HTTP, Websocket, SSL and other custom protocols. PHP>=5.3.

Workerman What is it Workerman is an asynchronous event-driven PHP framework with high performance to build fast and scalable network applications. Wo

walkor 10.2k Jan 4, 2023
Event-driven, non-blocking I/O with PHP.

Event-driven, non-blocking I/O with PHP. ReactPHP is a low-level library for event-driven programming in PHP. At its core is an event loop, on top of

ReactPHP 8.5k Jan 8, 2023
Événement is a very simple event dispatching library for PHP.

Événement Événement is a very simple event dispatching library for PHP. It has the same design goals as Silex and Pimple, to empower the user while st

Igor 1.2k Jan 4, 2023
A pragmatic event sourcing library for PHP with a focus on developer experience.

EventSaucePHP EventSauce is a somewhat opinionated, no-nonsense, and easy way to introduce event sourcing into PHP projects. It's designed so storage

EventSauce 685 Dec 31, 2022
PHP Application using DDD CQRS Event Sourcing with Hexagonal Architecture

PHP Application using DDD CQRS Event Sourcing with Hexagonal Architecture Application built with Ecotone Framework and powered by integrations with Pr

EcotoneFramework 65 Dec 27, 2022
[READ-ONLY] The event dispatcher library for CakePHP. This repo is a split of the main code that can be found in https://github.com/cakephp/cakephp

CakePHP Event Library This library emulates several aspects of how events are triggered and managed in popular JavaScript libraries such as jQuery: An

CakePHP 21 Oct 6, 2022
A non-blocking concurrency framework for PHP applications. 🐘

Amp is a non-blocking concurrency framework for PHP. It provides an event loop, promises and streams as a base for asynchronous programming. Promises

Amp 3.8k Jan 6, 2023
Icicle is a PHP library for writing asynchronous code using synchronous coding techniques

Icicle is now deprecated in favor of Amp v2.0. This version is is currently under development, but close to release. The v2.0 branches are amp_v2 in a

icicle.io 1.1k Dec 21, 2022
Asynchronous coroutines for PHP 7.

Recoil An asynchronous coroutine kernel for PHP 7. composer require recoil/recoil The Recoil project comprises the following packages: recoil/api - T

Recoil 787 Dec 8, 2022
PHP 7.4 EventStore Implementation

Prooph Event Store Common classes and interface for Prooph Event Store implementations. Installation You can install prooph/event-store via composer b

null 532 Dec 9, 2022
Reactive extensions for PHP.

This package is abandoned. Use https://github.com/ReactiveX/RxPHP instead Rx.PHP Reactive extensions for PHP. The reactive extensions for PHP are a se

Alexander 208 Apr 6, 2021
Golang's defer statement for PHP

PHP Defer A defer statement originally comes from Golang. This library allows you to use defer functionality in PHP code. Usage <?php defer($context,

null 249 Dec 31, 2022
Reactive extensions for PHP

RxPHP Reactive extensions for PHP. The reactive extensions for PHP are a set of libraries to compose asynchronous and event-based programs using obser

ReactiveX 1.6k Dec 12, 2022
🚀 Coroutine-based concurrency library for PHP

English | 中文 Swoole is an event-driven asynchronous & coroutine-based concurrency networking communication engine with high performance written in C++

Swoole Project 17.7k Jan 5, 2023
Flexible and rock solid audit log tracking for CakePHP 3

AuditStash Plugin For CakePHP This plugin implements an "audit trail" for any of your Table classes in your application, that is, the ability of recor

José Lorenzo Rodríguez 68 Dec 15, 2022
The SOLID principles demonstrated in PHP as seen on Elaniin's SOLID Booster Session.

SOLID Principles SOLID is the mnemonic acronym that represents 5 design principles that aim to make software designs more understandable, flexible, an

Vlass Contreras 5 Aug 24, 2022
A high-performance event loop library for PHP

?? A high-performance event loop library for PHP ??

workbunny 13 Dec 22, 2022
Cronlike scheduler running inside a ReactPHP Event Loop

Cronlike scheduler running inside a ReactPHP Event Loop Install To install via Composer, use the command below, it will automatically detect the lates

Cees-Jan Kiewiet 35 Jul 12, 2022
Light, concurrent RPC framework for PHP & C

Yar - Yet Another RPC framework for PHP Light, concurrent RPC framework for PHP(see also: Yar C framework, Yar Java framework) Requirement PHP 7.0+ (m

Xinchen Hui 1.4k Dec 28, 2022
Utilities for concurrent programming of PocketMine-MP plugins.

Utilities for concurrent programming of PocketMine-MP plugins Overview Plugin that implements the pthreads channels and in the future, promises (which

Dmitry Uzyanov 0 Aug 15, 2022