Skip to content

Commit e271212

Browse files
committed
Add CLI flag to retry HTTP requests
1 parent 3bd0118 commit e271212

File tree

7 files changed

+134
-27
lines changed

7 files changed

+134
-27
lines changed

src/Adapter/Http/AbstractHttp.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313

1414
abstract class AbstractHttp implements HttpInterface
1515
{
16+
// Default delay between HTTP retries
17+
protected const DEFAULT_DELAY_MS = 100;
18+
1619
protected $baseUrl;
1720

1821
public function __construct($baseUrl)

src/Adapter/Http/FileGetContents.php

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,32 @@
1313

1414
class FileGetContents extends AbstractHttp
1515
{
16+
public function __construct($baseUrl, protected int $maxRetries = 0, protected int $delayMs = self::DEFAULT_DELAY_MS)
17+
{
18+
parent::__construct($baseUrl);
19+
}
20+
1621
public function fetch($filename)
1722
{
1823
$url = "{$this->baseUrl}/{$filename}";
19-
$contents = @file_get_contents($url);
24+
$retry = $this->maxRetries;
2025

21-
if (false === $contents) {
22-
return serialize([
23-
'result' => false,
24-
'errors' => [
25-
[
26-
'no' => 0,
27-
'str' => "file_get_contents() call failed with url: {$url}",
28-
],
29-
],
30-
]);
31-
}
26+
do {
27+
$contents = @file_get_contents($url);
28+
if (false !== $contents) {
29+
return $contents;
30+
}
31+
usleep($this->delayMs * 1000);
32+
} while ($retry--);
3233

33-
return $contents;
34+
return serialize([
35+
'result' => false,
36+
'errors' => [
37+
[
38+
'no' => 0,
39+
'str' => "file_get_contents() call failed with url: {$url}",
40+
],
41+
],
42+
]);
3443
}
3544
}

src/Adapter/Http/SymfonyHttpClient.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,24 @@
1212
namespace CacheTool\Adapter\Http;
1313

1414
use Symfony\Component\HttpClient\HttpClient;
15+
use Symfony\Component\HttpClient\Retry\GenericRetryStrategy;
16+
use Symfony\Component\HttpClient\RetryableHttpClient;
1517
use Symfony\Component\HttpFoundation\Response;
1618

1719
class SymfonyHttpClient extends AbstractHttp
1820
{
1921
private $client;
2022

21-
public function __construct($baseUrl, $httpClientConfig = [])
23+
public function __construct($baseUrl, $httpClientConfig = [], int $maxRetries = 0, int $delayMs = self::DEFAULT_DELAY_MS)
2224
{
2325
$this->client = HttpClient::create($httpClientConfig);
26+
if ($maxRetries > 0) {
27+
$this->client = new RetryableHttpClient(
28+
$this->client,
29+
new GenericRetryStrategy(GenericRetryStrategy::DEFAULT_RETRY_STATUS_CODES, $delayMs),
30+
$maxRetries,
31+
);
32+
}
2433
parent::__construct($baseUrl);
2534
}
2635

src/Console/Application.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ protected function getDefaultInputDefinition(): InputDefinition
114114
$definition->addOption(new InputOption('--web-allow-insecure', null, InputOption::VALUE_NONE, 'If specified, verify_peer and verify_host are disabled (only for SymfonyHttpClient)'));
115115
$definition->addOption(new InputOption('--web-basic-auth', null, InputOption::VALUE_OPTIONAL, 'If specified, used for basic authorization (only for SymfonyHttpClient)'));
116116
$definition->addOption(new InputOption('--web-host', null, InputOption::VALUE_OPTIONAL, 'If specified, adds a Host header to web adapter request (only for SymfonyHttpClient)'));
117+
$definition->addOption(new InputOption('--web-retry', null, InputOption::VALUE_REQUIRED, 'If specified, failed HTTP requests will be retried for the specified amount of times'));
117118
$definition->addOption(new InputOption('--tmp-dir', '-t', InputOption::VALUE_REQUIRED, 'Temporary directory to write files to'));
118119
$definition->addOption(new InputOption('--config', '-c', InputOption::VALUE_REQUIRED, 'If specified use this yaml configuration file'));
119120
return $definition;
@@ -198,6 +199,7 @@ private function parseConfiguration(InputInterface $input)
198199
$this->config['webClient'] = $input->getOption('web') ?? 'FileGetContents';
199200
$this->config['webPath'] = $input->getOption('web-path') ?? $this->config['webPath'];
200201
$this->config['webUrl'] = $input->getOption('web-url') ?? $this->config['webUrl'];
202+
$this->config['webRetry'] = $input->getOption('web-retry') ?? $this->config['webRetry'];
201203

202204
if ($this->config['webClient'] === 'SymfonyHttpClient') {
203205
$this->config['webAllowInsecure'] = $input->getOption('web-allow-insecure');
@@ -214,7 +216,7 @@ private function parseConfiguration(InputInterface $input)
214216
}
215217

216218
/**
217-
* @return \CacheTool\Adapter\HttpInterface
219+
* @return \CacheTool\Adapter\Http\HttpInterface
218220
*/
219221
private function buildHttpClient()
220222
{
@@ -236,7 +238,7 @@ private function buildHttpClient()
236238
$options['headers']['Host'] = $this->config['webHost'];
237239
}
238240

239-
return new SymfonyHttpClient($this->config['webUrl'], $options);
241+
return new SymfonyHttpClient($this->config['webUrl'], $options, $this->config['webRetry']);
240242
}
241243

242244
if ($this->config['webClient'] !== 'FileGetContents') {
@@ -246,7 +248,7 @@ private function buildHttpClient()
246248
));
247249
}
248250

249-
return new FileGetContents($this->config['webUrl']);
251+
return new FileGetContents($this->config['webUrl'], $this->config['webRetry']);
250252
}
251253

252254
/**

src/Console/Config.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class Config implements \ArrayAccess
2929
'webAllowInsecure' => null,
3030
'webBasicAuth' => null,
3131
'webHost' => null,
32+
'webRetry' => 3,
3233

3334
'http' => null,
3435
];

tests/Adapter/Http/FileGetContentsTest.php

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,58 @@
77

88
class FileGetContentsTest extends \PHPUnit\Framework\TestCase
99
{
10-
/** @var Process */
11-
private static $process;
10+
private static ?Process $process = null;
1211

13-
public static function setUpBeforeClass(): void
12+
private static function startServer(int $wait = 100000): void
1413
{
14+
// Server is already running
15+
if (self::$process instanceof Process) {
16+
return;
17+
}
18+
1519
self::$process = new Process(['php', '-S', '127.0.0.1:9999', '-t', '.']);
1620
self::$process->start();
1721

18-
usleep(100000); //wait for server to get going
22+
if ($wait) {
23+
usleep($wait); //wait for server to get going
24+
}
1925
}
2026

21-
public static function tearDownAfterClass(): void
27+
private static function stopServer(): void
2228
{
29+
// Do nothing if server is not running
30+
if (!self::$process instanceof Process) {
31+
return;
32+
}
33+
2334
self::$process->stop();
35+
self::$process = null;
36+
}
37+
38+
public static function tearDownAfterClass(): void
39+
{
40+
// Make sure to stop server after all tests
41+
self::stopServer();
2442
}
2543

2644
public function testFetch()
2745
{
46+
self::startServer();
2847
$client = new FileGetContents('http://localhost:9999');
2948
$this->assertStringStartsWith('# CacheTool', $client->fetch('README.md'));
3049
}
3150

51+
public function testFetchRetry(): void
52+
{
53+
self::stopServer();
54+
self::startServer(0);
55+
$client = new FileGetContents('http://localhost:9999', 10, 10);
56+
$this->assertStringStartsWith('# CacheTool', $client->fetch('README.md'));
57+
}
58+
3259
public function testFetchUnderscores()
3360
{
61+
self::startServer();
3462
$sslipHostname = '_.127.0.0.1.sslip.io';
3563
if (!gethostbynamel($sslipHostname)) {
3664
$this->markTestSkipped(
@@ -43,11 +71,24 @@ public function testFetchUnderscores()
4371

4472
public function testFetchFailed()
4573
{
74+
self::startServer();
4675
$client = new FileGetContents('http://localhost:9999');
4776
$result = unserialize($client->fetch('does-not-exist'));
4877

4978
$this->assertIsArray($result);
5079
$this->assertEquals(false, $result['result']);
5180
$this->assertCount(1, $result['errors']);
5281
}
82+
83+
public function testFetchRetryFailed(): void
84+
{
85+
self::stopServer();
86+
self::startServer(0);
87+
$client = new FileGetContents('http://localhost:9999', 1, 2);
88+
$result = unserialize($client->fetch('README.md'));
89+
90+
$this->assertIsArray($result);
91+
$this->assertEquals(false, $result['result']);
92+
$this->assertCount(1, $result['errors']);
93+
}
5394
}

tests/Adapter/Http/SymfonyHttpClientTest.php

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,58 @@
77

88
class SymfonyHttpClientTest extends \PHPUnit\Framework\TestCase
99
{
10-
/** @var Process */
11-
private static $process;
10+
private static ?Process $process = null;
1211

13-
public static function setUpBeforeClass(): void
12+
private static function startServer(int $wait = 100000): void
1413
{
14+
// Server is already running
15+
if (self::$process instanceof Process) {
16+
return;
17+
}
18+
1519
self::$process = new Process(['php', '-S', '127.0.0.1:9999', '-t', '.']);
1620
self::$process->start();
1721

18-
usleep(100000); //wait for server to get going
22+
if ($wait) {
23+
usleep($wait); //wait for server to get going
24+
}
1925
}
2026

21-
public static function tearDownAfterClass(): void
27+
private static function stopServer(): void
2228
{
29+
// Do nothing if server is not running
30+
if (!self::$process instanceof Process) {
31+
return;
32+
}
33+
2334
self::$process->stop();
35+
self::$process = null;
36+
}
37+
38+
public static function tearDownAfterClass(): void
39+
{
40+
// Make sure to stop server after all tests
41+
self::stopServer();
2442
}
2543

2644
public function testFetch()
2745
{
46+
self::startServer();
2847
$client = new SymfonyHttpClient('http://localhost:9999');
2948
$this->assertStringStartsWith('# CacheTool', $client->fetch('README.md'));
3049
}
3150

51+
public function testFetchRetry(): void
52+
{
53+
self::stopServer();
54+
self::startServer(0);
55+
$client = new SymfonyHttpClient('http://localhost:9999', [], 10, 10);
56+
$this->assertStringStartsWith('# CacheTool', $client->fetch('README.md'));
57+
}
58+
3259
public function testFetchUnderscores()
3360
{
61+
self::startServer();
3462
$sslipHostname = '_.127.0.0.1.sslip.io';
3563
if (!gethostbynamel($sslipHostname)) {
3664
$this->markTestSkipped(
@@ -43,6 +71,7 @@ public function testFetchUnderscores()
4371

4472
public function testFetchFailed()
4573
{
74+
self::startServer();
4675
$client = new SymfonyHttpClient('http://localhost:9999');
4776
$result = unserialize($client->fetch('does-not-exist'));
4877

@@ -53,11 +82,24 @@ public function testFetchFailed()
5382

5483
public function testFetchInvalidUrl()
5584
{
85+
self::startServer();
5686
$client = new SymfonyHttpClient('foo');
5787
$result = unserialize($client->fetch('bar'));
5888

5989
$this->assertIsArray($result);
6090
$this->assertEquals(false, $result['result']);
6191
$this->assertCount(1, $result['errors']);
6292
}
93+
94+
public function testFetchRetryFailed(): void
95+
{
96+
self::stopServer();
97+
self::startServer(0);
98+
$client = new SymfonyHttpClient('http://localhost:9999', [], 1, 2);
99+
$result = unserialize($client->fetch('README.md'));
100+
101+
$this->assertIsArray($result);
102+
$this->assertEquals(false, $result['result']);
103+
$this->assertCount(1, $result['errors']);
104+
}
63105
}

0 commit comments

Comments
 (0)