捕获出站ChannelHandler的所有异常处理

时间:2018-05-30 19:48:39

标签: asynchronous exception exception-handling netty

在Netty中,您拥有入站和出站处理程序的概念。只需在管道的末尾(尾部)添加一个通道处理程序并实现exceptionCaught覆盖,即可实现catch-all入站异常处理程序。沿着入站管道发生的异常将沿着处理程序行进,直到遇到最后一个,如果没有沿途处理的话。

与传出处理程序完全相反。相反(根据Netty in Action,第94页),你需要为频道 Future添加一个监听器,或者向Promise传入一个监听器。 write的{​​{1}}方法。

由于我不确定在哪里插入前者,我以为我会选择后者,所以我做了以下Handler

ChannelOutboundHandler

这被添加到管道的负责人:

}

/**
 * Catch and log errors happening in the outgoing direction
 *
 * @see <p>p94 in "Netty In Action"</p>
 */
private ChannelOutboundHandlerAdapter createOutgoingErrorHandler() {
    return new ChannelOutboundHandlerAdapter() {
        @Override
        public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
            logger.info("howdy! (never gets this far)");

            final ChannelFutureListener channelFutureListener = future -> {
                if (!future.isSuccess()) {
                    future.cause().printStackTrace();
                    // ctx.writeAndFlush(serverErrorJSON("an error!"));
                    future.channel().writeAndFlush(serverErrorJSON("an error!"));
                    future.channel().close();
                }
            };
            promise.addListener(channelFutureListener);
            ctx.write(msg, promise);
        }
    };

问题是如果我在@Override public void addHandlersToPipeline(final ChannelPipeline pipeline) { pipeline.addLast( createOutgoingErrorHandler(), new HttpLoggerHandler(), // an error in this `write` should go "up" authHandlerFactory.get(), // etc 中抛出运行时异常,则永远不会调用我的错误处理程序的write方法。

我将如何使这项工作?任何传出处理程序中的错误都应该“冒出来”#34;到附在头上的那个。

需要注意的一点是,我不想仅仅关闭频道,我想将错误信息写回客户端(从HttpLoggerHandler.write()可以看出。)在我的洗牌试验期间处理程序的顺序(也试用this answer的东西),我已经激活了监听器,但我无法写任何东西。如果我在监听器中使用serverErrorJSON('...'),似乎我进入一个循环,而使用ctx.write()没有做任何事情。

3 个答案:

答案 0 :(得分:1)

我发现了一个非常简单的解决方案,它允许入站和出站异常都可以到达与管道中最后一个ChannelHandler相同的异常处理程序。

我的管道设置如下:

    //Inbound propagation
    socketChannel.pipeline()
      .addLast(new Decoder())
      .addLast(new ExceptionHandler());

    //Outbound propagation
    socketChannel.pipeline()
      .addFirst(new OutboundExceptionRouter())
      .addFirst(new Encoder());

这是我的ExceptionHandler的内容,它记录捕获的异常:

public class ExceptionHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.error("Exception caught on channel", cause);
    }
}

现在,OutHandler甚至可以通过ExceptionHandler处理出站异常:

public class OutboundExceptionRouter extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        promise.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
        super.write(ctx, msg, promise);
    }
}

这是在我的管道中调用的第一个出站处理程序,它的工作是在出站写承诺中添加一个侦听器,当承诺失败时,将执行future.channel().pipeline().fireExceptionCaught(future.cause());fireExceptionCaught方法沿入站方向通过管道传播异常,最终到达ExceptionHandler。


如果有人感兴趣,从Netty 4.1开始,我们需要添加一个侦听器以获取异常的原因是,对通道执行writeAndFlush之后,在AbstractChannelHandlerContext.java中调用invokeWrite0 method将写操作包装在try catch块中。 catch块通知Promise,而不是为入站消息调用fireExceptionCaught like the invokeChannelRead method does

答案 1 :(得分:0)

基本上你所做的是正确的...唯一不正确的是处理程序的顺序。您的ChannelOutboundHandlerAdapter桅杆被放置&#34;作为最后一个出站处理器&#34;在管线中。这意味着它应该是这样的:

pipeline.addLast(
        new HttpLoggerHandler(),
        createOutgoingErrorHandler(),
        authHandlerFactory.get());

这样做的原因是,当入站事件从头部流向尾部时,从尾部到管道头部的出站事件。

答案 2 :(得分:0)

似乎没有一个笼统的概念,即笼统的异常处理程序对于将在任何地方捕获错误的传出处理程序都适用。这意味着,除非您注册了侦听器以捕获某个错误,否则运行时错误很可能会导致该错误被“吞噬”,从而使您无所适从。

也就是说,也许总是有给定的错误执行的处理程序/侦听器没有意义(因为它必须非常通用),但是它确实使日志记录错误比需要的要复杂。 / p>

写完a bunch of learning tests(我建议您检查一下!)后,我得到了以下见解,这些见解基本上是我的JUnit测试的名称(经过一些正则表达式操作之后):

  • 父级写入完成后,监听者可以向频道写入
  • 写侦听器可以从管道中删除侦听器,并在错误的写操作中进行写
  • 如果传递相同的诺言,则成功调用所有侦听器
  • 靠近尾部的错误处理程序无法捕获来自靠近头部的处理程序的错误
  • netty不会调用下一个在运行时异常上写入的处理程序
  • netty在正常写入时调用一次写入侦听器
  • netty在错误写入时调用一次写入侦听器
  • netty调用下一个带有其书面消息的处理程序
  • 承诺可用于侦听下一个处理程序的成功或失败
  • 如果承诺被传递,
  • 承诺可用于侦听非立即处理程序的结果
  • 如果传递新的诺言,
  • 承诺不能用于侦听非立即处理程序的结果
  • 如果没有兑现承诺,
  • 承诺不能用于监听非立即处理程序的结果
  • 如果未传递承诺,则只会在错误时调用添加到最终写入的侦听器
  • 如果未传递承诺,只有成功添加到最后写入的侦听器才会被调用
  • 从后面调用
  • write侦听器

以问题的示例为例,这种洞察力意味着,如果错误应该在尾部 authHandler附近发生,那么错误处理程序将在头部附近永远不会被调用,因为它会提供新的承诺,因为ctx.write(msg)本质上是ctx.channel.write(msg, newPromise())

在这种情况下,我们最终通过在所有业务逻辑处理程序之间注入相同的可共享错误处理来解决了这种情况。

处理程序如下

@ChannelHandler.Sharable
class OutboundErrorHandler extends ChannelOutboundHandlerAdapter {

    private final static Logger logger = LoggerFactory.getLogger(OutboundErrorHandler.class);
    private Throwable handledCause = null;

    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
        ctx.write(msg, promise).addListener(writeResult -> handleWriteResult(ctx, writeResult));
    }

    private void handleWriteResult(ChannelHandlerContext ctx, Future<?> writeResult) {
        if (!writeResult.isSuccess()) {
            final Throwable cause = writeResult.cause();

            if (cause instanceof ClosedChannelException) {
                // no reason to close an already closed channel - just ignore
                return;
            }

            // Since this handler is shared and added multiple times
            // we need to avoid spamming the logs N number of times for the same error
            if (handledCause == cause) return;
            handledCause = cause;

            logger.error("Uncaught exception on write!", cause);

            // By checking on channel writability and closing the channel after writing the error message,
            // only the first listener will signal the error to the client
            final Channel channel = ctx.channel();
            if (channel.isWritable()) {
                ctx.writeAndFlush(serverErrorJSON(cause.getMessage()), channel.newPromise());
                ctx.close();
            }
        }
    }
}

然后在我们的管道设置中,我们有了这个

// Prepend the error handler to every entry in the pipeline. 
// The intention behind this is to have a catch-all
// outbound error handler and thereby avoiding the need to attach a
// listener to every ctx.write(...).
final OutboundErrorHandler outboundErrorHandler = new OutboundErrorHandler();
for (Map.Entry<String, ChannelHandler> entry : pipeline) {
    pipeline.addBefore(entry.getKey(), entry.getKey() + "#OutboundErrorHandler", outboundErrorHandler);
}