service_mesh_nats 0.5.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 +63 -0
- data/lib/service_mesh_nats/client.rb +81 -0
- data/lib/service_mesh_nats/errors.rb +36 -0
- data/lib/service_mesh_nats/runtime.rb +160 -0
- data/lib/service_mesh_nats/settings.rb +76 -0
- data/lib/service_mesh_nats/subject.rb +51 -0
- data/lib/service_mesh_nats/version.rb +5 -0
- data/lib/service_mesh_nats.rb +15 -0
- metadata +84 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 20a1213855e080bc361b292f2708b4f023ba38a5051c3ebddffc39cf55b91f7c
|
|
4
|
+
data.tar.gz: fd6939fad816e2beb146fbd8c3697b1221dabb3938a08bdbd73c23664488d71f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 411c35df69a5c8bf43b9ce66e088d666e129c34e64e93a6be3408a471775813f427cb32813a1e47a49bdbaa0e69f56e449ee8e73ee720c397646a70b187c6c0e
|
|
7
|
+
data.tar.gz: 9fbde8fda5e37e58e4d08b0ffe9fe341098bdf5f84d241ddba5bace38003fcf974517bd2cd009deeb5f28cf45f828e3f1f79aa48f2593e1eec4c8f2355dd5f55
|
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,63 @@
|
|
|
1
|
+
# service-mesh-nats-ruby
|
|
2
|
+
|
|
3
|
+
`service_mesh_nats` is the NATS transport implementation in Ruby for the
|
|
4
|
+
[Service Mesh API Specification](https://github.com/Paymentbox-com/service-mesh-api).
|
|
5
|
+
|
|
6
|
+
This gem depends on the contract types and errors in [service-mesh-ruby](https://github.com/Paymentbox-com/service-mesh-ruby).
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
# Gemfile
|
|
12
|
+
gem "service_mesh_nats"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires Ruby 3.3 or newer and a reachable NATS server. Depends on
|
|
16
|
+
`service_mesh` and `nats-pure`.
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
require "service_mesh_nats"
|
|
22
|
+
|
|
23
|
+
echo = ServiceMesh::Target.new(segments: %w[demo echo], kind: :route)
|
|
24
|
+
created = ServiceMesh::Target.new(segments: %w[demo created], kind: :topic)
|
|
25
|
+
map = ServiceMesh::ServiceMap.new(targets: [echo, created])
|
|
26
|
+
|
|
27
|
+
config = {
|
|
28
|
+
"url" => "nats://127.0.0.1:4222",
|
|
29
|
+
"deployment_group" => "demo"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
client = ServiceMeshNats::Client.new(config, map)
|
|
33
|
+
# Connects. Raises BadConfig or the connection failure.
|
|
34
|
+
|
|
35
|
+
runtime = ServiceMeshNats::Runtime.new(client, config,
|
|
36
|
+
endpoints: [
|
|
37
|
+
ServiceMesh::Endpoint.new(target: echo, handler: ->(m) { ServiceMesh::Message.new(target: echo, payload: m.payload) })
|
|
38
|
+
],
|
|
39
|
+
subscribers: [
|
|
40
|
+
ServiceMesh::Subscriber.new(target: created, handler: ->(m) { puts "created: #{m.payload}" })
|
|
41
|
+
])
|
|
42
|
+
# Raises NoDeploymentGroup, BadConfig, KindMismatch, or InvalidTarget, and
|
|
43
|
+
# ArgumentError for a handler that does not respond to call.
|
|
44
|
+
|
|
45
|
+
runtime.start
|
|
46
|
+
at_exit { runtime.stop(10) }
|
|
47
|
+
|
|
48
|
+
reply = client.request(ServiceMesh::Message.new(target: echo, payload: "hi"))
|
|
49
|
+
client.publish(ServiceMesh::Message.new(target: created, payload: "order 42"))
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The client is the runtime's connection. `runtime.client` returns it, and
|
|
53
|
+
`runtime.stop` closes it. A process that only requests and
|
|
54
|
+
publishes builds a client itself and calls `close` when done.
|
|
55
|
+
`runtime.service_map` and `client.service_map` return the map the client was
|
|
56
|
+
built with.
|
|
57
|
+
|
|
58
|
+
## Documentation
|
|
59
|
+
|
|
60
|
+
- [Examples](docs/examples.md): a server process, a call-only client process, and consumer groups
|
|
61
|
+
- [Public API](docs/public-api.md): the constants this gem exposes and how `Client` and `Runtime` behave
|
|
62
|
+
- [What the NATS Runtime Decides](docs/runtime-behavior.md): targets, configuration, metadata, delivery, handler failure, timeouts, concurrency, lifecycle, and transport errors
|
|
63
|
+
- [Development](docs/development.md): the recipes and the tests
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nats/io/client"
|
|
4
|
+
|
|
5
|
+
module ServiceMeshNats
|
|
6
|
+
# Owns one NATS connection and sends messages over it. A Runtime is built
|
|
7
|
+
# from a client and subscribes through its connection.
|
|
8
|
+
class Client
|
|
9
|
+
# The map this client was built with. The client does not otherwise use
|
|
10
|
+
# it.
|
|
11
|
+
attr_reader :service_map
|
|
12
|
+
|
|
13
|
+
# Parses the connection keys of +config+ (every other key is ignored) and
|
|
14
|
+
# opens the connection. A connection failure passes through unchanged.
|
|
15
|
+
# Connection errors reported by nats-pure after that are logged to
|
|
16
|
+
# +logger+ when one is given.
|
|
17
|
+
def initialize(config, service_map, logger: nil)
|
|
18
|
+
@settings = ClientSettings.parse(config)
|
|
19
|
+
@service_map = service_map
|
|
20
|
+
@lock = Mutex.new
|
|
21
|
+
@closed = false
|
|
22
|
+
@nc = NATS::IO::Client.new
|
|
23
|
+
@nc.on_error { |e| logger.warn("service_mesh_nats: #{e.class}: #{e.message}") } if logger
|
|
24
|
+
@nc.connect(@settings.connect_options)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Closes the connection and marks the client closed. Returns nil.
|
|
28
|
+
# Idempotent.
|
|
29
|
+
def close
|
|
30
|
+
@lock.synchronize do
|
|
31
|
+
return nil if @closed
|
|
32
|
+
|
|
33
|
+
@nc.close
|
|
34
|
+
@closed = true
|
|
35
|
+
end
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The NATS::IO::Client this client owns. Used by Runtime. Raises Closed
|
|
40
|
+
# after close.
|
|
41
|
+
def connection
|
|
42
|
+
raise Closed if @closed
|
|
43
|
+
|
|
44
|
+
@nc
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Sends +message+ to a route target and returns the reply.
|
|
48
|
+
#
|
|
49
|
+
# +opts+ may carry REQUEST_TIMEOUT_KEY to override the configured request
|
|
50
|
+
# timeout for this call. Expiry raises NATS::Timeout; no receiver raises
|
|
51
|
+
# NATS::IO::NoRespondersError.
|
|
52
|
+
def request(message, opts = {})
|
|
53
|
+
Subject.check_kind!(message.target, :route, "request")
|
|
54
|
+
subject = Subject.format(message.target)
|
|
55
|
+
timeout = Settings.duration(opts.to_h, REQUEST_TIMEOUT_KEY, @settings.request_timeout)
|
|
56
|
+
nc = connection
|
|
57
|
+
|
|
58
|
+
reply = nc.request_msg(nats_msg(subject, message), timeout: timeout)
|
|
59
|
+
header = reply.header || {}
|
|
60
|
+
raise HandlerError.new(header[HANDLER_ERROR_HEADER].to_s) if header.key?(HANDLER_ERROR_HEADER)
|
|
61
|
+
|
|
62
|
+
ServiceMesh::Message.new(target: message.target, metadata: header, payload: reply.data.to_s)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Sends +message+ to a topic target. +opts+ is accepted for the interface
|
|
66
|
+
# and not read.
|
|
67
|
+
def publish(message, opts = {})
|
|
68
|
+
Subject.check_kind!(message.target, :topic, "publish")
|
|
69
|
+
subject = Subject.format(message.target)
|
|
70
|
+
connection.publish_msg(nats_msg(subject, message))
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def nats_msg(subject, message)
|
|
77
|
+
header = message.metadata.empty? ? nil : message.metadata.transform_keys(&:to_s).transform_values(&:to_s)
|
|
78
|
+
NATS::Msg.new(subject: subject, data: message.payload, header: header)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ServiceMeshNats
|
|
4
|
+
# This transport's errors. The contract errors are ServiceMesh::KindMismatch,
|
|
5
|
+
# ServiceMesh::InvalidTarget, and ServiceMesh::NoDeploymentGroup.
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
|
|
8
|
+
class BadConfig < Error; end
|
|
9
|
+
|
|
10
|
+
class Closed < Error
|
|
11
|
+
def initialize(msg = "client has been closed") = super
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class AlreadyStarted < Error
|
|
15
|
+
def initialize(msg = "runtime already started") = super
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class Stopped < Error
|
|
19
|
+
def initialize(msg = "runtime has been stopped; it is not restartable") = super
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Raised by Client#request when the serving handler raised. +text+ is the
|
|
23
|
+
# handler's error message as sent by the serving runtime.
|
|
24
|
+
class HandlerError < Error
|
|
25
|
+
attr_reader :text
|
|
26
|
+
|
|
27
|
+
def initialize(text)
|
|
28
|
+
@text = text
|
|
29
|
+
super("handler failed: #{text}")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Reply header set when an endpoint handler fails. Application metadata
|
|
34
|
+
# should not use this key.
|
|
35
|
+
HANDLER_ERROR_HEADER = "Mesh-Handler-Error"
|
|
36
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "concurrent"
|
|
4
|
+
require "logger"
|
|
5
|
+
require "nats/io/client"
|
|
6
|
+
|
|
7
|
+
module ServiceMeshNats
|
|
8
|
+
# The service process: subscribes endpoints and subscribers on one NATS
|
|
9
|
+
# connection and runs their handlers on a bounded thread pool.
|
|
10
|
+
class Runtime
|
|
11
|
+
# A validated Endpoint or Subscriber. queue nil means a plain subscription.
|
|
12
|
+
Binding = Data.define(:subject, :queue, :target, :handler, :replies)
|
|
13
|
+
|
|
14
|
+
FLUSH_BUDGET = 5.0
|
|
15
|
+
|
|
16
|
+
# The client this runtime was built from, in every state.
|
|
17
|
+
attr_reader :client
|
|
18
|
+
|
|
19
|
+
# Reads ServiceMesh::DEPLOYMENT_GROUP_KEY and CONCURRENCY_KEY from +config+;
|
|
20
|
+
# connection keys are ignored, the client carries them. Raises
|
|
21
|
+
# ServiceMesh::NoDeploymentGroup, BadConfig, ServiceMesh::KindMismatch, or
|
|
22
|
+
# ServiceMesh::InvalidTarget. Two bindings on one subject become two
|
|
23
|
+
# subscriptions. The connection is not touched until start.
|
|
24
|
+
def initialize(client, config, endpoints: [], subscribers: [], logger: Logger.new($stderr))
|
|
25
|
+
@settings = RuntimeSettings.parse(config)
|
|
26
|
+
@client = client
|
|
27
|
+
@logger = logger
|
|
28
|
+
@bindings = bind_all(endpoints, subscribers)
|
|
29
|
+
|
|
30
|
+
@lock = Mutex.new
|
|
31
|
+
@state = :created
|
|
32
|
+
@subs = []
|
|
33
|
+
@pool = nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def running?
|
|
37
|
+
@state == :running
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The client's service map.
|
|
41
|
+
def service_map
|
|
42
|
+
@client.service_map
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Subscribes every binding on the client's connection and begins
|
|
46
|
+
# receiving. Raises AlreadyStarted on a running runtime, Stopped after
|
|
47
|
+
# stop, and Closed when the client has been closed. A failure while
|
|
48
|
+
# subscribing closes the client, marks the runtime stopped, and passes
|
|
49
|
+
# through.
|
|
50
|
+
def start
|
|
51
|
+
@lock.synchronize do
|
|
52
|
+
raise AlreadyStarted if @state == :running
|
|
53
|
+
raise Stopped if @state == :stopped
|
|
54
|
+
|
|
55
|
+
nc = @client.connection
|
|
56
|
+
|
|
57
|
+
pool = Concurrent::FixedThreadPool.new(@settings.concurrency, name: "service_mesh_nats")
|
|
58
|
+
subs = []
|
|
59
|
+
begin
|
|
60
|
+
@bindings.each do |b|
|
|
61
|
+
opts = b.queue ? {queue: b.queue} : {}
|
|
62
|
+
subs << nc.subscribe(b.subject, opts) { |msg| dispatch(pool, nc, b, msg) }
|
|
63
|
+
end
|
|
64
|
+
# Ensure the server has every subscription before start returns.
|
|
65
|
+
nc.flush(FLUSH_BUDGET)
|
|
66
|
+
rescue => e
|
|
67
|
+
subs.each { |s| s.unsubscribe rescue nil } # rubocop:disable Style/RescueModifier
|
|
68
|
+
pool.kill
|
|
69
|
+
@client.close
|
|
70
|
+
@state = :stopped
|
|
71
|
+
raise e
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
@subs = subs
|
|
75
|
+
@pool = pool
|
|
76
|
+
@state = :running
|
|
77
|
+
end
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Stops receiving, waits up to +drain+ seconds for in-flight handlers,
|
|
82
|
+
# flushes, then closes the client. Returns true when every handler
|
|
83
|
+
# finished, false when some were abandoned. A runtime that is not running
|
|
84
|
+
# returns true and leaves the client as it is. A client the caller closed
|
|
85
|
+
# directly is treated as gone: nothing is flushed and the drain result is
|
|
86
|
+
# still returned.
|
|
87
|
+
def stop(drain)
|
|
88
|
+
drain = Float(drain)
|
|
89
|
+
raise ArgumentError, "drain must be non-negative" if drain.negative?
|
|
90
|
+
|
|
91
|
+
@lock.synchronize do
|
|
92
|
+
return true unless @state == :running
|
|
93
|
+
|
|
94
|
+
@state = :stopped
|
|
95
|
+
@subs.each { |s| s.unsubscribe rescue nil } # rubocop:disable Style/RescueModifier
|
|
96
|
+
|
|
97
|
+
@pool.shutdown
|
|
98
|
+
finished = @pool.wait_for_termination(drain)
|
|
99
|
+
flush if finished
|
|
100
|
+
@client.close
|
|
101
|
+
@subs = []
|
|
102
|
+
finished
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def flush
|
|
109
|
+
@client.connection.flush(FLUSH_BUDGET)
|
|
110
|
+
rescue Closed
|
|
111
|
+
# The caller closed the client directly; there is nothing to flush.
|
|
112
|
+
rescue => e
|
|
113
|
+
@logger.warn("service_mesh_nats: flush during stop failed: #{e.message}")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def bind_all(endpoints, subscribers)
|
|
117
|
+
bind = lambda do |target, want, use, metadata, handler, replies|
|
|
118
|
+
Subject.check_kind!(target, want, use)
|
|
119
|
+
subject = Subject.format(target)
|
|
120
|
+
raise ArgumentError, "#{use} #{subject} handler must respond to call" unless handler.respond_to?(:call)
|
|
121
|
+
|
|
122
|
+
queue = Subject.consumer_group(metadata, target.metadata, @settings.deployment_group)
|
|
123
|
+
Binding.new(subject: subject, queue: queue, target: target, handler: handler, replies: replies)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
endpoints.map { |e| bind.call(e.target, :route, "Endpoint", e.metadata, e.handler, true) } +
|
|
127
|
+
subscribers.map { |s| bind.call(s.target, :topic, "Subscriber", s.metadata, s.handler, false) }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Runs on nats-pure's subscription thread; hands the message to the pool.
|
|
131
|
+
def dispatch(pool, nc, binding, msg)
|
|
132
|
+
pool.post { serve(nc, binding, msg) }
|
|
133
|
+
rescue Concurrent::RejectedExecutionError
|
|
134
|
+
# The pool is shutting down; the subscription is already gone.
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def serve(nc, binding, msg)
|
|
138
|
+
inbound = ServiceMesh::Message.new(target: binding.target, metadata: msg.header || {}, payload: msg.data.to_s)
|
|
139
|
+
|
|
140
|
+
out = binding.handler.call(inbound)
|
|
141
|
+
return unless binding.replies && msg.reply
|
|
142
|
+
|
|
143
|
+
unless out.is_a?(ServiceMesh::Message)
|
|
144
|
+
raise TypeError, "endpoint #{binding.subject} handler returned #{out.class}, expected ServiceMesh::Message"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
reply(nc, msg.reply, out.metadata, out.payload)
|
|
148
|
+
rescue => e
|
|
149
|
+
@logger.error("service_mesh_nats: handler failed on #{binding.subject}: #{e.class}: #{e.message}")
|
|
150
|
+
reply(nc, msg.reply, {HANDLER_ERROR_HEADER => e.message}, "") if binding.replies && msg.reply
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def reply(nc, subject, metadata, payload)
|
|
154
|
+
header = metadata.empty? ? nil : metadata.transform_keys(&:to_s).transform_values(&:to_s)
|
|
155
|
+
nc.publish_msg(NATS::Msg.new(subject: subject, data: payload.to_s.b, header: header))
|
|
156
|
+
rescue => e
|
|
157
|
+
@logger.error("service_mesh_nats: reply failed: #{e.class}: #{e.message}")
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "etc"
|
|
4
|
+
|
|
5
|
+
module ServiceMeshNats
|
|
6
|
+
# Configuration keys this transport reads beyond ServiceMesh::DEPLOYMENT_GROUP_KEY.
|
|
7
|
+
# Every value is a string. Durations are seconds, such as "5" or "0.25".
|
|
8
|
+
URL_KEY = "url"
|
|
9
|
+
NAME_KEY = "name"
|
|
10
|
+
CONNECT_TIMEOUT_KEY = "connect_timeout"
|
|
11
|
+
REQUEST_TIMEOUT_KEY = "request_timeout"
|
|
12
|
+
CONCURRENCY_KEY = "concurrency"
|
|
13
|
+
|
|
14
|
+
DEFAULT_URL = "nats://127.0.0.1:4222"
|
|
15
|
+
DEFAULT_CONNECT_TIMEOUT = 5.0
|
|
16
|
+
DEFAULT_REQUEST_TIMEOUT = 30.0
|
|
17
|
+
|
|
18
|
+
# Value parsers shared by the two settings types.
|
|
19
|
+
module Settings
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# Reads +key+ as a positive number of seconds, or returns +fallback+ when
|
|
23
|
+
# absent.
|
|
24
|
+
def duration(hash, key, fallback)
|
|
25
|
+
return fallback unless hash.key?(key)
|
|
26
|
+
|
|
27
|
+
value = Float(hash[key], exception: false)
|
|
28
|
+
raise BadConfig, "#{key}=#{hash[key].inspect} must be a positive number of seconds" if value.nil? || value <= 0
|
|
29
|
+
|
|
30
|
+
value
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def positive_integer(hash, key, fallback)
|
|
34
|
+
return fallback unless hash.key?(key)
|
|
35
|
+
|
|
36
|
+
value = Integer(hash[key], 10, exception: false)
|
|
37
|
+
raise BadConfig, "#{key}=#{hash[key].inspect} must be a positive integer" if value.nil? || value <= 0
|
|
38
|
+
|
|
39
|
+
value
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The keys a Client reads. Any other key is ignored.
|
|
44
|
+
ClientSettings = Data.define(:url, :name, :connect_timeout, :request_timeout) do
|
|
45
|
+
def self.parse(config)
|
|
46
|
+
config = config.to_h
|
|
47
|
+
new(
|
|
48
|
+
url: config.fetch(URL_KEY, DEFAULT_URL).then { |v| v.to_s.empty? ? DEFAULT_URL : v },
|
|
49
|
+
name: config[NAME_KEY],
|
|
50
|
+
connect_timeout: Settings.duration(config, CONNECT_TIMEOUT_KEY, DEFAULT_CONNECT_TIMEOUT),
|
|
51
|
+
request_timeout: Settings.duration(config, REQUEST_TIMEOUT_KEY, DEFAULT_REQUEST_TIMEOUT)
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def connect_options
|
|
56
|
+
opts = {servers: [url], connect_timeout: connect_timeout}
|
|
57
|
+
opts[:name] = name if name && !name.empty?
|
|
58
|
+
opts
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The keys a Runtime reads. Connection keys in a runtime's config are
|
|
63
|
+
# ignored; the runtime's client carries them.
|
|
64
|
+
RuntimeSettings = Data.define(:deployment_group, :concurrency) do
|
|
65
|
+
def self.parse(config)
|
|
66
|
+
config = config.to_h
|
|
67
|
+
deployment_group = config[ServiceMesh::DEPLOYMENT_GROUP_KEY].to_s
|
|
68
|
+
raise ServiceMesh::NoDeploymentGroup if deployment_group.empty?
|
|
69
|
+
|
|
70
|
+
new(
|
|
71
|
+
deployment_group: deployment_group,
|
|
72
|
+
concurrency: Settings.positive_integer(config, CONCURRENCY_KEY, Etc.nprocessors)
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ServiceMeshNats
|
|
4
|
+
# Assembles targets into NATS subjects and resolves queue groups.
|
|
5
|
+
module Subject
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# Joins segments with "." or raises InvalidTarget with the reason.
|
|
9
|
+
def format(target)
|
|
10
|
+
raise ServiceMesh::InvalidTarget, "no segments" if target.segments.empty?
|
|
11
|
+
|
|
12
|
+
target.segments.each_with_index do |seg, i|
|
|
13
|
+
reason = segment_problem(seg)
|
|
14
|
+
raise ServiceMesh::InvalidTarget, "segment #{i} #{seg.inspect}: #{reason}" if reason
|
|
15
|
+
end
|
|
16
|
+
target.segments.join(".")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def segment_problem(seg)
|
|
20
|
+
return "empty" if seg.empty?
|
|
21
|
+
return "contains '.'" if seg.include?(".")
|
|
22
|
+
return "contains wildcard" if seg.include?("*") || seg.include?(">")
|
|
23
|
+
return "contains whitespace" if seg.match?(/\s/)
|
|
24
|
+
return "contains non-printable character" if seg.match?(/[^[:print:]]/)
|
|
25
|
+
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Raises KindMismatch unless the target's kind is +want+.
|
|
30
|
+
def check_kind!(target, want, use)
|
|
31
|
+
return if target.kind == want
|
|
32
|
+
|
|
33
|
+
raise ServiceMesh::KindMismatch, "#{use} requires a #{want} target, got #{target.kind}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The queue group for a binding. Binding metadata wins, then target
|
|
37
|
+
# metadata, then the deployment group. nil means a plain subscription.
|
|
38
|
+
def consumer_group(binding_metadata, target_metadata, deployment_group)
|
|
39
|
+
[binding_metadata, target_metadata].each do |md|
|
|
40
|
+
next unless md.key?(ServiceMesh::CONSUMER_GROUP_KEY)
|
|
41
|
+
|
|
42
|
+
value = md[ServiceMesh::CONSUMER_GROUP_KEY].to_s
|
|
43
|
+
next if value.empty?
|
|
44
|
+
return nil if value == ServiceMesh::CONSUMER_GROUP_NONE
|
|
45
|
+
|
|
46
|
+
return value
|
|
47
|
+
end
|
|
48
|
+
deployment_group
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "service_mesh"
|
|
4
|
+
|
|
5
|
+
require_relative "service_mesh_nats/version"
|
|
6
|
+
require_relative "service_mesh_nats/errors"
|
|
7
|
+
require_relative "service_mesh_nats/settings"
|
|
8
|
+
require_relative "service_mesh_nats/subject"
|
|
9
|
+
require_relative "service_mesh_nats/client"
|
|
10
|
+
require_relative "service_mesh_nats/runtime"
|
|
11
|
+
|
|
12
|
+
# NATS transport for the Service Mesh API Specification. The contract types
|
|
13
|
+
# and errors come from the service_mesh gem.
|
|
14
|
+
module ServiceMeshNats
|
|
15
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: service_mesh_nats
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.5.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Bryant Morrill
|
|
8
|
+
- Paymentbox
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2026-09-29 00:00:00.000000000 Z
|
|
13
|
+
dependencies:
|
|
14
|
+
- !ruby/object:Gem::Dependency
|
|
15
|
+
name: nats-pure
|
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
|
17
|
+
requirements:
|
|
18
|
+
- - "~>"
|
|
19
|
+
- !ruby/object:Gem::Version
|
|
20
|
+
version: '2.5'
|
|
21
|
+
type: :runtime
|
|
22
|
+
prerelease: false
|
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
24
|
+
requirements:
|
|
25
|
+
- - "~>"
|
|
26
|
+
- !ruby/object:Gem::Version
|
|
27
|
+
version: '2.5'
|
|
28
|
+
- !ruby/object:Gem::Dependency
|
|
29
|
+
name: service_mesh
|
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
|
31
|
+
requirements:
|
|
32
|
+
- - "~>"
|
|
33
|
+
- !ruby/object:Gem::Version
|
|
34
|
+
version: '0.4'
|
|
35
|
+
type: :runtime
|
|
36
|
+
prerelease: false
|
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
38
|
+
requirements:
|
|
39
|
+
- - "~>"
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '0.4'
|
|
42
|
+
description:
|
|
43
|
+
email:
|
|
44
|
+
- bryantreadmorrill@gmail.com
|
|
45
|
+
executables: []
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files: []
|
|
48
|
+
files:
|
|
49
|
+
- LICENSE
|
|
50
|
+
- README.md
|
|
51
|
+
- lib/service_mesh_nats.rb
|
|
52
|
+
- lib/service_mesh_nats/client.rb
|
|
53
|
+
- lib/service_mesh_nats/errors.rb
|
|
54
|
+
- lib/service_mesh_nats/runtime.rb
|
|
55
|
+
- lib/service_mesh_nats/settings.rb
|
|
56
|
+
- lib/service_mesh_nats/subject.rb
|
|
57
|
+
- lib/service_mesh_nats/version.rb
|
|
58
|
+
homepage: https://github.com/Paymentbox-com/service-mesh-nats-ruby
|
|
59
|
+
licenses:
|
|
60
|
+
- MIT
|
|
61
|
+
metadata:
|
|
62
|
+
homepage_uri: https://github.com/Paymentbox-com/service-mesh-nats-ruby
|
|
63
|
+
source_code_uri: https://github.com/Paymentbox-com/service-mesh-nats-ruby
|
|
64
|
+
rubygems_mfa_required: 'true'
|
|
65
|
+
post_install_message:
|
|
66
|
+
rdoc_options: []
|
|
67
|
+
require_paths:
|
|
68
|
+
- lib
|
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '3.3'
|
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
75
|
+
requirements:
|
|
76
|
+
- - ">="
|
|
77
|
+
- !ruby/object:Gem::Version
|
|
78
|
+
version: '0'
|
|
79
|
+
requirements: []
|
|
80
|
+
rubygems_version: 3.5.22
|
|
81
|
+
signing_key:
|
|
82
|
+
specification_version: 4
|
|
83
|
+
summary: Ruby implementation of the Service Mesh API Specification over NATS
|
|
84
|
+
test_files: []
|