nsivabalan commented on code in PR #20151:
URL: https://github.com/apache/hudi/pull/20151#discussion_r4279810579


##########
hudi-utilities/src/main/java/org/apache/hudi/utilities/HoodieTableHealthChecker.java:
##########
@@ -0,0 +1,327 @@
+/*
+ * 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.hudi.utilities;
+
+import org.apache.hudi.common.config.TypedProperties;
+import org.apache.hudi.common.table.HoodieTableMetaClient;
+import org.apache.hudi.common.util.StringUtils;
+import org.apache.hudi.hadoop.fs.HadoopFSUtils;
+import org.apache.hudi.utilities.health.ArchivalCadenceCheck;
+import org.apache.hudi.utilities.health.CleanerCadenceCheck;
+import org.apache.hudi.utilities.health.CompactionCadenceCheck;
+import org.apache.hudi.utilities.health.HealthCheck;
+import org.apache.hudi.utilities.health.HealthCheckContext;
+import org.apache.hudi.utilities.health.HealthCheckResult;
+import org.apache.hudi.utilities.health.HealthStatus;
+import org.apache.hudi.utilities.health.SavepointArchivalBlockCheck;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hadoop.conf.Configuration;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Reports whether a Hudi table's table services are keeping up with ingestion.
+ *
+ * <p>Intended to be run on a schedule -- daily is typical -- against tables 
an operator is
+ * responsible for. Each check answers one question about whether a service is 
progressing as
+ * expected, and the process exit code makes the overall verdict usable from a 
cron job or CI
+ * pipeline without parsing the output.
+ *
+ * <p>This tool is strictly read-only. It inspects timeline state and reports; 
it never schedules or
+ * runs a table service, and never writes to the table.
+ *
+ * <h3>Writer properties are required</h3>
+ *
+ * <p>Nearly every question this tool answers is relative to how the table is 
written. Whether
+ * fifty delta commits since the last compaction is fine or alarming depends 
on the configured
+ * trigger; whether a thousand instants in the active timeline is fine depends 
on the configured
+ * retention. Hudi has a default for each of these, so the tool could always 
produce an answer --
+ * but for a table written with non-default settings that answer would be 
wrong, and wrong in the
+ * direction that erodes trust, reporting healthy tables as unhealthy.
+ *
+ * <p>So by default a check whose governing property was not supplied reports 
{@code SKIPPED} and
+ * names the property, rather than guessing. Supply properties with {@code 
--props} or
+ * {@code --hoodie-conf}, or pass {@code --apply-all-defaults} to explicitly 
accept Hudi defaults
+ * for anything unset.
+ *
+ * <h3>Exit codes</h3>
+ * <ul>
+ *   <li>{@code 0} -- every check that ran reported healthy</li>
+ *   <li>{@code 1} -- at least one check reported unhealthy</li>
+ *   <li>{@code 2} -- bad arguments, or the table could not be read</li>
+ * </ul>
+ *
+ * <h3>Example</h3>
+ * <pre>
+ * spark-submit --master local \
+ *   --class org.apache.hudi.utilities.HoodieTableHealthChecker \
+ *   
$HUDI_DIR/packaging/hudi-utilities-bundle/target/hudi-utilities-bundle_2.12-1.3.0-SNAPSHOT.jar
 \
+ *   --base-path s3://bucket/path/to/table \
+ *   --props s3://bucket/path/to/writer.properties \
+ *   --output JSON
+ * </pre>
+ */
+@Slf4j
+public class HoodieTableHealthChecker implements Serializable {
+
+  private static final long serialVersionUID = 1L;
+
+  static final int EXIT_HEALTHY = 0;
+  static final int EXIT_UNHEALTHY = 1;
+  static final int EXIT_ERROR = 2;
+
+  private static final ObjectMapper JSON_MAPPER =
+      new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
+
+  private final Config cfg;
+  private final TypedProperties props;
+  private final HoodieTableMetaClient metaClient;
+
+  public HoodieTableHealthChecker(Configuration hadoopConf, Config cfg) {
+    this.cfg = cfg;
+    this.props = UtilHelpers.buildProperties(hadoopConf, cfg.propsFilePath, 
cfg.configs);
+    this.metaClient = HoodieTableMetaClient.builder()
+        .setConf(HadoopFSUtils.getStorageConfWithCopy(hadoopConf))
+        .setBasePath(cfg.basePath)
+        .setLoadActiveTimelineOnLoad(true)
+        .build();
+  }
+
+  /** All checks this tool knows about, in report order. */
+  static List<HealthCheck> allChecks() {
+    return Arrays.asList(
+        new CompactionCadenceCheck(),
+        new CleanerCadenceCheck(),
+        new SavepointArchivalBlockCheck(),
+        new ArchivalCadenceCheck());
+  }
+
+  /**
+   * Runs the selected checks and renders the report.
+   *
+   * @return the exit code the process should terminate with.
+   */
+  public int run() {
+    List<HealthCheck> selected = selectChecks(cfg.checks);
+    HealthCheckContext context = new HealthCheckContext(metaClient, props, 
cfg.applyAllDefaults);
+
+    List<HealthCheckResult> results = new ArrayList<>();
+    List<String> erroredChecks = new ArrayList<>();
+
+    for (HealthCheck check : selected) {
+      try {
+        results.add(check.check(context));
+      } catch (Exception e) {
+        // One check failing to reach a verdict must not hide the others. 
Record it and continue.
+        log.error("Health check '{}' failed to run for table {}", 
check.getName(), cfg.basePath, e);
+        erroredChecks.add(check.getName() + ": " + e.getMessage());
+      }
+    }
+
+    if (OutputFormat.JSON == cfg.output) {
+      System.out.println(renderJson(results, erroredChecks));
+    } else {
+      System.out.println(renderTable(results, erroredChecks));
+    }
+
+    if (!erroredChecks.isEmpty()) {
+      return EXIT_ERROR;
+    }
+    return results.stream().anyMatch(r -> r.getStatus().isUnhealthy()) ? 
EXIT_UNHEALTHY : EXIT_HEALTHY;

Review Comment:
   Agreed — UNHEALTHY now wins. The table's verdict is the more actionable of 
the two, and an errored check shouldn't mask it.
   
   Both are still reported in full in the output; only the exit code is ranked. 
And since the codes moved anyway (see the other thread), "unhealthy + errors" 
has its own slot:
   
   | Code | Meaning |
   |---|---|
   | `0` | Every check that ran reported healthy (or was skipped) |
   | `2` | Bad arguments, or the table could not be read |
   | `3` | At least one check reported unhealthy |
   | `4` | No check reported unhealthy, but at least one could not run |
   



##########
hudi-utilities/src/main/java/org/apache/hudi/utilities/HoodieTableHealthChecker.java:
##########
@@ -0,0 +1,327 @@
+/*
+ * 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.hudi.utilities;
+
+import org.apache.hudi.common.config.TypedProperties;
+import org.apache.hudi.common.table.HoodieTableMetaClient;
+import org.apache.hudi.common.util.StringUtils;
+import org.apache.hudi.hadoop.fs.HadoopFSUtils;
+import org.apache.hudi.utilities.health.ArchivalCadenceCheck;
+import org.apache.hudi.utilities.health.CleanerCadenceCheck;
+import org.apache.hudi.utilities.health.CompactionCadenceCheck;
+import org.apache.hudi.utilities.health.HealthCheck;
+import org.apache.hudi.utilities.health.HealthCheckContext;
+import org.apache.hudi.utilities.health.HealthCheckResult;
+import org.apache.hudi.utilities.health.HealthStatus;
+import org.apache.hudi.utilities.health.SavepointArchivalBlockCheck;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hadoop.conf.Configuration;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Reports whether a Hudi table's table services are keeping up with ingestion.
+ *
+ * <p>Intended to be run on a schedule -- daily is typical -- against tables 
an operator is
+ * responsible for. Each check answers one question about whether a service is 
progressing as
+ * expected, and the process exit code makes the overall verdict usable from a 
cron job or CI
+ * pipeline without parsing the output.
+ *
+ * <p>This tool is strictly read-only. It inspects timeline state and reports; 
it never schedules or
+ * runs a table service, and never writes to the table.
+ *
+ * <h3>Writer properties are required</h3>
+ *
+ * <p>Nearly every question this tool answers is relative to how the table is 
written. Whether
+ * fifty delta commits since the last compaction is fine or alarming depends 
on the configured
+ * trigger; whether a thousand instants in the active timeline is fine depends 
on the configured
+ * retention. Hudi has a default for each of these, so the tool could always 
produce an answer --
+ * but for a table written with non-default settings that answer would be 
wrong, and wrong in the
+ * direction that erodes trust, reporting healthy tables as unhealthy.
+ *
+ * <p>So by default a check whose governing property was not supplied reports 
{@code SKIPPED} and
+ * names the property, rather than guessing. Supply properties with {@code 
--props} or
+ * {@code --hoodie-conf}, or pass {@code --apply-all-defaults} to explicitly 
accept Hudi defaults
+ * for anything unset.
+ *
+ * <h3>Exit codes</h3>
+ * <ul>
+ *   <li>{@code 0} -- every check that ran reported healthy</li>
+ *   <li>{@code 1} -- at least one check reported unhealthy</li>
+ *   <li>{@code 2} -- bad arguments, or the table could not be read</li>
+ * </ul>
+ *
+ * <h3>Example</h3>
+ * <pre>
+ * spark-submit --master local \
+ *   --class org.apache.hudi.utilities.HoodieTableHealthChecker \
+ *   
$HUDI_DIR/packaging/hudi-utilities-bundle/target/hudi-utilities-bundle_2.12-1.3.0-SNAPSHOT.jar
 \
+ *   --base-path s3://bucket/path/to/table \
+ *   --props s3://bucket/path/to/writer.properties \
+ *   --output JSON
+ * </pre>
+ */
+@Slf4j
+public class HoodieTableHealthChecker implements Serializable {
+
+  private static final long serialVersionUID = 1L;
+
+  static final int EXIT_HEALTHY = 0;
+  static final int EXIT_UNHEALTHY = 1;

Review Comment:
   Good catch — that's a real trap, and the documented command hit it exactly.
   
   `1` is now unused and reserved. UNHEALTHY moves to `3`, and a check that 
could not run gets `4`:
   
   | Code | Meaning |
   |---|---|
   | `0` | Every check that ran reported healthy (or was skipped) |
   | `2` | Bad arguments, or the table could not be read |
   | `3` | At least one check reported unhealthy |
   | `4` | No check reported unhealthy, but at least one could not run |
   
   The class javadoc, the `--help` output and the README table all say why `1` 
is skipped, so nobody reintroduces it. The `java -cp` command that produced the 
`NoClassDefFoundError` is gone too — see the README thread.
   



-- 
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