Skip to content

Commit 9852e1e

Browse files
committed
HHH-19551 - Address deficiencies in pessimistic locking
1 parent 06e5bec commit 9852e1e

File tree

6 files changed

+226
-95
lines changed

6 files changed

+226
-95
lines changed

hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55
package org.hibernate.dialect;
66

7+
import java.sql.Connection;
78
import java.sql.DatabaseMetaData;
89
import java.sql.SQLException;
910
import java.sql.Types;
@@ -313,6 +314,22 @@ public LockTimeoutStyle getLockTimeoutStyle(Timeout timeout) {
313314
return LockTimeoutStyle.QUERY;
314315
}
315316

317+
@Override
318+
public Timeout getLockTimeout(Connection connection, SessionFactoryImplementor factory) {
319+
// technically, Hibernate should never use these due to MariaDB supporting LockTimeoutStyle.QUERY;
320+
// but it supports the same operations as MySQL so nothing hurt
321+
// see https://mariadb.com/docs/server/server-usage/storage-engines/innodb/innodb-system-variables#innodb_lock_wait_timeout
322+
return super.getLockTimeout( connection, factory );
323+
}
324+
325+
@Override
326+
public void setLockTimeout(Timeout timeout, Connection connection, SessionFactoryImplementor factory) {
327+
// technically, Hibernate should never use these due to MariaDB supporting LockTimeoutStyle.QUERY;
328+
// but it supports the same operations as MySQL so nothing hurt
329+
// see https://mariadb.com/docs/server/server-usage/storage-engines/innodb/innodb-system-variables#innodb_lock_wait_timeout
330+
super.setLockTimeout( timeout, connection, factory );
331+
}
332+
316333
/**
317334
* @return {@code true} for 10.5 and above because Maria supports
318335
* {@code insert ... returning} even though MySQL does not

hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import jakarta.persistence.TemporalType;
88
import jakarta.persistence.Timeout;
9+
import org.hibernate.HibernateException;
910
import org.hibernate.Length;
1011
import org.hibernate.QueryTimeoutException;
1112
import org.hibernate.Timeouts;
@@ -19,6 +20,8 @@
1920
import org.hibernate.dialect.function.CommonFunctionFactory;
2021
import org.hibernate.dialect.identity.IdentityColumnSupport;
2122
import org.hibernate.dialect.identity.MySQLIdentityColumnSupport;
23+
import org.hibernate.dialect.lock.internal.Helper;
24+
import org.hibernate.dialect.lock.spi.ConnectionLockTimeoutType;
2225
import org.hibernate.dialect.lock.spi.LockTimeoutStyle;
2326
import org.hibernate.dialect.lock.spi.OuterJoinLockingType;
2427
import org.hibernate.dialect.pagination.LimitHandler;
@@ -82,6 +85,7 @@
8285
import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry;
8386

8487
import java.sql.CallableStatement;
88+
import java.sql.Connection;
8589
import java.sql.DatabaseMetaData;
8690
import java.sql.ResultSet;
8791
import java.sql.SQLException;
@@ -1236,13 +1240,60 @@ public boolean supportsSubqueryOnMutatingTable() {
12361240
return false;
12371241
}
12381242

1243+
@Override
1244+
public ConnectionLockTimeoutType getConnectionLockTimeoutType() {
1245+
return ConnectionLockTimeoutType.EXTENDED;
1246+
}
1247+
1248+
public static final String READ_LOCK_TIMEOUT = "SELECT @@SESSION.innodb_lock_wait_timeout";
1249+
public static final String SET_LOCK_TIMEOUT = "SET @@SESSION.innodb_lock_wait_timeout = %s";
1250+
1251+
@Override
1252+
public Timeout getLockTimeout(Connection connection, SessionFactoryImplementor factory) {
1253+
return Helper.getLockTimeout(
1254+
READ_LOCK_TIMEOUT,
1255+
(resultSet) -> {
1256+
// see https://dev.mysql.com/doc/refman/8.4/en/innodb-parameters.html#sysvar_innodb_lock_wait_timeout
1257+
final int millis = resultSet.getInt( 1 );
1258+
return switch ( millis ) {
1259+
case 0 -> Timeouts.NO_WAIT;
1260+
case 100000000 -> Timeouts.WAIT_FOREVER;
1261+
default -> Timeout.milliseconds( millis );
1262+
};
1263+
},
1264+
connection,
1265+
factory
1266+
);
1267+
}
1268+
1269+
@Override
1270+
public void setLockTimeout(Timeout timeout, Connection connection, SessionFactoryImplementor factory) {
1271+
Helper.setLockTimeout(
1272+
timeout,
1273+
(t) -> {
1274+
// see https://dev.mysql.com/doc/refman/8.4/en/innodb-parameters.html#sysvar_innodb_lock_wait_timeout
1275+
final int milliseconds = timeout.milliseconds();
1276+
if ( milliseconds == Timeouts.SKIP_LOCKED_MILLI ) {
1277+
throw new HibernateException( "Connection lock-timeout does not accept skip-locked" );
1278+
}
1279+
if ( milliseconds == Timeouts.WAIT_FOREVER_MILLI ) {
1280+
return 100000000;
1281+
}
1282+
return milliseconds;
1283+
},
1284+
SET_LOCK_TIMEOUT,
1285+
connection,
1286+
factory
1287+
);
1288+
}
1289+
12391290
@Override
12401291
public LockTimeoutStyle getLockTimeoutStyle(Timeout timeout) {
1241-
// yes, we do handle "lock timeout" conditions in the exception conversion delegate,
1242-
// but that's a hardcoded lock timeout period across the whole entire MySQL database.
1243-
// MySQL does not support specifying lock timeouts as part of the SQL statement, which is really
1244-
// what this meta method is asking.
1245-
return LockTimeoutStyle.NONE;
1292+
if ( timeout.milliseconds() == Timeouts.SKIP_LOCKED_MILLI ) {
1293+
return LockTimeoutStyle.UNSUPPORTED;
1294+
}
1295+
1296+
return LockTimeoutStyle.CONNECTION;
12461297
}
12471298

12481299
@Override

hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java

Lines changed: 43 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.hibernate.dialect.function.PostgreSQLTruncRoundFunction;
2525
import org.hibernate.dialect.identity.IdentityColumnSupport;
2626
import org.hibernate.dialect.identity.PostgreSQLIdentityColumnSupport;
27+
import org.hibernate.dialect.lock.internal.Helper;
2728
import org.hibernate.dialect.lock.spi.ConnectionLockTimeoutType;
2829
import org.hibernate.dialect.lock.spi.LockTimeoutStyle;
2930
import org.hibernate.dialect.lock.spi.OuterJoinLockingType;
@@ -49,7 +50,6 @@
4950
import org.hibernate.engine.jdbc.env.spi.IdentifierHelper;
5051
import org.hibernate.engine.jdbc.env.spi.IdentifierHelperBuilder;
5152
import org.hibernate.engine.jdbc.env.spi.NameQualifierSupport;
52-
import org.hibernate.engine.jdbc.spi.SqlExceptionHelper;
5353
import org.hibernate.engine.spi.SessionFactoryImplementor;
5454
import org.hibernate.exception.LockAcquisitionException;
5555
import org.hibernate.exception.LockTimeoutException;
@@ -1332,70 +1332,59 @@ public LockTimeoutStyle getLockTimeoutStyle(Timeout timeout) {
13321332
};
13331333
}
13341334

1335+
@Override
13351336
public ConnectionLockTimeoutType getConnectionLockTimeoutType() {
13361337
return ConnectionLockTimeoutType.SUPPORTED;
13371338
}
13381339

13391340
public static final String READ_LOCK_TIMEOUT = "select current_setting('lock_timeout', true)";
1340-
public static final String SET_LOCK_TIMEOUT = "set local lock_timeout = ";
1341+
public static final String SET_LOCK_TIMEOUT = "set local lock_timeout = %s";
13411342

1342-
/**
1343-
* Read the lock timeout (if one) associated with the JDBC connection, in milliseconds.
1344-
*/
1343+
@Override
13451344
public Timeout getLockTimeout(Connection connection, SessionFactoryImplementor factory) {
1346-
try (final java.sql.Statement statement = connection.createStatement()) {
1347-
factory.getJdbcServices().getSqlStatementLogger().logStatement( READ_LOCK_TIMEOUT );
1348-
try (final ResultSet result = statement.executeQuery( READ_LOCK_TIMEOUT ) ) {
1349-
if ( !result.next() ) {
1350-
throw new HibernateException( "Unable to query JDBC Connection for current lock-timeout (no result)" );
1351-
}
1352-
// even though lock_timeout is "in milliseconds", `current_setting`
1353-
// returns us a String form which unfortunately varies depending on
1354-
// the actual value:
1355-
// * for zero (no timeout), "0" is returned
1356-
// * for non-zero, `{timeout-in-seconds}s` is returned (e.g. "4s")
1357-
// so we need to "parse" that form here
1358-
final String value = result.getString( 1 );
1359-
if ( "0".equals( value ) ) {
1360-
return Timeouts.WAIT_FOREVER;
1361-
}
1362-
assert value.endsWith( "s" );
1363-
final int secondsValue = Integer.parseInt( value.substring( 0, value.length() - 1 ) );
1364-
return Timeout.seconds( secondsValue );
1365-
}
1366-
}
1367-
catch (SQLException sqle) {
1368-
final SqlExceptionHelper sqlExceptionHelper = factory.getJdbcServices().getJdbcEnvironment().getSqlExceptionHelper();
1369-
throw sqlExceptionHelper.convert( sqle, "Unable to query JDBC Connection for current lock-timeout" );
1370-
}
1345+
return Helper.getLockTimeout(
1346+
READ_LOCK_TIMEOUT,
1347+
(resultSet) -> {
1348+
// even though lock_timeout is "in milliseconds", `current_setting`
1349+
// returns a String form which unfortunately varies depending on
1350+
// the actual value:
1351+
// * for zero (no timeout), "0" is returned
1352+
// * for non-zero, `{timeout-in-seconds}s` is returned (e.g. "4s")
1353+
// so we need to "parse" that form here
1354+
final String value = resultSet.getString( 1 );
1355+
if ( "0".equals( value ) ) {
1356+
return Timeouts.WAIT_FOREVER;
1357+
}
1358+
assert value.endsWith( "s" );
1359+
final int secondsValue = Integer.parseInt( value.substring( 0, value.length() - 1 ) );
1360+
return Timeout.seconds( secondsValue );
1361+
},
1362+
connection,
1363+
factory
1364+
);
13711365
}
13721366

1373-
/**
1374-
* Set the lock timeout associated with the JDBC connection (if supported), in milliseconds.
1375-
*/
1367+
@Override
13761368
public void setLockTimeout(Timeout timeout, Connection connection, SessionFactoryImplementor factory) {
1377-
final int milliseconds = timeout.milliseconds();
1378-
if ( milliseconds == Timeouts.SKIP_LOCKED_MILLI ) {
1379-
throw new HibernateException( "Connection lock-timeout does not accept skip-locked" );
1380-
}
1381-
1382-
if ( milliseconds == Timeouts.NO_WAIT_MILLI ) {
1383-
throw new HibernateException( "Connection lock-timeout does not accept no-wait" );
1384-
}
1385-
1386-
final int millisecondsToUse = milliseconds == Timeouts.WAIT_FOREVER_MILLI
1387-
? 0
1388-
: milliseconds;
1369+
Helper.setLockTimeout(
1370+
timeout,
1371+
(t) -> {
1372+
final int milliseconds = timeout.milliseconds();
1373+
if ( milliseconds == Timeouts.SKIP_LOCKED_MILLI ) {
1374+
throw new HibernateException( "Connection lock-timeout does not accept skip-locked" );
1375+
}
13891376

1390-
final String qry = SET_LOCK_TIMEOUT + millisecondsToUse;
1391-
try (final java.sql.Statement stmnt = connection.createStatement()) {
1392-
factory.getJdbcServices().getSqlStatementLogger().logStatement( qry );
1393-
stmnt.execute( qry );
1394-
}
1395-
catch (SQLException sqle) {
1396-
final SqlExceptionHelper sqlExceptionHelper = factory.getJdbcServices().getJdbcEnvironment().getSqlExceptionHelper();
1397-
throw sqlExceptionHelper.convert( sqle, "Unable to set lock-timeout setting on JDBC connection" );
1398-
}
1377+
if ( milliseconds == Timeouts.NO_WAIT_MILLI ) {
1378+
throw new HibernateException( "Connection lock-timeout does not accept no-wait" );
1379+
}
1380+
return milliseconds == Timeouts.WAIT_FOREVER_MILLI
1381+
? 0
1382+
: milliseconds;
1383+
},
1384+
SET_LOCK_TIMEOUT,
1385+
connection,
1386+
factory
1387+
);
13991388
}
14001389

14011390
private String withTimeout(String lockString, int timeout) {

hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java

Lines changed: 32 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.hibernate.dialect.function.SqlServerConvertTruncFunction;
3838
import org.hibernate.dialect.identity.IdentityColumnSupport;
3939
import org.hibernate.dialect.identity.SQLServerIdentityColumnSupport;
40+
import org.hibernate.dialect.lock.internal.Helper;
4041
import org.hibernate.dialect.lock.spi.ConnectionLockTimeoutType;
4142
import org.hibernate.dialect.pagination.LimitHandler;
4243
import org.hibernate.dialect.pagination.SQLServer2012LimitHandler;
@@ -55,7 +56,6 @@
5556
import org.hibernate.engine.jdbc.env.spi.IdentifierHelper;
5657
import org.hibernate.engine.jdbc.env.spi.IdentifierHelperBuilder;
5758
import org.hibernate.engine.jdbc.env.spi.NameQualifierSupport;
58-
import org.hibernate.engine.jdbc.spi.SqlExceptionHelper;
5959
import org.hibernate.engine.spi.SessionFactoryImplementor;
6060
import org.hibernate.exception.ConstraintViolationException;
6161
import org.hibernate.exception.ConstraintViolationException.ConstraintKind;
@@ -662,48 +662,45 @@ public String appendLockHint(LockOptions lockOptions, String tableName) {
662662
};
663663
}
664664

665+
@Override
665666
public ConnectionLockTimeoutType getConnectionLockTimeoutType() {
666667
return ConnectionLockTimeoutType.EXTENDED;
667668
}
668669

669-
@Override
670-
public void setLockTimeout(Timeout timeout, Connection connection, SessionFactoryImplementor factory) {
671-
final int milliseconds = timeout.milliseconds();
672-
if ( milliseconds == Timeouts.SKIP_LOCKED_MILLI ) {
673-
throw new HibernateException( "Connection lock-timeout does not accept skip-locked" );
674-
}
670+
public static final String READ_LOCK_TIMEOUT = "select @@LOCK_TIMEOUT";
675671

676-
final String sql = "set lock_timeout " + milliseconds;
677-
try (final java.sql.Statement statement = connection.createStatement()) {
678-
factory.getJdbcServices().getSqlStatementLogger().logStatement( sql );
679-
statement.execute( sql );
680-
}
681-
catch (SQLException sqle) {
682-
final SqlExceptionHelper sqlExceptionHelper = factory.getJdbcServices().getJdbcEnvironment().getSqlExceptionHelper();
683-
throw sqlExceptionHelper.convert( sqle, "Unable to set lock_timeout setting on JDBC connection" );
684-
}
672+
@Override
673+
public Timeout getLockTimeout(Connection connection, SessionFactoryImplementor factory) {
674+
return Helper.getLockTimeout(
675+
READ_LOCK_TIMEOUT,
676+
(resultSet) -> {
677+
final int timeoutInMilliseconds = resultSet.getInt( 1 );
678+
return switch ( timeoutInMilliseconds ) {
679+
case -1 -> Timeouts.WAIT_FOREVER;
680+
case 0 -> Timeouts.NO_WAIT;
681+
default -> Timeout.milliseconds( timeoutInMilliseconds );
682+
};
683+
},
684+
connection,
685+
factory
686+
);
685687
}
686688

687689
@Override
688-
public Timeout getLockTimeout(Connection connection, SessionFactoryImplementor factory) {
689-
try (final java.sql.Statement statement = connection.createStatement()) {
690-
final String sql = "select @@LOCK_TIMEOUT";
691-
factory.getJdbcServices().getSqlStatementLogger().logStatement( sql );
692-
final ResultSet results = statement.executeQuery( sql );
693-
if ( !results.next() ) {
694-
throw new HibernateException( "Unable to query JDBC Connection for current @@LOCK_TIMEOUT setting (no result)" );
695-
}
696-
final int timeoutInMilliseconds = results.getInt( 1 );
697-
return switch ( timeoutInMilliseconds ) {
698-
case -1 -> Timeouts.WAIT_FOREVER;
699-
case 0 -> Timeouts.NO_WAIT;
700-
default -> Timeout.milliseconds( timeoutInMilliseconds );
701-
};
702-
}
703-
catch (SQLException sqle) {
704-
final SqlExceptionHelper sqlExceptionHelper = factory.getJdbcServices().getJdbcEnvironment().getSqlExceptionHelper();
705-
throw sqlExceptionHelper.convert( sqle, "Unable to query JDBC Connection for current @@LOCK_TIMEOUT setting" );
706-
}
690+
public void setLockTimeout(Timeout timeout, Connection connection, SessionFactoryImplementor factory) {
691+
Helper.setLockTimeout(
692+
timeout,
693+
(t) -> {
694+
final int milliseconds = timeout.milliseconds();
695+
if ( milliseconds == Timeouts.SKIP_LOCKED_MILLI ) {
696+
throw new HibernateException( "Connection lock-timeout does not accept skip-locked" );
697+
}
698+
return milliseconds;
699+
},
700+
"set lock_timeout %s",
701+
connection,
702+
factory
703+
);
707704
}
708705

709706
/**

0 commit comments

Comments
 (0)