leopard 0.1.5 → 0.1.6
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/.release-please-manifest.json +1 -1
- data/.version.txt +1 -1
- data/CHANGELOG.md +7 -0
- data/examples/echo_endpoint.rb +9 -1
- data/lib/leopard/nats_api_server.rb +124 -37
- data/lib/leopard/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: 8407a7e14f98bddcfb36e679dead026ab8f809e8da6d4d25ca2b4cda32ff348e
|
4
|
+
data.tar.gz: 283df9d8c6e450f397fa68fbec41d19a9b52497046e39b7a60f4ee817c479060
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: df9cae013a07f3478d9309b089db7a985e413963c5422e7c2cb6ee120adba4a40b1d7421320789c0e8e0a3afa6fdd496a1b32664990d2a4f166b177ca7686da3
|
7
|
+
data.tar.gz: 7072583f8e389016a2b1b6bdbe4aaddb2e32571730c205fbaee4c4fb73f9b78bd3099e57a8c87369f6e52193a4940b0a24d6bf9dca3bad32377671d79850c04f
|
data/.version.txt
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.6
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [0.1.6](https://github.com/rubyists/leopard/compare/v0.1.5...v0.1.6) (2025-08-03)
|
4
|
+
|
5
|
+
|
6
|
+
### Features
|
7
|
+
|
8
|
+
* Adds graceful shutdown when INT/TERM/QUIT signal is received ([#18](https://github.com/rubyists/leopard/issues/18)) ([ce03fb0](https://github.com/rubyists/leopard/commit/ce03fb00afcbbadadc413766b62df9451f7b73b8))
|
9
|
+
|
3
10
|
## [0.1.5](https://github.com/rubyists/leopard/compare/v0.1.4...v0.1.5) (2025-07-31)
|
4
11
|
|
5
12
|
|
data/examples/echo_endpoint.rb
CHANGED
@@ -7,13 +7,21 @@ require_relative '../lib/leopard/nats_api_server'
|
|
7
7
|
class EchoService
|
8
8
|
include Rubyists::Leopard::NatsApiServer
|
9
9
|
|
10
|
+
def initialize(a_var = 1)
|
11
|
+
logger.info "EchoService initialized with a_var: #{a_var}"
|
12
|
+
end
|
13
|
+
|
10
14
|
endpoint(:echo) { |msg| Success(msg.data) }
|
11
15
|
end
|
12
16
|
|
13
17
|
if __FILE__ == $PROGRAM_NAME
|
14
18
|
EchoService.run(
|
15
19
|
nats_url: 'nats://localhost:4222',
|
16
|
-
service_opts: {
|
20
|
+
service_opts: {
|
21
|
+
name: 'example.echo',
|
22
|
+
version: '1.0.0',
|
23
|
+
instance_args: [2],
|
24
|
+
},
|
17
25
|
instances: 4,
|
18
26
|
)
|
19
27
|
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'nats/client'
|
4
4
|
require 'dry/monads'
|
5
|
+
require 'dry/configurable'
|
5
6
|
require 'concurrent'
|
6
7
|
require_relative '../leopard'
|
7
8
|
require_relative 'message_wrapper'
|
@@ -14,10 +15,14 @@ module Rubyists
|
|
14
15
|
|
15
16
|
def self.included(base)
|
16
17
|
base.extend(ClassMethods)
|
18
|
+
base.include(InstanceMethods)
|
17
19
|
base.extend(Dry::Monads[:result])
|
18
|
-
base.
|
20
|
+
base.extend(Dry::Configurable)
|
21
|
+
base.setting :logger, default: Rubyists::Leopard.logger, reader: true
|
19
22
|
end
|
20
23
|
|
24
|
+
Endpoint = Struct.new(:name, :subject, :queue, :group, :handler)
|
25
|
+
|
21
26
|
module ClassMethods
|
22
27
|
def endpoints = @endpoints ||= []
|
23
28
|
def groups = @groups ||= {}
|
@@ -33,13 +38,7 @@ module Rubyists
|
|
33
38
|
#
|
34
39
|
# @return [void]
|
35
40
|
def endpoint(name, subject: nil, queue: nil, group: nil, &handler)
|
36
|
-
endpoints <<
|
37
|
-
name:,
|
38
|
-
subject: subject || name,
|
39
|
-
queue:,
|
40
|
-
group:,
|
41
|
-
handler:,
|
42
|
-
}
|
41
|
+
endpoints << Endpoint.new(name:, subject: subject || name, queue:, group:, handler:)
|
43
42
|
end
|
44
43
|
|
45
44
|
# Define a group for organizing endpoints.
|
@@ -75,11 +74,12 @@ module Rubyists
|
|
75
74
|
# @return [void]
|
76
75
|
def run(nats_url:, service_opts:, instances: 1, blocking: true)
|
77
76
|
logger.info 'Booting NATS API server...'
|
78
|
-
|
79
|
-
pool = spawn_instances(nats_url, service_opts, instances)
|
77
|
+
workers = Concurrent::Array.new
|
78
|
+
pool = spawn_instances(nats_url, service_opts, instances, workers)
|
79
|
+
logger.info 'Setting up signal trap...'
|
80
|
+
trap_signals(workers, pool)
|
80
81
|
return pool unless blocking
|
81
82
|
|
82
|
-
# Otherwise, just sleep the main thread forever
|
83
83
|
sleep
|
84
84
|
end
|
85
85
|
|
@@ -92,16 +92,86 @@ module Rubyists
|
|
92
92
|
# @param count [Integer] The number of instances to spawn.
|
93
93
|
#
|
94
94
|
# @return [Concurrent::FixedThreadPool] The thread pool managing the worker threads.
|
95
|
-
def spawn_instances(url, opts, count)
|
95
|
+
def spawn_instances(url, opts, count, workers)
|
96
96
|
pool = Concurrent::FixedThreadPool.new(count)
|
97
|
+
@instance_args = opts.delete(:instance_args) || nil
|
98
|
+
logger.info "Building #{count} workers with options: #{opts.inspect}, instance_args: #{@instance_args}"
|
97
99
|
count.times do
|
98
100
|
eps = endpoints.dup
|
99
101
|
gps = groups.dup
|
100
|
-
pool.post {
|
102
|
+
pool.post { build_worker(url, opts, eps, gps, workers) }
|
101
103
|
end
|
102
104
|
pool
|
103
105
|
end
|
104
106
|
|
107
|
+
# Builds a worker instance and sets it up with the NATS server.
|
108
|
+
#
|
109
|
+
# @param url [String] The URL of the NATS server.
|
110
|
+
# @param opts [Hash] Options for the NATS service.
|
111
|
+
# @param eps [Array<Hash>] The list of endpoints to add.
|
112
|
+
# @param gps [Hash] The groups to add.
|
113
|
+
# @param workers [Array] The array to store worker instances.
|
114
|
+
#
|
115
|
+
# @return [void]
|
116
|
+
def build_worker(url, opts, eps, gps, workers)
|
117
|
+
worker = @instance_args ? new(*@instance_args) : new
|
118
|
+
workers << worker
|
119
|
+
worker.setup_worker(url, opts, eps, gps)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Shuts down the NATS API server gracefully.
|
123
|
+
#
|
124
|
+
# @param workers [Array] The array of worker instances to stop.
|
125
|
+
# @param pool [Concurrent::FixedThreadPool] The thread pool managing the worker threads.
|
126
|
+
#
|
127
|
+
# @return [Proc] A lambda that performs the shutdown operations.
|
128
|
+
def shutdown(workers, pool)
|
129
|
+
lambda do
|
130
|
+
logger.warn 'Draining worker subscriptions...'
|
131
|
+
workers.each(&:stop)
|
132
|
+
logger.warn 'All workers stopped, shutting down pool...'
|
133
|
+
pool.shutdown
|
134
|
+
logger.warn 'Pool is shut down, waiting for termination!'
|
135
|
+
pool.wait_for_termination
|
136
|
+
logger.warn 'Bye bye!'
|
137
|
+
wake_main_thread
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Sets up signal traps for graceful shutdown of the NATS API server.
|
142
|
+
#
|
143
|
+
# @param workers [Array] The array of worker instances to stop on signal.
|
144
|
+
# @param pool [Concurrent::FixedThreadPool] The thread pool managing the worker threads.
|
145
|
+
#
|
146
|
+
# @return [void]
|
147
|
+
def trap_signals(workers, pool)
|
148
|
+
return if @trapped
|
149
|
+
|
150
|
+
%w[INT TERM QUIT].each do |sig|
|
151
|
+
trap(sig) do
|
152
|
+
logger.warn "Received #{sig} signal, shutting down..."
|
153
|
+
Thread.new { shutdown(workers, pool).call }
|
154
|
+
end
|
155
|
+
end
|
156
|
+
@trapped = true
|
157
|
+
end
|
158
|
+
|
159
|
+
# Wakes up the main thread to allow it to continue execution after the server is stopped.
|
160
|
+
# This is useful when the server is running in a blocking mode.
|
161
|
+
# If the main thread is not blocked, this method does nothing.
|
162
|
+
#
|
163
|
+
# @return [void]
|
164
|
+
def wake_main_thread
|
165
|
+
Thread.main.wakeup
|
166
|
+
rescue ThreadError
|
167
|
+
nil
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
module InstanceMethods
|
172
|
+
# Returns the logger configured for the NATS API server.
|
173
|
+
def logger = self.class.logger
|
174
|
+
|
105
175
|
# Sets up a worker thread for the NATS API server.
|
106
176
|
# This method connects to the NATS server, adds the service, groups, and endpoints,
|
107
177
|
# and keeps the worker thread alive.
|
@@ -113,62 +183,80 @@ module Rubyists
|
|
113
183
|
#
|
114
184
|
# @return [void]
|
115
185
|
def setup_worker(url, opts, eps, gps)
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
186
|
+
@thread = Thread.current
|
187
|
+
@client = NATS.connect url
|
188
|
+
@service = @client.services.add(**opts)
|
189
|
+
group_map = add_groups(gps)
|
190
|
+
add_endpoints eps, group_map
|
121
191
|
sleep
|
122
192
|
end
|
123
193
|
|
194
|
+
# Stops the NATS API server worker.
|
195
|
+
def stop
|
196
|
+
@service&.stop
|
197
|
+
@client&.close
|
198
|
+
@thread&.wakeup
|
199
|
+
rescue ThreadError
|
200
|
+
nil
|
201
|
+
end
|
202
|
+
|
203
|
+
private
|
204
|
+
|
124
205
|
# Adds groups to the NATS service.
|
125
206
|
#
|
126
|
-
# @param service [NATS::Service] The NATS service to add groups to.
|
127
207
|
# @param gps [Hash] The groups to add, where keys are group names and values are group definitions.
|
128
208
|
#
|
129
209
|
# @return [Hash] A map of group names to their created group objects.
|
130
|
-
def add_groups(
|
210
|
+
def add_groups(gps)
|
131
211
|
created = {}
|
132
|
-
gps.each_key { |name| build_group(
|
212
|
+
gps.each_key { |name| build_group(gps, created, name) }
|
133
213
|
created
|
134
214
|
end
|
135
215
|
|
136
216
|
# Builds a group in the NATS service.
|
137
217
|
#
|
138
|
-
# @param service [NATS::Service] The NATS service to add the group to.
|
139
218
|
# @param defs [Hash] The group definitions, where keys are group names and values are group definitions.
|
140
219
|
# @param cache [Hash] A cache to store already created groups.
|
141
220
|
# @param name [String] The name of the group to build.
|
142
221
|
#
|
143
222
|
# @return [NATS::Group] The created group object.
|
144
|
-
def build_group(
|
223
|
+
def build_group(defs, cache, name)
|
145
224
|
return cache[name] if cache.key?(name)
|
146
225
|
|
147
226
|
gdef = defs[name]
|
148
227
|
raise ArgumentError, "Group #{name} not defined" unless gdef
|
149
228
|
|
150
|
-
parent = gdef[:parent] ? build_group(
|
229
|
+
parent = gdef[:parent] ? build_group(defs, cache, gdef[:parent]) : @service
|
151
230
|
cache[name] = parent.groups.add(gdef[:name], queue: gdef[:queue])
|
152
231
|
end
|
153
232
|
|
154
233
|
# Adds endpoints to the NATS service.
|
155
234
|
#
|
156
|
-
# @param service [NATS::Service] The NATS service to add endpoints to.
|
157
235
|
# @param endpoints [Array<Hash>] The list of endpoints to add.
|
158
236
|
# @param group_map [Hash] A map of group names to their created group objects.
|
159
237
|
#
|
160
238
|
# @return [void]
|
161
|
-
def add_endpoints(
|
239
|
+
def add_endpoints(endpoints, group_map)
|
162
240
|
endpoints.each do |ep|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
241
|
+
grp = ep.group
|
242
|
+
parent = grp ? group_map[grp] : @service
|
243
|
+
raise ArgumentError, "Group #{grp} not defined" if grp && parent.nil?
|
244
|
+
|
245
|
+
build_endpoint(parent, ep)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
# Builds an endpoint in the NATS service.
|
250
|
+
#
|
251
|
+
# @param parent [NATS::Group] The parent group or service to add the endpoint to.
|
252
|
+
# @param ept [Endpoint] The endpoint definition containing name, subject, queue, and handler.
|
253
|
+
# NOTE: Named ept because `endpoint` is a DSL method we expose, to avoid confusion.
|
254
|
+
#
|
255
|
+
# @return [void]
|
256
|
+
def build_endpoint(parent, ept)
|
257
|
+
parent.endpoints.add(ept.name, subject: ept.subject, queue: ept.queue) do |raw_msg|
|
258
|
+
wrapper = MessageWrapper.new(raw_msg)
|
259
|
+
dispatch_with_middleware(wrapper, ept.handler)
|
172
260
|
end
|
173
261
|
end
|
174
262
|
|
@@ -180,7 +268,7 @@ module Rubyists
|
|
180
268
|
# @return [void]
|
181
269
|
def dispatch_with_middleware(wrapper, handler)
|
182
270
|
app = ->(w) { handle_message(w.raw, handler) }
|
183
|
-
middleware.reverse_each do |(klass, args, blk)|
|
271
|
+
self.class.middleware.reverse_each do |(klass, args, blk)|
|
184
272
|
app = klass.new(app, *args, &blk)
|
185
273
|
end
|
186
274
|
app.call(wrapper)
|
@@ -203,7 +291,6 @@ module Rubyists
|
|
203
291
|
|
204
292
|
# Processes the result of the handler execution.
|
205
293
|
#
|
206
|
-
#
|
207
294
|
# @param wrapper [MessageWrapper] The message wrapper containing the raw message.
|
208
295
|
# @param result [Dry::Monads::Result] The result of the handler execution.
|
209
296
|
#
|
data/lib/leopard/version.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: leopard
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- bougyman
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-08-03 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: concurrent-ruby
|