redis_queued_locks 1.7.0 → 1.9.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/CHANGELOG.md +60 -1
  4. data/README.md +485 -46
  5. data/lib/redis_queued_locks/acquier/acquire_lock/dequeue_from_lock_queue/log_visitor.rb +4 -0
  6. data/lib/redis_queued_locks/acquier/acquire_lock/dequeue_from_lock_queue.rb +4 -1
  7. data/lib/redis_queued_locks/acquier/acquire_lock/instr_visitor.rb +20 -5
  8. data/lib/redis_queued_locks/acquier/acquire_lock/log_visitor.rb +24 -0
  9. data/lib/redis_queued_locks/acquier/acquire_lock/try_to_lock/log_visitor.rb +56 -0
  10. data/lib/redis_queued_locks/acquier/acquire_lock/try_to_lock.rb +37 -30
  11. data/lib/redis_queued_locks/acquier/acquire_lock/with_acq_timeout.rb +41 -7
  12. data/lib/redis_queued_locks/acquier/acquire_lock/yield_expire/log_visitor.rb +8 -0
  13. data/lib/redis_queued_locks/acquier/acquire_lock/yield_expire.rb +21 -9
  14. data/lib/redis_queued_locks/acquier/acquire_lock.rb +61 -22
  15. data/lib/redis_queued_locks/acquier/clear_dead_requests.rb +5 -1
  16. data/lib/redis_queued_locks/acquier/extend_lock_ttl.rb +5 -1
  17. data/lib/redis_queued_locks/acquier/lock_info.rb +4 -3
  18. data/lib/redis_queued_locks/acquier/locks.rb +2 -2
  19. data/lib/redis_queued_locks/acquier/queue_info.rb +2 -2
  20. data/lib/redis_queued_locks/acquier/release_all_locks.rb +12 -2
  21. data/lib/redis_queued_locks/acquier/release_lock.rb +12 -2
  22. data/lib/redis_queued_locks/client.rb +320 -10
  23. data/lib/redis_queued_locks/errors.rb +8 -0
  24. data/lib/redis_queued_locks/instrument.rb +8 -1
  25. data/lib/redis_queued_locks/logging.rb +8 -1
  26. data/lib/redis_queued_locks/resource.rb +59 -1
  27. data/lib/redis_queued_locks/swarm/acquirers.rb +44 -0
  28. data/lib/redis_queued_locks/swarm/flush_zombies.rb +133 -0
  29. data/lib/redis_queued_locks/swarm/probe_hosts.rb +69 -0
  30. data/lib/redis_queued_locks/swarm/redis_client_builder.rb +67 -0
  31. data/lib/redis_queued_locks/swarm/supervisor.rb +83 -0
  32. data/lib/redis_queued_locks/swarm/swarm_element/isolated.rb +287 -0
  33. data/lib/redis_queued_locks/swarm/swarm_element/threaded.rb +351 -0
  34. data/lib/redis_queued_locks/swarm/swarm_element.rb +8 -0
  35. data/lib/redis_queued_locks/swarm/zombie_info.rb +145 -0
  36. data/lib/redis_queued_locks/swarm.rb +241 -0
  37. data/lib/redis_queued_locks/utilities/lock.rb +22 -0
  38. data/lib/redis_queued_locks/utilities.rb +75 -0
  39. data/lib/redis_queued_locks/version.rb +2 -2
  40. data/lib/redis_queued_locks.rb +2 -0
  41. data/redis_queued_locks.gemspec +6 -10
  42. metadata +24 -6
  43. data/lib/redis_queued_locks/watcher.rb +0 -1
@@ -0,0 +1,287 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 1.9.0
5
+ # rubocop:disable Metrics/ClassLength
6
+ class RedisQueuedLocks::Swarm::SwarmElement::Isolated
7
+ # @since 1.9.0
8
+ include RedisQueuedLocks::Utilities
9
+
10
+ # @return [RedisQueuedLocks::Client]
11
+ #
12
+ # @api private
13
+ # @since 1.9.0
14
+ attr_reader :rql_client
15
+
16
+ # @return [Ractor,NilClass]
17
+ #
18
+ # @api private
19
+ # @since 1.9.0
20
+ attr_reader :swarm_element
21
+
22
+ # @return [RedisQueuedLocks::Utilities::Lock]
23
+ #
24
+ # @api private
25
+ # @since 1.9.0
26
+ attr_reader :sync
27
+
28
+ # @return [void]
29
+ #
30
+ # @api private
31
+ # @since 1.9.0
32
+ def initialize(rql_client)
33
+ @rql_client = rql_client
34
+ @swarm_element = nil
35
+ @sync = RedisQueuedLocks::Utilities::Lock.new
36
+ end
37
+
38
+ # @return [void]
39
+ #
40
+ # @api private
41
+ # @since 1.9.0
42
+ def try_swarm!
43
+ return unless enabled?
44
+
45
+ sync.synchronize do
46
+ swarm_loop__kill
47
+ swarm!
48
+ swarm_loop__start
49
+ end
50
+ end
51
+
52
+ # @return [void]
53
+ #
54
+ # @api private
55
+ # @since 1.9.0
56
+ def reswarm_if_dead!
57
+ return unless enabled?
58
+
59
+ sync.synchronize do
60
+ if swarmed__stopped?
61
+ swarm_loop__start
62
+ elsif swarmed__dead? || idle?
63
+ swarm!
64
+ swarm_loop__start
65
+ end
66
+ end
67
+ end
68
+
69
+ # @return [void]
70
+ #
71
+ # @api private
72
+ # @since 1.9.0
73
+ def try_kill!
74
+ sync.synchronize do
75
+ swarm_loop__kill
76
+ end
77
+ end
78
+
79
+ # @return [Boolean]
80
+ #
81
+ # @api private
82
+ # @since 1.9.0
83
+ def enabled?
84
+ # NOTE: provide an <is enabled> logic here by analyzing the redis queued locks config.
85
+ end
86
+
87
+ # @return [Hash<Symbol,Boolean|Hash<Symbol,String|Boolean>>]
88
+ # Format: {
89
+ # enabled: <Boolean>,
90
+ # ractor: {
91
+ # running: <Boolean>,
92
+ # state: <String>,
93
+ # },
94
+ # main_loop: {
95
+ # running: <Boolean>,
96
+ # state: <String>
97
+ # }
98
+ # }
99
+ #
100
+ # @api private
101
+ # @since 1.9.0
102
+ def status
103
+ sync.synchronize do
104
+ ractor_running = swarmed__alive?
105
+ ractor_state = swarmed? ? ractor_status(swarm_element) : 'non_initialized'
106
+
107
+ main_loop_running = nil
108
+ main_loop_state = nil
109
+ begin
110
+ main_loop_running = swarmed__running?
111
+ main_loop_state =
112
+ main_loop_running ? swarm_loop__status[:main_loop][:state] : 'non_initialized'
113
+ rescue Ractor::ClosedError
114
+ # NOTE: it can happend when you run RedisQueuedLocks::Swarm#deswarm!;
115
+ main_loop_running = false
116
+ main_loop_state = 'non_initialized'
117
+ end
118
+
119
+ {
120
+ enabled: enabled?,
121
+ ractor: {
122
+ running: ractor_running,
123
+ state: ractor_state
124
+ },
125
+ main_loop: {
126
+ running: main_loop_running,
127
+ state: main_loop_state
128
+ }
129
+ }
130
+ end
131
+ end
132
+
133
+ private
134
+
135
+ # Swarm element lifecycle have the following scheme:
136
+ # => 1) init (swarm!): create a ractor, main loop is not started;
137
+ # => 2) start (swarm_loop__start!): run main lopp inside the ractor;
138
+ # => 3) stop (swarm_loop__stop!): stop the main loop inside a ractor;
139
+ # => 4) kill (swarm_loop__kill!): kill the main loop inside teh ractor and kill a ractor;
140
+ #
141
+ # @return [void]
142
+ #
143
+ # @api private
144
+ # @since 1.9.0
145
+ def swarm!
146
+ # IMPORTANT №1: initialize @swarm_element here with Ractor;
147
+ # IMPORTANT №2: your Ractor should invoke .swarm_loop inside (see below);
148
+ # IMPORTANT №3: you should pass the main loop logic as a block to .swarm_loop;
149
+ end
150
+
151
+ # @param main_loop_spawner [Block]
152
+ # @return [void]
153
+ #
154
+ # @api private
155
+ # @since 1.9.0
156
+ # rubocop:disable Layout/ClassStructure, Lint/IneffectiveAccessModifier
157
+ def self.swarm_loop(&main_loop_spawner)
158
+ # NOTE:
159
+ # This self.-related part of code is placed in the middle of class in order
160
+ # to provide better code readability (it is placed next to the method inside
161
+ # wich it should be called (see #swarm!)). That's why some rubocop cops are disabled.
162
+
163
+ main_loop = nil
164
+
165
+ loop do
166
+ command = Ractor.receive
167
+
168
+ case command
169
+ when :status
170
+ main_loop_alive = main_loop != nil && main_loop.alive?
171
+ main_loop_state =
172
+ if main_loop == nil
173
+ 'non_initialized'
174
+ else
175
+ # NOTE: (full name resolution): ractor has no syntax-based constatnt context;
176
+ RedisQueuedLocks::Utilities.thread_state(main_loop)
177
+ end
178
+ Ractor.yield({ main_loop: { alive: main_loop_alive, state: main_loop_state } })
179
+ when :is_active
180
+ Ractor.yield(main_loop != nil && main_loop.alive?)
181
+ when :start
182
+ main_loop.kill if main_loop != nil
183
+ main_loop = yield # REFERENCE: `main_loop_spawner.call`
184
+ when :stop
185
+ main_loop.kill if main_loop != nil
186
+ when :kill
187
+ main_loop.kill if main_loop != nil
188
+ exit
189
+ end
190
+ end
191
+ end
192
+ # rubocop:enable Layout/ClassStructure, Lint/IneffectiveAccessModifier
193
+
194
+ # @return [Boolean]
195
+ #
196
+ # @api private
197
+ # @since 1.9.0
198
+ def idle?
199
+ swarm_element == nil
200
+ end
201
+
202
+ # @return [Boolean]
203
+ #
204
+ # @api private
205
+ # @since 1.9.0
206
+ def swarmed?
207
+ swarm_element != nil
208
+ end
209
+
210
+ # @return [Boolean]
211
+ #
212
+ # @api private
213
+ # @since 1.9.0
214
+ def swarmed__alive?
215
+ swarm_element != nil && ractor_alive?(swarm_element)
216
+ end
217
+
218
+ # @return [Boolean]
219
+ #
220
+ # @api private
221
+ # @since 1.9.0
222
+ def swarmed__dead?
223
+ swarm_element != nil && !ractor_alive?(swarm_element)
224
+ end
225
+
226
+ # @return [Boolean]
227
+ #
228
+ # @api private
229
+ # @since 1.9.0
230
+ def swarmed__running?
231
+ swarm_element != nil && ractor_alive?(swarm_element) && swarm_loop__is_active
232
+ end
233
+
234
+ # @return [Boolean]
235
+ #
236
+ # @api private
237
+ # @since 1.9.0
238
+ def swarmed__stopped?
239
+ swarm_element != nil && ractor_alive?(swarm_element) && !swarm_loop__is_active
240
+ end
241
+
242
+ # @return [Boolean]
243
+ #
244
+ # @api private
245
+ # @since 1.9.0
246
+ def swarm_loop__is_active
247
+ return if idle? || swarmed__dead?
248
+ sync.synchronize { swarm_element.send(:is_active).take }
249
+ end
250
+
251
+ # @return [Hash]
252
+ #
253
+ # @api private
254
+ # @since 1.9.0
255
+ def swarm_loop__status
256
+ return if idle? || swarmed__dead?
257
+ sync.synchronize { swarm_element.send(:status).take }
258
+ end
259
+
260
+ # @return [void]
261
+ #
262
+ # @api private
263
+ # @since 1.9.0
264
+ def swarm_loop__start
265
+ return if idle? || swarmed__dead?
266
+ sync.synchronize { swarm_element.send(:start) }
267
+ end
268
+
269
+ # @return [void]
270
+ #
271
+ # @api private
272
+ # @since 1.9.0
273
+ def swarm_loop__pause
274
+ return if idle? || swarmed__dead?
275
+ sync.synchronize { swarm_element.send(:stop) }
276
+ end
277
+
278
+ # @return [void]
279
+ #
280
+ # @api private
281
+ # @since 1.9.0
282
+ def swarm_loop__kill
283
+ return if idle? || swarmed__dead?
284
+ sync.synchronize { swarm_element.send(:kill) }
285
+ end
286
+ end
287
+ # rubocop:enable Metrics/ClassLength
@@ -0,0 +1,351 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 1.9.0
5
+ # rubocop:disable Metrics/ClassLength
6
+ class RedisQueuedLocks::Swarm::SwarmElement::Threaded
7
+ # @since 1.9.0
8
+ include RedisQueuedLocks::Utilities
9
+
10
+ # @return [RedisQueuedLocks::Client]
11
+ #
12
+ # @api private
13
+ # @since 1.9.0
14
+ attr_reader :rql_client
15
+
16
+ # @return [Thread,NilClass]
17
+ #
18
+ # @api private
19
+ # @since 1.9.0
20
+ attr_reader :swarm_element
21
+
22
+ # The main loop reference is only used inside the swarm element thread and
23
+ # swarm element termination. It should not be used everywhere else!
24
+ # This is strongly technical variable refenrece.
25
+ #
26
+ # @return [Thread,NilClass]
27
+ #
28
+ # @api private
29
+ # @since 1.9.0
30
+ attr_reader :main_loop
31
+
32
+ # @return [Thread::SizedQueue,NilClass]
33
+ #
34
+ # @api private
35
+ # @since 1.9.0
36
+ attr_reader :swarm_element_commands
37
+
38
+ # @return [Thread::SizedQueue]
39
+ #
40
+ # @api private
41
+ # @since 1.9.0
42
+ attr_reader :swarm_element_results
43
+
44
+ # @return [RedisQueuedLocks::Utilities::Lock]
45
+ #
46
+ # @api private
47
+ # @since 1.9.0
48
+ attr_reader :sync
49
+
50
+ # @param rql_client [RedisQueuedLocks::Client]
51
+ # @return [void]
52
+ #
53
+ # @api private
54
+ # @since 1.9.0
55
+ def initialize(rql_client)
56
+ @rql_client = rql_client
57
+ @swarm_element = nil
58
+ @main_loop = nil # NOTE: strongly technical variable refenrece (see attr_reader docs)
59
+ @swarm_element_commands = nil
60
+ @swarm_element_results = nil
61
+ @sync = RedisQueuedLocks::Utilities::Lock.new
62
+ end
63
+
64
+ # @return [void]
65
+ #
66
+ # @api private
67
+ # @since 1.9.0
68
+ def try_swarm!
69
+ return unless enabled?
70
+
71
+ sync.synchronize do
72
+ swarm_element__termiante
73
+ swarm!
74
+ swarm_loop__start
75
+ end
76
+ end
77
+
78
+ # @return [void]
79
+ #
80
+ # @api private
81
+ # @since 19.0.0
82
+ def reswarm_if_dead!
83
+ return unless enabled?
84
+
85
+ sync.synchronize do
86
+ if swarmed__stopped?
87
+ swarm_loop__start
88
+ elsif swarmed__dead? || idle?
89
+ swarm!
90
+ swarm_loop__start
91
+ end
92
+ end
93
+ end
94
+
95
+ # @return [void]
96
+ #
97
+ # @api private
98
+ # @since 1.9.0
99
+ def try_kill!
100
+ sync.synchronize do
101
+ swarm_element__termiante
102
+ end
103
+ end
104
+
105
+ # @return [Boolean]
106
+ #
107
+ # @api private
108
+ # @since 1.9.0
109
+ def enabled?
110
+ # NOTE: provide an <is enabled> logic here by analyzing the redis queued locks config.
111
+ end
112
+
113
+ # @return [Hash<Symbol,Boolean|Hash<Symbol,String|Boolean>>]
114
+ # Format: {
115
+ # enabled: <Boolean>,
116
+ # thread: {
117
+ # running: <Boolean>,
118
+ # state: <String>,
119
+ # },
120
+ # main_loop: {
121
+ # running: <Boolean>,
122
+ # state: <String>
123
+ # }
124
+ # }
125
+ #
126
+ # @api private
127
+ # @since 1.9.0
128
+ # rubocop:disable Style/RedundantBegin
129
+ def status
130
+ sync.synchronize do
131
+ thread_running = swarmed__alive?
132
+ thread_state = swarmed? ? thread_state(swarm_element) : 'non_initialized'
133
+
134
+ main_loop_running = swarmed__running?
135
+ main_loop_state = begin
136
+ if main_loop_running
137
+ swarm_loop__status[:result][:main_loop][:state]
138
+ else
139
+ 'non_initialized'
140
+ end
141
+ end
142
+
143
+ {
144
+ enabled: enabled?,
145
+ thread: {
146
+ running: thread_running,
147
+ state: thread_state
148
+ },
149
+ main_loop: {
150
+ running: main_loop_running,
151
+ state: main_loop_state
152
+ }
153
+ }
154
+ end
155
+ end
156
+ # rubocop:enable Style/RedundantBegin
157
+
158
+ private
159
+
160
+ # Swarm element lifecycle have the following flow:
161
+ # => 1) init (swarm!): create a thread, main loop is not started;
162
+ # => 2) start (swarm_loop__start): run the main lopp inside the created thread;
163
+ # => 3) stop (swarm_loop__stop): stop the main loop inside the created thread;
164
+ # => 4) terminate (swarm_element__termiante): kill the created thread;
165
+ # In addition you should implement `#spawn_main_loop!` method that will be the main logic
166
+ # of the your swarm element.
167
+ #
168
+ # @return [void]
169
+ #
170
+ # @api private
171
+ # @since 1.9.0
172
+ # rubocop:disable Metrics/MethodLength
173
+ def swarm!
174
+ # NOTE: kill the main loop at start to prevent any async-thread-race-based memory leaks;
175
+ main_loop&.kill
176
+
177
+ @swarm_element_commands = Thread::SizedQueue.new(1)
178
+ @swarm_element_results = Thread::SizedQueue.new(1)
179
+
180
+ @swarm_element = Thread.new do
181
+ loop do
182
+ command = swarm_element_commands.pop
183
+
184
+ case command
185
+ when :status
186
+ main_loop_alive = main_loop != nil && main_loop.alive?
187
+ main_loop_state = (main_loop == nil) ? 'non_initialized' : thread_state(main_loop)
188
+ swarm_element_results.push({
189
+ ok: true,
190
+ result: { main_loop: { alive: main_loop_alive, state: main_loop_state } }
191
+ })
192
+ when :is_active
193
+ is_active = main_loop != nil && main_loop.alive?
194
+ swarm_element_results.push({ ok: true, result: { is_active: } })
195
+ when :start
196
+ main_loop&.kill
197
+ @main_loop = spawn_main_loop!.tap { |thread| thread.abort_on_exception = false }
198
+ swarm_element_results.push({ ok: true, result: nil })
199
+ when :stop
200
+ main_loop&.kill
201
+ swarm_element_results.push({ ok: true, result: nil })
202
+ end
203
+ end
204
+ end
205
+ end
206
+ # rubocop:enable Metrics/MethodLength
207
+
208
+ # @return [Thread] Thread with #abort_onexception == false that wraps loop'ed logic;
209
+ #
210
+ # @api private
211
+ # @since 1.9.0
212
+ def spawn_main_loop!
213
+ # NOTE:
214
+ # - provide the swarm element looped logic here wrapped into the thread;
215
+ # - created thread will be reconfigured inside the swarm_element logic with a
216
+ # `abort_on_exception = false` (cuz the stauts of the thread is
217
+ # totally controlled by the @swarm_element's logic);
218
+ end
219
+
220
+ # @return [Boolean]
221
+ #
222
+ # @api private
223
+ # @since 1.9.0
224
+ def idle?
225
+ swarm_element == nil
226
+ end
227
+
228
+ # @return [Boolean]
229
+ #
230
+ # @api private
231
+ # @since 1.9.0
232
+ def swarmed?
233
+ swarm_element != nil
234
+ end
235
+
236
+ # @return [Boolean]
237
+ #
238
+ # @api private
239
+ # @since 1.9.0
240
+ def swarmed__alive?
241
+ swarmed? && swarm_element.alive? && !terminating?
242
+ end
243
+
244
+ # @return [Boolean]
245
+ #
246
+ # @api private
247
+ # @since 1.9.0
248
+ def swarmed__dead?
249
+ swarmed? && (!swarm_element.alive? || terminating?)
250
+ end
251
+
252
+ # @return [Boolean]
253
+ #
254
+ # @api private
255
+ # @since 1.9.0
256
+ def swarmed__running?
257
+ swarmed__alive? && !terminating? && (swarm_loop__is_active.yield_self do |result|
258
+ result && result[:ok] && result[:result][:is_active]
259
+ end)
260
+ end
261
+
262
+ # @return [Boolean,NilClass]
263
+ #
264
+ # @api private
265
+ # @since 1.9.0
266
+ # rubocop:disable Layout/MultilineOperationIndentation
267
+ def terminating?
268
+ swarm_element_commands == nil ||
269
+ swarm_element_commands&.closed? ||
270
+ swarm_element_results == nil ||
271
+ swarm_element_results&.closed?
272
+ end
273
+ # rubocop:enable Layout/MultilineOperationIndentation
274
+
275
+ # @return [Boolean]
276
+ #
277
+ # @api private
278
+ # @since 1.9.0
279
+ def swarmed__stopped?
280
+ swarmed__alive? && (terminating? || !(swarm_loop__is_active.yield_self do |result|
281
+ result && result[:ok] && result[:result][:is_active]
282
+ end))
283
+ end
284
+
285
+ # @return [Boolean,NilClass]
286
+ #
287
+ # @api private
288
+ # @since 1.9.0
289
+ def swarm_loop__is_active
290
+ return if idle? || swarmed__dead? || terminating?
291
+ sync.synchronize do
292
+ swarm_element_commands.push(:is_active)
293
+ swarm_element_results.pop
294
+ end
295
+ end
296
+
297
+ # @return [Hash,NilClass]
298
+ #
299
+ # @api private
300
+ # @since 1.9.0
301
+ def swarm_loop__status
302
+ return if idle? || swarmed__dead? || terminating?
303
+ sync.synchronize do
304
+ swarm_element_commands.push(:status)
305
+ swarm_element_results.pop
306
+ end
307
+ end
308
+
309
+ # @return [void]
310
+ #
311
+ # @api private
312
+ # @since 1.9.0
313
+ def swarm_loop__start
314
+ return if idle? || swarmed__dead? || terminating?
315
+ sync.synchronize do
316
+ swarm_element_commands.push(:start)
317
+ swarm_element_results.pop
318
+ end
319
+ end
320
+
321
+ # @return [void]
322
+ #
323
+ # @api private
324
+ # @since 1.9.0
325
+ def swarm_loop__stop
326
+ return if idle? || swarmed__dead? || terminating?
327
+ sync.synchronize do
328
+ swarm_element_commands.push(:stop)
329
+ swarm_element_results.pop
330
+ end
331
+ end
332
+
333
+ # @return [void]
334
+ #
335
+ # @api private
336
+ # @since 1.9.0
337
+ def swarm_element__termiante
338
+ return if idle? || swarmed__dead?
339
+ sync.synchronize do
340
+ main_loop&.kill
341
+ swarm_element.kill
342
+ swarm_element_commands.close
343
+ swarm_element_results.close
344
+ swarm_element_commands.clear
345
+ swarm_element_results.clear
346
+ @swarm_element_commands = nil
347
+ @swarm_element_results = nil
348
+ end
349
+ end
350
+ end
351
+ # rubocop:enable Metrics/ClassLength
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 1.9.0
5
+ module RedisQueuedLocks::Swarm::SwarmElement
6
+ require_relative 'swarm_element/isolated'
7
+ require_relative 'swarm_element/threaded'
8
+ end