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.
@@ -0,0 +1,110 @@
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 "gapic/rest/resumable_upload/errors"
18
+ require "gapic/rest/resumable_upload/data_types"
19
+ require "gapic/rest/resumable_upload/events"
20
+ require "gapic/rest/resumable_upload/instructions"
21
+ require "gapic/rest/resumable_upload/retry_policies"
22
+ require "gapic/rest/resumable_upload/driver/abridge"
23
+ require "gapic/rest/resumable_upload/driver/upload_log"
24
+ require "gapic/rest/resumable_upload/rules"
25
+ require "gapic/rest/resumable_upload/core"
26
+ require "gapic/rest/resumable_upload/driver"
27
+
28
+ module Gapic
29
+ module Rest
30
+ ##
31
+ # Resumable Upload Protocol implementation for REST transport: session initiation, chunked
32
+ # streaming, automatic retries, progress reporting via {Progress}, and resumption via
33
+ # {ResumeHandle}.
34
+ #
35
+ # **This namespace has no callable surface.** Every method and class in it is internal machinery,
36
+ # documented as `@private` and excluded from these docs; what remains visible is the data a caller
37
+ # receives — {Progress}, {ResumeHandle}, {HasResumeHandle} — and the error classes listed below.
38
+ # Uploads are driven from {Gapic::ResumableUpload}, which sits above this namespace and coordinates
39
+ # runs against it.
40
+ #
41
+ # Errors raised from here carry a {ResumeHandle} where the upload can still be continued, so the
42
+ # usual shape of handling one is to rescue {HasResumeHandle} and hand the handle back to the
43
+ # coordinator:
44
+ #
45
+ # @example Uploading, then resuming after a recoverable failure
46
+ # upload = client.upload_media ... # returns a Gapic::ResumableUpload
47
+ # begin
48
+ # upload.start stream: File.open("movie.mp4", "rb"), upload_size: File.size("movie.mp4")
49
+ # rescue Gapic::Rest::ResumableUpload::HasResumeHandle => e
50
+ # raise unless e.resume_handle
51
+ # upload.resume stream: File.open("movie.mp4", "rb"), resume_handle: e.resume_handle
52
+ # end
53
+ #
54
+ # ### Error Types
55
+ # * {RequestFailedError} - Transport connection failure, timeout, or retries exhausted (includes {HasResumeHandle}).
56
+ # * {DeadlineExceededError} - Whole-upload timeout exceeded (includes {HasResumeHandle}).
57
+ # * {BadResponseError} - Unexpected, malformed, or out-of-phase HTTP response (includes {HasResumeHandle}).
58
+ # * {UnseekableStreamError} - Stream rewinding required on an unseekable stream (includes {HasResumeHandle}).
59
+ # * {StreamMismatchError} - Stream content or length does not match resumed upload (includes {HasResumeHandle}).
60
+ # * {UploadRejectedError} - Server explicitly rejected the upload session (final).
61
+ # * {UploadCancelledError} - Upload session was cancelled on the server while the upload was in flight. A
62
+ # cancelled session cannot be resumed, so this error carries no {ResumeHandle} (final).
63
+ # * {SessionStateError} - Upload session lifecycle rule violation, e.g. starting a second run while one
64
+ # is in flight (final).
65
+ # * Any other `Gapic::Common::Error` subclass signals a protocol implementation bug rather than a caller
66
+ # or server error (final).
67
+ #
68
+ module ResumableUpload
69
+ ##
70
+ # @private
71
+ # Converts the per-call options a generated client assembles into overrides for the initiation
72
+ # retry policy.
73
+ #
74
+ # Returns a **Hash**, never a policy object: the protocol treats a {Gapic::Common::RetryPolicy} as a
75
+ # wholesale replacement and a Hash as a per-key override, so a Hash keeps every protocol default the
76
+ # caller did not set (notably the initiation `retry_codes`).
77
+ #
78
+ # `timeout` is the one setting the caller cannot express here: it is set unconditionally from the
79
+ # call's own timeout and becomes the local deadline of the initiation request alone — the
80
+ # whole-upload budget is separate and is not derived here. A `timeout` inside the caller's retry
81
+ # policy is inert at the call layer (`Gapic::CallOptions::RetryPolicy` never populates `@timeout`,
82
+ # and `RpcCall` deadlines on `CallOptions#timeout`), so honouring it here would invent a meaning it
83
+ # has nowhere else. With no call timeout, initiation falls back to
84
+ # {Gapic::Common::RetryPolicy::DEFAULT_TIMEOUT} — one hour.
85
+ #
86
+ # Everything else the caller set is carried across as-is, by asking the policy what it carries
87
+ # ({Gapic::Common::RetryPolicy#overrides}) rather than inferring it from the readers, so a choice
88
+ # that happens to equal a library default still lands. That includes `retry_predicate`: the default
89
+ # initiation policy has none, so a caller's predicate is consulted as-is, ahead of `retry_codes`.
90
+ # The protocol's own retries (connection failures and a `200` without `X-Goog-Upload-Status`) and
91
+ # its refusals (a `final` rejection) are decided before the policy is asked, so no predicate can
92
+ # disable or override them.
93
+ #
94
+ # @param options [Gapic::CallOptions, nil] Per-call options from a generated client method
95
+ # @return [Hash] Overrides for the initiation retry policy
96
+ # @raise [ArgumentError] If the call options carry a Proc (or any other non-{Gapic::Common::RetryPolicy})
97
+ # retry policy, which has no coherent meaning across the three retry planes of an upload
98
+ def self.start_retry_policy_for options
99
+ policy = options&.retry_policy
100
+ if policy && !policy.is_a?(Gapic::Common::RetryPolicy)
101
+ raise ArgumentError,
102
+ "Resumable upload cannot derive an initiation retry policy from a #{policy.class}; " \
103
+ "use a Gapic::Common::RetryPolicy or a Hash of retry settings"
104
+ end
105
+
106
+ (policy&.overrides || {}).merge timeout: options&.timeout
107
+ end
108
+ end
109
+ end
110
+ end
data/lib/gapic/rest.rb CHANGED
@@ -28,6 +28,8 @@ require "gapic/rest/grpc_transcoder"
28
28
  require "gapic/rest/http_binding_override_configuration"
29
29
  require "gapic/rest/operation"
30
30
  require "gapic/rest/paged_enumerable"
31
+ require "gapic/rest/resumable_upload"
32
+ require "gapic/resumable_upload"
31
33
  require "gapic/rest/server_stream"
32
34
  require "gapic/rest/threaded_enumerator"
33
35
  require "gapic/rest/transport_operation"
@@ -0,0 +1,508 @@
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 "gapic/rest/resumable_upload"
18
+
19
+ module Gapic
20
+ ##
21
+ # Coordinates resumable uploads for a client method that performs them.
22
+ #
23
+ # A client method that uploads media returns one of these handles instead of a response. No request is
24
+ # sent and no byte is read from the stream until {#start} or {#resume} is called on it. Both are
25
+ # synchronous: they block the calling thread for the whole upload and return the decoded response
26
+ # message.
27
+ #
28
+ # ### Reusable
29
+ #
30
+ # A handle is reusable, and a failed run is resumed on the same object:
31
+ #
32
+ # @example Uploading, then resuming after a recoverable failure
33
+ # upload = client.create_media_upload request
34
+ # begin
35
+ # upload.start stream: File.open("movie.mp4", "rb"), content_type: "video/mp4"
36
+ # rescue Gapic::Rest::ResumableUpload::HasResumeHandle => e
37
+ # raise unless upload.resumable?
38
+ # upload.resume stream: File.open("movie.mp4", "rb")
39
+ # end
40
+ #
41
+ # The stream handed to {#resume} must be positioned at byte 0 of the whole object, not at the server's
42
+ # acknowledged offset; the upload fast-forwards on its own, by seeking on a seekable stream or by
43
+ # reading and discarding on an unseekable one. An unseekable stream therefore has to be freshly opened
44
+ # rather than rewound.
45
+ #
46
+ # A run that failed in a way the protocol can recover from leaves a {Gapic::Rest::ResumableUpload::ResumeHandle}
47
+ # behind, readable from {#resume_handle} and also carried on the error. Persisting that handle lets a
48
+ # later process resume the same upload:
49
+ #
50
+ # @example Resuming an upload started by an earlier process
51
+ # upload = client.create_media_upload
52
+ # upload.resume stream: File.open("movie.mp4", "rb"),
53
+ # resume_handle: Gapic::Rest::ResumableUpload::ResumeHandle.new(
54
+ # upload_url: row[:upload_url], chunk_size: row[:chunk_size]
55
+ # )
56
+ #
57
+ # A completed upload is finalized: {#resume_handle} returns `nil` and {#resumable?} returns `false`, so
58
+ # there is no handle to resume from. Calling {#start} again is permitted and begins a second, unrelated
59
+ # upload.
60
+ #
61
+ # ### The Two Timeouts
62
+ #
63
+ # An upload is bounded by two independent budgets, and they are three orders of magnitude apart:
64
+ #
65
+ # | Budget | Set by | Covers |
66
+ # |---|---|---|
67
+ # | whole upload | `upload_timeout:` on {#start} and {#resume} | every request, retry and byte of the run |
68
+ # | initiation request | per-call `timeout` | creating the session |
69
+ #
70
+ # The per-call `timeout` a client method takes reaches only the initiation request. An upload still
71
+ # transferring bytes an hour later has long outlived it, and that is expected. To bound the run as a
72
+ # whole, pass `upload_timeout:`.
73
+ #
74
+ # ### Threading
75
+ #
76
+ # {#start} and {#resume} block the calling thread, and the `on_progress` callback runs on that same
77
+ # thread. The readers ({#resume_handle}, {#resumable?}, {#running?}) are guarded by an internal mutex and
78
+ # may be called from another thread mid-run; values read that way are a best-effort snapshot of a state
79
+ # the upload thread is still advancing.
80
+ #
81
+ # ### Defaults
82
+ #
83
+ # * `chunk_size` defaults to 8 MB, then rounds down to a multiple of any chunk granularity the server
84
+ # requires.
85
+ # * `upload_timeout` defaults to `upload_size / 1 MB per second` when `upload_size` is known, floored at
86
+ # one hour, and to one hour flat when it is not.
87
+ #
88
+ # ### Retry Policies
89
+ #
90
+ # Retry behavior is partitioned across three policies. Only the initiation policy is caller-supplied;
91
+ # the other two are the protocol's own and govern the requests no call option describes.
92
+ #
93
+ # | Policy | Governs | Default `retry_codes` |
94
+ # |---|---|---|
95
+ # | initiation | session initiation | the 4xx and 5xx sets below |
96
+ # | control plane | `query` and `cancel` | the 4xx and 5xx sets below |
97
+ # | data plane | `upload` and `finalize` | the 5xx set below only |
98
+ #
99
+ # * 4xx set: `ALREADY_EXISTS` (HTTP `409`), `RESOURCE_EXHAUSTED` (`429`), `CANCELLED` (`499`).
100
+ # * 5xx set: `INTERNAL` (HTTP `500`), `UNAVAILABLE` (`503`), `DEADLINE_EXCEEDED` (`504`).
101
+ #
102
+ # None of the defaults carries a `retry_predicate`, so one supplied by the caller is consulted as-is,
103
+ # ahead of `retry_codes`. All three share the same backoff: `initial_delay` `1.0` s, `max_delay`
104
+ # `15.0` s, `multiplier` `1.3`.
105
+ #
106
+ # Some decisions belong to the protocol and are made before a policy is asked:
107
+ #
108
+ # * Initiation and control plane requests are re-sent, within the policy's deadline, after a connection
109
+ # or TLS failure, and after a `200` response missing `X-Goog-Upload-Status`.
110
+ # * Data plane requests are never re-sent after an outcome that leaves the server offset unknown — a
111
+ # timeout, a connection failure, a missing status header, or any `4xx`. The upload re-queries the
112
+ # session and resumes from the offset the server reports instead.
113
+ # * Consecutive recovery attempts that make no progress back off on a single schedule, with the same
114
+ # delays as above; the schedule starts over once the server confirms new bytes.
115
+ # * A response carrying `X-Goog-Upload-Status: final` is never retried.
116
+ #
117
+ class ResumableUpload
118
+ ##
119
+ # @private
120
+ # Builds a coordinator for one client method call.
121
+ #
122
+ # Instances come from generated client methods; the arguments below are what such a method has to
123
+ # hand over, not a surface a caller assembles.
124
+ #
125
+ # Both procs are deferred deliberately. `client_stub_proc` lets a client that cannot perform REST
126
+ # calls hand back a working handle and fail only when an upload is actually attempted.
127
+ # `initial_request_proc` means the initiation URL and body are computed on {#start} and never on
128
+ # {#resume}, so a handle built without a request message is still fully functional for resuming.
129
+ #
130
+ # @param client_stub_proc [Proc] Returns the {Gapic::Rest::ClientStub} to upload through. Called at
131
+ # the top of every run, and may raise if the client cannot perform REST calls.
132
+ # @param initial_request_proc [Proc] Returns the `[url, body]` pair for session initiation. Called by
133
+ # {#start} only.
134
+ # @param response_type [Class, nil] Protobuf message class the final response body is decoded into.
135
+ # `nil` returns the raw body; see {#start}.
136
+ # @param initial_headers [Hash] Headers for the initiation request. Keys and values are stringified.
137
+ # The five reserved protocol headers (`X-Goog-Upload-Protocol`, `X-Goog-Upload-Command`,
138
+ # `X-Goog-Upload-Offset`, `X-Goog-Upload-Header-Content-Type`, `X-Goog-Upload-Header-Content-Length`)
139
+ # are rejected in any casing; use `content_type` and `upload_size` on the run methods instead.
140
+ # @param start_retry_policy [Gapic::Common::RetryPolicy, Hash, nil] Retry policy for the initiation
141
+ # request. A {Gapic::Common::RetryPolicy} replaces the default policy outright; a Hash overrides only
142
+ # the settings it names. See the "Retry Policies" section in the class documentation.
143
+ # @param control_plane_retry_policy [Gapic::Common::RetryPolicy, Hash, nil] Retry policy for `query`
144
+ # and `cancel`. `nil` uses the protocol default.
145
+ # @param data_plane_retry_policy [Gapic::Common::RetryPolicy, Hash, nil] Retry policy for `upload` and
146
+ # `finalize`. `nil` uses the protocol default.
147
+ # @param error_handler [Proc, nil] Called with a run failure and **returns** the exception to raise in
148
+ # its place. It must not raise.
149
+ # @param method_name [String, nil] RPC name used in log entries.
150
+ #
151
+ def initialize client_stub_proc:,
152
+ initial_request_proc:,
153
+ response_type:,
154
+ initial_headers: {},
155
+ start_retry_policy: nil,
156
+ control_plane_retry_policy: nil,
157
+ data_plane_retry_policy: nil,
158
+ error_handler: nil,
159
+ method_name: nil
160
+ @client_stub_proc = client_stub_proc
161
+ @initial_request_proc = initial_request_proc
162
+ @response_type = response_type
163
+ @initial_headers = stringify_headers initial_headers
164
+ @start_retry_policy = start_retry_policy
165
+ @control_plane_retry_policy = control_plane_retry_policy
166
+ @data_plane_retry_policy = data_plane_retry_policy
167
+ @error_handler = error_handler
168
+ @method_name = method_name
169
+
170
+ @mutex = Mutex.new
171
+ @running = false
172
+ @driver = nil
173
+ end
174
+
175
+ ##
176
+ # Creates an upload session on the server and transfers the stream into it.
177
+ #
178
+ # Blocks the calling thread until the upload completes or fails. The stream is assumed to be
179
+ # positioned at byte 0 (it is not rewound before reading) and is not closed after use.
180
+ #
181
+ # @example
182
+ # response = upload.start stream: File.open("movie.mp4", "rb"),
183
+ # content_type: "video/mp4",
184
+ # upload_size: File.size("movie.mp4"),
185
+ # upload_timeout: 4 * 3600,
186
+ # on_progress: ->(p) { puts "#{p.phase}: #{p.bytes_uploaded}" }
187
+ #
188
+ # @param stream [IO] Binary input stream to upload, positioned at byte 0.
189
+ # @param content_type [String, nil] MIME type of the uploaded media.
190
+ # @param upload_size [Integer, nil] Total upload bytes, if known upfront.
191
+ # @param chunk_size [Integer, nil] Requested chunk size in bytes, defaulting to 8 MB. The effective
192
+ # size is rounded down to a multiple of any chunk granularity the server requires, or raised to that
193
+ # granularity if it exceeds the requested size. A resumed run has no such argument: it takes its
194
+ # chunk size from the {Gapic::Rest::ResumableUpload::ResumeHandle}.
195
+ # @param upload_timeout [Numeric, nil] Budget in seconds for the **whole run** — every request, every
196
+ # retry, every byte — not for any single request. The per-call `timeout` a client method takes bounds
197
+ # the initiation request alone. When `nil`, resolves to `upload_size / 1 MB per second` floored at one
198
+ # hour if `upload_size` is known, and to one hour flat otherwise.
199
+ # @param on_progress [Proc, nil] Called as `->(progress)` with a {Gapic::Rest::ResumableUpload::Progress}
200
+ # instance. Runs synchronously on the upload thread and must not block; an exception raised inside it
201
+ # aborts the run and propagates out of this method.
202
+ # @return [Object] The final response decoded into the handle's response type.
203
+ # @raise [ArgumentError] If the call metadata sets a reserved `X-Goog-Upload-*` protocol header, or if the
204
+ # client cannot perform REST calls
205
+ # @raise [Gapic::Rest::ResumableUpload::SessionStateError] If a run is already in progress
206
+ # @raise [Gapic::Rest::ResumableUpload::RequestFailedError] If a transport error, timeout, or retry
207
+ # exhaustion occurs
208
+ # @raise [Gapic::Rest::ResumableUpload::DeadlineExceededError] If `upload_timeout` is exceeded
209
+ # @raise [Gapic::Rest::ResumableUpload::BadResponseError] If an unexpected or malformed HTTP response
210
+ # is received
211
+ # @raise [Gapic::Rest::ResumableUpload::UnseekableStreamError] If stream rewinding is required during
212
+ # recovery on an unseekable stream
213
+ # @raise [Gapic::Rest::ResumableUpload::StreamMismatchError] If stream content or length does not match
214
+ # protocol expectations
215
+ # @raise [Gapic::Rest::ResumableUpload::UploadRejectedError] If the server explicitly rejects the upload
216
+ # @raise [Gapic::Common::Error] Any other subclass signals a protocol implementation bug rather than a
217
+ # caller or server error
218
+ #
219
+ def start stream:,
220
+ content_type: nil,
221
+ upload_size: nil,
222
+ chunk_size: nil,
223
+ upload_timeout: nil,
224
+ on_progress: nil
225
+ execute_run do
226
+ client_stub = @client_stub_proc.call
227
+ initial_url, initial_body = @initial_request_proc.call
228
+ config = ::Gapic::Rest::ResumableUpload::StartUploadConfig.new(
229
+ initial_url: initial_url,
230
+ initial_body: initial_body,
231
+ initial_headers: @initial_headers,
232
+ chunk_size: chunk_size,
233
+ start_retry_policy: @start_retry_policy,
234
+ **run_config_args(stream: stream, content_type: content_type, upload_size: upload_size,
235
+ upload_timeout: upload_timeout, on_progress: on_progress)
236
+ )
237
+ build_driver client_stub, config
238
+ end
239
+ end
240
+
241
+ ##
242
+ # Resumes an upload session the server has already created, transferring whatever it has not yet
243
+ # acknowledged.
244
+ #
245
+ # Blocks the calling thread until the upload completes or fails. A resumed run sends no initiation
246
+ # request, so it takes no initiation arguments and needs no request message.
247
+ #
248
+ # The target is a {Gapic::Rest::ResumableUpload::ResumeHandle}: either the one passed in, or — when
249
+ # `resume_handle` is omitted — the one left behind by this handle's last run. The bare form raises
250
+ # `ArgumentError` when there is none, which covers a handle that has never run, a run that finished
251
+ # successfully, and a run that failed in a way the protocol considers unresumable.
252
+ #
253
+ # Resuming against a finalized upload URL is undefined behavior: it queries the server and might
254
+ # return the response body or raise an error, depending on the server response.
255
+ #
256
+ # @param stream [IO] Binary input stream to upload, positioned at byte 0 of the **whole object**, not
257
+ # at the server's acknowledged offset. The upload fast-forwards on its own, by seeking or by reading
258
+ # and discarding. Not closed after use.
259
+ # @param resume_handle [Gapic::Rest::ResumableUpload::ResumeHandle, nil] Upload to resume. Defaults to
260
+ # the one left behind by this handle's last run.
261
+ # @param content_type [String, nil] MIME type of the uploaded media.
262
+ # @param upload_size [Integer, nil] Total upload bytes, if known upfront.
263
+ # @param upload_timeout [Numeric, nil] Budget in seconds for the **whole run**. See {#start}.
264
+ # @param on_progress [Proc, nil] Called as `->(progress)` with a {Gapic::Rest::ResumableUpload::Progress}
265
+ # instance. Runs synchronously on the upload thread and must not block.
266
+ # @return [Object] The final response decoded into the handle's response type.
267
+ # @raise [ArgumentError] If there is no upload to resume, if the stream is not positioned at byte 0, or if
268
+ # the client cannot perform REST calls
269
+ # @raise [Gapic::Rest::ResumableUpload::SessionStateError] If a run is already in progress
270
+ # @raise [Gapic::Rest::ResumableUpload::RequestFailedError] If a transport error, timeout, or retry
271
+ # exhaustion occurs
272
+ # @raise [Gapic::Rest::ResumableUpload::DeadlineExceededError] If `upload_timeout` is exceeded
273
+ # @raise [Gapic::Rest::ResumableUpload::BadResponseError] If an unexpected or malformed HTTP response
274
+ # is received
275
+ # @raise [Gapic::Rest::ResumableUpload::UnseekableStreamError] If stream rewinding is required during
276
+ # recovery on an unseekable stream
277
+ # @raise [Gapic::Rest::ResumableUpload::StreamMismatchError] If stream content or length does not match
278
+ # the resumed upload
279
+ # @raise [Gapic::Rest::ResumableUpload::UploadRejectedError] If the server explicitly rejects the upload
280
+ # @raise [Gapic::Common::Error] Any other subclass signals a protocol implementation bug rather than a
281
+ # caller or server error
282
+ #
283
+ def resume stream:,
284
+ resume_handle: nil,
285
+ content_type: nil,
286
+ upload_size: nil,
287
+ upload_timeout: nil,
288
+ on_progress: nil
289
+ execute_run do
290
+ verify_stream_at_origin stream
291
+ handle = resolve_resume_handle resume_handle
292
+ client_stub = @client_stub_proc.call
293
+ config = ::Gapic::Rest::ResumableUpload::ResumeUploadConfig.new(
294
+ upload_url: handle.upload_url,
295
+ chunk_size: handle.chunk_size,
296
+ **run_config_args(stream: stream, content_type: content_type, upload_size: upload_size,
297
+ upload_timeout: upload_timeout, on_progress: on_progress)
298
+ )
299
+ build_driver client_stub, config
300
+ end
301
+ end
302
+
303
+ ##
304
+ # Returns the handle needed to resume the last run, carrying its upload URL and resolved chunk size.
305
+ #
306
+ # `nil` before the first run, and after any run that left nothing to resume: a completed upload is
307
+ # finalized, and rejected and cancelled uploads are too.
308
+ #
309
+ # Each run replaces this value rather than accumulating handles, so it always describes the most
310
+ # recent one. Starting a second upload therefore discards whatever the previous run left behind —
311
+ # persist the handle first if the earlier upload still matters. A call that fails while building its
312
+ # configuration does not count as a run: it never reaches a driver, so the earlier value survives.
313
+ #
314
+ # @return [Gapic::Rest::ResumableUpload::ResumeHandle, nil]
315
+ def resume_handle
316
+ @mutex.synchronize { @driver&.resume_handle }
317
+ end
318
+
319
+ ##
320
+ # Returns whether the last run left an upload that can be resumed.
321
+ #
322
+ # @return [Boolean]
323
+ def resumable?
324
+ !resume_handle.nil?
325
+ end
326
+
327
+ ##
328
+ # Returns whether a run is currently executing.
329
+ #
330
+ # @return [Boolean]
331
+ def running?
332
+ @mutex.synchronize { @running }
333
+ end
334
+
335
+ private
336
+
337
+ ##
338
+ # @private
339
+ # Claims the single run slot, runs the driver built by the block, and releases the slot.
340
+ #
341
+ # The slot is claimed before the block runs, so a second concurrent run is rejected while this one is
342
+ # still building its configuration. It is released in an `ensure`, so every exit — a clean return, a
343
+ # protocol error, an `on_progress` callback raising, a `Thread#kill` — leaves the handle usable and the
344
+ # driver retained. A failure inside the block leaves no driver retained at all, so a configuration
345
+ # `ArgumentError` cannot disturb the resume handle of an earlier run.
346
+ #
347
+ # @yieldreturn [Gapic::Rest::ResumableUpload::Driver] Driver to run
348
+ # @return [Object] Decoded final response
349
+ def execute_run
350
+ claim_run_slot
351
+ begin
352
+ driver = yield
353
+ @mutex.synchronize { @driver = driver }
354
+ run_driver driver
355
+ ensure
356
+ @mutex.synchronize { @running = false }
357
+ end
358
+ end
359
+
360
+ ##
361
+ # @private
362
+ # Runs a driver and decodes its result, applying the caller's error handler to a failure.
363
+ #
364
+ # Only the run is wrapped. Argument and configuration errors are raised while the driver is still
365
+ # being built, and reach the caller as themselves: they describe a call that was never made.
366
+ #
367
+ # @param driver [Gapic::Rest::ResumableUpload::Driver] Driver to run
368
+ # @return [Object] Decoded final response
369
+ def run_driver driver
370
+ decode_response driver.run
371
+ rescue ::StandardError => e
372
+ raise wrap_error(e)
373
+ end
374
+
375
+ ##
376
+ # @private
377
+ # Marks a run as in flight, rejecting a second concurrent one.
378
+ #
379
+ # @return [void]
380
+ # @raise [Gapic::Rest::ResumableUpload::SessionStateError] If a run is already in progress
381
+ def claim_run_slot
382
+ @mutex.synchronize do
383
+ if @running
384
+ raise ::Gapic::Rest::ResumableUpload::SessionStateError,
385
+ "A run is already in progress for this upload"
386
+ end
387
+ @running = true
388
+ end
389
+ end
390
+
391
+ ##
392
+ # @private
393
+ # Builds the driver for a run.
394
+ #
395
+ # @param client_stub [Gapic::Rest::ClientStub] Stub returned by `client_stub_proc`
396
+ # @param config [Gapic::Rest::ResumableUpload::StartUploadConfig,
397
+ # Gapic::Rest::ResumableUpload::ResumeUploadConfig] Configuration for this run
398
+ # @return [Gapic::Rest::ResumableUpload::Driver]
399
+ def build_driver client_stub, config
400
+ ::Gapic::Rest::ResumableUpload::Driver.new client_stub: client_stub,
401
+ config: config,
402
+ method_name: @method_name
403
+ end
404
+
405
+ ##
406
+ # @private
407
+ # Returns the configuration members both run types share, mirroring
408
+ # {Gapic::Rest::ResumableUpload::COMMON_MEMBERS}.
409
+ #
410
+ # @return [Hash{Symbol=>Object}]
411
+ def run_config_args stream:, content_type:, upload_size:, upload_timeout:, on_progress:
412
+ {
413
+ stream: stream,
414
+ upload_size: upload_size,
415
+ content_type: content_type,
416
+ timeout: upload_timeout,
417
+ control_plane_retry_policy: @control_plane_retry_policy,
418
+ data_plane_retry_policy: @data_plane_retry_policy,
419
+ on_progress: on_progress
420
+ }
421
+ end
422
+
423
+ ##
424
+ # @private
425
+ # Resolves the upload a resumed run targets.
426
+ #
427
+ # @param resume_handle [Gapic::Rest::ResumableUpload::ResumeHandle, nil] Explicit handle, if given
428
+ # @return [Gapic::Rest::ResumableUpload::ResumeHandle]
429
+ # @raise [ArgumentError] If no handle was given and the last run left none
430
+ def resolve_resume_handle resume_handle
431
+ return resume_handle if resume_handle
432
+
433
+ handle = @mutex.synchronize { @driver&.resume_handle }
434
+ if handle.nil?
435
+ raise ArgumentError,
436
+ "No upload to resume: this handle has not run, or its last run left nothing resumable. " \
437
+ "Pass resume_handle: to resume an upload started elsewhere."
438
+ end
439
+ handle
440
+ end
441
+
442
+ ##
443
+ # @private
444
+ # Rejects a stream that is not positioned at byte 0. Streams that do not report a position are
445
+ # trusted.
446
+ #
447
+ # @param stream [IO] Stream a resumed run will read
448
+ # @return [void]
449
+ # @raise [ArgumentError] If the stream reports a non-zero position
450
+ def verify_stream_at_origin stream
451
+ return unless stream.respond_to? :pos
452
+ return if stream.pos.zero?
453
+
454
+ raise ArgumentError, "Stream must be positioned at byte 0 to resume an upload (got pos #{stream.pos})"
455
+ end
456
+
457
+ ##
458
+ # @private
459
+ # Decodes the final response body into the handle's response type.
460
+ #
461
+ # A `nil` response type returns the raw body, exactly as the driver produced it. Generated call sites
462
+ # always pass a message class; the raw form exists so this gem's own tests can assert on what the
463
+ # server sent without decoding through a message type they do not have.
464
+ #
465
+ # @param body [String, nil] Raw body of the finalizing HTTP response
466
+ # @return [Object] Decoded message, or the raw body when there is no response type
467
+ def decode_response body
468
+ return body if @response_type.nil?
469
+
470
+ @response_type.decode_json body.to_s, ignore_unknown_fields: true
471
+ end
472
+
473
+ ##
474
+ # @private
475
+ # Applies the caller's error handler to a run failure.
476
+ #
477
+ # The handler returns the exception to raise. A replacement that loses the
478
+ # {Gapic::Rest::ResumableUpload::HasResumeHandle} mixin is re-extended with it and given the original's
479
+ # handle, so a library-specific error type cannot erase the fact that the upload is resumable.
480
+ #
481
+ # @param error [StandardError] Failure raised by the run
482
+ # @return [Exception] Exception to raise in its place
483
+ def wrap_error error
484
+ return error unless @error_handler
485
+
486
+ wrapped = @error_handler.call error
487
+ return error if wrapped.nil? || wrapped.equal?(error)
488
+
489
+ if error.is_a?(::Gapic::Rest::ResumableUpload::HasResumeHandle) &&
490
+ !wrapped.is_a?(::Gapic::Rest::ResumableUpload::HasResumeHandle)
491
+ wrapped.extend ::Gapic::Rest::ResumableUpload::HasResumeHandle
492
+ wrapped.instance_variable_set :@resume_handle, error.resume_handle
493
+ end
494
+ wrapped
495
+ end
496
+
497
+ ##
498
+ # @private
499
+ # Stringifies the keys and values of the initiation headers, which generated clients carry as a
500
+ # symbol-keyed metadata hash.
501
+ #
502
+ # @param headers [Hash, nil] Initiation headers
503
+ # @return [Hash{String=>String}]
504
+ def stringify_headers headers
505
+ (headers || {}).to_h { |key, value| [key.to_s, value.to_s] }
506
+ end
507
+ end
508
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gapic-common
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.1
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Google API Authors
@@ -203,9 +203,22 @@ files:
203
203
  - lib/gapic/rest/http_binding_override_configuration.rb
204
204
  - lib/gapic/rest/operation.rb
205
205
  - lib/gapic/rest/paged_enumerable.rb
206
+ - lib/gapic/rest/resumable_upload.rb
207
+ - lib/gapic/rest/resumable_upload/core.rb
208
+ - lib/gapic/rest/resumable_upload/data_types.rb
209
+ - lib/gapic/rest/resumable_upload/driver.rb
210
+ - lib/gapic/rest/resumable_upload/driver/abridge.rb
211
+ - lib/gapic/rest/resumable_upload/driver/retry_decider.rb
212
+ - lib/gapic/rest/resumable_upload/driver/upload_log.rb
213
+ - lib/gapic/rest/resumable_upload/errors.rb
214
+ - lib/gapic/rest/resumable_upload/events.rb
215
+ - lib/gapic/rest/resumable_upload/instructions.rb
216
+ - lib/gapic/rest/resumable_upload/retry_policies.rb
217
+ - lib/gapic/rest/resumable_upload/rules.rb
206
218
  - lib/gapic/rest/server_stream.rb
207
219
  - lib/gapic/rest/threaded_enumerator.rb
208
220
  - lib/gapic/rest/transport_operation.rb
221
+ - lib/gapic/resumable_upload.rb
209
222
  - lib/gapic/stream_input.rb
210
223
  - lib/gapic/universe_domain_concerns.rb
211
224
  homepage: https://github.com/googleapis/ruby-core-libraries