redis_queued_locks 1.17.0 → 1.19.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/project-overview.md +87 -10
  3. data/.claude/rules/acquirer.md +31 -11
  4. data/.claude/rules/arguments.md +2 -1
  5. data/.claude/rules/git.md +46 -0
  6. data/.claude/rules/logic.md +9 -3
  7. data/.claude/rules/swarm.md +4 -3
  8. data/.claude/rules/tests.md +9 -3
  9. data/.claude/rules/type-checking.md +10 -2
  10. data/.claude/rules/visitors.md +12 -7
  11. data/CHANGELOG.md +68 -0
  12. data/CLAUDE.md +7 -2
  13. data/README.md +427 -79
  14. data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rb +6 -3
  15. data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rb +9 -7
  16. data/lib/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rb +20 -10
  17. data/lib/redis_queued_locks/acquirer/acquire_lock/log_visitor.rb +34 -16
  18. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rb +240 -28
  19. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +398 -129
  20. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire/log_visitor.rb +12 -6
  21. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +32 -8
  22. data/lib/redis_queued_locks/acquirer/acquire_lock.rb +73 -20
  23. data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +9 -1
  24. data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +182 -3
  25. data/lib/redis_queued_locks/acquirer/is_locked.rb +19 -2
  26. data/lib/redis_queued_locks/acquirer/is_queued.rb +6 -2
  27. data/lib/redis_queued_locks/acquirer/lock_info.rb +96 -6
  28. data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +8 -2
  29. data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +18 -6
  30. data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +126 -27
  31. data/lib/redis_queued_locks/acquirer/locks.rb +103 -6
  32. data/lib/redis_queued_locks/acquirer/queue_info.rb +38 -9
  33. data/lib/redis_queued_locks/acquirer/queues.rb +28 -4
  34. data/lib/redis_queued_locks/acquirer/release_all_locks.rb +35 -1
  35. data/lib/redis_queued_locks/acquirer/release_lock.rb +40 -9
  36. data/lib/redis_queued_locks/acquirer/release_locks_of.rb +43 -3
  37. data/lib/redis_queued_locks/acquirer/release_read_lock.rb +143 -0
  38. data/lib/redis_queued_locks/acquirer.rb +1 -0
  39. data/lib/redis_queued_locks/client.rb +143 -14
  40. data/lib/redis_queued_locks/resource.rb +117 -9
  41. data/lib/redis_queued_locks/swarm/flush_zombies.rb +38 -0
  42. data/lib/redis_queued_locks/swarm/zombie_info.rb +42 -0
  43. data/lib/redis_queued_locks/version.rb +2 -2
  44. data/rbs_collection.lock.yaml +10 -2
  45. data/rbs_collection.yaml +0 -2
  46. data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rbs +2 -1
  47. data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rbs +0 -1
  48. data/sig/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rbs +5 -0
  49. data/sig/redis_queued_locks/acquirer/acquire_lock/log_visitor.rbs +12 -6
  50. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rbs +74 -7
  51. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +4 -2
  52. data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +3 -0
  53. data/sig/redis_queued_locks/acquirer/acquire_lock/yield_with_expire/log_visitor.rbs +4 -2
  54. data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +19 -1
  55. data/sig/redis_queued_locks/acquirer/lock_info.rbs +9 -1
  56. data/sig/redis_queued_locks/acquirer/locks.rbs +10 -1
  57. data/sig/redis_queued_locks/acquirer/queue_info.rbs +2 -4
  58. data/sig/redis_queued_locks/acquirer/queues.rbs +1 -1
  59. data/sig/redis_queued_locks/acquirer/release_lock.rbs +8 -1
  60. data/sig/redis_queued_locks/acquirer/release_read_lock.rbs +44 -0
  61. data/sig/redis_queued_locks/client.rbs +23 -2
  62. data/sig/redis_queued_locks/resource.rbs +9 -2
  63. data/sig/redis_queued_locks/swarm/zombie_info.rbs +5 -0
  64. metadata +4 -1
@@ -2,7 +2,10 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
+ # @version 1.18.0
6
+ # rubocop:disable Metrics/ModuleLength
5
7
  module RedisQueuedLocks::Acquirer::Locks
8
+ # rubocop:disable Metrics/ClassLength
6
9
  class << self
7
10
  # @param redis_client [RedisClient]
8
11
  # @option scan_size [Integer]
@@ -23,9 +26,11 @@ module RedisQueuedLocks::Acquirer::Locks
23
26
  # @param redis_client [RedisClient]
24
27
  # @param scan_size [Integer]
25
28
  # @return [Set<String>]
29
+ # - (RW) read locks are represented by their lock keys (`rql:lock:<lock_name>`) too;
26
30
  #
27
31
  # @api private
28
32
  # @since 1.0.0
33
+ # @version 1.18.0
29
34
  def scan_locks(redis_client, scan_size)
30
35
  Set.new.tap do |lock_keys|
31
36
  redis_client.scan(
@@ -36,19 +41,33 @@ module RedisQueuedLocks::Acquirer::Locks
36
41
  # TODO: reduce unnecessary iterations
37
42
  lock_keys.add(lock_key)
38
43
  end
44
+
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))
53
+ end
39
54
  end
40
55
  end
41
56
 
57
+ # NOTE:
58
+ # - the lock info is extracted by the module itself with the same approach as in `LockInfo`:
59
+ # public operation modules do not reuse each other (they share logic via `Resource`,
60
+ # `Utilities`), so the similar logic is duplicated on purpose;
61
+ #
42
62
  # @param redis_client [RedisClient]
43
63
  # @param lock_keys [Set<String>]
44
64
  # @return [Set<Hash<Symbol,Any>>]
45
65
  #
46
66
  # @api private
47
67
  # @since 1.0.0
48
- # @version 1.9.0
68
+ # @version 1.18.0
49
69
  # rubocop:disable Metrics/MethodLength
50
70
  def extract_locks_info(redis_client, lock_keys)
51
- # TODO: refactor with RedisQueuedLocks::Acquier::LockInfo
52
71
  Set.new.tap do |seeded_locks|
53
72
  # rubocop:disable Layout/LineLength
54
73
  # @type var seeded_locks: Set[{ lock: String, status: :released|:alive, info: Hash[String,untyped] }]
@@ -58,10 +77,16 @@ module RedisQueuedLocks::Acquirer::Locks
58
77
  lock_keys.each do |lock_key|
59
78
  # Step 1: extract lock info from redis
60
79
 
61
- # @type var lock_info: Hash[String,String|Float|Integer]
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)
83
+
84
+ # @type var lock_info: RedisQueuedLocks::Acquirer::Locks::lockInfo
62
85
  lock_info = redis_client.pipelined do |pipeline|
63
86
  pipeline.call('HGETALL', lock_key)
64
87
  pipeline.call('PTTL', lock_key)
88
+ pipeline.call('TIME')
89
+ pipeline.call('ZRANGE', lock_readers_key, '0', '-1', 'WITHSCORES')
65
90
  end.yield_self do |result| # Step 2: format the result
66
91
  # Step 2.X: lock is released
67
92
  if result == nil
@@ -73,16 +98,22 @@ module RedisQueuedLocks::Acquirer::Locks
73
98
  # - result[1] (PTTL) (Integer)
74
99
  # => (without any mutation, integer is atomic)
75
100
 
76
- # @type var result: [Hash[String,String|Float|Integer],Integer]
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
77
104
  hget_cmd_res = result[0] # NOTE: HGETALL result (hash)
78
105
  pttl_cmd_res = result[1] # NOTE: PTTL result (integer)
79
106
 
80
- # Step 2.Y: lock is released
107
+ # Step 2.Y: write lock is released
81
108
  if hget_cmd_res == {} || pttl_cmd_res == -2 # NOTE: key does not exist
82
- {} #: Hash[String,String|Float|Integer]
109
+ # NOTE: (RW) read locks (or nothing if there are no live read locks)
110
+ read_lock_info(
111
+ redis_client, lock_name, lock_key, result[2], result[3]
112
+ ) || {} #: Hash[String,String|Float|Integer]
83
113
  else
84
114
  # Step 2.Z: lock is alive => format received info + add additional rem_ttl info
85
115
  hget_cmd_res.tap do |lock_data|
116
+ lock_data['rw_mode'] = 'write'
86
117
  lock_data['ts'] = Float(lock_data['ts'])
87
118
  lock_data['ini_ttl'] = Integer(lock_data['ini_ttl'])
88
119
  lock_data['rem_ttl'] = ((pttl_cmd_res == -1) ? Float::INFINITY : pttl_cmd_res)
@@ -109,5 +140,71 @@ module RedisQueuedLocks::Acquirer::Locks
109
140
  end
110
141
  end
111
142
  # rubocop:enable Metrics/MethodLength
143
+
144
+ # Formats live read locks of the lock (expired read locks are ignored).
145
+ #
146
+ # NOTE: the same approach as in `LockInfo` (duplicated on purpose: operation modules are
147
+ # independent of each other, see `extract_locks_info`);
148
+ #
149
+ # @param redis_client [RedisClient]
150
+ # @param lock_name [String]
151
+ # @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).
154
+ # @return [Hash<String,String|Numeric|Array<Hash<String,String|Numeric>>>,NilClass]
155
+ # - `nil` is returned when there are no live read locks;
156
+ # - each reader has the same data as the write lock (acq_id, hst_id, ts, ini_ttl, rem_ttl,
157
+ # meta, reentrant lock data);
158
+ #
159
+ # @api private
160
+ # @since 1.18.0
161
+ # 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 }
165
+ return nil if live_read_locks.empty?
166
+
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
+ longest_rem_ttl = 0
179
+ readers = live_read_locks.each_with_index.map do |(acquirer_id, expiration), index|
180
+ rem_ttl = (expiration - now).ceil
181
+ longest_rem_ttl = rem_ttl if rem_ttl > longest_rem_ttl
182
+
183
+ # @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)
187
+ reader['ts'] = Float(reader['ts']) if reader['ts']
188
+ reader['ini_ttl'] = Integer(reader['ini_ttl']) if reader['ini_ttl']
189
+ reader['rem_ttl'] = rem_ttl
190
+ reader['spc_cnt'] = Integer(reader['spc_cnt']) if reader['spc_cnt']
191
+ reader['l_spc_ts'] = Float(reader['l_spc_ts']) if reader['l_spc_ts']
192
+ reader['spc_ext_ttl'] = Integer(reader['spc_ext_ttl']) if reader['spc_ext_ttl']
193
+ reader['l_spc_ext_ini_ttl'] =
194
+ Integer(reader['l_spc_ext_ini_ttl']) if reader['l_spc_ext_ini_ttl']
195
+ reader['l_spc_ext_ts'] = Float(reader['l_spc_ext_ts']) if reader['l_spc_ext_ts']
196
+ reader
197
+ end
198
+
199
+ {
200
+ 'lock_key' => lock_key,
201
+ 'rw_mode' => 'read',
202
+ 'rem_ttl' => longest_rem_ttl,
203
+ 'readers' => readers
204
+ }
205
+ end
206
+ # rubocop:enable Metrics/MethodLength
112
207
  end
208
+ # rubocop:enable Metrics/ClassLength
113
209
  end
210
+ # rubocop:enable Metrics/ModuleLength
@@ -2,6 +2,7 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
+ # @version 1.18.0
5
6
  module RedisQueuedLocks::Acquirer::QueueInfo
6
7
  class << self
7
8
  # Returns an information about the required lock queue by the lock name. The result
@@ -12,41 +13,69 @@ module RedisQueuedLocks::Acquirer::QueueInfo
12
13
  #
13
14
  # @param redis_client [RedisClient]
14
15
  # @param lock_name [String]
15
- # @return [Hash<String,Array<Hash<String,String|Numeric>>,NilClass]
16
- # - `nil` is returned when lock queue does not exist;
16
+ # @return [Hash<String,String|Array<Hash<String,String|Numeric>>,NilClass]
17
+ # - `nil` is returned when lock queues (write and read) do not exist;
17
18
  # - result format: {
18
19
  # "lock_queue" => "rql:lock_queue:your_lock_name", # lock queue key in redis,
19
20
  # "queue" => [
20
- # { "acq_id" => "rql:acq:123/456/789/987/identity", "score" => 123.456 },
21
- # { "acq_id" => "rql:acq:123/686/789/987/identity", "score" => 456.789 },
21
+ # { "acq_id" => "rql:acq:123/456/789/987/id", "score" => 123.456, "rw_mode" => "write" },
22
+ # { "acq_id" => "rql:acq:123/686/789/987/id", "score" => 456.789, "rw_mode" => "write" },
22
23
  # ...
23
- # ] # ordered set (by score) with information about an acquirer and their position in queue
24
+ # ] # ordered set (by score) with information about an acquirer, their position in queue
25
+ # # and the requested lock mode
26
+ # <additional keys when the queue of read lock requests exists>:
27
+ # "read_lock_queue" => "rql:lock_read_queue:your_lock_name", # read lock queue key in redis
28
+ # "read_queue" => [
29
+ # { "acq_id" => "rql:acq:123/456/789/987/id", "score" => 123.456, "rw_mode" => "read" },
30
+ # ...
31
+ # ] # read lock requests (positions are comparable with the write lock request positions)
24
32
  # }
25
33
  #
26
34
  # @api private
27
35
  # @since 1.0.0
36
+ # @version 1.18.0
37
+ # rubocop:disable Metrics/MethodLength
28
38
  def queue_info(redis_client, lock_name)
29
39
  lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
40
+ read_lock_key_queue = RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name)
30
41
 
31
42
  result = redis_client.pipelined do |pipeline|
32
43
  pipeline.call('EXISTS', lock_key_queue)
33
44
  pipeline.call('ZRANGE', lock_key_queue, '0', '-1', 'WITHSCORES')
45
+ pipeline.call('EXISTS', read_lock_key_queue)
46
+ pipeline.call('ZRANGE', read_lock_key_queue, '0', '-1', 'WITHSCORES')
34
47
  end
35
48
 
36
- # @type var result: [Integer,Array[[String,Integer|Float]]]
49
+ # rubocop:disable Layout/LineLength
50
+ # @type var result: [Integer,Array[[String,Integer|Float]],Integer,Array[[String,Integer|Float]]]
51
+ # rubocop:enable Layout/LineLength
37
52
  exists_cmd_res = result[0]
38
53
  zrange_cmd_res = result[1]
54
+ read_exists_cmd_res = result[2]
55
+ read_zrange_cmd_res = result[3]
39
56
 
40
- if exists_cmd_res == 1
57
+ if exists_cmd_res == 1 || read_exists_cmd_res == 1
41
58
  # NOTE: queue existed during the piepline invocation
42
- {
59
+ # @type var queue_info: RedisQueuedLocks::Acquirer::QueueInfo::queueInfo
60
+ queue_info = {
43
61
  'lock_queue' => lock_key_queue,
44
- 'queue' => zrange_cmd_res.map { |val| { 'acq_id' => val[0], 'score' => val[1] } }
62
+ 'queue' => zrange_cmd_res.map do |val|
63
+ { 'acq_id' => val[0], 'score' => val[1], 'rw_mode' => 'write' }
64
+ end
45
65
  }
66
+ # NOTE: (RW) the queue of read lock requests
67
+ if read_exists_cmd_res == 1
68
+ queue_info['read_lock_queue'] = read_lock_key_queue
69
+ queue_info['read_queue'] = read_zrange_cmd_res.map do |val|
70
+ { 'acq_id' => val[0], 'score' => val[1], 'rw_mode' => 'read' }
71
+ end
72
+ end
73
+ queue_info
46
74
  else
47
75
  # NOTE: queue did not exist during the pipeline invocation
48
76
  nil
49
77
  end
50
78
  end
79
+ # rubocop:enable Metrics/MethodLength
51
80
  end
52
81
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
+ # @version 1.18.0
5
6
  module RedisQueuedLocks::Acquirer::Queues
6
7
  class << self
7
8
  # @param redis_client [RedisClient]
@@ -23,9 +24,11 @@ module RedisQueuedLocks::Acquirer::Queues
23
24
  # @param redis_client [RedisClient]
24
25
  # @param scan_size [Integer]
25
26
  # @return [Set<String>]
27
+ # - (RW) queues of read lock requests are included;
26
28
  #
27
29
  # @api private
28
30
  # @since 1.0.0
31
+ # @version 1.18.0
29
32
  def scan_queues(redis_client, scan_size)
30
33
  Set.new.tap do |lock_queues|
31
34
  # @type var lock_queues: Set[String]
@@ -38,21 +41,40 @@ module RedisQueuedLocks::Acquirer::Queues
38
41
  # @type var lock_queue: String
39
42
  lock_queues.add(lock_queue)
40
43
  end
44
+
45
+ # NOTE: (RW) queues of read lock requests
46
+ redis_client.scan(
47
+ 'MATCH',
48
+ RedisQueuedLocks::Resource::READ_LOCK_QUEUE_PATTERN,
49
+ count: scan_size
50
+ ) do |read_lock_queue|
51
+ # TODO: reduce unnecessary iterations
52
+ # @type var read_lock_queue: String
53
+ lock_queues.add(read_lock_queue)
54
+ end
41
55
  end
42
56
  end
43
57
 
44
58
  # @param redis_client [RedisClient]
45
59
  # @param lock_queues [Set<String>]
46
60
  # @return [Set<Hash<Symbol,Any>>]
61
+ # - each request: `{ 'acq_id' => String, 'score' => Float, 'rw_mode' => 'read'/'write' }`;
47
62
  #
48
63
  # @api private
49
64
  # @since 1.0.0
65
+ # @version 1.18.0
50
66
  def extract_queues_info(redis_client, lock_queues)
51
- # TODO: refactor with RedisQueuedLocks::Acquier::QueueInfo
67
+ # NOTE: the same approach as in `QueueInfo` (duplicated on purpose: public operation modules
68
+ # do not reuse each other, they share logic via `Resource`, `Utilities`);
52
69
  Set.new.tap do |seeded_queues|
53
70
  # Step X: iterate over each lock queue and extract their info
54
71
  # @type var seeded_queues: Set[Hash[Symbol,untyped]]
55
72
  lock_queues.each do |lock_queue|
73
+ # NOTE:
74
+ # - (RW) the mode of the requests of the queue (`'read'` or `'write'`);
75
+ # - lock queues are scanned by the lock queue patterns, so the mode is never `nil`;
76
+ rw_mode = RedisQueuedLocks::Resource.lock_queue_rw_mode(lock_queue) #: String
77
+
56
78
  # Step 1: extract lock queue info from reids
57
79
  queue_info = redis_client.pipelined do |pipeline|
58
80
  pipeline.call('EXISTS', lock_queue)
@@ -64,14 +86,16 @@ module RedisQueuedLocks::Acquirer::Queues
64
86
  zrange_cmd_res = result[1]
65
87
 
66
88
  if exists_cmd_res == 1 # Step 2.X: lock queue existed during the piepline invocation
67
- zrange_cmd_res.map { |val| { 'acq_id' => val[0], 'score' => val[1] } }
89
+ zrange_cmd_res.map do |val|
90
+ { 'acq_id' => val[0], 'score' => val[1], 'rw_mode' => rw_mode }
91
+ end
68
92
  else
69
93
  # Step 2.Y: lock queue did not exist during the pipeline invocation
70
- [] #: Array[Hash[String,Float]]
94
+ [] #: Array[Hash[String,String|Float]]
71
95
  end
72
96
  end
73
97
 
74
- # @type var queue_info: Array[Hash[String,Float]]
98
+ # @type var queue_info: Array[Hash[String,String|Float]]
75
99
 
76
100
  # Step 3: push the lock queue info to the result store
77
101
  seeded_queues << {
@@ -10,6 +10,7 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
10
10
  # Release all locks:
11
11
  # - 1. clear all lock queus: drop them all from Redis database by the lock queue pattern;
12
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
14
  #
14
15
  # @param redis [RedisClient]
15
16
  # Redis connection client.
@@ -71,7 +72,7 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
71
72
  #
72
73
  # @api private
73
74
  # @since 1.0.0
74
- # @version 1.14.0
75
+ # @version 1.18.0
75
76
  def release_all_locks(
76
77
  redis,
77
78
  batch_size,
@@ -129,6 +130,8 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
129
130
  #
130
131
  # @api private
131
132
  # @since 1.0.0
133
+ # @version 1.18.0
134
+ # rubocop:disable Metrics/MethodLength
132
135
  def fully_release_all_locks(redis, batch_size)
133
136
  result = redis.with do |rconn|
134
137
  rconn.pipelined do |pipeline|
@@ -151,10 +154,41 @@ module RedisQueuedLocks::Acquirer::ReleaseAllLocks
151
154
  # TODO: reduce unnecessary iterations
152
155
  pipeline.call('EXPIRE', lock_key, '0')
153
156
  end
157
+
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')
166
+ end
167
+
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
177
+
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')
186
+ end
154
187
  end
155
188
  end
156
189
 
157
190
  { ok: true, result: { rel_key_cnt: result.sum } }
158
191
  end
192
+ # rubocop:enable Metrics/MethodLength
159
193
  end
160
194
  end
@@ -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