Share unrouted messages amongst consumers and functions
This commit is contained in:
@@ -62,7 +62,8 @@ public class StreamAutoConfiguration {
|
|||||||
@Bean
|
@Bean
|
||||||
public SupplierInvokingMessageProducer<Object> supplierInvoker(
|
public SupplierInvokingMessageProducer<Object> supplierInvoker(
|
||||||
FunctionCatalog registry) {
|
FunctionCatalog registry) {
|
||||||
return new SupplierInvokingMessageProducer<Object>(registry, properties.getSource().getName());
|
return new SupplierInvokingMessageProducer<Object>(registry,
|
||||||
|
properties.getSource().getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -80,7 +81,8 @@ public class StreamAutoConfiguration {
|
|||||||
FunctionInspector functionInspector,
|
FunctionInspector functionInspector,
|
||||||
@Lazy CompositeMessageConverterFactory compositeMessageConverterFactory) {
|
@Lazy CompositeMessageConverterFactory compositeMessageConverterFactory) {
|
||||||
return new StreamListeningFunctionInvoker(registry, functionInspector,
|
return new StreamListeningFunctionInvoker(registry, functionInspector,
|
||||||
compositeMessageConverterFactory, properties.getDefaultRoute());
|
compositeMessageConverterFactory, properties.getDefaultRoute(),
|
||||||
|
properties.isShared());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -100,7 +102,8 @@ public class StreamAutoConfiguration {
|
|||||||
FunctionInspector functionInspector,
|
FunctionInspector functionInspector,
|
||||||
@Lazy CompositeMessageConverterFactory compositeMessageConverterFactory) {
|
@Lazy CompositeMessageConverterFactory compositeMessageConverterFactory) {
|
||||||
return new StreamListeningConsumerInvoker(registry, functionInspector,
|
return new StreamListeningConsumerInvoker(registry, functionInspector,
|
||||||
compositeMessageConverterFactory, properties.getSink().getName());
|
compositeMessageConverterFactory, properties.getSink().getName(),
|
||||||
|
properties.isShared());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -137,7 +140,8 @@ public class StreamAutoConfiguration {
|
|||||||
if (sink.matches(context, metadata)) {
|
if (sink.matches(context, metadata)) {
|
||||||
return ConditionOutcome.match("Sink is explicitly enabled");
|
return ConditionOutcome.match("Sink is explicitly enabled");
|
||||||
}
|
}
|
||||||
return ConditionOutcome.noMatch("Sink is not enabled and not available through Processor");
|
return ConditionOutcome
|
||||||
|
.noMatch("Sink is not enabled and not available through Processor");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ public class StreamConfigurationProperties {
|
|||||||
|
|
||||||
private Processor processor = new Processor();
|
private Processor processor = new Processor();
|
||||||
|
|
||||||
|
private boolean shared;
|
||||||
|
|
||||||
public static final String ROUTE_KEY = "stream_routekey";
|
public static final String ROUTE_KEY = "stream_routekey";
|
||||||
|
|
||||||
public Sink getSink() {
|
public Sink getSink() {
|
||||||
@@ -44,6 +46,14 @@ public class StreamConfigurationProperties {
|
|||||||
return this.processor;
|
return this.processor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isShared() {
|
||||||
|
return this.shared;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setShared(boolean shared) {
|
||||||
|
this.shared = shared;
|
||||||
|
}
|
||||||
|
|
||||||
public static class Sink {
|
public static class Sink {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,6 +82,7 @@ public class StreamConfigurationProperties {
|
|||||||
public void setEnabled(boolean enabled) {
|
public void setEnabled(boolean enabled) {
|
||||||
this.enabled = enabled;
|
this.enabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Source {
|
public static class Source {
|
||||||
@@ -149,10 +160,11 @@ public class StreamConfigurationProperties {
|
|||||||
public void setEnabled(boolean enabled) {
|
public void setEnabled(boolean enabled) {
|
||||||
this.enabled = enabled;
|
this.enabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getDefaultRoute() {
|
public String getDefaultRoute() {
|
||||||
return processor.getName()!=null ? processor.getName() : sink.getName();
|
return processor.getName() != null ? processor.getName() : sink.getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import org.springframework.messaging.Message;
|
|||||||
import org.springframework.messaging.converter.MessageConverter;
|
import org.springframework.messaging.converter.MessageConverter;
|
||||||
|
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Dave Syer
|
* @author Dave Syer
|
||||||
@@ -55,19 +56,21 @@ public class StreamListeningConsumerInvoker implements SmartInitializingSingleto
|
|||||||
|
|
||||||
private final Map<String, FluxMessageProcessor> processors = new HashMap<>();
|
private final Map<String, FluxMessageProcessor> processors = new HashMap<>();
|
||||||
|
|
||||||
private int count = -1;
|
private static final FluxMessageProcessor NOENDPOINT = flux -> Mono.empty();
|
||||||
|
|
||||||
private static final FluxMessageProcessor NOENDPOINT = flux -> Flux.empty();
|
|
||||||
|
|
||||||
private static final Object UNCONVERTED = new Object();
|
private static final Object UNCONVERTED = new Object();
|
||||||
|
|
||||||
|
private boolean share;
|
||||||
|
|
||||||
public StreamListeningConsumerInvoker(FunctionCatalog functionCatalog,
|
public StreamListeningConsumerInvoker(FunctionCatalog functionCatalog,
|
||||||
FunctionInspector functionInspector,
|
FunctionInspector functionInspector,
|
||||||
CompositeMessageConverterFactory converterFactory, String defaultRoute) {
|
CompositeMessageConverterFactory converterFactory, String defaultRoute,
|
||||||
|
boolean share) {
|
||||||
this.functionCatalog = functionCatalog;
|
this.functionCatalog = functionCatalog;
|
||||||
this.functionInspector = functionInspector;
|
this.functionInspector = functionInspector;
|
||||||
this.converterFactory = converterFactory;
|
this.converterFactory = converterFactory;
|
||||||
this.defaultRoute = defaultRoute;
|
this.defaultRoute = defaultRoute;
|
||||||
|
this.share = share;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -77,32 +80,35 @@ public class StreamListeningConsumerInvoker implements SmartInitializingSingleto
|
|||||||
|
|
||||||
@StreamListener
|
@StreamListener
|
||||||
public void handle(@Input(Processor.INPUT) Flux<Message<?>> input) {
|
public void handle(@Input(Processor.INPUT) Flux<Message<?>> input) {
|
||||||
input.groupBy(this::select).flatMap(group -> group.key().process(group)).subscribe();
|
input.groupBy(this::select).flatMap(group -> group.key().process(group))
|
||||||
|
.subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Flux<Message<?>> consumer(String name, Flux<Message<?>> flux) {
|
private Mono<Void> consumer(String name, Flux<Message<?>> flux) {
|
||||||
Consumer<Object> consumer = functionCatalog.lookup(Consumer.class, name);
|
Consumer<Object> consumer = functionCatalog.lookup(Consumer.class, name);
|
||||||
consumer.accept(flux.map(message -> convertInput(consumer).apply(message))
|
consumer.accept(flux.map(message -> convertInput(consumer).apply(message))
|
||||||
.filter(transformed -> transformed != UNCONVERTED));
|
.filter(transformed -> transformed != UNCONVERTED));
|
||||||
return Flux.empty();
|
return Mono.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Flux<Message<?>> balance(List<String> names, Flux<Message<?>> flux) {
|
private Mono<Void> balance(List<String> names, Flux<Message<?>> flux) {
|
||||||
if (names.isEmpty()) {
|
if (names.isEmpty()) {
|
||||||
return Flux.empty();
|
return Mono.empty();
|
||||||
}
|
}
|
||||||
String name = choose(names);
|
Flux<?> result = Flux.empty();
|
||||||
if (functionCatalog.lookup(Consumer.class, name) != null) {
|
if (names.size() > 1) {
|
||||||
return consumer(name, flux);
|
if (this.share) {
|
||||||
|
flux = flux.share();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return Mono.error(new IllegalStateException(
|
||||||
|
"Multiple matches and share disabled: " + names));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return Flux.empty();
|
for (String name : names) {
|
||||||
}
|
result = result.zipWith(consumer(name, flux));
|
||||||
|
|
||||||
private synchronized String choose(List<String> names) {
|
|
||||||
if (++count >= names.size() || count < 0) {
|
|
||||||
count = 0;
|
|
||||||
}
|
}
|
||||||
return names.get(count);
|
return result.then();
|
||||||
}
|
}
|
||||||
|
|
||||||
private FluxMessageProcessor select(Message<?> input) {
|
private FluxMessageProcessor select(Message<?> input) {
|
||||||
@@ -188,7 +194,7 @@ public class StreamListeningConsumerInvoker implements SmartInitializingSingleto
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface FluxMessageProcessor {
|
interface FluxMessageProcessor {
|
||||||
Flux<Message<?>> process(Flux<Message<?>> flux);
|
Mono<Void> process(Flux<Message<?>> flux);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,19 +60,21 @@ public class StreamListeningFunctionInvoker implements SmartInitializingSingleto
|
|||||||
|
|
||||||
private final Map<String, FluxMessageProcessor> processors = new HashMap<>();
|
private final Map<String, FluxMessageProcessor> processors = new HashMap<>();
|
||||||
|
|
||||||
private int count = -1;
|
|
||||||
|
|
||||||
private static final FluxMessageProcessor NOENDPOINT = flux -> Flux.empty();
|
private static final FluxMessageProcessor NOENDPOINT = flux -> Flux.empty();
|
||||||
|
|
||||||
private static final Object UNCONVERTED = new Object();
|
private static final Object UNCONVERTED = new Object();
|
||||||
|
|
||||||
|
private boolean share;
|
||||||
|
|
||||||
public StreamListeningFunctionInvoker(FunctionCatalog functionCatalog,
|
public StreamListeningFunctionInvoker(FunctionCatalog functionCatalog,
|
||||||
FunctionInspector functionInspector,
|
FunctionInspector functionInspector,
|
||||||
CompositeMessageConverterFactory converterFactory, String defaultRoute) {
|
CompositeMessageConverterFactory converterFactory, String defaultRoute,
|
||||||
|
boolean share) {
|
||||||
this.functionCatalog = functionCatalog;
|
this.functionCatalog = functionCatalog;
|
||||||
this.functionInspector = functionInspector;
|
this.functionInspector = functionInspector;
|
||||||
this.converterFactory = converterFactory;
|
this.converterFactory = converterFactory;
|
||||||
this.defaultRoute = defaultRoute;
|
this.defaultRoute = defaultRoute;
|
||||||
|
this.share = share;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -96,7 +98,8 @@ public class StreamListeningFunctionInvoker implements SmartInitializingSingleto
|
|||||||
result = result.map(message -> MessageUtils.unpack(function, message));
|
result = result.map(message -> MessageUtils.unpack(function, message));
|
||||||
}
|
}
|
||||||
Flux<Map<String, Object>> aggregate = headers(values);
|
Flux<Map<String, Object>> aggregate = headers(values);
|
||||||
return result.withLatestFrom(aggregate, (p, m) -> message(p, m));
|
return aggregate.withLatestFrom(result,
|
||||||
|
(map, payload) -> message(map, payload));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,10 +107,8 @@ public class StreamListeningFunctionInvoker implements SmartInitializingSingleto
|
|||||||
return flux.map(message -> message.getHeaders());
|
return flux.map(message -> message.getHeaders());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Message<?> message(Object result, Map<String, Object> headers) {
|
private Message<?> message(Map<String, Object> headers, Object result) {
|
||||||
return result instanceof Message
|
return result instanceof Message
|
||||||
// TODO: why do we have to do this? The headers should have come with the
|
|
||||||
// result.
|
|
||||||
? MessageBuilder.fromMessage((Message<?>) result)
|
? MessageBuilder.fromMessage((Message<?>) result)
|
||||||
.copyHeadersIfAbsent(headers).build()
|
.copyHeadersIfAbsent(headers).build()
|
||||||
: MessageBuilder.withPayload(result).copyHeadersIfAbsent(headers).build();
|
: MessageBuilder.withPayload(result).copyHeadersIfAbsent(headers).build();
|
||||||
@@ -115,27 +116,37 @@ public class StreamListeningFunctionInvoker implements SmartInitializingSingleto
|
|||||||
|
|
||||||
private Flux<Message<?>> consumer(String name, Flux<Message<?>> flux) {
|
private Flux<Message<?>> consumer(String name, Flux<Message<?>> flux) {
|
||||||
Consumer<Object> consumer = functionCatalog.lookup(Consumer.class, name);
|
Consumer<Object> consumer = functionCatalog.lookup(Consumer.class, name);
|
||||||
|
flux = flux.publish().refCount(2);
|
||||||
|
// The consumer will subscribe to the input flux, so we need to listen separately
|
||||||
consumer.accept(flux.map(message -> convertInput(consumer).apply(message))
|
consumer.accept(flux.map(message -> convertInput(consumer).apply(message))
|
||||||
.filter(transformed -> transformed != UNCONVERTED));
|
.filter(transformed -> transformed != UNCONVERTED));
|
||||||
return Flux.empty();
|
return flux.ignoreElements().flux();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Flux<Message<?>> balance(List<String> names, Flux<Message<?>> flux) {
|
private Flux<Message<?>> balance(List<String> names, Flux<Message<?>> flux) {
|
||||||
if (names.isEmpty()) {
|
if (names.isEmpty()) {
|
||||||
return Flux.empty();
|
return Flux.empty();
|
||||||
}
|
}
|
||||||
String name = choose(names);
|
flux = flux.hide();
|
||||||
if (functionCatalog.lookup(Consumer.class, name) != null) {
|
Flux<Message<?>> result = Flux.empty();
|
||||||
return consumer(name, flux);
|
if (names.size() > 1) {
|
||||||
|
if (this.share) {
|
||||||
|
flux = flux.publish().refCount(names.size());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return Flux.error(new IllegalStateException(
|
||||||
|
"Multiple matches and share disabled: " + names));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return function(name, flux);
|
for (String name : names) {
|
||||||
}
|
if (functionCatalog.lookup(Consumer.class, name) != null) {
|
||||||
|
result = result.mergeWith(consumer(name, flux));
|
||||||
private synchronized String choose(List<String> names) {
|
}
|
||||||
if (++count >= names.size() || count < 0) {
|
else {
|
||||||
count = 0;
|
result = result.mergeWith(function(name, flux));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return names.get(count);
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private FluxMessageProcessor select(Message<?> input) {
|
private FluxMessageProcessor select(Message<?> input) {
|
||||||
@@ -149,7 +160,8 @@ public class StreamListeningFunctionInvoker implements SmartInitializingSingleto
|
|||||||
name = stash(defaultRoute);
|
name = stash(defaultRoute);
|
||||||
}
|
}
|
||||||
if (name == null) {
|
if (name == null) {
|
||||||
Set<String> names = new LinkedHashSet<>(functionCatalog.getNames(Function.class));
|
Set<String> names = new LinkedHashSet<>(
|
||||||
|
functionCatalog.getNames(Function.class));
|
||||||
names.addAll(functionCatalog.getNames(Consumer.class));
|
names.addAll(functionCatalog.getNames(Consumer.class));
|
||||||
List<String> matches = new ArrayList<>();
|
List<String> matches = new ArrayList<>();
|
||||||
if (names.size() == 1) {
|
if (names.size() == 1) {
|
||||||
@@ -175,7 +187,6 @@ public class StreamListeningFunctionInvoker implements SmartInitializingSingleto
|
|||||||
name = stash(matches.iterator().next());
|
name = stash(matches.iterator().next());
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// TODO: do we really want this? Or maybe warn that it is happening?
|
|
||||||
return flux -> balance(matches, flux);
|
return flux -> balance(matches, flux);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
* @author Marius Bogoevici
|
* @author Marius Bogoevici
|
||||||
*/
|
*/
|
||||||
@RunWith(SpringRunner.class)
|
@RunWith(SpringRunner.class)
|
||||||
@SpringBootTest(classes = PojoStreamingMixedTests.StreamingFunctionApplication.class)
|
@SpringBootTest(classes = PojoStreamingMixedTests.StreamingFunctionApplication.class, properties = "spring.cloud.function.stream.shared=true")
|
||||||
public class PojoStreamingMixedTests {
|
public class PojoStreamingMixedTests {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@@ -69,8 +69,8 @@ public class PojoStreamingMixedTests {
|
|||||||
Message<?> result = messageCollector.forChannel(processor.output()).poll(1000,
|
Message<?> result = messageCollector.forChannel(processor.output()).poll(1000,
|
||||||
TimeUnit.MILLISECONDS);
|
TimeUnit.MILLISECONDS);
|
||||||
assertThat(result.getPayload()).isInstanceOf(Foo.class);
|
assertThat(result.getPayload()).isInstanceOf(Foo.class);
|
||||||
// 2 subscribers to the same channel so input messages are applied as round robin
|
// 2 subscribers to the same channel but input messages are sent to all
|
||||||
assertThat(collector).hasSize(1);
|
assertThat(collector).hasSize(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user