Skip to content

Commit 50d2f03

Browse files
committed
Split out decider and add tests
Signed-off-by: Kim Pepper <[email protected]>
1 parent 7d2e06b commit 50d2f03

10 files changed

+212
-108
lines changed

Diff for: CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
1212
- Added a test for the AWS signing client decorator
1313
- Added PHPStan Deprecation rules and baseline
1414
- Added PHPStan PHPUnit extensions and rules
15-
- Added Guzzle and Symfony HTTP client factories
15+
- Added Guzzle and Symfony HTTP client factories.
16+
- Added 'colinodell/psr-testlogger' as a dev dependency.
1617
### Changed
1718
- Switched to PSR Interfaces
1819
- Increased PHP min version to 8.1

Diff for: composer.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"require-dev": {
3939
"ext-zip": "*",
4040
"aws/aws-sdk-php": "^3.0",
41+
"colinodell/psr-testlogger": "^1.3",
4142
"friendsofphp/php-cs-fixer": "^v3.64",
4243
"guzzlehttp/psr7": "^2.7",
4344
"mockery/mockery": "^1.6",
@@ -53,7 +54,9 @@
5354
},
5455
"suggest": {
5556
"monolog/monolog": "Allows for client-level logging and tracing",
56-
"aws/aws-sdk-php": "Required (^3.0.0) in order to use the SigV4 handler"
57+
"aws/aws-sdk-php": "Required (^3.0.0) in order to use the AWS Signing Client Decorator",
58+
"guzzlehttp/psr7": "Required (^2.7) in order to use the Guzzle HTTP client",
59+
"symfony/http-client": "Required (^6.4|^7.0) in order to use the Symfony HTTP client"
5760
},
5861
"autoload": {
5962
"psr-4": {

Diff for: src/OpenSearch/GuzzleHttpClientFactory.php

-83
This file was deleted.
+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenSearch\HttpClient;
6+
7+
use GuzzleHttp\Client as GuzzleClient;
8+
use GuzzleHttp\HandlerStack;
9+
use GuzzleHttp\Middleware;
10+
use OpenSearch\Client;
11+
use Psr\Http\Client\ClientInterface;
12+
use Psr\Log\LoggerInterface;
13+
14+
/**
15+
* Builds an OpenSearch client using Guzzle.
16+
*/
17+
class GuzzleHttpClientFactory implements HttpClientFactoryInterface
18+
{
19+
public function __construct(
20+
protected int $maxRetries = 0,
21+
protected ?LoggerInterface $logger = null,
22+
) {
23+
}
24+
25+
/**
26+
* {@inheritdoc}
27+
*/
28+
public function create(array $options): ClientInterface
29+
{
30+
if (!isset($options['base_uri'])) {
31+
throw new \InvalidArgumentException('The base_uri option is required.');
32+
}
33+
// Set default configuration.
34+
$defaults = [
35+
'headers' => [
36+
'Accept' => 'application/json',
37+
'Content-Type' => 'application/json',
38+
'User-Agent' => sprintf('opensearch-php/%s (%s; PHP %s)', Client::VERSION, PHP_OS, PHP_VERSION),
39+
],
40+
];
41+
42+
// Merge the default options with the provided options.
43+
$config = array_merge_recursive($defaults, $options);
44+
45+
$stack = HandlerStack::create();
46+
47+
// Handle retries if max_retries is set.
48+
if ($this->maxRetries > 0) {
49+
$decider = new GuzzleRetryDecider($this->maxRetries, $this->logger);
50+
$stack->push(Middleware::retry($decider(...)));
51+
}
52+
53+
$config['handler'] = $stack;
54+
55+
return new GuzzleClient($config);
56+
}
57+
58+
}

Diff for: src/OpenSearch/HttpClient/GuzzleRetryDecider.php

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
namespace OpenSearch\HttpClient;
4+
5+
use GuzzleHttp\Exception\ConnectException;
6+
use Psr\Http\Message\RequestInterface;
7+
use Psr\Http\Message\ResponseInterface;
8+
use Psr\Log\LoggerInterface;
9+
10+
/**
11+
* Retry decider for Guzzle HTTP Client.
12+
*/
13+
class GuzzleRetryDecider
14+
{
15+
public function __construct(
16+
protected ?int $maxRetries = 0,
17+
protected ?LoggerInterface $logger = null,
18+
) {
19+
}
20+
21+
public function __invoke(int $retries, ?RequestInterface $request, ?ResponseInterface $response, $exception): bool
22+
{
23+
if ($retries >= $this->maxRetries) {
24+
return false;
25+
}
26+
if ($exception instanceof ConnectException) {
27+
$this->logger?->warning(
28+
'Retrying request {retries} of {maxRetries}: {exception}',
29+
[
30+
'retries' => $retries,
31+
'maxRetries' => $this->maxRetries,
32+
'exception' => $exception->getMessage(),
33+
]
34+
);
35+
return true;
36+
}
37+
if ($response && $response->getStatusCode() >= 500) {
38+
$this->logger?->warning(
39+
'Retrying request {retries} of {maxRetries}: Status code {status}',
40+
[
41+
'retries' => $retries,
42+
'maxRetries' => $this->maxRetries,
43+
'status' => $response->getStatusCode(),
44+
]
45+
);
46+
return true;
47+
}
48+
// We only retry if there is a 500 or a ConnectException.
49+
return false;
50+
}
51+
}

Diff for: src/OpenSearch/HttpClientFactoryInterface.php renamed to src/OpenSearch/HttpClient/HttpClientFactoryInterface.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
declare(strict_types=1);
44

5-
namespace OpenSearch;
5+
namespace OpenSearch\HttpClient;
66

77
use Psr\Http\Client\ClientInterface;
88

@@ -16,6 +16,6 @@ interface HttpClientFactoryInterface
1616
*
1717
* @param array<string,mixed> $options
1818
*/
19-
public static function create(array $options): ClientInterface;
19+
public function create(array $options): ClientInterface;
2020

2121
}

Diff for: src/OpenSearch/SymfonyHttpClientFactory.php renamed to src/OpenSearch/HttpClient/SymfonyHttpClientFactory.php

+12-9
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
declare(strict_types=1);
44

5-
namespace OpenSearch;
5+
namespace OpenSearch\HttpClient;
66

7+
use OpenSearch\Client;
78
use Psr\Http\Client\ClientInterface;
9+
use Psr\Log\LoggerInterface;
810
use Symfony\Component\HttpClient\HttpClient;
911
use Symfony\Component\HttpClient\Psr18Client;
1012
use Symfony\Component\HttpClient\RetryableHttpClient;
@@ -14,10 +16,16 @@
1416
*/
1517
class SymfonyHttpClientFactory implements HttpClientFactoryInterface
1618
{
19+
public function __construct(
20+
protected int $maxRetries = 0,
21+
protected ?LoggerInterface $logger = null,
22+
) {
23+
}
24+
1725
/**
1826
* {@inheritdoc}
1927
*/
20-
public static function create(array $options): ClientInterface
28+
public function create(array $options): ClientInterface
2129
{
2230
if (!isset($options['base_uri'])) {
2331
throw new \InvalidArgumentException('The base_uri option is required.');
@@ -31,16 +39,11 @@ public static function create(array $options): ClientInterface
3139
],
3240
];
3341
$options = array_merge_recursive($defaults, $options);
34-
$maxRetries = $options['max_retries'] ?? 0;
35-
unset($options['max_retries']);
36-
37-
$logger = $options['logger'] ?? null;
38-
unset($options['logger']);
3942

4043
$symfonyClient = HttpClient::create()->withOptions($options);
4144

42-
if ($maxRetries > 0) {
43-
$symfonyClient = new RetryableHttpClient($symfonyClient, null, $maxRetries, $logger);
45+
if ($this->maxRetries > 0) {
46+
$symfonyClient = new RetryableHttpClient($symfonyClient, null, $this->maxRetries, $this->logger);
4447
}
4548

4649
return new Psr18Client($symfonyClient);

Diff for: tests/GuzzleHttpClientFactoryTest.php renamed to tests/HttpClient/GuzzleHttpClientFactoryTest.php

+5-7
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,26 @@
22

33
declare(strict_types=1);
44

5-
namespace OpenSearch\Tests;
5+
namespace OpenSearch\Tests\HttpClient;
66

7-
use OpenSearch\GuzzleHttpClientFactory;
7+
use OpenSearch\HttpClient\GuzzleHttpClientFactory;
88
use PHPUnit\Framework\TestCase;
99
use Psr\Http\Client\ClientInterface;
10-
use Psr\Log\NullLogger;
1110

1211
/**
1312
* Test the Guzzle HTTP client factory.
1413
*
15-
* @coversDefaultClass \OpenSearch\GuzzleHttpClientFactory
14+
* @coversDefaultClass \OpenSearch\HttpClient\GuzzleHttpClientFactory
1615
*/
1716
class GuzzleHttpClientFactoryTest extends TestCase
1817
{
1918
public function testCreate()
2019
{
21-
$client = GuzzleHttpClientFactory::create([
20+
$factory = new GuzzleHttpClientFactory(2);
21+
$client = $factory->create([
2222
'base_uri' => 'http://example.com',
2323
'verify' => true,
24-
'max_retries' => 2,
2524
'auth' => ['username', 'password'],
26-
'logger' => new NullLogger(),
2725
]);
2826

2927
$this->assertInstanceOf(ClientInterface::class, $client);

Diff for: tests/HttpClient/GuzzleRetryDeciderTest.php

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenSearch\Tests\HttpClient;
6+
7+
use ColinODell\PsrTestLogger\TestLogger;
8+
use GuzzleHttp\Exception\ConnectException;
9+
use OpenSearch\HttpClient\GuzzleRetryDecider;
10+
use PHPUnit\Framework\TestCase;
11+
use Psr\Http\Message\RequestInterface;
12+
use Psr\Http\Message\ResponseInterface;
13+
14+
/**
15+
* Test the Guzzle retry decider.
16+
*
17+
* @coversDefaultClass \OpenSearch\HttpClient\GuzzleRetryDecider
18+
*/
19+
class GuzzleRetryDeciderTest extends TestCase
20+
{
21+
/**
22+
* @covers ::__invoke
23+
*/
24+
public function testMaxRetriesDoesNotRetry(): void
25+
{
26+
$decider = new GuzzleRetryDecider(2);
27+
$this->assertFalse($decider(2, null, null, null));
28+
}
29+
30+
public function test500orNoExceptionDoesNotRetry(): void
31+
{
32+
$decider = new GuzzleRetryDecider(2);
33+
$this->assertFalse($decider(0, null, null, null));
34+
}
35+
36+
/**
37+
* @covers ::__invoke
38+
*/
39+
public function testConnectExceptionRetries(): void
40+
{
41+
$logger = new TestLogger();
42+
$decider = new GuzzleRetryDecider(2, $logger);
43+
$this->assertTrue($decider(0, null, null, new ConnectException('Error', $this->createMock(RequestInterface::class))));
44+
$this->assertTrue($logger->hasWarning([
45+
'level' => 'warning',
46+
'message' => 'Retrying request {retries} of {maxRetries}: {exception}',
47+
'context' => [
48+
'retries' => 0,
49+
'maxRetries' => 2,
50+
'exception' => 'Error',
51+
],
52+
]));
53+
}
54+
55+
public function testStatus500Retries(): void
56+
{
57+
$logger = new TestLogger();
58+
$decider = new GuzzleRetryDecider(2, $logger);
59+
$response = $this->createMock(ResponseInterface::class);
60+
$response->method('getStatusCode')->willReturn(500);
61+
62+
$this->assertTrue($decider(0, null, $response, null));
63+
$this->assertTrue($logger->hasWarning([
64+
'level' => 'warning',
65+
'message' => 'Retrying request {retries} of {maxRetries}: Status code {status}',
66+
'context' => [
67+
'retries' => 0,
68+
'maxRetries' => 2,
69+
'status' => 500,
70+
],
71+
]));
72+
}
73+
}

0 commit comments

Comments
 (0)