From 0189f6f393764e28af7cc889678cde6289025382 Mon Sep 17 00:00:00 2001 From: michaelbausor Date: Tue, 3 Jan 2017 09:17:23 -0800 Subject: [PATCH] Add LRO support (#42) - Generate operations API - Add operations wrapper - Address PR comments - Add max duration setting to pollUntilComplete - Add operationSucceeded and operationFailed functions - Update OperationsClient namespace - Edit OperationsClient comments --- src/ApiCallable.php | 28 +- src/GrpcCredentialsHelper.php | 8 +- src/LongRunning/OperationsClient.php | 496 ++++++++++++++++++ .../resources/operations_client_config.json | 48 ++ src/OperationResponse.php | 295 +++++++++++ src/generated/operations.php | 6 +- tests/ApiCallableTest.php | 343 ++++++++++++ tests/OperationResponseTest.php | 148 ++++++ tests/PagedListResponseTest.php | 3 +- tests/mocks/MockStub.php | 12 +- 10 files changed, 1373 insertions(+), 14 deletions(-) create mode 100644 src/LongRunning/OperationsClient.php create mode 100644 src/LongRunning/resources/operations_client_config.json create mode 100644 src/OperationResponse.php create mode 100644 tests/OperationResponseTest.php diff --git a/src/ApiCallable.php b/src/ApiCallable.php index 36ca8187c..66fb57630 100644 --- a/src/ApiCallable.php +++ b/src/ApiCallable.php @@ -120,6 +120,20 @@ private static function setPageStreaming($callable, $pageStreamingDescriptor) return $inner; } + private static function setLongRunnning($callable, $longRunningDescriptor) + { + $inner = function () use ($callable, $longRunningDescriptor) { + $response = call_user_func_array($callable, func_get_args()); + $name = $response->getName(); + $client = $longRunningDescriptor['operationsClient']; + $options = $longRunningDescriptor + [ + 'lastProtoResponse' => $response, + ]; + return new OperationResponse($name, $client, $options); + }; + return $inner; + } + private static function setCustomHeader($callable, $headerDescriptor) { $inner = function () use ($callable, $headerDescriptor) { @@ -138,16 +152,18 @@ private static function setCustomHeader($callable, $headerDescriptor) } /** - * @param Grpc\BaseStub $stub the gRPC stub to make calls through. + * @param \Grpc\BaseStub $stub the gRPC stub to make calls through. * @param string $methodName the method name on the stub to call. - * @param Google\GAX\CallSettings $settings the call settings to use for this call. + * @param \Google\GAX\CallSettings $settings the call settings to use for this call. * @param array $options { * Optional. - * @type Google\GAX\PageStreamingDescriptor $pageStreamingDescriptor + * @type \Google\GAX\PageStreamingDescriptor $pageStreamingDescriptor * the descriptor used for page-streaming. - * @type Google\GAX\AgentHeaderDescriptor $headerDescriptor + * @type \Google\GAX\AgentHeaderDescriptor $headerDescriptor * the descriptor used for creating GAPIC header. * } + * + * @return callable */ public static function createApiCall($stub, $methodName, CallSettings $settings, $options = []) { @@ -176,6 +192,10 @@ public static function createApiCall($stub, $methodName, CallSettings $settings, $apiCall = self::setPageStreaming($apiCall, $options['pageStreamingDescriptor']); } + if (array_key_exists('longRunningDescriptor', $options)) { + $apiCall = self::setLongRunnning($apiCall, $options['longRunningDescriptor']); + } + if (array_key_exists('headerDescriptor', $options)) { $apiCall = self::setCustomHeader($apiCall, $options['headerDescriptor']); } diff --git a/src/GrpcCredentialsHelper.php b/src/GrpcCredentialsHelper.php index f0f70d8b4..712b2e76f 100644 --- a/src/GrpcCredentialsHelper.php +++ b/src/GrpcCredentialsHelper.php @@ -56,7 +56,7 @@ class GrpcCredentialsHelper * will be passed as optional arguments to Google\Auth\FetchAuthTokenCache * when caching is enabled. * - * @var Google\Auth\CredentialsLoader $credentialsLoader + * @var \Google\Auth\CredentialsLoader $credentialsLoader * A user-created CredentialsLoader object. Defaults to using * ApplicationDefaultCredentials * @var boolean $enableCaching @@ -111,7 +111,7 @@ public function createCallCredentialsCallback() /** * Creates a gRPC client stub. * - * @param function $generatedCreateStub + * @param callable $generatedCreateStub * Function callback which must accept two arguments ($hostname, $opts) * and return an instance of the stub of the specific API to call. * Generally, this should just call the stub's constructor and return @@ -121,10 +121,10 @@ public function createCallCredentialsCallback() * @param array $options { * Optional. Options for configuring the gRPC stub. * - * @type Grpc\ChannelCredentials $sslCreds + * @type \Grpc\ChannelCredentials $sslCreds * A `ChannelCredentials` for use with an SSL-enabled channel. * Default: a credentials object returned from - * Grpc\ChannelCredentials::createSsl() + * \Grpc\ChannelCredentials::createSsl() * } */ public function createStub($generatedCreateStub, $serviceAddress, $port, $options = []) diff --git a/src/LongRunning/OperationsClient.php b/src/LongRunning/OperationsClient.php new file mode 100644 index 000000000..4ae7a9260 --- /dev/null +++ b/src/LongRunning/OperationsClient.php @@ -0,0 +1,496 @@ +getOperation($name); + * } finally { + * if (isset($operationsClient)) { + * $operationsClient->close(); + * } + * } + * ``` + * + * Many parameters require resource names to be formatted in a particular way. To assist + * with these names, this class includes a format method for each type of name, and additionally + * a parse method to extract the individual identifiers contained within names that are + * returned. + */ +class OperationsClient +{ + /** + * The default port of the service. + */ + const DEFAULT_SERVICE_PORT = 443; + + /** + * The default timeout for non-retrying methods. + */ + const DEFAULT_TIMEOUT_MILLIS = 30000; + + const _CODEGEN_NAME = 'gapic'; + const _CODEGEN_VERSION = '0.1.0'; + + private $grpcCredentialsHelper; + private $operationsStub; + private $scopes; + private $defaultCallSettings; + private $descriptors; + + private static function getPageStreamingDescriptors() + { + $listOperationsPageStreamingDescriptor = + new PageStreamingDescriptor([ + 'requestPageTokenField' => 'page_token', + 'requestPageSizeField' => 'page_size', + 'responsePageTokenField' => 'next_page_token', + 'resourceField' => 'operations', + ]); + + $pageStreamingDescriptors = [ + 'listOperations' => $listOperationsPageStreamingDescriptor, + ]; + + return $pageStreamingDescriptors; + } + + // TODO(garrettjones): add channel (when supported in gRPC) + /** + * Constructor. + * + * @param array $options { + * Required. Options for configuring the service API wrapper. Those options + * that must be provided are marked as Required. + * + * @type string $serviceAddress Required. The domain name of the API remote host. + * @type mixed $port The port on which to connect to the remote host. Default 443. + * @type \Grpc\ChannelCredentials $sslCreds + * A `ChannelCredentials` for use with an SSL-enabled channel. + * Default: a credentials object returned from + * \Grpc\ChannelCredentials::createSsl() + * @type array $scopes Required. A string array of scopes to use when acquiring credentials. + * @type array $retryingOverride + * An associative array of string => RetryOptions, where the keys + * are method names (e.g. 'createFoo'), that overrides default retrying + * settings. A value of null indicates that the method in question should + * not retry. + * @type int $timeoutMillis The timeout in milliseconds to use for calls + * that don't use retries. For calls that use retries, + * set the timeout in RetryOptions. + * Default: 30000 (30 seconds) + * @type string $appName The codename of the calling service. Default 'gax'. + * @type string $appVersion The version of the calling service. + * Default: the current version of GAX. + * @type \Google\Auth\CredentialsLoader $credentialsLoader + * A CredentialsLoader object created using the + * Google\Auth library. + * } + */ + public function __construct($options = []) + { + if (!array_key_exists('serviceAddress', $options)) { + throw new ValidationException("The 'serviceAddress' option must be provided."); + } + if (!array_key_exists('scopes', $options)) { + throw new ValidationException("The 'scopes' option must be provided."); + } + $defaultOptions = [ + 'port' => self::DEFAULT_SERVICE_PORT, + 'retryingOverride' => null, + 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, + 'appName' => 'gax', + 'appVersion' => AgentHeaderDescriptor::getGaxVersion(), + ]; + $options = array_merge($defaultOptions, $options); + + $headerDescriptor = new AgentHeaderDescriptor([ + 'clientName' => $options['appName'], + 'clientVersion' => $options['appVersion'], + 'codeGenName' => self::_CODEGEN_NAME, + 'codeGenVersion' => self::_CODEGEN_VERSION, + 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(), + 'phpVersion' => phpversion(), + ]); + + $defaultDescriptors = ['headerDescriptor' => $headerDescriptor]; + $this->descriptors = [ + 'getOperation' => $defaultDescriptors, + 'listOperations' => $defaultDescriptors, + 'cancelOperation' => $defaultDescriptors, + 'deleteOperation' => $defaultDescriptors, + ]; + $pageStreamingDescriptors = self::getPageStreamingDescriptors(); + foreach ($pageStreamingDescriptors as $method => $pageStreamingDescriptor) { + $this->descriptors[$method]['pageStreamingDescriptor'] = $pageStreamingDescriptor; + } + + $clientConfigJsonString = file_get_contents(__DIR__.'/resources/operations_client_config.json'); + $clientConfig = json_decode($clientConfigJsonString, true); + $this->defaultCallSettings = + CallSettings::load( + 'google.longrunning.Operations', + $clientConfig, + $options['retryingOverride'], + GrpcConstants::getStatusCodeNames(), + $options['timeoutMillis'] + ); + + $this->scopes = $options['scopes']; + + $createStubOptions = []; + if (array_key_exists('sslCreds', $options)) { + $createStubOptions['sslCreds'] = $options['sslCreds']; + } + $grpcCredentialsHelperOptions = array_diff_key($options, $defaultOptions); + $this->grpcCredentialsHelper = new GrpcCredentialsHelper($this->scopes, $grpcCredentialsHelperOptions); + + $createOperationsStubFunction = function ($hostname, $opts) { + return new OperationsGrpcClient($hostname, $opts); + }; + if (array_key_exists('createOperationsStubFunction', $options)) { + $createOperationsStubFunction = $options['createOperationsStubFunction']; + } + $this->operationsStub = $this->grpcCredentialsHelper->createStub( + $createOperationsStubFunction, + $options['serviceAddress'], + $options['port'], + $createStubOptions + ); + } + + /** + * Gets the latest state of a long-running operation. Clients can use this + * method to poll the operation result at intervals as recommended by the API + * service. + * + * Sample code: + * ``` + * try { + * $operationsClient = new OperationsClient(); + * $name = ""; + * $response = $operationsClient->getOperation($name); + * } finally { + * if (isset($operationsClient)) { + * $operationsClient->close(); + * } + * } + * ``` + * + * @param string $name The name of the operation resource. + * @param array $optionalArgs { + * Optional. + * + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \google\longrunning\Operation + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function getOperation($name, $optionalArgs = []) + { + $request = new GetOperationRequest(); + $request->setName($name); + + $mergedSettings = $this->defaultCallSettings['getOperation']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->operationsStub, + 'GetOperation', + $mergedSettings, + $this->descriptors['getOperation'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Lists operations that match the specified filter in the request. If the + * server doesn't support this method, it returns `UNIMPLEMENTED`. + * + * NOTE: the `name` binding below allows API services to override the binding + * to use different resource name schemes, such as `users/*/operations`. + * + * Sample code: + * ``` + * try { + * $operationsClient = new OperationsClient(); + * $name = ""; + * $filter = ""; + * foreach ($operationsClient->listOperations($name, $filter) as $element) { + * // doThingsWith(element); + * } + * } finally { + * if (isset($operationsClient)) { + * $operationsClient->close(); + * } + * } + * ``` + * + * @param string $name The name of the operation collection. + * @param string $filter The standard list filter. + * @param array $optionalArgs { + * Optional. + * + * @type int $pageSize + * The maximum number of resources contained in the underlying API + * response. The API may return fewer values in a page, even if + * there are additional values to be retrieved. + * @type string $pageToken + * A page token is used to specify a page of values to be returned. + * If no page token is specified (the default), the first page + * of values will be returned. Any page token used here must have + * been generated by a previous call to the API. + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \Google\GAX\PagedListResponse + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function listOperations($name, $filter, $optionalArgs = []) + { + $request = new ListOperationsRequest(); + $request->setName($name); + $request->setFilter($filter); + if (isset($optionalArgs['pageSize'])) { + $request->setPageSize($optionalArgs['pageSize']); + } + if (isset($optionalArgs['pageToken'])) { + $request->setPageToken($optionalArgs['pageToken']); + } + + $mergedSettings = $this->defaultCallSettings['listOperations']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->operationsStub, + 'ListOperations', + $mergedSettings, + $this->descriptors['listOperations'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Starts asynchronous cancellation on a long-running operation. The server + * makes a best effort to cancel the operation, but success is not + * guaranteed. If the server doesn't support this method, it returns + * `google.rpc.Code.UNIMPLEMENTED`. Clients can use + * [Operations.GetOperation][google.longrunning.Operations.GetOperation] or + * other methods to check whether the cancellation succeeded or whether the + * operation completed despite cancellation. On successful cancellation, + * the operation is not deleted; instead, it becomes an operation with + * an [Operation.error][google.longrunning.Operation.error] value with a [google.rpc.Status.code][google.rpc.Status.code] of 1, + * corresponding to `Code.CANCELLED`. + * + * Sample code: + * ``` + * try { + * $operationsClient = new OperationsClient(); + * $name = ""; + * $operationsClient->cancelOperation($name); + * } finally { + * if (isset($operationsClient)) { + * $operationsClient->close(); + * } + * } + * ``` + * + * @param string $name The name of the operation resource to be cancelled. + * @param array $optionalArgs { + * Optional. + * + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function cancelOperation($name, $optionalArgs = []) + { + $request = new CancelOperationRequest(); + $request->setName($name); + + $mergedSettings = $this->defaultCallSettings['cancelOperation']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->operationsStub, + 'CancelOperation', + $mergedSettings, + $this->descriptors['cancelOperation'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Deletes a long-running operation. This method indicates that the client is + * no longer interested in the operation result. It does not cancel the + * operation. If the server doesn't support this method, it returns + * `google.rpc.Code.UNIMPLEMENTED`. + * + * Sample code: + * ``` + * try { + * $operationsClient = new OperationsClient(); + * $name = ""; + * $operationsClient->deleteOperation($name); + * } finally { + * if (isset($operationsClient)) { + * $operationsClient->close(); + * } + * } + * ``` + * + * @param string $name The name of the operation resource to be deleted. + * @param array $optionalArgs { + * Optional. + * + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function deleteOperation($name, $optionalArgs = []) + { + $request = new DeleteOperationRequest(); + $request->setName($name); + + $mergedSettings = $this->defaultCallSettings['deleteOperation']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->operationsStub, + 'DeleteOperation', + $mergedSettings, + $this->descriptors['deleteOperation'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Initiates an orderly shutdown in which preexisting calls continue but new + * calls are immediately cancelled. + */ + public function close() + { + $this->operationsStub->close(); + } + + private function createCredentialsCallback() + { + return $this->grpcCredentialsHelper->createCallCredentialsCallback(); + } +} diff --git a/src/LongRunning/resources/operations_client_config.json b/src/LongRunning/resources/operations_client_config.json new file mode 100644 index 000000000..86873bc95 --- /dev/null +++ b/src/LongRunning/resources/operations_client_config.json @@ -0,0 +1,48 @@ +{ + "interfaces": { + "google.longrunning.Operations": { + "retry_codes": { + "retry_codes_def": { + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ], + "non_idempotent": [] + } + }, + "retry_params": { + "default": { + "initial_retry_delay_millis": 100, + "retry_delay_multiplier": 1.3, + "max_retry_delay_millis": 60000, + "initial_rpc_timeout_millis": 20000, + "rpc_timeout_multiplier": 1.0, + "max_rpc_timeout_millis": 20000, + "total_timeout_millis": 600000 + } + }, + "methods": { + "GetOperation": { + "timeout_millis": 60000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "ListOperations": { + "timeout_millis": 60000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "CancelOperation": { + "timeout_millis": 60000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "DeleteOperation": { + "timeout_millis": 60000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + } + } + } + } +} diff --git a/src/OperationResponse.php b/src/OperationResponse.php new file mode 100644 index 000000000..eb4122529 --- /dev/null +++ b/src/OperationResponse.php @@ -0,0 +1,295 @@ +operationName = $operationName; + $this->operationsClient = $operationsClient; + if (isset($options['operationReturnType'])) { + $this->operationReturnType = $options['operationReturnType']; + } + if (isset($options['metadataReturnType'])) { + $this->metadataReturnType = $options['metadataReturnType']; + } + if (isset($options['lastProtoResponse'])) { + $this->lastProtoResponse = $options['lastProtoResponse']; + } + } + + /** + * Check whether the operation has completed. + * + * @return bool + */ + public function isDone() + { + return (is_null($this->lastProtoResponse) || is_null($this->lastProtoResponse->getDone())) + ? false + : $this->lastProtoResponse->getDone(); + } + + /** + * Check whether the operation completed successfully. If the operation is not complete, or if the operation + * failed, return false. + * + * @return bool + */ + public function operationSucceeded() + { + return !is_null($this->getResult()); + } + + /** + * Check whether the operation failed. If the operation is not complete, or if the operation + * succeeded, return false. + * + * @return bool + */ + public function operationFailed() + { + return !is_null($this->getError()); + } + + /** + * Get the formatted name of the operation + * + * @return string The formatted name of the operation + */ + public function getName() + { + return $this->operationName; + } + + /** + * Poll the server in a loop until the operation is complete. + * + * Return true if the operation completed, otherwise return false. If the + * $options['maxPollingDuration'] setting is not set (or set <= 0.0) then + * pollUntilComplete will continue polling until the operation completes, + * and therefore will always return true. + * + * @param array $options { + * Options for configuring the polling behaviour. + * + * @type float $pollingIntervalSeconds The polling interval to use, in seconds. + * Default: 1.0 + * @type float $maxPollingDurationSeconds The maximum amount of time to continue polling. + * Default: 0.0 (no maximum) + * } + * @return bool Indicates if the operation completed. + * @throws ApiException If an API call fails. + */ + public function pollUntilComplete($options = []) + { + $defaultPollSettings = [ + 'pollingIntervalSeconds' => $this::DEFAULT_POLLING_INTERVAL, + 'maxPollingDurationSeconds' => 0.0, + ]; + $pollSettings = array_merge($defaultPollSettings, $options); + + $pollingIntervalMicros = $pollSettings['pollingIntervalSeconds'] * 1000000; + $maxPollingDuration = $pollSettings['maxPollingDurationSeconds']; + + $hasMaxPollingDuration = $maxPollingDuration > 0.0; + $endTime = microtime(true) + $maxPollingDuration; + while (!$this->isDone() && (!$hasMaxPollingDuration || microtime(true) < $endTime)) { + usleep($pollingIntervalMicros); + $this->reload(); + } + + return $this->isDone(); + } + + /** + * Reload the status of the operation with a request to the service. + * + * @throws ApiException If the API call fails. + * @throws ValidationException If called on a deleted operation. + */ + public function reload() + { + if ($this->deleted) { + throw new ValidationException("Cannot call reload() on a deleted operation"); + } + $name = $this->getName(); + $this->lastProtoResponse = $this->operationsClient->getOperation($name); + } + + /** + * Return the result of the operation. If operationSucceeded() is false, return null. + * + * @return mixed|null The result of the operation, or null if operationSucceeded() is false + */ + public function getResult() + { + if (!$this->isDone() || !$this->lastProtoResponse->hasResponse()) { + return null; + } + + $anyResponse = $this->lastProtoResponse->getResponse(); + if (is_null($this->operationReturnType)) { + return $anyResponse; + } + $operationReturnType = $this->operationReturnType; + $response = new $operationReturnType(); + $response->parse($anyResponse->getValue()); + return $response; + } + + /** + * If the operation failed, return the status. If operationFailed() is false, return null. + * + * @return \google\rpc\Status|null The status of the operation in case of failure, or null if + * operationFailed() is false. + */ + public function getError() + { + if (!$this->isDone() || !$this->lastProtoResponse->hasError()) { + return null; + } + return $this->lastProtoResponse->getError(); + } + + /** + * @return \google\longrunning\Operation|null The last Operation object received from the server. + */ + public function getLastProtoResponse() + { + return $this->lastProtoResponse; + } + + /** + * @return \Google\GAX\LongRunning\OperationsClient The OperationsClient object used to make + * requests to the operations API. + */ + public function getOperationsClient() + { + return $this->operationsClient; + } + + /** + * Starts asynchronous cancellation on a long-running operation. The server + * makes a best effort to cancel the operation, but success is not + * guaranteed. If the server doesn't support this method, it will throw an + * ApiException with code \google\rpc\Code::UNIMPLEMENTED. Clients can continue + * to use reload and pollUntilComplete methods to check whether the cancellation + * succeeded or whether the operation completed despite cancellation. + * On successful cancellation, the operation is not deleted; instead, it becomes + * an operation with a getError() value with a \google\rpc\Status code of 1, + * corresponding to \google\rpc\Code::CANCELLED. + * + * @throws ApiException If the API call fails. + */ + public function cancel() + { + $this->operationsClient->cancelOperation($this->getName()); + } + + /** + * Delete the long-running operation. This method indicates that the client is + * no longer interested in the operation result. It does not cancel the operation. + * If the server doesn't support this method, it will throw an ApiException with + * code google\rpc\Code::UNIMPLEMENTED. + * + * @throws ApiException If the API call fails. + */ + public function delete() + { + $this->operationsClient->deleteOperation($this->getName()); + $this->deleted = true; + } + + /** + * Get the metadata returned with the last proto response. If a metadata type was provided, then + * the return value will be of that type - otherwise, the return value will be of type Any. If + * no metadata object is available, returns null. + * + * @return mixed The metadata returned from the server in the last response. + */ + public function getMetadata() + { + if (is_null($this->lastProtoResponse)) { + return null; + } + $any = $this->lastProtoResponse->getMetadata(); + if (is_null($this->metadataReturnType)) { + return $any; + } + if (is_null($any) || is_null($any->getValue())) { + return null; + } + $metadataReturnType = $this->metadataReturnType; + $metadata = new $metadataReturnType(); + $metadata->parse($any->getValue()); + return $metadata; + } +} diff --git a/src/generated/operations.php b/src/generated/operations.php index 069112b3f..4a5346727 100644 --- a/src/generated/operations.php +++ b/src/generated/operations.php @@ -827,7 +827,7 @@ public function setName( $value){ namespace google\longrunning { - class OperationsClient extends \Grpc\BaseStub { + class OperationsGrpcClient extends \Grpc\BaseStub { public function __construct($hostname, $opts) { parent::__construct($hostname, $opts); @@ -848,13 +848,13 @@ public function ListOperations(\google\longrunning\ListOperationsRequest $argume * @param google\longrunning\CancelOperationRequest $input */ public function CancelOperation(\google\longrunning\CancelOperationRequest $argument, $metadata = array(), $options = array()) { - return $this->_simpleRequest('/google.longrunning.Operations/CancelOperation', $argument, '\google\protobuf\Empty::deserialize', $metadata, $options); + return $this->_simpleRequest('/google.longrunning.Operations/CancelOperation', $argument, '\google\protobuf\EmptyC::deserialize', $metadata, $options); } /** * @param google\longrunning\DeleteOperationRequest $input */ public function DeleteOperation(\google\longrunning\DeleteOperationRequest $argument, $metadata = array(), $options = array()) { - return $this->_simpleRequest('/google.longrunning.Operations/DeleteOperation', $argument, '\google\protobuf\Empty::deserialize', $metadata, $options); + return $this->_simpleRequest('/google.longrunning.Operations/DeleteOperation', $argument, '\google\protobuf\EmptyC::deserialize', $metadata, $options); } } } diff --git a/tests/ApiCallableTest.php b/tests/ApiCallableTest.php index 5dc2c4d5f..a6ebcfefe 100644 --- a/tests/ApiCallableTest.php +++ b/tests/ApiCallableTest.php @@ -40,6 +40,10 @@ use Google\GAX\Testing\MockStatus; use Google\GAX\Testing\MockRequest; use Google\GAX\Testing\MockResponse; +use google\longrunning\Operation; +use Google\Longrunning\OperationsClient; +use google\protobuf\EmptyC; +use google\rpc\Code; class ApiCallableTest extends PHPUnit_Framework_TestCase { @@ -430,4 +434,343 @@ public function testCustomHeader() ]; $this->assertEquals($expectedMetadata, $actualCalls[0]['metadata']); } + + public static function createIncompleteOperationResponse($name, $metadataString = '') + { + $metadata = OperationResponseTest::createAny(OperationResponseTest::createStatus(Code::OK, $metadataString)); + $op = new Operation(); + $op->setName($name)->setMetadata($metadata)->setDone(false); + return $op; + } + + public static function createSuccessfulOperationResponse($name, $response, $metadataString = '') + { + $op = self::createIncompleteOperationResponse($name, $metadataString); + $op->setDone(true)->setResponse(OperationResponseTest::createAny($response)); + return $op; + } + + public static function createFailedOperationResponse($name, $code, $message, $metadataString = '') + { + $error = OperationResponseTest::createStatus($code, $message); + $op = self::createIncompleteOperationResponse($name, $metadataString); + $op->setDone(true)->setError($error); + return $op; + } + + public function testLongrunningSuccess() + { + $opName = 'operation/someop'; + + $request = null; + $result = OperationResponseTest::createStatus(Code::OK, 'someMessage'); + + $initialResponse = self::createIncompleteOperationResponse($opName, 'm1'); + $responseA = self::createIncompleteOperationResponse($opName, 'm2'); + $responseB = self::createSuccessfulOperationResponse($opName, $result, 'm3'); + $responseSequence = [ + [$responseA, new MockStatus(Grpc\STATUS_OK, '')], + [$responseB, new MockStatus(Grpc\STATUS_OK, '')], + ]; + $callStub = MockStub::createWithResponseSequence([[$initialResponse, new MockStatus(Grpc\STATUS_OK, '')]]); + $opStub = MockStub::createWithResponseSequence($responseSequence); + $opClient = OperationResponseTest::createOperationsClient($opStub); + $descriptor = [ + 'operationsClient' => $opClient, + 'operationReturnType' => '\google\rpc\Status', + 'metadataReturnType' => '\google\rpc\Status', + ]; + $callSettings = new CallSettings(); + $apiCall = ApiCallable::createApiCall( + $callStub, 'takeAction', $callSettings, ['longRunningDescriptor' => $descriptor]); + + /* @var $response \Google\GAX\OperationResponse */ + $response = $apiCall($request, [], []); + + $results = [$response->getResult()]; + $errors = [$response->getError()]; + $metadataResponses = [$response->getMetadata()]; + $isDoneResponses = [$response->isDone()]; + + $this->assertSame(1, count($callStub->actualCalls)); + $this->assertSame(0, count($opStub->actualCalls)); + + while (!$response->isDone()) { + $response->reload(); + $results[] = $response->getResult(); + $errors[] = $response->getError(); + $metadataResponses[] = $response->getMetadata(); + $isDoneResponses[] = $response->isDone(); + } + + $this->assertSame(1, count($callStub->actualCalls)); + $this->assertSame(2, count($opStub->actualCalls)); + + $this->assertSame('takeAction', $callStub->actualCalls[0]['funcName']); + $this->assertSame('GetOperation', $opStub->actualCalls[0]['funcName']); + $this->assertSame('GetOperation', $opStub->actualCalls[1]['funcName']); + + $this->assertEquals([null, null, OperationResponseTest::createStatus(Code::OK, 'someMessage')], $results); + $this->assertEquals([null, null, null], $errors); + $this->assertEquals([ + OperationResponseTest::createStatus(Code::OK, 'm1'), + OperationResponseTest::createStatus(Code::OK, 'm2'), + OperationResponseTest::createStatus(Code::OK, 'm3') + ], $metadataResponses); + $this->assertEquals([false, false, true], $isDoneResponses); + } + + public function testLongrunningPollingInterval() + { + $opName = 'operation/someop'; + + $request = null; + $result = OperationResponseTest::createStatus(Code::OK, 'someMessage'); + + $initialResponse = self::createIncompleteOperationResponse($opName, 'm1'); + $responseA = self::createIncompleteOperationResponse($opName, 'm2'); + $responseB = self::createSuccessfulOperationResponse($opName, $result, 'm3'); + $responseSequence = [ + [$responseA, new MockStatus(Grpc\STATUS_OK, '')], + [$responseB, new MockStatus(Grpc\STATUS_OK, '')], + ]; + $callStub = MockStub::createWithResponseSequence([[$initialResponse, new MockStatus(Grpc\STATUS_OK, '')]]); + $opStub = MockStub::createWithResponseSequence($responseSequence); + $opClient = OperationResponseTest::createOperationsClient($opStub); + $descriptor = [ + 'operationsClient' => $opClient, + 'operationReturnType' => '\google\rpc\Status', + 'metadataReturnType' => '\google\rpc\Status', + ]; + $callSettings = new CallSettings(); + $apiCall = ApiCallable::createApiCall( + $callStub, 'takeAction', $callSettings, ['longRunningDescriptor' => $descriptor]); + + /* @var $response \Google\GAX\OperationResponse */ + $response = $apiCall($request, [], []); + + $this->assertSame(1, count($callStub->actualCalls)); + $this->assertSame(0, count($opStub->actualCalls)); + + $complete = $response->pollUntilComplete(['pollingIntervalSeconds' => 0.1]); + $this->assertTrue($complete); + $this->assertTrue($response->isDone()); + + $this->assertSame(1, count($callStub->actualCalls)); + $this->assertSame(2, count($opStub->actualCalls)); + + $this->assertSame('takeAction', $callStub->actualCalls[0]['funcName']); + $this->assertSame('GetOperation', $opStub->actualCalls[0]['funcName']); + $this->assertSame('GetOperation', $opStub->actualCalls[1]['funcName']); + + $this->assertEquals(OperationResponseTest::createStatus(Code::OK, 'someMessage'), $response->getResult()); + $this->assertNull($response->getError()); + $this->assertEquals(OperationResponseTest::createStatus(Code::OK, 'm3'), $response->getMetadata()); + } + + public function testLongrunningMaxPollingDuration() + { + $opName = 'operation/someop'; + + $request = null; + $result = OperationResponseTest::createStatus(Code::OK, 'someMessage'); + + $initialResponse = self::createIncompleteOperationResponse($opName, 'm1'); + $responseA = self::createIncompleteOperationResponse($opName, 'm2'); + $responseB = self::createIncompleteOperationResponse($opName, 'm3'); + $responseSequence = [ + [$responseA, new MockStatus(Grpc\STATUS_OK, '')], + [$responseB, new MockStatus(Grpc\STATUS_OK, '')], + ]; + $callStub = MockStub::createWithResponseSequence([[$initialResponse, new MockStatus(Grpc\STATUS_OK, '')]]); + $opStub = MockStub::createWithResponseSequence($responseSequence); + $opClient = OperationResponseTest::createOperationsClient($opStub); + $descriptor = [ + 'operationsClient' => $opClient, + 'operationReturnType' => '\google\rpc\Status', + 'metadataReturnType' => '\google\rpc\Status', + ]; + $callSettings = new CallSettings(); + $apiCall = ApiCallable::createApiCall( + $callStub, 'takeAction', $callSettings, ['longRunningDescriptor' => $descriptor]); + + /* @var $response \Google\GAX\OperationResponse */ + $response = $apiCall($request, [], []); + + $this->assertSame(1, count($callStub->actualCalls)); + $this->assertSame(0, count($opStub->actualCalls)); + + $complete = $response->pollUntilComplete([ + 'pollingIntervalSeconds' => 0.1, + 'maxPollingDurationSeconds' => 0.15, + ]); + $this->assertFalse($complete); + $this->assertFalse($response->isDone()); + + $this->assertSame(1, count($callStub->actualCalls)); + $this->assertSame(2, count($opStub->actualCalls)); + + $this->assertSame('takeAction', $callStub->actualCalls[0]['funcName']); + $this->assertSame('GetOperation', $opStub->actualCalls[0]['funcName']); + + $this->assertNull($response->getResult()); + $this->assertNull($response->getError()); + $this->assertEquals(OperationResponseTest::createStatus(Code::OK, 'm3'), $response->getMetadata()); + } + + public function testLongrunningFailure() + { + $opName = 'operation/someop'; + + $request = null; + + $initialResponse = self::createIncompleteOperationResponse($opName, 'm1'); + $responseA = self::createIncompleteOperationResponse($opName, 'm2'); + $responseB = self::createFailedOperationResponse($opName, Code::UNKNOWN, 'someError', 'm3'); + $responseSequence = [ + [$responseA, new MockStatus(Grpc\STATUS_OK, '')], + [$responseB, new MockStatus(Grpc\STATUS_OK, '')], + ]; + $callStub = MockStub::createWithResponseSequence( + [[$initialResponse, new MockStatus(Grpc\STATUS_OK, '')]]); + $opStub = MockStub::createWithResponseSequence($responseSequence); + $opClient = OperationResponseTest::createOperationsClient($opStub); + $descriptor = [ + 'operationsClient' => $opClient, + 'operationReturnType' => '\google\rpc\Status', + 'metadataReturnType' => '\google\rpc\Status', + ]; + $callSettings = new CallSettings(); + $apiCall = ApiCallable::createApiCall( + $callStub, 'takeAction', $callSettings, ['longRunningDescriptor' => $descriptor]); + + /* @var $response \Google\GAX\OperationResponse */ + $response = $apiCall($request, [], []); + + $results = [$response->getResult()]; + $errors = [$response->getError()]; + $metadataResponses = [$response->getMetadata()]; + $isDoneResponses = [$response->isDone()]; + + $this->assertSame(1, count($callStub->actualCalls)); + $this->assertSame(0, count($opStub->actualCalls)); + + while (!$response->isDone()) { + $response->reload(); + $results[] = $response->getResult(); + $errors[] = $response->getError(); + $metadataResponses[] = $response->getMetadata(); + $isDoneResponses[] = $response->isDone(); + } + + $this->assertSame(1, count($callStub->actualCalls)); + $this->assertSame(2, count($opStub->actualCalls)); + + $this->assertSame('takeAction', $callStub->actualCalls[0]['funcName']); + $this->assertSame('GetOperation', $opStub->actualCalls[0]['funcName']); + $this->assertSame('GetOperation', $opStub->actualCalls[1]['funcName']); + + $this->assertEquals([null, null, null], $results); + $this->assertEquals([null, null, OperationResponseTest::createStatus(Code::UNKNOWN, 'someError')], $errors); + $this->assertEquals([ + OperationResponseTest::createStatus(Code::OK, 'm1'), + OperationResponseTest::createStatus(Code::OK, 'm2'), + OperationResponseTest::createStatus(Code::OK, 'm3') + ], $metadataResponses); + $this->assertEquals([false, false, true], $isDoneResponses); + } + + public function testLongrunningCancel() + { + $opName = 'operation/someop'; + + $request = null; + + $initialResponse = self::createIncompleteOperationResponse($opName, 'm1'); + $responseA = self::createIncompleteOperationResponse($opName, 'm2'); + $responseB = self::createFailedOperationResponse($opName, Code::CANCELLED, 'someError', 'm3'); + $responseSequence = [ + [new EmptyC(), new MockStatus(Grpc\STATUS_OK, '')], + [$responseA, new MockStatus(Grpc\STATUS_OK, '')], + [$responseB, new MockStatus(Grpc\STATUS_OK, '')], + ]; + $callStub = MockStub::createWithResponseSequence([[$initialResponse, new MockStatus(Grpc\STATUS_OK, '')]]); + $opStub = MockStub::createWithResponseSequence($responseSequence); + $opClient = OperationResponseTest::createOperationsClient($opStub); + $descriptor = [ + 'operationsClient' => $opClient, + 'operationReturnType' => '\google\rpc\Status', + 'metadataReturnType' => '\google\rpc\Status', + ]; + $callSettings = new CallSettings(); + $apiCall = ApiCallable::createApiCall( + $callStub, 'takeAction', $callSettings, ['longRunningDescriptor' => $descriptor]); + + /* @var $response \Google\GAX\OperationResponse */ + $response = $apiCall($request, [], []); + + $this->assertSame(1, count($callStub->actualCalls)); + $this->assertSame(0, count($opStub->actualCalls)); + + $response->cancel(); + + $this->assertSame(1, count($callStub->actualCalls)); + $this->assertSame(1, count($opStub->actualCalls)); + + while (!$response->isDone()) { + $response->reload(); + } + + $this->assertSame(1, count($callStub->actualCalls)); + $this->assertSame(3, count($opStub->actualCalls)); + + $this->assertSame('takeAction', $callStub->actualCalls[0]['funcName']); + $this->assertSame('CancelOperation', $opStub->actualCalls[0]['funcName']); + $this->assertSame('GetOperation', $opStub->actualCalls[1]['funcName']); + $this->assertSame('GetOperation', $opStub->actualCalls[2]['funcName']); + + $this->assertNull($response->getResult()); + $this->assertEquals(OperationResponseTest::createStatus(Code::CANCELLED, 'someError'), $response->getError()); + $this->assertEquals(OperationResponseTest::createStatus(Code::OK, 'm3'), $response->getMetadata()); + } + + /** + * @expectedException \Google\GAX\ValidationException + * @expectedExceptionMessage Cannot call reload() on a deleted operation + */ + public function testLongrunningDelete() + { + $opName = 'operation/someop'; + + $request = null; + + $initialResponse = self::createIncompleteOperationResponse($opName, 'm1'); + $callStub = MockStub::createWithResponseSequence([[$initialResponse, new MockStatus(Grpc\STATUS_OK, '')]]); + $opStub = MockStub::createWithResponseSequence([[new EmptyC(), new MockStatus(Grpc\STATUS_OK, '')]]); + $opClient = OperationResponseTest::createOperationsClient($opStub); + $descriptor = [ + 'operationsClient' => $opClient, + 'operationReturnType' => '\google\rpc\Status', + 'metadataReturnType' => '\google\rpc\Status', + ]; + $callSettings = new CallSettings(); + $apiCall = ApiCallable::createApiCall( + $callStub, 'takeAction', $callSettings, ['longRunningDescriptor' => $descriptor]); + + /* @var $response \Google\GAX\OperationResponse */ + $response = $apiCall($request, [], []); + + $this->assertSame(1, count($callStub->actualCalls)); + $this->assertSame(0, count($opStub->actualCalls)); + + $response->delete(); + + $this->assertSame(1, count($callStub->actualCalls)); + $this->assertSame(1, count($opStub->actualCalls)); + + $this->assertSame('takeAction', $callStub->actualCalls[0]['funcName']); + $this->assertSame('DeleteOperation', $opStub->actualCalls[0]['funcName']); + + $response->reload(); + } } diff --git a/tests/OperationResponseTest.php b/tests/OperationResponseTest.php new file mode 100644 index 000000000..259629b96 --- /dev/null +++ b/tests/OperationResponseTest.php @@ -0,0 +1,148 @@ +assertSame($opName, $op->getName()); + $this->assertSame($opClient, $op->getOperationsClient()); + } + + public function testWithoutResponse() + { + $opName = 'operations/opname'; + $opClient = self::createOperationsClient(); + $op = new OperationResponse($opName, $opClient); + + $this->assertNull($op->getLastProtoResponse()); + $this->assertFalse($op->isDone()); + $this->assertNull($op->getResult()); + $this->assertNull($op->getError()); + $this->assertNull($op->getMetadata()); + $this->assertFalse($op->operationSucceeded()); + $this->assertFalse($op->operationFailed()); + } + + public function testWithResponse() + { + $opName = 'operations/opname'; + $opClient = self::createOperationsClient(); + $protoResponse = new Operation(); + $op = new OperationResponse($opName, $opClient, [ + 'lastProtoResponse' => $protoResponse, + ]); + + $this->assertSame($protoResponse, $op->getLastProtoResponse()); + $this->assertFalse($op->isDone()); + $this->assertNull($op->getResult()); + $this->assertNull($op->getError()); + $this->assertNull($op->getMetadata()); + $this->assertFalse($op->operationSucceeded()); + $this->assertFalse($op->operationFailed()); + + $response = self::createAny(self::createStatus(0, "response")); + $error = self::createStatus(2, "error"); + $metadata = self::createAny(self::createStatus(0, "metadata")); + + $protoResponse->setDone(true)->setResponse($response)->setMetadata($metadata); + $this->assertTrue($op->isDone()); + $this->assertSame($response, $op->getResult()); + $this->assertSame($metadata, $op->getMetadata()); + $this->assertTrue($op->operationSucceeded()); + $this->assertFalse($op->operationFailed()); + + $protoResponse->clearResponse()->setError($error); + $this->assertNull($op->getResult()); + $this->assertSame($error, $op->getError()); + $this->assertFalse($op->operationSucceeded()); + $this->assertTrue($op->operationFailed()); + } + + public function testWithOptions() + { + $opName = 'operations/opname'; + $opClient = self::createOperationsClient(); + $protoResponse = new Operation(); + $op = new OperationResponse($opName, $opClient, [ + 'operationReturnType' => '\google\rpc\Status', + 'metadataReturnType' => '\google\rpc\Status', + 'lastProtoResponse' => $protoResponse, + ]); + + $this->assertSame($protoResponse, $op->getLastProtoResponse()); + $this->assertFalse($op->isDone()); + $this->assertNull($op->getResult()); + $this->assertNull($op->getError()); + $this->assertNull($op->getMetadata()); + + $response = self::createAny(self::createStatus(0, "response")); + $metadata = self::createAny(self::createStatus(0, "metadata")); + + $protoResponse->setDone(true)->setResponse($response)->setMetadata($metadata); + $this->assertTrue($op->isDone()); + $this->assertEquals(self::createStatus(0, "response"), $op->getResult()); + $this->assertEquals(self::createStatus(0, "metadata"), $op->getMetadata()); + } + + public static function createAny($value) { + $any = new Any(); + return $any->setValue($value->serialize()); + } + + public static function createStatus($code, $message) { + $value = new Status(); + return $value->setCode($code)->setMessage($message); + } + + public static function createOperationsClient($stub = null) + { + $client = new OperationsClient([ + 'createOperationsStubFunction' => function ($hostname, $opts) use ($stub) { + return $stub; + }, + 'serviceAddress' => '', + 'scopes' => [], + ]); + return $client; + } +} diff --git a/tests/PagedListResponseTest.php b/tests/PagedListResponseTest.php index db50febb6..f04e7d2d3 100644 --- a/tests/PagedListResponseTest.php +++ b/tests/PagedListResponseTest.php @@ -39,7 +39,8 @@ class PagedListResponseTest extends PHPUnit_Framework_TestCase { - public function testNextPageToken() { + public function testNextPageToken() + { $mockRequest = MockRequest::createPageStreamingRequest('mockToken'); $descriptor = new PageStreamingDescriptor([ 'requestPageTokenField' => 'pageToken', diff --git a/tests/mocks/MockStub.php b/tests/mocks/MockStub.php index 908af84c1..db65c9ff0 100644 --- a/tests/mocks/MockStub.php +++ b/tests/mocks/MockStub.php @@ -66,12 +66,20 @@ public static function createWithResponseSequence($sequence) return $stub; } - public function takeAction($request, $metadata = array(), $options = array()) + public function __call($name, $arguments) + { + $newArgs = array_merge([$name], $arguments); + return call_user_func_array(array($this, 'handleCall'), $newArgs); + } + + private function handleCall($funcName, $request, $metadata = array(), $options = array()) { $actualCall = [ + 'funcName' => $funcName, 'request' => $request, 'metadata' => $metadata, - 'options' => $options]; + 'options' => $options, + ]; array_push($this->actualCalls, $actualCall); if (count($this->responseSequence) == 1) { return new MockGrpcCall($this->responseSequence[0]);