diff --git a/src/Middleware/RetryMiddleware.php b/src/Middleware/RetryMiddleware.php index 4a791cd71..8b95c5aa2 100644 --- a/src/Middleware/RetryMiddleware.php +++ b/src/Middleware/RetryMiddleware.php @@ -34,6 +34,7 @@ use Google\ApiCore\ApiException; use Google\ApiCore\ApiStatus; use Google\ApiCore\Call; +use Google\ApiCore\Retrier; use Google\ApiCore\RetrySettings; use GuzzleHttp\Promise\PromiseInterface; @@ -44,15 +45,14 @@ class RetryMiddleware implements MiddlewareInterface { /** @var callable */ private $nextHandler; - private RetrySettings $retrySettings; - private ?float $deadlineMs; + private Retrier $retrier; - /* - * The number of retries that have already been attempted. - * The original API call will have $retryAttempts set to 0. + /** + * @param callable $nextHandler + * @param RetrySettings $retrySettings + * @param int $deadlineMs TODO: Deprecate if possible. + * @param int $retryAttempts TODO: Deprecate if possible. */ - private int $retryAttempts; - public function __construct( callable $nextHandler, RetrySettings $retrySettings, @@ -60,9 +60,11 @@ public function __construct( $retryAttempts = 0 ) { $this->nextHandler = $nextHandler; - $this->retrySettings = $retrySettings; - $this->deadlineMs = $deadlineMs; - $this->retryAttempts = $retryAttempts; + $retrySettings = $retrySettings->with([ + 'deadlineMillis' => $deadlineMs, + 'retryAttempts' => $retryAttempts + ]); + $this->retrier = new Retrier($retrySettings); } /** @@ -74,90 +76,63 @@ public function __construct( public function __invoke(Call $call, array $options) { $nextHandler = $this->nextHandler; + $retrySettings = $this->retrier->getRetrySettings(); if (!isset($options['timeoutMillis'])) { // default to "noRetriesRpcTimeoutMillis" when retries are disabled, otherwise use "initialRpcTimeoutMillis" - if (!$this->retrySettings->retriesEnabled() && $this->retrySettings->getNoRetriesRpcTimeoutMillis() > 0) { - $options['timeoutMillis'] = $this->retrySettings->getNoRetriesRpcTimeoutMillis(); - } elseif ($this->retrySettings->getInitialRpcTimeoutMillis() > 0) { - $options['timeoutMillis'] = $this->retrySettings->getInitialRpcTimeoutMillis(); + if (!$retrySettings->retriesEnabled() && $retrySettings->getNoRetriesRpcTimeoutMillis() > 0) { + $options['timeoutMillis'] = $retrySettings->getNoRetriesRpcTimeoutMillis(); + } elseif ($retrySettings->getInitialRpcTimeoutMillis() > 0) { + $options['timeoutMillis'] = $retrySettings->getInitialRpcTimeoutMillis(); } } // Call the handler immediately if retry settings are disabled. - if (!$this->retrySettings->retriesEnabled()) { + if (!$retrySettings->retriesEnabled()) { return $nextHandler($call, $options); } - return $nextHandler($call, $options)->then(null, function ($e) use ($call, $options) { - $retryFunction = $this->getRetryFunction(); - - // If the number of retries has surpassed the max allowed retries - // then throw the exception as we normally would. - // If the maxRetries is set to 0, then we don't check this condition. - if (0 !== $this->retrySettings->getMaxRetries() - && $this->retryAttempts >= $this->retrySettings->getMaxRetries() - ) { - throw $e; - } - // If the retry function returns false then throw the - // exception as we normally would. - if (!$retryFunction($e, $options)) { - throw $e; + return $nextHandler($call, $options)->then(null, function ($exception) use ($call, $options) { + if (!$this->retrier->isRetryable($exception)) { + throw $exception; } // Retry function returned true, so we attempt another retry - return $this->retry($call, $options, $e->getStatus()); + return $this->retry($call, $options, $exception); }); } /** * @param Call $call * @param array $options - * @param string $status + * @param ApiException $exception * * @return PromiseInterface * @throws ApiException */ - private function retry(Call $call, array $options, string $status) + private function retry(Call $call, array $options, ApiException $exception) { - $delayMult = $this->retrySettings->getRetryDelayMultiplier(); - $maxDelayMs = $this->retrySettings->getMaxRetryDelayMillis(); - $timeoutMult = $this->retrySettings->getRpcTimeoutMultiplier(); - $maxTimeoutMs = $this->retrySettings->getMaxRpcTimeoutMillis(); - $totalTimeoutMs = $this->retrySettings->getTotalTimeoutMillis(); - - $delayMs = $this->retrySettings->getInitialRetryDelayMillis(); - $timeoutMs = $options['timeoutMillis']; - $currentTimeMs = $this->getCurrentTimeMs(); - $deadlineMs = $this->deadlineMs ?: $currentTimeMs + $totalTimeoutMs; - - if ($currentTimeMs >= $deadlineMs) { - throw new ApiException( - 'Retry total timeout exceeded.', - \Google\Rpc\Code::DEADLINE_EXCEEDED, - ApiStatus::DEADLINE_EXCEEDED - ); - } - - $delayMs = min($delayMs * $delayMult, $maxDelayMs); - $timeoutMs = (int) min( - $timeoutMs * $timeoutMult, - $maxTimeoutMs, - $deadlineMs - $this->getCurrentTimeMs() + $currentTime = $this->retrier->getCurrentTimeMillis(); + $this->retrier->checkDeadlineExceeded($currentTime); + $deadlineMillis = $this->retrier->calculateRetryDeadlineMillis($currentTime); + $retrySettings = $this->retrier->getRetrySettings()->with([ + 'retryAttempts' => $this->retrier->getRetrySettings()->getRetryAttempts() + 1, + 'deadlineMillis' => $deadlineMillis + ]); + $timeout = $this->calculateTimeoutMillis( + $retrySettings, + $options['timeoutMillis'] ); $nextHandler = new RetryMiddleware( $this->nextHandler, - $this->retrySettings->with([ - 'initialRetryDelayMillis' => $delayMs, - ]), - $deadlineMs, - $this->retryAttempts + 1 + $retrySettings, + $retrySettings->getDeadlineMillis(), + $retrySettings->getRetryAttempts() ); // Set the timeout for the call - $options['timeoutMillis'] = $timeoutMs; + $options['timeoutMillis'] = $timeout; return $nextHandler( $call, @@ -165,30 +140,27 @@ private function retry(Call $call, array $options, string $status) ); } - protected function getCurrentTimeMs() - { - return microtime(true) * 1000.0; - } - /** - * This is the default retry behaviour. + * Calculates the timeout for the call. + * + * @param RetrySettings $retrySettings + * @param int $timeoutMillis + * + * @return int */ - private function getRetryFunction() - { - return $this->retrySettings->getRetryFunction() ?? - function (\Throwable $e, array $options): bool { - // This is the default retry behaviour, i.e. we don't retry an ApiException - // and for other exception types, we only retry when the error code is in - // the list of retryable error codes. - if (!$e instanceof ApiException) { - return false; - } - - if (!in_array($e->getStatus(), $this->retrySettings->getRetryableCodes())) { - return false; - } - - return true; - }; + private function calculateTimeoutMillis( + RetrySettings $retrySettings, + int $timeoutMillis + ) { + $maxTimeoutMillis = $retrySettings->getMaxRpcTimeoutMillis(); + $timeoutMult = $retrySettings->getRpcTimeoutMultiplier(); + $deadlineMillis = $retrySettings->getDeadlineMillis(); + $currentTime = $this->retrier->getCurrentTimeMillis(); + + return (int) min( + $timeoutMillis * $timeoutMult, + $maxTimeoutMillis, + $deadlineMillis - $currentTime + ); } } diff --git a/src/Retrier.php b/src/Retrier.php new file mode 100644 index 000000000..d169d84a2 --- /dev/null +++ b/src/Retrier.php @@ -0,0 +1,172 @@ +retrySettings = $retrySettings; + } + + /** + * @return RetrySettings + */ + public function getRetrySettings() + { + return $this->retrySettings; + } + + /** + * Execute the callable with the retry logic. + * + * @param callable $call + * @param array $options + * + * @return mixed + * @throws \Exception + */ + public function execute(callable $call, array $options) + { + try { + return call_user_func_array($call, $options); + } catch (\Exception $exception) { + if (!$this->isRetryable($exception)) { + throw $exception; + } + // Check if the deadline has already been exceeded. + $this->checkDeadlineExceeded($this->getCurrentTimeMillis()); + $this->retrySettings = $this->retrySettings->with([ + 'retryAttempts' => $this->retrySettings->getRetryAttempts() + 1 + ]); + $retryDelayMillis = $this->retrySettings->getRetryDelayMillis($exception); + // Millis to Micro conversion. + usleep($retryDelayMillis * 1000); + } + $this->execute($call, $options); + } + + /** + * @param \Exception $exception + * + * @return bool + */ + public function isRetryable(\Exception $exception, array $options = []) + { + $retryFunction = $this->getRetryFunction(); + + // Don't retry if the number of retries has surpassed the max allowed retries. + // If the maxRetries is set to 0, then we don't check this condition. + if (0 !== $this->retrySettings->getMaxRetries() + && $this->retrySettings->getRetryAttempts() >= $this->retrySettings->getMaxRetries() + ) { + return false; + } + // Don't retry if the retry function returns false. + return $retryFunction($exception, $options); + } + + /** + * @param float $currentTimeMillis + * + * @return void + * @throws ApiException + */ + public function checkDeadlineExceeded(float $currentTimeMillis) + { + $deadlineMillis = $this->calculateRetryDeadlineMillis($currentTimeMillis); + + if ($currentTimeMillis >= $deadlineMillis) { + throw new ApiException( + 'Retry total timeout exceeded.', + \Google\Rpc\Code::DEADLINE_EXCEEDED, + ApiStatus::DEADLINE_EXCEEDED + ); + } + } + + /** + * @param float $currentTimeMillis + * + * @return float + */ + public function calculateRetryDeadlineMillis(float $currentTimeMillis) + { + if ($this->retrySettings->getDeadlineMillis()) { + return $this->retrySettings->getDeadlineMillis(); + } + $totalTimeoutMillis = $this->retrySettings->getTotalTimeoutMillis(); + return $currentTimeMillis + $totalTimeoutMillis; + } + + /** + * @return float + */ + public function getCurrentTimeMillis() + { + return microtime(true) * 1000.0; + } + + /** + * This is the default retry behaviour. + * + * @return callable + */ + private function getRetryFunction() + { + return $this->retrySettings->getRetryFunction() ?? + function (\Throwable $e, array $options = []): bool { + // This is the default retry behaviour, i.e. we don't retry an ApiException + // and for other exception types, we only retry when the error code is in + // the list of retryable error codes. + if (!$e instanceof ApiException) { + return false; + } + + if (!in_array($e->getStatus(), $this->retrySettings->getRetryableCodes())) { + return false; + } + + return true; + }; + } +} diff --git a/src/RetrySettings.php b/src/RetrySettings.php index 94bb6f189..2ffae3a00 100644 --- a/src/RetrySettings.php +++ b/src/RetrySettings.php @@ -208,9 +208,7 @@ class RetrySettings const DEFAULT_MAX_RETRIES = 0; private $retriesEnabled; - private $retryableCodes; - private $initialRetryDelayMillis; private $retryDelayMultiplier; private $maxRetryDelayMillis; @@ -235,13 +233,32 @@ class RetrySettings */ private ?Closure $retryFunction; + /** + * When set, this function will be used to evaluate the dealy between + * retries. This overrides $retryableCodes, $initialRetryDelayMillis, + * and $retryDelayMultiplier. + * The callable will have the following signature: + * function (int attempts, Exception $e): int (delay in milliseconds) + */ + private ?Closure $retryDelayFunction; + + /** + * The deadline in milliseconds. + */ + private ?float $deadlineMillis; + + /* + * The number of retries that have already been attempted. + * The original API call will have $retryAttempts set to 0. + */ + private int $retryAttempts; + /** * Constructs an instance. * * @param array $settings { - * Required. Settings for configuring the retry behavior. All parameters are required except - * $retriesEnabled and $noRetriesRpcTimeoutMillis, which are optional and have defaults - * determined based on the other settings provided. + * Required. Settings for configuring the retry behavior. All parameters are required unless + * otherwise noted. * * @type bool $retriesEnabled Optional. Enables retries. If not specified, the value is * determined using the $retryableCodes setting. If $retryableCodes is empty, @@ -249,11 +266,11 @@ class RetrySettings * @type int $noRetriesRpcTimeoutMillis Optional. The timeout of the rpc call to be used * if $retriesEnabled is false, in milliseconds. It not specified, the value * of $initialRpcTimeoutMillis is used. - * @type array $retryableCodes The Status codes that are retryable. Each status should be + * @type array $retryableCodes Optional. The Status codes that are retryable. Each status should be * either one of the string constants defined on {@see \Google\ApiCore\ApiStatus} * or an integer constant defined on {@see \Google\Rpc\Code}. - * @type int $initialRetryDelayMillis The initial delay of retry in milliseconds. - * @type int $retryDelayMultiplier The exponential multiplier of retry delay. + * @type int $initialRetryDelayMillis Optional. The initial delay of retry in milliseconds. + * @type int $retryDelayMultiplier Optional. The exponential multiplier of retry delay. * @type int $maxRetryDelayMillis The max delay of retry in milliseconds. * @type int $initialRpcTimeoutMillis The initial timeout of rpc call in milliseconds. * @type int $rpcTimeoutMultiplier The exponential multiplier of rpc timeout. @@ -264,37 +281,72 @@ class RetrySettings * This option is experimental. * @type callable $retryFunction This function will be used to decide if we should retry or not. * Callable signature: `function (Exception $e, array $options): bool` - * This option is experimental. + * @type callable $retryDelayFunction Optional. This function will be used to decide the delay between retries. + * @type float $deadlineMillis Optional. The deadline of the rpc call in milliseconds. + * @type int $retryAttempts Optional. The number of retries that have already been attempted. + * The first call will have $retryAttempts set to 0. * } */ public function __construct(array $settings) { - $this->validateNotNull($settings, [ - 'initialRetryDelayMillis', - 'retryDelayMultiplier', - 'maxRetryDelayMillis', + $validateMutuallyExclusiveDelayCalcs = [ + [ + 'initialRetryDelayMillis', + 'retryDelayMultiplier' + ], + [ + 'retryDelayFunction', + ] + ]; + $validateMutuallyExclusiveRetryablityDecider = [ + [ + 'retryableCodes' + ], + [ + 'retryFunction' + ] + ]; + $validateNotNullArgs = [ + 'totalTimeoutMillis', + 'maxRetryDelayMillis' + ]; + // @TODO: Check with team on making this optional + // RetrySettings is going to be used for block level retries as well. + $optional = [ 'initialRpcTimeoutMillis', 'rpcTimeoutMultiplier', 'maxRpcTimeoutMillis', - 'totalTimeoutMillis', - 'retryableCodes' - ]); - $this->initialRetryDelayMillis = $settings['initialRetryDelayMillis']; - $this->retryDelayMultiplier = $settings['retryDelayMultiplier']; + ]; + $this->validateAllKeysFromOneOf( + $settings, + $validateMutuallyExclusiveDelayCalcs[0], + $validateMutuallyExclusiveDelayCalcs[1] + ); + $this->validateAllKeysFromOneOf( + $settings, + $validateMutuallyExclusiveRetryablityDecider[0], + $validateMutuallyExclusiveRetryablityDecider[1] + ); + $this->validateNotNull($settings, $validateNotNullArgs); + $this->initialRetryDelayMillis = $settings['initialRetryDelayMillis'] ?? null; + $this->retryDelayMultiplier = $settings['retryDelayMultiplier'] ?? null; $this->maxRetryDelayMillis = $settings['maxRetryDelayMillis']; $this->initialRpcTimeoutMillis = $settings['initialRpcTimeoutMillis']; $this->rpcTimeoutMultiplier = $settings['rpcTimeoutMultiplier']; $this->maxRpcTimeoutMillis = $settings['maxRpcTimeoutMillis']; $this->totalTimeoutMillis = $settings['totalTimeoutMillis']; - $this->retryableCodes = $settings['retryableCodes']; + $this->retryableCodes = $settings['retryableCodes'] ?? null; + $this->retryFunction = $settings['retryFunction'] ?? null; $this->retriesEnabled = array_key_exists('retriesEnabled', $settings) ? $settings['retriesEnabled'] - : (count($this->retryableCodes) > 0); + : (count($this->retryableCodes) > 0 || isset($this->retryFunction)); $this->noRetriesRpcTimeoutMillis = array_key_exists('noRetriesRpcTimeoutMillis', $settings) ? $settings['noRetriesRpcTimeoutMillis'] : $this->initialRpcTimeoutMillis; $this->maxRetries = $settings['maxRetries'] ?? self::DEFAULT_MAX_RETRIES; - $this->retryFunction = $settings['retryFunction'] ?? null; + $this->retryDelayFunction = $settings['retryDelayFunction'] ?? null; + $this->deadlineMillis = $settings['deadlineMillis'] ?? null; + $this->retryAttempts = $settings['retryAttempts'] ?? 0; } /** @@ -376,7 +428,11 @@ public static function constructDefault() 'totalTimeoutMillis' => 600000, 'retryableCodes' => [], 'maxRetries' => self::DEFAULT_MAX_RETRIES, - 'retryFunction' => null]); + 'retryFunction' => null, + 'retryDelayFunction' => null, + 'deadlineMillis' => null, + 'retryAttempts' => 0, + ]); } /** @@ -405,6 +461,9 @@ public function with(array $settings) 'noRetriesRpcTimeoutMillis' => $this->getNoRetriesRpcTimeoutMillis(), 'maxRetries' => $this->getMaxRetries(), 'retryFunction' => $this->getRetryFunction(), + 'retryDelayFunction' => $this->getRetryDelayFunction(), + 'deadlineMillis' => $this->getDeadlineMillis(), + 'retryAttempts' => $this->getRetryAttempts(), ]; return new RetrySettings($settings + $existingSettings); } @@ -535,6 +594,48 @@ public function getRetryFunction() return $this->retryFunction; } + /** + * @experimental + */ + public function getRetryDelayMillis(\Exception $exception) + { + if ($this->retryDelayFunction) { + return min( + call_user_func($this->retryDelayFunction, $this->retryAttempts, $exception), + $this->maxRetryDelayMillis + ); + } + + return min( + $this->initialRetryDelayMillis * ($this->retryDelayMultiplier ** ($this->retryAttempts - 1)), + $this->maxRetryDelayMillis + ); + } + + /** + * @experimental + */ + public function getDeadlineMillis() + { + return $this->deadlineMillis; + } + + /** + * @experimental + */ + public function getRetryAttempts() + { + return $this->retryAttempts; + } + + /** + * @experimental + */ + public function getRetryDelayFunction() + { + return $this->retryDelayFunction; + } + private static function convertArrayFromSnakeCase(array $settings) { $camelCaseSettings = []; diff --git a/src/ValidationTrait.php b/src/ValidationTrait.php index 5e554e3fb..07789f4a4 100644 --- a/src/ValidationTrait.php +++ b/src/ValidationTrait.php @@ -56,6 +56,26 @@ public static function validateNotNull(array $arr, array $requiredKeys) return self::validateImpl($arr, $requiredKeys, false); } + /** + * @param array $arr Associative array + * @param array $requiredKeys1 List of keys to check for in $arr + * @param array $requiredKeys2 List of keys to check for in $arr + * @return array Returns $arr for fluent use + * @throws ValidationException + */ + public static function validateAllKeysFromOneOf(array $arr, array $requiredKeys1, array $requiredKeys2) + { + // Check if all keys in requiredKeys1 are present in arr + $allKeys1Present = count(array_diff($requiredKeys1, array_keys($arr))) === 0; + // Check if all keys in requiredKeys2 are present in arr + $allKeys2Present = count(array_diff($requiredKeys2, array_keys($arr))) === 0; + // Return $arr if either all keys from requiredKeys1 or requiredKeys2 are present + if ($allKeys1Present || $allKeys2Present) { + return $arr; + } + throw new ValidationException("Missing required arguments"); + } + private static function validateImpl($arr, $requiredKeys, $allowNull) { foreach ($requiredKeys as $requiredKey) { diff --git a/tests/Tests/Unit/GapicClientTraitTest.php b/tests/Tests/Unit/GapicClientTraitTest.php index ef2861ad8..0dd8067ed 100644 --- a/tests/Tests/Unit/GapicClientTraitTest.php +++ b/tests/Tests/Unit/GapicClientTraitTest.php @@ -119,12 +119,7 @@ public function testHeadersOverwriteBehavior() ->willReturn($this->prophesize(PromiseInterface::class)->reveal()); $client = new StubGapicClient(); $client->set('agentHeader', $header); - $client->set('retrySettings', [ - 'method' => $this->getMockBuilder(RetrySettings::class) - ->disableOriginalConstructor() - ->getMock() - ] - ); + $client->set('retrySettings', ['method' => $this->getRetrySettingsMock()]); $client->set('transport', $transport); $client->set('credentialsWrapper', $credentialsWrapper); $client->set('descriptors', ['method' => $unaryDescriptors]); @@ -184,12 +179,7 @@ public function testVersionedHeadersOverwriteBehavion() ->willReturn($this->prophesize(PromiseInterface::class)->reveal()); $client = new VersionedStubGapicClient(); $client->set('agentHeader', $header); - $client->set('retrySettings', [ - 'method' => $this->getMockBuilder(RetrySettings::class) - ->disableOriginalConstructor() - ->getMock() - ] - ); + $client->set('retrySettings', ['method' => $this->getRetrySettingsMock()]); $client->set('transport', $transport); $client->set('credentialsWrapper', $credentialsWrapper); $client->set('descriptors', ['method' => $unaryDescriptors]); @@ -246,9 +236,6 @@ public function testConfigureCallConstructionOptionsAcceptsRetryObjectOrArray() public function testStartOperationsCall() { $header = AgentHeader::buildAgentHeader([]); - $retrySettings = $this->getMockBuilder(RetrySettings::class) - ->disableOriginalConstructor() - ->getMock(); $longRunningDescriptors = [ 'longRunning' => [ @@ -270,7 +257,7 @@ public function testStartOperationsCall() $client->set('transport', $transport); $client->set('credentialsWrapper', $credentialsWrapper); $client->set('agentHeader', $header); - $client->set('retrySettings', ['method' => $retrySettings]); + $client->set('retrySettings', ['method' => $this->getRetrySettingsMock()]); $client->set('descriptors', ['method' => $longRunningDescriptors]); $message = new MockRequest(); $operationsClient = $this->getMockBuilder(OperationsClient::class) @@ -297,9 +284,6 @@ public function testStartOperationsCall() public function testStartApiCallOperation() { $header = AgentHeader::buildAgentHeader([]); - $retrySettings = $this->getMockBuilder(RetrySettings::class) - ->disableOriginalConstructor() - ->getMock(); $longRunningDescriptors = [ 'callType' => Call::LONGRUNNING_CALL, @@ -322,7 +306,7 @@ public function testStartApiCallOperation() $client->set('transport', $transport); $client->set('credentialsWrapper', $credentialsWrapper); $client->set('agentHeader', $header); - $client->set('retrySettings', ['method' => $retrySettings]); + $client->set('retrySettings', ['method' => $this->getRetrySettingsMock()]); $client->set('descriptors', ['method' => $longRunningDescriptors]); $operationsClient = $this->getMockBuilder(OperationsClient::class) ->disableOriginalConstructor() @@ -348,9 +332,6 @@ public function testStartApiCallOperation() public function testStartApiCallCustomOperation() { $header = AgentHeader::buildAgentHeader([]); - $retrySettings = $this->getMockBuilder(RetrySettings::class) - ->disableOriginalConstructor() - ->getMock(); $longRunningDescriptors = [ 'callType' => Call::LONGRUNNING_CALL, @@ -374,7 +355,7 @@ public function testStartApiCallCustomOperation() $client->set('transport', $transport); $client->set('credentialsWrapper', $credentialsWrapper); $client->set('agentHeader', $header); - $client->set('retrySettings', ['method' => $retrySettings]); + $client->set('retrySettings', ['method' => $this->getRetrySettingsMock()]); $client->set('descriptors', ['method' => $longRunningDescriptors]); $operationsClient = $this->getMockBuilder(OperationsClient::class) ->disableOriginalConstructor() @@ -459,9 +440,6 @@ public function startApiCallExceptions() public function testStartApiCallUnary() { $header = AgentHeader::buildAgentHeader([]); - $retrySettings = $this->getMockBuilder(RetrySettings::class) - ->disableOriginalConstructor() - ->getMock(); $unaryDescriptors = [ 'callType' => Call::UNARY_CALL, 'responseType' => 'Google\Longrunning\Operation', @@ -483,7 +461,7 @@ public function testStartApiCallUnary() $client->set('transport', $transport); $client->set('credentialsWrapper', $credentialsWrapper); $client->set('agentHeader', $header); - $client->set('retrySettings', ['method' => $retrySettings]); + $client->set('retrySettings', ['method' => $this->getRetrySettingsMock()]); $client->set('descriptors', ['method' => $unaryDescriptors]); $request = new MockRequest(); @@ -496,9 +474,6 @@ public function testStartApiCallUnary() public function testStartApiCallPaged() { $header = AgentHeader::buildAgentHeader([]); - $retrySettings = $this->getMockBuilder(RetrySettings::class) - ->disableOriginalConstructor() - ->getMock(); $pagedDescriptors = [ 'callType' => Call::PAGINATED_CALL, 'responseType' => 'Google\Longrunning\ListOperationsResponse', @@ -521,7 +496,7 @@ public function testStartApiCallPaged() $client->set('transport', $transport); $client->set('credentialsWrapper', $credentialsWrapper); $client->set('agentHeader', $header); - $client->set('retrySettings', ['method' => $retrySettings]); + $client->set('retrySettings', ['method' => $this->getRetrySettingsMock()]); $client->set('descriptors', ['method' => $pagedDescriptors]); $request = new MockRequest(); @@ -534,9 +509,6 @@ public function testStartApiCallPaged() public function testStartAsyncCall() { $header = AgentHeader::buildAgentHeader([]); - $retrySettings = $this->getMockBuilder(RetrySettings::class) - ->disableOriginalConstructor() - ->getMock(); $unaryDescriptors = [ 'callType' => Call::UNARY_CALL, 'responseType' => 'Google\Longrunning\Operation' @@ -551,7 +523,7 @@ public function testStartAsyncCall() $client->set('transport', $transport); $client->set('credentialsWrapper', $credentialsWrapper); $client->set('agentHeader', $header); - $client->set('retrySettings', ['Method' => $retrySettings]); + $client->set('retrySettings', ['Method' => $this->getRetrySettingsMock()]); $client->set('descriptors', ['Method' => $unaryDescriptors]); $request = new MockRequest(); @@ -564,9 +536,6 @@ public function testStartAsyncCall() public function testStartAsyncCallPaged() { $header = AgentHeader::buildAgentHeader([]); - $retrySettings = $this->getMockBuilder(RetrySettings::class) - ->disableOriginalConstructor() - ->getMock(); $pagedDescriptors = [ 'callType' => Call::PAGINATED_CALL, 'responseType' => 'Google\Longrunning\ListOperationsResponse', @@ -596,7 +565,7 @@ public function testStartAsyncCallPaged() $client->set('transport', $transport); $client->set('credentialsWrapper', $credentialsWrapper); $client->set('agentHeader', $header); - $client->set('retrySettings', ['Method' => $retrySettings]); + $client->set('retrySettings', ['Method' => $this->getRetrySettingsMock()]); $client->set('descriptors', ['Method' => $pagedDescriptors]); $request = new MockRequest(); @@ -953,9 +922,7 @@ public function testModifyClientOptions() private function buildClientToTestModifyCallMethods($clientClass = null) { $header = AgentHeader::buildAgentHeader([]); - $retrySettings = $this->getMockBuilder(RetrySettings::class) - ->disableOriginalConstructor() - ->getMock(); + $retrySettings = $this->getRetrySettingsMock(); $longRunningDescriptors = [ 'longRunning' => [ @@ -1198,12 +1165,7 @@ public function testUserProjectHeaderIsSetWhenProvidingQuotaProject() ] ); $client->setClientOptions($updatedOptions); - $client->set('retrySettings', [ - 'method' => $this->getMockBuilder(RetrySettings::class) - ->disableOriginalConstructor() - ->getMock() - ] - ); + $client->set('retrySettings', ['method' => $this->getRetrySettingsMock()]); $client->startCall( 'method', 'decodeType' @@ -1212,7 +1174,6 @@ public function testUserProjectHeaderIsSetWhenProvidingQuotaProject() public function testDefaultAudience() { - $retrySettings = $this->prophesize(RetrySettings::class); $credentialsWrapper = $this->prophesize(CredentialsWrapper::class) ->reveal(); $transport = $this->prophesize(TransportInterface::class); @@ -1233,7 +1194,7 @@ public function testDefaultAudience() $client->set('agentHeader', []); $client->set( 'retrySettings', - ['method.name' => $retrySettings->reveal()] + ['method.name' => $this->getRetrySettingsMock()] ); $client->set('transport', $transport->reveal()); $client->startCall('method.name', 'decodeType'); @@ -1257,7 +1218,6 @@ public function testDefaultAudience() public function testDefaultAudienceWithOperations() { - $retrySettings = $this->prophesize(RetrySettings::class); $credentialsWrapper = $this->prophesize(CredentialsWrapper::class) ->reveal(); $transport = $this->prophesize(TransportInterface::class); @@ -1289,7 +1249,7 @@ public function testDefaultAudienceWithOperations() $client->set('agentHeader', []); $client->set( 'retrySettings', - ['method.name' => $retrySettings->reveal()] + ['method.name' => $this->getRetrySettingsMock()] ); $client->set('transport', $transport->reveal()); $client->set('descriptors', ['method.name' => $longRunningDescriptors]); @@ -1309,7 +1269,6 @@ public function testDefaultAudienceWithOperations() public function testDefaultAudienceWithPagedList() { - $retrySettings = $this->prophesize(RetrySettings::class); $credentialsWrapper = $this->prophesize(CredentialsWrapper::class) ->reveal(); $transport = $this->prophesize(TransportInterface::class); @@ -1339,7 +1298,7 @@ public function testDefaultAudienceWithPagedList() $client->set('agentHeader', []); $client->set( 'retrySettings', - ['method.name' => $retrySettings->reveal()] + ['method.name' => $this->getRetrySettingsMock()] ); $client->set('transport', $transport->reveal()); $client->set('descriptors', [ @@ -1565,6 +1524,17 @@ public function testSurfaceAgentHeaders() $this->assertStringContainsString(' gapic/0.0.1 ', $agentHeader['x-goog-api-client'][0]); $this->assertEquals('gcloud-php-new/0.0.1', $agentHeader['User-Agent'][0]); } + + private function getRetrySettingsMock() + { + $retrySettingsMock = $this->getMockBuilder(RetrySettings::class) + ->disableOriginalConstructor() + ->getMock(); + $retrySettingsMock->expects($this->any()) + ->method('with') + ->willReturn($this->prophesize(RetrySettings::class)->reveal()); + return $retrySettingsMock; + } } class StubGapicClient