This is an automated email from the ASF dual-hosted git repository.
lokiore 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 523a58b7f7 PHOENIX-7995 Tag client HA metrics with the HA group and
add missing HA/CRR/failover client metrics (#2609)
523a58b7f7 is described below
commit 523a58b7f7144535a3b17cc974a2b8af582d0a5b
Author: Lokesh Khurana <[email protected]>
AuthorDate: Tue Sep 1 09:58:06 2026 -0700
PHOENIX-7995 Tag client HA metrics with the HA group and add missing
HA/CRR/failover client metrics (#2609)
* PHOENIX-7872 Addendum record HA failover duration on the CRR-write path
and add a connection-failed counter
The HA failover observability metrics added under PHOENIX-7872 recorded
HA_FAILOVER_DURATION_MS inside FailoverPhoenixConnection.failover(long).
That
method is only reached through wrapActionDuringFailover -> FailoverPolicy
.shouldFailover(), which returns false under the default
ExplicitFailoverPolicy,
or through the explicit static failover(Connection, long) helper. Neither
runs
during an autonomous, CRR-driven failover, so the duration metric never
moved in
production.
Move the duration measurement to the path that actually drives failovers:
refreshClusterRoleRecord, where the cluster-role transition is dispatched
and
where HA_FAILOVER_COUNT is already gated by shouldCountFailover. The
dispatch
block is wrapped in a try/finally so the duration is recorded on every exit
(success, timeout, policy failure, or interrupt), avoiding a silent metric
miss
if a future exit path is added. The now-dead timing in failover(long) is
removed.
Add HA_FAILOVER_CONNECTION_FAILED_COUNTER, incremented at the single
SQLException
throw funnel in connectActive (no active cluster, cluster demoted
mid-connect, or
the underlying connect threw). This tracks real active-cluster connection
failures
regardless of the configured failover policy.
Tests: three unit tests in HighAvailabilityGroupTest -- a counted role-flip
transition records both HA_FAILOVER_COUNT and an HA_FAILOVER_DURATION_MS
sample on
the CRR-write path; a failed connectActive increments the connection-failed
counter; a successful connectActive leaves it unchanged.
HighAvailabilityGroupTest
16/16, FailoverPhoenixConnectionTest 8/8.
Generated-by: Claude Code (Opus 4.8)
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* PHOENIX-7995 Tag client HA metrics with the HA group and add missing
HA/CRR/failover client metrics
The ZK-less HA client emits all HA/CRR/failover metrics as JVM-global
GLOBAL_HA_* counters, so a JVM connected to more than one HA group cannot
attribute a failover, stale-CRR detection, or poller failure to a specific
group. It also lacks client counters for several HA/CRR/failover events that
are relevant on the ZK-less path.
Per-HA-group tagging
--------------------
Add HAGroupClientMetricsSource, a per-group Hadoop Metrics2 source (one per
HA
group name) that stamps a "ha_group" tag carrying the group name and
appends the
quoted group name to its JMX context so each group registers as a distinct
source/MBean. HAGroupMetricsManager is the process-wide registry: it lazily
creates a source per group (only when global client metrics are enabled),
routes per-group increment/update, and detaches the source on HA-group
close so
the same group can re-register later. Creating a source registers it with
the
metrics system and can throw; getOrCreate swallows that failure (logs and
returns null) so metrics registration never breaks a caller such as
HighAvailabilityGroup.init(). Emission is dual: every group-attributable
GLOBAL_HA_* increment is mirrored to the group's source, and the JVM-global
counters continue to emit unchanged.
The tag key lives in a new module-neutral MetricConstants.HA_GROUP_TAG_NAME
("ha_group") referenced by both this client source and the server-side
HAGroupStoreMetricsSource, so both sides tag with the same key and can be
filtered together downstream.
New client counters
-------------------
- HA_FAILOVER_CONNECTION_CREATED_COUNTER: FailoverPhoenixConnection
instances
successfully created against the active cluster; one increment per
constructed
connection. This is not a matched pair with the connection-failed counter,
which increments on every failed active-connect attempt across all callers
(including each failover retry), so failed/(created+failed) is not a rate.
- HA_ROLE_TRANSITION_FAILED_COUNTER: cluster-role-transition dispatch
failures
(execution error or timeout) on the CRR-write path.
- CRR_TRANSITION_COUNT: cluster-role-record transitions applied per HA
policy,
including transitions into a no-active state (distinct from
HA_FAILOVER_COUNT,
which counts only transitions that establish/move an ACTIVE cluster).
Group-attributable emission is wired at the existing sites in
HighAvailabilityGroup (failover count/duration, CRR refresh,
connect-failed, and
the two new transition metrics), FailoverPhoenixConnection (connection
created,
stale-CRR, mutation-blocked), HighAvailabilityPolicy (parallel fallback),
ParallelPhoenixContext/ParallelPhoenixUtil (parallel connection
created/error,
task timeout), and GetClusterRoleRecordUtil (poller tick count/failures).
The
JVM-shared parallel-executor pool metrics and the HA_CRR_CACHE_AGE_MS gauge
are
deliberately excluded from the per-group source (JVM-shared / not a
counter).
Tests: HAGroupClientMetricsSourceTest (6) and HAGroupMetricsManagerTest (7)
cover the ha_group tag/quoting, per-counter increment/update, per-group
isolation, distinct registered sources, and detach/re-create.
HighAvailabilityGroupTest additionally asserts the per-group
(ha_group-tagged)
counters mirror the JVM-global counters on a real counted transition
(HA_FAILOVER_COUNT + CRR_TRANSITION_COUNT) and on a failed / successful
connectActive (HA_FAILOVER_CONNECTION_FAILED_COUNTER), and that init
pre-registers the group's source while close detaches it. 30/30 pass across
the
three.
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
---
.../phoenix/jdbc/FailoverPhoenixConnection.java | 106 +++++-----
.../apache/phoenix/jdbc/HighAvailabilityGroup.java | 28 +++
.../phoenix/jdbc/HighAvailabilityPolicy.java | 51 ++++-
.../phoenix/jdbc/ParallelPhoenixContext.java | 5 +
.../apache/phoenix/jdbc/ParallelPhoenixUtil.java | 5 +
.../phoenix/monitoring/GlobalClientMetrics.java | 12 +-
.../monitoring/HAGroupClientMetricsSource.java | 133 ++++++++++++
.../phoenix/monitoring/HAGroupMetricsManager.java | 135 +++++++++++++
.../org/apache/phoenix/monitoring/MetricType.java | 26 ++-
.../phoenix/util/GetClusterRoleRecordUtil.java | 4 +
.../org/apache/phoenix/jdbc/HAGroupMetricsIT.java | 10 +-
.../phoenix/jdbc/HighAvailabilityGroupTest.java | 223 +++++++++++++++++++++
.../monitoring/HAGroupClientMetricsSourceTest.java | 130 ++++++++++++
.../monitoring/HAGroupMetricsManagerTest.java | 164 +++++++++++++++
14 files changed, 962 insertions(+), 70 deletions(-)
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/FailoverPhoenixConnection.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/FailoverPhoenixConnection.java
index 5916ea86c4..8541423269 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/FailoverPhoenixConnection.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/FailoverPhoenixConnection.java
@@ -19,7 +19,7 @@ package org.apache.phoenix.jdbc;
import static
org.apache.phoenix.jdbc.HighAvailabilityUtil.isMutationBlockedIOExceptionExistsInThrowable;
import static
org.apache.phoenix.jdbc.HighAvailabilityUtil.isStaleClusterRoleRecordExceptionExistsInThrowable;
-import static
org.apache.phoenix.monitoring.GlobalClientMetrics.GLOBAL_HA_FAILOVER_DURATION_MS;
+import static
org.apache.phoenix.monitoring.GlobalClientMetrics.GLOBAL_HA_FAILOVER_CONNECTION_CREATED_COUNTER;
import static
org.apache.phoenix.monitoring.GlobalClientMetrics.GLOBAL_HA_MUTATION_BLOCKED_COUNT;
import static
org.apache.phoenix.monitoring.GlobalClientMetrics.GLOBAL_HA_STALE_CRR_DETECTED_COUNT;
@@ -44,6 +44,7 @@ import java.util.concurrent.Executor;
import org.apache.phoenix.exception.FailoverSQLException;
import org.apache.phoenix.exception.SQLExceptionCode;
import org.apache.phoenix.exception.SQLExceptionInfo;
+import org.apache.phoenix.monitoring.HAGroupMetricsManager;
import org.apache.phoenix.monitoring.MetricType;
import org.apache.phoenix.util.EnvironmentEdgeManager;
import org.slf4j.Logger;
@@ -111,6 +112,13 @@ public class FailoverPhoenixConnection implements
PhoenixMonitoredConnection {
this.isClosed = false;
this.connection =
context.getHAGroup().connectActive(context.getProperties(),
context.getHAURLInfo());
+ // Count one successfully constructed FailoverPhoenixConnection. This is
not a matched pair with
+ // HA_FAILOVER_CONNECTION_FAILED_COUNTER: that counter increments on every
failed active-connect
+ // attempt across all callers (including each failover() retry), so
failed/(created+failed) is
+ // not a valid rate.
+ GLOBAL_HA_FAILOVER_CONNECTION_CREATED_COUNTER.increment();
+ HAGroupMetricsManager.increment(context.getHAGroup().getName(),
+ MetricType.HA_FAILOVER_CONNECTION_CREATED_COUNTER);
}
/**
@@ -175,63 +183,55 @@ public class FailoverPhoenixConnection implements
PhoenixMonitoredConnection {
return;
}
- final long failoverStartMs = EnvironmentEdgeManager.currentTimeMillis();
- try {
- PhoenixConnection newConn = null;
- SQLException cause = null;
- final long startTime = EnvironmentEdgeManager.currentTimeMillis();
- while (
- newConn == null && EnvironmentEdgeManager.currentTimeMillis() <
startTime + timeoutMs
- ) {
+ PhoenixConnection newConn = null;
+ SQLException cause = null;
+ final long startTime = EnvironmentEdgeManager.currentTimeMillis();
+ while (newConn == null && EnvironmentEdgeManager.currentTimeMillis() <
startTime + timeoutMs) {
+ try {
+ newConn =
+ context.getHAGroup().connectActive(context.getProperties(),
context.getHAURLInfo());
+ } catch (SQLException e) {
+ cause = e;
+ LOG.info("Got exception when trying to connect to active cluster.", e);
try {
- newConn =
- context.getHAGroup().connectActive(context.getProperties(),
context.getHAURLInfo());
- } catch (SQLException e) {
- cause = e;
- LOG.info("Got exception when trying to connect to active cluster.",
e);
- try {
- Thread.sleep(100); // TODO: be smart than this
- } catch (InterruptedException ie) {
- Thread.currentThread().interrupt();
- throw new SQLException("Got interrupted waiting for connection
failover", e);
- }
+ Thread.sleep(100); // TODO: be smart than this
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ throw new SQLException("Got interrupted waiting for connection
failover", e);
}
}
- if (newConn == null) {
- throw new FailoverSQLException("Can not failover connection",
- context.getHAGroup().getGroupInfo().toString(), cause);
- }
+ }
+ if (newConn == null) {
+ throw new FailoverSQLException("Can not failover connection",
+ context.getHAGroup().getGroupInfo().toString(), cause);
+ }
- final PhoenixConnection oldConn = connection;
- connection = newConn;
- if (oldConn != null) {
- // aggregate metrics
- previousMutationMetrics = oldConn.getMutationMetrics();
- previousReadMetrics = oldConn.getReadMetrics();
- oldConn.clearMetrics();
-
- // close old connection
- if (!oldConn.isClosed()) {
- // TODO: what happens to in-flight edits/mutations?
- // Can we copy into the new connection we do not allow this failover?
- // MutationState state = oldConn.getMutationState();
- try {
- oldConn.close(new
SQLExceptionInfo.Builder(SQLExceptionCode.HA_CLOSED_AFTER_FAILOVER)
- .setMessage("Phoenix connection got closed due to failover")
-
.setHaGroupInfo(context.getHAGroup().getGroupInfo().toString()).build()
- .buildException());
- } catch (SQLException e) {
- LOG.error("Failed to close old connection after failover: {}",
e.getMessage());
- LOG.info("Full stack when closing old connection after failover",
e);
- }
+ final PhoenixConnection oldConn = connection;
+ connection = newConn;
+ if (oldConn != null) {
+ // aggregate metrics
+ previousMutationMetrics = oldConn.getMutationMetrics();
+ previousReadMetrics = oldConn.getReadMetrics();
+ oldConn.clearMetrics();
+
+ // close old connection
+ if (!oldConn.isClosed()) {
+ // TODO: what happens to in-flight edits/mutations?
+ // Can we copy into the new connection we do not allow this failover?
+ // MutationState state = oldConn.getMutationState();
+ try {
+ oldConn.close(new
SQLExceptionInfo.Builder(SQLExceptionCode.HA_CLOSED_AFTER_FAILOVER)
+ .setMessage("Phoenix connection got closed due to failover")
+
.setHaGroupInfo(context.getHAGroup().getGroupInfo().toString()).build()
+ .buildException());
+ } catch (SQLException e) {
+ LOG.error("Failed to close old connection after failover: {}",
e.getMessage());
+ LOG.info("Full stack when closing old connection after failover", e);
}
}
- LOG.info("Connection {} failed over to {}",
context.getHAGroup().getGroupInfo(),
- connection.getURL());
- } finally {
- GLOBAL_HA_FAILOVER_DURATION_MS
- .update(EnvironmentEdgeManager.currentTimeMillis() - failoverStartMs);
}
+ LOG.info("Connection {} failed over to {}",
context.getHAGroup().getGroupInfo(),
+ connection.getURL());
}
/**
@@ -337,6 +337,8 @@ public class FailoverPhoenixConnection implements
PhoenixMonitoredConnection {
} catch (Exception e) {
if (isStaleClusterRoleRecordExceptionExistsInThrowable(e)) {
GLOBAL_HA_STALE_CRR_DETECTED_COUNT.increment();
+ HAGroupMetricsManager.increment(context.getHAGroup().getName(),
+ MetricType.HA_STALE_CRR_DETECTED_COUNT);
// If we receive StaleClusterRoleRecordException, that means
Operation was
// supposed to be executed on Active Cluster but was in reality was
sent to
// STANDBY Cluster, that can happen only when Failover is in
Progress, So we
@@ -363,6 +365,8 @@ public class FailoverPhoenixConnection implements
PhoenixMonitoredConnection {
}
if (isMutationBlockedIOExceptionExistsInThrowable(e)) {
GLOBAL_HA_MUTATION_BLOCKED_COUNT.increment();
+ HAGroupMetricsManager.increment(context.getHAGroup().getName(),
+ MetricType.HA_MUTATION_BLOCKED_COUNT);
}
if (policy.shouldFailover(e, ++failoverCount)) {
failover(timeoutMs);
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/HighAvailabilityGroup.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/HighAvailabilityGroup.java
index 05ce278764..90669e5f8d 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/HighAvailabilityGroup.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/HighAvailabilityGroup.java
@@ -19,7 +19,9 @@ package org.apache.phoenix.jdbc;
import static
org.apache.phoenix.monitoring.GlobalClientMetrics.GLOBAL_HA_CRR_CACHE_AGE_MS;
import static
org.apache.phoenix.monitoring.GlobalClientMetrics.GLOBAL_HA_CRR_REFRESH_COUNT;
+import static
org.apache.phoenix.monitoring.GlobalClientMetrics.GLOBAL_HA_FAILOVER_CONNECTION_FAILED_COUNTER;
import static
org.apache.phoenix.monitoring.GlobalClientMetrics.GLOBAL_HA_FAILOVER_COUNT;
+import static
org.apache.phoenix.monitoring.GlobalClientMetrics.GLOBAL_HA_ROLE_TRANSITION_FAILED_COUNTER;
import static
org.apache.phoenix.query.QueryServicesOptions.DEFAULT_CLIENT_CONNECTION_CACHE_MAX_DURATION;
import static org.apache.phoenix.util.PhoenixRuntime.JDBC_PROTOCOL_SEPARATOR;
@@ -61,6 +63,8 @@ import org.apache.phoenix.exception.SQLExceptionCode;
import org.apache.phoenix.exception.SQLExceptionInfo;
import org.apache.phoenix.jdbc.ClusterRoleRecord.ClusterRole;
import org.apache.phoenix.jdbc.ClusterRoleRecord.RegistryType;
+import org.apache.phoenix.monitoring.HAGroupMetricsManager;
+import org.apache.phoenix.monitoring.MetricType;
import org.apache.phoenix.query.HBaseFactoryProvider;
import org.apache.phoenix.util.GetClusterRoleRecordUtil;
import org.apache.phoenix.util.JDBCUtil;
@@ -635,6 +639,11 @@ public class HighAvailabilityGroup {
roleRecord = roleRecordFromEndpoint;
lastClusterRoleRecordRefreshTime = System.currentTimeMillis();
state = State.READY;
+ // Pre-register the per-group metrics2 source so the ha_group-tagged
series exists as soon as
+ // the
+ // group is ready, rather than only after the first HA metric fires. No-op
when global client
+ // metrics are disabled.
+ HAGroupMetricsManager.getOrCreate(getName());
}
/**
@@ -700,6 +709,11 @@ public class HighAvailabilityGroup {
} catch (SQLException e) {
LOG.error("Failed to connect to active cluster in HA group {}, record:
{}", info, roleRecord,
e);
+ // Single throw funnel for a failed active-cluster connection (no active
cluster, cluster
+ // demoted mid-connect, or the underlying connect threw). Counted here
so it tracks real
+ // production failures regardless of failover policy.
+ GLOBAL_HA_FAILOVER_CONNECTION_FAILED_COUNTER.increment();
+ HAGroupMetricsManager.increment(getName(),
MetricType.HA_FAILOVER_CONNECTION_FAILED_COUNTER);
throw new
SQLExceptionInfo.Builder(SQLExceptionCode.CANNOT_ESTABLISH_CONNECTION)
.setMessage("Failed to connect to active cluster in HA group")
.setHaGroupInfo(info.toString()).setRootCause(e).build().buildException();
@@ -789,6 +803,8 @@ public class HighAvailabilityGroup {
*/
void close() {
state = State.CLOSED;
+ // Detach the per-group metrics2 source so its JMX-context name is freed
for possible re-create.
+ HAGroupMetricsManager.remove(getName());
}
@Override
@@ -1160,6 +1176,7 @@ public class HighAvailabilityGroup {
// otherwise inflate this counter against its name (a "refresh" with no
fetch is a no-op
// from a CRR-state perspective).
GLOBAL_HA_CRR_REFRESH_COUNT.increment();
+ HAGroupMetricsManager.increment(getName(),
MetricType.HA_CRR_REFRESH_COUNT);
if (roleRecord == null) {
// First-load init path: no prior cache state to compare against, so
this is not a
// failover transition and HA_FAILOVER_COUNT is intentionally NOT
incremented here.
@@ -1197,6 +1214,11 @@ public class HighAvailabilityGroup {
return true;
}
+ // The detected CRR change is dispatched to the HA policy, which decides
whether it is a real
+ // transition (a url or role change) versus a pure version bump, and
counts and times it
+ // there (see HighAvailabilityPolicy#transitClusterRoleRecord). The
equals() and
+ // shouldApplyRefreshedRecord() guards above only establish that this is
a strictly newer
+ // record for the same HA group.
final ClusterRoleRecord oldRecord = roleRecord;
state = State.IN_TRANSITION;
LOG.info("HA group {} is in {} to set V{} record", info, state,
newRoleRecord.getVersion());
@@ -1225,6 +1247,11 @@ public class HighAvailabilityGroup {
} catch (ExecutionException | TimeoutException e) {
LOG.error("HA group {} failed to transit cluster roles per policy {}
to new " + "record {}",
info, roleRecord.getPolicy(), newRoleRecord, e);
+ // Dispatch of the policy-side cluster-role transition failed
(execution error or timed
+ // out). Count every such failure, including the
HA_ROLE_TRANSITION_NOT_ALLOWED case
+ // rethrown just below.
+ GLOBAL_HA_ROLE_TRANSITION_FAILED_COUNTER.increment();
+ HAGroupMetricsManager.increment(getName(),
MetricType.HA_ROLE_TRANSITION_FAILED_COUNTER);
// Rethrow the Role transitions not allowed exceptions
if (e.getCause() != null && e.getCause().getCause() != null) {
if (
@@ -1252,6 +1279,7 @@ public class HighAvailabilityGroup {
// unit-tested directly without driving a full mini-cluster transition.
if (shouldCountFailover(transitionSucceeded, oldRecord, newRoleRecord)) {
GLOBAL_HA_FAILOVER_COUNT.increment();
+ HAGroupMetricsManager.increment(getName(),
MetricType.HA_FAILOVER_COUNT);
}
// Update the role record and the last refresh time
roleRecord = newRoleRecord;
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/HighAvailabilityPolicy.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/HighAvailabilityPolicy.java
index 6b898ebeb5..267171dcb3 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/HighAvailabilityPolicy.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/HighAvailabilityPolicy.java
@@ -30,6 +30,8 @@ import java.util.Properties;
import org.apache.phoenix.exception.SQLExceptionCode;
import org.apache.phoenix.exception.SQLExceptionInfo;
import org.apache.phoenix.monitoring.GlobalClientMetrics;
+import org.apache.phoenix.monitoring.HAGroupMetricsManager;
+import org.apache.phoenix.monitoring.MetricType;
import org.apache.phoenix.query.ConnectionQueryServices;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -194,6 +196,8 @@ public enum HighAvailabilityPolicy {
// Give regular connection or a failover connection?
LOG.warn("Falling back to single phoenix connection due to resource
constraints");
GlobalClientMetrics.GLOBAL_HA_PARALLEL_CONNECTION_FALLBACK_COUNTER.increment();
+ HAGroupMetricsManager.increment(haGroup.getName(),
+ MetricType.HA_PARALLEL_CONNECTION_FALLBACK_COUNTER);
return haGroup.connectActive(info, haURLInfo);
}
}
@@ -322,15 +326,44 @@ public enum HighAvailabilityPolicy {
*/
public void transitClusterRoleRecord(HighAvailabilityGroup haGroup,
ClusterRoleRecord oldRecord,
ClusterRoleRecord newRecord) throws SQLException {
- if (
- !oldRecord.getUrl1().equals(newRecord.getUrl1())
- || !oldRecord.getUrl2().equals(newRecord.getUrl2())
- ) {
- // If there is a change in url then we need to handle the transitions.
- transitClusterUrl(haGroup, oldRecord, newRecord);
- } else {
- // If url is not changing then we need to check if there is a role
transition.
- transitClusterRole(haGroup, oldRecord, newRecord);
+ boolean registryChanged = oldRecord.getRegistryType() !=
newRecord.getRegistryType();
+ boolean urlChanged = !oldRecord.getUrl1().equals(newRecord.getUrl1())
+ || !oldRecord.getUrl2().equals(newRecord.getUrl2());
+ boolean roleChanged =
+ oldRecord.getRole1() != newRecord.getRole1() || oldRecord.getRole2() !=
newRecord.getRole2();
+
+ // "Transition" here means the CRR changed in a field this dispatch reacts
to: registry, url, or
+ // role. A strictly newer CRR version can arrive with all three identical
(a pure version bump)
+ // after clearing the isNewerThan + hasSameInfo guards in
refreshClusterRoleRecord; that is not
+ // a transition, so skip it rather than inflate CRR_TRANSITION_COUNT or
push a sample into
+ // CRR_TRANSITION_DURATION_MS.
+ if (!registryChanged && !urlChanged && !roleChanged) {
+ LOG.info("HA group {} applied a newer CRR version with no
registry/url/role change; no "
+ + "transition work to do", haGroup.getGroupInfo());
+ return;
+ }
+
+ // Count and time the transition here on the CRR-write path, the
autonomous-failover path for
+ // the ZK-less client: refreshClusterRoleRecord detects a newer CRR and
dispatches here to run
+ // the close/invalidate work per HA policy. Duration is recorded in
finally so a transit that
+ // fails partway still contributes its time-to-failure;
HA_ROLE_TRANSITION_FAILED_COUNTER
+ // (recorded by the caller on dispatch failure/timeout) distinguishes
failed transits.
+ GlobalClientMetrics.GLOBAL_HA_CRR_TRANSITION_COUNT.increment();
+ HAGroupMetricsManager.increment(haGroup.getName(),
MetricType.CRR_TRANSITION_COUNT);
+ long startTime = System.currentTimeMillis();
+ try {
+ if (urlChanged) {
+ // A change in url requires handling the connection transitions.
+ transitClusterUrl(haGroup, oldRecord, newRecord);
+ } else {
+ // If url is not changing then handle any role transition.
+ transitClusterRole(haGroup, oldRecord, newRecord);
+ }
+ } finally {
+ long durationMs = System.currentTimeMillis() - startTime;
+
GlobalClientMetrics.GLOBAL_HA_CRR_TRANSITION_DURATION_MS.update(durationMs);
+ HAGroupMetricsManager.update(haGroup.getName(),
MetricType.CRR_TRANSITION_DURATION_MS,
+ durationMs);
}
}
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/ParallelPhoenixContext.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/ParallelPhoenixContext.java
index 928ad2923b..f6269e7066 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/ParallelPhoenixContext.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/ParallelPhoenixContext.java
@@ -32,6 +32,7 @@ import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.phoenix.exception.SQLExceptionCode;
import org.apache.phoenix.exception.SQLExceptionInfo;
+import org.apache.phoenix.monitoring.HAGroupMetricsManager;
import org.apache.phoenix.monitoring.MetricType;
import org.apache.phoenix.query.QueryServices;
import org.apache.phoenix.query.QueryServicesOptions;
@@ -80,6 +81,8 @@ public class ParallelPhoenixContext {
Preconditions.checkArgument(executors.size() >= 2,
"Expected 2 executor pairs, one for each connection with a normal/close
executor");
GLOBAL_HA_PARALLEL_CONNECTION_CREATED_COUNTER.increment();
+ HAGroupMetricsManager.increment(haGroup != null ? haGroup.getName() : null,
+ MetricType.HA_PARALLEL_CONNECTION_CREATED_COUNTER);
this.properties = properties;
this.haGroup = haGroup;
this.haurlInfo = haurlInfo;
@@ -176,6 +179,8 @@ public class ParallelPhoenixContext {
isClosed = true;
if (isErrored) {
GLOBAL_HA_PARALLEL_CONNECTION_ERROR_COUNTER.increment();
+ HAGroupMetricsManager.increment(this.haGroup != null ?
this.haGroup.getName() : null,
+ MetricType.HA_PARALLEL_CONNECTION_ERROR_COUNTER);
}
}
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/ParallelPhoenixUtil.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/ParallelPhoenixUtil.java
index b223e3d9fb..c8f842345a 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/ParallelPhoenixUtil.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/ParallelPhoenixUtil.java
@@ -34,7 +34,9 @@ import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
import org.apache.hadoop.hbase.util.PairOfSameType;
import org.apache.phoenix.exception.SQLExceptionCode;
import org.apache.phoenix.exception.SQLExceptionInfo;
+import org.apache.phoenix.monitoring.HAGroupMetricsManager;
import org.apache.phoenix.monitoring.Metric;
+import org.apache.phoenix.monitoring.MetricType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -126,6 +128,9 @@ public class ParallelPhoenixUtil {
if (timedout) {
GLOBAL_HA_PARALLEL_TASK_TIMEOUT_COUNTER.increment();
+ HAGroupMetricsManager.increment(
+ context.getHaGroup() != null ? context.getHaGroup().getName() : null,
+ MetricType.HA_PARALLEL_TASK_TIMEOUT_COUNTER);
if (futures.isEmpty()) {
LOGGER.warn("Unexpected race between timeout and failure occurred.");
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/monitoring/GlobalClientMetrics.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/monitoring/GlobalClientMetrics.java
index bbd23bcbc5..b988730f14 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/monitoring/GlobalClientMetrics.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/monitoring/GlobalClientMetrics.java
@@ -35,10 +35,13 @@ import static
org.apache.phoenix.monitoring.MetricType.COUNT_ROWS_SCANNED;
import static org.apache.phoenix.monitoring.MetricType.COUNT_RPC_CALLS;
import static org.apache.phoenix.monitoring.MetricType.COUNT_RPC_RETRIES;
import static org.apache.phoenix.monitoring.MetricType.COUNT_SCANNED_REGIONS;
+import static org.apache.phoenix.monitoring.MetricType.CRR_TRANSITION_COUNT;
+import static
org.apache.phoenix.monitoring.MetricType.CRR_TRANSITION_DURATION_MS;
import static org.apache.phoenix.monitoring.MetricType.HA_CRR_CACHE_AGE_MS;
import static org.apache.phoenix.monitoring.MetricType.HA_CRR_REFRESH_COUNT;
+import static
org.apache.phoenix.monitoring.MetricType.HA_FAILOVER_CONNECTION_CREATED_COUNTER;
+import static
org.apache.phoenix.monitoring.MetricType.HA_FAILOVER_CONNECTION_FAILED_COUNTER;
import static org.apache.phoenix.monitoring.MetricType.HA_FAILOVER_COUNT;
-import static org.apache.phoenix.monitoring.MetricType.HA_FAILOVER_DURATION_MS;
import static
org.apache.phoenix.monitoring.MetricType.HA_MUTATION_BLOCKED_COUNT;
import static
org.apache.phoenix.monitoring.MetricType.HA_PARALLEL_CONNECTION_CREATED_COUNTER;
import static
org.apache.phoenix.monitoring.MetricType.HA_PARALLEL_CONNECTION_ERROR_COUNTER;
@@ -56,6 +59,7 @@ import static
org.apache.phoenix.monitoring.MetricType.HA_PARALLEL_POOL2_TASK_RE
import static
org.apache.phoenix.monitoring.MetricType.HA_PARALLEL_TASK_TIMEOUT_COUNTER;
import static org.apache.phoenix.monitoring.MetricType.HA_POLLER_TICK_COUNT;
import static org.apache.phoenix.monitoring.MetricType.HA_POLLER_TICK_FAILURES;
+import static
org.apache.phoenix.monitoring.MetricType.HA_ROLE_TRANSITION_FAILED_COUNTER;
import static
org.apache.phoenix.monitoring.MetricType.HA_STALE_CRR_DETECTED_COUNT;
import static org.apache.phoenix.monitoring.MetricType.HCONNECTIONS_COUNTER;
import static
org.apache.phoenix.monitoring.MetricType.INDEX_COMMIT_FAILURE_SIZE;
@@ -172,7 +176,11 @@ public enum GlobalClientMetrics {
GLOBAL_HA_PARALLEL_CONNECTION_CREATED_COUNTER(HA_PARALLEL_CONNECTION_CREATED_COUNTER),
GLOBAL_HA_FAILOVER_COUNT(HA_FAILOVER_COUNT),
- GLOBAL_HA_FAILOVER_DURATION_MS(HA_FAILOVER_DURATION_MS),
+
GLOBAL_HA_FAILOVER_CONNECTION_FAILED_COUNTER(HA_FAILOVER_CONNECTION_FAILED_COUNTER),
+
GLOBAL_HA_FAILOVER_CONNECTION_CREATED_COUNTER(HA_FAILOVER_CONNECTION_CREATED_COUNTER),
+ GLOBAL_HA_ROLE_TRANSITION_FAILED_COUNTER(HA_ROLE_TRANSITION_FAILED_COUNTER),
+ GLOBAL_HA_CRR_TRANSITION_COUNT(CRR_TRANSITION_COUNT),
+ GLOBAL_HA_CRR_TRANSITION_DURATION_MS(CRR_TRANSITION_DURATION_MS),
GLOBAL_HA_MUTATION_BLOCKED_COUNT(HA_MUTATION_BLOCKED_COUNT),
GLOBAL_HA_STALE_CRR_DETECTED_COUNT(HA_STALE_CRR_DETECTED_COUNT),
GLOBAL_HA_CRR_REFRESH_COUNT(HA_CRR_REFRESH_COUNT),
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/monitoring/HAGroupClientMetricsSource.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/monitoring/HAGroupClientMetricsSource.java
new file mode 100644
index 0000000000..7b6b127059
--- /dev/null
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/monitoring/HAGroupClientMetricsSource.java
@@ -0,0 +1,133 @@
+/*
+ * 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.monitoring;
+
+import static org.apache.phoenix.monitoring.MetricType.CRR_TRANSITION_COUNT;
+import static
org.apache.phoenix.monitoring.MetricType.CRR_TRANSITION_DURATION_MS;
+import static org.apache.phoenix.monitoring.MetricType.HA_CRR_REFRESH_COUNT;
+import static
org.apache.phoenix.monitoring.MetricType.HA_FAILOVER_CONNECTION_CREATED_COUNTER;
+import static
org.apache.phoenix.monitoring.MetricType.HA_FAILOVER_CONNECTION_FAILED_COUNTER;
+import static org.apache.phoenix.monitoring.MetricType.HA_FAILOVER_COUNT;
+import static
org.apache.phoenix.monitoring.MetricType.HA_MUTATION_BLOCKED_COUNT;
+import static
org.apache.phoenix.monitoring.MetricType.HA_PARALLEL_CONNECTION_CREATED_COUNTER;
+import static
org.apache.phoenix.monitoring.MetricType.HA_PARALLEL_CONNECTION_ERROR_COUNTER;
+import static
org.apache.phoenix.monitoring.MetricType.HA_PARALLEL_CONNECTION_FALLBACK_COUNTER;
+import static
org.apache.phoenix.monitoring.MetricType.HA_PARALLEL_TASK_TIMEOUT_COUNTER;
+import static org.apache.phoenix.monitoring.MetricType.HA_POLLER_TICK_COUNT;
+import static org.apache.phoenix.monitoring.MetricType.HA_POLLER_TICK_FAILURES;
+import static
org.apache.phoenix.monitoring.MetricType.HA_ROLE_TRANSITION_FAILED_COUNTER;
+import static
org.apache.phoenix.monitoring.MetricType.HA_STALE_CRR_DETECTED_COUNT;
+
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import javax.management.ObjectName;
+import org.apache.hadoop.hbase.metrics.BaseSourceImpl;
+import org.apache.hadoop.metrics2.lib.DefaultMetricsSystem;
+import org.apache.hadoop.metrics2.lib.Interns;
+import org.apache.hadoop.metrics2.lib.MutableFastCounter;
+import org.apache.phoenix.metrics.MetricConstants;
+
+import
org.apache.phoenix.thirdparty.com.google.common.annotations.VisibleForTesting;
+import org.apache.phoenix.thirdparty.com.google.common.collect.ImmutableList;
+
+/**
+ * Per-HA-group Hadoop Metrics2 source for the ZK-less HA client's
HA/CRR/failover metrics.
+ * <p>
+ * All groups share one metrics name/context; each instance appends {@code
,haGroup=<quoted-name>}
+ * to the JMX context so it registers as a distinct metrics2 source / MBean,
and stamps a
+ * {@code ha_group} tag ({@link MetricConstants#HA_GROUP_TAG_NAME}) carrying
the (unquoted) group
+ * name so the series can be sliced per HA group downstream. This mirrors the
server-side
+ * {@code HAGroupStoreMetricsSourceImpl} tagging pattern; both reference the
same tag key.
+ * <p>
+ * Each metric in {@link #METRIC_TYPES} is a monotonic {@link
MutableFastCounter}. Accumulating
+ * metrics such as {@code CRR_TRANSITION_DURATION_MS} add total milliseconds
via {@link #update},
+ * which matches the semantics of the JVM-global {@code GLOBAL_HA_*} counters;
those continue to
+ * emit unchanged alongside this per-group source (dual emit).
+ * <p>
+ * The set is intentionally limited to metrics attributable to a single HA
group (each emission site
+ * has a {@code HighAvailabilityGroup} in scope). The shared parallel-executor
pool metrics
+ * ({@code HA_PARALLEL_POOL1_*}/{@code HA_PARALLEL_POOL2_*}) and the {@code
HA_CRR_CACHE_AGE_MS}
+ * gauge are excluded: the pools are JVM-shared and the gauge is not a counter.
+ */
+public class HAGroupClientMetricsSource extends BaseSourceImpl {
+
+ static final String METRICS_NAME = "HAGroupClient";
+ static final String METRICS_DESC = "Phoenix HA Group Client Metrics";
+ static final String METRICS_CONTEXT = "phoenix";
+ static final String METRICS_JMX_CONTEXT = "Phoenix,sub=" + METRICS_NAME;
+
+ /**
+ * The HA metrics attributable to a single HA group; each emission site has a
+ * {@code HighAvailabilityGroup} in scope.
+ */
+ private static final List<MetricType> METRIC_TYPES =
ImmutableList.of(HA_FAILOVER_COUNT,
+ CRR_TRANSITION_DURATION_MS, HA_FAILOVER_CONNECTION_CREATED_COUNTER,
+ HA_FAILOVER_CONNECTION_FAILED_COUNTER, HA_STALE_CRR_DETECTED_COUNT,
HA_MUTATION_BLOCKED_COUNT,
+ HA_CRR_REFRESH_COUNT, HA_ROLE_TRANSITION_FAILED_COUNTER,
CRR_TRANSITION_COUNT,
+ HA_PARALLEL_CONNECTION_FALLBACK_COUNTER,
HA_PARALLEL_CONNECTION_CREATED_COUNTER,
+ HA_PARALLEL_CONNECTION_ERROR_COUNTER, HA_PARALLEL_TASK_TIMEOUT_COUNTER,
HA_POLLER_TICK_COUNT,
+ HA_POLLER_TICK_FAILURES);
+
+ private final Map<MetricType, MutableFastCounter> counters = new
EnumMap<>(MetricType.class);
+
+ public HAGroupClientMetricsSource(String haGroupName) {
+ super(METRICS_NAME, METRICS_DESC, METRICS_CONTEXT,
+ METRICS_JMX_CONTEXT + ",haGroup=" + ObjectName.quote(haGroupName));
+ getMetricsRegistry().tag(
+ Interns.info(MetricConstants.HA_GROUP_TAG_NAME,
MetricConstants.HA_GROUP_TAG_DESC),
+ haGroupName);
+ for (MetricType type : METRIC_TYPES) {
+ counters.put(type,
+ getMetricsRegistry().newCounter(type.columnName(), type.description(),
0L));
+ }
+ }
+
+ /** Increment the group's counter for the given HA metric type. */
+ public void increment(MetricType type) {
+ MutableFastCounter counter = counters.get(type);
+ if (counter != null) {
+ counter.incr();
+ }
+ }
+
+ /**
+ * Add {@code value} to the group's accumulating counter for the given HA
metric type (used for
+ * {@code CRR_TRANSITION_DURATION_MS}).
+ */
+ public void update(MetricType type, long value) {
+ MutableFastCounter counter = counters.get(type);
+ if (counter != null) {
+ counter.incr(value);
+ }
+ }
+
+ /**
+ * Detach this source from the metrics system on HA-group close, freeing its
JMX-context name in
+ * {@link DefaultMetricsSystem} so the same group can register again if
later re-created.
+ */
+ public void unregister() {
+ DefaultMetricsSystem.instance().unregisterSource(metricsJmxContext);
+ }
+
+ @VisibleForTesting
+ public long getCounterValue(MetricType type) {
+ MutableFastCounter counter = counters.get(type);
+ return counter == null ? -1L : counter.value();
+ }
+}
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/monitoring/HAGroupMetricsManager.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/monitoring/HAGroupMetricsManager.java
new file mode 100644
index 0000000000..ee32f706ad
--- /dev/null
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/monitoring/HAGroupMetricsManager.java
@@ -0,0 +1,135 @@
+/*
+ * 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.monitoring;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.phoenix.query.QueryServicesOptions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import
org.apache.phoenix.thirdparty.com.google.common.annotations.VisibleForTesting;
+
+/**
+ * Central registry of {@link HAGroupClientMetricsSource}, one per HA group
name. Each group's
+ * source registers with Hadoop's metrics2 {@code DefaultMetricsSystem} tagged
+ * {@code haGroup=<name>} so the HA/CRR/failover client metrics can be sliced
per HA group alongside
+ * the JVM-global {@code GLOBAL_HA_*} counters (which continue to emit
unchanged).
+ * <p>
+ * Sources are created only when global client metrics are enabled
(constructing a source registers
+ * it with the metrics system). {@link #remove(String)} detaches a group's
source on HA-group close.
+ */
+public class HAGroupMetricsManager {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(HAGroupMetricsManager.class);
+
+ private static final boolean IS_GLOBAL_METRICS_ENABLED =
+ QueryServicesOptions.withDefaults().isGlobalMetricsEnabled();
+
+ private static final Map<String, HAGroupClientMetricsSource> GROUP_SOURCES =
+ new ConcurrentHashMap<>();
+
+ private HAGroupMetricsManager() {
+ }
+
+ /**
+ * Get (registering a metrics2 source on first sight) the metrics source for
an HA group. Safe to
+ * call repeatedly for the same name. Returns {@code null} for a null/empty
group name, when
+ * global client metrics are disabled, or when registering the source fails.
+ */
+ public static HAGroupClientMetricsSource getOrCreate(String haGroupName) {
+ if (StringUtils.isEmpty(haGroupName) || !IS_GLOBAL_METRICS_ENABLED) {
+ return null;
+ }
+ HAGroupClientMetricsSource source = GROUP_SOURCES.get(haGroupName);
+ if (source != null) {
+ return source;
+ }
+ synchronized (HAGroupMetricsManager.class) {
+ source = GROUP_SOURCES.get(haGroupName);
+ if (source == null) {
+ try {
+ source = new HAGroupClientMetricsSource(haGroupName);
+ } catch (Exception e) {
+ // Constructing a source registers it with DefaultMetricsSystem and
can throw (e.g. a
+ // MetricsException on a duplicate JMX name). Metrics registration
is best-effort and must
+ // never break a caller such as HighAvailabilityGroup.init();
swallow and skip this group.
+ LOGGER.error("Failed creating HA-group client metrics source for
group '{}'", haGroupName,
+ e);
+ return null;
+ }
+ GROUP_SOURCES.put(haGroupName, source);
+ LOGGER.info("Created HA-group client metrics source for group '{}'",
haGroupName);
+ }
+ }
+ return source;
+ }
+
+ /**
+ * Increment the per-group counter for an HA metric type. Registers the
group's source if needed.
+ * Metric emission is best-effort and never propagates an exception to the
caller's request path.
+ */
+ public static void increment(String haGroupName, MetricType type) {
+ try {
+ HAGroupClientMetricsSource source = getOrCreate(haGroupName);
+ if (source != null) {
+ source.increment(type);
+ }
+ } catch (Exception e) {
+ LOGGER.error("Failed incrementing HA-group metric {} for group '{}'",
type, haGroupName, e);
+ }
+ }
+
+ /**
+ * Add a sample to a per-group accumulating HA metric (e.g. {@code
CRR_TRANSITION_DURATION_MS}).
+ * Registers the group's source if needed. Best-effort; never propagates an
exception.
+ */
+ public static void update(String haGroupName, MetricType type, long value) {
+ try {
+ HAGroupClientMetricsSource source = getOrCreate(haGroupName);
+ if (source != null) {
+ source.update(type, value);
+ }
+ } catch (Exception e) {
+ LOGGER.error("Failed updating HA-group metric {} for group '{}'", type,
haGroupName, e);
+ }
+ }
+
+ /**
+ * Tear down a group's metrics source on HA-group close, freeing its
metrics2 source name so the
+ * same group can be re-created later.
+ */
+ public static void remove(String haGroupName) {
+ if (StringUtils.isEmpty(haGroupName)) {
+ return;
+ }
+ synchronized (HAGroupMetricsManager.class) {
+ HAGroupClientMetricsSource source = GROUP_SOURCES.remove(haGroupName);
+ if (source != null) {
+ source.unregister();
+ LOGGER.info("Removed HA-group client metrics source for group '{}'",
haGroupName);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public static HAGroupClientMetricsSource getIfPresent(String haGroupName) {
+ return GROUP_SOURCES.get(haGroupName);
+ }
+}
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/monitoring/MetricType.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/monitoring/MetricType.java
index 22a9ecbdcf..3b8f0597f7 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/monitoring/MetricType.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/monitoring/MetricType.java
@@ -358,9 +358,29 @@ public enum MetricType {
"Counter for cluster-level failover transitions (active URL flips between
peers or recovers "
+ "from no-active state) recorded at the CRR write site",
LogLevel.DEBUG, PLong.INSTANCE),
- HA_FAILOVER_DURATION_MS("hafd",
- "Total time in milliseconds spent in connection-level failover
transitions, summed across "
- + "all observing connections (per-connection observation, not
per-cluster-event)",
+ CRR_TRANSITION_DURATION_MS("crtdms",
+ "Total time in milliseconds spent dispatching cluster role record
transitions per HA policy "
+ + "(HighAvailabilityPolicy#transitClusterRoleRecord), recorded on each
applied "
+ + "registry/url/role change; pairs with CRR_TRANSITION_COUNT",
+ LogLevel.DEBUG, PLong.INSTANCE),
+ HA_FAILOVER_CONNECTION_FAILED_COUNTER("hafcf",
+ "Counter for failed attempts to connect to the active cluster in an HA
group, recorded at the "
+ + "connectActive throw site (no active cluster, demoted mid-connect, or
connect error); "
+ + "incremented on every failed attempt across all callers, including
each failover retry",
+ LogLevel.DEBUG, PLong.INSTANCE),
+ HA_FAILOVER_CONNECTION_CREATED_COUNTER("hafcc",
+ "Counter for FailoverPhoenixConnection instances successfully created
(connected to the active "
+ + "cluster); one increment per constructed connection. Not a matched
pair with "
+ + "HA_FAILOVER_CONNECTION_FAILED_COUNTER, which counts every failed
active-connect attempt "
+ + "across all callers (including per failover retry)",
+ LogLevel.DEBUG, PLong.INSTANCE),
+ HA_ROLE_TRANSITION_FAILED_COUNTER("hrtf",
+ "Counter for cluster-role-transition dispatch failures
(ExecutionException/TimeoutException) "
+ + "when applying a new CRR per HA policy at the CRR-write path",
+ LogLevel.DEBUG, PLong.INSTANCE),
+ CRR_TRANSITION_COUNT("crtc",
+ "Counter for cluster role record transitions applied per HA policy (a
registry/url/role change "
+ + "that triggers a transit), including transitions to a no-active state",
LogLevel.DEBUG, PLong.INSTANCE),
HA_MUTATION_BLOCKED_COUNT("hambc",
"Counter for MutationBlockedIOException surfaces caught by
wrapActionDuringFailover",
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/util/GetClusterRoleRecordUtil.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/util/GetClusterRoleRecordUtil.java
index b25d5f46f2..1bc202d87e 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/util/GetClusterRoleRecordUtil.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/util/GetClusterRoleRecordUtil.java
@@ -43,6 +43,8 @@ import org.apache.phoenix.jdbc.HighAvailabilityGroup;
import org.apache.phoenix.jdbc.HighAvailabilityPolicy;
import org.apache.phoenix.jdbc.PhoenixConnection;
import org.apache.phoenix.monitoring.GlobalClientMetrics;
+import org.apache.phoenix.monitoring.HAGroupMetricsManager;
+import org.apache.phoenix.monitoring.MetricType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -223,6 +225,7 @@ public class GetClusterRoleRecordUtil {
// Increment unconditionally so a failed tick still alternates next
iteration.
long tick = tickCount.getAndIncrement();
GlobalClientMetrics.GLOBAL_HA_POLLER_TICK_COUNT.increment();
+ HAGroupMetricsManager.increment(haGroupName,
MetricType.HA_POLLER_TICK_COUNT);
// Sample current CRR cache age into the gauge each tick. Without
this, the
// HA_CRR_CACHE_AGE_MS counter-backed gauge is only updated on
connect() and would
// not advance during idle periods between connects, making it look
fresher than it
@@ -271,6 +274,7 @@ public class GetClusterRoleRecordUtil {
}
} catch (SQLException e) {
GlobalClientMetrics.GLOBAL_HA_POLLER_TICK_FAILURES.increment();
+ HAGroupMetricsManager.increment(haGroupName,
MetricType.HA_POLLER_TICK_FAILURES);
LOGGER.error(
"Exception found while polling for ClusterRoleRecord on {} for HA
group" + " {}: {}",
tickUrl, haGroupName, e.getMessage());
diff --git
a/phoenix-core/src/it/java/org/apache/phoenix/jdbc/HAGroupMetricsIT.java
b/phoenix-core/src/it/java/org/apache/phoenix/jdbc/HAGroupMetricsIT.java
index 7f02288cc6..efe9371972 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/jdbc/HAGroupMetricsIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/jdbc/HAGroupMetricsIT.java
@@ -21,8 +21,8 @@ import static
org.apache.phoenix.jdbc.HighAvailabilityGroup.PHOENIX_HA_GROUP_ATT
import static
org.apache.phoenix.jdbc.HighAvailabilityTestingUtility.getHighAvailibilityGroup;
import static
org.apache.phoenix.monitoring.GlobalClientMetrics.GLOBAL_HA_CRR_CACHE_AGE_MS;
import static
org.apache.phoenix.monitoring.GlobalClientMetrics.GLOBAL_HA_CRR_REFRESH_COUNT;
+import static
org.apache.phoenix.monitoring.GlobalClientMetrics.GLOBAL_HA_CRR_TRANSITION_DURATION_MS;
import static
org.apache.phoenix.monitoring.GlobalClientMetrics.GLOBAL_HA_FAILOVER_COUNT;
-import static
org.apache.phoenix.monitoring.GlobalClientMetrics.GLOBAL_HA_FAILOVER_DURATION_MS;
import static
org.apache.phoenix.monitoring.GlobalClientMetrics.GLOBAL_HA_MUTATION_BLOCKED_COUNT;
import static
org.apache.phoenix.monitoring.GlobalClientMetrics.GLOBAL_HA_POLLER_TICK_COUNT;
import static
org.apache.phoenix.monitoring.GlobalClientMetrics.GLOBAL_HA_POLLER_TICK_FAILURES;
@@ -109,7 +109,7 @@ public class HAGroupMetricsIT extends HABaseIT {
@Test(timeout = 300000)
public void testFailoverCountAndDuration() throws Exception {
long countBefore = GLOBAL_HA_FAILOVER_COUNT.getMetric().getValue();
- long durationBefore =
GLOBAL_HA_FAILOVER_DURATION_MS.getMetric().getValue();
+ long durationBefore =
GLOBAL_HA_CRR_TRANSITION_DURATION_MS.getMetric().getValue();
try (Connection conn =
DriverManager.getConnection(CLUSTERS.getJdbcHAUrl(), clientProperties)) {
conn.createStatement().execute("UPSERT INTO " + tableName + " VALUES (1,
1)");
@@ -118,10 +118,10 @@ public class HAGroupMetricsIT extends HABaseIT {
}
long countAfter = GLOBAL_HA_FAILOVER_COUNT.getMetric().getValue();
- long durationAfter = GLOBAL_HA_FAILOVER_DURATION_MS.getMetric().getValue();
+ long durationAfter =
GLOBAL_HA_CRR_TRANSITION_DURATION_MS.getMetric().getValue();
assertTrue("HA_FAILOVER_COUNT should increment on cluster role transition",
countAfter > countBefore);
- assertTrue("HA_FAILOVER_DURATION_MS sum should grow on transition",
+ assertTrue("CRR_TRANSITION_DURATION_MS sum should grow on transition",
durationAfter >= durationBefore);
}
@@ -281,7 +281,7 @@ public class HAGroupMetricsIT extends HABaseIT {
private void resetAllHaMetrics() {
GLOBAL_HA_FAILOVER_COUNT.getMetric().reset();
- GLOBAL_HA_FAILOVER_DURATION_MS.getMetric().reset();
+ GLOBAL_HA_CRR_TRANSITION_DURATION_MS.getMetric().reset();
GLOBAL_HA_MUTATION_BLOCKED_COUNT.getMetric().reset();
GLOBAL_HA_STALE_CRR_DETECTED_COUNT.getMetric().reset();
GLOBAL_HA_CRR_REFRESH_COUNT.getMetric().reset();
diff --git
a/phoenix-core/src/test/java/org/apache/phoenix/jdbc/HighAvailabilityGroupTest.java
b/phoenix-core/src/test/java/org/apache/phoenix/jdbc/HighAvailabilityGroupTest.java
index 5bf956fa88..5d275f3da7 100644
---
a/phoenix-core/src/test/java/org/apache/phoenix/jdbc/HighAvailabilityGroupTest.java
+++
b/phoenix-core/src/test/java/org/apache/phoenix/jdbc/HighAvailabilityGroupTest.java
@@ -19,6 +19,7 @@ package org.apache.phoenix.jdbc;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
@@ -27,12 +28,16 @@ import static org.junit.Assert.fail;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.sql.SQLException;
+import java.util.HashSet;
import java.util.Properties;
import org.apache.phoenix.exception.SQLExceptionCode;
import org.apache.phoenix.jdbc.ClusterRoleRecord.ClusterRole;
import org.apache.phoenix.jdbc.HighAvailabilityGroup.HAGroupInfo;
import org.apache.phoenix.jdbc.HighAvailabilityGroup.State;
import org.apache.phoenix.monitoring.GlobalClientMetrics;
+import org.apache.phoenix.monitoring.HAGroupClientMetricsSource;
+import org.apache.phoenix.monitoring.HAGroupMetricsManager;
+import org.apache.phoenix.monitoring.MetricType;
import org.junit.Test;
import org.mockito.Mockito;
import org.slf4j.Logger;
@@ -618,4 +623,222 @@ public class HighAvailabilityGroupTest {
Thread.interrupted();
}
}
+
+ /**
+ * A real counted role-flip transition driven through {@code
refreshClusterRoleRecord} must both
+ * increment {@code HA_FAILOVER_COUNT} and record a sample on {@code
CRR_TRANSITION_DURATION_MS}.
+ * This pins the duration metric to the CRR-write transition path (the path
that autonomous
+ * failovers actually take) rather than the connection-level
+ * {@code FailoverPhoenixConnection.failover()} path, which is never
auto-invoked under the
+ * default {@code ExplicitFailoverPolicy}. Active URL flips from url1 to
url2 so
+ * {@code shouldCountFailover} returns true; the {@code URLS} entry is
seeded empty so the policy
+ * transition is a clean no-op (no real connections needed).
+ */
+ @Test
+ public void testCountedTransitionRecordsFailoverCountAndDuration() throws
Exception {
+ String haGroupName =
"testCountedTransitionRecordsFailoverCountAndDuration";
+ String url1 = "host1\\:60010";
+ String url2 = "host2\\:60010";
+ ClusterRoleRecord aActiveBStandby = new ClusterRoleRecord(haGroupName,
+ HighAvailabilityPolicy.FAILOVER, url1, ClusterRole.ACTIVE, url2,
ClusterRole.STANDBY, 10L);
+ ClusterRoleRecord aStandbyBActive = new ClusterRoleRecord(haGroupName,
+ HighAvailabilityPolicy.FAILOVER, url1, ClusterRole.STANDBY, url2,
ClusterRole.ACTIVE, 11L);
+
+ HAGroupInfo info = new HAGroupInfo(haGroupName, url1, url2);
+ // Seed an empty URL set so the policy-side transition iterates nothing
and is a clean no-op.
+ HighAvailabilityGroup.URLS.put(info, new HashSet<>());
+ try {
+ HighAvailabilityGroup group = Mockito
+ .spy(new HighAvailabilityGroup(info, new Properties(),
aActiveBStandby, State.READY));
+
Mockito.doReturn(aStandbyBActive).when(group).getClusterRoleRecordFromEndpoint();
+
+ long countBefore =
GlobalClientMetrics.GLOBAL_HA_FAILOVER_COUNT.getMetric().getValue();
+ long durationSamplesBefore =
+
GlobalClientMetrics.GLOBAL_HA_CRR_TRANSITION_DURATION_MS.getMetric().getNumberOfSamples();
+
+ assertTrue("A real role-flip transition must apply and return true",
+ group.refreshClusterRoleRecord(true));
+ assertSame("The new record must be applied after the transition",
aStandbyBActive,
+ group.getRoleRecord());
+
+ assertEquals("An active-URL flip must increment HA_FAILOVER_COUNT",
countBefore + 1,
+ GlobalClientMetrics.GLOBAL_HA_FAILOVER_COUNT.getMetric().getValue());
+ assertEquals(
+ "The transition must record a CRR_TRANSITION_DURATION_MS sample on the
CRR-write " + "path",
+ durationSamplesBefore + 1,
+
GlobalClientMetrics.GLOBAL_HA_CRR_TRANSITION_DURATION_MS.getMetric().getNumberOfSamples());
+
+ // Per-group (ha_group-tagged) emission mirrors the JVM-global counters.
This group name is
+ // unique to this test, so its source starts empty and this single
transition yields exactly 1
+ // on both the failover and the applied-transition counters.
+ HAGroupClientMetricsSource source =
HAGroupMetricsManager.getIfPresent(haGroupName);
+ assertNotNull("The transition must register the per-group metrics
source", source);
+ assertEquals("Per-group HA_FAILOVER_COUNT must count the active-URL
flip", 1L,
+ source.getCounterValue(MetricType.HA_FAILOVER_COUNT));
+ assertEquals("Per-group CRR_TRANSITION_COUNT must count the applied
transition", 1L,
+ source.getCounterValue(MetricType.CRR_TRANSITION_COUNT));
+ } finally {
+ HighAvailabilityGroup.URLS.remove(info);
+ HAGroupMetricsManager.remove(haGroupName);
+ }
+ }
+
+ /**
+ * A strictly newer CRR version whose registry/url/role fields are all
identical is a pure version
+ * bump, not a transition. It clears the {@code equals()} and {@code
shouldApplyRefreshedRecord()}
+ * guards in {@code refreshClusterRoleRecord} (so the newer record is still
adopted), but the HA
+ * policy's change guard in {@code transitClusterRoleRecord} must skip it: no
+ * {@code CRR_TRANSITION_COUNT} increment, no {@code
CRR_TRANSITION_DURATION_MS} sample, and no
+ * failover. This pins the guard against a regression that would re-inflate
the transition
+ * counters on version bumps.
+ */
+ @Test
+ public void testPureVersionBumpDoesNotCountOrTimeTransition() throws
Exception {
+ String haGroupName = "testPureVersionBumpDoesNotCountOrTimeTransition";
+ String url1 = "host1\\:60010";
+ String url2 = "host2\\:60010";
+ ClusterRoleRecord recordV10 = new ClusterRoleRecord(haGroupName,
+ HighAvailabilityPolicy.FAILOVER, url1, ClusterRole.ACTIVE, url2,
ClusterRole.STANDBY, 10L);
+ // Same registry/url/role, only the admin version advances.
+ ClusterRoleRecord recordV11 = new ClusterRoleRecord(haGroupName,
+ HighAvailabilityPolicy.FAILOVER, url1, ClusterRole.ACTIVE, url2,
ClusterRole.STANDBY, 11L);
+
+ HAGroupInfo info = new HAGroupInfo(haGroupName, url1, url2);
+ HighAvailabilityGroup.URLS.put(info, new HashSet<>());
+ try {
+ HighAvailabilityGroup group =
+ Mockito.spy(new HighAvailabilityGroup(info, new Properties(),
recordV10, State.READY));
+
Mockito.doReturn(recordV11).when(group).getClusterRoleRecordFromEndpoint();
+
+ long transitionCountBefore =
+
GlobalClientMetrics.GLOBAL_HA_CRR_TRANSITION_COUNT.getMetric().getValue();
+ long durationSamplesBefore =
+
GlobalClientMetrics.GLOBAL_HA_CRR_TRANSITION_DURATION_MS.getMetric().getNumberOfSamples();
+ long failoverCountBefore =
+ GlobalClientMetrics.GLOBAL_HA_FAILOVER_COUNT.getMetric().getValue();
+
+ assertTrue("A strictly newer version must still be applied",
+ group.refreshClusterRoleRecord(true));
+ assertSame("The newer-version record must be adopted", recordV11,
group.getRoleRecord());
+
+ assertEquals("A pure version bump must not increment
CRR_TRANSITION_COUNT",
+ transitionCountBefore,
+
GlobalClientMetrics.GLOBAL_HA_CRR_TRANSITION_COUNT.getMetric().getValue());
+ assertEquals("A pure version bump must not record a
CRR_TRANSITION_DURATION_MS sample",
+ durationSamplesBefore,
+
GlobalClientMetrics.GLOBAL_HA_CRR_TRANSITION_DURATION_MS.getMetric().getNumberOfSamples());
+ assertEquals("A pure version bump is not a failover",
failoverCountBefore,
+ GlobalClientMetrics.GLOBAL_HA_FAILOVER_COUNT.getMetric().getValue());
+
+ // Nothing was emitted for this group, so if a per-group source exists
its transition counter
+ // must still be zero.
+ HAGroupClientMetricsSource source =
HAGroupMetricsManager.getIfPresent(haGroupName);
+ if (source != null) {
+ assertEquals("Per-group CRR_TRANSITION_COUNT must stay zero on a
version bump", 0L,
+ source.getCounterValue(MetricType.CRR_TRANSITION_COUNT));
+ }
+ } finally {
+ HighAvailabilityGroup.URLS.remove(info);
+ HAGroupMetricsManager.remove(haGroupName);
+ }
+ }
+
+ /**
+ * A failed {@code connectActive} (no active cluster in the record) must
increment
+ * {@code HA_FAILOVER_CONNECTION_FAILED_COUNTER} on its single SQLException
throw funnel.
+ */
+ @Test
+ public void testConnectActiveFailureIncrementsFailedCounter() {
+ String haGroupName = "testConnectActiveFailureIncrementsFailedCounter";
+ String url1 = "host1\\:60010";
+ String url2 = "host2\\:60010";
+ // Both STANDBY → no active URL → connectActive takes the
HA_NO_ACTIVE_CLUSTER throw path.
+ ClusterRoleRecord bothStandby = new ClusterRoleRecord(haGroupName,
+ HighAvailabilityPolicy.FAILOVER, url1, ClusterRole.STANDBY, url2,
ClusterRole.STANDBY, 10L);
+ HAGroupInfo info = new HAGroupInfo(haGroupName, url1, url2);
+ HighAvailabilityGroup group =
+ new HighAvailabilityGroup(info, new Properties(), bothStandby,
State.READY);
+
+ long failedBefore =
+
GlobalClientMetrics.GLOBAL_HA_FAILOVER_CONNECTION_FAILED_COUNTER.getMetric().getValue();
+ try {
+ group.connectActive(new Properties(), new HAURLInfo(haGroupName));
+ fail("connectActive must throw when the HA group has no active cluster");
+ } catch (SQLException e) {
+
assertEquals(SQLExceptionCode.CANNOT_ESTABLISH_CONNECTION.getErrorCode(),
e.getErrorCode());
+ }
+ assertEquals("A failed connectActive must increment the failed counter",
failedBefore + 1,
+
GlobalClientMetrics.GLOBAL_HA_FAILOVER_CONNECTION_FAILED_COUNTER.getMetric().getValue());
+ // Per-group mirror: unique group name → its FAILED counter reflects
exactly this one failure.
+ HAGroupClientMetricsSource source =
HAGroupMetricsManager.getIfPresent(haGroupName);
+ assertNotNull("A failed connectActive must register the per-group metrics
source", source);
+ assertEquals("Per-group HA_FAILOVER_CONNECTION_FAILED_COUNTER must count
the failed connect",
+ 1L,
source.getCounterValue(MetricType.HA_FAILOVER_CONNECTION_FAILED_COUNTER));
+ HAGroupMetricsManager.remove(haGroupName);
+ }
+
+ /**
+ * A successful {@code connectActive} must NOT increment
+ * {@code HA_FAILOVER_CONNECTION_FAILED_COUNTER}. Guards against the counter
being placed on a
+ * path that also runs on success (a non-vacuous negative assertion).
+ */
+ @Test
+ public void testConnectActiveSuccessLeavesFailedCounterUnchanged() throws
Exception {
+ String haGroupName =
"testConnectActiveSuccessLeavesFailedCounterUnchanged";
+ String url1 = "host1\\:60010";
+ String url2 = "host2\\:60010";
+ ClusterRoleRecord aActiveBStandby = new ClusterRoleRecord(haGroupName,
+ HighAvailabilityPolicy.FAILOVER, url1, ClusterRole.ACTIVE, url2,
ClusterRole.STANDBY, 10L);
+ HAGroupInfo info = new HAGroupInfo(haGroupName, url1, url2);
+ HighAvailabilityGroup group =
+ Mockito.spy(new HighAvailabilityGroup(info, new Properties(),
aActiveBStandby, State.READY));
+
+ PhoenixConnection conn = Mockito.mock(PhoenixConnection.class);
+
Mockito.doReturn(conn).when(group).connectToOneCluster(Mockito.any(String.class),
+ Mockito.any(Properties.class), Mockito.any(HAURLInfo.class));
+ Mockito.doReturn(true).when(group).isActive(conn);
+
+ long failedBefore =
+
GlobalClientMetrics.GLOBAL_HA_FAILOVER_CONNECTION_FAILED_COUNTER.getMetric().getValue();
+ assertSame("connectActive must return the established connection", conn,
+ group.connectActive(new Properties(), new HAURLInfo(haGroupName)));
+ assertEquals("A successful connectActive must not increment the failed
counter", failedBefore,
+
GlobalClientMetrics.GLOBAL_HA_FAILOVER_CONNECTION_FAILED_COUNTER.getMetric().getValue());
+ // Per-group negative check: a successful connect emits no FAILED for this
group. Either the
+ // source was never created (no emission touched it) or its FAILED counter
is still zero.
+ HAGroupClientMetricsSource source =
HAGroupMetricsManager.getIfPresent(haGroupName);
+ assertTrue("A successful connectActive must not emit a per-group failed
count", source == null
+ ||
source.getCounterValue(MetricType.HA_FAILOVER_CONNECTION_FAILED_COUNTER) == 0L);
+ HAGroupMetricsManager.remove(haGroupName);
+ }
+
+ /**
+ * The per-group metrics source lifecycle is bound to the HA group: {@code
init} pre-registers it
+ * (so the {@code ha_group}-tagged series exists as soon as the group is
ready) and {@code close}
+ * detaches it (freeing the metrics2 source name for a later re-create).
+ */
+ @Test
+ public void testInitRegistersPerGroupSourceAndCloseRemovesIt() throws
Exception {
+ String haGroupName = "testInitRegistersPerGroupSourceAndCloseRemovesIt";
+ String url1 = "host1\\:60010";
+ String url2 = "host2\\:60010";
+ ClusterRoleRecord record = new ClusterRoleRecord(haGroupName,
HighAvailabilityPolicy.FAILOVER,
+ url1, ClusterRole.ACTIVE, url2, ClusterRole.STANDBY, 10L);
+ HAGroupInfo info = new HAGroupInfo(haGroupName, url1, url2);
+ HighAvailabilityGroup group =
+ Mockito.spy(new HighAvailabilityGroup(info, new Properties(), record,
State.UNINITIALIZED));
+ Mockito.doReturn(record).when(group).getClusterRoleRecordFromEndpoint();
+ try {
+ assertNull("No per-group source should exist before init",
+ HAGroupMetricsManager.getIfPresent(haGroupName));
+ group.init();
+ assertNotNull("init must pre-register the per-group metrics source",
+ HAGroupMetricsManager.getIfPresent(haGroupName));
+ group.close();
+ assertNull("close must detach the per-group metrics source",
+ HAGroupMetricsManager.getIfPresent(haGroupName));
+ } finally {
+ HAGroupMetricsManager.remove(haGroupName);
+ }
+ }
}
diff --git
a/phoenix-core/src/test/java/org/apache/phoenix/monitoring/HAGroupClientMetricsSourceTest.java
b/phoenix-core/src/test/java/org/apache/phoenix/monitoring/HAGroupClientMetricsSourceTest.java
new file mode 100644
index 0000000000..552788577d
--- /dev/null
+++
b/phoenix-core/src/test/java/org/apache/phoenix/monitoring/HAGroupClientMetricsSourceTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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.monitoring;
+
+import static org.apache.phoenix.metrics.MetricConstants.HA_GROUP_TAG_NAME;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.management.ObjectName;
+import org.apache.hadoop.metrics2.MetricsTag;
+import org.apache.hadoop.metrics2.lib.DefaultMetricsSystem;
+import org.junit.After;
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link HAGroupClientMetricsSource}: the per-HA-group
metrics2 source stamps a
+ * {@code ha_group} tag carrying the group name, keeps per-group counters, and
detaches cleanly on
+ * {@link HAGroupClientMetricsSource#unregister()}.
+ * <p>
+ * Constructing a source registers it with the process-global {@link
DefaultMetricsSystem}, so each
+ * test uses a unique group name and {@link #tearDown()} unregisters every
source it created.
+ */
+public class HAGroupClientMetricsSourceTest {
+
+ private final List<HAGroupClientMetricsSource> created = new ArrayList<>();
+
+ private HAGroupClientMetricsSource newSource(String group) {
+ HAGroupClientMetricsSource source = new HAGroupClientMetricsSource(group);
+ created.add(source);
+ return source;
+ }
+
+ @After
+ public void tearDown() {
+ for (HAGroupClientMetricsSource source : created) {
+ source.unregister();
+ }
+ created.clear();
+ }
+
+ @Test
+ public void testTagCarriesUnquotedGroupName() {
+ String group = "srcTag";
+ HAGroupClientMetricsSource source = newSource(group);
+ MetricsTag tag = source.getMetricsRegistry().getTag(HA_GROUP_TAG_NAME);
+ assertNotNull("the ha_group tag must be present on the source", tag);
+ assertEquals("ha_group tag must carry the unquoted group name", group,
tag.value());
+ }
+
+ @Test
+ public void testJmxContextIsQuotedPerGroup() {
+ String group = "group,one=weird";
+ HAGroupClientMetricsSource source = newSource(group);
+ assertEquals(group,
source.getMetricsRegistry().getTag(HA_GROUP_TAG_NAME).value());
+ assertTrue(source.getMetricsJmxContext().endsWith(",haGroup=" +
ObjectName.quote(group)));
+ }
+
+ @Test
+ public void testIncrementAndUpdateArePerCounter() {
+ HAGroupClientMetricsSource source = newSource("srcCounters");
+ source.increment(MetricType.HA_FAILOVER_CONNECTION_FAILED_COUNTER);
+ source.increment(MetricType.HA_FAILOVER_CONNECTION_FAILED_COUNTER);
+ // The duration metric accumulates via update(); other counters are
untouched by it.
+ source.update(MetricType.CRR_TRANSITION_DURATION_MS, 40L);
+ source.update(MetricType.CRR_TRANSITION_DURATION_MS, 60L);
+
+ assertEquals(2L,
source.getCounterValue(MetricType.HA_FAILOVER_CONNECTION_FAILED_COUNTER));
+ assertEquals(100L,
source.getCounterValue(MetricType.CRR_TRANSITION_DURATION_MS));
+ assertEquals("an untouched counter stays at zero", 0L,
+ source.getCounterValue(MetricType.HA_STALE_CRR_DETECTED_COUNT));
+ }
+
+ @Test
+ public void testUnknownMetricTypeIsIgnored() {
+ HAGroupClientMetricsSource source = newSource("srcUnknown");
+ // A type that is not in METRIC_TYPES has no backing counter:
increment/update are no-ops and
+ // getCounterValue reports -1 to distinguish "absent" from a
present-but-zero counter.
+ source.increment(MetricType.MUTATION_BATCH_SIZE);
+ source.update(MetricType.MUTATION_BATCH_SIZE, 5L);
+ assertEquals(-1L, source.getCounterValue(MetricType.MUTATION_BATCH_SIZE));
+ }
+
+ @Test
+ public void testEachGroupIsADistinctRegisteredSource() {
+ HAGroupClientMetricsSource a = newSource("srcDistinctA");
+ HAGroupClientMetricsSource b = newSource("srcDistinctB");
+ assertNotEquals("distinct groups must get distinct JMX contexts",
a.getMetricsJmxContext(),
+ b.getMetricsJmxContext());
+
assertNotNull(DefaultMetricsSystem.instance().getSource(a.getMetricsJmxContext()));
+
assertNotNull(DefaultMetricsSystem.instance().getSource(b.getMetricsJmxContext()));
+ }
+
+ @Test
+ public void testUnregisterFreesTheSourceNameForReuse() {
+ String group = "srcReuse";
+ HAGroupClientMetricsSource first = new HAGroupClientMetricsSource(group);
+ String jmxContext = first.getMetricsJmxContext();
+ assertNotNull(DefaultMetricsSystem.instance().getSource(jmxContext));
+
+ first.unregister();
+ assertNull("unregister must free the source name in DefaultMetricsSystem",
+ DefaultMetricsSystem.instance().getSource(jmxContext));
+
+ // The same group name must be registrable again (would throw "source
already exists" if the
+ // prior source were still attached).
+ HAGroupClientMetricsSource second = newSource(group);
+
assertNotNull(DefaultMetricsSystem.instance().getSource(second.getMetricsJmxContext()));
+ assertEquals(jmxContext, second.getMetricsJmxContext());
+ }
+}
diff --git
a/phoenix-core/src/test/java/org/apache/phoenix/monitoring/HAGroupMetricsManagerTest.java
b/phoenix-core/src/test/java/org/apache/phoenix/monitoring/HAGroupMetricsManagerTest.java
new file mode 100644
index 0000000000..3d4ed86b04
--- /dev/null
+++
b/phoenix-core/src/test/java/org/apache/phoenix/monitoring/HAGroupMetricsManagerTest.java
@@ -0,0 +1,164 @@
+/*
+ * 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.monitoring;
+
+import static org.apache.phoenix.metrics.MetricConstants.HA_GROUP_TAG_NAME;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hadoop.metrics2.lib.DefaultMetricsSystem;
+import org.apache.phoenix.query.QueryServicesOptions;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link HAGroupMetricsManager}: the per-HA-group source
registry. Covers per-group
+ * isolation (two groups never cross-count), that each group registers as its
own
+ * {@code ha_group}-tagged metrics2 source, and that {@link
HAGroupMetricsManager#remove(String)}
+ * detaches the source so the same group can be re-created fresh.
+ * <p>
+ * These tests mutate process-global state ({@link DefaultMetricsSystem} and
the manager's static
+ * map), so each group name is unique per test and {@link #tearDown()} removes
every group this
+ * class created to avoid leaking sources into sibling tests.
+ */
+public class HAGroupMetricsManagerTest {
+
+ private final List<String> createdGroups = new ArrayList<>();
+
+ @BeforeClass
+ public static void assumeGlobalMetricsEnabled() {
+ // The manager creates sources only when global metrics are enabled (the
default). If a
+ // surrounding config disabled them, getOrCreate would return null and
these assertions would
+ // not apply.
+ assertTrue("these tests assume the default global-metrics-enabled=true",
+ QueryServicesOptions.withDefaults().isGlobalMetricsEnabled());
+ }
+
+ @Before
+ public void setUp() {
+ createdGroups.clear();
+ }
+
+ @After
+ public void tearDown() {
+ for (String group : createdGroups) {
+ HAGroupMetricsManager.remove(group);
+ }
+ }
+
+ private String track(String group) {
+ createdGroups.add(group);
+ return group;
+ }
+
+ @Test
+ public void testGetOrCreateIsIdempotentAndRegistersSource() {
+ String group = track("mgrCreate");
+ HAGroupClientMetricsSource first =
HAGroupMetricsManager.getOrCreate(group);
+ HAGroupClientMetricsSource second =
HAGroupMetricsManager.getOrCreate(group);
+ assertNotNull(first);
+ assertSame("getOrCreate must be idempotent for a group name", first,
second);
+ assertNotNull("the group's source must be registered with
DefaultMetricsSystem",
+ DefaultMetricsSystem.instance().getSource(first.getMetricsJmxContext()));
+ assertEquals("the source must carry the ha_group tag", group,
+ first.getMetricsRegistry().getTag(HA_GROUP_TAG_NAME).value());
+ }
+
+ @Test
+ public void testNullAndEmptyGroupNameIsNoOp() {
+ assertNull(HAGroupMetricsManager.getOrCreate(null));
+ assertNull(HAGroupMetricsManager.getOrCreate(""));
+ // remove must tolerate null/empty without throwing.
+ HAGroupMetricsManager.remove(null);
+ HAGroupMetricsManager.remove("");
+ }
+
+ @Test
+ public void testTwoGroupsDoNotCrossCount() {
+ String groupA = track("mgrIsoA");
+ String groupB = track("mgrIsoB");
+ MetricType type = MetricType.HA_FAILOVER_CONNECTION_FAILED_COUNTER;
+
+ HAGroupMetricsManager.increment(groupA, type);
+ HAGroupMetricsManager.increment(groupA, type);
+ HAGroupMetricsManager.increment(groupB, type);
+
+ assertEquals(2L,
HAGroupMetricsManager.getIfPresent(groupA).getCounterValue(type));
+ assertEquals("each HA group must maintain independent counters", 1L,
+ HAGroupMetricsManager.getIfPresent(groupB).getCounterValue(type));
+ }
+
+ @Test
+ public void testEachGroupGetsADistinctTaggedSource() {
+ String groupA = track("mgrDistinctA");
+ String groupB = track("mgrDistinctB");
+ HAGroupClientMetricsSource regA =
HAGroupMetricsManager.getOrCreate(groupA);
+ HAGroupClientMetricsSource regB =
HAGroupMetricsManager.getOrCreate(groupB);
+
+ assertNotEquals("distinct groups must map to distinct JMX contexts",
+ regA.getMetricsJmxContext(), regB.getMetricsJmxContext());
+ assertEquals(groupA,
regA.getMetricsRegistry().getTag(HA_GROUP_TAG_NAME).value());
+ assertEquals(groupB,
regB.getMetricsRegistry().getTag(HA_GROUP_TAG_NAME).value());
+ }
+
+ @Test
+ public void testUpdateAccumulatesPerGroupDuration() {
+ String group = track("mgrDuration");
+ MetricType type = MetricType.CRR_TRANSITION_DURATION_MS;
+ HAGroupMetricsManager.update(group, type, 40L);
+ HAGroupMetricsManager.update(group, type, 60L);
+ assertEquals(100L,
HAGroupMetricsManager.getIfPresent(group).getCounterValue(type));
+ }
+
+ @Test
+ public void testRemoveDetachesSource() {
+ String group = "mgrRemove"; // not tracked: this test removes it itself
+ HAGroupClientMetricsSource source =
HAGroupMetricsManager.getOrCreate(group);
+ String jmxContext = source.getMetricsJmxContext();
+ assertNotNull(HAGroupMetricsManager.getIfPresent(group));
+ assertNotNull(DefaultMetricsSystem.instance().getSource(jmxContext));
+
+ HAGroupMetricsManager.remove(group);
+
+ assertNull("holder must be gone after remove",
HAGroupMetricsManager.getIfPresent(group));
+ assertNull("source must be detached from DefaultMetricsSystem after
remove",
+ DefaultMetricsSystem.instance().getSource(jmxContext));
+ }
+
+ @Test
+ public void testGetOrCreateAfterRemoveRebuildsFresh() {
+ String group = track("mgrRebuild");
+ MetricType type = MetricType.HA_STALE_CRR_DETECTED_COUNT;
+ HAGroupMetricsManager.increment(group, type);
+ assertEquals(1L,
HAGroupMetricsManager.getIfPresent(group).getCounterValue(type));
+
+ HAGroupMetricsManager.remove(group);
+ // A re-create after remove must yield a fresh source with zeroed counters.
+ HAGroupClientMetricsSource rebuilt =
HAGroupMetricsManager.getOrCreate(group);
+ assertNotNull(rebuilt);
+ assertEquals("re-created group must start from zero", 0L,
rebuilt.getCounterValue(type));
+ }
+}