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.
- checksums.yaml +4 -4
- data/.claude/project-overview.md +205 -0
- data/.claude/rules/acquirer.md +61 -0
- data/.claude/rules/arguments.md +59 -0
- data/.claude/rules/logic.md +57 -0
- data/.claude/rules/swarm.md +119 -0
- data/.claude/rules/tests.md +42 -0
- data/.claude/rules/type-checking.md +42 -0
- data/.claude/rules/visitors.md +64 -0
- data/.rubocop.rbs.yml +31 -0
- data/.rubocop.yml +3 -3
- data/.ruby-version +1 -1
- data/CHANGELOG.md +18 -0
- data/CLAUDE.md +31 -0
- data/README.md +19 -3
- data/Rakefile +31 -2
- data/github_ci/.keep +0 -0
- data/lib/redis_queued_locks/acquirer/acquire_lock/delay_execution.rb +2 -2
- data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +1 -4
- data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +1 -1
- data/lib/redis_queued_locks/acquirer/acquire_lock.rb +0 -4
- data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +0 -2
- data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +0 -1
- data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +0 -2
- data/lib/redis_queued_locks/client.rb +145 -144
- data/lib/redis_queued_locks/config/dsl.rb +4 -10
- data/lib/redis_queued_locks/swarm/flush_zombies.rb +8 -6
- data/lib/redis_queued_locks/swarm/supervisor.rb +1 -1
- data/lib/redis_queued_locks/swarm/swarm_element/isolated.rb +166 -44
- data/lib/redis_queued_locks/swarm/swarm_element/threaded.rb +45 -47
- data/lib/redis_queued_locks/version.rb +2 -2
- data/redis_queued_locks.gemspec +1 -1
- data/sig/redis_queued_locks/swarm/flush_zombies.rbs +1 -1
- data/sig/redis_queued_locks/swarm/swarm_element/isolated.rbs +13 -5
- data/sig/redis_queued_locks/swarm/swarm_element/threaded.rbs +6 -6
- metadata +14 -5
- data/github_ci/ruby3.3.gemfile +0 -17
- 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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
138
|
-
#
|
|
139
|
-
# =>
|
|
140
|
-
# =>
|
|
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
|
-
#
|
|
148
|
-
|
|
149
|
-
|
|
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 #
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
251
|
+
swarm_element_results_port << (main_loop != nil && main_loop.alive?) # steep:ignore
|
|
186
252
|
when :start
|
|
187
|
-
main_loop
|
|
188
|
-
|
|
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
|
|
258
|
+
terminate_thread(main_loop)
|
|
259
|
+
swarm_element_results_port << true
|
|
191
260
|
when :kill
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
278
|
-
def
|
|
279
|
-
|
|
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
|
-
|
|
289
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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(
|
|
195
|
+
swarm_element_results.push(true) # steep:ignore
|
|
207
196
|
when :stop
|
|
208
197
|
main_loop&.kill
|
|
209
|
-
swarm_element_results.push(
|
|
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
|
-
|
|
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? &&
|
|
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
|
-
|
|
289
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
337
|
-
|
|
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
|
|
data/redis_queued_locks.gemspec
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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[
|
|
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[
|
|
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
|
|
50
|
-
|
|
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
|