Catch-all обработка исключений для исходящего ChannelHandler - PullRequest
0 голосов
/ 30 мая 2018

В Netty у вас есть концепция входящих и исходящих обработчиков.Универсальный обработчик входящих исключений реализуется простым добавлением обработчика канала в конец (хвост) конвейера и реализацией переопределения exceptionCaught.Исключение, происходящее вдоль входящего конвейера, будет перемещаться вдоль обработчиков до тех пор, пока не встретится последний, если не будет обработано по пути.

Для исходящих обработчиков не существует полной противоположности.Вместо этого (в соответствии с Netty в действии, стр. 94) вам нужно либо добавить прослушиватель для Future канала , либо прослушиватель для Promise, переданный в метод write вашего 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 моего обработчика ошибок никогда не вызывается, если я выбрасываю исключение времени выполнения в HttpLoggerHandler.write().

Как бы я сделал эту работу?Ошибка в любом из исходящих обработчиков должна «всплыть» перед тем, который прикреплен к голове.

Важно отметить, что я не просто хочу закрыть канал, я хочу написать сообщение об ошибке клиенту (как видно из serverErrorJSON('...'). Во время моих попыток перетасовкиПорядок обработчиков (также пробуя материал из этого ответа ), я активировал слушателя, но не смог ничего написать. Если я использовал ctx.write() в слушателе, кажется, что япопал в цикл, при использовании future.channel().write... ничего не делал.

Ответы [ 2 ]

0 голосов
/ 31 августа 2018

Похоже, не существует обобщенной концепции обработчика исключений для всех обработчиков исходящих обработчиков, который будет отлавливать ошибки независимо от того, где.Это означает, что если вы не зарегистрировали прослушиватель для обнаружения определенной ошибки, ошибка времени выполнения, вероятно, приведет к ее «проглатыванию», в результате чего вы почесываете голову, почему ничего не возвращается.

Тем не менее, возможно, этоне имеет смысла иметь обработчик / прослушиватель, который всегда будет выполняться при наличии ошибки (поскольку это должно быть очень общим), но он делает ошибки регистрации немного сложнее, чем нужно.

После записи куча обучающих тестов (которые я предлагаю проверить!) В итоге я получил эти идеи, которые в основном являются именами моих тестов JUnit (после некоторых манипуляций с регулярными выражениями):

  • слушатель может записать в канал после завершения родительской записи
  • слушатель записи может удалить слушателей из конвейера и записать ошибочную запись
  • все слушатели вызываютсяв случае успеха, если то же обещание передано
  • , обработчик ошибок рядом с хвостом не может отловить ошибку от обработчика ближе* head
  • netty не вызывает следующие обработчики записи при исключении времени выполнения
  • netty вызывает прослушиватель записи один раз при обычной записи
  • netty вызывает прослушиватель записи один раз в ошибочномwrite
  • netty вызывает следующие обработчики write с написанным сообщением
  • обещания могут быть использованы для прослушивания следующих обработчиков успеха или неудачи
  • обещания могут использоваться для прослушивания не немедленныхрезультат обработчиков, если обещание передано
  • обещания не могут быть использованы для прослушивания результата, не являющегося непосредственным обработчиком, если новое обещание передано
  • обещания не могут использоваться для прослушивания результата не немедленного обработчика, еслиобещание не передается
  • , только слушатель, добавленный к окончательной записи, вызывается при ошибке, если обещание не передается
  • , только слушатель, добавленный к окончательной записи, вызывается при успехе, еслиобещание не передается
  • запись слушателей вызывается из хвоста

ThisНа примере, приведенном в вопросе, понимание означает, что если ошибка возникает около хвоста и 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);
}
0 голосов
/ 01 июня 2018

В основном то, что вы сделали, является правильным ... Единственное, что не правильно, - это порядок обработчиков.Ваш ChannelOutboundHandlerAdapter должен быть размещен "как последний обработчик исходящих сообщений" в конвейере.Это означает, что это должно быть так:

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

Причина этого заключается в том, что исходящие события из хвоста в верхнюю часть конвейера в то время как входящие события перетекают из головы в хвост.

...