Use CAS/CAD commands in RedisLockRegistry#10930
Use CAS/CAD commands in RedisLockRegistry#10930Dgramada wants to merge 2 commits intospring-projects:mainfrom
Conversation
* Use CAS for lock renewal in RedisLock * Use CAD for lock release in RedisSpinLock * Add graceful fallback to Lua scripts for older Redis versions * Bump test container to Redis 8.4.0 * Add tests for CAS/CAD fallback and lock safety Signed-off-by: yordantsintsov <yordan.tsintsov@gmail.com>
Signed-off-by: yordantsintsov <yordan.tsintsov@gmail.com>
cppwfs
left a comment
There was a problem hiding this comment.
Thank you for your contribution!
| if (isCasCadNotSupportedError(ex)) { | ||
| LOGGER.debug("CAS/CAD for value operations not supported, falling back to Lua script", ex); | ||
| RedisLockRegistry.this.supportsCasCadOperations = false; | ||
| res = RedisLockRegistry.this.redisTemplate.execute( |
There was a problem hiding this comment.
The LUA script execution can be put into a method so that we can reduce code duplication.
Meaning we can use that method here at line 620 and 630.
| if (isCasCadNotSupportedError(ex)) { | ||
| LOGGER.debug("CAS/CAD for value operations not supported, falling back to Lua script", ex); | ||
| RedisLockRegistry.this.supportsCasCadOperations = false; | ||
| return Boolean.TRUE.equals(RedisLockRegistry.this.redisTemplate.execute( |
There was a problem hiding this comment.
The LUA script execution can be put into a method so that we can reduce code duplication.
Meaning we can use that method here at line 885 and 895.
|
|
||
| ReflectionTestUtils.setField(registry, "supportsCasCadOperations", false); | ||
|
|
||
| Lock lock = registry.obtain("foo"); |
There was a problem hiding this comment.
nitpick:
Can we change the name of the lock from "foo" to "testLock" in the tests.
Thanks!
| finally { | ||
| lock2.unlock(); | ||
| } | ||
| registry1.destroy(); |
There was a problem hiding this comment.
probably want to move the registry1.destroy and registry2.destroy in a finally block to make sure they are destroyed in the case that an exception is thrown.
| A `RedisLock` can now be acquired using the `lock(Duration ttl)` or `tryLock(long time, TimeUnit unit, Duration ttl)` method, with a specified time-to-live (TTL) value. | ||
| The `RedisLockRegistry` now provides new `renewLock(Object lockKey, Duration ttl)` method, allowing to renew the lock with a custom time-to-live value. | ||
|
|
||
| Starting with version 7.1, the `RedisLockRegistry` prefers native Redis CAS (Compare-And-Set) and CAD (Compare-And-Delete) commands over Lua scripts for lock renewal and spin-lock release. |
There was a problem hiding this comment.
Reword this as:
Starting with version 7.1, the RedisLockRegistry uses native Redis CAS (Compare-And-Set) and CAD (Compare-And-Delete) commands for Redis 8.4 or later. For older Redis versions, the registry automatically falls back to the previous Lua script-based approach.
There was a problem hiding this comment.
... and one sentence per line!
https://asciidoctor.org/docs/asciidoc-recommended-practices/#one-sentence-per-line
artembilan
left a comment
There was a problem hiding this comment.
Looks promising.
Thank you!
| }; | ||
| } | ||
|
|
||
| private boolean isCasCadNotSupportedError(Exception ex) { |
|
|
||
| if (RedisLockRegistry.this.supportsCasCadOperations) { | ||
| try { | ||
| res = RedisLockRegistry.this.redisTemplate.boundValueOps(this.lockKey).set( |
There was a problem hiding this comment.
Can boundValueOps be extracted into an instance property?
The RedisLock ctor looks like a nice place for that one.
| } | ||
| catch (RedisSystemException | InvalidDataAccessApiUsageException ex) { | ||
| if (isCasCadNotSupportedError(ex)) { | ||
| LOGGER.debug("CAS/CAD for value operations not supported, falling back to Lua script", ex); |
| RedisLockRegistry.this.clientId)); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
The blank line still must be present.
Every class member should be surrounded with blank line to make code more readable.
| RedisLockRegistry registry = new RedisLockRegistry(redisConnectionFactory, this.registryKey); | ||
| registry.setRedisLockType(testRedisLockType); | ||
|
|
||
| ReflectionTestUtils.setField(registry, "supportsCasCadOperations", false); |
There was a problem hiding this comment.
Doesn't look like this is about falback.
With this you explicitly say that we even don't try CasCad.
Consider to spy RedisTemplate and stub those respective methods to fail with error.
Then we really would see a fallback in action.
| assertThat(lock.tryLock()).isTrue(); | ||
| try { | ||
| registry.renewLock("foo", Duration.ofSeconds(10)); | ||
| Thread.sleep(shortExpiry + 100); |
There was a problem hiding this comment.
This is not reliable test.
The CPU could be busy with something else much longer than expire period.
It might be better to verify that TTL was extended after renew call.
But sleep and then check if it is still there is not OK.
| Lock lock = registry.obtain("foo"); | ||
| lock.lock(); | ||
| assertThatNoException().isThrownBy(lock::unlock); | ||
| registry.destroy(); |
There was a problem hiding this comment.
I think this test does not have a any value.
Can we consider to remove it altogether?
| A `RedisLock` can now be acquired using the `lock(Duration ttl)` or `tryLock(long time, TimeUnit unit, Duration ttl)` method, with a specified time-to-live (TTL) value. | ||
| The `RedisLockRegistry` now provides new `renewLock(Object lockKey, Duration ttl)` method, allowing to renew the lock with a custom time-to-live value. | ||
|
|
||
| Starting with version 7.1, the `RedisLockRegistry` prefers native Redis CAS (Compare-And-Set) and CAD (Compare-And-Delete) commands over Lua scripts for lock renewal and spin-lock release. |
There was a problem hiding this comment.
... and one sentence per line!
https://asciidoctor.org/docs/asciidoc-recommended-practices/#one-sentence-per-line
cppwfs
left a comment
There was a problem hiding this comment.
Again thank you for the contribution. Just a couple more requests.
| */ | ||
| private volatile @Nullable RedisMessageListenerContainer redisMessageListenerContainer; | ||
|
|
||
| private volatile boolean supportsCasCadOperations = true; |
There was a problem hiding this comment.
Need to provide a javadoc for this variable.
| RedisLockRegistry.this.clientId)); | ||
| if (RedisLockRegistry.this.supportsCasCadOperations) { | ||
| try { | ||
| return RedisLockRegistry.this.redisTemplate.delete(this.lockKey, it -> it.ifEquals().value(RedisLockRegistry.this.clientId)); |
There was a problem hiding this comment.
On line 450 the javadocs states we do an unlink, but here we are doing a delete. The javadoc needs to be updated to state that for redis 8.4+ we do a delete else its an unlink.
References #10782
This PR updates
RedisLockRegistryto prefer Redis CAS (Compare-And-Set) and CAD (Compare-And-Delete) commands over Lua scripts for lock renewal and lock release inRedisSpinLock.CAS/CAD are native Redis commands (since. 8.4) that provide the same conditional semantics as the equivalent Lua scripts but without the overhead of script evaluation.