connectrpc-async 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +542 -0
- data/lib/connectrpc/async/application.rb +173 -0
- data/lib/connectrpc/async/completion_join.rb +249 -0
- data/lib/connectrpc/async/connection.rb +711 -0
- data/lib/connectrpc/async/connection_manager.rb +358 -0
- data/lib/connectrpc/async/connection_policy.rb +60 -0
- data/lib/connectrpc/async/host.rb +171 -0
- data/lib/connectrpc/async/http1_connection.rb +144 -0
- data/lib/connectrpc/async/http1_exchange.rb +27 -0
- data/lib/connectrpc/async/http1_stream.rb +76 -0
- data/lib/connectrpc/async/http1_transport.rb +642 -0
- data/lib/connectrpc/async/ingress.rb +100 -0
- data/lib/connectrpc/async/internal.rb +270 -0
- data/lib/connectrpc/async/server_session.rb +457 -0
- data/lib/connectrpc/async/stream_exchange.rb +324 -0
- data/lib/connectrpc/async/tls.rb +124 -0
- data/lib/connectrpc/async/tls_configuration.rb +309 -0
- data/lib/connectrpc/async/transport.rb +488 -0
- data/lib/connectrpc/async/version.rb +7 -0
- data/lib/connectrpc/async.rb +39 -0
- data/sig/connectrpc-async.rbs +165 -0
- data/sig/manifest.yaml +4 -0
- metadata +202 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module ConnectRPC
|
|
6
|
+
module Async
|
|
7
|
+
class Transport
|
|
8
|
+
include Internal::Affinity
|
|
9
|
+
|
|
10
|
+
def initialize(origin:, ca_file: nil, certificate_chain: nil, private_key: nil, connection_limit: 1,
|
|
11
|
+
max_requests: 128, connect_timeout: 10, connection_policy: ConnectionPolicy.new,
|
|
12
|
+
connection_observer: nil)
|
|
13
|
+
initialize_affinity
|
|
14
|
+
@origin = parse_origin(origin)
|
|
15
|
+
@connection_limit = Internal.positive_integer(connection_limit, "connection_limit")
|
|
16
|
+
@max_requests = Internal.positive_integer(max_requests, "max_requests")
|
|
17
|
+
@connect_timeout = Internal.seconds(connect_timeout, "connect_timeout")
|
|
18
|
+
unless connection_policy.is_a?(ConnectionPolicy)
|
|
19
|
+
raise ArgumentError, "connection_policy must be a ConnectionPolicy"
|
|
20
|
+
end
|
|
21
|
+
unless connection_observer.nil? || connection_observer.respond_to?(:call)
|
|
22
|
+
raise ArgumentError, "connection_observer must respond to call"
|
|
23
|
+
end
|
|
24
|
+
if @origin.scheme == "http" && (ca_file || certificate_chain || private_key)
|
|
25
|
+
raise ArgumentError, "TLS options require an HTTPS origin"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
if @origin.scheme == "https"
|
|
29
|
+
@snapshot = Internal::TLS.client(ca_file: ca_file, certificate_chain: certificate_chain,
|
|
30
|
+
private_key: private_key)
|
|
31
|
+
end
|
|
32
|
+
@calls = {}.compare_by_identity
|
|
33
|
+
@acquiring = {}
|
|
34
|
+
@state = :running
|
|
35
|
+
install_owner_guard
|
|
36
|
+
@manager = Internal::ConnectionManager.new(
|
|
37
|
+
policy: connection_policy, observer: connection_observer, connection_limit: @connection_limit,
|
|
38
|
+
connect_timeout: @connect_timeout, lifetime: @lifetime, progress: @progress,
|
|
39
|
+
build: method(:build_resource), setup: method(:setup_connection), abort_setup: method(:abort_setup)
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def inspect = "#<#{self.class} state=#{@state} connection_state=#{@manager&.state} origin=#{@origin}>"
|
|
44
|
+
|
|
45
|
+
# A published observation, not a promise that the next call succeeds.
|
|
46
|
+
def connection_state = @manager.state
|
|
47
|
+
|
|
48
|
+
def call(request)
|
|
49
|
+
check_affinity!
|
|
50
|
+
@manager.check_reentry!
|
|
51
|
+
CallCancellation.with_request(request) { |outgoing| perform_call(outgoing) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def streaming_capabilities = %i[server_stream client_stream bidi_stream].freeze
|
|
55
|
+
|
|
56
|
+
def open_stream(request, &)
|
|
57
|
+
raise ArgumentError, "A streaming block is required" unless block_given?
|
|
58
|
+
|
|
59
|
+
check_affinity!
|
|
60
|
+
@manager.check_reentry!
|
|
61
|
+
CallCancellation.with_request(request) { |outgoing| perform_stream(outgoing, &) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def grpc_capabilities = %i[unary server_stream client_stream bidi_stream].freeze
|
|
65
|
+
|
|
66
|
+
def open_grpc_stream(request, &)
|
|
67
|
+
raise ArgumentError, "A gRPC block is required" unless block_given?
|
|
68
|
+
|
|
69
|
+
check_affinity!
|
|
70
|
+
@manager.check_reentry!
|
|
71
|
+
CallCancellation.with_request(request) { |outgoing| perform_stream(outgoing, grpc: true, &) }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def close(grace: 5)
|
|
75
|
+
check_affinity!
|
|
76
|
+
@manager.check_reentry!
|
|
77
|
+
super
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def perform_stream(request, grpc: false)
|
|
83
|
+
raise ArgumentError, "Request URL must match the transport origin" unless same_origin?(request.url)
|
|
84
|
+
raise Error.new(:unavailable, "HTTP/2 transport admission is closed or full") unless @state == :running
|
|
85
|
+
|
|
86
|
+
operation = { request: request, tasks: {}, grpc: grpc }
|
|
87
|
+
# Streams reserve an admission slot but no request bytes.
|
|
88
|
+
@manager.admit(0, full: @calls.size >= @max_requests)
|
|
89
|
+
@calls[operation] = true
|
|
90
|
+
body = Internal::OutputBody.new
|
|
91
|
+
completed = false
|
|
92
|
+
begin
|
|
93
|
+
stream_operation(operation) { dispatch_stream(operation, body) }
|
|
94
|
+
exchange = grpc ? Internal::GRPCStreamExchange : Internal::StreamExchange
|
|
95
|
+
result = yield exchange.new(self, operation, body)
|
|
96
|
+
completed = true
|
|
97
|
+
result
|
|
98
|
+
ensure
|
|
99
|
+
body.close
|
|
100
|
+
begin
|
|
101
|
+
cleanup(operation)
|
|
102
|
+
ensure
|
|
103
|
+
@calls.delete(operation)
|
|
104
|
+
@progress.signal
|
|
105
|
+
end
|
|
106
|
+
if grpc && completed && operation[:cleanup_error]
|
|
107
|
+
check_request!(request)
|
|
108
|
+
raise Error.new(:unavailable, "HTTP/2 transport cleanup failed"), cause: nil
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def stream_operation(operation, timeout: nil, &work)
|
|
114
|
+
check_affinity!
|
|
115
|
+
timeout = [timeout || Deadline::MAX_NATIVE_WAIT, Deadline::MAX_NATIVE_WAIT].min if operation[:grpc]
|
|
116
|
+
request = operation.fetch(:request)
|
|
117
|
+
raise Error.new(:unavailable, "HTTP/2 exchange is retired"), cause: nil if operation[:cleaned]
|
|
118
|
+
|
|
119
|
+
check_request!(request)
|
|
120
|
+
event = ::Async::Promise.new
|
|
121
|
+
registration = request.cancellation&.on_cancel { event.resolve([:cancellation, nil]) }
|
|
122
|
+
child = @lifetime.async(finished: false) do
|
|
123
|
+
result = activate(request, &work)
|
|
124
|
+
event.resolve([:result, result])
|
|
125
|
+
rescue StandardError => e
|
|
126
|
+
request.lifecycle&.record_failure(e)
|
|
127
|
+
event.resolve([:error, e])
|
|
128
|
+
ensure
|
|
129
|
+
event.resolve([:stopped, nil])
|
|
130
|
+
end
|
|
131
|
+
operation.fetch(:tasks)[child] = true
|
|
132
|
+
expires = Internal.now + timeout if timeout
|
|
133
|
+
expires = [expires || Float::INFINITY, Internal.now + request.deadline.remaining].min if request.deadline
|
|
134
|
+
tag, result = wait_event(event, expires)
|
|
135
|
+
check_request!(request)
|
|
136
|
+
raise result if tag == :error
|
|
137
|
+
raise IOError, "HTTP/2 operation stopped" unless tag == :result
|
|
138
|
+
|
|
139
|
+
result
|
|
140
|
+
rescue StandardError => e
|
|
141
|
+
request.lifecycle&.record_failure(e)
|
|
142
|
+
check_request!(request)
|
|
143
|
+
if operation[:grpc] && (failure = operation[:response]&.stream&.grpc_error)
|
|
144
|
+
raise failure, cause: nil
|
|
145
|
+
end
|
|
146
|
+
raise if e.is_a?(ConnectRPC::Error)
|
|
147
|
+
|
|
148
|
+
raise Error.new(:unavailable, "HTTP/2 transport failed"), cause: nil
|
|
149
|
+
ensure
|
|
150
|
+
registration&.unregister
|
|
151
|
+
# The call's one cleanup budget owns stopping a still-blocked task.
|
|
152
|
+
if child && !child.finished? && !%i[result error].include?(tag)
|
|
153
|
+
cleanup(operation)
|
|
154
|
+
else
|
|
155
|
+
operation[:tasks]&.delete(child)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def dispatch_stream(operation, body)
|
|
160
|
+
request = operation.fetch(:request)
|
|
161
|
+
dispatch(operation) do |connection, task|
|
|
162
|
+
headers = request.headers.dup
|
|
163
|
+
if request.deadline
|
|
164
|
+
key = operation[:grpc] ? "grpc-timeout" : "connect-timeout-ms"
|
|
165
|
+
headers[key] = [operation[:grpc] ? GRPCProtocol.timeout(request.deadline) : request.deadline.timeout_ms]
|
|
166
|
+
end
|
|
167
|
+
GRPCProtocol.check_fields!(headers) if operation[:grpc]
|
|
168
|
+
outgoing = ::Protocol::HTTP::Request["POST", request.url.request_uri, Internal.fields(headers), body,
|
|
169
|
+
scheme: @origin.scheme, authority: @origin.authority]
|
|
170
|
+
response = connection.create_response
|
|
171
|
+
operation[:response] = response
|
|
172
|
+
response.stream.grpc_mark! if operation[:grpc]
|
|
173
|
+
task.defer_cancel { connection.write_request(response, outgoing) }
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Acquires a lease and runs the block under its write guard. A connection
|
|
178
|
+
# found unusable before any stream ID is allocated is released and the
|
|
179
|
+
# call returns to acquisition; once dispatch starts it is never resent.
|
|
180
|
+
def dispatch(operation)
|
|
181
|
+
request = operation.fetch(:request)
|
|
182
|
+
task = ::Async::Task.current
|
|
183
|
+
loop do
|
|
184
|
+
connection = acquire(operation, task)
|
|
185
|
+
dispatched = false
|
|
186
|
+
begin
|
|
187
|
+
return connection.synchronize(dispatch: true) do
|
|
188
|
+
check_request!(request)
|
|
189
|
+
raise Internal::UnusableConnection, "Connection is not reusable" unless @manager.usable?(connection)
|
|
190
|
+
|
|
191
|
+
dispatched = true
|
|
192
|
+
yield connection, task
|
|
193
|
+
end
|
|
194
|
+
rescue Internal::UnusableConnection
|
|
195
|
+
raise if dispatched
|
|
196
|
+
|
|
197
|
+
reroute(operation)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def acquire(operation, task)
|
|
203
|
+
@acquiring[task] = operation
|
|
204
|
+
begin
|
|
205
|
+
connection = @manager.acquire
|
|
206
|
+
operation[:connection] = connection
|
|
207
|
+
operation[:resource] = connection.resource
|
|
208
|
+
connection
|
|
209
|
+
ensure
|
|
210
|
+
@acquiring.delete(task)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Detaches the unsent lease before any yielding release, so cleanup can
|
|
215
|
+
# never release it again. A failed release fails the call.
|
|
216
|
+
def reroute(operation)
|
|
217
|
+
connection = operation.delete(:connection)
|
|
218
|
+
operation.delete(:resource)
|
|
219
|
+
@manager.release(connection)
|
|
220
|
+
rescue StandardError
|
|
221
|
+
begin
|
|
222
|
+
connection.resource&.abort
|
|
223
|
+
ensure
|
|
224
|
+
@manager.retire(connection)
|
|
225
|
+
end
|
|
226
|
+
raise
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def perform_call(request)
|
|
230
|
+
raise ArgumentError, "Request URL must match the transport origin" unless same_origin?(request.url)
|
|
231
|
+
|
|
232
|
+
check_request!(request)
|
|
233
|
+
raise Error.new(:unavailable, "HTTP/2 transport admission is closed or full") unless @state == :running
|
|
234
|
+
|
|
235
|
+
event = ::Async::Promise.new
|
|
236
|
+
operation = { request: request }
|
|
237
|
+
# GET carries its encoded message in the request target.
|
|
238
|
+
bytes = request.http_method == "GET" ? request.url.request_uri.bytesize : request.body.bytesize
|
|
239
|
+
operation[:bytes] = @manager.admit(bytes, full: @calls.size >= @max_requests)
|
|
240
|
+
@calls[operation] = true
|
|
241
|
+
registration = request.cancellation&.on_cancel { event.resolve([:cancellation, nil]) }
|
|
242
|
+
begin
|
|
243
|
+
check_request!(request)
|
|
244
|
+
operation[:task] = @lifetime.async(finished: false) do
|
|
245
|
+
result = activate(request) { exchange(operation) }
|
|
246
|
+
event.resolve([:response, result])
|
|
247
|
+
rescue StandardError => e
|
|
248
|
+
request.lifecycle&.record_failure(e)
|
|
249
|
+
event.resolve([:error, e])
|
|
250
|
+
ensure
|
|
251
|
+
event.resolve([:stopped, nil])
|
|
252
|
+
end
|
|
253
|
+
expires = Internal.now + request.deadline.remaining if request.deadline
|
|
254
|
+
tag, result = wait_event(event, expires)
|
|
255
|
+
check_request!(request)
|
|
256
|
+
raise result if tag == :error
|
|
257
|
+
raise Error.new(:unavailable, "HTTP/2 connection closed") unless result
|
|
258
|
+
|
|
259
|
+
result
|
|
260
|
+
rescue ::Async::TimeoutError, StandardError => e
|
|
261
|
+
request.lifecycle&.record_failure(e)
|
|
262
|
+
check_request!(request)
|
|
263
|
+
raise e if e.is_a?(ConnectRPC::Error)
|
|
264
|
+
|
|
265
|
+
raise Error.new(:unavailable, "HTTP/2 transport failed"), cause: nil
|
|
266
|
+
ensure
|
|
267
|
+
registration&.unregister
|
|
268
|
+
begin
|
|
269
|
+
cleanup(operation)
|
|
270
|
+
ensure
|
|
271
|
+
@calls.delete(operation)
|
|
272
|
+
@manager.release_bytes(operation.fetch(:bytes))
|
|
273
|
+
@progress.signal
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def check_request!(request)
|
|
279
|
+
request.cancellation ? request.cancellation.check! : request.deadline&.check!
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def wait_event(event, expires)
|
|
283
|
+
loop do
|
|
284
|
+
return event.wait unless expires
|
|
285
|
+
|
|
286
|
+
remaining = expires - Internal.now
|
|
287
|
+
raise ::Async::TimeoutError if remaining <= 0
|
|
288
|
+
|
|
289
|
+
begin
|
|
290
|
+
return event.wait(timeout: [remaining, Internal::MAX_WAIT].min)
|
|
291
|
+
rescue ::Async::TimeoutError
|
|
292
|
+
raise if Internal.now >= expires
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def parse_origin(value)
|
|
298
|
+
uri = URI(value)
|
|
299
|
+
unless %w[http https].include?(uri.scheme) && uri.host && !uri.userinfo && !uri.query && !uri.fragment &&
|
|
300
|
+
uri.path.empty?
|
|
301
|
+
raise ArgumentError, "Origin must contain only an HTTP(S) scheme, host and port"
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
uri.freeze
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def same_origin?(uri)
|
|
308
|
+
[uri.scheme, uri.hostname&.downcase, uri.port] == [@origin.scheme, @origin.hostname.downcase, @origin.port]
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def build_resource
|
|
312
|
+
resource = Internal::Resource.new(progress: @progress)
|
|
313
|
+
resource.send(:retirement_parent=, @lifetime)
|
|
314
|
+
@resources.reject! { |entry| entry.send(:retirement_completed?) }
|
|
315
|
+
@resources << resource
|
|
316
|
+
resource
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def abort_setup(resource)
|
|
320
|
+
raw = resource.raw
|
|
321
|
+
# Ruby 4 close may wait for the blocked SSL handshake. A non-flushing
|
|
322
|
+
# shutdown wakes that setup operation before the existing raw-first abort.
|
|
323
|
+
raw.shutdown(Socket::SHUT_RDWR) if raw && !raw.closed?
|
|
324
|
+
rescue IOError, SystemCallError
|
|
325
|
+
nil
|
|
326
|
+
ensure
|
|
327
|
+
resource.abort
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def activate(request, &work)
|
|
331
|
+
request.lifecycle ? request.lifecycle.activate(&work) : work.call
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def setup_connection(resource)
|
|
335
|
+
resource.raw = ::IO::Endpoint.tcp(@origin.hostname, @origin.port).connect
|
|
336
|
+
socket = resource.raw
|
|
337
|
+
if @snapshot
|
|
338
|
+
socket = @snapshot.socket(resource.raw)
|
|
339
|
+
resource.socket = socket
|
|
340
|
+
socket.hostname = @origin.hostname
|
|
341
|
+
socket.connect
|
|
342
|
+
socket.post_connection_check(@origin.hostname)
|
|
343
|
+
raise OpenSSL::SSL::SSLError, "HTTP/2 ALPN is required" unless socket.alpn_protocol == "h2"
|
|
344
|
+
end
|
|
345
|
+
resource.socket = socket
|
|
346
|
+
connection = Internal::ClientConnection.new(::IO::Stream(socket))
|
|
347
|
+
resource.connection = connection
|
|
348
|
+
connection.resource = resource
|
|
349
|
+
connection.send_connection_preface(Internal.settings(@max_requests))
|
|
350
|
+
resource.ready = true
|
|
351
|
+
connection.read_in_background(parent: @lifetime)
|
|
352
|
+
connection
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def exchange(operation)
|
|
356
|
+
request = operation.fetch(:request)
|
|
357
|
+
check_request!(request)
|
|
358
|
+
connection = dispatch(operation) do |guarded, task|
|
|
359
|
+
headers = request.headers.dup
|
|
360
|
+
headers["connect-timeout-ms"] = [request.deadline.timeout_ms] if request.deadline
|
|
361
|
+
body = request.http_method == "GET" ? nil : [request.body]
|
|
362
|
+
outgoing = ::Protocol::HTTP::Request[request.http_method, request.url.request_uri, Internal.fields(headers),
|
|
363
|
+
body, scheme: @origin.scheme, authority: @origin.authority]
|
|
364
|
+
# Stream ID allocation shares the wire dispatch guard.
|
|
365
|
+
response = guarded.create_response
|
|
366
|
+
operation[:response] = response
|
|
367
|
+
task.defer_cancel { guarded.write_request(response, outgoing) }
|
|
368
|
+
guarded
|
|
369
|
+
end
|
|
370
|
+
response = operation.fetch(:response)
|
|
371
|
+
connection.read_response(response)
|
|
372
|
+
headers = Internal.headers(response)
|
|
373
|
+
bytes = read_body(response, request)
|
|
374
|
+
TransportResponse.new(status: response.status, headers: headers, body: bytes)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def read_body(response, request)
|
|
378
|
+
body = response.body
|
|
379
|
+
limit = request.response_limit
|
|
380
|
+
length = body&.length || response.stream.declared_length
|
|
381
|
+
raise Error.new(:resource_exhausted, "Response exceeds byte limit") if length && length > limit
|
|
382
|
+
|
|
383
|
+
bytes = +"".b
|
|
384
|
+
input = Internal::Input.new(body)
|
|
385
|
+
while (chunk = input.read([16_384, limit + 1 - bytes.bytesize].min))
|
|
386
|
+
check_request!(request)
|
|
387
|
+
bytes << chunk
|
|
388
|
+
raise Error.new(:resource_exhausted, "Response exceeds byte limit") if bytes.bytesize > limit
|
|
389
|
+
end
|
|
390
|
+
raise Error.new(:internal, "Content-Length does not match response") if length && bytes.bytesize != length
|
|
391
|
+
|
|
392
|
+
bytes
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def cleanup(operation)
|
|
396
|
+
return if operation[:cleaned]
|
|
397
|
+
|
|
398
|
+
operation[:cleaned] = true
|
|
399
|
+
expires = Internal.now + Internal::CLEANUP_SECONDS
|
|
400
|
+
cleanup_task = ::Async::Task.current.async(finished: false) do
|
|
401
|
+
# The manager owns connection setup; a call owns only its own tasks.
|
|
402
|
+
tasks = [operation[:task], *operation.fetch(:tasks, {}).keys].compact.uniq
|
|
403
|
+
tasks.each { |task| task.cancel unless task.finished? }
|
|
404
|
+
# Cancel every task before joining in reverse ownership order.
|
|
405
|
+
tasks.reverse_each do |task|
|
|
406
|
+
operation[:grpc] ? (task.wait unless task.current?) : Internal.join(task)
|
|
407
|
+
end
|
|
408
|
+
response = operation[:response]
|
|
409
|
+
if response
|
|
410
|
+
stream = response.stream
|
|
411
|
+
case stream.state
|
|
412
|
+
when :idle then stream.close!
|
|
413
|
+
when :closed then nil
|
|
414
|
+
else stream.send_reset_stream(::Protocol::HTTP2::Error::CANCEL)
|
|
415
|
+
end
|
|
416
|
+
response.close
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
begin
|
|
420
|
+
Internal.wait(cleanup_task, expires)
|
|
421
|
+
rescue StandardError => e
|
|
422
|
+
operation[:cleanup_error] ||= e
|
|
423
|
+
begin
|
|
424
|
+
operation[:resource]&.abort
|
|
425
|
+
rescue StandardError
|
|
426
|
+
raise unless operation[:grpc]
|
|
427
|
+
ensure
|
|
428
|
+
Internal.cancel(cleanup_task)
|
|
429
|
+
Internal.join(cleanup_task)
|
|
430
|
+
end
|
|
431
|
+
ensure
|
|
432
|
+
if (connection = operation.delete(:connection))
|
|
433
|
+
begin
|
|
434
|
+
@manager.release(connection)
|
|
435
|
+
rescue StandardError => e
|
|
436
|
+
operation[:cleanup_error] ||= e
|
|
437
|
+
begin
|
|
438
|
+
operation[:resource]&.abort
|
|
439
|
+
# A failed release may precede usage accounting. Retire the
|
|
440
|
+
# aborted connection without retrying release or cleanup.
|
|
441
|
+
@manager.retire(connection)
|
|
442
|
+
rescue StandardError
|
|
443
|
+
raise unless operation[:grpc]
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
if operation[:grpc] && (error = operation[:cleanup_error])
|
|
448
|
+
lifecycle = operation.fetch(:request).lifecycle
|
|
449
|
+
lifecycle&.record_failure(error)
|
|
450
|
+
lifecycle&.finish(delivery: :aborted, error: error)
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def close_owned(expires)
|
|
456
|
+
@manager.close
|
|
457
|
+
waiter = @lifetime.async(finished: false) { @progress.wait until @calls.empty? }
|
|
458
|
+
graceful = true
|
|
459
|
+
begin
|
|
460
|
+
Internal.wait(waiter, expires)
|
|
461
|
+
rescue ::Async::TimeoutError
|
|
462
|
+
graceful = false
|
|
463
|
+
@resources.each(&:abort)
|
|
464
|
+
@calls.each_key { |operation| Internal.cancel(operation[:task]) }
|
|
465
|
+
ensure
|
|
466
|
+
Internal.cancel(waiter)
|
|
467
|
+
end
|
|
468
|
+
drains = @resources.map { |resource| @lifetime.async(finished: false) { resource.drain(@lifetime, expires) } }
|
|
469
|
+
graceful = drains.map(&:wait).all? && graceful
|
|
470
|
+
@manager.finish
|
|
471
|
+
finish_closed(graceful)
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def abort_owned
|
|
475
|
+
return if @aborting
|
|
476
|
+
|
|
477
|
+
@aborting = true
|
|
478
|
+
@state = :closing unless @state == :closed
|
|
479
|
+
@manager&.close
|
|
480
|
+
@resources.each(&:abort)
|
|
481
|
+
@calls.each_key { |operation| Internal.cancel(operation[:task]) }
|
|
482
|
+
@manager&.finish
|
|
483
|
+
finish_closed(false) unless @state == :closed
|
|
484
|
+
Internal.cancel(@lifetime)
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "connectrpc"
|
|
4
|
+
require_relative "async/version"
|
|
5
|
+
require "async"
|
|
6
|
+
require "async/queue"
|
|
7
|
+
require "async/promise"
|
|
8
|
+
require "async/condition"
|
|
9
|
+
require "async/http/protocol/http2"
|
|
10
|
+
require "async/http/protocol/http"
|
|
11
|
+
require "protocol/http/request"
|
|
12
|
+
require "protocol/http/response"
|
|
13
|
+
require "protocol/http/body/wrapper"
|
|
14
|
+
require "protocol/http/body/buffered"
|
|
15
|
+
require "io/endpoint/host_endpoint"
|
|
16
|
+
require "io/endpoint/bound_endpoint"
|
|
17
|
+
require "openssl"
|
|
18
|
+
|
|
19
|
+
module ConnectRPC
|
|
20
|
+
# Explicit, optional integration. The application owns the Async reactor.
|
|
21
|
+
module Async
|
|
22
|
+
class LifecycleError < StandardError; end
|
|
23
|
+
private_constant :LifecycleError
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
require_relative "async/internal"
|
|
28
|
+
require_relative "async/connection_policy"
|
|
29
|
+
require_relative "async/tls"
|
|
30
|
+
require_relative "async/tls_configuration"
|
|
31
|
+
require_relative "async/ingress"
|
|
32
|
+
require_relative "async/connection"
|
|
33
|
+
require_relative "async/connection_manager"
|
|
34
|
+
require_relative "async/stream_exchange"
|
|
35
|
+
require_relative "async/application"
|
|
36
|
+
require_relative "async/server_session"
|
|
37
|
+
require_relative "async/host"
|
|
38
|
+
require_relative "async/transport"
|
|
39
|
+
require_relative "async/http1_transport"
|