-
Notifications
You must be signed in to change notification settings - Fork 53
Allow a callable parameter to decide if require is going to be retrie… #79
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
Conversation
…d or not. Also allow retry backoff
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 added some suggestions for the phpdoc on the new callables.
i have one larger worry with this code: we freeze the whole php process even when doing async requests.
when doing multiple requests that fail, we will unconditionally delay for each of the requests. i wonder if we could instead work with a timestamp. like recording that response was received at time X and if we get here we wait until X+$time. for a single request, this will result in the same delay, but if we sent out a bunch of requests we will only sleep once if we follow that logic.
src/Plugin/RetryPlugin.php
Outdated
@@ -35,18 +45,30 @@ | |||
* @param array $config { | |||
* | |||
* @var int $retries Number of retries to attempt if an exception occurs before letting the exception bubble up. | |||
* @var callable $decider A callback that gets a request and an exception to decide if we should retry this or not. |
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.
maybe: ... to decide after a failure whether the request should be retried.
src/Plugin/RetryPlugin.php
Outdated
@@ -35,18 +45,30 @@ | |||
* @param array $config { | |||
* | |||
* @var int $retries Number of retries to attempt if an exception occurs before letting the exception bubble up. | |||
* @var callable $decider A callback that gets a request and an exception to decide if we should retry this or not. | |||
* @var callable $delay A callback to return how many milliseconds we should wait before trying again. |
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 document the arguments for this callback as well.
src/Plugin/RetryPlugin.php
Outdated
} | ||
|
||
$time = call_user_func($this->delay, $request, $exception, ++$this->retryStorage[$chainIdentifier]); | ||
usleep($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.
this freezes the whole php process, even when doing async http requests.
* } | ||
*/ | ||
public function __construct(array $config = []) | ||
{ | ||
$resolver = new OptionsResolver(); | ||
$resolver->setDefaults([ | ||
'retries' => 1, | ||
'decider' => function (RequestInterface $request, Exception $e) { | ||
return true; |
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 we check the exception class and only do this when the exception indicates a network problem or a server error? 4xx responses or completely invalid requests don't need to be retried. (though we could want to retry dns errors)
if we make the logic more complicated, we should move this to a static public method of this class instead of the anonymous function, also to allow testing it.
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 do not think we can add any logic here because it will break the existing behavior.
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 agree. we could argue that its more of a bugfix because retrying something that can't ever work out is pointless. but its quite possible we miss some situation where retrying could be successful, and thus we better change to something like that when we do a version 2. should we add an issue for that? or a todo comment in the code?
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.
Technically, adding sleeps is not a semantic change. How can program tell if it has been switched out by the scheduler? It is technically possible, but extremely unlikely, for the original code to have also slept there, if it was not allocated 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.
I don't think we should check the exception here, 4XX can have meaning to be rety in some scenario (like you have a pool of servers and you know that some files are present on some of them so you want to retry until you find the file)
That's why i propose the change in #77 so an user can choose on which behavior he wants exception (only 500, only 400 or both of them)
Then if an user choose only 500, 4XX response will never have exception and enter this callback, thus having a check here is not needed
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.
@joelwurtz we agreed to keep it like this for BC reasons. can you please put your argument into #80 so that it does not get lost?
src/Plugin/RetryPlugin.php
Outdated
return true; | ||
}, | ||
'delay' => function (RequestInterface $request, Exception $e, $retries) { | ||
return ((int) pow(2, $retries - 1)) * 1000; |
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 could be a public static function on this class. then we could unit test it.
Does this makes sense? Or I mean, does it work? If one do two async requests that both will fail. At some point request A will fail, we check the timestamp and we sleep. Request B will not fail until Request A has been retried, right? It would only make sense if we slept between: $promise = $this->handleRequest($request, $next, $first);
return $promise->wait(); Or am I wrong? |
i guess you are correct. can we find a timestamp coming from the curl library that was doing the actual async requests? because those should happen in actual threads... if we can't i guess this has to be good enough. |
We can't I'm afraid.. Since we do not know what client they are using. |
$promise = $this->handleRequest($request, $next, $first);
return $promise->wait(); You have to sleep before
This will remove the |
i don't think saving microseconds is worth it. its an error case and we wait for a second or similar, which is magnitudes longer. the only problem i see is multiple waits of 1 (or several) seconds each. |
I do not think there is a solution to the problem you highlight. I updated the PR with a change log. I think this is ready to merge |
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.
lets squash and merge
do we need an update in the documentation? should we support this in the HttplugBundle configuration?
For the problem of having to wait here i recently come to an idea to be full asynchronous here instead of using the wait, but it will require some work :). First we need a custom Promise implementation which we cannot resolve and we will return this promise in this plugin : $promise = new CustomPromiseImplementation();
...
return $promise; Then when we receive the response from the upstream promise, we can resolve our custom promise with response if it's OK $promise = new CustomPromiseImplementation();
$next($request)->then(function (ResponseInterface $response) use ($request, $chainIdentifier) {
if (array_key_exists($chainIdentifier, $this->retryStorage)) {
unset($this->retryStorage[$chainIdentifier]);
}
$promise->resolve($response);
return $response;
}
...
return $promise; And then when it fails we can resend a new async request and resolve or make it failed (when too many retries) our custom promise just like before. If nobody is up to do that i may throw a PR for this in the summer to resolve this but i have so many things to do right now that not sure if i will have the time for it so feel free to do it. |
Also using something like this mean we can reuse this principle in other code and drop the wait method from our interface (for a 2.0 version of course) |
interesting idea, joel. i suggest we merge this anyways and see for other solutions. if there will be a BC break involved, we can do a 2.0 version. is that ok for you? |
@joelwurtz I remember you wanted to use async-interop. Is this something that they do as well. If not, what's your current point of view on this question: should we still lean towards async-interop? |
|
||
### Changed | ||
|
||
- The `RetryPlugin` does now wait between retries. To disable/change this feature you must write something like: |
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.
now waits
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.
Are you sure? 'It does now waits'?
spec/Plugin/RetryPluginSpec.php
Outdated
@@ -101,4 +101,12 @@ function it_does_not_keep_history_of_old_failure(RequestInterface $request, Resp | |||
$this->handleRequest($request, $next, function () {})->shouldReturnAnInstanceOf('Http\Client\Promise\HttpFulfilledPromise'); | |||
$this->handleRequest($request, $next, function () {})->shouldReturnAnInstanceOf('Http\Client\Promise\HttpFulfilledPromise'); | |||
} | |||
|
|||
function it_has_a_good_default_delay(RequestInterface $request, Exception\HttpException $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.
I'm sorry, but what does good mean? 😂 Dat software specification 😂
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.
Haha. You know... not "bad".
I'll update :)
src/Plugin/RetryPlugin.php
Outdated
*/ | ||
public static function defaultDelay(RequestInterface $request, Exception $e, $retries) | ||
{ | ||
return ((int) pow(2, $retries)) * 1000; |
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 guess $retries
can only be a positive integer or 0, so why the int casting?
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.
Not sure to be honest. Just copied this from guzzle.
I've updated the PR |
Let's go merge this, my comment is not a blocker, and can be done later. @sagikazarmark async-interop is dead so don't know actually, i would like to changing the async interface to be used with coroutines instead (like async / await in other languages, see php amp for the details), but may be a bit soon for the php community. |
@Nyholm just started using this. the default with 5 retries, the plugin retries pretty much instantly from 0.001 to 0.032 seconds. no delay at all really. Clearly I can define my own, delay, but maybe the default could be a bit more ...delaying? |
Hm, I guess you are correct. We should update our defaults. Could you make a suggestion in a PR? |
sure. |
…d or not.
Also allow retry backoff
This will fix #78
It is a minor breaking change here. Instead of retry immediately we wait a second. I do not consider it a BC break though..