Skip to content

Commit 4440cd4

Browse files
chore: Add metrics for active node percentage (#18655)
Signed-off-by: Neeharika-Sompalli <[email protected]> Signed-off-by: Neeharika Sompalli <[email protected]> Co-authored-by: Matt Hess <[email protected]>
1 parent 276e27a commit 4440cd4

File tree

5 files changed

+322
-7
lines changed

5 files changed

+322
-7
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
package com.hedera.node.app.metrics;
3+
4+
import static java.util.Objects.requireNonNull;
5+
6+
import com.hedera.hapi.node.state.roster.RosterEntry;
7+
import com.swirlds.common.metrics.RunningAverageMetric;
8+
import com.swirlds.metrics.api.DoubleGauge;
9+
import com.swirlds.metrics.api.Metrics;
10+
import edu.umd.cs.findbugs.annotations.NonNull;
11+
import java.util.HashMap;
12+
import java.util.List;
13+
import java.util.Map;
14+
import javax.inject.Inject;
15+
import javax.inject.Singleton;
16+
import org.apache.logging.log4j.LogManager;
17+
import org.apache.logging.log4j.Logger;
18+
19+
@Singleton
20+
public class NodeMetrics {
21+
private static final String APP_CATEGORY = "app_";
22+
private static final Logger log = LogManager.getLogger(NodeMetrics.class);
23+
private final Map<Long, RunningAverageMetric> activeRoundsAverages = new HashMap<>();
24+
private final Map<Long, DoubleGauge> activeRoundsSnapshots = new HashMap<>();
25+
private final Metrics metrics;
26+
27+
@Inject
28+
public NodeMetrics(@NonNull final Metrics metrics) {
29+
this.metrics = requireNonNull(metrics);
30+
}
31+
32+
/**
33+
* Registers the metrics for the active round % for each node in the given roster.
34+
*
35+
* @param rosterEntries the list of roster entries
36+
*/
37+
public void registerNodeMetrics(@NonNull List<RosterEntry> rosterEntries) {
38+
for (final var entry : rosterEntries) {
39+
final var nodeId = entry.nodeId();
40+
final String name = "nodeActivePercent_node" + nodeId;
41+
final String snapshotName = "nodeActivePercentSnapshot_node" + nodeId;
42+
43+
if (!activeRoundsAverages.containsKey(nodeId)) {
44+
final var averageMetric = metrics.getOrCreate(new RunningAverageMetric.Config(APP_CATEGORY, name)
45+
.withDescription("Active round % average for node " + nodeId));
46+
activeRoundsAverages.put(nodeId, averageMetric);
47+
}
48+
49+
if (!activeRoundsSnapshots.containsKey(nodeId)) {
50+
final var snapshot = metrics.getOrCreate(new DoubleGauge.Config(APP_CATEGORY, snapshotName)
51+
.withDescription("Active round % snapshot for node " + nodeId));
52+
activeRoundsSnapshots.put(nodeId, snapshot);
53+
}
54+
}
55+
}
56+
57+
/**
58+
* Updates the active round percentage for a node.
59+
*
60+
* @param nodeId the node ID
61+
* @param activePercent the active round percentage
62+
*/
63+
public void updateNodeActiveMetrics(final long nodeId, final double activePercent) {
64+
if (activeRoundsAverages.containsKey(nodeId)) {
65+
activeRoundsAverages.get(nodeId).update(activePercent);
66+
}
67+
if (activeRoundsSnapshots.containsKey(nodeId)) {
68+
activeRoundsSnapshots.get(nodeId).set(activePercent);
69+
}
70+
}
71+
}

hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/NodeRewardManager.java

+29-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import static com.swirlds.platform.state.service.schemas.V0540PlatformStateSchema.PLATFORM_STATE_KEY;
1010
import static java.util.Objects.requireNonNull;
1111
import static java.util.stream.Collectors.toCollection;
12+
import static java.util.stream.Collectors.toMap;
1213

1314
import com.google.common.annotations.VisibleForTesting;
1415
import com.hedera.hapi.node.state.roster.RosterEntry;
@@ -19,6 +20,7 @@
1920
import com.hedera.node.app.fees.ExchangeRateManager;
2021
import com.hedera.node.app.ids.EntityIdService;
2122
import com.hedera.node.app.ids.ReadableEntityIdStoreImpl;
23+
import com.hedera.node.app.metrics.NodeMetrics;
2224
import com.hedera.node.app.roster.RosterService;
2325
import com.hedera.node.app.service.token.TokenService;
2426
import com.hedera.node.app.service.token.impl.ReadableAccountStoreImpl;
@@ -36,6 +38,7 @@
3638
import com.swirlds.state.lifecycle.EntityIdFactory;
3739
import com.swirlds.state.spi.CommittableWritableStates;
3840
import edu.umd.cs.findbugs.annotations.NonNull;
41+
import edu.umd.cs.findbugs.annotations.Nullable;
3942
import java.math.BigInteger;
4043
import java.time.Instant;
4144
import java.util.HashSet;
@@ -44,6 +47,8 @@
4447
import java.util.TreeMap;
4548
import javax.inject.Inject;
4649
import javax.inject.Singleton;
50+
import org.apache.logging.log4j.LogManager;
51+
import org.apache.logging.log4j.Logger;
4752

4853
/**
4954
* Manages the node rewards for the network. This includes tracking the number of rounds in the current staking
@@ -53,6 +58,7 @@
5358
*/
5459
@Singleton
5560
public class NodeRewardManager {
61+
private static final Logger log = LogManager.getLogger(NodeRewardManager.class);
5662
private final ConfigProvider configProvider;
5763
private final EntityIdFactory entityIdFactory;
5864
private final ExchangeRateManager exchangeRateManager;
@@ -62,15 +68,18 @@ public class NodeRewardManager {
6268
// The number of rounds each node missed creating judge. This is updated from state at the start of every round
6369
// and will be written back to state at the end of every block
6470
private final SortedMap<Long, Long> missedJudgeCounts = new TreeMap<>();
71+
private final NodeMetrics metrics;
6572

6673
@Inject
6774
public NodeRewardManager(
6875
@NonNull final ConfigProvider configProvider,
6976
@NonNull final EntityIdFactory entityIdFactory,
70-
@NonNull final ExchangeRateManager exchangeRateManager) {
77+
@NonNull final ExchangeRateManager exchangeRateManager,
78+
@Nullable final NodeMetrics metrics) {
7179
this.configProvider = requireNonNull(configProvider);
7280
this.entityIdFactory = requireNonNull(entityIdFactory);
7381
this.exchangeRateManager = requireNonNull(exchangeRateManager);
82+
this.metrics = metrics;
7483
}
7584

7685
public void onOpenBlock(@NonNull final State state) {
@@ -88,7 +97,8 @@ public void onOpenBlock(@NonNull final State state) {
8897

8998
/**
9099
* Updates node rewards state at the end of a block given the collected node fees.
91-
* @param state the state
100+
*
101+
* @param state the state
92102
* @param nodeFeesCollected the fees collected into node accounts in the block
93103
*/
94104
public void onCloseBlock(@NonNull final State state, final long nodeFeesCollected) {
@@ -192,6 +202,8 @@ public boolean maybeRewardActiveNodes(
192202
requireNonNull(rosterStore.getActiveRoster()).rosterEntries();
193203
final var activeNodeIds =
194204
nodeRewardStore.getActiveNodeIds(currentRoster, nodesConfig.activeRoundsPercent());
205+
// Update metrics for the nodes that were active in the last staking period
206+
updateNodeMetrics(currentRoster, nodeRewardStore);
195207

196208
// And pay whatever rewards the network can afford
197209
final var rewardsAccountId = entityIdFactory.newAccountId(
@@ -240,6 +252,21 @@ public boolean maybeRewardActiveNodes(
240252
return true;
241253
}
242254

255+
private void updateNodeMetrics(
256+
final List<RosterEntry> rosterEntries, final WritableNodeRewardsStoreImpl nodeRewardStore) {
257+
final long roundsLastPeriod = nodeRewardStore.get().numRoundsInStakingPeriod();
258+
metrics.registerNodeMetrics(rosterEntries);
259+
final var missedJudgeCounts = nodeRewardStore.get().nodeActivities().stream()
260+
.collect(toMap(NodeActivity::nodeId, NodeActivity::numMissedJudgeRounds));
261+
rosterEntries.forEach(node -> {
262+
final var nodeId = node.nodeId();
263+
final var missedJudges = missedJudgeCounts.getOrDefault(nodeId, 0L);
264+
final var activeRounds = Math.max(roundsLastPeriod - missedJudges, 0);
265+
final var activePercent = activeRounds == 0 ? 0 : ((double) ((activeRounds * 100) / roundsLastPeriod));
266+
metrics.updateNodeActiveMetrics(nodeId, activePercent);
267+
});
268+
}
269+
243270
/**
244271
* Gets the node reward info state from the given state.
245272
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
package com.hedera.node.app.metrics;
3+
4+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
5+
import static org.junit.jupiter.api.Assertions.assertEquals;
6+
import static org.junit.jupiter.api.Assertions.assertThrows;
7+
import static org.mockito.ArgumentMatchers.any;
8+
import static org.mockito.Mockito.*;
9+
10+
import com.hedera.hapi.node.state.roster.RosterEntry;
11+
import com.hedera.pbj.runtime.io.buffer.Bytes;
12+
import com.swirlds.common.metrics.RunningAverageMetric;
13+
import com.swirlds.metrics.api.DoubleGauge;
14+
import com.swirlds.metrics.api.Metrics;
15+
import java.util.List;
16+
import org.junit.jupiter.api.BeforeEach;
17+
import org.junit.jupiter.api.Test;
18+
import org.junit.jupiter.api.extension.ExtendWith;
19+
import org.mockito.ArgumentCaptor;
20+
import org.mockito.Mock;
21+
import org.mockito.junit.jupiter.MockitoExtension;
22+
23+
/**
24+
* Unit tests for the NodeMetrics class.
25+
*/
26+
@ExtendWith(MockitoExtension.class)
27+
class NodeMetricsTest {
28+
@Mock
29+
private Metrics metrics;
30+
31+
@Mock
32+
private RunningAverageMetric averageMetric;
33+
34+
@Mock
35+
private DoubleGauge doubleGauge;
36+
37+
private NodeMetrics nodeMetrics;
38+
39+
@BeforeEach
40+
void setUp() {
41+
nodeMetrics = new NodeMetrics(metrics);
42+
}
43+
44+
@Test
45+
void constructorTest() {
46+
assertThrows(NullPointerException.class, () -> new NodeMetrics(null));
47+
}
48+
49+
@Test
50+
void registerNodeMetrics() {
51+
long nodeId = 1L;
52+
RosterEntry entry = new RosterEntry(nodeId, 0, Bytes.EMPTY, List.of());
53+
List<RosterEntry> roster = List.of(entry);
54+
55+
when(metrics.getOrCreate(any(RunningAverageMetric.Config.class))).thenReturn(averageMetric);
56+
when(metrics.getOrCreate(any(DoubleGauge.Config.class))).thenReturn(doubleGauge);
57+
58+
nodeMetrics.registerNodeMetrics(roster);
59+
60+
double activePercent = 0.75;
61+
nodeMetrics.updateNodeActiveMetrics(nodeId, activePercent);
62+
63+
verify(averageMetric, times(1)).update(activePercent);
64+
verify(doubleGauge, times(1)).set(activePercent);
65+
}
66+
67+
@Test
68+
void registerNodeMetricsDuplicateEntriesRegistersOnlyOnce() {
69+
long nodeId = 2L;
70+
RosterEntry entry1 = new RosterEntry(nodeId, 0, Bytes.EMPTY, List.of());
71+
RosterEntry entry2 = new RosterEntry(nodeId, 0, Bytes.EMPTY, List.of());
72+
List<RosterEntry> roster = List.of(entry1, entry2);
73+
74+
when(metrics.getOrCreate(any(RunningAverageMetric.Config.class))).thenReturn(averageMetric);
75+
when(metrics.getOrCreate(any(DoubleGauge.Config.class))).thenReturn(doubleGauge);
76+
77+
nodeMetrics.registerNodeMetrics(roster);
78+
79+
double activePercent = 0.9;
80+
nodeMetrics.updateNodeActiveMetrics(nodeId, activePercent);
81+
82+
verify(averageMetric, times(1)).update(activePercent);
83+
verify(doubleGauge, times(1)).set(activePercent);
84+
}
85+
86+
@Test
87+
void updateNodeActiveMetricsNoMetricsRegistered() {
88+
long nodeId = 3L;
89+
assertDoesNotThrow(() -> nodeMetrics.updateNodeActiveMetrics(nodeId, 0.5));
90+
91+
verifyNoInteractions(averageMetric, doubleGauge);
92+
}
93+
94+
@Test
95+
void registerNodeMetricsConfigurationPassedToMetrics() {
96+
long nodeId = 4L;
97+
RosterEntry entry = new RosterEntry(nodeId, 0, Bytes.EMPTY, List.of());
98+
List<RosterEntry> roster = List.of(entry);
99+
100+
when(metrics.getOrCreate(any(RunningAverageMetric.Config.class))).thenReturn(averageMetric);
101+
when(metrics.getOrCreate(any(DoubleGauge.Config.class))).thenReturn(doubleGauge);
102+
103+
nodeMetrics.registerNodeMetrics(roster);
104+
105+
ArgumentCaptor<RunningAverageMetric.Config> avgConfigCaptor =
106+
ArgumentCaptor.forClass(RunningAverageMetric.Config.class);
107+
verify(metrics, times(1)).getOrCreate(avgConfigCaptor.capture());
108+
RunningAverageMetric.Config avgConfig = avgConfigCaptor.getValue();
109+
110+
ArgumentCaptor<DoubleGauge.Config> gaugeConfigCaptor = ArgumentCaptor.forClass(DoubleGauge.Config.class);
111+
verify(metrics, times(1)).getOrCreate(gaugeConfigCaptor.capture());
112+
DoubleGauge.Config gaugeConfig = gaugeConfigCaptor.getValue();
113+
114+
assertEquals("app_", avgConfig.getCategory(), "Average metric category should be 'app_'");
115+
assertEquals(
116+
"nodeActivePercent_node" + nodeId,
117+
avgConfig.getName(),
118+
"Average metric name should be 'nodeActivePercent_node{nodeId}'");
119+
120+
assertEquals("app_", gaugeConfig.getCategory(), "Gauge metric category should be 'app_'");
121+
assertEquals(
122+
"nodeActivePercentSnapshot_node" + nodeId,
123+
gaugeConfig.getName(),
124+
"Gauge metric name should be 'nodeActivePercentSnapshot_node{nodeId}'");
125+
}
126+
}

hedera-node/hedera-app/src/test/java/com/hedera/node/app/services/NodeRewardManagerTest.java

+10-4
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import com.hedera.node.app.blocks.BlockStreamService;
3535
import com.hedera.node.app.fees.ExchangeRateManager;
3636
import com.hedera.node.app.ids.EntityIdService;
37+
import com.hedera.node.app.metrics.NodeMetrics;
3738
import com.hedera.node.app.roster.RosterService;
3839
import com.hedera.node.app.service.token.TokenService;
3940
import com.hedera.node.app.spi.fixtures.ids.FakeEntityIdFactoryImpl;
@@ -42,6 +43,7 @@
4243
import com.hedera.node.config.VersionedConfigImpl;
4344
import com.hedera.node.config.testfixtures.HederaTestConfigBuilder;
4445
import com.hedera.pbj.runtime.io.buffer.Bytes;
46+
import com.swirlds.common.metrics.noop.NoOpMetrics;
4547
import com.swirlds.platform.state.service.PlatformStateService;
4648
import com.swirlds.platform.state.service.WritableRosterStore;
4749
import com.swirlds.state.State;
@@ -109,7 +111,8 @@ void setUp() {
109111
.withValue("staking.periodMins", 1)
110112
.getOrCreateConfig();
111113
given(configProvider.getConfiguration()).willReturn(new VersionedConfigImpl(config, 1));
112-
nodeRewardManager = new NodeRewardManager(configProvider, entityIdFactory, exchangeRateManager);
114+
nodeRewardManager = new NodeRewardManager(
115+
configProvider, entityIdFactory, exchangeRateManager, new NodeMetrics(new NoOpMetrics()));
113116
}
114117

115118
@Test
@@ -154,7 +157,8 @@ void testMaybeRewardActiveNodeRewardsDisabled() {
154157
.getOrCreateConfig();
155158
given(configProvider.getConfiguration()).willReturn(new VersionedConfigImpl(config, 1L));
156159

157-
nodeRewardManager = new NodeRewardManager(configProvider, entityIdFactory, exchangeRateManager);
160+
nodeRewardManager = new NodeRewardManager(
161+
configProvider, entityIdFactory, exchangeRateManager, new NodeMetrics(new NoOpMetrics()));
158162

159163
nodeRewardManager.maybeRewardActiveNodes(state, Instant.now(), systemTransactions);
160164
verify(systemTransactions, never())
@@ -164,7 +168,8 @@ void testMaybeRewardActiveNodeRewardsDisabled() {
164168
@Test
165169
void testMaybeRewardActiveNodesWhenCurrentPeriod() {
166170
givenSetup(NodeRewards.DEFAULT, platformStateWithFreezeTime(null), null);
167-
nodeRewardManager = new NodeRewardManager(configProvider, entityIdFactory, exchangeRateManager);
171+
nodeRewardManager = new NodeRewardManager(
172+
configProvider, entityIdFactory, exchangeRateManager, new NodeMetrics(new NoOpMetrics()));
168173
nodeRewardManager.maybeRewardActiveNodes(state, NOW_MINUS_600, systemTransactions);
169174
verify(systemTransactions, never())
170175
.dispatchNodeRewards(any(), any(), any(), anyLong(), any(), anyLong(), anyLong(), any());
@@ -180,7 +185,8 @@ void testMaybeRewardActiveNodesWhenPreviousPeriod() {
180185
.stakingRewardsActivated(true)
181186
.build();
182187
givenSetup(NodeRewards.DEFAULT, platformStateWithFreezeTime(null), networkStakingRewards);
183-
nodeRewardManager = new NodeRewardManager(configProvider, entityIdFactory, exchangeRateManager);
188+
nodeRewardManager = new NodeRewardManager(
189+
configProvider, entityIdFactory, exchangeRateManager, new NodeMetrics(new NoOpMetrics()));
184190
when(exchangeRateManager.getTinybarsFromTinycents(anyLong(), any())).thenReturn(5000L);
185191

186192
nodeRewardManager.maybeRewardActiveNodes(state, NOW, systemTransactions);

0 commit comments

Comments
 (0)