-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Asynchronous events #1881
Comments
Suggestions:
|
@CtrlAltCuteness Allowing both ticks and seconds is unreasonable. I suggested using ticks because it may be correlated with how tasks execute on the server (which does not change with real-life time when the server is laggy. I just suggested real-life time too because that's what native timeout functions accept. But if both time systems are supported, it is an overkill. Timeout is, after all, just a mechanism to prevent bugs. |
I am inclined to agree with @SOF3 for the usage of ticks. You can always provide a lower bound in seconds given an amount of ticks (multiply by 20). However, when converting from seconds to ticks, you have an upper bound. I think in timeouts, a being given a little extra is better than a little less. |
I much prefer |
Sorry @SOF3 as I think I was not fully awake at the time. The idea is to allow a unexpectedly slow local network (or relatively slow HTTP request & parse, etc.) to get data from a server database independent of the game speed, while knowing if it is too slow for a (example) game time-critical data fetch. |
Actually, the pause timeout is only to prevent people from forgetting to continue() the event. It is a real error, and it was not intended to help you handle expected timeout conditions. |
Understood. Thank you for clarifying, @SOF3 |
Proposed addition. Behavioural changes from the suggested result in the proposal:
|
Obsoleted by #2470. |
TL;DR
- Async event allows you to use async task, wait for user response or do other time-consuming actions in an event handler. - Async event is opt-in. This is not a compatibility-destructive proposal.Example
This example shows how a plugin can translate PlayerChatEvent messages with an online API:
There are no changes to PlayerChatEvent.php
Full RFC
Warning: The following RFC has more than 1500 words. If you don't want to read it, just read the TL;DR above and the "Expected use cases" at the end.
Introduction
Introduction: Why async events?
It is difficult to write a plugin that fetches data asynchronously, because many parts of the PocketMine API expect you to return a value immediately.
Currently, plugins usually use either of these two solutions:
PlayerChatEvent->setMessage()
.Each of these solutions has serious drawbacks. This RFC seeks to provide a better solution on behalf of the events API itself.
Proposal Phase I: The API
This RFC allows event handlers to properly pause the whole event. The message in PlayerChatEvent will not be sent until you are ready; the player in PlayerPreLoginEvent will stay "Connecting to server..." until you get your data.
The mechanism comprises two parts: event calling and event handling.
Event calling
Traditional way of calling events:
To call async events:
where the second parameter is the handler when the event completes.
It is yet to be decided whether
callAsyncEvent
orcallEventAsync
would be better.Any event can be called asynchronously. Whether an event can be handled asynchronously depends on the caller, not the event type.
Event handling
An event handler can request the event execution to be paused by calling
Event->pause()
, and continue it withEvent->continue()
.If
Event->continue()
is never called, the event would theoretically get stuck forever. To prevent this, PocketMine should be responsible for tracking the executing async events. By default, an event can only pause for 5 seconds; beyond that time limit, PocketMine will trigger an error message and the event will continue executing (go to next handler, or complete). There would not be effective measures to stop the event from modifying event data later (e.g. while executing another handler), but since an error message is shown, it should be clear enough that there is something wrong, and the problem of timeout rather than the problem of concurrent event modification should be tackled.Event->pause()
accepts an optional argumentfloat $timeout = 5
specifying the timeout in seconds. The implementation shall ensure the timeout not to exceed 60. Event subclasses may also override this method to disallow asynchronous event handling or limit the timeout. However, PocketMine shall hard-limit this timeout to 60 seconds to preventstupidabusive behaviour.(It is yet to be decided whether this should be in seconds or in ticks -- using ticks seems more reliable)
It is also proposed that a second optional argument,
?callable $onInterrupt
, be accepted, which can be triggered when the timeout is reached. If this is null, the default behaviour (display error message) will be triggered.In cases where the event is called with the traditional
callEvent
rather thancallAsyncEvent
,Event->pause()
shall throw an InvalidStateException upon called.In conclusion: the whole flow
For each of the
--->
, the flow of control between is undefined -- callAsyncEvent may or may not immediately result in an onComplete call, or there may be a few ticks in between even if there are no handlers at all -- developers should not rely on any observed or predicted behaviour regarding this, as this is subject to change.As it currently is, the
Event
object should be recyclable after the event call has fully completed (after onComplete is called; not during onComplete is called). Therefore, the implementation in PocketMine is responsible for resetting anEvent
to its initial state (in terms of what PocketMine will change) after onComplete is called.Proposal Phase II: Migration of (some) current events calls to callAsyncEvent
Current events triggered by PocketMine shall be gradually migrated from
callEvent
tocallAsyncEvent
. This may involve complex refactoring in major sections of PocketMine's source code, and can be carried out throughout a longer span.Backward compatibility
This RFC aims to preserve as much backward compatibility as possible, as this change is not totally necessary.
Phase I only: For event handlers
Traditional event handlers for traditional event calls do are not affected.
Phase II: For events migrated to callAsyncEvent
Event handlers must no longer assume that the event is fired in the same tick the event is eventually executed. This may need to minor but unobservable bugs with some hacky plugins.
For event callers
Calling events in the traditional way should not result in any semantic changes. If other (newer) plugins want to handle events from a non-async call, it's their own fault.
For event subclasses
There is minimal backward incompatibility for event subclasses. There are only edge cases with which this proposal will clash, namely:
Event
subclass with a method calledpause
/continue
Event
subclass to be serializable/thread-safe. This is because handler queue may be stored in theEvent
class in the implementation.Performance assessment
This RFC involves changes to the event calling mechanism. Its impact on performance is yet to be assessed after #1792 has been finalized, as it depends on #1792 (see below).
Tentatively, almost no impact on callEvent performance is expected. Minimal difference in performance between a callEvent and a callAsyncEvent call is expected, because the handler queue can be merged into an iterable array held in the Event instance. However, regular checking of timeouts may consume CPU unnecessarily, and shall be configurable.
Proposed versions
This RFC targets the API version that introduces #1792, or versions after that. Implementation will be based on #1792, because there may be semantic complexities regarding how a listener is triggered.
Expected ues cases
Regarding handlers
As stated in the introduction, this is mainly used for handlers that use AsyncTask to fetch some data. Nevertheless, it is also possible to suspend an event to wait for user input. If the event caller doesn't like this, they should override the
pause
method to limit the event to an acceptable amount of timeout.One possible abuse include "queuing". For example, it may be tempting for a developer to pause a PlayerPreLoginEvent to wait for the server to get less full. This is an incorrect use case, because it is totally uncertain when an online player would quit voluntarily. Instead, this suspends the server's resources for some time (although limited to be 1 minute by specification in this RFC), and weakens existent mechanisms like player limit.
On the other hand, it is correct to use this, for example, to wait for an immediate user input. For example, a developer could suspend a PlayerCommandPreprocessEvent of
/kill
to send a modal form, "Do you really want to suicide?" This is a form of feedback that the user is expected to reply to immediately, and setting the timeout to a few seconds is reasonable. The post-execution of PlayerCommandPreprocessEvent is pretty light as well. (As a matter of fact, the forms API as currently in #1476 is pretty similar to how AsyncTask looks like)Plugins can always register one
LOWEST
(or other priorities) and oneHIGHEST
priority handler respectively, where they start the AsyncTask atLOWEST
without pausing the event there, and then pause the event and wait for the result at theHIGHEST
(or otherwise any other appropriate) priority. This allows multiple plugins to execute their async tasks simultaneously. In this way, rather than adding up the waiting time for all events, only the waiting time of the slowest one will be the bottleneck. This concept is slightly complicated for elementary developers. On behalf of PocketMine API, it's possible although pretty difficult (by difficult, I mean we will spend weeks arguing which approach is better, just like... ykwim) to add a more convenient framework for such a mechanism. If this is to be done, it is out of scope of this RFC.Regarding callers
Not all events should be allowed to be async. In particular, it is almost a "must not" for high-frequency events like PlayerMoveEvent to be async, because hundreds or thousands of other instances of the same event will execute during the async time.
The text was updated successfully, but these errors were encountered: