Skip to content

Commit fa9be7b

Browse files
committed
fix(locks): handle retry_interval=0 without jitter errors
1 parent 28ed059 commit fa9be7b

File tree

4 files changed

+29
-9
lines changed

4 files changed

+29
-9
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1919
removal of the default transaction implementation from Nebulex core, allowing
2020
adapters to provide implementations tailored to their specific needs.
2121
[#7](https://github.com/elixir-nebulex/nebulex_local/issues/7).
22+
- [Nebulex.Locks] Improved retry behavior when `:retry_interval` is `0` by
23+
handling immediate retries without jitter. This prevents errors during lock
24+
acquisition retries and keeps zero-delay retry strategies working as expected.
2225

2326
## [v3.0.0-rc.2](https://github.com/elixir-nebulex/nebulex_local/tree/v3.0.0-rc.2) (2025-12-07)
2427
> [Full Changelog](https://github.com/elixir-nebulex/nebulex_local/compare/v3.0.0-rc.1...v3.0.0-rc.2)

lib/nebulex/locks.ex

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,10 @@ defmodule Nebulex.Locks do
252252
This function is typically called in an `after` block to ensure
253253
locks are released even if an exception occurs.
254254
255+
This API assumes trusted callers: it does not verify lock ownership
256+
when deleting lock entries. Releasing keys not owned by the current
257+
process is considered undefined behavior.
258+
255259
The first parameter can be either a named table (atom) or a table reference.
256260
257261
## Examples
@@ -474,8 +478,12 @@ defmodule Nebulex.Locks do
474478
end
475479
end
476480

477-
# Sleep with random jitter to prevent thundering herd (integer or :infinity)
478-
defp sleep_with_jitter(interval, _attempt) do
481+
# Sleep with random jitter to prevent thundering herd (fixed interval)
482+
defp sleep_with_jitter(0, _attempt) do
483+
:ok
484+
end
485+
486+
defp sleep_with_jitter(interval, _attempt) when is_integer(interval) and interval > 0 do
479487
jitter = :rand.uniform(interval)
480488
sleep_time = interval + jitter
481489

lib/nebulex/locks/options.ex

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,20 +85,18 @@ defmodule Nebulex.Locks.Options do
8585
"""
8686
],
8787
retry_interval: [
88-
type: {:or, [:timeout, {:fun, 1}]},
89-
type_doc: "`t:timeout/0` | `(attempt :: non_neg_integer() -> timeout())`",
88+
type: {:or, [:non_neg_integer, {:fun, 1}]},
89+
type_doc: "`t:non_neg_integer/0` | `(attempt :: non_neg_integer() -> non_neg_integer())`",
9090
required: false,
9191
default: 10,
9292
doc: """
9393
The time in milliseconds to wait between lock acquisition retries.
9494
9595
Can be either:
9696
97-
* A timeout value (`non_neg_integer()` or `:infinity`) - A fixed
98-
interval. When using a non-negative integer, a small random jitter is
99-
added to prevent thundering herd issues. The value `:infinity` is
100-
accepted for type compatibility but not recommended (would block
101-
indefinitely).
97+
* A timeout value (`non_neg_integer()`) - A fixed interval. When using
98+
a non-negative integer, a small random jitter is added to prevent
99+
thundering herd issues.
102100
* An anonymous function - Receives the current attempt number
103101
(0-indexed) and must return a non-negative integer representing the
104102
interval in milliseconds. No jitter is added, giving you full control

test/nebulex/locks_test.exs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,17 @@ defmodule Nebulex.LocksTest do
253253

254254
assert Locks.release(table, [:zero_retry_key]) == :ok
255255
end
256+
257+
test "retry_interval zero performs immediate retries without crashing", %{table: table} do
258+
# First process locks a key
259+
assert Locks.acquire(table, [:zero_interval_key]) == :ok
260+
261+
# Integer retry_interval: 0 should not crash and should timeout quickly
262+
assert Locks.acquire(table, [:zero_interval_key], retries: 2, retry_interval: 0) ==
263+
{:error, :timeout}
264+
265+
assert Locks.release(table, [:zero_interval_key]) == :ok
266+
end
256267
end
257268

258269
describe "periodic cleanup" do

0 commit comments

Comments
 (0)