This is an automated email from the ASF dual-hosted git repository.

adoroszlai pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new 2a05dc0860a HDDS-15783. Handle empty lists in ScmListCodec 
deserialization (#10690)
2a05dc0860a is described below

commit 2a05dc0860a39d28d97959cd3b84d91a5759eac6
Author: Priyesh Karatha <[email protected]>
AuthorDate: Fri Jul 10 16:16:20 2026 +0530

    HDDS-15783. Handle empty lists in ScmListCodec deserialization (#10690)
---
 .../apache/hadoop/hdds/scm/ha/io/ScmListCodec.java |  6 ++
 .../hadoop/hdds/scm/ha/io/TestScmListCodec.java    | 69 ++++++++++++++++++++++
 2 files changed, 75 insertions(+)

diff --git 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/ha/io/ScmListCodec.java
 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/ha/io/ScmListCodec.java
index 36c0531b812..0276c758b3e 100644
--- 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/ha/io/ScmListCodec.java
+++ 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/ha/io/ScmListCodec.java
@@ -69,6 +69,12 @@ public Object deserialize(ByteString value) throws 
InvalidProtocolBufferExceptio
           "Missing ListArgument.type: " + argument);
     }
 
+    // Empty list was serialized with type=Object.class.getName() as a 
sentinel.
+    // Skip element-type resolution — there are no elements to decode.
+    if (argument.getValueCount() == 0) {
+      return new ArrayList<>();
+    }
+
     final Class<?> elementClass = resolver.get(argument.getType());
 
     final ScmCodec<?> elementCodec = 
ScmCodecFactory.getInstance().getCodec(elementClass);
diff --git 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/ha/io/TestScmListCodec.java
 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/ha/io/TestScmListCodec.java
index f7230bcd2f4..bec07f239b5 100644
--- 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/ha/io/TestScmListCodec.java
+++ 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/ha/io/TestScmListCodec.java
@@ -17,10 +17,14 @@
 
 package org.apache.hadoop.hdds.scm.ha.io;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
 import org.apache.hadoop.hdds.protocol.proto.SCMRatisProtocol;
 import org.apache.ratis.thirdparty.com.google.protobuf.ByteString;
 import 
org.apache.ratis.thirdparty.com.google.protobuf.InvalidProtocolBufferException;
@@ -49,4 +53,69 @@ public void testListDecodeMissingTypeShouldFail() throws 
Exception {
 
     assertTrue(ex.getMessage().contains("Missing ListArgument.type"));
   }
+
+  /**
+   * An empty list serialized with the Object.class sentinel must round-trip
+   * cleanly without triggering "Failed to resolve java.lang.Object".
+   */
+  @Test
+  public void testEmptyListRoundTrip() throws Exception {
+    ScmListCodec codec = new ScmListCodec(
+        new ScmCodecFactory.ClassResolver(Collections.emptyList()));
+
+    List<?> result = (List<?>) codec.deserialize(codec.serialize(new 
ArrayList<>()));
+
+    assertEquals(0, result.size());
+  }
+
+  /**
+   * The EMPTY_LIST sentinel (type=java.lang.Object, no values) stored in an
+   * existing Ratis log must deserialize successfully.
+   */
+  @Test
+  public void testEmptyListSentinelDeserialization() throws Exception {
+    SCMRatisProtocol.ListArgument sentinel =
+        SCMRatisProtocol.ListArgument.newBuilder()
+            .setType(Object.class.getName())
+            // no values
+            .build();
+
+    ScmListCodec codec = new ScmListCodec(
+        new ScmCodecFactory.ClassResolver(Collections.emptyList()));
+
+    List<?> result = (List<?>) codec.deserialize(sentinel.toByteString());
+
+    assertEquals(0, result.size());
+  }
+
+  /**
+   * Deserialized empty lists must be concrete {@link ArrayList} instances.
+   * Generated invokers (e.g. DeletedBlockLogStateManagerInvoker) cast the
+   * decoded argument directly to {@code ArrayList}; returning an unmodifiable
+   * or fixed-size list would cause a ClassCastException during Ratis log
+   * replay even though the list is logically empty.
+   */
+  @Test
+  public void testEmptyListDeserializedAsArrayList() throws Exception {
+    ScmListCodec codec = new ScmListCodec(
+        new ScmCodecFactory.ClassResolver(Collections.emptyList()));
+
+    // Round-trip path: serialize an empty list then deserialize it.
+    Object roundTrip = codec.deserialize(codec.serialize(new ArrayList<>()));
+    assertInstanceOf(ArrayList.class, roundTrip,
+        "round-trip empty list must be an ArrayList, not " + 
roundTrip.getClass());
+
+    // Sentinel path: the exact bytes that older logs contain.
+    SCMRatisProtocol.ListArgument sentinel =
+        SCMRatisProtocol.ListArgument.newBuilder()
+            .setType(Object.class.getName())
+            .build();
+    Object fromSentinel = codec.deserialize(sentinel.toByteString());
+    assertInstanceOf(ArrayList.class, fromSentinel,
+        "sentinel empty list must be an ArrayList, not " + 
fromSentinel.getClass());
+
+    // Verify the cast that invokers actually perform does not throw.
+    ArrayList<?> cast = (ArrayList<?>) roundTrip;
+    assertEquals(0, cast.size());
+  }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to