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

Feature/add promisesraces cancel mechanism #39

Conversation

kronostof
Copy link

Give possibility to cancel a promise, to stop the job necessary to resolve this promise.

Related to this issues

@kronostof kronostof force-pushed the feature/add-promisesraces-cancel-mechanism branch from b043c34 to 9265417 Compare December 23, 2019 15:59
src/Adapter/Amp/EventLoop.php Outdated Show resolved Hide resolved
src/Adapter/Amp/EventLoop.php Outdated Show resolved Hide resolved
src/Adapter/Amp/EventLoop.php Outdated Show resolved Hide resolved
src/Adapter/Amp/EventLoop.php Outdated Show resolved Hide resolved
Copy link
Contributor

@b-viguier b-viguier left a 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';
}
Copy link
Contributor

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

examples/06-advanced-cancellable-promise.php Outdated Show resolved Hide resolved
echo "[$id] Working after cancellation !\n";
yield $eventLoop->delay($time);
echo "[$id] Working after cancellation !\n";
yield $eventLoop->delay($time);
Copy link
Contributor

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)
Copy link
Contributor

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)
Copy link
Contributor

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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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
Copy link
Contributor

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? 🤔

tests/EventLoopTest/CancellationTest.php Outdated Show resolved Hide resolved
src/Adapter/Amp/EventLoop.php Outdated Show resolved Hide resolved
foreach ($promises as $promise) {
$promise->cancel();
}
};
Copy link
Contributor

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

… and must be tested 😅

Copy link
Contributor

@b-viguier b-viguier left a 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();
}
};
Copy link
Contributor

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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;
Copy link
Contributor

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 = [];
Copy link
Contributor

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'));
Copy link
Contributor

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 () {};
Copy link
Contributor

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 () {};
Copy link
Contributor

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)();
Copy link
Contributor

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());
}
);
Copy link
Contributor

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
Copy link
Contributor

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

Copy link
Contributor

@b-viguier b-viguier left a 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');
}
Copy link
Contributor

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()) {
Copy link
Contributor

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'));
Copy link
Contributor

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 = [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$wrappedPromise = [];
$wrappedPromises = [];

if (empty($promises)) {
return $this->promiseFulfilled(null);
}

$globalPromise = Internal\PendingPromise::createUnhandled($this->unhandledFailingPromises);
$promisesCancellation = function () use (&$wrappedPromise) {
foreach ($wrappedPromise as $index => $promise) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
foreach ($wrappedPromise as $index => $promise) {
foreach ($wrappedPromise as $promise) {


$cancellation = function () use (&$promisesCancellation) {
($promisesCancellation)();
};
Copy link
Contributor

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) {
Copy link
Contributor

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
Copy link
Contributor

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…)

@b-viguier
Copy link
Contributor

After a lot of discussions, we decided to cancel this feature… 😕
The main question is to know if it's legit to cancel a promise. A function could cancel a promise useful for an other asynchronous function. In use case involving cache for promises, it's even harder to know if it's a good idea to cancel a promise… so we decided to cancel cancellation 😓

@b-viguier b-viguier closed this Feb 27, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants