This is an automated email from the ASF dual-hosted git repository.

tkhurana pushed a commit to branch PHOENIX-7562-feature-new
in repository https://gitbox.apache.org/repos/asf/phoenix.git


The following commit(s) were added to refs/heads/PHOENIX-7562-feature-new by 
this push:
     new daaf9b7caf PHOENIX-8029 Make the replication mode-switch notification 
non-blocking (#2641)
daaf9b7caf is described below

commit daaf9b7caf309fb31d8dfabf9f6489fb579e7e0e
Author: tkhurana <[email protected]>
AuthorDate: Tue Oct 27 11:24:24 2026 -0700

    PHOENIX-8029 Make the replication mode-switch notification non-blocking 
(#2641)
    
    checkAndSetModeAndNotify woke the event handler with a blocking sync(),
    which waits up to the sync timeout and can abort the RS. It runs on the
    ZK cache-event thread (HA state listeners) and the forwarder thread.
    
    Publish a non-blocking MODE_CHECK marker with tryNext() instead, like the
    rotation SWAP marker. The handler applies the switch on the marker only
    when no syncs are pending and the current batch is empty; otherwise it
    applies at the next sync point. The mode-switch tail of
    processPendingSyncs is extracted into checkModeSwitch, which skips the
    switch once the group is closed, so close()'s drain does not apply
    switches made by other threads.
---
 .../phoenix/replication/ReplicationLogGroup.java   |  68 ++++++++++++--
 .../replication/ReplicationLogGroupTest.java       | 104 +++++++++++++++++++++
 2 files changed, 163 insertions(+), 9 deletions(-)

diff --git 
a/phoenix-core-server/src/main/java/org/apache/phoenix/replication/ReplicationLogGroup.java
 
b/phoenix-core-server/src/main/java/org/apache/phoenix/replication/ReplicationLogGroup.java
index 8f05dd0c4d..b1751f11c5 100644
--- 
a/phoenix-core-server/src/main/java/org/apache/phoenix/replication/ReplicationLogGroup.java
+++ 
b/phoenix-core-server/src/main/java/org/apache/phoenix/replication/ReplicationLogGroup.java
@@ -20,6 +20,7 @@ package org.apache.phoenix.replication;
 import static org.apache.hadoop.hbase.HConstants.DEFAULT_ZK_SESSION_TIMEOUT;
 import static org.apache.hadoop.hbase.HConstants.ZK_SESSION_TIMEOUT;
 import static 
org.apache.phoenix.replication.ReplicationLogGroup.LogEvent.EVENT_TYPE_DATA;
+import static 
org.apache.phoenix.replication.ReplicationLogGroup.LogEvent.EVENT_TYPE_MODE_CHECK;
 import static 
org.apache.phoenix.replication.ReplicationLogGroup.LogEvent.EVENT_TYPE_SWAP;
 import static 
org.apache.phoenix.replication.ReplicationLogGroup.LogEvent.EVENT_TYPE_SYNC;
 import static 
org.apache.phoenix.replication.ReplicationLogGroup.ReplicationMode.INIT;
@@ -418,6 +419,7 @@ public class ReplicationLogGroup {
     public static final byte EVENT_TYPE_DATA = 0;
     public static final byte EVENT_TYPE_SYNC = 1;
     public static final byte EVENT_TYPE_SWAP = 2;
+    public static final byte EVENT_TYPE_MODE_CHECK = 3;
 
     public void setValues(int type, Record record, CompletableFuture<Void> 
syncFuture) {
       this.type = type;
@@ -434,6 +436,8 @@ public class ReplicationLogGroup {
           return "SYNC";
         case EVENT_TYPE_SWAP:
           return "SWAP";
+        case EVENT_TYPE_MODE_CHECK:
+          return "MODE_CHECK";
         default:
           return "UNKNOWN(" + type + ")";
       }
@@ -799,18 +803,15 @@ public class ReplicationLogGroup {
   }
 
   /**
-   * Conditionally switch replication mode and, if it changed, notify the 
event handler with a sync
-   * so the disruptor picks up the new mode.
+   * Conditionally switch replication mode and, if it changed, wake the event 
handler so it applies
+   * the new mode even when idle. Never blocks, aborts or throws: it runs on 
the ZK cache-event
+   * thread (state listeners) and the forwarder thread.
    */
   protected boolean checkAndSetModeAndNotify(ReplicationMode expectedMode,
     ReplicationMode newMode) {
     boolean switched = checkAndSetMode(expectedMode, newMode);
     if (switched) {
-      try {
-        sync();
-      } catch (IOException e) {
-        LOG.info("Failed to notify event handler for {}", this, e);
-      }
+      publishModeCheckEvent();
     }
     return switched;
   }
@@ -1044,6 +1045,30 @@ public class ReplicationLogGroup {
     }
   }
 
+  /**
+   * Publish a non-blocking mode-check marker so an idle consumer applies a 
mode switch promptly.
+   * Uses {@code tryNext()} like {@link #publishSwapEvent()}: when the ring 
buffer is full, the
+   * consumer is busy and the switch applies at the next sync point. Skipped 
once closed:
+   * {@code checkModeSwitch} ignores the switch then anyway.
+   */
+  protected void publishModeCheckEvent() {
+    if (isClosed()) {
+      LOG.info("HAGroup {} closed, skipping mode check event publish", this);
+      return;
+    }
+    try {
+      long sequence = ringBuffer.tryNext();
+      try {
+        LogEvent event = ringBuffer.get(sequence);
+        event.setValues(EVENT_TYPE_MODE_CHECK, null, null);
+      } finally {
+        ringBuffer.publish(sequence);
+      }
+    } catch (InsufficientCapacityException e) {
+      LOG.info("HAGroup {} ring buffer full, skipping mode check event 
publish", this);
+    }
+  }
+
   /**
    * Check if this ReplicationLogGroup is closed.
    * @return true if closed, false otherwise
@@ -1347,6 +1372,12 @@ public class ReplicationLogGroup {
     return eventHandler.getCurrentModeImpl().getReplicationLog();
   }
 
+  /** Returns the ring buffer's free slots; equals its size once every 
published event is done. */
+  @VisibleForTesting
+  long getRingBufferRemainingCapacity() {
+    return ringBuffer.remainingCapacity();
+  }
+
   protected void setHAGroupStatusToStoreAndForward() throws Exception {
     try {
       haGroupStoreManager.setHAGroupStatusToStoreAndForward(haGroupName);
@@ -1510,6 +1541,18 @@ public class ReplicationLogGroup {
       // Doing the mode check on sync points makes the implementation more 
robust
       // since we can guarantee that all unsynced appends have been flushed to 
the
       // replication log before we switch the replication mode
+      checkModeSwitch(sequence);
+    }
+
+    /**
+     * Applies a mode switched on the replication group by another thread, if 
any. Callers must
+     * guarantee that no unsynced appends are outstanding. Once closed, 
switches made by other
+     * threads are ignored, so the drain stays in the current mode unless a 
write fails.
+     */
+    private void checkModeSwitch(long sequence) throws IOException {
+      if (isClosed()) {
+        return;
+      }
       ReplicationMode newMode = getMode();
       if (newMode != currentModeImpl.getMode()) {
         // some other thread switched the mode on the replication group
@@ -1596,8 +1639,8 @@ public class ReplicationLogGroup {
     }
 
     /**
-     * Processes a single event from the Disruptor ring buffer. This method 
handles both data and
-     * sync events.
+     * Processes a single event from the Disruptor ring buffer: data and sync 
events, and the SWAP
+     * and MODE_CHECK wake-up markers.
      * <p>
      * For data events, it:
      * <ol>
@@ -1649,6 +1692,13 @@ public class ReplicationLogGroup {
             // closed before the reader's round buffer expires. No append/sync 
to perform.
             currentModeImpl.getReplicationLog().checkAndReplaceWriter(true);
             break;
+          case EVENT_TYPE_MODE_CHECK:
+            // Wake-up marker from checkAndSetModeAndNotify. Switch now only 
if nothing is
+            // outstanding; otherwise the switch applies at the next sync 
point.
+            if (pendingSyncs.isEmpty() && 
currentModeImpl.log.getCurrentBatch().isEmpty()) {
+              checkModeSwitch(sequence);
+            }
+            break;
           default:
             throw new UnsupportedOperationException("Unknown event type: " + 
event.type);
         }
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/replication/ReplicationLogGroupTest.java
 
b/phoenix-core/src/test/java/org/apache/phoenix/replication/ReplicationLogGroupTest.java
index 22bc3f1d8e..25dbf4b45f 100644
--- 
a/phoenix-core/src/test/java/org/apache/phoenix/replication/ReplicationLogGroupTest.java
+++ 
b/phoenix-core/src/test/java/org/apache/phoenix/replication/ReplicationLogGroupTest.java
@@ -20,6 +20,7 @@ package org.apache.phoenix.replication;
 import static java.lang.Thread.sleep;
 import static 
org.apache.phoenix.replication.ReplicationLogGroup.ReplicationMode.STORE_AND_FORWARD;
 import static 
org.apache.phoenix.replication.ReplicationLogGroup.ReplicationMode.SYNC;
+import static 
org.apache.phoenix.replication.ReplicationLogGroup.ReplicationMode.SYNC_AND_FORWARD;
 import static 
org.apache.phoenix.replication.ReplicationShardDirectoryManager.PHOENIX_REPLICATION_ROUND_DURATION_SECONDS_KEY;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -55,6 +56,7 @@ import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
 import java.util.concurrent.SynchronousQueue;
 import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.ThreadPoolExecutor;
@@ -888,6 +890,108 @@ public class ReplicationLogGroupTest extends 
ReplicationLogBaseTest {
     }
   }
 
+  private void waitForHandlerMode(ReplicationLogGroup.ReplicationMode expected)
+    throws InterruptedException {
+    long deadline = System.currentTimeMillis() + 5000;
+    while (
+      logGroup.eventHandler.getCurrentModeImpl().getMode() != expected
+        && System.currentTimeMillis() < deadline
+    ) {
+      sleep(20);
+    }
+    assertEquals(expected, 
logGroup.eventHandler.getCurrentModeImpl().getMode());
+  }
+
+  /**
+   * An idle group applies a mode switch from checkAndSetModeAndNotify on its 
own: the mode-check
+   * marker wakes the consumer, and no sync() is issued.
+   */
+  @Test
+  public void testModeSwitchNotifyAppliesWhenIdle() throws Exception {
+    assertEquals(SYNC, logGroup.eventHandler.getCurrentModeImpl().getMode());
+    assertTrue(logGroup.checkAndSetModeAndNotify(SYNC, SYNC_AND_FORWARD));
+    waitForHandlerMode(SYNC_AND_FORWARD);
+    verify(logGroup, never()).sync();
+  }
+
+  private void waitForRingBufferDrained() throws InterruptedException {
+    long deadline = System.currentTimeMillis() + 5000;
+    while (
+      logGroup.getRingBufferRemainingCapacity() < TEST_RINGBUFFER_SIZE
+        && System.currentTimeMillis() < deadline
+    ) {
+      sleep(20);
+    }
+    assertEquals(TEST_RINGBUFFER_SIZE, 
logGroup.getRingBufferRemainingCapacity());
+  }
+
+  /**
+   * A mode-check marker that finds unsynced appends in the current batch must 
not switch the mode:
+   * the switch waits for the next sync, so the outstanding appends are synced 
in the old mode.
+   */
+  @Test
+  public void testModeSwitchNotifyDefersWhileAppendsUnsynced() throws 
Exception {
+    logGroup.append("TBLMODEDEFER", 1L, LogFileTestUtil.newPut("row", 1, 1));
+    waitForRingBufferDrained();
+
+    assertTrue(logGroup.checkAndSetModeAndNotify(SYNC, SYNC_AND_FORWARD));
+    waitForRingBufferDrained();
+    assertEquals("The marker must not switch with unsynced appends 
outstanding", SYNC,
+      logGroup.eventHandler.getCurrentModeImpl().getMode());
+
+    logGroup.sync();
+    waitForHandlerMode(SYNC_AND_FORWARD);
+  }
+
+  /**
+   * While the consumer is busy with an outstanding sync, 
checkAndSetModeAndNotify returns at once.
+   */
+  @Test
+  public void testModeSwitchNotifyDoesNotBlockOnBusyConsumer() throws 
Exception {
+    // Headroom so a slow CI run cannot time out the held sync, and no 
scheduled rotation can swap
+    // the spied writer mid-test.
+    conf.setLong(ReplicationLogGroup.REPLICATION_LOG_SYNC_TIMEOUT_KEY, 
30_000L);
+    conf.setInt(PHOENIX_REPLICATION_ROUND_DURATION_SECONDS_KEY, 3600);
+    recreateLogGroup();
+    final Mutation put = LogFileTestUtil.newPut("row", 1, 1);
+    LogFileWriter writer = logGroup.getActiveLog().getWriter();
+    CountDownLatch release = new CountDownLatch(1);
+    doAnswer(invocation -> {
+      release.await();
+      return invocation.callRealMethod();
+    }).when(writer).sync();
+
+    logGroup.append("TBLMODEBUSY", 1L, put);
+    ExecutorService executor = Executors.newSingleThreadExecutor();
+    try {
+      Future<?> syncResult = executor.submit(() -> {
+        logGroup.sync();
+        return null;
+      });
+      verify(writer, timeout(5000)).sync();
+
+      long start = System.nanoTime();
+      assertTrue(logGroup.checkAndSetModeAndNotify(SYNC, SYNC_AND_FORWARD));
+      assertTrue("checkAndSetModeAndNotify must not block on a busy consumer",
+        TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start) < 500);
+
+      release.countDown();
+      syncResult.get(5, TimeUnit.SECONDS);
+      waitForHandlerMode(SYNC_AND_FORWARD);
+    } finally {
+      release.countDown();
+      executor.shutdownNow();
+    }
+  }
+
+  /** On a fatal group, checkAndSetModeAndNotify returns normally and never 
aborts. */
+  @Test
+  public void testModeSwitchNotifyOnFatalGroupDoesNotAbort() throws Exception {
+    logGroup.eventHandler.setFatalException(new IOException("Simulated 
fatal"));
+    assertTrue(logGroup.checkAndSetModeAndNotify(SYNC, SYNC_AND_FORWARD));
+    verify(logGroup, never()).abort(anyString(), any());
+  }
+
   /**
    * Tests rotation during an in-flight batch. When a pending writer is staged 
by the rotation task
    * while appends are in flight, the sync's apply() drains the pending 
writer, replays the 5

Reply via email to