Skip to content

sync=true on @Cacheable looses reactor's Context #33079

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

Closed
Crystark opened this issue Jun 20, 2024 · 5 comments
Closed

sync=true on @Cacheable looses reactor's Context #33079

Crystark opened this issue Jun 20, 2024 · 5 comments
Assignees
Labels
in: core Issues in core modules (aop, beans, core, context, expression) status: declined A suggestion or change that we don't feel we should currently apply

Comments

@Crystark
Copy link

Crystark commented Jun 20, 2024

Affects: 6.1.8


Describe the bug
@Cacheable annotation with sync=true on a reactor publisher returning method looses the reactive context

To Reproduce

This code reproduces the issue:

import com.github.benmanes.caffeine.cache.Caffeine
import org.springframework.cache.annotation.Cacheable
import org.springframework.cache.caffeine.CaffeineCache
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.scheduling.annotation.EnableScheduling
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component
import org.springframework.stereotype.Service
import reactor.core.publisher.Mono
import reactor.util.context.Context
import java.util.concurrent.TimeUnit
import kotlin.random.Random

@EnableScheduling
@EnableCaching
@Configuration
class ZeConfig {
    @Bean
    fun myCache() = createCache("MyCacheName")

    private fun createCache(name: String) = CaffeineCache(
        name,
        Caffeine.newBuilder()
            .maximumSize(100)
            .removalListener<Any, Any> { key, _, cause ->
                println("Removal of $key: $cause")
            }
            .buildAsync(),
        false
    )
}

@Service
class ServiceWithCache {
    @Cacheable("MyCacheName", sync = true)
    fun get() = Mono.deferContextual { context ->
        println(context)
        // ... do something
        Mono.just(Random.nextInt(1000))
    }
}

@Component
class Example(private val serviceWithCache: ServiceWithCache) {
    @Scheduled(fixedRate = 5, timeUnit = TimeUnit.SECONDS)
    fun process() {
        val it = serviceWithCache
            .get()
            .contextWrite(Context.of("some-thing1", "lala1"))
            .block()
        println("Result: $it")
    }
}

Actual behavior

Code execution prints the following

Context0{}
Result: 242
Result: 242
Result: 242
Result: 242
Result: 242

Expected behavior
Code should print

Context1{some-thing1=lala1}
Result: 242
Result: 242
Result: 242
Result: 242
Result: 242

Workaround
If I remove the sync=true it all works as expected so the issue comes from the use of sync=true

This was originally a question on stackoverflow.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Jun 20, 2024
@jhoeller jhoeller added the in: core Issues in core modules (aop, beans, core, context, expression) label Jun 20, 2024
@simonbasle
Copy link
Contributor

I could not reproduce this as is. If you'd like us to spend some time investigating, please take the time to provide a complete minimal sample (something that we can unzip or git clone, build, and deploy) that reproduces the problem.
Another option is some form of integration test that can be easily added to Spring Framework's test suite).

It would be interesting to try and avoid the scheduling aspect, in order to ensure there are as little moving parts as possible.

@simonbasle simonbasle added the status: waiting-for-feedback We need additional information before we can continue label Jul 10, 2024
@spring-projects-issues
Copy link
Collaborator

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

@spring-projects-issues spring-projects-issues added the status: feedback-reminder We've sent a reminder that we need additional information before we can continue label Jul 17, 2024
@Crystark
Copy link
Author

Hi, sorry for my late answer @simonbasle

Thanks for getting back to me.

It seems I have missed in my example the @EnableCaching annotation. Maybe that's why you couldn't reproduce the issue ? Once added I could successfully make this work in a standalone app.

You'll find a maven project here that reproduces the issue. I didn't remove scheduling due to the lack of time on my end. I hope it's sufficient.

example.spring.issue33079.zip

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue status: feedback-reminder We've sent a reminder that we need additional information before we can continue labels Jul 18, 2024
@simonbasle simonbasle self-assigned this Jul 18, 2024
@simonbasle simonbasle added type: bug A general bug and removed status: waiting-for-triage An issue we've not yet triaged or decided on status: feedback-provided Feedback has been provided labels Jul 18, 2024
@simonbasle simonbasle added this to the 6.1.x milestone Jul 18, 2024
@simonbasle simonbasle added status: declined A suggestion or change that we don't feel we should currently apply and removed type: bug A general bug labels Jul 18, 2024
@simonbasle simonbasle removed this from the 6.1.x milestone Jul 18, 2024
@simonbasle
Copy link
Contributor

Thanks for the reproducer, @Crystark, this provided good context for deeper analysis. Unfortunately, we cannot support context propagation in the case of sync=true.

Think of it that way: in order to avoid any double invocation of the service method, it is not your controller which subscribes to the mono it returns. Instead, it is the CaffeineCache which does (by converting the Mono to a Future, which subscribes to it without passing any context).

Imagine two concurrent calls to the cached service, each with a different contextWrite. With sync=false both calls fall through and the actual service method ends up being called twice with both contexts. With sync=true, it becomes undeterministic which of the two callers would "win" and instead the cache itself invokes the method, effectively passing the cached result to both callers.

Hope this helps.

@Crystark
Copy link
Author

Yes I understand. Thanks for the investigation

@simonbasle simonbasle closed this as not planned Won't fix, can't repro, duplicate, stale Jul 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: core Issues in core modules (aop, beans, core, context, expression) status: declined A suggestion or change that we don't feel we should currently apply
Projects
None yet
Development

No branches or pull requests

4 participants