Skip to content

Commit e8c6aae

Browse files
author
likun
committed
Reduce excessive off-heap memory consumption caused by occasional large keys.
1 parent 8321f5d commit e8c6aae

File tree

2 files changed

+81
-3
lines changed

2 files changed

+81
-3
lines changed

src/main/java/io/lettuce/core/protocol/CommandHandler.java

+34-3
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import io.lettuce.core.tracing.Tracer;
5656
import io.lettuce.core.tracing.Tracing;
5757
import io.netty.buffer.ByteBuf;
58+
import io.netty.buffer.ByteBufAllocator;
5859
import io.netty.channel.Channel;
5960
import io.netty.channel.ChannelDuplexHandler;
6061
import io.netty.channel.ChannelHandler;
@@ -93,6 +94,8 @@ public class CommandHandler extends ChannelDuplexHandler implements HasQueuedCom
9394

9495
private static final AtomicLong COMMAND_HANDLER_COUNTER = new AtomicLong();
9596

97+
private static final int DEFAULT_READER_BUFFER_CAPACITY = 8192 * 8;
98+
9699
private final ClientOptions clientOptions;
97100

98101
private final ClientResources clientResources;
@@ -139,6 +142,10 @@ public class CommandHandler extends ChannelDuplexHandler implements HasQueuedCom
139142

140143
private Tracing.Endpoint tracedEndpoint;
141144

145+
private ByteBuf tmpReadBuffer;
146+
147+
private ByteBufAllocator byteBufAllocator;
148+
142149
/**
143150
* Initialize a new instance that handles commands from the supplied queue.
144151
*
@@ -222,7 +229,8 @@ public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
222229

223230
setState(LifecycleState.REGISTERED);
224231

225-
readBuffer = ctx.alloc().buffer(8192 * 8);
232+
byteBufAllocator = ctx.alloc();
233+
readBuffer = ctx.alloc().buffer(DEFAULT_READER_BUFFER_CAPACITY);
226234
rsm = new RedisStateMachine();
227235
ctx.fireChannelRegistered();
228236
}
@@ -614,6 +622,15 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception
614622
if (traceEnabled) {
615623
logger.trace("{} Buffer: {}", logPrefix(), input.toString(Charset.defaultCharset()).trim());
616624
}
625+
// if buffer capacity larger than default capacity, then create a new buffer with double capacity
626+
// in most cases, key size is less than 64k
627+
if (readBuffer.capacity() == DEFAULT_READER_BUFFER_CAPACITY && readBuffer.writableBytes() < input.readableBytes()
628+
&& byteBufAllocator != null) {
629+
ByteBuf byteBuf = byteBufAllocator.directBuffer(readBuffer.capacity() << 1);
630+
byteBuf.writeBytes(readBuffer);
631+
tmpReadBuffer = readBuffer;
632+
readBuffer = byteBuf;
633+
}
617634

618635
readBuffer.touch("CommandHandler.read(…)");
619636
readBuffer.writeBytes(input);
@@ -661,6 +678,8 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Interrup
661678

662679
ctx.close();
663680
throw e;
681+
} finally {
682+
releaseLargeBufferIfNecessary(buffer);
664683
}
665684

666685
hasDecodeProgress = false;
@@ -704,14 +723,26 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Interrup
704723
complete(command);
705724
} catch (Exception e) {
706725
logger.warn("{} Unexpected exception during request: {}", logPrefix, e.toString(), e);
726+
} finally {
727+
releaseLargeBufferIfNecessary(buffer);
707728
}
708729
}
709730
}
710731
afterDecode(ctx, command);
711732
}
712733
}
734+
if (buffer.refCnt() > 0) {
735+
decodeBufferPolicy.afterDecoding(buffer);
736+
}
737+
}
713738

714-
decodeBufferPolicy.afterDecoding(buffer);
739+
private void releaseLargeBufferIfNecessary(ByteBuf buffer) {
740+
if (this.tmpReadBuffer != null) {
741+
buffer.release();
742+
this.readBuffer = tmpReadBuffer;
743+
this.readBuffer.clear();
744+
this.tmpReadBuffer = null;
745+
}
715746
}
716747

717748
protected void notifyPushListeners(PushMessage notification) {
@@ -734,7 +765,7 @@ protected void notifyPushListeners(PushMessage notification) {
734765
* @return
735766
*/
736767
protected boolean canDecode(ByteBuf buffer) {
737-
return buffer.isReadable() && (isMessageDecode() || isPushDecode(buffer));
768+
return buffer.refCnt()>0 && buffer.isReadable() && (isMessageDecode() || isPushDecode(buffer));
738769
}
739770

740771
private boolean isPushMessage(ByteBuf buffer) {

src/test/java/io/lettuce/core/protocol/CommandHandlerUnitTests.java

+47
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.net.Inet4Address;
3131
import java.net.InetSocketAddress;
3232
import java.nio.ByteBuffer;
33+
import java.nio.charset.StandardCharsets;
3334
import java.time.Duration;
3435
import java.util.Arrays;
3536
import java.util.Collection;
@@ -65,6 +66,7 @@
6566
import io.lettuce.core.output.KeyValueListOutput;
6667
import io.lettuce.core.output.StatusOutput;
6768
import io.lettuce.core.output.ValueListOutput;
69+
import io.lettuce.core.output.ValueOutput;
6870
import io.lettuce.core.resource.ClientResources;
6971
import io.lettuce.core.tracing.Tracing;
7072
import io.lettuce.test.Delay;
@@ -650,4 +652,49 @@ void shouldHandleNullBuffers() throws Exception {
650652
sut.channelUnregistered(context);
651653
}
652654

655+
/**
656+
* if large keys are received ,the large buffer will created and then released
657+
*/
658+
@Test
659+
void shouldLargeBufferCreatedAndRelease() throws Exception {
660+
ChannelPromise channelPromise = new DefaultChannelPromise(channel, ImmediateEventExecutor.INSTANCE);
661+
channelPromise.setSuccess();
662+
sut.channelRegistered(context);
663+
sut.channelActive(context);
664+
sut.getStack().add(new Command<>(CommandType.GET, new ValueOutput<String, String>(StringCodec.UTF8)));
665+
666+
ByteBuf internalBuffer = ReflectionTestUtils.getField(sut, "readBuffer");
667+
668+
// step1 Receive First TCP Packet
669+
ByteBuf msg = context.alloc().buffer(13);
670+
// 1+5+2+7 ($+length+\r\n+len(value))
671+
msg.writeBytes("$65536\r\nval_abc".getBytes(StandardCharsets.UTF_8));
672+
sut.channelRead(context, msg);
673+
674+
int markedReaderIndex = ReflectionTestUtils.getField(internalBuffer, "markedReaderIndex");
675+
assertThat(markedReaderIndex).isEqualTo(8);
676+
assertThat(internalBuffer.readerIndex()).isEqualTo(8);
677+
assertThat(internalBuffer.writerIndex()).isEqualTo(15);
678+
679+
// step2 Receive Second TCP Packet
680+
ByteBuf msg2 = context.alloc().buffer(64 * 1024);
681+
StringBuilder sb = new StringBuilder();
682+
// 65536-7
683+
for (int i = 0; i < 65529; i++) {
684+
sb.append((char) ('a' + i % 26));
685+
}
686+
sb.append("\r\n");
687+
msg2.writeBytes(sb.toString().getBytes(StandardCharsets.UTF_8));
688+
sut.channelRead(context, msg2);
689+
690+
// step3 Got Result: readBuffer.capacity = 64k and tmpBuffer is null
691+
ByteBuf readBuffer = ReflectionTestUtils.getField(sut, "readBuffer");
692+
assertThat(readBuffer.capacity()).isEqualTo(64 * 1024);
693+
assertThat(readBuffer.readerIndex()).isZero();
694+
assertThat(readBuffer.writerIndex()).isZero();
695+
696+
ByteBuf tmpBuffer = ReflectionTestUtils.getField(sut, "tmpReadBuffer");
697+
assertThat(tmpBuffer).isNull();
698+
}
699+
653700
}

0 commit comments

Comments
 (0)