diff --git a/servlet/src/main/java/io/grpc/servlet/AsyncServletOutputStreamWriter.java b/servlet/src/main/java/io/grpc/servlet/AsyncServletOutputStreamWriter.java index 3c8d3d07571..85972bf9461 100644 --- a/servlet/src/main/java/io/grpc/servlet/AsyncServletOutputStreamWriter.java +++ b/servlet/src/main/java/io/grpc/servlet/AsyncServletOutputStreamWriter.java @@ -217,21 +217,37 @@ private void assureReadyAndDrainedTurnsFalse() { */ private void runOrBuffer(ActionItem actionItem) throws IOException { WriteState curState = writeState.get(); - if (curState.readyAndDrained) { // write to the outputStream directly - actionItem.run(); - if (actionItem == completeAction) { + + if (curState.readyAndDrained) { + if (isReady.getAsBoolean()) { + // Path 1: Container is truly ready. Write directly. + actionItem.run(); + if (actionItem == completeAction) { + return; + } + if (!isReady.getAsBoolean()) { + boolean successful = + writeState.compareAndSet(curState, curState.withReadyAndDrained(false)); + LockSupport.unpark(parkingThread); + checkState(successful, "Bug: curState is unexpectedly changed by another thread"); + log.finest("the servlet output stream becomes not ready"); + } return; } - if (!isReady.getAsBoolean()) { - boolean successful = - writeState.compareAndSet(curState, curState.withReadyAndDrained(false)); - LockSupport.unpark(parkingThread); - checkState(successful, "Bug: curState is unexpectedly changed by another thread"); - log.finest("the servlet output stream becomes not ready"); - } - } else { // buffer to the writeChain - writeChain.offer(actionItem); - if (!writeState.compareAndSet(curState, curState.withReadyAndDrained(false))) { + } + + // Path 2: Container is secretly not ready (Tomcat bug) OR already known to be false. + // We must safely buffer the item and ensure the state reflects reality. + writeChain.offer(actionItem); + if (!writeState.compareAndSet(curState, curState.withReadyAndDrained(false))) { + // CAS failed. State changed mid-flight. + if (curState.readyAndDrained) { + // Started as true, but CAS failed because another thread + // concurrently buffered and flipped it to false. + // Safe to do nothing. The winning thread handles the unpark. + } else { + // Started as false, CAS failed because onWritePossible flipped it to true. + // Original logic: retry the write since it's ready again. checkState( writeState.get().readyAndDrained, "Bug: onWritePossible() should have changed readyAndDrained to true, but not"); @@ -240,7 +256,15 @@ private void runOrBuffer(ActionItem actionItem) throws IOException { checkState(lastItem == actionItem, "Bug: lastItem != actionItem"); runOrBuffer(lastItem); } - } // state has not changed since + } + } else { + // CAS succeeded! + // CRITICAL FIX: If we just flipped the state from true to false, + // we MUST wake up the container! + if (curState.readyAndDrained) { + LockSupport.unpark(parkingThread); + log.finest("the servlet output stream becomes not ready"); + } } }