package io.ripc.transport.netty4.tcp; import io.netty.channel.Channel; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.util.ReferenceCountUtil; import io.ripc.protocol.tcp.TcpHandler; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A bridge between netty's {@link Channel} and {@link io.ripc.protocol.tcp.TcpConnection}. It has the following * responsibilities: * <p> * <ul> * <li>Create a new {@link io.ripc.protocol.tcp.TcpConnection} instance when the channel is active and forwards it to * the configured * {@link TcpHandler}.</li> * <li>Reads any data from the channel and forwards it to the {@link Subscriber} attached via the event * {@link ChannelToConnectionBridge.ConnectionInputSubscriberEvent}</li> * <li>Accepts writes of {@link Publisher} on the channel and translates the items emitted from that publisher to the * channel.</li> * </ul> * * @param <R> The type of objects read from the underneath channel. * @param <W> The type of objects read written to the underneath channel. */ public class ChannelToConnectionBridge<R, W> extends ChannelDuplexHandler { private static final Logger logger = LoggerFactory.getLogger(ChannelToConnectionBridge.class); private final TcpHandler<R, W> handler; private TcpConnectionImpl<R, W> conn; private Subscriber<R> inputSubscriber; /*Populated via event ConnectionInputSubscriberEvent*/ public ChannelToConnectionBridge(TcpHandler<R, W> handler) { this.handler = handler; } @Override public void channelActive(final ChannelHandlerContext ctx) throws Exception { super.channelActive(ctx); conn = new TcpConnectionImpl<>(ctx.channel()); handler.handle(conn) .subscribe(new Subscriber<Void>() { @Override public void onSubscribe(Subscription s) { s.request(Long.MAX_VALUE); //no op } @Override public void onNext(Void aVoid) { // Void, no op } @Override public void onError(Throwable t) { logger.error("Error processing connection. Closing the channel.", t); ctx.channel().close(); } @Override public void onComplete() { ctx.channel().close(); } }); } @SuppressWarnings("unchecked") @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (null == conn || null == inputSubscriber) { logger.error("No connection input subscriber available. Disposing data."); ReferenceCountUtil.release(msg); return; } try { inputSubscriber.onNext((R) msg); } catch (ClassCastException e) { logger.error("Invalid message type read from the pipeline.", e); inputSubscriber.onError(e); } } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { if (null != conn && inputSubscriber != null) { inputSubscriber.onComplete(); } super.channelInactive(ctx); } @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof ConnectionInputSubscriberEvent) { @SuppressWarnings("unchecked") ConnectionInputSubscriberEvent<R> subscriberEvent = (ConnectionInputSubscriberEvent<R>) evt; if (null == inputSubscriber) { inputSubscriber = subscriberEvent.getInputSubscriber(); subscriberEvent.init(ctx); } else { inputSubscriber.onError(new IllegalStateException("Only one connection input subscriber allowed.")); } } super.userEventTriggered(ctx, evt); } @Override public void write(final ChannelHandlerContext ctx, Object msg, final ChannelPromise promise) throws Exception { if (msg instanceof Publisher) { @SuppressWarnings("unchecked") final Publisher<W> data = (Publisher<W>) msg; data.subscribe(new Subscriber<W>() { // TODO: Needs to be fixed to wire all futures to the promise of the Publisher write. private ChannelFuture lastWriteFuture; @Override public void onSubscribe(Subscription s) { s.request(Long.MAX_VALUE); // TODO: Backpressure } @Override public void onNext(W w) { lastWriteFuture = ctx.channel().write(w); } @Override public void onError(Throwable t) { onTerminate(); } @Override public void onComplete() { onTerminate(); } private void onTerminate() { ctx.channel().flush(); lastWriteFuture.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { if (future.isSuccess()) { promise.trySuccess(); } else { promise.tryFailure(future.cause()); } } }); } }); } else { super.write(ctx, msg, promise); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { logger.error(cause.getMessage(), cause); } /** * An event to attach a {@link Subscriber} to the {@link io.ripc.protocol.tcp.TcpConnection} created by {@link * ChannelToConnectionBridge} * * @param <R> */ public static final class ConnectionInputSubscriberEvent<R> { private final Subscriber<R> inputSubscriber; public ConnectionInputSubscriberEvent(Subscriber<R> inputSubscriber) { if (null == inputSubscriber) { throw new IllegalArgumentException("Connection input subscriber must not be null."); } this.inputSubscriber = inputSubscriber; } public Subscriber<R> getInputSubscriber() { return inputSubscriber; } void init(ChannelHandlerContext ctx) { try { inputSubscriber.onSubscribe(new Subscription() { @Override public void request(long n) { /*if(n == Long.MAX_VALUE){ ctx.channel().config().setAutoRead(true); }*/ //ctx.read(); implements backpressure ctx.channel().config().setAutoRead(true); } @Override public void cancel() { //implements close on cancel (must be after any pending onComplete) } }); } catch (Throwable error) { inputSubscriber.onError(error); } } } }