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