View Javadoc
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 }