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
@@ -5,9 +5,10 @@
5
5
  # - `:read` lock is shared: it is obtained when there is no write lock;
6
6
  # - read locks are stored in the readers registry: a sorted set where the member is an acquirer
7
7
  # id and the score is the read lock expiration time (redis server time, in milliseconds);
8
- # - each read lock has its own data hash (the same structure as the write lock: acquirer, host,
9
- # timestamp, ttl, meta, reentrant lock data) with the read lock ttl; the readers registry is
10
- # the only source of truth for the read lock existence (the data hash is a data carrier only);
8
+ # - each read lock has its own data hash (the same structure as the write lock: timestamp, ttl,
9
+ # meta, reentrant lock data; acquirer and host are derived from the acquirer id and are not
10
+ # stored) with the read lock ttl; the readers registry is the only source of truth for
11
+ # the read lock existence (the data hash is a data carrier only);
11
12
  # - mutual exclusion relies on the optimistic transactions only (WATCH/MULTI):
12
13
  # - readers WATCH the write lock key: a new write lock invalidates the reader's transaction;
13
14
  # - writers WATCH the write lock key and the readers registry: a new read lock invalidates
@@ -21,9 +22,24 @@
21
22
  # - `:random` access strategy ignores both queues (no ordering between modes, so
22
23
  # a continuous flow of readers can starve writers);
23
24
  #
25
+ # Lock index (`RedisQueuedLocks::Resource::LOCK_INDEX_KEY`): only with the enabled lock index
26
+ # (`lock_index_enabled`, `config['lock_index_enabled']`); with the disabled lock index (default)
27
+ # no lock index commands are sent and lock request queues have no TTL:
28
+ # - every command that creates a lock key is followed by the lock index update in the same
29
+ # pipeline/transaction: the lock name score is the upper bound of the key lifetime (`ZADD GT
30
+ # <client time + ttl + grace period>`): the request queue in the queueing pipeline, the lock
31
+ # (write lock / readers registry) in the obtaining transaction (atomically: the lock index
32
+ # entry can be dropped by `#unlock`/`#clear_locks` between the queueing and the obtaining);
33
+ # - lock extensions (reentrant `:extendable_work_through`) increase the score by the extension
34
+ # in the same transaction (`ZADD XX INCR`);
35
+ # - lock request queues live as long as the latest lock request (`PEXPIRE NX` + `GT` with
36
+ # the queue ttl on each queueing) so the lifetime of every lock key is bounded;
37
+ # - each queueing drops the expired lock names from the index (`ZREMRANGEBYSCORE`): the index
38
+ # is maintained by the lock acquirement itself (no background jobs);
39
+ #
24
40
  # @api private
25
41
  # @since 1.0.0
26
- # @version 1.18.0
42
+ # @version 1.20.0
27
43
  # rubocop:disable Metrics/ModuleLength
28
44
  module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
29
45
  require_relative 'try_to_lock/log_visitor'
@@ -37,6 +53,73 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
37
53
  return redis.call("PEXPIRE", KEYS[1], new_lock_pttl);
38
54
  LUA_SCRIPT
39
55
 
56
+ # Lock data for the try-lock log events when the lock data is not extracted
57
+ # (logs are disabled): shared frozen object instead of a new hash for each failed attempt.
58
+ #
59
+ # NOTE: (steep) an empty hash literal with a method call can't be annotated inline
60
+ # (the constant type is declared in RBS);
61
+ #
62
+ # @return [Hash]
63
+ #
64
+ # @api private
65
+ # @since 1.20.0
66
+ NO_LOCK_DATA = {}.freeze # steep:ignore
67
+
68
+ # Position of the write lock obtainer (`HGET acq_id`) in the lock state pipeline
69
+ # (see "pipeline layout" in `pipeline_lock_state`).
70
+ #
71
+ # @return [Integer]
72
+ #
73
+ # @api private
74
+ # @since 1.20.0
75
+ LOCK_STATE_OBTAINER_REPLY = 0
76
+
77
+ # Position of the redis server time (`TIME`) in the lock state pipeline
78
+ # (see "pipeline layout" in `pipeline_lock_state`).
79
+ #
80
+ # @return [Integer]
81
+ #
82
+ # @api private
83
+ # @since 1.20.0
84
+ LOCK_STATE_REDIS_TIME_REPLY = 1
85
+
86
+ # Position of the read lock expiration of the current acquirer (`ZSCORE`) in the lock state
87
+ # pipeline (see "pipeline layout" in `pipeline_lock_state`).
88
+ #
89
+ # @return [Integer]
90
+ #
91
+ # @api private
92
+ # @since 1.20.0
93
+ LOCK_STATE_OWN_READ_LOCK_REPLY = 2
94
+
95
+ # Position of the first reply of the lock request queueing commands (`queue_lock_request`)
96
+ # in the lock state pipeline (see "pipeline layout" in `pipeline_lock_state`).
97
+ #
98
+ # @return [Integer]
99
+ #
100
+ # @api private
101
+ # @since 1.20.0
102
+ LOCK_STATE_QUEUE_REPLIES_AT = 3
103
+
104
+ # Position of the first request of the request queue (`ZRANGE`) in the replies of the lock
105
+ # request queueing commands (`queue_lock_request`), relative to their first reply.
106
+ #
107
+ # @return [Integer]
108
+ #
109
+ # @api private
110
+ # @since 1.20.0
111
+ QUEUE_HEAD_REPLY = 3
112
+
113
+ # Position of the first request of the opposite request queue (`ZRANGE WITHSCORES`) in the
114
+ # replies of the lock request queueing commands (`queue_lock_request`), relative to their
115
+ # first reply.
116
+ #
117
+ # @return [Integer]
118
+ #
119
+ # @api private
120
+ # @since 1.20.0
121
+ OPPOSITE_QUEUE_HEAD_REPLY = 4
122
+
40
123
  # @param redis [RedisClient]
41
124
  # @param logger [::Logger,#debug]
42
125
  # @param log_lock_try [Boolean]
@@ -45,23 +128,33 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
45
128
  # @param lock_key_queue [String] Queue of write lock requests.
46
129
  # @param read_lock_key_queue [String] Queue of read lock requests.
47
130
  # @param lock_readers_key [String] Registry of read lock holders.
48
- # @param read_lock_key [String] Read lock data of the current acquirer.
131
+ # @param read_lock_key [String,NilClass]
132
+ # Read lock data of the current acquirer (`nil` for write lock requests: they never obtain
133
+ # read locks).
134
+ # @param lock_name [String] The lock name (the member of the lock index).
49
135
  # @param acquirer_id [String]
50
136
  # @param host_id [String]
51
137
  # @param acquirer_position [Numeric]
52
138
  # @param ttl [Integer]
53
139
  # @param queue_ttl [Integer]
140
+ # @param lock_index_enabled [Boolean]
141
+ # Maintain the lock index (the lock index commands and the lock request queue TTL).
142
+ # @param lock_index_grace_period [Integer]
143
+ # The lock index score margin (in milliseconds) over the key lifetime (client clock skew and
144
+ # network latency tolerance).
54
145
  # @param fail_fast [Boolean]
55
146
  # @param conflict_strategy [Symbol]
56
147
  # @param access_strategy [Symbol]
57
148
  # @param meta [NilClass,Hash<String|Symbol,Any>]
149
+ # @param retry_attempt [Boolean]
150
+ # The attempt is a retry (the previous attempt of the acquirement did not obtain the lock).
58
151
  # @param log_sampled [Boolean]
59
152
  # @param instr_sampled [Boolean]
60
153
  # @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Symbol|Hash<Symbol,Any> }
61
154
  #
62
155
  # @api private
63
156
  # @since 1.0.0
64
- # @version 1.18.0
157
+ # @version 1.20.0
65
158
  # rubocop:disable Metrics/MethodLength
66
159
  def try_to_lock(
67
160
  redis,
@@ -73,15 +166,19 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
73
166
  read_lock_key_queue,
74
167
  lock_readers_key,
75
168
  read_lock_key,
169
+ lock_name,
76
170
  acquirer_id,
77
171
  host_id,
78
172
  acquirer_position,
79
173
  ttl,
80
174
  queue_ttl,
175
+ lock_index_enabled,
176
+ lock_index_grace_period,
81
177
  fail_fast,
82
178
  conflict_strategy,
83
179
  access_strategy,
84
180
  meta,
181
+ retry_attempt,
85
182
  log_sampled,
86
183
  instr_sampled
87
184
  )
@@ -98,6 +195,23 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
98
195
  held_rw_mode = read_write_mode
99
196
  # Step RW: read lock request flag
100
197
  read_mode = (read_write_mode == :read)
198
+ # Step RW: request queue of the current mode and the queue of the opposite mode
199
+ request_queue = read_mode ? read_lock_key_queue : lock_key_queue
200
+ opposite_request_queue = read_mode ? lock_key_queue : read_lock_key_queue
201
+ # Step 2.1 (prepare): requests queued before this score are expired (in both queues)
202
+ acquirer_dead_score = RedisQueuedLocks::Resource.acquirer_dead_score(queue_ttl)
203
+ # Step X: eager queueing - the lock request is queued in the lock state round trip (Step 0.1)
204
+ # instead of a separate round trip in the classic logic (Steps 1-3). Only when the attempt
205
+ # always reaches the classic logic or dequeues the request after the failure:
206
+ # - `:wait_for_lock`: same-process conflicts are processed in the classic way;
207
+ # - `:dead_locking`: same-process conflicts fail the acquirement (the request is dequeued);
208
+ # - not for `fail_fast` (the failed "no try" attempt does not dequeue the request) and not for
209
+ # `:work_through`/`:extendable_work_through` (same-process conflicts exit without queueing);
210
+ eager_queueing =
211
+ !fail_fast && (conflict_strategy == :wait_for_lock || conflict_strategy == :dead_locking)
212
+ # Step X: pre-check - the lock state is checked without WATCH first (see Step PRE): `fail_fast`
213
+ # attempts (they fail on the obtained lock) and retries (the lock was not obtained yet);
214
+ precheck = fail_fast || retry_attempt
101
215
 
102
216
  LogVisitor.start(
103
217
  logger, log_sampled, log_lock_try, lock_key,
@@ -111,6 +225,21 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
111
225
  queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
112
226
  )
113
227
 
228
+ # Step PRE: (pre-check) the lock state without WATCH: the attempt that can not obtain
229
+ # the lock fails in one round trip (no WATCH, lock state and MULTI/EXEC round trips);
230
+ # - the pre-check never obtains the lock: the attempt that can obtain the lock continues
231
+ # with the WATCH/MULTI logic below (the lock state is read again after WATCH);
232
+ # - see `precheck_lock` for details;
233
+ if precheck
234
+ inter_result = precheck_lock(
235
+ rconn, logger, log_sampled, log_lock_try, lock_key, read_write_mode, lock_readers_key,
236
+ request_queue, opposite_request_queue, lock_name, acquirer_id, host_id,
237
+ acquirer_position, acquirer_dead_score, queue_ttl, lock_index_enabled,
238
+ lock_index_grace_period, fail_fast, access_strategy
239
+ )
240
+ next if inter_result
241
+ end
242
+
114
243
  # Step 0:
115
244
  # watch the lock key changes (and discard acquirement if lock is already
116
245
  # obtained by another acquirer during the current lock acquiremntt)
@@ -124,23 +253,33 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
124
253
  # - current write lock obtainer;
125
254
  # - redis server time (read lock expiration is calculated in redis time);
126
255
  # - read lock expiration of the current acquirer;
127
- # - the longest read lock (the last one in the readers registry);
128
- # @type var lock_state: [String?, Array[String], Float?, Array[[String, Float]]]
256
+ # - the longest read lock (the last one in the readers registry): write lock requests
257
+ # only (read lock requests do not wait for other readers);
258
+ # - (eager queueing) the lock request queueing: Steps 1, 2.1, 3 (`queue_lock_request`);
259
+ # NOTE: replies are read by their positions: see "pipeline layout" in `pipeline_lock_state`
260
+ # @type var lock_state: Array[untyped]
129
261
  lock_state = rconn.pipelined do |pipeline|
130
- pipeline.call('HGET', lock_key, 'acq_id')
131
- pipeline.call('TIME')
132
- pipeline.call('ZSCORE', lock_readers_key, acquirer_id)
133
- pipeline.call('ZRANGE', lock_readers_key, '-1', '-1', 'WITHSCORES')
262
+ pipeline_lock_state(
263
+ pipeline, lock_key, lock_readers_key, acquirer_id, read_mode, eager_queueing,
264
+ request_queue, opposite_request_queue, acquirer_position, acquirer_dead_score,
265
+ lock_name, queue_ttl, lock_index_enabled, lock_index_grace_period
266
+ )
134
267
  end
135
- current_lock_obtainer = lock_state[0]
136
- redis_time = RedisQueuedLocks::Resource.redis_time_ms(lock_state[1])
137
- own_read_lock_expiration = lock_state[2]
138
- longest_read_lock = lock_state[3].first
268
+ # @type var current_lock_obtainer: String?
269
+ current_lock_obtainer = lock_state[LOCK_STATE_OBTAINER_REPLY]
270
+ redis_time = RedisQueuedLocks::Resource.redis_time_ms(lock_state[LOCK_STATE_REDIS_TIME_REPLY])
271
+ # @type var own_read_lock_expiration: Float?
272
+ own_read_lock_expiration = lock_state[LOCK_STATE_OWN_READ_LOCK_REPLY]
273
+ # NOTE: the write-only tail is the last reply of write lock requests only (read lock
274
+ # requests have no such reply: never read it without the condition of the pipeline)
275
+ # @type var longest_read_lock: [String, Float]?
276
+ longest_read_lock = read_mode ? nil : lock_state.last.first
139
277
  # NOTE:
140
278
  # - expired read locks are ignored (they can live in the registry till the cleanup);
141
279
  # - nil expiration (no read lock) is converted to 0.0 (always expired);
280
+ # - `read_locked` is used by write lock requests only (always false for read requests);
142
281
  read_locked_by_acquirer = own_read_lock_expiration.to_f > redis_time
143
- read_locked = longest_read_lock != nil && longest_read_lock[1] > redis_time
282
+ read_locked = longest_read_lock ? longest_read_lock[1] > redis_time : false
144
283
 
145
284
  # SP-Conflict status PREPARING: the mode of the lock (already obtained by the current
146
285
  # acquirer) that the current lock request conflicts with;
@@ -254,6 +393,18 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
254
393
  transact.call('PEXPIRE', read_lock_key, extended_read_lock_ttl, 'NX')
255
394
  transact.call('PEXPIRE', read_lock_key, extended_read_lock_ttl, 'GT')
256
395
  end
396
+ # SPCF Step 5: (lock index) the lock (write or read) lives longer by the extension:
397
+ # - the lock index score is increased by the extension (the upper bound of the lock
398
+ # lifetime stays the upper bound);
399
+ # - XX => locks without the index entry (of older RQL versions) are not indexed with
400
+ # a wrong score (see `reindex_locks`);
401
+ # - [REDIS RESULT]: the new lock index score (nil for the lock without the index entry);
402
+ # - only with the enabled lock index (the last command of the transaction);
403
+ if lock_index_enabled
404
+ transact.call(
405
+ 'ZADD', RedisQueuedLocks::Resource::LOCK_INDEX_KEY, 'XX', 'INCR', ttl, lock_name
406
+ )
407
+ end
257
408
  inter_result = :extendable_conflict_work_through
258
409
 
259
410
  # @type var spc_processed_timestamp: Float
@@ -342,131 +493,65 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
342
493
  # Fast-Step X1: lock is already obtained. fail fast leads to "no try".
343
494
  inter_result = :fail_fast_no_try
344
495
  else
345
- # Step RW: request queue of the current mode and the queue of the opposite mode
346
- request_queue = read_mode ? read_lock_key_queue : lock_key_queue
347
- opposite_request_queue = read_mode ? lock_key_queue : read_lock_key_queue
348
-
349
496
  # Step 1: add an acquirer to the lock acquirement queue
350
- # NOTE:
351
- # 'NX' means "Only add new elements. Don't update already existing elements."
352
- # that works as:
353
- # 1. (enqueue) <<if you are already in the queue - do nothing and wait your time>>
354
- # 2. (requeue) or <<add to the right pre-calculated position if you are
355
- # not in the queue now>>;
356
- rconn.call('ZADD', request_queue, 'NX', acquirer_position, acquirer_id)
497
+ # Step 2.1: drop expired acquirers from the lock queues
498
+ # Step 3: get the actual acquirer waiting in the queue
499
+ # - one round trip for all of them (see `queue_lock_request`);
500
+ # - eager queueing: already done in the lock state round trip (Step 0.1);
501
+ # @type var queue_replies: Array[untyped]
502
+ queue_replies =
503
+ if eager_queueing
504
+ lock_state
505
+ else
506
+ rconn.pipelined do |pipeline|
507
+ queue_lock_request(
508
+ pipeline, request_queue, opposite_request_queue,
509
+ acquirer_position, acquirer_id, acquirer_dead_score,
510
+ lock_name, queue_ttl, lock_index_enabled, lock_index_grace_period
511
+ )
512
+ end
513
+ end
514
+ # NOTE: the position of the first queueing reply (see "pipeline layout" in Step 0.1)
515
+ queue_replies_at = eager_queueing ? LOCK_STATE_QUEUE_REPLIES_AT : 0
516
+ # @type var waiting_acquirer: String?
517
+ waiting_acquirer = queue_replies[queue_replies_at + QUEUE_HEAD_REPLY].first
518
+ # @type var opposite_waiting_request: [String, Float]?
519
+ opposite_waiting_request =
520
+ queue_replies[queue_replies_at + OPPOSITE_QUEUE_HEAD_REPLY].first
357
521
 
358
522
  LogVisitor.acq_added_to_queue(
359
523
  logger, log_sampled, log_lock_try, lock_key,
360
524
  queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
361
525
  )
362
-
363
- # Step 2.1: drop expired acquirers from the lock queues
364
- # - (RW) both queues are cleared: an expired request of the opposite mode should not
365
- # block the current request forever;
366
- acquirer_dead_score = RedisQueuedLocks::Resource.acquirer_dead_score(queue_ttl)
367
- rconn.pipelined do |pipeline|
368
- pipeline.call('ZREMRANGEBYSCORE', request_queue, '-inf', acquirer_dead_score)
369
- pipeline.call('ZREMRANGEBYSCORE', opposite_request_queue, '-inf', acquirer_dead_score)
370
- end
371
-
372
526
  LogVisitor.remove_expired_acqs(
373
527
  logger, log_sampled, log_lock_try, lock_key,
374
528
  queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
375
529
  )
376
-
377
- # Step 3: get the actual acquirer waiting in the queue
378
- # - (RW) and the first request of the opposite mode;
379
- # @type var queue_heads: [Array[String], Array[[String, Float]]]
380
- queue_heads = rconn.pipelined do |pipeline|
381
- pipeline.call('ZRANGE', request_queue, '0', '0')
382
- pipeline.call('ZRANGE', opposite_request_queue, '0', '0', 'WITHSCORES')
383
- end
384
- waiting_acquirer = queue_heads[0].first
385
- opposite_waiting_request = queue_heads[1].first
386
-
387
530
  LogVisitor.get_first_from_queue(
388
531
  logger, log_sampled, log_lock_try, lock_key,
389
532
  queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode, waiting_acquirer
390
533
  )
391
534
 
392
- # Step 3.1 (RW): the opposite mode request that should obtain the lock earlier
393
- # - FIFO between modes (`:queued` access strategy only);
394
- # - equal positions are ordered by acquirer ids;
535
+ # Step 3.1 (RW) + Steps PRE-4.x, ROLLBACK 1.1, 1.2, 2, 3: the obstacle to the lock
536
+ # obtaining (the same decision and logs as in the pre-check: `lock_obstacle`,
537
+ # `log_lock_obstacle`);
395
538
  # @type var opposite_request_ahead: String?
396
- opposite_request_ahead =
397
- if access_strategy == :queued && opposite_waiting_request != nil &&
398
- (opposite_waiting_request[1] < acquirer_position ||
399
- (opposite_waiting_request[1] == acquirer_position &&
400
- opposite_waiting_request[0] < acquirer_id))
401
- opposite_waiting_request[0]
402
- end
403
-
404
- # Step PRE-4.x: check if the request time limit is reached
405
- # (when the current try self-removes itself from queue (queue ttl has come))
406
- if waiting_acquirer == nil
407
- LogVisitor.exit__queue_ttl_reached(
408
- logger, log_sampled, log_lock_try, lock_key,
409
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
410
- )
411
-
412
- inter_result = :dead_score_reached
413
- # Step STRATEGY: check the stragegy and corresponding preventing factor
414
- # Step STRATEGY (queued): check the actual acquirer: is it ours? are we aready to lock?
415
- # - (RW) write lock requests are processed one by one;
416
- # - (RW) read lock requests are processed in parallel;
417
- elsif !read_mode && access_strategy == :queued && waiting_acquirer != acquirer_id
418
- # Step ROLLBACK 1.1: our time hasn't come yet. retry!
539
+ opposite_request_ahead = find_opposite_request_ahead(
540
+ access_strategy, opposite_waiting_request, acquirer_position, acquirer_id
541
+ )
542
+ obstacle = lock_obstacle(
543
+ read_mode, access_strategy, acquirer_id, waiting_acquirer, opposite_request_ahead,
544
+ current_lock_obtainer, (read_mode ? read_locked_by_acquirer : read_locked)
545
+ )
419
546
 
420
- LogVisitor.exit__no_first(
421
- logger, log_sampled, log_lock_try, lock_key,
422
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode, waiting_acquirer,
423
- # NOTE: lock data is extracted for logs only
424
- ((log_sampled && log_lock_try) ? rconn.call('HGETALL', lock_key).to_h : {})
425
- )
426
- inter_result = :acquirer_is_not_first_in_queue
427
- elsif opposite_request_ahead
428
- # Step ROLLBACK 1.2 (RW): an earlier request of the opposite mode goes first. retry!
429
- if read_mode
430
- LogVisitor.exit__write_request_ahead(
431
- logger, log_sampled, log_lock_try, lock_key,
432
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
433
- opposite_request_ahead
434
- )
435
- inter_result = :write_request_is_ahead
436
- else
437
- LogVisitor.exit__read_request_ahead(
438
- logger, log_sampled, log_lock_try, lock_key,
439
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
440
- opposite_request_ahead
441
- )
442
- inter_result = :read_request_is_ahead
443
- end
444
- # NOTE: our time has come! let's try to acquire the lock!
445
- # Step 5: find the lock -> check if the our lock is already acquired
446
- # - NOTE: the lock state is fetched after WATCH (step 0.1) so any lock state change
447
- # invalidates the transaction;
448
- elsif current_lock_obtainer
449
- # Step ROLLBACK 2: required lock is stil acquired. retry!
450
-
451
- LogVisitor.exit__lock_still_obtained(
452
- logger, log_sampled, log_lock_try, lock_key,
453
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
454
- waiting_acquirer, current_lock_obtainer,
455
- # NOTE: lock data is extracted for logs only
456
- ((log_sampled && log_lock_try) ? rconn.call('HGETALL', lock_key).to_h : {})
547
+ if obstacle
548
+ # Step ROLLBACK: the lock can not be obtained now. retry!
549
+ log_lock_obstacle(
550
+ rconn, obstacle, logger, log_sampled, log_lock_try, lock_key, queue_ttl,
551
+ acquirer_id, host_id, access_strategy, read_write_mode,
552
+ waiting_acquirer, opposite_request_ahead, current_lock_obtainer
457
553
  )
458
- inter_result = :lock_is_still_acquired
459
- elsif read_mode ? read_locked_by_acquirer : read_locked
460
- # Step ROLLBACK 3 (RW): the lock is still obtained by readers. retry!
461
- # - write lock request waits for all readers;
462
- # - read lock request waits for its own read lock
463
- # (`:wait_for_lock` conflict strategy);
464
-
465
- LogVisitor.exit__read_lock_still_obtained(
466
- logger, log_sampled, log_lock_try, lock_key,
467
- queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode, waiting_acquirer
468
- )
469
- inter_result = :read_lock_is_still_acquired
554
+ inter_result = obstacle
470
555
  elsif read_mode
471
556
  # NOTE: required lock is free and ready to be acquired! acquire! (read lock)
472
557
 
@@ -485,21 +570,35 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
485
570
  transact.call('PEXPIRE', lock_readers_key, ttl, 'NX')
486
571
  transact.call('PEXPIRE', lock_readers_key, ttl, 'GT')
487
572
 
488
- # Step 6.5 (RW): store the read lock data (the same structure as the write lock data)
573
+ # Step 6.5 (RW): store the read lock data (the same structure as the write lock data
574
+ # without `acq_id`/`hst_id`: both are derived from the read lock acquirer id that is
575
+ # a part of the data key and the registry member, so the info adds them on formatting)
489
576
  # - drop the data of the expired read lock of the current acquirer (if any);
490
577
  # - the read lock data lives as long as the read lock;
491
578
  transact.call('DEL', read_lock_key)
492
579
  transact.call(
493
580
  'HSET',
494
581
  read_lock_key,
495
- 'acq_id', acquirer_id,
496
- 'hst_id', host_id,
497
582
  'ts', timestamp = Time.now.to_f,
498
583
  'ini_ttl', ttl,
499
584
  *(meta.to_a if meta != nil) # steep:ignore
500
585
  )
501
586
  transact.call('PEXPIRE', read_lock_key, ttl)
502
587
 
588
+ # Step 6.6 (lock index): the lock name lives in the lock index as long as the read lock
589
+ # (the upper bound of the read lock lifetime: client time + ttl + grace period);
590
+ # - in the same transaction: an index entry dropped between the queueing and this
591
+ # transaction (`#unlock`, `#clear_locks`) is restored with the read lock;
592
+ # - GT => the score is never decreased (other keys of the lock can live longer);
593
+ # - only with the enabled lock index (the last command of the transaction);
594
+ if lock_index_enabled
595
+ transact.call(
596
+ 'ZADD', RedisQueuedLocks::Resource::LOCK_INDEX_KEY, 'GT',
597
+ RedisQueuedLocks::Resource.lock_index_time + ttl.to_i + lock_index_grace_period,
598
+ lock_name
599
+ )
600
+ end
601
+
503
602
  inter_result = :lock_obtaining
504
603
 
505
604
  LogVisitor.obtain__free_to_acquire(
@@ -526,6 +625,20 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
526
625
  # Step 6.3: set the lock expiration time in order to prevent "infinite locks"
527
626
  transact.call('PEXPIRE', lock_key, ttl) # NOTE: in milliseconds
528
627
 
628
+ # Step 6.4 (lock index): the lock name lives in the lock index as long as the lock
629
+ # (the upper bound of the lock lifetime: client time + ttl + grace period);
630
+ # - in the same transaction: an index entry dropped between the queueing and this
631
+ # transaction (`#unlock`, `#clear_locks`) is restored with the lock;
632
+ # - GT => the score is never decreased (other keys of the lock can live longer);
633
+ # - only with the enabled lock index (the last command of the transaction);
634
+ if lock_index_enabled
635
+ transact.call(
636
+ 'ZADD', RedisQueuedLocks::Resource::LOCK_INDEX_KEY, 'GT',
637
+ RedisQueuedLocks::Resource.lock_index_time + ttl.to_i + lock_index_grace_period,
638
+ lock_name
639
+ )
640
+ end
641
+
529
642
  inter_result = :lock_obtaining
530
643
 
531
644
  LogVisitor.obtain__free_to_acquire(
@@ -552,6 +665,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
552
665
  # 1. ZADD XX INCR (new read lock expiration) (OK for != nil)
553
666
  # 2. PEXPIRE NX/GT (registry TTL)
554
667
  # 3. HINCRBY/HSET/PEXPIRE (read lock data)
668
+ # 4. ZADD XX INCR (the lock index score; the enabled lock index only)
555
669
  if result[0] != nil
556
670
  {
557
671
  ok: true,
@@ -569,13 +683,16 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
569
683
  # NOTE: the read lock is expired and removed from the registry during the extension
570
684
  { ok: false, result: :read_lock_is_expired_during_extension }
571
685
  end
572
- elsif result.is_a?(::Array) && result.size == 4 # NOTE: four commands should be processed
686
+ elsif result.is_a?(::Array) && result.size == (lock_index_enabled ? 5 : 4)
687
+ # NOTE: four commands should be processed (five with the enabled lock index)
573
688
  # TODO:
574
689
  # => (!) analyze the command result and do actions with the depending on it
575
690
  # 1. EVAL (extend lock pttl) (OK for != nil)
576
691
  # 2. HINCRBY (ttl extension) (OK for != nil)
577
692
  # 3. HINCRBY (increased spc count) (OK for != nil)
578
693
  # 4. HSET (store the last spc time and ttl data) (OK for == 2 or != nil)
694
+ # 5. ZADD XX INCR (the lock index score; the enabled lock index only) (nil for the lock
695
+ # without the index entry: it does not affect the extension)
579
696
  if result[0] != nil && result[1] != nil && result[2] != nil && result[3] != nil
580
697
  {
581
698
  ok: true,
@@ -698,5 +815,444 @@ module RedisQueuedLocks::Acquirer::AcquireLock::TryToLock
698
815
  # rubocop:enable Lint/DuplicateBranch
699
816
  end
700
817
  # rubocop:enable Metrics/MethodLength
818
+
819
+ private
820
+
821
+ # Queues the lock request and reads the lock queue state (Steps 1, 2.1, 3 and 3.2-3.4 of
822
+ # the attempt): the commands are added to the given pipeline (one round trip with other
823
+ # pipelined commands).
824
+ # Replies (positions relative to the first one): [0] ZADD NX, [1] ZREMRANGEBYSCORE (request
825
+ # queue), [2] ZREMRANGEBYSCORE (opposite request queue), [QUEUE_HEAD_REPLY] ZRANGE (the first
826
+ # request of the request queue), [OPPOSITE_QUEUE_HEAD_REPLY] ZRANGE WITHSCORES (the first
827
+ # request of the opposite request queue); the enabled lock index only: [5] PEXPIRE NX, [6]
828
+ # PEXPIRE GT (the request queue TTL), [7] ZADD GT (the lock index), [8] ZREMRANGEBYSCORE
829
+ # (expired lock names of the lock index). Update these constants when the commands change
830
+ # (new commands go to the end: the replies are not read after `OPPOSITE_QUEUE_HEAD_REPLY`, so
831
+ # the conditional lock index commands do not shift the read replies).
832
+ #
833
+ # @param pipeline [RedisClient::Pipeline]
834
+ # @param request_queue [String] Queue of the requests of the requested lock mode.
835
+ # @param opposite_request_queue [String] Queue of the requests of the opposite lock mode.
836
+ # @param acquirer_position [Numeric]
837
+ # @param acquirer_id [String]
838
+ # @param acquirer_dead_score [Float] Requests queued before this score are expired.
839
+ # @param lock_name [String] The lock name (the member of the lock index).
840
+ # @param queue_ttl [Integer] Lifetime of the lock request (in seconds).
841
+ # @param lock_index_enabled [Boolean] Send the lock index commands (Steps 3.2-3.4).
842
+ # @param lock_index_grace_period [Integer] In milliseconds.
843
+ # @return [void]
844
+ #
845
+ # @api private
846
+ # @since 1.20.0
847
+ def queue_lock_request(
848
+ pipeline,
849
+ request_queue,
850
+ opposite_request_queue,
851
+ acquirer_position,
852
+ acquirer_id,
853
+ acquirer_dead_score,
854
+ lock_name,
855
+ queue_ttl,
856
+ lock_index_enabled,
857
+ lock_index_grace_period
858
+ )
859
+ # Step 1: add an acquirer to the lock acquirement queue
860
+ # NOTE:
861
+ # 'NX' means "Only add new elements. Don't update already existing elements."
862
+ # that works as:
863
+ # 1. (enqueue) <<if you are already in the queue - do nothing and wait your time>>
864
+ # 2. (requeue) or <<add to the right pre-calculated position if you are
865
+ # not in the queue now>>;
866
+ pipeline.call('ZADD', request_queue, 'NX', acquirer_position, acquirer_id)
867
+
868
+ # Step 2.1: drop expired acquirers from the lock queues
869
+ # - (RW) both queues are cleared: an expired request of the opposite mode should not
870
+ # block the current request forever;
871
+ pipeline.call('ZREMRANGEBYSCORE', request_queue, '-inf', acquirer_dead_score)
872
+ pipeline.call('ZREMRANGEBYSCORE', opposite_request_queue, '-inf', acquirer_dead_score)
873
+
874
+ # Step 3: get the actual acquirer waiting in the queue
875
+ # - (RW) and the first request of the opposite mode;
876
+ pipeline.call('ZRANGE', request_queue, '0', '0')
877
+ pipeline.call('ZRANGE', opposite_request_queue, '0', '0', 'WITHSCORES')
878
+
879
+ # NOTE: the disabled lock index: no lock index commands, lock request queues have no TTL
880
+ # (lock-iterating operations scan the keyspace);
881
+ return unless lock_index_enabled
882
+
883
+ # Step 3.2: (lock index) the request queue lives as long as the latest lock request
884
+ # - NX + GT => sets the queue TTL to max(current TTL, queue ttl): waiting acquirers refresh
885
+ # it on each attempt, the queue of dead acquirers expires (no "infinite queues");
886
+ # - queues order the lock requests only: a live acquirer whose request is dropped with
887
+ # the expired queue re-queues it on its next attempt (`ZADD NX` with its position);
888
+ # NOTE: `ceil` - queue ttl can be passed as a Float (in seconds), PEXPIRE requires Integer
889
+ queue_pttl = (queue_ttl * 1_000).ceil
890
+ pipeline.call('PEXPIRE', request_queue, queue_pttl, 'NX')
891
+ pipeline.call('PEXPIRE', request_queue, queue_pttl, 'GT')
892
+
893
+ # Step 3.3: (lock index) the lock name lives in the lock index as long as the request queue
894
+ # - the score is the upper bound of the queue lifetime: client time + queue ttl + grace;
895
+ # (the lock obtained by the attempt is indexed by its own transaction: Step 6.4/6.6);
896
+ # - GT => the score is never decreased (other keys of the lock can live longer);
897
+ # - after the queue command: an index entry dropping between the queue command and
898
+ # the index command (pruning, `#unlock`, `#clear_locks`) can not leave the queue without
899
+ # the index entry;
900
+ lock_index_time = RedisQueuedLocks::Resource.lock_index_time
901
+ pipeline.call(
902
+ 'ZADD', RedisQueuedLocks::Resource::LOCK_INDEX_KEY, 'GT',
903
+ lock_index_time + queue_pttl + lock_index_grace_period, lock_name
904
+ )
905
+
906
+ # Step 3.4: (lock index) drop the expired lock names from the lock index: all the keys of
907
+ # these locks are expired (amortized cleanup: no background jobs are required);
908
+ pipeline.call(
909
+ 'ZREMRANGEBYSCORE', RedisQueuedLocks::Resource::LOCK_INDEX_KEY, '-inf', lock_index_time - 1
910
+ )
911
+ end
912
+
913
+ # Adds the lock state commands to the given pipeline (the lock state of the attempt and of
914
+ # the pre-check: one round trip).
915
+ #
916
+ # NOTE: (pipeline layout) replies are read by their positions in the pipeline:
917
+ # - [LOCK_STATE_OBTAINER_REPLY] HGET, [LOCK_STATE_REDIS_TIME_REPLY] TIME,
918
+ # [LOCK_STATE_OWN_READ_LOCK_REPLY] ZSCORE - all requests (the head of the pipeline);
919
+ # - [LOCK_STATE_QUEUE_REPLIES_AT, +5] lock request queueing (`queue_lock_request`: the request
920
+ # queue commands; +9 with the lock index commands of the enabled lock index) - only with
921
+ # `queueing` (eager queueing, pre-check of retries);
922
+ # - the last reply: ZRANGE (the longest read lock) - write lock requests only (the "write-only
923
+ # tail" of the pipeline: it is read by `lock_state.last` under the same condition);
924
+ # - conditional commands go after the head (the queueing commands, then the write-only
925
+ # tail); the write-only tail must stay the last one: read lock requests do not send it,
926
+ # so a command added after it would get another position for read lock requests
927
+ # (and its reply would be read as the reply of another command);
928
+ # - new unconditional commands go into the head: update the position constants, the reply
929
+ # reading (`try_to_lock` Step 0.1 and `precheck_lock`) and the spec "lock state pipeline
930
+ # layout" (it checks the layout of both modes, with and without the queueing);
931
+ #
932
+ # @param pipeline [RedisClient::Pipeline]
933
+ # @param lock_key [String]
934
+ # @param lock_readers_key [String]
935
+ # @param acquirer_id [String]
936
+ # @param read_mode [Boolean]
937
+ # @param queueing [Boolean] Queue the lock request in the same round trip.
938
+ # @param request_queue [String]
939
+ # @param opposite_request_queue [String]
940
+ # @param acquirer_position [Numeric]
941
+ # @param acquirer_dead_score [Float]
942
+ # @param lock_name [String] The lock name (the member of the lock index).
943
+ # @param queue_ttl [Integer] In seconds.
944
+ # @param lock_index_enabled [Boolean]
945
+ # @param lock_index_grace_period [Integer] In milliseconds.
946
+ # @return [void]
947
+ #
948
+ # @api private
949
+ # @since 1.20.0
950
+ def pipeline_lock_state(
951
+ pipeline,
952
+ lock_key,
953
+ lock_readers_key,
954
+ acquirer_id,
955
+ read_mode,
956
+ queueing,
957
+ request_queue,
958
+ opposite_request_queue,
959
+ acquirer_position,
960
+ acquirer_dead_score,
961
+ lock_name,
962
+ queue_ttl,
963
+ lock_index_enabled,
964
+ lock_index_grace_period
965
+ )
966
+ pipeline.call('HGET', lock_key, 'acq_id')
967
+ pipeline.call('TIME')
968
+ pipeline.call('ZSCORE', lock_readers_key, acquirer_id)
969
+ # NOTE: the queueing commands (see "pipeline layout" above)
970
+ if queueing
971
+ queue_lock_request(
972
+ pipeline, request_queue, opposite_request_queue,
973
+ acquirer_position, acquirer_id, acquirer_dead_score,
974
+ lock_name, queue_ttl, lock_index_enabled, lock_index_grace_period
975
+ )
976
+ end
977
+ # NOTE: the write-only tail (keep it the last: see "pipeline layout" above)
978
+ pipeline.call('ZRANGE', lock_readers_key, '-1', '-1', 'WITHSCORES') unless read_mode
979
+ end
980
+
981
+ # (RW) The request of the opposite mode that should obtain the lock earlier
982
+ # - FIFO between modes (`:queued` access strategy only);
983
+ # - equal positions are ordered by acquirer ids;
984
+ #
985
+ # @param access_strategy [Symbol]
986
+ # @param opposite_waiting_request [Array<String,Float>,NilClass]
987
+ # The first request of the opposite request queue (acquirer id, position).
988
+ # @param acquirer_position [Numeric]
989
+ # @param acquirer_id [String]
990
+ # @return [String,NilClass] The acquirer of the earlier request of the opposite mode.
991
+ #
992
+ # @api private
993
+ # @since 1.20.0
994
+ def find_opposite_request_ahead(
995
+ access_strategy,
996
+ opposite_waiting_request,
997
+ acquirer_position,
998
+ acquirer_id
999
+ )
1000
+ return unless access_strategy == :queued && opposite_waiting_request
1001
+
1002
+ if opposite_waiting_request[1] < acquirer_position ||
1003
+ (opposite_waiting_request[1] == acquirer_position &&
1004
+ opposite_waiting_request[0] < acquirer_id)
1005
+ opposite_waiting_request[0]
1006
+ end
1007
+ end
1008
+
1009
+ # The obstacle to the lock obtaining for the classic (queued) logic of the attempt
1010
+ # (the same decision for the attempt and for its pre-check).
1011
+ #
1012
+ # @param read_mode [Boolean]
1013
+ # @param access_strategy [Symbol]
1014
+ # @param acquirer_id [String]
1015
+ # @param waiting_acquirer [String,NilClass] The first request of the request queue.
1016
+ # @param opposite_request_ahead [String,NilClass] See `find_opposite_request_ahead`.
1017
+ # @param current_lock_obtainer [String,NilClass] The write lock obtainer.
1018
+ # @param read_locked [Boolean]
1019
+ # Write lock requests: live read locks exist; read lock requests: the own read lock is live.
1020
+ # @return [Symbol,NilClass] The obstacle (the failure result of the attempt) or `nil`.
1021
+ #
1022
+ # @api private
1023
+ # @since 1.20.0
1024
+ def lock_obstacle(
1025
+ read_mode,
1026
+ access_strategy,
1027
+ acquirer_id,
1028
+ waiting_acquirer,
1029
+ opposite_request_ahead,
1030
+ current_lock_obtainer,
1031
+ read_locked
1032
+ )
1033
+ # Step PRE-4.x: check if the request time limit is reached
1034
+ # (when the current try self-removes itself from queue (queue ttl has come))
1035
+ if waiting_acquirer == nil
1036
+ :dead_score_reached
1037
+ # Step STRATEGY (queued): check the actual acquirer: is it ours? are we aready to lock?
1038
+ # - (RW) write lock requests are processed one by one;
1039
+ # - (RW) read lock requests are processed in parallel;
1040
+ elsif !read_mode && access_strategy == :queued && waiting_acquirer != acquirer_id
1041
+ # Step ROLLBACK 1.1: our time hasn't come yet. retry!
1042
+ :acquirer_is_not_first_in_queue
1043
+ elsif opposite_request_ahead
1044
+ # Step ROLLBACK 1.2 (RW): an earlier request of the opposite mode goes first. retry!
1045
+ read_mode ? :write_request_is_ahead : :read_request_is_ahead
1046
+ # Step 5: find the lock -> check if the our lock is already acquired
1047
+ # - NOTE: the attempt fetches the lock state after WATCH (step 0.1) so any lock state change
1048
+ # invalidates its transaction;
1049
+ elsif current_lock_obtainer
1050
+ # Step ROLLBACK 2: required lock is stil acquired. retry!
1051
+ :lock_is_still_acquired
1052
+ elsif read_locked
1053
+ # Step ROLLBACK 3 (RW): the lock is still obtained by readers. retry!
1054
+ # - write lock request waits for all readers;
1055
+ # - read lock request waits for its own read lock (`:wait_for_lock` conflict strategy);
1056
+ :read_lock_is_still_acquired
1057
+ end
1058
+ end
1059
+
1060
+ # Logs the obstacle to the lock obtaining (see `lock_obstacle`): the exit event of the attempt.
1061
+ #
1062
+ # @param rconn [RedisClient]
1063
+ # @param obstacle [Symbol]
1064
+ # @param logger [::Logger,#debug]
1065
+ # @param log_sampled [Boolean]
1066
+ # @param log_lock_try [Boolean]
1067
+ # @param lock_key [String]
1068
+ # @param queue_ttl [Integer]
1069
+ # @param acquirer_id [String]
1070
+ # @param host_id [String]
1071
+ # @param access_strategy [Symbol]
1072
+ # @param read_write_mode [Symbol]
1073
+ # @param waiting_acquirer [String,NilClass]
1074
+ # @param opposite_request_ahead [String,NilClass]
1075
+ # @param current_lock_obtainer [String,NilClass]
1076
+ # @return [void]
1077
+ #
1078
+ # @api private
1079
+ # @since 1.20.0
1080
+ # rubocop:disable Metrics/MethodLength
1081
+ def log_lock_obstacle(
1082
+ rconn,
1083
+ obstacle,
1084
+ logger,
1085
+ log_sampled,
1086
+ log_lock_try,
1087
+ lock_key,
1088
+ queue_ttl,
1089
+ acquirer_id,
1090
+ host_id,
1091
+ access_strategy,
1092
+ read_write_mode,
1093
+ waiting_acquirer,
1094
+ opposite_request_ahead,
1095
+ current_lock_obtainer
1096
+ )
1097
+ case obstacle
1098
+ when :dead_score_reached
1099
+ LogVisitor.exit__queue_ttl_reached(
1100
+ logger, log_sampled, log_lock_try, lock_key,
1101
+ queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
1102
+ )
1103
+ when :acquirer_is_not_first_in_queue
1104
+ LogVisitor.exit__no_first(
1105
+ logger, log_sampled, log_lock_try, lock_key,
1106
+ queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode, waiting_acquirer,
1107
+ # NOTE: lock data is extracted for logs only
1108
+ ((log_sampled && log_lock_try) ? rconn.call('HGETALL', lock_key).to_h : NO_LOCK_DATA)
1109
+ )
1110
+ when :write_request_is_ahead
1111
+ LogVisitor.exit__write_request_ahead(
1112
+ logger, log_sampled, log_lock_try, lock_key,
1113
+ queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
1114
+ opposite_request_ahead #: String
1115
+ )
1116
+ when :read_request_is_ahead
1117
+ LogVisitor.exit__read_request_ahead(
1118
+ logger, log_sampled, log_lock_try, lock_key,
1119
+ queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
1120
+ opposite_request_ahead #: String
1121
+ )
1122
+ when :lock_is_still_acquired
1123
+ LogVisitor.exit__lock_still_obtained(
1124
+ logger, log_sampled, log_lock_try, lock_key,
1125
+ queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode,
1126
+ waiting_acquirer, current_lock_obtainer, #: String
1127
+ # NOTE: lock data is extracted for logs only
1128
+ ((log_sampled && log_lock_try) ? rconn.call('HGETALL', lock_key).to_h : NO_LOCK_DATA)
1129
+ )
1130
+ when :read_lock_is_still_acquired
1131
+ LogVisitor.exit__read_lock_still_obtained(
1132
+ logger, log_sampled, log_lock_try, lock_key,
1133
+ queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode, waiting_acquirer
1134
+ )
1135
+ end
1136
+ end
1137
+ # rubocop:enable Metrics/MethodLength
1138
+
1139
+ # Pre-check of the attempt (Step PRE): the lock state without WATCH (one round trip).
1140
+ # - `fail_fast` attempts: the lock state only (the request is not queued: the failed "no try"
1141
+ # attempt does not dequeue the request); fails with `:fail_fast_no_try` on the obtained lock;
1142
+ # - retries: the lock state and the lock request queueing (the request is already queued by
1143
+ # the previous attempts); fails with the obstacle of the classic logic (`lock_obstacle`)
1144
+ # and logs the same events as the failed attempt;
1145
+ # - same-process conflicts (the lock is obtained by the current acquirer) and locks that can be
1146
+ # obtained are not decided: the attempt continues with the WATCH/MULTI logic (`nil`);
1147
+ # - the pre-check never obtains the lock (mutual exclusion relies on WATCH/MULTI only);
1148
+ #
1149
+ # @param rconn [RedisClient]
1150
+ # @param logger [::Logger,#debug]
1151
+ # @param log_sampled [Boolean]
1152
+ # @param log_lock_try [Boolean]
1153
+ # @param lock_key [String]
1154
+ # @param read_write_mode [Symbol]
1155
+ # @param lock_readers_key [String]
1156
+ # @param request_queue [String]
1157
+ # @param opposite_request_queue [String]
1158
+ # @param lock_name [String] The lock name (the member of the lock index).
1159
+ # @param acquirer_id [String]
1160
+ # @param host_id [String]
1161
+ # @param acquirer_position [Numeric]
1162
+ # @param acquirer_dead_score [Float]
1163
+ # @param queue_ttl [Integer]
1164
+ # @param lock_index_enabled [Boolean]
1165
+ # @param lock_index_grace_period [Integer] In milliseconds.
1166
+ # @param fail_fast [Boolean]
1167
+ # @param access_strategy [Symbol]
1168
+ # @return [Symbol,NilClass] The failure result of the attempt or `nil` (continue the attempt).
1169
+ #
1170
+ # @api private
1171
+ # @since 1.20.0
1172
+ # rubocop:disable Metrics/MethodLength
1173
+ def precheck_lock(
1174
+ rconn,
1175
+ logger,
1176
+ log_sampled,
1177
+ log_lock_try,
1178
+ lock_key,
1179
+ read_write_mode,
1180
+ lock_readers_key,
1181
+ request_queue,
1182
+ opposite_request_queue,
1183
+ lock_name,
1184
+ acquirer_id,
1185
+ host_id,
1186
+ acquirer_position,
1187
+ acquirer_dead_score,
1188
+ queue_ttl,
1189
+ lock_index_enabled,
1190
+ lock_index_grace_period,
1191
+ fail_fast,
1192
+ access_strategy
1193
+ )
1194
+ read_mode = (read_write_mode == :read)
1195
+ # NOTE: the same pipeline layout and the same reply reading as in Step 0.1 of the attempt
1196
+ # @type var lock_state: Array[untyped]
1197
+ lock_state = rconn.pipelined do |pipeline|
1198
+ pipeline_lock_state(
1199
+ pipeline, lock_key, lock_readers_key, acquirer_id, read_mode, !fail_fast,
1200
+ request_queue, opposite_request_queue, acquirer_position, acquirer_dead_score,
1201
+ lock_name, queue_ttl, lock_index_enabled, lock_index_grace_period
1202
+ )
1203
+ end
1204
+ # @type var current_lock_obtainer: String?
1205
+ current_lock_obtainer = lock_state[LOCK_STATE_OBTAINER_REPLY]
1206
+ redis_time = RedisQueuedLocks::Resource.redis_time_ms(lock_state[LOCK_STATE_REDIS_TIME_REPLY])
1207
+ # @type var own_read_lock_expiration: Float?
1208
+ own_read_lock_expiration = lock_state[LOCK_STATE_OWN_READ_LOCK_REPLY]
1209
+ # @type var longest_read_lock: [String, Float]?
1210
+ longest_read_lock = read_mode ? nil : lock_state.last.first
1211
+ read_locked_by_acquirer = own_read_lock_expiration.to_f > redis_time
1212
+ read_locked = longest_read_lock ? longest_read_lock[1] > redis_time : false
1213
+
1214
+ # NOTE: same-process conflicts are processed by the attempt (conflict strategies)
1215
+ return if acquirer_id == current_lock_obtainer || read_locked_by_acquirer
1216
+
1217
+ # NOTE: the fail-fast check of the attempt (Fast-Step X0) without same-process conflicts
1218
+ return ((current_lock_obtainer || read_locked) ? :fail_fast_no_try : nil) if fail_fast
1219
+
1220
+ # @type var waiting_acquirer: String?
1221
+ waiting_acquirer =
1222
+ lock_state[LOCK_STATE_QUEUE_REPLIES_AT + QUEUE_HEAD_REPLY].first
1223
+ # @type var opposite_waiting_request: [String, Float]?
1224
+ opposite_waiting_request =
1225
+ lock_state[LOCK_STATE_QUEUE_REPLIES_AT + OPPOSITE_QUEUE_HEAD_REPLY].first
1226
+ # @type var opposite_request_ahead: String?
1227
+ opposite_request_ahead = find_opposite_request_ahead(
1228
+ access_strategy, opposite_waiting_request, acquirer_position, acquirer_id
1229
+ )
1230
+ obstacle = lock_obstacle(
1231
+ read_mode, access_strategy, acquirer_id, waiting_acquirer, opposite_request_ahead,
1232
+ current_lock_obtainer, read_locked
1233
+ )
1234
+ return unless obstacle
1235
+
1236
+ # NOTE: the same log events as the failed attempt (queueing steps + the exit event)
1237
+ LogVisitor.acq_added_to_queue(
1238
+ logger, log_sampled, log_lock_try, lock_key,
1239
+ queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
1240
+ )
1241
+ LogVisitor.remove_expired_acqs(
1242
+ logger, log_sampled, log_lock_try, lock_key,
1243
+ queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode
1244
+ )
1245
+ LogVisitor.get_first_from_queue(
1246
+ logger, log_sampled, log_lock_try, lock_key,
1247
+ queue_ttl, acquirer_id, host_id, access_strategy, read_write_mode, waiting_acquirer
1248
+ )
1249
+ log_lock_obstacle(
1250
+ rconn, obstacle, logger, log_sampled, log_lock_try, lock_key, queue_ttl,
1251
+ acquirer_id, host_id, access_strategy, read_write_mode,
1252
+ waiting_acquirer, opposite_request_ahead, current_lock_obtainer
1253
+ )
1254
+ obstacle
1255
+ end
1256
+ # rubocop:enable Metrics/MethodLength
701
1257
  end
702
1258
  # rubocop:enable Metrics/ModuleLength