Cherry-pick type of merge the work from Artem in #580

Resolves #580
This commit is contained in:
Oleg Zhurakousky
2020-08-27 14:34:24 +02:00
parent 2607cfc34b
commit b681fb96d6
9 changed files with 610 additions and 527 deletions

View File

@@ -20,7 +20,16 @@
</properties> </properties>
<dependencies> <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-rsocket</artifactId>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-cbor</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency> <dependency>
<groupId>io.rsocket</groupId> <groupId>io.rsocket</groupId>
<artifactId>rsocket-core</artifactId> <artifactId>rsocket-core</artifactId>

View File

@@ -0,0 +1,144 @@
/*
* Copyright 2020-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.function.rsocket;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import io.rsocket.frame.FrameType;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.ByteArrayDecoder;
import org.springframework.core.codec.ByteArrayEncoder;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.Encoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.CompositeMessageCondition;
import org.springframework.messaging.handler.DestinationPatternsMessageCondition;
import org.springframework.messaging.handler.invocation.reactive.HandlerMethodArgumentResolver;
import org.springframework.messaging.handler.invocation.reactive.HandlerMethodReturnValueHandler;
import org.springframework.messaging.handler.invocation.reactive.SyncHandlerMethodArgumentResolver;
import org.springframework.messaging.rsocket.annotation.support.RSocketFrameTypeMessageCondition;
import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
import org.springframework.messaging.rsocket.annotation.support.RSocketPayloadReturnValueHandler;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.ReflectionUtils;
/**
* An {@link RSocketMessageHandler} extension for Spring Cloud Function specifics.
*
* @author Artem Bilan
*
* @since 3.1
*/
public class FunctionRSocketMessageHandler extends RSocketMessageHandler {
private static final Method FUNCTION_APPLY_METHOD =
ReflectionUtils.findMethod(Function.class, "apply", (Class<?>[]) null);
private static final RSocketFrameTypeMessageCondition REQUEST_CONDITION =
new RSocketFrameTypeMessageCondition(
FrameType.REQUEST_FNF,
FrameType.REQUEST_RESPONSE,
FrameType.REQUEST_STREAM,
FrameType.REQUEST_CHANNEL);
public FunctionRSocketMessageHandler() {
setHandlerPredicate((clazz) -> false);
}
@Override
public void afterPropertiesSet() {
setEncoders(Collections.singletonList(new ByteArrayEncoder()));
super.afterPropertiesSet();
}
public void registerFunctionHandler(Function<?, ?> function, String route) {
CompositeMessageCondition condition =
new CompositeMessageCondition(REQUEST_CONDITION,
new DestinationPatternsMessageCondition(new String[]{ route },
obtainRouteMatcher()));
registerHandlerMethod(function, FUNCTION_APPLY_METHOD, condition);
}
@Override
protected List<? extends HandlerMethodArgumentResolver> initArgumentResolvers() {
return Collections.singletonList(new MessageHandlerMethodArgumentResolver());
}
@SuppressWarnings("unchecked")
@Override
protected List<? extends HandlerMethodReturnValueHandler> initReturnValueHandlers() {
return Collections.singletonList(new FunctionRSocketPayloadReturnValueHandler((List<Encoder<?>>) getEncoders(),
getReactiveAdapterRegistry()));
}
protected static final class MessageHandlerMethodArgumentResolver implements SyncHandlerMethodArgumentResolver {
private final Decoder<byte[]> decoder = new ByteArrayDecoder();
@Override
public boolean supportsParameter(MethodParameter parameter) {
return true;
}
@SuppressWarnings("unchecked")
@Override
public Object resolveArgumentValue(MethodParameter parameter, Message<?> message) {
Flux<DataBuffer> data;
Object payload = message.getPayload();
if (payload instanceof DataBuffer) {
data = Flux.just((DataBuffer) payload);
}
else {
data = Flux.from((Publisher<DataBuffer>) payload);
}
Flux<byte[]> decoded = this.decoder.decode(data, ResolvableType.forType(byte[].class), null, null);
return MessageBuilder.createMessage(decoded, message.getHeaders());
}
}
protected static final class FunctionRSocketPayloadReturnValueHandler extends RSocketPayloadReturnValueHandler {
public FunctionRSocketPayloadReturnValueHandler(List<Encoder<?>> encoders, ReactiveAdapterRegistry registry) {
super(encoders, registry);
}
@Override
public Mono<Void> handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
Message<?> message) {
if (returnValue instanceof Publisher<?> && !message.getHeaders().containsKey(RESPONSE_HEADER)) {
return Mono.from((Publisher<?>) returnValue).then();
}
return super.handleReturnValue(returnValue, returnType, message);
}
}
}

View File

@@ -16,32 +16,32 @@
package org.springframework.cloud.function.rsocket; package org.springframework.cloud.function.rsocket;
import java.net.InetSocketAddress; import java.net.URI;
import java.util.regex.Pattern;
import io.rsocket.RSocket;
import io.rsocket.SocketAcceptor;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.rsocket.RSocketMessageHandlerCustomizer;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.rsocket.context.RSocketServerBootstrap;
import org.springframework.boot.rsocket.server.RSocketServerFactory;
import org.springframework.cloud.function.context.FunctionCatalog; import org.springframework.cloud.function.context.FunctionCatalog;
import org.springframework.cloud.function.context.FunctionProperties; import org.springframework.cloud.function.context.FunctionProperties;
import org.springframework.cloud.function.context.FunctionRegistration; import org.springframework.cloud.function.context.FunctionRegistration;
import org.springframework.cloud.function.context.FunctionRegistry; import org.springframework.cloud.function.context.FunctionRegistry;
import org.springframework.cloud.function.context.catalog.FunctionTypeUtils; import org.springframework.cloud.function.context.catalog.FunctionTypeUtils;
import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper;
import org.springframework.cloud.function.json.JsonMapper;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.GenericApplicationContext; import org.springframework.context.annotation.Primary;
import org.springframework.messaging.rsocket.RSocketRequester;
import org.springframework.messaging.rsocket.RSocketRequester.Builder;
import org.springframework.messaging.rsocket.RSocketStrategies;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@@ -55,104 +55,89 @@ import org.springframework.util.StringUtils;
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties({ FunctionProperties.class, RSocketFunctionProperties.class }) @EnableConfigurationProperties({ FunctionProperties.class, RSocketFunctionProperties.class })
@ConditionalOnProperty(name = FunctionProperties.PREFIX + ".rsocket.enabled", matchIfMissing = true) @ConditionalOnProperty(name = FunctionProperties.PREFIX + ".rsocket.enabled", matchIfMissing = true)
class RSocketAutoConfiguration { class RSocketAutoConfiguration implements ApplicationContextAware {
private static Log logger = LogFactory.getLog(RSocketAutoConfiguration.class); private static final Log LOGGER = LogFactory.getLog(RSocketAutoConfiguration.class);
@Bean private static final Pattern WS_URI_PATTERN = Pattern.compile("^(https?|wss?)://.+");
public FunctionToRSocketBinder functionToDestinationBinder(FunctionCatalog functionCatalog,
FunctionProperties functionProperties, JsonMapper jsonMapper) { private ApplicationContext applicationContext;
return new FunctionToRSocketBinder(functionCatalog, functionProperties, jsonMapper);
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
} }
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
@ConditionalOnProperty("spring.rsocket.server.port") @Primary
RSocketServerBootstrap rSocketServerBootstrap(RSocketServerFactory rSocketServerFactory, public FunctionRSocketMessageHandler functionRSocketMessageHandler(RSocketStrategies rSocketStrategies,
FunctionToRSocketBinder binder) { ObjectProvider<RSocketMessageHandlerCustomizer> customizers, FunctionCatalog functionCatalog,
return new RSocketServerBootstrap(rSocketServerFactory, SocketAcceptor.with(binder.getRSocket())); FunctionProperties functionProperties) {
FunctionRSocketMessageHandler rsocketMessageHandler = new FunctionRSocketMessageHandler();
rsocketMessageHandler.setRSocketStrategies(rSocketStrategies);
customizers.orderedStream().forEach((customizer) -> customizer.customize(rsocketMessageHandler));
registerFunctionsWithRSocketHandler(rsocketMessageHandler, functionCatalog, functionProperties);
return rsocketMessageHandler;
} }
/** private void registerFunctionsWithRSocketHandler(FunctionRSocketMessageHandler rsocketMessageHandler,
* FunctionCatalog functionCatalog, FunctionProperties functionProperties) {
*/ String definition = functionProperties.getDefinition();
static class FunctionToRSocketBinder implements InitializingBean, ApplicationContextAware { if (StringUtils.hasText(definition)) {
String rootFunctionName = registerRSocketForwardingFunctionIfNecessary(definition, functionCatalog);
private final FunctionCatalog functionCatalog; //TODO externalize content-type
private final FunctionProperties functionProperties;
private final JsonMapper jsonMapper;
private RSocketListenerFunction invocableFunction;
private GenericApplicationContext context;
FunctionToRSocketBinder(FunctionCatalog functionCatalog, FunctionProperties functionProperties, JsonMapper jsonMapper) {
this.functionCatalog = functionCatalog;
this.functionProperties = functionProperties;
this.jsonMapper = jsonMapper;
}
@Override
public void afterPropertiesSet() throws Exception {
String definition = this.functionProperties.getDefinition();
if (!StringUtils.hasText(definition)) {
FunctionInvocationWrapper f = this.functionCatalog.lookup("");
if (f != null) {
definition = f.getFunctionDefinition();
}
}
Assert.isTrue(StringUtils.hasText(definition), "Failed to determine target function for RSocket.");
this.registerRsocketForwardingFunctionIfNecessary(definition);
// TODO externalize content-type
FunctionInvocationWrapper function = functionCatalog.lookup(definition, "application/json"); FunctionInvocationWrapper function = functionCatalog.lookup(definition, "application/json");
if (function.isSupplier()) { rsocketMessageHandler.registerFunctionHandler(new RSocketListenerFunction(function), rootFunctionName);
throw new UnsupportedOperationException("Supplier is not currently supported for RSocket interaction");
}
this.invocableFunction = new RSocketListenerFunction(function, this.jsonMapper);
} }
else {
RSocket getRSocket() { functionCatalog.getNames(null)
if (this.invocableFunction == null) { .forEach((name) -> {
return null; FunctionInvocationWrapper function = functionCatalog.lookup(name, "application/json");
} rsocketMessageHandler.registerFunctionHandler(new RSocketListenerFunction(function), name);
return this.invocableFunction.getRsocket(); });
} }
}
@SuppressWarnings({ "rawtypes", "unchecked" }) private String registerRSocketForwardingFunctionIfNecessary(String definition, FunctionCatalog functionCatalog) {
private void registerRsocketForwardingFunctionIfNecessary(String definition) { String[] names = StringUtils.delimitedListToStringArray(definition.replaceAll(",", "|").trim(), "|");
String[] names = StringUtils.delimitedListToStringArray(definition.replaceAll(",", "|").trim(), "|"); String rootFunctionName = names[0];
for (String name : names) {
for (String name : names) { if (!this.applicationContext.containsBean(name)) { // this means RSocket
if (!this.context.containsBean(name)) { // this means RSocket if (LOGGER.isDebugEnabled()) {
if (logger.isDebugEnabled()) { LOGGER.debug("Registering RSocket forwarder for '" + name + "' function.");
logger.debug("Registering rsocket forwarder for '" + name + "' function.");
}
String[] functionToRSocketDefinition = StringUtils.delimitedListToStringArray(name, ">");
Assert.isTrue(functionToRSocketDefinition.length == 2, "Must only contain one output redirect");
FunctionInvocationWrapper function = functionCatalog.lookup(functionToRSocketDefinition[0],
"application/json");
String[] hostPort = StringUtils.delimitedListToStringArray(functionToRSocketDefinition[1], ":");
InetSocketAddress outputAddress = InetSocketAddress.createUnresolved(hostPort[0],
Integer.valueOf(hostPort[1]));
RSocketForwardingFunction rsocketFunction = new RSocketForwardingFunction(function, outputAddress);
FunctionRegistration functionRegistration = new FunctionRegistration(rsocketFunction, name);
functionRegistration
.type(FunctionTypeUtils.discoverFunctionTypeFromClass(RSocketListenerFunction.class));
((FunctionRegistry) this.functionCatalog).register(functionRegistration);
} }
String[] functionToRSocketDefinition = StringUtils.delimitedListToStringArray(name, ">");
Assert.isTrue(functionToRSocketDefinition.length == 2, "Must only contain one output redirect");
FunctionInvocationWrapper function = functionCatalog.lookup(functionToRSocketDefinition[0], "application/json");
String[] hostPort = StringUtils.delimitedListToStringArray(functionToRSocketDefinition[1], ":");
rootFunctionName = function.getFunctionDefinition();
String forwardingUrl = functionToRSocketDefinition[1];
RSocketRequester rsocketRequester;
Builder rsocketRequesterBuilder = RSocketRequester.builder();
if (WS_URI_PATTERN.matcher(forwardingUrl).matches()) {
rsocketRequester = rsocketRequesterBuilder.websocket(URI.create(forwardingUrl));
}
else {
rsocketRequester = rsocketRequesterBuilder.tcp(hostPort[0], Integer.parseInt(hostPort[1]));
}
RSocketForwardingFunction rsocketFunction =
new RSocketForwardingFunction(function, rsocketRequester, null);
FunctionRegistration<RSocketForwardingFunction> functionRegistration =
new FunctionRegistration<>(rsocketFunction, name);
functionRegistration.type(
FunctionTypeUtils.discoverFunctionTypeFromClass(RSocketForwardingFunction.class));
((FunctionRegistry) functionCatalog).register(functionRegistration);
} }
} }
@Override return rootFunctionName;
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = (GenericApplicationContext) applicationContext;
}
} }
} }

View File

@@ -16,38 +16,25 @@
package org.springframework.cloud.function.rsocket; package org.springframework.cloud.function.rsocket;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.function.Function; import java.util.function.Function;
import io.rsocket.Payload;
import io.rsocket.RSocket;
import io.rsocket.core.RSocketConnector;
import io.rsocket.transport.netty.client.TcpClientTransport;
import io.rsocket.util.DefaultPayload;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;
import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper;
import org.springframework.messaging.Message; import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder; import org.springframework.messaging.rsocket.RSocketRequester;
import org.springframework.messaging.support.GenericMessage;
/** /**
* * Wrapper over an instance of target Function (represented by {@link FunctionInvocationWrapper})
* An implementation of {@link Function} to support distributed function composition. * which will use the result of the invocation of such function as an input to another RSocket
* <br> * effectively composing two functions over RSocket.
* This function wraps target function and forwards the result of * <p>
* the invocation of the target function to another RSocket returning the result of such forwarding as {@link Publisher}. * Note: the remote RSocket route is not necessary to be as a Spring Cloud Function binding.
* <br><br>
* A typical example is `spring.cloud.function.definition=uppercase>localhost:8888'.
* <br>
* In this case 'uppercase' is targetFunction which will be invoked during the call to 'apply' and the result of
* this invocation sent to RSocket reachable at localhost:8888.
* *
* @author Oleg Zhurakousky * @author Oleg Zhurakousky
* @author Artem Bilan * @author Artem Bilan
@@ -59,39 +46,37 @@ class RSocketForwardingFunction implements Function<Message<byte[]>, Publisher<M
private static final Log LOGGER = LogFactory.getLog(RSocketForwardingFunction.class); private static final Log LOGGER = LogFactory.getLog(RSocketForwardingFunction.class);
private final Mono<RSocket> rsocketMono;
private final FunctionInvocationWrapper targetFunction; private final FunctionInvocationWrapper targetFunction;
RSocketForwardingFunction(FunctionInvocationWrapper targetFunction, InetSocketAddress outputAddress) { private final RSocketRequester rSocketRequester;
// private final String remoteFunctionName;
RSocketForwardingFunction(FunctionInvocationWrapper targetFunction, RSocketRequester rsocketRequester,
String remoteFunctionName) {
this.targetFunction = targetFunction; this.targetFunction = targetFunction;
this.rsocketMono = this.rSocketRequester = rsocketRequester;
outputAddress == null // this.remoteFunctionName = remoteFunctionName;
? null
: RSocketConnector.create()
.reconnect(Retry.backoff(5, Duration.ofSeconds(1)))
.connect(TcpClientTransport.create(outputAddress));
} }
@SuppressWarnings("unchecked")
@Override @Override
public Publisher<Message<byte[]>> apply(Message<byte[]> input) { public Publisher<Message<byte[]>> apply(Message<byte[]> input) {
if (LOGGER.isDebugEnabled()) { if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Executing: " + this.targetFunction); LOGGER.debug("Executing: " + this.targetFunction);
} }
Object rawResult = this.targetFunction.apply(input); Mono<Object> targetFunctionCall = Mono.just(input)
return this.rsocketMono .map(this.targetFunction)
.flatMapMany((rsocket) -> .cast(Message.class)
rsocket.requestStream(DefaultPayload.create(((Message<byte[]>) rawResult).getPayload()))) .map(Message::getPayload);
.map(this::buildResultMessage);
}
private Message<byte[]> buildResultMessage(Payload payload) { return this.rSocketRequester
ByteBuffer payloadBuffer = payload.getData(); // .route(this.remoteFunctionName)
byte[] payloadData = new byte[payloadBuffer.remaining()]; .route("uppercase")
payloadBuffer.get(payloadData); .data(targetFunctionCall, byte[].class)
return MessageBuilder.withPayload(payloadData).build(); .retrieveFlux(byte[].class)
.map(GenericMessage::new);
} }
} }

View File

@@ -17,181 +17,93 @@
package org.springframework.cloud.function.rsocket; package org.springframework.cloud.function.rsocket;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.function.Function; import java.util.function.Function;
import io.rsocket.Payload; import io.rsocket.frame.FrameType;
import io.rsocket.RSocket;
import io.rsocket.util.DefaultPayload;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.cloud.function.context.catalog.FunctionTypeUtils; import org.springframework.cloud.function.context.catalog.FunctionTypeUtils;
import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper;
import org.springframework.cloud.function.json.JsonMapper;
import org.springframework.messaging.Message; import org.springframework.messaging.Message;
import org.springframework.messaging.rsocket.annotation.support.RSocketFrameTypeMessageCondition;
import org.springframework.messaging.support.MessageBuilder; import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.CollectionUtils;
/** /**
* Wrapper over an instance of target Function (represented by {@link FunctionInvocationWrapper}) * A function wrapper which is bound onto an RSocket route.
* which will use the result of the invocation of such function as an input to another RSocket
* effectively composing two functions over RSocket.
* *
* @author Oleg Zhurakousky * @author Oleg Zhurakousky
* @author Artem Bilan
*
* @since 3.1 * @since 3.1
*/ */
class RSocketListenerFunction implements Function<Message<byte[]>, Publisher<Message<byte[]>>> { public class RSocketListenerFunction implements Function<Message<Flux<byte[]>>, Publisher<?>> {
private static String splash = " ____ _ _______ __ ____ __ _ ___ ____ __ __ \n" +
" / __/__ ____(_)__ ___ _ / ___/ /__ __ _____/ / / __/_ _____ ____/ /_(_)__ ___ / _ \\/ __/__ ____/ /_____ / /_\n" +
" _\\ \\/ _ \\/ __/ / _ \\/ _ `/ / /__/ / _ \\/ // / _ / / _// // / _ \\/ __/ __/ / _ \\/ _ \\ / , _/\\ \\/ _ \\/ __/ '_/ -_) __/\n" +
"/___/ .__/_/ /_/_//_/\\_, / \\___/_/\\___/\\_,_/\\_,_/ /_/ \\_,_/_//_/\\__/\\__/_/\\___/_//_/ /_/|_/___/\\___/\\__/_/\\_\\\\__/\\__/ \n" +
" /_/ /___/ \n" +
"";
private static Log logger = LogFactory.getLog(RSocketListenerFunction.class);
private final FunctionInvocationWrapper targetFunction; private final FunctionInvocationWrapper targetFunction;
private RSocket rsocket; RSocketListenerFunction(FunctionInvocationWrapper targetFunction) {
private final JsonMapper jsonMapper;
RSocketListenerFunction(FunctionInvocationWrapper targetFunction, JsonMapper jsonMapper) {
this.targetFunction = targetFunction; this.targetFunction = targetFunction;
this.jsonMapper = jsonMapper;
} }
@SuppressWarnings("unchecked")
@Override @Override
public Publisher<Message<byte[]>> apply(Message<byte[]> input) { public Publisher<?> apply(Message<Flux<byte[]>> input) {
if (logger.isDebugEnabled()) { FrameType frameType = RSocketFrameTypeMessageCondition.getFrameType(input);
logger.debug("Executiing: " + this.targetFunction); switch (frameType) {
case REQUEST_FNF:
return handle(input);
case REQUEST_RESPONSE:
case REQUEST_STREAM:
case REQUEST_CHANNEL:
return handleAndReply(input);
default:
throw new UnsupportedOperationException();
} }
Object rawResult = this.targetFunction.apply(input);
return rawResult instanceof Publisher ? (Publisher<Message<byte[]>>) rawResult : Mono.just((Message<byte[]>) rawResult);
} }
public RSocket getRsocket() { @SuppressWarnings({ "unchecked", "rawtypes" })
if (this.rsocket == null) { private Mono<Void> handle(Message<Flux<byte[]>> messageToProcess) {
Type functionType = this.targetFunction.getFunctionType(); if (this.targetFunction.isConsumer()) {
Flux<?> dataFlux =
if (this.rsocket == null) { messageToProcess.getPayload()
this.rsocket = this.buildRSocket(this.targetFunction, functionType, this); .map((payload) -> MessageBuilder.createMessage(payload, messageToProcess.getHeaders()));
if (isFunctionInputReactive(this.targetFunction.getFunctionType())) {
dataFlux = dataFlux.transform((Function) this.targetFunction);
} }
this.printSplashScreen(this.targetFunction.getFunctionDefinition(), functionType); else {
dataFlux = dataFlux.doOnNext(this.targetFunction);
}
return dataFlux.then();
}
else {
return Mono.error(new IllegalStateException("Only 'Consumer' can handle 'fire-and-forget' RSocket frame."));
} }
return this.rsocket;
} }
private RSocket buildRSocket(FunctionInvocationWrapper targetFunction, Type functionType, Function<Message<byte[]>, Publisher<Message<byte[]>>> function) { @SuppressWarnings({ "unchecked", "rawtypes" })
String definition = targetFunction.getFunctionDefinition(); private Flux<?> handleAndReply(Message<Flux<byte[]>> messageToProcess) {
RSocket clientRSocket = new RSocket() { // imperative function or Function<?, Mono> = requestResponse Flux<?> dataFlux =
@Override messageToProcess.getPayload()
public Mono<Payload> requestResponse(Payload payload) { .map((payload) -> MessageBuilder.createMessage(payload, messageToProcess.getHeaders()));
if (logger.isDebugEnabled()) { if (isFunctionInputReactive(this.targetFunction.getFunctionType())) {
logger.debug("Invoking function '" + definition + "' as RSocket `requestResponse`."); dataFlux = dataFlux.transform((Function) this.targetFunction);
} }
else {
if (isFunctionReactive(functionType)) { dataFlux = dataFlux.flatMap((data) -> {
Flux<Payload> result = this.requestChannel(Flux.just(payload)); Object result = this.targetFunction.apply(data);
return Mono.from(result); return result instanceof Publisher<?>
} ? (Publisher<Message<byte[]>>) result
else { : Mono.just((Message<byte[]>) result);
Message<byte[]> inputMessage = deserealizePayload(payload); });
Mono<Message<byte[]>> result = Mono.from(function.apply(inputMessage)); }
return result.map(message -> DefaultPayload.create(message.getPayload(), jsonMapper.toJson(message.getHeaders()))); return dataFlux.cast(Message.class).map(Message::getPayload);
}
}
@Override
public Flux<Payload> requestStream(Payload payload) {
if (logger.isDebugEnabled()) {
logger.debug("Invoking function '" + definition + "' as RSocket `requestStream`.");
}
if (isFunctionReactive(functionType)) {
return this.requestChannel(Flux.just(payload));
}
else {
Message<byte[]> inputMessage = deserealizePayload(payload);
Flux<Message<byte[]>> result = Flux.from(function.apply(inputMessage));
return result.map(message -> DefaultPayload.create(message.getPayload()));
}
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public Flux<Payload> requestChannel(Publisher<Payload> payloads) {
if (logger.isDebugEnabled()) {
logger.debug("Invoking function '" + definition + "' as RSocket `requestChannel`.");
}
if (isFunctionReactive(functionType)) {
return Flux.from(payloads)
.transform(inputFlux -> inputFlux.map(payload -> deserealizePayload(payload)))
.transform((Function) targetFunction)
.transform(outputFlux -> ((Flux<Message<byte[]>>) outputFlux).map(message -> DefaultPayload.create(message.getPayload())));
}
else {
return Flux.from(payloads)
.transform(flux -> {
return flux.flatMap(payload -> {
Message<byte[]> inputMessage = deserealizePayload(payload);
Flux<Message<byte[]>> result = Flux.from(function.apply(inputMessage));
return result;
});
})
.doOnNext(System.out::println)
.transform(outputFlux -> outputFlux.map(message -> DefaultPayload.create(message.getPayload())));
}
}
};
return clientRSocket;
} }
private static boolean isFunctionReactive(Type functionType) { private static boolean isFunctionInputReactive(Type functionType) {
Type inputType = FunctionTypeUtils.getInputType(functionType, 0); Type inputType = FunctionTypeUtils.getInputType(functionType, 0);
Type outputType = FunctionTypeUtils.getOutputType(functionType, 0); return FunctionTypeUtils.isPublisher(inputType);
return FunctionTypeUtils.isPublisher(inputType) && FunctionTypeUtils.isFlux(outputType);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private Message<byte[]> deserealizePayload(Payload payload) {
ByteBuffer buffer = payload.getData();
byte[] rawData = new byte[buffer.remaining()];
buffer.get(rawData);
Map<String, Object> headers = null;
if (payload.hasMetadata()) {
try {
ByteBuffer metadata = payload.getMetadata();
byte[] metadataBytes = new byte[metadata.remaining()];
metadata.get(metadataBytes);
headers = this.jsonMapper.fromJson(metadataBytes, Map.class);
}
catch (Exception e) {
//throw new IllegalStateException(e);
logger.warn("Failed to extract headers from metadata", e);
}
}
MessageBuilder builder = MessageBuilder.withPayload(rawData);
if (!CollectionUtils.isEmpty(headers)) {
builder.copyHeaders(headers);
}
Message<byte[]> inputMessage = builder.build();
return inputMessage;
}
private void printSplashScreen(String definition, Type type) {
System.out.println(splash);
System.out.println("Function Definition: " + definition + "; T[" + type + "]");
System.out.println("======================================================\n");
} }
} }

View File

@@ -16,7 +16,6 @@
package org.springframework.cloud.function.rsocket; package org.springframework.cloud.function.rsocket;
import io.rsocket.SocketAcceptor;
import io.rsocket.routing.client.spring.RoutingClientAutoConfiguration; import io.rsocket.routing.client.spring.RoutingClientAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureAfter;
@@ -24,7 +23,6 @@ import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.function.context.FunctionProperties; import org.springframework.cloud.function.context.FunctionProperties;
import org.springframework.cloud.function.rsocket.RSocketAutoConfiguration.FunctionToRSocketBinder;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.rsocket.RSocketConnectorConfigurer; import org.springframework.messaging.rsocket.RSocketConnectorConfigurer;
@@ -45,8 +43,8 @@ class RSocketRoutingAutoConfiguration {
@Bean @Bean
public RSocketConnectorConfigurer functionRSocketConnectorConfigurer( public RSocketConnectorConfigurer functionRSocketConnectorConfigurer(
FunctionToRSocketBinder binder) { FunctionRSocketMessageHandler handler) {
return connector -> connector.acceptor(SocketAcceptor.with(binder.getRSocket())); return connector -> connector.acceptor(handler.responder());
} }
} }

View File

@@ -18,23 +18,25 @@ package org.springframework.cloud.function.rsocket;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier;
import io.rsocket.Payload; import org.junit.jupiter.api.Disabled;
import io.rsocket.util.DefaultPayload;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks;
import reactor.test.StepVerifier; import reactor.test.StepVerifier;
import org.springframework.boot.WebApplicationType; import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ApplicationContext; import org.springframework.boot.rsocket.context.RSocketServerBootstrap;
import org.springframework.boot.rsocket.server.RSocketServer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.messaging.rsocket.RSocketRequester; import org.springframework.messaging.rsocket.RSocketRequester;
import org.springframework.util.Assert; import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.SocketUtils; import org.springframework.util.SocketUtils;
/** /**
@@ -44,235 +46,342 @@ import org.springframework.util.SocketUtils;
*/ */
public class RSocketAutoConfigurationTests { public class RSocketAutoConfigurationTests {
@Test @Test
public void testImperativeFunctionAsRequestReply() throws Exception { public void testImperativeFunctionAsRequestReply() {
int port = SocketUtils.findAvailableTcpPort(); int port = SocketUtils.findAvailableTcpPort();
ApplicationContext context = new SpringApplicationBuilder(SampleFunctionConfiguration.class).web(WebApplicationType.NONE).run( try (
"--logging.level.org.springframework.cloud.function=DEBUG", ConfigurableApplicationContext applicationContext =
"--spring.cloud.function.definition=uppercase", new SpringApplicationBuilder(SampleFunctionConfiguration.class)
"--spring.rsocket.server.port=" + port); .web(WebApplicationType.NONE)
.run("--logging.level.org.springframework.cloud.function=DEBUG",
"--spring.cloud.function.definition=uppercase",
"--spring.rsocket.server.port=" + port);
) {
RSocketRequester.Builder rsocketRequesterBuilder =
applicationContext.getBean(RSocketRequester.Builder.class);
RSocketRequester requester = context.getBean(RSocketRequester.class); rsocketRequesterBuilder.tcp("localhost", port)
Mono<String> result = requester.rsocket().requestResponse(DefaultPayload.create("\"hello\"")).map(Payload::getDataUtf8); .route("uppercase")
.data("\"hello\"")
StepVerifier .retrieveMono(String.class)
.create(result) .as(StepVerifier::create)
.expectNext("\"HELLO\"") .expectNext("\"HELLO\"")
.expectComplete() .expectComplete()
.verify(); .verify();
}
} }
@Test @Test
public void testImperativeFunctionAsRequestReplyWithMetadata() throws Exception { public void testSupplierAsRequestReply() {
int port = SocketUtils.findAvailableTcpPort(); int port = SocketUtils.findAvailableTcpPort();
ApplicationContext context = new SpringApplicationBuilder(SampleFunctionConfiguration.class).web(WebApplicationType.NONE).run( try (
"--logging.level.org.springframework.cloud.function=DEBUG", ConfigurableApplicationContext applicationContext =
"--spring.cloud.function.definition=uppercase", new SpringApplicationBuilder(SampleFunctionConfiguration.class)
"--spring.rsocket.server.port=" + port); .web(WebApplicationType.NONE)
.run("--logging.level.org.springframework.cloud.function=DEBUG",
"--spring.cloud.function.definition=source",
"--spring.rsocket.server.port=" + port);
) {
RSocketRequester.Builder rsocketRequesterBuilder =
applicationContext.getBean(RSocketRequester.Builder.class);
RSocketRequester requester = context.getBean(RSocketRequester.class); rsocketRequesterBuilder.tcp("localhost", port)
Mono<String> result = requester.rsocket().requestResponse(DefaultPayload.create("\"hello\"", "{\"name\":\"bob\", \"age\":23}")) .route("source")
.map(payload -> { .data("\"hello\"")
Assert.hasText(payload.getMetadataUtf8(), "Metadata must not be null"); .retrieveMono(String.class)
return payload.getDataUtf8(); .as(StepVerifier::create)
}); .expectNext("\"test data\"")
.expectComplete()
StepVerifier .verify();
.create(result) }
.expectNext("\"HELLO\"")
.expectComplete()
.verify();
} }
@Test @Test
public void testImperativeFunctionAsRequestStream() throws Exception { public void testImperativeFunctionAsRequestStream() {
int port = SocketUtils.findAvailableTcpPort(); int port = SocketUtils.findAvailableTcpPort();
ApplicationContext context = new SpringApplicationBuilder(SampleFunctionConfiguration.class).web(WebApplicationType.NONE).run( try (
"--logging.level.org.springframework.cloud.function=DEBUG", ConfigurableApplicationContext applicationContext =
"--spring.cloud.function.definition=uppercase", new SpringApplicationBuilder(SampleFunctionConfiguration.class)
"--spring.rsocket.server.port=" + port); .web(WebApplicationType.NONE)
.run("--logging.level.org.springframework.cloud.function=DEBUG",
"--spring.cloud.function.definition=uppercase",
"--spring.rsocket.server.port=" + port);
) {
RSocketRequester.Builder rsocketRequesterBuilder =
applicationContext.getBean(RSocketRequester.Builder.class);
RSocketRequester requester = context.getBean(RSocketRequester.class); rsocketRequesterBuilder.tcp("localhost", port)
Flux<String> result = requester.rsocket().requestStream(DefaultPayload.create("\"hello\"")).map(Payload::getDataUtf8); .route("uppercase")
.data("\"hello\"")
StepVerifier .retrieveFlux(String.class)
.create(result) .as(StepVerifier::create)
.expectNext("\"HELLO\"") .expectNext("\"HELLO\"")
.expectComplete() .expectComplete()
.verify(); .verify();
}
} }
@Test @Test
public void testImperativeFunctionAsRequestChannel() throws Exception { public void testImperativeFunctionAsRequestChannel() {
int port = SocketUtils.findAvailableTcpPort(); int port = SocketUtils.findAvailableTcpPort();
ApplicationContext context = new SpringApplicationBuilder(SampleFunctionConfiguration.class).web(WebApplicationType.NONE).run( try (
"--logging.level.org.springframework.cloud.function=DEBUG", ConfigurableApplicationContext applicationContext =
"--spring.cloud.function.definition=uppercase", new SpringApplicationBuilder(SampleFunctionConfiguration.class)
"--spring.rsocket.server.port=" + port); .web(WebApplicationType.NONE)
.run("--logging.level.org.springframework.cloud.function=DEBUG",
"--spring.cloud.function.definition=uppercase",
"--spring.rsocket.server.port=" + port);
) {
RSocketRequester.Builder rsocketRequesterBuilder =
applicationContext.getBean(RSocketRequester.Builder.class);
RSocketRequester requester = context.getBean(RSocketRequester.class); rsocketRequesterBuilder.tcp("localhost", port)
Flux<String> result = requester.rsocket().requestChannel(Flux.just( .route("uppercase")
DefaultPayload.create("\"Ricky\""), .data(Flux.just("\"Ricky\"", "\"Julien\"", "\"Bubbles\""))
DefaultPayload.create("\"Julien\""), .retrieveFlux(String.class)
DefaultPayload.create("\"Bubbles\"")) .as(StepVerifier::create)
) .expectNext("\"RICKY\"", "\"JULIEN\"", "\"BUBBLES\"")
.map(Payload::getDataUtf8); .expectComplete()
.verify();
StepVerifier.create(result) }
.expectNext("\"RICKY\"")
.expectNext("\"JULIEN\"")
.expectNext("\"BUBBLES\"")
.expectComplete()
.verify();
} }
@Test @Test
public void testReactiveFunctionAsRequestReply() throws Exception { public void testReactiveFunctionAsRequestReply() {
int port = SocketUtils.findAvailableTcpPort(); int port = SocketUtils.findAvailableTcpPort();
ApplicationContext context = new SpringApplicationBuilder(SampleFunctionConfiguration.class).web(WebApplicationType.NONE).run( try (
"--logging.level.org.springframework.cloud.function=DEBUG", ConfigurableApplicationContext applicationContext =
"--spring.cloud.function.definition=uppercaseReactive", new SpringApplicationBuilder(SampleFunctionConfiguration.class)
"--spring.rsocket.server.port=" + port); .web(WebApplicationType.NONE)
.run("--logging.level.org.springframework.cloud.function=DEBUG",
"--spring.cloud.function.definition=uppercaseReactive",
"--spring.rsocket.server.port=" + port);
) {
RSocketRequester.Builder rsocketRequesterBuilder =
applicationContext.getBean(RSocketRequester.Builder.class);
RSocketRequester requester = context.getBean(RSocketRequester.class); rsocketRequesterBuilder.tcp("localhost", port)
Mono<String> result = requester.rsocket().requestResponse(DefaultPayload.create("\"hello\"")).map(Payload::getDataUtf8); .route("uppercaseReactive")
.data("\"hello\"")
StepVerifier .retrieveMono(String.class)
.create(result) .as(StepVerifier::create)
.expectNext("\"HELLO\"") .expectNext("\"HELLO\"")
.expectComplete() .expectComplete()
.verify(); .verify();
}
} }
@Test @Test
public void testReactiveFunctionAsRequestStream() throws Exception { public void testReactiveFunctionAsRequestStream() {
int port = SocketUtils.findAvailableTcpPort(); int port = SocketUtils.findAvailableTcpPort();
ApplicationContext context = new SpringApplicationBuilder(SampleFunctionConfiguration.class).web(WebApplicationType.NONE).run( try (
"--logging.level.org.springframework.cloud.function=DEBUG", ConfigurableApplicationContext applicationContext =
"--spring.cloud.function.definition=uppercaseReactive", new SpringApplicationBuilder(SampleFunctionConfiguration.class)
"--spring.rsocket.server.port=" + port); .web(WebApplicationType.NONE)
.run("--logging.level.org.springframework.cloud.function=DEBUG",
"--spring.cloud.function.definition=uppercaseReactive",
"--spring.rsocket.server.port=" + port);
) {
RSocketRequester.Builder rsocketRequesterBuilder =
applicationContext.getBean(RSocketRequester.Builder.class);
RSocketRequester requester = context.getBean(RSocketRequester.class); rsocketRequesterBuilder.tcp("localhost", port)
Flux<String> result = requester.rsocket().requestStream(DefaultPayload.create("\"hello\"")).map(Payload::getDataUtf8); .route("uppercaseReactive")
.data("\"hello\"")
StepVerifier .retrieveFlux(String.class)
.create(result) .as(StepVerifier::create)
.expectNext("\"HELLO\"") .expectNext("\"HELLO\"")
.expectComplete() .expectComplete()
.verify(); .verify();
}
} }
@Test @Test
public void testReactiveFunctionAsRequestChannel() throws Exception { public void testReactiveFunctionAsRequestChannel() {
int port = SocketUtils.findAvailableTcpPort(); int port = SocketUtils.findAvailableTcpPort();
ApplicationContext context = new SpringApplicationBuilder(SampleFunctionConfiguration.class).web(WebApplicationType.NONE).run( try (
"--logging.level.org.springframework.cloud.function=DEBUG", ConfigurableApplicationContext applicationContext =
"--spring.cloud.function.definition=uppercaseReactive", new SpringApplicationBuilder(SampleFunctionConfiguration.class)
"--spring.rsocket.server.port=" + port); .web(WebApplicationType.NONE)
.run("--logging.level.org.springframework.cloud.function=DEBUG",
"--spring.cloud.function.definition=uppercaseReactive",
"--spring.rsocket.server.port=" + port);
) {
RSocketRequester.Builder rsocketRequesterBuilder =
applicationContext.getBean(RSocketRequester.Builder.class);
RSocketRequester requester = context.getBean(RSocketRequester.class); rsocketRequesterBuilder.tcp("localhost", port)
Flux<String> result = requester.rsocket().requestChannel(Flux.just( .route("uppercaseReactive")
DefaultPayload.create("\"Ricky\""), .data(Flux.just("\"Ricky\"", "\"Julien\"", "\"Bubbles\""))
DefaultPayload.create("\"Julien\""), .retrieveFlux(String.class)
DefaultPayload.create("\"Bubbles\"")) .as(StepVerifier::create)
) .expectNext("\"RICKY\"", "\"JULIEN\"", "\"BUBBLES\"")
.map(Payload::getDataUtf8); .expectComplete()
.verify();
StepVerifier }
.create(result)
.expectNext("\"RICKY\"")
.expectNext("\"JULIEN\"")
.expectNext("\"BUBBLES\"")
.expectComplete()
.verify();
} }
@Disabled
@Test @Test
public void testRequestReplyFunctionWithComposition() throws Exception { public void testRequestReplyFunctionWithComposition() {
int portA = SocketUtils.findAvailableTcpPort(); int portA = SocketUtils.findAvailableTcpPort();
int portB = SocketUtils.findAvailableTcpPort(); int portB = SocketUtils.findAvailableTcpPort();
new SpringApplicationBuilder(SampleFunctionConfiguration.class).web(WebApplicationType.NONE).run( try (
"--logging.level.org.springframework.cloud.function=DEBUG", ConfigurableApplicationContext applicationContext =
"--spring.cloud.function.definition=uppercase|concat", new SpringApplicationBuilder(SampleFunctionConfiguration.class)
"--spring.rsocket.server.port=" + portA); .web(WebApplicationType.NONE)
.run("--logging.level.org.springframework.cloud.function=DEBUG",
"--spring.cloud.function.definition=uppercase|concat",
"--spring.rsocket.server.port=" + portA);
) {
ApplicationContext bContext = new SpringApplicationBuilder(AdditionalFunctionConfiguration.class).web(WebApplicationType.NONE).run( try (
"--logging.level.org.springframework.cloud.function=DEBUG", ConfigurableApplicationContext applicationContext2 =
"--spring.cloud.function.definition=reverse>localhost:" + portA + "|wrap", new SpringApplicationBuilder(AdditionalFunctionConfiguration.class)
"--spring.rsocket.server.port=" + portB); .web(WebApplicationType.NONE)
.run("--logging.level.org.springframework.cloud.function=DEBUG",
"--spring.cloud.function.definition=reverse>localhost:" + portA + "|wrap",
"--spring.rsocket.server.port=" + portB);
) {
RSocketRequester requester = bContext.getBean(RSocketRequester.class); RSocketRequester.Builder rsocketRequesterBuilder =
applicationContext2.getBean(RSocketRequester.Builder.class);
Mono<String> result = requester.rsocket().requestResponse(DefaultPayload.create("\"hello\"")).map(Payload::getDataUtf8); rsocketRequesterBuilder.tcp("localhost", portB)
StepVerifier .route("reverse")
.create(result) .data("\"hello\"")
.expectNext("\"(OLLEHOLLEH)\"") .retrieveMono(String.class)
.expectComplete() .as(StepVerifier::create)
.verify(); .expectNext("\"(OLLEHOLLEH)\"")
.expectComplete()
.verify();
}
}
}
@Disabled("TODO")
@Test
public void testCompositionOverWebSocket() {
try (
ConfigurableApplicationContext applicationContext =
new SpringApplicationBuilder(SampleFunctionConfiguration.class)
.web(WebApplicationType.REACTIVE)
.run("--logging.level.org.springframework.cloud.function=DEBUG",
"--spring.cloud.function.definition=uppercase|concat",
"--spring.rsocket.server.transport=websocket",
"--spring.rsocket.server.mapping-path=rsockets",
"--server.port=0");
) {
ConfigurableEnvironment environment = applicationContext.getEnvironment();
String httpServerPort = environment.getProperty("local.server.port");
try (
ConfigurableApplicationContext applicationContext2 =
new SpringApplicationBuilder(AdditionalFunctionConfiguration.class)
.web(WebApplicationType.NONE)
.run("--logging.level.org.springframework.cloud.function=DEBUG",
"--spring.cloud.function.definition=reverse>http://localhost:" + httpServerPort + "/rsockets/uppercase|wrap",
"--spring.rsocket.server.port=0");
) {
RSocketServerBootstrap serverBootstrap = applicationContext2.getBean(RSocketServerBootstrap.class);
RSocketServer server = (RSocketServer) ReflectionTestUtils.getField(serverBootstrap, "server");
RSocketRequester.Builder rsocketRequesterBuilder =
applicationContext2.getBean(RSocketRequester.Builder.class);
rsocketRequesterBuilder.tcp("localhost", server.address().getPort())
.route("reverse")
.data("\"hello\"")
.retrieveMono(String.class)
.as(StepVerifier::create)
.expectNext("\"(OLLEHOLLEH)\"")
.expectComplete()
.verify();
}
}
} }
@Test @Test
public void testRequestChannelFunction() throws Exception { public void testFireAndForgetConsumer() {
int port = SocketUtils.findAvailableTcpPort(); try (
ApplicationContext context = new SpringApplicationBuilder(SampleFunctionConfiguration.class).web(WebApplicationType.NONE).run( ConfigurableApplicationContext applicationContext =
"--logging.level.org.springframework.cloud.function=DEBUG", new SpringApplicationBuilder(SampleFunctionConfiguration.class)
"--spring.cloud.function.definition=uppercaseReactive", .web(WebApplicationType.NONE)
"--spring.rsocket.server.port=" + port); .run("--logging.level.org.springframework.cloud.function=DEBUG",
"--spring.cloud.function.definition=log",
"--spring.rsocket.server.port=0");
) {
RSocketRequester.Builder rsocketRequesterBuilder =
applicationContext.getBean(RSocketRequester.Builder.class);
RSocketServerBootstrap serverBootstrap = applicationContext.getBean(RSocketServerBootstrap.class);
RSocketServer server = (RSocketServer) ReflectionTestUtils.getField(serverBootstrap, "server");
RSocketRequester requester = context.getBean(RSocketRequester.class); rsocketRequesterBuilder.tcp("localhost", server.address().getPort())
Flux<String> result = requester.rsocket().requestChannel(Flux.just( .route("log")
DefaultPayload.create("\"Ricky\""), .data("\"hello\"")
DefaultPayload.create("\"Julien\""), .send()
DefaultPayload.create("\"Bubbles\"")) .as(StepVerifier::create)
) .expectComplete()
.map(Payload::getDataUtf8); .verify();
StepVerifier applicationContext.getBean(SampleFunctionConfiguration.class).consumerData
.create(result) .asMono()
.expectNext("\"RICKY\"") .map(String::new)
.expectNext("\"JULIEN\"") .as(StepVerifier::create)
.expectNext("\"BUBBLES\"") .expectNext("\"hello\"")
.expectComplete() .expectComplete()
.verify(); .verify();
}
} }
@Test
public void testRsocketRoutesForAllFunctions() {
try (
ConfigurableApplicationContext applicationContext =
new SpringApplicationBuilder(AdditionalFunctionConfiguration.class)
.web(WebApplicationType.NONE)
.run("--logging.level.org.springframework.cloud.function=DEBUG",
"--spring.rsocket.server.port=0");
) {
RSocketRequester.Builder rsocketRequesterBuilder =
applicationContext.getBean(RSocketRequester.Builder.class);
RSocketServerBootstrap serverBootstrap = applicationContext.getBean(RSocketServerBootstrap.class);
RSocketServer server = (RSocketServer) ReflectionTestUtils.getField(serverBootstrap, "server");
RSocketRequester requester = rsocketRequesterBuilder.tcp("localhost", server.address().getPort());
requester.route("reverse")
.data("\"hello\"")
.retrieveMono(String.class)
.as(StepVerifier::create)
.expectNext("\"olleh\"")
.expectComplete()
.verify();
requester.route("wrap")
.data("\"hello\"")
.retrieveMono(String.class)
.as(StepVerifier::create)
.expectNext("\"(hello)\"")
.expectComplete()
.verify();
}
}
// @Test
// public void testFireAndForgetConsumer() throws Exception {
// new SpringApplicationBuilder(SampleFunctionConfiguration.class)
// .run("--logging.level.org.springframework.cloud.function=DEBUG",
// "--spring.cloud.function.definition=log");
//
// RSocket socket = RSocketConnector.connectWith(TcpClientTransport.create("localhost", 7000))
// .log()
// .retryWhen(Retry.backoff(5, Duration.ofSeconds(1)))
// .block();
// socket.fireAndForget(DefaultPayload.create("Hello"))
// .log()
// .onErrorContinue((e, x) -> {
// System.out.println(e);
// })
// .block();
// Thread.sleep(2000);
// System.out.println();
// }
@EnableAutoConfiguration @EnableAutoConfiguration
@Configuration @Configuration
@Import(RSocketTestConfiguration.class)
public static class SampleFunctionConfiguration { public static class SampleFunctionConfiguration {
final Sinks.One<byte[]> consumerData = Sinks.one();
@Bean @Bean
public Function<String, String> uppercase() { public Function<String, String> uppercase() {
return v -> { return String::toUpperCase;
return v.toUpperCase();
};
} }
@Bean @Bean
public Function<String, String> concat() { public Function<String, String> concat() {
return v -> { return v -> v + v;
return v + v;
};
} }
@Bean @Bean
@@ -290,28 +399,30 @@ public class RSocketAutoConfigurationTests {
@Bean @Bean
public Consumer<byte[]> log() { public Consumer<byte[]> log() {
return v -> { return this.consumerData::emitValue;
System.out.println("==> In Consumer: " + new String(v));
};
} }
@Bean
public Supplier<String> source() {
return () -> "test data";
}
} }
@EnableAutoConfiguration @EnableAutoConfiguration
@Configuration @Configuration
@Import(RSocketTestConfiguration.class)
public static class AdditionalFunctionConfiguration { public static class AdditionalFunctionConfiguration {
@Bean @Bean
public Function<String, String> reverse() { public Function<String, String> reverse() {
return v -> { return v -> new StringBuilder(v).reverse().toString();
return new StringBuilder(v).reverse().toString();
};
} }
@Bean @Bean
public Function<String, String> wrap() { public Function<String, String> wrap() {
return v -> { return v -> "(" + v + ")";
return "(" + v + ")";
};
} }
} }
} }

View File

@@ -1,61 +0,0 @@
/*
* Copyright 2020-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.function.rsocket;
import java.net.InetSocketAddress;
import java.time.Duration;
import io.rsocket.RSocket;
import io.rsocket.core.RSocketConnector;
import io.rsocket.transport.netty.client.TcpClientTransport;
import reactor.util.retry.Retry;
import reactor.util.retry.RetrySpec;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.core.env.Environment;
import org.springframework.lang.Nullable;
import org.springframework.messaging.rsocket.RSocketRequester;
import org.springframework.messaging.rsocket.RSocketStrategies;
import org.springframework.util.Assert;
import org.springframework.util.MimeTypeUtils;
/**
*
* @author Oleg Zhurakousky
*
*/
@Configuration
public class RSocketTestConfiguration {
@Bean
@Scope("prototype")
RSocketRequester rSocketRequester(RSocketStrategies rSocketStrategies, Environment environment,
@Nullable RetrySpec retrySpec) {
String port = environment.getProperty("spring.rsocket.server.port");
Assert.hasText(port, "'spring.rsocket.server.port' must be specified");
String host = environment.getProperty("spring.rsocket.server.address", "localhost");
RSocket socket = RSocketConnector
.connectWith(
TcpClientTransport.create(InetSocketAddress.createUnresolved(host, Integer.parseInt(port))))
.log()
.retryWhen(retrySpec == null ? Retry.backoff(5, Duration.ofSeconds(1)) : retrySpec).block();
return RSocketRequester.wrap(socket, MimeTypeUtils.APPLICATION_JSON, MimeTypeUtils.APPLICATION_JSON,
rSocketStrategies);
}
}

View File

@@ -62,7 +62,7 @@ public class RoutingBrokerTests {
public void testRoutingWithProperty() throws Exception { public void testRoutingWithProperty() throws Exception {
this.setup(true); this.setup(true);
RSocketRequester requester = clientContext.getBean(RSocketRequester.class); RSocketRequester requester = clientContext.getBean(RSocketRequester.class);
Mono<String> result = requester.route("toupper") // used to find a messagemapping, so unused here Mono<String> result = requester.route("uppercase") // used to find a messagemapping, so unused here
// auto creates metadata // auto creates metadata
.data("\"hello\"") .data("\"hello\"")
.retrieveMono(String.class); .retrieveMono(String.class);
@@ -79,7 +79,7 @@ public class RoutingBrokerTests {
this.setup(false); this.setup(false);
RSocketRequester requester = clientContext.getBean(RSocketRequester.class); RSocketRequester requester = clientContext.getBean(RSocketRequester.class);
RoutingMetadata metadata = clientContext.getBean(RoutingMetadata.class); RoutingMetadata metadata = clientContext.getBean(RoutingMetadata.class);
Mono<String> result = requester.route("toupper") // used to find a messagemapping, so unused here Mono<String> result = requester.route("uppercase") // used to find a messagemapping, so unused here
.metadata(metadata.address("samplefn")) .metadata(metadata.address("samplefn"))
.data("\"hello\"") .data("\"hello\"")
.retrieveMono(String.class); .retrieveMono(String.class);