From 33d8bfa99d0669e7aef54567e8f3ee9dae1c215b Mon Sep 17 00:00:00 2001 From: dreis2211 Date: Tue, 24 Sep 2019 14:46:57 +0200 Subject: [PATCH] Apply TTL invocation caching on reactor types Update `CachingOperationInvoker` so that TTL caching is applied directly to reactive types. Prior to this commit, a `Mono` would be cached, but the values that it emitted would not. See gh-18339 --- .../cache/CachingOperationInvoker.java | 12 +++++- .../cache/CachingOperationInvokerTests.java | 37 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvoker.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvoker.java index 6578c103fd..7a514f5340 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvoker.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvoker.java @@ -16,9 +16,12 @@ package org.springframework.boot.actuate.endpoint.invoker.cache; +import java.time.Duration; import java.util.Map; import java.util.Objects; +import reactor.core.publisher.Mono; + import org.springframework.boot.actuate.endpoint.InvocationContext; import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; import org.springframework.util.Assert; @@ -67,13 +70,20 @@ public class CachingOperationInvoker implements OperationInvoker { long accessTime = System.currentTimeMillis(); CachedResponse cached = this.cachedResponse; if (cached == null || cached.isStale(accessTime, this.timeToLive)) { - Object response = this.invoker.invoke(context); + Object response = handleMonoResponse(this.invoker.invoke(context)); this.cachedResponse = new CachedResponse(response, accessTime); return response; } return cached.getResponse(); } + private Object handleMonoResponse(Object response) { + if (response instanceof Mono) { + return ((Mono) response).cache(Duration.ofMillis(this.timeToLive)); + } + return response; + } + private boolean hasInput(InvocationContext context) { if (context.getSecurityContext().getPrincipal() != null) { return true; diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerTests.java index 050171e9c7..b3c6202345 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerTests.java @@ -17,15 +17,20 @@ package org.springframework.boot.actuate.endpoint.invoker.cache; import java.security.Principal; +import java.time.Duration; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import org.junit.Rule; import org.junit.Test; +import reactor.core.publisher.Mono; import org.springframework.boot.actuate.endpoint.InvocationContext; import org.springframework.boot.actuate.endpoint.SecurityContext; +import org.springframework.boot.actuate.endpoint.invoke.MissingParametersException; import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; +import org.springframework.boot.test.rule.OutputCapture; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -42,6 +47,9 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; */ public class CachingOperationInvokerTests { + @Rule + public OutputCapture outputCapture = new OutputCapture(); + @Test public void createInstanceWithTtlSetToZero() { assertThatIllegalArgumentException() @@ -62,6 +70,21 @@ public class CachingOperationInvokerTests { assertCacheIsUsed(parameters); } + @Test + public void cacheInTtlWithMonoResponse() { + MonoOperationInvoker target = new MonoOperationInvoker(); + InvocationContext context = new InvocationContext(mock(SecurityContext.class), Collections.emptyMap()); + CachingOperationInvoker invoker = new CachingOperationInvoker(target, 500L); + Object monoResponse = invoker.invoke(context); + assertThat(monoResponse).isInstanceOf(Mono.class); + Object response = ((Mono) monoResponse).block(Duration.ofSeconds(30)); + Object cachedMonoResponse = invoker.invoke(context); + assertThat(cachedMonoResponse).isInstanceOf(Mono.class); + Object cachedResponse = ((Mono) cachedMonoResponse).block(Duration.ofSeconds(30)); + assertThat(response).isSameAs(cachedResponse); + assertThat(this.outputCapture.toString()).containsOnlyOnce("invoked"); + } + private void assertCacheIsUsed(Map parameters) { OperationInvoker target = mock(OperationInvoker.class); Object expected = new Object(); @@ -119,4 +142,18 @@ public class CachingOperationInvokerTests { verify(target, times(2)).invoke(context); } + private static class MonoOperationInvoker implements OperationInvoker { + + @Override + public Object invoke(InvocationContext context) throws MissingParametersException { + return Mono.fromCallable(this::printInvocation); + } + + private Mono printInvocation() { + System.out.println("MonoOperationInvoker invoked"); + return Mono.just("test"); + } + + } + }