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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8cb911c6ac11f486f56610fd734ae2a4e5187495d28524621d742fca976377eb
4
- data.tar.gz: 713a3ea176a74f8a47abc83c4340a9432b781262b9fd5e431ada44991d9c2395
3
+ metadata.gz: 8407a7e14f98bddcfb36e679dead026ab8f809e8da6d4d25ca2b4cda32ff348e
4
+ data.tar.gz: 283df9d8c6e450f397fa68fbec41d19a9b52497046e39b7a60f4ee817c479060
5
5
  SHA512:
6
- metadata.gz: ee37dc08e56cb6dc95262c0c61df02ad8f9fc2168cfff2c30fcf908dfb51c502676e796d00e2903bc9c8251d1d866b829579abce5fca3172336f1a74fb18b1a6
7
- data.tar.gz: f09054e25ce41281175c0d09ccc993ec735f2c3278c9d631d964aa39850fc9fdfd54b08144c67df4d938079245f60a5da0079fda68fb209131f83384c8e421f8
6
+ metadata.gz: df9cae013a07f3478d9309b089db7a985e413963c5422e7c2cb6ee120adba4a40b1d7421320789c0e8e0a3afa6fdd496a1b32664990d2a4f166b177ca7686da3
7
+ data.tar.gz: 7072583f8e389016a2b1b6bdbe4aaddb2e32571730c205fbaee4c4fb73f9b78bd3099e57a8c87369f6e52193a4940b0a24d6bf9dca3bad32377671d79850c04f
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.1.5"
2
+ ".": "0.1.6"
3
3
  }
data/.version.txt CHANGED
@@ -1 +1 @@
1
- 0.1.5
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
 
@@ -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: { name: 'example.echo', version: '1.0.0' },
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.include(SemanticLogger::Loggable)
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
- # Return the thread pool if non-blocking
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 { setup_worker(url, opts, eps, gps) }
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
- client = NATS.connect url
117
- service = client.services.add(**opts)
118
- group_map = add_groups(service, gps)
119
- add_endpoints service, eps, group_map
120
- # Keep the worker thread alive
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(service, gps)
210
+ def add_groups(gps)
131
211
  created = {}
132
- gps.each_key { |name| build_group(service, gps, created, name) }
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(service, defs, cache, name)
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(service, defs, cache, gdef[:parent]) : service
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(service, endpoints, group_map)
239
+ def add_endpoints(endpoints, group_map)
162
240
  endpoints.each do |ep|
163
- parent = ep[:group] ? group_map[ep[:group]] : service
164
- raise ArgumentError, "Group #{ep[:group]} not defined" if ep[:group] && parent.nil?
165
-
166
- parent.endpoints.add(
167
- ep[:name], subject: ep[:subject], queue: ep[:queue]
168
- ) do |raw_msg|
169
- wrapper = MessageWrapper.new(raw_msg)
170
- dispatch_with_middleware(wrapper, ep[:handler])
171
- end
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
  #
@@ -3,7 +3,7 @@
3
3
  module Rubyists
4
4
  module Leopard
5
5
  # x-release-please-start-version
6
- VERSION = '0.1.5'
6
+ VERSION = '0.1.6'
7
7
  # x-release-please-end
8
8
  end
9
9
  end
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.5
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-07-31 00:00:00.000000000 Z
10
+ date: 2025-08-03 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: concurrent-ruby