feat: retries of api requests in case of failure

This commit is contained in:
Ondrej Vlach 2024-01-18 20:50:23 +01:00
parent 7eac908bc4
commit 6e5f929dc3
Signed by: ovlach
GPG Key ID: 4FF1A23B4914DE70
7 changed files with 92 additions and 39 deletions

View File

@ -39,6 +39,13 @@ services:
arguments: arguments:
$username: '%app.usetreno.username%' $username: '%app.usetreno.username%'
$password: '%app.usetreno.password%' $password: '%app.usetreno.password%'
$retryCount: 2
$retryWaitSeconds: 0.5
App\Service\Remote\UsetrenoQRCodeProvider:
arguments:
$retryCount: 2
$retryWaitSeconds: 0.5
App\Service\CachedQRCodeGenerator: App\Service\CachedQRCodeGenerator:
arguments: arguments:

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\Entity\Remote\Usetreno; namespace App\Entity\Remote\Usetreno;

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Service\Remote\Exception;
class UsetrenoQRCodeRemoteServerErrorException extends UsetrenoQRCodeException
{
}

View File

@ -5,7 +5,9 @@ namespace App\Service\Remote;
use App\Entity\Remote\Usetreno\AuthRequest; use App\Entity\Remote\Usetreno\AuthRequest;
use App\Service\Remote\Exception\AuthorizeException; use App\Service\Remote\Exception\AuthorizeException;
use Nubium\Exception\ThisShouldNeverHappenException;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
@ -16,12 +18,16 @@ use Symfony\Contracts\HttpClient\ResponseStreamInterface;
class UsetrenoHttpClient implements HttpClientInterface class UsetrenoHttpClient implements HttpClientInterface
{ {
use RetryingFailClientTrait;
const AUTHORIZE_API = "https://topapi.top-test.cz/chameleon/api/v1/token"; const AUTHORIZE_API = "https://topapi.top-test.cz/chameleon/api/v1/token";
protected ?string $authorizationToken = null; protected ?string $authorizationToken = null;
public function __construct(protected HttpClientInterface $innerClient, protected readonly LoggerInterface $logger, protected readonly string $username, public function __construct(protected HttpClientInterface $innerClient, protected readonly LoggerInterface $logger, protected readonly string $username,
protected readonly string $password) { } protected readonly string $password,
protected readonly float $retryWaitSeconds,
protected readonly int $retryCount) { }
/** /**
* @param string $method * @param string $method
@ -88,6 +94,8 @@ class UsetrenoHttpClient implements HttpClientInterface
"AUTHORIZE_API" => static::AUTHORIZE_API "AUTHORIZE_API" => static::AUTHORIZE_API
]); ]);
$responseData = $this->retryingFailRequest($this->retryCount, $this->retryWaitSeconds,
[AuthorizeException::class, TransportException::class], $this->logger, function() {
$rq = $this->innerClient->request( $rq = $this->innerClient->request(
"POST", "POST",
static::AUTHORIZE_API, static::AUTHORIZE_API,
@ -110,6 +118,13 @@ class UsetrenoHttpClient implements HttpClientInterface
throw new AuthorizeException("Return code is not 200 OK (got: code: $statusCode)"); throw new AuthorizeException("Return code is not 200 OK (got: code: $statusCode)");
} }
return $responseData;
});
if (!is_string($responseData)) {
throw new ThisShouldNeverHappenException("responseData is not a string");
}
$this->authorizationToken = $this->processAuthorizeResponse($responseData); $this->authorizationToken = $this->processAuthorizeResponse($responseData);
$this->innerClient = $this->innerClient->withOptions([ $this->innerClient = $this->innerClient->withOptions([
'headers' => [ 'headers' => [

View File

@ -7,16 +7,23 @@ use App\Entity\DTO\QRCode\QRCode;
use App\Service\CacheableQRCodeGeneratorInterface; use App\Service\CacheableQRCodeGeneratorInterface;
use App\Service\Remote\Edge\QRCodeEntityConverter; use App\Service\Remote\Edge\QRCodeEntityConverter;
use App\Service\Remote\Exception\UsetrenoQRCodeException; use App\Service\Remote\Exception\UsetrenoQRCodeException;
use App\Service\Remote\Exception\UsetrenoQRCodeRemoteServerErrorException;
use Nubium\Exception\ThisShouldNeverHappenException;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\HttpClient\Exception\TransportException;
class UsetrenoQRCodeProvider implements CacheableQRCodeGeneratorInterface class UsetrenoQRCodeProvider implements CacheableQRCodeGeneratorInterface
{ {
use RetryingFailClientTrait;
const QRCODE_API = 'https://topapi.top-test.cz/chameleon/api/v1/qr-code/create-for-bank-account-payment'; const QRCODE_API = 'https://topapi.top-test.cz/chameleon/api/v1/qr-code/create-for-bank-account-payment';
const QRCODE_METHOD = 'POST'; const QRCODE_METHOD = 'POST';
public function __construct(protected readonly LoggerInterface $logger, public function __construct(protected readonly LoggerInterface $logger,
protected readonly UsetrenoHttpClient $client, protected readonly UsetrenoHttpClient $client,
protected readonly QRCodeEntityConverter $codeEntityConverter) { } protected readonly QRCodeEntityConverter $codeEntityConverter,
protected readonly float $retryWaitSeconds,
protected readonly int $retryCount) { }
public function generateQRCodeFromEntity(QRCode $entity): string public function generateQRCodeFromEntity(QRCode $entity): string
{ {
@ -27,6 +34,9 @@ class UsetrenoQRCodeProvider implements CacheableQRCodeGeneratorInterface
"edgeEntity" => $edgeEntity "edgeEntity" => $edgeEntity
]); ]);
$responseData = $this->retryingFailRequest($this->retryCount, $this->retryWaitSeconds,
[TransportException::class, UsetrenoQRCodeRemoteServerErrorException::class], $this->logger,
function() use ($edgeEntity) {
$response = $this->client->request(static::QRCODE_METHOD, static::QRCODE_API, [ $response = $this->client->request(static::QRCODE_METHOD, static::QRCODE_API, [
'json' => $edgeEntity, 'json' => $edgeEntity,
]); ]);
@ -41,6 +51,10 @@ class UsetrenoQRCodeProvider implements CacheableQRCodeGeneratorInterface
"content" => $responseData, "content" => $responseData,
]); ]);
if ($statusCode > 500) {
throw new UsetrenoQRCodeRemoteServerErrorException("Return code is not 200 OK (got: code: $statusCode)");
}
throw new UsetrenoQRCodeException("Return code is not 200 OK (got: code: $statusCode)"); throw new UsetrenoQRCodeException("Return code is not 200 OK (got: code: $statusCode)");
} }
@ -48,6 +62,13 @@ class UsetrenoQRCodeProvider implements CacheableQRCodeGeneratorInterface
"responseContent" => $responseData, "responseContent" => $responseData,
]); ]);
return $responseData;
});
if (!is_string($responseData)) {
throw new ThisShouldNeverHappenException("responseData is not a string");
}
return $this->parseBase64String($this->processQRCodeResponseEntity($responseData)); return $this->parseBase64String($this->processQRCodeResponseEntity($responseData));
} }

View File

@ -24,7 +24,7 @@ class UsetrenoHttpClientTest extends TestCase
]); ]);
$authorizedRequestResponse = clone $authMockResponse; $authorizedRequestResponse = clone $authMockResponse;
$mockedClient = new MockHttpClient([$authMockResponse, $authorizedRequestResponse]); $mockedClient = new MockHttpClient([$authMockResponse, $authorizedRequestResponse]);
$client = new UsetrenoHttpClient($mockedClient, $this->getLogger(), "foo", "bar"); $client = new UsetrenoHttpClient($mockedClient, $this->getLogger(), "foo", "bar", 0, 0);
$client->request("POST", "https://www.root.cz/"); $client->request("POST", "https://www.root.cz/");
$this->assertEquals("https://topapi.top-test.cz/chameleon/api/v1/token", $authMockResponse->getRequestUrl()); $this->assertEquals("https://topapi.top-test.cz/chameleon/api/v1/token", $authMockResponse->getRequestUrl());
$headers = $authorizedRequestResponse->getRequestOptions()['headers']; $headers = $authorizedRequestResponse->getRequestOptions()['headers'];
@ -48,7 +48,7 @@ class UsetrenoHttpClientTest extends TestCase
$authorizedRequestResponse = clone $authMockResponse; $authorizedRequestResponse = clone $authMockResponse;
$mockedClient = new MockHttpClient([$authMockResponse, $authorizedRequestResponse]); $mockedClient = new MockHttpClient([$authMockResponse, $authorizedRequestResponse]);
$client = new UsetrenoHttpClient($mockedClient, $this->getLogger(), "foo", "bar"); $client = new UsetrenoHttpClient($mockedClient, $this->getLogger(), "foo", "bar", 0, 0);
$client->request("POST", "https://www.root.cz/"); $client->request("POST", "https://www.root.cz/");
} }
} }

View File

@ -75,7 +75,7 @@ class UsetrenoQRCodeProviderTest extends TestCase
->method('convert') ->method('convert')
->will($this->returnValue($edgeEntity)); ->will($this->returnValue($edgeEntity));
return new UsetrenoQRCodeProvider($this->getLogger(), $mock, $converterMock); return new UsetrenoQRCodeProvider($this->getLogger(), $mock, $converterMock, 0, 0);
} }
protected function createQRCodeEntityPair() { protected function createQRCodeEntityPair() {