Copilot commented on code in PR #13166:
URL: https://github.com/apache/gravitino/pull/13166#discussion_r4012280228


##########
spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/catalog/GravitinoCatalogManager.java:
##########
@@ -259,4 +264,27 @@ private static void closeClient(GravitinoIdentity 
identity, GravitinoClient clie
       LOG.warn("Failed to close the Gravitino client of {}.", identity, e);
     }
   }
+
+  /**
+   * Holds a cached client so that the shutdown drain and the removal listener 
can both try to close
+   * it while the underlying client is closed at most once.
+   */
+  private static class CachedClient {
+    private final GravitinoClient client;
+    private final AtomicBoolean closed = new AtomicBoolean(false);
+
+    private CachedClient(GravitinoClient client) {
+      this.client = client;
+    }
+
+    private GravitinoClient client() {
+      return client;
+    }
+
+    private void close() {
+      if (closed.compareAndSet(false, true)) {
+        client.close();
+      }
+    }
+  }
 }

Review Comment:
   The CAS is set before invoking `client.close()`. If a removal listener wins 
the CAS while `close()`'s `asMap().forEach` is holding the same entry, the 
drain returns immediately on `closed == true` without waiting for the 
listener's close to finish, so `GravitinoCatalogManager.close()` can return 
while the underlying client is still closing. Serialize this method (or 
otherwise await completion) so the synchronous shutdown path cannot lose the 
close and return early.



##########
docs/review-6c2878fb1.md:
##########
@@ -0,0 +1,51 @@
+# Review:6c2878fb1 — 跳过 EXPLICIT 修掉了双关,但换来一个漏关窗口
+
+## 一、改动概述
+
+`GravitinoCatalogManager` 的 client cache 原来在 removal listener 里无条件 
`closeClient`,而 `close()` 已经先同步遍历关过一遍,于是每个 client 被关两次,第二次由 Caffeine 派发到 
`ForkJoinPool.commonPool()`、在 `close()` 返回之后才跑。
+
+改动分两部分:生产代码让 removal listener 跳过 `RemovalCause.EXPLICIT`(只有 `close()` 会产生这个 
cause);测试把 `ClientFactory` 的 per-client `AtomicBoolean` 换成 `AtomicInteger` 
计数,`testCloseClosesEveryCachedClient` 在 `close()` 之后 drain common pool,再断言每个 
client 只关一次。

Review Comment:
   This added document describes the rejected `RemovalCause.EXPLICIT`-skipping 
implementation, but the production diff now closes on every removal and uses 
`CachedClient`'s CAS for idempotence. Keeping this text under `docs/` leaves an 
inaccurate description of the current behavior; update the whole report to the 
final implementation or remove this review artifact.



##########
spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/catalog/TestGravitinoCatalogManager.java:
##########
@@ -145,7 +147,18 @@ void testCloseClosesEveryCachedClient() {
 
     manager.close();
 
-    assertEquals(3, clientFactory.closedCount());
+    // Exact already here: no client the shutdown drain saw may outlive 
close().
+    assertEquals(
+        List.of(1, 1, 1),
+        clientFactory.closeCounts(),
+        "Shutdown must close each cached client on the calling thread");
+    // Caffeine dispatches removal listeners on the common pool, so a second 
close would land after
+    // close() returned. Draining the pool makes that visible instead of 
leaving it to timing.
+    ForkJoinPool.commonPool().awaitQuiescence(30, TimeUnit.SECONDS);

Review Comment:
   The return value of `awaitQuiescence` is ignored. If the shared common pool 
remains busy for 30 seconds, this returns `false` while a removal-listener task 
can still be pending, and the following assertion may pass before a second 
close is delivered. Use a per-test completion signal or otherwise verify that 
the removal task has completed before asserting the final counts.



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