redis_queued_locks 1.19.0 → 1.21.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 +96 -23
- data/.claude/rules/acquirer.md +39 -20
- data/.claude/rules/arguments.md +3 -2
- data/.claude/rules/git.md +3 -2
- data/.claude/rules/logic.md +32 -8
- data/.claude/rules/swarm.md +14 -9
- data/.claude/rules/tests.md +12 -3
- data/.claude/rules/type-checking.md +2 -1
- data/.claude/rules/visitors.md +10 -11
- data/.claude/skills/rql-code-review/SKILL.md +386 -0
- data/.claude/skills/rql-code-review/templates/report.en.md +31 -0
- data/.claude/skills/rql-code-review/templates/report.ru.md +31 -0
- data/CHANGELOG.md +194 -0
- data/CLAUDE.md +6 -3
- data/README.md +502 -92
- 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 +42 -0
- data/lib/redis_queued_locks/acquirer/acquire_lock/log_visitor.rb +45 -0
- data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rb +1 -0
- data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +695 -128
- 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 +8 -6
- data/lib/redis_queued_locks/acquirer/acquire_lock.rb +134 -26
- data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +229 -24
- data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +189 -18
- 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 +151 -7
- data/lib/redis_queued_locks/acquirer/lock_info.rb +39 -41
- data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +3 -2
- data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +5 -4
- data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +79 -20
- data/lib/redis_queued_locks/acquirer/locks.rb +212 -83
- data/lib/redis_queued_locks/acquirer/queue_info.rb +11 -18
- data/lib/redis_queued_locks/acquirer/queues.rb +123 -50
- data/lib/redis_queued_locks/acquirer/reindex_locks.rb +253 -0
- data/lib/redis_queued_locks/acquirer/release_all_locks.rb +177 -61
- data/lib/redis_queued_locks/acquirer/release_lock.rb +84 -25
- data/lib/redis_queued_locks/acquirer/release_locks_of.rb +274 -70
- data/lib/redis_queued_locks/acquirer/release_read_lock.rb +22 -3
- data/lib/redis_queued_locks/acquirer.rb +1 -0
- data/lib/redis_queued_locks/client.rb +164 -36
- data/lib/redis_queued_locks/config.rb +16 -13
- 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 +113 -6
- data/lib/redis_queued_locks/swarm/flush_zombies.rb +333 -61
- data/lib/redis_queued_locks/swarm/redis_client_builder.rb +8 -4
- data/lib/redis_queued_locks/swarm/swarm_element/threaded.rb +3 -2
- data/lib/redis_queued_locks/swarm/zombie_info.rb +203 -53
- data/lib/redis_queued_locks/swarm.rb +97 -5
- data/lib/redis_queued_locks/utilities.rb +1 -1
- data/lib/redis_queued_locks/version.rb +2 -2
- data/sig/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rbs +14 -0
- data/sig/redis_queued_locks/acquirer/acquire_lock/log_visitor.rbs +13 -0
- data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +107 -1
- data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +3 -3
- data/sig/redis_queued_locks/acquirer/acquire_lock.rbs +3 -1
- data/sig/redis_queued_locks/acquirer/clear_dead_requests.rbs +19 -1
- data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +20 -4
- data/sig/redis_queued_locks/acquirer/keys.rbs +11 -1
- data/sig/redis_queued_locks/acquirer/locks.rbs +15 -6
- data/sig/redis_queued_locks/acquirer/queues.rbs +9 -2
- data/sig/redis_queued_locks/acquirer/reindex_locks.rbs +52 -0
- data/sig/redis_queued_locks/acquirer/release_all_locks.rbs +8 -1
- data/sig/redis_queued_locks/acquirer/release_lock.rbs +6 -3
- data/sig/redis_queued_locks/acquirer/release_locks_of.rbs +20 -1
- data/sig/redis_queued_locks/acquirer/release_read_lock.rbs +1 -1
- data/sig/redis_queued_locks/client.rbs +21 -2
- data/sig/redis_queued_locks/errors.rbs +5 -2
- data/sig/redis_queued_locks/instrument/sampler.rbs +0 -2
- data/sig/redis_queued_locks/logging/sampler.rbs +0 -2
- data/sig/redis_queued_locks/resource.rbs +7 -1
- data/sig/redis_queued_locks/swarm/flush_zombies.rbs +51 -1
- data/sig/redis_queued_locks/swarm/zombie_info.rbs +58 -7
- data/sig/vendor/redis_client.rbs +2 -0
- metadata +6 -1
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,199 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.21.0] - 2026-10-13
|
|
4
|
+
### Added
|
|
5
|
+
- Logging and instrumentation of all operations that change Redis keys (the swarm is not covered yet):
|
|
6
|
+
- the lock acquirement: the new `redis_queued_locks.lock_obtaining_failed` instrumentation event and log
|
|
7
|
+
(`#lock`/`#lock!` and each lock of `#lock_series`/`#lock_series!`: the lock is not obtained, the failure `result`
|
|
8
|
+
(`:timeout_reached`, `:retry_limit_reached`, `:fail_fast_no_try`, `:fail_fast_after_try`, `:conflict_dead_lock`,
|
|
9
|
+
`:conflict_lock_upgrade`) is in the payload; with `raise_errors: true` it is reported before the error is raised);
|
|
10
|
+
- each lock mutation operation is reported with one log line and one instrumentation event with the time of the whole
|
|
11
|
+
operation (the same name and data; the payload ends with `instrument`): the new `redis_queued_locks.extend_lock_ttl`
|
|
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);
|
|
16
|
+
- `#reindex_locks`: `logger:`, `instrumenter:`, `instrument:` and the log/instrumentation sampling options;
|
|
17
|
+
### Changed
|
|
18
|
+
- stricter validation (the details are in "Fixed"): invalid values raise `RedisQueuedLocks::ArgumentError`
|
|
19
|
+
(`RedisQueuedLocks::SwarmArgumentError` for the swarm methods, `RedisQueuedLocks::ConfigValidationError` for the config)
|
|
20
|
+
instead of the previous silent misbehavior:
|
|
21
|
+
- `ttl` of `#lock`/`#lock!`/`#lock_series`/`#lock_series!` should be a positive `Integer` in both modes (`ttl: 0` was
|
|
22
|
+
accepted in the write mode);
|
|
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;
|
|
27
|
+
- `:meta`: reserved lock data keys are rejected as Symbol keys too;
|
|
28
|
+
- the internal timeout errors of `timed: true` and `timeout` inherit from `RedisQueuedLocks::Exception < ::Exception`
|
|
29
|
+
(were `Timeout::Error`s): a `rescue Timeout::Error` (or `rescue => e`) inside the block does not catch the lock timeout
|
|
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`;
|
|
33
|
+
### Fixed
|
|
34
|
+
- the `instrument:` option of `#unlock`, `#clear_locks`, `#clear_locks_of`/`#clear_current_locks` (README: "passed to
|
|
35
|
+
the instrumenter's payload") was not passed to the payload (`#unlock` did not even forward it); the payloads of
|
|
36
|
+
`explicit_lock_release`, `explicit_all_locks_release` and `release_locks_of` have `instrument` now; `#extend_lock_ttl` and
|
|
37
|
+
`#clear_dead_requests` had no instrumentation events at all (see "Added");
|
|
38
|
+
- `#lock`/`#lock!` with a non-positive `ttl` in the write mode: the lock was reported as obtained, but `PEXPIRE` with
|
|
39
|
+
a non-positive ttl deleted the lock key in the obtaining transaction, so other acquirers could obtain the same lock
|
|
40
|
+
at the same time (no mutual exclusion); `ttl` should be a positive `Integer` in both modes now
|
|
41
|
+
(`RedisQueuedLocks::ArgumentError`, previously validated for read locks only); `ttl: nil` was never supported
|
|
42
|
+
(the YARD docs described it as "without timeout");
|
|
43
|
+
- `#lock_series`/`#lock_series!` (PoC) with a non-positive `ttl` (both modes): the locks of the series were obtained with
|
|
44
|
+
the ttl made of the retry delay and jitter only; the series `ttl` should be a positive `Integer` now
|
|
45
|
+
(`RedisQueuedLocks::ArgumentError`);
|
|
46
|
+
- `config['default_lock_ttl']` should be a positive `Integer` (`RedisQueuedLocks::ConfigValidationError`);
|
|
47
|
+
- the config validation accepted zero and negative TTLs, periods and batch/scan sizes: `config['default_queue_ttl'] = 0`
|
|
48
|
+
dropped the own lock request in the queueing pipeline (endless `:dead_score_reached`), zero batch/scan sizes failed
|
|
49
|
+
`SCAN`/`ZSCAN` (`COUNT 0`: `ERR syntax error`) and `each_slice(0)`, zero/negative swarm periods killed the swarm main loops
|
|
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;
|
|
128
|
+
|
|
129
|
+
## [1.20.0] - 2026-10-12
|
|
130
|
+
### Added
|
|
131
|
+
- **Lock index** (opt-in: `config['lock_index_enabled']`, `false` by default: lock-iterating operations scan the keyspace as before):
|
|
132
|
+
`rql:index:locks` (`ZSET`: lock name => the upper bound of the lifetime of all lock keys in client time, ms); with the enabled
|
|
133
|
+
lock index all lock-iterating operations find locks without the full keyspace `SCAN` (see the "Lock Index" README section):
|
|
134
|
+
- the client-wide mode (not a per-call option): the lock acquirement maintains the lock index and lock-iterating operations
|
|
135
|
+
read it only when it is enabled; use the same mode on all processes that share the Redis database (enable it everywhere,
|
|
136
|
+
then invoke `#reindex_locks` once); the swarm reads the mode when it is started;
|
|
137
|
+
- maintained by the lock acquirement itself without new Redis round trips: each lock request adds the lock name (`ZADD GT`
|
|
138
|
+
with the queue ttl), sets the request queue TTL and drops the expired lock names (`ZREMRANGEBYSCORE`) in the queueing pipeline
|
|
139
|
+
(4 more commands per attempt), each lock obtaining (write and read) adds the lock name with the lock ttl in the same
|
|
140
|
+
transaction (1 more command), each extension (`#extend_lock_ttl`, `:extendable_work_through` reentrant locks) increases
|
|
141
|
+
the score by the extension (`ZADD XX INCR`; `#extend_lock_ttl` of write locks sends `EVAL` and the index update in one
|
|
142
|
+
transaction);
|
|
143
|
+
- `#unlock` and `#clear_locks` drop the lock names from the index in the same transaction with the lock keys;
|
|
144
|
+
- lock request queues have TTL: the queue lives as long as its latest lock request (`PEXPIRE NX` + `GT` with the queue ttl on
|
|
145
|
+
each lock request), queues of dead acquirers expire by themselves;
|
|
146
|
+
- lock-iterating operations find locks and lock queues via one `ZSCAN` pass over the lock names (lock keys are derived from
|
|
147
|
+
the lock names): the cost depends on the number of locks only, keys of other applications are never visited; locks without
|
|
148
|
+
the lock index entry (older RQL versions, the disabled lock index) are not found until `#reindex_locks`:
|
|
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;
|
|
196
|
+
|
|
3
197
|
## [1.19.0] - 2026-10-11
|
|
4
198
|
### Added
|
|
5
199
|
- `#unlock_read` (aka `#release_read_lock`): an ability to unlock the read lock of any acquirer
|
data/CLAUDE.md
CHANGED
|
@@ -14,6 +14,8 @@ Full details (config keys, key layout, events, errors, stack, CI): `.claude/proj
|
|
|
14
14
|
- `acquirer/acquire_lock.rb`: composed via `extend` mixins in `acquire_lock/` (try_to_lock, with_acq_timeout, delay_execution, dequeue_from_lock_queue, yield_expire) + `LogVisitor`/`InstrVisitor` event hooks.
|
|
15
15
|
- Locking: `ZADD NX` into the request queue, then WATCH/MULTI; Lua only for write TTL extension/decrease.
|
|
16
16
|
- Read/Write locks (`read_write_mode: :write|:read`): write lock `rql:lock:<name>` + queue `rql:lock_queue:<name>`; readers registry `rql:lock_readers:<name>` (ZSET acq_id => Redis-time expiry), read queue `rql:lock_read_queue:<name>`, per-reader data `rql:lock_reader:<name>:<acq_id>`. Readers WATCH the write key only, writers WATCH the write key + registry; queues give ordering only. Details: overview "Read/Write locks".
|
|
17
|
+
- Lock lookup modes: `config['lock_index_enabled']` (client-wide, `false` by default) is read by `Client`/`Swarm` and passed to every lock-iterating module (swarm, `locks`, `queues`, `keys`, `clear_*`) and to the hot path; each module keeps the keyspace lookup (one `SCAN` pass, the pre-index code) and the lock index lookup (one `ZSCAN` pass) side by side with the shared processing in one copy.
|
|
18
|
+
- Lock index `rql:index:locks` (enabled mode only; ZSET lock name => upper bound of the lifetime of all lock keys, client ms + ttl + `lock_index_grace_period`): every key-creating command is followed by `ZADD GT` (queueing pipeline, obtaining `MULTI`), every extension by `ZADD XX INCR`, names are removed only with all their keys (`unlock`, `clear_locks`), queues have TTL, expired names are pruned on queueing; lock index commands are conditional tails of their pipelines/transactions (none in the disabled mode); `reindex_locks` (one keyspace `SCAN`, both modes) adopts keys without entries after enabling. Details: overview "Lock index".
|
|
17
19
|
- Strategies: `access_strategy` `:queued`|`:random`; `conflict_strategy` `:wait_for_lock`|`:work_through`|`:extendable_work_through`|`:dead_locking`.
|
|
18
20
|
- `resource.rb`: all key names and acquirer/host IDs. `config.rb`: `setting`/`validate` DSL, read as `config['a.b']`.
|
|
19
21
|
- `swarm/`: zombie-lock cleanup. `Supervisor` thread revives `ProbeHosts` (`SwarmElement::Threaded`) and `FlushZombies` (`SwarmElement::Isolated`, Ractor talking via `Ractor::Port`s); each element runs a main-loop thread with its own Redis connection.
|
|
@@ -24,13 +26,14 @@ Full details (config keys, key layout, events, errors, stack, CI): `.claude/proj
|
|
|
24
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).
|
|
25
27
|
- 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.
|
|
26
28
|
- Update the matching `sig/*.rbs` for every `lib/` change; keep Steep green.
|
|
27
|
-
- Keep `# frozen_string_literal: true` and YARD `@api`/`@since`/`@version` tags.
|
|
29
|
+
- 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`).
|
|
28
30
|
- `{ 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`).
|
|
29
31
|
- New operation: `Acquirer::*` module, thin `Client` method (+ `!` variant), RBS, spec.
|
|
30
32
|
- Operation modules behind `Client` methods (all `Acquirer::*` except the `AcquireLock` acquisition core and PoC modules such as `LockSeriesPoC`, which may reuse other modules) are independent: they never call each other or reference each other's RBS types, share logic only via `Resource`/`Utilities`, and duplicate similar logic on purpose (independence over DRY; e.g. `Locks` duplicates the `LockInfo` formatting). Details: `acquirer.md`.
|
|
31
33
|
- New option: `setting` (+ `validate`) in `config.rb`.
|
|
32
34
|
- Dev gems go in `Gemfile`, not the gemspec.
|
|
33
|
-
- Commits (`git.md`): subject only `[<scope>] <summary>` (several features: 1-2 words each, comma-separated), no body; when a step is complete (requirement fully implemented, specs/rubocop/steep green) ask before committing, with the proposed subject
|
|
35
|
+
- Commits (`git.md`): subject only `[<scope>] <summary>` (several features: 1-2 words each, comma-separated), no body; when a step is complete (requirement fully implemented, specs/rubocop/steep green), and only then, run the `rql-code-review` skill on the uncommitted changes, then ask before committing, with the proposed subject, a recap of the changes and the review verdict; an explicit "commit" request needs no extra question and no review (a commit authorized in advance for a task still gets the review); never on `master`, never push.
|
|
34
36
|
- Before implementing, read the relevant sections of `.claude/project-overview.md` (key layout, lock flow, "Read/Write locks", events) and keep their invariants. In the same change, update the overview and the affected `.claude/rules/*.md` for every new/changed key, option, result shape, event, algorithm step or limitation (and README/CHANGELOG `[Unreleased]` for user-visible behavior). New or changed log keys / instrumentation payload keys (visitors or inline `notify`) also go to README `## Logging` / `### Instrumentation Events` (see `visitors.md`).
|
|
35
|
-
-
|
|
37
|
+
- 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
|
+
- Lock-touching code is read/write-aware and index-aware: any operation over locks, queues, zombies or lock info must cover the read family (`rql:lock_readers:*`, `rql:lock_read_queue:*`, `rql:lock_reader:*`) next to `rql:lock:*`/`rql:lock_queue:*`, and any new key-creating/extending command must keep the lock index a superset of the existing locks (enabled mode); a new lock-iterating operation implements both lookups (keyspace and lock index, `lock_index_enabled`). Mutual exclusion comes only from WATCH/MULTI on lock state (no Lua in the RW acquisition path unless asked); queues never guarantee safety.
|
|
36
39
|
- Known debt: rspec-retry, disabled coverage minimum, runtime type-check CI uses `--failure-exit-code=0`.
|