1# -*- coding: utf-8 -*- # 2# Copyright 2015 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"""Common utility functions for sql operations.""" 16 17from __future__ import absolute_import 18from __future__ import division 19from __future__ import unicode_literals 20 21import time 22 23from apitools.base.py import exceptions as base_exceptions 24 25from googlecloudsdk.api_lib.sql import exceptions 26from googlecloudsdk.core.console import progress_tracker as console_progress_tracker 27from googlecloudsdk.core.util import retry 28 29_MS_PER_SECOND = 1000 30# Ten mins, based off of the max time it usually takes to create a SQL instance. 31_INSTANCE_CREATION_TIMEOUT_SECONDS = 600 32 33 34class _BaseOperations(object): 35 """Common utility functions for sql operations.""" 36 37 _PRE_START_SLEEP_SEC = 1 38 _INITIAL_SLEEP_MS = 2000 39 _WAIT_CEILING_MS = 20000 40 _HTTP_MAX_RETRY_MS = 2000 41 42 @classmethod 43 def WaitForOperation(cls, 44 sql_client, 45 operation_ref, 46 message, 47 max_wait_seconds=_INSTANCE_CREATION_TIMEOUT_SECONDS): 48 """Wait for a Cloud SQL operation to complete. 49 50 No operation is done instantly. Wait for it to finish following this logic: 51 First wait 1s, then query, then retry waiting exponentially more from 2s. 52 We want to limit to 20s between retries to maintain some responsiveness. 53 Finally, we want to limit the whole process to a conservative 300s. If we 54 get to that point it means something is wrong and we can throw an exception. 55 56 Args: 57 sql_client: apitools.BaseApiClient, The client used to make requests. 58 operation_ref: resources.Resource, A reference for the operation to poll. 59 message: str, The string to print while polling. 60 max_wait_seconds: integer or None, the number of seconds before the 61 poller times out. 62 63 Returns: 64 Operation: The polled operation. 65 66 Raises: 67 OperationError: If the operation has an error code, is in UNKNOWN state, 68 or if the operation takes more than max_wait_seconds when a value is 69 specified. 70 """ 71 72 def ShouldRetryFunc(result, state): 73 # In case of HttpError, retry for up to _HTTP_MAX_RETRY_MS at most. 74 if isinstance(result, base_exceptions.HttpError): 75 if state.time_passed_ms > _BaseOperations._HTTP_MAX_RETRY_MS: 76 raise result 77 return True 78 # In case of other Exceptions, raise them immediately. 79 if isinstance(result, Exception): 80 raise result 81 # Otherwise let the retryer do it's job until the Operation is done. 82 is_operation_done = result.status == sql_client.MESSAGES_MODULE.Operation.StatusValueValuesEnum.DONE 83 return not is_operation_done 84 85 # Set the max wait time. 86 max_wait_ms = None 87 if max_wait_seconds: 88 max_wait_ms = max_wait_seconds * _MS_PER_SECOND 89 with console_progress_tracker.ProgressTracker( 90 message, autotick=False) as pt: 91 time.sleep(_BaseOperations._PRE_START_SLEEP_SEC) 92 retryer = retry.Retryer( 93 exponential_sleep_multiplier=2, 94 max_wait_ms=max_wait_ms, 95 wait_ceiling_ms=_BaseOperations._WAIT_CEILING_MS) 96 try: 97 return retryer.RetryOnResult( 98 cls.GetOperation, [sql_client, operation_ref], 99 {'progress_tracker': pt}, 100 should_retry_if=ShouldRetryFunc, 101 sleep_ms=_BaseOperations._INITIAL_SLEEP_MS) 102 except retry.WaitException: 103 raise exceptions.OperationError( 104 ('Operation {0} is taking longer than expected. You can continue ' 105 'waiting for the operation by running `{1}`').format( 106 operation_ref, cls.GetOperationWaitCommand(operation_ref))) 107 108 109class OperationsV1Beta4(_BaseOperations): 110 """Common utility functions for sql operations V1Beta4.""" 111 112 @staticmethod 113 def GetOperation(sql_client, operation_ref, progress_tracker=None): 114 """Helper function for getting the status of an operation for V1Beta4 API. 115 116 Args: 117 sql_client: apitools.BaseApiClient, The client used to make requests. 118 operation_ref: resources.Resource, A reference for the operation to poll. 119 progress_tracker: progress_tracker.ProgressTracker, A reference for the 120 progress tracker to tick, in case this function is used in a Retryer. 121 122 Returns: 123 Operation: if the operation succeeded without error or is not yet done. 124 OperationError: If the operation has an error code or is in UNKNOWN state. 125 Exception: Any other exception that can occur when calling Get 126 """ 127 128 if progress_tracker: 129 progress_tracker.Tick() 130 try: 131 op = sql_client.operations.Get( 132 sql_client.MESSAGES_MODULE.SqlOperationsGetRequest( 133 project=operation_ref.project, operation=operation_ref.operation)) 134 except Exception as e: # pylint:disable=broad-except 135 # Since we use this function in a retryer.RetryOnResult block, where we 136 # retry for different exceptions up to different amounts of time, we 137 # have to catch all exceptions here and return them. 138 return e 139 if op.error and op.error.errors: 140 error_object = op.error.errors[0] 141 # If there's an error message to show, show it in addition to the code. 142 error = '[{}]'.format(error_object.code) 143 if error_object.message: 144 error += ' ' + error_object.message 145 return exceptions.OperationError(error) 146 if op.status == sql_client.MESSAGES_MODULE.Operation.StatusValueValuesEnum.SQL_OPERATION_STATUS_UNSPECIFIED: 147 return exceptions.OperationError(op.status) 148 return op 149 150 @staticmethod 151 def GetOperationWaitCommand(operation_ref): 152 return 'gcloud beta sql operations wait --project {0} {1}'.format( 153 operation_ref.project, operation_ref.operation) 154