Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generalize asynchronous events #6092

Open
wants to merge 23 commits into
base: minor-next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
23 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
File filter

Filter by extension

Filter by extension

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

/**
* 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{
/**
* Add a promise to the set of promises that will be awaited before the next priority level is called.
*
* @phpstan-param Promise<null> $promise
*/
public function addPromise(Promise $promise) : void;

/**
* 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-param ObjectSet<Promise<null>> $promiseSet
*
* @phpstan-return Promise<null>
*/
public static function callAsync(AsyncEvent&Event $event, ObjectSet $promiseSet) : Promise;
}
84 changes: 84 additions & 0 deletions src/event/AsyncEventTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?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;

trait AsyncEventTrait {
ShockedPlot7560 marked this conversation as resolved.
Show resolved Hide resolved
/** @phpstan-var ObjectSet<Promise<null>> */
private ObjectSet $promises;

/**
* @phpstan-param ObjectSet<Promise<null>>|null $promises
*/
private function initializePromises(?ObjectSet &$promises) : void{
$promises ??= new ObjectSet();
$this->promises = $promises;
}

public function addPromise(Promise $promise) : void{
ShockedPlot7560 marked this conversation as resolved.
Show resolved Hide resolved
if(!isset($this->promises)){
throw new \RuntimeException("Cannot add promises, be sure to initialize the promises set in the constructor");
}
$this->promises->add($promise);
}

final public static function callAsync(AsyncEvent&Event $event, ObjectSet $promiseSet) : Promise{
ShockedPlot7560 marked this conversation as resolved.
Show resolved Hide resolved
$event->checkMaxDepthCall();

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

$callable = function(int $priority) use ($event, $promiseSet) : Promise{
$handlers = HandlerListManager::global()->getListFor(static::class)->getListenersByPriority($priority);
$event->callHandlers($handlers);

$array = $promiseSet->toArray();
$promiseSet->clear();

return Promise::all($array);
ShockedPlot7560 marked this conversation as resolved.
Show resolved Hide resolved
};

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

$testResolve();

return $globalResolver->getPromise();
}
}
17 changes: 16 additions & 1 deletion src/event/Event.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,30 @@ final public function getEventName() : string{
* @throws \RuntimeException if event call recursion reaches the max depth limit
*/
public function call() : void{
$this->checkMaxDepthCall();
$this->callHandlers(null);
}
ShockedPlot7560 marked this conversation as resolved.
Show resolved Hide resolved

/**
* @internal used by AsyncEventTrait and Event
*/
final protected function checkMaxDepthCall() : void{
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)");
}
}

/**
* @param RegisteredListener[]|null $handlers
*
* @internal used by AsyncEventTrait and Event
*/
final protected function callHandlers(?array $handlers) : void{
$timings = Timings::getEventTimings($this);
$timings->startTiming();

$handlers = HandlerListManager::global()->getHandlersFor(static::class);
$handlers = $handlers ?? HandlerListManager::global()->getHandlersFor(static::class);
ShockedPlot7560 marked this conversation as resolved.
Show resolved Hide resolved

++self::$eventCallDepth;
try{
Expand Down
53 changes: 52 additions & 1 deletion src/promise/Promise.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@

namespace pocketmine\promise;

use function count;
use function spl_object_id;

/**
* @phpstan-template TValue
* @phpstan-template-covariant TValue
ShockedPlot7560 marked this conversation as resolved.
Show resolved Hide resolved
*/
final class Promise{
ShockedPlot7560 marked this conversation as resolved.
Show resolved Hide resolved
/**
Expand All @@ -52,4 +53,54 @@ public function onCompletion(\Closure $onSuccess, \Closure $onFailure) : void{
public function isResolved() : bool{
return $this->shared->resolved;
}

/**
* Returns a promise that will resolve only once all the Promises in
* `$promises` have resolved. The resolution value of the returned promise
* will be an array containing the resolution values of each Promises in
* `$promises` indexed by the respective Promises' array keys.
*
* @template TPromiseValue
* @phpstan-param Promise<TPromiseValue>[] $promises
ShockedPlot7560 marked this conversation as resolved.
Show resolved Hide resolved
*
* @phpstan-return Promise<array<int, TPromiseValue>>
*/
public static function all(array $promises) : Promise {
/** @phpstan-var PromiseResolver<array<int, TPromiseValue>> $resolver */
$resolver = new PromiseResolver();
$values = [];
$toResolve = count($promises);
$continue = true;

foreach($promises as $key => $promise){
$values[$key] = null;
ShockedPlot7560 marked this conversation as resolved.
Show resolved Hide resolved

$promise->onCompletion(
function(mixed $value) use ($resolver, $key, &$toResolve, &$continue, &$values) : void{
$values[$key] = $value;

if(--$toResolve === 0 && $continue){
$resolver->resolve($values);
}
},
function() use ($resolver, &$continue) : void{
if($continue){
$continue = false;
$resolver->reject();
}
}
);

if(!$continue){
break;
}
}

if($toResolve === 0){
$continue = false;
$resolver->resolve($values);
}

return $resolver->getPromise();
}
}
5 changes: 5 additions & 0 deletions tests/phpstan/configs/actual-problems.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1205,3 +1205,8 @@ parameters:
count: 1
path: ../../phpunit/scheduler/AsyncPoolTest.php

-
message: "#^Right side of && is always true\\.$#"
count: 1
path: ../../../src/promise/Promise.php