assertTrue($request->hasHeader('aws-sdk-retry')); return new RejectedPromise( new AwsException('e', $command, ['connection_error' => true]) ); }; $retryMW = new RetryMiddleware( RetryMiddleware::createDefaultDecider($retries = 3), [RetryMiddleware::class, 'exponentialDelay'], $nextHandler, true ); try { $retryMW(new Command('SomeCommand'), new Request('GET', ''))->wait(); $this->fail(); } catch (AwsException $e) { } } public function testDeciderRetriesWhenStatusCodeMatches() { $decider = RetryMiddleware::createDefaultDecider(); $command = new Command('foo'); $request = new Request('GET', 'http://www.example.com'); $result = new Result(['@metadata' => ['statusCode' => '500']]); $this->assertTrue($decider(0, $command, $request, $result, null)); $result = new Result(['@metadata' => ['statusCode' => '503']]); $this->assertTrue($decider(0, $command, $request, $result, null)); } public function testDeciderRetriesWhenConnectionError() { $decider = RetryMiddleware::createDefaultDecider(); $command = new Command('foo'); $request = new Request('GET', 'http://www.example.com'); $err = new AwsException('e', $command, ['connection_error' => true]); $this->assertTrue($decider(0, $command, $request, null, $err)); $err = new AwsException('e', $command, ['connection_error' => false]); $this->assertFalse($decider(0, $command, $request, null, $err)); } public function testDeciderIgnoresNonAwsExceptions() { $decider = RetryMiddleware::createDefaultDecider(); $command = new Command('foo'); $request = new Request('GET', 'http://www.example.com'); $err = new \Exception('e'); $this->assertFalse($decider(0, $command, $request, null, $err)); } public function testDeciderIgnoresPHPError() { if (interface_exists('Throwable', false)) { $decider = RetryMiddleware::createDefaultDecider(); $command = new Command('foo'); $request = new Request('GET', 'http://www.example.com'); $err = new \Error('e'); $this->assertFalse($decider(0, $command, $request, null, $err)); } } public function testDeciderRetriesWhenCurlErrorCodeMatches() { if (!extension_loaded('curl')) { $this->markTestSkipped('Test skipped on no cURL extension'); } $decider = RetryMiddleware::createDefaultDecider(); $command = new Command('foo'); $request = new Request('GET', 'http://www.example.com'); $version = \Aws\guzzle_major_version(); if ($version === 6 || $version === 7) { $previous = new RequestException( 'test', $request, null, null, ['errno' => CURLE_RECV_ERROR] ); } elseif ($version === 5) { $previous = new RequestException( 'cURL error ' . CURLE_RECV_ERROR . ': test', new \GuzzleHttp\Message\Request('GET', 'http://www.example.com') ); } $err = new AwsException( 'e', $command, ['connection_error' => false], $previous ); $this->assertTrue($decider(0, $command, $request, null, $err)); } public function testDeciderRetriesForCustomCurlErrors() { if (!extension_loaded('curl')) { $this->markTestSkipped('Test skipped on no cURL extension'); } $decider = RetryMiddleware::createDefaultDecider( 3, ['curl_errors' => [CURLE_BAD_CONTENT_ENCODING]] ); $command = new Command('foo'); $request = new Request('GET', 'http://www.example.com'); $version = \Aws\guzzle_major_version(); // Custom error passed in to decider config should result in a retry if ($version === 6 || $version === 7) { $previous = new RequestException( 'test', $request, null, null, ['errno' => CURLE_BAD_CONTENT_ENCODING] ); } elseif ($version === 5) { $previous = new RequestException( 'cURL error ' . CURLE_BAD_CONTENT_ENCODING . ': test', new \GuzzleHttp\Message\Request('GET', 'http://www.example.com') ); } $err = new AwsException( 'e', $command, ['connection_error' => false], $previous ); $this->assertTrue($decider(0, $command, $request, null, $err)); // Error not passed in to decider config should result in no retry if ($version === 6 || $version === 7) { $previous = new RequestException( 'test', $request, null, null, ['errno' => CURLE_ABORTED_BY_CALLBACK] ); } elseif ($version === 5) { $previous = new RequestException( 'cURL error ' . CURLE_ABORTED_BY_CALLBACK . ': test', new \GuzzleHttp\Message\Request('GET', 'http://www.example.com') ); } $err = new AwsException( 'e', $command, ['connection_error' => false], $previous ); $this->assertFalse($decider(0, $command, $request, null, $err)); } public function awsErrorCodeProvider() { $command = new Command('foo'); return [ [new AwsException('e', $command, ['code' => 'RequestLimitExceeded'])], [new AwsException('e', $command, ['code' => 'Throttling'])], [new AwsException('e', $command, ['code' => 'ThrottlingException'])], [new AwsException('e', $command, ['code' => 'ProvisionedThroughputExceededException'])], [new AwsException('e', $command, ['code' => 'RequestThrottled'])], [new AwsException('e', $command, ['code' => 'BandwidthLimitExceeded'])], [new AwsException('e', $command, ['code' => 'RequestThrottledException'])], [new AwsException('e', $command, ['code' => 'TooManyRequestsException'])], [new AwsException('e', $command, ['code' => 'IDPCommunicationError'])], [new AwsException('e', $command, ['code' => 'EC2ThrottledException'])], ]; } /** * @param $err * * @dataProvider awsErrorCodeProvider */ public function testDeciderRetriesWhenAwsErrorCodeMatches($err) { $decider = RetryMiddleware::createDefaultDecider(); $command = new Command('foo'); $request = new Request('GET', 'http://www.example.com'); $this->assertTrue($decider(0, $command, $request, null, $err)); } public function testDeciderRetriesWhenExceptionStatusCodeMatches() { $decider = RetryMiddleware::createDefaultDecider(); $command = new Command('foo'); $request = new Request('GET', 'http://www.example.com'); $err = new AwsException('e', $command, ['response' => new Response(500)]); $this->assertTrue($decider(0, $command, $request, null, $err)); $err = new AwsException('e', $command, ['response' => new Response(502)]); $this->assertTrue($decider(0, $command, $request, null, $err)); $err = new AwsException('e', $command, ['response' => new Response(503)]); $this->assertTrue($decider(0, $command, $request, null, $err)); $err = new AwsException('e', $command, ['response' => new Response(504)]); $this->assertTrue($decider(0, $command, $request, null, $err)); $err = new AwsException('e', $command, ['response' => new Response(403)]); $this->assertFalse($decider(0, $command, $request, null, $err)); } public function testDeciderRetriesForCustomErrorCodes() { $decider = RetryMiddleware::createDefaultDecider( 3, ['error_codes' => ['CustomRetryableException']] ); $command = new Command('foo'); $request = new Request('GET', 'http://www.example.com'); $err = new AwsException('e', $command, [ 'code' => 'CustomRetryableException' ]); $this->assertTrue($decider(0, $command, $request, null, $err)); $err = new AwsException('e', $command, [ 'code' => 'CustomNonRetryableException' ]); $this->assertFalse($decider(0, $command, $request, null, $err)); } public function testDeciderRetriesForCustomStatusCodes() { $decider = RetryMiddleware::createDefaultDecider( 3, ['status_codes' => [400]] ); $command = new Command('foo'); $request = new Request('GET', 'http://www.example.com'); $err = new AwsException('e', $command, ['response' => new Response(400)]); $this->assertTrue($decider(0, $command, $request, null, $err)); $err = new AwsException('e', $command, ['response' => new Response(401)]); $this->assertFalse($decider(0, $command, $request, null, $err)); } public function testDeciderDoesNotRetryAfterMaxAttempts() { $decider = RetryMiddleware::createDefaultDecider(); $command = new Command('foo'); $request = new Request('GET', 'http://www.example.com'); $err = new AwsException('e', $command, ['code' => 'RequestLimitExceeded']); $this->assertTrue($decider(0, $command, $request, null, $err)); $this->assertFalse($decider(3, $command, $request, null, $err)); } public function testDelaysExponentially() { $this->assertLessThanOrEqual(100, RetryMiddleware::exponentialDelay(0)); $this->assertLessThanOrEqual(200, RetryMiddleware::exponentialDelay(1)); $this->assertLessThanOrEqual(400, RetryMiddleware::exponentialDelay(2)); $this->assertLessThanOrEqual(800, RetryMiddleware::exponentialDelay(3)); $this->assertLessThanOrEqual(20000, RetryMiddleware::exponentialDelay(10)); } public function testDelaysWithSomeRandomness() { $maxDelay = 100 * pow(2, 5); $values = array_map(function () { return RetryMiddleware::exponentialDelay(5); }, range(1, 200)); $this->assertGreaterThan(1, count(array_unique($values))); foreach ($values as $value) { $this->assertGreaterThanOrEqual(0, $value); $this->assertLessThanOrEqual($maxDelay, $value); } } public function testRetriesWhenResultMatches() { $command = new Command('foo'); $request = new Request('GET', 'http://www.example.com'); $res1 = new Result(['@metadata' => ['statusCode' => '503']]); $res2 = new Result(['@metadata' => ['statusCode' => '200']]); $mock = new MockHandler( [ function ($command, $request) use ($res1) { $this->assertArrayNotHasKey('delay', $command['@http']); return $res1; }, function ($command, $request) use ($res2) { $this->assertLessThanOrEqual(100, $command['@http']['delay']); return $res2; }, ], function () use (&$called) { $called[] = func_get_args(); } ); $wrapped = new RetryMiddleware( RetryMiddleware::createDefaultDecider(), [RetryMiddleware::class, 'exponentialDelay'], $mock ); $result = $wrapped($command, $request)->wait(); $this->assertSame($res2, $result); $this->assertCount(2, $called); $this->assertSame([$res1], $called[0]); $this->assertSame([$res2], $called[1]); } public function testRetriesWhenExceptionMatches() { $command = new Command('foo'); $request = new Request('GET', 'http://www.example.com'); $mock = new MockHandler( [ function ($command, $request) { $this->assertArrayNotHasKey('delay', $command['@http']); return new AwsException('foo', $command, [ 'connection_error' => true ]); }, function ($command, $request) { $this->assertLessThanOrEqual(100, $command['@http']['delay']); return new Result(); }, ], function () use (&$called) { $called[] = func_get_args(); }, function () use (&$called) { $called[] = func_get_args(); } ); $wrapped = new RetryMiddleware( RetryMiddleware::createDefaultDecider(), [RetryMiddleware::class, 'exponentialDelay'], $mock ); $result = $wrapped($command, $request)->wait(); $this->assertInstanceOf(ResultInterface::class, $result); $this->assertCount(2, $called); $this->assertInstanceOf(AwsException::class, $called[0][0]); $this->assertInstanceOf(ResultInterface::class, $called[1][0]); } public function testForwardRejectionWhenExceptionDoesNotMatch() { $command = new Command('foo'); $request = new Request('GET', 'http://www.example.com'); $mock = new MockHandler( [ function ($command, $request) { $this->assertArrayNotHasKey('delay', $command['@http']); return new AwsException('foo', $command); } ], function () use (&$called) { $called[] = func_get_args(); }, function () use (&$called) { $called[] = func_get_args(); } ); $wrapped = new RetryMiddleware( RetryMiddleware::createDefaultDecider(), [RetryMiddleware::class, 'exponentialDelay'], $mock ); try { $wrapped($command, $request)->wait(); $this->fail(); } catch (AwsException $e) { $this->assertCount(1, $called); $this->assertStringContainsString('foo', $e->getMessage()); } } public function testForwardValueWhenResultDoesNotMatch() { $command = new Command('foo'); $request = new Request('GET', 'http://www.example.com'); $res1 = new Result(); $mock = new MockHandler( [$res1], function () use (&$called) { $called[] = func_get_args(); }, function () use (&$called) { $called[] = func_get_args(); } ); $wrapped = new RetryMiddleware( RetryMiddleware::createDefaultDecider(), [RetryMiddleware::class, 'exponentialDelay'], $mock ); $result = $wrapped($command, $request)->wait(); $this->assertSame($res1, $result); $this->assertCount(1, $called); } public function testRetriesCanBeDisabledOnACommand() { $decider = RetryMiddleware::createDefaultDecider($retries = 3); $command = new Command('foo', ['@retries' => 0]); $request = new Request('GET', 'http://www.example.com'); $err = new AwsException('e', $command, ['connection_error' => true]); $this->assertFalse($decider(0, $command, $request, null, $err)); } public function testResultReportsTheNumberOfRetries() { $handler = new MockHandler([ new Result(['@metadata' => ['statusCode' => '503']]), new Result(['@metadata' => ['statusCode' => '503']]), new Result(['@metadata' => ['statusCode' => '200']]), ]); $retryMW = new RetryMiddleware( RetryMiddleware::createDefaultDecider($retries = 3), [RetryMiddleware::class, 'exponentialDelay'], $handler, true ); $result = $retryMW(new Command('SomeCommand'), new Request('GET', '')) ->wait(); $this->assertArrayHasKey('retries_attempted', $result['@metadata']['transferStats']); $this->assertSame(2, $result['@metadata']['transferStats']['retries_attempted']); } public function testExceptionReportsTheNumberOfRetries() { $nextHandler = function (CommandInterface $command) { return new RejectedPromise( new AwsException('e', $command, ['connection_error' => true]) ); }; $retryMW = new RetryMiddleware( RetryMiddleware::createDefaultDecider($retries = 3), [RetryMiddleware::class, 'exponentialDelay'], $nextHandler, true ); try { $retryMW(new Command('SomeCommand'), new Request('GET', ''))->wait(); $this->fail(); } catch (AwsException $e) { $this->assertSame(3, $e->getTransferInfo('retries_attempted')); } } public function testResultReportsTotalRetryDelay() { $handler = new MockHandler([ new Result(['@metadata' => ['statusCode' => '503']]), new Result(['@metadata' => ['statusCode' => '503']]), new Result(['@metadata' => ['statusCode' => '200']]), ]); $retryMW = new RetryMiddleware( RetryMiddleware::createDefaultDecider($retries = 3), function () { return 100; }, $handler, true ); $result = $retryMW(new Command('SomeCommand'), new Request('GET', '')) ->wait(); $this->assertArrayHasKey('total_retry_delay', $result['@metadata']['transferStats']); $this->assertSame(200, $result['@metadata']['transferStats']['total_retry_delay']); } public function testExceptionReportsTotalRetryDelay() { $nextHandler = function (CommandInterface $command) { return new RejectedPromise( new AwsException('e', $command, ['connection_error' => true]) ); }; $retryMW = new RetryMiddleware( RetryMiddleware::createDefaultDecider($retries = 3), function () { return 100; }, $nextHandler, true ); try { $retryMW(new Command('SomeCommand'), new Request('GET', ''))->wait(); $this->fail(); } catch (AwsException $e) { $this->assertSame(300, $e->getTransferInfo('total_retry_delay')); } } public function testReportsHttpStatsForEachRequest() { $handler = new MockHandler([ new Result(['@metadata' => [ 'statusCode' => '503', 'transferStats' => ['http' => [['foo' => 'bar']]] ]]), new Result(['@metadata' => [ 'statusCode' => '503', 'transferStats' => ['http' => [['baz' => 'quux']]] ]]), new Result(['@metadata' => [ 'statusCode' => '200', 'transferStats' => ['http' => [['fizz' => 'buzz']]] ]]), ]); $retryMW = new RetryMiddleware( RetryMiddleware::createDefaultDecider($retries = 3), [RetryMiddleware::class, 'exponentialDelay'], $handler, true ); $result = $retryMW(new Command('SomeCommand'), new Request('GET', '')) ->wait(); $httpStats = $result['@metadata']['transferStats']['http']; $this->assertCount(3, $httpStats); $this->assertSame([ ['foo' => 'bar'], ['baz' => 'quux'], ['fizz' => 'buzz'], ], $httpStats); } public function testReportsHttpStatsForEachException() { $command = new Command('TestCommand'); $response = new Response(500); $handler = new MockHandler([ new AwsException( 'Test Exception', $command, [ 'response' => $response, 'transfer_stats' => [ 'starttransfer_time' => 5, 'appconnect_time' => 4 ] ] ), new AwsException( 'Test Exception', $command, [ 'response' => $response, 'transfer_stats' => [ 'starttransfer_time' => 10, 'appconnect_time' => 8 ] ] ), new AwsException( 'Test Exception', $command, [ 'response' => $response, 'transfer_stats' => [ 'starttransfer_time' => 15, 'appconnect_time' => 12 ] ] ), new AwsException( 'Test Exception', $command, [ 'response' => $response, 'transfer_stats' => [ 'starttransfer_time' => 20, 'appconnect_time' => 16 ] ] ) ]); $retryMW = new RetryMiddleware( RetryMiddleware::createDefaultDecider($retries = 3), [RetryMiddleware::class, 'exponentialDelay'], $handler, true ); try { $retryMW(new Command('SomeCommand'), new Request('GET', '')) ->wait(); $this->fail('This command should have produced an AwsException.'); } catch (AwsException $e) { $stats= $e->getTransferInfo(); $this->assertEquals( [ [ 'starttransfer_time' => 5, 'appconnect_time' => 4 ], [ 'starttransfer_time' => 10, 'appconnect_time' => 8 ], [ 'starttransfer_time' => 15, 'appconnect_time' => 12 ], [ 'starttransfer_time' => 20, 'appconnect_time' => 16 ], ], $stats['http'] ); } } public function testReportsHttpStatsForEachRequestEvenIfRetryStatsDisabled() { $handler = new MockHandler([ new Result(['@metadata' => [ 'statusCode' => '503', 'transferStats' => ['http' => [['foo' => 'bar']]] ]]), new Result(['@metadata' => [ 'statusCode' => '503', 'transferStats' => ['http' => [['baz' => 'quux']]] ]]), new Result(['@metadata' => [ 'statusCode' => '200', 'transferStats' => ['http' => [['fizz' => 'buzz']]] ]]), ]); $retryMW = new RetryMiddleware( RetryMiddleware::createDefaultDecider($retries = 3), [RetryMiddleware::class, 'exponentialDelay'], $handler, false ); $result = $retryMW(new Command('SomeCommand'), new Request('GET', '')) ->wait(); $httpStats = $result['@metadata']['transferStats']['http']; $this->assertCount(3, $httpStats); $this->assertSame([ ['foo' => 'bar'], ['baz' => 'quux'], ['fizz' => 'buzz'], ], $httpStats); } }