Fix ConcurrentModificationException in transcript handling (#102) - #103
Open
xiajon wants to merge 1 commit into
Open
Fix ConcurrentModificationException in transcript handling (#102)#103xiajon wants to merge 1 commit into
xiajon wants to merge 1 commit into
Conversation
…nnect#102) handleTranscriptItemUpdate iterates internalTranscript (indexOfFirst) on the service's Main-dispatcher coroutine while other threads mutate the same collection: the typing-indicator Timer thread (removeIf in removeTypingIndicators) and the Dispatchers.IO coroutines that back ChatSession's suspend methods. A structural modification on one thread invalidates an in-flight iteration on another, throwing ConcurrentModificationException (issue amazon-connect#102). The crash reported in amazon-connect#102 points at handleTranscriptItemUpdate, but that is only where it surfaces. transcriptDict and internalTranscript are read and written from all three threads across many methods; guarding only the reported site just relocates the crash to the next unsynchronized reader (verified: getRecentDisplayName and removeTypingIndicators both throw once handleTranscriptItemUpdate alone is guarded). Guard every access to transcriptDict and internalTranscript with a single reentrant monitor (transcriptLock). Snapshots are taken under the lock and handed to subscribers, and the lock is never held across a coroutine suspension point. Compound read-modify-write sequences (the indexOfFirst-then-insert in handleTranscriptItemUpdate) run inside one locked region, which a concurrent collection such as ConcurrentHashMap or CopyOnWriteArrayList would not fix. Adds a regression test that drives the transcript from 8 threads and fails with ConcurrentModificationException without this change.
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.
Issue
Fixes #102 —
ConcurrentModificationExceptioninChatServiceImpl.handleTranscriptItemUpdate.Root cause
transcriptDict(aLinkedHashMap) andinternalTranscript(anArrayList) are read and mutated from three threads with no synchronization:handleTranscriptItemUpdateiteratesinternalTranscriptviaindexOfFirst).removeTypingIndicatorscallsremoveIfon both collections when the typing indicator expires.Dispatchers.IO— everyChatSessionsuspend method (sendMessage,getTranscript,resendFailedMessage, …) runs itsChatServicecall on IO.A structural modification on one thread invalidates an in-flight iteration on another →
ConcurrentModificationException.Why the fix touches more than the reported function
The stack trace in #102 points at
handleTranscriptItemUpdate, but that is only where the crash surfaced. I verified empirically that guarding that one function just relocates the crash: with onlyhandleTranscriptItemUpdatesynchronized, the regression test then throws atgetRecentDisplayName; guarding that too, it throws inremoveTypingIndicators. A lock only works if every accessor takes it.The fix
transcriptLock) guards all access to the shared collections.indexOfFirst-then-insert) run inside one locked region.A concurrent collection (
ConcurrentHashMap/CopyOnWriteArrayList) is not sufficient here: it makes each individual operation thread-safe but does not make the compound index-then-mutate atomic. I confirmed this — swapping in concurrent collections makes the CME disappear but surfaces anArrayIndexOutOfBoundsExceptionand duplicate items instead.The change is deliberately mechanical: existing control flow is preserved and each hunk wraps existing statements in
synchronized(transcriptLock) { … }. No message-handling logic was altered.Testing
test_concurrentTranscriptUpdates_doNotThrowConcurrentModificationExceptiondrives the transcript from 8 threads; it fails withConcurrentModificationExceptiononmainand passes with this change../gradlew :chat-sdk:testDebugUnitTest --rerun-tasks).Follow-ups (not in this PR)
Message/MessageMetadataitems handed to subscribers; per-item field mutation while the UI reads the same instance is a separate concern.TranscriptStoretype would make "hold the lock" structurally enforced rather than a convention.