Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Global event-loop accessor part four #226

Conversation

WyriHaximus
Copy link
Member

This topic has come up before in #10, #77, and #82. And their aim was to provide all the pieces in one go, the aim for this PR, and its successors in line, is to discuss all bits in their own PR. As such this PR is providing the bare minimum.

To help that I've made several examples to provide different angles on this topic:

Resolves: #10 #77 #82

@WyriHaximus WyriHaximus added this to the v1.2.0 milestone Apr 21, 2021
@mbonneau
Copy link

Should Loop::set allow setting the loop if it has already been set?

What do you think about registering a shutdown function to run the loop?

@WyriHaximus
Copy link
Member Author

Should Loop::set allow setting the loop if it has already been set?

Yes, but only when it hasn't been started yet. The issue with this is testing of course so not 100% sure yet.

What do you think about registering a shutdown function to run the loop?

I'm open for it, but not sure if it's wise thing to do. Have you done this in your projects, and how did it work out?

@cboden
Copy link
Member

cboden commented Apr 22, 2021

I'm open for it, but not sure if it's wise thing to do. Have you done this in your projects, and how did it work out?

Matt added this to Pawl. It's been a great quality of life improvement.

@mbonneau
Copy link

In https://github.com/voryx/event-loop, we have been using the register_shutdown_function the same way it is implemented in Pawl. We have not experienced any problems with the shutdown function usage. This library is used in almost every php project that I have worked on in the last 5 years or so. I am definitely an advocate of that being included in the official library.

We tried to only expose things from the library that made sense from the user standpoint. So for testing we used reflection to reset the private static property. (https://github.com/voryx/event-loop/blob/master/tests/Unit/EventLoopTest.php#L11)

@WyriHaximus
Copy link
Member Author

In https://github.com/voryx/event-loop, we have been using the register_shutdown_function the same way it is implemented in Pawl. We have not experienced any problems with the shutdown function usage. This library is used in almost every php project that I have worked on in the last 5 years or so. I am definitely an advocate of that being included in the official library.

Cool, but I really would prefer to do this in several smaller PR's than one big one.

We tried to only expose things from the library that made sense from the user standpoint. So for testing we used reflection to reset the private static property. (https://github.com/voryx/event-loop/blob/master/tests/Unit/EventLoopTest.php#L11)

That makes sense, would you think it makes sense to provide a utility for it?

@trowski
Copy link

trowski commented May 3, 2021

Running the event loop in a shutdown function is not a good idea. The engine has moved to shutdown mode and executing an entire app within that mode can lead to subtle breakages: https://heap.space/xref/php-src/main/main.c?r=39ddf6b8#1772

I thought of this long ago, and on the surface it seems to work well, but is a bit of a hack.

@HLeithner
Copy link

With this implementation it's not possible to detect if an event loop already exists,

This could be useful in scenarios with 3rd party plugins and mixed workflows (with and without event loop, sync / async flow).

I'm not 100% sure that it's necessary. ymmv

Copy link
Member

@clue clue left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@WyriHaximus Thanks for filing this PR among with clue/reactphp-redis#109 and clue/reactphp-redis#110, this definitely helps reviewing and seeing its real-world impact on existing code!

I think we all agree that having a global loop makes perfect sense and is a real improvement towards a more streamlined API for consumers of any ReactPHP-based project! It also looks like this PR is an excellent starting point to discuss the minimum required to get this rolling.

That being said, I'd like to ensure we have a smooth upgrade path for existing consumers and a clear understanding of the consequences of this approach.

The suggested changes help making any LoopInterface arguments optional throughout the ReactPHP ecosystem. As a consequence, this means that most consumers will no longer create and/or pass the loop around to all ReactPHP components.

I agree with this suggestion and think this is a great first step. I think there's definitely room to improve this even further, but I suppose the goal seems to be to discuss this in separate tickets? (additional helper methods, automatic shutdown handler, etc.)

Other than that, have you checked how this works together with existing code still using the loop factory? It's my understanding both styles will be mixed at least during a transition period, so I'd like to see some additional safeguards in place. If this is taken care of, consider my vote a strong 👍

@WyriHaximus WyriHaximus force-pushed the global-event-loop-accessor-part-four branch from 3eabeac to 9c7b0db Compare May 17, 2021 17:13
@WyriHaximus WyriHaximus marked this pull request as ready for review May 17, 2021 17:13
@WyriHaximus WyriHaximus force-pushed the global-event-loop-accessor-part-four branch from 9c7b0db to c8709ce Compare May 17, 2021 17:24
@WyriHaximus WyriHaximus changed the title Global event loop accessor part four Global event-loop accessor part four May 17, 2021
@WyriHaximus
Copy link
Member Author

@clue Updated the PR as discussed earlier today during our zoom call. Factory::create will now call Loop::set with the created event loop. (Had to do a small refactor.) Also marked Loop::set as internal.

@HLeithner The changes I've just pushed should address your concern. As long as you call Factory::create before Loop::get you'll be fine. Also during mixed workflows, you can now use Loop::get and you'll always get the same loop back. That means if you also use https://github.com/clue/reactphp-block you might have leftovers in your loop from your previous run if you didn't clean up. Highly doubt that is going to be an issue, unless it's used in a messy way that leaves a lot of garbage between runs.

@WyriHaximus
Copy link
Member Author

Running the event loop in a shutdown function is not a good idea. The engine has moved to shutdown mode and executing an entire app within that mode can lead to subtle breakages: https://heap.space/xref/php-src/main/main.c?r=39ddf6b8#1772

I thought of this long ago, and on the surface it seems to work well, but is a bit of a hack.

@trowski What kind of issue did you run into?

@trowski
Copy link

trowski commented May 17, 2021

@trowski What kind of issue did you run into?

Some behaviors are modified during shutdown. An uncaught exception or fatal error from a shutdown function will not run other shutdown functions. This differs from an uncaught exception in {main}, which will run shutdown functions.

Consider the following:

Loop::get()->nextTick(fn () => throw new \Exception('test'));

register_shutdown_function(fn () => print "shutdown 1\n");
register_shutdown_function(fn () => Loop::get()->run());
register_shutdown_function(fn () => print "shutdown 2\n");

The first shutdown function will be invoked before the loop is started. Upon entering the loop, an exception is thrown from a loop callback, resulting in an uncaught exception. However, the second shutdown function is never invoked.

In short, conditions that trigger a bailout in a shutdown function differ in behavior than a bailout from {main}.

The above code also demonstrates how defining a shutdown function becomes timing dependent, and may conflict with libraries that define shutdown functions when loading code or user application code that defines a shutdown function before starting the loop.

Entering shutdown can have unexpected behaviors in certain extensions that may use EG(flags) to determine execution state.

While starting the loop in a shutdown function works on first glance, it is my opinion that the loop package should not endorse this practice due to the potential change in execution and shutdown behavior. Auto-starting the loop in a shutdown function could be available in a supplementary package (with potential caveats explained), but I would not suggest it as the official method to run the event loop.

@mbonneau
Copy link

@trowski Firing off the loop inside the register_shutdown_function is a hack. But I think it is worth it. It is just so much more ergonomic for new reactphp users.

There are libraries that use register_shutdown_function that may not work properly with this use, but I would hazard a guess that it is far less than 1% of all PHP libraries.

Users can also completely mitigate this problem, if needed, by running the loop themselves, which would cancel the run inside the shutdown function.

Copy link
Member

@clue clue left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@WyriHaximus Thanks for the update!

The changes look good to me, but I'd like to check if we can add some additional tests and address the minor remarks below.

Other than that, I think this is ready to go in 👍

Regarding the automatic running of the loop (possibly with the help of a shutdown handler), I've discussed this matter with @WyriHaximus and we've agreed that this is out of scope for this PR. We've also agreed that this should be addressed in a separate PR as part of the same milestone to avoid any unexpected regressions in upcoming versions, so this will be addressed shortly.

Once merged, I'll pick this up and see how reasonable automatic running is 👍

src/Loop.php Show resolved Hide resolved
examples/95-benchmark-memory.php Outdated Show resolved Hide resolved
src/Factory.php Show resolved Hide resolved
@WyriHaximus WyriHaximus force-pushed the global-event-loop-accessor-part-four branch 2 times, most recently from 3673a6c to 22c9972 Compare May 18, 2021 17:17
@WyriHaximus WyriHaximus force-pushed the global-event-loop-accessor-part-four branch from 22c9972 to b6e704a Compare May 18, 2021 17:35
@WyriHaximus
Copy link
Member Author

@clue Updated the PR based on your comments. Added several tests and the requested docblock.

@WyriHaximus WyriHaximus requested a review from clue May 18, 2021 17:35
* Internal undocumented method, behavior might change or throw in the
* future. Use with cation and at your own risk.
*
* @internal
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems strange to me to make this method internal. Users can then no longer decide to use a specific event loop implementation without using internal APIs, because the factory is the only public API, e.g. if both uv and ev are installed, a user might still prefer to use ev.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Until we are sure about what such behaviour should be of the Loop::set method it will be marked as internal, and undocumented. You can still use it, but at your own risk. We don't want to introduce a function without being absolutely sure it is the right thing to do. Because once we add it, we can't remove it until the next major version

Copy link
Member

@clue clue Jun 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Users can then no longer decide to use a specific event loop implementation without using internal APIs […]

Any event loop can still be instantiated as usual if you need to make an explicit choice, this PR does not change anything about this. For the >99% use cases only a single event loop implementation would be available and the default choice is still reasonable.

It seems strange to me to make this method internal.

I agree that this may appear odd on first sight. Like @WyriHaximus pointed out, we're using this as a starting point to be able to gather more feedback. Expect a public set() method not too far in the future in a separate PR :shipit:

{
$loop = self::construct();

Loop::set($loop);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This side effect of a factory is pretty much unexpected IMO.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Main reason for adding that was to support mixed use cases where both the Factory and the Loop object are used.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This side effect of a factory is pretty much unexpected IMO.

I agree that this may appear odd on first sight. Like @WyriHaximus pointed out, we're using this as a starting point to be able to gather more feedback. Right now this has no effect on any consumer because the Loop::set() is entirely internal only and the Loop::get() uses the very same Factory::create() method. Expect a cleaner approach not too far in the future once we have a public Loop::set() method in a separate PR :shipit:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants