This is an automated email from the ASF dual-hosted git repository.
tkalkirill 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 59abb56b209 IGNITE-26649 Add unit tests for serialization
compatibility of all MetaStorage Raft commands (#6736)
59abb56b209 is described below
commit 59abb56b209f03490a2e24b7a7829b510d461c76
Author: Kirill Tkalenko <[email protected]>
AuthorDate: Fri Oct 10 11:54:20 2025 +0300
IGNITE-26649 Add unit tests for serialization compatibility of all
MetaStorage Raft commands (#6736)
---
modules/metastorage/build.gradle | 1 +
.../MetastorageCommandsCompatibilityTest.java | 485 +++++++++++++++++++++
2 files changed, 486 insertions(+)
diff --git a/modules/metastorage/build.gradle b/modules/metastorage/build.gradle
index 17ba03e0618..e599d32d928 100644
--- a/modules/metastorage/build.gradle
+++ b/modules/metastorage/build.gradle
@@ -44,6 +44,7 @@ dependencies {
annotationProcessor project(':ignite-network-annotation-processor')
+ testImplementation project(':ignite-network')
testImplementation testFixtures(project(':ignite-core'))
testImplementation testFixtures(project(':ignite-vault'))
testImplementation testFixtures(project(':ignite-configuration'))
diff --git
a/modules/metastorage/src/test/java/org/apache/ignite/internal/metastorage/command/MetastorageCommandsCompatibilityTest.java
b/modules/metastorage/src/test/java/org/apache/ignite/internal/metastorage/command/MetastorageCommandsCompatibilityTest.java
new file mode 100644
index 00000000000..3d8e5cf8059
--- /dev/null
+++
b/modules/metastorage/src/test/java/org/apache/ignite/internal/metastorage/command/MetastorageCommandsCompatibilityTest.java
@@ -0,0 +1,485 @@
+/*
+ * 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.metastorage.command;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.ByteBuffer;
+import java.util.Base64;
+import java.util.List;
+import java.util.UUID;
+import org.apache.ignite.internal.hlc.HybridTimestamp;
+import org.apache.ignite.internal.lang.ByteArray;
+import org.apache.ignite.internal.metastorage.CommandId;
+import org.apache.ignite.internal.metastorage.dsl.Condition;
+import org.apache.ignite.internal.metastorage.dsl.Conditions;
+import org.apache.ignite.internal.metastorage.dsl.Iif;
+import
org.apache.ignite.internal.metastorage.dsl.MetaStorageMessagesSerializationRegistryInitializer;
+import org.apache.ignite.internal.metastorage.dsl.Operations;
+import org.apache.ignite.internal.metastorage.dsl.Statements;
+import org.apache.ignite.internal.network.MessageSerializationRegistryImpl;
+import
org.apache.ignite.internal.network.serialization.MessageSerializationRegistry;
+import org.apache.ignite.internal.raft.Command;
+import org.apache.ignite.internal.raft.Marshaller;
+import org.apache.ignite.internal.raft.util.ThreadLocalOptimizedMarshaller;
+import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Compatibility testing for serialization/deserialization of metastorage raft
commands. It is verified that deserialization of commands
+ * that were created on earlier versions of the product will be error-free.
+ *
+ * <p>For MAC users with aarch64 architecture, you will need to add {@code ||
"aarch64".equals(arch)} to the
+ * {@code GridUnsafe#unaligned()} for the tests to pass. For more details, see
+ * <a
href="https://lists.apache.org/thread/67coyvm8mo7106mkndt24yqwtbvb7590">discussion</a>.</p>
+ *
+ * <p>To serialize commands, use {@link #serializeAll()} and insert the result
into the appropriate tests.</p>
+ */
+public class MetastorageCommandsCompatibilityTest extends
BaseIgniteAbstractTest {
+ private final MessageSerializationRegistry registry = new
MessageSerializationRegistryImpl();
+
+ private final Marshaller marshaller = new
ThreadLocalOptimizedMarshaller(registry);
+
+ private final MetaStorageCommandsFactory factory = new
MetaStorageCommandsFactory();
+
+ @BeforeEach
+ void setUp() {
+ new
MetaStorageCommandsSerializationRegistryInitializer().registerFactories(registry);
+ new
MetaStorageMessagesSerializationRegistryInitializer().registerFactories(registry);
+ }
+
+ @Test
+ void testCompactionCommand() {
+ CompactionCommand command = decodeCommand("cEkrR0Y=");
+
+ assertEquals(safeTime(), command.safeTime());
+ assertEquals(initiatorTime(), command.initiatorTime());
+ assertEquals(42, command.compactionRevision());
+ }
+
+ @Test
+ void testEvictIdempotentCommandsCacheCommand() {
+ EvictIdempotentCommandsCacheCommand command =
decodeCommand("cEhlR0Y=");
+
+ assertEquals(safeTime(), command.safeTime());
+ assertEquals(initiatorTime(), command.initiatorTime());
+ assertEquals(HybridTimestamp.hybridTimestamp(100),
command.evictionTimestamp());
+ }
+
+ @Test
+ void testGetAllCommand() {
+ GetAllCommand command = decodeCommand("cB8DAwVrZXkxAwVrZXkyKw==");
+
+ assertEquals(42, command.revision());
+ assertEquals(List.of(key("key1"), key("key2")), command.keys());
+ }
+
+ @Test
+ void testGetChecksumCommand() {
+ GetChecksumCommand command = decodeCommand("cCMr");
+
+ assertEquals(42, command.revision());
+ }
+
+ @Test
+ void testGetCommand() {
+ GetCommand command = decodeCommand("cBUDBWtleTEr");
+
+ assertEquals(42, command.revision());
+ assertEquals(key("key1"), command.key());
+ }
+
+ @Test
+ void testGetCurrentRevisionsCommand() {
+ Command command = decodeCommand("cCI=");
+
+ assertInstanceOf(GetCurrentRevisionsCommand.class, command);
+ }
+
+ @Test
+ void testGetPrefixCommand() {
+ GetPrefixCommand command =
decodeCommand("cD5lAQMIcHJlZml4MQ1wcmV2aW91c0tleTEr");
+
+ assertEquals(key("prefix1"), command.prefix());
+ assertEquals(100, command.batchSize());
+ assertTrue(command.includeTombstones());
+ assertArrayEquals(keyBytes("previousKey1"), command.previousKey());
+ assertEquals(42, command.revUpperBound());
+ }
+
+ @Test
+ void testGetRangeCommand() {
+ GetRangeCommand command =
decodeCommand("cD1lAQMJa2V5RnJvbTEDB2tleVRvMQ1wcmV2aW91c0tleTEr");
+
+ assertEquals(key("keyFrom1"), command.keyFrom());
+ assertEquals(key("keyTo1"), command.keyTo());
+ assertEquals(100, command.batchSize());
+ assertTrue(command.includeTombstones());
+ assertArrayEquals(keyBytes("previousKey1"), command.previousKey());
+ assertEquals(42, command.revUpperBound());
+ }
+
+ @Test
+ void testInvokeCommand() {
+ InvokeCommand command = decodeCommand(
+
"cAvfAQIDCGV4aXN0czEOAt8BBgMIcmVtb3ZlMQQA3wEMRwAAAAAAAAAAKgAAAAAAAABFR0YC3wEGAwVwdXQxAwMFdmFsMQ=="
+ );
+
+ assertEquals(safeTime(), command.safeTime());
+ assertEquals(initiatorTime(), command.initiatorTime());
+ assertEquals(commandId(), command.id());
+ assertEquals(Conditions.exists(keyAsByteArray("exists1")),
command.condition());
+ assertEquals(
+ List.of(Operations.put(keyAsByteArray("put1"),
keyBytes("val1"))),
+ command.success()
+ );
+ assertEquals(
+ List.of(Operations.remove(keyAsByteArray("remove1"))),
+ command.failure()
+ );
+ }
+
+ @Test
+ @Disabled("https://issues.apache.org/jira/browse/IGNITE-26664")
+ void testMultiInvokeCommand() {
+ MultiInvokeCommand command = decodeCommand(
+
"cAzfAQxHAAAAAAAAAAAqAAAAAAAAAEXfAQnfAQvfAQgC3wEGAwVwdXQxAwMFdmFsMd8BBwMCAd8BBd8BBd8BAgMLdG9tYnN0b25lMRDfAQIDDm5vdFRvbWJzd"
+ +
"G9uZTERAt8BBd8BBd8BAgMIZXhpc3RzMQ7fAQIDC25vdEV4aXN0czEPA98BBd8BAwMKcmV2aXNpb24xAgLfAQQDB3ZhbHVlMQkDBXZhbDECAwLf"
+ + "AQvfAQgC3wEGAAIA3wEHAwIAR0Y="
+ );
+
+ Condition tombstonesConditions = Conditions.and(
+ Conditions.tombstone(keyAsByteArray("tombstone1")),
+ Conditions.notTombstone(keyAsByteArray("notTombstone1"))
+ );
+
+ Condition existsConditions = Conditions.or(
+ Conditions.exists(keyAsByteArray("exists1")),
+ Conditions.notExists(keyAsByteArray("notExists1")
+ ));
+
+ Condition revisionAndValueConsitions = Conditions.and(
+ Conditions.revision(keyAsByteArray("revision1")).eq(1L),
+ Conditions.value(keyAsByteArray("value1")).ne(keyBytes("val1"))
+ );
+
+ Condition complexCondition = Conditions.and(
+ tombstonesConditions,
+ Conditions.or(existsConditions, revisionAndValueConsitions)
+ );
+
+ Iif iif = Statements.iif(
+ complexCondition,
+ Operations.ops(Operations.put(keyAsByteArray("put1"),
keyBytes("val1"))).yield(true),
+ Operations.ops(Operations.noop()).yield(false)
+ );
+
+ assertEquals(safeTime(), command.safeTime());
+ assertEquals(initiatorTime(), command.initiatorTime());
+ assertEquals(commandId(), command.id());
+ assertEquals(iif, command.iif());
+ }
+
+ @Test
+ void testPutAllCommand() {
+ PutAllCommand command =
decodeCommand("cDNHAwMFa2V5MQMFa2V5MkYDAwV2YWwxAwV2YWwy");
+
+ assertEquals(safeTime(), command.safeTime());
+ assertEquals(initiatorTime(), command.initiatorTime());
+ assertEquals(List.of(key("key1"), key("key2")), command.keys());
+ assertEquals(List.of(key("val1"), key("val2")), command.values());
+ }
+
+ @Test
+ void testPutCommand() {
+ PutCommand command = decodeCommand("cClHAwVrZXkxRgMFdmFsMQ==");
+
+ assertEquals(safeTime(), command.safeTime());
+ assertEquals(initiatorTime(), command.initiatorTime());
+ assertEquals(key("key1"), command.key());
+ assertEquals(key("val1"), command.value());
+ }
+
+ @Test
+ void testRemoveAllCommand() {
+ RemoveAllCommand command = decodeCommand("cDRHAwMFa2V5MQMFa2V5MkY=");
+
+ assertEquals(safeTime(), command.safeTime());
+ assertEquals(initiatorTime(), command.initiatorTime());
+ assertEquals(List.of(key("key1"), key("key2")), command.keys());
+ }
+
+ @Test
+ void testRemoveByPrefixCommand() {
+ RemoveByPrefixCommand command = decodeCommand("cDVHAwhwcmVmaXgxRg==");
+
+ assertEquals(safeTime(), command.safeTime());
+ assertEquals(initiatorTime(), command.initiatorTime());
+ assertEquals(key("prefix1"), command.prefix());
+ }
+
+ @Test
+ void testRemoveCommand() {
+ RemoveCommand command = decodeCommand("cCpHAwVrZXkxRg==");
+
+ assertEquals(safeTime(), command.safeTime());
+ assertEquals(initiatorTime(), command.initiatorTime());
+ assertEquals(key("key1"), command.key());
+ }
+
+ @Test
+ void testSyncTimeCommand() {
+ SyncTimeCommand command = decodeCommand("cEcrR0Y=");
+
+ assertEquals(safeTime(), command.safeTime());
+ assertEquals(initiatorTime(), command.initiatorTime());
+ assertEquals(42, command.initiatorTerm());
+ }
+
+ private static HybridTimestamp initiatorTime() {
+ return HybridTimestamp.hybridTimestamp(70);
+ }
+
+ private static HybridTimestamp safeTime() {
+ return HybridTimestamp.hybridTimestamp(69);
+ }
+
+ private static UUID uuid() {
+ return new UUID(42, 69);
+ }
+
+ private static CommandId commandId() {
+ return CommandId.fromString(uuid() + "_cnt_" + 70);
+ }
+
+ private static ByteBuffer key(String key) {
+ return ByteBuffer.wrap(keyBytes(key));
+ }
+
+ private static byte[] keyBytes(String key) {
+ return key.getBytes(UTF_8);
+ }
+
+ private static ByteArray keyAsByteArray(String key) {
+ return ByteArray.fromString(key);
+ }
+
+ private <T extends Command> T deserializeCommand(byte[] bytes) {
+ return marshaller.unmarshall(bytes);
+ }
+
+ private <T extends Command> T decodeCommand(String base64) {
+ return deserializeCommand(Base64.getDecoder().decode(base64));
+ }
+
+ @SuppressWarnings("unused")
+ private void serializeAll() {
+ List<Command> commands = List.of(
+ createCompactionCommand(),
+ createEvictIdempotentCommandsCacheCommand(),
+ createGetAllCommand(),
+ createGetChecksumCommand(),
+ createGetCommand(),
+ createGetCurrentRevisionsCommand(),
+ createGetPrefixCommand(),
+ createGetRangeCommand(),
+ createInvokeCommand(),
+ createMultiInvokeCommand(),
+ createPutAllCommand(),
+ createPutCommand(),
+ createRemoveAllCommand(),
+ createRemoveByPrefixCommand(),
+ createRemoveCommand(),
+ createSyncTimeCommand()
+ );
+
+ for (Command c : commands) {
+ log.info(">>>>> Serialized command: [c={}, base64='{}']",
c.getClass().getSimpleName(), encodeCommand(c));
+ }
+ }
+
+ private SyncTimeCommand createSyncTimeCommand() {
+ return factory.syncTimeCommand()
+ .safeTime(safeTime())
+ .initiatorTime(initiatorTime())
+ .initiatorTerm(42)
+ .build();
+ }
+
+ private RemoveCommand createRemoveCommand() {
+ return factory.removeCommand()
+ .safeTime(safeTime())
+ .initiatorTime(initiatorTime())
+ .key(key("key1"))
+ .build();
+ }
+
+ private RemoveByPrefixCommand createRemoveByPrefixCommand() {
+ return factory.removeByPrefixCommand()
+ .safeTime(safeTime())
+ .initiatorTime(initiatorTime())
+ .prefix(key("prefix1"))
+ .build();
+ }
+
+ private RemoveAllCommand createRemoveAllCommand() {
+ return factory.removeAllCommand()
+ .safeTime(safeTime())
+ .initiatorTime(initiatorTime())
+ .keys(List.of(key("key1"), key("key2")))
+ .build();
+ }
+
+ private PutCommand createPutCommand() {
+ return factory.putCommand()
+ .safeTime(safeTime())
+ .initiatorTime(initiatorTime())
+ .key(key("key1"))
+ .value(key("val1"))
+ .build();
+ }
+
+ private PutAllCommand createPutAllCommand() {
+ return factory.putAllCommand()
+ .safeTime(safeTime())
+ .initiatorTime(initiatorTime())
+ .keys(List.of(key("key1"), key("key2")))
+ .values(List.of(key("val1"), key("val2")))
+ .build();
+ }
+
+ private MultiInvokeCommand createMultiInvokeCommand() {
+ Condition tombstonesConditions = Conditions.and(
+ Conditions.tombstone(keyAsByteArray("tombstone1")),
+ Conditions.notTombstone(keyAsByteArray("notTombstone1"))
+ );
+
+ Condition existsConditions = Conditions.or(
+ Conditions.exists(keyAsByteArray("exists1")),
+ Conditions.notExists(keyAsByteArray("notExists1")
+ ));
+
+ Condition revisionAndValueConsitions = Conditions.and(
+ Conditions.revision(keyAsByteArray("revision1")).eq(1L),
+ Conditions.value(keyAsByteArray("value1")).ne(keyBytes("val1"))
+ );
+
+ Condition complexCondition = Conditions.and(
+ tombstonesConditions,
+ Conditions.or(existsConditions, revisionAndValueConsitions)
+ );
+
+ return factory.multiInvokeCommand()
+ .safeTime(safeTime())
+ .initiatorTime(initiatorTime())
+ .id(commandId())
+ .iif(Statements.iif(
+ complexCondition,
+ Operations.ops(Operations.put(keyAsByteArray("put1"),
keyBytes("val1"))).yield(true),
+ Operations.ops(Operations.noop()).yield(false)
+ ))
+ .build();
+ }
+
+ private InvokeCommand createInvokeCommand() {
+ return factory.invokeCommand()
+ .safeTime(safeTime())
+ .initiatorTime(initiatorTime())
+ .id(commandId())
+ .condition(Conditions.exists(keyAsByteArray("exists1")))
+ .success(List.of(Operations.put(keyAsByteArray("put1"),
keyBytes("val1"))))
+ .failure(List.of(Operations.remove(keyAsByteArray("remove1"))))
+ .build();
+ }
+
+ private GetRangeCommand createGetRangeCommand() {
+ return factory.getRangeCommand()
+ .keyFrom(key("keyFrom1"))
+ .keyTo(key("keyTo1"))
+ .batchSize(100)
+ .includeTombstones(true)
+ .previousKey(keyBytes("previousKey1"))
+ .revUpperBound(42)
+ .build();
+ }
+
+ private GetPrefixCommand createGetPrefixCommand() {
+ return factory.getPrefixCommand()
+ .prefix(key("prefix1"))
+ .batchSize(100)
+ .includeTombstones(true)
+ .previousKey(keyBytes("previousKey1"))
+ .revUpperBound(42)
+ .build();
+ }
+
+ private GetCurrentRevisionsCommand createGetCurrentRevisionsCommand() {
+ return factory.getCurrentRevisionsCommand()
+ .build();
+ }
+
+ private GetCommand createGetCommand() {
+ return factory.getCommand()
+ .revision(42)
+ .key(key("key1"))
+ .build();
+ }
+
+ private GetChecksumCommand createGetChecksumCommand() {
+ return factory.getChecksumCommand()
+ .revision(42)
+ .build();
+ }
+
+ private GetAllCommand createGetAllCommand() {
+ return factory.getAllCommand()
+ .revision(42)
+ .keys(List.of(key("key1"), key("key2")))
+ .build();
+ }
+
+ private EvictIdempotentCommandsCacheCommand
createEvictIdempotentCommandsCacheCommand() {
+ return factory.evictIdempotentCommandsCacheCommand()
+ .safeTime(safeTime())
+ .initiatorTime(initiatorTime())
+ .evictionTimestamp(HybridTimestamp.hybridTimestamp(100))
+ .build();
+ }
+
+ private CompactionCommand createCompactionCommand() {
+ return factory.compactionCommand()
+ .safeTime(safeTime())
+ .initiatorTime(initiatorTime())
+ .compactionRevision(42)
+ .build();
+ }
+
+ private byte[] serializeCommand(Command c) {
+ return marshaller.marshall(c);
+ }
+
+ private String encodeCommand(Command c) {
+ return Base64.getEncoder().encodeToString(serializeCommand(c));
+ }
+}