yihua commented on code in PR #19575:
URL: https://github.com/apache/hudi/pull/19575#discussion_r3867084520


##########
hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/table/HoodieSparkTable.java:
##########
@@ -142,9 +146,19 @@ protected Option<HoodieTableMetadataWriter> 
getMetadataWriter(
     return Option.empty();
   }
 
+  /**
+   * Carries thread-local task state onto Hudi's writer pools. The metrics 
binding travels with the
+   * {@link TaskContext} for the same reason that does: {@code 
BoundedInMemoryExecutor} and
+   * {@code DisruptorExecutor} run on threads the task did not create. No 
unbind, since these pools are
+   * created and shut down per operation.
+   */
   @Override
   public Runnable getPreExecuteRunnable() {
     final TaskContext taskContext = TaskContext.get();
-    return () -> TaskContext$.MODULE$.setTaskContext(taskContext);
+    final Map<String, Registry> metricsBinding = 
ExecutorMetricsContext.capture();

Review Comment:
   non-blocking: Tracing the three `bind` call sites, this one can only ever 
capture an empty map — the two lookup functions bind and unbind within a single 
`call()` (and `unbind(null)` does `BOUND.remove()`), and 
`getPreExecuteRunnable` is only reached from the write stage 
(`HoodieMergeHelper`, `SparkLazyInsertIterable`, the bootstrap handlers, 
`SingleSparkJobConsistentHashingExecutionStrategy`), never from inside a 
lookup. So the net effect is that `isBound()` becomes true on every writer pool 
thread with nothing bound, which is exactly the case where 
`Registry.getRegistry(name)` now returns `NoOpRegistry` instead of the 
process-wide `LocalRegistry`. Would it be simpler to drop this hunk until 
something below the write API actually emits, rather than carrying the flip for 
a binding that is always empty?



##########
hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/client/common/HoodieSparkEngineContext.java:
##########
@@ -278,11 +279,47 @@ public String getApplicationId() {
     return javaSparkContext.sc().applicationId();
   }
 
+  /**
+   * Drops a registry from both process-wide maps. Only for tests that create 
their own SparkContexts:
+   * without it they leave accumulators bound to stopped contexts behind for 
whatever runs next in the
+   * same JVM.
+   */
+  @VisibleForTesting
+  public static void removeMetricRegistry(String tableName, String 
registryName) {
+    String prefixedName = tableName.isEmpty() ? registryName : tableName + "." 
+ registryName;
+    DISTRIBUTED_REGISTRY_MAP.remove(prefixedName);
+    Registry.REGISTRY_MAP.remove(Registry.makeKey(tableName, registryName));
+    // setRegistries also indexes under the empty table name, so evicting only 
the table-scoped key leaves
+    // the accumulator reachable under ::<table>.<registry>.
+    Registry.REGISTRY_MAP.remove(Registry.makeKey("", prefixedName));
+  }
+
   @Override
   public Registry getMetricRegistry(String tableName, String registryName) {
     final String prefixedName = tableName.isEmpty() ? registryName : tableName 
+ "." + registryName;
-    return DISTRIBUTED_REGISTRY_MAP.computeIfAbsent(prefixedName, key -> {
+    // Both maps are process-wide statics that outlive any SparkContext, so 
the staleness check and the
+    // recreation have to be atomic: otherwise one caller can evict the 
registry another caller just
+    // created, leaving two live accumulators for one metric name while 
reporting only ever reads the
+    // one still in the map.
+    return DISTRIBUTED_REGISTRY_MAP.compute(prefixedName, (key, cached) -> {
+      if (cached instanceof DistributedRegistry && ((DistributedRegistry) 
cached).isRegisteredWith(javaSparkContext)) {
+        return cached;
+      }
+      // Nothing usable cached, or the cached accumulator is bound to a 
SparkContext that is no longer
+      // live (a restart in the same JVM: shells, notebooks, Spark Connect). 
Drop the shared-map entry
+      // first, since getRegistryOfClass() would otherwise hand back that same 
stale instance.
+      final String sharedKey = Registry.makeKey(tableName, registryName);
+      Registry.REGISTRY_MAP.remove(sharedKey);

Review Comment:
   non-blocking: `removeMetricRegistry` above drops all three keys and its 
comment explains why the `::<table>.<registry>` one matters, but this eviction 
path only removes `makeKey(tableName, registryName)`. In local mode 
`setRegistries` puts the registry into the driver's own `REGISTRY_MAP` under 
`makeKey("", prefixedName)`, so after a SparkContext restart that entry still 
points at the dead accumulator while the new one is registered under the 
table-scoped key — and since both report the same prefixed names from 
`getAllMetrics`, which value survives depends on map iteration order. Worth 
adding the same `Registry.REGISTRY_MAP.remove(Registry.makeKey("", 
prefixedName))` here?



##########
hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/index/RecordIndexLookupMetrics.java:
##########
@@ -0,0 +1,102 @@
+/*
+ * 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.index;
+
+import org.apache.hudi.common.engine.HoodieEngineContext;
+import org.apache.hudi.common.metrics.NoOpRegistry;
+import org.apache.hudi.common.metrics.Registry;
+import org.apache.hudi.config.HoodieWriteConfig;
+import org.apache.hudi.metrics.DistributedRegistry;
+import org.apache.hudi.metrics.ExecutorMetricRegistry;
+import org.apache.hudi.metrics.RecordIndexMetricNames;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/** Executor-side emission for the record index lookup counters. */
+public class RecordIndexLookupMetrics {
+
+  private RecordIndexLookupMetrics() {
+  }
+
+  /**
+   * The registries a lookup task collects into, keyed by bare name. Includes 
every entry on
+   * {@link ExecutorMetricRegistry}. Delivery is by closure capture, which is 
deterministic; resolution is
+   * by name, which lets code below the write API take part without a 
signature change.
+   */
+  public static Map<String, Registry> resolveBundle(HoodieEngineContext 
context, HoodieWriteConfig config) {
+    Map<String, Registry> bundle = new HashMap<>();
+    for (ExecutorMetricRegistry metricRegistry : 
ExecutorMetricRegistry.values()) {
+      if (!metricRegistry.isEnabled(config)) {

Review Comment:
   Since the "follows `hoodie.metrics.on`" behaviour comes from the infer 
function, an explicit `hoodie.metrics.rli.lookup.enable=true` with 
`hoodie.metrics.on=false` still registers the accumulator and counts on every 
task, but `HoodieMetrics.metrics` is null so `publishToReporter` returns at its 
null guard and the counters are released unread each commit. Would it be worth 
adding `config.isMetricsOn()` to the gate here, so the Impact section's 
"nothing is collected and no accumulator is created" holds for the explicit 
setting too?



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