Skip to content

HHH-19551 - Address deficiencies in pessimistic locking #10194

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
Jul 22, 2025

Conversation

sebersole
Copy link
Member

@sebersole sebersole commented May 21, 2025

HHH-19551 - Address deficiencies in pessimistic locking

[Please describe here what your change is about]


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license
and can be relicensed under the terms of the LGPL v2.1 license in the future at the maintainers' discretion.
For more information on licensing, please check here.


https://hibernate.atlassian.net/browse/HHH-19551

@gavinking
Copy link
Member

Excellent!

@sebersole
Copy link
Member Author

Unless I did something silly, this should be just Oracle failures now. Need to investigate those more.

@sebersole sebersole force-pushed the lock-scope-2 branch 2 times, most recently from eaede26 to 321fd2e Compare June 4, 2025 21:47
@sebersole sebersole force-pushed the lock-scope-2 branch 2 times, most recently from 776d5f6 to e0e279e Compare June 16, 2025 19:29
@sebersole sebersole force-pushed the lock-scope-2 branch 2 times, most recently from 535fa89 to 00f0c68 Compare June 20, 2025 15:40
private final LockTimeoutType noWaitType;
private final LockTimeoutType waitType;

public MariaDBLockingSupport(boolean supportsSkipLocked, boolean supportsNoWait, boolean supportsWait) {

Check notice

Code scanning / CodeQL

Useless parameter Note

The parameter 'supportsWait' is never used.
@sebersole sebersole changed the title HHH-19336 - Proper implementation for JPA extended locking scope HHH-19551 - Address deficiencies in pessimistic locking Jun 20, 2025
@sebersole
Copy link
Member Author

About to head out for ~2 weeks of PTO...

About test failures:

  1. MariaDB - one test fails (LockedRowsTests#testFindSkipLocked). It does not fail locally if I just run that test or run all the tests in that class or run all the tests in that package or ... makes it hard to debug, but I'll gwt back to it after PTO
  2. Oracle - I have not even started digging into these. I'm 100% certain it is because of the way AbstractSqlAstTranslator has LOTS of code specific to Oracle. Handling of for-update was definitely part of that.

Some things I'd still like accomplish here...

  1. The main one is to move rendering of the lock fragments from Dialect proper into the LockingSupport impls.
  2. A related task is to make LockingSupport.Metadata sensitive to write/read locking. I think tjis will be needed for (1).
  3. Integrate support for LockTimeoutType#CONNECTION into the acquisition of locks when performing find/lock/refresh operations. I've implemented ConnectionLockTimeoutStrategy for quite a few dialects already, which handles the actual setting/resetting.
  4. Adjust design of JdbcOperation to simply be a concept of something that needs to be performed via JDBC. Currently it implies a single statement. Ultimately I want this to be open-ended and mean 1-or-more statements. This would allow grouping all the SQL needed for locking into a single JdbcOperation and combine ConnectionLockTimeoutStrategy, follow-on locking as well as the main query.

WRT (2), currently I have:

public interface LockingSupport {
	Metadata getMetadata();

	...

	interface Metadata {
		LockTimeoutType getLockTimeoutType(Timeout timeout);
		RowLockStrategy getReadRowLockStrategy();
		RowLockStrategy getWriteRowLockStrategy();
		...
	}
}

public class Dialect {
	...
	LockingSupport getLockingSupport();
}

Thinking instead of something like:

public interface LockingSupport {
	Metadata getMetadata();

	...

	interface Metadata {
		LockTimeoutType getLockTimeoutType(Timeout timeout);
		RowLockStrategy getRowLockStrategy();
		...
	}
}

public class Dialect {
	...
	LockingSupport getWriteLockingSupport();
	LockingSupport getReadLockingSupport();
}

@sebersole sebersole force-pushed the lock-scope-2 branch 3 times, most recently from b111da9 to e82dd20 Compare July 18, 2025 20:11
if ( factoryHint instanceof Integer number ) {
return number;
}
return Integer.parseInt( factoryHint.toString() );

Check notice

Code scanning / CodeQL

Missing catch of NumberFormatException Note

Potential uncaught 'java.lang.NumberFormatException'.
return Timeouts.WAIT_FOREVER;
}
assert value.endsWith( "s" );
final int secondsValue = Integer.parseInt( value.substring( 0, value.length() - 1 ) );

Check notice

Code scanning / CodeQL

Missing catch of NumberFormatException Note

Potential uncaught 'java.lang.NumberFormatException'.
@sebersole sebersole force-pushed the lock-scope-2 branch 2 times, most recently from 876b982 to 9429a7a Compare July 21, 2025 12:09
@sebersole
Copy link
Member Author

Let's see if the MariaDB run still fails. The test failure there makes no sense and I cannot reproduce it locally. It is testing SKIP LOCKED support, which MariaDB supports since 10.3 or 10.6 (I've seen conflicting info online). Locally, at least, the image is:

	Database driver: org.mariadb.jdbc.Driver
	Database version: 11.7.1

@sebersole
Copy link
Member Author

Let's see if the MariaDB run still fails.

LockedRowsTests#testFindSkipLocked does still fail with MariaDB on CI. Still passes locally. So... 🤷‍♂️

I skipped it for MariaDB.

@sebersole sebersole force-pushed the lock-scope-2 branch 2 times, most recently from 03e73da to 369fa94 Compare July 21, 2025 18:02
@sebersole
Copy link
Member Author

sebersole commented Jul 22, 2025

Thinking instead of something like:

public interface LockingSupport {
	Metadata getMetadata();

	...

	interface Metadata {
		LockTimeoutType getLockTimeoutType(Timeout timeout);
		RowLockStrategy getRowLockStrategy();
		...
	}
}

public class Dialect {
	...
	LockingSupport getWriteLockingSupport();
	LockingSupport getReadLockingSupport();
}

The next steps should be to move actual handling of applying locking from the Dialect into this support contract. With that in mind, I'm thinking something like this:

public interface LockingSupport {
	/**
	 * Used as part of {@linkplain org.hibernate.sql.ast.SqlAstTranslator SQL AST translation}.
	 */
	LockingClauseStrategy getLockingClauseStrategy(QuerySpec querySpec, LockOptions lockOptions);

	/**
	 * Used with {@linkplain org.hibernate.Session#lock Session-based locking}
	 */
	LockingStrategy getLockingStrategy(EntityPersister lockable, LockMode lockMode, Locking.Scope lockScope);

	/**
	 * Used to apply locking to simple String-based SQL building, such as
	 * {@linkplain org.hibernate.Session#createNativeQuery "native" queries}.
	 */
	String applyLocksToSql(
			String sql,
			LockOptions aliasedLockOptions,
			Map<String, String[]> keyColumnNames);

	Metadata getReadLockMetadata();
	Metadata getWriteLockMetadata();

	OuterJoinLockingType getOuterJoinLockingType();
	ConnectionLockTimeoutStrategy getConnectionLockTimeoutStrategy();

	interface Metadata {
		default PessimisticLockStyle getPessimisticLockStyle() {
			return PessimisticLockStyle.CLAUSE;
		}

		default LockTimeoutType getLockTimeoutType(Timeout timeout) {
			// matches legacy definition from Dialect where only "wait forever" was supported
			if ( timeout.milliseconds() == Timeouts.WAIT_FOREVER_MILLI ) {
				return LockTimeoutType.QUERY;
			}
			return LockTimeoutType.NONE;
		}

		default RowLockStrategy getRowLockStrategy() {
			// by default, we report no support
			return RowLockStrategy.NONE;
		}
	}
}

As we cannot immediately remove methods such as Dialect#getForUpdateString, Dialect#getReadLockString, Dialect#getWriteLockString, ... we will need to (temporarily?) expose similar methods here as well.

@sebersole sebersole marked this pull request as ready for review July 22, 2025 13:05
@sebersole
Copy link
Member Author

Marking as ready for review, but keep in mind that this is just the first steps. I simply want to get this incorporated upstream asap to not have to deal with these painful rebases from upstream.

@sebersole sebersole merged commit a104469 into hibernate:main Jul 22, 2025
20 of 21 checks passed
@sebersole sebersole deleted the lock-scope-2 branch July 22, 2025 16:52
// "SELECT FOR UPDATE can only be used with a single-table SELECT statement"
return false;
public LockingSupport getLockingSupport() {
return NoLockingSupport.NO_LOCKING_SUPPORT;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return NoLockingSupport.NO_LOCKING_SUPPORT;
return LockingSupportSimple.NO_OUTER_JOIN;

public RowLockStrategy getReadRowLockStrategy() {
return RowLockStrategy.NONE;
public LockingSupport getLockingSupport() {
return NoLockingSupport.NO_LOCKING_SUPPORT;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return NoLockingSupport.NO_LOCKING_SUPPORT;
return LockingSupportSimple.NO_OUTER_JOIN;

//TODO: check this!
return false;
public LockingSupport getLockingSupport() {
return NoLockingSupport.NO_LOCKING_SUPPORT;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return NoLockingSupport.NO_LOCKING_SUPPORT;
return LockingSupportSimple.NO_OUTER_JOIN;

// https://www.firebirdsql.org/file/documentation/reference_manuals/fblangref25-en/html/fblangref25-dml-select.html#fblangref25-dml-with-lock
return false;
public LockingSupport getLockingSupport() {
return NoLockingSupport.NO_LOCKING_SUPPORT;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return NoLockingSupport.NO_LOCKING_SUPPORT;
return LockingSupportSimple.NO_OUTER_JOIN;

* Set the associated lock scope
*/
public LockOptions setScope(Locking.Scope scope) {
this.scope = scope;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we keep the immutability checking code around for the time being to avoid accidental changes to the objects that are represented by the constants in this class?

Suggested change
this.scope = scope;
if ( immutable ) {
throw new UnsupportedOperationException("immutable global instance of LockOptions");
}
this.scope = scope;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is why I wanted to remove all of the constants here. Because having even one requires we have these checks. But one thing we could do is to subclass LockOptions to error on calls to setters to use with the constants.

Comment on lines +17 to +18
final ExecutorService executorService = Executors.newSingleThreadExecutor();
try {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
final ExecutorService executorService = Executors.newSingleThreadExecutor();
try {
try ( ExecutorService executorService = Executors.newSingleThreadExecutor() ) {

Comment on lines +30 to +31
final ExecutorService executorService = Executors.newSingleThreadExecutor();
try {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
final ExecutorService executorService = Executors.newSingleThreadExecutor();
try {
try ( ExecutorService executorService = Executors.newSingleThreadExecutor() ) {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants