Symfony Retryable HttpClient and timeout option. How it works when used together?

3.7k Views Asked by At

In the new Symfomy 5.2 a Retryable HttpClient was implemented.

Failed requests can now be retried by default in case of failure.

However, I could not debug or understand how the timeout option is parsed in it.

For example:

    $response = $client->request('POST', $url, [
        'body' => $reqBody,
        'timeout' => 45
    ]);

If the first request fails after 30 seconds, and the second attempt succeed after another 30 seconds, do I get a timeout error (considering both requests took 30 seconds, 30+30 = 60s)?

Or the timeout option for this case will be valid for every attempt?

If so, is there a way to set the global timeout of the request?

1

There are 1 best solutions below

0
On

After some testing using webhook.site with some timeout options, I could see the timeout option refers to every request.

I could not find a way to set the total timeout, but at least I was able to avoid further retries if an specific time had elapsed.

For that, I created a custom strategy. I added the following to my framework.yaml file:

framework
    http_client:
        default_options:
            retry_failed:
                retry_strategy: App\MyStrategy

And under src/MyStrategy.php I did:

<?php

namespace App;
use Symfony\Component\HttpClient\Response\AsyncContext;
use Symfony\Component\HttpClient\Retry\GenericRetryStrategy;
use Symfony\Component\HttpClient\Retry\RetryStrategyInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;

class MyStrategy extends GenericRetryStrategy  implements RetryStrategyInterface
{
    public const MAX_RESPONSE_TIME = 40; //total time with all attempts
    
    public $totalTime = 0;
    
    /**
     * @inheritDoc
     */
    public function shouldRetry(
        AsyncContext $context,
        ?string $responseContent,
        ?TransportExceptionInterface $exception
    ): ?bool 
    {
        $info = $context->getInfo();
        $this->totalTime += $info['total_time'] ?? 0;

        if ($this->totalTime >= self::MAX_RESPONSE_TIME) {
            return false;
        }

        return parent::shouldRetry($context, $responseContent, $exception);
    }

    /**
     * @inheritDoc
     */
    public function getDelay(
        AsyncContext $context,
        ?string $responseContent,
        ?TransportExceptionInterface $exception
    ): int
    {
        $delay = parent::getDelay($context, $responseContent, $exception);
        $this->totalTime += $delay / 1000;
        if ($this->totalTime >= self::MAX_RESPONSE_TIME) {
            $context->cancel();
        }
        
        return $delay;
    }
}

But the original problem still remains. If the first attempt fails in 30 seconds, a new one can still take up to 40 seconds to be completed, and the whole thing can take up to 70 seconds, instead of 40.