service_mesh 0.4.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 +21 -0
- data/README.md +106 -0
- data/lib/service_mesh/errors.rb +15 -0
- data/lib/service_mesh/rspec.rb +262 -0
- data/lib/service_mesh/types.rb +54 -0
- data/lib/service_mesh/version.rb +5 -0
- data/lib/service_mesh.rb +11 -0
- metadata +52 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: fef05ef9dacd492215d76304b8f1938056e862e35c0e0450f7b120213bf7e8d7
|
|
4
|
+
data.tar.gz: 97cb3e42dc4797d618d3eacf61aa3d2ba994887aa94ee88d37d90b6b04211fb6
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c7da5185432f80a50b73ec3942c736367455bc337918b7afba2b8393a1575353e54a9fbdb54d98aeaf03c7b0922766490a8da075c8daf4d2a58087044e1e448b
|
|
7
|
+
data.tar.gz: 4418d34275bc4dd1d532b4b84f48361967601275181c64c50c5cb7ca7177ddecd9ebdedbb773d813d07e55307efeb24b63c2753a8c6881ea66803e1e619f3127
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Paymentbox
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# service-mesh-ruby
|
|
2
|
+
|
|
3
|
+
The Ruby contract for the
|
|
4
|
+
[Service Mesh API Specification](https://github.com/Paymentbox-com/service-mesh-api),
|
|
5
|
+
packaged as the gem `service_mesh`. It holds what every transport and every
|
|
6
|
+
caller must agree on, and nothing that moves bytes. Transports are separate
|
|
7
|
+
gems that depend on it and implement `Client` and `Runtime`.
|
|
8
|
+
|
|
9
|
+
The [gRPC Service Mesh API](https://github.com/Paymentbox-com/grpc-service-mesh-api) is a protocol layer that generates code against this contract from protobuf
|
|
10
|
+
definitions, through its Ruby library [grpc-service-mesh-ruby](https://github.com/Paymentbox-com/grpc-service-mesh-ruby). Other protocol layers may be implemented
|
|
11
|
+
to do the same.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
# Gemfile
|
|
17
|
+
gem "service_mesh"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Requires Ruby 3.3 or newer. The gem has no runtime dependencies.
|
|
21
|
+
|
|
22
|
+
## What it Implements
|
|
23
|
+
|
|
24
|
+
- The value types from the specification: `ServiceMesh::Target`, `ServiceMesh::ServiceMap`,
|
|
25
|
+
`ServiceMesh::Message`, `ServiceMesh::Endpoint` and `ServiceMesh::Subscriber`.
|
|
26
|
+
- Target Kinds are implemented as `:route` and `:topic`. A `Target` built with a kind outside the two
|
|
27
|
+
raises `KindMismatch`.
|
|
28
|
+
- `Message#payload` is always `Encoding::BINARY`.
|
|
29
|
+
- `Target#same_channel?` compares segments and kind and ignores metadata.
|
|
30
|
+
- The configuration keys the specification defines: `DEPLOYMENT_GROUP_KEY`,
|
|
31
|
+
`CONSUMER_GROUP_KEY`, and the value `CONSUMER_GROUP_NONE`.
|
|
32
|
+
- The errors defined by the specification as `ServiceMesh::Error`: `ServiceMesh::KindMismatch`,
|
|
33
|
+
`ServiceMesh::InvalidTarget`, `ServiceMesh::NoDeploymentGroup`.
|
|
34
|
+
- A conformance suite a transport runs against its own `Client` and
|
|
35
|
+
`Runtime`.
|
|
36
|
+
|
|
37
|
+
`Client` and `Runtime` are duck types. The specification names their methods;
|
|
38
|
+
a transport satisfies the contract by responding to them and by passing the
|
|
39
|
+
conformance suite.
|
|
40
|
+
|
|
41
|
+
## Transports
|
|
42
|
+
|
|
43
|
+
- NATS: [service-mesh-nats-ruby](https://github.com/Paymentbox-com/service-mesh-nats-ruby),
|
|
44
|
+
gem `service_mesh_nats`.
|
|
45
|
+
|
|
46
|
+
The Go counterpart of this gem is
|
|
47
|
+
[service-mesh-go](https://github.com/Paymentbox-com/service-mesh-go).
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
A transport that implements Client and Runtime according to the specification will used the
|
|
52
|
+
types defined here.
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
require "service_mesh"
|
|
56
|
+
|
|
57
|
+
TARGET = ServiceMesh::Target.new(segments: %w[accounts lookup], kind: :route)
|
|
58
|
+
|
|
59
|
+
def lookup(client, id)
|
|
60
|
+
reply = client.request(ServiceMesh::Message.new(target: TARGET, payload: id))
|
|
61
|
+
reply.payload
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Conformance
|
|
66
|
+
|
|
67
|
+
A transport implemented to use these types should include the shared examples defined in this
|
|
68
|
+
library in its own spec suite and supply its own constructors and a pair of valid targets:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
require "service_mesh/rspec"
|
|
72
|
+
|
|
73
|
+
RSpec.describe MyTransport do
|
|
74
|
+
it_behaves_like "a service mesh transport" do
|
|
75
|
+
let(:new_runtime) do
|
|
76
|
+
->(client, config, endpoints:, subscribers:) { MyTransport::Runtime.new(client, config, endpoints:, subscribers:) }
|
|
77
|
+
end
|
|
78
|
+
let(:new_client) { ->(config, map) { MyTransport::Client.new(config, map) } }
|
|
79
|
+
let(:runtime_config) { {"deployment_group" => "test", "url" => url} }
|
|
80
|
+
let(:client_config) { {"url" => url} }
|
|
81
|
+
let(:route_target) { ServiceMesh::Target.new(segments: %w[test echo], kind: :route) }
|
|
82
|
+
let(:topic_target) { ServiceMesh::Target.new(segments: %w[test event], kind: :topic) }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The suite covers kind checks, the deployment group requirement, request and
|
|
88
|
+
reply with metadata both ways, publish, the consumer-group delivery
|
|
89
|
+
permutations across two runtimes, a standalone client refusing requests after
|
|
90
|
+
close, the runtime's client being the one it was given and closed by stop, the lifecycle state machine, and drain completing and expiring.
|
|
91
|
+
Anything that names a transport's own errors, config keys, or address syntax
|
|
92
|
+
stays in the transport's specs.
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
mise install
|
|
98
|
+
just install
|
|
99
|
+
just check # lint, test, build
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Tests
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
just test
|
|
106
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ServiceMesh
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
# Raised for misuse of the contract. Transport errors pass through as the
|
|
7
|
+
# transport library's own exceptions.
|
|
8
|
+
class KindMismatch < Error; end
|
|
9
|
+
|
|
10
|
+
class InvalidTarget < Error; end
|
|
11
|
+
|
|
12
|
+
class NoDeploymentGroup < Error
|
|
13
|
+
def initialize(msg = "config #{DEPLOYMENT_GROUP_KEY} is required") = super
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rspec"
|
|
4
|
+
require "timeout"
|
|
5
|
+
require_relative "../service_mesh"
|
|
6
|
+
|
|
7
|
+
# Conformance suite. A transport includes it from its own specs:
|
|
8
|
+
#
|
|
9
|
+
# require "service_mesh/rspec"
|
|
10
|
+
#
|
|
11
|
+
# RSpec.describe MyTransport do
|
|
12
|
+
# it_behaves_like "a service mesh transport" do
|
|
13
|
+
# let(:new_runtime) { ->(client, config, endpoints:, subscribers:) { MyTransport::Runtime.new(client, config, endpoints:, subscribers:) } }
|
|
14
|
+
# let(:new_client) { ->(config, map) { MyTransport::Client.new(config, map) } }
|
|
15
|
+
# let(:runtime_config) { {"deployment_group" => "test", ...transport keys...} }
|
|
16
|
+
# let(:client_config) { {...transport keys...} }
|
|
17
|
+
# let(:route_target) { ServiceMesh::Target.new(segments: %w[test echo], kind: :route) }
|
|
18
|
+
# let(:topic_target) { ServiceMesh::Target.new(segments: %w[test event], kind: :topic) }
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# Every example here follows from the specification alone. Anything that
|
|
23
|
+
# names a transport's own errors, config keys, or address syntax belongs in
|
|
24
|
+
# the transport's specs.
|
|
25
|
+
RSpec.shared_examples "a service mesh transport" do
|
|
26
|
+
let(:service_map) { ServiceMesh::ServiceMap.new }
|
|
27
|
+
let(:wait) { 3 }
|
|
28
|
+
|
|
29
|
+
def endpoint(target, handler, metadata: {})
|
|
30
|
+
ServiceMesh::Endpoint.new(target: target, handler: handler, metadata: metadata)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def subscriber(target, handler, metadata: {})
|
|
34
|
+
ServiceMesh::Subscriber.new(target: target, handler: handler, metadata: metadata)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def message(target, metadata: {}, payload: "")
|
|
38
|
+
ServiceMesh::Message.new(target: target, metadata: metadata, payload: payload)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def config_for(group)
|
|
42
|
+
runtime_config.merge(ServiceMesh::DEPLOYMENT_GROUP_KEY => group)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def build(config = runtime_config, endpoints: [], subscribers: [])
|
|
46
|
+
new_runtime.call(new_client.call(client_config, service_map), config, endpoints: endpoints, subscribers: subscribers)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def serve(config = runtime_config, endpoints: [], subscribers: [])
|
|
50
|
+
rt = build(config, endpoints: endpoints, subscribers: subscribers)
|
|
51
|
+
rt.start
|
|
52
|
+
@conformance_runtimes << rt
|
|
53
|
+
rt
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def wait_until
|
|
57
|
+
Timeout.timeout(wait) { sleep 0.01 until yield }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
before do
|
|
61
|
+
@conformance_runtimes = []
|
|
62
|
+
@conformance_threads = []
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
after do
|
|
66
|
+
@conformance_threads.each(&:kill)
|
|
67
|
+
@conformance_runtimes.each { |rt| rt.stop(wait) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
let(:client) { new_client.call(client_config, service_map) }
|
|
71
|
+
after { client.close }
|
|
72
|
+
|
|
73
|
+
describe "kind checks" do
|
|
74
|
+
it "rejects a request to a topic" do
|
|
75
|
+
expect { client.request(message(topic_target)) }.to raise_error(ServiceMesh::KindMismatch)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "rejects a publish to a route" do
|
|
79
|
+
expect { client.publish(message(route_target)) }.to raise_error(ServiceMesh::KindMismatch)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "rejects an endpoint on a topic" do
|
|
83
|
+
expect { build(endpoints: [endpoint(topic_target, ->(m) { m })]) }.to raise_error(ServiceMesh::KindMismatch)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "rejects a subscriber on a route" do
|
|
87
|
+
expect { build(subscribers: [subscriber(route_target, ->(m) { m })]) }.to raise_error(ServiceMesh::KindMismatch)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
describe "configuration" do
|
|
92
|
+
it "requires a deployment group" do
|
|
93
|
+
without = runtime_config.reject { |k, _| k == ServiceMesh::DEPLOYMENT_GROUP_KEY }
|
|
94
|
+
expect { build(without) }.to raise_error(ServiceMesh::NoDeploymentGroup)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it "rejects an empty deployment group" do
|
|
98
|
+
expect { build(config_for("")) }.to raise_error(ServiceMesh::NoDeploymentGroup)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
describe "request and reply" do
|
|
103
|
+
it "round-trips payload and metadata both ways and ignores the reply's target" do
|
|
104
|
+
seen = nil
|
|
105
|
+
serve(endpoints: [endpoint(route_target, lambda { |m|
|
|
106
|
+
seen = m
|
|
107
|
+
message(topic_target, metadata: {"Reply-Key" => "reply-value"}, payload: m.payload.upcase)
|
|
108
|
+
})])
|
|
109
|
+
|
|
110
|
+
reply = client.request(message(route_target, metadata: {"Request-Key" => "request-value"}, payload: "hello"))
|
|
111
|
+
|
|
112
|
+
expect(seen.target.same_channel?(route_target)).to be(true)
|
|
113
|
+
expect(seen.metadata["Request-Key"]).to eq("request-value")
|
|
114
|
+
expect(reply.payload).to eq("HELLO")
|
|
115
|
+
expect(reply.payload.encoding).to eq(Encoding::BINARY)
|
|
116
|
+
expect(reply.metadata["Reply-Key"]).to eq("reply-value")
|
|
117
|
+
expect(reply.target.same_channel?(route_target)).to be(true)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it "carries an empty payload with no metadata" do
|
|
121
|
+
serve(endpoints: [endpoint(route_target, ->(m) { m })])
|
|
122
|
+
reply = client.request(message(route_target))
|
|
123
|
+
expect([reply.payload, reply.metadata]).to eq(["", {}])
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
describe "publish" do
|
|
128
|
+
it "reaches a subscriber with payload and metadata" do
|
|
129
|
+
got = Queue.new
|
|
130
|
+
serve(subscribers: [subscriber(topic_target, ->(m) { got << m })])
|
|
131
|
+
|
|
132
|
+
client.publish(message(topic_target, metadata: {"Event-Id" => "42"}, payload: "created"))
|
|
133
|
+
|
|
134
|
+
m = Timeout.timeout(wait) { got.pop }
|
|
135
|
+
expect(m.target.same_channel?(topic_target)).to be(true)
|
|
136
|
+
expect([m.payload, m.metadata["Event-Id"]]).to eq(["created", "42"])
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
describe "consumer groups" do
|
|
141
|
+
none = {ServiceMesh::CONSUMER_GROUP_KEY => ServiceMesh::CONSUMER_GROUP_NONE}
|
|
142
|
+
shared = {ServiceMesh::CONSUMER_GROUP_KEY => "order-consumers"}
|
|
143
|
+
|
|
144
|
+
[
|
|
145
|
+
["same deployment, key absent: one instance handles it", %w[billing billing], [{}, {}], 1],
|
|
146
|
+
["different deployments, key absent: each deployment handles it", %w[billing audit], [{}, {}], 2],
|
|
147
|
+
["same deployment, none: every instance handles it", %w[billing billing], [none, none], 2],
|
|
148
|
+
["different deployments, shared named group: one instance handles it", %w[billing audit], [shared, shared], 1]
|
|
149
|
+
].each do |name, groups, metadata, want|
|
|
150
|
+
it name do
|
|
151
|
+
count = Queue.new
|
|
152
|
+
2.times do |i|
|
|
153
|
+
serve(config_for(groups[i]), subscribers: [subscriber(topic_target, ->(_) { count << true }, metadata: metadata[i])])
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
client.publish(message(topic_target))
|
|
157
|
+
|
|
158
|
+
wait_until { count.size >= want }
|
|
159
|
+
sleep 0.1 # let an unwanted extra delivery show up
|
|
160
|
+
expect(count.size).to eq(want)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it "honours a group set on the target rather than the binding" do
|
|
165
|
+
count = Queue.new
|
|
166
|
+
%w[a b].each do |group|
|
|
167
|
+
t = ServiceMesh::Target.new(segments: topic_target.segments, kind: :topic, metadata: {ServiceMesh::CONSUMER_GROUP_KEY => group})
|
|
168
|
+
serve(config_for("same"), subscribers: [subscriber(t, ->(_) { count << true })])
|
|
169
|
+
end
|
|
170
|
+
client.publish(message(topic_target))
|
|
171
|
+
wait_until { count.size == 2 }
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
describe "a standalone client" do
|
|
176
|
+
it "accepts no requests after close" do
|
|
177
|
+
serve(endpoints: [endpoint(route_target, ->(m) { m })])
|
|
178
|
+
own = new_client.call(client_config, service_map)
|
|
179
|
+
|
|
180
|
+
expect(own.request(message(route_target)).payload).to eq("")
|
|
181
|
+
own.close
|
|
182
|
+
expect { own.request(message(route_target)) }.to raise_error(StandardError)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
describe "the runtime's client" do
|
|
187
|
+
it "is the client the runtime was given, and stop closes it" do
|
|
188
|
+
given = new_client.call(client_config, service_map)
|
|
189
|
+
rt = new_runtime.call(given, runtime_config, endpoints: [endpoint(route_target, ->(m) { m })], subscribers: [])
|
|
190
|
+
@conformance_runtimes << rt
|
|
191
|
+
req = message(route_target)
|
|
192
|
+
|
|
193
|
+
expect(rt.client).to equal(given)
|
|
194
|
+
expect(rt.service_map).to equal(service_map)
|
|
195
|
+
rt.start
|
|
196
|
+
expect(given.request(req).payload).to eq("")
|
|
197
|
+
rt.stop(1)
|
|
198
|
+
expect { given.request(req) }.to raise_error(StandardError)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
describe "lifecycle" do
|
|
203
|
+
it "moves created -> running -> stopped and does not restart" do
|
|
204
|
+
rt = build
|
|
205
|
+
@conformance_runtimes << rt
|
|
206
|
+
|
|
207
|
+
expect(rt.running?).to be(false)
|
|
208
|
+
expect(rt.stop(0)).to be(true)
|
|
209
|
+
rt.start
|
|
210
|
+
expect(rt.running?).to be(true)
|
|
211
|
+
expect { rt.start }.to raise_error(StandardError)
|
|
212
|
+
expect(rt.stop(1)).to be(true)
|
|
213
|
+
expect(rt.running?).to be(false)
|
|
214
|
+
expect(rt.stop(0)).to be(true)
|
|
215
|
+
expect { rt.start }.to raise_error(StandardError)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
it "rejects a negative drain" do
|
|
219
|
+
expect { build.stop(-1) }.to raise_error(ArgumentError)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
describe "stop" do
|
|
224
|
+
it "waits for an in-flight handler and the requester still gets the reply" do
|
|
225
|
+
entered = Queue.new
|
|
226
|
+
release = Queue.new
|
|
227
|
+
rt = serve(endpoints: [endpoint(route_target, lambda { |_|
|
|
228
|
+
entered << true
|
|
229
|
+
release.pop
|
|
230
|
+
message(route_target, payload: "done")
|
|
231
|
+
})])
|
|
232
|
+
|
|
233
|
+
reply = Thread.new { client.request(message(route_target)) }
|
|
234
|
+
@conformance_threads << reply
|
|
235
|
+
Timeout.timeout(wait) { entered.pop }
|
|
236
|
+
|
|
237
|
+
stopped = Thread.new { rt.stop(wait) }
|
|
238
|
+
sleep 0.2
|
|
239
|
+
expect(stopped.alive?).to be(true)
|
|
240
|
+
|
|
241
|
+
release << true
|
|
242
|
+
expect(stopped.value).to be(true)
|
|
243
|
+
expect(reply.value.payload).to eq("done")
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
it "returns false when the drain expires with a handler still running" do
|
|
247
|
+
entered = Queue.new
|
|
248
|
+
release = Queue.new
|
|
249
|
+
rt = serve(endpoints: [endpoint(route_target, lambda { |_|
|
|
250
|
+
entered << true
|
|
251
|
+
release.pop
|
|
252
|
+
message(route_target)
|
|
253
|
+
})])
|
|
254
|
+
|
|
255
|
+
@conformance_threads << Thread.new { client.request(message(route_target)) rescue nil } # rubocop:disable Style/RescueModifier
|
|
256
|
+
Timeout.timeout(wait) { entered.pop }
|
|
257
|
+
|
|
258
|
+
expect(rt.stop(0.1)).to be(false)
|
|
259
|
+
release << true
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ServiceMesh
|
|
4
|
+
KINDS = %i[route topic].freeze
|
|
5
|
+
|
|
6
|
+
# Configuration keys the specification defines. Everything else belongs to
|
|
7
|
+
# a transport.
|
|
8
|
+
DEPLOYMENT_GROUP_KEY = "deployment_group"
|
|
9
|
+
CONSUMER_GROUP_KEY = "consumer_group"
|
|
10
|
+
CONSUMER_GROUP_NONE = "none"
|
|
11
|
+
|
|
12
|
+
# Identifies a receiving channel on the mesh. A transport assembles the
|
|
13
|
+
# segments into its own address; that string never leaves the transport.
|
|
14
|
+
Target = Data.define(:segments, :kind, :metadata) do
|
|
15
|
+
def initialize(segments:, kind:, metadata: {})
|
|
16
|
+
raise KindMismatch, "kind must be one of #{KINDS.inspect}, got #{kind.inspect}" unless KINDS.include?(kind)
|
|
17
|
+
|
|
18
|
+
super(segments: Array(segments).map(&:to_s).freeze, kind: kind, metadata: metadata.to_h.freeze)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Same channel: same segments and kind. Metadata is configuration.
|
|
22
|
+
def same_channel?(other)
|
|
23
|
+
segments == other.segments && kind == other.kind
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Every Target reachable over one transport.
|
|
28
|
+
ServiceMap = Data.define(:targets) do
|
|
29
|
+
def initialize(targets: [])
|
|
30
|
+
super(targets: Array(targets).freeze)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# What travels between services. Payload is always BINARY encoded.
|
|
35
|
+
Message = Data.define(:target, :metadata, :payload) do
|
|
36
|
+
def initialize(target:, metadata: {}, payload: "")
|
|
37
|
+
super(target: target, metadata: metadata.to_h.freeze, payload: payload.to_s.b.freeze)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# A route target paired with a handler that returns a Message.
|
|
42
|
+
Endpoint = Data.define(:target, :metadata, :handler) do
|
|
43
|
+
def initialize(target:, handler:, metadata: {})
|
|
44
|
+
super(target: target, metadata: metadata.to_h.freeze, handler: handler)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# A topic target paired with a handler whose return value is ignored.
|
|
49
|
+
Subscriber = Data.define(:target, :metadata, :handler) do
|
|
50
|
+
def initialize(target:, handler:, metadata: {})
|
|
51
|
+
super(target: target, metadata: metadata.to_h.freeze, handler: handler)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
data/lib/service_mesh.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "service_mesh/version"
|
|
4
|
+
require_relative "service_mesh/types"
|
|
5
|
+
require_relative "service_mesh/errors"
|
|
6
|
+
|
|
7
|
+
# Ruby contract for the Service Mesh API Specification. Transports implement
|
|
8
|
+
# Client and Runtime against these types; see service_mesh/rspec for the
|
|
9
|
+
# conformance suite a transport runs.
|
|
10
|
+
module ServiceMesh
|
|
11
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: service_mesh
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.4.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Paymentbox
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-09-25 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description:
|
|
14
|
+
email:
|
|
15
|
+
executables: []
|
|
16
|
+
extensions: []
|
|
17
|
+
extra_rdoc_files: []
|
|
18
|
+
files:
|
|
19
|
+
- LICENSE
|
|
20
|
+
- README.md
|
|
21
|
+
- lib/service_mesh.rb
|
|
22
|
+
- lib/service_mesh/errors.rb
|
|
23
|
+
- lib/service_mesh/rspec.rb
|
|
24
|
+
- lib/service_mesh/types.rb
|
|
25
|
+
- lib/service_mesh/version.rb
|
|
26
|
+
homepage: https://github.com/Paymentbox-com/service-mesh-ruby
|
|
27
|
+
licenses:
|
|
28
|
+
- MIT
|
|
29
|
+
metadata:
|
|
30
|
+
homepage_uri: https://github.com/Paymentbox-com/service-mesh-ruby
|
|
31
|
+
source_code_uri: https://github.com/Paymentbox-com/service-mesh-ruby
|
|
32
|
+
rubygems_mfa_required: 'true'
|
|
33
|
+
post_install_message:
|
|
34
|
+
rdoc_options: []
|
|
35
|
+
require_paths:
|
|
36
|
+
- lib
|
|
37
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
38
|
+
requirements:
|
|
39
|
+
- - ">="
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '3.3'
|
|
42
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
requirements: []
|
|
48
|
+
rubygems_version: 3.5.22
|
|
49
|
+
signing_key:
|
|
50
|
+
specification_version: 4
|
|
51
|
+
summary: Ruby contract for the Service Mesh API Specification
|
|
52
|
+
test_files: []
|