OperationResponse.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. <?php
  2. /*
  3. * Copyright 2016 Google LLC
  4. * All rights reserved.
  5. *
  6. * Redistribution and use in source and binary forms, with or without
  7. * modification, are permitted provided that the following conditions are
  8. * met:
  9. *
  10. * * Redistributions of source code must retain the above copyright
  11. * notice, this list of conditions and the following disclaimer.
  12. * * Redistributions in binary form must reproduce the above
  13. * copyright notice, this list of conditions and the following disclaimer
  14. * in the documentation and/or other materials provided with the
  15. * distribution.
  16. * * Neither the name of Google Inc. nor the names of its
  17. * contributors may be used to endorse or promote products derived from
  18. * this software without specific prior written permission.
  19. *
  20. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  21. * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  22. * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  23. * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  24. * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  25. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  26. * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  27. * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  28. * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  29. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  30. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  31. */
  32. namespace Google\ApiCore;
  33. use Google\LongRunning\Operation;
  34. use Google\Protobuf\Any;
  35. use Google\Protobuf\Internal\Message;
  36. use Google\Rpc\Status;
  37. use LogicException;
  38. /**
  39. * Response object from a long running API method.
  40. *
  41. * The OperationResponse object is returned by API methods that perform
  42. * a long running operation. It provides methods that can be used to
  43. * poll the status of the operation, retrieve the results, and cancel
  44. * the operation.
  45. *
  46. * To support a long running operation, the server must implement the
  47. * Operations API, which is used by the OperationResponse object. If
  48. * more control is required, it is possible to make calls against the
  49. * Operations API directly instead of via the OperationResponse object
  50. * using an Operations Client instance.
  51. */
  52. class OperationResponse
  53. {
  54. use PollingTrait;
  55. const DEFAULT_POLLING_INTERVAL = 1000;
  56. const DEFAULT_POLLING_MULTIPLIER = 2;
  57. const DEFAULT_MAX_POLLING_INTERVAL = 60000;
  58. const DEFAULT_MAX_POLLING_DURATION = 0;
  59. private string $operationName;
  60. private ?object $operationsClient;
  61. private ?string $operationReturnType;
  62. private ?string $metadataReturnType;
  63. private array $defaultPollSettings = [
  64. 'initialPollDelayMillis' => self::DEFAULT_POLLING_INTERVAL,
  65. 'pollDelayMultiplier' => self::DEFAULT_POLLING_MULTIPLIER,
  66. 'maxPollDelayMillis' => self::DEFAULT_MAX_POLLING_INTERVAL,
  67. 'totalPollTimeoutMillis' => self::DEFAULT_MAX_POLLING_DURATION,
  68. ];
  69. private ?object $lastProtoResponse;
  70. private bool $deleted = false;
  71. private array $additionalArgs;
  72. private string $getOperationMethod;
  73. private ?string $cancelOperationMethod;
  74. private ?string $deleteOperationMethod;
  75. private string $operationStatusMethod;
  76. /** @var mixed */
  77. private $operationStatusDoneValue;
  78. private ?string $operationErrorCodeMethod;
  79. private ?string $operationErrorMessageMethod;
  80. /**
  81. * OperationResponse constructor.
  82. *
  83. * @param string $operationName
  84. * @param object $operationsClient
  85. * @param array $options {
  86. * Optional. Options for configuring the operation response object.
  87. *
  88. * @type string $operationReturnType The return type of the longrunning operation.
  89. * @type string $metadataReturnType The type of the metadata returned in the operation response.
  90. * @type int $initialPollDelayMillis The initial polling interval to use, in milliseconds.
  91. * @type int $pollDelayMultiplier Multiplier applied to the polling interval on each retry.
  92. * @type int $maxPollDelayMillis The maximum polling interval to use, in milliseconds.
  93. * @type int $totalPollTimeoutMillis The maximum amount of time to continue polling.
  94. * @type object $lastProtoResponse A response already received from the server.
  95. * @type string $getOperationMethod The method on $operationsClient to get the operation.
  96. * @type string $cancelOperationMethod The method on $operationsClient to cancel the operation.
  97. * @type string $deleteOperationMethod The method on $operationsClient to delete the operation.
  98. * @type string $operationStatusMethod The method on the operation to get the status.
  99. * @type string $operationStatusDoneValue The method on the operation to determine if the status is done.
  100. * @type array $additionalOperationArguments Additional arguments to pass to $operationsClient methods.
  101. * @type string $operationErrorCodeMethod The method on the operation to get the error code
  102. * @type string $operationErrorMessageMethod The method on the operation to get the error status
  103. * }
  104. */
  105. public function __construct(string $operationName, $operationsClient, array $options = [])
  106. {
  107. $this->operationName = $operationName;
  108. $this->operationsClient = $operationsClient;
  109. $options += [
  110. 'operationReturnType' => null,
  111. 'metadataReturnType' => null,
  112. 'lastProtoResponse' => null,
  113. 'getOperationMethod' => 'getOperation',
  114. 'cancelOperationMethod' => 'cancelOperation',
  115. 'deleteOperationMethod' => 'deleteOperation',
  116. 'operationStatusMethod' => 'getDone',
  117. 'operationStatusDoneValue' => true,
  118. 'additionalOperationArguments' => [],
  119. 'operationErrorCodeMethod' => null,
  120. 'operationErrorMessageMethod' => null,
  121. ];
  122. $this->operationReturnType = $options['operationReturnType'];
  123. $this->metadataReturnType = $options['metadataReturnType'];
  124. $this->lastProtoResponse = $options['lastProtoResponse'];
  125. $this->getOperationMethod = $options['getOperationMethod'];
  126. $this->cancelOperationMethod = $options['cancelOperationMethod'];
  127. $this->deleteOperationMethod = $options['deleteOperationMethod'];
  128. $this->additionalArgs = $options['additionalOperationArguments'];
  129. $this->operationStatusMethod = $options['operationStatusMethod'];
  130. $this->operationStatusDoneValue = $options['operationStatusDoneValue'];
  131. $this->operationErrorCodeMethod = $options['operationErrorCodeMethod'];
  132. $this->operationErrorMessageMethod = $options['operationErrorMessageMethod'];
  133. if (isset($options['initialPollDelayMillis'])) {
  134. $this->defaultPollSettings['initialPollDelayMillis'] = $options['initialPollDelayMillis'];
  135. }
  136. if (isset($options['pollDelayMultiplier'])) {
  137. $this->defaultPollSettings['pollDelayMultiplier'] = $options['pollDelayMultiplier'];
  138. }
  139. if (isset($options['maxPollDelayMillis'])) {
  140. $this->defaultPollSettings['maxPollDelayMillis'] = $options['maxPollDelayMillis'];
  141. }
  142. if (isset($options['totalPollTimeoutMillis'])) {
  143. $this->defaultPollSettings['totalPollTimeoutMillis'] = $options['totalPollTimeoutMillis'];
  144. }
  145. }
  146. /**
  147. * Check whether the operation has completed.
  148. *
  149. * @return bool
  150. */
  151. public function isDone()
  152. {
  153. if (!$this->hasProtoResponse()) {
  154. return false;
  155. }
  156. $status = call_user_func([$this->lastProtoResponse, $this->operationStatusMethod]);
  157. if (is_null($status)) {
  158. return false;
  159. }
  160. return $status === $this->operationStatusDoneValue;
  161. }
  162. /**
  163. * Check whether the operation completed successfully. If the operation is not complete, or if the operation
  164. * failed, return false.
  165. *
  166. * @return bool
  167. */
  168. public function operationSucceeded()
  169. {
  170. if (!$this->hasProtoResponse()) {
  171. return false;
  172. }
  173. if (!$this->canHaveResult()) {
  174. // For Operations which do not have a result, we consider a successful
  175. // operation when the operation has completed without errors.
  176. return $this->isDone() && !$this->hasErrors();
  177. }
  178. return !is_null($this->getResult());
  179. }
  180. /**
  181. * Check whether the operation failed. If the operation is not complete, or if the operation
  182. * succeeded, return false.
  183. *
  184. * @return bool
  185. */
  186. public function operationFailed()
  187. {
  188. return $this->hasErrors();
  189. }
  190. /**
  191. * Get the formatted name of the operation
  192. *
  193. * @return string The formatted name of the operation
  194. */
  195. public function getName()
  196. {
  197. return $this->operationName;
  198. }
  199. /**
  200. * Poll the server in a loop until the operation is complete.
  201. *
  202. * Return true if the operation completed, otherwise return false. If the
  203. * $options['totalPollTimeoutMillis'] setting is not set (or set <= 0) then
  204. * pollUntilComplete will continue polling until the operation completes,
  205. * and therefore will always return true.
  206. *
  207. * @param array $options {
  208. * Options for configuring the polling behaviour.
  209. *
  210. * @type int $initialPollDelayMillis The initial polling interval to use, in milliseconds.
  211. * @type int $pollDelayMultiplier Multiplier applied to the polling interval on each retry.
  212. * @type int $maxPollDelayMillis The maximum polling interval to use, in milliseconds.
  213. * @type int $totalPollTimeoutMillis The maximum amount of time to continue polling, in milliseconds.
  214. * }
  215. * @throws ApiException If an API call fails.
  216. * @throws ValidationException
  217. * @return bool Indicates if the operation completed.
  218. */
  219. public function pollUntilComplete(array $options = [])
  220. {
  221. if ($this->isDone()) {
  222. return true;
  223. }
  224. $pollSettings = array_merge($this->defaultPollSettings, $options);
  225. return $this->poll(function () {
  226. $this->reload();
  227. return $this->isDone();
  228. }, $pollSettings);
  229. }
  230. /**
  231. * Reload the status of the operation with a request to the service.
  232. *
  233. * @throws ApiException If the API call fails.
  234. * @throws ValidationException If called on a deleted operation.
  235. */
  236. public function reload()
  237. {
  238. if ($this->deleted) {
  239. throw new ValidationException("Cannot call reload() on a deleted operation");
  240. }
  241. $this->lastProtoResponse = $this->operationsCall(
  242. $this->getOperationMethod,
  243. $this->getName(),
  244. $this->additionalArgs
  245. );
  246. }
  247. /**
  248. * Return the result of the operation. If operationSucceeded() is false, return null.
  249. *
  250. * @return mixed|null The result of the operation, or null if operationSucceeded() is false
  251. */
  252. public function getResult()
  253. {
  254. if (!$this->hasProtoResponse()) {
  255. return null;
  256. }
  257. if (!$this->canHaveResult()) {
  258. return null;
  259. }
  260. if (!$this->isDone()) {
  261. return null;
  262. }
  263. /** @var Any|null $anyResponse */
  264. $anyResponse = $this->lastProtoResponse->getResponse();
  265. if (is_null($anyResponse)) {
  266. return null;
  267. }
  268. if (is_null($this->operationReturnType)) {
  269. return $anyResponse;
  270. }
  271. $operationReturnType = $this->operationReturnType;
  272. /** @var Message $response */
  273. $response = new $operationReturnType();
  274. $response->mergeFromString($anyResponse->getValue());
  275. return $response;
  276. }
  277. /**
  278. * If the operation failed, return the status. If operationFailed() is false, return null.
  279. *
  280. * @return Status|null The status of the operation in case of failure, or null if
  281. * operationFailed() is false.
  282. */
  283. public function getError()
  284. {
  285. if (!$this->hasProtoResponse() || !$this->isDone()) {
  286. return null;
  287. }
  288. if ($this->operationErrorCodeMethod || $this->operationErrorMessageMethod) {
  289. $errorCode = $this->operationErrorCodeMethod
  290. ? call_user_func([$this->lastProtoResponse, $this->operationErrorCodeMethod])
  291. : null;
  292. $errorMessage = $this->operationErrorMessageMethod
  293. ? call_user_func([$this->lastProtoResponse, $this->operationErrorMessageMethod])
  294. : null;
  295. return (new Status())
  296. ->setCode(ApiStatus::rpcCodeFromHttpStatusCode($errorCode))
  297. ->setMessage($errorMessage);
  298. }
  299. if (method_exists($this->lastProtoResponse, 'getError')) {
  300. return $this->lastProtoResponse->getError();
  301. }
  302. return null;
  303. }
  304. /**
  305. * Get an array containing the values of 'operationReturnType', 'metadataReturnType', and
  306. * the polling options `initialPollDelayMillis`, `pollDelayMultiplier`, `maxPollDelayMillis`,
  307. * and `totalPollTimeoutMillis`. The array can be passed as the $options argument to the
  308. * constructor when creating another OperationResponse object.
  309. *
  310. * @return array
  311. */
  312. public function getDescriptorOptions()
  313. {
  314. return [
  315. 'operationReturnType' => $this->operationReturnType,
  316. 'metadataReturnType' => $this->metadataReturnType,
  317. ] + $this->defaultPollSettings;
  318. }
  319. /**
  320. * @return Operation|mixed|null The last Operation object received from the server.
  321. */
  322. public function getLastProtoResponse()
  323. {
  324. return $this->lastProtoResponse;
  325. }
  326. /**
  327. * @return object The OperationsClient object used to make
  328. * requests to the operations API.
  329. */
  330. public function getOperationsClient()
  331. {
  332. return $this->operationsClient;
  333. }
  334. /**
  335. * Cancel the long-running operation.
  336. *
  337. * For operations of type Google\LongRunning\Operation, this method starts
  338. * asynchronous cancellation on a long-running operation. The server
  339. * makes a best effort to cancel the operation, but success is not
  340. * guaranteed. If the server doesn't support this method, it will throw an
  341. * ApiException with code \Google\Rpc\Code::UNIMPLEMENTED. Clients can continue
  342. * to use reload and pollUntilComplete methods to check whether the cancellation
  343. * succeeded or whether the operation completed despite cancellation.
  344. * On successful cancellation, the operation is not deleted; instead, it becomes
  345. * an operation with a getError() value with a \Google\Rpc\Status code of 1,
  346. * corresponding to \Google\Rpc\Code::CANCELLED.
  347. *
  348. * @throws ApiException If the API call fails.
  349. * @throws LogicException If the API call method has not been configured
  350. */
  351. public function cancel()
  352. {
  353. if (is_null($this->cancelOperationMethod)) {
  354. throw new LogicException('The cancel operation is not supported by this API');
  355. }
  356. $this->operationsCall($this->cancelOperationMethod, $this->getName(), $this->additionalArgs);
  357. }
  358. /**
  359. * Delete the long-running operation.
  360. *
  361. * For operations of type Google\LongRunning\Operation, this method
  362. * indicates that the client is no longer interested in the operation result.
  363. * It does not cancel the operation. If the server doesn't support this method,
  364. * it will throw an ApiException with code \Google\Rpc\Code::UNIMPLEMENTED.
  365. *
  366. * @throws ApiException If the API call fails.
  367. * @throws LogicException If the API call method has not been configured
  368. */
  369. public function delete()
  370. {
  371. if (is_null($this->deleteOperationMethod)) {
  372. throw new LogicException('The delete operation is not supported by this API');
  373. }
  374. $this->operationsCall($this->deleteOperationMethod, $this->getName(), $this->additionalArgs);
  375. $this->deleted = true;
  376. }
  377. /**
  378. * Get the metadata returned with the last proto response. If a metadata type was provided, then
  379. * the return value will be of that type - otherwise, the return value will be of type Any. If
  380. * no metadata object is available, returns null.
  381. *
  382. * @return mixed The metadata returned from the server in the last response.
  383. */
  384. public function getMetadata()
  385. {
  386. if (!$this->hasProtoResponse()) {
  387. return null;
  388. }
  389. if (!method_exists($this->lastProtoResponse, 'getMetadata')) {
  390. // The call to getMetadata is only for OnePlatform LROs, and is not
  391. // supported by other LRO GAPIC clients (e.g. Compute)
  392. return null;
  393. }
  394. /** @var Any|null $any */
  395. $any = $this->lastProtoResponse->getMetadata();
  396. if (is_null($this->metadataReturnType)) {
  397. return $any;
  398. }
  399. if (is_null($any)) {
  400. return null;
  401. }
  402. // @TODO: This is probably not doing anything and can be removed in the next release.
  403. if (is_null($any->getValue())) {
  404. return null;
  405. }
  406. $metadataReturnType = $this->metadataReturnType;
  407. /** @var Message $metadata */
  408. $metadata = new $metadataReturnType();
  409. $metadata->mergeFromString($any->getValue());
  410. return $metadata;
  411. }
  412. private function operationsCall($method, $name, array $additionalArgs)
  413. {
  414. $args = array_merge([$name], $additionalArgs);
  415. return call_user_func_array([$this->operationsClient, $method], $args);
  416. }
  417. private function canHaveResult()
  418. {
  419. // The call to getResponse is only for OnePlatform LROs, and is not
  420. // supported by other LRO GAPIC clients (e.g. Compute)
  421. return method_exists($this->lastProtoResponse, 'getResponse');
  422. }
  423. private function hasErrors()
  424. {
  425. if (!$this->hasProtoResponse()) {
  426. return false;
  427. }
  428. if (method_exists($this->lastProtoResponse, 'getError')) {
  429. return !empty($this->lastProtoResponse->getError());
  430. }
  431. if ($this->operationErrorCodeMethod) {
  432. $errorCode = call_user_func([$this->lastProtoResponse, $this->operationErrorCodeMethod]);
  433. return !empty($errorCode);
  434. }
  435. // This should never happen unless an API is misconfigured
  436. throw new LogicException('Unable to determine operation error status for this service');
  437. }
  438. private function hasProtoResponse()
  439. {
  440. return !is_null($this->lastProtoResponse);
  441. }
  442. }