gapic-common 1.4.0 → 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 +15 -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/grpc_transcoder.rb +39 -0
- 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,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
|
+
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
|