Понимание предупреждения Jetty "Закрыто пока ожидает / не готово" - PullRequest
0 голосов
/ 25 июня 2018

У нас есть асинхронный сервлет, который выдает следующий журнал предупреждений от Jetty:

java.io.IOException: Closed while Pending/Unready

После включения журналов отладки я получил следующую трассировку стека:

WARN [jetty-25948] (HttpOutput.java:278)   - java.io.IOException: Closed while Pending/Unready
DEBUG [jetty-25948] (HttpOutput.java:279)   - 
java.io.IOException: Closed while Pending/Unready
        at org.eclipse.jetty.server.HttpOutput.close(HttpOutput.java:277) ~[jetty-server.jar:9.4.8.v20171121]
        at org.eclipse.jetty.server.Response.closeOutput(Response.java:1044) [jetty-server.jar:9.4.8.v20171121]
        at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:488) [jetty-server.jar:9.4.8.v20171121]
        at org.eclipse.jetty.server.HttpChannel.run(HttpChannel.java:293) [jetty-server.jar:9.4.8.v20171121]
        at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:708) [jetty-util.jar:9.4.8.v20171121]
        at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:626) [jetty-util.jar:9.4.8.v20171121]
        at java.lang.Thread.run(Thread.java:748) [na:1.8.0_151]

Это тоже не помогломного.

Предупреждение приходит только после того, как Jetty вызывает метод onTimeout() нашего AsyncListener.QA иногда может воспроизвести его, используя kill -9 в клиентском приложении.

Как я могу воспроизвести это предупреждение с примером кода сервлета-клиента?Я хотел бы понять эту проблему в более простой среде, чем наш производственный код, чтобы потом можно было исправить производственный код.Как должен вести себя образец сервлета?Можно ли воспроизвести это на стороне клиента Apache Commons HttpClient в том же тесте JUnit?(Это было бы здорово для написания интеграционного теста без сложного взлома сети, например kill -9.)

Я несколько раз пытался реализовать пример асинхронного сервлета и клиента без успеха.Я не думаю, что присоединение этого кода поможет слишком много, но я могу сделать это, если кому-то интересно.

Версия Jetty: 9.4.8.v20171121

обновление (2018-06-27):

Обращаясь к полезному ответу @Joakim Erdfelt, я не нашел ни одного вызова close() в нашем коде, но обнаружил подозрительную пропущенную синхронизацию.Вот база нашего сервлета асинхронного опроса:

public class QueuePollServlet extends HttpServlet {

    public QueuePollServlet() {
    }

    @Override
    protected void doPost(final HttpServletRequest req, final HttpServletResponse resp)
            throws ServletException, IOException {
        resp.setContentType(MediaType.OCTET_STREAM.type());
        resp.setStatus(HttpServletResponse.SC_OK);
        resp.flushBuffer();
        final AsyncContext async = req.startAsync();
        async.setTimeout(30_000);
        final ServletOutputStream output = resp.getOutputStream();
        final QueueWriteListener writeListener = new QueueWriteListener(async, output);
        async.addListener(writeListener);
        output.setWriteListener(writeListener);
    }

    private static class QueueWriteListener implements AsyncListener, WriteListener {

        private final AsyncContext asyncContext;
        private final ServletOutputStream output;

        public QueueWriteListener(final AsyncContext asyncContext, final ServletOutputStream output) {
            this.asyncContext = checkNotNull(asyncContext, "asyncContext cannot be null");
            this.output = checkNotNull(output, "output cannot be null");
        }

        @Override
        public void onWritePossible() throws IOException {
            writeImpl();
        }

        private synchronized void writeImpl() throws IOException {
            while (output.isReady()) {
                final byte[] message = getNextMessage();
                if (message == null) {
                    output.flush();
                    return;
                }
                output.write(message);
            }
        }

        private void completeImpl() {
            asyncContext.complete();
        }

        public void dataArrived() {
            try {
                writeImpl();
            } catch (IOException e) {
                ...
            }
        }

        public void noMoreBuffers() {
            completeImpl();
        }

        @Override
        public void onTimeout(final AsyncEvent event) throws IOException {
            completeImpl();
        }

        @Override
        public void onError(final Throwable t) {
            logger.error("Writer.onError", t);
            completeImpl();
        }


        ...
    }
}

Возможное условие гонки:

  1. DataFeederThread: вызывает dataArrived() -> writeImpl(), затем он получает это output.isReady() is true.
  2. Jetty вызывает onTimeout(), что завершает контекст.
  3. DataFeederThread: вызывает output.write() в цикле while, но находит завершенный контекст.

Может ли этот сценарий вызвать предупреждение Closed while Pending/Unready или это другая проблема?Я прав, что решение completeImpl() synchronized решает проблему, или есть что-то еще, о чем нужно заботиться?

обновление (2018-06-28):

У нас также есть аналогичная реализация onError в QueueWriteListener, как в следующем фрагменте:

@Override
public void onError(final Throwable t) {
    logger.error("Writer.onError", t);
    completeImpl();
}

В любом случае, нет сообщения об ошибке onError вокруг сообщения журнала Closed while Pending/Unready (глядя на два часасроки для каждого), просто EOF, как следующие из наших DataFeederThread:

DEBUG [DataFeederThread] (HttpOutput.java:224) - 
org.eclipse.jetty.io.EofException: null
        at org.eclipse.jetty.server.HttpConnection$SendCallback.reset(HttpConnection.java:704) ~[jetty-server.jar:9.4.8.v20171121]
        at org.eclipse.jetty.server.HttpConnection$SendCallback.access$300(HttpConnection.java:668) ~[jetty-server.jar:9.4.8.v20171121]
        at org.eclipse.jetty.server.HttpConnection.send(HttpConnection.java:526) ~[jetty-server.jar:9.4.8.v20171121]
        at org.eclipse.jetty.server.HttpChannel.sendResponse(HttpChannel.java:778) ~[jetty-server.jar:9.4.8.v20171121]
        at org.eclipse.jetty.server.HttpChannel.write(HttpChannel.java:834) ~[jetty-server.jar:9.4.8.v20171121]
        at org.eclipse.jetty.server.HttpOutput.write(HttpOutput.java:234) [jetty-server.jar:9.4.8.v20171121]
        at org.eclipse.jetty.server.HttpOutput.write(HttpOutput.java:218) [jetty-server.jar:9.4.8.v20171121]
        at org.eclipse.jetty.server.HttpOutput.flush(HttpOutput.java:392) [jetty-server.jar:9.4.8.v20171121]
        at com.example.QueuePollServlet$QueueWriteListener.writeImpl()
        at com.example.QueuePollServlet$QueueWriteListener.dataArrived()


DEBUG [DataFeederThread] (QueuePollServlet.java:217) - messageArrived exception
org.eclipse.jetty.io.EofException: Closed
        at org.eclipse.jetty.server.HttpOutput.write(HttpOutput.java:476) ~[jetty-server.jar:9.4.8.v20171121]
        at com.example.QueuePollServlet$QueueWriteListener.writeImpl()
        at com.example.QueuePollServlet$QueueWriteListener.dataArrived()

Ответы [ 2 ]

0 голосов
/ 29 июня 2018

Можно воспроизвести предупреждение Closed while Pending/Unready с некоторой ручной отладкой и простым curl клиентом. Я проверил это с Jetty 9.4.8.v20171121 и Jetty 9.4.11.v20180605. Вам нужны две точки останова, поэтому надежно воспроизвести их в тестовой среде нелегко.

  1. Первая точка останова находится в методе HttpOutput.write() сразу после того, как она изменит свое состояние с READY на PENDING:

    case READY:
        if (!_state.compareAndSet(OutputState.READY, OutputState.PENDING))
            continue;
        // put a breakpoint here
    
  2. Второй в Response.closeOutput():

    case STREAM:
        // put a breakpiont here
        if (!_out.isClosed())
            getOutputStream().close();
        break;
    

Шаги для воспроизведения:

  1. [JettyThread1] Вызывает QueueWriteListener.onWritePossible(), который записывает несколько байтов в вывод, а затем возвращает (так как его входной буфер пуст).
  2. Ожидание события onTimeout.
  3. [JettyThread2] HttpChannelState.onTimeout() звонит QueueWriteListener.onTimeout() который звонит asyncContext.complete().
  4. [JettyThread2] HttpChannelState.onTimeout() планирует отправку по истечении времени асинхронного ожидания.
  5. [JettyThread2] Пауза во второй точке останова
  6. [DFT] DataFeederThread звонки writeImpl()
  7. [DFT] DataFeederThread звонки HttpOutput.write() (это поток вывода)
  8. [DFT] HttpOutput.write() меняет свое состояние с READY на PENDING
  9. [DFT] DataFeederThread пауза здесь из-за точки останова выше
  10. [JettyThread2] Запланированная отправка закрывает выходной поток и выдает предупреждение Closed while Pending/Unready.

Итак, фактически Jetty закрывает поток вывода в этом стеке (Jetty 9.4.8.v20171121):

Thread [jetty-19] (Suspended)   
    Response.closeOutput() line: 1043   
    HttpChannelOverHttp(HttpChannel).handle() line: 488 
    HttpChannelOverHttp(HttpChannel).run() line: 293    
    QueuedThreadPool.runJob(Runnable) line: 708 
    QueuedThreadPool$2.run() line: 626  
    Thread.run() line: 748  

Создание onTimeout() synchronized (а также writeImpl() также synchronized) в слушателе не помогает, поскольку запланированное закрытие все еще может перекрываться с writeImpl (из DataFeederThread). Рассмотрим этот случай:

  1. [JettyThread1] Вызывает QueueWriteListener.onWritePossible(), который записывает несколько байтов в вывод, а затем возвращает (так как его входной буфер пуст).
  2. Ожидание события onTimeout.
  3. [JettyThread2] HttpChannelState.onTimeout() звонки QueueWriteListener.onTimeout(), звонки asyncContext.complete().
  4. [DFT] DataFeederThread вызывает writeImpl() (он заблокирован, так как onTimeout еще не закончен)
  5. [JettyThread2] QueueWriteListener.onTimeout() отделка
  6. [DFT] writeImpl() может работать
  7. [JettyThread2] HttpChannelState.onTimeout() планирует отправку по истечении времени асинхронного ожидания.
  8. [JettyThread2] Пауза во второй точке останова
  9. [DFT] DataFeederThread вызывает HttpOutput.write() (это выходной поток)
  10. [DFT] HttpOutput.write() меняет свое состояние с READY на PENDING
  11. [DFT] DataFeederThread пауза здесь из-за точки останова выше
  12. [JettyThread2] Запланированная отправка закрывает выходной поток и выдает предупреждение Closed while Pending/Unready.

К сожалению, после asnyContext.complete() недостаточно проверить output.isReady(). Он возвращает true, так как Jetty повторно открывает HttpOutput (см. Стек ниже), поэтому для этого требуется отдельный флаг в слушателе.

Thread [jetty-13] (Suspended (access of field _state in HttpOutput))    
    HttpOutput.reopen() line: 195   
    HttpOutput.recycle() line: 923  
    Response.recycle() line: 138    
    HttpChannelOverHttp(HttpChannel).recycle() line: 269    
    HttpChannelOverHttp.recycle() line: 83  
    HttpConnection.onCompleted() line: 424  
    HttpChannelOverHttp(HttpChannel).onCompleted() line: 695    
    HttpChannelOverHttp(HttpChannel).handle() line: 493 
    HttpChannelOverHttp(HttpChannel).run() line: 293    
    QueuedThreadPool.runJob(Runnable) line: 708 
    QueuedThreadPool$2.run() line: 626  
    Thread.run() line: 748  

Кроме того, isReady() также возвращает true, когда выход все еще закрыт (до повторного запуска / повторного открытия). Связанный вопрос: isReady () возвращает true в закрытом состоянии - почему?

Окончательная реализация выглядит примерно так:

@Override
protected void doPost(final HttpServletRequest req, final HttpServletResponse resp)
        throws ServletException, IOException {
    resp.setContentType(MediaType.OCTET_STREAM.type());
    resp.setStatus(HttpServletResponse.SC_OK);
    resp.setBufferSize(4096);
    resp.flushBuffer();
    final AsyncContext async = req.startAsync();
    async.setTimeout(5_000); // millis
    final ServletOutputStream output = resp.getOutputStream();
    final QueueWriteListener writeListener = new QueueWriteListener(async, output);
    async.addListener(writeListener);
    output.setWriteListener(writeListener);
}

private static class QueueWriteListener implements AsyncListener, WriteListener {

    private static final Logger logger = LoggerFactory.getLogger(QueueWriteListener.class);

    private final AsyncContext asyncContext;
    private final ServletOutputStream output;

    @GuardedBy("this")
    private boolean completed = false;

    public QueueWriteListener(final AsyncContext asyncContext, final ServletOutputStream output) {
        this.asyncContext = checkNotNull(asyncContext, "asyncContext cannot be null");
        this.output = checkNotNull(output, "output cannot be null");
    }

    @Override
    public void onWritePossible() throws IOException {
        writeImpl();
    }

    private synchronized void writeImpl() throws IOException {
        if (completed) {
            return;
        }
        while (output.isReady()) {
            final byte[] message = getNextMessage();
            if (message == null) {
                output.flush();
                return;
            }
            output.write(message);
        }
    }

    private synchronized void completeImpl() {
        // also stops DataFeederThread to call bufferArrived
        completed = true;
        asyncContext.complete();
    }

    @Override
    public void onError(final Throwable t) {
        logger.error("Writer.onError", t);
        completeImpl();
    }

    public void dataArrived() {
        try {
            writeImpl();
        } catch (RuntimeException | IOException e) {
            ...
        }
    }

    public void noMoreData() {
        completeImpl();
    }

    @Override
    public synchronized void onComplete(final AsyncEvent event) throws IOException {
        completed = true; // might not needed but does not hurt
    }

    @Override
    public synchronized void onTimeout(final AsyncEvent event) throws IOException {
        completeImpl();
    }

    @Override
    public void onError(final AsyncEvent event) throws IOException {
        logger.error("onError", event.getThrowable());
    }

    ...
}

обновление 2018-08-01 : На самом деле предупреждение не было полностью исправлено, см .: Предупреждения «Закрыто во время ожидания / Не готово» от Jetty

0 голосов
/ 27 июня 2018

Вы используете асинхронный ввод-вывод из спецификации сервлета и вручную закрыли поток ответа.

Ключевой факт: вызов close предполагает операцию записи.

В случае JettyIOException сообщает вам, что ваш ручной вызов закрытия потока Response вызвал нежелательный побочный эффект.

Так что в режиме асинхронного ввода-вывода должен был быть сделан вызов ServletOutputStream.isReady()перед использованием ServletOutputStream.close() проверки того, что isReady() является истинным.

Обратите внимание, что может быть желательно разрешить ServletOutputStream.close() в любое время, особенно если был вызван AsyncContext.complete()на эту рассылку.

Однако, если предыдущая запись еще не завершена и / или ServletOutputStream.isReady() не была вызвана, этот конкретный случай «голого» * ​​1023 * допускается, но поведениепрервать ответ, отбросив все ожидающие / неписаные записи в ServletOutputStream.

. Вот почему вы получаете IOException("Closed while Pending/Unready")

Теперь вы должны спросить себя, почему вы закрываете поток ответов?Это не требуется согласно спецификации сервлета и нежелательно, если вы когда-либо хотели использовать RequestDispatcher для объединения нескольких ответов сервлетов вместе.

...