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

Generalize asynchronous events #6092

Open
wants to merge 56 commits into
base: minor-next
Choose a base branch
from
Open

Conversation

ShockedPlot7560
Copy link
Member

@ShockedPlot7560 ShockedPlot7560 commented Oct 14, 2023

Introduction

This PR would be more of a foretaste for #6015 in that it provides a means of generalizing asynchronous event calling.

An asynchronous event in this context is one whose action can be deferred by plugins specifying promises to be resolved.
The integrity of event priorities is guaranteed by calling them one by one. Before moving on to the next priority, we wait until all the promises of the current one have been fulfilled.
Only the state of the server and player context can't be guaranteed. However, the state of the event is unaffected, whether it is called asynchronous or synchronous.

Behaviour

When a listener of an asynchronous event returns a Promise, the event is marked as asynchronous and placed at the end of the call stack for its priority.
When a callAsync is called, the call flow is as follows:

1. for each priority:
   2. call async handlers which allows concurrent execution
   3. for each async handlers which doesn't allows concurrent execution:
       1. wait for previous promises
       2. clear all the promise
       3. call the handler and add the return promise into the waiting list
   4. wait for all promises
2. the promise returned by callAsync is resolved with their instance

In this way, each listener can add only one promise.
If, on the other hand, the handler is sensitive to changes in the event, it is preferable to add the @exclusiveCall tag, which specifies that it must be the only one being called.

Event transformation

If we wanted to implement the asynchronous chat event, this would look like this:

class PlayerChatAsyncEvent extends AsyncEvent implements Cancellable{
	use CancellableTrait;
... Logic here
}

When calling:

$ev = new PlayerChatAsyncEvent($this, $messagePart, $this->server->getBroadcastChannelSubscribers(Server::BROADCAST_CHANNEL_USERS), new StandardChatFormatter());
$ev->callAsync()->onCompletion(function(PlayerChatAsyncEvent $ev) : void{
	if(!$ev->isCancelled()){
		$this->server->broadcastMessage($ev->getFormatter()->format($ev->getPlayer()->getDisplayName(), $ev->getMessage()), $ev->getRecipients());
	}
}, function() {
	// NOOP
});

Changes

API changes

  • Add AsyncEvent class
  • Add Promise::all()

Behavioural changes

Backwards compatibility

Not for the moment

Follow-up

n/a

Tests

class Main extends PluginBase implements Listener{
	protected function onEnable() : void{
		$this->getServer()->getPluginManager()->registerEvents($this, $this);
	}

	public function onPlayerChatAsyncConc1(PlayerChatEvent $event) : Promise {
		return $this->timeout("Player " . $event->getPlayer()->getName() . " chat async concurrent 1");
	}

	public function onPlayerChatAsyncConc2(PlayerChatEvent $event) : Promise {
		return $this->timeout("Player " . $event->getPlayer()->getName() . " chat async concurrent 2");
	}

	/**
	 * @exclusiveCall
	 */
	public function onPlayerChatAsyncNoConc(PlayerChatEvent $event) : Promise {
		return $this->timeout("Player " . $event->getPlayer()->getName() . " chat async exclusiveCall");
	}

	private function timeout(string $message) : Promise {
		$promise = new PromiseResolver();

		$this->getScheduler()->scheduleDelayedTask(new ClosureTask(function() use ($message, $promise) {
			$this->getLogger()->debug("Resolving promise for message " . $message);
			$promise->resolve(null);
		}), 5 * 20);

		return $promise->getPromise();
	}
}

code came from #6015 and will be subject to probable changes
An asynchronous event is one that allows the addition of promises to be resolved before being completed.
This implementation integrates priority levels, allowing you to wait for all promises added to one level before moving on to the next.
This is made possible by separating the event call logic into several functions, which can then be integrated into AsyncEventTrait::callAsync()
@ShockedPlot7560 ShockedPlot7560 added Category: API Related to the plugin API Category: Core Related to internal functionality Type: Enhancement Contributes features or other improvements to PocketMine-MP labels Oct 14, 2023
@SOF3 SOF3 self-requested a review October 15, 2023 02:36
src/promise/Promise.php Outdated Show resolved Hide resolved
src/promise/Promise.php Outdated Show resolved Hide resolved
src/promise/Promise.php Outdated Show resolved Hide resolved
src/promise/Promise.php Outdated Show resolved Hide resolved
src/event/AsyncEventTrait.php Outdated Show resolved Hide resolved
src/event/AsyncEventTrait.php Outdated Show resolved Hide resolved
src/event/AsyncEventTrait.php Outdated Show resolved Hide resolved
src/event/Event.php Outdated Show resolved Hide resolved
If the event is async and the handler return Promise, it will be handle as an async event.
However, if the event is async and the handler return smth different than Promise, it will be handle synchronously.
An async handler can specify if it wishes to be called with no concurrent handlers with the tag @noConcurrentCall
@ShockedPlot7560
Copy link
Member Author

New behaviour

Above all, the logic of synchronous events remains the same, even for asynchronous events, as long as they are called synchronously.

When a listener of an asynchronous event returns a Promise, the event is marked as asynchronous and placed at the end of the call stack for its priority.
When a callAsync is called, the call flow is as follows:

1. for each priority:
   1. call sync handlers (return type is different than Promise).
   2. call async handlers which allows concurrent execution
   3. for each async handlers which doesn't allows concurrent execution:
       1. wait for previous promises
       2. clear all the promise
       3. call the handler and add the return promise into the waiting list
   4. wait for all promises
2. the promise returned by callAsync is resolved

In this way, each listener can add only one promise.
If, on the other hand, the handler is sensitive to changes in the event, it is preferable to add the @noConcurrentCall tag, which specifies that it must be the only one being called.

@ShockedPlot7560 ShockedPlot7560 changed the title Add "async" events Generalize asynchronous events Oct 22, 2023
Copy link
Member

@SOF3 SOF3 left a comment

Choose a reason for hiding this comment

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

tldr AsyncEventDelegate, will look again when PR is ready

src/event/AsyncEvent.php Outdated Show resolved Hide resolved
src/event/AsyncEventTrait.php Outdated Show resolved Hide resolved
src/event/HandlerList.php Outdated Show resolved Hide resolved
src/event/HandlerList.php Outdated Show resolved Hide resolved
src/event/HandlerList.php Outdated Show resolved Hide resolved
src/event/ListenerMethodTags.php Outdated Show resolved Hide resolved
src/plugin/PluginManager.php Show resolved Hide resolved
src/plugin/PluginManager.php Outdated Show resolved Hide resolved
src/plugin/PluginManager.php Show resolved Hide resolved
src/plugin/PluginManager.php Show resolved Hide resolved
src/player/Player.php Outdated Show resolved Hide resolved
Co-authored-by: Javier León <[email protected]>
github-actions[bot]
github-actions bot previously approved these changes Nov 14, 2024
@ShockedPlot7560
Copy link
Member Author

As I understand, now, we will accept async listeners to return null or Promise ?

@SenseiTarzan

This comment was marked as off-topic.

@dktapps
Copy link
Member

dktapps commented Nov 19, 2024

As I understand, now, we will accept async listeners to return null or Promise ?

Yep, they'll allow the next handler in the queue to be called immediately if returning null

this required mocking to get around #6524. Longer term we should make improvements to avoid the need for mocking here.
pmmp-admin-bot[bot]
pmmp-admin-bot bot previously approved these changes Nov 20, 2024
pmmp-admin-bot[bot]
pmmp-admin-bot bot previously approved these changes Nov 20, 2024
pmmp-admin-bot[bot]
pmmp-admin-bot bot previously approved these changes Nov 20, 2024
@dktapps
Copy link
Member

dktapps commented Nov 29, 2024

I think the main thing still on the TODO list is to buffer incomplete events. The existing mechanism using stack depth won't cut it. We need a way to track which handlers didn't fulfil their promises so that the correct plugin can be properly blamed when an event fails to complete.

@dktapps dktapps added the Status: Incomplete Work in progress label Nov 29, 2024
pmmp-admin-bot[bot]
pmmp-admin-bot bot previously approved these changes Nov 29, 2024
pmmp-admin-bot[bot]
pmmp-admin-bot bot previously approved these changes Nov 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Category: API Related to the plugin API Category: Async events :( Related to async events Category: Core Related to internal functionality Status: Incomplete Work in progress Type: Enhancement Contributes features or other improvements to PocketMine-MP
Development

Successfully merging this pull request may close these issues.

5 participants