RSocket @MessageMapping handling

See gh-21987
This commit is contained in:
Rossen Stoyanchev
2019-02-10 14:45:16 -05:00
parent f2bb95ba7b
commit 4e78b5df2f
10 changed files with 1153 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2002-2019 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
*
* http://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.messaging.rsocket;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import io.rsocket.AbstractRSocket;
import io.rsocket.Payload;
import io.rsocket.RSocket;
import io.rsocket.plugins.RSocketInterceptor;
import io.rsocket.util.RSocketProxy;
import reactor.core.publisher.Mono;
/**
* Intercept received RSockets and count successfully completed requests seen
* on the server side. This is useful for verifying fire-and-forget
* interactions.
*
* @author Rossen Stoyanchev
*/
class FireAndForgetCountingInterceptor extends AbstractRSocket implements RSocketInterceptor {
private final List<CountingDecorator> rsockets = new CopyOnWriteArrayList<>();
public int getRSocketCount() {
return this.rsockets.size();
}
public int getFireAndForgetCount(int index) {
return this.rsockets.get(index).getFireAndForgetCount();
}
@Override
public RSocket apply(RSocket rsocket) {
CountingDecorator decorator = new CountingDecorator(rsocket);
this.rsockets.add(decorator);
return decorator;
}
private static class CountingDecorator extends RSocketProxy {
private final AtomicInteger fireAndForget = new AtomicInteger(0);
CountingDecorator(RSocket delegate) {
super(delegate);
}
public int getFireAndForgetCount() {
return this.fireAndForget.get();
}
@Override
public Mono<Void> fireAndForget(Payload payload) {
return super.fireAndForget(payload).doOnSuccess(aVoid -> this.fireAndForget.incrementAndGet());
}
}
}

View File

@@ -0,0 +1,235 @@
/*
* Copyright 2002-2019 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
*
* http://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.messaging.rsocket;
import java.time.Duration;
import java.util.Collections;
import io.rsocket.Payload;
import io.rsocket.RSocket;
import io.rsocket.RSocketFactory;
import io.rsocket.transport.netty.client.TcpClientTransport;
import io.rsocket.transport.netty.server.CloseableChannel;
import io.rsocket.transport.netty.server.TcpServerTransport;
import io.rsocket.util.DefaultPayload;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.ReplayProcessor;
import reactor.test.StepVerifier;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.codec.CharSequenceEncoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.messaging.ReactiveMessageChannel;
import org.springframework.messaging.ReactiveSubscribableChannel;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.support.DefaultReactiveMessageChannel;
import org.springframework.stereotype.Controller;
import static org.junit.Assert.*;
/**
* Server-side handling of RSocket requests.
*
* @author Rossen Stoyanchev
*/
public class RSocketClientToServerIntegrationTests {
private static AnnotationConfigApplicationContext context;
private static CloseableChannel serverChannel;
private static FireAndForgetCountingInterceptor interceptor = new FireAndForgetCountingInterceptor();
private static RSocket clientRsocket;
@BeforeClass
@SuppressWarnings("ConstantConditions")
public static void setupOnce() {
context = new AnnotationConfigApplicationContext(ServerConfig.class);
MessagingAcceptor acceptor = new MessagingAcceptor(
context.getBean("rsocketChannel", ReactiveMessageChannel.class));
serverChannel = RSocketFactory.receive()
.addServerPlugin(interceptor)
.acceptor(acceptor)
.transport(TcpServerTransport.create("localhost", 7000))
.start()
.block();
clientRsocket = RSocketFactory.connect()
.dataMimeType("text/plain")
.transport(TcpClientTransport.create("localhost", 7000))
.start()
.block();
}
@AfterClass
public static void tearDownOnce() {
clientRsocket.dispose();
serverChannel.dispose();
}
@Test
public void fireAndForget() {
Flux.range(1, 3)
.concatMap(i -> clientRsocket.fireAndForget(payload("receive", "Hello " + i)))
.blockLast();
StepVerifier.create(context.getBean(ServerController.class).fireForgetPayloads)
.expectNext("Hello 1")
.expectNext("Hello 2")
.expectNext("Hello 3")
.thenCancel()
.verify(Duration.ofSeconds(5));
assertEquals(1, interceptor.getRSocketCount());
assertEquals("Fire and forget requests did not actually complete handling on the server side",
3, interceptor.getFireAndForgetCount(0));
}
@Test
public void echo() {
Flux<String> result = Flux.range(1, 3).concatMap(i ->
clientRsocket.requestResponse(payload("echo", "Hello " + i)).map(Payload::getDataUtf8));
StepVerifier.create(result)
.expectNext("Hello 1")
.expectNext("Hello 2")
.expectNext("Hello 3")
.verifyComplete();
}
@Test
public void echoAsync() {
Flux<String> result = Flux.range(1, 3).concatMap(i ->
clientRsocket.requestResponse(payload("echo-async", "Hello " + i)).map(Payload::getDataUtf8));
StepVerifier.create(result)
.expectNext("Hello 1 async")
.expectNext("Hello 2 async")
.expectNext("Hello 3 async")
.verifyComplete();
}
@Test
public void echoStream() {
Flux<String> result = clientRsocket.requestStream(payload("echo-stream", "Hello"))
.map(io.rsocket.Payload::getDataUtf8);
StepVerifier.create(result)
.expectNext("Hello 0")
.expectNextCount(5)
.expectNext("Hello 6")
.expectNext("Hello 7")
.thenCancel()
.verify();
}
@Test
public void echoChannel() {
Flux<Payload> payloads = Flux.concat(
Flux.just(payload("echo-channel", "Hello 1")),
Flux.range(2, 9).map(i -> DefaultPayload.create("Hello " + i)));
Flux<String> result = clientRsocket.requestChannel(payloads).map(Payload::getDataUtf8);
StepVerifier.create(result)
.expectNext("Hello 1 async")
.expectNextCount(7)
.expectNext("Hello 9 async")
.expectNext("Hello 10 async")
.verifyComplete();
}
private static Payload payload(String destination, String data) {
return DefaultPayload.create(data, destination);
}
@Controller
static class ServerController {
final ReplayProcessor<String> fireForgetPayloads = ReplayProcessor.create();
@MessageMapping("receive")
void receive(String payload) {
this.fireForgetPayloads.onNext(payload);
}
@MessageMapping("echo")
String echo(String payload) {
return payload;
}
@MessageMapping("echo-async")
Mono<String> echoAsync(String payload) {
return Mono.delay(Duration.ofMillis(10)).map(aLong -> payload + " async");
}
@MessageMapping("echo-stream")
Flux<String> echoStream(String payload) {
return Flux.interval(Duration.ofMillis(10)).map(aLong -> payload + " " + aLong);
}
@MessageMapping("echo-channel")
Flux<String> echoChannel(Flux<String> payloads) {
return payloads.delayElements(Duration.ofMillis(10)).map(payload -> payload + " async");
}
}
@Configuration
static class ServerConfig {
@Bean
public ServerController controller() {
return new ServerController();
}
@Bean
public ReactiveSubscribableChannel rsocketChannel() {
return new DefaultReactiveMessageChannel();
}
@Bean
public RSocketMessageHandler rsocketMessageHandler() {
RSocketMessageHandler handler = new RSocketMessageHandler(rsocketChannel());
handler.setDecoders(Collections.singletonList(StringDecoder.allMimeTypes()));
handler.setEncoders(Collections.singletonList(CharSequenceEncoder.allMimeTypes()));
return handler;
}
}
}

View File

@@ -0,0 +1,305 @@
/*
* Copyright 2002-2019 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
*
* http://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.messaging.rsocket;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import io.rsocket.Payload;
import io.rsocket.RSocket;
import io.rsocket.RSocketFactory;
import io.rsocket.transport.netty.client.TcpClientTransport;
import io.rsocket.transport.netty.server.CloseableChannel;
import io.rsocket.transport.netty.server.TcpServerTransport;
import io.rsocket.util.DefaultPayload;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import reactor.core.publisher.ReplayProcessor;
import reactor.core.scheduler.Schedulers;
import reactor.test.StepVerifier;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.codec.CharSequenceEncoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.messaging.ReactiveMessageChannel;
import org.springframework.messaging.ReactiveSubscribableChannel;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.support.DefaultReactiveMessageChannel;
import org.springframework.stereotype.Controller;
/**
* Client-side handling of requests initiated from the server side.
*
* @author Rossen Stoyanchev
*/
public class RSocketServerToClientIntegrationTests {
private static AnnotationConfigApplicationContext context;
private static CloseableChannel serverChannel;
private static MessagingAcceptor clientAcceptor;
@BeforeClass
@SuppressWarnings("ConstantConditions")
public static void setupOnce() {
context = new AnnotationConfigApplicationContext(ServerConfig.class);
clientAcceptor = new MessagingAcceptor(
context.getBean("clientChannel", ReactiveMessageChannel.class));
MessagingAcceptor serverAcceptor = new MessagingAcceptor(
context.getBean("serverChannel", ReactiveMessageChannel.class));
serverChannel = RSocketFactory.receive()
.acceptor(serverAcceptor)
.transport(TcpServerTransport.create("localhost", 7000))
.start()
.block();
}
@AfterClass
public static void tearDownOnce() {
serverChannel.dispose();
}
@Test
public void echo() {
connectAndVerify("connect.echo");
}
@Test
public void echoAsync() {
connectAndVerify("connect.echo-async");
}
@Test
public void echoStream() {
connectAndVerify("connect.echo-stream");
}
@Test
public void echoChannel() {
connectAndVerify("connect.echo-channel");
}
private static void connectAndVerify(String destination) {
ServerController serverController = context.getBean(ServerController.class);
serverController.reset();
RSocket rsocket = null;
try {
rsocket = RSocketFactory.connect()
.setupPayload(DefaultPayload.create("", destination))
.dataMimeType("text/plain")
.acceptor(clientAcceptor)
.transport(TcpClientTransport.create("localhost", 7000))
.start()
.block();
serverController.await(Duration.ofSeconds(5));
}
finally {
if (rsocket != null) {
rsocket.dispose();
}
}
}
@Controller
@SuppressWarnings({"unused", "NullableProblems"})
static class ServerController {
// Must be initialized by @Test method...
volatile MonoProcessor<Void> result;
@MessageMapping("connect.echo")
void echo(RSocket rsocket) {
runTest(() -> {
Flux<String> result = Flux.range(1, 3).concatMap(i ->
rsocket.requestResponse(payload("echo", "Hello " + i)).map(Payload::getDataUtf8));
StepVerifier.create(result)
.expectNext("Hello 1")
.expectNext("Hello 2")
.expectNext("Hello 3")
.verifyComplete();
});
}
@MessageMapping("connect.echo-async")
void echoAsync(RSocket rsocket) {
runTest(() -> {
Flux<String> result = Flux.range(1, 3).concatMap(i ->
rsocket.requestResponse(payload("echo-async", "Hello " + i)).map(Payload::getDataUtf8));
StepVerifier.create(result)
.expectNext("Hello 1 async")
.expectNext("Hello 2 async")
.expectNext("Hello 3 async")
.verifyComplete();
});
}
@MessageMapping("connect.echo-stream")
void echoStream(RSocket rsocket) {
runTest(() -> {
Flux<String> result = rsocket.requestStream(payload("echo-stream", "Hello"))
.map(io.rsocket.Payload::getDataUtf8);
StepVerifier.create(result)
.expectNext("Hello 0")
.expectNextCount(5)
.expectNext("Hello 6")
.expectNext("Hello 7")
.thenCancel()
.verify();
});
}
@MessageMapping("connect.echo-channel")
void echoChannel(RSocket rsocket) {
runTest(() -> {
Flux<Payload> payloads = Flux.concat(
Flux.just(payload("echo-channel", "Hello 1")),
Flux.range(2, 9).map(i -> DefaultPayload.create("Hello " + i)));
Flux<String> result = rsocket.requestChannel(payloads).map(Payload::getDataUtf8);
StepVerifier.create(result)
.expectNext("Hello 1 async")
.expectNextCount(7)
.expectNext("Hello 9 async")
.expectNext("Hello 10 async")
.verifyComplete();
});
}
private void runTest(Runnable testEcho) {
Mono.fromRunnable(testEcho)
.doOnError(ex -> result.onError(ex))
.doOnSuccess(o -> result.onComplete())
.subscribeOn(Schedulers.elastic())
.subscribe();
}
private static Payload payload(String destination, String data) {
return DefaultPayload.create(data, destination);
}
public void reset() {
this.result = MonoProcessor.create();
}
public void await(Duration duration) {
this.result.block(duration);
}
}
private static class ClientController {
final ReplayProcessor<String> fireForgetPayloads = ReplayProcessor.create();
@MessageMapping("receive")
void receive(String payload) {
this.fireForgetPayloads.onNext(payload);
}
@MessageMapping("echo")
String echo(String payload) {
return payload;
}
@MessageMapping("echo-async")
Mono<String> echoAsync(String payload) {
return Mono.delay(Duration.ofMillis(10)).map(aLong -> payload + " async");
}
@MessageMapping("echo-stream")
Flux<String> echoStream(String payload) {
return Flux.interval(Duration.ofMillis(10)).map(aLong -> payload + " " + aLong);
}
@MessageMapping("echo-channel")
Flux<String> echoChannel(Flux<String> payloads) {
return payloads.delayElements(Duration.ofMillis(10)).map(payload -> payload + " async");
}
}
@Configuration
static class ServerConfig {
@Bean
public ClientController clientController() {
return new ClientController();
}
@Bean
public ServerController serverController() {
return new ServerController();
}
@Bean
public ReactiveSubscribableChannel clientChannel() {
return new DefaultReactiveMessageChannel();
}
@Bean
public ReactiveSubscribableChannel serverChannel() {
return new DefaultReactiveMessageChannel();
}
@Bean
public RSocketMessageHandler clientMessageHandler() {
List<Object> handlers = Collections.singletonList(clientController());
RSocketMessageHandler handler = new RSocketMessageHandler(clientChannel(), handlers);
addDefaultCodecs(handler);
return handler;
}
@Bean
public RSocketMessageHandler serverMessageHandler() {
RSocketMessageHandler handler = new RSocketMessageHandler(serverChannel());
addDefaultCodecs(handler);
return handler;
}
private void addDefaultCodecs(RSocketMessageHandler handler) {
handler.setDecoders(Collections.singletonList(StringDecoder.allMimeTypes()));
handler.setEncoders(Collections.singletonList(CharSequenceEncoder.allMimeTypes()));
}
}
}