connectrpc 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 +501 -0
- data/docs/design.md +504 -0
- data/docs/development.md +566 -0
- data/docs/grpc-web.md +165 -0
- data/docs/grpc.md +75 -0
- data/docs/rbs.md +469 -0
- data/docs/streaming.md +251 -0
- data/exe/protoc-gen-connectrpc-ruby +23 -0
- data/lib/connectrpc/bindings/descriptors/status.bin +15 -0
- data/lib/connectrpc/bindings/google_protobuf.rb +92 -0
- data/lib/connectrpc/cancellation.rb +220 -0
- data/lib/connectrpc/client.rb +260 -0
- data/lib/connectrpc/compression.rb +120 -0
- data/lib/connectrpc/deadline.rb +81 -0
- data/lib/connectrpc/error.rb +72 -0
- data/lib/connectrpc/generator/names.rb +105 -0
- data/lib/connectrpc/generator/rbs_renderer.rb +93 -0
- data/lib/connectrpc/generator/renderer.rb +115 -0
- data/lib/connectrpc/generator/runtime_names.rb +84 -0
- data/lib/connectrpc/generator/schema.rb +218 -0
- data/lib/connectrpc/generator/streaming_methods.rb +29 -0
- data/lib/connectrpc/generator.rb +42 -0
- data/lib/connectrpc/get_query.rb +159 -0
- data/lib/connectrpc/grpc.rb +200 -0
- data/lib/connectrpc/grpc_client.rb +23 -0
- data/lib/connectrpc/grpc_server.rb +158 -0
- data/lib/connectrpc/grpc_web.rb +131 -0
- data/lib/connectrpc/grpc_web_cors.rb +135 -0
- data/lib/connectrpc/grpc_web_io.rb +156 -0
- data/lib/connectrpc/grpc_web_server.rb +207 -0
- data/lib/connectrpc/instrumentation.rb +216 -0
- data/lib/connectrpc/message_io.rb +47 -0
- data/lib/connectrpc/metadata.rb +105 -0
- data/lib/connectrpc/method.rb +68 -0
- data/lib/connectrpc/protocol.rb +189 -0
- data/lib/connectrpc/server.rb +398 -0
- data/lib/connectrpc/streaming/client.rb +104 -0
- data/lib/connectrpc/streaming/client_execution.rb +87 -0
- data/lib/connectrpc/streaming/client_scope.rb +105 -0
- data/lib/connectrpc/streaming/framing.rb +123 -0
- data/lib/connectrpc/streaming/framing_io.rb +61 -0
- data/lib/connectrpc/streaming/method.rb +33 -0
- data/lib/connectrpc/streaming/server.rb +122 -0
- data/lib/connectrpc/streaming/server_execution.rb +88 -0
- data/lib/connectrpc/streaming/server_terminal.rb +70 -0
- data/lib/connectrpc/streaming/session.rb +143 -0
- data/lib/connectrpc/streaming/session_completion.rb +134 -0
- data/lib/connectrpc/streaming/session_io.rb +107 -0
- data/lib/connectrpc/streaming/session_messages.rb +119 -0
- data/lib/connectrpc/streaming/strategy.rb +95 -0
- data/lib/connectrpc/streaming/streams.rb +116 -0
- data/lib/connectrpc/streaming/terminal.rb +52 -0
- data/lib/connectrpc/transports/net_http.rb +246 -0
- data/lib/connectrpc/version.rb +5 -0
- data/lib/connectrpc.rb +38 -0
- data/rbs/google_protobuf_4_36/runtime.rbs +41 -0
- data/sig/connectrpc.rbs +772 -0
- data/sig/manifest.yaml +2 -0
- data/third_party/README.md +16 -0
- data/third_party/google/LICENSE +201 -0
- data/third_party/google/rpc/status.proto +48 -0
- data/third_party/protobuf-LICENSE +32 -0
- metadata +106 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ConnectRPC
|
|
4
|
+
module Streaming
|
|
5
|
+
# Read and write guards release only their own accepted operation.
|
|
6
|
+
class Direction
|
|
7
|
+
attr_reader :busy
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@busy = false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run
|
|
14
|
+
entered = false
|
|
15
|
+
begin
|
|
16
|
+
raise StreamStateError, "Overlapping stream operation" if @busy
|
|
17
|
+
|
|
18
|
+
@busy = entered = true
|
|
19
|
+
yield
|
|
20
|
+
ensure
|
|
21
|
+
@busy = false if entered
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class Session
|
|
27
|
+
attr_reader :trailers, :received, :sent
|
|
28
|
+
|
|
29
|
+
def initialize(binding:, method:, codec:, state:, input:, output:, request_limit:, response_limit:,
|
|
30
|
+
request_wire_limit:, response_wire_limit:, request_encoding:, response_encoding:,
|
|
31
|
+
terminal_wire_limit:, terminal_limit:, server: false, context: nil, lifecycle: nil,
|
|
32
|
+
accept_encoding: [:identity], strategy: ConnectStrategy.new, publish: nil)
|
|
33
|
+
@strategy = strategy
|
|
34
|
+
@publish = publish
|
|
35
|
+
initialize_owner(binding, method, codec, state)
|
|
36
|
+
initialize_channels(input, output, server, context, lifecycle)
|
|
37
|
+
@accept_encoding = accept_encoding
|
|
38
|
+
initialize_messages(request_limit, response_limit)
|
|
39
|
+
initialize_framing(request_wire_limit, response_wire_limit, request_encoding, response_encoding,
|
|
40
|
+
terminal_wire_limit: terminal_wire_limit, terminal_limit: terminal_limit)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def abandoned? = @abandoned
|
|
44
|
+
def consumed? = @eof
|
|
45
|
+
def send_failure?(error) = error.equal?(@send_failure)
|
|
46
|
+
|
|
47
|
+
def owned_error?(error)
|
|
48
|
+
error.equal?(@reading_error) || error.equal?(@writing_error) || error.equal?(@abandon_error)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def close
|
|
52
|
+
@closed = true
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def initialize_owner(binding, method, codec, state)
|
|
59
|
+
@binding = binding
|
|
60
|
+
@method = method
|
|
61
|
+
@codec = codec
|
|
62
|
+
@state = state
|
|
63
|
+
@thread = Thread.current
|
|
64
|
+
@scheduler = Fiber.scheduler
|
|
65
|
+
@closed = @send_closed = @eof = @abandoned = false
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def initialize_channels(input, output, server, context, lifecycle)
|
|
69
|
+
@input = input
|
|
70
|
+
@output = output
|
|
71
|
+
@server = server
|
|
72
|
+
@context = context
|
|
73
|
+
@lifecycle = lifecycle
|
|
74
|
+
@reading = Direction.new
|
|
75
|
+
@writing = Direction.new
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def initialize_messages(request_limit, response_limit)
|
|
79
|
+
@received = @sent = 0
|
|
80
|
+
@read_limit = @server ? request_limit : response_limit
|
|
81
|
+
@write_limit = @server ? response_limit : request_limit
|
|
82
|
+
@read_type = @server ? @method.request_type : @method.response_type
|
|
83
|
+
@write_type = @server ? @method.response_type : @method.request_type
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def initialize_framing(request_wire, response_wire, request_encoding, response_encoding,
|
|
87
|
+
terminal_wire_limit:, terminal_limit:)
|
|
88
|
+
initialize_reader(request_wire, response_wire, request_encoding, response_encoding,
|
|
89
|
+
terminal_wire_limit: terminal_wire_limit, terminal_limit: terminal_limit)
|
|
90
|
+
initialize_writer(request_wire, response_wire, request_encoding, response_encoding,
|
|
91
|
+
terminal_wire_limit: terminal_wire_limit, terminal_limit: terminal_limit)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def initialize_reader(request_wire, response_wire, request_encoding, response_encoding,
|
|
95
|
+
terminal_wire_limit:, terminal_limit:)
|
|
96
|
+
@reader = @strategy.framing.new(input: self, request: @server, limit: @read_limit, state: @state,
|
|
97
|
+
wire_limit: @server ? request_wire : response_wire,
|
|
98
|
+
encoding: @server ? request_encoding : response_encoding,
|
|
99
|
+
terminal_wire_limit:, terminal_limit:,
|
|
100
|
+
on_error: ->(error) { @lifecycle&.record_failure(error) })
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def initialize_writer(request_wire, response_wire, request_encoding, response_encoding,
|
|
104
|
+
terminal_wire_limit:, terminal_limit:)
|
|
105
|
+
@writer = @strategy.framing.new(input: nil, output: self, request: !@server, limit: @write_limit, state: @state,
|
|
106
|
+
wire_limit: @server ? response_wire : request_wire,
|
|
107
|
+
encoding: @server ? response_encoding : request_encoding,
|
|
108
|
+
terminal_wire_limit: terminal_wire_limit,
|
|
109
|
+
terminal_limit: terminal_limit,
|
|
110
|
+
on_error: ->(error) { @lifecycle&.record_failure(error) })
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def check_open!
|
|
114
|
+
raise StreamStateError, "Stream scope is closed" if @closed || @abandoned
|
|
115
|
+
return if Thread.current.equal?(@thread) && Fiber.scheduler.equal?(@scheduler)
|
|
116
|
+
|
|
117
|
+
raise StreamStateError, "Use the stream's owning reactor and thread"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def operation(direction)
|
|
121
|
+
guard = direction == :reading ? @reading : @writing
|
|
122
|
+
guard.run do
|
|
123
|
+
check_open!
|
|
124
|
+
@state.check!
|
|
125
|
+
yield
|
|
126
|
+
end
|
|
127
|
+
rescue StandardError => e
|
|
128
|
+
record_operation_failure(e, direction)
|
|
129
|
+
raise
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def record_operation_failure(error, direction)
|
|
133
|
+
# Independent directions retain one failure each across application unwind.
|
|
134
|
+
case direction
|
|
135
|
+
when :reading then @reading_error = error
|
|
136
|
+
when :writing then @writing_error = error
|
|
137
|
+
when :abandon then @abandon_error = error
|
|
138
|
+
end
|
|
139
|
+
@lifecycle&.record_failure(error)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ConnectRPC
|
|
4
|
+
module Streaming
|
|
5
|
+
class Session
|
|
6
|
+
# Recovery runs only after the scope relinquishes normal consumption.
|
|
7
|
+
# It cannot steal a live reader, decode skipped data or resume the endpoint.
|
|
8
|
+
def recover_send_failure
|
|
9
|
+
return @remote_error if @eof
|
|
10
|
+
return unless recovery_available?
|
|
11
|
+
|
|
12
|
+
@recovering = true
|
|
13
|
+
@recovery_expires ||= now + RECOVERY_SECONDS
|
|
14
|
+
@recovery_remaining ||= RECOVERY_LIMIT
|
|
15
|
+
operation(:reading) { recover_terminal }
|
|
16
|
+
rescue StandardError => e
|
|
17
|
+
@lifecycle&.record_failure(e)
|
|
18
|
+
nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def recovery_available? = !@reading.busy && !@server && !@abandoned
|
|
24
|
+
|
|
25
|
+
def recover_terminal # rubocop:disable Metrics/MethodLength
|
|
26
|
+
leading
|
|
27
|
+
loop do
|
|
28
|
+
frame = @reader.read_frame(skip_data: true)
|
|
29
|
+
unless frame
|
|
30
|
+
return recovered_grpc_error if @strategy.grpc?
|
|
31
|
+
|
|
32
|
+
return
|
|
33
|
+
end
|
|
34
|
+
return recovered_error(frame.last) if frame.first.anybits?(2)
|
|
35
|
+
|
|
36
|
+
@received += 1
|
|
37
|
+
return if !@strategy.grpc? && @method.is_a?(ClientStreamMethod) && @received > 1
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def recovered_grpc_error
|
|
42
|
+
strategy = @strategy #: GRPCProtocol::Strategy
|
|
43
|
+
exchange = @input #: _GRPCStreamExchange
|
|
44
|
+
return unless strategy.recoverable?(exchange)
|
|
45
|
+
|
|
46
|
+
accept_grpc_terminal
|
|
47
|
+
nil
|
|
48
|
+
rescue Error => e
|
|
49
|
+
return e if e.equal?(@remote_error) && @eof
|
|
50
|
+
|
|
51
|
+
raise
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def accept_grpc_terminal
|
|
55
|
+
strategy = @strategy #: GRPCProtocol::Strategy
|
|
56
|
+
exchange = @input #: _GRPCStreamExchange
|
|
57
|
+
@remote_error, @trailers = strategy.terminal(exchange, @headers || Metadata.new)
|
|
58
|
+
validate_terminal_count
|
|
59
|
+
expires = @recovery_expires || (now + Deadline::MAX_NATIVE_WAIT)
|
|
60
|
+
finish_transport(expires)
|
|
61
|
+
@eof = true
|
|
62
|
+
error = @remote_error
|
|
63
|
+
raise error, cause: nil if error
|
|
64
|
+
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def recovered_error(bytes)
|
|
69
|
+
accept_terminal(bytes)
|
|
70
|
+
nil
|
|
71
|
+
rescue Error => e
|
|
72
|
+
return e if e.equal?(@remote_error) && @eof
|
|
73
|
+
|
|
74
|
+
raise
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def accept_terminal(bytes)
|
|
78
|
+
@remote_error, @trailers = parse_terminal(bytes)
|
|
79
|
+
validate_terminal_count
|
|
80
|
+
expires = @recovery_expires || (now + TAIL_SECONDS)
|
|
81
|
+
verify_tail(expires)
|
|
82
|
+
finish_transport(expires)
|
|
83
|
+
@eof = true
|
|
84
|
+
error = @remote_error
|
|
85
|
+
raise error, cause: nil if error
|
|
86
|
+
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def parse_terminal(bytes)
|
|
91
|
+
Streaming.parse_terminal(bytes, metadata: @headers || Metadata.new,
|
|
92
|
+
on_error: ->(error) { @lifecycle&.record_failure(error) })
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def validate_terminal_count
|
|
96
|
+
return unless !@remote_error && @strategy.single_response?(@method) && @received != 1
|
|
97
|
+
|
|
98
|
+
raise Error.new(:unimplemented, "Expected exactly one response"), cause: nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def verify_tail(expires)
|
|
102
|
+
extra = read(1, timeout: completion_timeout(expires))
|
|
103
|
+
return unless extra && !extra.empty?
|
|
104
|
+
|
|
105
|
+
raise Error.new(:internal, "Body bytes after streaming terminal"), cause: nil
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def finish_transport(expires)
|
|
109
|
+
exchange = @input #: _StreamExchange
|
|
110
|
+
output = @output #: _StreamExchange
|
|
111
|
+
output.close_send
|
|
112
|
+
@send_closed = true
|
|
113
|
+
exchange.finish(timeout: completion_timeout(expires))
|
|
114
|
+
@state.check!
|
|
115
|
+
completion_timeout(expires)
|
|
116
|
+
nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def completion_timeout(expires)
|
|
120
|
+
remaining = expires - now
|
|
121
|
+
raise Error.new(:unavailable, "Streaming completion policy expired"), cause: nil unless remaining.positive?
|
|
122
|
+
|
|
123
|
+
remaining
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def recovery_timeout
|
|
127
|
+
expires = @recovery_expires #: Float
|
|
128
|
+
completion_timeout(expires)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ConnectRPC
|
|
4
|
+
module Streaming
|
|
5
|
+
class Session
|
|
6
|
+
def write(bytes)
|
|
7
|
+
check_open!
|
|
8
|
+
write_chunk(bytes)
|
|
9
|
+
check_open!
|
|
10
|
+
nil
|
|
11
|
+
rescue StandardError => e
|
|
12
|
+
record_send_failure(e)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def read(length, timeout: nil)
|
|
16
|
+
check_open!
|
|
17
|
+
timeout = recovery_read_timeout(length) if @recovery_expires
|
|
18
|
+
deadline = @context&.deadline
|
|
19
|
+
timeout = deadline.remaining if @server && deadline
|
|
20
|
+
bytes = @input.read(length, timeout: @strategy.wait(timeout))
|
|
21
|
+
check_open!
|
|
22
|
+
count_recovery_bytes(bytes)
|
|
23
|
+
bytes
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def write_chunk(bytes)
|
|
29
|
+
if @server
|
|
30
|
+
output = @output #: _StreamingOutput
|
|
31
|
+
output.write(bytes, timeout: @strategy.wait(@context&.deadline&.remaining))
|
|
32
|
+
else
|
|
33
|
+
@output.write(bytes)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def record_send_failure(error)
|
|
38
|
+
@lifecycle&.record_failure(error)
|
|
39
|
+
@state.check!
|
|
40
|
+
if error.is_a?(IOError) || error.is_a?(SystemCallError)
|
|
41
|
+
failure = @send_failure = Error.new(:unavailable, "Streaming write failed")
|
|
42
|
+
raise failure, cause: nil
|
|
43
|
+
end
|
|
44
|
+
@send_failure = error if error.is_a?(Error) && error.code == "unavailable"
|
|
45
|
+
raise error
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def recovery_read_timeout(length)
|
|
49
|
+
remaining = @recovery_remaining #: Integer
|
|
50
|
+
raise Error.new(:resource_exhausted, "Streaming recovery byte limit"), cause: nil if length > remaining
|
|
51
|
+
|
|
52
|
+
recovery_timeout
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def count_recovery_bytes(bytes)
|
|
56
|
+
remaining = @recovery_remaining
|
|
57
|
+
@recovery_remaining = remaining - bytes.bytesize if remaining && bytes
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def leading
|
|
61
|
+
snapshot = @headers
|
|
62
|
+
return snapshot if snapshot
|
|
63
|
+
|
|
64
|
+
@headers = if @server
|
|
65
|
+
@context #: StreamingContext
|
|
66
|
+
.request_metadata.freeze
|
|
67
|
+
else
|
|
68
|
+
response_metadata
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def response_metadata # rubocop:disable Metrics/MethodLength
|
|
73
|
+
exchange = @input #: _StreamExchange
|
|
74
|
+
strategy = @strategy
|
|
75
|
+
if strategy.is_a?(GRPCProtocol::Strategy)
|
|
76
|
+
grpc = @input #: _GRPCStreamExchange
|
|
77
|
+
return strategy.leading(grpc, @reader, @accept_encoding, @recovery_expires ? recovery_timeout : nil,
|
|
78
|
+
input: self)
|
|
79
|
+
end
|
|
80
|
+
head = @recovery_expires ? exchange.response_head(timeout: recovery_timeout) : exchange.response_head
|
|
81
|
+
validate_response_status(head.status)
|
|
82
|
+
values = Protocol.headers(head.headers)
|
|
83
|
+
validate_response_headers(values)
|
|
84
|
+
Protocol.metadata(values).freeze
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def validate_response_status(status)
|
|
88
|
+
return if status == 200
|
|
89
|
+
|
|
90
|
+
raise Error.new(Error::HTTP_FALLBACK.fetch(status, "unknown"), "HTTP #{status}"), cause: nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def validate_response_headers(values)
|
|
94
|
+
unless Streaming.codec(Protocol.single(values, "content-type")) == @codec
|
|
95
|
+
raise Error.new(:internal, "Unexpected streaming response content type"), cause: nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
encoding = Protocol.content_encoding(Streaming.coding_headers(values))
|
|
99
|
+
unless encoding && @accept_encoding.include?(encoding)
|
|
100
|
+
raise Error.new(:internal, "Unsupported streaming response encoding"), cause: nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
@reader.encoding = encoding
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ConnectRPC
|
|
4
|
+
module Streaming
|
|
5
|
+
class Session
|
|
6
|
+
def remote_completed?(error)
|
|
7
|
+
@strategy.grpc? && @eof && error.equal?(@remote_error)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def headers
|
|
11
|
+
@headers || operation(:reading) { leading }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def receive
|
|
15
|
+
operation(:reading) { receive_message }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def send_message(message)
|
|
19
|
+
operation(:writing) do
|
|
20
|
+
validate_send(message)
|
|
21
|
+
encode_message(message)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def close_send
|
|
26
|
+
operation(:writing) do
|
|
27
|
+
return nil if @send_closed
|
|
28
|
+
|
|
29
|
+
@send_closed = true
|
|
30
|
+
output = @output #: _StreamExchange
|
|
31
|
+
output.close_send
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def abandon
|
|
37
|
+
check_open!
|
|
38
|
+
@abandoned = true
|
|
39
|
+
@output.abort
|
|
40
|
+
nil
|
|
41
|
+
rescue StandardError => e
|
|
42
|
+
record_operation_failure(e, :abandon)
|
|
43
|
+
raise
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def receive_message
|
|
49
|
+
raise StreamStateError, "Normal response consumption was relinquished" if @recovering
|
|
50
|
+
|
|
51
|
+
return completed_receive if @eof
|
|
52
|
+
|
|
53
|
+
leading
|
|
54
|
+
frame = @reader.read_frame
|
|
55
|
+
return receive_eof unless frame
|
|
56
|
+
return accept_terminal(frame.last) if frame.first.anybits?(2)
|
|
57
|
+
|
|
58
|
+
decode_message(frame.last)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def completed_receive
|
|
62
|
+
error = @remote_error
|
|
63
|
+
raise error, cause: nil if error
|
|
64
|
+
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def receive_eof
|
|
69
|
+
return accept_grpc_terminal unless @server || !@strategy.grpc?
|
|
70
|
+
|
|
71
|
+
raise Error.new(:internal, "Missing streaming terminal"), cause: nil unless @server
|
|
72
|
+
|
|
73
|
+
@eof = true
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def decode_message(bytes)
|
|
78
|
+
@received += 1
|
|
79
|
+
validate_response_count unless @strategy.grpc?
|
|
80
|
+
message = @binding.decode(bytes, type: @read_type, codec: @codec)
|
|
81
|
+
@state.check!
|
|
82
|
+
raise Error.new(:internal, "Binding decoded a nil streaming message"), cause: nil if message.nil?
|
|
83
|
+
|
|
84
|
+
message
|
|
85
|
+
rescue CodecError => e
|
|
86
|
+
@lifecycle&.record_failure(e)
|
|
87
|
+
raise Error.new(@strategy.decode_error(@server), "Cannot decode streaming message"), cause: nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def validate_response_count
|
|
91
|
+
return unless !@server && @method.is_a?(ClientStreamMethod) && @received > 1
|
|
92
|
+
|
|
93
|
+
raise Error.new(:unimplemented, "Expected exactly one response"), cause: nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def validate_send(message)
|
|
97
|
+
raise StreamStateError, "Request sending is closed" if @send_closed
|
|
98
|
+
raise Error.new(:invalid_argument, "Nil is not a streaming message"), cause: nil if message.nil?
|
|
99
|
+
return unless !@server && @method.is_a?(ServerStreamMethod) && @sent.positive?
|
|
100
|
+
|
|
101
|
+
raise StreamStateError, "Server streaming accepts one request"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def encode_message(message) # rubocop:disable Metrics/MethodLength
|
|
105
|
+
bytes = @binding.encode(message, type: @write_type, codec: @codec)
|
|
106
|
+
@state.check!
|
|
107
|
+
Protocol.check_size!(bytes, @write_limit)
|
|
108
|
+
publisher = @publish
|
|
109
|
+
publisher ? publisher.call : @context&.send(:publish_headers)
|
|
110
|
+
@writer.emit(bytes)
|
|
111
|
+
@sent += 1
|
|
112
|
+
nil
|
|
113
|
+
rescue CodecError => e
|
|
114
|
+
@lifecycle&.record_failure(e)
|
|
115
|
+
raise Error.new(@server ? :internal : :invalid_argument, "Cannot encode streaming message"), cause: nil
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ConnectRPC
|
|
4
|
+
module Streaming
|
|
5
|
+
# Protocol policy alone varies; Session owns all scope and I/O arbitration.
|
|
6
|
+
class ConnectStrategy
|
|
7
|
+
def grpc? = false
|
|
8
|
+
def framing = Framing
|
|
9
|
+
def wait(timeout) = timeout
|
|
10
|
+
def single_response?(method) = method.is_a?(ClientStreamMethod)
|
|
11
|
+
def decode_error(server) = server ? :invalid_argument : :internal
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module GRPCProtocol
|
|
16
|
+
class Framing < Streaming::Framing
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def validate_flags(flags)
|
|
20
|
+
protocol_error("Invalid gRPC envelope flags") unless [0, 1].include?(flags)
|
|
21
|
+
protocol_error("Unnegotiated compression") if flags == 1 && @encoding != :gzip
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def protocol_error(message)
|
|
25
|
+
raise Error.new(:internal, message), cause: nil
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class Strategy
|
|
30
|
+
def initialize(codec:, wire_limit:, limit:, on_error: nil)
|
|
31
|
+
@on_error = on_error
|
|
32
|
+
@codec = codec
|
|
33
|
+
@wire_limit = wire_limit
|
|
34
|
+
@limit = limit
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def grpc? = true
|
|
38
|
+
def framing = Framing
|
|
39
|
+
def wait(timeout) = [timeout || Deadline::MAX_NATIVE_WAIT, Deadline::MAX_NATIVE_WAIT].min
|
|
40
|
+
def single_response?(method) = method.is_a?(Method) || method.is_a?(ClientStreamMethod)
|
|
41
|
+
def decode_error(_server) = :internal
|
|
42
|
+
|
|
43
|
+
def leading(exchange, reader, accept_encoding, timeout, input:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
44
|
+
head = @head = exchange.response_head(timeout: wait(timeout))
|
|
45
|
+
values = Protocol.headers(head.headers)
|
|
46
|
+
GRPCProtocol.check_fields!(values)
|
|
47
|
+
reject_media(input, exchange, head, timeout) unless GRPCProtocol.media?(Protocol.single(values, "content-type"))
|
|
48
|
+
if !head.end_stream && values.key?("grpc-status") && GRPCProtocol.status_value(values).nil?
|
|
49
|
+
raise Error.new(:unknown, "Malformed initial grpc-status"), cause: nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
coding = GRPCProtocol.encoding(values)
|
|
53
|
+
raise Error.new(:internal, "Unadvertised gRPC encoding"), cause: nil unless accept_encoding.include?(coding)
|
|
54
|
+
|
|
55
|
+
reader.encoding = coding
|
|
56
|
+
head.end_stream ? Metadata.new.freeze : Protocol.metadata(values).freeze
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def reject_media(input, exchange, head, timeout) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
60
|
+
if head.end_stream
|
|
61
|
+
terminal = head.headers
|
|
62
|
+
else
|
|
63
|
+
loop do
|
|
64
|
+
bytes = input.read(Streaming::FRAGMENT_SIZE, timeout: wait(timeout))
|
|
65
|
+
break if bytes.nil? || bytes.empty?
|
|
66
|
+
end
|
|
67
|
+
terminal = exchange.response_trailers || {}
|
|
68
|
+
end
|
|
69
|
+
GRPCProtocol.check_fields!(terminal, @wire_limit)
|
|
70
|
+
code = if terminal.key?("grpc-status") || head.headers.key?("grpc-status")
|
|
71
|
+
"internal"
|
|
72
|
+
else
|
|
73
|
+
Error::HTTP_FALLBACK.fetch(head.status, "unknown")
|
|
74
|
+
end
|
|
75
|
+
raise Error.new(code, "Unexpected gRPC response content type"), cause: nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def recoverable?(exchange)
|
|
79
|
+
head = @head #: GRPCHead
|
|
80
|
+
fields = head.end_stream ? head.headers : exchange.response_trailers
|
|
81
|
+
return false unless fields
|
|
82
|
+
|
|
83
|
+
number = GRPCProtocol.status_value(fields)
|
|
84
|
+
!!number&.positive?
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def terminal(exchange, metadata)
|
|
88
|
+
head = @head #: GRPCHead
|
|
89
|
+
fields = head.end_stream ? head.headers : exchange.response_trailers
|
|
90
|
+
GRPCProtocol.parse_status(fields || {}, codec: @codec, http_status: head.status, metadata: metadata,
|
|
91
|
+
wire_limit: @wire_limit, limit: @limit, on_error: @on_error)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|