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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2319a3362d49284e70a8bd451d920af4c02741c9afa6019a413bfe6938d3d1e6
4
- data.tar.gz: e825b5d474fb3f4215566545f0240b5772a2a95284d16054c838b5d0e28e1c3d
3
+ metadata.gz: 8407a7e14f98bddcfb36e679dead026ab8f809e8da6d4d25ca2b4cda32ff348e
4
+ data.tar.gz: 283df9d8c6e450f397fa68fbec41d19a9b52497046e39b7a60f4ee817c479060
5
5
  SHA512:
6
- metadata.gz: c5845ddcda3aafb62b637bdcac59c33040203fdc8b162c29cdd58df82be86982cf0e45705240478d61a86da867ba9851f308c3c1b6a74858f22bfc9b021e3442
7
- data.tar.gz: f3e91dd96d21a71e995359ce31e3ae525d0e042786ed3e7439da5b2274157e1a4d15cc6f9620bd3ed49796e8404021191f886fb3f0799d2471ba5e1e5fef0325
6
+ metadata.gz: df9cae013a07f3478d9309b089db7a985e413963c5422e7c2cb6ee120adba4a40b1d7421320789c0e8e0a3afa6fdd496a1b32664990d2a4f166b177ca7686da3
7
+ data.tar.gz: 7072583f8e389016a2b1b6bdbe4aaddb2e32571730c205fbaee4c4fb73f9b78bd3099e57a8c87369f6e52193a4940b0a24d6bf9dca3bad32377671d79850c04f
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.1.4"
2
+ ".": "0.1.6"
3
3
  }
data/.version.txt CHANGED
@@ -1 +1 @@
1
- 0.1.4
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
 
@@ -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,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
- # Return the thread pool if non-blocking
79
- return spawn_instances(nats_url, service_opts, instances) unless blocking
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 { setup_worker(url, opts, eps, gps) }
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
- client = NATS.connect url
116
- service = client.services.add(**opts)
117
- group_map = add_groups(service, gps)
118
- add_endpoints service, eps, group_map
119
- # 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
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(service, gps)
210
+ def add_groups(gps)
130
211
  created = {}
131
- gps.each_key { |name| build_group(service, gps, created, name) }
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(service, defs, cache, name)
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(service, defs, cache, gdef[:parent]) : service
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(service, endpoints, group_map)
239
+ def add_endpoints(endpoints, group_map)
161
240
  endpoints.each do |ep|
162
- parent = ep[:group] ? group_map[ep[:group]] : service
163
- raise ArgumentError, "Group #{ep[:group]} not defined" if ep[:group] && parent.nil?
164
-
165
- parent.endpoints.add(
166
- ep[:name], subject: ep[:subject], queue: ep[:queue]
167
- ) do |raw_msg|
168
- wrapper = MessageWrapper.new(raw_msg)
169
- dispatch_with_middleware(wrapper, ep[:handler])
170
- 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)
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
  #
@@ -3,7 +3,7 @@
3
3
  module Rubyists
4
4
  module Leopard
5
5
  # x-release-please-start-version
6
- VERSION = '0.1.4'
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.4
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