Skip to content

Commit 4a8caef

Browse files
committed
GH-9999: Fix FluxMessageChannel for restarted application
Fixes: #9999 Issue link: #9999 After the application context stop, the `FluxMessageChannel` continues to try to emit messages. Just because subscriptions to the provided publishers are not cancelled. More over, we would like to come back to the production from those publishers when we start application back. * Fix `FluxMessageChannel` implementing a `Lifecycle` contract. Gather provided publisher in a local cache to come back to them when we call `start()`, essentially, initiating a new subscription to those provided publishers **Auto-cherry-pick to `6.3.x`** # Conflicts: # spring-integration-core/src/main/java/org/springframework/integration/channel/AbstractMessageChannel.java
1 parent c045873 commit 4a8caef

File tree

2 files changed

+84
-14
lines changed

2 files changed

+84
-14
lines changed

spring-integration-core/src/main/java/org/springframework/integration/channel/FluxMessageChannel.java

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package org.springframework.integration.channel;
1818

1919
import java.time.Duration;
20+
import java.util.ArrayList;
21+
import java.util.List;
2022
import java.util.concurrent.TimeUnit;
2123
import java.util.concurrent.atomic.AtomicReference;
2224
import java.util.concurrent.locks.LockSupport;
@@ -30,6 +32,7 @@
3032
import reactor.core.publisher.Sinks;
3133
import reactor.util.context.ContextView;
3234

35+
import org.springframework.context.Lifecycle;
3336
import org.springframework.core.log.LogMessage;
3437
import org.springframework.integration.IntegrationMessageHeaderAccessor;
3538
import org.springframework.integration.StaticMessageHeaderAccessor;
@@ -42,6 +45,9 @@
4245
/**
4346
* The {@link AbstractMessageChannel} implementation for the
4447
* Reactive Streams {@link Publisher} based on the Project Reactor {@link Flux}.
48+
* <p>
49+
* This class implements {@link Lifecycle} to control subscriptions to publishers
50+
* attached via {@link #subscribeTo(Publisher)}, when this channel is restarted.
4551
*
4652
* @author Artem Bilan
4753
* @author Gary Russell
@@ -50,11 +56,13 @@
5056
* @since 5.0
5157
*/
5258
public class FluxMessageChannel extends AbstractMessageChannel
53-
implements Publisher<Message<?>>, ReactiveStreamsSubscribableChannel {
59+
implements Publisher<Message<?>>, ReactiveStreamsSubscribableChannel, Lifecycle {
5460

5561
private final Sinks.Many<Message<?>> sink = Sinks.many().multicast().onBackpressureBuffer(1, false);
5662

57-
private final Disposable.Composite upstreamSubscriptions = Disposables.composite();
63+
private final List<Publisher<? extends Message<?>>> sourcePublishers = new ArrayList<>();
64+
65+
private volatile Disposable.Composite upstreamSubscriptions = Disposables.composite();
5866

5967
private volatile boolean active = true;
6068

@@ -111,19 +119,22 @@ public void subscribe(Subscriber<? super Message<?>> subscriber) {
111119
.subscribe(subscriber);
112120
}
113121

114-
private void addPublisherToSubscribe(Flux<?> publisher) {
115-
AtomicReference<Disposable> disposableReference = new AtomicReference<>();
122+
@Override
123+
public void start() {
124+
this.active = true;
125+
this.upstreamSubscriptions = Disposables.composite();
126+
this.sourcePublishers.forEach(this::doSubscribeTo);
127+
}
116128

117-
Disposable disposable =
118-
publisher
119-
.doOnTerminate(() -> disposeUpstreamSubscription(disposableReference))
120-
.subscribe();
129+
@Override
130+
public void stop() {
131+
this.active = false;
132+
this.upstreamSubscriptions.dispose();
133+
}
121134

122-
if (!disposable.isDisposed()) {
123-
if (this.upstreamSubscriptions.add(disposable)) {
124-
disposableReference.set(disposable);
125-
}
126-
}
135+
@Override
136+
public boolean isRunning() {
137+
return this.active;
127138
}
128139

129140
private void disposeUpstreamSubscription(AtomicReference<Disposable> disposableReference) {
@@ -136,8 +147,14 @@ private void disposeUpstreamSubscription(AtomicReference<Disposable> disposableR
136147

137148
@Override
138149
public void subscribeTo(Publisher<? extends Message<?>> publisher) {
150+
this.sourcePublishers.add(publisher);
151+
doSubscribeTo(publisher);
152+
}
153+
154+
private void doSubscribeTo(Publisher<? extends Message<?>> publisher) {
139155
Flux<Object> upstreamPublisher =
140156
Flux.from(publisher)
157+
.doOnComplete(() -> this.sourcePublishers.remove(publisher))
141158
.delaySubscription(
142159
Mono.fromCallable(this.sink::currentSubscriberCount)
143160
.filter((value) -> value > 0)
@@ -152,6 +169,21 @@ public void subscribeTo(Publisher<? extends Message<?>> publisher) {
152169
addPublisherToSubscribe(upstreamPublisher);
153170
}
154171

172+
private void addPublisherToSubscribe(Flux<?> publisher) {
173+
AtomicReference<Disposable> disposableReference = new AtomicReference<>();
174+
175+
Disposable disposable =
176+
publisher
177+
.doOnTerminate(() -> disposeUpstreamSubscription(disposableReference))
178+
.subscribe();
179+
180+
if (!disposable.isDisposed()) {
181+
if (this.upstreamSubscriptions.add(disposable)) {
182+
disposableReference.set(disposable);
183+
}
184+
}
185+
}
186+
155187
private void sendReactiveMessage(Message<?> message) {
156188
Message<?> messageToSend = message;
157189
// We have just restored Reactor context, so no need in a header anymore.
@@ -169,14 +201,15 @@ private void sendReactiveMessage(Message<?> message) {
169201
}
170202
}
171203
catch (Exception ex) {
172-
logger.warn(ex, LogMessage.format("Error during processing event: %s", messageToSend));
204+
logger.error(ex, LogMessage.format("Error during processing event: %s", messageToSend));
173205
}
174206
}
175207

176208
@Override
177209
public void destroy() {
178210
this.active = false;
179211
this.upstreamSubscriptions.dispose();
212+
this.sourcePublishers.clear();
180213
this.sink.emitComplete(Sinks.EmitFailureHandler.busyLooping(Duration.ofSeconds(1)));
181214
super.destroy();
182215
}

spring-integration-core/src/test/java/org/springframework/integration/dsl/reactivestreams/ReactiveStreamsTests.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838

3939
import org.springframework.beans.factory.annotation.Autowired;
4040
import org.springframework.beans.factory.annotation.Qualifier;
41+
import org.springframework.context.ConfigurableApplicationContext;
4142
import org.springframework.context.Lifecycle;
4243
import org.springframework.context.annotation.Bean;
4344
import org.springframework.context.annotation.Configuration;
@@ -266,6 +267,35 @@ void messageProducerIsNotStartedAutomatically() {
266267
.verify(Duration.ofSeconds(10));
267268
}
268269

270+
@Autowired
271+
QueueChannel fromPublisherResult;
272+
273+
@Autowired
274+
ConfigurableApplicationContext applicationContext;
275+
276+
@Test
277+
@DirtiesContext
278+
// Use disruptive this.applicationContext.start()
279+
void verifyFluxMessageChannelRestart() {
280+
for (long i = 0; i < 3L; i++) {
281+
assertThat(this.fromPublisherResult.receive(10_000)).extracting(Message::getPayload).isEqualTo(i);
282+
}
283+
284+
this.applicationContext.stop();
285+
286+
this.fromPublisherResult.purge(null);
287+
288+
this.applicationContext.start();
289+
290+
// The applicationContext restart causes all the endpoint to be started,
291+
// while we really don't have a subscription to this producer
292+
this.testMessageProducer.stop();
293+
294+
for (long i = 0; i < 3L; i++) {
295+
assertThat(this.fromPublisherResult.receive(10_000)).extracting(Message::getPayload).isEqualTo(i);
296+
}
297+
}
298+
269299
@Configuration
270300
@EnableIntegration
271301
public static class ContextConfiguration {
@@ -325,6 +355,13 @@ public Publisher<Message<String>> messageProducerFlow() {
325355
.toReactivePublisher(true);
326356
}
327357

358+
@Bean
359+
IntegrationFlow fromPublisher() {
360+
return IntegrationFlow.from(Flux.interval(Duration.ofMillis(100)).map(GenericMessage::new))
361+
.channel(c -> c.queue("fromPublisherResult"))
362+
.get();
363+
}
364+
328365
}
329366

330367
private static class TestMessageProducerSpec

0 commit comments

Comments
 (0)