service_mesh 0.5.0 → 0.7.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 +4 -4
- data/README.md +23 -3
- data/lib/service_mesh/rspec.rb +47 -23
- data/lib/service_mesh/types.rb +60 -13
- data/lib/service_mesh/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 966f879206d92063d0a9fb94576db740746b6809e629548df3cad531547b543f
|
|
4
|
+
data.tar.gz: 0e411f07db7f14c1fd9e7bbe483a8d44d75c8bc13df6b111eb4ffdd8b0377d7e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9590a21777d4afacd500e6e294042a44ad31581a8688d32119e51d547e4c4917ecb7e5149b834862785971a8942e071f406ab590eb65aa3c81f1c9859f6e7a21
|
|
7
|
+
data.tar.gz: 374757753729d92f5e20930aaef9739676f600a75eb6d9fbdfecf3194590e03c29c9c9f11a0d748105b645b9fdf94438412c276083a13716a321ec732faa166f
|
data/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# service-mesh-ruby
|
|
2
2
|
|
|
3
|
+
This version implements the Service Mesh API Specification v0.4.0.
|
|
4
|
+
|
|
3
5
|
service-mesh-ruby is the Ruby contract for the
|
|
4
6
|
[Service Mesh API Specification](https://github.com/Paymentbox-com/service-mesh-api).
|
|
5
7
|
It is packaged as the gem `service_mesh`. It holds what every transport and every
|
|
@@ -23,9 +25,13 @@ Requires Ruby 3.3 or newer. The gem has no runtime dependencies.
|
|
|
23
25
|
raises `KindMismatch`.
|
|
24
26
|
- `Message#payload` is always `Encoding::BINARY`.
|
|
25
27
|
- `Target#same_channel?` compares segments and kind and ignores metadata.
|
|
26
|
-
- The configuration
|
|
27
|
-
configuration.
|
|
28
|
-
|
|
28
|
+
- The configuration key the specification defines, `DEPLOYMENT_GROUP_KEY`, which is runtime
|
|
29
|
+
configuration.
|
|
30
|
+
- The consumer group is the `consumer_group` member of `Endpoint` and `Subscriber`, defaulting to
|
|
31
|
+
`nil`. `CONSUMER_GROUP_NONE` is the value for no group.
|
|
32
|
+
- The reserved metadata prefix `RESERVED_PREFIX` (`Mesh-`).
|
|
33
|
+
- The metadata keys the specification defines: `HANDLER_ERROR_KEY`, `TIMEOUT_KEY`, `DEADLINE_KEY`,
|
|
34
|
+
`DELIVERY_ATTEMPT_KEY`, and `MESSAGE_ID_KEY`.
|
|
29
35
|
- The errors defined by the specification as `ServiceMesh::Error`: `ServiceMesh::KindMismatch`,
|
|
30
36
|
`ServiceMesh::InvalidTarget`, and `ServiceMesh::NoDeploymentGroup`.
|
|
31
37
|
- A conformance suite a transport runs against its own `Client` and
|
|
@@ -73,6 +79,20 @@ RSpec.describe MyTransport do
|
|
|
73
79
|
end
|
|
74
80
|
```
|
|
75
81
|
|
|
82
|
+
A transport that needs its own settings for the suite to pass supplies any of
|
|
83
|
+
five optional lets, each defaulting to an empty Hash:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
let(:target_metadata) { {...} } # merged under the metadata of every Target the suite builds
|
|
87
|
+
let(:endpoint_metadata) { {...} } # merged under the metadata of every Endpoint the suite builds
|
|
88
|
+
let(:subscriber_metadata) { {...} } # merged under the metadata of every Subscriber the suite builds
|
|
89
|
+
let(:request_options) { {...} } # the options of every request the suite makes
|
|
90
|
+
let(:publish_options) { {...} } # the options of every publish the suite makes
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Metadata an example sets itself wins over `target_metadata`, `endpoint_metadata`, and
|
|
94
|
+
`subscriber_metadata`.
|
|
95
|
+
|
|
76
96
|
The suite covers:
|
|
77
97
|
|
|
78
98
|
- kind checks
|
data/lib/service_mesh/rspec.rb
CHANGED
|
@@ -16,6 +16,13 @@ require_relative "../service_mesh"
|
|
|
16
16
|
# let(:client_config) { {...transport keys...} }
|
|
17
17
|
# let(:route_target) { ServiceMesh::Target.new(segments: %w[test echo], kind: :route) }
|
|
18
18
|
# let(:topic_target) { ServiceMesh::Target.new(segments: %w[test event], kind: :topic) }
|
|
19
|
+
#
|
|
20
|
+
# # Optional, each defaulting to an empty Hash.
|
|
21
|
+
# let(:target_metadata) { {...} } # merged under the metadata of each Target the suite builds
|
|
22
|
+
# let(:endpoint_metadata) { {...} } # merged under each Endpoint's metadata
|
|
23
|
+
# let(:subscriber_metadata) { {...} } # merged under each Subscriber's metadata
|
|
24
|
+
# let(:request_options) { {...} } # options for every request
|
|
25
|
+
# let(:publish_options) { {...} } # options for every publish
|
|
19
26
|
# end
|
|
20
27
|
# end
|
|
21
28
|
#
|
|
@@ -25,13 +32,30 @@ require_relative "../service_mesh"
|
|
|
25
32
|
RSpec.shared_examples "a service mesh transport" do
|
|
26
33
|
let(:service_map) { ServiceMesh::ServiceMap.new }
|
|
27
34
|
let(:wait) { 3 }
|
|
35
|
+
let(:target_metadata) { {} }
|
|
36
|
+
let(:endpoint_metadata) { {} }
|
|
37
|
+
let(:subscriber_metadata) { {} }
|
|
38
|
+
let(:request_options) { {} }
|
|
39
|
+
let(:publish_options) { {} }
|
|
40
|
+
|
|
41
|
+
def target(segments, kind, metadata: {})
|
|
42
|
+
ServiceMesh::Target.new(segments: segments, kind: kind, metadata: target_metadata.merge(metadata))
|
|
43
|
+
end
|
|
28
44
|
|
|
29
45
|
def endpoint(target, handler, metadata: {})
|
|
30
|
-
ServiceMesh::Endpoint.new(target: target, handler: handler, metadata: metadata)
|
|
46
|
+
ServiceMesh::Endpoint.new(target: target, handler: handler, metadata: endpoint_metadata.merge(metadata))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def subscriber(target, handler, consumer_group: nil, metadata: {})
|
|
50
|
+
ServiceMesh::Subscriber.new(target: target, handler: handler, consumer_group: consumer_group, metadata: subscriber_metadata.merge(metadata))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def request(msg, via: client)
|
|
54
|
+
via.request(msg, request_options)
|
|
31
55
|
end
|
|
32
56
|
|
|
33
|
-
def
|
|
34
|
-
|
|
57
|
+
def publish(msg, via: client)
|
|
58
|
+
via.publish(msg, publish_options)
|
|
35
59
|
end
|
|
36
60
|
|
|
37
61
|
def message(target, metadata: {}, payload: "")
|
|
@@ -72,11 +96,11 @@ RSpec.shared_examples "a service mesh transport" do
|
|
|
72
96
|
|
|
73
97
|
describe "kind checks" do
|
|
74
98
|
it "rejects a request to a topic" do
|
|
75
|
-
expect {
|
|
99
|
+
expect { request(message(topic_target)) }.to raise_error(ServiceMesh::KindMismatch)
|
|
76
100
|
end
|
|
77
101
|
|
|
78
102
|
it "rejects a publish to a route" do
|
|
79
|
-
expect {
|
|
103
|
+
expect { publish(message(route_target)) }.to raise_error(ServiceMesh::KindMismatch)
|
|
80
104
|
end
|
|
81
105
|
|
|
82
106
|
it "rejects an endpoint on a topic" do
|
|
@@ -107,7 +131,7 @@ RSpec.shared_examples "a service mesh transport" do
|
|
|
107
131
|
message(topic_target, metadata: {"Reply-Key" => "reply-value"}, payload: m.payload.upcase)
|
|
108
132
|
})])
|
|
109
133
|
|
|
110
|
-
reply =
|
|
134
|
+
reply = request(message(route_target, metadata: {"Request-Key" => "request-value"}, payload: "hello"))
|
|
111
135
|
|
|
112
136
|
expect(seen.target.same_channel?(route_target)).to be(true)
|
|
113
137
|
expect(seen.metadata["Request-Key"]).to eq("request-value")
|
|
@@ -119,7 +143,7 @@ RSpec.shared_examples "a service mesh transport" do
|
|
|
119
143
|
|
|
120
144
|
it "carries an empty payload with no metadata" do
|
|
121
145
|
serve(endpoints: [endpoint(route_target, ->(m) { m })])
|
|
122
|
-
reply =
|
|
146
|
+
reply = request(message(route_target))
|
|
123
147
|
expect([reply.payload, reply.metadata]).to eq(["", {}])
|
|
124
148
|
end
|
|
125
149
|
end
|
|
@@ -129,7 +153,7 @@ RSpec.shared_examples "a service mesh transport" do
|
|
|
129
153
|
got = Queue.new
|
|
130
154
|
serve(subscribers: [subscriber(topic_target, ->(m) { got << m })])
|
|
131
155
|
|
|
132
|
-
|
|
156
|
+
publish(message(topic_target, metadata: {"Event-Id" => "42"}, payload: "created"))
|
|
133
157
|
|
|
134
158
|
m = Timeout.timeout(wait) { got.pop }
|
|
135
159
|
expect(m.target.same_channel?(topic_target)).to be(true)
|
|
@@ -138,22 +162,22 @@ RSpec.shared_examples "a service mesh transport" do
|
|
|
138
162
|
end
|
|
139
163
|
|
|
140
164
|
describe "consumer groups" do
|
|
141
|
-
none =
|
|
142
|
-
shared =
|
|
165
|
+
none = ServiceMesh::CONSUMER_GROUP_NONE
|
|
166
|
+
shared = "order-consumers"
|
|
143
167
|
|
|
144
168
|
[
|
|
145
|
-
["same deployment,
|
|
146
|
-
["different deployments,
|
|
169
|
+
["same deployment, group unset: one instance handles it", %w[billing billing], [nil, nil], 1],
|
|
170
|
+
["different deployments, group unset: each deployment handles it", %w[billing audit], [nil, nil], 2],
|
|
147
171
|
["same deployment, none: every instance handles it", %w[billing billing], [none, none], 2],
|
|
148
172
|
["different deployments, shared named group: one instance handles it", %w[billing audit], [shared, shared], 1]
|
|
149
|
-
].each do |name, groups,
|
|
173
|
+
].each do |name, groups, consumer_groups, want|
|
|
150
174
|
it name do
|
|
151
175
|
count = Queue.new
|
|
152
176
|
2.times do |i|
|
|
153
|
-
serve(config_for(groups[i]), subscribers: [subscriber(topic_target, ->(_) { count << true },
|
|
177
|
+
serve(config_for(groups[i]), subscribers: [subscriber(topic_target, ->(_) { count << true }, consumer_group: consumer_groups[i])])
|
|
154
178
|
end
|
|
155
179
|
|
|
156
|
-
|
|
180
|
+
publish(message(topic_target))
|
|
157
181
|
|
|
158
182
|
wait_until { count.size >= want }
|
|
159
183
|
sleep 0.1 # let an unwanted extra delivery show up
|
|
@@ -164,11 +188,11 @@ RSpec.shared_examples "a service mesh transport" do
|
|
|
164
188
|
it "ignores a group set on the target, which carries none" do
|
|
165
189
|
count = Queue.new
|
|
166
190
|
%w[a b].each do |group|
|
|
167
|
-
t =
|
|
191
|
+
t = target(topic_target.segments, :topic, metadata: {"consumer_group" => group})
|
|
168
192
|
serve(config_for("same"), subscribers: [subscriber(t, ->(_) { count << true })])
|
|
169
193
|
end
|
|
170
194
|
|
|
171
|
-
|
|
195
|
+
publish(message(topic_target))
|
|
172
196
|
|
|
173
197
|
wait_until { count.size >= 1 }
|
|
174
198
|
sleep 0.1 # let an unwanted extra delivery show up
|
|
@@ -181,9 +205,9 @@ RSpec.shared_examples "a service mesh transport" do
|
|
|
181
205
|
serve(endpoints: [endpoint(route_target, ->(m) { m })])
|
|
182
206
|
own = new_client.call(client_config, service_map)
|
|
183
207
|
|
|
184
|
-
expect(
|
|
208
|
+
expect(request(message(route_target), via: own).payload).to eq("")
|
|
185
209
|
own.close
|
|
186
|
-
expect {
|
|
210
|
+
expect { request(message(route_target), via: own) }.to raise_error(StandardError)
|
|
187
211
|
end
|
|
188
212
|
end
|
|
189
213
|
|
|
@@ -197,9 +221,9 @@ RSpec.shared_examples "a service mesh transport" do
|
|
|
197
221
|
expect(rt.client).to equal(given)
|
|
198
222
|
expect(rt.service_map).to equal(service_map)
|
|
199
223
|
rt.start
|
|
200
|
-
expect(
|
|
224
|
+
expect(request(req, via: given).payload).to eq("")
|
|
201
225
|
rt.stop(1)
|
|
202
|
-
expect {
|
|
226
|
+
expect { request(req, via: given) }.to raise_error(StandardError)
|
|
203
227
|
end
|
|
204
228
|
end
|
|
205
229
|
|
|
@@ -234,7 +258,7 @@ RSpec.shared_examples "a service mesh transport" do
|
|
|
234
258
|
message(route_target, payload: "done")
|
|
235
259
|
})])
|
|
236
260
|
|
|
237
|
-
reply = Thread.new {
|
|
261
|
+
reply = Thread.new { request(message(route_target)) }
|
|
238
262
|
@conformance_threads << reply
|
|
239
263
|
Timeout.timeout(wait) { entered.pop }
|
|
240
264
|
|
|
@@ -256,7 +280,7 @@ RSpec.shared_examples "a service mesh transport" do
|
|
|
256
280
|
message(route_target)
|
|
257
281
|
})])
|
|
258
282
|
|
|
259
|
-
@conformance_threads << Thread.new {
|
|
283
|
+
@conformance_threads << Thread.new { request(message(route_target)) rescue nil } # rubocop:disable Style/RescueModifier
|
|
260
284
|
Timeout.timeout(wait) { entered.pop }
|
|
261
285
|
|
|
262
286
|
expect(rt.stop(0.1)).to be(false)
|
data/lib/service_mesh/types.rb
CHANGED
|
@@ -3,17 +3,52 @@
|
|
|
3
3
|
module ServiceMesh
|
|
4
4
|
KINDS = %i[route topic].freeze
|
|
5
5
|
|
|
6
|
-
#
|
|
7
|
-
# a transport. DEPLOYMENT_GROUP_KEY is runtime configuration, and nothing
|
|
8
|
-
# else carries it.
|
|
9
|
-
# when it is unset or empty, the runtime's deployment group applies, and
|
|
10
|
-
# CONSUMER_GROUP_NONE means no group. A Target carries neither.
|
|
6
|
+
# The configuration key the specification defines. Everything else belongs
|
|
7
|
+
# to a transport. DEPLOYMENT_GROUP_KEY is runtime configuration, and nothing
|
|
8
|
+
# else carries it.
|
|
11
9
|
DEPLOYMENT_GROUP_KEY = "deployment_group"
|
|
12
|
-
|
|
10
|
+
|
|
11
|
+
# The Endpoint and Subscriber consumer_group value that requests no group.
|
|
13
12
|
CONSUMER_GROUP_NONE = "none"
|
|
14
13
|
|
|
14
|
+
# Metadata keys that start with RESERVED_PREFIX belong to transports and
|
|
15
|
+
# protocol layers, and application code does not set them. A key a
|
|
16
|
+
# transport defines for itself continues with the transport's name, such as
|
|
17
|
+
# "Mesh-Nats-". A transport may use any of the keys below, and one that does
|
|
18
|
+
# follows the meaning and format given here.
|
|
19
|
+
RESERVED_PREFIX = "Mesh-"
|
|
20
|
+
|
|
21
|
+
# Set by the serving transport on a reply when the Endpoint's handler
|
|
22
|
+
# failed. The value is the failure's text.
|
|
23
|
+
HANDLER_ERROR_KEY = "Mesh-Handler-Error"
|
|
24
|
+
|
|
25
|
+
# Set by the requesting transport on a request. The value is how long the
|
|
26
|
+
# caller waits for the reply, as a whole number of milliseconds.
|
|
27
|
+
TIMEOUT_KEY = "Mesh-Timeout"
|
|
28
|
+
|
|
29
|
+
# Set by the receiving transport on a message it hands to a handler. The
|
|
30
|
+
# value is when the handler's result stops mattering, in RFC 3339 with
|
|
31
|
+
# fractional seconds.
|
|
32
|
+
DEADLINE_KEY = "Mesh-Deadline"
|
|
33
|
+
|
|
34
|
+
# Set by the receiving transport on a message it hands to a handler. The
|
|
35
|
+
# value is which delivery of the message this is, starting at 1. It is
|
|
36
|
+
# absent when the transport does not track deliveries.
|
|
37
|
+
DELIVERY_ATTEMPT_KEY = "Mesh-Delivery-Attempt"
|
|
38
|
+
|
|
39
|
+
# Set by the publisher or caller. The value is an ID that stays the same
|
|
40
|
+
# when the same message is sent again, so handlers can recognize repeats.
|
|
41
|
+
MESSAGE_ID_KEY = "Mesh-Message-Id"
|
|
42
|
+
|
|
15
43
|
# Identifies a receiving channel on the mesh. A transport assembles the
|
|
16
44
|
# segments into its own address; that string never leaves the transport.
|
|
45
|
+
# Metadata holds addressing and the default transport settings for the
|
|
46
|
+
# target, and never a deployment group or consumer group.
|
|
47
|
+
#
|
|
48
|
+
# A transport reads each of its settings from the first of these that has a
|
|
49
|
+
# value: the per-call options of request or publish, the Endpoint's or
|
|
50
|
+
# Subscriber's metadata, the Target's metadata, and the transport's own
|
|
51
|
+
# default.
|
|
17
52
|
Target = Data.define(:segments, :kind, :metadata) do
|
|
18
53
|
def initialize(segments:, kind:, metadata: {})
|
|
19
54
|
raise KindMismatch, "kind must be one of #{KINDS.inspect}, got #{kind.inspect}" unless KINDS.include?(kind)
|
|
@@ -34,7 +69,9 @@ module ServiceMesh
|
|
|
34
69
|
end
|
|
35
70
|
end
|
|
36
71
|
|
|
37
|
-
# What travels between services. Payload is always BINARY encoded.
|
|
72
|
+
# What travels between services. Payload is always BINARY encoded. Metadata
|
|
73
|
+
# keys that start with RESERVED_PREFIX belong to transports and protocol
|
|
74
|
+
# layers.
|
|
38
75
|
Message = Data.define(:target, :metadata, :payload) do
|
|
39
76
|
def initialize(target:, metadata: {}, payload: "")
|
|
40
77
|
super(target: target, metadata: metadata.to_h.freeze, payload: payload.to_s.b.freeze)
|
|
@@ -42,16 +79,26 @@ module ServiceMesh
|
|
|
42
79
|
end
|
|
43
80
|
|
|
44
81
|
# A route target paired with a handler that returns a Message.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
82
|
+
#
|
|
83
|
+
# consumer_group names the logical group the endpoint joins. When it is nil
|
|
84
|
+
# or empty, the runtime's deployment group applies. CONSUMER_GROUP_NONE means
|
|
85
|
+
# no group, so every instance handles every message. Any other value names
|
|
86
|
+
# the group, and one handler in that group handles each message.
|
|
87
|
+
#
|
|
88
|
+
# Metadata holds transport settings for the endpoint, which override the
|
|
89
|
+
# target's.
|
|
90
|
+
Endpoint = Data.define(:target, :consumer_group, :metadata, :handler) do
|
|
91
|
+
def initialize(target:, handler:, consumer_group: nil, metadata: {})
|
|
92
|
+
super(target: target, consumer_group: consumer_group, metadata: metadata.to_h.freeze, handler: handler)
|
|
48
93
|
end
|
|
49
94
|
end
|
|
50
95
|
|
|
51
96
|
# A topic target paired with a handler whose return value is ignored.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
97
|
+
# consumer_group has the same meaning as on Endpoint. Metadata holds
|
|
98
|
+
# transport settings for the subscriber, which override the target's.
|
|
99
|
+
Subscriber = Data.define(:target, :consumer_group, :metadata, :handler) do
|
|
100
|
+
def initialize(target:, handler:, consumer_group: nil, metadata: {})
|
|
101
|
+
super(target: target, consumer_group: consumer_group, metadata: metadata.to_h.freeze, handler: handler)
|
|
55
102
|
end
|
|
56
103
|
end
|
|
57
104
|
end
|
data/lib/service_mesh/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: service_mesh
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bryant Morrill
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2026-
|
|
12
|
+
date: 2026-10-08 00:00:00.000000000 Z
|
|
13
13
|
dependencies: []
|
|
14
14
|
description:
|
|
15
15
|
email:
|