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 b2411f57ca PHOENIX-8011 Classify batch origin and gate HA write-path 
on the sync-replication master switch (#2627)
b2411f57ca is described below

commit b2411f57ca3d0620aba4613177321567986a3a27
Author: tkhurana <[email protected]>
AuthorDate: Thu Oct 1 11:02:40 2026 -0700

    PHOENIX-8011 Classify batch origin and gate HA write-path on the 
sync-replication master switch (#2627)
    
    * PHOENIX-8011 Classify batch origin and gate HA write-path on the 
sync-replication master switch
    
    Replace the scattered per-batch re-derivation of a batch's HA disposition 
with a
    single BatchOrigin classification, gate all HA write-path semantics on the
    SYNCHRONOUS_REPLICATION_ENABLED master switch, and add replication-log 
commit-id
    observability.
    
    - BatchOrigin (CLIENT_HA, CLIENT_NON_HA, PHX_REPLAY, NATIVE_IN): classified 
once
      per batch and carried on BatchMutateContext as the single source of truth;
      isReplay() is derived from it (no separate flag).
    - Split shouldReplicate into syncReplicationEnabled (master switch) and
      replicateTable (switch AND per-table eligibility). Gate the cluster-role
      mutation-block / staleness check on the master switch so it never runs, or
      resolves a not-yet-created HA group, while the feature is off -- makes the
      rollout window safe.
    - bypassedMutationBlockCount increments only for CLIENT_NON_HA (replay and
      native-replicated-in excluded). getReplicationLogGroup owns the full 
"which
      writer" decision; isReplicatedBatch reduces to log-group presence, with a
      checkState enforcing present => CLIENT_HA.
    - Replication-log observability: carry the WAL seq id as each record's 
commitId;
      advance lastCommitId through a single appendRecord() path (normal and
      post-failover replay) and log it at mode switches; log recordsRead on 
standby
      replay; drop the batchSize histogram.
    - Correct the operator-facing bypassedMutationBlockCount JMX/javadoc to the
      CLIENT_NON_HA-on-eligible-table definition.
    - Add HA_GROUP_NAME_ATTRIB to ReplicationLogGroup.REPLICATION_ATTR_KEYS so 
the HA
      group name rides the replication envelope like schema/table/tenant; drop 
the
      special-case WAL-key stamp in IndexRegionObserver (the envelope loop now 
carries
      it, and the WAL-restore path reads it back via getHAGroupFromWALKey).
    - Gate BaseScannerRegionObserver's scan-path HAGroup staleness check on the
      SYNCHRONOUS_REPLICATION_ENABLED master switch, mirroring the 
IndexRegionObserver
      write-path gate, so it never runs while the feature is off (including the 
rollout
      window where clients send _HAGroupName before the HA group exists).
    - Log a per-table WARN (once per table per RegionServer, unbounded set, no 
TTL) when
      a CLIENT_NON_HA batch reaches a replication-eligible table without 
_HAGroupName, to
      identify the write path that is missing the attribute.
---
 .../coprocessor/BaseScannerRegionObserver.java     |  11 +-
 .../phoenix/hbase/index/IndexRegionObserver.java   | 269 +++++++++++++++------
 .../hbase/index/metrics/MetricsHaBypassSource.java |  50 ++--
 .../phoenix/replication/ReplicationLogGroup.java   |  46 ++--
 .../replication/log/LogFileFormatWriter.java       |   7 +-
 .../metrics/MetricsReplicationLogGroupSource.java  |   9 -
 .../MetricsReplicationLogGroupSourceImpl.java      |  11 +-
 .../metrics/ReplicationLogMetricValues.java        |  12 -
 .../reader/ReplicationLogProcessor.java            |   7 +-
 ...cReplicationRolloutFeatureOnMissingGroupIT.java | 117 +++++++++
 .../end2end/SyncReplicationRolloutStep1IT.java     | 108 +++++++++
 .../index/BypassedMutationBlockMetricsIT.java      |   9 +-
 .../replication/ReplicationLogGroupBaseIT.java     |  13 +-
 .../phoenix/replication/ReplicationLogGroupIT.java |  81 +++++++
 .../replication/ReplicationLogGroupParallelIT.java |  97 ++++++++
 .../hbase/index/IndexRegionObserverReplayTest.java | 114 +++++++++
 .../replication/ReplicationLogGroupTest.java       |  12 +-
 .../phoenix/replication/log/LogFileCodecTest.java  |   3 +
 .../MetricsReplicationLogGroupSourceImplTest.java  |   4 +-
 19 files changed, 811 insertions(+), 169 deletions(-)

diff --git 
a/phoenix-core-server/src/main/java/org/apache/phoenix/coprocessor/BaseScannerRegionObserver.java
 
b/phoenix-core-server/src/main/java/org/apache/phoenix/coprocessor/BaseScannerRegionObserver.java
index 6d09ce9a90..4977548412 100644
--- 
a/phoenix-core-server/src/main/java/org/apache/phoenix/coprocessor/BaseScannerRegionObserver.java
+++ 
b/phoenix-core-server/src/main/java/org/apache/phoenix/coprocessor/BaseScannerRegionObserver.java
@@ -190,10 +190,19 @@ abstract public class BaseScannerRegionObserver 
implements RegionObserver {
     }
     byte[] haGroupNameBytes =
       
scan.getAttribute(BaseScannerRegionObserverConstants.HA_GROUP_NAME_ATTRIB);
-    // Check if scan's HAGroup is stale on client. If yes, throw an exception.
+    // Check if scan's HAGroup is stale on client. If yes, throw an exception. 
Gated on the
+    // sync-replication master switch (mirrors IndexRegionObserver's 
write-path gate) so the check
+    // never runs while the feature is off — including the rollout window 
where clients already send
+    // _HAGroupName but the HA group does not exist yet.
     if (haGroupNameBytes != null) {
       String haGroupName = Bytes.toString(haGroupNameBytes);
       final Configuration conf = c.getEnvironment().getConfiguration();
+      if (
+        !conf.getBoolean(QueryServices.SYNCHRONOUS_REPLICATION_ENABLED,
+          QueryServicesOptions.DEFAULT_SYNCHRONOUS_REPLICATION_ENABLED)
+      ) {
+        return;
+      }
       final HAGroupStoreManager haGroupStoreManager = 
HAGroupStoreManager.getInstance(conf);
       if (haGroupStoreManager == null) {
         throw new IOException(
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 3cc6310196..f80da44167 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
@@ -48,6 +48,7 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 import java.util.TreeSet;
+import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CountDownLatch;
@@ -196,6 +197,11 @@ import 
org.apache.hadoop.hbase.shaded.protobuf.generated.ClientProtos;
 public class IndexRegionObserver implements RegionCoprocessor, RegionObserver {
 
   private static final Logger LOG = 
LoggerFactory.getLogger(IndexRegionObserver.class);
+  // Tables already warned about for a CLIENT_NON_HA write on a 
replication-eligible table (a writer
+  // not attaching the _HAGroupName attribute). Each table is logged at most 
once per RegionServer
+  // lifetime so a high-volume untagged writer cannot flood the log. Shared 
across all per-region
+  // instances; bounded in practice by the finite set of tables on the 
RegionServer.
+  private static final Set<String> BYPASS_WARNED_TABLES = 
ConcurrentHashMap.newKeySet();
   private static final OperationStatus IGNORE = new OperationStatus(SUCCESS);
   private static final OperationStatus NOWRITE = new OperationStatus(SUCCESS);
   public static final String PHOENIX_APPEND_METADATA_TO_WAL = 
"phoenix.append.metadata.to.wal";
@@ -263,9 +269,10 @@ public class IndexRegionObserver implements 
RegionCoprocessor, RegionObserver {
   public static final boolean DEFAULT_PHOENIX_INDEX_CDC_MUTATION_SERIALIZE = 
false;
   // Generic marker attribute set on every mutation produced by the standby 
reader from a
   // replication-log record. Value is opaque (presence is the signal). 
Detected in
-  // preBatchMutateWithExceptions to set context.isReplication, which gates 
the primary-side clock
-  // work (getBatchTimestamp/setTimestamps) off so the cell timestamps shipped 
from the active
-  // cluster are preserved. Never set on primary-side mutations.
+  // preBatchMutateWithExceptions to classify the batch as PHX_REPLAY (see 
context.isReplay()),
+  // which
+  // gates the primary-side clock work (getBatchTimestamp/setTimestamps) off 
so the cell timestamps
+  // shipped from the active cluster are preserved. Never set on primary-side 
mutations.
   public static final String REPLICATED_MUTATION = "_ReplicatedMutation";
   // Per-row mutation attribute that the standby reader synthesizes from the 
pre-image cell and
   // attaches to each reconstructed mutation when its row had a pre-image 
entry. Value is the
@@ -491,19 +498,17 @@ public class IndexRegionObserver implements 
RegionCoprocessor, RegionObserver {
     private boolean returnOldRow;
     private boolean hasConditionalTTL; // table has Conditional TTL
     private boolean immutableRows;
-    // True when this batch was produced by the standby reader from a 
replication-log record (i.e.
-    // every mutation carries the {@link 
IndexRegionObserver#REPLICATED_MUTATION} marker). Batch-
-    // uniform by construction: the standby reader stamps every reconstructed 
mutation, so checking
-    // the first one is sufficient. The standby uses this to skip the 
data-table scan in the PRE
-    // phase (pre-image cells are carried as the per-row {@link 
IndexRegionObserver#PRE_IMAGE}
-    // attribute when the table has a global/uncovered/transform index — same 
schema on both
-    // clusters, so when we're inside that branch on the standby we always 
have pre-images).
-    private boolean isReplication;
+    // Where this batch came from, classified once from the first mutation and 
carried as the single
+    // source of truth for this batch's HA disposition. The counter, the 
cluster-role gate, and
+    // log-group resolution key off it in preBatchMutate; the replay predicate 
{@link #isReplay()}
+    // is
+    // derived from it. Defaults to CLIENT_NON_HA (benign: no HA handling).
+    private BatchOrigin origin = BatchOrigin.CLIENT_NON_HA;
     // HAGroup associated with the batch
     private Optional<ReplicationLogGroup> logGroup = Optional.empty();
     // Per-(row, ts) groups folded from this replicated batch, computed once 
and shared by the
     // global-index path (prepareReplicatedIndexMutations) and the local-index 
path. Null until
-    // first built; only populated on the standby replay path (isReplication).
+    // first built; only populated on the standby replay path (isReplay).
     private List<ReplicatedRowGroup> replicatedRowGroups;
 
     public BatchMutateContext() {
@@ -514,6 +519,20 @@ public class IndexRegionObserver implements 
RegionCoprocessor, RegionObserver {
       this.clientVersion = clientVersion;
     }
 
+    /**
+     * True when this batch was produced by the standby reader from a 
replication-log record (batch
+     * origin {@link BatchOrigin#PHX_REPLAY} — every mutation carries the
+     * {@link IndexRegionObserver#REPLICATED_MUTATION} marker). Batch-uniform 
by construction: the
+     * standby reader stamps every reconstructed mutation, so the origin 
classified from the first
+     * one holds for the whole batch. The standby uses this to skip the 
data-table scan in the PRE
+     * phase (pre-image cells are carried as the per-row {@link 
IndexRegionObserver#PRE_IMAGE}
+     * attribute when the table has a global/uncovered/transform index — same 
schema on both
+     * clusters, so when we're inside that branch on the standby we always 
have pre-images).
+     */
+    boolean isReplay() {
+      return origin == BatchOrigin.PHX_REPLAY;
+    }
+
     public void 
populateOriginalMutations(MiniBatchOperationInProgress<Mutation> miniBatchOp) {
       originalMutations = new ArrayList<Mutation>(miniBatchOp.size());
       for (int k = 0; k < miniBatchOp.size(); k++) {
@@ -622,7 +641,14 @@ public class IndexRegionObserver implements 
RegionCoprocessor, RegionObserver {
   private static final int DEFAULT_ROWLOCK_WAIT_DURATION = 30000;
   private static final int DEFAULT_CONCURRENT_MUTATION_WAIT_DURATION_IN_MS = 
100;
   private byte[] encodedRegionName;
-  private boolean shouldReplicate;
+  // Raw SYNCHRONOUS_REPLICATION_ENABLED value: the master switch for all HA 
semantics. Unlike
+  // replicateTable it is not narrowed by per-table replication eligibility, 
so the cluster-role
+  // gates key off this — applying across all tables when the feature is on, 
and fully skipped when
+  // it is off (e.g. the rollout window before the HA group exists).
+  private boolean syncReplicationEnabled;
+  // Sync replication is enabled AND this table is replication-eligible: i.e. 
this table's writes
+  // should ride the replication log on this cluster. Set once per coproc load.
+  private boolean replicateTable;
   private Abortable abortable;
 
   // Don't replicate the mutation if this attribute is set
@@ -732,13 +758,12 @@ public class IndexRegionObserver implements 
RegionCoprocessor, RegionObserver {
       // when the table descriptor changes, the coproc is reloaded
       this.useBloomFilter = bloomFilterType == BloomType.ROW;
       byte[] tableName = env.getRegionInfo().getTable().getName();
-      this.shouldReplicate = 
env.getConfiguration().getBoolean(SYNCHRONOUS_REPLICATION_ENABLED,
-        DEFAULT_SYNCHRONOUS_REPLICATION_ENABLED);
-      if (this.shouldReplicate) {
-        // replication feature is enabled, check if it is enabled for the table
-        this.shouldReplicate = SchemaUtil.shouldReplicateTable(tableName);
-      }
-      if (this.shouldReplicate) {
+      this.syncReplicationEnabled = env.getConfiguration()
+        .getBoolean(SYNCHRONOUS_REPLICATION_ENABLED, 
DEFAULT_SYNCHRONOUS_REPLICATION_ENABLED);
+      // replication feature is enabled, check if it is enabled for the table
+      this.replicateTable =
+        this.syncReplicationEnabled && 
SchemaUtil.shouldReplicateTable(tableName);
+      if (this.replicateTable) {
         this.ignoreReplicationFilter = 
getSynchronousReplicationFilter(tableName);
       }
       // @CoreCoprocessor guarantees HasRegionServerServices, but guard for 
testability
@@ -839,6 +864,24 @@ public class IndexRegionObserver implements 
RegionCoprocessor, RegionObserver {
     }
   }
 
+  /**
+   * Where a mutation batch reaching the write path originated. Classified 
once (see
+   * {@link #classifyBatch}) and then used by every HA decision instead of 
re-deriving from raw
+   * attributes.
+   */
+  enum BatchOrigin {
+    /** Originating client write carrying the {@code _HAGroupName} attribute. 
*/
+    CLIENT_HA,
+    /** Originating write with no {@code _HAGroupName} (non-HA client, or one 
that omitted it). */
+    CLIENT_NON_HA,
+    /** Standby replay: reconstructed by the log reader, carries {@link 
#REPLICATED_MUTATION}. */
+    PHX_REPLAY,
+    /**
+     * Arrived via HBase-native replication: carries source cluster ids, not 
{@code _HAGroupName}.
+     */
+    NATIVE_IN
+  }
+
   /*
    * Also checks for mutationBlockEnabled if 
CLUSTER_ROLE_BASED_MUTATION_BLOCK_ENABLED is enabled.
    */
@@ -859,41 +902,54 @@ public class IndexRegionObserver implements 
RegionCoprocessor, RegionObserver {
       // gates below key off this name, not off writer presence, so they apply 
on a standby or
       // demoted cluster too — not only where this cluster is active.
       Optional<String> haGroupName = getHAGroupNameFromBatch(miniBatchOp);
-
-      // Path-coverage counter: increments whenever a mutation batch reaches 
preBatchMutate
-      // without a resolvable HA group attribute, so the cluster-role-based 
mutation-block gate
-      // has no haGroupName to evaluate against and is skipped. This counts 
the code path being
-      // short-circuited — it does NOT imply any safety property was breached 
(when the block
-      // feature is disabled or no block window is active, there is no 
property to breach).
-      // Tracked globally rather than per-table so operators can compare 
baseline vs.
-      // post-deploy delta to spot new write paths that forgot to attach 
_HAGroupName.
-      // Intentionally scoped to !haGroupName.isPresent() regardless of 
dataTableName —
-      // system-HA-group writes WITH a haGroup are an intended gate exemption 
(state writes
-      // must proceed during a block window) and are not counted here.
-      if (shouldReplicate && !haGroupName.isPresent()) {
+      BatchOrigin origin = classifyBatch(miniBatchOp, haGroupName);
+
+      // Path-coverage counter: increments only for a genuine client write 
that reached the write
+      // path without attaching _HAGroupName (CLIENT_NON_HA), on a 
replication-eligible table. The
+      // signal is "a client missed attaching _HAGroupName" — it does NOT 
imply any safety property
+      // was breached. Tracked globally rather than per-table so operators can 
compare baseline vs.
+      // post-deploy delta to spot new write paths that forgot the attribute. 
Standby replay
+      // (PHX_REPLAY) and native-replicated-in (NATIVE_IN) batches are 
distinct origins and so are
+      // excluded, as are system-HA-group state writes (which carry a haGroup, 
i.e. CLIENT_HA).
+      if (replicateTable && origin == BatchOrigin.CLIENT_NON_HA) {
         try {
           
MetricsHaBypassSourceFactory.getInstance().incrementBypassedMutationBlockCount();
         } catch (Throwable t) {
           LOG.warn("Failed to increment bypassed mutation block count metric; 
continuing", t);
         }
+        // Per-table breadcrumb for triage: identify which table has a writer 
not attaching
+        // _HAGroupName. Logged once per table per RegionServer lifetime 
(add() is true only on
+        // first insertion) so it names the offender without flooding on 
sustained writes.
+        if (BYPASS_WARNED_TABLES.add(dataTableName)) {
+          LOG.warn(
+            "Table {} received a mutation batch without an _HAGroupName 
attribute while sync "
+              + "replication is enabled (origin CLIENT_NON_HA); a writer to 
this table is not "
+              + "attaching the HA group attribute. Investigate and fix that 
write path.",
+            dataTableName);
+        }
       }
 
-      // We don't want to check for mutation blocking for the system ha group 
table
-      if (!dataTableName.equals(SYSTEM_HA_GROUP_NAME) && 
haGroupName.isPresent()) {
+      // Cluster-role mutation-block / staleness gate. All HA semantics are 
gated on the master
+      // switch (syncReplicationEnabled): with the feature off this never runs 
— including the
+      // rollout window where clients already send _HAGroupName but the HA 
group does not exist yet.
+      // The system ha group table is exempt so its state writes proceed 
during a block window.
+      if (
+        syncReplicationEnabled && origin == BatchOrigin.CLIENT_HA
+          && !dataTableName.equals(SYSTEM_HA_GROUP_NAME)
+      ) {
         String name = haGroupName.get();
         // TODO: Below approach might be slow need to figure out faster way,
         // slower part is getting haGroupStoreClient We can also cache
         // roleRecord (I tried it and still it's slow due to haGroupStoreClient
         // initialization) and caching will give us old result in case one 
cluster
         // is unreachable instead of UNKNOWN.
-
+        // Check if mutation's haGroup is stale
         boolean isHAGroupOnClientStale = 
haGroupStoreManager.isHAGroupOnClientStale(name);
         if (StringUtils.isNotBlank(name) && isHAGroupOnClientStale) {
           throw new StaleClusterRoleRecordException(
             String.format("HAGroupStoreRecord is stale for haGroup %s on " + 
"client", name));
         }
 
-        // Check if mutation's haGroup is stale
         if (StringUtils.isNotBlank(name) && 
haGroupStoreManager.isMutationBlocked(name)) {
           throw new MutationBlockedIOException(
             "Blocking Mutation as Some CRRs " + "are in ACTIVE_TO_STANDBY 
state and "
@@ -901,13 +957,11 @@ public class IndexRegionObserver implements 
RegionCoprocessor, RegionObserver {
         }
       }
 
-      // Resolve the replication writer for this batch (present only where 
this cluster is active
-      // for the HA group). This also enforces the split-brain guard for 
non-PARALLEL live writes
-      // to a non-active cluster.
-      Optional<ReplicationLogGroup> logGroup = haGroupName.isPresent()
-        ? getReplicationLogGroup(c.getEnvironment(), haGroupStoreManager, 
haGroupName.get())
-        : Optional.empty();
-      preBatchMutateWithExceptions(c, miniBatchOp, logGroup);
+      // Resolve the replication writer for this batch (empty if none should 
be used). This also
+      // enforces the split-brain guard for non-PARALLEL live writes to a 
non-active cluster.
+      Optional<ReplicationLogGroup> logGroup =
+        getReplicationLogGroup(c.getEnvironment(), haGroupStoreManager, 
origin, haGroupName);
+      preBatchMutateWithExceptions(c, miniBatchOp, logGroup, origin);
       return;
     } catch (Throwable t) {
       rethrowIndexingException(t);
@@ -917,17 +971,22 @@ public class IndexRegionObserver implements 
RegionCoprocessor, RegionObserver {
   }
 
   /**
-   * Resolve the replication writer for the named HA group. Returns the writer 
when this cluster is
-   * active for the group, empty when replication is disabled or a non-active 
PARALLEL cluster
-   * legitimately receives a double-write, and throws for a non-active 
non-PARALLEL (split-brain)
-   * live write.
+   * Resolve the replication writer for this batch, or empty when none should 
be used. Returns empty
+   * unless this is a CLIENT_HA write on a replication-eligible cluster 
({@code replicateTable} =
+   * feature on AND table eligible) — which covers the feature-off rollout 
window and non-eligible
+   * tables. For such a write it then answers the role/policy question: the 
writer when this cluster
+   * is active for the group, empty for a non-active PARALLEL cluster 
(legitimate double-write, skip
+   * re-ship), and throws for a non-active non-PARALLEL (split-brain) live 
write.
    * @return the HA group writer, or empty if none should be used
    */
   private Optional<ReplicationLogGroup> 
getReplicationLogGroup(RegionCoprocessorEnvironment env,
-    HAGroupStoreManager haGroupStoreManager, String haGroup) throws 
IOException {
-    if (!shouldReplicate) {
+    HAGroupStoreManager haGroupStoreManager, BatchOrigin origin, 
Optional<String> haGroupName)
+    throws IOException {
+    if (!replicateTable || origin != BatchOrigin.CLIENT_HA) {
       return Optional.empty();
     }
+    // origin == CLIENT_HA guarantees the HA group name is present.
+    String haGroup = haGroupName.get();
     Optional<ReplicationLogGroup> logGroup = 
ReplicationLogGroup.get(env.getConfiguration(),
       env.getServerName(), haGroup, haGroupStoreManager, abortable);
     if (logGroup.isPresent()) {
@@ -945,8 +1004,9 @@ public class IndexRegionObserver implements 
RegionCoprocessor, RegionObserver {
     if (isParallelPolicy(haGroupStoreManager, haGroup)) {
       return Optional.empty();
     }
-    throw new IOException(String
-      .format("HAGroup %s cannot accept a sync-path write: this cluster is not 
active", haGroup));
+    throw new IOException(
+      String.format("HAGroup %s cannot accept a HA write on %s: this cluster 
is not active",
+        haGroup, dataTableName));
   }
 
   /**
@@ -967,6 +1027,40 @@ public class IndexRegionObserver implements 
RegionCoprocessor, RegionObserver {
     return Optional.empty();
   }
 
+  /**
+   * A replay batch is one the standby reader reconstructed; every mutation 
carries the
+   * {@link #REPLICATED_MUTATION} marker. The marker is batch-uniform by 
construction, so the first
+   * mutation is authoritative.
+   * @return true if the batch is a standby replay batch
+   */
+  boolean isReplayBatch(MiniBatchOperationInProgress<Mutation> miniBatchOp) {
+    return miniBatchOp.size() > 0
+      && miniBatchOp.getOperation(0).getAttribute(REPLICATED_MUTATION) != null;
+  }
+
+  /**
+   * Classify where a batch originated, from the first mutation (all mutations 
in a batch are
+   * uniform by construction). The order is significant: the standby-replay 
marker and the
+   * native-replication cluster ids are authoritative and mutually exclusive 
with an originating
+   * client write, so they are checked before falling back to {@code 
_HAGroupName} presence.
+   * @param haGroupName the batch's HA group name as already extracted by
+   *                    {@link #getHAGroupNameFromBatch}
+   * @return the batch origin
+   */
+  BatchOrigin classifyBatch(MiniBatchOperationInProgress<Mutation> miniBatchOp,
+    Optional<String> haGroupName) {
+    if (isReplayBatch(miniBatchOp)) {
+      return BatchOrigin.PHX_REPLAY;
+    }
+    if (miniBatchOp.size() > 0) {
+      List<UUID> clusterIds = miniBatchOp.getOperation(0).getClusterIds();
+      if (clusterIds != null && !clusterIds.isEmpty()) {
+        return BatchOrigin.NATIVE_IN;
+      }
+    }
+    return haGroupName.isPresent() ? BatchOrigin.CLIENT_HA : 
BatchOrigin.CLIENT_NON_HA;
+  }
+
   /**
    * True when the HA group uses the PARALLEL policy, under which the client 
double-writes to both
    * clusters. On a non-active cluster this distinguishes a legitimate 
PARALLEL standby write (which
@@ -1012,7 +1106,7 @@ public class IndexRegionObserver implements 
RegionCoprocessor, RegionObserver {
     if (this.disabled) {
       return;
     }
-    if (!shouldReplicate) {
+    if (!replicateTable) {
       return;
     }
     Map<String, byte[]> walKeyAttrs = getAttributeValuesFromWALKey(logKey);
@@ -1063,7 +1157,9 @@ public class IndexRegionObserver implements 
RegionCoprocessor, RegionObserver {
     // The stamped value is always empty, so re-stamping empty is equivalent 
to copying it through.
     Map<String, byte[]> replicationAttrs = 
MutationCellGrouper.buildReplicationAttributes(
       walKeyAttrs, walKeyAttrs.containsKey(PhoenixIndexCodec.INDEX_UUID));
-    logGroup.append(tableName, -1, replicable, replicationAttrs);
+    // Cold restore path: all cells in a WAL edit share one sequence id, so 
use the WAL key's
+    // sequence id as the per-record commit id.
+    logGroup.append(tableName, logKey.getSequenceId(), replicable, 
replicationAttrs);
     logGroup.sync();
   }
 
@@ -1144,7 +1240,7 @@ public class IndexRegionObserver implements 
RegionCoprocessor, RegionObserver {
           miniBatchOp.setOperationStatus(i, new OperationStatus(SUCCESS, 
result));
           // since this mutation is ignored by setting it's status to success 
in the coproc
           // it shouldn't be synchronously replicated
-          if (this.shouldReplicate) {
+          if (this.replicateTable) {
             m.setAttribute(IGNORE_REPLICATION_ATTRIB, 
IGNORE_REPLICATION_ATTRIB_VAL);
           }
         }
@@ -1729,8 +1825,9 @@ public class IndexRegionObserver implements 
RegionCoprocessor, RegionObserver {
   }
 
   /**
-   * True when this batch is replicated on an active, replication-configured 
cluster: replication is
-   * on and an HA log group is present. Gates the active-side pre-image 
capture — the pre-image
+   * True when this batch must be replicated from this cluster: a resolved HA 
log group is present,
+   * which (per its sole resolution site) already means a CLIENT_HA write on a 
replication-eligible
+   * cluster that is active for the group. Gates the active-side pre-image 
capture — the pre-image
    * exists only so the standby can regenerate its index, so capturing it on a 
non-replicated batch
    * would be wasted work (and an unnecessary region scan on the local path) — 
and the WAL-key
    * replication stamp. It intentionally does NOT gate on {@code 
ignoreSyncReplicationForTesting}:
@@ -1739,7 +1836,12 @@ public class IndexRegionObserver implements 
RegionCoprocessor, RegionObserver {
    * WAL stamp, and the replay path intact so crash-recovery re-ship is 
faithfully exercised.
    */
   private boolean isReplicatedBatch(BatchMutateContext context) {
-    return shouldReplicate && context.logGroup.isPresent();
+    // The log group is resolved (in preBatchMutate) only for a CLIENT_HA 
write on a
+    // replication-eligible cluster that is active for the group, and that 
resolution site is its
+    // sole assignment — so its presence alone is the full predicate for "ship 
this batch". The
+    // "present implies CLIENT_HA" half of that is asserted at context setup in
+    // preBatchMutateWithExceptions.
+    return context.logGroup.isPresent();
   }
 
   /**
@@ -2187,7 +2289,7 @@ public class IndexRegionObserver implements 
RegionCoprocessor, RegionObserver {
       // The rest of this method is for handling global index updates
       context.indexUpdates =
         ArrayListMultimap.<HTableInterfaceReference, Pair<Mutation, byte[]>> 
create();
-      if (context.isReplication) {
+      if (context.isReplay()) {
         // Replicated batches carry per-row pre-images and per-cell timestamps 
from the active.
         // Group by (row, ts) so each active-side batch's mutations are 
processed against their own
         // pre-image — recovers the active-batch boundary the reader's 
coalescing can erase.
@@ -2381,7 +2483,7 @@ public class IndexRegionObserver implements 
RegionCoprocessor, RegionObserver {
    * read it from the mutation.
    */
   private static long indexMutationTimestamp(BatchMutateContext context, 
Mutation m) {
-    return context.isReplication ? IndexUtil.getMaxTimestamp(m) : 
context.batchTimestamp;
+    return context.isReplay() ? IndexUtil.getMaxTimestamp(m) : 
context.batchTimestamp;
   }
 
   private void preparePostIndexMutations(BatchMutateContext context,
@@ -2621,23 +2723,27 @@ public class IndexRegionObserver implements 
RegionCoprocessor, RegionObserver {
   }
 
   public void 
preBatchMutateWithExceptions(ObserverContext<RegionCoprocessorEnvironment> c,
-    MiniBatchOperationInProgress<Mutation> miniBatchOp, 
Optional<ReplicationLogGroup> logGroup)
-    throws Throwable {
+    MiniBatchOperationInProgress<Mutation> miniBatchOp, 
Optional<ReplicationLogGroup> logGroup,
+    BatchOrigin origin) throws Throwable {
     PhoenixIndexMetaData indexMetaData = getPhoenixIndexMetaData(c, 
miniBatchOp);
     BatchMutateContext context = new 
BatchMutateContext(indexMetaData.getClientVersion());
+    context.origin = origin;
     context.logGroup = logGroup;
+    // A resolved log group is produced (in preBatchMutate) only for a 
CLIENT_HA write on a
+    // replication-eligible cluster active for the group, so its presence must 
imply CLIENT_HA. This
+    // is the invariant isReplicatedBatch() relies on to key "ship this batch" 
off log-group
+    // presence
+    // alone; assert it here so a future resolution change can't silently 
break the ship predicate.
+    Preconditions.checkState(!logGroup.isPresent() || origin == 
BatchOrigin.CLIENT_HA,
+      "log group present but batch origin is %s, not CLIENT_HA", origin);
     setBatchMutateContext(c, context);
     identifyIndexMaintainerTypes(indexMetaData, context);
     identifyMutationTypes(miniBatchOp, context);
     context.populateOriginalMutations(miniBatchOp);
-    // The standby reader stamps every reconstructed mutation with 
REPLICATED_MUTATION; checking
-    // the first one is sufficient since the marker is batch-uniform by 
construction.
-    context.isReplication = !context.getOriginalMutations().isEmpty()
-      && 
context.getOriginalMutations().get(0).getAttribute(REPLICATED_MUTATION) != null;
     // Replicated batches must not carry active-side resolution flags. These 
are resolved on the
     // active cluster before replication, so cells reach the standby already 
in their final form.
     Preconditions.checkState(
-      !context.isReplication
+      !context.isReplay()
         || (!context.hasAtomic && !context.returnResult && 
!context.hasConditionalTTL),
       "replicated batch must not carry active-side resolution flags");
 
@@ -2662,7 +2768,7 @@ public class IndexRegionObserver implements 
RegionCoprocessor, RegionObserver {
     // per-row PRE_IMAGE the active shipped, never reading prior state from 
the data-table region.
     // Routing it here keeps every active-only step (row-state scans, 
timestamp assignment, atomic
     // resolution, replication capture) out of a batch that must not run any 
of them.
-    if (context.isReplication) {
+    if (context.isReplay()) {
       preBatchMutateReplication(c, miniBatchOp, context, indexMetaData);
       return;
     }
@@ -2893,21 +2999,21 @@ public class IndexRegionObserver implements 
RegionCoprocessor, RegionObserver {
   }
 
   /**
-   * Stamp the WAL key with everything the WAL-restore re-ship path consumes: 
the HA group name it
-   * keys on ({@link #getHAGroupFromWALKey}) plus the replication attribute 
envelope
-   * (schema/table/tenant, and an empty INDEX_UUID for indexed tables) it 
reads back in
-   * {@link #replicateEditOnWALRestore}. Gated on {@link #isReplicatedBatch} — 
the same predicate
-   * that gates pre-image capture — so the WAL carries this stamp exactly when 
it carries the
-   * pre-images the re-ship needs. On a standby replay the log group is 
absent, so the standby's WAL
-   * never carries replication metadata it could never re-ship.
+   * Stamp the WAL key with everything the WAL-restore re-ship path consumes: 
the replication
+   * attribute envelope (schema/table/tenant, the HA group name it keys on via
+   * {@link #getHAGroupFromWALKey}, and an empty INDEX_UUID for indexed 
tables) it reads back in
+   * {@link #replicateEditOnWALRestore}. The HA group name rides the envelope 
like the other origin
+   * keys (it is in {@code ReplicationLogGroup.REPLICATION_ATTR_KEYS}), so it 
needs no separate
+   * stamp. Gated on {@link #isReplicatedBatch} — the same predicate that 
gates pre-image capture —
+   * so the WAL carries this stamp exactly when it carries the pre-images the 
re-ship needs. On a
+   * standby replay the log group is absent, so the standby's WAL never 
carries replication metadata
+   * it could never re-ship.
    */
   private void appendReplicationAttributesToWALKey(WALKey key,
     IndexRegionObserver.BatchMutateContext context) {
     if (context == null || !isReplicatedBatch(context)) {
       return;
     }
-    IndexRegionObserver.appendToWALKey(key, 
BaseScannerRegionObserverConstants.HA_GROUP_NAME_ATTRIB,
-      Bytes.toBytes(context.logGroup.get().getHAGroupName()));
     for (Map.Entry<String, byte[]> e : 
buildReplicationAttributes(context).entrySet()) {
       IndexRegionObserver.appendToWALKey(key, e.getKey(), e.getValue());
     }
@@ -3574,8 +3680,10 @@ public class IndexRegionObserver implements 
RegionCoprocessor, RegionObserver {
   private void replicateMutations(ReplicationLogGroup logGroup,
     MiniBatchOperationInProgress<Mutation> miniBatchOp, BatchMutateContext 
context)
     throws IOException {
-    // Replicated batches on the standby never re-replicate.
-    if (context.isReplication) {
+    // Replicated batches on the standby never re-replicate. Unreachable in 
practice — the only
+    // caller gates on isReplicatedBatch (origin == CLIENT_HA), which excludes 
PHX_REPLAY — but kept
+    // as a defensive invariant.
+    if (context.isReplay()) {
       return;
     }
     if (context.getOriginalMutations().isEmpty()) {
@@ -3615,9 +3723,12 @@ public class IndexRegionObserver implements 
RegionCoprocessor, RegionObserver {
     if (flattened.isEmpty()) {
       return;
     }
+    // All cells in a miniBatch share one WAL sequence id; capture it from the 
first contributing
+    // data cell (data cells precede any appended pre-image cell) as the 
per-record commit id.
+    long commitId = flattened.get(0).getSequenceId();
     Map<String, byte[]> replicationAttributes = 
buildReplicationAttributes(context);
     try {
-      logGroup.append(dataTableName, -1, flattened, replicationAttributes);
+      logGroup.append(dataTableName, commitId, flattened, 
replicationAttributes);
       logGroup.sync();
     } finally {
       metricSource.updateReplicationSyncTime(dataTableName,
diff --git 
a/phoenix-core-server/src/main/java/org/apache/phoenix/hbase/index/metrics/MetricsHaBypassSource.java
 
b/phoenix-core-server/src/main/java/org/apache/phoenix/hbase/index/metrics/MetricsHaBypassSource.java
index 276f1f2bb6..0f8dc0590f 100644
--- 
a/phoenix-core-server/src/main/java/org/apache/phoenix/hbase/index/metrics/MetricsHaBypassSource.java
+++ 
b/phoenix-core-server/src/main/java/org/apache/phoenix/hbase/index/metrics/MetricsHaBypassSource.java
@@ -20,19 +20,21 @@ package org.apache.phoenix.hbase.index.metrics;
 import org.apache.hadoop.hbase.metrics.BaseSource;
 
 /**
- * Server-side JMX metrics source that counts how many mutation batches pass 
through
- * {@code IndexRegionObserver.preBatchMutate} <em>without</em> a resolvable HA 
group attribute, so
- * the cluster-role-based mutation-block gate has no haGroupName to evaluate 
against and is skipped
- * for that batch.
+ * Server-side JMX metrics source that counts how many client-originated 
mutation batches pass
+ * through {@code IndexRegionObserver.preBatchMutate} <em>without</em> an 
{@code _HAGroupName}
+ * attribute, so the cluster-role-based mutation-block gate has no haGroupName 
to evaluate against
+ * and is skipped for that batch.
  * <p>
  * This is a <strong>path-coverage detector</strong>, not a safety violation 
alarm. The counter is
- * incremented in {@code IndexRegionObserver.preBatchMutate} for 
<em>every</em> mutation batch whose
- * {@code _HAGroupName} attribute cannot be resolved — regardless of whether 
the cluster-role-based
- * mutation-block feature is enabled, regardless of whether a mutation-block 
window is currently
- * active for any HA group, and regardless of whether the table being written 
to is HA-replicated.
- * The "bypass" terminology refers strictly to the gate-evaluation code path 
being short-circuited;
- * it does <strong>not</strong> imply that any safety property was breached — 
when the block feature
- * is disabled or no block window is active, there is no property to breach in 
the first place.
+ * incremented in {@code IndexRegionObserver.preBatchMutate} only for a 
genuine client write that
+ * reached the write path without attaching {@code _HAGroupName} (batch origin
+ * {@code CLIENT_NON_HA}) on a replication-eligible table (the 
sync-replication master switch is on
+ * AND the table is eligible). Standby replay ({@code PHX_REPLAY}) and 
native-replicated-in
+ * ({@code NATIVE_IN}) batches also lack {@code _HAGroupName} but are distinct 
origins and are
+ * <em>excluded</em>, as are system-HA-group state writes (which carry a 
haGroup, i.e.
+ * {@code CLIENT_HA}). The "bypass" terminology refers strictly to the 
gate-evaluation code path
+ * being short-circuited; it does <strong>not</strong> imply that any safety 
property was breached —
+ * the signal is simply "a client missed attaching {@code _HAGroupName}."
  * <p>
  * Intended operator use:
  * <ul>
@@ -58,20 +60,22 @@ public interface MetricsHaBypassSource extends BaseSource {
 
   String BYPASSED_MUTATION_BLOCK_COUNT = "bypassedMutationBlockCount";
   String BYPASSED_MUTATION_BLOCK_COUNT_DESC =
-    "Path-coverage counter: number of mutation batches that reached 
preBatchMutate without a "
-      + "resolvable _HAGroupName attribute (so the cluster-role-based 
mutation-block gate had "
-      + "nothing to evaluate against and was skipped). Counts the code path 
being skipped, not "
-      + "a safety breach — when the block feature is disabled or no block 
window is active, "
-      + "there is no property to breach. Actionable signal is 
delta-against-baseline (e.g., a "
-      + "spike after a deploy introducing a new mutation path), not absolute 
value";
+    "Path-coverage counter: number of client-originated mutation batches that 
reached "
+      + "preBatchMutate on a replication-eligible table without an 
_HAGroupName attribute "
+      + "(origin CLIENT_NON_HA), so the cluster-role-based mutation-block gate 
had nothing to "
+      + "evaluate against and was skipped. Standby-replay and 
native-replicated-in batches are "
+      + "excluded. Counts the code path being skipped, not a safety breach — 
the signal is that "
+      + "a client missed attaching _HAGroupName. Actionable signal is 
delta-against-baseline "
+      + "(e.g., a spike after a deploy introducing a new mutation path), not 
absolute value";
 
   /**
-   * Increments the gate-skipped-path counter. Called unconditionally from
-   * {@code IndexRegionObserver.preBatchMutate} whenever the resolved
-   * {@code Optional<ReplicationLogGroup>} is empty — i.e., the mutation batch 
carries no
-   * {@code _HAGroupName} attribute and the cluster-role-based mutation-block 
gate cannot be
-   * evaluated for it. Independent of whether the mutation-block feature is 
enabled or any block
-   * window is active.
+   * Increments the gate-skipped-path counter. Called from
+   * {@code IndexRegionObserver.preBatchMutate} only for a {@code 
CLIENT_NON_HA} batch on a
+   * replication-eligible table — i.e., a genuine client write that reached 
the write path without
+   * an {@code _HAGroupName} attribute while the sync-replication master 
switch is on and the table
+   * is eligible, so the cluster-role-based mutation-block gate cannot be 
evaluated for it.
+   * Standby-replay ({@code PHX_REPLAY}), native-replicated-in ({@code 
NATIVE_IN}), and
+   * {@code CLIENT_HA} batches are excluded.
    */
   void incrementBypassedMutationBlockCount();
 }
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 8f73a8c886..dbf75c98b3 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
@@ -65,6 +65,7 @@ import org.apache.hadoop.hbase.Abortable;
 import org.apache.hadoop.hbase.Cell;
 import org.apache.hadoop.hbase.ServerName;
 import org.apache.hadoop.hbase.client.Mutation;
+import org.apache.phoenix.coprocessorclient.BaseScannerRegionObserverConstants;
 import org.apache.phoenix.execute.MutationState;
 import org.apache.phoenix.jdbc.ClusterRoleRecord;
 import org.apache.phoenix.jdbc.ClusterType;
@@ -205,11 +206,18 @@ public class ReplicationLogGroup {
    * stamps an empty UUID onto the envelope (see {@code IndexRegionObserver}). 
Keying off the
    * client-set attribute here would make the standby's index regeneration 
depend on client behavior
    * rather than on whether the table actually has indexes.
+   * <p>
+   * {@code HA_GROUP_NAME_ATTRIB} ({@code _HAGroupName}) IS in this list: it 
is a batch-uniform,
+   * client-set attribute that identifies the originating HA group, so it 
belongs with the other
+   * origin-metadata keys. Carrying it through the single envelope makes both 
the log record and the
+   * WAL key (for the WAL-restore path) self-describing, and lets the 
WAL-append path stamp it
+   * uniformly rather than as a special case.
    */
   public static final List<String> REPLICATION_ATTR_KEYS = Collections
     
.unmodifiableList(Arrays.asList(MutationState.MutationMetadataType.SCHEMA_NAME.toString(),
       MutationState.MutationMetadataType.LOGICAL_TABLE_NAME.toString(),
-      MutationState.MutationMetadataType.TENANT_ID.toString()));
+      MutationState.MutationMetadataType.TENANT_ID.toString(),
+      BaseScannerRegionObserverConstants.HA_GROUP_NAME_ATTRIB));
 
   public static final String STANDBY_DIR = "in";
   public static final String FALLBACK_DIR = "out";
@@ -1356,8 +1364,11 @@ public class ReplicationLogGroup {
     private final List<PendingSync> pendingSyncs = new ArrayList<>();
     private ReplicationModeImpl currentModeImpl;
     private volatile IOException fatalException;
-    // Counts events drained per Disruptor batch. Single-threaded access from 
onEvent.
-    private int batchEventCount;
+    // commitId (the HBase WAL sequence id) of the last data record handed to 
a mode via
+    // appendRecord -- covers both the normal event path and the post-failover 
replay path, so it
+    // stays current across a failure-driven mode switch. Logged at mode 
switches so a transition is
+    // correlatable with the WAL position. -1 until the first data record.
+    private long lastCommitId = -1;
     // Counted down by onStart() once the consumer thread has entered its run 
loop (past
     // BatchEventProcessor.run()'s opening clearAlert()). close() awaits this 
before halting so
     // halt()'s alert cannot be swallowed by a clearAlert() that races an 
immediate close.
@@ -1451,7 +1462,8 @@ public class ReplicationLogGroup {
       ReplicationMode newMode = getMode();
       if (newMode != currentModeImpl.getMode()) {
         // some other thread switched the mode on the replication group
-        LOG.info("Mode switched at sequence {} from {} to {}", sequence, 
currentModeImpl, newMode);
+        LOG.info("Mode switched at sequence {} (lastCommitId {}) from {} to 
{}", sequence,
+          lastCommitId, currentModeImpl, newMode);
         // call exit on the last mode here since we can guarantee that the 
lastMode
         // is not processing any event like append/sync because this is the 
only thread
         // that is consuming the events from the ring buffer and handing them 
off to the
@@ -1501,10 +1513,22 @@ public class ReplicationLogGroup {
       replayFailedEvent(failedEvent, sequence);
     }
 
+    /**
+     * Hand a data record to the current mode and advance {@link 
#lastCommitId}. The single append
+     * path for every source (normal events and post-failover replays) so the 
last-seen commitId
+     * stays current across a failure-driven mode switch.
+     */
+    private void appendRecord(Record record) throws IOException {
+      currentModeImpl.append(record);
+      lastCommitId = record.commitId;
+    }
+
     /** Replay all append events which were not yet synced */
     private void replayBatch(List<Record> unsyncedAppends) throws IOException {
+      LOG.info("Replaying {} unsynced records into new mode {}", 
unsyncedAppends.size(),
+        currentModeImpl.getMode());
       for (Record r : unsyncedAppends) {
-        currentModeImpl.append(r);
+        appendRecord(r);
       }
     }
 
@@ -1515,7 +1539,7 @@ public class ReplicationLogGroup {
       // sync event future to the pending future list before the sync event 
can potentially
       // fail.
       if (failedEvent.type == EVENT_TYPE_DATA) {
-        currentModeImpl.append(failedEvent.record);
+        appendRecord(failedEvent.record);
       }
       processPendingSyncs(sequence);
     }
@@ -1553,7 +1577,6 @@ public class ReplicationLogGroup {
       if (event.type == EVENT_TYPE_SYNC) {
         metrics.updateRingBufferTime(currentTimeNs - event.timestampNs);
       }
-      batchEventCount++;
       if (fatalException != null) {
         // Append events are ignored; sync futures are failed immediately
         // so producer threads unblock without waiting for the sync timeout.
@@ -1565,7 +1588,7 @@ public class ReplicationLogGroup {
       try {
         switch (event.type) {
           case EVENT_TYPE_DATA:
-            currentModeImpl.append(event.record);
+            appendRecord(event.record);
             break;
           case EVENT_TYPE_SYNC:
             pendingSyncs.add(new PendingSync(event.syncFuture, currentTimeNs));
@@ -1597,13 +1620,6 @@ public class ReplicationLogGroup {
           new IOException("Unexpected error in event handler at sequence " + 
sequence, t);
         setFatalException(wrapped);
         failPendingSyncs(sequence, wrapped);
-      } finally {
-        // Reset on endOfBatch regardless of success/failure so the counter 
never leaks
-        // across batches when an exception path bypasses processPendingSyncs.
-        if (endOfBatch) {
-          metrics.updateBatchSize(batchEventCount);
-          batchEventCount = 0;
-        }
       }
     }
 
diff --git 
a/phoenix-core-server/src/main/java/org/apache/phoenix/replication/log/LogFileFormatWriter.java
 
b/phoenix-core-server/src/main/java/org/apache/phoenix/replication/log/LogFileFormatWriter.java
index 942e9fa9f3..b52889829b 100644
--- 
a/phoenix-core-server/src/main/java/org/apache/phoenix/replication/log/LogFileFormatWriter.java
+++ 
b/phoenix-core-server/src/main/java/org/apache/phoenix/replication/log/LogFileFormatWriter.java
@@ -42,6 +42,9 @@ public class LogFileFormatWriter implements Closeable {
   private DataOutputStream blockDataStream;
   private long recordCount = 0;
   private long blockCount = 0;
+  // commitId (WAL seqId) of the last appended record, so a writer switch is 
correlatable across
+  // files. -1 until the first append.
+  private long lastCommitId = -1;
   private long blocksStartOffset = -1;
   private CRC64 crc = new CRC64(); // Indirect this when we have more than one 
type
   // Cached buffer for compression for performance
@@ -80,6 +83,7 @@ public class LogFileFormatWriter implements Closeable {
     }
     encoder.write(record);
     recordCount++;
+    lastCommitId = record.getCommitId();
 
     // Check if the current block size exceeds the limit AFTER writing the 
record
     if (currentBlockBytes.size() >= context.getMaxBlockSize()) {
@@ -220,7 +224,8 @@ public class LogFileFormatWriter implements Closeable {
   @Override
   public String toString() {
     return "LogFileFormatWriter [writerContext=" + context + ", recordCount=" 
+ recordCount
-      + ", blockCount=" + blockCount + ", blocksStartOffset=" + 
blocksStartOffset + "]";
+      + ", lastCommitId=" + lastCommitId + ", blockCount=" + blockCount + ", 
blocksStartOffset="
+      + blocksStartOffset + "]";
   }
 
 }
diff --git 
a/phoenix-core-server/src/main/java/org/apache/phoenix/replication/metrics/MetricsReplicationLogGroupSource.java
 
b/phoenix-core-server/src/main/java/org/apache/phoenix/replication/metrics/MetricsReplicationLogGroupSource.java
index 13dd680998..56d9f64184 100644
--- 
a/phoenix-core-server/src/main/java/org/apache/phoenix/replication/metrics/MetricsReplicationLogGroupSource.java
+++ 
b/phoenix-core-server/src/main/java/org/apache/phoenix/replication/metrics/MetricsReplicationLogGroupSource.java
@@ -51,9 +51,6 @@ public interface MetricsReplicationLogGroupSource extends 
BaseSource {
   String FS_SYNC_TIME_DESC =
     "Histogram of time taken for the underlying filesystem sync (fsync) in 
milliseconds";
 
-  String BATCH_SIZE = "phoenixWALBatchSize";
-  String BATCH_SIZE_DESC = "Histogram of number of events drained per 
Disruptor batch";
-
   String PENDING_SYNC_COUNT = "phoenixWALPendingSyncCount";
   String PENDING_SYNC_COUNT_DESC = "Histogram of pending sync futures 
coalesced into one fsync";
 
@@ -102,12 +99,6 @@ public interface MetricsReplicationLogGroupSource extends 
BaseSource {
    */
   void updateFsSyncTime(long timeNs);
 
-  /**
-   * Update the number of events drained in a single Disruptor batch.
-   * @param size Number of events in the batch
-   */
-  void updateBatchSize(long size);
-
   /**
    * Update the number of pending sync futures coalesced into one fsync.
    * @param count Number of sync futures
diff --git 
a/phoenix-core-server/src/main/java/org/apache/phoenix/replication/metrics/MetricsReplicationLogGroupSourceImpl.java
 
b/phoenix-core-server/src/main/java/org/apache/phoenix/replication/metrics/MetricsReplicationLogGroupSourceImpl.java
index 845cb19184..c605eaa449 100644
--- 
a/phoenix-core-server/src/main/java/org/apache/phoenix/replication/metrics/MetricsReplicationLogGroupSourceImpl.java
+++ 
b/phoenix-core-server/src/main/java/org/apache/phoenix/replication/metrics/MetricsReplicationLogGroupSourceImpl.java
@@ -44,7 +44,6 @@ public class MetricsReplicationLogGroupSourceImpl extends 
BaseSourceImpl
   private final MutableTimeHistogram rotationTimeMs;
   private final MutableHistogram ringBufferTimeNs;
   private final MutableTimeHistogram fsSyncTimeMs;
-  private final MutableSizeHistogram batchSize;
   private final MutableSizeHistogram pendingSyncCount;
   private final MutableHistogram pendingSyncWaitTimeNs;
 
@@ -67,7 +66,6 @@ public class MetricsReplicationLogGroupSourceImpl extends 
BaseSourceImpl
     rotationTimeMs = getMetricsRegistry().newTimeHistogram(ROTATION_TIME, 
ROTATION_TIME_DESC);
     ringBufferTimeNs = getMetricsRegistry().newHistogram(RING_BUFFER_TIME, 
RING_BUFFER_TIME_DESC);
     fsSyncTimeMs = getMetricsRegistry().newTimeHistogram(FS_SYNC_TIME, 
FS_SYNC_TIME_DESC);
-    batchSize = getMetricsRegistry().newSizeHistogram(BATCH_SIZE, 
BATCH_SIZE_DESC);
     pendingSyncCount =
       getMetricsRegistry().newSizeHistogram(PENDING_SYNC_COUNT, 
PENDING_SYNC_COUNT_DESC);
     pendingSyncWaitTimeNs =
@@ -119,11 +117,6 @@ public class MetricsReplicationLogGroupSourceImpl extends 
BaseSourceImpl
     fsSyncTimeMs.add(TimeUnit.NANOSECONDS.toMillis(timeNs));
   }
 
-  @Override
-  public void updateBatchSize(long size) {
-    batchSize.add(size);
-  }
-
   @Override
   public void updatePendingSyncCount(long count) {
     pendingSyncCount.add(count);
@@ -156,8 +149,8 @@ public class MetricsReplicationLogGroupSourceImpl extends 
BaseSourceImpl
       
.ringBufferTimeMax(ringSnap.getMax()).ringBufferTimeP50(ringSnap.getMedian())
       
.ringBufferTimeP99(ringSnap.get99thPercentile()).fsSyncTimeMax(fsSnap.getMax())
       
.fsSyncTimeP50(fsSnap.getMedian()).fsSyncTimeP99(fsSnap.get99thPercentile())
-      
.batchSizeMax(batchSize.getMax()).pendingSyncCountMax(pendingSyncCount.getMax())
-      
.pendingSyncWaitTimeMax(pendSnap.getMax()).pendingSyncWaitTimeP50(pendSnap.getMedian())
+      
.pendingSyncCountMax(pendingSyncCount.getMax()).pendingSyncWaitTimeMax(pendSnap.getMax())
+      .pendingSyncWaitTimeP50(pendSnap.getMedian())
       .pendingSyncWaitTimeP99(pendSnap.get99thPercentile()).build();
   }
 
diff --git 
a/phoenix-core-server/src/main/java/org/apache/phoenix/replication/metrics/ReplicationLogMetricValues.java
 
b/phoenix-core-server/src/main/java/org/apache/phoenix/replication/metrics/ReplicationLogMetricValues.java
index c177743d6b..abf93952d3 100644
--- 
a/phoenix-core-server/src/main/java/org/apache/phoenix/replication/metrics/ReplicationLogMetricValues.java
+++ 
b/phoenix-core-server/src/main/java/org/apache/phoenix/replication/metrics/ReplicationLogMetricValues.java
@@ -39,7 +39,6 @@ public class ReplicationLogMetricValues {
   private final long fsSyncTimeMs;
   private final long fsSyncTimeP50Ms;
   private final long fsSyncTimeP99Ms;
-  private final long batchSize;
   private final long pendingSyncCount;
   private final long pendingSyncWaitTimeNs;
   private final long pendingSyncWaitTimeP50Ns;
@@ -60,7 +59,6 @@ public class ReplicationLogMetricValues {
     this.fsSyncTimeMs = b.fsSyncTimeMs;
     this.fsSyncTimeP50Ms = b.fsSyncTimeP50Ms;
     this.fsSyncTimeP99Ms = b.fsSyncTimeP99Ms;
-    this.batchSize = b.batchSize;
     this.pendingSyncCount = b.pendingSyncCount;
     this.pendingSyncWaitTimeNs = b.pendingSyncWaitTimeNs;
     this.pendingSyncWaitTimeP50Ns = b.pendingSyncWaitTimeP50Ns;
@@ -127,10 +125,6 @@ public class ReplicationLogMetricValues {
     return fsSyncTimeP99Ms;
   }
 
-  public long getBatchSizeMax() {
-    return batchSize;
-  }
-
   public long getPendingSyncCountMax() {
     return pendingSyncCount;
   }
@@ -162,7 +156,6 @@ public class ReplicationLogMetricValues {
     private long fsSyncTimeMs;
     private long fsSyncTimeP50Ms;
     private long fsSyncTimeP99Ms;
-    private long batchSize;
     private long pendingSyncCount;
     private long pendingSyncWaitTimeNs;
     private long pendingSyncWaitTimeP50Ns;
@@ -238,11 +231,6 @@ public class ReplicationLogMetricValues {
       return this;
     }
 
-    public Builder batchSizeMax(long v) {
-      this.batchSize = v;
-      return this;
-    }
-
     public Builder pendingSyncCountMax(long v) {
       this.pendingSyncCount = v;
       return this;
diff --git 
a/phoenix-core-server/src/main/java/org/apache/phoenix/replication/reader/ReplicationLogProcessor.java
 
b/phoenix-core-server/src/main/java/org/apache/phoenix/replication/reader/ReplicationLogProcessor.java
index 423b2202bc..d754f49f8e 100644
--- 
a/phoenix-core-server/src/main/java/org/apache/phoenix/replication/reader/ReplicationLogProcessor.java
+++ 
b/phoenix-core-server/src/main/java/org/apache/phoenix/replication/reader/ReplicationLogProcessor.java
@@ -219,7 +219,7 @@ public class ReplicationLogProcessor implements Closeable {
     // Map from Table Name to List of Mutations
     Map<TableName, List<Mutation>> tableToMutationsMap = new HashMap<>();
 
-    // Track the total number of processed records from input log file
+    // Track the total number of mutations replayed from the input log file
     long totalProcessed = 0;
 
     // Track the current batch size as records will be processed in batch size 
of
@@ -275,8 +275,9 @@ public class ReplicationLogProcessor implements Closeable {
         totalProcessed += currentBatchSize;
       }
 
-      LOG.info("Completed processing log file {}. Total mutations processed: 
{}",
-        logFileReader.getContext().getFilePath(), totalProcessed);
+      LOG.info("Completed processing log file {}. Records read: {}, mutations 
replayed: {}",
+        logFileReader.getContext().getFilePath(), 
logFileReader.getContext().getRecordsRead(),
+        totalProcessed);
       getMetrics().incrementLogFileReplaySuccessCount();
       // Replay throughput for files that completed successfully. This runs 
only after the whole
       // file replayed without error (before the catch), so mutations already 
applied by a file that
diff --git 
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/SyncReplicationRolloutFeatureOnMissingGroupIT.java
 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/SyncReplicationRolloutFeatureOnMissingGroupIT.java
new file mode 100644
index 0000000000..78944df564
--- /dev/null
+++ 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/SyncReplicationRolloutFeatureOnMissingGroupIT.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.phoenix.end2end;
+
+import static 
org.apache.phoenix.coprocessorclient.BaseScannerRegionObserverConstants.HA_GROUP_NAME_ATTRIB;
+import static 
org.apache.phoenix.query.QueryConstants.DEFAULT_COLUMN_FAMILY_BYTES;
+import static org.apache.phoenix.query.QueryConstants.EMPTY_COLUMN_BYTES;
+import static org.apache.phoenix.query.QueryConstants.EMPTY_COLUMN_VALUE_BYTES;
+import static 
org.apache.phoenix.query.QueryServices.CLUSTER_ROLE_BASED_MUTATION_BLOCK_ENABLED;
+import static 
org.apache.phoenix.query.QueryServices.HA_GROUP_STALE_FOR_MUTATION_CHECK_ENABLED;
+import static 
org.apache.phoenix.query.QueryServices.SYNCHRONOUS_REPLICATION_ENABLED;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.util.Map;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.HConstants;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.client.ConnectionFactory;
+import org.apache.hadoop.hbase.client.Put;
+import org.apache.hadoop.hbase.client.Table;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.apache.phoenix.query.BaseTest;
+import org.apache.phoenix.util.ReadOnlyProps;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.phoenix.thirdparty.com.google.common.collect.Maps;
+
+/**
+ * Complement of {@link SyncReplicationRolloutStep1IT}: the same 
production-representative HA guard
+ * flags, but with the master switch {@code SYNCHRONOUS_REPLICATION_ENABLED = 
true} while the HA
+ * group has not been created yet — an out-of-order rollout (the feature 
enabled before the group
+ * exists).
+ * <p>
+ * With the feature on, {@code IndexRegionObserver.preBatchMutate} runs the 
cluster-role-based
+ * mutation-block / staleness gate for a {@code CLIENT_HA} write and tries to 
resolve the (absent)
+ * HA group, so the write fails closed. This pins that fail-closed contract, 
ensuring the master
+ * switch must not be flipped on until the HA group is created (rollout Step 2 
before Step 3).
+ */
+@Category(NeedsOwnMiniClusterTest.class)
+public class SyncReplicationRolloutFeatureOnMissingGroupIT extends BaseTest {
+
+  private static final Logger LOG =
+    
LoggerFactory.getLogger(SyncReplicationRolloutFeatureOnMissingGroupIT.class);
+
+  @BeforeClass
+  public static synchronized void doSetup() throws Exception {
+    Map<String, String> props = Maps.newHashMapWithExpectedSize(3);
+    // Feature on, but the HA group does not exist yet; 
production-representative HA guard flags on.
+    props.put(SYNCHRONOUS_REPLICATION_ENABLED, Boolean.TRUE.toString());
+    props.put(CLUSTER_ROLE_BASED_MUTATION_BLOCK_ENABLED, 
Boolean.TRUE.toString());
+    props.put(HA_GROUP_STALE_FOR_MUTATION_CHECK_ENABLED, 
Boolean.TRUE.toString());
+    setUpTestDriver(new ReadOnlyProps(props.entrySet().iterator()));
+  }
+
+  @Test
+  public void 
testWriteWithHAGroupAttributeFailsWhenFeatureEnabledAndGroupMissing()
+    throws Exception {
+    String tableName = generateUniqueName();
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      // COLUMN_ENCODED_BYTES=0 so the raw Put below can use literal column 
qualifiers.
+      conn.createStatement().execute("CREATE TABLE " + tableName
+        + " (id VARCHAR PRIMARY KEY, v VARCHAR) COLUMN_ENCODED_BYTES=0");
+    }
+
+    // Emulate an upgraded client: a well-formed Phoenix row whose mutation 
carries _HAGroupName for
+    // a group that has no server-side HAGroupStoreRecord (the feature was 
enabled before Step 2).
+    Put put = new Put(Bytes.toBytes("row1"));
+    put.addColumn(DEFAULT_COLUMN_FAMILY_BYTES, EMPTY_COLUMN_BYTES, 
EMPTY_COLUMN_VALUE_BYTES);
+    put.addColumn(DEFAULT_COLUMN_FAMILY_BYTES, Bytes.toBytes("V"), 
Bytes.toBytes("hello"));
+    put.setAttribute(HA_GROUP_NAME_ATTRIB, Bytes.toBytes("group_" + 
tableName));
+
+    // Bound client retries so a retriable failure surfaces promptly rather 
than storming.
+    Configuration conf = new Configuration(getUtility().getConfiguration());
+    conf.setInt(HConstants.HBASE_CLIENT_RETRIES_NUMBER, 1);
+    conf.setLong(HConstants.HBASE_CLIENT_OPERATION_TIMEOUT, 60000);
+
+    try (org.apache.hadoop.hbase.client.Connection hcon = 
ConnectionFactory.createConnection(conf);
+      Table htable = hcon.getTable(TableName.valueOf(tableName))) {
+      // Feature on -> the HA gate runs, resolves the absent HA group, and 
fails the write.
+      htable.put(put);
+      fail("write carrying _HAGroupName must fail when the feature is enabled 
but the HA group "
+        + "does not exist yet");
+    } catch (IOException expected) {
+      LOG.info("Write failed as expected for missing HA group", expected);
+    }
+
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      ResultSet rs =
+        conn.createStatement().executeQuery("SELECT v FROM " + tableName + " 
WHERE id = 'row1'");
+      assertFalse("a write that failed closed must not be visible", rs.next());
+    }
+  }
+}
diff --git 
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/SyncReplicationRolloutStep1IT.java
 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/SyncReplicationRolloutStep1IT.java
new file mode 100644
index 0000000000..b6c50b9249
--- /dev/null
+++ 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/SyncReplicationRolloutStep1IT.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.phoenix.end2end;
+
+import static 
org.apache.phoenix.coprocessorclient.BaseScannerRegionObserverConstants.HA_GROUP_NAME_ATTRIB;
+import static 
org.apache.phoenix.query.QueryConstants.DEFAULT_COLUMN_FAMILY_BYTES;
+import static org.apache.phoenix.query.QueryConstants.EMPTY_COLUMN_BYTES;
+import static org.apache.phoenix.query.QueryConstants.EMPTY_COLUMN_VALUE_BYTES;
+import static 
org.apache.phoenix.query.QueryServices.CLUSTER_ROLE_BASED_MUTATION_BLOCK_ENABLED;
+import static 
org.apache.phoenix.query.QueryServices.HA_GROUP_STALE_FOR_MUTATION_CHECK_ENABLED;
+import static 
org.apache.phoenix.query.QueryServices.SYNCHRONOUS_REPLICATION_ENABLED;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.util.Map;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.client.ConnectionFactory;
+import org.apache.hadoop.hbase.client.Put;
+import org.apache.hadoop.hbase.client.Table;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.apache.phoenix.query.BaseTest;
+import org.apache.phoenix.util.ReadOnlyProps;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+import org.apache.phoenix.thirdparty.com.google.common.collect.Maps;
+
+/**
+ * Reproduces the Step-1 window of the synchronous-replication rollout: 
servers are upgraded and the
+ * new write-path code is live, but the feature is still dark
+ * ({@code SYNCHRONOUS_REPLICATION_ENABLED
+ * = false}) and the HA group has not been created yet, while clients — 
upgraded first — already
+ * attach the {@code _HAGroupName} attribute to every mutation.
+ * <p>
+ * Production runs {@code phoenix.cluster.role.based.mutation.block.enabled = 
true} and the
+ * stale-CRR check defaults on, so in this window {@code 
IndexRegionObserver.preBatchMutate} reaches
+ * the cluster-role-based mutation-block / staleness gate — which keys off 
{@code _HAGroupName}
+ * presence, not the feature flag — and resolves the (absent) HA group, 
failing the write.
+ * <p>
+ * The contract this test asserts (design decision D1): {@code 
SYNCHRONOUS_REPLICATION_ENABLED} is
+ * the master switch for all HA semantics, so with the feature off the gate 
must not run and the
+ * write must succeed regardless of the block/stale sub-flags or whether the 
HA group exists.
+ */
+@Category(NeedsOwnMiniClusterTest.class)
+public class SyncReplicationRolloutStep1IT extends BaseTest {
+
+  @BeforeClass
+  public static synchronized void doSetup() throws Exception {
+    Map<String, String> props = Maps.newHashMapWithExpectedSize(3);
+    // Step-1 state: feature dark, but production-representative HA guard 
flags on.
+    props.put(SYNCHRONOUS_REPLICATION_ENABLED, Boolean.FALSE.toString());
+    props.put(CLUSTER_ROLE_BASED_MUTATION_BLOCK_ENABLED, 
Boolean.TRUE.toString());
+    props.put(HA_GROUP_STALE_FOR_MUTATION_CHECK_ENABLED, 
Boolean.TRUE.toString());
+    setUpTestDriver(new ReadOnlyProps(props.entrySet().iterator()));
+  }
+
+  @Test
+  public void 
testWriteWithHAGroupAttributeSucceedsWhenFeatureDisabledAndGroupMissing()
+    throws Exception {
+    String tableName = generateUniqueName();
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      // COLUMN_ENCODED_BYTES=0 so the raw Put below can use literal column 
qualifiers.
+      conn.createStatement().execute("CREATE TABLE " + tableName
+        + " (id VARCHAR PRIMARY KEY, v VARCHAR) COLUMN_ENCODED_BYTES=0");
+    }
+
+    // Emulate an upgraded client: a well-formed Phoenix row whose mutation 
carries _HAGroupName for
+    // a group that has no server-side HAGroupStoreRecord (not created until 
rollout Step 2).
+    Put put = new Put(Bytes.toBytes("row1"));
+    put.addColumn(DEFAULT_COLUMN_FAMILY_BYTES, EMPTY_COLUMN_BYTES, 
EMPTY_COLUMN_VALUE_BYTES);
+    put.addColumn(DEFAULT_COLUMN_FAMILY_BYTES, Bytes.toBytes("V"), 
Bytes.toBytes("hello"));
+    put.setAttribute(HA_GROUP_NAME_ATTRIB, Bytes.toBytes("group_" + 
tableName));
+
+    try (
+      org.apache.hadoop.hbase.client.Connection hcon =
+        ConnectionFactory.createConnection(getUtility().getConfiguration());
+      Table htable = hcon.getTable(TableName.valueOf(tableName))) {
+      // With the feature off this must not evaluate the HA gate for the 
missing group.
+      htable.put(put);
+    }
+
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      ResultSet rs =
+        conn.createStatement().executeQuery("SELECT v FROM " + tableName + " 
WHERE id = 'row1'");
+      assertTrue("row written with the _HAGroupName attribute must be 
visible", rs.next());
+      assertEquals("hello", rs.getString(1));
+    }
+  }
+}
diff --git 
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/BypassedMutationBlockMetricsIT.java
 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/BypassedMutationBlockMetricsIT.java
index 4db777d287..aad94f4d40 100644
--- 
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/BypassedMutationBlockMetricsIT.java
+++ 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/BypassedMutationBlockMetricsIT.java
@@ -35,9 +35,12 @@ import org.junit.experimental.categories.Category;
 
 /**
  * Integration test for the server-side {@code bypassedMutationBlockCount} JMX 
counter. A "bypass"
- * is a mutation batch that reaches {@code IndexRegionObserver.preBatchMutate} 
without an associated
- * HA group attribute, causing the cluster-role-based mutation-block gate to 
be skipped. This IT
- * drives a write that does not carry {@code _HAGroupName} and asserts the 
counter increments.
+ * is a client-originated mutation batch (origin {@code CLIENT_NON_HA}) that 
reaches
+ * {@code IndexRegionObserver.preBatchMutate} on a replication-eligible table 
without an
+ * {@code _HAGroupName} attribute, causing the cluster-role-based 
mutation-block gate to be skipped;
+ * standby-replay and native-replicated-in batches are excluded. This IT 
relies on {@link HABaseIT}
+ * enabling the sync-replication master switch on both clusters, then drives a 
write that does not
+ * carry {@code _HAGroupName} and asserts the counter increments.
  */
 @Category(NeedsOwnMiniClusterTest.class)
 public class BypassedMutationBlockMetricsIT extends HABaseIT {
diff --git 
a/phoenix-core/src/it/java/org/apache/phoenix/replication/ReplicationLogGroupBaseIT.java
 
b/phoenix-core/src/it/java/org/apache/phoenix/replication/ReplicationLogGroupBaseIT.java
index 5ac326789e..e00a046d0a 100644
--- 
a/phoenix-core/src/it/java/org/apache/phoenix/replication/ReplicationLogGroupBaseIT.java
+++ 
b/phoenix-core/src/it/java/org/apache/phoenix/replication/ReplicationLogGroupBaseIT.java
@@ -21,7 +21,6 @@ import static 
org.apache.phoenix.hbase.index.IndexRegionObserver.PHOENIX_INDEX_C
 import static 
org.apache.phoenix.jdbc.HighAvailabilityGroup.PHOENIX_HA_GROUP_ATTR;
 import static 
org.apache.phoenix.jdbc.HighAvailabilityTestingUtility.getHighAvailibilityGroup;
 import static 
org.apache.phoenix.jdbc.PhoenixDatabaseMetaData.SYSTEM_CATALOG_NAME;
-import static 
org.apache.phoenix.query.QueryServices.SYNCHRONOUS_REPLICATION_ENABLED;
 import static 
org.apache.phoenix.replication.ReplicationShardDirectoryManager.PHOENIX_REPLICATION_ROUND_DURATION_SECONDS_KEY;
 import static 
org.apache.phoenix.replication.reader.ReplicationLogReplayService.PHOENIX_REPLICATION_REPLAY_ENABLED;
 import static org.junit.Assert.assertArrayEquals;
@@ -102,8 +101,6 @@ public abstract class ReplicationLogGroupBaseIT extends 
HABaseIT {
     conf1.setBoolean(PHOENIX_REPLICATION_REPLAY_ENABLED, false);
     // Disable replay on cluster 2, we will explicitly replay the log files
     conf2.setBoolean(PHOENIX_REPLICATION_REPLAY_ENABLED, false);
-    // Disable writer on cluster 2
-    conf2.setBoolean(SYNCHRONOUS_REPLICATION_ENABLED, false);
     CLUSTERS.start();
     DriverManager.registerDriver(PhoenixDriver.INSTANCE);
   }
@@ -120,7 +117,7 @@ public abstract class ReplicationLogGroupBaseIT extends 
HABaseIT {
     haGroupName = name.getMethodName();
     clientProps = HighAvailabilityTestingUtility.getHATestProperties();
     clientProps.setProperty(PHOENIX_HA_GROUP_ATTR, haGroupName);
-    CLUSTERS.initClusterRole(haGroupName, HighAvailabilityPolicy.FAILOVER);
+    CLUSTERS.initClusterRole(haGroupName, getHAPolicy());
     haGroup = getHighAvailibilityGroup(CLUSTERS.getJdbcHAUrl(), clientProps);
     LOG.info("Initialized haGroup {} with URL {}", haGroup, 
CLUSTERS.getJdbcHAUrl());
     logGroup = getReplicationLogGroup();
@@ -133,6 +130,14 @@ public abstract class ReplicationLogGroupBaseIT extends 
HABaseIT {
     LOG.info("Ending cleanup for test {}", name.getMethodName());
   }
 
+  /**
+   * HA policy for the per-test group. Defaults to FAILOVER; a subclass 
overrides this to exercise
+   * the PARALLEL role/policy path (e.g. a double-write reaching a non-active 
PARALLEL cluster).
+   */
+  protected HighAvailabilityPolicy getHAPolicy() {
+    return HighAvailabilityPolicy.FAILOVER;
+  }
+
   private ReplicationLogGroup getReplicationLogGroup() throws IOException {
     HRegionServer rs = 
CLUSTERS.getHBaseCluster1().getHBaseCluster().getRegionServer(0);
     return ReplicationLogGroup.get(conf1, rs.getServerName(), haGroupName)
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 4b5bd6c1d3..a512b79389 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
@@ -56,6 +56,8 @@ import org.apache.hadoop.hbase.util.JVMClusterUtil;
 import org.apache.hadoop.hbase.util.Threads;
 import org.apache.phoenix.end2end.NeedsOwnMiniClusterTest;
 import org.apache.phoenix.hbase.index.IndexRegionObserver;
+import org.apache.phoenix.hbase.index.metrics.MetricsHaBypassSourceFactory;
+import org.apache.phoenix.hbase.index.metrics.MetricsHaBypassSourceImpl;
 import org.apache.phoenix.jdbc.ClusterRoleRecord;
 import org.apache.phoenix.jdbc.FailoverPhoenixConnection;
 import org.apache.phoenix.jdbc.PhoenixResultSet;
@@ -305,6 +307,85 @@ public class ReplicationLogGroupIT extends 
ReplicationLogGroupBaseIT {
     }
   }
 
+  /**
+   * Negative counter case for PHOENIX-8011: standby replay batches (origin 
{@code PHX_REPLAY})
+   * carry no {@code _HAGroupName} and previously inflated {@code 
bypassedMutationBlockCount}, which
+   * is meant to flag only live client writes that forgot the attribute. This 
drives an HA write on
+   * cluster 1, then replays its log onto cluster 2 and asserts the counter 
does not move across the
+   * replay -- snapshotting tightly around the replay loop only, since the 
cluster-2 schema DDL runs
+   * on a non-HA connection ({@code CLIENT_NON_HA}) and would itself tick the 
counter. Cross-cluster
+   * equality proves the replay actually pushed batches through the standby 
IRO, so the assertion is
+   * not vacuous. Complements {@code BypassedMutationBlockMetricsIT}, which 
covers the positive
+   * ({@code CLIENT_NON_HA} increments) case.
+   */
+  @Test
+  public void testReplayBatchDoesNotIncrementBypassCounter() throws Exception {
+    final String tableName = "T_" + generateUniqueName();
+    final String indexName = "I_" + generateUniqueName();
+    final String createTableDdl = String.format(
+      "create table if not exists %s (id integer not null primary key, val1 
varchar, val2 varchar)",
+      tableName);
+    final String createIndexDdl =
+      String.format("create index if not exists %s on %s (val1)", indexName, 
tableName);
+
+    try (FailoverPhoenixConnection conn = (FailoverPhoenixConnection) 
DriverManager
+      .getConnection(CLUSTERS.getJdbcHAUrl(), clientProps)) {
+      conn.createStatement().execute(createTableDdl);
+      conn.createStatement().execute(createIndexDdl);
+      conn.commit();
+      PreparedStatement stmt =
+        conn.prepareStatement("upsert into " + tableName + " VALUES(?, ?, ?)");
+      for (int i = 0; i < 10; ++i) {
+        stmt.setInt(1, i);
+        stmt.setString(2, "val1_" + i);
+        stmt.setString(3, "val2_" + i);
+        stmt.executeUpdate();
+      }
+      conn.commit();
+    }
+
+    // Quiesce the group so every log file has a durable trailer before the 
reader recovers its
+    // lease (see replayAndVerifyAcrossClusters).
+    Path standByLogDir = 
logGroup.getOrCreatePeerShardManager().getRootDirectoryPath();
+    logGroup.close();
+
+    // Create the same schema on cluster 2 before replay. This uses a non-HA 
connection, so its
+    // catalog writes are CLIENT_NON_HA and may tick the counter -- hence the 
snapshot is taken
+    // after this step, not before.
+    try (Connection conn2 = CLUSTERS.getCluster2Connection(haGroup)) {
+      conn2.createStatement().execute(createTableDdl);
+      conn2.createStatement().execute(createIndexDdl);
+      conn2.commit();
+    }
+
+    FileSystem fs = standByLogDir.getFileSystem(conf2);
+    List<Path> logFiles = findLogFiles(standByLogDir, fs);
+    assertFalse("Should have at least one log file", logFiles.isEmpty());
+
+    MetricsHaBypassSourceImpl source =
+      (MetricsHaBypassSourceImpl) MetricsHaBypassSourceFactory.getInstance();
+    long before = source.getBypassedMutationBlockCountForTesting();
+
+    ReplicationLogProcessor processor = ReplicationLogProcessor.get(conf2, 
haGroupName);
+    try {
+      for (Path logFile : logFiles) {
+        processor.processLogFile(fs, logFile);
+      }
+    } finally {
+      processor.close();
+    }
+
+    long after = source.getBypassedMutationBlockCountForTesting();
+    assertEquals(
+      "standby replay (PHX_REPLAY) batches must not increment 
bypassedMutationBlockCount", before,
+      after);
+
+    // Prove the replay actually drove batches through the standby IRO, so the 
counter assertion is
+    // meaningful and not passing on a no-op replay.
+    assertTablesEqualAcrossClusters(tableName);
+    assertTablesEqualAcrossClusters(indexName);
+  }
+
   /**
    * Local-index-only variant of {@link #testAppendAndSync}: the table has a 
local index but no
    * global/uncovered/transform index. Such a table never enters the 
global-index branch that
diff --git 
a/phoenix-core/src/it/java/org/apache/phoenix/replication/ReplicationLogGroupParallelIT.java
 
b/phoenix-core/src/it/java/org/apache/phoenix/replication/ReplicationLogGroupParallelIT.java
new file mode 100644
index 0000000000..ae3b4047bf
--- /dev/null
+++ 
b/phoenix-core/src/it/java/org/apache/phoenix/replication/ReplicationLogGroupParallelIT.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.phoenix.replication;
+
+import static org.apache.phoenix.query.BaseTest.generateUniqueName;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import org.apache.phoenix.end2end.NeedsOwnMiniClusterTest;
+import org.apache.phoenix.jdbc.HighAvailabilityPolicy;
+import org.apache.phoenix.jdbc.ParallelPhoenixConnection;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+/**
+ * PARALLEL-policy coverage for the write-path role/policy branch in
+ * {@code IndexRegionObserver.getReplicationLogGroup}. Under PARALLEL the 
client double-writes to
+ * both clusters via a {@link ParallelPhoenixConnection}; the leg that lands 
on the non-active
+ * cluster must commit locally and skip re-ship (the {@code isParallelPolicy} 
branch returns empty)
+ * rather than reject the write as split-brain — which is the FAILOVER 
behavior covered by
+ * {@link ReplicationLogGroupIT}. Both clusters run with the replication 
feature enabled, so the
+ * standby actually reaches this branch.
+ */
+@Category(NeedsOwnMiniClusterTest.class)
+public class ReplicationLogGroupParallelIT extends ReplicationLogGroupBaseIT {
+
+  @BeforeClass
+  public static void doSetup() throws Exception {
+    setupClusters();
+  }
+
+  @Override
+  protected HighAvailabilityPolicy getHAPolicy() {
+    return HighAvailabilityPolicy.PARALLEL;
+  }
+
+  @Test
+  public void 
testParallelDoubleWriteCommitsOnNonActiveClusterWithoutSplitBrainThrow()
+    throws Exception {
+    final String tableName = "T_" + generateUniqueName();
+    final int rowCount = 10;
+
+    // Create the table on both clusters (schema: id INTEGER PK, v INTEGER). 
replicationScope=false
+    // disables HBase-native replication, so cluster2's rows come only from 
the parallel
+    // double-write's standby leg -- isolating the PARALLEL branch under test.
+    CLUSTERS.createTableOnClusterPair(haGroup, tableName, false);
+
+    // PARALLEL policy => a ParallelPhoenixConnection that writes to both 
clusters. The leg landing
+    // on the non-active cluster must commit locally (isParallelPolicy 
branch), not throw
+    // split-brain.
+    try (Connection conn = 
DriverManager.getConnection(CLUSTERS.getJdbcHAUrl(), clientProps)) {
+      assertTrue("expected a ParallelPhoenixConnection under PARALLEL policy",
+        conn instanceof ParallelPhoenixConnection);
+      PreparedStatement stmt = conn.prepareStatement("upsert into " + 
tableName + " values (?, ?)");
+      for (int i = 0; i < rowCount; i++) {
+        stmt.setInt(1, i);
+        stmt.setInt(2, i);
+        stmt.executeUpdate();
+      }
+      conn.commit();
+    }
+
+    // Rows present on BOTH clusters => the double-write committed on the 
non-active cluster too,
+    // i.e. its PARALLEL leg took the commit-locally path rather than a 
split-brain rejection.
+    assertRowCount(CLUSTERS.getCluster1Connection(haGroup), tableName, 
rowCount);
+    assertRowCount(CLUSTERS.getCluster2Connection(haGroup), tableName, 
rowCount);
+  }
+
+  private static void assertRowCount(Connection conn, String tableName, int 
expected)
+    throws Exception {
+    try (Connection c = conn;
+      ResultSet rs = c.createStatement().executeQuery("select count(*) from " 
+ tableName)) {
+      assertTrue(rs.next());
+      assertEquals(expected, rs.getInt(1));
+    }
+  }
+}
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/hbase/index/IndexRegionObserverReplayTest.java
 
b/phoenix-core/src/test/java/org/apache/phoenix/hbase/index/IndexRegionObserverReplayTest.java
index 651eb54dde..008f4d68b0 100644
--- 
a/phoenix-core/src/test/java/org/apache/phoenix/hbase/index/IndexRegionObserverReplayTest.java
+++ 
b/phoenix-core/src/test/java/org/apache/phoenix/hbase/index/IndexRegionObserverReplayTest.java
@@ -19,6 +19,7 @@ package org.apache.phoenix.hbase.index;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -27,13 +28,18 @@ import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
 import org.apache.hadoop.hbase.CellUtil;
 import org.apache.hadoop.hbase.DoNotRetryIOException;
 import org.apache.hadoop.hbase.HConstants;
 import org.apache.hadoop.hbase.client.Delete;
 import org.apache.hadoop.hbase.client.Mutation;
 import org.apache.hadoop.hbase.client.Put;
+import org.apache.hadoop.hbase.regionserver.MiniBatchOperationInProgress;
+import org.apache.hadoop.hbase.regionserver.OperationStatus;
 import org.apache.hadoop.hbase.util.Bytes;
+import org.apache.hadoop.hbase.wal.WALEdit;
 import org.junit.Test;
 
 /**
@@ -72,6 +78,114 @@ public class IndexRegionObserverReplayTest {
     return m;
   }
 
+  /** Wraps the given mutations in a MiniBatchOperationInProgress spanning the 
whole array. */
+  private static MiniBatchOperationInProgress<Mutation> batchOf(Mutation... 
mutations) {
+    return new MiniBatchOperationInProgress<>(mutations, new 
OperationStatus[mutations.length],
+      new WALEdit[mutations.length], 0, mutations.length, mutations.length);
+  }
+
+  // ---- isReplayBatch ----
+
+  @Test
+  public void testIsReplayBatchTrueWhenFirstMutationCarriesMarker() {
+    Put replayed = putRowTs(ROW, TS, Q1);
+    replayed.setAttribute(IndexRegionObserver.REPLICATED_MUTATION, 
Bytes.toBytes(true));
+    assertTrue("a batch whose first mutation carries REPLICATED_MUTATION is a 
replay batch",
+      iro.isReplayBatch(batchOf(replayed)));
+  }
+
+  @Test
+  public void testIsReplayBatchFalseForLiveWrite() {
+    // A live client write with no REPLICATED_MUTATION marker (and, as here, 
no _HAGroupName)
+    // must not be treated as replay -- this is exactly the case the bypass 
counter must count.
+    assertFalse("a batch without the marker is not a replay batch",
+      iro.isReplayBatch(batchOf(putRowTs(ROW, TS, Q1))));
+  }
+
+  @Test
+  public void testIsReplayBatchFalseForEmptyBatch() {
+    assertFalse("an empty batch is not a replay batch", 
iro.isReplayBatch(batchOf()));
+  }
+
+  // ---- classifyBatch ----
+
+  @Test
+  public void testClassifyBatchClientHaWhenHAGroupPresent() {
+    assertEquals(IndexRegionObserver.BatchOrigin.CLIENT_HA,
+      iro.classifyBatch(batchOf(putRowTs(ROW, TS, Q1)), Optional.of("g")));
+  }
+
+  @Test
+  public void testClassifyBatchClientNonHaWhenNoHAGroup() {
+    assertEquals(IndexRegionObserver.BatchOrigin.CLIENT_NON_HA,
+      iro.classifyBatch(batchOf(putRowTs(ROW, TS, Q1)), Optional.empty()));
+  }
+
+  @Test
+  public void testClassifyBatchNativeInWhenClusterIdsPresent() {
+    // A write replicated in via HBase-native replication carries source 
cluster ids and no
+    // _HAGroupName -- it must not be mistaken for a client write that forgot 
the attribute.
+    Put p = putRowTs(ROW, TS, Q1);
+    p.setClusterIds(Collections.singletonList(UUID.randomUUID()));
+    assertEquals(IndexRegionObserver.BatchOrigin.NATIVE_IN,
+      iro.classifyBatch(batchOf(p), Optional.empty()));
+  }
+
+  @Test
+  public void testClassifyBatchReplayMarkerWinsOverClusterIds() {
+    // The replay marker is authoritative and checked first.
+    Put p = putRowTs(ROW, TS, Q1);
+    p.setClusterIds(Collections.singletonList(UUID.randomUUID()));
+    p.setAttribute(IndexRegionObserver.REPLICATED_MUTATION, 
Bytes.toBytes(true));
+    assertEquals(IndexRegionObserver.BatchOrigin.PHX_REPLAY,
+      iro.classifyBatch(batchOf(p), Optional.empty()));
+  }
+
+  @Test
+  public void testClassifyBatchClusterIdsWinOverHAGroup() {
+    // Origin precedence: native-replicated-in (cluster ids) before 
_HAGroupName presence.
+    Put p = putRowTs(ROW, TS, Q1);
+    p.setClusterIds(Collections.singletonList(UUID.randomUUID()));
+    assertEquals(IndexRegionObserver.BatchOrigin.NATIVE_IN,
+      iro.classifyBatch(batchOf(p), Optional.of("g")));
+  }
+
+  @Test
+  public void testClassifyBatchEmptyIsClientNonHa() {
+    assertEquals(IndexRegionObserver.BatchOrigin.CLIENT_NON_HA,
+      iro.classifyBatch(batchOf(), Optional.empty()));
+  }
+
+  @Test
+  public void 
testClassifyBatchFirstMutationIsAuthoritativeForMultiMutationBatch() {
+    // A batch is uniform in origin by construction (the standby reader stamps 
every reconstructed
+    // mutation), so classifyBatch/isReplayBatch inspect only the first 
mutation. Prove that
+    // contract holds for a multi-mutation batch: the replay marker on the 
FIRST mutation classifies
+    // the whole batch as PHX_REPLAY even though the trailing mutations carry 
no marker.
+    Put replayed = putRowTs(ROW, TS, Q1);
+    replayed.setAttribute(IndexRegionObserver.REPLICATED_MUTATION, 
Bytes.toBytes(true));
+    Put unmarked = putRowTs(R2, TS, Q2);
+    assertEquals("first mutation's marker classifies the whole batch",
+      IndexRegionObserver.BatchOrigin.PHX_REPLAY,
+      iro.classifyBatch(batchOf(replayed, unmarked), Optional.empty()));
+    assertTrue("isReplayBatch keys off the first mutation for a multi-mutation 
batch",
+      iro.isReplayBatch(batchOf(replayed, unmarked)));
+  }
+
+  @Test
+  public void testReplayBatchIsNotBypassCounterCandidate() {
+    // The bypassedMutationBlockCount gate fires only for CLIENT_NON_HA. A 
standby replay batch
+    // carries REPLICATED_MUTATION and classifies as PHX_REPLAY -- never 
CLIENT_NON_HA -- so the
+    // counter is not incremented for replayed mutations.
+    Put replayed = putRowTs(ROW, TS, Q1);
+    replayed.setAttribute(IndexRegionObserver.REPLICATED_MUTATION, 
Bytes.toBytes(true));
+    IndexRegionObserver.BatchOrigin origin = 
iro.classifyBatch(batchOf(replayed), Optional.empty());
+    assertEquals("a replay batch classifies as PHX_REPLAY",
+      IndexRegionObserver.BatchOrigin.PHX_REPLAY, origin);
+    assertNotEquals("a replay batch must not be the counter-triggering origin",
+      IndexRegionObserver.BatchOrigin.CLIENT_NON_HA, origin);
+  }
+
   // ---- decodePreImage ----
 
   @Test
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 3dfde73c26..2b337cbd6a 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
@@ -2844,12 +2844,12 @@ public class ReplicationLogGroupTest extends 
ReplicationLogBaseTest {
       long componentSumNs = ringBufferTimeNs + pendingSyncWaitTimeNs + 
fsSyncTimeNs;
       long truncationToleranceNs = TimeUnit.MILLISECONDS.toNanos(1);
       LOG.info(
-        "Metrics snapshot: maxBatchSize={} maxPendingSyncCount={}"
-          + " syncTime[p50={}ms p99={}ms max={}ms]" + " ringBuffer[p50={}ns 
p99={}ns max={}ns]"
-          + " fsSync[p50={}ms p99={}ms max={}ms]" + " pendingSyncWait[p50={}ns 
p99={}ns max={}ns]"
+        "Metrics snapshot: maxPendingSyncCount={}" + " syncTime[p50={}ms 
p99={}ms max={}ms]"
+          + " ringBuffer[p50={}ns p99={}ns max={}ns]" + " fsSync[p50={}ms 
p99={}ms max={}ms]"
+          + " pendingSyncWait[p50={}ns p99={}ns max={}ns]"
           + " maxComponentSumNs={} syncTimeWithinBound={}",
-        metricValues.getBatchSizeMax(), metricValues.getPendingSyncCountMax(),
-        metricValues.getSyncTimeP50(), metricValues.getSyncTimeP99(), 
metricValues.getSyncTimeMax(),
+        metricValues.getPendingSyncCountMax(), metricValues.getSyncTimeP50(),
+        metricValues.getSyncTimeP99(), metricValues.getSyncTimeMax(),
         metricValues.getRingBufferTimeP50(), 
metricValues.getRingBufferTimeP99(),
         metricValues.getRingBufferTimeMax(), metricValues.getFsSyncTimeP50(),
         metricValues.getFsSyncTimeP99(), metricValues.getFsSyncTimeMax(),
@@ -2919,8 +2919,6 @@ public class ReplicationLogGroupTest extends 
ReplicationLogBaseTest {
     assertTrue("pendingSyncWaitTime should be >= 0, got " + 
values.getPendingSyncWaitTimeMax(),
       values.getPendingSyncWaitTimeMax() >= 0);
     // Counts.
-    assertTrue("batchSize should be > 0, got " + values.getBatchSizeMax(),
-      values.getBatchSizeMax() > 0);
     assertTrue("pendingSyncCount should be > 0, got " + 
values.getPendingSyncCountMax(),
       values.getPendingSyncCountMax() > 0);
     // Millisecond-resolution timers: the injected fsync delay clears the 
truncation floor.
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/replication/log/LogFileCodecTest.java
 
b/phoenix-core/src/test/java/org/apache/phoenix/replication/log/LogFileCodecTest.java
index c3dfb162d3..6e5862ca63 100644
--- 
a/phoenix-core/src/test/java/org/apache/phoenix/replication/log/LogFileCodecTest.java
+++ 
b/phoenix-core/src/test/java/org/apache/phoenix/replication/log/LogFileCodecTest.java
@@ -41,6 +41,7 @@ import org.apache.hadoop.hbase.client.Delete;
 import org.apache.hadoop.hbase.client.Mutation;
 import org.apache.hadoop.hbase.client.Put;
 import org.apache.hadoop.hbase.util.Bytes;
+import org.apache.phoenix.coprocessorclient.BaseScannerRegionObserverConstants;
 import org.apache.phoenix.execute.MutationState;
 import org.apache.phoenix.hbase.index.IndexRegionObserver;
 import org.apache.phoenix.index.PhoenixIndexCodec;
@@ -760,6 +761,8 @@ public class LogFileCodecTest {
       Bytes.toBytes("MY_TABLE"));
     put.setAttribute(MutationState.MutationMetadataType.TENANT_ID.toString(),
       Bytes.toBytes("tenant1"));
+    put.setAttribute(BaseScannerRegionObserverConstants.HA_GROUP_NAME_ATTRIB,
+      Bytes.toBytes("MY_HA_GROUP"));
     put.setAttribute(IndexRegionObserver.REPLICATED_MUTATION, 
HConstants.EMPTY_BYTE_ARRAY);
 
     LogFile.Record original =
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/replication/metrics/MetricsReplicationLogGroupSourceImplTest.java
 
b/phoenix-core/src/test/java/org/apache/phoenix/replication/metrics/MetricsReplicationLogGroupSourceImplTest.java
index a53c54280e..2b6699df50 100644
--- 
a/phoenix-core/src/test/java/org/apache/phoenix/replication/metrics/MetricsReplicationLogGroupSourceImplTest.java
+++ 
b/phoenix-core/src/test/java/org/apache/phoenix/replication/metrics/MetricsReplicationLogGroupSourceImplTest.java
@@ -95,11 +95,9 @@ public class MetricsReplicationLogGroupSourceImplTest {
   }
 
   @Test
-  public void testBatchSizeAndPendingSyncCount() {
-    source.updateBatchSize(100L);
+  public void testPendingSyncCount() {
     source.updatePendingSyncCount(50L);
     ReplicationLogMetricValues v = source.getCurrentMetricValues();
-    assertEquals(100L, v.getBatchSizeMax());
     assertEquals(50L, v.getPendingSyncCountMax());
   }
 }

Reply via email to