leopard 0.1.4 → 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 +14 -0
- data/examples/echo_endpoint.rb +9 -1
- data/lib/leopard/nats_api_server.rb +125 -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,19 @@
|
|
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
|
+
|
10
|
+
## [0.1.5](https://github.com/rubyists/leopard/compare/v0.1.4...v0.1.5) (2025-07-31)
|
11
|
+
|
12
|
+
|
13
|
+
### Bug Fixes
|
14
|
+
|
15
|
+
* Run in blocking mode, not just non-blocking ([#15](https://github.com/rubyists/leopard/issues/15)) ([a659145](https://github.com/rubyists/leopard/commit/a659145d8a04efe3b3932b99ab4c11ef0ba2025e))
|
16
|
+
|
3
17
|
## [0.1.4](https://github.com/rubyists/leopard/compare/v0.1.3...v0.1.4) (2025-07-31)
|
4
18
|
|
5
19
|
|
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,10 +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
|
-
|
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)
|
81
|
+
return pool unless blocking
|
80
82
|
|
81
|
-
# Otherwise, just sleep the main thread forever
|
82
83
|
sleep
|
83
84
|
end
|
84
85
|
|
@@ -91,16 +92,86 @@ module Rubyists
|
|
91
92
|
# @param count [Integer] The number of instances to spawn.
|
92
93
|
#
|
93
94
|
# @return [Concurrent::FixedThreadPool] The thread pool managing the worker threads.
|
94
|
-
def spawn_instances(url, opts, count)
|
95
|
+
def spawn_instances(url, opts, count, workers)
|
95
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}"
|
96
99
|
count.times do
|
97
100
|
eps = endpoints.dup
|
98
101
|
gps = groups.dup
|
99
|
-
pool.post {
|
102
|
+
pool.post { build_worker(url, opts, eps, gps, workers) }
|
100
103
|
end
|
101
104
|
pool
|
102
105
|
end
|
103
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
|
+
|
104
175
|
# Sets up a worker thread for the NATS API server.
|
105
176
|
# This method connects to the NATS server, adds the service, groups, and endpoints,
|
106
177
|
# and keeps the worker thread alive.
|
@@ -112,62 +183,80 @@ module Rubyists
|
|
112
183
|
#
|
113
184
|
# @return [void]
|
114
185
|
def setup_worker(url, opts, eps, gps)
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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
|
120
191
|
sleep
|
121
192
|
end
|
122
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
|
+
|
123
205
|
# Adds groups to the NATS service.
|
124
206
|
#
|
125
|
-
# @param service [NATS::Service] The NATS service to add groups to.
|
126
207
|
# @param gps [Hash] The groups to add, where keys are group names and values are group definitions.
|
127
208
|
#
|
128
209
|
# @return [Hash] A map of group names to their created group objects.
|
129
|
-
def add_groups(
|
210
|
+
def add_groups(gps)
|
130
211
|
created = {}
|
131
|
-
gps.each_key { |name| build_group(
|
212
|
+
gps.each_key { |name| build_group(gps, created, name) }
|
132
213
|
created
|
133
214
|
end
|
134
215
|
|
135
216
|
# Builds a group in the NATS service.
|
136
217
|
#
|
137
|
-
# @param service [NATS::Service] The NATS service to add the group to.
|
138
218
|
# @param defs [Hash] The group definitions, where keys are group names and values are group definitions.
|
139
219
|
# @param cache [Hash] A cache to store already created groups.
|
140
220
|
# @param name [String] The name of the group to build.
|
141
221
|
#
|
142
222
|
# @return [NATS::Group] The created group object.
|
143
|
-
def build_group(
|
223
|
+
def build_group(defs, cache, name)
|
144
224
|
return cache[name] if cache.key?(name)
|
145
225
|
|
146
226
|
gdef = defs[name]
|
147
227
|
raise ArgumentError, "Group #{name} not defined" unless gdef
|
148
228
|
|
149
|
-
parent = gdef[:parent] ? build_group(
|
229
|
+
parent = gdef[:parent] ? build_group(defs, cache, gdef[:parent]) : @service
|
150
230
|
cache[name] = parent.groups.add(gdef[:name], queue: gdef[:queue])
|
151
231
|
end
|
152
232
|
|
153
233
|
# Adds endpoints to the NATS service.
|
154
234
|
#
|
155
|
-
# @param service [NATS::Service] The NATS service to add endpoints to.
|
156
235
|
# @param endpoints [Array<Hash>] The list of endpoints to add.
|
157
236
|
# @param group_map [Hash] A map of group names to their created group objects.
|
158
237
|
#
|
159
238
|
# @return [void]
|
160
|
-
def add_endpoints(
|
239
|
+
def add_endpoints(endpoints, group_map)
|
161
240
|
endpoints.each do |ep|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
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)
|
171
260
|
end
|
172
261
|
end
|
173
262
|
|
@@ -179,7 +268,7 @@ module Rubyists
|
|
179
268
|
# @return [void]
|
180
269
|
def dispatch_with_middleware(wrapper, handler)
|
181
270
|
app = ->(w) { handle_message(w.raw, handler) }
|
182
|
-
middleware.reverse_each do |(klass, args, blk)|
|
271
|
+
self.class.middleware.reverse_each do |(klass, args, blk)|
|
183
272
|
app = klass.new(app, *args, &blk)
|
184
273
|
end
|
185
274
|
app.call(wrapper)
|
@@ -202,7 +291,6 @@ module Rubyists
|
|
202
291
|
|
203
292
|
# Processes the result of the handler execution.
|
204
293
|
#
|
205
|
-
#
|
206
294
|
# @param wrapper [MessageWrapper] The message wrapper containing the raw message.
|
207
295
|
# @param result [Dry::Monads::Result] The result of the handler execution.
|
208
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
|