Fix cached ID reuse after commit failure in MySQLMaxValueIncrementer - #37322
Open
cookie-meringue wants to merge 1 commit into
Open
cookie-meringue wants to merge 1 commit into
cookie-meringue wants to merge 1 commit into
Conversation
Invalidate the cached ID range when commit or auto-commit restoration fails so subsequent calls obtain a new range from the database. Signed-off-by: cookie-meringue <daehyeon3351@gmail.com>
Contributor
Author
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
In
MySQLMaxValueIncrementer, if commit fails withuseNewConnectionset totrueandcacheSizegreater than 1, an ID range that was not reserved in the database can remain in the cache.Even if the sequence increment is rolled back, subsequent calls can still use the cached range. If the database allocates the same range again, duplicate IDs can result.
Normal ID allocation
The incrementer obtains IDs by increasing a value in a sequence table. With
cacheSizegreater than 1, it reserves a range at once and serves the remaining IDs without querying the database.For example, with a sequence value of
98andcacheSizeset to 2, it updates the sequence to100. Once that update is committed, the range99–100is reserved.The first ID request returns
99, and the next returns cached ID100. Since the database now stores100, the next range obtained from the database starts at101.Failure flow
When replenishing the cache, the current implementation performs these steps:
SELECT LAST_INSERT_ID()and assign it tomaxId.nextIdtomaxId - getCacheSize() + 1.nextIdto the caller.The problem: the cache fields are changed before commit succeeds.
If commit throws an exception, ID generation fails without returning an ID, but the
nextIdandmaxIdvalues assigned before commit remain unchanged.The next ID request only checks whether
maxId == nextId. If the values are equal, it increases the sequence value in the database to obtain more IDs.If the values differ, it increments
nextIdand returns it without accessing the database.For the example above, suppose the connection is terminated before commit and the sequence increment is rolled back. The stored sequence value returns to
98, while the fields remainnextId=99andmaxId=100.98to100, reads100, and setsnextId=99,maxId=100. The connection is then terminated, rolling back the update and causing commit to fail. The database value returns to98, but the fields remain unchanged.nextId=99andmaxId=100differ, incrementsnextIdto100without accessing the database. The database value is still98.100. Inserting it as a primary key succeeds.nextIdandmaxIdare now both100, increases the database value from98to100again, reads it, and setsnextId=99,maxId=100. This time, commit succeeds.99. Inserting it succeeds.nextId=99andmaxId=100differ, incrementsnextIdto100without accessing the database.100again. The INSERT fails because request 2 already inserted that ID.The INSERT using ID
100from request 2 succeeds. The duplicate-key error occurs when request 4 returns100again and attempts another INSERT.A commit exception does not always mean that the sequence increment was rolled back. The example above describes a case where it was rolled back, as verified in the MySQL test below.
This issue does not occur with the default
cacheSizeof 1. SincenextIdequalsmaxIdwhen commit fails, the next ID request obtains IDs from the database again.Fix
Set
nextIdtomaxIdin the existing exception handler for commit and auto-commit restoration:Setting the two fields to the same value makes the next ID request satisfy
maxId == nextId. The incrementer then increases the sequence value in the database to obtain IDs again.Reproduction against MySQL
Verified with MySQL 9.1.0 and
cacheSize=2.98.100and read that value.KILL CONNECTIONfor the test connection, then invoke the real JDBCcommit().CommunicationsExceptionand confirm that the stored sequence value is98from a separate connection.100, 99, 100Duplicate entry '100'99, 100, 101Exception stack traces