Don't swallow intermediate write failures in MessageToMessageEncoder (#8454)
Motivation: If the encoder needs to flush more than one outbound message it will create a new ChannelPromise for all but the last write which will swallow failures. Modification: Use a PromiseCombiner in the case of multiple messages and the parent promise isn't the `VoidPromise`. Result: Intermediate failures are propagated to the original ChannelPromise.
This commit is contained in:
parent
9f6ebab514
commit
6563f23a9b
@ -22,6 +22,7 @@ import io.netty.channel.ChannelPipeline;
|
|||||||
import io.netty.channel.ChannelPromise;
|
import io.netty.channel.ChannelPromise;
|
||||||
import io.netty.util.ReferenceCountUtil;
|
import io.netty.util.ReferenceCountUtil;
|
||||||
import io.netty.util.ReferenceCounted;
|
import io.netty.util.ReferenceCounted;
|
||||||
|
import io.netty.util.concurrent.PromiseCombiner;
|
||||||
import io.netty.util.internal.StringUtil;
|
import io.netty.util.internal.StringUtil;
|
||||||
import io.netty.util.internal.TypeParameterMatcher;
|
import io.netty.util.internal.TypeParameterMatcher;
|
||||||
|
|
||||||
@ -108,28 +109,36 @@ public abstract class MessageToMessageEncoder<I> extends ChannelOutboundHandlerA
|
|||||||
if (out != null) {
|
if (out != null) {
|
||||||
final int sizeMinusOne = out.size() - 1;
|
final int sizeMinusOne = out.size() - 1;
|
||||||
if (sizeMinusOne == 0) {
|
if (sizeMinusOne == 0) {
|
||||||
ctx.write(out.get(0), promise);
|
ctx.write(out.getUnsafe(0), promise);
|
||||||
} else if (sizeMinusOne > 0) {
|
} else if (sizeMinusOne > 0) {
|
||||||
// Check if we can use a voidPromise for our extra writes to reduce GC-Pressure
|
// Check if we can use a voidPromise for our extra writes to reduce GC-Pressure
|
||||||
// See https://github.com/netty/netty/issues/2525
|
// See https://github.com/netty/netty/issues/2525
|
||||||
ChannelPromise voidPromise = ctx.voidPromise();
|
if (promise == ctx.voidPromise()) {
|
||||||
boolean isVoidPromise = promise == voidPromise;
|
writeVoidPromise(ctx, out);
|
||||||
for (int i = 0; i < sizeMinusOne; i ++) {
|
|
||||||
ChannelPromise p;
|
|
||||||
if (isVoidPromise) {
|
|
||||||
p = voidPromise;
|
|
||||||
} else {
|
} else {
|
||||||
p = ctx.newPromise();
|
writePromiseCombiner(ctx, out, promise);
|
||||||
}
|
}
|
||||||
ctx.write(out.getUnsafe(i), p);
|
|
||||||
}
|
|
||||||
ctx.write(out.getUnsafe(sizeMinusOne), promise);
|
|
||||||
}
|
}
|
||||||
out.recycle();
|
out.recycle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void writeVoidPromise(ChannelHandlerContext ctx, CodecOutputList out) {
|
||||||
|
final ChannelPromise voidPromise = ctx.voidPromise();
|
||||||
|
for (int i = 0; i < out.size(); i++) {
|
||||||
|
ctx.write(out.getUnsafe(i), voidPromise);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writePromiseCombiner(ChannelHandlerContext ctx, CodecOutputList out, ChannelPromise promise) {
|
||||||
|
final PromiseCombiner combiner = new PromiseCombiner();
|
||||||
|
for (int i = 0; i < out.size(); i++) {
|
||||||
|
combiner.add(ctx.write(out.getUnsafe(i)));
|
||||||
|
}
|
||||||
|
combiner.finish(promise);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encode from one message to an other. This method will be called for each written message that can be handled
|
* Encode from one message to an other. This method will be called for each written message that can be handled
|
||||||
* by this encoder.
|
* by this encoder.
|
||||||
|
@ -15,9 +15,14 @@
|
|||||||
*/
|
*/
|
||||||
package io.netty.handler.codec;
|
package io.netty.handler.codec;
|
||||||
|
|
||||||
|
import io.netty.channel.ChannelFuture;
|
||||||
|
import io.netty.channel.ChannelHandler;
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
|
import io.netty.channel.ChannelOutboundHandlerAdapter;
|
||||||
|
import io.netty.channel.ChannelPromise;
|
||||||
import io.netty.channel.embedded.EmbeddedChannel;
|
import io.netty.channel.embedded.EmbeddedChannel;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -37,4 +42,37 @@ public class MessageToMessageEncoderTest {
|
|||||||
});
|
});
|
||||||
channel.writeOutbound(new Object());
|
channel.writeOutbound(new Object());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testIntermediateWriteFailures() {
|
||||||
|
ChannelHandler encoder = new MessageToMessageEncoder<Object>() {
|
||||||
|
@Override
|
||||||
|
protected void encode(ChannelHandlerContext ctx, Object msg, List<Object> out) {
|
||||||
|
out.add(new Object());
|
||||||
|
out.add(msg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
final Exception firstWriteException = new Exception();
|
||||||
|
|
||||||
|
ChannelHandler writeThrower = new ChannelOutboundHandlerAdapter() {
|
||||||
|
private boolean firstWritten;
|
||||||
|
@Override
|
||||||
|
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
|
||||||
|
if (firstWritten) {
|
||||||
|
ctx.write(msg, promise);
|
||||||
|
} else {
|
||||||
|
firstWritten = true;
|
||||||
|
promise.setFailure(firstWriteException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
EmbeddedChannel channel = new EmbeddedChannel(writeThrower, encoder);
|
||||||
|
Object msg = new Object();
|
||||||
|
ChannelFuture write = channel.writeAndFlush(msg);
|
||||||
|
assertSame(firstWriteException, write.cause());
|
||||||
|
assertSame(msg, channel.readOutbound());
|
||||||
|
assertFalse(channel.finish());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user