Skip to content

fix(sqs): prevent ConcurrentModificationException on container stop - #1632

Merged
tomazfernandes merged 2 commits into
awspring:mainfrom
Baqirrizvidev:fix/sqs-container-stop-concurrency
Aug 8, 2026
Merged

fix(sqs): prevent ConcurrentModificationException on container stop#1632
tomazfernandes merged 2 commits into
awspring:mainfrom
Baqirrizvidev:fix/sqs-container-stop-concurrency

Conversation

@Baqirrizvidev

Copy link
Copy Markdown
Contributor

When an SQS message listener container is stopped under heavy load, AbstractPollingMessageSource.stop() iterates over this.pollingFutures to cancel active polls. Each future has a whenComplete callback registered in managePollingFuture that removes itself from the pollingFutures list.

Cancelling the future inside the iteration loop triggers the callback synchronously/concurrently, modifying the list while it is being iterated, which throws a ConcurrentModificationException and halts proper container shutdown.

This commit resolves the issue by taking a snapshot copy (new ArrayList<>(this.pollingFutures)) of the collection before iterating and cancelling, decoupling iteration from list modifications.

Fixes #1594

@github-actions github-actions Bot added the component: sqs SQS integration related issue label May 29, 2026
@tomazfernandes

Copy link
Copy Markdown
Contributor

Thanks for the PR @Baqirrizvidev.

As explained in the original issue, the new ArrayList() approach has the same problem of throwing ConcurrentModificationException when modifications happen while the new list is being built, and further synchronization is required.

Let me know if you'd like to amend the PR.

@tomazfernandes tomazfernandes added the status: waiting-for-feedback Waiting for feedback from issuer label Jul 18, 2026
@KimDoubleB

Copy link
Copy Markdown

I'm hitting this one too, so I've been waiting on this PR.

I had a look at the concern raised about the snapshot:

the new ArrayList() approach has the same problem of throwing ConcurrentModificationException when modifications happen while the new list is being built

I might be missing something, but I don't think that applies to this particular field. pollingFutures is a Collections.synchronizedCollection, and new ArrayList<>(Collection) copies through c.toArray(), which the synchronized wrapper already runs under the mutex — so the copy itself looks atomic to me as it stands.

The way I read ConcurrentModificationException, it isn't really a cross-thread race either: cancel(true) completes the future, the whenComplete callback registered in managePollingFuture then runs inline on that same thread, and it removes from the very collection being iterated. If that's right, iterating a private copy is the part that actually fixes it, and the mutex isn't the load-bearing bit here.

Not arguing against the explicit synchronized block — it documents the intent and would survive the field's type changing later. I just wondered whether the one-liner might already be enough on its own, in case that's what's holding this up.
Happy to be told I've got this wrong.

@tomazfernandes

Copy link
Copy Markdown
Contributor

@KimDoubleB I recall trying the new ArrayList() approach and getting ConcurrentModificationException, but it's been a while. I'll take another look.

Baqirrizvidev and others added 2 commits August 8, 2026 13:54
When an SQS message listener container is stopped, AbstractPollingMessageSource.stop() iterates over this.pollingFutures to cancel active polls. Each future has a whenComplete callback registered in managePollingFuture that synchronously/concurrently removes itself from the pollingFutures collection. Iterating over the collection while elements are being removed throws a ConcurrentModificationException.

This commit prevents the exception by taking a shallow snapshot copy of the pollingFutures collection before iterating and cancelling the futures.

Fixes awspring#1594
… container stop

Cancelling an in-flight polling future during stop() runs the whenComplete
callback registered in managePollingFuture inline on the stopping thread,
removing the future from pollingFutures while the collection is iterated.
The synchronizedCollection mutex is reentrant, so synchronization alone
cannot prevent this; iterating a snapshot copy does.

Regression introduced in awspring#1455 by replacing thenRun with whenComplete,
first released in 3.4.1 and 4.0.0.
@tomazfernandes
tomazfernandes force-pushed the fix/sqs-container-stop-concurrency branch from 533226e to 7209e91 Compare August 8, 2026 17:24
@tomazfernandes

Copy link
Copy Markdown
Contributor

Thanks @Baqirrizvidev for the fix, and @KimDoubleB for the analysis, which I can confirm is correct: the snapshot copy is atomic, since the synchronized wrapper runs toArray() under its mutex, and the exception comes from the whenComplete callback removing the future inline on the stopping thread during cancellation.

I've rebased the branch on current main, dropped the unrelated ConfigurationChangeDetector commit, and added a regression test. I'll merge the PR as soon as the tests return green.

@tomazfernandes
tomazfernandes merged commit 131e941 into awspring:main Aug 8, 2026
6 checks passed
tomazfernandes added a commit that referenced this pull request Aug 8, 2026
…1632)

* fix(sqs): prevent ConcurrentModificationException on container stop

When an SQS message listener container is stopped, AbstractPollingMessageSource.stop() iterates over this.pollingFutures to cancel active polls. Each future has a whenComplete callback registered in managePollingFuture that synchronously/concurrently removes itself from the pollingFutures collection. Iterating over the collection while elements are being removed throws a ConcurrentModificationException.

This commit prevents the exception by taking a shallow snapshot copy of the pollingFutures collection before iterating and cancelling the futures.

Fixes #1594

* test(sqs): add regression test for ConcurrentModificationException on container stop

Cancelling an in-flight polling future during stop() runs the whenComplete
callback registered in managePollingFuture inline on the stopping thread,
removing the future from pollingFutures while the collection is iterated.
The synchronizedCollection mutex is reentrant, so synchronization alone
cannot prevent this; iterating a snapshot copy does.

Regression introduced in #1455 by replacing thenRun with whenComplete,
first released in 3.4.1 and 4.0.0.

---------

Co-authored-by: Tomaz Fernandes <tomaz.fernandes.se@gmail.com>
(cherry picked from commit 131e941)
@tomazfernandes tomazfernandes removed the status: waiting-for-feedback Waiting for feedback from issuer label Aug 8, 2026
tomazfernandes added a commit to tomazfernandes/spring-cloud-aws that referenced this pull request Aug 8, 2026
…wspring#1632)

* fix(sqs): prevent ConcurrentModificationException on container stop

When an SQS message listener container is stopped, AbstractPollingMessageSource.stop() iterates over this.pollingFutures to cancel active polls. Each future has a whenComplete callback registered in managePollingFuture that synchronously/concurrently removes itself from the pollingFutures collection. Iterating over the collection while elements are being removed throws a ConcurrentModificationException.

This commit prevents the exception by taking a shallow snapshot copy of the pollingFutures collection before iterating and cancelling the futures.

Fixes awspring#1594

* test(sqs): add regression test for ConcurrentModificationException on container stop

Cancelling an in-flight polling future during stop() runs the whenComplete
callback registered in managePollingFuture inline on the stopping thread,
removing the future from pollingFutures while the collection is iterated.
The synchronizedCollection mutex is reentrant, so synchronization alone
cannot prevent this; iterating a snapshot copy does.

Regression introduced in awspring#1455 by replacing thenRun with whenComplete,
first released in 3.4.1 and 4.0.0.

---------

Co-authored-by: Tomaz Fernandes <tomaz.fernandes.se@gmail.com>
(cherry picked from commit 131e941)
tomazfernandes added a commit that referenced this pull request Aug 8, 2026
…1632)

* fix(sqs): prevent ConcurrentModificationException on container stop

When an SQS message listener container is stopped, AbstractPollingMessageSource.stop() iterates over this.pollingFutures to cancel active polls. Each future has a whenComplete callback registered in managePollingFuture that synchronously/concurrently removes itself from the pollingFutures collection. Iterating over the collection while elements are being removed throws a ConcurrentModificationException.

This commit prevents the exception by taking a shallow snapshot copy of the pollingFutures collection before iterating and cancelling the futures.

Fixes #1594

* test(sqs): add regression test for ConcurrentModificationException on container stop

Cancelling an in-flight polling future during stop() runs the whenComplete
callback registered in managePollingFuture inline on the stopping thread,
removing the future from pollingFutures while the collection is iterated.
The synchronizedCollection mutex is reentrant, so synchronization alone
cannot prevent this; iterating a snapshot copy does.

Regression introduced in #1455 by replacing thenRun with whenComplete,
first released in 3.4.1 and 4.0.0.

---------

Co-authored-by: Tomaz Fernandes <tomaz.fernandes.se@gmail.com>
(cherry picked from commit 131e941)
@tomazfernandes

Copy link
Copy Markdown
Contributor

Backported to 3.4.x in #1671

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component: sqs SQS integration related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Issue: Concurrent ModificationException when running multiple containers and restarting one of them

3 participants