Adopt the Revolt event loop for async task orchestration

Created on 16 October 2023, 8 months ago
Updated 13 March 2024, 3 months ago

Problem/Motivation

My opinions are formed by wrangling PHP into serving subscriptions from a Drupal back-end and from having more than a handful of legitimate needs to bootstrap Drupal in a place where Drupal was not the owner of the process.

I believe strongly that Drupal should adopt the Revolt event-loop.

Why an event loop?

We should adopt an event loop because as we make more and more of Drupal able to be async (e.g. finishing the database layer; perhaps adding file access; possibly making external HTTP requests for an aggregated JSON API) managing all those interactions become increasingly complex. There are tried and tested lower level PHP extensions that have spent many years solving this problem; which extensions are available will differ and supporting them all is a challenge. Event loops like Revolt are built to work across these different implementations and automatically select the right one.

In our initial implementations we manually implemented a Fiber loop, but managing tasks on that can quickly become cumbersome and such custom loops provide no extension points for contrib. Additionally, mistakes by implementing this ourselves are easy to make. For exmaple user ReINFaTe on the Drupal Slack pointed out "all fiber loops should call sleep() if every fiber waits for something. Without sleep, while all code waits, the loop will use 100% CPU just to keep checking all the fibers."

Why Revolt specifically?

We should specifically adopt the Revolt event-loop because just like the Drupal community works towards common goals, there's a clear showing that the async PHP community has worked towards a common goal. ReactPHP and AmPHP were the largest options (and only options if you ignore Swoole which requires a custom PHP extension) in the ecosystem and they've bundled forces to create a single re-usable event loop. The goal of Revolt is to be an event-loop only and provide only primitives for interacting with it and scheduling work, leaving the creation of higher level concepts to other libraries. AmPHP has now adopted Revolt as its event loop and Revolt itself provides a ReactPHP adapter by implementing ReactPHP's EventLoop interface.

This means that adopting Revolt as event loop will immediately allow any ReactPHP or AmPHP code to be used in Drupal projects (even if that's outside of core).

Revolts Pedigree

The event loop will play a core part in how Drupal schedules its asynchronous tasks and as such will become an important part of Drupal. This means that it's important to know that Revolt will be around for a long time and that it's solidly built.

Revolt is maintained by Aaron Piotrowski, Niklas Keller, and Saif Eddin Gmati. Niklas and Aaron are also maintainers of Amphp (together with Bob Weinand). Niklas and Aaron are also the authors of the Fibers RFC, so I'd argue they have some knowledge of how to use Fibers.

Async Primitives

Whether Drupal should also adopt an "async tools" library on top of is a discussion for a separate issue ( 🌱 [PP-1] Adopt a library like amp or provide async management primitives in Drupal core Postponed ). The most important accelerator for asynchronous tasks in Drupal is the adoption of the Revolt event loop, because it suddenly provides an entire async ecosystem for contrib to use.

Drupal's optional tasks on an event loop

There's two important building blocks that Revolt offers here:

  1. Cancellation of callbacks
  2. Referenced/unreferenced callbacks

1) What we could do is to build some primitives in Drupal that say "Attach this optional task to this request" and at the end of the request cancel all those tasks that are optional.

2) However, in the event of the event loop being used within an process like PHP-FPM serving a single request there is an easier way. Revolt allows callbacks to be either "referenced" or "unreferenced". A referenced callback will keep the event loop alive (this is the default). An unreferenced callback will not keep the event loop alive and will allow the process to exit once done. This means that those background tasks can be registered as unreferenced callbacks. Then if the things needed for the request are done and the clean-up tasks are completed the event loop will shut down, throwing away those optional tasks that were not yet completed.

Proposed resolution

Adopt the Revolt event loop. This should happen in the index.php outside of what would be considered Drupal's Runtime ( ✨ Use symfony/runtime for less bespoke bootstrap/compatibility with varied runtime environments Active ). This ensures that applications that have different lifecycles (e.g. Drush) can control the starting and stopping of the event loop themselves and decide when Drupal might need to be bootstrapped as part of a longer running process.

Remaining tasks

Useful reading

Since Fibers and async programming may be new to some below is a list of useful reading:

User interface changes

API changes

Data model changes

Release notes snippet

🌱 Plan
Status

Active

Version

11.0 πŸ”₯

Component
BaseΒ  β†’

Last updated 33 minutes ago

Created by

πŸ‡³πŸ‡±Netherlands Kingdutch

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @Kingdutch
  • πŸ‡³πŸ‡±Netherlands Kingdutch
  • πŸ‡¬πŸ‡§United Kingdom catch

    We should add a dependency evaluation to the issue summary, it almost there already, just release cycle and security policy I think: https://www.drupal.org/about/core/policies/core-dependency-policies/depe... β†’

    I still have not fully grasped the benefit of the central event loop vs. for example adding a trait to cover the current raw Fibers implementation (which could then handle sleeping, suspending to any parent Fiber in one place etc. centrally, but would still keep the management of each loop local). For me at least, it would be useful to see a conversion of the manual Fibers loops in core, we also have some examples of suspension in the cache prewarming/stampede issue (if not actual async anywhere yet).

  • πŸ‡³πŸ‡±Netherlands Kingdutch

    I've filled out the dependency evaluation.

    I still have not fully grasped the benefit of the central event loop vs. for example adding a trait to cover the current raw Fibers implementation (which could then handle sleeping, suspending to any parent Fiber in one place etc. centrally, but would still keep the management of each loop local). For me at least, it would be useful to see a conversion of the manual Fibers loops in core, we also have some examples of suspension in the cache prewarming/stampede issue (if not actual async anywhere yet).

    I emailed Aaron with this question and he replied with

    Drupal absolutely should use the Revolt event loop. The entire reason Revolt exists is to avoid fragmentation of the event loop component among PHP libraries which want to run asynchronous tasks. The event loop essentially becomes a part of the runtime – you cannot mix multiple event loops in the same application because only one can be running at a time. We talk a bit more about this at https://revolt.run/fundamentals.

    Using a propriety loop which schedules fibers would make Drupal incompatible with any library using a different fiber scheduler – i.e. any library using Revolt.

    AMPHP might also be useful for some of the primitives it provides, such as Futures and Cancellations, as well as some of the lower-level helper libraries like amphp/pipeline. Note though using AMPHP would be completely optional – Drupal could implement it's own promises/futures, etc. and still be compatible with AMPHP so long as it was using Revolt to schedule events.

    Revolt is flexible and un-opinionated, making it easy to create new fibers, use timers, and wait for I/O. Check out the docs at https://revolt.run and let me know if I can provide any additional examples or assistance.

    The relevant part of that fundamentals document (in case it changes and someone is reading this in 2025 πŸ‘‹):

    Every application making use of cooperative multitasking can only have one scheduler. It doesn’t make sense to have two event loops running at the same time, as they would just have to schedule each other in a busy waiting manner, wasting CPU cycles.

    Revolt provides global access to the scheduler using methods on the Revolt\EventLoop class. On the first use of the class, it will automatically create the best available driver. Revolt\EventLoop::setDriver() can be used to set a custom driver.

    To add to that personally I think with the initial Fiber code that was added we've already seen some challenges with CPU spinlocking. To me this feels very much like a problem where the initial case is trivial and as we adopt Fibers more we'll find more of these edge cases (oh we'd like to just let the system sleep until I/O comes back if there's nothing else to do). We'd then be solving exactly the problems that Revolt has already solved, but doing so in a way not compatible with other async code in the ecosystem.

  • πŸ‡¬πŸ‡§United Kingdom longwave UK

    No current security policy published.

    Can we ask the maintainers if they are willing to publish a security policy? Given that this is a low level runtime dependency it seems quite important that if there is a security issue the maintainers are prepared to fix it within a reasonable timescale.

  • πŸ‡³πŸ‡±Netherlands Kingdutch

    I've opened an issue with the request: https://github.com/revoltphp/event-loop/issues/87

  • πŸ‡¬πŸ‡§United Kingdom catch

    To add to that personally I think with the initial Fiber code that was added we've already seen some challenges with CPU spinlocking. To me this feels very much like a problem where the initial case is trivial and as we adopt Fibers more we'll find more of these edge cases (oh we'd like to just let the system sleep until I/O comes back if there's nothing else to do). We'd then be solving exactly the problems that Revolt has already solved, but doing so in a way not compatible with other async code in the ecosystem.

    I think we could solve some of that by moving the individual loops to use a helper class
    so there's less repetition, if that was the only reason I'm not sure it would be worth it, but the interoperability arguments here are quite strong so that is pushing me over from neutral/on the fence towards pro-adoption of Revolt at the moment.

  • πŸ‡³πŸ‡±Netherlands Kingdutch

    Updated the remaining tasks. I've created a child issue to add the dependency to the composer.json: πŸ“Œ Add revoltphp/event-loop dependency to core Active which now also contains the dependency evaluation.

  • πŸ‡³πŸ‡±Netherlands Kingdutch

    Updated the issue summary with the remaining tasks to show tasks in progress. At least with the current proposed implementations it appears no work for PHPUnit is needed. If tests want to test something specifically that doesn't block the main thread at some point then they'll have to run EventLoop::run() in the test themselves.

Production build 0.69.0 2024