This is an automated email from the ASF dual-hosted git repository.
rpuch pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push:
new 038f12f5ad IGNITE-23592 Align serialization methods of HybridTimestamp
(#4670)
038f12f5ad is described below
commit 038f12f5adb907561c95ce3f56f6d57a668f7539
Author: Roman Puchkovskiy <[email protected]>
AuthorDate: Tue Nov 5 14:07:31 2024 +0400
IGNITE-23592 Align serialization methods of HybridTimestamp (#4670)
---
.../ignite/internal/hlc/HybridTimestamp.java | 61 +++++++++++++++++-
.../org/apache/ignite/internal/util/ByteUtils.java | 58 ++++++++++++++---
.../apache/ignite/internal/util/VarIntUtils.java | 24 +++++++
.../ignite/internal/hlc/HybridTimestampTest.java | 73 ++++++++++++++++++----
4 files changed, 193 insertions(+), 23 deletions(-)
diff --git
a/modules/core/src/main/java/org/apache/ignite/internal/hlc/HybridTimestamp.java
b/modules/core/src/main/java/org/apache/ignite/internal/hlc/HybridTimestamp.java
index a7947892da..b132c82f1d 100644
---
a/modules/core/src/main/java/org/apache/ignite/internal/hlc/HybridTimestamp.java
+++
b/modules/core/src/main/java/org/apache/ignite/internal/hlc/HybridTimestamp.java
@@ -24,12 +24,26 @@ import java.io.Serializable;
import java.time.Instant;
import java.time.ZoneOffset;
import org.apache.ignite.internal.util.ByteUtils;
+import org.apache.ignite.internal.util.VarIntUtils;
import org.apache.ignite.internal.util.io.IgniteDataInput;
import org.apache.ignite.internal.util.io.IgniteDataOutput;
import org.jetbrains.annotations.Nullable;
/**
* A hybrid timestamp that combines physical clock and logical clock.
+ *
+ * <p>Serialization methods ({@link #toBytes()}, {@link
#writeTo(IgniteDataOutput)}, {@link #write(HybridTimestamp, IgniteDataOutput)})
+ * write a timestamp in the following format:
+ *
+ * <pre>{@code
+ * <TS> ::= <PHYSICAL_PART> <LOGICAL_PART>
+ *
+ * <PHYSICAL_PART> ::= <PHYSICAL_HIGH> (4 bytes int) <PHYSICAL_LOW> (2 bytes
short) // both are in little-endian
+ *
+ * <LOGICAL_PART> ::= (varint)
+ * }</pre>
+ *
+ * <p>A null timestamp is serialized as both parts (physical and logical)
equal to 0.
*/
public final class HybridTimestamp implements Comparable<HybridTimestamp>,
Serializable {
/** Serial version UID. */
@@ -128,10 +142,24 @@ public final class HybridTimestamp implements
Comparable<HybridTimestamp>, Seria
/**
* Restores a timestamp converted to bytes using {@link #toBytes()}.
*
+ * <p>See the class javadoc for details on the serialization format.
+ *
* @param bytes Byte array representing a timestamp.
+ * @see #toBytes()
*/
public static HybridTimestamp fromBytes(byte[] bytes) {
- return hybridTimestamp(ByteUtils.bytesToLong(bytes));
+ // Reversing bytes as ByteUtils works in BE, but we store in LE (as
IgniteDataOutput uses LE and we want to be consistent between
+ // serialization methods).
+ long physicalHigh = Integer.reverseBytes(ByteUtils.bytesToInt(bytes,
0));
+ int physicalLow = Short.reverseBytes(ByteUtils.bytesToShort(bytes,
Integer.BYTES)) & 0xFFFF;
+
+ long physical = (physicalHigh << Short.SIZE) | physicalLow;
+ //noinspection NumericCastThatLosesPrecision
+ int logical = (int) VarIntUtils.readVarInt(bytes, Integer.BYTES +
Short.BYTES);
+
+ assert physical != 0 || logical != 0;
+
+ return new HybridTimestamp(physical, logical);
}
/**
@@ -255,16 +283,36 @@ public final class HybridTimestamp implements
Comparable<HybridTimestamp>, Seria
/**
* Returns a byte array representing this timestamp.
+ *
+ * <p>See the class javadoc for details on the serialization format.
+ *
+ * @see #fromBytes(byte[])
*/
+ @SuppressWarnings("NumericCastThatLosesPrecision")
public byte[] toBytes() {
- return ByteUtils.longToBytes(longValue());
+ long physical = getPhysical();
+ int logical = getLogical();
+
+ byte[] bytes = new byte[Integer.BYTES + Short.BYTES +
VarIntUtils.varIntLength(logical)];
+
+ // Reversing bytes as ByteUtils works in BE, but we store in LE (as
IgniteDataOutput uses LE and we want to be consistent between
+ // serialization methods).
+ ByteUtils.putIntToBytes(Integer.reverseBytes((int) (physical >>
Short.SIZE)), bytes, 0);
+ ByteUtils.putShortToBytes(Short.reverseBytes((short) (physical &
0xFFFF)), bytes, Integer.BYTES);
+
+ VarIntUtils.putVarIntToBytes(logical, bytes, Integer.BYTES +
Short.BYTES);
+
+ return bytes;
}
/**
* Writes this timestamp to the output.
*
+ * <p>See the class javadoc for details on the serialization format.
+ *
* @param out Output to write to.
* @throws IOException If something goes wrong.
+ * @see #readFrom(IgniteDataInput)
*/
public void writeTo(IgniteDataOutput out) throws IOException {
long physical = getPhysical();
@@ -279,9 +327,12 @@ public final class HybridTimestamp implements
Comparable<HybridTimestamp>, Seria
/**
* Writes a nullable timestamp to an output.
*
+ * <p>See the class javadoc for details on the serialization format.
+ *
* @param timestamp Timestamp to write
* @param out Output to write to.
* @throws IOException If something goes wrong.
+ * @see #readNullableFrom(IgniteDataInput)
*/
public static void write(@Nullable HybridTimestamp timestamp,
IgniteDataOutput out) throws IOException {
if (timestamp == null) {
@@ -296,8 +347,11 @@ public final class HybridTimestamp implements
Comparable<HybridTimestamp>, Seria
/**
* Reads a timestamp written with {@link #writeTo(IgniteDataOutput)}.
*
+ * <p>See the class javadoc for details on the serialization format.
+ *
* @param in Input from which to read.
* @throws IOException If something goes wrong.
+ * @see #write(HybridTimestamp, IgniteDataOutput)
*/
public static @Nullable HybridTimestamp readNullableFrom(IgniteDataInput
in) throws IOException {
long physicalHigh = in.readInt();
@@ -316,8 +370,11 @@ public final class HybridTimestamp implements
Comparable<HybridTimestamp>, Seria
/**
* Reads a timestamp written with {@link #writeTo(IgniteDataOutput)}.
*
+ * <p>See the class javadoc for details on the serialization format.
+ *
* @param in Input from which to read.
* @throws IOException If something goes wrong.
+ * @see #writeTo(IgniteDataOutput)
*/
public static HybridTimestamp readFrom(IgniteDataInput in) throws
IOException {
HybridTimestamp ts = readNullableFrom(in);
diff --git
a/modules/core/src/main/java/org/apache/ignite/internal/util/ByteUtils.java
b/modules/core/src/main/java/org/apache/ignite/internal/util/ByteUtils.java
index 745fb38605..d39ca490dc 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/util/ByteUtils.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/util/ByteUtils.java
@@ -48,11 +48,11 @@ public class ByteUtils {
public static long bytesToLong(byte[] bytes, int off) {
assert bytes != null;
- int bytesCnt = Math.min(Long.BYTES, bytes.length - off);
+ int bytesCount = Math.min(Long.BYTES, bytes.length - off);
long res = 0;
- for (int i = 0; i < bytesCnt; i++) {
+ for (int i = 0; i < bytesCount; i++) {
res = (res << 8) | (0xffL & bytes[off + i]);
}
@@ -131,11 +131,11 @@ public class ByteUtils {
public static int bytesToInt(byte[] bytes, int off) {
assert bytes != null;
- int bytesCnt = Math.min(Integer.BYTES, bytes.length - off);
+ int bytesCount = Math.min(Integer.BYTES, bytes.length - off);
int res = 0;
- for (int i = 0; i < bytesCnt; i++) {
+ for (int i = 0; i < bytesCount; i++) {
res = (res << 8) | (0xff & bytes[off + i]);
}
@@ -163,6 +163,26 @@ public class ByteUtils {
return bytesToInt(bytes) ^ 0x00808080;
}
+ /**
+ * Constructs {@code short} from byte array in Big Endian order.
+ *
+ * @param bytes Array of bytes.
+ * @param off Offset in {@code bytes} array.
+ * @return Short value.
+ */
+ public static short bytesToShort(byte[] bytes, int off) {
+ assert bytes != null;
+
+ int bytesCount = Math.min(Short.BYTES, bytes.length - off);
+
+ int res = 0;
+
+ for (int i = 0; i < bytesCount; i++) {
+ res = (res << 8) | (0xff & bytes[off + i]);
+ }
+
+ return (short) res;
+ }
/**
* Converts a primitive {@code int} value to a byte array in Big Endian
order.
@@ -194,17 +214,35 @@ public class ByteUtils {
* @return The array.
*/
public static byte[] putIntToBytes(int i, byte[] bytes, int off) {
- assert bytes != null;
- assert bytes.length >= off + Integer.BYTES;
+ putToBytes(i, Integer.BYTES, bytes, off);
- for (int k = Integer.BYTES - 1; k >= 0; k--) {
- bytes[off + k] = (byte) i;
- i >>>= 8;
- }
+ return bytes;
+ }
+
+ /**
+ * Converts a primitive {@code short} value to a byte array in Big Endian
order and stores it in the specified byte array.
+ *
+ * @param s Unsigned int value.
+ * @param bytes Bytes array to write result to.
+ * @param off Offset in the target array to write result to.
+ * @return The array.
+ */
+ public static byte[] putShortToBytes(short s, byte[] bytes, int off) {
+ putToBytes(s, Short.BYTES, bytes, off);
return bytes;
}
+ private static void putToBytes(long value, int sizeInBytes, byte[] bytes,
int off) {
+ assert bytes != null;
+ assert off + sizeInBytes <= bytes.length;
+
+ for (int k = sizeInBytes - 1; k >= 0; k--) {
+ bytes[off + k] = (byte) value;
+ value >>>= Byte.SIZE;
+ }
+ }
+
/**
* Converts a {@link Boolean} value to a single byte.
*
diff --git
a/modules/core/src/main/java/org/apache/ignite/internal/util/VarIntUtils.java
b/modules/core/src/main/java/org/apache/ignite/internal/util/VarIntUtils.java
index 6a43e19ab0..550cb56768 100644
---
a/modules/core/src/main/java/org/apache/ignite/internal/util/VarIntUtils.java
+++
b/modules/core/src/main/java/org/apache/ignite/internal/util/VarIntUtils.java
@@ -129,6 +129,30 @@ public class VarIntUtils {
return res - 1;
}
+ /**
+ * Reads a varint from a byte buffer.
+ *
+ * @param bytes Array from which to read.
+ * @param off Offset to start reading from.
+ * @return Long value that was encoded as a varint.
+ */
+ public static long readVarInt(byte[] bytes, int off) {
+ long res = 0;
+
+ int index = off;
+ for (int shift = 0; ; shift += 7) {
+ byte b = bytes[index++];
+
+ res |= ((long) b & 0x7F) << shift;
+
+ if (b >= 0) {
+ break;
+ }
+ }
+
+ return res - 1;
+ }
+
/**
* Reads a varint from an input.
*
diff --git
a/modules/core/src/test/java/org/apache/ignite/internal/hlc/HybridTimestampTest.java
b/modules/core/src/test/java/org/apache/ignite/internal/hlc/HybridTimestampTest.java
index 13739bf28d..96cfe7cf52 100644
---
a/modules/core/src/test/java/org/apache/ignite/internal/hlc/HybridTimestampTest.java
+++
b/modules/core/src/test/java/org/apache/ignite/internal/hlc/HybridTimestampTest.java
@@ -34,6 +34,8 @@ import org.apache.ignite.internal.util.io.IgniteDataOutput;
import org.apache.ignite.internal.util.io.IgniteUnsafeDataInput;
import org.apache.ignite.internal.util.io.IgniteUnsafeDataOutput;
import org.junit.jupiter.api.Test;
+import org.junitpioneer.jupiter.cartesian.CartesianTest;
+import org.junitpioneer.jupiter.cartesian.CartesianTest.Enum;
/**
* Tests of a hybrid timestamp implementation.
@@ -165,21 +167,14 @@ class HybridTimestampTest {
assertThat(ts.roundUpToPhysicalTick(), is(new HybridTimestamp(2, 0)));
}
- @Test
- void serializationAndDeserializationForNonNull() throws Exception {
+ @CartesianTest
+ void serializationAndDeserializationForNonNull(@Enum Serializer
serializer, @Enum Deserializer deserializer) throws Exception {
HybridTimestamp originalTs = new
HybridTimestamp(System.currentTimeMillis(), 2);
- IgniteDataOutput out = new IgniteUnsafeDataOutput(100);
-
- originalTs.writeTo(out);
-
- IgniteDataInput in = new IgniteUnsafeDataInput(out.array());
-
- HybridTimestamp restoredTs = HybridTimestamp.readFrom(in);
+ byte[] bytes = serializer.serialize(originalTs);
+ HybridTimestamp restoredTs = deserializer.deserialize(bytes);
assertThat(restoredTs, is(originalTs));
-
- assertThat(in.available(), is(0));
}
@Test
@@ -195,6 +190,7 @@ class HybridTimestampTest {
assertThat(in.available(), is(0));
}
+ @SuppressWarnings("resource")
@Test
void readFromFailsWhenDeserializingNull() throws Exception {
IgniteDataOutput out = new IgniteUnsafeDataOutput(100);
@@ -207,4 +203,59 @@ class HybridTimestampTest {
assertThat(in.available(), is(0));
}
+
+ private enum Serializer {
+ TO_BYTES(HybridTimestamp::toBytes),
+ WRITE_TO(ts -> {
+ IgniteDataOutput out = new IgniteUnsafeDataOutput(100);
+ ts.writeTo(out);
+ return out.array();
+ }),
+ WRITE(ts -> {
+ IgniteDataOutput out = new IgniteUnsafeDataOutput(100);
+ HybridTimestamp.write(ts, out);
+ return out.array();
+ });
+
+ private final IoFunction<HybridTimestamp, byte[]> transformation;
+
+ Serializer(IoFunction<HybridTimestamp, byte[]> transformation) {
+ this.transformation = transformation;
+ }
+
+ byte[] serialize(HybridTimestamp timestamp) throws IOException {
+ return transformation.apply(timestamp);
+ }
+ }
+
+ private enum Deserializer {
+ FROM_BYTES(HybridTimestamp::fromBytes),
+ READ_FROM(bytes -> {
+ IgniteDataInput in = new IgniteUnsafeDataInput(bytes);
+ HybridTimestamp ts = HybridTimestamp.readFrom(in);
+ assertThat(in.available(), is(0));
+ return ts;
+ }),
+ READ_NULLABLE_FROM(bytes -> {
+ IgniteDataInput in = new IgniteUnsafeDataInput(bytes);
+ HybridTimestamp ts = HybridTimestamp.readNullableFrom(in);
+ assertThat(in.available(), is(0));
+ return ts;
+ });
+
+ private final IoFunction<byte[], HybridTimestamp> transformation;
+
+ Deserializer(IoFunction<byte[], HybridTimestamp> transformation) {
+ this.transformation = transformation;
+ }
+
+ HybridTimestamp deserialize(byte[] bytes) throws IOException {
+ return transformation.apply(bytes);
+ }
+ }
+
+ @FunctionalInterface
+ private interface IoFunction<T, R> {
+ R apply(T t) throws IOException;
+ }
}