This is an automated email from the ASF dual-hosted git repository.
wernerdv pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite.git
The following commit(s) were added to refs/heads/master by this push:
new fc33c9c6867 IGNITE-28853 Optimize serialization and deserialization of
CompessedMessage (#13329)
fc33c9c6867 is described below
commit fc33c9c6867d753ba726defd8973b4ec6acdad80
Author: Dmitry Werner <[email protected]>
AuthorDate: Wed Jul 15 10:32:13 2026 +0500
IGNITE-28853 Optimize serialization and deserialization of CompessedMessage
(#13329)
---
.../direct/JmhDirectMessageReaderBenchmark.java | 128 ++++++++++++++++
.../internal/direct/DirectMessageReader.java | 131 ++++++++++------
.../internal/direct/state/DirectMessageState.java | 14 ++
.../managers/communication/CompressedMessage.java | 167 +++++++++++----------
.../communication/CompressedMessageSerializer.java | 34 ++---
.../communication/CompressedMessageTest.java | 151 ++++++++++++++++++-
6 files changed, 481 insertions(+), 144 deletions(-)
diff --git
a/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/direct/JmhDirectMessageReaderBenchmark.java
b/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/direct/JmhDirectMessageReaderBenchmark.java
new file mode 100644
index 00000000000..e9b2a81f658
--- /dev/null
+++
b/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/direct/JmhDirectMessageReaderBenchmark.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.benchmarks.jmh.direct;
+
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import org.apache.ignite.internal.CoreMessagesProvider;
+import org.apache.ignite.internal.benchmarks.jmh.runner.JmhIdeBenchmarkRunner;
+import org.apache.ignite.internal.direct.DirectMessageReader;
+import org.apache.ignite.internal.direct.DirectMessageWriter;
+import
org.apache.ignite.internal.managers.communication.IgniteMessageFactoryImpl;
+import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion;
+import
org.apache.ignite.internal.processors.cache.distributed.dht.preloader.GridDhtPartitionsFullMessage;
+import
org.apache.ignite.internal.processors.cache.distributed.dht.preloader.GroupPartitionIdPair;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.plugin.extensions.communication.Message;
+import org.apache.ignite.plugin.extensions.communication.MessageFactory;
+import
org.apache.ignite.plugin.extensions.communication.MessageFactoryProvider;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.apache.ignite.marshaller.Marshallers.jdk;
+import static org.openjdk.jmh.annotations.Mode.Throughput;
+
+/** Benchmarks the {@link DirectMessageReader} compressed-field hot path. */
+@State(Scope.Thread)
+@BenchmarkMode(Throughput)
+@Warmup(iterations = 5, time = 3, timeUnit = SECONDS)
+@Measurement(iterations = 5, time = 3, timeUnit = SECONDS)
+@Fork(1)
+public class JmhDirectMessageReaderBenchmark {
+ /** */
+ @Param({"30", "500"})
+ private int entries;
+
+ /** */
+ private DirectMessageReader reader;
+
+ /** Fully serialized compressed message, as received from the network. */
+ private ByteBuffer buf;
+
+ /** */
+ public static void main(String[] args) throws Exception {
+ JmhIdeBenchmarkRunner.create()
+ .benchmarks(JmhDirectMessageReaderBenchmark.class.getName())
+ .run();
+ }
+
+ /** */
+ @Setup
+ public void setup() {
+ MessageFactory msgFactory = msgFactory();
+
+ reader = new DirectMessageReader(msgFactory, null);
+
+ Map<UUID, Map<GroupPartitionIdPair, Long>> partHistSuppliers = new
HashMap<>();
+ Map<UUID, Map<Integer, Set<Integer>>> partsToReload = new HashMap<>();
+
+ for (int i = 0; i < entries; i++) {
+ UUID nodeId = UUID.randomUUID();
+
+ partHistSuppliers.put(nodeId, Map.of(new GroupPartitionIdPair(i, i
+ 1), i + 2L));
+ partsToReload.put(nodeId, Map.of(i, Set.of(i + 1)));
+ }
+
+ GridDhtPartitionsFullMessage msg = new
GridDhtPartitionsFullMessage(null, null,
+ new AffinityTopologyVersion(0), partHistSuppliers, partsToReload);
+
+ DirectMessageWriter writer = new DirectMessageWriter(msgFactory);
+
+ buf = ByteBuffer.allocate(4 * 1024 * 1024);
+
+ writer.setBuffer(buf);
+
+ boolean finished = writer.writeMessage(msg, true);
+
+ if (!finished)
+ throw new IllegalStateException("Message does not fit into the
buffer.");
+
+ buf.flip();
+ }
+
+ /** Exchange-style compressed message deserialization. */
+ @Benchmark
+ public Message compressedMessage() {
+ buf.rewind();
+
+ reader.setBuffer(buf);
+
+ Message msg = reader.readMessage(true);
+
+ reader.reset();
+
+ return msg;
+ }
+
+ /** */
+ private static MessageFactory msgFactory() {
+ return new IgniteMessageFactoryImpl(new MessageFactoryProvider[]{
+ new CoreMessagesProvider(jdk(), jdk(), U.gridClassLoader())});
+ }
+}
diff --git
a/modules/core/src/main/java/org/apache/ignite/internal/direct/DirectMessageReader.java
b/modules/core/src/main/java/org/apache/ignite/internal/direct/DirectMessageReader.java
index 045db2f8cf9..9ec9b68cc18 100644
---
a/modules/core/src/main/java/org/apache/ignite/internal/direct/DirectMessageReader.java
+++
b/modules/core/src/main/java/org/apache/ignite/internal/direct/DirectMessageReader.java
@@ -23,6 +23,7 @@ import java.util.Collection;
import java.util.Map;
import java.util.UUID;
import java.util.function.Function;
+import org.apache.ignite.IgniteException;
import org.apache.ignite.internal.direct.state.DirectMessageState;
import org.apache.ignite.internal.direct.state.DirectMessageStateItem;
import org.apache.ignite.internal.direct.stream.DirectByteBufferStream;
@@ -50,6 +51,9 @@ import org.jetbrains.annotations.Nullable;
* Message reader implementation.
*/
public class DirectMessageReader implements MessageReader {
+ /** Empty buffer to release the payload reference from {@link #tmpReader}
streams between compressed reads. */
+ private static final ByteBuffer EMPTY_BUF = ByteBuffer.wrap(new byte[0]);
+
/** State. */
@GridToStringInclude
private final DirectMessageState<StateItem> state;
@@ -63,6 +67,15 @@ public class DirectMessageReader implements MessageReader {
/** Buffer for reading. */
private ByteBuffer buf;
+ /**
+ * Cached stream of the current state item. Avoids re-resolving {@code
state.item().stream} on every primitive
+ * read; updated only when the current item changes (buffer set / nested
read enter or exit).
+ */
+ private DirectByteBufferStream curStream;
+
+ /** Reusable reader for compressed payload deserialization. Lazily
created; thread-confined like the reader itself. */
+ private DirectMessageReader tmpReader;
+
/** Whether last field was fully read. */
private boolean lastRead;
@@ -85,7 +98,9 @@ public class DirectMessageReader implements MessageReader {
@Override public void setBuffer(ByteBuffer buf) {
this.buf = buf;
- state.item().stream.setBuffer(buf);
+ curStream = state.item().stream;
+
+ curStream.setBuffer(buf);
}
/**
@@ -99,7 +114,7 @@ public class DirectMessageReader implements MessageReader {
/** {@inheritDoc} */
@Override public byte readByte() {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
byte val = stream.readByte();
@@ -110,7 +125,7 @@ public class DirectMessageReader implements MessageReader {
/** {@inheritDoc} */
@Override public short readShort() {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
short val = stream.readShort();
@@ -121,7 +136,7 @@ public class DirectMessageReader implements MessageReader {
/** {@inheritDoc} */
@Override public int readInt() {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
int val = stream.readInt();
@@ -132,7 +147,7 @@ public class DirectMessageReader implements MessageReader {
/** {@inheritDoc} */
@Override public long readLong() {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
long val = stream.readLong();
@@ -143,7 +158,7 @@ public class DirectMessageReader implements MessageReader {
/** {@inheritDoc} */
@Override public float readFloat() {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
float val = stream.readFloat();
@@ -154,7 +169,7 @@ public class DirectMessageReader implements MessageReader {
/** {@inheritDoc} */
@Override public double readDouble() {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
double val = stream.readDouble();
@@ -165,7 +180,7 @@ public class DirectMessageReader implements MessageReader {
/** {@inheritDoc} */
@Override public char readChar() {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
char val = stream.readChar();
@@ -176,7 +191,7 @@ public class DirectMessageReader implements MessageReader {
/** {@inheritDoc} */
@Override public boolean readBoolean() {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
boolean val = stream.readBoolean();
@@ -187,7 +202,7 @@ public class DirectMessageReader implements MessageReader {
/** {@inheritDoc} */
@Nullable @Override public byte[] readByteArray() {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
byte[] arr = stream.readByteArray();
@@ -198,7 +213,7 @@ public class DirectMessageReader implements MessageReader {
/** {@inheritDoc} */
@Nullable @Override public short[] readShortArray() {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
short[] arr = stream.readShortArray();
@@ -209,7 +224,7 @@ public class DirectMessageReader implements MessageReader {
/** {@inheritDoc} */
@Nullable @Override public int[] readIntArray() {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
int[] arr = stream.readIntArray();
@@ -220,7 +235,7 @@ public class DirectMessageReader implements MessageReader {
/** {@inheritDoc} */
@Nullable @Override public long[] readLongArray() {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
long[] arr = stream.readLongArray();
@@ -231,7 +246,7 @@ public class DirectMessageReader implements MessageReader {
/** {@inheritDoc} */
@Nullable @Override public float[] readFloatArray() {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
float[] arr = stream.readFloatArray();
@@ -242,7 +257,7 @@ public class DirectMessageReader implements MessageReader {
/** {@inheritDoc} */
@Nullable @Override public double[] readDoubleArray() {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
double[] arr = stream.readDoubleArray();
@@ -253,7 +268,7 @@ public class DirectMessageReader implements MessageReader {
/** {@inheritDoc} */
@Nullable @Override public char[] readCharArray() {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
char[] arr = stream.readCharArray();
@@ -264,7 +279,7 @@ public class DirectMessageReader implements MessageReader {
/** {@inheritDoc} */
@Nullable @Override public boolean[] readBooleanArray() {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
boolean[] arr = stream.readBooleanArray();
@@ -275,7 +290,7 @@ public class DirectMessageReader implements MessageReader {
/** {@inheritDoc} */
@Override public String readString() {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
String val = stream.readString();
@@ -286,7 +301,7 @@ public class DirectMessageReader implements MessageReader {
/** {@inheritDoc} */
@Override public BitSet readBitSet() {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
BitSet val = stream.readBitSet();
@@ -297,7 +312,7 @@ public class DirectMessageReader implements MessageReader {
/** {@inheritDoc} */
@Override public UUID readUuid() {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
UUID val = stream.readUuid();
@@ -308,7 +323,7 @@ public class DirectMessageReader implements MessageReader {
/** {@inheritDoc} */
@Override public IgniteUuid readIgniteUuid() {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
IgniteUuid val = stream.readIgniteUuid();
@@ -319,7 +334,7 @@ public class DirectMessageReader implements MessageReader {
/** {@inheritDoc} */
@Override public AffinityTopologyVersion readAffinityTopologyVersion() {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
AffinityTopologyVersion val = stream.readAffinityTopologyVersion();
@@ -330,14 +345,14 @@ public class DirectMessageReader implements MessageReader
{
/** {@inheritDoc} */
@Nullable @Override public <T extends Message> T readMessage(boolean
compress) {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
T msg;
if (compress)
msg = readCompressedMessageAndDeserialize(
stream,
- tmpReader ->
tmpReader.state.item().stream.readMessage(tmpReader)
+ r -> r.state.item().stream.readMessage(r)
);
else {
msg = stream.readMessage(this);
@@ -350,7 +365,7 @@ public class DirectMessageReader implements MessageReader {
/** {@inheritDoc} */
@Override public CacheObject readCacheObject() {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
CacheObject val = stream.readCacheObject();
@@ -361,7 +376,7 @@ public class DirectMessageReader implements MessageReader {
/** {@inheritDoc} */
@Override public KeyCacheObject readKeyCacheObject() {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
KeyCacheObject key = stream.readKeyCacheObject();
@@ -372,7 +387,7 @@ public class DirectMessageReader implements MessageReader {
/** {@inheritDoc} */
@Override public GridLongList readGridLongList() {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
GridLongList ll = stream.readGridLongList();
@@ -383,7 +398,7 @@ public class DirectMessageReader implements MessageReader {
/** {@inheritDoc} */
@Override public <T> T[] readObjectArray(MessageArrayType type) {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
T[] msg = stream.readObjectArray(type, this);
@@ -394,7 +409,7 @@ public class DirectMessageReader implements MessageReader {
/** {@inheritDoc} */
@Override public <C extends Collection<?>> C
readCollection(MessageCollectionType type) {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
C col = stream.readCollection(type, this);
@@ -405,14 +420,14 @@ public class DirectMessageReader implements MessageReader
{
/** {@inheritDoc} */
@Override public <M extends Map<?, ?>> M readMap(MessageMapType type,
boolean compress) {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
M map;
if (compress)
map = readCompressedMessageAndDeserialize(
stream,
- tmpReader -> tmpReader.state.item().stream.readMap(type,
tmpReader)
+ r -> r.state.item().stream.readMap(type, r)
);
else {
map = stream.readMap(type, this);
@@ -425,7 +440,7 @@ public class DirectMessageReader implements MessageReader {
/** {@inheritDoc} */
@Override public IgniteProductVersion readIgniteProductVersion() {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
IgniteProductVersion v = stream.readIgniteProductVersion();
@@ -436,7 +451,7 @@ public class DirectMessageReader implements MessageReader {
/** {@inheritDoc} */
@Override public GridCacheVersion readGridCacheVersion() {
- DirectByteBufferStream stream = state.item().stream;
+ DirectByteBufferStream stream = curStream;
GridCacheVersion v = stream.readGridCacheVersion();
@@ -469,12 +484,18 @@ public class DirectMessageReader implements MessageReader
{
@Override public void beforeNestedRead() {
state.forward();
- state.item().stream.setBuffer(buf);
+ curStream = state.item().stream;
+
+ curStream.setBuffer(buf);
}
/** {@inheritDoc} */
@Override public void afterNestedRead(boolean finished) {
state.backward(finished);
+
+ curStream = state.item().stream;
+
+ curStream.setBuffer(buf);
}
/** {@inheritDoc} */
@@ -503,24 +524,50 @@ public class DirectMessageReader implements MessageReader
{
if (msg0.dataSize() == 0)
return null;
- byte[] uncompressed = msg0.uncompressed();
+ // Reuse the temp reader across fields/messages instead of allocating
a fresh state stack each time.
+ if (tmpReader == null)
+ tmpReader = new DirectMessageReader(msgFactory, cacheObjProc);
+ else
+ tmpReader.reset();
- ByteBuffer tmpBuf = ByteBuffer.allocateDirect(uncompressed.length);
+ tmpReader.setBuffer(ByteBuffer.wrap(msg0.uncompressed()));
- tmpBuf.put(uncompressed);
- tmpBuf.flip();
+ T res;
- DirectMessageReader tmpReader = new DirectMessageReader(msgFactory,
cacheObjProc);
+ boolean ok = false;
- tmpReader.setBuffer(tmpBuf);
+ try {
+ res = fun.apply(tmpReader);
- T res = fun.apply(tmpReader);
+ // The payload buffer is always complete, so a partial read means
a corrupted stream, not a lack of data.
+ if (!tmpReader.state.item().stream.lastFinished()) {
+ throw new IgniteException("Failed to deserialize compressed
payload: uncompressed data ended " +
+ "unexpectedly [dataSize=" + msg0.dataSize() + ']');
+ }
+
+ ok = true;
+ }
+ finally {
+ // Don't reuse a reader with half-read state.
+ if (!ok)
+ tmpReader = null;
+ }
- lastRead = tmpReader.state.item().stream.lastFinished();
+ // Don't pin the payload: the reader lives as long as the connection.
+ tmpReader.releasePayload();
+
+ lastRead = true;
return res;
}
+ /** Releases the payload buffer reference from all streams of this reader.
*/
+ private void releasePayload() {
+ buf = EMPTY_BUF;
+
+ state.forEachItem(item -> item.stream.setBuffer(EMPTY_BUF));
+ }
+
/**
*/
private static class StateItem implements DirectMessageStateItem {
diff --git
a/modules/core/src/main/java/org/apache/ignite/internal/direct/state/DirectMessageState.java
b/modules/core/src/main/java/org/apache/ignite/internal/direct/state/DirectMessageState.java
index b92f6000fdd..67bd7683425 100644
---
a/modules/core/src/main/java/org/apache/ignite/internal/direct/state/DirectMessageState.java
+++
b/modules/core/src/main/java/org/apache/ignite/internal/direct/state/DirectMessageState.java
@@ -19,6 +19,7 @@ package org.apache.ignite.internal.direct.state;
import java.lang.reflect.Array;
import java.util.Arrays;
+import java.util.function.Consumer;
import org.apache.ignite.internal.util.tostring.GridToStringExclude;
import org.apache.ignite.internal.util.typedef.internal.S;
import org.apache.ignite.lang.IgniteOutClosure;
@@ -98,6 +99,19 @@ public class DirectMessageState<T extends
DirectMessageStateItem> {
stack[0].reset();
}
+ /**
+ * @param c Closure to apply to every created item.
+ */
+ public void forEachItem(Consumer<T> c) {
+ // Items are created contiguously by forward().
+ for (T item : stack) {
+ if (item == null)
+ break;
+
+ c.accept(item);
+ }
+ }
+
/** {@inheritDoc} */
@Override public String toString() {
return S.toString(DirectMessageState.class, this, "stack",
Arrays.toString(stack));
diff --git
a/modules/core/src/main/java/org/apache/ignite/internal/managers/communication/CompressedMessage.java
b/modules/core/src/main/java/org/apache/ignite/internal/managers/communication/CompressedMessage.java
index e3e46c43ff3..57756c38584 100644
---
a/modules/core/src/main/java/org/apache/ignite/internal/managers/communication/CompressedMessage.java
+++
b/modules/core/src/main/java/org/apache/ignite/internal/managers/communication/CompressedMessage.java
@@ -17,14 +17,13 @@
package org.apache.ignite.internal.managers.communication;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
-import java.util.zip.DeflaterOutputStream;
import java.util.zip.Inflater;
-import java.util.zip.InflaterInputStream;
import org.apache.ignite.IgniteException;
import org.apache.ignite.internal.util.typedef.internal.S;
import org.apache.ignite.plugin.extensions.communication.Message;
@@ -38,18 +37,27 @@ public class CompressedMessage implements Message {
/** Chunk size. */
static final int CHUNK_SIZE = 10 * 1024;
- /** Reader buffer capacity. */
- static final int BUFFER_CAPACITY = 10 * CHUNK_SIZE;
+ /**
+ * Maximum expansion ratio for raw deflate.
+ * Raw deflate cannot expand beyond roughly 1032:1; a larger ratio
indicates a corrupted size header.
+ */
+ private static final int MAX_DEFLATE_EXPANSION_RATIO = 1032;
+
+ /**
+ * Fixed additive margin for the expansion ratio check.
+ * Accounts for small constant overhead in deflate so the check doesn't
falsely reject valid small messages.
+ */
+ private static final int MAX_DEFLATE_BLOCK_OVERHEAD = 64;
+
+ /** Compressed data chunks: filled by {@link #compress(ByteBuffer)} on
send, by the serializer on receive. */
+ List<byte[]> chunks;
- /** Temporary buffer for compressed data received over the network. */
- ByteBuffer tmpBuf;
+ /** Index of the next chunk to send. */
+ private int chunkIdx;
/** Raw data size. */
int dataSize;
- /** Chunked byte reader. */
- ChunkedByteReader chunkedReader;
-
/** Chunk. */
byte[] chunk;
@@ -73,7 +81,7 @@ public class CompressedMessage implements Message {
this.compressionLvl = compressionLvl;
if (dataSize > 0)
- chunkedReader = new ChunkedByteReader(compress(buf));
+ compress(buf);
}
/** @return Raw data size. */
@@ -90,99 +98,106 @@ public class CompressedMessage implements Message {
/** @return Next chunk of data or null. */
public byte[] nextChunk() {
- return chunkedReader.nextChunk();
+ return chunkIdx < chunks.size() ? chunks.get(chunkIdx++) : null;
}
- /**
- * @param buf Buffer.
- * @return Compressed data.
- */
- private byte[] compress(ByteBuffer buf) {
- byte[] data = new byte[dataSize];
+ /** @param buf Buffer. */
+ private void compress(ByteBuffer buf) {
+ Deflater deflater = new Deflater(compressionLvl, true);
- buf.get(data);
+ try {
+ deflater.setInput(buf);
+ deflater.finish();
- ByteArrayOutputStream baos = new ByteArrayOutputStream(data.length);
- Deflater deflater = new Deflater(compressionLvl, true);
+ chunks = new ArrayList<>(dataSize / CHUNK_SIZE + 1);
- try (DeflaterOutputStream dos = new DeflaterOutputStream(baos,
deflater)) {
- dos.write(data);
- dos.finish();
- }
- catch (IOException ex) {
- throw new IgniteException(ex);
+ // Incompressible data may expand: raw-deflate worst case below
CHUNK_SIZE is one stored block (~11 bytes overhead).
+ byte[] chunk0 = new byte[dataSize >= CHUNK_SIZE ? CHUNK_SIZE :
dataSize + 16];
+
+ int len = 0;
+
+ while (!deflater.finished()) {
+ len += deflater.deflate(chunk0, len, chunk0.length - len);
+
+ if (len == chunk0.length && !deflater.finished()) {
+ chunks.add(chunk0);
+
+ chunk0 = new byte[CHUNK_SIZE];
+ len = 0;
+ }
+ }
+
+ if (len > 0)
+ chunks.add(len == chunk0.length ? chunk0 :
Arrays.copyOf(chunk0, len));
}
finally {
deflater.end();
}
-
- return baos.toByteArray();
}
/** @return Uncompressed data. */
private byte[] uncompress() {
- if (tmpBuf == null)
- return null;
+ if (chunks == null)
+ throw new IgniteException("Compressed stream is truncated
[expected=" + dataSize + ", inflated=0]");
+
+ long compressedTotal = 0;
- byte[] uncompressedData;
+ for (int i = 0; i < chunks.size(); i++)
+ compressedTotal += chunks.get(i).length;
+
+ // Maximum expansion ratio check to detect corrupted size headers.
+ if (dataSize > compressedTotal * MAX_DEFLATE_EXPANSION_RATIO +
MAX_DEFLATE_BLOCK_OVERHEAD) {
+ throw new IgniteException("Invalid compressed message data size
[dataSize=" + dataSize +
+ ", compressedBytes=" + compressedTotal + ']');
+ }
+
+ byte[] data = new byte[dataSize];
Inflater inflater = new Inflater(true);
- byte[] bytes = new byte[tmpBuf.position()];
+ try {
+ int off = 0;
+ int i = 0;
+
+ for (; i < chunks.size() && off < dataSize; i++) {
+ inflater.setInput(chunks.get(i));
+
+ int n;
+
+ while (off < dataSize && (n = inflater.inflate(data, off,
dataSize - off)) > 0)
+ off += n;
+ }
+
+ if (off != dataSize)
+ throw new IgniteException("Compressed stream is truncated
[expected=" + dataSize + ", inflated=" + off + ']');
- tmpBuf.flip();
- tmpBuf.get(bytes);
+ // Any extra inflatable byte means the size header is understated.
+ byte[] probe = new byte[1];
- try (InflaterInputStream iis = new InflaterInputStream(new
ByteArrayInputStream(bytes), inflater)) {
- uncompressedData = iis.readAllBytes();
+ while (true) {
+ if (inflater.inflate(probe, 0, 1) > 0)
+ throw new IgniteException("Compressed stream is longer
than expected [expected=" + dataSize + ']');
+
+ if (inflater.finished() || i == chunks.size())
+ break;
+
+ inflater.setInput(chunks.get(i++));
+ }
}
- catch (IOException ex) {
- throw new IgniteException(ex);
+ catch (DataFormatException e) {
+ throw new IgniteException(e);
}
finally {
inflater.end();
}
- assert uncompressedData != null;
- assert uncompressedData.length == dataSize : "Expected=" + dataSize +
", actual=" + uncompressedData.length;
-
- tmpBuf = null;
+ chunks = null;
- return uncompressedData;
+ return data;
}
/** {@inheritDoc} */
@Override public String toString() {
return S.toString(CompressedMessage.class, this);
}
-
- /** Byte reader returning data chunks of predefined size. */
- private static class ChunkedByteReader {
- /** Input data. */
- private final byte[] inputData;
-
- /** Current position. */
- private int pos;
-
- /** Constructor. */
- ChunkedByteReader(byte[] inputData) {
- this.inputData = inputData;
- }
-
- /** @return Next chunk of bytes or null. */
- byte[] nextChunk() {
- if (pos >= inputData.length)
- return null;
-
- int curChunkSize = Math.min(inputData.length - pos, CHUNK_SIZE);
-
- byte[] chunk = new byte[curChunkSize];
-
- System.arraycopy(inputData, pos, chunk, 0, curChunkSize);
-
- pos += curChunkSize;
-
- return chunk;
- }
- }
}
diff --git
a/modules/core/src/main/java/org/apache/ignite/internal/managers/communication/CompressedMessageSerializer.java
b/modules/core/src/main/java/org/apache/ignite/internal/managers/communication/CompressedMessageSerializer.java
index 594aab336e0..44ae6ab390b 100644
---
a/modules/core/src/main/java/org/apache/ignite/internal/managers/communication/CompressedMessageSerializer.java
+++
b/modules/core/src/main/java/org/apache/ignite/internal/managers/communication/CompressedMessageSerializer.java
@@ -17,12 +17,12 @@
package org.apache.ignite.internal.managers.communication;
-import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import org.apache.ignite.IgniteException;
import org.apache.ignite.plugin.extensions.communication.MessageReader;
import org.apache.ignite.plugin.extensions.communication.MessageSerializer;
import org.apache.ignite.plugin.extensions.communication.MessageWriter;
-import static
org.apache.ignite.internal.managers.communication.CompressedMessage.BUFFER_CAPACITY;
import static
org.apache.ignite.internal.managers.communication.CompressedMessage.CHUNK_SIZE;
/** Message serializer for compressed message. */
@@ -37,7 +37,7 @@ public class CompressedMessageSerializer implements
MessageSerializer<Compressed
}
while (true) {
- if (msg.chunk == null && msg.chunkedReader != null) {
+ if (msg.chunk == null && msg.chunks != null) {
msg.chunk = msg.nextChunk();
msg.finalChunk = (msg.chunk == null);
@@ -75,9 +75,6 @@ public class CompressedMessageSerializer implements
MessageSerializer<Compressed
/** {@inheritDoc} */
@Override public boolean readFrom(CompressedMessage msg, MessageReader
reader) {
- if (msg.tmpBuf == null)
- msg.tmpBuf = ByteBuffer.allocateDirect(BUFFER_CAPACITY);
-
assert msg.chunk == null : msg.chunk;
while (true) {
@@ -88,6 +85,9 @@ public class CompressedMessageSerializer implements
MessageSerializer<Compressed
if (!reader.isLastRead())
return false;
+ if (msg.dataSize < 0)
+ throw new IgniteException("Invalid compressed message
data size: " + msg.dataSize);
+
if (msg.dataSize == 0)
return true;
@@ -110,23 +110,19 @@ public class CompressedMessageSerializer implements
MessageSerializer<Compressed
if (!reader.isLastRead())
return false;
- if (msg.chunk != null) {
- if (msg.tmpBuf.remaining() <= CHUNK_SIZE) {
- ByteBuffer newTmpBuf =
ByteBuffer.allocateDirect(msg.tmpBuf.capacity() * 2);
-
- msg.tmpBuf.flip();
-
- newTmpBuf.put(msg.tmpBuf);
+ if (msg.chunk == null) {
+ throw new IgniteException("Failed to read compressed
message: unexpected null chunk " +
+ "(stream is corrupted or the sender is
incompatible).");
+ }
- msg.tmpBuf = newTmpBuf;
- }
+ if (msg.chunks == null)
+ msg.chunks = new ArrayList<>(Math.min(msg.dataSize /
CHUNK_SIZE + 1, 1024));
- msg.tmpBuf.put(msg.chunk);
+ msg.chunks.add(msg.chunk);
- reader.decrementState();
+ reader.decrementState();
- msg.chunk = null;
- }
+ msg.chunk = null;
}
}
}
diff --git
a/modules/core/src/test/java/org/apache/ignite/internal/managers/communication/CompressedMessageTest.java
b/modules/core/src/test/java/org/apache/ignite/internal/managers/communication/CompressedMessageTest.java
index 53fa342460b..98f1478c2b3 100644
---
a/modules/core/src/test/java/org/apache/ignite/internal/managers/communication/CompressedMessageTest.java
+++
b/modules/core/src/test/java/org/apache/ignite/internal/managers/communication/CompressedMessageTest.java
@@ -19,9 +19,13 @@ package org.apache.ignite.internal.managers.communication;
import java.nio.ByteBuffer;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
+import java.util.Random;
import java.util.Set;
import java.util.UUID;
+import java.util.zip.Deflater;
+import org.apache.ignite.IgniteException;
import org.apache.ignite.internal.CoreMessagesProvider;
import org.apache.ignite.internal.direct.DirectMessageReader;
import org.apache.ignite.internal.direct.DirectMessageWriter;
@@ -34,6 +38,7 @@ import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.plugin.extensions.communication.Message;
import org.apache.ignite.plugin.extensions.communication.MessageFactory;
import
org.apache.ignite.plugin.extensions.communication.MessageFactoryProvider;
+import org.apache.ignite.testframework.GridTestUtils;
import org.junit.Test;
import static org.apache.ignite.marshaller.Marshallers.jdk;
@@ -42,13 +47,14 @@ import static org.junit.Assert.assertTrue;
/** Test for {@link CompressedMessage}. */
public class CompressedMessageTest {
+ /** */
+ private static final MessageFactory MSG_FACTORY = new
IgniteMessageFactoryImpl(new MessageFactoryProvider[]{
+ new CoreMessagesProvider(jdk(), jdk(), U.gridClassLoader())});
+
/** */
@Test
public void testWriteReadHugeMessage() {
- MessageFactory msgFactory = new IgniteMessageFactoryImpl(new
MessageFactoryProvider[]{
- new CoreMessagesProvider(jdk(), jdk(), U.gridClassLoader())});
-
- DirectMessageWriter writer = new DirectMessageWriter(msgFactory);
+ DirectMessageWriter writer = new DirectMessageWriter(MSG_FACTORY);
ByteBuffer tmpBuf = ByteBuffer.allocate(4096);
@@ -74,9 +80,9 @@ public class CompressedMessageTest {
assertTrue(compressedMsg.dataSize() > 0);
- byte[] compressedData = U.field((Object)U.field(compressedMsg,
"chunkedReader"), "inputData");
+ List<byte[]> chunks = U.field(compressedMsg, "chunks");
- assertTrue(compressedData.length >
CompressedMessage.CHUNK_SIZE * 2);
+ assertTrue(chunks.size() > 2);
checkChunkCnt = false;
}
@@ -94,7 +100,7 @@ public class CompressedMessageTest {
msgBuf.flip();
- DirectMessageReader reader = new DirectMessageReader(msgFactory, null);
+ DirectMessageReader reader = new DirectMessageReader(MSG_FACTORY,
null);
reader.setBuffer(msgBuf);
@@ -105,6 +111,137 @@ public class CompressedMessageTest {
assertEqualsFullMsg(fullMsg, (GridDhtPartitionsFullMessage)readMsg);
}
+ /** Read must fail with an exception on a null chunk from the wire instead
of looping forever. */
+ @Test
+ public void testReadFailsOnNullChunk() {
+ DirectMessageWriter writer = new DirectMessageWriter(MSG_FACTORY);
+
+ ByteBuffer buf = ByteBuffer.allocate(16);
+
+ writer.setBuffer(buf);
+
+ // Emulate a corrupted stream or an incompatible peer: dataSize > 0,
non-final chunk, then the null-array
+ // marker (-1), which CompressedMessageSerializer.writeTo() never
produces at the chunk position.
+ writer.writeInt(100);
+ writer.writeBoolean(false);
+ writer.writeByteArray(null);
+
+ buf.flip();
+
+ DirectMessageReader reader = new DirectMessageReader(MSG_FACTORY,
null);
+
+ reader.setBuffer(buf);
+
+ GridTestUtils.assertThrows(null,
+ () -> new CompressedMessageSerializer().readFrom(new
CompressedMessage(), reader),
+ IgniteException.class,
+ "unexpected null chunk");
+ }
+
+ /** Read must fail fast on a negative data size from the wire. */
+ @Test
+ public void testReadFailsOnNegativeDataSize() {
+ DirectMessageWriter writer = new DirectMessageWriter(MSG_FACTORY);
+
+ ByteBuffer buf = ByteBuffer.allocate(16);
+
+ writer.setBuffer(buf);
+
+ writer.writeInt(-5);
+
+ buf.flip();
+
+ DirectMessageReader reader = new DirectMessageReader(MSG_FACTORY,
null);
+
+ reader.setBuffer(buf);
+
+ GridTestUtils.assertThrows(null,
+ () -> new CompressedMessageSerializer().readFrom(new
CompressedMessage(), reader),
+ IgniteException.class,
+ "Invalid compressed message data size");
+ }
+
+ /** Uncompress must fail when dataSize > 0 but no chunks were received. */
+ @Test
+ public void testUncompressFailsWithoutChunks() {
+ CompressedMessage rcvd = new CompressedMessage();
+
+ rcvd.dataSize = 100;
+ rcvd.finalChunk = true;
+
+ GridTestUtils.assertThrows(null, rcvd::uncompressed,
IgniteException.class, "truncated");
+ }
+
+ /** Uncompress must fail when the stream inflates to more bytes than the
size header claims. */
+ @Test
+ public void testUncompressFailsOnUnderstatedDataSize() {
+ byte[] data = new byte[1000];
+
+ CompressedMessage sent = new CompressedMessage(ByteBuffer.wrap(data),
Deflater.BEST_SPEED);
+
+ CompressedMessage rcvd = new CompressedMessage();
+
+ rcvd.dataSize = data.length - 1;
+ rcvd.chunks = sent.chunks;
+ rcvd.finalChunk = true;
+
+ GridTestUtils.assertThrows(null, rcvd::uncompressed,
IgniteException.class, "longer than expected");
+ }
+
+ /** Same as {@link #testUncompressFailsOnUnderstatedDataSize()}, but with
a multi-chunk compressed stream. */
+ @Test
+ public void testUncompressFailsOnUnderstatedDataSizeMultiChunk() {
+ byte[] data = new byte[CompressedMessage.CHUNK_SIZE * 3];
+
+ new Random(42).nextBytes(data);
+
+ CompressedMessage sent = new CompressedMessage(ByteBuffer.wrap(data),
Deflater.BEST_SPEED);
+
+ assertTrue(sent.chunks.size() > 1);
+
+ CompressedMessage rcvd = new CompressedMessage();
+
+ rcvd.dataSize = CompressedMessage.CHUNK_SIZE / 2;
+ rcvd.chunks = sent.chunks;
+ rcvd.finalChunk = true;
+
+ GridTestUtils.assertThrows(null, rcvd::uncompressed,
IgniteException.class, "longer than expected");
+ }
+
+ /** A complete envelope whose payload doesn't deserialize fully must fail
instead of hanging as a partial read. */
+ @Test
+ public void testReadFailsOnTruncatedPayload() {
+ DirectMessageWriter writer = new DirectMessageWriter(MSG_FACTORY);
+
+ ByteBuffer tmpBuf = ByteBuffer.allocate(1 << 20);
+
+ writer.setBuffer(tmpBuf);
+
+ assertTrue(writer.writeMessage(fullMessage(), false));
+
+ tmpBuf.flip();
+
+ tmpBuf.limit(tmpBuf.limit() - 5); // Truncate the serialized message.
+
+ CompressedMessage compressedMsg = new CompressedMessage(tmpBuf,
Deflater.BEST_SPEED);
+
+ DirectMessageWriter wireWriter = new DirectMessageWriter(MSG_FACTORY);
+
+ ByteBuffer wire = ByteBuffer.allocate(1 << 20);
+
+ wireWriter.setBuffer(wire);
+
+ assertTrue(wireWriter.writeMessage(compressedMsg, false));
+
+ wire.flip();
+
+ DirectMessageReader reader = new DirectMessageReader(MSG_FACTORY,
null);
+
+ reader.setBuffer(wire);
+
+ GridTestUtils.assertThrows(null, () -> reader.readMessage(true),
IgniteException.class, "ended unexpectedly");
+ }
+
/** */
private GridDhtPartitionsFullMessage fullMessage() {
Map<UUID, Map<GroupPartitionIdPair, Long>> partHistSuppliers = new
HashMap<>();