You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@seata.apache.org by "wushiyuanmaimob (via GitHub)" <gi...@apache.org> on 2026/05/26 07:42:15 UTC
[PR] [ISSUE #7993] Implement NoWait mechanism for database distributed lock [incubator-seata]
wushiyuanmaimob opened a new pull request, #8111:
URL: https://github.com/apache/incubator-seata/pull/8111
## Summary
This PR implements the NoWait mechanism for database distributed locks to address issue #7993, resolving the TODO comment at line 150 in `DataBaseDistributedLocker`.
**Problem**: Under high concurrency (500+ TPS), the current `SELECT ... FOR UPDATE` blocks threads for 100-500ms when lock contention occurs, waiting for database lock timeout.
**Solution**: Introduce `FOR UPDATE NOWAIT` support for MySQL 8.0+, PostgreSQL, and Oracle, reducing lock contention latency to <1ms by failing fast instead of blocking.
## Changes
- **SQL Dialect Support**: Added `MysqlDistributedLockSql`, `PostgresqlDistributedLockSql`, and `OracleDistributedLockSql` with NOWAIT support
- **Interface Extension**: Added `getSelectDistributeForUpdateNoWaitSql()` to `DistributedLockSql` interface with default fallback
- **Configuration Switch**: Added `store.db.distributedLockNoWaitEnabled` (default `false`) for opt-in activation
- **Exception Handling**: Extended `ignoreSQLException()` to recognize:
- MySQL 8.0+ error code 3572 (ER_LOCK_NOWAIT)
- PostgreSQL SQLState 55P03 (lock_not_available)
- Oracle error code 54 (ORA-00054)
- **Backward Compatibility**: Databases without NOWAIT support (SQL Server, MySQL 5.x, MariaDB) fall back to blocking behavior
- **Tests**: Added comprehensive unit tests for SQL generation and exception handling
## Test Plan
- [x] Core SQL factory tests pass (17 tests, 0 failures)
- [x] Spotless formatting check passes
- [x] New dialect implementations generate correct NOWAIT SQL
- [x] Default implementations fall back to blocking SQL
- [x] Exception handling correctly identifies NOWAIT error codes
- [ ] Integration tests with real databases (MySQL 8.0+, PostgreSQL, Oracle) - requires `dbCaseEnabled=true`
## Configuration
To enable NOWAIT on supported databases, add to `application.yml`:
```yaml
store:
db:
distributedLockNoWaitEnabled: true
```
**Note**: Only enable on MySQL 8.0+, PostgreSQL, or Oracle. MySQL 5.x/MariaDB do not support NOWAIT and will raise syntax errors if enabled.
## Related Issue
Resolves #7993
🤖 Generated with [Claude Code](https://claude.com/claude-code)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@seata.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@seata.apache.org
For additional commands, e-mail: notifications-help@seata.apache.org
Re: [PR] feat: Implement NoWait mechanism for database distributed lock [incubator-seata]
Posted by "Copilot (via GitHub)" <gi...@apache.org>.
Copilot commented on code in PR #8111:
URL: https://github.com/apache/incubator-seata/pull/8111#discussion_r3556746785
##########
server/src/test/java/org/apache/seata/server/storage/db/lock/DataBaseDistributedLockerTest.java:
##########
@@ -830,6 +830,125 @@ void testReleaseLock_NullConnection_HandlesGracefully() throws Exception {
verify(mockConnection, never()).close();
}
+ @Test
+ void testIgnoreSQLException_WithMySqlNoWaitErrorCode_ReturnsTrue() throws Exception {
+ // MySQL 8.0+ ER_LOCK_NOWAIT
+ SQLException exception = new SQLException(
+ "Statement aborted because lock(s) could not be acquired " + "immediately and NOWAIT is set",
+ "HY000",
+ 3572);
+
+ Method method = DataBaseDistributedLocker.class.getDeclaredMethod("ignoreSQLException", SQLException.class);
+ method.setAccessible(true);
+ boolean result = (boolean) method.invoke(locker, exception);
+
+ assertTrue(result, "ignoreSQLException should treat MySQL error code 3572 as fast-fail");
+ }
+
+ @Test
+ void testIgnoreSQLException_WithOracleNoWaitErrorCode_ReturnsTrue() throws Exception {
+ // ORA-00054 resource busy
+ SQLException exception = new SQLException(
+ "ORA-00054: resource busy and acquire with NOWAIT specified or timeout expired", "61000", 54);
+
+ Method method = DataBaseDistributedLocker.class.getDeclaredMethod("ignoreSQLException", SQLException.class);
+ method.setAccessible(true);
+ boolean result = (boolean) method.invoke(locker, exception);
+
+ assertTrue(result, "ignoreSQLException should treat Oracle error code 54 as fast-fail");
+ }
+
+ @Test
+ void testIgnoreSQLException_WithPostgresqlNoWaitSqlState_ReturnsTrue() throws Exception {
+ // PostgreSQL 55P03 lock_not_available - vendor-specific error code is not 0
+ // in real life, but we match on SQLState rather than vendor code.
+ SQLException exception = new SQLException("could not obtain lock on row in relation", "55P03", 0);
+
+ Method method = DataBaseDistributedLocker.class.getDeclaredMethod("ignoreSQLException", SQLException.class);
+ method.setAccessible(true);
+ boolean result = (boolean) method.invoke(locker, exception);
+
+ assertTrue(result, "ignoreSQLException should treat PostgreSQL SQLState 55P03 as fast-fail");
+ }
+
+ @Test
+ void testStaticInitializer_PopulatesNoWaitIgnoreSets() throws Exception {
+ Field codeField = DataBaseDistributedLocker.class.getDeclaredField("IGNORE_NOWAIT_CODE");
+ codeField.setAccessible(true);
+ @SuppressWarnings("unchecked")
+ Set<Integer> ignoreNoWaitCode = (Set<Integer>) codeField.get(null);
+
+ Field stateField = DataBaseDistributedLocker.class.getDeclaredField("IGNORE_NOWAIT_SQLSTATE");
+ stateField.setAccessible(true);
+ @SuppressWarnings("unchecked")
+ Set<String> ignoreNoWaitSqlState = (Set<String>) stateField.get(null);
+
+ assertTrue(ignoreNoWaitCode.contains(3572), "IGNORE_NOWAIT_CODE should contain 3572 (MySQL)");
+ assertTrue(ignoreNoWaitCode.contains(54), "IGNORE_NOWAIT_CODE should contain 54 (Oracle)");
+ assertTrue(
+ ignoreNoWaitSqlState.contains("55P03"), "IGNORE_NOWAIT_SQLSTATE should contain 55P03 (PostgreSQL)");
+ }
+
+ @Test
+ void testAcquireLock_NoWaitEnabled_UsesNoWaitSelectSql() throws Exception {
+ when(mockDataSource.getConnection()).thenReturn(mockConnection);
+ when(mockConnection.getAutoCommit()).thenReturn(true);
+ when(mockConnection.prepareStatement(anyString())).thenReturn(mockPreparedStatement);
+ when(mockPreparedStatement.executeQuery()).thenReturn(mockResultSet);
+ when(mockResultSet.next()).thenReturn(false);
+ when(mockPreparedStatement.executeUpdate()).thenReturn(1);
+
+ // Force the locker into nowait mode and switch its dbType so that
+ // the SPI returns a dialect with a real NOWAIT implementation.
+ setNowaitEnabled(locker, true);
+ setDbType(locker, "mysql");
+ setDistributedLockTable(locker, "distributed_lock");
+
+ DistributedLockDO lockDO = new DistributedLockDO();
+ lockDO.setLockKey("test-key");
+ lockDO.setLockValue("test-value");
+ lockDO.setExpireTime(5000L);
+
+ ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
+
+ boolean result = locker.acquireLock(lockDO);
+
+ assertTrue(result);
+ verify(mockConnection, atLeastOnce()).prepareStatement(sqlCaptor.capture());
+ boolean noWaitSqlSeen = sqlCaptor.getAllValues().stream()
+ .anyMatch(sql -> sql.toUpperCase().contains("FOR UPDATE NOWAIT"));
+ assertTrue(noWaitSqlSeen, "NOWAIT mode should issue a SELECT ... FOR UPDATE NOWAIT statement");
+ }
+
+ @Test
+ void testAcquireLock_NoWaitDisabled_UsesBlockingSelectSql() throws Exception {
+ when(mockDataSource.getConnection()).thenReturn(mockConnection);
+ when(mockConnection.getAutoCommit()).thenReturn(true);
+ when(mockConnection.prepareStatement(anyString())).thenReturn(mockPreparedStatement);
+ when(mockPreparedStatement.executeQuery()).thenReturn(mockResultSet);
+ when(mockResultSet.next()).thenReturn(false);
+ when(mockPreparedStatement.executeUpdate()).thenReturn(1);
+
+ setNowaitEnabled(locker, false);
+ setDbType(locker, "mysql");
+ setDistributedLockTable(locker, "distributed_lock");
+
Review Comment:
Same as the NOWAIT-enabled test: avoid mutating the `final` `dbType` field via reflection; it can be unreliable and makes the test dependent on JVM reflection behavior rather than the feature under test.
##########
server/src/test/java/org/apache/seata/server/storage/db/lock/DataBaseDistributedLockerTest.java:
##########
@@ -830,6 +830,125 @@ void testReleaseLock_NullConnection_HandlesGracefully() throws Exception {
verify(mockConnection, never()).close();
}
+ @Test
+ void testIgnoreSQLException_WithMySqlNoWaitErrorCode_ReturnsTrue() throws Exception {
+ // MySQL 8.0+ ER_LOCK_NOWAIT
+ SQLException exception = new SQLException(
+ "Statement aborted because lock(s) could not be acquired " + "immediately and NOWAIT is set",
+ "HY000",
+ 3572);
+
+ Method method = DataBaseDistributedLocker.class.getDeclaredMethod("ignoreSQLException", SQLException.class);
+ method.setAccessible(true);
+ boolean result = (boolean) method.invoke(locker, exception);
+
+ assertTrue(result, "ignoreSQLException should treat MySQL error code 3572 as fast-fail");
+ }
+
+ @Test
+ void testIgnoreSQLException_WithOracleNoWaitErrorCode_ReturnsTrue() throws Exception {
+ // ORA-00054 resource busy
+ SQLException exception = new SQLException(
+ "ORA-00054: resource busy and acquire with NOWAIT specified or timeout expired", "61000", 54);
+
+ Method method = DataBaseDistributedLocker.class.getDeclaredMethod("ignoreSQLException", SQLException.class);
+ method.setAccessible(true);
+ boolean result = (boolean) method.invoke(locker, exception);
+
+ assertTrue(result, "ignoreSQLException should treat Oracle error code 54 as fast-fail");
+ }
+
+ @Test
+ void testIgnoreSQLException_WithPostgresqlNoWaitSqlState_ReturnsTrue() throws Exception {
+ // PostgreSQL 55P03 lock_not_available - vendor-specific error code is not 0
+ // in real life, but we match on SQLState rather than vendor code.
+ SQLException exception = new SQLException("could not obtain lock on row in relation", "55P03", 0);
+
+ Method method = DataBaseDistributedLocker.class.getDeclaredMethod("ignoreSQLException", SQLException.class);
+ method.setAccessible(true);
+ boolean result = (boolean) method.invoke(locker, exception);
+
+ assertTrue(result, "ignoreSQLException should treat PostgreSQL SQLState 55P03 as fast-fail");
+ }
+
+ @Test
+ void testStaticInitializer_PopulatesNoWaitIgnoreSets() throws Exception {
+ Field codeField = DataBaseDistributedLocker.class.getDeclaredField("IGNORE_NOWAIT_CODE");
+ codeField.setAccessible(true);
+ @SuppressWarnings("unchecked")
+ Set<Integer> ignoreNoWaitCode = (Set<Integer>) codeField.get(null);
+
+ Field stateField = DataBaseDistributedLocker.class.getDeclaredField("IGNORE_NOWAIT_SQLSTATE");
+ stateField.setAccessible(true);
+ @SuppressWarnings("unchecked")
+ Set<String> ignoreNoWaitSqlState = (Set<String>) stateField.get(null);
+
+ assertTrue(ignoreNoWaitCode.contains(3572), "IGNORE_NOWAIT_CODE should contain 3572 (MySQL)");
+ assertTrue(ignoreNoWaitCode.contains(54), "IGNORE_NOWAIT_CODE should contain 54 (Oracle)");
+ assertTrue(
+ ignoreNoWaitSqlState.contains("55P03"), "IGNORE_NOWAIT_SQLSTATE should contain 55P03 (PostgreSQL)");
+ }
+
+ @Test
+ void testAcquireLock_NoWaitEnabled_UsesNoWaitSelectSql() throws Exception {
+ when(mockDataSource.getConnection()).thenReturn(mockConnection);
+ when(mockConnection.getAutoCommit()).thenReturn(true);
+ when(mockConnection.prepareStatement(anyString())).thenReturn(mockPreparedStatement);
+ when(mockPreparedStatement.executeQuery()).thenReturn(mockResultSet);
+ when(mockResultSet.next()).thenReturn(false);
+ when(mockPreparedStatement.executeUpdate()).thenReturn(1);
+
+ // Force the locker into nowait mode and switch its dbType so that
+ // the SPI returns a dialect with a real NOWAIT implementation.
+ setNowaitEnabled(locker, true);
+ setDbType(locker, "mysql");
+ setDistributedLockTable(locker, "distributed_lock");
Review Comment:
`dbType` in `DataBaseDistributedLocker` is a `final` field, so mutating it via reflection (`setDbType`) is brittle and may not reliably take effect across JVMs (and can even be blocked under stricter reflection settings). This test already has MySQL configured via test config, so it should avoid attempting to overwrite `dbType` and just toggle `nowaitEnabled` / table name.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@seata.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@seata.apache.org
For additional commands, e-mail: notifications-help@seata.apache.org