Skip to content

Commit 97aa34f

Browse files
authored
Restore old behavior of NettyAdaptiveCumulator, but avoid using that class if Netty is on version 4.1.111 or later. (#11367) (#11369)
1 parent c2a3ed3 commit 97aa34f

File tree

3 files changed

+92
-60
lines changed

3 files changed

+92
-60
lines changed

netty/src/main/java/io/grpc/netty/GrpcHttp2ConnectionHandler.java

+36-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import static com.google.common.base.Preconditions.checkState;
2020

21+
import com.google.common.annotations.VisibleForTesting;
2122
import io.grpc.Attributes;
2223
import io.grpc.ChannelLogger;
2324
import io.grpc.Internal;
@@ -27,6 +28,7 @@
2728
import io.netty.handler.codec.http2.Http2ConnectionEncoder;
2829
import io.netty.handler.codec.http2.Http2ConnectionHandler;
2930
import io.netty.handler.codec.http2.Http2Settings;
31+
import io.netty.util.Version;
3032
import javax.annotation.Nullable;
3133

3234
/**
@@ -41,6 +43,30 @@ public abstract class GrpcHttp2ConnectionHandler extends Http2ConnectionHandler
4143
@Nullable
4244
protected final ChannelPromise channelUnused;
4345
private final ChannelLogger negotiationLogger;
46+
private static final boolean usingPre4_1_111_Netty;
47+
48+
static {
49+
// Netty 4.1.111 introduced a change in the behavior of duplicate() method
50+
// that breaks the assumption of the cumulator. We need to detect this version
51+
// and adjust the behavior accordingly.
52+
53+
boolean identifiedOldVersion = false;
54+
try {
55+
Version version = Version.identify().get("netty-buffer");
56+
if (version != null) {
57+
String[] split = version.artifactVersion().split("\\.");
58+
if (split.length >= 3
59+
&& Integer.parseInt(split[0]) == 4
60+
&& Integer.parseInt(split[1]) <= 1
61+
&& Integer.parseInt(split[2]) < 111) {
62+
identifiedOldVersion = true;
63+
}
64+
}
65+
} catch (Exception e) {
66+
// Ignore, we'll assume it's a new version.
67+
}
68+
usingPre4_1_111_Netty = identifiedOldVersion;
69+
}
4470

4571
protected GrpcHttp2ConnectionHandler(
4672
ChannelPromise channelUnused,
@@ -51,7 +77,16 @@ protected GrpcHttp2ConnectionHandler(
5177
super(decoder, encoder, initialSettings);
5278
this.channelUnused = channelUnused;
5379
this.negotiationLogger = negotiationLogger;
54-
setCumulator(ADAPTIVE_CUMULATOR);
80+
if (usingPre4_1_111_Netty()) {
81+
// We need to use the adaptive cumulator only if we're using a version of Netty that
82+
// doesn't have the behavior that breaks it.
83+
setCumulator(ADAPTIVE_CUMULATOR);
84+
}
85+
}
86+
87+
@VisibleForTesting
88+
static boolean usingPre4_1_111_Netty() {
89+
return usingPre4_1_111_Netty;
5590
}
5691

5792
/**

netty/src/main/java/io/grpc/netty/NettyAdaptiveCumulator.java

+26-24
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,6 @@
2727
/**
2828
* "Adaptive" cumulator: cumulate {@link ByteBuf}s by dynamically switching between merge and
2929
* compose strategies.
30-
* <br><br>
31-
*
32-
* <p><b><font color="red">Avoid using</font></b>
33-
* {@link CompositeByteBuf#addFlattenedComponents(boolean, ByteBuf)} as it can lead
34-
* to corruption, where the components' readable area are not equal to the Composite's capacity
35-
* (see https://github.com/netty/netty/issues/12844).
3630
*/
3731

3832
class NettyAdaptiveCumulator implements Cumulator {
@@ -95,7 +89,8 @@ public final ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBu
9589
composite.capacity(composite.writerIndex());
9690
}
9791
} else {
98-
composite = alloc.compositeBuffer(Integer.MAX_VALUE).addComponent(true, cumulation);
92+
composite = alloc.compositeBuffer(Integer.MAX_VALUE)
93+
.addFlattenedComponents(true, cumulation);
9994
}
10095
addInput(alloc, composite, in);
10196
in = null;
@@ -115,7 +110,7 @@ public final ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBu
115110
@VisibleForTesting
116111
void addInput(ByteBufAllocator alloc, CompositeByteBuf composite, ByteBuf in) {
117112
if (shouldCompose(composite, in, composeMinSize)) {
118-
composite.addComponent(true, in);
113+
composite.addFlattenedComponents(true, in);
119114
} else {
120115
// The total size of the new data and the last component are below the threshold. Merge them.
121116
mergeWithCompositeTail(alloc, composite, in);
@@ -161,13 +156,32 @@ static void mergeWithCompositeTail(
161156
ByteBuf tail = composite.component(tailComponentIndex);
162157
ByteBuf newTail = null;
163158
try {
164-
if (tail.refCnt() == 1 && !tail.isReadOnly() && newTailSize <= tail.maxCapacity()
165-
&& !isCompositeOrWrappedComposite(tail)) {
159+
if (tail.refCnt() == 1 && !tail.isReadOnly() && newTailSize <= tail.maxCapacity()) {
166160
// Ideal case: the tail isn't shared, and can be expanded to the required capacity.
167161

168162
// Take ownership of the tail.
169163
newTail = tail.retain();
170164

165+
// TODO(https://github.com/netty/netty/issues/12844): remove when we use Netty with
166+
// the issue fixed.
167+
// In certain cases, removing the CompositeByteBuf component, and then adding it back
168+
// isn't idempotent. An example is provided in https://github.com/netty/netty/issues/12844.
169+
// This happens because the buffer returned by composite.component() has out-of-sync
170+
// indexes. Under the hood the CompositeByteBuf returns a duplicate() of the underlying
171+
// buffer, but doesn't set the indexes.
172+
//
173+
// To get the right indexes we use the fact that composite.internalComponent() returns
174+
// the slice() into the readable portion of the underlying buffer.
175+
// We use this implementation detail (internalComponent() returning a *SlicedByteBuf),
176+
// and combine it with the fact that SlicedByteBuf duplicates have their indexes
177+
// adjusted so they correspond to the to the readable portion of the slice.
178+
//
179+
// Hence composite.internalComponent().duplicate() returns a buffer with the
180+
// indexes that should've been on the composite.component() in the first place.
181+
// Until the issue is fixed, we manually adjust the indexes of the removed component.
182+
ByteBuf sliceDuplicate = composite.internalComponent(tailComponentIndex).duplicate();
183+
newTail.setIndex(sliceDuplicate.readerIndex(), sliceDuplicate.writerIndex());
184+
171185
/*
172186
* The tail is a readable non-composite buffer, so writeBytes() handles everything for us.
173187
*
@@ -183,11 +197,7 @@ static void mergeWithCompositeTail(
183197
newTail.writeBytes(in);
184198

185199
} else {
186-
// The tail satisfies one or more criteria:
187-
// - Shared
188-
// - Not expandable
189-
// - Composite
190-
// - Wrapped Composite
200+
// The tail is shared, or not expandable. Replace it with a new buffer of desired capacity.
191201
newTail = alloc.buffer(alloc.calculateNewCapacity(newTailSize, Integer.MAX_VALUE));
192202
newTail.setBytes(0, composite, tailStart, tailSize)
193203
.setBytes(tailSize, in, in.readerIndex(), inputSize)
@@ -200,7 +210,7 @@ static void mergeWithCompositeTail(
200210
// Remove the old tail, reset writer index.
201211
composite.removeComponent(tailComponentIndex).setIndex(0, tailStart);
202212
// Add back the new tail.
203-
composite.addComponent(true, newTail);
213+
composite.addFlattenedComponents(true, newTail);
204214
// New tail's ownership transferred to the composite buf.
205215
newTail = null;
206216
composite.readerIndex(prevReader);
@@ -215,12 +225,4 @@ static void mergeWithCompositeTail(
215225
}
216226
}
217227
}
218-
219-
private static boolean isCompositeOrWrappedComposite(ByteBuf tail) {
220-
ByteBuf cur = tail;
221-
while (cur.unwrap() != null) {
222-
cur = cur.unwrap();
223-
}
224-
return cur instanceof CompositeByteBuf;
225-
}
226228
}

netty/src/test/java/io/grpc/netty/NettyAdaptiveCumulatorTest.java

+30-35
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public void setUp() {
8181
@Override
8282
void addInput(ByteBufAllocator alloc, CompositeByteBuf composite, ByteBuf in) {
8383
// To limit the testing scope to NettyAdaptiveCumulator.cumulate(), always compose
84-
composite.addComponent(true, in);
84+
composite.addFlattenedComponents(true, in);
8585
}
8686
};
8787

@@ -122,7 +122,7 @@ public void cumulate_contiguousCumulation_newCompositeFromContiguousAndInput() {
122122

123123
@Test
124124
public void cumulate_compositeCumulation_inputAppendedAsANewComponent() {
125-
CompositeByteBuf composite = alloc.compositeBuffer().addComponent(true, contiguous);
125+
CompositeByteBuf composite = alloc.compositeBuffer().addFlattenedComponents(true, contiguous);
126126
assertSame(composite, cumulator.cumulate(alloc, composite, in));
127127
assertEquals(DATA_INITIAL, composite.component(0).toString(US_ASCII));
128128
assertEquals(DATA_INCOMING, composite.component(1).toString(US_ASCII));
@@ -136,7 +136,7 @@ public void cumulate_compositeCumulation_inputAppendedAsANewComponent() {
136136

137137
@Test
138138
public void cumulate_compositeCumulation_inputReleasedOnError() {
139-
CompositeByteBuf composite = alloc.compositeBuffer().addComponent(true, contiguous);
139+
CompositeByteBuf composite = alloc.compositeBuffer().addFlattenedComponents(true, contiguous);
140140
try {
141141
throwingCumulator.cumulate(alloc, composite, in);
142142
fail("Cumulator didn't throw");
@@ -208,8 +208,8 @@ public void setUp() {
208208
in = ByteBufUtil.writeAscii(alloc, inData);
209209
tail = ByteBufUtil.writeAscii(alloc, tailData);
210210
composite = alloc.compositeBuffer(Integer.MAX_VALUE);
211-
// Note that addComponent() will not add a new component when tail is not readable.
212-
composite.addComponent(true, tail);
211+
// Note that addFlattenedComponents() will not add a new component when tail is not readable.
212+
composite.addFlattenedComponents(true, tail);
213213
}
214214

215215
@After
@@ -345,7 +345,7 @@ public void mergeWithCompositeTail_tailExpandable_write() {
345345
assertThat(in.readableBytes()).isAtMost(tail.writableBytes());
346346

347347
// All fits, so tail capacity must stay the same.
348-
composite.addComponent(true, tail);
348+
composite.addFlattenedComponents(true, tail);
349349
assertTailExpanded(EXPECTED_TAIL_DATA, fitCapacity);
350350
}
351351

@@ -362,7 +362,7 @@ public void mergeWithCompositeTail_tailExpandable_fastWrite() {
362362
alloc.calculateNewCapacity(EXPECTED_TAIL_DATA.length(), Integer.MAX_VALUE);
363363

364364
// Tail capacity is extended to its fast capacity.
365-
composite.addComponent(true, tail);
365+
composite.addFlattenedComponents(true, tail);
366366
assertTailExpanded(EXPECTED_TAIL_DATA, tailFastCapacity);
367367
}
368368

@@ -372,7 +372,7 @@ public void mergeWithCompositeTail_tailExpandable_reallocateInMemory() {
372372
@SuppressWarnings("InlineMeInliner") // Requires Java 11
373373
String inSuffixOverFastBytes = Strings.repeat("a", tailFastCapacity + 1);
374374
int newTailSize = tail.readableBytes() + inSuffixOverFastBytes.length();
375-
composite.addComponent(true, tail);
375+
composite.addFlattenedComponents(true, tail);
376376

377377
// Make input larger than tailFastCapacity
378378
in.writeCharSequence(inSuffixOverFastBytes, US_ASCII);
@@ -386,6 +386,9 @@ public void mergeWithCompositeTail_tailExpandable_reallocateInMemory() {
386386
}
387387

388388
private void assertTailExpanded(String expectedTailReadableData, int expectedNewTailCapacity) {
389+
if (!GrpcHttp2ConnectionHandler.usingPre4_1_111_Netty()) {
390+
return; // Netty 4.1.111 doesn't work with NettyAdaptiveCumulator
391+
}
389392
int originalNumComponents = composite.numComponents();
390393

391394
// Handle the case when reader index is beyond all readable bytes of the cumulation.
@@ -435,21 +438,21 @@ public void mergeWithCompositeTail_tailNotExpandable_maxCapacityReached() {
435438
@SuppressWarnings("InlineMeInliner") // Requires Java 11
436439
String tailSuffixFullCapacity = Strings.repeat("a", tail.maxWritableBytes());
437440
tail.writeCharSequence(tailSuffixFullCapacity, US_ASCII);
438-
composite.addComponent(true, tail);
441+
composite.addFlattenedComponents(true, tail);
439442
assertTailReplaced();
440443
}
441444

442445
@Test
443446
public void mergeWithCompositeTail_tailNotExpandable_shared() {
444447
tail.retain();
445-
composite.addComponent(true, tail);
448+
composite.addFlattenedComponents(true, tail);
446449
assertTailReplaced();
447450
tail.release();
448451
}
449452

450453
@Test
451454
public void mergeWithCompositeTail_tailNotExpandable_readOnly() {
452-
composite.addComponent(true, tail.asReadOnly());
455+
composite.addFlattenedComponents(true, tail.asReadOnly());
453456
assertTailReplaced();
454457
}
455458

@@ -527,7 +530,8 @@ public void mergeWithCompositeTail_tailExpandable_mergedReleaseOnThrow() {
527530
CompositeByteBuf compositeThrows = new CompositeByteBuf(alloc, false, Integer.MAX_VALUE,
528531
tail) {
529532
@Override
530-
public CompositeByteBuf addComponent(boolean increaseWriterIndex, ByteBuf buffer) {
533+
public CompositeByteBuf addFlattenedComponents(boolean increaseWriterIndex,
534+
ByteBuf buffer) {
531535
throw expectedError;
532536
}
533537
};
@@ -560,7 +564,8 @@ public void mergeWithCompositeTail_tailNotExpandable_mergedReleaseOnThrow() {
560564
CompositeByteBuf compositeRo = new CompositeByteBuf(alloc, false, Integer.MAX_VALUE,
561565
tail.asReadOnly()) {
562566
@Override
563-
public CompositeByteBuf addComponent(boolean increaseWriterIndex, ByteBuf buffer) {
567+
public CompositeByteBuf addFlattenedComponents(boolean increaseWriterIndex,
568+
ByteBuf buffer) {
564569
throw expectedError;
565570
}
566571
};
@@ -614,16 +619,20 @@ public void mergeWithCompositeTail_outOfSyncComposite() {
614619
ByteBuf buf = alloc.buffer(32).writeBytes("---01234".getBytes(US_ASCII));
615620

616621
// Start with a regular cumulation and add the buf as the only component.
617-
CompositeByteBuf composite1 = alloc.compositeBuffer(8).addComponent(true, buf);
622+
CompositeByteBuf composite1 = alloc.compositeBuffer(8).addFlattenedComponents(true, buf);
618623
// Read composite1 buf to the beginning of the numbers.
619624
assertThat(composite1.readCharSequence(3, US_ASCII).toString()).isEqualTo("---");
620625

621626
// Wrap composite1 into another cumulation. This is similar to
622627
// what NettyAdaptiveCumulator.cumulate() does in the case the cumulation has refCnt != 1.
623628
CompositeByteBuf composite2 =
624-
alloc.compositeBuffer(8).addComponent(true, composite1);
629+
alloc.compositeBuffer(8).addFlattenedComponents(true, composite1);
625630
assertThat(composite2.toString(US_ASCII)).isEqualTo("01234");
626631

632+
if (!GrpcHttp2ConnectionHandler.usingPre4_1_111_Netty()) {
633+
return; // Netty 4.1.111 doesn't work with NettyAdaptiveCumulator
634+
}
635+
627636
// The previous operation does not adjust the read indexes of the underlying buffers,
628637
// only the internal Component offsets. When the cumulator attempts to append the input to
629638
// the tail buffer, it extracts it from the cumulation, writes to it, and then adds it back.
@@ -637,27 +646,13 @@ public void mergeWithCompositeTail_outOfSyncComposite() {
637646
CompositeByteBuf cumulation = (CompositeByteBuf) cumulator.cumulate(alloc, composite2,
638647
ByteBufUtil.writeAscii(alloc, "56789"));
639648
assertThat(cumulation.toString(US_ASCII)).isEqualTo("0123456789");
640-
}
641-
642-
@Test
643-
public void mergeWithNonCompositeTail() {
644-
NettyAdaptiveCumulator cumulator = new NettyAdaptiveCumulator(1024);
645-
ByteBufAllocator alloc = new PooledByteBufAllocator();
646-
ByteBuf buf = alloc.buffer().writeBytes("tail".getBytes(US_ASCII));
647-
ByteBuf in = alloc.buffer().writeBytes("-012345".getBytes(US_ASCII));
648-
649-
CompositeByteBuf composite = alloc.compositeBuffer().addComponent(true, buf);
650-
651-
CompositeByteBuf cumulation = (CompositeByteBuf) cumulator.cumulate(alloc, composite, in);
652649

653-
assertEquals("tail-012345", cumulation.toString(US_ASCII));
654-
assertEquals(0, in.refCnt());
655-
assertEquals(1, cumulation.numComponents());
656-
657-
buf.setByte(2, '*').setByte(7, '$');
658-
assertEquals("ta*l-01$345", cumulation.toString(US_ASCII));
659-
660-
composite.release();
650+
// Correctness check: we still have a single component, and this component is still the
651+
// original underlying buffer.
652+
assertThat(cumulation.numComponents()).isEqualTo(1);
653+
// Replace '2' with '*', and '8' with '$'.
654+
buf.setByte(5, '*').setByte(11, '$');
655+
assertThat(cumulation.toString(US_ASCII)).isEqualTo("01*34567$9");
661656
}
662657
}
663658
}

0 commit comments

Comments
 (0)