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 c9ea66c177 PHOENIX-8030 Close replication log groups after the regions 
close on RegionServer stop (#2642)
c9ea66c177 is described below

commit c9ea66c1779ed1969518242767b419b11db9196d
Author: tkhurana <[email protected]>
AuthorDate: Wed Oct 28 16:32:01 2026 -0700

    PHOENIX-8030 Close replication log groups after the regions close on 
RegionServer stop (#2642)
    
    PhoenixRegionServerEndpoint.stop() ran ReplicationLogGroup.closeAll() in
    preStop, before HBase closes the regions. A live write already committed
    to the WAL could then fail replication with IOException("Closed"); with a
    clean region close the WAL is never replayed, so the edit was never
    shipped.
    
    The endpoint's stop now closes the groups only if the server has no open
    IndexRegionObserver region, e.g. after the regions were moved away before
    a graceful stop (ReplicationLogGroup.closeAllIfNoOpenRegions). Otherwise
    the close is deferred: IndexRegionObserver counts its open regions per
    server (postOpen/postClose), and when the last one closes while the
    server is stopped, stopping or aborted, ReplicationLogGroup.regionClosed()
    closes that server's groups. HBase calls postClose after the region close
    has drained in-flight writes, including their replication sync, so every
    committed write is shipped and the writers close cleanly with a trailer.
    A failed open runs postClose, possibly without postOpen, so a per-region
    flag keeps the count from being undercounted. A count that reaches zero
    in normal operation (regions moved away) leaves the groups open.
    
    A kill skips the region close, so testWALRestore still closes the killed
    RS's groups by hand. testGracefulRegionServerStopClosesLogGroups holds a
    write after its WAL commit across a stop and checks that the stopping
    RS's own log carries it; it fails with the old preStop close.
    testRegionServerStopAfterUnloadClosesLogGroups moves the regions away
    first and checks that the stop closes the groups.
---
 .../coprocessor/PhoenixRegionServerEndpoint.java   |  35 ++--
 .../phoenix/hbase/index/IndexRegionObserver.java   |  25 +++
 .../phoenix/replication/ReplicationLogGroup.java   |  56 ++++-
 .../phoenix/replication/ReplicationLogGroupIT.java | 231 +++++++++++++++++++++
 .../replication/ReplicationLogGroupTest.java       | 135 ++++++++++++
 5 files changed, 465 insertions(+), 17 deletions(-)

diff --git 
a/phoenix-core-server/src/main/java/org/apache/phoenix/coprocessor/PhoenixRegionServerEndpoint.java
 
b/phoenix-core-server/src/main/java/org/apache/phoenix/coprocessor/PhoenixRegionServerEndpoint.java
index d271ec5fa0..9aae123630 100644
--- 
a/phoenix-core-server/src/main/java/org/apache/phoenix/coprocessor/PhoenixRegionServerEndpoint.java
+++ 
b/phoenix-core-server/src/main/java/org/apache/phoenix/coprocessor/PhoenixRegionServerEndpoint.java
@@ -108,21 +108,26 @@ public class PhoenixRegionServerEndpoint extends
 
   @Override
   public void stop(CoprocessorEnvironment env) throws IOException {
-    // Stop replication log replay
-    ReplicationLogReplayService.getInstance(conf).stop();
-    // Close all ReplicationLogGroup instances belonging to this server
-    if (serverName != null) {
-      ReplicationLogGroup.closeAll(serverName);
-    }
-    RegionServerCoprocessor.super.stop(env);
-    if (uncoveredIndexThreadPool != null) {
-      uncoveredIndexThreadPool
-        .stop("PhoenixRegionServerEndpoint is stopping. Shutting down 
uncovered index threadpool.");
-    }
-    ServerUtil.ConnectionFactory.shutdown();
-    // Stop prewarming executor
-    if (prewarmExecutor != null) {
-      prewarmExecutor.shutdownNow();
+    try {
+      // Stop replication log replay
+      ReplicationLogReplayService.getInstance(conf).stop();
+      RegionServerCoprocessor.super.stop(env);
+      if (uncoveredIndexThreadPool != null) {
+        uncoveredIndexThreadPool.stop(
+          "PhoenixRegionServerEndpoint is stopping. Shutting down uncovered 
index threadpool.");
+      }
+      ServerUtil.ConnectionFactory.shutdown();
+      // Stop prewarming executor
+      if (prewarmExecutor != null) {
+        prewarmExecutor.shutdownNow();
+      }
+    } finally {
+      // On a graceful stop this runs in preStop, before HBase closes the 
regions, so close the
+      // groups only if no IndexRegionObserver region is open; otherwise the 
last region close
+      // does it.
+      if (serverName != null) {
+        ReplicationLogGroup.closeAllIfNoOpenRegions(serverName);
+      }
     }
   }
 
diff --git 
a/phoenix-core-server/src/main/java/org/apache/phoenix/hbase/index/IndexRegionObserver.java
 
b/phoenix-core-server/src/main/java/org/apache/phoenix/hbase/index/IndexRegionObserver.java
index a11b5ec144..6cbd8e7a54 100644
--- 
a/phoenix-core-server/src/main/java/org/apache/phoenix/hbase/index/IndexRegionObserver.java
+++ 
b/phoenix-core-server/src/main/java/org/apache/phoenix/hbase/index/IndexRegionObserver.java
@@ -68,6 +68,7 @@ import org.apache.hadoop.hbase.CoprocessorEnvironment;
 import org.apache.hadoop.hbase.DoNotRetryIOException;
 import org.apache.hadoop.hbase.HConstants;
 import org.apache.hadoop.hbase.KeyValue;
+import org.apache.hadoop.hbase.Server;
 import org.apache.hadoop.hbase.TableName;
 import org.apache.hadoop.hbase.client.Delete;
 import org.apache.hadoop.hbase.client.Durability;
@@ -622,6 +623,8 @@ public class IndexRegionObserver implements 
RegionCoprocessor, RegionObserver {
 
   private boolean stopped;
   private boolean disabled;
+  // Whether postOpen counted this region; postClose decrements only then.
+  private boolean countedOpen;
   private long slowIndexPrepareThreshold;
   private long slowPreIncrementThreshold;
   private int rowLockWaitDuration;
@@ -823,6 +826,28 @@ public class IndexRegionObserver implements 
RegionCoprocessor, RegionObserver {
     }
   }
 
+  @Override
+  public void postOpen(ObserverContext<RegionCoprocessorEnvironment> c) {
+    if (this.disabled) {
+      return;
+    }
+    // Count open regions so the server's replication log groups close only 
after its last region
+    // has drained (see ReplicationLogGroup.regionClosed).
+    ReplicationLogGroup.regionOpened(c.getEnvironment().getServerName());
+    this.countedOpen = true;
+  }
+
+  @Override
+  public void postClose(ObserverContext<RegionCoprocessorEnvironment> c, 
boolean abortRequested) {
+    // A failed open runs postClose, possibly without postOpen; only undo what 
postOpen counted.
+    if (!this.countedOpen) {
+      return;
+    }
+    this.countedOpen = false;
+    ReplicationLogGroup.regionClosed(c.getEnvironment().getServerName(),
+      this.abortable instanceof Server ? (Server) this.abortable : null);
+  }
+
   /**
    * We use an Increment to serialize the ON DUPLICATE KEY clause so that the 
HBase plumbing sets up
    * the necessary locks and mvcc to allow an atomic update. The Increment is 
not a real increment,
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 a738066e72..5d469f0ea9 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
@@ -46,6 +46,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.EnumSet;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -65,6 +66,7 @@ import org.apache.hadoop.fs.FileSystem;
 import org.apache.hadoop.fs.Path;
 import org.apache.hadoop.hbase.Abortable;
 import org.apache.hadoop.hbase.Cell;
+import org.apache.hadoop.hbase.Server;
 import org.apache.hadoop.hbase.ServerName;
 import org.apache.hadoop.hbase.client.Mutation;
 import org.apache.phoenix.coprocessorclient.BaseScannerRegionObserverConstants;
@@ -241,6 +243,9 @@ public class ReplicationLogGroup {
   protected static final ConcurrentHashMap<String, ReplicationLogGroup> 
INSTANCES =
     new ConcurrentHashMap<>();
 
+  /** Open IndexRegionObserver regions per server, keyed by serverIdentity 
(see regionClosed). */
+  private static final Map<String, Integer> OPEN_REGIONS = new HashMap<>();
+
   /**
    * What a write to this group should do with the replication log.
    */
@@ -1216,10 +1221,57 @@ public class ReplicationLogGroup {
     LOG.info("HAGroup {} closed", this);
   }
 
+  /** Records that an IndexRegionObserver region opened on the given server. */
+  public static void regionOpened(ServerName serverName) {
+    synchronized (OPEN_REGIONS) {
+      OPEN_REGIONS.merge(serverIdentity(serverName), 1, Integer::sum);
+    }
+  }
+
   /**
-   * Close all ReplicationLogGroup instances belonging to the given server. 
Called during
-   * RegionServer shutdown.
+   * Records that an IndexRegionObserver region closed on the given server, 
and closes the server's
+   * groups when that was its last such region and the server is stopped, 
stopping or aborted. A
+   * graceful stop sets isStopped before the regions close; cluster shutdown 
sets isStopping. HBase
+   * calls postClose after the close write lock is taken, so every batch on 
the region, including
+   * its replication sync, has finished. Reaching zero in normal operation 
(regions moved away)
+   * leaves the groups open; {@link #closeAllIfNoOpenRegions} closes them on 
stop.
    */
+  public static void regionClosed(ServerName serverName, Server server) {
+    String identity = serverIdentity(serverName);
+    synchronized (OPEN_REGIONS) {
+      Integer count = OPEN_REGIONS.get(identity);
+      if (count == null) {
+        return;
+      }
+      if (count > 1) {
+        OPEN_REGIONS.put(identity, count - 1);
+        return;
+      }
+      OPEN_REGIONS.remove(identity);
+      if (server != null && (server.isStopped() || server.isStopping() || 
server.isAborted())) {
+        closeAll(serverName);
+      }
+    }
+  }
+
+  /**
+   * Closes the server's groups now if none of its IndexRegionObserver regions 
is open, for example
+   * after the regions were moved away before a graceful stop. Otherwise the 
close is left to the
+   * last region's {@link #regionClosed}.
+   */
+  public static void closeAllIfNoOpenRegions(ServerName serverName) {
+    synchronized (OPEN_REGIONS) {
+      Integer count = OPEN_REGIONS.get(serverIdentity(serverName));
+      if (count == null) {
+        closeAll(serverName);
+      } else {
+        LOG.info("Deferring ReplicationLogGroup close for {} until its {} open 
regions close",
+          serverName, count);
+      }
+    }
+  }
+
+  /** Closes all ReplicationLogGroup instances belonging to the given server. 
*/
   public static void closeAll(ServerName serverName) {
     LOG.info("Closing all ReplicationLogGroup instances for server {}, total 
count={}", serverName,
       INSTANCES.size());
diff --git 
a/phoenix-core/src/it/java/org/apache/phoenix/replication/ReplicationLogGroupIT.java
 
b/phoenix-core/src/it/java/org/apache/phoenix/replication/ReplicationLogGroupIT.java
index 727af83a00..d354c9f797 100644
--- 
a/phoenix-core/src/it/java/org/apache/phoenix/replication/ReplicationLogGroupIT.java
+++ 
b/phoenix-core/src/it/java/org/apache/phoenix/replication/ReplicationLogGroupIT.java
@@ -36,26 +36,39 @@ import java.sql.ResultSet;
 import java.sql.Statement;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Properties;
 import java.util.Random;
+import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.stream.Collectors;
 import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.LocatedFileStatus;
 import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.fs.RemoteIterator;
 import org.apache.hadoop.hbase.Cell;
 import org.apache.hadoop.hbase.CellUtil;
 import org.apache.hadoop.hbase.HBaseTestingUtility;
 import org.apache.hadoop.hbase.MiniHBaseCluster;
 import org.apache.hadoop.hbase.ServerName;
 import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.client.Admin;
 import org.apache.hadoop.hbase.client.Mutation;
+import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
+import org.apache.hadoop.hbase.coprocessor.ObserverContext;
+import org.apache.hadoop.hbase.coprocessor.RegionCoprocessor;
+import org.apache.hadoop.hbase.coprocessor.RegionCoprocessorEnvironment;
+import org.apache.hadoop.hbase.coprocessor.RegionObserver;
+import org.apache.hadoop.hbase.regionserver.MiniBatchOperationInProgress;
 import org.apache.hadoop.hbase.util.Bytes;
 import org.apache.hadoop.hbase.util.JVMClusterUtil;
 import org.apache.hadoop.hbase.util.Threads;
+import org.apache.hadoop.test.GenericTestUtils;
 import org.apache.phoenix.end2end.NeedsOwnMiniClusterTest;
 import org.apache.phoenix.hbase.index.IndexRegionObserver;
 import org.apache.phoenix.hbase.index.metrics.MetricsHaBypassSourceFactory;
@@ -65,6 +78,8 @@ import org.apache.phoenix.jdbc.FailoverPhoenixConnection;
 import org.apache.phoenix.jdbc.PhoenixResultSet;
 import org.apache.phoenix.query.QueryConstants;
 import org.apache.phoenix.replication.reader.ReplicationLogProcessor;
+import org.apache.phoenix.replication.tool.LogFileAnalyzer;
+import org.apache.phoenix.schema.types.PInteger;
 import org.apache.phoenix.util.CDCUtil;
 import org.apache.phoenix.util.MetaDataUtil;
 import org.apache.phoenix.util.PhoenixRuntime;
@@ -1003,6 +1018,10 @@ public class ReplicationLogGroupIT extends 
ReplicationLogGroupBaseIT {
     }
     // Kill the RS
     cluster.killRegionServer(rs2.getRegionServer().getServerName());
+    // A kill skips the region close, so no postClose closes the killed RS's 
groups. In a real kill
+    // the process is gone; here the group lives on in the test JVM with an 
open writer, so close
+    // it.
+    ReplicationLogGroup.closeAll(sn2);
     Threads.sleep(20000); // just to be sure that the kill has fully started.
     // Regions will be re-opened and the WAL will be replayed
     util.waitUntilAllRegionsAssigned(table);
@@ -1026,6 +1045,218 @@ public class ReplicationLogGroupIT extends 
ReplicationLogGroupBaseIT {
       indexName);
   }
 
+  /**
+   * Holds one write after its WAL commit (in postBatchMutate, before Phoenix 
replicates it in
+   * postBatchMutateIndispensably) until the test releases it.
+   */
+  public static class HoldAfterWalCommitObserver implements RegionCoprocessor, 
RegionObserver {
+    static volatile byte[] holdRow;
+    static volatile CountDownLatch held = new CountDownLatch(1);
+    static volatile CountDownLatch release = new CountDownLatch(1);
+
+    @Override
+    public Optional<RegionObserver> getRegionObserver() {
+      return Optional.of(this);
+    }
+
+    @Override
+    public void postBatchMutate(ObserverContext<RegionCoprocessorEnvironment> 
c,
+      MiniBatchOperationInProgress<Mutation> miniBatchOp) {
+      byte[] row = holdRow;
+      if (row == null) {
+        return;
+      }
+      for (int i = 0; i < miniBatchOp.size(); i++) {
+        if (Bytes.equals(row, miniBatchOp.getOperation(i).getRow())) {
+          LOG.info("Holding write after WAL commit on {} region {}",
+            c.getEnvironment().getServerName(),
+            c.getEnvironment().getRegionInfo().getEncodedName());
+          held.countDown();
+          try {
+            release.await(120, TimeUnit.SECONDS);
+            LOG.info("Released held write on {}", 
c.getEnvironment().getServerName());
+          } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+          }
+          return;
+        }
+      }
+    }
+  }
+
+  /**
+   * A graceful RegionServer stop with a write already committed to the WAL 
but not yet replicated.
+   * The write must still be shipped: if the log groups were closed in preStop 
(before the regions),
+   * it would fail replication with "Closed" and never be shipped. Closing 
rs2's groups after its
+   * last region closes also writes their log trailers, so the log check 
passes without closing them
+   * by hand.
+   */
+  @Test
+  public void testGracefulRegionServerStopClosesLogGroups() throws Exception {
+    HBaseTestingUtility util = CLUSTERS.getHBaseCluster1();
+    MiniHBaseCluster cluster = util.getHBaseCluster();
+    final String tableName = "T_" + generateUniqueName();
+    TableName table = TableName.valueOf(tableName);
+    try (FailoverPhoenixConnection conn = (FailoverPhoenixConnection) 
DriverManager
+      .getConnection(CLUSTERS.getJdbcHAUrl(), clientProps)) {
+      conn.createStatement().execute(String.format("create table if not exists 
%s (id1 integer "
+        + "not null, val1 varchar constraint pk primary key (id1))", 
tableName));
+      conn.commit();
+    }
+    try (Admin admin = util.getConnection().getAdmin()) {
+      
admin.modifyTable(TableDescriptorBuilder.newBuilder(admin.getDescriptor(table))
+        .setCoprocessor(HoldAfterWalCommitObserver.class.getName()).build());
+    }
+    JVMClusterUtil.RegionServerThread rs2 = cluster.startRegionServer();
+    ServerName sn2 = rs2.getRegionServer().getServerName();
+    moveRegionToServer(table, sn2);
+
+    final int heldId = 1000;
+    try (FailoverPhoenixConnection conn = (FailoverPhoenixConnection) 
DriverManager
+      .getConnection(CLUSTERS.getJdbcHAUrl(), clientProps)) {
+      PreparedStatement stmt = conn.prepareStatement("upsert into " + 
tableName + " VALUES(?, ?)");
+      for (int i = 0; i < 10; ++i) {
+        stmt.setInt(1, i);
+        stmt.setString(2, "v");
+        stmt.executeUpdate();
+        conn.commit();
+      }
+    }
+    assertTrue("rs2 should have a replication log group before the stop",
+      ReplicationLogGroup.INSTANCES.values().stream().anyMatch(g -> 
g.getServerName().equals(sn2)));
+
+    HoldAfterWalCommitObserver.held = new CountDownLatch(1);
+    HoldAfterWalCommitObserver.release = new CountDownLatch(1);
+    HoldAfterWalCommitObserver.holdRow = PInteger.INSTANCE.toBytes(heldId);
+    Thread writer = new Thread(() -> {
+      try (FailoverPhoenixConnection conn = (FailoverPhoenixConnection) 
DriverManager
+        .getConnection(CLUSTERS.getJdbcHAUrl(), clientProps)) {
+        conn.createStatement()
+          .execute("upsert into " + tableName + " VALUES(" + heldId + ", 
'held')");
+        conn.commit();
+      } catch (Exception e) {
+        LOG.info("Held upsert failed: {}", e.toString());
+      }
+    }, "held-writer");
+    writer.start();
+    try {
+      assertTrue("The write should be held after its WAL commit",
+        HoldAfterWalCommitObserver.held.await(60, TimeUnit.SECONDS));
+      // stop() runs the RS coprocessors' stop in preStop, then marks the 
server stopped; the held
+      // write keeps its region open, so the region close waits for it.
+      cluster.stopRegionServer(sn2);
+      GenericTestUtils.waitFor(() -> rs2.getRegionServer().isStopped(), 100, 
60000);
+    } finally {
+      HoldAfterWalCommitObserver.holdRow = null;
+      HoldAfterWalCommitObserver.release.countDown();
+    }
+    writer.join(120000);
+    rs2.join(120000);
+    assertFalse("rs2 should have stopped", rs2.isAlive());
+    assertFalse("rs2's replication log groups should be closed on a graceful 
stop",
+      ReplicationLogGroup.INSTANCES.values().stream().anyMatch(g -> 
g.getServerName().equals(sn2)));
+    util.waitUntilAllRegionsAssigned(table);
+
+    // Every row committed on cluster-a, acknowledged or not, must be in the 
shipped logs.
+    Set<String> committed = new HashSet<>();
+    try (Connection conn = 
DriverManager.getConnection(CLUSTERS.getJdbcUrl1(haGroup));
+      ResultSet rs = conn.createStatement().executeQuery("select id1 from " + 
tableName)) {
+      while (rs.next()) {
+        
committed.add(Bytes.toStringBinary(PInteger.INSTANCE.toBytes(rs.getInt(1))));
+      }
+    }
+    assertTrue("The held write was committed on cluster-a",
+      
committed.contains(Bytes.toStringBinary(PInteger.INSTANCE.toBytes(heldId))));
+    logGroup.close();
+    List<Mutation> shippedMutations = groupLogsByTable().get(tableName);
+    Set<String> shipped = new HashSet<>();
+    if (shippedMutations != null) {
+      for (Mutation m : shippedMutations) {
+        shipped.add(Bytes.toStringBinary(m.getRow()));
+      }
+    }
+    Set<String> missing = new HashSet<>(committed);
+    missing.removeAll(shipped);
+    assertTrue("Rows committed on the active but never shipped: " + missing, 
missing.isEmpty());
+
+    // rs2 committed the held write, so rs2's own log must carry it. A client 
retry through the
+    // reopened region would also ship the row, but from another server's log, 
hiding a lost first
+    // attempt.
+    Path root = logGroup.getOrCreatePeerShardManager().getRootDirectoryPath();
+    FileSystem fs = root.getFileSystem(conf2);
+    LogFileAnalyzer analyzer = new LogFileAnalyzer();
+    analyzer.setConf(conf2);
+    Set<String> shippedByRs2 = new HashSet<>();
+    RemoteIterator<LocatedFileStatus> files = fs.listFiles(root, true);
+    while (files.hasNext()) {
+      Path file = files.next().getPath();
+      if (!file.getName().contains(sn2.getServerName())) {
+        continue;
+      }
+      List<Mutation> mutations = 
analyzer.groupLogsByTable(file.toString()).get(tableName);
+      if (mutations != null) {
+        for (Mutation m : mutations) {
+          shippedByRs2.add(Bytes.toStringBinary(m.getRow()));
+        }
+      }
+    }
+    assertTrue("rs2 committed the held write but its log does not carry it",
+      
shippedByRs2.contains(Bytes.toStringBinary(PInteger.INSTANCE.toBytes(heldId))));
+  }
+
+  /**
+   * A stop after the server's regions were moved away, as a 
graceful_stop.sh-style unload does. No
+   * IndexRegionObserver region close happens during the stop, so the stop 
itself must close the
+   * server's groups.
+   */
+  @Test
+  public void testRegionServerStopAfterUnloadClosesLogGroups() throws 
Exception {
+    HBaseTestingUtility util = CLUSTERS.getHBaseCluster1();
+    MiniHBaseCluster cluster = util.getHBaseCluster();
+    final String tableName = "T_" + generateUniqueName();
+    TableName table = TableName.valueOf(tableName);
+    try (FailoverPhoenixConnection conn = (FailoverPhoenixConnection) 
DriverManager
+      .getConnection(CLUSTERS.getJdbcHAUrl(), clientProps)) {
+      conn.createStatement().execute(String.format("create table if not exists 
%s (id1 integer "
+        + "not null, val1 varchar constraint pk primary key (id1))", 
tableName));
+      conn.commit();
+    }
+    ServerName sn1 = cluster.getRegionServer(0).getServerName();
+    // Keep the balancer from moving another IndexRegionObserver region (e.g. 
SYSTEM.CATALOG) onto
+    // rs2, which would make its stop close the groups through regionClosed 
instead.
+    boolean balancerWasOn = util.getAdmin().balancerSwitch(false, true);
+    try {
+      JVMClusterUtil.RegionServerThread rs2 = cluster.startRegionServer();
+      ServerName sn2 = rs2.getRegionServer().getServerName();
+      moveRegionToServer(table, sn2);
+      try (FailoverPhoenixConnection conn = (FailoverPhoenixConnection) 
DriverManager
+        .getConnection(CLUSTERS.getJdbcHAUrl(), clientProps)) {
+        conn.createStatement().execute("upsert into " + tableName + " 
VALUES(1, 'v')");
+        conn.commit();
+      }
+      assertTrue("rs2 should have a replication log group before the unload",
+        ReplicationLogGroup.INSTANCES.values().stream()
+          .anyMatch(g -> g.getServerName().equals(sn2)));
+
+      moveRegionToServer(table, sn1);
+      assertTrue("rs2 must host no IndexRegionObserver region before the stop",
+        rs2.getRegionServer().getRegions().stream().noneMatch(
+          r -> 
r.getCoprocessorHost().findCoprocessor(IndexRegionObserver.class) != null));
+      assertTrue("Regions moving away while running must leave rs2's groups 
open",
+        ReplicationLogGroup.INSTANCES.values().stream()
+          .anyMatch(g -> g.getServerName().equals(sn2)));
+
+      cluster.stopRegionServer(sn2);
+      rs2.join(120000);
+      assertFalse("rs2 should have stopped", rs2.isAlive());
+      assertFalse("rs2's replication log groups should be closed by the stop",
+        ReplicationLogGroup.INSTANCES.values().stream()
+          .anyMatch(g -> g.getServerName().equals(sn2)));
+    } finally {
+      util.getAdmin().balancerSwitch(balancerWasOn, true);
+    }
+  }
+
   /**
    * End-to-end replication of the tenant-owned SYSTEM.CATALOG and 
SYSTEM.CHILD_LINK rows that back
    * a tenant view. Models the intended HA topology: the base table and the 
global view are global
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 1e0854f2f3..20aae6b679 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
@@ -67,6 +67,7 @@ import java.util.concurrent.atomic.AtomicLong;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.fs.Path;
 import org.apache.hadoop.hbase.Cell;
+import org.apache.hadoop.hbase.Server;
 import org.apache.hadoop.hbase.ServerName;
 import org.apache.hadoop.hbase.client.Mutation;
 import org.apache.phoenix.jdbc.ClusterType;
@@ -992,6 +993,140 @@ public class ReplicationLogGroupTest extends 
ReplicationLogBaseTest {
     verify(logGroup, never()).abort(anyString(), any());
   }
 
+  /** Registers an initialized group for a distinct server so closeAll can 
reach it. */
+  private ReplicationLogGroup registerGroupForServer(ServerName sn, String 
group) throws Exception {
+    ReplicationLogGroup g =
+      new TestableLogGroup(conf, sn, group, haGroupStoreManager, 
useAlignedRotation());
+    g.init();
+    ReplicationLogGroup.cacheForTesting(g);
+    return g;
+  }
+
+  private static Server server(boolean stopped, boolean stopping, boolean 
aborted) {
+    Server server = mock(Server.class);
+    doReturn(stopped).when(server).isStopped();
+    doReturn(stopping).when(server).isStopping();
+    doReturn(aborted).when(server).isAborted();
+    return server;
+  }
+
+  private static ServerName uniqueServer(int port) {
+    return ServerName.valueOf("rc-test", port, System.nanoTime());
+  }
+
+  @Test
+  public void testGroupsCloseOnLastRegionCloseWhileStopping() throws Exception 
{
+    ServerName sn = uniqueServer(61001);
+    ReplicationLogGroup g = registerGroupForServer(sn, haGroupName + "-rc1");
+    try {
+      Server stopping = server(true, false, false);
+      ReplicationLogGroup.regionOpened(sn);
+      ReplicationLogGroup.regionOpened(sn);
+      ReplicationLogGroup.regionClosed(sn, stopping);
+      assertFalse("Groups must stay open while a region is still open", 
g.isClosed());
+      ReplicationLogGroup.regionClosed(sn, stopping);
+      assertTrue("Groups must close when the last region closes while 
stopping", g.isClosed());
+    } finally {
+      g.close();
+    }
+  }
+
+  @Test
+  public void testGroupsStayOpenWhenLastRegionClosesWhileRunning() throws 
Exception {
+    ServerName sn = uniqueServer(61002);
+    ReplicationLogGroup g = registerGroupForServer(sn, haGroupName + "-rc2");
+    try {
+      ReplicationLogGroup.regionOpened(sn);
+      ReplicationLogGroup.regionClosed(sn, server(false, false, false));
+      assertFalse("Regions moving away while running must not close the 
groups", g.isClosed());
+    } finally {
+      g.close();
+    }
+  }
+
+  @Test
+  public void testGroupsCloseOnLastRegionCloseWhileAbortingOrClusterStopping() 
throws Exception {
+    for (Server s : new Server[] { server(false, false, true), server(false, 
true, false) }) {
+      ServerName sn = uniqueServer(61003);
+      ReplicationLogGroup g = registerGroupForServer(sn, haGroupName + "-rc3");
+      try {
+        ReplicationLogGroup.regionOpened(sn);
+        ReplicationLogGroup.regionClosed(sn, s);
+        assertTrue(g.isClosed());
+      } finally {
+        g.close();
+      }
+    }
+  }
+
+  @Test
+  public void testRegionCloseWithoutOpenDoesNotGoNegative() throws Exception {
+    ServerName sn = uniqueServer(61004);
+    ReplicationLogGroup g = registerGroupForServer(sn, haGroupName + "-rc4");
+    try {
+      Server stopping = server(true, false, false);
+      ReplicationLogGroup.regionClosed(sn, stopping);
+      assertFalse("A close with no open region must not close the groups", 
g.isClosed());
+      ReplicationLogGroup.regionOpened(sn);
+      ReplicationLogGroup.regionOpened(sn);
+      ReplicationLogGroup.regionClosed(sn, stopping);
+      assertFalse("The stray close must not have consumed a real region's 
count", g.isClosed());
+      ReplicationLogGroup.regionClosed(sn, stopping);
+      assertTrue(g.isClosed());
+    } finally {
+      g.close();
+    }
+  }
+
+  @Test
+  public void testRegionCountsAreSeparatePerServer() throws Exception {
+    ServerName sn1 = uniqueServer(61005);
+    ServerName sn2 = uniqueServer(61006);
+    ReplicationLogGroup g1 = registerGroupForServer(sn1, haGroupName + 
"-rc5a");
+    ReplicationLogGroup g2 = registerGroupForServer(sn2, haGroupName + 
"-rc5b");
+    try {
+      ReplicationLogGroup.regionOpened(sn1);
+      ReplicationLogGroup.regionOpened(sn2);
+      ReplicationLogGroup.regionClosed(sn1, server(true, false, false));
+      assertTrue(g1.isClosed());
+      assertFalse("Another server's groups must stay open", g2.isClosed());
+    } finally {
+      g1.close();
+      g2.close();
+    }
+  }
+
+  @Test
+  public void testStopClosesGroupsWhenNoRegionIsOpen() throws Exception {
+    ServerName sn = uniqueServer(61007);
+    ReplicationLogGroup g = registerGroupForServer(sn, haGroupName + "-rc6");
+    try {
+      // Regions moved away while running (e.g. unloaded before a graceful 
stop).
+      ReplicationLogGroup.regionOpened(sn);
+      ReplicationLogGroup.regionClosed(sn, server(false, false, false));
+      assertFalse(g.isClosed());
+      ReplicationLogGroup.closeAllIfNoOpenRegions(sn);
+      assertTrue("Stop must close the groups when no region is left to do it", 
g.isClosed());
+    } finally {
+      g.close();
+    }
+  }
+
+  @Test
+  public void testStopDefersCloseWhileARegionIsOpen() throws Exception {
+    ServerName sn = uniqueServer(61008);
+    ReplicationLogGroup g = registerGroupForServer(sn, haGroupName + "-rc7");
+    try {
+      ReplicationLogGroup.regionOpened(sn);
+      ReplicationLogGroup.closeAllIfNoOpenRegions(sn);
+      assertFalse("Stop must not close the groups while a region can still 
write", g.isClosed());
+      ReplicationLogGroup.regionClosed(sn, server(true, false, false));
+      assertTrue(g.isClosed());
+    } finally {
+      g.close();
+    }
+  }
+
   /**
    * 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