Skip to content

HDDS-14846. Implement ScmCodecFactory without reflection#9952

Merged
szetszwo merged 7 commits intoapache:masterfrom
Russole:HDDS-14846
Mar 23, 2026
Merged

HDDS-14846. Implement ScmCodecFactory without reflection#9952
szetszwo merged 7 commits intoapache:masterfrom
Russole:HDDS-14846

Conversation

@Russole
Copy link
Contributor

@Russole Russole commented Mar 19, 2026

What changes were proposed in this pull request?

  • Replace Class parameter in ScmCodecFactory.getCodec with class name (String)
  • Move type resolution handling from getCodec to ClassResolver
  • Remove superclass/interface scanning and remove reflection-based lookup
  • Update all call sites to switch to class name for codec resolution
  • Refactor ScmCodecFactory into a singleton (Convert codec map to non-static and add a static instance)

What is the link to the Apache JIRA

https://issues.apache.org/jira/browse/HDDS-14846

How was this patch tested?

@Russole Russole changed the title HDDS-14846. HDDS-14846. Implement ScmCodecFactory without reflection. Mar 19, 2026
@Russole Russole changed the title HDDS-14846. Implement ScmCodecFactory without reflection. HDDS-14846. Implement ScmCodecFactory without reflection Mar 19, 2026
@Russole
Copy link
Contributor Author

Russole commented Mar 19, 2026

Hi @szetszwo, could you please help review this PR when you have time? Thanks!

@adoroszlai adoroszlai requested a review from szetszwo March 20, 2026 07:21
@szetszwo
Copy link
Contributor

@Russole , When reviewing this, I found that the change can be simplified after remove the type parameter from ScmCodec.deserialize(..); filed HDDS-14875.

Copy link
Contributor

@szetszwo szetszwo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Russole , thanks for working on this! Please see the comments inlined and also https://issues.apache.org/jira/secure/attachment/13081315/9952_review.patch

BTW, please wait for HDDS-14875 -- 9952_review.patch requires it.

Comment on lines +107 to +109
final String parameterType = parameterTypes[paramCounter++].getName();
argBuilder.setType(parameterType);
argBuilder.setValue(ScmCodecFactory.getInstance().getCodec(parameterType)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the class is available, we should getCodec using the class.

BTW, could you also remove this args list? We should just use the proto builder.

  public Message encode() throws InvalidProtocolBufferException {
    final SCMRatisRequestProto.Builder requestProtoBuilder =
        SCMRatisRequestProto.newBuilder();
    requestProtoBuilder.setType(type);

    final Method.Builder methodBuilder = Method.newBuilder();
    methodBuilder.setName(operation);

    for (int i = 0; i < parameterTypes.length; i++) {
      // Set actual method parameter type, not actual argument type.
      // This is done to avoid MethodNotFoundException in case if argument is
      // subclass type, where as method is defined with super class type.
      final Class<?> parameterType = parameterTypes[i];
      methodBuilder.addArgs(MethodArgument.newBuilder()
          .setType(parameterType.getName())
          .setValue(FACTORY.getCodec(parameterType).serialize(arguments[i]))
          .build());
    }
    requestProtoBuilder.setMethod(methodBuilder.build());
    final SCMRatisRequestProto requestProto = requestProtoBuilder.build();
    return Message.valueOf(requestProto.toByteString());
  }

Comment on lines +154 to +157
final String argumentType = argument.getType();
final Class<?> clazz = ReflectionUtil.getClass(argument.getType());
parameterTypes[paramCounter++] = clazz;
args.add(ScmCodecFactory.getCodec(clazz)
args.add(ScmCodecFactory.getInstance().getCodec(argumentType)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use factory to resolve the class:

      final String argumentType = argument.getType();
      final Class<?> clazz = FACTORY.resolve(argumentType);
      parameterTypes[paramCounter++] = clazz;
      args.add(FACTORY.getCodec(clazz).deserialize(argument.getValue()));

Comment on lines +75 to +76
final Class<?> type = result.getClass();
final String typeName = type.getName();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pass the declaration type as a parameter:

  public static Message encode(Object result, Class<?> type) throws InvalidProtocolBufferException {
    if (result == null) {
      return Message.EMPTY;
    }

    final SCMRatisResponseProto response = SCMRatisResponseProto.newBuilder()
        .setType(type.getName())
        .setValue(FACTORY.getCodec(type).serialize(result))
        .build();
    return Message.valueOf(UnsafeByteOperations.unsafeWrap(response.toByteString().asReadOnlyByteBuffer()));
  }

final Class<?> type = ReflectionUtil.getClass(responseProto.getType());
return new SCMRatisResponse(ScmCodecFactory.getCodec(type)
final String typeName = responseProto.getType();
final Class<?> type = ReflectionUtil.getClass(typeName);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use the factory to resolve it.

    final Class<?> type = FACTORY.resolve(responseProto.getType());
    return new SCMRatisResponse(FACTORY.getCodec(type).deserialize(responseProto.getValue()));

Comment on lines +93 to +97
public static ScmCodec getCodec(Class<?> type)
public ScmCodec getCodec(String className)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep the parameter declared using Class.

For the className case, we should resolve the class first.

@Russole
Copy link
Contributor Author

Russole commented Mar 21, 2026

Thanks @szetszwo for the review. I will update the patch after HDDS-14875 is merged.

Copy link
Contributor

@szetszwo szetszwo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 the change looks good.

Comment on lines -97 to -98
classes.addAll(ClassUtils.getAllSuperclasses(type));
classes.addAll(ClassUtils.getAllInterfaces(type));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think removing supertypes causes the following error:

org.apache.hadoop.hdds.scm.exceptions.SCMException: org.apache.ratis.thirdparty.com.google.protobuf.InvalidProtocolBufferException: Codec not found for class java.util.ArrayList
	at org.apache.hadoop.hdds.scm.ha.SCMHAInvocationHandler.translateException(SCMHAInvocationHandler.java:163)
	at org.apache.hadoop.hdds.scm.ha.SCMHAInvocationHandler.invokeRatis(SCMHAInvocationHandler.java:113)
	at org.apache.hadoop.hdds.scm.ha.SCMHAInvocationHandler.invoke(SCMHAInvocationHandler.java:72)
	at jdk.proxy2/jdk.proxy2.$Proxy41.addTransactionsToDB(Unknown Source)
	at org.apache.hadoop.hdds.scm.block.SCMDeletedBlockTransactionStatusManager.addTransactions(SCMDeletedBlockTransactionStatusManager.java:470)
	at org.apache.hadoop.hdds.scm.block.DeletedBlockLogImpl.addTransactions(DeletedBlockLogImpl.java:253)
	at org.apache.hadoop.ozone.shell.TestDeletedBlocksTxnShell.testGetDeletedBlockSummarySubcommand(TestDeletedBlocksTxnShell.java:178)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:387)
	at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1312)
	at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1843)
	at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1808)
	at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:188)
Caused by: org.apache.ratis.thirdparty.com.google.protobuf.InvalidProtocolBufferException: Codec not found for class java.util.ArrayList
	at org.apache.hadoop.hdds.scm.ha.io.ScmCodecFactory.getCodec(ScmCodecFactory.java:107)
	at org.apache.hadoop.hdds.scm.ha.SCMRatisRequest.encode(SCMRatisRequest.java:107)
	at org.apache.hadoop.hdds.scm.ha.SCMRatisServerImpl.submitRequest(SCMRatisServerImpl.java:238)
	at org.apache.hadoop.hdds.scm.ha.SCMHAInvocationHandler.invokeRatisServer(SCMHAInvocationHandler.java:121)
	at org.apache.hadoop.hdds.scm.ha.SCMHAInvocationHandler.invokeRatis(SCMHAInvocationHandler.java:110)

Maybe we can fix it by changing ArrayList parameter to List in DeletedBlockLogStateManager and its implementation:

public interface DeletedBlockLogStateManager {
@Replicate
void addTransactionsToDB(ArrayList<DeletedBlocksTransaction> txs,
DeletedBlocksTransactionSummary summary) throws IOException;
@Replicate
void addTransactionsToDB(ArrayList<DeletedBlocksTransaction> txs) throws IOException;
@Replicate
void removeTransactionsFromDB(ArrayList<Long> txIDs, DeletedBlocksTransactionSummary summary)
throws IOException;
@Replicate
void removeTransactionsFromDB(ArrayList<Long> txIDs)
throws IOException;
@Deprecated
@Replicate
void increaseRetryCountOfTransactionInDB(ArrayList<Long> txIDs)
throws IOException;
@Deprecated
@Replicate
int resetRetryCountOfTransactionInDB(ArrayList<Long> txIDs)
throws IOException;

Declaring as List is good practice in any case.

Copy link
Contributor

@adoroszlai adoroszlai Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

@szetszwo szetszwo Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adoroszlai , thanks for debugging the test failures!

We should put ArrayList to the factory for backward compatibility.

+++ b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/ha/io/ScmCodecFactory.java
@@ -75,7 +75,9 @@ public final class ScmCodecFactory {

     // Must be the last one
     final ClassResolver resolver = new ClassResolver(codecs.keySet());
-    codecs.put(List.class, new ScmListCodec(resolver));
+    final ScmListCodec listCodec = new ScmListCodec(resolver);
+    codecs.put(List.class, listCodec);
+    codecs.put(ArrayList.class, listCodec);
   }

@Russole
Copy link
Contributor Author

Russole commented Mar 22, 2026

Thanks @adoroszlai and @szetszwo for the suggestion.

I think the issue is that the method parameter types and the codec-resolved types were previously treated as the same, which can lead to method lookup failures during decoding and codec lookup failures during encoding.

So I separated the two concerns:

  • use the original declared type (via getClass) for method signature reconstruction
  • use the resolved type for codec serialization/deserialization

I also added a getClass(String) helper in ScmCodecFactory to reconstruct the declared parameter types from the serialized type names:

  public Class<?> getClass(String className)
      throws InvalidProtocolBufferException {
    try {
      return Class.forName(className);
    } catch (ClassNotFoundException e) {
      throw new InvalidProtocolBufferException(
          "Class not found for " + className, e);
    }
  }

CI tests have passed: https://github.com/Russole/ozone/actions/runs/23406070564

@Russole Russole requested a review from szetszwo March 23, 2026 02:17
Copy link
Contributor

@szetszwo szetszwo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Russole , thanks for the update!

Please see the comments inlined and also https://issues.apache.org/jira/secure/attachment/13081331/9952_review2.patch

throw new InvalidProtocolBufferException("Codec not found for " + resolved);
}

public Class<?> getClass(String className)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add one more map to avoid calling Class.forName(..) multiple times for the same class.

  public Class<?> getClass(String className) throws InvalidProtocolBufferException {
    final Class<?> found = classes.get(className);
    if (found != null) {
      return found;
    }

    final Class<?> clazz;
    try {
      clazz = Class.forName(className);
    } catch (ClassNotFoundException e) {
      throw new InvalidProtocolBufferException("Class not found for " + className, e);
    }
    classes.put(className, clazz);
    return clazz;
  }

Comment on lines +155 to +159
try {
clazz = Class.forName(className);
} catch (ClassNotFoundException e) {
throw new InvalidProtocolBufferException("Class not found for " + className, e);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reuse the getClass method

        clazz = getInstance().getClass(className);

}

public static ScmCodec getCodec(Class<?> type)
public Class<?> resolve(String className)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add one more resolve method for Class<?>.

  public Class<?> resolve(Class<?> clazz) throws InvalidProtocolBufferException {
    return resolver.get(clazz);
  }

@Russole Russole requested a review from szetszwo March 23, 2026 17:28
Copy link
Contributor

@szetszwo szetszwo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 the change looks good.

@szetszwo szetszwo merged commit 9c97ef2 into apache:master Mar 23, 2026
45 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants