Jackie-Jiang commented on code in PR #19686:
URL: https://github.com/apache/pinot/pull/19686#discussion_r4251535559
##########
pinot-common/src/main/java/org/apache/pinot/common/config/DefaultClusterConfigChangeHandler.java:
##########
@@ -19,50 +19,129 @@
package org.apache.pinot.common.config;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
import org.apache.helix.NotificationContext;
import org.apache.helix.api.listeners.BatchMode;
import org.apache.helix.api.listeners.ClusterConfigChangeListener;
import org.apache.helix.model.ClusterConfig;
import org.apache.pinot.spi.config.provider.PinotClusterConfigChangeListener;
import org.apache.pinot.spi.config.provider.PinotClusterConfigProvider;
+import org.apache.pinot.spi.env.PinotConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-/// Bridges Helix cluster config callbacks to
[PinotClusterConfigChangeListener]s. All access is serialized on this
-/// instance, so a listener sees the snapshot handed to it at registration and
every later change in order.
+/// Bridges Helix cluster config callbacks to
[PinotClusterConfigChangeListener]s. Instance configs captured at
+/// construction override cluster configs in the effective view exposed to
listeners and [#getClusterConfigs()]. All
+/// access is serialized on this instance, so a listener sees the snapshot
handed to it at registration and every later
+/// change in order.
@BatchMode(enabled = false)
public class DefaultClusterConfigChangeHandler implements
ClusterConfigChangeListener, PinotClusterConfigProvider {
private static final Logger LOGGER =
LoggerFactory.getLogger(DefaultClusterConfigChangeHandler.class);
+ private static final Comparator<String> CONFIG_KEY_COMPARATOR =
+
Comparator.comparing(DefaultClusterConfigChangeHandler::normalizeConfigKey);
private final List<PinotClusterConfigChangeListener> _listeners = new
ArrayList<>();
- private Map<String, String> _clusterConfigs = Map.of();
+ private final Map<String, String> _instanceConfigs;
+ private Map<String, String> _effectiveConfigs;
+
+ public DefaultClusterConfigChangeHandler() {
+ this(Map.of());
+ }
+
+ /// Snapshots the instance config at construction time. Callers must
construct the handler before folding cluster
+ /// configs into the instance config.
+ /// @param instanceConfig instance config to snapshot
+ public DefaultClusterConfigChangeHandler(PinotConfiguration instanceConfig) {
+ this(snapshot(instanceConfig));
+ }
+
+ DefaultClusterConfigChangeHandler(Map<String, String> instanceConfigs) {
+ _instanceConfigs = immutableConfigMap(instanceConfigs);
+ _effectiveConfigs = _instanceConfigs;
+ }
@Override
public synchronized void onClusterConfigChange(ClusterConfig clusterConfig,
NotificationContext context) {
Map<String, String> clusterConfigs =
copyWithoutNullValues(clusterConfig.getRecord().getSimpleFields());
- Set<String> changedConfigs = getChangedProperties(_clusterConfigs,
clusterConfigs);
+ Map<String, String> effectiveConfigs = getEffectiveConfigs(clusterConfigs);
+ Set<String> changedConfigs =
getChangedEffectiveProperties(_effectiveConfigs, effectiveConfigs);
LOGGER.info("Cluster configs changed: {}", changedConfigs);
- _clusterConfigs = clusterConfigs;
+ _effectiveConfigs = effectiveConfigs;
for (PinotClusterConfigChangeListener listener : _listeners) {
- listener.onChange(changedConfigs, clusterConfigs);
+ listener.onChange(changedConfigs, effectiveConfigs);
}
}
@Override
public synchronized Map<String, String> getClusterConfigs() {
- return _clusterConfigs;
+ return _effectiveConfigs;
}
@Override
public synchronized boolean
registerClusterConfigChangeListener(PinotClusterConfigChangeListener listener) {
LOGGER.info("Registering cluster config change listener: {}",
listener.getClass().getName());
_listeners.add(listener);
// Treat every key as newly added so that the listener picks up the
current values
- listener.onChange(_clusterConfigs.keySet(), _clusterConfigs);
+ listener.onChange(_effectiveConfigs.keySet(), _effectiveConfigs);
Review Comment:
[P1] Seed the complete effective configuration before replaying it
With instance `pinot.server.consumption.rate.limit=0` and cluster
`pinot.server.consumption.rate.limit.bytes=1000`, `BaseServerStarter` first
creates the correct byte limiter from its merged configuration. This new
registration callback contains only instance settings, so
`ServerRateLimitConfigChangeListener` defaults the missing byte setting to zero
and replaces that limiter with `NOOP_RATE_LIMITER`.
Helix connects at `BaseServerStarter:807`, before the cluster-config
listener is registered at line 825. A consuming segment constructed in that
window caches the no-op limiter in the final
`RealtimeSegmentDataManager._serverRateLimiter` field (assignment at line
1940). The first cluster callback creates a new real limiter, but the existing
consumer keeps its no-op reference and bypasses the server cap for the
segment's lifetime.
An exact-source reproduction shows real limiter → no-op → new real limiter
on this head; the base preserves the original real limiter throughout. Please
initialize the complete effective snapshot before replaying it, or ensure
cluster configuration is applied before consumer state transitions can create
segments. Add a startup-order regression test covering a rate limit split
between instance and cluster configuration.
##########
pinot-common/src/main/java/org/apache/pinot/common/config/DefaultClusterConfigChangeHandler.java:
##########
@@ -19,50 +19,129 @@
package org.apache.pinot.common.config;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
import org.apache.helix.NotificationContext;
import org.apache.helix.api.listeners.BatchMode;
import org.apache.helix.api.listeners.ClusterConfigChangeListener;
import org.apache.helix.model.ClusterConfig;
import org.apache.pinot.spi.config.provider.PinotClusterConfigChangeListener;
import org.apache.pinot.spi.config.provider.PinotClusterConfigProvider;
+import org.apache.pinot.spi.env.PinotConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-/// Bridges Helix cluster config callbacks to
[PinotClusterConfigChangeListener]s. All access is serialized on this
-/// instance, so a listener sees the snapshot handed to it at registration and
every later change in order.
+/// Bridges Helix cluster config callbacks to
[PinotClusterConfigChangeListener]s. Instance configs captured at
+/// construction override cluster configs in the effective view exposed to
listeners and [#getClusterConfigs()]. All
+/// access is serialized on this instance, so a listener sees the snapshot
handed to it at registration and every later
+/// change in order.
@BatchMode(enabled = false)
public class DefaultClusterConfigChangeHandler implements
ClusterConfigChangeListener, PinotClusterConfigProvider {
private static final Logger LOGGER =
LoggerFactory.getLogger(DefaultClusterConfigChangeHandler.class);
+ private static final Comparator<String> CONFIG_KEY_COMPARATOR =
+
Comparator.comparing(DefaultClusterConfigChangeHandler::normalizeConfigKey);
private final List<PinotClusterConfigChangeListener> _listeners = new
ArrayList<>();
- private Map<String, String> _clusterConfigs = Map.of();
+ private final Map<String, String> _instanceConfigs;
+ private Map<String, String> _effectiveConfigs;
+
+ public DefaultClusterConfigChangeHandler() {
+ this(Map.of());
+ }
+
+ /// Snapshots the instance config at construction time. Callers must
construct the handler before folding cluster
+ /// configs into the instance config.
+ /// @param instanceConfig instance config to snapshot
+ public DefaultClusterConfigChangeHandler(PinotConfiguration instanceConfig) {
+ this(snapshot(instanceConfig));
+ }
+
+ DefaultClusterConfigChangeHandler(Map<String, String> instanceConfigs) {
+ _instanceConfigs = immutableConfigMap(instanceConfigs);
+ _effectiveConfigs = _instanceConfigs;
+ }
@Override
public synchronized void onClusterConfigChange(ClusterConfig clusterConfig,
NotificationContext context) {
Map<String, String> clusterConfigs =
copyWithoutNullValues(clusterConfig.getRecord().getSimpleFields());
- Set<String> changedConfigs = getChangedProperties(_clusterConfigs,
clusterConfigs);
+ Map<String, String> effectiveConfigs = getEffectiveConfigs(clusterConfigs);
+ Set<String> changedConfigs =
getChangedEffectiveProperties(_effectiveConfigs, effectiveConfigs);
LOGGER.info("Cluster configs changed: {}", changedConfigs);
- _clusterConfigs = clusterConfigs;
+ _effectiveConfigs = effectiveConfigs;
for (PinotClusterConfigChangeListener listener : _listeners) {
- listener.onChange(changedConfigs, clusterConfigs);
+ listener.onChange(changedConfigs, effectiveConfigs);
}
}
@Override
public synchronized Map<String, String> getClusterConfigs() {
- return _clusterConfigs;
+ return _effectiveConfigs;
}
@Override
public synchronized boolean
registerClusterConfigChangeListener(PinotClusterConfigChangeListener listener) {
LOGGER.info("Registering cluster config change listener: {}",
listener.getClass().getName());
_listeners.add(listener);
// Treat every key as newly added so that the listener picks up the
current values
- listener.onChange(_clusterConfigs.keySet(), _clusterConfigs);
+ listener.onChange(_effectiveConfigs.keySet(), _effectiveConfigs);
return true;
}
+
+ private Map<String, String> getEffectiveConfigs(Map<String, String>
clusterConfigs) {
+ Map<String, String> effectiveConfigs = new
TreeMap<>(CONFIG_KEY_COMPARATOR);
+ effectiveConfigs.putAll(_instanceConfigs);
+ for (Map.Entry<String, String> entry : clusterConfigs.entrySet()) {
+ effectiveConfigs.putIfAbsent(entry.getKey(), entry.getValue());
+ }
+ return Collections.unmodifiableMap(effectiveConfigs);
+ }
+
+ private static Set<String> getChangedEffectiveProperties(Map<String, String>
oldConfigs,
+ Map<String, String> newConfigs) {
+ Set<String> changedConfigs = new TreeSet<>(CONFIG_KEY_COMPARATOR);
+ for (Map.Entry<String, String> entry : newConfigs.entrySet()) {
+ if (!entry.getValue().equals(oldConfigs.get(entry.getKey()))) {
+ changedConfigs.add(entry.getKey());
+ }
+ }
+ for (String key : oldConfigs.keySet()) {
+ if (!newConfigs.containsKey(key)) {
+ changedConfigs.add(key);
+ }
+ }
+ return changedConfigs;
+ }
+
+ private static Map<String, String> snapshot(PinotConfiguration
instanceConfig) {
+ Map<String, String> snapshot = new TreeMap<>(CONFIG_KEY_COMPARATOR);
+ for (String key : instanceConfig.getKeys()) {
+ String value = instanceConfig.getProperty(key);
Review Comment:
[P1] Preserve subset-relative interpolation when replaying instance settings
With the resource-usage accountant enabled, this instance configuration
works before the PR:
```properties
pinot.server.query.accounting.sleep.ms=${sleeptime}
pinot.server.query.accounting.sleeptime=1000
```
`ThreadAccountantUtils.extractAccountingConfig()` strips the accounting
prefix, allowing the initial `QueryMonitorConfig` to resolve `${sleeptime}` to
`1000`. This snapshot reads the property in the full configuration instead,
where that relative reference remains the literal `${sleeptime}`. Registration
then replays it to the update constructor, whose `Integer.parseInt()` throws
`NumberFormatException`. The exception propagates from the accountant listener
registration in `BaseServerStarter:914` (and the equivalent broker path),
preventing startup.
The same issue silently changes enabled OOM protection to disabled when
`pinot.server.query.accounting.oom.enable.killing.query=${enableoom}` and
`pinot.server.query.accounting.enableoom=true`: initialization resolves `true`,
but replay passes the unresolved string to `Boolean.parseBoolean()`, yielding
`false`.
Both numeric and boolean cases were reproduced against exact base/head
sources: the base retains `1000`/`true`; this head throws/changes to `false`.
Please preserve the same interpolation context and typed conversion used during
initialization, and cover both cases with regression tests. Resolving every
property only against the full configuration is insufficient.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]