redis_queued_locks 1.19.0 → 1.20.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/project-overview.md +62 -11
  3. data/.claude/rules/acquirer.md +29 -11
  4. data/.claude/rules/arguments.md +3 -2
  5. data/.claude/rules/git.md +3 -2
  6. data/.claude/rules/logic.md +4 -3
  7. data/.claude/rules/swarm.md +12 -7
  8. data/.claude/rules/tests.md +6 -0
  9. data/.claude/rules/type-checking.md +2 -1
  10. data/.claude/rules/visitors.md +1 -1
  11. data/.claude/skills/rql-code-review/SKILL.md +372 -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 +68 -0
  15. data/CLAUDE.md +4 -2
  16. data/README.md +257 -22
  17. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +684 -128
  18. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +3 -1
  19. data/lib/redis_queued_locks/acquirer/acquire_lock.rb +24 -3
  20. data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +145 -18
  21. data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +71 -10
  22. data/lib/redis_queued_locks/acquirer/keys.rb +142 -7
  23. data/lib/redis_queued_locks/acquirer/lock_info.rb +10 -4
  24. data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +26 -7
  25. data/lib/redis_queued_locks/acquirer/locks.rb +195 -83
  26. data/lib/redis_queued_locks/acquirer/queue_info.rb +12 -18
  27. data/lib/redis_queued_locks/acquirer/queues.rb +114 -50
  28. data/lib/redis_queued_locks/acquirer/reindex_locks.rb +140 -0
  29. data/lib/redis_queued_locks/acquirer/release_all_locks.rb +147 -58
  30. data/lib/redis_queued_locks/acquirer/release_lock.rb +52 -19
  31. data/lib/redis_queued_locks/acquirer/release_locks_of.rb +235 -66
  32. data/lib/redis_queued_locks/acquirer.rb +1 -0
  33. data/lib/redis_queued_locks/client.rb +97 -11
  34. data/lib/redis_queued_locks/config.rb +5 -1
  35. data/lib/redis_queued_locks/resource.rb +99 -2
  36. data/lib/redis_queued_locks/swarm/flush_zombies.rb +332 -61
  37. data/lib/redis_queued_locks/swarm/zombie_info.rb +203 -53
  38. data/lib/redis_queued_locks/swarm.rb +15 -4
  39. data/lib/redis_queued_locks/version.rb +2 -2
  40. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +107 -1
  41. data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +1 -1
  42. data/sig/redis_queued_locks/acquirer/acquire_lock.rbs +2 -0
  43. data/sig/redis_queued_locks/acquirer/clear_dead_requests.rbs +10 -1
  44. data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +5 -2
  45. data/sig/redis_queued_locks/acquirer/keys.rbs +11 -1
  46. data/sig/redis_queued_locks/acquirer/locks.rbs +15 -6
  47. data/sig/redis_queued_locks/acquirer/queues.rbs +9 -2
  48. data/sig/redis_queued_locks/acquirer/reindex_locks.rbs +31 -0
  49. data/sig/redis_queued_locks/acquirer/release_all_locks.rbs +8 -1
  50. data/sig/redis_queued_locks/acquirer/release_lock.rbs +3 -1
  51. data/sig/redis_queued_locks/acquirer/release_locks_of.rbs +20 -1
  52. data/sig/redis_queued_locks/client.rbs +8 -0
  53. data/sig/redis_queued_locks/resource.rbs +6 -0
  54. data/sig/redis_queued_locks/swarm/flush_zombies.rbs +51 -1
  55. data/sig/redis_queued_locks/swarm/zombie_info.rbs +58 -7
  56. data/sig/vendor/redis_client.rbs +1 -0
  57. metadata +6 -1
@@ -14,7 +14,7 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
14
14
  class << self
15
15
  # @api private
16
16
  # @since 1.16.0
17
- # @version 1.18.0
17
+ # @version 1.20.0
18
18
  def lock_series_poc( # steep:ignore
19
19
  redis,
20
20
  lock_names,
@@ -25,6 +25,8 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
25
25
  ractor_id:,
26
26
  ttl:,
27
27
  queue_ttl:,
28
+ lock_index_enabled:,
29
+ lock_index_grace_period:,
28
30
  timeout:,
29
31
  timed:,
30
32
  retry_count:,
@@ -155,6 +157,8 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
155
157
  ractor_id:,
156
158
  ttl: lock_operation_options[:ttl],
157
159
  queue_ttl:,
160
+ lock_index_enabled:,
161
+ lock_index_grace_period:,
158
162
  timeout:,
159
163
  timed:,
160
164
  retry_count:,
@@ -357,7 +361,11 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
357
361
  end
358
362
 
359
363
  # Releases write locks of the given acquirer (compare-and-delete: the lock can be expired
360
- # and obtained by another acquirer when the series logic exceeds the lock ttl).
364
+ # and obtained by another acquirer when the series logic exceeds the lock ttl) in one
365
+ # transaction for the whole series (three round trips: WATCH, lock owners, MULTI/EXEC):
366
+ # - an aborted transaction (one of the watched locks is changed) is retried with the locks
367
+ # that were still owned by the acquirer at the last check: the locks of other acquirers
368
+ # are neither deleted nor watched, so the retries converge;
361
369
  #
362
370
  # @param redis [RedisClient]
363
371
  # @param lock_names [Array<String>]
@@ -366,17 +374,28 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
366
374
  #
367
375
  # @api private
368
376
  # @since 1.18.0
377
+ # @version 1.20.0
369
378
  def release_write_lock_series(redis, lock_names, acquirer_id) # steep:ignore
370
379
  return if lock_names.empty?
371
380
 
381
+ own_lock_keys = lock_names.map do |lock_name|
382
+ RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
383
+ end
384
+
372
385
  redis.with do |conn|
373
- lock_names.each do |lock_name|
374
- lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
375
- conn.multi(watch: [lock_key]) do |transaction|
376
- if conn.call('HGET', lock_key, 'acq_id') == acquirer_id
377
- transaction.call('DEL', lock_key)
386
+ until own_lock_keys.empty?
387
+ released = conn.multi(watch: own_lock_keys) do |transaction|
388
+ # NOTE: (pipeline layout) one reply (lock owner) per lock in the series order
389
+ lock_owners = conn.pipelined do |pipeline|
390
+ own_lock_keys.each { |lock_key| pipeline.call('HGET', lock_key, 'acq_id') }
391
+ end
392
+ own_lock_keys = own_lock_keys.select.with_index do |_lock_key, index|
393
+ lock_owners[index] == acquirer_id
378
394
  end
395
+ own_lock_keys.each { |lock_key| transaction.call('DEL', lock_key) }
379
396
  end
397
+ # NOTE: `nil` => the transaction is aborted (a watched lock is changed) => retry
398
+ break unless released.nil?
380
399
  end
381
400
  end
382
401
  end
@@ -2,27 +2,42 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
- # @version 1.18.0
5
+ # @version 1.20.0
6
6
  # rubocop:disable Metrics/ModuleLength
7
7
  module RedisQueuedLocks::Acquirer::Locks
8
+ # The number of replies of one lock in the lock state pipeline of a batch of locks
9
+ # (`extract_locks_info`): HGETALL (write lock), PTTL (write lock), ZRANGE (readers registry).
10
+ #
11
+ # @return [Integer]
12
+ #
13
+ # @api private
14
+ # @since 1.20.0
15
+ LOCK_STATE_REPLIES = 3
16
+
8
17
  # rubocop:disable Metrics/ClassLength
9
18
  class << self
10
19
  # @param redis_client [RedisClient]
11
20
  # @option scan_size [Integer]
12
21
  # @option with_info [Boolean]
22
+ # @option lock_index_enabled [Boolean]
23
+ # Find locks via the lock index (`index_locks`) instead of the keyspace SCAN (`scan_locks`).
13
24
  # @return [Set<String>,Set<Hash<Symbol,Any>>]
14
25
  #
15
26
  # @api private
16
27
  # @since 1.0.0
17
- def locks(redis_client, scan_size:, with_info:)
28
+ # @version 1.20.0
29
+ def locks(redis_client, scan_size:, with_info:, lock_index_enabled:)
18
30
  redis_client.with do |rconn|
19
- lock_keys = scan_locks(rconn, scan_size)
20
- with_info ? extract_locks_info(rconn, lock_keys) : lock_keys
31
+ lock_keys =
32
+ lock_index_enabled ? index_locks(rconn, scan_size) : scan_locks(rconn, scan_size)
33
+ with_info ? extract_locks_info(rconn, lock_keys, scan_size) : lock_keys
21
34
  end
22
35
  end
23
36
 
24
37
  private
25
38
 
39
+ # Write locks and read locks in one SCAN pass (the disabled lock index).
40
+ #
26
41
  # @param redis_client [RedisClient]
27
42
  # @param scan_size [Integer]
28
43
  # @return [Set<String>]
@@ -30,88 +45,192 @@ module RedisQueuedLocks::Acquirer::Locks
30
45
  #
31
46
  # @api private
32
47
  # @since 1.0.0
33
- # @version 1.18.0
48
+ # @version 1.20.0
34
49
  def scan_locks(redis_client, scan_size)
35
50
  Set.new.tap do |lock_keys|
51
+ # @type var lock_keys: Set[String]
36
52
  redis_client.scan(
37
53
  'MATCH',
38
- RedisQueuedLocks::Resource::LOCK_PATTERN,
54
+ RedisQueuedLocks::Resource::KEY_PATTERN,
39
55
  count: scan_size
40
- ) do |lock_key|
41
- # TODO: reduce unnecessary iterations
42
- lock_keys.add(lock_key)
56
+ ) do |key|
57
+ case RedisQueuedLocks::Resource.lock_key_family(key)
58
+ when :lock
59
+ lock_keys.add(key)
60
+ when :lock_readers
61
+ # NOTE: (RW) read locks (readers registries can contain expired read locks only)
62
+ lock_keys.add(RedisQueuedLocks::Resource.lock_key_from_readers(key))
63
+ end
43
64
  end
65
+ end
66
+ end
44
67
 
45
- # NOTE: (RW) read locks (readers registries can contain expired read locks only)
46
- redis_client.scan(
47
- 'MATCH',
48
- RedisQueuedLocks::Resource::LOCK_READERS_PATTERN,
49
- count: scan_size
50
- ) do |lock_readers_key|
51
- # TODO: reduce unnecessary iterations
52
- lock_keys.add(RedisQueuedLocks::Resource.lock_key_from_readers(lock_readers_key))
68
+ # Write locks and read locks of the lock index (the enabled lock index): one ZSCAN pass over
69
+ # the lock names (`RedisQueuedLocks::Resource::LOCK_INDEX_KEY`, the keyspace is never scanned)
70
+ # with one round trip per batch of `scan_size` lock names (`EXISTS` of the write lock and of
71
+ # the readers registry of each lock name).
72
+ #
73
+ # @param redis_client [RedisClient]
74
+ # @param scan_size [Integer] ZSCAN count and the number of lock names in one batch.
75
+ # @return [Set<String>]
76
+ # - (RW) read locks are represented by their lock keys (`rql:lock:<lock_name>`) too
77
+ # (readers registries can contain expired read locks only);
78
+ #
79
+ # @api private
80
+ # @since 1.20.0
81
+ def index_locks(redis_client, scan_size)
82
+ Set.new.tap do |lock_keys|
83
+ # @type var lock_keys: Set[String]
84
+ each_lock_name_batch(redis_client, scan_size) do |lock_names|
85
+ # NOTE: (pipeline layout) one reply per lock name in the batch order: the number of
86
+ # the existing keys of the lock (the write lock and the readers registry);
87
+ # @type var lock_keys_count: Array[Integer]
88
+ lock_keys_count = redis_client.pipelined do |pipeline|
89
+ lock_names.each do |lock_name|
90
+ pipeline.call(
91
+ 'EXISTS',
92
+ RedisQueuedLocks::Resource.prepare_lock_key(lock_name),
93
+ RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
94
+ )
95
+ end
96
+ end
97
+
98
+ lock_names.each_with_index do |lock_name, index|
99
+ next if lock_keys_count[index] == 0
100
+
101
+ lock_keys.add(RedisQueuedLocks::Resource.prepare_lock_key(lock_name))
102
+ end
53
103
  end
54
104
  end
55
105
  end
56
106
 
107
+ # Iterates over the lock names of the lock index (one ZSCAN pass) in batches of `scan_size`
108
+ # lock names (batches are collected across ZSCAN pages). Expired lock names (not dropped from
109
+ # the index yet) are skipped: all their keys are expired.
110
+ #
111
+ # NOTE: each operation module iterates over the lock index by itself (duplicated on purpose:
112
+ # operation modules are independent of each other, see `extract_locks_info`);
113
+ #
114
+ # @param redis_client [RedisClient]
115
+ # @param scan_size [Integer] ZSCAN count and the number of lock names in one batch.
116
+ # @yield [lock_names]
117
+ # @yieldparam lock_names [Array<String>] The batch of lock names (reused between batches).
118
+ # @return [void]
119
+ #
120
+ # @api private
121
+ # @since 1.20.0
122
+ def each_lock_name_batch(redis_client, scan_size)
123
+ lock_index_time = RedisQueuedLocks::Resource.lock_index_time
124
+ # @type var lock_names: Array[String]
125
+ lock_names = []
126
+
127
+ redis_client.zscan(
128
+ RedisQueuedLocks::Resource::LOCK_INDEX_KEY, count: scan_size
129
+ ) do |lock_name, lock_index_score|
130
+ next if lock_index_score.to_f < lock_index_time
131
+
132
+ lock_names << lock_name
133
+ next if lock_names.size < scan_size
134
+
135
+ yield(lock_names)
136
+ lock_names.clear
137
+ end
138
+
139
+ yield(lock_names) unless lock_names.empty?
140
+ end
141
+
57
142
  # NOTE:
58
143
  # - the lock info is extracted by the module itself with the same approach as in `LockInfo`:
59
144
  # public operation modules do not reuse each other (they share logic via `Resource`,
60
145
  # `Utilities`), so the similar logic is duplicated on purpose;
146
+ # - locks are processed in batches of `scan_size` locks: two round trips per batch
147
+ # (the state of the locks, then the data of the live read locks);
61
148
  #
62
149
  # @param redis_client [RedisClient]
63
150
  # @param lock_keys [Set<String>]
151
+ # @param scan_size [Integer] The number of locks in one batch.
64
152
  # @return [Set<Hash<Symbol,Any>>]
65
153
  #
66
154
  # @api private
67
155
  # @since 1.0.0
68
- # @version 1.18.0
156
+ # @version 1.20.0
69
157
  # rubocop:disable Metrics/MethodLength
70
- def extract_locks_info(redis_client, lock_keys)
158
+ def extract_locks_info(redis_client, lock_keys, scan_size)
71
159
  Set.new.tap do |seeded_locks|
72
160
  # rubocop:disable Layout/LineLength
73
161
  # @type var seeded_locks: Set[{ lock: String, status: :released|:alive, info: Hash[String,untyped] }]
74
162
  # rubocop:enable Layout/LineLength
75
163
 
76
- # Step X: iterate each lock and extract their info
77
- lock_keys.each do |lock_key|
78
- # Step 1: extract lock info from redis
164
+ lock_keys.each_slice(scan_size) do |lock_keys_batch|
165
+ # Step 1: extract the state of the batch of locks in one round trip
166
+ # NOTE: (pipeline layout) [0] TIME (read locks are checked in redis time), then
167
+ # `LOCK_STATE_REPLIES` replies per lock in the batch order (the replies of the lock
168
+ # with the index N start at `1 + N * LOCK_STATE_REPLIES`): HGETALL (write lock),
169
+ # PTTL (write lock), ZRANGE WITHSCORES (readers registry);
170
+ # @type var batch_state: Array[untyped]
171
+ batch_state = redis_client.pipelined do |pipeline|
172
+ pipeline.call('TIME')
173
+ lock_keys_batch.each do |lock_key|
174
+ lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(
175
+ lock_key.delete_prefix('rql:lock:')
176
+ )
177
+ pipeline.call('HGETALL', lock_key)
178
+ pipeline.call('PTTL', lock_key)
179
+ pipeline.call('ZRANGE', lock_readers_key, '0', '-1', 'WITHSCORES')
180
+ end
181
+ end
182
+ now = RedisQueuedLocks::Resource.redis_time_ms(batch_state[0])
79
183
 
80
- # NOTE: (RW) readers registry of the lock
81
- lock_name = lock_key.delete_prefix('rql:lock:')
82
- lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
184
+ # Step 2: (RW) live read locks of the locks without a write lock
185
+ # (`nil` for the locks with a write lock: their read locks are not reported);
186
+ # @type var live_read_locks: Array[Array[[String, Float]]?]
187
+ live_read_locks = lock_keys_batch.each_with_index.map do |_lock_key, index|
188
+ replies_at = 1 + (index * LOCK_STATE_REPLIES)
189
+ # NOTE: `nil` when the write lock is alive (the key exists)
190
+ next unless batch_state[replies_at].empty? || batch_state[replies_at + 1] == -2
83
191
 
84
- # @type var lock_info: RedisQueuedLocks::Acquirer::Locks::lockInfo
85
- lock_info = redis_client.pipelined do |pipeline|
86
- pipeline.call('HGETALL', lock_key)
87
- pipeline.call('PTTL', lock_key)
88
- pipeline.call('TIME')
89
- pipeline.call('ZRANGE', lock_readers_key, '0', '-1', 'WITHSCORES')
90
- end.yield_self do |result| # Step 2: format the result
91
- # Step 2.X: lock is released
92
- if result == nil
93
- {} #: Hash[String,String|Float|Integer]
94
- else
95
- # NOTE: the result of MULTI-command is an array of results of each internal command
96
- # - result[0] (HGETALL) (Hash<String,String>)
97
- # => (will be mutated further with different value types)
98
- # - result[1] (PTTL) (Integer)
99
- # => (without any mutation, integer is atomic)
100
-
101
- # rubocop:disable Layout/LineLength
102
- # @type var result: [Hash[String,String|Float|Integer],Integer,Array[String],Array[[String, Float]]]
103
- # rubocop:enable Layout/LineLength
104
- hget_cmd_res = result[0] # NOTE: HGETALL result (hash)
105
- pttl_cmd_res = result[1] # NOTE: PTTL result (integer)
106
-
107
- # Step 2.Y: write lock is released
108
- if hget_cmd_res == {} || pttl_cmd_res == -2 # NOTE: key does not exist
192
+ # @type var read_locks: Array[[String, Float]]
193
+ read_locks = batch_state[replies_at + 2]
194
+ read_locks.select { |(_acquirer_id, expiration)| expiration > now }
195
+ end
196
+
197
+ # Step 3: (RW) read lock data of the live read locks in one round trip
198
+ # NOTE: (pipeline layout) one reply per live read lock in the lock and reader order;
199
+ # read lock data can be absent if it is dropped during the read lock life;
200
+ # @type var read_locks_data: Array[Hash[String,String]]
201
+ read_locks_data = redis_client.pipelined do |pipeline|
202
+ lock_keys_batch.each_with_index do |lock_key, index|
203
+ lock_name = lock_key.delete_prefix('rql:lock:')
204
+ live_read_locks[index]&.each do |(acquirer_id, _expiration)|
205
+ pipeline.call(
206
+ 'HGETALL',
207
+ RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
208
+ )
209
+ end
210
+ end
211
+ end
212
+
213
+ # Step 4: format the info of each lock and push it to the result store
214
+ read_locks_data_at = 0
215
+ lock_keys_batch.each_with_index do |lock_key, index|
216
+ lock_live_read_locks = live_read_locks[index]
217
+
218
+ # @type var lock_info: RedisQueuedLocks::Acquirer::Locks::lockInfo
219
+ lock_info =
220
+ if lock_live_read_locks
221
+ # Step 4.Y: write lock is released
109
222
  # NOTE: (RW) read locks (or nothing if there are no live read locks)
110
223
  read_lock_info(
111
- redis_client, lock_name, lock_key, result[2], result[3]
224
+ lock_key, now, lock_live_read_locks, read_locks_data, read_locks_data_at
112
225
  ) || {} #: Hash[String,String|Float|Integer]
113
226
  else
114
- # Step 2.Z: lock is alive => format received info + add additional rem_ttl info
227
+ # Step 4.Z: lock is alive => format received info + add additional rem_ttl info
228
+ replies_at = 1 + (index * LOCK_STATE_REPLIES)
229
+ # @type var hget_cmd_res: Hash[String,String|Float|Integer]
230
+ hget_cmd_res = batch_state[replies_at] # NOTE: HGETALL result (hash)
231
+ # @type var pttl_cmd_res: Integer
232
+ pttl_cmd_res = batch_state[replies_at + 1] # NOTE: PTTL result (integer)
233
+
115
234
  hget_cmd_res.tap do |lock_data|
116
235
  lock_data['rw_mode'] = 'write'
117
236
  lock_data['ts'] = Float(lock_data['ts'])
@@ -127,30 +246,30 @@ module RedisQueuedLocks::Acquirer::Locks
127
246
  Float(lock_data['l_spc_ext_ts']) if lock_data['l_spc_ext_ts']
128
247
  end
129
248
  end
130
- end
131
- end
249
+ read_locks_data_at += lock_live_read_locks.size if lock_live_read_locks
132
250
 
133
- # Step 3: push the lock info to the result store
134
- seeded_locks << {
135
- lock: lock_key,
136
- status: (lock_info.empty? ? :released : :alive),
137
- info: lock_info
138
- }
251
+ seeded_locks << {
252
+ lock: lock_key,
253
+ status: (lock_info.empty? ? :released : :alive),
254
+ info: lock_info
255
+ }
256
+ end
139
257
  end
140
258
  end
141
259
  end
142
260
  # rubocop:enable Metrics/MethodLength
143
261
 
144
- # Formats live read locks of the lock (expired read locks are ignored).
262
+ # Formats live read locks of the lock.
145
263
  #
146
264
  # NOTE: the same approach as in `LockInfo` (duplicated on purpose: operation modules are
147
- # independent of each other, see `extract_locks_info`);
265
+ # independent of each other, see `extract_locks_info`); the read lock data is extracted
266
+ # for the whole batch of locks in advance;
148
267
  #
149
- # @param redis_client [RedisClient]
150
- # @param lock_name [String]
151
268
  # @param lock_key [String]
152
- # @param redis_time [Array<String>] Result of the redis `TIME` command.
153
- # @param read_locks [Array<Array<String,Float>>] Readers registry (acquirer id, expiration).
269
+ # @param now [Integer] Redis time (in milliseconds).
270
+ # @param live_read_locks [Array<Array<String,Float>>] Live read locks (acquirer id, expiration).
271
+ # @param read_locks_data [Array<Hash<String,String>>] Read lock data of the batch of locks.
272
+ # @param read_locks_data_at [Integer] Position of the first read lock data of the lock.
154
273
  # @return [Hash<String,String|Numeric|Array<Hash<String,String|Numeric>>>,NilClass]
155
274
  # - `nil` is returned when there are no live read locks;
156
275
  # - each reader has the same data as the write lock (acq_id, hst_id, ts, ini_ttl, rem_ttl,
@@ -158,32 +277,25 @@ module RedisQueuedLocks::Acquirer::Locks
158
277
  #
159
278
  # @api private
160
279
  # @since 1.18.0
280
+ # @version 1.20.0
161
281
  # rubocop:disable Metrics/MethodLength
162
- def read_lock_info(redis_client, lock_name, lock_key, redis_time, read_locks)
163
- now = RedisQueuedLocks::Resource.redis_time_ms(redis_time)
164
- live_read_locks = read_locks.select { |(_acquirer_id, expiration)| expiration > now }
282
+ def read_lock_info(lock_key, now, live_read_locks, read_locks_data, read_locks_data_at)
165
283
  return nil if live_read_locks.empty?
166
284
 
167
- # NOTE: read lock data (it can be absent if it is dropped during the read lock life)
168
- # @type var read_locks_data: Array[Hash[String,String]]
169
- read_locks_data = redis_client.pipelined do |pipeline|
170
- live_read_locks.each do |(acquirer_id, _expiration)|
171
- pipeline.call(
172
- 'HGETALL',
173
- RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
174
- )
175
- end
176
- end
177
-
178
285
  longest_rem_ttl = 0
179
286
  readers = live_read_locks.each_with_index.map do |(acquirer_id, expiration), index|
180
287
  rem_ttl = (expiration - now).ceil
181
288
  longest_rem_ttl = rem_ttl if rem_ttl > longest_rem_ttl
182
289
 
290
+ # NOTE: acquirer and host ids are not stored in the read lock data (both are derived from
291
+ # the acquirer id): they are the first fields like in the write lock info (read lock data
292
+ # of older versions contains them with the same values);
183
293
  # @type var reader: Hash[String,String|Float|Integer|nil]
184
- reader = read_locks_data[index] || {}
185
- reader['acq_id'] = acquirer_id
186
- reader['hst_id'] ||= RedisQueuedLocks::Resource.host_identifier_from_acquirer(acquirer_id)
294
+ reader = {
295
+ 'acq_id' => acquirer_id,
296
+ 'hst_id' => RedisQueuedLocks::Resource.host_identifier_from_acquirer(acquirer_id)
297
+ }
298
+ read_locks_data[read_locks_data_at + index]&.each { |field, value| reader[field] = value }
187
299
  reader['ts'] = Float(reader['ts']) if reader['ts']
188
300
  reader['ini_ttl'] = Integer(reader['ini_ttl']) if reader['ini_ttl']
189
301
  reader['rem_ttl'] = rem_ttl
@@ -2,14 +2,13 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
- # @version 1.18.0
5
+ # @version 1.20.0
6
6
  module RedisQueuedLocks::Acquirer::QueueInfo
7
7
  class << self
8
8
  # Returns an information about the required lock queue by the lock name. The result
9
9
  # represnts the ordered lock request queue that is ordered by score (Redis sets) and shows
10
- # lock acquirers and their position in queue. Async nature with redis communcation can lead
11
- # the sitaution when the queue becomes empty during the queue data extraction. So sometimes
12
- # you can receive the lock queue info with empty queue.
10
+ # lock acquirers and their position in queue. The write lock queue is empty when only
11
+ # the queue of read lock requests exists.
13
12
  #
14
13
  # @param redis_client [RedisClient]
15
14
  # @param lock_name [String]
@@ -33,28 +32,24 @@ module RedisQueuedLocks::Acquirer::QueueInfo
33
32
  #
34
33
  # @api private
35
34
  # @since 1.0.0
36
- # @version 1.18.0
37
- # rubocop:disable Metrics/MethodLength
35
+ # @version 1.20.0
38
36
  def queue_info(redis_client, lock_name)
39
37
  lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
40
38
  read_lock_key_queue = RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name)
41
39
 
40
+ # NOTE:
41
+ # redis deletes empty sorted sets, so an empty ZRANGE result means that the queue
42
+ # does not exist (no need for EXISTS checks);
42
43
  result = redis_client.pipelined do |pipeline|
43
- pipeline.call('EXISTS', lock_key_queue)
44
44
  pipeline.call('ZRANGE', lock_key_queue, '0', '-1', 'WITHSCORES')
45
- pipeline.call('EXISTS', read_lock_key_queue)
46
45
  pipeline.call('ZRANGE', read_lock_key_queue, '0', '-1', 'WITHSCORES')
47
46
  end
48
47
 
49
- # rubocop:disable Layout/LineLength
50
- # @type var result: [Integer,Array[[String,Integer|Float]],Integer,Array[[String,Integer|Float]]]
51
- # rubocop:enable Layout/LineLength
52
- exists_cmd_res = result[0]
53
- zrange_cmd_res = result[1]
54
- read_exists_cmd_res = result[2]
55
- read_zrange_cmd_res = result[3]
48
+ # @type var result: [Array[[String,Integer|Float]],Array[[String,Integer|Float]]]
49
+ zrange_cmd_res = result[0]
50
+ read_zrange_cmd_res = result[1]
56
51
 
57
- if exists_cmd_res == 1 || read_exists_cmd_res == 1
52
+ if !zrange_cmd_res.empty? || !read_zrange_cmd_res.empty?
58
53
  # NOTE: queue existed during the piepline invocation
59
54
  # @type var queue_info: RedisQueuedLocks::Acquirer::QueueInfo::queueInfo
60
55
  queue_info = {
@@ -64,7 +59,7 @@ module RedisQueuedLocks::Acquirer::QueueInfo
64
59
  end
65
60
  }
66
61
  # NOTE: (RW) the queue of read lock requests
67
- if read_exists_cmd_res == 1
62
+ unless read_zrange_cmd_res.empty?
68
63
  queue_info['read_lock_queue'] = read_lock_key_queue
69
64
  queue_info['read_queue'] = read_zrange_cmd_res.map do |val|
70
65
  { 'acq_id' => val[0], 'score' => val[1], 'rw_mode' => 'read' }
@@ -76,6 +71,5 @@ module RedisQueuedLocks::Acquirer::QueueInfo
76
71
  nil
77
72
  end
78
73
  end
79
- # rubocop:enable Metrics/MethodLength
80
74
  end
81
75
  end