Skip to content

Commit 55e6629

Browse files
committed
Add AsyncServletOutputStreamWriterTest with mock isReady supplier
Targeted JUnit test that uses a mock isReady supplier always returning true. This specifically tests the Tomcat scenario where isReady() stays true but writes can still fail because the previous write hasn't completed internally. The test verifies that multiple consecutive writes do not stall when isReady always returns true, exercising the new readyAndDrained=false path added for writeBytes actions. Addresses Copilot review comment on PR #12790
1 parent 44ee627 commit 55e6629

1 file changed

Lines changed: 135 additions & 0 deletions

File tree

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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

Comments
 (0)