redis_queued_locks 1.17.0 → 1.18.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 +86 -10
- data/.claude/rules/acquirer.md +31 -11
- data/.claude/rules/arguments.md +2 -1
- data/.claude/rules/git.md +46 -0
- data/.claude/rules/logic.md +9 -3
- data/.claude/rules/swarm.md +4 -3
- data/.claude/rules/tests.md +9 -3
- data/.claude/rules/type-checking.md +10 -2
- data/.claude/rules/visitors.md +12 -7
- data/CHANGELOG.md +62 -0
- data/CLAUDE.md +7 -2
- data/README.md +348 -79
- data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rb +6 -3
- data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rb +9 -7
- data/lib/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rb +20 -10
- data/lib/redis_queued_locks/acquirer/acquire_lock/log_visitor.rb +34 -16
- data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rb +240 -28
- data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +398 -129
- data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire/log_visitor.rb +12 -6
- data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +32 -8
- data/lib/redis_queued_locks/acquirer/acquire_lock.rb +73 -20
- data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +9 -1
- data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +182 -3
- data/lib/redis_queued_locks/acquirer/is_locked.rb +19 -2
- data/lib/redis_queued_locks/acquirer/is_queued.rb +6 -2
- data/lib/redis_queued_locks/acquirer/lock_info.rb +96 -6
- data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +8 -2
- data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +18 -6
- data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +126 -27
- data/lib/redis_queued_locks/acquirer/locks.rb +103 -6
- data/lib/redis_queued_locks/acquirer/queue_info.rb +38 -9
- data/lib/redis_queued_locks/acquirer/queues.rb +28 -4
- data/lib/redis_queued_locks/acquirer/release_all_locks.rb +35 -1
- data/lib/redis_queued_locks/acquirer/release_lock.rb +40 -9
- data/lib/redis_queued_locks/acquirer/release_locks_of.rb +43 -3
- data/lib/redis_queued_locks/acquirer/release_read_lock.rb +143 -0
- data/lib/redis_queued_locks/acquirer.rb +1 -0
- data/lib/redis_queued_locks/client.rb +129 -14
- data/lib/redis_queued_locks/resource.rb +117 -9
- data/lib/redis_queued_locks/swarm/flush_zombies.rb +38 -0
- data/lib/redis_queued_locks/swarm/zombie_info.rb +42 -0
- data/lib/redis_queued_locks/version.rb +1 -1
- data/rbs_collection.lock.yaml +10 -2
- data/rbs_collection.yaml +0 -2
- data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rbs +2 -1
- data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rbs +0 -1
- data/sig/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rbs +5 -0
- data/sig/redis_queued_locks/acquirer/acquire_lock/log_visitor.rbs +12 -6
- data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rbs +74 -7
- data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +4 -2
- data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +3 -0
- data/sig/redis_queued_locks/acquirer/acquire_lock/yield_with_expire/log_visitor.rbs +4 -2
- data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +19 -1
- data/sig/redis_queued_locks/acquirer/lock_info.rbs +9 -1
- data/sig/redis_queued_locks/acquirer/locks.rbs +10 -1
- data/sig/redis_queued_locks/acquirer/queue_info.rbs +2 -4
- data/sig/redis_queued_locks/acquirer/queues.rbs +1 -1
- data/sig/redis_queued_locks/acquirer/release_lock.rbs +8 -1
- data/sig/redis_queued_locks/acquirer/release_read_lock.rbs +44 -0
- data/sig/redis_queued_locks/client.rbs +22 -2
- data/sig/redis_queued_locks/resource.rbs +9 -2
- data/sig/redis_queued_locks/swarm/zombie_info.rbs +5 -0
- metadata +4 -1
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,67 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.18.0] - 2026-10-11
|
|
4
|
+
### Added
|
|
5
|
+
- Brand new feature - **Read/Write locks**: `read_write_mode:` attribute of `#lock`/`#lock!`/`#lock_series`/`#lock_series!` (`:write` by default):
|
|
6
|
+
- `:write` - exclusive lock (the classic RQL lock): waits for the write lock and for all read locks;
|
|
7
|
+
- `:read` - shared lock: waits for the write lock only (read locks of different acquirers work in parallel);
|
|
8
|
+
- `:queued` access strategy orders read and write lock requests in FIFO between modes (writers are not starved by readers);
|
|
9
|
+
- `:random` access strategy ignores lock queues (a continuous flow of readers can starve writers);
|
|
10
|
+
- read locks are stored in `rql:lock_readers:<lock_name>` (`ZSET`: acquirer id => expiration time in Redis time),
|
|
11
|
+
read lock requests - in `rql:lock_read_queue:<lock_name>`, read lock data - in `rql:lock_reader:<lock_name>:<acquirer_id>`
|
|
12
|
+
(the same structure as the write lock: acquirer, host, timestamp, ttl, `:meta`, reentrant lock data);
|
|
13
|
+
write locks and write lock requests use the classic keys;
|
|
14
|
+
- mutual exclusion is based on optimistic transactions (`WATCH`/`MULTI`, no Lua): readers watch the write lock key,
|
|
15
|
+
writers watch the write lock key and the read locks key (readers never invalidate transactions of each other);
|
|
16
|
+
- reentrant locks: read lock request under the obtained lock works as a reentrant lock of the obtained lock;
|
|
17
|
+
read-to-write lock upgrade fails with `RedisQueuedLocks::ConflictLockObtainError` (`:conflict_lock_upgrade`)
|
|
18
|
+
for `:work_through`/`:extendable_work_through` conflict strategies;
|
|
19
|
+
- `#unlock`, `#clear_locks`, `#clear_locks_of`, `#clear_current_locks`, `#clear_dead_requests`, `#flush_zombies`,
|
|
20
|
+
`#zombie_locks`, `#zombie_acquirers`, `#zombies_info`, `#locked?`, `#queued?`, `#lock_info`, `#queue_info`,
|
|
21
|
+
`#locks`, `#locks_info`, `#queues`, `#queues_info` support read locks and read lock requests;
|
|
22
|
+
- logs: `rw_mode` in the lock logs and in the lock series logs (`start_lock_series_obtaining`, `lock_series_obtained`,
|
|
23
|
+
`expire_lock_series`: the mode of the series); new try-lock log events: `exit__write_request_ahead`, `exit__read_request_ahead`,
|
|
24
|
+
`exit__read_lock_still_obtained`, `single_process_lock_conflict__lock_upgrade`;
|
|
25
|
+
- instrumentation: `rw_mode` in `lock_obtained`, `reentrant_lock_obtained`, `extendable_reentrant_lock_obtained`,
|
|
26
|
+
`lock_hold_and_release`, `reentrant_lock_hold_completes` payloads and in the lock series payloads
|
|
27
|
+
(`lock_series_obtained`, `lock_series_hold_and_release`: the mode of the series);
|
|
28
|
+
- `:meta` for read locks (stored in the read lock data of each reader, returned by `#lock_info`/`#locks_info`);
|
|
29
|
+
- `#lock_series` with `read_write_mode: :read` (PoC): a series of read locks; only read locks obtained by the series
|
|
30
|
+
are released (reentrant read locks of the outer logic are kept);
|
|
31
|
+
- `#unlock_read` (aka `#release_read_lock`): releases the read lock of the current acquirer only
|
|
32
|
+
(new instrumentation event: `redis_queued_locks.explicit_read_lock_release`);
|
|
33
|
+
- `#extend_lock_ttl`: `read_write_mode:` (`:write` by default) and `identity:` options; `:read` mode extends the read lock
|
|
34
|
+
of the current acquirer only (an expired read lock or a read lock replaced by a writer is not extended);
|
|
35
|
+
- `#extend_lock_ttl`: `all_read_locks:` option (`false` by default, `:read` mode only, ignored in `:write` mode):
|
|
36
|
+
extends all live read locks of the lock (expired read locks are not revived);
|
|
37
|
+
- RBS: `read_write_mode` is typed as `:read | :write`;
|
|
38
|
+
- `#queue_info` / `#queues_info`: each lock request has `"rw_mode"` (`"write"` / `"read"`);
|
|
39
|
+
- `#lock_info` / `#locks_info`: write lock info has `"rw_mode" => "write"` (read locks info - `"rw_mode" => "read"`);
|
|
40
|
+
- `:meta`: `"rw_mode"` is a reserved lock data key (`RedisQueuedLocks::ArgumentError`), for write and read locks and lock series;
|
|
41
|
+
- **all processes should use the RQL version with read/write locks** before read locks are used
|
|
42
|
+
(write locks of older RQL versions do not check read locks);
|
|
43
|
+
### Changed
|
|
44
|
+
- `lock_series_obtained` instrumentation event (`#lock_series`, PoC): `:ts` is an epoch timestamp (`Float`)
|
|
45
|
+
like in all other instrumentation events (previously a `Time#to_s` string);
|
|
46
|
+
- **Breaking**: `#extend_lock_ttl` success result is `{ ok: true, result: { extended_locks_count: Integer } }`
|
|
47
|
+
instead of `{ ok: true, result: :ttl_extended }` (the number of locks whose TTL is extended: `1` for the write lock
|
|
48
|
+
and for the own read lock, the number of extended read locks for `all_read_locks: true`);
|
|
49
|
+
the failure result is unchanged (`{ ok: false, result: :async_expire_or_no_lock }`);
|
|
50
|
+
- Lock acquirement attempt: the lock state is fetched in one pipelined round trip; lock data for
|
|
51
|
+
`exit__no_first`/`exit__lock_still_obtained` try-lock logs is extracted only when these logs are enabled
|
|
52
|
+
(previously `HGETALL` was called on every unsuccessful attempt);
|
|
53
|
+
### Fixed
|
|
54
|
+
- `dequeue_from_lock_queue` log: unclosed quote in the `acs_strat` value;
|
|
55
|
+
- `extendable_reentrant_lock_obtained` log: the host identifier is logged as `hst_id` (was `host_id`), like in all other logs;
|
|
56
|
+
- `start_lock_series_obtaining` log: missing space between the `lock_keys` and `queue_ttl` values;
|
|
57
|
+
- `lock_series` PoC:
|
|
58
|
+
- locks obtained by the failed series are released when the series fails without an exception
|
|
59
|
+
(`raise_errors: false`); previously they lived till their TTL;
|
|
60
|
+
- reentrant locks of the series (already obtained by the outer logic of the same acquirer) are not released
|
|
61
|
+
by the series anymore (previously the outer logic continued to work without its lock);
|
|
62
|
+
- the series releases only write locks that are still owned by the current acquirer (compare-and-delete):
|
|
63
|
+
a lock expired during the block and obtained by another acquirer is not deleted anymore;
|
|
64
|
+
|
|
3
65
|
## [1.17.0] - 2026-10-04
|
|
4
66
|
### Changed
|
|
5
67
|
- [**Breaking**] Minimal Ruby Version - **4.0** (previously - **3.3**):
|
data/CLAUDE.md
CHANGED
|
@@ -12,7 +12,8 @@ Full details (config keys, key layout, events, errors, stack, CI): `.claude/proj
|
|
|
12
12
|
- `client.rb`: public API facade; fills defaults from `config[...]`, delegates to `Acquirer::*`.
|
|
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
|
-
- Locking: `ZADD NX` into
|
|
15
|
+
- Locking: `ZADD NX` into the request queue, then WATCH/MULTI; Lua only for write TTL extension/decrease.
|
|
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".
|
|
16
17
|
- Strategies: `access_strategy` `:queued`|`:random`; `conflict_strategy` `:wait_for_lock`|`:work_through`|`:extendable_work_through`|`:dead_locking`.
|
|
17
18
|
- `resource.rb`: all key names and acquirer/host IDs. `config.rb`: `setting`/`validate` DSL, read as `config['a.b']`.
|
|
18
19
|
- `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.
|
|
@@ -20,12 +21,16 @@ Full details (config keys, key layout, events, errors, stack, CI): `.claude/proj
|
|
|
20
21
|
- `sig/`: RBS mirror of `lib/`. `spec/redis_queued_locks_spec.rb`: single integration spec.
|
|
21
22
|
|
|
22
23
|
## Rules
|
|
23
|
-
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).
|
|
24
|
+
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).
|
|
24
25
|
- 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.
|
|
25
26
|
- Update the matching `sig/*.rbs` for every `lib/` change; keep Steep green.
|
|
26
27
|
- Keep `# frozen_string_literal: true` and YARD `@api`/`@since`/`@version` tags.
|
|
27
28
|
- `{ 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`).
|
|
28
29
|
- New operation: `Acquirer::*` module, thin `Client` method (+ `!` variant), RBS, spec.
|
|
30
|
+
- 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`.
|
|
29
31
|
- New option: `setting` (+ `validate`) in `config.rb`.
|
|
30
32
|
- 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 and a recap of the changes; an explicit "commit" request needs no extra question; never on `master`, never push.
|
|
34
|
+
- Before implementing, read the relevant sections of `.claude/project-overview.md` (key layout, lock flow, "Read/Write locks", events) and keep their invariants. In the same change, update the overview and the affected `.claude/rules/*.md` for every new/changed key, option, result shape, event, algorithm step or limitation (and README/CHANGELOG `[Unreleased]` for user-visible behavior). New or changed log keys / instrumentation payload keys (visitors or inline `notify`) also go to README `## Logging` / `### Instrumentation Events` (see `visitors.md`).
|
|
35
|
+
- Lock-touching code is read/write-aware: any operation over locks, queues, zombies or lock info must cover the read family (`rql:lock_readers:*`, `rql:lock_read_queue:*`, `rql:lock_reader:*`) next to `rql:lock:*`/`rql:lock_queue:*`. Mutual exclusion comes only from WATCH/MULTI on lock state (no Lua in the RW acquisition path unless asked); queues never guarantee safety.
|
|
31
36
|
- Known debt: rspec-retry, disabled coverage minimum, runtime type-check CI uses `--failure-exit-code=0`.
|