redis_queued_locks 1.19.0 → 1.21.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 +96 -23
  3. data/.claude/rules/acquirer.md +39 -20
  4. data/.claude/rules/arguments.md +3 -2
  5. data/.claude/rules/git.md +3 -2
  6. data/.claude/rules/logic.md +32 -8
  7. data/.claude/rules/swarm.md +14 -9
  8. data/.claude/rules/tests.md +12 -3
  9. data/.claude/rules/type-checking.md +2 -1
  10. data/.claude/rules/visitors.md +10 -11
  11. data/.claude/skills/rql-code-review/SKILL.md +386 -0
  12. data/.claude/skills/rql-code-review/templates/report.en.md +31 -0
  13. data/.claude/skills/rql-code-review/templates/report.ru.md +31 -0
  14. data/CHANGELOG.md +194 -0
  15. data/CLAUDE.md +6 -3
  16. data/README.md +502 -92
  17. data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rb +1 -0
  18. data/lib/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rb +42 -0
  19. data/lib/redis_queued_locks/acquirer/acquire_lock/log_visitor.rb +45 -0
  20. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rb +1 -0
  21. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +695 -128
  22. data/lib/redis_queued_locks/acquirer/acquire_lock/with_acq_timeout.rb +2 -2
  23. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire/log_visitor.rb +1 -0
  24. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +8 -6
  25. data/lib/redis_queued_locks/acquirer/acquire_lock.rb +134 -26
  26. data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +229 -24
  27. data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +189 -18
  28. data/lib/redis_queued_locks/acquirer/is_locked.rb +0 -1
  29. data/lib/redis_queued_locks/acquirer/is_queued.rb +0 -1
  30. data/lib/redis_queued_locks/acquirer/keys.rb +151 -7
  31. data/lib/redis_queued_locks/acquirer/lock_info.rb +39 -41
  32. data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +3 -2
  33. data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +5 -4
  34. data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +79 -20
  35. data/lib/redis_queued_locks/acquirer/locks.rb +212 -83
  36. data/lib/redis_queued_locks/acquirer/queue_info.rb +11 -18
  37. data/lib/redis_queued_locks/acquirer/queues.rb +123 -50
  38. data/lib/redis_queued_locks/acquirer/reindex_locks.rb +253 -0
  39. data/lib/redis_queued_locks/acquirer/release_all_locks.rb +177 -61
  40. data/lib/redis_queued_locks/acquirer/release_lock.rb +84 -25
  41. data/lib/redis_queued_locks/acquirer/release_locks_of.rb +274 -70
  42. data/lib/redis_queued_locks/acquirer/release_read_lock.rb +22 -3
  43. data/lib/redis_queued_locks/acquirer.rb +1 -0
  44. data/lib/redis_queued_locks/client.rb +164 -36
  45. data/lib/redis_queued_locks/config.rb +16 -13
  46. data/lib/redis_queued_locks/errors.rb +34 -4
  47. data/lib/redis_queued_locks/instrument/sampler.rb +4 -7
  48. data/lib/redis_queued_locks/logging/sampler.rb +4 -8
  49. data/lib/redis_queued_locks/resource.rb +113 -6
  50. data/lib/redis_queued_locks/swarm/flush_zombies.rb +333 -61
  51. data/lib/redis_queued_locks/swarm/redis_client_builder.rb +8 -4
  52. data/lib/redis_queued_locks/swarm/swarm_element/threaded.rb +3 -2
  53. data/lib/redis_queued_locks/swarm/zombie_info.rb +203 -53
  54. data/lib/redis_queued_locks/swarm.rb +97 -5
  55. data/lib/redis_queued_locks/utilities.rb +1 -1
  56. data/lib/redis_queued_locks/version.rb +2 -2
  57. data/sig/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rbs +14 -0
  58. data/sig/redis_queued_locks/acquirer/acquire_lock/log_visitor.rbs +13 -0
  59. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +107 -1
  60. data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +3 -3
  61. data/sig/redis_queued_locks/acquirer/acquire_lock.rbs +3 -1
  62. data/sig/redis_queued_locks/acquirer/clear_dead_requests.rbs +19 -1
  63. data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +20 -4
  64. data/sig/redis_queued_locks/acquirer/keys.rbs +11 -1
  65. data/sig/redis_queued_locks/acquirer/locks.rbs +15 -6
  66. data/sig/redis_queued_locks/acquirer/queues.rbs +9 -2
  67. data/sig/redis_queued_locks/acquirer/reindex_locks.rbs +52 -0
  68. data/sig/redis_queued_locks/acquirer/release_all_locks.rbs +8 -1
  69. data/sig/redis_queued_locks/acquirer/release_lock.rbs +6 -3
  70. data/sig/redis_queued_locks/acquirer/release_locks_of.rbs +20 -1
  71. data/sig/redis_queued_locks/acquirer/release_read_lock.rbs +1 -1
  72. data/sig/redis_queued_locks/client.rbs +21 -2
  73. data/sig/redis_queued_locks/errors.rbs +5 -2
  74. data/sig/redis_queued_locks/instrument/sampler.rbs +0 -2
  75. data/sig/redis_queued_locks/logging/sampler.rbs +0 -2
  76. data/sig/redis_queued_locks/resource.rbs +7 -1
  77. data/sig/redis_queued_locks/swarm/flush_zombies.rbs +51 -1
  78. data/sig/redis_queued_locks/swarm/zombie_info.rbs +58 -7
  79. data/sig/vendor/redis_client.rbs +2 -0
  80. metadata +6 -1
@@ -2,20 +2,29 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
+ # @version 1.20.0
6
+ # rubocop:disable Metrics/ModuleLength
5
7
  module RedisQueuedLocks::Acquirer::ReleaseAllLocks
6
8
  # @since 1.0.0
7
9
  extend RedisQueuedLocks::Utilities
8
10
 
11
+ # rubocop:disable Metrics/ClassLength
9
12
  class << self
10
- # Release all locks:
11
- # - 1. clear all lock queus: drop them all from Redis database by the lock queue pattern;
12
- # - 2. delete all locks: drop lock keys from Redis by the lock key pattern;
13
- # - 3. (RW) clear all read lock queues and drop all read locks (readers registries and data);
13
+ # Release all locks: delete the keys of all lock families (write locks, lock queues,
14
+ # readers registries, read lock queues and read lock data):
15
+ # - the disabled lock index: the keys found in one SCAN pass, with one `DEL` per batch of
16
+ # keys;
17
+ # - the enabled lock index: the keys of the locks of the lock index (one ZSCAN pass), their
18
+ # lock names are dropped from the lock index, with one transaction (`DEL` + `ZREM`) per
19
+ # batch of lock names;
14
20
  #
15
21
  # @param redis [RedisClient]
16
22
  # Redis connection client.
17
23
  # @param batch_size [Integer]
18
- # The number of lock keys that should be released in a time.
24
+ # The number of lock keys (the disabled lock index) or locks (lock names, the enabled lock
25
+ # index) that should be released in a time.
26
+ # @param lock_index_enabled [Boolean]
27
+ # Find locks via the lock index instead of the keyspace SCAN.
19
28
  # @param logger [::Logger,#debug]
20
29
  # - Logger object used from `configuration` layer (see config['logger']);
21
30
  # - See RedisQueuedLocks::Logging::VoidLogger for example;
@@ -30,7 +39,7 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
30
39
  # - works in tandem with <config['log_sampling_percent']> and <config['log_sampler']>;
31
40
  # @param log_sampling_percent [Integer]
32
41
  # - the percent of cases that should be logged;
33
- # - take an effect when <config['log_sampling_enalbed']> is true;
42
+ # - take an effect when <config['log_sampling_enabled']> is true;
34
43
  # - works in tandem with <config['log_sampling_enabled']> and <config['log_sampler']> configs;
35
44
  # @param log_sampler [#sampling_happened?,Module<RedisQueuedLocks::Logging::Sampler>]
36
45
  # - percent-based log sampler that decides should be RQL case logged or not;
@@ -51,7 +60,7 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
51
60
  # - works in tandem with <config['instr_sampling_percent']> and <config['instr_sampler']>;
52
61
  # @param instr_sampling_percent [Integer]
53
62
  # - the percent of cases that should be instrumented;
54
- # - take an effect when <config['instr_sampling_enalbed']> is true;
63
+ # - take an effect when <config['instr_sampling_enabled']> is true;
55
64
  # - works in tandem with <config['instr_sampling_enabled']>
56
65
  # and <config['instr_sampler']> configs;
57
66
  # @param instr_sampler [#sampling_happened?,Module<RedisQueuedLocks::Instrument::Sampler>]
@@ -72,10 +81,12 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
72
81
  #
73
82
  # @api private
74
83
  # @since 1.0.0
75
- # @version 1.18.0
84
+ # @version 1.21.0
85
+ # rubocop:disable Metrics/MethodLength
76
86
  def release_all_locks(
77
87
  redis,
78
88
  batch_size,
89
+ lock_index_enabled,
79
90
  logger,
80
91
  instrumenter,
81
92
  instrument,
@@ -88,8 +99,19 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
88
99
  instr_sampler,
89
100
  instr_sample_this
90
101
  )
102
+ # NOTE: (arguments) the same ranges as the config keys: zero/negative sizes fail SCAN/ZSCAN
103
+ # (`COUNT 0`) and the batching (`each_slice(0)`);
104
+ if !batch_size.is_a?(::Integer) || batch_size <= 0
105
+ raise(
106
+ RedisQueuedLocks::ArgumentError,
107
+ "`:batch_size` argument should be a positive Integer, got #{batch_size.inspect}."
108
+ )
109
+ end
110
+
91
111
  rel_start_time = clock_gettime
92
- fully_release_all_locks(redis, batch_size) => { ok:, result: } # steep:ignore
112
+ # steep:ignore:start
113
+ fully_release_all_locks(redis, batch_size, lock_index_enabled) => { ok:, result: }
114
+ # steep:ignore:end
93
115
 
94
116
  # @type var ok: bool
95
117
  # @type var result: Hash[Symbol,Integer]
@@ -98,6 +120,12 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
98
120
  rel_end_time = clock_gettime
99
121
  rel_time = ((rel_end_time - rel_start_time) / 1_000.0).ceil(2)
100
122
 
123
+ log_sampled = RedisQueuedLocks::Logging.should_log?(
124
+ log_sampling_enabled,
125
+ log_sample_this,
126
+ log_sampling_percent,
127
+ log_sampler
128
+ )
101
129
  instr_sampled = RedisQueuedLocks::Instrument.should_instrument?(
102
130
  instr_sampling_enabled,
103
131
  instr_sample_this,
@@ -105,11 +133,21 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
105
133
  instr_sampler
106
134
  )
107
135
 
136
+ run_non_critical do
137
+ logger.debug do
138
+ "[redis_queued_locks.explicit_all_locks_release] " \
139
+ "at => #{time_at} " \
140
+ "rel_time => #{rel_time} " \
141
+ "rel_key_cnt => #{result[:rel_key_cnt]}"
142
+ end
143
+ end if log_sampled
144
+
108
145
  run_non_critical do
109
146
  instrumenter.notify('redis_queued_locks.explicit_all_locks_release', {
110
147
  at: time_at,
111
148
  rel_time: rel_time,
112
- rel_key_cnt: result[:rel_key_cnt]
149
+ rel_key_cnt: result[:rel_key_cnt],
150
+ instrument: instrument
113
151
  })
114
152
  end if instr_sampled
115
153
 
@@ -118,77 +156,155 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
118
156
  result: { rel_key_cnt: result[:rel_key_cnt], rel_time: rel_time }
119
157
  }
120
158
  end
159
+ # rubocop:enable Metrics/MethodLength
121
160
 
122
161
  private
123
162
 
124
- # Release all locks: clear all lock queus and expire all locks.
163
+ # Release all locks:
164
+ # - the disabled lock index: one SCAN pass over the RQL keys (`KEY_PATTERN`); the keys of all
165
+ # lock families are deleted with one `DEL` per batch of `batch_size` keys (the keys are
166
+ # deleted during the scan: no list of all keys is kept in memory);
167
+ # - the enabled lock index: one ZSCAN pass over the lock index (`LOCK_INDEX_KEY`: lock names,
168
+ # the keyspace is never scanned); the keys of all lock families of each batch of
169
+ # `batch_size` lock names are deleted with one `DEL` in one transaction with the `ZREM` of
170
+ # the lock names from the lock index (two round trips per batch: the read lock acquirers,
171
+ # then the transaction):
172
+ # - expired lock names are dropped from the lock index first (all their keys are expired);
173
+ # - lock names of the lock requests queued (or the locks obtained) after the transaction of
174
+ # their batch are indexed again by the lock acquirement;
125
175
  #
126
176
  # @param redis [RedisClient]
127
177
  # @param batch_size [Integer]
178
+ # SCAN count and the number of keys in one `DEL` (the disabled lock index) or ZSCAN count and
179
+ # the number of lock names in one transaction (the enabled lock index).
180
+ # @param lock_index_enabled [Boolean]
128
181
  # @return [Hash<Symbol,Boolean|Hash<Symbol,Integer>>]
129
182
  # - Exmaple: { ok: true, result: { rel_key_cnt: 12345 } }
130
183
  #
131
184
  # @api private
132
185
  # @since 1.0.0
133
- # @version 1.18.0
134
- # rubocop:disable Metrics/MethodLength
135
- def fully_release_all_locks(redis, batch_size)
136
- result = redis.with do |rconn|
137
- rconn.pipelined do |pipeline|
138
- # Step A: release all queus and their related locks
139
- rconn.scan(
140
- 'MATCH',
141
- RedisQueuedLocks::Resource::LOCK_QUEUE_PATTERN,
142
- count: batch_size
143
- ) do |lock_queue|
144
- # TODO: reduce unnecessary iterations
145
- pipeline.call('EXPIRE', lock_queue, '0')
146
- end
186
+ # @version 1.20.0
187
+ def fully_release_all_locks(redis, batch_size, lock_index_enabled)
188
+ rel_key_cnt = 0
147
189
 
148
- # Step B: release all locks
149
- rconn.scan(
150
- 'MATCH',
151
- RedisQueuedLocks::Resource::LOCK_PATTERN,
152
- count: batch_size
153
- ) do |lock_key|
154
- # TODO: reduce unnecessary iterations
155
- pipeline.call('EXPIRE', lock_key, '0')
156
- end
190
+ redis.with do |rconn|
191
+ if lock_index_enabled
192
+ # Step 1 (lock index): drop the expired lock names from the lock index
193
+ # (all their keys are expired);
194
+ rconn.call(
195
+ 'ZREMRANGEBYSCORE', RedisQueuedLocks::Resource::LOCK_INDEX_KEY,
196
+ '-inf', RedisQueuedLocks::Resource.lock_index_time - 1
197
+ )
157
198
 
158
- # Step C (RW): release all read lock queues
159
- rconn.scan(
160
- 'MATCH',
161
- RedisQueuedLocks::Resource::READ_LOCK_QUEUE_PATTERN,
162
- count: batch_size
163
- ) do |read_lock_queue|
164
- # TODO: reduce unnecessary iterations
165
- pipeline.call('EXPIRE', read_lock_queue, '0')
199
+ # Step 2 (lock index): release the locks of the lock index in batches of lock names
200
+ each_lock_name_batch(rconn, batch_size) do |lock_names|
201
+ rel_key_cnt += release_locks(rconn, lock_names)
166
202
  end
203
+ else
204
+ # Step 1 (keyspace): delete the RQL keys of the lock families during one SCAN pass
205
+ # @type var lock_keys: Array[String]
206
+ lock_keys = []
167
207
 
168
- # Step D (RW): release all read locks
169
- rconn.scan(
170
- 'MATCH',
171
- RedisQueuedLocks::Resource::LOCK_READERS_PATTERN,
172
- count: batch_size
173
- ) do |lock_readers|
174
- # TODO: reduce unnecessary iterations
175
- pipeline.call('EXPIRE', lock_readers, '0')
176
- end
208
+ rconn.scan('MATCH', RedisQueuedLocks::Resource::KEY_PATTERN, count: batch_size) do |key|
209
+ # NOTE: lock families only (KEY_PATTERN matches any `rql:lock*` key)
210
+ next if RedisQueuedLocks::Resource.lock_key_family(key) == nil
211
+
212
+ lock_keys << key
213
+ next if lock_keys.size < batch_size
177
214
 
178
- # Step E (RW): release all read lock data
179
- rconn.scan(
180
- 'MATCH',
181
- RedisQueuedLocks::Resource::READ_LOCK_PATTERN,
182
- count: batch_size
183
- ) do |read_lock|
184
- # TODO: reduce unnecessary iterations
185
- pipeline.call('EXPIRE', read_lock, '0')
215
+ rel_key_cnt += rconn.call('DEL', *lock_keys)
216
+ lock_keys.clear
186
217
  end
218
+
219
+ rel_key_cnt += rconn.call('DEL', *lock_keys) unless lock_keys.empty?
187
220
  end
188
221
  end
189
222
 
190
- { ok: true, result: { rel_key_cnt: result.sum } }
223
+ { ok: true, result: { rel_key_cnt: } }
224
+ end
225
+
226
+ # (lock index) Releases the locks of the given batch of lock names: deletes all their keys and
227
+ # drops the lock names from the lock index in one transaction.
228
+ # - (RW) read lock data keys are found via the readers registries (one more round trip
229
+ # before the transaction); the data of a read lock obtained between the registry read and
230
+ # the transaction stays till its ttl (the read lock itself is released with the registry);
231
+ #
232
+ # @param redis_client [RedisClient]
233
+ # @param lock_names [Array<String>]
234
+ # @return [Integer] The number of the deleted keys.
235
+ #
236
+ # @api private
237
+ # @since 1.20.0
238
+ def release_locks(redis_client, lock_names)
239
+ # Step A: (RW) read lock acquirers of the readers registries (their read lock data keys)
240
+ # NOTE: (pipeline layout) one reply per lock name in the batch order
241
+ # @type var read_lock_acquirers: Array[Array[String]]
242
+ read_lock_acquirers = redis_client.pipelined do |pipeline|
243
+ lock_names.each do |lock_name|
244
+ pipeline.call(
245
+ 'ZRANGE', RedisQueuedLocks::Resource.prepare_lock_readers(lock_name), '0', '-1'
246
+ )
247
+ end
248
+ end
249
+
250
+ # @type var lock_keys: Array[String]
251
+ lock_keys = []
252
+ lock_names.each_with_index do |lock_name, index|
253
+ lock_keys << RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
254
+ lock_keys << RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
255
+ lock_keys << RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
256
+ lock_keys << RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name)
257
+ read_lock_acquirers[index].each do |acquirer_id|
258
+ lock_keys << RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
259
+ end
260
+ end
261
+
262
+ # Step B: delete the keys and drop the lock names from the lock index in one transaction
263
+ # (a lock obtained after the transaction indexes its lock name again);
264
+ # NOTE: (transaction layout) [0] DEL (the number of the deleted keys), [1] ZREM (not read)
265
+ # @type var released: [Integer, Integer]
266
+ released = redis_client.multi do |transact|
267
+ transact.call('DEL', *lock_keys)
268
+ transact.call('ZREM', RedisQueuedLocks::Resource::LOCK_INDEX_KEY, *lock_names)
269
+ end
270
+ released[0]
271
+ end
272
+
273
+ # Iterates over the lock names of the lock index (one ZSCAN pass) in batches of `scan_size`
274
+ # lock names (batches are collected across ZSCAN pages). Expired lock names (not dropped from
275
+ # the index yet) are skipped: all their keys are expired.
276
+ #
277
+ # NOTE: each operation module iterates over the lock index by itself (duplicated on purpose:
278
+ # operation modules are independent of each other, see `Locks.each_lock_name_batch`);
279
+ #
280
+ # @param redis_client [RedisClient]
281
+ # @param scan_size [Integer] ZSCAN count and the number of lock names in one batch.
282
+ # @yield [lock_names]
283
+ # @yieldparam lock_names [Array<String>] The batch of lock names (reused between batches).
284
+ # @return [void]
285
+ #
286
+ # @api private
287
+ # @since 1.20.0
288
+ def each_lock_name_batch(redis_client, scan_size)
289
+ lock_index_time = RedisQueuedLocks::Resource.lock_index_time
290
+ # @type var lock_names: Array[String]
291
+ lock_names = []
292
+
293
+ redis_client.zscan(
294
+ RedisQueuedLocks::Resource::LOCK_INDEX_KEY, count: scan_size
295
+ ) do |lock_name, lock_index_score|
296
+ next if lock_index_score.to_f < lock_index_time
297
+
298
+ lock_names << lock_name
299
+ next if lock_names.size < scan_size
300
+
301
+ yield(lock_names)
302
+ lock_names.clear
303
+ end
304
+
305
+ yield(lock_names) unless lock_names.empty?
191
306
  end
192
- # rubocop:enable Metrics/MethodLength
193
307
  end
308
+ # rubocop:enable Metrics/ClassLength
194
309
  end
310
+ # rubocop:enable Metrics/ModuleLength
@@ -2,16 +2,20 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
+ # @version 1.21.0
6
+ # rubocop:disable Metrics/ModuleLength
5
7
  module RedisQueuedLocks::Acquirer::ReleaseLock
6
8
  # @since 1.0.0
7
9
  extend RedisQueuedLocks::Utilities
8
10
 
11
+ # rubocop:disable Metrics/ClassLength
9
12
  class << self
10
13
  # Release the concrete lock:
11
14
  # - 1. clear lock queue: al; related processes released
12
15
  # from the lock aquierment and should retry;
13
16
  # - 2. delete the lock: drop lock key from Redis;
14
17
  # - 3. (RW) clear the read lock queue and drop all read locks (the readers registry);
18
+ # - 4. (lock index) drop the lock name from the lock index (the enabled lock index only);
15
19
  # It is safe because the lock obtain logic is transactional and
16
20
  # watches the original lock for changes.
17
21
  #
@@ -19,18 +23,23 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
19
23
  # Redis connection client.
20
24
  # @param lock_name [String]
21
25
  # The lock name that should be released.
22
- # @param instrumenter [#notify]
23
- # See RedisQueuedLocks::Instrument::ActiveSupport for example.
26
+ # @param lock_index_enabled [Boolean]
27
+ # Drop the lock name from the lock index (the enabled lock index).
24
28
  # @param logger [::Logger,#debug]
25
29
  # - Logger object used from `configuration` layer (see config['logger']);
26
30
  # - See RedisQueuedLocks::Logging::VoidLogger for example;
31
+ # @param instrumenter [#notify]
32
+ # See RedisQueuedLocks::Instrument::ActiveSupport for example.
33
+ # @param instrument [NilClass,Any]
34
+ # - Custom instrumentation data wich will be passed to the instrumenter's payload
35
+ # with :instrument key;
27
36
  # @param log_sampling_enabled [Boolean]
28
37
  # - enables <log sampling>: only the configured percent of RQL cases will be logged;
29
38
  # - disabled by default;
30
39
  # - works in tandem with <config['log_sampling_percent']> and <config['log_sampler']>;
31
40
  # @param log_sampling_percent [Integer]
32
41
  # - the percent of cases that should be logged;
33
- # - take an effect when <config['log_sampling_enalbed']> is true;
42
+ # - take an effect when <config['log_sampling_enabled']> is true;
34
43
  # - works in tandem with <config['log_sampling_enabled']> and <config['log_sampler']> configs;
35
44
  # @param log_sampler [#sampling_happened?,Module<RedisQueuedLocks::Logging::Sampler>]
36
45
  # - percent-based log sampler that decides should be RQL case logged or not;
@@ -51,7 +60,7 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
51
60
  # - works in tandem with <config['instr_sampling_percent']> and <config['instr_sampler']>;
52
61
  # @param instr_sampling_percent [Integer]
53
62
  # - the percent of cases that should be instrumented;
54
- # - take an effect when <config['instr_sampling_enalbed']> is true;
63
+ # - take an effect when <config['instr_sampling_enabled']> is true;
55
64
  # - works in tandem with <config['instr_sampling_enabled']>
56
65
  # and <config['instr_sampler']> configs;
57
66
  # @param instr_sampler [#sampling_happened?,Module<RedisQueuedLocks::Instrument::Sampler>]
@@ -72,13 +81,15 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
72
81
  #
73
82
  # @api private
74
83
  # @since 1.0.0
75
- # @version 1.18.0
84
+ # @version 1.21.0
76
85
  # rubocop:disable Metrics/MethodLength
77
86
  def release_lock(
78
87
  redis,
79
88
  lock_name,
80
- instrumenter,
89
+ lock_index_enabled,
81
90
  logger,
91
+ instrumenter,
92
+ instrument,
82
93
  log_sampling_enabled,
83
94
  log_sampling_percent,
84
95
  log_sampler,
@@ -96,7 +107,8 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
96
107
  rel_start_time = clock_gettime
97
108
  # steep:ignore:start
98
109
  fully_release_lock(
99
- redis, lock_name, lock_key, lock_key_queue, read_lock_key_queue, lock_readers_key
110
+ redis, lock_name, lock_key, lock_key_queue, read_lock_key_queue, lock_readers_key,
111
+ lock_index_enabled
100
112
  ) => { ok:, result: }
101
113
  # steep:ignore:end
102
114
 
@@ -107,6 +119,12 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
107
119
  rel_end_time = clock_gettime
108
120
  rel_time = ((rel_end_time - rel_start_time) / 1_000.0).ceil(2)
109
121
 
122
+ log_sampled = RedisQueuedLocks::Logging.should_log?(
123
+ log_sampling_enabled,
124
+ log_sample_this,
125
+ log_sampling_percent,
126
+ log_sampler
127
+ )
110
128
  instr_sampled = RedisQueuedLocks::Instrument.should_instrument?(
111
129
  instr_sampling_enabled,
112
130
  instr_sample_this,
@@ -114,12 +132,23 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
114
132
  instr_sampler
115
133
  )
116
134
 
135
+ run_non_critical do
136
+ logger.debug do
137
+ "[redis_queued_locks.explicit_lock_release] " \
138
+ "lock_key => '#{lock_key}' " \
139
+ "lock_key_queue => '#{lock_key_queue}' " \
140
+ "rel_time => #{rel_time} " \
141
+ "at => #{time_at}"
142
+ end
143
+ end if log_sampled
144
+
117
145
  run_non_critical do
118
146
  instrumenter.notify('redis_queued_locks.explicit_lock_release', {
119
147
  lock_key: lock_key,
120
148
  lock_key_queue: lock_key_queue,
121
149
  rel_time: rel_time,
122
- at: time_at
150
+ at: time_at,
151
+ instrument: instrument
123
152
  })
124
153
  end if instr_sampled
125
154
 
@@ -138,7 +167,8 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
138
167
 
139
168
  private
140
169
 
141
- # Realease the lock: clear the lock queues and expire the lock (write lock and read locks).
170
+ # Realease the lock: clear the lock queues and expire the lock (write lock and read locks),
171
+ # drop the lock name from the lock index (the enabled lock index).
142
172
  #
143
173
  # @param redis [RedisClient]
144
174
  # @param lock_name [String]
@@ -146,6 +176,7 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
146
176
  # @param lock_key_queue [String]
147
177
  # @param read_lock_key_queue [String]
148
178
  # @param lock_readers_key [String]
179
+ # @param lock_index_enabled [Boolean]
149
180
  # @return [Hash<Symbol,Boolean|Hash<Symbol,Symbol>>]
150
181
  # Format: {
151
182
  # ok: true/false,
@@ -157,43 +188,71 @@ module RedisQueuedLocks::Acquirer::ReleaseLock
157
188
  #
158
189
  # @api private
159
190
  # @since 1.0.0
160
- # @version 1.18.0
191
+ # @version 1.20.0
192
+ # rubocop:disable Metrics/MethodLength
161
193
  def fully_release_lock(
162
194
  redis,
163
195
  lock_name,
164
196
  lock_key,
165
197
  lock_key_queue,
166
198
  read_lock_key_queue,
167
- lock_readers_key
199
+ lock_readers_key,
200
+ lock_index_enabled
168
201
  )
169
- # @type var result: [Integer,Integer,Integer,Integer]
202
+ # @type var result: [Array[String],Integer,Integer,Integer,Integer]
170
203
  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
-
176
- rconn.multi do |transact|
204
+ # Step 1: release the lock in one transaction (one round trip):
205
+ # - (RW) the readers registry is read in the same transaction: the snapshot of all
206
+ # released read locks (their data is dropped in Step 2);
207
+ # - (lock index) the enabled lock index: all the indexed keys of the lock are dropped,
208
+ # so the lock name is dropped from the lock index in the same transaction (a lock
209
+ # request queued or a lock obtained after this transaction indexes the lock name again);
210
+ # NOTE: (transaction layout) replies are read by their positions:
211
+ # [0] ZRANGE (released read locks), [1] write lock queue, [2] write lock,
212
+ # [3] read lock queue, [4] readers registry, [5] lock index (the enabled lock index
213
+ # only, the last command: not read);
214
+ released = rconn.multi do |transact|
215
+ transact.call('ZRANGE', lock_readers_key, '0', '-1')
177
216
  transact.call('ZREMRANGEBYSCORE', lock_key_queue, '-inf', '+inf')
178
217
  transact.call('EXPIRE', lock_key, '0')
179
218
  transact.call('ZREMRANGEBYSCORE', read_lock_key_queue, '-inf', '+inf')
180
219
  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
- )
220
+ if lock_index_enabled
221
+ transact.call('ZREM', RedisQueuedLocks::Resource::LOCK_INDEX_KEY, lock_name)
186
222
  end
187
223
  end
224
+
225
+ # Step 2: (RW) drop the data of the released read locks (only when read locks existed)
226
+ # NOTE:
227
+ # - the read lock data is a data carrier only: read locks are released via the readers
228
+ # registry (Step 1);
229
+ # - (trade-off) a reader of the snapshot that obtains a new read lock between Step 1 and
230
+ # Step 2 loses the data of the new read lock (meta, timestamps); the read lock itself is
231
+ # not affected (the readers registry is the source of truth);
232
+ # @type var read_lock_acquirers: Array[String]
233
+ read_lock_acquirers = released[0]
234
+ unless read_lock_acquirers.empty?
235
+ rconn.call(
236
+ 'DEL',
237
+ *read_lock_acquirers.map do |acquirer_id|
238
+ RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
239
+ end
240
+ )
241
+ end
242
+
243
+ released
188
244
  end
189
245
 
190
246
  {
191
247
  ok: true,
192
248
  result: {
193
- queue: (result[0] != 0 || result[2] != 0) ? :released : :nothing_to_release,
194
- lock: (result[1] != 0 || result[3] != 0) ? :released : :nothing_to_release
249
+ queue: (result[1] != 0 || result[3] != 0) ? :released : :nothing_to_release,
250
+ lock: (result[2] != 0 || result[4] != 0) ? :released : :nothing_to_release
195
251
  }
196
252
  }
197
253
  end
254
+ # rubocop:enable Metrics/MethodLength
198
255
  end
256
+ # rubocop:enable Metrics/ClassLength
199
257
  end
258
+ # rubocop:enable Metrics/ModuleLength