redis_queued_locks 1.20.0 → 1.22.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 +249 -36
- data/.claude/rules/acquirer.md +16 -14
- 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 +34 -13
- data/.claude/rules/swarm.md +4 -5
- data/.claude/rules/tests.md +9 -6
- data/.claude/rules/type-checking.md +13 -14
- data/.claude/rules/visitors.md +13 -14
- data/.claude/skills/rql-code-review/SKILL.md +134 -43
- data/CHANGELOG.md +105 -125
- data/CLAUDE.md +11 -9
- data/README.md +501 -136
- data/Rakefile +11 -0
- data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rb +1 -0
- data/lib/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rb +68 -10
- data/lib/redis_queued_locks/acquirer/acquire_lock/log_visitor.rb +82 -15
- data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rb +92 -14
- data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +441 -63
- data/lib/redis_queued_locks/acquirer/acquire_lock/with_acq_timeout.rb +2 -2
- data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire/log_visitor.rb +1 -0
- data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +5 -5
- data/lib/redis_queued_locks/acquirer/acquire_lock.rb +176 -39
- data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +322 -49
- data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +125 -13
- data/lib/redis_queued_locks/acquirer/is_locked.rb +0 -1
- data/lib/redis_queued_locks/acquirer/is_queued.rb +0 -1
- data/lib/redis_queued_locks/acquirer/keys.rb +16 -3
- data/lib/redis_queued_locks/acquirer/lock_info.rb +31 -39
- data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +11 -5
- data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +20 -10
- data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +105 -36
- data/lib/redis_queued_locks/acquirer/locks.rb +32 -11
- data/lib/redis_queued_locks/acquirer/queue_info.rb +30 -8
- data/lib/redis_queued_locks/acquirer/queues.rb +26 -6
- data/lib/redis_queued_locks/acquirer/reindex_locks.rb +117 -4
- data/lib/redis_queued_locks/acquirer/release_all_locks.rb +37 -6
- data/lib/redis_queued_locks/acquirer/release_lock.rb +33 -7
- data/lib/redis_queued_locks/acquirer/release_locks_of.rb +45 -6
- data/lib/redis_queued_locks/acquirer/release_read_lock.rb +22 -3
- data/lib/redis_queued_locks/client.rb +140 -56
- data/lib/redis_queued_locks/config.rb +105 -14
- data/lib/redis_queued_locks/errors.rb +34 -4
- data/lib/redis_queued_locks/instrument/sampler.rb +4 -7
- data/lib/redis_queued_locks/logging/sampler.rb +4 -8
- data/lib/redis_queued_locks/resource.rb +137 -12
- data/lib/redis_queued_locks/swarm/flush_zombies.rb +1 -0
- data/lib/redis_queued_locks/swarm/redis_client_builder.rb +8 -4
- data/lib/redis_queued_locks/swarm/swarm_element/threaded.rb +8 -6
- data/lib/redis_queued_locks/swarm/zombie_info.rb +6 -2
- data/lib/redis_queued_locks/swarm.rb +86 -5
- data/lib/redis_queued_locks/utilities.rb +1 -1
- data/lib/redis_queued_locks/version.rb +2 -2
- data/rbs_collection.lock.yaml +4 -0
- data/rbs_collection.yaml +2 -1
- 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 +25 -5
- data/sig/redis_queued_locks/acquirer/acquire_lock/log_visitor.rbs +32 -12
- 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 +59 -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 +4 -4
- data/sig/redis_queued_locks/acquirer/acquire_lock.rbs +6 -4
- data/sig/redis_queued_locks/acquirer/clear_dead_requests.rbs +23 -1
- data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +16 -3
- data/sig/redis_queued_locks/acquirer/queues.rbs +1 -1
- data/sig/redis_queued_locks/acquirer/reindex_locks.rbs +22 -1
- data/sig/redis_queued_locks/acquirer/release_lock.rbs +3 -2
- data/sig/redis_queued_locks/acquirer/release_read_lock.rbs +1 -1
- data/sig/redis_queued_locks/client.rbs +24 -10
- data/sig/redis_queued_locks/config.rbs +54 -1
- data/sig/redis_queued_locks/errors.rbs +5 -2
- data/sig/redis_queued_locks/instrument/sampler.rbs +3 -3
- data/sig/redis_queued_locks/instrument/void_notifier.rbs +3 -1
- data/sig/redis_queued_locks/logging/sampler.rbs +3 -3
- data/sig/redis_queued_locks/resource.rbs +23 -13
- 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
- data/sig/vendor/redis_client.rbs +1 -0
- metadata +4 -3
- /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,24 @@ locks", key layout, "Lock index", lock lookup modes, events, errors, conventions
|
|
|
80
82
|
- `EXTEND_LOCK_PTTL` in `TryToLock` and `ExtendLockTTL`;
|
|
81
83
|
- lock state reply reading in `try_to_lock` Step 0.1 and `precheck_lock` (shared `pipeline_lock_state`,
|
|
82
84
|
position constants, `lock_obstacle`, `log_lock_obstacle`);
|
|
83
|
-
- reserved `meta` keys validation in `AcquireLock` and `LockSeriesPoC`;
|
|
85
|
+
- reserved `meta` keys validation in `AcquireLock` and `LockSeriesPoC`; the `priority` resolution and range check
|
|
86
|
+
in `AcquireLock` Step 0.1 and `LockSeriesPoC` (the same message);
|
|
87
|
+
- the band liveness rule (a request is dead when its score is `<= Resource.lock_priority_band_dead_score(dead_score,
|
|
88
|
+
band)`): `TryToLock#dead_request_priority` / `pipeline_dead_requests_removal` and `ClearDeadRequests.drop_dead_requests` /
|
|
89
|
+
`drop_dead_requests_by_priority`;
|
|
90
|
+
- the `'priority'` field of a request (`Resource.lock_request_priority`) in `QueueInfo.queue_info` and
|
|
91
|
+
`Queues.extract_queues_info`;
|
|
84
92
|
- lock-family options in `Client#lock`, `#lock!`, `#lock_series`, `#lock_series!` and their forwarding in
|
|
85
93
|
`LockSeriesPoC.lock_series_poc`.
|
|
86
|
-
- Mirror path
|
|
87
|
-
`acquire_lock/yield_expire/log_visitor.rb` → `sig/.../acquire_lock/yield_with_expire/log_visitor.rbs`;
|
|
88
|
-
`lock_series_poc*` has no RBS.
|
|
94
|
+
- Mirror path exception: `lock_series_poc*` has no RBS.
|
|
89
95
|
- Next version for `@since`/`@version`: the version the unreleased work already uses (`git diff <base> -- lib |
|
|
90
96
|
grep -oE '^\+.*@(since|version) [0-9.]+'`, the CHANGELOG `[Unreleased]` code), otherwise the next minor of
|
|
91
|
-
`RedisQueuedLocks::VERSION` (`version.rb`)
|
|
92
|
-
A value older than `VERSION` on new code,
|
|
97
|
+
`RedisQueuedLocks::VERSION` (`version.rb`); don't take the maximum over `lib/` (old tags can be wrong).
|
|
98
|
+
A value older than `VERSION` on new code, a method behavior/signature change (a typo fix in the method or keyword
|
|
99
|
+
argument name included; a renamed positional parameter is not a signature change) without the method `@version` bump, or a
|
|
100
|
+
class/module header `@version` that does not match its latest interface change (`logic.md#2`: signatures,
|
|
101
|
+
added/removed methods, `extend`/`include`/`prepend`, the superclass; behavior changes never bump the header) is a
|
|
102
|
+
Minor finding.
|
|
93
103
|
|
|
94
104
|
## 3. Spec files are exempt (until the suite rework)
|
|
95
105
|
|
|
@@ -140,15 +150,31 @@ Still in scope (the exemption covers how specs are written, not whether the libr
|
|
|
140
150
|
the reentrant write extension check `result.size == (lock_index_enabled ? 5 : 4)` follows its `MULTI`.
|
|
141
151
|
- Round trips of the hot path must not regress silently (overview "Lock acquisition flow"): `lock {}` = 4, a
|
|
142
152
|
failed attempt = 3, a failed retry = 1 (pre-check), `fail_fast` on an obtained lock = 1, `unlock` without
|
|
143
|
-
readers = 1
|
|
153
|
+
readers = 1; the only extra round trips are the dead blocking request removals of Step 2.2 (one per removal, none
|
|
154
|
+
while nothing dead blocks). Independent reads share one `pipelined` round trip (also inside `multi(watch:)`).
|
|
144
155
|
- New destructive paths (delete/shorten a lock) check the owner, unless the change documents the trade-off as
|
|
145
156
|
accepted by the user (like the known limitations in §5).
|
|
146
157
|
- Shared mutable state behind `Utilities::Lock#synchronize` (`logic.md#9`).
|
|
147
158
|
|
|
148
|
-
### B. Read/write awareness (`logic.md#13`, `acquirer.md#12
|
|
159
|
+
### B. Read/write awareness (`logic.md#13`, `acquirer.md#12`, overview "Read/Write locks in the architecture")
|
|
149
160
|
- 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
|
|
161
|
+
`rql:lock_readers:*`, `rql:lock_read_queue:*` and `rql:lock_reader:<name>:<acq_id>`; every attempt prunes the dead
|
|
162
|
+
requests of its priority band in both queues (and the bands of the dead requests that block it, §4 C);
|
|
163
|
+
dequeue/cleanup removes the request from the queue of its mode.
|
|
164
|
+
- Order between modes (`:queued`, `find_opposite_request_ahead`/`lock_obstacle`, shared by the attempt and the
|
|
165
|
+
pre-check): a write request must be the head of the write queue and have no earlier read request, a read request
|
|
166
|
+
must have no earlier write request ("earlier" = the `(score, acq_id)` comparison on the shifted scores); the
|
|
167
|
+
order never decides mutual exclusion.
|
|
168
|
+
- The held mode decides the release: `acquire_lock` keeps the try result `rw_mode` as `held_rw_mode` for
|
|
169
|
+
`yield_expire` (a read request that works through the own write lock holds the write lock); a reader is released
|
|
170
|
+
owner-safe (`ZREM` of the own member + `DEL` of the own reader data), `unlock` releases everything; the internal
|
|
171
|
+
`rw_mode` never appears in the public `lock` result.
|
|
172
|
+
- Read lock extension (`extend_lock_ttl(..., read_write_mode: :read)`): `multi(watch: [lock_key])`, only a live read
|
|
173
|
+
lock and only while no write lock exists, never revives an expired reader; `all_read_locks: true` extends every
|
|
174
|
+
live reader (`ZADD XX INCR`), the registry/reader data TTLs keep the max (`PEXPIRE NX` + `GT`).
|
|
175
|
+
- `fail_fast`: write fails on a write holder or live readers, read fails on a write holder (or the own live read);
|
|
176
|
+
lock series: one mode for the whole series, `release_lock_series` releases only the locks the series obtained and
|
|
177
|
+
only the current acquirer's (write: compare-and-delete in one transaction, read: `ZREM` + `DEL`).
|
|
152
178
|
- Write-only result shapes stay unchanged; read keys/fields appear only when read data exists.
|
|
153
179
|
- `read_lock_key` is `nil` for write requests; reader data is a data carrier only (the registry decides existence),
|
|
154
180
|
without `acq_id`/`hst_id`; don't store derivable values; a new lock field updates every formatting copy (§2,
|
|
@@ -157,7 +183,41 @@ Still in scope (the exemption covers how specs are written, not whether the libr
|
|
|
157
183
|
entries) is still read correctly; a key/data format change states the rolling-deploy requirement in README and
|
|
158
184
|
CHANGELOG.
|
|
159
185
|
|
|
160
|
-
### C. Lock
|
|
186
|
+
### C. Lock request priorities (`acquirer.md` "Lock request priorities", overview "Lock request prioritization in the architecture")
|
|
187
|
+
- Queue scores only through `Resource` (the single source of the layout): `calc_initial_acquirer_position(priority)`
|
|
188
|
+
(enqueue time - `priority * LOCK_PRIORITY_BAND`), the requeue check `dead_score_reached?(position, priority, ttl)`
|
|
189
|
+
(a requeued request goes to the end of its own band), decoding with `lock_request_priority(score)`; no raw
|
|
190
|
+
`Time.now.to_f` positions, no own band arithmetic outside `Resource`.
|
|
191
|
+
- Dead request pruning per band only: `ZREMRANGEBYSCORE <queue> <lock_priority_band_floor(p)>
|
|
192
|
+
<lock_priority_band_dead_score(dead_score, p)>`; a new `-inf .. now - ttl` range on a request queue is a Blocker
|
|
193
|
+
(it drops alive higher-priority requests and keeps dead lower-priority ones); full clears (`-inf .. +inf`) and
|
|
194
|
+
member removals (`ZREM`) are band-independent; `-inf .. -inf` drops the requests at `-inf` only (manual writes:
|
|
195
|
+
always dead, out of all bands; `+inf` is never dead), and decoding an infinite score must never raise
|
|
196
|
+
(`lock_request_priority` maps `-inf`/`+inf` to the range bounds).
|
|
197
|
+
- Head-driven cleanup, cost independent of the priority range: the queueing prunes the own band in both queues (in
|
|
198
|
+
its pipeline, before `ZADD NX`: a requeued request must not lose its new position to the removal of its old one),
|
|
199
|
+
Step 2.2 (`drop_dead_requests_ahead`, attempt and pre-check, `:queued` only) drops the bands of the
|
|
200
|
+
dead blocking requests (the write queue head for writers, the earlier opposite-mode request) with the queue heads
|
|
201
|
+
re-read in the same round trip, bounded by `MAX_DEAD_REQUEST_REMOVALS`; `ClearDeadRequests` prunes only the bands
|
|
202
|
+
whose first request is dead. Never enumerate the priority range per attempt or per queue; any band is decoded
|
|
203
|
+
(also out of the range: newer versions).
|
|
204
|
+
- Ordering uses the shifted scores as is (queue heads, the cross-mode `(score, acq_id)` comparison): strict priority
|
|
205
|
+
between bands, FIFO inside a band, the same in both modes; the priority never takes part in mutual exclusion.
|
|
206
|
+
- The option: `priority:` in all four lock `Client` methods (default `config['default_lock_priority']`), an Integer
|
|
207
|
+
`MIN_LOCK_PRIORITY..MAX_LOCK_PRIORITY` or a name of `config['lock_priorities']` (the client-wide registry is
|
|
208
|
+
forwarded as the internal keyword `lock_priorities:`, never a public keyword); the name is resolved and the range
|
|
209
|
+
checked in `AcquireLock` Step 0.1 and before a lock series (`LockSeriesPoC`): code below sees Integers only (RBS
|
|
210
|
+
`RQL::lockPriority` at the boundary, `Integer` below); config validators for both keys, and a name of
|
|
211
|
+
`default_lock_priority` checked against `lock_priorities` by `Config` (after a `configure` block, before a direct `[]=`).
|
|
212
|
+
- Observability: every lock acquirement log line and event (classic, reentrant: the requested priority, lock series:
|
|
213
|
+
the series priority, `lock_obtaining_failed`) carries the Integer `priority` right after `rw_mode`; try-lock logs
|
|
214
|
+
that name another request carry its decoded priority (`first_priority_in_queue`, `ahead_priority`, `dead_priority`)
|
|
215
|
+
computed at the call site only when `log_sampled && log_lock_try` (one helper: `TryToLock#logged_request_priority`); info results carry `'priority'` (every copy).
|
|
216
|
+
- Compatibility: priority `0` keeps the raw timestamp scores of older versions/data; a change of the score layout or
|
|
217
|
+
of the pruning states the rolling-deploy effect in README and CHANGELOG (older versions prune positive-priority
|
|
218
|
+
requests as dead ones).
|
|
219
|
+
|
|
220
|
+
### D. Lock index, both lookup modes (`acquirer.md#16`, overview "Lock index")
|
|
161
221
|
- Disabled mode (default): no lock index command at all (`LOCK_INDEX_KEY` only under `if lock_index_enabled` /
|
|
162
222
|
after `return unless lock_index_enabled`), no `ZSCAN`, queues without TTL. Enabled mode: no keyspace `SCAN`
|
|
163
223
|
except `ReindexLocks`.
|
|
@@ -174,9 +234,9 @@ Still in scope (the exemption covers how specs are written, not whether the libr
|
|
|
174
234
|
`lock`, `lock!`, `lock_series`, `lock_series!`, `reindex_locks`. Pruning (`ZREMRANGEBYSCORE <index> -inf
|
|
175
235
|
<now - 1>`) stays in the queueing pipeline, `flush_zombies`, `clear_locks`, `clear_dead_requests`.
|
|
176
236
|
|
|
177
|
-
###
|
|
237
|
+
### E. Iteration and performance (`acquirer.md#7`, `arguments.md#7`)
|
|
178
238
|
- 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`,
|
|
239
|
+
`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`,
|
|
180
240
|
never one round trip per key; per-key commands in pipelined batches collected across pages, plus the
|
|
181
241
|
remainder batch.
|
|
182
242
|
- A new lock-iterating operation implements both lookups side by side, chooses the lookup in one place
|
|
@@ -188,7 +248,7 @@ Still in scope (the exemption covers how specs are written, not whether the libr
|
|
|
188
248
|
`NO_LOCK_DATA`); keys computed once in `acquire_lock`; log-only Redis reads gated at the call site
|
|
189
249
|
(`acquirer.md#13`); no `Set` splatted in a loop.
|
|
190
250
|
|
|
191
|
-
###
|
|
251
|
+
### F. Architecture and API design
|
|
192
252
|
- `Client` only fills defaults from `config['...']` and delegates (`logic.md#3`); an `x!` variant exists only when it
|
|
193
253
|
must raise and repeats the full keyword list.
|
|
194
254
|
- No parameter objects, `Struct`/`Data.define`/`OpenStruct`, builders, `**opts`, `options = {}`, `...` forwarding
|
|
@@ -214,14 +274,18 @@ Still in scope (the exemption covers how specs are written, not whether the libr
|
|
|
214
274
|
key / acquirer id in the message.
|
|
215
275
|
- Keys only via `Resource.prepare_*`, `*_PATTERN`, `SWARM_KEY`, `LOCK_INDEX_KEY` (`logic.md#5`): flag every new
|
|
216
276
|
`'rql:...'` literal outside `resource.rb`.
|
|
217
|
-
- Config: `setting` + `validate` in `config.rb` (booleans `val == true || val == false`, Integers with ranges
|
|
218
|
-
|
|
219
|
-
|
|
277
|
+
- Config: `setting` + `validate` in `config.rb` (booleans `val == true || val == false`, Integers with ranges; per-call
|
|
278
|
+
numeric options validated with the same ranges in the operation module, `logic.md#7`), dotted
|
|
279
|
+
keys for groups, unit in a trailing `# NOTE:` comment, read as `config['key']` (`logic.md#7`), one typed
|
|
280
|
+
`Config#[]` overload line per key in `config.rbs` (`type-checking.md#12`). Errors:
|
|
281
|
+
`class XError < Error; end` in `errors.rb`, each under the global base of its branch (`Error`, `ArgumentError`,
|
|
282
|
+
`Exception`: super exceptional cases only, with the reason in the comment; `logic.md#15`). Runtime dependencies:
|
|
283
|
+
`redis-client` only; dev gems in `Gemfile`.
|
|
220
284
|
- `AcquireLock`: new behavior as a new step or mixin with `# Step N.x` numbering and consistent `acq_process` keys
|
|
221
285
|
(`acquirer.md#10`); `lock_series_poc.rb` is experimental: don't accept new features built on it without the
|
|
222
286
|
user's request.
|
|
223
287
|
|
|
224
|
-
###
|
|
288
|
+
### G. Swarm, Ractors, threads (`swarm.md`)
|
|
225
289
|
- Redis work in stateless class functions shared by the main loop and the manual API (`#1`); main loops build
|
|
226
290
|
their own client with `RedisClientBuilder` inside the loop thread (`#4`).
|
|
227
291
|
- Into `Ractor.new` only copied plain values (`config.slice(...)`, scalars, `lock_index_enabled`): no `rql_client`,
|
|
@@ -235,9 +299,14 @@ Still in scope (the exemption covers how specs are written, not whether the libr
|
|
|
235
299
|
- Lifecycle names and semantics identical in both bases (`#7`); a new element is registered everywhere (`#3`);
|
|
236
300
|
status shapes `{ running:, state: }` with `'non_initialized'` and their RBS (`#10`).
|
|
237
301
|
|
|
238
|
-
###
|
|
239
|
-
- Logs/events of `AcquireLock` and its mixins and of `LockSeriesPoC` go through visitor methods (`#1`);
|
|
240
|
-
modules
|
|
302
|
+
### H. Logging and instrumentation (`visitors.md`)
|
|
303
|
+
- Logs/events of `AcquireLock` and its mixins and of `LockSeriesPoC` go through visitor methods (`#1`); mutation
|
|
304
|
+
modules write one log line and one event inline inside `run_non_critical`, gated by `log_sampled`/`instr_sampled`
|
|
305
|
+
(`acquirer.md#9`).
|
|
306
|
+
- Observability coverage (`logic.md#16`): a new or changed logic that creates/changes/deletes keys without logs and
|
|
307
|
+
instrumentation, a mutation event/log that times an internal part instead of the whole root operation, a mutation
|
|
308
|
+
payload without `instrument`, a `Client` mutation method without the observability keywords, or instrumented lock
|
|
309
|
+
info queries are findings; the swarm elements are known debt.
|
|
241
310
|
- Visitor shape: `class << self`, method name = event name (`__` between phase and detail), same name in the
|
|
242
311
|
Log and Instr visitors, first line `return unless log_sampled` (`&& log_lock_try` for `TryToLock` steps), one
|
|
243
312
|
`logger.debug do ... end rescue nil` with `"[redis_queued_locks.<event>] "` and `key => 'value'` pairs, or
|
|
@@ -250,7 +319,7 @@ Still in scope (the exemption covers how specs are written, not whether the libr
|
|
|
250
319
|
the default write success path (`#13`); new or changed log keys/payload keys go to README `## Logging` /
|
|
251
320
|
`### Instrumentation Events` and CHANGELOG (`#14`).
|
|
252
321
|
|
|
253
|
-
###
|
|
322
|
+
### I. Types (`type-checking.md`)
|
|
254
323
|
- Every changed method, param, return shape, constant or ivar is mirrored in `sig/` (`#1`): nested module blocks,
|
|
255
324
|
`use RedisQueuedLocks as RQL` / `use RedisClient as RC`, shared types (`RC::client`, `RQL::loggerObj`,
|
|
256
325
|
`RQL::instrObj`, `samplerObj`), `type xxxResult = { ok: bool, result: ... }` for public results, literal types for
|
|
@@ -260,12 +329,13 @@ Still in scope (the exemption covers how specs are written, not whether the libr
|
|
|
260
329
|
annotation carries `NOTE: (steep)`; never shape runtime code around Steep (`#5`). `Steepfile` is never loosened (`#6`).
|
|
261
330
|
- RBS must match runtime values (runtime type checks run the suite): e.g. `Integer|Float` where a Float can arrive.
|
|
262
331
|
|
|
263
|
-
###
|
|
332
|
+
### J. Ruby style of the codebase
|
|
264
333
|
- `# frozen_string_literal: true`; compact constant paths (`module RedisQueuedLocks::Acquirer::Keys`); namespace
|
|
265
334
|
files only `require_relative` (`logic.md#1`).
|
|
266
335
|
- 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
|
|
268
|
-
method
|
|
336
|
+
results), blank `#`, `@api public|private`, `@since <next>`; behavior/signature changes bump `@version <next>` on
|
|
337
|
+
the method; the module/class header `@version` changes only with the interface (signatures, added/removed methods,
|
|
338
|
+
`extend`/`include`/`prepend`, the superclass), never for behavior changes inside the methods (`logic.md#2`).
|
|
269
339
|
- Comments: `# NOTE:` with `# - item;` lists, `# Step N.x` in algorithms, `(RW)`/`(lock index)`/`(keyspace)`
|
|
270
340
|
tags, `NOTE: (pipeline layout)`/`(transaction layout)` for positional replies.
|
|
271
341
|
- Redis: uppercase String commands, String numeric args (`'0'`, `'-1'`, `'-inf'`, `'+inf'`), `redis.with do |rconn|`
|
|
@@ -281,12 +351,16 @@ Still in scope (the exemption covers how specs are written, not whether the libr
|
|
|
281
351
|
- Rubocop: only specific `Metrics`/`Layout` cops disabled locally with a paired `enable`, never
|
|
282
352
|
`# rubocop:disable all` in new code (`logic.md#10`); long signatures are not "fixed" (`arguments.md#8`).
|
|
283
353
|
- Suggestions only (`logic.md` recommendations, not violations): correctly spelled new identifiers (don't copy the
|
|
284
|
-
known typos)
|
|
354
|
+
known typos).
|
|
285
355
|
|
|
286
|
-
###
|
|
356
|
+
### K. Docs and process
|
|
287
357
|
- The same change updates `.claude/project-overview.md` and the affected `.claude/rules/*.md` for every new/changed
|
|
288
358
|
key, option, result shape, event, algorithm step or limitation (`logic.md#12`), README and CHANGELOG
|
|
289
359
|
`[Unreleased]` for user-visible behavior, and the YARD of the touched `Client` methods.
|
|
360
|
+
- CHANGELOG entries are compact (`changelog.md`): one 1-2 line bullet per subject-level change, no implementation
|
|
361
|
+
details (algorithm steps, Redis commands, round trips, internals, full event lists), no entries for internal-only
|
|
362
|
+
changes; one entry per feature, released sections untouched; a fundamentally new feature or globally new support
|
|
363
|
+
(Read/Write locks, Lock Index, Garnet support) is a separate `*Brand New Feature*` entry first in `### Added`.
|
|
290
364
|
- Commits under review (`git.md`): subject only `[<scope>] <summary>`, lowercase, imperative, no trailing period,
|
|
291
365
|
no body except the attribution trailer, never on `master`.
|
|
292
366
|
|
|
@@ -297,13 +371,24 @@ Still in scope (the exemption covers how specs are written, not whether the libr
|
|
|
297
371
|
read→write upgrade as an error unless `:wait_for_lock`; nested reads through `conflict_strategy`; no fairness
|
|
298
372
|
between modes with `:random`; best-effort cross-host FIFO; no Lua in the RW path; one mode per lock series;
|
|
299
373
|
`lock_index_enabled` as a config-only mode; the lock index defaulting to `false`.
|
|
374
|
+
- Settled design of the lock request priorities (overview "Lock request priorities", limitations kept by the user):
|
|
375
|
+
strict priority (lower priorities can starve within their timeout/retry limits), no preemption of an obtained lock,
|
|
376
|
+
the polled handoff (`retry_delay`), dead requests pruned with the pruner's `queue_ttl`, dead requests of lower bands
|
|
377
|
+
lingering until an attempt of their band/`clear_dead_requests`/the queue TTL/the swarm (they never block), the
|
|
378
|
+
library-constant range `-100..100` and its ~61 µs FIFO precision inside a band, `MAX_DEAD_REQUEST_REMOVALS` per
|
|
379
|
+
attempt, `:random` requests ignoring the order and the priorities of other requests (while their own requests are queued
|
|
380
|
+
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
|
|
381
|
+
request of each band only, the most bands of a batch queue + 1 round trips),
|
|
382
|
+
the order degradation with older versions (no safety impact), the queue score as the only order and liveness of a request
|
|
383
|
+
(a separate liveness registry of request deadlines was rejected by the user: don't propose it).
|
|
384
|
+
- Unused `Resource` methods marked `# NOTE: (debug)` (manual runtime checks, `logic.md#17`); report a debug method
|
|
385
|
+
only when it is wrong for a key family it can get.
|
|
300
386
|
- Known limitations kept by the user: `YieldExpire` write release (`EXPIRE 0`) and ttl decrease without an owner
|
|
301
387
|
check; `HMGET`→`DEL` without `WATCH` in `ReleaseLocksOf`/`FlushZombies`; `unlock` dropping the data of a reader
|
|
302
388
|
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
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
one flusher per process; `EVAL` without `SCRIPT LOAD`; `# rubocop:disable all` around lock series.
|
|
389
|
+
- Known debt: `WithAcqTimeout` → `LockInfo`/`QueueInfo`; rspec-retry, disabled coverage minimum;
|
|
390
|
+
`sleep(0.1)` readiness waits and missing logging in the swarm;
|
|
391
|
+
one flusher per process; `EVAL` without `SCRIPT LOAD`.
|
|
307
392
|
- "Recommendations (proposed, not yet project policy)" in the rule files: never violations; mention one only as an
|
|
308
393
|
optional suggestion for touched code.
|
|
309
394
|
- Pre-existing problems outside the diff: out of scope; mention a severe one briefly in the template's
|
|
@@ -314,7 +399,9 @@ Still in scope (the exemption covers how specs are written, not whether the libr
|
|
|
314
399
|
|
|
315
400
|
- Confirm it in the code (call sites, copies, RBS, specs), not by pattern match: concurrency claims name the
|
|
316
401
|
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
|
|
402
|
+
commands and positions; mode claims check both `lock_index_enabled` values and both `read_write_mode` values;
|
|
403
|
+
priority claims check a positive, the default (`0`) and a negative band, a request of another band ahead (alive and
|
|
404
|
+
dead) and requests in both queues.
|
|
318
405
|
- Drop findings contradicted by §5, by a design `NOTE` in the code or by the overview. Mark the rest
|
|
319
406
|
`CONFIRMED` (traced end to end) or `PLAUSIBLE` (depends on an assumption: name it).
|
|
320
407
|
- Helpful greps:
|
|
@@ -328,8 +415,10 @@ Still in scope (the exemption covers how specs are written, not whether the libr
|
|
|
328
415
|
- Checks (read-only): a review on request of local changes runs `bundle exec rake rubocop` and
|
|
329
416
|
`bundle exec rbs collection install && bundle exec rake steep:check` (grep the output for `ERROR`/`FATAL`, restore a
|
|
330
417
|
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
|
|
332
|
-
asking:
|
|
418
|
+
reports the completion checks instead of re-running them (see "When to run"). Don't run the specs without
|
|
419
|
+
asking: they need a local Redis; when asked, run them in parallel processes (`rake rspec:parallel`: ~1 minute,
|
|
420
|
+
flushes db `0..2n-1` for `n` workers; the serial `rake rspec`, ~6 minutes and db 0/1, only for the one-process checks
|
|
421
|
+
of `tests.md#1`).
|
|
333
422
|
|
|
334
423
|
## 7. Report
|
|
335
424
|
|
|
@@ -361,11 +450,13 @@ Both templates have the same structure: change them together. A new language = a
|
|
|
361
450
|
same structure plus a row in the table above.
|
|
362
451
|
|
|
363
452
|
### Severity and order
|
|
364
|
-
- Blocker: breaks mutual exclusion or the lock index superset, loses or leaks locks/data
|
|
365
|
-
API (method, result shape, event,
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
453
|
+
- Blocker: breaks mutual exclusion or the lock index superset, loses or leaks locks/data (e.g. a queue pruning that
|
|
454
|
+
drops alive requests of another priority band), crashes, breaks the public API (method, result shape, event,
|
|
455
|
+
config key) or turns CI red.
|
|
456
|
+
- Major: a rule violation with a real consequence: a missed read key family or lookup mode, a broken order between
|
|
457
|
+
modes or priorities, a release of the wrong (requested instead of held) mode, a dead request that can block a
|
|
458
|
+
queue, a cost that grows with the priority range, per-key round trips, a hot-path regression, broken module
|
|
459
|
+
independence, a missing RBS/docs update for a changed contract, new behavior without specs.
|
|
369
460
|
- Minor: convention deviations with little impact (YARD tags, comment format, naming, idioms).
|
|
370
461
|
- Nit: optional polish.
|
|
371
462
|
- Order by severity, then by file. Empty sections are omitted; with no findings the verdict says so and the sync and
|
data/CHANGELOG.md
CHANGED
|
@@ -1,140 +1,120 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.22.0] - 2026-10-27
|
|
4
|
+
### Added
|
|
5
|
+
- *Brand New Feature*: **Lock Request Prioritization**: the `priority:` option of `#lock`/`#lock!`/`#lock_series`/`#lock_series!`
|
|
6
|
+
(`-100..100`, `0` by default) orders the waiting `:queued` lock requests of both modes: strict priority, FIFO inside
|
|
7
|
+
a priority; the obtained lock is never taken away (see README "Lock Request Prioritization"):
|
|
8
|
+
- `config['default_lock_priority']`: the default priority of the client;
|
|
9
|
+
- priority names: `config['lock_priorities']` (`:lowest`, `:low`, `:normal`, `:high`, `:highest` by default, custom names
|
|
10
|
+
allowed), accepted by `priority:` and `config['default_lock_priority']`;
|
|
11
|
+
- `#clear_dead_requests`: drops the dead lock requests of all priorities; the new `by_priority:` option reads only the first
|
|
12
|
+
lock request of each priority (long lock queues with few priorities);
|
|
13
|
+
- the new `log_lock_try` log `[redis_queued_locks.try_lock.dead_request_removed]`;
|
|
14
|
+
- upgrade all processes that share locks before using positive priorities: older versions drop such lock requests as dead
|
|
15
|
+
ones (the order breaks, the mutual exclusion is not affected);
|
|
16
|
+
### Changed
|
|
17
|
+
- `#queue_info`/`#queues_info`: each lock request has the `"priority"` field;
|
|
18
|
+
- the lock acquirement logs and instrumentation events carry the Integer `priority`, the try-lock logs that name another lock
|
|
19
|
+
request carry its priority (see README `## Logging` / `### Instrumentation Events`);
|
|
20
|
+
- RBS: `RedisQueuedLocks::Config#[]` is typed per config key (unknown keys are type errors, dynamic `String` keys no longer
|
|
21
|
+
type-check);
|
|
22
|
+
- RBS: `read_write_mode`, `access_strategy` and `conflict_strategy` of `Client` are typed as literal unions;
|
|
23
|
+
### Fixed
|
|
24
|
+
- a lock request requeued after waiting longer than its `queue_ttl` could lose its next attempt (a spent retry);
|
|
25
|
+
- the enabled lock index: locks without TTL adopted by `#reindex_locks` were skipped by all lock-iterating operations;
|
|
26
|
+
|
|
27
|
+
## [1.21.0] - 2026-10-13
|
|
28
|
+
### Added
|
|
29
|
+
- Logging and instrumentation of all operations that change Redis keys (the swarm is not covered yet; see README `## Logging` /
|
|
30
|
+
`### Instrumentation Events`):
|
|
31
|
+
- the `redis_queued_locks.lock_obtaining_failed` event and log (with the failure `result`);
|
|
32
|
+
- the `redis_queued_locks.extend_lock_ttl`, `redis_queued_locks.clear_dead_requests` and `redis_queued_locks.reindex_locks`
|
|
33
|
+
events and logs;
|
|
34
|
+
- logs of the existing `explicit_lock_release`, `explicit_read_lock_release`, `explicit_all_locks_release` and
|
|
35
|
+
`release_locks_of` events;
|
|
36
|
+
- `#reindex_locks`: `logger:`, `instrumenter:`, `instrument:` and the log/instrumentation sampling options;
|
|
37
|
+
### Changed
|
|
38
|
+
- stricter validation: invalid values raise `RedisQueuedLocks::ArgumentError` (`RedisQueuedLocks::SwarmArgumentError` for
|
|
39
|
+
the swarm methods, `RedisQueuedLocks::ConfigValidationError` for the config) instead of a silent misbehavior:
|
|
40
|
+
- `ttl` of the lock methods and `milliseconds` of `#extend_lock_ttl`: positive `Integer`s;
|
|
41
|
+
- the per-call numeric options: `Integer`s in the ranges of their config keys (`Float`s like `timeout: 0.5` are rejected);
|
|
42
|
+
- the config: positive `Integer` TTLs, periods and batch/scan sizes;
|
|
43
|
+
- `:meta`: reserved lock data keys are rejected as Symbol keys too;
|
|
44
|
+
- the internal timeout errors of `timed: true` and `timeout` are not `StandardError`s anymore: `rescue => e` /
|
|
45
|
+
`rescue Timeout::Error` inside the block does not catch them (the public errors and results are the same);
|
|
46
|
+
### Fixed
|
|
47
|
+
- write locks with a non-positive `ttl` were reported as obtained without the mutual exclusion;
|
|
48
|
+
- non-positive config and per-call values broke the operations (endless `:dead_score_reached`, failing scans, stopped swarm
|
|
49
|
+
loops, all hosts as zombies, `#extend_lock_ttl` shortening the lock, `#lock_series` locks with a near-zero ttl);
|
|
50
|
+
- Symbol keys of `:meta` overwrote the reserved lock data (broken reentrant locks, owner checks and lock info);
|
|
51
|
+
- a `rescue` inside a `timed` block or in a logger/instrumenter swallowed the lock timeouts: the block could run past
|
|
52
|
+
the lock TTL, the acquirement timeout could be lost;
|
|
53
|
+
- a connection interrupted inside `WATCH` (lock timeouts, `Timeout.timeout` of your code) could abort the next transaction
|
|
54
|
+
on it (e.g. a lock release); it is closed now;
|
|
55
|
+
- the `instrument:` option of `#unlock`, `#clear_locks`, `#clear_locks_of`/`#clear_current_locks` was not passed to the payload;
|
|
56
|
+
- `#lock_series` (PoC): logger/instrumenter errors broke the series call (the locks were kept till their ttl);
|
|
57
|
+
- `#locks_info`: the write lock info had no `"lock_key"` field (unlike `#lock_info`);
|
|
58
|
+
- the default log/instrumentation samplers were off by one percent (`0%` sampled ~1% of the cases);
|
|
59
|
+
- swarm: frozen `swarm.*.redis_config` hashes failed the swarm main loops with `FrozenError`;
|
|
60
|
+
- RBS: the missing `Client#current_acq_id` alias and the wrong `Client#queues_info` return type;
|
|
61
|
+
- README/YARD descriptions that did not match the code (config key names, `#lock` statuses, `#lock_info` fields, results of
|
|
62
|
+
`#unlock`, `#clear_dead_requests` and `#extend_lock_ttl`);
|
|
63
|
+
|
|
3
64
|
## [1.20.0] - 2026-10-12
|
|
4
65
|
### Added
|
|
5
|
-
- **Lock
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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;
|
|
66
|
+
- *Brand New Feature*: **Lock Index** (opt-in: `config['lock_index_enabled']`, `false` by default): lock-iterating operations
|
|
67
|
+
(`#locks`, `#queues`, `#keys`, `#clear_*`, the swarm) find locks through the `rql:index:locks` index instead of the keyspace
|
|
68
|
+
`SCAN` (see README "Lock Index"):
|
|
69
|
+
- a client-wide mode: use the same mode on all processes that share the Redis database;
|
|
70
|
+
- `config['lock_index_grace_period']` and the `lock_index_grace_period:` lock option (`60_000` ms by default): the clock
|
|
71
|
+
skew tolerance of the index;
|
|
72
|
+
- lock request queues have TTL in the enabled mode (queues of dead acquirers expire by themselves);
|
|
73
|
+
- `#reindex_locks`: adds the existing lock keys to the lock index (keys of older versions and of the disabled lock index
|
|
74
|
+
are not found until then); **invoke it once after the lock index is enabled on all your processes**;
|
|
75
|
+
### Changed
|
|
76
|
+
- fewer Redis round trips of the lock acquirement: `#lock` with a block takes 4 instead of 7, a failed attempt - 3 instead
|
|
77
|
+
of 6, a failed retry or a `fail_fast` attempt on an obtained lock - 1 instead of 3;
|
|
78
|
+
- `#unlock` (without read locks) and the `#lock_series` release take fewer round trips;
|
|
79
|
+
- SCAN-based operations make one keyspace pass with pipelined batches instead of a pass per key family and a round trip per key;
|
|
80
|
+
- `clear_locks_of__queue_scan_size` and `zombie_queue_scan_size` are the number of lock queues processed in one round trip;
|
|
81
|
+
- read lock data takes ~40% less memory (read locks of older versions are still supported);
|
|
82
|
+
- `#flush_zombies` (and the zombie flusher): lock requests of zombie hosts without locks are dropped too and reported in
|
|
83
|
+
`deleted_zombie_acquirers`;
|
|
70
84
|
|
|
71
85
|
## [1.19.0] - 2026-10-11
|
|
72
86
|
### Added
|
|
73
|
-
- `#unlock_read` (aka `#release_read_lock`):
|
|
74
|
-
|
|
75
|
-
(`identity:` is ignored when `acquirer_id:` is passed: the acquirer id already contains its own identity);
|
|
87
|
+
- `#unlock_read` (aka `#release_read_lock`): the `acquirer_id:` option releases the read lock of another acquirer
|
|
88
|
+
(`nil` by default: the current acquirer);
|
|
76
89
|
|
|
77
90
|
## [1.18.0] - 2026-10-11
|
|
78
91
|
### Added
|
|
79
|
-
- Brand
|
|
80
|
-
|
|
81
|
-
- `:
|
|
82
|
-
-
|
|
83
|
-
|
|
84
|
-
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
- reentrant locks: read lock request under the obtained lock works as a reentrant lock of the obtained lock;
|
|
91
|
-
read-to-write lock upgrade fails with `RedisQueuedLocks::ConflictLockObtainError` (`:conflict_lock_upgrade`)
|
|
92
|
-
for `:work_through`/`:extendable_work_through` conflict strategies;
|
|
93
|
-
- `#unlock`, `#clear_locks`, `#clear_locks_of`, `#clear_current_locks`, `#clear_dead_requests`, `#flush_zombies`,
|
|
94
|
-
`#zombie_locks`, `#zombie_acquirers`, `#zombies_info`, `#locked?`, `#queued?`, `#lock_info`, `#queue_info`,
|
|
95
|
-
`#locks`, `#locks_info`, `#queues`, `#queues_info` support read locks and read lock requests;
|
|
96
|
-
- logs: `rw_mode` in the lock logs and in the lock series logs (`start_lock_series_obtaining`, `lock_series_obtained`,
|
|
97
|
-
`expire_lock_series`: the mode of the series); new try-lock log events: `exit__write_request_ahead`, `exit__read_request_ahead`,
|
|
98
|
-
`exit__read_lock_still_obtained`, `single_process_lock_conflict__lock_upgrade`;
|
|
99
|
-
- instrumentation: `rw_mode` in `lock_obtained`, `reentrant_lock_obtained`, `extendable_reentrant_lock_obtained`,
|
|
100
|
-
`lock_hold_and_release`, `reentrant_lock_hold_completes` payloads and in the lock series payloads
|
|
101
|
-
(`lock_series_obtained`, `lock_series_hold_and_release`: the mode of the series);
|
|
102
|
-
- `:meta` for read locks (stored in the read lock data of each reader, returned by `#lock_info`/`#locks_info`);
|
|
103
|
-
- `#lock_series` with `read_write_mode: :read` (PoC): a series of read locks; only read locks obtained by the series
|
|
104
|
-
are released (reentrant read locks of the outer logic are kept);
|
|
105
|
-
- `#unlock_read` (aka `#release_read_lock`): releases the read lock of the current acquirer only
|
|
106
|
-
(new instrumentation event: `redis_queued_locks.explicit_read_lock_release`);
|
|
107
|
-
- `#extend_lock_ttl`: `read_write_mode:` (`:write` by default) and `identity:` options; `:read` mode extends the read lock
|
|
108
|
-
of the current acquirer only (an expired read lock or a read lock replaced by a writer is not extended);
|
|
109
|
-
- `#extend_lock_ttl`: `all_read_locks:` option (`false` by default, `:read` mode only, ignored in `:write` mode):
|
|
110
|
-
extends all live read locks of the lock (expired read locks are not revived);
|
|
92
|
+
- *Brand New Feature*: **Read/Write locks**: `read_write_mode:` of `#lock`/`#lock!`/`#lock_series`/`#lock_series!`
|
|
93
|
+
(`:write` by default; `:read` is a shared lock that waits for the write lock only; see README "Read/Write Locks"):
|
|
94
|
+
- `:queued` orders read and write lock requests in FIFO between modes (`:random` can starve writers);
|
|
95
|
+
- reentrant read locks; a read-to-write upgrade fails with `:conflict_lock_upgrade` for `:work_through` /
|
|
96
|
+
`:extendable_work_through`;
|
|
97
|
+
- all lock operations (releases, cleanups, zombies, lock info, `#lock_series`) and `:meta` support read locks;
|
|
98
|
+
- `#unlock_read` (aka `#release_read_lock`) with the `redis_queued_locks.explicit_read_lock_release` event;
|
|
99
|
+
- `#extend_lock_ttl`: `read_write_mode:`, `identity:` and `all_read_locks:` options;
|
|
100
|
+
- `rw_mode` in the lock logs and instrumentation payloads, `#lock_info`/`#locks_info` and `#queue_info`/`#queues_info`;
|
|
101
|
+
new try-lock logs (see README `## Logging`);
|
|
102
|
+
- `:meta`: `"rw_mode"` is a reserved lock data key;
|
|
111
103
|
- RBS: `read_write_mode` is typed as `:read | :write`;
|
|
112
|
-
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
-
|
|
119
|
-
like in all other instrumentation events (previously a `Time#to_s` string);
|
|
120
|
-
- **Breaking**: `#extend_lock_ttl` success result is `{ ok: true, result: { extended_locks_count: Integer } }`
|
|
121
|
-
instead of `{ ok: true, result: :ttl_extended }` (the number of locks whose TTL is extended: `1` for the write lock
|
|
122
|
-
and for the own read lock, the number of extended read locks for `all_read_locks: true`);
|
|
123
|
-
the failure result is unchanged (`{ ok: false, result: :async_expire_or_no_lock }`);
|
|
124
|
-
- Lock acquirement attempt: the lock state is fetched in one pipelined round trip; lock data for
|
|
125
|
-
`exit__no_first`/`exit__lock_still_obtained` try-lock logs is extracted only when these logs are enabled
|
|
126
|
-
(previously `HGETALL` was called on every unsuccessful attempt);
|
|
104
|
+
- **all processes should use the RQL version with read/write locks** before read locks are used (write locks of older
|
|
105
|
+
versions do not check read locks);
|
|
106
|
+
### Changed
|
|
107
|
+
- [**Breaking**] `#extend_lock_ttl` success result is `{ ok: true, result: { extended_locks_count: Integer } }` instead of
|
|
108
|
+
`{ ok: true, result: :ttl_extended }`;
|
|
109
|
+
- `lock_series_obtained` instrumentation event (`#lock_series`, PoC): `:ts` is an epoch `Float` (was a `Time#to_s` string);
|
|
110
|
+
- a lock acquirement attempt makes fewer Redis calls;
|
|
127
111
|
### Fixed
|
|
128
|
-
-
|
|
129
|
-
|
|
130
|
-
- `
|
|
131
|
-
- `
|
|
132
|
-
-
|
|
133
|
-
|
|
134
|
-
- reentrant locks of the series (already obtained by the outer logic of the same acquirer) are not released
|
|
135
|
-
by the series anymore (previously the outer logic continued to work without its lock);
|
|
136
|
-
- the series releases only write locks that are still owned by the current acquirer (compare-and-delete):
|
|
137
|
-
a lock expired during the block and obtained by another acquirer is not deleted anymore;
|
|
112
|
+
- logs: an unclosed quote in `dequeue_from_lock_queue`, a missing space in `start_lock_series_obtaining`, `host_id` instead of
|
|
113
|
+
`hst_id` in `extendable_reentrant_lock_obtained`;
|
|
114
|
+
- `#lock_series` (PoC):
|
|
115
|
+
- a series failed without an exception (`raise_errors: false`) kept its obtained locks till their TTL;
|
|
116
|
+
- the series released the reentrant locks of the outer logic of the same acquirer;
|
|
117
|
+
- the series could delete a lock that expired during the block and was obtained by another acquirer;
|
|
138
118
|
|
|
139
119
|
## [1.17.0] - 2026-10-04
|
|
140
120
|
### Changed
|