From 3ddbac2a97cc5d471d941b9cac4f9dd9e5f3b865 Mon Sep 17 00:00:00 2001
From: Tihomir Mateev
Date: Fri, 27 Feb 2026 16:11:21 +0200
Subject: [PATCH 1/8] Code cleanup, CI/CD improvements, licensing and
documentation
---
.github/workflows/nightly.yml | 2 +-
.idea/AugmentWebviewStateStore.xml | 2 +-
CONTRIBUTING.md | 69 +-
README.md | 97 +--
docs/comparison.md | 10 +
.../countdownlatch-implementation.md | 663 ++++++++++++++
docs/comparison/fairlock-implementation.md | 564 ++++++++++++
docs/comparison/multilock-implementation.md | 813 ++++++++++++++++++
.../readwritelock-implementation.md | 712 +++++++++++++++
docs/comparison/semaphore-implementation.md | 806 +++++++++++++++++
pom.xml | 21 +
.../java/org/codarama/redlock4j/FairLock.java | 124 ++-
.../org/codarama/redlock4j/LockResult.java | 43 +-
.../org/codarama/redlock4j/MultiLock.java | 164 ++--
.../java/org/codarama/redlock4j/Redlock.java | 96 +--
.../redlock4j/RedlockCountDownLatch.java | 229 ++---
.../codarama/redlock4j/RedlockException.java | 27 +-
.../codarama/redlock4j/RedlockManager.java | 165 ++--
.../redlock4j/RedlockReadWriteLock.java | 160 ++--
.../codarama/redlock4j/RedlockSemaphore.java | 198 ++---
.../redlock4j/async/AsyncRedlock.java | 83 +-
.../redlock4j/async/AsyncRedlockImpl.java | 170 ++--
.../codarama/redlock4j/async/RxRedlock.java | 110 +--
.../configuration/RedisNodeConfiguration.java | 30 +-
.../configuration/RedlockConfiguration.java | 32 +-
.../redlock4j/driver/JedisRedisDriver.java | 128 +--
.../redlock4j/driver/LettuceRedisDriver.java | 156 ++--
.../redlock4j/driver/RedisDriver.java | 239 ++---
.../driver/RedisDriverException.java | 27 +-
.../codarama/redlock4j/LockExtensionTest.java | 141 ++-
.../redlock4j/RedlockManagerTest.java | 107 +--
.../org/codarama/redlock4j/RedlockTest.java | 182 ++--
.../redlock4j/ReentrantAsyncRedlockTest.java | 160 ++--
.../codarama/redlock4j/ReentrantLockDemo.java | 142 ++-
.../redlock4j/ReentrantRedlockTest.java | 134 ++-
.../async/AsyncLockExtensionTest.java | 121 +--
.../redlock4j/async/AsyncRedlockImplTest.java | 150 ++--
.../RedlockConfigurationTest.java | 147 +---
.../driver/JedisRedisDriverTest.java | 82 +-
.../driver/LettuceRedisDriverTest.java | 95 +-
.../examples/AdvancedLockingExample.java | 134 ++-
.../examples/AsyncRxUsageExample.java | 317 +++----
.../examples/RedlockUsageExample.java | 62 +-
.../AdvancedLockingIntegrationTest.java | 53 +-
.../AsyncRedlockIntegrationTest.java | 119 ++-
.../integration/RedlockIntegrationTest.java | 101 +--
.../performance/RedlockPerformanceTest.java | 123 ++-
47 files changed, 5462 insertions(+), 2848 deletions(-)
create mode 100644 docs/comparison/countdownlatch-implementation.md
create mode 100644 docs/comparison/fairlock-implementation.md
create mode 100644 docs/comparison/multilock-implementation.md
create mode 100644 docs/comparison/readwritelock-implementation.md
create mode 100644 docs/comparison/semaphore-implementation.md
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index 81756c1..bd43935 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -146,7 +146,7 @@ jobs:
run: |
# Update the Redis image version in test files
sed -i 's/redis:7-alpine/redis:${{ matrix.redis-version }}/g' src/test/java/org/codarama/redlock4j/integration/RedlockIntegrationTest.java
- sed -i 's/redis:7-alpine/redis:${{ matrix.redis-version }}/g' src/test/java/org/codarama/redlock4j/pderformance/RedlockPerformanceTest.java
+ sed -i 's/redis:7-alpine/redis:${{ matrix.redis-version }}/g' src/test/java/org/codarama/redlock4j/performance/RedlockPerformanceTest.java
- name: Run integration tests with Redis ${{ matrix.redis-version }}
run: mvn test -Dtest=RedlockIntegrationTest
diff --git a/.idea/AugmentWebviewStateStore.xml b/.idea/AugmentWebviewStateStore.xml
index b542716..1f9e321 100644
--- a/.idea/AugmentWebviewStateStore.xml
+++ b/.idea/AugmentWebviewStateStore.xml
@@ -3,7 +3,7 @@
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9fc9fd4..d98005e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -33,52 +33,35 @@ mvn test jacoco:report
mvn org.owasp:dependency-check-maven:check
```
-### Automated Testing
-Our CI/CD pipeline automatically runs:
-
-- ✅ **Compilation** on multiple platforms (Ubuntu, Windows, macOS)
-- ✅ **Unit tests** with Java 8, 11, 17, and 21
-- ✅ **Integration tests** with Testcontainers (Redis 6, 7, latest)
-- ✅ **Code coverage** analysis with JaCoCo
-- ✅ **Security scanning** with OWASP dependency check
-- ✅ **License header** validation
-- ✅ **Code style** checks
-
## 📝 Code Guidelines
### Code Style
-- Use **4 spaces** for indentation (no tabs)
-- Follow **Java naming conventions**
-- Add **Javadoc** for public APIs
-- Keep **line length** under 120 characters
-- Remove **trailing whitespace**
+We use automated code formatting to ensure consistency across the codebase:
+
+- **Formatter**: Eclipse formatter with custom configuration (`formatting.xml`)
+- **Indentation**: 4 spaces (no tabs)
+- **Line length**: 120 characters maximum
+- **Braces**: End-of-line style
+- **Javadoc**: Required for all public APIs
+
+#### Formatting Commands
+```bash
+# Format all code
+mvn formatter:format
+
+# Validate formatting (runs automatically during build)
+mvn formatter:validate
+```
+
+**Note**: The build will fail if code is not properly formatted. Always run `mvn formatter:format` before committing.
### License Headers
-All Java files must include the MIT license header:
+All Java files must include an [SPDX-compliant](https://spdx.dev/learn/handling-license-info/) license header:
```java
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
```
@@ -122,12 +105,6 @@ When you submit a PR, our automation will:
- **Docker** (for integration tests)
- **Git**
-### IDE Setup
-We recommend:
-- **IntelliJ IDEA** or **Eclipse**
-- **Checkstyle** plugin for code style
-- **SonarLint** for code quality
-
### Environment Variables
For local development:
```bash
@@ -188,11 +165,5 @@ We welcome contributions in:
- **GitHub Discussions** - For questions and ideas
- **Code Review** - Learn from PR feedback
-## 🏆 Recognition
-
-Contributors are recognized in:
-- **GitHub contributors** list
-- **Release notes** for significant contributions
-- **Documentation** acknowledgments
Thank you for contributing to Redlock4j! 🚀
diff --git a/README.md b/README.md
index 142ce4e..0676363 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
> [!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.
-A Java implementation of the [Redlock distributed locking algorithm](https://redis.io/docs/latest/develop/use/patterns/distributed-locks/) that implements the standard Java locking interfaces.
+A simple and lightweight Java implementation of the [Redlock distributed locking algorithm](https://redis.io/docs/latest/develop/use/patterns/distributed-locks/) that implements the standard Java locking interfaces.
## Features
@@ -19,7 +19,7 @@ A Java implementation of the [Redlock distributed locking algorithm](https://red
- **Advanced Locking Primitives**: Fair locks, multi-locks, read-write locks, semaphores, and countdown latches
- **Lock Extension**: Extend lock validity time without releasing and re-acquiring
- **Atomic CAS/CAD Detection**: Auto-detects and uses native [Redis 8.4+ CAS/CAD commands](https://redis.io/docs/latest/operate/oss_and_stack/stack-with-enterprise/release-notes/redisce/redisos-8.4-release-notes/) when available
-- **Java 8+** - Compatible with Java 8 and higher
+- **Java 8+** - Compatible with Java 8 and higher, tested against Java 8, 11, 17, and 21
## Requirements
@@ -30,33 +30,6 @@ A Java implementation of the [Redlock distributed locking algorithm](https://red
Visit the complete Redlock4j documentation at [redlock4j.codarama.org](https://redlock4j.codarama.org).
-## Dependencies
-
-Add the following dependencies to your `pom.xml`:
-
-```xml
-
-
- redis.clients
- jedis
- 5.1.0
-
-
-
-
- io.lettuce
- lettuce-core
- 6.7.1.RELEASE
-
-
-
-
- org.slf4j
- slf4j-api
- 2.0.9
-
-```
-
## Quick Start
### 1. Add Dependencies
@@ -85,6 +58,25 @@ Add this library and your preferred Redis client to your `pom.xml`:
```
+Optionally you can also add SLF4J for logging or RxJava to use the reactive APIs:
+
+```xml
+
+
+ org.slf4j
+ slf4j-api
+ 2.0.9
+
+
+
+
+ io.reactivex.rxjava3
+ rxjava
+ 3.1.8
+
+
+```
+
### 2. Configure Redis Nodes
```java
@@ -92,9 +84,9 @@ 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)
+ .defaultLockTimeout(Duration.ofSeconds(30)) // if not set would use a default of 30 seconds
+ .retryDelay(Duration.ofMillis(500)) // if not set would use a default of 200ms
+ .maxRetryAttempts(5) // if not set would use a default of 3 retries
.build();
```
@@ -514,47 +506,6 @@ sequenceDiagram
5. **Consider lock validity time** for long-running operations
6. **Use unique lock keys** to avoid conflicts between different resources
-## CI/CD and Testing
-
-This project uses GitHub Actions for continuous integration and comprehensive testing:
-
-### Automated Testing
-- **Pull Request Validation**: Every PR is automatically tested with compilation, unit tests, and integration tests
-- **Nightly Comprehensive Tests**: Full test suite including performance tests runs every night
-- **Multi-Platform Testing**: Tests run on Ubuntu, Windows, and macOS
-- **Multi-Java Version**: Tested against Java 8, 11, 17, and 21
-- **Multi-Redis Version**: Tested against Redis 6, 7, and latest versions
-
-### Security and Quality
-- **Dependency Security Scanning**: OWASP dependency check for known vulnerabilities
-- **Code Coverage**: JaCoCo integration for test coverage reporting
-- **License Compliance**: Automated verification of license headers
-- **Code Style**: Basic formatting and style checks
-
-### Workflows
-- **CI Workflow** (`.github/workflows/ci.yml`): Runs on every push and PR
-- **Nightly Workflow** (`.github/workflows/nightly.yml`): Comprehensive testing every night
-- **PR Validation** (`.github/workflows/pr-validation.yml`): Detailed PR validation with comments
-- **Release Workflow** (`.github/workflows/release.yml`): Automated Maven Central publishing
-
-### Running Tests Locally
-```bash
-# Run all tests
-mvn test
-
-# Run only unit tests
-mvn test -Dtest=RedlockConfigurationTest
-
-# Run only integration tests
-mvn test -Dtest=RedlockIntegrationTest
-
-# Run with coverage
-mvn test jacoco:report
-
-# Security scan
-mvn org.owasp:dependency-check-maven:check
-```
-
## Releases and Maven Central
Redlock4j is automatically published to Maven Central when new GitHub releases are created.
diff --git a/docs/comparison.md b/docs/comparison.md
index da2b515..a017a07 100644
--- a/docs/comparison.md
+++ b/docs/comparison.md
@@ -193,6 +193,16 @@ RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
lock.lock();
```
+## Detailed Comparisons
+
+For in-depth technical comparisons of specific features:
+
+- **[FairLock Implementation](comparison/fairlock-implementation.md)** - Detailed comparison of FairLock implementations between redlock4j and Redisson, including data structures, algorithms, and trade-offs
+- **[MultiLock Implementation](comparison/multilock-implementation.md)** - Comprehensive comparison of MultiLock implementations, covering deadlock prevention, acquisition strategies, and use cases
+- **[Semaphore Implementation](comparison/semaphore-implementation.md)** - In-depth comparison of distributed semaphore implementations, analyzing permit management, performance, and consistency models
+- **[ReadWriteLock Implementation](comparison/readwritelock-implementation.md)** - Detailed comparison of read-write lock implementations, covering reader/writer coordination, lock upgrade/downgrade, and performance characteristics
+- **[CountDownLatch Implementation](comparison/countdownlatch-implementation.md)** - Comprehensive comparison of countdown latch implementations, analyzing counting mechanisms, notification strategies, and consistency guarantees
+
## Conclusion
**Redlock4j** is ideal for applications that:
diff --git a/docs/comparison/countdownlatch-implementation.md b/docs/comparison/countdownlatch-implementation.md
new file mode 100644
index 0000000..bea6633
--- /dev/null
+++ b/docs/comparison/countdownlatch-implementation.md
@@ -0,0 +1,663 @@
+# CountDownLatch Implementation Comparison: redlock4j vs Redisson
+
+This document provides a detailed technical comparison of the CountDownLatch implementations in redlock4j and Redisson.
+
+## Overview
+
+Both libraries implement distributed countdown latches for coordinating multiple threads/processes, but they use different approaches for counting and notification.
+
+## Purpose & Use Case
+
+### redlock4j RedlockCountDownLatch
+
+**Purpose**: Distributed countdown latch with quorum-based consistency
+
+**Use Case**: Coordinating distributed processes with strong consistency requirements
+
+```java
+// Create latch waiting for 3 operations
+RedlockCountDownLatch latch = new RedlockCountDownLatch(
+ "startup", 3, redisDrivers, config
+);
+
+// Worker threads
+new Thread(() -> {
+ initializeService1();
+ latch.countDown();
+}).start();
+
+new Thread(() -> {
+ initializeService2();
+ latch.countDown();
+}).start();
+
+new Thread(() -> {
+ initializeService3();
+ latch.countDown();
+}).start();
+
+// Main thread waits
+latch.await(); // Blocks until count reaches 0
+System.out.println("All services initialized!");
+```
+
+### Redisson RedissonCountDownLatch
+
+**Purpose**: Distributed countdown latch with pub/sub notifications
+
+**Use Case**: High-performance coordination with single Redis instance
+
+```java
+RCountDownLatch latch = redisson.getCountDownLatch("startup");
+latch.trySetCount(3);
+
+// Worker threads
+new Thread(() -> {
+ initializeService1();
+ latch.countDown();
+}).start();
+
+// ... more workers
+
+// Main thread waits
+latch.await();
+System.out.println("All services initialized!");
+```
+
+## Architecture & Data Model
+
+### redlock4j
+
+**Design**: Counter with quorum-based reads/writes and pub/sub
+
+**Data Structure**:
+```
+{latchKey} = {count} (counter on each node)
+{latchKey}:channel = (pub/sub channel) (notification channel)
+```
+
+**Key Characteristics**:
+- Counter replicated across all nodes
+- Quorum-based count reads
+- Quorum-based decrements
+- Pub/sub for zero notification
+- Local latch for waiting
+- Automatic expiration (10x lock timeout)
+
+**Architecture**:
+```
+RedlockCountDownLatch
+ ├─ latchKey (counter key)
+ ├─ channelKey (pub/sub)
+ ├─ List (quorum-based)
+ ├─ CountDownLatch localLatch (for waiting)
+ ├─ AtomicBoolean subscribed
+ └─ Quorum-based DECR
+```
+
+### Redisson
+
+**Design**: Single counter with pub/sub and Lua scripts
+
+**Data Structure**:
+```
+{latchKey} = {count} (single counter)
+redisson_countdownlatch__channel__{latchKey} = (pub/sub channel)
+```
+
+**Key Characteristics**:
+- Single counter (not replicated)
+- Atomic Lua scripts
+- Pub/sub for notifications
+- Async/reactive support
+- No automatic expiration
+
+**Architecture**:
+```
+RedissonCountDownLatch
+ ├─ latchKey (counter key)
+ ├─ channelName (pub/sub)
+ ├─ CountDownLatchPubSub (notification handler)
+ ├─ Lua scripts for atomicity
+ └─ Async futures
+```
+
+## Initialization
+
+### redlock4j
+
+**Initialization**: Automatic in constructor
+
+```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,
+
+
+## Count Down Operation
+
+### redlock4j
+
+**Algorithm**: Quorum-based DECR with notification
+
+```java
+public void countDown() {
+ int successfulNodes = 0;
+ long newCount = -1;
+
+ // Decrement on all nodes
+ for (RedisDriver driver : redisDrivers) {
+ try {
+ long count = driver.decr(latchKey);
+ newCount = count;
+ successfulNodes++;
+ } catch (Exception e) {
+ logger.debug("Failed to decrement on {}", driver.getIdentifier());
+ }
+ }
+
+ // Check quorum
+ if (successfulNodes >= config.getQuorum()) {
+ // If reached zero, publish notification
+ if (newCount <= 0) {
+ publishZeroNotification();
+ }
+ } else {
+ logger.warn("Failed to decrement on quorum");
+ }
+}
+
+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());
+ }
+ }
+}
+```
+
+**Characteristics**:
+- DECR on all nodes
+- Quorum check for success
+- Publish to all nodes when zero
+- No atomicity between decrement and publish
+
+**Redis Operations** (M nodes):
+- M × `DECR`
+- M × `PUBLISH` (if zero)
+
+### Redisson
+
+**Algorithm**: Atomic Lua script with notification
+
+```lua
+-- countDownAsync
+local v = redis.call('decr', KEYS[1]);
+if v <= 0 then
+ redis.call('del', KEYS[1])
+end;
+if v == 0 then
+ redis.call(ARGV[2], KEYS[2], ARGV[1])
+end;
+```
+
+**Characteristics**:
+- Atomic decrement + delete + publish
+- Single operation
+- Deletes key when zero
+- Publishes ZERO_COUNT_MESSAGE
+
+**Redis Operations**:
+- 1 Lua script execution
+
+## Await Operation
+
+### redlock4j
+
+**Algorithm**: Subscribe + poll with local latch
+
+```java
+public boolean await(long timeout, TimeUnit unit) throws InterruptedException {
+ // Subscribe to notifications
+ subscribeToNotifications();
+
+ // Check if already zero
+ long currentCount = getCount();
+ if (currentCount <= 0) {
+ return true;
+ }
+
+ // Wait on local latch (released by pub/sub notification)
+ boolean completed = localLatch.await(timeout, unit);
+
+ return completed;
+}
+
+private void subscribeToNotifications() {
+ if (subscribed.compareAndSet(false, true)) {
+ new Thread(() -> {
+ // Subscribe to first driver
+ RedisDriver driver = redisDrivers.get(0);
+ driver.subscribe(new MessageHandler() {
+ @Override
+ public void onMessage(String channel, String message) {
+ if ("zero".equals(message)) {
+ localLatch.countDown(); // Release waiters
+ }
+ }
+ }, channelKey);
+ }).start();
+ }
+}
+
+public long getCount() {
+ int successfulReads = 0;
+ long totalCount = 0;
+
+ for (RedisDriver driver : redisDrivers) {
+ String countStr = driver.get(latchKey);
+ if (countStr != null) {
+ totalCount += Long.parseLong(countStr);
+ successfulReads++;
+ }
+ }
+
+ if (successfulReads >= config.getQuorum()) {
+ return Math.max(0, totalCount / successfulReads);
+ }
+
+ return 0; // Conservative fallback
+}
+```
+
+**Characteristics**:
+- Subscribe to single driver (first one)
+- Check count via quorum read
+- Wait on local CountDownLatch
+- Pub/sub releases local latch
+- Average count across nodes
+
+**Redis Operations** (M nodes):
+- 1 × `SUBSCRIBE`
+- M × `GET` (check count)
+
+### Redisson
+
+**Algorithm**: Subscribe + async polling
+
+```java
+public void await() throws InterruptedException {
+ if (getCount() == 0) {
+ return;
+ }
+
+ CompletableFuture future = subscribe();
+ RedissonCountDownLatchEntry entry = future.join();
+
+ try {
+ while (getCount() > 0) {
+ entry.getLatch().await(); // Wait for notification
+ }
+ } finally {
+ unsubscribe(entry);
+ }
+}
+
+private CompletableFuture subscribe() {
+ return pubSub.subscribe(getEntryName(), getChannelName());
+}
+
+public long getCount() {
+ return commandExecutor.writeAsync(
+ getRawName(), LongCodec.INSTANCE,
+ RedisCommands.GET_LONG, getRawName()
+ ).join();
+}
+```
+
+**Characteristics**:
+- Subscribe via pub/sub service
+- Poll count in loop
+- Semaphore-based waiting
+- Async futures
+- Unsubscribe when done
+
+**Redis Operations**:
+- 1 × `SUBSCRIBE`
+- N × `GET` (polling)
+
+## Reset Operation
+
+### redlock4j
+
+**Reset**: Supported (non-standard)
+
+```java
+public void reset() {
+ // Delete existing latch
+ for (RedisDriver driver : redisDrivers) {
+ driver.del(latchKey);
+ }
+
+ // Reset local state
+ localLatch = new CountDownLatch(1);
+ subscribed.set(false);
+
+ // Reinitialize
+ initializeLatch(initialCount);
+}
+```
+
+**Characteristics**:
+- Deletes on all nodes
+- Resets local latch
+- Reinitializes with original count
+- Not atomic
+- Can cause race conditions
+
+### Redisson
+
+**Reset**: Supported via `trySetCount()`
+
+```java
+// Delete old latch
+latch.delete();
+
+// Create new one
+latch.trySetCount(initialCount);
+```
+
+**Characteristics**:
+- Must explicitly delete first
+- Then set new count
+- Two separate operations
+- Not atomic
+- Publishes notifications
+
+## Performance Comparison
+
+### redlock4j
+
+**Count Down** (M nodes):
+- M × `DECR`
+- M × `PUBLISH` (if zero)
+- Total: M or 2M operations
+
+**Await**:
+- 1 × `SUBSCRIBE`
+- M × `GET` (quorum read)
+- Total: M+1 operations
+
+**Complexity**: O(M) per operation
+
+**Latency**:
+- Higher due to quorum
+- Multiple round trips
+- Pub/sub to all nodes
+
+### Redisson
+
+**Count Down**:
+- 1 Lua script execution
+- Total: 1 operation
+
+**Await**:
+- 1 × `SUBSCRIBE`
+- N × `GET` (polling)
+- Total: N+1 operations
+
+**Complexity**: O(1) for countDown, O(N) for await
+
+**Latency**:
+- Lower for single instance
+- Single round trip for countDown
+- Polling overhead for await
+
+## Safety & Correctness
+
+### redlock4j
+
+**Safety Guarantees**:
+- ✅ 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
+
+**Consistency Model**:
+```
+Count decremented if:
+ - Quorum of nodes decremented
+ - Average count used for reads
+
+Notification sent if:
+ - Any node reaches zero
+ - Published to all nodes
+```
+
+
+### Redisson
+
+**Safety Guarantees**:
+- ✅ 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
+
+**Consistency Model**:
+```
+Count decremented if:
+ - Atomic Lua script succeeds
+ - Single instance
+
+Notification sent if:
+ - Count reaches exactly zero
+ - Atomic with decrement
+```
+
+## 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 |
+
+## Use Case Comparison
+
+### redlock4j RedlockCountDownLatch
+
+**Best For**:
+- Distributed systems requiring quorum-based safety
+- Coordination with strong consistency
+- Multi-master Redis setups
+- Fault-tolerant coordination
+- Automatic expiration needed
+
+**Example Scenarios**:
+```java
+// Distributed service startup coordination
+RedlockCountDownLatch startupLatch = new RedlockCountDownLatch(
+ "app:startup", 5, redisDrivers, config
+);
+
+// Batch job coordination
+RedlockCountDownLatch batchLatch = new RedlockCountDownLatch(
+ "batch:job:123", 100, redisDrivers, config
+);
+
+// Multi-stage workflow
+RedlockCountDownLatch stageLatch = new RedlockCountDownLatch(
+ "workflow:stage1", 10, redisDrivers, config
+);
+```
+
+### Redisson RedissonCountDownLatch
+
+**Best For**:
+- Single Redis instance deployments
+- High-throughput coordination
+- Applications needing async/reactive APIs
+- Scenarios requiring exact count
+- Low-latency requirements
+
+**Example Scenarios**:
+```java
+// High-performance service coordination
+RCountDownLatch startupLatch = redisson.getCountDownLatch("app:startup");
+startupLatch.trySetCount(5);
+
+// Async coordination
+RCountDownLatch asyncLatch = redisson.getCountDownLatch("async:task");
+asyncLatch.trySetCount(10);
+RFuture future = asyncLatch.awaitAsync();
+
+// Reusable latch
+RCountDownLatch reusableLatch = redisson.getCountDownLatch("reusable");
+reusableLatch.trySetCount(3);
+// ... use it
+reusableLatch.delete();
+reusableLatch.trySetCount(5); // Reuse
+```
+
+## Recommendations
+
+### 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
+
+### 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
+
+## Migration Considerations
+
+### From Redisson to redlock4j
+
+```java
+// Before (Redisson)
+RCountDownLatch latch = redisson.getCountDownLatch("startup");
+latch.trySetCount(3);
+latch.await();
+
+// After (redlock4j)
+RedlockCountDownLatch latch = new RedlockCountDownLatch(
+ "startup", 3, redisDrivers, config
+);
+latch.await();
+```
+
+**Benefits**:
+- Quorum-based safety
+- Fault tolerance
+- Automatic expiration
+
+**Considerations**:
+- Higher latency
+- Count is averaged
+- No async support
+
+### From redlock4j to Redisson
+
+```java
+// Before (redlock4j)
+RedlockCountDownLatch latch = new RedlockCountDownLatch(
+ "startup", 3, redisDrivers, config
+);
+
+// After (Redisson)
+RCountDownLatch latch = redisson.getCountDownLatch("startup");
+latch.trySetCount(3);
+```
+
+**Benefits**:
+- Lower latency
+- Exact count
+- Async/reactive support
+- Atomic operations
+
+**Considerations**:
+- Single point of failure
+- Must initialize explicitly
+- No automatic expiration
+
+## Conclusion
+
+Both implementations provide distributed countdown latches with different trade-offs:
+
+**redlock4j RedlockCountDownLatch**:
+- Quorum-based with fault tolerance
+- Automatic initialization and expiration
+- Higher latency but survives failures
+- Count averaged across nodes
+- Best for multi-master setups requiring strong consistency
+
+**Redisson RedissonCountDownLatch**:
+- Atomic Lua scripts with exact counting
+- Lower latency but single point of failure
+- Pub/sub notifications
+- Async/reactive support
+- Best for high-throughput single-instance deployments
+
+Choose based on your specific requirements:
+- **Distributed consistency & fault tolerance** → redlock4j RedlockCountDownLatch
+- **High throughput & low latency** → Redisson RedissonCountDownLatch
+- **Exact count tracking** → Redisson RedissonCountDownLatch
+- **Automatic expiration** → redlock4j RedlockCountDownLatch
+
+
diff --git a/docs/comparison/fairlock-implementation.md b/docs/comparison/fairlock-implementation.md
new file mode 100644
index 0000000..d991d47
--- /dev/null
+++ b/docs/comparison/fairlock-implementation.md
@@ -0,0 +1,564 @@
+# FairLock Implementation Comparison: redlock4j vs Redisson
+
+This document provides a detailed technical comparison of the FairLock implementations in redlock4j and Redisson.
+
+## Overview
+
+Both libraries implement fair locks to ensure FIFO (First-In-First-Out) ordering for lock acquisition, but they use different approaches and data structures.
+
+## Architecture & Data Structures
+
+### redlock4j
+
+- **Data Structure**: Redis Sorted Sets (ZSET) exclusively
+- **Keys Used**:
+ - Lock key: `{lockKey}`
+ - Queue key: `{lockKey}:queue`
+- **Queue Management**: Tokens stored with timestamps as scores
+- **Approach**: Simpler two-key design
+
+### Redisson
+
+- **Data Structures**: Redis List (LIST) + Sorted Set (ZSET)
+- **Keys Used**:
+ - Lock key: `{lockName}`
+ - Queue key: `redisson_lock_queue:{lockName}`
+ - Timeout key: `redisson_lock_timeout:{lockName}`
+- **Queue Management**: List for ordering, sorted set for timeout tracking
+- **Approach**: Three-key design with separate timeout management
+
+## Queue Management Operations
+
+### redlock4j
+
+```java
+// Add to queue
+driver.zAdd(queueKey, timestamp, token)
+
+// Check position
+List first = driver.zRange(queueKey, 0, 0)
+boolean atFront = token.equals(first.get(0))
+
+// Remove from queue
+driver.zRem(queueKey, token)
+
+// Cleanup expired
+driver.zRemRangeByScore(queueKey, 0, expirationThreshold)
+```
+
+**Characteristics**:
+- Position determined by timestamp score
+- Natural ordering by insertion time
+- Single operation for position check
+
+### Redisson
+
+```lua
+-- Add to queue
+redis.call('rpush', KEYS[2], ARGV[2])
+redis.call('zadd', KEYS[3], timeout, ARGV[2])
+
+-- Check position
+local firstThreadId = redis.call('lindex', KEYS[2], 0)
+
+-- Remove from queue
+redis.call('lpop', KEYS[2])
+redis.call('zrem', KEYS[3], ARGV[2])
+```
+
+**Characteristics**:
+- Position determined by list insertion order
+- Separate timeout tracking in sorted set
+- Two operations required for queue management
+
+## Fairness Guarantee
+
+### redlock4j
+
+- **Ordering**: Based on timestamp when added to queue
+- **Quorum**: Requires majority agreement on queue position
+- **Verification**: `isAtFrontOfQueue()` checks across multiple nodes
+- **Clock Dependency**: Relies on reasonably synchronized clocks
+
+```java
+int votesForFront = 0;
+for (RedisDriver driver : redisDrivers) {
+ List firstElements = driver.zRange(queueKey, 0, 0);
+ if (!firstElements.isEmpty() && token.equals(firstElements.get(0))) {
+ votesForFront++;
+ }
+}
+return votesForFront >= config.getQuorum();
+```
+
+### Redisson
+
+- **Ordering**: Based on list insertion order (FIFO)
+- **Stale Cleanup**: Removes expired threads before each operation
+- **Timeout Calculation**: Dynamic based on queue position
+- **Clock Dependency**: Uses timeouts but less sensitive to clock skew
+
+```lua
+-- Cleanup stale threads first
+while true do
+ local firstThreadId = redis.call('lindex', KEYS[2], 0)
+ if firstThreadId == false then break end
+ local timeout = redis.call('zscore', KEYS[3], firstThreadId)
+ if timeout ~= false and tonumber(timeout) <= tonumber(ARGV[4]) then
+ redis.call('zrem', KEYS[3], firstThreadId)
+ redis.call('lpop', KEYS[2])
+ else break end
+end
+```
+
+## Stale Entry Cleanup
+
+### redlock4j
+
+**Strategy**: Periodic cleanup during queue operations
+
+```java
+private void addToQueue(String token, long timestamp) {
+ // Add to queue
+ for (RedisDriver driver : redisDrivers) {
+ driver.zAdd(queueKey, timestamp, token);
+ }
+
+ // Cleanup expired entries
+ long expirationThreshold = System.currentTimeMillis()
+ - config.getDefaultLockTimeoutMs() * 2;
+ cleanupExpiredQueueEntries(expirationThreshold);
+}
+```
+
+**Characteristics**:
+- Cleanup triggered on `addToQueue()`
+- Removes entries older than 2x lock timeout
+- Separate cleanup operation
+
+### Redisson
+
+**Strategy**: Cleanup before every lock operation
+
+```lua
+-- Embedded in every Lua script
+while true do
+ local firstThreadId = redis.call('lindex', KEYS[2], 0)
+ if firstThreadId == false then break end
+ local timeout = redis.call('zscore', KEYS[3], firstThreadId)
+ if timeout ~= false and tonumber(timeout) <= currentTime then
+ redis.call('zrem', KEYS[3], firstThreadId)
+ redis.call('lpop', KEYS[2])
+ else break end
+end
+```
+
+**Characteristics**:
+- State stored in Redis
+- Requires Redis call for reentrant check
+- Survives JVM restart
+- Consistent across instances
+
+## Lock Acquisition Flow
+
+### redlock4j
+
+```java
+public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
+ // 1. Check for reentrancy (thread-local)
+ LockState currentState = lockState.get();
+ if (currentState != null && currentState.isValid()) {
+ currentState.incrementHoldCount();
+ return true;
+ }
+
+ // 2. Add to queue with timestamp
+ String queueToken = generateToken();
+ long timestamp = System.currentTimeMillis();
+ addToQueue(queueToken, timestamp);
+
+ // 3. Retry loop
+ for (int attempt = 0; attempt <= maxRetryAttempts; attempt++) {
+ // 4. Check if at front (quorum-based)
+ if (isAtFrontOfQueue(queueToken)) {
+ // 5. Try standard Redlock acquisition
+ LockResult result = attemptLock();
+ if (result.isAcquired()) {
+ lockState.set(new LockState(...));
+ return true;
+ }
+ }
+
+ // 6. Check timeout
+ if (timeoutExceeded) {
+ removeFromQueue(queueToken);
+ break;
+ }
+
+ // 7. Wait and retry
+ Thread.sleep(retryDelayMs);
+ }
+
+ removeFromQueue(queueToken);
+ return false;
+}
+```
+
+**Flow**:
+1. Check thread-local reentrancy
+2. Add to queue with current timestamp
+3. Poll until at front of queue (quorum check)
+4. Attempt standard Redlock acquisition
+5. Remove from queue on success/failure
+
+### Redisson
+
+```lua
+-- 1. Clean stale threads
+while true do
+ -- Remove expired threads from front
+end
+
+-- 2. Check if lock can be acquired
+if (redis.call('exists', KEYS[1]) == 0)
+ and ((redis.call('exists', KEYS[2]) == 0)
+ or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then
+
+ -- 3. Remove from queue
+ redis.call('lpop', KEYS[2])
+ redis.call('zrem', KEYS[3], ARGV[2])
+
+ -- 4. Decrease timeouts for remaining waiters
+ local keys = redis.call('zrange', KEYS[3], 0, -1)
+ for i = 1, #keys, 1 do
+ redis.call('zincrby', KEYS[3], -tonumber(ARGV[3]), keys[i])
+ end
+
+ -- 5. Acquire lock
+ redis.call('hset', KEYS[1], ARGV[2], 1)
+ redis.call('pexpire', KEYS[1], ARGV[1])
+ return nil
+end
+
+-- 6. Check for reentrancy
+if redis.call('hexists', KEYS[1], ARGV[2]) == 1 then
+ redis.call('hincrby', KEYS[1], ARGV[2], 1)
+ return nil
+end
+
+-- 7. Add to queue if not present
+local timeout = redis.call('zscore', KEYS[3], ARGV[2])
+if timeout == false then
+ -- Calculate timeout based on queue position
+ local lastThreadId = redis.call('lindex', KEYS[2], -1)
+ local ttl = redis.call('pttl', KEYS[1])
+ local timeout = ttl + threadWaitTime + currentTime
+ redis.call('zadd', KEYS[3], timeout, ARGV[2])
+ redis.call('rpush', KEYS[2], ARGV[2])
+end
+return ttl
+```
+
+**Flow**:
+1. Clean stale threads from queue
+2. Check if lock is free AND (queue empty OR at front)
+3. Remove self from queue
+4. Adjust timeouts for remaining threads
+5. Acquire lock or return TTL
+
+## Timeout Handling
+
+### redlock4j
+
+**Strategy**: Fixed timeout with retry
+
+```java
+long timeoutMs = unit.toMillis(time);
+long startTime = System.currentTimeMillis();
+
+for (int attempt = 0; attempt <= maxRetryAttempts; attempt++) {
+ // Try to acquire
+
+ if (timeoutMs > 0 && (System.currentTimeMillis() - startTime) >= timeoutMs) {
+ removeFromQueue(queueToken);
+ break;
+ }
+
+ Thread.sleep(retryDelayMs);
+}
+```
+
+**Characteristics**:
+- Simple timeout check
+- Fixed retry delay
+- Client-side timeout management
+- No timeout estimation
+
+### Redisson
+
+**Strategy**: Dynamic timeout calculation
+
+```lua
+-- Calculate timeout based on queue position
+local lastThreadId = redis.call('lindex', KEYS[2], -1)
+local ttl
+if lastThreadId ~= false and lastThreadId ~= ARGV[2] then
+ ttl = tonumber(redis.call('zscore', KEYS[3], lastThreadId)) - currentTime
+else
+ ttl = redis.call('pttl', KEYS[1])
+end
+local timeout = ttl + threadWaitTime + currentTime
+```
+
+**Characteristics**:
+- Returns estimated wait time (TTL)
+- Timeout = previous thread timeout + threadWaitTime
+- Server-side calculation
+- More accurate wait estimation
+- Default threadWaitTime: 5 seconds
+
+## Unlock & Notification
+
+### redlock4j
+
+**Strategy**: Simple unlock without notification
+
+```java
+public void unlock() {
+ LockState state = lockState.get();
+
+ // Handle reentrancy
+ int remainingHolds = state.decrementHoldCount();
+ if (remainingHolds > 0) {
+ return;
+ }
+
+ // Release lock
+ releaseLock(state.lockValue);
+ removeFromQueue(state.queueToken);
+ lockState.remove();
+}
+```
+
+**Characteristics**:
+- No explicit notification to next waiter
+- Next thread discovers via polling
+- Simpler implementation
+- Higher latency for next waiter
+
+### Redisson
+
+**Strategy**: Pub/Sub notification to next waiter
+
+```lua
+-- After releasing lock
+redis.call('del', KEYS[1])
+local nextThreadId = redis.call('lindex', KEYS[2], 0)
+if nextThreadId ~= false then
+ redis.call('publish', KEYS[4] .. ':' .. nextThreadId, ARGV[1])
+end
+```
+
+**Characteristics**:
+- Explicit wake-up via Redis pub/sub
+- Next thread notified immediately
+- Lower latency for next waiter
+- More complex implementation
+- Requires pub/sub subscription per thread
+
+## Complexity Analysis
+
+### redlock4j
+
+**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
+
+**Cons**:
+- ❌ 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
+
+**Cons**:
+- ❌ More complex implementation
+- ❌ Two data structures to maintain
+- ❌ More Redis operations per attempt
+- ❌ Cleanup overhead on every operation
+- ❌ Requires pub/sub infrastructure
+
+## Performance Comparison
+
+### redlock4j
+
+**Lock Acquisition**:
+- 1x `ZADD` per node (add to queue)
+- 1x `ZRANGE` per node per attempt (check position)
+- Nx `SET NX` per node (standard Redlock)
+- 1x `ZREM` per node (remove from queue)
+
+**Unlock**:
+- 1x Lua script per node (delete if matches)
+- 1x `ZREM` per node (remove from queue)
+
+**Total**: Moderate Redis operations, polling overhead
+
+### Redisson
+
+**Lock Acquisition**:
+- 1x Lua script (all operations atomic)
+ - Stale cleanup (variable)
+ - Queue check
+ - Lock acquisition
+ - Timeout updates
+
+**Unlock**:
+- 1x Lua script
+ - Stale cleanup
+ - Lock release
+ - Pub/sub notification
+
+**Total**: Fewer round trips, but heavier Lua scripts
+
+## Edge Cases & Robustness
+
+### redlock4j
+
+**Clock Skew**:
+- Timestamps used for ordering
+- Significant clock skew could affect fairness
+- Mitigated by clock drift factor in Redlock
+
+**Stale Entries**:
+- Cleaned up on `addToQueue()`
+- Could accumulate between additions
+- Threshold: 2x lock timeout
+
+**Network Partitions**:
+- Quorum-based queue position check
+- Handles minority node failures
+- Consistent with Redlock guarantees
+
+### Redisson
+
+**Clock Skew**:
+- List ordering independent of clocks
+- Timeouts use timestamps but less critical
+- More resilient to clock issues
+
+**Stale Entries**:
+- Cleaned up on every operation
+- Only from front of queue
+- More consistent cleanup
+
+**Network Partitions**:
+- Single instance by default
+- No quorum mechanism
+- Less resilient than redlock4j
+
+## Recommendations
+
+### For redlock4j
+
+**Consider Adopting**:
+1. **Pub/Sub Notifications**: Reduce polling latency
+2. **Dynamic Timeout Estimation**: Return TTL to caller
+3. **More Frequent Cleanup**: Clean on every operation
+4. **List-Based Ordering**: Reduce clock dependency
+
+**Keep**:
+1. Quorum-based queue position check
+2. Thread-local state for performance
+3. Simple two-key design
+4. Standard Redlock integration
+
+### For Production Use
+
+**Choose redlock4j when**:
+- True distributed locking is required
+- Simplicity and maintainability matter
+- Quorum-based safety is essential
+- Minimal dependencies preferred
+
+**Choose Redisson when**:
+- Single instance is acceptable
+- Need full Redis framework
+- Pub/Sub infrastructure available
+- Production-hardened solution needed
+
+## Conclusion
+
+Both implementations provide fair locking with different trade-offs:
+
+- **redlock4j**: Simpler, quorum-based, timestamp-ordered, polling-based
+- **Redisson**: Complex, single-instance, list-ordered, notification-based
+
+The choice depends on your specific requirements for safety, performance, and operational complexity.
+
+**Characteristics**:
+- Cleanup on every lock attempt
+- Only removes from front of queue
+- Integrated into lock acquisition logic
+- Higher overhead but more consistent
+
+## Reentrancy Handling
+
+### redlock4j
+
+**Storage**: Thread-local state
+
+```java
+private final ThreadLocal lockState = new ThreadLocal<>();
+
+private static class LockState {
+ final String lockValue;
+ final String queueToken;
+ final long acquisitionTime;
+ final long validityTime;
+ int holdCount;
+}
+
+// On reentrant lock
+LockState currentState = lockState.get();
+if (currentState != null && currentState.isValid()) {
+ currentState.incrementHoldCount();
+ return true;
+}
+```
+
+**Characteristics**:
+- State stored in JVM memory
+- Fast access (no Redis call)
+- Per-thread tracking
+- Lost on JVM restart
+
+### Redisson
+
+**Storage**: Redis hash field
+
+```lua
+-- Check for reentrant lock
+if redis.call('hexists', KEYS[1], ARGV[2]) == 1 then
+ redis.call('hincrby', KEYS[1], ARGV[2], 1)
+ redis.call('pexpire', KEYS[1], ARGV[1])
+ return nil
+end
+```
+
+
diff --git a/docs/comparison/multilock-implementation.md b/docs/comparison/multilock-implementation.md
new file mode 100644
index 0000000..00f9c09
--- /dev/null
+++ b/docs/comparison/multilock-implementation.md
@@ -0,0 +1,813 @@
+# MultiLock Implementation Comparison: redlock4j vs Redisson
+
+This document provides a detailed technical comparison of the MultiLock implementations in redlock4j and Redisson.
+
+## Overview
+
+Both libraries implement multi-lock functionality to atomically acquire multiple locks, but they use fundamentally different approaches and have different design goals.
+
+## Purpose & Use Case
+
+### redlock4j MultiLock
+
+**Purpose**: Atomic acquisition of multiple independent resources with deadlock prevention
+
+**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
+);
+multiLock.lock();
+try {
+ // All three accounts are now locked
+ transferBetweenAccounts();
+} finally {
+ multiLock.unlock();
+}
+```
+
+### Redisson RedissonMultiLock
+
+**Purpose**: Group multiple RLock objects and manage them as a single lock
+
+**Use Case**: When you have multiple independent locks (possibly from different Redisson instances) that need to be acquired together
+
+```java
+RLock lock1 = redisson1.getLock("lock1");
+RLock lock2 = redisson2.getLock("lock2");
+RLock lock3 = redisson3.getLock("lock3");
+
+RedissonMultiLock multiLock = new RedissonMultiLock(lock1, lock2, lock3);
+multiLock.lock();
+try {
+ // All locks acquired
+} finally {
+ multiLock.unlock();
+}
+```
+
+## Architecture & Design Philosophy
+
+### redlock4j
+
+**Design**: Integrated Redlock implementation for multiple resources
+
+**Key Characteristics**:
+- Each resource is a separate key on the same Redis cluster
+- Uses the same Redlock quorum mechanism for all resources
+- Sorted keys to prevent deadlocks
+- All-or-nothing acquisition with automatic rollback
+
+**Architecture**:
+```
+MultiLock
+ ├─ List lockKeys (sorted)
+ ├─ List redisDrivers (shared cluster)
+ ├─ Quorum-based acquisition per resource
+ └─ Thread-local state tracking
+```
+
+### Redisson
+
+**Design**: Wrapper around multiple independent RLock objects
+
+**Key Characteristics**:
+- Each RLock can be from a different Redisson instance
+- Each lock uses its own Redis connection/cluster
+- No inherent deadlock prevention (no key sorting)
+- Sequential acquisition with retry logic
+
+**Architecture**:
+```
+RedissonMultiLock
+ ├─ List locks (order preserved)
+ ├─ Each RLock has its own connection
+ ├─ Sequential acquisition
+ └─ Configurable failure tolerance
+```
+
+## Deadlock Prevention
+
+### redlock4j
+
+**Strategy**: Automatic key sorting
+
+```java
+// Constructor automatically sorts keys
+this.lockKeys = lockKeys.stream()
+ .distinct()
+ .sorted() // Lexicographic ordering
+ .collect(Collectors.toList());
+```
+
+**Guarantee**: All threads acquire locks in the same order, preventing circular wait conditions
+
+**Example**:
+```java
+// Thread 1: locks ["account:1", "account:2", "account:3"]
+// Thread 2: locks ["account:3", "account:1", "account:2"]
+// Both will acquire in order: account:1 → account:2 → account:3
+```
+
+### Redisson
+
+**Strategy**: No automatic deadlock prevention
+
+```java
+// Locks are acquired in the order provided
+public RedissonMultiLock(RLock... locks) {
+ this.locks.addAll(Arrays.asList(locks));
+}
+```
+
+**Risk**: Developer must ensure consistent ordering
+
+**Example**:
+```java
+// Thread 1: new RedissonMultiLock(lock1, lock2, lock3)
+// Thread 2: new RedissonMultiLock(lock3, lock1, lock2)
+// DEADLOCK POSSIBLE if not careful!
+```
+
+## Lock Acquisition Algorithm
+
+### redlock4j
+
+**Algorithm**: Quorum-based atomic acquisition
+
+```java
+private MultiLockResult attemptMultiLock() {
+ Map lockValues = new HashMap<>();
+
+ // 1. Generate unique values for each key
+ for (String key : lockKeys) {
+ 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++;
+ }
+ }
+
+ // 3. Check quorum and validity
+ 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
+
+### Redisson
+
+**Algorithm**: Sequential acquisition with retry and failure tolerance
+
+```java
+public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) {
+ long remainTime = unit.toMillis(waitTime);
+ long lockWaitTime = calcLockWaitTime(remainTime);
+
+ int failedLocksLimit = failedLocksLimit(); // Default: 0
+ List acquiredLocks = new ArrayList<>();
+
+ // 1. Iterate through locks sequentially
+ for (ListIterator iterator = locks.listIterator(); iterator.hasNext();) {
+ RLock lock = iterator.next();
+ boolean lockAcquired;
+
+ try {
+ // 2. Try to acquire this lock
+ long awaitTime = Math.min(lockWaitTime, remainTime);
+ lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
+ } catch (Exception e) {
+ lockAcquired = false;
+ }
+
+ if (lockAcquired) {
+ acquiredLocks.add(lock);
+ } else {
+ // 3. Check if we can tolerate this failure
+ if (locks.size() - acquiredLocks.size() == failedLocksLimit()) {
+ break; // Acquired enough
+ }
+
+ // 4. If no tolerance, retry from beginning
+ if (failedLocksLimit == 0) {
+ unlockInner(acquiredLocks);
+ if (waitTime <= 0) {
+ return false;
+ }
+ // Reset and retry
+ acquiredLocks.clear();
+ while (iterator.hasPrevious()) {
+ iterator.previous();
+ }
+ } else {
+ failedLocksLimit--;
+ }
+ }
+
+ // 5. Check timeout
+ if (remainTime > 0) {
+ remainTime -= elapsed;
+ if (remainTime <= 0) {
+ unlockInner(acquiredLocks);
+ return false;
+ }
+ }
+ }
+
+ // 6. Set lease time on all acquired locks
+ if (leaseTime > 0) {
+ acquiredLocks.stream()
+ .map(l -> (RedissonBaseLock) l)
+ .map(l -> l.expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS))
+ .forEach(f -> f.toCompletableFuture().join());
+ }
+
+ return true;
+}
+```
+
+**Flow**:
+1. Iterate through locks in provided order
+2. Try to acquire each lock individually
+3. If failure and no tolerance: unlock all and retry from start
+4. If failure with tolerance: continue to next lock
+5. Check timeout after each attempt
+6. Set lease time on all acquired locks
+
+## Failure Handling & Rollback
+
+### redlock4j
+
+**Strategy**: Per-node rollback with all-or-nothing semantics
+
+```java
+private void rollbackOnNode(RedisDriver driver, Map lockValues,
+ List acquiredKeys) {
+ for (String key : acquiredKeys) {
+ try {
+ driver.deleteIfValueMatches(key, lockValues.get(key));
+ } catch (Exception e) {
+ logger.warn("Failed to rollback lock {} on {}", key, driver);
+ }
+ }
+}
+```
+
+**Characteristics**:
+- Immediate rollback on any failure within a node
+- All-or-nothing per node
+- Quorum check after all nodes attempted
+- Global rollback if quorum not achieved
+
+**Example**:
+```
+Node 1: account:1 ✓, account:2 ✓, account:3 ✓ → Success
+Node 2: account:1 ✓, account:2 ✗ → Rollback account:1 on Node 2
+Node 3: account:1 ✓, account:2 ✓, account:3 ✓ → Success
+
+Result: 2/3 nodes succeeded
+If quorum=2: SUCCESS
+If quorum=3: FAIL → Rollback all nodes
+```
+
+### Redisson
+
+**Strategy**: Retry from beginning on failure (default) or tolerance-based
+
+```java
+protected int failedLocksLimit() {
+ return 0; // No tolerance by default
+}
+```
+
+**Characteristics**:
+- Default: unlock all and retry from start
+- Can be overridden for failure tolerance
+- No per-lock rollback
+- Sequential retry logic
+
+**Example** (default behavior):
+```
+Attempt 1: lock1 ✓, lock2 ✗ → Unlock lock1, retry
+Attempt 2: lock1 ✓, lock2 ✓, lock3 ✓ → Success
+```
+
+**Example** (with tolerance in RedissonRedLock):
+```
+RedissonRedLock extends RedissonMultiLock {
+ protected int failedLocksLimit() {
+ return locks.size() - minLocksAmount(locks);
+ }
+
+ protected int minLocksAmount(List locks) {
+ return locks.size() / 2 + 1; // Quorum
+ }
+}
+
+Attempt: lock1 ✓, lock2 ✗, lock3 ✓ → Success (2/3 acquired)
+```
+
+## Reentrancy Support
+
+### redlock4j
+
+**Implementation**: Thread-local state with hold count
+
+```java
+private final ThreadLocal lockState = new ThreadLocal<>();
+
+private static class LockState {
+ final Map lockValues; // All lock values
+ final long acquisitionTime;
+ final long validityTime;
+ int holdCount;
+}
+
+public boolean tryLock(long time, TimeUnit unit) {
+ // Check reentrancy
+ LockState currentState = lockState.get();
+ if (currentState != null && currentState.isValid()) {
+ currentState.incrementHoldCount();
+ return true;
+ }
+ // ... acquire logic
+}
+
+public void unlock() {
+ LockState state = lockState.get();
+ int remainingHolds = state.decrementHoldCount();
+ if (remainingHolds > 0) {
+ return; // Still held
+ }
+ // ... release logic
+}
+```
+
+**Characteristics**:
+- Single hold count for all locks together
+- Fast (no Redis calls for reentrant acquisition)
+- Thread-local storage
+- All locks treated as atomic unit
+
+### Redisson
+
+**Implementation**: Delegates to individual RLock reentrancy
+
+```java
+// Each RLock handles its own reentrancy
+public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) {
+ for (RLock lock : locks) {
+ // Each lock checks its own hold count in Redis
+ lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
+ }
+}
+
+public void unlock() {
+ locks.forEach(Lock::unlock);
+}
+```
+
+**Characteristics**:
+- Each lock maintains its own hold count
+- Requires Redis calls for each lock
+- Independent reentrancy per lock
+- Locks can have different hold counts
+
+## Timeout & Validity Calculation
+
+### redlock4j
+
+**Strategy**: Single validity time for all locks
+
+```java
+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;
+
+boolean acquired = successfulNodes >= config.getQuorum() && validityTime > 0;
+```
+
+**Characteristics**:
+- Single validity calculation for entire multi-lock
+- Clock drift compensation
+- All locks expire together
+- Validity must be positive for success
+
+### Redisson
+
+**Strategy**: Individual timeout per lock with dynamic wait time
+
+```java
+long baseWaitTime = locks.size() * 1500; // 1.5s per lock
+
+long waitTime;
+if (leaseTime <= 0) {
+ waitTime = baseWaitTime;
+} else {
+ waitTime = unit.toMillis(leaseTime);
+ if (waitTime <= baseWaitTime) {
+ waitTime = ThreadLocalRandom.current().nextLong(waitTime/2, waitTime);
+ } else {
+ waitTime = ThreadLocalRandom.current().nextLong(baseWaitTime, waitTime);
+ }
+}
+```
+
+**Characteristics**:
+- Dynamic wait time based on number of locks
+- Randomization to avoid thundering herd
+- Each lock can have different lease time
+- No global validity check
+
+## Performance Comparison
+
+### redlock4j
+
+**Lock Acquisition**:
+- For N resources on M nodes:
+ - N × M `SET NX` operations (parallel per node)
+ - Rollback: up to N × M `DELETE` operations
+- Single round of attempts per retry
+- All nodes contacted in parallel
+
+**Complexity**: O(N × M) per attempt
+
+**Example** (3 resources, 3 nodes):
+```
+Attempt 1:
+ Node 1: SET account:1, SET account:2, SET account:3
+ Node 2: SET account:1, SET account:2, SET account:3
+ Node 3: SET account:1, SET account:2, SET account:3
+Total: 9 operations (parallel)
+```
+
+### Redisson
+
+**Lock Acquisition**:
+- For N locks:
+ - N sequential lock attempts
+ - Each lock may involve multiple Redis operations
+ - Retry from beginning on failure
+- Sequential processing
+
+**Complexity**: O(N × R) where R = retry attempts
+
+**Example** (3 locks):
+```
+Attempt 1:
+ lock1.tryLock() → Redis operations
+ lock2.tryLock() → Redis operations
+ lock2 fails → unlock lock1
+Attempt 2:
+ lock1.tryLock() → Redis operations
+ lock2.tryLock() → Redis operations
+ lock3.tryLock() → Redis operations
+Total: Variable, sequential
+```
+
+
+## Use Case Differences
+
+### redlock4j MultiLock
+
+**Best For**:
+- Locking multiple resources on the same Redis cluster
+- Scenarios requiring strict deadlock prevention
+- Atomic operations across multiple keys
+- Distributed systems with quorum requirements
+
+**Example Scenarios**:
+```java
+// Bank transfer between multiple accounts
+MultiLock lock = new MultiLock(
+ Arrays.asList("account:1", "account:2", "account:3"),
+ redisDrivers, config
+);
+
+// Inventory management across warehouses
+MultiLock lock = new MultiLock(
+ Arrays.asList("warehouse:A:item:123", "warehouse:B:item:123"),
+ redisDrivers, config
+);
+```
+
+### Redisson RedissonMultiLock
+
+**Best For**:
+- Grouping locks from different Redis instances
+- Coordinating across multiple independent systems
+- Flexible lock composition
+- When you already have RLock objects
+
+**Example Scenarios**:
+```java
+// Locks from different Redis clusters
+RLock lock1 = redisson1.getLock("resource1"); // Cluster 1
+RLock lock2 = redisson2.getLock("resource2"); // Cluster 2
+RLock lock3 = redisson3.getLock("resource3"); // Cluster 3
+RedissonMultiLock multiLock = new RedissonMultiLock(lock1, lock2, lock3);
+
+// Mix different lock types
+RLock fairLock = redisson.getFairLock("fair");
+RLock readLock = redisson.getReadWriteLock("rw").readLock();
+RedissonMultiLock multiLock = new RedissonMultiLock(fairLock, readLock);
+```
+
+## Safety & Correctness
+
+### redlock4j
+
+**Safety Guarantees**:
+- ✅ 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)
+
+### Redisson
+
+**Safety Guarantees**:
+- ✅ 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
+
+## Complexity Analysis
+
+### redlock4j
+
+**Code Complexity**: ~370 lines
+
+**Pros**:
+- ✅ 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
+
+### 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
+
+**Cons**:
+- ❌ No deadlock prevention
+- ❌ Sequential acquisition
+- ❌ More complex retry logic
+- ❌ Requires careful lock ordering
+
+## RedissonRedLock vs redlock4j MultiLock
+
+Redisson also has `RedissonRedLock` which extends `RedissonMultiLock`:
+
+### RedissonRedLock
+
+```java
+public class RedissonRedLock extends RedissonMultiLock {
+
+ @Override
+ protected int failedLocksLimit() {
+ return locks.size() - minLocksAmount(locks);
+ }
+
+ protected int minLocksAmount(List locks) {
+ return locks.size() / 2 + 1; // Quorum
+ }
+}
+```
+
+**Key Difference**: Implements quorum-based failure tolerance
+
+**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 |
+
+## 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
+
+### 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
+
+### 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
+
+## Migration Considerations
+
+### From Redisson to redlock4j
+
+```java
+// Before (Redisson)
+RLock lock1 = redisson.getLock("account:1");
+RLock lock2 = redisson.getLock("account:2");
+RLock lock3 = redisson.getLock("account:3");
+RedissonMultiLock multiLock = new RedissonMultiLock(lock1, lock2, lock3);
+multiLock.lock();
+try {
+ // work
+} finally {
+ multiLock.unlock();
+}
+
+// After (redlock4j)
+MultiLock multiLock = new MultiLock(
+ Arrays.asList("account:1", "account:2", "account:3"),
+ redisDrivers,
+ config
+);
+multiLock.lock();
+try {
+ // work
+} finally {
+ multiLock.unlock();
+}
+```
+
+**Benefits**:
+- Automatic deadlock prevention
+- Quorum-based safety
+- Simpler API
+
+**Considerations**:
+- All resources must be on same cluster
+- Different performance characteristics
+
+### From redlock4j to Redisson
+
+```java
+// Before (redlock4j)
+MultiLock multiLock = new MultiLock(
+ Arrays.asList("resource1", "resource2", "resource3"),
+ redisDrivers,
+ config
+);
+
+// After (Redisson) - if using multiple instances
+RLock lock1 = redisson1.getLock("resource1");
+RLock lock2 = redisson2.getLock("resource2");
+RLock lock3 = redisson3.getLock("resource3");
+RedissonRedLock multiLock = new RedissonRedLock(lock1, lock2, lock3);
+```
+
+**Benefits**:
+- Can use different Redis instances
+- Async/reactive support
+- More flexible composition
+
+**Considerations**:
+- Must ensure consistent lock ordering
+- Different acquisition semantics
+
+## Conclusion
+
+Both implementations serve different purposes:
+
+**redlock4j MultiLock**:
+- Designed for locking multiple resources on the same distributed Redis cluster
+- Automatic deadlock prevention through key sorting
+- Quorum-based safety per resource
+- All-or-nothing atomic acquisition
+- Simpler, more focused implementation
+
+**Redisson RedissonMultiLock**:
+- Designed for grouping independent locks (possibly from different instances)
+- Flexible composition of different lock types
+- Sequential acquisition with retry logic
+- Requires manual deadlock prevention
+- More flexible but more complex
+
+**Redisson RedissonRedLock**:
+- Implements Redlock algorithm across multiple Redis instances
+- Quorum-based failure tolerance
+- Similar goals to redlock4j but different scope (instances vs resources)
+
+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
new file mode 100644
index 0000000..41458ee
--- /dev/null
+++ b/docs/comparison/readwritelock-implementation.md
@@ -0,0 +1,712 @@
+# ReadWriteLock Implementation Comparison: redlock4j vs Redisson
+
+This document provides a detailed technical comparison of the ReadWriteLock implementations in redlock4j and Redisson.
+
+## Overview
+
+Both libraries implement distributed read-write locks to allow multiple concurrent readers or a single exclusive writer, but they use different data structures and synchronization mechanisms.
+
+## Purpose & Use Case
+
+### redlock4j RedlockReadWriteLock
+
+**Purpose**: Distributed read-write lock with quorum-based safety guarantees
+
+**Use Case**: Scenarios requiring strong consistency for read-heavy workloads
+
+```java
+RedlockReadWriteLock rwLock = new RedlockReadWriteLock(
+ "resource", redisDrivers, config
+);
+
+// Multiple readers can acquire simultaneously
+rwLock.readLock().lock();
+try {
+ readData();
+} finally {
+ rwLock.readLock().unlock();
+}
+
+// Writer has exclusive access
+rwLock.writeLock().lock();
+try {
+ writeData();
+} finally {
+ rwLock.writeLock().unlock();
+}
+```
+
+### Redisson RedissonReadWriteLock
+
+**Purpose**: Distributed read-write lock with pub/sub notifications
+
+**Use Case**: High-performance read-write scenarios with single Redis instance
+
+```java
+RReadWriteLock rwLock = redisson.getReadWriteLock("resource");
+
+// Read lock
+rwLock.readLock().lock();
+try {
+ readData();
+} finally {
+ rwLock.readLock().unlock();
+}
+
+// Write lock
+rwLock.writeLock().lock();
+try {
+ writeData();
+} finally {
+ rwLock.writeLock().unlock();
+}
+```
+
+## Architecture & Data Model
+
+### redlock4j
+
+**Design**: Counter-based with separate write lock
+
+**Data Structure**:
+```
+{resourceKey}:readers = {count} (reader counter)
+{resourceKey}:readers:{lockValue1} = "1" (individual reader tracking)
+{resourceKey}:readers:{lockValue2} = "1" (individual reader tracking)
+{resourceKey}:write = {lockValue} (exclusive write lock)
+```
+
+**Key Characteristics**:
+- Reader count tracked via `INCR`/`DECR`
+- Individual reader keys for tracking
+- Write lock uses standard Redlock
+- Quorum-based for both read and write
+- Thread-local state for reentrancy
+
+**Architecture**:
+```
+RedlockReadWriteLock
+ ├─ ReadLock
+ │ ├─ readCountKey ({key}:readers)
+ │ ├─ writeLockKey ({key}:write)
+ │ ├─ ThreadLocal
+ │ └─ Quorum-based INCR/DECR
+ └─ WriteLock
+ ├─ Redlock (for write lock)
+ ├─ readCountKey ({key}:readers)
+ └─ Polling for reader count
+```
+
+### Redisson
+
+**Design**: Hash-based with mode tracking
+
+**Data Structure**:
+```
+{resourceKey} = {
+ "mode": "read" or "write",
+ "{threadId1}": {holdCount},
+ "{threadId2}": {holdCount},
+ ...
+}
+redisson_rwlock:{resourceKey} = (pub/sub channel)
+```
+
+**Key Characteristics**:
+- Single hash stores all lock state
+- Mode field tracks read/write state
+- Thread IDs as hash fields
+- Lua scripts for atomicity
+- Pub/sub for notifications
+
+**Architecture**:
+```
+RedissonReadWriteLock
+ ├─ RedissonReadLock
+ │ ├─ Lua scripts for acquisition
+ │ ├─ Hash-based state
+ │ └─ Pub/sub notifications
+ └─ RedissonWriteLock
+ ├─ Lua scripts for acquisition
+ ├─ Hash-based state
+ └─ Pub/sub notifications
+```
+
+## Read Lock Acquisition
+
+### redlock4j
+
+**Algorithm**: Check write lock, then increment reader count
+
+```java
+public boolean tryLock(long time, TimeUnit unit) {
+ // 1. Check reentrancy
+ LockState currentState = lockState.get();
+ if (currentState != null && currentState.isValid()) {
+ currentState.incrementHoldCount();
+ return true;
+ }
+
+ // 2. Retry loop
+
+
+### Redisson
+
+**Algorithm**: Lua script with hash-based state management
+
+```lua
+-- tryLockInnerAsync (simplified)
+local mode = redis.call('hget', KEYS[1], 'mode');
+
+-- If no lock or already in read mode
+if (mode == false) then
+ redis.call('hset', KEYS[1], 'mode', 'read');
+ redis.call('hset', KEYS[1], ARGV[2], 1);
+ redis.call('pexpire', KEYS[1], ARGV[1]);
+ return nil;
+end;
+
+if (mode == 'read') or (mode == 'write' and redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
+ local ind = redis.call('hincrby', KEYS[1], ARGV[2], 1);
+ local remainTime = redis.call('pttl', KEYS[1]);
+ redis.call('pexpire', KEYS[1], math.max(remainTime, ARGV[1]));
+ return nil;
+end;
+
+-- Write lock held by another thread
+return redis.call('pttl', KEYS[1]);
+```
+
+**Flow**:
+1. Get current mode from hash
+2. If no lock: set mode='read', add thread, set TTL
+3. If read mode: increment thread's hold count
+4. If write mode by same thread: allow (lock downgrade)
+5. If write mode by other thread: return TTL
+
+**Redis Operations**:
+- 1 Lua script execution (atomic)
+
+## Write Lock Acquisition
+
+### redlock4j
+
+**Algorithm**: Wait for readers, then acquire exclusive lock
+
+```java
+public boolean tryLock(long time, TimeUnit unit) {
+ long timeoutMs = unit.toMillis(time);
+ long startTime = System.currentTimeMillis();
+
+ // 1. Wait for readers to finish
+ while (hasActiveReaders()) {
+ if (timeoutExceeded(startTime, timeoutMs)) {
+ return false;
+ }
+ Thread.sleep(retryDelayMs);
+ }
+
+ // 2. Acquire write lock using standard Redlock
+ long remainingTime = timeoutMs - elapsed;
+ return underlyingLock.tryLock(remainingTime, TimeUnit.MILLISECONDS);
+}
+
+private boolean hasActiveReaders() {
+ int nodesWithoutReaders = 0;
+
+ for (RedisDriver driver : redisDrivers) {
+ String countStr = driver.get(readCountKey);
+ if (countStr == null || Long.parseLong(countStr) <= 0) {
+ nodesWithoutReaders++;
+ }
+ }
+
+ // Quorum of nodes must have no readers
+ return nodesWithoutReaders < quorum;
+}
+```
+
+**Flow**:
+1. Poll reader count on all nodes
+2. Wait until quorum shows no readers
+3. Acquire exclusive lock via Redlock
+4. Check timeout throughout
+
+**Redis Operations** (M nodes):
+- N × M `GET` (polling reader count)
+- M × `SET NX` (Redlock acquisition)
+
+### Redisson
+
+**Algorithm**: Lua script with mode transition
+
+```lua
+-- tryLockInnerAsync (simplified)
+local mode = redis.call('hget', KEYS[1], 'mode');
+
+-- No lock exists
+if (mode == false) then
+ redis.call('hset', KEYS[1], 'mode', 'write');
+ redis.call('hset', KEYS[1], ARGV[2], 1);
+ redis.call('pexpire', KEYS[1], ARGV[1]);
+ return nil;
+end;
+
+-- Same thread already holds write lock (reentrant)
+if (mode == 'write') then
+ if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
+ redis.call('hincrby', KEYS[1], ARGV[2], 1);
+ redis.call('pexpire', KEYS[1], ARGV[1]);
+ return nil;
+ end;
+end;
+
+-- Same thread holds read lock (lock upgrade)
+if (mode == 'read') then
+ local ind = redis.call('hget', KEYS[1], ARGV[2]);
+ if (ind ~= false) then
+ -- Check if this is the only reader
+ if (redis.call('hlen', KEYS[1]) == 2) then -- mode + this thread
+ redis.call('hset', KEYS[1], 'mode', 'write');
+ return nil;
+ end;
+ end;
+end;
+
+-- Lock held by others
+return redis.call('pttl', KEYS[1]);
+```
+
+**Flow**:
+1. Get current mode
+2. If no lock: set mode='write', add thread
+3. If write mode by same thread: increment (reentrant)
+4. If read mode by same thread only: upgrade to write
+5. If held by others: return TTL
+
+**Redis Operations**:
+- 1 Lua script execution (atomic)
+
+## Read Lock Release
+
+### redlock4j
+
+**Algorithm**: Decrement reader count
+
+```java
+public void unlock() {
+ LockState state = lockState.get();
+
+ // Handle reentrancy
+ int remainingHolds = state.decrementHoldCount();
+ if (remainingHolds > 0) {
+ return;
+ }
+
+ // Decrement reader count on all nodes
+ decrementReaderCount(state.lockValue);
+ lockState.remove();
+}
+
+private void decrementReaderCount(String lockValue) {
+ for (RedisDriver driver : redisDrivers) {
+ // Decrement counter
+ long count = driver.decr(readCountKey);
+
+ // Delete individual reader key
+ driver.del(readCountKey + ":" + lockValue);
+
+ // Clean up counter if zero
+ if (count <= 0) {
+ driver.del(readCountKey);
+ }
+ }
+}
+```
+
+**Characteristics**:
+- DECR on all nodes
+- Delete individual reader key
+- Clean up counter when zero
+- No notification to waiting writers
+
+**Redis Operations** (M nodes):
+- M × `DECR`
+- M × `DEL` (individual key)
+- M × `DEL` (counter, if zero)
+
+### Redisson
+
+**Algorithm**: Lua script with notification
+
+```lua
+-- unlockInnerAsync (simplified)
+local mode = redis.call('hget', KEYS[1], 'mode');
+if (mode == false) then
+ return 1; -- Already unlocked
+end;
+
+local lockExists = redis.call('hexists', KEYS[1], ARGV[2]);
+if (lockExists == 0) then
+ return nil; -- Not held by this thread
+end;
+
+local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1);
+if (counter > 0) then
+ redis.call('pexpire', KEYS[1], ARGV[1]);
+ return 0; -- Still held (reentrant)
+end;
+
+redis.call('hdel', KEYS[1], ARGV[2]);
+if (redis.call('hlen', KEYS[1]) > 1) then
+ -- Other readers still exist
+ redis.call('pexpire', KEYS[1], ARGV[1]);
+ return 0;
+end;
+
+-- Last reader, delete lock and notify
+redis.call('del', KEYS[1]);
+redis.call('publish', KEYS[2], ARGV[1]);
+return 1;
+```
+
+**Characteristics**:
+- Atomic decrement in hash
+- Delete thread field when zero
+- Publish notification when last reader
+- Waiting writers wake up immediately
+
+**Redis Operations**:
+- 1 Lua script execution
+- 1 `PUBLISH` (if last reader)
+
+## Write Lock Release
+
+### redlock4j
+
+**Algorithm**: Standard Redlock unlock
+
+```java
+public void unlock() {
+ underlyingLock.unlock(); // Delegates to Redlock
+}
+```
+
+**Characteristics**:
+- Uses Redlock unlock mechanism
+- Delete if value matches
+- No notification to waiting readers/writers
+
+**Redis Operations** (M nodes):
+- M × Lua script (delete if matches)
+
+### Redisson
+
+**Algorithm**: Lua script with notification
+
+```lua
+-- Similar to read unlock but for write mode
+-- Decrements hold count, deletes when zero, publishes notification
+```
+
+**Characteristics**:
+- Atomic decrement in hash
+- Delete lock when zero
+- Publish notification to all waiters
+- Immediate wake-up
+
+**Redis Operations**:
+- 1 Lua script execution
+- 1 `PUBLISH`
+
+
+## Lock Upgrade/Downgrade
+
+### redlock4j
+
+**Lock Upgrade** (Read → Write): Not supported
+
+**Lock Downgrade** (Write → Read): Not supported
+
+**Characteristics**:
+- Must release read lock before acquiring write lock
+- Must release write lock before acquiring read lock
+- No automatic conversion
+- Prevents potential deadlocks
+
+### Redisson
+
+**Lock Upgrade** (Read → Write): Supported (single reader only)
+
+```lua
+-- If this thread is the only reader, can upgrade to write
+if (mode == 'read') then
+ if (redis.call('hlen', KEYS[1]) == 2) then -- mode + this thread
+ redis.call('hset', KEYS[1], 'mode', 'write');
+ return nil;
+ end;
+end;
+```
+
+**Lock Downgrade** (Write → Read): Supported
+
+```lua
+-- If thread holds write lock, can acquire read lock (downgrade)
+if (mode == 'write' and redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
+ local ind = redis.call('hincrby', KEYS[1], ARGV[2], 1);
+ return nil;
+end;
+```
+
+**Characteristics**:
+- Automatic lock downgrade (write → read)
+- Lock upgrade only if sole reader
+- Prevents deadlock from multiple readers upgrading
+- More flexible but more complex
+
+## Fairness & Ordering
+
+### redlock4j
+
+**Fairness**: Non-fair (no ordering guarantees)
+
+**Characteristics**:
+- Readers and writers compete equally
+- No FIFO queue
+- Retry-based acquisition
+- Potential writer starvation if many readers
+
+**Example**:
+```
+Reader 1: acquire → success
+Reader 2: acquire → success
+Writer 1: tryLock → blocks (waiting for readers)
+Reader 3: acquire → success (can acquire while writer waits)
+Writer 1: still waiting...
+```
+
+### Redisson
+
+**Fairness**: Non-fair by default
+
+**Characteristics**:
+- No ordering between readers and writers
+- Pub/sub wakes all waiters
+- Race to acquire after notification
+- Also has `RedissonFairReadWriteLock` for FIFO ordering
+
+**Note**: Redisson provides `RedissonFairReadWriteLock` for fair ordering with FIFO guarantees.
+
+## Performance Comparison
+
+### redlock4j
+
+**Read Lock Acquisition** (M nodes):
+- M × `GET` (check write lock)
+- M × `INCR` (increment counter)
+- M × `SETEX` (individual reader key)
+- Total: 3M operations
+
+**Write Lock Acquisition** (M nodes):
+- N × M `GET` (poll reader count)
+- M × `SET NX` (Redlock)
+- Total: (N+1)M operations
+
+**Complexity**: O(M) for reads, O(N×M) for writes
+
+**Latency**:
+- Higher due to quorum requirement
+- Polling overhead for writers
+- No pub/sub overhead
+
+### Redisson
+
+**Read Lock Acquisition**:
+- 1 Lua script execution
+- If blocked: 1 pub/sub subscription
+- Total: 1-2 operations
+
+**Write Lock Acquisition**:
+- 1 Lua script execution
+- If blocked: 1 pub/sub subscription
+- Total: 1-2 operations
+
+**Complexity**: O(1) per operation
+
+**Latency**:
+- Lower for single instance
+- Single round trip
+- Pub/sub notification overhead
+
+## Safety & Correctness
+
+### redlock4j
+
+**Safety Guarantees**:
+- ✅ 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
+
+**Consistency Model**:
+```
+Read lock acquired if:
+ - Quorum shows no write lock
+ - Reader count incremented on quorum
+
+Write lock acquired if:
+ - Quorum shows no readers
+ - Exclusive lock acquired on quorum
+```
+
+### Redisson
+
+**Safety Guarantees**:
+- ✅ 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
+
+**Consistency Model**:
+```
+Lock acquired if:
+ - Mode allows acquisition
+ - Atomic state transition succeeds
+ - No distributed consistency
+```
+
+## Use Case Comparison
+
+### redlock4j RedlockReadWriteLock
+
+**Best For**:
+- Distributed systems requiring quorum-based safety
+- Read-heavy workloads with strong consistency
+- Multi-master Redis setups
+- Fault-tolerant read-write scenarios
+- Can tolerate higher latency
+
+**Example Scenarios**:
+```java
+// Distributed cache with strong consistency
+RedlockReadWriteLock cacheLock = new RedlockReadWriteLock(
+ "cache:users", redisDrivers, config
+);
+
+// Configuration management
+RedlockReadWriteLock configLock = new RedlockReadWriteLock(
+ "config:app", redisDrivers, config
+);
+```
+
+### Redisson RedissonReadWriteLock
+
+**Best For**:
+- Single Redis instance deployments
+- High-throughput read-write scenarios
+- Applications needing async/reactive APIs
+- Lock upgrade/downgrade requirements
+- Low-latency requirements
+
+**Example Scenarios**:
+```java
+// High-performance cache
+RReadWriteLock cacheLock = redisson.getReadWriteLock("cache:users");
+
+// Document editing with lock downgrade
+RReadWriteLock docLock = redisson.getReadWriteLock("doc:123");
+docLock.writeLock().lock();
+try {
+ editDocument();
+ docLock.readLock().lock(); // Downgrade
+ docLock.writeLock().unlock();
+ try {
+ readDocument();
+ } finally {
+ docLock.readLock().unlock();
+ }
+} finally {
+ if (docLock.writeLock().isHeldByCurrentThread()) {
+ docLock.writeLock().unlock();
+ }
+}
+```
+
+## 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 |
+
+## 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
+
+### 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)
+
+## Conclusion
+
+Both implementations provide distributed read-write locks with different trade-offs:
+
+**redlock4j RedlockReadWriteLock**:
+- Counter-based with quorum safety
+- Higher latency but fault-tolerant
+- Polling-based waiting
+- No lock conversion support
+- Best for multi-master setups requiring strong consistency
+
+**Redisson RedissonReadWriteLock**:
+- Hash-based with atomic Lua scripts
+- Lower latency but single point of failure
+- Pub/sub notifications
+- Lock upgrade/downgrade support
+- Best for high-throughput single-instance deployments
+
+Choose based on your specific requirements:
+- **Distributed consistency & fault tolerance** → redlock4j
+- **High throughput & low latency** → Redisson
+- **Fair ordering** → Redisson RedissonFairReadWriteLock
+
+
+
diff --git a/docs/comparison/semaphore-implementation.md b/docs/comparison/semaphore-implementation.md
new file mode 100644
index 0000000..ab41a90
--- /dev/null
+++ b/docs/comparison/semaphore-implementation.md
@@ -0,0 +1,806 @@
+# Semaphore Implementation Comparison: redlock4j vs Redisson
+
+This document provides a detailed technical comparison of the Semaphore implementations in redlock4j and Redisson.
+
+## Overview
+
+Both libraries implement distributed semaphores to limit concurrent access to resources, but they use fundamentally different approaches and data structures.
+
+## Purpose & Use Case
+
+### redlock4j RedlockSemaphore
+
+**Purpose**: Distributed semaphore with quorum-based safety guarantees
+
+**Use Case**: Rate limiting and resource pooling with strong consistency requirements
+
+```java
+// Create a semaphore with 5 permits
+RedlockSemaphore semaphore = new RedlockSemaphore(
+ "api-limiter", 5, redisDrivers, config
+);
+
+// Acquire a permit
+if (semaphore.tryAcquire(5, TimeUnit.SECONDS)) {
+ try {
+ // Perform rate-limited operation
+ callExternalAPI();
+ } finally {
+ semaphore.release();
+ }
+}
+```
+
+### Redisson RedissonSemaphore
+
+**Purpose**: Distributed semaphore with pub/sub notification
+
+**Use Case**: General-purpose semaphore with async support and efficient waiting
+
+```java
+RSemaphore semaphore = redisson.getSemaphore("api-limiter");
+semaphore.trySetPermits(5);
+
+// Acquire a permit
+if (semaphore.tryAcquire(5, TimeUnit.SECONDS)) {
+ try {
+ // Perform rate-limited operation
+ callExternalAPI();
+ } finally {
+ semaphore.release();
+ }
+}
+```
+
+## Architecture & Data Model
+
+### redlock4j
+
+**Design**: Individual permit keys with quorum-based acquisition
+
+**Data Structure**:
+```
+{semaphoreKey}:permit:{permitId1} = {permitId1} (TTL: lockTimeout)
+{semaphoreKey}:permit:{permitId2} = {permitId2} (TTL: lockTimeout)
+{semaphoreKey}:permit:{permitId3} = {permitId3} (TTL: lockTimeout)
+...
+```
+
+**Key Characteristics**:
+- Each permit is a separate Redis key
+- Permit acquisition uses `SET NX` (same as lock)
+- Quorum required for each permit
+- No central counter
+- Thread-local state tracking
+
+**Architecture**:
+```
+RedlockSemaphore
+ ├─ semaphoreKey (base key)
+ ├─ maxPermits (configured limit)
+ ├─ List (quorum-based)
+ └─ ThreadLocal
+ ├─ List permitIds
+ ├─ acquisitionTime
+ └─ validityTime
+```
+
+### Redisson
+
+**Design**: Single counter with pub/sub notification
+
+**Data Structure**:
+```
+{semaphoreKey} = {availablePermits} (integer counter)
+redisson_sc:{semaphoreKey} = (pub/sub channel)
+```
+
+**Key Characteristics**:
+- Single Redis key stores available permit count
+- Uses `DECRBY` for acquisition, `INCRBY` for release
+- Pub/sub for efficient waiting
+- No quorum mechanism
+- Async/reactive support
+
+**Architecture**:
+```
+RedissonSemaphore
+ ├─ semaphoreKey (counter key)
+ ├─ channelName (pub/sub)
+ ├─ SemaphorePubSub (notification)
+ └─ No thread-local state
+```
+
+## Permit Acquisition Algorithm
+
+### redlock4j
+
+**Algorithm**: Create individual permit keys with quorum
+
+```java
+private SemaphoreResult attemptAcquire(int permits) {
+ List permitIds = new ArrayList<>();
+
+ // 1. For each permit needed
+ for (int i = 0; i < permits; i++) {
+ String permitId = generatePermitId();
+ 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++;
+ }
+ }
+
+ // 3. Check quorum for this permit
+ if (successfulNodes >= config.getQuorum()) {
+ permitIds.add(permitId);
+ } else {
+ // Failed - rollback all permits
+ releasePermits(permitIds);
+ return new SemaphoreResult(false, 0, new ArrayList<>());
+ }
+ }
+
+ // 4. Check validity time
+ long validityTime = timeout - elapsedTime - driftTime;
+ boolean acquired = permitIds.size() == permits && validityTime > 0;
+
+
+
+### Redisson
+
+**Algorithm**: Atomic counter decrement with Lua script
+
+```lua
+-- tryAcquireAsync0
+local value = redis.call('get', KEYS[1]);
+if (value ~= false and tonumber(value) >= tonumber(ARGV[1])) then
+ local val = redis.call('decrby', KEYS[1], ARGV[1]);
+ return 1;
+end;
+return 0;
+```
+
+**Flow**:
+1. Get current permit count
+2. Check if enough permits available
+3. Atomically decrement counter
+4. Return success/failure
+
+**Redis Operations** (for N permits):
+- 1 Lua script execution (atomic)
+- If waiting: pub/sub subscription + notifications
+
+**Waiting Mechanism**:
+```java
+public void acquire(int permits) throws InterruptedException {
+ if (tryAcquire(permits)) {
+ return; // Got it immediately
+ }
+
+ // Subscribe to notifications
+ CompletableFuture future = subscribe();
+ RedissonLockEntry entry = future.get();
+
+ try {
+ while (true) {
+ if (tryAcquire(permits)) {
+ return; // Got it
+ }
+
+ // Wait for notification
+ entry.getLatch().acquire();
+ }
+ } finally {
+ unsubscribe(entry);
+ }
+}
+```
+
+## Permit Release Algorithm
+
+### redlock4j
+
+**Algorithm**: Delete individual permit keys
+
+```java
+private void releasePermits(List permitIds) {
+ for (String permitId : permitIds) {
+ String permitKey = semaphoreKey + ":permit:" + permitId;
+
+ // Delete on all nodes
+ for (RedisDriver driver : redisDrivers) {
+ driver.deleteIfValueMatches(permitKey, permitId);
+ }
+ }
+}
+```
+
+**Characteristics**:
+- Delete each permit key individually
+- No notification to waiting threads
+- Waiting threads discover via polling
+- Quorum-based deletion
+
+**Redis Operations** (for N permits on M nodes):
+- N × M `DELETE` operations
+
+### Redisson
+
+**Algorithm**: Atomic counter increment with pub/sub notification
+
+```lua
+-- releaseAsync
+local value = redis.call('incrby', KEYS[1], ARGV[1]);
+redis.call(ARGV[2], KEYS[2], value);
+```
+
+**Characteristics**:
+- Increment counter atomically
+- Publish notification to channel
+- Waiting threads wake up immediately
+- Single Redis operation
+
+**Redis Operations** (for N permits):
+- 1 Lua script execution
+- 1 `PUBLISH` to channel
+
+**Notification Flow**:
+```
+Thread 1: acquire() → blocks → subscribes to channel
+Thread 2: release() → INCRBY + PUBLISH
+Thread 1: receives notification → wakes up → tryAcquire() → success
+```
+
+## Fairness & Ordering
+
+### redlock4j
+
+**Fairness**: Non-fair (no ordering guarantees)
+
+**Characteristics**:
+- Permits acquired in arbitrary order
+- No queue for waiting threads
+- Retry-based acquisition
+- First to successfully acquire wins
+
+**Example**:
+```
+Thread 1: tryAcquire() → retry → retry → success
+Thread 2: tryAcquire() → success (may acquire before Thread 1)
+Thread 3: tryAcquire() → retry → timeout
+```
+
+### Redisson
+
+**Fairness**: Non-fair (explicitly documented)
+
+**Characteristics**:
+- No FIFO ordering
+- Pub/sub wakes all waiters
+- Race to acquire after notification
+- First to execute Lua script wins
+
+**Example**:
+```
+Thread 1: acquire() → blocks → subscribes
+Thread 2: acquire() → blocks → subscribes
+Thread 3: release() → PUBLISH
+Thread 1 & 2: wake up → race to tryAcquire()
+Winner: unpredictable
+```
+
+**Note**: Redisson also provides `RedissonPermitExpirableSemaphore` for fair semaphores with FIFO ordering.
+
+## Permit Counting & Availability
+
+### redlock4j
+
+**Counting**: Implicit (count active permit keys)
+
+```java
+public int availablePermits() {
+ // Note: This would require counting active permits across all nodes
+ // Current implementation returns maxPermits (placeholder)
+ return maxPermits;
+}
+```
+
+**Challenges**:
+- No central counter
+- Would need to count keys matching pattern
+- Expensive operation (SCAN on all nodes)
+- Not implemented accurately
+
+**Actual Available Permits**:
+```
+Available = maxPermits - (number of active permit keys with quorum)
+```
+
+### Redisson
+
+**Counting**: Explicit counter
+
+```java
+public int availablePermits() {
+ return get(availablePermitsAsync());
+}
+
+// Implementation
+public RFuture availablePermitsAsync() {
+ return commandExecutor.writeAsync(
+ getRawName(), LongCodec.INSTANCE,
+ RedisCommands.GET_INTEGER, getRawName()
+ );
+}
+```
+
+**Characteristics**:
+- Single `GET` operation
+- Accurate and fast
+- O(1) complexity
+- Real-time availability
+
+## Initialization & Configuration
+
+### redlock4j
+
+**Initialization**: Implicit (no setup required)
+
+```java
+// Create and use immediately
+RedlockSemaphore semaphore = new RedlockSemaphore(
+ "api-limiter", 5, redisDrivers, config
+);
+
+// No need to set permits - maxPermits is just a limit
+semaphore.tryAcquire();
+```
+
+**Characteristics**:
+- `maxPermits` is a configuration parameter
+- No Redis initialization needed
+- Permits created on-demand
+- No way to "drain" or "reset" permits
+
+### Redisson
+
+**Initialization**: Explicit (must set permits)
+
+```java
+RSemaphore semaphore = redisson.getSemaphore("api-limiter");
+
+// Must initialize before use
+semaphore.trySetPermits(5);
+
+// Or add permits
+semaphore.addPermits(5);
+
+// Now can use
+semaphore.tryAcquire();
+```
+
+**Characteristics**:
+- Must explicitly set initial permits
+- `trySetPermits()` - sets only if not exists
+- `addPermits()` - adds to existing count
+- `drainPermits()` - removes all permits
+- Can reset/reconfigure at runtime
+
+**Additional Operations**:
+```java
+// Set permits with TTL
+semaphore.trySetPermits(5, Duration.ofMinutes(10));
+
+// Drain all permits
+int drained = semaphore.drainPermits();
+
+// Release even if not held (add permits)
+semaphore.addPermits(3);
+```
+
+## Timeout & Validity
+
+### redlock4j
+
+**Validity**: Per-acquisition validity time
+
+```java
+private static class PermitState {
+ final List permitIds;
+ final long acquisitionTime;
+ final long validityTime; // Calculated validity
+
+ boolean isValid() {
+ return System.currentTimeMillis() < acquisitionTime + validityTime;
+ }
+}
+```
+
+**Characteristics**:
+- Validity time calculated per acquisition
+- Clock drift compensation
+- Permits auto-expire via Redis TTL
+- Thread-local validity tracking
+
+**Validity Calculation**:
+```java
+long elapsedTime = System.currentTimeMillis() - startTime;
+long driftTime = (long) (timeout * clockDriftFactor) + 2;
+long validityTime = timeout - elapsedTime - driftTime;
+```
+
+### Redisson
+
+**Validity**: No automatic expiration
+
+**Characteristics**:
+- Permits don't expire automatically
+- Counter persists indefinitely
+- Can set TTL on semaphore key explicitly
+- No validity tracking per acquisition
+
+**Optional TTL**:
+```java
+// Set permits with expiration
+semaphore.trySetPermits(5, Duration.ofMinutes(10));
+
+// After 10 minutes, the entire semaphore key expires
+// All permits lost
+```
+
+
+## Performance Comparison
+
+### redlock4j
+
+**Acquisition** (N permits on M nodes):
+- N × M `SET NX` operations
+- Sequential per permit
+- Parallel across nodes
+- Rollback on failure: N × M `DELETE`
+
+**Release** (N permits on M nodes):
+- N × M `DELETE` operations
+- No notification overhead
+
+**Complexity**: O(N × M) per operation
+
+**Latency**:
+- Higher due to quorum requirement
+- Multiple round trips per permit
+- No pub/sub overhead
+
+**Example** (3 permits, 3 nodes):
+```
+Acquire:
+ Permit 1: SET on Node1, Node2, Node3 (parallel)
+ Permit 2: SET on Node1, Node2, Node3 (parallel)
+ Permit 3: SET on Node1, Node2, Node3 (parallel)
+Total: 9 operations
+
+Release:
+ DELETE permit1 on Node1, Node2, Node3
+ DELETE permit2 on Node1, Node2, Node3
+ DELETE permit3 on Node1, Node2, Node3
+Total: 9 operations
+```
+
+### Redisson
+
+**Acquisition** (N permits):
+- 1 Lua script execution (atomic)
+- If waiting: 1 pub/sub subscription
+- Notifications on release
+
+**Release** (N permits):
+- 1 Lua script execution
+- 1 `PUBLISH` to channel
+
+**Complexity**: O(1) per operation
+
+**Latency**:
+- Lower for single instance
+- Single round trip
+- Pub/sub notification overhead
+
+**Example** (3 permits):
+```
+Acquire:
+ 1 Lua script: GET + DECRBY
+ If blocked: subscribe to channel
+Total: 1-2 operations
+
+Release:
+ 1 Lua script: INCRBY + PUBLISH
+Total: 1 operation
+```
+
+## Safety & Correctness
+
+### redlock4j
+
+**Safety Guarantees**:
+- ✅ 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)
+
+**Consistency Model**:
+```
+Permit acquired if:
+ - Quorum of nodes have the permit key
+ - Validity time > 0
+ - No clock drift issues
+```
+
+### Redisson
+
+**Safety Guarantees**:
+- ✅ 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
+
+**Consistency Model**:
+```
+Permit acquired if:
+ - Counter >= requested permits
+ - Atomic decrement succeeds
+ - No distributed consistency
+```
+
+## Use Case Comparison
+
+### redlock4j RedlockSemaphore
+
+**Best For**:
+- Distributed systems requiring quorum-based safety
+- Rate limiting with strong consistency
+- Scenarios where permit expiration is critical
+- Multi-master Redis setups
+- Fault-tolerant resource pooling
+
+**Example Scenarios**:
+```java
+// API rate limiting with fault tolerance
+RedlockSemaphore apiLimiter = new RedlockSemaphore(
+ "api:external:rate-limit", 100, redisDrivers, config
+);
+
+// Database connection pool with auto-expiration
+RedlockSemaphore dbPool = new RedlockSemaphore(
+ "db:connection:pool", 50, redisDrivers, config
+);
+
+// Distributed job throttling
+RedlockSemaphore jobThrottle = new RedlockSemaphore(
+ "jobs:concurrent-limit", 10, redisDrivers, config
+);
+```
+
+### Redisson RedissonSemaphore
+
+**Best For**:
+- Single Redis instance deployments
+- High-throughput rate limiting
+- Scenarios requiring accurate permit counting
+- Applications needing async/reactive APIs
+- Dynamic permit management
+
+**Example Scenarios**:
+```java
+// High-throughput API rate limiting
+RSemaphore apiLimiter = redisson.getSemaphore("api:rate-limit");
+apiLimiter.trySetPermits(1000);
+
+// Resource pool with dynamic sizing
+RSemaphore resourcePool = redisson.getSemaphore("resource:pool");
+resourcePool.addPermits(50); // Can adjust at runtime
+
+// Async rate limiting
+RSemaphore asyncLimiter = redisson.getSemaphore("async:limiter");
+asyncLimiter.trySetPermits(100);
+RFuture future = asyncLimiter.tryAcquireAsync(5, TimeUnit.SECONDS);
+```
+
+## Complexity Analysis
+
+### redlock4j
+
+**Code Complexity**: ~370 lines
+
+**Pros**:
+- ✅ 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
+
+### 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)
+
+**Cons**:
+- ❌ 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 |
+
+## 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
+
+### 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
+
+### For Fair Semaphores:
+
+- Use Redisson's `RedissonPermitExpirableSemaphore` for FIFO ordering
+- redlock4j doesn't currently provide fair semaphore
+
+## Migration Considerations
+
+### From Redisson to redlock4j
+
+```java
+// Before (Redisson)
+RSemaphore semaphore = redisson.getSemaphore("api-limiter");
+semaphore.trySetPermits(5);
+if (semaphore.tryAcquire(5, TimeUnit.SECONDS)) {
+ try {
+ // work
+ } finally {
+ semaphore.release();
+ }
+}
+
+// After (redlock4j)
+RedlockSemaphore semaphore = new RedlockSemaphore(
+ "api-limiter", 5, redisDrivers, config
+);
+if (semaphore.tryAcquire(5, TimeUnit.SECONDS)) {
+ try {
+ // work
+ } finally {
+ semaphore.release();
+ }
+}
+```
+
+**Benefits**:
+- Quorum-based safety
+- Fault tolerance
+- Automatic expiration
+
+**Considerations**:
+- Higher latency
+- No accurate permit counting
+- No dynamic permit management
+
+### From redlock4j to Redisson
+
+```java
+// Before (redlock4j)
+RedlockSemaphore semaphore = new RedlockSemaphore(
+ "api-limiter", 5, redisDrivers, config
+);
+
+// After (Redisson)
+RSemaphore semaphore = redisson.getSemaphore("api-limiter");
+semaphore.trySetPermits(5);
+```
+
+**Benefits**:
+- Lower latency
+- Accurate permit counting
+- Async/reactive support
+- Dynamic permit management
+
+**Considerations**:
+- Single point of failure
+- Must initialize explicitly
+- No automatic expiration
+
+## Conclusion
+
+Both implementations serve different purposes:
+
+**redlock4j RedlockSemaphore**:
+- Designed for distributed consistency with quorum-based safety
+- Individual permit keys with automatic expiration
+- Higher latency but fault-tolerant
+- Simpler initialization, limited management
+- Best for multi-master setups requiring strong consistency
+
+**Redisson RedissonSemaphore**:
+- Designed for high-performance single-instance deployments
+- Atomic counter with pub/sub notifications
+- Lower latency but single point of failure
+- Rich API with dynamic permit management
+- Best for high-throughput scenarios with single Redis instance
+
+Choose based on your specific requirements:
+- **Distributed consistency & fault tolerance** → redlock4j RedlockSemaphore
+- **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/pom.xml b/pom.xml
index 9b71426..8b26911 100644
--- a/pom.xml
+++ b/pom.xml
@@ -191,6 +191,27 @@
false
+
+
+
+ net.revelc.code.formatter
+ formatter-maven-plugin
+ 2.16.0
+
+
+ ${project.build.sourceDirectory}
+ ${project.build.testSourceDirectory}
+
+ formatting.xml
+
+
+
+
+ validate
+
+
+
+
diff --git a/src/main/java/org/codarama/redlock4j/FairLock.java b/src/main/java/org/codarama/redlock4j/FairLock.java
index 023b591..6b2aed6 100644
--- a/src/main/java/org/codarama/redlock4j/FairLock.java
+++ b/src/main/java/org/codarama/redlock4j/FairLock.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j;
@@ -35,33 +16,37 @@
import java.util.concurrent.locks.Lock;
/**
- * A distributed fair lock implementation that ensures FIFO (First-In-First-Out) ordering
- * for lock acquisition. This lock uses Redis sorted sets to maintain a queue of waiters,
- * ensuring that threads acquire the lock in the order they requested it.
+ * A distributed fair lock implementation that ensures FIFO (First-In-First-Out) ordering for lock acquisition. This
+ * lock uses Redis sorted sets to maintain a queue of waiters, ensuring that threads acquire the lock in the order they
+ * requested it.
*
- *
The fair lock provides stronger ordering guarantees than the standard Redlock but
- * may have slightly lower throughput due to the additional coordination required.
+ *
+ * The fair lock provides stronger ordering guarantees than the standard Redlock but may have slightly lower throughput
+ * due to the additional coordination required.
+ *
*
- *
Implementation Details:
+ *
+ * Implementation Details:
+ *
*
- *
Uses Redis sorted sets with timestamps to maintain FIFO order
- *
Each waiter is assigned a unique token and timestamp
- *
Only the waiter with the lowest timestamp can acquire the lock
- *
Automatic cleanup of expired waiters
+ *
Uses Redis sorted sets with timestamps to maintain FIFO order
+ *
Each waiter is assigned a unique token and timestamp
+ *
Only the waiter with the lowest timestamp can acquire the lock
+ *
Automatic cleanup of expired waiters
*
*/
public class FairLock implements Lock {
private static final Logger logger = LoggerFactory.getLogger(FairLock.class);
-
+
private final String lockKey;
private final String queueKey;
private final List redisDrivers;
private final RedlockConfiguration config;
private final SecureRandom secureRandom;
-
+
// Thread-local storage for lock state
private final ThreadLocal lockState = new ThreadLocal<>();
-
+
private static class LockState {
final String lockValue;
final String queueToken;
@@ -89,7 +74,7 @@ int decrementHoldCount() {
return --holdCount;
}
}
-
+
public FairLock(String lockKey, List redisDrivers, RedlockConfiguration config) {
this.lockKey = lockKey;
this.queueKey = lockKey + ":queue";
@@ -97,7 +82,7 @@ public FairLock(String lockKey, List redisDrivers, RedlockConfigura
this.config = config;
this.secureRandom = new SecureRandom();
}
-
+
@Override
public void lock() {
try {
@@ -109,14 +94,14 @@ public void lock() {
throw new RedlockException("Interrupted while acquiring fair lock: " + lockKey, e);
}
}
-
+
@Override
public void lockInterruptibly() throws InterruptedException {
if (!tryLock(config.getLockAcquisitionTimeoutMs(), TimeUnit.MILLISECONDS)) {
throw new RedlockException("Failed to acquire fair lock within timeout: " + lockKey);
}
}
-
+
@Override
public boolean tryLock() {
try {
@@ -126,15 +111,14 @@ public boolean tryLock() {
return false;
}
}
-
+
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
// Check if current thread already holds the lock (reentrancy)
LockState currentState = lockState.get();
if (currentState != null && currentState.isValid()) {
currentState.incrementHoldCount();
- logger.debug("Reentrant fair lock acquisition for {} (hold count: {})",
- lockKey, currentState.holdCount);
+ logger.debug("Reentrant fair lock acquisition for {} (hold count: {})", lockKey, currentState.holdCount);
return true;
}
@@ -158,10 +142,9 @@ public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
// Try to acquire the lock
LockResult result = attemptLock();
if (result.isAcquired()) {
- lockState.set(new LockState(result.getLockValue(), queueToken,
- System.currentTimeMillis(), result.getValidityTimeMs()));
- logger.debug("Successfully acquired fair lock {} on attempt {}",
- lockKey, attempt + 1);
+ lockState.set(new LockState(result.getLockValue(), queueToken, System.currentTimeMillis(),
+ result.getValidityTimeMs()));
+ logger.debug("Successfully acquired fair lock {} on attempt {}", lockKey, attempt + 1);
return true;
}
}
@@ -186,7 +169,7 @@ public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
throw e;
}
}
-
+
@Override
public void unlock() {
LockState state = lockState.get();
@@ -215,10 +198,9 @@ public void unlock() {
lockState.remove();
logger.debug("Successfully released fair lock {}", lockKey);
}
-
+
/**
- * Adds a token to the queue with the given timestamp.
- * Uses Redis sorted sets (ZADD) to maintain FIFO ordering.
+ * Adds a token to the queue with the given timestamp. Uses Redis sorted sets (ZADD) to maintain FIFO ordering.
*/
private void addToQueue(String token, long timestamp) {
int successfulNodes = 0;
@@ -238,13 +220,12 @@ private void addToQueue(String token, long timestamp) {
long expirationThreshold = System.currentTimeMillis() - config.getDefaultLockTimeoutMs() * 2;
cleanupExpiredQueueEntries(expirationThreshold);
- logger.debug("Added token {} to queue {} with timestamp {} on {}/{} nodes",
- token, queueKey, timestamp, successfulNodes, redisDrivers.size());
+ logger.debug("Added token {} to queue {} with timestamp {} on {}/{} nodes", token, queueKey, timestamp,
+ successfulNodes, redisDrivers.size());
}
/**
- * Removes a token from the queue.
- * Uses Redis sorted sets (ZREM) to remove the token.
+ * Removes a token from the queue. Uses Redis sorted sets (ZREM) to remove the token.
*/
private void removeFromQueue(String token) {
int successfulNodes = 0;
@@ -259,13 +240,12 @@ private void removeFromQueue(String token) {
}
}
- logger.debug("Removed token {} from queue {} on {}/{} nodes",
- token, queueKey, successfulNodes, redisDrivers.size());
+ logger.debug("Removed token {} from queue {} on {}/{} nodes", token, queueKey, successfulNodes,
+ redisDrivers.size());
}
/**
- * Checks if the given token is at the front of the queue.
- * Uses Redis sorted sets (ZRANGE) to get the first element.
+ * Checks if the given token is at the front of the queue. Uses Redis sorted sets (ZRANGE) to get the first element.
*/
private boolean isAtFrontOfQueue(String token) {
int votesForFront = 0;
@@ -285,15 +265,14 @@ private boolean isAtFrontOfQueue(String token) {
// Require quorum agreement that we're at the front
boolean atFront = votesForFront >= config.getQuorum();
- logger.debug("Token {} is {} at front of queue (votes: {}/{})",
- token, atFront ? "" : "NOT", votesForFront, redisDrivers.size());
+ logger.debug("Token {} is {} at front of queue (votes: {}/{})", token, atFront ? "" : "NOT", votesForFront,
+ redisDrivers.size());
return atFront;
}
/**
- * Cleans up expired entries from the queue.
- * Removes entries with timestamps older than the threshold.
+ * Cleans up expired entries from the queue. Removes entries with timestamps older than the threshold.
*/
private void cleanupExpiredQueueEntries(long expirationThreshold) {
for (RedisDriver driver : redisDrivers) {
@@ -308,12 +287,12 @@ private void cleanupExpiredQueueEntries(long expirationThreshold) {
}
}
}
-
+
private LockResult attemptLock() {
String lockValue = generateToken();
long startTime = System.currentTimeMillis();
int successfulNodes = 0;
-
+
for (RedisDriver driver : redisDrivers) {
try {
if (driver.setIfNotExists(lockKey, lockValue, config.getDefaultLockTimeoutMs())) {
@@ -323,20 +302,20 @@ private LockResult attemptLock() {
logger.debug("Failed to acquire lock on {}: {}", driver.getIdentifier(), e.getMessage());
}
}
-
+
long elapsedTime = System.currentTimeMillis() - startTime;
long driftTime = (long) (config.getDefaultLockTimeoutMs() * config.getClockDriftFactor()) + 2;
long validityTime = config.getDefaultLockTimeoutMs() - elapsedTime - driftTime;
-
+
boolean acquired = successfulNodes >= config.getQuorum() && validityTime > 0;
-
+
if (!acquired) {
releaseLock(lockValue);
}
-
+
return new LockResult(acquired, validityTime, lockValue, successfulNodes, redisDrivers.size());
}
-
+
private void releaseLock(String lockValue) {
for (RedisDriver driver : redisDrivers) {
try {
@@ -346,7 +325,7 @@ private void releaseLock(String lockValue) {
}
}
}
-
+
private String generateToken() {
byte[] bytes = new byte[20];
secureRandom.nextBytes(bytes);
@@ -356,12 +335,12 @@ private String generateToken() {
}
return sb.toString();
}
-
+
@Override
public Condition newCondition() {
throw new UnsupportedOperationException("Conditions are not supported by distributed fair locks");
}
-
+
/**
* Checks if the current thread holds this lock.
*/
@@ -369,7 +348,7 @@ public boolean isHeldByCurrentThread() {
LockState state = lockState.get();
return state != null && state.isValid();
}
-
+
/**
* Gets the remaining validity time of the lock.
*/
@@ -380,7 +359,7 @@ public long getRemainingValidityTime() {
}
return (state.acquisitionTime + state.validityTime) - System.currentTimeMillis();
}
-
+
/**
* Gets the hold count for this lock.
*/
@@ -389,4 +368,3 @@ public int getHoldCount() {
return state != null ? state.holdCount : 0;
}
}
-
diff --git a/src/main/java/org/codarama/redlock4j/LockResult.java b/src/main/java/org/codarama/redlock4j/LockResult.java
index 9654fa7..1a33e76 100644
--- a/src/main/java/org/codarama/redlock4j/LockResult.java
+++ b/src/main/java/org/codarama/redlock4j/LockResult.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j;
@@ -32,7 +13,7 @@ public class LockResult {
private final String lockValue;
private final int successfulNodes;
private final int totalNodes;
-
+
public LockResult(boolean acquired, long validityTimeMs, String lockValue, int successfulNodes, int totalNodes) {
this.acquired = acquired;
this.validityTimeMs = validityTimeMs;
@@ -40,49 +21,45 @@ public LockResult(boolean acquired, long validityTimeMs, String lockValue, int s
this.successfulNodes = successfulNodes;
this.totalNodes = totalNodes;
}
-
+
/**
* @return true if the lock was successfully acquired
*/
public boolean isAcquired() {
return acquired;
}
-
+
/**
* @return the remaining validity time of the lock in milliseconds
*/
public long getValidityTimeMs() {
return validityTimeMs;
}
-
+
/**
* @return the unique value associated with this lock
*/
public String getLockValue() {
return lockValue;
}
-
+
/**
* @return the number of Redis nodes that successfully acquired the lock
*/
public int getSuccessfulNodes() {
return successfulNodes;
}
-
+
/**
* @return the total number of Redis nodes
*/
public int getTotalNodes() {
return totalNodes;
}
-
+
@Override
public String toString() {
- return "LockResult{" +
- "acquired=" + acquired +
- ", validityTimeMs=" + validityTimeMs +
- ", successfulNodes=" + successfulNodes +
- ", totalNodes=" + totalNodes +
- '}';
+ return "LockResult{" + "acquired=" + acquired + ", validityTimeMs=" + validityTimeMs + ", successfulNodes="
+ + successfulNodes + ", totalNodes=" + totalNodes + '}';
}
}
diff --git a/src/main/java/org/codarama/redlock4j/MultiLock.java b/src/main/java/org/codarama/redlock4j/MultiLock.java
index ad6b9c2..70623f8 100644
--- a/src/main/java/org/codarama/redlock4j/MultiLock.java
+++ b/src/main/java/org/codarama/redlock4j/MultiLock.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j;
@@ -36,49 +17,54 @@
import java.util.stream.Collectors;
/**
- * A distributed multi-lock implementation that allows atomic acquisition of multiple
- * resources. This prevents deadlocks by always acquiring locks in a consistent order
- * (lexicographically sorted by key).
+ * A distributed multi-lock implementation that allows atomic acquisition of multiple resources. This prevents deadlocks
+ * by always acquiring locks in a consistent order (lexicographically sorted by key).
*
- *
The MultiLock is useful when you need to perform operations that span multiple
- * resources and require exclusive access to all of them simultaneously.
+ *
+ * The MultiLock is useful when you need to perform operations that span multiple resources and require exclusive access
+ * to all of them simultaneously.
+ *
*
- *
Key Features:
+ *
+ * Key Features:
+ *
*
- *
Atomic acquisition of multiple locks
- *
Deadlock prevention through consistent ordering
- *
All-or-nothing semantics: either all locks are acquired or none
- *
Automatic cleanup on failure
+ *
Atomic acquisition of multiple locks
+ *
Deadlock prevention through consistent ordering
+ *
All-or-nothing semantics: either all locks are acquired or none
*/
public class MultiLock implements Lock {
private static final Logger logger = LoggerFactory.getLogger(MultiLock.class);
-
+
private final List lockKeys;
private final List redisDrivers;
private final RedlockConfiguration config;
private final SecureRandom secureRandom;
-
+
// Thread-local storage for lock state
private final ThreadLocal lockState = new ThreadLocal<>();
-
+
private static class LockState {
final Map lockValues; // key -> lockValue
final long acquisitionTime;
@@ -104,32 +90,32 @@ int decrementHoldCount() {
return --holdCount;
}
}
-
+
/**
* Creates a new MultiLock for the specified resources.
*
- * @param lockKeys the keys to lock (will be sorted internally to prevent deadlocks)
- * @param redisDrivers the Redis drivers to use
- * @param config the Redlock configuration
+ * @param lockKeys
+ * the keys to lock (will be sorted internally to prevent deadlocks)
+ * @param redisDrivers
+ * the Redis drivers to use
+ * @param config
+ * the Redlock configuration
*/
public MultiLock(List lockKeys, List redisDrivers, RedlockConfiguration config) {
if (lockKeys == null || lockKeys.isEmpty()) {
throw new IllegalArgumentException("Lock keys cannot be null or empty");
}
-
+
// Sort keys to ensure consistent ordering and prevent deadlocks
- this.lockKeys = lockKeys.stream()
- .distinct()
- .sorted()
- .collect(Collectors.toList());
-
+ this.lockKeys = lockKeys.stream().distinct().sorted().collect(Collectors.toList());
+
this.redisDrivers = redisDrivers;
this.config = config;
this.secureRandom = new SecureRandom();
-
+
logger.debug("Created MultiLock for {} resources: {}", this.lockKeys.size(), this.lockKeys);
}
-
+
@Override
public void lock() {
try {
@@ -141,14 +127,14 @@ public void lock() {
throw new RedlockException("Interrupted while acquiring multi-lock for keys: " + lockKeys, e);
}
}
-
+
@Override
public void lockInterruptibly() throws InterruptedException {
if (!tryLock(config.getLockAcquisitionTimeoutMs(), TimeUnit.MILLISECONDS)) {
throw new RedlockException("Failed to acquire multi-lock within timeout for keys: " + lockKeys);
}
}
-
+
@Override
public boolean tryLock() {
try {
@@ -158,15 +144,15 @@ public boolean tryLock() {
return false;
}
}
-
+
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
// Check if current thread already holds the lock (reentrancy)
LockState currentState = lockState.get();
if (currentState != null && currentState.isValid()) {
currentState.incrementHoldCount();
- logger.debug("Reentrant multi-lock acquisition for {} keys (hold count: {})",
- lockKeys.size(), currentState.holdCount);
+ logger.debug("Reentrant multi-lock acquisition for {} keys (hold count: {})", lockKeys.size(),
+ currentState.holdCount);
return true;
}
@@ -180,10 +166,10 @@ public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
MultiLockResult result = attemptMultiLock();
if (result.isAcquired()) {
- lockState.set(new LockState(result.getLockValues(), System.currentTimeMillis(),
- result.getValidityTimeMs()));
- logger.debug("Successfully acquired multi-lock for {} keys on attempt {}",
- lockKeys.size(), attempt + 1);
+ lockState.set(
+ new LockState(result.getLockValues(), System.currentTimeMillis(), result.getValidityTimeMs()));
+ logger.debug("Successfully acquired multi-lock for {} keys on attempt {}", lockKeys.size(),
+ attempt + 1);
return true;
}
@@ -201,7 +187,7 @@ public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
-
+
@Override
public void unlock() {
LockState state = lockState.get();
@@ -228,19 +214,19 @@ public void unlock() {
lockState.remove();
logger.debug("Successfully released multi-lock for {} keys", lockKeys.size());
}
-
+
/**
* Attempts to acquire all locks atomically.
*/
private MultiLockResult attemptMultiLock() {
Map lockValues = new HashMap<>();
long startTime = System.currentTimeMillis();
-
+
// Generate unique lock values for each key
for (String key : lockKeys) {
lockValues.put(key, generateLockValue());
}
-
+
// Try to acquire all locks on each Redis node
int successfulNodes = 0;
for (RedisDriver driver : redisDrivers) {
@@ -248,27 +234,27 @@ private MultiLockResult attemptMultiLock() {
successfulNodes++;
}
}
-
+
long elapsedTime = System.currentTimeMillis() - startTime;
long driftTime = (long) (config.getDefaultLockTimeoutMs() * config.getClockDriftFactor()) + 2;
long validityTime = config.getDefaultLockTimeoutMs() - elapsedTime - driftTime;
-
+
boolean acquired = successfulNodes >= config.getQuorum() && validityTime > 0;
-
+
if (!acquired) {
// Release any locks we managed to acquire
releaseAllLocks(lockValues);
}
-
+
return new MultiLockResult(acquired, validityTime, lockValues, successfulNodes, redisDrivers.size());
}
-
+
/**
* Attempts to acquire all locks on a single Redis node.
*/
private boolean acquireAllOnNode(RedisDriver driver, Map lockValues) {
List acquiredKeys = new ArrayList<>();
-
+
try {
// Try to acquire each lock in order
for (String key : lockKeys) {
@@ -288,7 +274,7 @@ private boolean acquireAllOnNode(RedisDriver driver, Map lockVal
return false;
}
}
-
+
/**
* Rolls back locks acquired on a single node.
*/
@@ -297,12 +283,11 @@ private void rollbackOnNode(RedisDriver driver, Map lockValues,
try {
driver.deleteIfValueMatches(key, lockValues.get(key));
} catch (Exception e) {
- logger.warn("Failed to rollback lock {} on {}: {}",
- key, driver.getIdentifier(), e.getMessage());
+ logger.warn("Failed to rollback lock {} on {}: {}", key, driver.getIdentifier(), e.getMessage());
}
}
}
-
+
/**
* Releases all locks across all nodes.
*/
@@ -312,13 +297,13 @@ private void releaseAllLocks(Map lockValues) {
try {
driver.deleteIfValueMatches(entry.getKey(), entry.getValue());
} catch (Exception e) {
- logger.warn("Failed to release lock {} on {}: {}",
- entry.getKey(), driver.getIdentifier(), e.getMessage());
+ logger.warn("Failed to release lock {} on {}: {}", entry.getKey(), driver.getIdentifier(),
+ e.getMessage());
}
}
}
}
-
+
private String generateLockValue() {
byte[] bytes = new byte[20];
secureRandom.nextBytes(bytes);
@@ -328,12 +313,12 @@ private String generateLockValue() {
}
return sb.toString();
}
-
+
@Override
public Condition newCondition() {
throw new UnsupportedOperationException("Conditions are not supported by distributed multi-locks");
}
-
+
/**
* Result of a multi-lock acquisition attempt.
*/
@@ -344,8 +329,8 @@ private static class MultiLockResult {
private final int successfulNodes;
private final int totalNodes;
- MultiLockResult(boolean acquired, long validityTimeMs, Map lockValues,
- int successfulNodes, int totalNodes) {
+ MultiLockResult(boolean acquired, long validityTimeMs, Map lockValues, int successfulNodes,
+ int totalNodes) {
this.acquired = acquired;
this.validityTimeMs = validityTimeMs;
this.lockValues = lockValues;
@@ -366,4 +351,3 @@ Map getLockValues() {
}
}
}
-
diff --git a/src/main/java/org/codarama/redlock4j/Redlock.java b/src/main/java/org/codarama/redlock4j/Redlock.java
index cef2336..5e03390 100644
--- a/src/main/java/org/codarama/redlock4j/Redlock.java
+++ b/src/main/java/org/codarama/redlock4j/Redlock.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j;
@@ -41,15 +22,15 @@
*/
public class Redlock implements Lock {
private static final Logger logger = LoggerFactory.getLogger(Redlock.class);
-
+
private final String lockKey;
private final List redisDrivers;
private final RedlockConfiguration config;
private final SecureRandom secureRandom;
-
+
// Thread-local storage for lock state
private final ThreadLocal lockState = new ThreadLocal<>();
-
+
private static class LockState {
final String lockValue;
final long acquisitionTime;
@@ -75,14 +56,14 @@ int decrementHoldCount() {
return --holdCount;
}
}
-
+
public Redlock(String lockKey, List redisDrivers, RedlockConfiguration config) {
this.lockKey = lockKey;
this.redisDrivers = redisDrivers;
this.config = config;
this.secureRandom = new SecureRandom();
}
-
+
@Override
public void lock() {
try {
@@ -94,14 +75,14 @@ public void lock() {
throw new RedlockException("Interrupted while acquiring lock: " + lockKey, e);
}
}
-
+
@Override
public void lockInterruptibly() throws InterruptedException {
if (!tryLock(config.getLockAcquisitionTimeoutMs(), TimeUnit.MILLISECONDS)) {
throw new RedlockException("Failed to acquire lock within timeout: " + lockKey);
}
}
-
+
@Override
public boolean tryLock() {
try {
@@ -111,7 +92,7 @@ public boolean tryLock() {
return false;
}
}
-
+
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
// Check if current thread already holds the lock (reentrancy)
@@ -132,7 +113,8 @@ public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
LockResult result = attemptLock();
if (result.isAcquired()) {
- lockState.set(new LockState(result.getLockValue(), System.currentTimeMillis(), result.getValidityTimeMs()));
+ lockState.set(
+ new LockState(result.getLockValue(), System.currentTimeMillis(), result.getValidityTimeMs()));
logger.debug("Successfully acquired lock {} on attempt {}", lockKey, attempt + 1);
return true;
}
@@ -153,12 +135,12 @@ public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
logger.debug("Failed to acquire lock {} after {} attempts", lockKey, config.getMaxRetryAttempts() + 1);
return false;
}
-
+
private LockResult attemptLock() {
String lockValue = generateLockValue();
long startTime = System.currentTimeMillis();
int successfulNodes = 0;
-
+
// Try to acquire the lock on all nodes
for (RedisDriver driver : redisDrivers) {
try {
@@ -169,21 +151,21 @@ private LockResult attemptLock() {
logger.warn("Failed to acquire lock on {}: {}", driver.getIdentifier(), e.getMessage());
}
}
-
+
long elapsedTime = System.currentTimeMillis() - startTime;
long driftTime = (long) (config.getDefaultLockTimeoutMs() * config.getClockDriftFactor()) + 2;
long validityTime = config.getDefaultLockTimeoutMs() - elapsedTime - driftTime;
-
+
boolean acquired = successfulNodes >= config.getQuorum() && validityTime > 0;
-
+
if (!acquired) {
// Release any locks we managed to acquire
releaseLock(lockValue);
}
-
+
return new LockResult(acquired, validityTime, lockValue, successfulNodes, redisDrivers.size());
}
-
+
@Override
public void unlock() {
LockState state = lockState.get();
@@ -210,7 +192,7 @@ public void unlock() {
lockState.remove();
logger.debug("Successfully released lock {}", lockKey);
}
-
+
private void releaseLock(String lockValue) {
for (RedisDriver driver : redisDrivers) {
try {
@@ -220,7 +202,7 @@ private void releaseLock(String lockValue) {
}
}
}
-
+
private String generateLockValue() {
byte[] bytes = new byte[20];
secureRandom.nextBytes(bytes);
@@ -230,12 +212,12 @@ private String generateLockValue() {
}
return sb.toString();
}
-
+
@Override
public Condition newCondition() {
throw new UnsupportedOperationException("Conditions are not supported by distributed locks");
}
-
+
/**
* Checks if the current thread holds this lock.
*
@@ -245,7 +227,7 @@ public boolean isHeldByCurrentThread() {
LockState state = lockState.get();
return state != null && state.isValid();
}
-
+
/**
* Gets the remaining validity time of the lock for the current thread.
*
@@ -261,8 +243,8 @@ public long getRemainingValidityTime() {
}
/**
- * Gets the hold count for the current thread.
- * This indicates how many times the current thread has acquired this lock.
+ * Gets the hold count for the current thread. This indicates how many times the current thread has acquired this
+ * lock.
*
* @return hold count, or 0 if not held by current thread
*/
@@ -274,23 +256,25 @@ public int getHoldCount() {
/**
* Extends the validity time of the lock held by the current thread.
*
- * This method attempts to extend the lock on a quorum of Redis nodes using the same
- * lock value. The extension is only successful if:
+ * This method attempts to extend the lock on a quorum of Redis nodes using the same lock value. The extension is
+ * only successful if:
*
- *
The current thread holds a valid lock
- *
The extension succeeds on at least a quorum (N/2+1) of nodes
- *
The new validity time (after accounting for clock drift) is positive
+ *
The current thread holds a valid lock
+ *
The extension succeeds on at least a quorum (N/2+1) of nodes
+ *
The new validity time (after accounting for clock drift) is positive
*
*
* Important limitations:
*
- *
Lock extension is for efficiency, not correctness
- *
Should not be used as a substitute for proper timeout configuration
+ *
Lock extension is for efficiency, not correctness
+ *
Should not be used as a substitute for proper timeout configuration
*
*
- * @param additionalTimeMs additional time in milliseconds to extend the lock
+ * @param additionalTimeMs
+ * additional time in milliseconds to extend the lock
* @return true if the lock was successfully extended on a quorum of nodes, false otherwise
- * @throws IllegalArgumentException if additionalTimeMs is negative or zero
+ * @throws IllegalArgumentException
+ * if additionalTimeMs is negative or zero
*/
public boolean extend(long additionalTimeMs) {
if (additionalTimeMs <= 0) {
@@ -331,11 +315,11 @@ public boolean extend(long additionalTimeMs) {
LockState newState = new LockState(state.lockValue, System.currentTimeMillis(), newValidityTime);
newState.holdCount = state.holdCount; // Preserve hold count
lockState.set(newState);
- logger.debug("Successfully extended lock {} on {}/{} nodes (new validity: {}ms)",
- lockKey, successfulNodes, redisDrivers.size(), newValidityTime);
+ logger.debug("Successfully extended lock {} on {}/{} nodes (new validity: {}ms)", lockKey, successfulNodes,
+ redisDrivers.size(), newValidityTime);
} else {
- logger.debug("Failed to extend lock {} - only {}/{} nodes succeeded (quorum: {})",
- lockKey, successfulNodes, redisDrivers.size(), config.getQuorum());
+ logger.debug("Failed to extend lock {} - only {}/{} nodes succeeded (quorum: {})", lockKey, successfulNodes,
+ redisDrivers.size(), config.getQuorum());
}
return extended;
diff --git a/src/main/java/org/codarama/redlock4j/RedlockCountDownLatch.java b/src/main/java/org/codarama/redlock4j/RedlockCountDownLatch.java
index e38fa0e..968a06f 100644
--- a/src/main/java/org/codarama/redlock4j/RedlockCountDownLatch.java
+++ b/src/main/java/org/codarama/redlock4j/RedlockCountDownLatch.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j;
@@ -34,52 +15,61 @@
import java.util.concurrent.atomic.AtomicBoolean;
/**
- * A distributed countdown latch that allows one or more threads to wait until a set of
- * operations being performed in other threads completes. This is the distributed equivalent
- * of {@link java.util.concurrent.CountDownLatch}.
+ * A distributed countdown latch that allows one or more threads to wait until a set of operations being performed in
+ * other threads completes. This is the distributed equivalent of {@link java.util.concurrent.CountDownLatch}.
*
- *
Key Features:
+ *
+ * Key Features:
+ *
*
- *
Initialized with a count value
- *
Threads can wait for the count to reach zero
- *
Other threads decrement the count by calling countDown()
- *
Once the count reaches zero, all waiting threads are released
- *
The count cannot be reset (one-time use)
+ *
Initialized with a count value
+ *
Threads can wait for the count to reach zero
+ *
Other threads decrement the count by calling countDown()
+ *
Once the count reaches zero, all waiting threads are released
+ *
The count cannot be reset (one-time use)
*
*
- *
Use Cases:
+ *
+ * Use Cases:
+ *
*
- *
Coordinating startup: Wait for all services to initialize
- *
Batch processing: Wait for all workers to complete
+ * new Thread(() -> {
+ * initializeService3();
+ * latch.countDown(); // Decrement count
+ * }).start();
+ *
+ * // Main thread waits for all services
+ * latch.await(); // Blocks until count reaches 0
+ * System.out.println("All services initialized!");
+ * }
+ *
*/
public class RedlockCountDownLatch {
private static final Logger logger = LoggerFactory.getLogger(RedlockCountDownLatch.class);
@@ -91,17 +81,21 @@ public class RedlockCountDownLatch {
private final RedlockConfiguration config;
private final AtomicBoolean subscribed = new AtomicBoolean(false);
private volatile CountDownLatch localLatch;
-
+
/**
* Creates a new distributed countdown latch.
*
- * @param latchKey the key for this latch
- * @param count the initial count (must be positive)
- * @param redisDrivers the Redis drivers to use
- * @param config the Redlock configuration
+ * @param latchKey
+ * the key for this latch
+ * @param count
+ * the initial count (must be positive)
+ * @param redisDrivers
+ * the Redis drivers to use
+ * @param config
+ * the Redlock configuration
*/
public RedlockCountDownLatch(String latchKey, int count, List redisDrivers,
- RedlockConfiguration config) {
+ RedlockConfiguration config) {
if (count < 0) {
throw new IllegalArgumentException("Count cannot be negative");
}
@@ -118,30 +112,37 @@ public RedlockCountDownLatch(String latchKey, int count, List redis
logger.debug("Created RedlockCountDownLatch {} with count {}", latchKey, count);
}
-
+
/**
* Causes the current thread to wait until the latch has counted down to zero.
*
- *
If the current count is zero then this method returns immediately.
+ *
+ * If the current count is zero then this method returns immediately.
+ *
*
- *
If the current count is greater than zero then the current thread becomes
- * disabled for thread scheduling purposes and lies dormant until the count reaches
- * zero due to invocations of the {@link #countDown} method.
+ *
+ * If the current count is greater than zero then the current thread becomes disabled for thread scheduling purposes
+ * and lies dormant until the count reaches zero due to invocations of the {@link #countDown} method.
+ *
*
- * @throws InterruptedException if the current thread is interrupted while waiting
+ * @throws InterruptedException
+ * if the current thread is interrupted while waiting
*/
public void await() throws InterruptedException {
await(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
}
-
+
/**
- * Causes the current thread to wait until the latch has counted down to zero,
- * unless the specified waiting time elapses.
+ * Causes the current thread to wait until the latch has counted down to zero, unless the specified waiting time
+ * elapses.
*
- * @param timeout the maximum time to wait
- * @param unit the time unit of the timeout
+ * @param timeout
+ * the maximum time to wait
+ * @param unit
+ * the time unit of the timeout
* @return true if the count reached zero, false if the timeout elapsed
- * @throws InterruptedException if the current thread is interrupted while waiting
+ * @throws InterruptedException
+ * if the current thread is interrupted while waiting
*/
public boolean await(long timeout, TimeUnit unit) throws InterruptedException {
// Subscribe to notifications if not already subscribed
@@ -165,14 +166,18 @@ public boolean await(long timeout, TimeUnit unit) throws InterruptedException {
return completed;
}
-
+
/**
* Decrements the count of the latch, releasing all waiting threads if the count reaches zero.
*
- *
If the current count is greater than zero then it is decremented. If the new count is
- * zero then all waiting threads are re-enabled for thread scheduling purposes.
+ *
+ * If the current count is greater than zero then it is decremented. If the new count is zero then all waiting
+ * threads are re-enabled for thread scheduling purposes.
+ *
*
- *
If the current count equals zero then nothing happens.
+ *
+ * If the current count equals zero then nothing happens.
+ *
*/
public void countDown() {
// Decrement the count atomically using Redis DECR
@@ -186,11 +191,9 @@ public void countDown() {
newCount = count;
successfulNodes++;
- logger.debug("Decremented latch {} count to {} on {}",
- latchKey, count, driver.getIdentifier());
+ logger.debug("Decremented latch {} count to {} on {}", latchKey, count, driver.getIdentifier());
} catch (Exception e) {
- logger.debug("Failed to decrement latch count on {}: {}",
- driver.getIdentifier(), e.getMessage());
+ logger.debug("Failed to decrement latch count on {}: {}", driver.getIdentifier(), e.getMessage());
}
}
@@ -205,11 +208,13 @@ public void countDown() {
logger.warn("Failed to decrement latch {} count on quorum of nodes", latchKey);
}
}
-
+
/**
* Returns the current count.
*
- *
This method is typically used for debugging and testing purposes.
+ *
+ * This method is typically used for debugging and testing purposes.
+ *
*
* @return the current count
*/
@@ -227,8 +232,7 @@ public long getCount() {
successfulReads++;
}
} catch (Exception e) {
- logger.debug("Failed to read latch count from {}: {}",
- driver.getIdentifier(), e.getMessage());
+ logger.debug("Failed to read latch count from {}: {}", driver.getIdentifier(), e.getMessage());
}
}
@@ -241,7 +245,7 @@ public long getCount() {
logger.warn("Failed to read latch {} count from quorum of nodes", latchKey);
return 0; // Conservative fallback - assume completed
}
-
+
/**
* Initializes the latch count in Redis.
*/
@@ -255,17 +259,16 @@ private void initializeLatch(int count) {
driver.setex(latchKey, countValue, config.getDefaultLockTimeoutMs() * 10);
successfulNodes++;
} catch (Exception e) {
- logger.warn("Failed to initialize latch on {}: {}",
- driver.getIdentifier(), e.getMessage());
+ logger.warn("Failed to initialize latch on {}: {}", driver.getIdentifier(), e.getMessage());
}
}
if (successfulNodes < config.getQuorum()) {
- logger.warn("Failed to initialize latch {} on quorum of nodes (only {} of {} succeeded)",
- latchKey, successfulNodes, redisDrivers.size());
+ logger.warn("Failed to initialize latch {} on quorum of nodes (only {} of {} succeeded)", latchKey,
+ successfulNodes, redisDrivers.size());
} else {
- logger.debug("Successfully initialized latch {} with count {} on {} nodes",
- latchKey, count, successfulNodes);
+ logger.debug("Successfully initialized latch {} with count {} on {} nodes", latchKey, count,
+ successfulNodes);
}
}
@@ -313,23 +316,26 @@ private void publishZeroNotification() {
for (RedisDriver driver : redisDrivers) {
try {
long subscribers = driver.publish(channelKey, "zero");
- logger.debug("Published zero notification for latch {} to {} subscribers on {}",
- latchKey, subscribers, driver.getIdentifier());
+ logger.debug("Published zero notification for latch {} to {} subscribers on {}", latchKey, subscribers,
+ driver.getIdentifier());
} catch (Exception e) {
- logger.debug("Failed to publish zero notification on {}: {}",
- driver.getIdentifier(), e.getMessage());
+ logger.debug("Failed to publish zero notification on {}: {}", driver.getIdentifier(), e.getMessage());
}
}
}
-
+
/**
* Resets the latch to its initial count.
*
- *
Warning: This is not part of the standard CountDownLatch API and should be
- * used with caution. It's provided for scenarios where you need to reuse a latch.
+ *
+ * Warning: This is not part of the standard CountDownLatch API and should be used with caution. It's
+ * provided for scenarios where you need to reuse a latch.
+ *
*
- *
This operation is not atomic and may lead to race conditions if called while
- * other threads are waiting or counting down.
+ *
+ * This operation is not atomic and may lead to race conditions if called while other threads are waiting or
+ * counting down.
+ *
*/
public void reset() {
logger.debug("Resetting latch {} to initial count {}", latchKey, initialCount);
@@ -339,8 +345,7 @@ public void reset() {
try {
driver.del(latchKey);
} catch (Exception e) {
- logger.warn("Failed to delete latch on {}: {}",
- driver.getIdentifier(), e.getMessage());
+ logger.warn("Failed to delete latch on {}: {}", driver.getIdentifier(), e.getMessage());
}
}
@@ -351,12 +356,14 @@ public void reset() {
// Reinitialize with the original count
initializeLatch(initialCount);
}
-
+
/**
* Queries if any threads are waiting on this latch.
*
- *
Note: In a distributed environment, this is an approximation and may not be
- * accurate due to network delays and the distributed nature of the system.
+ *
+ * Note: In a distributed environment, this is an approximation and may not be accurate due to network delays and
+ * the distributed nature of the system.
+ *
*
* @return true if there may be threads waiting, false otherwise
*/
@@ -365,14 +372,10 @@ public boolean hasQueuedThreads() {
// Return true if count > 0 as a heuristic
return getCount() > 0;
}
-
+
@Override
public String toString() {
- return "RedlockCountDownLatch{" +
- "latchKey='" + latchKey + '\'' +
- ", count=" + getCount() +
- ", initialCount=" + initialCount +
- '}';
+ return "RedlockCountDownLatch{" + "latchKey='" + latchKey + '\'' + ", count=" + getCount() + ", initialCount="
+ + initialCount + '}';
}
}
-
diff --git a/src/main/java/org/codarama/redlock4j/RedlockException.java b/src/main/java/org/codarama/redlock4j/RedlockException.java
index bf712ff..62cf0f0 100644
--- a/src/main/java/org/codarama/redlock4j/RedlockException.java
+++ b/src/main/java/org/codarama/redlock4j/RedlockException.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j;
@@ -27,15 +8,15 @@
* Exception thrown by Redlock operations.
*/
public class RedlockException extends RuntimeException {
-
+
public RedlockException(String message) {
super(message);
}
-
+
public RedlockException(String message, Throwable cause) {
super(message, cause);
}
-
+
public RedlockException(Throwable cause) {
super(cause);
}
diff --git a/src/main/java/org/codarama/redlock4j/RedlockManager.java b/src/main/java/org/codarama/redlock4j/RedlockManager.java
index effde39..ddc1c11 100644
--- a/src/main/java/org/codarama/redlock4j/RedlockManager.java
+++ b/src/main/java/org/codarama/redlock4j/RedlockManager.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j;
@@ -46,38 +27,40 @@
*/
public class RedlockManager implements AutoCloseable {
private static final Logger logger = LoggerFactory.getLogger(RedlockManager.class);
-
+
public enum DriverType {
JEDIS, LETTUCE
}
-
+
private final RedlockConfiguration config;
private final List redisDrivers;
private final DriverType driverType;
private final ExecutorService executorService;
private final ScheduledExecutorService scheduledExecutorService;
private volatile boolean closed = false;
-
+
/**
* Creates a RedlockManager with Jedis drivers.
*
- * @param config the Redlock configuration
+ * @param config
+ * the Redlock configuration
* @return a new RedlockManager instance
*/
public static RedlockManager withJedis(RedlockConfiguration config) {
return new RedlockManager(config, DriverType.JEDIS);
}
-
+
/**
* Creates a RedlockManager with Lettuce drivers.
*
- * @param config the Redlock configuration
+ * @param config
+ * the Redlock configuration
* @return a new RedlockManager instance
*/
public static RedlockManager withLettuce(RedlockConfiguration config) {
return new RedlockManager(config, DriverType.LETTUCE);
}
-
+
private RedlockManager(RedlockConfiguration config, DriverType driverType) {
this.config = config;
this.driverType = driverType;
@@ -93,61 +76,62 @@ private RedlockManager(RedlockConfiguration config, DriverType driverType) {
return t;
});
- logger.info("Created RedlockManager with {} driver and {} Redis nodes",
- driverType, redisDrivers.size());
+ logger.info("Created RedlockManager with {} driver and {} Redis nodes", driverType, redisDrivers.size());
}
-
+
private List createDrivers() {
List drivers = new ArrayList<>();
-
+
for (RedisNodeConfiguration nodeConfig : config.getRedisNodes()) {
try {
RedisDriver driver;
switch (driverType) {
- case JEDIS:
+ case JEDIS :
driver = new JedisRedisDriver(nodeConfig);
break;
- case LETTUCE:
+ case LETTUCE :
driver = new LettuceRedisDriver(nodeConfig);
break;
- default:
+ default :
throw new IllegalArgumentException("Unsupported driver type: " + driverType);
}
-
+
// Test the connection
if (!driver.isConnected()) {
logger.warn("Failed to connect to Redis node: {}", driver.getIdentifier());
driver.close();
continue;
}
-
+
drivers.add(driver);
logger.debug("Successfully connected to Redis node: {}", driver.getIdentifier());
-
+
} catch (Exception e) {
- logger.error("Failed to create driver for Redis node {}:{}",
- nodeConfig.getHost(), nodeConfig.getPort(), e);
+ logger.error("Failed to create driver for Redis node {}:{}", nodeConfig.getHost(), nodeConfig.getPort(),
+ e);
}
}
-
+
if (drivers.isEmpty()) {
throw new RedlockException("Failed to connect to any Redis nodes");
}
-
+
if (drivers.size() < config.getQuorum()) {
- logger.warn("Connected to {} Redis nodes, but quorum requires {}. " +
- "Lock operations may fail.", drivers.size(), config.getQuorum());
+ logger.warn("Connected to {} Redis nodes, but quorum requires {}. " + "Lock operations may fail.",
+ drivers.size(), config.getQuorum());
}
-
+
return drivers;
}
-
+
/**
* Creates a new distributed lock for the given key.
*
- * @param lockKey the key to lock
+ * @param lockKey
+ * the key to lock
* @return a new Lock instance
- * @throws RedlockException if the manager is closed
+ * @throws RedlockException
+ * if the manager is closed
*/
public Lock createLock(String lockKey) {
if (closed) {
@@ -164,9 +148,11 @@ public Lock createLock(String lockKey) {
/**
* Creates a new asynchronous distributed lock for the given key.
*
- * @param lockKey the key to lock
+ * @param lockKey
+ * the key to lock
* @return a new AsyncRedlock instance
- * @throws RedlockException if the manager is closed
+ * @throws RedlockException
+ * if the manager is closed
*/
public AsyncRedlock createAsyncLock(String lockKey) {
if (closed) {
@@ -183,9 +169,11 @@ public AsyncRedlock createAsyncLock(String lockKey) {
/**
* Creates a new RxJava reactive distributed lock for the given key.
*
- * @param lockKey the key to lock
+ * @param lockKey
+ * the key to lock
* @return a new AsyncRedlockImpl instance
- * @throws RedlockException if the manager is closed
+ * @throws RedlockException
+ * if the manager is closed
*/
public RxRedlock createRxLock(String lockKey) {
if (closed) {
@@ -200,12 +188,14 @@ public RxRedlock createRxLock(String lockKey) {
}
/**
- * Creates a comprehensive lock that implements both async and reactive interfaces.
- * This lock supports both CompletionStage and RxJava reactive types.
+ * Creates a comprehensive lock that implements both async and reactive interfaces. This lock supports both
+ * CompletionStage and RxJava reactive types.
*
- * @param lockKey the key to lock
+ * @param lockKey
+ * the key to lock
* @return a new lock instance implementing both AsyncRedlock and AsyncRedlockImpl
- * @throws RedlockException if the manager is closed
+ * @throws RedlockException
+ * if the manager is closed
*/
public AsyncRedlockImpl createAsyncRxLock(String lockKey) {
if (closed) {
@@ -220,12 +210,13 @@ public AsyncRedlockImpl createAsyncRxLock(String lockKey) {
}
/**
- * Creates a new distributed fair lock for the given key.
- * Fair locks ensure FIFO ordering for lock acquisition.
+ * Creates a new distributed fair lock for the given key. Fair locks ensure FIFO ordering for lock acquisition.
*
- * @param lockKey the key to lock
+ * @param lockKey
+ * the key to lock
* @return a new FairLock instance
- * @throws RedlockException if the manager is closed
+ * @throws RedlockException
+ * if the manager is closed
*/
public Lock createFairLock(String lockKey) {
if (closed) {
@@ -240,12 +231,14 @@ public Lock createFairLock(String lockKey) {
}
/**
- * Creates a new distributed multi-lock for the given keys.
- * Multi-locks allow atomic acquisition of multiple resources.
+ * Creates a new distributed multi-lock for the given keys. Multi-locks allow atomic acquisition of multiple
+ * resources.
*
- * @param lockKeys the keys to lock atomically
+ * @param lockKeys
+ * the keys to lock atomically
* @return a new MultiLock instance
- * @throws RedlockException if the manager is closed
+ * @throws RedlockException
+ * if the manager is closed
*/
public Lock createMultiLock(List lockKeys) {
if (closed) {
@@ -260,12 +253,14 @@ public Lock createMultiLock(List lockKeys) {
}
/**
- * Creates a new distributed read-write lock for the given key.
- * Read-write locks allow multiple concurrent readers or a single exclusive writer.
+ * Creates a new distributed read-write lock for the given key. Read-write locks allow multiple concurrent readers
+ * or a single exclusive writer.
*
- * @param resourceKey the resource key
+ * @param resourceKey
+ * the resource key
* @return a new RedlockReadWriteLock instance
- * @throws RedlockException if the manager is closed
+ * @throws RedlockException
+ * if the manager is closed
*/
public RedlockReadWriteLock createReadWriteLock(String resourceKey) {
if (closed) {
@@ -280,13 +275,16 @@ public RedlockReadWriteLock createReadWriteLock(String resourceKey) {
}
/**
- * Creates a new distributed semaphore with the specified number of permits.
- * Semaphores control concurrent access to a resource with multiple permits.
+ * Creates a new distributed semaphore with the specified number of permits. Semaphores control concurrent access to
+ * a resource with multiple permits.
*
- * @param semaphoreKey the semaphore key
- * @param permits the number of permits available
+ * @param semaphoreKey
+ * the semaphore key
+ * @param permits
+ * the number of permits available
* @return a new RedlockSemaphore instance
- * @throws RedlockException if the manager is closed
+ * @throws RedlockException
+ * if the manager is closed
*/
public RedlockSemaphore createSemaphore(String semaphoreKey, int permits) {
if (closed) {
@@ -305,13 +303,16 @@ public RedlockSemaphore createSemaphore(String semaphoreKey, int permits) {
}
/**
- * Creates a new distributed countdown latch with the specified count.
- * Countdown latches allow threads to wait until a set of operations completes.
+ * Creates a new distributed countdown latch with the specified count. Countdown latches allow threads to wait until
+ * a set of operations completes.
*
- * @param latchKey the latch key
- * @param count the initial count
+ * @param latchKey
+ * the latch key
+ * @param count
+ * the initial count
* @return a new RedlockCountDownLatch instance
- * @throws RedlockException if the manager is closed
+ * @throws RedlockException
+ * if the manager is closed
*/
public RedlockCountDownLatch createCountDownLatch(String latchKey, int count) {
if (closed) {
@@ -338,7 +339,7 @@ public int getConnectedNodeCount() {
if (closed) {
return 0;
}
-
+
int connected = 0;
for (RedisDriver driver : redisDrivers) {
if (driver.isConnected()) {
@@ -347,7 +348,7 @@ public int getConnectedNodeCount() {
}
return connected;
}
-
+
/**
* Gets the required quorum size.
*
@@ -356,7 +357,7 @@ public int getConnectedNodeCount() {
public int getQuorum() {
return config.getQuorum();
}
-
+
/**
* Checks if the manager has enough connected nodes to potentially acquire locks.
*
@@ -365,7 +366,7 @@ public int getQuorum() {
public boolean isHealthy() {
return !closed && getConnectedNodeCount() >= getQuorum();
}
-
+
/**
* Gets the driver type being used.
*
@@ -374,7 +375,7 @@ public boolean isHealthy() {
public DriverType getDriverType() {
return driverType;
}
-
+
@Override
public void close() {
if (closed) {
diff --git a/src/main/java/org/codarama/redlock4j/RedlockReadWriteLock.java b/src/main/java/org/codarama/redlock4j/RedlockReadWriteLock.java
index af3dbab..73bc251 100644
--- a/src/main/java/org/codarama/redlock4j/RedlockReadWriteLock.java
+++ b/src/main/java/org/codarama/redlock4j/RedlockReadWriteLock.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j;
@@ -36,87 +17,95 @@
import java.util.concurrent.locks.ReadWriteLock;
/**
- * A distributed read-write lock implementation that allows multiple concurrent readers
- * or a single exclusive writer. This is useful for scenarios where reads are frequent
- * and writes are infrequent.
+ * A distributed read-write lock implementation that allows multiple concurrent readers or a single exclusive writer.
+ * This is useful for scenarios where reads are frequent and writes are infrequent.
*
- *
Key Features:
+ *
+ * Key Features:
+ *
*
- *
Multiple readers can hold the lock simultaneously
- *
Writers have exclusive access (no readers or other writers)
- *
Readers are blocked while a writer holds the lock
- *
Writers are blocked while any readers or writers hold the lock
+ *
Multiple readers can hold the lock simultaneously
+ *
Writers have exclusive access (no readers or other writers)
+ *
Readers are blocked while a writer holds the lock
+ *
Writers are blocked while any readers or writers hold the lock
*
*
- *
Implementation Details:
+ *
+ * Implementation Details:
+ *
*
- *
Uses Redis counters to track the number of active readers
- *
Uses a separate key for the write lock
- *
Readers increment/decrement the reader count atomically
- *
Writers must wait for reader count to reach zero
+ *
Uses Redis counters to track the number of active readers
+ *
Uses a separate key for the write lock
+ *
Readers increment/decrement the reader count atomically
*/
public class RedlockReadWriteLock implements ReadWriteLock {
private static final Logger logger = LoggerFactory.getLogger(RedlockReadWriteLock.class);
-
+
private final String resourceKey;
private final ReadLock readLock;
private final WriteLock writeLock;
-
- public RedlockReadWriteLock(String resourceKey, List redisDrivers,
- RedlockConfiguration config) {
+
+ public RedlockReadWriteLock(String resourceKey, List redisDrivers, RedlockConfiguration config) {
this.resourceKey = resourceKey;
this.readLock = new ReadLock(resourceKey, redisDrivers, config);
this.writeLock = new WriteLock(resourceKey, redisDrivers, config);
}
-
+
@Override
public Lock readLock() {
return readLock;
}
-
+
@Override
public Lock writeLock() {
return writeLock;
}
-
+
/**
* Read lock implementation that allows multiple concurrent readers.
*/
private static class ReadLock implements Lock {
private static final Logger logger = LoggerFactory.getLogger(ReadLock.class);
-
+
private final String readCountKey;
private final String writeLockKey;
private final List redisDrivers;
private final RedlockConfiguration config;
private final SecureRandom secureRandom;
-
+
private final ThreadLocal lockState = new ThreadLocal<>();
-
+
private static class LockState {
final String lockValue;
final long acquisitionTime;
@@ -142,7 +131,7 @@ int decrementHoldCount() {
return --holdCount;
}
}
-
+
ReadLock(String resourceKey, List redisDrivers, RedlockConfiguration config) {
this.readCountKey = resourceKey + ":readers";
this.writeLockKey = resourceKey + ":write";
@@ -150,7 +139,7 @@ int decrementHoldCount() {
this.config = config;
this.secureRandom = new SecureRandom();
}
-
+
@Override
public void lock() {
try {
@@ -162,14 +151,14 @@ public void lock() {
throw new RedlockException("Interrupted while acquiring read lock", e);
}
}
-
+
@Override
public void lockInterruptibly() throws InterruptedException {
if (!tryLock(config.getLockAcquisitionTimeoutMs(), TimeUnit.MILLISECONDS)) {
throw new RedlockException("Failed to acquire read lock within timeout");
}
}
-
+
@Override
public boolean tryLock() {
try {
@@ -179,7 +168,7 @@ public boolean tryLock() {
return false;
}
}
-
+
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
// Check if current thread already holds the lock (reentrancy)
@@ -203,8 +192,8 @@ public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
// Try to increment reader count
String lockValue = generateLockValue();
if (incrementReaderCount(lockValue)) {
- lockState.set(new LockState(lockValue, System.currentTimeMillis(),
- config.getDefaultLockTimeoutMs()));
+ lockState.set(
+ new LockState(lockValue, System.currentTimeMillis(), config.getDefaultLockTimeoutMs()));
logger.debug("Successfully acquired read lock on attempt {}", attempt + 1);
return true;
}
@@ -224,7 +213,7 @@ public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
-
+
@Override
public void unlock() {
LockState state = lockState.get();
@@ -245,7 +234,7 @@ public void unlock() {
lockState.remove();
logger.debug("Successfully released read lock");
}
-
+
private boolean isWriteLockHeld() {
// Check if write lock exists on a quorum of nodes using GET
int nodesWithoutWriteLock = 0;
@@ -256,8 +245,7 @@ private boolean isWriteLockHeld() {
nodesWithoutWriteLock++;
}
} catch (Exception e) {
- logger.debug("Failed to check write lock on {}: {}",
- driver.getIdentifier(), e.getMessage());
+ logger.debug("Failed to check write lock on {}: {}", driver.getIdentifier(), e.getMessage());
}
}
// If a quorum of nodes don't have the write lock, it's not held
@@ -276,19 +264,16 @@ private boolean incrementReaderCount(String lockValue) {
// Set expiration on the counter key to prevent leaks
if (count == 1) {
// First reader, set expiration
- driver.setex(readCountKey, String.valueOf(count),
- config.getDefaultLockTimeoutMs() * 2);
+ driver.setex(readCountKey, String.valueOf(count), config.getDefaultLockTimeoutMs() * 2);
}
// Store the lock value for this reader
- driver.setex(readCountKey + ":" + lockValue, "1",
- config.getDefaultLockTimeoutMs());
+ driver.setex(readCountKey + ":" + lockValue, "1", config.getDefaultLockTimeoutMs());
successfulNodes++;
logger.debug("Incremented reader count to {} on {}", count, driver.getIdentifier());
} catch (Exception e) {
- logger.debug("Failed to increment reader count on {}: {}",
- driver.getIdentifier(), e.getMessage());
+ logger.debug("Failed to increment reader count on {}: {}", driver.getIdentifier(), e.getMessage());
}
}
@@ -312,12 +297,11 @@ private void decrementReaderCount(String lockValue) {
logger.debug("Decremented reader count to {} on {}", count, driver.getIdentifier());
} catch (Exception e) {
- logger.warn("Failed to decrement reader count on {}: {}",
- driver.getIdentifier(), e.getMessage());
+ logger.warn("Failed to decrement reader count on {}: {}", driver.getIdentifier(), e.getMessage());
}
}
}
-
+
private String generateLockValue() {
byte[] bytes = new byte[20];
secureRandom.nextBytes(bytes);
@@ -327,16 +311,16 @@ private String generateLockValue() {
}
return sb.toString();
}
-
+
@Override
public Condition newCondition() {
throw new UnsupportedOperationException("Conditions are not supported");
}
}
-
+
/**
- * Write lock implementation that provides exclusive access.
- * Writers must wait for all readers to finish before acquiring the lock.
+ * Write lock implementation that provides exclusive access. Writers must wait for all readers to finish before
+ * acquiring the lock.
*/
private static class WriteLock implements Lock {
private static final Logger logger = LoggerFactory.getLogger(WriteLock.class);
@@ -435,8 +419,7 @@ private boolean hasActiveReaders() {
nodesWithoutReaders++;
}
} catch (Exception e) {
- logger.debug("Failed to check reader count on {}: {}",
- driver.getIdentifier(), e.getMessage());
+ logger.debug("Failed to check reader count on {}: {}", driver.getIdentifier(), e.getMessage());
}
}
@@ -445,4 +428,3 @@ private boolean hasActiveReaders() {
}
}
}
-
diff --git a/src/main/java/org/codarama/redlock4j/RedlockSemaphore.java b/src/main/java/org/codarama/redlock4j/RedlockSemaphore.java
index 98ae277..91818fa 100644
--- a/src/main/java/org/codarama/redlock4j/RedlockSemaphore.java
+++ b/src/main/java/org/codarama/redlock4j/RedlockSemaphore.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j;
@@ -34,53 +15,62 @@
import java.util.concurrent.TimeUnit;
/**
- * A distributed semaphore implementation that limits the number of concurrent accesses
- * to a shared resource. Unlike a lock which allows only one holder, a semaphore allows
- * a configurable number of permits.
+ * A distributed semaphore implementation that limits the number of concurrent accesses to a shared resource. Unlike a
+ * lock which allows only one holder, a semaphore allows a configurable number of permits.
*
- *
Key Features:
+ *
+ * Key Features:
+ *
*
- *
Configurable number of permits
- *
Multiple threads can acquire permits simultaneously
- *
Blocks when no permits are available
- *
Automatic permit release on timeout
+ *
Configurable number of permits
+ *
Multiple threads can acquire permits simultaneously
*/
public class RedlockSemaphore {
private static final Logger logger = LoggerFactory.getLogger(RedlockSemaphore.class);
-
+
private final String semaphoreKey;
private final int maxPermits;
private final List redisDrivers;
private final RedlockConfiguration config;
private final SecureRandom secureRandom;
-
+
// Thread-local storage for permit state
private final ThreadLocal permitState = new ThreadLocal<>();
-
+
private static class PermitState {
final List permitIds; // IDs of acquired permits
final long acquisitionTime;
@@ -96,54 +86,61 @@ boolean isValid() {
return System.currentTimeMillis() < acquisitionTime + validityTime;
}
}
-
+
/**
* Creates a new distributed semaphore.
*
- * @param semaphoreKey the key for this semaphore
- * @param maxPermits the maximum number of permits available
- * @param redisDrivers the Redis drivers to use
- * @param config the Redlock configuration
+ * @param semaphoreKey
+ * the key for this semaphore
+ * @param maxPermits
+ * the maximum number of permits available
+ * @param redisDrivers
+ * the Redis drivers to use
+ * @param config
+ * the Redlock configuration
*/
- public RedlockSemaphore(String semaphoreKey, int maxPermits, List redisDrivers,
- RedlockConfiguration config) {
+ public RedlockSemaphore(String semaphoreKey, int maxPermits, List redisDrivers,
+ RedlockConfiguration config) {
if (maxPermits <= 0) {
throw new IllegalArgumentException("Max permits must be positive");
}
-
+
this.semaphoreKey = semaphoreKey;
this.maxPermits = maxPermits;
this.redisDrivers = redisDrivers;
this.config = config;
this.secureRandom = new SecureRandom();
-
+
logger.debug("Created RedlockSemaphore {} with {} permits", semaphoreKey, maxPermits);
}
-
+
/**
* Acquires a permit, blocking until one is available.
*
- * @throws RedlockException if unable to acquire within the configured timeout
+ * @throws RedlockException
+ * if unable to acquire within the configured timeout
*/
public void acquire() throws InterruptedException {
if (!tryAcquire(config.getLockAcquisitionTimeoutMs(), TimeUnit.MILLISECONDS)) {
throw new RedlockException("Failed to acquire semaphore permit within timeout: " + semaphoreKey);
}
}
-
+
/**
* Acquires the specified number of permits, blocking until they are available.
*
- * @param permits the number of permits to acquire
- * @throws RedlockException if unable to acquire within the configured timeout
+ * @param permits
+ * the number of permits to acquire
+ * @throws RedlockException
+ * if unable to acquire within the configured timeout
*/
public void acquire(int permits) throws InterruptedException {
if (!tryAcquire(permits, config.getLockAcquisitionTimeoutMs(), TimeUnit.MILLISECONDS)) {
- throw new RedlockException("Failed to acquire " + permits + " semaphore permits within timeout: "
- + semaphoreKey);
+ throw new RedlockException(
+ "Failed to acquire " + permits + " semaphore permits within timeout: " + semaphoreKey);
}
}
-
+
/**
* Acquires a permit if one is immediately available.
*
@@ -157,31 +154,36 @@ public boolean tryAcquire() {
return false;
}
}
-
+
/**
* Acquires a permit, waiting up to the specified time if necessary.
*
- * @param timeout the maximum time to wait
- * @param unit the time unit of the timeout
+ * @param timeout
+ * the maximum time to wait
+ * @param unit
+ * the time unit of the timeout
* @return true if a permit was acquired, false if the timeout elapsed
*/
public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException {
return tryAcquire(1, timeout, unit);
}
-
+
/**
* Acquires the specified number of permits, waiting up to the specified time if necessary.
*
- * @param permits the number of permits to acquire
- * @param timeout the maximum time to wait
- * @param unit the time unit of the timeout
+ * @param permits
+ * the number of permits to acquire
+ * @param timeout
+ * the maximum time to wait
+ * @param unit
+ * the time unit of the timeout
* @return true if the permits were acquired, false if the timeout elapsed
*/
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException {
if (permits <= 0 || permits > maxPermits) {
throw new IllegalArgumentException("Invalid number of permits: " + permits);
}
-
+
// Check if current thread already has permits
PermitState currentState = permitState.get();
if (currentState != null && currentState.isValid()) {
@@ -199,10 +201,10 @@ public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws Inter
SemaphoreResult result = attemptAcquire(permits);
if (result.isAcquired()) {
- permitState.set(new PermitState(result.getPermitIds(), System.currentTimeMillis(),
- result.getValidityTimeMs()));
- logger.debug("Successfully acquired {} permit(s) for {} on attempt {}",
- permits, semaphoreKey, attempt + 1);
+ permitState.set(
+ new PermitState(result.getPermitIds(), System.currentTimeMillis(), result.getValidityTimeMs()));
+ logger.debug("Successfully acquired {} permit(s) for {} on attempt {}", permits, semaphoreKey,
+ attempt + 1);
return true;
}
@@ -220,18 +222,19 @@ public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws Inter
return false;
}
-
+
/**
* Releases a permit, returning it to the semaphore.
*/
public void release() {
release(1);
}
-
+
/**
* Releases the specified number of permits.
*
- * @param permits the number of permits to release
+ * @param permits
+ * the number of permits to release
*/
public void release(int permits) {
PermitState state = permitState.get();
@@ -248,39 +251,39 @@ public void release(int permits) {
// Release the specified number of permits
List toRelease = state.permitIds.subList(0, permits);
releasePermits(toRelease);
-
+
// Update or clear state
if (permits >= state.permitIds.size()) {
permitState.remove();
} else {
state.permitIds.subList(0, permits).clear();
}
-
+
logger.debug("Successfully released {} permit(s) for {}", permits, semaphoreKey);
}
-
+
/**
- * Returns the number of permits currently available (approximate).
- * Note: This is an estimate and may not be accurate in a distributed environment.
+ * Returns the number of permits currently available (approximate). Note: This is an estimate and may not be
+ * accurate in a distributed environment.
*/
public int availablePermits() {
// Note: This would require counting active permits across all nodes
// For now, return a placeholder
return maxPermits;
}
-
+
/**
* Attempts to acquire the specified number of permits.
*/
private SemaphoreResult attemptAcquire(int permits) {
List permitIds = new ArrayList<>();
long startTime = System.currentTimeMillis();
-
+
// Try to acquire permits by creating unique keys
for (int i = 0; i < permits; i++) {
String permitId = generatePermitId();
String permitKey = semaphoreKey + ":permit:" + permitId;
-
+
int successfulNodes = 0;
for (RedisDriver driver : redisDrivers) {
try {
@@ -291,7 +294,7 @@ private SemaphoreResult attemptAcquire(int permits) {
logger.debug("Failed to acquire permit on {}: {}", driver.getIdentifier(), e.getMessage());
}
}
-
+
if (successfulNodes >= config.getQuorum()) {
permitIds.add(permitId);
} else {
@@ -300,21 +303,21 @@ private SemaphoreResult attemptAcquire(int permits) {
return new SemaphoreResult(false, 0, new ArrayList<>());
}
}
-
+
long elapsedTime = System.currentTimeMillis() - startTime;
long driftTime = (long) (config.getDefaultLockTimeoutMs() * config.getClockDriftFactor()) + 2;
long validityTime = config.getDefaultLockTimeoutMs() - elapsedTime - driftTime;
-
+
boolean acquired = permitIds.size() == permits && validityTime > 0;
-
+
if (!acquired) {
releasePermits(permitIds);
return new SemaphoreResult(false, 0, new ArrayList<>());
}
-
+
return new SemaphoreResult(true, validityTime, permitIds);
}
-
+
/**
* Releases the specified permits.
*/
@@ -325,13 +328,13 @@ private void releasePermits(List permitIds) {
try {
driver.deleteIfValueMatches(permitKey, permitId);
} catch (Exception e) {
- logger.warn("Failed to release permit {} on {}: {}",
- permitId, driver.getIdentifier(), e.getMessage());
+ logger.warn("Failed to release permit {} on {}: {}", permitId, driver.getIdentifier(),
+ e.getMessage());
}
}
}
}
-
+
private String generatePermitId() {
byte[] bytes = new byte[16];
secureRandom.nextBytes(bytes);
@@ -341,7 +344,7 @@ private String generatePermitId() {
}
return sb.toString();
}
-
+
/**
* Result of a semaphore acquisition attempt.
*/
@@ -369,4 +372,3 @@ List getPermitIds() {
}
}
}
-
diff --git a/src/main/java/org/codarama/redlock4j/async/AsyncRedlock.java b/src/main/java/org/codarama/redlock4j/async/AsyncRedlock.java
index c3037d2..511d2fe 100644
--- a/src/main/java/org/codarama/redlock4j/async/AsyncRedlock.java
+++ b/src/main/java/org/codarama/redlock4j/async/AsyncRedlock.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j.async;
@@ -29,58 +10,59 @@
import java.util.concurrent.CompletionStage;
/**
- * Asynchronous distributed lock interface using CompletionStage for non-blocking operations.
- * This interface provides async alternatives to the standard Lock interface methods.
+ * Asynchronous distributed lock interface using CompletionStage for non-blocking operations. This interface provides
+ * async alternatives to the standard Lock interface methods.
*/
public interface AsyncRedlock {
-
+
/**
* Attempts to acquire the lock asynchronously without waiting.
*
* @return a CompletionStage that completes with true if the lock was acquired, false otherwise
*/
CompletionStage tryLockAsync();
-
+
/**
* Attempts to acquire the lock asynchronously with a timeout.
*
- * @param timeout the maximum time to wait for the lock
+ * @param timeout
+ * the maximum time to wait for the lock
* @return a CompletionStage that completes with true if the lock was acquired within the timeout, false otherwise
*/
CompletionStage tryLockAsync(Duration timeout);
-
+
/**
- * Acquires the lock asynchronously, waiting if necessary until the lock becomes available
- * or the acquisition timeout is reached.
+ * Acquires the lock asynchronously, waiting if necessary until the lock becomes available or the acquisition
+ * timeout is reached.
*
* @return a CompletionStage that completes when the lock is acquired
- * @throws RedlockException if the lock cannot be acquired within the configured timeout
+ * @throws RedlockException
+ * if the lock cannot be acquired within the configured timeout
*/
CompletionStage lockAsync();
-
+
/**
* Releases the lock asynchronously.
*
* @return a CompletionStage that completes when the lock is released
*/
CompletionStage unlockAsync();
-
+
/**
- * Checks if the current thread holds this lock.
- * This is a synchronous operation as it only checks local state.
+ * Checks if the current thread holds this lock. This is a synchronous operation as it only checks local state.
*
* @return true if the current thread holds the lock and it's still valid
*/
boolean isHeldByCurrentThread();
-
+
/**
- * Gets the remaining validity time of the lock for the current thread.
- * This is a synchronous operation as it only checks local state.
+ * Gets the remaining validity time of the lock for the current thread. This is a synchronous operation as it only
+ * checks local state.
*
* @return remaining validity time in milliseconds, or 0 if not held or expired
*/
long getRemainingValidityTime();
-
+
/**
* Gets the lock key.
*
@@ -89,9 +71,8 @@ public interface AsyncRedlock {
String getLockKey();
/**
- * Gets the hold count for the async lock.
- * This indicates how many times the lock has been acquired.
- * This is a synchronous operation as it only checks local state.
+ * Gets the hold count for the async lock. This indicates how many times the lock has been acquired. This is a
+ * synchronous operation as it only checks local state.
*
* @return hold count, or 0 if not held
*/
@@ -100,24 +81,26 @@ public interface AsyncRedlock {
/**
* Extends the validity time of the lock asynchronously.
*
- * This method attempts to extend the lock on a quorum of Redis nodes using the same
- * lock value. The extension is only successful if:
+ * This method attempts to extend the lock on a quorum of Redis nodes using the same lock value. The extension is
+ * only successful if:
*
- *
The lock is currently held and valid
- *
The extension succeeds on at least a quorum (N/2+1) of nodes
- *
The new validity time (after accounting for clock drift) is positive
+ *
The lock is currently held and valid
+ *
The extension succeeds on at least a quorum (N/2+1) of nodes
+ *
The new validity time (after accounting for clock drift) is positive
*
*
* Important limitations:
*
- *
Lock extension is for efficiency, not correctness
- *
Does not solve the GC pause problem - use fencing tokens for correctness
- *
Should not be used as a substitute for proper timeout configuration
+ *
Lock extension is for efficiency, not correctness
+ *
Does not solve the GC pause problem - use fencing tokens for correctness
+ *
Should not be used as a substitute for proper timeout configuration
*
*
- * @param additionalTime additional time to extend the lock
+ * @param additionalTime
+ * additional time to extend the lock
* @return a CompletionStage that completes with true if the lock was successfully extended, false otherwise
- * @throws IllegalArgumentException if additionalTime is negative or zero
+ * @throws IllegalArgumentException
+ * if additionalTime is negative or zero
*/
CompletionStage extendAsync(Duration additionalTime);
}
diff --git a/src/main/java/org/codarama/redlock4j/async/AsyncRedlockImpl.java b/src/main/java/org/codarama/redlock4j/async/AsyncRedlockImpl.java
index e072923..35ea450 100644
--- a/src/main/java/org/codarama/redlock4j/async/AsyncRedlockImpl.java
+++ b/src/main/java/org/codarama/redlock4j/async/AsyncRedlockImpl.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j.async;
@@ -42,25 +23,25 @@
import java.util.concurrent.*;
/**
- * Implementation supporting both AsyncRedlock and AsyncRedlockImpl interfaces.
- * Provides asynchronous CompletionStage and RxJava reactive capabilities.
+ * Implementation supporting both AsyncRedlock and AsyncRedlockImpl interfaces. Provides asynchronous CompletionStage
+ * and RxJava reactive capabilities.
*/
public class AsyncRedlockImpl implements AsyncRedlock, RxRedlock {
private static final Logger logger = LoggerFactory.getLogger(AsyncRedlockImpl.class);
-
+
private final String lockKey;
private final List redisDrivers;
private final RedlockConfiguration config;
private final SecureRandom secureRandom;
private final ExecutorService executorService;
private final ScheduledExecutorService scheduledExecutorService;
-
+
// Shared lock state for async operations (not thread-local)
private volatile LockStateInfo lockState;
// RxJava subject for lock state changes
private final BehaviorSubject lockStateSubject = BehaviorSubject.createDefault(LockState.RELEASED);
-
+
private static class LockStateInfo {
final String lockValue;
final long acquisitionTime;
@@ -86,10 +67,9 @@ synchronized int decrementHoldCount() {
return --holdCount;
}
}
-
- public AsyncRedlockImpl(String lockKey, List redisDrivers,
- RedlockConfiguration config, ExecutorService executorService,
- ScheduledExecutorService scheduledExecutorService) {
+
+ public AsyncRedlockImpl(String lockKey, List redisDrivers, RedlockConfiguration config,
+ ExecutorService executorService, ScheduledExecutorService scheduledExecutorService) {
this.lockKey = lockKey;
this.redisDrivers = redisDrivers;
this.config = config;
@@ -97,9 +77,9 @@ public AsyncRedlockImpl(String lockKey, List redisDrivers,
this.executorService = executorService;
this.scheduledExecutorService = scheduledExecutorService;
}
-
+
// AsyncRedlock implementation (CompletionStage)
-
+
@Override
public CompletionStage tryLockAsync() {
return CompletableFuture.supplyAsync(() -> {
@@ -108,14 +88,16 @@ public CompletionStage tryLockAsync() {
LockStateInfo currentState = lockState;
if (currentState != null && currentState.isValid()) {
currentState.incrementHoldCount();
- logger.debug("Reentrant async lock acquisition for {} (hold count: {})", lockKey, currentState.holdCount);
+ logger.debug("Reentrant async lock acquisition for {} (hold count: {})", lockKey,
+ currentState.holdCount);
return true;
}
lockStateSubject.onNext(LockState.ACQUIRING);
LockResult result = attemptLock();
if (result.isAcquired()) {
- lockState = new LockStateInfo(result.getLockValue(), System.currentTimeMillis(), result.getValidityTimeMs());
+ lockState = new LockStateInfo(result.getLockValue(), System.currentTimeMillis(),
+ result.getValidityTimeMs());
lockStateSubject.onNext(LockState.ACQUIRED);
logger.debug("Successfully acquired async lock {}", lockKey);
return true;
@@ -130,7 +112,7 @@ public CompletionStage tryLockAsync() {
}
}, executorService);
}
-
+
@Override
public CompletionStage tryLockAsync(Duration timeout) {
long timeoutMs = timeout.toMillis();
@@ -138,57 +120,55 @@ public CompletionStage tryLockAsync(Duration timeout) {
return tryLockWithRetryAsync(timeoutMs, startTime, 0);
}
-
+
private CompletionStage tryLockWithRetryAsync(long timeoutMs, long startTime, int attempt) {
return tryLockAsync().thenCompose(acquired -> {
if (acquired) {
return CompletableFuture.completedFuture(true);
}
-
+
// Check timeout
if (timeoutMs > 0 && (System.currentTimeMillis() - startTime) >= timeoutMs) {
return CompletableFuture.completedFuture(false);
}
-
+
// Check max attempts
if (attempt >= config.getMaxRetryAttempts()) {
return CompletableFuture.completedFuture(false);
}
-
+
// Schedule retry with delay
long delay = config.getRetryDelayMs() + ThreadLocalRandom.current().nextLong(config.getRetryDelayMs());
-
+
CompletableFuture future = new CompletableFuture<>();
scheduledExecutorService.schedule(() -> {
- tryLockWithRetryAsync(timeoutMs, startTime, attempt + 1)
- .whenComplete((result, throwable) -> {
- if (throwable != null) {
- future.completeExceptionally(throwable);
- } else {
- future.complete(result);
- }
- });
+ tryLockWithRetryAsync(timeoutMs, startTime, attempt + 1).whenComplete((result, throwable) -> {
+ if (throwable != null) {
+ future.completeExceptionally(throwable);
+ } else {
+ future.complete(result);
+ }
+ });
}, delay, TimeUnit.MILLISECONDS);
-
+
return future;
});
}
-
+
@Override
public CompletionStage lockAsync() {
- return tryLockAsync(Duration.ofMillis(config.getLockAcquisitionTimeoutMs()))
- .thenCompose(acquired -> {
- if (acquired) {
- return CompletableFuture.completedFuture(null);
- } else {
- CompletableFuture failedFuture = new CompletableFuture<>();
- failedFuture.completeExceptionally(
+ return tryLockAsync(Duration.ofMillis(config.getLockAcquisitionTimeoutMs())).thenCompose(acquired -> {
+ if (acquired) {
+ return CompletableFuture.completedFuture(null);
+ } else {
+ CompletableFuture failedFuture = new CompletableFuture<>();
+ failedFuture.completeExceptionally(
new RedlockException("Failed to acquire lock within timeout: " + lockKey));
- return failedFuture;
- }
- });
+ return failedFuture;
+ }
+ });
}
-
+
@Override
public CompletionStage unlockAsync() {
return CompletableFuture.runAsync(() -> {
@@ -219,47 +199,40 @@ public CompletionStage unlockAsync() {
logger.debug("Successfully released async lock {}", lockKey);
}, executorService);
}
-
+
// AsyncRedlockImpl implementation (RxJava)
-
+
@Override
public Single tryLockRx() {
- return Single.fromCompletionStage(tryLockAsync())
- .subscribeOn(Schedulers.io());
+ return Single.fromCompletionStage(tryLockAsync()).subscribeOn(Schedulers.io());
}
-
+
@Override
public Single tryLockRx(Duration timeout) {
- return Single.fromCompletionStage(tryLockAsync(timeout))
- .subscribeOn(Schedulers.io());
+ return Single.fromCompletionStage(tryLockAsync(timeout)).subscribeOn(Schedulers.io());
}
-
+
@Override
public Completable lockRx() {
- return Completable.fromCompletionStage(lockAsync())
- .subscribeOn(Schedulers.io());
+ return Completable.fromCompletionStage(lockAsync()).subscribeOn(Schedulers.io());
}
-
+
@Override
public Completable unlockRx() {
- return Completable.fromCompletionStage(unlockAsync())
- .subscribeOn(Schedulers.io());
+ return Completable.fromCompletionStage(unlockAsync()).subscribeOn(Schedulers.io());
}
-
+
@Override
public Observable validityObservable(Duration checkInterval) {
return Observable.interval(checkInterval.toMillis(), TimeUnit.MILLISECONDS, Schedulers.io())
- .map(tick -> getRemainingValidityTime())
- .takeWhile(validity -> validity > 0);
+ .map(tick -> getRemainingValidityTime()).takeWhile(validity -> validity > 0);
}
@Override
public Single tryLockWithRetryRx(int maxRetries, Duration retryDelay) {
- return tryLockRx()
- .retry(maxRetries)
- .delay(retryDelay.toMillis(), TimeUnit.MILLISECONDS);
+ return tryLockRx().retry(maxRetries).delay(retryDelay.toMillis(), TimeUnit.MILLISECONDS);
}
-
+
@Override
public Observable lockStateObservable() {
return lockStateSubject.distinctUntilChanged();
@@ -267,12 +240,11 @@ public Observable lockStateObservable() {
@Override
public Single extendRx(Duration additionalTime) {
- return Single.fromCompletionStage(extendAsync(additionalTime))
- .subscribeOn(Schedulers.io());
+ return Single.fromCompletionStage(extendAsync(additionalTime)).subscribeOn(Schedulers.io());
}
// Common methods
-
+
@Override
public boolean isHeldByCurrentThread() {
LockStateInfo state = lockState;
@@ -288,15 +260,14 @@ public long getRemainingValidityTime() {
long remaining = state.acquisitionTime + state.validityTime - System.currentTimeMillis();
return Math.max(0, remaining);
}
-
+
@Override
public String getLockKey() {
return lockKey;
}
/**
- * Gets the hold count for the async lock.
- * This indicates how many times the lock has been acquired.
+ * Gets the hold count for the async lock. This indicates how many times the lock has been acquired.
*
* @return hold count, or 0 if not held
*/
@@ -345,14 +316,15 @@ public CompletionStage extendAsync(Duration additionalTime) {
if (extended) {
// Update lock state with new validity time
- LockStateInfo newState = new LockStateInfo(state.lockValue, System.currentTimeMillis(), newValidityTime);
+ LockStateInfo newState = new LockStateInfo(state.lockValue, System.currentTimeMillis(),
+ newValidityTime);
newState.holdCount = state.holdCount; // Preserve hold count
lockState = newState;
- logger.debug("Successfully extended async lock {} on {}/{} nodes (new validity: {}ms)",
- lockKey, successfulNodes, redisDrivers.size(), newValidityTime);
+ logger.debug("Successfully extended async lock {} on {}/{} nodes (new validity: {}ms)", lockKey,
+ successfulNodes, redisDrivers.size(), newValidityTime);
} else {
- logger.debug("Failed to extend async lock {} - only {}/{} nodes succeeded (quorum: {})",
- lockKey, successfulNodes, redisDrivers.size(), config.getQuorum());
+ logger.debug("Failed to extend async lock {} - only {}/{} nodes succeeded (quorum: {})", lockKey,
+ successfulNodes, redisDrivers.size(), config.getQuorum());
}
return extended;
@@ -360,12 +332,12 @@ public CompletionStage extendAsync(Duration additionalTime) {
}
// Private helper methods
-
+
private LockResult attemptLock() {
String lockValue = generateLockValue();
long startTime = System.currentTimeMillis();
int successfulNodes = 0;
-
+
// Try to acquire the lock on all nodes
for (RedisDriver driver : redisDrivers) {
try {
@@ -376,21 +348,21 @@ private LockResult attemptLock() {
logger.warn("Failed to acquire lock on {}: {}", driver.getIdentifier(), e.getMessage());
}
}
-
+
long elapsedTime = System.currentTimeMillis() - startTime;
long driftTime = (long) (config.getDefaultLockTimeoutMs() * config.getClockDriftFactor()) + 2;
long validityTime = config.getDefaultLockTimeoutMs() - elapsedTime - driftTime;
-
+
boolean acquired = successfulNodes >= config.getQuorum() && validityTime > 0;
-
+
if (!acquired) {
// Release any locks we managed to acquire
releaseLock(lockValue);
}
-
+
return new LockResult(acquired, validityTime, lockValue, successfulNodes, redisDrivers.size());
}
-
+
private void releaseLock(String lockValue) {
for (RedisDriver driver : redisDrivers) {
try {
@@ -400,7 +372,7 @@ private void releaseLock(String lockValue) {
}
}
}
-
+
private String generateLockValue() {
byte[] bytes = new byte[20];
secureRandom.nextBytes(bytes);
diff --git a/src/main/java/org/codarama/redlock4j/async/RxRedlock.java b/src/main/java/org/codarama/redlock4j/async/RxRedlock.java
index beaebae..d67d627 100644
--- a/src/main/java/org/codarama/redlock4j/async/RxRedlock.java
+++ b/src/main/java/org/codarama/redlock4j/async/RxRedlock.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j.async;
@@ -29,84 +10,86 @@
import java.time.Duration;
/**
- * RxJava reactive distributed lock interface providing reactive streams for lock operations.
- * This interface uses RxJava 3 reactive types for maximum compatibility with reactive applications.
+ * RxJava reactive distributed lock interface providing reactive streams for lock operations. This interface uses RxJava
+ * 3 reactive types for maximum compatibility with reactive applications.
*/
public interface RxRedlock {
-
+
/**
* Attempts to acquire the lock reactively without waiting.
*
* @return a Single that emits true if the lock was acquired, false otherwise
*/
Single tryLockRx();
-
+
/**
* Attempts to acquire the lock reactively with a timeout.
*
- * @param timeout the maximum time to wait for the lock
+ * @param timeout
+ * the maximum time to wait for the lock
* @return a Single that emits true if the lock was acquired within the timeout, false otherwise
*/
Single tryLockRx(Duration timeout);
-
+
/**
- * Acquires the lock reactively, waiting if necessary until the lock becomes available
- * or the acquisition timeout is reached.
+ * Acquires the lock reactively, waiting if necessary until the lock becomes available or the acquisition timeout is
+ * reached.
*
* @return a Completable that completes when the lock is acquired
*/
Completable lockRx();
-
+
/**
* Releases the lock reactively.
*
* @return a Completable that completes when the lock is released
*/
Completable unlockRx();
-
+
/**
- * Creates a reactive stream that periodically emits the lock validity time.
- * Useful for monitoring lock health in reactive applications.
+ * Creates a reactive stream that periodically emits the lock validity time. Useful for monitoring lock health in
+ * reactive applications.
*
- * @param checkInterval the interval between validity checks
+ * @param checkInterval
+ * the interval between validity checks
* @return an Observable that emits the remaining validity time at each check
*/
Observable validityObservable(Duration checkInterval);
-
+
/**
- * Creates a reactive stream that emits lock acquisition attempts with retry logic.
- * This provides fine-grained control over retry behavior in reactive applications.
+ * Creates a reactive stream that emits lock acquisition attempts with retry logic. This provides fine-grained
+ * control over retry behavior in reactive applications.
*
- * @param maxRetries maximum number of retry attempts
- * @param retryDelay delay between retry attempts
+ * @param maxRetries
+ * maximum number of retry attempts
+ * @param retryDelay
+ * delay between retry attempts
* @return a Single that emits true when lock is acquired, or error if all retries fail
*/
Single tryLockWithRetryRx(int maxRetries, Duration retryDelay);
-
+
/**
- * Creates an observable that emits lock state changes.
- * Useful for monitoring when locks are acquired or released.
+ * Creates an observable that emits lock state changes. Useful for monitoring when locks are acquired or released.
*
* @return an Observable that emits LockState events
*/
Observable lockStateObservable();
-
+
/**
- * Checks if the current thread holds this lock.
- * This is a synchronous operation as it only checks local state.
+ * Checks if the current thread holds this lock. This is a synchronous operation as it only checks local state.
*
* @return true if the current thread holds the lock and it's still valid
*/
boolean isHeldByCurrentThread();
-
+
/**
- * Gets the remaining validity time of the lock for the current thread.
- * This is a synchronous operation as it only checks local state.
+ * Gets the remaining validity time of the lock for the current thread. This is a synchronous operation as it only
+ * checks local state.
*
* @return remaining validity time in milliseconds, or 0 if not held or expired
*/
long getRemainingValidityTime();
-
+
/**
* Gets the lock key.
*
@@ -115,9 +98,8 @@ public interface RxRedlock {
String getLockKey();
/**
- * Gets the hold count for the reactive lock.
- * This indicates how many times the lock has been acquired.
- * This is a synchronous operation as it only checks local state.
+ * Gets the hold count for the reactive lock. This indicates how many times the lock has been acquired. This is a
+ * synchronous operation as it only checks local state.
*
* @return hold count, or 0 if not held
*/
@@ -126,23 +108,25 @@ public interface RxRedlock {
/**
* Extends the validity time of the lock reactively.
*
- * This method attempts to extend the lock on a quorum of Redis nodes using the same
- * lock value. The extension is only successful if:
+ * This method attempts to extend the lock on a quorum of Redis nodes using the same lock value. The extension is
+ * only successful if:
*
- *
The lock is currently held and valid
- *
The extension succeeds on at least a quorum (N/2+1) of nodes
- *
The new validity time (after accounting for clock drift) is positive
+ *
The lock is currently held and valid
+ *
The extension succeeds on at least a quorum (N/2+1) of nodes
+ *
The new validity time (after accounting for clock drift) is positive
*
*
* Important limitations:
*
- *
Lock extension is for efficiency, not correctness
- *
Should not be used as a substitute for proper timeout configuration
+ *
Lock extension is for efficiency, not correctness
+ *
Should not be used as a substitute for proper timeout configuration
*
*
- * @param additionalTime additional time to extend the lock
+ * @param additionalTime
+ * additional time to extend the lock
* @return a Single that emits true if the lock was successfully extended, false otherwise
- * @throws IllegalArgumentException if additionalTime is negative or zero
+ * @throws IllegalArgumentException
+ * if additionalTime is negative or zero
*/
Single extendRx(Duration additionalTime);
@@ -150,10 +134,6 @@ public interface RxRedlock {
* Represents the state of a lock for reactive monitoring.
*/
enum LockState {
- ACQUIRING,
- ACQUIRED,
- RELEASED,
- EXPIRED,
- FAILED
+ ACQUIRING, ACQUIRED, RELEASED, EXPIRED, FAILED
}
}
diff --git a/src/main/java/org/codarama/redlock4j/configuration/RedisNodeConfiguration.java b/src/main/java/org/codarama/redlock4j/configuration/RedisNodeConfiguration.java
index 41387b2..107842a 100644
--- a/src/main/java/org/codarama/redlock4j/configuration/RedisNodeConfiguration.java
+++ b/src/main/java/org/codarama/redlock4j/configuration/RedisNodeConfiguration.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j.configuration;
@@ -122,12 +103,7 @@ public RedisNodeConfiguration build() {
@Override
public String toString() {
- return "RedisNodeConfiguration{" +
- "host='" + host + '\'' +
- ", port=" + port +
- ", database=" + database +
- ", connectionTimeoutMs=" + connectionTimeoutMs +
- ", socketTimeoutMs=" + socketTimeoutMs +
- '}';
+ return "RedisNodeConfiguration{" + "host='" + host + '\'' + ", port=" + port + ", database=" + database
+ + ", connectionTimeoutMs=" + connectionTimeoutMs + ", socketTimeoutMs=" + socketTimeoutMs + '}';
}
}
diff --git a/src/main/java/org/codarama/redlock4j/configuration/RedlockConfiguration.java b/src/main/java/org/codarama/redlock4j/configuration/RedlockConfiguration.java
index 7ab16b0..f14bb53 100644
--- a/src/main/java/org/codarama/redlock4j/configuration/RedlockConfiguration.java
+++ b/src/main/java/org/codarama/redlock4j/configuration/RedlockConfiguration.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j.configuration;
@@ -93,18 +74,11 @@ public Builder addRedisNode(RedisNodeConfiguration nodeConfig) {
}
public Builder addRedisNode(String host, int port) {
- return addRedisNode(RedisNodeConfiguration.builder()
- .host(host)
- .port(port)
- .build());
+ return addRedisNode(RedisNodeConfiguration.builder().host(host).port(port).build());
}
public Builder addRedisNode(String host, int port, String password) {
- return addRedisNode(RedisNodeConfiguration.builder()
- .host(host)
- .port(port)
- .password(password)
- .build());
+ return addRedisNode(RedisNodeConfiguration.builder().host(host).port(port).password(password).build());
}
public Builder defaultLockTimeout(Duration timeout) {
diff --git a/src/main/java/org/codarama/redlock4j/driver/JedisRedisDriver.java b/src/main/java/org/codarama/redlock4j/driver/JedisRedisDriver.java
index 920b119..28ca816 100644
--- a/src/main/java/org/codarama/redlock4j/driver/JedisRedisDriver.java
+++ b/src/main/java/org/codarama/redlock4j/driver/JedisRedisDriver.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j.driver;
@@ -40,29 +21,23 @@
/**
* Jedis implementation of the RedisDriver interface with automatic CAS/CAD detection.
*
- *
This driver automatically detects and uses the best available method for each operation:
+ *
+ * This driver automatically detects and uses the best available method for each operation:
*
- *
Native Redis 8.4+ CAS/CAD commands (DELEX, SET IFEQ) when available
- *
Lua script-based operations for older Redis versions
+ *
Native Redis 8.4+ CAS/CAD commands (DELEX, SET IFEQ) when available
+ *
Lua script-based operations for older Redis versions
*
- * Detection happens once at driver initialization.
+ * Detection happens once at driver initialization.
+ *
*/
public class JedisRedisDriver implements RedisDriver {
private static final Logger logger = LoggerFactory.getLogger(JedisRedisDriver.class);
- private static final String DELETE_IF_VALUE_MATCHES_SCRIPT =
- "if redis.call('get', KEYS[1]) == ARGV[1] then " +
- " return redis.call('del', KEYS[1]) " +
- "else " +
- " return 0 " +
- "end";
+ private static final String DELETE_IF_VALUE_MATCHES_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then "
+ + " return redis.call('del', KEYS[1]) " + "else " + " return 0 " + "end";
- private static final String SET_IF_VALUE_MATCHES_SCRIPT =
- "if redis.call('get', KEYS[1]) == ARGV[1] then " +
- " return redis.call('set', KEYS[1], ARGV[2], 'PX', ARGV[3]) " +
- "else " +
- " return nil " +
- "end";
+ private static final String SET_IF_VALUE_MATCHES_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then "
+ + " return redis.call('set', KEYS[1], ARGV[2], 'PX', ARGV[3]) " + "else " + " return nil " + "end";
/**
* Strategy for CAS/CAD operations.
@@ -77,7 +52,7 @@ private enum CADStrategy {
private final JedisPool jedisPool;
private final String identifier;
private final CADStrategy cadStrategy;
-
+
public JedisRedisDriver(RedisNodeConfiguration config) {
this.identifier = "redis://" + config.getHost() + ":" + config.getPort();
@@ -101,12 +76,8 @@ public JedisRedisDriver(RedisNodeConfiguration config) {
try {
java.net.URI redisUri = java.net.URI.create(uriBuilder.toString());
- this.jedisPool = new JedisPool(
- poolConfig,
- redisUri,
- config.getConnectionTimeoutMs(),
- config.getSocketTimeoutMs()
- );
+ this.jedisPool = new JedisPool(poolConfig, redisUri, config.getConnectionTimeoutMs(),
+ config.getSocketTimeoutMs());
} catch (Exception e) {
throw new RuntimeException("Failed to create Jedis pool for " + identifier, e);
}
@@ -119,26 +90,22 @@ public JedisRedisDriver(RedisNodeConfiguration config) {
}
/**
- * Detects whether native CAS/CAD commands are available.
- * This is called once during driver initialization.
+ * Detects whether native CAS/CAD commands are available. This is called once during driver initialization.
*/
private CADStrategy detectCADStrategy() {
try (Jedis jedis = jedisPool.getResource()) {
// Try to execute DELEX on a test key
String testKey = "__redlock4j_cad_test__" + System.currentTimeMillis();
- jedis.sendCommand(
- redis.clients.jedis.Protocol.Command.DELEX,
- testKey, "IFEQ", "test_value"
- );
+ jedis.sendCommand(redis.clients.jedis.Protocol.Command.DELEX, testKey, "IFEQ", "test_value");
logger.debug("Native CAS/CAD commands detected for {}", identifier);
return CADStrategy.NATIVE;
} catch (Exception e) {
- logger.debug("Native CAS/CAD commands not available for {}, using Lua scripts: {}",
- identifier, e.getMessage());
+ logger.debug("Native CAS/CAD commands not available for {}, using Lua scripts: {}", identifier,
+ e.getMessage());
return CADStrategy.SCRIPT;
}
}
-
+
@Override
public boolean setIfNotExists(String key, String value, long expireTimeMs) throws RedisDriverException {
try (Jedis jedis = jedisPool.getResource()) {
@@ -149,15 +116,15 @@ public boolean setIfNotExists(String key, String value, long expireTimeMs) throw
throw new RedisDriverException("Failed to execute SET NX PX command on " + identifier, e);
}
}
-
+
@Override
public boolean deleteIfValueMatches(String key, String expectedValue) throws RedisDriverException {
switch (cadStrategy) {
- case NATIVE:
+ case NATIVE :
return deleteIfValueMatchesNative(key, expectedValue);
- case SCRIPT:
+ case SCRIPT :
return deleteIfValueMatchesScript(key, expectedValue);
- default:
+ default :
throw new IllegalStateException("Unknown CAD strategy: " + cadStrategy);
}
}
@@ -167,10 +134,7 @@ public boolean deleteIfValueMatches(String key, String expectedValue) throws Red
*/
private boolean deleteIfValueMatchesNative(String key, String expectedValue) throws RedisDriverException {
try (Jedis jedis = jedisPool.getResource()) {
- Object result = jedis.sendCommand(
- redis.clients.jedis.Protocol.Command.DELEX,
- key, "IFEQ", expectedValue
- );
+ Object result = jedis.sendCommand(redis.clients.jedis.Protocol.Command.DELEX, key, "IFEQ", expectedValue);
return Long.valueOf(1).equals(result);
} catch (JedisException e) {
throw new RedisDriverException("Failed to execute DELEX command on " + identifier, e);
@@ -182,17 +146,14 @@ private boolean deleteIfValueMatchesNative(String key, String expectedValue) thr
*/
private boolean deleteIfValueMatchesScript(String key, String expectedValue) throws RedisDriverException {
try (Jedis jedis = jedisPool.getResource()) {
- Object result = jedis.eval(
- DELETE_IF_VALUE_MATCHES_SCRIPT,
- Collections.singletonList(key),
- Collections.singletonList(expectedValue)
- );
+ Object result = jedis.eval(DELETE_IF_VALUE_MATCHES_SCRIPT, Collections.singletonList(key),
+ Collections.singletonList(expectedValue));
return Long.valueOf(1).equals(result);
} catch (JedisException e) {
throw new RedisDriverException("Failed to execute delete script on " + identifier, e);
}
}
-
+
@Override
public boolean isConnected() {
try (Jedis jedis = jedisPool.getResource()) {
@@ -202,12 +163,12 @@ public boolean isConnected() {
return false;
}
}
-
+
@Override
public String getIdentifier() {
return identifier;
}
-
+
@Override
public void close() {
if (jedisPool != null && !jedisPool.isClosed()) {
@@ -217,14 +178,14 @@ public void close() {
}
@Override
- public boolean setIfValueMatches(String key, String newValue, String expectedCurrentValue,
- long expireTimeMs) throws RedisDriverException {
+ public boolean setIfValueMatches(String key, String newValue, String expectedCurrentValue, long expireTimeMs)
+ throws RedisDriverException {
switch (cadStrategy) {
- case NATIVE:
+ case NATIVE :
return setIfValueMatchesNative(key, newValue, expectedCurrentValue, expireTimeMs);
- case SCRIPT:
+ case SCRIPT :
return setIfValueMatchesScript(key, newValue, expectedCurrentValue, expireTimeMs);
- default:
+ default :
throw new IllegalStateException("Unknown CAD strategy: " + cadStrategy);
}
}
@@ -232,13 +193,11 @@ public boolean setIfValueMatches(String key, String newValue, String expectedCur
/**
* Sets a key using native SET IFEQ command (Redis 8.4+).
*/
- private boolean setIfValueMatchesNative(String key, String newValue, String expectedCurrentValue,
- long expireTimeMs) throws RedisDriverException {
+ private boolean setIfValueMatchesNative(String key, String newValue, String expectedCurrentValue, long expireTimeMs)
+ throws RedisDriverException {
try (Jedis jedis = jedisPool.getResource()) {
- Object result = jedis.sendCommand(
- redis.clients.jedis.Protocol.Command.SET,
- key, newValue, "IFEQ", expectedCurrentValue, "PX", String.valueOf(expireTimeMs)
- );
+ Object result = jedis.sendCommand(redis.clients.jedis.Protocol.Command.SET, key, newValue, "IFEQ",
+ expectedCurrentValue, "PX", String.valueOf(expireTimeMs));
return "OK".equals(result);
} catch (JedisException e) {
throw new RedisDriverException("Failed to execute SET IFEQ command on " + identifier, e);
@@ -248,14 +207,11 @@ private boolean setIfValueMatchesNative(String key, String newValue, String expe
/**
* Sets a key using Lua script (legacy compatibility).
*/
- private boolean setIfValueMatchesScript(String key, String newValue, String expectedCurrentValue,
- long expireTimeMs) throws RedisDriverException {
+ private boolean setIfValueMatchesScript(String key, String newValue, String expectedCurrentValue, long expireTimeMs)
+ throws RedisDriverException {
try (Jedis jedis = jedisPool.getResource()) {
- Object result = jedis.eval(
- SET_IF_VALUE_MATCHES_SCRIPT,
- Collections.singletonList(key),
- java.util.Arrays.asList(expectedCurrentValue, newValue, String.valueOf(expireTimeMs))
- );
+ Object result = jedis.eval(SET_IF_VALUE_MATCHES_SCRIPT, Collections.singletonList(key),
+ java.util.Arrays.asList(expectedCurrentValue, newValue, String.valueOf(expireTimeMs)));
return "OK".equals(result);
} catch (JedisException e) {
throw new RedisDriverException("Failed to execute SET script on " + identifier, e);
diff --git a/src/main/java/org/codarama/redlock4j/driver/LettuceRedisDriver.java b/src/main/java/org/codarama/redlock4j/driver/LettuceRedisDriver.java
index 34723fe..c1cb6ab 100644
--- a/src/main/java/org/codarama/redlock4j/driver/LettuceRedisDriver.java
+++ b/src/main/java/org/codarama/redlock4j/driver/LettuceRedisDriver.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j.driver;
@@ -42,29 +23,23 @@
/**
* Lettuce implementation of the RedisDriver interface with automatic CAS/CAD detection.
*
- *
This driver automatically detects and uses the best available method for each operation:
+ *
+ * This driver automatically detects and uses the best available method for each operation:
*
- *
Native Redis 8.4+ CAS/CAD commands (DELEX, SET IFEQ) when available
- *
Lua script-based operations for older Redis versions
+ *
Native Redis 8.4+ CAS/CAD commands (DELEX, SET IFEQ) when available
+ *
Lua script-based operations for older Redis versions
*
- * Detection happens once at driver initialization.
+ * Detection happens once at driver initialization.
+ *
*/
public class LettuceRedisDriver implements RedisDriver {
private static final Logger logger = LoggerFactory.getLogger(LettuceRedisDriver.class);
- private static final String DELETE_IF_VALUE_MATCHES_SCRIPT =
- "if redis.call('get', KEYS[1]) == ARGV[1] then " +
- " return redis.call('del', KEYS[1]) " +
- "else " +
- " return 0 " +
- "end";
+ private static final String DELETE_IF_VALUE_MATCHES_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then "
+ + " return redis.call('del', KEYS[1]) " + "else " + " return 0 " + "end";
- private static final String SET_IF_VALUE_MATCHES_SCRIPT =
- "if redis.call('get', KEYS[1]) == ARGV[1] then " +
- " return redis.call('set', KEYS[1], ARGV[2], 'PX', ARGV[3]) " +
- "else " +
- " return nil " +
- "end";
+ private static final String SET_IF_VALUE_MATCHES_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then "
+ + " return redis.call('set', KEYS[1], ARGV[2], 'PX', ARGV[3]) " + "else " + " return nil " + "end";
/**
* Strategy for CAS/CAD operations.
@@ -87,10 +62,8 @@ public LettuceRedisDriver(RedisNodeConfiguration config) {
}
// Package-private constructor for testing with dependency injection
- LettuceRedisDriver(RedisNodeConfiguration config,
- RedisClient redisClient,
- StatefulRedisConnection connection,
- RedisCommands commands) {
+ LettuceRedisDriver(RedisNodeConfiguration config, RedisClient redisClient,
+ StatefulRedisConnection connection, RedisCommands commands) {
this.identifier = "redis://" + config.getHost() + ":" + config.getPort();
if (redisClient != null && connection != null && commands != null) {
@@ -101,11 +74,8 @@ public LettuceRedisDriver(RedisNodeConfiguration config) {
logger.debug("Created Lettuce driver for {} with injected dependencies", identifier);
} else {
// Create real connections (production)
- RedisURI.Builder uriBuilder = RedisURI.builder()
- .withHost(config.getHost())
- .withPort(config.getPort())
- .withDatabase(config.getDatabase())
- .withTimeout(Duration.ofMillis(config.getConnectionTimeoutMs()));
+ RedisURI.Builder uriBuilder = RedisURI.builder().withHost(config.getHost()).withPort(config.getPort())
+ .withDatabase(config.getDatabase()).withTimeout(Duration.ofMillis(config.getConnectionTimeoutMs()));
if (config.getPassword() != null && !config.getPassword().trim().isEmpty()) {
uriBuilder.withPassword(config.getPassword().toCharArray());
@@ -129,30 +99,25 @@ public LettuceRedisDriver(RedisNodeConfiguration config) {
}
/**
- * Detects whether native CAS/CAD commands are available.
- * This is called once during driver initialization.
+ * Detects whether native CAS/CAD commands are available. This is called once during driver initialization.
*/
private CADStrategy detectCADStrategy() {
try {
// Try to execute DELEX on a test key
String testKey = "__redlock4j_cad_test__" + System.currentTimeMillis();
- commands.dispatch(
- io.lettuce.core.protocol.CommandType.DELEX,
- new io.lettuce.core.output.IntegerOutput<>(io.lettuce.core.codec.StringCodec.UTF8),
- new io.lettuce.core.protocol.CommandArgs<>(io.lettuce.core.codec.StringCodec.UTF8)
- .addKey(testKey)
- .add("IFEQ")
- .add("test_value")
- );
+ commands.dispatch(io.lettuce.core.protocol.CommandType.DELEX,
+ new io.lettuce.core.output.IntegerOutput<>(io.lettuce.core.codec.StringCodec.UTF8),
+ new io.lettuce.core.protocol.CommandArgs<>(io.lettuce.core.codec.StringCodec.UTF8).addKey(testKey)
+ .add("IFEQ").add("test_value"));
logger.debug("Native CAS/CAD commands detected for {}", identifier);
return CADStrategy.NATIVE;
} catch (Exception e) {
- logger.debug("Native CAS/CAD commands not available for {}, using Lua scripts: {}",
- identifier, e.getMessage());
+ logger.debug("Native CAS/CAD commands not available for {}, using Lua scripts: {}", identifier,
+ e.getMessage());
return CADStrategy.SCRIPT;
}
}
-
+
@Override
public boolean setIfNotExists(String key, String value, long expireTimeMs) throws RedisDriverException {
try {
@@ -163,15 +128,15 @@ public boolean setIfNotExists(String key, String value, long expireTimeMs) throw
throw new RedisDriverException("Failed to execute SET NX PX command on " + identifier, e);
}
}
-
+
@Override
public boolean deleteIfValueMatches(String key, String expectedValue) throws RedisDriverException {
switch (cadStrategy) {
- case NATIVE:
+ case NATIVE :
return deleteIfValueMatchesNative(key, expectedValue);
- case SCRIPT:
+ case SCRIPT :
return deleteIfValueMatchesScript(key, expectedValue);
- default:
+ default :
throw new IllegalStateException("Unknown CAD strategy: " + cadStrategy);
}
}
@@ -181,14 +146,10 @@ public boolean deleteIfValueMatches(String key, String expectedValue) throws Red
*/
private boolean deleteIfValueMatchesNative(String key, String expectedValue) throws RedisDriverException {
try {
- Object result = commands.dispatch(
- io.lettuce.core.protocol.CommandType.DELEX,
- new io.lettuce.core.output.IntegerOutput<>(io.lettuce.core.codec.StringCodec.UTF8),
- new io.lettuce.core.protocol.CommandArgs<>(io.lettuce.core.codec.StringCodec.UTF8)
- .addKey(key)
- .add("IFEQ")
- .add(expectedValue)
- );
+ Object result = commands.dispatch(io.lettuce.core.protocol.CommandType.DELEX,
+ new io.lettuce.core.output.IntegerOutput<>(io.lettuce.core.codec.StringCodec.UTF8),
+ new io.lettuce.core.protocol.CommandArgs<>(io.lettuce.core.codec.StringCodec.UTF8).addKey(key)
+ .add("IFEQ").add(expectedValue));
return Long.valueOf(1).equals(result);
} catch (Exception e) {
throw new RedisDriverException("Failed to execute DELEX command on " + identifier, e);
@@ -200,18 +161,14 @@ private boolean deleteIfValueMatchesNative(String key, String expectedValue) thr
*/
private boolean deleteIfValueMatchesScript(String key, String expectedValue) throws RedisDriverException {
try {
- Object result = commands.eval(
- DELETE_IF_VALUE_MATCHES_SCRIPT,
- io.lettuce.core.ScriptOutputType.INTEGER,
- new String[]{key},
- expectedValue
- );
+ Object result = commands.eval(DELETE_IF_VALUE_MATCHES_SCRIPT, io.lettuce.core.ScriptOutputType.INTEGER,
+ new String[]{key}, expectedValue);
return Long.valueOf(1).equals(result);
} catch (Exception e) {
throw new RedisDriverException("Failed to execute delete script on " + identifier, e);
}
}
-
+
@Override
public boolean isConnected() {
try {
@@ -221,12 +178,12 @@ public boolean isConnected() {
return false;
}
}
-
+
@Override
public String getIdentifier() {
return identifier;
}
-
+
@Override
public void close() {
try {
@@ -243,14 +200,14 @@ public void close() {
}
@Override
- public boolean setIfValueMatches(String key, String newValue, String expectedCurrentValue,
- long expireTimeMs) throws RedisDriverException {
+ public boolean setIfValueMatches(String key, String newValue, String expectedCurrentValue, long expireTimeMs)
+ throws RedisDriverException {
switch (cadStrategy) {
- case NATIVE:
+ case NATIVE :
return setIfValueMatchesNative(key, newValue, expectedCurrentValue, expireTimeMs);
- case SCRIPT:
+ case SCRIPT :
return setIfValueMatchesScript(key, newValue, expectedCurrentValue, expireTimeMs);
- default:
+ default :
throw new IllegalStateException("Unknown CAD strategy: " + cadStrategy);
}
}
@@ -258,20 +215,13 @@ public boolean setIfValueMatches(String key, String newValue, String expectedCur
/**
* Sets a key using native SET IFEQ command (Redis 8.4+).
*/
- private boolean setIfValueMatchesNative(String key, String newValue, String expectedCurrentValue,
- long expireTimeMs) throws RedisDriverException {
+ private boolean setIfValueMatchesNative(String key, String newValue, String expectedCurrentValue, long expireTimeMs)
+ throws RedisDriverException {
try {
- String result = commands.dispatch(
- io.lettuce.core.protocol.CommandType.SET,
- new io.lettuce.core.output.StatusOutput<>(io.lettuce.core.codec.StringCodec.UTF8),
- new io.lettuce.core.protocol.CommandArgs<>(io.lettuce.core.codec.StringCodec.UTF8)
- .addKey(key)
- .addValue(newValue)
- .add("IFEQ")
- .add(expectedCurrentValue)
- .add("PX")
- .add(expireTimeMs)
- );
+ String result = commands.dispatch(io.lettuce.core.protocol.CommandType.SET,
+ new io.lettuce.core.output.StatusOutput<>(io.lettuce.core.codec.StringCodec.UTF8),
+ new io.lettuce.core.protocol.CommandArgs<>(io.lettuce.core.codec.StringCodec.UTF8).addKey(key)
+ .addValue(newValue).add("IFEQ").add(expectedCurrentValue).add("PX").add(expireTimeMs));
return "OK".equals(result);
} catch (Exception e) {
throw new RedisDriverException("Failed to execute SET IFEQ command on " + identifier, e);
@@ -281,15 +231,11 @@ private boolean setIfValueMatchesNative(String key, String newValue, String expe
/**
* Sets a key using Lua script (legacy compatibility).
*/
- private boolean setIfValueMatchesScript(String key, String newValue, String expectedCurrentValue,
- long expireTimeMs) throws RedisDriverException {
+ private boolean setIfValueMatchesScript(String key, String newValue, String expectedCurrentValue, long expireTimeMs)
+ throws RedisDriverException {
try {
- Object result = commands.eval(
- SET_IF_VALUE_MATCHES_SCRIPT,
- io.lettuce.core.ScriptOutputType.STATUS,
- new String[]{key},
- expectedCurrentValue, newValue, String.valueOf(expireTimeMs)
- );
+ Object result = commands.eval(SET_IF_VALUE_MATCHES_SCRIPT, io.lettuce.core.ScriptOutputType.STATUS,
+ new String[]{key}, expectedCurrentValue, newValue, String.valueOf(expireTimeMs));
return "OK".equals(result);
} catch (Exception e) {
throw new RedisDriverException("Failed to execute SET script on " + identifier, e);
diff --git a/src/main/java/org/codarama/redlock4j/driver/RedisDriver.java b/src/main/java/org/codarama/redlock4j/driver/RedisDriver.java
index e14f949..cc4f50d 100644
--- a/src/main/java/org/codarama/redlock4j/driver/RedisDriver.java
+++ b/src/main/java/org/codarama/redlock4j/driver/RedisDriver.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j.driver;
@@ -27,46 +8,55 @@
import java.util.Set;
/**
- * Abstraction over different Redis client libraries (Jedis, Lettuce).
- * Provides the minimal interface needed for implementing Redlock and advanced locking primitives.
+ * Abstraction over different Redis client libraries (Jedis, Lettuce). Provides the minimal interface needed for
+ * implementing Redlock and advanced locking primitives.
*
- *
This interface automatically uses the best available implementation for each operation:
+ *
+ * This interface automatically uses the best available implementation for each operation:
*
- *
Native Redis 8.4+ CAS/CAD commands when available (DELEX, SET IFEQ)
- *
Lua script-based operations for older Redis versions
+ *
Native Redis 8.4+ CAS/CAD commands when available (DELEX, SET IFEQ)
+ *
Lua script-based operations for older Redis versions
*
- * The detection and selection happens automatically at driver initialization.
+ * The detection and selection happens automatically at driver initialization.
+ *
*/
public interface RedisDriver extends AutoCloseable {
-
+
/**
- * Attempts to set a key with a value if the key doesn't exist, with an expiration time.
- * This corresponds to the Redis SET command with NX and PX options.
+ * Attempts to set a key with a value if the key doesn't exist, with an expiration time. This corresponds to the
+ * Redis SET command with NX and PX options.
*
- * @param key the key to set
- * @param value the value to set
- * @param expireTimeMs expiration time in milliseconds
+ * @param key
+ * the key to set
+ * @param value
+ * the value to set
+ * @param expireTimeMs
+ * expiration time in milliseconds
* @return true if the key was set, false if it already existed
- * @throws RedisDriverException if there's an error communicating with Redis
+ * @throws RedisDriverException
+ * if there's an error communicating with Redis
*/
boolean setIfNotExists(String key, String value, long expireTimeMs) throws RedisDriverException;
/**
- * Deletes a key only if its value matches the expected value.
- * This is used for safe lock release.
+ * Deletes a key only if its value matches the expected value. This is used for safe lock release.
*
- *
This method automatically uses the best available implementation:
+ *
+ * This method automatically uses the best available implementation:
*
- *
Redis 8.4+: Native DELEX command for optimal performance
- *
Older versions: Lua script for compatibility
+ *
Redis 8.4+: Native DELEX command for optimal performance
+ *
Older versions: Lua script for compatibility
*
- * The implementation is selected automatically at driver initialization based on
- * Redis server capabilities.
+ * The implementation is selected automatically at driver initialization based on Redis server capabilities.
+ *
*
- * @param key the key to potentially delete
- * @param expectedValue the expected value of the key
+ * @param key
+ * the key to potentially delete
+ * @param expectedValue
+ * the expected value of the key
* @return true if the key was deleted, false if it didn't exist or had a different value
- * @throws RedisDriverException if there's an error communicating with Redis
+ * @throws RedisDriverException
+ * if there's an error communicating with Redis
*/
boolean deleteIfValueMatches(String key, String expectedValue) throws RedisDriverException;
@@ -91,131 +81,163 @@ public interface RedisDriver extends AutoCloseable {
void close();
/**
- * Sets a key only if its current value matches the expected value.
- * This is used for atomic compare-and-swap operations like lock extension.
+ * Sets a key only if its current value matches the expected value. This is used for atomic compare-and-swap
+ * operations like lock extension.
*
- *
This method automatically uses the best available implementation:
+ *
+ * This method automatically uses the best available implementation:
*
- *
Redis 8.4+: Native SET IFEQ command for optimal performance
- *
Older versions: Lua script for compatibility
+ *
Redis 8.4+: Native SET IFEQ command for optimal performance
+ *
Older versions: Lua script for compatibility
*
- * The implementation is selected automatically at driver initialization based on
- * Redis server capabilities.
+ * The implementation is selected automatically at driver initialization based on Redis server capabilities.
+ *
*
- * @param key the key to set
- * @param newValue the new value to set
- * @param expectedCurrentValue the expected current value that must match
- * @param expireTimeMs expiration time in milliseconds
+ * @param key
+ * the key to set
+ * @param newValue
+ * the new value to set
+ * @param expectedCurrentValue
+ * the expected current value that must match
+ * @param expireTimeMs
+ * expiration time in milliseconds
* @return true if the key was set, false if the current value didn't match
- * @throws RedisDriverException if there's an error communicating with Redis
+ * @throws RedisDriverException
+ * if there's an error communicating with Redis
*/
- boolean setIfValueMatches(String key, String newValue,
- String expectedCurrentValue,
- long expireTimeMs)
+ boolean setIfValueMatches(String key, String newValue, String expectedCurrentValue, long expireTimeMs)
throws RedisDriverException;
// ========== Sorted Set Operations (for Fair Lock) ==========
/**
- * Adds a member to a sorted set with the given score.
- * If the member already exists, its score is updated.
+ * Adds a member to a sorted set with the given score. If the member already exists, its score is updated.
*
- * @param key the sorted set key
- * @param score the score for the member
- * @param member the member to add
+ * @param key
+ * the sorted set key
+ * @param score
+ * the score for the member
+ * @param member
+ * the member to add
* @return true if a new member was added, false if an existing member's score was updated
- * @throws RedisDriverException if there's an error communicating with Redis
+ * @throws RedisDriverException
+ * if there's an error communicating with Redis
*/
boolean zAdd(String key, double score, String member) throws RedisDriverException;
/**
* Removes a member from a sorted set.
*
- * @param key the sorted set key
- * @param member the member to remove
+ * @param key
+ * the sorted set key
+ * @param member
+ * the member to remove
* @return true if the member was removed, false if it didn't exist
- * @throws RedisDriverException if there's an error communicating with Redis
+ * @throws RedisDriverException
+ * if there's an error communicating with Redis
*/
boolean zRem(String key, String member) throws RedisDriverException;
/**
* Returns a range of members from a sorted set, ordered by score (ascending).
*
- * @param key the sorted set key
- * @param start the start index (0-based, inclusive)
- * @param stop the stop index (0-based, inclusive, -1 for end)
+ * @param key
+ * the sorted set key
+ * @param start
+ * the start index (0-based, inclusive)
+ * @param stop
+ * the stop index (0-based, inclusive, -1 for end)
* @return list of members in the specified range
- * @throws RedisDriverException if there's an error communicating with Redis
+ * @throws RedisDriverException
+ * if there's an error communicating with Redis
*/
List zRange(String key, long start, long stop) throws RedisDriverException;
/**
* Returns the score of a member in a sorted set.
*
- * @param key the sorted set key
- * @param member the member to get the score for
+ * @param key
+ * the sorted set key
+ * @param member
+ * the member to get the score for
* @return the score, or null if the member doesn't exist
- * @throws RedisDriverException if there's an error communicating with Redis
+ * @throws RedisDriverException
+ * if there's an error communicating with Redis
*/
Double zScore(String key, String member) throws RedisDriverException;
/**
* Removes all members from a sorted set with scores less than or equal to the given score.
*
- * @param key the sorted set key
- * @param maxScore the maximum score (inclusive)
+ * @param key
+ * the sorted set key
+ * @param maxScore
+ * the maximum score (inclusive)
* @return the number of members removed
- * @throws RedisDriverException if there's an error communicating with Redis
+ * @throws RedisDriverException
+ * if there's an error communicating with Redis
*/
long zRemRangeByScore(String key, double minScore, double maxScore) throws RedisDriverException;
// ========== String/Counter Operations (for ReadWriteLock, CountDownLatch) ==========
/**
- * Increments the integer value of a key by one.
- * If the key doesn't exist, it is set to 0 before performing the operation.
+ * Increments the integer value of a key by one. If the key doesn't exist, it is set to 0 before performing the
+ * operation.
*
- * @param key the key to increment
+ * @param key
+ * the key to increment
* @return the value after incrementing
- * @throws RedisDriverException if there's an error communicating with Redis
+ * @throws RedisDriverException
+ * if there's an error communicating with Redis
*/
long incr(String key) throws RedisDriverException;
/**
- * Decrements the integer value of a key by one.
- * If the key doesn't exist, it is set to 0 before performing the operation.
+ * Decrements the integer value of a key by one. If the key doesn't exist, it is set to 0 before performing the
+ * operation.
*
- * @param key the key to decrement
+ * @param key
+ * the key to decrement
* @return the value after decrementing
- * @throws RedisDriverException if there's an error communicating with Redis
+ * @throws RedisDriverException
+ * if there's an error communicating with Redis
*/
long decr(String key) throws RedisDriverException;
/**
* Gets the value of a key.
*
- * @param key the key to get
+ * @param key
+ * the key to get
* @return the value, or null if the key doesn't exist
- * @throws RedisDriverException if there's an error communicating with Redis
+ * @throws RedisDriverException
+ * if there's an error communicating with Redis
*/
String get(String key) throws RedisDriverException;
/**
* Sets a key to a value with an expiration time.
*
- * @param key the key to set
- * @param value the value to set
- * @param expireTimeMs expiration time in milliseconds
- * @throws RedisDriverException if there's an error communicating with Redis
+ * @param key
+ * the key to set
+ * @param value
+ * the value to set
+ * @param expireTimeMs
+ * expiration time in milliseconds
+ * @throws RedisDriverException
+ * if there's an error communicating with Redis
*/
void setex(String key, String value, long expireTimeMs) throws RedisDriverException;
/**
* Deletes one or more keys.
*
- * @param keys the keys to delete
+ * @param keys
+ * the keys to delete
* @return the number of keys that were deleted
- * @throws RedisDriverException if there's an error communicating with Redis
+ * @throws RedisDriverException
+ * if there's an error communicating with Redis
*/
long del(String... keys) throws RedisDriverException;
@@ -224,20 +246,26 @@ boolean setIfValueMatches(String key, String newValue,
/**
* Publishes a message to a channel.
*
- * @param channel the channel to publish to
- * @param message the message to publish
+ * @param channel
+ * the channel to publish to
+ * @param message
+ * the message to publish
* @return the number of clients that received the message
- * @throws RedisDriverException if there's an error communicating with Redis
+ * @throws RedisDriverException
+ * if there's an error communicating with Redis
*/
long publish(String channel, String message) throws RedisDriverException;
/**
- * Subscribes to one or more channels and processes messages with the given handler.
- * This is a blocking operation that should typically be run in a separate thread.
+ * Subscribes to one or more channels and processes messages with the given handler. This is a blocking operation
+ * that should typically be run in a separate thread.
*
- * @param handler the message handler
- * @param channels the channels to subscribe to
- * @throws RedisDriverException if there's an error communicating with Redis
+ * @param handler
+ * the message handler
+ * @param channels
+ * the channels to subscribe to
+ * @throws RedisDriverException
+ * if there's an error communicating with Redis
*/
void subscribe(MessageHandler handler, String... channels) throws RedisDriverException;
@@ -248,15 +276,18 @@ interface MessageHandler {
/**
* Called when a message is received on a subscribed channel.
*
- * @param channel the channel the message was received on
- * @param message the message content
+ * @param channel
+ * the channel the message was received on
+ * @param message
+ * the message content
*/
void onMessage(String channel, String message);
/**
* Called when an error occurs during subscription.
*
- * @param error the error that occurred
+ * @param error
+ * the error that occurred
*/
void onError(Throwable error);
}
diff --git a/src/main/java/org/codarama/redlock4j/driver/RedisDriverException.java b/src/main/java/org/codarama/redlock4j/driver/RedisDriverException.java
index e5e7996..6f2e7bc 100644
--- a/src/main/java/org/codarama/redlock4j/driver/RedisDriverException.java
+++ b/src/main/java/org/codarama/redlock4j/driver/RedisDriverException.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j.driver;
@@ -27,15 +8,15 @@
* Exception thrown when there's an error communicating with Redis through a driver.
*/
public class RedisDriverException extends Exception {
-
+
public RedisDriverException(String message) {
super(message);
}
-
+
public RedisDriverException(String message, Throwable cause) {
super(message, cause);
}
-
+
public RedisDriverException(Throwable cause) {
super(cause);
}
diff --git a/src/test/java/org/codarama/redlock4j/LockExtensionTest.java b/src/test/java/org/codarama/redlock4j/LockExtensionTest.java
index 41aab94..8354c69 100644
--- a/src/test/java/org/codarama/redlock4j/LockExtensionTest.java
+++ b/src/test/java/org/codarama/redlock4j/LockExtensionTest.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j;
@@ -45,30 +26,25 @@
*/
@ExtendWith(MockitoExtension.class)
public class LockExtensionTest {
-
+
@Mock
private RedisDriver mockDriver1;
-
+
@Mock
private RedisDriver mockDriver2;
-
+
@Mock
private RedisDriver mockDriver3;
-
+
private RedlockConfiguration testConfig;
private List drivers;
-
+
@BeforeEach
void setUp() {
- testConfig = RedlockConfiguration.builder()
- .addRedisNode("localhost", 6379)
- .addRedisNode("localhost", 6380)
- .addRedisNode("localhost", 6381)
- .defaultLockTimeout(Duration.ofSeconds(30))
- .retryDelay(Duration.ofMillis(100))
- .maxRetryAttempts(3)
- .lockAcquisitionTimeout(Duration.ofSeconds(10))
- .build();
+ testConfig = RedlockConfiguration.builder().addRedisNode("localhost", 6379).addRedisNode("localhost", 6380)
+ .addRedisNode("localhost", 6381).defaultLockTimeout(Duration.ofSeconds(30))
+ .retryDelay(Duration.ofMillis(100)).maxRetryAttempts(3).lockAcquisitionTimeout(Duration.ofSeconds(10))
+ .build();
drivers = Arrays.asList(mockDriver1, mockDriver2, mockDriver3);
@@ -77,192 +53,191 @@ void setUp() {
lenient().when(mockDriver2.getIdentifier()).thenReturn("redis://localhost:6380");
lenient().when(mockDriver3.getIdentifier()).thenReturn("redis://localhost:6381");
}
-
+
@Test
public void testExtendSuccess() throws RedisDriverException {
// Mock successful lock acquisition
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
-
+
// Mock successful extension on all nodes
when(mockDriver1.setIfValueMatches(anyString(), anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfValueMatches(anyString(), anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfValueMatches(anyString(), anyString(), anyString(), anyLong())).thenReturn(true);
-
+
Redlock lock = new Redlock("test-key", drivers, testConfig);
-
+
// Acquire lock
assertTrue(lock.tryLock());
long initialValidity = lock.getRemainingValidityTime();
-
+
// Extend lock
boolean extended = lock.extend(10000); // Extend by 10 seconds
-
+
assertTrue(extended);
assertTrue(lock.isHeldByCurrentThread());
-
+
// Validity time should be greater after extension
long newValidity = lock.getRemainingValidityTime();
- assertTrue(newValidity > initialValidity,
- "New validity (" + newValidity + ") should be greater than initial (" + initialValidity + ")");
-
+ assertTrue(newValidity > initialValidity,
+ "New validity (" + newValidity + ") should be greater than initial (" + initialValidity + ")");
+
// Verify setIfValueMatches was called on all drivers
verify(mockDriver1).setIfValueMatches(eq("test-key"), anyString(), anyString(), eq(40000L));
verify(mockDriver2).setIfValueMatches(eq("test-key"), anyString(), anyString(), eq(40000L));
verify(mockDriver3).setIfValueMatches(eq("test-key"), anyString(), anyString(), eq(40000L));
}
-
+
@Test
public void testExtendWithQuorum() throws RedisDriverException {
// Mock successful lock acquisition
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
-
+
// Mock extension succeeds on quorum (2 out of 3)
when(mockDriver1.setIfValueMatches(anyString(), anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfValueMatches(anyString(), anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfValueMatches(anyString(), anyString(), anyString(), anyLong())).thenReturn(false);
-
+
Redlock lock = new Redlock("test-key", drivers, testConfig);
-
+
// Acquire lock
assertTrue(lock.tryLock());
-
+
// Extend lock - should succeed with quorum
boolean extended = lock.extend(10000);
-
+
assertTrue(extended);
assertTrue(lock.isHeldByCurrentThread());
}
-
+
@Test
public void testExtendFailsWithoutQuorum() throws RedisDriverException {
// Mock successful lock acquisition
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
-
+
// Mock extension fails on majority (only 1 out of 3 succeeds)
when(mockDriver1.setIfValueMatches(anyString(), anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfValueMatches(anyString(), anyString(), anyString(), anyLong())).thenReturn(false);
when(mockDriver3.setIfValueMatches(anyString(), anyString(), anyString(), anyLong())).thenReturn(false);
-
+
Redlock lock = new Redlock("test-key", drivers, testConfig);
-
+
// Acquire lock
assertTrue(lock.tryLock());
-
+
// Extend lock - should fail without quorum
boolean extended = lock.extend(10000);
-
+
assertFalse(extended);
// Lock should still be held (original lock not affected)
assertTrue(lock.isHeldByCurrentThread());
}
-
+
@Test
public void testExtendWithoutHoldingLock() {
Redlock lock = new Redlock("test-key", drivers, testConfig);
-
+
// Try to extend without holding lock
boolean extended = lock.extend(10000);
-
+
assertFalse(extended);
assertFalse(lock.isHeldByCurrentThread());
}
-
+
@Test
public void testExtendWithNegativeTime() throws RedisDriverException {
// Mock successful lock acquisition
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
-
+
Redlock lock = new Redlock("test-key", drivers, testConfig);
-
+
// Acquire lock
assertTrue(lock.tryLock());
-
+
// Try to extend with negative time
assertThrows(IllegalArgumentException.class, () -> lock.extend(-1000));
}
-
+
@Test
public void testExtendWithZeroTime() throws RedisDriverException {
// Mock successful lock acquisition
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
-
+
Redlock lock = new Redlock("test-key", drivers, testConfig);
-
+
// Acquire lock
assertTrue(lock.tryLock());
-
+
// Try to extend with zero time
assertThrows(IllegalArgumentException.class, () -> lock.extend(0));
}
-
+
@Test
public void testExtendPreservesHoldCount() throws RedisDriverException {
// Mock successful lock acquisition
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
-
+
// Mock successful extension
when(mockDriver1.setIfValueMatches(anyString(), anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfValueMatches(anyString(), anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfValueMatches(anyString(), anyString(), anyString(), anyLong())).thenReturn(true);
-
+
Redlock lock = new Redlock("test-key", drivers, testConfig);
-
+
// Acquire lock twice (reentrant)
assertTrue(lock.tryLock());
assertTrue(lock.tryLock());
assertEquals(2, lock.getHoldCount());
-
+
// Extend lock
assertTrue(lock.extend(10000));
-
+
// Hold count should be preserved
assertEquals(2, lock.getHoldCount());
-
+
// Should need to unlock twice
lock.unlock();
assertEquals(1, lock.getHoldCount());
assertTrue(lock.isHeldByCurrentThread());
-
+
lock.unlock();
assertEquals(0, lock.getHoldCount());
assertFalse(lock.isHeldByCurrentThread());
}
-
+
@Test
public void testExtendWithDriverException() throws RedisDriverException {
// Mock successful lock acquisition
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
-
+
// Mock extension with one driver throwing exception
when(mockDriver1.setIfValueMatches(anyString(), anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfValueMatches(anyString(), anyString(), anyString(), anyLong()))
- .thenThrow(new RedisDriverException("Connection failed"));
+ .thenThrow(new RedisDriverException("Connection failed"));
when(mockDriver3.setIfValueMatches(anyString(), anyString(), anyString(), anyLong())).thenReturn(true);
-
+
Redlock lock = new Redlock("test-key", drivers, testConfig);
-
+
// Acquire lock
assertTrue(lock.tryLock());
-
+
// Extend should still succeed with quorum (2 out of 3)
boolean extended = lock.extend(10000);
-
+
assertTrue(extended);
assertTrue(lock.isHeldByCurrentThread());
}
}
-
diff --git a/src/test/java/org/codarama/redlock4j/RedlockManagerTest.java b/src/test/java/org/codarama/redlock4j/RedlockManagerTest.java
index 196870f..9cbe5ce 100644
--- a/src/test/java/org/codarama/redlock4j/RedlockManagerTest.java
+++ b/src/test/java/org/codarama/redlock4j/RedlockManagerTest.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j;
@@ -32,26 +13,20 @@
import static org.junit.jupiter.api.Assertions.*;
/**
- * Unit tests for RedlockManager.
- * These tests focus on the manager's public interface and configuration handling.
+ * Unit tests for RedlockManager. These tests focus on the manager's public interface and configuration handling.
*/
public class RedlockManagerTest {
-
+
private RedlockConfiguration testConfig;
-
+
@BeforeEach
void setUp() {
- testConfig = RedlockConfiguration.builder()
- .addRedisNode("localhost", 6379)
- .addRedisNode("localhost", 6380)
- .addRedisNode("localhost", 6381)
- .defaultLockTimeout(Duration.ofSeconds(30))
- .retryDelay(Duration.ofMillis(200))
- .maxRetryAttempts(3)
- .lockAcquisitionTimeout(Duration.ofSeconds(10))
- .build();
+ testConfig = RedlockConfiguration.builder().addRedisNode("localhost", 6379).addRedisNode("localhost", 6380)
+ .addRedisNode("localhost", 6381).defaultLockTimeout(Duration.ofSeconds(30))
+ .retryDelay(Duration.ofMillis(200)).maxRetryAttempts(3).lockAcquisitionTimeout(Duration.ofSeconds(10))
+ .build();
}
-
+
@Test
public void testConfigurationValidation() {
assertNotNull(testConfig);
@@ -61,27 +36,20 @@ public void testConfigurationValidation() {
assertEquals(3, testConfig.getMaxRetryAttempts());
assertEquals(10000, testConfig.getLockAcquisitionTimeoutMs());
}
-
+
@Test
public void testInvalidConfigurationWithTooFewNodes() {
- assertThrows(IllegalArgumentException.class, () ->
- RedlockConfiguration.builder()
- .addRedisNode("localhost", 6379)
- .addRedisNode("localhost", 6380)
- .build());
+ assertThrows(IllegalArgumentException.class, () -> RedlockConfiguration.builder()
+ .addRedisNode("localhost", 6379).addRedisNode("localhost", 6380).build());
}
-
+
@Test
public void testInvalidConfigurationWithNegativeTimeout() {
- assertThrows(IllegalArgumentException.class, () ->
- RedlockConfiguration.builder()
- .addRedisNode("localhost", 6379)
- .addRedisNode("localhost", 6380)
- .addRedisNode("localhost", 6381)
- .defaultLockTimeout(Duration.ofSeconds(-1))
- .build());
+ assertThrows(IllegalArgumentException.class,
+ () -> RedlockConfiguration.builder().addRedisNode("localhost", 6379).addRedisNode("localhost", 6380)
+ .addRedisNode("localhost", 6381).defaultLockTimeout(Duration.ofSeconds(-1)).build());
}
-
+
@Test
public void testQuorumCalculation() {
// Test quorum calculation for different node counts
@@ -89,20 +57,14 @@ public void testQuorumCalculation() {
assertEquals(3, (5 / 2) + 1); // 5 nodes -> quorum of 3
assertEquals(4, (7 / 2) + 1); // 7 nodes -> quorum of 4
}
-
+
@Test
public void testConfigurationBuilderPattern() {
- RedlockConfiguration config = RedlockConfiguration.builder()
- .addRedisNode("redis1.example.com", 6379)
- .addRedisNode("redis2.example.com", 6379)
- .addRedisNode("redis3.example.com", 6379)
- .defaultLockTimeout(Duration.ofMinutes(1))
- .retryDelay(Duration.ofMillis(500))
- .maxRetryAttempts(5)
- .clockDriftFactor(0.02)
- .lockAcquisitionTimeout(Duration.ofSeconds(30))
- .build();
-
+ RedlockConfiguration config = RedlockConfiguration.builder().addRedisNode("redis1.example.com", 6379)
+ .addRedisNode("redis2.example.com", 6379).addRedisNode("redis3.example.com", 6379)
+ .defaultLockTimeout(Duration.ofMinutes(1)).retryDelay(Duration.ofMillis(500)).maxRetryAttempts(5)
+ .clockDriftFactor(0.02).lockAcquisitionTimeout(Duration.ofSeconds(30)).build();
+
assertNotNull(config);
assertEquals(3, config.getRedisNodes().size());
assertEquals(60000, config.getDefaultLockTimeoutMs());
@@ -111,27 +73,20 @@ public void testConfigurationBuilderPattern() {
assertEquals(0.02, config.getClockDriftFactor(), 0.001);
assertEquals(30000, config.getLockAcquisitionTimeoutMs());
}
-
+
@Test
public void testConfigurationWithCustomClockDrift() {
- RedlockConfiguration config = RedlockConfiguration.builder()
- .addRedisNode("localhost", 6379)
- .addRedisNode("localhost", 6380)
- .addRedisNode("localhost", 6381)
- .clockDriftFactor(0.05)
- .build();
-
+ RedlockConfiguration config = RedlockConfiguration.builder().addRedisNode("localhost", 6379)
+ .addRedisNode("localhost", 6380).addRedisNode("localhost", 6381).clockDriftFactor(0.05).build();
+
assertEquals(0.05, config.getClockDriftFactor(), 0.001);
}
-
+
@Test
public void testConfigurationDefaults() {
- RedlockConfiguration config = RedlockConfiguration.builder()
- .addRedisNode("localhost", 6379)
- .addRedisNode("localhost", 6380)
- .addRedisNode("localhost", 6381)
- .build();
-
+ RedlockConfiguration config = RedlockConfiguration.builder().addRedisNode("localhost", 6379)
+ .addRedisNode("localhost", 6380).addRedisNode("localhost", 6381).build();
+
// Test default values
assertEquals(Duration.ofSeconds(30).toMillis(), config.getDefaultLockTimeoutMs());
assertEquals(200, config.getRetryDelayMs());
diff --git a/src/test/java/org/codarama/redlock4j/RedlockTest.java b/src/test/java/org/codarama/redlock4j/RedlockTest.java
index ec3360a..ae3050f 100644
--- a/src/test/java/org/codarama/redlock4j/RedlockTest.java
+++ b/src/test/java/org/codarama/redlock4j/RedlockTest.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j;
@@ -46,30 +27,25 @@
*/
@ExtendWith(MockitoExtension.class)
public class RedlockTest {
-
+
@Mock
private RedisDriver mockDriver1;
-
+
@Mock
private RedisDriver mockDriver2;
-
+
@Mock
private RedisDriver mockDriver3;
-
+
private RedlockConfiguration testConfig;
private List drivers;
-
+
@BeforeEach
void setUp() {
- testConfig = RedlockConfiguration.builder()
- .addRedisNode("localhost", 6379)
- .addRedisNode("localhost", 6380)
- .addRedisNode("localhost", 6381)
- .defaultLockTimeout(Duration.ofSeconds(30))
- .retryDelay(Duration.ofMillis(100))
- .maxRetryAttempts(3)
- .lockAcquisitionTimeout(Duration.ofSeconds(10))
- .build();
+ testConfig = RedlockConfiguration.builder().addRedisNode("localhost", 6379).addRedisNode("localhost", 6380)
+ .addRedisNode("localhost", 6381).defaultLockTimeout(Duration.ofSeconds(30))
+ .retryDelay(Duration.ofMillis(100)).maxRetryAttempts(3).lockAcquisitionTimeout(Duration.ofSeconds(10))
+ .build();
drivers = Arrays.asList(mockDriver1, mockDriver2, mockDriver3);
@@ -78,121 +54,121 @@ void setUp() {
lenient().when(mockDriver2.getIdentifier()).thenReturn("redis://localhost:6380");
lenient().when(mockDriver3.getIdentifier()).thenReturn("redis://localhost:6381");
}
-
+
@Test
public void testTryLockSuccess() throws RedisDriverException {
// Mock successful lock acquisition on all nodes
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
-
+
Redlock lock = new Redlock("test-key", drivers, testConfig);
-
+
boolean acquired = lock.tryLock();
-
+
assertTrue(acquired);
assertTrue(lock.isHeldByCurrentThread());
assertTrue(lock.getRemainingValidityTime() > 0);
-
+
// Verify all drivers were called
verify(mockDriver1).setIfNotExists(eq("test-key"), anyString(), eq(30000L));
verify(mockDriver2).setIfNotExists(eq("test-key"), anyString(), eq(30000L));
verify(mockDriver3).setIfNotExists(eq("test-key"), anyString(), eq(30000L));
}
-
+
@Test
public void testTryLockFailureInsufficientNodes() throws RedisDriverException {
// Mock failure on majority of nodes (only 1 success, need 2 for quorum)
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false);
-
+
Redlock lock = new Redlock("test-key", drivers, testConfig);
-
+
boolean acquired = lock.tryLock();
-
+
assertFalse(acquired);
assertFalse(lock.isHeldByCurrentThread());
assertEquals(0, lock.getRemainingValidityTime());
-
+
// Verify cleanup - should try to delete the lock that was acquired
verify(mockDriver1, atLeastOnce()).deleteIfValueMatches(eq("test-key"), anyString());
verify(mockDriver2, atLeastOnce()).deleteIfValueMatches(eq("test-key"), anyString());
verify(mockDriver3, atLeastOnce()).deleteIfValueMatches(eq("test-key"), anyString());
}
-
+
@Test
public void testTryLockWithTimeout() throws InterruptedException, RedisDriverException {
// Mock successful lock acquisition on quorum
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false);
-
+
Redlock lock = new Redlock("test-key", drivers, testConfig);
-
+
boolean acquired = lock.tryLock(5, TimeUnit.SECONDS);
-
+
assertTrue(acquired);
assertTrue(lock.isHeldByCurrentThread());
}
-
+
@Test
public void testTryLockTimeoutExceeded() throws InterruptedException, RedisDriverException {
// Mock failure on all attempts
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false);
-
+
Redlock lock = new Redlock("test-key", drivers, testConfig);
-
+
long startTime = System.currentTimeMillis();
boolean acquired = lock.tryLock(200, TimeUnit.MILLISECONDS);
long elapsedTime = System.currentTimeMillis() - startTime;
-
+
assertFalse(acquired);
assertFalse(lock.isHeldByCurrentThread());
assertTrue(elapsedTime >= 200); // Should respect timeout
}
-
+
@Test
public void testLockSuccess() throws RedisDriverException {
// Mock successful lock acquisition
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false);
-
+
Redlock lock = new Redlock("test-key", drivers, testConfig);
-
+
assertDoesNotThrow(lock::lock);
assertTrue(lock.isHeldByCurrentThread());
}
-
+
@Test
public void testLockFailureThrowsException() throws RedisDriverException {
// Mock failure on all nodes
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false);
-
+
Redlock lock = new Redlock("test-key", drivers, testConfig);
RedlockException exception = assertThrows(RedlockException.class, () -> lock.lock());
-
+
assertTrue(exception.getMessage().contains("Failed to acquire lock within timeout"));
}
-
+
@Test
public void testLockInterruptibly() throws RedisDriverException {
// Mock successful lock acquisition
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false);
-
+
Redlock lock = new Redlock("test-key", drivers, testConfig);
-
+
assertDoesNotThrow(lock::lockInterruptibly);
assertTrue(lock.isHeldByCurrentThread());
}
-
+
@Test
public void testLockInterruptiblyWithInterruption() throws RedisDriverException {
// Use lenient stubbing to avoid unnecessary stubbing warnings
@@ -210,93 +186,89 @@ public void testLockInterruptiblyWithInterruption() throws RedisDriverException
// Clear interrupt flag
Thread.interrupted();
}
-
+
@Test
public void testUnlockSuccess() throws RedisDriverException {
// First acquire the lock
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false);
-
+
Redlock lock = new Redlock("test-key", drivers, testConfig);
assertTrue(lock.tryLock());
-
+
// Now unlock
lock.unlock();
-
+
assertFalse(lock.isHeldByCurrentThread());
assertEquals(0, lock.getRemainingValidityTime());
-
+
// Verify unlock was called on all drivers
verify(mockDriver1, atLeastOnce()).deleteIfValueMatches(eq("test-key"), anyString());
verify(mockDriver2, atLeastOnce()).deleteIfValueMatches(eq("test-key"), anyString());
verify(mockDriver3, atLeastOnce()).deleteIfValueMatches(eq("test-key"), anyString());
}
-
+
@Test
public void testUnlockWithoutLock() {
Redlock lock = new Redlock("test-key", drivers, testConfig);
-
+
// Should not throw exception when unlocking without holding lock
assertDoesNotThrow(lock::unlock);
-
+
assertFalse(lock.isHeldByCurrentThread());
}
-
+
@Test
public void testDriverExceptionDuringLockAcquisition() throws RedisDriverException {
// Mock exception on one driver, success on others
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong()))
- .thenThrow(new RedisDriverException("Connection failed"));
+ .thenThrow(new RedisDriverException("Connection failed"));
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
-
+
Redlock lock = new Redlock("test-key", drivers, testConfig);
-
+
boolean acquired = lock.tryLock();
-
+
assertTrue(acquired); // Should still succeed with 2/3 nodes
assertTrue(lock.isHeldByCurrentThread());
}
-
+
@Test
public void testDriverExceptionDuringUnlock() throws RedisDriverException {
// First acquire the lock
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false);
-
+
Redlock lock = new Redlock("test-key", drivers, testConfig);
assertTrue(lock.tryLock());
-
+
// Mock exception during unlock
- doThrow(new RedisDriverException("Delete failed")).when(mockDriver1)
- .deleteIfValueMatches(anyString(), anyString());
-
+ doThrow(new RedisDriverException("Delete failed")).when(mockDriver1).deleteIfValueMatches(anyString(),
+ anyString());
+
// Should not throw exception
assertDoesNotThrow(lock::unlock);
-
+
assertFalse(lock.isHeldByCurrentThread());
}
-
+
@Test
public void testNewConditionThrowsUnsupportedOperation() {
Redlock lock = new Redlock("test-key", drivers, testConfig);
-
+
assertThrows(UnsupportedOperationException.class, lock::newCondition);
}
-
+
@Test
public void testIsHeldByCurrentThreadAfterExpiry() throws RedisDriverException, InterruptedException {
// Use very short timeout for testing expiry with 3 nodes to satisfy validation
- RedlockConfiguration shortConfig = RedlockConfiguration.builder()
- .addRedisNode("localhost", 6379)
- .addRedisNode("localhost", 6380)
- .addRedisNode("localhost", 6381)
- .defaultLockTimeout(Duration.ofMillis(50))
- .retryDelay(Duration.ofMillis(10))
- .maxRetryAttempts(1)
- .build();
+ RedlockConfiguration shortConfig = RedlockConfiguration.builder().addRedisNode("localhost", 6379)
+ .addRedisNode("localhost", 6380).addRedisNode("localhost", 6381)
+ .defaultLockTimeout(Duration.ofMillis(50)).retryDelay(Duration.ofMillis(10)).maxRetryAttempts(1)
+ .build();
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
@@ -317,14 +289,10 @@ public void testIsHeldByCurrentThreadAfterExpiry() throws RedisDriverException,
@Test
public void testUnlockExpiredLock() throws RedisDriverException, InterruptedException {
// Use very short timeout for testing expiry with 3 nodes to satisfy validation
- RedlockConfiguration shortConfig = RedlockConfiguration.builder()
- .addRedisNode("localhost", 6379)
- .addRedisNode("localhost", 6380)
- .addRedisNode("localhost", 6381)
- .defaultLockTimeout(Duration.ofMillis(50))
- .retryDelay(Duration.ofMillis(10))
- .maxRetryAttempts(1)
- .build();
+ RedlockConfiguration shortConfig = RedlockConfiguration.builder().addRedisNode("localhost", 6379)
+ .addRedisNode("localhost", 6380).addRedisNode("localhost", 6381)
+ .defaultLockTimeout(Duration.ofMillis(50)).retryDelay(Duration.ofMillis(10)).maxRetryAttempts(1)
+ .build();
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
@@ -344,12 +312,10 @@ public void testUnlockExpiredLock() throws RedisDriverException, InterruptedExce
@Test
public void testRetryLogicWithEventualSuccess() throws RedisDriverException {
// Mock failure on first attempts, success on later attempt
- when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong()))
- .thenReturn(false) // First attempt fails
- .thenReturn(true); // Second attempt succeeds
- when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong()))
- .thenReturn(false) // First attempt fails
- .thenReturn(true); // Second attempt succeeds
+ when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false) // First attempt fails
+ .thenReturn(true); // Second attempt succeeds
+ when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false) // First attempt fails
+ .thenReturn(true); // Second attempt succeeds
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false);
Redlock lock = new Redlock("test-key", drivers, testConfig);
@@ -384,7 +350,7 @@ public void testClockDriftCalculation() throws RedisDriverException {
// Validity time should be reduced due to elapsed time and clock drift
long remainingTime = lock.getRemainingValidityTime();
assertTrue(remainingTime < 30000); // Should be less than full timeout
- assertTrue(remainingTime > 0); // But still positive
+ assertTrue(remainingTime > 0); // But still positive
}
@Test
diff --git a/src/test/java/org/codarama/redlock4j/ReentrantAsyncRedlockTest.java b/src/test/java/org/codarama/redlock4j/ReentrantAsyncRedlockTest.java
index 9a8b3a4..87e1d00 100644
--- a/src/test/java/org/codarama/redlock4j/ReentrantAsyncRedlockTest.java
+++ b/src/test/java/org/codarama/redlock4j/ReentrantAsyncRedlockTest.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j;
@@ -52,252 +33,247 @@
*/
@ExtendWith(MockitoExtension.class)
public class ReentrantAsyncRedlockTest {
-
+
@Mock
private RedisDriver mockDriver1;
-
+
@Mock
private RedisDriver mockDriver2;
-
+
@Mock
private RedisDriver mockDriver3;
-
+
private RedlockConfiguration testConfig;
private List drivers;
private ExecutorService executorService;
private ScheduledExecutorService scheduledExecutorService;
-
+
@BeforeEach
void setUp() {
- testConfig = RedlockConfiguration.builder()
- .addRedisNode("localhost", 6379)
- .addRedisNode("localhost", 6380)
- .addRedisNode("localhost", 6381)
- .defaultLockTimeout(Duration.ofSeconds(30))
- .retryDelay(Duration.ofMillis(100))
- .maxRetryAttempts(3)
- .lockAcquisitionTimeout(Duration.ofSeconds(10))
- .build();
-
+ testConfig = RedlockConfiguration.builder().addRedisNode("localhost", 6379).addRedisNode("localhost", 6380)
+ .addRedisNode("localhost", 6381).defaultLockTimeout(Duration.ofSeconds(30))
+ .retryDelay(Duration.ofMillis(100)).maxRetryAttempts(3).lockAcquisitionTimeout(Duration.ofSeconds(10))
+ .build();
+
drivers = Arrays.asList(mockDriver1, mockDriver2, mockDriver3);
executorService = Executors.newFixedThreadPool(4);
scheduledExecutorService = Executors.newScheduledThreadPool(2);
-
+
// Setup default mock behavior
lenient().when(mockDriver1.getIdentifier()).thenReturn("redis://localhost:6379");
lenient().when(mockDriver2.getIdentifier()).thenReturn("redis://localhost:6380");
lenient().when(mockDriver3.getIdentifier()).thenReturn("redis://localhost:6381");
}
-
+
@AfterEach
void tearDown() {
executorService.shutdown();
scheduledExecutorService.shutdown();
}
-
+
@Test
public void testBasicAsyncReentrancy() throws Exception {
// Mock successful lock acquisition on quorum
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false);
-
- AsyncRedlockImpl lock = new AsyncRedlockImpl("test-key", drivers, testConfig,
- executorService, scheduledExecutorService);
-
+
+ AsyncRedlockImpl lock = new AsyncRedlockImpl("test-key", drivers, testConfig, executorService,
+ scheduledExecutorService);
+
// First acquisition
CompletionStage firstAcquisition = lock.tryLockAsync();
assertTrue(firstAcquisition.toCompletableFuture().get(5, TimeUnit.SECONDS));
assertTrue(lock.isHeldByCurrentThread());
assertEquals(1, lock.getHoldCount());
-
+
// Second acquisition (reentrant)
CompletionStage secondAcquisition = lock.tryLockAsync();
assertTrue(secondAcquisition.toCompletableFuture().get(5, TimeUnit.SECONDS));
assertTrue(lock.isHeldByCurrentThread());
assertEquals(2, lock.getHoldCount());
-
+
// Third acquisition (reentrant)
CompletionStage thirdAcquisition = lock.tryLockAsync();
assertTrue(thirdAcquisition.toCompletableFuture().get(5, TimeUnit.SECONDS));
assertTrue(lock.isHeldByCurrentThread());
assertEquals(3, lock.getHoldCount());
-
+
// First unlock
CompletionStage firstUnlock = lock.unlockAsync();
firstUnlock.toCompletableFuture().get(5, TimeUnit.SECONDS);
assertTrue(lock.isHeldByCurrentThread());
assertEquals(2, lock.getHoldCount());
-
+
// Second unlock
CompletionStage secondUnlock = lock.unlockAsync();
secondUnlock.toCompletableFuture().get(5, TimeUnit.SECONDS);
assertTrue(lock.isHeldByCurrentThread());
assertEquals(1, lock.getHoldCount());
-
+
// Final unlock
CompletionStage finalUnlock = lock.unlockAsync();
finalUnlock.toCompletableFuture().get(5, TimeUnit.SECONDS);
assertFalse(lock.isHeldByCurrentThread());
assertEquals(0, lock.getHoldCount());
-
+
// Verify Redis operations were called only once (for initial acquisition)
verify(mockDriver1, times(1)).setIfNotExists(anyString(), anyString(), anyLong());
verify(mockDriver2, times(1)).setIfNotExists(anyString(), anyString(), anyLong());
verify(mockDriver3, times(1)).setIfNotExists(anyString(), anyString(), anyLong());
-
+
// Verify unlock was called only once (for final release)
verify(mockDriver1, times(1)).deleteIfValueMatches(anyString(), anyString());
verify(mockDriver2, times(1)).deleteIfValueMatches(anyString(), anyString());
verify(mockDriver3, times(1)).deleteIfValueMatches(anyString(), anyString());
}
-
+
@Test
public void testAsyncReentrantLockWithTimeout() throws Exception {
// Mock successful lock acquisition
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false);
-
- AsyncRedlockImpl lock = new AsyncRedlockImpl("test-key", drivers, testConfig,
- executorService, scheduledExecutorService);
-
+
+ AsyncRedlockImpl lock = new AsyncRedlockImpl("test-key", drivers, testConfig, executorService,
+ scheduledExecutorService);
+
// First acquisition
CompletionStage firstAcquisition = lock.tryLockAsync(Duration.ofSeconds(5));
assertTrue(firstAcquisition.toCompletableFuture().get(10, TimeUnit.SECONDS));
assertEquals(1, lock.getHoldCount());
-
+
// Reentrant acquisition with timeout
CompletionStage secondAcquisition = lock.tryLockAsync(Duration.ofSeconds(5));
assertTrue(secondAcquisition.toCompletableFuture().get(10, TimeUnit.SECONDS));
assertEquals(2, lock.getHoldCount());
-
+
// Unlock both
lock.unlockAsync().toCompletableFuture().get(5, TimeUnit.SECONDS);
assertEquals(1, lock.getHoldCount());
lock.unlockAsync().toCompletableFuture().get(5, TimeUnit.SECONDS);
assertEquals(0, lock.getHoldCount());
-
+
// Verify Redis operations were called only once
verify(mockDriver1, times(1)).setIfNotExists(anyString(), anyString(), anyLong());
}
-
+
@Test
public void testAsyncReentrantLockWithLockAsync() throws Exception {
// Mock successful lock acquisition
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false);
-
- AsyncRedlockImpl lock = new AsyncRedlockImpl("test-key", drivers, testConfig,
- executorService, scheduledExecutorService);
-
+
+ AsyncRedlockImpl lock = new AsyncRedlockImpl("test-key", drivers, testConfig, executorService,
+ scheduledExecutorService);
+
// First acquisition using lockAsync()
CompletionStage firstLock = lock.lockAsync();
assertDoesNotThrow(() -> firstLock.toCompletableFuture().get(5, TimeUnit.SECONDS));
assertEquals(1, lock.getHoldCount());
-
+
// Reentrant acquisition using tryLockAsync()
CompletionStage secondLock = lock.tryLockAsync();
assertTrue(secondLock.toCompletableFuture().get(5, TimeUnit.SECONDS));
assertEquals(2, lock.getHoldCount());
-
+
// Unlock both
lock.unlockAsync().toCompletableFuture().get(5, TimeUnit.SECONDS);
assertEquals(1, lock.getHoldCount());
lock.unlockAsync().toCompletableFuture().get(5, TimeUnit.SECONDS);
assertEquals(0, lock.getHoldCount());
}
-
+
@Test
public void testAsyncUnlockWithoutLockDoesNotThrow() throws Exception {
- AsyncRedlockImpl lock = new AsyncRedlockImpl("test-key", drivers, testConfig,
- executorService, scheduledExecutorService);
-
+ AsyncRedlockImpl lock = new AsyncRedlockImpl("test-key", drivers, testConfig, executorService,
+ scheduledExecutorService);
+
// Should not throw exception when unlocking without holding lock
CompletionStage unlock = lock.unlockAsync();
assertDoesNotThrow(() -> unlock.toCompletableFuture().get(5, TimeUnit.SECONDS));
assertEquals(0, lock.getHoldCount());
}
-
+
@Test
public void testRxReentrantLock() throws Exception {
// Mock successful lock acquisition
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false);
-
- AsyncRedlockImpl lock = new AsyncRedlockImpl("test-key", drivers, testConfig,
- executorService, scheduledExecutorService);
-
+
+ AsyncRedlockImpl lock = new AsyncRedlockImpl("test-key", drivers, testConfig, executorService,
+ scheduledExecutorService);
+
// First acquisition using RxJava
Boolean firstResult = lock.tryLockRx().blockingGet();
assertTrue(firstResult);
assertEquals(1, lock.getHoldCount());
-
+
// Reentrant acquisition using RxJava
Boolean secondResult = lock.tryLockRx().blockingGet();
assertTrue(secondResult);
assertEquals(2, lock.getHoldCount());
-
+
// Unlock both using RxJava
assertDoesNotThrow(() -> lock.unlockRx().blockingAwait());
assertEquals(1, lock.getHoldCount());
assertDoesNotThrow(() -> lock.unlockRx().blockingAwait());
assertEquals(0, lock.getHoldCount());
-
+
// Verify Redis operations were called only once
verify(mockDriver1, times(1)).setIfNotExists(anyString(), anyString(), anyLong());
}
-
+
@Test
public void testAsyncReentrantLockValidityTime() throws Exception {
// Mock successful lock acquisition
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false);
-
- AsyncRedlockImpl lock = new AsyncRedlockImpl("test-key", drivers, testConfig,
- executorService, scheduledExecutorService);
-
+
+ AsyncRedlockImpl lock = new AsyncRedlockImpl("test-key", drivers, testConfig, executorService,
+ scheduledExecutorService);
+
// Acquire lock
assertTrue(lock.tryLockAsync().toCompletableFuture().get(5, TimeUnit.SECONDS));
long firstValidityTime = lock.getRemainingValidityTime();
assertTrue(firstValidityTime > 0);
-
+
// Reentrant acquisition should not change validity time significantly
assertTrue(lock.tryLockAsync().toCompletableFuture().get(5, TimeUnit.SECONDS));
long secondValidityTime = lock.getRemainingValidityTime();
assertTrue(secondValidityTime > 0);
// Should be approximately the same (allowing for small time differences)
assertTrue(Math.abs(firstValidityTime - secondValidityTime) < 1000);
-
+
// Unlock both
lock.unlockAsync().toCompletableFuture().get(5, TimeUnit.SECONDS);
assertTrue(lock.getRemainingValidityTime() > 0); // Still valid after first unlock
-
+
lock.unlockAsync().toCompletableFuture().get(5, TimeUnit.SECONDS);
assertEquals(0, lock.getRemainingValidityTime()); // Should be 0 after final unlock
}
-
+
@Test
public void testAsyncReentrantLockKey() throws Exception {
- AsyncRedlockImpl lock = new AsyncRedlockImpl("test-key", drivers, testConfig,
- executorService, scheduledExecutorService);
-
+ AsyncRedlockImpl lock = new AsyncRedlockImpl("test-key", drivers, testConfig, executorService,
+ scheduledExecutorService);
+
assertEquals("test-key", lock.getLockKey());
-
+
// Lock key should remain the same regardless of hold count
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false);
-
+
assertTrue(lock.tryLockAsync().toCompletableFuture().get(5, TimeUnit.SECONDS));
assertEquals("test-key", lock.getLockKey());
-
+
assertTrue(lock.tryLockAsync().toCompletableFuture().get(5, TimeUnit.SECONDS));
assertEquals("test-key", lock.getLockKey());
-
+
lock.unlockAsync().toCompletableFuture().get(5, TimeUnit.SECONDS);
lock.unlockAsync().toCompletableFuture().get(5, TimeUnit.SECONDS);
}
diff --git a/src/test/java/org/codarama/redlock4j/ReentrantLockDemo.java b/src/test/java/org/codarama/redlock4j/ReentrantLockDemo.java
index 4568e0e..155c15d 100644
--- a/src/test/java/org/codarama/redlock4j/ReentrantLockDemo.java
+++ b/src/test/java/org/codarama/redlock4j/ReentrantLockDemo.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j;
@@ -49,213 +30,208 @@
*/
@ExtendWith(MockitoExtension.class)
public class ReentrantLockDemo {
-
+
@Mock
private RedisDriver mockDriver1;
-
+
@Mock
private RedisDriver mockDriver2;
-
+
@Mock
private RedisDriver mockDriver3;
-
+
private RedlockConfiguration testConfig;
private List drivers;
-
+
@BeforeEach
void setUp() {
- testConfig = RedlockConfiguration.builder()
- .addRedisNode("localhost", 6379)
- .addRedisNode("localhost", 6380)
- .addRedisNode("localhost", 6381)
- .defaultLockTimeout(Duration.ofSeconds(30))
- .retryDelay(Duration.ofMillis(100))
- .maxRetryAttempts(3)
- .lockAcquisitionTimeout(Duration.ofSeconds(10))
- .build();
-
+ testConfig = RedlockConfiguration.builder().addRedisNode("localhost", 6379).addRedisNode("localhost", 6380)
+ .addRedisNode("localhost", 6381).defaultLockTimeout(Duration.ofSeconds(30))
+ .retryDelay(Duration.ofMillis(100)).maxRetryAttempts(3).lockAcquisitionTimeout(Duration.ofSeconds(10))
+ .build();
+
drivers = Arrays.asList(mockDriver1, mockDriver2, mockDriver3);
-
+
// Setup default mock behavior
lenient().when(mockDriver1.getIdentifier()).thenReturn("redis://localhost:6379");
lenient().when(mockDriver2.getIdentifier()).thenReturn("redis://localhost:6380");
lenient().when(mockDriver3.getIdentifier()).thenReturn("redis://localhost:6381");
}
-
+
@Test
public void demonstrateReentrantLockUsage() throws RedisDriverException {
// Mock successful lock acquisition on quorum
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false);
-
+
Redlock lock = new Redlock("demo-key", drivers, testConfig);
-
+
System.out.println("=== Reentrant Lock Demonstration ===");
-
+
// Method that acquires the lock
performCriticalSection(lock);
-
+
// Verify the lock is fully released
assertFalse(lock.isHeldByCurrentThread());
assertEquals(0, lock.getHoldCount());
-
+
System.out.println("✅ Demonstration completed successfully!");
}
-
+
private void performCriticalSection(Redlock lock) {
System.out.println("🔒 Acquiring lock in performCriticalSection()");
lock.lock();
-
+
try {
System.out.println(" Hold count: " + lock.getHoldCount());
System.out.println(" Is held by current thread: " + lock.isHeldByCurrentThread());
-
+
// Call another method that also needs the same lock
performNestedOperation(lock);
-
+
System.out.println(" Back in performCriticalSection()");
System.out.println(" Hold count: " + lock.getHoldCount());
-
+
} finally {
System.out.println("🔓 Releasing lock in performCriticalSection()");
lock.unlock();
System.out.println(" Hold count after unlock: " + lock.getHoldCount());
}
}
-
+
private void performNestedOperation(Redlock lock) {
System.out.println(" 🔒 Acquiring lock in performNestedOperation() (reentrant)");
lock.lock();
-
+
try {
System.out.println(" Hold count: " + lock.getHoldCount());
System.out.println(" Is held by current thread: " + lock.isHeldByCurrentThread());
-
+
// Call yet another method that needs the lock
performDeeplyNestedOperation(lock);
-
+
System.out.println(" Back in performNestedOperation()");
System.out.println(" Hold count: " + lock.getHoldCount());
-
+
} finally {
System.out.println(" 🔓 Releasing lock in performNestedOperation()");
lock.unlock();
System.out.println(" Hold count after unlock: " + lock.getHoldCount());
}
}
-
+
private void performDeeplyNestedOperation(Redlock lock) {
System.out.println(" 🔒 Acquiring lock in performDeeplyNestedOperation() (reentrant)");
lock.lock();
-
+
try {
System.out.println(" Hold count: " + lock.getHoldCount());
System.out.println(" Is held by current thread: " + lock.isHeldByCurrentThread());
System.out.println(" Performing deeply nested critical work...");
-
+
} finally {
System.out.println(" 🔓 Releasing lock in performDeeplyNestedOperation()");
lock.unlock();
System.out.println(" Hold count after unlock: " + lock.getHoldCount());
}
}
-
+
@Test
public void demonstrateAsyncReentrantLockUsage() throws Exception {
// Mock successful lock acquisition on quorum
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false);
-
+
ExecutorService executorService = Executors.newFixedThreadPool(4);
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
-
+
try {
- AsyncRedlockImpl asyncLock = new AsyncRedlockImpl("async-demo-key", drivers, testConfig,
- executorService, scheduledExecutorService);
-
+ AsyncRedlockImpl asyncLock = new AsyncRedlockImpl("async-demo-key", drivers, testConfig, executorService,
+ scheduledExecutorService);
+
System.out.println("\n=== Async Reentrant Lock Demonstration ===");
-
+
// First acquisition
System.out.println("🔒 First async lock acquisition");
assertTrue(asyncLock.tryLockAsync().toCompletableFuture().get());
System.out.println(" Hold count: " + asyncLock.getHoldCount());
-
+
// Second acquisition (reentrant)
System.out.println("🔒 Second async lock acquisition (reentrant)");
assertTrue(asyncLock.tryLockAsync().toCompletableFuture().get());
System.out.println(" Hold count: " + asyncLock.getHoldCount());
-
+
// Third acquisition (reentrant)
System.out.println("🔒 Third async lock acquisition (reentrant)");
assertTrue(asyncLock.tryLockAsync().toCompletableFuture().get());
System.out.println(" Hold count: " + asyncLock.getHoldCount());
-
+
// Release locks
System.out.println("🔓 First async unlock");
asyncLock.unlockAsync().toCompletableFuture().get();
System.out.println(" Hold count: " + asyncLock.getHoldCount());
-
+
System.out.println("🔓 Second async unlock");
asyncLock.unlockAsync().toCompletableFuture().get();
System.out.println(" Hold count: " + asyncLock.getHoldCount());
-
+
System.out.println("🔓 Third async unlock");
asyncLock.unlockAsync().toCompletableFuture().get();
System.out.println(" Hold count: " + asyncLock.getHoldCount());
-
+
assertFalse(asyncLock.isHeldByCurrentThread());
assertEquals(0, asyncLock.getHoldCount());
-
+
System.out.println("✅ Async demonstration completed successfully!");
-
+
} finally {
executorService.shutdown();
scheduledExecutorService.shutdown();
}
}
-
+
@Test
public void demonstrateRxReentrantLockUsage() throws Exception {
// Mock successful lock acquisition on quorum
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false);
-
+
ExecutorService executorService = Executors.newFixedThreadPool(4);
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
-
+
try {
- AsyncRedlockImpl rxLock = new AsyncRedlockImpl("rx-demo-key", drivers, testConfig,
- executorService, scheduledExecutorService);
-
+ AsyncRedlockImpl rxLock = new AsyncRedlockImpl("rx-demo-key", drivers, testConfig, executorService,
+ scheduledExecutorService);
+
System.out.println("\n=== RxJava Reentrant Lock Demonstration ===");
-
+
// First acquisition using RxJava
System.out.println("🔒 First RxJava lock acquisition");
assertTrue(rxLock.tryLockRx().blockingGet());
System.out.println(" Hold count: " + rxLock.getHoldCount());
-
+
// Second acquisition (reentrant) using RxJava
System.out.println("🔒 Second RxJava lock acquisition (reentrant)");
assertTrue(rxLock.tryLockRx().blockingGet());
System.out.println(" Hold count: " + rxLock.getHoldCount());
-
+
// Release locks using RxJava
System.out.println("🔓 First RxJava unlock");
rxLock.unlockRx().blockingAwait();
System.out.println(" Hold count: " + rxLock.getHoldCount());
-
+
System.out.println("🔓 Second RxJava unlock");
rxLock.unlockRx().blockingAwait();
System.out.println(" Hold count: " + rxLock.getHoldCount());
-
+
assertFalse(rxLock.isHeldByCurrentThread());
assertEquals(0, rxLock.getHoldCount());
-
+
System.out.println("✅ RxJava demonstration completed successfully!");
-
+
} finally {
executorService.shutdown();
scheduledExecutorService.shutdown();
diff --git a/src/test/java/org/codarama/redlock4j/ReentrantRedlockTest.java b/src/test/java/org/codarama/redlock4j/ReentrantRedlockTest.java
index 6fc37b7..03539d8 100644
--- a/src/test/java/org/codarama/redlock4j/ReentrantRedlockTest.java
+++ b/src/test/java/org/codarama/redlock4j/ReentrantRedlockTest.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j;
@@ -50,132 +31,127 @@
*/
@ExtendWith(MockitoExtension.class)
public class ReentrantRedlockTest {
-
+
@Mock
private RedisDriver mockDriver1;
-
+
@Mock
private RedisDriver mockDriver2;
-
+
@Mock
private RedisDriver mockDriver3;
-
+
private RedlockConfiguration testConfig;
private List drivers;
-
+
@BeforeEach
void setUp() {
- testConfig = RedlockConfiguration.builder()
- .addRedisNode("localhost", 6379)
- .addRedisNode("localhost", 6380)
- .addRedisNode("localhost", 6381)
- .defaultLockTimeout(Duration.ofSeconds(30))
- .retryDelay(Duration.ofMillis(100))
- .maxRetryAttempts(3)
- .lockAcquisitionTimeout(Duration.ofSeconds(10))
- .build();
-
+ testConfig = RedlockConfiguration.builder().addRedisNode("localhost", 6379).addRedisNode("localhost", 6380)
+ .addRedisNode("localhost", 6381).defaultLockTimeout(Duration.ofSeconds(30))
+ .retryDelay(Duration.ofMillis(100)).maxRetryAttempts(3).lockAcquisitionTimeout(Duration.ofSeconds(10))
+ .build();
+
drivers = Arrays.asList(mockDriver1, mockDriver2, mockDriver3);
-
+
// Setup default mock behavior
lenient().when(mockDriver1.getIdentifier()).thenReturn("redis://localhost:6379");
lenient().when(mockDriver2.getIdentifier()).thenReturn("redis://localhost:6380");
lenient().when(mockDriver3.getIdentifier()).thenReturn("redis://localhost:6381");
}
-
+
@Test
public void testBasicReentrancy() throws RedisDriverException {
// Mock successful lock acquisition on quorum
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false);
-
+
Redlock lock = new Redlock("test-key", drivers, testConfig);
-
+
// First acquisition
assertTrue(lock.tryLock());
assertTrue(lock.isHeldByCurrentThread());
assertEquals(1, lock.getHoldCount());
-
+
// Second acquisition (reentrant)
assertTrue(lock.tryLock());
assertTrue(lock.isHeldByCurrentThread());
assertEquals(2, lock.getHoldCount());
-
+
// Third acquisition (reentrant)
assertTrue(lock.tryLock());
assertTrue(lock.isHeldByCurrentThread());
assertEquals(3, lock.getHoldCount());
-
+
// First unlock
lock.unlock();
assertTrue(lock.isHeldByCurrentThread());
assertEquals(2, lock.getHoldCount());
-
+
// Second unlock
lock.unlock();
assertTrue(lock.isHeldByCurrentThread());
assertEquals(1, lock.getHoldCount());
-
+
// Final unlock
lock.unlock();
assertFalse(lock.isHeldByCurrentThread());
assertEquals(0, lock.getHoldCount());
-
+
// Verify Redis operations were called only once (for initial acquisition)
verify(mockDriver1, times(1)).setIfNotExists(anyString(), anyString(), anyLong());
verify(mockDriver2, times(1)).setIfNotExists(anyString(), anyString(), anyLong());
verify(mockDriver3, times(1)).setIfNotExists(anyString(), anyString(), anyLong());
-
+
// Verify unlock was called only once (for final release)
verify(mockDriver1, times(1)).deleteIfValueMatches(anyString(), anyString());
verify(mockDriver2, times(1)).deleteIfValueMatches(anyString(), anyString());
verify(mockDriver3, times(1)).deleteIfValueMatches(anyString(), anyString());
}
-
+
@Test
public void testReentrantLockWithTimeout() throws RedisDriverException, InterruptedException {
// Mock successful lock acquisition
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false);
-
+
Redlock lock = new Redlock("test-key", drivers, testConfig);
-
+
// First acquisition
assertTrue(lock.tryLock(1, TimeUnit.SECONDS));
assertEquals(1, lock.getHoldCount());
-
+
// Reentrant acquisition with timeout
assertTrue(lock.tryLock(1, TimeUnit.SECONDS));
assertEquals(2, lock.getHoldCount());
-
+
// Unlock both
lock.unlock();
assertEquals(1, lock.getHoldCount());
lock.unlock();
assertEquals(0, lock.getHoldCount());
-
+
// Verify Redis operations were called only once
verify(mockDriver1, times(1)).setIfNotExists(anyString(), anyString(), anyLong());
}
-
+
@Test
public void testReentrantLockAcrossThreads() throws RedisDriverException, InterruptedException {
// Mock successful lock acquisition
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false);
-
+
Redlock lock = new Redlock("test-key", drivers, testConfig);
-
+
// Acquire lock in main thread
assertTrue(lock.tryLock());
assertEquals(1, lock.getHoldCount());
-
+
CountDownLatch latch = new CountDownLatch(1);
AtomicInteger otherThreadHoldCount = new AtomicInteger(-1);
-
+
// Try to access from another thread
Thread otherThread = new Thread(() -> {
// Other thread should not see the lock as held
@@ -184,92 +160,92 @@ public void testReentrantLockAcrossThreads() throws RedisDriverException, Interr
otherThreadHoldCount.set(lock.getHoldCount());
latch.countDown();
});
-
+
otherThread.start();
latch.await(5, TimeUnit.SECONDS);
-
+
// Main thread should still hold the lock
assertTrue(lock.isHeldByCurrentThread());
assertEquals(1, lock.getHoldCount());
assertEquals(0, otherThreadHoldCount.get());
-
+
lock.unlock();
assertFalse(lock.isHeldByCurrentThread());
}
-
+
@Test
public void testUnlockWithoutLockDoesNotThrow() {
Redlock lock = new Redlock("test-key", drivers, testConfig);
-
+
// Should not throw exception when unlocking without holding lock
assertDoesNotThrow(() -> lock.unlock());
assertEquals(0, lock.getHoldCount());
}
-
+
@Test
public void testReentrantLockWithLockMethod() throws RedisDriverException {
// Mock successful lock acquisition
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false);
-
+
Redlock lock = new Redlock("test-key", drivers, testConfig);
-
+
// First acquisition using lock()
assertDoesNotThrow(() -> lock.lock());
assertEquals(1, lock.getHoldCount());
-
+
// Reentrant acquisition using tryLock()
assertTrue(lock.tryLock());
assertEquals(2, lock.getHoldCount());
-
+
// Unlock both
lock.unlock();
assertEquals(1, lock.getHoldCount());
lock.unlock();
assertEquals(0, lock.getHoldCount());
}
-
+
@Test
public void testReentrantLockWithLockInterruptibly() throws RedisDriverException, InterruptedException {
// Mock successful lock acquisition
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false);
-
+
Redlock lock = new Redlock("test-key", drivers, testConfig);
-
+
// First acquisition using lockInterruptibly()
assertDoesNotThrow(() -> lock.lockInterruptibly());
assertEquals(1, lock.getHoldCount());
-
+
// Reentrant acquisition using lockInterruptibly()
assertDoesNotThrow(() -> lock.lockInterruptibly());
assertEquals(2, lock.getHoldCount());
-
+
// Unlock both
lock.unlock();
assertEquals(1, lock.getHoldCount());
lock.unlock();
assertEquals(0, lock.getHoldCount());
}
-
+
@Test
public void testConcurrentReentrantAccess() throws InterruptedException, RedisDriverException {
// Mock successful lock acquisition
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(false);
-
+
Redlock lock = new Redlock("test-key", drivers, testConfig);
ExecutorService executor = Executors.newFixedThreadPool(3);
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch completeLatch = new CountDownLatch(3);
-
+
// Acquire lock in main thread
assertTrue(lock.tryLock());
assertEquals(1, lock.getHoldCount());
-
+
// Submit tasks that try to acquire the same lock from different threads
for (int i = 0; i < 3; i++) {
executor.submit(() -> {
@@ -285,14 +261,14 @@ public void testConcurrentReentrantAccess() throws InterruptedException, RedisDr
}
});
}
-
+
startLatch.countDown();
assertTrue(completeLatch.await(5, TimeUnit.SECONDS));
-
+
// Main thread should still hold the lock
assertTrue(lock.isHeldByCurrentThread());
assertEquals(1, lock.getHoldCount());
-
+
lock.unlock();
executor.shutdown();
}
diff --git a/src/test/java/org/codarama/redlock4j/async/AsyncLockExtensionTest.java b/src/test/java/org/codarama/redlock4j/async/AsyncLockExtensionTest.java
index 3b430a5..23d2e8a 100644
--- a/src/test/java/org/codarama/redlock4j/async/AsyncLockExtensionTest.java
+++ b/src/test/java/org/codarama/redlock4j/async/AsyncLockExtensionTest.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j.async;
@@ -65,15 +46,10 @@ public class AsyncLockExtensionTest {
@BeforeEach
void setUp() {
- testConfig = RedlockConfiguration.builder()
- .addRedisNode("localhost", 6379)
- .addRedisNode("localhost", 6380)
- .addRedisNode("localhost", 6381)
- .defaultLockTimeout(Duration.ofSeconds(30))
- .retryDelay(Duration.ofMillis(100))
- .maxRetryAttempts(3)
- .lockAcquisitionTimeout(Duration.ofSeconds(10))
- .build();
+ testConfig = RedlockConfiguration.builder().addRedisNode("localhost", 6379).addRedisNode("localhost", 6380)
+ .addRedisNode("localhost", 6381).defaultLockTimeout(Duration.ofSeconds(30))
+ .retryDelay(Duration.ofMillis(100)).maxRetryAttempts(3).lockAcquisitionTimeout(Duration.ofSeconds(10))
+ .build();
drivers = Arrays.asList(mockDriver1, mockDriver2, mockDriver3);
executorService = Executors.newFixedThreadPool(2);
@@ -94,58 +70,56 @@ void tearDown() {
scheduledExecutorService.shutdown();
}
}
-
+
@Test
public void testExtendAsyncSuccess() throws Exception {
// Mock successful lock acquisition
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
-
+
// Mock successful extension on all nodes
when(mockDriver1.setIfValueMatches(anyString(), anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfValueMatches(anyString(), anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfValueMatches(anyString(), anyString(), anyString(), anyLong())).thenReturn(true);
-
+
asyncLock = new AsyncRedlockImpl("test-key", drivers, testConfig, executorService, scheduledExecutorService);
// Acquire lock
Boolean acquired = asyncLock.tryLockAsync().toCompletableFuture().get(5, TimeUnit.SECONDS);
assertTrue(acquired);
-
+
long initialValidity = asyncLock.getRemainingValidityTime();
-
+
// Extend lock
- Boolean extended = asyncLock.extendAsync(Duration.ofSeconds(10))
- .toCompletableFuture()
- .get(5, TimeUnit.SECONDS);
-
+ Boolean extended = asyncLock.extendAsync(Duration.ofSeconds(10)).toCompletableFuture().get(5, TimeUnit.SECONDS);
+
assertTrue(extended);
assertTrue(asyncLock.isHeldByCurrentThread());
-
+
// Validity time should be greater after extension
long newValidity = asyncLock.getRemainingValidityTime();
assertTrue(newValidity > initialValidity,
- "New validity (" + newValidity + ") should be greater than initial (" + initialValidity + ")");
-
+ "New validity (" + newValidity + ") should be greater than initial (" + initialValidity + ")");
+
// Verify setIfValueMatches was called on all drivers
verify(mockDriver1).setIfValueMatches(eq("test-key"), anyString(), anyString(), eq(40000L));
verify(mockDriver2).setIfValueMatches(eq("test-key"), anyString(), anyString(), eq(40000L));
verify(mockDriver3).setIfValueMatches(eq("test-key"), anyString(), anyString(), eq(40000L));
}
-
+
@Test
public void testExtendAsyncWithQuorum() throws Exception {
// Mock successful lock acquisition
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
-
+
// Mock extension succeeds on quorum (2 out of 3)
when(mockDriver1.setIfValueMatches(anyString(), anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfValueMatches(anyString(), anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfValueMatches(anyString(), anyString(), anyString(), anyLong())).thenReturn(false);
-
+
asyncLock = new AsyncRedlockImpl("test-key", drivers, testConfig, executorService, scheduledExecutorService);
// Acquire lock
@@ -153,26 +127,24 @@ public void testExtendAsyncWithQuorum() throws Exception {
assertTrue(acquired);
// Extend lock - should succeed with quorum
- Boolean extended = asyncLock.extendAsync(Duration.ofSeconds(10))
- .toCompletableFuture()
- .get(5, TimeUnit.SECONDS);
-
+ Boolean extended = asyncLock.extendAsync(Duration.ofSeconds(10)).toCompletableFuture().get(5, TimeUnit.SECONDS);
+
assertTrue(extended);
assertTrue(asyncLock.isHeldByCurrentThread());
}
-
+
@Test
public void testExtendAsyncFailsWithoutQuorum() throws Exception {
// Mock successful lock acquisition
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
-
+
// Mock extension fails on majority (only 1 out of 3 succeeds)
when(mockDriver1.setIfValueMatches(anyString(), anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfValueMatches(anyString(), anyString(), anyString(), anyLong())).thenReturn(false);
when(mockDriver3.setIfValueMatches(anyString(), anyString(), anyString(), anyLong())).thenReturn(false);
-
+
asyncLock = new AsyncRedlockImpl("test-key", drivers, testConfig, executorService, scheduledExecutorService);
// Acquire lock
@@ -180,35 +152,31 @@ public void testExtendAsyncFailsWithoutQuorum() throws Exception {
assertTrue(acquired);
// Extend lock - should fail without quorum
- Boolean extended = asyncLock.extendAsync(Duration.ofSeconds(10))
- .toCompletableFuture()
- .get(5, TimeUnit.SECONDS);
-
+ Boolean extended = asyncLock.extendAsync(Duration.ofSeconds(10)).toCompletableFuture().get(5, TimeUnit.SECONDS);
+
assertFalse(extended);
// Lock should still be held (original lock not affected)
assertTrue(asyncLock.isHeldByCurrentThread());
}
-
+
@Test
public void testExtendAsyncWithoutHoldingLock() throws Exception {
asyncLock = new AsyncRedlockImpl("test-key", drivers, testConfig, executorService, scheduledExecutorService);
// Try to extend without holding lock
- Boolean extended = asyncLock.extendAsync(Duration.ofSeconds(10))
- .toCompletableFuture()
- .get(5, TimeUnit.SECONDS);
-
+ Boolean extended = asyncLock.extendAsync(Duration.ofSeconds(10)).toCompletableFuture().get(5, TimeUnit.SECONDS);
+
assertFalse(extended);
assertFalse(asyncLock.isHeldByCurrentThread());
}
-
+
@Test
public void testExtendAsyncWithNegativeTime() throws Exception {
// Mock successful lock acquisition
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
-
+
asyncLock = new AsyncRedlockImpl("test-key", drivers, testConfig, executorService, scheduledExecutorService);
// Acquire lock
@@ -217,25 +185,25 @@ public void testExtendAsyncWithNegativeTime() throws Exception {
// Try to extend with negative time
CompletionStage future = asyncLock.extendAsync(Duration.ofSeconds(-1));
-
- ExecutionException exception = assertThrows(ExecutionException.class, () ->
- future.toCompletableFuture().get(5, TimeUnit.SECONDS));
- assertInstanceOf(IllegalArgumentException.class, exception.getCause());
+ ExecutionException exception = assertThrows(ExecutionException.class,
+ () -> future.toCompletableFuture().get(5, TimeUnit.SECONDS));
+
+ assertInstanceOf(IllegalArgumentException.class, exception.getCause());
}
-
+
@Test
public void testExtendRxSuccess() throws Exception {
// Mock successful lock acquisition
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
-
+
// Mock successful extension on all nodes
when(mockDriver1.setIfValueMatches(anyString(), anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfValueMatches(anyString(), anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfValueMatches(anyString(), anyString(), anyString(), anyLong())).thenReturn(true);
-
+
asyncLock = new AsyncRedlockImpl("test-key", drivers, testConfig, executorService, scheduledExecutorService);
// Acquire lock
@@ -248,22 +216,22 @@ public void testExtendRxSuccess() throws Exception {
extendObserver.await(5, TimeUnit.SECONDS);
extendObserver.assertValue(true);
extendObserver.assertComplete();
-
+
assertTrue(asyncLock.isHeldByCurrentThread());
}
-
+
@Test
public void testExtendRxWithQuorum() throws Exception {
// Mock successful lock acquisition
when(mockDriver1.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfNotExists(anyString(), anyString(), anyLong())).thenReturn(true);
-
+
// Mock extension succeeds on quorum (2 out of 3)
when(mockDriver1.setIfValueMatches(anyString(), anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver2.setIfValueMatches(anyString(), anyString(), anyString(), anyLong())).thenReturn(true);
when(mockDriver3.setIfValueMatches(anyString(), anyString(), anyString(), anyLong())).thenReturn(false);
-
+
asyncLock = new AsyncRedlockImpl("test-key", drivers, testConfig, executorService, scheduledExecutorService);
// Acquire lock
@@ -276,10 +244,10 @@ public void testExtendRxWithQuorum() throws Exception {
extendObserver.await(5, TimeUnit.SECONDS);
extendObserver.assertValue(true);
extendObserver.assertComplete();
-
+
assertTrue(asyncLock.isHeldByCurrentThread());
}
-
+
@Test
public void testExtendRxWithoutHoldingLock() throws Exception {
asyncLock = new AsyncRedlockImpl("test-key", drivers, testConfig, executorService, scheduledExecutorService);
@@ -289,8 +257,7 @@ public void testExtendRxWithoutHoldingLock() throws Exception {
extendObserver.await(5, TimeUnit.SECONDS);
extendObserver.assertValue(false);
extendObserver.assertComplete();
-
+
assertFalse(asyncLock.isHeldByCurrentThread());
}
}
-
diff --git a/src/test/java/org/codarama/redlock4j/async/AsyncRedlockImplTest.java b/src/test/java/org/codarama/redlock4j/async/AsyncRedlockImplTest.java
index 0f3f990..0b3f324 100644
--- a/src/test/java/org/codarama/redlock4j/async/AsyncRedlockImplTest.java
+++ b/src/test/java/org/codarama/redlock4j/async/AsyncRedlockImplTest.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j.async;
@@ -43,109 +24,102 @@
*/
@Testcontainers
public class AsyncRedlockImplTest {
-
+
// Create 3 Redis containers for testing
@Container
static GenericContainer> redis1 = new GenericContainer<>(DockerImageName.parse("redis:7-alpine"))
- .withExposedPorts(6379)
- .withCommand("redis-server", "--appendonly", "yes");
-
+ .withExposedPorts(6379).withCommand("redis-server", "--appendonly", "yes");
+
@Container
static GenericContainer> redis2 = new GenericContainer<>(DockerImageName.parse("redis:7-alpine"))
- .withExposedPorts(6379)
- .withCommand("redis-server", "--appendonly", "yes");
-
+ .withExposedPorts(6379).withCommand("redis-server", "--appendonly", "yes");
+
@Container
static GenericContainer> redis3 = new GenericContainer<>(DockerImageName.parse("redis:7-alpine"))
- .withExposedPorts(6379)
- .withCommand("redis-server", "--appendonly", "yes");
-
+ .withExposedPorts(6379).withCommand("redis-server", "--appendonly", "yes");
+
private static RedlockConfiguration testConfiguration;
-
+
@BeforeAll
static void setUp() {
// Create configuration with dynamic ports from containers
- testConfiguration = RedlockConfiguration.builder()
- .addRedisNode("localhost", redis1.getMappedPort(6379))
- .addRedisNode("localhost", redis2.getMappedPort(6379))
- .addRedisNode("localhost", redis3.getMappedPort(6379))
- .defaultLockTimeout(Duration.ofSeconds(10))
- .retryDelay(Duration.ofMillis(100))
- .maxRetryAttempts(3)
- .lockAcquisitionTimeout(Duration.ofSeconds(5))
- .build();
+ testConfiguration = RedlockConfiguration.builder().addRedisNode("localhost", redis1.getMappedPort(6379))
+ .addRedisNode("localhost", redis2.getMappedPort(6379))
+ .addRedisNode("localhost", redis3.getMappedPort(6379)).defaultLockTimeout(Duration.ofSeconds(10))
+ .retryDelay(Duration.ofMillis(100)).maxRetryAttempts(3).lockAcquisitionTimeout(Duration.ofSeconds(5))
+ .build();
}
-
+
@Test
public void testCompletionStageAsyncLock() throws Exception {
try (RedlockManager manager = RedlockManager.withJedis(testConfiguration)) {
AsyncRedlock asyncLock = manager.createAsyncLock("test-completion-stage-lock");
-
+
// Test async tryLock
CompletionStage lockResult = asyncLock.tryLockAsync();
Boolean acquired = lockResult.toCompletableFuture().get(5, TimeUnit.SECONDS);
assertTrue(acquired, "Should acquire lock asynchronously");
-
+
// Verify lock state
assertTrue(asyncLock.isHeldByCurrentThread(), "Lock should be held by current thread");
assertTrue(asyncLock.getRemainingValidityTime() > 0, "Lock should have remaining validity time");
assertEquals("test-completion-stage-lock", asyncLock.getLockKey());
-
+
// Test async unlock
CompletionStage unlockResult = asyncLock.unlockAsync();
unlockResult.toCompletableFuture().get(5, TimeUnit.SECONDS);
-
+
assertFalse(asyncLock.isHeldByCurrentThread(), "Lock should not be held after unlock");
}
}
-
+
@Test
public void testAsyncLockWithTimeout() throws Exception {
try (RedlockManager manager = RedlockManager.withJedis(testConfiguration)) {
AsyncRedlock asyncLock = manager.createAsyncLock("test-async-timeout");
-
+
// Test async tryLock with timeout
CompletionStage lockResult = asyncLock.tryLockAsync(Duration.ofSeconds(2));
Boolean acquired = lockResult.toCompletableFuture().get(5, TimeUnit.SECONDS);
assertTrue(acquired, "Should acquire lock with timeout");
-
+
// Test blocking async lock
AsyncRedlock asyncLock2 = manager.createAsyncLock("test-async-blocking");
CompletionStage blockingResult = asyncLock2.lockAsync();
blockingResult.toCompletableFuture().get(10, TimeUnit.SECONDS);
-
+
assertTrue(asyncLock2.isHeldByCurrentThread(), "Blocking lock should be acquired");
-
+
// Cleanup
asyncLock.unlockAsync().toCompletableFuture().get();
asyncLock2.unlockAsync().toCompletableFuture().get();
}
}
-
+
@Test
public void testRxJavaReactiveLock() throws Exception {
try (RedlockManager manager = RedlockManager.withJedis(testConfiguration)) {
RxRedlock rxLock = manager.createRxLock("test-rxjava-lock");
-
+
// Test RxJava Single tryLock
TestObserver lockObserver = rxLock.tryLockRx().test();
lockObserver.await(5, TimeUnit.SECONDS);
lockObserver.assertComplete();
lockObserver.assertValue(true);
-
+
// Verify lock state
assertTrue(rxLock.isHeldByCurrentThread(), "Lock should be held by current thread");
assertEquals("test-rxjava-lock", rxLock.getLockKey());
-
+
// Test RxJava Completable unlock
TestObserver unlockObserver = rxLock.unlockRx().test();
unlockObserver.await(5, TimeUnit.SECONDS);
unlockObserver.assertComplete();
-
+
assertFalse(rxLock.isHeldByCurrentThread(), "Lock should not be held after unlock");
}
}
-
+
@Test
public void testRxJavaValidityObservable() throws Exception {
try (RedlockManager manager = RedlockManager.withJedis(testConfiguration)) {
@@ -157,9 +131,12 @@ public void testRxJavaValidityObservable() throws Exception {
lockObserver.assertValue(true);
// Test validity observable - start it immediately after lock acquisition
- TestObserver validityObserver = rxLock.validityObservable(Duration.ofMillis(200))
- .take(2) // Take only 2 emissions to be safe
- .test();
+ TestObserver validityObserver = rxLock.validityObservable(Duration.ofMillis(200)).take(2) // Take only
+ // 2
+ // emissions
+ // to be
+ // safe
+ .test();
// Wait a bit for emissions
validityObserver.await(1, TimeUnit.SECONDS);
@@ -168,23 +145,24 @@ public void testRxJavaValidityObservable() throws Exception {
assertFalse(validityObserver.values().isEmpty(), "Should have at least 1 validity emission");
// All validity values should be positive
- validityObserver.values().forEach(validity ->
- assertTrue(validity > 0, "Validity time should be positive: " + validity));
+ validityObserver.values()
+ .forEach(validity -> assertTrue(validity > 0, "Validity time should be positive: " + validity));
// Cleanup
rxLock.unlockRx().test().await();
}
}
-
+
@Test
public void testRxJavaLockStateObservable() throws Exception {
try (RedlockManager manager = RedlockManager.withJedis(testConfiguration)) {
RxRedlock rxLock = manager.createRxLock("test-lock-state-observable");
// Subscribe to lock state changes
- TestObserver stateObserver = rxLock.lockStateObservable()
- .take(3) // RELEASED -> ACQUIRING -> ACQUIRED
- .test();
+ TestObserver stateObserver = rxLock.lockStateObservable().take(3) // RELEASED ->
+ // ACQUIRING ->
+ // ACQUIRED
+ .test();
// Small delay to ensure subscription is active
Thread.sleep(100);
@@ -199,84 +177,82 @@ public void testRxJavaLockStateObservable() throws Exception {
assertTrue(stateObserver.values().size() >= 2, "Should have at least 2 state changes");
// First state should be RELEASED (initial state)
- assertEquals(
- RxRedlock.LockState.RELEASED, stateObserver.values().get(0));
+ assertEquals(RxRedlock.LockState.RELEASED, stateObserver.values().get(0));
// Last state should be ACQUIRED (if lock was successful)
RxRedlock.LockState lastState = stateObserver.values().get(stateObserver.values().size() - 1);
assertTrue(lastState == RxRedlock.LockState.ACQUIRED || lastState == RxRedlock.LockState.ACQUIRING,
- "Last state should be ACQUIRED or ACQUIRING, but was: " + lastState);
+ "Last state should be ACQUIRED or ACQUIRING, but was: " + lastState);
// Cleanup
rxLock.unlockRx().test().await();
}
}
-
+
@Test
public void testRxJavaRetryLogic() throws Exception {
try (RedlockManager manager = RedlockManager.withJedis(testConfiguration)) {
RxRedlock rxLock = manager.createRxLock("test-retry-logic");
-
+
// Test retry with immediate success (should succeed on first try)
TestObserver retryObserver = rxLock.tryLockWithRetryRx(3, Duration.ofMillis(100)).test();
retryObserver.await(5, TimeUnit.SECONDS);
retryObserver.assertComplete();
retryObserver.assertValue(true);
-
+
assertTrue(rxLock.isHeldByCurrentThread(), "Lock should be held after retry success");
-
+
// Cleanup
rxLock.unlockRx().test().await();
}
}
-
+
@Test
public void testCombinedAsyncRxLock() throws Exception {
try (RedlockManager manager = RedlockManager.withJedis(testConfiguration)) {
AsyncRedlockImpl combinedLock = manager.createAsyncRxLock("test-combined-lock");
-
+
// Test CompletionStage interface
Boolean asyncResult = combinedLock.tryLockAsync().toCompletableFuture().get(5, TimeUnit.SECONDS);
assertTrue(asyncResult, "Should acquire lock via CompletionStage interface");
-
+
// Test RxJava interface on same lock
assertTrue(combinedLock.isHeldByCurrentThread(), "Lock should be held");
-
+
// Test validity observable
- TestObserver validityObserver = combinedLock.validityObservable(Duration.ofMillis(200))
- .take(2)
- .test();
-
+ TestObserver validityObserver = combinedLock.validityObservable(Duration.ofMillis(200)).take(2)
+ .test();
+
validityObserver.await(1, TimeUnit.SECONDS);
validityObserver.assertValueCount(2);
-
+
// Test async unlock
combinedLock.unlockAsync().toCompletableFuture().get(5, TimeUnit.SECONDS);
assertFalse(combinedLock.isHeldByCurrentThread(), "Lock should not be held after unlock");
}
}
-
+
@Test
public void testConcurrentAsyncOperations() throws Exception {
try (RedlockManager manager = RedlockManager.withJedis(testConfiguration)) {
AsyncRedlock asyncLock1 = manager.createAsyncLock("concurrent-async-test");
AsyncRedlock asyncLock2 = manager.createAsyncLock("concurrent-async-test"); // Same key
-
+
// First lock should succeed
Boolean acquired1 = asyncLock1.tryLockAsync().toCompletableFuture().get(5, TimeUnit.SECONDS);
assertTrue(acquired1, "First async lock should succeed");
-
+
// Second lock should fail (same resource)
Boolean acquired2 = asyncLock2.tryLockAsync().toCompletableFuture().get(5, TimeUnit.SECONDS);
assertFalse(acquired2, "Second async lock should fail for same resource");
-
+
// Release first lock
asyncLock1.unlockAsync().toCompletableFuture().get();
-
+
// Now second lock should succeed
Boolean acquired3 = asyncLock2.tryLockAsync().toCompletableFuture().get(5, TimeUnit.SECONDS);
assertTrue(acquired3, "Second lock should succeed after first is released");
-
+
// Cleanup
asyncLock2.unlockAsync().toCompletableFuture().get();
}
diff --git a/src/test/java/org/codarama/redlock4j/configuration/RedlockConfigurationTest.java b/src/test/java/org/codarama/redlock4j/configuration/RedlockConfigurationTest.java
index 568742f..3d01212 100644
--- a/src/test/java/org/codarama/redlock4j/configuration/RedlockConfigurationTest.java
+++ b/src/test/java/org/codarama/redlock4j/configuration/RedlockConfigurationTest.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j.configuration;
@@ -31,15 +12,12 @@
* Unit tests for RedlockConfiguration.
*/
public class RedlockConfigurationTest {
-
+
@Test
public void testBasicConfiguration() {
- RedlockConfiguration config = RedlockConfiguration.builder()
- .addRedisNode("localhost", 6379)
- .addRedisNode("localhost", 6380)
- .addRedisNode("localhost", 6381)
- .build();
-
+ RedlockConfiguration config = RedlockConfiguration.builder().addRedisNode("localhost", 6379)
+ .addRedisNode("localhost", 6380).addRedisNode("localhost", 6381).build();
+
assertEquals(3, config.getRedisNodes().size());
assertEquals(2, config.getQuorum()); // (3/2) + 1 = 2
assertEquals(Duration.ofSeconds(30).toMillis(), config.getDefaultLockTimeoutMs());
@@ -47,19 +25,13 @@ public void testBasicConfiguration() {
assertEquals(3, config.getMaxRetryAttempts());
assertEquals(0.01, config.getClockDriftFactor(), 0.001);
}
-
+
@Test
public void testCustomConfiguration() {
- RedlockConfiguration config = RedlockConfiguration.builder()
- .addRedisNode("redis1", 6379, "password")
- .addRedisNode("redis2", 6379, "password")
- .addRedisNode("redis3", 6379, "password")
- .defaultLockTimeout(Duration.ofSeconds(60))
- .retryDelay(Duration.ofMillis(500))
- .maxRetryAttempts(5)
- .clockDriftFactor(0.02)
- .lockAcquisitionTimeout(Duration.ofSeconds(20))
- .build();
+ RedlockConfiguration config = RedlockConfiguration.builder().addRedisNode("redis1", 6379, "password")
+ .addRedisNode("redis2", 6379, "password").addRedisNode("redis3", 6379, "password")
+ .defaultLockTimeout(Duration.ofSeconds(60)).retryDelay(Duration.ofMillis(500)).maxRetryAttempts(5)
+ .clockDriftFactor(0.02).lockAcquisitionTimeout(Duration.ofSeconds(20)).build();
assertEquals(3, config.getRedisNodes().size());
assertEquals(Duration.ofSeconds(60).toMillis(), config.getDefaultLockTimeoutMs());
@@ -68,18 +40,12 @@ public void testCustomConfiguration() {
assertEquals(0.02, config.getClockDriftFactor(), 0.001);
assertEquals(Duration.ofSeconds(20).toMillis(), config.getLockAcquisitionTimeoutMs());
}
-
+
@Test
public void testNodeConfiguration() {
- RedisNodeConfiguration nodeConfig = RedisNodeConfiguration.builder()
- .host("redis.example.com")
- .port(6380)
- .password("secret")
- .database(1)
- .connectionTimeoutMs(3000)
- .socketTimeoutMs(3000)
- .build();
-
+ RedisNodeConfiguration nodeConfig = RedisNodeConfiguration.builder().host("redis.example.com").port(6380)
+ .password("secret").database(1).connectionTimeoutMs(3000).socketTimeoutMs(3000).build();
+
assertEquals("redis.example.com", nodeConfig.getHost());
assertEquals(6380, nodeConfig.getPort());
assertEquals("secret", nodeConfig.getPassword());
@@ -87,79 +53,60 @@ public void testNodeConfiguration() {
assertEquals(3000, nodeConfig.getConnectionTimeoutMs());
assertEquals(3000, nodeConfig.getSocketTimeoutMs());
}
-
+
@Test
public void testValidationErrors() {
// Test insufficient nodes
assertThrows(IllegalArgumentException.class, () -> {
- RedlockConfiguration.builder()
- .addRedisNode("localhost", 6379)
- .addRedisNode("localhost", 6380)
- .build(); // Only 2 nodes, need at least 3
+ RedlockConfiguration.builder().addRedisNode("localhost", 6379).addRedisNode("localhost", 6380).build(); // Only
+ // 2
+ // nodes,
+ // need
+ // at
+ // least
+ // 3
});
-
+
// Test no nodes
assertThrows(IllegalArgumentException.class, () -> RedlockConfiguration.builder().build());
// Test invalid timeout
- assertThrows(IllegalArgumentException.class, () ->
- RedlockConfiguration.builder()
- .addRedisNode("localhost", 6379)
- .addRedisNode("localhost", 6380)
- .addRedisNode("localhost", 6381)
- .defaultLockTimeout(Duration.ofSeconds(-1))
- .build());
-
+ assertThrows(IllegalArgumentException.class,
+ () -> RedlockConfiguration.builder().addRedisNode("localhost", 6379).addRedisNode("localhost", 6380)
+ .addRedisNode("localhost", 6381).defaultLockTimeout(Duration.ofSeconds(-1)).build());
+
// Test invalid clock drift factor
- assertThrows(IllegalArgumentException.class, () ->
- RedlockConfiguration.builder()
- .addRedisNode("localhost", 6379)
- .addRedisNode("localhost", 6380)
- .addRedisNode("localhost", 6381)
- .clockDriftFactor(1.5) // > 1.0
- .build());
+ assertThrows(IllegalArgumentException.class,
+ () -> RedlockConfiguration.builder().addRedisNode("localhost", 6379).addRedisNode("localhost", 6380)
+ .addRedisNode("localhost", 6381).clockDriftFactor(1.5) // > 1.0
+ .build());
}
-
+
@Test
public void testNodeConfigurationValidation() {
// Test invalid host
- assertThrows(IllegalArgumentException.class, () ->
- RedisNodeConfiguration.builder()
- .host("")
- .build());
-
+ assertThrows(IllegalArgumentException.class, () -> RedisNodeConfiguration.builder().host("").build());
+
// Test invalid port
- assertThrows(IllegalArgumentException.class, () ->
- RedisNodeConfiguration.builder()
- .host("localhost")
- .port(0)
- .build());
-
- assertThrows(IllegalArgumentException.class, () ->
- RedisNodeConfiguration.builder()
- .host("localhost")
- .port(70000) // > 65535
- .build());
+ assertThrows(IllegalArgumentException.class,
+ () -> RedisNodeConfiguration.builder().host("localhost").port(0).build());
+
+ assertThrows(IllegalArgumentException.class,
+ () -> RedisNodeConfiguration.builder().host("localhost").port(70000) // > 65535
+ .build());
}
-
+
@Test
public void testQuorumCalculation() {
// Test with 3 nodes
- RedlockConfiguration config3 = RedlockConfiguration.builder()
- .addRedisNode("localhost", 6379)
- .addRedisNode("localhost", 6380)
- .addRedisNode("localhost", 6381)
- .build();
+ RedlockConfiguration config3 = RedlockConfiguration.builder().addRedisNode("localhost", 6379)
+ .addRedisNode("localhost", 6380).addRedisNode("localhost", 6381).build();
assertEquals(2, config3.getQuorum());
-
+
// Test with 5 nodes
- RedlockConfiguration config5 = RedlockConfiguration.builder()
- .addRedisNode("localhost", 6379)
- .addRedisNode("localhost", 6380)
- .addRedisNode("localhost", 6381)
- .addRedisNode("localhost", 6382)
- .addRedisNode("localhost", 6383)
- .build();
+ RedlockConfiguration config5 = RedlockConfiguration.builder().addRedisNode("localhost", 6379)
+ .addRedisNode("localhost", 6380).addRedisNode("localhost", 6381).addRedisNode("localhost", 6382)
+ .addRedisNode("localhost", 6383).build();
assertEquals(3, config5.getQuorum());
}
}
diff --git a/src/test/java/org/codarama/redlock4j/driver/JedisRedisDriverTest.java b/src/test/java/org/codarama/redlock4j/driver/JedisRedisDriverTest.java
index d317e61..b13764f 100644
--- a/src/test/java/org/codarama/redlock4j/driver/JedisRedisDriverTest.java
+++ b/src/test/java/org/codarama/redlock4j/driver/JedisRedisDriverTest.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j.driver;
@@ -30,64 +11,49 @@
import static org.junit.jupiter.api.Assertions.*;
/**
- * Unit tests for JedisRedisDriver.
- * These tests focus on the driver's public interface and configuration handling.
+ * Unit tests for JedisRedisDriver. These tests focus on the driver's public interface and configuration handling.
*/
public class JedisRedisDriverTest {
-
+
private RedisNodeConfiguration testConfig;
private JedisRedisDriver driver;
-
+
@BeforeEach
void setUp() {
- testConfig = RedisNodeConfiguration.builder()
- .host("localhost")
- .port(6379)
- .connectionTimeoutMs(5000)
- .socketTimeoutMs(5000)
- .build();
+ testConfig = RedisNodeConfiguration.builder().host("localhost").port(6379).connectionTimeoutMs(5000)
+ .socketTimeoutMs(5000).build();
}
-
+
@Test
public void testDriverCreationWithBasicConfig() {
driver = new JedisRedisDriver(testConfig);
-
+
assertNotNull(driver);
assertEquals("redis://localhost:6379", driver.getIdentifier());
}
-
+
@Test
public void testDriverCreationWithPassword() {
- RedisNodeConfiguration configWithPassword = RedisNodeConfiguration.builder()
- .host("localhost")
- .port(6379)
- .password("testpass")
- .connectionTimeoutMs(5000)
- .socketTimeoutMs(5000)
- .build();
-
+ RedisNodeConfiguration configWithPassword = RedisNodeConfiguration.builder().host("localhost").port(6379)
+ .password("testpass").connectionTimeoutMs(5000).socketTimeoutMs(5000).build();
+
driver = new JedisRedisDriver(configWithPassword);
-
+
assertNotNull(driver);
assertEquals("redis://localhost:6379", driver.getIdentifier());
}
-
+
@Test
public void testDriverCreationWithDatabase() {
- RedisNodeConfiguration configWithDb = RedisNodeConfiguration.builder()
- .host("localhost")
- .port(6379)
- .database(2)
- .connectionTimeoutMs(5000)
- .socketTimeoutMs(5000)
- .build();
-
+ RedisNodeConfiguration configWithDb = RedisNodeConfiguration.builder().host("localhost").port(6379).database(2)
+ .connectionTimeoutMs(5000).socketTimeoutMs(5000).build();
+
driver = new JedisRedisDriver(configWithDb);
-
+
assertNotNull(driver);
assertEquals("redis://localhost:6379", driver.getIdentifier());
}
-
+
@Test
public void testGetIdentifierFormat() {
// Test identifier format without creating multiple drivers
@@ -95,26 +61,26 @@ public void testGetIdentifierFormat() {
assertEquals("redis://localhost:6379", driver.getIdentifier());
driver.close();
}
-
+
@Test
public void testDriverCreationWithNullConfig() {
assertThrows(NullPointerException.class, () -> {
new JedisRedisDriver(null);
});
}
-
+
@Test
public void testCloseDoesNotThrowException() {
driver = new JedisRedisDriver(testConfig);
-
+
// Should not throw exception even if connection fails
assertDoesNotThrow(() -> driver.close());
}
-
+
@Test
public void testMultipleCloseCallsAreIdempotent() {
driver = new JedisRedisDriver(testConfig);
-
+
// Multiple close calls should not throw exceptions
assertDoesNotThrow(() -> {
driver.close();
diff --git a/src/test/java/org/codarama/redlock4j/driver/LettuceRedisDriverTest.java b/src/test/java/org/codarama/redlock4j/driver/LettuceRedisDriverTest.java
index 53713af..bd86b9b 100644
--- a/src/test/java/org/codarama/redlock4j/driver/LettuceRedisDriverTest.java
+++ b/src/test/java/org/codarama/redlock4j/driver/LettuceRedisDriverTest.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j.driver;
@@ -39,8 +20,7 @@
import static org.mockito.Mockito.*;
/**
- * Unit tests for LettuceRedisDriver using Mockito mocks.
- * These tests do not require a working Redis server.
+ * Unit tests for LettuceRedisDriver using Mockito mocks. These tests do not require a working Redis server.
*/
@ExtendWith(MockitoExtension.class)
public class LettuceRedisDriverTest {
@@ -56,15 +36,11 @@ public class LettuceRedisDriverTest {
private RedisNodeConfiguration testConfig;
private LettuceRedisDriver driver;
-
+
@BeforeEach
void setUp() {
- testConfig = RedisNodeConfiguration.builder()
- .host("localhost")
- .port(6379)
- .connectionTimeoutMs(5000)
- .socketTimeoutMs(5000)
- .build();
+ testConfig = RedisNodeConfiguration.builder().host("localhost").port(6379).connectionTimeoutMs(5000)
+ .socketTimeoutMs(5000).build();
}
@Test
@@ -77,36 +53,26 @@ public void testDriverCreationWithBasicConfig() {
@Test
public void testDriverCreationWithPassword() {
- RedisNodeConfiguration configWithPassword = RedisNodeConfiguration.builder()
- .host("localhost")
- .port(6379)
- .password("testpass")
- .connectionTimeoutMs(5000)
- .socketTimeoutMs(5000)
- .build();
+ RedisNodeConfiguration configWithPassword = RedisNodeConfiguration.builder().host("localhost").port(6379)
+ .password("testpass").connectionTimeoutMs(5000).socketTimeoutMs(5000).build();
driver = new LettuceRedisDriver(configWithPassword, mockRedisClient, mockConnection, mockCommands);
assertNotNull(driver);
assertEquals("redis://localhost:6379", driver.getIdentifier());
}
-
+
@Test
public void testDriverCreationWithDatabase() {
- RedisNodeConfiguration configWithDb = RedisNodeConfiguration.builder()
- .host("localhost")
- .port(6379)
- .database(2)
- .connectionTimeoutMs(5000)
- .socketTimeoutMs(5000)
- .build();
-
+ RedisNodeConfiguration configWithDb = RedisNodeConfiguration.builder().host("localhost").port(6379).database(2)
+ .connectionTimeoutMs(5000).socketTimeoutMs(5000).build();
+
driver = new LettuceRedisDriver(configWithDb, mockRedisClient, mockConnection, mockCommands);
-
+
assertNotNull(driver);
assertEquals("redis://localhost:6379", driver.getIdentifier());
}
-
+
@Test
public void testGetIdentifierFormat() {
// Test identifier format without creating multiple drivers
@@ -114,13 +80,13 @@ public void testGetIdentifierFormat() {
assertEquals("redis://localhost:6379", driver.getIdentifier());
driver.close();
}
-
+
@Test
public void testDriverCreationWithNullConfig() {
- assertThrows(NullPointerException.class, () ->
- new LettuceRedisDriver(null, mockRedisClient, mockConnection, mockCommands));
+ assertThrows(NullPointerException.class,
+ () -> new LettuceRedisDriver(null, mockRedisClient, mockConnection, mockCommands));
}
-
+
@Test
public void testCloseDoesNotThrowException() {
driver = new LettuceRedisDriver(testConfig, mockRedisClient, mockConnection, mockCommands);
@@ -132,7 +98,7 @@ public void testCloseDoesNotThrowException() {
verify(mockConnection).close();
verify(mockRedisClient).shutdown();
}
-
+
@Test
public void testMultipleCloseCallsAreIdempotent() {
driver = new LettuceRedisDriver(testConfig, mockRedisClient, mockConnection, mockCommands);
@@ -178,9 +144,10 @@ public void testSetIfNotExistsException() {
driver = new LettuceRedisDriver(testConfig, mockRedisClient, mockConnection, mockCommands);
when(mockCommands.set(eq("test-key"), eq("test-value"), any(SetArgs.class)))
- .thenThrow(new RuntimeException("Connection failed"));
+ .thenThrow(new RuntimeException("Connection failed"));
- RedisDriverException exception = assertThrows(RedisDriverException.class, () -> driver.setIfNotExists("test-key", "test-value", 10000));
+ RedisDriverException exception = assertThrows(RedisDriverException.class,
+ () -> driver.setIfNotExists("test-key", "test-value", 10000));
assertTrue(exception.getMessage().contains("Failed to execute SET NX PX command"));
assertTrue(exception.getMessage().contains("redis://localhost:6379"));
@@ -218,13 +185,13 @@ public void testDeleteIfValueMatchesFailure() throws RedisDriverException {
public void testDeleteIfValueMatchesException() {
// Mock dispatch() for CAD detection (returns success, indicating native support)
// Then throw exception for the actual delete
- when(mockCommands.dispatch(any(), any(), any()))
- .thenReturn(1L)
- .thenThrow(new RuntimeException("DELEX execution failed"));
+ when(mockCommands.dispatch(any(), any(), any())).thenReturn(1L)
+ .thenThrow(new RuntimeException("DELEX execution failed"));
driver = new LettuceRedisDriver(testConfig, mockRedisClient, mockConnection, mockCommands);
- RedisDriverException exception = assertThrows(RedisDriverException.class, () -> driver.deleteIfValueMatches("test-key", "test-value"));
+ RedisDriverException exception = assertThrows(RedisDriverException.class,
+ () -> driver.deleteIfValueMatches("test-key", "test-value"));
assertTrue(exception.getMessage().contains("Failed to execute DELEX command"));
assertTrue(exception.getMessage().contains("redis://localhost:6379"));
@@ -256,15 +223,9 @@ public void testIsConnectedFailure() {
@Test
public void testGetIdentifierWithDifferentConfigurations() {
- RedisNodeConfiguration config1 = RedisNodeConfiguration.builder()
- .host("redis1.example.com")
- .port(6379)
- .build();
-
- RedisNodeConfiguration config2 = RedisNodeConfiguration.builder()
- .host("redis2.example.com")
- .port(6380)
- .build();
+ RedisNodeConfiguration config1 = RedisNodeConfiguration.builder().host("redis1.example.com").port(6379).build();
+
+ RedisNodeConfiguration config2 = RedisNodeConfiguration.builder().host("redis2.example.com").port(6380).build();
LettuceRedisDriver driver1 = new LettuceRedisDriver(config1, mockRedisClient, mockConnection, mockCommands);
LettuceRedisDriver driver2 = new LettuceRedisDriver(config2, mockRedisClient, mockConnection, mockCommands);
diff --git a/src/test/java/org/codarama/redlock4j/examples/AdvancedLockingExample.java b/src/test/java/org/codarama/redlock4j/examples/AdvancedLockingExample.java
index 4081055..4c77caa 100644
--- a/src/test/java/org/codarama/redlock4j/examples/AdvancedLockingExample.java
+++ b/src/test/java/org/codarama/redlock4j/examples/AdvancedLockingExample.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j.examples;
@@ -32,66 +13,58 @@
import java.util.concurrent.locks.Lock;
/**
- * Example demonstrating advanced locking primitives in Redlock4j:
- * - FairLock: FIFO ordering for lock acquisition
- * - MultiLock: Atomic multi-resource locking
- * - ReadWriteLock: Separate read/write locks
- * - Semaphore: Distributed semaphore with permits
- * - CountDownLatch: Distributed countdown latch
+ * Example demonstrating advanced locking primitives in Redlock4j: - FairLock: FIFO ordering for lock acquisition -
+ * MultiLock: Atomic multi-resource locking - ReadWriteLock: Separate read/write locks - Semaphore: Distributed
+ * semaphore with permits - CountDownLatch: Distributed countdown latch
*/
public class AdvancedLockingExample {
-
+
public static void main(String[] args) throws Exception {
// Configure Redis nodes
- RedlockConfiguration config = RedlockConfiguration.builder()
- .addRedisNode("localhost", 6379)
- .addRedisNode("localhost", 6380)
- .addRedisNode("localhost", 6381)
- .defaultLockTimeout(Duration.ofSeconds(30))
- .retryDelay(Duration.ofMillis(200))
- .maxRetryAttempts(3)
- .lockAcquisitionTimeout(Duration.ofSeconds(10))
- .build();
-
+ RedlockConfiguration config = RedlockConfiguration.builder().addRedisNode("localhost", 6379)
+ .addRedisNode("localhost", 6380).addRedisNode("localhost", 6381)
+ .defaultLockTimeout(Duration.ofSeconds(30)).retryDelay(Duration.ofMillis(200)).maxRetryAttempts(3)
+ .lockAcquisitionTimeout(Duration.ofSeconds(10)).build();
+
try (RedlockManager manager = RedlockManager.withJedis(config)) {
-
+
if (!manager.isHealthy()) {
System.err.println("RedlockManager is not healthy");
return;
}
-
+
System.out.println("=== Advanced Locking Primitives Demo ===\n");
-
+
// 1. Fair Lock Example
demonstrateFairLock(manager);
-
+
// 2. Multi-Lock Example
demonstrateMultiLock(manager);
-
+
// 3. Read-Write Lock Example
demonstrateReadWriteLock(manager);
-
+
// 4. Semaphore Example
demonstrateSemaphore(manager);
-
+
// 5. CountDownLatch Example
demonstrateCountDownLatch(manager);
-
+
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
e.printStackTrace();
}
}
-
+
/**
* Demonstrates Fair Lock with FIFO ordering.
*/
private static void demonstrateFairLock(RedlockManager manager) throws InterruptedException {
System.out.println("1. FAIR LOCK EXAMPLE");
System.out.println(" Fair locks ensure FIFO ordering for lock acquisition\n");
-
+
Lock fairLock = manager.createFairLock("fair-resource");
-
+
// Simulate multiple threads competing for the lock
Thread t1 = new Thread(() -> {
try {
@@ -108,7 +81,7 @@ private static void demonstrateFairLock(RedlockManager manager) throws Interrupt
Thread.currentThread().interrupt();
}
});
-
+
Thread t2 = new Thread(() -> {
try {
Thread.sleep(100); // Start slightly after t1
@@ -125,27 +98,25 @@ private static void demonstrateFairLock(RedlockManager manager) throws Interrupt
Thread.currentThread().interrupt();
}
});
-
+
t1.start();
t2.start();
t1.join();
t2.join();
-
+
System.out.println();
}
-
+
/**
* Demonstrates Multi-Lock for atomic multi-resource locking.
*/
private static void demonstrateMultiLock(RedlockManager manager) throws InterruptedException {
System.out.println("2. MULTI-LOCK EXAMPLE");
System.out.println(" Multi-locks allow atomic acquisition of multiple resources\n");
-
+
// Lock multiple accounts atomically to prevent deadlocks
- Lock multiLock = manager.createMultiLock(
- Arrays.asList("account:1", "account:2", "account:3")
- );
-
+ Lock multiLock = manager.createMultiLock(Arrays.asList("account:1", "account:2", "account:3"));
+
System.out.println(" Attempting to lock 3 accounts atomically...");
if (multiLock.tryLock(5, TimeUnit.SECONDS)) {
try {
@@ -160,19 +131,19 @@ private static void demonstrateMultiLock(RedlockManager manager) throws Interrup
} else {
System.out.println(" ✗ Failed to acquire all locks");
}
-
+
System.out.println();
}
-
+
/**
* Demonstrates Read-Write Lock for concurrent reads.
*/
private static void demonstrateReadWriteLock(RedlockManager manager) throws InterruptedException {
System.out.println("3. READ-WRITE LOCK EXAMPLE");
System.out.println(" Multiple readers can access simultaneously, writers get exclusive access\n");
-
+
RedlockReadWriteLock rwLock = manager.createReadWriteLock("shared-data");
-
+
// Start multiple reader threads
Thread reader1 = new Thread(() -> {
try {
@@ -189,7 +160,7 @@ private static void demonstrateReadWriteLock(RedlockManager manager) throws Inte
Thread.currentThread().interrupt();
}
});
-
+
Thread reader2 = new Thread(() -> {
try {
Thread.sleep(100);
@@ -206,7 +177,7 @@ private static void demonstrateReadWriteLock(RedlockManager manager) throws Inte
Thread.currentThread().interrupt();
}
});
-
+
Thread writer = new Thread(() -> {
try {
Thread.sleep(200);
@@ -223,31 +194,31 @@ private static void demonstrateReadWriteLock(RedlockManager manager) throws Inte
Thread.currentThread().interrupt();
}
});
-
+
reader1.start();
reader2.start();
writer.start();
-
+
reader1.join();
reader2.join();
writer.join();
-
+
System.out.println();
}
-
+
/**
* Demonstrates Semaphore for rate limiting.
*/
private static void demonstrateSemaphore(RedlockManager manager) throws InterruptedException {
System.out.println("4. SEMAPHORE EXAMPLE");
System.out.println(" Semaphores limit concurrent access to a resource\n");
-
+
// Create a semaphore with 2 permits (max 2 concurrent accesses)
RedlockSemaphore semaphore = manager.createSemaphore("api-limiter", 2);
-
+
System.out.println(" Created semaphore with 2 permits");
System.out.println(" Starting 4 threads that need permits...\n");
-
+
for (int i = 1; i <= 4; i++) {
final int threadNum = i;
new Thread(() -> {
@@ -269,26 +240,26 @@ private static void demonstrateSemaphore(RedlockManager manager) throws Interrup
Thread.currentThread().interrupt();
}
}).start();
-
+
Thread.sleep(100); // Stagger thread starts
}
-
+
Thread.sleep(5000); // Wait for all threads to complete
System.out.println();
}
-
+
/**
* Demonstrates CountDownLatch for coordinating startup.
*/
private static void demonstrateCountDownLatch(RedlockManager manager) throws InterruptedException {
System.out.println("5. COUNTDOWN LATCH EXAMPLE");
System.out.println(" Countdown latches coordinate multi-stage processes\n");
-
+
// Create a latch that waits for 3 services to initialize
RedlockCountDownLatch latch = manager.createCountDownLatch("startup-latch", 3);
-
+
System.out.println(" Waiting for 3 services to initialize...\n");
-
+
// Start 3 service initialization threads
for (int i = 1; i <= 3; i++) {
final int serviceNum = i;
@@ -298,25 +269,24 @@ private static void demonstrateCountDownLatch(RedlockManager manager) throws Int
Thread.sleep(1000 * serviceNum); // Simulate varying init times
System.out.println(" Service " + serviceNum + ": ✓ Initialized");
latch.countDown();
- System.out.println(" Service " + serviceNum + ": Counted down (remaining: " +
- latch.getCount() + ")");
+ System.out.println(
+ " Service " + serviceNum + ": Counted down (remaining: " + latch.getCount() + ")");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
-
+
// Main thread waits for all services
System.out.println(" Main: Waiting for all services...");
boolean completed = latch.await(10, TimeUnit.SECONDS);
-
+
if (completed) {
System.out.println(" Main: ✓ All services initialized! Application ready.");
} else {
System.out.println(" Main: ✗ Timeout waiting for services");
}
-
+
System.out.println();
}
}
-
diff --git a/src/test/java/org/codarama/redlock4j/examples/AsyncRxUsageExample.java b/src/test/java/org/codarama/redlock4j/examples/AsyncRxUsageExample.java
index 73c52d8..7ceac03 100644
--- a/src/test/java/org/codarama/redlock4j/examples/AsyncRxUsageExample.java
+++ b/src/test/java/org/codarama/redlock4j/examples/AsyncRxUsageExample.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j.examples;
@@ -39,49 +20,42 @@
import java.util.concurrent.locks.Lock;
/**
- * Comprehensive example demonstrating all available lock APIs:
- * - Standard Java Lock interface
- * - CompletionStage asynchronous API
- * - RxJava reactive API
+ * Comprehensive example demonstrating all available lock APIs: - Standard Java Lock interface - CompletionStage
+ * asynchronous API - RxJava reactive API
*/
public class AsyncRxUsageExample {
-
+
public static void main(String[] args) {
// Configure Redis nodes
- RedlockConfiguration config = RedlockConfiguration.builder()
- .addRedisNode("localhost", 6379)
- .addRedisNode("localhost", 6380)
- .addRedisNode("localhost", 6381)
- .defaultLockTimeout(Duration.ofSeconds(30))
- .retryDelay(Duration.ofMillis(200))
- .maxRetryAttempts(3)
- .lockAcquisitionTimeout(Duration.ofSeconds(10))
- .build();
-
+ RedlockConfiguration config = RedlockConfiguration.builder().addRedisNode("localhost", 6379)
+ .addRedisNode("localhost", 6380).addRedisNode("localhost", 6381)
+ .defaultLockTimeout(Duration.ofSeconds(30)).retryDelay(Duration.ofMillis(200)).maxRetryAttempts(3)
+ .lockAcquisitionTimeout(Duration.ofSeconds(10)).build();
+
try (RedlockManager redlockManager = RedlockManager.withJedis(config)) {
-
+
// Example 1: Standard Java Lock Interface
demonstrateStandardLock(redlockManager);
-
+
// Example 2: CompletionStage Asynchronous API
demonstrateCompletionStageAsync(redlockManager);
-
+
// Example 3: RxJava Reactive API
demonstrateRxJavaReactive(redlockManager);
-
+
// Example 4: Combined Async/Reactive Lock
demonstrateCombinedLock(redlockManager);
-
+
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
}
}
-
+
private static void demonstrateStandardLock(RedlockManager manager) {
System.out.println("\n=== Standard Java Lock Interface ===");
-
+
Lock lock = manager.createLock("standard-lock-resource");
-
+
// Traditional lock usage
lock.lock();
try {
@@ -95,7 +69,7 @@ private static void demonstrateStandardLock(RedlockManager manager) {
lock.unlock();
System.out.println("✅ Standard lock released");
}
-
+
// Try lock with timeout
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
@@ -110,194 +84,165 @@ private static void demonstrateStandardLock(RedlockManager manager) {
Thread.currentThread().interrupt();
}
}
-
+
private static void demonstrateCompletionStageAsync(RedlockManager manager) {
System.out.println("\n=== CompletionStage Asynchronous API ===");
-
+
AsyncRedlock asyncLock = manager.createAsyncLock("async-resource");
-
+
// Async lock with CompletionStage
CompletionStage lockFuture = asyncLock.tryLockAsync();
-
- lockFuture
- .thenAccept(acquired -> {
- if (acquired) {
- System.out.println("✅ Async lock acquired successfully!");
- System.out.println("Lock key: " + asyncLock.getLockKey());
- System.out.println("Held by current thread: " + asyncLock.isHeldByCurrentThread());
- System.out.println("Remaining validity: " + asyncLock.getRemainingValidityTime() + "ms");
-
- // Simulate async work
- try {
- Thread.sleep(2000);
- System.out.println("Async work completed");
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- } else {
- System.out.println("❌ Failed to acquire async lock");
+
+ lockFuture.thenAccept(acquired -> {
+ if (acquired) {
+ System.out.println("✅ Async lock acquired successfully!");
+ System.out.println("Lock key: " + asyncLock.getLockKey());
+ System.out.println("Held by current thread: " + asyncLock.isHeldByCurrentThread());
+ System.out.println("Remaining validity: " + asyncLock.getRemainingValidityTime() + "ms");
+
+ // Simulate async work
+ try {
+ Thread.sleep(2000);
+ System.out.println("Async work completed");
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
}
- })
- .thenCompose(v -> {
- // Async unlock
- System.out.println("Releasing async lock...");
- return asyncLock.unlockAsync();
- })
- .thenRun(() -> System.out.println("✅ Async lock released successfully!"))
- .exceptionally(throwable -> {
- System.err.println("❌ Async lock error: " + throwable.getMessage());
- return null;
- })
- .toCompletableFuture()
- .join(); // Wait for completion in this example
-
+ } else {
+ System.out.println("❌ Failed to acquire async lock");
+ }
+ }).thenCompose(v -> {
+ // Async unlock
+ System.out.println("Releasing async lock...");
+ return asyncLock.unlockAsync();
+ }).thenRun(() -> System.out.println("✅ Async lock released successfully!")).exceptionally(throwable -> {
+ System.err.println("❌ Async lock error: " + throwable.getMessage());
+ return null;
+ }).toCompletableFuture().join(); // Wait for completion in this example
+
// Async lock with timeout
- asyncLock.tryLockAsync(Duration.ofSeconds(3))
- .thenAccept(acquired -> {
- System.out.println("Async lock with timeout: " + (acquired ? "✅ Success" : "❌ Failed"));
- if (acquired) {
- asyncLock.unlockAsync().toCompletableFuture().join();
- }
- })
- .toCompletableFuture()
- .join();
+ asyncLock.tryLockAsync(Duration.ofSeconds(3)).thenAccept(acquired -> {
+ System.out.println("Async lock with timeout: " + (acquired ? "✅ Success" : "❌ Failed"));
+ if (acquired) {
+ asyncLock.unlockAsync().toCompletableFuture().join();
+ }
+ }).toCompletableFuture().join();
}
-
+
private static void demonstrateRxJavaReactive(RedlockManager manager) {
System.out.println("\n=== RxJava Reactive API ===");
-
+
RxRedlock rxLock = manager.createRxLock("rxjava-resource");
-
+
// RxJava Single for lock acquisition
Single lockSingle = rxLock.tryLockRx();
-
- Disposable lockDisposable = lockSingle
- .subscribe(
- acquired -> {
- if (acquired) {
- System.out.println("✅ RxJava lock acquired successfully!");
- System.out.println("Lock key: " + rxLock.getLockKey());
- System.out.println("Held by current thread: " + rxLock.isHeldByCurrentThread());
-
- // Start RxJava validity monitoring
- startRxValidityMonitoring(rxLock);
-
- // Start lock state monitoring
- startRxLockStateMonitoring(rxLock);
-
- // Simulate work
- try {
- Thread.sleep(4000);
- System.out.println("RxJava work completed");
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
-
- // Release lock with RxJava Completable
- Completable unlockCompletable = rxLock.unlockRx();
- Disposable result = unlockCompletable.subscribe(
- () -> System.out.println("✅ RxJava lock released successfully!"),
- throwable -> System.err.println("❌ RxJava unlock error: " + throwable.getMessage())
- );
- } else {
- System.out.println("❌ Failed to acquire RxJava lock");
- }
- },
- throwable -> System.err.println("❌ RxJava lock error: " + throwable.getMessage())
- );
-
+
+ Disposable lockDisposable = lockSingle.subscribe(acquired -> {
+ if (acquired) {
+ System.out.println("✅ RxJava lock acquired successfully!");
+ System.out.println("Lock key: " + rxLock.getLockKey());
+ System.out.println("Held by current thread: " + rxLock.isHeldByCurrentThread());
+
+ // Start RxJava validity monitoring
+ startRxValidityMonitoring(rxLock);
+
+ // Start lock state monitoring
+ startRxLockStateMonitoring(rxLock);
+
+ // Simulate work
+ try {
+ Thread.sleep(4000);
+ System.out.println("RxJava work completed");
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+
+ // Release lock with RxJava Completable
+ Completable unlockCompletable = rxLock.unlockRx();
+ Disposable result = unlockCompletable.subscribe(
+ () -> System.out.println("✅ RxJava lock released successfully!"),
+ throwable -> System.err.println("❌ RxJava unlock error: " + throwable.getMessage()));
+ } else {
+ System.out.println("❌ Failed to acquire RxJava lock");
+ }
+ }, throwable -> System.err.println("❌ RxJava lock error: " + throwable.getMessage()));
+
// Wait for RxJava operations to complete
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
-
+
if (!lockDisposable.isDisposed()) {
lockDisposable.dispose();
}
}
-
+
private static void startRxValidityMonitoring(RxRedlock rxLock) {
System.out.println("📊 Starting RxJava validity monitoring...");
-
+
Observable validityObservable = rxLock.validityObservable(Duration.ofSeconds(1));
-
- Disposable validityDisposable = validityObservable
- .take(3) // Take only 3 emissions
- .subscribe(
- validityTime -> System.out.println("📊 RxJava validity: " + validityTime + "ms remaining"),
- throwable -> System.err.println("❌ RxJava validity error: " + throwable.getMessage()),
- () -> System.out.println("🏁 RxJava validity monitoring completed")
- );
+
+ Disposable validityDisposable = validityObservable.take(3) // Take only 3 emissions
+ .subscribe(validityTime -> System.out.println("📊 RxJava validity: " + validityTime + "ms remaining"),
+ throwable -> System.err.println("❌ RxJava validity error: " + throwable.getMessage()),
+ () -> System.out.println("🏁 RxJava validity monitoring completed"));
}
-
+
private static void startRxLockStateMonitoring(RxRedlock rxLock) {
System.out.println("📊 Starting RxJava lock state monitoring...");
-
+
Observable stateObservable = rxLock.lockStateObservable();
-
- Disposable stateDisposable = stateObservable
- .take(3) // Monitor a few state changes
- .subscribe(
- state -> System.out.println("📊 RxJava lock state: " + state),
- throwable -> System.err.println("❌ RxJava state monitoring error: " + throwable.getMessage()),
- () -> System.out.println("🏁 RxJava state monitoring completed")
- );
+
+ Disposable stateDisposable = stateObservable.take(3) // Monitor a few state changes
+ .subscribe(state -> System.out.println("📊 RxJava lock state: " + state),
+ throwable -> System.err.println("❌ RxJava state monitoring error: " + throwable.getMessage()),
+ () -> System.out.println("🏁 RxJava state monitoring completed"));
}
-
+
private static void demonstrateCombinedLock(RedlockManager manager) {
System.out.println("\n=== Combined Async/Reactive Lock ===");
-
+
AsyncRedlockImpl combinedLock = manager.createAsyncRxLock("combined-resource");
-
+
// Use CompletionStage interface for acquisition
System.out.println("🔄 Acquiring lock via CompletionStage interface...");
- combinedLock.tryLockAsync()
- .thenAccept(acquired -> {
- if (acquired) {
- System.out.println("✅ Lock acquired via CompletionStage interface!");
-
- // Use RxJava interface for monitoring
- System.out.println("📊 Monitoring via RxJava interface...");
- Observable validityObservable = combinedLock.validityObservable(Duration.ofMillis(500));
-
- Disposable monitoringDisposable = validityObservable
- .take(2)
- .subscribe(
- validityTime -> System.out.println("📊 Combined lock validity: " + validityTime + "ms"),
- throwable -> System.err.println("❌ Monitoring error: " + throwable.getMessage()),
- () -> {
- System.out.println("🏁 Combined lock monitoring completed");
-
- // Release via CompletionStage interface
- System.out.println("🔄 Releasing lock via CompletionStage interface...");
- combinedLock.unlockAsync()
- .thenRun(() -> System.out.println("✅ Combined lock released!"))
+ combinedLock.tryLockAsync().thenAccept(acquired -> {
+ if (acquired) {
+ System.out.println("✅ Lock acquired via CompletionStage interface!");
+
+ // Use RxJava interface for monitoring
+ System.out.println("📊 Monitoring via RxJava interface...");
+ Observable validityObservable = combinedLock.validityObservable(Duration.ofMillis(500));
+
+ Disposable monitoringDisposable = validityObservable.take(2).subscribe(
+ validityTime -> System.out.println("📊 Combined lock validity: " + validityTime + "ms"),
+ throwable -> System.err.println("❌ Monitoring error: " + throwable.getMessage()), () -> {
+ System.out.println("🏁 Combined lock monitoring completed");
+
+ // Release via CompletionStage interface
+ System.out.println("🔄 Releasing lock via CompletionStage interface...");
+ combinedLock.unlockAsync().thenRun(() -> System.out.println("✅ Combined lock released!"))
.exceptionally(throwable -> {
System.err.println("❌ Release error: " + throwable.getMessage());
return null;
});
- }
- );
- } else {
- System.out.println("❌ Failed to acquire combined lock");
- }
- })
- .exceptionally(throwable -> {
- System.err.println("❌ Combined lock error: " + throwable.getMessage());
- return null;
- })
- .toCompletableFuture()
- .join();
-
+ });
+ } else {
+ System.out.println("❌ Failed to acquire combined lock");
+ }
+ }).exceptionally(throwable -> {
+ System.err.println("❌ Combined lock error: " + throwable.getMessage());
+ return null;
+ }).toCompletableFuture().join();
+
// Wait for operations to complete
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
-
+
System.out.println("\n🎉 All examples completed!");
}
}
diff --git a/src/test/java/org/codarama/redlock4j/examples/RedlockUsageExample.java b/src/test/java/org/codarama/redlock4j/examples/RedlockUsageExample.java
index 0d88401..3bc49de 100644
--- a/src/test/java/org/codarama/redlock4j/examples/RedlockUsageExample.java
+++ b/src/test/java/org/codarama/redlock4j/examples/RedlockUsageExample.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j.examples;
@@ -35,31 +16,26 @@
* Example demonstrating how to use the Redlock implementation.
*/
public class RedlockUsageExample {
-
+
public static void main(String[] args) {
// Configure Redis nodes (minimum 3 for proper Redlock operation)
- RedlockConfiguration config = RedlockConfiguration.builder()
- .addRedisNode("localhost", 6379)
- .addRedisNode("localhost", 6380)
- .addRedisNode("localhost", 6381)
- .defaultLockTimeout(Duration.ofSeconds(30))
- .retryDelay(Duration.ofMillis(200))
- .maxRetryAttempts(3)
- .lockAcquisitionTimeout(Duration.ofSeconds(10))
- .build();
-
+ RedlockConfiguration config = RedlockConfiguration.builder().addRedisNode("localhost", 6379)
+ .addRedisNode("localhost", 6380).addRedisNode("localhost", 6381)
+ .defaultLockTimeout(Duration.ofSeconds(30)).retryDelay(Duration.ofMillis(200)).maxRetryAttempts(3)
+ .lockAcquisitionTimeout(Duration.ofSeconds(10)).build();
+
// Create RedlockManager with Jedis (or use withLettuce for Lettuce)
try (RedlockManager redlockManager = RedlockManager.withJedis(config)) {
-
+
// Check if the manager is healthy
if (!redlockManager.isHealthy()) {
System.err.println("RedlockManager is not healthy - not enough connected nodes");
return;
}
-
+
// Create a lock for a specific resource
Lock lock = redlockManager.createLock("my-resource-key");
-
+
// Example 1: Simple lock/unlock
System.out.println("Attempting to acquire lock...");
lock.lock();
@@ -75,7 +51,7 @@ public static void main(String[] args) {
lock.unlock();
System.out.println("Lock released.");
}
-
+
// Example 2: Try lock with timeout
System.out.println("\nAttempting to acquire lock with timeout...");
try {
@@ -95,7 +71,7 @@ public static void main(String[] args) {
Thread.currentThread().interrupt();
System.err.println("Lock acquisition interrupted");
}
-
+
// Example 3: Non-blocking try lock
System.out.println("\nAttempting non-blocking lock acquisition...");
if (lock.tryLock()) {
@@ -113,22 +89,22 @@ public static void main(String[] args) {
} else {
System.out.println("Lock is currently held by another process.");
}
-
+
// Example 4: Using Redlock specific methods
if (lock instanceof Redlock) {
Redlock redlock = (Redlock) lock;
-
+
if (redlock.tryLock()) {
try {
System.out.println("\nLock acquired. Checking lock state...");
System.out.println("Is held by current thread: " + redlock.isHeldByCurrentThread());
System.out.println("Remaining validity time: " + redlock.getRemainingValidityTime() + "ms");
-
+
Thread.sleep(1000);
-
+
System.out.println("After 1 second...");
System.out.println("Remaining validity time: " + redlock.getRemainingValidityTime() + "ms");
-
+
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
@@ -137,11 +113,11 @@ public static void main(String[] args) {
}
}
}
-
+
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
}
-
+
System.out.println("\nExample completed.");
}
}
diff --git a/src/test/java/org/codarama/redlock4j/integration/AdvancedLockingIntegrationTest.java b/src/test/java/org/codarama/redlock4j/integration/AdvancedLockingIntegrationTest.java
index 62f6ca3..cb6fdff 100644
--- a/src/test/java/org/codarama/redlock4j/integration/AdvancedLockingIntegrationTest.java
+++ b/src/test/java/org/codarama/redlock4j/integration/AdvancedLockingIntegrationTest.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j.integration;
@@ -49,31 +30,24 @@ public class AdvancedLockingIntegrationTest {
@Container
static GenericContainer> redis1 = new GenericContainer<>(DockerImageName.parse("redis:7-alpine"))
- .withExposedPorts(6379)
- .withCommand("redis-server", "--appendonly", "yes");
+ .withExposedPorts(6379).withCommand("redis-server", "--appendonly", "yes");
@Container
static GenericContainer> redis2 = new GenericContainer<>(DockerImageName.parse("redis:7-alpine"))
- .withExposedPorts(6379)
- .withCommand("redis-server", "--appendonly", "yes");
+ .withExposedPorts(6379).withCommand("redis-server", "--appendonly", "yes");
@Container
static GenericContainer> redis3 = new GenericContainer<>(DockerImageName.parse("redis:7-alpine"))
- .withExposedPorts(6379)
- .withCommand("redis-server", "--appendonly", "yes");
+ .withExposedPorts(6379).withCommand("redis-server", "--appendonly", "yes");
private static RedlockConfiguration testConfiguration;
@BeforeAll
static void setUp() {
- testConfiguration = RedlockConfiguration.builder()
- .addRedisNode("localhost", redis1.getMappedPort(6379))
+ testConfiguration = RedlockConfiguration.builder().addRedisNode("localhost", redis1.getMappedPort(6379))
.addRedisNode("localhost", redis2.getMappedPort(6379))
- .addRedisNode("localhost", redis3.getMappedPort(6379))
- .defaultLockTimeout(Duration.ofSeconds(10))
- .retryDelay(Duration.ofMillis(100))
- .maxRetryAttempts(5)
- .lockAcquisitionTimeout(Duration.ofSeconds(10))
+ .addRedisNode("localhost", redis3.getMappedPort(6379)).defaultLockTimeout(Duration.ofSeconds(10))
+ .retryDelay(Duration.ofMillis(100)).maxRetryAttempts(5).lockAcquisitionTimeout(Duration.ofSeconds(10))
.build();
}
@@ -143,7 +117,7 @@ public void testSemaphoreConcurrentAccess() throws InterruptedException {
int permits = 2;
int threadCount = 5;
RedlockSemaphore semaphore = manager.createSemaphore("test-semaphore-concurrent", permits);
-
+
AtomicInteger successCount = new AtomicInteger(0);
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch endLatch = new CountDownLatch(threadCount);
@@ -267,7 +241,7 @@ public void testCountDownLatchAwait() throws InterruptedException {
public void testMultipleConcurrentReaders() throws InterruptedException {
try (RedlockManager manager = RedlockManager.withJedis(testConfiguration)) {
RedlockReadWriteLock rwLock = manager.createReadWriteLock("test-rwlock-concurrent");
-
+
int readerCount = 3;
AtomicInteger concurrentReaders = new AtomicInteger(0);
AtomicInteger maxConcurrent = new AtomicInteger(0);
@@ -279,15 +253,15 @@ public void testMultipleConcurrentReaders() throws InterruptedException {
try (RedlockManager mgr = RedlockManager.withJedis(testConfiguration)) {
RedlockReadWriteLock lock = mgr.createReadWriteLock("test-rwlock-concurrent");
Lock readLock = lock.readLock();
-
+
startLatch.await();
readLock.lock();
-
+
int current = concurrentReaders.incrementAndGet();
maxConcurrent.updateAndGet(max -> Math.max(max, current));
-
+
Thread.sleep(100);
-
+
concurrentReaders.decrementAndGet();
readLock.unlock();
} catch (InterruptedException e) {
@@ -304,4 +278,3 @@ public void testMultipleConcurrentReaders() throws InterruptedException {
}
}
}
-
diff --git a/src/test/java/org/codarama/redlock4j/integration/AsyncRedlockIntegrationTest.java b/src/test/java/org/codarama/redlock4j/integration/AsyncRedlockIntegrationTest.java
index 4eadd8f..403d488 100644
--- a/src/test/java/org/codarama/redlock4j/integration/AsyncRedlockIntegrationTest.java
+++ b/src/test/java/org/codarama/redlock4j/integration/AsyncRedlockIntegrationTest.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j.integration;
@@ -46,118 +27,111 @@
*/
@Testcontainers
public class AsyncRedlockIntegrationTest {
-
+
@Container
static GenericContainer> redis1 = new GenericContainer<>(DockerImageName.parse("redis:7-alpine"))
- .withExposedPorts(6379)
- .withCommand("redis-server", "--appendonly", "yes");
-
+ .withExposedPorts(6379).withCommand("redis-server", "--appendonly", "yes");
+
@Container
static GenericContainer> redis2 = new GenericContainer<>(DockerImageName.parse("redis:7-alpine"))
- .withExposedPorts(6379)
- .withCommand("redis-server", "--appendonly", "yes");
-
+ .withExposedPorts(6379).withCommand("redis-server", "--appendonly", "yes");
+
@Container
static GenericContainer> redis3 = new GenericContainer<>(DockerImageName.parse("redis:7-alpine"))
- .withExposedPorts(6379)
- .withCommand("redis-server", "--appendonly", "yes");
-
+ .withExposedPorts(6379).withCommand("redis-server", "--appendonly", "yes");
+
private static RedlockConfiguration testConfiguration;
-
+
@BeforeAll
static void setUp() {
- testConfiguration = RedlockConfiguration.builder()
- .addRedisNode("localhost", redis1.getMappedPort(6379))
- .addRedisNode("localhost", redis2.getMappedPort(6379))
- .addRedisNode("localhost", redis3.getMappedPort(6379))
- .defaultLockTimeout(Duration.ofSeconds(10))
- .retryDelay(Duration.ofMillis(100))
- .maxRetryAttempts(3)
- .lockAcquisitionTimeout(Duration.ofSeconds(5))
- .build();
+ testConfiguration = RedlockConfiguration.builder().addRedisNode("localhost", redis1.getMappedPort(6379))
+ .addRedisNode("localhost", redis2.getMappedPort(6379))
+ .addRedisNode("localhost", redis3.getMappedPort(6379)).defaultLockTimeout(Duration.ofSeconds(10))
+ .retryDelay(Duration.ofMillis(100)).maxRetryAttempts(3).lockAcquisitionTimeout(Duration.ofSeconds(5))
+ .build();
}
-
+
@Test
public void testAsyncLockBasicFunctionality() throws Exception {
try (RedlockManager manager = RedlockManager.withJedis(testConfiguration)) {
AsyncRedlock asyncLock = manager.createAsyncLock("test-async-basic");
-
+
// Test async tryLock
CompletionStage lockResult = asyncLock.tryLockAsync();
Boolean acquired = lockResult.toCompletableFuture().get(5, TimeUnit.SECONDS);
assertTrue(acquired, "Should acquire lock asynchronously");
-
+
// Verify lock key
assertEquals("test-async-basic", asyncLock.getLockKey());
-
+
// Test async unlock
CompletionStage unlockResult = asyncLock.unlockAsync();
unlockResult.toCompletableFuture().get(5, TimeUnit.SECONDS);
-
+
// Test should complete without exceptions
System.out.println("✅ Async lock test completed successfully");
}
}
-
+
@Test
public void testRxJavaBasicFunctionality() throws Exception {
try (RedlockManager manager = RedlockManager.withJedis(testConfiguration)) {
RxRedlock rxLock = manager.createRxLock("test-rxjava-basic");
-
+
// Test RxJava Single tryLock
TestObserver lockObserver = rxLock.tryLockRx().test();
lockObserver.await(5, TimeUnit.SECONDS);
lockObserver.assertComplete();
lockObserver.assertValue(true);
-
+
// Verify lock key
assertEquals("test-rxjava-basic", rxLock.getLockKey());
-
+
// Test RxJava Completable unlock
TestObserver unlockObserver = rxLock.unlockRx().test();
unlockObserver.await(5, TimeUnit.SECONDS);
unlockObserver.assertComplete();
-
+
System.out.println("✅ RxJava lock test completed successfully");
}
}
-
+
@Test
public void testCombinedAsyncRxLock() throws Exception {
try (RedlockManager manager = RedlockManager.withJedis(testConfiguration)) {
AsyncRedlockImpl combinedLock = manager.createAsyncRxLock("test-combined-basic");
-
+
// Test CompletionStage interface
Boolean asyncResult = combinedLock.tryLockAsync().toCompletableFuture().get(5, TimeUnit.SECONDS);
assertTrue(asyncResult, "Should acquire lock via CompletionStage interface");
-
+
// Test RxJava interface on same lock instance
assertEquals("test-combined-basic", combinedLock.getLockKey());
-
+
// Test async unlock
combinedLock.unlockAsync().toCompletableFuture().get(5, TimeUnit.SECONDS);
-
+
System.out.println("✅ Combined async/reactive lock test completed successfully");
}
}
-
+
@Test
public void testAsyncLockWithTimeout() throws Exception {
try (RedlockManager manager = RedlockManager.withJedis(testConfiguration)) {
AsyncRedlock asyncLock = manager.createAsyncLock("test-async-timeout");
-
+
// Test async tryLock with timeout
CompletionStage lockResult = asyncLock.tryLockAsync(Duration.ofSeconds(2));
Boolean acquired = lockResult.toCompletableFuture().get(5, TimeUnit.SECONDS);
assertTrue(acquired, "Should acquire lock with timeout");
-
+
// Cleanup
asyncLock.unlockAsync().toCompletableFuture().get();
-
+
System.out.println("✅ Async lock with timeout test completed successfully");
}
}
-
+
@Test
public void testRxJavaValidityObservable() throws Exception {
try (RedlockManager manager = RedlockManager.withJedis(testConfiguration)) {
@@ -169,9 +143,12 @@ public void testRxJavaValidityObservable() throws Exception {
lockObserver.assertValue(true);
// Test validity observable - start immediately and take fewer emissions
- TestObserver validityObserver = rxLock.validityObservable(Duration.ofMillis(300))
- .take(1) // Take only 1 emission to be safe
- .test();
+ TestObserver validityObserver = rxLock.validityObservable(Duration.ofMillis(300)).take(1) // Take only
+ // 1
+ // emission
+ // to be
+ // safe
+ .test();
validityObserver.await(1, TimeUnit.SECONDS);
@@ -179,8 +156,8 @@ public void testRxJavaValidityObservable() throws Exception {
assertFalse(validityObserver.values().isEmpty(), "Should have at least 1 validity emission");
// All validity values should be positive
- validityObserver.values().forEach(validity ->
- assertTrue(validity > 0, "Validity time should be positive: " + validity));
+ validityObserver.values()
+ .forEach(validity -> assertTrue(validity > 0, "Validity time should be positive: " + validity));
// Cleanup
rxLock.unlockRx().test().await();
@@ -188,28 +165,28 @@ public void testRxJavaValidityObservable() throws Exception {
System.out.println("✅ RxJava validity observable test completed successfully");
}
}
-
+
@Test
public void testFactoryMethods() {
try (RedlockManager manager = RedlockManager.withJedis(testConfiguration)) {
-
+
// Test all factory methods work
AsyncRedlock asyncLock = manager.createAsyncLock("factory-async");
assertNotNull(asyncLock);
assertEquals("factory-async", asyncLock.getLockKey());
-
+
RxRedlock rxLock = manager.createRxLock("factory-rx");
assertNotNull(rxLock);
assertEquals("factory-rx", rxLock.getLockKey());
-
+
AsyncRedlockImpl combinedLock = manager.createAsyncRxLock("factory-combined");
assertNotNull(combinedLock);
assertEquals("factory-combined", combinedLock.getLockKey());
-
+
// Verify combined lock implements both interfaces
assertInstanceOf(AsyncRedlock.class, combinedLock);
assertInstanceOf(RxRedlock.class, combinedLock);
-
+
System.out.println("✅ Factory methods test completed successfully");
}
}
diff --git a/src/test/java/org/codarama/redlock4j/integration/RedlockIntegrationTest.java b/src/test/java/org/codarama/redlock4j/integration/RedlockIntegrationTest.java
index cc7b9f9..7215f52 100644
--- a/src/test/java/org/codarama/redlock4j/integration/RedlockIntegrationTest.java
+++ b/src/test/java/org/codarama/redlock4j/integration/RedlockIntegrationTest.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j.integration;
@@ -42,8 +23,8 @@
import java.util.concurrent.locks.Lock;
/**
- * Integration tests for Redlock functionality using Testcontainers.
- * These tests automatically spin up Redis containers for testing.
+ * Integration tests for Redlock functionality using Testcontainers. These tests automatically spin up Redis containers
+ * for testing.
*/
@Testcontainers
public class RedlockIntegrationTest {
@@ -51,18 +32,15 @@ public class RedlockIntegrationTest {
// Create 3 Redis containers for Redlock testing
@Container
static GenericContainer> redis1 = new GenericContainer<>(DockerImageName.parse("redis:7-alpine"))
- .withExposedPorts(6379)
- .withCommand("redis-server", "--appendonly", "yes");
+ .withExposedPorts(6379).withCommand("redis-server", "--appendonly", "yes");
@Container
static GenericContainer> redis2 = new GenericContainer<>(DockerImageName.parse("redis:7-alpine"))
- .withExposedPorts(6379)
- .withCommand("redis-server", "--appendonly", "yes");
+ .withExposedPorts(6379).withCommand("redis-server", "--appendonly", "yes");
@Container
static GenericContainer> redis3 = new GenericContainer<>(DockerImageName.parse("redis:7-alpine"))
- .withExposedPorts(6379)
- .withCommand("redis-server", "--appendonly", "yes");
+ .withExposedPorts(6379).withCommand("redis-server", "--appendonly", "yes");
private static RedlockConfiguration testConfiguration;
@@ -74,22 +52,18 @@ static void setUp() {
redis3.start();
// Create configuration with dynamic ports from containers
- testConfiguration = RedlockConfiguration.builder()
- .addRedisNode("localhost", redis1.getMappedPort(6379))
- .addRedisNode("localhost", redis2.getMappedPort(6379))
- .addRedisNode("localhost", redis3.getMappedPort(6379))
- .defaultLockTimeout(Duration.ofSeconds(10))
- .retryDelay(Duration.ofMillis(100))
- .maxRetryAttempts(3)
- .lockAcquisitionTimeout(Duration.ofSeconds(5))
- .build();
+ testConfiguration = RedlockConfiguration.builder().addRedisNode("localhost", redis1.getMappedPort(6379))
+ .addRedisNode("localhost", redis2.getMappedPort(6379))
+ .addRedisNode("localhost", redis3.getMappedPort(6379)).defaultLockTimeout(Duration.ofSeconds(10))
+ .retryDelay(Duration.ofMillis(100)).maxRetryAttempts(3).lockAcquisitionTimeout(Duration.ofSeconds(5))
+ .build();
}
@AfterAll
static void tearDown() {
// Containers are automatically stopped by Testcontainers
}
-
+
@Test
public void testJedisBasicLockOperations() {
try (RedlockManager manager = RedlockManager.withJedis(testConfiguration)) {
@@ -116,7 +90,7 @@ public void testJedisBasicLockOperations() {
}
}
}
-
+
@Test
public void testLettuceBasicLockOperations() {
try (RedlockManager manager = RedlockManager.withLettuce(testConfiguration)) {
@@ -130,7 +104,7 @@ public void testLettuceBasicLockOperations() {
lock.unlock();
}
}
-
+
@Test
public void testLockTimeout() throws InterruptedException {
try (RedlockManager manager = RedlockManager.withJedis(testConfiguration)) {
@@ -145,7 +119,7 @@ public void testLockTimeout() throws InterruptedException {
lock.unlock();
}
}
-
+
@Test
public void testConcurrentLockAccess() {
try (RedlockManager manager = RedlockManager.withJedis(testConfiguration)) {
@@ -166,33 +140,18 @@ public void testConcurrentLockAccess() {
lock2.unlock();
}
}
-
+
@Test
public void testLockWithCustomConfiguration() {
RedlockConfiguration config = RedlockConfiguration.builder()
- .addRedisNode(RedisNodeConfiguration.builder()
- .host("localhost")
- .port(redis1.getMappedPort(6379))
- .connectionTimeoutMs(1000)
- .socketTimeoutMs(1000)
- .build())
- .addRedisNode(RedisNodeConfiguration.builder()
- .host("localhost")
- .port(redis2.getMappedPort(6379))
- .connectionTimeoutMs(1000)
- .socketTimeoutMs(1000)
- .build())
- .addRedisNode(RedisNodeConfiguration.builder()
- .host("localhost")
- .port(redis3.getMappedPort(6379))
- .connectionTimeoutMs(1000)
- .socketTimeoutMs(1000)
- .build())
- .defaultLockTimeout(Duration.ofSeconds(5))
- .retryDelay(Duration.ofMillis(50))
- .maxRetryAttempts(2)
- .clockDriftFactor(0.02)
- .build();
+ .addRedisNode(RedisNodeConfiguration.builder().host("localhost").port(redis1.getMappedPort(6379))
+ .connectionTimeoutMs(1000).socketTimeoutMs(1000).build())
+ .addRedisNode(RedisNodeConfiguration.builder().host("localhost").port(redis2.getMappedPort(6379))
+ .connectionTimeoutMs(1000).socketTimeoutMs(1000).build())
+ .addRedisNode(RedisNodeConfiguration.builder().host("localhost").port(redis3.getMappedPort(6379))
+ .connectionTimeoutMs(1000).socketTimeoutMs(1000).build())
+ .defaultLockTimeout(Duration.ofSeconds(5)).retryDelay(Duration.ofMillis(50)).maxRetryAttempts(2)
+ .clockDriftFactor(0.02).build();
try (RedlockManager manager = RedlockManager.withJedis(config)) {
Lock lock = manager.createLock("custom-config-lock");
@@ -201,7 +160,7 @@ public void testLockWithCustomConfiguration() {
lock.unlock();
}
}
-
+
@Test
public void testManagerLifecycle() {
RedlockManager manager = RedlockManager.withJedis(testConfiguration);
@@ -218,7 +177,7 @@ public void testManagerLifecycle() {
// Should throw exception when trying to create locks after close
assertThrows(RedlockException.class, () -> manager.createLock("should-fail"));
}
-
+
@Test
public void testInvalidLockKey() {
try (RedlockManager manager = RedlockManager.withJedis(testConfiguration)) {
@@ -226,7 +185,7 @@ public void testInvalidLockKey() {
assertThrows(IllegalArgumentException.class, () -> manager.createLock(null));
// Test empty key
- assertThrows(IllegalArgumentException.class, () ->manager.createLock(""));
+ assertThrows(IllegalArgumentException.class, () -> manager.createLock(""));
// Test whitespace-only key
assertThrows(IllegalArgumentException.class, () -> manager.createLock(" "));
@@ -245,9 +204,7 @@ public void testRedisContainerConnectivity() {
assertTrue(redis2.getMappedPort(6379) > 0, "Redis container 2 should have mapped port");
assertTrue(redis3.getMappedPort(6379) > 0, "Redis container 3 should have mapped port");
- System.out.println("Redis containers running on ports: " +
- redis1.getMappedPort(6379) + ", " +
- redis2.getMappedPort(6379) + ", " +
- redis3.getMappedPort(6379));
+ System.out.println("Redis containers running on ports: " + redis1.getMappedPort(6379) + ", "
+ + redis2.getMappedPort(6379) + ", " + redis3.getMappedPort(6379));
}
}
diff --git a/src/test/java/org/codarama/redlock4j/performance/RedlockPerformanceTest.java b/src/test/java/org/codarama/redlock4j/performance/RedlockPerformanceTest.java
index 9f9a593..ef6aa62 100644
--- a/src/test/java/org/codarama/redlock4j/performance/RedlockPerformanceTest.java
+++ b/src/test/java/org/codarama/redlock4j/performance/RedlockPerformanceTest.java
@@ -1,25 +1,6 @@
/*
- * MIT License
- *
+ * SPDX-License-Identifier: MIT
* Copyright (c) 2025 Codarama
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
*/
package org.codarama.redlock4j.performance;
@@ -43,8 +24,8 @@
import java.util.concurrent.locks.Lock;
/**
- * Performance tests for Redlock functionality using Testcontainers.
- * These tests are disabled by default as they are for performance analysis.
+ * Performance tests for Redlock functionality using Testcontainers. These tests are disabled by default as they are for
+ * performance analysis.
*/
@Disabled("Performance tests - enable manually when needed")
@Testcontainers
@@ -53,41 +34,34 @@ public class RedlockPerformanceTest {
// Create 3 Redis containers for performance testing
@Container
static GenericContainer> redis1 = new GenericContainer<>(DockerImageName.parse("redis:7-alpine"))
- .withExposedPorts(6379)
- .withCommand("redis-server", "--appendonly", "yes");
+ .withExposedPorts(6379).withCommand("redis-server", "--appendonly", "yes");
@Container
static GenericContainer> redis2 = new GenericContainer<>(DockerImageName.parse("redis:7-alpine"))
- .withExposedPorts(6379)
- .withCommand("redis-server", "--appendonly", "yes");
+ .withExposedPorts(6379).withCommand("redis-server", "--appendonly", "yes");
@Container
static GenericContainer> redis3 = new GenericContainer<>(DockerImageName.parse("redis:7-alpine"))
- .withExposedPorts(6379)
- .withCommand("redis-server", "--appendonly", "yes");
+ .withExposedPorts(6379).withCommand("redis-server", "--appendonly", "yes");
private static RedlockConfiguration testConfiguration;
@BeforeAll
static void setUp() {
// Create configuration with dynamic ports from containers
- testConfiguration = RedlockConfiguration.builder()
- .addRedisNode("localhost", redis1.getMappedPort(6379))
- .addRedisNode("localhost", redis2.getMappedPort(6379))
- .addRedisNode("localhost", redis3.getMappedPort(6379))
- .defaultLockTimeout(Duration.ofSeconds(5))
- .retryDelay(Duration.ofMillis(50))
- .maxRetryAttempts(2)
- .lockAcquisitionTimeout(Duration.ofSeconds(2))
- .build();
+ testConfiguration = RedlockConfiguration.builder().addRedisNode("localhost", redis1.getMappedPort(6379))
+ .addRedisNode("localhost", redis2.getMappedPort(6379))
+ .addRedisNode("localhost", redis3.getMappedPort(6379)).defaultLockTimeout(Duration.ofSeconds(5))
+ .retryDelay(Duration.ofMillis(50)).maxRetryAttempts(2).lockAcquisitionTimeout(Duration.ofSeconds(2))
+ .build();
}
-
+
@Test
public void testLockAcquisitionPerformance() throws InterruptedException {
try (RedlockManager manager = RedlockManager.withJedis(testConfiguration)) {
int iterations = 1000;
long startTime = System.currentTimeMillis();
-
+
for (int i = 0; i < iterations; i++) {
Lock lock = manager.createLock("perf-test-" + i);
if (lock.tryLock()) {
@@ -99,11 +73,11 @@ public void testLockAcquisitionPerformance() throws InterruptedException {
}
}
}
-
+
long endTime = System.currentTimeMillis();
long totalTime = endTime - startTime;
double avgTime = (double) totalTime / iterations;
-
+
System.out.println("Lock acquisition performance test:");
System.out.println("Total iterations: " + iterations);
System.out.println("Total time: " + totalTime + "ms");
@@ -111,27 +85,27 @@ public void testLockAcquisitionPerformance() throws InterruptedException {
System.out.println("Locks per second: " + String.format("%.2f", 1000.0 / avgTime));
}
}
-
+
@Test
public void testConcurrentLockContention() throws InterruptedException {
try (RedlockManager manager = RedlockManager.withJedis(testConfiguration)) {
int threadCount = 10;
int iterationsPerThread = 100;
String lockKey = "contention-test-lock";
-
+
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch endLatch = new CountDownLatch(threadCount);
AtomicInteger successfulLocks = new AtomicInteger(0);
AtomicInteger failedLocks = new AtomicInteger(0);
-
+
long startTime = System.currentTimeMillis();
-
+
for (int i = 0; i < threadCount; i++) {
executor.submit(() -> {
try {
startLatch.await(); // Wait for all threads to be ready
-
+
for (int j = 0; j < iterationsPerThread; j++) {
Lock lock = manager.createLock(lockKey);
if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
@@ -153,54 +127,53 @@ public void testConcurrentLockContention() throws InterruptedException {
}
});
}
-
+
startLatch.countDown(); // Start all threads
endLatch.await(); // Wait for all threads to complete
-
+
long endTime = System.currentTimeMillis();
long totalTime = endTime - startTime;
-
+
executor.shutdown();
-
+
System.out.println("\nConcurrent lock contention test:");
System.out.println("Threads: " + threadCount);
System.out.println("Iterations per thread: " + iterationsPerThread);
System.out.println("Total attempts: " + (threadCount * iterationsPerThread));
System.out.println("Successful locks: " + successfulLocks.get());
System.out.println("Failed locks: " + failedLocks.get());
- System.out.println("Success rate: " + String.format("%.2f",
- (double) successfulLocks.get() / (threadCount * iterationsPerThread) * 100) + "%");
+ System.out.println("Success rate: "
+ + String.format("%.2f", (double) successfulLocks.get() / (threadCount * iterationsPerThread) * 100)
+ + "%");
System.out.println("Total time: " + totalTime + "ms");
- System.out.println("Average time per attempt: " +
- String.format("%.2f", (double) totalTime / (threadCount * iterationsPerThread)) + "ms");
+ System.out.println("Average time per attempt: "
+ + String.format("%.2f", (double) totalTime / (threadCount * iterationsPerThread)) + "ms");
}
}
-
+
@Test
public void testJedisVsLettucePerformance() throws InterruptedException {
int iterations = 500;
// Test Jedis performance
- long jedisTime = testDriverPerformance("Jedis",
- RedlockManager.withJedis(testConfiguration), iterations);
+ long jedisTime = testDriverPerformance("Jedis", RedlockManager.withJedis(testConfiguration), iterations);
// Test Lettuce performance
- long lettuceTime = testDriverPerformance("Lettuce",
- RedlockManager.withLettuce(testConfiguration), iterations);
-
+ long lettuceTime = testDriverPerformance("Lettuce", RedlockManager.withLettuce(testConfiguration), iterations);
+
System.out.println("\nDriver Performance Comparison:");
System.out.println("Jedis total time: " + jedisTime + "ms");
System.out.println("Lettuce total time: " + lettuceTime + "ms");
System.out.println("Jedis avg per lock: " + String.format("%.2f", (double) jedisTime / iterations) + "ms");
System.out.println("Lettuce avg per lock: " + String.format("%.2f", (double) lettuceTime / iterations) + "ms");
-
+
if (jedisTime < lettuceTime) {
System.out.println("Jedis is " + String.format("%.2f", (double) lettuceTime / jedisTime) + "x faster");
} else {
System.out.println("Lettuce is " + String.format("%.2f", (double) jedisTime / lettuceTime) + "x faster");
}
}
-
+
private long testDriverPerformance(String driverName, RedlockManager manager, int iterations)
throws InterruptedException {
try {
@@ -224,34 +197,34 @@ private long testDriverPerformance(String driverName, RedlockManager manager, in
manager.close();
}
}
-
+
@Test
public void testLockValidityTimeAccuracy() throws InterruptedException {
RedlockConfiguration config = RedlockConfiguration.builder()
- .addRedisNode("localhost", redis1.getMappedPort(6379))
- .addRedisNode("localhost", redis2.getMappedPort(6379))
- .addRedisNode("localhost", redis3.getMappedPort(6379))
- .defaultLockTimeout(Duration.ofSeconds(2)) // Short timeout for testing
- .retryDelay(Duration.ofMillis(50))
- .maxRetryAttempts(1)
- .build();
+ .addRedisNode("localhost", redis1.getMappedPort(6379))
+ .addRedisNode("localhost", redis2.getMappedPort(6379))
+ .addRedisNode("localhost", redis3.getMappedPort(6379)).defaultLockTimeout(Duration.ofSeconds(2)) // Short
+ // timeout
+ // for
+ // testing
+ .retryDelay(Duration.ofMillis(50)).maxRetryAttempts(1).build();
try (RedlockManager manager = RedlockManager.withJedis(config)) {
Lock lock = manager.createLock("validity-test-lock");
-
+
if (lock.tryLock() && lock instanceof Redlock) {
Redlock redlock = (Redlock) lock;
-
+
long initialValidity = redlock.getRemainingValidityTime();
System.out.println("\nLock validity time test:");
System.out.println("Initial validity time: " + initialValidity + "ms");
-
+
Thread.sleep(500);
-
+
long afterDelay = redlock.getRemainingValidityTime();
System.out.println("Validity after 500ms: " + afterDelay + "ms");
System.out.println("Actual time passed: " + (initialValidity - afterDelay) + "ms");
-
+
redlock.unlock();
}
}
From b5e99c073ddc8727a3d2a69f2c49311709333214 Mon Sep 17 00:00:00 2001
From: Tihomir Mateev
Date: Fri, 27 Feb 2026 16:11:42 +0200
Subject: [PATCH 2/8] Formatter
---
formatting.xml | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 59 insertions(+)
create mode 100644 formatting.xml
diff --git a/formatting.xml b/formatting.xml
new file mode 100644
index 0000000..d09d5e7
--- /dev/null
+++ b/formatting.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
From f716d3d24d7702ff8590a90d1128c2496515c486 Mon Sep 17 00:00:00 2001
From: Tihomir Mateev
Date: Fri, 27 Feb 2026 16:21:03 +0200
Subject: [PATCH 3/8] Ignore anything in the idea folder
---
.gitignore | 1 +
.idea/AugmentWebviewStateStore.xml | 10 ----------
2 files changed, 1 insertion(+), 10 deletions(-)
delete mode 100644 .idea/AugmentWebviewStateStore.xml
diff --git a/.gitignore b/.gitignore
index c63f96b..a5e6632 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,7 @@ target/
.idea/compiler.xml
.idea/libraries/
.idea/AugmentWebviewStateStore.xml
+.idea/*
*.iws
*.iml
diff --git a/.idea/AugmentWebviewStateStore.xml b/.idea/AugmentWebviewStateStore.xml
deleted file mode 100644
index 1f9e321..0000000
--- a/.idea/AugmentWebviewStateStore.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
From c65cc308bd8e4ee9b446aa53b8a4b1c89e8c666b Mon Sep 17 00:00:00 2001
From: Tihomir Mateev
Date: Fri, 27 Feb 2026 16:30:50 +0200
Subject: [PATCH 4/8] Remove emoticons
---
.github/workflows/ci.yml | 4 +-
.github/workflows/nightly.yml | 18 ++---
.github/workflows/pr-validation.yml | 118 ++++++++++++++++++++--------
.github/workflows/release.yml | 10 +--
4 files changed, 103 insertions(+), 47 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index fce8d7c..1669968 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -141,7 +141,7 @@ jobs:
- name: Check for Maven wrapper
run: |
if [ -f "mvnw" ]; then
- echo "✅ Maven wrapper found" >> $GITHUB_STEP_SUMMARY
+ echo "Maven wrapper found" >> $GITHUB_STEP_SUMMARY
else
- echo "ℹ️ Maven wrapper not found (using system Maven)" >> $GITHUB_STEP_SUMMARY
+ 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 bd43935..f0c4658 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -80,8 +80,8 @@ jobs:
}
}
EOF
-
- echo "✅ Stress test configuration validated" >> $GITHUB_STEP_SUMMARY
+
+ echo "Stress test configuration validated" >> $GITHUB_STEP_SUMMARY
- name: Generate comprehensive test report
run: |
@@ -99,11 +99,11 @@ jobs:
echo "- **Total Tests:** ${TOTAL_TESTS:-0}" >> $GITHUB_STEP_SUMMARY
echo "- **Failed Tests:** ${FAILED_TESTS:-0}" >> $GITHUB_STEP_SUMMARY
echo "- **Error Tests:** ${ERROR_TESTS:-0}" >> $GITHUB_STEP_SUMMARY
-
+
if [ "${FAILED_TESTS:-0}" -eq 0 ] && [ "${ERROR_TESTS:-0}" -eq 0 ]; then
- echo "- **Status:** ✅ All tests passed" >> $GITHUB_STEP_SUMMARY
+ echo "- **Status:** All tests passed" >> $GITHUB_STEP_SUMMARY
else
- echo "- **Status:** ❌ Some tests failed" >> $GITHUB_STEP_SUMMARY
+ echo "- **Status:** Some tests failed" >> $GITHUB_STEP_SUMMARY
fi
fi
@@ -157,9 +157,9 @@ jobs:
run: |
echo "## Redis ${{ matrix.redis-version }} Test Results" >> $GITHUB_STEP_SUMMARY
if [ $? -eq 0 ]; then
- echo "✅ Tests passed with Redis ${{ matrix.redis-version }}" >> $GITHUB_STEP_SUMMARY
+ echo "Tests passed with Redis ${{ matrix.redis-version }}" >> $GITHUB_STEP_SUMMARY
else
- echo "❌ Tests failed with Redis ${{ matrix.redis-version }}" >> $GITHUB_STEP_SUMMARY
+ echo "Tests failed with Redis ${{ matrix.redis-version }}" >> $GITHUB_STEP_SUMMARY
fi
notification:
@@ -174,10 +174,10 @@ jobs:
run: |
if [[ "${{ needs.comprehensive-test.result }}" == "success" && "${{ needs.multi-redis-version-test.result }}" == "success" ]]; then
echo "status=success" >> $GITHUB_OUTPUT
- echo "message=✅ All nightly tests passed successfully" >> $GITHUB_OUTPUT
+ echo "message=All nightly tests passed successfully" >> $GITHUB_OUTPUT
else
echo "status=failure" >> $GITHUB_OUTPUT
- echo "message=❌ Some nightly tests failed" >> $GITHUB_OUTPUT
+ echo "message=Some nightly tests failed" >> $GITHUB_OUTPUT
fi
- name: Create issue on failure
diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml
index 3271f7f..f8f01cf 100644
--- a/.github/workflows/pr-validation.yml
+++ b/.github/workflows/pr-validation.yml
@@ -5,11 +5,6 @@ on:
branches: [ main, develop ]
types: [opened, synchronize, reopened, ready_for_review]
-permissions:
- contents: read
- pull-requests: write
- issues: write
-
jobs:
validate-pr:
name: Validate Pull Request
@@ -57,15 +52,76 @@ jobs:
COVERAGE=$(mvn jacoco:report | grep -o 'Total.*[0-9]*%' | tail -1 | grep -o '[0-9]*%' || echo "0%")
echo "Code coverage: $COVERAGE"
echo "## PR Validation Results" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "**Status:** PASSED" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "### Test Results" >> $GITHUB_STEP_SUMMARY
- echo "- Maven POM validation: PASSED" >> $GITHUB_STEP_SUMMARY
- echo "- Project compilation: PASSED" >> $GITHUB_STEP_SUMMARY
- echo "- Unit tests: PASSED" >> $GITHUB_STEP_SUMMARY
- echo "- Integration tests: PASSED" >> $GITHUB_STEP_SUMMARY
- echo "- Code coverage: $COVERAGE" >> $GITHUB_STEP_SUMMARY
+ echo "- **Code Coverage:** $COVERAGE" >> $GITHUB_STEP_SUMMARY
+ echo "- **Build Status:** Passed" >> $GITHUB_STEP_SUMMARY
+
+ - name: Comment PR with results
+ uses: actions/github-script@v7
+ if: always()
+ with:
+ script: |
+ const fs = require('fs');
+
+ // Read test results
+ let testResults = "Tests completed";
+ try {
+ const summaryFile = process.env.GITHUB_STEP_SUMMARY;
+ if (fs.existsSync(summaryFile)) {
+ testResults = fs.readFileSync(summaryFile, 'utf8');
+ }
+ } catch (error) {
+ console.log('Could not read test results:', error);
+ }
+
+ const comment = `
+ ## PR Validation Results
+
+ **Commit:** \`${{ github.event.pull_request.head.sha }}\`
+ **Status:** ${{ job.status == 'success' && 'Passed' || 'Failed' }}
+
+ ### Test Results
+ - Maven POM validation
+ - Project compilation
+ - Unit tests
+ - Integration tests with Testcontainers
+ - Code coverage analysis
+
+ ### Next Steps
+ ${job.status == 'success'
+ ? 'All checks passed! This PR is ready for review.'
+ : 'Some checks failed. Please review the workflow logs and fix any issues.'}
+
+ **Workflow:** [${{ github.workflow }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
+ `;
+
+ // Find existing comment
+ const comments = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ });
+
+ const existingComment = comments.data.find(comment =>
+ comment.body.includes('PR Validation Results')
+ );
+
+ if (existingComment) {
+ // Update existing comment
+ await github.rest.issues.updateComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: existingComment.id,
+ body: comment
+ });
+ } else {
+ // Create new comment
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: comment
+ });
+ }
security-scan:
name: Security Scan
@@ -120,42 +176,42 @@ jobs:
run: |
echo "Checking license headers in Java files..."
MISSING_LICENSE=0
-
+
for file in $(find src -name "*.java"); do
if ! head -5 "$file" | grep -q "MIT License"; then
- echo "Missing license header: $file"
+ echo "❌ Missing license header: $file"
MISSING_LICENSE=1
fi
done
-
+
if [ $MISSING_LICENSE -eq 0 ]; then
- echo "All Java files have proper license headers"
- echo "- **License Headers:** All files compliant" >> $GITHUB_STEP_SUMMARY
+ echo "✅ All Java files have proper license headers"
+ echo "- **License Headers:** ✅ All files compliant" >> $GITHUB_STEP_SUMMARY
else
- echo "Some files are missing license headers"
- echo "- **License Headers:** Some files missing headers" >> $GITHUB_STEP_SUMMARY
+ echo "❌ Some files are missing license headers"
+ echo "- **License Headers:** ❌ Some files missing 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
+ 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
+ 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
+ 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
+ echo "✅ No trailing whitespace found"
+ echo "- **Trailing Whitespace:** ✅ None found" >> $GITHUB_STEP_SUMMARY
fi
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 392fa42..dc726df 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -53,11 +53,11 @@ jobs:
run: |
echo "Running tests before release..."
mvn --no-transfer-progress --batch-mode clean test -Dtest=RedlockConfigurationTest
- echo "✅ Unit tests passed"
-
+ echo "Unit tests passed"
+
# Run integration tests
mvn --no-transfer-progress --batch-mode test -Dtest=RedlockIntegrationTest
- echo "✅ Integration tests passed"
+ echo "Integration tests passed"
- name: Install GPG key
run: |
@@ -76,7 +76,7 @@ jobs:
- name: Create release summary
run: |
- echo "## 🎉 Release Published Successfully" >> $GITHUB_STEP_SUMMARY
+ echo "## Release Published Successfully" >> $GITHUB_STEP_SUMMARY
echo "- **Version:** ${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "- **Repository:** Maven Central" >> $GITHUB_STEP_SUMMARY
echo "- **Group ID:** org.codarama" >> $GITHUB_STEP_SUMMARY
@@ -105,7 +105,7 @@ jobs:
script: |
const version = '${{ steps.version.outputs.version }}';
const comment = `
- ## 🎉 Successfully Published to Maven Central!
+ ## Successfully Published to Maven Central!
**Version:** \`${version}\`
From dbc82766a6883e4d78ea3f7b07db1fa62fff07ee Mon Sep 17 00:00:00 2001
From: Tihomir Mateev
Date: Fri, 27 Feb 2026 17:33:15 +0200
Subject: [PATCH 5/8] Fixing the flow
---
.github/workflows/pr-validation.yml | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml
index f8f01cf..d12dfd8 100644
--- a/.github/workflows/pr-validation.yml
+++ b/.github/workflows/pr-validation.yml
@@ -5,6 +5,11 @@ on:
branches: [ main, develop ]
types: [opened, synchronize, reopened, ready_for_review]
+permissions:
+ contents: read
+ checks: write
+ pull-requests: write
+
jobs:
validate-pr:
name: Validate Pull Request
From 7698437cf161b476d58bedc2195071e030eb951d Mon Sep 17 00:00:00 2001
From: Tihomir Mateev
Date: Fri, 27 Feb 2026 17:37:59 +0200
Subject: [PATCH 6/8] Fixing the flow 2
---
.github/workflows/ci.yml | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1669968..2222a4d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,6 +11,11 @@ on:
workflow_dispatch:
# Allow manual triggering
+permissions:
+ contents: read
+ checks: write
+ pull-requests: write
+
jobs:
test:
name: Test on ubuntu-latest with Java ${{ matrix.java }}
@@ -58,7 +63,7 @@ jobs:
- name: Generate test report
uses: dorny/test-reporter@v1
- if: success() || failure()
+ if: (success() || failure()) && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
with:
name: Maven Tests (ubuntu-latest, Java ${{ matrix.java }})
path: target/surefire-reports/*.xml
From d18bd6e12a0678624c143aa7b7fb7844403ce3d6 Mon Sep 17 00:00:00 2001
From: Tihomir Mateev
Date: Fri, 27 Feb 2026 17:44:03 +0200
Subject: [PATCH 7/8] Fixing flow 3
---
.github/workflows/pr-validation.yml | 56 +++++++++++++++--------------
1 file changed, 30 insertions(+), 26 deletions(-)
diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml
index d12dfd8..dc92947 100644
--- a/.github/workflows/pr-validation.yml
+++ b/.github/workflows/pr-validation.yml
@@ -66,7 +66,7 @@ jobs:
with:
script: |
const fs = require('fs');
-
+
// Read test results
let testResults = "Tests completed";
try {
@@ -77,12 +77,18 @@ jobs:
} catch (error) {
console.log('Could not read test results:', error);
}
-
+
+ const jobStatus = '${{ job.status }}';
+ const statusText = jobStatus === 'success' ? 'Passed' : 'Failed';
+ const nextSteps = jobStatus === 'success'
+ ? 'All checks passed! This PR is ready for review.'
+ : 'Some checks failed. Please review the workflow logs and fix any issues.';
+
const comment = `
## PR Validation Results
**Commit:** \`${{ github.event.pull_request.head.sha }}\`
- **Status:** ${{ job.status == 'success' && 'Passed' || 'Failed' }}
+ **Status:** ${statusText}
### Test Results
- Maven POM validation
@@ -92,9 +98,7 @@ jobs:
- Code coverage analysis
### Next Steps
- ${job.status == 'success'
- ? 'All checks passed! This PR is ready for review.'
- : 'Some checks failed. Please review the workflow logs and fix any issues.'}
+ ${nextSteps}
**Workflow:** [${{ github.workflow }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
`;
@@ -109,7 +113,7 @@ jobs:
const existingComment = comments.data.find(comment =>
comment.body.includes('PR Validation Results')
);
-
+
if (existingComment) {
// Update existing comment
await github.rest.issues.updateComment({
@@ -179,44 +183,44 @@ jobs:
- name: Check license headers
run: |
- echo "Checking license headers in Java files..."
+ 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 "MIT License"; then
- echo "❌ Missing license header: $file"
+ 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 license headers"
- echo "- **License Headers:** ✅ All files compliant" >> $GITHUB_STEP_SUMMARY
+ 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 license headers"
- echo "- **License Headers:** ❌ Some files missing headers" >> $GITHUB_STEP_SUMMARY
+ 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
+ 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
+ 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
+ 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
+ echo "No trailing whitespace found"
+ echo "- **Trailing Whitespace:** None found" >> $GITHUB_STEP_SUMMARY
fi
From 990439edc49b2d65cc79e7ab61012007cfa11263 Mon Sep 17 00:00:00 2001
From: Tihomir Mateev
Date: Fri, 27 Feb 2026 17:56:17 +0200
Subject: [PATCH 8/8] Fixing flow 4
---
.github/workflows/pr-validation.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml
index dc92947..d5a9ad0 100644
--- a/.github/workflows/pr-validation.yml
+++ b/.github/workflows/pr-validation.yml
@@ -62,7 +62,7 @@ jobs:
- name: Comment PR with results
uses: actions/github-script@v7
- if: always()
+ if: always() && github.event.pull_request.head.repo.full_name == github.repository
with:
script: |
const fs = require('fs');