DefaultPollableMessageSource: invoke afterSendCompletion for already-applied interceptors when a message is discarded
维护者通常 1 天内回复
还没有人认领这个 Issue。
评估
- 难度
- 3/5
- 预计耗时
- 1-2 天
- 新手友好度
- 74/100
- Issue 类型
- 缺陷
- 描述清晰度
- 描述清楚
- 活跃度
- 活跃
- 技术栈
- java
调研方向
Start in DefaultPollableMessageSource, especially ReceiveAdvice.invoke, setSource(), and the poll() overload that converts to ParameterizedTypeReference. Trace how interceptors are applied and where completion callbacks run for discarded messages and conversion exceptions. Add focused tests based on the two reproductions; done means each interceptor whose preSend succeeds gets exactly one completion callback on both paths, and the tests pass.
由索引模型根据 Issue 内容生成。
描述
Describe the issue
Follow-up to #3131 / #3253. Since 5.0.4, DefaultPollableMessageSource invokes the full ChannelInterceptor contract, but there are two paths where interceptors whose preSend already succeeded never receive afterSendCompletion, unlike AbstractMessageChannel (which calls triggerAfterSendCompletion for the already-applied interceptors):
- A later interceptor returns
nullfrompreSend: thepreSendloop inReceiveAdvice(the local class created insetSource()that wrapsMessageSource.receive()) returnsnull,poll()returnsfalse, and noafterSendCompletionis invoked for the interceptors already applied. - Payload conversion fails in
receive(ParameterizedTypeReference): theMessageConversionExceptionis thrown afterReceiveAdvicehas runpreSend, but before thetry/finallyofpoll(), soafterSendCompletionis skipped for all interceptors.
Interceptors that open resources in preSend and release them in afterSendCompletion (tracing scope, MDC, thread locals) leak them on the poller thread. A typical case is a filtering interceptor that discards messages by returning null, registered after a tracing interceptor.
To Reproduce
Plain JUnit test, no application context needed (spring-cloud-stream 5.0.4). Both tests fail at the completion assertion:
package com.example.demoStreamPollable;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.stream.binder.DefaultPollableMessageSource;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.converter.CompositeMessageConverter;
import org.springframework.messaging.converter.StringMessageConverter;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.GenericMessage;
class PollableInterceptorCompletionTests {
private final List<String> events = new ArrayList<>();
@Test
void afterSendCompletionIsInvokedForAppliedInterceptorsWhenLaterPreSendReturnsNull() {
DefaultPollableMessageSource source = newSource();
source.setSource(() -> new GenericMessage<>("foo"));
source.addInterceptor(recorder("A"));
source.addInterceptor(new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
return null; // e.g. a filtering interceptor discarding the message
}
});
assertThat(source.poll(message -> { })).isFalse();
// Same contract as AbstractMessageChannel: A's preSend succeeded, so it must be completed
assertThat(this.events).containsExactly("A.preSend", "A.afterSendCompletion(sent=false)");
}
@Test
void afterSendCompletionIsInvokedWhenPayloadConversionFails() {
DefaultPollableMessageSource source = newSource();
source.setSource(() -> new GenericMessage<>("not a Foo"));
source.addInterceptor(recorder("A"));
assertThatThrownBy(() -> source.poll(message -> { }, new ParameterizedTypeReference<Foo>() { }))
.isInstanceOf(RuntimeException.class);
assertThat(this.events).contains("A.preSend").anyMatch(e -> e.startsWith("A.afterSendCompletion"));
}
private static DefaultPollableMessageSource newSource() {
return new DefaultPollableMessageSource(new CompositeMessageConverter(List.of(new StringMessageConverter())));
}
private ChannelInterceptor recorder(String name) {
return new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
events.add(name + ".preSend");
return message;
}
@Override
public void afterSendCompletion(Message<?> message, MessageChannel channel, boolean sent, Exception ex) {
events.add(name + ".afterSendCompletion(sent=" + sent + ")");
}
};
}
record Foo(String value) {
}
}
Version of the framework
Spring Cloud Stream 5.0.4 (Spring Cloud 2025.1.4), Spring Boot 4.1.2.
Expected behavior
Every interceptor whose preSend returned a message receives afterSendCompletion exactly once, also when the message is discarded by a later interceptor or when conversion fails, consistent with AbstractMessageChannel.
Additional context
Proposal:
- In the
preSendloop ofReceiveAdvice.invoke, track the interceptors whosepreSendreturned a message, and callafterSendCompletion(message, DUMMY_CHANNEL, false, null)in reverse order when a later one returnsnull, the same asAbstractMessageChannel. - Run the completion when conversion fails as well (for example, by moving the conversion inside the
tryofpoll(), or by completing with the exception).
Happy to submit a PR.
- 主要语言
- Java
- 星标
- 1.1k
- 派生
- 651
- 平均合并
- 1 天 15 小时
- 30 天内合并 PR
- 17
环境准备
这个项目没有提供开发容器、Dockerfile 或贡献指南,环境需要你自己搭建:先看它的 README,通用步骤见我们的新手贡献指南。
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
spring-cloud/spring-cloud-stream 的其他 Issue
-
BindingServiceProperties.bindings regressed to a non-thread-safe TreeMap in GH-3158: concurrent first use of binding names corrupts it可能已有人在做 @AkashB23 于 1 天前认领。 未关闭
难度 2/5 1-3 小时 新手友好度 62/100
spring-cloud/spring-cloud-stream#3321 ·
维护者通常 1 天内回复
-
StreamBridge removing BindingProperties leads to wrong destination in ProvisioningProvider可能重新可做 @olegz 于 57 天前认领,目前没有进行中的 PR。 未关闭bug
spring-cloud/spring-cloud-stream#3257 · 6 条评论 · 2 个 reaction · 已指派 1 人 ·
维护者通常 1 天内回复
-
StreamBridge's hashProducerProperties produces hash collisions across different binding names, causing Partition key cannot be null可能重新可做 关联的 PR 已关闭且未合并。 未关闭
难度 4/5 3-5 天 新手友好度 48/100
spring-cloud/spring-cloud-stream#3242 · 2 条评论 ·
维护者通常 1 天内回复
-
v4.3.3 no longer adds a header "TIMESTAMP" on kafka messages consumed可能重新可做 @olegz 于 88 天前认领,目前没有进行中的 PR。 未关闭Backport 4.3.x bug
spring-cloud/spring-cloud-stream#3211 · 3 条评论 · 已指派 1 人 ·
维护者通常 1 天内回复
-
Clarify support for manual ack/nack with batch mode + DLQ; potential bug in acknowledge(index) path未关闭
难度 4/5 3-5 天 新手友好度 48/100
spring-cloud/spring-cloud-stream#3189 · 3 条评论 · 1 个 reaction ·
维护者通常 1 天内回复
查看 spring-cloud/spring-cloud-stream 的全部 Issue
相似的 Issue
-
5.0.x backport enhancement
难度 2/5 1-3 小时 新手友好度 63/100
spring-cloud/spring-cloud-function#1507 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 65/100
open-learning-exchange/myplanet#18484 ·
维护者通常 1 天内回复
-
ai-driven-fuzzing bug claude Minor
难度 2/5 1-3 小时 新手友好度 66/100
linagora/twake-calendar-side-service#1417 · 2 条评论 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 74/100
line/armeria#7109 · 3 个 reaction ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 72/100
open-feature/java-sdk-contrib#1966 ·
维护者通常 1 天内回复