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.
@@ -43,6 +43,7 @@ import org.springframework.util.Assert;
* @author Artem Bilan
* @author Glenn Renfro
* @author Gary Russell
* @author Alexandre Strubel
*
* @since 4.3
*/
@@ -80,7 +81,8 @@ public class DefaultLockRepository implements LockRepository, InitializingBean {
private String insertQuery = "INSERT INTO %sLOCK (REGION, LOCK_KEY, CLIENT_ID, CREATED_DATE) VALUES (?, ?, ?, ?)";
private String countQuery = "SELECT COUNT(REGION) FROM %sLOCK WHERE REGION=? AND LOCK_KEY=? AND CLIENT_ID=? AND CREATED_DATE>=?";
private String countQuery =
"SELECT COUNT(REGION) FROM %sLOCK WHERE REGION=? AND LOCK_KEY=? AND CLIENT_ID=? AND CREATED_DATE>=?";
/**
* Constructor that initializes the client id that will be associated for
@@ -108,7 +110,7 @@ public class DefaultLockRepository implements LockRepository, InitializingBean {
/**
* A unique grouping identifier for all locks persisted with this store. Using
* multiple regions allows the store to be partitioned (if necessary) for different
* purposes. Defaults to <code>DEFAULT</code>.
* purposes. Defaults to {@code DEFAULT}.
* @param region the region name to set
*/
public void setRegion(String region) {
@@ -179,4 +181,9 @@ public class DefaultLockRepository implements LockRepository, InitializingBean {
new Date(System.currentTimeMillis() - this.ttl));
}
@Override
public boolean renew(String lock) {
return this.template.update(this.updateQuery, new Date(), this.region, lock, this.id) > 0;
}
}

View File

@@ -30,6 +30,7 @@ import org.springframework.dao.CannotAcquireLockException;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.dao.TransientDataAccessException;
import org.springframework.integration.support.locks.ExpirableLockRegistry;
import org.springframework.integration.support.locks.RenewableLockRegistry;
import org.springframework.integration.util.UUIDConverter;
import org.springframework.transaction.TransactionTimedOutException;
import org.springframework.util.Assert;
@@ -49,10 +50,11 @@ import org.springframework.util.Assert;
* @author Bartosz Rempuszewski
* @author Gary Russell
* @author Alexandre Strubel
* @author Stefan Vassilev
*
* @since 4.3
*/
public class JdbcLockRegistry implements ExpirableLockRegistry {
public class JdbcLockRegistry implements ExpirableLockRegistry, RenewableLockRegistry {
private static final int DEFAULT_IDLE = 100;
@@ -101,6 +103,19 @@ public class JdbcLockRegistry implements ExpirableLockRegistry {
}
}
@Override
public void renewLock(Object lockKey) {
Assert.isInstanceOf(String.class, lockKey);
String path = pathFor((String) lockKey);
JdbcLock jdbcLock = this.locks.get(path);
if (jdbcLock == null) {
throw new IllegalStateException("Could not found mutex at " + path);
}
if (!jdbcLock.renew()) {
throw new IllegalStateException("Could not renew mutex at " + path);
}
}
private static final class JdbcLock implements Lock {
private final LockRepository mutex;
@@ -232,7 +247,7 @@ public class JdbcLockRegistry implements ExpirableLockRegistry {
@Override
public void unlock() {
if (!this.delegate.isHeldByCurrentThread()) {
throw new IllegalMonitorStateException("You do not own mutex at " + this.path);
throw new IllegalMonitorStateException("The current thread doesn't own mutex at " + this.path);
}
if (this.delegate.getHoldCount() > 1) {
this.delegate.unlock();
@@ -243,7 +258,7 @@ public class JdbcLockRegistry implements ExpirableLockRegistry {
this.mutex.delete(this.path);
return;
}
catch (TransientDataAccessException e) {
catch (TransientDataAccessException | TransactionTimedOutException e) {
// try again
}
catch (Exception e) {
@@ -264,6 +279,27 @@ public class JdbcLockRegistry implements ExpirableLockRegistry {
return this.mutex.isAcquired(this.path);
}
public boolean renew() {
if (!this.delegate.isHeldByCurrentThread()) {
throw new IllegalMonitorStateException("The current thread doesn't own mutex at " + this.path);
}
while (true) {
try {
boolean renewed = this.mutex.renew(this.path);
if (renewed) {
this.lastUsed = System.currentTimeMillis();
}
return renewed;
}
catch (TransientDataAccessException | TransactionTimedOutException e) {
// try again
}
catch (Exception e) {
throw new DataAccessResourceFailureException("Failed to renew mutex at " + this.path, e);
}
}
}
}
}

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.
@@ -24,6 +24,8 @@ import java.io.Closeable;
* has to be declared as a bean.
*
* @author Dave Syer
* @author Alexandre Strubel
*
* @since 4.3
*/
public interface LockRepository extends Closeable {
@@ -34,6 +36,8 @@ public interface LockRepository extends Closeable {
boolean acquire(String lock);
boolean renew(String lock);
@Override
void close();