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
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: rql-code-review
3
- description: Code review of redis_queued_locks changes against the project's own invariants and conventions (WATCH/MULTI lock safety, read/write key families, the lock index superset invariant in both lookup modes, one-pass iteration, operation-module independence, long explicit argument lists, log/instrumentation visitors, RBS/Steep, docs sync). Use when the user asks to review / code-review (ревью, код-ревью, проверь изменения) a branch, diff, commit range, PR, staged changes or files of this repository. Also run it automatically on the uncommitted changes when a task in this repository is done and ready to commit, right before asking about the commit (never earlier). Spec files (*_spec.rb) are exempt from style/principle checks until the suite rework; only the coverage of the changed library behavior is checked.
3
+ description: Code review of redis_queued_locks changes against the project's own invariants and conventions (WATCH/MULTI lock safety, read/write key families and the order between modes, lock request priority bands and their head-driven dead request cleanup, the lock index superset invariant in both lookup modes, one-pass iteration, operation-module independence, long explicit argument lists, log/instrumentation visitors, RBS/Steep, docs sync). Use when the user asks to review / code-review (ревью, код-ревью, проверь изменения) a branch, diff, commit range, PR, staged changes or files of this repository. Also run it automatically on the uncommitted changes when a task in this repository is done and ready to commit, right before asking about the commit (never earlier). Spec files (*_spec.rb) are exempt from style/principle checks until the suite rework; only the coverage of the changed library behavior is checked.
4
4
  argument-hint: "[uncommitted | base..head | <commit> | pr <N> | staged | <paths...>]"
5
5
  ---
6
6
 
7
7
  # redis_queued_locks code review
8
8
 
9
9
  Review a change of this gem the way its maintainer does: lock safety first, then the project invariants
10
- (read/write locks, lock index, iteration), then architecture, types, docs and style. The rules live in
10
+ (read/write locks, lock request priorities, lock index, iteration), then architecture, types, docs and style. The rules live in
11
11
  `CLAUDE.md`, `.claude/project-overview.md` and `.claude/rules/*.md`; this skill says what to check, where the
12
12
  details are, and what must not be reported.
13
13
 
@@ -28,7 +28,7 @@ details are, and what must not be reported.
28
28
  - Scope `uncommitted`: the changes of the step (earlier steps are reviewed before their own commits).
29
29
  - The completion checks have just run: report their results, don't re-run them. A step without code changes (docs,
30
30
  `.claude` files) gets a docs review only: accuracy against the code, the overview and the rules, cross-file
31
- consistency (§4 J).
31
+ consistency (§4 K).
32
32
  - Blocker/Major findings in the step's own changes mean the step is not complete: fix them as part of the task (the
33
33
  review itself stays read-only), re-run the affected checks and review the fixed hunks once more; findings that
34
34
  need the user's decision, or remain after that pass, are reported instead of the commit question (`git.md#9`).
@@ -56,7 +56,8 @@ details are, and what must not be reported.
56
56
  ## 2. Context to load before judging
57
57
 
58
58
  Read the rule files of the touched paths and the overview sections of the touched topics (lock flow, "Read/Write
59
- locks", key layout, "Lock index", lock lookup modes, events, errors, conventions):
59
+ locks in the architecture" + "Read/Write locks", "Lock request prioritization in the architecture" + "Lock request
60
+ priorities", key layout, "Lock index", lock lookup modes, events, errors, conventions):
60
61
 
61
62
  | Changed path | Rule files |
62
63
  |---|---|
@@ -67,6 +68,7 @@ locks", key layout, "Lock index", lock lookup modes, events, errors, conventions
67
68
  | `logging/**`, `instrument/**` | + `visitors.md` |
68
69
  | `sig/**`, `Steepfile`, `rbs_collection.yaml` | `type-checking.md` |
69
70
  | `spec/**` | `tests.md` (coverage expectations only, see §3) |
71
+ | `CHANGELOG.md` | `changelog.md` |
70
72
  | commits / messages | `git.md` |
71
73
 
72
74
  - Read every changed method in full, its callers, its RBS signature and every copy of duplicated logic. Copies
@@ -80,16 +82,23 @@ locks", key layout, "Lock index", lock lookup modes, events, errors, conventions
80
82
  - `EXTEND_LOCK_PTTL` in `TryToLock` and `ExtendLockTTL`;
81
83
  - lock state reply reading in `try_to_lock` Step 0.1 and `precheck_lock` (shared `pipeline_lock_state`,
82
84
  position constants, `lock_obstacle`, `log_lock_obstacle`);
83
- - reserved `meta` keys validation in `AcquireLock` and `LockSeriesPoC`;
84
- - lock-family options in `Client#lock`, `#lock!`, `#lock_series`, `#lock_series!` and their forwarding in
85
- `LockSeriesPoC.lock_series_poc`.
86
- - Mirror path exceptions: `acquirer.rb` → `sig/redis_queued_locks/acquier.rbs`;
87
- `acquire_lock/yield_expire/log_visitor.rb` → `sig/.../acquire_lock/yield_with_expire/log_visitor.rbs`;
88
- `lock_series_poc*` has no RBS.
85
+ - the argument validations of `AcquireLock` Step 0 and `LockSeries` Step 0 (reserved `meta` keys, numeric ranges, the
86
+ `priority` resolution and range check: the same messages);
87
+ - the attempt flow of `TryToLock#try_to_lock`/`precheck_lock` and `TryToLockSeries#try_to_lock_series`/`precheck_lock_series`
88
+ (the commands and decisions are shared helpers, the flow exists twice), `drop_dead_requests_ahead` and
89
+ `drop_series_dead_requests_ahead`;
90
+ - the band liveness rule (a request is dead when its score is `<= Resource.lock_priority_band_dead_score(dead_score,
91
+ band)`): `TryToLock#dead_request_priority` / `pipeline_dead_requests_removal` and `ClearDeadRequests.drop_dead_requests` /
92
+ `drop_dead_requests_by_priority`;
93
+ - the `'priority'` field of a request (`Resource.lock_request_priority`) in `QueueInfo.queue_info` and
94
+ `Queues.extract_queues_info`;
95
+ - lock-family options in `Client#lock`, `#lock!`, `#lock_series`, `#lock_series!` and the keyword lists of
96
+ `AcquireLock.acquire_lock` and `LockSeries.lock_series`.
89
97
  - Next version for `@since`/`@version`: the version the unreleased work already uses (`git diff <base> -- lib |
90
98
  grep -oE '^\+.*@(since|version) [0-9.]+'`, the CHANGELOG `[Unreleased]` code), otherwise the next minor of
91
99
  `RedisQueuedLocks::VERSION` (`version.rb`); don't take the maximum over `lib/` (old tags can be wrong).
92
- A value older than `VERSION` on new code, a method behavior/signature change without the method `@version` bump, or a
100
+ A value older than `VERSION` on new code, a method behavior/signature change (a typo fix in the method or keyword
101
+ argument name included; a renamed positional parameter is not a signature change) without the method `@version` bump, or a
93
102
  class/module header `@version` that does not match its latest interface change (`logic.md#2`: signatures,
94
103
  added/removed methods, `extend`/`include`/`prepend`, the superclass; behavior changes never bump the header) is a
95
104
  Minor finding.
@@ -143,15 +152,33 @@ Still in scope (the exemption covers how specs are written, not whether the libr
143
152
  the reentrant write extension check `result.size == (lock_index_enabled ? 5 : 4)` follows its `MULTI`.
144
153
  - Round trips of the hot path must not regress silently (overview "Lock acquisition flow"): `lock {}` = 4, a
145
154
  failed attempt = 3, a failed retry = 1 (pre-check), `fail_fast` on an obtained lock = 1, `unlock` without
146
- readers = 1. Independent reads share one `pipelined` round trip (also inside `multi(watch:)`).
155
+ readers = 1; the only extra round trips are the dead blocking request removals of Step 2.2 (one per removal, none
156
+ while nothing dead blocks). Independent reads share one `pipelined` round trip (also inside `multi(watch:)`).
147
157
  - New destructive paths (delete/shorten a lock) check the owner, unless the change documents the trade-off as
148
158
  accepted by the user (like the known limitations in §5).
149
159
  - Shared mutable state behind `Utilities::Lock#synchronize` (`logic.md#9`).
150
160
 
151
- ### B. Read/write awareness (`logic.md#13`, `acquirer.md#12`)
161
+ ### B. Read/write awareness (`logic.md#13`, `acquirer.md#12`, overview "Read/Write locks in the architecture")
152
162
  - Every operation over locks, queues, zombies or lock info covers `rql:lock:*`, `rql:lock_queue:*`,
153
- `rql:lock_readers:*`, `rql:lock_read_queue:*` and `rql:lock_reader:<name>:<acq_id>`; every attempt prunes dead
154
- requests in both queues; dequeue/cleanup removes the request from the queue of its mode.
163
+ `rql:lock_readers:*`, `rql:lock_read_queue:*` and `rql:lock_reader:<name>:<acq_id>`; every attempt prunes the dead
164
+ requests of its priority band in both queues (and the bands of the dead requests that block it, §4 C);
165
+ dequeue/cleanup removes the request from the queue of its mode.
166
+ - Order between modes (`:queued`, `find_opposite_request_ahead`/`lock_obstacle`, shared by the attempt and the
167
+ pre-check): a write request must be the head of the write queue and have no earlier read request, a read request
168
+ must have no earlier write request ("earlier" = the `(score, acq_id)` comparison on the shifted scores); the
169
+ order never decides mutual exclusion.
170
+ - The held mode decides the release: `acquire_lock` keeps the try result `rw_mode` as `held_rw_mode` for
171
+ `yield_expire` (a read request that works through the own write lock holds the write lock); a reader is released
172
+ owner-safe (`ZREM` of the own member + `DEL` of the own reader data), `unlock` releases everything; the internal
173
+ `rw_mode` never appears in the public `lock` result.
174
+ - Read lock extension (`extend_lock_ttl(..., rw_mode: :read)`): `multi(watch: [lock_key])`, only a live read
175
+ lock and only while no write lock exists, never revives an expired reader; `all_read_locks: true` extends every
176
+ live reader (`ZADD XX INCR`), the registry/reader data TTLs keep the max (`PEXPIRE NX` + `GT`).
177
+ - `fail_fast`: write fails on a write holder or live readers, read fails on a write holder (or the own live read);
178
+ lock series: one mode for the whole series, one `acquirer_position` in all its queues, all locks in one transaction,
179
+ nothing held between attempts, `release_lock_series` releases only the locks the series obtained and only the current
180
+ acquirer's (write: compare-and-delete in one transaction, read: `ZREM` + `DEL`), extended reentrant locks get their
181
+ ttl back (overview "Lock series").
155
182
  - Write-only result shapes stay unchanged; read keys/fields appear only when read data exists.
156
183
  - `read_lock_key` is `nil` for write requests; reader data is a data carrier only (the registry decides existence),
157
184
  without `acq_id`/`hst_id`; don't store derivable values; a new lock field updates every formatting copy (§2,
@@ -160,7 +187,41 @@ Still in scope (the exemption covers how specs are written, not whether the libr
160
187
  entries) is still read correctly; a key/data format change states the rolling-deploy requirement in README and
161
188
  CHANGELOG.
162
189
 
163
- ### C. Lock index, both lookup modes (`acquirer.md#16`, overview "Lock index")
190
+ ### C. Lock request priorities (`acquirer.md` "Lock request priorities", overview "Lock request prioritization in the architecture")
191
+ - Queue scores only through `Resource` (the single source of the layout): `calc_initial_acquirer_position(priority)`
192
+ (enqueue time - `priority * LOCK_PRIORITY_BAND`), the requeue check `dead_score_reached?(position, priority, ttl)`
193
+ (a requeued request goes to the end of its own band), decoding with `lock_request_priority(score)`; no raw
194
+ `Time.now.to_f` positions, no own band arithmetic outside `Resource`.
195
+ - Dead request pruning per band only: `ZREMRANGEBYSCORE <queue> <lock_priority_band_floor(p)>
196
+ <lock_priority_band_dead_score(dead_score, p)>`; a new `-inf .. now - ttl` range on a request queue is a Blocker
197
+ (it drops alive higher-priority requests and keeps dead lower-priority ones); full clears (`-inf .. +inf`) and
198
+ member removals (`ZREM`) are band-independent; `-inf .. -inf` drops the requests at `-inf` only (manual writes:
199
+ always dead, out of all bands; `+inf` is never dead), and decoding an infinite score must never raise
200
+ (`lock_request_priority` maps `-inf`/`+inf` to the range bounds).
201
+ - Head-driven cleanup, cost independent of the priority range: the queueing prunes the own band in both queues (in
202
+ its pipeline, before `ZADD NX`: a requeued request must not lose its new position to the removal of its old one),
203
+ Step 2.2 (`drop_dead_requests_ahead`, attempt and pre-check, `:queued` only) drops the bands of the
204
+ dead blocking requests (the write queue head for writers, the earlier opposite-mode request) with the queue heads
205
+ re-read in the same round trip, bounded by `MAX_DEAD_REQUEST_REMOVALS`; `ClearDeadRequests` prunes only the bands
206
+ whose first request is dead. Never enumerate the priority range per attempt or per queue; any band is decoded
207
+ (also out of the range: newer versions).
208
+ - Ordering uses the shifted scores as is (queue heads, the cross-mode `(score, acq_id)` comparison): strict priority
209
+ between bands, FIFO inside a band, the same in both modes; the priority never takes part in mutual exclusion.
210
+ - The option: `priority:` in all four lock `Client` methods (default `config['default_lock_priority']`), an Integer
211
+ `MIN_LOCK_PRIORITY..MAX_LOCK_PRIORITY` or a name of `config['lock_priorities']` (the client-wide registry is
212
+ forwarded as the internal keyword `lock_priorities:`, never a public keyword); the name is resolved and the range
213
+ checked in `AcquireLock` Step 0.1 and `LockSeries` Step 0.1: code below sees Integers only (RBS
214
+ `RQL::lockPriority` at the boundary, `Integer` below); config validators for both keys, and a name of
215
+ `default_lock_priority` checked against `lock_priorities` by `Config` (after a `configure` block, before a direct `[]=`).
216
+ - Observability: every lock acquirement log line and event (classic, reentrant: the requested priority, lock series:
217
+ the series priority, `lock_obtaining_failed`) carries the Integer `priority` right after `rw_mode`; try-lock logs
218
+ that name another request carry its decoded priority (`first_priority_in_queue`, `ahead_priority`, `dead_priority`)
219
+ computed at the call site only when `log_sampled && log_lock_try` (one helper: `TryToLock#logged_request_priority`); info results carry `'priority'` (every copy).
220
+ - Compatibility: priority `0` keeps the raw timestamp scores of older versions/data; a change of the score layout or
221
+ of the pruning states the rolling-deploy effect in README and CHANGELOG (older versions prune positive-priority
222
+ requests as dead ones).
223
+
224
+ ### D. Lock index, both lookup modes (`acquirer.md#16`, overview "Lock index")
164
225
  - Disabled mode (default): no lock index command at all (`LOCK_INDEX_KEY` only under `if lock_index_enabled` /
165
226
  after `return unless lock_index_enabled`), no `ZSCAN`, queues without TTL. Enabled mode: no keyspace `SCAN`
166
227
  except `ReindexLocks`.
@@ -177,9 +238,9 @@ Still in scope (the exemption covers how specs are written, not whether the libr
177
238
  `lock`, `lock!`, `lock_series`, `lock_series!`, `reindex_locks`. Pruning (`ZREMRANGEBYSCORE <index> -inf
178
239
  <now - 1>`) stays in the queueing pipeline, `flush_zombies`, `clear_locks`, `clear_dead_requests`.
179
240
 
180
- ### D. Iteration and performance (`acquirer.md#7`, `arguments.md#7`)
241
+ ### E. Iteration and performance (`acquirer.md#7`, `arguments.md#7`)
181
242
  - One pass per operation: one `SCAN` over `KEY_PATTERN`/`ALL_LOCK_QUEUES_PATTERN` classified by
182
- `Resource.lock_key_family`, or one `ZSCAN` via `each_lock_name_batch` (expired scores skipped). Never `KEYS`,
243
+ `Resource.lock_key_family`, or one `ZSCAN` via `each_lock_name_batch` (expired scores skipped by `Resource.lock_index_score_expired?`: the `"inf"` score never expires). Never `KEYS`,
183
244
  never one round trip per key; per-key commands in pipelined batches collected across pages, plus the
184
245
  remainder batch.
185
246
  - A new lock-iterating operation implements both lookups side by side, chooses the lookup in one place
@@ -191,7 +252,7 @@ Still in scope (the exemption covers how specs are written, not whether the libr
191
252
  `NO_LOCK_DATA`); keys computed once in `acquire_lock`; log-only Redis reads gated at the call site
192
253
  (`acquirer.md#13`); no `Set` splatted in a loop.
193
254
 
194
- ### E. Architecture and API design
255
+ ### F. Architecture and API design
195
256
  - `Client` only fills defaults from `config['...']` and delegates (`logic.md#3`); an `x!` variant exists only when it
196
257
  must raise and repeats the full keyword list.
197
258
  - No parameter objects, `Struct`/`Data.define`/`OpenStruct`, builders, `**opts`, `options = {}`, `...` forwarding
@@ -199,11 +260,11 @@ Still in scope (the exemption covers how specs are written, not whether the libr
199
260
  (`arguments.md#2`, `#10`) and is forwarded by name with shorthand; defaults live only in `Client`, internal
200
261
  params are required (`arguments.md#4`); positional order: subject → domain data → observability tail
201
262
  `logger, instrumenter, instrument, log_sampling_*, log_sample_this, instr_sampling_*, instr_sample_this`, lock key
202
- params `lock_key, read_write_mode, lock_key_queue, read_lock_key_queue, lock_readers_key, read_lock_key, lock_name,
263
+ params `lock_key, rw_mode, lock_key_queue, read_lock_key_queue, lock_readers_key, read_lock_key, lock_name,
203
264
  acquirer_id, host_id, ...`, TTL params `ttl, queue_ttl, lock_index_enabled, lock_index_grace_period`
204
265
  (`arguments.md#5`); every call site and the RBS updated with the signature.
205
266
  - Operation modules are independent (`acquirer.md#14`): no calls to another `Acquirer::*` module and no references to
206
- its RBS types (exceptions: the `AcquireLock` core, `*PoC` modules); shared logic only via `Resource`/`Utilities`;
267
+ its RBS types (exception: the acquisition core, `AcquireLock` and `LockSeries`); shared logic only via `Resource`/`Utilities`;
207
268
  a duplicate keeps the original name and shape with a `NOTE` that it is duplicated on purpose; every copy changes
208
269
  in the same change. Don't copy the `WithAcqTimeout` → `LockInfo`/`QueueInfo` debt.
209
270
  - New operation: `acquirer/<snake_name>.rb` with one public `class << self` entry named after the file and private
@@ -219,15 +280,15 @@ Still in scope (the exemption covers how specs are written, not whether the libr
219
280
  `'rql:...'` literal outside `resource.rb`.
220
281
  - Config: `setting` + `validate` in `config.rb` (booleans `val == true || val == false`, Integers with ranges; per-call
221
282
  numeric options validated with the same ranges in the operation module, `logic.md#7`), dotted
222
- keys for groups, unit in a trailing `# NOTE:` comment, read as `config['key']` (`logic.md#7`). Errors:
283
+ keys for groups, unit in a trailing `# NOTE:` comment, read as `config['key']` (`logic.md#7`), one typed
284
+ `Config#[]` overload line per key in `config.rbs` (`type-checking.md#12`). Errors:
223
285
  `class XError < Error; end` in `errors.rb`, each under the global base of its branch (`Error`, `ArgumentError`,
224
286
  `Exception`: super exceptional cases only, with the reason in the comment; `logic.md#15`). Runtime dependencies:
225
287
  `redis-client` only; dev gems in `Gemfile`.
226
288
  - `AcquireLock`: new behavior as a new step or mixin with `# Step N.x` numbering and consistent `acq_process` keys
227
- (`acquirer.md#10`); `lock_series_poc.rb` is experimental: don't accept new features built on it without the
228
- user's request.
289
+ (`acquirer.md#10`).
229
290
 
230
- ### F. Swarm, Ractors, threads (`swarm.md`)
291
+ ### G. Swarm, Ractors, threads (`swarm.md`)
231
292
  - Redis work in stateless class functions shared by the main loop and the manual API (`#1`); main loops build
232
293
  their own client with `RedisClientBuilder` inside the loop thread (`#4`).
233
294
  - Into `Ractor.new` only copied plain values (`config.slice(...)`, scalars, `lock_index_enabled`): no `rql_client`,
@@ -241,8 +302,8 @@ Still in scope (the exemption covers how specs are written, not whether the libr
241
302
  - Lifecycle names and semantics identical in both bases (`#7`); a new element is registered everywhere (`#3`);
242
303
  status shapes `{ running:, state: }` with `'non_initialized'` and their RBS (`#10`).
243
304
 
244
- ### G. Logging and instrumentation (`visitors.md`)
245
- - Logs/events of `AcquireLock` and its mixins and of `LockSeriesPoC` go through visitor methods (`#1`); mutation
305
+ ### H. Logging and instrumentation (`visitors.md`)
306
+ - Logs/events of `AcquireLock`, `LockSeries` and their mixins go through visitor methods (`#1`); mutation
246
307
  modules write one log line and one event inline inside `run_non_critical`, gated by `log_sampled`/`instr_sampled`
247
308
  (`acquirer.md#9`).
248
309
  - Observability coverage (`logic.md#16`): a new or changed logic that creates/changes/deletes keys without logs and
@@ -255,13 +316,13 @@ Still in scope (the exemption covers how specs are written, not whether the libr
255
316
  `instrumenter.notify('redis_queued_locks.<event>', { ... }) rescue nil` with shorthand Symbol keys (`#5`-`#9`).
256
317
  - Parameter order: port, sampled flag, extra gates, `lock_key`, data; `rw_mode` after `access_strategy` (logs) or
257
318
  after `lock_key` (instrumentation), user `instrument` last (`#6`). `rw_mode` is the requested mode in
258
- `AcquireLock`/`TryToLock`/`DequeueFromLockQueue`, the held mode in `YieldExpire`, the series mode in `LockSeriesPoC`.
319
+ `AcquireLock`/`TryToLock`/`DequeueFromLockQueue`, the held mode in `YieldExpire`, the series mode in `LockSeries`.
259
320
  - Sampling decided once per operation (`Logging.should_log?` / `Instrument.should_instrument?`) and passed down (`#7`).
260
321
  - Event names and payload keys are public API: no rename/removal without the user's request; no new event on
261
322
  the default write success path (`#13`); new or changed log keys/payload keys go to README `## Logging` /
262
323
  `### Instrumentation Events` and CHANGELOG (`#14`).
263
324
 
264
- ### H. Types (`type-checking.md`)
325
+ ### I. Types (`type-checking.md`)
265
326
  - Every changed method, param, return shape, constant or ivar is mirrored in `sig/` (`#1`): nested module blocks,
266
327
  `use RedisQueuedLocks as RQL` / `use RedisClient as RC`, shared types (`RC::client`, `RQL::loggerObj`,
267
328
  `RQL::instrObj`, `samplerObj`), `type xxxResult = { ok: bool, result: ... }` for public results, literal types for
@@ -271,13 +332,15 @@ Still in scope (the exemption covers how specs are written, not whether the libr
271
332
  annotation carries `NOTE: (steep)`; never shape runtime code around Steep (`#5`). `Steepfile` is never loosened (`#6`).
272
333
  - RBS must match runtime values (runtime type checks run the suite): e.g. `Integer|Float` where a Float can arrive.
273
334
 
274
- ### I. Ruby style of the codebase
335
+ ### J. Ruby style of the codebase
275
336
  - `# frozen_string_literal: true`; compact constant paths (`module RedisQueuedLocks::Acquirer::Keys`); namespace
276
337
  files only `require_relative` (`logic.md#1`).
277
338
  - YARD on every class, module, constant, attr and method: `@param`/`@option`/`@return` (`Format: { ... }` for hash
278
- results), blank `#`, `@api public|private`, `@since <next>`; behavior/signature changes bump `@version <next>` on
279
- the method; the module/class header `@version` changes only with the interface (signatures, added/removed methods,
280
- `extend`/`include`/`prepend`, the superclass), never for behavior changes inside the methods (`logic.md#2`).
339
+ results), blank `#`, `@api public|private`, `@since <next>`; a new `include`/`extend`/`prepend` line has
340
+ `# @since <next>` right above it (no YARD block; the version that added the mixin); behavior/signature changes bump
341
+ `@version <next>` on the method; the module/class header `@version` changes only with the interface (signatures,
342
+ added/removed methods, `extend`/`include`/`prepend`, the superclass), never for behavior changes inside the methods
343
+ (`logic.md#2`).
281
344
  - Comments: `# NOTE:` with `# - item;` lists, `# Step N.x` in algorithms, `(RW)`/`(lock index)`/`(keyspace)`
282
345
  tags, `NOTE: (pipeline layout)`/`(transaction layout)` for positional replies.
283
346
  - Redis: uppercase String commands, String numeric args (`'0'`, `'-1'`, `'-inf'`, `'+inf'`), `redis.with do |rconn|`
@@ -293,12 +356,16 @@ Still in scope (the exemption covers how specs are written, not whether the libr
293
356
  - Rubocop: only specific `Metrics`/`Layout` cops disabled locally with a paired `enable`, never
294
357
  `# rubocop:disable all` in new code (`logic.md#10`); long signatures are not "fixed" (`arguments.md#8`).
295
358
  - Suggestions only (`logic.md` recommendations, not violations): correctly spelled new identifiers (don't copy the
296
- known typos), `then` instead of `yield_self` in new code.
359
+ known typos).
297
360
 
298
- ### J. Docs and process
361
+ ### K. Docs and process
299
362
  - The same change updates `.claude/project-overview.md` and the affected `.claude/rules/*.md` for every new/changed
300
363
  key, option, result shape, event, algorithm step or limitation (`logic.md#12`), README and CHANGELOG
301
364
  `[Unreleased]` for user-visible behavior, and the YARD of the touched `Client` methods.
365
+ - CHANGELOG entries are compact (`changelog.md`): one 1-2 line bullet per subject-level change, no implementation
366
+ details (algorithm steps, Redis commands, round trips, internals, full event lists), no entries for internal-only
367
+ changes; one entry per feature, released sections untouched; a fundamentally new feature or globally new support
368
+ (Read/Write locks, Lock Index, Garnet support) is a separate `*Brand New Feature*` entry first in `### Added`.
302
369
  - Commits under review (`git.md`): subject only `[<scope>] <summary>`, lowercase, imperative, no trailing period,
303
370
  no body except the attribution trailer, never on `master`.
304
371
 
@@ -309,15 +376,24 @@ Still in scope (the exemption covers how specs are written, not whether the libr
309
376
  read→write upgrade as an error unless `:wait_for_lock`; nested reads through `conflict_strategy`; no fairness
310
377
  between modes with `:random`; best-effort cross-host FIFO; no Lua in the RW path; one mode per lock series;
311
378
  `lock_index_enabled` as a config-only mode; the lock index defaulting to `false`.
379
+ - Settled design of the lock request priorities (overview "Lock request priorities", limitations kept by the user):
380
+ strict priority (lower priorities can starve within their timeout/retry limits), no preemption of an obtained lock,
381
+ the polled handoff (`retry_delay`), dead requests pruned with the pruner's `queue_ttl`, dead requests of lower bands
382
+ lingering until an attempt of their band/`clear_dead_requests`/the queue TTL/the swarm (they never block), the
383
+ library-constant range `-100..100` and its ~61 µs FIFO precision inside a band, `MAX_DEAD_REQUEST_REMOVALS` per
384
+ attempt, `:random` requests ignoring the order and the priorities of other requests (while their own requests are queued
385
+ at their priority: `:queued` requests wait for them), `clear_dead_requests` reading the batch queues (2 round trips per batch; `by_priority: true`: the first
386
+ request of each band only, the most bands of a batch queue + 1 round trips),
387
+ the order degradation with older versions (no safety impact), the queue score as the only order and liveness of a request
388
+ (a separate liveness registry of request deadlines was rejected by the user: don't propose it).
312
389
  - Unused `Resource` methods marked `# NOTE: (debug)` (manual runtime checks, `logic.md#17`); report a debug method
313
390
  only when it is wrong for a key family it can get.
314
391
  - Known limitations kept by the user: `YieldExpire` write release (`EXPIRE 0`) and ttl decrease without an owner
315
392
  check; `HMGET`→`DEL` without `WATCH` in `ReleaseLocksOf`/`FlushZombies`; `unlock` dropping the data of a reader
316
393
  re-acquired between its two round trips; `zombie_acquirers` vs `flush_zombies` acquirer sets.
317
- - Known debt: `WithAcqTimeout` → `LockInfo`/`QueueInfo`; rspec-retry, disabled coverage minimum, non-blocking runtime
318
- type checks; `acquier.rbs` and the `yield_with_expire` RBS path; the `swarm_element__termiante` typo;
394
+ - Known debt: `WithAcqTimeout` → `LockInfo`/`QueueInfo`; rspec-retry, disabled coverage minimum;
319
395
  `sleep(0.1)` readiness waits and missing logging in the swarm;
320
- one flusher per process; `EVAL` without `SCRIPT LOAD`; `# rubocop:disable all` around lock series.
396
+ one flusher per process; `EVAL` without `SCRIPT LOAD`.
321
397
  - "Recommendations (proposed, not yet project policy)" in the rule files: never violations; mention one only as an
322
398
  optional suggestion for touched code.
323
399
  - Pre-existing problems outside the diff: out of scope; mention a severe one briefly in the template's
@@ -328,7 +404,9 @@ Still in scope (the exemption covers how specs are written, not whether the libr
328
404
 
329
405
  - Confirm it in the code (call sites, copies, RBS, specs), not by pattern match: concurrency claims name the
330
406
  interleaving (acquirer A at step X, B at step Y) and why `WATCH` doesn't catch it; layout claims count the
331
- commands and positions; mode claims check both `lock_index_enabled` values and both `read_write_mode` values.
407
+ commands and positions; mode claims check both `lock_index_enabled` values and both `rw_mode` values;
408
+ priority claims check a positive, the default (`0`) and a negative band, a request of another band ahead (alive and
409
+ dead) and requests in both queues.
332
410
  - Drop findings contradicted by §5, by a design `NOTE` in the code or by the overview. Mark the rest
333
411
  `CONFIRMED` (traced end to end) or `PLAUSIBLE` (depends on an assumption: name it).
334
412
  - Helpful greps:
@@ -337,13 +415,15 @@ Still in scope (the exemption covers how specs are written, not whether the libr
337
415
  - `git grep -n LOCK_INDEX_KEY -- lib`: every use reachable only with `lock_index_enabled` (an `if`/`return unless`
338
416
  guard or a lock index lookup branch), except `ReindexLocks` and `Resource`;
339
417
  - cross-module calls: `git grep -n "RedisQueuedLocks::Acquirer::" -- lib/redis_queued_locks/acquirer` (only the
340
- known `WithAcqTimeout` debt and `LockSeriesPoC` are allowed);
418
+ known `WithAcqTimeout` debt and the `LockSeries` uses of the `AcquireLock` mixins are allowed);
341
419
  - `git diff --name-only <base>` against the `sig/` mirror (§2 exceptions); `git diff --check <base>`.
342
420
  - Checks (read-only): a review on request of local changes runs `bundle exec rake rubocop` and
343
421
  `bundle exec rbs collection install && bundle exec rake steep:check` (grep the output for `ERROR`/`FATAL`, restore a
344
422
  rewritten `rbs_collection.lock.yaml` with `git checkout -- rbs_collection.lock.yaml`); the automatic pre-commit run
345
- reports the completion checks instead of re-running them (see "When to run"). Don't run `rake rspec` without
346
- asking: it needs a local Redis, `FLUSHDB`s db 0 and db 1 and takes ~6 minutes.
423
+ reports the completion checks instead of re-running them (see "When to run"). Don't run the specs without
424
+ asking: they need a local Redis; when asked, run them in parallel processes (`rake rspec:parallel`: ~1 minute,
425
+ flushes db `0..2n-1` for `n` workers; the serial `rake rspec`, ~6 minutes and db 0/1, only for the one-process checks
426
+ of `tests.md#1`).
347
427
 
348
428
  ## 7. Report
349
429
 
@@ -375,11 +455,13 @@ Both templates have the same structure: change them together. A new language = a
375
455
  same structure plus a row in the table above.
376
456
 
377
457
  ### Severity and order
378
- - Blocker: breaks mutual exclusion or the lock index superset, loses or leaks locks/data, crashes, breaks the public
379
- API (method, result shape, event, config key) or turns CI red.
380
- - Major: a rule violation with a real consequence: a missed read key family or lookup mode, per-key round trips, a
381
- hot-path regression, broken module independence, a missing RBS/docs update for a changed contract, new
382
- behavior without specs.
458
+ - Blocker: breaks mutual exclusion or the lock index superset, loses or leaks locks/data (e.g. a queue pruning that
459
+ drops alive requests of another priority band), crashes, breaks the public API (method, result shape, event,
460
+ config key) or turns CI red.
461
+ - Major: a rule violation with a real consequence: a missed read key family or lookup mode, a broken order between
462
+ modes or priorities, a release of the wrong (requested instead of held) mode, a dead request that can block a
463
+ queue, a cost that grows with the priority range, per-key round trips, a hot-path regression, broken module
464
+ independence, a missing RBS/docs update for a changed contract, new behavior without specs.
383
465
  - Minor: convention deviations with little impact (YARD tags, comment format, naming, idioms).
384
466
  - Nit: optional polish.
385
467
  - Order by severity, then by file. Empty sections are omitted; with no findings the verdict says so and the sync and
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 4.0.7
1
+ 4.0.9