redis_queued_locks 1.23.0 → 1.23.1
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 +2 -2
- data/.claude/rules/acquirer.md +1 -0
- data/CHANGELOG.md +6 -0
- data/README.md +12 -7
- data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +29 -12
- data/lib/redis_queued_locks/acquirer/acquire_lock.rb +11 -2
- data/lib/redis_queued_locks/acquirer/lock_series.rb +11 -2
- data/lib/redis_queued_locks/errors.rb +8 -0
- data/lib/redis_queued_locks/version.rb +2 -2
- data/sig/redis_queued_locks/errors.rbs +6 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ad3786d20407687dc7cfdb06956fecec429c199c4fb50d984af30afa15899352
|
|
4
|
+
data.tar.gz: cd71e3b222e1267ddbb29b34c2f2de66be834d9100a97d86b3cb3e3fc952f87b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 44dff1c6e0b2321b75cea2efea7094ffa47c88e8a0e8095e49d030f823048ff0834916030feba524ca354f3559ef97afc5d803c3fe41b88017ff6751bf9c479d
|
|
7
|
+
data.tar.gz: 73ad1a2679f8c84b5672e95036c06831a415c02878683ac886ac752552352c2d7d1d444fc34e6daf6f0eeef4e21bce02d925b5c4821cdb7df0cae834e53de022
|
data/.claude/project-overview.md
CHANGED
|
@@ -174,7 +174,7 @@ AcquireLock Step 0.1 / LockSeries Step 0.1 name -> Integer, range -
|
|
|
174
174
|
| `WithAcqTimeout` | Global acquisition timeout (`timeout`). |
|
|
175
175
|
| `DelayExecution` | Retry delay + jitter between attempts. |
|
|
176
176
|
| `DequeueFromLockQueue` | Removes the acquirer from the queue of the requested mode on timeout/failure. |
|
|
177
|
-
| `YieldExpire` | Runs the user block (optionally `timed`), then releases/expires the lock of the held mode (write: `EXPIRE 0`; read: `ZREM` own reader + `DEL` own read lock data; extendable reentrant: decrease). Known limitation (kept by the user's decision, documented in README): the write release and the write ttl decrease have no owner check, so a block longer than the ttl can delete/shorten the lock of another acquirer. |
|
|
177
|
+
| `YieldExpire` | Runs the user block (optionally `timed`: budget = `ttl` - time from the start of the successful attempt - `REDIS_TIMESHIFT_ERROR`, always within the lock lifetime; no budget left => the block is not invoked, `LockTtlTimeBudgetIsSpentError`; the budget exceeded => `TimedBlockExceedsLockTtlError`; `Timeout.timeout(0)` means no timeout; a long `ensure` of the block, `rescue Exception` and non-interruptible code can still outlive the lock), then releases/expires the lock of the held mode (write: `EXPIRE 0`; read: `ZREM` own reader + `DEL` own read lock data; extendable reentrant: decrease). Known limitation (kept by the user's decision, documented in README): the write release and the write ttl decrease have no owner check, so a block longer than the ttl can delete/shorten the lock of another acquirer. |
|
|
178
178
|
| `LogVisitor` / `InstrVisitor` | One method per lifecycle event for logs and instrumentation. |
|
|
179
179
|
|
|
180
180
|
### Lock series (`acquirer/lock_series.rb`)
|
|
@@ -483,7 +483,7 @@ log event: `dead_request_removed` (`dead_acq_id`, `dead_priority`: a dead blocki
|
|
|
483
483
|
|
|
484
484
|
Global base errors per branch (`logic.md#15`): `RedisQueuedLocks::Error < StandardError` (the main
|
|
485
485
|
hierarchy: `LockAlreadyObtainedError`, `LockAcquirementTimeoutError`, `LockAcquirementRetryLimitError`,
|
|
486
|
-
`TimedLockTimeoutError`, `ConflictLockObtainError`, `SwarmError`, `ConfigError` (`ConfigNotFoundError`,
|
|
486
|
+
`TimedLockTimeoutError` (`LockTtlTimeBudgetIsSpentError`, `TimedBlockExceedsLockTtlError`), `ConflictLockObtainError`, `SwarmError`, `ConfigError` (`ConfigNotFoundError`,
|
|
487
487
|
`ConfigValidationError`)), `RedisQueuedLocks::ArgumentError < ::ArgumentError` (`SwarmArgumentError`),
|
|
488
488
|
`RedisQueuedLocks::Exception < ::Exception` (`@api private`, super exceptional cases only: the internal
|
|
489
489
|
`*IntermediateTimeoutError`s of `Timeout.timeout` for `timeout` and `timed`; like `Timeout::ExitException`,
|
data/.claude/rules/acquirer.md
CHANGED
|
@@ -65,6 +65,7 @@ paths:
|
|
|
65
65
|
- `TryToLock` attempt (both modes): `multi(watch:)` (write: `[lock_key, lock_readers_key]`, read: `[lock_key]`) → one pipelined state read (`HGET acq_id`, `TIME`, own reader score, longest reader: write requests only, the write-only tail of the pipeline) → same-process conflict detection (`sp_conflict_rw_mode` = held `:write`/`:read`) → the lock request queueing in one pipelined round trip (`queue_lock_request`: prune dead requests of the request's priority band in **both** queues (before `ZADD NX`: the old dead position of a requeued request is dropped before its new position is added; the reverse order keeps the old position with `NX` and drops it: the request is missing in the queue for the attempt) → `ZADD NX` into the queue of the requested mode (the position is shifted by the priority band) → queue heads of both queues (`WITHSCORES`) → (the enabled lock index only) request queue `PEXPIRE NX`+`GT` → lock index `ZADD GT` → lock index pruning (5 replies, 9 with the lock index; new commands go to the end, replies after the queue heads are not read); eager queueing (`:wait_for_lock`/`:dead_locking` without `fail_fast`) sends these commands in the state pipeline right after its head, so `lock {}` = 4 round trips; the queueing log events are still emitted in the classic branch in the same order) → Step 2.2 (priorities, `:queued`, also in the pre-check, before the obstacle decision): the dead blocking requests of other bands (the write queue head for writers and the earlier opposite-mode request, decoded from their scores by `dead_request_priority`) are dropped with their bands in both queues and both heads are re-read in one more round trip (`drop_dead_requests_ahead`: one or two bands per round trip, the heads are read from the end of the pipeline, up to `MAX_DEAD_REQUEST_REMOVALS` round trips, `try_lock.dead_request_removed` log per dropped request, written by the caller (`log_dropped_requests`) after the queueing events; no round trip while the blocking requests are alive; a live write queue head stops the removal) (`:queued`: write must be first in its queue; any mode waits for an earlier `(score, acq_id)` request of the opposite mode) → lock state (write held → `:lock_is_still_acquired`; live readers for write / own live read for read → `:read_lock_is_still_acquired`) → MULTI (write: `ZREM` + `HSET` + `PEXPIRE` (+ lock index `ZADD GT`); read: `ZREM` + prune registry + `ZADD expiry` + registry `PEXPIRE NX`+`GT` + reader data `DEL`+`HSET`+`PEXPIRE` (+ lock index `ZADD GT`)). With the enabled lock index the `:extendable_work_through` extension `MULTI` ends with the lock index `ZADD XX INCR` (write: 4 replies, 5 with the lock index: `result.size == (lock_index_enabled ? 5 : 4)`).
|
|
66
66
|
- The try result carries internal `rw_mode` (the **held** lock mode); `acquire_lock` stores it as `acq_process[:held_rw_mode]` and passes it to `yield_expire`, which releases/decreases the held lock. The public `lock` result shape does not include it.
|
|
67
67
|
- Read lock data (`rql:lock_reader:<name>:<acq_id>`) uses the write-lock field format without `acq_id`/`hst_id` (`ts`, `ini_ttl`, meta, `spc_*`: both ids are derived from the acquirer id, `LockInfo.read_lock_info`/`Locks.read_lock_info` put them first in each reader and still accept older data that contains them) and the read lock TTL; it is a data carrier only (writers never read it). Its key (`read_lock_key`) is built only for read lock requests (`nil` for write requests: they never hold read locks).
|
|
68
|
+
- `timed` budget (`AcquireLock`, `LockSeries`): `ttl - ttl_shift`, `ttl_shift` = time from `acq_process[:try_start_time]` (the start of the successful attempt: the ttl starts later: write: `EXEC`, read: `TIME` of the lock state read) + `REDIS_TIMESHIFT_ERROR` (a margin: it always shortens the budget); no budget left (`0.0`) => `yield_with_timeout` does not invoke the block (`Timeout.timeout(0)` means no timeout): `LockTtlTimeBudgetIsSpentError`; a block that exceeds the budget: `TimedBlockExceedsLockTtlError` (both are `TimedLockTimeoutError` subclasses).
|
|
68
69
|
- Timing via monotonic `clock_gettime` (microseconds); wall time only for `at:`/`ts` fields.
|
|
69
70
|
- **LockSeries** (`lock_series.rb`, `lock_series/try_to_lock_series.rb`, `lock_series/release_lock_series.rb`; overview "Lock series"):
|
|
70
71
|
- The attempt is the single lock attempt for N locks, built from the shared `TryToLock` helpers: lock state and queueing commands (`pipeline_lock_state`, `queue_lock_request`), decisions (`same_process_conflict_status`, `find_opposite_request_ahead`, `lock_obstacle`, `find_dead_requests_ahead`, `pipeline_dead_requests_ahead_removal`), obtaining and reentrant commands (`transact_*`). A change of these helpers applies to both attempts; a change of the single lock attempt flow is checked against `try_to_lock_series`/`precheck_lock_series` (two copies of the flow, not of the commands).
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.23.1] = 2026-11-01
|
|
4
|
+
### Added
|
|
5
|
+
- `timed: true` of `#lock`/`#lock_series`: separate errors of the failures, both are `TimedLockTimeoutError` subclasses: `LockTtlTimeBudgetIsSpentError` (the block was not invoked: no time left) and `TimedBlockExceedsLockTtlError` (the block exceeded the lock TTL);
|
|
6
|
+
### Fixed
|
|
7
|
+
- `timed: true` of `#lock`/`#lock_series`: in some cases the time budget of the block could have been a little bit longer than the lock lifetime (2ms);
|
|
8
|
+
|
|
3
9
|
## [1.23.0] - 2026-11-01
|
|
4
10
|
### Changed
|
|
5
11
|
- [**Breaking**] `#lock_series`/`#lock_series!` are no longer a proof of concept. The final feature design:
|
data/README.md
CHANGED
|
@@ -589,6 +589,11 @@ def lock(
|
|
|
589
589
|
- the block is interrupted by an internal `RedisQueuedLocks::Exception` (an `::Exception`, like `Timeout::ExitException` of Ruby's `Timeout`): `rescue => e`,
|
|
590
590
|
`rescue StandardError`, `rescue Timeout::Error` and `rescue nil` inside the block can not swallow it (only `rescue Exception`),
|
|
591
591
|
`ensure` of the block still runs;
|
|
592
|
+
- the block gets the lock's TTL minus the time from the start of the successful acquirement attempt and minus a 2 ms margin;
|
|
593
|
+
with no time left the block is not invoked (`RedisQueuedLocks::LockTtlTimeBudgetIsSpentError`);
|
|
594
|
+
- both failures have the common superclass `RedisQueuedLocks::TimedLockTimeoutError`: `RedisQueuedLocks::TimedBlockExceedsLockTtlError` (the block exceeded the time) and `RedisQueuedLocks::LockTtlTimeBudgetIsSpentError` (the block was not invoked);
|
|
595
|
+
- (limitation) `timed` can only interrupt the block: a long `ensure` of the block, `rescue Exception` in the block and the code that
|
|
596
|
+
can not be interrupted (a C extension holding the GVL) can outlive the lock;
|
|
592
597
|
- pre-configured in `config['is_timed_by_default']`;
|
|
593
598
|
- `false` by default;
|
|
594
599
|
- `retry_count` - (optional) `[Integer,NilClass]`
|
|
@@ -837,7 +842,7 @@ rql.lock("my_lock", ttl: 5_000, timed: true) { sleep(4) }
|
|
|
837
842
|
# => OK
|
|
838
843
|
|
|
839
844
|
rql.lock("my_lock", ttl: 5_000, timed: true) { sleep(6) }
|
|
840
|
-
# => fails with RedisQueuedLocks::TimedLockTimeoutError
|
|
845
|
+
# => fails with RedisQueuedLocks::TimedBlockExceedsLockTtlError (a RedisQueuedLocks::TimedLockTimeoutError)
|
|
841
846
|
```
|
|
842
847
|
|
|
843
848
|
- infinite lock obtaining (no retry limit, no timeout limit):
|
|
@@ -3528,6 +3533,12 @@ Detalized event semantics and payload structure:
|
|
|
3528
3533
|
<sup>\[[back to top](#table-of-contents)\]</sup>
|
|
3529
3534
|
|
|
3530
3535
|
- **Major**:
|
|
3536
|
+
- **Experimental feature**: (non-`timed` locks): per-ruby-block-holding-the-lock sidecar `Ractor` and `in progress queue` in RedisDB that will extend
|
|
3537
|
+
the acquired lock for long-running blocks of code (that invoked "under" the lock
|
|
3538
|
+
whose ttl may expire before the block execution completes). It makes sense for non-`timed` locks *only*;
|
|
3539
|
+
- think about "thread priority" configuration :thinking:;
|
|
3540
|
+
- add `hst_id` to all methods that works with queues info;
|
|
3541
|
+
- try to return the `fiber object id` to the lock host identifier (we cant use fiber object id cuz `ObjectSpace` has no access to the fiber object space after the any ractor object initialization)
|
|
3531
3542
|
- Swarm:
|
|
3532
3543
|
- circuit-breaker for long-living failures of your infrastructure inside the swarm elements and supervisor:
|
|
3533
3544
|
the supervisor will stop (for some period of time or while the some factor will return `true`)
|
|
@@ -3553,9 +3564,6 @@ Detalized event semantics and payload structure:
|
|
|
3553
3564
|
- an ability to return all insturmentation metrics from the `lock` invocation (and after block `yield`ing);
|
|
3554
3565
|
- think about a PG-like `stats`-data/table, that should be helpful for lock history acquirement analyzation :thinking: (suitable for cases when we want
|
|
3555
3566
|
to check if the lock was acquired by some different parallel process durign the some period of time in the past or not);
|
|
3556
|
-
- think about "thread priority" configuration :thinking:;
|
|
3557
|
-
- add `hst_id` to all methods that works with queues info;
|
|
3558
|
-
- try to return the `fiber object id` to the lock host identifier (we cant use fiber object id cuz `ObjectSpace` has no access to the fiber object space after the any ractor object initialization)
|
|
3559
3567
|
- named RQL's threads (`Thread#name`) and RQL's ractors (`Ractor#name`) in order to have an ability to find and work with RQL's threads and ractors outside of RQL logic (stop threads before process forking, for example);
|
|
3560
3568
|
- `#lock`/`#lock!` - `timeout:` option: support for granular periods (it supports only `seconds` at the moment, but we need `milliseconds`);
|
|
3561
3569
|
- Add `after timeout hook` ability for timeout errors with an ability to access the binding context (if it possibly) of the thread where it failed;
|
|
@@ -3567,9 +3575,6 @@ Detalized event semantics and payload structure:
|
|
|
3567
3575
|
- support for `Dragonfly` database backend (https://github.com/dragonflydb/dragonfly) (https://www.dragonflydb.io/);
|
|
3568
3576
|
- (_research_) GVL-isolation for lock acquirement logic (try to bypass Ruby's "context-switching" in order to prevent any time-based affects on lock-acquirement logic);
|
|
3569
3577
|
- Semantic error objects for unexpected Redis errors;
|
|
3570
|
-
- **Experimental feature**: (non-`timed` locks): per-ruby-block-holding-the-lock sidecar `Ractor` and `in progress queue` in RedisDB that will extend
|
|
3571
|
-
the acquired lock for long-running blocks of code (that invoked "under" the lock
|
|
3572
|
-
whose ttl may expire before the block execution completes). It makes sense for non-`timed` locks *only*;
|
|
3573
3578
|
- sized lock queues (with an ability of dynamically growing size);
|
|
3574
3579
|
- better code stylization (+ some refactorings);
|
|
3575
3580
|
- `RedisQueuedLocks::Acquirer::Try.try_to_lock` - detailed successful result analization;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.3.0
|
|
5
5
|
# @version 1.18.0
|
|
6
|
+
# rubocop:disable Metrics/ModuleLength
|
|
6
7
|
module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire
|
|
7
8
|
require_relative 'yield_expire/log_visitor'
|
|
8
9
|
|
|
@@ -158,21 +159,37 @@ module RedisQueuedLocks::Acquirer::AcquireLock::YieldExpire
|
|
|
158
159
|
# @return [Any]
|
|
159
160
|
#
|
|
160
161
|
# @raise [RedisQueuedLocks::TimedLockTimeoutError]
|
|
162
|
+
# @raise [RedisQueuedLocks::LockTtlTimeBudgetIsSpentError]
|
|
163
|
+
# @raise [RedisQueuedLocks::TimedBlockExceedsLockTtlError]
|
|
161
164
|
#
|
|
162
165
|
# @api private
|
|
163
166
|
# @since 1.3.0
|
|
164
|
-
# @version 1.
|
|
167
|
+
# @version 1.23.1
|
|
165
168
|
def yield_with_timeout(timeout, lock_key, lock_ttl, acquirer_id, host_id, meta, &block)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
169
|
+
# NOTE: no budget left => the block is not invoked (`Timeout.timeout(0)` means "no timeout")
|
|
170
|
+
if timeout == 0.0
|
|
171
|
+
raise(
|
|
172
|
+
RedisQueuedLocks::LockTtlTimeBudgetIsSpentError,
|
|
173
|
+
"Passed <timed> block of code was not invoked: the time budget of the lock TTL is spent " \
|
|
174
|
+
"(lock: \"#{lock_key}\", ttl: #{lock_ttl}, " \
|
|
175
|
+
"meta: #{meta ? meta.inspect : '<no-meta>'}, " \
|
|
176
|
+
"acq_id: \"#{acquirer_id}\", " \
|
|
177
|
+
"hst_id: \"#{host_id}\")"
|
|
178
|
+
)
|
|
179
|
+
else
|
|
180
|
+
begin
|
|
181
|
+
::Timeout.timeout(timeout, RedisQueuedLocks::TimedLockIntermediateTimeoutError, &block)
|
|
182
|
+
rescue RedisQueuedLocks::TimedLockIntermediateTimeoutError
|
|
183
|
+
raise(
|
|
184
|
+
RedisQueuedLocks::TimedBlockExceedsLockTtlError,
|
|
185
|
+
"Passed <timed> block of code exceeded the lock TTL " \
|
|
186
|
+
"(lock: \"#{lock_key}\", ttl: #{lock_ttl}, " \
|
|
187
|
+
"meta: #{meta ? meta.inspect : '<no-meta>'}, " \
|
|
188
|
+
"acq_id: \"#{acquirer_id}\", " \
|
|
189
|
+
"hst_id: \"#{host_id}\")"
|
|
190
|
+
)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
177
193
|
end
|
|
178
194
|
end
|
|
195
|
+
# rubocop:enable Metrics/ModuleLength
|
|
@@ -193,7 +193,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
193
193
|
#
|
|
194
194
|
# @api private
|
|
195
195
|
# @since 1.0.0
|
|
196
|
-
# @version 1.23.
|
|
196
|
+
# @version 1.23.1
|
|
197
197
|
def acquire_lock(
|
|
198
198
|
redis,
|
|
199
199
|
lock_name,
|
|
@@ -412,6 +412,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
412
412
|
tries: 0,
|
|
413
413
|
acquired: false,
|
|
414
414
|
result: nil,
|
|
415
|
+
try_start_time: nil,
|
|
415
416
|
acq_time: nil, # NOTE: in milliseconds
|
|
416
417
|
hold_time: nil, # NOTE: in milliseconds
|
|
417
418
|
rel_time: nil # NOTE: in milliseconds
|
|
@@ -472,6 +473,8 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
472
473
|
)
|
|
473
474
|
end
|
|
474
475
|
|
|
476
|
+
acq_process[:try_start_time] = clock_gettime
|
|
477
|
+
|
|
475
478
|
# NOTE: (steep ignorance) pattern matching is not supported in steep
|
|
476
479
|
# steep:ignore:start
|
|
477
480
|
try_to_lock(
|
|
@@ -662,8 +665,14 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
662
665
|
begin
|
|
663
666
|
yield_time = clock_gettime
|
|
664
667
|
|
|
668
|
+
# NOTE: the budget of a `timed` block (see `Resource::REDIS_TIMESHIFT_ERROR`):
|
|
669
|
+
# - the lock ttl starts inside the attempt (write: `EXEC` of the transaction,
|
|
670
|
+
# read: `TIME` of the lock state read), this moment is unknown on the client:
|
|
671
|
+
# the time is counted from the start of the successful attempt (it is
|
|
672
|
+
# earlier, the budget can only be shorter than the real lifetime of the lock);
|
|
673
|
+
# - the Redis time error is a margin: it decreases the budget;
|
|
665
674
|
ttl_shift = (
|
|
666
|
-
(yield_time - acq_process[:
|
|
675
|
+
(yield_time - acq_process[:try_start_time]) / 1_000.0 +
|
|
667
676
|
RedisQueuedLocks::Resource::REDIS_TIMESHIFT_ERROR
|
|
668
677
|
).ceil(2)
|
|
669
678
|
|
|
@@ -126,7 +126,7 @@ module RedisQueuedLocks::Acquirer::LockSeries
|
|
|
126
126
|
#
|
|
127
127
|
# @api private
|
|
128
128
|
# @since 1.23.0
|
|
129
|
-
# @version 1.23.
|
|
129
|
+
# @version 1.23.1
|
|
130
130
|
# rubocop:disable Metrics/MethodLength
|
|
131
131
|
def lock_series(
|
|
132
132
|
redis,
|
|
@@ -356,6 +356,7 @@ module RedisQueuedLocks::Acquirer::LockSeries
|
|
|
356
356
|
ts: nil, # NOTE: see `try_to_lock_series`
|
|
357
357
|
lock_processes: nil, # NOTE: see `try_to_lock_series`
|
|
358
358
|
held_rw_modes: nil, # NOTE: see `try_to_lock_series`
|
|
359
|
+
try_start_time: nil,
|
|
359
360
|
acq_time: nil, # NOTE: in milliseconds
|
|
360
361
|
acq_end_time: nil,
|
|
361
362
|
rel_ts: nil,
|
|
@@ -404,6 +405,8 @@ module RedisQueuedLocks::Acquirer::LockSeries
|
|
|
404
405
|
)
|
|
405
406
|
end
|
|
406
407
|
|
|
408
|
+
acq_process[:try_start_time] = clock_gettime
|
|
409
|
+
|
|
407
410
|
attempt = try_to_lock_series(
|
|
408
411
|
redis,
|
|
409
412
|
logger,
|
|
@@ -536,8 +539,14 @@ module RedisQueuedLocks::Acquirer::LockSeries
|
|
|
536
539
|
host_id, acq_process[:ts], acq_process[:acq_time], reentrant_lock_keys, instrument
|
|
537
540
|
)
|
|
538
541
|
|
|
542
|
+
# NOTE: the budget of a `timed` block (see `Resource::REDIS_TIMESHIFT_ERROR`):
|
|
543
|
+
# - the lock ttl starts inside the attempt (write: `EXEC` of the transaction,
|
|
544
|
+
# read: `TIME` of the lock state read), this moment is unknown on the client:
|
|
545
|
+
# the time is counted from the start of the successful attempt (it is
|
|
546
|
+
# earlier, the budget can only be shorter than the real lifetime of the lock);
|
|
547
|
+
# - the Redis time error is a margin: it decreases the budget;
|
|
539
548
|
ttl_shift = (
|
|
540
|
-
(clock_gettime - acq_process[:
|
|
549
|
+
(clock_gettime - acq_process[:try_start_time]) / 1_000.0 +
|
|
541
550
|
RedisQueuedLocks::Resource::REDIS_TIMESHIFT_ERROR
|
|
542
551
|
).ceil(2)
|
|
543
552
|
|
|
@@ -67,6 +67,14 @@ module RedisQueuedLocks
|
|
|
67
67
|
# @since 1.0.0
|
|
68
68
|
class TimedLockTimeoutError < Error; end
|
|
69
69
|
|
|
70
|
+
# @api public
|
|
71
|
+
# @since 1.23.1
|
|
72
|
+
class LockTtlTimeBudgetIsSpentError < TimedLockTimeoutError; end
|
|
73
|
+
|
|
74
|
+
# @api public
|
|
75
|
+
# @since 1.23.1
|
|
76
|
+
class TimedBlockExceedsLockTtlError < TimedLockTimeoutError; end
|
|
77
|
+
|
|
70
78
|
# @api public
|
|
71
79
|
# @since 1.3.0
|
|
72
80
|
class ConflictLockObtainError < Error; end
|
|
@@ -26,6 +26,12 @@ module RedisQueuedLocks
|
|
|
26
26
|
class TimedLockTimeoutError < Error
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
class LockTtlTimeBudgetIsSpentError < TimedLockTimeoutError
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class TimedBlockExceedsLockTtlError < TimedLockTimeoutError
|
|
33
|
+
end
|
|
34
|
+
|
|
29
35
|
class ConflictLockObtainError < Error
|
|
30
36
|
end
|
|
31
37
|
|