gapic-common 1.4.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +2 -5
- data/lib/gapic/common/retry_policy.rb +34 -0
- data/lib/gapic/common/version.rb +1 -1
- data/lib/gapic/logging_concerns.rb +4 -0
- data/lib/gapic/rest/client_stub.rb +13 -3
- data/lib/gapic/rest/error.rb +4 -1
- data/lib/gapic/rest/resumable_upload/core.rb +77 -0
- data/lib/gapic/rest/resumable_upload/data_types.rb +428 -0
- data/lib/gapic/rest/resumable_upload/driver/abridge.rb +168 -0
- data/lib/gapic/rest/resumable_upload/driver/retry_decider.rb +190 -0
- data/lib/gapic/rest/resumable_upload/driver/upload_log.rb +343 -0
- data/lib/gapic/rest/resumable_upload/driver.rb +855 -0
- data/lib/gapic/rest/resumable_upload/errors.rb +581 -0
- data/lib/gapic/rest/resumable_upload/events.rb +129 -0
- data/lib/gapic/rest/resumable_upload/instructions.rb +273 -0
- data/lib/gapic/rest/resumable_upload/retry_policies.rb +116 -0
- data/lib/gapic/rest/resumable_upload/rules.rb +1210 -0
- data/lib/gapic/rest/resumable_upload.rb +110 -0
- data/lib/gapic/rest.rb +2 -0
- data/lib/gapic/resumable_upload.rb +508 -0
- metadata +14 -1
|
@@ -0,0 +1,855 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 Google LLC
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
require "uri"
|
|
18
|
+
require "gapic/logging_concerns"
|
|
19
|
+
require "gapic/rest/error"
|
|
20
|
+
require "gapic/rest/resumable_upload/core"
|
|
21
|
+
require "gapic/rest/resumable_upload/data_types"
|
|
22
|
+
require "gapic/rest/resumable_upload/errors"
|
|
23
|
+
require "gapic/rest/resumable_upload/events"
|
|
24
|
+
require "gapic/rest/resumable_upload/instructions"
|
|
25
|
+
require "gapic/rest/resumable_upload/retry_policies"
|
|
26
|
+
require "gapic/rest/resumable_upload/driver/retry_decider"
|
|
27
|
+
require "gapic/rest/resumable_upload/driver/upload_log"
|
|
28
|
+
|
|
29
|
+
module Gapic
|
|
30
|
+
module Rest
|
|
31
|
+
module ResumableUpload
|
|
32
|
+
##
|
|
33
|
+
# @private
|
|
34
|
+
# Synchronous execution engine for the Resumable Upload Protocol.
|
|
35
|
+
# Coordinates HTTP network operations, stream buffering, monotonic deadlines,
|
|
36
|
+
# and delegates state transitions to Core.
|
|
37
|
+
#
|
|
38
|
+
# The outer tier of the three-tier design. All side effects live here; all protocol decisions live in
|
|
39
|
+
# {Rules}, which carries the state graph and the error category taxonomy. Every retry decision is made
|
|
40
|
+
# here by {RetryDecider}; `ClientStub` is handed a never-retry policy. See
|
|
41
|
+
# `design/resumable_upload/implementation-guide.md` section 2.7 for the buffer and stream position
|
|
42
|
+
# invariants, and section 6.3 for the deadline model.
|
|
43
|
+
#
|
|
44
|
+
# rubocop:disable Metrics/ClassLength
|
|
45
|
+
class Driver
|
|
46
|
+
include Gapic::LoggingConcerns
|
|
47
|
+
|
|
48
|
+
##
|
|
49
|
+
# @private
|
|
50
|
+
# Minimum assumed upload throughput in bytes per second (1 MB/s).
|
|
51
|
+
# @return [Integer]
|
|
52
|
+
MIN_ASSUMED_THROUGHPUT = 1_048_576
|
|
53
|
+
|
|
54
|
+
##
|
|
55
|
+
# @private
|
|
56
|
+
# Default base timeout in seconds (1 hour).
|
|
57
|
+
# @return [Integer]
|
|
58
|
+
BASE_TIMEOUT = 3_600
|
|
59
|
+
|
|
60
|
+
##
|
|
61
|
+
# @private
|
|
62
|
+
# Retry policy handed to `ClientStub` for every request, so that each `ClientStub` call is exactly one
|
|
63
|
+
# attempt and {RetryDecider} makes every retry decision. `Gapic::CallOptions` accepts a Proc policy.
|
|
64
|
+
# @return [Proc]
|
|
65
|
+
CLIENT_STUB_NO_RETRY = ->(_error) { false }
|
|
66
|
+
|
|
67
|
+
# @private
|
|
68
|
+
# @return [Core]
|
|
69
|
+
attr_reader :core
|
|
70
|
+
|
|
71
|
+
##
|
|
72
|
+
# @private
|
|
73
|
+
# Returns a {ResumeHandle} representing the current upload session parameters.
|
|
74
|
+
# Reading this property mid-run provides a best-effort snapshot of the current session state.
|
|
75
|
+
# Completed uploads (`:success`), rejected uploads (`:rejected`), and cancelled uploads
|
|
76
|
+
# (`:cancelled`) are finalized and not resumable, returning `nil`.
|
|
77
|
+
#
|
|
78
|
+
# @return [ResumeHandle, nil] Resume handle if upload URL is established and resumable, or nil
|
|
79
|
+
def resume_handle
|
|
80
|
+
Rules.resume_handle_from @core.state
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
##
|
|
84
|
+
# @private
|
|
85
|
+
# Returns the raw upload session URL from protocol state, regardless of lifecycle status.
|
|
86
|
+
#
|
|
87
|
+
# @return [String, nil] Session upload URL if established, or nil
|
|
88
|
+
def upload_url
|
|
89
|
+
@core.state.upload_url
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
##
|
|
93
|
+
# @private
|
|
94
|
+
# Initializes a new Resumable Upload Driver.
|
|
95
|
+
#
|
|
96
|
+
# @param client_stub [Gapic::Rest::ClientStub] Underlying REST client stub
|
|
97
|
+
# @param config [StartUploadConfig, ResumeUploadConfig] Configuration for this upload session
|
|
98
|
+
# @param core [Core, nil] Optional Core state machine (defaults to new Core with config)
|
|
99
|
+
# @param logger [Logger, nil] Optional logger override
|
|
100
|
+
# @param method_name [String, nil] RPC name this upload was started from, prefixed onto the
|
|
101
|
+
# per-request logging names (`"create_media_upload.start"`, `"create_media_upload.upload"`, and
|
|
102
|
+
# so on). Defaults to `"ResumableUpload"`.
|
|
103
|
+
def initialize client_stub:, config:, core: nil, logger: nil, method_name: nil
|
|
104
|
+
@client_stub = client_stub
|
|
105
|
+
@config = config
|
|
106
|
+
@core = core || Core.new(config)
|
|
107
|
+
@buffer = "".b
|
|
108
|
+
@buffer_start_offset = 0
|
|
109
|
+
@method_name_prefix = method_name || "ResumableUpload"
|
|
110
|
+
|
|
111
|
+
endpoint = client_stub.respond_to?(:endpoint) ? client_stub.endpoint : nil
|
|
112
|
+
setup_logging logger: logger || (client_stub.respond_to?(:logger) ? client_stub.logger : nil),
|
|
113
|
+
system_name: "gapic-common",
|
|
114
|
+
service: "ResumableUpload",
|
|
115
|
+
endpoint: endpoint,
|
|
116
|
+
client_id: client_stub.object_id
|
|
117
|
+
@upload_log = UploadLog.new stub_logger, upload_id: "unstarted"
|
|
118
|
+
|
|
119
|
+
# Only an initiating run carries a start policy; a resumed run issues no initiation request.
|
|
120
|
+
configured_start_policy = config.is_a?(StartUploadConfig) ? config.start_retry_policy : nil
|
|
121
|
+
@start_retry_policy = resolve_retry_policy configured_start_policy, RetryPolicies::START_DEFAULTS
|
|
122
|
+
|
|
123
|
+
@control_plane_retry_policy = resolve_retry_policy config.control_plane_retry_policy,
|
|
124
|
+
RetryPolicies::CONTROL_PLANE_DEFAULTS
|
|
125
|
+
@data_plane_retry_policy = resolve_retry_policy config.data_plane_retry_policy,
|
|
126
|
+
RetryPolicies::DATA_PLANE_DEFAULTS
|
|
127
|
+
# Started copy of the control plane policy for the open recovery episode; see #execute_send_query.
|
|
128
|
+
@recovery_policy = nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
##
|
|
132
|
+
# @private
|
|
133
|
+
# Default retry policy for session initiation requests (start).
|
|
134
|
+
#
|
|
135
|
+
# @return [Gapic::Common::RetryPolicy]
|
|
136
|
+
def self.default_start_retry_policy
|
|
137
|
+
RetryPolicies.default_start
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
##
|
|
141
|
+
# @private
|
|
142
|
+
# Default retry policy for control plane requests (query, cancel).
|
|
143
|
+
#
|
|
144
|
+
# @return [Gapic::Common::RetryPolicy]
|
|
145
|
+
def self.default_control_plane_retry_policy
|
|
146
|
+
RetryPolicies.default_control_plane
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
##
|
|
150
|
+
# @private
|
|
151
|
+
# Default retry policy for data plane requests (upload, finalize).
|
|
152
|
+
#
|
|
153
|
+
# @return [Gapic::Common::RetryPolicy]
|
|
154
|
+
def self.default_data_plane_retry_policy
|
|
155
|
+
RetryPolicies.default_data_plane
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
##
|
|
159
|
+
# @private
|
|
160
|
+
# Executes event loop until terminal state.
|
|
161
|
+
# Establishes a guaranteed monotonic deadline at the start of execution
|
|
162
|
+
# so the upload cannot stall indefinitely.
|
|
163
|
+
#
|
|
164
|
+
# Enforces the trampoline loop invariant: each dispatched instruction batch
|
|
165
|
+
# is validated by {#validate_batch} before any instruction executes, ensuring it
|
|
166
|
+
# produces either a single continuation event or terminates the session
|
|
167
|
+
# (via {Instruction::TerminateSuccess} or {Instruction::TerminateFailure}).
|
|
168
|
+
# Side-effect instructions ({Instruction::NotifyProgress},
|
|
169
|
+
# {Instruction::RealignBuffer}) explicitly return `nil` by construction,
|
|
170
|
+
# so only {Instruction::FillBuffer} and `Send*` instructions produce
|
|
171
|
+
# continuation events.
|
|
172
|
+
#
|
|
173
|
+
# @return [String, nil] Final response body
|
|
174
|
+
def run
|
|
175
|
+
@upload_log = UploadLog.new stub_logger, upload_id: LoggingConcerns.random_uuid4
|
|
176
|
+
@deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + resolve_timeout
|
|
177
|
+
pending_event = initial_event
|
|
178
|
+
|
|
179
|
+
loop do
|
|
180
|
+
instructions = dispatch_event pending_event
|
|
181
|
+
|
|
182
|
+
if deadline_exceeded? && !terminal_instructions?(instructions)
|
|
183
|
+
instructions = dispatch_event Event::GlobalDeadlineExceeded.new
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
pending_event, terminal_result = execute_batch instructions
|
|
187
|
+
return terminal_result if pending_event.nil?
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
private
|
|
192
|
+
|
|
193
|
+
##
|
|
194
|
+
# @private
|
|
195
|
+
# Executes an instruction batch and enforces the single-continuation-event invariant.
|
|
196
|
+
#
|
|
197
|
+
# @param instructions [Array<Object>] Emitted instructions
|
|
198
|
+
# @return [Array<Object, nil>] Tuple of [pending_event, terminal_result]
|
|
199
|
+
#
|
|
200
|
+
def execute_batch instructions
|
|
201
|
+
recipe = @core.last_decision&.recipe
|
|
202
|
+
validate_batch instructions, recipe
|
|
203
|
+
|
|
204
|
+
pending_event = nil
|
|
205
|
+
instructions.each do |instruction|
|
|
206
|
+
result = dispatch_instruction instruction
|
|
207
|
+
return [nil, result] if instruction.is_a? Instruction::TerminateSuccess
|
|
208
|
+
pending_event = result if Instruction::CONTINUATION.any? { |klass| instruction.is_a? klass }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
unless pending_event_type? pending_event
|
|
212
|
+
raise InternalError,
|
|
213
|
+
"Resumable upload internal error: recipe :#{recipe} continuation instruction " \
|
|
214
|
+
"returned #{pending_event.class} instead of an event"
|
|
215
|
+
end
|
|
216
|
+
[pending_event, nil]
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
##
|
|
220
|
+
# @private
|
|
221
|
+
# Validates that an instruction batch satisfies the trampoline invariant before execution.
|
|
222
|
+
#
|
|
223
|
+
# @param instructions [Array<Object>] Emitted instructions
|
|
224
|
+
# @param recipe [Symbol, nil] Recipe symbol from last decision
|
|
225
|
+
# @return [void]
|
|
226
|
+
# @raise [InternalError] If the batch is malformed or contains an unclassified instruction
|
|
227
|
+
#
|
|
228
|
+
def validate_batch instructions, recipe
|
|
229
|
+
continuation = 0
|
|
230
|
+
terminal = 0
|
|
231
|
+
instructions.each do |instruction|
|
|
232
|
+
case instruction
|
|
233
|
+
when *Instruction::CONTINUATION then continuation += 1
|
|
234
|
+
when *Instruction::TERMINAL then terminal += 1
|
|
235
|
+
when *Instruction::SIDE_EFFECT then nil
|
|
236
|
+
else
|
|
237
|
+
raise InternalError,
|
|
238
|
+
"Resumable upload internal error: recipe :#{recipe} emitted " \
|
|
239
|
+
"unclassified instruction #{instruction.class}"
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
return if continuation + terminal == 1
|
|
243
|
+
|
|
244
|
+
raise InternalError, batch_shape_message(recipe, continuation, terminal)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
##
|
|
248
|
+
# @private
|
|
249
|
+
# Formats diagnostic error message for a malformed instruction batch.
|
|
250
|
+
#
|
|
251
|
+
# @param recipe [Symbol, nil] Recipe symbol from last decision
|
|
252
|
+
# @param continuation [Integer] Number of continuation instructions
|
|
253
|
+
# @param terminal [Integer] Number of terminal instructions
|
|
254
|
+
# @return [String] Error message
|
|
255
|
+
#
|
|
256
|
+
def batch_shape_message recipe, continuation, terminal
|
|
257
|
+
reason = if continuation.zero? && terminal.zero?
|
|
258
|
+
"produced no continuation event and did not terminate"
|
|
259
|
+
elsif continuation > 1 && terminal.zero?
|
|
260
|
+
"produced multiple continuation events"
|
|
261
|
+
elsif continuation.zero? && terminal > 1
|
|
262
|
+
"produced multiple terminal instructions"
|
|
263
|
+
else
|
|
264
|
+
"produced both a continuation event and a terminal instruction"
|
|
265
|
+
end
|
|
266
|
+
"Resumable upload internal error: recipe :#{recipe} #{reason}"
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
##
|
|
270
|
+
# @private
|
|
271
|
+
# Dispatches an event to Core, logging decisions and transitions.
|
|
272
|
+
#
|
|
273
|
+
# @param event [Object] Input event
|
|
274
|
+
# @return [Array<Object>] Emitted instructions
|
|
275
|
+
#
|
|
276
|
+
def dispatch_event event
|
|
277
|
+
instructions = begin
|
|
278
|
+
@core.dispatch event
|
|
279
|
+
rescue InvalidTransitionError => e
|
|
280
|
+
@upload_log.unmatched_transition @core.state, event, e
|
|
281
|
+
raise
|
|
282
|
+
end
|
|
283
|
+
@upload_log.decision @core.last_decision
|
|
284
|
+
@upload_log.lifecycle @core.last_decision, @config
|
|
285
|
+
instructions
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
##
|
|
289
|
+
# @private
|
|
290
|
+
# Checks whether an instruction execution result represents a pending event.
|
|
291
|
+
#
|
|
292
|
+
# @param obj [Object] Execution result
|
|
293
|
+
# @return [Boolean]
|
|
294
|
+
#
|
|
295
|
+
def pending_event_type? obj
|
|
296
|
+
obj.is_a?(Event::ChunkRead) || obj.is_a?(Event::HttpResponse) ||
|
|
297
|
+
obj.is_a?(Event::RequestFailed) || obj.is_a?(Event::GlobalDeadlineExceeded)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
##
|
|
301
|
+
# @private
|
|
302
|
+
# Executes an instruction emitted by the state machine.
|
|
303
|
+
#
|
|
304
|
+
# @param instruction [Object] Instruction to execute
|
|
305
|
+
# @return [Object, nil] Resulting event or terminal response
|
|
306
|
+
#
|
|
307
|
+
def dispatch_instruction instruction
|
|
308
|
+
case instruction
|
|
309
|
+
when Instruction::NotifyProgress then execute_notify_progress instruction
|
|
310
|
+
when Instruction::RealignBuffer then execute_realign_buffer instruction
|
|
311
|
+
when Instruction::FillBuffer then execute_fill_buffer instruction
|
|
312
|
+
when Instruction::SendStart then execute_send_start instruction
|
|
313
|
+
when Instruction::SendChunk then execute_send_chunk instruction
|
|
314
|
+
when Instruction::SendFinalize then execute_send_finalize instruction
|
|
315
|
+
when Instruction::SendQuery then execute_send_query instruction
|
|
316
|
+
when Instruction::SendCancel then execute_send_cancel instruction
|
|
317
|
+
when Instruction::TerminateSuccess
|
|
318
|
+
instruction.response.body
|
|
319
|
+
when Instruction::TerminateFailure then raise instruction.error
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
##
|
|
324
|
+
# @private
|
|
325
|
+
# Resolves a configured retry policy or applies defaults.
|
|
326
|
+
#
|
|
327
|
+
# @param value [Gapic::Common::RetryPolicy, Hash, nil] Configured policy or overrides
|
|
328
|
+
# @param defaults [Hash] Default policy configuration
|
|
329
|
+
# @return [Gapic::Common::RetryPolicy]
|
|
330
|
+
#
|
|
331
|
+
def resolve_retry_policy value, defaults
|
|
332
|
+
case value
|
|
333
|
+
when Gapic::Common::RetryPolicy
|
|
334
|
+
value
|
|
335
|
+
when Hash
|
|
336
|
+
Gapic::Common::RetryPolicy.new(**value).apply_defaults(defaults)
|
|
337
|
+
when nil
|
|
338
|
+
Gapic::Common::RetryPolicy.new(**defaults)
|
|
339
|
+
else
|
|
340
|
+
raise ArgumentError, "Expected RetryPolicy, Hash, or nil, got #{value.class}"
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
##
|
|
345
|
+
# @private
|
|
346
|
+
# Determines the initial event to dispatch based on configuration class.
|
|
347
|
+
#
|
|
348
|
+
# @return [Event::StartUpload, Event::ResumeUpload]
|
|
349
|
+
def initial_event
|
|
350
|
+
if @config.is_a? ResumeUploadConfig
|
|
351
|
+
Event::ResumeUpload.new
|
|
352
|
+
else
|
|
353
|
+
Event::StartUpload.new
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
##
|
|
358
|
+
# @private
|
|
359
|
+
# Resolves the total upload deadline timeout in seconds.
|
|
360
|
+
#
|
|
361
|
+
# @return [Numeric] Timeout in seconds
|
|
362
|
+
#
|
|
363
|
+
def resolve_timeout
|
|
364
|
+
return @config.timeout if @config.timeout&.positive?
|
|
365
|
+
|
|
366
|
+
# When timeout is unset, BASE_TIMEOUT (1 hour) acts as a floor so small uploads still get
|
|
367
|
+
# a full hour while large uploads scale past it at MIN_ASSUMED_THROUGHPUT (1 MiB/s).
|
|
368
|
+
if @config.upload_size
|
|
369
|
+
[@config.upload_size.fdiv(MIN_ASSUMED_THROUGHPUT), BASE_TIMEOUT].max
|
|
370
|
+
else
|
|
371
|
+
BASE_TIMEOUT
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
##
|
|
376
|
+
# @private
|
|
377
|
+
# Computes the per-attempt timeout: whatever is left of the global monotonic deadline, and of the
|
|
378
|
+
# command's own retry budget when the command has one.
|
|
379
|
+
#
|
|
380
|
+
# @param retry_policy [Gapic::Common::RetryPolicy, nil] Target command retry policy
|
|
381
|
+
# @param started_at [Numeric, nil] Monotonic time the command started; `nil` grants the full budget
|
|
382
|
+
# @return [Numeric] Effective per-attempt timeout
|
|
383
|
+
#
|
|
384
|
+
def request_timeout retry_policy, started_at: nil
|
|
385
|
+
remaining = if @deadline
|
|
386
|
+
[@deadline - monotonic_now, 0].max
|
|
387
|
+
else
|
|
388
|
+
resolve_timeout
|
|
389
|
+
end
|
|
390
|
+
return remaining unless retry_policy&.timeout
|
|
391
|
+
|
|
392
|
+
elapsed = started_at ? monotonic_now - started_at : 0
|
|
393
|
+
(retry_policy.timeout - elapsed).clamp 0, remaining
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
##
|
|
397
|
+
# @private
|
|
398
|
+
# Checks whether the monotonic clock has exceeded the session deadline.
|
|
399
|
+
#
|
|
400
|
+
# @return [Boolean]
|
|
401
|
+
#
|
|
402
|
+
def deadline_exceeded?
|
|
403
|
+
return false unless @deadline
|
|
404
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) > @deadline
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
##
|
|
408
|
+
# @private
|
|
409
|
+
# Determines whether the instruction list contains a terminal instruction.
|
|
410
|
+
#
|
|
411
|
+
# @param instructions [Array<Object>] Instruction list
|
|
412
|
+
# @return [Boolean]
|
|
413
|
+
#
|
|
414
|
+
def terminal_instructions? instructions
|
|
415
|
+
instructions.any? do |i|
|
|
416
|
+
i.is_a?(Instruction::TerminateSuccess) || i.is_a?(Instruction::TerminateFailure)
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
##
|
|
421
|
+
# @private
|
|
422
|
+
# Invokes caller progress callback with snapshot.
|
|
423
|
+
#
|
|
424
|
+
# @param instruction [Instruction::NotifyProgress] Progress instruction
|
|
425
|
+
#
|
|
426
|
+
def execute_notify_progress instruction
|
|
427
|
+
@config.on_progress&.call instruction.progress
|
|
428
|
+
nil
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
##
|
|
432
|
+
# @private
|
|
433
|
+
# Realigns in-memory buffer and underlying stream to match server offset.
|
|
434
|
+
#
|
|
435
|
+
# @param instruction [Instruction::RealignBuffer] Realign instruction
|
|
436
|
+
#
|
|
437
|
+
def execute_realign_buffer instruction
|
|
438
|
+
server_offset = instruction.server_offset
|
|
439
|
+
if @config.upload_size && server_offset > @config.upload_size
|
|
440
|
+
raise StreamMismatchError.new(
|
|
441
|
+
"Server reported offset #{server_offset} exceeds total upload size #{@config.upload_size}",
|
|
442
|
+
resume_handle: resume_handle
|
|
443
|
+
)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
buffer_start = @buffer_start_offset
|
|
447
|
+
buffer_end = @buffer_start_offset + @buffer.bytesize
|
|
448
|
+
|
|
449
|
+
realign_case = if server_offset >= buffer_start && server_offset <= buffer_end
|
|
450
|
+
"within_buffer"
|
|
451
|
+
elsif server_offset < buffer_start
|
|
452
|
+
"rewind"
|
|
453
|
+
else
|
|
454
|
+
"fast_forward"
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
unseekable = realign_case == "rewind" && !@config.stream.respond_to?(:seek)
|
|
458
|
+
@upload_log.buffer_realign realign_case, server_offset: server_offset,
|
|
459
|
+
current_offset: buffer_start,
|
|
460
|
+
unseekable: unseekable
|
|
461
|
+
|
|
462
|
+
if server_offset >= buffer_start && server_offset <= buffer_end
|
|
463
|
+
realign_within_buffer server_offset
|
|
464
|
+
elsif server_offset < buffer_start
|
|
465
|
+
realign_rewind_stream server_offset
|
|
466
|
+
else
|
|
467
|
+
realign_fast_forward_stream server_offset, buffer_end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
nil
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
##
|
|
474
|
+
# @private
|
|
475
|
+
# Slices the in-memory buffer when server offset falls within current buffer range.
|
|
476
|
+
#
|
|
477
|
+
# @param server_offset [Integer] Target server offset
|
|
478
|
+
#
|
|
479
|
+
def realign_within_buffer server_offset
|
|
480
|
+
slice_index = server_offset - @buffer_start_offset
|
|
481
|
+
@buffer = @buffer.byteslice(slice_index..-1) || "".b
|
|
482
|
+
@buffer_start_offset = server_offset
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
##
|
|
486
|
+
# @private
|
|
487
|
+
# Rewinds seekable stream when server offset is before current buffer window.
|
|
488
|
+
#
|
|
489
|
+
# @param server_offset [Integer] Target server offset
|
|
490
|
+
# @raise [UnseekableStreamError] If stream does not respond to #seek
|
|
491
|
+
#
|
|
492
|
+
def realign_rewind_stream server_offset
|
|
493
|
+
unless @config.stream.respond_to? :seek
|
|
494
|
+
raise UnseekableStreamError.new(
|
|
495
|
+
"Cannot rewind unseekable stream to offset #{server_offset} (buffered from #{@buffer_start_offset})",
|
|
496
|
+
resume_handle: resume_handle
|
|
497
|
+
)
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
if @config.upload_size.nil? && @config.stream.respond_to?(:size) && server_offset > @config.stream.size
|
|
501
|
+
raise StreamMismatchError.new(
|
|
502
|
+
"Server reported offset #{server_offset} exceeds stream size #{@config.stream.size}",
|
|
503
|
+
resume_handle: resume_handle
|
|
504
|
+
)
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
@config.stream.seek server_offset
|
|
508
|
+
@buffer = "".b
|
|
509
|
+
@buffer_start_offset = server_offset
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
##
|
|
513
|
+
# @private
|
|
514
|
+
# Fast-forwards stream by seeking or discarding bytes.
|
|
515
|
+
#
|
|
516
|
+
# @param server_offset [Integer] Target server offset
|
|
517
|
+
# @param buffer_end [Integer] Current end offset of buffered data
|
|
518
|
+
#
|
|
519
|
+
def realign_fast_forward_stream server_offset, buffer_end
|
|
520
|
+
@buffer = "".b
|
|
521
|
+
if @config.stream.respond_to? :seek
|
|
522
|
+
if @config.upload_size.nil? && @config.stream.respond_to?(:size) && server_offset > @config.stream.size
|
|
523
|
+
raise StreamMismatchError.new(
|
|
524
|
+
"Server reported offset #{server_offset} exceeds stream size #{@config.stream.size}",
|
|
525
|
+
resume_handle: resume_handle
|
|
526
|
+
)
|
|
527
|
+
end
|
|
528
|
+
@config.stream.seek server_offset
|
|
529
|
+
else
|
|
530
|
+
needed_discard = server_offset - buffer_end
|
|
531
|
+
while needed_discard.positive?
|
|
532
|
+
chunk = @config.stream.read [needed_discard, 65_536].min
|
|
533
|
+
if chunk.nil? || chunk.empty?
|
|
534
|
+
raise StreamMismatchError.new(
|
|
535
|
+
"Stream encountered unexpected EOF during fast-forward to offset #{server_offset} " \
|
|
536
|
+
"(expected at least #{needed_discard} more bytes)",
|
|
537
|
+
resume_handle: resume_handle
|
|
538
|
+
)
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
needed_discard -= chunk.bytesize
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
@buffer_start_offset = server_offset
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
##
|
|
548
|
+
# @private
|
|
549
|
+
# Fills internal buffer from stream up to target byte size or EOF.
|
|
550
|
+
#
|
|
551
|
+
# @param instruction [Instruction::FillBuffer] FillBuffer instruction
|
|
552
|
+
# @return [Event::ChunkRead] Chunk read event
|
|
553
|
+
#
|
|
554
|
+
def execute_fill_buffer instruction
|
|
555
|
+
target = instruction.target_bytesize
|
|
556
|
+
eof = false
|
|
557
|
+
|
|
558
|
+
while @buffer.bytesize < target
|
|
559
|
+
bytes_needed = target - @buffer.bytesize
|
|
560
|
+
chunk = @config.stream.read bytes_needed
|
|
561
|
+
if chunk.nil? || chunk.empty?
|
|
562
|
+
eof = true
|
|
563
|
+
break
|
|
564
|
+
end
|
|
565
|
+
@buffer << chunk.b
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
Event::ChunkRead.new bytes_buffered: @buffer.bytesize, eof: eof
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
##
|
|
572
|
+
# @private
|
|
573
|
+
# Executes session initiation HTTP request.
|
|
574
|
+
#
|
|
575
|
+
# @param instruction [Instruction::SendStart] SendStart instruction
|
|
576
|
+
# @return [Event::HttpResponse, Event::RequestFailed, Event::GlobalDeadlineExceeded]
|
|
577
|
+
#
|
|
578
|
+
def execute_send_start instruction
|
|
579
|
+
make_post_request instruction.url, headers: start_headers(instruction), body: instruction.body,
|
|
580
|
+
retry_policy: @start_retry_policy.dup.start!,
|
|
581
|
+
method_name: "#{@method_name_prefix}.start"
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
##
|
|
585
|
+
# @private
|
|
586
|
+
# Builds initiation HTTP headers from instruction and config.
|
|
587
|
+
#
|
|
588
|
+
# Every header derived here is listed in {RESERVED_INITIAL_HEADERS}, and caller headers in
|
|
589
|
+
# that list are rejected when the config is built. The two sets are disjoint, so a plain merge
|
|
590
|
+
# cannot drop a driver header or duplicate one under a different casing.
|
|
591
|
+
#
|
|
592
|
+
# @param instruction [Instruction::SendStart] Start instruction
|
|
593
|
+
# @return [Hash<String, String>] HTTP request headers
|
|
594
|
+
#
|
|
595
|
+
def start_headers instruction
|
|
596
|
+
headers = { "X-Goog-Upload-Protocol" => "resumable", "X-Goog-Upload-Command" => "start" }
|
|
597
|
+
headers["X-Goog-Upload-Header-Content-Type"] = @config.content_type if @config.content_type
|
|
598
|
+
headers["X-Goog-Upload-Header-Content-Length"] = @config.upload_size.to_s if @config.upload_size
|
|
599
|
+
headers.merge instruction.headers || {}
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
##
|
|
603
|
+
# @private
|
|
604
|
+
# Transmits a buffered chunk over HTTP.
|
|
605
|
+
#
|
|
606
|
+
# @param instruction [Instruction::SendChunk] SendChunk instruction
|
|
607
|
+
# @return [Event::HttpResponse, Event::RequestFailed, Event::GlobalDeadlineExceeded]
|
|
608
|
+
#
|
|
609
|
+
def execute_send_chunk instruction
|
|
610
|
+
headers = {
|
|
611
|
+
"X-Goog-Upload-Command" => instruction.finalize ? "upload, finalize" : "upload",
|
|
612
|
+
"X-Goog-Upload-Offset" => instruction.offset.to_s,
|
|
613
|
+
"Content-Type" => @config.content_type || "application/octet-stream",
|
|
614
|
+
"Content-Length" => instruction.length.to_s
|
|
615
|
+
}
|
|
616
|
+
slice_index = instruction.offset - @buffer_start_offset
|
|
617
|
+
body = @buffer.byteslice slice_index, instruction.length
|
|
618
|
+
|
|
619
|
+
make_post_request instruction.url, headers: headers, body: body,
|
|
620
|
+
retry_policy: @data_plane_retry_policy.dup.start!, data_plane: true,
|
|
621
|
+
method_name: "#{@method_name_prefix}.upload"
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
##
|
|
625
|
+
# @private
|
|
626
|
+
# Sends a standalone finalize command over HTTP.
|
|
627
|
+
#
|
|
628
|
+
# @param instruction [Instruction::SendFinalize] SendFinalize instruction
|
|
629
|
+
# @return [Event::HttpResponse, Event::RequestFailed, Event::GlobalDeadlineExceeded]
|
|
630
|
+
#
|
|
631
|
+
def execute_send_finalize instruction
|
|
632
|
+
headers = {
|
|
633
|
+
"X-Goog-Upload-Command" => "finalize",
|
|
634
|
+
"X-Goog-Upload-Offset" => @core.state.offset.to_s,
|
|
635
|
+
"Content-Length" => "0"
|
|
636
|
+
}
|
|
637
|
+
make_post_request instruction.url, headers: headers, body: "",
|
|
638
|
+
retry_policy: @data_plane_retry_policy.dup.start!, data_plane: true,
|
|
639
|
+
method_name: "#{@method_name_prefix}.finalize"
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
##
|
|
643
|
+
# @private
|
|
644
|
+
# Sends an offset query command over HTTP.
|
|
645
|
+
#
|
|
646
|
+
# Every query runs under the recovery episode's retry policy, a started copy of the control plane
|
|
647
|
+
# policy that lives for the whole episode rather than for one command. {RetryDecider} draws the delay
|
|
648
|
+
# between attempts of this query from it, and a query that continues the episode
|
|
649
|
+
# (`instruction.backoff`) first waits for its next delay via `RetryPolicy#perform_delay!`. So the
|
|
650
|
+
# backoff grows across every re-send in the episode, and the control plane `timeout` budgets the
|
|
651
|
+
# episode rather than each query.
|
|
652
|
+
#
|
|
653
|
+
# `perform_delay!` is used rather than `RetryPolicy#call` because `call` stops sleeping once the
|
|
654
|
+
# policy's deadline has passed, which would let recovery re-query with no delay at all. Once `delay`
|
|
655
|
+
# reaches `max_delay` it stays there, so recovery then queries once per `max_delay` until the global
|
|
656
|
+
# deadline.
|
|
657
|
+
#
|
|
658
|
+
# @param instruction [Instruction::SendQuery] SendQuery instruction
|
|
659
|
+
# @return [Event::HttpResponse, Event::RequestFailed, Event::GlobalDeadlineExceeded]
|
|
660
|
+
#
|
|
661
|
+
def execute_send_query instruction
|
|
662
|
+
if instruction.backoff
|
|
663
|
+
return Event::GlobalDeadlineExceeded.new if deadline_exceeded?
|
|
664
|
+
@recovery_policy ||= @control_plane_retry_policy.dup.start!
|
|
665
|
+
@upload_log.recovery_backoff delay: @recovery_policy.delay,
|
|
666
|
+
attempt: @recovery_policy.perform_delay_count + 1
|
|
667
|
+
@recovery_policy.perform_delay!
|
|
668
|
+
else
|
|
669
|
+
@recovery_policy = @control_plane_retry_policy.dup.start!
|
|
670
|
+
end
|
|
671
|
+
headers = { "X-Goog-Upload-Command" => "query", "Content-Length" => "0" }
|
|
672
|
+
make_post_request instruction.url, headers: headers, body: "",
|
|
673
|
+
retry_policy: @recovery_policy,
|
|
674
|
+
method_name: "#{@method_name_prefix}.query"
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
##
|
|
678
|
+
# @private
|
|
679
|
+
# Sends a cancellation command over HTTP.
|
|
680
|
+
#
|
|
681
|
+
# @param instruction [Instruction::SendCancel] SendCancel instruction
|
|
682
|
+
# @return [Event::HttpResponse, Event::RequestFailed, Event::GlobalDeadlineExceeded]
|
|
683
|
+
#
|
|
684
|
+
def execute_send_cancel instruction
|
|
685
|
+
headers = { "X-Goog-Upload-Command" => "cancel", "Content-Length" => "0" }
|
|
686
|
+
make_post_request instruction.url, headers: headers, body: "",
|
|
687
|
+
retry_policy: @control_plane_retry_policy.dup.start!,
|
|
688
|
+
method_name: "#{@method_name_prefix}.cancel"
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
##
|
|
692
|
+
# @private
|
|
693
|
+
# Sends an HTTP POST request through the client stub, re-sending it for as long as {RetryDecider} says
|
|
694
|
+
# so, and converts the final outcome into an event.
|
|
695
|
+
#
|
|
696
|
+
# Every attempt is logged. The event for the last attempt is returned unchanged: this method only
|
|
697
|
+
# chooses whether to send again, never what an outcome means; {Rules} decides that.
|
|
698
|
+
#
|
|
699
|
+
# @param url [String] Target URL
|
|
700
|
+
# @param headers [Hash] Request headers
|
|
701
|
+
# @param body [String] Request body
|
|
702
|
+
# @param retry_policy [Gapic::Common::RetryPolicy, nil] Started retry policy; `nil` sends once. For
|
|
703
|
+
# `query` this is the recovery episode's policy, started before this command, so its deadline and
|
|
704
|
+
# delay carry over from earlier commands in the episode. `started_at` still bounds each attempt's
|
|
705
|
+
# timeout and the post-delay budget check by this command's own start.
|
|
706
|
+
# @param data_plane [Boolean] Whether the request transmits upload bytes (`upload`, `finalize`)
|
|
707
|
+
# @param method_name [String, nil] RPC method name for logging
|
|
708
|
+
# @return [Event::HttpResponse, Event::RequestFailed, Event::GlobalDeadlineExceeded]
|
|
709
|
+
#
|
|
710
|
+
def make_post_request url, headers:, body:, retry_policy:, data_plane: false, method_name: nil
|
|
711
|
+
decider = retry_policy && RetryDecider.new(retry_policy, data_plane: data_plane)
|
|
712
|
+
started_at = monotonic_now
|
|
713
|
+
attempt = 1
|
|
714
|
+
|
|
715
|
+
loop do
|
|
716
|
+
return Event::GlobalDeadlineExceeded.new if deadline_exceeded?
|
|
717
|
+
|
|
718
|
+
timeout = request_timeout retry_policy, started_at: started_at
|
|
719
|
+
outcome = attempt_post_request url, headers: headers, body: body, timeout: timeout,
|
|
720
|
+
method_name: method_name, attempt: attempt
|
|
721
|
+
# If the global deadline expired during the HTTP call (e.g. Net::HTTP connection or read timeout
|
|
722
|
+
# triggered by request_timeout reaching 0 at @deadline), emit GlobalDeadlineExceeded rather than
|
|
723
|
+
# Event::RequestFailed. Otherwise, in states like Recovery where Event::RequestFailed is immediately
|
|
724
|
+
# terminal, the state machine would raise the underlying transport error instead of DeadlineExceededError.
|
|
725
|
+
return Event::GlobalDeadlineExceeded.new if outcome.is_a?(Exception) && deadline_exceeded?
|
|
726
|
+
|
|
727
|
+
event = outcome_event outcome
|
|
728
|
+
return event unless decider&.retry?(outcome) && command_budget_left?(retry_policy, started_at)
|
|
729
|
+
|
|
730
|
+
attempt += 1
|
|
731
|
+
end
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
##
|
|
735
|
+
# @private
|
|
736
|
+
# Performs one `ClientStub` call, returning the error it raises instead of raising it.
|
|
737
|
+
#
|
|
738
|
+
# @param url [String] Target URL
|
|
739
|
+
# @param headers [Hash] Request headers
|
|
740
|
+
# @param body [String] Request body
|
|
741
|
+
# @param timeout [Numeric] Per-attempt timeout
|
|
742
|
+
# @param method_name [String, nil] RPC method name for logging
|
|
743
|
+
# @param attempt [Integer] 1-based attempt number, for logging
|
|
744
|
+
# @return [Object, StandardError] The client stub response, or the error it raised
|
|
745
|
+
#
|
|
746
|
+
def attempt_post_request url, headers:, body:, timeout:, method_name:, attempt:
|
|
747
|
+
options = { metadata: headers, retry_policy: CLIENT_STUB_NO_RETRY, timeout: timeout }
|
|
748
|
+
@upload_log.wire_send method: "POST", url: url, headers: headers,
|
|
749
|
+
start_attempt: attempt, body_size: body.to_s.bytesize, body: body
|
|
750
|
+
@client_stub.make_post_request uri: url, body: body, params: {}, options: options, method_name: method_name
|
|
751
|
+
rescue StandardError => e
|
|
752
|
+
e
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
##
|
|
756
|
+
# @private
|
|
757
|
+
# Converts one attempt's outcome into an event and logs it.
|
|
758
|
+
#
|
|
759
|
+
# @param outcome [Object, StandardError] Client stub response or raised error
|
|
760
|
+
# @return [Event::HttpResponse, Event::RequestFailed]
|
|
761
|
+
#
|
|
762
|
+
def outcome_event outcome
|
|
763
|
+
event = if outcome.is_a? Exception
|
|
764
|
+
rescue_request_error outcome
|
|
765
|
+
else
|
|
766
|
+
Event::HttpResponse.new status: outcome.status, headers: outcome.headers || {}, body: outcome.body
|
|
767
|
+
end
|
|
768
|
+
if event.is_a? Event::HttpResponse
|
|
769
|
+
@upload_log.wire_receive event
|
|
770
|
+
else
|
|
771
|
+
@upload_log.wire_failure event
|
|
772
|
+
end
|
|
773
|
+
event
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
##
|
|
777
|
+
# @private
|
|
778
|
+
# Whether the command's own retry budget has time left, checked after the backoff delay.
|
|
779
|
+
#
|
|
780
|
+
# `RetryPolicy#deadline` is private, so the budget is measured from when the command started.
|
|
781
|
+
#
|
|
782
|
+
# @param retry_policy [Gapic::Common::RetryPolicy] Command retry policy
|
|
783
|
+
# @param started_at [Numeric] Monotonic time the command started
|
|
784
|
+
# @return [Boolean]
|
|
785
|
+
#
|
|
786
|
+
def command_budget_left? retry_policy, started_at
|
|
787
|
+
monotonic_now - started_at < retry_policy.timeout
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
##
|
|
791
|
+
# @private
|
|
792
|
+
# Current monotonic clock reading.
|
|
793
|
+
#
|
|
794
|
+
# @return [Float]
|
|
795
|
+
#
|
|
796
|
+
def monotonic_now
|
|
797
|
+
Process.clock_gettime Process::CLOCK_MONOTONIC
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
##
|
|
801
|
+
# @private
|
|
802
|
+
# Converts client stub transport exceptions into canonical events.
|
|
803
|
+
#
|
|
804
|
+
# Classified by {RetryDecider.failure_kind}, the same function that decides how the error is retried.
|
|
805
|
+
#
|
|
806
|
+
# @param err [StandardError] Rescued transport error
|
|
807
|
+
# @return [Event::HttpResponse, Event::RequestFailed]
|
|
808
|
+
#
|
|
809
|
+
def rescue_request_error err
|
|
810
|
+
return rescue_faraday_error err if err.is_a? Faraday::Error
|
|
811
|
+
|
|
812
|
+
case RetryDecider.failure_kind err
|
|
813
|
+
when :status
|
|
814
|
+
Event::HttpResponse.new status: err.status_code, headers: err.headers || {}, body: err.message,
|
|
815
|
+
error: err
|
|
816
|
+
when :timeout
|
|
817
|
+
Event::RequestFailed.new kind: :timeout, message: err.message, source_error: err
|
|
818
|
+
when :connection_failed
|
|
819
|
+
Event::RequestFailed.new kind: :connection_failed, message: err.message, source_error: err
|
|
820
|
+
else
|
|
821
|
+
# Not a transport error at all, e.g. the authorization middleware failing to refresh a token. Nothing
|
|
822
|
+
# reached the server, so this is neither a connection failure nor a reason to recover.
|
|
823
|
+
Event::RequestFailed.new kind: :unknown, message: err.message, source_error: err
|
|
824
|
+
end
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
##
|
|
828
|
+
# @private
|
|
829
|
+
# Converts Faraday client exceptions into canonical events.
|
|
830
|
+
#
|
|
831
|
+
# @param err [Faraday::Error] Rescued Faraday error
|
|
832
|
+
# @return [Event::HttpResponse, Event::RequestFailed]
|
|
833
|
+
#
|
|
834
|
+
def rescue_faraday_error err
|
|
835
|
+
case RetryDecider.failure_kind err
|
|
836
|
+
when :status
|
|
837
|
+
Event::HttpResponse.new(
|
|
838
|
+
status: err.response[:status],
|
|
839
|
+
headers: err.response[:headers] || {},
|
|
840
|
+
body: err.response[:body],
|
|
841
|
+
error: Gapic::Rest::Error.wrap_faraday_error(err)
|
|
842
|
+
)
|
|
843
|
+
when :timeout
|
|
844
|
+
Event::RequestFailed.new kind: :timeout, message: err.message, source_error: err
|
|
845
|
+
when :connection_failed
|
|
846
|
+
Event::RequestFailed.new kind: :connection_failed, message: err.message, source_error: err
|
|
847
|
+
else
|
|
848
|
+
Event::RequestFailed.new kind: :retries_exhausted, message: err.message, source_error: err
|
|
849
|
+
end
|
|
850
|
+
end
|
|
851
|
+
end
|
|
852
|
+
# rubocop:enable Metrics/ClassLength
|
|
853
|
+
end
|
|
854
|
+
end
|
|
855
|
+
end
|