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.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/README.md +10 -0
- data/lib/grpc_dc.rb +1 -0
- data/lib/grpcdc/channel.rb +327 -0
- data/lib/grpcdc/codec.rb +262 -0
- data/lib/grpcdc/dynamic.rb +677 -0
- data/lib/grpcdc/endpoint.rb +397 -0
- data/lib/grpcdc/eventbus.rb +250 -0
- data/lib/grpcdc/eventbus_git.rb +132 -0
- data/lib/grpcdc/events_grpc_dc.rb +232 -0
- data/lib/grpcdc/events_pb.rb +432 -0
- data/lib/grpcdc/loopback.rb +43 -0
- data/lib/grpcdc/server.rb +106 -0
- data/lib/grpcdc/sidecar.rb +63 -0
- data/lib/grpcdc/wire_gen.rb +58 -0
- data/lib/grpcdc.rb +12 -0
- metadata +61 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# @generated by grpc-dc codegen. DO NOT EDIT.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
# Git log serde for the event bus: exports an event log to a
|
|
4
|
+
# git branch (one commit per event) and imports it back. The
|
|
5
|
+
# live broker never touches git; this is the durable serde
|
|
6
|
+
# layer beside it. Stdlib Open3 drives the git plumbing.
|
|
7
|
+
#
|
|
8
|
+
# Deterministic: the same events always produce the same
|
|
9
|
+
# commits, so re-exporting a log the branch already has is a
|
|
10
|
+
# no-op (each event's sequence number is the dedup key). The
|
|
11
|
+
# commit blob (event.pb) is authoritative; the message subject and
|
|
12
|
+
# trailers are a human index for `git log`.
|
|
13
|
+
|
|
14
|
+
require "open3"
|
|
15
|
+
|
|
16
|
+
require_relative "eventbus"
|
|
17
|
+
require_relative "events_pb"
|
|
18
|
+
|
|
19
|
+
module Grpcdc
|
|
20
|
+
module EventbusGit
|
|
21
|
+
AUTHOR_NAME = "grpc-dc eventbus"
|
|
22
|
+
AUTHOR_EMAIL = "eventbus@grpc-dc.local"
|
|
23
|
+
BLOB_PATH = "event.pb"
|
|
24
|
+
COMMIT_SUBJECT = "grpc-dc event"
|
|
25
|
+
TRAILER_SEQ = "Seq"
|
|
26
|
+
TRAILER_PUBLISHER = "Publisher"
|
|
27
|
+
TRAILER_TAG = "Tag"
|
|
28
|
+
ZERO_SHA = "0" * 40
|
|
29
|
+
|
|
30
|
+
EVENT_LOG_BRANCH = "grpc-dc-events"
|
|
31
|
+
|
|
32
|
+
def self.git(repo, args, input_bytes = nil, env_extra = nil)
|
|
33
|
+
out, _err, status = Open3.capture3(env_extra || {}, "git", "-C", repo, *args, stdin_data: input_bytes || "".b)
|
|
34
|
+
raise "git #{args.first} failed in #{repo}" unless status.success?
|
|
35
|
+
|
|
36
|
+
out
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.git_text(repo, args, input_bytes = nil, env_extra = nil)
|
|
40
|
+
git(repo, args, input_bytes, env_extra).strip
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Create the log repo if needed (a plain `git init`).
|
|
44
|
+
def self.init_event_log_repo(repo)
|
|
45
|
+
git_text(repo, ["rev-parse", "--git-dir"])
|
|
46
|
+
rescue StandardError
|
|
47
|
+
out, _err, status = Open3.capture3("git", "init", "-q", repo)
|
|
48
|
+
raise "git init failed for #{repo}" unless status.success?
|
|
49
|
+
|
|
50
|
+
out
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.branch_head(repo, branch)
|
|
54
|
+
git_text(repo, ["rev-parse", "--verify", "refs/heads/#{branch}"])
|
|
55
|
+
rescue StandardError
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# "grpc-dc event 42" plus Seq/Publisher/Tag trailers, so
|
|
60
|
+
# `git log` alone can filter the log without decoding blobs.
|
|
61
|
+
def self.commit_message(event)
|
|
62
|
+
lines = ["#{COMMIT_SUBJECT} #{event.seq}", "", "#{TRAILER_SEQ}: #{event.seq}"]
|
|
63
|
+
lines << "#{TRAILER_PUBLISHER}: #{event.publisher_id}" unless event.publisher_id.empty?
|
|
64
|
+
(event.tags || []).each { |tv| lines << "#{TRAILER_TAG}: #{Eventbus.tag_value_key(tv)}" }
|
|
65
|
+
lines.join("\n") + "\n"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.commit_env(event)
|
|
69
|
+
seconds = [0, event.timestamp_unix_ms / 1000].max
|
|
70
|
+
date = "#{seconds} +0000"
|
|
71
|
+
{
|
|
72
|
+
"GIT_AUTHOR_NAME" => AUTHOR_NAME,
|
|
73
|
+
"GIT_AUTHOR_EMAIL" => AUTHOR_EMAIL,
|
|
74
|
+
"GIT_COMMITTER_NAME" => AUTHOR_NAME,
|
|
75
|
+
"GIT_COMMITTER_EMAIL" => AUTHOR_EMAIL,
|
|
76
|
+
"GIT_AUTHOR_DATE" => date,
|
|
77
|
+
"GIT_COMMITTER_DATE" => date
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Import a branch: events in chain order (oldest first),
|
|
82
|
+
# optionally windowed by sequence number and narrowed by a
|
|
83
|
+
# tag filter (same containment semantics as Subscribe). A
|
|
84
|
+
# missing branch imports as an empty log.
|
|
85
|
+
def self.import_events(repo, branch = EVENT_LOG_BRANCH, from_seq: 0, to_seq: nil, filter_tags: nil)
|
|
86
|
+
return [] if branch_head(repo, branch).nil?
|
|
87
|
+
|
|
88
|
+
events = []
|
|
89
|
+
git_text(repo, ["rev-list", "--reverse", branch]).split("\n").each do |sha|
|
|
90
|
+
next if sha.empty?
|
|
91
|
+
|
|
92
|
+
data = git(repo, ["cat-file", "-p", "#{sha}:#{BLOB_PATH}"])
|
|
93
|
+
event = EventsPb::Event.new
|
|
94
|
+
event.unmarshal_dc(data)
|
|
95
|
+
next if event.seq <= from_seq
|
|
96
|
+
next if !to_seq.nil? && event.seq > to_seq
|
|
97
|
+
next if filter_tags && !Eventbus.tags_match(event.tags, filter_tags)
|
|
98
|
+
|
|
99
|
+
events << event
|
|
100
|
+
end
|
|
101
|
+
events
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Export events onto the branch: one commit per event, in
|
|
105
|
+
# sequence order, skipping sequence numbers the branch
|
|
106
|
+
# already has. Returns the new head sha (or the existing one
|
|
107
|
+
# when nothing was appended). The ref update is a
|
|
108
|
+
# compare-and-swap: a concurrent exporter fails loudly
|
|
109
|
+
# instead of forking the log; retrying is safe.
|
|
110
|
+
def self.export_events(repo, events, branch = EVENT_LOG_BRANCH)
|
|
111
|
+
init_event_log_repo(repo)
|
|
112
|
+
existing = {}
|
|
113
|
+
import_events(repo, branch).each { |e| existing[e.seq] = true }
|
|
114
|
+
head = branch_head(repo, branch)
|
|
115
|
+
before = head
|
|
116
|
+
events.sort_by(&:seq).each do |event|
|
|
117
|
+
next if existing.key?(event.seq)
|
|
118
|
+
|
|
119
|
+
blob = git_text(repo, ["hash-object", "-w", "--stdin"], event.marshal_dc)
|
|
120
|
+
tree = git_text(repo, ["mktree"], "100644 blob #{blob}\t#{BLOB_PATH}\n".encode(Encoding::UTF_8))
|
|
121
|
+
args = ["commit-tree", tree, "-m", commit_message(event)]
|
|
122
|
+
args += ["-p", head] unless head.nil?
|
|
123
|
+
head = git_text(repo, args, nil, commit_env(event))
|
|
124
|
+
existing[event.seq] = true
|
|
125
|
+
end
|
|
126
|
+
if !head.nil? && head != before
|
|
127
|
+
git_text(repo, ["update-ref", "refs/heads/#{branch}", head, before || ZERO_SHA])
|
|
128
|
+
end
|
|
129
|
+
head
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# Generated code: use without restriction (see GENERATED-CODE-LICENSE.md).
|
|
2
|
+
# @generated by protoc-gen-grpc-dc-ruby. DO NOT EDIT.
|
|
3
|
+
# source: events.proto
|
|
4
|
+
|
|
5
|
+
require_relative "events_pb"
|
|
6
|
+
require "grpcdc"
|
|
7
|
+
|
|
8
|
+
module EventsGrpcDc
|
|
9
|
+
# Client receiving stream: recv raises EOFError at end.
|
|
10
|
+
class RecvStream
|
|
11
|
+
include Enumerable
|
|
12
|
+
|
|
13
|
+
def initialize(stream, resp_cls)
|
|
14
|
+
@stream = stream
|
|
15
|
+
@resp_cls = resp_cls
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def recv
|
|
19
|
+
@stream.recv(@resp_cls.new)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Response initial metadata (after the first recv).
|
|
23
|
+
def headers
|
|
24
|
+
@stream.headers&.metadata
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Response trailing metadata (after the call ends).
|
|
28
|
+
def trailers
|
|
29
|
+
@stream.trailers
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Abandon the call early: RST so the server stops producing.
|
|
33
|
+
def cancel
|
|
34
|
+
@stream.cancel
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def close
|
|
38
|
+
@stream.cancel
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def each
|
|
42
|
+
return enum_for(:each) unless block_given?
|
|
43
|
+
|
|
44
|
+
begin
|
|
45
|
+
loop do
|
|
46
|
+
begin
|
|
47
|
+
yield recv
|
|
48
|
+
rescue EOFError
|
|
49
|
+
return
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
ensure
|
|
53
|
+
# Breaking out of the block abandons the call: RST so
|
|
54
|
+
# the server stops producing (no-op once terminated).
|
|
55
|
+
close
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Server-streaming sink passed to service implementations.
|
|
61
|
+
class SendStream
|
|
62
|
+
attr_reader :context
|
|
63
|
+
|
|
64
|
+
def initialize(ctx, sink)
|
|
65
|
+
@context = ctx
|
|
66
|
+
@sink = sink
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def send(resp)
|
|
70
|
+
@sink.send(resp.marshal_dc)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Client-streaming stream passed to service implementations.
|
|
75
|
+
class ClientStreamImpl
|
|
76
|
+
attr_reader :context, :resp
|
|
77
|
+
|
|
78
|
+
def initialize(ctx, reqs, req_cls)
|
|
79
|
+
@context = ctx
|
|
80
|
+
@reqs = reqs
|
|
81
|
+
@req_cls = req_cls
|
|
82
|
+
@resp = nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def recv
|
|
86
|
+
@reqs.recv_into(@req_cls.new)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def send_and_close(resp)
|
|
90
|
+
@resp = resp
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Bidi stream passed to service implementations.
|
|
95
|
+
class BidiStream
|
|
96
|
+
attr_reader :context
|
|
97
|
+
|
|
98
|
+
def initialize(ctx, reqs, sink, req_cls)
|
|
99
|
+
@context = ctx
|
|
100
|
+
@reqs = reqs
|
|
101
|
+
@sink = sink
|
|
102
|
+
@req_cls = req_cls
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def send(resp)
|
|
106
|
+
@sink.send(resp.marshal_dc)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def recv
|
|
110
|
+
@reqs.recv_into(@req_cls.new)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Fiber-based client receiving stream: a producer Fiber
|
|
115
|
+
# yields each response; next_msg resumes it and raises
|
|
116
|
+
# StopIteration when the call completes. cancel aborts
|
|
117
|
+
# the underlying stream (RST).
|
|
118
|
+
class FiberRecvStream
|
|
119
|
+
include Enumerable
|
|
120
|
+
|
|
121
|
+
DONE = Object.new
|
|
122
|
+
|
|
123
|
+
def initialize(stream, resp_cls)
|
|
124
|
+
@stream = stream
|
|
125
|
+
@resp_cls = resp_cls
|
|
126
|
+
@fiber = Fiber.new do
|
|
127
|
+
begin
|
|
128
|
+
loop { Fiber.yield(@stream.recv(@resp_cls.new)) }
|
|
129
|
+
rescue EOFError
|
|
130
|
+
DONE
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def next_msg
|
|
136
|
+
value = @fiber.resume
|
|
137
|
+
raise StopIteration if value.equal?(DONE)
|
|
138
|
+
|
|
139
|
+
value
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Response initial metadata (after the first recv).
|
|
143
|
+
def headers
|
|
144
|
+
@stream.headers&.metadata
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Response trailing metadata (after the call ends).
|
|
148
|
+
def trailers
|
|
149
|
+
@stream.trailers
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def cancel
|
|
153
|
+
@stream.cancel
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def close
|
|
157
|
+
@stream.cancel
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def each
|
|
161
|
+
return enum_for(:each) unless block_given?
|
|
162
|
+
|
|
163
|
+
begin
|
|
164
|
+
loop do
|
|
165
|
+
begin
|
|
166
|
+
yield next_msg
|
|
167
|
+
rescue StopIteration
|
|
168
|
+
return
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
ensure
|
|
172
|
+
# Breaking out of the block abandons the call: RST so
|
|
173
|
+
# the server stops producing (no-op once terminated).
|
|
174
|
+
close
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
class EventBusClient
|
|
180
|
+
def initialize(channel)
|
|
181
|
+
@channel = channel
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def Publish(req, metadata = nil, timeout: nil)
|
|
185
|
+
@channel.unary("/grpcdc.events.v1.EventBus/Publish", req, EventsPb::PublishReply.new, metadata, timeout: timeout)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def Subscribe(req, metadata = nil, timeout: nil)
|
|
189
|
+
stream = @channel.server_stream("/grpcdc.events.v1.EventBus/Subscribe", req, metadata, timeout: timeout)
|
|
190
|
+
RecvStream.new(stream, EventsPb::Event)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Fiber client: wraps a Grpcdc::FiberChannel (a sync Channel is wrapped).
|
|
196
|
+
class EventBusClientFiber
|
|
197
|
+
def initialize(channel)
|
|
198
|
+
@channel = channel.is_a?(Grpcdc::FiberChannel) ? channel : Grpcdc::FiberChannel.new(channel)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def Publish(req, metadata = nil, timeout: nil)
|
|
202
|
+
@channel.unary("/grpcdc.events.v1.EventBus/Publish", req, EventsPb::PublishReply.new, metadata, timeout: timeout)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def Subscribe(req, metadata = nil, timeout: nil)
|
|
206
|
+
stream = @channel.server_stream("/grpcdc.events.v1.EventBus/Subscribe", req, metadata, timeout: timeout)
|
|
207
|
+
FiberRecvStream.new(stream, EventsPb::Event)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def self.register_event_bus_server(server, impl)
|
|
213
|
+
publish_handler = lambda do |ctx, reqs, sink|
|
|
214
|
+
req = reqs.recv_into(EventsPb::PublishRequest.new)
|
|
215
|
+
resp = impl.Publish(ctx, req)
|
|
216
|
+
sink.send(resp.marshal_dc)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
subscribe_handler = lambda do |ctx, reqs, sink|
|
|
220
|
+
req = reqs.recv_into(EventsPb::SubscribeRequest.new)
|
|
221
|
+
impl.Subscribe(req, SendStream.new(ctx, sink))
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
server.register_service(Grpcdc::ServiceDesc.new(
|
|
225
|
+
"grpcdc.events.v1.EventBus",
|
|
226
|
+
[
|
|
227
|
+
Grpcdc::Method.new("Publish", "/grpcdc.events.v1.EventBus/Publish", Grpcdc::KIND_UNARY, publish_handler),
|
|
228
|
+
Grpcdc::Method.new("Subscribe", "/grpcdc.events.v1.EventBus/Subscribe", Grpcdc::KIND_SERVER_STREAMING, subscribe_handler),
|
|
229
|
+
]
|
|
230
|
+
))
|
|
231
|
+
end
|
|
232
|
+
end
|