diff --git a/servlet/src/main/java/io/grpc/servlet/AsyncServletOutputStreamWriter.java b/servlet/src/main/java/io/grpc/servlet/AsyncServletOutputStreamWriter.java index 3c8d3d07571..be9452daeda 100644 --- a/servlet/src/main/java/io/grpc/servlet/AsyncServletOutputStreamWriter.java +++ b/servlet/src/main/java/io/grpc/servlet/AsyncServletOutputStreamWriter.java @@ -28,14 +28,11 @@ import java.io.IOException; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.LockSupport; import java.util.function.BiFunction; import java.util.function.BooleanSupplier; import java.util.logging.Level; import java.util.logging.Logger; -import javax.annotation.Nullable; import javax.servlet.AsyncContext; import javax.servlet.ServletOutputStream; @@ -76,10 +73,6 @@ final class AsyncServletOutputStreamWriter { */ // SPSC queue would do private final Queue writeChain = new ConcurrentLinkedQueue<>(); - // for a theoretical race condition that onWritePossible() is called immediately after isReady() - // returns false and before writeState.compareAndSet() - @Nullable - private volatile Thread parkingThread; AsyncServletOutputStreamWriter( AsyncContext asyncContext, @@ -173,9 +166,6 @@ void complete() { /** Called from the container thread {@link javax.servlet.WriteListener#onWritePossible()}. */ void onWritePossible() throws IOException { log.finest("onWritePossible: ENTRY. The servlet output stream becomes ready"); - if (writeState.get().readyAndDrained) { - assureReadyAndDrainedTurnsFalse(); - } while (isReady.getAsBoolean()) { WriteState curState = writeState.get(); @@ -198,17 +188,6 @@ void onWritePossible() throws IOException { log.finest("onWritePossible: EXIT. The servlet output stream becomes not ready"); } - private void assureReadyAndDrainedTurnsFalse() { - // readyAndDrained should have been set to false already. - // Just in case due to a race condition readyAndDrained is still true at this moment and is - // being set to false by runOrBuffer() concurrently. - parkingThread = Thread.currentThread(); - while (writeState.get().readyAndDrained) { - LockSupport.parkNanos(TimeUnit.MINUTES.toNanos(1)); // should return immediately - } - parkingThread = null; - } - /** * Either execute the write action directly, or buffer the action and let the container thread * drain it. @@ -223,10 +202,7 @@ private void runOrBuffer(ActionItem actionItem) throws IOException { 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"); + writeState.set(curState.withReadyAndDrained(false)); log.finest("the servlet output stream becomes not ready"); } } else { // buffer to the writeChain diff --git a/servlet/src/test/java/io/grpc/servlet/AsyncServletOutputStreamWriterTest.java b/servlet/src/test/java/io/grpc/servlet/AsyncServletOutputStreamWriterTest.java new file mode 100644 index 00000000000..dce14ac50d0 --- /dev/null +++ b/servlet/src/test/java/io/grpc/servlet/AsyncServletOutputStreamWriterTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2026 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.servlet; + +import static org.junit.Assert.assertTrue; + +import io.grpc.servlet.AsyncServletOutputStreamWriter.ActionItem; +import io.grpc.servlet.AsyncServletOutputStreamWriter.Log; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link AsyncServletOutputStreamWriter}. */ +@RunWith(JUnit4.class) +public class AsyncServletOutputStreamWriterTest { + + @Test(timeout = 5000) + public void onWritePossibleWhenAlreadyReadyAndDrainedReturnsImmediately() throws IOException { + AtomicBoolean written = new AtomicBoolean(false); + ActionItem writeAction = () -> written.set(true); + + AsyncServletOutputStreamWriter writer = + new AsyncServletOutputStreamWriter( + (bytes, len) -> writeAction, + () -> { }, + () -> { }, + () -> true, + new Log() { }); + + // Initial onWritePossible call turns readyAndDrained to true + writer.onWritePossible(); + + long startTime = System.nanoTime(); + // Subsequent call when readyAndDrained is true must return non-blockingly + writer.onWritePossible(); + long durationMs = (System.nanoTime() - startTime) / 1_000_000; + + assertTrue("onWritePossible must not park or block thread", durationMs < 1000); + } +}