redis_queued_locks 1.21.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 +216 -25
- data/.claude/rules/acquirer.md +6 -5
- 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 +10 -12
- data/.claude/rules/swarm.md +3 -4
- data/.claude/rules/tests.md +6 -6
- data/.claude/rules/type-checking.md +13 -14
- data/.claude/rules/visitors.md +5 -5
- data/.claude/skills/rql-code-review/SKILL.md +112 -35
- data/CHANGELOG.md +98 -244
- data/CLAUDE.md +9 -8
- data/README.md +275 -85
- data/Rakefile +11 -0
- 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 +430 -63
- data/lib/redis_queued_locks/acquirer/acquire_lock.rb +72 -22
- data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +240 -45
- data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +3 -1
- data/lib/redis_queued_locks/acquirer/keys.rb +6 -2
- data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +10 -5
- data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +18 -9
- data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +52 -23
- 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/client.rb +66 -24
- 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/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 +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 +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/queues.rbs +1 -1
- data/sig/redis_queued_locks/client.rbs +10 -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 +4 -3
- /data/sig/redis_queued_locks/{acquier.rbs → acquirer.rbs} +0 -0
data/CHANGELOG.md
CHANGED
|
@@ -1,266 +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
|
+
|
|
3
27
|
## [1.21.0] - 2026-10-13
|
|
4
28
|
### Added
|
|
5
|
-
- Logging and instrumentation of all operations that change Redis keys (the swarm is not covered yet
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
(`#extend_lock_ttl`), `redis_queued_locks.clear_dead_requests` (`#clear_dead_requests`), `redis_queued_locks.reindex_locks`
|
|
13
|
-
(`#reindex_locks`) events, and the new log lines of the existing `explicit_lock_release` (`#unlock`),
|
|
14
|
-
`explicit_read_lock_release` (`#unlock_read`), `explicit_all_locks_release` (`#clear_locks`), `release_locks_of`
|
|
15
|
-
(`#clear_locks_of`/`#clear_current_locks`) events (the `logger:` option of these methods was not used);
|
|
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;
|
|
16
36
|
- `#reindex_locks`: `logger:`, `instrumenter:`, `instrument:` and the log/instrumentation sampling options;
|
|
17
37
|
### Changed
|
|
18
|
-
- stricter validation
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
- `#extend_lock_ttl`: `milliseconds` should be a positive `Integer` (`extend_lock_ttl(name, 0)` was a successful no-op);
|
|
24
|
-
- the per-call numeric options should be `Integer`s in the ranges of their config keys: `Float` values (e.g.
|
|
25
|
-
`timeout: 0.5`, `retry_delay: 10.0`) are rejected now;
|
|
26
|
-
- the config: positive TTLs, periods and batch/scan sizes;
|
|
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;
|
|
27
43
|
- `:meta`: reserved lock data keys are rejected as Symbol keys too;
|
|
28
|
-
- the internal timeout errors of `timed: true` and `timeout`
|
|
29
|
-
|
|
30
|
-
anymore; the public errors/results are the same;
|
|
31
|
-
- `#locks_info`: the info of write locks has the `"lock_key"` field (like `#lock_info`);
|
|
32
|
-
- instrumentation payloads: `explicit_lock_release`, `explicit_all_locks_release` and `release_locks_of` have `instrument`;
|
|
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);
|
|
33
46
|
### Fixed
|
|
34
|
-
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
- `#lock_series
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
- `
|
|
47
|
-
- the
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
and the supervisor (`sleep` with a negative value) or made them busy loops, a non-positive `zombie_ttl` made all hosts
|
|
51
|
-
zombies; these keys should be positive `Integer`s now (`RedisQueuedLocks::ConfigValidationError`): `default_queue_ttl`,
|
|
52
|
-
`lock_release_batch_size`, `clear_locks_of__lock_scan_size`, `clear_locks_of__queue_scan_size`, `key_extraction_batch_size`,
|
|
53
|
-
`swarm.supervisor.liveness_probing_period`, `swarm.probe_hosts.probe_period`, `swarm.flush_zombies.zombie_ttl`,
|
|
54
|
-
`swarm.flush_zombies.zombie_flush_period`, `swarm.flush_zombies.zombie_lock_scan_size`,
|
|
55
|
-
`swarm.flush_zombies.zombie_queue_scan_size`;
|
|
56
|
-
- `#extend_lock_ttl` with a non-positive `milliseconds` decreased the lock TTL (or deleted the lock) and reported a successful
|
|
57
|
-
extension: `milliseconds` should be a positive `Integer` now (`RedisQueuedLocks::ArgumentError`);
|
|
58
|
-
- `#lock`/`#lock!`/`#lock_series`/`#lock_series!` (both modes): reserved lock data keys were rejected in `:meta` only as
|
|
59
|
-
String keys, while Symbol keys (`meta: { acq_id: '...' }`) passed the validation and overwrote the lock data fields
|
|
60
|
-
(`acq_id`, `hst_id`, `ts`, `ini_ttl`, `spc_*`, ...: reentrant locks, owner checks of lock releases and lock info were
|
|
61
|
-
broken while the lock lived); Symbol keys are validated too now (`RedisQueuedLocks::ArgumentError`);
|
|
62
|
-
- `timed: true` and `timeout`: the internal timeout errors inherited from `Timeout::Error` (a `StandardError`), so `rescue => e`,
|
|
63
|
-
`rescue StandardError`, `rescue Timeout::Error` and `rescue nil` of the interrupted code swallowed them: a `timed` block with
|
|
64
|
-
such a `rescue` continued after the lock TTL (another acquirer could obtain the expired lock, and the write lock release
|
|
65
|
-
after the block deleted the lock of that acquirer), and the acquirement timeout that interrupted a logger/instrumenter
|
|
66
|
-
call (`rescue nil` of the log/instrumentation events) was lost (with `retry_count: nil` the acquirement waited without
|
|
67
|
-
the timeout); the internal timeouts inherit from the new private base `RedisQueuedLocks::Exception < ::Exception` now
|
|
68
|
-
(like `Timeout::ExitException`): only `rescue Exception` catches them, `ensure` still runs, the public errors/results
|
|
69
|
-
are the same (`RedisQueuedLocks::TimedLockTimeoutError`, `RedisQueuedLocks::LockAcquirementTimeoutError`,
|
|
70
|
-
`:timeout_reached`); a connection interrupted inside a `WATCH` block is closed (redis-client sends `UNWATCH` only on
|
|
71
|
-
`StandardError`s: the next transaction on the connection would be aborted);
|
|
72
|
-
- `#lock_series`/`#lock_series!` (PoC): the log events and the `redis_queued_locks.lock_series_hold_and_release`
|
|
73
|
-
instrumentation event raised the errors of the logger/instrumenter: a failing logger broke the series call (after
|
|
74
|
-
obtaining all the locks of the series the locks were not released till their ttl), a failing instrumenter replaced
|
|
75
|
-
the block result with its error; the logger/instrumenter errors are swallowed now (like in all other events);
|
|
76
|
-
- `#locks_info` (and `#locks(with_info: true)`): the info of write locks had no `"lock_key"` field (the info of read locks and
|
|
77
|
-
`#lock_info` have it), so the info shape depended on the lock mode; the write lock info has `"lock_key"` now (the same
|
|
78
|
-
fields as `#lock_info`);
|
|
79
|
-
- `RedisQueuedLocks::Logging::Sampler` and `RedisQueuedLocks::Instrument::Sampler` (the default log/instrumentation samplers)
|
|
80
|
-
sampled `sampling_percent` of 101 values (`SecureRandom.rand(0..100)`): `0%` sampled ~1% of the cases (instead of none),
|
|
81
|
-
`15%` sampled ~15.8%; they sample exactly `sampling_percent` of 100 values now (`0%` - never, `100%` - always);
|
|
82
|
-
the private `SAMPLING_PERCENT_RANGE` constants are removed;
|
|
83
|
-
- swarm: the Redis client builder of the swarm elements mutated the `swarm.*.redis_config.config`/`pool_config` hashes of
|
|
84
|
-
the client config (`transform_keys!`): with frozen hashes each start of the host probing and zombie flushing main loops
|
|
85
|
-
failed with `FrozenError` (the hosts were not probed, zombie locks were not flushed); the hashes are not mutated now;
|
|
86
|
-
- RBS: the `RedisQueuedLocks::Client#current_acq_id` alias was declared as `ccurrent_acq_id` (a typo: the public method was
|
|
87
|
-
missing in the signatures); `RedisQueuedLocks::Client#queues_info` returned the `queues` union (`queueList | queuesInfo`)
|
|
88
|
-
instead of `queuesInfo`;
|
|
89
|
-
- `RedisQueuedLocks::Resource.lock_key_from_queue` (a private debug helper for manual checks in a console, not used by
|
|
90
|
-
the library) returned a wrong key for the read lock request queues (`rql:lock:ueue:<name>`); it supports the write and
|
|
91
|
-
read lock request queues now and returns `nil` for other keys;
|
|
92
|
-
- a connection interrupted inside a `WATCH` block by a `Timeout.timeout` of your code without an exception class
|
|
93
|
-
(`Timeout::ExitException`, e.g. `Timeout.timeout(5) { rql.lock("x") { ... } }`) returned to the connection pool with
|
|
94
|
-
the active `WATCH` (redis-client sends `UNWATCH` only on `StandardError`s): the next transaction on the connection could
|
|
95
|
-
be aborted (e.g. a lock release); the connection is closed now (like with the internal timeouts);
|
|
96
|
-
- the per-call numeric options were not validated (zero/negative values broke the operations like the same config values
|
|
97
|
-
before the config validation fix): they are validated with the same ranges as their config keys now
|
|
98
|
-
(`RedisQueuedLocks::ArgumentError`; `RedisQueuedLocks::SwarmArgumentError` for the swarm methods), before any Redis
|
|
99
|
-
command; the values should be `Integer`s (like the config values: `Float` values such as `timeout: 0.5` or
|
|
100
|
-
`retry_delay: 10.0` are rejected now):
|
|
101
|
-
- `#lock`/`#lock!`/`#lock_series`/`#lock_series!`: `queue_ttl` > 0, `timeout` and `retry_count` - `nil` or >= 0,
|
|
102
|
-
`retry_delay`, `retry_jitter` and `lock_index_grace_period` >= 0;
|
|
103
|
-
- `#locks`, `#locks_info`, `#queues`, `#queues_info`, `#keys`, `#clear_dead_requests`, `#reindex_locks`: `scan_size` > 0;
|
|
104
|
-
`#clear_locks`: `batch_size` > 0; `#clear_locks_of`/`#clear_current_locks`: `lock_scan_size`, `queue_scan_size` > 0;
|
|
105
|
-
`#clear_dead_requests`: `dead_ttl` > 0; `#reindex_locks`: `queue_ttl` > 0, `lock_index_grace_period` >= 0;
|
|
106
|
-
- `#swarm_info`, `#flush_zombies`, `#zombie_locks`, `#zombie_acquirers`, `#zombie_hosts`, `#zombies_info`: `zombie_ttl`
|
|
107
|
-
and the scan sizes > 0;
|
|
108
|
-
- README/YARD documentation that did not match the code:
|
|
109
|
-
- config keys: `config['instr_sampling_enabled']` in the configuration example (was `nstr_sampling_enabled`),
|
|
110
|
-
`config['log_sampling_enabled']` (was `log_samplign_enabled`), `*_sampling_enabled` (was `*_sampling_enalbed`);
|
|
111
|
-
- `#lock` results: `:retry_limit_reached` (was `:retry_count_reached`), `:conflict_lock_upgrade` is listed, `:unknown`
|
|
112
|
-
is not returned, `:dead_locking` is not a `:process` value (it fails with `:conflict_dead_lock`), the result has `hst_id`;
|
|
113
|
-
- `#lock_info`: `"ini_ttl"` (was `"init_ttl"`), `"spc_cnt"` (was `"spc_count"`), host ids `rql:hst:...` in the examples;
|
|
114
|
-
- `#unlock`: `rel_time` is in milliseconds (was "in seconds");
|
|
115
|
-
- `#clear_dead_requests` returns `{ ok: true, result: { processed_queues: Set<String> } }` (write and read request queues);
|
|
116
|
-
- `#extend_lock_ttl`: the write lock TTL is extended atomically by one Lua script, without the lock owner check
|
|
117
|
-
(was "non-atomic, two commands");
|
|
118
|
-
- the log lists of the `### Configuration` section: `rw_mode`, the key order, the read/write and lock series logs
|
|
119
|
-
(the same as in the `## Logging` section);
|
|
120
|
-
- the `#lock!` link of the Usage list and the `#lock` link of `#lock_info`, the Usage list and the table of contents: `unlock_read`, `clear_locks_of`,
|
|
121
|
-
`clear_current_locks`, `reindex_locks`;
|
|
122
|
-
- YARD: the `#clear_current_locks` options (`lock_scan_size`, `queue_scan_size` instead of the missing `batch_size`), the tag
|
|
123
|
-
typos (`@sine`, `@since ?.?.?`, `@since 19.0.0`, `@api ppublic`/`pulic`/`pub;ic`); the inert `# steep:ingore` comments
|
|
124
|
-
are removed;
|
|
125
|
-
- YARD: the `@version` tags of the classes/modules (versions >= 1.17.0) follow the interface changes only (method
|
|
126
|
-
signatures, added/removed methods, `extend`/`include`/`prepend`, the superclass), not the behavior changes inside
|
|
127
|
-
the methods;
|
|
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`);
|
|
128
63
|
|
|
129
64
|
## [1.20.0] - 2026-10-12
|
|
130
65
|
### Added
|
|
131
|
-
- **Lock
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
- swarm (`#flush_zombies`, the zombie flusher, `#zombie_locks`/`#zombie_acquirers`/`#zombies_info`): `zombie_lock_scan_size` is
|
|
150
|
-
the `ZSCAN` count and the batch size of locks, `zombie_queue_scan_size` - the batch size of lock queues (both processed during
|
|
151
|
-
the same pass); `#flush_zombies` drops the expired lock names from the index;
|
|
152
|
-
- `#locks`/`#locks_info`, `#queues`/`#queues_info`, `#keys`: `scan_size` is the `ZSCAN` count and the batch size of locks;
|
|
153
|
-
`#keys` returns the existing keys of the lock key families of the indexed locks (other `rql:lock*` keys are not listed);
|
|
154
|
-
- `#clear_locks`: the keys of each batch of locks are deleted with one `DEL` in one transaction with the removal of their lock
|
|
155
|
-
names from the lock index; `batch_size` is the number of locks;
|
|
156
|
-
- `#clear_locks_of`/`#clear_current_locks`: `clear_locks_of__lock_scan_size` is the `ZSCAN` count and the batch size of locks,
|
|
157
|
-
`clear_locks_of__queue_scan_size` - the number of lock queues processed in one round trip;
|
|
158
|
-
- `#clear_dead_requests`: `scan_size` is the `ZSCAN` count and the number of locks (both lock queues of each lock) processed in
|
|
159
|
-
one round trip; the expired lock names are dropped from the index;
|
|
160
|
-
- `config['lock_index_grace_period']` (`60_000` ms by default) and `lock_index_grace_period:` option of `#lock`/`#lock!`/
|
|
161
|
-
`#lock_series`/`#lock_series!`: the lock name stays in the index for this period after the expiration of all its keys
|
|
162
|
-
(the client clock skew and network latency tolerance);
|
|
163
|
-
- `#reindex_locks`: adds the lock keys without the lock index entry (keys of older RQL versions and keys created with the disabled
|
|
164
|
-
lock index) to the lock index (one `SCAN` pass, lock queues without TTL get the queue ttl); **invoke it once after the lock index
|
|
165
|
-
is enabled on all your processes**;
|
|
166
|
-
### Changed
|
|
167
|
-
- `#flush_zombies` (and the zombie flusher of the swarm): zombie lock requests are dropped with one `ZREM` per lock queue
|
|
168
|
-
(previously one `ZREM` per zombie acquirer and lock queue);
|
|
169
|
-
- `#queue_info` / `#queues_info`: lock queues are read without `EXISTS` checks (Redis does not keep empty sorted sets);
|
|
170
|
-
- Lock acquirement attempt: read lock requests do not fetch the longest read lock (it is used by write lock requests only),
|
|
171
|
-
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;
|
|
172
|
-
- Lock acquirement attempt: the lock request queueing (`ZADD NX`, expired requests pruning, queue heads) is one pipelined
|
|
173
|
-
round trip instead of three; with `:wait_for_lock`/`:dead_locking` conflict strategies (without `fail_fast`) it is sent
|
|
174
|
-
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;
|
|
175
|
-
- SCAN-based operations (`#clear_locks`, `#clear_locks_of`/`#clear_current_locks`, `#clear_dead_requests`, `#locks`/`#locks_info`,
|
|
176
|
-
`#queues`/`#queues_info`, `#flush_zombies`, `#zombie_locks`/`#zombie_acquirers`/`#zombies_info`) make one `SCAN` pass instead
|
|
177
|
-
of 2-5 passes (one per key family) and send per-key commands in pipelined batches instead of one round trip per key:
|
|
178
|
-
- `#clear_locks` deletes the found keys during the scan with one `DEL` per batch (previously all keys were expired at the end,
|
|
179
|
-
lock queues first);
|
|
180
|
-
- `#clear_locks_of`/`#clear_current_locks` (`clear_locks_of__queue_scan_size`) and `#flush_zombies` (`zombie_queue_scan_size`):
|
|
181
|
-
the queue scan size is the number of lock queues processed in one round trip (queues are found by the same pass as locks);
|
|
182
|
-
- `#zombie_locks`/`#zombie_acquirers`/`#zombies_info` do not scan the keyspace when there are no zombie hosts;
|
|
183
|
-
- Read lock data (`rql:lock_reader:<lock_name>:<acquirer_id>`) does not store `acq_id`/`hst_id` (both are derived from the acquirer id):
|
|
184
|
-
~40% less memory per read lock (`MEMORY USAGE` 280 -> 168 bytes without `:meta`); `#lock_info`/`#locks_info` report them as before
|
|
185
|
-
(the first fields of each reader; read lock data of older versions is supported);
|
|
186
|
-
- `#unlock`: one Redis round trip when there are no read locks (the readers registry is read in the release transaction;
|
|
187
|
-
the read lock data is dropped with one more `DEL` only when read locks existed);
|
|
188
|
-
- `#lock_series` (PoC): write locks of the series are released (compare-and-delete) in one transaction for the whole series
|
|
189
|
-
(three round trips per series instead of three round trips per lock);
|
|
190
|
-
- `#flush_zombies` (and the zombie flusher of the swarm): requests of zombie hosts are dropped from the lock queues by the host
|
|
191
|
-
of the request (derived from its acquirer id): requests of dead hosts that hold no locks are dropped too (previously they stayed
|
|
192
|
-
until the queue ttl) and their acquirers are reported in `deleted_zombie_acquirers`;
|
|
193
|
-
- Lock acquirement: retries and `fail_fast` attempts pre-check the lock state without `WATCH` (one round trip): a retry that can not
|
|
194
|
-
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
|
|
195
|
-
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`;
|
|
196
84
|
|
|
197
85
|
## [1.19.0] - 2026-10-11
|
|
198
86
|
### Added
|
|
199
|
-
- `#unlock_read` (aka `#release_read_lock`):
|
|
200
|
-
|
|
201
|
-
(`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);
|
|
202
89
|
|
|
203
90
|
## [1.18.0] - 2026-10-11
|
|
204
91
|
### Added
|
|
205
|
-
- Brand
|
|
206
|
-
|
|
207
|
-
- `:
|
|
208
|
-
-
|
|
209
|
-
|
|
210
|
-
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
- reentrant locks: read lock request under the obtained lock works as a reentrant lock of the obtained lock;
|
|
217
|
-
read-to-write lock upgrade fails with `RedisQueuedLocks::ConflictLockObtainError` (`:conflict_lock_upgrade`)
|
|
218
|
-
for `:work_through`/`:extendable_work_through` conflict strategies;
|
|
219
|
-
- `#unlock`, `#clear_locks`, `#clear_locks_of`, `#clear_current_locks`, `#clear_dead_requests`, `#flush_zombies`,
|
|
220
|
-
`#zombie_locks`, `#zombie_acquirers`, `#zombies_info`, `#locked?`, `#queued?`, `#lock_info`, `#queue_info`,
|
|
221
|
-
`#locks`, `#locks_info`, `#queues`, `#queues_info` support read locks and read lock requests;
|
|
222
|
-
- logs: `rw_mode` in the lock logs and in the lock series logs (`start_lock_series_obtaining`, `lock_series_obtained`,
|
|
223
|
-
`expire_lock_series`: the mode of the series); new try-lock log events: `exit__write_request_ahead`, `exit__read_request_ahead`,
|
|
224
|
-
`exit__read_lock_still_obtained`, `single_process_lock_conflict__lock_upgrade`;
|
|
225
|
-
- instrumentation: `rw_mode` in `lock_obtained`, `reentrant_lock_obtained`, `extendable_reentrant_lock_obtained`,
|
|
226
|
-
`lock_hold_and_release`, `reentrant_lock_hold_completes` payloads and in the lock series payloads
|
|
227
|
-
(`lock_series_obtained`, `lock_series_hold_and_release`: the mode of the series);
|
|
228
|
-
- `:meta` for read locks (stored in the read lock data of each reader, returned by `#lock_info`/`#locks_info`);
|
|
229
|
-
- `#lock_series` with `read_write_mode: :read` (PoC): a series of read locks; only read locks obtained by the series
|
|
230
|
-
are released (reentrant read locks of the outer logic are kept);
|
|
231
|
-
- `#unlock_read` (aka `#release_read_lock`): releases the read lock of the current acquirer only
|
|
232
|
-
(new instrumentation event: `redis_queued_locks.explicit_read_lock_release`);
|
|
233
|
-
- `#extend_lock_ttl`: `read_write_mode:` (`:write` by default) and `identity:` options; `:read` mode extends the read lock
|
|
234
|
-
of the current acquirer only (an expired read lock or a read lock replaced by a writer is not extended);
|
|
235
|
-
- `#extend_lock_ttl`: `all_read_locks:` option (`false` by default, `:read` mode only, ignored in `:write` mode):
|
|
236
|
-
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;
|
|
237
103
|
- RBS: `read_write_mode` is typed as `:read | :write`;
|
|
238
|
-
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
-
|
|
245
|
-
like in all other instrumentation events (previously a `Time#to_s` string);
|
|
246
|
-
- **Breaking**: `#extend_lock_ttl` success result is `{ ok: true, result: { extended_locks_count: Integer } }`
|
|
247
|
-
instead of `{ ok: true, result: :ttl_extended }` (the number of locks whose TTL is extended: `1` for the write lock
|
|
248
|
-
and for the own read lock, the number of extended read locks for `all_read_locks: true`);
|
|
249
|
-
the failure result is unchanged (`{ ok: false, result: :async_expire_or_no_lock }`);
|
|
250
|
-
- Lock acquirement attempt: the lock state is fetched in one pipelined round trip; lock data for
|
|
251
|
-
`exit__no_first`/`exit__lock_still_obtained` try-lock logs is extracted only when these logs are enabled
|
|
252
|
-
(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;
|
|
253
111
|
### Fixed
|
|
254
|
-
-
|
|
255
|
-
|
|
256
|
-
- `
|
|
257
|
-
- `
|
|
258
|
-
-
|
|
259
|
-
|
|
260
|
-
- reentrant locks of the series (already obtained by the outer logic of the same acquirer) are not released
|
|
261
|
-
by the series anymore (previously the outer logic continued to work without its lock);
|
|
262
|
-
- the series releases only write locks that are still owned by the current acquirer (compare-and-delete):
|
|
263
|
-
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;
|
|
264
118
|
|
|
265
119
|
## [1.17.0] - 2026-10-04
|
|
266
120
|
### Changed
|
data/CLAUDE.md
CHANGED
|
@@ -4,8 +4,8 @@ Ruby gem: distributed Redis locks with a per-lock FIFO acquisition queue. Ruby >
|
|
|
4
4
|
Full details (config keys, key layout, events, errors, stack, CI): `.claude/project-overview.md`.
|
|
5
5
|
|
|
6
6
|
## Commands
|
|
7
|
-
- Tests (need a running local Redis): `bundle exec rake rspec`
|
|
8
|
-
- Lint: `bundle exec rake rubocop`
|
|
7
|
+
- Tests (need a running local Redis): `bundle exec rake rspec:parallel` by default (~1 min; `WORKERS=<n>`, default: Redis `databases` / 2 up to the CPU count; each worker flushes its own pair of databases: db 0..2n-1 in total); the serial `bundle exec rake rspec` (~6 min) only when something must be checked in one process (order-dependent failures, a whole-suite `--seed`, leaks between examples, changes of the parallel runner: `tests.md#1`)
|
|
8
|
+
- Lint: `bundle exec rake rubocop` (two passes, Ruby then RBS: both must be clean; never tune `.rubocop.yml` to pass, fix the code)
|
|
9
9
|
- Static types: `bundle exec rbs collection install && bundle exec rake steep:check`
|
|
10
10
|
|
|
11
11
|
## Architecture
|
|
@@ -13,27 +13,28 @@ Full details (config keys, key layout, events, errors, stack, CI): `.claude/proj
|
|
|
13
13
|
- `acquirer/*.rb`: one stateless module per operation (`class << self`), returns `{ ok:, result: }`; `!` client methods raise.
|
|
14
14
|
- `acquirer/acquire_lock.rb`: composed via `extend` mixins in `acquire_lock/` (try_to_lock, with_acq_timeout, delay_execution, dequeue_from_lock_queue, yield_expire) + `LogVisitor`/`InstrVisitor` event hooks.
|
|
15
15
|
- Locking: `ZADD NX` into the request queue, then WATCH/MULTI; Lua only for write TTL extension/decrease.
|
|
16
|
-
-
|
|
16
|
+
- Lock request priorities (`priority:` -100..100 or a priority name of `config['lock_priorities']`, resolved to its Integer in `AcquireLock` Step 0.1 and before a lock series; `config['default_lock_priority']`): queue score = enqueue time - priority * 2**32 (`Resource.calc_initial_acquirer_position`), strict priority + FIFO inside a priority; dead requests are pruned per priority band (never one `-inf..now-ttl` range): the own band on each queueing, the bands of the dead requests that block the attempt (the write queue head, the earlier opposite-mode request) in one more round trip (`TryToLock#drop_dead_requests_ahead`, band and liveness decoded from the score); the attempt cost never depends on the priority range; every lock acquirement log/event carries the Integer `priority`. Component map: overview "Lock request prioritization in the architecture"; details: overview "Lock request priorities".
|
|
17
|
+
- Read/Write locks (`read_write_mode: :write|:read`): write lock `rql:lock:<name>` + queue `rql:lock_queue:<name>`; readers registry `rql:lock_readers:<name>` (ZSET acq_id => Redis-time expiry), read queue `rql:lock_read_queue:<name>`, per-reader data `rql:lock_reader:<name>:<acq_id>`. Readers WATCH the write key only, writers WATCH the write key + registry; queues give ordering only. Component map: overview "Read/Write locks in the architecture"; details: overview "Read/Write locks".
|
|
17
18
|
- Lock lookup modes: `config['lock_index_enabled']` (client-wide, `false` by default) is read by `Client`/`Swarm` and passed to every lock-iterating module (swarm, `locks`, `queues`, `keys`, `clear_*`) and to the hot path; each module keeps the keyspace lookup (one `SCAN` pass, the pre-index code) and the lock index lookup (one `ZSCAN` pass) side by side with the shared processing in one copy.
|
|
18
19
|
- Lock index `rql:index:locks` (enabled mode only; ZSET lock name => upper bound of the lifetime of all lock keys, client ms + ttl + `lock_index_grace_period`): every key-creating command is followed by `ZADD GT` (queueing pipeline, obtaining `MULTI`), every extension by `ZADD XX INCR`, names are removed only with all their keys (`unlock`, `clear_locks`), queues have TTL, expired names are pruned on queueing; lock index commands are conditional tails of their pipelines/transactions (none in the disabled mode); `reindex_locks` (one keyspace `SCAN`, both modes) adopts keys without entries after enabling. Details: overview "Lock index".
|
|
19
20
|
- Strategies: `access_strategy` `:queued`|`:random`; `conflict_strategy` `:wait_for_lock`|`:work_through`|`:extendable_work_through`|`:dead_locking`.
|
|
20
21
|
- `resource.rb`: all key names and acquirer/host IDs. `config.rb`: `setting`/`validate` DSL, read as `config['a.b']`.
|
|
21
22
|
- `swarm/`: zombie-lock cleanup. `Supervisor` thread revives `ProbeHosts` (`SwarmElement::Threaded`) and `FlushZombies` (`SwarmElement::Isolated`, Ractor talking via `Ractor::Port`s); each element runs a main-loop thread with its own Redis connection.
|
|
22
23
|
- `logging/`, `instrument/`: Void null-object defaults, percent samplers, ActiveSupport adapter.
|
|
23
|
-
- `sig/`: RBS mirror of `lib/`. `spec/redis_queued_locks_spec.rb`: single integration spec.
|
|
24
|
+
- `sig/`: RBS mirror of `lib/`. `spec/redis_queued_locks_spec.rb`: single integration spec (+ Redis-free `spec/redis_queued_locks/config_spec.rb`).
|
|
24
25
|
|
|
25
26
|
## Rules
|
|
26
|
-
Detailed, path-scoped rules in `.claude/rules/`: `logic.md` (lib, general), `acquirer.md` (acquirer modules, Redis access), `visitors.md` (log/instrumentation visitors), `arguments.md` (long keyword lists), `swarm.md` (swarm supervisor/elements, Ractor/Thread rules), `tests.md` (spec), `type-checking.md` (sig/Steep), `git.md` (commit messages and commit flow, always loaded).
|
|
27
|
+
Detailed, path-scoped rules in `.claude/rules/`: `logic.md` (lib, general), `acquirer.md` (acquirer modules, Redis access), `visitors.md` (log/instrumentation visitors), `arguments.md` (long keyword lists), `swarm.md` (swarm supervisor/elements, Ractor/Thread rules), `tests.md` (spec), `type-checking.md` (sig/Steep), `changelog.md` (CHANGELOG entries), `git.md` (commit messages and commit flow, always loaded).
|
|
27
28
|
- Long explicit keyword/parameter lists are intentional (minimal allocations, signature-as-DSL): never introduce parameter objects, option structs or `**opts`; add new options as explicit keywords to every `Client` variant and forward them by name.
|
|
28
29
|
- Update the matching `sig/*.rbs` for every `lib/` change; keep Steep green.
|
|
29
30
|
- Keep `# frozen_string_literal: true` and YARD `@api`/`@since`/`@version` tags (class/module `@version` changes only with the interface: signatures, added/removed methods, `extend`/`include`/`prepend`, the superclass; `logic.md#2`).
|
|
30
31
|
- `{ ok:, result: }` is for the public API only (`Client`, `Acquirer::*`, `Swarm` facade actions). Internal swarm element APIs (commands, replies, helpers) use bare scalars/primitives (see `swarm.md`).
|
|
31
32
|
- New operation: `Acquirer::*` module, thin `Client` method (+ `!` variant), RBS, spec.
|
|
32
33
|
- Operation modules behind `Client` methods (all `Acquirer::*` except the `AcquireLock` acquisition core and PoC modules such as `LockSeriesPoC`, which may reuse other modules) are independent: they never call each other or reference each other's RBS types, share logic only via `Resource`/`Utilities`, and duplicate similar logic on purpose (independence over DRY; e.g. `Locks` duplicates the `LockInfo` formatting). Details: `acquirer.md`.
|
|
33
|
-
- New option: `setting` (+ `validate`) in `config.rb
|
|
34
|
+
- New option: `setting` (+ `validate`) in `config.rb` + its `Config#[]` overload line in `sig/redis_queued_locks/config.rbs` (typed per key).
|
|
34
35
|
- Dev gems go in `Gemfile`, not the gemspec.
|
|
35
36
|
- Commits (`git.md`): subject only `[<scope>] <summary>` (several features: 1-2 words each, comma-separated), no body; when a step is complete (requirement fully implemented, specs/rubocop/steep green), and only then, run the `rql-code-review` skill on the uncommitted changes, then ask before committing, with the proposed subject, a recap of the changes and the review verdict; an explicit "commit" request needs no extra question and no review (a commit authorized in advance for a task still gets the review); never on `master`, never push.
|
|
36
|
-
- Before implementing, read the relevant sections of `.claude/project-overview.md` (key layout, lock flow, "Read/Write locks", events) and keep their invariants. In the same change, update the overview and the affected `.claude/rules/*.md` for every new/changed key, option, result shape, event, algorithm step or limitation (and README/CHANGELOG `[Unreleased]` for user-visible behavior). New or changed log keys / instrumentation payload keys (visitors or inline `notify`) also go to README `## Logging` / `### Instrumentation Events` (see `visitors.md`).
|
|
37
|
+
- Before implementing, read the relevant sections of `.claude/project-overview.md` (key layout, lock flow, "Read/Write locks", events) and keep their invariants. In the same change, update the overview and the affected `.claude/rules/*.md` for every new/changed key, option, result shape, event, algorithm step or limitation (and README/CHANGELOG `[Unreleased]` for user-visible behavior; CHANGELOG entries are compact: only the subject-level change in 1-2 lines, no implementation details; a fundamentally new feature or globally new support is a separate `*Brand New Feature*` entry, `changelog.md`). New or changed log keys / instrumentation payload keys (visitors or inline `notify`) also go to README `## Logging` / `### Instrumentation Events` (see `visitors.md`).
|
|
37
38
|
- Observability (`logic.md#16`): logic that creates/changes/deletes keys is logged and instrumented: the lock acquirement per meaningful outcome (visitors), each mutation operation with one log line + one event of the whole root operation called by `Client` (inline, `instrument` last in the payload); lock info queries are neither; the swarm elements are known debt.
|
|
38
39
|
- Lock-touching code is read/write-aware and index-aware: any operation over locks, queues, zombies or lock info must cover the read family (`rql:lock_readers:*`, `rql:lock_read_queue:*`, `rql:lock_reader:*`) next to `rql:lock:*`/`rql:lock_queue:*`, and any new key-creating/extending command must keep the lock index a superset of the existing locks (enabled mode); a new lock-iterating operation implements both lookups (keyspace and lock index, `lock_index_enabled`). Mutual exclusion comes only from WATCH/MULTI on lock state (no Lua in the RW acquisition path unless asked); queues never guarantee safety.
|
|
39
|
-
- Known debt: rspec-retry, disabled coverage minimum
|
|
40
|
+
- Known debt: rspec-retry, disabled coverage minimum.
|