Compare commits

...

58 Commits

Author SHA1 Message Date
39c9387efe Implement handlers stuck detection system 2025-08-05 15:37:24 +02:00
31275ba681 Merge remote-tracking branch 'upstream/minor-next' into feat/async-events 2025-07-29 14:12:40 +02:00
a0d69a9fb8 github web editor don't fuck up indentation, challenge impossible 2024-11-29 14:13:25 +00:00
e8ec81d123 fix PHPStan error 2024-11-29 14:08:06 +00:00
866d473553 Merge branch 'minor-next' into feat/async-events 2024-11-29 13:49:20 +00:00
d9080f182c we don't need a fake server instance outside of setUp() in these tests 2024-11-20 16:44:00 +00:00
d9f5634262 CS 2024-11-20 16:37:49 +00:00
406e2c6c57 Convert integration tests to unit tests
this required mocking to get around #6524. Longer term we should make improvements to avoid the need for mocking here.
2024-11-20 16:35:20 +00:00
4451770ca3 Merge branch 'minor-next' into feat/async-events 2024-11-20 15:43:17 +00:00
d2d663b1c9 Simplify handler sorting 2024-11-14 13:11:38 +00:00
117026cb83 Merge branch 'minor-next' into feat/async-events 2024-11-13 23:06:05 +00:00
a7a1077676 CONTRIBUTING: changing an event from sync to async or vice versa is a BC break 2024-11-13 23:00:49 +00:00
0a56cf877b Remove unused class 2024-11-13 22:58:29 +00:00
11fdf79a7c ... 2024-11-13 22:55:08 +00:00
edae9f26e4 Reduce number of classes 2024-11-13 22:23:43 +00:00
667656b1c6 Split AsyncHandlerListManager
this allows further code deduplication at the expense of needing 2 calls to unregister all handlers
2024-11-13 22:08:28 +00:00
972a9fb201 PluginManager: ensure that handler candidates of async events with wrong return types don't attempt to register as sync events
this will cause other, more confusing errors to be thrown.

to be honest, I'm not sure if enforcing the return type here is even necessary (or desirable).
2024-11-13 21:30:24 +00:00
ac1cf73f8e Reduce code duplication 2024-11-13 21:09:52 +00:00
96989d1dc4 cleanup 2024-11-13 20:44:35 +00:00
8aed5d6b27 Handler inheritance is now working
this code should also perform somewhat better
2024-11-13 20:35:14 +00:00
fa796535ff ah hello my old friend, impossible-generics.neon
propagating generics all the way through the likes of HandlerList etc is more trouble than it's worth.
2024-11-13 19:09:52 +00:00
32b1d6c0c2 Fixed test code
the test still doesn't pass, but at least it's actually testing the problem now...
2024-11-13 18:52:56 +00:00
6f40c6fc1d CS 2024-11-13 18:49:58 +00:00
a6a44bde90 Fix doc comments 2024-11-13 18:49:32 +00:00
409066c8f5 AsyncEvent: make the code easier to make sense of 2024-11-13 18:49:15 +00:00
cb2fadeb26 Fixed bug in concurrency integration test 2024-11-13 18:47:46 +00:00
a14afb4bb5 Add integration tests
most of these are failing - needs to be investigated
2024-11-13 18:39:14 +00:00
db88e543fa Fix PHPStan error 2024-11-13 16:04:27 +00:00
c426677841 optimization 2024-11-13 16:03:10 +00:00
17ae932d31 HandlerListManager: added getter 2024-11-13 15:36:51 +00:00
8f48fe4856 Fully separate hierarchies for sync & async events
there's no way to combine these without causing type compatibility issues for one side or the other.
we might be able to use traits to reduce duplication, but the separation here seems to be necessary.
2024-11-13 15:35:41 +00:00
48d2430064 Update PHPStan baseline 2024-11-13 14:58:09 +00:00
b82d47dd32 Merge branch 'minor-next' into feat/async-events 2024-11-13 14:57:03 +00:00
86fb041a65 Merge branch 'minor-next' of github.com:pmmp/PocketMine-MP into feat/async-events 2024-11-13 14:46:32 +00:00
b276133003 Merge remote-tracking branch 'origin/minor-next' into feat/async-events 2024-07-02 13:44:36 +00:00
c1e3903934 fix PHPstan 2024-01-21 12:04:23 +01:00
eb9814197b resolve AsyncEvent with self instance 2024-01-21 11:50:31 +01:00
d6b7a9ed45 merge remote tracking upstream 2024-01-21 11:28:24 +01:00
64bbff6286 Merge remote-tracking branch 'upstream/minor-next' into feat/async-events 2024-01-21 11:25:37 +01:00
f82c422f64 remove using of Event API 2024-01-21 11:25:34 +01:00
aaa37baf2e handlerListe: reduce code complexity 2023-10-27 22:28:04 +02:00
243a3035ba follow up of #6110 2023-10-27 22:14:21 +02:00
823d4ead6a inconsistency correction 2023-10-27 22:12:03 +02:00
ca95b2f28d fix PHPStan 2023-10-27 22:08:49 +02:00
cc6e8ef232 move the asynchronous registration of handlers to a dedicated PluginManager function 2023-10-27 21:58:43 +02:00
5beaa3ce4e correction of various problems 2023-10-27 21:45:22 +02:00
7e87fbbb7a clarifying the exception message 2023-10-27 21:39:18 +02:00
ed739cff4f cannot call async event in sync context + remove Event dependency for AsyncEventDelegate 2023-10-27 21:37:56 +02:00
dc85bba995 merge remote tracking 2023-10-27 21:28:09 +02:00
1176b7090c Update src/player/Player.php
Co-authored-by: Javier León <58715544+JavierLeon9966@users.noreply.github.com>
2023-10-23 17:31:53 +01:00
2b2fa9ddf1 phpstan: populate baseline 2023-10-22 15:45:05 +02:00
58155a77fb fix PHPstan 2023-10-22 15:18:39 +02:00
c250bb0da7 undo Promise covariant + improve array types 2023-10-22 15:08:50 +02:00
b78ff00418 fix style 2023-10-22 15:06:17 +02:00
9b2b92ac1f oops, remove test code 2023-10-22 15:05:06 +02:00
7a4b9a0367 events: asynchandler is defined by their return type and event type
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
2023-10-22 15:01:11 +02:00
a84fc2b901 introduce AsyncEvent and ::callAsync()
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()
2023-10-14 23:13:35 +02:00
5fe57a8f6f temporaly add Promise::all
code came from #6015 and will be subject to probable changes
2023-10-14 21:36:44 +02:00
20 changed files with 955 additions and 148 deletions

View File

@ -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`) | ❌ | ✔️ | ✔️ |

View File

@ -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
View 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();
}
}
}

View 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(...));
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View File

@ -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
);
}
}

View File

@ -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);
}
}

View File

@ -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";
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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])){

View File

@ -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

View 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");
}
}

View 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");
}
}

View 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{
}

View 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{
}

View 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{
}