GH-9999: Fix FluxMessageChannel for restarted application
Fixes: #9999 Issue link: https://github.com/spring-projects/spring-integration/issues/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
This commit is contained in:
@@ -17,6 +17,8 @@
|
||||
package org.springframework.integration.channel;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
@@ -30,6 +32,7 @@ import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.Sinks;
|
||||
import reactor.util.context.ContextView;
|
||||
|
||||
import org.springframework.context.Lifecycle;
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.integration.IntegrationMessageHeaderAccessor;
|
||||
import org.springframework.integration.StaticMessageHeaderAccessor;
|
||||
@@ -42,6 +45,9 @@ import org.springframework.util.Assert;
|
||||
/**
|
||||
* The {@link AbstractMessageChannel} implementation for the
|
||||
* Reactive Streams {@link Publisher} based on the Project Reactor {@link Flux}.
|
||||
* <p>
|
||||
* This class implements {@link Lifecycle} to control subscriptions to publishers
|
||||
* attached via {@link #subscribeTo(Publisher)}, when this channel is restarted.
|
||||
*
|
||||
* @author Artem Bilan
|
||||
* @author Gary Russell
|
||||
@@ -50,11 +56,13 @@ import org.springframework.util.Assert;
|
||||
* @since 5.0
|
||||
*/
|
||||
public class FluxMessageChannel extends AbstractMessageChannel
|
||||
implements Publisher<Message<?>>, ReactiveStreamsSubscribableChannel {
|
||||
implements Publisher<Message<?>>, ReactiveStreamsSubscribableChannel, Lifecycle {
|
||||
|
||||
private final Sinks.Many<Message<?>> sink = Sinks.many().multicast().onBackpressureBuffer(1, false);
|
||||
|
||||
private final Disposable.Composite upstreamSubscriptions = Disposables.composite();
|
||||
private final List<Publisher<? extends Message<?>>> sourcePublishers = new ArrayList<>();
|
||||
|
||||
private volatile Disposable.Composite upstreamSubscriptions = Disposables.composite();
|
||||
|
||||
private volatile boolean active = true;
|
||||
|
||||
@@ -111,6 +119,56 @@ public class FluxMessageChannel extends AbstractMessageChannel
|
||||
.subscribe(subscriber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
this.active = true;
|
||||
this.upstreamSubscriptions = Disposables.composite();
|
||||
this.sourcePublishers.forEach(this::doSubscribeTo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
this.active = false;
|
||||
this.upstreamSubscriptions.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
private void disposeUpstreamSubscription(AtomicReference<Disposable> disposableReference) {
|
||||
Disposable disposable = disposableReference.get();
|
||||
if (disposable != null) {
|
||||
this.upstreamSubscriptions.remove(disposable);
|
||||
disposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribeTo(Publisher<? extends Message<?>> publisher) {
|
||||
this.sourcePublishers.add(publisher);
|
||||
doSubscribeTo(publisher);
|
||||
}
|
||||
|
||||
private void doSubscribeTo(Publisher<? extends Message<?>> publisher) {
|
||||
Flux<Object> upstreamPublisher =
|
||||
Flux.from(publisher)
|
||||
.doOnComplete(() -> this.sourcePublishers.remove(publisher))
|
||||
.delaySubscription(
|
||||
Mono.fromCallable(this.sink::currentSubscriberCount)
|
||||
.filter((value) -> value > 0)
|
||||
.repeatWhenEmpty((repeat) ->
|
||||
this.active ? repeat.delayElements(Duration.ofMillis(100)) : repeat))
|
||||
.flatMap((message) ->
|
||||
Mono.just(message)
|
||||
.handle((messageToHandle, syncSink) -> sendReactiveMessage(messageToHandle))
|
||||
.contextWrite(StaticMessageHeaderAccessor.getReactorContext(message)))
|
||||
.contextCapture();
|
||||
|
||||
addPublisherToSubscribe(upstreamPublisher);
|
||||
}
|
||||
|
||||
private void addPublisherToSubscribe(Flux<?> publisher) {
|
||||
AtomicReference<Disposable> disposableReference = new AtomicReference<>();
|
||||
|
||||
@@ -126,32 +184,6 @@ public class FluxMessageChannel extends AbstractMessageChannel
|
||||
}
|
||||
}
|
||||
|
||||
private void disposeUpstreamSubscription(AtomicReference<Disposable> disposableReference) {
|
||||
Disposable disposable = disposableReference.get();
|
||||
if (disposable != null) {
|
||||
this.upstreamSubscriptions.remove(disposable);
|
||||
disposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribeTo(Publisher<? extends Message<?>> publisher) {
|
||||
Flux<Object> upstreamPublisher =
|
||||
Flux.from(publisher)
|
||||
.delaySubscription(
|
||||
Mono.fromCallable(this.sink::currentSubscriberCount)
|
||||
.filter((value) -> value > 0)
|
||||
.repeatWhenEmpty((repeat) ->
|
||||
this.active ? repeat.delayElements(Duration.ofMillis(100)) : repeat))
|
||||
.flatMap((message) ->
|
||||
Mono.just(message)
|
||||
.handle((messageToHandle, syncSink) -> sendReactiveMessage(messageToHandle))
|
||||
.contextWrite(StaticMessageHeaderAccessor.getReactorContext(message)))
|
||||
.contextCapture();
|
||||
|
||||
addPublisherToSubscribe(upstreamPublisher);
|
||||
}
|
||||
|
||||
private void sendReactiveMessage(Message<?> message) {
|
||||
Message<?> messageToSend = message;
|
||||
// We have just restored Reactor context, so no need in a header anymore.
|
||||
@@ -169,7 +201,7 @@ public class FluxMessageChannel extends AbstractMessageChannel
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
logger.warn(ex, LogMessage.format("Error during processing event: %s", messageToSend));
|
||||
logger.error(ex, LogMessage.format("Error during processing event: %s", messageToSend));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,6 +209,7 @@ public class FluxMessageChannel extends AbstractMessageChannel
|
||||
public void destroy() {
|
||||
this.active = false;
|
||||
this.upstreamSubscriptions.dispose();
|
||||
this.sourcePublishers.clear();
|
||||
this.sink.emitComplete(Sinks.EmitFailureHandler.busyLooping(Duration.ofSeconds(1)));
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.Lifecycle;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -266,6 +267,35 @@ public class ReactiveStreamsTests {
|
||||
.verify(Duration.ofSeconds(10));
|
||||
}
|
||||
|
||||
@Autowired
|
||||
QueueChannel fromPublisherResult;
|
||||
|
||||
@Autowired
|
||||
ConfigurableApplicationContext applicationContext;
|
||||
|
||||
@Test
|
||||
@DirtiesContext
|
||||
// Use disruptive this.applicationContext.start()
|
||||
void verifyFluxMessageChannelRestart() {
|
||||
for (long i = 0; i < 3L; i++) {
|
||||
assertThat(this.fromPublisherResult.receive(10_000)).extracting(Message::getPayload).isEqualTo(i);
|
||||
}
|
||||
|
||||
this.applicationContext.stop();
|
||||
|
||||
this.fromPublisherResult.purge(null);
|
||||
|
||||
this.applicationContext.start();
|
||||
|
||||
// The applicationContext restart causes all the endpoint to be started,
|
||||
// while we really don't have a subscription to this producer
|
||||
this.testMessageProducer.stop();
|
||||
|
||||
for (long i = 0; i < 3L; i++) {
|
||||
assertThat(this.fromPublisherResult.receive(10_000)).extracting(Message::getPayload).isEqualTo(i);
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableIntegration
|
||||
public static class ContextConfiguration {
|
||||
@@ -325,6 +355,13 @@ public class ReactiveStreamsTests {
|
||||
.toReactivePublisher(true);
|
||||
}
|
||||
|
||||
@Bean
|
||||
IntegrationFlow fromPublisher() {
|
||||
return IntegrationFlow.from(Flux.interval(Duration.ofMillis(100)).map(GenericMessage::new))
|
||||
.channel(c -> c.queue("fromPublisherResult"))
|
||||
.get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class TestMessageProducerSpec
|
||||
|
||||
Reference in New Issue
Block a user