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


##########
core/src/main/java/org/apache/gravitino/listener/AsyncQueueListener.java:
##########
@@ -97,13 +98,22 @@ public void start() {
 
   @Override
   public void stop() {
-    Preconditions.checkState(!stopped.get(), asyncQueueListenerName + " had 
already stopped");
-    stopped.compareAndSet(false, true);
-    asyncProcessor.interrupt();
+    // Stop can be called more than once, e.g. by the server shutdown hook 
after an explicit stop.
+    if (!stopped.compareAndSet(false, true)) {
+      LOG.debug("{} had already stopped", asyncQueueListenerName);
+      return;
+    }
     try {
+      // Give the dispatcher thread time to drain the queued events. It exits 
by itself once the
+      // queue is empty, and is interrupted if it does not finish in time.
       asyncProcessor.join(dispatcherJoinSeconds * 1000L);
+      if (asyncProcessor.isAlive()) {
+        asyncProcessor.interrupt();
+        asyncProcessor.join(dispatcherJoinSeconds * 1000L);
+      }
     } catch (InterruptedException e) {
       LOG.warn("{} interrupt async processor failed.", asyncQueueListenerName, 
e);
+      Thread.currentThread().interrupt();
     }
     eventListeners.forEach(listenerPlugin -> listenerPlugin.stop());
   }

Review Comment:
   After the second `join`, the dispatcher thread may still be alive. In that 
case, `eventListeners.forEach(...stop)` can race with the dispatcher still 
invoking `onPreEvent/onPostEvent` on those listeners. Consider re-checking 
`asyncProcessor.isAlive()` after the second join and handling it explicitly 
(e.g., log a warning and delay/stabilize shutdown semantics, or loop with a 
bounded total wait) so inner listeners are not stopped while they may still be 
in use.



##########
core/src/test/java/org/apache/gravitino/audit/DummyAuditWriter.java:
##########
@@ -19,14 +19,15 @@
 
 package org.apache.gravitino.audit;
 
-import java.util.LinkedList;
+import java.util.List;
 import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
 import lombok.Getter;
 
 public class DummyAuditWriter implements AuditLogWriter {
   private Formatter formatter;
 
-  @Getter private final LinkedList<DummyAuditLog> auditLogs = new 
LinkedList<>();
+  @Getter private final List<DummyAuditLog> auditLogs = new 
CopyOnWriteArrayList<>();

Review Comment:
   `CopyOnWriteArrayList` copies the backing array on every write; with async 
audit writes and tests pushing large counts (e.g., thousands), this can add 
unnecessary overhead. Since this is a test utility primarily appending logs, a 
concurrent append-friendly structure (e.g., `ConcurrentLinkedQueue`) or a 
synchronized `ArrayList` can provide thread-safety with lower write 
amplification; pick based on whether indexed access is actually required.



##########
core/src/main/java/org/apache/gravitino/listener/AsyncQueueListener.java:
##########
@@ -120,7 +130,14 @@ public List<EventListenerPlugin> getEventListeners() {
   private void processEvents() {
     while (!Thread.currentThread().isInterrupted()) {
       try {
-        BaseEvent baseEvent = queue.take();
+        BaseEvent baseEvent = queue.poll(POLL_INTERVAL_MS, 
TimeUnit.MILLISECONDS);
+        if (baseEvent == null) {
+          if (stopped.get()) {
+            // The listener is stopped and all the queued events are processed.
+            break;
+          }
+          continue;
+        }

Review Comment:
   Switching from `queue.take()` to a fixed-interval `poll()` introduces 
periodic wakeups even when idle (up to 10x/sec per async listener), which can 
add avoidable CPU overhead at scale. A common alternative is to keep `take()` 
and use a shutdown signal (e.g., a sentinel/poison event) or interrupt-only 
shutdown while still ensuring drain semantics.



##########
core/src/test/java/org/apache/gravitino/listener/TestEventListenerManager.java:
##########
@@ -270,6 +332,116 @@ void testTransformPreEvent() {
     Assertions.assertEquals(2, ((CountingPreEvent) 
transformedEvent.get()).count());
   }
 
+  @Test
+  void testAddEventListenerHonorsMode() {
+    EventListenerManager eventListenerManager = new EventListenerManager();
+    eventListenerManager.init(new HashMap<>());
+
+    ThreadRecordingEventListener syncListener = new 
ThreadRecordingEventListener(Mode.SYNC);
+    ThreadRecordingEventListener isolatedListener =
+        new ThreadRecordingEventListener(Mode.ASYNC_ISOLATED);
+    ThreadRecordingEventListener sharedListener1 =
+        new ThreadRecordingEventListener(Mode.ASYNC_SHARED);
+    ThreadRecordingEventListener sharedListener2 =
+        new ThreadRecordingEventListener(Mode.ASYNC_SHARED);
+    eventListenerManager.addEventListener("sync", syncListener);
+    eventListenerManager.addEventListener("isolated", isolatedListener);
+    eventListenerManager.addEventListener("shared1", sharedListener1);
+    eventListenerManager.addEventListener("shared2", sharedListener2);
+    eventListenerManager.start();
+    EventBus eventBus = eventListenerManager.createEventBus();
+    try {
+      List<EventListenerPlugin> listeners = eventBus.getEventListeners();
+      Assertions.assertEquals(3, listeners.size());
+      Assertions.assertEquals(
+          1, 
listeners.stream().filter(EventListenerPluginWrapper.class::isInstance).count());
+      Set<Integer> queueSizes =
+          listeners.stream()
+              .filter(AsyncQueueListener.class::isInstance)
+              .map(listener -> ((AsyncQueueListener) 
listener).getEventListeners().size())
+              .collect(Collectors.toSet());
+      Assertions.assertEquals(ImmutableSet.of(1, 2), queueSizes);
+
+      eventBus.dispatchEvent(DUMMY_POST_EVENT_INSTANCE);
+
+      Assertions.assertEquals(
+          ImmutableList.of(Thread.currentThread().getName()), 
syncListener.awaitPostEventThreads());
+      Assertions.assertEquals(
+          ImmutableList.of("async-queue-listener-isolated"),
+          isolatedListener.awaitPostEventThreads());
+      Assertions.assertEquals(
+          ImmutableList.of("async-queue-listener-default"),
+          sharedListener1.awaitPostEventThreads());
+      Assertions.assertEquals(
+          ImmutableList.of("async-queue-listener-default"),
+          sharedListener2.awaitPostEventThreads());

Review Comment:
   These assertions depend on exact dispatcher thread names, making the test 
brittle to internal naming changes (or future suffixing/uniqueness changes). 
Consider asserting on a stable property instead (e.g., thread name starts with 
`async-queue-listener-` and contains the queue name), so the test continues to 
validate dispatch mode without coupling to exact naming.



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