redis_queued_locks 1.16.2 → 1.17.0

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/project-overview.md +205 -0
  3. data/.claude/rules/acquirer.md +61 -0
  4. data/.claude/rules/arguments.md +59 -0
  5. data/.claude/rules/logic.md +57 -0
  6. data/.claude/rules/swarm.md +119 -0
  7. data/.claude/rules/tests.md +42 -0
  8. data/.claude/rules/type-checking.md +42 -0
  9. data/.claude/rules/visitors.md +64 -0
  10. data/.rubocop.rbs.yml +31 -0
  11. data/.rubocop.yml +3 -3
  12. data/.ruby-version +1 -1
  13. data/CHANGELOG.md +18 -0
  14. data/CLAUDE.md +31 -0
  15. data/README.md +19 -3
  16. data/Rakefile +31 -2
  17. data/github_ci/.keep +0 -0
  18. data/lib/redis_queued_locks/acquirer/acquire_lock/delay_execution.rb +2 -2
  19. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +1 -4
  20. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +1 -1
  21. data/lib/redis_queued_locks/acquirer/acquire_lock.rb +0 -4
  22. data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +0 -2
  23. data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +0 -1
  24. data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +0 -2
  25. data/lib/redis_queued_locks/client.rb +145 -144
  26. data/lib/redis_queued_locks/config/dsl.rb +4 -10
  27. data/lib/redis_queued_locks/swarm/flush_zombies.rb +8 -6
  28. data/lib/redis_queued_locks/swarm/supervisor.rb +1 -1
  29. data/lib/redis_queued_locks/swarm/swarm_element/isolated.rb +166 -44
  30. data/lib/redis_queued_locks/swarm/swarm_element/threaded.rb +45 -47
  31. data/lib/redis_queued_locks/version.rb +2 -2
  32. data/redis_queued_locks.gemspec +1 -1
  33. data/sig/redis_queued_locks/swarm/flush_zombies.rbs +1 -1
  34. data/sig/redis_queued_locks/swarm/swarm_element/isolated.rbs +13 -5
  35. data/sig/redis_queued_locks/swarm/swarm_element/threaded.rbs +6 -6
  36. metadata +14 -5
  37. data/github_ci/ruby3.3.gemfile +0 -17
  38. data/github_ci/ruby3.3.gemfile.lock +0 -216
@@ -1,7 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Swarm element isolated inside its own Ractor. Communication works via `Ractor::Port`s:
4
+ # - `swarm_element_results_port`: created in the main Ractor (where the Swarm and its Supervisor
5
+ # live), receives element replies and the element ractor termination notice (`:exited`/`:aborted`,
6
+ # registered via `Ractor#monitor`);
7
+ # - `swarm_element_commands_port`: created inside the element ractor (only the creator ractor can
8
+ # receive from a port) and handed over to the main Ractor during the element startup;
9
+ # Each element instance owns its own pair of ports, so any number of isolated elements can work
10
+ # side by side.
11
+ #
3
12
  # @api private
4
13
  # @since 1.9.0
14
+ # @version 1.17.0
5
15
  # rubocop:disable Metrics/ClassLength
6
16
  class RedisQueuedLocks::Swarm::SwarmElement::Isolated
7
17
  # @since 1.9.0
@@ -19,6 +29,22 @@ class RedisQueuedLocks::Swarm::SwarmElement::Isolated
19
29
  # @since 1.9.0
20
30
  attr_reader :swarm_element
21
31
 
32
+ # Port of the element ractor that receives commands (created inside the element ractor).
33
+ #
34
+ # @return [Ractor::Port,NilClass]
35
+ #
36
+ # @api private
37
+ # @since 1.17.0
38
+ attr_reader :swarm_element_commands_port
39
+
40
+ # Port of the main Ractor that receives element replies and the element termination notice.
41
+ #
42
+ # @return [Ractor::Port,NilClass]
43
+ #
44
+ # @api private
45
+ # @since 1.17.0
46
+ attr_reader :swarm_element_results_port
47
+
22
48
  # @return [RedisQueuedLocks::Utilities::Lock]
23
49
  #
24
50
  # @api private
@@ -30,9 +56,12 @@ class RedisQueuedLocks::Swarm::SwarmElement::Isolated
30
56
  #
31
57
  # @api private
32
58
  # @since 1.9.0
59
+ # @version 1.17.0
33
60
  def initialize(rql_client)
34
61
  @rql_client = rql_client
35
62
  @swarm_element = nil
63
+ @swarm_element_commands_port = nil
64
+ @swarm_element_results_port = nil
36
65
  @sync = RedisQueuedLocks::Utilities::Lock.new
37
66
  end
38
67
 
@@ -100,19 +129,18 @@ class RedisQueuedLocks::Swarm::SwarmElement::Isolated
100
129
  #
101
130
  # @api private
102
131
  # @since 1.9.0
132
+ # @version 1.17.0
103
133
  def status
104
134
  sync.synchronize do
105
135
  ractor_running = swarmed__alive?
106
136
  ractor_state = swarmed? ? ractor_status(swarm_element) : 'non_initialized' # steep:ignore
107
137
 
108
- begin
109
- main_loop_running = swarmed__running?
110
- # steep:ignore:start
111
- main_loop_state =
112
- main_loop_running ? swarm_loop__status[:main_loop][:state] : 'non_initialized'
113
- # steep:ignore:end
114
- rescue Ractor::ClosedError
115
- # NOTE: it can happend when you running RedisQueuedLocks::Swarm#deswarm!
138
+ # NOTE: `nil` when the element ractor is not alive (or has died during the request);
139
+ loop_status = swarm_loop__status
140
+ if loop_status && loop_status[:alive]
141
+ main_loop_running = true
142
+ main_loop_state = loop_status[:state]
143
+ else
116
144
  main_loop_running = false
117
145
  main_loop_state = 'non_initialized'
118
146
  end
@@ -134,38 +162,79 @@ class RedisQueuedLocks::Swarm::SwarmElement::Isolated
134
162
  private
135
163
 
136
164
  # Swarm element lifecycle have the following scheme:
137
- # => 1) init (swarm!): create a ractor, main loop is not started;
138
- # => 2) start (swarm_loop__start!): run main lopp inside the ractor;
139
- # => 3) stop (swarm_loop__stop!): stop the main loop inside a ractor;
140
- # => 4) kill (swarm_loop__kill!): kill the main loop inside teh ractor and kill a ractor;
165
+ # => 1) init (swarm!): create a results port and a ractor (main loop is not started),
166
+ # receive the ractor's command port;
167
+ # => 2) start (swarm_loop__start): run the main loop inside the ractor;
168
+ # => 3) stop (swarm_loop__stop): stop the main loop inside the ractor;
169
+ # => 4) kill (swarm_loop__kill): kill the main loop inside the ractor and finish the ractor;
141
170
  #
142
171
  # @return [void]
143
172
  #
144
173
  # @api private
145
174
  # @since 1.9.0
175
+ # @version 1.17.0
146
176
  def swarm!
147
- # IMPORTANT №1: initialize @swarm_element here with Ractor;
148
- # IMPORTANT №2: your Ractor should invoke .swarm_loop inside (see below);
149
- # IMPORTANT №3: you should pass the main loop logic as a block to .swarm_loop;
177
+ # NOTE: the results port is created in the current (main) Ractor: only it can receive from it;
178
+ results = Ractor::Port.new
179
+ element = spawn_swarm_element!(results)
180
+ @swarm_element_results_port = results
181
+ @swarm_element = element
182
+ # NOTE: `:exited`/`:aborted` will be sent to the results port when the ractor is finished
183
+ # (immediately if it is already finished), so no request can wait for a reply forever;
184
+ element.monitor(results)
185
+ # NOTE: the first message from the element ractor is its command port (see .swarm_loop),
186
+ # any other message is the termination notice (the ractor has died during the startup);
187
+ handshake = results.receive
188
+ @swarm_element_commands_port = handshake.is_a?(Ractor::Port) ? handshake : nil
189
+ end
190
+
191
+ # @param swarm_element_results_port [Ractor::Port] Results port of the main Ractor.
192
+ # @return [Ractor]
193
+ #
194
+ # @api private
195
+ # @since 1.17.0
196
+ def spawn_swarm_element!(swarm_element_results_port)
197
+ # IMPORTANT №1: create and return a Ractor here (pass `swarm_element_results_port` and all
198
+ # required configs into `Ractor.new` as shareable/copyable values);
199
+ # IMPORTANT №2: your Ractor should invoke .swarm_loop(swarm_element_results_port) inside
200
+ # (see below);
201
+ # IMPORTANT №3: you should pass the main loop logic as a block to .swarm_loop
202
+ # (the block should return a Thread that wraps the looped logic);
203
+ raise NotImplementedError, "#{self.class}#spawn_swarm_element! is not implemented"
150
204
  end
151
205
 
206
+ # Internal protocol (bare scalars/primitives, no `{ ok:, result: }` wrapper):
207
+ # - handshake: the command port itself (the first message on the results port);
208
+ # - `:status` => `{ alive: <Boolean>, state: <String> }`;
209
+ # - `:is_active` => `true`/`false`;
210
+ # - `:start`, `:stop`, `:kill` => `true` (ack);
211
+ # - Symbol messages on the results port are reserved for the Ractor#monitor notice;
212
+ #
213
+ # @param swarm_element_results_port [Ractor::Port] Results port of the main Ractor.
152
214
  # @param main_loop_spawner [Block]
153
215
  # @return [void]
154
216
  #
155
217
  # @api private
156
218
  # @since 1.9.0
219
+ # @version 1.17.0
157
220
  # rubocop:disable Layout/ClassStructure, Lint/IneffectiveAccessModifier, Metrics/MethodLength
158
- def self.swarm_loop(&main_loop_spawner)
221
+ def self.swarm_loop(swarm_element_results_port, &main_loop_spawner)
159
222
  # NOTE:
160
223
  # This self.-related part of code is placed in the middle of class in order
161
224
  # to provide better code readability (it is placed next to the method inside
162
- # wich it should be called (see #swarm!)). That's why some rubocop cops are disabled.
225
+ # wich it should be called (see #spawn_swarm_element!)). That's why some rubocop
226
+ # cops are disabled.
227
+
228
+ # NOTE: the command port should be created inside the element ractor (only the creator
229
+ # ractor can receive from the port), so it is handed over to the main Ractor at startup;
230
+ swarm_element_commands_port = Ractor::Port.new
231
+ swarm_element_results_port << swarm_element_commands_port
163
232
 
164
233
  # @type var main_loop: Thread?
165
234
  main_loop = nil
166
235
 
167
236
  loop do
168
- command = Ractor.receive
237
+ command = swarm_element_commands_port.receive
169
238
 
170
239
  case command
171
240
  when :status
@@ -177,25 +246,46 @@ class RedisQueuedLocks::Swarm::SwarmElement::Isolated
177
246
  # @type var main_loop: Thread
178
247
  RedisQueuedLocks::Utilities.thread_state(main_loop)
179
248
  end
180
- # NOTE: (will be reworked with Ractor::Port in the next RQL release (~1.17))
181
- Ractor.yield({ # steep:ignore
182
- main_loop: { alive: main_loop_alive, state: main_loop_state }
183
- })
249
+ swarm_element_results_port << { alive: main_loop_alive, state: main_loop_state }
184
250
  when :is_active
185
- Ractor.yield(main_loop != nil && main_loop.alive?) # steep:ignore
251
+ swarm_element_results_port << (main_loop != nil && main_loop.alive?) # steep:ignore
186
252
  when :start
187
- main_loop.kill if main_loop != nil # steep:ignore
188
- main_loop = yield # REFERENCE: `main_loop_spawner.call`
253
+ terminate_thread(main_loop)
254
+ # REFERENCE: `main_loop_spawner.call`
255
+ main_loop = yield.tap { |thread| thread.abort_on_exception = false }
256
+ swarm_element_results_port << true
189
257
  when :stop
190
- main_loop.kill if main_loop != nil # steep:ignore
258
+ terminate_thread(main_loop)
259
+ swarm_element_results_port << true
191
260
  when :kill
192
- main_loop.kill if main_loop != nil # steep:ignore
193
- exit
261
+ # NOTE: terminate the main loop and all auxiliary threads it may have left behind (for
262
+ # example, socket connection helper threads when the loop is killed during connection):
263
+ # the ractor stays alive while it has unfinished threads;
264
+ (::Thread.list - [::Thread.current]).each { |thread| terminate_thread(thread) }
265
+ swarm_element_results_port << true
266
+ break
194
267
  end
195
268
  end
196
269
  end
197
270
  # rubocop:enable Layout/ClassStructure, Lint/IneffectiveAccessModifier, Metrics/MethodLength
198
271
 
272
+ # Kills the thread and waits for its termination: the ractor can not finish
273
+ # (and its status stays "running") while it has unfinished threads.
274
+ #
275
+ # @param thread [Thread,NilClass]
276
+ # @return [void]
277
+ #
278
+ # @api private
279
+ # @since 1.17.0
280
+ # rubocop:disable Lint/IneffectiveAccessModifier
281
+ def self.terminate_thread(thread)
282
+ return if thread == nil
283
+ thread.kill
284
+ # NOTE: join re-raises the exception that has failed the thread: it is not our case here;
285
+ thread.join rescue nil
286
+ end
287
+ # rubocop:enable Lint/IneffectiveAccessModifier
288
+
199
289
  # @return [Boolean]
200
290
  #
201
291
  # @api private
@@ -232,61 +322,93 @@ class RedisQueuedLocks::Swarm::SwarmElement::Isolated
232
322
  #
233
323
  # @api private
234
324
  # @since 1.9.0
325
+ # @version 1.17.0
235
326
  def swarmed__running?
236
- swarm_element != nil && ractor_alive?(swarm_element) && swarm_loop__is_active # steep:ignore
327
+ swarmed__alive? && swarm_loop__is_active == true
237
328
  end
238
329
 
239
330
  # @return [Boolean]
240
331
  #
241
332
  # @api private
242
333
  # @since 1.9.0
334
+ # @version 1.17.0
243
335
  def swarmed__stopped?
244
- swarm_element != nil && ractor_alive?(swarm_element) && !swarm_loop__is_active # steep:ignore
336
+ # NOTE: `nil` (the element ractor has died during the request) is neither active nor stopped;
337
+ swarmed__alive? && swarm_loop__is_active == false
245
338
  end
246
339
 
247
- # @return [Boolean]
340
+ # @return [Boolean,NilClass] Is the main loop alive (`nil` when the element is not available).
248
341
  #
249
342
  # @api private
250
343
  # @since 1.9.0
344
+ # @version 1.17.0
251
345
  def swarm_loop__is_active
252
- return false if idle? || swarmed__dead?
253
- sync.synchronize { swarm_element.send(:is_active).take }
346
+ swarm_loop__send_command(:is_active) #: bool?
254
347
  end
255
348
 
256
- # @return [Hash]
349
+ # @return [Hash<Symbol,Boolean|String>,NilClass]
350
+ # Format: `{ alive: <Boolean>, state: <String> }` (`nil` when the element is not available).
257
351
  #
258
352
  # @api private
259
353
  # @since 1.9.0
354
+ # @version 1.17.0
260
355
  def swarm_loop__status
261
- return if idle? || swarmed__dead?
262
- sync.synchronize { swarm_element.send(:status).take }
356
+ swarm_loop__send_command(:status) #: swarmLoopStatus?
263
357
  end
264
358
 
265
359
  # @return [void]
266
360
  #
267
361
  # @api private
268
362
  # @since 1.9.0
363
+ # @version 1.17.0
269
364
  def swarm_loop__start
270
- return if idle? || swarmed__dead?
271
- sync.synchronize { swarm_element.send(:start) }
365
+ swarm_loop__send_command(:start)
272
366
  end
273
367
 
274
368
  # @return [void]
275
369
  #
276
370
  # @api private
277
- # @since 1.9.0
278
- def swarm_loop__pause
279
- return if idle? || swarmed__dead?
280
- sync.synchronize { swarm_element.send(:stop) }
371
+ # @since 1.17.0
372
+ def swarm_loop__stop
373
+ swarm_loop__send_command(:stop)
281
374
  end
282
375
 
283
376
  # @return [void]
284
377
  #
285
378
  # @api private
286
379
  # @since 1.9.0
380
+ # @version 1.17.0
287
381
  def swarm_loop__kill
288
- return if idle? || swarmed__dead?
289
- sync.synchronize { swarm_element.send(:kill) }
382
+ sync.synchronize do
383
+ # NOTE: wait for the ractor finish only when it has confirmed the kill command;
384
+ killed = swarm_loop__send_command(:kill) == true
385
+ @swarm_element_commands_port = nil
386
+ swarm_element.join if killed # steep:ignore
387
+ end
388
+ rescue Ractor::RemoteError
389
+ # NOTE: the element ractor has been finished with an exception: it is dead anyway;
390
+ end
391
+
392
+ # Sends the command to the element ractor and waits for its reply. Replies are received
393
+ # strictly one by one under the lock, so each reply belongs to the sent command.
394
+ #
395
+ # @param command [Symbol]
396
+ # @return [Boolean,Hash<Symbol,Boolean|String>,NilClass]
397
+ # The bare reply value (see .swarm_loop) or `nil` if the element is dead.
398
+ #
399
+ # @api private
400
+ # @since 1.17.0
401
+ def swarm_loop__send_command(command)
402
+ return if idle? || swarmed__dead? || swarm_element_commands_port == nil
403
+ sync.synchronize do
404
+ swarm_element_commands_port << command
405
+ reply = swarm_element_results_port.receive # steep:ignore
406
+ # NOTE: Symbol replies are reserved for the Ractor#monitor notice (`:exited`/`:aborted`);
407
+ reply.is_a?(Symbol) ? nil : reply
408
+ end
409
+ rescue Ractor::ClosedError
410
+ # NOTE: the command port is closed together with the finished element ractor;
411
+ nil
290
412
  end
291
413
  end
292
414
  # rubocop:enable Metrics/ClassLength
@@ -125,20 +125,16 @@ class RedisQueuedLocks::Swarm::SwarmElement::Threaded
125
125
  #
126
126
  # @api private
127
127
  # @since 1.9.0
128
- # rubocop:disable Style/RedundantBegin
128
+ # @version 1.17.0
129
129
  def status
130
130
  sync.synchronize do
131
131
  thread_running = swarmed__alive?
132
132
  thread_state = swarmed? ? thread_state(swarm_element) : 'non_initialized' # steep:ignore
133
133
 
134
134
  main_loop_running = swarmed__running?
135
- main_loop_state = begin
136
- if main_loop_running
137
- swarm_loop__status[:result][:main_loop][:state] # steep:ignore
138
- else
139
- 'non_initialized'
140
- end
141
- end
135
+ # NOTE: `nil` when the element is not alive (or has been terminated during the request);
136
+ loop_status = swarm_loop__status if main_loop_running
137
+ main_loop_state = loop_status ? loop_status[:state] : 'non_initialized'
142
138
 
143
139
  {
144
140
  enabled: enabled?,
@@ -153,7 +149,6 @@ class RedisQueuedLocks::Swarm::SwarmElement::Threaded
153
149
  }
154
150
  end
155
151
  end
156
- # rubocop:enable Style/RedundantBegin
157
152
 
158
153
  private
159
154
 
@@ -169,7 +164,7 @@ class RedisQueuedLocks::Swarm::SwarmElement::Threaded
169
164
  #
170
165
  # @api private
171
166
  # @since 1.9.0
172
- # rubocop:disable Metrics/MethodLength
167
+ # @version 1.17.0
173
168
  def swarm!
174
169
  # NOTE: kill the main loop at start to prevent any async-thread-race-based memory leaks;
175
170
  main_loop&.kill
@@ -187,42 +182,37 @@ class RedisQueuedLocks::Swarm::SwarmElement::Threaded
187
182
  main_loop_alive = main_loop != nil && main_loop.alive?
188
183
  # steep:ignore:end
189
184
 
190
- # steep:ignore:start
191
185
  main_loop_state = (main_loop == nil) ? 'non_initialized' : thread_state(main_loop)
192
- # steep:ignore:end
193
186
 
194
187
  # steep:ignore:start
195
- swarm_element_results.push({
196
- ok: true,
197
- result: { main_loop: { alive: main_loop_alive, state: main_loop_state } }
198
- })
188
+ swarm_element_results.push({ alive: main_loop_alive, state: main_loop_state })
199
189
  # steep:ignore:end
200
190
  when :is_active
201
- is_active = main_loop != nil && main_loop.alive? # steep:ignore
202
- swarm_element_results.push({ ok: true, result: { is_active: } }) # steep:ignore
191
+ swarm_element_results.push(main_loop != nil && main_loop.alive?) # steep:ignore
203
192
  when :start
204
193
  main_loop&.kill
205
194
  @main_loop = spawn_main_loop!.tap { |thread| thread.abort_on_exception = false }
206
- swarm_element_results.push({ ok: true, result: nil }) # steep:ignore
195
+ swarm_element_results.push(true) # steep:ignore
207
196
  when :stop
208
197
  main_loop&.kill
209
- swarm_element_results.push({ ok: true, result: nil }) # steep:ignore
198
+ swarm_element_results.push(true) # steep:ignore
210
199
  end
211
200
  end
212
201
  end
213
202
  end
214
- # rubocop:enable Metrics/MethodLength
215
203
 
216
204
  # @return [Thread] Thread with #abort_onexception == false that wraps loop'ed logic;
217
205
  #
218
206
  # @api private
219
207
  # @since 1.9.0
220
- def spawn_main_loop! # steep:ignore
208
+ # @version 1.17.0
209
+ def spawn_main_loop!
221
210
  # NOTE:
222
211
  # - provide the swarm element looped logic here wrapped into the thread;
223
212
  # - created thread will be reconfigured inside the swarm_element logic with a
224
213
  # `abort_on_exception = false` (cuz the stauts of the thread is
225
214
  # totally controlled by the @swarm_element's logic);
215
+ raise NotImplementedError, "#{self.class}#spawn_main_loop! is not implemented"
226
216
  end
227
217
 
228
218
  # @return [Boolean]
@@ -261,10 +251,9 @@ class RedisQueuedLocks::Swarm::SwarmElement::Threaded
261
251
  #
262
252
  # @api private
263
253
  # @since 1.9.0
254
+ # @version 1.17.0
264
255
  def swarmed__running?
265
- swarmed__alive? && !terminating? && (swarm_loop__is_active.yield_self do |result|
266
- result != nil && result[:ok] && result[:result][:is_active] # steep:ignore
267
- end)
256
+ swarmed__alive? && !terminating? && swarm_loop__is_active == true
268
257
  end
269
258
 
270
259
  # @return [Boolean,NilClass]
@@ -284,57 +273,66 @@ class RedisQueuedLocks::Swarm::SwarmElement::Threaded
284
273
  #
285
274
  # @api private
286
275
  # @since 1.9.0
276
+ # @version 1.17.0
287
277
  def swarmed__stopped?
288
- swarmed__alive? && (terminating? || !(swarm_loop__is_active.yield_self do |result|
289
- result && result[:ok] && result[:result][:is_active] # steep:ignore
290
- end))
278
+ # NOTE: `nil` (no answer) is treated as "not active";
279
+ swarmed__alive? && (terminating? || swarm_loop__is_active != true)
291
280
  end
292
281
 
293
- # @return [Boolean,NilClass]
282
+ # @return [Boolean,NilClass] Is the main loop alive (`nil` when the element is not available).
294
283
  #
295
284
  # @api private
296
285
  # @since 1.9.0
286
+ # @version 1.17.0
297
287
  def swarm_loop__is_active
298
- return if idle? || swarmed__dead? || terminating?
299
- sync.synchronize do
300
- swarm_element_commands.push(:is_active) # steep:ignore
301
- swarm_element_results.pop # steep:ignore
302
- end
288
+ swarm_loop__send_command(:is_active) #: bool?
303
289
  end
304
290
 
305
- # @return [Hash,NilClass]
291
+ # @return [Hash<Symbol,Boolean|String>,NilClass]
292
+ # Format: `{ alive: <Boolean>, state: <String> }` (`nil` when the element is not available).
306
293
  #
307
294
  # @api private
308
295
  # @since 1.9.0
296
+ # @version 1.17.0
309
297
  def swarm_loop__status
310
- return if idle? || swarmed__dead? || terminating?
311
- sync.synchronize do
312
- swarm_element_commands.push(:status) # steep:ignore
313
- swarm_element_results.pop # steep:ignore
314
- end
298
+ swarm_loop__send_command(:status) #: swarmLoopStatus?
315
299
  end
316
300
 
317
301
  # @return [void]
318
302
  #
319
303
  # @api private
320
304
  # @since 1.9.0
305
+ # @version 1.17.0
321
306
  def swarm_loop__start
322
- return if idle? || swarmed__dead? || terminating?
323
- sync.synchronize do
324
- swarm_element_commands.push(:start) # steep:ignore
325
- swarm_element_results.pop # steep:ignore
326
- end
307
+ swarm_loop__send_command(:start)
327
308
  end
328
309
 
329
310
  # @return [void]
330
311
  #
331
312
  # @api private
332
313
  # @since 1.9.0
314
+ # @version 1.17.0
333
315
  def swarm_loop__stop
316
+ swarm_loop__send_command(:stop)
317
+ end
318
+
319
+ # Sends the command to the control thread and waits for its reply. Replies are received
320
+ # strictly one by one under the lock, so each reply belongs to the sent command.
321
+ #
322
+ # @param command [Symbol]
323
+ # @return [Boolean,Hash<Symbol,Boolean|String>,NilClass]
324
+ # The bare reply value (see #swarm!) or `nil` if the element is not available.
325
+ #
326
+ # @api private
327
+ # @since 1.17.0
328
+ def swarm_loop__send_command(command)
334
329
  return if idle? || swarmed__dead? || terminating?
335
330
  sync.synchronize do
336
- swarm_element_commands.push(:stop) # steep:ignore
337
- swarm_element_results.pop # steep:ignore
331
+ commands = swarm_element_commands
332
+ results = swarm_element_results
333
+ next if commands == nil || results == nil
334
+ commands.push(command)
335
+ results.pop
338
336
  end
339
337
  end
340
338
 
@@ -5,6 +5,6 @@ module RedisQueuedLocks
5
5
  #
6
6
  # @api public
7
7
  # @since 0.0.1
8
- # @version 1.16.2
9
- VERSION = '1.16.2'
8
+ # @version 1.17.0
9
+ VERSION = '1.17.0'
10
10
  end
@@ -3,7 +3,7 @@
3
3
  require_relative 'lib/redis_queued_locks/version'
4
4
 
5
5
  Gem::Specification.new do |spec|
6
- spec.required_ruby_version = '>= 3.3'
6
+ spec.required_ruby_version = '>= 4.0'
7
7
 
8
8
  spec.name = 'redis_queued_locks'
9
9
  spec.version = RedisQueuedLocks::VERSION
@@ -7,7 +7,7 @@ module RedisQueuedLocks
7
7
  def self.flush_zombies: (RC::client redis_client, Integer zombie_ttl, Integer lock_scan_size, Integer queue_scan_size) -> flushedZombies
8
8
 
9
9
  def enabled?: () -> bool
10
- def swarm!: () -> void
10
+ def spawn_swarm_element!: (Ractor::Port[SwarmElement::Isolated::resultsPortMessage] swarm_element_results_port) -> Ractor
11
11
  end
12
12
  end
13
13
  end
@@ -8,10 +8,14 @@ module RedisQueuedLocks
8
8
 
9
9
  @rql_client: RQL::Client
10
10
  @swarm_element: Ractor?
11
+ @swarm_element_commands_port: Ractor::Port[Symbol]?
12
+ @swarm_element_results_port: Ractor::Port[resultsPortMessage]?
11
13
  @sync: RQL::Utilities::Lock
12
14
 
13
15
  attr_reader rql_client: RQL::Client
14
16
  attr_reader swarm_element: Ractor?
17
+ attr_reader swarm_element_commands_port: Ractor::Port[Symbol]?
18
+ attr_reader swarm_element_results_port: Ractor::Port[resultsPortMessage]?
15
19
  attr_reader sync: RQL::Utilities::Lock
16
20
 
17
21
  def initialize: (RQL::Client rql_client) -> void
@@ -27,25 +31,29 @@ module RedisQueuedLocks
27
31
  }
28
32
  def status: () -> elementStatus
29
33
 
30
- def self.swarm_loop: () { () -> Thread } -> void
34
+ type swarmLoopStatus = { alive: bool, state: String }
35
+ type swarmLoopReply = swarmLoopStatus | bool
36
+ type resultsPortMessage = swarmLoopReply | Ractor::Port[Symbol] | Symbol
37
+ def self.swarm_loop: (Ractor::Port[resultsPortMessage] swarm_element_results_port) { () -> Thread } -> void
38
+ def self.terminate_thread: (Thread? thread) -> void
31
39
 
32
40
  private
33
41
 
34
42
  def swarm!: () -> void
43
+ def spawn_swarm_element!: (Ractor::Port[resultsPortMessage] swarm_element_results_port) -> Ractor
35
44
  def idle?: () -> bool
36
45
  def swarmed?: () -> bool
37
46
  def swarmed__alive?: () -> bool
38
47
  def swarmed__dead?: () -> bool
39
48
  def swarmed__running?: () -> bool
40
49
  def swarmed__stopped?: () -> bool
41
- def swarm_loop__is_active: () -> bool
42
-
43
- type swarmLoopStatus = { main_loop: { alive: bool, state: String } }
50
+ def swarm_loop__is_active: () -> bool?
44
51
  def swarm_loop__status: () -> swarmLoopStatus?
45
52
 
46
53
  def swarm_loop__start: () -> void
47
- def swarm_loop__pause: () -> void
54
+ def swarm_loop__stop: () -> void
48
55
  def swarm_loop__kill: () -> void
56
+ def swarm_loop__send_command: (Symbol command) -> swarmLoopReply?
49
57
  end
50
58
  end
51
59
  end
@@ -10,14 +10,14 @@ module RedisQueuedLocks
10
10
  @swarm_element: Thread?
11
11
  @main_loop: Thread?
12
12
  @swarm_element_commands: Thread::SizedQueue[Symbol]?
13
- @swarm_element_results: Thread::SizedQueue[{ ok: bool, result: Hash[untyped,untyped] }]?
13
+ @swarm_element_results: Thread::SizedQueue[swarmLoopReply]?
14
14
  @sync: RQL::Utilities::Lock
15
15
 
16
16
  attr_reader rql_client: RQL::Client
17
17
  attr_reader swarm_element: Thread?
18
18
  attr_reader main_loop: Thread?
19
19
  attr_reader swarm_element_commands: Thread::SizedQueue[Symbol]?
20
- attr_reader swarm_element_results: Thread::SizedQueue[{ ok: bool, result: Hash[untyped,untyped] }]?
20
+ attr_reader swarm_element_results: Thread::SizedQueue[swarmLoopReply]?
21
21
  attr_reader sync: RQL::Utilities::Lock
22
22
 
23
23
  def initialize: (RQL::Client rql_client) -> void
@@ -46,14 +46,14 @@ module RedisQueuedLocks
46
46
  def terminating?: () -> bool?
47
47
  def swarmed__stopped?: () -> bool
48
48
 
49
- type swarmLoopIsActive = { ok: bool, result: { is_active: bool } }
50
- def swarm_loop__is_active: () -> swarmLoopIsActive?
51
-
52
- type swarmLoopStatus = { ok: bool, result: { main_loop: { alive: bool, state: String } } }
49
+ type swarmLoopStatus = { alive: bool, state: String }
50
+ type swarmLoopReply = swarmLoopStatus | bool
51
+ def swarm_loop__is_active: () -> bool?
53
52
  def swarm_loop__status: () -> swarmLoopStatus?
54
53
 
55
54
  def swarm_loop__start: () -> void
56
55
  def swarm_loop__stop: () -> void
56
+ def swarm_loop__send_command: (Symbol command) -> swarmLoopReply?
57
57
  def swarm_element__termiante: () -> void
58
58
  end
59
59
  end