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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: edccbfa1e181934bed8df91ecb53d15849335cb10730450e088fee1e28ac44ef
4
- data.tar.gz: 64cbe1ba2b464e7ccd1dc840066eedf2dd0cd4639804af86e0fde65615cf064c
3
+ metadata.gz: 966f879206d92063d0a9fb94576db740746b6809e629548df3cad531547b543f
4
+ data.tar.gz: 0e411f07db7f14c1fd9e7bbe483a8d44d75c8bc13df6b111eb4ffdd8b0377d7e
5
5
  SHA512:
6
- metadata.gz: bdff9e8e86405e3fcca5df769e800d317df47307d4974c88f9161728c165242ecfa7acf72880bfb41b2606f310c30a6fd6cf35dbb56d22dab202d27e1201725f
7
- data.tar.gz: c3513ecbc6ae3832509419f8f3b9e9c93c637199eb6d8fd9b9183ef4046bf75d53dd3cc1cc73ee6d16dcaa2b116626956068ce3e3569404aba69bb90e0bc3e7a
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 keys the specification defines. `DEPLOYMENT_GROUP_KEY` is runtime
27
- configuration. `CONSUMER_GROUP_KEY`, with the value `CONSUMER_GROUP_NONE`, is `Endpoint` and
28
- `Subscriber` metadata.
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
@@ -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 subscriber(target, handler, metadata: {})
34
- ServiceMesh::Subscriber.new(target: target, handler: handler, metadata: metadata)
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 { client.request(message(topic_target)) }.to raise_error(ServiceMesh::KindMismatch)
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 { client.publish(message(route_target)) }.to raise_error(ServiceMesh::KindMismatch)
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 = client.request(message(route_target, metadata: {"Request-Key" => "request-value"}, payload: "hello"))
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 = client.request(message(route_target))
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
- client.publish(message(topic_target, metadata: {"Event-Id" => "42"}, payload: "created"))
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 = {ServiceMesh::CONSUMER_GROUP_KEY => ServiceMesh::CONSUMER_GROUP_NONE}
142
- shared = {ServiceMesh::CONSUMER_GROUP_KEY => "order-consumers"}
165
+ none = ServiceMesh::CONSUMER_GROUP_NONE
166
+ shared = "order-consumers"
143
167
 
144
168
  [
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],
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, metadata, want|
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 }, metadata: metadata[i])])
177
+ serve(config_for(groups[i]), subscribers: [subscriber(topic_target, ->(_) { count << true }, consumer_group: consumer_groups[i])])
154
178
  end
155
179
 
156
- client.publish(message(topic_target))
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 = ServiceMesh::Target.new(segments: topic_target.segments, kind: :topic, metadata: {ServiceMesh::CONSUMER_GROUP_KEY => group})
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
- client.publish(message(topic_target))
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(own.request(message(route_target)).payload).to eq("")
208
+ expect(request(message(route_target), via: own).payload).to eq("")
185
209
  own.close
186
- expect { own.request(message(route_target)) }.to raise_error(StandardError)
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(given.request(req).payload).to eq("")
224
+ expect(request(req, via: given).payload).to eq("")
201
225
  rt.stop(1)
202
- expect { given.request(req) }.to raise_error(StandardError)
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 { client.request(message(route_target)) }
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 { client.request(message(route_target)) rescue nil } # rubocop:disable Style/RescueModifier
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)
@@ -3,17 +3,52 @@
3
3
  module ServiceMesh
4
4
  KINDS = %i[route topic].freeze
5
5
 
6
- # Configuration keys the specification defines. Everything else belongs to
7
- # a transport. DEPLOYMENT_GROUP_KEY is runtime configuration, and nothing
8
- # else carries it. CONSUMER_GROUP_KEY is Endpoint and Subscriber metadata;
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
- CONSUMER_GROUP_KEY = "consumer_group"
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
- Endpoint = Data.define(:target, :metadata, :handler) do
46
- def initialize(target:, handler:, metadata: {})
47
- super(target: target, metadata: metadata.to_h.freeze, handler: handler)
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
- Subscriber = Data.define(:target, :metadata, :handler) do
53
- def initialize(target:, handler:, metadata: {})
54
- super(target: target, metadata: metadata.to_h.freeze, handler: handler)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ServiceMesh
4
- VERSION = "0.5.0"
4
+ VERSION = "0.7.0"
5
5
  end
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.5.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-09-30 00:00:00.000000000 Z
12
+ date: 2026-10-08 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description:
15
15
  email: