mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-10-08 16:19:48 +00:00
Avoid serving stale values after failed identity cache refill
`AbstractIdentityColumnMaxValueIncrementer` replaces its value cache and resets the cache index before it inserts into the sequence table. If an insert, the identity query, or the cleanup statement fails, the exception is propagated but the partially populated cache remains in place. The next call then serves values from that cache without accessing the database, so it may return `0` for slots that were never filled. This commit populates a local array and only exposes it once the entire range has been obtained and the cleanup statement has succeeded, so the next call after a failure refills the cache again. This aligns with the handling of a failed refill in `MySQLMaxValueIncrementer`. See gh-37322 Closes gh-37402 Signed-off-by: lejuho <52308082+lejuho@users.noreply.github.com>
This commit is contained in:
+6
-4
@@ -96,8 +96,7 @@ public abstract class AbstractIdentityColumnMaxValueIncrementer extends Abstract
|
||||
try {
|
||||
stmt = con.createStatement();
|
||||
DataSourceUtils.applyTransactionTimeout(stmt, getDataSource());
|
||||
this.valueCache = new long[getCacheSize()];
|
||||
this.nextValueIndex = 0;
|
||||
long[] newValues = new long[getCacheSize()];
|
||||
for (int i = 0; i < getCacheSize(); i++) {
|
||||
stmt.executeUpdate(getIncrementStatement());
|
||||
ResultSet rs = stmt.executeQuery(getIdentityStatement());
|
||||
@@ -105,13 +104,16 @@ public abstract class AbstractIdentityColumnMaxValueIncrementer extends Abstract
|
||||
if (!rs.next()) {
|
||||
throw new DataAccessResourceFailureException("Identity statement failed after inserting");
|
||||
}
|
||||
this.valueCache[i] = rs.getLong(1);
|
||||
newValues[i] = rs.getLong(1);
|
||||
}
|
||||
finally {
|
||||
JdbcUtils.closeResultSet(rs);
|
||||
}
|
||||
}
|
||||
stmt.executeUpdate(getDeleteStatement(this.valueCache));
|
||||
stmt.executeUpdate(getDeleteStatement(newValues));
|
||||
// Only expose the new values once the entire range has been obtained
|
||||
this.valueCache = newValues;
|
||||
this.nextValueIndex = 0;
|
||||
}
|
||||
catch (SQLException ex) {
|
||||
throw new DataAccessResourceFailureException("Could not increment identity", ex);
|
||||
|
||||
+110
@@ -30,6 +30,7 @@ import org.springframework.dao.DataAccessResourceFailureException;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.willReturn;
|
||||
import static org.mockito.BDDMockito.willThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
@@ -106,6 +107,115 @@ class DataFieldMaxValueIncrementerTests {
|
||||
verify(connection, times(2)).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void hsqlMaxValueIncrementerWithIncrementFailure() throws SQLException {
|
||||
given(dataSource.getConnection()).willReturn(connection);
|
||||
given(connection.createStatement()).willReturn(statement);
|
||||
willThrow(new SQLException("Cannot insert")).willReturn(1)
|
||||
.given(statement).executeUpdate("insert into myseq values(null)");
|
||||
given(statement.executeQuery("select max(identity()) from myseq")).willReturn(resultSet);
|
||||
given(resultSet.next()).willReturn(true);
|
||||
given(resultSet.getLong(1)).willReturn(1L, 2L);
|
||||
|
||||
HsqlMaxValueIncrementer incrementer = new HsqlMaxValueIncrementer();
|
||||
incrementer.setDataSource(dataSource);
|
||||
incrementer.setIncrementerName("myseq");
|
||||
incrementer.setColumnName("seq");
|
||||
incrementer.setCacheSize(2);
|
||||
incrementer.afterPropertiesSet();
|
||||
|
||||
assertThatExceptionOfType(DataAccessResourceFailureException.class)
|
||||
.isThrownBy(incrementer::nextLongValue);
|
||||
assertThat(incrementer.nextLongValue()).isEqualTo(1);
|
||||
assertThat(incrementer.nextLongValue()).isEqualTo(2);
|
||||
|
||||
verify(statement, times(3)).executeUpdate("insert into myseq values(null)");
|
||||
verify(statement).executeUpdate("delete from myseq where seq < 2");
|
||||
verify(connection, times(2)).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void hsqlMaxValueIncrementerWithDeleteFailure() throws SQLException {
|
||||
given(dataSource.getConnection()).willReturn(connection);
|
||||
given(connection.createStatement()).willReturn(statement);
|
||||
willThrow(new SQLException("Cannot delete")).willReturn(1)
|
||||
.given(statement).executeUpdate("delete from myseq where seq < 2");
|
||||
given(statement.executeQuery("select max(identity()) from myseq")).willReturn(resultSet);
|
||||
given(resultSet.next()).willReturn(true);
|
||||
given(resultSet.getLong(1)).willReturn(1L, 2L, 3L, 4L);
|
||||
|
||||
HsqlMaxValueIncrementer incrementer = new HsqlMaxValueIncrementer();
|
||||
incrementer.setDataSource(dataSource);
|
||||
incrementer.setIncrementerName("myseq");
|
||||
incrementer.setColumnName("seq");
|
||||
incrementer.setCacheSize(2);
|
||||
incrementer.afterPropertiesSet();
|
||||
|
||||
assertThatExceptionOfType(DataAccessResourceFailureException.class)
|
||||
.isThrownBy(incrementer::nextLongValue);
|
||||
assertThat(incrementer.nextLongValue()).isEqualTo(3);
|
||||
assertThat(incrementer.nextLongValue()).isEqualTo(4);
|
||||
|
||||
verify(statement, times(4)).executeUpdate("insert into myseq values(null)");
|
||||
verify(statement).executeUpdate("delete from myseq where seq < 4");
|
||||
verify(connection, times(2)).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void hsqlMaxValueIncrementerWithEmptyIdentityResult() throws SQLException {
|
||||
given(dataSource.getConnection()).willReturn(connection);
|
||||
given(connection.createStatement()).willReturn(statement);
|
||||
given(statement.executeQuery("select max(identity()) from myseq")).willReturn(resultSet);
|
||||
given(resultSet.next()).willReturn(false).willReturn(true);
|
||||
given(resultSet.getLong(1)).willReturn(1L, 2L);
|
||||
|
||||
HsqlMaxValueIncrementer incrementer = new HsqlMaxValueIncrementer();
|
||||
incrementer.setDataSource(dataSource);
|
||||
incrementer.setIncrementerName("myseq");
|
||||
incrementer.setColumnName("seq");
|
||||
incrementer.setCacheSize(2);
|
||||
incrementer.afterPropertiesSet();
|
||||
|
||||
assertThatExceptionOfType(DataAccessResourceFailureException.class)
|
||||
.isThrownBy(incrementer::nextLongValue)
|
||||
.withMessage("Identity statement failed after inserting")
|
||||
.withNoCause();
|
||||
assertThat(incrementer.nextLongValue()).isEqualTo(1);
|
||||
assertThat(incrementer.nextLongValue()).isEqualTo(2);
|
||||
|
||||
verify(statement, times(3)).executeUpdate("insert into myseq values(null)");
|
||||
verify(statement).executeUpdate("delete from myseq where seq < 2");
|
||||
verify(connection, times(2)).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void hsqlMaxValueIncrementerWithPartialIncrementFailure() throws SQLException {
|
||||
given(dataSource.getConnection()).willReturn(connection);
|
||||
given(connection.createStatement()).willReturn(statement);
|
||||
willReturn(1).willThrow(new SQLException("Cannot insert")).willReturn(1)
|
||||
.given(statement).executeUpdate("insert into myseq values(null)");
|
||||
given(statement.executeQuery("select max(identity()) from myseq")).willReturn(resultSet);
|
||||
given(resultSet.next()).willReturn(true);
|
||||
given(resultSet.getLong(1)).willReturn(1L, 2L, 3L);
|
||||
|
||||
HsqlMaxValueIncrementer incrementer = new HsqlMaxValueIncrementer();
|
||||
incrementer.setDataSource(dataSource);
|
||||
incrementer.setIncrementerName("myseq");
|
||||
incrementer.setColumnName("seq");
|
||||
incrementer.setCacheSize(2);
|
||||
incrementer.afterPropertiesSet();
|
||||
|
||||
assertThatExceptionOfType(DataAccessResourceFailureException.class)
|
||||
.isThrownBy(incrementer::nextLongValue);
|
||||
// Values obtained before the partial failure must not be served
|
||||
assertThat(incrementer.nextLongValue()).isEqualTo(2);
|
||||
assertThat(incrementer.nextLongValue()).isEqualTo(3);
|
||||
|
||||
verify(statement, times(4)).executeUpdate("insert into myseq values(null)");
|
||||
verify(statement).executeUpdate("delete from myseq where seq < 3");
|
||||
verify(connection, times(2)).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void hsqlMaxValueIncrementerWithDeleteSpecificValues() throws SQLException {
|
||||
given(dataSource.getConnection()).willReturn(connection);
|
||||
|
||||
Reference in New Issue
Block a user