Skip to content

Commit b2cde71

Browse files
committed
chore: trigger CI pipeline
1 parent 9bf9ad1 commit b2cde71

File tree

1 file changed

+42
-30
lines changed

1 file changed

+42
-30
lines changed

servlet/src/main/java/io/grpc/servlet/AsyncServletOutputStreamWriter.java

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -217,42 +217,54 @@ private void assureReadyAndDrainedTurnsFalse() {
217217
*/
218218
private void runOrBuffer(ActionItem actionItem) throws IOException {
219219
WriteState curState = writeState.get();
220-
221-
// Evaluate Tomcat's actual state alongside our cached state
222-
boolean actualReady = curState.readyAndDrained && isReady.getAsBoolean();
223220

224-
if (actualReady) { // write to the outputStream directly
225-
actionItem.run();
226-
if (actionItem == completeAction) {
221+
if (curState.readyAndDrained) {
222+
if (isReady.getAsBoolean()) {
223+
// Path 1: Container is truly ready. Write directly.
224+
actionItem.run();
225+
if (actionItem == completeAction) {
226+
return;
227+
}
228+
if (!isReady.getAsBoolean()) {
229+
boolean successful =
230+
writeState.compareAndSet(curState, curState.withReadyAndDrained(false));
231+
LockSupport.unpark(parkingThread);
232+
checkState(successful, "Bug: curState is unexpectedly changed by another thread");
233+
log.finest("the servlet output stream becomes not ready");
234+
}
227235
return;
228236
}
229-
if (!isReady.getAsBoolean()) {
230-
boolean successful =
231-
writeState.compareAndSet(curState, curState.withReadyAndDrained(false));
237+
}
238+
239+
// Path 2: Container is secretly not ready (Tomcat bug) OR already known to be false.
240+
// We must safely buffer the item and ensure the state reflects reality.
241+
writeChain.offer(actionItem);
242+
if (!writeState.compareAndSet(curState, curState.withReadyAndDrained(false))) {
243+
// CAS failed. State changed mid-flight.
244+
if (curState.readyAndDrained) {
245+
// Started as true, but CAS failed because another thread
246+
// concurrently buffered and flipped it to false.
247+
// Safe to do nothing. The winning thread handles the unpark.
248+
} else {
249+
// Started as false, CAS failed because onWritePossible flipped it to true.
250+
// Original logic: retry the write since it's ready again.
251+
checkState(
252+
writeState.get().readyAndDrained,
253+
"Bug: onWritePossible() should have changed readyAndDrained to true, but not");
254+
ActionItem lastItem = writeChain.poll();
255+
if (lastItem != null) {
256+
checkState(lastItem == actionItem, "Bug: lastItem != actionItem");
257+
runOrBuffer(lastItem);
258+
}
259+
}
260+
} else {
261+
// CAS succeeded!
262+
// CRITICAL FIX: If we just flipped the state from true to false,
263+
// we MUST wake up the container!
264+
if (curState.readyAndDrained) {
232265
LockSupport.unpark(parkingThread);
233-
checkState(successful, "Bug: curState is unexpectedly changed by another thread");
234266
log.finest("the servlet output stream becomes not ready");
235267
}
236-
} else { // buffer to the writeChain
237-
writeChain.offer(actionItem);
238-
if (!writeState.compareAndSet(curState, curState.withReadyAndDrained(false))) {
239-
// STATE CHANGED! Determine why the CAS failed based on our initial state.
240-
if (curState.readyAndDrained) {
241-
// We dropped here solely because isReady() was false.
242-
// CAS failed because another concurrent thread already CAS'd it to false.
243-
// This is completely safe. Tomcat will call onWritePossible(). Do nothing.
244-
} else {
245-
// Original logic: We started as false, CAS failed because onWritePossible set it to true.
246-
checkState(
247-
writeState.get().readyAndDrained,
248-
"Bug: onWritePossible() should have changed readyAndDrained to true, but not");
249-
ActionItem lastItem = writeChain.poll();
250-
if (lastItem != null) {
251-
checkState(lastItem == actionItem, "Bug: lastItem != actionItem");
252-
runOrBuffer(lastItem);
253-
}
254-
}
255-
} // state has not changed since
256268
}
257269
}
258270

0 commit comments

Comments
 (0)