1# -*- coding: utf-8 -*- #
2# Copyright 2021 Google LLC. All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#    http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15"""A library used to interact with Operations objects."""
16# TODO(b/73491568) Refactor to use api_lib.util.waiter
17
18from __future__ import absolute_import
19from __future__ import division
20from __future__ import unicode_literals
21
22from googlecloudsdk.api_lib.functions.v1 import exceptions
23from googlecloudsdk.core.console import progress_tracker as console_progress_tracker
24from googlecloudsdk.core.util import encoding
25from googlecloudsdk.core.util import retry
26
27MAX_WAIT_MS = 1820000
28WAIT_CEILING_MS = 2000
29SLEEP_MS = 1000
30
31
32def OperationErrorToString(error):
33  """Returns a human readable string representation from the operation.
34
35  Args:
36    error: A string representing the raw json of the operation error.
37
38  Returns:
39    A human readable string representation of the error.
40  """
41  return 'OperationError: code={0}, message={1}'.format(
42      error.code, encoding.Decode(error.message))
43
44
45# TODO(b/130604453): Remove try_set_invoker option.
46def _GetOperationStatus(client, get_request,
47                        progress_tracker=None, try_set_invoker=None,
48                        on_every_poll=None):
49  """Helper function for getting the status of an operation.
50
51  Args:
52    client: The client used to make requests.
53    get_request: A GetOperationRequest message.
54    progress_tracker: progress_tracker.ProgressTracker, A reference for the
55        progress tracker to tick, in case this function is used in a Retryer.
56    try_set_invoker: function to try setting invoker, see above TODO.
57    on_every_poll: list of functions to execute every time we poll.
58                   Functions should take in Operation as an argument.
59
60  Returns:
61    True if the operation succeeded without error.
62    False if the operation is not yet done.
63
64  Raises:
65    FunctionsError: If the operation is finished with error.
66  """
67  if try_set_invoker:
68    try_set_invoker()
69  if progress_tracker:
70    progress_tracker.Tick()
71  op = client.operations.Get(get_request)
72  if op.error:
73    raise exceptions.FunctionsError(OperationErrorToString(op.error))
74  if on_every_poll:
75    for function in on_every_poll:
76      function(op)
77  return op.done
78
79
80# TODO(b/139026575): Remove try_set_invoker option.
81def _WaitForOperation(client, get_request, message, try_set_invoker=None,
82                      on_every_poll=None):
83  """Wait for an operation to complete.
84
85  No operation is done instantly. Wait for it to finish following this logic:
86  * we wait 1s (jitter is also 1s)
87  * we query service
88  * if the operation is not finished we loop to first point
89  * wait limit is 1820s - if we get to that point it means something is wrong
90        and we can throw an exception
91
92  Args:
93    client:  The client used to make requests.
94    get_request: A GetOperationRequest message.
95    message: str, The string to print while polling.
96    try_set_invoker: function to try setting invoker, see above TODO.
97    on_every_poll: list of functions to execute every time we poll.
98                   Functions should take in Operation as an argument.
99
100  Returns:
101    True if the operation succeeded without error.
102
103  Raises:
104    FunctionsError: If the operation takes more than 1820s.
105  """
106
107  with console_progress_tracker.ProgressTracker(message, autotick=False) as pt:
108    # This is actually linear retryer.
109    retryer = retry.Retryer(exponential_sleep_multiplier=1,
110                            max_wait_ms=MAX_WAIT_MS,
111                            wait_ceiling_ms=WAIT_CEILING_MS)
112    try:
113      retryer.RetryOnResult(_GetOperationStatus,
114                            [client, get_request],
115                            {
116                                'progress_tracker': pt,
117                                'try_set_invoker': try_set_invoker,
118                                'on_every_poll': on_every_poll
119                            },
120                            should_retry_if=None,
121                            sleep_ms=SLEEP_MS)
122    except retry.WaitException:
123      raise exceptions.FunctionsError(
124          'Operation {0} is taking too long'.format(get_request.name))
125
126
127def Wait(operation, messages, client, notice=None, try_set_invoker=None,
128         on_every_poll=None):
129  """Initialize waiting for operation to finish.
130
131  Generate get request based on the operation and wait for an operation
132  to complete.
133
134  Args:
135    operation: The operation which we are waiting for.
136    messages: GCF messages module.
137    client: GCF client module.
138    notice: str, displayed when waiting for the operation to finish.
139    try_set_invoker: function to try setting invoker, see above TODO.
140    on_every_poll: list of functions to execute every time we poll.
141                   Functions should take in Operation as an argument.
142
143  Raises:
144    FunctionsError: If the operation takes more than 620s.
145  """
146  if notice is None:
147    notice = 'Waiting for operation to finish'
148  request = messages.CloudfunctionsOperationsGetRequest()
149  request.name = operation.name
150  _WaitForOperation(client, request, notice, try_set_invoker,
151                    on_every_poll)
152