Compare commits
4 Commits
acd9bfc2b3
...
70080bf80a
Author | SHA1 | Date | |
---|---|---|---|
70080bf80a | |||
6c23728c85 | |||
d7ff806129 | |||
13bdd096be |
@ -10,6 +10,7 @@
|
|||||||
"ext-iconv": "*",
|
"ext-iconv": "*",
|
||||||
"grpc/grpc": "^1.57",
|
"grpc/grpc": "^1.57",
|
||||||
"guzzlehttp/promises": "*",
|
"guzzlehttp/promises": "*",
|
||||||
|
"nubium/this-should-never-happen-exception": "^1.0",
|
||||||
"nyholm/psr7": "*",
|
"nyholm/psr7": "*",
|
||||||
"open-telemetry/exporter-otlp": "^1.0",
|
"open-telemetry/exporter-otlp": "^1.0",
|
||||||
"open-telemetry/opentelemetry-auto-symfony": "^1.0@beta",
|
"open-telemetry/opentelemetry-auto-symfony": "^1.0@beta",
|
||||||
|
43
composer.lock
generated
43
composer.lock
generated
@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "61ab226932ff1cf0567a22bbf802e20c",
|
"content-hash": "60708df22ad1ee4eca712d46f8e02c3b",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "clue/stream-filter",
|
"name": "clue/stream-filter",
|
||||||
@ -658,6 +658,47 @@
|
|||||||
],
|
],
|
||||||
"time": "2023-10-27T15:32:31+00:00"
|
"time": "2023-10-27T15:32:31+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "nubium/this-should-never-happen-exception",
|
||||||
|
"version": "v1.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/nubium/this-should-never-happen-exception.git",
|
||||||
|
"reference": "3ed1b6f725881c527050c235e2503a8300427b86"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/nubium/this-should-never-happen-exception/zipball/3ed1b6f725881c527050c235e2503a8300427b86",
|
||||||
|
"reference": "3ed1b6f725881c527050c235e2503a8300427b86",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"jakub-onderka/php-parallel-lint": "~1.0",
|
||||||
|
"phpstan/phpstan": "~0.9"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Nubium\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Jiri Travnicek",
|
||||||
|
"email": "jiri.travnicek@nubium.cz"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Extend this exception and throw it anytime something unexpected happens.",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/nubium/this-should-never-happen-exception/issues",
|
||||||
|
"source": "https://github.com/nubium/this-should-never-happen-exception/tree/master"
|
||||||
|
},
|
||||||
|
"time": "2018-03-27T10:16:09+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "nyholm/psr7",
|
"name": "nyholm/psr7",
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
|
@ -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:
|
||||||
|
@ -11,7 +11,7 @@ use Symfony\Component\Routing\Attribute\Route;
|
|||||||
class ExceptionController extends AbstractController
|
class ExceptionController extends AbstractController
|
||||||
{
|
{
|
||||||
#[Route("give-me-error-please/exception")]
|
#[Route("give-me-error-please/exception")]
|
||||||
public function makeException() {
|
public function makeException(): void {
|
||||||
throw new \InvalidArgumentException("There is exception");
|
throw new \InvalidArgumentException("There is exception");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Entity\Remote\Usetreno;
|
namespace App\Entity\Remote\Usetreno;
|
||||||
|
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Service\Remote\Exception;
|
||||||
|
|
||||||
|
class UsetrenoQRCodeRemoteServerErrorException extends UsetrenoQRCodeException
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
59
src/Service/Remote/RetryingFailClientTrait.php
Normal file
59
src/Service/Remote/RetryingFailClientTrait.php
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service\Remote;
|
||||||
|
|
||||||
|
use App\Service\Remote\Exception\AuthorizeException;
|
||||||
|
use Exception;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||||
|
|
||||||
|
trait RetryingFailClientTrait
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Lepsi zopakovat request kvuli drobnemu vypadku site nebo sluzby (u nedestruktivni operace)
|
||||||
|
* nez hodit klientovi rovnou 500
|
||||||
|
*
|
||||||
|
* @param int $count
|
||||||
|
* @param float $sleep
|
||||||
|
* @param array<string> $catchableExceptions
|
||||||
|
* @param LoggerInterface $logger
|
||||||
|
* @param callable $callback
|
||||||
|
* @return mixed
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
protected function retryingFailRequest(
|
||||||
|
int $count,
|
||||||
|
float $sleep,
|
||||||
|
array $catchableExceptions,
|
||||||
|
LoggerInterface $logger,
|
||||||
|
callable $callback
|
||||||
|
): mixed {
|
||||||
|
for ($i = 0; ; $i++) {
|
||||||
|
try {
|
||||||
|
return $callback();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
foreach ($catchableExceptions as $exceptionClass) {
|
||||||
|
if ($e instanceof $exceptionClass) {
|
||||||
|
$logger->error("transport: fail request retrying... got catchable exception", [
|
||||||
|
'exception' => $e,
|
||||||
|
'try' => $i
|
||||||
|
]);
|
||||||
|
|
||||||
|
usleep((int) ($sleep * 1_000_000));
|
||||||
|
|
||||||
|
if ($i == $count) {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// phpstan fail
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -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' => [
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
56
tests/Service/Remote/RetryingFailClientTraitTest.php
Normal file
56
tests/Service/Remote/RetryingFailClientTraitTest.php
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Service\Remote;
|
||||||
|
|
||||||
|
use App\Service\Remote\RetryingFailClientTrait;
|
||||||
|
use App\Tests\Common\LoggerTrait;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
class RetryingFailClientTraitTest extends TestCase {
|
||||||
|
use LoggerTrait;
|
||||||
|
|
||||||
|
private int $callCount = 0;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp(); // TODO: Change the autogenerated stub
|
||||||
|
$this->callCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSuccess() {
|
||||||
|
$trait = new class {
|
||||||
|
use RetryingFailClientTrait {
|
||||||
|
retryingFailRequest as public; // make the method public
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
$result = $trait->retryingFailRequest(2, 0, [\RuntimeException::class], $this->getLogger(), function () {
|
||||||
|
$this->callCount = $this->callCount + 1;
|
||||||
|
return 'foo';
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->assertEquals(1, $this->callCount);
|
||||||
|
$this->assertEquals('foo', $result);
|
||||||
|
}
|
||||||
|
public function testRetyingFail() {
|
||||||
|
$trait = new class {
|
||||||
|
use RetryingFailClientTrait {
|
||||||
|
retryingFailRequest as public; // make the method public
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
$trait->retryingFailRequest(2, 0, [\RuntimeException::class], $this->getLogger(), function () {
|
||||||
|
$this->callCount = $this->callCount + 1;
|
||||||
|
throw new \RuntimeException("test");
|
||||||
|
});
|
||||||
|
} catch (\RuntimeException) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertEquals(3, $this->callCount);
|
||||||
|
}
|
||||||
|
}
|
@ -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/");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user