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 #2

Closed
wants to merge 28 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5fe57a8
temporaly add Promise::all
ShockedPlot7560 Oct 14, 2023
a84fc2b
introduce AsyncEvent and ::callAsync()
ShockedPlot7560 Oct 14, 2023
7a4b9a0
events: asynchandler is defined by their return type and event type
ShockedPlot7560 Oct 22, 2023
9b2b92a
oops, remove test code
ShockedPlot7560 Oct 22, 2023
b78ff00
fix style
ShockedPlot7560 Oct 22, 2023
c250bb0
undo Promise covariant + improve array types
ShockedPlot7560 Oct 22, 2023
58155a7
fix PHPstan
ShockedPlot7560 Oct 22, 2023
2b2fa9d
phpstan: populate baseline
ShockedPlot7560 Oct 22, 2023
1176b70
Update src/player/Player.php
dktapps Oct 23, 2023
dc85bba
merge remote tracking
ShockedPlot7560 Oct 27, 2023
ed739cf
cannot call async event in sync context + remove Event dependency for…
ShockedPlot7560 Oct 27, 2023
7e87fbb
clarifying the exception message
ShockedPlot7560 Oct 27, 2023
5beaa3c
correction of various problems
ShockedPlot7560 Oct 27, 2023
cc6e8ef
move the asynchronous registration of handlers to a dedicated PluginM…
ShockedPlot7560 Oct 27, 2023
ca95b2f
fix PHPStan
ShockedPlot7560 Oct 27, 2023
823d4ea
inconsistency correction
ShockedPlot7560 Oct 27, 2023
243a303
follow up of #6110
ShockedPlot7560 Oct 27, 2023
aaa37ba
handlerListe: reduce code complexity
ShockedPlot7560 Oct 27, 2023
f82c422
remove using of Event API
ShockedPlot7560 Jan 21, 2024
64bbff6
Merge remote-tracking branch 'upstream/minor-next' into feat/async-ev…
ShockedPlot7560 Jan 21, 2024
d6b7a9e
merge remote tracking upstream
ShockedPlot7560 Jan 21, 2024
eb98141
resolve AsyncEvent with self instance
ShockedPlot7560 Jan 21, 2024
c1e3903
fix PHPstan
ShockedPlot7560 Jan 21, 2024
92319e7
merge minor-next
ShockedPlot7560 Feb 19, 2024
7ba0e15
Merge branch 'minor-next' into feat/async-events
ShockedPlot7560 May 29, 2024
c70397d
implement PlayerCreation & PlayerDataSave as async events
ShockedPlot7560 May 29, 2024
5d2c973
Release 5.100.0
ShockedPlot7560 May 29, 2024
6490590
5.100.1 is next
ShockedPlot7560 May 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@
use pocketmine\crash\CrashDumpRenderer;
use pocketmine\entity\EntityDataHelper;
use pocketmine\entity\Location;
use pocketmine\event\AsyncEvent;
use pocketmine\event\HandlerListManager;
use pocketmine\event\player\PlayerCreationAsyncEvent;
use pocketmine\event\player\PlayerCreationEvent;
use pocketmine\event\player\PlayerDataSaveAsyncEvent;
use pocketmine\event\player\PlayerDataSaveEvent;
use pocketmine\event\player\PlayerLoginEvent;
use pocketmine\event\server\CommandEvent;
Expand Down Expand Up @@ -530,6 +533,31 @@
});
}

/**
* @return Promise<null>
*/
public function saveOfflinePlayerDataAsync(string $name, CompoundTag $nbtTag) : Promise{
$ev = new PlayerDataSaveAsyncEvent($nbtTag, $name, $this->getPlayerExact($name));
if(!$this->shouldSavePlayerData()){
$ev->cancel();
}
$resolver = new PromiseResolver();

$ev->call()->onCompletion(
function (PlayerDataSaveAsyncEvent $event) use ($name, $resolver) : void{

Check failure on line 547 in src/Server.php

View workflow job for this annotation

GitHub Actions / PHP 8.1 / PHPStan analysis

Parameter #1 $onSuccess of method pocketmine\promise\Promise<pocketmine\event\AsyncEvent>::onCompletion() expects Closure(pocketmine\event\AsyncEvent): void, Closure(pocketmine\event\player\PlayerDataSaveAsyncEvent): void given.

Check failure on line 547 in src/Server.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 / PHPStan analysis

Parameter #1 $onSuccess of method pocketmine\promise\Promise<pocketmine\event\AsyncEvent>::onCompletion() expects Closure(pocketmine\event\AsyncEvent): void, Closure(pocketmine\event\player\PlayerDataSaveAsyncEvent): void given.

Check failure on line 547 in src/Server.php

View workflow job for this annotation

GitHub Actions / PHP 8.1 / PHPStan analysis

Parameter #1 $onSuccess of method pocketmine\promise\Promise<pocketmine\event\AsyncEvent>::onCompletion() expects Closure(pocketmine\event\AsyncEvent): void, Closure(pocketmine\event\player\PlayerDataSaveAsyncEvent): void given.

Check failure on line 547 in src/Server.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 / PHPStan analysis

Parameter #1 $onSuccess of method pocketmine\promise\Promise<pocketmine\event\AsyncEvent>::onCompletion() expects Closure(pocketmine\event\AsyncEvent): void, Closure(pocketmine\event\player\PlayerDataSaveAsyncEvent): void given.

Check failure on line 547 in src/Server.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 / PHPStan analysis

Parameter #1 $onSuccess of method pocketmine\promise\Promise<pocketmine\event\AsyncEvent>::onCompletion() expects Closure(pocketmine\event\AsyncEvent): void, Closure(pocketmine\event\player\PlayerDataSaveAsyncEvent): void given.

Check failure on line 547 in src/Server.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 / PHPStan analysis

Parameter #1 $onSuccess of method pocketmine\promise\Promise<pocketmine\event\AsyncEvent>::onCompletion() expects Closure(pocketmine\event\AsyncEvent): void, Closure(pocketmine\event\player\PlayerDataSaveAsyncEvent): void given.
if($event->isCancelled()){
$resolver->reject();
return;
}
$this->saveOfflinePlayerData($name, $event->getSaveData());
$resolver->resolve(null);
},
fn() => $this->logger->debug("Cancelled saving player data for $name")
);

return $resolver->getPromise();
}

public function saveOfflinePlayerData(string $name, CompoundTag $nbtTag) : void{
$ev = new PlayerDataSaveEvent($nbtTag, $name, $this->getPlayerExact($name));
if(!$this->shouldSavePlayerData()){
Expand All @@ -554,7 +582,29 @@
* @phpstan-return Promise<Player>
*/
public function createPlayer(NetworkSession $session, PlayerInfo $playerInfo, bool $authenticated, ?CompoundTag $offlinePlayerData) : Promise{
$globalResolver = new PromiseResolver();

$evAsync = new PlayerCreationAsyncEvent($session);
$evAsync->call()->onCompletion(
fn(PlayerCreationAsyncEvent $event) =>

Check failure on line 589 in src/Server.php

View workflow job for this annotation

GitHub Actions / PHP 8.1 / PHPStan analysis

Parameter #1 $onSuccess of method pocketmine\promise\Promise<pocketmine\event\AsyncEvent>::onCompletion() expects Closure(pocketmine\event\AsyncEvent): void, Closure(pocketmine\event\player\PlayerCreationAsyncEvent): void given.

Check failure on line 589 in src/Server.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 / PHPStan analysis

Parameter #1 $onSuccess of method pocketmine\promise\Promise<pocketmine\event\AsyncEvent>::onCompletion() expects Closure(pocketmine\event\AsyncEvent): void, Closure(pocketmine\event\player\PlayerCreationAsyncEvent): void given.

Check failure on line 589 in src/Server.php

View workflow job for this annotation

GitHub Actions / PHP 8.1 / PHPStan analysis

Parameter #1 $onSuccess of method pocketmine\promise\Promise<pocketmine\event\AsyncEvent>::onCompletion() expects Closure(pocketmine\event\AsyncEvent): void, Closure(pocketmine\event\player\PlayerCreationAsyncEvent): void given.

Check failure on line 589 in src/Server.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 / PHPStan analysis

Parameter #1 $onSuccess of method pocketmine\promise\Promise<pocketmine\event\AsyncEvent>::onCompletion() expects Closure(pocketmine\event\AsyncEvent): void, Closure(pocketmine\event\player\PlayerCreationAsyncEvent): void given.

Check failure on line 589 in src/Server.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 / PHPStan analysis

Parameter #1 $onSuccess of method pocketmine\promise\Promise<pocketmine\event\AsyncEvent>::onCompletion() expects Closure(pocketmine\event\AsyncEvent): void, Closure(pocketmine\event\player\PlayerCreationAsyncEvent): void given.

Check failure on line 589 in src/Server.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 / PHPStan analysis

Parameter #1 $onSuccess of method pocketmine\promise\Promise<pocketmine\event\AsyncEvent>::onCompletion() expects Closure(pocketmine\event\AsyncEvent): void, Closure(pocketmine\event\player\PlayerCreationAsyncEvent): void given.
$this->onCreatePlayer($event, $playerInfo, $authenticated, $offlinePlayerData)->onCompletion(
fn(Player $player) => $globalResolver->resolve($player),
fn() => $globalResolver->reject()
),
fn() => $globalResolver->reject()
);

return $globalResolver->getPromise();
}

/**
* @phpstan-return Promise<Player>
*/
private function onCreatePlayer(PlayerCreationAsyncEvent $event, PlayerInfo $playerInfo, bool $authenticated, ?CompoundTag $offlinePlayerData) : Promise{
$session = $event->getNetworkSession();
$ev = new PlayerCreationEvent($session);
$ev->setBaseClass($event->getBaseClass());
$ev->setPlayerClass($event->getPlayerClass());
$ev->call();
$class = $ev->getPlayerClass();

Expand Down
2 changes: 1 addition & 1 deletion src/VersionInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

final class VersionInfo{
public const NAME = "PocketMine-MP";
public const BASE_VERSION = "5.15.1";
public const BASE_VERSION = "5.100.1";
public const IS_DEVELOPMENT_BUILD = true;
public const BUILD_CHANNEL = "stable";

Expand Down
27 changes: 23 additions & 4 deletions src/command/defaults/SaveCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
use pocketmine\command\CommandSender;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\permission\DefaultPermissionNames;
use pocketmine\promise\Promise;
use pocketmine\promise\PromiseResolver;
use function microtime;
use function round;

Expand All @@ -44,15 +46,32 @@ public function execute(CommandSender $sender, string $commandLabel, array $args
Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_save_start());
$start = microtime(true);

$promises = [];
foreach($sender->getServer()->getOnlinePlayers() as $player){
$player->save();
$promises[] = $player->saveAsync();
}

foreach($sender->getServer()->getWorldManager()->getWorlds() as $world){
$world->save(true);
$resolver = new PromiseResolver();

if(count($promises) === 0){
$resolver->resolve(null);
} else {
Promise::all($promises)->onCompletion(
fn () => $resolver->resolve(null),
fn () => $resolver->reject()
);
}

Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_save_success((string) round(microtime(true) - $start, 3)));
$resolver->getPromise()->onCompletion(
function () use ($sender, $start) : void {
foreach($sender->getServer()->getWorldManager()->getWorlds() as $world){
$world->save(true);
}

Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_save_success((string) round(microtime(true) - $start, 3)));
},
fn() => Command::broadcastCommandMessage($sender, "§cUnable to save the server")
);

return true;
}
Expand Down
162 changes: 162 additions & 0 deletions src/event/AsyncEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
<?php

/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/

declare(strict_types=1);

namespace pocketmine\event;

use pocketmine\promise\Promise;
use pocketmine\promise\PromiseResolver;
use pocketmine\timings\Timings;
use pocketmine\utils\ObjectSet;
use function array_shift;
use function assert;
use function count;

/**
* This class is used to permit asynchronous event handling.
*
* When an event is called asynchronously, the event handlers are called by priority level.
* When all the promises of a priority level have been resolved, the next priority level is called.
*/
abstract class AsyncEvent{
/** @phpstan-var ObjectSet<Promise<null>> $promises */
private ObjectSet $promises;
/** @var array<class-string<AsyncEvent>, int> $delegatesCallDepth */
private static array $delegatesCallDepth = [];
private const MAX_EVENT_CALL_DEPTH = 50;

/**
* @phpstan-return Promise<self>
*/
final public function call() : Promise{
$this->promises = new ObjectSet();
if(!isset(self::$delegatesCallDepth[$class = static::class])){
self::$delegatesCallDepth[$class] = 0;
}

if(self::$delegatesCallDepth[$class] >= self::MAX_EVENT_CALL_DEPTH){
//this exception will be caught by the parent event call if all else fails
throw new \RuntimeException("Recursive event call detected (reached max depth of " . self::MAX_EVENT_CALL_DEPTH . " calls)");
}

$timings = Timings::getAsyncEventTimings($this);
$timings->startTiming();

++self::$delegatesCallDepth[$class];
try{
return $this->callAsyncDepth();
}finally{
--self::$delegatesCallDepth[$class];
$timings->stopTiming();
}
}

/**
* @phpstan-return Promise<self>
*/
private function callAsyncDepth() : Promise{
/** @phpstan-var PromiseResolver<self> $globalResolver */
$globalResolver = new PromiseResolver();

$priorities = EventPriority::ALL;
$testResolve = function () use (&$testResolve, &$priorities, $globalResolver){
if(count($priorities) === 0){
$globalResolver->resolve($this);
}else{
$this->callPriority(array_shift($priorities))->onCompletion(function() use ($testResolve) : void{
$testResolve();
}, function () use ($globalResolver) {
$globalResolver->reject();
});
}
};

$testResolve();

return $globalResolver->getPromise();
}

/**
* @phpstan-return Promise<null>
*/
private function callPriority(int $priority) : Promise{
$handlers = HandlerListManager::global()->getListFor(static::class)->getListenersByPriority($priority);

/** @phpstan-var PromiseResolver<null> $resolver */
$resolver = new PromiseResolver();

$nonConcurrentHandlers = [];
foreach($handlers as $registration){
assert($registration instanceof RegisteredAsyncListener);
if($registration->canBeCalledConcurrently()){
$result = $registration->callAsync($this);
if($result !== null) {
$this->promises->add($result);
}
}else{
$nonConcurrentHandlers[] = $registration;
}
}

$testResolve = function() use (&$nonConcurrentHandlers, &$testResolve, $resolver){
if(count($nonConcurrentHandlers) === 0){
$this->waitForPromises()->onCompletion(function() use ($resolver){
$resolver->resolve(null);
}, function() use ($resolver){
$resolver->reject();
});
}else{
$this->waitForPromises()->onCompletion(function() use (&$nonConcurrentHandlers, $testResolve){
$handler = array_shift($nonConcurrentHandlers);
assert($handler instanceof RegisteredAsyncListener);
$result = $handler->callAsync($this);
if($result !== null) {
$this->promises->add($result);
}
$testResolve();
}, function() use ($resolver) {
$resolver->reject();
});
}
};

$testResolve();

return $resolver->getPromise();
}

/**
* @phpstan-return Promise<array<int, null>>
*/
private function waitForPromises() : Promise{
$array = $this->promises->toArray();
$this->promises->clear();
if(count($array) === 0){
$resolver = new PromiseResolver();
$resolver->resolve([]);

return $resolver->getPromise();
}

return Promise::all($array);
}
}
20 changes: 16 additions & 4 deletions src/event/HandlerList.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

use pocketmine\plugin\Plugin;
use function array_merge;
use function array_merge_recursive;
use function krsort;
use function spl_object_id;
use const SORT_NUMERIC;
Expand All @@ -37,7 +38,7 @@ class HandlerList{
private array $affectedHandlerCaches = [];

/**
* @phpstan-param class-string<covariant Event> $class
* @phpstan-param class-string<Event|AsyncEvent> $class
*/
public function __construct(
private string $class,
Expand Down Expand Up @@ -126,12 +127,23 @@ public function getListenerList() : array{
$handlerLists[] = $currentList;
}

$listenersByPriority = [];
$listeners = [];
$asyncListeners = [];
$exclusiveAsyncListeners = [];
foreach($handlerLists as $currentList){
foreach($currentList->handlerSlots as $priority => $listeners){
$listenersByPriority[$priority] = array_merge($listenersByPriority[$priority] ?? [], $listeners);
foreach($currentList->handlerSlots as $priority => $listenersToSort){
foreach($listenersToSort as $listener){
if(!$listener instanceof RegisteredAsyncListener){
$listeners[$priority][] = $listener;
}elseif(!$listener->canBeCalledConcurrently()){
$asyncListeners[$priority][] = $listener;
}else{
$exclusiveAsyncListeners[$priority][] = $listener;
}
}
}
}
$listenersByPriority = array_merge_recursive($listeners, $asyncListeners, $exclusiveAsyncListeners);

//TODO: why on earth do the priorities have higher values for lower priority?
krsort($listenersByPriority, SORT_NUMERIC);
Expand Down
13 changes: 7 additions & 6 deletions src/event/HandlerListManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public static function global() : self{
private array $allLists = [];
/**
* @var RegisteredListenerCache[] event class name => cache
* @phpstan-var array<class-string<Event>, RegisteredListenerCache>
* @phpstan-var array<class-string<Event|AsyncEvent>, RegisteredListenerCache>
*/
private array $handlerCaches = [];

Expand All @@ -59,17 +59,17 @@ public function unregisterAll(RegisteredListener|Plugin|Listener|null $object =
}

/**
* @phpstan-param \ReflectionClass<Event> $class
* @phpstan-param \ReflectionClass<Event|AsyncEvent> $class
*/
private static function isValidClass(\ReflectionClass $class) : bool{
$tags = Utils::parseDocComment((string) $class->getDocComment());
return !$class->isAbstract() || isset($tags["allowHandle"]);
}

/**
* @phpstan-param \ReflectionClass<Event> $class
* @phpstan-param \ReflectionClass<Event|AsyncEvent> $class
*
* @phpstan-return \ReflectionClass<Event>|null
* @phpstan-return \ReflectionClass<Event|AsyncEvent>|null
*/
private static function resolveNearestHandleableParent(\ReflectionClass $class) : ?\ReflectionClass{
for($parent = $class->getParentClass(); $parent !== false; $parent = $parent->getParentClass()){
Expand All @@ -86,7 +86,8 @@ private static function resolveNearestHandleableParent(\ReflectionClass $class)
*
* Calling this method also lazily initializes the $classMap inheritance tree of handler lists.
*
* @phpstan-param class-string<covariant Event> $event
* @phpstan-template TEvent of Event|AsyncEvent
* @phpstan-param class-string<TEvent> $event
*
* @throws \ReflectionException
* @throws \InvalidArgumentException
Expand All @@ -112,7 +113,7 @@ public function getListFor(string $event) : HandlerList{
}

/**
* @phpstan-param class-string<covariant Event> $event
* @phpstan-param class-string<Event|AsyncEvent> $event
*
* @return RegisteredListener[]
*/
Expand Down
1 change: 1 addition & 0 deletions src/event/ListenerMethodTags.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ final class ListenerMethodTags{
public const HANDLE_CANCELLED = "handleCancelled";
public const NOT_HANDLER = "notHandler";
public const PRIORITY = "priority";
public const EXCLUSIVE_CALL = "exclusiveCall";
}
Loading
Loading