mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-09-14 05:15:11 +00:00
Compare commits
58 Commits
dependabot
...
feat/async
Author | SHA1 | Date | |
---|---|---|---|
39c9387efe | |||
31275ba681 | |||
a0d69a9fb8 | |||
e8ec81d123 | |||
866d473553 | |||
d9080f182c | |||
d9f5634262 | |||
406e2c6c57 | |||
4451770ca3 | |||
d2d663b1c9 | |||
117026cb83 | |||
a7a1077676 | |||
0a56cf877b | |||
11fdf79a7c | |||
edae9f26e4 | |||
667656b1c6 | |||
972a9fb201 | |||
ac1cf73f8e | |||
96989d1dc4 | |||
8aed5d6b27 | |||
fa796535ff | |||
32b1d6c0c2 | |||
6f40c6fc1d | |||
a6a44bde90 | |||
409066c8f5 | |||
cb2fadeb26 | |||
a14afb4bb5 | |||
db88e543fa | |||
c426677841 | |||
17ae932d31 | |||
8f48fe4856 | |||
48d2430064 | |||
b82d47dd32 | |||
86fb041a65 | |||
b276133003 | |||
c1e3903934 | |||
eb9814197b | |||
d6b7a9ed45 | |||
64bbff6286 | |||
f82c422f64 | |||
aaa37baf2e | |||
243a3035ba | |||
823d4ead6a | |||
ca95b2f28d | |||
cc6e8ef232 | |||
5beaa3ce4e | |||
7e87fbbb7a | |||
ed739cff4f | |||
dc85bba995 | |||
1176b7090c | |||
2b2fa9ddf1 | |||
58155a77fb | |||
c250bb0da7 | |||
b78ff00418 | |||
9b2b92ac1f | |||
7a4b9a0367 | |||
a84fc2b901 | |||
5fe57a8f6f |
@ -56,6 +56,7 @@ PocketMine-MP has three primary branches of development.
|
||||
| Deprecating API classes, methods or constants | ❌ | ✔️ | ✔️ |
|
||||
| Adding optional parameters to an API method | ❌ | ✔️ | ✔️ |
|
||||
| Changing API behaviour | ❌ | 🟡 Only if backwards-compatible | ✔️ |
|
||||
| Changing an event from sync to async or vice versa | ❌ | ❌ | ✔️ |
|
||||
| Removal of API | ❌ | ❌ | ✔️ |
|
||||
| Backwards-incompatible API change (e.g. renaming a method) | ❌ | ❌ | ✔️ |
|
||||
| Backwards-incompatible internals change (e.g. changing things in `pocketmine\network\mcpe`) | ❌ | ✔️ | ✔️ |
|
||||
|
@ -39,6 +39,7 @@ use pocketmine\crash\CrashDumpRenderer;
|
||||
use pocketmine\data\bedrock\BedrockDataFiles;
|
||||
use pocketmine\entity\EntityDataHelper;
|
||||
use pocketmine\entity\Location;
|
||||
use pocketmine\event\AsyncHandlerListManager;
|
||||
use pocketmine\event\HandlerListManager;
|
||||
use pocketmine\event\player\PlayerCreationEvent;
|
||||
use pocketmine\event\player\PlayerDataSaveEvent;
|
||||
@ -1527,6 +1528,7 @@ class Server{
|
||||
|
||||
$this->logger->debug("Removing event handlers");
|
||||
HandlerListManager::global()->unregisterAll();
|
||||
AsyncHandlerListManager::global()->unregisterAll();
|
||||
|
||||
if(isset($this->asyncPool)){
|
||||
$this->logger->debug("Shutting down async task worker pool");
|
||||
|
134
src/event/AsyncEvent.php
Normal file
134
src/event/AsyncEvent.php
Normal file
@ -0,0 +1,134 @@
|
||||
<?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\Utils;
|
||||
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{
|
||||
/** @var array<int, int> $handlersCallState */
|
||||
private static array $handlersCallState = [];
|
||||
private const MAX_CONCURRENT_CALLS = 1000; //max number of concurrent calls to a single handler
|
||||
|
||||
/**
|
||||
* @phpstan-return Promise<static>
|
||||
*/
|
||||
final public function call() : Promise{
|
||||
$timings = Timings::getAsyncEventTimings($this);
|
||||
$timings->startTiming();
|
||||
|
||||
try{
|
||||
/** @phpstan-var PromiseResolver<static> $globalResolver */
|
||||
$globalResolver = new PromiseResolver();
|
||||
|
||||
$handlers = AsyncHandlerListManager::global()->getHandlersFor(static::class);
|
||||
if(count($handlers) > 0){
|
||||
$this->processRemainingHandlers($handlers, fn() => $globalResolver->resolve($this), $globalResolver->reject(...));
|
||||
}else{
|
||||
$globalResolver->resolve($this);
|
||||
}
|
||||
|
||||
return $globalResolver->getPromise();
|
||||
}finally{
|
||||
$timings->stopTiming();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AsyncRegisteredListener[] $handlers
|
||||
* @phpstan-param list<AsyncRegisteredListener> $handlers
|
||||
* @phpstan-param \Closure() : void $resolve
|
||||
* @phpstan-param \Closure() : void $reject
|
||||
*/
|
||||
private function processRemainingHandlers(array $handlers, \Closure $resolve, \Closure $reject) : void{
|
||||
$currentPriority = null;
|
||||
$awaitPromises = [];
|
||||
foreach($handlers as $k => $handler){
|
||||
$priority = $handler->getPriority();
|
||||
if(count($awaitPromises) > 0 && $currentPriority !== null && $currentPriority !== $priority){
|
||||
//wait for concurrent promises from previous priority to complete
|
||||
break;
|
||||
}
|
||||
|
||||
$currentPriority = $priority;
|
||||
$handlerId = spl_object_id($handler) << 3 | $priority;
|
||||
if(!isset(self::$handlersCallState[$handlerId])){
|
||||
self::$handlersCallState[$handlerId] = 0;
|
||||
}
|
||||
if(self::$handlersCallState[$handlerId] >= self::MAX_CONCURRENT_CALLS){
|
||||
throw new \RuntimeException("Concurrent call limit reached for handler " .
|
||||
Utils::getNiceClosureName($handler->getHandler()) . "(" . Utils::getNiceClassName($this) . ")" .
|
||||
" (max: " . self::MAX_CONCURRENT_CALLS . ")");
|
||||
}
|
||||
$removeCallback = static fn() => --self::$handlersCallState[$handlerId];
|
||||
if($handler->canBeCalledConcurrently()){
|
||||
unset($handlers[$k]);
|
||||
++self::$handlersCallState[$handlerId];
|
||||
$promise = $handler->callAsync($this);
|
||||
if($promise !== null){
|
||||
$promise->onCompletion($removeCallback, $removeCallback);
|
||||
$awaitPromises[] = $promise;
|
||||
}else{
|
||||
$removeCallback();
|
||||
}
|
||||
}else{
|
||||
if(count($awaitPromises) > 0){
|
||||
//wait for concurrent promises to complete
|
||||
break;
|
||||
}
|
||||
|
||||
unset($handlers[$k]);
|
||||
++self::$handlersCallState[$handlerId];
|
||||
$promise = $handler->callAsync($this);
|
||||
if($promise !== null){
|
||||
$promise->onCompletion($removeCallback, $removeCallback);
|
||||
$promise->onCompletion(
|
||||
onSuccess: fn() => $this->processRemainingHandlers($handlers, $resolve, $reject),
|
||||
onFailure: $reject
|
||||
);
|
||||
return;
|
||||
}
|
||||
$removeCallback();
|
||||
}
|
||||
}
|
||||
|
||||
if(count($awaitPromises) > 0){
|
||||
Promise::all($awaitPromises)->onCompletion(
|
||||
onSuccess: fn() => $this->processRemainingHandlers($handlers, $resolve, $reject),
|
||||
onFailure: $reject
|
||||
);
|
||||
}else{
|
||||
$resolve();
|
||||
}
|
||||
}
|
||||
}
|
58
src/event/AsyncHandlerListManager.php
Normal file
58
src/event/AsyncHandlerListManager.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?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 function uasort;
|
||||
|
||||
/**
|
||||
* @phpstan-extends BaseHandlerListManager<AsyncEvent, AsyncRegisteredListener>
|
||||
*/
|
||||
final class AsyncHandlerListManager extends BaseHandlerListManager{
|
||||
private static ?self $globalInstance = null;
|
||||
|
||||
public static function global() : self{
|
||||
return self::$globalInstance ?? (self::$globalInstance = new self());
|
||||
}
|
||||
|
||||
protected function getBaseEventClass() : string{
|
||||
return AsyncEvent::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param array<int, AsyncRegisteredListener> $listeners
|
||||
* @phpstan-return array<int, AsyncRegisteredListener>
|
||||
*/
|
||||
private static function sortSamePriorityHandlers(array $listeners) : array{
|
||||
uasort($listeners, function(AsyncRegisteredListener $left, AsyncRegisteredListener $right) : int{
|
||||
//Promise::all() can be used more efficiently if concurrent handlers are grouped together.
|
||||
//It's not important whether they are grouped before or after exclusive handlers.
|
||||
return $left->canBeCalledConcurrently() <=> $right->canBeCalledConcurrently();
|
||||
});
|
||||
return $listeners;
|
||||
}
|
||||
|
||||
protected function createHandlerList(string $event, ?HandlerList $parentList, RegisteredListenerCache $handlerCache) : HandlerList{
|
||||
return new HandlerList($event, $parentList, $handlerCache, self::sortSamePriorityHandlers(...));
|
||||
}
|
||||
}
|
60
src/event/AsyncRegisteredListener.php
Normal file
60
src/event/AsyncRegisteredListener.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?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\plugin\Plugin;
|
||||
use pocketmine\promise\Promise;
|
||||
use pocketmine\timings\TimingsHandler;
|
||||
|
||||
class AsyncRegisteredListener extends BaseRegisteredListener{
|
||||
public function __construct(
|
||||
\Closure $handler,
|
||||
int $priority,
|
||||
Plugin $plugin,
|
||||
bool $handleCancelled,
|
||||
private bool $exclusiveCall,
|
||||
TimingsHandler $timings
|
||||
){
|
||||
parent::__construct($handler, $priority, $plugin, $handleCancelled, $timings);
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-return Promise<null>|null
|
||||
*/
|
||||
public function callAsync(AsyncEvent $event) : ?Promise{
|
||||
if($event instanceof Cancellable && $event->isCancelled() && !$this->isHandlingCancelled()){
|
||||
return null;
|
||||
}
|
||||
$this->timings->startTiming();
|
||||
try{
|
||||
return ($this->handler)($event);
|
||||
}finally{
|
||||
$this->timings->stopTiming();
|
||||
}
|
||||
}
|
||||
|
||||
public function canBeCalledConcurrently() : bool{
|
||||
return !$this->exclusiveCall;
|
||||
}
|
||||
}
|
156
src/event/BaseHandlerListManager.php
Normal file
156
src/event/BaseHandlerListManager.php
Normal file
@ -0,0 +1,156 @@
|
||||
<?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\plugin\Plugin;
|
||||
use pocketmine\utils\Utils;
|
||||
|
||||
/**
|
||||
* @phpstan-template TEvent of Event|AsyncEvent
|
||||
* @phpstan-template TRegisteredListener of BaseRegisteredListener
|
||||
*
|
||||
* @phpstan-type THandlerList HandlerList<TRegisteredListener>
|
||||
*/
|
||||
abstract class BaseHandlerListManager{
|
||||
/**
|
||||
* @var HandlerList[] classname => HandlerList
|
||||
* @phpstan-var array<class-string<covariant TEvent>, THandlerList>
|
||||
*/
|
||||
private array $allLists = [];
|
||||
/**
|
||||
* @var RegisteredListenerCache[] event class name => cache
|
||||
* @phpstan-var array<class-string<TEvent>, RegisteredListenerCache<TRegisteredListener>>
|
||||
*/
|
||||
private array $handlerCaches = [];
|
||||
|
||||
/**
|
||||
* Unregisters all the listeners
|
||||
* If a Plugin or Listener is passed, all the listeners with that object will be removed
|
||||
*
|
||||
* @phpstan-param TRegisteredListener|Plugin|Listener|null $object
|
||||
*/
|
||||
public function unregisterAll(BaseRegisteredListener|Plugin|Listener|null $object = null) : void{
|
||||
if($object !== null){
|
||||
foreach($this->allLists as $h){
|
||||
$h->unregister($object);
|
||||
}
|
||||
}else{
|
||||
foreach($this->allLists as $h){
|
||||
$h->clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param \ReflectionClass<TEvent> $class
|
||||
*/
|
||||
private static function isValidClass(\ReflectionClass $class) : bool{
|
||||
$tags = Utils::parseDocComment((string) $class->getDocComment());
|
||||
return !$class->isAbstract() || isset($tags["allowHandle"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param \ReflectionClass<TEvent> $class
|
||||
*
|
||||
* @phpstan-return \ReflectionClass<TEvent>|null
|
||||
*/
|
||||
private static function resolveNearestHandleableParent(\ReflectionClass $class) : ?\ReflectionClass{
|
||||
for($parent = $class->getParentClass(); $parent !== false; $parent = $parent->getParentClass()){
|
||||
if(self::isValidClass($parent)){
|
||||
return $parent;
|
||||
}
|
||||
//NOOP
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-return class-string<TEvent>
|
||||
*/
|
||||
abstract protected function getBaseEventClass() : string;
|
||||
|
||||
/**
|
||||
* @phpstan-param class-string<covariant TEvent> $event
|
||||
* @phpstan-param HandlerList<TRegisteredListener>|null $parentList
|
||||
* @phpstan-param RegisteredListenerCache<TRegisteredListener> $handlerCache
|
||||
*
|
||||
* @phpstan-return THandlerList
|
||||
*/
|
||||
abstract protected function createHandlerList(string $event, ?HandlerList $parentList, RegisteredListenerCache $handlerCache) : HandlerList;
|
||||
|
||||
/**
|
||||
* Returns the HandlerList for listeners that explicitly handle this event.
|
||||
*
|
||||
* Calling this method also lazily initializes the $classMap inheritance tree of handler lists.
|
||||
*
|
||||
* @phpstan-param class-string<covariant TEvent> $event
|
||||
* @phpstan-return THandlerList
|
||||
*
|
||||
* @throws \ReflectionException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function getListFor(string $event) : HandlerList{
|
||||
if(isset($this->allLists[$event])){
|
||||
return $this->allLists[$event];
|
||||
}
|
||||
|
||||
$class = new \ReflectionClass($event);
|
||||
if(!$class->isSubclassOf($this->getBaseEventClass())){
|
||||
throw new \InvalidArgumentException("Cannot get sync handler list for async event");
|
||||
}
|
||||
if(!self::isValidClass($class)){
|
||||
throw new \InvalidArgumentException("Event must be non-abstract or have the @allowHandle annotation");
|
||||
}
|
||||
|
||||
$parent = self::resolveNearestHandleableParent($class);
|
||||
/** @phpstan-var RegisteredListenerCache<TRegisteredListener> $cache */
|
||||
$cache = new RegisteredListenerCache();
|
||||
$this->handlerCaches[$event] = $cache;
|
||||
return $this->allLists[$event] = $this->createHandlerList(
|
||||
$event,
|
||||
parentList: $parent !== null ? $this->getListFor($parent->getName()) : null,
|
||||
handlerCache: $cache
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param class-string<covariant TEvent> $event
|
||||
*
|
||||
* @return RegisteredListener[]
|
||||
* @phpstan-return list<TRegisteredListener>
|
||||
*/
|
||||
public function getHandlersFor(string $event) : array{
|
||||
$cache = $this->handlerCaches[$event] ?? null;
|
||||
//getListFor() will populate the cache for the next call
|
||||
return $cache?->list ?? $this->getListFor($event)->getListenerList();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HandlerList[]
|
||||
* @phpstan-return array<class-string<covariant TEvent>, THandlerList>
|
||||
*/
|
||||
public function getAll() : array{
|
||||
return $this->allLists;
|
||||
}
|
||||
}
|
58
src/event/BaseRegisteredListener.php
Normal file
58
src/event/BaseRegisteredListener.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?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\plugin\Plugin;
|
||||
use pocketmine\timings\TimingsHandler;
|
||||
use function in_array;
|
||||
|
||||
abstract class BaseRegisteredListener{
|
||||
public function __construct(
|
||||
protected \Closure $handler,
|
||||
private int $priority,
|
||||
private Plugin $plugin,
|
||||
private bool $handleCancelled,
|
||||
protected TimingsHandler $timings
|
||||
){
|
||||
if(!in_array($priority, EventPriority::ALL, true)){
|
||||
throw new \InvalidArgumentException("Invalid priority number $priority");
|
||||
}
|
||||
}
|
||||
|
||||
public function getHandler() : \Closure{
|
||||
return $this->handler;
|
||||
}
|
||||
|
||||
public function getPlugin() : Plugin{
|
||||
return $this->plugin;
|
||||
}
|
||||
|
||||
public function getPriority() : int{
|
||||
return $this->priority;
|
||||
}
|
||||
|
||||
public function isHandlingCancelled() : bool{
|
||||
return $this->handleCancelled;
|
||||
}
|
||||
}
|
@ -29,23 +29,33 @@ use function krsort;
|
||||
use function spl_object_id;
|
||||
use const SORT_NUMERIC;
|
||||
|
||||
/**
|
||||
* @phpstan-template TListener of BaseRegisteredListener
|
||||
*/
|
||||
class HandlerList{
|
||||
/**
|
||||
* @var RegisteredListener[][]
|
||||
* @phpstan-var array<int, array<int, RegisteredListener>>
|
||||
* @var BaseRegisteredListener[][]
|
||||
* @phpstan-var array<int, array<int, TListener>>
|
||||
*/
|
||||
private array $handlerSlots = [];
|
||||
|
||||
/** @var RegisteredListenerCache[] */
|
||||
/**
|
||||
* @var RegisteredListenerCache[]
|
||||
* @phpstan-var array<int, RegisteredListenerCache<TListener>>
|
||||
*/
|
||||
private array $affectedHandlerCaches = [];
|
||||
|
||||
/**
|
||||
* @phpstan-param class-string<covariant Event> $class
|
||||
* @phpstan-param class-string $class
|
||||
* @phpstan-param ?static<TListener> $parentList
|
||||
* @phpstan-param RegisteredListenerCache<TListener> $handlerCache
|
||||
* @phpstan-param ?\Closure(array<int, TListener>) : array<int, TListener> $sortSamePriorityHandlers
|
||||
*/
|
||||
public function __construct(
|
||||
private string $class,
|
||||
private ?HandlerList $parentList,
|
||||
private RegisteredListenerCache $handlerCache = new RegisteredListenerCache()
|
||||
private RegisteredListenerCache $handlerCache = new RegisteredListenerCache(),
|
||||
private ?\Closure $sortSamePriorityHandlers = null
|
||||
){
|
||||
for($list = $this; $list !== null; $list = $list->parentList){
|
||||
$list->affectedHandlerCaches[spl_object_id($this->handlerCache)] = $this->handlerCache;
|
||||
@ -53,9 +63,9 @@ class HandlerList{
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
* @phpstan-param TListener $listener
|
||||
*/
|
||||
public function register(RegisteredListener $listener) : void{
|
||||
public function register(BaseRegisteredListener $listener) : void{
|
||||
if(isset($this->handlerSlots[$listener->getPriority()][spl_object_id($listener)])){
|
||||
throw new \InvalidArgumentException("This listener is already registered to priority {$listener->getPriority()} of event {$this->class}");
|
||||
}
|
||||
@ -64,7 +74,8 @@ class HandlerList{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RegisteredListener[] $listeners
|
||||
* @param BaseRegisteredListener[] $listeners
|
||||
* @phpstan-param array<TListener> $listeners
|
||||
*/
|
||||
public function registerAll(array $listeners) : void{
|
||||
foreach($listeners as $listener){
|
||||
@ -73,7 +84,10 @@ class HandlerList{
|
||||
$this->invalidateAffectedCaches();
|
||||
}
|
||||
|
||||
public function unregister(RegisteredListener|Plugin|Listener $object) : void{
|
||||
/**
|
||||
* @phpstan-param TListener|Plugin|Listener $object
|
||||
*/
|
||||
public function unregister(BaseRegisteredListener|Plugin|Listener $object) : void{
|
||||
if($object instanceof Plugin || $object instanceof Listener){
|
||||
foreach($this->handlerSlots as $priority => $list){
|
||||
foreach($list as $hash => $listener){
|
||||
@ -96,12 +110,16 @@ class HandlerList{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RegisteredListener[]
|
||||
* @return BaseRegisteredListener[]
|
||||
* @phpstan-return array<int, TListener>
|
||||
*/
|
||||
public function getListenersByPriority(int $priority) : array{
|
||||
return $this->handlerSlots[$priority] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-return static<TListener>
|
||||
*/
|
||||
public function getParent() : ?HandlerList{
|
||||
return $this->parentList;
|
||||
}
|
||||
@ -116,8 +134,8 @@ class HandlerList{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RegisteredListener[]
|
||||
* @phpstan-return list<RegisteredListener>
|
||||
* @return BaseRegisteredListener[]
|
||||
* @phpstan-return list<TListener>
|
||||
*/
|
||||
public function getListenerList() : array{
|
||||
if($this->handlerCache->list !== null){
|
||||
@ -132,7 +150,12 @@ class HandlerList{
|
||||
$listenersByPriority = [];
|
||||
foreach($handlerLists as $currentList){
|
||||
foreach($currentList->handlerSlots as $priority => $listeners){
|
||||
$listenersByPriority[$priority] = array_merge($listenersByPriority[$priority] ?? [], $listeners);
|
||||
$listenersByPriority[$priority] = array_merge(
|
||||
$listenersByPriority[$priority] ?? [],
|
||||
$this->sortSamePriorityHandlers !== null ?
|
||||
($this->sortSamePriorityHandlers)($listeners) :
|
||||
$listeners
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,109 +23,21 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\event;
|
||||
|
||||
use pocketmine\plugin\Plugin;
|
||||
use pocketmine\utils\Utils;
|
||||
|
||||
class HandlerListManager{
|
||||
|
||||
/**
|
||||
* @phpstan-extends BaseHandlerListManager<Event, RegisteredListener>
|
||||
*/
|
||||
class HandlerListManager extends BaseHandlerListManager{
|
||||
private static ?self $globalInstance = null;
|
||||
|
||||
public static function global() : self{
|
||||
return self::$globalInstance ?? (self::$globalInstance = new self());
|
||||
}
|
||||
|
||||
/** @var HandlerList[] classname => HandlerList */
|
||||
private array $allLists = [];
|
||||
/**
|
||||
* @var RegisteredListenerCache[] event class name => cache
|
||||
* @phpstan-var array<class-string<Event>, RegisteredListenerCache>
|
||||
*/
|
||||
private array $handlerCaches = [];
|
||||
|
||||
/**
|
||||
* Unregisters all the listeners
|
||||
* If a Plugin or Listener is passed, all the listeners with that object will be removed
|
||||
*/
|
||||
public function unregisterAll(RegisteredListener|Plugin|Listener|null $object = null) : void{
|
||||
if($object instanceof Listener || $object instanceof Plugin || $object instanceof RegisteredListener){
|
||||
foreach($this->allLists as $h){
|
||||
$h->unregister($object);
|
||||
}
|
||||
}else{
|
||||
foreach($this->allLists as $h){
|
||||
$h->clear();
|
||||
}
|
||||
}
|
||||
protected function getBaseEventClass() : string{
|
||||
return Event::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param \ReflectionClass<Event> $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-return \ReflectionClass<Event>|null
|
||||
*/
|
||||
private static function resolveNearestHandleableParent(\ReflectionClass $class) : ?\ReflectionClass{
|
||||
for($parent = $class->getParentClass(); $parent !== false; $parent = $parent->getParentClass()){
|
||||
if(self::isValidClass($parent)){
|
||||
return $parent;
|
||||
}
|
||||
//NOOP
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HandlerList for listeners that explicitly handle this event.
|
||||
*
|
||||
* Calling this method also lazily initializes the $classMap inheritance tree of handler lists.
|
||||
*
|
||||
* @phpstan-param class-string<covariant Event> $event
|
||||
*
|
||||
* @throws \ReflectionException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function getListFor(string $event) : HandlerList{
|
||||
if(isset($this->allLists[$event])){
|
||||
return $this->allLists[$event];
|
||||
}
|
||||
|
||||
$class = new \ReflectionClass($event);
|
||||
if(!self::isValidClass($class)){
|
||||
throw new \InvalidArgumentException("Event must be non-abstract or have the @allowHandle annotation");
|
||||
}
|
||||
|
||||
$parent = self::resolveNearestHandleableParent($class);
|
||||
$cache = new RegisteredListenerCache();
|
||||
$this->handlerCaches[$event] = $cache;
|
||||
return $this->allLists[$event] = new HandlerList(
|
||||
$event,
|
||||
parentList: $parent !== null ? $this->getListFor($parent->getName()) : null,
|
||||
handlerCache: $cache
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param class-string<covariant Event> $event
|
||||
*
|
||||
* @return RegisteredListener[]
|
||||
*/
|
||||
public function getHandlersFor(string $event) : array{
|
||||
$cache = $this->handlerCaches[$event] ?? null;
|
||||
//getListFor() will populate the cache for the next call
|
||||
return $cache->list ?? $this->getListFor($event)->getListenerList();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HandlerList[]
|
||||
*/
|
||||
public function getAll() : array{
|
||||
return $this->allLists;
|
||||
protected function createHandlerList(string $event, ?HandlerList $parentList, RegisteredListenerCache $handlerCache) : HandlerList{
|
||||
return new HandlerList($event, $parentList, $handlerCache);
|
||||
}
|
||||
}
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -23,34 +23,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\event;
|
||||
|
||||
use pocketmine\plugin\Plugin;
|
||||
use pocketmine\timings\TimingsHandler;
|
||||
use function in_array;
|
||||
|
||||
class RegisteredListener{
|
||||
public function __construct(
|
||||
private \Closure $handler,
|
||||
private int $priority,
|
||||
private Plugin $plugin,
|
||||
private bool $handleCancelled,
|
||||
private TimingsHandler $timings
|
||||
){
|
||||
if(!in_array($priority, EventPriority::ALL, true)){
|
||||
throw new \InvalidArgumentException("Invalid priority number $priority");
|
||||
}
|
||||
}
|
||||
|
||||
public function getHandler() : \Closure{
|
||||
return $this->handler;
|
||||
}
|
||||
|
||||
public function getPlugin() : Plugin{
|
||||
return $this->plugin;
|
||||
}
|
||||
|
||||
public function getPriority() : int{
|
||||
return $this->priority;
|
||||
}
|
||||
class RegisteredListener extends BaseRegisteredListener{
|
||||
|
||||
public function callEvent(Event $event) : void{
|
||||
if($event instanceof Cancellable && $event->isCancelled() && !$this->isHandlingCancelled()){
|
||||
@ -63,8 +36,4 @@ class RegisteredListener{
|
||||
$this->timings->stopTiming();
|
||||
}
|
||||
}
|
||||
|
||||
public function isHandlingCancelled() : bool{
|
||||
return $this->handleCancelled;
|
||||
}
|
||||
}
|
||||
|
@ -25,14 +25,14 @@ namespace pocketmine\event;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @phpstan-template TListener
|
||||
*/
|
||||
final class RegisteredListenerCache{
|
||||
|
||||
/**
|
||||
* List of all handlers that will be called for a particular event, ordered by execution order.
|
||||
*
|
||||
* @var RegisteredListener[]
|
||||
* @phpstan-var list<RegisteredListener>
|
||||
* @phpstan-var list<TListener>
|
||||
*/
|
||||
public ?array $list = null;
|
||||
}
|
||||
|
@ -23,6 +23,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\plugin;
|
||||
|
||||
use pocketmine\event\AsyncEvent;
|
||||
use pocketmine\event\AsyncHandlerListManager;
|
||||
use pocketmine\event\AsyncRegisteredListener;
|
||||
use pocketmine\event\Cancellable;
|
||||
use pocketmine\event\Event;
|
||||
use pocketmine\event\EventPriority;
|
||||
@ -36,6 +39,7 @@ use pocketmine\lang\KnownTranslationFactory;
|
||||
use pocketmine\permission\DefaultPermissions;
|
||||
use pocketmine\permission\PermissionManager;
|
||||
use pocketmine\permission\PermissionParser;
|
||||
use pocketmine\promise\Promise;
|
||||
use pocketmine\Server;
|
||||
use pocketmine\timings\Timings;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
@ -529,6 +533,7 @@ class PluginManager{
|
||||
$plugin->onEnableStateChange(false);
|
||||
$plugin->getScheduler()->shutdown();
|
||||
HandlerListManager::global()->unregisterAll($plugin);
|
||||
AsyncHandlerListManager::global()->unregisterAll($plugin);
|
||||
}
|
||||
}
|
||||
|
||||
@ -582,7 +587,7 @@ class PluginManager{
|
||||
/** @phpstan-var class-string $paramClass */
|
||||
$paramClass = $paramType->getName();
|
||||
$eventClass = new \ReflectionClass($paramClass);
|
||||
if(!$eventClass->isSubclassOf(Event::class)){
|
||||
if(!$eventClass->isSubclassOf(Event::class) && !$eventClass->isSubclassOf(AsyncEvent::class)){
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -636,8 +641,36 @@ class PluginManager{
|
||||
throw new PluginException("Event handler " . Utils::getNiceClosureName($handlerClosure) . "() declares invalid @" . ListenerMethodTags::HANDLE_CANCELLED . " value \"" . $tags[ListenerMethodTags::HANDLE_CANCELLED] . "\"");
|
||||
}
|
||||
}
|
||||
$exclusiveCall = false;
|
||||
if(isset($tags[ListenerMethodTags::EXCLUSIVE_CALL])){
|
||||
if(!is_a($eventClass, AsyncEvent::class, true)){
|
||||
throw new PluginException(sprintf(
|
||||
"Event handler %s() declares @%s for non-async event of type %s",
|
||||
Utils::getNiceClosureName($handlerClosure),
|
||||
ListenerMethodTags::EXCLUSIVE_CALL,
|
||||
$eventClass
|
||||
));
|
||||
}
|
||||
switch(strtolower($tags[ListenerMethodTags::EXCLUSIVE_CALL])){
|
||||
case "true":
|
||||
case "":
|
||||
$exclusiveCall = true;
|
||||
break;
|
||||
case "false":
|
||||
break;
|
||||
default:
|
||||
throw new PluginException("Event handler " . Utils::getNiceClosureName($handlerClosure) . "() declares invalid @" . ListenerMethodTags::EXCLUSIVE_CALL . " value \"" . $tags[ListenerMethodTags::EXCLUSIVE_CALL] . "\"");
|
||||
}
|
||||
}
|
||||
|
||||
$this->registerEvent($eventClass, $handlerClosure, $priority, $plugin, $handleCancelled);
|
||||
if(is_subclass_of($eventClass, AsyncEvent::class)){
|
||||
if(!$this->canHandleAsyncEvent($handlerClosure)){
|
||||
throw new PluginException("Event handler " . Utils::getNiceClosureName($handlerClosure) . " must return null|Promise<null> to be able to handle async events");
|
||||
}
|
||||
$this->registerAsyncEvent($eventClass, $handlerClosure, $priority, $plugin, $handleCancelled, $exclusiveCall);
|
||||
}else{
|
||||
$this->registerEvent($eventClass, $handlerClosure, $priority, $plugin, $handleCancelled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -672,4 +705,46 @@ class PluginManager{
|
||||
HandlerListManager::global()->getListFor($event)->register($registeredListener);
|
||||
return $registeredListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $event Class name that extends Event and AsyncEvent
|
||||
*
|
||||
* @phpstan-template TEvent of AsyncEvent
|
||||
* @phpstan-param class-string<TEvent> $event
|
||||
* @phpstan-param \Closure(TEvent) : ?Promise<null> $handler
|
||||
*
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
public function registerAsyncEvent(string $event, \Closure $handler, int $priority, Plugin $plugin, bool $handleCancelled = false, bool $exclusiveCall = false) : AsyncRegisteredListener{
|
||||
//TODO: Not loving the code duplication here
|
||||
if(!is_subclass_of($event, AsyncEvent::class)){
|
||||
throw new PluginException($event . " is not an AsyncEvent");
|
||||
}
|
||||
|
||||
$handlerName = Utils::getNiceClosureName($handler);
|
||||
|
||||
if(!$plugin->isEnabled()){
|
||||
throw new PluginException("Plugin attempted to register event handler " . $handlerName . "() to event " . $event . " while not enabled");
|
||||
}
|
||||
|
||||
$timings = Timings::getEventHandlerTimings($event, $handlerName, $plugin->getDescription()->getFullName());
|
||||
|
||||
$registeredListener = new AsyncRegisteredListener($handler, $priority, $plugin, $handleCancelled, $exclusiveCall, $timings);
|
||||
AsyncHandlerListManager::global()->getListFor($event)->register($registeredListener);
|
||||
return $registeredListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given handler return type is async-compatible (equal to Promise)
|
||||
*
|
||||
* @phpstan-param \Closure(AsyncEvent) : Promise<null> $handler
|
||||
*
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
private function canHandleAsyncEvent(\Closure $handler) : bool{
|
||||
$reflection = new \ReflectionFunction($handler);
|
||||
$return = $reflection->getReturnType();
|
||||
|
||||
return $return instanceof \ReflectionNamedType && $return->getName() === Promise::class;
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ namespace pocketmine\timings;
|
||||
|
||||
use pocketmine\block\tile\Tile;
|
||||
use pocketmine\entity\Entity;
|
||||
use pocketmine\event\AsyncEvent;
|
||||
use pocketmine\event\Event;
|
||||
use pocketmine\network\mcpe\protocol\ClientboundPacket;
|
||||
use pocketmine\network\mcpe\protocol\ServerboundPacket;
|
||||
@ -116,6 +117,8 @@ abstract class Timings{
|
||||
|
||||
/** @var TimingsHandler[] */
|
||||
private static array $events = [];
|
||||
/** @var TimingsHandler[] */
|
||||
private static array $asyncEvents = [];
|
||||
/** @var TimingsHandler[][] */
|
||||
private static array $eventHandlers = [];
|
||||
|
||||
@ -313,8 +316,18 @@ abstract class Timings{
|
||||
return self::$events[$eventClass];
|
||||
}
|
||||
|
||||
public static function getAsyncEventTimings(AsyncEvent $event) : TimingsHandler{
|
||||
$eventClass = get_class($event);
|
||||
if(!isset(self::$asyncEvents[$eventClass])){
|
||||
self::$asyncEvents[$eventClass] = new TimingsHandler(self::shortenCoreClassName($eventClass, "pocketmine\\event\\"), group: "Events");
|
||||
}
|
||||
|
||||
return self::$asyncEvents[$eventClass];
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param class-string<covariant Event> $event
|
||||
* @phpstan-template TEvent of Event|AsyncEvent
|
||||
* @phpstan-param class-string<TEvent> $event
|
||||
*/
|
||||
public static function getEventHandlerTimings(string $event, string $handlerName, string $group) : TimingsHandler{
|
||||
if(!isset(self::$eventHandlers[$event][$handlerName])){
|
||||
|
@ -4,11 +4,17 @@ parameters:
|
||||
message: '#^Method pocketmine\\event\\RegisteredListener\:\:__construct\(\) has parameter \$handler with no signature specified for Closure\.$#'
|
||||
identifier: missingType.callable
|
||||
count: 1
|
||||
path: ../../../src/event/RegisteredListener.php
|
||||
path: ../../../src/event/AsyncRegisteredListener.php
|
||||
|
||||
-
|
||||
message: '#^Method pocketmine\\event\\RegisteredListener\:\:getHandler\(\) return type has no signature specified for Closure\.$#'
|
||||
identifier: missingType.callable
|
||||
count: 1
|
||||
path: ../../../src/event/RegisteredListener.php
|
||||
path: ../../../src/event/BaseRegisteredListener.php
|
||||
|
||||
-
|
||||
message: "#^Method pocketmine\\\\event\\\\BaseRegisteredListener\\:\\:getHandler\\(\\) return type has no signature specified for Closure\\.$#"
|
||||
identifier: missingType.callable
|
||||
count: 1
|
||||
path: ../../../src/event/BaseRegisteredListener.php
|
||||
|
||||
|
124
tests/phpunit/event/AsyncEventConcurrencyTest.php
Normal file
124
tests/phpunit/event/AsyncEventConcurrencyTest.php
Normal file
@ -0,0 +1,124 @@
|
||||
<?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 PHPUnit\Framework\TestCase;
|
||||
use pocketmine\event\fixtures\TestGrandchildAsyncEvent;
|
||||
use pocketmine\plugin\Plugin;
|
||||
use pocketmine\plugin\PluginManager;
|
||||
use pocketmine\promise\Promise;
|
||||
use pocketmine\promise\PromiseResolver;
|
||||
use pocketmine\Server;
|
||||
use function count;
|
||||
|
||||
final class AsyncEventConcurrencyTest extends TestCase{
|
||||
|
||||
private Plugin $mockPlugin;
|
||||
private PluginManager $pluginManager;
|
||||
|
||||
//this one gets its own class because it requires a bunch of context variables
|
||||
|
||||
/**
|
||||
* @var PromiseResolver[]
|
||||
* @phpstan-var list<PromiseResolver<null>>
|
||||
*/
|
||||
private array $resolvers = [];
|
||||
|
||||
private bool $activeExclusiveHandler = false;
|
||||
private bool $activeConcurrentHandler = false;
|
||||
|
||||
private int $done = 0;
|
||||
|
||||
protected function setUp() : void{
|
||||
AsyncHandlerListManager::global()->unregisterAll();
|
||||
|
||||
//TODO: this is a really bad hack and could break any time if PluginManager decides to access its Server field
|
||||
//we really need to make it possible to register events without a Plugin or Server context
|
||||
$mockServer = $this->createMock(Server::class);
|
||||
$this->mockPlugin = self::createStub(Plugin::class);
|
||||
$this->mockPlugin->method('isEnabled')->willReturn(true);
|
||||
$this->pluginManager = new PluginManager($mockServer, null);
|
||||
}
|
||||
|
||||
public static function tearDownAfterClass() : void{
|
||||
AsyncHandlerListManager::global()->unregisterAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-return Promise<null>
|
||||
*/
|
||||
private function handler(bool &$flag, string $label) : Promise{
|
||||
$flag = true;
|
||||
$resolver = new PromiseResolver();
|
||||
$this->resolvers[] = $resolver;
|
||||
$resolver->getPromise()->onCompletion(
|
||||
function() use (&$flag) : void{
|
||||
$flag = false;
|
||||
$this->done++;
|
||||
},
|
||||
fn() => self::fail("Not expecting this to be rejected for $label")
|
||||
);
|
||||
return $resolver->getPromise();
|
||||
}
|
||||
|
||||
public function testConcurrency() : void{
|
||||
$this->pluginManager->registerAsyncEvent(
|
||||
TestGrandchildAsyncEvent::class,
|
||||
function(TestGrandchildAsyncEvent $event) : Promise{
|
||||
self::assertFalse($this->activeExclusiveHandler, "Concurrent handler can't run while exclusive handlers are waiting to complete");
|
||||
|
||||
return $this->handler($this->activeConcurrentHandler, "concurrent");
|
||||
},
|
||||
EventPriority::NORMAL,
|
||||
$this->mockPlugin,
|
||||
//non-exclusive - this must be completed before any exclusive handlers are run (or run after them)
|
||||
);
|
||||
for($i = 0; $i < 2; $i++){
|
||||
$this->pluginManager->registerAsyncEvent(
|
||||
TestGrandchildAsyncEvent::class,
|
||||
function(TestGrandchildAsyncEvent $event) use ($i) : Promise{
|
||||
self::assertFalse($this->activeExclusiveHandler, "Exclusive handler $i can't run alongside other exclusive handlers");
|
||||
self::assertFalse($this->activeConcurrentHandler, "Exclusive handler $i can't run alongside concurrent handler");
|
||||
|
||||
return $this->handler($this->activeExclusiveHandler, "exclusive $i");
|
||||
},
|
||||
EventPriority::NORMAL,
|
||||
$this->mockPlugin,
|
||||
exclusiveCall: true
|
||||
);
|
||||
}
|
||||
|
||||
(new TestGrandchildAsyncEvent())->call();
|
||||
|
||||
while(count($this->resolvers) > 0 && $this->done < 3){
|
||||
foreach($this->resolvers as $k => $resolver){
|
||||
unset($this->resolvers[$k]);
|
||||
//don't clear the array here - resolving this will trigger adding the next resolver
|
||||
$resolver->resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
self::assertSame(3, $this->done, "Expected feedback from exactly 3 handlers");
|
||||
}
|
||||
}
|
129
tests/phpunit/event/AsyncEventTest.php
Normal file
129
tests/phpunit/event/AsyncEventTest.php
Normal file
@ -0,0 +1,129 @@
|
||||
<?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 PHPUnit\Framework\TestCase;
|
||||
use pocketmine\event\fixtures\TestChildAsyncEvent;
|
||||
use pocketmine\event\fixtures\TestGrandchildAsyncEvent;
|
||||
use pocketmine\event\fixtures\TestParentAsyncEvent;
|
||||
use pocketmine\plugin\Plugin;
|
||||
use pocketmine\plugin\PluginManager;
|
||||
use pocketmine\promise\Promise;
|
||||
use pocketmine\promise\PromiseResolver;
|
||||
use pocketmine\Server;
|
||||
use function shuffle;
|
||||
|
||||
final class AsyncEventTest extends TestCase{
|
||||
private Plugin $mockPlugin;
|
||||
private PluginManager $pluginManager;
|
||||
|
||||
protected function setUp() : void{
|
||||
AsyncHandlerListManager::global()->unregisterAll();
|
||||
|
||||
//TODO: this is a really bad hack and could break any time if PluginManager decides to access its Server field
|
||||
//we really need to make it possible to register events without a Plugin or Server context
|
||||
$mockServer = $this->createMock(Server::class);
|
||||
$this->mockPlugin = self::createStub(Plugin::class);
|
||||
$this->mockPlugin->method('isEnabled')->willReturn(true);
|
||||
$this->pluginManager = new PluginManager($mockServer, null);
|
||||
}
|
||||
|
||||
public static function tearDownAfterClass() : void{
|
||||
AsyncHandlerListManager::global()->unregisterAll();
|
||||
}
|
||||
|
||||
public function testHandlerInheritance() : void{
|
||||
$expectedOrder = [
|
||||
TestGrandchildAsyncEvent::class,
|
||||
TestChildAsyncEvent::class,
|
||||
TestParentAsyncEvent::class
|
||||
];
|
||||
$classes = $expectedOrder;
|
||||
$actualOrder = [];
|
||||
shuffle($classes);
|
||||
foreach($classes as $class){
|
||||
$this->pluginManager->registerAsyncEvent(
|
||||
$class,
|
||||
function(AsyncEvent $event) use (&$actualOrder, $class) : ?Promise{
|
||||
$actualOrder[] = $class;
|
||||
return null;
|
||||
},
|
||||
EventPriority::NORMAL,
|
||||
$this->mockPlugin
|
||||
);
|
||||
}
|
||||
|
||||
$event = new TestGrandchildAsyncEvent();
|
||||
$promise = $event->call();
|
||||
|
||||
$resolved = false;
|
||||
$promise->onCompletion(
|
||||
function() use ($expectedOrder, $actualOrder, &$resolved){
|
||||
self::assertSame($expectedOrder, $actualOrder, "Expected event handlers to be called from most specific to least specific");
|
||||
$resolved = true;
|
||||
},
|
||||
fn() => self::fail("Not expecting this to be rejected")
|
||||
);
|
||||
|
||||
self::assertTrue($resolved, "No promises were used, expected this promise to resolve immediately");
|
||||
}
|
||||
|
||||
public function testPriorityLock() : void{
|
||||
$resolver = null;
|
||||
$firstCompleted = false;
|
||||
$run = 0;
|
||||
|
||||
$this->pluginManager->registerAsyncEvent(
|
||||
TestGrandchildAsyncEvent::class,
|
||||
function(TestGrandchildAsyncEvent $event) use (&$resolver, &$firstCompleted, &$run) : Promise{
|
||||
$run++;
|
||||
$resolver = new PromiseResolver();
|
||||
|
||||
$resolver->getPromise()->onCompletion(
|
||||
function() use (&$firstCompleted) : void{ $firstCompleted = true; },
|
||||
fn() => self::fail("Not expecting this to be rejected")
|
||||
);
|
||||
|
||||
return $resolver->getPromise();
|
||||
},
|
||||
EventPriority::LOW, //anything below NORMAL is fine
|
||||
$this->mockPlugin
|
||||
);
|
||||
$this->pluginManager->registerAsyncEvent(
|
||||
TestGrandchildAsyncEvent::class,
|
||||
function(TestGrandchildAsyncEvent $event) use (&$firstCompleted, &$run) : ?Promise{
|
||||
$run++;
|
||||
self::assertTrue($firstCompleted, "This shouldn't run until the previous priority is done");
|
||||
return null;
|
||||
},
|
||||
EventPriority::NORMAL,
|
||||
$this->mockPlugin
|
||||
);
|
||||
|
||||
(new TestGrandchildAsyncEvent())->call();
|
||||
self::assertNotNull($resolver, "First handler didn't provide a resolver");
|
||||
$resolver->resolve(null);
|
||||
self::assertSame(2, $run, "Expected feedback from 2 handlers");
|
||||
}
|
||||
}
|
28
tests/phpunit/event/fixtures/TestChildAsyncEvent.php
Normal file
28
tests/phpunit/event/fixtures/TestChildAsyncEvent.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?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\fixtures;
|
||||
|
||||
class TestChildAsyncEvent extends TestParentAsyncEvent{
|
||||
|
||||
}
|
28
tests/phpunit/event/fixtures/TestGrandchildAsyncEvent.php
Normal file
28
tests/phpunit/event/fixtures/TestGrandchildAsyncEvent.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?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\fixtures;
|
||||
|
||||
class TestGrandchildAsyncEvent extends TestChildAsyncEvent{
|
||||
|
||||
}
|
30
tests/phpunit/event/fixtures/TestParentAsyncEvent.php
Normal file
30
tests/phpunit/event/fixtures/TestParentAsyncEvent.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?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\fixtures;
|
||||
|
||||
use pocketmine\event\AsyncEvent;
|
||||
|
||||
class TestParentAsyncEvent extends AsyncEvent{
|
||||
|
||||
}
|
Reference in New Issue
Block a user