redis_queued_locks 1.17.0 → 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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/project-overview.md +86 -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 +62 -0
  12. data/CLAUDE.md +7 -2
  13. data/README.md +348 -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 +129 -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 +1 -1
  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 +22 -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,17 +2,34 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
+ # @version 1.18.0
5
6
  module RedisQueuedLocks::Acquirer::IsLocked
6
7
  class << self
7
8
  # @param redis_client [RedisClient]
8
9
  # @param lock_name [String]
9
- # @return [Boolean]
10
+ # @return [Boolean] Is the lock obtained by a writer or by any reader (live read lock).
10
11
  #
11
12
  # @api private
12
13
  # @since 1.0.0
14
+ # @version 1.18.0
13
15
  def locked?(redis_client, lock_name)
14
16
  lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
15
- redis_client.call('EXISTS', lock_key) == 1
17
+ lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
18
+
19
+ # @type var result: [Integer, Array[String], Array[[String, Float]]]
20
+ result = redis_client.pipelined do |pipeline|
21
+ pipeline.call('EXISTS', lock_key)
22
+ pipeline.call('TIME')
23
+ pipeline.call('ZRANGE', lock_readers_key, '-1', '-1', 'WITHSCORES')
24
+ end
25
+
26
+ # NOTE: write lock
27
+ return true if result[0] == 1
28
+
29
+ # NOTE: (RW) read lock (expired read locks can live in the readers registry till the cleanup)
30
+ longest_read_lock = result[2].first
31
+ longest_read_lock != nil &&
32
+ longest_read_lock[1] > RedisQueuedLocks::Resource.redis_time_ms(result[1])
16
33
  end
17
34
  end
18
35
  end
@@ -2,17 +2,21 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
+ # @version 1.18.0
5
6
  module RedisQueuedLocks::Acquirer::IsQueued
6
7
  class << self
7
8
  # @param redis_client [RedisClient]
8
9
  # @param lock_name [String]
9
- # @return [Boolean]
10
+ # @return [Boolean] Are there any write or read lock requests.
10
11
  #
11
12
  # @api private
12
13
  # @since 1.0.0
14
+ # @version 1.18.0
13
15
  def queued?(redis_client, lock_name)
14
16
  lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
15
- redis_client.call('EXISTS', lock_key_queue) == 1
17
+ read_lock_key_queue = RedisQueuedLocks::Resource.prepare_read_lock_queue(lock_name)
18
+ # NOTE: EXISTS returns the number of existing keys
19
+ redis_client.call('EXISTS', lock_key_queue, read_lock_key_queue) > 0
16
20
  end
17
21
  end
18
22
  end
@@ -2,14 +2,16 @@
2
2
 
3
3
  # @api private
4
4
  # @since 1.0.0
5
+ # @version 1.18.0
5
6
  module RedisQueuedLocks::Acquirer::LockInfo
6
7
  class << self
7
8
  # @param redis_client [RedisClient]
8
9
  # @param lock_name [String]
9
- # @return [Hash<String,String|Numeric>,NilClass]
10
- # - `nil` is returned when lock key does not exist or expired;
11
- # - result format: {
10
+ # @return [Hash<String,String|Numeric|Array<Hash<String,String|Numeric>>>,NilClass]
11
+ # - `nil` is returned when lock key does not exist or expired (and there are no read locks);
12
+ # - result format (write lock): {
12
13
  # 'lock_key' => "rql:lock:your_lockname", # acquired lock key
14
+ # 'rw_mode' => 'write', # lock mode
13
15
  # 'acq_id' => "rql:acq:123/456/789/987/uniqstring", # lock acquirer identifier
14
16
  # 'hst_id' => "rql:hst:123/456/987/uniqstring", # lock host identifier
15
17
  # 'ts' => 123456789.2649841, # <locked at> time stamp (epoch, seconds.microseconds)
@@ -22,17 +24,36 @@ module RedisQueuedLocks::Acquirer::LockInfo
22
24
  # 'l_spc_ext_ini_ttl' => 5000, # (milliseconds) the last ttl of reentrant lock
23
25
  # 'l_spc_ext_ts' => 123456.789 # (epoch) <extendable reentrant lock obtained at> timestamp
24
26
  # }
27
+ # - result format (read locks, when there is no write lock): {
28
+ # 'lock_key' => "rql:lock:your_lockname", # lock key
29
+ # 'rw_mode' => 'read', # lock mode
30
+ # 'rem_ttl' => 123456789, # remaining ttl of the longest read lock (milliseconds)
31
+ # 'readers' => [ # live read locks
32
+ # {
33
+ # 'acq_id' => "rql:acq:123/456/789/987/uniqstring", # read lock acquirer identifier
34
+ # 'hst_id' => "rql:hst:123/456/987/uniqstring", # read lock host identifier
35
+ # 'ts' => 123456789.2649841, # <locked at> time stamp (epoch, seconds.microseconds)
36
+ # 'ini_ttl' => 123456789, # initial read lock ttl (milliseconds)
37
+ # 'rem_ttl' => 123456789, # remaining read lock ttl (milliseconds)
38
+ # <custom metadata (`meta`) and additional keys for reentrant locks (see above)>
39
+ # },
40
+ # ...
41
+ # ]
42
+ # }
25
43
  #
26
44
  # @api private
27
45
  # @since 1.0.0
28
- # @version 1.9.0
46
+ # @version 1.18.0
29
47
  # rubocop:disable Metrics/MethodLength
30
48
  def lock_info(redis_client, lock_name)
31
49
  lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
50
+ lock_readers_key = RedisQueuedLocks::Resource.prepare_lock_readers(lock_name)
32
51
 
33
52
  result = redis_client.pipelined do |pipeline|
34
53
  pipeline.call('HGETALL', lock_key)
35
54
  pipeline.call('PTTL', lock_key)
55
+ pipeline.call('TIME')
56
+ pipeline.call('ZRANGE', lock_readers_key, '0', '-1', 'WITHSCORES')
36
57
  end
37
58
 
38
59
  if result == nil
@@ -48,16 +69,22 @@ module RedisQueuedLocks::Acquirer::LockInfo
48
69
  # => (will be mutated further with different value types)
49
70
  # - result[1] (PTTL) (Integer)
50
71
  # => (without any mutation, integer is atomic)
72
+ # - result[2] (TIME) (Array<String>) (redis time)
73
+ # - result[3] (ZRANGE) (Array<[String,Float]>) (read locks)
51
74
 
52
- # @type var result: [Hash[String,String|Float|Integer],Integer]
75
+ # rubocop:disable Layout/LineLength
76
+ # @type var result: [Hash[String,String|Float|Integer], Integer, Array[String], Array[[String, Float]]]
77
+ # rubocop:enable Layout/LineLength
53
78
  hget_cmd_res = result[0]
54
79
  pttl_cmd_res = result[1]
55
80
 
56
81
  if hget_cmd_res == {} || pttl_cmd_res == -2 # NOTE: key does not exist
57
- nil
82
+ # NOTE: (RW) there is no write lock => check read locks
83
+ read_lock_info(redis_client, lock_name, lock_key, result[2], result[3])
58
84
  else
59
85
  hget_cmd_res.tap do |lock_data|
60
86
  lock_data['lock_key'] = lock_key
87
+ lock_data['rw_mode'] = 'write'
61
88
  lock_data['ts'] = Float(lock_data['ts'])
62
89
  lock_data['ini_ttl'] = Integer(lock_data['ini_ttl'])
63
90
  lock_data['rem_ttl'] = ((pttl_cmd_res == -1) ? Float::INFINITY : pttl_cmd_res)
@@ -73,5 +100,68 @@ module RedisQueuedLocks::Acquirer::LockInfo
73
100
  end
74
101
  end
75
102
  # rubocop:enable Metrics/MethodLength
103
+
104
+ private
105
+
106
+ # Formats live read locks of the lock (expired read locks are ignored).
107
+ #
108
+ # @param redis_client [RedisClient]
109
+ # @param lock_name [String]
110
+ # @param lock_key [String]
111
+ # @param redis_time [Array<String>] Result of the redis `TIME` command.
112
+ # @param read_locks [Array<Array<String,Float>>] Readers registry (acquirer id, expiration).
113
+ # @return [Hash<String,String|Numeric|Array<Hash<String,String|Numeric>>>,NilClass]
114
+ # - `nil` is returned when there are no live read locks;
115
+ # - each reader has the same data as the write lock (acq_id, hst_id, ts, ini_ttl, rem_ttl,
116
+ # meta, reentrant lock data);
117
+ #
118
+ # @api private
119
+ # @since 1.18.0
120
+ # rubocop:disable Metrics/MethodLength
121
+ def read_lock_info(redis_client, lock_name, lock_key, redis_time, read_locks)
122
+ now = RedisQueuedLocks::Resource.redis_time_ms(redis_time)
123
+ live_read_locks = read_locks.select { |(_acquirer_id, expiration)| expiration > now }
124
+ return nil if live_read_locks.empty?
125
+
126
+ # NOTE: read lock data (it can be absent if it is dropped during the read lock life)
127
+ # @type var read_locks_data: Array[Hash[String,String]]
128
+ read_locks_data = redis_client.pipelined do |pipeline|
129
+ live_read_locks.each do |(acquirer_id, _expiration)|
130
+ pipeline.call(
131
+ 'HGETALL',
132
+ RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
133
+ )
134
+ end
135
+ end
136
+
137
+ longest_rem_ttl = 0
138
+ readers = live_read_locks.each_with_index.map do |(acquirer_id, expiration), index|
139
+ rem_ttl = (expiration - now).ceil
140
+ longest_rem_ttl = rem_ttl if rem_ttl > longest_rem_ttl
141
+
142
+ # @type var reader: Hash[String,String|Float|Integer|nil]
143
+ reader = read_locks_data[index] || {}
144
+ reader['acq_id'] = acquirer_id
145
+ reader['hst_id'] ||= RedisQueuedLocks::Resource.host_identifier_from_acquirer(acquirer_id)
146
+ reader['ts'] = Float(reader['ts']) if reader['ts']
147
+ reader['ini_ttl'] = Integer(reader['ini_ttl']) if reader['ini_ttl']
148
+ reader['rem_ttl'] = rem_ttl
149
+ reader['spc_cnt'] = Integer(reader['spc_cnt']) if reader['spc_cnt']
150
+ reader['l_spc_ts'] = Float(reader['l_spc_ts']) if reader['l_spc_ts']
151
+ reader['spc_ext_ttl'] = Integer(reader['spc_ext_ttl']) if reader['spc_ext_ttl']
152
+ reader['l_spc_ext_ini_ttl'] =
153
+ Integer(reader['l_spc_ext_ini_ttl']) if reader['l_spc_ext_ini_ttl']
154
+ reader['l_spc_ext_ts'] = Float(reader['l_spc_ext_ts']) if reader['l_spc_ext_ts']
155
+ reader
156
+ end
157
+
158
+ {
159
+ 'lock_key' => lock_key,
160
+ 'rw_mode' => 'read',
161
+ 'rem_ttl' => longest_rem_ttl,
162
+ 'readers' => readers
163
+ }
164
+ end
165
+ # rubocop:enable Metrics/MethodLength
76
166
  end
77
167
  end
@@ -6,12 +6,15 @@
6
6
  module RedisQueuedLocks::Acquirer::LockSeriesPoC::InstrVisitor # steep:ignore
7
7
  class << self
8
8
  # NOTE: Lock Series PoC
9
+ # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
9
10
  # @api private
10
11
  # @since 1.16.1
12
+ # @version 1.18.0
11
13
  def lock_series_obtained( # steep:ignore
12
14
  instrumenter,
13
15
  instr_sampled,
14
16
  lock_keys,
17
+ rw_mode,
15
18
  ttl,
16
19
  acq_id,
17
20
  hst_id,
@@ -21,17 +24,20 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::InstrVisitor # steep:ignore
21
24
  )
22
25
  return unless instr_sampled
23
26
  instrumenter.notify('redis_queued_locks.lock_series_obtained', {
24
- lock_keys:, ttl:, acq_id:, hst_id:, ts:, acq_time:, instrument:
27
+ lock_keys:, rw_mode:, ttl:, acq_id:, hst_id:, ts:, acq_time:, instrument:
25
28
  }) rescue nil
26
29
  end
27
30
 
28
31
  # NOTE: Lock Series PoC
32
+ # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
29
33
  # @api private
30
34
  # @since 1.16.1
35
+ # @version 1.18.0
31
36
  def lock_series_hold_and_release( # steep:ignore
32
37
  instrumenter,
33
38
  instr_sampled,
34
39
  lock_keys,
40
+ rw_mode,
35
41
  ttl,
36
42
  acq_id,
37
43
  hst_id,
@@ -42,7 +48,7 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::InstrVisitor # steep:ignore
42
48
  )
43
49
  return unless instr_sampled
44
50
  instrumenter.notify('redis_queued_locks.lock_series_hold_and_release', {
45
- lock_keys:, hold_time:, ttl:, acq_id:, hst_id:, ts:, acq_time:, instrument:
51
+ lock_keys:, rw_mode:, hold_time:, ttl:, acq_id:, hst_id:, ts:, acq_time:, instrument:
46
52
  }) # rescue nil
47
53
  end
48
54
  end
@@ -6,8 +6,10 @@
6
6
  module RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor # steep:ignore
7
7
  class << self
8
8
  # NOTE: Lock Series PoC
9
+ # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
9
10
  # @api private
10
11
  # @since 1.16.0
12
+ # @version 1.18.0
11
13
  def start_lock_series_obtaining( # steep:ignore
12
14
  logger,
13
15
  log_sampled,
@@ -15,23 +17,27 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor # steep:ignore
15
17
  queue_ttl,
16
18
  acquirer_id,
17
19
  host_id,
18
- access_strategy
20
+ access_strategy,
21
+ rw_mode
19
22
  )
20
23
  return unless log_sampled
21
24
 
22
25
  logger.debug do
23
26
  "[redis_queued_locks.start_lock_series_obtaining] " \
24
- "lock_keys => '#{lock_keys.inspect}'" \
27
+ "lock_keys => '#{lock_keys.inspect}' " \
25
28
  "queue_ttl => #{queue_ttl} " \
26
29
  "acq_id => '#{acquirer_id}' " \
27
30
  "hst_id => '#{host_id}' " \
28
- "acs_strat => '#{access_strategy}'"
31
+ "acs_strat => '#{access_strategy}' " \
32
+ "rw_mode => '#{rw_mode}'"
29
33
  end # rescue nil
30
34
  end
31
35
 
32
36
  # NOTE: Lock Series PoC
37
+ # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
33
38
  # @api private
34
39
  # @since 1.16.0
40
+ # @version 1.18.0
35
41
  def lock_series_obtained( # steep:ignore
36
42
  logger,
37
43
  log_sampled,
@@ -40,7 +46,8 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor # steep:ignore
40
46
  acquirer_id,
41
47
  host_id,
42
48
  acq_time,
43
- access_strategy
49
+ access_strategy,
50
+ rw_mode
44
51
  )
45
52
  return unless log_sampled
46
53
 
@@ -51,13 +58,16 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor # steep:ignore
51
58
  "acq_id => '#{acquirer_id}' " \
52
59
  "hst_id => '#{host_id}' " \
53
60
  "acs_strat => '#{access_strategy}' " \
61
+ "rw_mode => '#{rw_mode}' " \
54
62
  "acq_time => #{acq_time} (ms)"
55
63
  end # rescue nil
56
64
  end
57
65
 
58
66
  # NOTE: Lock Series PoC
67
+ # @param rw_mode [Symbol] The mode of the lock series (`:read` or `:write`).
59
68
  # @api private
60
69
  # @since 1.16.0
70
+ # @version 1.18.0
61
71
  def expire_lock_series( # steep:ignore
62
72
  logger,
63
73
  log_sampled,
@@ -65,7 +75,8 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor # steep:ignore
65
75
  queue_ttl,
66
76
  acquirer_id,
67
77
  host_id,
68
- access_strategy
78
+ access_strategy,
79
+ rw_mode
69
80
  )
70
81
  return unless log_sampled
71
82
 
@@ -75,7 +86,8 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor # steep:ignore
75
86
  "queue_ttl => #{queue_ttl} " \
76
87
  "acq_id => '#{acquirer_id}' " \
77
88
  "hst_id => '#{host_id}' " \
78
- "acs_strat => '#{access_strategy}'"
89
+ "acs_strat => '#{access_strategy}' " \
90
+ "rw_mode => '#{rw_mode}'"
79
91
  end rescue nil
80
92
  end
81
93
  end
@@ -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.16.2
17
+ # @version 1.18.0
18
18
  def lock_series_poc( # steep:ignore
19
19
  redis,
20
20
  lock_names,
@@ -52,6 +52,9 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
52
52
  instr_sample_this:,
53
53
  &block
54
54
  )
55
+ # NOTE: (RW) the lock mode is the same for all locks of the series
56
+ # (see RedisQueuedLocks::Acquirer::AcquireLock for read_write_mode validations);
57
+
55
58
  case meta
56
59
  when Hash, NilClass then nil
57
60
  else
@@ -72,13 +75,14 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
72
75
  key == 'spc_cnt' ||
73
76
  key == 'l_spc_ext_ini_ttl' ||
74
77
  key == 'l_spc_ext_ts' ||
75
- key == 'l_spc_ts'
78
+ key == 'l_spc_ts' ||
79
+ key == 'rw_mode'
76
80
  end)
77
81
  raise(
78
82
  RedisQueuedLocks::ArgumentError,
79
83
  '`:meta` keys can not overlap reserved lock data keys ' \
80
84
  '"acq_id", "hst_id", "ts", "ini_ttl", "lock_key", "rem_ttl", "spc_cnt", ' \
81
- '"spc_ext_ttl", "l_spc_ext_ini_ttl", "l_spc_ext_ts", "l_spc_ts"'
85
+ '"spc_ext_ttl", "l_spc_ext_ini_ttl", "l_spc_ext_ts", "l_spc_ts", "rw_mode"'
82
86
  )
83
87
  end
84
88
 
@@ -128,13 +132,16 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
128
132
 
129
133
  RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor.start_lock_series_obtaining( # steep:ignore
130
134
  logger, log_sampled, lock_keys_for_instrumentation,
131
- queue_ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation, access_strategy
135
+ queue_ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation, access_strategy,
136
+ read_write_mode
132
137
  )
133
138
 
134
139
  acq_start_time = RedisQueuedLocks::Utilities.clock_gettime
135
140
  successfully_acquired_locks = [] # steep:ignore
136
141
  failed_locks_and_errors = {} # steep:ignore
137
142
  failed_on_lock = nil
143
+ # NOTE: locks of the failed series are released once (exceptional or not)
144
+ is_failed_series_released = false
138
145
 
139
146
  lock_acquirement_operations.map do |lock_operation_options|
140
147
  result =
@@ -179,17 +186,11 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
179
186
  RedisQueuedLocks::LockAcquirementTimeoutError,
180
187
  RedisQueuedLocks::LockAcquirementRetryLimitError,
181
188
  RedisQueuedLocks::ConflictLockObtainError => error
182
- if successfully_acquired_locks.any?
183
- # NOTE: release all previously acquired locks if any next lock is already locked
184
- successfully_acquired_locks.each do |operation_result|
185
- lock_key = RedisQueuedLocks::Resource.prepare_lock_key(operation_result[:lock_name])
186
- redis.with do |conn|
187
- conn.multi(watch: [lock_key]) do |transact|
188
- transact.call('DEL', lock_key)
189
- end
190
- end
191
- end
192
- end
189
+ # NOTE: release all previously acquired locks if any next lock is already locked
190
+ release_lock_series( # steep:ignore
191
+ redis, read_write_mode, successfully_acquired_locks, acquirer_id_for_instrumentation
192
+ )
193
+ is_failed_series_released = true
193
194
 
194
195
  raise(error) if raise_errors
195
196
  end
@@ -203,6 +204,13 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
203
204
  else
204
205
  failed_on_lock = lock_operation_options[:lock_name]
205
206
  failed_locks_and_errors[lock_operation_options[:lock_name]] = result
207
+ # NOTE: release all previously acquired locks (non-exceptional failure: raise_errors: false)
208
+ unless is_failed_series_released
209
+ release_lock_series( # steep:ignore
210
+ redis, read_write_mode, successfully_acquired_locks, acquirer_id_for_instrumentation
211
+ )
212
+ is_failed_series_released = true
213
+ end
206
214
  break
207
215
  end
208
216
  end
@@ -210,16 +218,16 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
210
218
  if (successfully_acquired_locks.size == lock_names.size && (successfully_acquired_locks.all? { |res| res[:ok] }))
211
219
  acq_end_time = RedisQueuedLocks::Utilities.clock_gettime
212
220
  acq_time = ((acq_end_time - acq_start_time) / 1_000.0).ceil(2)
213
- ts = Time.now.to_s
221
+ ts = Time.now.to_f
214
222
 
215
223
  RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor.lock_series_obtained( # steep:ignore
216
224
  logger, log_sampled, lock_keys_for_instrumentation,
217
225
  queue_ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation,
218
- acq_time, access_strategy
226
+ acq_time, access_strategy, read_write_mode
219
227
  )
220
228
 
221
229
  RedisQueuedLocks::Acquirer::LockSeriesPoC::InstrVisitor.lock_series_obtained( # steep:ignore
222
- instrumenter, instr_sampled, lock_keys_for_instrumentation,
230
+ instrumenter, instr_sampled, lock_keys_for_instrumentation, read_write_mode,
223
231
  ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation, ts, acq_time, instrument
224
232
  )
225
233
 
@@ -238,6 +246,11 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
238
246
  redis,
239
247
  logger,
240
248
  lock_keys_for_instrumentation.last,
249
+ RedisQueuedLocks::Resource.prepare_lock_readers(lock_names.last),
250
+ RedisQueuedLocks::Resource.prepare_read_lock_key(
251
+ lock_names.last, acquirer_id_for_instrumentation
252
+ ),
253
+ read_write_mode,
241
254
  acquirer_id_for_instrumentation,
242
255
  host_id_for_instrumentation,
243
256
  access_strategy,
@@ -257,14 +270,9 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
257
270
 
258
271
  # expire locks manually
259
272
  if block_given?
260
- redis.with do |conn|
261
- # use transaction in order to exclude any cross-locking during the group expiration
262
- conn.multi(watch: lock_keys_for_instrumentation) do |transaction|
263
- lock_keys_for_instrumentation.each do |lock_key|
264
- transaction.call('EXPIRE', lock_key, '0')
265
- end
266
- end
267
- end
273
+ release_lock_series( # steep:ignore
274
+ redis, read_write_mode, successfully_acquired_locks, acquirer_id_for_instrumentation
275
+ )
268
276
  is_lock_manually_released = true
269
277
  end
270
278
 
@@ -274,13 +282,15 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
274
282
 
275
283
  RedisQueuedLocks::Acquirer::LockSeriesPoC::LogVisitor.expire_lock_series( # steep:ignore
276
284
  logger, log_sampled, lock_keys_for_instrumentation,
277
- queue_ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation, access_strategy
285
+ queue_ttl, acquirer_id_for_instrumentation, host_id_for_instrumentation, access_strategy,
286
+ read_write_mode
278
287
  ) if is_lock_manually_released
279
288
 
280
289
  RedisQueuedLocks::Acquirer::LockSeriesPoC::InstrVisitor.lock_series_hold_and_release( # steep:ignore
281
290
  instrumenter,
282
291
  instr_sampled,
283
292
  lock_keys_for_instrumentation,
293
+ read_write_mode,
284
294
  ttl,
285
295
  acquirer_id_for_instrumentation,
286
296
  host_id_for_instrumentation,
@@ -321,6 +331,95 @@ module RedisQueuedLocks::Acquirer::LockSeriesPoC # steep:ignore
321
331
  }
322
332
  end
323
333
  end
334
+
335
+ private
336
+
337
+ # Releases locks of the series that are obtained by the series itself:
338
+ # - reentrant locks (already obtained by the outer logic of the current acquirer) are kept;
339
+ # - only locks of the current acquirer are released (owner check);
340
+ #
341
+ # @param redis [RedisClient]
342
+ # @param read_write_mode [Symbol]
343
+ # @param successfully_acquired_locks [Array<Hash<Symbol,Any>>]
344
+ # @param acquirer_id [String]
345
+ # @return [void]
346
+ #
347
+ # @api private
348
+ # @since 1.18.0
349
+ def release_lock_series(redis, read_write_mode, successfully_acquired_locks, acquirer_id) # steep:ignore
350
+ lock_names = obtained_lock_names(successfully_acquired_locks) # steep:ignore
351
+
352
+ if read_write_mode == :read
353
+ release_read_lock_series(redis, lock_names, acquirer_id) # steep:ignore
354
+ else
355
+ release_write_lock_series(redis, lock_names, acquirer_id) # steep:ignore
356
+ end
357
+ end
358
+
359
+ # 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).
361
+ #
362
+ # @param redis [RedisClient]
363
+ # @param lock_names [Array<String>]
364
+ # @param acquirer_id [String]
365
+ # @return [void]
366
+ #
367
+ # @api private
368
+ # @since 1.18.0
369
+ def release_write_lock_series(redis, lock_names, acquirer_id) # steep:ignore
370
+ return if lock_names.empty?
371
+
372
+ 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)
378
+ end
379
+ end
380
+ end
381
+ end
382
+ end
383
+
384
+ # Locks of the series that are obtained by the series itself (reentrant locks are obtained
385
+ # by the outer logic of the current acquirer and should be released by it).
386
+ #
387
+ # @param successfully_acquired_locks [Array<Hash<Symbol,Any>>]
388
+ # @return [Array<String>]
389
+ #
390
+ # @api private
391
+ # @since 1.18.0
392
+ def obtained_lock_names(successfully_acquired_locks) # steep:ignore
393
+ successfully_acquired_locks.filter_map do |operation_result|
394
+ operation_result[:lock_name] if operation_result[:result][:process] == :lock_obtaining
395
+ end
396
+ end
397
+
398
+ # Releases read locks of the given acquirer (other readers are not affected).
399
+ #
400
+ # @param redis [RedisClient]
401
+ # @param lock_names [Array<String>]
402
+ # @param acquirer_id [String]
403
+ # @return [void]
404
+ #
405
+ # @api private
406
+ # @since 1.18.0
407
+ def release_read_lock_series(redis, lock_names, acquirer_id) # steep:ignore
408
+ return if lock_names.empty?
409
+
410
+ redis.with do |conn|
411
+ conn.multi do |transaction|
412
+ lock_names.each do |lock_name|
413
+ transaction.call(
414
+ 'ZREM', RedisQueuedLocks::Resource.prepare_lock_readers(lock_name), acquirer_id
415
+ )
416
+ transaction.call(
417
+ 'DEL', RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
418
+ )
419
+ end
420
+ end
421
+ end
422
+ end
324
423
  end
325
424
  end
326
425
  # rubocop:enable all