1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275
|
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require "gapic/common/error_codes"
module Gapic
module Common
##
# Gapic Common retry policy base class.
#
class RetryPolicy
# @return [Numeric] Default initial delay in seconds.
DEFAULT_INITIAL_DELAY = 1
# @return [Numeric] Default maximum delay in seconds.
DEFAULT_MAX_DELAY = 15
# @return [Numeric] Default delay scaling factor for subsequent retry attempts.
DEFAULT_MULTIPLIER = 1.3
# @return [Array<String|Integer>] Default list of retry codes.
DEFAULT_RETRY_CODES = [].freeze
# @return [Numeric] Default timeout threshold value in seconds.
DEFAULT_TIMEOUT = 3600 # One hour
# @private
# @return [Numeric] Default random jitter added to delay in seconds.
DEFAULT_JITTER = 1.0
private_constant :DEFAULT_JITTER
##
# Create new Gapic::Common::RetryPolicy instance.
#
# @param initial_delay [Numeric] Initial delay in seconds.
# @param max_delay [Numeric] Maximum delay in seconds.
# @param multiplier [Numeric] The delay scaling factor for each subsequent retry attempt.
# @param retry_codes [Array<String|Integer>] List of retry codes.
# @param timeout [Numeric] Timeout threshold value in seconds.
# @param jitter [Numeric] Random jitter added to the delay in seconds.
#
def initialize initial_delay: nil, max_delay: nil, multiplier: nil, retry_codes: nil, timeout: nil, jitter: nil
raise ArgumentError, "jitter cannot be negative" if jitter&.negative?
# Instance values are set as `nil` to determine whether values are overriden from default.
@initial_delay = initial_delay
@max_delay = max_delay
@multiplier = multiplier
@retry_codes = convert_codes retry_codes
@timeout = timeout
@jitter = jitter
start!
end
# @return [Numeric] Initial delay in seconds.
def initial_delay
@initial_delay || DEFAULT_INITIAL_DELAY
end
# @return [Numeric] Maximum delay in seconds.
def max_delay
@max_delay || DEFAULT_MAX_DELAY
end
# @return [Numeric] The delay scaling factor for each subsequent retry attempt.
def multiplier
@multiplier || DEFAULT_MULTIPLIER
end
# @return [Array<Integer>] List of retry codes.
def retry_codes
@retry_codes || DEFAULT_RETRY_CODES
end
# @return [Numeric] Timeout threshold value in seconds.
def timeout
@timeout || DEFAULT_TIMEOUT
end
# @return [Numeric] Random jitter added to the delay in seconds.
def jitter
@jitter || DEFAULT_JITTER
end
##
# Returns a duplicate in a non-executing state, i.e. with the deadline
# and current delay reset.
#
# @return [RetryPolicy]
#
def dup
RetryPolicy.new initial_delay: @initial_delay,
max_delay: @max_delay,
multiplier: @multiplier,
retry_codes: @retry_codes,
timeout: @timeout,
jitter: @jitter
end
##
# Perform delay if and only if retriable.
#
# If positional argument `error` is provided, the retriable logic uses
# `retry_codes`. Otherwise, `timeout` is used.
#
# @return [Boolean] Whether the delay was executed.
#
def call error = nil
should_retry = error.nil? ? retry_with_deadline? : retry_error?(error)
return false unless should_retry
perform_delay!
end
alias perform_delay call
##
# Perform delay.
#
# @return [Boolean] Whether the delay was executed.
#
def perform_delay!
delay!
increment_delay!
@perform_delay_count += 1
true
end
##
# Current delay value in seconds.
#
# @return [Numeric] Time delay in seconds.
#
def delay
@delay
end
##
# Current number of times the delay has been performed
#
# @return [Integer]
#
def perform_delay_count
@perform_delay_count
end
##
# Start tracking the deadline and delay by initializing those values.
#
# This is normally done when the object is constructed, but it can be
# done explicitly in order to reinitialize the state in case this
# retry policy was created in the past or is being reused.
#
# @param mock_delay [boolean,Proc] if truthy, delays are "mocked",
# meaning they do not actually take time, but are measured as if they
# did, which is useful for tests. If set to a Proc, it will be called
# whenever a delay would happen, and passed the delay in seconds,
# also useful for testing.
#
def start! mock_delay: false
@mock_time = mock_delay ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : nil
@mock_delay_callback = mock_delay.respond_to?(:call) ? mock_delay : nil
@deadline = cur_time + timeout
@delay = initial_delay
@perform_delay_count = 0
self
end
##
# @private
# @return [Boolean] Whether this error should be retried.
#
def retry_error? error
(defined?(::GRPC) && error.is_a?(::GRPC::BadStatus) && retry_codes.include?(error.code)) ||
(error.respond_to?(:response_status) &&
retry_codes.include?(ErrorCodes.grpc_error_for(error.response_status)))
end
# @private
# @return [Boolean] Whether this policy should be retried based on the deadline.
def retry_with_deadline?
deadline > cur_time
end
##
# @private
# Apply default values to the policy object. This does not replace user-provided values,
# it only overrides empty values.
#
# @param retry_policy [Hash] The policy for error retry. Keys must match the arguments for
# {Gapic::Common::RetryPolicy.new}.
#
def apply_defaults retry_policy
return unless retry_policy.is_a? Hash
@retry_codes ||= convert_codes retry_policy[:retry_codes]
@initial_delay ||= retry_policy[:initial_delay]
@multiplier ||= retry_policy[:multiplier]
@max_delay ||= retry_policy[:max_delay]
@jitter ||= retry_policy[:jitter]
self
end
# @private Equality test
def eql? other
other.is_a?(RetryPolicy) &&
other.initial_delay == initial_delay &&
other.max_delay == max_delay &&
other.multiplier == multiplier &&
other.retry_codes == retry_codes &&
other.timeout == timeout &&
other.jitter == jitter
end
alias == eql?
# @private Hash code
def hash
[initial_delay, max_delay, multiplier, retry_codes, timeout, jitter].hash
end
private
# @private
# Perform the currently calculated delay, adding a jitter.
#
# @return [Numeric] The performed delay.
def delay!
delay_to_perform = [delay + Kernel.rand(0.0..jitter), max_delay].min
if @mock_time
@mock_time += delay_to_perform
@mock_delay_callback&.call delay_to_perform
else
Kernel.sleep delay_to_perform
end
end
# @private
# @return [Numeric] The new delay (sans jitter) in seconds.
def increment_delay!
@delay = [delay * multiplier, max_delay].min
end
# @private
# @return [Numeric] The deadline for timeout-based policies.
def deadline
@deadline
end
# @private
# Mockable way to get time.
def cur_time
@mock_time || Process.clock_gettime(Process::CLOCK_MONOTONIC)
end
# @private
# @return [Array<Integer> Error codes converted to their respective integer values.
def convert_codes input_codes
return nil if input_codes.nil?
Array(input_codes).map do |obj|
case obj
when String
ErrorCodes::ERROR_STRING_MAPPING[obj]
when Integer
obj
end
end.compact
end
end
end
end
|