1 /*
2 * Copyright 2012 The Netty Project
3 *
4 * The Netty Project licenses this file to you under the Apache License,
5 * version 2.0 (the "License"); you may not use this file except in compliance
6 * with the License. You may obtain a copy of the License at:
7 *
8 * https://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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations
14 * under the License.
15 */
16 package io.netty.handler.stream;
17
18 import static io.netty.util.internal.ObjectUtil.checkPositive;
19
20 import io.netty.buffer.ByteBufAllocator;
21 import io.netty.buffer.Unpooled;
22 import io.netty.channel.Channel;
23 import io.netty.channel.ChannelDuplexHandler;
24 import io.netty.channel.ChannelException;
25 import io.netty.channel.ChannelFuture;
26 import io.netty.channel.ChannelFutureListener;
27 import io.netty.channel.ChannelHandler;
28 import io.netty.channel.ChannelHandlerContext;
29 import io.netty.channel.ChannelPipeline;
30 import io.netty.channel.ChannelProgressivePromise;
31 import io.netty.channel.ChannelPromise;
32 import io.netty.util.ReferenceCountUtil;
33 import io.netty.util.internal.logging.InternalLogger;
34 import io.netty.util.internal.logging.InternalLoggerFactory;
35
36 import java.nio.channels.ClosedChannelException;
37 import java.util.ArrayDeque;
38 import java.util.Queue;
39
40 /**
41 * A {@link ChannelHandler} that adds support for writing a large data stream
42 * asynchronously neither spending a lot of memory nor getting
43 * {@link OutOfMemoryError}. Large data streaming such as file
44 * transfer requires complicated state management in a {@link ChannelHandler}
45 * implementation. {@link ChunkedWriteHandler} manages such complicated states
46 * so that you can send a large data stream without difficulties.
47 * <p>
48 * To use {@link ChunkedWriteHandler} in your application, you have to insert
49 * a new {@link ChunkedWriteHandler} instance:
50 * <pre>
51 * {@link ChannelPipeline} p = ...;
52 * p.addLast("streamer", <b>new {@link ChunkedWriteHandler}()</b>);
53 * p.addLast("handler", new MyHandler());
54 * </pre>
55 * Once inserted, you can write a {@link ChunkedInput} so that the
56 * {@link ChunkedWriteHandler} can pick it up and fetch the content of the
57 * stream chunk by chunk and write the fetched chunk downstream:
58 * <pre>
59 * {@link Channel} ch = ...;
60 * ch.write(new {@link ChunkedFile}(new File("video.mkv"));
61 * </pre>
62 *
63 * <h3>Sending a stream which generates a chunk intermittently</h3>
64 *
65 * Some {@link ChunkedInput} generates a chunk on a certain event or timing.
66 * Such {@link ChunkedInput} implementation often returns {@code null} on
67 * {@link ChunkedInput#readChunk(ChannelHandlerContext)}, resulting in the indefinitely suspended
68 * transfer. To resume the transfer when a new chunk is available, you have to
69 * call {@link #resumeTransfer()}.
70 */
71 public class ChunkedWriteHandler extends ChannelDuplexHandler {
72
73 private static final InternalLogger logger =
74 InternalLoggerFactory.getInstance(ChunkedWriteHandler.class);
75
76 private Queue<PendingWrite> queue;
77 private volatile ChannelHandlerContext ctx;
78 private boolean inFlush;
79 private boolean flushPending;
80
81 public ChunkedWriteHandler() {
82 }
83
84 /**
85 * @deprecated use {@link #ChunkedWriteHandler()}
86 */
87 @Deprecated
88 public ChunkedWriteHandler(int maxPendingWrites) {
89 checkPositive(maxPendingWrites, "maxPendingWrites");
90 }
91
92 private void allocateQueue() {
93 if (queue == null) {
94 queue = new ArrayDeque<PendingWrite>();
95 }
96 }
97
98 private boolean queueIsEmpty() {
99 return queue == null || queue.isEmpty();
100 }
101
102 @Override
103 public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
104 this.ctx = ctx;
105 }
106
107 @Override
108 public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
109 // Nothing will write the queued messages once this handler was removed, not even channelInactive(),
110 // so fail them now and close / release what they hold.
111 if (!queueIsEmpty()) {
112 // Only create the exception if there is something to fail, as creating it is expensive.
113 discard(new ChannelException("Pending write on removal of ChunkedWriteHandler"));
114 }
115 }
116
117 /**
118 * Continues to fetch the chunks from the input.
119 */
120 public void resumeTransfer() {
121 final ChannelHandlerContext ctx = this.ctx;
122 if (ctx == null) {
123 return;
124 }
125 if (ctx.executor().inEventLoop()) {
126 resumeTransfer0(ctx);
127 } else {
128 // let the transfer resume on the next event loop round
129 ctx.executor().execute(new Runnable() {
130
131 @Override
132 public void run() {
133 resumeTransfer0(ctx);
134 }
135 });
136 }
137 }
138
139 private void resumeTransfer0(ChannelHandlerContext ctx) {
140 try {
141 doFlush(ctx);
142 } catch (Exception e) {
143 logger.warn("Unexpected exception while sending chunks.", e);
144 }
145 }
146
147 @Override
148 public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
149 if (!queueIsEmpty() || msg instanceof ChunkedInput) {
150 allocateQueue();
151 queue.add(new PendingWrite(msg, promise));
152 } else {
153 ctx.write(msg, promise);
154 }
155 }
156
157 @Override
158 public void flush(ChannelHandlerContext ctx) throws Exception {
159 doFlush(ctx);
160 }
161
162 @Override
163 public void channelInactive(ChannelHandlerContext ctx) throws Exception {
164 doFlush(ctx);
165 ctx.fireChannelInactive();
166 }
167
168 @Override
169 public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
170 if (ctx.channel().isWritable()) {
171 // channel is writable again try to continue flushing
172 doFlush(ctx);
173 }
174 ctx.fireChannelWritabilityChanged();
175 }
176
177 private void discard(Throwable cause) {
178 if (queueIsEmpty()) {
179 return;
180 }
181 for (;;) {
182 PendingWrite currentWrite = queue.poll();
183
184 if (currentWrite == null) {
185 break;
186 }
187 Object message = currentWrite.msg;
188 if (message instanceof ChunkedInput) {
189 ChunkedInput<?> in = (ChunkedInput<?>) message;
190 boolean endOfInput;
191 long inputLength;
192 try {
193 endOfInput = in.isEndOfInput();
194 inputLength = in.length();
195 closeInput(in);
196 } catch (Exception e) {
197 closeInput(in);
198 currentWrite.fail(e);
199 logger.warn("ChunkedInput failed", e);
200 continue;
201 }
202
203 if (!endOfInput) {
204 if (cause == null) {
205 cause = new ClosedChannelException();
206 }
207 currentWrite.fail(cause);
208 } else {
209 currentWrite.success(inputLength);
210 }
211 } else {
212 if (cause == null) {
213 cause = new ClosedChannelException();
214 }
215 currentWrite.fail(cause);
216 }
217 }
218 }
219
220 private void doFlush(final ChannelHandlerContext ctx) {
221 if (inFlush) {
222 // doFlush() must not re-enter itself: ChunkedInput.readChunk(...) and isEndOfInput() run
223 // between queue.peek() and queue.remove(), and user code executed there may
224 // synchronously trigger flush(), resumeTransfer(), channelInactive(...) or
225 // channelWritabilityChanged(...) on this very handler. A nested doFlush() would peek
226 // and consume the same queue entry again, which made the outer invocation fail with
227 // NoSuchElementException and let both invocations read from the same (already closed)
228 // input. Remember the nested call instead: the outer invocation re-runs the drain
229 // loop before returning, so a resumeTransfer(...) that arrives while it is about to
230 // suspend is not lost (see the old lock-based guard and its flushRequired follow-up).
231 flushPending = true;
232 return;
233 }
234 inFlush = true;
235 try {
236 do {
237 flushPending = false;
238 doFlush0(ctx);
239 } while (flushPending);
240 } finally {
241 inFlush = false;
242 }
243 }
244
245 private void doFlush0(final ChannelHandlerContext ctx) {
246 final Channel channel = ctx.channel();
247 if (!channel.isActive()) {
248 // Even after discarding all previous queued objects we should propagate the flush through
249 // to ensure previous written objects via writeAndFlush(...) that were not queued will be flushed and
250 // so eventually fail the promise.
251 discard(null);
252 ctx.flush();
253 return;
254 }
255
256 if (queueIsEmpty()) {
257 ctx.flush();
258 return;
259 }
260
261 boolean requiresFlush = true;
262 ByteBufAllocator allocator = ctx.alloc();
263 while (channel.isWritable()) {
264 final PendingWrite currentWrite = queue.peek();
265
266 if (currentWrite == null) {
267 break;
268 }
269
270 if (currentWrite.promise.isDone()) {
271 // This might happen e.g. in the case when a write operation
272 // failed, but there are still unconsumed chunks left.
273 // Most chunked input sources would stop generating chunks
274 // and report end of input, but this doesn't work with any
275 // source wrapped in HttpChunkedInput.
276 // Note, that we're not trying to release the message/chunks
277 // as this had to be done already by someone who resolved the
278 // promise (using ChunkedInput.close method).
279 // See https://github.com/netty/netty/issues/8700.
280 // A promise that was cancelled by the user is different: nobody closed or released the message.
281 queue.remove();
282 if (currentWrite.promise.isCancelled()) {
283 if (currentWrite.msg instanceof ChunkedInput) {
284 closeInput((ChunkedInput<?>) currentWrite.msg);
285 } else {
286 ReferenceCountUtil.release(currentWrite.msg);
287 }
288 }
289 continue;
290 }
291
292 final Object pendingMessage = currentWrite.msg;
293
294 if (pendingMessage instanceof ChunkedInput) {
295 final ChunkedInput<?> chunks = (ChunkedInput<?>) pendingMessage;
296 boolean endOfInput;
297 boolean suspend;
298 Object message = null;
299 try {
300 message = chunks.readChunk(allocator);
301 endOfInput = chunks.isEndOfInput();
302 // No need to suspend when reached at the end.
303 suspend = message == null && !endOfInput;
304
305 } catch (final Throwable t) {
306 queue.remove();
307
308 if (message != null) {
309 ReferenceCountUtil.release(message);
310 }
311
312 closeInput(chunks);
313 currentWrite.fail(t);
314 break;
315 }
316
317 if (suspend) {
318 // ChunkedInput.nextChunk() returned null and it has
319 // not reached at the end of input. Let's wait until
320 // more chunks arrive. Nothing to write or notify.
321 break;
322 }
323
324 if (message == null) {
325 // If message is null write an empty ByteBuf.
326 // See https://github.com/netty/netty/issues/1671
327 message = Unpooled.EMPTY_BUFFER;
328 }
329
330 if (endOfInput) {
331 // We need to remove the element from the queue before we call writeAndFlush() as this operation
332 // may cause an action that also touches the queue.
333 queue.remove();
334 }
335 // Flush each chunk to conserve memory
336 ChannelFuture f = ctx.writeAndFlush(message);
337 if (endOfInput) {
338 if (f.isDone()) {
339 handleEndOfInputFuture(f, chunks, currentWrite);
340 } else {
341 // Register a listener which will close the input once the write is complete.
342 // This is needed because the Chunk may have some resource bound that can not
343 // be closed before it's not written.
344 //
345 // See https://github.com/netty/netty/issues/303
346 f.addListener((ChannelFutureListener) future ->
347 handleEndOfInputFuture(future, chunks, currentWrite));
348 }
349 } else {
350 final boolean resume = !channel.isWritable();
351 if (f.isDone()) {
352 handleFuture(f, chunks, currentWrite, resume);
353 } else {
354 f.addListener((ChannelFutureListener) future ->
355 handleFuture(future, chunks, currentWrite, resume));
356 }
357 }
358 requiresFlush = false;
359 } else {
360 queue.remove();
361 ctx.write(pendingMessage, currentWrite.promise);
362 requiresFlush = true;
363 }
364
365 if (!channel.isActive()) {
366 discard(new ClosedChannelException());
367 break;
368 }
369 }
370
371 if (requiresFlush) {
372 ctx.flush();
373 }
374 }
375
376 private static void handleEndOfInputFuture(ChannelFuture future, ChunkedInput<?> input, PendingWrite currentWrite) {
377 if (!future.isSuccess()) {
378 closeInput(input);
379 currentWrite.fail(future.cause());
380 } else {
381 // read state of the input in local variables before closing it
382 long inputProgress = input.progress();
383 long inputLength = input.length();
384 closeInput(input);
385 currentWrite.progress(inputProgress, inputLength);
386 currentWrite.success(inputLength);
387 }
388 }
389
390 private void handleFuture(ChannelFuture future, ChunkedInput<?> input, PendingWrite currentWrite, boolean resume) {
391 if (!future.isSuccess()) {
392 closeInput(input);
393 currentWrite.fail(future.cause());
394 } else {
395 currentWrite.progress(input.progress(), input.length());
396 if (resume && future.channel().isWritable()) {
397 resumeTransfer();
398 }
399 }
400 }
401
402 private static void closeInput(ChunkedInput<?> chunks) {
403 try {
404 chunks.close();
405 } catch (Throwable t) {
406 logger.warn("Failed to close a ChunkedInput.", t);
407 }
408 }
409
410 private static final class PendingWrite {
411 final Object msg;
412 final ChannelPromise promise;
413
414 PendingWrite(Object msg, ChannelPromise promise) {
415 this.msg = msg;
416 this.promise = promise;
417 }
418
419 void fail(Throwable cause) {
420 ReferenceCountUtil.release(msg);
421 promise.tryFailure(cause);
422 }
423
424 void success(long total) {
425 if (promise.isDone()) {
426 // No need to notify the progress or fulfill the promise because it's done already.
427 return;
428 }
429 progress(total, total);
430 promise.trySuccess();
431 }
432
433 void progress(long progress, long total) {
434 if (promise instanceof ChannelProgressivePromise) {
435 ((ChannelProgressivePromise) promise).tryProgress(progress, total);
436 }
437 }
438 }
439 }