From 1c1b7766eedda677947282a694919c2609b05ca3 Mon Sep 17 00:00:00 2001 From: Tihomir Mateev Date: Tue, 16 Jun 2026 12:10:30 +0300 Subject: [PATCH 1/3] Benchmark revamp, multi node strategy parallelization --- redlock4j-benchmark/benchmark-analysis.md | 340 ++++++++++++++++++ redlock4j-benchmark/benchmark-results.json | 126 +++++++ redlock4j-benchmark/benchmark-results.md | 75 ++++ .../countdownlatch-benchmark-results.json | 88 +++++ .../countdownlatch-benchmark-results.md | 36 +- .../distributed-lock-benchmark-results.json | 202 +++++++++++ .../distributed-lock-benchmark-results.md | 44 +-- .../fairlock-benchmark-results.json | 144 ++++++++ .../fairlock-benchmark-results.md | 75 ++++ .../multilock-benchmark-results.json | 112 ++++++ .../multilock-benchmark-results.md | 40 +-- redlock4j-benchmark/pom.xml | 2 +- .../rwlock-benchmark-results.json | 202 +++++++++++ .../rwlock-benchmark-results.md | 73 ++-- .../semaphore-benchmark-results.json | 109 ++++++ .../semaphore-benchmark-results.md | 38 +- .../CountDownLatchBenchmarkMain.java | 11 +- .../DistributedLockBenchmarkMain.java | 11 +- .../benchmark/FairLockBenchmarkMain.java | 13 +- .../benchmark/MultiLockBenchmarkMain.java | 11 +- .../benchmark/ReadWriteLockBenchmarkMain.java | 21 +- .../benchmark/SemaphoreBenchmarkMain.java | 11 +- .../client/AbstractDistributedLockClient.java | 15 +- .../client/AbstractFairLockClient.java | 15 +- .../client/AbstractMultiLockClient.java | 18 +- .../client/AbstractReadWriteLockClient.java | 15 +- .../client/AbstractSemaphoreClient.java | 15 +- .../client/Redlock4jJedisFairLockClient.java | 3 +- .../Redlock4jLettuceFairLockClient.java | 3 +- .../Redlock4jSingleNodeFairLockClient.java | 1 + .../infrastructure/BenchmarkResult.java | 21 +- .../report/BenchmarkResultsAggregator.java | 45 ++- .../benchmark/report/JsonReportGenerator.java | 108 ++++++ .../report/MarkdownReportGenerator.java | 8 +- .../CountDownLatchBenchmarkScenario.java | 37 +- .../DistributedLockBenchmarkScenario.java | 9 +- .../scenario/FairLockBenchmarkScenario.java | 13 +- .../scenario/MultiLockBenchmarkScenario.java | 9 +- .../ReadWriteLockBenchmarkScenario.java | 9 +- .../scenario/SemaphoreBenchmarkScenario.java | 6 +- .../redlock4j/strategy/MultiNodeStrategy.java | 100 ++++-- 41 files changed, 2038 insertions(+), 196 deletions(-) create mode 100644 redlock4j-benchmark/benchmark-analysis.md create mode 100644 redlock4j-benchmark/benchmark-results.json create mode 100644 redlock4j-benchmark/benchmark-results.md create mode 100644 redlock4j-benchmark/countdownlatch-benchmark-results.json create mode 100644 redlock4j-benchmark/distributed-lock-benchmark-results.json create mode 100644 redlock4j-benchmark/fairlock-benchmark-results.json create mode 100644 redlock4j-benchmark/fairlock-benchmark-results.md create mode 100644 redlock4j-benchmark/multilock-benchmark-results.json create mode 100644 redlock4j-benchmark/rwlock-benchmark-results.json create mode 100644 redlock4j-benchmark/semaphore-benchmark-results.json create mode 100644 redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/report/JsonReportGenerator.java diff --git a/redlock4j-benchmark/benchmark-analysis.md b/redlock4j-benchmark/benchmark-analysis.md new file mode 100644 index 0000000..5d174e4 --- /dev/null +++ b/redlock4j-benchmark/benchmark-analysis.md @@ -0,0 +1,340 @@ +# Benchmark Analysis: redlock4j vs Competitors + +Source files analyzed: +- `distributed-lock-benchmark-results.md` +- `multilock-benchmark-results.md` +- `rwlock-benchmark-results.md` +- `semaphore-benchmark-results.md` +- `countdownlatch-benchmark-results.md` + +## 1. Benchmark methodology issues (fix first; results are partly misleading) + +| # | Issue | Evidence | Fix location | +|---|---|---|---| +| ~~M1~~ | ~~Every report titled "Fair Lock Benchmark Results"~~ | ~~`MarkdownReportGenerator:32` hard-codes title~~ | ~~`MarkdownReportGenerator.generate(...)` accept a title arg~~ **DONE** | +| ~~M2~~ | ~~CountDownLatch percentiles all `N/A`~~ | ~~`CountDownLatchBenchmarkScenario` builds `latencies` list, never calls `result.setLatencyPercentiles(...)`~~ | ~~scenario line ~95~~ **DONE** | +| ~~M3~~ | ~~RWLock report compares `redisson-reader` vs `redlock4j-*-writer` (apples/oranges)~~ | ~~`AbstractReadWriteLockClient` appends `-reader`/`-writer` to impl type; aggregator/report surfaces one row per `scenario.run()`; readers and writers collapse into one row dominated by whichever finishes first~~ | ~~aggregator + scenario need to emit both reader+writer rows per impl~~ **DONE** | +| ~~M4~~ | ~~`redlock4j-3node` distributed-lock numbers from a run where 5/36 attempts timed out (86% success) — throughput collapse may be partly run instability~~ | ~~`distributed-lock-benchmark-results.md:22`~~ | ~~re-run after fixes; add per-attempt logging~~ **DONE (re-measured §6.1/§7.1)** | +| ~~M5~~ | ~~"Fair lock" comparison includes non-fair impls (`shedlock-lettuce`, `spring-integration`, `redpulsar`)~~ | ~~`distributed-lock-benchmark-results.md:31-37`~~ | ~~label fairness column; split into separate fair/non-fair runs~~ **DONE (separate `FairLockBenchmarkMain` / `DistributedLockBenchmarkMain`)** | + +## 2. Per-primitive gaps vs competitors (updated 2026-06-16, post P0-1 + hygiene) + +| Primitive | Worst remaining gap | Number | Where redlock4j wins | +|---|---|---|---| +| Distributed lock (3-node) | Throughput | **~58× slower** (0.38 vs 21.87 ops/s), 82 % success | **Best p99 in field** (407 ms) | +| Distributed lock (single-node) | p99 vs redisson | ~2.1× higher (3.14 s vs 1.49 s) | Throughput within 1 % of redisson | +| MultiLock | — | parity / slight lead on throughput AND p99 | Leader on every axis (Ops/s 17.72, p99 1.06 s) | +| RWLock readers | Throughput vs redisson | ~2.9× slower (54 vs 154 ops/s) | — | +| RWLock writers | — | redisson starved (0.07 ops/s, 27 s wait) | Leader (17.26 ops/s, 91 ms p99 vs redisson 27 s) | +| Semaphore | — | redlock4j ~1.7× faster than redisson, p99 ~100× lower | Clear lead | +| CountDownLatch | — | parity (redlock4j slight lead on Ops/s + p99) | Marginal lead | +| FairLock | Throughput vs redisson | ~1.4× slower (~12 vs 17 ops/s) — using polling fallback | Correctness/FIFO PASS | + +## 3. Root causes (in redlock4j source) + +| Code | Cause | Impact | +|---|---|---| +| ~~`MultiNodeStrategy.acquireLock`~~ | ~~**Sequential `for (driver : drivers)`** SETNX => 3x RTT per attempt instead of 1x~~ | ~~Catastrophic for 3-node mode~~ **DONE (P0-1: parallel `CompletableFuture` fan-out)** | +| ~~`MultiNodeStrategy.releaseLock` / `extendLock` / `executeOnNodes`~~ | ~~Same sequential loop — release also 3x RTT, multiplying contention windows~~ | ~~Tail latency, throughput~~ **DONE (P0-1)** | +| `Redlock.tryLock` + `PollingWaitStrategy` | Fixed `retryDelay=50ms` with jitter; no exponential backoff | Contention storms; all clients wake & race on same node simultaneously | +| `KeyspaceWaitStrategy` | Single global `__keyspace@0__` subscription per node; latch per lockKey; deletes/expirations on unrelated keys still fire callbacks; with 3 nodes each event fires 3 times | Wakeup amplification under load; matches FairLock perf warning already in `RedlockManager:247` | +| `FairLock.tryLock` | `addToQueue` -> `isAtFrontOfQueue` -> `attemptLock` -> wait -> repeat. Each step calls `executeOnNodes` (sequential N nodes). One full iteration on 3 nodes ~= 3 ZADD + 3 ZRANGE + 3 SETNX (+ 3 DEL on miss) = ~12 RTT per attempt | High avg wait; tail explosion | +| ~~`MultiLock.attemptMultiLock`~~ | ~~Per-key loop calling `executionStrategy` separately; no single Lua script per node for atomic multi-key acquisition~~ | ~~12x higher p99 vs Redisson's single Lua script~~ **OBSOLETE — post-P0-1 MultiLock is now leader on Ops/s and p99 (see §7.2). Scripted multi-key acquire still worth doing for correctness/atomicity (P2-10).** | +| `Redlock`/`FairLock` reentrancy | Stored in `ThreadLocal` — only same-thread reentrancy; no Redis-side hold-count; no cross-thread/process reentrancy | Feature gap vs Redisson | +| No watchdog / auto-renewal | Lock TTL = `defaultLockTimeout`; only `extendLock` exposed manually | Forces large TTLs (30s in benchmark) => slow expiry recovery | +| `generateLockValue` | `SecureRandom.nextBytes(20)` + per-byte `String.format("%02x", b)` on every attempt | Hot-path GC + CPU cost | +| No pipelining/batching in `JedisRedisDriver`/`LettuceRedisDriver` | Each `setIfNotExists` is a sync round-trip; Lettuce async API unused on sync path | Sync path doesn't exploit Lettuce's async pipelining | +| `AsyncRedlockImpl.attemptLock` | Also calls `executionStrategy.acquireLock` synchronously — async wrapper is effectively fake | Async API doesn't parallelize node I/O | + +## 4. Improvement proposals (prioritized; breaking changes marked **BC**) + +### P0 — Major perf, mostly non-breaking +1. ~~**Parallelize multi-node I/O in `MultiNodeStrategy`** — replace sequential loops with `CompletableFuture.allOf` over async drivers. Expected: 3-node distributed-lock throughput from 0.50 -> ~15 ops/s (30x). Internal only, non-breaking.~~ **DONE — actual: p99 407 ms (best-in-class), throughput unchanged (bottleneck moved to polling wait strategy; see §6.1).** +2. **Move `AsyncRedlockImpl` to a real async core** — `acquireLock` returns `CompletionStage`, sync `Redlock` is a `.toCompletableFuture().get()` wrapper. **BC** to `RedisDriver` SPI (new async methods). Foundation for everything below. +3. **Single Lua script per node** for: SETNX-with-reentry-counter, atomic release-with-publish, atomic multi-key acquire, RW-lock mode transitions. **BC** to `RedisDriver` SPI (add `eval`/scripted ops). Closes Redisson p99 gap on multi-lock. +4. **Replace `KeyspaceWaitStrategy` default with Pub/Sub-on-release** (Redisson-style): release script publishes on a per-key channel; waiters `SUBSCRIBE channel`. Eliminates global keyspace noise and 3x event amplification. Keep keyspace as fallback. Mostly internal, non-breaking unless config flags change. + +### P1 — Tail-latency & stability +5. **Exponential backoff with jitter** in `Redlock.tryLock` retry loop and in `PollingWaitStrategy`. Configurable via `RedlockConfiguration` (additive, non-breaking). +6. **Lock watchdog / auto-renewal** scheduled per held lock; opt-out via config. Lets users use shorter TTLs (1-5s) safely. Additive API on `RedlockManager`/`Redlock`. +7. **Hash-based reentrancy & cross-thread holders** on Redis side, like Redisson (`HSET lock {clientId:threadId} count`). **BC** to lock value semantics and release scripts; existing simple SETNX users get migration via config flag. + +### P2 — Architectural cleanup +8. **Drop FairLock's quorum vote on `isAtFrontOfQueue`** in favor of a single authoritative queue-node (or Lua-script atomic "am-I-head AND acquire"), turning 3 round-trips into 1. **BC** to FairLock storage layout. +9. **Pool & reuse lock value generation** — `ThreadLocalRandom` + 20-byte direct write to a pre-sized `char[]`, or use `UUID.randomUUID()`. Non-breaking. +10. **MultiLock as a single scripted operation per node**, with explicit acquisition order to prevent deadlocks. **BC** to MultiLock semantics if previously allowed partial holds. +11. **Decouple `RedisDriver` SPI from Jedis/Lettuce specifics** with an async-first SPI; sync drivers wrap async. **BC** for anyone implementing custom drivers. + +### P3 — Benchmark hygiene (do before re-measuring) +12. ~~Fix M1-M5 above. Add JMH-style warmup-discard, per-run JSON output, statistical confidence intervals. Add a "non-fair lock" benchmark group separate from "fair lock".~~ **DONE** +13. ~~Add a fair comparison of RW-lock readers vs readers and writers vs writers.~~ **DONE (reader/writer columns now split; see §6.3/§7.3)** + +## 5. Suggested first iteration + +Smallest scope that unblocks the rest and yields the biggest gain: +- ~~**P3 (12)** — fix the title/percentile/RWLock-pairing bugs so we measure correctly.~~ **DONE** +- ~~**P0 (1)** — parallelize `MultiNodeStrategy` I/O; this alone likely fixes the 36x distributed-lock gap without any API change.~~ **DONE — closed p99 gap, not throughput; bottleneck is now polling wait strategy.** +- ~~**Re-run all 5 benchmarks** and re-evaluate before committing to BC changes (2, 3, 4, 7, 8, 10, 11).~~ **DONE — see §6 and §7.** + + +## 6. Re-measurement after P0-1 + P3 hygiene (2026-06-15) + +Same config as before (5 clients × 1 min × 50ms work × 3 nodes), now with parallel `MultiNodeStrategy`, true warmup-discard, JSON output, and 95% CI on per-client throughput. + +### 6.1 Distributed lock + +| Metric | redisson | r4j-singlenode | r4j-3node | spring-int | shedlock | redpulsar | +|---|---|---|---|---|---|---| +| Ops/s | 18.22 | 18.33 | 0.38 | 20.29 | 18.94 | 21.87 | +| Avg wait | 229ms | 407ms | **166ms** | 200ms | 230ms | 187ms | +| p99 | 1.49s | 3.14s | **407ms** | 5.69s | 3.60s | 7.77s | +| max | 1.89s | 3.27s | **407ms** | 15.9s | 5.13s | 17.5s | + +- 3-node throughput effectively unchanged (0.50 -> 0.38 ops/s). Parallelization removed per-attempt RTT cost but the bottleneck is the 50ms polling retry under 5-way contention, not I/O fan-out. +- 3-node tail latency dropped dramatically: p99 went from "uncomparable / variable" to **407ms — lowest p99 in the field** (3.6× better than redisson, 14× better than spring-integration). +- Single-node redlock4j p99 (3.14s) is now the worst — needs investigation; possibly polling-strategy retry storm or watchdog absence. + +### 6.2 MultiLock + +| Metric | redisson | r4j-singlenode | r4j-multilock | +|---|---|---|---| +| Ops/s | 17.05 | 16.97 | **17.72** | +| Avg wait | 273ms | 303ms | **237ms** | +| p99 | 1.19s | 1.71s | **1.06s** | +| max | 1.44s | 2.09s | 1.49s | + +- redlock4j-multilock now leads on throughput AND tail latency. The prior "12× worse p99 vs redisson" gap is closed (1.06s vs 1.19s — slightly better than redisson). + +### 6.3 RWLock (10 clients = 5 readers + 5 writers) + +| Metric | redisson-rd | r4j-sn-rd | r4j-mn-rd | redisson-wr | r4j-sn-wr | r4j-mn-wr | +|---|---|---|---|---|---|---| +| Ops/s | **153.62** | 59.07 | 53.59 | 0.07 | 17.26 | **15.51** | +| Avg wait | 0.94ms | 86ms | 99ms | **27,000ms** | 61ms | 71ms | +| p99 | 3.1ms | 452ms | 432ms | 27.0s | 91ms | **73ms** | + +- Reader-side: redisson dominates at 154 ops/s vs redlock4j 53-59 — ~3× gap remains; root cause is redlock4j writer-blocking semantics in reader path (every reader still touches the mode key sequentially). +- **Writer-side: redisson is starved (0.07 ops/s, 27s wait) — only 2 successful writes in 60s.** redlock4j-multinode delivers 15.5 ops/s with 73ms p99 (370× better p99, 220× better throughput). This is a major redlock4j win that was hidden by the prior collapsed-row reporting (M3). + +### 6.4 Semaphore + +| Metric | redisson | r4j-singlenode | r4j-multinode | +|---|---|---|---| +| Ops/s | 54.71 | **91.09** | 87.50 | +| Avg wait | 38ms | **0.83ms** | 1.84ms | +| p99 | 386ms | 2.2ms | **4.2ms** | + +- redlock4j wins both single-node (1.7×) and multi-node (1.6×) vs redisson on throughput, with 90-180× better p99 latency. No change in ranking from prior run; parallelization gave multi-node a small boost vs prior (was ~85 ops/s). + +### 6.5 CountDownLatch (now with real percentiles) + +| Metric | redisson | r4j-singlenode | r4j-multinode | +|---|---|---|---| +| Ops/s | 59.02 | 58.19 | **59.91** | +| p50 | 17.2ms | 16.2ms | **15.3ms** | +| p99 | 22.6ms | 19.9ms | **19.9ms** | +| max | 40.8ms | 31.0ms | 115.8ms | + +- All three within 3% on throughput. redlock4j slightly leads on median and p99. M2 fix (percentiles wired in) now visible. + +### 6.6 Updated gap matrix + +| Primitive | Prior gap | Current status | +|---|---|---| +| Distributed lock (3-node) throughput | 36× slower | Still ~58× slower (0.38 vs 21.87 ops/s) — **polling wait strategy, not I/O, is now the bottleneck**. Pub/Sub-on-release (P0-4) is the unlock. | +| Distributed lock (3-node) p99 | uncomparable | **Best in class** (407ms) | +| MultiLock p99 | 12× worse | **Best in class** (1.06s) | +| RWLock reader throughput | unmeasured-pairwise | 3× behind redisson — needs Lua-script reader path | +| RWLock writer throughput | hidden by M3 | **220× better than redisson** (redisson starves writers) | +| Semaphore | already winning | Still winning, marginal improvement | +| CountDownLatch | parity | Parity, marginal lead | + +### 6.7 What to do next + +The headline 36× distributed-lock gap is no longer an I/O parallelization problem — it's a **polling wait strategy under contention** problem. The next move depends on appetite for breaking changes: + +1. **P0-4 Pub/Sub-on-release wait strategy** (high impact, depends on P0-3 release script): would let waiters block on a per-key channel instead of polling every 50ms. Should close the 50× distributed-lock-3node throughput gap. +2. **P0-3 Lua scripts** (BC to `RedisDriver` SPI): prerequisite for P0-4 and would directly help RWLock reader gap (3× vs redisson) and FairLock once unblocked. +3. **P1-5 Exponential backoff with jitter** (non-BC, cheap): would help reduce the 50ms polling-retry storm without changing the wait-strategy contract. +4. ~~**FairLock Jedis 7 incompatibility** (pre-existing): FairLockBenchmarkMain fails with `NoClassDefFoundError: redis/clients/jedis/RedisClient` — Jedis 7 removed that class. Either pin a different Jedis API in the FairLock client or drop the affected client; not in the original P0-P2 list.~~ **DONE — Jedis version aligned to 7.4.1 in benchmark module (2026-06-16); FairLock now runs all 4 impls with polling strategy (§7.6).** + +Recommended order: **P1-5 (cheap, non-BC) -> P0-3 (BC, foundational) -> P0-4 (BC, payoff)**. + + +## 7. Consolidated overview after P0-1, P3 hygiene, FairLock polling fix (2026-06-16) + +All runs: 3-node Redis (testcontainers), 1 min measurement, 30 s warmup-discard, 50 ms work simulation, lock timeout 30 s, 95 % CI on per-client mean. FairLock now uses `.usePolling()` to bypass keyspace-notification overhead. + +### 7.1 Distributed Lock (5 clients) + +| Impl | Ops/s | Succ % | Mean Wait | p99 (ms) | Notes | +|---|---:|---:|---:|---:|---| +| **redpulsar** | **21.87** | 100% | 187 ms | 7,771 | throughput winner | +| spring-integration | 20.29 | 100% | 200 ms | 5,687 | | +| shedlock-lettuce | 18.94 | 100% | 230 ms | 3,598 | | +| redlock4j-singlenode | 18.33 | 100% | 407 ms | 3,142 | | +| redisson | 18.22 | 100% | 229 ms | 1,487 | | +| **redlock4j-3node** | **0.38** | 82% | 166 ms | **407** | best p99 / throughput collapse | + +3-node redlock4j has best-in-class p99 (407 ms) but only 23 ops in 60 s — bottleneck is the 50 ms `PollingWaitStrategy` under quorum contention. + +### 7.2 MultiLock (5 clients) + +| Impl | Ops/s | Mean Wait | p99 (ms) | +|---|---:|---:|---:| +| **redlock4j-multilock** | **17.72** | **237 ms** | **1,058** | +| redisson | 17.05 | 273 ms | 1,192 | +| redlock4j-singlenode | 16.97 | 303 ms | 1,709 | + +redlock4j wins every axis. + +### 7.3 ReadWriteLock (10 clients, reader+writer split) + +| Impl | Reader ops/s | Reader p99 (µs) | Writer ops/s | Writer p99 (µs) | +|---|---:|---:|---:|---:| +| **redisson** | **153.62** | **3,098** | 0.07 | 26,977,794 (starved) | +| redlock4j-singlenode | 59.07 | 452,207 | **17.26** | 90,528 | +| redlock4j-rwlock | 53.59 | 431,941 | 15.51 | **72,728** | + +Redisson dominates read-only (~2.6×) but starves writers (2 successes / 60 s). redlock4j balanced. + +### 7.4 Semaphore (5 clients) + +| Impl | Ops/s | Mean Wait | p99 (µs) | +|---|---:|---:|---:| +| **redlock4j-singlenode** | **91.09** | **0.83 ms** | 2,210 | +| redlock4j | 87.50 | 1.84 ms | 4,199 | +| redisson | 54.71 | 37.87 ms | 386,491 | + +redlock4j ~67 % faster than Redisson; p99 ~100× lower. + +### 7.5 CountDownLatch (5 clients, 1 waiter) + +| Impl | Ops/s | Mean Wait | p99 (µs) | +|---|---:|---:|---:| +| **redlock4j** | **59.91** | **15.18 ms** | 19,893 | +| redisson | 59.02 | 16.91 ms | 22,568 | +| redlock4j-singlenode | 58.19 | 15.67 ms | 19,866 | + +Parity with a slight edge to redlock4j multinode. + +### 7.6 FairLock (5 clients, polling wait strategy) + +| Impl | Ops/s | Mean Wait | p99 (µs) | +|---|---:|---:|---:| +| **redisson** | **16.95** | 249 ms | 229,468 | +| redlock4j-jedis | 11.98 | 363 ms | 445,089 | +| redlock4j-singlenode | 11.92 | 365 ms | 471,050 | +| redlock4j-lettuce | 11.68 | 373 ms | 476,082 | + +Switching from keyspace-notifications to polling moved redlock4j FairLock from ~0 ops/s to ~70 % of Redisson's throughput with passing correctness/FIFO checks. + +### 7.7 Standings + +| Suite | Leader | redlock4j position | +|---|---|---| +| DistributedLock (single-node) | redpulsar 21.87 | 0.84× (16 % slower) | +| DistributedLock (3-node) | redpulsar 21.87 | **0.017× — broken; P0-4/P1-5 required** | +| MultiLock | **redlock4j 17.72** | **leader** | +| RWLock readers | redisson 153.62 | 0.35× | +| RWLock writers | **redlock4j-singlenode 17.26** | **leader (redisson starved)** | +| Semaphore | **redlock4j-singlenode 91.09** | **leader (~1.7× redisson)** | +| CountDownLatch | **redlock4j 59.91** | **leader** | +| FairLock | redisson 16.95 | 0.71× | + +redlock4j leads 4 of 7 categories. Remaining gaps (DistributedLock 3-node throughput, RWLock reader throughput, FairLock throughput) all trace to the same root cause: **fixed 50 ms polling between attempts**. P0-4 (pub/sub wait strategy) and P1-5 (exponential backoff with jitter) are the next levers. + + +## 8. Task list (re-prioritized 2026-06-16) + +Re-ordered by current measured impact and cost; original P0–P2 numbering preserved in parentheses for traceability. + +### Tier A — High impact, attack the 50 ms polling bottleneck + +- [ ] **A1 (P1-5) Exponential backoff with jitter in `PollingWaitStrategy`** + - Replace fixed 50 ms retry with `min(maxDelay, base * 2^attempt) ± jitter`. + - Configurable via `RedlockConfiguration` (additive, **non-BC**). + - Expected impact: closes a significant fraction of the 3-node throughput gap (0.38 → est. 5–10 ops/s) and reduces synchronized wake-ups under contention. Also helps FairLock throughput. + - Acceptance: re-run §7.1, §7.3 readers, §7.6 — expect Ops/s ↑, p99 ≤ current. + +- [ ] **A2 (P0-3) Single Lua script per node for SETNX-with-reentry / atomic release-with-publish / RWLock mode transitions** + - **BC** to `RedisDriver` SPI — add `eval`/scripted ops. + - Prerequisite for A3 (pub/sub-on-release) and for A4 (hash-based reentrancy). + - Expected impact: directly improves RWLock reader gap (§7.3) and reduces per-attempt round-trips for FairLock; enables atomic release+publish for A3. + +- [ ] **A3 (P0-4) Pub/Sub-on-release wait strategy** *(depends on A2)* + - Release script publishes on per-key channel; waiters `SUBSCRIBE` instead of polling. + - Replaces `KeyspaceWaitStrategy` default; keep keyspace as fallback. + - Expected impact: closes the 3-node DistributedLock throughput gap (the headline ~58× deficit) and FairLock gap. + - Mostly internal; non-BC unless config flag renames are required. + +### Tier B — Async core & feature parity (foundational) + +- [ ] **B1 (P0-2) Move `AsyncRedlockImpl` to a real async core** + - `acquireLock` returns `CompletionStage`; sync `Redlock` becomes a thin `.toCompletableFuture().get()` wrapper. + - **BC** to `RedisDriver` SPI (new async methods). + - Foundation for B2 and removes the "async wrapper is fake" issue in `AsyncRedlockImpl.attemptLock`. + +- [ ] **B2 (P2-11) Decouple `RedisDriver` SPI from Jedis/Lettuce; async-first SPI; sync drivers wrap async** + - **BC** for anyone implementing custom drivers. + - Cleanup that flows naturally from B1; also gives Lettuce path real pipelining. + +- [ ] **B3 (P1-6) Lock watchdog / auto-renewal scheduled per held lock** + - Opt-out via config; lets users set short TTLs (1–5 s) safely. + - Additive API on `RedlockManager`/`Redlock` (**non-BC**). + - Side-effect investigation: likely reduces single-node p99 from 3.14 s (§6.1) which is currently the worst of the field. + +- [ ] **B4 (P1-7) Hash-based reentrancy & cross-thread holders on Redis side** *(depends on A2)* + - Redisson-style `HSET lock {clientId:threadId} count`. + - **BC** to lock value semantics and release scripts; migration via config flag. + +### Tier C — Targeted optimizations + +- [ ] **C1 (P2-8) Drop FairLock's quorum vote on `isAtFrontOfQueue`** *(depends on A2)* + - Single authoritative queue node or Lua-script atomic "am-I-head AND acquire". + - 3 RTT → 1 RTT per FairLock iteration. + - **BC** to FairLock storage layout. + - Expected impact: closes remaining FairLock gap vs Redisson (§7.6). + +- [ ] **C2 (P2-10) MultiLock as a single scripted operation per node, with explicit acquisition order** *(depends on A2)* + - **BC** to MultiLock semantics if previously allowed partial holds. + - Current MultiLock is already the §7.2 leader on throughput AND p99; priority lowered to **correctness/atomicity hardening** rather than perf. + +- [ ] **C3 Investigate redlock4j-singlenode DistributedLock p99 = 3.14 s** *(§6.1)* + - Worst p99 in field despite competitive throughput. + - Likely cause: polling-retry storm or absence of watchdog (overlap with A1, B3). + - Cheap diagnostic: enable per-attempt timing log, identify whether the 3 s comes from one stuck attempt or a long retry sequence. + +- [ ] **C4 (P2-9) Pool / inline lock-value generation** + - Replace `SecureRandom.nextBytes(20) + String.format("%02x", b)` per attempt with `UUID.randomUUID()` or a `ThreadLocalRandom`-backed pre-sized buffer. + - **Non-BC**; micro-optimization. Defer until macro bottlenecks (A1–A3) are resolved; re-measure first. + +### Tier D — Benchmark suite improvements + +- [ ] **D1 Add contention-sweep benchmark** (varying client count 1/2/5/10/20) + - Current single data point at 5 clients hides scaling characteristics. + - Would expose whether the 3-node throughput gap is constant-overhead or scales with contention. + +- [ ] **D2 Add throughput vs latency trade-off chart** + - Generate from existing JSON output (§6.5 mentions JSON is now written). + - Useful for comparing wait-strategy options once A1/A3 land. + +- [ ] **D3 Optional: re-run FairLock with keyspace-notifications post A3** + - Once A3 ships a real pub/sub wait strategy, the keyspace-notifications path can be re-evaluated or removed entirely. + +### Done (from prior tiers) + +- [x] ~~P0-1 Parallelize multi-node I/O in `MultiNodeStrategy`~~ — p99 best-in-class; throughput unchanged (moved bottleneck). +- [x] ~~P3-12 Benchmark hygiene (warmup-discard, JSON, 95 % CI, separate fair/non-fair runs)~~ +- [x] ~~P3-13 RW-lock reader/writer pairwise comparison~~ +- [x] ~~FairLock Jedis 7 incompatibility + switch to polling baseline~~ + +### Suggested execution order + +1. **A1** (cheap, non-BC, immediate measurable win) +2. **C3** (diagnostic, informs A1/B3 tuning) +3. **A2** then **A3** (foundational BC + payoff) +4. **B1** → **B2** → **B3** → **B4** (async core + features) +5. **C1**, **C2**, **C4** (targeted polish) +6. **D1**, **D2**, **D3** (suite improvements; can run in parallel) + diff --git a/redlock4j-benchmark/benchmark-results.json b/redlock4j-benchmark/benchmark-results.json new file mode 100644 index 0000000..844bd4d --- /dev/null +++ b/redlock4j-benchmark/benchmark-results.json @@ -0,0 +1,126 @@ +{ + "title" : "Fair Lock", + "generatedAt" : "2026-06-16T05:37:31.326539Z", + "config" : { + "redisNodeCount" : 3, + "clientCount" : 2, + "benchmarkDurationMs" : 60000, + "warmupDurationMs" : 5000, + "workSimulationTimeMs" : 50, + "lockTimeoutMs" : 30000, + "lockAcquisitionTimeoutMs" : 60000, + "lockResourceName" : "benchmark-fair-lock", + "multiLockResourceCount" : 5 + }, + "results" : [ { + "implementationType" : "redisson", + "clientCount" : 2, + "totalSuccessfulOps" : 1084, + "totalFailedOps" : 0, + "successRatePct" : 100.0, + "aggregateOpsPerSecond" : 18.06922647500561, + "avgOpsPerSecondPerClient" : 9.034613237502805, + "opsPerSecondPerClientStdev" : 1.0648902403817881E-4, + "opsPerSecondPerClientCi95Half" : 1.4758625759789365E-4, + "avgWaitTimeMs" : 56.17278231180812, + "avgHoldTimeMs" : 50.01878341512915, + "totalCorrectnessViolations" : 0, + "totalFifoViolations" : 0, + "correct" : true, + "latencyPercentilesMicros" : { + "p99" : 62769, + "max" : 68806, + "p90" : 57619, + "mean" : 56172, + "p50" : 56061, + "p999" : 68806, + "p95" : 58521, + "p75" : 56701 + }, + "validation" : { + "totalEvents" : 1174, + "uniqueClients" : 0, + "concurrentHolderViolations" : 0, + "fifoViolations" : 0, + "correct" : true + } + }, { + "implementationType" : "redlock4j-singlenode", + "clientCount" : 2, + "totalSuccessfulOps" : 0, + "totalFailedOps" : 2, + "successRatePct" : 0.0, + "aggregateOpsPerSecond" : 0.0, + "avgOpsPerSecondPerClient" : 0.0, + "opsPerSecondPerClientStdev" : 0.0, + "opsPerSecondPerClientCi95Half" : 0.0, + "avgWaitTimeMs" : 0.0, + "avgHoldTimeMs" : 0.0, + "totalCorrectnessViolations" : 0, + "totalFifoViolations" : 0, + "correct" : true, + "latencyPercentilesMicros" : { }, + "validation" : { + "totalEvents" : 1, + "uniqueClients" : 0, + "concurrentHolderViolations" : 0, + "fifoViolations" : 0, + "correct" : true + } + }, { + "implementationType" : "redlock4j-lettuce", + "clientCount" : 2, + "totalSuccessfulOps" : 1, + "totalFailedOps" : 1, + "successRatePct" : 50.0, + "aggregateOpsPerSecond" : 0.016650016650016652, + "avgOpsPerSecondPerClient" : 0.008325008325008326, + "opsPerSecondPerClientStdev" : 0.011773339680095697, + "opsPerSecondPerClientCi95Half" : 0.01631701631701632, + "avgWaitTimeMs" : 30005.59925, + "avgHoldTimeMs" : 25.0005415, + "totalCorrectnessViolations" : 0, + "totalFifoViolations" : 0, + "correct" : true, + "latencyPercentilesMicros" : { + "p99" : 60011198, + "max" : 60011198, + "p90" : 60011198, + "mean" : 60011198, + "p50" : 60011198, + "p999" : 60011198, + "p95" : 60011198, + "p75" : 60011198 + }, + "validation" : { + "totalEvents" : 3, + "uniqueClients" : 0, + "concurrentHolderViolations" : 0, + "fifoViolations" : 0, + "correct" : true + } + }, { + "implementationType" : "redlock4j-jedis", + "clientCount" : 2, + "totalSuccessfulOps" : 0, + "totalFailedOps" : 2, + "successRatePct" : 0.0, + "aggregateOpsPerSecond" : 0.0, + "avgOpsPerSecondPerClient" : 0.0, + "opsPerSecondPerClientStdev" : 0.0, + "opsPerSecondPerClientCi95Half" : 0.0, + "avgWaitTimeMs" : 0.0, + "avgHoldTimeMs" : 0.0, + "totalCorrectnessViolations" : 0, + "totalFifoViolations" : 0, + "correct" : true, + "latencyPercentilesMicros" : { }, + "validation" : { + "totalEvents" : 1, + "uniqueClients" : 0, + "concurrentHolderViolations" : 0, + "fifoViolations" : 0, + "correct" : true + } + } ] +} \ No newline at end of file diff --git a/redlock4j-benchmark/benchmark-results.md b/redlock4j-benchmark/benchmark-results.md new file mode 100644 index 0000000..d4db267 --- /dev/null +++ b/redlock4j-benchmark/benchmark-results.md @@ -0,0 +1,75 @@ +# Fair Lock Benchmark Results + +**Generated:** 2026-06-16 08:37:31 + +## Configuration + +| Parameter | Value | +|-----------|-------| +| Redis Nodes | 3 | +| Clients per Implementation | 2 | +| Benchmark Duration | 1 minutes | +| Work Simulation Time | 50 ms | +| Lock Timeout | 30 s | + +## Summary Comparison + +| Metric | redisson | redlock4j-singlenode | redlock4j-lettuce | redlock4j-jedis | +|--------|--------|--------|--------|--------| +| Total Ops/s | 18.07 | 0.00 | 0.02 | 0.00 | +| Avg Ops/s/Client (95% CI) | 9.03 ± 0.00 | 0.00 ± 0.00 | 0.01 ± 0.02 | 0.00 ± 0.00 | +| Successful Ops | 1,084 | 0 | 1 | 0 | +| Failed Ops | 0 | 2 | 1 | 2 | +| Success Rate | 100.00% | 0.00% | 50.00% | 0.00% | +| Avg Wait Time | 56.17 ms | 0.00 ms | 30005.60 ms | 0.00 ms | +| Correctness | PASS | PASS | PASS | PASS | + +## Latency Percentiles (microseconds) + +| Percentile | redisson | redlock4j-singlenode | redlock4j-lettuce | redlock4j-jedis | +|------------|--------|--------|--------|--------| +| p50 | 56,061 | N/A | 60,011,198 | N/A | +| p75 | 56,701 | N/A | 60,011,198 | N/A | +| p90 | 57,619 | N/A | 60,011,198 | N/A | +| p95 | 58,521 | N/A | 60,011,198 | N/A | +| p99 | 62,769 | N/A | 60,011,198 | N/A | +| p999 | 68,806 | N/A | 60,011,198 | N/A | +| max | 68,806 | N/A | 60,011,198 | N/A | +| mean | 56,172 | N/A | 60,011,198 | N/A | + +## Correctness Validation + +### redisson + +- **Status:** PASSED +- **Correctness Violations:** 0 +- **FIFO Violations:** 0 +- **Lock Events Analyzed:** 1174 + +### redlock4j-singlenode + +- **Status:** PASSED +- **Correctness Violations:** 0 +- **FIFO Violations:** 0 +- **Lock Events Analyzed:** 1 + +### redlock4j-lettuce + +- **Status:** PASSED +- **Correctness Violations:** 0 +- **FIFO Violations:** 0 +- **Lock Events Analyzed:** 3 + +### redlock4j-jedis + +- **Status:** PASSED +- **Correctness Violations:** 0 +- **FIFO Violations:** 0 +- **Lock Events Analyzed:** 1 + +## Analysis + +**Highest Throughput:** redisson with 18.07 ops/s + +**Lowest Latency:** redlock4j-singlenode with 0.00 ms average wait time + diff --git a/redlock4j-benchmark/countdownlatch-benchmark-results.json b/redlock4j-benchmark/countdownlatch-benchmark-results.json new file mode 100644 index 0000000..557ed50 --- /dev/null +++ b/redlock4j-benchmark/countdownlatch-benchmark-results.json @@ -0,0 +1,88 @@ +{ + "title" : "CountDownLatch", + "generatedAt" : "2026-06-15T16:15:01.353284Z", + "config" : { + "redisNodeCount" : 3, + "clientCount" : 5, + "benchmarkDurationMs" : 60000, + "warmupDurationMs" : 30000, + "workSimulationTimeMs" : 50, + "lockTimeoutMs" : 30000, + "lockAcquisitionTimeoutMs" : 60000, + "lockResourceName" : "benchmark-fair-lock", + "multiLockResourceCount" : 5 + }, + "results" : [ { + "implementationType" : "redisson", + "clientCount" : 1, + "totalSuccessfulOps" : 3542, + "totalFailedOps" : 0, + "successRatePct" : 100.0, + "aggregateOpsPerSecond" : 59.02251253936778, + "avgOpsPerSecondPerClient" : 59.02251253936778, + "opsPerSecondPerClientStdev" : 0.0, + "opsPerSecondPerClientCi95Half" : 0.0, + "avgWaitTimeMs" : 16.9135684635799, + "avgHoldTimeMs" : 0.0, + "totalCorrectnessViolations" : 0, + "totalFifoViolations" : 0, + "correct" : true, + "latencyPercentilesMicros" : { + "max" : 40818, + "p90" : 18579, + "p50" : 17170, + "p95" : 19225, + "p75" : 17869, + "p99" : 22568, + "mean" : 16913 + } + }, { + "implementationType" : "redlock4j-singlenode", + "clientCount" : 1, + "totalSuccessfulOps" : 3500, + "totalFailedOps" : 1, + "successRatePct" : 99.97143673236218, + "aggregateOpsPerSecond" : 58.19076595673932, + "avgOpsPerSecondPerClient" : 58.19076595673932, + "opsPerSecondPerClientStdev" : 0.0, + "opsPerSecondPerClientCi95Half" : 0.0, + "avgWaitTimeMs" : 15.674230057428572, + "avgHoldTimeMs" : 0.0, + "totalCorrectnessViolations" : 0, + "totalFifoViolations" : 0, + "correct" : true, + "latencyPercentilesMicros" : { + "max" : 31027, + "p90" : 17426, + "p50" : 16167, + "p95" : 17889, + "p75" : 16850, + "p99" : 19866, + "mean" : 15673 + } + }, { + "implementationType" : "redlock4j", + "clientCount" : 1, + "totalSuccessfulOps" : 3601, + "totalFailedOps" : 1, + "successRatePct" : 99.97223764575236, + "aggregateOpsPerSecond" : 59.91182097995175, + "avgOpsPerSecondPerClient" : 59.91182097995175, + "opsPerSecondPerClientStdev" : 0.0, + "opsPerSecondPerClientCi95Half" : 0.0, + "avgWaitTimeMs" : 15.180534230769231, + "avgHoldTimeMs" : 0.0, + "totalCorrectnessViolations" : 0, + "totalFifoViolations" : 0, + "correct" : true, + "latencyPercentilesMicros" : { + "max" : 115837, + "p90" : 17196, + "p50" : 15285, + "p95" : 17813, + "p75" : 16437, + "p99" : 19893, + "mean" : 15180 + } + } ] +} \ No newline at end of file diff --git a/redlock4j-benchmark/countdownlatch-benchmark-results.md b/redlock4j-benchmark/countdownlatch-benchmark-results.md index 698d7f1..568fcfc 100644 --- a/redlock4j-benchmark/countdownlatch-benchmark-results.md +++ b/redlock4j-benchmark/countdownlatch-benchmark-results.md @@ -1,13 +1,13 @@ -# Fair Lock Benchmark Results +# CountDownLatch Benchmark Results -**Generated:** 2026-04-21 01:45:35 +**Generated:** 2026-06-15 19:15:01 ## Configuration | Parameter | Value | |-----------|-------| | Redis Nodes | 3 | -| Clients per Implementation | 10 | +| Clients per Implementation | 5 | | Benchmark Duration | 1 minutes | | Work Simulation Time | 50 ms | | Lock Timeout | 30 s | @@ -16,26 +16,26 @@ | Metric | redisson | redlock4j-singlenode | redlock4j | |--------|--------|--------|--------| -| Total Ops/s | 58.36 | 35.65 | 62.08 | -| Avg Ops/s/Client | 58.36 | 35.65 | 62.08 | -| Successful Ops | 3,503 | 2,142 | 3,729 | -| Failed Ops | 0 | 5 | 0 | -| Success Rate | 100.00% | 99.77% | 100.00% | -| Avg Wait Time | 17.10 ms | 16.29 ms | 16.04 ms | +| Total Ops/s | 59.02 | 58.19 | 59.91 | +| Avg Ops/s/Client (95% CI) | 59.02 | 58.19 | 59.91 | +| Successful Ops | 3,542 | 3,500 | 3,601 | +| Failed Ops | 0 | 1 | 1 | +| Success Rate | 100.00% | 99.97% | 99.97% | +| Avg Wait Time | 16.91 ms | 15.67 ms | 15.18 ms | | Correctness | PASS | PASS | PASS | ## Latency Percentiles (microseconds) | Percentile | redisson | redlock4j-singlenode | redlock4j | |------------|--------|--------|--------| -| p50 | N/A | N/A | N/A | -| p75 | N/A | N/A | N/A | -| p90 | N/A | N/A | N/A | -| p95 | N/A | N/A | N/A | -| p99 | N/A | N/A | N/A | +| p50 | 17,170 | 16,167 | 15,285 | +| p75 | 17,869 | 16,850 | 16,437 | +| p90 | 18,579 | 17,426 | 17,196 | +| p95 | 19,225 | 17,889 | 17,813 | +| p99 | 22,568 | 19,866 | 19,893 | | p999 | N/A | N/A | N/A | -| max | N/A | N/A | N/A | -| mean | N/A | N/A | N/A | +| max | 40,818 | 31,027 | 115,837 | +| mean | 16,913 | 15,673 | 15,180 | ## Correctness Validation @@ -59,7 +59,7 @@ ## Analysis -**Highest Throughput:** redlock4j with 62.08 ops/s +**Highest Throughput:** redlock4j with 59.91 ops/s -**Lowest Latency:** redlock4j with 16.04 ms average wait time +**Lowest Latency:** redlock4j with 15.18 ms average wait time diff --git a/redlock4j-benchmark/distributed-lock-benchmark-results.json b/redlock4j-benchmark/distributed-lock-benchmark-results.json new file mode 100644 index 0000000..7d8c4d6 --- /dev/null +++ b/redlock4j-benchmark/distributed-lock-benchmark-results.json @@ -0,0 +1,202 @@ +{ + "title" : "Distributed Lock", + "generatedAt" : "2026-06-15T15:45:04.347228Z", + "config" : { + "redisNodeCount" : 3, + "clientCount" : 5, + "benchmarkDurationMs" : 60000, + "warmupDurationMs" : 30000, + "workSimulationTimeMs" : 50, + "lockTimeoutMs" : 30000, + "lockAcquisitionTimeoutMs" : 60000, + "lockResourceName" : "benchmark-fair-lock", + "multiLockResourceCount" : 5 + }, + "results" : [ { + "implementationType" : "redisson", + "clientCount" : 5, + "totalSuccessfulOps" : 1091, + "totalFailedOps" : 0, + "successRatePct" : 100.0, + "aggregateOpsPerSecond" : 18.219729731885934, + "avgOpsPerSecondPerClient" : 3.643945946377187, + "opsPerSecondPerClientStdev" : 0.7282852023543905, + "opsPerSecondPerClientCi95Half" : 0.63837012603287, + "avgWaitTimeMs" : 228.80625464658414, + "avgHoldTimeMs" : 50.02439906110198, + "totalCorrectnessViolations" : 0, + "totalFifoViolations" : 0, + "correct" : true, + "latencyPercentilesMicros" : { + "p99" : 1486782, + "max" : 1886599, + "p90" : 629993, + "mean" : 228805, + "p50" : 89320, + "p95" : 914320, + "p75" : 342395 + }, + "validation" : { + "totalEvents" : 1639, + "uniqueClients" : 0, + "concurrentHolderViolations" : 0, + "fifoViolations" : 0, + "correct" : true + } + }, { + "implementationType" : "redlock4j-singlenode", + "clientCount" : 5, + "totalSuccessfulOps" : 1093, + "totalFailedOps" : 0, + "successRatePct" : 100.0, + "aggregateOpsPerSecond" : 18.328310757378034, + "avgOpsPerSecondPerClient" : 3.665662151475607, + "opsPerSecondPerClientStdev" : 3.2023636945434286, + "opsPerSecondPerClientCi95Half" : 2.8069955405931797, + "avgWaitTimeMs" : 406.725834718043, + "avgHoldTimeMs" : 50.01394005119315, + "totalCorrectnessViolations" : 0, + "totalFifoViolations" : 0, + "correct" : true, + "latencyPercentilesMicros" : { + "p99" : 3142396, + "max" : 3272320, + "p90" : 1198846, + "mean" : 406725, + "p50" : 133024, + "p95" : 1573656, + "p75" : 572609 + }, + "validation" : { + "totalEvents" : 1644, + "uniqueClients" : 0, + "concurrentHolderViolations" : 0, + "fifoViolations" : 0, + "correct" : true + } + }, { + "implementationType" : "redlock4j-3node", + "clientCount" : 5, + "totalSuccessfulOps" : 23, + "totalFailedOps" : 5, + "successRatePct" : 82.14285714285714, + "aggregateOpsPerSecond" : 0.3771735290591251, + "avgOpsPerSecondPerClient" : 0.07543470581182501, + "opsPerSecondPerClientStdev" : 0.035724067407212595, + "opsPerSecondPerClientCi95Half" : 0.03131352571688231, + "avgWaitTimeMs" : 166.12947764761904, + "avgHoldTimeMs" : 50.00284293333333, + "totalCorrectnessViolations" : 0, + "totalFifoViolations" : 0, + "correct" : true, + "latencyPercentilesMicros" : { + "p99" : 406971, + "max" : 406971, + "p90" : 406971, + "mean" : 166128, + "p50" : 104842, + "p95" : 406971, + "p75" : 337742 + }, + "validation" : { + "totalEvents" : 185, + "uniqueClients" : 0, + "concurrentHolderViolations" : 0, + "fifoViolations" : 0, + "correct" : true + } + }, { + "implementationType" : "spring-integration", + "clientCount" : 5, + "totalSuccessfulOps" : 1106, + "totalFailedOps" : 0, + "successRatePct" : 100.0, + "aggregateOpsPerSecond" : 20.294657240921563, + "avgOpsPerSecondPerClient" : 4.058931448184312, + "opsPerSecondPerClientStdev" : 0.800505632419941, + "opsPerSecondPerClientCi95Half" : 0.7016741241012794, + "avgWaitTimeMs" : 199.83917454160374, + "avgHoldTimeMs" : 50.00662257927626, + "totalCorrectnessViolations" : 0, + "totalFifoViolations" : 0, + "correct" : true, + "latencyPercentilesMicros" : { + "p99" : 5687493, + "max" : 15908574, + "p90" : 930, + "mean" : 199838, + "p50" : 648, + "p95" : 218039, + "p75" : 764 + }, + "validation" : { + "totalEvents" : 1663, + "uniqueClients" : 0, + "concurrentHolderViolations" : 0, + "fifoViolations" : 0, + "correct" : true + } + }, { + "implementationType" : "shedlock-lettuce", + "clientCount" : 5, + "totalSuccessfulOps" : 1123, + "totalFailedOps" : 0, + "successRatePct" : 100.0, + "aggregateOpsPerSecond" : 18.93837900723612, + "avgOpsPerSecondPerClient" : 3.787675801447224, + "opsPerSecondPerClientStdev" : 1.2387096497966192, + "opsPerSecondPerClientCi95Half" : 1.085776880681438, + "avgWaitTimeMs" : 230.23130147224202, + "avgHoldTimeMs" : 50.03801120697934, + "totalCorrectnessViolations" : 0, + "totalFifoViolations" : 0, + "correct" : true, + "latencyPercentilesMicros" : { + "p99" : 3598404, + "max" : 5130364, + "p90" : 760011, + "mean" : 230230, + "p50" : 494, + "p95" : 1618169, + "p75" : 706 + }, + "validation" : { + "totalEvents" : 1689, + "uniqueClients" : 0, + "concurrentHolderViolations" : 0, + "fifoViolations" : 0, + "correct" : true + } + }, { + "implementationType" : "redpulsar", + "clientCount" : 5, + "totalSuccessfulOps" : 1107, + "totalFailedOps" : 0, + "successRatePct" : 100.0, + "aggregateOpsPerSecond" : 21.866724456107136, + "avgOpsPerSecondPerClient" : 4.373344891221427, + "opsPerSecondPerClientStdev" : 1.1416276655101916, + "opsPerSecondPerClientCi95Half" : 1.0006807695094706, + "avgWaitTimeMs" : 187.27340002934926, + "avgHoldTimeMs" : 50.020220456998, + "totalCorrectnessViolations" : 0, + "totalFifoViolations" : 0, + "correct" : true, + "latencyPercentilesMicros" : { + "p99" : 7771265, + "max" : 17503920, + "p90" : 1075, + "mean" : 187272, + "p50" : 656, + "p95" : 1630, + "p75" : 821 + }, + "validation" : { + "totalEvents" : 1664, + "uniqueClients" : 0, + "concurrentHolderViolations" : 0, + "fifoViolations" : 0, + "correct" : true + } + } ] +} \ No newline at end of file diff --git a/redlock4j-benchmark/distributed-lock-benchmark-results.md b/redlock4j-benchmark/distributed-lock-benchmark-results.md index 3959bbd..ddf59c8 100644 --- a/redlock4j-benchmark/distributed-lock-benchmark-results.md +++ b/redlock4j-benchmark/distributed-lock-benchmark-results.md @@ -1,6 +1,6 @@ -# Fair Lock Benchmark Results +# Distributed Lock Benchmark Results -**Generated:** 2026-04-22 00:45:16 +**Generated:** 2026-06-15 18:45:04 ## Configuration @@ -16,26 +16,26 @@ | Metric | redisson | redlock4j-singlenode | redlock4j-3node | spring-integration | shedlock-lettuce | redpulsar | |--------|--------|--------|--------|--------|--------|--------| -| Total Ops/s | 18.33 | 18.36 | 0.50 | 18.55 | 18.87 | 18.49 | -| Avg Ops/s/Client | 3.67 | 3.67 | 0.10 | 3.71 | 3.77 | 3.70 | -| Successful Ops | 1,102 | 1,104 | 31 | 1,118 | 1,135 | 1,114 | +| Total Ops/s | 18.22 | 18.33 | 0.38 | 20.29 | 18.94 | 21.87 | +| Avg Ops/s/Client (95% CI) | 3.64 ± 0.64 | 3.67 ± 2.81 | 0.08 ± 0.03 | 4.06 ± 0.70 | 3.79 ± 1.09 | 4.37 ± 1.00 | +| Successful Ops | 1,091 | 1,093 | 23 | 1,106 | 1,123 | 1,107 | | Failed Ops | 0 | 0 | 5 | 0 | 0 | 0 | -| Success Rate | 100.00% | 100.00% | 86.11% | 100.00% | 100.00% | 100.00% | -| Avg Wait Time | 224.20 ms | 256.57 ms | 239.75 ms | 223.69 ms | 218.28 ms | 378.76 ms | +| Success Rate | 100.00% | 100.00% | 82.14% | 100.00% | 100.00% | 100.00% | +| Avg Wait Time | 228.81 ms | 406.73 ms | 166.13 ms | 199.84 ms | 230.23 ms | 187.27 ms | | Correctness | PASS | PASS | PASS | PASS | PASS | PASS | ## Latency Percentiles (microseconds) | Percentile | redisson | redlock4j-singlenode | redlock4j-3node | spring-integration | shedlock-lettuce | redpulsar | |------------|--------|--------|--------|--------|--------|--------| -| p50 | 99,701 | 99,108 | 216,678 | 511 | 437 | 570 | -| p75 | 338,708 | 341,942 | 414,533 | 675 | 687 | 778 | -| p90 | 649,703 | 758,400 | 617,272 | 846 | 781,306 | 1,180 | -| p95 | 885,987 | 1,045,643 | 617,272 | 1,318 | 1,649,013 | 2,432,775 | -| p99 | 1,258,035 | 1,692,837 | 617,272 | 8,344,652 | 3,083,844 | 10,604,848 | +| p50 | 89,320 | 133,024 | 104,842 | 648 | 494 | 656 | +| p75 | 342,395 | 572,609 | 337,742 | 764 | 706 | 821 | +| p90 | 629,993 | 1,198,846 | 406,971 | 930 | 760,011 | 1,075 | +| p95 | 914,320 | 1,573,656 | 406,971 | 218,039 | 1,618,169 | 1,630 | +| p99 | 1,486,782 | 3,142,396 | 406,971 | 5,687,493 | 3,598,404 | 7,771,265 | | p999 | N/A | N/A | N/A | N/A | N/A | N/A | -| max | 1,792,524 | 2,228,022 | 617,272 | 17,979,202 | 4,140,203 | 13,796,471 | -| mean | 224,195 | 256,572 | 239,748 | 223,693 | 218,275 | 378,756 | +| max | 1,886,599 | 3,272,320 | 406,971 | 15,908,574 | 5,130,364 | 17,503,920 | +| mean | 228,805 | 406,725 | 166,128 | 199,838 | 230,230 | 187,272 | ## Correctness Validation @@ -44,46 +44,46 @@ - **Status:** PASSED - **Correctness Violations:** 0 - **FIFO Violations:** 0 -- **Lock Events Analyzed:** 1102 +- **Lock Events Analyzed:** 1639 ### redlock4j-singlenode - **Status:** PASSED - **Correctness Violations:** 0 - **FIFO Violations:** 0 -- **Lock Events Analyzed:** 1104 +- **Lock Events Analyzed:** 1644 ### redlock4j-3node - **Status:** PASSED - **Correctness Violations:** 0 - **FIFO Violations:** 0 -- **Lock Events Analyzed:** 31 +- **Lock Events Analyzed:** 185 ### spring-integration - **Status:** PASSED - **Correctness Violations:** 0 - **FIFO Violations:** 0 -- **Lock Events Analyzed:** 1118 +- **Lock Events Analyzed:** 1663 ### shedlock-lettuce - **Status:** PASSED - **Correctness Violations:** 0 - **FIFO Violations:** 0 -- **Lock Events Analyzed:** 1135 +- **Lock Events Analyzed:** 1689 ### redpulsar - **Status:** PASSED - **Correctness Violations:** 0 - **FIFO Violations:** 0 -- **Lock Events Analyzed:** 1114 +- **Lock Events Analyzed:** 1664 ## Analysis -**Highest Throughput:** shedlock-lettuce with 18.87 ops/s +**Highest Throughput:** redpulsar with 21.87 ops/s -**Lowest Latency:** shedlock-lettuce with 218.28 ms average wait time +**Lowest Latency:** redlock4j-3node with 166.13 ms average wait time diff --git a/redlock4j-benchmark/fairlock-benchmark-results.json b/redlock4j-benchmark/fairlock-benchmark-results.json new file mode 100644 index 0000000..99bb4f8 --- /dev/null +++ b/redlock4j-benchmark/fairlock-benchmark-results.json @@ -0,0 +1,144 @@ +{ + "title" : "Fair Lock", + "generatedAt" : "2026-06-16T06:01:07.099059Z", + "config" : { + "redisNodeCount" : 3, + "clientCount" : 5, + "benchmarkDurationMs" : 60000, + "warmupDurationMs" : 30000, + "workSimulationTimeMs" : 50, + "lockTimeoutMs" : 30000, + "lockAcquisitionTimeoutMs" : 60000, + "lockResourceName" : "benchmark-fair-lock", + "multiLockResourceCount" : 5 + }, + "results" : [ { + "implementationType" : "redisson", + "clientCount" : 5, + "totalSuccessfulOps" : 1088, + "totalFailedOps" : 0, + "successRatePct" : 100.0, + "aggregateOpsPerSecond" : 16.948400513826027, + "avgOpsPerSecondPerClient" : 3.3896801027652055, + "opsPerSecondPerClientStdev" : 0.5407494028915475, + "opsPerSecondPerClientCi95Half" : 0.4739877500739052, + "avgWaitTimeMs" : 248.5259728903733, + "avgHoldTimeMs" : 50.00616075320255, + "totalCorrectnessViolations" : 0, + "totalFifoViolations" : 0, + "correct" : true, + "latencyPercentilesMicros" : { + "p99" : 229468, + "max" : 6187845, + "p90" : 223792, + "mean" : 248525, + "p50" : 221531, + "p999" : 6187845, + "p95" : 224794, + "p75" : 222592 + }, + "validation" : { + "totalEvents" : 1633, + "uniqueClients" : 0, + "concurrentHolderViolations" : 0, + "fifoViolations" : 0, + "correct" : true + } + }, { + "implementationType" : "redlock4j-singlenode", + "clientCount" : 5, + "totalSuccessfulOps" : 716, + "totalFailedOps" : 0, + "successRatePct" : 100.0, + "aggregateOpsPerSecond" : 11.923406452877419, + "avgOpsPerSecondPerClient" : 2.3846812905754837, + "opsPerSecondPerClientStdev" : 0.0012890227320495476, + "opsPerSecondPerClientCi95Half" : 0.0011298782417348717, + "avgWaitTimeMs" : 365.0210132327408, + "avgHoldTimeMs" : 50.00527406869658, + "totalCorrectnessViolations" : 0, + "totalFifoViolations" : 0, + "correct" : true, + "latencyPercentilesMicros" : { + "p99" : 471050, + "max" : 492703, + "p90" : 436671, + "mean" : 365020, + "p50" : 376841, + "p999" : 492703, + "p95" : 439692, + "p75" : 386740 + }, + "validation" : { + "totalEvents" : 1080, + "uniqueClients" : 0, + "concurrentHolderViolations" : 0, + "fifoViolations" : 0, + "correct" : true + } + }, { + "implementationType" : "redlock4j-lettuce", + "clientCount" : 5, + "totalSuccessfulOps" : 701, + "totalFailedOps" : 0, + "successRatePct" : 100.0, + "aggregateOpsPerSecond" : 11.684396716301906, + "avgOpsPerSecondPerClient" : 2.336879343260381, + "opsPerSecondPerClientStdev" : 0.0012425107664290186, + "opsPerSecondPerClientCi95Half" : 0.0010891087063121744, + "avgWaitTimeMs" : 373.32055120367784, + "avgHoldTimeMs" : 50.01238781612969, + "totalCorrectnessViolations" : 0, + "totalFifoViolations" : 0, + "correct" : true, + "latencyPercentilesMicros" : { + "p99" : 476082, + "max" : 482587, + "p90" : 416142, + "mean" : 373319, + "p50" : 361576, + "p999" : 482587, + "p95" : 439925, + "p75" : 407230 + }, + "validation" : { + "totalEvents" : 1054, + "uniqueClients" : 0, + "concurrentHolderViolations" : 0, + "fifoViolations" : 0, + "correct" : true + } + }, { + "implementationType" : "redlock4j-jedis", + "clientCount" : 5, + "totalSuccessfulOps" : 719, + "totalFailedOps" : 0, + "successRatePct" : 100.0, + "aggregateOpsPerSecond" : 11.975816088448317, + "avgOpsPerSecondPerClient" : 2.3951632176896633, + "opsPerSecondPerClientStdev" : 0.0014100612515607193, + "opsPerSecondPerClientCi95Half" : 0.0012359731818838541, + "avgWaitTimeMs" : 362.72678545551673, + "avgHoldTimeMs" : 50.01029643166278, + "totalCorrectnessViolations" : 0, + "totalFifoViolations" : 0, + "correct" : true, + "latencyPercentilesMicros" : { + "p99" : 445089, + "max" : 455135, + "p90" : 411908, + "mean" : 362725, + "p50" : 354855, + "p999" : 455135, + "p95" : 418482, + "p75" : 398471 + }, + "validation" : { + "totalEvents" : 1079, + "uniqueClients" : 0, + "concurrentHolderViolations" : 0, + "fifoViolations" : 0, + "correct" : true + } + } ] +} \ No newline at end of file diff --git a/redlock4j-benchmark/fairlock-benchmark-results.md b/redlock4j-benchmark/fairlock-benchmark-results.md new file mode 100644 index 0000000..7f575d7 --- /dev/null +++ b/redlock4j-benchmark/fairlock-benchmark-results.md @@ -0,0 +1,75 @@ +# Fair Lock Benchmark Results + +**Generated:** 2026-06-16 09:01:07 + +## Configuration + +| Parameter | Value | +|-----------|-------| +| Redis Nodes | 3 | +| Clients per Implementation | 5 | +| Benchmark Duration | 1 minutes | +| Work Simulation Time | 50 ms | +| Lock Timeout | 30 s | + +## Summary Comparison + +| Metric | redisson | redlock4j-singlenode | redlock4j-lettuce | redlock4j-jedis | +|--------|--------|--------|--------|--------| +| Total Ops/s | 16.95 | 11.92 | 11.68 | 11.98 | +| Avg Ops/s/Client (95% CI) | 3.39 ± 0.47 | 2.38 ± 0.00 | 2.34 ± 0.00 | 2.40 ± 0.00 | +| Successful Ops | 1,088 | 716 | 701 | 719 | +| Failed Ops | 0 | 0 | 0 | 0 | +| Success Rate | 100.00% | 100.00% | 100.00% | 100.00% | +| Avg Wait Time | 248.53 ms | 365.02 ms | 373.32 ms | 362.73 ms | +| Correctness | PASS | PASS | PASS | PASS | + +## Latency Percentiles (microseconds) + +| Percentile | redisson | redlock4j-singlenode | redlock4j-lettuce | redlock4j-jedis | +|------------|--------|--------|--------|--------| +| p50 | 221,531 | 376,841 | 361,576 | 354,855 | +| p75 | 222,592 | 386,740 | 407,230 | 398,471 | +| p90 | 223,792 | 436,671 | 416,142 | 411,908 | +| p95 | 224,794 | 439,692 | 439,925 | 418,482 | +| p99 | 229,468 | 471,050 | 476,082 | 445,089 | +| p999 | 6,187,845 | 492,703 | 482,587 | 455,135 | +| max | 6,187,845 | 492,703 | 482,587 | 455,135 | +| mean | 248,525 | 365,020 | 373,319 | 362,725 | + +## Correctness Validation + +### redisson + +- **Status:** PASSED +- **Correctness Violations:** 0 +- **FIFO Violations:** 0 +- **Lock Events Analyzed:** 1633 + +### redlock4j-singlenode + +- **Status:** PASSED +- **Correctness Violations:** 0 +- **FIFO Violations:** 0 +- **Lock Events Analyzed:** 1080 + +### redlock4j-lettuce + +- **Status:** PASSED +- **Correctness Violations:** 0 +- **FIFO Violations:** 0 +- **Lock Events Analyzed:** 1054 + +### redlock4j-jedis + +- **Status:** PASSED +- **Correctness Violations:** 0 +- **FIFO Violations:** 0 +- **Lock Events Analyzed:** 1079 + +## Analysis + +**Highest Throughput:** redisson with 16.95 ops/s + +**Lowest Latency:** redisson with 248.53 ms average wait time + diff --git a/redlock4j-benchmark/multilock-benchmark-results.json b/redlock4j-benchmark/multilock-benchmark-results.json new file mode 100644 index 0000000..7a1d8cd --- /dev/null +++ b/redlock4j-benchmark/multilock-benchmark-results.json @@ -0,0 +1,112 @@ +{ + "title" : "MultiLock", + "generatedAt" : "2026-06-15T16:01:18.421252Z", + "config" : { + "redisNodeCount" : 3, + "clientCount" : 5, + "benchmarkDurationMs" : 60000, + "warmupDurationMs" : 30000, + "workSimulationTimeMs" : 50, + "lockTimeoutMs" : 30000, + "lockAcquisitionTimeoutMs" : 60000, + "lockResourceName" : "benchmark-fair-lock", + "multiLockResourceCount" : 5 + }, + "results" : [ { + "implementationType" : "redisson", + "clientCount" : 5, + "totalSuccessfulOps" : 1022, + "totalFailedOps" : 0, + "successRatePct" : 100.0, + "aggregateOpsPerSecond" : 17.05015326003449, + "avgOpsPerSecondPerClient" : 3.410030652006898, + "opsPerSecondPerClientStdev" : 1.2236718103220292, + "opsPerSecondPerClientCi95Half" : 1.072595633211872, + "avgWaitTimeMs" : 272.92606806895094, + "avgHoldTimeMs" : 50.020637363991845, + "totalCorrectnessViolations" : 0, + "totalFifoViolations" : 0, + "correct" : true, + "latencyPercentilesMicros" : { + "p99" : 1192186, + "max" : 1439439, + "p90" : 583926, + "mean" : 272925, + "p50" : 190208, + "p999" : 1439439, + "p95" : 789649, + "p75" : 348389 + }, + "validation" : { + "totalEvents" : 1537, + "uniqueClients" : 0, + "concurrentHolderViolations" : 0, + "fifoViolations" : 0, + "correct" : true + } + }, { + "implementationType" : "redlock4j-singlenode", + "clientCount" : 5, + "totalSuccessfulOps" : 1018, + "totalFailedOps" : 0, + "successRatePct" : 100.0, + "aggregateOpsPerSecond" : 16.973326435738322, + "avgOpsPerSecondPerClient" : 3.3946652871476646, + "opsPerSecondPerClientStdev" : 1.2426388367874797, + "opsPerSecondPerClientCi95Half" : 1.0892209649309237, + "avgWaitTimeMs" : 302.9894674610356, + "avgHoldTimeMs" : 50.009192451060095, + "totalCorrectnessViolations" : 0, + "totalFifoViolations" : 0, + "correct" : true, + "latencyPercentilesMicros" : { + "p99" : 1709499, + "max" : 2087382, + "p90" : 649731, + "mean" : 302988, + "p50" : 202917, + "p999" : 2087382, + "p95" : 1038258, + "p75" : 373225 + }, + "validation" : { + "totalEvents" : 1539, + "uniqueClients" : 0, + "concurrentHolderViolations" : 0, + "fifoViolations" : 0, + "correct" : true + } + }, { + "implementationType" : "redlock4j-multilock", + "clientCount" : 5, + "totalSuccessfulOps" : 1061, + "totalFailedOps" : 0, + "successRatePct" : 100.0, + "aggregateOpsPerSecond" : 17.71924395180982, + "avgOpsPerSecondPerClient" : 3.543848790361964, + "opsPerSecondPerClientStdev" : 0.7830012104488336, + "opsPerSecondPerClientCi95Half" : 0.6863308217470585, + "avgWaitTimeMs" : 237.40170136893624, + "avgHoldTimeMs" : 50.00750716650992, + "totalCorrectnessViolations" : 0, + "totalFifoViolations" : 0, + "correct" : true, + "latencyPercentilesMicros" : { + "p99" : 1057619, + "max" : 1489792, + "p90" : 556400, + "mean" : 237400, + "p50" : 170985, + "p999" : 1489792, + "p95" : 687449, + "p75" : 298446 + }, + "validation" : { + "totalEvents" : 1598, + "uniqueClients" : 0, + "concurrentHolderViolations" : 0, + "fifoViolations" : 0, + "correct" : true + } + } ] +} \ No newline at end of file diff --git a/redlock4j-benchmark/multilock-benchmark-results.md b/redlock4j-benchmark/multilock-benchmark-results.md index 79c6363..c5beab1 100644 --- a/redlock4j-benchmark/multilock-benchmark-results.md +++ b/redlock4j-benchmark/multilock-benchmark-results.md @@ -1,13 +1,13 @@ -# Fair Lock Benchmark Results +# MultiLock Benchmark Results -**Generated:** 2026-04-21 01:35:27 +**Generated:** 2026-06-15 19:01:18 ## Configuration | Parameter | Value | |-----------|-------| | Redis Nodes | 3 | -| Clients per Implementation | 3 | +| Clients per Implementation | 5 | | Benchmark Duration | 1 minutes | | Work Simulation Time | 50 ms | | Lock Timeout | 30 s | @@ -16,26 +16,26 @@ | Metric | redisson | redlock4j-singlenode | redlock4j-multilock | |--------|--------|--------|--------| -| Total Ops/s | 17.44 | 17.80 | 16.73 | -| Avg Ops/s/Client | 5.81 | 5.93 | 5.58 | -| Successful Ops | 1,048 | 1,069 | 1,005 | +| Total Ops/s | 17.05 | 16.97 | 17.72 | +| Avg Ops/s/Client (95% CI) | 3.41 ± 1.07 | 3.39 ± 1.09 | 3.54 ± 0.69 | +| Successful Ops | 1,022 | 1,018 | 1,061 | | Failed Ops | 0 | 0 | 0 | | Success Rate | 100.00% | 100.00% | 100.00% | -| Avg Wait Time | 115.48 ms | 142.73 ms | 126.66 ms | +| Avg Wait Time | 272.93 ms | 302.99 ms | 237.40 ms | | Correctness | PASS | PASS | PASS | ## Latency Percentiles (microseconds) | Percentile | redisson | redlock4j-singlenode | redlock4j-multilock | |------------|--------|--------|--------| -| p50 | 95,354 | 1,319 | 2,996 | -| p75 | 137,200 | 1,550 | 3,465 | -| p90 | 223,156 | 2,588 | 146,669 | -| p95 | 267,811 | 374,907 | 1,003,327 | -| p99 | 439,992 | 5,194,174 | 2,569,274 | -| p999 | 577,919 | 6,879,269 | 3,921,713 | -| max | 577,919 | 6,879,269 | 3,921,713 | -| mean | 115,481 | 142,728 | 126,659 | +| p50 | 190,208 | 202,917 | 170,985 | +| p75 | 348,389 | 373,225 | 298,446 | +| p90 | 583,926 | 649,731 | 556,400 | +| p95 | 789,649 | 1,038,258 | 687,449 | +| p99 | 1,192,186 | 1,709,499 | 1,057,619 | +| p999 | 1,439,439 | 2,087,382 | 1,489,792 | +| max | 1,439,439 | 2,087,382 | 1,489,792 | +| mean | 272,925 | 302,988 | 237,400 | ## Correctness Validation @@ -44,25 +44,25 @@ - **Status:** PASSED - **Correctness Violations:** 0 - **FIFO Violations:** 0 -- **Lock Events Analyzed:** 1048 +- **Lock Events Analyzed:** 1537 ### redlock4j-singlenode - **Status:** PASSED - **Correctness Violations:** 0 - **FIFO Violations:** 0 -- **Lock Events Analyzed:** 1069 +- **Lock Events Analyzed:** 1539 ### redlock4j-multilock - **Status:** PASSED - **Correctness Violations:** 0 - **FIFO Violations:** 0 -- **Lock Events Analyzed:** 1005 +- **Lock Events Analyzed:** 1598 ## Analysis -**Highest Throughput:** redlock4j-singlenode with 17.80 ops/s +**Highest Throughput:** redlock4j-multilock with 17.72 ops/s -**Lowest Latency:** redisson with 115.48 ms average wait time +**Lowest Latency:** redlock4j-multilock with 237.40 ms average wait time diff --git a/redlock4j-benchmark/pom.xml b/redlock4j-benchmark/pom.xml index c175aaf..6b7433e 100644 --- a/redlock4j-benchmark/pom.xml +++ b/redlock4j-benchmark/pom.xml @@ -11,7 +11,7 @@ 17 17 UTF-8 - 7.1.0 + 7.4.1 7.1.0.RELEASE 3.46.0 6.4.10 diff --git a/redlock4j-benchmark/rwlock-benchmark-results.json b/redlock4j-benchmark/rwlock-benchmark-results.json new file mode 100644 index 0000000..2983187 --- /dev/null +++ b/redlock4j-benchmark/rwlock-benchmark-results.json @@ -0,0 +1,202 @@ +{ + "title" : "ReadWriteLock", + "generatedAt" : "2026-06-15T16:05:53.105072Z", + "config" : { + "redisNodeCount" : 3, + "clientCount" : 10, + "benchmarkDurationMs" : 60000, + "warmupDurationMs" : 30000, + "workSimulationTimeMs" : 50, + "lockTimeoutMs" : 30000, + "lockAcquisitionTimeoutMs" : 60000, + "lockResourceName" : "benchmark-fair-lock", + "multiLockResourceCount" : 5 + }, + "results" : [ { + "implementationType" : "redisson-reader", + "clientCount" : 8, + "totalSuccessfulOps" : 9217, + "totalFailedOps" : 0, + "successRatePct" : 100.0, + "aggregateOpsPerSecond" : 153.61539977176437, + "avgOpsPerSecondPerClient" : 19.201924971470547, + "opsPerSecondPerClientStdev" : 0.019677973091967525, + "opsPerSecondPerClientCi95Half" : 0.013636139649069918, + "avgWaitTimeMs" : 0.9391922212370032, + "avgHoldTimeMs" : 50.02672539902383, + "totalCorrectnessViolations" : 0, + "totalFifoViolations" : 0, + "correct" : true, + "latencyPercentilesMicros" : { + "p99" : 3098, + "max" : 13802, + "p90" : 1578, + "mean" : 938, + "p50" : 787, + "p95" : 1920, + "p75" : 1130 + }, + "validation" : { + "totalEvents" : 32, + "uniqueClients" : 0, + "concurrentHolderViolations" : 0, + "fifoViolations" : 0, + "correct" : true + } + }, { + "implementationType" : "redlock4j-singlenode-reader", + "clientCount" : 8, + "totalSuccessfulOps" : 3543, + "totalFailedOps" : 0, + "successRatePct" : 100.0, + "aggregateOpsPerSecond" : 59.07161117901987, + "avgOpsPerSecondPerClient" : 7.3839513973774835, + "opsPerSecondPerClientStdev" : 1.458537524174648, + "opsPerSecondPerClientCi95Half" : 1.010714938479753, + "avgWaitTimeMs" : 86.22786999240297, + "avgHoldTimeMs" : 50.00520677979282, + "totalCorrectnessViolations" : 0, + "totalFifoViolations" : 0, + "correct" : true, + "latencyPercentilesMicros" : { + "p99" : 452207, + "max" : 636015, + "p90" : 213040, + "mean" : 86227, + "p50" : 58444, + "p95" : 277235, + "p75" : 122436 + }, + "validation" : { + "totalEvents" : 1563, + "uniqueClients" : 0, + "concurrentHolderViolations" : 0, + "fifoViolations" : 0, + "correct" : true + } + }, { + "implementationType" : "redlock4j-rwlock-reader", + "clientCount" : 8, + "totalSuccessfulOps" : 3210, + "totalFailedOps" : 0, + "successRatePct" : 100.0, + "aggregateOpsPerSecond" : 53.58673234771001, + "avgOpsPerSecondPerClient" : 6.698341543463751, + "opsPerSecondPerClientStdev" : 1.4734786700914049, + "opsPerSecondPerClientCi95Half" : 1.0210686243642606, + "avgWaitTimeMs" : 98.96458969377471, + "avgHoldTimeMs" : 50.00932495815257, + "totalCorrectnessViolations" : 0, + "totalFifoViolations" : 0, + "correct" : true, + "latencyPercentilesMicros" : { + "p99" : 431941, + "max" : 508208, + "p90" : 236833, + "mean" : 98963, + "p50" : 65604, + "p95" : 311223, + "p75" : 133819 + }, + "validation" : { + "totalEvents" : 1426, + "uniqueClients" : 0, + "concurrentHolderViolations" : 0, + "fifoViolations" : 0, + "correct" : true + } + }, { + "implementationType" : "redisson-writer", + "clientCount" : 2, + "totalSuccessfulOps" : 2, + "totalFailedOps" : 0, + "successRatePct" : 100.0, + "aggregateOpsPerSecond" : 0.07398345113884638, + "avgOpsPerSecondPerClient" : 0.03699172556942319, + "opsPerSecondPerClientStdev" : 2.0802990732073854E-4, + "opsPerSecondPerClientCi95Half" : 2.8831474198596233E-4, + "avgWaitTimeMs" : 26977.794708499998, + "avgHoldTimeMs" : 50.002979499999995, + "totalCorrectnessViolations" : 0, + "totalFifoViolations" : 0, + "correct" : true, + "latencyPercentilesMicros" : { + "p99" : 26977794, + "max" : 26977794, + "p90" : 26977794, + "mean" : 26977794, + "p50" : 26977794, + "p95" : 26977794, + "p75" : 26977794 + }, + "validation" : { + "totalEvents" : 32, + "uniqueClients" : 0, + "concurrentHolderViolations" : 0, + "fifoViolations" : 0, + "correct" : true + } + }, { + "implementationType" : "redlock4j-singlenode-writer", + "clientCount" : 2, + "totalSuccessfulOps" : 1036, + "totalFailedOps" : 0, + "successRatePct" : 100.0, + "aggregateOpsPerSecond" : 17.26450749345652, + "avgOpsPerSecondPerClient" : 8.63225374672826, + "opsPerSecondPerClientStdev" : 0.09416740114704245, + "opsPerSecondPerClientCi95Half" : 0.13050935951886367, + "avgWaitTimeMs" : 60.99441234387719, + "avgHoldTimeMs" : 50.00259181538753, + "totalCorrectnessViolations" : 0, + "totalFifoViolations" : 0, + "correct" : true, + "latencyPercentilesMicros" : { + "p99" : 90528, + "max" : 1449909, + "p90" : 59401, + "mean" : 60993, + "p50" : 57704, + "p95" : 60295, + "p75" : 58483 + }, + "validation" : { + "totalEvents" : 1563, + "uniqueClients" : 0, + "concurrentHolderViolations" : 0, + "fifoViolations" : 0, + "correct" : true + } + }, { + "implementationType" : "redlock4j-rwlock-writer", + "clientCount" : 2, + "totalSuccessfulOps" : 931, + "totalFailedOps" : 0, + "successRatePct" : 100.0, + "aggregateOpsPerSecond" : 15.511247795354276, + "avgOpsPerSecondPerClient" : 7.755623897677138, + "opsPerSecondPerClientStdev" : 0.10675985846090473, + "opsPerSecondPerClientCi95Half" : 0.1479616149573946, + "avgWaitTimeMs" : 70.82498696262519, + "avgHoldTimeMs" : 50.00880279989615, + "totalCorrectnessViolations" : 0, + "totalFifoViolations" : 0, + "correct" : true, + "latencyPercentilesMicros" : { + "p99" : 72728, + "max" : 3583183, + "p90" : 67367, + "mean" : 70824, + "p50" : 63961, + "p95" : 68532, + "p75" : 65647 + }, + "validation" : { + "totalEvents" : 1426, + "uniqueClients" : 0, + "concurrentHolderViolations" : 0, + "fifoViolations" : 0, + "correct" : true + } + } ] +} \ No newline at end of file diff --git a/redlock4j-benchmark/rwlock-benchmark-results.md b/redlock4j-benchmark/rwlock-benchmark-results.md index d181ead..a0267ea 100644 --- a/redlock4j-benchmark/rwlock-benchmark-results.md +++ b/redlock4j-benchmark/rwlock-benchmark-results.md @@ -1,6 +1,6 @@ -# Fair Lock Benchmark Results +# ReadWriteLock Benchmark Results -**Generated:** 2026-04-21 01:38:55 +**Generated:** 2026-06-15 19:05:53 ## Configuration @@ -14,28 +14,28 @@ ## Summary Comparison -| Metric | redisson-reader | redlock4j-singlenode-writer | redlock4j-rwlock-writer | -|--------|--------|--------|--------| -| Total Ops/s | 67.55 | 31.55 | 27.89 | -| Avg Ops/s/Client | 11.26 | 5.26 | 4.65 | -| Successful Ops | 4,056 | 1,896 | 1,676 | -| Failed Ops | 0 | 0 | 0 | -| Success Rate | 100.00% | 100.00% | 100.00% | -| Avg Wait Time | 229.26 ms | 218.25 ms | 202.78 ms | -| Correctness | PASS | PASS | PASS | +| Metric | redisson-reader | redlock4j-singlenode-reader | redlock4j-rwlock-reader | redisson-writer | redlock4j-singlenode-writer | redlock4j-rwlock-writer | +|--------|--------|--------|--------|--------|--------|--------| +| Total Ops/s | 153.62 | 59.07 | 53.59 | 0.07 | 17.26 | 15.51 | +| Avg Ops/s/Client (95% CI) | 19.20 ± 0.01 | 7.38 ± 1.01 | 6.70 ± 1.02 | 0.04 ± 0.00 | 8.63 ± 0.13 | 7.76 ± 0.15 | +| Successful Ops | 9,217 | 3,543 | 3,210 | 2 | 1,036 | 931 | +| Failed Ops | 0 | 0 | 0 | 0 | 0 | 0 | +| Success Rate | 100.00% | 100.00% | 100.00% | 100.00% | 100.00% | 100.00% | +| Avg Wait Time | 0.94 ms | 86.23 ms | 98.96 ms | 26977.79 ms | 60.99 ms | 70.82 ms | +| Correctness | PASS | PASS | PASS | PASS | PASS | PASS | ## Latency Percentiles (microseconds) -| Percentile | redisson-reader | redlock4j-singlenode-writer | redlock4j-rwlock-writer | -|------------|--------|--------|--------| -| p50 | 10,158 | 1,451 | 3,475 | -| p75 | 133,469 | 1,791 | 70,346 | -| p90 | 570,819 | 249,584 | 758,417 | -| p95 | 782,472 | 1,354,260 | 1,362,848 | -| p99 | 4,284,011 | 4,606,454 | 2,665,878 | -| p999 | N/A | N/A | N/A | -| max | 4,672,595 | 8,791,700 | 3,940,744 | -| mean | 229,257 | 218,248 | 202,774 | +| Percentile | redisson-reader | redlock4j-singlenode-reader | redlock4j-rwlock-reader | redisson-writer | redlock4j-singlenode-writer | redlock4j-rwlock-writer | +|------------|--------|--------|--------|--------|--------|--------| +| p50 | 787 | 58,444 | 65,604 | 26,977,794 | 57,704 | 63,961 | +| p75 | 1,130 | 122,436 | 133,819 | 26,977,794 | 58,483 | 65,647 | +| p90 | 1,578 | 213,040 | 236,833 | 26,977,794 | 59,401 | 67,367 | +| p95 | 1,920 | 277,235 | 311,223 | 26,977,794 | 60,295 | 68,532 | +| p99 | 3,098 | 452,207 | 431,941 | 26,977,794 | 90,528 | 72,728 | +| p999 | N/A | N/A | N/A | N/A | N/A | N/A | +| max | 13,802 | 636,015 | 508,208 | 26,977,794 | 1,449,909 | 3,583,183 | +| mean | 938 | 86,227 | 98,963 | 26,977,794 | 60,993 | 70,824 | ## Correctness Validation @@ -44,25 +44,46 @@ - **Status:** PASSED - **Correctness Violations:** 0 - **FIFO Violations:** 0 -- **Lock Events Analyzed:** 167 +- **Lock Events Analyzed:** 32 + +### redlock4j-singlenode-reader + +- **Status:** PASSED +- **Correctness Violations:** 0 +- **FIFO Violations:** 0 +- **Lock Events Analyzed:** 1563 + +### redlock4j-rwlock-reader + +- **Status:** PASSED +- **Correctness Violations:** 0 +- **FIFO Violations:** 0 +- **Lock Events Analyzed:** 1426 + +### redisson-writer + +- **Status:** PASSED +- **Correctness Violations:** 0 +- **FIFO Violations:** 0 +- **Lock Events Analyzed:** 32 ### redlock4j-singlenode-writer - **Status:** PASSED - **Correctness Violations:** 0 - **FIFO Violations:** 0 -- **Lock Events Analyzed:** 1119 +- **Lock Events Analyzed:** 1563 ### redlock4j-rwlock-writer - **Status:** PASSED - **Correctness Violations:** 0 - **FIFO Violations:** 0 -- **Lock Events Analyzed:** 924 +- **Lock Events Analyzed:** 1426 ## Analysis -**Highest Throughput:** redisson-reader with 67.55 ops/s +**Highest Throughput:** redisson-reader with 153.62 ops/s -**Lowest Latency:** redlock4j-rwlock-writer with 202.78 ms average wait time +**Lowest Latency:** redisson-reader with 0.94 ms average wait time diff --git a/redlock4j-benchmark/semaphore-benchmark-results.json b/redlock4j-benchmark/semaphore-benchmark-results.json new file mode 100644 index 0000000..012c508 --- /dev/null +++ b/redlock4j-benchmark/semaphore-benchmark-results.json @@ -0,0 +1,109 @@ +{ + "title" : "Semaphore", + "generatedAt" : "2026-06-15T16:10:27.140541Z", + "config" : { + "redisNodeCount" : 3, + "clientCount" : 5, + "benchmarkDurationMs" : 60000, + "warmupDurationMs" : 30000, + "workSimulationTimeMs" : 50, + "lockTimeoutMs" : 30000, + "lockAcquisitionTimeoutMs" : 60000, + "lockResourceName" : "benchmark-fair-lock", + "multiLockResourceCount" : 5 + }, + "results" : [ { + "implementationType" : "redisson", + "clientCount" : 5, + "totalSuccessfulOps" : 3282, + "totalFailedOps" : 0, + "successRatePct" : 100.0, + "aggregateOpsPerSecond" : 54.713033579214894, + "avgOpsPerSecondPerClient" : 10.94260671584298, + "opsPerSecondPerClientStdev" : 0.8151793252318069, + "opsPerSecondPerClientCi95Half" : 0.714536182947726, + "avgWaitTimeMs" : 37.87120533518565, + "avgHoldTimeMs" : 50.00261423307209, + "totalCorrectnessViolations" : 0, + "totalFifoViolations" : 0, + "correct" : true, + "latencyPercentilesMicros" : { + "p99" : 386491, + "max" : 734851, + "p90" : 122490, + "mean" : 37870, + "p50" : 1002, + "p95" : 197197, + "p75" : 44403 + }, + "validation" : { + "totalEvents" : 4925, + "uniqueClients" : 0, + "concurrentHolderViolations" : 0, + "fifoViolations" : 0, + "correct" : true + } + }, { + "implementationType" : "redlock4j-singlenode", + "clientCount" : 5, + "totalSuccessfulOps" : 5465, + "totalFailedOps" : 0, + "successRatePct" : 100.0, + "aggregateOpsPerSecond" : 91.09396096211225, + "avgOpsPerSecondPerClient" : 18.21879219242245, + "opsPerSecondPerClientStdev" : 0.0, + "opsPerSecondPerClientCi95Half" : 0.0, + "avgWaitTimeMs" : 0.8263092237877402, + "avgHoldTimeMs" : 50.00515131546203, + "totalCorrectnessViolations" : 0, + "totalFifoViolations" : 0, + "correct" : true, + "latencyPercentilesMicros" : { + "p99" : 2210, + "max" : 12744, + "p90" : 1216, + "mean" : 825, + "p50" : 733, + "p95" : 1409, + "p75" : 951 + }, + "validation" : { + "totalEvents" : 8170, + "uniqueClients" : 0, + "concurrentHolderViolations" : 0, + "fifoViolations" : 0, + "correct" : true + } + }, { + "implementationType" : "redlock4j", + "clientCount" : 5, + "totalSuccessfulOps" : 5251, + "totalFailedOps" : 0, + "successRatePct" : 100.0, + "aggregateOpsPerSecond" : 87.50354114521419, + "avgOpsPerSecondPerClient" : 17.500708229042836, + "opsPerSecondPerClientStdev" : 0.007455292459396976, + "opsPerSecondPerClientCi95Half" : 0.006534851966690465, + "avgWaitTimeMs" : 1.838960574248199, + "avgHoldTimeMs" : 50.01362019524353, + "totalCorrectnessViolations" : 0, + "totalFifoViolations" : 0, + "correct" : true, + "latencyPercentilesMicros" : { + "p99" : 4199, + "max" : 13774, + "p90" : 2466, + "mean" : 1837, + "p50" : 1767, + "p95" : 2802, + "p75" : 2098 + }, + "validation" : { + "totalEvents" : 7882, + "uniqueClients" : 0, + "concurrentHolderViolations" : 0, + "fifoViolations" : 0, + "correct" : true + } + } ] +} \ No newline at end of file diff --git a/redlock4j-benchmark/semaphore-benchmark-results.md b/redlock4j-benchmark/semaphore-benchmark-results.md index 61e38c4..e4443fb 100644 --- a/redlock4j-benchmark/semaphore-benchmark-results.md +++ b/redlock4j-benchmark/semaphore-benchmark-results.md @@ -1,13 +1,13 @@ -# Fair Lock Benchmark Results +# Semaphore Benchmark Results -**Generated:** 2026-04-21 01:42:24 +**Generated:** 2026-06-15 19:10:27 ## Configuration | Parameter | Value | |-----------|-------| | Redis Nodes | 3 | -| Clients per Implementation | 4 | +| Clients per Implementation | 5 | | Benchmark Duration | 1 minutes | | Work Simulation Time | 50 ms | | Lock Timeout | 30 s | @@ -16,26 +16,26 @@ | Metric | redisson | redlock4j-singlenode | redlock4j | |--------|--------|--------|--------| -| Total Ops/s | 36.62 | 72.94 | 70.17 | -| Avg Ops/s/Client | 9.16 | 18.24 | 17.54 | -| Successful Ops | 2,200 | 4,380 | 4,212 | +| Total Ops/s | 54.71 | 91.09 | 87.50 | +| Avg Ops/s/Client (95% CI) | 10.94 ± 0.71 | 18.22 ± 0.00 | 17.50 ± 0.01 | +| Successful Ops | 3,282 | 5,465 | 5,251 | | Failed Ops | 0 | 0 | 0 | | Success Rate | 100.00% | 100.00% | 100.00% | -| Avg Wait Time | 55.71 ms | 0.78 ms | 1.70 ms | +| Avg Wait Time | 37.87 ms | 0.83 ms | 1.84 ms | | Correctness | PASS | PASS | PASS | ## Latency Percentiles (microseconds) | Percentile | redisson | redlock4j-singlenode | redlock4j | |------------|--------|--------|--------| -| p50 | 986 | 750 | 1,662 | -| p75 | 28,597 | 912 | 1,789 | -| p90 | 192,859 | 1,023 | 1,959 | -| p95 | 344,472 | 1,122 | 2,235 | -| p99 | 614,249 | 1,441 | 3,024 | +| p50 | 1,002 | 733 | 1,767 | +| p75 | 44,403 | 951 | 2,098 | +| p90 | 122,490 | 1,216 | 2,466 | +| p95 | 197,197 | 1,409 | 2,802 | +| p99 | 386,491 | 2,210 | 4,199 | | p999 | N/A | N/A | N/A | -| max | 1,022,297 | 5,403 | 6,445 | -| mean | 55,706 | 778 | 1,696 | +| max | 734,851 | 12,744 | 13,774 | +| mean | 37,870 | 825 | 1,837 | ## Correctness Validation @@ -44,25 +44,25 @@ - **Status:** PASSED - **Correctness Violations:** 0 - **FIFO Violations:** 0 -- **Lock Events Analyzed:** 2200 +- **Lock Events Analyzed:** 4925 ### redlock4j-singlenode - **Status:** PASSED - **Correctness Violations:** 0 - **FIFO Violations:** 0 -- **Lock Events Analyzed:** 4380 +- **Lock Events Analyzed:** 8170 ### redlock4j - **Status:** PASSED - **Correctness Violations:** 0 - **FIFO Violations:** 0 -- **Lock Events Analyzed:** 4212 +- **Lock Events Analyzed:** 7882 ## Analysis -**Highest Throughput:** redlock4j-singlenode with 72.94 ops/s +**Highest Throughput:** redlock4j-singlenode with 91.09 ops/s -**Lowest Latency:** redlock4j-singlenode with 0.78 ms average wait time +**Lowest Latency:** redlock4j-singlenode with 0.83 ms average wait time diff --git a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/CountDownLatchBenchmarkMain.java b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/CountDownLatchBenchmarkMain.java index 82b78f4..c57d237 100644 --- a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/CountDownLatchBenchmarkMain.java +++ b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/CountDownLatchBenchmarkMain.java @@ -53,7 +53,7 @@ public static void main(String[] args) { } MarkdownReportGenerator reportGenerator = new MarkdownReportGenerator(); - String report = reportGenerator.generate(config, allResults); + String report = reportGenerator.generate("CountDownLatch", config, allResults); Path outputPath = Paths.get("countdownlatch-benchmark-results.md"); try { @@ -64,6 +64,15 @@ public static void main(String[] args) { logger.error("Failed to write report: {}", e.getMessage()); } + JsonReportGenerator jsonGenerator = new JsonReportGenerator(); + String json = jsonGenerator.generate("CountDownLatch", config, allResults); + Path jsonOutputPath = Paths.get("countdownlatch-benchmark-results.json"); + try { + jsonGenerator.writeToFile(json, jsonOutputPath); + } catch (Exception e) { + logger.error("Failed to write JSON report: {}", e.getMessage()); + } + System.out.println("\n" + report); } diff --git a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/DistributedLockBenchmarkMain.java b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/DistributedLockBenchmarkMain.java index d1d6a87..2effb45 100644 --- a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/DistributedLockBenchmarkMain.java +++ b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/DistributedLockBenchmarkMain.java @@ -75,7 +75,7 @@ public static void main(String[] args) { } MarkdownReportGenerator reportGenerator = new MarkdownReportGenerator(); - String report = reportGenerator.generate(config, allResults); + String report = reportGenerator.generate("Distributed Lock", config, allResults); Path outputPath = Paths.get("distributed-lock-benchmark-results.md"); try { @@ -87,6 +87,15 @@ public static void main(String[] args) { System.out.println(report); } + JsonReportGenerator jsonGenerator = new JsonReportGenerator(); + String json = jsonGenerator.generate("Distributed Lock", config, allResults); + Path jsonOutputPath = Paths.get("distributed-lock-benchmark-results.json"); + try { + jsonGenerator.writeToFile(json, jsonOutputPath); + } catch (Exception e) { + logger.error("Failed to write JSON report: {}", e.getMessage()); + } + System.out.println("\n" + report); } diff --git a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/FairLockBenchmarkMain.java b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/FairLockBenchmarkMain.java index 07e14c8..cf922ac 100644 --- a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/FairLockBenchmarkMain.java +++ b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/FairLockBenchmarkMain.java @@ -68,9 +68,9 @@ public static void main(String[] args) { // Generate and write report MarkdownReportGenerator reportGenerator = new MarkdownReportGenerator(); - String report = reportGenerator.generate(config, allResults); + String report = reportGenerator.generate("Fair Lock", config, allResults); - Path outputPath = Paths.get("benchmark-results.md"); + Path outputPath = Paths.get("fairlock-benchmark-results.md"); try { reportGenerator.writeToFile(report, outputPath); logger.info("\n========== BENCHMARK COMPLETE =========="); @@ -80,6 +80,15 @@ public static void main(String[] args) { System.out.println(report); // Print to console as fallback } + JsonReportGenerator jsonGenerator = new JsonReportGenerator(); + String json = jsonGenerator.generate("Fair Lock", config, allResults); + Path jsonOutputPath = Paths.get("fairlock-benchmark-results.json"); + try { + jsonGenerator.writeToFile(json, jsonOutputPath); + } catch (Exception e) { + logger.error("Failed to write JSON report: {}", e.getMessage()); + } + // Print summary to console System.out.println("\n" + report); } diff --git a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/MultiLockBenchmarkMain.java b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/MultiLockBenchmarkMain.java index 35b0011..d88991e 100644 --- a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/MultiLockBenchmarkMain.java +++ b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/MultiLockBenchmarkMain.java @@ -62,7 +62,7 @@ public static void main(String[] args) { // Generate report MarkdownReportGenerator reportGenerator = new MarkdownReportGenerator(); - String report = reportGenerator.generate(config, allResults); + String report = reportGenerator.generate("MultiLock", config, allResults); Path outputPath = Paths.get("multilock-benchmark-results.md"); try { @@ -74,6 +74,15 @@ public static void main(String[] args) { System.out.println(report); } + JsonReportGenerator jsonGenerator = new JsonReportGenerator(); + String json = jsonGenerator.generate("MultiLock", config, allResults); + Path jsonOutputPath = Paths.get("multilock-benchmark-results.json"); + try { + jsonGenerator.writeToFile(json, jsonOutputPath); + } catch (Exception e) { + logger.error("Failed to write JSON report: {}", e.getMessage()); + } + System.out.println("\n" + report); } diff --git a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/ReadWriteLockBenchmarkMain.java b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/ReadWriteLockBenchmarkMain.java index 0abfd9f..f7ab34e 100644 --- a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/ReadWriteLockBenchmarkMain.java +++ b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/ReadWriteLockBenchmarkMain.java @@ -48,22 +48,26 @@ public static void main(String[] args) { logger.info("\n========== REDISSON READWRITELOCK (SINGLE NODE) ==========\n"); List redissonResults = scenario.run(RedissonReadWriteLockClient::new); CorrectnessValidator.ValidationResult redissonValidation = validator.validate(resultsStore); - allResults.add(aggregator.aggregate(redissonResults, redissonValidation)); + allResults.addAll(aggregator.aggregateByImplementationType(redissonResults, redissonValidation)); logger.info("\n========== REDLOCK4J READWRITELOCK SINGLE-NODE MODE ==========\n"); List singleNodeResults = scenario.run(Redlock4jSingleNodeReadWriteLockClient::new); CorrectnessValidator.ValidationResult singleNodeValidation = validator.validate(resultsStore); - allResults.add(aggregator.aggregate(singleNodeResults, singleNodeValidation)); + allResults.addAll(aggregator.aggregateByImplementationType(singleNodeResults, singleNodeValidation)); logger.info("\n========== REDLOCK4J READWRITELOCK (3-NODE) ==========\n"); List multiNodeResults = scenario.run(Redlock4jReadWriteLockClient::new); CorrectnessValidator.ValidationResult multiNodeValidation = validator.validate(resultsStore); - allResults.add(aggregator.aggregate(multiNodeResults, multiNodeValidation)); + allResults.addAll(aggregator.aggregateByImplementationType(multiNodeResults, multiNodeValidation)); } } + // Group all readers first, then all writers (stable sort preserves impl order within each role) + allResults.sort((a, b) -> Boolean.compare( + a.implementationType.endsWith("-writer"), b.implementationType.endsWith("-writer"))); + MarkdownReportGenerator reportGenerator = new MarkdownReportGenerator(); - String report = reportGenerator.generate(config, allResults); + String report = reportGenerator.generate("ReadWriteLock", config, allResults); Path outputPath = Paths.get("rwlock-benchmark-results.md"); try { @@ -75,6 +79,15 @@ public static void main(String[] args) { System.out.println(report); } + JsonReportGenerator jsonGenerator = new JsonReportGenerator(); + String json = jsonGenerator.generate("ReadWriteLock", config, allResults); + Path jsonOutputPath = Paths.get("rwlock-benchmark-results.json"); + try { + jsonGenerator.writeToFile(json, jsonOutputPath); + } catch (Exception e) { + logger.error("Failed to write JSON report: {}", e.getMessage()); + } + System.out.println("\n" + report); } diff --git a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/SemaphoreBenchmarkMain.java b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/SemaphoreBenchmarkMain.java index 5166033..9f4bdb2 100644 --- a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/SemaphoreBenchmarkMain.java +++ b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/SemaphoreBenchmarkMain.java @@ -61,7 +61,7 @@ public static void main(String[] args) { } MarkdownReportGenerator reportGenerator = new MarkdownReportGenerator(); - String report = reportGenerator.generate(config, allResults); + String report = reportGenerator.generate("Semaphore", config, allResults); Path outputPath = Paths.get("semaphore-benchmark-results.md"); try { @@ -72,6 +72,15 @@ public static void main(String[] args) { logger.error("Failed to write report: {}", e.getMessage()); } + JsonReportGenerator jsonGenerator = new JsonReportGenerator(); + String json = jsonGenerator.generate("Semaphore", config, allResults); + Path jsonOutputPath = Paths.get("semaphore-benchmark-results.json"); + try { + jsonGenerator.writeToFile(json, jsonOutputPath); + } catch (Exception e) { + logger.error("Failed to write JSON report: {}", e.getMessage()); + } + System.out.println("\n" + report); } diff --git a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/AbstractDistributedLockClient.java b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/AbstractDistributedLockClient.java index da9cf83..9795d4a 100644 --- a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/AbstractDistributedLockClient.java +++ b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/AbstractDistributedLockClient.java @@ -28,12 +28,23 @@ public BenchmarkResult runBenchmark(String clientId, BenchmarkConfiguration conf Lock lock = getLock(config.getLockResourceName()); - long endTime = System.currentTimeMillis() + config.getBenchmarkDuration().toMillis(); + long now = System.currentTimeMillis(); + long warmupEnd = now + config.getWarmupDuration().toMillis(); + long endTime = warmupEnd + config.getBenchmarkDuration().toMillis(); long workTimeNanos = config.getWorkSimulationTime().toNanos(); + boolean inWarmup = config.getWarmupDuration().toMillis() > 0; - logger.info("Client {} ({}) starting distributed lock benchmark", clientId, getImplementationType()); + logger.info("Client {} ({}) starting distributed lock benchmark (warmup {}s + measure {}s)", + clientId, getImplementationType(), + config.getWarmupDuration().getSeconds(), config.getBenchmarkDuration().getSeconds()); while (System.currentTimeMillis() < endTime && !Thread.currentThread().isInterrupted()) { + if (inWarmup && System.currentTimeMillis() >= warmupEnd) { + inWarmup = false; + waitTimes.clear(); + result.reset(); + logger.info("Client {} warmup complete, beginning measurement", clientId); + } long waitStartNanos = System.nanoTime(); boolean acquired = false; diff --git a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/AbstractFairLockClient.java b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/AbstractFairLockClient.java index e98a006..969a03c 100644 --- a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/AbstractFairLockClient.java +++ b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/AbstractFairLockClient.java @@ -28,12 +28,23 @@ public BenchmarkResult runBenchmark(String clientId, BenchmarkConfiguration conf List waitTimes = new ArrayList<>(); Lock fairLock = getFairLock(config.getLockResourceName()); - long endTime = System.currentTimeMillis() + config.getBenchmarkDuration().toMillis(); + long now = System.currentTimeMillis(); + long warmupEnd = now + config.getWarmupDuration().toMillis(); + long endTime = warmupEnd + config.getBenchmarkDuration().toMillis(); long workTimeNanos = config.getWorkSimulationTime().toNanos(); + boolean inWarmup = config.getWarmupDuration().toMillis() > 0; - logger.info("Client {} ({}) starting benchmark", clientId, getImplementationType()); + logger.info("Client {} ({}) starting benchmark (warmup {}s + measure {}s)", + clientId, getImplementationType(), + config.getWarmupDuration().getSeconds(), config.getBenchmarkDuration().getSeconds()); while (System.currentTimeMillis() < endTime && !Thread.currentThread().isInterrupted()) { + if (inWarmup && System.currentTimeMillis() >= warmupEnd) { + inWarmup = false; + waitTimes.clear(); + result.reset(); + logger.info("Client {} warmup complete, beginning measurement", clientId); + } long waitStartNanos = System.nanoTime(); boolean acquired = false; diff --git a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/AbstractMultiLockClient.java b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/AbstractMultiLockClient.java index 9f578ea..af0d62d 100644 --- a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/AbstractMultiLockClient.java +++ b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/AbstractMultiLockClient.java @@ -29,14 +29,24 @@ public BenchmarkResult runBenchmark(String clientId, BenchmarkConfiguration conf // Generate resource names for multi-lock (e.g., account:1, account:2, account:3) List resourceNames = generateResourceNames(config.getMultiLockResourceCount()); Lock multiLock = getMultiLock(resourceNames); - - long endTime = System.currentTimeMillis() + config.getBenchmarkDuration().toMillis(); + + long now = System.currentTimeMillis(); + long warmupEnd = now + config.getWarmupDuration().toMillis(); + long endTime = warmupEnd + config.getBenchmarkDuration().toMillis(); long workTimeNanos = config.getWorkSimulationTime().toNanos(); + boolean inWarmup = config.getWarmupDuration().toMillis() > 0; - logger.info("Client {} ({}) starting multi-lock benchmark with {} resources", - clientId, getImplementationType(), resourceNames.size()); + logger.info("Client {} ({}) starting multi-lock benchmark with {} resources (warmup {}s + measure {}s)", + clientId, getImplementationType(), resourceNames.size(), + config.getWarmupDuration().getSeconds(), config.getBenchmarkDuration().getSeconds()); while (System.currentTimeMillis() < endTime && !Thread.currentThread().isInterrupted()) { + if (inWarmup && System.currentTimeMillis() >= warmupEnd) { + inWarmup = false; + waitTimes.clear(); + result.reset(); + logger.info("Client {} warmup complete, beginning measurement", clientId); + } long waitStartNanos = System.nanoTime(); boolean acquired = false; diff --git a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/AbstractReadWriteLockClient.java b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/AbstractReadWriteLockClient.java index c3763f0..b110e18 100644 --- a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/AbstractReadWriteLockClient.java +++ b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/AbstractReadWriteLockClient.java @@ -32,12 +32,23 @@ public BenchmarkResult runBenchmark(String clientId, BenchmarkConfiguration conf ReadWriteLock rwLock = getReadWriteLock("benchmark-rwlock"); Lock lock = isWriter ? rwLock.writeLock() : rwLock.readLock(); - long endTime = System.currentTimeMillis() + config.getBenchmarkDuration().toMillis(); + long now = System.currentTimeMillis(); + long warmupEnd = now + config.getWarmupDuration().toMillis(); + long endTime = warmupEnd + config.getBenchmarkDuration().toMillis(); long workTimeNanos = config.getWorkSimulationTime().toNanos(); + boolean inWarmup = config.getWarmupDuration().toMillis() > 0; - logger.info("Client {} ({}) starting as {}", clientId, getImplementationType(), role); + logger.info("Client {} ({}) starting as {} (warmup {}s + measure {}s)", + clientId, getImplementationType(), role, + config.getWarmupDuration().getSeconds(), config.getBenchmarkDuration().getSeconds()); while (System.currentTimeMillis() < endTime && !Thread.currentThread().isInterrupted()) { + if (inWarmup && System.currentTimeMillis() >= warmupEnd) { + inWarmup = false; + waitTimes.clear(); + result.reset(); + logger.info("Client {} warmup complete, beginning measurement", clientId); + } long waitStartNanos = System.nanoTime(); boolean acquired = false; diff --git a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/AbstractSemaphoreClient.java b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/AbstractSemaphoreClient.java index 06c64e4..f34e850 100644 --- a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/AbstractSemaphoreClient.java +++ b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/AbstractSemaphoreClient.java @@ -25,12 +25,23 @@ public BenchmarkResult runBenchmark(String clientId, BenchmarkConfiguration conf BenchmarkResult result = new BenchmarkResult(clientId, getImplementationType()); List waitTimes = new ArrayList<>(); - long endTime = System.currentTimeMillis() + config.getBenchmarkDuration().toMillis(); + long now = System.currentTimeMillis(); + long warmupEnd = now + config.getWarmupDuration().toMillis(); + long endTime = warmupEnd + config.getBenchmarkDuration().toMillis(); long workTimeNanos = config.getWorkSimulationTime().toNanos(); + boolean inWarmup = config.getWarmupDuration().toMillis() > 0; - logger.info("Client {} ({}) starting semaphore benchmark", clientId, getImplementationType()); + logger.info("Client {} ({}) starting semaphore benchmark (warmup {}s + measure {}s)", + clientId, getImplementationType(), + config.getWarmupDuration().getSeconds(), config.getBenchmarkDuration().getSeconds()); while (System.currentTimeMillis() < endTime && !Thread.currentThread().isInterrupted()) { + if (inWarmup && System.currentTimeMillis() >= warmupEnd) { + inWarmup = false; + waitTimes.clear(); + result.reset(); + logger.info("Client {} warmup complete, beginning measurement", clientId); + } long waitStartNanos = System.nanoTime(); boolean acquired = false; diff --git a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/Redlock4jJedisFairLockClient.java b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/Redlock4jJedisFairLockClient.java index 8861c27..ac7338a 100644 --- a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/Redlock4jJedisFairLockClient.java +++ b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/Redlock4jJedisFairLockClient.java @@ -36,7 +36,8 @@ public void initialize(BenchmarkConfiguration config, RedisClusterManager cluste .defaultLockTimeout(config.getLockTimeout()) .lockAcquisitionTimeout(config.getLockAcquisitionTimeout()) .retryDelay(Duration.ofMillis(50)) - .maxRetryAttempts(1000); // High retry count for long benchmarks + .maxRetryAttempts(1000) // High retry count for long benchmarks + .usePolling(); // FairLock recommended; avoids keyspace-notification overhead for (RedisClusterManager.RedisNodeInfo node : nodes) { configBuilder.addRedisNode(node.getHost(), node.getPort()); diff --git a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/Redlock4jLettuceFairLockClient.java b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/Redlock4jLettuceFairLockClient.java index dc8af9c..7bc9adf 100644 --- a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/Redlock4jLettuceFairLockClient.java +++ b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/Redlock4jLettuceFairLockClient.java @@ -36,7 +36,8 @@ public void initialize(BenchmarkConfiguration config, RedisClusterManager cluste .defaultLockTimeout(config.getLockTimeout()) .lockAcquisitionTimeout(config.getLockAcquisitionTimeout()) .retryDelay(Duration.ofMillis(50)) - .maxRetryAttempts(1000); // High retry count for long benchmarks + .maxRetryAttempts(1000) // High retry count for long benchmarks + .usePolling(); // FairLock recommended; avoids keyspace-notification overhead for (RedisClusterManager.RedisNodeInfo node : nodes) { configBuilder.addRedisNode(node.getHost(), node.getPort()); diff --git a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/Redlock4jSingleNodeFairLockClient.java b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/Redlock4jSingleNodeFairLockClient.java index 14cc91c..71f8a03 100644 --- a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/Redlock4jSingleNodeFairLockClient.java +++ b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/client/Redlock4jSingleNodeFairLockClient.java @@ -40,6 +40,7 @@ public void initialize(BenchmarkConfiguration config, RedisClusterManager cluste .lockAcquisitionTimeout(config.getLockAcquisitionTimeout()) .retryDelay(Duration.ofMillis(50)) .maxRetryAttempts(1000) + .usePolling() // FairLock recommended; avoids keyspace-notification overhead .build(); try { diff --git a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/infrastructure/BenchmarkResult.java b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/infrastructure/BenchmarkResult.java index 232fd4e..f5e2951 100644 --- a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/infrastructure/BenchmarkResult.java +++ b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/infrastructure/BenchmarkResult.java @@ -16,7 +16,7 @@ public class BenchmarkResult { private final String clientId; private final String implementationType; - private final Instant startTime; + private volatile Instant startTime; private volatile Instant endTime; private final AtomicLong successfulLockAcquisitions = new AtomicLong(0); @@ -25,7 +25,7 @@ public class BenchmarkResult { private final AtomicLong totalLockWaitTimeNanos = new AtomicLong(0); private final AtomicLong correctnessViolations = new AtomicLong(0); private final AtomicLong fifoViolations = new AtomicLong(0); - + private final Map latencyPercentiles = new ConcurrentHashMap<>(); public BenchmarkResult(String clientId, String implementationType) { @@ -34,6 +34,23 @@ public BenchmarkResult(String clientId, String implementationType) { this.startTime = Instant.now(); } + /** + * Resets all accumulated counters and restarts the measurement clock. + * Intended for use at the warmup/measurement boundary so only post-warmup + * operations contribute to the reported metrics. + */ + public void reset() { + successfulLockAcquisitions.set(0); + failedLockAcquisitions.set(0); + totalLockHoldTimeNanos.set(0); + totalLockWaitTimeNanos.set(0); + correctnessViolations.set(0); + fifoViolations.set(0); + latencyPercentiles.clear(); + this.endTime = null; + this.startTime = Instant.now(); + } + public void recordLockAcquisition(boolean success, long waitTimeNanos, long holdTimeNanos) { if (success) { successfulLockAcquisitions.incrementAndGet(); diff --git a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/report/BenchmarkResultsAggregator.java b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/report/BenchmarkResultsAggregator.java index 89561dc..6e2f3ea 100644 --- a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/report/BenchmarkResultsAggregator.java +++ b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/report/BenchmarkResultsAggregator.java @@ -29,6 +29,8 @@ public static class AggregatedResult { public final long totalFailedOps; public final double aggregateOpsPerSecond; public final double avgOpsPerSecondPerClient; + public final double opsPerSecondPerClientStdev; + public final double opsPerSecondPerClientCi95Half; public final double avgWaitTimeMs; public final double avgHoldTimeMs; public final long totalCorrectnessViolations; @@ -38,6 +40,7 @@ public static class AggregatedResult { public AggregatedResult(String implementationType, int clientCount, long totalSuccessfulOps, long totalFailedOps, double aggregateOpsPerSecond, double avgOpsPerSecondPerClient, + double opsPerSecondPerClientStdev, double opsPerSecondPerClientCi95Half, double avgWaitTimeMs, double avgHoldTimeMs, long totalCorrectnessViolations, long totalFifoViolations, Map aggregatedLatencyPercentiles, ValidationResult validationResult) { @@ -47,6 +50,8 @@ public AggregatedResult(String implementationType, int clientCount, long totalSu this.totalFailedOps = totalFailedOps; this.aggregateOpsPerSecond = aggregateOpsPerSecond; this.avgOpsPerSecondPerClient = avgOpsPerSecondPerClient; + this.opsPerSecondPerClientStdev = opsPerSecondPerClientStdev; + this.opsPerSecondPerClientCi95Half = opsPerSecondPerClientCi95Half; this.avgWaitTimeMs = avgWaitTimeMs; this.avgHoldTimeMs = avgHoldTimeMs; this.totalCorrectnessViolations = totalCorrectnessViolations; @@ -86,6 +91,9 @@ public AggregatedResult aggregate(List results, ValidationResul long violations = results.stream().mapToLong(BenchmarkResult::getCorrectnessViolations).sum(); long fifoViols = results.stream().mapToLong(BenchmarkResult::getFifoViolations).sum(); + double stdev = sampleStdev(results, avgOps); + double ci95Half = clientCount > 1 ? 1.96 * stdev / Math.sqrt(clientCount) : 0.0; + // Aggregate latency percentiles (average across clients) Map aggregatedPercentiles = aggregatePercentiles(results); @@ -93,7 +101,40 @@ public AggregatedResult aggregate(List results, ValidationResul implType, clientCount, String.format("%.2f", totalOps), violations); return new AggregatedResult(implType, clientCount, totalSuccessful, totalFailed, - totalOps, avgOps, avgWait, avgHold, violations, fifoViols, aggregatedPercentiles, validationResult); + totalOps, avgOps, stdev, ci95Half, avgWait, avgHold, violations, fifoViols, + aggregatedPercentiles, validationResult); + } + + private double sampleStdev(List results, double mean) { + int n = results.size(); + if (n < 2) return 0.0; + double sumSq = 0; + for (BenchmarkResult r : results) { + double d = r.getOpsPerSecond() - mean; + sumSq += d * d; + } + return Math.sqrt(sumSq / (n - 1)); + } + + /** + * Aggregates results grouped by implementation type. Use when a single benchmark run produces + * multiple roles per implementation (e.g. RWLock readers and writers) that must be reported separately. + */ + public List aggregateByImplementationType(List results, + ValidationResult validationResult) { + if (results.isEmpty()) { + throw new IllegalArgumentException("No results to aggregate"); + } + + Map> grouped = results.stream() + .collect(Collectors.groupingBy(BenchmarkResult::getImplementationType, + LinkedHashMap::new, Collectors.toList())); + + List aggregated = new ArrayList<>(grouped.size()); + for (List group : grouped.values()) { + aggregated.add(aggregate(group, validationResult)); + } + return aggregated; } /** @@ -107,6 +148,8 @@ public AggregatedResult aggregateSingle(BenchmarkResult result) { result.getFailedLockAcquisitions(), result.getOpsPerSecond(), result.getOpsPerSecond(), + 0.0, + 0.0, result.getAverageWaitTimeMs(), result.getAverageHoldTimeMs(), result.getCorrectnessViolations(), diff --git a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/report/JsonReportGenerator.java b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/report/JsonReportGenerator.java new file mode 100644 index 0000000..d94faf6 --- /dev/null +++ b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/report/JsonReportGenerator.java @@ -0,0 +1,108 @@ +/* + * SPDX-License-Identifier: MIT + * Copyright (c) 2025 Codarama + */ +package org.codarama.redlock4j.benchmark.report; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.codarama.redlock4j.benchmark.infrastructure.BenchmarkConfiguration; +import org.codarama.redlock4j.benchmark.report.BenchmarkResultsAggregator.AggregatedResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Emits machine-readable JSON output for a benchmark run alongside the markdown report. + */ +public class JsonReportGenerator { + + private static final Logger logger = LoggerFactory.getLogger(JsonReportGenerator.class); + + private final ObjectMapper mapper; + + public JsonReportGenerator() { + this.mapper = new ObjectMapper() + .registerModule(new JavaTimeModule()) + .enable(SerializationFeature.INDENT_OUTPUT) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + } + + public String generate(String title, BenchmarkConfiguration config, List results) { + Map root = new LinkedHashMap<>(); + root.put("title", title); + root.put("generatedAt", Instant.now().toString()); + root.put("config", configToMap(config)); + + List> resultMaps = new ArrayList<>(results.size()); + for (AggregatedResult r : results) { + resultMaps.add(resultToMap(r)); + } + root.put("results", resultMaps); + + try { + return mapper.writeValueAsString(root); + } catch (IOException e) { + throw new RuntimeException("Failed to serialize benchmark JSON report", e); + } + } + + public void writeToFile(String content, Path outputPath) throws IOException { + Files.writeString(outputPath, content); + logger.info("JSON report written to: {}", outputPath); + } + + private Map configToMap(BenchmarkConfiguration c) { + Map m = new LinkedHashMap<>(); + m.put("redisNodeCount", c.getRedisNodeCount()); + m.put("clientCount", c.getClientCount()); + m.put("benchmarkDurationMs", c.getBenchmarkDuration().toMillis()); + m.put("warmupDurationMs", c.getWarmupDuration().toMillis()); + m.put("workSimulationTimeMs", c.getWorkSimulationTime().toMillis()); + m.put("lockTimeoutMs", c.getLockTimeout().toMillis()); + m.put("lockAcquisitionTimeoutMs", c.getLockAcquisitionTimeout().toMillis()); + m.put("lockResourceName", c.getLockResourceName()); + m.put("multiLockResourceCount", c.getMultiLockResourceCount()); + return m; + } + + private Map resultToMap(AggregatedResult r) { + Map m = new LinkedHashMap<>(); + m.put("implementationType", r.implementationType); + m.put("clientCount", r.clientCount); + m.put("totalSuccessfulOps", r.totalSuccessfulOps); + m.put("totalFailedOps", r.totalFailedOps); + m.put("successRatePct", r.getSuccessRate()); + m.put("aggregateOpsPerSecond", r.aggregateOpsPerSecond); + m.put("avgOpsPerSecondPerClient", r.avgOpsPerSecondPerClient); + m.put("opsPerSecondPerClientStdev", r.opsPerSecondPerClientStdev); + m.put("opsPerSecondPerClientCi95Half", r.opsPerSecondPerClientCi95Half); + m.put("avgWaitTimeMs", r.avgWaitTimeMs); + m.put("avgHoldTimeMs", r.avgHoldTimeMs); + m.put("totalCorrectnessViolations", r.totalCorrectnessViolations); + m.put("totalFifoViolations", r.totalFifoViolations); + m.put("correct", r.isCorrect()); + m.put("latencyPercentilesMicros", r.aggregatedLatencyPercentiles); + + if (r.validationResult != null) { + Map v = new LinkedHashMap<>(); + v.put("totalEvents", r.validationResult.getTotalEvents()); + v.put("uniqueClients", r.validationResult.getUniqueClients()); + v.put("concurrentHolderViolations", r.validationResult.getConcurrentHolderViolationCount()); + v.put("fifoViolations", r.validationResult.getFifoViolationCount()); + v.put("correct", r.validationResult.isCorrect()); + m.put("validation", v); + } + + return m; + } +} diff --git a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/report/MarkdownReportGenerator.java b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/report/MarkdownReportGenerator.java index c5665ce..85d9175 100644 --- a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/report/MarkdownReportGenerator.java +++ b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/report/MarkdownReportGenerator.java @@ -25,11 +25,11 @@ public class MarkdownReportGenerator { private static final Logger logger = LoggerFactory.getLogger(MarkdownReportGenerator.class); - public String generate(BenchmarkConfiguration config, List results) { + public String generate(String title, BenchmarkConfiguration config, List results) { StringBuilder sb = new StringBuilder(); String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); - sb.append("# Fair Lock Benchmark Results\n\n"); + sb.append("# ").append(title).append(" Benchmark Results\n\n"); sb.append("**Generated:** ").append(timestamp).append("\n\n"); // Configuration Section @@ -54,7 +54,9 @@ public String generate(BenchmarkConfiguration config, List res sb.append("\n"); addRow(sb, "Total Ops/s", results, r -> String.format("%.2f", r.aggregateOpsPerSecond)); - addRow(sb, "Avg Ops/s/Client", results, r -> String.format("%.2f", r.avgOpsPerSecondPerClient)); + addRow(sb, "Avg Ops/s/Client (95% CI)", results, r -> r.clientCount > 1 + ? String.format("%.2f \u00b1 %.2f", r.avgOpsPerSecondPerClient, r.opsPerSecondPerClientCi95Half) + : String.format("%.2f", r.avgOpsPerSecondPerClient)); addRow(sb, "Successful Ops", results, r -> String.format("%,d", r.totalSuccessfulOps)); addRow(sb, "Failed Ops", results, r -> String.format("%,d", r.totalFailedOps)); addRow(sb, "Success Rate", results, r -> String.format("%.2f%%", r.getSuccessRate())); diff --git a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/scenario/CountDownLatchBenchmarkScenario.java b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/scenario/CountDownLatchBenchmarkScenario.java index e2563b1..8e6fdfe 100644 --- a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/scenario/CountDownLatchBenchmarkScenario.java +++ b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/scenario/CountDownLatchBenchmarkScenario.java @@ -12,7 +12,10 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -49,13 +52,23 @@ public BenchmarkResult run(Supplier clientSupplie List latencies = new ArrayList<>(); int workers = config.getClientCount(); - long endTime = System.currentTimeMillis() + config.getBenchmarkDuration().toMillis(); + long now = System.currentTimeMillis(); + long warmupEnd = now + config.getWarmupDuration().toMillis(); + long endTime = warmupEnd + config.getBenchmarkDuration().toMillis(); AtomicInteger completedLatches = new AtomicInteger(0); + boolean[] inWarmup = { config.getWarmupDuration().toMillis() > 0 }; ExecutorService executor = Executors.newFixedThreadPool(workers + 1); try { while (System.currentTimeMillis() < endTime) { + if (inWarmup[0] && System.currentTimeMillis() >= warmupEnd) { + inWarmup[0] = false; + latencies.clear(); + result.reset(); + completedLatches.set(0); + logger.info("CountDownLatch warmup complete for {}, beginning measurement", implType); + } String latchName = "latch-" + completedLatches.get(); long startTime = System.nanoTime(); @@ -98,8 +111,30 @@ public BenchmarkResult run(Supplier clientSupplie } result.complete(); + result.setLatencyPercentiles(calculatePercentiles(latencies)); logger.info("=== CountDownLatch benchmark for {} completed: {} latches ===", implType, completedLatches.get()); return result; } + + private Map calculatePercentiles(List values) { + Map percentiles = new HashMap<>(); + if (values.isEmpty()) return percentiles; + + Collections.sort(values); + int size = values.size(); + + percentiles.put("p50", values.get((int) (size * 0.50))); + percentiles.put("p75", values.get((int) (size * 0.75))); + percentiles.put("p90", values.get(Math.min((int) (size * 0.90), size - 1))); + percentiles.put("p95", values.get(Math.min((int) (size * 0.95), size - 1))); + percentiles.put("p99", values.get(Math.min((int) (size * 0.99), size - 1))); + percentiles.put("max", values.get(size - 1)); + + long sum = 0; + for (Long v : values) sum += v; + percentiles.put("mean", sum / size); + + return percentiles; + } } diff --git a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/scenario/DistributedLockBenchmarkScenario.java b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/scenario/DistributedLockBenchmarkScenario.java index 1ac9f36..41b0059 100644 --- a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/scenario/DistributedLockBenchmarkScenario.java +++ b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/scenario/DistributedLockBenchmarkScenario.java @@ -78,16 +78,15 @@ public List run(Supplier client }); } - logger.info("Warmup phase: {} seconds", config.getWarmupDuration().getSeconds()); - Thread.sleep(config.getWarmupDuration().toMillis()); - - logger.info("Starting benchmark execution..."); + logger.info("Starting benchmark execution (warmup {}s + measure {}s, clients do warmup-discard internally)...", + config.getWarmupDuration().getSeconds(), config.getBenchmarkDuration().getSeconds()); long startTime = System.currentTimeMillis(); startLatch.countDown(); + long totalDurationMs = config.getWarmupDuration().toMillis() + config.getBenchmarkDuration().toMillis(); while (!completeLatch.await(config.getReportingInterval().toMillis(), TimeUnit.MILLISECONDS)) { long elapsed = System.currentTimeMillis() - startTime; - long remaining = config.getBenchmarkDuration().toMillis() - elapsed; + long remaining = totalDurationMs - elapsed; logger.info("Progress: {} elapsed, {} remaining", formatDuration(elapsed), formatDuration(remaining)); } diff --git a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/scenario/FairLockBenchmarkScenario.java b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/scenario/FairLockBenchmarkScenario.java index 1bacbb5..d31f1fa 100644 --- a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/scenario/FairLockBenchmarkScenario.java +++ b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/scenario/FairLockBenchmarkScenario.java @@ -90,19 +90,16 @@ public List run(Supplier clientSupplie }); } - // Warmup phase - logger.info("Warmup phase: {} seconds", config.getWarmupDuration().getSeconds()); - Thread.sleep(config.getWarmupDuration().toMillis()); - - // Start all clients simultaneously - logger.info("Starting benchmark execution..."); + // Start all clients simultaneously; warmup-discard happens inside each client + logger.info("Starting benchmark execution (warmup {}s + measure {}s, clients do warmup-discard internally)...", + config.getWarmupDuration().getSeconds(), config.getBenchmarkDuration().getSeconds()); long startTime = System.currentTimeMillis(); startLatch.countDown(); - // Wait for completion with progress logging + long totalDurationMs = config.getWarmupDuration().toMillis() + config.getBenchmarkDuration().toMillis(); while (!completeLatch.await(config.getReportingInterval().toMillis(), TimeUnit.MILLISECONDS)) { long elapsed = System.currentTimeMillis() - startTime; - long remaining = config.getBenchmarkDuration().toMillis() - elapsed; + long remaining = totalDurationMs - elapsed; logger.info("Progress: {} elapsed, {} remaining", formatDuration(elapsed), formatDuration(remaining)); } diff --git a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/scenario/MultiLockBenchmarkScenario.java b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/scenario/MultiLockBenchmarkScenario.java index 14e8f9d..e14d36f 100644 --- a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/scenario/MultiLockBenchmarkScenario.java +++ b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/scenario/MultiLockBenchmarkScenario.java @@ -77,16 +77,15 @@ public List run(Supplier clientSuppli }); } - logger.info("Warmup phase: {} seconds", config.getWarmupDuration().getSeconds()); - Thread.sleep(config.getWarmupDuration().toMillis()); - - logger.info("Starting benchmark execution..."); + logger.info("Starting benchmark execution (warmup {}s + measure {}s, clients do warmup-discard internally)...", + config.getWarmupDuration().getSeconds(), config.getBenchmarkDuration().getSeconds()); long startTime = System.currentTimeMillis(); startLatch.countDown(); + long totalDurationMs = config.getWarmupDuration().toMillis() + config.getBenchmarkDuration().toMillis(); while (!completeLatch.await(config.getReportingInterval().toMillis(), TimeUnit.MILLISECONDS)) { long elapsed = System.currentTimeMillis() - startTime; - long remaining = config.getBenchmarkDuration().toMillis() - elapsed; + long remaining = totalDurationMs - elapsed; logger.info("Progress: {} elapsed, {} remaining", formatDuration(elapsed), formatDuration(remaining)); } diff --git a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/scenario/ReadWriteLockBenchmarkScenario.java b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/scenario/ReadWriteLockBenchmarkScenario.java index 9e7b0ad..097c8d0 100644 --- a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/scenario/ReadWriteLockBenchmarkScenario.java +++ b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/scenario/ReadWriteLockBenchmarkScenario.java @@ -86,16 +86,15 @@ public List run(Supplier clientSu }); } - logger.info("Warmup phase: {} seconds", config.getWarmupDuration().getSeconds()); - Thread.sleep(config.getWarmupDuration().toMillis()); - - logger.info("Starting benchmark execution..."); + logger.info("Starting benchmark execution (warmup {}s + measure {}s, clients do warmup-discard internally)...", + config.getWarmupDuration().getSeconds(), config.getBenchmarkDuration().getSeconds()); long startTime = System.currentTimeMillis(); startLatch.countDown(); + long totalDurationMs = config.getWarmupDuration().toMillis() + config.getBenchmarkDuration().toMillis(); while (!completeLatch.await(config.getReportingInterval().toMillis(), TimeUnit.MILLISECONDS)) { long elapsed = System.currentTimeMillis() - startTime; - long remaining = config.getBenchmarkDuration().toMillis() - elapsed; + long remaining = totalDurationMs - elapsed; logger.info("Progress: {} elapsed, {} remaining", formatDuration(elapsed), formatDuration(remaining)); } diff --git a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/scenario/SemaphoreBenchmarkScenario.java b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/scenario/SemaphoreBenchmarkScenario.java index f3d9759..01c105d 100644 --- a/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/scenario/SemaphoreBenchmarkScenario.java +++ b/redlock4j-benchmark/src/main/java/org/codarama/redlock4j/benchmark/scenario/SemaphoreBenchmarkScenario.java @@ -80,10 +80,8 @@ public List run(Supplier clientSuppli }); } - logger.info("Warmup phase: {} seconds", config.getWarmupDuration().getSeconds()); - Thread.sleep(config.getWarmupDuration().toMillis()); - - logger.info("Starting benchmark..."); + logger.info("Starting benchmark (warmup {}s + measure {}s, clients do warmup-discard internally)...", + config.getWarmupDuration().getSeconds(), config.getBenchmarkDuration().getSeconds()); long startTime = System.currentTimeMillis(); startLatch.countDown(); diff --git a/src/main/java/org/codarama/redlock4j/strategy/MultiNodeStrategy.java b/src/main/java/org/codarama/redlock4j/strategy/MultiNodeStrategy.java index 89b31ba..41ca4c0 100644 --- a/src/main/java/org/codarama/redlock4j/strategy/MultiNodeStrategy.java +++ b/src/main/java/org/codarama/redlock4j/strategy/MultiNodeStrategy.java @@ -13,7 +13,16 @@ import java.time.Duration; import java.time.Instant; +import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; /** @@ -35,6 +44,22 @@ public class MultiNodeStrategy implements LockExecutionStrategy { private static final Logger logger = LoggerFactory.getLogger(MultiNodeStrategy.class); + /** + * Shared executor for fanning out per-node Redis operations in parallel. Cached pool of daemon threads with idle + * timeout so it scales with concurrent acquisition load without blocking JVM shutdown. + */ + private static final Executor FANOUT_EXECUTOR = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, + new SynchronousQueue<>(), new ThreadFactory() { + private final AtomicInteger counter = new AtomicInteger(); + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r, "redlock4j-multinode-" + counter.incrementAndGet()); + t.setDaemon(true); + return t; + } + }); + private final List drivers; private final int quorum; private final double clockDriftFactor; @@ -62,17 +87,8 @@ public MultiNodeStrategy(List drivers, RedlockConfiguration config) @Override public LockResult acquireLock(String key, String value, long timeoutMs) { Instant startTime = Instant.now(); - int successfulNodes = 0; - for (RedisDriver driver : drivers) { - try { - if (driver.setIfNotExists(key, value, timeoutMs)) { - successfulNodes++; - } - } catch (RedisDriverException e) { - logger.warn("Failed to acquire lock on {}: {}", driver.getIdentifier(), e.getMessage()); - } - } + int successfulNodes = fanOut("acquire lock", driver -> driver.setIfNotExists(key, value, timeoutMs)); Duration elapsed = Duration.between(startTime, Instant.now()); long validityTime = calculateValidityTime(timeoutMs, elapsed.toMillis()); @@ -94,30 +110,19 @@ public LockResult acquireLock(String key, String value, long timeoutMs) { @Override public void releaseLock(String key, String value) { - for (RedisDriver driver : drivers) { - try { - driver.deleteIfValueMatches(key, value); - } catch (RedisDriverException e) { - logger.warn("Failed to release lock on {}: {}", driver.getIdentifier(), e.getMessage()); - } - } + fanOut("release lock", driver -> { + driver.deleteIfValueMatches(key, value); + return Boolean.TRUE; + }); logger.debug("Lock released on all nodes: key={}", key); } @Override public boolean extendLock(String key, String currentValue, long newTimeoutMs) { Instant startTime = Instant.now(); - int successfulNodes = 0; - for (RedisDriver driver : drivers) { - try { - if (driver.setIfValueMatches(key, currentValue, currentValue, newTimeoutMs)) { - successfulNodes++; - } - } catch (RedisDriverException e) { - logger.warn("Failed to extend lock on {}: {}", driver.getIdentifier(), e.getMessage()); - } - } + int successfulNodes = fanOut("extend lock", + driver -> driver.setIfValueMatches(key, currentValue, currentValue, newTimeoutMs)); Duration elapsed = Duration.between(startTime, Instant.now()); long validityTime = calculateValidityTime(newTimeoutMs, elapsed.toMillis()); @@ -137,20 +142,51 @@ public long calculateValidityTime(long timeoutMs, long elapsedMs) { @Override public int executeOnNodes(Function operation) { - int successCount = 0; + return fanOut("execute operation", operation::apply); + } + + /** + * Fans an operation out across all drivers in parallel and returns the count of nodes where the operation returned + * {@code Boolean.TRUE}. Per-node exceptions are logged and counted as failures so a single slow or down node cannot + * block the quorum. + */ + private int fanOut(String description, FanOutOperation operation) { + List> futures = new ArrayList<>(drivers.size()); for (RedisDriver driver : drivers) { + futures.add(CompletableFuture.supplyAsync(() -> { + try { + return Boolean.TRUE.equals(operation.apply(driver)); + } catch (Exception e) { + logger.warn("Failed to {} on {}: {}", description, driver.getIdentifier(), e.getMessage()); + return Boolean.FALSE; + } + }, FANOUT_EXECUTOR)); + } + + int successCount = 0; + for (CompletableFuture future : futures) { try { - Boolean result = operation.apply(driver); - if (Boolean.TRUE.equals(result)) { + if (Boolean.TRUE.equals(future.get())) { successCount++; } - } catch (Exception e) { - logger.warn("Error executing operation on {}: {}", driver.getIdentifier(), e.getMessage()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } catch (ExecutionException e) { + logger.warn("Unexpected error during {}: {}", description, e.getMessage()); } } return successCount; } + /** + * Functional interface for per-node operations that may throw checked Redis driver exceptions. + */ + @FunctionalInterface + private interface FanOutOperation { + Boolean apply(RedisDriver driver) throws RedisDriverException; + } + @Override public boolean isSuccessful(int successCount) { return successCount >= quorum; From 87ad8d01a9bf88c36185317529f5bea34a7570be Mon Sep 17 00:00:00 2001 From: Tihomir Mateev Date: Wed, 17 Jun 2026 11:48:19 +0300 Subject: [PATCH 2/3] Performance improvements to the distributed lock and RWLock --- README.md | 1 + redlock4j-benchmark/benchmark-analysis.md | 91 +++++---- .../distributed-lock-benchmark-results.json | 184 +++++++++--------- .../distributed-lock-benchmark-results.md | 42 ++-- .../fairlock-benchmark-results.json | 130 ++++++------- .../fairlock-benchmark-results.md | 38 ++-- .../rwlock-benchmark-results.json | 182 ++++++++--------- .../rwlock-benchmark-results.md | 40 ++-- .../Redlock4jDistributedLockClient.java | 3 + .../client/Redlock4jReadWriteLockClient.java | 3 + .../codarama/redlock4j/AbstractRedlock.java | 29 ++- .../java/org/codarama/redlock4j/FairLock.java | 2 +- .../org/codarama/redlock4j/MultiLock.java | 2 +- .../java/org/codarama/redlock4j/Redlock.java | 10 +- .../codarama/redlock4j/RedlockManager.java | 3 +- .../redlock4j/RedlockReadWriteLock.java | 14 +- .../codarama/redlock4j/RedlockSemaphore.java | 2 +- .../configuration/RedlockConfiguration.java | 103 ++++++++++ .../redlock4j/strategy/BackoffCalculator.java | 71 +++++++ .../redlock4j/strategy/LockWaitStrategy.java | 47 +++++ .../strategy/PollingWaitStrategy.java | 34 +++- .../redlock4j/strategy/WaitStrategy.java | 4 +- .../strategy/WaitStrategyFactory.java | 24 ++- .../RedlockConfigurationTest.java | 32 +++ .../strategy/BackoffCalculatorTest.java | 68 +++++++ .../strategy/PollingWaitStrategyTest.java | 30 +++ 26 files changed, 824 insertions(+), 365 deletions(-) create mode 100644 src/main/java/org/codarama/redlock4j/strategy/BackoffCalculator.java create mode 100644 src/test/java/org/codarama/redlock4j/strategy/BackoffCalculatorTest.java diff --git a/README.md b/README.md index 2636de6..3b09e15 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ [![Maven Central](https://img.shields.io/maven-central/v/org.codarama/redlock4j?versionSuffix=RELEASE)](https://maven-badges.herokuapp.com/maven-central/org.codarama/redlock4j) [![Javadocs](https://www.javadoc.io/badge/org.codarama/redlock4j.svg)](https://javadoc.io/doc/org.codarama/redlock4j) [![Java](https://img.shields.io/badge/Java-8%2B-blue.svg)](https://openjdk.java.net/) +[![Guide](https://img.shields.io/badge/mkdocs-guide-526CFE?logo=materialformkdocs&logoColor=white)](https://codarama.github.io/redlock4j/) > [!IMPORTANT] > This project is a personal project and is not currently affiliated or endorsed in any way with Redis or any other company. Use the software freely and at your own risk. diff --git a/redlock4j-benchmark/benchmark-analysis.md b/redlock4j-benchmark/benchmark-analysis.md index 5d174e4..b44527f 100644 --- a/redlock4j-benchmark/benchmark-analysis.md +++ b/redlock4j-benchmark/benchmark-analysis.md @@ -17,15 +17,15 @@ Source files analyzed: | ~~M4~~ | ~~`redlock4j-3node` distributed-lock numbers from a run where 5/36 attempts timed out (86% success) — throughput collapse may be partly run instability~~ | ~~`distributed-lock-benchmark-results.md:22`~~ | ~~re-run after fixes; add per-attempt logging~~ **DONE (re-measured §6.1/§7.1)** | | ~~M5~~ | ~~"Fair lock" comparison includes non-fair impls (`shedlock-lettuce`, `spring-integration`, `redpulsar`)~~ | ~~`distributed-lock-benchmark-results.md:31-37`~~ | ~~label fairness column; split into separate fair/non-fair runs~~ **DONE (separate `FairLockBenchmarkMain` / `DistributedLockBenchmarkMain`)** | -## 2. Per-primitive gaps vs competitors (updated 2026-06-16, post P0-1 + hygiene) +## 2. Per-primitive gaps vs competitors (updated 2026-06-16, post P0-1 + A1 backoff) | Primitive | Worst remaining gap | Number | Where redlock4j wins | |---|---|---|---| -| Distributed lock (3-node) | Throughput | **~58× slower** (0.38 vs 21.87 ops/s), 82 % success | **Best p99 in field** (407 ms) | -| Distributed lock (single-node) | p99 vs redisson | ~2.1× higher (3.14 s vs 1.49 s) | Throughput within 1 % of redisson | +| Distributed lock (3-node) | Throughput | **~27× slower** (0.81 vs 21.63 ops/s), 91 % success | **Best p99 in field** (858 ms vs redisson 1,287 ms) | +| Distributed lock (single-node) | p99 vs redisson | ~1.5× higher (1.98 s vs 1.29 s) | Throughput within 1 % of redisson | | MultiLock | — | parity / slight lead on throughput AND p99 | Leader on every axis (Ops/s 17.72, p99 1.06 s) | -| RWLock readers | Throughput vs redisson | ~2.9× slower (54 vs 154 ops/s) | — | -| RWLock writers | — | redisson starved (0.07 ops/s, 27 s wait) | Leader (17.26 ops/s, 91 ms p99 vs redisson 27 s) | +| RWLock readers | Throughput vs redisson | ~1.6× slower (95.4 vs 151.7 ops/s) | redlock4j-rwlock now beats single-node (+28 %) | +| RWLock writers | — | redisson starved (0.21 ops/s, 9.7 s wait) | Leader (16.42 ops/s, 104 ms p99 vs redisson 16.8 s) | | Semaphore | — | redlock4j ~1.7× faster than redisson, p99 ~100× lower | Clear lead | | CountDownLatch | — | parity (redlock4j slight lead on Ops/s + p99) | Marginal lead | | FairLock | Throughput vs redisson | ~1.4× slower (~12 vs 17 ops/s) — using polling fallback | Correctness/FIFO PASS | @@ -137,15 +137,15 @@ Same config as before (5 clients × 1 min × 50ms work × 3 nodes), now with par - All three within 3% on throughput. redlock4j slightly leads on median and p99. M2 fix (percentiles wired in) now visible. -### 6.6 Updated gap matrix +### 6.6 Updated gap matrix (post-A1 backoff) | Primitive | Prior gap | Current status | |---|---|---| -| Distributed lock (3-node) throughput | 36× slower | Still ~58× slower (0.38 vs 21.87 ops/s) — **polling wait strategy, not I/O, is now the bottleneck**. Pub/Sub-on-release (P0-4) is the unlock. | -| Distributed lock (3-node) p99 | uncomparable | **Best in class** (407ms) | -| MultiLock p99 | 12× worse | **Best in class** (1.06s) | -| RWLock reader throughput | unmeasured-pairwise | 3× behind redisson — needs Lua-script reader path | -| RWLock writer throughput | hidden by M3 | **220× better than redisson** (redisson starves writers) | +| Distributed lock (3-node) throughput | 58× slower (0.38 ops/s) | **~27× slower** (0.81 ops/s) — backoff halved the cliff; A3 (pub/sub-on-release) is the next unlock. | +| Distributed lock (3-node) p99 | uncomparable | **Best in class** (858 ms vs redisson 1,287 ms) | +| MultiLock p99 | 12× worse | **Best in class** (1.06 s) | +| RWLock reader throughput | 3× behind redisson | ~1.6× behind (95.4 vs 151.7 ops/s) — backoff added +77 %; further closure needs A2 Lua reader path. | +| RWLock writer throughput | hidden by M3 | **78× better than redisson** (16.42 vs 0.21 ops/s — redisson starves writers) | | Semaphore | already winning | Still winning, marginal improvement | | CountDownLatch | parity | Parity, marginal lead | @@ -165,18 +165,18 @@ Recommended order: **P1-5 (cheap, non-BC) -> P0-3 (BC, foundational) -> P0-4 (BC All runs: 3-node Redis (testcontainers), 1 min measurement, 30 s warmup-discard, 50 ms work simulation, lock timeout 30 s, 95 % CI on per-client mean. FairLock now uses `.usePolling()` to bypass keyspace-notification overhead. -### 7.1 Distributed Lock (5 clients) +### 7.1 Distributed Lock (5 clients, post-A1 backoff) | Impl | Ops/s | Succ % | Mean Wait | p99 (ms) | Notes | |---|---:|---:|---:|---:|---| -| **redpulsar** | **21.87** | 100% | 187 ms | 7,771 | throughput winner | -| spring-integration | 20.29 | 100% | 200 ms | 5,687 | | -| shedlock-lettuce | 18.94 | 100% | 230 ms | 3,598 | | -| redlock4j-singlenode | 18.33 | 100% | 407 ms | 3,142 | | -| redisson | 18.22 | 100% | 229 ms | 1,487 | | -| **redlock4j-3node** | **0.38** | 82% | 166 ms | **407** | best p99 / throughput collapse | +| **redpulsar** | **21.63** | 100% | 302 ms | 13,022 | throughput leader | +| spring-integration | 19.47 | 100% | 273 ms | 9,981 | | +| shedlock-lettuce | 18.86 | 100% | 217 ms | 3,022 | | +| redlock4j-singlenode | 18.33 | 100% | 251 ms | 1,977 | | +| redisson | 18.24 | 100% | 229 ms | 1,287 | | +| **redlock4j-3node** | **0.81** | 91% | 259 ms | **858** | **best p99** / throughput still capped | -3-node redlock4j has best-in-class p99 (407 ms) but only 23 ops in 60 s — bottleneck is the 50 ms `PollingWaitStrategy` under quorum contention. +3-node redlock4j now leads p99 (858 ms — ~1.5× better than redisson) and doubled throughput (0.38 → 0.81) after A1. Remaining throughput gap requires A3 (pub/sub-on-release) to eliminate polling entirely. ### 7.2 MultiLock (5 clients) @@ -188,15 +188,15 @@ All runs: 3-node Redis (testcontainers), 1 min measurement, 30 s warmup-discard, redlock4j wins every axis. -### 7.3 ReadWriteLock (10 clients, reader+writer split) +### 7.3 ReadWriteLock (10 clients, reader+writer split, post-A1 backoff) | Impl | Reader ops/s | Reader p99 (µs) | Writer ops/s | Writer p99 (µs) | |---|---:|---:|---:|---:| -| **redisson** | **153.62** | **3,098** | 0.07 | 26,977,794 (starved) | -| redlock4j-singlenode | 59.07 | 452,207 | **17.26** | 90,528 | -| redlock4j-rwlock | 53.59 | 431,941 | 15.51 | **72,728** | +| **redisson** | **151.65** | **5,823** | 0.21 | 16,820,725 (starved) | +| redlock4j-rwlock | 95.41 | 348,441 | **16.42** | **104,200** | +| redlock4j-singlenode | 74.66 | 340,626 | 15.63 | 755,568 | -Redisson dominates read-only (~2.6×) but starves writers (2 successes / 60 s). redlock4j balanced. +Redisson still leads read-only (~1.6×, down from 2.6× pre-backoff) but starves writers (6 successes / 60 s). redlock4j-rwlock now beats single-node on both reader throughput (+28 %) and writer p99 (~7× better). Redisson reader p99 microsecond-scale is from in-process semaphore counting; redlock4j numbers are dominated by Redis round-trips (one per acquire). ### 7.4 Semaphore (5 clients) @@ -229,21 +229,38 @@ Parity with a slight edge to redlock4j multinode. Switching from keyspace-notifications to polling moved redlock4j FairLock from ~0 ops/s to ~70 % of Redisson's throughput with passing correctness/FIFO checks. -### 7.7 Standings +### 7.7 Standings (post-A1 backoff) | Suite | Leader | redlock4j position | |---|---|---| -| DistributedLock (single-node) | redpulsar 21.87 | 0.84× (16 % slower) | -| DistributedLock (3-node) | redpulsar 21.87 | **0.017× — broken; P0-4/P1-5 required** | +| DistributedLock (single-node) | redpulsar 21.63 | 0.85× (15 % slower) | +| DistributedLock (3-node) | redpulsar 21.63 | **0.037× throughput — still capped; A3 required.** **Best p99 (858 ms vs redisson 1,287 ms)** | | MultiLock | **redlock4j 17.72** | **leader** | -| RWLock readers | redisson 153.62 | 0.35× | -| RWLock writers | **redlock4j-singlenode 17.26** | **leader (redisson starved)** | +| RWLock readers | redisson 151.65 | 0.63× (was 0.35× pre-backoff) | +| RWLock writers | **redlock4j-rwlock 16.42** | **leader (redisson starved at 0.21 ops/s)** | | Semaphore | **redlock4j-singlenode 91.09** | **leader (~1.7× redisson)** | | CountDownLatch | **redlock4j 59.91** | **leader** | | FairLock | redisson 16.95 | 0.71× | -redlock4j leads 4 of 7 categories. Remaining gaps (DistributedLock 3-node throughput, RWLock reader throughput, FairLock throughput) all trace to the same root cause: **fixed 50 ms polling between attempts**. P0-4 (pub/sub wait strategy) and P1-5 (exponential backoff with jitter) are the next levers. +redlock4j leads 4 of 7 categories and is best-in-class on p99 for 5 of 7. Remaining throughput gaps (DistributedLock 3-node, RWLock reader, FairLock) all trace to the same root cause: **the polling wait strategy itself**. A1 (backoff) absorbed the biggest cliff in DistributedLock 3-node and RWLock reader; A3 (pub/sub-on-release) is the next lever for the rest. +### 7.8 A1 impact: exponential backoff with jitter (2026-06-16) + +Configuration applied to 3-node redlock4j benchmark clients: `retryDelay=50ms`, `maxRetryDelay=500ms`, `retryDelayMultiplier=2.0`, `retryDelayJitterRatio=0.5`. Same workload as §7.1/§7.3/§7.6 (5 clients, 1 min, 30 s warmup, 50 ms work, polling wait strategy). + +| Suite | Metric | Baseline (fixed 50 ms) | With backoff | Δ | +|---|---|---:|---:|---:| +| DistributedLock 3-node | Ops/s | 0.38 | **0.81** | **+113 %** | +| DistributedLock 3-node | Success rate | 82 % | **91 %** | +9 pp | +| DistributedLock 3-node | Mean wait | 166 ms | 259 ms | +56 % (still 0) { + Thread.sleep(sleepMs); + } } } diff --git a/src/main/java/org/codarama/redlock4j/FairLock.java b/src/main/java/org/codarama/redlock4j/FairLock.java index b74216e..d7f24e0 100644 --- a/src/main/java/org/codarama/redlock4j/FairLock.java +++ b/src/main/java/org/codarama/redlock4j/FairLock.java @@ -148,7 +148,7 @@ public boolean tryLock(Duration timeout) throws InterruptedException { // Wait before retrying remaining = Duration.between(Instant.now(), deadline); if (!remaining.isNegative()) { - waitForLockRelease(lockKey, remaining.toMillis()); + waitForLockRelease(lockKey, remaining.toMillis(), attempt); } attempt++; } diff --git a/src/main/java/org/codarama/redlock4j/MultiLock.java b/src/main/java/org/codarama/redlock4j/MultiLock.java index e111833..4dfcdf4 100644 --- a/src/main/java/org/codarama/redlock4j/MultiLock.java +++ b/src/main/java/org/codarama/redlock4j/MultiLock.java @@ -194,7 +194,7 @@ public boolean tryLock(Duration timeout) throws InterruptedException { remaining = Duration.between(Instant.now(), deadline); if (!remaining.isNegative()) { // Wait on the first lock key (any release might allow us to proceed) - waitForLockRelease(lockKeys.get(0), remaining.toMillis()); + waitForLockRelease(lockKeys.get(0), remaining.toMillis(), attempt); } attempt++; } diff --git a/src/main/java/org/codarama/redlock4j/Redlock.java b/src/main/java/org/codarama/redlock4j/Redlock.java index 9a8c0c8..443cda9 100644 --- a/src/main/java/org/codarama/redlock4j/Redlock.java +++ b/src/main/java/org/codarama/redlock4j/Redlock.java @@ -147,18 +147,20 @@ public boolean tryLock(Duration timeout) throws InterruptedException { // Wait before retrying remaining = Duration.between(Instant.now(), deadline); if (!remaining.isNegative()) { - waitForLockReleaseWithJitter(remaining); + waitForLockReleaseWithJitter(remaining, attempt); } attempt++; } } /** - * Waits for the lock to be released with added jitter for backward compatibility. + * Waits for the lock to be released. Uses the configured wait strategy when present (which applies its own + * exponential backoff if so configured) and otherwise falls back to a legacy random-jitter sleep based on + * {@code retryDelay} \u2014 preserved for backward compatibility for callers that do not set a wait strategy. */ - private void waitForLockReleaseWithJitter(Duration remainingTimeout) throws InterruptedException { + private void waitForLockReleaseWithJitter(Duration remainingTimeout, int attempt) throws InterruptedException { if (waitStrategy != null) { - waitForLockRelease(lockKey, remainingTimeout.toMillis()); + waitForLockRelease(lockKey, remainingTimeout.toMillis(), attempt); } else { // Fallback to simple sleep with jitter (backward compatibility for Redlock) long retryDelayMs = config.getRetryDelay().toMillis(); diff --git a/src/main/java/org/codarama/redlock4j/RedlockManager.java b/src/main/java/org/codarama/redlock4j/RedlockManager.java index 76e1de3..16c5722 100644 --- a/src/main/java/org/codarama/redlock4j/RedlockManager.java +++ b/src/main/java/org/codarama/redlock4j/RedlockManager.java @@ -84,7 +84,8 @@ private RedlockManager(RedlockConfiguration config, DriverType driverType) { }); // Initialize wait strategy - this.waitStrategy = WaitStrategyFactory.create(config.getWaitStrategy(), redisDrivers, config.getRetryDelay()); + this.waitStrategy = WaitStrategyFactory.create(config.getWaitStrategy(), redisDrivers, config.getRetryDelay(), + config.getMaxRetryDelay(), config.getRetryDelayMultiplier(), config.getRetryDelayJitterRatio()); logger.info("Created RedlockManager with {} driver, {} Redis nodes, and {} wait strategy", driverType, redisDrivers.size(), config.getWaitStrategy()); diff --git a/src/main/java/org/codarama/redlock4j/RedlockReadWriteLock.java b/src/main/java/org/codarama/redlock4j/RedlockReadWriteLock.java index 6863fdc..9e2a04e 100644 --- a/src/main/java/org/codarama/redlock4j/RedlockReadWriteLock.java +++ b/src/main/java/org/codarama/redlock4j/RedlockReadWriteLock.java @@ -259,18 +259,24 @@ public boolean tryLock(Duration timeout) throws InterruptedException { // Wait before retrying if (attempt < config.getMaxRetryAttempts()) { - waitForLockRelease(remaining.toMillis()); + waitForLockRelease(remaining.toMillis(), attempt); } } return false; } - private void waitForLockRelease(long remainingTimeoutMs) throws InterruptedException { + private void waitForLockRelease(long remainingTimeoutMs, int attempt) throws InterruptedException { if (waitStrategy != null) { - waitStrategy.waitForRelease(writeLockKey, Duration.ofMillis(Math.max(remainingTimeoutMs, 1))); + waitStrategy.waitForRelease(writeLockKey, Duration.ofMillis(Math.max(remainingTimeoutMs, 1)), attempt); } else { - Thread.sleep(config.getRetryDelay().toMillis()); + Duration delay = org.codarama.redlock4j.strategy.BackoffCalculator.compute(config.getRetryDelay(), + config.getMaxRetryDelay(), config.getRetryDelayMultiplier(), config.getRetryDelayJitterRatio(), + attempt); + long sleepMs = Math.min(delay.toMillis(), Math.max(remainingTimeoutMs, 1)); + if (sleepMs > 0) { + Thread.sleep(sleepMs); + } } } diff --git a/src/main/java/org/codarama/redlock4j/RedlockSemaphore.java b/src/main/java/org/codarama/redlock4j/RedlockSemaphore.java index 1f39dfa..ac3bb0a 100644 --- a/src/main/java/org/codarama/redlock4j/RedlockSemaphore.java +++ b/src/main/java/org/codarama/redlock4j/RedlockSemaphore.java @@ -215,7 +215,7 @@ public boolean tryAcquire(int permits, Duration timeout) throws InterruptedExcep // Wait before retrying if (attempt < config.getMaxRetryAttempts()) { - waitForLockRelease(semaphoreKey, remaining.toMillis()); + waitForLockRelease(semaphoreKey, remaining.toMillis(), attempt); } } diff --git a/src/main/java/org/codarama/redlock4j/configuration/RedlockConfiguration.java b/src/main/java/org/codarama/redlock4j/configuration/RedlockConfiguration.java index dab86d2..d7c4111 100644 --- a/src/main/java/org/codarama/redlock4j/configuration/RedlockConfiguration.java +++ b/src/main/java/org/codarama/redlock4j/configuration/RedlockConfiguration.java @@ -20,6 +20,9 @@ public class RedlockConfiguration { private final List redisNodes; private final Duration defaultLockTimeout; private final Duration retryDelay; + private final Duration maxRetryDelay; + private final double retryDelayMultiplier; + private final double retryDelayJitterRatio; private final int maxRetryAttempts; private final double clockDriftFactor; private final Duration lockAcquisitionTimeout; @@ -29,6 +32,9 @@ private RedlockConfiguration(Builder builder) { this.redisNodes = new ArrayList<>(builder.redisNodes); this.defaultLockTimeout = builder.defaultLockTimeout; this.retryDelay = builder.retryDelay; + this.maxRetryDelay = builder.maxRetryDelay != null ? builder.maxRetryDelay : builder.retryDelay; + this.retryDelayMultiplier = builder.retryDelayMultiplier; + this.retryDelayJitterRatio = builder.retryDelayJitterRatio; this.maxRetryAttempts = builder.maxRetryAttempts; this.clockDriftFactor = builder.clockDriftFactor; this.lockAcquisitionTimeout = builder.lockAcquisitionTimeout; @@ -62,6 +68,46 @@ public Duration getRetryDelay() { return retryDelay; } + /** + * Returns the upper bound on the retry delay after exponential backoff growth. + * + *

+ * When {@link #getRetryDelayMultiplier()} is greater than 1.0, the effective delay grows with each retry attempt + * but is capped at this value. Defaults to {@link #getRetryDelay()} (no growth). + *

+ * + * @return maximum retry delay + */ + public Duration getMaxRetryDelay() { + return maxRetryDelay; + } + + /** + * Returns the multiplier applied per retry attempt for exponential backoff. + * + *

+ * Effective delay = min(maxRetryDelay, retryDelay * multiplier^attempt). A value of 1.0 disables growth. + *

+ * + * @return retry delay multiplier (>= 1.0) + */ + public double getRetryDelayMultiplier() { + return retryDelayMultiplier; + } + + /** + * Returns the jitter ratio applied to the computed retry delay. + * + *

+ * The actual sleep is sampled uniformly from [(1-r)*delay, (1+r)*delay]. A value of 0.0 disables jitter. + *

+ * + * @return jitter ratio in [0.0, 1.0] + */ + public double getRetryDelayJitterRatio() { + return retryDelayJitterRatio; + } + /** * Returns the maximum number of lock acquisition retry attempts. * @@ -143,6 +189,9 @@ public static class Builder { private final List redisNodes = new ArrayList<>(); private Duration defaultLockTimeout = Duration.ofSeconds(30); private Duration retryDelay = Duration.ofMillis(200); + private Duration maxRetryDelay = null; + private double retryDelayMultiplier = 1.0; + private double retryDelayJitterRatio = 0.0; private int maxRetryAttempts = 3; private double clockDriftFactor = 0.01; private Duration lockAcquisitionTimeout = Duration.ofSeconds(10); @@ -212,6 +261,51 @@ public Builder retryDelay(Duration delay) { return this; } + /** + * Sets the upper bound on the retry delay after exponential backoff growth. Default: equal to + * {@link #retryDelay(Duration)} (no growth). + * + * @param maxDelay + * the maximum retry delay duration + * @return this builder + */ + public Builder maxRetryDelay(Duration maxDelay) { + this.maxRetryDelay = maxDelay; + return this; + } + + /** + * Sets the multiplier applied per retry attempt for exponential backoff. Default: 1.0 (no growth). + * + *

+ * Effective delay = min(maxRetryDelay, retryDelay * multiplier^attempt). + *

+ * + * @param multiplier + * the multiplier (must be >= 1.0) + * @return this builder + */ + public Builder retryDelayMultiplier(double multiplier) { + this.retryDelayMultiplier = multiplier; + return this; + } + + /** + * Sets the jitter ratio applied to the computed retry delay. Default: 0.0 (no jitter). + * + *

+ * The actual sleep is sampled uniformly from [(1-r)*delay, (1+r)*delay]. + *

+ * + * @param ratio + * the jitter ratio in [0.0, 1.0] + * @return this builder + */ + public Builder retryDelayJitterRatio(double ratio) { + this.retryDelayJitterRatio = ratio; + return this; + } + /** * Sets the maximum number of lock acquisition retries. Default: 3. * @@ -320,6 +414,15 @@ public RedlockConfiguration build() { if (retryDelay == null || retryDelay.isNegative()) { throw new IllegalArgumentException("Retry delay cannot be negative"); } + if (maxRetryDelay != null && (maxRetryDelay.isNegative() || maxRetryDelay.compareTo(retryDelay) < 0)) { + throw new IllegalArgumentException("Max retry delay must be >= retry delay"); + } + if (retryDelayMultiplier < 1.0) { + throw new IllegalArgumentException("Retry delay multiplier must be >= 1.0"); + } + if (retryDelayJitterRatio < 0.0 || retryDelayJitterRatio > 1.0) { + throw new IllegalArgumentException("Retry delay jitter ratio must be between 0.0 and 1.0"); + } if (maxRetryAttempts < 0) { throw new IllegalArgumentException("Max retry attempts cannot be negative"); } diff --git a/src/main/java/org/codarama/redlock4j/strategy/BackoffCalculator.java b/src/main/java/org/codarama/redlock4j/strategy/BackoffCalculator.java new file mode 100644 index 0000000..525924d --- /dev/null +++ b/src/main/java/org/codarama/redlock4j/strategy/BackoffCalculator.java @@ -0,0 +1,71 @@ +/* + * SPDX-License-Identifier: MIT + * Copyright (c) 2025 Codarama + */ +package org.codarama.redlock4j.strategy; + +import java.time.Duration; +import java.util.concurrent.ThreadLocalRandom; + +/** + * Computes exponential-backoff retry delays with optional jitter. + * + *

+ * Used by {@link PollingWaitStrategy} and by the {@link org.codarama.redlock4j.Redlock} retry-loop fallback path when + * no wait strategy is configured. + *

+ * + * @since 1.0 + * @author Tihomir Mateev + */ +public final class BackoffCalculator { + + private BackoffCalculator() { + } + + /** + * Computes the delay for a given attempt index. + * + *

+ * Effective delay = min(maxRetryDelay, retryDelay * multiplier^attempt). When {@code jitterRatio} is greater than + * zero, the returned value is sampled uniformly from {@code [(1-r)*delay, (1+r)*delay]}. + *

+ * + * @param retryDelay + * the base delay + * @param maxRetryDelay + * the upper bound (must not be null; defaults to {@code retryDelay} for no growth) + * @param multiplier + * multiplier per attempt (1.0 disables growth) + * @param jitterRatio + * jitter ratio in [0.0, 1.0] + * @param attempt + * 0-based attempt index + * @return the computed delay + */ + public static Duration compute(Duration retryDelay, Duration maxRetryDelay, double multiplier, double jitterRatio, + int attempt) { + long baseMs = retryDelay.toMillis(); + long capMs = maxRetryDelay.toMillis(); + + double grown = baseMs; + if (multiplier > 1.0 && attempt > 0) { + grown = baseMs * Math.pow(multiplier, attempt); + } + long delayMs = (long) Math.min(grown, (double) capMs); + + if (jitterRatio > 0.0 && delayMs > 0) { + long low = (long) Math.floor(delayMs * (1.0 - jitterRatio)); + long high = (long) Math.ceil(delayMs * (1.0 + jitterRatio)); + if (low < 0) { + low = 0; + } + if (high <= low) { + high = low + 1; + } + delayMs = ThreadLocalRandom.current().nextLong(low, high); + } + + return Duration.ofMillis(delayMs); + } +} diff --git a/src/main/java/org/codarama/redlock4j/strategy/LockWaitStrategy.java b/src/main/java/org/codarama/redlock4j/strategy/LockWaitStrategy.java index aabeba0..39c153e 100644 --- a/src/main/java/org/codarama/redlock4j/strategy/LockWaitStrategy.java +++ b/src/main/java/org/codarama/redlock4j/strategy/LockWaitStrategy.java @@ -50,6 +50,30 @@ public interface LockWaitStrategy extends AutoCloseable { */ void initialize(List drivers, Duration retryDelay); + /** + * Initializes the wait strategy with backoff parameters in addition to the base retry delay. + * + *

+ * Default implementation delegates to {@link #initialize(List, Duration)} for backward compatibility; strategies + * that honor exponential backoff (e.g. {@link PollingWaitStrategy}) should override this overload. + *

+ * + * @param drivers + * the Redis drivers to use + * @param retryDelay + * the base retry delay + * @param maxRetryDelay + * the upper bound on the retry delay after backoff growth + * @param retryDelayMultiplier + * multiplier applied per attempt (1.0 disables growth) + * @param retryDelayJitterRatio + * jitter ratio in [0.0, 1.0] (0.0 disables jitter) + */ + default void initialize(List drivers, Duration retryDelay, Duration maxRetryDelay, + double retryDelayMultiplier, double retryDelayJitterRatio) { + initialize(drivers, retryDelay); + } + /** * Waits for a lock to be released on the specified key. * @@ -77,6 +101,29 @@ public interface LockWaitStrategy extends AutoCloseable { */ boolean waitForRelease(String lockKey, Duration timeout) throws InterruptedException; + /** + * Attempt-aware overload of {@link #waitForRelease(String, Duration)}. + * + *

+ * The {@code attempt} parameter (0-based) lets strategies grow the wait between successive retries (exponential + * backoff). Default implementation ignores {@code attempt} and delegates to + * {@link #waitForRelease(String, Duration)} for backward compatibility. + *

+ * + * @param lockKey + * the key of the lock to wait for + * @param timeout + * maximum time to wait + * @param attempt + * 0-based attempt counter + * @return true if a release event was detected, false if timeout was reached + * @throws InterruptedException + * if the current thread is interrupted while waiting + */ + default boolean waitForRelease(String lockKey, Duration timeout, int attempt) throws InterruptedException { + return waitForRelease(lockKey, timeout); + } + /** * Returns the type of wait strategy. * diff --git a/src/main/java/org/codarama/redlock4j/strategy/PollingWaitStrategy.java b/src/main/java/org/codarama/redlock4j/strategy/PollingWaitStrategy.java index 263246e..a0d16ec 100644 --- a/src/main/java/org/codarama/redlock4j/strategy/PollingWaitStrategy.java +++ b/src/main/java/org/codarama/redlock4j/strategy/PollingWaitStrategy.java @@ -38,26 +38,44 @@ public class PollingWaitStrategy implements LockWaitStrategy { private static final Logger logger = LoggerFactory.getLogger(PollingWaitStrategy.class); private Duration retryDelay; + private Duration maxRetryDelay; + private double retryDelayMultiplier; + private double retryDelayJitterRatio; private volatile boolean closed = false; @Override public void initialize(List drivers, Duration retryDelay) { + initialize(drivers, retryDelay, retryDelay, 1.0, 0.0); + } + + @Override + public void initialize(List drivers, Duration retryDelay, Duration maxRetryDelay, + double retryDelayMultiplier, double retryDelayJitterRatio) { this.retryDelay = retryDelay; - logger.info("Polling wait strategy initialized with retry delay {}", retryDelay); + this.maxRetryDelay = maxRetryDelay != null ? maxRetryDelay : retryDelay; + this.retryDelayMultiplier = retryDelayMultiplier; + this.retryDelayJitterRatio = retryDelayJitterRatio; + logger.info("Polling wait strategy initialized with retry delay {}, max {} , multiplier {}, jitter ratio {}", + retryDelay, this.maxRetryDelay, retryDelayMultiplier, retryDelayJitterRatio); } @Override public boolean waitForRelease(String lockKey, Duration timeout) throws InterruptedException { + return waitForRelease(lockKey, timeout, 0); + } + + @Override + public boolean waitForRelease(String lockKey, Duration timeout, int attempt) throws InterruptedException { if (closed) { throw new IllegalStateException("Strategy has been closed"); } Instant deadline = Instant.now().plus(timeout); - while (Instant.now().isBefore(deadline)) { - // Sleep for the retry delay + if (Instant.now().isBefore(deadline)) { Duration remaining = Duration.between(Instant.now(), deadline); - Duration sleepDuration = remaining.compareTo(retryDelay) < 0 ? remaining : retryDelay; + Duration backoff = computeBackoff(attempt); + Duration sleepDuration = remaining.compareTo(backoff) < 0 ? remaining : backoff; if (!sleepDuration.isNegative() && !sleepDuration.isZero()) { Thread.sleep(sleepDuration.toMillis()); @@ -71,6 +89,14 @@ public boolean waitForRelease(String lockKey, Duration timeout) throws Interrupt return false; } + /** + * Computes the backoff delay for the given attempt index, capped at {@link #maxRetryDelay} and jittered. + */ + private Duration computeBackoff(int attempt) { + return BackoffCalculator.compute(retryDelay, maxRetryDelay, retryDelayMultiplier, retryDelayJitterRatio, + attempt); + } + @Override public WaitStrategy getType() { return WaitStrategy.POLLING; diff --git a/src/main/java/org/codarama/redlock4j/strategy/WaitStrategy.java b/src/main/java/org/codarama/redlock4j/strategy/WaitStrategy.java index 7cd3c67..e8ca387 100644 --- a/src/main/java/org/codarama/redlock4j/strategy/WaitStrategy.java +++ b/src/main/java/org/codarama/redlock4j/strategy/WaitStrategy.java @@ -59,7 +59,9 @@ public enum WaitStrategy { * *

* The polling interval is configured via {@code retryDelay} in - * {@link org.codarama.redlock4j.configuration.RedlockConfiguration}. + * {@link org.codarama.redlock4j.configuration.RedlockConfiguration}. Exponential backoff is also supported via + * {@code maxRetryDelay}, {@code retryDelayMultiplier} and {@code retryDelayJitterRatio} \u2014 these default to + * passive values that preserve a fixed-interval poll. *

*/ POLLING diff --git a/src/main/java/org/codarama/redlock4j/strategy/WaitStrategyFactory.java b/src/main/java/org/codarama/redlock4j/strategy/WaitStrategyFactory.java index 95a3a15..f5ce11b 100644 --- a/src/main/java/org/codarama/redlock4j/strategy/WaitStrategyFactory.java +++ b/src/main/java/org/codarama/redlock4j/strategy/WaitStrategyFactory.java @@ -33,6 +33,28 @@ private WaitStrategyFactory() { * @return an initialized wait strategy */ public static LockWaitStrategy create(WaitStrategy strategyType, List drivers, Duration retryDelay) { + return create(strategyType, drivers, retryDelay, retryDelay, 1.0, 0.0); + } + + /** + * Creates and initializes a wait strategy, passing backoff parameters to strategies that honor exponential backoff. + * + * @param strategyType + * the type of strategy to create + * @param drivers + * the Redis drivers to use + * @param retryDelay + * the base retry delay + * @param maxRetryDelay + * the upper bound on the retry delay after backoff growth + * @param retryDelayMultiplier + * multiplier per attempt (1.0 disables growth) + * @param retryDelayJitterRatio + * jitter ratio in [0.0, 1.0] + * @return an initialized wait strategy + */ + public static LockWaitStrategy create(WaitStrategy strategyType, List drivers, Duration retryDelay, + Duration maxRetryDelay, double retryDelayMultiplier, double retryDelayJitterRatio) { LockWaitStrategy strategy; switch (strategyType) { @@ -46,7 +68,7 @@ public static LockWaitStrategy create(WaitStrategy strategyType, List RedlockConfiguration.builder().addRedisNode("localhost", 6379).retryDelay(Duration.ofMillis(200)) + .maxRetryDelay(Duration.ofMillis(100)).build()); + assertThrows(IllegalArgumentException.class, + () -> RedlockConfiguration.builder().addRedisNode("localhost", 6379).retryDelayMultiplier(0.5).build()); + assertThrows(IllegalArgumentException.class, () -> RedlockConfiguration.builder() + .addRedisNode("localhost", 6379).retryDelayJitterRatio(1.5).build()); + assertThrows(IllegalArgumentException.class, () -> RedlockConfiguration.builder() + .addRedisNode("localhost", 6379).retryDelayJitterRatio(-0.1).build()); + } + @Test public void testQuorumCalculation() { // Test with 3 nodes diff --git a/src/test/java/org/codarama/redlock4j/strategy/BackoffCalculatorTest.java b/src/test/java/org/codarama/redlock4j/strategy/BackoffCalculatorTest.java new file mode 100644 index 0000000..0e378cd --- /dev/null +++ b/src/test/java/org/codarama/redlock4j/strategy/BackoffCalculatorTest.java @@ -0,0 +1,68 @@ +/* + * SPDX-License-Identifier: MIT + * Copyright (c) 2025 Codarama + */ +package org.codarama.redlock4j.strategy; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for {@link BackoffCalculator}. + */ +@Tag("unit") +class BackoffCalculatorTest { + + private static final Duration BASE = Duration.ofMillis(50); + private static final Duration CAP = Duration.ofMillis(500); + + @Test + void multiplierOneReturnsBaseDelayForAnyAttempt() { + for (int attempt = 0; attempt < 10; attempt++) { + Duration d = BackoffCalculator.compute(BASE, CAP, 1.0, 0.0, attempt); + assertEquals(BASE, d, "attempt=" + attempt); + } + } + + @Test + void zeroAttemptReturnsBaseDelay() { + Duration d = BackoffCalculator.compute(BASE, CAP, 2.0, 0.0, 0); + assertEquals(BASE, d); + } + + @Test + void exponentialGrowthRespectsCap() { + Duration a1 = BackoffCalculator.compute(BASE, CAP, 2.0, 0.0, 1); + Duration a2 = BackoffCalculator.compute(BASE, CAP, 2.0, 0.0, 2); + Duration a3 = BackoffCalculator.compute(BASE, CAP, 2.0, 0.0, 3); + Duration a10 = BackoffCalculator.compute(BASE, CAP, 2.0, 0.0, 10); + assertEquals(100, a1.toMillis()); + assertEquals(200, a2.toMillis()); + assertEquals(400, a3.toMillis()); + assertEquals(CAP, a10); + } + + @Test + void jitterStaysWithinBounds() { + long min = Long.MAX_VALUE; + long max = Long.MIN_VALUE; + for (int i = 0; i < 200; i++) { + long ms = BackoffCalculator.compute(BASE, CAP, 1.0, 0.5, 0).toMillis(); + min = Math.min(min, ms); + max = Math.max(max, ms); + } + assertTrue(min >= 25, "min=" + min); + assertTrue(max <= 75, "max=" + max); + } + + @Test + void zeroJitterIsDeterministic() { + Duration d1 = BackoffCalculator.compute(BASE, CAP, 1.0, 0.0, 5); + Duration d2 = BackoffCalculator.compute(BASE, CAP, 1.0, 0.0, 5); + assertEquals(d1, d2); + } +} diff --git a/src/test/java/org/codarama/redlock4j/strategy/PollingWaitStrategyTest.java b/src/test/java/org/codarama/redlock4j/strategy/PollingWaitStrategyTest.java index c874a73..769f727 100644 --- a/src/test/java/org/codarama/redlock4j/strategy/PollingWaitStrategyTest.java +++ b/src/test/java/org/codarama/redlock4j/strategy/PollingWaitStrategyTest.java @@ -111,6 +111,36 @@ void waitForRelease_shouldHandleNegativeTimeout() throws InterruptedException { assertTrue(elapsed <= 20, "Negative timeout should return quickly"); } + @Test + void waitForRelease_withBackoffShouldGrowAndRespectCap() throws InterruptedException { + PollingWaitStrategy strategy = new PollingWaitStrategy(); + strategy.initialize(Collections.emptyList(), Duration.ofMillis(20), Duration.ofMillis(100), 2.0, 0.0); + + // attempt 0 -> 20ms + long t0 = System.currentTimeMillis(); + strategy.waitForRelease("k", Duration.ofMillis(500), 0); + long e0 = System.currentTimeMillis() - t0; + assertTrue(e0 >= 15 && e0 <= 60, "attempt=0 elapsed=" + e0); + + // attempt 3 -> 20*2^3 = 160 -> capped at 100ms + long t3 = System.currentTimeMillis(); + strategy.waitForRelease("k", Duration.ofMillis(500), 3); + long e3 = System.currentTimeMillis() - t3; + assertTrue(e3 >= 90 && e3 <= 150, "attempt=3 elapsed=" + e3); + } + + @Test + void waitForRelease_defaultsPreserveLegacyBehavior() throws InterruptedException { + // initialize via the legacy single-arg overload -> multiplier=1.0, jitter=0.0 + PollingWaitStrategy strategy = new PollingWaitStrategy(); + strategy.initialize(Collections.emptyList(), Duration.ofMillis(50)); + + long start = System.currentTimeMillis(); + strategy.waitForRelease("k", Duration.ofMillis(500), 4); // attempt should not grow delay + long elapsed = System.currentTimeMillis() - start; + assertTrue(elapsed >= 40 && elapsed <= 90, "elapsed=" + elapsed); + } + @Test void waitForRelease_shouldHandleInterruption() throws InterruptedException { PollingWaitStrategy strategy = new PollingWaitStrategy(); From 2d8541338a9615f2321c1fe2b210ca3a5ea74d1a Mon Sep 17 00:00:00 2001 From: Tihomir Mateev Date: Wed, 22 Jul 2026 11:05:32 +0300 Subject: [PATCH 3/3] Major guide update - (additionally) reduce noise in logs - (additionally) show some love to the CI --- .github/workflows/ci.yml | 101 +------ .github/workflows/nightly.yml | 3 - .github/workflows/pr-validation.yml | 105 ------- README.md | 2 +- docs/README.md | 2 +- docs/api/async-reactive.md | 53 ++-- docs/api/configuration.md | 57 ++-- docs/api/lock-types.md | 71 ++--- docs/api/redlock-manager.md | 24 ++ .../countdownlatch-implementation.md | 284 +++++++++--------- docs/comparison/fairlock-implementation.md | 82 ++--- docs/comparison/multilock-implementation.md | 220 ++++++-------- .../readwritelock-implementation.md | 185 +++++++----- docs/comparison/semaphore-implementation.md | 247 ++++++++------- docs/getting-started/installation.md | 45 +-- docs/getting-started/quick-start.md | 181 ++++++----- docs/guide/architecture.md | 208 +++++++++---- docs/guide/basic-usage.md | 207 ++++++++----- docs/guide/benchmarks.md | 210 +++++++------ docs/guide/best-practices.md | 262 +++++++++------- docs/guide/redis-clients.md | 170 +++++------ docs/index.md | 43 ++- docs/primitives/count-down-latch.md | 56 ++-- docs/primitives/fair-lock.md | 42 +-- docs/primitives/multi-lock.md | 38 +-- docs/primitives/read-write-lock.md | 36 +-- docs/primitives/redlock.md | 32 +- docs/primitives/semaphore.md | 58 ++-- docs/redlock4j-social-preview.png | Bin 0 -> 18261 bytes docs/redlock4j-social-preview.svg | 58 ++++ .../redlock4j/driver/JedisRedisDriver.java | 5 +- .../strategy/KeyspaceWaitStrategy.java | 4 +- .../LockExecutionStrategyFactory.java | 4 +- .../strategy/PollingWaitStrategy.java | 2 +- 34 files changed, 1635 insertions(+), 1462 deletions(-) delete mode 100644 .github/workflows/pr-validation.yml create mode 100644 docs/redlock4j-social-preview.png create mode 100644 docs/redlock4j-social-preview.svg diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8952755..bf296cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,74 +45,20 @@ jobs: restore-keys: ${{ runner.os }}-m2 - name: Compile project - run: mvn clean compile test-compile + run: mvn formatter:format - - name: Run unit tests - run: mvn test -Dtest=RedlockConfigurationTest - - - name: Run integration tests with Testcontainers - run: mvn test -Dtest=RedlockIntegrationTest + - name: Run unit and integration tests (requires Testcontainers) + run: mvn verify jacoco:report env: TESTCONTAINERS_RYUK_DISABLED: false - name: Run all tests (including performance tests on nightly) if: github.event_name == 'schedule' - run: mvn test -Dtest=RedlockPerformanceTest + run: mvn test -Dgroups=performance continue-on-error: true env: TESTCONTAINERS_RYUK_DISABLED: false - - name: Generate test report - uses: dorny/test-reporter@v1 - if: (success() || failure()) && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) - continue-on-error: true - with: - name: Maven Tests (ubuntu-latest, Java ${{ matrix.java }}) - path: 'target/surefire-reports/*.xml' - reporter: java-junit - fail-on-error: false - fail-on-empty: false - - - name: Upload test results - uses: actions/upload-artifact@v4 - if: always() - continue-on-error: true - with: - name: test-results-ubuntu-latest-java${{ matrix.java }} - path: | - target/surefire-reports/ - target/site/jacoco/ - retention-days: 30 - - code-quality: - name: Code Quality Analysis - runs-on: ubuntu-latest - continue-on-error: true - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up JDK 11 - uses: actions/setup-java@v4 - with: - java-version: 11 - distribution: 'temurin' - - - name: Cache Maven dependencies - uses: actions/cache@v4 - continue-on-error: true - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - - name: Run tests with coverage - run: mvn clean test jacoco:report - continue-on-error: true - - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v4 continue-on-error: true @@ -121,42 +67,3 @@ jobs: flags: unittests name: codecov-umbrella fail_ci_if_error: false - - build-info: - name: Build Information - runs-on: ubuntu-latest - continue-on-error: true - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up JDK 11 - uses: actions/setup-java@v4 - with: - java-version: 11 - distribution: 'temurin' - - - name: Display build information - continue-on-error: true - run: | - echo "## Build Information" >> $GITHUB_STEP_SUMMARY - echo "- **Repository:** ${{ github.repository }}" >> $GITHUB_STEP_SUMMARY - echo "- **Branch:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY - echo "- **Commit:** ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY - echo "- **Trigger:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY - echo "- **Java Version:** $(java -version 2>&1 | head -n 1)" >> $GITHUB_STEP_SUMMARY - echo "- **Maven Version:** $(mvn -version | head -n 1)" >> $GITHUB_STEP_SUMMARY - echo "- **Docker Version:** $(docker --version)" >> $GITHUB_STEP_SUMMARY - - - name: Validate Maven POM - run: mvn validate - - - name: Check for Maven wrapper - continue-on-error: true - run: | - if [ -f "mvnw" ]; then - echo "Maven wrapper found" >> $GITHUB_STEP_SUMMARY - else - echo "Maven wrapper not found (using system Maven)" >> $GITHUB_STEP_SUMMARY - fi diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 6ae24b3..8038be7 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -36,9 +36,6 @@ jobs: key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-m2 - - name: Run unit tests - run: mvn test - - name: Run integration tests run: mvn verify -Dmaven.test.skip.exec=false env: diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml deleted file mode 100644 index ada9a5a..0000000 --- a/.github/workflows/pr-validation.yml +++ /dev/null @@ -1,105 +0,0 @@ -name: PR Validation - -on: - pull_request: - branches: [ main, develop ] - types: [opened, synchronize, reopened, ready_for_review] - -permissions: - contents: read - checks: write - pull-requests: write - -jobs: - security-scan: - name: Security Scan - runs-on: ubuntu-latest - if: github.event.pull_request.draft == false - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up JDK 11 - uses: actions/setup-java@v4 - with: - java-version: 11 - distribution: 'temurin' - - - name: Cache Maven dependencies - uses: actions/cache@v4 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - - name: Run security scan - run: mvn org.owasp:dependency-check-maven:check - continue-on-error: true - - - name: Upload security scan results - uses: actions/upload-artifact@v4 - if: always() - with: - name: security-scan-results - path: target/dependency-check-report.html - retention-days: 30 - - code-style: - name: Code Style Check - runs-on: ubuntu-latest - if: github.event.pull_request.draft == false - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up JDK 11 - uses: actions/setup-java@v4 - with: - java-version: 11 - distribution: 'temurin' - - - name: Check license headers - run: | - echo "Checking SPDX-compliant license headers in Java files..." - MISSING_LICENSE=0 - - for file in $(find src -name "*.java"); do - if ! head -5 "$file" | grep -q "SPDX-License-Identifier: MIT"; then - echo "Missing SPDX license header: $file" - MISSING_LICENSE=1 - fi - done - - if [ $MISSING_LICENSE -eq 0 ]; then - echo "All Java files have proper SPDX license headers" - echo "- **License Headers:** All files SPDX-compliant" >> $GITHUB_STEP_SUMMARY - else - echo "Some files are missing SPDX license headers" - echo "- **License Headers:** Some files missing SPDX headers" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - - name: Check code formatting - run: | - echo "Checking basic code formatting..." - - # Check for tabs vs spaces - if grep -r $'\t' src/ --include="*.java"; then - echo "Found tab characters in Java files (should use spaces)" - echo "- **Code Formatting:** Tab characters found" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "No tab characters found" - echo "- **Code Formatting:** No tab characters" >> $GITHUB_STEP_SUMMARY - fi - - # Check for trailing whitespace - if grep -r '[[:space:]]$' src/ --include="*.java"; then - echo "Found trailing whitespace in Java files" - echo "- **Trailing Whitespace:** Found in some files" >> $GITHUB_STEP_SUMMARY - else - echo "No trailing whitespace found" - echo "- **Trailing Whitespace:** None found" >> $GITHUB_STEP_SUMMARY - fi diff --git a/README.md b/README.md index 3b09e15..90deb18 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@

- [![CI](https://github.com/Codarama/redlock4j/actions/workflows/ci.yml/badge.svg)](https://github.com/Codarama/redlock4j/actions/workflows/ci.yml) +[![codecov](https://codecov.io/gh/Codarama/redlock4j/graph/badge.svg?token=EK4LMLJ533)](https://codecov.io/gh/Codarama/redlock4j) [![Maven Central](https://img.shields.io/maven-central/v/org.codarama/redlock4j?versionSuffix=RELEASE)](https://maven-badges.herokuapp.com/maven-central/org.codarama/redlock4j) [![Javadocs](https://www.javadoc.io/badge/org.codarama/redlock4j.svg)](https://javadoc.io/doc/org.codarama/redlock4j) [![Java](https://img.shields.io/badge/Java-8%2B-blue.svg)](https://openjdk.java.net/) diff --git a/docs/README.md b/docs/README.md index e0500cf..22775e7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -121,7 +121,7 @@ The documentation supports: ```markdown \`\`\`java -Lock lock = redlock.lock("resource", 10000); +Lock lock = manager.createLock("resource"); \`\`\` ``` diff --git a/docs/api/async-reactive.md b/docs/api/async-reactive.md index 0783c1f..d72870d 100644 --- a/docs/api/async-reactive.md +++ b/docs/api/async-reactive.md @@ -14,13 +14,17 @@ AsyncRedlock asyncLock = manager.createAsyncLock("async-resource"); ### Methods -| Method | Return Type | Description | -|--------|-------------|-------------| -| `tryLockAsync()` | `CompletionStage` | Tries to acquire the lock | -| `tryLockAsync(Duration)` | `CompletionStage` | Tries with timeout | -| `unlockAsync()` | `CompletionStage` | Releases the lock | -| `isHeldByCurrentThread()` | `boolean` | Checks if held by current thread | -| `getLockKey()` | `String` | Returns the lock key | +| Method | Return Type | Description | +|------------------------------|----------------------------|---------------------------------------------------------| +| `tryLockAsync()` | `CompletionStage` | Tries to acquire the lock | +| `tryLockAsync(Duration)` | `CompletionStage` | Tries with timeout | +| `lockAsync()` | `CompletionStage` | Acquires the lock, waiting if necessary | +| `unlockAsync()` | `CompletionStage` | Releases the lock | +| `isHeldByCurrentThread()` | `boolean` | Checks if held by current thread | +| `getRemainingValidityTime()` | `Duration` | Remaining validity time, or `Duration.ZERO` if not held | +| `getHoldCount()` | `int` | Number of times the lock has been acquired, or 0 | +| `getLockKey()` | `String` | Returns the lock key | +| `extendAsync(Duration)` | `CompletionStage` | Extends the lock validity time | ### Example @@ -74,24 +78,31 @@ RxRedlock rxLock = manager.createRxLock("rx-resource"); ### Methods -| Method | Return Type | Description | -|--------|-------------|-------------| -| `tryLockSingle()` | `Single` | Tries to acquire the lock | -| `tryLockSingle(Duration)` | `Single` | Tries with timeout | -| `unlockCompletable()` | `Completable` | Releases the lock | -| `validityObservable(Duration)` | `Observable` | Emits remaining validity time | -| `getLockKey()` | `String` | Returns the lock key | +| Method | Return Type | Description | +|-----------------------------------------------------------|-------------------------|---------------------------------------------------------------------------| +| `tryLockRx()` | `Single` | Tries to acquire the lock | +| `tryLockRx(Duration)` | `Single` | Tries with timeout | +| `lockRx()` | `Completable` | Acquires the lock, waiting if necessary | +| `unlockRx()` | `Completable` | Releases the lock | +| `tryLockWithRetryRx(int maxRetries, Duration retryDelay)` | `Single` | Tries to acquire with retry logic | +| `validityObservable(Duration checkInterval)` | `Observable` | Emits remaining validity time at each check | +| `lockStateObservable()` | `Observable` | Emits lock state changes (ACQUIRING, ACQUIRED, RELEASED, EXPIRED, FAILED) | +| `isHeldByCurrentThread()` | `boolean` | Checks if held by current thread | +| `getRemainingValidityTime()` | `Duration` | Remaining validity time, or `Duration.ZERO` if not held | +| `getHoldCount()` | `int` | Number of times the lock has been acquired, or 0 | +| `getLockKey()` | `String` | Returns the lock key | +| `extendRx(Duration)` | `Single` | Extends the lock validity time | ### Example ```java RxRedlock rxLock = manager.createRxLock("rx-resource"); -rxLock.tryLockSingle(Duration.ofSeconds(5)) +rxLock.tryLockRx(Duration.ofSeconds(5)) .flatMapCompletable(acquired -> { if (acquired) { return performReactiveWork() - .andThen(rxLock.unlockCompletable()); + .andThen(rxLock.unlockRx()); } return Completable.complete(); }) @@ -107,8 +118,8 @@ Monitor lock validity in real-time: ```java rxLock.validityObservable(Duration.ofSeconds(1)) - .subscribe(remainingMs -> { - if (remainingMs < 5000) { + .subscribe(remaining -> { + if (remaining.toMillis() < 5000) { System.out.println("Warning: Lock expiring soon!"); } }); @@ -127,7 +138,7 @@ AsyncRedlockImpl combinedLock = manager.createAsyncRxLock("combined-resource"); combinedLock.tryLockAsync().thenAccept(acquired -> { ... }); // Use as RxJava -combinedLock.tryLockSingle().subscribe(acquired -> { ... }); +combinedLock.tryLockRx().subscribe(acquired -> { ... }); ``` --- @@ -148,7 +159,7 @@ asyncLock.tryLockAsync() }); // RxJava auto-renewal -rxLock.tryLockSingle() +rxLock.tryLockRx() .flatMapCompletable(acquired -> { if (acquired) { return longRunningReactiveOperation(); @@ -177,7 +188,7 @@ asyncLock.tryLockAsync() ### RxJava ```java -rxLock.tryLockSingle() +rxLock.tryLockRx() .onErrorReturn(ex -> { logger.error("Lock failed", ex); return false; diff --git a/docs/api/configuration.md b/docs/api/configuration.md index 2edada1..f3c9b37 100644 --- a/docs/api/configuration.md +++ b/docs/api/configuration.md @@ -23,17 +23,20 @@ RedlockConfiguration config = RedlockConfiguration.builder() ### Configuration Properties -| Method | Type | Default | Description | -|--------|------|---------|-------------| -| `addRedisNode(host, port)` | String, int | - | Adds a Redis node | -| `addRedisNode(host, port, password)` | String, int, String | - | Adds a Redis node with password | -| `defaultLockTimeout(Duration)` | Duration | 30s | Default lock TTL | -| `retryDelay(Duration)` | Duration | 200ms | Delay between retry attempts | -| `maxRetryAttempts(int)` | int | 3 | Maximum retry attempts | -| `clockDriftFactor(double)` | double | 0.01 | Clock drift compensation | -| `lockAcquisitionTimeout(Duration)` | Duration | 10s | Max time to wait for lock | -| `usePolling()` | - | - | Use polling instead of keyspace notifications | -| `waitStrategy(WaitStrategy)` | WaitStrategy | KEYSPACE_NOTIFICATIONS | Wait strategy | +| Method | Type | Default | Description | +|--------------------------------------|---------------------|------------------------|-----------------------------------------------------------------------------------| +| `addRedisNode(host, port)` | String, int | - | Adds a Redis node | +| `addRedisNode(host, port, password)` | String, int, String | - | Adds a Redis node with password | +| `defaultLockTimeout(Duration)` | Duration | 30s | Default lock TTL | +| `retryDelay(Duration)` | Duration | 200ms | Delay between retry attempts | +| `maxRetryDelay(Duration)` | Duration | = retryDelay | Upper bound on retry delay after exponential backoff growth | +| `retryDelayMultiplier(double)` | double | 1.0 | Exponential backoff multiplier applied per retry attempt (must be >= 1.0) | +| `retryDelayJitterRatio(double)` | double | 0.0 | Random jitter ratio added to retry delay to avoid retry storms (range [0.0, 1.0]) | +| `maxRetryAttempts(int)` | int | 3 | Maximum retry attempts | +| `clockDriftFactor(double)` | double | 0.01 | Clock drift compensation | +| `lockAcquisitionTimeout(Duration)` | Duration | 10s | Max time to wait for lock | +| `usePolling()` | - | - | Use polling instead of keyspace notifications | +| `waitStrategy(WaitStrategy)` | WaitStrategy | KEYSPACE_NOTIFICATIONS | Wait strategy | --- @@ -58,6 +61,7 @@ RedisNodeConfiguration node = RedisNodeConfiguration.builder() .host("redis.example.com") .port(6379) .password("secretpassword") + .database(0) .connectionTimeoutMs(5000) .socketTimeoutMs(5000) .build(); @@ -65,6 +69,17 @@ RedisNodeConfiguration node = RedisNodeConfiguration.builder() config.addRedisNode(node); ``` +### RedisNodeConfiguration Properties + +| Method | Type | Default | Description | +|----------------------------|--------|-----------|-------------------------------------------| +| `host(String)` | String | localhost | Redis server hostname or IP | +| `port(int)` | int | 6379 | Redis server port | +| `password(String)` | String | null | Authentication password | +| `database(int)` | int | 0 | Redis database index | +| `connectionTimeoutMs(int)` | int | 2000 | Connection timeout in milliseconds | +| `socketTimeoutMs(int)` | int | 2000 | Socket read/write timeout in milliseconds | + --- ## Wait Strategy @@ -106,11 +121,11 @@ RedlockConfiguration.builder() Valid deployment modes: -| Nodes | Mode | Description | -|:-----:|------|-------------| -| 1 | Single-node | Optimized, no distributed consensus | -| 2 | **Invalid** | Cannot form quorum | -| 3+ | Multi-node | Distributed consensus with quorum | +| Nodes | Mode | Description | +|:------:|-------------|-------------------------------------| +| 1 | Single-node | Optimized, no distributed consensus | +| 2 | **Invalid** | Cannot form quorum | +| 3+ | Multi-node | Distributed consensus with quorum | ### Single Node (Development) @@ -179,11 +194,11 @@ public class ConfigurationExample { ### Lock TTL -| Operation Duration | Recommended TTL | -|-------------------|-----------------| -| < 1 second | 5-10 seconds | -| 1-10 seconds | 30-60 seconds | -| > 10 seconds | Reconsider approach | +| Operation Duration | Recommended TTL | +|--------------------|---------------------| +| < 1 second | 5-10 seconds | +| 1-10 seconds | 30-60 seconds | +| > 10 seconds | Reconsider approach | ### Retry Configuration diff --git a/docs/api/lock-types.md b/docs/api/lock-types.md index c996f83..745390e 100644 --- a/docs/api/lock-types.md +++ b/docs/api/lock-types.md @@ -4,14 +4,14 @@ This page documents the API for all distributed lock types. For detailed explana ## Quick Reference -| Type | Interface | Details | -|------|-----------|---------| -| [Redlock](../primitives/redlock.md) | `Lock` | Standard distributed lock | -| [FairLock](../primitives/fair-lock.md) | `Lock` | FIFO ordering | -| [MultiLock](../primitives/multi-lock.md) | `Lock` | Multi-resource | -| [ReadWriteLock](../primitives/read-write-lock.md) | `ReadWriteLock` | Reader/writer | -| [Semaphore](../primitives/semaphore.md) | Custom | Permit-based | -| [CountDownLatch](../primitives/count-down-latch.md) | Custom | Coordination | +| Type | Interface | Details | +|-----------------------------------------------------|-----------------|---------------------------| +| [Redlock](../primitives/redlock.md) | `Lock` | Standard distributed lock | +| [FairLock](../primitives/fair-lock.md) | `Lock` | FIFO ordering | +| [MultiLock](../primitives/multi-lock.md) | `Lock` | Multi-resource | +| [ReadWriteLock](../primitives/read-write-lock.md) | `ReadWriteLock` | Reader/writer | +| [Semaphore](../primitives/semaphore.md) | Custom | Permit-based | +| [CountDownLatch](../primitives/count-down-latch.md) | Custom | Coordination | --- @@ -19,49 +19,50 @@ This page documents the API for all distributed lock types. For detailed explana Applies to: `Redlock`, `FairLock`, `MultiLock`, read/write locks -| Method | Description | -|--------|-------------| -| `void lock()` | Acquires the lock, blocking until available | -| `void lockInterruptibly()` | Acquires the lock, can be interrupted | -| `boolean tryLock()` | Tries to acquire immediately | -| `boolean tryLock(long time, TimeUnit unit)` | Tries to acquire within timeout | -| `boolean tryLock(Duration timeout)` | Tries to acquire within timeout | -| `void unlock()` | Releases the lock | -| `boolean isHeldByCurrentThread()` | Returns true if current thread holds lock | +| Method | Description | +|---------------------------------------------|---------------------------------------------| +| `void lock()` | Acquires the lock, blocking until available | +| `void lockInterruptibly()` | Acquires the lock, can be interrupted | +| `boolean tryLock()` | Tries to acquire immediately | +| `boolean tryLock(long time, TimeUnit unit)` | Tries to acquire within timeout | +| `boolean tryLock(Duration timeout)` | Tries to acquire within timeout | +| `void unlock()` | Releases the lock | +| `boolean isHeldByCurrentThread()` | Returns true if current thread holds lock | --- ## RedlockReadWriteLock Methods -| Method | Description | -|--------|-------------| -| `Lock readLock()` | Returns the read lock | +| Method | Description | +|--------------------|------------------------| +| `Lock readLock()` | Returns the read lock | | `Lock writeLock()` | Returns the write lock | --- ## RedlockSemaphore Methods -| Method | Description | -|--------|-------------| -| `void acquire()` | Acquires one permit, blocking | -| `void acquire(int permits)` | Acquires multiple permits | -| `boolean tryAcquire(Duration timeout)` | Tries to acquire one permit | -| `boolean tryAcquire(int permits, Duration timeout)` | Tries to acquire multiple permits | -| `void release()` | Releases one permit | -| `void release(int permits)` | Releases multiple permits | -| `int availablePermits()` | Returns estimated available permits | +| Method | Description | +|-----------------------------------------------------|-----------------------------------------| +| `void acquire()` | Acquires one permit, blocking | +| `void acquire(int permits)` | Acquires multiple permits | +| `boolean tryAcquire()` | Tries to acquire one permit immediately | +| `boolean tryAcquire(Duration timeout)` | Tries to acquire one permit | +| `boolean tryAcquire(int permits, Duration timeout)` | Tries to acquire multiple permits | +| `void release()` | Releases one permit | +| `void release(int permits)` | Releases multiple permits | +| `int availablePermits()` | Returns estimated available permits | --- ## RedlockCountDownLatch Methods -| Method | Description | -|--------|-------------| -| `void countDown()` | Decrements the count by one | -| `void await()` | Waits until count reaches zero | -| `boolean await(Duration timeout)` | Waits with timeout | -| `long getCount()` | Returns current count | +| Method | Description | +|-----------------------------------|--------------------------------| +| `void countDown()` | Decrements the count by one | +| `void await()` | Waits until count reaches zero | +| `boolean await(Duration timeout)` | Waits with timeout | +| `long getCount()` | Returns current count | --- diff --git a/docs/api/redlock-manager.md b/docs/api/redlock-manager.md index fee71e3..e85a074 100644 --- a/docs/api/redlock-manager.md +++ b/docs/api/redlock-manager.md @@ -166,6 +166,30 @@ public AsyncRedlockImpl createAsyncRxLock(String lockKey) ## Utility Methods +### getConnectedNodeCount() + +Returns the number of Redis nodes currently connected. + +```java +public int getConnectedNodeCount() +``` + +### getQuorum() + +Returns the quorum size required for lock acquisition. + +```java +public int getQuorum() +``` + +### getDriverType() + +Returns the Redis driver type in use. + +```java +public DriverType getDriverType() +``` + ### isHealthy() Checks if the manager can reach a quorum of Redis nodes. diff --git a/docs/comparison/countdownlatch-implementation.md b/docs/comparison/countdownlatch-implementation.md index bea6633..58b8a9e 100644 --- a/docs/comparison/countdownlatch-implementation.md +++ b/docs/comparison/countdownlatch-implementation.md @@ -16,9 +16,7 @@ Both libraries implement distributed countdown latches for coordinating multiple ```java // Create latch waiting for 3 operations -RedlockCountDownLatch latch = new RedlockCountDownLatch( - "startup", 3, redisDrivers, config -); +RedlockCountDownLatch latch = manager.createCountDownLatch("startup", 3); // Worker threads new Thread(() -> { @@ -80,7 +78,7 @@ System.out.println("All services initialized!"); - Counter replicated across all nodes - Quorum-based count reads - Quorum-based decrements -- Pub/sub for zero notification +- Atomic per-node decrement + publish (single Lua script) - Local latch for waiting - Automatic expiration (10x lock timeout) @@ -89,10 +87,10 @@ System.out.println("All services initialized!"); RedlockCountDownLatch ├─ latchKey (counter key) ├─ channelKey (pub/sub) - ├─ List (quorum-based) + ├─ LockExecutionStrategy (node fan-out + quorum) ├─ CountDownLatch localLatch (for waiting) ├─ AtomicBoolean subscribed - └─ Quorum-based DECR + └─ Atomic DECR + PUBLISH (decrAndPublishIfZero) ``` ### Redisson @@ -126,84 +124,73 @@ RedissonCountDownLatch ### redlock4j -**Initialization**: Automatic in constructor +**Initialization**: Automatic when created via the manager factory. Users call +`manager.createCountDownLatch(latchKey, count)`; the manager wires the internal +drivers and execution strategy and seeds the counter on all nodes with a long +expiration (10x lock timeout). ```java -public RedlockCountDownLatch(String latchKey, int count, - List redisDrivers, - RedlockConfiguration config) { - this.latchKey = latchKey; - this.channelKey = latchKey + ":channel"; - this.initialCount = count; - this.localLatch = new CountDownLatch(1); - - // Initialize on all nodes - initializeLatch(count); -} - -private void initializeLatch(int count) { - String countValue = String.valueOf(count); - int successfulNodes = 0; - - for (RedisDriver driver : redisDrivers) { - // Set with long expiration (10x lock timeout) - driver.setex(latchKey, countValue, - +// Simplified/conceptual pseudocode of the internal setup +this.latchKey = latchKey; +this.channelKey = latchKey + ":channel"; +this.initialCount = count; +this.localLatch = new CountDownLatch(1); + +// Seed the counter on the nodes via the execution strategy, +// with a long expiration (10x lock timeout) +executionStrategy.executeOnNodes(driver -> { + driver.setex(latchKey, String.valueOf(count), + config.getDefaultLockTimeout().toMillis() * 10); + return true; +}); +``` ## Count Down Operation ### redlock4j -**Algorithm**: Quorum-based DECR with notification +**Algorithm**: Atomic per-node decrement-and-publish, fanned out via the execution strategy ```java public void countDown() { - int successfulNodes = 0; - long newCount = -1; - - // Decrement on all nodes - for (RedisDriver driver : redisDrivers) { + // Fan out to the appropriate nodes via the execution strategy + int successCount = executionStrategy.executeOnNodes(driver -> { try { - long count = driver.decr(latchKey); - newCount = count; - successfulNodes++; + // Atomic: decrement and publish "zero" if the count hits zero, + // in a single Lua script (decrAndPublishIfZero) + long count = driver.decrAndPublishIfZero(latchKey, channelKey, "zero"); + return true; } catch (Exception e) { - logger.debug("Failed to decrement on {}", driver.getIdentifier()); + logger.debug("Failed to decrement latch count on {}", driver.getIdentifier()); + return false; } - } + }); - // Check quorum - if (successfulNodes >= config.getQuorum()) { - // If reached zero, publish notification - if (newCount <= 0) { - publishZeroNotification(); - } + if (executionStrategy.isSuccessful(successCount)) { + logger.debug("Successfully decremented latch {} on {} nodes", latchKey, successCount); } else { - logger.warn("Failed to decrement on quorum"); + logger.warn("Failed to decrement latch {} on sufficient nodes", latchKey); } } +``` -private void publishZeroNotification() { - for (RedisDriver driver : redisDrivers) { - try { - long subscribers = driver.publish(channelKey, "zero"); - logger.debug("Published to {} subscribers", subscribers); - } catch (Exception e) { - logger.debug("Failed to publish on {}", driver.getIdentifier()); - } - } -} +Where `decrAndPublishIfZero` runs this Lua script atomically on each node: + +```lua +local count = redis.call("DECR", KEYS[1]) +if count <= 0 then + redis.call("PUBLISH", KEYS[2], ARGV[1]) +end +return count ``` **Characteristics**: -- DECR on all nodes -- Quorum check for success -- Publish to all nodes when zero -- No atomicity between decrement and publish +- Atomic DECR + PUBLISH per node (single Lua script) +- Fan-out and quorum check delegated to the execution strategy +- Decrement and zero-notification cannot interleave on a node **Redis Operations** (M nodes): -- M × `DECR` -- M × `PUBLISH` (if zero) +- M × `decrAndPublishIfZero` Lua script (DECR, plus PUBLISH when zero) ### Redisson @@ -236,7 +223,7 @@ end; **Algorithm**: Subscribe + poll with local latch ```java -public boolean await(long timeout, TimeUnit unit) throws InterruptedException { +public boolean await(Duration timeout) throws InterruptedException { // Subscribe to notifications subscribeToNotifications(); @@ -247,17 +234,14 @@ public boolean await(long timeout, TimeUnit unit) throws InterruptedException { } // Wait on local latch (released by pub/sub notification) - boolean completed = localLatch.await(timeout, unit); - - return completed; + return localLatch.await(timeout.toMillis(), TimeUnit.MILLISECONDS); } private void subscribeToNotifications() { if (subscribed.compareAndSet(false, true)) { new Thread(() -> { - // Subscribe to first driver - RedisDriver driver = redisDrivers.get(0); - driver.subscribe(new MessageHandler() { + // Subscribe on a single node + subscriptionDriver.subscribe(new MessageHandler() { @Override public void onMessage(String channel, String message) { if ("zero".equals(message)) { @@ -270,19 +254,22 @@ private void subscribeToNotifications() { } public long getCount() { - int successfulReads = 0; - long totalCount = 0; + List results = new ArrayList<>(); - for (RedisDriver driver : redisDrivers) { + // Read the count from the appropriate nodes via the execution strategy + executionStrategy.executeOnNodes(driver -> { String countStr = driver.get(latchKey); if (countStr != null) { - totalCount += Long.parseLong(countStr); - successfulReads++; + synchronized (results) { + results.add(Long.parseLong(countStr)); + } } - } + return true; + }); - if (successfulReads >= config.getQuorum()) { - return Math.max(0, totalCount / successfulReads); + if (!results.isEmpty()) { + long total = results.stream().mapToLong(Long::longValue).sum(); + return Math.max(0, total / results.size()); // Average across nodes } return 0; // Conservative fallback @@ -353,16 +340,17 @@ public long getCount() { ```java public void reset() { - // Delete existing latch - for (RedisDriver driver : redisDrivers) { + // Delete the existing latch on the appropriate nodes + executionStrategy.executeOnNodes(driver -> { driver.del(latchKey); - } + return true; + }); // Reset local state localLatch = new CountDownLatch(1); subscribed.set(false); - // Reinitialize + // Reinitialize with the original count initializeLatch(initialCount); } ``` @@ -398,9 +386,8 @@ latch.trySetCount(initialCount); ### redlock4j **Count Down** (M nodes): -- M × `DECR` -- M × `PUBLISH` (if zero) -- Total: M or 2M operations +- M × `decrAndPublishIfZero` Lua script (atomic DECR, plus PUBLISH when zero) +- Total: M script executions **Await**: - 1 × `SUBSCRIBE` @@ -432,24 +419,35 @@ latch.trySetCount(initialCount); - Single round trip for countDown - Polling overhead for await +### Measured Performance + +Benchmark: 5 clients, count=5, 50 ms work per cycle, 3-node Redis 7 cluster, 60 s measurement (full methodology in [Architecture › Performance Analysis](../guide/architecture.md#performance-analysis)). + +| Implementation | Ops/s | p50 (ms) | p99 (ms) | mean (ms) | success | +|------------------------|----------:|----------:|----------:|----------:|--------:| +| Redisson | 59.02 | 17.17 | 22.57 | 16.91 | 100 % | +| redlock4j-singlenode | 58.19 | 16.17 | 19.87 | 15.67 | 100 % | +| **redlock4j-3node** | **59.91** | **15.29** | **19.89** | **15.18** | 100 % | + +**Reading the numbers**: `redlock4j-3node` matches Redisson on throughput while delivering slightly lower p50/p99/mean latency. Quorum overhead is fully amortized by the parallel multi-node I/O — coordination primitives benefit from multi-node distribution rather than paying for it. + ## Safety & Correctness ### redlock4j **Safety Guarantees**: -- ✅ Quorum-based consistency -- ✅ Survives minority node failures -- ✅ Count averaged across nodes -- ✅ Automatic expiration -- ✅ No single point of failure +- Quorum-based consistency +- Survives minority node failures +- Count averaged across nodes +- Automatic expiration +- No single point of failure **Potential Issues**: -- ⚠️ Higher latency -- ⚠️ More network overhead -- ⚠️ Non-atomic decrement + publish -- ⚠️ Subscribe to single node only -- ⚠️ Count averaging may be inaccurate -- ⚠️ Reset not atomic +- Higher latency +- More network overhead +- Subscribe to single node only +- Count averaging may be inaccurate +- Reset not atomic **Consistency Model**: ``` @@ -458,26 +456,26 @@ Count decremented if: - Average count used for reads Notification sent if: - - Any node reaches zero - - Published to all nodes + - The decrement drives a node's count to zero + - Decrement + publish are atomic on that node (single Lua script) ``` ### Redisson **Safety Guarantees**: -- ✅ Atomic operations (Lua scripts) -- ✅ Accurate count -- ✅ Pub/sub notifications -- ✅ Async/reactive support -- ✅ Low latency +- Atomic operations (Lua scripts) +- Accurate count +- Pub/sub notifications +- Async/reactive support +- Low latency **Potential Issues**: -- ⚠️ Single point of failure -- ⚠️ No quorum mechanism -- ⚠️ No automatic expiration -- ⚠️ Polling in await loop -- ⚠️ Reset not atomic +- Single point of failure +- No quorum mechanism +- No automatic expiration +- Polling in await loop +- Reset not atomic **Consistency Model**: ``` @@ -492,22 +490,22 @@ Notification sent if: ## Feature Comparison Table -| Feature | redlock4j | Redisson | -|---------|-----------|----------| -| **Data Model** | Counter on all nodes | Single counter | -| **Quorum** | Yes | No | -| **Fault Tolerance** | Survives minority failures | Single point of failure | -| **Initialization** | Automatic in constructor | Explicit via trySetCount() | -| **Expiration** | Automatic (10x timeout) | No automatic expiration | -| **Count Accuracy** | Average across nodes | Exact | -| **Atomicity** | Non-atomic (DECR + PUBLISH) | Atomic (Lua script) | -| **Subscription** | Single node | Managed pub/sub service | -| **Reset** | Supported (non-standard) | Supported via delete + trySetCount | -| **Async Support** | No | Yes | -| **Reactive Support** | No | Yes | -| **Performance** | O(M) | O(1) for countDown | -| **Latency** | Higher | Lower | -| **Network Overhead** | High | Low | +| Feature | redlock4j | Redisson | +|----------------------|---------------------------------------------|------------------------------------| +| **Data Model** | Counter on all nodes | Single counter | +| **Quorum** | Yes | No | +| **Fault Tolerance** | Survives minority failures | Single point of failure | +| **Initialization** | Automatic via manager factory | Explicit via trySetCount() | +| **Expiration** | Automatic (10x timeout) | No automatic expiration | +| **Count Accuracy** | Average across nodes | Exact | +| **Atomicity** | Atomic per-node DECR + PUBLISH (Lua script) | Atomic (Lua script) | +| **Subscription** | Single node | Managed pub/sub service | +| **Reset** | Supported (non-standard) | Supported via delete + trySetCount | +| **Async Support** | No | Yes | +| **Reactive Support** | No | Yes | +| **Performance** | O(M) | O(1) for countDown | +| **Latency** | Higher | Lower | +| **Network Overhead** | High | Low | ## Use Case Comparison @@ -523,19 +521,13 @@ Notification sent if: **Example Scenarios**: ```java // Distributed service startup coordination -RedlockCountDownLatch startupLatch = new RedlockCountDownLatch( - "app:startup", 5, redisDrivers, config -); +RedlockCountDownLatch startupLatch = manager.createCountDownLatch("app:startup", 5); // Batch job coordination -RedlockCountDownLatch batchLatch = new RedlockCountDownLatch( - "batch:job:123", 100, redisDrivers, config -); +RedlockCountDownLatch batchLatch = manager.createCountDownLatch("batch:job:123", 100); // Multi-stage workflow -RedlockCountDownLatch stageLatch = new RedlockCountDownLatch( - "workflow:stage1", 10, redisDrivers, config -); +RedlockCountDownLatch stageLatch = manager.createCountDownLatch("workflow:stage1", 10); ``` ### Redisson RedissonCountDownLatch @@ -570,20 +562,20 @@ reusableLatch.trySetCount(5); // Reuse ### Choose redlock4j RedlockCountDownLatch when: -- ✅ Need quorum-based distributed consistency -- ✅ Require fault tolerance (multi-master) -- ✅ Automatic expiration is important -- ✅ Can tolerate higher latency -- ✅ Count averaging is acceptable +- Need quorum-based distributed consistency +- Require fault tolerance (multi-master) +- Automatic expiration is important +- Can tolerate higher latency +- Count averaging is acceptable ### Choose Redisson RedissonCountDownLatch when: -- ✅ Single Redis instance is acceptable -- ✅ Need high throughput / low latency -- ✅ Require exact count tracking -- ✅ Need async/reactive APIs -- ✅ Want atomic operations -- ✅ Explicit initialization preferred +- Single Redis instance is acceptable +- Need high throughput / low latency +- Require exact count tracking +- Need async/reactive APIs +- Want atomic operations +- Explicit initialization preferred ## Migration Considerations @@ -596,9 +588,7 @@ latch.trySetCount(3); latch.await(); // After (redlock4j) -RedlockCountDownLatch latch = new RedlockCountDownLatch( - "startup", 3, redisDrivers, config -); +RedlockCountDownLatch latch = manager.createCountDownLatch("startup", 3); latch.await(); ``` @@ -616,9 +606,7 @@ latch.await(); ```java // Before (redlock4j) -RedlockCountDownLatch latch = new RedlockCountDownLatch( - "startup", 3, redisDrivers, config -); +RedlockCountDownLatch latch = manager.createCountDownLatch("startup", 3); // After (Redisson) RCountDownLatch latch = redisson.getCountDownLatch("startup"); diff --git a/docs/comparison/fairlock-implementation.md b/docs/comparison/fairlock-implementation.md index d991d47..44cf7c7 100644 --- a/docs/comparison/fairlock-implementation.md +++ b/docs/comparison/fairlock-implementation.md @@ -81,14 +81,14 @@ redis.call('zrem', KEYS[3], ARGV[2]) - **Clock Dependency**: Relies on reasonably synchronized clocks ```java -int votesForFront = 0; -for (RedisDriver driver : redisDrivers) { +// The execution strategy fans out to the appropriate nodes and applies the +// quorum rule; a node "votes" for the token if it is at the front of that +// node's queue. +int votesForFront = executionStrategy.executeOnNodes(driver -> { List firstElements = driver.zRange(queueKey, 0, 0); - if (!firstElements.isEmpty() && token.equals(firstElements.get(0))) { - votesForFront++; - } -} -return votesForFront >= config.getQuorum(); + return !firstElements.isEmpty() && token.equals(firstElements.get(0)); +}); +return executionStrategy.isSuccessful(votesForFront); ``` ### Redisson @@ -119,14 +119,15 @@ end ```java private void addToQueue(String token, long timestamp) { - // Add to queue - for (RedisDriver driver : redisDrivers) { + // Add to queue on the appropriate nodes via the execution strategy + executionStrategy.executeOnNodes(driver -> { driver.zAdd(queueKey, timestamp, token); - } + return true; + }); - // Cleanup expired entries - long expirationThreshold = System.currentTimeMillis() - - config.getDefaultLockTimeoutMs() * 2; + // Cleanup expired entries (older than 2x the lock timeout) + Duration expirationAge = config.getDefaultLockTimeout().multipliedBy(2); + long expirationThreshold = System.currentTimeMillis() - expirationAge.toMillis(); cleanupExpiredQueueEntries(expirationThreshold); } ``` @@ -373,35 +374,35 @@ end **Lines of Code**: ~390 lines **Pros**: -- ✅ Simpler to understand -- ✅ Single data structure (sorted set) -- ✅ Fewer Redis operations -- ✅ Less state to manage -- ✅ Timestamp-based ordering is intuitive +- Simpler to understand +- Single data structure (sorted set) +- Fewer Redis operations +- Less state to manage +- Timestamp-based ordering is intuitive **Cons**: -- ❌ Clock skew sensitivity -- ❌ Polling-based (no notifications) -- ❌ Cleanup only on addToQueue -- ❌ Less sophisticated timeout handling +- Clock skew sensitivity +- Polling-based (no notifications) +- Cleanup only on addToQueue +- Less sophisticated timeout handling ### Redisson **Lines of Code**: ~350 lines (but denser Lua scripts) **Pros**: -- ✅ Robust stale thread handling -- ✅ Better timeout estimation -- ✅ Explicit thread notification (pub/sub) -- ✅ Less clock-dependent (list ordering) -- ✅ Production-hardened +- Robust stale thread handling +- Better timeout estimation +- Explicit thread notification (pub/sub) +- Less clock-dependent (list ordering) +- Production-hardened **Cons**: -- ❌ More complex implementation -- ❌ Two data structures to maintain -- ❌ More Redis operations per attempt -- ❌ Cleanup overhead on every operation -- ❌ Requires pub/sub infrastructure +- More complex implementation +- Two data structures to maintain +- More Redis operations per attempt +- Cleanup overhead on every operation +- Requires pub/sub infrastructure ## Performance Comparison @@ -436,6 +437,19 @@ end **Total**: Fewer round trips, but heavier Lua scripts +### Measured Performance + +Benchmark: 5 clients, 50 ms work per cycle, 3-node Redis 7 cluster, 60 s measurement (full methodology in [Architecture › Performance Analysis](../guide/architecture.md#performance-analysis)). + +| Implementation | Ops/s | p50 (ms) | p99 (ms) | mean (ms) | success | +|---------------------------|-----------:|---------:|---------:|----------:|--------:| +| **Redisson** | **16.99** | **221** | **230** | 248 | 100 % | +| redlock4j-singlenode | 12.42 | 332 | 435 | 349 | 100 % | +| redlock4j-3node (Jedis) | 2.95 | 1,666 | 2,457 | 1,642 | 100 % | +| redlock4j-3node (Lettuce) | 2.87 | 1,667 | 2,549 | 1,685 | 100 % | + +**Reading the numbers**: Redisson leads on both throughput and p99 in single-node mode because its atomic Lua scripts collapse the queue-check + acquire into one round trip. redlock4j's 3-node mode is throughput-limited by the per-attempt quorum head-check (`ZRANGE` on every node). The structural fix is tracked as P2-8 (single-shot atomic head-check + acquire) in `redlock4j-benchmark/benchmark-analysis.md` §8. Note: FairLock deliberately uses fixed-interval polling — exponential backoff hurts FairLock because head-of-queue waiters must claim the lock the instant the holder releases. + ## Edge Cases & Robustness ### redlock4j @@ -529,8 +543,8 @@ private final ThreadLocal lockState = new ThreadLocal<>(); private static class LockState { final String lockValue; final String queueToken; - final long acquisitionTime; - final long validityTime; + final Instant acquisitionTime; + final Duration validityDuration; int holdCount; } diff --git a/docs/comparison/multilock-implementation.md b/docs/comparison/multilock-implementation.md index 00f9c09..b209afb 100644 --- a/docs/comparison/multilock-implementation.md +++ b/docs/comparison/multilock-implementation.md @@ -15,10 +15,8 @@ Both libraries implement multi-lock functionality to atomically acquire multiple **Use Case**: When you need to lock multiple resources simultaneously (e.g., transferring between multiple bank accounts) ```java -MultiLock multiLock = new MultiLock( - Arrays.asList("account:1", "account:2", "account:3"), - redisDrivers, - config +Lock multiLock = manager.createMultiLock( + Arrays.asList("account:1", "account:2", "account:3") ); multiLock.lock(); try { @@ -65,7 +63,7 @@ try { ``` MultiLock ├─ List lockKeys (sorted) - ├─ List redisDrivers (shared cluster) + ├─ LockExecutionStrategy (node fan-out + quorum) ├─ Quorum-based acquisition per resource └─ Thread-local state tracking ``` @@ -147,16 +145,16 @@ private MultiLockResult attemptMultiLock() { lockValues.put(key, generateLockValue()); } - // 2. Try to acquire ALL locks on EACH Redis node - int successfulNodes = 0; - for (RedisDriver driver : redisDrivers) { - if (acquireAllOnNode(driver, lockValues)) { - successfulNodes++; - } - } + // 2. Try to acquire ALL locks on EACH Redis node. + // The execution strategy fans out to the nodes and returns how many + // succeeded. + int successfulNodes = executionStrategy.executeOnNodes( + driver -> acquireAllOnNode(driver, lockValues)); - // 3. Check quorum and validity - boolean acquired = successfulNodes >= config.getQuorum() + // 3. Check quorum (via the strategy) and validity + long validityTime = executionStrategy.calculateValidityTime( + config.getDefaultLockTimeout().toMillis(), startTime); + boolean acquired = executionStrategy.isSuccessful(successfulNodes) && validityTime > 0; // 4. Rollback if failed @@ -357,8 +355,8 @@ private final ThreadLocal lockState = new ThreadLocal<>(); private static class LockState { final Map lockValues; // All lock values - final long acquisitionTime; - final long validityTime; + final Instant acquisitionTime; + final Duration validityDuration; int holdCount; } @@ -423,11 +421,12 @@ long startTime = System.currentTimeMillis(); // Acquire all locks... -long elapsedTime = System.currentTimeMillis() - startTime; -long driftTime = (long) (config.getDefaultLockTimeoutMs() * config.getClockDriftFactor()) + 2; -long validityTime = config.getDefaultLockTimeoutMs() - elapsedTime - driftTime; +// The execution strategy computes the remaining validity, applying clock-drift +// compensation to the configured lock timeout (a Duration). +long validityTime = executionStrategy.calculateValidityTime( + config.getDefaultLockTimeout().toMillis(), startTime); -boolean acquired = successfulNodes >= config.getQuorum() && validityTime > 0; +boolean acquired = executionStrategy.isSuccessful(successfulNodes) && validityTime > 0; ``` **Characteristics**: @@ -508,6 +507,17 @@ Attempt 2: Total: Variable, sequential ``` +### Measured Performance + +Benchmark: 5 clients, 5 resources per MultiLock, 50 ms work per cycle, 3-node Redis 7 cluster, 60 s measurement (full methodology in [Architecture › Performance Analysis](../guide/architecture.md#performance-analysis)). + +| Implementation | Ops/s | p50 (ms) | p99 (ms) | mean (ms) | success | +|------------------------|-----------:|-----------:|------------:|-----------:|---------:| +| Redisson | 17.05 | 190.2 | 1,192.2 | 272.9 | 100 % | +| redlock4j-singlenode | 16.97 | 202.9 | 1,709.5 | 303.0 | 100 % | +| **redlock4j-3node** | **17.72** | **171.0** | **1,057.6** | **237.4** | 100 % | + +**Reading the numbers**: `redlock4j-3node` leads on throughput, p50, p99, and mean — the only primitive where the 3-node Redlock variant beats every single-node implementation in the field. The parallel multi-node I/O (each `SET NX` for all 5 resources fans out across all 3 nodes simultaneously) is what makes this possible. ## Use Case Differences @@ -522,15 +532,13 @@ Total: Variable, sequential **Example Scenarios**: ```java // Bank transfer between multiple accounts -MultiLock lock = new MultiLock( - Arrays.asList("account:1", "account:2", "account:3"), - redisDrivers, config +Lock lock = manager.createMultiLock( + Arrays.asList("account:1", "account:2", "account:3") ); // Inventory management across warehouses -MultiLock lock = new MultiLock( - Arrays.asList("warehouse:A:item:123", "warehouse:B:item:123"), - redisDrivers, config +Lock lock = manager.createMultiLock( + Arrays.asList("warehouse:A:item:123", "warehouse:B:item:123") ); ``` @@ -561,30 +569,30 @@ RedissonMultiLock multiLock = new RedissonMultiLock(fairLock, readLock); ### redlock4j **Safety Guarantees**: -- ✅ Deadlock-free (automatic key sorting) -- ✅ Quorum-based consistency -- ✅ All-or-nothing atomicity -- ✅ Clock drift compensation -- ✅ Validity time enforcement +- Deadlock-free (automatic key sorting) +- Quorum-based consistency +- All-or-nothing atomicity +- Clock drift compensation +- Validity time enforcement **Potential Issues**: -- ⚠️ All resources must be on same Redis cluster -- ⚠️ Higher latency due to quorum requirement -- ⚠️ More network overhead (N×M operations) +- All resources must be on same Redis cluster +- Higher latency due to quorum requirement +- More network overhead (N×M operations) ### Redisson **Safety Guarantees**: -- ✅ Flexible lock composition -- ✅ Works across different Redis instances -- ✅ Extensible failure tolerance -- ✅ Async/reactive support +- Flexible lock composition +- Works across different Redis instances +- Extensible failure tolerance +- Async/reactive support **Potential Issues**: -- ⚠️ No automatic deadlock prevention -- ⚠️ Developer must ensure lock ordering -- ⚠️ Sequential acquisition (slower for many locks) -- ⚠️ No quorum mechanism by default +- No automatic deadlock prevention +- Developer must ensure lock ordering +- Sequential acquisition (slower for many locks) +- No quorum mechanism by default ## Complexity Analysis @@ -593,34 +601,34 @@ RedissonMultiLock multiLock = new RedissonMultiLock(fairLock, readLock); **Code Complexity**: ~370 lines **Pros**: -- ✅ Integrated Redlock implementation -- ✅ Automatic deadlock prevention -- ✅ Clear all-or-nothing semantics -- ✅ Single validity time -- ✅ Thread-local state (fast reentrancy) +- Integrated Redlock implementation +- Automatic deadlock prevention +- Clear all-or-nothing semantics +- Single validity time +- Thread-local state (fast reentrancy) **Cons**: -- ❌ Limited to single Redis cluster -- ❌ More Redis operations -- ❌ Higher network overhead -- ❌ Less flexible composition +- Limited to single Redis cluster +- More Redis operations +- Higher network overhead +- Less flexible composition ### Redisson **Code Complexity**: ~450 lines (with async support) **Pros**: -- ✅ Works across multiple Redis instances -- ✅ Flexible lock composition -- ✅ Extensible (can override failedLocksLimit) -- ✅ Async/reactive support -- ✅ Can mix different lock types +- Works across multiple Redis instances +- Flexible lock composition +- Extensible (can override failedLocksLimit) +- Async/reactive support +- Can mix different lock types **Cons**: -- ❌ No deadlock prevention -- ❌ Sequential acquisition -- ❌ More complex retry logic -- ❌ Requires careful lock ordering +- No deadlock prevention +- Sequential acquisition +- More complex retry logic +- Requires careful lock ordering ## RedissonRedLock vs redlock4j MultiLock @@ -646,38 +654,38 @@ public class RedissonRedLock extends RedissonMultiLock { **Comparison with redlock4j MultiLock**: -| Feature | redlock4j MultiLock | RedissonRedLock | -|---------|---------------------|-----------------| -| **Purpose** | Multiple resources on same cluster | Multiple independent Redis instances | -| **Quorum** | Per-resource across nodes | Across different locks | -| **Deadlock Prevention** | Automatic (sorted keys) | Manual (developer responsibility) | -| **Acquisition** | Parallel per node | Sequential across locks | -| **Use Case** | Multi-resource locking | Multi-instance Redlock | +| Feature | redlock4j MultiLock | RedissonRedLock | +|-------------------------|------------------------------------|--------------------------------------| +| **Purpose** | Multiple resources on same cluster | Multiple independent Redis instances | +| **Quorum** | Per-resource across nodes | Across different locks | +| **Deadlock Prevention** | Automatic (sorted keys) | Manual (developer responsibility) | +| **Acquisition** | Parallel per node | Sequential across locks | +| **Use Case** | Multi-resource locking | Multi-instance Redlock | ## Recommendations ### Choose redlock4j MultiLock when: -- ✅ Locking multiple resources on the same Redis cluster -- ✅ Need automatic deadlock prevention -- ✅ Require strict all-or-nothing semantics -- ✅ Want quorum-based safety per resource -- ✅ Prefer simpler, integrated solution +- Locking multiple resources on the same Redis cluster +- Need automatic deadlock prevention +- Require strict all-or-nothing semantics +- Want quorum-based safety per resource +- Prefer simpler, integrated solution ### Choose Redisson RedissonMultiLock when: -- ✅ Need to coordinate locks across different Redis instances -- ✅ Want to compose different lock types -- ✅ Require async/reactive support -- ✅ Can manage lock ordering manually -- ✅ Need flexible failure tolerance +- Need to coordinate locks across different Redis instances +- Want to compose different lock types +- Require async/reactive support +- Can manage lock ordering manually +- Need flexible failure tolerance ### Choose Redisson RedissonRedLock when: -- ✅ Implementing Redlock across multiple Redis instances -- ✅ Each lock represents a different Redis master -- ✅ Need quorum-based distributed locking -- ✅ Can ensure proper lock ordering +- Implementing Redlock across multiple Redis instances +- Each lock represents a different Redis master +- Need quorum-based distributed locking +- Can ensure proper lock ordering ## Migration Considerations @@ -697,10 +705,8 @@ try { } // After (redlock4j) -MultiLock multiLock = new MultiLock( - Arrays.asList("account:1", "account:2", "account:3"), - redisDrivers, - config +Lock multiLock = manager.createMultiLock( + Arrays.asList("account:1", "account:2", "account:3") ); multiLock.lock(); try { @@ -723,10 +729,8 @@ try { ```java // Before (redlock4j) -MultiLock multiLock = new MultiLock( - Arrays.asList("resource1", "resource2", "resource3"), - redisDrivers, - config +Lock multiLock = manager.createMultiLock( + Arrays.asList("resource1", "resource2", "resource3") ); // After (Redisson) - if using multiple instances @@ -772,42 +776,4 @@ Choose based on your specific requirements: - **Same cluster, multiple resources** → redlock4j MultiLock - **Multiple instances, flexible composition** → Redisson RedissonMultiLock - **Multiple instances, Redlock algorithm** → Redisson RedissonRedLock - boolean acquired = successfulNodes >= config.getQuorum() - && validityTime > 0; - - // 4. Rollback if failed - if (!acquired) { - releaseAllLocks(lockValues); - } - - return new MultiLockResult(acquired, validityTime, lockValues, ...); -} -``` - -**Per-Node Acquisition**: -```java -private boolean acquireAllOnNode(RedisDriver driver, Map lockValues) { - List acquiredKeys = new ArrayList<>(); - - for (String key : lockKeys) { - if (driver.setIfNotExists(key, lockValue, timeout)) { - acquiredKeys.add(key); - } else { - // Failed - rollback this node - rollbackOnNode(driver, lockValues, acquiredKeys); - return false; - } - } - return true; -} -``` - -**Flow**: -1. Generate unique lock values for all keys -2. For each Redis node: - - Try to acquire ALL locks - - If any fails, rollback that node -3. Check if quorum achieved -4. If not, release all acquired locks - diff --git a/docs/comparison/readwritelock-implementation.md b/docs/comparison/readwritelock-implementation.md index 41458ee..e498107 100644 --- a/docs/comparison/readwritelock-implementation.md +++ b/docs/comparison/readwritelock-implementation.md @@ -15,9 +15,7 @@ Both libraries implement distributed read-write locks to allow multiple concurre **Use Case**: Scenarios requiring strong consistency for read-heavy workloads ```java -RedlockReadWriteLock rwLock = new RedlockReadWriteLock( - "resource", redisDrivers, config -); +RedlockReadWriteLock rwLock = manager.createReadWriteLock("resource"); // Multiple readers can acquire simultaneously rwLock.readLock().lock(); @@ -139,16 +137,46 @@ RedissonReadWriteLock **Algorithm**: Check write lock, then increment reader count ```java -public boolean tryLock(long time, TimeUnit unit) { - // 1. Check reentrancy +public boolean tryLock(Duration timeout) throws InterruptedException { + // 1. Check reentrancy (thread-local state) LockState currentState = lockState.get(); if (currentState != null && currentState.isValid()) { currentState.incrementHoldCount(); return true; } + Instant deadline = Instant.now().plus(timeout); + // 2. Retry loop + for (int attempt = 0; attempt <= config.getMaxRetryAttempts(); attempt++) { + // 3. Only proceed when no writer holds the lock + if (!isWriteLockHeld()) { + String lockValue = generateLockValue(); + // 4. Increment the reader count (quorum-based via the strategy) + if (incrementReaderCount(lockValue)) { + lockState.set(new LockState(lockValue, Instant.now(), + config.getDefaultLockTimeout())); + return true; + } + } + + // 5. Stop once the timeout is exhausted, otherwise wait and retry + Duration remaining = Duration.between(Instant.now(), deadline); + if (!timeout.isZero() && remaining.isNegative()) { + break; + } + waitForLockRelease(remaining.toMillis(), attempt); + } + + return false; +} +``` +**Flow**: +1. Check thread-local reentrancy +2. Poll until no writer holds the lock +3. Increment the reader count (quorum-based) +4. Track validity in thread-local state ### Redisson @@ -212,17 +240,15 @@ public boolean tryLock(long time, TimeUnit unit) { } private boolean hasActiveReaders() { - int nodesWithoutReaders = 0; - - for (RedisDriver driver : redisDrivers) { + // The execution strategy fans out to the nodes; a node "votes" when it + // reports no active readers. + int nodesWithoutReaders = executionStrategy.executeOnNodes(driver -> { String countStr = driver.get(readCountKey); - if (countStr == null || Long.parseLong(countStr) <= 0) { - nodesWithoutReaders++; - } - } + return countStr == null || Long.parseLong(countStr) <= 0; + }); - // Quorum of nodes must have no readers - return nodesWithoutReaders < quorum; + // Readers are considered gone once a quorum of nodes report none + return !executionStrategy.isSuccessful(nodesWithoutReaders); } ``` @@ -309,7 +335,8 @@ public void unlock() { } private void decrementReaderCount(String lockValue) { - for (RedisDriver driver : redisDrivers) { + // Fan out to the appropriate nodes via the execution strategy + executionStrategy.executeOnNodes(driver -> { // Decrement counter long count = driver.decr(readCountKey); @@ -320,7 +347,8 @@ private void decrementReaderCount(String lockValue) { if (count <= 0) { driver.del(readCountKey); } - } + return true; + }); } ``` @@ -538,23 +566,48 @@ Writer 1: still waiting... - Single round trip - Pub/sub notification overhead +### Measured Performance + +Benchmark: 10 clients (8 readers + 2 writers), 50 ms work per cycle, 3-node Redis 7 cluster, 60 s measurement (full methodology in [Architecture › Performance Analysis](../guide/architecture.md#performance-analysis)). + +**Readers**: + +| Implementation | Ops/s | p50 (ms) | p99 (ms) | mean (ms) | success | +|-------------------------------|------------:|------------:|------------:|-----------:|---------:| +| **Redisson reader** | **151.65** | **0.79** | **5.82** | **1.32** | 100 % | +| redlock4j-3node reader | 95.41 | 2.72 | 348.4 | 30.4 | 100 % | +| redlock4j-singlenode reader | 74.66 | 12.54 | 340.6 | 54.2 | 100 % | + +**Writers**: + +| Implementation | Ops/s | p50 (ms) | p99 (ms) | mean (ms) | success | +|-------------------------------|-----------:|----------:|----------:|-----------:|---------:| +| Redisson writer | 0.21 | 8,649 | 16,821 | 9,680 | 100 % | +| redlock4j-singlenode writer | 15.63 | 57.8 | 755.6 | 72.4 | 100 % | +| **redlock4j-3node writer** | **16.42** | **64.1** | **104.2** | **63.5** | 100 % | + +**Reading the numbers**: + +- **Readers**: Redisson wins decisively. Its reader implementation tracks active readers via an in-process semaphore counter and only hits Redis on the transition; redlock4j hits Redis on every read acquire. This is an architectural choice, not a regression — the trade-off is that Redisson's reader count is local to one JVM while redlock4j's is distributed. +- **Writers**: redlock4j wins decisively on both throughput and tail latency. Redisson exhibits writer starvation under reader load (p99 = 16.8 s); redlock4j's 3-node mode keeps writer p99 under 105 ms thanks to the parallel multi-node acquire path. + ## Safety & Correctness ### redlock4j **Safety Guarantees**: -- ✅ Quorum-based consistency -- ✅ Survives minority node failures -- ✅ Multiple readers guaranteed -- ✅ Exclusive writer guaranteed -- ✅ No single point of failure +- Quorum-based consistency +- Survives minority node failures +- Multiple readers guaranteed +- Exclusive writer guaranteed +- No single point of failure **Potential Issues**: -- ⚠️ Higher latency -- ⚠️ More network overhead -- ⚠️ Polling-based (no notifications) -- ⚠️ Potential writer starvation -- ⚠️ No lock upgrade/downgrade +- Higher latency +- More network overhead +- Polling-based (no notifications) +- Potential writer starvation +- No lock upgrade/downgrade **Consistency Model**: ``` @@ -570,18 +623,18 @@ Write lock acquired if: ### Redisson **Safety Guarantees**: -- ✅ Atomic operations (Lua scripts) -- ✅ Multiple readers guaranteed -- ✅ Exclusive writer guaranteed -- ✅ Lock upgrade/downgrade support -- ✅ Pub/sub notifications -- ✅ Async/reactive support +- Atomic operations (Lua scripts) +- Multiple readers guaranteed +- Exclusive writer guaranteed +- Lock upgrade/downgrade support +- Pub/sub notifications +- Async/reactive support **Potential Issues**: -- ⚠️ Single point of failure -- ⚠️ No quorum mechanism -- ⚠️ Potential writer starvation (non-fair) -- ⚠️ More complex Lua scripts +- Single point of failure +- No quorum mechanism +- Potential writer starvation (non-fair) +- More complex Lua scripts **Consistency Model**: ``` @@ -605,14 +658,10 @@ Lock acquired if: **Example Scenarios**: ```java // Distributed cache with strong consistency -RedlockReadWriteLock cacheLock = new RedlockReadWriteLock( - "cache:users", redisDrivers, config -); +RedlockReadWriteLock cacheLock = manager.createReadWriteLock("cache:users"); // Configuration management -RedlockReadWriteLock configLock = new RedlockReadWriteLock( - "config:app", redisDrivers, config -); +RedlockReadWriteLock configLock = manager.createReadWriteLock("config:app"); ``` ### Redisson RedissonReadWriteLock @@ -650,40 +699,40 @@ try { ## Feature Comparison Table -| Feature | redlock4j | Redisson | -|---------|-----------|----------| -| **Data Model** | Counter + individual keys | Hash with mode field | -| **Quorum** | Yes | No | -| **Fault Tolerance** | Survives minority failures | Single point of failure | -| **Lock Upgrade** | No | Yes (single reader only) | -| **Lock Downgrade** | No | Yes | -| **Waiting Mechanism** | Polling | Pub/sub | -| **Fairness** | Non-fair | Non-fair (fair variant available) | -| **Async Support** | No | Yes | -| **Reactive Support** | No | Yes | -| **Performance** | O(M) reads, O(N×M) writes | O(1) | -| **Latency** | Higher | Lower | -| **Network Overhead** | High | Low | -| **Atomicity** | Quorum-based | Lua scripts | +| Feature | redlock4j | Redisson | +|-----------------------|----------------------------|-----------------------------------| +| **Data Model** | Counter + individual keys | Hash with mode field | +| **Quorum** | Yes | No | +| **Fault Tolerance** | Survives minority failures | Single point of failure | +| **Lock Upgrade** | No | Yes (single reader only) | +| **Lock Downgrade** | No | Yes | +| **Waiting Mechanism** | Polling | Pub/sub | +| **Fairness** | Non-fair | Non-fair (fair variant available) | +| **Async Support** | No | Yes | +| **Reactive Support** | No | Yes | +| **Performance** | O(M) reads, O(N×M) writes | O(1) | +| **Latency** | Higher | Lower | +| **Network Overhead** | High | Low | +| **Atomicity** | Quorum-based | Lua scripts | ## Recommendations ### Choose redlock4j RedlockReadWriteLock when: -- ✅ Need quorum-based distributed consistency -- ✅ Require fault tolerance (multi-master) -- ✅ Read-heavy workloads with strong consistency -- ✅ Can tolerate higher latency -- ✅ Don't need lock upgrade/downgrade +- Need quorum-based distributed consistency +- Require fault tolerance (multi-master) +- Read-heavy workloads with strong consistency +- Can tolerate higher latency +- Don't need lock upgrade/downgrade ### Choose Redisson RedissonReadWriteLock when: -- ✅ Single Redis instance is acceptable -- ✅ Need high throughput / low latency -- ✅ Require lock upgrade/downgrade -- ✅ Need async/reactive APIs -- ✅ Want pub/sub notifications -- ✅ Need fair ordering (use RedissonFairReadWriteLock) +- Single Redis instance is acceptable +- Need high throughput / low latency +- Require lock upgrade/downgrade +- Need async/reactive APIs +- Want pub/sub notifications +- Need fair ordering (use RedissonFairReadWriteLock) ## Conclusion diff --git a/docs/comparison/semaphore-implementation.md b/docs/comparison/semaphore-implementation.md index ab41a90..1015941 100644 --- a/docs/comparison/semaphore-implementation.md +++ b/docs/comparison/semaphore-implementation.md @@ -16,12 +16,10 @@ Both libraries implement distributed semaphores to limit concurrent access to re ```java // Create a semaphore with 5 permits -RedlockSemaphore semaphore = new RedlockSemaphore( - "api-limiter", 5, redisDrivers, config -); +RedlockSemaphore semaphore = manager.createSemaphore("api-limiter", 5); // Acquire a permit -if (semaphore.tryAcquire(5, TimeUnit.SECONDS)) { +if (semaphore.tryAcquire(Duration.ofSeconds(5))) { try { // Perform rate-limited operation callExternalAPI(); @@ -120,22 +118,20 @@ RedissonSemaphore ```java private SemaphoreResult attemptAcquire(int permits) { List permitIds = new ArrayList<>(); + Instant startTime = Instant.now(); // 1. For each permit needed for (int i = 0; i < permits; i++) { - String permitId = generatePermitId(); + String permitId = generateLockValue(); String permitKey = semaphoreKey + ":permit:" + permitId; - // 2. Try to acquire on each Redis node - int successfulNodes = 0; - for (RedisDriver driver : redisDrivers) { - if (driver.setIfNotExists(permitKey, permitId, timeout)) { - successfulNodes++; - } - } + // 2. Acquire the permit key on the nodes via the execution strategy + // (fan-out + quorum, same path as a standard lock) + LockResult result = executionStrategy.acquireLock(permitKey, permitId, + config.getDefaultLockTimeout().toMillis()); - // 3. Check quorum for this permit - if (successfulNodes >= config.getQuorum()) { + // 3. Check the result for this permit + if (result.isAcquired()) { permitIds.add(permitId); } else { // Failed - rollback all permits @@ -144,11 +140,19 @@ private SemaphoreResult attemptAcquire(int permits) { } } - // 4. Check validity time - long validityTime = timeout - elapsedTime - driftTime; + // 4. Compute validity via the strategy (handles single- vs multi-node drift) + Duration elapsed = Duration.between(startTime, Instant.now()); + long validityTime = executionStrategy.calculateValidityTime( + config.getDefaultLockTimeout().toMillis(), elapsed.toMillis()); boolean acquired = permitIds.size() == permits && validityTime > 0; - + if (!acquired) { + releasePermits(permitIds); + return new SemaphoreResult(false, 0, new ArrayList<>()); + } + return new SemaphoreResult(true, validityTime, permitIds); +} +``` ### Redisson @@ -211,10 +215,11 @@ private void releasePermits(List permitIds) { for (String permitId : permitIds) { String permitKey = semaphoreKey + ":permit:" + permitId; - // Delete on all nodes - for (RedisDriver driver : redisDrivers) { + // Delete on the appropriate nodes via the execution strategy + executionStrategy.executeOnNodes(driver -> { driver.deleteIfValueMatches(permitKey, permitId); - } + return true; + }); } } ``` @@ -352,9 +357,7 @@ public RFuture availablePermitsAsync() { ```java // Create and use immediately -RedlockSemaphore semaphore = new RedlockSemaphore( - "api-limiter", 5, redisDrivers, config -); +RedlockSemaphore semaphore = manager.createSemaphore("api-limiter", 5); // No need to set permits - maxPermits is just a limit semaphore.tryAcquire(); @@ -411,11 +414,11 @@ semaphore.addPermits(3); ```java private static class PermitState { final List permitIds; - final long acquisitionTime; - final long validityTime; // Calculated validity + final Instant acquisitionTime; + final Duration validityDuration; // Calculated validity boolean isValid() { - return System.currentTimeMillis() < acquisitionTime + validityTime; + return Instant.now().isBefore(acquisitionTime.plus(validityDuration)); } } ``` @@ -428,9 +431,11 @@ private static class PermitState { **Validity Calculation**: ```java -long elapsedTime = System.currentTimeMillis() - startTime; -long driftTime = (long) (timeout * clockDriftFactor) + 2; -long validityTime = timeout - elapsedTime - driftTime; +// Delegated to the execution strategy, which applies clock-drift +// compensation to the configured lock timeout (a Duration). +Duration elapsed = Duration.between(startTime, Instant.now()); +long validityTime = executionStrategy.calculateValidityTime( + config.getDefaultLockTimeout().toMillis(), elapsed.toMillis()); ``` ### Redisson @@ -519,23 +524,35 @@ Release: Total: 1 operation ``` +### Measured Performance + +Benchmark: 5 clients, 3 permits, 50 ms work per cycle, 3-node Redis 7 cluster, 60 s measurement (full methodology in [Architecture › Performance Analysis](../guide/architecture.md#performance-analysis)). + +| Implementation | Ops/s | p50 (ms) | p99 (ms) | mean (ms) | success | +|--------------------------|-----------:|----------:|-----------:|-----------:|---------:| +| Redisson | 54.71 | 1.00 | 386.5 | 37.9 | 100 % | +| **redlock4j-singlenode** | **91.09** | **0.73** | **2.21** | **0.82** | 100 % | +| redlock4j-3node | 87.50 | 1.77 | 4.20 | 1.84 | 100 % | + +**Reading the numbers**: redlock4j wins decisively on every metric — **~1.7× Redisson's throughput**, **~175× lower p99**, and **~46× lower mean latency**. Redisson's pub/sub semaphore can stall waiters when the publish round-trips with the release; redlock4j's simpler per-permit `SET NX` approach turns out to be both faster and more predictable in the steady state. The 3-node variant pays only a ~4 % throughput tax for full quorum safety. + ## Safety & Correctness ### redlock4j **Safety Guarantees**: -- ✅ Quorum-based consistency -- ✅ Survives minority node failures -- ✅ Clock drift compensation -- ✅ Automatic permit expiration -- ✅ No single point of failure +- Quorum-based consistency +- Survives minority node failures +- Clock drift compensation +- Automatic permit expiration +- No single point of failure **Potential Issues**: -- ⚠️ Higher latency -- ⚠️ More network overhead -- ⚠️ `availablePermits()` not accurate -- ⚠️ No permit counting mechanism -- ⚠️ Polling-based (no notifications) +- Higher latency +- More network overhead +- `availablePermits()` not accurate +- No permit counting mechanism +- Polling-based (no notifications) **Consistency Model**: ``` @@ -548,18 +565,18 @@ Permit acquired if: ### Redisson **Safety Guarantees**: -- ✅ Atomic operations (Lua scripts) -- ✅ Accurate permit counting -- ✅ Efficient pub/sub notifications -- ✅ Async/reactive support -- ✅ Low latency +- Atomic operations (Lua scripts) +- Accurate permit counting +- Efficient pub/sub notifications +- Async/reactive support +- Low latency **Potential Issues**: -- ⚠️ Single point of failure (single instance) -- ⚠️ No quorum mechanism -- ⚠️ No automatic permit expiration -- ⚠️ Permits persist indefinitely -- ⚠️ Thundering herd on notification +- Single point of failure (single instance) +- No quorum mechanism +- No automatic permit expiration +- Permits persist indefinitely +- Thundering herd on notification **Consistency Model**: ``` @@ -583,19 +600,13 @@ Permit acquired if: **Example Scenarios**: ```java // API rate limiting with fault tolerance -RedlockSemaphore apiLimiter = new RedlockSemaphore( - "api:external:rate-limit", 100, redisDrivers, config -); +RedlockSemaphore apiLimiter = manager.createSemaphore("api:external:rate-limit", 100); // Database connection pool with auto-expiration -RedlockSemaphore dbPool = new RedlockSemaphore( - "db:connection:pool", 50, redisDrivers, config -); +RedlockSemaphore dbPool = manager.createSemaphore("db:connection:pool", 50); // Distributed job throttling -RedlockSemaphore jobThrottle = new RedlockSemaphore( - "jobs:concurrent-limit", 10, redisDrivers, config -); +RedlockSemaphore jobThrottle = manager.createSemaphore("jobs:concurrent-limit", 10); ``` ### Redisson RedissonSemaphore @@ -630,76 +641,76 @@ RFuture future = asyncLimiter.tryAcquireAsync(5, TimeUnit.SECONDS); **Code Complexity**: ~370 lines **Pros**: -- ✅ Quorum-based safety -- ✅ Automatic permit expiration -- ✅ Fault-tolerant -- ✅ Clock drift compensation -- ✅ Thread-local state tracking +- Quorum-based safety +- Automatic permit expiration +- Fault-tolerant +- Clock drift compensation +- Thread-local state tracking **Cons**: -- ❌ Higher latency -- ❌ More Redis operations -- ❌ No accurate permit counting -- ❌ No permit management operations -- ❌ Polling-based waiting +- Higher latency +- More Redis operations +- No accurate permit counting +- No permit management operations +- Polling-based waiting ### Redisson **Code Complexity**: ~600 lines (with async support) **Pros**: -- ✅ Low latency -- ✅ Atomic operations -- ✅ Accurate permit counting -- ✅ Pub/sub notifications -- ✅ Async/reactive support -- ✅ Rich API (drain, add, set permits) +- Low latency +- Atomic operations +- Accurate permit counting +- Pub/sub notifications +- Async/reactive support +- Rich API (drain, add, set permits) **Cons**: -- ❌ Single point of failure -- ❌ No quorum mechanism -- ❌ No automatic expiration -- ❌ Permits persist indefinitely -- ❌ More complex implementation +- Single point of failure +- No quorum mechanism +- No automatic expiration +- Permits persist indefinitely +- More complex implementation ## Feature Comparison Table -| Feature | redlock4j | Redisson | -|---------|-----------|----------| -| **Data Model** | Individual permit keys | Single counter | -| **Quorum** | Yes (per permit) | No | -| **Fault Tolerance** | Survives minority failures | Single point of failure | -| **Permit Expiration** | Automatic (TTL) | Manual (optional) | -| **Permit Counting** | Not accurate | Accurate (O(1)) | -| **Waiting Mechanism** | Polling | Pub/sub | -| **Fairness** | Non-fair | Non-fair | -| **Async Support** | No | Yes | -| **Reactive Support** | No | Yes | -| **Initialization** | Implicit | Explicit | -| **Permit Management** | Limited | Rich (add/drain/set) | -| **Performance** | O(N×M) | O(1) | -| **Latency** | Higher | Lower | -| **Network Overhead** | High | Low | -| **Clock Drift** | Compensated | Not applicable | +| Feature | redlock4j | Redisson | +|-----------------------|----------------------------|-------------------------| +| **Data Model** | Individual permit keys | Single counter | +| **Quorum** | Yes (per permit) | No | +| **Fault Tolerance** | Survives minority failures | Single point of failure | +| **Permit Expiration** | Automatic (TTL) | Manual (optional) | +| **Permit Counting** | Not accurate | Accurate (O(1)) | +| **Waiting Mechanism** | Polling | Pub/sub | +| **Fairness** | Non-fair | Non-fair | +| **Async Support** | No | Yes | +| **Reactive Support** | No | Yes | +| **Initialization** | Implicit | Explicit | +| **Permit Management** | Limited | Rich (add/drain/set) | +| **Performance** | O(N×M) | O(1) | +| **Latency** | Higher | Lower | +| **Network Overhead** | High | Low | +| **Clock Drift** | Compensated | Not applicable | ## Recommendations ### Choose redlock4j RedlockSemaphore when: -- ✅ Need quorum-based distributed consistency -- ✅ Require fault tolerance (multi-master) -- ✅ Automatic permit expiration is critical -- ✅ Can tolerate higher latency -- ✅ Prefer simpler initialization +- Need quorum-based distributed consistency +- Require fault tolerance (multi-master) +- Automatic permit expiration is critical +- Can tolerate higher latency +- Prefer simpler initialization ### Choose Redisson RedissonSemaphore when: -- ✅ Single Redis instance is acceptable -- ✅ Need high throughput / low latency -- ✅ Require accurate permit counting -- ✅ Need async/reactive APIs -- ✅ Want dynamic permit management -- ✅ Efficient waiting (pub/sub) is important +- Single Redis instance is acceptable +- Need high throughput / low latency +- Require accurate permit counting +- Need async/reactive APIs +- Want dynamic permit management +- Efficient waiting (pub/sub) is important ### For Fair Semaphores: @@ -723,10 +734,8 @@ if (semaphore.tryAcquire(5, TimeUnit.SECONDS)) { } // After (redlock4j) -RedlockSemaphore semaphore = new RedlockSemaphore( - "api-limiter", 5, redisDrivers, config -); -if (semaphore.tryAcquire(5, TimeUnit.SECONDS)) { +RedlockSemaphore semaphore = manager.createSemaphore("api-limiter", 5); +if (semaphore.tryAcquire(Duration.ofSeconds(5))) { try { // work } finally { @@ -749,9 +758,7 @@ if (semaphore.tryAcquire(5, TimeUnit.SECONDS)) { ```java // Before (redlock4j) -RedlockSemaphore semaphore = new RedlockSemaphore( - "api-limiter", 5, redisDrivers, config -); +RedlockSemaphore semaphore = manager.createSemaphore("api-limiter", 5); // After (Redisson) RSemaphore semaphore = redisson.getSemaphore("api-limiter"); @@ -792,15 +799,3 @@ Choose based on your specific requirements: - **High throughput & low latency** → Redisson RedissonSemaphore - **Fair ordering (FIFO)** → Redisson RedissonPermitExpirableSemaphore -**Flow**: -1. Generate unique permit ID for each permit -2. For each permit, try `SET NX` on all nodes -3. Check if quorum achieved for each permit -4. If any permit fails quorum, rollback all -5. Validate total acquisition time - -**Redis Operations** (for N permits on M nodes): -- N × M `SET NX` operations -- Rollback: up to N × M `DELETE` operations - - diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 53129fc..05add85 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -8,7 +8,7 @@ org.codarama redlock4j - 1.1.0 + 1.0.0 @@ -17,14 +17,14 @@ redis.clients jedis - 7.1.0 + 7.4.1 io.lettuce lettuce-core - 7.1.0.RELEASE + 7.5.1.RELEASE ``` @@ -34,39 +34,48 @@ ```groovy dependencies { - implementation 'org.codarama:redlock4j:1.1.0' + implementation 'org.codarama:redlock4j:1.0.0' // Choose one Redis client: // For Jedis: - implementation 'redis.clients:jedis:7.1.0' + implementation 'redis.clients:jedis:7.4.1' // Or for Lettuce: - implementation 'io.lettuce:lettuce-core:7.1.0.RELEASE' + implementation 'io.lettuce:lettuce-core:7.5.1.RELEASE' } ``` ## Requirements -- **Java 8** or higher -- **Redis 2.6.12** or higher (for Lua script support) -- One of the supported Redis clients: - - Jedis 3.0+ or 4.0+ - - Lettuce 5.0+ or 6.0+ +- **Java 8** or higher (tested against Java 8, 11, 17, and 21) +- **Redis** - the default keyspace-notification wait strategy relies on RESP3, + so **Redis 6.0+** is recommended. Older servers are still usable via the + polling fallback strategy. Native atomic CAS/CAD is used automatically on + Redis 8.4+, with a Lua-script fallback on earlier versions. +- One of the supported Redis clients (RESP3 is required for the default + keyspace strategy): + - Jedis 5+ (RESP3) + - Lettuce 6+ (RESP3) ## Verifying Installation -After adding the dependencies, verify the installation by creating a simple test: +After adding the dependencies, verify the installation by connecting to a Redis +node and checking that the manager reports it as connected: ```java -import org.codarama.redlock4j.Redlock; -import redis.clients.jedis.JedisPool; +import org.codarama.redlock4j.RedlockManager; +import org.codarama.redlock4j.configuration.RedlockConfiguration; public class RedlockTest { public static void main(String[] args) { - JedisPool pool = new JedisPool("localhost", 6379); - Redlock redlock = new Redlock(pool); - System.out.println("Redlock4j is ready!"); - pool.close(); + RedlockConfiguration config = RedlockConfiguration.builder() + .addRedisNode("localhost", 6379) + .build(); + + try (RedlockManager manager = RedlockManager.withJedis(config)) { + System.out.println("Connected nodes: " + manager.getConnectedNodeCount()); + System.out.println("Redlock4j is ready!"); + } } } ``` diff --git a/docs/getting-started/quick-start.md b/docs/getting-started/quick-start.md index 30f68b8..5dcc9ea 100644 --- a/docs/getting-started/quick-start.md +++ b/docs/getting-started/quick-start.md @@ -4,48 +4,86 @@ This guide will help you get started with Redlock4j in just a few minutes. ## Basic Setup -### 1. Create Redis Connection Pools +### 1. Configure the Redis Nodes -First, create connection pools for your Redis instances. For production use, you should have at least 3 independent Redis instances. +Redlock4j does not take pre-built connection pools or clients. Instead, you +declare each Redis node as a host/port pair in a `RedlockConfiguration`. For a +full Redlock deployment you should use at least 3 independent Redis instances. ```java -import redis.clients.jedis.JedisPool; - -// Create pools for multiple Redis instances -JedisPool pool1 = new JedisPool("redis1.example.com", 6379); -JedisPool pool2 = new JedisPool("redis2.example.com", 6379); -JedisPool pool3 = new JedisPool("redis3.example.com", 6379); +import org.codarama.redlock4j.configuration.RedlockConfiguration; + +import java.time.Duration; + +RedlockConfiguration config = RedlockConfiguration.builder() + .addRedisNode("redis1.example.com", 6379) + .addRedisNode("redis2.example.com", 6379) + .addRedisNode("redis3.example.com", 6379) + .defaultLockTimeout(Duration.ofSeconds(30)) + .retryDelay(Duration.ofMillis(200)) + .maxRetryAttempts(3) + .build(); ``` -### 2. Create a Redlock Instance +!!! info "Number of nodes" + A single node runs in single-node mode, and 3 or more nodes run in quorum + mode. Configuring exactly 2 nodes throws an `IllegalArgumentException` at + `build()`, since a quorum cannot be formed. + +### 2. Create a RedlockManager + +The `RedlockManager` owns the connections to the configured nodes. It is +`AutoCloseable`, so create it inside a try-with-resources block and let it +manage the underlying pools for you. Use `withJedis(config)` for the Jedis +driver, or `withLettuce(config)` for Lettuce. ```java -import org.codarama.redlock4j.Redlock; +import org.codarama.redlock4j.RedlockManager; -Redlock redlock = new Redlock(pool1, pool2, pool3); +try (RedlockManager manager = RedlockManager.withJedis(config)) { + // create and use locks here +} ``` ### 3. Acquire and Release Locks +`createLock(name)` returns a standard `java.util.concurrent.locks.Lock`. The +lock TTL and retry behaviour come from the configuration, so `lock()` and +`unlock()` take no arguments. + ```java -import org.codarama.redlock4j.Lock; +import java.util.concurrent.locks.Lock; + +Lock lock = manager.createLock("my-resource"); + +lock.lock(); +try { + // Lock acquired successfully - perform your critical section here + System.out.println("Lock acquired! Performing critical operation..."); + performCriticalOperation(); +} finally { + // Always release the lock + lock.unlock(); + System.out.println("Lock released"); +} +``` -// Try to acquire a lock for "my-resource" with 10 second TTL -Lock lock = redlock.lock("my-resource", 10000); +If you would rather not block indefinitely, use `tryLock` with a timeout: -if (lock != null) { +```java +import java.util.concurrent.locks.Lock; + +Lock lock = manager.createLock("my-resource"); + +if (lock.tryLock(Duration.ofSeconds(5))) { try { - // Lock acquired successfully - // Perform your critical section here System.out.println("Lock acquired! Performing critical operation..."); performCriticalOperation(); } finally { - // Always release the lock - redlock.unlock(lock); - System.out.println("Lock released"); + lock.unlock(); } } else { - // Failed to acquire lock + // Could not acquire the lock within the timeout System.out.println("Could not acquire lock"); } ``` @@ -55,83 +93,80 @@ if (lock != null) { Here's a complete working example: ```java -import org.codarama.redlock4j.Redlock; -import org.codarama.redlock4j.Lock; -import redis.clients.jedis.JedisPool; +import org.codarama.redlock4j.RedlockManager; +import org.codarama.redlock4j.configuration.RedlockConfiguration; + +import java.time.Duration; +import java.util.concurrent.locks.Lock; public class RedlockExample { public static void main(String[] args) { - // Setup Redis pools - JedisPool pool1 = new JedisPool("localhost", 6379); - JedisPool pool2 = new JedisPool("localhost", 6380); - JedisPool pool3 = new JedisPool("localhost", 6381); - - // Create Redlock instance - Redlock redlock = new Redlock(pool1, pool2, pool3); - - // Resource identifier - String resourceId = "shared-resource"; - - // Lock TTL in milliseconds (10 seconds) - int ttl = 10000; - - // Try to acquire lock - Lock lock = redlock.lock(resourceId, ttl); - - if (lock != null) { - try { - // Critical section - System.out.println("Processing shared resource..."); - Thread.sleep(2000); // Simulate work - System.out.println("Done processing"); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } finally { - // Release lock - redlock.unlock(lock); + // Configure the Redis nodes (host/port pairs) + RedlockConfiguration config = RedlockConfiguration.builder() + .addRedisNode("localhost", 6379) + .addRedisNode("localhost", 6380) + .addRedisNode("localhost", 6381) + .defaultLockTimeout(Duration.ofSeconds(10)) + .retryDelay(Duration.ofMillis(200)) + .maxRetryAttempts(3) + .build(); + + // RedlockManager is AutoCloseable - it owns the node connections + try (RedlockManager manager = RedlockManager.withJedis(config)) { + Lock lock = manager.createLock("shared-resource"); + + if (lock.tryLock(Duration.ofSeconds(5))) { + try { + // Critical section + System.out.println("Processing shared resource..."); + Thread.sleep(2000); // Simulate work + System.out.println("Done processing"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + lock.unlock(); + } + } else { + System.out.println("Another process is using the resource"); } - } else { - System.out.println("Another process is using the resource"); } - - // Cleanup - pool1.close(); - pool2.close(); - pool3.close(); } } ``` ## Using Lettuce Instead of Jedis -If you prefer Lettuce over Jedis: +The configuration is identical - nodes are always host/port pairs. To use the +Lettuce driver, create the manager with `withLettuce(config)` instead of +`withJedis(config)`: ```java -import io.lettuce.core.RedisClient; -import io.lettuce.core.api.StatefulRedisConnection; -import org.codarama.redlock4j.Redlock; - -// Create Lettuce clients -RedisClient client1 = RedisClient.create("redis://localhost:6379"); -RedisClient client2 = RedisClient.create("redis://localhost:6380"); -RedisClient client3 = RedisClient.create("redis://localhost:6381"); +import org.codarama.redlock4j.RedlockManager; +import org.codarama.redlock4j.configuration.RedlockConfiguration; -// Create Redlock instance -Redlock redlock = new Redlock(client1, client2, client3); +import java.util.concurrent.locks.Lock; -// Use the same lock/unlock pattern as above +try (RedlockManager manager = RedlockManager.withLettuce(config)) { + Lock lock = manager.createLock("my-resource"); + // Use the same lock/unlock pattern as above +} ``` ## Important Notes !!! warning "Lock TTL" - Always set a TTL that's longer than your critical section execution time. If the lock expires while you're still processing, another client might acquire the lock. + The lock TTL is derived from `defaultLockTimeout` in the configuration. + Make sure it is longer than your critical section execution time. If the + lock expires while you're still processing, another client might acquire + the lock. !!! tip "Always Unlock" Always release locks in a `finally` block to ensure they're released even if an exception occurs. !!! info "Minimum Redis Instances" - For production use, always use at least 3 independent Redis instances to ensure proper fault tolerance. + For a full Redlock deployment, use at least 3 independent Redis instances + to ensure proper fault tolerance. A single node is supported for + development and standalone use. ## Next Steps diff --git a/docs/guide/architecture.md b/docs/guide/architecture.md index db37d50..91c4d6c 100644 --- a/docs/guide/architecture.md +++ b/docs/guide/architecture.md @@ -8,10 +8,10 @@ When a lock is contended (another client holds it), redlock4j needs to wait for ### Available Strategies -| Strategy | Description | Default | -|----------|-------------|:-------:| -| `KEYSPACE_NOTIFICATIONS` | Uses Redis pub/sub to get instant notification when lock is released | Yes | -| `POLLING` | Polls Redis every 50ms to check if lock is available | No | +| Strategy | Description | Default | +|--------------------------|---------------------------------------------------------------------------------------------------|:-------:| +| `KEYSPACE_NOTIFICATIONS` | Uses Redis pub/sub to get instant notification when lock is released | Yes | +| `POLLING` | Polls Redis at the configured `retryDelay` interval (default 200ms) to check if lock is available | No | ### Keyspace Notifications (Default) @@ -57,10 +57,10 @@ sequenceDiagram B->>R: SET NX lock:resource R-->>B: nil - Note over B: sleep 50ms + Note over B: sleep retryDelay
(default 200ms) B->>R: SET NX lock:resource R-->>B: nil - Note over B: sleep 50ms + Note over B: sleep retryDelay
(default 200ms) A->>R: DEL lock:resource B->>R: SET NX lock:resource R-->>B: OK @@ -85,27 +85,27 @@ RedlockConfiguration config = RedlockConfiguration.builder() The two strategies optimize for different things: -| Metric | Keyspace Notifications | Polling | -|--------|:---------------------:|:-------:| -| **Wake-up latency** | ~6ms | ~18-50ms (poll interval) | +| Metric | Keyspace Notifications | Polling | +|---------------------------------|:-----------------------:|:--------------------------:| +| **Wake-up latency** | ~6ms | ~18-50ms (poll interval) | | **Throughput under contention** | Lower (thundering herd) | Higher (staggered retries) | -| **CPU usage** | Lower (event-driven) | Higher (busy-wait) | +| **CPU usage** | Lower (event-driven) | Higher (busy-wait) | **Latency benchmark** (time from lock release to waiter acquiring): -| Strategy | Wake-up Latency | Speedup | -|----------|:---------------:|:-------:| -| Keyspace Notifications | 6.78ms | **2.7x faster** | -| Polling (50ms interval) | 18.02ms | baseline | +| Strategy | Wake-up Latency | Speedup | +|-----------------------------------|:----------------:|:---------------:| +| Keyspace Notifications | 6.78ms | **2.7x faster** | +| Polling (50ms benchmark interval) | 18.02ms | baseline | **Throughput benchmark** (10 waiters, 20ms lock hold): -| Lock Type | Mode | Keyspace (ops/s) | Polling (ops/s) | Notes | -|-----------|:----:|:----------------:|:---------------:|-------| -| Distributed Lock | Single | 39.00 | 41.00 | ~Equal (throughput bounded by lock hold time) | -| ReadWriteLock | Single | 151.80 | 137.10 | Keyspace 10% better (readers benefit) | -| Semaphore | Single | 400.35 | 402.55 | ~Equal | -| FairLock | Single | 0.50 | 30.15 | **Polling required** (see note) | +| Lock Type | Mode | Keyspace (ops/s) | Polling (ops/s) | Notes | +|------------------|:------:|:-----------------:|:----------------:|-----------------------------------------------| +| Distributed Lock | Single | 39.00 | 41.00 | ~Equal (throughput bounded by lock hold time) | +| ReadWriteLock | Single | 151.80 | 137.10 | Keyspace 10% better (readers benefit) | +| Semaphore | Single | 400.35 | 402.55 | ~Equal | +| FairLock | Single | 0.50 | 30.15 | **Polling required** (see note) | !!! info "Understanding the Results" When lock hold times are short (20ms), throughput is bounded by hold time, not wait time. Both strategies achieve similar throughput (~40 ops/s for a single lock with 20ms hold). @@ -136,12 +136,12 @@ flowchart TB **The tradeoff:** -| Aspect | Keyspace Notifications | Polling | -|--------|----------------------|---------| -| Wakeup behavior | All waiters wake simultaneously | Waiters check independently (staggered) | -| Wakeup overhead (10 waiters) | 10 wakeups per release | 1 wakeup per release | -| Latency | ~1-10ms | Up to poll interval | -| CPU usage | Low (event-driven) | Higher (busy-wait) | +| Aspect | Keyspace Notifications | Polling | +|------------------------------|---------------------------------|-----------------------------------------| +| Wakeup behavior | All waiters wake simultaneously | Waiters check independently (staggered) | +| Wakeup overhead (10 waiters) | 10 wakeups per release | 1 wakeup per release | +| Latency | ~1-10ms | Up to poll interval | +| CPU usage | Low (event-driven) | Higher (busy-wait) | This design was chosen for **simplicity**, **correctness** (no missed wakeups), and **memory efficiency** (O(keys) not O(keys × waiters)). @@ -155,27 +155,27 @@ This design was chosen for **simplicity**, **correctness** (no missed wakeups), Based on comprehensive benchmarks: -| Lock Type | Recommended Strategy | Reason | -|-----------|---------------------|--------| -| **FairLock** | **Polling (required)** | Queue management conflicts with keyspace | -| **Distributed Lock** | Either | Similar throughput; keyspace has lower latency | -| **ReadWriteLock** | Keyspace | Readers benefit from instant wake-up | -| **Semaphore** | Either | Similar performance | +| Lock Type | Recommended Strategy | Reason | +|----------------------|------------------------|------------------------------------------------| +| **FairLock** | **Polling (required)** | Queue management conflicts with keyspace | +| **Distributed Lock** | Either | Similar throughput; keyspace has lower latency | +| **ReadWriteLock** | Keyspace | Readers benefit from instant wake-up | +| **Semaphore** | Either | Similar performance | **Choose Keyspace Notifications (default) when:** -- ✅ Wake-up latency matters (~3x faster than polling) -- ✅ Using ReadWriteLock (readers wake up together instantly) -- ✅ Lock expiry detection must be instant -- ✅ Long lock hold times (latency benefit more noticeable) -- ✅ Low CPU usage is important (event-driven, not busy-wait) +- Wake-up latency matters (~3x faster than polling) +- Using ReadWriteLock (readers wake up together instantly) +- Lock expiry detection must be instant +- Long lock hold times (latency benefit more noticeable) +- Low CPU usage is important (event-driven, not busy-wait) **Choose Polling when:** -- ✅ Using FairLock (required - keyspace has severe issues) -- ✅ Redis doesn't support keyspace notifications (some managed services) -- ✅ Network reliability concerns (polling is more resilient) -- ✅ Consistent retry timing is needed +- Using FairLock (required - keyspace has severe issues) +- Redis doesn't support keyspace notifications (some managed services) +- Network reliability concerns (polling is more resilient) +- Consistent retry timing is needed **Decision flowchart:** @@ -360,7 +360,7 @@ sequenceDiagram B->>R: SET lock:resource "token-B" R-->>B: OK A->>R: DEL lock:resource - Note over R: ⚠️ DELETES CLIENT B's LOCK! + Note over R: DELETES CLIENT B's LOCK! ``` **Result**: Client A accidentally deletes Client B's lock! @@ -378,7 +378,7 @@ sequenceDiagram A->>R: DELEX lock:resource IFEQ "token-A" Note over R: Lock value is "token-B" ≠ "token-A" R-->>A: 0 (not deleted) - Note over A: ✅ Client B's lock safe! + Note over A: Client B's lock safe! ``` ### Implementation Strategies @@ -387,8 +387,8 @@ redlock4j automatically detects and uses the best available method: | Redis Version | Strategy | Command | |---------------|------------|------------------------------------| -| 8.0+ | Native | `DELEX key IFEQ value` | -| < 8.0 | Lua Script | `EVAL "if get==expected then del"` | +| 8.4+ | Native | `DELEX key IFEQ value` | +| < 8.4 | Lua Script | `EVAL "if get==expected then del"` | ```java // Automatic detection at driver initialization @@ -396,7 +396,7 @@ private CADStrategy detectCADStrategy() { try { // Try native DELEX command redis.dispatch(CommandType.DELEX, testKey, "IFEQ", "test"); - return CADStrategy.NATIVE; // Redis 8.0+ + return CADStrategy.NATIVE; // Redis 8.4+ } catch (Exception e) { return CADStrategy.SCRIPT; // Fallback to Lua } @@ -405,7 +405,7 @@ private CADStrategy detectCADStrategy() { ### Performance Comparison -| Operation | Native (Redis 8.0+) | Lua Script | +| Operation | Native (Redis 8.4+) | Lua Script | |-------------------------|:-------------------:|:---------------:| | **Commands** | 1 | 1 (EVAL) | | **Network round-trips** | 1 | 1 | @@ -428,7 +428,7 @@ boolean extended = driver.setIfValueMatches( ); ``` -**Redis 8.0+ command:** +**Redis 8.4+ command:** ``` SET lock:resource "token-A" IFEQ "token-A" PX 30000 ``` @@ -444,12 +444,12 @@ end ### Why This Matters -| Scenario | Without CAS/CAD | With CAS/CAD | -|---------------------------------|-------------------------|-----------------------| -| Lock expires during operation | ❌ May delete wrong lock | ✅ Safely fails | -| Network partition during unlock | ❌ Unpredictable | ✅ Atomic check | -| Concurrent lock extension | ❌ Race condition | ✅ Only owner extends | -| Lock ownership verification | ❌ Separate GET+DEL | ✅ Single atomic op | +| Scenario | Without CAS/CAD | With CAS/CAD | +|---------------------------------|-------------------------|--------------------| +| Lock expires during operation | May delete wrong lock | Safely fails | +| Network partition during unlock | Unpredictable | Atomic check | +| Concurrent lock extension | Race condition | Only owner extends | +| Lock ownership verification | Separate GET+DEL | Single atomic op | ### Configuration @@ -458,7 +458,7 @@ CAS/CAD strategy is automatically detected—no configuration needed: ```java // Driver auto-detects Redis version capabilities RedlockManager manager = RedlockManager.withJedis(config); -// Native commands used if Redis 8.0+ detected +// Native commands used if Redis 8.4+ detected // Lua scripts used otherwise ``` @@ -476,6 +476,100 @@ DEBUG o.c.r.driver.JedisRedisDriver - Native CAS/CAD not available for redis://l --- +## Performance Analysis + +This section summarizes the optimization work driven by the `redlock4j-benchmark` suite and reports the current cross-primitive standings against Redisson and other Redis-based locking libraries. + +### Methodology + +All numbers below come from the same harness (`redlock4j-benchmark/`) running against a fresh 3-node Redis 7 Testcontainers cluster, 50 ms simulated work per critical section, 30 s warm-up, 60 s measurement. Lock-acquisition latency is captured per attempt; throughput is aggregated across all clients. Full raw data lives in `redlock4j-benchmark/benchmark-analysis.md` and the per-suite `*-benchmark-results.json` files. + +### Key Optimizations + +Two architectural changes deliver the bulk of the recent performance improvements: + +**1. Parallel multi-node I/O** — `MultiNodeStrategy.acquireLock` / `releaseLock` / `extendLock` previously iterated nodes sequentially. They now fan out via `CompletableFuture` and wait for the quorum on the join. Effect: the per-attempt RTT on a 3-node cluster collapses from `3 × RTT` to `max(RTT)`. + +**2. Exponential backoff with jitter** — `PollingWaitStrategy` and the `Redlock.tryLock` fallback path now grow the inter-attempt delay geometrically (configurable multiplier, cap, and jitter ratio). The previous fixed retry (50 ms in the benchmark config) produced retry storms under contention. Backoff reduces wasted RTTs and stabilizes throughput. `FairLock` is intentionally excluded because head-of-queue waiters need tight polling to claim the lock the instant the holder releases. + +### p99 vs Throughput + +These two metrics measure opposite ends of the same latency distribution and trade off against each other: + +- **Throughput (Ops/s)** is system capacity — how many acquire+release cycles complete per second. Driven by RTTs per attempt, inter-attempt delay, and hold time. +- **p99 latency** is the worst-case experience for 99 % of callers. Driven by contention bursts, retry chains, and any single stuck attempt. + +Aggressive polling raises throughput but inflates p99 (more attempts compete for the same release window). Backoff lowers p99 by reducing wasted attempts but can cap throughput when waiters sleep through release windows. Pub/sub-on-release (planned, see `benchmark-analysis.md` §8 A3) is the rare lever that improves both. + +Which metric matters depends on the workload: + +| Workload | Primary metric | +|-----------------------------------------|--------------------| +| Leader election, failover | **p99 / max** | +| API request serialization (per-user) | **p99** | +| Cache-stampede prevention | **p99 of waiters** | +| Batch / background jobs | **Throughput** | +| Rate limiting, semaphores | **Throughput** | +| Critical section on a hot business path | **Both** | + +### Consolidated Results + +All measurements taken on the same 3-node Redis cluster. `redlock4j-singlenode` is `redlock4j` in single-node mode (`SingleNodeStrategy`); `redlock4j` (or `redlock4j-3node`) uses full quorum Redlock across all 3 nodes. + +#### Throughput (Ops/s — higher is better) + +| Primitive | Redisson | redlock4j-singlenode | redlock4j-3node | Field leader | +|-------------------------|-----------:|---------------------:|----------------:|---------------------| +| DistributedLock | 18.24 | **18.33** | 0.81 | redpulsar (21.63) | +| FairLock | **16.99** | 12.42 | 2.95 | redisson | +| MultiLock | 17.05 | 16.97 | **17.72** | **redlock4j-3node** | +| ReadWriteLock (reader) | **151.65** | 74.66 | 95.41 | redisson | +| ReadWriteLock (writer) | 0.21 | 15.63 | **16.42** | **redlock4j-3node** | +| Semaphore | 54.71 | **91.09** | 87.50 | **redlock4j** | +| CountDownLatch | 59.02 | 58.19 | **59.91** | **redlock4j-3node** | + +#### p50 Latency (ms — lower is better) + +| Primitive | Redisson | redlock4j-singlenode | redlock4j-3node | +|-------------------------|----------:|---------------------:|----------------:| +| DistributedLock | 122.9 | **67.1** | 191.3 | +| FairLock | **221.0** | 332.0 | 1666.0 | +| MultiLock | 190.2 | 202.9 | **171.0** | +| ReadWriteLock (reader) | **0.79** | 12.54 | 2.72 | +| ReadWriteLock (writer) | 8649.0 | 57.8 | **64.1** | +| Semaphore | 1.00 | **0.73** | 1.77 | +| CountDownLatch | 17.2 | 16.2 | **15.3** | + +#### p99 Latency (ms — lower is better) + +| Primitive | Redisson | redlock4j-singlenode | redlock4j-3node | +|-------------------------|----------:|---------------------:|----------------:| +| DistributedLock | 1286.7 | 1977.5 | **858.0** | +| FairLock | **229.8** | 434.8 | 2457.1 | +| MultiLock | 1192.2 | 1709.5 | **1057.6** | +| ReadWriteLock (reader) | **5.82** | 340.6 | 348.4 | +| ReadWriteLock (writer) | 16820.7 | 755.6 | **104.2** | +| Semaphore | 386.5 | **2.21** | 4.20 | +| CountDownLatch | 22.6 | 19.9 | **19.9** | + +### Standings Summary + +**redlock4j leads on throughput in 4 / 7 categories** (MultiLock, RWLock writer, Semaphore, CountDownLatch — plus single-node DistributedLock parity with the field), and **leads on p99 in 5 / 7** (DistributedLock 3-node, RWLock writer, MultiLock, Semaphore, CountDownLatch). + +Remaining gaps under active investigation (`benchmark-analysis.md` §8): + +- **DistributedLock 3-node throughput** is the largest residual gap. The 50 ms polling floor is the bottleneck; A3 (pub/sub-on-release wait strategy) targets this directly. +- **FairLock 3-node throughput** suffers from the per-attempt quorum head-check round-trip. P2-8 (single-shot atomic head-check + acquire) is the structural fix. +- **RWLock readers** trail Redisson because Redisson decrements readers in-process via semaphore counting while redlock4j hits Redis on every read acquire. This is an architectural choice, not a regression. + +### What This Means for Users + +- Pick `redlock4j-singlenode` when you have a single Redis instance — you get parity with single-node libraries on most primitives and a clear win on Semaphore. +- Pick `redlock4j-3node` (full Redlock quorum) when you need fault tolerance across independent Redis masters. Expect lower throughput on the basic `DistributedLock` and `FairLock` primitives until A3 / P2-8 land; everything else (MultiLock, RWLock writers, Semaphore, CountDownLatch) is already at or above field-leader performance. +- For latency-sensitive paths, the recent backoff change generally improves p99 (best-in-class on 5 / 7 primitives). The exception is `FairLock`, where polling is required for correctness reasons (see *FairLock Recommendation* above). + +--- + ## Summary | Decision | Choice | Rationale | @@ -484,4 +578,6 @@ DEBUG o.c.r.driver.JedisRedisDriver - Native CAS/CAD not available for redis://l | Protocol requirement | RESP3 | Single connection for commands + pub/sub | | Server configuration | Auto-configure | Zero manual setup required | | Fallback option | Polling | For restricted environments | -| CAS/CAD operations | Auto-detect | Native commands on Redis 8.0+, Lua fallback | +| CAS/CAD operations | Auto-detect | Native commands on Redis 8.4+, Lua fallback | +| Multi-node I/O | Parallel fan-out | `max(RTT)` per attempt instead of `N × RTT` | +| Retry backoff | Exponential + jitter | Reduces retry storms under contention | diff --git a/docs/guide/basic-usage.md b/docs/guide/basic-usage.md index 833f8d4..2240677 100644 --- a/docs/guide/basic-usage.md +++ b/docs/guide/basic-usage.md @@ -2,32 +2,54 @@ This guide covers common usage patterns and scenarios for Redlock4j. +All examples assume you have created a `RedlockManager` from a `RedlockConfiguration`. +The manager is the entry point: it owns the Redis connections and hands out locks +that implement `java.util.concurrent.locks.Lock`. + +```java +import org.codarama.redlock4j.RedlockManager; +import org.codarama.redlock4j.Redlock; +import org.codarama.redlock4j.configuration.RedlockConfiguration; +import java.time.Duration; +import java.util.concurrent.locks.Lock; + +RedlockConfiguration config = RedlockConfiguration.builder() + .addRedisNode("redis1", 6379) + .addRedisNode("redis2", 6379) + .addRedisNode("redis3", 6379) + .defaultLockTimeout(Duration.ofSeconds(30)) + .retryDelay(Duration.ofMillis(200)) + .maxRetryAttempts(3) + .build(); + +RedlockManager manager = RedlockManager.withJedis(config); // or withLettuce(config) +``` + ## Simple Lock Pattern -The most basic usage pattern: +The most basic usage pattern. Acquire the lock, do the work, release in a `finally`: ```java -Lock lock = redlock.lock("resource-id", 10000); -if (lock != null) { - try { - // Critical section - } finally { - redlock.unlock(lock); - } +Lock lock = manager.createLock("resource-id"); +lock.lock(); +try { + // Critical section +} finally { + lock.unlock(); } ``` ## Try-Lock Pattern -Attempt to acquire a lock without retries: +Attempt to acquire a lock without blocking: ```java -Lock lock = redlock.tryLock("resource-id", 10000); -if (lock != null) { +Lock lock = manager.createLock("resource-id"); +if (lock.tryLock()) { try { // Got the lock immediately } finally { - redlock.unlock(lock); + lock.unlock(); } } else { // Lock not available, handle accordingly @@ -36,118 +58,133 @@ if (lock != null) { ## Lock with Timeout -Wait for a lock with a timeout: +Wait for a lock up to a bounded amount of time: ```java -Lock lock = redlock.lock("resource-id", 10000, 5000); // 5 second timeout -if (lock != null) { +Lock lock = manager.createLock("resource-id"); +if (lock.tryLock(Duration.ofSeconds(5))) { // wait up to 5 seconds try { // Acquired lock within timeout } finally { - redlock.unlock(lock); + lock.unlock(); } } else { // Timeout expired } ``` +`tryLock(long, TimeUnit)` is also available if you prefer the standard +`java.util.concurrent` signature: + +```java +if (lock.tryLock(5, TimeUnit.SECONDS)) { + // ... +} +``` + ## Extending Lock Duration -If your operation takes longer than expected, you can extend the lock: +If your operation takes longer than expected, you can extend the lock. The +`extend` method is defined on `Redlock`, so cast the lock reference and pass the +additional time in milliseconds: ```java -Lock lock = redlock.lock("resource-id", 10000); -if (lock != null) { - try { - // Do some work - performPartialWork(); - - // Need more time, extend the lock - boolean extended = redlock.extend(lock, 10000); - if (extended) { - // Continue working - performMoreWork(); - } - } finally { - redlock.unlock(lock); +Lock lock = manager.createLock("resource-id"); +lock.lock(); +try { + // Do some work + performPartialWork(); + + // Need more time, extend the lock by 10 seconds + boolean extended = ((Redlock) lock).extend(10000); + if (extended) { + // Continue working + performMoreWork(); } +} finally { + lock.unlock(); } ``` ## Checking Lock Validity -Check if a lock is still valid: +There is no `isValid` method. Instead, ask whether the current thread still holds +the lock, or inspect the remaining validity time: ```java -Lock lock = redlock.lock("resource-id", 10000); -if (lock != null) { - try { - performWork(); - - if (redlock.isValid(lock)) { - // Lock is still valid - performMoreWork(); - } else { - // Lock expired or was released - handleExpiredLock(); - } - } finally { - redlock.unlock(lock); +Lock lock = manager.createLock("resource-id"); +lock.lock(); +try { + performWork(); + + Redlock redlock = (Redlock) lock; + if (redlock.isHeldByCurrentThread() + && !redlock.getRemainingValidityTime().isZero()) { + // Lock is still held and has time left + performMoreWork(); + } else { + // Lock expired or was released + handleExpiredLock(); } +} finally { + lock.unlock(); } ``` ## Multiple Resources -Lock multiple resources atomically: +Lock multiple resources atomically with a multi-lock. `createMultiLock` takes a +`List` of resource keys and returns a single `Lock` that governs all of them: ```java -String[] resources = {"resource-1", "resource-2", "resource-3"}; -Lock lock = redlock.lock(resources, 10000); +import java.util.Arrays; -if (lock != null) { +Lock lock = manager.createMultiLock( + Arrays.asList("resource-1", "resource-2", "resource-3")); + +if (lock.tryLock()) { try { // All resources are locked processMultipleResources(); } finally { - redlock.unlock(lock); + lock.unlock(); } } ``` ## Reentrant Locks -Redlock4j supports reentrant locks (same thread can acquire the same lock multiple times): +Redlock4j supports reentrant locks (the same thread can acquire the same lock +multiple times; the lock is only released when the hold count returns to zero): ```java -Lock lock1 = redlock.lock("resource-id", 10000); -if (lock1 != null) { +Lock lock = manager.createLock("resource-id"); +lock.lock(); +try { + // First acquisition + + lock.lock(); try { - // First acquisition - - Lock lock2 = redlock.lock("resource-id", 10000); - if (lock2 != null) { - try { - // Second acquisition by same thread - } finally { - redlock.unlock(lock2); - } - } + // Second acquisition by the same thread } finally { - redlock.unlock(lock1); + lock.unlock(); } +} finally { + lock.unlock(); } ``` ## Error Handling -Proper error handling is crucial: +Proper error handling is crucial. Acquire the lock, and only unlock what you +actually hold: ```java -Lock lock = null; +Lock lock = manager.createLock("resource-id"); +boolean acquired = false; try { - lock = redlock.lock("resource-id", 10000); - if (lock != null) { + acquired = lock.tryLock(); + if (acquired) { // Critical section performCriticalOperation(); } else { @@ -159,9 +196,9 @@ try { logger.error("Error during critical section", e); handleError(e); } finally { - if (lock != null) { + if (acquired) { try { - redlock.unlock(lock); + lock.unlock(); } catch (Exception e) { logger.error("Error releasing lock", e); } @@ -171,15 +208,22 @@ try { ## Using with Try-With-Resources -If your Lock implementation supports AutoCloseable: +The lock is **not** `AutoCloseable`, so you cannot put it in a +try-with-resources block. The `RedlockManager` **is** `AutoCloseable`, so use +try-with-resources on the manager to guarantee its connections are closed, and +release individual locks in a `finally` block: ```java -try (Lock lock = redlock.lock("resource-id", 10000)) { - if (lock != null) { +try (RedlockManager manager = RedlockManager.withJedis(config)) { + Lock lock = manager.createLock("resource-id"); + lock.lock(); + try { // Critical section performCriticalOperation(); + } finally { + lock.unlock(); } -} // Lock automatically released +} // manager (and its connections) automatically closed ``` ## Common Patterns @@ -190,13 +234,13 @@ Ensure only one instance executes a task: ```java public void executeScheduledTask() { - Lock lock = redlock.lock("scheduled-task-id", 60000); - if (lock != null) { + Lock lock = manager.createLock("scheduled-task-id"); + if (lock.tryLock()) { try { // Only one instance will execute this performScheduledTask(); } finally { - redlock.unlock(lock); + lock.unlock(); } } else { // Another instance is already executing @@ -211,13 +255,13 @@ Manage access to a limited resource pool: ```java public void processWithResource(String resourceId) { - Lock lock = redlock.lock("resource-pool:" + resourceId, 30000); - if (lock != null) { + Lock lock = manager.createLock("resource-pool:" + resourceId); + if (lock.tryLock()) { try { Resource resource = acquireResource(resourceId); processResource(resource); } finally { - redlock.unlock(lock); + lock.unlock(); } } } @@ -227,4 +271,3 @@ public void processWithResource(String resourceId) { - [Advanced Locking](advanced-locking.md) - Learn about advanced features - [Best Practices](best-practices.md) - Follow recommended practices - diff --git a/docs/guide/benchmarks.md b/docs/guide/benchmarks.md index 7a1e33b..4f7a8e7 100644 --- a/docs/guide/benchmarks.md +++ b/docs/guide/benchmarks.md @@ -1,17 +1,22 @@ # Performance Benchmarks -This guide provides comprehensive performance comparisons between **redlock4j** and other Redis-based locking libraries. +This guide summarizes performance comparisons between **redlock4j** and other Redis-based locking libraries. + +!!! note "Authoritative source" + The full, authoritative benchmark results live in the [Architecture guide's Performance Analysis section](architecture.md#performance-analysis) and in `redlock4j-benchmark/benchmark-analysis.md` (§7). This page summarizes those results; if the numbers ever diverge, the Architecture guide and `benchmark-analysis.md` win. ## Test Environment -| Parameter | Value | -|-----------|-------| -| Redis Nodes | 3 (Testcontainers) | -| Redis Version | 7-alpine | -| Work Simulation | 50ms per lock hold | -| Lock Timeout | 30s | -| Benchmark Duration | 1 minute per implementation | -| JDK | 17 | +| Parameter | Value | +|--------------------|-------------------------------------------------| +| Redis Nodes | 3 (Testcontainers) | +| Redis Version | 7-alpine | +| Work Simulation | 50ms per lock hold | +| Lock Timeout | 30s | +| Clients | 5 (10 for ReadWriteLock: 5 readers + 5 writers) | +| Warmup (discarded) | 30s | +| Measurement | 60s (1 minute) per implementation | +| JDK | 17 | --- @@ -23,20 +28,20 @@ redlock4j automatically detects single-node deployments and uses an optimized `S - Clock drift compensation - Node iteration overhead -### Distributed Lock Comparison (1 client, no contention) +### Distributed Lock Comparison (5 clients under contention) -| Implementation | Throughput (ops/s) | Notes | -|----------------|-------------------|-------| -| **redlock4j-singlenode** | **18.33** | SingleNodeStrategy optimized | -| ShedLock | 18.37 | Single node | -| Spring Integration | 18.23 | Single node | -| Redisson RLock | 18.32 | Single node | -| RedPulsar | 18.26 | 3-node Redlock | -| redlock4j-3node | 17.60 | Full 3-node Redlock | +| Implementation | Throughput (ops/s) | Notes | +|--------------------------|--------------------|------------------------------------------------| +| RedPulsar | 21.63 | 3-node Redlock; throughput leader | +| Spring Integration | 19.47 | Single node | +| ShedLock | 18.86 | Single node | +| **redlock4j-singlenode** | **18.33** | SingleNodeStrategy optimized | +| Redisson RLock | 18.24 | Single node | +| redlock4j-3node | 0.81 | Full 3-node Redlock (best p99 in field: 858ms) | **Key Finding**: Single-node mode is **competitive with other single-node implementations** while retaining the ability to scale to multi-node Redlock when needed. -> **Note**: Multi-client contention scenarios with 3-node Redlock show degraded performance due to the inherent cost of distributed consensus. For high-contention workloads, consider single-node mode or tuning retry delays. +> **Note**: 3-node Redlock shows low throughput on the basic `DistributedLock` primitive because of the **polling wait strategy under contention**, not the cost of consensus per se — waiters sleep on a fixed poll interval and race on release. Exponential backoff (already landed) doubled 3-node throughput; a pub/sub-on-release wait strategy (planned, benchmark-analysis §8 A3) targets the remainder. Despite the throughput gap, 3-node redlock4j has **best-in-class p99** on several primitives (DistributedLock, RWLock writer, MultiLock, Semaphore, CountDownLatch). For high-throughput contention workloads today, prefer single-node mode. --- @@ -46,29 +51,30 @@ redlock4j automatically detects single-node deployments and uses an optimized `S The fundamental distributed lock implementation. -| Metric | Redisson | redlock4j-singlenode | redlock4j-3node | -|--------|----------|---------------------|-----------------| -| **Total Ops/s** | 18.21 | **18.56** | 17.49 | -| **Avg Wait Time** | 229ms | **218ms** | 265ms | -| **p50 Latency** | 56.5ms | **0.6ms** | 1.3ms | -| **p95 Latency** | 923ms | 110ms | 1,775ms | -| **Correctness** | ✅ PASS | ✅ PASS | ✅ PASS | +| Metric | Redisson | redlock4j-singlenode | redlock4j-3node | +|--------------------|-----------|----------------------|------------------| +| **Total Ops/s** | 18.24 | **18.33** | 0.81 | +| **Mean Wait Time** | 229ms | 251ms | 259ms | +| **p50 Latency** | 122.9ms | **67.1ms** | 191.3ms | +| **p99 Latency** | 1,286.7ms | 1,977.5ms | **858.0ms** | +| **Correctness** | PASS | PASS | PASS | -**Analysis**: redlock4j-singlenode outperforms Redisson by 2% with **94x better p50 latency**. +**Analysis**: redlock4j-singlenode is at parity with Redisson on throughput (within 1%) and has better p50 latency. redlock4j-3node trades throughput for the **best p99 in the field** (858ms vs Redisson 1,286.7ms); the 3-node throughput gap traces to the polling wait strategy under contention (see note above). --- ### 2. FairLock (FIFO Ordering) -Guarantees lock acquisition in request order using Redis sorted sets. +Guarantees lock acquisition in request order using Redis sorted sets. FairLock uses the polling wait strategy (keyspace notifications degrade FairLock, and exponential backoff also hurts the head-of-queue waiter). -| Metric | Redisson | redlock4j-singlenode | redlock4j-3node | -|--------|----------|---------------------|-----------------| -| **Total Ops/s** | **18.25** | 17.85 | 16.52 | -| **Avg Wait Time** | **115ms** | 116ms | 126ms | -| **FIFO Violations** | 0 | 0 | 0 | +| Metric | Redisson | redlock4j-singlenode | redlock4j-3node | +|---------------------|-------------|----------------------|------------------| +| **Total Ops/s** | **16.95** | 11.92 | ~2.95 | +| **Mean Wait Time** | **249ms** | 365ms | 1,685ms | +| **p99 Latency** | **229.5ms** | 471.1ms | 2,457.1ms | +| **FIFO Violations** | 0 | 0 | 0 | -**Analysis**: Redisson slightly faster due to Lua script optimization. Both maintain strict FIFO ordering. +**Analysis**: Redisson leads FairLock (~1.4x faster than single-node redlock4j) thanks to its single-Lua-script head-of-queue path. Switching redlock4j FairLock to polling moved it from ~0 ops/s to ~70% of Redisson's throughput; the per-attempt quorum head-check round-trip on 3 nodes is the remaining bottleneck (planned fix: atomic head-check + acquire). All implementations maintain strict FIFO ordering. --- @@ -76,27 +82,30 @@ Guarantees lock acquisition in request order using Redis sorted sets. Acquires multiple resources atomically with deadlock prevention. -| Metric | Redisson | redlock4j-singlenode | redlock4j-3node | -|--------|----------|---------------------|-----------------| -| **Total Ops/s** | 17.58 | **18.02** | 16.87 | -| **Avg Wait Time** | **114ms** | 117ms | 124ms | -| **Correctness** | ✅ PASS | ✅ PASS | ✅ PASS | +| Metric | Redisson | redlock4j-singlenode | redlock4j-3node | +|--------------------|-----------|----------------------|------------------| +| **Total Ops/s** | 17.05 | 16.97 | **17.72** | +| **Mean Wait Time** | 273ms | 303ms | **237ms** | +| **p99 Latency** | 1,192ms | 1,709.5ms | **1,057.6ms** | +| **Correctness** | PASS | PASS | PASS | -**Analysis**: redlock4j-singlenode is **2.5% faster** than Redisson. +**Analysis**: redlock4j-3node (multilock) is the leader on **every axis** — throughput, mean wait, and p99 — after the parallel multi-node I/O change. --- ### 4. ReadWriteLock -Allows concurrent readers with exclusive writers. +Allows concurrent readers with exclusive writers (10 clients: 5 readers + 5 writers). -| Metric | Redisson | redlock4j-singlenode | redlock4j-3node | -|--------|----------|---------------------|-----------------| -| **Reader Ops/s** | **50.44** | 20.40 | 9.24 | -| **Writer Ops/s** | 6.09 | **17.81** | 16.65 | -| **Correctness** | ✅ PASS | ✅ PASS | ✅ PASS | +| Metric | Redisson | redlock4j-singlenode | redlock4j-3node | +|------------------|------------|----------------------|------------------| +| **Reader Ops/s** | **151.65** | 74.66 | 95.41 | +| **Reader p99** | **5.82ms** | 340.6ms | 348.4ms | +| **Writer Ops/s** | 0.21 | 15.63 | **16.42** | +| **Writer p99** | 16,820.7ms | 755.6ms | **104.2ms** | +| **Correctness** | PASS | PASS | PASS | -**Analysis**: Redisson excels at concurrent reads; redlock4j has **3x better writer throughput**. +**Analysis**: Redisson leads reader throughput (~1.6x) because it decrements readers in-process via semaphore counting, while redlock4j hits Redis on every read acquire (an architectural choice, not a regression). On the writer side, **Redisson is starved** (0.21 ops/s, ~9.7s mean wait — only a handful of successful writes in 60s), while redlock4j-3node leads with 16.42 ops/s and a best-in-class 104.2ms writer p99. --- @@ -104,13 +113,14 @@ Allows concurrent readers with exclusive writers. Limits concurrent access to a resource (configurable permits). -| Metric | Redisson | redlock4j-singlenode | redlock4j-3node | -|--------|----------|---------------------|-----------------| -| **Total Ops/s** | 54.69 | **108.49** | 103.32 | -| **Avg Wait Time** | 56.94ms | **0.85ms** | 2.09ms | -| **Correctness** | ✅ PASS | ✅ PASS | ✅ PASS | +| Metric | Redisson | redlock4j-singlenode | redlock4j-3node | +|--------------------|-----------|----------------------|------------------| +| **Total Ops/s** | 54.71 | **91.09** | 87.50 | +| **Mean Wait Time** | 37.87ms | **0.83ms** | 1.84ms | +| **p99 Latency** | 386.5ms | **2.21ms** | 4.20ms | +| **Correctness** | PASS | PASS | PASS | -**Analysis**: redlock4j-singlenode is **2x faster** with **67x lower latency**. +**Analysis**: redlock4j-singlenode is **~1.7x faster** than Redisson with **~100x lower p99 latency**; the 3-node mode is nearly as fast. --- @@ -118,13 +128,14 @@ Limits concurrent access to a resource (configurable permits). Distributed coordination - wait for N events to complete. -| Metric | Redisson | redlock4j-singlenode | redlock4j-3node | -|--------|----------|---------------------|-----------------| -| **Latches/s** | 59.66 | 35.59 | **60.97** | -| **Avg Wait Time** | 16.74ms | **16.32ms** | 16.34ms | -| **Correctness** | ✅ PASS | ✅ PASS | ✅ PASS | +| Metric | Redisson | redlock4j-singlenode | redlock4j-3node | +|--------------------|-----------|----------------------|------------------| +| **Latches/s** | 59.02 | 58.19 | **59.91** | +| **Mean Wait Time** | 16.91ms | 15.67ms | **15.18ms** | +| **p99 Latency** | 22.6ms | 19.9ms | **19.9ms** | +| **Correctness** | PASS | PASS | PASS | -**Analysis**: redlock4j-3node slightly beats Redisson. Coordination primitives benefit from multi-node distribution. +**Analysis**: All three are within ~3% on throughput; redlock4j-3node has a slight edge on both throughput and p99 latency. --- @@ -132,38 +143,41 @@ Distributed coordination - wait for N events to complete. ### Throughput (ops/s) - Higher is Better -| Lock Type | Redisson | redlock4j-singlenode | redlock4j-3node | Winner | -|-----------|----------|---------------------|-----------------|--------| -| Distributed Lock | 18.21 | **18.56** | 17.49 | redlock4j | -| FairLock | **18.25** | 17.85 | 16.52 | Redisson | -| MultiLock | 17.58 | **18.02** | 16.87 | redlock4j | -| ReadWriteLock | **56.53** | 38.22 | 25.89 | Redisson | -| **Semaphore** | 54.69 | **108.49** | 103.32 | **redlock4j** | -| CountDownLatch | 59.66 | 35.59 | **60.97** | redlock4j | - -### Latency (avg ms) - Lower is Better - -| Lock Type | Redisson | redlock4j-singlenode | redlock4j-3node | Winner | -|-----------|----------|---------------------|-----------------|--------| -| Distributed Lock | 229 | **218** | 265 | redlock4j | -| FairLock | **115** | 116 | 126 | Redisson | -| MultiLock | **114** | 117 | 124 | Redisson | -| ReadWriteLock | **110** | 117 | 275 | Redisson | -| **Semaphore** | 56.94 | **0.85** | 2.09 | **redlock4j** | -| CountDownLatch | 16.74 | **16.32** | 16.34 | redlock4j | +| Lock Type | Redisson | redlock4j-singlenode | redlock4j-3node | Winner | +|------------------------|------------|----------------------|------------------|-------------------------| +| Distributed Lock | 18.24 | **18.33** | 0.81 | redlock4j (single-node) | +| FairLock | **16.95** | 11.92 | ~2.95 | Redisson | +| MultiLock | 17.05 | 16.97 | **17.72** | redlock4j (3-node) | +| ReadWriteLock (reader) | **151.65** | 74.66 | 95.41 | Redisson | +| ReadWriteLock (writer) | 0.21 | 15.63 | **16.42** | redlock4j (3-node) | +| **Semaphore** | 54.71 | **91.09** | 87.50 | **redlock4j** | +| CountDownLatch | 59.02 | 58.19 | **59.91** | redlock4j (3-node) | + +### p99 Latency (ms) - Lower is Better + +| Lock Type | Redisson | redlock4j-singlenode | redlock4j-3node | Winner | +|------------------------|-----------|----------------------|------------------|--------------------| +| Distributed Lock | 1,286.7 | 1,977.5 | **858.0** | redlock4j (3-node) | +| FairLock | **229.8** | 434.8 | 2,457.1 | Redisson | +| MultiLock | 1,192.2 | 1,709.5 | **1,057.6** | redlock4j (3-node) | +| ReadWriteLock (reader) | **5.82** | 340.6 | 348.4 | Redisson | +| ReadWriteLock (writer) | 16,820.7 | 755.6 | **104.2** | redlock4j (3-node) | +| **Semaphore** | 386.5 | **2.21** | 4.20 | **redlock4j** | +| CountDownLatch | 22.6 | 19.9 | **19.9** | redlock4j (3-node) | --- ## Key Takeaways -1. **Basic Distributed Lock**: redlock4j matches or beats Redisson -2. **FairLock**: Redisson has slight edge due to Lua optimization -3. **MultiLock**: redlock4j competitive, simpler implementation -4. **ReadWriteLock**: Redisson better for read-heavy, redlock4j better for write-heavy -5. **Semaphore**: redlock4j is **2x faster** with **67x lower latency** -6. **CountDownLatch**: redlock4j-3node slightly faster -7. **3-node overhead**: ~5-10% performance cost for distributed consensus -8. **Correctness**: All implementations pass with zero violations +1. **Basic Distributed Lock**: single-node redlock4j is at parity with Redisson on throughput; 3-node has the best p99 in the field but low throughput under contention (polling wait strategy, not consensus cost) +2. **FairLock**: Redisson has the edge due to its single-Lua-script head-of-queue path +3. **MultiLock**: redlock4j-3node leads on every axis (throughput and p99) +4. **ReadWriteLock**: Redisson better for read-heavy; redlock4j far better for write-heavy (Redisson starves writers) and best-in-class writer p99 +5. **Semaphore**: redlock4j is **~1.7x faster** with **~100x lower p99 latency** +6. **CountDownLatch**: redlock4j-3node slightly faster on throughput and p99 +7. **3-node throughput gap**: on `DistributedLock` and `FairLock` the bottleneck is the polling wait strategy under contention (pub/sub-on-release is the planned fix); other primitives are at or above field-leader performance +8. **redlock4j leads 4 of 7 categories on throughput and is best-in-class on p99 for 5 of 7** +9. **Correctness**: All implementations pass with zero violations --- @@ -205,17 +219,19 @@ mvn exec:java -Dbenchmark.mainClass="org.codarama.redlock4j.benchmark.CountDownL ### Command Line Options -| Option | Description | Default | -|--------|-------------|---------| -| `--duration ` | Benchmark duration in minutes | 30 | -| `--clients ` | Number of concurrent clients | 10 | -| `--nodes ` | Number of Redis nodes | 3 | -| `--warmup ` | Warmup duration in seconds | 60 | -| `--resources ` | Resources per MultiLock | 5 | -| `--writers ` | Writer count for RWLock | 2 | -| `--readers ` | Reader count for RWLock | 8 | -| `--permits ` | Semaphore permits | 3 | -| `--count ` | CountDownLatch count | 5 | +The table below lists the CLI **tool defaults**. Note these differ from the settings used to produce the results on this page: the published numbers were measured with **30s warmup + 60s (1 min) measurement, 5 clients** (10 for ReadWriteLock), matching the Architecture guide's Performance Analysis and `benchmark-analysis.md` §7. + +| Option | Description | Tool Default | +|--------------------|----------------------------------------|--------------| +| `--duration ` | Measurement duration in minutes | 30 | +| `--clients ` | Number of concurrent clients | 10 | +| `--nodes ` | Number of Redis nodes | 3 | +| `--warmup ` | Warmup duration in seconds (discarded) | 60 | +| `--resources ` | Resources per MultiLock | 5 | +| `--writers ` | Writer count for RWLock | 2 | +| `--readers ` | Reader count for RWLock | 8 | +| `--permits ` | Semaphore permits | 3 | +| `--count ` | CountDownLatch count | 5 | ### Quick Benchmark diff --git a/docs/guide/best-practices.md b/docs/guide/best-practices.md index 99eba36..c9685aa 100644 --- a/docs/guide/best-practices.md +++ b/docs/guide/best-practices.md @@ -2,6 +2,9 @@ Follow these best practices to use Redlock4j effectively and safely in production. +All examples assume a `RedlockManager manager` created from a +`RedlockConfiguration`, and locks obtained via `manager.createLock(...)`. + ## Lock Management ### Always Release Locks @@ -9,24 +12,27 @@ Follow these best practices to use Redlock4j effectively and safely in productio Always release locks in a `finally` block: ```java -Lock lock = redlock.lock("resource", 10000); -if (lock != null) { - try { - // Critical section - } finally { - redlock.unlock(lock); // Always execute - } +Lock lock = manager.createLock("resource"); +lock.lock(); +try { + // Critical section +} finally { + lock.unlock(); // Always execute } ``` ### Check Lock Acquisition -Always check if lock acquisition succeeded: +When you don't want to block indefinitely, use `tryLock()` and check the result: ```java -Lock lock = redlock.lock("resource", 10000); -if (lock != null) { - // Lock acquired successfully +Lock lock = manager.createLock("resource"); +if (lock.tryLock()) { + try { + // Lock acquired successfully + } finally { + lock.unlock(); + } } else { // Failed to acquire lock - handle appropriately handleLockFailure(); @@ -35,18 +41,28 @@ if (lock != null) { ### Use Appropriate TTL -Set TTL longer than your operation: +Set the lock timeout longer than your operation. The lock's time-to-live is the +`defaultLockTimeout` configured on the manager: ```java // Bad: TTL too short -Lock lock = redlock.lock("resource", 1000); // 1 second -performLongOperation(); // Takes 5 seconds - lock will expire! +RedlockConfiguration bad = RedlockConfiguration.builder() + .addRedisNode("redis1", 6379) + .defaultLockTimeout(Duration.ofSeconds(1)) // 1 second + .build(); +// performLongOperation() takes 5 seconds - lock will expire! // Good: TTL with safety margin -Lock lock = redlock.lock("resource", 10000); // 10 seconds -performLongOperation(); // Takes 5 seconds - safe +RedlockConfiguration good = RedlockConfiguration.builder() + .addRedisNode("redis1", 6379) + .defaultLockTimeout(Duration.ofSeconds(10)) // 10 seconds + .build(); +// performLongOperation() takes 5 seconds - safe ``` +If an operation occasionally runs long, extend the lock instead of setting an +excessively large TTL: `((Redlock) lock).extend(10000)`. + ## Redis Configuration ### Use Independent Redis Instances @@ -55,53 +71,73 @@ For production, use truly independent Redis instances: ```java // Good: Independent instances on different servers -Redlock redlock = new Redlock( - new JedisPool("redis1.example.com", 6379), - new JedisPool("redis2.example.com", 6379), - new JedisPool("redis3.example.com", 6379) -); +RedlockConfiguration config = RedlockConfiguration.builder() + .addRedisNode("redis1.example.com", 6379) + .addRedisNode("redis2.example.com", 6379) + .addRedisNode("redis3.example.com", 6379) + .build(); // Bad: Master-slave replication (not independent) -// Don't use master and its slaves as separate instances +// Don't use a master and its slaves as separate nodes ``` ### Minimum 3 Instances -Always use at least 3 Redis instances: +Use at least 3 Redis instances for fault tolerance: ```java // Minimum for fault tolerance -Redlock redlock = new Redlock(pool1, pool2, pool3); +RedlockConfiguration config = RedlockConfiguration.builder() + .addRedisNode("redis1", 6379) + .addRedisNode("redis2", 6379) + .addRedisNode("redis3", 6379) + .build(); // Better: 5 instances for higher availability -Redlock redlock = new Redlock(pool1, pool2, pool3, pool4, pool5); +RedlockConfiguration config = RedlockConfiguration.builder() + .addRedisNode("redis1", 6379) + .addRedisNode("redis2", 6379) + .addRedisNode("redis3", 6379) + .addRedisNode("redis4", 6379) + .addRedisNode("redis5", 6379) + .build(); ``` +A single node is also valid for local development or non-critical use: with one +node Redlock4j runs in single-node mode. With three or more nodes it requires a +quorum. + ### Use Odd Numbers Always use an odd number of instances: -- ✅ 3, 5, 7 instances -- ❌ 2, 4, 6 instances +- 3, 5, 7 instances +- 2, 4, 6 instances + +!!! warning "Exactly 2 nodes is not supported" + A configuration with **exactly 2 nodes** throws `IllegalArgumentException` + at `build()`. Use **1 node** (single-node mode) or **3 or more** nodes + (quorum mode). Two nodes cannot form a meaningful quorum, so it is rejected + outright. ## Error Handling ### Handle Lock Failures ```java -Lock lock = redlock.lock("resource", 10000); -if (lock == null) { +Lock lock = manager.createLock("resource"); +if (!lock.tryLock()) { // Log the failure logger.warn("Failed to acquire lock for resource"); - - // Implement fallback strategy + + // Implement a fallback strategy // Option 1: Retry later scheduleRetry(); - - // Option 2: Return error to caller + + // Option 2: Return an error to the caller throw new LockAcquisitionException("Could not acquire lock"); - - // Option 3: Use alternative approach + + // Option 3: Use an alternative approach performAlternativeOperation(); } ``` @@ -109,22 +145,23 @@ if (lock == null) { ### Handle Exceptions ```java -Lock lock = null; +Lock lock = manager.createLock("resource"); +boolean acquired = false; try { - lock = redlock.lock("resource", 10000); - if (lock != null) { + acquired = lock.tryLock(); + if (acquired) { performCriticalOperation(); } } catch (Exception e) { logger.error("Error in critical section", e); handleError(e); } finally { - if (lock != null) { + if (acquired) { try { - redlock.unlock(lock); + lock.unlock(); } catch (Exception e) { logger.error("Error releasing lock", e); - // Don't throw - we're in finally block + // Don't throw - we're in a finally block } } } @@ -132,46 +169,56 @@ try { ## Performance -### Reuse Redlock Instances +### Reuse the RedlockManager -Create Redlock instances once and reuse: +Create the `RedlockManager` once and reuse it. It owns the Redis connections, so +creating one per operation is wasteful. Creating individual locks from a shared +manager is cheap: ```java -// Good: Singleton pattern +// Good: single shared manager public class LockService { - private static final Redlock REDLOCK = createRedlock(); - + private static final RedlockManager MANAGER = RedlockManager.withJedis(createConfig()); + public Lock acquireLock(String resource) { - return REDLOCK.lock(resource, 10000); + Lock lock = MANAGER.createLock(resource); + lock.lock(); + return lock; } } -// Bad: Creating new instance each time +// Bad: creating a new manager each time public Lock acquireLock(String resource) { - Redlock redlock = new Redlock(pool1, pool2, pool3); // Wasteful! - return redlock.lock(resource, 10000); + RedlockManager manager = RedlockManager.withJedis(createConfig()); // Wasteful! + Lock lock = manager.createLock(resource); + lock.lock(); + return lock; } ``` -### Configure Connection Pools +### Tune Node Connections -Properly configure connection pools: +Connection tuning happens per node via `RedisNodeConfiguration`, not via a pool +config object: ```java -JedisPoolConfig config = new JedisPoolConfig(); -config.setMaxTotal(128); // Enough for your load -config.setMaxIdle(64); // Keep some idle connections -config.setMinIdle(16); // Minimum ready connections -config.setTestOnBorrow(true); // Validate connections -config.setTestWhileIdle(true); // Clean up stale connections +RedisNodeConfiguration node = RedisNodeConfiguration.builder() + .host("redis1") + .port(6379) + .connectionTimeoutMs(2000) // time to establish a connection + .socketTimeoutMs(2000) // time to wait on a command + .build(); ``` ### Use Appropriate Retry Settings ```java RedlockConfiguration config = RedlockConfiguration.builder() - .retryCount(3) // Don't retry too many times - .retryDelay(200) // Reasonable delay between retries + .addRedisNode("redis1", 6379) + .addRedisNode("redis2", 6379) + .addRedisNode("redis3", 6379) + .maxRetryAttempts(3) // Don't retry too many times + .retryDelay(Duration.ofMillis(200)) // Reasonable delay between retries .build(); ``` @@ -180,24 +227,25 @@ RedlockConfiguration config = RedlockConfiguration.builder() ### Use Descriptive Names ```java -// Good: Clear and descriptive -Lock lock = redlock.lock("user:123:profile:update", 10000); -Lock lock = redlock.lock("order:456:payment:process", 10000); +// Good: clear and descriptive +Lock lock = manager.createLock("user:123:profile:update"); +Lock lock = manager.createLock("order:456:payment:process"); -// Bad: Unclear names -Lock lock = redlock.lock("lock1", 10000); -Lock lock = redlock.lock("temp", 10000); +// Bad: unclear names +Lock lock = manager.createLock("lock1"); +Lock lock = manager.createLock("temp"); ``` -### Use Consistent Naming Convention +### Use a Consistent Naming Convention ```java // Establish a pattern -String lockKey = String.format("%s:%s:%s", +String lockKey = String.format("%s:%s:%s", entityType, // "user", "order", "product" entityId, // "123", "456" - operation // "update", "delete", "process" -); + operation); // "update", "delete", "process" + +Lock lock = manager.createLock(lockKey); ``` ## Monitoring and Logging @@ -205,13 +253,13 @@ String lockKey = String.format("%s:%s:%s", ### Log Lock Operations ```java -Lock lock = redlock.lock(resourceId, ttl); -if (lock != null) { +Lock lock = manager.createLock(resourceId); +if (lock.tryLock()) { logger.info("Acquired lock for resource: {}", resourceId); try { performOperation(); } finally { - redlock.unlock(lock); + lock.unlock(); logger.info("Released lock for resource: {}", resourceId); } } else { @@ -235,68 +283,78 @@ Track important metrics: ```java @Test public void testLockAcquisition() { - Lock lock = redlock.lock("test-resource", 10000); - assertNotNull(lock, "Should acquire lock"); - - // Try to acquire same lock - should fail - Lock lock2 = redlock.tryLock("test-resource", 10000); - assertNull(lock2, "Should not acquire already locked resource"); - - redlock.unlock(lock); + Lock lock = manager.createLock("test-resource"); + assertTrue(lock.tryLock(), "Should acquire lock"); + + // Try to acquire the same lock from another thread - should fail + Lock lock2 = manager.createLock("test-resource"); + assertFalse(lockFromOtherThread(lock2), "Should not acquire an already locked resource"); + + lock.unlock(); } ``` ### Use Testcontainers -For integration tests: +For integration tests, point node configuration at the container's mapped port: ```java @Testcontainers public class RedlockIntegrationTest { @Container - private static GenericContainer redis = + private static GenericContainer redis = new GenericContainer<>("redis:7-alpine") .withExposedPorts(6379); - + @Test public void testWithRealRedis() { - JedisPool pool = new JedisPool( - redis.getHost(), - redis.getFirstMappedPort() - ); - Redlock redlock = new Redlock(pool); - // Test with real Redis + RedlockConfiguration config = RedlockConfiguration.builder() + .addRedisNode(redis.getHost(), redis.getFirstMappedPort()) + .build(); + + try (RedlockManager manager = RedlockManager.withJedis(config)) { + // Test with real Redis + } } } ``` ## Common Pitfalls -### ❌ Don't Forget to Unlock +### Don't Forget to Unlock ```java -// Bad: No unlock -Lock lock = redlock.lock("resource", 10000); -performOperation(); // If this throws, lock is never released! +// Bad: no unlock +Lock lock = manager.createLock("resource"); +lock.lock(); +performOperation(); // If this throws, the lock is never released! ``` -### ❌ Don't Use Same Redis Instance Multiple Times +Always pair `lock()` / successful `tryLock()` with `unlock()` in a `finally` +block. + +### Don't Configure Exactly 2 Nodes ```java -// Bad: Same instance counted 3 times -Redlock redlock = new Redlock(pool, pool, pool); // Wrong! +// Bad: exactly 2 nodes throws IllegalArgumentException at build() +RedlockConfiguration config = RedlockConfiguration.builder() + .addRedisNode("redis1", 6379) + .addRedisNode("redis2", 6379) + .build(); // throws! ``` -### ❌ Don't Ignore Lock Acquisition Failures +Use 1 node (single-node mode) or 3+ nodes (quorum mode). + +### Don't Ignore Lock Acquisition Failures ```java -// Bad: Assuming lock is always acquired -Lock lock = redlock.lock("resource", 10000); -performOperation(); // What if lock is null? +// Bad: assuming the lock is always acquired +Lock lock = manager.createLock("resource"); +lock.tryLock(); // return value ignored +performOperation(); // What if the lock wasn't acquired? ``` ## Next Steps - [API Reference](../api/redlock-manager.md) - Detailed API documentation - [Advanced Locking](advanced-locking.md) - Advanced features - diff --git a/docs/guide/redis-clients.md b/docs/guide/redis-clients.md index 3a48f33..bd80345 100644 --- a/docs/guide/redis-clients.md +++ b/docs/guide/redis-clients.md @@ -1,6 +1,11 @@ # Redis Clients -Redlock4j supports both Jedis and Lettuce Redis clients through a clean driver abstraction. +Redlock4j supports both Jedis and Lettuce Redis clients through a clean driver +abstraction. You never construct pools or clients yourself: you describe your +Redis nodes with `RedisNodeConfiguration` / `RedlockConfiguration`, and pick the +driver once when you create the manager via `RedlockManager.withJedis(config)` +or `RedlockManager.withLettuce(config)`. The chosen driver is used for every +node, and the manager owns the underlying connections. ## Jedis @@ -19,34 +24,43 @@ Jedis is a synchronous Redis client that's simple and straightforward. ### Basic Usage ```java -import redis.clients.jedis.JedisPool; -import org.codarama.redlock4j.Redlock; +import org.codarama.redlock4j.RedlockManager; +import org.codarama.redlock4j.configuration.RedlockConfiguration; -JedisPool pool1 = new JedisPool("localhost", 6379); -JedisPool pool2 = new JedisPool("localhost", 6380); -JedisPool pool3 = new JedisPool("localhost", 6381); +RedlockConfiguration config = RedlockConfiguration.builder() + .addRedisNode("localhost", 6379) + .addRedisNode("localhost", 6380) + .addRedisNode("localhost", 6381) + .build(); -Redlock redlock = new Redlock(pool1, pool2, pool3); +RedlockManager manager = RedlockManager.withJedis(config); ``` -### Connection Pool Configuration +### Node Configuration + +You don't pass a `JedisPool` or pool config object. Instead, tune each node with +`RedisNodeConfiguration`, which exposes the connection settings Redlock4j needs +(host, port, optional password and database, and connection/socket timeouts): ```java -import redis.clients.jedis.JedisPoolConfig; - -JedisPoolConfig config = new JedisPoolConfig(); -config.setMaxTotal(128); -config.setMaxIdle(128); -config.setMinIdle(16); -config.setTestOnBorrow(true); -config.setTestOnReturn(true); -config.setTestWhileIdle(true); -config.setMinEvictableIdleTimeMillis(60000); -config.setTimeBetweenEvictionRunsMillis(30000); -config.setNumTestsPerEvictionRun(3); -config.setBlockWhenExhausted(true); - -JedisPool pool = new JedisPool(config, "localhost", 6379, 2000); +import org.codarama.redlock4j.configuration.RedisNodeConfiguration; +import org.codarama.redlock4j.configuration.RedlockConfiguration; + +RedisNodeConfiguration node = RedisNodeConfiguration.builder() + .host("localhost") + .port(6379) + .password("secret") // optional, default null + .database(0) // optional, default 0 + .connectionTimeoutMs(2000) // default 2000 + .socketTimeoutMs(2000) // default 2000 + .build(); + +RedlockConfiguration config = RedlockConfiguration.builder() + .addRedisNode(node) + // ... add the other nodes ... + .build(); + +RedlockManager manager = RedlockManager.withJedis(config); ``` ### Advantages @@ -78,49 +92,46 @@ Lettuce is an advanced Redis client with async and reactive support. ### Basic Usage +Selecting Lettuce is a one-line change: the node configuration is identical, you +just create the manager with `withLettuce`: + ```java -import io.lettuce.core.RedisClient; -import org.codarama.redlock4j.Redlock; +import org.codarama.redlock4j.RedlockManager; +import org.codarama.redlock4j.configuration.RedlockConfiguration; -RedisClient client1 = RedisClient.create("redis://localhost:6379"); -RedisClient client2 = RedisClient.create("redis://localhost:6380"); -RedisClient client3 = RedisClient.create("redis://localhost:6381"); +RedlockConfiguration config = RedlockConfiguration.builder() + .addRedisNode("localhost", 6379) + .addRedisNode("localhost", 6380) + .addRedisNode("localhost", 6381) + .build(); -Redlock redlock = new Redlock(client1, client2, client3); +RedlockManager manager = RedlockManager.withLettuce(config); ``` -### Advanced Configuration +### Node Configuration -```java -import io.lettuce.core.RedisURI; -import io.lettuce.core.ClientOptions; -import io.lettuce.core.TimeoutOptions; -import io.lettuce.core.resource.ClientResources; -import io.lettuce.core.resource.DefaultClientResources; -import java.time.Duration; - -// Configure client resources -ClientResources resources = DefaultClientResources.builder() - .ioThreadPoolSize(4) - .computationThreadPoolSize(4) - .build(); +As with Jedis, you don't build `RedisURI`, `ClientOptions`, or +`ClientResources` yourself. The same `RedisNodeConfiguration` fields drive the +Lettuce driver, including per-node timeouts, password, and database selection: -// Configure Redis URI -RedisURI redisUri = RedisURI.builder() - .withHost("localhost") - .withPort(6379) - .withTimeout(Duration.ofSeconds(5)) - .withDatabase(0) +```java +import org.codarama.redlock4j.configuration.RedisNodeConfiguration; +import org.codarama.redlock4j.configuration.RedlockConfiguration; + +RedisNodeConfiguration node = RedisNodeConfiguration.builder() + .host("localhost") + .port(6379) + .database(0) + .connectionTimeoutMs(5000) + .socketTimeoutMs(5000) .build(); -// Configure client options -ClientOptions options = ClientOptions.builder() - .autoReconnect(true) - .timeoutOptions(TimeoutOptions.enabled(Duration.ofSeconds(5))) +RedlockConfiguration config = RedlockConfiguration.builder() + .addRedisNode(node) + // ... add the other nodes ... .build(); -RedisClient client = RedisClient.create(resources, redisUri); -client.setOptions(options); +RedlockManager manager = RedlockManager.withLettuce(config); ``` ### Advantages @@ -152,54 +163,23 @@ client.setOptions(options); - You're using Redis Cluster or Sentinel - You need advanced features -## Mixed Usage - -You can use different clients for different Redis instances: - -```java -// Mix Jedis and Lettuce (not recommended, but possible) -JedisPool jedisPool = new JedisPool("localhost", 6379); -RedisClient lettuceClient1 = RedisClient.create("redis://localhost:6380"); -RedisClient lettuceClient2 = RedisClient.create("redis://localhost:6381"); - -// This works, but stick to one client type for consistency -Redlock redlock = new Redlock(jedisPool, lettuceClient1, lettuceClient2); -``` - -!!! warning "Consistency Recommendation" - While mixing clients is technically possible, it's recommended to use the same client type for all Redis instances for consistency and easier maintenance. - ## Connection Management -### Jedis +The `RedlockManager` owns the connections to every configured node, regardless +of which driver you selected. You do not open or close pools or clients +yourself. Because `RedlockManager` implements `AutoCloseable`, closing the +manager releases all underlying connections: ```java -// Always close pools when done -try { - // Use redlock -} finally { - pool1.close(); - pool2.close(); - pool3.close(); -} +try (RedlockManager manager = RedlockManager.withJedis(config)) { + // Use the manager and the locks it creates +} // all connections closed automatically ``` -### Lettuce - -```java -// Shutdown clients and resources -try { - // Use redlock -} finally { - client1.shutdown(); - client2.shutdown(); - client3.shutdown(); - resources.shutdown(); -} -``` +If you manage the manager's lifecycle manually, call `manager.close()` when your +application shuts down. ## Next Steps - [Best Practices](best-practices.md) - Follow recommended practices - [Configuration](../api/configuration.md) - Detailed configuration options - diff --git a/docs/index.md b/docs/index.md index 65df5d0..92a0cee 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,13 @@ -# Redlock4j +

+ Redlock4j logo +

+ +[![CI](https://github.com/Codarama/redlock4j/actions/workflows/ci.yml/badge.svg)](https://github.com/Codarama/redlock4j/actions/workflows/ci.yml) +[![codecov](https://codecov.io/gh/Codarama/redlock4j/graph/badge.svg?token=EK4LMLJ533)](https://codecov.io/gh/Codarama/redlock4j) +[![Maven Central](https://img.shields.io/maven-central/v/org.codarama/redlock4j?versionSuffix=RELEASE)](https://maven-badges.herokuapp.com/maven-central/org.codarama/redlock4j) +[![Javadocs](https://www.javadoc.io/badge/org.codarama/redlock4j.svg)](https://javadoc.io/doc/org.codarama/redlock4j) +[![Java](https://img.shields.io/badge/Java-8%2B-blue.svg)](https://openjdk.java.net/) +[![Guide](https://img.shields.io/badge/mkdocs-guide-526CFE?logo=materialformkdocs&logoColor=white)](https://codarama.github.io/redlock4j/) A robust Java implementation of the [Redlock distributed locking algorithm](https://redis.io/topics/distlock) for Redis. @@ -20,23 +29,33 @@ Redlock4j provides a reliable distributed locking mechanism using Redis, impleme ## Quick Example ```java -// Create a Redlock instance -Redlock redlock = new Redlock(jedisPool1, jedisPool2, jedisPool3); - -// Acquire a lock -Lock lock = redlock.lock("my-resource", 10000); - -if (lock != null) { +import org.codarama.redlock4j.RedlockManager; +import org.codarama.redlock4j.configuration.RedlockConfiguration; + +import java.time.Duration; +import java.util.concurrent.locks.Lock; + +// Configure the Redis nodes (host/port) and lock behaviour +RedlockConfiguration config = RedlockConfiguration.builder() + .addRedisNode("redis1.example.com", 6379) + .addRedisNode("redis2.example.com", 6379) + .addRedisNode("redis3.example.com", 6379) + .defaultLockTimeout(Duration.ofSeconds(30)) + .retryDelay(Duration.ofMillis(200)) + .maxRetryAttempts(3) + .build(); + +// RedlockManager is AutoCloseable - use try-with-resources +try (RedlockManager manager = RedlockManager.withJedis(config)) { + Lock lock = manager.createLock("my-resource"); + lock.lock(); try { // Critical section - your protected code here performCriticalOperation(); } finally { // Always unlock in a finally block - redlock.unlock(lock); + lock.unlock(); } -} else { - // Failed to acquire lock - handleLockFailure(); } ``` diff --git a/docs/primitives/count-down-latch.md b/docs/primitives/count-down-latch.md index 8bdddf4..b79729a 100644 --- a/docs/primitives/count-down-latch.md +++ b/docs/primitives/count-down-latch.md @@ -109,7 +109,7 @@ latch.await(); // Blocks until count reaches 0 System.out.println("All services ready!"); // Or wait with timeout -if (latch.await(30, TimeUnit.SECONDS)) { +if (latch.await(Duration.ofSeconds(30))) { System.out.println("All services ready!"); } else { System.out.println("Timeout waiting for services"); @@ -118,47 +118,47 @@ if (latch.await(30, TimeUnit.SECONDS)) { ## Use Cases -| Scenario | Initial Count | Purpose | -|----------|---------------|---------| -| Service startup | N services | Wait for all to initialize | -| Batch processing | N jobs | Wait for all to complete | -| Test coordination | N threads | Synchronize test execution | -| Distributed workflow | N stages | Gate between stages | +| Scenario | Initial Count | Purpose | +|----------------------|---------------|----------------------------| +| Service startup | N services | Wait for all to initialize | +| Batch processing | N jobs | Wait for all to complete | +| Test coordination | N threads | Synchronize test execution | +| Distributed workflow | N stages | Gate between stages | ## Methods -| Method | Description | -|--------|-------------| -| `countDown()` | Decrement count by 1 | -| `await()` | Wait indefinitely for count=0 | -| `await(long, TimeUnit)` | Wait with timeout | -| `getCount()` | Current count value | +| Method | Description | +|-------------------|-------------------------------| +| `countDown()` | Decrement count by 1 | +| `await()` | Wait indefinitely for count=0 | +| `await(Duration)` | Wait with timeout | +| `getCount()` | Current count value | ## Supported Modes -| Mode | Supported | Notes | -|------|-----------|-------| -| **Single Node** | ✅ | Count on single instance | -| **Multi-Node (Quorum)** | ✅ | Count averaged (median) across nodes | +| Mode | Supported | Notes | +|-------------------------|------------|--------------------------------------| +| **Single Node** | Yes | Count on single instance | +| **Multi-Node (Quorum)** | Yes | Count averaged (median) across nodes | In multi-node mode, `getCount()` returns the median value across all nodes for consistency. ## Configuration -| Parameter | Default | Description | -|-----------|---------|-------------| -| `initialCount` | (required) | Starting count value | -| `latchTimeoutMs` | 300000 | Latch expiration TTL | -| `useKeyspaceNotifications` | true | Use Pub/Sub vs polling | +| Parameter | Default | Description | +|----------------------------|------------|------------------------| +| `initialCount` | (required) | Starting count value | +| `latchTimeoutMs` | 300000 | Latch expiration TTL | +| `useKeyspaceNotifications` | true | Use Pub/Sub vs polling | ## Comparison with Java CountDownLatch -| Feature | Java CDL | Redlock CDL | -|---------|----------|-------------| -| Scope | Single JVM | Distributed | -| Persistence | Memory | Redis | -| Failure recovery | Lost on crash | Survives restarts | -| Network partition | N/A | Quorum-based | +| Feature | Java CDL | Redlock CDL | +|-------------------|---------------|-------------------| +| Scope | Single JVM | Distributed | +| Persistence | Memory | Redis | +| Failure recovery | Lost on crash | Survives restarts | +| Network partition | N/A | Quorum-based | ## When to Use diff --git a/docs/primitives/fair-lock.md b/docs/primitives/fair-lock.md index 0070b3d..a90cece 100644 --- a/docs/primitives/fair-lock.md +++ b/docs/primitives/fair-lock.md @@ -4,12 +4,12 @@ Fair locks ensure threads acquire locks in the order they requested them (First- ## Overview -| Property | Value | -|----------|-------| -| **Type** | Exclusive Lock with FIFO ordering | -| **Reentrancy** | Supported (per-thread) | -| **Interface** | `java.util.concurrent.locks.Lock` | -| **Data Structure** | Redis Sorted Set (queue) | +| Property | Value | +|--------------------|-----------------------------------| +| **Type** | Exclusive Lock with FIFO ordering | +| **Reentrancy** | Supported (per-thread) | +| **Interface** | `java.util.concurrent.locks.Lock` | +| **Data Structure** | Redis Sorted Set (queue) | ## How It Works @@ -98,27 +98,27 @@ try { ## Trade-offs -| Aspect | FairLock | Standard Redlock | -|--------|----------|------------------| -| **Ordering** | Guaranteed FIFO | Non-deterministic | -| **Throughput** | Lower | Higher | -| **Redis Operations** | More (queue mgmt) | Fewer | -| **Starvation** | Prevented | Possible | +| Aspect | FairLock | Standard Redlock | +|----------------------|-------------------|-------------------| +| **Ordering** | Guaranteed FIFO | Non-deterministic | +| **Throughput** | Lower | Higher | +| **Redis Operations** | More (queue mgmt) | Fewer | +| **Starvation** | Prevented | Possible | ## Supported Modes -| Mode | Supported | Notes | -|------|-----------|-------| -| **Single Node** | ✅ | Queue maintained on single instance | -| **Multi-Node (Quorum)** | ✅ | Queue replicated, quorum for lock acquisition | +| Mode | Supported | Notes | +|-------------------------|------------|-----------------------------------------------| +| **Single Node** | Yes | Queue maintained on single instance | +| **Multi-Node (Quorum)** | Yes | Queue replicated, quorum for lock acquisition | ## Configuration -| Parameter | Default | Description | -|-----------|---------|-------------| -| `lockTimeoutMs` | 30000 | Lock TTL in Redis | -| `queueTimeoutMs` | 60000 | Queue entry TTL | -| `cleanupIntervalMs` | 5000 | Stale entry cleanup interval | +| Parameter | Default | Description | +|---------------------|----------|------------------------------| +| `lockTimeoutMs` | 30000 | Lock TTL in Redis | +| `queueTimeoutMs` | 60000 | Queue entry TTL | +| `cleanupIntervalMs` | 5000 | Stale entry cleanup interval | ## When to Use diff --git a/docs/primitives/multi-lock.md b/docs/primitives/multi-lock.md index 6c86aa7..91cc93d 100644 --- a/docs/primitives/multi-lock.md +++ b/docs/primitives/multi-lock.md @@ -4,12 +4,12 @@ MultiLock enables atomic acquisition of multiple resources, preventing deadlocks ## Overview -| Property | Value | -|----------|-------| -| **Type** | Exclusive Lock (multiple keys) | -| **Reentrancy** | Supported (per-thread) | -| **Interface** | `java.util.concurrent.locks.Lock` | -| **Deadlock Prevention** | Sorted key ordering | +| Property | Value | +|-------------------------|-----------------------------------| +| **Type** | Exclusive Lock (multiple keys) | +| **Reentrancy** | Supported (per-thread) | +| **Interface** | `java.util.concurrent.locks.Lock` | +| **Deadlock Prevention** | Sorted key ordering | ## How It Works @@ -139,29 +139,29 @@ try { Without consistent ordering, deadlocks can occur: -| Time | Client A | Client B | -|------|----------|----------| -| T1 | Lock account:1 ✓ | Lock account:2 ✓ | -| T2 | Wait account:2 | Wait account:1 | -| T3 | **DEADLOCK** | **DEADLOCK** | +| Time | Client A | Client B | +|-------|------------------|------------------| +| T1 | Lock account:1 ✓ | Lock account:2 ✓ | +| T2 | Wait account:2 | Wait account:1 | +| T3 | **DEADLOCK** | **DEADLOCK** | With sorted ordering, both clients lock `account:1` first → no deadlock. ## Supported Modes -| Mode | Supported | Notes | -|------|-----------|-------| -| **Single Node** | ✅ | All keys locked on single instance | -| **Multi-Node (Quorum)** | ✅ | All keys must achieve quorum on each node | +| Mode | Supported | Notes | +|-------------------------|------------|-------------------------------------------| +| **Single Node** | Yes | All keys locked on single instance | +| **Multi-Node (Quorum)** | Yes | All keys must achieve quorum on each node | In multi-node mode, ALL keys must be successfully locked on a quorum of nodes for the MultiLock to succeed. ## Configuration -| Parameter | Default | Description | -|-----------|---------|-------------| -| `lockTimeoutMs` | 30000 | Lock TTL per key | -| `acquisitionTimeoutMs` | 10000 | Max total wait time | +| Parameter | Default | Description | +|------------------------|---------|---------------------| +| `lockTimeoutMs` | 30000 | Lock TTL per key | +| `acquisitionTimeoutMs` | 10000 | Max total wait time | ## When to Use diff --git a/docs/primitives/read-write-lock.md b/docs/primitives/read-write-lock.md index 596594f..84a725e 100644 --- a/docs/primitives/read-write-lock.md +++ b/docs/primitives/read-write-lock.md @@ -4,11 +4,11 @@ ReadWriteLock allows multiple concurrent readers OR a single exclusive writer. ## Overview -| Property | Value | -|----------|-------| -| **Type** | Shared/Exclusive Lock | -| **Readers** | Multiple concurrent | -| **Writers** | Single exclusive | +| Property | Value | +|---------------|--------------------------------------------| +| **Type** | Shared/Exclusive Lock | +| **Readers** | Multiple concurrent | +| **Writers** | Single exclusive | | **Interface** | `java.util.concurrent.locks.ReadWriteLock` | ## How It Works @@ -115,25 +115,25 @@ try { ## Concurrency Matrix -| Holder | Read Request | Write Request | -|--------|--------------|---------------| -| None | ✓ Granted | ✓ Granted | -| Reader(s) | ✓ Granted | ✗ Blocked | -| Writer | ✗ Blocked | ✗ Blocked | +| Holder | Read Request | Write Request | +|-----------|---------------|----------------| +| None | ✓ Granted | ✓ Granted | +| Reader(s) | ✓ Granted | ✗ Blocked | +| Writer | ✗ Blocked | ✗ Blocked | ## Supported Modes -| Mode | Supported | Notes | -|------|-----------|-------| -| **Single Node** | ✅ | Reader count on single instance | -| **Multi-Node (Quorum)** | ✅ | Reader count averaged across nodes | +| Mode | Supported | Notes | +|-------------------------|------------|------------------------------------| +| **Single Node** | Yes | Reader count on single instance | +| **Multi-Node (Quorum)** | Yes | Reader count averaged across nodes | ## Configuration -| Parameter | Default | Description | -|-----------|---------|-------------| -| `lockTimeoutMs` | 30000 | Lock TTL | -| `readerTimeoutMs` | 30000 | Reader counter TTL | +| Parameter | Default | Description | +|-------------------|----------|--------------------| +| `lockTimeoutMs` | 30000 | Lock TTL | +| `readerTimeoutMs` | 30000 | Reader counter TTL | ## When to Use diff --git a/docs/primitives/redlock.md b/docs/primitives/redlock.md index a0e4d99..f4ebf11 100644 --- a/docs/primitives/redlock.md +++ b/docs/primitives/redlock.md @@ -4,12 +4,12 @@ The standard Redlock implementation provides mutual exclusion across distributed ## Overview -| Property | Value | -|----------|-------| -| **Type** | Exclusive Lock | -| **Reentrancy** | Supported (per-thread) | -| **Interface** | `java.util.concurrent.locks.Lock` | -| **Consensus** | Quorum-based (N/2+1) | +| Property | Value | +|----------------|-----------------------------------| +| **Type** | Exclusive Lock | +| **Reentrancy** | Supported (per-thread) | +| **Interface** | `java.util.concurrent.locks.Lock` | +| **Consensus** | Quorum-based (N/2+1) | ## How It Works @@ -120,10 +120,10 @@ lock.unlock(); // hold count = 0, released ## Supported Modes -| Mode | Supported | Notes | -|------|-----------|-------| -| **Single Node** | ✅ | Optimized path, no quorum overhead | -| **Multi-Node (Quorum)** | ✅ | Full Redlock algorithm with N/2+1 consensus | +| Mode | Supported | Notes | +|-------------------------|------------|---------------------------------------------| +| **Single Node** | Yes | Optimized path, no quorum overhead | +| **Multi-Node (Quorum)** | Yes | Full Redlock algorithm with N/2+1 consensus | Mode is auto-selected based on configuration: - 1 Redis node → Single Node mode @@ -131,12 +131,12 @@ Mode is auto-selected based on configuration: ## Configuration -| Parameter | Default | Description | -|-----------|---------|-------------| -| `lockTimeoutMs` | 30000 | Lock TTL in Redis | -| `acquisitionTimeoutMs` | 10000 | Max time to wait for lock | -| `retryDelayMs` | 100 | Delay between retry attempts | -| `clockDriftFactor` | 0.01 | Factor for clock drift compensation (multi-node only) | +| Parameter | Default | Description | +|------------------------|----------|-------------------------------------------------------| +| `lockTimeoutMs` | 30000 | Lock TTL in Redis | +| `acquisitionTimeoutMs` | 10000 | Max time to wait for lock | +| `retryDelayMs` | 100 | Delay between retry attempts | +| `clockDriftFactor` | 0.01 | Factor for clock drift compensation (multi-node only) | ## When to Use diff --git a/docs/primitives/semaphore.md b/docs/primitives/semaphore.md index 2e17b5f..569d92b 100644 --- a/docs/primitives/semaphore.md +++ b/docs/primitives/semaphore.md @@ -4,12 +4,12 @@ Semaphore limits concurrent access to a shared resource with a configurable numb ## Overview -| Property | Value | -|----------|-------| -| **Type** | Counting Semaphore | -| **Permits** | Configurable (1 to N) | -| **Fairness** | Non-fair (first-come basis) | -| **Expiration** | Auto-release on timeout | +| Property | Value | +|----------------|-----------------------------| +| **Type** | Counting Semaphore | +| **Permits** | Configurable (1 to N) | +| **Fairness** | Non-fair (first-come basis) | +| **Expiration** | Auto-release on timeout | ## How It Works @@ -111,38 +111,38 @@ if (semaphore.tryAcquire(3, Duration.ofSeconds(5))) { ## Use Cases -| Scenario | Permits | Purpose | -|----------|---------|---------| -| API rate limit | 10/sec | Prevent quota exhaustion | -| DB connection pool | 20 | Limit concurrent connections | -| File uploads | 5 | Control bandwidth usage | -| Batch jobs | 3 | Prevent resource starvation | +| Scenario | Permits | Purpose | +|--------------------|----------|------------------------------| +| API rate limit | 10/sec | Prevent quota exhaustion | +| DB connection pool | 20 | Limit concurrent connections | +| File uploads | 5 | Control bandwidth usage | +| Batch jobs | 3 | Prevent resource starvation | ## Supported Modes -| Mode | Supported | Notes | -|------|-----------|-------| -| **Single Node** | ✅ | Permit set on single instance | -| **Multi-Node (Quorum)** | ✅ | Permits tracked per node, quorum required | +| Mode | Supported | Notes | +|-------------------------|------------|-------------------------------------------| +| **Single Node** | Yes | Permit set on single instance | +| **Multi-Node (Quorum)** | Yes | Permits tracked per node, quorum required | ## Configuration -| Parameter | Default | Description | -|-----------|---------|-------------| -| `maxPermits` | (required) | Maximum concurrent permits | -| `permitTimeoutMs` | 30000 | Auto-release timeout | -| `acquisitionTimeoutMs` | 10000 | Max wait time for permit | +| Parameter | Default | Description | +|------------------------|------------|----------------------------| +| `maxPermits` | (required) | Maximum concurrent permits | +| `permitTimeoutMs` | 30000 | Auto-release timeout | +| `acquisitionTimeoutMs` | 10000 | Max wait time for permit | ## Methods -| Method | Description | -|--------|-------------| -| `acquire()` | Block until permit available | -| `tryAcquire(Duration)` | Try with timeout | -| `tryAcquire(int, Duration)` | Try for multiple permits | -| `release()` | Release one permit | -| `release(int)` | Release multiple permits | -| `availablePermits()` | Current available count | +| Method | Description | +|-----------------------------|------------------------------| +| `acquire()` | Block until permit available | +| `tryAcquire(Duration)` | Try with timeout | +| `tryAcquire(int, Duration)` | Try for multiple permits | +| `release()` | Release one permit | +| `release(int)` | Release multiple permits | +| `availablePermits()` | Current available count | ## When to Use diff --git a/docs/redlock4j-social-preview.png b/docs/redlock4j-social-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..fafe0d66d0ab975983f0aa7f569c92f6c0600a6f GIT binary patch literal 18261 zcmeHvX*65y+iz4`Z58cPr6@W)sx`J0MM*uVmZEB^`O(%qq~;+aeX6RZXw5SwPT>8mu0ak ze&nS5`3C|gi)k(LGny-nBay6Ah+5?7i&fPZ?YnP@6+)=(v84oHUky>7da=@KINz)t2{{2+_k{m{U^biu_2nIS z0Pg1%e1tL2YsPr^t9xvfgS|O2*=_c&>9MsI-ml?#b!qs>X#n8fqZ8ja(E0Hi@OvQl zpE!79CfFD#4k+M5!W|~PT^tJ@{{tK^DrzOAFJ|a>;Wq3ve&Oo$J3w1piai z9oJ)RTlhJ$A71?b0h9lYamv3;HlQG{P#+5n+EcNU^|ZZqkpO-yCwIh-!LEV|a z9RgHQu~PvFcb?(FDhb$>c4CIK(*>2gHHfvN_geo~1rxXlc-0~s;nH0vtr(8#mcZ$p zd0~=wlkyoC9?@hD1jygxfa0FRd7ph|Vp_yU8Rn7UzTd_v$ov+gd5rut$$RUgH{iB1 z$I%oh7!n*`P7TiV{cw8KWWa%Pd0V{?6dB5z9ZiEB2T(3^jQ+UQP8-Su<>wlW;hfZa zeHZ}ed(VvALByaO0yf`a@N7&72g45CbFY1@n%ZgJuGc?ON_}1B!)e z5TNcL#WX3q774)ZR~#XKH1uc2Rz)Jh8}@mJn@)1BNF^>D(;R*tpd)_R}vnCfLX;si@L7NfRRonzE_d5?OYoZDg7y zF4XN0BGPe9MTKko+3hre?gS(;-mV!7K{ht{tS-}U!_8;;rx?t-D*4FLYsY8T8K?U%?zVtxIx8nNjllp5XgxcG-Q=vRMXWn>- zK&YERi+;?+yG=Aaw~}pnvksj88HY8zNnMuh2kE(r7GV^TsUpKq z6BSrxDLcmm1{pX|dh3@jeNP<&j`dr&HO|+9YW8Oom5xJvf7%elN8Tk`d)h=BqtSO3 zG#iajxh+t3UN0f{qYcgNWbXkqE0Q!@X^OmAHP#H9fzSqg5;0*70Qe=v_gv#f{$q5p z_Wf}+sQJtuqwe~2d%e4X{MDWzaW?C_a!l@>-A@S0Lsb6t+Xz;pKJf5x!HG?2iKmG7 zOUq0iV#o4W^ohm?D9KuL#C%<5nFMfQX>w39*?xC+rc82coCdPr)yW+tOIvv3sQZnI zk}3xib_6+}*(tX;VaUsaxMi^SIJb=zDzHZaLj)+dr*;*;?biLcqbkII84r&d+ zu;m?Fl_^Q@hqNNMYNJT0kx{YHyijV)pe5}K|GCbg%$N8(4u@RR7daMav(VV9mOkGP z4X=U!f}Hbuj0hf_=mGAwp+HlZ2c78qOH$_0-BG_X`5e2av>;wp+}=bwEVAiq>!u_k zbh9u3b(JEwBf zz^=3%f@vn$S;f($f%dGZ8AsF}p^J|cC0+ye?nZCknC>Dzi8m~Bq>dgyXRJn72=?{f zbdaMq$<%IbdV%%a?BIhFYA@ak3-V}4yqrX`a@<)`%XiFeF$%M#;ep_}0#JMf@qu)& zWxoQ-6_-#MVlm0+Ux400L?T3A32{8=xW0eq4k;2@_erD3s;F=-Z?}~B+&MY8HbGTO zZTGd9D)ySOl(hn5sgYE5k~`>=9Avdm9qr?_6>M9(5~b;goJ*_eGTs}MG9t9DP4wqg zLqE^DXOmQ*11iYe;>2f93w0fCU7gGWMaMv}TBsG=nY z?#K;g#cA{94V`|Kc7?Ml9gD5`o%BkCvhSb0G+)TV8Zo;mTK&Z9OO+Xn7&YIV)O!7< zXVYXt?&t;OPsepW&f3CTnmVD4T1&ga_JLDbgT@Dg`sbqsBH1-DR@gAfw02E$M?MTc z?m7l~3_n1Zq5MfR6$H%(A6(;laju$hf zM>}+{WMjs>ElM=imobAD%HNyZRC)9>#lCKRnGsBh?2j*e)*~*A(yS_Pii>eBbjT0p z4tI6ECffv4cv|`t*!c30#_4|?J7%jY~K^1JbW3!Qouwb=qZ!OEkU)Wm0EvZta9~mcIjtb zdR|5mMYurz+$%0og-3A7Nf&mnRyTRddAOhzwlNkE(DA~<d(z)gXYe5)t&i}H0D}|4Xr)* zigHS8awtz*cGAjRAFjW)sL8qeb$O!&ml4$2X`Dh z68%=Cq;cjiyQWnFh;ZmyqvT!a5K}ZH7KFjl6E>1rW@^>$j8Tf|tSY;71_Lu}t1UE6 zL?irc^PMCf_vdf9Q*aHwnGU2|GaWZUFB2C`L_&Xj)}o505^j+eP!^kV4h`=8)oQAb zAg)URwLS#fg_NoI()yKcUMG5&veXSptr8jux)GIZA~OA|-N$SyrOH*izH}|=kKAyF zh`|E<;B=6J-?)#{!CX^T`gFN2F2&!?3L-seUrHd3OqU6QzAg!nJk=utZPQ_84%#e+)8;6mawZDCULE;7oFXSUWk>q%(T5vZWsI`cZ zjJ(2({0pfSgWI2bgAO62ChLy$$C2$aG=90A#%*--&<-BanNL|OIM&{`H=W_dcx(MJ zA%b$V)%Sq0B3?;Hvnr|G`y0W#yfF@8wiBx~bETFL^6{`si+ZeTlvzs(u4p4*xhiW4K+54^=3BkR}Lwbh4>vL>(Tk5tLL^)fP z1+eO|pDXaQgBO$*u_&=w>819{N!Cw@m^n?yL`chzHa&ft?Rr{-9>bui*F$l>ynl#m zNI(xhn*tlr+q$A{1~y9_c8*L;P(@75&_$({W`Pape8F-2u*E>TGhN)MGBWn;a-bHw z<(ICqsMQfb4X1kbIOM$qvlbCTjw0PWJtF-kbHQX?YfkxO*?LW?Vzucfi_sW*>SkLf7TP{jV6k1+f9p9K__uJbM&sYO zWW|z;`8U>gC;W(;mQ^W}2V1v<#nUp^XzI4i#lB{9sky~9OVdZDTE2D}6RSCPRG4vp zK&n^BMYZ)r^u^G%SKW;8qQxDsljbhb%bdR=S0(T#S#p}a&&Ai4eIr+y?FPkL1sRZT z`Yaw}JU3=vaMN+@#AmyLHn9!D{dd%Jy9)$Ygx-B+PohG7pDid(VohDiWQ_?ld zEorDW=ViB;6R=4t6PA&fBBxx5%NYcX#qgSy$)`ufg2g_Ml-Y0VBT!RnjVOqPM zh%UXMI-t(yiX#4c8-p@N`La`OtQtmSfw0m z;8b_bk{q?}tjpftYVXp>MJ@V_!PYap5yg`~xyfYxGehszUSePUaQalW_v-2No(ieNOM7Rkd zB-5#tIMau$?@|1wl)wDN1m$y@{J?#_Eew8*$j9Q^U}}3*f>@I0LG0^QkP!B%f7d`o zwvl;cj5-b#=1ZG?yIY!W-@aHFfVaz9bJ@>1i)60;I%%NV5m3IE#HL{e8!9EFGzV6; z;z{@8`#oM>{tuYszVQGv2>ezR@f+kM^g>WHSdFmAes}&ahGoGY~i&?d%a_t;yZ{|3j-Vf z@9UyvS1KpBhcqjQ_&%+d!*w+tFBcZ6NqiHDDXh=4T5oZCTiP)+&o`dd3JyEFBvm0x zacCzO;R3sj`6z?yi|o|IrXC>cX?P4b|A5Dym)lb8hLJgLuZ-dWEN~RRwX5k`ip>!$ zbe~}8=Nlq5t%XW2PTpv9!qx?yxV-wDwgIn-ejh6ymsZ@D;eh~~KA@bQAN%7fjg*w_ zi4~H^x3|2F5!9a%mN%VZWnT!}{T!C!KtK>4c2XpjIhk_P(M9Cw_XT3X?w|z{Px~ zEV6d^=JtMx$6%I1ipSn1XpLfO9!-6j4c0prEvs5KY3zKz=Tqm};lQ6>G?S>Ybd-bR zm{M=*FuR~Dss3lkaOwihW1V$qT>ElbGo9Ul_8~kNX$Gdc4?>h-P(&8j zz^K1ztLysWeQ=$~E33bFZ{Bk#ru33=N5>AiB`?t*GYWQ6D$>5r`F3$bK_Iu zZv}h<4%AL0xzyUL?0%kT%p!c9_Nlgu>{7&fSA+{k;}Qv^G6PHH=V}0--?@XZGqKtv z9=i0Q38dgNaww*!zr@b|R@5QBbYsjMD-q;%_fcT)YQ5L7Id`wsrW9m_kx6>^MP-pG zoJQF+vbSE|u8Qv;7dt~<^O)UMN1uT3nZ@M^6l-cHWT*_PO`3|gB)91`w*%Yi zq0CZs?t&mr#I<(iQ~ng}BrK=vMvs}N=TfifL_ziKAk(EoevzOVk@p{{NQ`W%nVG&{ zrRz^U#sHNf(kfS^%Sh?67K{vLRVE6ax6-QLW($tqsCsVKcJ7_3wl*V6Iv7)X{>}l5 z(X4IzgmO9WXI8`%UYd$8;kN*2&x>=?kO%z_yx z6bTwY>13qY^n`t|LzK5FQSS8Hyr*AOcD|yleEM}(bDmr+Rq9uHs~kn=ylYwGFW>gm zu0)pikY6&DVNcYk6}7Zrbd>~=THL;{z9YR5pF5%}T>mkF&Ft;tJT}-Nzz`8E`6Etw zdb6F!z2SFG^uQEUwc&C@ucFY(vg6B;+E$QLlxP0;3SW;UD=-L9qYubd(jrbdxJLL0VET>2-hKXp_kWINUGaOfn)x0h^g>C= z+-)K@rS>G$&_XxPC0o$7xWuHK)?{RdCU7cr|Mjd#iolT`-)oUQNB zBNW9**alcHUbpolejv0pd5K#BZeQl4$NU}P@G#7M-^eP`$K=e+mY~Xlf`pTIWEADR zr`qxHasx)D>UJQ1@Ki$hhY7({5aCn({@!7$Cp#lO{~4Wl=|o|TraAxogi;6myHjQ; zdE*?c4Q@KvIi$a&$=WVLG(0gb;sfZM;S^PCbS6Isxb~6!F1+>dFs2d8G{)XGUYL`a zZuty1@l%~{3J5Stq)vSGRDNdxN4yBo`pflTD1IVw>rnFjz>s(qAHpWL!9NqcsK;rd z{EG`j zd3DE=a8+9RwCCn_R@mN;sSy6^_V2Zyg4UBVE15T^zt(@PRzUrkTE&YkmauEl86A7l zJYubifA{?TCIa^)dSH>>VcGRvTYRl)sZZrt)f0+|3A}$551U54#RarTzphbnCA$-; zUHwH@@54*^_no_cHVAxOsKm3UI3+|=l>a`nXTZ*)6aP+z$o8Pu^oX-^Kjo3QA1k8- z0X6puL6Kw&t%PLFwTa}Wybcpw{n(SP!cnQ2G;E1}^UD$HHGyvx*`raT^L^ch-<%nT zEQcrSrR5PbtzAK^WkX?ahcr*ZHF_h-n(3U3sddOt!A;5r4`i4%yB?ONX+PT!vfg7Y zK98<$qkdhK4O4-pl)u;=6jrnyJ!RLxFeNvZ`9g2hIR*=nh^Q&mu!9ARfoRx`VWPqW$F zst-1EzA6r*Vr6ow!65#j`fD41R)7~~Vl~;?8?qW*Xc6>iT^nX@JSI~Xa-qR(&F{-8 zCwuB%i&g5Ys*fw`ThhWo{Z$8@dP|@~&u&P+9m9>QK%Y}HuhFq(;Tg-NKeLzY^1|!C zOlj!EIB0xW`BcyRa+n%}ExlN|s_W8Vb(g7=9Fwe9R-MJe$N##(e-)#zDOUCE<&PgQ z@h7QI>C247Hm4Z0BOSw&C~w>t5srjVYr;~h;C|E7JWD`*Q@=SL9cqVe0Wj0Uw`~5te~)kNTZTQ z!jy7Q$hH(SNj7_% z7}UsIOOi^&!R1^+XmETR|8(0jS$aM4CHa#`MQmIljis>*+7(R^O}0CwJvsW3Z~QN- z^Z?j}q)O|*;7gh6)>`&gj&MKbE4s%G=bvKGG~uLChU&QBHCfn>Z zdx5eW_odww#pu|p&`E@cLGn=kUHbfU>qs&|MdCqk635{&rlPJ%Ox2Vcm+*;E_UUHv zkIP~QQF;3VgX9T#l(MvG27TD~2GSfbEXQdz!v8>MweLp@>f(-@`ESdcquIM@9mj*D z5>BPV5R?u#0a5m$A`9VR)X1f_4t`ntxnkEA&iX8Z3N8Qo_ISTLsfA$woU;J)f~48|v2FIqFNWvAw4YM} z_LamFI%iVJUv~%6VG-2@#kcwfG(#uTW6DCcR8G9!kun<645N?rS25>fmlY6mi_Lj8 zT^}vNlHcZPw5S@#I@c=l154d}(mtGWRlGwQCf2qWaif2y4zXG@XCkjWnw?sItGq|V+l23~r7dO;iR*D(tG!n|6^>Qnl zgn9ldHV@t4w+q2&mcP+O-3>$m zTU8o`I@R=3OUQU-h8=Ba$UA?g$}arrlNMhekAPMRL&WABU3}JO6+-e-zHRNlkdq3{ zZ`dKHe>*n#P2L-lW`q*BU$3Dvz}>&@jR&k4E>oK`drI5KQq=bZl>XC}VDi&_YG{ zv$)mH-}ZWH1cG*h?rViD=GZifk<<;ZHP=iw`RM2NKMW)V^jdPaOnB&(f8h!B9nU50R_@co zEpNC@2;^zSB})RXDYL}%5Wmu!D_W+S3ErEnwIUa(K7Kaa9Fm{nSzdfU{gywC;5ee$ zWpBlIu9OJbetS}yQnRZ)FatspoTl5D3cE%5KiPe$$V8Kaj)+wZJ7oLyvBCXVB|r^c zVb|`&m;MKf7b!Enz}%b(L)h^9_9#V$U*i3Y&>QmP#r*imtCXKz@s~zGKTT0HC8|Y| zYQA+*LmB&J4Vo&Ji1z~!MXi;-=K*Vt2XGu3xMj-5)HbVp7(yd3UeA)8EcO^W~j=HWfKPnRA@^?2R*f?AqbVM~hCH zYSs@4VUxqv?}Mx}mj^WF`gDB{UC%8J=q+mlHTE&Q8bl13*O~eqD-qt0$lbN;(wTU< z%5c0*olI#U-9N7k%il(q9Dj3sPM1^uBBfvtve|bJ({@{eeEAe_mM%el3Gn0qwTtjT z(Xi(Rtj$V%Zq}}WbP*HFj@(MT=2oT>fHzu<*u>^D7>VFuuU0eFj_SiADDwd2G+P7V z1{z&)+eytwGO}3QPl6We+Q_=`N2oUduFaXgD)kfXKo-az z=Uzwlgx?TKgRjDZR<+;M2&}Jgpuz#+%#DNEnet`iXKJ zuL@Bnw&_tLff|bh_QjyT^T(69%ThGqY$*ZBurYR!XHMG8&47U^9&P#$ZRx}zZ@2-s z^Q3RH?^UGQC}!=+%lz5|O*ij8^jROU$B-gfR_GtD-|hpUhwcR*E=?@3C^Ns3dE9RQ zm2-;Ympk;r3C!bp(9X-~A;x?%RCKa77|wD}eLMa=QeCQIzC1RStC8`9i{DszVzKuXLp`TZ5>L1HcsZ^MOV2pf*{Yo@Ek4EGR}{GGT|2vOi@oKY4s?A+gQ*Oe zrmEhlH7RsFgfAGS!WT_FoKBYJP( z@p7LP7UtUmOq*?WAG6~yI&KA!!2PGJtFPnhiweIM*DF1GK`bQ({oK^L1pW3gp{P4# z#_d`22)A zlI}kk|Lwi5@7+~ut?JB$bwEvK%s&8tkHrD?Anu8Z%bV3)jpl?K69C&h4%=o})pEdz zP4Bkgx+C8Vk~|RpZAbw3vMp{g^91mw>MUzp{I|#>H^0UFYmV|z>Y*3Q=IKJgG$)Sm zh|=H#QEk3**~-J!vnZib*83;7M~28%vr0!Yn<3^ zkqmy2ruK?UyO8%bhZ}!mJ!1UhRb*43JizuWXXd}H1zm&hZnSD|G_sFGJmT=8CMmyd zY&XQqv^gA*zgT$!@TI>O7mz3xIc2n~_>KJshp!tfA667Y2qb4f9{hu!xExUF75u?4>+=O%E(1xPwJxYfc9?{DUaEzD>M{q)p<$OO}*F#p(TZkOC5FqhFaNr6IW! z<<+iqsGMK$3{xX~=yHF`K!vrX9I`e7oyWg}YVIx=2V@6~5b0e9@ zl6Qa({<<{FGb!z1y8^zs4vx53kp&i+Ml>$3T;{tA6TO01e(wc6_!(P1Es$ym4;UOE z=0$iczdkR<+c)N~NfTA2zPg%_b^Fh*x^1VW>}6AkbKhP_I&8}RvFMAXkWH1)y*Cee zHqBY&t zkf)N7%klk0AA4;!#Tri^J?18fLA|{`{q*&grcq$D=X6Jj=3R&ntjXK zFws9{GQb2f`*6N;lzlPze6$!agEG6*>^8{m)^{_mR(%y&{A_Fw32J%1mX)Y%(3?Q% zU#@nd4ps9JP}FhfpuUCFYjIR0NKbQ zK-4+jyF4QViw5O8CaE480X5y`W2%bXh?Xc%_(r@Yo4z>OaS1xNc+TWb;QE}z#+dA> z=ISu$_jNT#h>uUB%0~FlV_4Jb!+l8^q1k`~0$$E=W}}#s?G<$PEJ-Q(dwq>FAuzqH z^MMr(XO5G4r{YpymYczm+`U`b4Dp*oi_@%72vtWgNG@#Y3}Onv@14?;h-* z)ef3w&(lVxWHu29?r`M>~q=;Pm zbUu0)A6EM;!cwh=Z}PgZ$V7uy#RxhXb?N7( z^73TBA9ji;zv(nu9yD@Wr!2mlDl6ke9d}TysX9>~leg6tm#|687r??8AKSEnFJ`&2 zRa3F;-{oW+cYYc}bL&g56rsPnL5Y)})h8mYDcXsm#=q!^EhCPYAUwLs$iCB*9BjR2#p!Z)#&vY2a8UvLs&KE^r@k}+2CoI*P^ zzPx_Zb$t$g>0PXKE#3-e7jJxJ1mQ*0JEOPMC3V9I^u{6ZBw)|bk>p)k)kRI{q^=4TM^%W^61W%z2vrB4x#IIGvwFTYd0{YsiKbNr+sZie@e; znzlP-t!VuEK+aRD?55aGg;~h*O`|QnDt4Qr`{HpFk$2);7u>m4xw|)~q|WMf@e)fV z^eZF^ug7u#cwOE|^*e@i`O%*pH%}Dy#m2hweLL@EU0ZefwH{I-*Um1=-Q0a`q4Kg} zx}sg~>DO>=-Qz4!;yAor_qc0}jz5Dz2EjI8$~!5!>G%o$8NRL(`B+!}+V+B;ew8)$ zz!ff}6i(atbfR#+IX^7i!?^)pXI1l}w{Ypa*%{ZsRZK;g0@BQ7HKCE$seN}y7%Sa5 z5XoaKI$IodwOPnG!d~T`e?Q&$_sPMV(=78ryVYD|@ad>a2#?NE&$S%;<_LX1!8bPC z$0;!OcJiz$2_{C)X;bMF51$Vi%9@p>Xg;plxLX;0e1o`ln|&}-ztp`!Km8*NaN$kc)6w-=|x{VP*-BhVWCur~9S@4EdI1Tsqw8(6&mx(fv5+@Ve zT%7}?{9-r~gpR#80h@|Wr4%9VFO9JySbL_2x*2#Fc9X|Md-IJhqZwZXIT)}*GcYyz z2gDC;DCSH6t>QT{=$xcA-I7-q4JhXA#R?WWA<8JgRgSV#a`Mqrh5 znP}e{8#Li<>df1rqBJ1vRj5~6v)(ra`GLLxj@)EsY^DX}0nR-q-Rz&{1@th-tOQ}% zK+5|A^C%9DkEHS}KZ+kXKZny)+i0wPh8Gvk4d`K8U9ahUx6mH6vogC3LDba4%fG&G z^Uzm1@oePC?0AnQQ-8^2$?VLVRgu+v^`oO>V?f8^N~~mmNScM7&c~mIxAH)IUFq_n zt6c(YHAG}zr#b(=W98?MQJ36TcgwX!;rZ;Mk|6>n05{oKi@vvx_z93Nt{|_}(;sOt z%N7zHmB8II0m5>&t=<$&uD;iDdjz>vjd161o!4}_ZECsqWali4C(nPmmqGDa`|Qy1 z0Wx&JoC23{c*2$`Lf*F8A#3LZINZKZV(B1_vY?^Uf=6{e-lB@3Gy>M$@ZNl54QpzJ zKAm3Ue~PAD6}|fAjfVq`Iel^?54mul632#MQ_KjO#j0n&Mx%==%XCgD`&YJsJ@#5L z5kydDpU-omSrtN?`_`WTZuu)Rvhs1SFAWUzd3T8A-mD^hVJ2K`B7D;MDCdZJF;r!$ z#X3A3l)MX`f#@o*QGW57iks3AxAWaB5CJ3}Wl0qAyP9JgJ6{~>6uSP=6&cMx)-X-G zsJgxQk5;D_lsS;)LF14deW0Z{J6*`OqpzYeB&)*0i@?B5F!l`#^wRQmcLT^&jJSSD>&(%~#`0yzgY~?NLf>Wcb~ePNYy0&S8Lf#st;q+>wSy_A zq82$xuA}*&yQ=e*E=8RykKmbhBd$Q0r|bYIUGlh&g~fiR8MLtQb>WLMZ>Bb;EtNY0 z3eD3^oX_aZsQOe?yKQH73S^ASYMtngjb=KD`2N*A9xW$0q5Vxu&Ev{mAWeng;BN zQQ#(CMeB57y?l0Xgr|YE_Y_?I)N4KLrbAUlKw-}-mb&wq4g2lLpu}6A@&n?OXhq|d zjCZlZcOz)`hL$v_QM3F_7YJ06y>3VC7~2g|eU9LyfM$Mv4><~6*bI!%dM}N*bS##N;BU)3r4G&4SpFFut1{*WD?i;)Ez!0XdT#cTbI*3n2lAO~ z&20zcUSio4i~?diH^fRR>Sm2wex79_ckaq4(%S06pK5ZB^^|1GIKwmr$4{p_;CXFe zc>LMo?B70DuUV^yYRp?Sk1%)hJqR^0km4r^dQEjQ-{9a)SB`wlBxUZ>BQx zoh+#psQa2~J~HL~&gX-3wn`KehIoq#5dt{nM*4}_>7k-&_u)v`eWP5Iyt(fo41I4F z-ej=&P2onRoT8$sRf;zOzSh=UrcLYEvy{@Y#WGFRT|?u?S}an8HslgHMJyRZ?N+-= zIe8hm!>#!7z4g|0^ibeV{!XkAkdBnz6ury(_8y!ls#@Sy*7~8Zkdq1+gXcfF5fl^u z5T|9`Rw^?CI?GI5708@qG?rPRXdMA@{QGO~Dq|`uXhdf13;nRLFwWggkyFICP}?%m zz@5d>`s)MYxBMcoo>oJmb?ki;P+PsrFI^}?c_Za&58rx{LmgzIHC?imOxftY2~K2o z2YXK6v@x@_c)Gv(Soo;Um8;TfRo&|j=!MF&dgrC0qoXY?6ds|6`e8d|DkHnEu6Aof zNdvDD0*43qy|Dz*8MS8b&W;;2oVt&Z(ts!(fflG;9ULFPC8$%rg@7VlI>7>L)?4|W z{Fhsk-*LNtXg0%a=$j${N#yXAYUfihJ8hTrrGNczs3Sz2O=CvxY(` zdJY$Y&N@o{sUuwY$AyQC*rS{m1?>EOFTiEogG^EWyC0(-_3M_W6zkc1KUM#bR2Zj6 z=*95IqR_-0_9n!nmF>o8XKx3RH9`u^cbkO+T~HA1M1>-T@<)9DcN>hkmp6eXt9un( zhqZk6!<+9)3n2cs$(=$`Q!SCKyIxBR1E2Y76>*PcQVmOFuif2VP7)E<`do57-N>hm zpF9~F+S482BzkB0pRPp8!71m7@=}q7=4#+ePogx^EgseU1QfUs!JfclcJN7Q0S0lh z2DcULA0u8GmOQDr{rCLUI9K+U@Uancb8Q%yk<~!3nfda! z6UOJLeO_=fXVU^ExLK^mgIt%vRL51drHWMatj#Tp*fq^%IqkrC+=g_b4+PqUVRu(~ zUK?}>By902hMkY{g&ZQH%dBf~%!N-rBr9J|C&p_jYg!@yudo$3LV#>j_iKiLqPsJP z=x$;%kU_uI!6K6te9-1S>19yu%3&lULc+m{=5{F-aGZYMETxJraM2&PWwzj(IXZdU z#XCq55ADMeP>YS9B+3Ds*H(;24%?PakAE=rXj`3OlwGVR7JX%+fdj_T(hc# z7mo3Z2f3EtPHa8eE=V+U3*6PP#=ANV5!wl$L#D`sCpqj{Q_e$rYdfgOs|B8WKwv)cI~=Zg&N<#@Bv972_r@u! z6}xK{3tX?-UmW{gzdLc{KVhp61~a;}Igcd+R)VZJrAffMOaI>fTLS+VOJGaj-9XG+ V5w89}e;b;I`X+j?zy5yxKLC;9Ozi*w literal 0 HcmV?d00001 diff --git a/docs/redlock4j-social-preview.svg b/docs/redlock4j-social-preview.svg new file mode 100644 index 0000000..508287c --- /dev/null +++ b/docs/redlock4j-social-preview.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + redlock4j + + + DISTRIBUTED LOCKS FOR REDIS + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/org/codarama/redlock4j/driver/JedisRedisDriver.java b/src/main/java/org/codarama/redlock4j/driver/JedisRedisDriver.java index bceb8a2..dcfc7b2 100644 --- a/src/main/java/org/codarama/redlock4j/driver/JedisRedisDriver.java +++ b/src/main/java/org/codarama/redlock4j/driver/JedisRedisDriver.java @@ -14,7 +14,6 @@ import redis.clients.jedis.JedisPubSub; import redis.clients.jedis.Protocol; import redis.clients.jedis.RedisClient; -import redis.clients.jedis.UnifiedJedis; import redis.clients.jedis.exceptions.JedisException; import redis.clients.jedis.params.SetParams; import redis.clients.jedis.util.CompareCondition; @@ -25,8 +24,6 @@ import java.util.Map; import static java.util.Arrays.asList; -import static redis.clients.jedis.Protocol.Command.CONFIG; -import static redis.clients.jedis.Protocol.Command.HELLO; import static redis.clients.jedis.util.CompareCondition.valueEq; /** @@ -104,7 +101,7 @@ public JedisRedisDriver(RedisNodeConfiguration config) { // Detect CAS/CAD support once at initialization this.cadStrategy = detectCADStrategy(); - logger.info("Using {} strategy for CAS/CAD operations on {}", cadStrategy, identifier); + logger.debug("Using {} strategy for CAS/CAD operations on {}", cadStrategy, identifier); } /** diff --git a/src/main/java/org/codarama/redlock4j/strategy/KeyspaceWaitStrategy.java b/src/main/java/org/codarama/redlock4j/strategy/KeyspaceWaitStrategy.java index 088d4a4..c644063 100644 --- a/src/main/java/org/codarama/redlock4j/strategy/KeyspaceWaitStrategy.java +++ b/src/main/java/org/codarama/redlock4j/strategy/KeyspaceWaitStrategy.java @@ -83,7 +83,7 @@ public void initialize(List drivers, Duration retryDelay) { // Start background subscription thread for keyspace events startKeyspaceSubscription(); - logger.info("Keyspace wait strategy initialized with {} Redis nodes", drivers.size()); + logger.debug("Keyspace wait strategy initialized with {} Redis nodes", drivers.size()); } /** @@ -160,7 +160,7 @@ private void configureKeyspaceNotifications() { if (!hasAllFlags(currentConfig, REQUIRED_FLAGS)) { driver.configSet("notify-keyspace-events", mergedConfig); - logger.info("Configured keyspace notifications on {}: {} -> {}", driver.getIdentifier(), + logger.debug("Configured keyspace notifications on {}: {} -> {}", driver.getIdentifier(), currentConfig, mergedConfig); } else { logger.debug("Keyspace notifications already configured on {}: {}", driver.getIdentifier(), diff --git a/src/main/java/org/codarama/redlock4j/strategy/LockExecutionStrategyFactory.java b/src/main/java/org/codarama/redlock4j/strategy/LockExecutionStrategyFactory.java index 65cd81d..5b6354c 100644 --- a/src/main/java/org/codarama/redlock4j/strategy/LockExecutionStrategyFactory.java +++ b/src/main/java/org/codarama/redlock4j/strategy/LockExecutionStrategyFactory.java @@ -51,11 +51,11 @@ public static LockExecutionStrategy create(List drivers, RedlockCon } if (drivers.size() == 1) { - logger.info("Using SingleNodeStrategy - optimized for single Redis instance"); + logger.debug("Using SingleNodeStrategy - optimized for single Redis instance"); return new SingleNodeStrategy(drivers.get(0)); } - logger.info("Using MultiNodeStrategy with {} nodes, quorum={}", drivers.size(), config.getQuorum()); + logger.debug("Using MultiNodeStrategy with {} nodes, quorum={}", drivers.size(), config.getQuorum()); return new MultiNodeStrategy(drivers, config); } } diff --git a/src/main/java/org/codarama/redlock4j/strategy/PollingWaitStrategy.java b/src/main/java/org/codarama/redlock4j/strategy/PollingWaitStrategy.java index a0d16ec..52d0d2a 100644 --- a/src/main/java/org/codarama/redlock4j/strategy/PollingWaitStrategy.java +++ b/src/main/java/org/codarama/redlock4j/strategy/PollingWaitStrategy.java @@ -55,7 +55,7 @@ public void initialize(List drivers, Duration retryDelay, Duration this.maxRetryDelay = maxRetryDelay != null ? maxRetryDelay : retryDelay; this.retryDelayMultiplier = retryDelayMultiplier; this.retryDelayJitterRatio = retryDelayJitterRatio; - logger.info("Polling wait strategy initialized with retry delay {}, max {} , multiplier {}, jitter ratio {}", + logger.debug("Polling wait strategy initialized with retry delay {}, max {} , multiplier {}, jitter ratio {}", retryDelay, this.maxRetryDelay, retryDelayMultiplier, retryDelayJitterRatio); }