Polish Reactor Netty TCP client support
This commit is contained in:
parent
870f61fd8e
commit
85c93f5d67
|
@ -29,11 +29,10 @@ import org.springframework.messaging.tcp.reactor.ReactorNettyTcpClient;
|
||||||
import org.springframework.util.concurrent.ListenableFuture;
|
import org.springframework.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A STOMP over TCP client that uses
|
* A STOMP over TCP client that uses {@link ReactorNettyTcpClient}.
|
||||||
* {@link ReactorNettyTcpClient}.
|
|
||||||
*
|
*
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
* @since 4.2
|
* @since 5.0
|
||||||
*/
|
*/
|
||||||
public class ReactorNettyTcpStompClient extends StompClientSupport {
|
public class ReactorNettyTcpStompClient extends StompClientSupport {
|
||||||
|
|
||||||
|
@ -99,25 +98,21 @@ public class ReactorNettyTcpStompClient extends StompClientSupport {
|
||||||
* Create a new {@link ReactorNettyTcpClient} with Stomp specific configuration for
|
* Create a new {@link ReactorNettyTcpClient} with Stomp specific configuration for
|
||||||
* encoding, decoding and hand-off.
|
* encoding, decoding and hand-off.
|
||||||
*
|
*
|
||||||
* @param relayHost target host
|
* @param host target host
|
||||||
* @param relayPort target port
|
* @param port target port
|
||||||
* @param decoder {@link StompDecoder} to use
|
* @param decoder {@link StompDecoder} to use
|
||||||
* @return a new {@link TcpOperations}
|
* @return a new {@link TcpOperations}
|
||||||
*/
|
*/
|
||||||
protected static TcpOperations<byte[]> create(String relayHost,
|
protected static TcpOperations<byte[]> create(String host, int port, StompDecoder decoder) {
|
||||||
int relayPort,
|
return new ReactorNettyTcpClient<>(host, port,
|
||||||
StompDecoder decoder) {
|
new ReactorNettyTcpClient.MessageHandlerConfiguration<>(
|
||||||
return new ReactorNettyTcpClient<>(relayHost,
|
new DecodingFunction(decoder),
|
||||||
relayPort,
|
|
||||||
new ReactorNettyTcpClient.MessageHandlerConfiguration<>(new DecodingFunction(
|
|
||||||
decoder),
|
|
||||||
new EncodingConsumer(new StompEncoder()),
|
new EncodingConsumer(new StompEncoder()),
|
||||||
128,
|
128,
|
||||||
Schedulers.newParallel("StompClient")));
|
Schedulers.newParallel("StompClient")));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class EncodingConsumer
|
private static final class EncodingConsumer implements BiConsumer<ByteBuf, Message<byte[]>> {
|
||||||
implements BiConsumer<ByteBuf, Message<byte[]>> {
|
|
||||||
|
|
||||||
private final StompEncoder encoder;
|
private final StompEncoder encoder;
|
||||||
|
|
||||||
|
@ -127,12 +122,11 @@ public class ReactorNettyTcpStompClient extends StompClientSupport {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void accept(ByteBuf byteBuf, Message<byte[]> message) {
|
public void accept(ByteBuf byteBuf, Message<byte[]> message) {
|
||||||
byteBuf.writeBytes(encoder.encode(message));
|
byteBuf.writeBytes(this.encoder.encode(message));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class DecodingFunction
|
private static final class DecodingFunction implements Function<ByteBuf, List<Message<byte[]>>> {
|
||||||
implements Function<ByteBuf, List<Message<byte[]>>> {
|
|
||||||
|
|
||||||
private final StompDecoder decoder;
|
private final StompDecoder decoder;
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
package org.springframework.messaging.tcp.reactor;
|
package org.springframework.messaging.tcp.reactor;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
|
@ -33,51 +32,53 @@ import org.springframework.util.concurrent.ListenableFutureCallbackRegistry;
|
||||||
import org.springframework.util.concurrent.SuccessCallback;
|
import org.springframework.util.concurrent.SuccessCallback;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adapts a reactor {@link Mono} to {@link ListenableFuture} optionally converting
|
* Adapts {@link Mono} to {@link ListenableFuture} optionally converting the
|
||||||
* the result Object type {@code <S>} to the expected target type {@code <T>}.
|
* result Object type {@code <S>} to the expected target type {@code <T>}.
|
||||||
*
|
*
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
* @since 4.0
|
* @since 5.0
|
||||||
* @param <S> the type of object expected from the {@link Mono}
|
* @param <S> the type of object expected from the {@link Mono}
|
||||||
* @param <T> the type of object expected from the {@link ListenableFuture}
|
* @param <T> the type of object expected from the {@link ListenableFuture}
|
||||||
*/
|
*/
|
||||||
abstract class AbstractMonoToListenableFutureAdapter<S, T>
|
abstract class AbstractMonoToListenableFutureAdapter<S, T> implements ListenableFuture<T> {
|
||||||
implements ListenableFuture<T> {
|
|
||||||
|
|
||||||
private final MonoProcessor<S> promise;
|
private final MonoProcessor<S> monoProcessor;
|
||||||
|
|
||||||
private final ListenableFutureCallbackRegistry<T> registry = new ListenableFutureCallbackRegistry<>();
|
private final ListenableFutureCallbackRegistry<T> registry = new ListenableFutureCallbackRegistry<>();
|
||||||
|
|
||||||
protected AbstractMonoToListenableFutureAdapter(Mono<S> promise) {
|
|
||||||
Assert.notNull(promise, "Mono must not be null");
|
protected AbstractMonoToListenableFutureAdapter(Mono<S> mono) {
|
||||||
this.promise = promise.doOnSuccess(result -> {
|
Assert.notNull(mono, "'mono' must not be null");
|
||||||
T adapted;
|
this.monoProcessor = mono
|
||||||
try {
|
.doOnSuccess(result -> {
|
||||||
adapted = adapt(result);
|
T adapted;
|
||||||
}
|
try {
|
||||||
catch (Throwable ex) {
|
adapted = adapt(result);
|
||||||
registry.failure(ex);
|
}
|
||||||
return;
|
catch (Throwable ex) {
|
||||||
}
|
registry.failure(ex);
|
||||||
registry.success(adapted);
|
return;
|
||||||
})
|
}
|
||||||
.doOnError(registry::failure)
|
registry.success(adapted);
|
||||||
.subscribe();
|
})
|
||||||
|
.doOnError(this.registry::failure)
|
||||||
|
.subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public T get() throws InterruptedException {
|
public T get() throws InterruptedException {
|
||||||
S result = this.promise.block();
|
S result = this.monoProcessor.block();
|
||||||
return adapt(result);
|
return adapt(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
|
public T get(long timeout, TimeUnit unit)
|
||||||
Objects.requireNonNull(unit, "unit");
|
throws InterruptedException, ExecutionException, TimeoutException {
|
||||||
S result = this.promise.block(Duration.ofMillis(TimeUnit.MILLISECONDS.convert(
|
|
||||||
timeout,
|
Assert.notNull(unit);
|
||||||
unit)));
|
Duration duration = Duration.ofMillis(TimeUnit.MILLISECONDS.convert(timeout, unit));
|
||||||
|
S result = this.monoProcessor.block(duration);
|
||||||
return adapt(result);
|
return adapt(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,18 +87,18 @@ abstract class AbstractMonoToListenableFutureAdapter<S, T>
|
||||||
if (isCancelled()) {
|
if (isCancelled()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this.promise.cancel();
|
this.monoProcessor.cancel();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isCancelled() {
|
public boolean isCancelled() {
|
||||||
return this.promise.isCancelled();
|
return this.monoProcessor.isCancelled();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isDone() {
|
public boolean isDone() {
|
||||||
return this.promise.isTerminated();
|
return this.monoProcessor.isTerminated();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -111,7 +112,6 @@ abstract class AbstractMonoToListenableFutureAdapter<S, T>
|
||||||
this.registry.addFailureCallback(failureCallback);
|
this.registry.addFailureCallback(failureCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected abstract T adapt(S result);
|
protected abstract T adapt(S result);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2015 the original author or authors.
|
* Copyright 2002-2016 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -19,15 +19,14 @@ package org.springframework.messaging.tcp.reactor;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Mono-to-ListenableFutureAdapter where the source and the target from
|
* A Mono-to-ListenableFuture adapter where the source and the target from
|
||||||
* the Promise and the ListenableFuture respectively are of the same type.
|
* the Promise and the ListenableFuture respectively are of the same type.
|
||||||
*
|
*
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
* @author Stephane Maldini
|
* @author Stephane Maldini
|
||||||
* @since 4.0
|
* @since 5.0
|
||||||
*/
|
*/
|
||||||
class MonoToListenableFutureAdapter<T> extends
|
class MonoToListenableFutureAdapter<T> extends AbstractMonoToListenableFutureAdapter<T, T> {
|
||||||
AbstractMonoToListenableFutureAdapter<T, T> {
|
|
||||||
|
|
||||||
|
|
||||||
public MonoToListenableFutureAdapter(Mono<T> mono) {
|
public MonoToListenableFutureAdapter(Mono<T> mono) {
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
package org.springframework.messaging.tcp.reactor;
|
package org.springframework.messaging.tcp.reactor;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
@ -58,16 +57,19 @@ import org.springframework.util.concurrent.ListenableFuture;
|
||||||
*
|
*
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
* @author Stephane Maldini
|
* @author Stephane Maldini
|
||||||
* @since 4.2
|
* @since 5.0
|
||||||
*/
|
*/
|
||||||
public class ReactorNettyTcpClient<P> implements TcpOperations<P> {
|
public class ReactorNettyTcpClient<P> implements TcpOperations<P> {
|
||||||
|
|
||||||
private final TcpClient tcpClient;
|
private final TcpClient tcpClient;
|
||||||
|
|
||||||
private final MessageHandlerConfiguration<P> configuration;
|
private final MessageHandlerConfiguration<P> configuration;
|
||||||
private final ChannelGroup group;
|
|
||||||
|
private final ChannelGroup group;
|
||||||
|
|
||||||
private volatile boolean stopping;
|
private volatile boolean stopping;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A constructor that creates a {@link TcpClient TcpClient} factory relying on
|
* A constructor that creates a {@link TcpClient TcpClient} factory relying on
|
||||||
* Reactor Netty TCP threads. The number of Netty threads can be tweaked with
|
* Reactor Netty TCP threads. The number of Netty threads can be tweaked with
|
||||||
|
@ -80,120 +82,116 @@ public class ReactorNettyTcpClient<P> implements TcpOperations<P> {
|
||||||
* @param port the port to connect to
|
* @param port the port to connect to
|
||||||
* @param configuration the client configuration
|
* @param configuration the client configuration
|
||||||
*/
|
*/
|
||||||
public ReactorNettyTcpClient(String host,
|
public ReactorNettyTcpClient(String host, int port, MessageHandlerConfiguration<P> configuration) {
|
||||||
int port,
|
this(opts -> opts.connect(host, port), configuration);
|
||||||
MessageHandlerConfiguration<P> configuration) {
|
|
||||||
this.configuration = Objects.requireNonNull(configuration, "configuration");
|
|
||||||
this.group = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
|
|
||||||
this.tcpClient = TcpClient.create(options -> options.connect(host, port)
|
|
||||||
.channelGroup(group));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A constructor with a configurator {@link Consumer} that will receive default {@link
|
* A constructor with a configurator {@link Consumer} that will receive
|
||||||
* ClientOptions} from {@link TcpClient}. This might be used to add SSL or specific
|
* default {@link ClientOptions} from {@link TcpClient}. This might be used
|
||||||
* network parameters to the generated client configuration.
|
* to add SSL or specific network parameters to the generated client
|
||||||
|
* configuration.
|
||||||
*
|
*
|
||||||
* @param tcpOptions the {@link Consumer} of {@link ClientOptions} shared to use by
|
* @param tcpOptions callback for configuring shared {@link ClientOptions}
|
||||||
* connected handlers.
|
|
||||||
* @param configuration the client configuration
|
* @param configuration the client configuration
|
||||||
*/
|
*/
|
||||||
public ReactorNettyTcpClient(Consumer<? super ClientOptions> tcpOptions,
|
public ReactorNettyTcpClient(Consumer<? super ClientOptions> tcpOptions,
|
||||||
MessageHandlerConfiguration<P> configuration) {
|
MessageHandlerConfiguration<P> configuration) {
|
||||||
this.configuration = Objects.requireNonNull(configuration, "configuration");
|
|
||||||
|
Assert.notNull(configuration, "'configuration' is required");
|
||||||
this.group = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
|
this.group = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
|
||||||
this.tcpClient =
|
this.tcpClient = TcpClient.create(opts -> tcpOptions.accept(opts.channelGroup(group)));
|
||||||
TcpClient.create(opts -> tcpOptions.accept(opts.channelGroup(group)));
|
this.configuration = configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ListenableFuture<Void> connect(final TcpConnectionHandler<P> connectionHandler) {
|
public ListenableFuture<Void> connect(final TcpConnectionHandler<P> handler) {
|
||||||
Assert.notNull(connectionHandler, "TcpConnectionHandler must not be null");
|
Assert.notNull(handler, "'handler' is required");
|
||||||
if (stopping) {
|
|
||||||
|
if (this.stopping) {
|
||||||
IllegalStateException ex = new IllegalStateException("Shutting down.");
|
IllegalStateException ex = new IllegalStateException("Shutting down.");
|
||||||
connectionHandler.afterConnectFailure(ex);
|
handler.afterConnectFailure(ex);
|
||||||
return new MonoToListenableFutureAdapter<>(Mono.<Void>error(ex));
|
return new MonoToListenableFutureAdapter<>(Mono.<Void>error(ex));
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageHandler<P> handler =
|
Mono<Void> connectMono = this.tcpClient
|
||||||
new MessageHandler<>(connectionHandler, configuration);
|
.newHandler(new MessageHandler<>(handler, this.configuration))
|
||||||
|
.doOnError(handler::afterConnectFailure)
|
||||||
|
.then();
|
||||||
|
|
||||||
Mono<Void> promise = tcpClient.newHandler(handler)
|
return new MonoToListenableFutureAdapter<>(connectMono);
|
||||||
.doOnError(connectionHandler::afterConnectFailure)
|
|
||||||
.then();
|
|
||||||
|
|
||||||
return new MonoToListenableFutureAdapter<>(promise);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ListenableFuture<Void> connect(TcpConnectionHandler<P> connectionHandler,
|
public ListenableFuture<Void> connect(TcpConnectionHandler<P> handler, ReconnectStrategy strategy) {
|
||||||
ReconnectStrategy strategy) {
|
Assert.notNull(handler, "'handler' is required");
|
||||||
Assert.notNull(connectionHandler, "TcpConnectionHandler must not be null");
|
Assert.notNull(strategy, "'reconnectStrategy' is required");
|
||||||
Assert.notNull(strategy, "ReconnectStrategy must not be null");
|
|
||||||
|
|
||||||
if (stopping) {
|
if (this.stopping) {
|
||||||
IllegalStateException ex = new IllegalStateException("Shutting down.");
|
IllegalStateException ex = new IllegalStateException("Shutting down.");
|
||||||
connectionHandler.afterConnectFailure(ex);
|
handler.afterConnectFailure(ex);
|
||||||
return new MonoToListenableFutureAdapter<>(Mono.<Void>error(ex));
|
return new MonoToListenableFutureAdapter<>(Mono.<Void>error(ex));
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageHandler<P> handler =
|
MonoProcessor<Void> connectMono = MonoProcessor.create();
|
||||||
new MessageHandler<>(connectionHandler, configuration);
|
|
||||||
|
|
||||||
MonoProcessor<Void> promise = MonoProcessor.create();
|
this.tcpClient.newHandler(new MessageHandler<>(handler, this.configuration))
|
||||||
|
.doOnNext(item -> {
|
||||||
|
if (!connectMono.isTerminated()) {
|
||||||
|
connectMono.onComplete();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.doOnError(ex -> {
|
||||||
|
if (!connectMono.isTerminated()) {
|
||||||
|
connectMono.onError(ex);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(NettyContext::onClose)
|
||||||
|
.retryWhen(new Reconnector<>(strategy))
|
||||||
|
.repeatWhen(new Reconnector<>(strategy))
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
tcpClient.newHandler(handler)
|
return new MonoToListenableFutureAdapter<>(connectMono);
|
||||||
.doOnNext(e -> {
|
|
||||||
if (!promise.isTerminated()) {
|
|
||||||
promise.onComplete();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.doOnError(e -> {
|
|
||||||
if (!promise.isTerminated()) {
|
|
||||||
promise.onError(e);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(NettyContext::onClose)
|
|
||||||
.retryWhen(new Reconnector<>(strategy))
|
|
||||||
.repeatWhen(new Reconnector<>(strategy))
|
|
||||||
.subscribe();
|
|
||||||
|
|
||||||
return new MonoToListenableFutureAdapter<>(promise);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ListenableFuture<Void> shutdown() {
|
public ListenableFuture<Void> shutdown() {
|
||||||
if (stopping) {
|
if (this.stopping) {
|
||||||
return new MonoToListenableFutureAdapter<>(Mono.empty());
|
return new MonoToListenableFutureAdapter<>(Mono.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
stopping = true;
|
this.stopping = true;
|
||||||
|
|
||||||
Mono<Void> closing = ChannelFutureMono.from(group.close());
|
Mono<Void> completion = ChannelFutureMono.from(this.group.close());
|
||||||
|
|
||||||
if (configuration.scheduler != null) {
|
if (this.configuration.scheduler != null) {
|
||||||
closing =
|
completion = completion.doAfterTerminate((x, e) -> configuration.scheduler.shutdown());
|
||||||
closing.doAfterTerminate((x, e) -> configuration.scheduler.shutdown());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new MonoToListenableFutureAdapter<>(closing);
|
return new MonoToListenableFutureAdapter<>(completion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A configuration holder
|
* A configuration holder
|
||||||
*/
|
*/
|
||||||
public static final class MessageHandlerConfiguration<P> {
|
public static final class MessageHandlerConfiguration<P> {
|
||||||
|
|
||||||
private final Function<? super ByteBuf, ? extends Collection<Message<P>>> decoder;
|
private final Function<? super ByteBuf, ? extends Collection<Message<P>>> decoder;
|
||||||
private final BiConsumer<? super ByteBuf, ? super Message<P>> encoder;
|
|
||||||
private final int backlog;
|
|
||||||
private final Scheduler
|
|
||||||
scheduler;
|
|
||||||
|
|
||||||
public MessageHandlerConfiguration(Function<? super ByteBuf, ? extends Collection<Message<P>>> decoder,
|
private final BiConsumer<? super ByteBuf, ? super Message<P>> encoder;
|
||||||
|
|
||||||
|
private final int backlog;
|
||||||
|
|
||||||
|
private final Scheduler scheduler;
|
||||||
|
|
||||||
|
|
||||||
|
public MessageHandlerConfiguration(
|
||||||
|
Function<? super ByteBuf, ? extends Collection<Message<P>>> decoder,
|
||||||
BiConsumer<? super ByteBuf, ? super Message<P>> encoder,
|
BiConsumer<? super ByteBuf, ? super Message<P>> encoder,
|
||||||
int backlog,
|
int backlog, Scheduler scheduler) {
|
||||||
Scheduler scheduler) {
|
|
||||||
this.decoder = decoder;
|
this.decoder = decoder;
|
||||||
this.encoder = encoder;
|
this.encoder = encoder;
|
||||||
this.backlog = backlog > 0 ? backlog : QueueSupplier.SMALL_BUFFER_SIZE;
|
this.backlog = backlog > 0 ? backlog : QueueSupplier.SMALL_BUFFER_SIZE;
|
||||||
|
@ -201,34 +199,30 @@ public class ReactorNettyTcpClient<P> implements TcpOperations<P> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class MessageHandler<P> implements BiFunction<NettyInbound, NettyOutbound, Publisher<Void>> {
|
private static final class MessageHandler<P>
|
||||||
|
implements BiFunction<NettyInbound, NettyOutbound, Publisher<Void>> {
|
||||||
|
|
||||||
private final TcpConnectionHandler<P> connectionHandler;
|
private final TcpConnectionHandler<P> connectionHandler;
|
||||||
|
|
||||||
private final MessageHandlerConfiguration<P> configuration;
|
private final MessageHandlerConfiguration<P> configuration;
|
||||||
|
|
||||||
MessageHandler(TcpConnectionHandler<P> connectionHandler,
|
|
||||||
MessageHandlerConfiguration<P> configuration) {
|
MessageHandler(TcpConnectionHandler<P> handler, MessageHandlerConfiguration<P> config) {
|
||||||
this.connectionHandler = connectionHandler;
|
this.connectionHandler = handler;
|
||||||
this.configuration = configuration;
|
this.configuration = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Publisher<Void> apply(NettyInbound in, NettyOutbound out) {
|
public Publisher<Void> apply(NettyInbound in, NettyOutbound out) {
|
||||||
Flux<Collection<Message<P>>> inbound = in.receive()
|
Flux<Collection<Message<P>>> inbound = in.receive().map(configuration.decoder);
|
||||||
.map(configuration.decoder);
|
|
||||||
|
|
||||||
DirectProcessor<Void> promise = DirectProcessor.create();
|
DirectProcessor<Void> closeProcessor = DirectProcessor.create();
|
||||||
TcpConnection<P> tcpConnection = new ReactorNettyTcpConnection<>(in,
|
TcpConnection<P> tcpConnection =
|
||||||
out,
|
new ReactorNettyTcpConnection<>(in, out, configuration.encoder, closeProcessor);
|
||||||
configuration.encoder,
|
|
||||||
promise);
|
|
||||||
|
|
||||||
if (configuration.scheduler != null) {
|
if (configuration.scheduler != null) {
|
||||||
configuration.scheduler.schedule(() -> connectionHandler.afterConnected(
|
configuration.scheduler.schedule(() -> connectionHandler.afterConnected(tcpConnection));
|
||||||
tcpConnection));
|
inbound = inbound.publishOn(configuration.scheduler, configuration.backlog);
|
||||||
inbound =
|
|
||||||
inbound.publishOn(configuration.scheduler, configuration.backlog);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
connectionHandler.afterConnected(tcpConnection);
|
connectionHandler.afterConnected(tcpConnection);
|
||||||
|
@ -239,15 +233,16 @@ public class ReactorNettyTcpClient<P> implements TcpOperations<P> {
|
||||||
connectionHandler::handleFailure,
|
connectionHandler::handleFailure,
|
||||||
connectionHandler::afterConnectionClosed);
|
connectionHandler::afterConnectionClosed);
|
||||||
|
|
||||||
return promise;
|
return closeProcessor;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static final class Reconnector<T> implements Function<Flux<T>, Publisher<?>> {
|
private static final class Reconnector<T> implements Function<Flux<T>, Publisher<?>> {
|
||||||
|
|
||||||
private final ReconnectStrategy strategy;
|
private final ReconnectStrategy strategy;
|
||||||
|
|
||||||
|
|
||||||
Reconnector(ReconnectStrategy strategy) {
|
Reconnector(ReconnectStrategy strategy) {
|
||||||
this.strategy = strategy;
|
this.strategy = strategy;
|
||||||
}
|
}
|
||||||
|
@ -255,9 +250,7 @@ public class ReactorNettyTcpClient<P> implements TcpOperations<P> {
|
||||||
@Override
|
@Override
|
||||||
public Publisher<?> apply(Flux<T> flux) {
|
public Publisher<?> apply(Flux<T> flux) {
|
||||||
return flux.scan(1, (p, e) -> p++)
|
return flux.scan(1, (p, e) -> p++)
|
||||||
.doOnCancel(() -> new Exception().printStackTrace())
|
.flatMap(attempt -> Mono.delayMillis(strategy.getTimeToNextAttempt(attempt)));
|
||||||
.flatMap(attempt -> Mono.delayMillis(strategy.getTimeToNextAttempt(
|
|
||||||
attempt)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,47 +35,52 @@ import org.springframework.util.concurrent.ListenableFuture;
|
||||||
* @param <P> the payload type of messages read or written to the TCP stream.
|
* @param <P> the payload type of messages read or written to the TCP stream.
|
||||||
*
|
*
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
* @since 4.2
|
* @since 5.0
|
||||||
*/
|
*/
|
||||||
public class ReactorNettyTcpConnection<P> implements TcpConnection<P> {
|
public class ReactorNettyTcpConnection<P> implements TcpConnection<P> {
|
||||||
|
|
||||||
private final NettyInbound in;
|
private final NettyInbound inbound;
|
||||||
private final NettyOutbound out;
|
|
||||||
private final DirectProcessor<Void> close;
|
private final NettyOutbound outbound;
|
||||||
|
|
||||||
|
private final DirectProcessor<Void> closeProcessor;
|
||||||
|
|
||||||
private final BiConsumer<? super ByteBuf, ? super Message<P>> encoder;
|
private final BiConsumer<? super ByteBuf, ? super Message<P>> encoder;
|
||||||
|
|
||||||
public ReactorNettyTcpConnection(NettyInbound in,
|
|
||||||
NettyOutbound out,
|
public ReactorNettyTcpConnection(NettyInbound inbound, NettyOutbound outbound,
|
||||||
BiConsumer<? super ByteBuf, ? super Message<P>> encoder,
|
BiConsumer<? super ByteBuf, ? super Message<P>> encoder,
|
||||||
DirectProcessor<Void> close) {
|
DirectProcessor<Void> closeProcessor) {
|
||||||
this.out = out;
|
|
||||||
this.in = in;
|
this.inbound = inbound;
|
||||||
|
this.outbound = outbound;
|
||||||
this.encoder = encoder;
|
this.encoder = encoder;
|
||||||
this.close = close;
|
this.closeProcessor = closeProcessor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ListenableFuture<Void> send(Message<P> message) {
|
public ListenableFuture<Void> send(Message<P> message) {
|
||||||
ByteBuf byteBuf = in.channel().alloc().buffer();
|
ByteBuf byteBuf = this.inbound.channel().alloc().buffer();
|
||||||
encoder.accept(byteBuf, message);
|
this.encoder.accept(byteBuf, message);
|
||||||
return new MonoToListenableFutureAdapter<>(out.send(Mono.just(byteBuf)));
|
return new MonoToListenableFutureAdapter<>(this.outbound.send(Mono.just(byteBuf)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
public void onReadInactivity(Runnable runnable, long inactivityDuration) {
|
public void onReadInactivity(Runnable runnable, long inactivityDuration) {
|
||||||
in.onReadIdle(inactivityDuration, runnable);
|
this.inbound.onReadIdle(inactivityDuration, runnable);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
public void onWriteInactivity(Runnable runnable, long inactivityDuration) {
|
public void onWriteInactivity(Runnable runnable, long inactivityDuration) {
|
||||||
out.onWriteIdle(inactivityDuration, runnable);
|
this.outbound.onWriteIdle(inactivityDuration, runnable);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
close.onComplete();
|
this.closeProcessor.onComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,10 +24,10 @@ import org.junit.Test;
|
||||||
|
|
||||||
import org.springframework.messaging.Message;
|
import org.springframework.messaging.Message;
|
||||||
import org.springframework.messaging.simp.SimpMessageType;
|
import org.springframework.messaging.simp.SimpMessageType;
|
||||||
import org.springframework.messaging.support.MessageBuilder;
|
|
||||||
import org.springframework.util.InvalidMimeTypeException;
|
import org.springframework.util.InvalidMimeTypeException;
|
||||||
|
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test fixture for {@link StompDecoder}.
|
* Test fixture for {@link StompDecoder}.
|
||||||
|
@ -39,6 +39,7 @@ public class StompDecoderTests {
|
||||||
|
|
||||||
private final StompDecoder decoder = new StompDecoder();
|
private final StompDecoder decoder = new StompDecoder();
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodeFrameWithCrLfEols() {
|
public void decodeFrameWithCrLfEols() {
|
||||||
Message<byte[]> frame = decode("DISCONNECT\r\n\r\n\0");
|
Message<byte[]> frame = decode("DISCONNECT\r\n\r\n\0");
|
||||||
|
|
|
@ -34,10 +34,10 @@ public class StompEncoderTests {
|
||||||
|
|
||||||
private final StompEncoder encoder = new StompEncoder();
|
private final StompEncoder encoder = new StompEncoder();
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void encodeFrameWithNoHeadersAndNoBody() {
|
public void encodeFrameWithNoHeadersAndNoBody() {
|
||||||
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.DISCONNECT);
|
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.DISCONNECT);
|
||||||
|
|
||||||
Message<byte[]> frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders());
|
Message<byte[]> frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders());
|
||||||
|
|
||||||
assertEquals("DISCONNECT\n\n\0", new String(encoder.encode(frame)));
|
assertEquals("DISCONNECT\n\n\0", new String(encoder.encode(frame)));
|
||||||
|
@ -48,20 +48,18 @@ public class StompEncoderTests {
|
||||||
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECT);
|
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECT);
|
||||||
headers.setAcceptVersion("1.2");
|
headers.setAcceptVersion("1.2");
|
||||||
headers.setHost("github.org");
|
headers.setHost("github.org");
|
||||||
|
|
||||||
Message<byte[]> frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders());
|
Message<byte[]> frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders());
|
||||||
|
|
||||||
String frameString = new String(encoder.encode(frame));
|
String frameString = new String(encoder.encode(frame));
|
||||||
|
|
||||||
assertTrue("CONNECT\naccept-version:1.2\nhost:github.org\n\n\0".equals(frameString) || "CONNECT\nhost:github.org\naccept-version:1.2\n\n\0".equals(
|
assertTrue(
|
||||||
frameString));
|
"CONNECT\naccept-version:1.2\nhost:github.org\n\n\0".equals(frameString) ||
|
||||||
|
"CONNECT\nhost:github.org\naccept-version:1.2\n\n\0".equals(frameString));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void encodeFrameWithHeadersThatShouldBeEscaped() {
|
public void encodeFrameWithHeadersThatShouldBeEscaped() {
|
||||||
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.DISCONNECT);
|
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.DISCONNECT);
|
||||||
headers.addNativeHeader("a:\r\n\\b", "alpha:bravo\r\n\\");
|
headers.addNativeHeader("a:\r\n\\b", "alpha:bravo\r\n\\");
|
||||||
|
|
||||||
Message<byte[]> frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders());
|
Message<byte[]> frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders());
|
||||||
|
|
||||||
assertEquals("DISCONNECT\na\\c\\r\\n\\\\b:alpha\\cbravo\\r\\n\\\\\n\n\0",
|
assertEquals("DISCONNECT\na\\c\\r\\n\\\\b:alpha\\cbravo\\r\\n\\\\\n\n\0",
|
||||||
|
@ -72,8 +70,8 @@ public class StompEncoderTests {
|
||||||
public void encodeFrameWithHeadersBody() {
|
public void encodeFrameWithHeadersBody() {
|
||||||
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND);
|
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND);
|
||||||
headers.addNativeHeader("a", "alpha");
|
headers.addNativeHeader("a", "alpha");
|
||||||
|
Message<byte[]> frame = MessageBuilder.createMessage(
|
||||||
Message<byte[]> frame = MessageBuilder.createMessage("Message body".getBytes(), headers.getMessageHeaders());
|
"Message body".getBytes(), headers.getMessageHeaders());
|
||||||
|
|
||||||
assertEquals("SEND\na:alpha\ncontent-length:12\n\nMessage body\0",
|
assertEquals("SEND\na:alpha\ncontent-length:12\n\nMessage body\0",
|
||||||
new String(encoder.encode(frame)));
|
new String(encoder.encode(frame)));
|
||||||
|
@ -83,8 +81,8 @@ public class StompEncoderTests {
|
||||||
public void encodeFrameWithContentLengthPresent() {
|
public void encodeFrameWithContentLengthPresent() {
|
||||||
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND);
|
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND);
|
||||||
headers.setContentLength(12);
|
headers.setContentLength(12);
|
||||||
|
Message<byte[]> frame = MessageBuilder.createMessage(
|
||||||
Message<byte[]> frame = MessageBuilder.createMessage("Message body".getBytes(), headers.getMessageHeaders());
|
"Message body".getBytes(), headers.getMessageHeaders());
|
||||||
|
|
||||||
assertEquals("SEND\ncontent-length:12\n\nMessage body\0",
|
assertEquals("SEND\ncontent-length:12\n\nMessage body\0",
|
||||||
new String(encoder.encode(frame)));
|
new String(encoder.encode(frame)));
|
||||||
|
|
Loading…
Reference in New Issue