Skip to content

Commit d08aece

Browse files
committed
Fix FunctionAroundWrapper state and wrapped-function cache eviction
Reset the 'wrapped' flag in a finally block in FunctionInvocationWrapper.apply(). Previously it stayed true after a call with non-Message or Publisher input, so later Message calls on the same cached function skipped the around-advice (e.g. tracing). Make the wrapped-function cache access-ordered so that eviction is least-recently-used instead of first-in-first-out, and correct the copy-pasted eviction debug message. Add regression tests for both. Signed-off-by: Oleg Zhurakousky <oleg.zhurakousky@broadcom.com>
1 parent e0231b1 commit d08aece

2 files changed

Lines changed: 53 additions & 3 deletions

File tree

‎spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/SimpleFunctionRegistry.java‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,13 +135,14 @@ public SimpleFunctionRegistry(ConversionService conversionService, CompositeMess
135135
this.messageConverter = messageConverter;
136136
this.functionInvocationHelper = functionInvocationHelper;
137137
this.functionProperties = functionProperties;
138-
this.wrappedFunctionDefinitions = new LinkedHashMap<>() {
138+
// access-ordered so that eviction is least-recently-used rather than first-in-first-out
139+
this.wrappedFunctionDefinitions = new LinkedHashMap<>(16, 0.75f, true) {
139140
@Override
140141
protected boolean removeEldestEntry(Map.Entry<String, FunctionInvocationWrapper> eldest) {
141142
boolean remove = size() > wrappedFunctionDefinitionsCacheSize;
142143
if (remove) {
143144
if (logger.isDebugEnabled()) {
144-
logger.debug("Removing message channel from cache " + eldest.getKey());
145+
logger.debug("Evicting wrapped function from cache: " + eldest.getKey());
145146
}
146147
}
147148
return remove;
@@ -662,7 +663,12 @@ public Object apply(Object input) {
662663
Object result;
663664
if (functionAroundWrapper != null && !this.wrapped) {
664665
this.wrapped = true;
665-
result = functionAroundWrapper.apply(input, this);
666+
try {
667+
result = functionAroundWrapper.apply(input, this);
668+
}
669+
finally {
670+
this.wrapped = false;
671+
}
666672
}
667673
else {
668674
result = this.doApply(input);

‎spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/catalog/SimpleFunctionRegistryTests.java‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
import org.springframework.messaging.converter.ProtobufMessageConverter;
8181
import org.springframework.messaging.converter.StringMessageConverter;
8282
import org.springframework.messaging.support.MessageBuilder;
83+
import org.springframework.test.util.ReflectionTestUtils;
8384
import org.springframework.util.MimeType;
8485
import org.springframework.util.ReflectionUtils;
8586

@@ -110,6 +111,49 @@ public void before() {
110111
this.conversionService = new DefaultConversionService();
111112
}
112113

114+
@Test
115+
public void testAroundWrapperAppliedAfterNonMessageInvocation() {
116+
SimpleFunctionRegistry catalog = new SimpleFunctionRegistry(this.conversionService, this.messageConverter,
117+
new GsonMapper(new Gson()));
118+
AtomicInteger aroundInvocations = new AtomicInteger();
119+
ReflectionTestUtils.setField(catalog, "functionAroundWrapper", new FunctionAroundWrapper() {
120+
@Override
121+
protected Object doApply(Object input, FunctionInvocationWrapper targetFunction) {
122+
aroundInvocations.incrementAndGet();
123+
return targetFunction.apply(input);
124+
}
125+
});
126+
Function<String, String> uppercase = String::toUpperCase;
127+
catalog.register(new FunctionRegistration<>(uppercase, "uppercase")
128+
.type(ResolvableType.forClassWithGenerics(Function.class, String.class, String.class).getType()));
129+
FunctionInvocationWrapper function = catalog.lookup("uppercase");
130+
131+
// non-Message input is not advised, but must not leave the wrapper stuck in 'wrapped' state
132+
function.apply("a");
133+
assertThat(aroundInvocations.get()).isEqualTo(0);
134+
135+
function.apply(MessageBuilder.withPayload("b").build());
136+
assertThat(aroundInvocations.get()).isEqualTo(1);
137+
function.apply(MessageBuilder.withPayload("c").build());
138+
assertThat(aroundInvocations.get()).isEqualTo(2);
139+
}
140+
141+
@Test
142+
public void testWrappedFunctionCacheEvictsLeastRecentlyUsed() {
143+
SimpleFunctionRegistry catalog = new SimpleFunctionRegistry(this.conversionService, this.messageConverter,
144+
new GsonMapper(new Gson()), null, null, 2);
145+
Type type = ResolvableType.forClassWithGenerics(Function.class, String.class, String.class).getType();
146+
for (String name : new String[] {"a", "b", "c"}) {
147+
Function<String, String> f = v -> v;
148+
catalog.register(new FunctionRegistration<>(f, name).type(type));
149+
}
150+
Object a = catalog.lookup("a");
151+
catalog.lookup("b");
152+
assertThat((Object) catalog.lookup("a")).isSameAs(a); // 'a' is now most recently used
153+
catalog.lookup("c"); // exceeds cache size; least recently used ('b') must be evicted
154+
assertThat((Object) catalog.lookup("a")).isSameAs(a);
155+
}
156+
113157
@ParameterizedTest
114158
@ValueSource(strings = {
115159
"aaaaaaaaaa", // no problem

0 commit comments

Comments
 (0)