Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Guard against ConcurrentModificationException when the response processes commitActions #27587

Closed
libetl opened this issue Oct 21, 2021 · 6 comments
Assignees
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: enhancement A general enhancement
Milestone

Comments

@libetl
Copy link

libetl commented Oct 21, 2021

Affects: <Spring Framework version>v5.3.8


  • Version : spring-web v5.3.8
  • Server type : reactive
  • Java class : org.springframework.http.server.reactive.AbstractServerHttpResponse
  • Method : protected Mono<Void> doCommit(@Nullable Supplier<? extends Mono<Void>> writeAction) {

Hello,

Sometimes there is a race condition between beforeCommit statements for the same request.
This error only arises during traffic peaks

Both HttpHeaderWriterWebFilter and DefaultWebSessionManager add commitActions to the response
https://github.com/spring-projects/spring-security-reactive/blob/37749a64f782c2b2f81afb3db1b30cea3e956839/spring-security-reactive/src/main/java/org/springframework/security/web/server/header/HttpHeaderWriterWebFilter.java#L42

.doOnNext(session -> exchange.getResponse().beforeCommit(() -> save(exchange, session))));

But sometimes (fortunately it is not happening a lot) DefaultWebSessionManager adds its commitAction after the method doCommit has started.

As a result,
doCommit does Flux.concat(Flux.fromIterable(this.commitActions).map(Supplier::get)), which immediately stores an iterator over commitActions.

allActions = Flux.concat(Flux.fromIterable(this.commitActions).map(Supplier::get))

Then DefaultWebSessionManager adds its commitAction,
which means that this.commitActions has size 2, but the iterator was built for a list of 1.

When the FluxIterable subscribes, it does iterator.next, which invokes ArrayList$Itr.checkForComodification
This is where I get the ConcurrentModificationException .

To illustrate the problem, I am providing you this test class in kotlin.
This test systematically fails, but there is no race condition, it is simply a beforeCommit that takes place during another one, making the code build an iterator on a list of 1 when the list size increases to 2.

package mypackage

import org.junit.jupiter.api.Test
import org.springframework.mock.http.server.reactive.MockServerHttpResponse
import reactor.core.publisher.Mono

class AbstractServerHttpResponseTest {

    @Test
    fun test1() {
        MockServerHttpResponse()
            .apply {
                beforeCommit {
                    beforeCommit {
                        Mono.empty()
                    }
                    Mono.empty()
                }
            }
            .setComplete().block();
    }
}

If it is an app-level error, can we think of a better error handling to avoid such problems ?
Otherwise, if it is a framework-level error, I would be happy to contribute, just let me know.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Oct 21, 2021
@libetl
Copy link
Author

libetl commented Oct 21, 2021

If it is a framework problem, we could try using a different primitive instead of Flux.fromIterable, so even if there are race conditions, it can accept the entire list of commit actions.

@sbrannen sbrannen added the in: web Issues in web modules (web, webmvc, webflux, websocket) label Oct 21, 2021
@rstoyanchev
Copy link
Contributor

I realize the above snippet is just a way to cause the error, but I do wonder if the root cause is a race condition where the WebSession is used while the response is being committed, or a pre-commit action that itself uses the WebSession.

For the former, we should arguably reject a concurrent registration, in the call to beforeCommit, because the registration otherwise may or may not succeed, depending on exact timing. Generally, the response is committed fairly late, after the controller returns and the response body is written, or at the end of the filter chain, and things happen in order thanks to the reactive chain declaration, so it's a bit hard to picture a concurrent issue but not ruling it out.

For the latter, i think there is a case to allow it, but it'd be useful to confirm that's what happens. I've looked at Spring Security header writers and don't see anything trying to use the WebSession, but I could ask @rwinch or @jzheaux to comment whether Spring Security registers any preCommit action with the response that might in turn use the WebSession internally?

@rstoyanchev rstoyanchev added this to the Triage Queue milestone Oct 10, 2022
@bclozel bclozel removed this from the Triage Queue milestone Jan 17, 2023
@sbrannen
Copy link
Member

@rwinch and @jzheaux, can you please comment on the above question?

whether Spring Security registers any preCommit action with the response that might in turn use the WebSession internally?

@sbrannen sbrannen changed the title spring-web (reactive) ConcurrentModificationException when the response process commitActions ConcurrentModificationException in WebFlux when the response processes commitActions Feb 21, 2023
@rwinch
Copy link
Member

rwinch commented Feb 21, 2023

Spring Security does not register any beforeCommit action that uses the WebSession internally.

@rstoyanchev
Copy link
Contributor

rstoyanchev commented Oct 3, 2023

Unfortunately not possible to determine the root cause. I think the best we can do is to reject further commitActions from being added when the response is already committed, or ignore them, e.g. by using CopyOnWriteArrayList.

Generally, if all request handling is composed in one reactive chain, response writing should happen at the end, and there shouldn't be any competition. The fact that there is implies that there is either a component that executes asynchronously without being composed into the request handling chain (e.g. started with an explicit call to subscribe), or that the response is written early by some asynchronous component (e.g. WebFilter) integrated with flatMap into the request handling chain.

@rstoyanchev rstoyanchev added type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Oct 3, 2023
@rstoyanchev rstoyanchev added this to the 6.1.0-RC1 milestone Oct 3, 2023
@rstoyanchev rstoyanchev changed the title ConcurrentModificationException in WebFlux when the response processes commitActions Guard against ConcurrentModificationException when the response processes commitActions Oct 3, 2023
@rstoyanchev rstoyanchev modified the milestones: 6.1.0-RC1, 6.1.0-RC2 Oct 11, 2023
@rstoyanchev rstoyanchev self-assigned this Oct 11, 2023
@rstoyanchev
Copy link
Contributor

On closer look, the above test with the nested beforeCommit action no longer fails. The nested action is simply ignored. There have a been a number of improvements in FluxIterable that likely account for this. I have replaced ArrayList with CopyOnWriteArrayList in any case to avoid ConcurrentModificationException.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

7 participants