Thomas Escolan
Thomas Escolan

Reputation: 1529

Spring Integration JDBC lock failure

I don't understand the behavior for distributed locks obtained from a JdbcLockRegistry.

@Bean
public LockRepository lockRepository(DataSource datasource) {
    return new DefaultLockRepository(datasource);
}

@Bean
public LockRegistry lockRegistry(LockRepository repository) {
    return new JdbcLockRegistry(repository);
}

My project is running upon PostgreSQL and Spring Boot version is 2.2.2 And this is the demonstration use case :

@GetMapping("/isolate")
public String isolate() throws InterruptedException {
    Lock lock = registry.obtain("the-lock");
    if (lock.tryLock(10, TimeUnit.SECONDS)) {   // close
        try {
            Thread.sleep(30 * 1000L);
        } finally {
            lock.unlock();                      // open
        }
    } else {
        return "rejected";
    }
    return "acquired";
}

NB: that use case works when playing with Hazelcast distributed locks.

The observed behavior is that a first lock is duly registered in database through a call to the API on a first instance. Then, within 30 seconds, a second on is requested on a different instance (other port), and it's updating the existing int_lock table's line (client_id changes) instead of failing. So the first endpoint delivers after 30 seconds (no unlock failure), and the second endpoint is delivering after its own period of 30 seconds. There is no mutual exclusion.

These are the logs for a single acquisition :

Trying to acquire lock...
Executing prepared SQL update
Executing prepared SQL statement [DELETE FROM INT_LOCK WHERE REGION=? AND LOCK_KEY=? AND CREATED_DATE<?]
Executing prepared SQL update
Executing prepared SQL statement [UPDATE INT_LOCK SET CREATED_DATE=? WHERE REGION=? AND LOCK_KEY=? AND CLIENT_ID=?]
Executing prepared SQL update
Executing prepared SQL statement [INSERT INTO INT_LOCK (REGION, LOCK_KEY, CLIENT_ID, CREATED_DATE) VALUES (?, ?, ?, ?)]
Processing...
Executing prepared SQL update
Executing prepared SQL statement [DELETE FROM INT_LOCK WHERE REGION=? AND LOCK_KEY=? AND CLIENT_ID=?]

It sounds strange that acquisition process begins with DELETE, though... I've tried to set a constant client id for the DefaultLockRepository, without improvement. Does anyone have a clue of understanding of how to fix this ? Thx for any help.

Upvotes: 3

Views: 6196

Answers (3)

Dzerjinsky
Dzerjinsky

Reputation: 11

try lock.renew to extend lock period. lock.lock() doesn't update lock until it expires.

Upvotes: 1

Thomas Escolan
Thomas Escolan

Reputation: 1529

Trying to maintain a lock, I tried to take benefit of DefaultLockRepository#acquire, called by Lock#lock, which attempts update before inserting a new lock (and after cleaning up expired locks, as said before):

@GetMapping("/isolate")
public String isolate() throws InterruptedException {
    Lock lock = registry.obtain("the-lock");
    log.warn("Trying to acquire lock...");
    if (lock.tryLock(10, TimeUnit.SECONDS)) {    // close lock
        try {
            for (int i=0; i < 6; i++) {          // very...
                log.warn("Processing...");
                Thread.sleep(5 * 1000L);         // ... long task
                lock.lock();                     //DEBUG holding (lock update)
            }
        } finally {
            if (!repository.isAcquired("the-lock")) {
                throw new IllegalStateException("lock lost");
            } else {
                lock.unlock();                   // open lock
            }
        }
    } else {
        return "rejected";
    }
    return "acquired";
}

But this didn't work as expected (NB: ttl is on default 10s in this test); I always get a lock lost IllegalStateException in the end, despite the fact that I can see the lock date changing in PostgreSQL's console.

Upvotes: 0

Thomas Escolan
Thomas Escolan

Reputation: 1529

All right. It happens that the repository's TTL is 10s by default, just like my timeout in that specific use case. So the lock obviously dies (DELETE) before timeout period. Here is a fix then:

@Bean
public LockRepository lockRepository(DataSource datasource) {
    DefaultLockRepository repository = new DefaultLockRepository(datasource);
    repository.setTimeToLive(60 * 1000);
    return repository;
}

Upvotes: 6

Related Questions