/* * Copyright 2002-2017 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.tcp.reactor; import java.lang.reflect.Method; import java.time.Duration; import java.util.Collection; import java.util.List; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.group.ChannelGroup; import io.netty.channel.group.ChannelGroupFuture; import io.netty.channel.group.DefaultChannelGroup; import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.util.concurrent.ImmediateEventExecutor; import org.reactivestreams.Publisher; import reactor.core.publisher.DirectProcessor; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; import reactor.ipc.netty.FutureMono; import reactor.ipc.netty.NettyContext; import reactor.ipc.netty.NettyInbound; import reactor.ipc.netty.NettyOutbound; import reactor.ipc.netty.options.ClientOptions; import reactor.ipc.netty.resources.LoopResources; import reactor.ipc.netty.resources.PoolResources; import reactor.ipc.netty.tcp.TcpClient; import reactor.ipc.netty.tcp.TcpResources; import reactor.util.concurrent.QueueSupplier; import org.springframework.messaging.Message; import org.springframework.messaging.tcp.ReconnectStrategy; import org.springframework.messaging.tcp.TcpConnection; import org.springframework.messaging.tcp.TcpConnectionHandler; import org.springframework.messaging.tcp.TcpOperations; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; import org.springframework.util.concurrent.ListenableFuture; import org.springframework.util.concurrent.SettableListenableFuture; /** * Reactor Netty based implementation of {@link TcpOperations}. * * @author Rossen Stoyanchev * @author Stephane Maldini * @since 5.0 */ public class ReactorNettyTcpClient<P> implements TcpOperations<P> { private final TcpClient tcpClient; private final ReactorNettyCodec<P> codec; private final ChannelGroup channelGroup; private final LoopResources loopResources; private final PoolResources poolResources; private final Scheduler scheduler = Schedulers.newParallel("ReactorNettyTcpClient"); private volatile boolean stopping = false; /** * Basic constructor with a host and a port. */ public ReactorNettyTcpClient(String host, int port, ReactorNettyCodec<P> codec) { this(opts -> opts.connect(host, port), codec); } /** * Alternate constructor with a {@link ClientOptions} consumer providing * additional control beyond a host and a port. */ public ReactorNettyTcpClient(Consumer<ClientOptions> optionsConsumer, ReactorNettyCodec<P> codec) { Assert.notNull(optionsConsumer, "Consumer<ClientOptions> is required"); Assert.notNull(codec, "ReactorNettyCodec is required"); this.channelGroup = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE); this.loopResources = LoopResources.create("reactor-netty-tcp-client"); this.poolResources = PoolResources.fixed("reactor-netty-tcp-pool"); Consumer<ClientOptions> builtInConsumer = opts -> opts .channelGroup(this.channelGroup) .loopResources(this.loopResources) .poolResources(this.poolResources) .preferNative(false); this.tcpClient = TcpClient.create(optionsConsumer.andThen(builtInConsumer)); this.codec = codec; } @Override public ListenableFuture<Void> connect(final TcpConnectionHandler<P> handler) { Assert.notNull(handler, "TcpConnectionHandler is required"); if (this.stopping) { return handleShuttingDownConnectFailure(handler); } Mono<Void> connectMono = this.tcpClient .newHandler(new ReactorNettyHandler(handler)) .doOnError(handler::afterConnectFailure) .then(); return new MonoToListenableFutureAdapter<>(connectMono); } @Override public ListenableFuture<Void> connect(TcpConnectionHandler<P> handler, ReconnectStrategy strategy) { Assert.notNull(handler, "TcpConnectionHandler is required"); Assert.notNull(strategy, "ReconnectStrategy is required"); if (this.stopping) { return handleShuttingDownConnectFailure(handler); } // Report first connect to the ListenableFuture MonoProcessor<Void> connectMono = MonoProcessor.create(); this.tcpClient .newHandler(new ReactorNettyHandler(handler)) .doOnNext(updateConnectMono(connectMono)) .doOnError(updateConnectMono(connectMono)) .doOnError(handler::afterConnectFailure) // report all connect failures to the handler .flatMap(NettyContext::onClose) // post-connect issues .retryWhen(reconnectFunction(strategy)) .repeatWhen(reconnectFunction(strategy)) .subscribe(); return new MonoToListenableFutureAdapter<>(connectMono); } private ListenableFuture<Void> handleShuttingDownConnectFailure(TcpConnectionHandler<P> handler) { IllegalStateException ex = new IllegalStateException("Shutting down."); handler.afterConnectFailure(ex); return new MonoToListenableFutureAdapter<>(Mono.error(ex)); } private <T> Consumer<T> updateConnectMono(MonoProcessor<Void> connectMono) { return o -> { if (!connectMono.isTerminated()) { if (o instanceof Throwable) { connectMono.onError((Throwable) o); } else { connectMono.onComplete(); } } }; } private <T> Function<Flux<T>, Publisher<?>> reconnectFunction(ReconnectStrategy reconnectStrategy) { return flux -> flux .scan(1, (count, element) -> count++) .flatMap(attempt -> Mono.delay( Duration.ofMillis(reconnectStrategy.getTimeToNextAttempt(attempt)))); } @Override public ListenableFuture<Void> shutdown() { if (this.stopping) { SettableListenableFuture<Void> future = new SettableListenableFuture<>(); future.set(null); return future; } this.stopping = true; ChannelGroupFuture close = this.channelGroup.close(); Mono<Void> completion = FutureMono.from(close) .doAfterTerminate((x, e) -> { // TODO: https://github.com/reactor/reactor-netty/issues/24 shutdownGlobalResources(); this.loopResources.dispose(); this.poolResources.dispose(); // TODO: https://github.com/reactor/reactor-netty/issues/25 try { Thread.sleep(2000); } catch (InterruptedException ex) { ex.printStackTrace(); } // Scheduler after loop resources... this.scheduler.dispose(); }); return new MonoToListenableFutureAdapter<>(completion); } private void shutdownGlobalResources() { try { Method method = TcpResources.class.getDeclaredMethod("_dispose"); ReflectionUtils.makeAccessible(method); ReflectionUtils.invokeMethod(method, TcpResources.get()); } catch (NoSuchMethodException ex) { // ignore } } private class ReactorNettyHandler implements BiFunction<NettyInbound, NettyOutbound, Publisher<Void>> { private final TcpConnectionHandler<P> connectionHandler; ReactorNettyHandler(TcpConnectionHandler<P> handler) { this.connectionHandler = handler; } @Override @SuppressWarnings("unchecked") public Publisher<Void> apply(NettyInbound inbound, NettyOutbound outbound) { DirectProcessor<Void> completion = DirectProcessor.create(); TcpConnection<P> connection = new ReactorNettyTcpConnection<>(inbound, outbound, codec, completion); scheduler.schedule(() -> connectionHandler.afterConnected(connection)); inbound.context().addHandler(new StompMessageDecoder<>(codec)); inbound.receiveObject() .cast(Message.class) .publishOn(scheduler, QueueSupplier.SMALL_BUFFER_SIZE) .subscribe( connectionHandler::handleMessage, connectionHandler::handleFailure, connectionHandler::afterConnectionClosed); return completion; } } private static class StompMessageDecoder<P> extends ByteToMessageDecoder { private final ReactorNettyCodec<P> codec; public StompMessageDecoder(ReactorNettyCodec<P> codec) { this.codec = codec; } @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { Collection<Message<P>> messages = codec.decode(in); out.addAll(messages); } } }