This is an automated email from the ASF dual-hosted git repository.
xiangfu0 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push:
new ecb288d666d Add server-level consumption rate limit observability
metrics and fix byte-mode overflow (#18942)
ecb288d666d is described below
commit ecb288d666d4918b1a30e8dc185e0e761036db55
Author: NOOB <[email protected]>
AuthorDate: Tue Jul 14 09:49:51 2026 +0530
Add server-level consumption rate limit observability metrics and fix
byte-mode overflow (#18942)
* Add server-level consumption rate limit observability metrics and fix
byte-mode overflow
Server-level realtime consumption rate limiting had two observability gaps:
1. The server-wide quota utilization was published through the per-table
CONSUMPTION_QUOTA_UTILIZATION gauge keyed by the meter name
"realtimeRowsConsumed".
Being a server-wide value, exporters mapped it to a misleading,
per-table-shaped
series that server-level dashboards could not match.
2. QuotaUtilizationTracker/AsyncMetricEmitter aggregated the per-minute
unit count in
an int and cast the LongAdder sum to int in emit(). In byte-based
throttling mode
the per-minute byte sum for a busy server exceeds Integer.MAX_VALUE
(~35.8 MB/s over
the 60s window), overflowing to a negative utilization.
There was also no metric exposing the configured rate limit, so a dashboard
could not
show the cap or whether limiting was enabled.
Changes:
- Add SERVER_CONSUMPTION_QUOTA_UTILIZATION (global) and emit the
server-level
utilization through it instead of the per-table gauge.
- Add SERVER_CONSUMPTION_RATE_LIMIT (global) and CONSUMPTION_RATE_LIMIT
(per-table),
emitting the configured caps on limiter setup/config change (server gauge
is set to
-1 when disabled; server utilization is reset to 0 on disable so the two
server
gauges stay consistent).
- Widen the per-minute aggregate and the emit() read to long to fix the
byte-mode
overflow.
Backward-incompatible metric change: the server-wide quota utilization
moves from
consumptionQuotaUtilization{table="realtimeRowsConsumed"} to the new global
gauge
serverConsumptionQuotaUtilization; dashboards keyed on the old series
should switch.
* Add listener-level test verifying the rate-limit gauge updates on cluster
config change
* Do not reset the server utilization gauge on disable
The utilization gauge is a live measurement that stops updating once the
limiter is closed; on a rate-limit update the emitter keeps running so it
self-corrects, and on disable the -1 cap already signals any last value is
stale. Leaving it untouched keeps it consistent with the per-partition
utilization gauge, which is likewise not reset on removal.
* Remove per-partition rate limit gauges when the limit is removed
A partition rate limit change only takes effect when the next consuming
segment creates its rate limiter. When that creation finds no configured
limit (i.e. the limit was removed), remove the per-partition cap and
utilization gauges so they do not linger with stale values. Removing a
never-emitted gauge is a no-op, so this is safe for partitions that never
had a limit and creates no new series.
* Add lifecycle test: partition rate limit set, removed, then set again
Uses a real metrics registry to verify the cap gauge is registered with the
configured value, removed when a consumer is created without a limit, and
cleanly re-registered with the new value when the limit is configured again.
---
.../apache/pinot/common/metrics/ServerGauge.java | 8 +
.../realtime/RealtimeConsumptionRateManager.java | 84 +++++++++--
.../RealtimeConsumptionRateManagerTest.java | 161 ++++++++++++++++++++-
.../ServerRateLimitConfigChangeListenerTest.java | 17 +++
4 files changed, 254 insertions(+), 16 deletions(-)
diff --git
a/pinot-common/src/main/java/org/apache/pinot/common/metrics/ServerGauge.java
b/pinot-common/src/main/java/org/apache/pinot/common/metrics/ServerGauge.java
index 0a909ce4341..312ace46ee4 100644
---
a/pinot-common/src/main/java/org/apache/pinot/common/metrics/ServerGauge.java
+++
b/pinot-common/src/main/java/org/apache/pinot/common/metrics/ServerGauge.java
@@ -48,6 +48,14 @@ public enum ServerGauge implements AbstractMetrics.Gauge {
// Dedup metrics
DEDUP_PRIMARY_KEYS_COUNT("dedupPrimaryKeysCount", false),
CONSUMPTION_QUOTA_UTILIZATION("ratio", false),
+ // Server-level consumption rate limiting is server-wide, so these are
global gauges (no table/partition
+ // dimension), kept distinct from the per-partition
CONSUMPTION_QUOTA_UTILIZATION above.
+ SERVER_CONSUMPTION_QUOTA_UTILIZATION("ratio", true),
+ // Configured consumption rate limits, emitted when the limiter is created
or its config changes; the server gauge
+ // is set to -1 when server-level rate limiting is disabled. Units follow
the active throttling strategy
+ // (bytes/sec in byte mode, messages/sec in message mode).
+ SERVER_CONSUMPTION_RATE_LIMIT("perSecond", true),
+ CONSUMPTION_RATE_LIMIT("perSecond", false),
JVM_HEAP_USED_BYTES("bytes", true),
NETTY_POOLED_USED_DIRECT_MEMORY("bytes", true),
NETTY_POOLED_USED_HEAP_MEMORY("bytes", true),
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/data/manager/realtime/RealtimeConsumptionRateManager.java
b/pinot-core/src/main/java/org/apache/pinot/core/data/manager/realtime/RealtimeConsumptionRateManager.java
index 56d54ffdd3b..1c0ce0577dd 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/data/manager/realtime/RealtimeConsumptionRateManager.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/data/manager/realtime/RealtimeConsumptionRateManager.java
@@ -35,7 +35,6 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.LongAdder;
import org.apache.pinot.common.metrics.ServerGauge;
-import org.apache.pinot.common.metrics.ServerMeter;
import org.apache.pinot.common.metrics.ServerMetrics;
import org.apache.pinot.spi.env.PinotConfiguration;
import org.apache.pinot.spi.stream.MessageBatch;
@@ -65,8 +64,6 @@ public class RealtimeConsumptionRateManager {
private static final Logger LOGGER =
LoggerFactory.getLogger(RealtimeConsumptionRateManager.class);
private static final int CACHE_ENTRY_EXPIRATION_TIME_IN_MINUTES = 10;
- private static final String SERVER_CONSUMPTION_RATE_METRIC_KEY_NAME =
- ServerMeter.REALTIME_ROWS_CONSUMED.getMeterName();
private volatile ConsumptionRateLimiter _serverRateLimiter =
NOOP_RATE_LIMITER;
// stream config object is required for fetching the partition count from
the stream
@@ -147,6 +144,11 @@ public class RealtimeConsumptionRateManager {
// result of it, But the metric related to throttling won't be emitted
since as a result of here above the
// AsyncMetricEmitter will be closed. It's recommended to forceCommit
segments to avoid this.
}
+ // Expose the configured cap (-1 when disabled) so operators can see
that rate limiting is off. The server
+ // utilization gauge is intentionally not touched here: it is a global
gauge seeded at server startup, so
+ // removing it would be undone on restart, and rate-limit updates keep
the emitter running so it self-corrects
+ // on the common update path. The -1 cap already signals that any last
utilization value is stale.
+ emitServerRateLimit(serverMetrics, -1);
return;
}
@@ -154,11 +156,42 @@ public class RealtimeConsumptionRateManager {
ServerRateLimiter existingLimiter = (ServerRateLimiter)
_serverRateLimiter;
existingLimiter.updateRateLimit(serverRateLimitConfig._serverRateLimit,
serverRateLimitConfig._throttlingStrategy);
+ emitServerRateLimit(serverMetrics,
serverRateLimitConfig._serverRateLimit);
return;
}
_serverRateLimiter = new
ServerRateLimiter(serverRateLimitConfig._serverRateLimit, serverMetrics,
- SERVER_CONSUMPTION_RATE_METRIC_KEY_NAME,
serverRateLimitConfig._throttlingStrategy);
+ serverRateLimitConfig._throttlingStrategy);
+ emitServerRateLimit(serverMetrics, serverRateLimitConfig._serverRateLimit);
+ }
+
+ // Exposes the configured server-level consumption rate limit as a global
gauge so operators can see the cap
+ // (and whether it is enabled) without inspecting server config. Set on
setup and on each config change, mirroring
+ // the configured-threshold gauge pattern used by SegmentOperationsThrottler.
+ private static void emitServerRateLimit(ServerMetrics serverMetrics, double
rateLimit) {
+ if (serverMetrics != null) {
+ // Round rather than truncate so a sub-1 limit does not report 0 and
collide with the -1 "disabled" sentinel.
+
serverMetrics.setValueOfGlobalGauge(ServerGauge.SERVER_CONSUMPTION_RATE_LIMIT,
Math.round(rateLimit));
+ }
+ }
+
+ // Exposes the configured per-partition consumption rate limit alongside its
utilization gauge.
+ private static void emitPartitionRateLimit(ServerMetrics serverMetrics,
String metricKeyName,
+ double partitionRateLimit) {
+ if (serverMetrics != null && metricKeyName != null) {
+ serverMetrics.setValueOfTableGauge(metricKeyName,
ServerGauge.CONSUMPTION_RATE_LIMIT,
+ Math.round(partitionRateLimit));
+ }
+ }
+
+ // Removes the per-partition rate limit gauges when a consumer is created
without a rate limit, so that a
+ // previously configured limit that has since been removed does not linger
as stale series. Removing a gauge that
+ // was never emitted is a no-op, so this is safe (and cheap) for partitions
that never had a limit.
+ private static void removePartitionRateLimitGauges(ServerMetrics
serverMetrics, String metricKeyName) {
+ if (serverMetrics != null && metricKeyName != null) {
+ serverMetrics.removeTableGauge(metricKeyName,
ServerGauge.CONSUMPTION_RATE_LIMIT);
+ serverMetrics.removeTableGauge(metricKeyName,
ServerGauge.CONSUMPTION_QUOTA_UTILIZATION);
+ }
}
public void updateServerRateLimiter(ServerRateLimitConfig
serverRateLimitConfig, ServerMetrics serverMetrics) {
@@ -177,10 +210,14 @@ public class RealtimeConsumptionRateManager {
if (partitionRateLimit > 0) {
LOGGER.info("A consumption rate limiter is set up for topic {} in table
{} with partition rate limit: {}",
streamConfig.getTopicName(), tableName, partitionRateLimit);
+ emitPartitionRateLimit(serverMetrics, metricKeyName, partitionRateLimit);
return new PartitionRateLimiter(partitionRateLimit, serverMetrics,
metricKeyName);
}
double topicRateLimit = streamConfig.getTopicConsumptionRateLimit();
if (topicRateLimit <= 0) {
+ // No rate limit configured (possibly removed since the previous
consuming segment): clean up any gauges left
+ // behind by a previous limiter for this partition so the metrics
reflect the current config.
+ removePartitionRateLimitGauges(serverMetrics, metricKeyName);
return NOOP_RATE_LIMITER;
}
int partitionCount;
@@ -194,6 +231,7 @@ public class RealtimeConsumptionRateManager {
LOGGER.info("A consumption rate limiter is set up for topic {} in table {}
with rate limit: {} "
+ "(topic rate limit: {}, partition count: {})",
streamConfig.getTopicName(), tableName, partitionRateLimit,
topicRateLimit, partitionCount);
+ emitPartitionRateLimit(serverMetrics, metricKeyName, partitionRateLimit);
return new PartitionRateLimiter(partitionRateLimit, serverMetrics,
metricKeyName);
}
@@ -243,19 +281,33 @@ public class RealtimeConsumptionRateManager {
*/
static class QuotaUtilizationTracker {
private long _previousMinute = -1;
- private int _aggregateUnits = 0;
+ // Aggregated over a minute; must be long because in byte-throttling mode
this counts bytes/minute, which for a
+ // busy server easily exceeds Integer.MAX_VALUE (~35.8 MB/s over the 60s
window) and would otherwise overflow.
+ private long _aggregateUnits = 0;
private final ServerMetrics _serverMetrics;
private final String _metricKeyName;
+ private final boolean _serverLevel;
+ // Partition-level: emits the per-table/partition
CONSUMPTION_QUOTA_UTILIZATION gauge.
public QuotaUtilizationTracker(ServerMetrics serverMetrics, String
metricKeyName) {
+ this(serverMetrics, metricKeyName, false);
+ }
+
+ // Server-level: emits the server-wide (global)
SERVER_CONSUMPTION_QUOTA_UTILIZATION gauge.
+ public QuotaUtilizationTracker(ServerMetrics serverMetrics) {
+ this(serverMetrics, null, true);
+ }
+
+ private QuotaUtilizationTracker(ServerMetrics serverMetrics, String
metricKeyName, boolean serverLevel) {
_serverMetrics = serverMetrics;
_metricKeyName = metricKeyName;
+ _serverLevel = serverLevel;
}
/**
* Update count and return utilization ratio percentage (0 if not enough
data yet).
*/
- public int update(int unitsConsumed, double rateLimit, Instant now) {
+ public int update(long unitsConsumed, double rateLimit, Instant now) {
int ratioPercentage = 0;
long nowInMinutes = now.getEpochSecond() / 60;
if (nowInMinutes == _previousMinute) {
@@ -264,8 +316,12 @@ public class RealtimeConsumptionRateManager {
if (_previousMinute != -1) { // not first time
double actualRate = _aggregateUnits / ((nowInMinutes -
_previousMinute) * 60.0); // units per second
ratioPercentage = (int) Math.round(actualRate / rateLimit * 100);
- _serverMetrics.setValueOfTableGauge(_metricKeyName,
ServerGauge.CONSUMPTION_QUOTA_UTILIZATION,
- ratioPercentage);
+ if (_serverLevel) {
+
_serverMetrics.setValueOfGlobalGauge(ServerGauge.SERVER_CONSUMPTION_QUOTA_UTILIZATION,
ratioPercentage);
+ } else {
+ _serverMetrics.setValueOfTableGauge(_metricKeyName,
ServerGauge.CONSUMPTION_QUOTA_UTILIZATION,
+ ratioPercentage);
+ }
}
_aggregateUnits = unitsConsumed;
_previousMinute = nowInMinutes;
@@ -274,7 +330,7 @@ public class RealtimeConsumptionRateManager {
}
@VisibleForTesting
- int getAggregateUnits() {
+ long getAggregateUnits() {
return _aggregateUnits;
}
}
@@ -383,10 +439,10 @@ public class RealtimeConsumptionRateManager {
private final AsyncMetricEmitter _metricEmitter;
private ThrottlingStrategy _throttlingStrategy;
- public ServerRateLimiter(double initialRateLimit, ServerMetrics
serverMetrics, String metricKeyName,
+ public ServerRateLimiter(double initialRateLimit, ServerMetrics
serverMetrics,
ThrottlingStrategy throttlingStrategy) {
_rateLimiter = RateLimiter.create(initialRateLimit);
- _metricEmitter = new AsyncMetricEmitter(serverMetrics, metricKeyName,
initialRateLimit);
+ _metricEmitter = new AsyncMetricEmitter(serverMetrics, initialRateLimit);
_throttlingStrategy = throttlingStrategy;
_metricEmitter.start(); // start background emission
}
@@ -477,9 +533,9 @@ public class RealtimeConsumptionRateManager {
private final AtomicBoolean _running = new AtomicBoolean(false);
private final QuotaUtilizationTracker _tracker;
- public AsyncMetricEmitter(ServerMetrics serverMetrics, String
metricKeyName, double initialRateLimit) {
+ public AsyncMetricEmitter(ServerMetrics serverMetrics, double
initialRateLimit) {
_rateLimit = new AtomicDouble(initialRateLimit);
- _tracker = new QuotaUtilizationTracker(serverMetrics, metricKeyName);
+ _tracker = new QuotaUtilizationTracker(serverMetrics);
_executor = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "server-rate-limit-metric-emitter");
t.setDaemon(true);
@@ -512,7 +568,7 @@ public class RealtimeConsumptionRateManager {
try {
double rateLimit = _rateLimit.get();
Instant now = Instant.now();
- int count = (int) _messageCount.sumThenReset();
+ long count = _messageCount.sumThenReset();
_tracker.update(count, rateLimit, now);
} catch (Exception e) {
LOGGER.warn("Encountered an error while emitting the metrics.", e);
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/data/manager/realtime/RealtimeConsumptionRateManagerTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/data/manager/realtime/RealtimeConsumptionRateManagerTest.java
index bcc40a7d9ca..93acf64c3ec 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/data/manager/realtime/RealtimeConsumptionRateManagerTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/data/manager/realtime/RealtimeConsumptionRateManagerTest.java
@@ -26,19 +26,26 @@ import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
+import org.apache.pinot.common.metrics.ServerGauge;
import org.apache.pinot.common.metrics.ServerMetrics;
import org.apache.pinot.spi.env.PinotConfiguration;
+import org.apache.pinot.spi.metrics.PinotMetricUtils;
import org.apache.pinot.spi.stream.StreamConfig;
import org.apache.pinot.spi.utils.CommonConstants;
import org.apache.pinot.util.TestUtils;
import org.testng.annotations.Test;
import static
org.apache.pinot.core.data.manager.realtime.RealtimeConsumptionRateManager.*;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
public class RealtimeConsumptionRateManagerTest {
@@ -260,7 +267,7 @@ public class RealtimeConsumptionRateManagerTest {
@Test
public void testAsyncMetricEmitter()
throws InterruptedException {
- AsyncMetricEmitter emitter = new
AsyncMetricEmitter(mock(ServerMetrics.class), "testMetric", 10.0);
+ AsyncMetricEmitter emitter = new
AsyncMetricEmitter(mock(ServerMetrics.class), 10.0);
try {
emitter.start(0, 1);
Thread.sleep(1500); // Let emitter run at-least once
@@ -276,7 +283,7 @@ public class RealtimeConsumptionRateManagerTest {
emitter.close();
}
- AsyncMetricEmitter emitter1 = new
AsyncMetricEmitter(mock(ServerMetrics.class), "testMetric", 10.0);
+ AsyncMetricEmitter emitter1 = new
AsyncMetricEmitter(mock(ServerMetrics.class), 10.0);
try {
emitter1.start(10, 10);
for (int i = 0; i < 20; i++) {
@@ -287,11 +294,161 @@ public class RealtimeConsumptionRateManagerTest {
== 0)), 5000,
"Expected messageCount to be greater than zero because messageCount
will reset post initial delay (first "
+ "run).");
+ } finally {
+ emitter1.close();
+ }
+ }
+
+ @Test
+ public void testAsyncMetricEmitterByteOverflow()
+ throws InterruptedException {
+ // emit() previously cast the per-minute LongAdder sum to int; in byte
mode the sum can exceed Integer.MAX_VALUE
+ // and would overflow to a negative aggregate. This exercises the fixed
emit() path end to end.
+ AsyncMetricEmitter emitter = new
AsyncMetricEmitter(mock(ServerMetrics.class), 100_000_000.0);
+ try {
+ emitter.record(2_000_000_000);
+ emitter.record(2_000_000_000); // total 4e9, exceeds Integer.MAX_VALUE
+ emitter.start(0, 3600); // fire emit() once, ~immediately
+ TestUtils.waitForCondition(
+ aVoid -> emitter.getMessageCount().sum() == 0 &&
emitter.getTracker().getAggregateUnits() == 4_000_000_000L,
+ 5000, "emit() must read the LongAdder sum as long without int
truncation");
} finally {
emitter.close();
}
}
+ @Test
+ public void testQuotaUtilizationTrackerByteOverflow() {
+ // In byte-throttling mode the per-minute aggregate counts bytes and can
exceed Integer.MAX_VALUE. This is a
+ // regression for a (int) overflow that previously turned high byte rates
into a negative utilization.
+ ServerMetrics serverMetrics = mock(ServerMetrics.class);
+ QuotaUtilizationTracker tracker = new
QuotaUtilizationTracker(serverMetrics); // server-level
+ double byteRateLimit = 100_000_000; // 100 MB/s
+ Instant now = Clock.fixed(Instant.parse("2022-08-10T12:00:02Z"),
ZoneOffset.UTC).instant();
+ assertEquals(tracker.update(2_000_000_000, byteRateLimit, now), 0); //
first minute, no emit
+ now = Clock.fixed(Instant.parse("2022-08-10T12:00:40Z"),
ZoneOffset.UTC).instant();
+ assertEquals(tracker.update(4_000_000_000L, byteRateLimit, now), 0); //
same minute: aggregate = 6e9 (> 2^32)
+ assertEquals(tracker.getAggregateUnits(), 6_000_000_000L); // long: not
overflowed to a negative int
+ // next minute: 6e9 bytes / 60s = 100 MB/s == limit -> 100% (pre-fix the
overflow produced a negative ratio)
+ now = Clock.fixed(Instant.parse("2022-08-10T12:01:02Z"),
ZoneOffset.UTC).instant();
+ assertEquals(tracker.update(0, byteRateLimit, now), 100);
+ }
+
+ @Test
+ public void testQuotaUtilizationTrackerServerVsPartitionGauge() {
+ Instant firstMinute = Clock.fixed(Instant.parse("2022-08-10T12:00:02Z"),
ZoneOffset.UTC).instant();
+ Instant secondMinute = Clock.fixed(Instant.parse("2022-08-10T12:01:02Z"),
ZoneOffset.UTC).instant();
+
+ // Server-level tracker emits the server-wide (global) gauge, never the
per-table gauge.
+ ServerMetrics serverMetrics = mock(ServerMetrics.class);
+ QuotaUtilizationTracker serverTracker = new
QuotaUtilizationTracker(serverMetrics);
+ serverTracker.update(3000, 50, firstMinute);
+ serverTracker.update(0, 50, secondMinute);
+
verify(serverMetrics).setValueOfGlobalGauge(eq(ServerGauge.SERVER_CONSUMPTION_QUOTA_UTILIZATION),
anyLong());
+ verify(serverMetrics, never()).setValueOfTableGauge(anyString(),
+ eq(ServerGauge.CONSUMPTION_QUOTA_UTILIZATION), anyLong());
+
+ // Partition-level tracker emits the per-table/partition gauge, never the
server-wide gauge.
+ ServerMetrics partitionMetrics = mock(ServerMetrics.class);
+ QuotaUtilizationTracker partitionTracker =
+ new QuotaUtilizationTracker(partitionMetrics,
"tableA_REALTIME-topicA-0");
+ partitionTracker.update(3000, 50, firstMinute);
+ partitionTracker.update(0, 50, secondMinute);
+
verify(partitionMetrics).setValueOfTableGauge(eq("tableA_REALTIME-topicA-0"),
+ eq(ServerGauge.CONSUMPTION_QUOTA_UTILIZATION), anyLong());
+ verify(partitionMetrics, never()).setValueOfGlobalGauge(
+ eq(ServerGauge.SERVER_CONSUMPTION_QUOTA_UTILIZATION), anyLong());
+ }
+
+ @Test
+ public void testServerRateLimitGaugeEmitted() {
+ RealtimeConsumptionRateManager manager = new
RealtimeConsumptionRateManager(mock(LoadingCache.class));
+
+ // Byte rate limit configured -> gauge exposes the configured cap.
+ ServerMetrics metrics = mock(ServerMetrics.class);
+ PinotConfiguration config = mock(PinotConfiguration.class);
+
when(config.getProperty(CommonConstants.Server.CONFIG_OF_SERVER_CONSUMPTION_RATE_LIMIT,
+
CommonConstants.Server.DEFAULT_SERVER_CONSUMPTION_RATE_LIMIT)).thenReturn(0.0);
+
when(config.getProperty(CommonConstants.Server.CONFIG_OF_SERVER_CONSUMPTION_RATE_LIMIT_BYTES,
+
CommonConstants.Server.DEFAULT_SERVER_CONSUMPTION_RATE_LIMIT)).thenReturn(112_000_000.0);
+ ServerRateLimiter limiter = (ServerRateLimiter)
manager.createServerRateLimiter(config, metrics);
+ try {
+
verify(metrics).setValueOfGlobalGauge(ServerGauge.SERVER_CONSUMPTION_RATE_LIMIT,
112_000_000L);
+ } finally {
+ limiter.close();
+ }
+
+ // Disabled -> gauge is set to -1 so operators can still see rate limiting
is off.
+ ServerMetrics disabledMetrics = mock(ServerMetrics.class);
+ PinotConfiguration disabledConfig = mock(PinotConfiguration.class);
+
when(disabledConfig.getProperty(CommonConstants.Server.CONFIG_OF_SERVER_CONSUMPTION_RATE_LIMIT,
+
CommonConstants.Server.DEFAULT_SERVER_CONSUMPTION_RATE_LIMIT)).thenReturn(0.0);
+
when(disabledConfig.getProperty(CommonConstants.Server.CONFIG_OF_SERVER_CONSUMPTION_RATE_LIMIT_BYTES,
+
CommonConstants.Server.DEFAULT_SERVER_CONSUMPTION_RATE_LIMIT)).thenReturn(0.0);
+ manager.createServerRateLimiter(disabledConfig, disabledMetrics);
+
verify(disabledMetrics).setValueOfGlobalGauge(ServerGauge.SERVER_CONSUMPTION_RATE_LIMIT,
-1L);
+ }
+
+ @Test
+ public void testPartitionRateLimitGaugeEmitted() {
+ // Topic-derived: STREAM_CONFIG_A topic rate limit 50 over 10 partitions
-> per-partition rate limit 5.
+ ServerMetrics metrics = mock(ServerMetrics.class);
+ CONSUMPTION_RATE_MANAGER.createRateLimiter(STREAM_CONFIG_A, TABLE_NAME,
metrics, "tableA_REALTIME-topicA-0");
+ verify(metrics).setValueOfTableGauge("tableA_REALTIME-topicA-0",
ServerGauge.CONSUMPTION_RATE_LIMIT, 5L);
+
+ // Direct partition rate limit: STREAM_CONFIG_D specifies 4.0 per
partition, used as-is.
+ ServerMetrics directMetrics = mock(ServerMetrics.class);
+ CONSUMPTION_RATE_MANAGER.createRateLimiter(STREAM_CONFIG_D, TABLE_NAME,
directMetrics, "tableD_REALTIME-topicD-0");
+ verify(directMetrics).setValueOfTableGauge("tableD_REALTIME-topicD-0",
ServerGauge.CONSUMPTION_RATE_LIMIT, 4L);
+
+ // No rate limit configured (STREAM_CONFIG_C) -> NOOP limiter, no gauge
emitted, and any gauges left behind by a
+ // previously configured (since removed) limit are cleaned up so they do
not linger as stale series.
+ ServerMetrics noneMetrics = mock(ServerMetrics.class);
+ ConsumptionRateLimiter limiter =
+ CONSUMPTION_RATE_MANAGER.createRateLimiter(STREAM_CONFIG_C,
TABLE_NAME, noneMetrics, "keyC");
+ assertEquals(limiter, NOOP_RATE_LIMITER);
+ verify(noneMetrics, never()).setValueOfTableGauge(anyString(),
eq(ServerGauge.CONSUMPTION_RATE_LIMIT), anyLong());
+ verify(noneMetrics).removeTableGauge("keyC",
ServerGauge.CONSUMPTION_RATE_LIMIT);
+ verify(noneMetrics).removeTableGauge("keyC",
ServerGauge.CONSUMPTION_QUOTA_UTILIZATION);
+ }
+
+ @Test
+ public void testPartitionRateLimitGaugeLifecycle() {
+ // set -> removed -> set again across consuming segments, against a real
metrics registry to verify the gauge is
+ // registered, removed and cleanly re-registered as each new consumer
picks up the current config.
+ ServerMetrics metrics = new
ServerMetrics(PinotMetricUtils.getPinotMetricsRegistry());
+ String key = "tableL_REALTIME-topicL-3";
+ String capGaugeName = ServerGauge.CONSUMPTION_RATE_LIMIT.getGaugeName() +
"." + key;
+
+ // Limit set (STREAM_CONFIG_D: direct partition limit 4): cap gauge
registered with the configured value.
+ CONSUMPTION_RATE_MANAGER.createRateLimiter(STREAM_CONFIG_D, TABLE_NAME,
metrics, key);
+ assertEquals(metrics.getGaugeValue(capGaugeName), Long.valueOf(4));
+
+ // Limit removed (STREAM_CONFIG_C: no limit): the next consumer creation
removes the series.
+ CONSUMPTION_RATE_MANAGER.createRateLimiter(STREAM_CONFIG_C, TABLE_NAME,
metrics, key);
+ assertNull(metrics.getGaugeValue(capGaugeName));
+
+ // Limit set again (STREAM_CONFIG_A: topic limit 50 over 10 partitions):
gauge re-registers with the new value.
+ CONSUMPTION_RATE_MANAGER.createRateLimiter(STREAM_CONFIG_A, TABLE_NAME,
metrics, key);
+ assertEquals(metrics.getGaugeValue(capGaugeName), Long.valueOf(5));
+ }
+
+ @Test
+ public void testServerRateLimitGaugeEmittedOnUpdate() {
+ RealtimeConsumptionRateManager manager = new
RealtimeConsumptionRateManager(mock(LoadingCache.class));
+ ServerMetrics metrics = mock(ServerMetrics.class);
+ // Create an active server limiter, then update its cap -> gauge
re-emitted with the new value.
+ manager.updateServerRateLimiter(
+ new ServerRateLimitConfig(100_000_000,
ByteCountThrottlingStrategy.INSTANCE), metrics);
+ try {
+ manager.updateServerRateLimiter(
+ new ServerRateLimitConfig(200_000_000,
ByteCountThrottlingStrategy.INSTANCE), metrics);
+
verify(metrics).setValueOfGlobalGauge(ServerGauge.SERVER_CONSUMPTION_RATE_LIMIT,
200_000_000L);
+ } finally {
+ ((ServerRateLimiter) manager.getServerRateLimiter()).close();
+ }
+ }
+
private int calcExpectedRatio(double rateLimitInMinutes, int
sumOfMsgsInPrevMinute) {
return (int) Math.round(sumOfMsgsInPrevMinute / rateLimitInMinutes * 100);
}
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/data/manager/realtime/ServerRateLimitConfigChangeListenerTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/data/manager/realtime/ServerRateLimitConfigChangeListenerTest.java
index 4f8327c0e68..c1c99e1ecf8 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/data/manager/realtime/ServerRateLimitConfigChangeListenerTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/data/manager/realtime/ServerRateLimitConfigChangeListenerTest.java
@@ -25,6 +25,7 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
+import org.apache.pinot.common.metrics.ServerGauge;
import org.apache.pinot.common.metrics.ServerMetrics;
import org.apache.pinot.spi.env.PinotConfiguration;
import org.apache.pinot.spi.stream.MessageBatch;
@@ -33,6 +34,7 @@ import org.mockito.Mockito;
import org.testng.annotations.Test;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
@@ -113,6 +115,21 @@ public class ServerRateLimitConfigChangeListenerTest {
}
}
+ @Test
+ public void testRateLimitConfigChangeUpdatesGauge() {
+ ServerMetrics metrics = mock(ServerMetrics.class);
+ ServerRateLimitConfigChangeListener listener = new
ServerRateLimitConfigChangeListener(metrics);
+ String byteKey =
CommonConstants.Server.CONFIG_OF_SERVER_CONSUMPTION_RATE_LIMIT_BYTES;
+
+ // Enabling/updating the limit via cluster config re-emits the
configured-cap gauge with the new value.
+ listener.onChange(new HashSet<>(List.of(byteKey)), Map.of(byteKey,
"150000000"));
+
verify(metrics).setValueOfGlobalGauge(ServerGauge.SERVER_CONSUMPTION_RATE_LIMIT,
150_000_000L);
+
+ // Disabling via cluster config resets the gauge to -1.
+ listener.onChange(new HashSet<>(List.of(byteKey)), Map.of(byteKey, "0"));
+
verify(metrics).setValueOfGlobalGauge(ServerGauge.SERVER_CONSUMPTION_RATE_LIMIT,
-1L);
+ }
+
private void simulateThrottling(AtomicReference<Throwable> errorRef) {
// A helper method to test side effects of throttling during
serverRateLimit config change.
MessageBatch messageBatch = Mockito.mock(MessageBatch.class);
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]