JoegenUSTC commented on code in PR #11846:
URL: https://github.com/apache/gravitino/pull/11846#discussion_r3577162816


##########
core/src/main/java/org/apache/gravitino/catalog/CapabilityHelpers.java:
##########
@@ -47,17 +47,56 @@
 import org.apache.gravitino.rel.partitions.Partition;
 import org.apache.gravitino.rel.partitions.Partitions;
 import org.apache.gravitino.rel.partitions.RangePartition;
+import org.apache.gravitino.utils.ThrowableFunction;
 
 public class CapabilityHelpers {
 
-  public static Capability getCapability(NameIdentifier ident, CatalogManager 
catalogManager) {
+  /**
+   * Executes {@code fn} with the {@link Capability} of the catalog identified 
by {@code ident},
+   * inside the catalog's classloader boundary. This prevents the catalog's 
{@link
+   * org.apache.gravitino.utils.IsolatedClassLoader} from being closed while 
capability methods are
+   * executing.
+   *
+   * <p>If the first attempt encounters a stale (already-closed) {@link
+   * CatalogManager.CatalogWrapper}, the wrapper is evicted from the cache and 
the call is retried
+   * once with a freshly loaded wrapper. Retry is safe because {@code fn} is 
restricted to pure
+   * normalization and never performs external I/O, so re-execution is 
idempotent. The retry is
+   * triggered only on {@link CatalogManager.CatalogWrapperClosedException}, 
which is thrown
+   * exclusively at wrapper-entry before {@code fn} has run, preventing 
accidental replay of
+   * partially-executed non-idempotent operations.
+   *
+   * <p>Callers should use this method instead of calling {@code 
capabilities()} on a raw wrapper
+   * invoke {@link Capability} methods so that the classloader lifecycle is 
properly bounded.
+   *
+   * @param ident any {@link NameIdentifier} that belongs to the target catalog
+   * @param catalogManager the catalog manager used to load the catalog
+   * @param fn function to execute with the catalog's {@link Capability}
+   * @param <R> return type
+   * @return the result of {@code fn}
+   */
+  public static <R> R withCapability(
+      NameIdentifier ident, CatalogManager catalogManager, 
ThrowableFunction<Capability, R> fn) {
     NameIdentifier catalogIdent = getCatalogIdentifier(ident);
-    CatalogManager.CatalogWrapper c = 
catalogManager.loadCatalogAndWrap(catalogIdent);
-    try {
-      return c.capabilities();
-    } catch (Exception e) {
-      throw new RuntimeException("Failed to get capabilities for catalog: " + 
catalogIdent, e);
+    CatalogManager.CatalogWrapperClosedException lastClosed = null;
+    for (int i = 0; i < 2; i++) {
+      CatalogManager.CatalogWrapper c = 
catalogManager.loadCatalogAndWrap(catalogIdent);
+      try {
+        return c.doWithCapabilityOps(fn);

Review Comment:
   Great question — adding sleep/jitter is a reasonable reflex for any retry 
loop. However, I don't think it's necessary here, for the following reasons:
   
   The purpose of sleep/jitter in retry logic is typically to back off from a 
temporarily unavailable external resource (e.g. a server under load, a network 
partition) or to spread out retry storms when many callers fail simultaneously. 
Neither condition applies here:
   
   1. **The resource is immediately available after retry.** 
`CatalogWrapperClosedException` is thrown because Caffeine evicted the wrapper 
and called `close()` on it. The retry calls `loadCatalogAndWrap` again, which 
uses Caffeine's atomic `cache.get(key, loader)` to synchronously construct a 
fresh `CatalogWrapper` and cache it. There is no waiting for external readiness 
— the new wrapper is available on the very next line.
   
   2. **No thundering herd.** Caffeine already serializes concurrent loads for 
the same key internally (similar to `ConcurrentHashMap.computeIfAbsent`), so 
even if many threads hit this path simultaneously, the catalog plugin is loaded 
only once. There is no fan-out amplification that jitter could prevent.
   
   3. **`fn` is pure normalization.** Since `fn` is now restricted to CPU-only 
name normalization with no I/O, the retry is safe and idempotent regardless of 
how quickly it fires.
   
   If rapid re-eviction were a concern (i.e. the newly loaded wrapper is 
evicted again immediately), that would indicate a Caffeine capacity/TTL 
configuration issue — sleeping a few milliseconds would not help in that case 
either.
   
   The pattern is closer to a stale-cache-entry reload than a network-level 
retry. Adding a sleep would only introduce unnecessary latency on an 
already-rare code path without improving correctness or reducing contention.
   
   That said, if you see a specific scenario where sleep/jitter would make a 
difference, happy to discuss further and reconsider.



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