Make Neo4jChatMemoryRepository.saveAll atomic - #6798
Open
dlwldn30 wants to merge 1 commit into
Open
Conversation
saveAll() removed the existing conversation through deleteByConversationId(), which opens its own session and commits, and only then opened a second session to write the replacement messages. If that write failed, the conversation was left with neither its previous messages nor the new ones. Run the delete and the inserts in a single write transaction by extracting the delete statements into a method that takes the runner to execute them on. deleteByConversationId() keeps its own transaction and reuses that method. JdbcChatMemoryRepository already wraps the equivalent delete-and-insert flow in a TransactionTemplate. The same defect on the MongoDB repository is tracked in spring-projects#6770. Signed-off-by: dlwldn30 <dlwldn30@naver.com>
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.
Problem
Neo4jChatMemoryRepository.saveAll()replaces a conversation by callingdeleteByConversationId()first. That method opens its own session, runs thetwo delete statements in a transaction and commits. Only then does
saveAll()open a second session to write the replacement messages.
If that write fails, the delete is already committed, so the conversation is
left with neither its previous messages nor the new ones.
JdbcChatMemoryRepositoryalready runs the equivalent delete-and-insert flowinside a
TransactionTemplate. The same defect on the MongoDB repository istracked in #6770.
Solution
Extract the delete statements into
deleteConversation(SimpleQueryRunner, String)and run them inside the samewrite transaction as the inserts.
deleteByConversationId()keeps its owntransaction and reuses that method, so its behaviour is unchanged.
Both statements are
MATCH ... DETACH DELETE, and the inserts derivemsg.idxfrom a count taken inside the transaction, so the combined unit ofwork stays safe under the driver's retry behaviour.
Testing
Neo4jChatMemoryRepositoryIT.saveAllKeepsTheExistingConversationWhenAWriteFailssaves two messages, then attempts a replacement whose second message cannot be
serialized — the same
Optional-typed metadata that the existingsaveAssistantMessageWithOptionalMetadataFailsrelies on.Reverting only the production change and keeping the test shows the
conversation is wiped:
With the fix the two original messages survive the failed write.
32 tests run, 0 failures, 0 errors, 0 skips. The downstream
spring-ai-autoconfigure-model-chat-memory-repository-neo4jmodule passes too(3 tests).
spring-javaformat:applyandcheckstyle:checkreported noviolations.
Follow-up (not in this PR)
deleteByConversationId()is the only method here still using an unmanagedtransaction, so unlike the others it does not get the driver's retry behaviour
on transient failures. That is unrelated to atomicity, so I left it out — happy
to follow up separately if you would like it aligned with the rest of the class.
Contributed on behalf of Goatshave.