-
Notifications
You must be signed in to change notification settings - Fork 5
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
Feature/add promisesraces cancel mechanism #39
Feature/add promisesraces cancel mechanism #39
Conversation
b043c34
to
9265417
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks… promising!! (Joke inside 😉 )
This is a partial review, I still have to read internal implementations, and check "complex" tests.
Good job, keep going :)
$result = 'cancelled promise'; | ||
} catch (\Exception $e) { | ||
$result = 'other exception'; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we agree that this example should not throw any exception? $result
should be equal to ["not resolved", "B"]
right?
If so, can you remove the last try/catch
block around the wait
? It's sound confusing to me
echo "[$id] Working after cancellation !\n"; | ||
yield $eventLoop->delay($time); | ||
echo "[$id] Working after cancellation !\n"; | ||
yield $eventLoop->delay($time); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about a loop?
$eventLoop = new Adapter\Amp\EventLoop(); | ||
//$eventLoop = new Adapter\ReactPhp\EventLoop(new React\EventLoop\StreamSelectLoop()); | ||
|
||
function timer(EventLoop $eventLoop, string $id, int $time, \M6Web\Tornado\Promise &$promise = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why a reference to an object? Object are already passed by reference in Php 🤔
//$eventLoop = new Adapter\Amp\EventLoop(); | ||
//$eventLoop = new Adapter\ReactPhp\EventLoop(new React\EventLoop\StreamSelectLoop()); | ||
|
||
function timer(EventLoop $eventLoop, string $id, int $time, \M6Web\Tornado\Promise &$promise = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why a reference to an object? Object are already passed by reference in Php 🤔
src/Promise.php
Outdated
@@ -7,4 +7,5 @@ | |||
*/ | |||
interface Promise | |||
{ | |||
public function cancel(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
public function cancel(); | |
/** | |
* Requests the cancellation of the promise. | |
* Functions still yielding for this promise will receive a `CancelledException`. | |
*/ | |
public function cancel(string $reason): void; |
- Since php 7.1, use
void
when nothing is returned. - What about the
$reason
parameter? To be forwarded in the exception (and documented)
|
||
namespace M6WebTest\Tornado; | ||
|
||
abstract class CancellableEventLoopTest extends EventLoopTest |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the purpose of this new class? It doesn't define any abstract function… so why not include the trait and the constant in the base EventLoopTest
? 🤔
foreach ($promises as $promise) { | ||
$promise->cancel(); | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This behavior should be clearly documented in function DocBlock: If you cancel the returned promise, every sub-promise will be cancelled too
Same for every function
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
… and must be tested 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AMP / Guzzle / ReactPhp adapters review
foreach ($promises as $promise) { | ||
$promise->cancel(); | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
… and must be tested 😅
}, | ||
$promises | ||
); | ||
|
||
foreach ($promises as $index => $promise) { | ||
\Amp\Promise\rethrow(new \Amp\Coroutine($wrapPromise($promise))); | ||
} | ||
$promisesCancellation = function () use (&$toWrapPromises) { | ||
foreach ($toWrapPromises as $index => $promise) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
foreach ($toWrapPromises as $index => $promise) { | |
foreach ($toWrapPromises as $promise) { |
@@ -121,6 +137,8 @@ public function promiseForeach($traversable, callable $function): Promise | |||
*/ | |||
public function promiseRace(Promise ...$promises): Promise | |||
{ | |||
$toWrapPromises = []; | |||
$promisesCancellation = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why pre declaring this variable? Looks confusing…
@@ -121,6 +137,8 @@ public function promiseForeach($traversable, callable $function): Promise | |||
*/ | |||
public function promiseRace(Promise ...$promises): Promise | |||
{ | |||
$toWrapPromises = []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is confusing… what about using original $promises
array and use a new name like ampPromises
for array_map
result?
}; | ||
|
||
$cancellation = function () use (&$deferred, &$promisesCancellation) { | ||
$deferred->fail(new CancelledException('promise race cancellation')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should use the cancellation reason provided when cancelling the promise
@@ -232,7 +281,9 @@ function ($watcherId, $stream) use ($deferred) { | |||
} | |||
); | |||
|
|||
return Internal\PromiseWrapper::createUnhandled($deferred->promise(), $this->unhandledFailingPromises); | |||
$cancellation = function () {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
onReadable
returns a watcher ID that we can cancel, in the same way that the delay
function
@@ -250,7 +301,9 @@ function ($watcherId, $stream) use ($deferred) { | |||
} | |||
); | |||
|
|||
return Internal\PromiseWrapper::createUnhandled($deferred->promise(), $this->unhandledFailingPromises); | |||
$cancellation = function () {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same remark: onWritable
returns a watcher ID that we can cancel, in the same way that the delay
function
public static function createUnhandled(\Amp\Promise $ampPromise, FailingPromiseCollection $failingPromiseCollection) | ||
public function cancel(): void | ||
{ | ||
($this->cancellation)(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
public function cancel(string $reason): void
{
($this->cancellation)($reason);
}
function () use (&$deferred) { | ||
$deferred->reject(new \M6Web\Tornado\CancelledException()); | ||
} | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks strange… this code means that when the promise of a deferred is cancelled you want to reject the deferred to… reject the promise that you cancelled 🤨
According to the documentation, it's not possible to cancel futureTick
in ReactPhp… to ensure consistent behavior, we could use a timer with a 0 delay 🤔
@@ -208,10 +235,10 @@ function () use ($deferred) { | |||
/** | |||
* {@inheritdoc} | |||
*/ | |||
public function deferred(): Deferred | |||
public function deferred(callable $canceller = null): Deferred |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At the end, all $canceller
parameters must be mandatory
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Internal Tornado implementation review
@@ -46,12 +49,19 @@ function (\Throwable $throwable) use (&$finalAction, &$promiseIsPending) { | |||
return count($this->tasks) !== 0; | |||
}; | |||
|
|||
if (method_exists($promise, 'isCancelled') && $promise->isCancelled()) { | |||
throw new CancelledException('cancelled wait'); | |||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚫 No
You can use Internal\PendingPromise::toHandledPromise($promise)
to safely cast the promise interface to a concrete internal promise… if needed :)
$promise->reject(new CancelledException('async cancellation')); | ||
/** @var PendingPromise $currentPromise */ | ||
$currentPromise = $task->getGenerator()->current(); | ||
if (!$currentPromise->isCancelled()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test seems useless since PendingPromise::cancel
already check this (and it is its responsibility!)
$promise = Internal\PendingPromise::createUnhandled($this->unhandledFailingPromises, | ||
function () use (&$task, &$promise) { | ||
if ($task->getGenerator()->current()) { | ||
$promise->reject(new CancelledException('async cancellation')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This callback is called if the promise is canceled, and here you try to reject the canceled promise… not sure if it's relevant 🤔
@@ -162,11 +195,24 @@ public function promiseForeach($traversable, callable $function): Promise | |||
*/ | |||
public function promiseRace(Promise ...$promises): Promise | |||
{ | |||
$cancellation = null; | |||
$promisesCancellation = null; | |||
$wrappedPromise = []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$wrappedPromise = []; | |
$wrappedPromises = []; |
if (empty($promises)) { | ||
return $this->promiseFulfilled(null); | ||
} | ||
|
||
$globalPromise = Internal\PendingPromise::createUnhandled($this->unhandledFailingPromises); | ||
$promisesCancellation = function () use (&$wrappedPromise) { | ||
foreach ($wrappedPromise as $index => $promise) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
foreach ($wrappedPromise as $index => $promise) { | |
foreach ($wrappedPromise as $promise) { |
|
||
$cancellation = function () use (&$promisesCancellation) { | ||
($promisesCancellation)(); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When you call $cancellation
, it calls $promisesCancellation
… what about calling $promisesCancellation
directly? 😄
yield $this->idle(); | ||
try { | ||
yield $this->idle(); | ||
} catch (\Throwable $throwable) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you should catch only CancelledException
, and cancel the promise… not even sure if it's required or not… 🤔
@@ -103,8 +122,17 @@ private function triggerCallbacks(): self | |||
return $this; | |||
} | |||
|
|||
public function isSettled(): bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be private
… (then, the property may be sufficient…)
After a lot of discussions, we decided to cancel this feature… 😕 |
Give possibility to cancel a promise, to stop the job necessary to resolve this promise.
Related to this issues