Skip to content

Improve locking test speed by using skip locked instead of waiting for timeout #10667

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public void testQueryLocking(SessionFactoryScope scope, boolean followOnLocking)
statementInspector.assertExecutedCount( 1 );
}

TransactionUtil.updateTable( scope, "employees", "salary", true );
TransactionUtil.assertRowLock( scope, "employees", "salary", "id", employees.get( 0 ).getId(), true );
}
);
} );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,21 @@ void testSimpleUsage(SessionFactoryScope factoryScope) {
final Timeout initialLockTimeout = connectionStrategy.getLockTimeout( conn, session.getFactory() );
assertThat( initialLockTimeout.milliseconds() ).isEqualTo( expectedInitialValue );

final Timeout timeout = Timeout.milliseconds( 2000 );
connectionStrategy.setLockTimeout( timeout, conn, session.getFactory() );
try {
final Timeout timeout = Timeout.milliseconds( 2000 );
connectionStrategy.setLockTimeout( timeout, conn, session.getFactory() );

final Timeout adjustedLockTimeout = connectionStrategy.getLockTimeout( conn, session.getFactory() );
assertThat( adjustedLockTimeout.milliseconds() ).isEqualTo( 2000 );
final Timeout adjustedLockTimeout = connectionStrategy.getLockTimeout( conn, session.getFactory() );
assertThat( adjustedLockTimeout.milliseconds() ).isEqualTo( 2000 );

connectionStrategy.setLockTimeout( Timeouts.WAIT_FOREVER, conn, session.getFactory() );
connectionStrategy.setLockTimeout( Timeouts.WAIT_FOREVER, conn, session.getFactory() );

final Timeout resetLockTimeout = connectionStrategy.getLockTimeout( conn, session.getFactory() );
assertThat( resetLockTimeout.milliseconds() ).isEqualTo( Timeouts.WAIT_FOREVER_MILLI );
final Timeout resetLockTimeout = connectionStrategy.getLockTimeout( conn, session.getFactory() );
assertThat( resetLockTimeout.milliseconds() ).isEqualTo( Timeouts.WAIT_FOREVER_MILLI );
}
finally {
connectionStrategy.setLockTimeout( Timeout.milliseconds( expectedInitialValue ), conn, session.getFactory() );
}
} ) );
}

Expand Down Expand Up @@ -85,6 +90,13 @@ void testNoWait(SessionFactoryScope factoryScope) {

final ConnectionLockTimeoutStrategy connectionStrategy = lockingSupport.getConnectionLockTimeoutStrategy();
final ConnectionLockTimeoutStrategy.Level lockTimeoutType = connectionStrategy.getSupportedLevel();
final int initialValue;
if ( session.getDialect() instanceof MySQLDialect ) {
initialValue = 50;
}
else {
initialValue = Timeouts.WAIT_FOREVER_MILLI;
}

try {
connectionStrategy.setLockTimeout( Timeouts.NO_WAIT, conn, session.getFactory() );
Expand All @@ -101,6 +113,9 @@ void testNoWait(SessionFactoryScope factoryScope) {
throw e;
}
}
finally {
connectionStrategy.setLockTimeout( Timeout.milliseconds( initialValue ), conn, session.getFactory() );
}
} ) );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,44 @@ public Report(Integer id, Person reporter, String... labels) {
this.reporter = reporter;
this.labels = Helper.toSet( labels );
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public Integer getRevision() {
return revision;
}

public void setRevision(Integer revision) {
this.revision = revision;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public Person getReporter() {
return reporter;
}

public void setReporter(Person reporter) {
this.reporter = reporter;
}

public Set<String> getLabels() {
return labels;
}

public void setLabels(Set<String> labels) {
this.labels = labels;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ void simpleTest(SessionFactoryScope factoryScope) {

session.clear();
sqlCollector.clear();
session.find( Detail.class, 1, LockModeType.PESSIMISTIC_WRITE );
final Detail detail = session.find( Detail.class, 1, LockModeType.PESSIMISTIC_WRITE );
assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 );
Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), session.getDialect(), Tables.DETAILS, Tables.SUPPLEMENTALS );
TransactionUtil.updateTable( factoryScope, Tables.DETAILS.getTableName(), "name", true );
TransactionUtil.updateTable( factoryScope, Tables.SUPPLEMENTALS.getTableName(), "txt", true );
TransactionUtil.assertRowLock( factoryScope, Tables.DETAILS.getTableName(), "name", "id", detail.getId(), true );
TransactionUtil.assertRowLock( factoryScope, Tables.SUPPLEMENTALS.getTableName(), "txt", "detail_fk", detail.getId(), true );
} );
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ void testFind(SessionFactoryScope factoryScope) {
assertThat( Hibernate.isInitialized( theTalisman ) ).isTrue();
assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 );
Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), session.getDialect(), BOOKS );
TransactionUtil.updateTable( factoryScope, BOOKS.getTableName(), "title", true );
TransactionUtil.updateTable( factoryScope, BOOK_GENRES.getTableName(), "genre", false );
TransactionUtil.assertRowLock( factoryScope, BOOKS.getTableName(), "title", "id", theTalisman.getId(), true );
TransactionUtil.assertRowLock( factoryScope, BOOK_GENRES.getTableName(), "genre", "book_fk", theTalisman.getId(), false );
} );
}

Expand All @@ -85,9 +85,9 @@ void testFindWithExtended(SessionFactoryScope factoryScope) {
assertThat( Hibernate.isInitialized( theTalisman ) ).isTrue();
assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 );
Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), session.getDialect(), BOOKS );
TransactionUtil.updateTable( factoryScope, BOOKS.getTableName(), "title", true );
TransactionUtil.assertRowLock( factoryScope, BOOKS.getTableName(), "title", "id", theTalisman.getId(), true );
// For strict compliance, EXTENDED here should lock `book_genres` but we do not
TransactionUtil.updateTable( factoryScope, BOOK_GENRES.getTableName(), "genre", false );
TransactionUtil.assertRowLock( factoryScope, BOOK_GENRES.getTableName(), "genre", "book_fk", theTalisman.getId(), false );
} );
}

Expand All @@ -103,8 +103,8 @@ void testFindWithExtendedJpaExpectation(SessionFactoryScope factoryScope) {
// these 2 assertions would depend a bit on the approach and/or dialect
// assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 );
// Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), session.getDialect(), Helper.Table.BOOK_GENRES );
TransactionUtil.updateTable( factoryScope, BOOKS.getTableName(), "title", true );
TransactionUtil.updateTable( factoryScope, BOOK_GENRES.getTableName(), "genre", true );
TransactionUtil.assertRowLock( factoryScope, BOOKS.getTableName(), "title", "id", theTalisman.getId(), true );
TransactionUtil.assertRowLock( factoryScope, BOOK_GENRES.getTableName(), "genre", "book_fk", theTalisman.getId(), true );
} );
}

Expand All @@ -131,8 +131,8 @@ void testFindWithExtendedAndFetch(SessionFactoryScope factoryScope) {
assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 );
Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), session.getDialect(), BOOKS, BOOK_GENRES );

TransactionUtil.updateTable( factoryScope, BOOKS.getTableName(), "title", true );
TransactionUtil.updateTable( factoryScope, BOOK_GENRES.getTableName(), "genre", true );
TransactionUtil.assertRowLock( factoryScope, BOOKS.getTableName(), "title", "id", theTalisman.getId(), true );
TransactionUtil.assertRowLock( factoryScope, BOOK_GENRES.getTableName(), "genre", "book_fk", theTalisman.getId(), true );
}
else {
// should be 3, but follow-on locking is not locking collection tables...
Expand All @@ -142,7 +142,7 @@ void testFindWithExtendedAndFetch(SessionFactoryScope factoryScope) {

// todo : track this down - HHH-19513
//Helper.checkSql( sqlCollector.getSqlQueries().get( 2 ), session.getDialect(), BOOK_GENRES );
//TransactionUtil.updateTable( factoryScope, BOOK_GENRES.getTableName(), "genre", true );
//TransactionUtil.assertRowLock( factoryScope, BOOK_GENRES.getTableName(), "genre", "book_fk", theTalisman.getId(), true );
}
} );
}
Expand All @@ -161,8 +161,8 @@ void testLock(SessionFactoryScope factoryScope) {

Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), session.getDialect(), BOOKS );

TransactionUtil.updateTable( factoryScope, BOOKS.getTableName(), "title", true );
TransactionUtil.updateTable( factoryScope, BOOK_GENRES.getTableName(), "genre", false );
TransactionUtil.assertRowLock( factoryScope, BOOKS.getTableName(), "title", "id", theTalisman.getId(), true );
TransactionUtil.assertRowLock( factoryScope, BOOK_GENRES.getTableName(), "genre", "book_fk", theTalisman.getId(), false );
} );
}

Expand All @@ -178,9 +178,9 @@ void testLockWithExtended(SessionFactoryScope factoryScope) {
session.lock( theTalisman, PESSIMISTIC_WRITE, EXTENDED );
assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 );
Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), session.getDialect(), BOOKS );
TransactionUtil.updateTable( factoryScope, BOOKS.getTableName(), "title", true );
TransactionUtil.assertRowLock( factoryScope, BOOKS.getTableName(), "title", "id", theTalisman.getId(), true );
// Again, for strict compliance, EXTENDED here should lock `book_genres` but we do not
TransactionUtil.updateTable( factoryScope, BOOK_GENRES.getTableName(), "genre", false );
TransactionUtil.assertRowLock( factoryScope, BOOK_GENRES.getTableName(), "genre", "book_fk", theTalisman.getId(), false );
} );
}

Expand All @@ -196,8 +196,8 @@ void testRefresh(SessionFactoryScope factoryScope) {
session.refresh( theTalisman, PESSIMISTIC_WRITE );
assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 );
Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), session.getDialect(), BOOKS );
TransactionUtil.updateTable( factoryScope, BOOKS.getTableName(), "title", true );
TransactionUtil.updateTable( factoryScope, BOOK_GENRES.getTableName(), "genre", false );
TransactionUtil.assertRowLock( factoryScope, BOOKS.getTableName(), "title", "id", theTalisman.getId(), true );
TransactionUtil.assertRowLock( factoryScope, BOOK_GENRES.getTableName(), "genre", "book_fk", theTalisman.getId(), false );
} );
}

Expand All @@ -213,9 +213,9 @@ void testRefreshWithExtended(SessionFactoryScope factoryScope) {
session.refresh( theTalisman, PESSIMISTIC_WRITE, EXTENDED );
assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 );
Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), session.getDialect(), BOOKS );
TransactionUtil.updateTable( factoryScope, BOOKS.getTableName(), "title", true );
TransactionUtil.assertRowLock( factoryScope, BOOKS.getTableName(), "title", "id", theTalisman.getId(), true );
// Again, for strict compliance, EXTENDED here should lock `book_genres` but we do not
TransactionUtil.updateTable( factoryScope, BOOK_GENRES.getTableName(), "genre", false );
TransactionUtil.assertRowLock( factoryScope, BOOK_GENRES.getTableName(), "genre", "book_fk", theTalisman.getId(), false );
} );
}

Expand All @@ -226,12 +226,12 @@ void testEagerFind(SessionFactoryScope factoryScope) {

factoryScope.inTransaction( (session) -> {
sqlCollector.clear();
session.find( Report.class, 2, PESSIMISTIC_WRITE );
final Report report = session.find( Report.class, 2, PESSIMISTIC_WRITE );
assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 );
Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), session.getDialect(), REPORTS );
TransactionUtil.updateTable( factoryScope, REPORTS.getTableName(), "title", true );
TransactionUtil.updateTable( factoryScope, REPORT_LABELS.getTableName(), "txt", willAggressivelyLockJoinedTables( session.getDialect() ) );
TransactionUtil.updateTable( factoryScope, PERSONS.getTableName(), "name", willAggressivelyLockJoinedTables( session.getDialect() ) );
TransactionUtil.assertRowLock( factoryScope, REPORTS.getTableName(), "title", "id", report.getId(), true );
TransactionUtil.assertRowLock( factoryScope, REPORT_LABELS.getTableName(), "txt", "report_fk", report.getId(), willAggressivelyLockJoinedTables( session.getDialect() ) );
TransactionUtil.assertRowLock( factoryScope, PERSONS.getTableName(), "name", "id", report.getReporter().getId(), willAggressivelyLockJoinedTables( session.getDialect() ) );
} );
}

Expand All @@ -258,28 +258,29 @@ void testEagerFindWithExtended(SessionFactoryScope factoryScope) {

factoryScope.inTransaction( (session) -> {
sqlCollector.clear();
session.find( Report.class, 2, PESSIMISTIC_WRITE, EXTENDED );
final Report report = session.find( Report.class, 2, PESSIMISTIC_WRITE, EXTENDED );
if ( session.getDialect().supportsOuterJoinForUpdate() ) {
assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 );
Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), session.getDialect(), REPORTS, REPORT_LABELS );
TransactionUtil.updateTable( factoryScope, REPORTS.getTableName(), "title", true );
TransactionUtil.updateTable( factoryScope, PERSONS.getTableName(), "name", willAggressivelyLockJoinedTables( session.getDialect() ) );
TransactionUtil.updateTable( factoryScope, REPORT_LABELS.getTableName(), "txt", true );
TransactionUtil.assertRowLock( factoryScope, REPORTS.getTableName(), "title", "id", report.getId(), true );
TransactionUtil.assertRowLock( factoryScope, PERSONS.getTableName(), "name", "id", report.getReporter().getId(),
willAggressivelyLockJoinedTables( session.getDialect() ) );
TransactionUtil.assertRowLock( factoryScope, REPORT_LABELS.getTableName(), "txt", "report_fk", report.getId(), true );
}
else {
assertThat( sqlCollector.getSqlQueries() ).hasSize( 3 );
Helper.checkSql( sqlCollector.getSqlQueries().get( 1 ), session.getDialect(), REPORTS );
Helper.checkSql( sqlCollector.getSqlQueries().get( 2 ), session.getDialect(), PERSONS );
TransactionUtil.updateTable( factoryScope, REPORTS.getTableName(), "title", true );
TransactionUtil.assertRowLock( factoryScope, REPORTS.getTableName(), "title", "id", report.getId(), true );

// these should happen but currently do not - follow-on locking is not locking element-collection tables...
// todo : track this down - HHH-19513
//Helper.checkSql( sqlCollector.getSqlQueries().get( 2 ), session.getDialect(), REPORT_LABELS );
//TransactionUtil.updateTable( factoryScope, REPORT_LABELS.getTableName(), "txt", true );
//TransactionUtil.assertRowLock( factoryScope, REPORT_LABELS.getTableName(), "txt", "report_fk", report.getId(), true );

// this one should not happen at all. follow-on locking is not understanding scope probably..
// todo : track this down - HHH-19514
TransactionUtil.updateTable( factoryScope, PERSONS.getTableName(), "name", true );
TransactionUtil.assertRowLock( factoryScope, PERSONS.getTableName(), "name", "id", report.getReporter().getId(), true );
}
} );
}
Expand All @@ -292,29 +293,29 @@ void testEagerFindWithFetchScope(SessionFactoryScope factoryScope) {

factoryScope.inTransaction( (session) -> {
sqlCollector.clear();
session.find( Report.class, 2, PESSIMISTIC_WRITE, Locking.Scope.INCLUDE_FETCHES );
final Report report = session.find( Report.class, 2, PESSIMISTIC_WRITE, Locking.Scope.INCLUDE_FETCHES );

if ( session.getDialect().supportsOuterJoinForUpdate() ) {
assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 );
Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), session.getDialect(), REPORTS, REPORT_LABELS, JOINED_REPORTER );
TransactionUtil.updateTable( factoryScope, REPORTS.getTableName(), "title", true );
TransactionUtil.updateTable( factoryScope, PERSONS.getTableName(), "name", true );
TransactionUtil.updateTable( factoryScope, REPORT_LABELS.getTableName(), "txt", true );
TransactionUtil.assertRowLock( factoryScope, REPORTS.getTableName(), "title", "id", report.getId(), true );
TransactionUtil.assertRowLock( factoryScope, PERSONS.getTableName(), "name", "id", report.getReporter().getId(), true );
TransactionUtil.assertRowLock( factoryScope, REPORT_LABELS.getTableName(), "txt", "report_fk", report.getId(), true );
}
else {
assertThat( sqlCollector.getSqlQueries() ).hasSize( 3 );
Helper.checkSql( sqlCollector.getSqlQueries().get( 1 ), session.getDialect(), REPORTS );
Helper.checkSql( sqlCollector.getSqlQueries().get( 2 ), session.getDialect(), PERSONS );
TransactionUtil.updateTable( factoryScope, REPORTS.getTableName(), "title", true );
TransactionUtil.assertRowLock( factoryScope, REPORTS.getTableName(), "title", "id", report.getId(), true );

// these should happen but currently do not - follow-on locking is not locking element-collection tables...
// todo : track this down - HHH-19513
//Helper.checkSql( sqlCollector.getSqlQueries().get( 2 ), session.getDialect(), REPORT_LABELS );
//TransactionUtil.updateTable( factoryScope, REPORT_LABELS.getTableName(), "txt", true );
//TransactionUtil.assertRowLock( factoryScope, REPORT_LABELS.getTableName(), "txt", "report_fk", report.getId(), true );

// this one should not happen at all. follow-on locking is not understanding scope probably..
// todo : track this down - HHH-19514
TransactionUtil.updateTable( factoryScope, PERSONS.getTableName(), "name", true );
TransactionUtil.assertRowLock( factoryScope, PERSONS.getTableName(), "name", "id", report.getReporter().getId(), true );
}
} );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,42 @@
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

/**
* @author Steve Ebersole
*/
public class AsyncExecutor {

// Need more than a single thread, because not all databases support cancellation of statements waiting for locks
private static final ExecutorService EXECUTOR_SERVICE = Executors.newCachedThreadPool();

public static void executeAsync(Runnable action) {
final ExecutorService executorService = Executors.newSingleThreadExecutor();
final Future<?> future = EXECUTOR_SERVICE.submit( action );
try {
executorService.submit( action ).get();
future.get();
}
catch (InterruptedException e) {
throw new RuntimeException( "Thread interruption", e );
future.cancel( true );
throw new TimeoutException( "Thread interruption", e );
}
catch (ExecutionException e) {
throw new RuntimeException( "Async execution error", e );
throw new RuntimeException( "Async execution error", e.getCause() );
}
}

public static void executeAsync(int timeout, TimeUnit timeoutUnit, Runnable action) {
final ExecutorService executorService = Executors.newSingleThreadExecutor();
final Future<?> future = EXECUTOR_SERVICE.submit( action );
try {
executorService.submit( action ).get( timeout, timeoutUnit );
future.get( timeout, timeoutUnit );
}
catch (InterruptedException e) {
future.cancel( true );
throw new TimeoutException( "Thread interruption", e );
}
catch (java.util.concurrent.TimeoutException e) {
future.cancel( true );
throw new TimeoutException( "Thread timeout exceeded", e );
}
catch (ExecutionException e) {
Expand Down
Loading