redis_queued_locks 1.16.2 → 1.18.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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/project-overview.md +281 -0
  3. data/.claude/rules/acquirer.md +81 -0
  4. data/.claude/rules/arguments.md +60 -0
  5. data/.claude/rules/git.md +46 -0
  6. data/.claude/rules/logic.md +63 -0
  7. data/.claude/rules/swarm.md +120 -0
  8. data/.claude/rules/tests.md +48 -0
  9. data/.claude/rules/type-checking.md +50 -0
  10. data/.claude/rules/visitors.md +69 -0
  11. data/.rubocop.rbs.yml +31 -0
  12. data/.rubocop.yml +3 -3
  13. data/.ruby-version +1 -1
  14. data/CHANGELOG.md +80 -0
  15. data/CLAUDE.md +36 -0
  16. data/README.md +367 -82
  17. data/Rakefile +31 -2
  18. data/github_ci/.keep +0 -0
  19. data/lib/redis_queued_locks/acquirer/acquire_lock/delay_execution.rb +2 -2
  20. data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rb +6 -3
  21. data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rb +9 -7
  22. data/lib/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rb +20 -10
  23. data/lib/redis_queued_locks/acquirer/acquire_lock/log_visitor.rb +34 -16
  24. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rb +240 -28
  25. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +398 -132
  26. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire/log_visitor.rb +12 -6
  27. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +33 -9
  28. data/lib/redis_queued_locks/acquirer/acquire_lock.rb +73 -24
  29. data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +9 -1
  30. data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +182 -3
  31. data/lib/redis_queued_locks/acquirer/is_locked.rb +19 -2
  32. data/lib/redis_queued_locks/acquirer/is_queued.rb +6 -2
  33. data/lib/redis_queued_locks/acquirer/lock_info.rb +96 -6
  34. data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +8 -4
  35. data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +18 -7
  36. data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +126 -29
  37. data/lib/redis_queued_locks/acquirer/locks.rb +103 -6
  38. data/lib/redis_queued_locks/acquirer/queue_info.rb +38 -9
  39. data/lib/redis_queued_locks/acquirer/queues.rb +28 -4
  40. data/lib/redis_queued_locks/acquirer/release_all_locks.rb +35 -1
  41. data/lib/redis_queued_locks/acquirer/release_lock.rb +40 -9
  42. data/lib/redis_queued_locks/acquirer/release_locks_of.rb +43 -3
  43. data/lib/redis_queued_locks/acquirer/release_read_lock.rb +143 -0
  44. data/lib/redis_queued_locks/acquirer.rb +1 -0
  45. data/lib/redis_queued_locks/client.rb +274 -158
  46. data/lib/redis_queued_locks/config/dsl.rb +4 -10
  47. data/lib/redis_queued_locks/resource.rb +117 -9
  48. data/lib/redis_queued_locks/swarm/flush_zombies.rb +46 -6
  49. data/lib/redis_queued_locks/swarm/supervisor.rb +1 -1
  50. data/lib/redis_queued_locks/swarm/swarm_element/isolated.rb +166 -44
  51. data/lib/redis_queued_locks/swarm/swarm_element/threaded.rb +45 -47
  52. data/lib/redis_queued_locks/swarm/zombie_info.rb +42 -0
  53. data/lib/redis_queued_locks/version.rb +2 -2
  54. data/rbs_collection.lock.yaml +10 -2
  55. data/rbs_collection.yaml +0 -2
  56. data/redis_queued_locks.gemspec +1 -1
  57. data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rbs +2 -1
  58. data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rbs +0 -1
  59. data/sig/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rbs +5 -0
  60. data/sig/redis_queued_locks/acquirer/acquire_lock/log_visitor.rbs +12 -6
  61. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rbs +74 -7
  62. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +4 -2
  63. data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +3 -0
  64. data/sig/redis_queued_locks/acquirer/acquire_lock/yield_with_expire/log_visitor.rbs +4 -2
  65. data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +19 -1
  66. data/sig/redis_queued_locks/acquirer/lock_info.rbs +9 -1
  67. data/sig/redis_queued_locks/acquirer/locks.rbs +10 -1
  68. data/sig/redis_queued_locks/acquirer/queue_info.rbs +2 -4
  69. data/sig/redis_queued_locks/acquirer/queues.rbs +1 -1
  70. data/sig/redis_queued_locks/acquirer/release_lock.rbs +8 -1
  71. data/sig/redis_queued_locks/acquirer/release_read_lock.rbs +44 -0
  72. data/sig/redis_queued_locks/client.rbs +22 -2
  73. data/sig/redis_queued_locks/resource.rbs +9 -2
  74. data/sig/redis_queued_locks/swarm/flush_zombies.rbs +1 -1
  75. data/sig/redis_queued_locks/swarm/swarm_element/isolated.rbs +13 -5
  76. data/sig/redis_queued_locks/swarm/swarm_element/threaded.rbs +6 -6
  77. data/sig/redis_queued_locks/swarm/zombie_info.rbs +5 -0
  78. metadata +17 -5
  79. data/github_ci/ruby3.3.gemfile +0 -17
  80. data/github_ci/ruby3.3.gemfile.lock +0 -216
@@ -11,6 +11,7 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
11
11
  # - 1. clear lock queue: al; related processes released
12
12
  # from the lock aquierment and should retry;
13
13
  # - 2. delete the lock: drop lock key from Redis;
14
+ # - 3. (RW) clear the read lock queue and drop all read locks (the readers registry);
14
15
  # It is safe because the lock obtain logic is transactional and
15
16
  # watches the original lock for changes.
16
17
  #
@@ -71,7 +72,7 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
71
72
  #
72
73
  # @api private
73
74
  # @since 1.0.0
74
- # @version 1.14.0
75
+ # @version 1.18.0
75
76
  # rubocop:disable Metrics/MethodLength
76
77
  def release_lock(
77
78
  redis,
@@ -89,9 +90,15 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
89
90
  )
90
91
  lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
91
92
  lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
93
+ read_lock_key_queue = RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name)
94
+ lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
92
95
 
93
96
  rel_start_time = clock_gettime
94
- fully_release_lock(redis, lock_key, lock_key_queue) => { ok:, result: } # steep:ignore
97
+ # steep:ignore:start
98
+ fully_release_lock(
99
+ redis, lock_name, lock_key, lock_key_queue, read_lock_key_queue, lock_readers_key
100
+ ) => { ok:, result: }
101
+ # steep:ignore:end
95
102
 
96
103
  # @type var ok: bool
97
104
  # @type var result: Hash[Symbol,Symbol]
@@ -131,36 +138,60 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
131
138
 
132
139
  private
133
140
 
134
- # Realease the lock: clear the lock queue and expire the lock.
141
+ # Realease the lock: clear the lock queues and expire the lock (write lock and read locks).
135
142
  #
136
143
  # @param redis [RedisClient]
144
+ # @param lock_name [String]
137
145
  # @param lock_key [String]
138
146
  # @param lock_key_queue [String]
147
+ # @param read_lock_key_queue [String]
148
+ # @param lock_readers_key [String]
139
149
  # @return [Hash<Symbol,Boolean|Hash<Symbol,Symbol>>]
140
150
  # Format: {
141
151
  # ok: true/false,
142
152
  # result: {
143
- # queue: :released/:nothing_to_release,
144
- # lock: :released/:nothing_to_release
153
+ # queue: :released/:nothing_to_release, # (any of the lock queues)
154
+ # lock: :released/:nothing_to_release # (write lock or read locks)
145
155
  # }
146
156
  # }
147
157
  #
148
158
  # @api private
149
159
  # @since 1.0.0
150
- def fully_release_lock(redis, lock_key, lock_key_queue)
151
- # @type var result: [Integer,Integer]
160
+ # @version 1.18.0
161
+ def fully_release_lock(
162
+ redis,
163
+ lock_name,
164
+ lock_key,
165
+ lock_key_queue,
166
+ read_lock_key_queue,
167
+ lock_readers_key
168
+ )
169
+ # @type var result: [Integer,Integer,Integer,Integer]
152
170
  result = redis.with do |rconn|
171
+ # NOTE: (RW) data of read locks (it is a data carrier only: read locks are released via
172
+ # the readers registry, so the data of concurrently obtained read locks expires itself)
173
+ # @type var read_lock_acquirers: Array[String]
174
+ read_lock_acquirers = rconn.call('ZRANGE', lock_readers_key, '0', '-1')
175
+
153
176
  rconn.multi do |transact|
154
177
  transact.call('ZREMRANGEBYSCORE', lock_key_queue, '-inf', '+inf')
155
178
  transact.call('EXPIRE', lock_key, '0')
179
+ transact.call('ZREMRANGEBYSCORE', read_lock_key_queue, '-inf', '+inf')
180
+ transact.call('EXPIRE', lock_readers_key, '0')
181
+ read_lock_acquirers.each do |acquirer_id|
182
+ transact.call(
183
+ 'DEL',
184
+ RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
185
+ )
186
+ end
156
187
  end
157
188
  end
158
189
 
159
190
  {
160
191
  ok: true,
161
192
  result: {
162
- queue: (result[0] != 0) ? :released : :nothing_to_release,
163
- lock: (result[1] != 0) ? :released : :nothing_to_release
193
+ queue: (result[0] != 0 || result[2] != 0) ? :released : :nothing_to_release,
194
+ lock: (result[1] != 0 || result[3] != 0) ? :released : :nothing_to_release
164
195
  }
165
196
  }
166
197
  end
@@ -2,12 +2,16 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.14.0
5
+ # @version 1.18.0
6
+ # rubocop:disable Metrics/ModuleLength
5
7
  module RedisQueuedLocks::Acquirer::ReleaseLocksOf
6
8
  # @since 1.14.0
7
9
  extend RedisQueuedLocks::Utilities
8
10
 
11
+ # rubocop:disable Metrics/ClassLength
9
12
  class << self
10
- # Release all queues and locks that belong to the given host and its associated acquirer.
13
+ # Release all queues and locks (write locks and read locks) that belong to the given host
14
+ # and its associated acquirer.
11
15
  #
12
16
  # @param refused_host_id [String]
13
17
  # A host whose locks and queues should be released.
@@ -84,7 +88,7 @@ module RedisQueuedLocks::Acquirer::ReleaseLocksOf
84
88
  #
85
89
  # @api private
86
90
  # @since 1.14.0
87
- # @version 1.15.0
91
+ # @version 1.18.0
88
92
  # rubocop:disable Metrics/MethodLength
89
93
  def release_locks_of(
90
94
  refused_host_id,
@@ -164,6 +168,7 @@ module RedisQueuedLocks::Acquirer::ReleaseLocksOf
164
168
  #
165
169
  # @api private
166
170
  # @since 1.14.0
171
+ # @version 1.18.0
167
172
  # rubocop:disable Metrics/MethodLength
168
173
  def fully_release_locks_of(
169
174
  refused_host_id,
@@ -203,6 +208,30 @@ module RedisQueuedLocks::Acquirer::ReleaseLocksOf
203
208
  rel_key_cnt += refused_locks.size
204
209
  end
205
210
 
211
+ # Step A (RW): drop read locks of the passed host/acquirer
212
+ # - read locks store acquirer ids only (the host is a part of the acquirer id);
213
+ if RedisQueuedLocks::Resource.host_identifier_from_acquirer(refused_acquirer_id) ==
214
+ refused_host_id
215
+ rconn.scan(
216
+ 'MATCH',
217
+ RedisQueuedLocks::Resource::LOCK_READERS_PATTERN,
218
+ count: lock_scan_size
219
+ ) do |lock_readers_key|
220
+ res = rconn.call('ZREM', lock_readers_key, refused_acquirer_id)
221
+ next if res == 0
222
+
223
+ rel_key_cnt += 1
224
+ # NOTE: drop the read lock data
225
+ rconn.call(
226
+ 'DEL',
227
+ RedisQueuedLocks::Resource.prepare_read_lock_key(
228
+ RedisQueuedLocks::Resource.lock_name_from_readers(lock_readers_key),
229
+ refused_acquirer_id
230
+ )
231
+ )
232
+ end
233
+ end
234
+
206
235
  # Step B: drop passed host/acquirer from lock queues
207
236
  rconn.scan(
208
237
  'MATCH',
@@ -212,10 +241,21 @@ module RedisQueuedLocks::Acquirer::ReleaseLocksOf
212
241
  res = rconn.call('ZREM', lock_queue, refused_acquirer_id)
213
242
  tch_queue_cnt += 1 if res != 0
214
243
  end
244
+
245
+ # Step B (RW): drop passed host/acquirer from read lock queues
246
+ rconn.scan(
247
+ 'MATCH',
248
+ RedisQueuedLocks::Resource::READ_LOCK_QUEUE_PATTERN,
249
+ count: queue_scan_size
250
+ ) do |read_lock_queue|
251
+ res = rconn.call('ZREM', read_lock_queue, refused_acquirer_id)
252
+ tch_queue_cnt += 1 if res != 0
253
+ end
215
254
  end
216
255
 
217
256
  { ok: true, result: { rel_key_cnt:, tch_queue_cnt: } }
218
257
  end
219
258
  end
220
- # rubocop:enable Metrics/MethodLength
259
+ # rubocop:enable Metrics/MethodLength, Metrics/ClassLength
221
260
  end
261
+ # rubocop:enable Metrics/ModuleLength
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 1.18.0
5
+ module RedisQueuedLocks::Acquirer::ReleaseReadLock
6
+ # @since 1.18.0
7
+ extend RedisQueuedLocks::Utilities
8
+
9
+ class << self
10
+ # Release the read lock of the concrete acquirer (other read locks, the write lock and
11
+ # lock requests are not affected): drop the acquirer from the readers registry and drop
12
+ # its read lock data.
13
+ #
14
+ # @param redis [RedisClient]
15
+ # Redis connection client.
16
+ # @param lock_name [String]
17
+ # The lock name whose read lock should be released.
18
+ # @param acquirer_id [String]
19
+ # The read lock acquirer.
20
+ # @param instrumenter [#notify]
21
+ # See RedisQueuedLocks::Instrument::ActiveSupport for example.
22
+ # @param logger [::Logger,#debug]
23
+ # - Logger object used from `configuration` layer (see config['logger']);
24
+ # - See RedisQueuedLocks::Logging::VoidLogger for example;
25
+ # @param instrument [NilClass,Any]
26
+ # - Custom instrumentation data wich will be passed to the instrumenter's payload
27
+ # with :instrument key;
28
+ # @param log_sampling_enabled [Boolean]
29
+ # @param log_sampling_percent [Integer]
30
+ # @param log_sampler [#sampling_happened?,Module<RedisQueuedLocks::Logging::Sampler>]
31
+ # @param log_sample_this [Boolean]
32
+ # @param instr_sampling_enabled [Boolean]
33
+ # - enables <instrumentaion sampling>: only the configured percent
34
+ # of RQL cases will be instrumented;
35
+ # @param instr_sampling_percent [Integer]
36
+ # - the percent of cases that should be instrumented;
37
+ # @param instr_sampler [#sampling_happened?,Module<RedisQueuedLocks::Instrument::Sampler>]
38
+ # - percent-based sampler that decides should be RQL case instrumented or not;
39
+ # @param instr_sample_this [Boolean]
40
+ # - marks the method that everything should be instrumneted
41
+ # despite the enabled instrumentation sampling;
42
+ # @return [Hash<Symbol,Boolean<Hash<Symbol,Numeric|String|Symbol>>]
43
+ # Format: {
44
+ # ok: true,
45
+ # result: {
46
+ # rel_time: Numeric, # <milliseconds>
47
+ # rel_key: String, # lock key
48
+ # rel_acq_id: String, # read lock acquirer
49
+ # lock_res: Symbol # :released or :nothing_to_release
50
+ # }
51
+ # }
52
+ #
53
+ # @api private
54
+ # @since 1.18.0
55
+ # rubocop:disable Metrics/MethodLength
56
+ def release_read_lock(
57
+ redis,
58
+ lock_name,
59
+ acquirer_id,
60
+ instrumenter,
61
+ logger,
62
+ instrument,
63
+ log_sampling_enabled,
64
+ log_sampling_percent,
65
+ log_sampler,
66
+ log_sample_this,
67
+ instr_sampling_enabled,
68
+ instr_sampling_percent,
69
+ instr_sampler,
70
+ instr_sample_this
71
+ )
72
+ lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
73
+ lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
74
+ read_lock_key = RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
75
+
76
+ rel_start_time = clock_gettime
77
+ # steep:ignore:start
78
+ fully_release_read_lock(
79
+ redis, lock_readers_key, read_lock_key, acquirer_id
80
+ ) => { ok:, result: }
81
+ # steep:ignore:end
82
+
83
+ # @type var ok: bool
84
+ # @type var result: Hash[Symbol,Symbol]
85
+
86
+ time_at = Time.now.to_f
87
+ rel_end_time = clock_gettime
88
+ rel_time = ((rel_end_time - rel_start_time) / 1_000.0).ceil(2)
89
+
90
+ instr_sampled = RedisQueuedLocks::Instrument.should_instrument?(
91
+ instr_sampling_enabled,
92
+ instr_sample_this,
93
+ instr_sampling_percent,
94
+ instr_sampler
95
+ )
96
+
97
+ run_non_critical do
98
+ instrumenter.notify('redis_queued_locks.explicit_read_lock_release', {
99
+ lock_key: lock_key,
100
+ acq_id: acquirer_id,
101
+ lock_res: result[:lock],
102
+ rel_time: rel_time,
103
+ at: time_at,
104
+ instrument: instrument
105
+ })
106
+ end if instr_sampled
107
+
108
+ {
109
+ ok: true,
110
+ result: {
111
+ rel_time: rel_time,
112
+ rel_key: lock_key,
113
+ rel_acq_id: acquirer_id,
114
+ lock_res: result[:lock]
115
+ }
116
+ }
117
+ end
118
+ # rubocop:enable Metrics/MethodLength
119
+
120
+ private
121
+
122
+ # @param redis [RedisClient]
123
+ # @param lock_readers_key [String]
124
+ # @param read_lock_key [String]
125
+ # @param acquirer_id [String]
126
+ # @return [Hash<Symbol,Boolean|Hash<Symbol,Symbol>>]
127
+ # Format: { ok: true, result: { lock: :released/:nothing_to_release } }
128
+ #
129
+ # @api private
130
+ # @since 1.18.0
131
+ def fully_release_read_lock(redis, lock_readers_key, read_lock_key, acquirer_id)
132
+ # @type var result: [Integer,Integer]
133
+ result = redis.with do |rconn|
134
+ rconn.multi do |transact|
135
+ transact.call('ZREM', lock_readers_key, acquirer_id)
136
+ transact.call('DEL', read_lock_key)
137
+ end
138
+ end
139
+
140
+ { ok: true, result: { lock: (result[0] != 0) ? :released : :nothing_to_release } }
141
+ end
142
+ end
143
+ end
@@ -6,6 +6,7 @@ module RedisQueuedLocks::Acquirer
6
6
  require_relative 'acquirer/acquire_lock'
7
7
  require_relative 'acquirer/lock_series_poc'
8
8
  require_relative 'acquirer/release_lock'
9
+ require_relative 'acquirer/release_read_lock'
9
10
  require_relative 'acquirer/release_all_locks'
10
11
  require_relative 'acquirer/release_locks_of'
11
12
  require_relative 'acquirer/is_locked'