Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
5fe57a8
temporaly add Promise::all
ShockedPlot7560 Oct 14, 2023
a84fc2b
introduce AsyncEvent and ::callAsync()
ShockedPlot7560 Oct 14, 2023
7a4b9a0
events: asynchandler is defined by their return type and event type
ShockedPlot7560 Oct 22, 2023
9b2b92a
oops, remove test code
ShockedPlot7560 Oct 22, 2023
b78ff00
fix style
ShockedPlot7560 Oct 22, 2023
c250bb0
undo Promise covariant + improve array types
ShockedPlot7560 Oct 22, 2023
58155a7
fix PHPstan
ShockedPlot7560 Oct 22, 2023
2b2fa9d
phpstan: populate baseline
ShockedPlot7560 Oct 22, 2023
1176b70
Update src/player/Player.php
dktapps Oct 23, 2023
dc85bba
merge remote tracking
ShockedPlot7560 Oct 27, 2023
ed739cf
cannot call async event in sync context + remove Event dependency for…
ShockedPlot7560 Oct 27, 2023
7e87fbb
clarifying the exception message
ShockedPlot7560 Oct 27, 2023
5beaa3c
correction of various problems
ShockedPlot7560 Oct 27, 2023
cc6e8ef
move the asynchronous registration of handlers to a dedicated PluginM…
ShockedPlot7560 Oct 27, 2023
ca95b2f
fix PHPStan
ShockedPlot7560 Oct 27, 2023
823d4ea
inconsistency correction
ShockedPlot7560 Oct 27, 2023
243a303
follow up of #6110
ShockedPlot7560 Oct 27, 2023
aaa37ba
handlerListe: reduce code complexity
ShockedPlot7560 Oct 27, 2023
f82c422
remove using of Event API
ShockedPlot7560 Jan 21, 2024
64bbff6
Merge remote-tracking branch 'upstream/minor-next' into feat/async-ev…
ShockedPlot7560 Jan 21, 2024
d6b7a9e
merge remote tracking upstream
ShockedPlot7560 Jan 21, 2024
eb98141
resolve AsyncEvent with self instance
ShockedPlot7560 Jan 21, 2024
c1e3903
fix PHPstan
ShockedPlot7560 Jan 21, 2024
b276133
Merge remote-tracking branch 'origin/minor-next' into feat/async-events
ShockedPlot7560 Jul 2, 2024
86fb041
Merge branch 'minor-next' of github.com:pmmp/PocketMine-MP into feat/…
dktapps Nov 13, 2024
b82d47d
Merge branch 'minor-next' into feat/async-events
dktapps Nov 13, 2024
48d2430
Update PHPStan baseline
dktapps Nov 13, 2024
8f48fe4
Fully separate hierarchies for sync & async events
dktapps Nov 13, 2024
17ae932
HandlerListManager: added getter
dktapps Nov 13, 2024
c426677
optimization
dktapps Nov 13, 2024
db88e54
Fix PHPStan error
dktapps Nov 13, 2024
a14afb4
Add integration tests
dktapps Nov 13, 2024
cb2fade
Fixed bug in concurrency integration test
dktapps Nov 13, 2024
409066c
AsyncEvent: make the code easier to make sense of
dktapps Nov 13, 2024
a6a44bd
Fix doc comments
dktapps Nov 13, 2024
6f40c6f
CS
dktapps Nov 13, 2024
32b1d6c
Fixed test code
dktapps Nov 13, 2024
fa79653
ah hello my old friend, impossible-generics.neon
dktapps Nov 13, 2024
8aed5d6
Handler inheritance is now working
dktapps Nov 13, 2024
96989d1
cleanup
dktapps Nov 13, 2024
ac1cf73
Reduce code duplication
dktapps Nov 13, 2024
972a9fb
PluginManager: ensure that handler candidates of async events with wr…
dktapps Nov 13, 2024
667656b
Split AsyncHandlerListManager
dktapps Nov 13, 2024
edae9f2
Reduce number of classes
dktapps Nov 13, 2024
11fdf79
...
dktapps Nov 13, 2024
0a56cf8
Remove unused class
dktapps Nov 13, 2024
a7a1077
CONTRIBUTING: changing an event from sync to async or vice versa is a…
dktapps Nov 13, 2024
117026c
Merge branch 'minor-next' into feat/async-events
dktapps Nov 13, 2024
d2d663b
Simplify handler sorting
dktapps Nov 14, 2024
4451770
Merge branch 'minor-next' into feat/async-events
dktapps Nov 20, 2024
406e2c6
Convert integration tests to unit tests
dktapps Nov 20, 2024
d9f5634
CS
dktapps Nov 20, 2024
d9080f1
we don't need a fake server instance outside of setUp() in these tests
dktapps Nov 20, 2024
866d473
Merge branch 'minor-next' into feat/async-events
dktapps Nov 29, 2024
e8ec81d
fix PHPStan error
dktapps Nov 29, 2024
a0d69a9
github web editor don't fuck up indentation, challenge impossible
dktapps Nov 29, 2024
31275ba
Merge remote-tracking branch 'upstream/minor-next' into feat/async-ev…
ShockedPlot7560 Jul 29, 2025
39c9387
Implement handlers stuck detection system
ShockedPlot7560 Aug 5, 2025
9233fa0
Merge branch 'minor-next' into feat/async-events
dktapps Oct 29, 2025
8a5893d
Compare handlers only by their IDs
ShockedPlot7560 Nov 4, 2025
e97243d
add concurrent calls test
ShockedPlot7560 Nov 4, 2025
8b286a9
phpstan :/
ShockedPlot7560 Nov 4, 2025
e919b19
Fix PHPstan & CS
ShockedPlot7560 Nov 5, 2025
126f836
thanks impossible-generics.neon
ShockedPlot7560 Nov 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/event/AsyncEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?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;

/**
* This interface is implemented by an Event subclass if and only if it can be called asynchronously.
*
* Used with {@see AsyncEventTrait} to provide a way to call an event asynchronously.
* 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.
*/
interface AsyncEvent{
/**
* Be prudent, calling an event asynchronously can produce unexpected results.
* During the execution of the event, the server, the player and the event context may have changed state.
*
* @phpstan-return Promise<null>
*/
public function callAsync() : Promise;
}
131 changes: 131 additions & 0 deletions src/event/AsyncEventDelegate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?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\utils\ObjectSet;
use function array_shift;
use function count;

final class AsyncEventDelegate extends Event{
/** @phpstan-var ObjectSet<Promise<null>> $promises */
private ObjectSet $promises;

public function __construct(
private AsyncEvent&Event $event
){
$this->promises = new ObjectSet();
}

/**
* @phpstan-return Promise<null>
*/
public function callAsync() : Promise{
$this->promises->clear();
return $this->callDepth($this->callAsyncDepth(...));
}

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

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

$testResolve();

return $globalResolver->getPromise();
}

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

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

$nonConcurrentHandlers = [];
foreach($handlers as $registration){
if($registration instanceof RegisteredAsyncListener){
if($registration->canBeCallConcurrently()){
$this->promises->add($registration->callAsync($this->event));
}else{
$nonConcurrentHandlers[] = $registration;
}
}else{
$registration->callEvent($this->event);
}
}

$testResolve = function() use (&$nonConcurrentHandlers, &$testResolve, $resolver){
if(count($nonConcurrentHandlers) === 0){
$this->waitForPromises()->onCompletion(function() use ($resolver){
$resolver->resolve(""); // TODO: see #6110
}, function() use ($resolver){
$resolver->reject();
});
}else{
$this->waitForPromises()->onCompletion(function() use (&$nonConcurrentHandlers, $testResolve){
$handler = array_shift($nonConcurrentHandlers);
if($handler instanceof RegisteredAsyncListener){
$this->promises->add($handler->callAsync($this->event));
}
$testResolve();
}, function() use ($resolver) {
$resolver->reject();
});
}
};

$testResolve();

return $resolver->getPromise();
}

/**
* @phpstan-return Promise<array<int, null>>
*/
private function waitForPromises() : Promise{
$array = $this->promises->toArray();
$this->promises->clear();

return Promise::all($array);
}
}
40 changes: 40 additions & 0 deletions src/event/AsyncEventTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?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;

trait AsyncEventTrait {
private AsyncEventDelegate $delegate;

/**
* @phpstan-return Promise<null>
*/
final public function callAsync() : Promise{
if(!isset($this->delegate)){
$this->delegate = new AsyncEventDelegate($this);
}
return $this->delegate->callAsync();
}
}
21 changes: 16 additions & 5 deletions src/event/Event.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,21 @@ final public function getEventName() : string{
* @throws \RuntimeException if event call recursion reaches the max depth limit
*/
public function call() : void{
$this->callDepth(function(){
$handlers = HandlerListManager::global()->getHandlersFor(static::class);

foreach($handlers as $registration){
$registration->callEvent($this);
}
});
}

/**
* @template T
* @phpstan-param \Closure() : T $closure
* @phpstan-return T
*/
final protected function callDepth(\Closure $closure) : mixed{
if(self::$eventCallDepth >= self::MAX_EVENT_CALL_DEPTH){
//this exception will be caught by the parent event call if all else fails
throw new \RuntimeException("Recursive event call detected (reached max depth of " . self::MAX_EVENT_CALL_DEPTH . " calls)");
Expand All @@ -55,13 +70,9 @@ public function call() : void{
$timings = Timings::getEventTimings($this);
$timings->startTiming();

$handlers = HandlerListManager::global()->getHandlersFor(static::class);

++self::$eventCallDepth;
try{
foreach($handlers as $registration){
$registration->callEvent($this);
}
return $closure();
}finally{
--self::$eventCallDepth;
$timings->stopTiming();
Expand Down
19 changes: 18 additions & 1 deletion src/event/HandlerList.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@
namespace pocketmine\event;

use pocketmine\plugin\Plugin;
use function array_filter;
use function array_merge;
use function krsort;
use function spl_object_id;
use function usort;
use const SORT_NUMERIC;

class HandlerList{
Expand Down Expand Up @@ -128,11 +130,26 @@ public function getListenerList() : array{
}

$listenersByPriority = [];
$asyncListenersByPriority = [];
foreach($handlerLists as $currentList){
foreach($currentList->handlerSlots as $priority => $listeners){
$listenersByPriority[$priority] = array_merge($listenersByPriority[$priority] ?? [], $listeners);
$syncListeners = array_filter($listeners, static function(RegisteredListener $listener) : bool{ return !($listener instanceof RegisteredAsyncListener); });
$listenersByPriority[$priority] = array_merge($listenersByPriority[$priority] ?? [], $syncListeners);

$asyncListeners = array_filter($listeners, static function(RegisteredListener $listener) : bool{ return $listener instanceof RegisteredAsyncListener; });
$asyncListenersByPriority[$priority] = array_merge($asyncListenersByPriority[$priority] ?? [], $asyncListeners);
}
}
foreach($asyncListenersByPriority as $priority => $asyncListeners){
usort($asyncListeners, static function(RegisteredAsyncListener $a, RegisteredAsyncListener $b) : int{
if($a->canBeCallConcurrently()){
return $b->canBeCallConcurrently() ? 0 : -1;
}else{
return $b->canBeCallConcurrently() ? -1 : 0;
}
});
$listenersByPriority[$priority] = array_merge($listenersByPriority[$priority] ?? [], $asyncListeners);
}

//TODO: why on earth do the priorities have higher values for lower priority?
krsort($listenersByPriority, SORT_NUMERIC);
Expand Down
1 change: 1 addition & 0 deletions src/event/ListenerMethodTags.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ final class ListenerMethodTags{
public const HANDLE_CANCELLED = "handleCancelled";
public const NOT_HANDLER = "notHandler";
public const PRIORITY = "priority";
public const NO_CONCURRENT_CALL = "noConcurrentCall";
}
65 changes: 65 additions & 0 deletions src/event/RegisteredAsyncListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?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 RegisteredAsyncListener extends RegisteredListener{
/** @phpstan-var Promise<null> $returnPromise */
private Promise $returnPromise;

/**
* @phpstan-param \Closure(AsyncEvent&Event) : Promise<null> $handler
*/
public function __construct(
\Closure $handler,
int $priority,
Plugin $plugin,
bool $handleCancelled,
private bool $noConcurrentCall,
TimingsHandler $timings
){
$handler = function(AsyncEvent&Event $event) use($handler) : void{
$this->returnPromise = $handler($event);
if(!$this->returnPromise instanceof Promise){
throw new \TypeError("Async event handler must return a Promise");
}
};
parent::__construct($handler, $priority, $plugin, $handleCancelled, $timings);
}

public function canBeCallConcurrently() : bool{
return !$this->noConcurrentCall;
}

/**
* @phpstan-return Promise<null>
*/
public function callAsync(AsyncEvent&Event $event) : Promise{
$this->callEvent($event);
return $this->returnPromise;
}
}
1 change: 1 addition & 0 deletions src/player/Player.php
Original file line number Diff line number Diff line change
Expand Up @@ -1517,6 +1517,7 @@ public function chat(string $message) : bool{
if(!$ev->isCancelled()){
$this->server->broadcastMessage($ev->getFormatter()->format($ev->getPlayer()->getDisplayName(), $ev->getMessage()), $ev->getRecipients());
}

}
}
}
Expand Down
Loading