GH-3272: Add lease renewal for distributed locks

Fixes https://github.com/spring-projects/spring-integration/issues/3272

* Introduce a `RenewableLockRegistry` since not all `LockRegistry` implementations
provide a way to renew the lease for the lock
* Implement `RenewableLockRegistry` in the `JdbcLockRegistry`
* Test and document the feature
This commit is contained in:
Alexandre Strubel
2020-07-01 15:17:37 +02:00
committed by Artem Bilan
parent 381a071287
commit e2c6e77f2f
8 changed files with 250 additions and 30 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016-2019 the original author or authors.
* Copyright 2016-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -34,29 +34,27 @@ import javax.sql.DataSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.util.StopWatch;
/**
* @author Dave Syer
* @author Artem Bilan
* @author Glenn Renfro
* @author Alexandre Strubel
*
* @since 4.3
*/
@ContextConfiguration("JdbcLockRegistryTests-context.xml")
@RunWith(SpringJUnit4ClassRunner.class)
@SpringJUnitConfig(locations = "JdbcLockRegistryTests-context.xml")
@DirtiesContext
public class JdbcLockRegistryDifferentClientTests {
@@ -76,7 +74,7 @@ public class JdbcLockRegistryDifferentClientTests {
@Autowired
private DataSource dataSource;
@Before
@BeforeEach
public void clear() {
this.registry.expireUnusedOlderThan(0);
this.client.close();
@@ -86,7 +84,7 @@ public class JdbcLockRegistryDifferentClientTests {
this.child.refresh();
}
@After
@AfterEach
public void close() {
if (this.child != null) {
this.child.close();
@@ -276,4 +274,111 @@ public class JdbcLockRegistryDifferentClientTests {
}
}
@Test
public void testOutOfDateLockTaken() throws Exception {
DefaultLockRepository client1 = new DefaultLockRepository(dataSource);
client1.setTimeToLive(500);
client1.afterPropertiesSet();
final DefaultLockRepository client2 = new DefaultLockRepository(dataSource);
client2.afterPropertiesSet();
Lock lock1 = new JdbcLockRegistry(client1).obtain("foo");
final BlockingQueue<Integer> data = new LinkedBlockingQueue<>();
final CountDownLatch latch1 = new CountDownLatch(1);
final CountDownLatch latch2 = new CountDownLatch(1);
lock1.lockInterruptibly();
Thread.sleep(500);
new SimpleAsyncTaskExecutor()
.execute(() -> {
Lock lock2 = new JdbcLockRegistry(client2).obtain("foo");
try {
latch1.countDown();
StopWatch stopWatch = new StopWatch();
stopWatch.start();
lock2.lockInterruptibly();
stopWatch.stop();
data.add(1);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
finally {
lock2.unlock();
}
latch2.countDown();
});
assertThat(latch1.await(10, TimeUnit.SECONDS)).isTrue();
assertThat(latch2.await(10, TimeUnit.SECONDS)).isTrue();
data.add(2);
lock1.unlock();
for (int i = 0; i < 2; i++) {
Integer integer = data.poll(10, TimeUnit.SECONDS);
assertThat(integer).isNotNull();
assertThat(integer.intValue()).isEqualTo(i + 1);
}
}
@Test
public void testRenewLock() throws Exception {
DefaultLockRepository client1 = new DefaultLockRepository(dataSource);
client1.setTimeToLive(500);
client1.afterPropertiesSet();
final DefaultLockRepository client2 = new DefaultLockRepository(dataSource);
client2.afterPropertiesSet();
JdbcLockRegistry registry = new JdbcLockRegistry(client1);
Lock lock1 = registry.obtain("foo");
final BlockingQueue<Integer> data = new LinkedBlockingQueue<>();
final CountDownLatch latch1 = new CountDownLatch(2);
final CountDownLatch latch2 = new CountDownLatch(1);
lock1.lockInterruptibly();
new SimpleAsyncTaskExecutor()
.execute(() -> {
Lock lock2 = new JdbcLockRegistry(client2).obtain("foo");
try {
latch1.countDown();
StopWatch stopWatch = new StopWatch();
stopWatch.start();
lock2.lockInterruptibly();
stopWatch.stop();
data.add(4);
Thread.sleep(10);
data.add(5);
Thread.sleep(10);
data.add(6);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
finally {
lock2.unlock();
}
});
new SimpleAsyncTaskExecutor()
.execute(() -> {
try {
latch1.countDown();
Thread.sleep(1000);
data.add(1);
Thread.sleep(100);
data.add(2);
Thread.sleep(100);
data.add(3);
latch2.countDown();
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
assertThat(latch1.await(10, TimeUnit.SECONDS)).isTrue();
while (latch2.getCount() > 0) {
Thread.sleep(100);
registry.renewLock("foo");
}
lock1.unlock();
for (int i = 0; i < 6; i++) {
Integer integer = data.poll(10, TimeUnit.SECONDS);
assertThat(integer).isNotNull();
assertThat(integer.intValue()).isEqualTo(i + 1);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016-2019 the original author or authors.
* Copyright 2016-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
package org.springframework.integration.jdbc.lock;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
@@ -25,26 +26,24 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
/**
* @author Dave Syer
* @author Artem Bilan
* @author Stefan Vassilev
*
* @since 4.3
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@SpringJUnitConfig
@DirtiesContext
public class JdbcLockRegistryTests {
@@ -56,7 +55,7 @@ public class JdbcLockRegistryTests {
@Autowired
private LockRepository client;
@Before
@BeforeEach
public void clear() {
this.registry.expireUnusedOlderThan(0);
this.client.close();
@@ -95,7 +94,7 @@ public class JdbcLockRegistryTests {
}
@Test
public void testReentrantLock() throws Exception {
public void testReentrantLock() {
for (int i = 0; i < 10; i++) {
Lock lock1 = this.registry.obtain("foo");
lock1.lock();
@@ -168,7 +167,7 @@ public class JdbcLockRegistryTests {
lock1.unlock();
Object ise = result.get(10, TimeUnit.SECONDS);
assertThat(ise).isInstanceOf(IllegalMonitorStateException.class);
assertThat(((Exception) ise).getMessage()).contains("You do not own");
assertThat(((Exception) ise).getMessage()).contains("own");
}
@Test
@@ -263,7 +262,28 @@ public class JdbcLockRegistryTests {
lock.unlock();
Object imse = result.get(10, TimeUnit.SECONDS);
assertThat(imse).isInstanceOf(IllegalMonitorStateException.class);
assertThat(((Exception) imse).getMessage()).contains("You do not own");
assertThat(((Exception) imse).getMessage()).contains("own");
}
@Test
public void testLockRenew() {
final Lock lock = this.registry.obtain("foo");
assertThat(lock.tryLock()).isTrue();
try {
registry.renewLock("foo");
}
finally {
lock.unlock();
}
}
@Test
public void testLockRenewLockNotOwned() {
this.registry.obtain("foo");
assertThatExceptionOfType(IllegalMonitorStateException.class)
.isThrownBy(() -> registry.renewLock("foo"));
}
}