redis_queued_locks 1.21.0 → 1.23.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/project-overview.md +294 -51
  3. data/.claude/rules/acquirer.md +16 -11
  4. data/.claude/rules/arguments.md +2 -2
  5. data/.claude/rules/changelog.md +27 -0
  6. data/.claude/rules/git.md +1 -1
  7. data/.claude/rules/logic.md +14 -15
  8. data/.claude/rules/swarm.md +3 -4
  9. data/.claude/rules/tests.md +9 -9
  10. data/.claude/rules/type-checking.md +14 -16
  11. data/.claude/rules/visitors.md +11 -9
  12. data/.claude/skills/rql-code-review/SKILL.md +129 -47
  13. data/.ruby-version +1 -1
  14. data/CHANGELOG.md +122 -245
  15. data/CLAUDE.md +12 -10
  16. data/README.md +516 -175
  17. data/Rakefile +11 -0
  18. data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rb +4 -4
  19. data/lib/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rb +28 -12
  20. data/lib/redis_queued_locks/acquirer/acquire_lock/log_visitor.rb +38 -16
  21. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rb +92 -15
  22. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +945 -272
  23. data/lib/redis_queued_locks/acquirer/acquire_lock.rb +82 -32
  24. data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +240 -45
  25. data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +15 -13
  26. data/lib/redis_queued_locks/acquirer/keys.rb +6 -2
  27. data/lib/redis_queued_locks/acquirer/lock_series/instr_visitor.rb +243 -0
  28. data/lib/redis_queued_locks/acquirer/lock_series/log_visitor.rb +472 -0
  29. data/lib/redis_queued_locks/acquirer/lock_series/release_lock_series.rb +181 -0
  30. data/lib/redis_queued_locks/acquirer/lock_series/try_to_lock_series.rb +1051 -0
  31. data/lib/redis_queued_locks/acquirer/lock_series.rb +948 -0
  32. data/lib/redis_queued_locks/acquirer/locks.rb +6 -2
  33. data/lib/redis_queued_locks/acquirer/queue_info.rb +30 -7
  34. data/lib/redis_queued_locks/acquirer/queues.rb +17 -6
  35. data/lib/redis_queued_locks/acquirer/release_all_locks.rb +6 -2
  36. data/lib/redis_queued_locks/acquirer/release_locks_of.rb +6 -2
  37. data/lib/redis_queued_locks/acquirer.rb +1 -1
  38. data/lib/redis_queued_locks/client.rb +116 -44
  39. data/lib/redis_queued_locks/config.rb +93 -1
  40. data/lib/redis_queued_locks/resource.rb +124 -9
  41. data/lib/redis_queued_locks/swarm/swarm_element/threaded.rb +6 -5
  42. data/lib/redis_queued_locks/swarm/zombie_info.rb +6 -2
  43. data/lib/redis_queued_locks/version.rb +2 -2
  44. data/rbs_collection.lock.yaml +4 -0
  45. data/rbs_collection.yaml +2 -1
  46. data/redis_queued_locks.gemspec +21 -14
  47. data/sig/manifest.yml +1 -0
  48. data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rbs +2 -2
  49. data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rbs +2 -2
  50. data/sig/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rbs +12 -6
  51. data/sig/redis_queued_locks/acquirer/acquire_lock/log_visitor.rbs +21 -14
  52. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rbs +61 -40
  53. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +160 -11
  54. data/sig/redis_queued_locks/acquirer/acquire_lock/{yield_with_expire → yield_expire}/log_visitor.rbs +4 -4
  55. data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +2 -2
  56. data/sig/redis_queued_locks/acquirer/acquire_lock.rbs +5 -3
  57. data/sig/redis_queued_locks/acquirer/clear_dead_requests.rbs +14 -1
  58. data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +2 -2
  59. data/sig/redis_queued_locks/acquirer/lock_series/instr_visitor.rbs +102 -0
  60. data/sig/redis_queued_locks/acquirer/lock_series/log_visitor.rbs +154 -0
  61. data/sig/redis_queued_locks/acquirer/lock_series/release_lock_series.rbs +37 -0
  62. data/sig/redis_queued_locks/acquirer/lock_series/try_to_lock_series.rbs +198 -0
  63. data/sig/redis_queued_locks/acquirer/lock_series.rbs +106 -0
  64. data/sig/redis_queued_locks/acquirer/queues.rbs +1 -1
  65. data/sig/redis_queued_locks/client.rbs +77 -7
  66. data/sig/redis_queued_locks/config.rbs +54 -1
  67. data/sig/redis_queued_locks/instrument/sampler.rbs +3 -1
  68. data/sig/redis_queued_locks/instrument/void_notifier.rbs +3 -1
  69. data/sig/redis_queued_locks/logging/sampler.rbs +3 -1
  70. data/sig/redis_queued_locks/resource.rbs +22 -12
  71. data/sig/redis_queued_locks/swarm/swarm_element/threaded.rbs +1 -1
  72. data/sig/redis_queued_locks/utilities.rbs +2 -1
  73. data/sig/redis_queued_locks.rbs +7 -0
  74. metadata +35 -13
  75. data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +0 -56
  76. data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +0 -95
  77. data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +0 -484
  78. /data/sig/redis_queued_locks/{acquier.rbs → acquirer.rbs} +0 -0
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Releases the lock series: only the locks that are obtained by the series itself and only if they
4
+ # are still owned by the current acquirer; the reentrant locks (obtained by the outer logic of
5
+ # the current acquirer) are kept and the extended ones get their ttl back.
6
+ #
7
+ # @api private
8
+ # @since 1.23.0
9
+ module RedisQueuedLocks::Acquirer::LockSeries::ReleaseLockSeries
10
+ # @param redis [RedisClient]
11
+ # @param lock_names [Array<String>]
12
+ # @param lock_processes [Array<Symbol,NilClass>]
13
+ # - The way each lock of the series is held (the order of `lock_names`):
14
+ # - `:lock_obtaining` - obtained by the series itself (released);
15
+ # - `:extendable_conflict_work_through` - extended reentrant lock (its ttl is decreased);
16
+ # - `:conflict_work_through` - reentrant lock (kept as is);
17
+ # @param held_rw_modes [Array<Symbol>]
18
+ # The mode of the lock that is really held by the acquirer (the order of `lock_names`).
19
+ # @param acquirer_id [String]
20
+ # @param decreased_ttl [Integer]
21
+ # The ttl to take back from the extended reentrant locks (in milliseconds): the extension
22
+ # without the time spent under the locks; a non-positive value keeps these locks as is.
23
+ # @return [void]
24
+ #
25
+ # @api private
26
+ # @since 1.23.0
27
+ def release_lock_series(
28
+ redis,
29
+ lock_names,
30
+ lock_processes,
31
+ held_rw_modes,
32
+ acquirer_id,
33
+ decreased_ttl
34
+ )
35
+ # @type var write_lock_names: Array[String]
36
+ write_lock_names = []
37
+ # @type var read_lock_names: Array[String]
38
+ read_lock_names = []
39
+ # @type var extended_write_lock_names: Array[String]
40
+ extended_write_lock_names = []
41
+ # @type var extended_read_lock_names: Array[String]
42
+ extended_read_lock_names = []
43
+
44
+ lock_names.each_with_index do |lock_name, index|
45
+ read_lock = held_rw_modes[index] == :read
46
+
47
+ case lock_processes[index]
48
+ when :lock_obtaining
49
+ (read_lock ? read_lock_names : write_lock_names) << lock_name
50
+ when :extendable_conflict_work_through
51
+ (read_lock ? extended_read_lock_names : extended_write_lock_names) << lock_name
52
+ end
53
+ end
54
+
55
+ # NOTE: a non-positive decreased ttl: the spent time exceeds the extension
56
+ if decreased_ttl <= 0
57
+ extended_read_lock_names.clear
58
+ extended_write_lock_names.clear
59
+ end
60
+
61
+ release_write_lock_series(
62
+ redis, write_lock_names, extended_write_lock_names, acquirer_id, decreased_ttl
63
+ )
64
+ release_read_lock_series(
65
+ redis, read_lock_names, extended_read_lock_names, acquirer_id, decreased_ttl
66
+ )
67
+ end
68
+
69
+ private
70
+
71
+ # Releases the write locks (and decreases the ttl of the extended ones) of the given acquirer
72
+ # (compare-and-delete: the lock can be expired and obtained by another acquirer when the series
73
+ # logic exceeds the lock ttl) in one transaction for the whole series (three round trips: WATCH,
74
+ # lock owners, MULTI/EXEC):
75
+ # - an aborted transaction (one of the watched locks is changed) is retried with the locks
76
+ # that were still owned by the acquirer at the last check: the locks of other acquirers
77
+ # are neither touched nor watched, so the retries converge;
78
+ #
79
+ # @param redis [RedisClient]
80
+ # @param lock_names [Array<String>] The locks to release.
81
+ # @param extended_lock_names [Array<String>] The extended reentrant locks.
82
+ # @param acquirer_id [String]
83
+ # @param decreased_ttl [Integer] In milliseconds.
84
+ # @return [void]
85
+ #
86
+ # @api private
87
+ # @since 1.23.0
88
+ # rubocop:disable Metrics/MethodLength
89
+ def release_write_lock_series(redis, lock_names, extended_lock_names, acquirer_id, decreased_ttl)
90
+ return if lock_names.empty? && extended_lock_names.empty?
91
+
92
+ own_lock_keys = lock_names.map do |lock_name|
93
+ RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
94
+ end
95
+ own_extended_lock_keys = extended_lock_names.map do |lock_name|
96
+ RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
97
+ end
98
+
99
+ redis.with do |conn|
100
+ until own_lock_keys.empty? && own_extended_lock_keys.empty?
101
+ released = conn.multi(watch: own_lock_keys + own_extended_lock_keys) do |transaction|
102
+ # NOTE: (pipeline layout) one reply (lock owner) per lock: the released locks first
103
+ # @type var lock_owners: Array[String?]
104
+ lock_owners = conn.pipelined do |pipeline|
105
+ own_lock_keys.each { |lock_key| pipeline.call('HGET', lock_key, 'acq_id') }
106
+ own_extended_lock_keys.each { |lock_key| pipeline.call('HGET', lock_key, 'acq_id') }
107
+ end
108
+ extended_locks_at = own_lock_keys.size
109
+ own_lock_keys = own_lock_keys.select.with_index do |_lock_key, index|
110
+ lock_owners[index] == acquirer_id
111
+ end
112
+ own_extended_lock_keys = own_extended_lock_keys.select.with_index do |_lock_key, index|
113
+ lock_owners[extended_locks_at + index] == acquirer_id
114
+ end
115
+ own_lock_keys.each { |lock_key| transaction.call('DEL', lock_key) }
116
+ # NOTE: EVAL signature -> <lua script>, (number of keys), *(keys), *(arguments)
117
+ own_extended_lock_keys.each do |lock_key|
118
+ transaction.call(
119
+ 'EVAL',
120
+ RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire::DECREASE_LOCK_PTTL,
121
+ 1,
122
+ lock_key,
123
+ decreased_ttl
124
+ )
125
+ end
126
+ end
127
+ # NOTE: `nil` => the transaction is aborted (a watched lock is changed) => retry
128
+ break unless released == nil
129
+ end
130
+ rescue RedisQueuedLocks::LockAcquirementIntermediateTimeoutError,
131
+ RedisQueuedLocks::TimedLockIntermediateTimeoutError,
132
+ ::Timeout::ExitException
133
+ # NOTE: (timeouts) an interrupted WATCH block (the internal timeouts and the user's
134
+ # `Timeout.timeout` without an exception class): the closed connection drops the WATCH
135
+ # state (redis-client sends `UNWATCH` only on `StandardError`);
136
+ conn.close
137
+ raise
138
+ end
139
+ end
140
+ # rubocop:enable Metrics/MethodLength
141
+
142
+ # Releases the read locks (and decreases the extended ones) of the given acquirer in one
143
+ # transaction (other readers are not affected).
144
+ #
145
+ # @param redis [RedisClient]
146
+ # @param lock_names [Array<String>] The locks to release.
147
+ # @param extended_lock_names [Array<String>] The extended reentrant locks.
148
+ # @param acquirer_id [String]
149
+ # @param decreased_ttl [Integer] In milliseconds.
150
+ # @return [void]
151
+ #
152
+ # @api private
153
+ # @since 1.23.0
154
+ def release_read_lock_series(redis, lock_names, extended_lock_names, acquirer_id, decreased_ttl)
155
+ return if lock_names.empty? && extended_lock_names.empty?
156
+
157
+ redis.multi do |transaction|
158
+ lock_names.each do |lock_name|
159
+ transaction.call(
160
+ 'ZREM', RedisQueuedLocks::Resource.prepare_lock_readers(lock_name), acquirer_id
161
+ )
162
+ transaction.call(
163
+ 'DEL', RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
164
+ )
165
+ end
166
+ # NOTE:
167
+ # - XX => do nothing if the read lock is already expired and removed from the registry;
168
+ # - the read lock data TTL is not decreased (it is not used without the read lock);
169
+ extended_lock_names.each do |lock_name|
170
+ transaction.call(
171
+ 'ZADD',
172
+ RedisQueuedLocks::Resource.prepare_lock_readers(lock_name),
173
+ 'XX',
174
+ 'INCR',
175
+ -decreased_ttl,
176
+ acquirer_id
177
+ )
178
+ end
179
+ end
180
+ end
181
+ end