Skip to content

Commit 66cd225

Browse files
committed
Adjust cache capacity in the JdbcLockRegistry (#11172)
Fixes: #11172 When `cacheCapacity` has shrunk, and we obtain already held lock again, it is evicted due to the current `if (this.locks.size() > this.cacheCapacity)` logic. This leads to several side effects for already locked instance. * Implement the logic for shrinking `cacheCapacity`: evict unsed lock when cache size is higher than new `cacheCapacity`. * Fix `obtain()` logic to skip eviction attempt if obtained lock is held **Auto-cherry-pick to `7.0.x`** # Conflicts: # spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/lock/JdbcLockRegistryTests.java
1 parent df68d49 commit 66cd225

2 files changed

Lines changed: 77 additions & 2 deletions

File tree

spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/lock/JdbcLockRegistry.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,17 @@ public void setIdleBetweenTries(Duration idleBetweenTries) {
139139

140140
/**
141141
* Set the capacity of cached locks.
142+
* If the number of currently cached locks exceeds the new capacity,
143+
* this method tries to evict unused locks.
142144
* @param cacheCapacity The capacity of cached lock, (default 100_000).
143145
* @since 5.5.6
146+
* @see #expireUnusedOlderThan(long)
144147
*/
145148
public void setCacheCapacity(int cacheCapacity) {
149+
Assert.isTrue(cacheCapacity > 0, "'cacheCapacity' must be greater than 0");
150+
if (this.locks.size() > cacheCapacity) {
151+
expireUnusedOlderThan(0);
152+
}
146153
this.cacheCapacity = cacheCapacity;
147154
}
148155

@@ -154,7 +161,8 @@ public DistributedLock obtain(Object lockKey) {
154161
try {
155162
JdbcLock jdbcLock =
156163
this.locks.computeIfAbsent(lockKeyToUse, key -> new JdbcLock(lockKeyToUse));
157-
if (this.locks.size() > this.cacheCapacity) {
164+
165+
if (!jdbcLock.isAcquiredInThisProcess() && this.locks.size() > this.cacheCapacity) {
158166
long now = System.currentTimeMillis();
159167
this.locks.entrySet()
160168
.removeIf(entry -> {
@@ -168,6 +176,7 @@ public DistributedLock obtain(Object lockKey) {
168176
"from " + this + ". Cannot obtain more at the moment.");
169177
}
170178
}
179+
171180
return jdbcLock;
172181
}
173182
finally {
@@ -187,7 +196,7 @@ public void expireUnusedOlderThan(long age) {
187196
this.locks.entrySet()
188197
.removeIf(entry -> {
189198
JdbcLock lock = entry.getValue();
190-
return now - lock.getLastUsed() > age && !lock.isAcquiredInThisProcess();
199+
return now - lock.getLastUsed() >= age && !lock.isAcquiredInThisProcess();
191200
});
192201
}
193202
finally {

spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/lock/JdbcLockRegistryTests.java

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,72 @@ void unusedLockIsStale() {
727727
.withMessageStartingWith("The lock 'lock1' was evicted from the exhausted cache due to its unused period for");
728728
}
729729

730+
@Test
731+
void shrinkingCacheCapacityEvictsUnusedEntries() {
732+
DefaultLockRepository client = new DefaultLockRepository(dataSource);
733+
client.setApplicationContext(this.context);
734+
client.afterPropertiesSet();
735+
client.afterSingletonsInstantiated();
736+
JdbcLockRegistry registry = new JdbcLockRegistry(client);
737+
registry.setCacheCapacity(2);
738+
739+
DistributedLock lock1 = registry.obtain("lock1");
740+
lock1.lock();
741+
try {
742+
DistributedLock lock2 = registry.obtain("lock2");
743+
registry.setCacheCapacity(1);
744+
assertThatExceptionOfType(CannotAcquireLockException.class)
745+
.isThrownBy(lock2::lock)
746+
.havingCause()
747+
.isInstanceOf(IllegalStateException.class)
748+
.withMessageStartingWith(
749+
"The lock 'lock2' was evicted from the exhausted cache due to its unused period for");
750+
}
751+
finally {
752+
lock1.unlock();
753+
}
754+
}
755+
756+
@Test
757+
void obtainDoesNotEvictCurrentlyHeldLockOfRequestedKey() {
758+
DefaultLockRepository client = new DefaultLockRepository(dataSource);
759+
client.setApplicationContext(this.context);
760+
client.afterPropertiesSet();
761+
client.afterSingletonsInstantiated();
762+
JdbcLockRegistry registry = new JdbcLockRegistry(client);
763+
registry.setCacheCapacity(2);
764+
765+
DistributedLock lock1 = registry.obtain("lock1");
766+
lock1.lock();
767+
try {
768+
DistributedLock lock2 = registry.obtain("lock2");
769+
lock2.lock();
770+
try {
771+
// Shrinking the capacity below the number of currently held locks forces
772+
// the next `obtain` for an already-cached, held key to see `size() > cacheCapacity`.
773+
registry.setCacheCapacity(1);
774+
775+
Map<String, Lock> registryLocks = getRegistryLocks(registry);
776+
777+
assertThat(registryLocks).containsKeys("lock1", "lock2");
778+
779+
assertThat(registry.obtain("lock1")).isSameAs(lock1);
780+
781+
assertThat(registryLocks).containsKeys("lock1", "lock2");
782+
783+
assertThatNoException().isThrownBy(lock1::lock);
784+
785+
lock1.unlock();
786+
}
787+
finally {
788+
lock2.unlock();
789+
}
790+
}
791+
finally {
792+
lock1.unlock();
793+
}
794+
}
795+
730796
@SuppressWarnings("unchecked")
731797
private static Map<String, Lock> getRegistryLocks(JdbcLockRegistry registry) {
732798
return TestUtils.getPropertyValue(registry, "locks", Map.class);

0 commit comments

Comments
 (0)