grpc-dc 0.37.1

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,397 @@
1
+ # @generated by grpc-dc codegen. DO NOT EDIT.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ # Endpoint: multiplexes grpc-dc streams over one Transport.
4
+
5
+ require_relative "codec"
6
+ require_relative "wire_gen"
7
+
8
+ module Grpcdc
9
+ EVENT_HEADERS = "headers"
10
+ EVENT_DATA = "data"
11
+ EVENT_HALF_CLOSE = "half-close"
12
+ EVENT_TRAILERS = "trailers"
13
+ EVENT_RST = "rst"
14
+
15
+ # Carries whole DataChannel messages (boundaries preserved).
16
+ #
17
+ # recv returns one message, or raises EOFError when the
18
+ # transport is closed.
19
+ class Transport
20
+ def send(_data)
21
+ raise NotImplementedError
22
+ end
23
+
24
+ def recv
25
+ raise NotImplementedError
26
+ end
27
+ end
28
+
29
+ class StreamEvent
30
+ attr_reader :kind, :headers, :payload, :status
31
+
32
+ def initialize(kind, headers: nil, payload: nil, status: nil)
33
+ @kind = kind
34
+ @headers = headers
35
+ @payload = payload
36
+ @status = status
37
+ end
38
+ end
39
+
40
+ # Receiving queue of one stream; closed after a terminal event.
41
+ class StreamInput
42
+ CLOSED = Object.new
43
+
44
+ def initialize
45
+ @events = Queue.new
46
+ end
47
+
48
+ def put(event)
49
+ @events << event
50
+ end
51
+
52
+ def close
53
+ @events << CLOSED
54
+ end
55
+
56
+ TIMEOUT = Object.new
57
+
58
+ # Next event, or nil once the stream is closed. With a
59
+ # timeout (seconds), returns TIMEOUT when the wait expires.
60
+ def get(timeout = nil)
61
+ return get_blocking if timeout.nil?
62
+
63
+ # Portable timed wait: Queue#pop(timeout:) only exists on
64
+ # Ruby >= 3.2 (3.1 raises immediately), so poll non-blocking.
65
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
66
+ loop do
67
+ begin
68
+ event = @events.pop(true)
69
+ return event.equal?(CLOSED) ? nil : event
70
+ rescue ThreadError
71
+ return TIMEOUT if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
72
+
73
+ sleep(0.001)
74
+ end
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def get_blocking
81
+ event = @events.pop
82
+ event.equal?(CLOSED) ? nil : event
83
+ end
84
+
85
+ public
86
+ end
87
+
88
+ class ResponseSink
89
+ # Wired to the CallContext maps by the endpoint.
90
+ attr_accessor :resp_headers, :resp_trailers
91
+
92
+ def initialize(endpoint, stream_id)
93
+ @endpoint = endpoint
94
+ @id = stream_id
95
+ @resp_headers = {}
96
+ @resp_trailers = {}
97
+ @headers_sent = false
98
+ end
99
+
100
+ def send_data(payload)
101
+ # A stream the endpoint has forgotten was RST by the peer
102
+ # (or the channel went away): fail the send so a producing
103
+ # handler stops instead of streaming into the void.
104
+ if @endpoint.stream(@id).nil?
105
+ raise StatusError.new(CODE_CANCELLED, "call cancelled")
106
+ end
107
+
108
+ flush_headers
109
+ @endpoint.send_frame(Codec.encode_frame(FRAME_DATA, @id, payload))
110
+ end
111
+
112
+ def flush_headers
113
+ return if @headers_sent
114
+
115
+ @headers_sent = true
116
+ send_headers(@resp_headers)
117
+ end
118
+
119
+ def send_headers(metadata = nil)
120
+ if @endpoint.stream(@id).nil?
121
+ raise StatusError.new(CODE_CANCELLED, "call cancelled")
122
+ end
123
+
124
+ @headers_sent = true
125
+ @endpoint.send_frame(Codec.headers_frame(@id, nil, metadata || @resp_headers))
126
+ end
127
+
128
+ def send_trailers(status)
129
+ if @resp_trailers && !@resp_trailers.empty?
130
+ merged = @resp_trailers.merge(status.trailers || {})
131
+ status = StatusError.new(status.code, status.message)
132
+ status.trailers = merged
133
+ end
134
+ @endpoint.send_frame(Codec.status_frame(FRAME_TRAILERS, @id, status))
135
+ end
136
+
137
+ # Server sink interface used by Server.
138
+ def send(payload)
139
+ send_data(payload)
140
+ end
141
+ end
142
+
143
+ # Per-call context: metadata sent in HEADERS, plus deadline.
144
+ class CallContext
145
+ attr_reader :metadata, :deadline, :response_headers, :response_trailers
146
+
147
+ # Record response initial metadata (sent lazily, before
148
+ # the first response message).
149
+ def set_headers(md)
150
+ @response_headers.merge!(md)
151
+ end
152
+
153
+ # Record response trailing metadata (rides the terminal
154
+ # status frame).
155
+ def set_trailers(md)
156
+ @response_trailers.merge!(md)
157
+ end
158
+
159
+ def initialize(metadata = nil, timeout: nil)
160
+ @metadata = metadata || {}
161
+ @response_headers = {}
162
+ @response_trailers = {}
163
+ remaining = Codec.parse_timeout(@metadata["grpc-timeout"])
164
+ @deadline = remaining.nil? ? nil : Process.clock_gettime(Process::CLOCK_MONOTONIC) + remaining
165
+ end
166
+
167
+ # Seconds left before the deadline, or nil when unbounded.
168
+ def time_remaining
169
+ return nil if @deadline.nil?
170
+
171
+ [@deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0.0].max
172
+ end
173
+
174
+ def deadline_exceeded?
175
+ time_remaining == 0.0
176
+ end
177
+ end
178
+
179
+ # Multiplexes streams over one Transport.
180
+ #
181
+ # The side that created the channel allocates odd stream ids,
182
+ # the answerer even.
183
+ class Endpoint
184
+ attr_reader :transport
185
+ attr_accessor :dispatcher
186
+
187
+ def initialize(transport, creator:)
188
+ @transport = transport
189
+ @dispatcher = nil
190
+ @next_id = creator ? 1 : 2
191
+ @send_lock = Mutex.new
192
+ @streams = {}
193
+ @streams_lock = Mutex.new
194
+ @closed = false
195
+ @started = false
196
+ end
197
+
198
+ # Start the read loop (idempotent). Called after any
199
+ # dispatcher is installed, so an early HEADERS can never race
200
+ # dispatcher setup.
201
+ def start
202
+ return if @started
203
+
204
+ @started = true
205
+ Thread.new { read_loop }
206
+ end
207
+
208
+ def set_dispatcher(dispatcher)
209
+ @dispatcher = dispatcher
210
+ end
211
+
212
+ def send_frame(frame)
213
+ @send_lock.synchronize { @transport.send(frame) }
214
+ end
215
+
216
+ def open_stream
217
+ stream_id = @send_lock.synchronize do
218
+ id = @next_id
219
+ @next_id += 2
220
+ id
221
+ end
222
+ stream_input = StreamInput.new
223
+ @streams_lock.synchronize { @streams[stream_id] = stream_input }
224
+ [stream_id, stream_input]
225
+ end
226
+
227
+ def close_stream(stream_id)
228
+ @streams_lock.synchronize { @streams.delete(stream_id) }
229
+ end
230
+
231
+ # Abort one stream: RST towards the peer; any thread blocked
232
+ # in recv wakes with the cancellation status (the local input
233
+ # is closed with a synthetic RST event).
234
+ def cancel_stream(stream_id)
235
+ status = StatusError.new(CODE_CANCELLED, "call cancelled")
236
+ input = @streams_lock.synchronize { @streams.delete(stream_id) }
237
+ unless input.nil?
238
+ input.put(StreamEvent.new(EVENT_RST, status: status))
239
+ input.close
240
+ end
241
+ begin
242
+ send_frame(Codec.status_frame(FRAME_RST, stream_id, status))
243
+ rescue StandardError
244
+ nil
245
+ end
246
+ end
247
+
248
+ def stream(stream_id)
249
+ @streams_lock.synchronize { @streams[stream_id] }
250
+ end
251
+
252
+ def close
253
+ return if @closed
254
+
255
+ @closed = true
256
+ begin
257
+ @transport.close if @transport.respond_to?(:close)
258
+ rescue StandardError
259
+ nil
260
+ end
261
+ fail_all
262
+ end
263
+
264
+ private
265
+
266
+ def fail_all
267
+ pending = @streams_lock.synchronize do
268
+ items = @streams.to_a
269
+ @streams.clear
270
+ items
271
+ end
272
+ pending.each do |_stream_id, stream_input|
273
+ stream_input.put(StreamEvent.new(
274
+ EVENT_RST,
275
+ status: StatusError.new(CODE_UNAVAILABLE, "data channel closed")
276
+ ))
277
+ stream_input.close
278
+ end
279
+ end
280
+
281
+ def read_loop
282
+ loop do
283
+ begin
284
+ data = @transport.recv
285
+ rescue StandardError
286
+ fail_all
287
+ return
288
+ end
289
+ return if @closed
290
+
291
+ on_bytes(data)
292
+ end
293
+ end
294
+
295
+ def on_bytes(data)
296
+ begin
297
+ frame_type, stream_id, payload = Codec.decode_frame(data)
298
+ rescue StatusError
299
+ return # malformed frame: drop, keep the channel up
300
+ end
301
+ case frame_type
302
+ when 1 then on_headers(stream_id, payload)
303
+ when 2 then push_event(stream_id, StreamEvent.new(EVENT_DATA, payload: payload))
304
+ when 3 then on_terminal(stream_id, payload, EVENT_TRAILERS)
305
+ when 4 then push_event(stream_id, StreamEvent.new(EVENT_HALF_CLOSE))
306
+ when 5 then on_terminal(stream_id, payload, EVENT_RST)
307
+ end
308
+
309
+ end
310
+
311
+ def on_terminal(stream_id, payload, kind)
312
+ stream_input = stream(stream_id)
313
+ return unless stream_input
314
+
315
+ stream_input.put(StreamEvent.new(kind, status: Codec.parse_status(payload)))
316
+ stream_input.close
317
+ close_stream(stream_id)
318
+ end
319
+
320
+ def push_event(stream_id, event)
321
+ stream_input = stream(stream_id)
322
+ stream_input&.put(event)
323
+ end
324
+
325
+ def on_headers(stream_id, payload)
326
+ begin
327
+ headers = Codec.parse_headers(payload)
328
+ rescue StatusError => e
329
+ begin
330
+ send_frame(Codec.status_frame(FRAME_RST, stream_id, Grpcdc.to_status(e)))
331
+ rescue StandardError
332
+ nil
333
+ end
334
+ return
335
+ end
336
+ if headers.path.nil?
337
+ # Server response headers for one of our outbound streams.
338
+ stream_input = stream(stream_id)
339
+ stream_input&.put(StreamEvent.new(EVENT_HEADERS, headers: headers))
340
+ return
341
+ end
342
+ if stream(stream_id)
343
+ begin
344
+ send_frame(Codec.status_frame(
345
+ FRAME_RST, stream_id,
346
+ StatusError.new(CODE_INTERNAL, "HEADERS for stream already in use")
347
+ ))
348
+ rescue StandardError
349
+ nil
350
+ end
351
+ return
352
+ end
353
+ if @dispatcher.nil?
354
+ begin
355
+ send_frame(Codec.status_frame(
356
+ FRAME_TRAILERS, stream_id,
357
+ StatusError.new(CODE_UNIMPLEMENTED, "unknown method #{headers.path}")
358
+ ))
359
+ rescue StandardError
360
+ nil
361
+ end
362
+ return
363
+ end
364
+ stream_input = StreamInput.new
365
+ @streams_lock.synchronize { @streams[stream_id] = stream_input }
366
+ sink = ResponseSink.new(self, stream_id)
367
+ dispatcher = @dispatcher
368
+ Thread.new do
369
+ begin
370
+ # No eager HEADERS: initial metadata flushes lazily
371
+ # via the sink (first DATA, or below on a DATA-less
372
+ # success) so handlers can set_headers first. Errors
373
+ # end trailers-only, as gRPC allows.
374
+ ctx = CallContext.new(headers.metadata)
375
+ sink.resp_headers = ctx.response_headers
376
+ sink.resp_trailers = ctx.response_trailers
377
+ status = dispatcher.call(ctx, headers, stream_input, sink)
378
+ if status.nil?
379
+ status = StatusError.new(CODE_OK, "")
380
+ begin
381
+ sink.flush_headers
382
+ rescue StandardError
383
+ nil
384
+ end
385
+ end
386
+ begin
387
+ sink.send_trailers(status)
388
+ rescue StandardError
389
+ nil
390
+ end
391
+ ensure
392
+ close_stream(stream_id)
393
+ end
394
+ end
395
+ end
396
+ end
397
+ end
@@ -0,0 +1,250 @@
1
+ # @generated by grpc-dc codegen. DO NOT EDIT.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ # Event bus (grpcdc.events.v1.EventBus): in-memory broker,
4
+ # client helpers, and tag matching. Wire messages and stubs
5
+ # are generated from proto/events.proto (events_pb,
6
+ # events_grpc_dc); the broker policy comes from the shared
7
+ # event-bus spec.
8
+ #
9
+ # Delivery is at-most-once: each subscriber has a bounded
10
+ # queue (SUBSCRIBER_QUEUE_CAPACITY) and drops its oldest
11
+ # events when it falls behind; stats reports the drops.
12
+
13
+ require "thread"
14
+
15
+ require_relative "events_pb"
16
+ require_relative "events_grpc_dc"
17
+
18
+ module Grpcdc
19
+ module Eventbus
20
+ SUBSCRIBER_QUEUE_CAPACITY = 128
21
+
22
+ # Canonical identity of one tag value. The oneof kind is
23
+ # part of the identity (int 5 != "5" != enum 5). Kinds are
24
+ # told apart by value, so an all-default TagValue reads as
25
+ # the empty string value; numbers compare bit-exact, never
26
+ # via decimal text.
27
+ def self.value_key(tv)
28
+ return "s:#{tv.string_value}" unless tv.string_value.empty?
29
+ return "i:#{tv.int_value}" unless tv.int_value.zero?
30
+ return "d:#{[tv.double_value].pack("G").unpack1("H*")}" unless tv.double_value.zero?
31
+ return "b:true" if tv.bool_value
32
+ return "e:#{tv.enum_value}" unless tv.enum_value.zero?
33
+
34
+ "s:"
35
+ end
36
+
37
+ # Stable key for one TagValue, e.g. "1|s:orders".
38
+ def self.tag_value_key(tv)
39
+ "#{tv.tag}|#{value_key(tv)}"
40
+ end
41
+
42
+ # Containment match: the event is delivered when its tag
43
+ # list contains every (tag, value) pair of the filter. An
44
+ # empty filter matches everything.
45
+ def self.tags_match(event_tags, filter_tags)
46
+ return true if filter_tags.nil? || filter_tags.empty?
47
+
48
+ have = {}
49
+ (event_tags || []).each { |tv| have[tag_value_key(tv)] = true }
50
+ filter_tags.all? { |f| have.key?(tag_value_key(f)) }
51
+ end
52
+
53
+ def self.tag_string(tag, value)
54
+ EventsPb::TagValue.new(tag: tag, string_value: value.to_s)
55
+ end
56
+
57
+ def self.tag_int(tag, value)
58
+ EventsPb::TagValue.new(tag: tag, int_value: Integer(value))
59
+ end
60
+
61
+ def self.tag_double(tag, value)
62
+ EventsPb::TagValue.new(tag: tag, double_value: Float(value))
63
+ end
64
+
65
+ def self.tag_bool(tag, value)
66
+ EventsPb::TagValue.new(tag: tag, bool_value: value ? true : false)
67
+ end
68
+
69
+ def self.tag_enum(tag, value)
70
+ EventsPb::TagValue.new(tag: tag, enum_value: Integer(value))
71
+ end
72
+
73
+ class Subscriber
74
+ attr_reader :filter, :dropped
75
+
76
+ def initialize(filter_tags, capacity)
77
+ @filter = filter_tags || []
78
+ @capacity = capacity
79
+ @queue = []
80
+ @mutex = Mutex.new
81
+ @cond = ConditionVariable.new
82
+ @dropped = 0
83
+ end
84
+
85
+ def push(event)
86
+ @mutex.synchronize do
87
+ dropped = false
88
+ if @queue.length >= @capacity
89
+ @queue.shift
90
+ @dropped += 1
91
+ dropped = true
92
+ end
93
+ @queue << event
94
+ @cond.broadcast
95
+ dropped
96
+ end
97
+ end
98
+
99
+ # Next queued event, or nil when the wait times out. The
100
+ # broker re-checks every tick; a cancelled call is reaped
101
+ # when its next matching send fails (the runtime surfaces
102
+ # cancellation only at send time).
103
+ def take(timeout)
104
+ @mutex.synchronize do
105
+ @cond.wait(@mutex, timeout) if @queue.empty?
106
+ @queue.empty? ? nil : @queue.shift
107
+ end
108
+ end
109
+ end
110
+
111
+ # In-memory grpcdc.events.v1.EventBus broker. Any peer can
112
+ # host it: register() it on a Grpcdc::Server next to your
113
+ # own services, or call publish_event directly in-process.
114
+ class EventBusBroker
115
+ def initialize(queue_capacity: SUBSCRIBER_QUEUE_CAPACITY)
116
+ @queue_capacity = queue_capacity
117
+ @mutex = Mutex.new
118
+ @subscribers = []
119
+ @seq = 0
120
+ @published = 0
121
+ @dropped = 0
122
+ end
123
+
124
+ # Accept an event: assign its sequence number and
125
+ # timestamp and fan it out to matching subscribers.
126
+ # Returns the stamped EventsPb::Event.
127
+ def publish_event(tags, payload, publisher_id = "")
128
+ event = nil
129
+ subscribers = nil
130
+ @mutex.synchronize do
131
+ @seq += 1
132
+ event = EventsPb::Event.new(
133
+ tags: tags || [],
134
+ payload: (payload || "".b).to_s.b,
135
+ seq: @seq,
136
+ publisher_id: publisher_id || "",
137
+ timestamp_unix_ms: (Time.now.to_f * 1000).to_i
138
+ )
139
+ @published += 1
140
+ subscribers = @subscribers.dup
141
+ end
142
+ subscribers.each do |sub|
143
+ next unless Eventbus.tags_match(event.tags, sub.filter)
144
+
145
+ if sub.push(event)
146
+ @mutex.synchronize { @dropped += 1 }
147
+ end
148
+ end
149
+ event
150
+ end
151
+
152
+ def stats
153
+ @mutex.synchronize do
154
+ { "published" => @published, "subscribers" => @subscribers.length, "dropped" => @dropped }
155
+ end
156
+ end
157
+
158
+ # grpcdc.events.v1.EventBus service implementation
159
+ # (register calls these).
160
+ def Publish(_ctx, req)
161
+ event = publish_event(req.tags, req.payload, req.publisher_id)
162
+ EventsPb::PublishReply.new(seq: event.seq)
163
+ end
164
+
165
+ def Subscribe(req, stream)
166
+ sub = Subscriber.new(req.filter, @queue_capacity)
167
+ @mutex.synchronize { @subscribers << sub }
168
+ begin
169
+ loop do
170
+ event = sub.take(0.1)
171
+ stream.send(event) unless event.nil?
172
+ end
173
+ ensure
174
+ @mutex.synchronize { @subscribers.delete(sub) }
175
+ end
176
+ end
177
+
178
+ # In-process subscription: events fan out to it exactly
179
+ # like an RPC subscriber, without a call. Useful embedded
180
+ # and for tests; close() unregisters.
181
+ def subscribe_local(filter_tags)
182
+ sub = Subscriber.new(filter_tags, @queue_capacity)
183
+ @mutex.synchronize { @subscribers << sub }
184
+ LocalSubscription.new(self, sub)
185
+ end
186
+
187
+ def remove_subscriber(sub)
188
+ @mutex.synchronize { @subscribers.delete(sub) }
189
+ end
190
+
191
+ def register(server)
192
+ EventsGrpcDc.register_event_bus_server(server, self)
193
+ self
194
+ end
195
+ end
196
+
197
+ # Handle for EventBusBroker#subscribe_local: pull events
198
+ # in-process. take returns the next event (waiting up to
199
+ # the timeout, in seconds), or nil; dropped counts this
200
+ # subscription's drop-oldest losses.
201
+ class LocalSubscription
202
+ def initialize(broker, sub)
203
+ @broker = broker
204
+ @sub = sub
205
+ end
206
+
207
+ def dropped
208
+ @sub.dropped
209
+ end
210
+
211
+ def take(timeout = 1.0)
212
+ deadline = Time.now + timeout
213
+ loop do
214
+ remaining = deadline - Time.now
215
+ return @sub.take(0) if remaining <= 0
216
+
217
+ event = @sub.take([remaining, 0.1].min)
218
+ return event unless event.nil?
219
+ return nil if Time.now >= deadline
220
+ end
221
+ end
222
+
223
+ def close
224
+ @broker.remove_subscriber(@sub)
225
+ end
226
+ end
227
+
228
+ # Publish through a generated EventBusClient; returns the
229
+ # broker-assigned sequence number.
230
+ def self.publish_event(client, tags, payload, publisher_id = "")
231
+ reply = client.Publish(EventsPb::PublishRequest.new(
232
+ tags: tags || [],
233
+ payload: (payload || "".b).to_s.b,
234
+ publisher_id: publisher_id || ""
235
+ ))
236
+ reply.seq
237
+ end
238
+
239
+ # Subscribe through a generated EventBusClient; returns the
240
+ # receiving stream (enumerate it, or close() it to
241
+ # unsubscribe).
242
+ def self.subscribe_events(client, filter_tags)
243
+ client.Subscribe(EventsPb::SubscribeRequest.new(filter: filter_tags || []))
244
+ end
245
+
246
+ # Re-exported so users can build a client without requiring
247
+ # the stub module directly.
248
+ EventBusClient = EventsGrpcDc::EventBusClient
249
+ end
250
+ end