Skip to content

Commit

Permalink
GH-3683: Always new TX in DefaultLockRepository
Browse files Browse the repository at this point in the history
Fixes #3683

If a transaction is already active while `JdbcLockRegistry` uses
`DefaultLockRepository` to acquire/release a lock, the repository
must execute SQL queries in a separate transaction to prevent
problems with blocking, deadlocking etc.

It also allows to properly follow transaction isolation level that
is set on some methods of `DefaultLockRepository`.

Previously all methods of DefaultLockRepository supported the current
transaction if there are any. Now all methods will always create new
transaction on each call.

* Change tests to be more unit-ish
* Change `@since` for the `DefaultLockRepositoryTests` to `5.3.10`

**Cherry-pick to `5.4.x` & `5.3.x`**
  • Loading branch information
xak2000 authored and artembilan committed Dec 7, 2021
1 parent 8c85fc9 commit ad34310
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016-2020 the original author or authors.
* Copyright 2016-2021 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.
Expand Down Expand Up @@ -27,6 +27,7 @@
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

Expand All @@ -44,11 +45,11 @@
* @author Glenn Renfro
* @author Gary Russell
* @author Alexandre Strubel
* @author Ruslan Stelmachenko
*
* @since 4.3
*/
@Repository
@Transactional
public class DefaultLockRepository implements LockRepository, InitializingBean {

/**
Expand Down Expand Up @@ -149,17 +150,19 @@ public void afterPropertiesSet() {
this.renewQuery = String.format(this.renewQuery, this.prefix);
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void close() {
this.template.update(this.deleteAllQuery, this.region, this.id);
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void delete(String lock) {
this.template.update(this.deleteQuery, this.region, lock, this.id);
}

@Transactional(isolation = Isolation.SERIALIZABLE)
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE)
@Override
public boolean acquire(String lock) {
if (this.template.update(this.updateQuery, this.id, new Date(), this.region, lock, this.id,
Expand All @@ -174,17 +177,20 @@ public boolean acquire(String lock) {
}
}

@Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)
@Override
public boolean isAcquired(String lock) {
return this.template.queryForObject(this.countQuery, Integer.class, // NOSONAR query never returns null
this.region, lock, this.id, new Date(System.currentTimeMillis() - this.ttl)) == 1;
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void deleteExpired() {
this.template.update(this.deleteExpiredQuery, this.region, new Date(System.currentTimeMillis() - this.ttl));
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public boolean renew(String lock) {
return this.template.update(this.renewQuery, new Date(), this.region, lock, this.id) > 0;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright 2021 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.integration.jdbc.lock;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import java.sql.Connection;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;

/**
* @author Ruslan Stelmachenko
*
* @since 5.3.10
*/
@SpringJUnitConfig(locations = "JdbcLockRegistryTests-context.xml")
@DirtiesContext
public class DefaultLockRepositoryTests {

@Autowired
private LockRepository client;

@BeforeEach
public void clear() {
this.client.close();
}

@Transactional
@Test
public void testNewTransactionIsStartedWhenTransactionIsAlreadyActive() {
// Make sure a transaction is active
assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue();

TransactionSynchronization transactionSynchronization = spy(TransactionSynchronization.class);
TransactionSynchronizationManager.registerSynchronization(transactionSynchronization);

this.client.acquire("foo"); // 1
this.client.renew("foo"); // 2
this.client.delete("foo"); // 3
this.client.isAcquired("foo"); // 4
this.client.deleteExpired(); // 5
this.client.close(); // 6

// Make sure a transaction is still active
assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue();
// And was suspended for each invocation of @Transactional methods of DefaultLockRepository,
// that confirms that these methods were called in a separate transaction each.
verify(transactionSynchronization, times(6)).suspend();
}

@Transactional(isolation = Isolation.REPEATABLE_READ)
@Test
public void testIsAcquiredFromRepeatableReadTransaction() {
// Make sure a transaction with REPEATABLE_READ isolation level is active
assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue();
assertThat(TransactionSynchronizationManager.getCurrentTransactionIsolationLevel())
.isEqualTo(Connection.TRANSACTION_REPEATABLE_READ);

this.client.acquire("foo");
assertThat(this.client.isAcquired("foo")).isTrue();

this.client.delete("foo");
assertThat(this.client.isAcquired("foo")).isFalse();
}

}

0 comments on commit ad34310

Please sign in to comment.