Spring-session: ReactiveRedisOperationsSessionRepository causes IllegalStateException on logout

Created on 1 Apr 2019  Â·  12Comments  Â·  Source: spring-projects/spring-session

While working on a Spring WebFlux application secured via OAuth2 (Spring Boot 2, Spring Cloud Gateway, Spring Security, Spring Session Redis) I observed users receiving a 500 status code caused by an IllegalStateEx raised by ReactiveRedisOperationsSessionRepository#save.

Imho for spring-data-redis-2.2.0.M2 the issue is that ReactiveRedisOperationsSessionRepository#saveDelta is called two times (WebSessionServerCsrfTokenRepository and WebSessionServerSecurityContextRepository) and both receive a reference to this.delta with entries at creation time, on execution time when the first finishes this.delta is cleared and the second call fails due to an empty map.

Simple sample:

https://github.com/AndreasKl/issue-in-spring-session-redis/tree/master

When I bump the version to Spring Boot 2.1.3 a slightly different behaviour with an additional exception occurs.

Steps to reproduce:

  • Start the application
  • Navigate to http://localhost:8080/login
  • Login with user: user pw: 123456
  • Navigate to http://localhost:8080/logout
  • Press "Log Out"
  • Observe the error page and the following stacktrace in your log:
java.lang.IllegalStateException: Session was invalidated
    at org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository.lambda$save$2(ReactiveRedisOperationsSessionRepository.java:159) ~[spring-session-data-redis-2.1.3.RELEASE.jar:2.1.3.RELEASE]
    at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:118) ~[reactor-core-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onNext(FluxOnAssembly.java:353) ~[reactor-core-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onNext(FluxOnAssembly.java:353) ~[reactor-core-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:76) ~[reactor-core-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onNext(FluxOnAssembly.java:353) ~[reactor-core-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at reactor.core.publisher.FluxDoFinally$DoFinallySubscriber.onNext(FluxDoFinally.java:123) ~[reactor-core-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onNext(FluxOnAssembly.java:353) ~[reactor-core-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:121) ~[reactor-core-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onNext(FluxOnAssembly.java:353) ~[reactor-core-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:76) ~[reactor-core-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onNext(FluxOnAssembly.java:353) ~[reactor-core-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:73) ~[reactor-core-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onNext(FluxOnAssembly.java:353) ~[reactor-core-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at reactor.core.publisher.MonoFlatMapMany$FlatMapManyInner.onNext(MonoFlatMapMany.java:238) ~[reactor-core-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onNext(FluxOnAssembly.java:353) ~[reactor-core-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.innerNext(FluxConcatMap.java:275) ~[reactor-core-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at reactor.core.publisher.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:849) ~[reactor-core-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onNext(FluxOnAssembly.java:353) ~[reactor-core-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:121) ~[reactor-core-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onNext(FluxOnAssembly.java:353) ~[reactor-core-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:121) ~[reactor-core-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onNext(FluxOnAssembly.java:353) ~[reactor-core-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:76) ~[reactor-core-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at io.lettuce.core.RedisPublisher$RedisSubscription.onNext(RedisPublisher.java:270) ~[lettuce-core-5.1.3.RELEASE.jar:na]
    at io.lettuce.core.RedisPublisher$SubscriptionCommand.complete(RedisPublisher.java:754) ~[lettuce-core-5.1.3.RELEASE.jar:na]
    at io.lettuce.core.protocol.CommandHandler.complete(CommandHandler.java:646) ~[lettuce-core-5.1.3.RELEASE.jar:na]
    at io.lettuce.core.protocol.CommandHandler.decode(CommandHandler.java:604) ~[lettuce-core-5.1.3.RELEASE.jar:na]
    at io.lettuce.core.protocol.CommandHandler.channelRead(CommandHandler.java:556) ~[lettuce-core-5.1.3.RELEASE.jar:na]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1434) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:965) ~[netty-transport-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:799) ~[netty-transport-native-epoll-4.1.31.Final-linux-x86_64.jar:4.1.31.Final]
    at io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:433) ~[netty-transport-native-epoll-4.1.31.Final-linux-x86_64.jar:4.1.31.Final]
    at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:330) ~[netty-transport-native-epoll-4.1.31.Final-linux-x86_64.jar:4.1.31.Final]
    at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:897) ~[netty-common-4.1.31.Final.jar:4.1.31.Final]
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.31.Final.jar:4.1.31.Final]
    at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Assembly trace from producer [reactor.core.publisher.MonoFlatMap] :
    reactor.core.publisher.Mono.flatMap(Mono.java:2491)
    org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository.save(ReactiveRedisOperationsSessionRepository.java:159)
Error has been observed by the following operator(s):
    |_  Mono.flatMap ⇢ org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository.save(ReactiveRedisOperationsSessionRepository.java:159)
    |_  Flux.concat ⇢ org.springframework.http.server.reactive.AbstractServerHttpResponse.doCommit(AbstractServerHttpResponse.java:234)
    |_  Flux.then ⇢ org.springframework.http.server.reactive.AbstractServerHttpResponse.doCommit(AbstractServerHttpResponse.java:234)
    |_  Mono.doOnError ⇢ org.springframework.http.server.reactive.AbstractServerHttpResponse.writeWith(AbstractServerHttpResponse.java:179)
    |_  Mono.flatMap ⇢ org.springframework.http.codec.EncoderHttpMessageWriter.write(EncoderHttpMessageWriter.java:126)
    |_  Mono.flatMap ⇢ org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler.handle(AbstractErrorWebExceptionHandler.java:247)
    |_  Mono.onErrorResume ⇢ org.springframework.web.server.handler.ExceptionHandlingWebHandler.handle(ExceptionHandlingWebHandler.java:68)
    |_  Mono.error ⇢ org.springframework.web.server.handler.ResponseStatusExceptionHandler.handle(ResponseStatusExceptionHandler.java:67)
    |_  Mono.onErrorResume ⇢ org.springframework.web.server.handler.ExceptionHandlingWebHandler.handle(ExceptionHandlingWebHandler.java:68)
    |_  Mono.doOnSuccess ⇢ org.springframework.web.server.adapter.HttpWebHandlerAdapter.handle(HttpWebHandlerAdapter.java:247)
redis backported bug

All 12 comments

Thanks for the report @AndreasKl, I've confirmed this also with a sample of my own.

I've marked this as a bug, although I'm not sure this is Spring Session's problem - for me the issue reported here goes away if I downgrade Spring Security to 5.0.x. Can you confirm this?

/cc @rwinch

Hi, @vpavic thanks for looking into this issue.

I can't reproduce the issue on spring-security-core 5.0.12, however the 5.0.x release does not contain a CSRF-filter for Webflux; therefore only one saveDelta call is executed.

I'll try to create an unit test to reproduce the issue in an isolated environment.


@Test
public void saveFailsWhenCalledMoreThanOnceForSingleRequest() {
    ReactiveRedisOperationsSessionRepository.RedisSession toSave = this.repository
            .createSession().block();

    String expectedAttributeName = "a";
    String expectedAttributeValue = "b";

    toSave.setAttribute(expectedAttributeName, expectedAttributeValue);

    // Add some arbitrary attributes.
    Mono<Void> save1 = this.repository.save(toSave);

    // toSave.changeSessionId(); // Causes the same behaviour.
    toSave.setAttribute("c", "x");
    Mono<Void> save2 = this.repository.save(toSave);

    Mono.zip(save1, save2).block();

    Session session = this.repository.findById(toSave.getId()).block();

    assertThat(session.getId()).isEqualTo(toSave.getId());
    assertThat(session.getAttributeNames()).isEqualTo(toSave.getAttributeNames());
    assertThat(session.<String>getAttribute(expectedAttributeName))
            .isEqualTo(toSave.getAttribute(expectedAttributeName));

    this.repository.deleteById(toSave.getId()).block();

    assertThat(this.repository.findById(toSave.getId()).block()).isNull();
}

@vpavic As promised the test case, I just added a new failing test case to: ReactiveRedisOperationsSessionRepositoryITests

Thanks @AndreasKl - I've confirmed the problem with your test.

The issue here is due to a race condition involving ReactiveRedisOperationsSessionRepository.RedisSession#saveDelta.

The delta gets originally evaluated in:

https://github.com/spring-projects/spring-session/blob/397d4e5ca11954072187ac701ab0379505029f7d/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/ReactiveRedisOperationsSessionRepository.java#L318-L320
When multiple #save operations are chained, both invocations will observe delta as not empty. However, when update Mono is processed the second invocation will see it as empy:

https://github.com/spring-projects/spring-session/blob/397d4e5ca11954072187ac701ab0379505029f7d/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/ReactiveRedisOperationsSessionRepository.java#L323-L324
It seems that simply creating the copy of delta in update operation does the job, but I'll do more testing on a real project.

I've tried the delta copy fix against your sample project @AndreasKl, and it turns out to be only half of the fix. Now I'm seeing the following error:

2019-05-02 21:41:06.335 ERROR 21070 --- [llEventLoop-4-5] a.w.r.e.AbstractErrorWebExceptionHandler : [5d1dde4a] 500 Server Error for HTTP POST "/logout"

org.springframework.data.redis.RedisSystemException: Error in execution; nested exception is io.lettuce.core.RedisCommandExecutionException: ERR no such key
    at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:54) ~[spring-data-redis-2.1.6.RELEASE.jar:2.1.6.RELEASE]
    at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:41) ~[spring-data-redis-2.1.6.RELEASE.jar:2.1.6.RELEASE]
    at org.springframework.data.redis.connection.lettuce.LettuceReactiveRedisConnection.lambda$translateException$1(LettuceReactiveRedisConnection.java:283) ~[spring-data-redis-2.1.6.RELEASE.jar:2.1.6.RELEASE]
    at reactor.core.publisher.Flux.lambda$onErrorMap$24(Flux.java:6186) ~[reactor-core-3.2.8.RELEASE.jar:3.2.8.RELEASE]
    at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onError(FluxOnErrorResume.java:88) ~[reactor-core-3.2.8.RELEASE.jar:3.2.8.RELEASE]
    at reactor.core.publisher.MonoFlatMapMany$FlatMapManyInner.onError(MonoFlatMapMany.java:243) ~[reactor-core-3.2.8.RELEASE.jar:3.2.8.RELEASE]
    at reactor.core.publisher.FluxMap$MapSubscriber.onError(FluxMap.java:126) ~[reactor-core-3.2.8.RELEASE.jar:3.2.8.RELEASE]
    at reactor.core.publisher.FluxMap$MapSubscriber.onError(FluxMap.java:126) ~[reactor-core-3.2.8.RELEASE.jar:3.2.8.RELEASE]
    at reactor.core.publisher.MonoNext$NextSubscriber.onError(MonoNext.java:87) ~[reactor-core-3.2.8.RELEASE.jar:3.2.8.RELEASE]
    at io.lettuce.core.RedisPublisher$ImmediateSubscriber.onError(RedisPublisher.java:905) ~[lettuce-core-5.1.6.RELEASE.jar:na]
    at io.lettuce.core.RedisPublisher$State.onError(RedisPublisher.java:703) ~[lettuce-core-5.1.6.RELEASE.jar:na]
    at io.lettuce.core.RedisPublisher$RedisSubscription.onError(RedisPublisher.java:349) ~[lettuce-core-5.1.6.RELEASE.jar:na]
    at io.lettuce.core.RedisPublisher$SubscriptionCommand.onError(RedisPublisher.java:815) ~[lettuce-core-5.1.6.RELEASE.jar:na]
    at io.lettuce.core.RedisPublisher$SubscriptionCommand.complete(RedisPublisher.java:751) ~[lettuce-core-5.1.6.RELEASE.jar:na]
    at io.lettuce.core.protocol.CommandWrapper.complete(CommandWrapper.java:59) ~[lettuce-core-5.1.6.RELEASE.jar:na]
    at io.lettuce.core.protocol.CommandHandler.complete(CommandHandler.java:646) ~[lettuce-core-5.1.6.RELEASE.jar:na]
    at io.lettuce.core.protocol.CommandHandler.decode(CommandHandler.java:604) ~[lettuce-core-5.1.6.RELEASE.jar:na]
    at io.lettuce.core.protocol.CommandHandler.channelRead(CommandHandler.java:556) ~[lettuce-core-5.1.6.RELEASE.jar:na]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:359) ~[netty-transport-4.1.34.Final.jar:4.1.34.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:345) ~[netty-transport-4.1.34.Final.jar:4.1.34.Final]
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:337) ~[netty-transport-4.1.34.Final.jar:4.1.34.Final]
    at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86) ~[netty-transport-4.1.34.Final.jar:4.1.34.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:359) ~[netty-transport-4.1.34.Final.jar:4.1.34.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:345) ~[netty-transport-4.1.34.Final.jar:4.1.34.Final]
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:337) ~[netty-transport-4.1.34.Final.jar:4.1.34.Final]
    at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86) ~[netty-transport-4.1.34.Final.jar:4.1.34.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:359) ~[netty-transport-4.1.34.Final.jar:4.1.34.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:345) ~[netty-transport-4.1.34.Final.jar:4.1.34.Final]
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:337) ~[netty-transport-4.1.34.Final.jar:4.1.34.Final]
    at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1408) ~[netty-transport-4.1.34.Final.jar:4.1.34.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:359) ~[netty-transport-4.1.34.Final.jar:4.1.34.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:345) ~[netty-transport-4.1.34.Final.jar:4.1.34.Final]
    at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:930) ~[netty-transport-4.1.34.Final.jar:4.1.34.Final]
    at io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:796) ~[netty-transport-native-epoll-4.1.34.Final-linux-x86_64.jar:4.1.34.Final]
    at io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:427) ~[netty-transport-native-epoll-4.1.34.Final-linux-x86_64.jar:4.1.34.Final]
    at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:328) ~[netty-transport-native-epoll-4.1.34.Final-linux-x86_64.jar:4.1.34.Final]
    at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:905) ~[netty-common-4.1.34.Final.jar:4.1.34.Final]
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.34.Final.jar:4.1.34.Final]
    at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]
Caused by: io.lettuce.core.RedisCommandExecutionException: ERR no such key
    at io.lettuce.core.ExceptionFactory.createExecutionException(ExceptionFactory.java:135) ~[lettuce-core-5.1.6.RELEASE.jar:na]
    at io.lettuce.core.ExceptionFactory.createExecutionException(ExceptionFactory.java:108) ~[lettuce-core-5.1.6.RELEASE.jar:na]
    ... 26 common frames omitted

This is specific to scenario with two #changeSessionId invocations on the same request.

This should probably be addressed by having RedisSession#saveChangeSessionId and RedisSession#saveDelta wrap their logic into a Mono#defer - @AndreasKl, are you interested in providing the PR to address this?

Hi @vpavic, I'm googing looking into this over the weekend and will provide a PR.

Hi @vpavic , sorry for the delay.

I had time to look into this and did not came up with a smart solution. The general issue I see is that org.springframework.session.Session is mutable, which could case all kind of race issues when adding, removing attributes or rotating session ids. Imho this also manifests in unhandled Mono(s) like in RedisSession#flushImmediateIfNecessary.

I started to apply locks where necessary and use copies where possible, after two hours of digging I'm still able to build race conditions that causes unexpected behavior.

What I'm currently thinking of is introducing a new interface for sessions in reactive environments with an all reactive API e.g. returning Mono<Void> instead of void for RedisSession#setAttribute or an immutable Session that is only flushed at the end of the request and calling save is only permitted to be called once.

I'm not sure if such a breaking change is an option at all, as this would also bubble up into the frameworks WebSession?

Thank for your efforts here @AndreasKl. Unfortunately, it seems that you've gone to lengths not necessary to address this issue since what we really needed to do is to ensure that write operations in ReactiveRedisOperationsSessionRepository#save are deferred until someone subscribes as suggested in this comment.

Imho this also manifests in unhandled Mono(s) like in RedisSession#flushImmediateIfNecessary.

This indeed is a problem and I've opened #1441 to consider what to do about it.

If you could give 2.2.0.BUILD-SNAPSHOT or 2.1.7.BUILD-SNAPSHOT a try that would be appreciated, they should be built within half an hour from now.

@vpavic I had exact same issue and applying 2.1.7.BUILD-SNAPSHOT fixed it.

Thanks for the feedback @serghei-ilin, it's much appreciated! Spring Session 2.1.7.RELEASE was released recently so you can use the fix in a stable release now.

Was this page helpful?
0 / 5 - 0 ratings