redis_queued_locks 1.19.0 → 1.21.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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/project-overview.md +96 -23
  3. data/.claude/rules/acquirer.md +39 -20
  4. data/.claude/rules/arguments.md +3 -2
  5. data/.claude/rules/git.md +3 -2
  6. data/.claude/rules/logic.md +32 -8
  7. data/.claude/rules/swarm.md +14 -9
  8. data/.claude/rules/tests.md +12 -3
  9. data/.claude/rules/type-checking.md +2 -1
  10. data/.claude/rules/visitors.md +10 -11
  11. data/.claude/skills/rql-code-review/SKILL.md +386 -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 +194 -0
  15. data/CLAUDE.md +6 -3
  16. data/README.md +502 -92
  17. data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rb +1 -0
  18. data/lib/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rb +42 -0
  19. data/lib/redis_queued_locks/acquirer/acquire_lock/log_visitor.rb +45 -0
  20. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rb +1 -0
  21. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +695 -128
  22. data/lib/redis_queued_locks/acquirer/acquire_lock/with_acq_timeout.rb +2 -2
  23. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire/log_visitor.rb +1 -0
  24. data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +8 -6
  25. data/lib/redis_queued_locks/acquirer/acquire_lock.rb +134 -26
  26. data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +229 -24
  27. data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +189 -18
  28. data/lib/redis_queued_locks/acquirer/is_locked.rb +0 -1
  29. data/lib/redis_queued_locks/acquirer/is_queued.rb +0 -1
  30. data/lib/redis_queued_locks/acquirer/keys.rb +151 -7
  31. data/lib/redis_queued_locks/acquirer/lock_info.rb +39 -41
  32. data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +3 -2
  33. data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +5 -4
  34. data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +79 -20
  35. data/lib/redis_queued_locks/acquirer/locks.rb +212 -83
  36. data/lib/redis_queued_locks/acquirer/queue_info.rb +11 -18
  37. data/lib/redis_queued_locks/acquirer/queues.rb +123 -50
  38. data/lib/redis_queued_locks/acquirer/reindex_locks.rb +253 -0
  39. data/lib/redis_queued_locks/acquirer/release_all_locks.rb +177 -61
  40. data/lib/redis_queued_locks/acquirer/release_lock.rb +84 -25
  41. data/lib/redis_queued_locks/acquirer/release_locks_of.rb +274 -70
  42. data/lib/redis_queued_locks/acquirer/release_read_lock.rb +22 -3
  43. data/lib/redis_queued_locks/acquirer.rb +1 -0
  44. data/lib/redis_queued_locks/client.rb +164 -36
  45. data/lib/redis_queued_locks/config.rb +16 -13
  46. data/lib/redis_queued_locks/errors.rb +34 -4
  47. data/lib/redis_queued_locks/instrument/sampler.rb +4 -7
  48. data/lib/redis_queued_locks/logging/sampler.rb +4 -8
  49. data/lib/redis_queued_locks/resource.rb +113 -6
  50. data/lib/redis_queued_locks/swarm/flush_zombies.rb +333 -61
  51. data/lib/redis_queued_locks/swarm/redis_client_builder.rb +8 -4
  52. data/lib/redis_queued_locks/swarm/swarm_element/threaded.rb +3 -2
  53. data/lib/redis_queued_locks/swarm/zombie_info.rb +203 -53
  54. data/lib/redis_queued_locks/swarm.rb +97 -5
  55. data/lib/redis_queued_locks/utilities.rb +1 -1
  56. data/lib/redis_queued_locks/version.rb +2 -2
  57. data/sig/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rbs +14 -0
  58. data/sig/redis_queued_locks/acquirer/acquire_lock/log_visitor.rbs +13 -0
  59. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +107 -1
  60. data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +3 -3
  61. data/sig/redis_queued_locks/acquirer/acquire_lock.rbs +3 -1
  62. data/sig/redis_queued_locks/acquirer/clear_dead_requests.rbs +19 -1
  63. data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +20 -4
  64. data/sig/redis_queued_locks/acquirer/keys.rbs +11 -1
  65. data/sig/redis_queued_locks/acquirer/locks.rbs +15 -6
  66. data/sig/redis_queued_locks/acquirer/queues.rbs +9 -2
  67. data/sig/redis_queued_locks/acquirer/reindex_locks.rbs +52 -0
  68. data/sig/redis_queued_locks/acquirer/release_all_locks.rbs +8 -1
  69. data/sig/redis_queued_locks/acquirer/release_lock.rbs +6 -3
  70. data/sig/redis_queued_locks/acquirer/release_locks_of.rbs +20 -1
  71. data/sig/redis_queued_locks/acquirer/release_read_lock.rbs +1 -1
  72. data/sig/redis_queued_locks/client.rbs +21 -2
  73. data/sig/redis_queued_locks/errors.rbs +5 -2
  74. data/sig/redis_queued_locks/instrument/sampler.rbs +0 -2
  75. data/sig/redis_queued_locks/logging/sampler.rbs +0 -2
  76. data/sig/redis_queued_locks/resource.rbs +7 -1
  77. data/sig/redis_queued_locks/swarm/flush_zombies.rbs +51 -1
  78. data/sig/redis_queued_locks/swarm/zombie_info.rbs +58 -7
  79. data/sig/vendor/redis_client.rbs +2 -0
  80. metadata +6 -1
@@ -0,0 +1,386 @@
1
+ ---
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.
4
+ argument-hint: "[uncommitted | base..head | <commit> | pr <N> | staged | <paths...>]"
5
+ ---
6
+
7
+ # redis_queued_locks code review
8
+
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
11
+ `CLAUDE.md`, `.claude/project-overview.md` and `.claude/rules/*.md`; this skill says what to check, where the
12
+ details are, and what must not be reported.
13
+
14
+ - Report defects, with a concrete failure scenario and the rule they break. Every rule-based finding cites its
15
+ source (`acquirer.md#12` = rule 12 of the "Claude rules" list in `.claude/rules/acquirer.md`, `overview "Lock index"`).
16
+ - The review is read-only: don't edit, stage or commit anything. After a review on request, offer to fix the
17
+ findings; the automatic pre-commit run acts on them itself (see "When to run").
18
+ - Write the report from the template that matches the Claude Code `language` setting, English when that language
19
+ has no template (§7); code identifiers, rule names and quotes stay as they are.
20
+
21
+ ## When to run
22
+
23
+ - On request: the user asks for a review (any scope of §1).
24
+ - Automatically, as the pre-commit review (`git.md#7`): when a task in this repository (a feature, a fix, a prompt,
25
+ a docs or `.claude` change) is done and committable, at the moment the commit question would be asked or a commit
26
+ authorized in advance would be made (`git.md#8`). Never earlier: not during the implementation, not after
27
+ intermediate edits, not for incomplete or red states (`git.md#9`), not for an explicit commit request.
28
+ - Scope `uncommitted`: the changes of the step (earlier steps are reviewed before their own commits).
29
+ - The completion checks have just run: report their results, don't re-run them. A step without code changes (docs,
30
+ `.claude` files) gets a docs review only: accuracy against the code, the overview and the rules, cross-file
31
+ consistency (§4 J).
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
+ review itself stays read-only), re-run the affected checks and review the fixed hunks once more; findings that
34
+ need the user's decision, or remain after that pass, are reported instead of the commit question (`git.md#9`).
35
+ - The report is followed by the commit question (`git.md#7`) with the verdict and the remaining Minor/Nit findings.
36
+
37
+ ## 1. Scope
38
+
39
+ | Argument | Diff to review |
40
+ |---|---|
41
+ | `uncommitted` | the changes of the current step: `git diff HEAD` (staged + unstaged) plus untracked files (`git status --porcelain`); the scope of the automatic pre-commit run |
42
+ | none | the branch against `master`: `git merge-base master HEAD`, then `git diff <base>` (committed + staged + unstaged) plus untracked files (`git status --porcelain`). On `master` with a clean tree there is nothing to review: ask for a target |
43
+ | `base..head` / `<commit>` | `git diff base..head` / `git show <commit>` |
44
+ | `staged` | `git diff --cached` |
45
+ | `pr <N>` | `gh pr diff <N>` when `gh` is installed, otherwise `git fetch origin pull/<N>/head` + `git diff master...FETCH_HEAD` (no local branch) |
46
+ | `<paths...>` | the whole files: report real defects and rule violations, but pre-existing known debt (§5) is not a finding |
47
+
48
+ - Start with `git diff --name-status` and `git diff --stat`, record the base sha.
49
+ - Environment artifacts are never part of a change (`git.md#10`): `rbs_collection.lock.yaml` rewritten by
50
+ `rbs collection install`, `coverage/`, `.gem_rbs_collection/`, `.claude/settings.local.json`, `.DS_Store`.
51
+ Flag them only when they are committed or about to be.
52
+ - Big diffs: learn the intent first (commit subjects `git log --format=%s <base>..HEAD`, the CHANGELOG
53
+ `[Unreleased]` and overview hunks), then review in dependency order: `resource.rb`/`config.rb` →
54
+ `acquire_lock/*` → other `acquirer/*` → `client.rb` → `swarm/*` → `sig/` → docs; specs last, for coverage only.
55
+
56
+ ## 2. Context to load before judging
57
+
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):
60
+
61
+ | Changed path | Rule files |
62
+ |---|---|
63
+ | `lib/**` | `logic.md`, `type-checking.md` |
64
+ | `lib/redis_queued_locks/acquirer/**` | + `acquirer.md`, `arguments.md`, `visitors.md` |
65
+ | `client.rb`, `config.rb`, `swarm.rb` | + `arguments.md` |
66
+ | `swarm/**`, `sig/redis_queued_locks/swarm/**` | + `swarm.md` |
67
+ | `logging/**`, `instrument/**` | + `visitors.md` |
68
+ | `sig/**`, `Steepfile`, `rbs_collection.yaml` | `type-checking.md` |
69
+ | `spec/**` | `tests.md` (coverage expectations only, see §3) |
70
+ | commits / messages | `git.md` |
71
+
72
+ - Read every changed method in full, its callers, its RBS signature and every copy of duplicated logic. Copies
73
+ that must change together:
74
+ - write lock info formatting `LockInfo.lock_info` ↔ `Locks.extract_locks_info`; reader formatting
75
+ `LockInfo.read_lock_info` ↔ `Locks.read_lock_info`; request formatting `QueueInfo.queue_info` ↔
76
+ `Queues.extract_queues_info`;
77
+ - `each_lock_name_batch` in `Locks`, `Queues`, `Keys`, `ReleaseAllLocks`, `ReleaseLocksOf`,
78
+ `ClearDeadRequests` (inline iteration in `FlushZombies.flush_lock_index_zombies` and
79
+ `ZombieInfo.each_lock_index_zombie_lock`); `ZombieInfo` lookups mirror `FlushZombies`;
80
+ - `EXTEND_LOCK_PTTL` in `TryToLock` and `ExtendLockTTL`;
81
+ - lock state reply reading in `try_to_lock` Step 0.1 and `precheck_lock` (shared `pipeline_lock_state`,
82
+ 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.
89
+ - Next version for `@since`/`@version`: the version the unreleased work already uses (`git diff <base> -- lib |
90
+ grep -oE '^\+.*@(since|version) [0-9.]+'`, the CHANGELOG `[Unreleased]` code), otherwise the next minor of
91
+ `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
93
+ class/module header `@version` that does not match its latest interface change (`logic.md#2`: signatures,
94
+ added/removed methods, `extend`/`include`/`prepend`, the superclass; behavior changes never bump the header) is a
95
+ Minor finding.
96
+
97
+ ## 3. Spec files are exempt (until the suite rework)
98
+
99
+ `spec/**/*_spec.rb` are not ideal and are going to be rewritten entirely (header `NOTE` of
100
+ `spec/redis_queued_locks_spec.rb`, overview "Conventions", rspec-retry as temporary debt). Until then any coding
101
+ principle may be broken there. Never report the way specs are written: duplication, long or nested examples, `sleep` waits,
102
+ anonymous fakes, magic numbers, naming, structure, `it`/`specify`, rubocop directives, thread handling,
103
+ cleanup style, retry reliance, file size, the `tests.md` rules on writing specs and its recommendations.
104
+
105
+ Still in scope (the exemption covers how specs are written, not whether the library is tested):
106
+ 1. Coverage of the changed library behavior (finding against the `lib/` change, category `test-coverage`):
107
+ new behavior without an example; RW changes without both modes (`tests.md#13`); lock-iterating changes without
108
+ the small-batch examples of both lookups and the recorded-command checks (`acquirer.md#7`); new lock index
109
+ writes without `describe 'lock index'` examples (`acquirer.md#16`); a changed pipeline layout without an updated
110
+ `lock state pipeline layout` spec (`acquirer.md#15`); a touched obtaining transaction that the spec
111
+ `the lock obtaining transaction indexes the lock` no longer exercises (it drops the index entry right before the
112
+ obtaining `MULTI`); a new log/instrumentation event without a fake logger/notifier assertion (`visitors.md#11`).
113
+ 2. A changed spec that asserts behavior contradicting the overview/rules, or that can't fail for the behavior
114
+ it claims to check (vacuous expectation, expectation never reached).
115
+
116
+ `spec/spec_helper.rb` and `spec/setup_simplecov.rb` are support files: review them for correctness only (the
117
+ `around` hook that switches `config['lock_index_enabled']` and restores the library default).
118
+
119
+ ## 4. Checklist
120
+
121
+ ### A. Lock safety and Redis correctness
122
+ - Mutual exclusion only from the lock state checked under `WATCH`/`MULTI` (`logic.md#6`, `#14`); Lua only for the
123
+ existing write TTL extension/decrease, no new Lua in the RW acquisition path unless the user asked. No
124
+ check-then-set in Ruby without a transaction; queues, client clocks, the lock index and the pre-check never
125
+ decide mutual exclusion (the pre-check never obtains the lock).
126
+ - Asymmetric WATCH (`acquirer.md#12`): writers watch `[lock_key, lock_readers_key]`, readers `[lock_key]` only.
127
+ No write to a watched key outside `MULTI` after `WATCH` (aborts the own `EXEC`); never merge `WATCH` into a
128
+ pipeline (unsafe on reconnect).
129
+ - Lock state: write needs no write lock and no live readers; read needs no write lock; same-acquirer cases go
130
+ through `conflict_strategy`; read→write upgrade never works through (`:conflict_lock_upgrade`, waits with
131
+ `:wait_for_lock`).
132
+ - Time: reader liveness and expirations in Redis time (`TIME` → `Resource.redis_time_ms`, `PEXPIRE`); client
133
+ `Time.now.to_f` only for queue positions, `ts` fields, zombie probes; lock index scores in
134
+ `Resource.lock_index_time` (epoch ms) + grace. Readers are live only while `score > now`: never decide by
135
+ `ZCARD`/`EXISTS` alone.
136
+ - TTLs: max-TTL keys (registry, reader data, queues) get the `PEXPIRE NX` + `PEXPIRE GT` pair (`GT` alone sets
137
+ nothing on a key without TTL); `PTTL` sentinels handled (`-2` missing, `-1` → `Float::INFINITY` / `'+inf'`);
138
+ units right (ttl ms, `queue_ttl` s → `(queue_ttl * 1_000).ceil`, `zombie_ttl` ms → `/ 1_000.0`), integers for
139
+ `PEXPIRE`.
140
+ - Replies read by position (`acquirer.md#15`): `NOTE: (pipeline layout)` next to the commands, conditional
141
+ commands form a tail after the unconditional ones and are read under the same condition, position constants
142
+ (`LOCK_STATE_*`, `QUEUE_HEAD_REPLY`, `KEY_STATE_REPLIES`, `LOCK_STATE_REPLIES`) and the layout spec updated;
143
+ the reentrant write extension check `result.size == (lock_index_enabled ? 5 : 4)` follows its `MULTI`.
144
+ - Round trips of the hot path must not regress silently (overview "Lock acquisition flow"): `lock {}` = 4, a
145
+ 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:)`).
147
+ - New destructive paths (delete/shorten a lock) check the owner, unless the change documents the trade-off as
148
+ accepted by the user (like the known limitations in §5).
149
+ - Shared mutable state behind `Utilities::Lock#synchronize` (`logic.md#9`).
150
+
151
+ ### B. Read/write awareness (`logic.md#13`, `acquirer.md#12`)
152
+ - 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.
155
+ - Write-only result shapes stay unchanged; read keys/fields appear only when read data exists.
156
+ - `read_lock_key` is `nil` for write requests; reader data is a data carrier only (the registry decides existence),
157
+ without `acq_id`/`hst_id`; don't store derivable values; a new lock field updates every formatting copy (§2,
158
+ `acquirer.md#11`); `rw_mode` stays a reserved `meta` key.
159
+ - Backward compatibility: data written by older versions (readers with `acq_id`/`hst_id`, names without index
160
+ entries) is still read correctly; a key/data format change states the rolling-deploy requirement in README and
161
+ CHANGELOG.
162
+
163
+ ### C. Lock index, both lookup modes (`acquirer.md#16`, overview "Lock index")
164
+ - Disabled mode (default): no lock index command at all (`LOCK_INDEX_KEY` only under `if lock_index_enabled` /
165
+ after `return unless lock_index_enabled`), no `ZSCAN`, queues without TTL. Enabled mode: no keyspace `SCAN`
166
+ except `ReindexLocks`.
167
+ - Superset invariant: every key-creating command is followed by `ZADD <index> GT <now + lifetime + grace> <name>`
168
+ in the same pipeline/transaction, after the key command; the obtaining `MULTI` indexes the lock itself; every
169
+ extension is followed by `ZADD XX INCR`; scores never decrease; names are removed only in the same `MULTI`
170
+ that deletes all their keys (`ReleaseLocksOf`, `YieldExpire` releases and `FlushZombies` never remove names).
171
+ - Lock index commands are conditional tails: reply positions never depend on the mode.
172
+ - A new key family: distinct prefix `rql:lock_<kind>:<name>` (never a suffix of the name), bounded TTL in the
173
+ enabled mode, covered by the index score, `Resource.lock_key_family`/`lock_name_from_key`, both lookups and
174
+ `ReindexLocks`.
175
+ - `lock_index_enabled` is client-wide (`arguments.md#9`): read from `config` inside `Client`/`Swarm` and forwarded,
176
+ never a keyword of a public method; the flusher Ractor gets a copy. `lock_index_grace_period` is forwarded by
177
+ `lock`, `lock!`, `lock_series`, `lock_series!`, `reindex_locks`. Pruning (`ZREMRANGEBYSCORE <index> -inf
178
+ <now - 1>`) stays in the queueing pipeline, `flush_zombies`, `clear_locks`, `clear_dead_requests`.
179
+
180
+ ### D. Iteration and performance (`acquirer.md#7`, `arguments.md#7`)
181
+ - 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`,
183
+ never one round trip per key; per-key commands in pipelined batches collected across pages, plus the
184
+ remainder batch.
185
+ - A new lock-iterating operation implements both lookups side by side, chooses the lookup in one place
186
+ (`lock_index_enabled ? index_x : scan_x` or one `if`/`else` with `(lock index)`/`(keyspace)` step comments) and
187
+ keeps the shared processing in one copy.
188
+ - No `EXISTS` next to `ZRANGE`/`HGETALL`: Redis never keeps empty sorted sets/hashes.
189
+ - Hot path (`AcquireLock`, `TryToLock`, `YieldExpire`, `DequeueFromLockQueue`, visitor call sites): no wrapper
190
+ objects, hash merging, splats to build arguments, closures for argument plumbing, new `{}` per attempt (use
191
+ `NO_LOCK_DATA`); keys computed once in `acquire_lock`; log-only Redis reads gated at the call site
192
+ (`acquirer.md#13`); no `Set` splatted in a loop.
193
+
194
+ ### E. Architecture and API design
195
+ - `Client` only fills defaults from `config['...']` and delegates (`logic.md#3`); an `x!` variant exists only when it
196
+ must raise and repeats the full keyword list.
197
+ - No parameter objects, `Struct`/`Data.define`/`OpenStruct`, builders, `**opts`, `options = {}`, `...` forwarding
198
+ (`arguments.md#1`, `#3`); a new public option goes to every `Client` variant with a `config` default
199
+ (`arguments.md#2`, `#10`) and is forwarded by name with shorthand; defaults live only in `Client`, internal
200
+ params are required (`arguments.md#4`); positional order: subject → domain data → observability tail
201
+ `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,
203
+ acquirer_id, host_id, ...`, TTL params `ttl, queue_ttl, lock_index_enabled, lock_index_grace_period`
204
+ (`arguments.md#5`); every call site and the RBS updated with the signature.
205
+ - 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`;
207
+ a duplicate keeps the original name and shape with a `NOTE` that it is duplicated on purpose; every copy changes
208
+ in the same change. Don't copy the `WithAcqTimeout` → `LockInfo`/`QueueInfo` debt.
209
+ - New operation: `acquirer/<snake_name>.rb` with one public `class << self` entry named after the file and private
210
+ helpers, `require_relative` in `acquirer.rb`, thin `Client` method, RBS, spec, README/CHANGELOG
211
+ (`acquirer.md#1`). Mutating operations follow the two-layer pattern (timing wrapper + private `fully_*`,
212
+ `acquirer.md#2`).
213
+ - Results: `{ ok:, result: }` only at the public boundary (`Client`, `Acquirer::*`, `Swarm` facade actions) with
214
+ Symbol statuses and abbreviated Symbol keys (`rel_*`, `*_cnt`, `*_time`); info queries return a String-keyed
215
+ Hash/Set or `nil`; swarm element internals return bare scalars (`swarm.md#15`). Raise only in `x!` methods, the
216
+ `raise_errors` path of `AcquireLock` and on invalid arguments (`RedisQueuedLocks::ArgumentError`), with the lock
217
+ key / acquirer id in the message.
218
+ - Keys only via `Resource.prepare_*`, `*_PATTERN`, `SWARM_KEY`, `LOCK_INDEX_KEY` (`logic.md#5`): flag every new
219
+ `'rql:...'` literal outside `resource.rb`.
220
+ - Config: `setting` + `validate` in `config.rb` (booleans `val == true || val == false`, Integers with ranges; per-call
221
+ 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:
223
+ `class XError < Error; end` in `errors.rb`, each under the global base of its branch (`Error`, `ArgumentError`,
224
+ `Exception`: super exceptional cases only, with the reason in the comment; `logic.md#15`). Runtime dependencies:
225
+ `redis-client` only; dev gems in `Gemfile`.
226
+ - `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.
229
+
230
+ ### F. Swarm, Ractors, threads (`swarm.md`)
231
+ - Redis work in stateless class functions shared by the main loop and the manual API (`#1`); main loops build
232
+ their own client with `RedisClientBuilder` inside the loop thread (`#4`).
233
+ - Into `Ractor.new` only copied plain values (`config.slice(...)`, scalars, `lock_index_enabled`): no `rql_client`,
234
+ logger, Redis client; inside only shareable constants and module functions (`#4`, `#5`).
235
+ - Ports (`#13`): results port created in the main Ractor, commands port inside the element Ractor, `monitor`
236
+ registered before waiting, one reply per command, Symbol messages reserved for death notices; no
237
+ `Ractor.receive`/`Ractor#send`/default port; names end in `_port`, block params `r_res_p`/`r_com_p`.
238
+ - Threads: kill + join inside the Ractor (`#14`), `abort_on_exception = false` for main loops, no retry loops in
239
+ elements (`#8`); state changes and request/reply pairs inside `sync.synchronize` (`#6`); the supervisor is
240
+ stopped before elements are killed or re-created (`#9`).
241
+ - Lifecycle names and semantics identical in both bases (`#7`); a new element is registered everywhere (`#3`);
242
+ status shapes `{ running:, state: }` with `'non_initialized'` and their RBS (`#10`).
243
+
244
+ ### G. Logging and instrumentation (`visitors.md`)
245
+ - Logs/events of `AcquireLock` and its mixins and of `LockSeriesPoC` go through visitor methods (`#1`); mutation
246
+ modules write one log line and one event inline inside `run_non_critical`, gated by `log_sampled`/`instr_sampled`
247
+ (`acquirer.md#9`).
248
+ - Observability coverage (`logic.md#16`): a new or changed logic that creates/changes/deletes keys without logs and
249
+ instrumentation, a mutation event/log that times an internal part instead of the whole root operation, a mutation
250
+ payload without `instrument`, a `Client` mutation method without the observability keywords, or instrumented lock
251
+ info queries are findings; the swarm elements are known debt.
252
+ - Visitor shape: `class << self`, method name = event name (`__` between phase and detail), same name in the
253
+ Log and Instr visitors, first line `return unless log_sampled` (`&& log_lock_try` for `TryToLock` steps), one
254
+ `logger.debug do ... end rescue nil` with `"[redis_queued_locks.<event>] "` and `key => 'value'` pairs, or
255
+ `instrumenter.notify('redis_queued_locks.<event>', { ... }) rescue nil` with shorthand Symbol keys (`#5`-`#9`).
256
+ - Parameter order: port, sampled flag, extra gates, `lock_key`, data; `rw_mode` after `access_strategy` (logs) or
257
+ 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`.
259
+ - Sampling decided once per operation (`Logging.should_log?` / `Instrument.should_instrument?`) and passed down (`#7`).
260
+ - Event names and payload keys are public API: no rename/removal without the user's request; no new event on
261
+ the default write success path (`#13`); new or changed log keys/payload keys go to README `## Logging` /
262
+ `### Instrumentation Events` and CHANGELOG (`#14`).
263
+
264
+ ### H. Types (`type-checking.md`)
265
+ - Every changed method, param, return shape, constant or ivar is mirrored in `sig/` (`#1`): nested module blocks,
266
+ `use RedisQueuedLocks as RQL` / `use RedisClient as RC`, shared types (`RC::client`, `RQL::loggerObj`,
267
+ `RQL::instrObj`, `samplerObj`), `type xxxResult = { ok: bool, result: ... }` for public results, literal types for
268
+ swarm internals, `Hash[String, ...]` instead of String-keyed records (they crash Steep).
269
+ - `# steep:ignore` only where Steep reports a diagnostic (strict mode fails on redundant ignores); prefer
270
+ `# @type var x: T` / `#: T` and tuple annotations of pipeline results; a needed ignore without an honest
271
+ annotation carries `NOTE: (steep)`; never shape runtime code around Steep (`#5`). `Steepfile` is never loosened (`#6`).
272
+ - RBS must match runtime values (runtime type checks run the suite): e.g. `Integer|Float` where a Float can arrive.
273
+
274
+ ### I. Ruby style of the codebase
275
+ - `# frozen_string_literal: true`; compact constant paths (`module RedisQueuedLocks::Acquirer::Keys`); namespace
276
+ files only `require_relative` (`logic.md#1`).
277
+ - 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`).
281
+ - Comments: `# NOTE:` with `# - item;` lists, `# Step N.x` in algorithms, `(RW)`/`(lock index)`/`(keyspace)`
282
+ tags, `NOTE: (pipeline layout)`/`(transaction layout)` for positional replies.
283
+ - Redis: uppercase String commands, String numeric args (`'0'`, `'-1'`, `'-inf'`, `'+inf'`), `redis.with do |rconn|`
284
+ for multi-command work, `multi do |transact|`, `pipelined do |pipeline|`, named reply variables (`hget_cmd_res`,
285
+ `lock_state`), Lua as a frozen `<<~LUA_SCRIPT.strip.tr("\n", '').freeze` constant (`acquirer.md#5`, `#6`).
286
+ - Normalization with `Float(...)`/`Integer(...)` inside `tap do |lock_data|`, optional fields guarded with
287
+ `if lock_data['x']` (`acquirer.md#11`).
288
+ - Idioms: `x == nil`/`x != nil` or truthiness (not `.nil?`); `raise(Klass, "message")` with `\` continuations;
289
+ subject-less `case` with `start_with?` instead of regexps on hot paths; shorthand `key:` forwarding;
290
+ `clock_gettime` (µs) for durations reported as `((finish - start) / 1_000.0).ceil(2)` ms, `Time.now.to_f` only for
291
+ `at:`/`ts`; names `rconn`, `transact`, `pipeline`, `*_key`, `*_queue`, `acquirer_id`/`host_id` (`acq_id`/`hst_id`
292
+ in data, logs and payloads), private `fully_*`, `scan_*`/`index_*` lookups, `swarmed__*` predicates.
293
+ - Rubocop: only specific `Metrics`/`Layout` cops disabled locally with a paired `enable`, never
294
+ `# rubocop:disable all` in new code (`logic.md#10`); long signatures are not "fixed" (`arguments.md#8`).
295
+ - 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.
297
+
298
+ ### J. Docs and process
299
+ - The same change updates `.claude/project-overview.md` and the affected `.claude/rules/*.md` for every new/changed
300
+ key, option, result shape, event, algorithm step or limitation (`logic.md#12`), README and CHANGELOG
301
+ `[Unreleased]` for user-visible behavior, and the YARD of the touched `Client` methods.
302
+ - Commits under review (`git.md`): subject only `[<scope>] <summary>`, lowercase, imperative, no trailing period,
303
+ no body except the attribution trailer, never on `master`.
304
+
305
+ ## 5. Do not report
306
+
307
+ - Settled design (overview, `acquirer.md`, `swarm.md`): long explicit parameter lists and disabled
308
+ `Metrics/ParameterLists`; logic duplicated between operation modules (report only diverged copies);
309
+ read→write upgrade as an error unless `:wait_for_lock`; nested reads through `conflict_strategy`; no fairness
310
+ between modes with `:random`; best-effort cross-host FIFO; no Lua in the RW path; one mode per lock series;
311
+ `lock_index_enabled` as a config-only mode; the lock index defaulting to `false`.
312
+ - Unused `Resource` methods marked `# NOTE: (debug)` (manual runtime checks, `logic.md#17`); report a debug method
313
+ only when it is wrong for a key family it can get.
314
+ - Known limitations kept by the user: `YieldExpire` write release (`EXPIRE 0`) and ttl decrease without an owner
315
+ check; `HMGET`→`DEL` without `WATCH` in `ReleaseLocksOf`/`FlushZombies`; `unlock` dropping the data of a reader
316
+ 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;
319
+ `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.
321
+ - "Recommendations (proposed, not yet project policy)" in the rule files: never violations; mention one only as an
322
+ optional suggestion for touched code.
323
+ - Pre-existing problems outside the diff: out of scope; mention a severe one briefly in the template's
324
+ "outside the diff" section.
325
+ - Anything in `*_spec.rb` except §3.
326
+
327
+ ## 6. Verify every finding
328
+
329
+ - Confirm it in the code (call sites, copies, RBS, specs), not by pattern match: concurrency claims name the
330
+ 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.
332
+ - Drop findings contradicted by §5, by a design `NOTE` in the code or by the overview. Mark the rest
333
+ `CONFIRMED` (traced end to end) or `PLAUSIBLE` (depends on an assumption: name it).
334
+ - Helpful greps:
335
+ - added key literals and scans (outside `resource.rb` every `'rql:` literal is a finding):
336
+ `git diff <base> -U0 -- lib | grep '^+' | grep -v '^+\s*#' | grep -n "'rql:\|\"rql:\|'KEYS'\|'SCAN'\|\.scan(\|\.zscan("`;
337
+ - `git grep -n LOCK_INDEX_KEY -- lib`: every use reachable only with `lock_index_enabled` (an `if`/`return unless`
338
+ guard or a lock index lookup branch), except `ReindexLocks` and `Resource`;
339
+ - cross-module calls: `git grep -n "RedisQueuedLocks::Acquirer::" -- lib/redis_queued_locks/acquirer` (only the
340
+ known `WithAcqTimeout` debt and `LockSeriesPoC` are allowed);
341
+ - `git diff --name-only <base>` against the `sig/` mirror (§2 exceptions); `git diff --check <base>`.
342
+ - Checks (read-only): a review on request of local changes runs `bundle exec rake rubocop` and
343
+ `bundle exec rbs collection install && bundle exec rake steep:check` (grep the output for `ERROR`/`FATAL`, restore a
344
+ 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.
347
+
348
+ ## 7. Report
349
+
350
+ ### Template language
351
+ The report template is chosen by the Claude Code `language` setting:
352
+ 1. Take the effective value: the first settings file that defines `language`, in precedence order (run from the
353
+ repository root; Ruby is always available here, `jq` may be not):
354
+ ```sh
355
+ for f in "/Library/Application Support/ClaudeCode/managed-settings.json" /etc/claude-code/managed-settings.json \
356
+ .claude/settings.local.json .claude/settings.json ~/.claude/settings.json; do
357
+ [ -f "$f" ] && v=$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["language"].to_s' "$f" 2>/dev/null) \
358
+ && [ -n "$v" ] && { echo "$v"; break; }
359
+ done
360
+ ```
361
+ Settings passed with `--settings` override these files; the session's response-language instruction (it comes
362
+ from the same setting) then shows the effective value.
363
+ 2. Map the value case-insensitively to a template in `.claude/skills/rql-code-review/templates/`:
364
+
365
+ | `language` value | Template |
366
+ |---|---|
367
+ | Russian: `russian`, `русский`, `ru`, `ru-RU`, `ru_RU`, ... | `report.ru.md` |
368
+ | English (`english`, `en`, `en-US`, ...), any language without a template (`japanese`, `de`, ...), no value | `report.en.md` |
369
+
370
+ 3. Read the chosen template and fill it in. The whole report (headings, labels, verdict, findings) is written in the
371
+ template language; code identifiers, rule names, quotes and the `CONFIRMED`/`PLAUSIBLE` markers stay as they
372
+ are. Text outside the report (the offer to fix) follows the session language.
373
+
374
+ Both templates have the same structure: change them together. A new language = a new `report.<code>.md` with the
375
+ same structure plus a row in the table above.
376
+
377
+ ### 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.
383
+ - Minor: convention deviations with little impact (YARD tags, comment format, naming, idioms).
384
+ - Nit: optional polish.
385
+ - Order by severity, then by file. Empty sections are omitted; with no findings the verdict says so and the sync and
386
+ checks sections list what was checked.
@@ -0,0 +1,31 @@
1
+ ## Review: <scope> (<N> files, base <sha>)
2
+ **Verdict:** <blockers found | mergeable after fixes | no findings> — <one sentence why>
3
+
4
+ ### Blockers
5
+ 1. `<path>:<line>` — <the defect in one sentence>. [CONFIRMED | PLAUSIBLE: <assumption>]
6
+ Scenario: <input / state / interleaving → wrong result>.
7
+ Rule: <`acquirer.md#16` | overview "Lock index">.
8
+ Fix: <what to do>.
9
+
10
+ ### Major
11
+ <findings in the same format>
12
+
13
+ ### Minor
14
+ <findings in the same format>
15
+
16
+ ### Nits
17
+ <findings in the same format>
18
+
19
+ ### Docs and types sync
20
+ - RBS: <ok | what is missing>
21
+ - overview and rules: <ok | what is missing>
22
+ - README and CHANGELOG: <ok | what is missing>
23
+ - YARD (`@since`/`@version`): <ok | what is missing>
24
+
25
+ ### Checks
26
+ - rubocop: <result>
27
+ - steep: <result>
28
+ - rspec: <not run (reason) | result>
29
+
30
+ ### Outside the diff and suggestions
31
+ - <a severe problem outside the diff or an optional suggestion>
@@ -0,0 +1,31 @@
1
+ ## Ревью: <scope> (<N> файлов, base <sha>)
2
+ **Вердикт:** <есть блокеры | можно мержить после правок | замечаний нет> — <одна фраза почему>
3
+
4
+ ### Блокеры
5
+ 1. `<path>:<line>` — <дефект одним предложением>. [CONFIRMED | PLAUSIBLE: <допущение>]
6
+ Сценарий: <входные данные / состояние / интерливинг → неверный результат>.
7
+ Правило: <`acquirer.md#16` | overview "Lock index">.
8
+ Исправление: <что сделать>.
9
+
10
+ ### Важные
11
+ <находки в том же формате>
12
+
13
+ ### Мелкие
14
+ <находки в том же формате>
15
+
16
+ ### Придирки
17
+ <находки в том же формате>
18
+
19
+ ### Синхронизация документации и типов
20
+ - RBS: <ок | чего не хватает>
21
+ - overview и rules: <ок | чего не хватает>
22
+ - README и CHANGELOG: <ок | чего не хватает>
23
+ - YARD (`@since`/`@version`): <ок | чего не хватает>
24
+
25
+ ### Проверки
26
+ - rubocop: <результат>
27
+ - steep: <результат>
28
+ - rspec: <не запускался (причина) | результат>
29
+
30
+ ### Вне диффа и предложения
31
+ - <серьёзная проблема вне диффа или необязательное предложение>