diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fce8d7c..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 @@ -141,7 +146,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 81756c1..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 @@ -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 @@ -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..d5a9ad0 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -7,8 +7,8 @@ on: permissions: contents: read + checks: write pull-requests: write - issues: write jobs: validate-pr: @@ -57,15 +57,80 @@ 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() && github.event.pull_request.head.repo.full_name == github.repository + 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 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:** ${statusText} + + ### Test Results + - Maven POM validation + - Project compilation + - Unit tests + - Integration tests with Testcontainers + - Code coverage analysis + + ### Next Steps + ${nextSteps} + + **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 @@ -118,22 +183,22 @@ 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 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}\` 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 b542716..0000000 --- a/.idea/AugmentWebviewStateStore.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file 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/formatting.xml b/formatting.xml new file mode 100644 index 0000000..d09d5e7 --- /dev/null +++ b/formatting.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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
  • + *
  • Automatic cleanup on failure
  • *
* - *

Example Usage:

- *
{@code
- * MultiLock multiLock = new MultiLock(
- *     Arrays.asList("account:1", "account:2", "account:3"),
- *     redisDrivers,
- *     config
- * );
+ * 

+ * Example Usage: + *

* - * multiLock.lock(); - * try { - * // All three accounts are now locked - * transferBetweenAccounts(); - * } finally { - * multiLock.unlock(); + *
+ * {
+ *     @code
+ *     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();
+ *     }
  * }
- * }
+ *
*/ 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
  • - *
  • Testing: Synchronize test threads
  • - *
  • Distributed workflows: Coordinate multi-stage processes
  • + *
  • Coordinating startup: Wait for all services to initialize
  • + *
  • Batch processing: Wait for all workers to complete
  • + *
  • Testing: Synchronize test threads
  • + *
  • Distributed workflows: Coordinate multi-stage processes
  • *
* - *

Example Usage:

- *
{@code
- * // Create a latch that waits for 3 operations
- * RedlockCountDownLatch latch = new RedlockCountDownLatch("startup", 3, redisDrivers, config);
+ * 

+ * Example Usage: + *

* - * // Worker threads - * new Thread(() -> { - * initializeService1(); - * latch.countDown(); // Decrement count - * }).start(); + *
+ * {
+ *     @code
+ *     // Create a latch that waits for 3 operations
+ *     RedlockCountDownLatch latch = new RedlockCountDownLatch("startup", 3, redisDrivers, config);
  * 
- * new Thread(() -> {
- *     initializeService2();
- *     latch.countDown(); // Decrement count
- * }).start();
+ *     // Worker threads
+ *     new Thread(() -> {
+ *         initializeService1();
+ *         latch.countDown(); // Decrement count
+ *     }).start();
  * 
- * new Thread(() -> {
- *     initializeService3();
- *     latch.countDown(); // Decrement count
- * }).start();
+ *     new Thread(() -> {
+ *         initializeService2();
+ *         latch.countDown(); // Decrement count
+ *     }).start();
  * 
- * // Main thread waits for all services
- * latch.await(); // Blocks until count reaches 0
- * System.out.println("All services initialized!");
- * }
+ * 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
  • + *
  • Writers must wait for reader count to reach zero
  • *
* - *

Example Usage:

- *
{@code
- * RedlockReadWriteLock rwLock = new RedlockReadWriteLock("resource", redisDrivers, config);
+ * 

+ * Example Usage: + *

* - * // Reading - * rwLock.readLock().lock(); - * try { - * // Multiple threads can read simultaneously - * readData(); - * } finally { - * rwLock.readLock().unlock(); - * } + *
+ * {
+ *     @code
+ *     RedlockReadWriteLock rwLock = new RedlockReadWriteLock("resource", redisDrivers, config);
+ * 
+ *     // Reading
+ *     rwLock.readLock().lock();
+ *     try {
+ *         // Multiple threads can read simultaneously
+ *         readData();
+ *     } finally {
+ *         rwLock.readLock().unlock();
+ *     }
  * 
- * // Writing
- * rwLock.writeLock().lock();
- * try {
- *     // Exclusive access for writing
- *     writeData();
- * } finally {
- *     rwLock.writeLock().unlock();
+ *     // Writing
+ *     rwLock.writeLock().lock();
+ *     try {
+ *         // Exclusive access for writing
+ *         writeData();
+ *     } finally {
+ *         rwLock.writeLock().unlock();
+ *     }
  * }
- * }
+ *
*/ 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
  • + *
  • Blocks when no permits are available
  • + *
  • Automatic permit release on timeout
  • *
* - *

Use Cases:

+ *

+ * Use Cases: + *

*
    - *
  • Rate limiting: Limit concurrent API calls
  • - *
  • Resource pooling: Limit concurrent database connections
  • - *
  • Throttling: Control concurrent access to expensive operations
  • + *
  • Rate limiting: Limit concurrent API calls
  • + *
  • Resource pooling: Limit concurrent database connections
  • + *
  • Throttling: Control concurrent access to expensive operations
  • *
* - *

Example Usage:

- *
{@code
- * // Create a semaphore with 5 permits
- * RedlockSemaphore semaphore = new RedlockSemaphore("api-limiter", 5, redisDrivers, config);
+ * 

+ * Example Usage: + *

* - * // Acquire a permit - * if (semaphore.tryAcquire(5, TimeUnit.SECONDS)) { - * try { - * // Perform rate-limited operation - * callExternalAPI(); - * } finally { - * semaphore.release(); + *
+ * {
+ *     @code
+ *     // 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();
+ *         }
  *     }
  * }
- * }
+ *
*/ 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(); } }