Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -76,10 +73,6 @@ final class AsyncServletOutputStreamWriter {
*/
// SPSC queue would do
private final Queue<ActionItem> 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,
Expand Down Expand Up @@ -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();

Expand All @@ -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.
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading