1+ /*
2+ * Copyright 2026 The gRPC Authors
3+ *
4+ * Licensed under the Apache License, Version 2.0 (the "License");
5+ * you may not use this file except in compliance with the License.
6+ * You may obtain a copy of the License at
7+ *
8+ * http://www.apache.org/licenses/LICENSE-2.0
9+ *
10+ * Unless required by applicable law or agreed to in writing, software
11+ * distributed under the License is distributed on an "AS IS" BASIS,
12+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+ * See the License for the specific language governing permissions and
14+ * limitations under the License.
15+ */
16+
17+ package io .grpc .servlet ;
18+
19+ import static org .junit .Assert .assertEquals ;
20+ import static org .junit .Assert .assertTrue ;
21+
22+ import io .grpc .servlet .AsyncServletOutputStreamWriter .ActionItem ;
23+ import io .grpc .servlet .AsyncServletOutputStreamWriter .Log ;
24+ import java .io .IOException ;
25+ import java .util .ArrayList ;
26+ import java .util .List ;
27+ import java .util .concurrent .atomic .AtomicBoolean ;
28+ import java .util .concurrent .atomic .AtomicInteger ;
29+ import java .util .function .BiFunction ;
30+ import java .util .function .BooleanSupplier ;
31+ import org .junit .Test ;
32+ import org .junit .runner .RunWith ;
33+ import org .junit .runners .JUnit4 ;
34+
35+ /**
36+ * Unit test for {@link AsyncServletOutputStreamWriter} with a mock isReady supplier
37+ * that always returns true. This specifically tests the scenario where isReady stays
38+ * true across multiple write calls (as can happen with Tomcat and other servlet
39+ * containers that defer onWritePossible until the previous write completes internally).
40+ */
41+ @ RunWith (JUnit4 .class )
42+ public class AsyncServletOutputStreamWriterTest {
43+
44+ /**
45+ * Test that multiple consecutive writes succeed even when isReady() always returns true.
46+ * This reproduces the Tomcat issue where isReady() returns true but write() still fails
47+ * because the previous write hasn't completed internally.
48+ */
49+ @ Test
50+ public void writeBytes_alwaysReady_doesNotStall () throws IOException {
51+ AtomicBoolean isReady = new AtomicBoolean (true );
52+ List <byte []> writtenData = new ArrayList <>();
53+ AtomicInteger onWritePossibleCount = new AtomicInteger (0 );
54+
55+ BiFunction <byte [], Integer , ActionItem > writeAction =
56+ (bytes , numBytes ) -> () -> {
57+ writtenData .add (bytes );
58+ };
59+
60+ ActionItem flushAction = () -> {};
61+
62+ ActionItem completeAction = () -> {};
63+
64+ BooleanSupplier isReadySupplier = () -> {
65+ // Note: isReady stays true throughout this test
66+ return isReady .get ();
67+ };
68+
69+ AsyncServletOutputStreamWriter writer =
70+ new AsyncServletOutputStreamWriter (writeAction , flushAction , completeAction , isReadySupplier , new Log () {});
71+
72+ // Simulate the first onWritePossible call (container init)
73+ isReady .set (true );
74+ writer .onWritePossible ();
75+ onWritePossibleCount .incrementAndGet ();
76+
77+ // Write multiple times while isReady stays true
78+ // Before the fix, writes would stall because isReady returned true
79+ // but the previous write hadn't completed internally
80+ for (int i = 0 ; i < 5 ; i ++) {
81+ byte [] data = new byte []{(byte ) i };
82+ writer .writeBytes (data , 1 );
83+ }
84+
85+ // Verify all writes completed
86+ assertEquals ("All writes should complete" , 5 , writtenData .size ());
87+ }
88+
89+ /**
90+ * Test that writeBytes with isReady returning false eventually triggers onWritePossible.
91+ */
92+ @ Test
93+ public void writeBytes_isReadyBecomesFalse_triggersOnWritePossible () throws IOException {
94+ AtomicBoolean isReady = new AtomicBoolean (true );
95+ List <String > actions = new ArrayList <>();
96+
97+ BiFunction <byte [], Integer , ActionItem > writeAction =
98+ (bytes , numBytes ) -> () -> {
99+ actions .add ("write" );
100+ };
101+
102+ ActionItem flushAction = () -> {
103+ actions .add ("flush" );
104+ };
105+
106+ ActionItem completeAction = () -> {};
107+
108+ BooleanSupplier isReadySupplier = () -> {
109+ return isReady .get ();
110+ };
111+
112+ AsyncServletOutputStreamWriter writer =
113+ new AsyncServletOutputStreamWriter (writeAction , flushAction , completeAction , isReadySupplier , new Log () {});
114+
115+ // Initial onWritePossible
116+ writer .onWritePossible ();
117+
118+ // First write - isReady is true
119+ byte [] data1 = new byte []{1 };
120+ writer .writeBytes (data1 , 1 );
121+
122+ // Simulate isReady becoming false (Tomcat calls onWritePossible when ready)
123+ isReady .set (false );
124+ writer .onWritePossible ();
125+
126+ // Second write after isReady returns to true
127+ isReady .set (true );
128+ byte [] data2 = new byte []{2 };
129+ writer .writeBytes (data2 , 1 );
130+
131+ assertEquals ("Two writes should complete" , 2 , actions .size ());
132+ assertEquals ("write" , actions .get (0 ));
133+ assertEquals ("write" , actions .get (1 ));
134+ }
135+ }
0 commit comments