Search code examples
spring-webfluxproject-reactorreactive

How to reuse cached value in ProjectReactor


I would like my server to call the login endpoint of another server and cache the auth token for later use. My issue is that when the server tries to reuse the existing token it hangs indefinitely or infinite loops.

@Component
class ApiWebClient {

    private var authToken = Mono.just(AuthToken("", Instant.ofEpochSecond(0)))

    fun login(): Mono<AuthToken> {
        authToken = doesTokenNeedRefreshing().flatMap { needsRefreshing ->
            if (needsRefreshing) {
                WebClient.create().post()
                    .uri("https://example.com/login")
                    .body(
                        Mono.just("Credentials"),
                        String::class.java
                    ).exchangeToMono { response ->
                        response.bodyToMono<LoginResponse>()
                    }.map { response ->
                        LOGGER.info("Successfully logged in")
                        AuthToken(response.token, Instant.now())
                    }
            } else {
                LOGGER.info("Reuse token")
                authToken
            }
        }.cache()

        return authToken
    }

    private fun doesTokenNeedRefreshing(): Mono<Boolean> {
        return authToken.map {
            Instant.now().minusMillis(ONE_MINUTE_IN_MILLIS).isAfter(it.lastModified)
        }
    }


    class AuthToken(
        var token: String,
        var lastModified: Instant
    )

    companion object {
        private const val ONE_MINUTE_IN_MILLIS = 60 * 1000L

        @Suppress("JAVA_CLASS_ON_COMPANION")
        @JvmStatic
        private val LOGGER = LoggerFactory.getLogger(javaClass.enclosingClass)
    }
}

If login gets called twice within the ONE_MINUTE_IN_MILLIS amount of time then it just hangs. I suspect this is because the doesTokenNeedRefreshing() calls a .map {} on authToken and then later down the chain authToken is reassigned to itself. As well, there's an attempt to recache that exact same value. I've played around with recreating AuthToken each time instead of returning the same instance but no luck. The server either hangs or infinite loops.

How can I achieve returning the same instance of the cached value so I don't have to make a web request each time?


Solution

  • What I was looking for was a switchIfEmpty statement to return the existing cached value if it doesn't need to be refreshed.

    @Component
    class ApiWebClient {
    
        private var authToken = Mono.just(AuthToken("", Instant.ofEpochSecond(0)))
    
        fun login(): Mono<AuthToken> {
            authToken = doesTokenNeedRefreshing().flatMap { needsRefreshing ->
                if (needsRefreshing) {
                    WebClient.create().post()
                        .uri("https://example.com/login")
                        .body(
                            Mono.just("Credentials"),
                            String::class.java
                        ).exchangeToMono { response ->
                            response.bodyToMono<LoginResponse>()
                        }.map { response ->
                            LOGGER.info("Successfully logged in")
                            AuthToken(response.token, Instant.now())
                        }
                } else {
                    LOGGER.info("Reuse token")
                    Mono.empty()
                }
            }.cache()
                .switchIfEmpty(authToken)
    
            return authToken
        }
    }
    

    In this example, if the token doesn't need to be refreshed then the stream returns an empty Mono. Then the switchIfEmpty statement returns the original auth token. This, therefore, avoids "recursively" returning the same stream over and over again.