Skip to content

Commit 33d8bfa

Browse files
dreis2211philwebb
authored andcommitted
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
1 parent 89e7d5f commit 33d8bfa

File tree

2 files changed

+48
-1
lines changed

2 files changed

+48
-1
lines changed

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvoker.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@
1616

1717
package org.springframework.boot.actuate.endpoint.invoker.cache;
1818

19+
import java.time.Duration;
1920
import java.util.Map;
2021
import java.util.Objects;
2122

23+
import reactor.core.publisher.Mono;
24+
2225
import org.springframework.boot.actuate.endpoint.InvocationContext;
2326
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
2427
import org.springframework.util.Assert;
@@ -67,13 +70,20 @@ public Object invoke(InvocationContext context) {
6770
long accessTime = System.currentTimeMillis();
6871
CachedResponse cached = this.cachedResponse;
6972
if (cached == null || cached.isStale(accessTime, this.timeToLive)) {
70-
Object response = this.invoker.invoke(context);
73+
Object response = handleMonoResponse(this.invoker.invoke(context));
7174
this.cachedResponse = new CachedResponse(response, accessTime);
7275
return response;
7376
}
7477
return cached.getResponse();
7578
}
7679

80+
private Object handleMonoResponse(Object response) {
81+
if (response instanceof Mono) {
82+
return ((Mono) response).cache(Duration.ofMillis(this.timeToLive));
83+
}
84+
return response;
85+
}
86+
7787
private boolean hasInput(InvocationContext context) {
7888
if (context.getSecurityContext().getPrincipal() != null) {
7989
return true;

spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerTests.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,20 @@
1717
package org.springframework.boot.actuate.endpoint.invoker.cache;
1818

1919
import java.security.Principal;
20+
import java.time.Duration;
2021
import java.util.Collections;
2122
import java.util.HashMap;
2223
import java.util.Map;
2324

25+
import org.junit.Rule;
2426
import org.junit.Test;
27+
import reactor.core.publisher.Mono;
2528

2629
import org.springframework.boot.actuate.endpoint.InvocationContext;
2730
import org.springframework.boot.actuate.endpoint.SecurityContext;
31+
import org.springframework.boot.actuate.endpoint.invoke.MissingParametersException;
2832
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
33+
import org.springframework.boot.test.rule.OutputCapture;
2934

3035
import static org.assertj.core.api.Assertions.assertThat;
3136
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -42,6 +47,9 @@
4247
*/
4348
public class CachingOperationInvokerTests {
4449

50+
@Rule
51+
public OutputCapture outputCapture = new OutputCapture();
52+
4553
@Test
4654
public void createInstanceWithTtlSetToZero() {
4755
assertThatIllegalArgumentException()
@@ -62,6 +70,21 @@ public void cacheInTtlWithNullParameters() {
6270
assertCacheIsUsed(parameters);
6371
}
6472

73+
@Test
74+
public void cacheInTtlWithMonoResponse() {
75+
MonoOperationInvoker target = new MonoOperationInvoker();
76+
InvocationContext context = new InvocationContext(mock(SecurityContext.class), Collections.emptyMap());
77+
CachingOperationInvoker invoker = new CachingOperationInvoker(target, 500L);
78+
Object monoResponse = invoker.invoke(context);
79+
assertThat(monoResponse).isInstanceOf(Mono.class);
80+
Object response = ((Mono) monoResponse).block(Duration.ofSeconds(30));
81+
Object cachedMonoResponse = invoker.invoke(context);
82+
assertThat(cachedMonoResponse).isInstanceOf(Mono.class);
83+
Object cachedResponse = ((Mono) cachedMonoResponse).block(Duration.ofSeconds(30));
84+
assertThat(response).isSameAs(cachedResponse);
85+
assertThat(this.outputCapture.toString()).containsOnlyOnce("invoked");
86+
}
87+
6588
private void assertCacheIsUsed(Map<String, Object> parameters) {
6689
OperationInvoker target = mock(OperationInvoker.class);
6790
Object expected = new Object();
@@ -119,4 +142,18 @@ public void targetInvokedWhenCacheExpires() throws InterruptedException {
119142
verify(target, times(2)).invoke(context);
120143
}
121144

145+
private static class MonoOperationInvoker implements OperationInvoker {
146+
147+
@Override
148+
public Object invoke(InvocationContext context) throws MissingParametersException {
149+
return Mono.fromCallable(this::printInvocation);
150+
}
151+
152+
private Mono<String> printInvocation() {
153+
System.out.println("MonoOperationInvoker invoked");
154+
return Mono.just("test");
155+
}
156+
157+
}
158+
122159
}

0 commit comments

Comments
 (0)