rangareddy commented on code in PR #19477:
URL: https://github.com/apache/hudi/pull/19477#discussion_r3710088977


##########
hudi-common/src/main/java/org/apache/hudi/metrics/MetricsReporterFactory.java:
##########
@@ -122,6 +123,18 @@ private static MetricsReporter 
createCloudWatchReporter(HoodieMetricsConfig metr
                 + "different reporter type.",
             CLOUDWATCH_REPORTER_CLASS, 
HoodieMetricsConfig.METRICS_REPORTER_TYPE_VALUE.key()), e);
       }
+      if (e.getCause() instanceof NoSuchMethodException) {

Review Comment:
   You are right, and I reproduced it independently rather than take it on 
trust. Released `hudi-aws-bundle-1.1.0-rc1` against this branch's 
`hudi-common`, JDK 11:
   
   ```
   STEP1 Class.forName OK -> class 
org.apache.hudi.aws.metrics.cloudwatch.CloudWatchMetricsReporter
   STEP2 getConstructor THREW java.lang.NoClassDefFoundError: 
org/apache/hudi/config/metrics/HoodieMetricsConfig
           is NoSuchMethodException?                   false
           is Error (escapes catch(Exception))?        true
   ```
   
   Corroborating details I checked: `HoodieMetricsConfig` exists **only** at 
`org.apache.hudi.common.config.metrics` on master, so there is no stub at the 
old path; `ReflectionUtils.loadClass` catches exactly `InstantiationException | 
IllegalAccessException | InvocationTargetException | NoSuchMethodException`, so 
an `Error` walks straight past it; and `3cc8fd128b40` is indeed the move.
   
   So the branch fired on duplicate jars and never on clean skew, exactly as 
you said.
   
   Fixed by adding a `catch (NoClassDefFoundError e)` ahead of the 
`HoodieException` catch, with its own message that quotes `e.getMessage()` — 
the vanished type is, as you say, the strongest evidence available. There are 
now three branches with three different remedies rather than two.



##########
hudi-common/src/main/java/org/apache/hudi/metrics/MetricsReporterFactory.java:
##########
@@ -122,6 +123,18 @@ private static MetricsReporter 
createCloudWatchReporter(HoodieMetricsConfig metr
                 + "different reporter type.",
             CLOUDWATCH_REPORTER_CLASS, 
HoodieMetricsConfig.METRICS_REPORTER_TYPE_VALUE.key()), e);
       }
+      if (e.getCause() instanceof NoSuchMethodException) {
+        // The class resolved, so hudi-aws is present; only the constructor 
did not match. That means the
+        // jar providing it was built against a different Hudi version, which 
is a mismatch rather than
+        // something missing, and the two need different remedies.
+        throw new HoodieException(String.format(
+            "Cannot report metrics to CloudWatch: %s was found on the 
classpath but has no (%s, %s) "
+                + "constructor. The jar providing it was most likely built 
against a different Hudi "
+                + "version. Use a hudi-aws-bundle whose version matches the 
Hudi bundle in use, or set %s "
+                + "to a different reporter type.",

Review Comment:
   Applied, and the #12902 point is the part that convinced me. That report had 
`hudi-spark3.5-bundle_2.12:0.15.0` and `hudi-aws-bundle:0.15.0` — already 
matching — and 0.15.0 declared the requested constructor, so "use a matching 
hudi-aws-bundle" was advice that could not have helped. My own closing comment 
on that issue said "pin every Hudi artifact to the same version", which is the 
stale-duplicate remedy, so the message and the diagnosis disagreed.
   
   Took your wording essentially as written. The `NoSuchMethodException` branch 
now says the classpath is supplying a stale or duplicate copy and points at a 
leftover `hudi-common` or `hudi-client-common`, and the clean-skew advice moved 
to the new `NoClassDefFoundError` branch where it is actually true.
   
   The `different Hudi version` assertion moved with it: the mismatch test now 
asserts `stale or duplicate copy`, and a new test asserts `built against a 
different Hudi version` against the `NoClassDefFoundError` branch.



##########
hudi-common/src/main/java/org/apache/hudi/metrics/MetricsReporterFactory.java:
##########
@@ -122,6 +123,18 @@ private static MetricsReporter 
createCloudWatchReporter(HoodieMetricsConfig metr
                 + "different reporter type.",
             CLOUDWATCH_REPORTER_CLASS, 
HoodieMetricsConfig.METRICS_REPORTER_TYPE_VALUE.key()), e);
       }
+      if (e.getCause() instanceof NoSuchMethodException) {
+        // The class resolved, so hudi-aws is present; only the constructor 
did not match. That means the
+        // jar providing it was built against a different Hudi version, which 
is a mismatch rather than
+        // something missing, and the two need different remedies.
+        throw new HoodieException(String.format(
+            "Cannot report metrics to CloudWatch: %s was found on the 
classpath but has no (%s, %s) "
+                + "constructor. The jar providing it was most likely built 
against a different Hudi "
+                + "version. Use a hudi-aws-bundle whose version matches the 
Hudi bundle in use, or set %s "
+                + "to a different reporter type.",
+            CLOUDWATCH_REPORTER_CLASS, 
HoodieMetricsConfig.class.getSimpleName(),
+            MetricRegistry.class.getSimpleName(), 
HoodieMetricsConfig.METRICS_REPORTER_TYPE_VALUE.key()), e);

Review Comment:
   Applied — `getName()` for both. Your point is confirmed by javap on the 
released bundle:
   
   ```
   declared:  (org.apache.hudi.config.metrics.HoodieMetricsConfig,
               org.apache.hudi.com.codahale.metrics.MetricRegistry)
   requested: (org.apache.hudi.common.config.metrics.HoodieMetricsConfig,
               com.codahale.metrics.MetricRegistry)
   ```
   
   Both differ, and for two independent reasons — the package move and the 
shaded-codahale relocation — yet under simple names both render 
`(HoodieMetricsConfig, MetricRegistry)`. A user running javap would have seen 
an apparently matching constructor and concluded the error was wrong. I put 
that reasoning in a comment above the format string so it does not get 
"simplified" back later, and the mismatch test now asserts both fully qualified 
names are present.



##########
hudi-common/src/main/java/org/apache/hudi/metrics/MetricsReporterFactory.java:
##########
@@ -106,8 +106,9 @@ public static Option<MetricsReporter> 
createReporter(HoodieMetricsConfig metrics
 
   /**
    * The CloudWatch reporter ships in the optional {@code hudi-aws} module and 
so is loaded reflectively.
-   * Not every engine bundle shades that module, in which case class loading 
fails without pointing at a
-   * remedy. Translate that into an actionable error.
+   * Reflection reports its two distinct failures - the module is absent, or 
the module is present but was
+   * built against a different Hudi version - as the same opaque {@link 
HoodieException}. Translate each into
+   * an error that names its own remedy, and leave anything else untouched.

Review Comment:
   Applied, with one adjustment: it is now three rather than two, since the 
`NoClassDefFoundError` branch from your other comment lands here too.
   
   ```java
   * Reflection collapses several unrelated failures into the same opaque 
{@link HoodieException}. Three of
   * them have distinct remedies - the module is absent, it was built against a 
Hudi that has since moved a
   * class, or the classpath carries a stale duplicate - so translate those 
three, and leave every other
   * failure untouched.
   ```
   
   "several ... three of them" also survives the next branch, as you intended.



##########
hudi-common/src/test/java/org/apache/hudi/metrics/TestMetricsReporterFactory.java:
##########
@@ -137,22 +139,51 @@ void 
metricsReporterFactoryRewritesClassNotFoundIntoAnActionableMessage() {
   }

Review Comment:
   Applied to both. You are right that every string they asserted also appears 
in the mismatch message, so neither was discriminating:
   
   ```java
   assertTrue(message.contains("was not found on the classpath"),
       () -> "A missing class must not be reported as a constructor mismatch, 
but was: " + message);
   ```
   
   on the classpath-based test, and the same phrase asserted on the mocked one. 
With that, merging or reordering the branches can no longer ship green.



##########
hudi-common/src/test/java/org/apache/hudi/metrics/TestMetricsReporterFactory.java:
##########
@@ -137,22 +139,51 @@ void 
metricsReporterFactoryRewritesClassNotFoundIntoAnActionableMessage() {
   }
 
   /**
-   * The other direction, and the branch most likely to regress: a failure 
that is not a missing class must
-   * pass through untouched, so an unrelated instantiation error is never 
rewritten into "add hudi-aws-bundle".
+   * A missing constructor means hudi-aws resolved but was built against a 
different Hudi version, which is
+   * a mismatch rather than something absent. Reflection reports it as the 
same opaque HoodieException as a
+   * missing class, and the bare "Unable to instantiate class" that resulted 
took a maintainer reading the
+   * buried NoSuchMethodException to explain (#12902).
    */
   @Test
-  void metricsReporterFactoryLeavesNonClassNotFoundFailuresUntouched() {
+  void metricsReporterFactoryExplainsAConstructorMismatch() {
     
when(metricsConfig.getMetricsReporterType()).thenReturn(MetricsReporterType.CLOUDWATCH);
     try (MockedStatic<ReflectionUtils> mockedStatic = 
Mockito.mockStatic(ReflectionUtils.class)) {

Review Comment:
   Added, and it is the comment I am most glad you made — a suite that only 
asserts the shapes the production code assumes is exactly how the 
`NoClassDefFoundError` gap stayed invisible.
   
   `metricsReporterFactoryTranslatesARealReflectionFailure` drives the real 
`ReflectionUtils.loadClass` with a fixture whose only public constructor takes 
a `String`, and asserts both the translated message and that the real 
`NoSuchMethodException` is chained.
   
   Your constraint about the FQCN was right, and it is why the translation now 
takes the class name as a parameter:
   
   ```java
   @VisibleForTesting
   static MetricsReporter createCloudWatchReporter(String reporterClass, 
HoodieMetricsConfig metricsConfig,
                                                  MetricRegistry registry)
   ```
   
   The no-arg overload keeps `CLOUDWATCH_REPORTER_CLASS` for production 
callers, so
   
`metricsReporterFactoryShouldExplainHowToEnableCloudWatchWhenHudiAwsIsMissing` 
still depends on that name being absent from the test classpath and still 
passes.



##########
hudi-common/src/test/java/org/apache/hudi/metrics/TestMetricsReporterFactory.java:
##########
@@ -137,22 +139,51 @@ void 
metricsReporterFactoryRewritesClassNotFoundIntoAnActionableMessage() {
   }
 
   /**
-   * The other direction, and the branch most likely to regress: a failure 
that is not a missing class must
-   * pass through untouched, so an unrelated instantiation error is never 
rewritten into "add hudi-aws-bundle".
+   * A missing constructor means hudi-aws resolved but was built against a 
different Hudi version, which is
+   * a mismatch rather than something absent. Reflection reports it as the 
same opaque HoodieException as a
+   * missing class, and the bare "Unable to instantiate class" that resulted 
took a maintainer reading the
+   * buried NoSuchMethodException to explain (#12902).
    */
   @Test
-  void metricsReporterFactoryLeavesNonClassNotFoundFailuresUntouched() {
+  void metricsReporterFactoryExplainsAConstructorMismatch() {
     
when(metricsConfig.getMetricsReporterType()).thenReturn(MetricsReporterType.CLOUDWATCH);
     try (MockedStatic<ReflectionUtils> mockedStatic = 
Mockito.mockStatic(ReflectionUtils.class)) {
       mockedStatic.when(() -> ReflectionUtils.loadClass(
           eq(MetricsReporterFactory.CLOUDWATCH_REPORTER_CLASS), 
any(Class[].class), eq(metricsConfig), eq(registry)))
           .thenThrow(new HoodieException("Unable to instantiate class " + 
MetricsReporterFactory.CLOUDWATCH_REPORTER_CLASS,
               new NoSuchMethodException("<init>")));
 
+      HoodieException exception = assertThrows(HoodieException.class,
+          () -> MetricsReporterFactory.createReporter(metricsConfig, 
registry));

Review Comment:
   Applied. You are right that deleting `, e` left all three assertions green — 
the buried exception was the one thing worth pinning and nothing pinned it.
   
   ```java
   assertEquals(NoSuchMethodException.class, 
exception.getCause().getCause().getClass(),
       "The original NoSuchMethodException must stay in the chain - it is the 
evidence #12902 needed");
   ```
   
   The new `NoClassDefFoundError` test pins its cause the same way, and the 
non-mocked test asserts the chain on a real reflection failure rather than a 
stubbed one.



##########
hudi-common/src/test/java/org/apache/hudi/metrics/TestMetricsReporterFactory.java:
##########
@@ -137,22 +139,51 @@ void 
metricsReporterFactoryRewritesClassNotFoundIntoAnActionableMessage() {
   }
 
   /**
-   * The other direction, and the branch most likely to regress: a failure 
that is not a missing class must
-   * pass through untouched, so an unrelated instantiation error is never 
rewritten into "add hudi-aws-bundle".
+   * A missing constructor means hudi-aws resolved but was built against a 
different Hudi version, which is
+   * a mismatch rather than something absent. Reflection reports it as the 
same opaque HoodieException as a
+   * missing class, and the bare "Unable to instantiate class" that resulted 
took a maintainer reading the
+   * buried NoSuchMethodException to explain (#12902).
    */
   @Test
-  void metricsReporterFactoryLeavesNonClassNotFoundFailuresUntouched() {
+  void metricsReporterFactoryExplainsAConstructorMismatch() {
     
when(metricsConfig.getMetricsReporterType()).thenReturn(MetricsReporterType.CLOUDWATCH);
     try (MockedStatic<ReflectionUtils> mockedStatic = 
Mockito.mockStatic(ReflectionUtils.class)) {
       mockedStatic.when(() -> ReflectionUtils.loadClass(
           eq(MetricsReporterFactory.CLOUDWATCH_REPORTER_CLASS), 
any(Class[].class), eq(metricsConfig), eq(registry)))
           .thenThrow(new HoodieException("Unable to instantiate class " + 
MetricsReporterFactory.CLOUDWATCH_REPORTER_CLASS,
               new NoSuchMethodException("<init>")));
 
+      HoodieException exception = assertThrows(HoodieException.class,
+          () -> MetricsReporterFactory.createReporter(metricsConfig, 
registry));
+      String message = exception.getMessage();
+      assertTrue(message.contains("constructor"),
+          () -> "The failure should say the constructor did not match, but 
was: " + message);
+      assertTrue(message.contains("different Hudi version"),
+          () -> "The failure should name version skew as the likely cause, but 
was: " + message);

Review Comment:
   Applied, including replacing `different Hudi version` with the phrase this 
branch actually lands on. The mismatch test now asserts: `constructor`, `stale 
or duplicate copy`, the reporter class, the config key, both fully qualified 
parameter types, `was found on the classpath but`, and the chained 
`NoSuchMethodException`.
   
   The two assertions you flagged as reword-proof — reporter class and config 
key — are the ones I would have missed.



##########
hudi-common/src/test/java/org/apache/hudi/metrics/TestMetricsReporterFactory.java:
##########
@@ -137,22 +139,51 @@ void 
metricsReporterFactoryRewritesClassNotFoundIntoAnActionableMessage() {
   }
 
   /**
-   * The other direction, and the branch most likely to regress: a failure 
that is not a missing class must
-   * pass through untouched, so an unrelated instantiation error is never 
rewritten into "add hudi-aws-bundle".
+   * A missing constructor means hudi-aws resolved but was built against a 
different Hudi version, which is
+   * a mismatch rather than something absent. Reflection reports it as the 
same opaque HoodieException as a
+   * missing class, and the bare "Unable to instantiate class" that resulted 
took a maintainer reading the
+   * buried NoSuchMethodException to explain (#12902).
    */
   @Test
-  void metricsReporterFactoryLeavesNonClassNotFoundFailuresUntouched() {
+  void metricsReporterFactoryExplainsAConstructorMismatch() {
     
when(metricsConfig.getMetricsReporterType()).thenReturn(MetricsReporterType.CLOUDWATCH);
     try (MockedStatic<ReflectionUtils> mockedStatic = 
Mockito.mockStatic(ReflectionUtils.class)) {
       mockedStatic.when(() -> ReflectionUtils.loadClass(
           eq(MetricsReporterFactory.CLOUDWATCH_REPORTER_CLASS), 
any(Class[].class), eq(metricsConfig), eq(registry)))
           .thenThrow(new HoodieException("Unable to instantiate class " + 
MetricsReporterFactory.CLOUDWATCH_REPORTER_CLASS,
               new NoSuchMethodException("<init>")));
 
+      HoodieException exception = assertThrows(HoodieException.class,
+          () -> MetricsReporterFactory.createReporter(metricsConfig, 
registry));
+      String message = exception.getMessage();
+      assertTrue(message.contains("constructor"),
+          () -> "The failure should say the constructor did not match, but 
was: " + message);
+      assertTrue(message.contains("different Hudi version"),
+          () -> "The failure should name version skew as the likely cause, but 
was: " + message);
+      assertFalse(message.contains("was not found on the classpath"),
+          () -> "A class that resolved must not be reported as missing, but 
was: " + message);

Review Comment:
   Applied — positive assertion on this branch's own phrase, and the now-unused 
`assertFalse` import is gone (checkstyle flagged it on the first run, exactly 
as you predicted):
   
   ```java
   assertTrue(message.contains("was found on the classpath but"),
       () -> "A class that resolved must not be reported as missing, but was: " 
+ message);
   ```
   
   The point that a vacuous `assertFalse` never fails, so nothing tells you it 
stopped discriminating, is the part worth remembering.



##########
hudi-common/src/test/java/org/apache/hudi/metrics/TestMetricsReporterFactory.java:
##########
@@ -137,22 +139,51 @@ void 
metricsReporterFactoryRewritesClassNotFoundIntoAnActionableMessage() {
   }
 
   /**
-   * The other direction, and the branch most likely to regress: a failure 
that is not a missing class must
-   * pass through untouched, so an unrelated instantiation error is never 
rewritten into "add hudi-aws-bundle".
+   * A missing constructor means hudi-aws resolved but was built against a 
different Hudi version, which is
+   * a mismatch rather than something absent. Reflection reports it as the 
same opaque HoodieException as a
+   * missing class, and the bare "Unable to instantiate class" that resulted 
took a maintainer reading the
+   * buried NoSuchMethodException to explain (#12902).
    */
   @Test
-  void metricsReporterFactoryLeavesNonClassNotFoundFailuresUntouched() {
+  void metricsReporterFactoryExplainsAConstructorMismatch() {
     
when(metricsConfig.getMetricsReporterType()).thenReturn(MetricsReporterType.CLOUDWATCH);
     try (MockedStatic<ReflectionUtils> mockedStatic = 
Mockito.mockStatic(ReflectionUtils.class)) {
       mockedStatic.when(() -> ReflectionUtils.loadClass(
           eq(MetricsReporterFactory.CLOUDWATCH_REPORTER_CLASS), 
any(Class[].class), eq(metricsConfig), eq(registry)))
           .thenThrow(new HoodieException("Unable to instantiate class " + 
MetricsReporterFactory.CLOUDWATCH_REPORTER_CLASS,
               new NoSuchMethodException("<init>")));
 
+      HoodieException exception = assertThrows(HoodieException.class,
+          () -> MetricsReporterFactory.createReporter(metricsConfig, 
registry));
+      String message = exception.getMessage();
+      assertTrue(message.contains("constructor"),
+          () -> "The failure should say the constructor did not match, but 
was: " + message);
+      assertTrue(message.contains("different Hudi version"),
+          () -> "The failure should name version skew as the likely cause, but 
was: " + message);
+      assertFalse(message.contains("was not found on the classpath"),
+          () -> "A class that resolved must not be reported as missing, but 
was: " + message);
+    }
+  }
+
+  /**
+   * The other direction, and the branch most likely to regress: a failure 
that is neither a missing class
+   * nor a missing constructor must pass through untouched, so an error raised 
by the reporter's own
+   * constructor is never rewritten into a classpath diagnosis.
+   */
+  @Test
+  void metricsReporterFactoryLeavesOtherFailuresUntouched() {
+    
when(metricsConfig.getMetricsReporterType()).thenReturn(MetricsReporterType.CLOUDWATCH);
+    try (MockedStatic<ReflectionUtils> mockedStatic = 
Mockito.mockStatic(ReflectionUtils.class)) {

Review Comment:
   Taken, and it did compose with the cause assertion as you expected — 
`captureCloudWatchFailure(Throwable)` now owns the stub and returns the caught 
exception, so each test asserts on it directly:
   
   ```java
   HoodieException exception = captureCloudWatchFailure(
       new HoodieException("Unable to instantiate class " + 
CLOUDWATCH_REPORTER_CLASS,
           new NoSuchMethodException("<init>")));
   ```
   
   It also took the `NoClassDefFoundError` case for free, since that is just a 
different `Throwable` argument.
   
   Left them as separate `@Test` methods rather than a `@ParameterizedTest`, 
for the reason you gave — they assert structurally different things.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to