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
@@ -0,0 +1,372 @@
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/`: the typo `@since 19.0.0` wins it.
92
+ A value older than `VERSION` on new code, or a behavior change without a `@version` bump, is a Minor finding.
93
+
94
+ ## 3. Spec files are exempt (until the suite rework)
95
+
96
+ `spec/**/*_spec.rb` are not ideal and are going to be rewritten entirely (header `NOTE` of
97
+ `spec/redis_queued_locks_spec.rb`, overview "Conventions", rspec-retry as temporary debt). Until then any coding
98
+ principle may be broken there. Never report the way specs are written: duplication, long or nested examples, `sleep` waits,
99
+ anonymous fakes, magic numbers, naming, structure, `it`/`specify`, rubocop directives, thread handling,
100
+ cleanup style, retry reliance, file size, the `tests.md` rules on writing specs and its recommendations.
101
+
102
+ Still in scope (the exemption covers how specs are written, not whether the library is tested):
103
+ 1. Coverage of the changed library behavior (finding against the `lib/` change, category `test-coverage`):
104
+ new behavior without an example; RW changes without both modes (`tests.md#13`); lock-iterating changes without
105
+ the small-batch examples of both lookups and the recorded-command checks (`acquirer.md#7`); new lock index
106
+ writes without `describe 'lock index'` examples (`acquirer.md#16`); a changed pipeline layout without an updated
107
+ `lock state pipeline layout` spec (`acquirer.md#15`); a touched obtaining transaction that the spec
108
+ `the lock obtaining transaction indexes the lock` no longer exercises (it drops the index entry right before the
109
+ obtaining `MULTI`); a new log/instrumentation event without a fake logger/notifier assertion (`visitors.md#11`).
110
+ 2. A changed spec that asserts behavior contradicting the overview/rules, or that can't fail for the behavior
111
+ it claims to check (vacuous expectation, expectation never reached).
112
+
113
+ `spec/spec_helper.rb` and `spec/setup_simplecov.rb` are support files: review them for correctness only (the
114
+ `around` hook that switches `config['lock_index_enabled']` and restores the library default).
115
+
116
+ ## 4. Checklist
117
+
118
+ ### A. Lock safety and Redis correctness
119
+ - Mutual exclusion only from the lock state checked under `WATCH`/`MULTI` (`logic.md#6`, `#14`); Lua only for the
120
+ existing write TTL extension/decrease, no new Lua in the RW acquisition path unless the user asked. No
121
+ check-then-set in Ruby without a transaction; queues, client clocks, the lock index and the pre-check never
122
+ decide mutual exclusion (the pre-check never obtains the lock).
123
+ - Asymmetric WATCH (`acquirer.md#12`): writers watch `[lock_key, lock_readers_key]`, readers `[lock_key]` only.
124
+ No write to a watched key outside `MULTI` after `WATCH` (aborts the own `EXEC`); never merge `WATCH` into a
125
+ pipeline (unsafe on reconnect).
126
+ - Lock state: write needs no write lock and no live readers; read needs no write lock; same-acquirer cases go
127
+ through `conflict_strategy`; read→write upgrade never works through (`:conflict_lock_upgrade`, waits with
128
+ `:wait_for_lock`).
129
+ - Time: reader liveness and expirations in Redis time (`TIME` → `Resource.redis_time_ms`, `PEXPIRE`); client
130
+ `Time.now.to_f` only for queue positions, `ts` fields, zombie probes; lock index scores in
131
+ `Resource.lock_index_time` (epoch ms) + grace. Readers are live only while `score > now`: never decide by
132
+ `ZCARD`/`EXISTS` alone.
133
+ - TTLs: max-TTL keys (registry, reader data, queues) get the `PEXPIRE NX` + `PEXPIRE GT` pair (`GT` alone sets
134
+ nothing on a key without TTL); `PTTL` sentinels handled (`-2` missing, `-1` → `Float::INFINITY` / `'+inf'`);
135
+ units right (ttl ms, `queue_ttl` s → `(queue_ttl * 1_000).ceil`, `zombie_ttl` ms → `/ 1_000.0`), integers for
136
+ `PEXPIRE`.
137
+ - Replies read by position (`acquirer.md#15`): `NOTE: (pipeline layout)` next to the commands, conditional
138
+ commands form a tail after the unconditional ones and are read under the same condition, position constants
139
+ (`LOCK_STATE_*`, `QUEUE_HEAD_REPLY`, `KEY_STATE_REPLIES`, `LOCK_STATE_REPLIES`) and the layout spec updated;
140
+ the reentrant write extension check `result.size == (lock_index_enabled ? 5 : 4)` follows its `MULTI`.
141
+ - Round trips of the hot path must not regress silently (overview "Lock acquisition flow"): `lock {}` = 4, a
142
+ failed attempt = 3, a failed retry = 1 (pre-check), `fail_fast` on an obtained lock = 1, `unlock` without
143
+ readers = 1. Independent reads share one `pipelined` round trip (also inside `multi(watch:)`).
144
+ - New destructive paths (delete/shorten a lock) check the owner, unless the change documents the trade-off as
145
+ accepted by the user (like the known limitations in §5).
146
+ - Shared mutable state behind `Utilities::Lock#synchronize` (`logic.md#9`).
147
+
148
+ ### B. Read/write awareness (`logic.md#13`, `acquirer.md#12`)
149
+ - Every operation over locks, queues, zombies or lock info covers `rql:lock:*`, `rql:lock_queue:*`,
150
+ `rql:lock_readers:*`, `rql:lock_read_queue:*` and `rql:lock_reader:<name>:<acq_id>`; every attempt prunes dead
151
+ requests in both queues; dequeue/cleanup removes the request from the queue of its mode.
152
+ - Write-only result shapes stay unchanged; read keys/fields appear only when read data exists.
153
+ - `read_lock_key` is `nil` for write requests; reader data is a data carrier only (the registry decides existence),
154
+ without `acq_id`/`hst_id`; don't store derivable values; a new lock field updates every formatting copy (§2,
155
+ `acquirer.md#11`); `rw_mode` stays a reserved `meta` key.
156
+ - Backward compatibility: data written by older versions (readers with `acq_id`/`hst_id`, names without index
157
+ entries) is still read correctly; a key/data format change states the rolling-deploy requirement in README and
158
+ CHANGELOG.
159
+
160
+ ### C. Lock index, both lookup modes (`acquirer.md#16`, overview "Lock index")
161
+ - Disabled mode (default): no lock index command at all (`LOCK_INDEX_KEY` only under `if lock_index_enabled` /
162
+ after `return unless lock_index_enabled`), no `ZSCAN`, queues without TTL. Enabled mode: no keyspace `SCAN`
163
+ except `ReindexLocks`.
164
+ - Superset invariant: every key-creating command is followed by `ZADD <index> GT <now + lifetime + grace> <name>`
165
+ in the same pipeline/transaction, after the key command; the obtaining `MULTI` indexes the lock itself; every
166
+ extension is followed by `ZADD XX INCR`; scores never decrease; names are removed only in the same `MULTI`
167
+ that deletes all their keys (`ReleaseLocksOf`, `YieldExpire` releases and `FlushZombies` never remove names).
168
+ - Lock index commands are conditional tails: reply positions never depend on the mode.
169
+ - A new key family: distinct prefix `rql:lock_<kind>:<name>` (never a suffix of the name), bounded TTL in the
170
+ enabled mode, covered by the index score, `Resource.lock_key_family`/`lock_name_from_key`, both lookups and
171
+ `ReindexLocks`.
172
+ - `lock_index_enabled` is client-wide (`arguments.md#9`): read from `config` inside `Client`/`Swarm` and forwarded,
173
+ never a keyword of a public method; the flusher Ractor gets a copy. `lock_index_grace_period` is forwarded by
174
+ `lock`, `lock!`, `lock_series`, `lock_series!`, `reindex_locks`. Pruning (`ZREMRANGEBYSCORE <index> -inf
175
+ <now - 1>`) stays in the queueing pipeline, `flush_zombies`, `clear_locks`, `clear_dead_requests`.
176
+
177
+ ### D. Iteration and performance (`acquirer.md#7`, `arguments.md#7`)
178
+ - One pass per operation: one `SCAN` over `KEY_PATTERN`/`ALL_LOCK_QUEUES_PATTERN` classified by
179
+ `Resource.lock_key_family`, or one `ZSCAN` via `each_lock_name_batch` (expired scores skipped). Never `KEYS`,
180
+ never one round trip per key; per-key commands in pipelined batches collected across pages, plus the
181
+ remainder batch.
182
+ - A new lock-iterating operation implements both lookups side by side, chooses the lookup in one place
183
+ (`lock_index_enabled ? index_x : scan_x` or one `if`/`else` with `(lock index)`/`(keyspace)` step comments) and
184
+ keeps the shared processing in one copy.
185
+ - No `EXISTS` next to `ZRANGE`/`HGETALL`: Redis never keeps empty sorted sets/hashes.
186
+ - Hot path (`AcquireLock`, `TryToLock`, `YieldExpire`, `DequeueFromLockQueue`, visitor call sites): no wrapper
187
+ objects, hash merging, splats to build arguments, closures for argument plumbing, new `{}` per attempt (use
188
+ `NO_LOCK_DATA`); keys computed once in `acquire_lock`; log-only Redis reads gated at the call site
189
+ (`acquirer.md#13`); no `Set` splatted in a loop.
190
+
191
+ ### E. Architecture and API design
192
+ - `Client` only fills defaults from `config['...']` and delegates (`logic.md#3`); an `x!` variant exists only when it
193
+ must raise and repeats the full keyword list.
194
+ - No parameter objects, `Struct`/`Data.define`/`OpenStruct`, builders, `**opts`, `options = {}`, `...` forwarding
195
+ (`arguments.md#1`, `#3`); a new public option goes to every `Client` variant with a `config` default
196
+ (`arguments.md#2`, `#10`) and is forwarded by name with shorthand; defaults live only in `Client`, internal
197
+ params are required (`arguments.md#4`); positional order: subject → domain data → observability tail
198
+ `logger, instrumenter, instrument, log_sampling_*, log_sample_this, instr_sampling_*, instr_sample_this`, lock key
199
+ params `lock_key, read_write_mode, lock_key_queue, read_lock_key_queue, lock_readers_key, read_lock_key, lock_name,
200
+ acquirer_id, host_id, ...`, TTL params `ttl, queue_ttl, lock_index_enabled, lock_index_grace_period`
201
+ (`arguments.md#5`); every call site and the RBS updated with the signature.
202
+ - Operation modules are independent (`acquirer.md#14`): no calls to another `Acquirer::*` module and no references to
203
+ its RBS types (exceptions: the `AcquireLock` core, `*PoC` modules); shared logic only via `Resource`/`Utilities`;
204
+ a duplicate keeps the original name and shape with a `NOTE` that it is duplicated on purpose; every copy changes
205
+ in the same change. Don't copy the `WithAcqTimeout` → `LockInfo`/`QueueInfo` debt.
206
+ - New operation: `acquirer/<snake_name>.rb` with one public `class << self` entry named after the file and private
207
+ helpers, `require_relative` in `acquirer.rb`, thin `Client` method, RBS, spec, README/CHANGELOG
208
+ (`acquirer.md#1`). Mutating operations follow the two-layer pattern (timing wrapper + private `fully_*`,
209
+ `acquirer.md#2`).
210
+ - Results: `{ ok:, result: }` only at the public boundary (`Client`, `Acquirer::*`, `Swarm` facade actions) with
211
+ Symbol statuses and abbreviated Symbol keys (`rel_*`, `*_cnt`, `*_time`); info queries return a String-keyed
212
+ Hash/Set or `nil`; swarm element internals return bare scalars (`swarm.md#15`). Raise only in `x!` methods, the
213
+ `raise_errors` path of `AcquireLock` and on invalid arguments (`RedisQueuedLocks::ArgumentError`), with the lock
214
+ key / acquirer id in the message.
215
+ - Keys only via `Resource.prepare_*`, `*_PATTERN`, `SWARM_KEY`, `LOCK_INDEX_KEY` (`logic.md#5`): flag every new
216
+ `'rql:...'` literal outside `resource.rb`.
217
+ - Config: `setting` + `validate` in `config.rb` (booleans `val == true || val == false`, Integers with ranges), dotted
218
+ keys for groups, unit in a trailing `# NOTE:` comment, read as `config['key']` (`logic.md#7`). Errors:
219
+ `class XError < Error; end` in `errors.rb`. Runtime dependencies: `redis-client` only; dev gems in `Gemfile`.
220
+ - `AcquireLock`: new behavior as a new step or mixin with `# Step N.x` numbering and consistent `acq_process` keys
221
+ (`acquirer.md#10`); `lock_series_poc.rb` is experimental: don't accept new features built on it without the
222
+ user's request.
223
+
224
+ ### F. Swarm, Ractors, threads (`swarm.md`)
225
+ - Redis work in stateless class functions shared by the main loop and the manual API (`#1`); main loops build
226
+ their own client with `RedisClientBuilder` inside the loop thread (`#4`).
227
+ - Into `Ractor.new` only copied plain values (`config.slice(...)`, scalars, `lock_index_enabled`): no `rql_client`,
228
+ logger, Redis client; inside only shareable constants and module functions (`#4`, `#5`).
229
+ - Ports (`#13`): results port created in the main Ractor, commands port inside the element Ractor, `monitor`
230
+ registered before waiting, one reply per command, Symbol messages reserved for death notices; no
231
+ `Ractor.receive`/`Ractor#send`/default port; names end in `_port`, block params `r_res_p`/`r_com_p`.
232
+ - Threads: kill + join inside the Ractor (`#14`), `abort_on_exception = false` for main loops, no retry loops in
233
+ elements (`#8`); state changes and request/reply pairs inside `sync.synchronize` (`#6`); the supervisor is
234
+ stopped before elements are killed or re-created (`#9`).
235
+ - Lifecycle names and semantics identical in both bases (`#7`); a new element is registered everywhere (`#3`);
236
+ status shapes `{ running:, state: }` with `'non_initialized'` and their RBS (`#10`).
237
+
238
+ ### G. Logging and instrumentation (`visitors.md`)
239
+ - Logs/events of `AcquireLock` and its mixins and of `LockSeriesPoC` go through visitor methods (`#1`); release
240
+ modules call `instrumenter.notify` inline inside `run_non_critical`, gated by `instr_sampled` (`acquirer.md#9`).
241
+ - Visitor shape: `class << self`, method name = event name (`__` between phase and detail), same name in the
242
+ Log and Instr visitors, first line `return unless log_sampled` (`&& log_lock_try` for `TryToLock` steps), one
243
+ `logger.debug do ... end rescue nil` with `"[redis_queued_locks.<event>] "` and `key => 'value'` pairs, or
244
+ `instrumenter.notify('redis_queued_locks.<event>', { ... }) rescue nil` with shorthand Symbol keys (`#5`-`#9`).
245
+ - Parameter order: port, sampled flag, extra gates, `lock_key`, data; `rw_mode` after `access_strategy` (logs) or
246
+ after `lock_key` (instrumentation), user `instrument` last (`#6`). `rw_mode` is the requested mode in
247
+ `AcquireLock`/`TryToLock`/`DequeueFromLockQueue`, the held mode in `YieldExpire`, the series mode in `LockSeriesPoC`.
248
+ - Sampling decided once per operation (`Logging.should_log?` / `Instrument.should_instrument?`) and passed down (`#7`).
249
+ - Event names and payload keys are public API: no rename/removal without the user's request; no new event on
250
+ the default write success path (`#13`); new or changed log keys/payload keys go to README `## Logging` /
251
+ `### Instrumentation Events` and CHANGELOG (`#14`).
252
+
253
+ ### H. Types (`type-checking.md`)
254
+ - Every changed method, param, return shape, constant or ivar is mirrored in `sig/` (`#1`): nested module blocks,
255
+ `use RedisQueuedLocks as RQL` / `use RedisClient as RC`, shared types (`RC::client`, `RQL::loggerObj`,
256
+ `RQL::instrObj`, `samplerObj`), `type xxxResult = { ok: bool, result: ... }` for public results, literal types for
257
+ swarm internals, `Hash[String, ...]` instead of String-keyed records (they crash Steep).
258
+ - `# steep:ignore` only where Steep reports a diagnostic (strict mode fails on redundant ignores); prefer
259
+ `# @type var x: T` / `#: T` and tuple annotations of pipeline results; a needed ignore without an honest
260
+ annotation carries `NOTE: (steep)`; never shape runtime code around Steep (`#5`). `Steepfile` is never loosened (`#6`).
261
+ - RBS must match runtime values (runtime type checks run the suite): e.g. `Integer|Float` where a Float can arrive.
262
+
263
+ ### I. Ruby style of the codebase
264
+ - `# frozen_string_literal: true`; compact constant paths (`module RedisQueuedLocks::Acquirer::Keys`); namespace
265
+ files only `require_relative` (`logic.md#1`).
266
+ - YARD on every class, module, constant, attr and method: `@param`/`@option`/`@return` (`Format: { ... }` for hash
267
+ results), blank `#`, `@api public|private`, `@since <next>`; behavior changes bump `@version <next>` on the
268
+ method and on the module/class header (`logic.md#2`).
269
+ - Comments: `# NOTE:` with `# - item;` lists, `# Step N.x` in algorithms, `(RW)`/`(lock index)`/`(keyspace)`
270
+ tags, `NOTE: (pipeline layout)`/`(transaction layout)` for positional replies.
271
+ - Redis: uppercase String commands, String numeric args (`'0'`, `'-1'`, `'-inf'`, `'+inf'`), `redis.with do |rconn|`
272
+ for multi-command work, `multi do |transact|`, `pipelined do |pipeline|`, named reply variables (`hget_cmd_res`,
273
+ `lock_state`), Lua as a frozen `<<~LUA_SCRIPT.strip.tr("\n", '').freeze` constant (`acquirer.md#5`, `#6`).
274
+ - Normalization with `Float(...)`/`Integer(...)` inside `tap do |lock_data|`, optional fields guarded with
275
+ `if lock_data['x']` (`acquirer.md#11`).
276
+ - Idioms: `x == nil`/`x != nil` or truthiness (not `.nil?`); `raise(Klass, "message")` with `\` continuations;
277
+ subject-less `case` with `start_with?` instead of regexps on hot paths; shorthand `key:` forwarding;
278
+ `clock_gettime` (µs) for durations reported as `((finish - start) / 1_000.0).ceil(2)` ms, `Time.now.to_f` only for
279
+ `at:`/`ts`; names `rconn`, `transact`, `pipeline`, `*_key`, `*_queue`, `acquirer_id`/`host_id` (`acq_id`/`hst_id`
280
+ in data, logs and payloads), private `fully_*`, `scan_*`/`index_*` lookups, `swarmed__*` predicates.
281
+ - Rubocop: only specific `Metrics`/`Layout` cops disabled locally with a paired `enable`, never
282
+ `# rubocop:disable all` in new code (`logic.md#10`); long signatures are not "fixed" (`arguments.md#8`).
283
+ - Suggestions only (`logic.md` recommendations, not violations): correctly spelled new identifiers (don't copy the
284
+ known typos), `then` instead of `yield_self` in new code.
285
+
286
+ ### J. Docs and process
287
+ - The same change updates `.claude/project-overview.md` and the affected `.claude/rules/*.md` for every new/changed
288
+ key, option, result shape, event, algorithm step or limitation (`logic.md#12`), README and CHANGELOG
289
+ `[Unreleased]` for user-visible behavior, and the YARD of the touched `Client` methods.
290
+ - Commits under review (`git.md`): subject only `[<scope>] <summary>`, lowercase, imperative, no trailing period,
291
+ no body except the attribution trailer, never on `master`.
292
+
293
+ ## 5. Do not report
294
+
295
+ - Settled design (overview, `acquirer.md`, `swarm.md`): long explicit parameter lists and disabled
296
+ `Metrics/ParameterLists`; logic duplicated between operation modules (report only diverged copies);
297
+ read→write upgrade as an error unless `:wait_for_lock`; nested reads through `conflict_strategy`; no fairness
298
+ between modes with `:random`; best-effort cross-host FIFO; no Lua in the RW path; one mode per lock series;
299
+ `lock_index_enabled` as a config-only mode; the lock index defaulting to `false`.
300
+ - Known limitations kept by the user: `YieldExpire` write release (`EXPIRE 0`) and ttl decrease without an owner
301
+ check; `HMGET`→`DEL` without `WATCH` in `ReleaseLocksOf`/`FlushZombies`; `unlock` dropping the data of a reader
302
+ re-acquired between its two round trips; `zombie_acquirers` vs `flush_zombies` acquirer sets.
303
+ - Known debt: `WithAcqTimeout` → `LockInfo`/`QueueInfo`; rspec-retry, disabled coverage minimum, non-blocking runtime
304
+ type checks; `acquier.rbs` and the `yield_with_expire` RBS path; typos `swarm_element__termiante`,
305
+ `@api ppublic`/`pulic`/`pub;ic`, `@since 19.0.0`; `sleep(0.1)` readiness waits and missing logging in the swarm;
306
+ one flusher per process; `EVAL` without `SCRIPT LOAD`; `# rubocop:disable all` around lock series.
307
+ - "Recommendations (proposed, not yet project policy)" in the rule files: never violations; mention one only as an
308
+ optional suggestion for touched code.
309
+ - Pre-existing problems outside the diff: out of scope; mention a severe one briefly in the template's
310
+ "outside the diff" section.
311
+ - Anything in `*_spec.rb` except §3.
312
+
313
+ ## 6. Verify every finding
314
+
315
+ - Confirm it in the code (call sites, copies, RBS, specs), not by pattern match: concurrency claims name the
316
+ interleaving (acquirer A at step X, B at step Y) and why `WATCH` doesn't catch it; layout claims count the
317
+ commands and positions; mode claims check both `lock_index_enabled` values and both `read_write_mode` values.
318
+ - Drop findings contradicted by §5, by a design `NOTE` in the code or by the overview. Mark the rest
319
+ `CONFIRMED` (traced end to end) or `PLAUSIBLE` (depends on an assumption: name it).
320
+ - Helpful greps:
321
+ - added key literals and scans (outside `resource.rb` every `'rql:` literal is a finding):
322
+ `git diff <base> -U0 -- lib | grep '^+' | grep -v '^+\s*#' | grep -n "'rql:\|\"rql:\|'KEYS'\|'SCAN'\|\.scan(\|\.zscan("`;
323
+ - `git grep -n LOCK_INDEX_KEY -- lib`: every use reachable only with `lock_index_enabled` (an `if`/`return unless`
324
+ guard or a lock index lookup branch), except `ReindexLocks` and `Resource`;
325
+ - cross-module calls: `git grep -n "RedisQueuedLocks::Acquirer::" -- lib/redis_queued_locks/acquirer` (only the
326
+ known `WithAcqTimeout` debt and `LockSeriesPoC` are allowed);
327
+ - `git diff --name-only <base>` against the `sig/` mirror (§2 exceptions); `git diff --check <base>`.
328
+ - Checks (read-only): a review on request of local changes runs `bundle exec rake rubocop` and
329
+ `bundle exec rbs collection install && bundle exec rake steep:check` (grep the output for `ERROR`/`FATAL`, restore a
330
+ rewritten `rbs_collection.lock.yaml` with `git checkout -- rbs_collection.lock.yaml`); the automatic pre-commit run
331
+ reports the completion checks instead of re-running them (see "When to run"). Don't run `rake rspec` without
332
+ asking: it needs a local Redis, `FLUSHDB`s db 0 and db 1 and takes ~6 minutes.
333
+
334
+ ## 7. Report
335
+
336
+ ### Template language
337
+ The report template is chosen by the Claude Code `language` setting:
338
+ 1. Take the effective value: the first settings file that defines `language`, in precedence order (run from the
339
+ repository root; Ruby is always available here, `jq` may be not):
340
+ ```sh
341
+ for f in "/Library/Application Support/ClaudeCode/managed-settings.json" /etc/claude-code/managed-settings.json \
342
+ .claude/settings.local.json .claude/settings.json ~/.claude/settings.json; do
343
+ [ -f "$f" ] && v=$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["language"].to_s' "$f" 2>/dev/null) \
344
+ && [ -n "$v" ] && { echo "$v"; break; }
345
+ done
346
+ ```
347
+ Settings passed with `--settings` override these files; the session's response-language instruction (it comes
348
+ from the same setting) then shows the effective value.
349
+ 2. Map the value case-insensitively to a template in `.claude/skills/rql-code-review/templates/`:
350
+
351
+ | `language` value | Template |
352
+ |---|---|
353
+ | Russian: `russian`, `русский`, `ru`, `ru-RU`, `ru_RU`, ... | `report.ru.md` |
354
+ | English (`english`, `en`, `en-US`, ...), any language without a template (`japanese`, `de`, ...), no value | `report.en.md` |
355
+
356
+ 3. Read the chosen template and fill it in. The whole report (headings, labels, verdict, findings) is written in the
357
+ template language; code identifiers, rule names, quotes and the `CONFIRMED`/`PLAUSIBLE` markers stay as they
358
+ are. Text outside the report (the offer to fix) follows the session language.
359
+
360
+ Both templates have the same structure: change them together. A new language = a new `report.<code>.md` with the
361
+ same structure plus a row in the table above.
362
+
363
+ ### Severity and order
364
+ - Blocker: breaks mutual exclusion or the lock index superset, loses or leaks locks/data, crashes, breaks the public
365
+ API (method, result shape, event, config key) or turns CI red.
366
+ - Major: a rule violation with a real consequence: a missed read key family or lookup mode, per-key round trips, a
367
+ hot-path regression, broken module independence, a missing RBS/docs update for a changed contract, new
368
+ behavior without specs.
369
+ - Minor: convention deviations with little impact (YARD tags, comment format, naming, idioms).
370
+ - Nit: optional polish.
371
+ - Order by severity, then by file. Empty sections are omitted; with no findings the verdict says so and the sync and
372
+ 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
+ - <серьёзная проблема вне диффа или необязательное предложение>
data/CHANGELOG.md CHANGED
@@ -1,5 +1,73 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.20.0] - 2026-10-12
4
+ ### Added
5
+ - **Lock index** (opt-in: `config['lock_index_enabled']`, `false` by default: lock-iterating operations scan the keyspace as before):
6
+ `rql:index:locks` (`ZSET`: lock name => the upper bound of the lifetime of all lock keys in client time, ms); with the enabled
7
+ lock index all lock-iterating operations find locks without the full keyspace `SCAN` (see the "Lock Index" README section):
8
+ - the client-wide mode (not a per-call option): the lock acquirement maintains the lock index and lock-iterating operations
9
+ read it only when it is enabled; use the same mode on all processes that share the Redis database (enable it everywhere,
10
+ then invoke `#reindex_locks` once); the swarm reads the mode when it is started;
11
+ - maintained by the lock acquirement itself without new Redis round trips: each lock request adds the lock name (`ZADD GT`
12
+ with the queue ttl), sets the request queue TTL and drops the expired lock names (`ZREMRANGEBYSCORE`) in the queueing pipeline
13
+ (4 more commands per attempt), each lock obtaining (write and read) adds the lock name with the lock ttl in the same
14
+ transaction (1 more command), each extension (`#extend_lock_ttl`, `:extendable_work_through` reentrant locks) increases
15
+ the score by the extension (`ZADD XX INCR`; `#extend_lock_ttl` of write locks sends `EVAL` and the index update in one
16
+ transaction);
17
+ - `#unlock` and `#clear_locks` drop the lock names from the index in the same transaction with the lock keys;
18
+ - lock request queues have TTL: the queue lives as long as its latest lock request (`PEXPIRE NX` + `GT` with the queue ttl on
19
+ each lock request), queues of dead acquirers expire by themselves;
20
+ - lock-iterating operations find locks and lock queues via one `ZSCAN` pass over the lock names (lock keys are derived from
21
+ the lock names): the cost depends on the number of locks only, keys of other applications are never visited; locks without
22
+ the lock index entry (older RQL versions, the disabled lock index) are not found until `#reindex_locks`:
23
+ - swarm (`#flush_zombies`, the zombie flusher, `#zombie_locks`/`#zombie_acquirers`/`#zombies_info`): `zombie_lock_scan_size` is
24
+ the `ZSCAN` count and the batch size of locks, `zombie_queue_scan_size` - the batch size of lock queues (both processed during
25
+ the same pass); `#flush_zombies` drops the expired lock names from the index;
26
+ - `#locks`/`#locks_info`, `#queues`/`#queues_info`, `#keys`: `scan_size` is the `ZSCAN` count and the batch size of locks;
27
+ `#keys` returns the existing keys of the lock key families of the indexed locks (other `rql:lock*` keys are not listed);
28
+ - `#clear_locks`: the keys of each batch of locks are deleted with one `DEL` in one transaction with the removal of their lock
29
+ names from the lock index; `batch_size` is the number of locks;
30
+ - `#clear_locks_of`/`#clear_current_locks`: `clear_locks_of__lock_scan_size` is the `ZSCAN` count and the batch size of locks,
31
+ `clear_locks_of__queue_scan_size` - the number of lock queues processed in one round trip;
32
+ - `#clear_dead_requests`: `scan_size` is the `ZSCAN` count and the number of locks (both lock queues of each lock) processed in
33
+ one round trip; the expired lock names are dropped from the index;
34
+ - `config['lock_index_grace_period']` (`60_000` ms by default) and `lock_index_grace_period:` option of `#lock`/`#lock!`/
35
+ `#lock_series`/`#lock_series!`: the lock name stays in the index for this period after the expiration of all its keys
36
+ (the client clock skew and network latency tolerance);
37
+ - `#reindex_locks`: adds the lock keys without the lock index entry (keys of older RQL versions and keys created with the disabled
38
+ lock index) to the lock index (one `SCAN` pass, lock queues without TTL get the queue ttl); **invoke it once after the lock index
39
+ is enabled on all your processes**;
40
+ ### Changed
41
+ - `#flush_zombies` (and the zombie flusher of the swarm): zombie lock requests are dropped with one `ZREM` per lock queue
42
+ (previously one `ZREM` per zombie acquirer and lock queue);
43
+ - `#queue_info` / `#queues_info`: lock queues are read without `EXISTS` checks (Redis does not keep empty sorted sets);
44
+ - Lock acquirement attempt: read lock requests do not fetch the longest read lock (it is used by write lock requests only),
45
+ write lock requests do not build the read lock data key, disabled try-lock logs get a shared frozen hash instead of a new one;
46
+ - Lock acquirement attempt: the lock request queueing (`ZADD NX`, expired requests pruning, queue heads) is one pipelined
47
+ round trip instead of three; with `:wait_for_lock`/`:dead_locking` conflict strategies (without `fail_fast`) it is sent
48
+ in the lock state round trip: `#lock` with a block takes 4 Redis round trips instead of 7, a failed attempt - 3 instead of 6;
49
+ - SCAN-based operations (`#clear_locks`, `#clear_locks_of`/`#clear_current_locks`, `#clear_dead_requests`, `#locks`/`#locks_info`,
50
+ `#queues`/`#queues_info`, `#flush_zombies`, `#zombie_locks`/`#zombie_acquirers`/`#zombies_info`) make one `SCAN` pass instead
51
+ of 2-5 passes (one per key family) and send per-key commands in pipelined batches instead of one round trip per key:
52
+ - `#clear_locks` deletes the found keys during the scan with one `DEL` per batch (previously all keys were expired at the end,
53
+ lock queues first);
54
+ - `#clear_locks_of`/`#clear_current_locks` (`clear_locks_of__queue_scan_size`) and `#flush_zombies` (`zombie_queue_scan_size`):
55
+ the queue scan size is the number of lock queues processed in one round trip (queues are found by the same pass as locks);
56
+ - `#zombie_locks`/`#zombie_acquirers`/`#zombies_info` do not scan the keyspace when there are no zombie hosts;
57
+ - Read lock data (`rql:lock_reader:<lock_name>:<acquirer_id>`) does not store `acq_id`/`hst_id` (both are derived from the acquirer id):
58
+ ~40% less memory per read lock (`MEMORY USAGE` 280 -> 168 bytes without `:meta`); `#lock_info`/`#locks_info` report them as before
59
+ (the first fields of each reader; read lock data of older versions is supported);
60
+ - `#unlock`: one Redis round trip when there are no read locks (the readers registry is read in the release transaction;
61
+ the read lock data is dropped with one more `DEL` only when read locks existed);
62
+ - `#lock_series` (PoC): write locks of the series are released (compare-and-delete) in one transaction for the whole series
63
+ (three round trips per series instead of three round trips per lock);
64
+ - `#flush_zombies` (and the zombie flusher of the swarm): requests of zombie hosts are dropped from the lock queues by the host
65
+ of the request (derived from its acquirer id): requests of dead hosts that hold no locks are dropped too (previously they stayed
66
+ until the queue ttl) and their acquirers are reported in `deleted_zombie_acquirers`;
67
+ - Lock acquirement: retries and `fail_fast` attempts pre-check the lock state without `WATCH` (one round trip): a retry that can not
68
+ obtain the lock takes 1 Redis round trip instead of 3 and a `fail_fast` attempt on an obtained lock - 1 instead of 3 (an attempt
69
+ that can obtain the lock continues with `WATCH`/`MULTI`: one more round trip); mutual exclusion still relies on `WATCH`/`MULTI` only;
70
+
3
71
  ## [1.19.0] - 2026-10-11
4
72
  ### Added
5
73
  - `#unlock_read` (aka `#release_read_lock`): an ability to unlock the read lock of any acquirer
data/CLAUDE.md CHANGED
@@ -14,6 +14,8 @@ Full details (config keys, key layout, events, errors, stack, CI): `.claude/proj
14
14
  - `acquirer/acquire_lock.rb`: composed via `extend` mixins in `acquire_lock/` (try_to_lock, with_acq_timeout, delay_execution, dequeue_from_lock_queue, yield_expire) + `LogVisitor`/`InstrVisitor` event hooks.
15
15
  - Locking: `ZADD NX` into the request queue, then WATCH/MULTI; Lua only for write TTL extension/decrease.
16
16
  - Read/Write locks (`read_write_mode: :write|:read`): write lock `rql:lock:<name>` + queue `rql:lock_queue:<name>`; readers registry `rql:lock_readers:<name>` (ZSET acq_id => Redis-time expiry), read queue `rql:lock_read_queue:<name>`, per-reader data `rql:lock_reader:<name>:<acq_id>`. Readers WATCH the write key only, writers WATCH the write key + registry; queues give ordering only. Details: overview "Read/Write locks".
17
+ - Lock lookup modes: `config['lock_index_enabled']` (client-wide, `false` by default) is read by `Client`/`Swarm` and passed to every lock-iterating module (swarm, `locks`, `queues`, `keys`, `clear_*`) and to the hot path; each module keeps the keyspace lookup (one `SCAN` pass, the pre-index code) and the lock index lookup (one `ZSCAN` pass) side by side with the shared processing in one copy.
18
+ - Lock index `rql:index:locks` (enabled mode only; ZSET lock name => upper bound of the lifetime of all lock keys, client ms + ttl + `lock_index_grace_period`): every key-creating command is followed by `ZADD GT` (queueing pipeline, obtaining `MULTI`), every extension by `ZADD XX INCR`, names are removed only with all their keys (`unlock`, `clear_locks`), queues have TTL, expired names are pruned on queueing; lock index commands are conditional tails of their pipelines/transactions (none in the disabled mode); `reindex_locks` (one keyspace `SCAN`, both modes) adopts keys without entries after enabling. Details: overview "Lock index".
17
19
  - Strategies: `access_strategy` `:queued`|`:random`; `conflict_strategy` `:wait_for_lock`|`:work_through`|`:extendable_work_through`|`:dead_locking`.
18
20
  - `resource.rb`: all key names and acquirer/host IDs. `config.rb`: `setting`/`validate` DSL, read as `config['a.b']`.
19
21
  - `swarm/`: zombie-lock cleanup. `Supervisor` thread revives `ProbeHosts` (`SwarmElement::Threaded`) and `FlushZombies` (`SwarmElement::Isolated`, Ractor talking via `Ractor::Port`s); each element runs a main-loop thread with its own Redis connection.
@@ -30,7 +32,7 @@ Detailed, path-scoped rules in `.claude/rules/`: `logic.md` (lib, general), `acq
30
32
  - Operation modules behind `Client` methods (all `Acquirer::*` except the `AcquireLock` acquisition core and PoC modules such as `LockSeriesPoC`, which may reuse other modules) are independent: they never call each other or reference each other's RBS types, share logic only via `Resource`/`Utilities`, and duplicate similar logic on purpose (independence over DRY; e.g. `Locks` duplicates the `LockInfo` formatting). Details: `acquirer.md`.
31
33
  - New option: `setting` (+ `validate`) in `config.rb`.
32
34
  - Dev gems go in `Gemfile`, not the gemspec.
33
- - Commits (`git.md`): subject only `[<scope>] <summary>` (several features: 1-2 words each, comma-separated), no body; when a step is complete (requirement fully implemented, specs/rubocop/steep green) ask before committing, with the proposed subject and a recap of the changes; an explicit "commit" request needs no extra question; never on `master`, never push.
35
+ - Commits (`git.md`): subject only `[<scope>] <summary>` (several features: 1-2 words each, comma-separated), no body; when a step is complete (requirement fully implemented, specs/rubocop/steep green), and only then, run the `rql-code-review` skill on the uncommitted changes, then ask before committing, with the proposed subject, a recap of the changes and the review verdict; an explicit "commit" request needs no extra question and no review (a commit authorized in advance for a task still gets the review); never on `master`, never push.
34
36
  - Before implementing, read the relevant sections of `.claude/project-overview.md` (key layout, lock flow, "Read/Write locks", events) and keep their invariants. In the same change, update the overview and the affected `.claude/rules/*.md` for every new/changed key, option, result shape, event, algorithm step or limitation (and README/CHANGELOG `[Unreleased]` for user-visible behavior). New or changed log keys / instrumentation payload keys (visitors or inline `notify`) also go to README `## Logging` / `### Instrumentation Events` (see `visitors.md`).
35
- - Lock-touching code is read/write-aware: any operation over locks, queues, zombies or lock info must cover the read family (`rql:lock_readers:*`, `rql:lock_read_queue:*`, `rql:lock_reader:*`) next to `rql:lock:*`/`rql:lock_queue:*`. Mutual exclusion comes only from WATCH/MULTI on lock state (no Lua in the RW acquisition path unless asked); queues never guarantee safety.
37
+ - Lock-touching code is read/write-aware and index-aware: any operation over locks, queues, zombies or lock info must cover the read family (`rql:lock_readers:*`, `rql:lock_read_queue:*`, `rql:lock_reader:*`) next to `rql:lock:*`/`rql:lock_queue:*`, and any new key-creating/extending command must keep the lock index a superset of the existing locks (enabled mode); a new lock-iterating operation implements both lookups (keyspace and lock index, `lock_index_enabled`). Mutual exclusion comes only from WATCH/MULTI on lock state (no Lua in the RW acquisition path unless asked); queues never guarantee safety.
36
38
  - Known debt: rspec-retry, disabled coverage minimum, runtime type-check CI uses `--failure-exit-code=0`.