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.
- checksums.yaml +4 -4
- data/.claude/project-overview.md +62 -11
- data/.claude/rules/acquirer.md +29 -11
- data/.claude/rules/arguments.md +3 -2
- data/.claude/rules/git.md +3 -2
- data/.claude/rules/logic.md +4 -3
- data/.claude/rules/swarm.md +12 -7
- data/.claude/rules/tests.md +6 -0
- data/.claude/rules/type-checking.md +2 -1
- data/.claude/rules/visitors.md +1 -1
- data/.claude/skills/rql-code-review/SKILL.md +372 -0
- data/.claude/skills/rql-code-review/templates/report.en.md +31 -0
- data/.claude/skills/rql-code-review/templates/report.ru.md +31 -0
- data/CHANGELOG.md +68 -0
- data/CLAUDE.md +4 -2
- data/README.md +257 -22
- data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +684 -128
- data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +3 -1
- data/lib/redis_queued_locks/acquirer/acquire_lock.rb +24 -3
- data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +145 -18
- data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +71 -10
- data/lib/redis_queued_locks/acquirer/keys.rb +142 -7
- data/lib/redis_queued_locks/acquirer/lock_info.rb +10 -4
- data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +26 -7
- data/lib/redis_queued_locks/acquirer/locks.rb +195 -83
- data/lib/redis_queued_locks/acquirer/queue_info.rb +12 -18
- data/lib/redis_queued_locks/acquirer/queues.rb +114 -50
- data/lib/redis_queued_locks/acquirer/reindex_locks.rb +140 -0
- data/lib/redis_queued_locks/acquirer/release_all_locks.rb +147 -58
- data/lib/redis_queued_locks/acquirer/release_lock.rb +52 -19
- data/lib/redis_queued_locks/acquirer/release_locks_of.rb +235 -66
- data/lib/redis_queued_locks/acquirer.rb +1 -0
- data/lib/redis_queued_locks/client.rb +97 -11
- data/lib/redis_queued_locks/config.rb +5 -1
- data/lib/redis_queued_locks/resource.rb +99 -2
- data/lib/redis_queued_locks/swarm/flush_zombies.rb +332 -61
- data/lib/redis_queued_locks/swarm/zombie_info.rb +203 -53
- data/lib/redis_queued_locks/swarm.rb +15 -4
- data/lib/redis_queued_locks/version.rb +2 -2
- data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +107 -1
- data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +1 -1
- data/sig/redis_queued_locks/acquirer/acquire_lock.rbs +2 -0
- data/sig/redis_queued_locks/acquirer/clear_dead_requests.rbs +10 -1
- data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +5 -2
- data/sig/redis_queued_locks/acquirer/keys.rbs +11 -1
- data/sig/redis_queued_locks/acquirer/locks.rbs +15 -6
- data/sig/redis_queued_locks/acquirer/queues.rbs +9 -2
- data/sig/redis_queued_locks/acquirer/reindex_locks.rbs +31 -0
- data/sig/redis_queued_locks/acquirer/release_all_locks.rbs +8 -1
- data/sig/redis_queued_locks/acquirer/release_lock.rbs +3 -1
- data/sig/redis_queued_locks/acquirer/release_locks_of.rbs +20 -1
- data/sig/redis_queued_locks/client.rbs +8 -0
- data/sig/redis_queued_locks/resource.rbs +6 -0
- data/sig/redis_queued_locks/swarm/flush_zombies.rbs +51 -1
- data/sig/redis_queued_locks/swarm/zombie_info.rbs +58 -7
- data/sig/vendor/redis_client.rbs +1 -0
- 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
|
|
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
|
|
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`.
|