diff --git a/src/BigQuery/Connection/Rest.php b/src/BigQuery/Connection/Rest.php index dc5084580381..1118a56c0a59 100644 --- a/src/BigQuery/Connection/Rest.php +++ b/src/BigQuery/Connection/Rest.php @@ -254,8 +254,9 @@ private function resolveUploadOptions(array $args) unset($args['configuration']); $uploaderOptionKeys = [ - 'httpOptions', + 'restOptions', 'retries', + 'requestTimeout', 'metadata' ]; diff --git a/src/Core/GrpcRequestWrapper.php b/src/Core/GrpcRequestWrapper.php index f7c7e2f8f938..f8de0d63c22f 100644 --- a/src/Core/GrpcRequestWrapper.php +++ b/src/Core/GrpcRequestWrapper.php @@ -98,6 +98,8 @@ public function __construct(array $config = []) * @param array $options [optional] { * Request options. * + * @type float $requestTimeout Seconds to wait before timing out the + * request. **Defaults to** `60`. * @type int $retries Number of retries for a failed request. * **Defaults to** `3`. * @type array $grpcOptions gRPC specific configuration options. @@ -108,6 +110,7 @@ public function send(callable $request, array $args, array $options = []) { $retries = isset($options['retries']) ? $options['retries'] : $this->retries; $grpcOptions = isset($options['grpcOptions']) ? $options['grpcOptions'] : $this->grpcOptions; + $timeout = isset($options['requestTimeout']) ? $options['requestTimeout'] : $this->requestTimeout; $backoff = new ExponentialBackoff($retries, function (\Exception $ex) { $statusCode = $ex->getCode(); @@ -122,6 +125,10 @@ public function send(callable $request, array $args, array $options = []) $grpcOptions['retrySettings'] = new RetrySettings(null, null); } + if ($timeout && !array_key_exists('timeoutMs', $grpcOptions)) { + $grpcOptions['timeoutMs'] = $timeout * 1000; + } + $optionalArgs = &$args[count($args) - 1]; $optionalArgs += $grpcOptions; diff --git a/src/Core/GrpcTrait.php b/src/Core/GrpcTrait.php index a3a61fb4e661..4261233fb339 100644 --- a/src/Core/GrpcTrait.php +++ b/src/Core/GrpcTrait.php @@ -56,12 +56,13 @@ public function setRequestWrapper(GrpcRequestWrapper $requestWrapper) */ public function send(callable $request, array $args) { - $requestOptions = $args[count($args) - 1]; + $requestOptions = $this->pluckArray([ + 'grpcOptions', + 'retries', + 'requestTimeout' + ], $args[count($args) - 1]); - return $this->requestWrapper->send($request, $args, array_intersect_key($requestOptions, [ - 'grpcOptions' => null, - 'retries' => null - ])); + return $this->requestWrapper->send($request, $args, $requestOptions); } /** diff --git a/src/Core/RequestWrapper.php b/src/Core/RequestWrapper.php index 40d49cb4a980..0319747025b6 100644 --- a/src/Core/RequestWrapper.php +++ b/src/Core/RequestWrapper.php @@ -58,7 +58,7 @@ class RequestWrapper /** * @var array HTTP client specific configuration options. */ - private $httpOptions; + private $restOptions; /** * @var array @@ -96,7 +96,7 @@ class RequestWrapper * @type callable $authHttpHandler A handler used to deliver Psr7 * requests specifically for authentication. * @type callable $httpHandler A handler used to deliver Psr7 requests. - * @type array $httpOptions HTTP client specific configuration options. + * @type array $restOptions HTTP client specific configuration options. * @type bool $shouldSignRequest Whether to enable request signing. * } */ @@ -107,7 +107,7 @@ public function __construct(array $config = []) 'accessToken' => null, 'authHttpHandler' => null, 'httpHandler' => null, - 'httpOptions' => [], + 'restOptions' => [], 'shouldSignRequest' => true, 'componentVersion' => null ]; @@ -116,7 +116,7 @@ public function __construct(array $config = []) $this->accessToken = $config['accessToken']; $this->httpHandler = $config['httpHandler'] ?: HttpHandlerFactory::build(); $this->authHttpHandler = $config['authHttpHandler'] ?: $this->httpHandler; - $this->httpOptions = $config['httpOptions']; + $this->restOptions = $config['restOptions']; $this->shouldSignRequest = $config['shouldSignRequest']; } @@ -127,20 +127,27 @@ public function __construct(array $config = []) * @param array $options [optional] { * Request options. * + * @type float $requestTimeout Seconds to wait before timing out the + * request. **Defaults to** `0`. * @type int $retries Number of retries for a failed request. * **Defaults to** `3`. - * @type array $httpOptions HTTP client specific configuration options. + * @type array $restOptions HTTP client specific configuration options. * } * @return ResponseInterface */ public function send(RequestInterface $request, array $options = []) { $retries = isset($options['retries']) ? $options['retries'] : $this->retries; - $httpOptions = isset($options['httpOptions']) ? $options['httpOptions'] : $this->httpOptions; + $restOptions = isset($options['restOptions']) ? $options['restOptions'] : $this->restOptions; + $timeout = isset($options['requestTimeout']) ? $options['requestTimeout'] : $this->requestTimeout; $backoff = new ExponentialBackoff($retries, $this->getRetryFunction()); + if ($timeout && !array_key_exists('timeout', $restOptions)) { + $restOptions['timeout'] = $timeout; + } + try { - return $backoff->execute($this->httpHandler, [$this->applyHeaders($request), $httpOptions]); + return $backoff->execute($this->httpHandler, [$this->applyHeaders($request), $restOptions]); } catch (\Exception $ex) { throw $this->convertToGoogleException($ex); } diff --git a/src/Core/RequestWrapperTrait.php b/src/Core/RequestWrapperTrait.php index 95990f5613a5..2d4830ab6543 100644 --- a/src/Core/RequestWrapperTrait.php +++ b/src/Core/RequestWrapperTrait.php @@ -50,6 +50,12 @@ trait RequestWrapperTrait */ private $keyFile; + /** + * @var float Seconds to wait before timing out the request. **Defaults to** + * `0` with REST and `60` with gRPC. + */ + private $requestTimeout; + /** * @var int Number of retries for a failed request. **Defaults to** `3`. */ @@ -74,6 +80,8 @@ trait RequestWrapperTrait * @type array $keyFile The contents of the service account credentials * .json file retrieved from the Google Developer's Console. * Ex: `json_decode(file_get_contents($path), true)`. + * @type float $requestTimeout Seconds to wait before timing out the + * request. **Defaults to** `0` with REST and `60` with gRPC. * @type int $retries Number of retries for a failed request. * **Defaults to** `3`. * @type array $scopes Scopes to be used for the request. @@ -87,6 +95,7 @@ public function setCommonDefaults(array $config) 'authCacheOptions' => [], 'credentialsFetcher' => null, 'keyFile' => null, + 'requestTimeout' => null, 'retries' => null, 'scopes' => null ]; diff --git a/src/Core/RestTrait.php b/src/Core/RestTrait.php index 3bff556e07bb..663687a807e9 100644 --- a/src/Core/RestTrait.php +++ b/src/Core/RestTrait.php @@ -22,6 +22,7 @@ */ trait RestTrait { + use ArrayTrait; use JsonTrait; /** @@ -67,10 +68,11 @@ public function setRequestWrapper(RequestWrapper $requestWrapper) */ public function send($resource, $method, array $options = []) { - $requestOptions = array_intersect_key($options, [ - 'httpOptions' => null, - 'retries' => null - ]); + $requestOptions = $this->pluckArray([ + 'restOptions', + 'retries', + 'requestTimeout' + ], $options); return json_decode( $this->requestWrapper->send( diff --git a/src/Core/Upload/AbstractUploader.php b/src/Core/Upload/AbstractUploader.php index 05a3e9b5bcbf..ca8ce62479ee 100644 --- a/src/Core/Upload/AbstractUploader.php +++ b/src/Core/Upload/AbstractUploader.php @@ -78,7 +78,9 @@ abstract class AbstractUploader * @type array $metadata Metadata on the resource. * @type int $chunkSize Size of the chunks to send incrementally during * a resumable upload. Must be in multiples of 262144 bytes. - * @type array $httpOptions HTTP client specific configuration options. + * @type array $restOptions HTTP client specific configuration options. + * @type float $requestTimeout Seconds to wait before timing out the + * request. **Defaults to** `0`. * @type int $retries Number of retries for a failed request. * **Defaults to** `3`. * @type string $contentType Content type of the resource. @@ -96,8 +98,9 @@ public function __construct( $this->metadata = isset($options['metadata']) ? $options['metadata'] : []; $this->chunkSize = isset($options['chunkSize']) ? $options['chunkSize'] : null; $this->requestOptions = array_intersect_key($options, [ - 'httpOptions' => null, - 'retries' => null + 'restOptions' => null, + 'retries' => null, + 'requestTimeout' => null ]); $this->contentType = isset($options['contentType']) diff --git a/src/Storage/Connection/Rest.php b/src/Storage/Connection/Rest.php index f4751f010156..11c6f843ef64 100644 --- a/src/Storage/Connection/Rest.php +++ b/src/Storage/Connection/Rest.php @@ -208,7 +208,7 @@ public function downloadObject(array $args = []) ]; $requestOptions = array_intersect_key($args, [ - 'httpOptions' => null, + 'restOptions' => null, 'retries' => null ]); @@ -297,8 +297,9 @@ private function resolveUploadOptions(array $args) : Psr7\mimetype_from_filename($args['metadata']['name']); $uploaderOptionKeys = [ - 'httpOptions', + 'restOptions', 'retries', + 'requestTimeout', 'chunkSize', 'contentType', 'metadata' diff --git a/src/Storage/EncryptionTrait.php b/src/Storage/EncryptionTrait.php index 694145782850..719079336e30 100644 --- a/src/Storage/EncryptionTrait.php +++ b/src/Storage/EncryptionTrait.php @@ -70,10 +70,10 @@ public function formatEncryptionHeaders(array $options) + $this->buildHeaders($destinationKey, $destinationKeySHA256, false); if (!empty($encryptionHeaders)) { - if (isset($options['httpOptions']['headers'])) { - $options['httpOptions']['headers'] += $encryptionHeaders; + if (isset($options['restOptions']['headers'])) { + $options['restOptions']['headers'] += $encryptionHeaders; } else { - $options['httpOptions']['headers'] = $encryptionHeaders; + $options['restOptions']['headers'] = $encryptionHeaders; } } diff --git a/src/Storage/StorageObject.php b/src/Storage/StorageObject.php index 35242e035186..3e53882ba832 100644 --- a/src/Storage/StorageObject.php +++ b/src/Storage/StorageObject.php @@ -513,7 +513,7 @@ public function rename($name, array $options = []) $this->delete( array_intersect_key($options, [ - 'httpOptions' => null, + 'restOptions' => null, 'retries' => null ]) ); diff --git a/src/Storage/StreamWrapper.php b/src/Storage/StreamWrapper.php index b393fa5bb1a3..5f74c23b371a 100644 --- a/src/Storage/StreamWrapper.php +++ b/src/Storage/StreamWrapper.php @@ -166,7 +166,7 @@ public function stream_open($path, $mode, $flags, &$openedPath) } elseif ($mode == 'r') { try { // Lazy read from the source - $options['httpOptions']['stream'] = true; + $options['restOptions']['stream'] = true; $this->stream = new ReadStream( $this->bucket->object($this->file)->downloadAsStream($options) ); diff --git a/tests/unit/Core/GrpcRequestWrapperTest.php b/tests/unit/Core/GrpcRequestWrapperTest.php index ff171e827864..11243cb17ab5 100644 --- a/tests/unit/Core/GrpcRequestWrapperTest.php +++ b/tests/unit/Core/GrpcRequestWrapperTest.php @@ -45,12 +45,17 @@ public function setUp() public function testSuccessfullySendsRequest($response, $expectedMessage) { $requestWrapper = new GrpcRequestWrapper(); + $requestOptions = [ + 'requestTimeout' => 3.5 + ]; $actualResponse = $requestWrapper->send( - function ($test) use ($response) { + function ($test, $options) use ($response, $requestOptions) { + $this->assertEquals($requestOptions['requestTimeout'] * 1000, $options['timeoutMs']); return $response; }, - ['test', []] + ['test', []], + $requestOptions ); $this->assertEquals($expectedMessage, $actualResponse); diff --git a/tests/unit/Core/GrpcTraitTest.php b/tests/unit/Core/GrpcTraitTest.php index 642b4aed9085..5bc792fa3c21 100644 --- a/tests/unit/Core/GrpcTraitTest.php +++ b/tests/unit/Core/GrpcTraitTest.php @@ -62,6 +62,28 @@ public function testSendsRequest() $this->assertEquals($message, $actualResponse); } + public function testSendsRequestWithOptions() + { + $options = [ + 'requestTimeout' => 3.5, + 'grpcOptions' => ['timeoutMs' => 100], + 'retries' => 0 + ]; + $message = ['successful' => 'message']; + $this->requestWrapper->send( + Argument::type('callable'), + Argument::type('array'), + $options + )->willReturn($message); + + $this->implementation->setRequestWrapper($this->requestWrapper->reveal()); + $actualResponse = $this->implementation->send(function () { + return true; + }, [$options]); + + $this->assertEquals($message, $actualResponse); + } + public function testGetsGaxConfig() { $version = '1.0.0'; diff --git a/tests/unit/Core/RequestWrapperTest.php b/tests/unit/Core/RequestWrapperTest.php index 00ca37d1b7e7..517e5aa8a36f 100644 --- a/tests/unit/Core/RequestWrapperTest.php +++ b/tests/unit/Core/RequestWrapperTest.php @@ -37,16 +37,23 @@ public function testSuccessfullySendsRequest() { $expectedBody = 'responseBody'; $response = new Response(200, [], $expectedBody); + $requestOptions = [ + 'restOptions' => ['debug' => true], + 'requestTimeout' => 3.5 + ]; $requestWrapper = new RequestWrapper([ 'accessToken' => 'abc', - 'httpHandler' => function ($request, $options = []) use ($response) { + 'httpHandler' => function ($request, $options = []) use ($response, $requestOptions) { + $this->assertEquals($requestOptions['restOptions']['debug'], $options['debug']); + $this->assertEquals($requestOptions['requestTimeout'], $options['timeout']); return $response; } ]); $actualResponse = $requestWrapper->send( - new Request('GET', 'http://www.test.com') + new Request('GET', 'http://www.test.com'), + $requestOptions ); $this->assertEquals($expectedBody, (string) $actualResponse->getBody()); diff --git a/tests/unit/Core/RestTraitTest.php b/tests/unit/Core/RestTraitTest.php index 1be4b20e42f6..b40db24868d7 100644 --- a/tests/unit/Core/RestTraitTest.php +++ b/tests/unit/Core/RestTraitTest.php @@ -57,17 +57,18 @@ public function testSendsRequest() public function testSendsRequestWithOptions() { - $httpOptions = [ - 'httpOptions' => ['debug' => true], - 'retries' => 5 + $restOptions = [ + 'restOptions' => ['debug' => true], + 'retries' => 5, + 'requestTimeout' => 3.5 ]; $responseBody = '{"whatAWonderful": "response"}'; - $this->requestWrapper->send(Argument::any(), $httpOptions) + $this->requestWrapper->send(Argument::any(), $restOptions) ->willReturn(new Response(200, [], $responseBody)); $this->implementation->setRequestBuilder($this->requestBuilder->reveal()); $this->implementation->setRequestWrapper($this->requestWrapper->reveal()); - $actualResponse = $this->implementation->send('resource', 'method', $httpOptions); + $actualResponse = $this->implementation->send('resource', 'method', $restOptions); $this->assertEquals(json_decode($responseBody, true), $actualResponse); } diff --git a/tests/unit/Storage/Connection/RestTest.php b/tests/unit/Storage/Connection/RestTest.php index 4d7ade35bf39..d3c1813a93f4 100644 --- a/tests/unit/Storage/Connection/RestTest.php +++ b/tests/unit/Storage/Connection/RestTest.php @@ -122,7 +122,7 @@ function ($args) use (&$actualRequest, $response) { 'bucket' => 'bigbucket', 'object' => 'myfile.txt', 'generation' => 100, - 'httpOptions' => ['debug' => true], + 'restOptions' => ['debug' => true], 'retries' => 0 ]); diff --git a/tests/unit/Storage/EncryptionTraitTest.php b/tests/unit/Storage/EncryptionTraitTest.php index 30cbf1d8f833..db09f49355f8 100644 --- a/tests/unit/Storage/EncryptionTraitTest.php +++ b/tests/unit/Storage/EncryptionTraitTest.php @@ -52,7 +52,7 @@ public function encryptionProvider() return [ [ [ - 'httpOptions' => [ + 'restOptions' => [ 'headers' => $this->getEncryptionHeaders($key, $hash) ] ], @@ -63,7 +63,7 @@ public function encryptionProvider() ], [ [ - 'httpOptions' => [ + 'restOptions' => [ 'headers' => $this->getEncryptionHeaders($key, $hash) ] ], @@ -73,7 +73,7 @@ public function encryptionProvider() ], [ [ - 'httpOptions' => [ + 'restOptions' => [ 'headers' => array_merge( $this->getEncryptionHeaders($destinationKey, $destinationHash), $this->getCopySourceEncryptionHeaders($key, $hash) @@ -90,14 +90,14 @@ public function encryptionProvider() ], [ [ - 'httpOptions' => [ + 'restOptions' => [ 'headers' => $this->getEncryptionHeaders($key, $hash) + ['hey' => 'dont clobber me'] ] ], [ 'encryptionKey' => $key, 'encryptionKeySHA256' => $hash, - 'httpOptions' => [ + 'restOptions' => [ 'headers' => [ 'hey' => 'dont clobber me' ] diff --git a/tests/unit/Storage/StorageObjectTest.php b/tests/unit/Storage/StorageObjectTest.php index ebe0e9d80aab..36063b4f508b 100644 --- a/tests/unit/Storage/StorageObjectTest.php +++ b/tests/unit/Storage/StorageObjectTest.php @@ -94,7 +94,7 @@ public function testCopyObjectWithDefaultName() 'destinationBucket' => $destinationBucket, 'destinationObject' => $objectName, 'destinationPredefinedAcl' => $acl, - 'httpOptions' => [ + 'restOptions' => [ 'headers' => [ 'x-goog-encryption-algorithm' => 'AES256', 'x-goog-encryption-key' => $key, @@ -176,7 +176,7 @@ public function testRewriteObjectWithDefaultName() 'destinationBucket' => $destinationBucket, 'destinationObject' => $objectName, 'destinationPredefinedAcl' => $acl, - 'httpOptions' => [ + 'restOptions' => [ 'headers' => [ 'x-goog-copy-source-encryption-algorithm' => 'AES256', 'x-goog-copy-source-encryption-key' => $key, @@ -280,7 +280,7 @@ public function testRenamesObject() 'destinationBucket' => $sourceBucket, 'destinationObject' => $newObjectName, 'destinationPredefinedAcl' => $acl, - 'httpOptions' => [ + 'restOptions' => [ 'headers' => [ 'x-goog-encryption-algorithm' => 'AES256', 'x-goog-encryption-key' => $key, @@ -319,7 +319,7 @@ public function testDownloadsAsString() 'bucket' => $bucket, 'object' => $object, 'generation' => null, - 'httpOptions' => [ + 'restOptions' => [ 'headers' => [ 'x-goog-encryption-algorithm' => 'AES256', 'x-goog-encryption-key' => $key, @@ -349,7 +349,7 @@ public function testDownloadsToFile() 'bucket' => $bucket, 'object' => $object, 'generation' => null, - 'httpOptions' => [ + 'restOptions' => [ 'headers' => [ 'x-goog-encryption-algorithm' => 'AES256', 'x-goog-encryption-key' => $key, @@ -400,7 +400,7 @@ public function testGetBodyWithExtraOptions() 'bucket' => $bucket, 'object' => $object, 'generation' => null, - 'httpOptions' => [ + 'restOptions' => [ 'headers' => [ 'x-goog-encryption-algorithm' => 'AES256', 'x-goog-encryption-key' => $key, @@ -450,7 +450,7 @@ public function testGetsInfoWithReload() 'bucket' => $bucket, 'object' => $object, 'generation' => null, - 'httpOptions' => [ + 'restOptions' => [ 'headers' => [ 'x-goog-encryption-algorithm' => 'AES256', 'x-goog-encryption-key' => $key,