@@ -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