|
11 | 11 |
|
12 | 12 | import io.netty.buffer.Unpooled; |
13 | 13 | import io.netty.channel.ChannelHandlerContext; |
| 14 | +import io.netty.channel.ChannelInboundHandlerAdapter; |
| 15 | +import io.netty.channel.DefaultEventLoop; |
14 | 16 | import io.netty.channel.SimpleChannelInboundHandler; |
15 | 17 | import io.netty.channel.embedded.EmbeddedChannel; |
16 | 18 | import io.netty.handler.codec.http.DefaultHttpContent; |
|
19 | 21 | import io.netty.handler.flow.FlowControlHandler; |
20 | 22 |
|
21 | 23 | import org.elasticsearch.common.bytes.ReleasableBytesReference; |
| 24 | +import org.elasticsearch.common.network.ThreadWatchdog; |
22 | 25 | import org.elasticsearch.common.settings.Settings; |
23 | 26 | import org.elasticsearch.common.util.concurrent.ThreadContext; |
24 | 27 | import org.elasticsearch.http.HttpBody; |
|
27 | 30 | import java.util.ArrayList; |
28 | 31 | import java.util.HashMap; |
29 | 32 | import java.util.Map; |
| 33 | +import java.util.concurrent.CountDownLatch; |
| 34 | +import java.util.concurrent.TimeUnit; |
30 | 35 | import java.util.concurrent.atomic.AtomicBoolean; |
31 | 36 | import java.util.concurrent.atomic.AtomicInteger; |
32 | 37 | import java.util.concurrent.atomic.AtomicReference; |
|
35 | 40 |
|
36 | 41 | public class Netty4HttpRequestBodyStreamTests extends ESTestCase { |
37 | 42 |
|
| 43 | + static HttpBody.ChunkHandler discardHandler = (chunk, isLast) -> chunk.close(); |
38 | 44 | private final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); |
39 | 45 | private EmbeddedChannel channel; |
40 | 46 | private Netty4HttpRequestBodyStream stream; |
41 | | - static HttpBody.ChunkHandler discardHandler = (chunk, isLast) -> chunk.close(); |
| 47 | + private ThreadWatchdog.ActivityTracker activityTracker; |
42 | 48 |
|
43 | 49 | @Override |
44 | 50 | public void setUp() throws Exception { |
45 | 51 | super.setUp(); |
46 | 52 | channel = new EmbeddedChannel(); |
47 | | - threadContext.putHeader("header1", "value1"); |
48 | | - stream = new Netty4HttpRequestBodyStream(channel, threadContext); |
| 53 | + activityTracker = new ThreadWatchdog.ActivityTracker(); |
| 54 | + stream = new Netty4HttpRequestBodyStream(channel, threadContext, activityTracker); |
49 | 55 | stream.setHandler(discardHandler); // set default handler, each test might override one |
50 | 56 | channel.pipeline().addLast(new SimpleChannelInboundHandler<HttpContent>(false) { |
51 | 57 | @Override |
@@ -128,57 +134,112 @@ public void testReadFromChannel() { |
128 | 134 | } |
129 | 135 |
|
130 | 136 | public void testReadFromHasCorrectThreadContext() throws InterruptedException { |
131 | | - var gotLast = new AtomicBoolean(false); |
132 | 137 | AtomicReference<Map<String, String>> headers = new AtomicReference<>(); |
133 | | - stream.setHandler(new HttpBody.ChunkHandler() { |
134 | | - @Override |
135 | | - public void onNext(ReleasableBytesReference chunk, boolean isLast) { |
136 | | - headers.set(threadContext.getHeaders()); |
137 | | - gotLast.set(isLast); |
138 | | - chunk.close(); |
139 | | - } |
140 | | - |
141 | | - @Override |
142 | | - public void close() { |
143 | | - headers.set(threadContext.getHeaders()); |
144 | | - } |
145 | | - }); |
146 | | - channel.pipeline().addFirst(new FlowControlHandler()); // block all incoming messages, need explicit channel.read() |
| 138 | + var eventLoop = new DefaultEventLoop(); |
| 139 | + var gotLast = new AtomicBoolean(false); |
147 | 140 | var chunkSize = 1024; |
| 141 | + threadContext.putHeader("header1", "value1"); |
| 142 | + try { |
| 143 | + // activity tracker requires stream execution in the same thread, setting up stream inside event-loop |
| 144 | + eventLoop.submit(() -> { |
| 145 | + channel = new EmbeddedChannel(); |
| 146 | + stream = new Netty4HttpRequestBodyStream(channel, threadContext, new ThreadWatchdog.ActivityTracker()); |
| 147 | + channel.pipeline().addLast(new SimpleChannelInboundHandler<HttpContent>(false) { |
| 148 | + @Override |
| 149 | + protected void channelRead0(ChannelHandlerContext ctx, HttpContent msg) { |
| 150 | + stream.handleNettyContent(msg); |
| 151 | + } |
| 152 | + }); |
| 153 | + stream.setHandler(new HttpBody.ChunkHandler() { |
| 154 | + @Override |
| 155 | + public void onNext(ReleasableBytesReference chunk, boolean isLast) { |
| 156 | + headers.set(threadContext.getHeaders()); |
| 157 | + gotLast.set(isLast); |
| 158 | + chunk.close(); |
| 159 | + } |
| 160 | + |
| 161 | + @Override |
| 162 | + public void close() { |
| 163 | + headers.set(threadContext.getHeaders()); |
| 164 | + } |
| 165 | + }); |
| 166 | + channel.pipeline().addFirst(new FlowControlHandler()); // block all incoming messages, need explicit channel.read() |
| 167 | + }).await(); |
148 | 168 |
|
149 | | - channel.writeInbound(randomContent(chunkSize)); |
150 | | - channel.writeInbound(randomLastContent(chunkSize)); |
| 169 | + channel.writeInbound(randomContent(chunkSize)); |
| 170 | + channel.writeInbound(randomLastContent(chunkSize)); |
151 | 171 |
|
152 | | - threadContext.putHeader("header2", "value2"); |
153 | | - stream.next(); |
| 172 | + threadContext.putHeader("header2", "value2"); |
| 173 | + stream.next(); |
154 | 174 |
|
155 | | - Thread thread = new Thread(() -> channel.runPendingTasks()); |
156 | | - thread.start(); |
157 | | - thread.join(); |
| 175 | + eventLoop.submit(() -> channel.runPendingTasks()).await(); |
| 176 | + assertThat(headers.get(), hasEntry("header1", "value1")); |
| 177 | + assertThat(headers.get(), hasEntry("header2", "value2")); |
158 | 178 |
|
159 | | - assertThat(headers.get(), hasEntry("header1", "value1")); |
160 | | - assertThat(headers.get(), hasEntry("header2", "value2")); |
| 179 | + threadContext.putHeader("header3", "value3"); |
| 180 | + stream.next(); |
161 | 181 |
|
162 | | - threadContext.putHeader("header3", "value3"); |
163 | | - stream.next(); |
| 182 | + eventLoop.submit(() -> channel.runPendingTasks()).await(); |
| 183 | + assertThat(headers.get(), hasEntry("header1", "value1")); |
| 184 | + assertThat(headers.get(), hasEntry("header2", "value2")); |
| 185 | + assertThat(headers.get(), hasEntry("header3", "value3")); |
164 | 186 |
|
165 | | - thread = new Thread(() -> channel.runPendingTasks()); |
166 | | - thread.start(); |
167 | | - thread.join(); |
| 187 | + assertTrue("should receive last content", gotLast.get()); |
168 | 188 |
|
169 | | - assertThat(headers.get(), hasEntry("header1", "value1")); |
170 | | - assertThat(headers.get(), hasEntry("header2", "value2")); |
171 | | - assertThat(headers.get(), hasEntry("header3", "value3")); |
| 189 | + headers.set(new HashMap<>()); |
172 | 190 |
|
173 | | - assertTrue("should receive last content", gotLast.get()); |
| 191 | + stream.close(); |
| 192 | + |
| 193 | + assertThat(headers.get(), hasEntry("header1", "value1")); |
| 194 | + assertThat(headers.get(), hasEntry("header2", "value2")); |
| 195 | + assertThat(headers.get(), hasEntry("header3", "value3")); |
| 196 | + } finally { |
| 197 | + eventLoop.shutdownGracefully(0, 0, TimeUnit.SECONDS); |
| 198 | + } |
| 199 | + } |
174 | 200 |
|
175 | | - headers.set(new HashMap<>()); |
| 201 | + public void testStreamNextActivityTracker() { |
| 202 | + var t0 = activityTracker.get(); |
| 203 | + var N = between(1, 10); |
| 204 | + for (int i = 0; i < N; i++) { |
| 205 | + channel.writeInbound(randomContent(1024)); |
| 206 | + stream.next(); |
| 207 | + channel.runPendingTasks(); |
| 208 | + } |
| 209 | + var t1 = activityTracker.get(); |
| 210 | + assertEquals("stream#next() must trigger activity tracker: N*step=" + N + "*2=" + N * 2L + " times", t1, t0 + N * 2L); |
| 211 | + } |
176 | 212 |
|
177 | | - stream.close(); |
| 213 | + // ensure that we catch all exceptions and throw them into channel pipeline |
| 214 | + public void testCatchExceptions() { |
| 215 | + var gotExceptions = new CountDownLatch(3); // number of tests below |
| 216 | + |
| 217 | + channel.pipeline().addLast(new ChannelInboundHandlerAdapter() { |
| 218 | + @Override |
| 219 | + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { |
| 220 | + gotExceptions.countDown(); |
| 221 | + } |
| 222 | + }); |
| 223 | + |
| 224 | + // catch exception for not buffered chunk, will be thrown on channel.fireChannelRead() |
| 225 | + stream.setHandler((a, b) -> { throw new RuntimeException(); }); |
| 226 | + stream.next(); |
| 227 | + channel.runPendingTasks(); |
| 228 | + channel.writeInbound(randomContent(1)); |
| 229 | + |
| 230 | + // catch exception for buffered chunk, will be thrown from eventLoop.submit() |
| 231 | + channel.writeInbound(randomContent(1)); |
| 232 | + stream.next(); |
| 233 | + channel.runPendingTasks(); |
| 234 | + |
| 235 | + // should catch OOM exceptions too, see DieWithDignity |
| 236 | + // swallowing exceptions can result in dangling streams, hanging channels, and delayed shutdowns |
| 237 | + stream.setHandler((a, b) -> { throw new OutOfMemoryError(); }); |
| 238 | + channel.writeInbound(randomContent(1)); |
| 239 | + stream.next(); |
| 240 | + channel.runPendingTasks(); |
178 | 241 |
|
179 | | - assertThat(headers.get(), hasEntry("header1", "value1")); |
180 | | - assertThat(headers.get(), hasEntry("header2", "value2")); |
181 | | - assertThat(headers.get(), hasEntry("header3", "value3")); |
| 242 | + safeAwait(gotExceptions); |
182 | 243 | } |
183 | 244 |
|
184 | 245 | HttpContent randomContent(int size, boolean isLast) { |
|
0 commit comments