xiangfu0 commented on code in PR #19503:
URL: https://github.com/apache/pinot/pull/19503#discussion_r3954577446


##########
pinot-common/src/main/java/org/apache/pinot/common/metrics/AbstractMetrics.java:
##########
@@ -711,6 +718,106 @@ public String composePluginGaugeName(String pluginName, 
Gauge gauge) {
     return gauge.getGaugeName() + "." + pluginName;
   }
 
+  /// Removes every series this instance registered for the given table.
+  ///
+  /// Unlike the targeted `removeTable*` methods, this does not rebuild names 
from the rules used to emit them -- it
+  /// scans what is actually registered. That is the whole point. A series 
emitted with an extra key, or with a
+  /// composite table name, embeds a segment no caller can rediscover from the 
table name alone, so a sweep built on
+  /// reconstruction strands exactly those series and keeps stranding each new 
one that gets added.
+  ///
+  /// Matching is deliberately narrow:
+  ///
+  ///   - Only names under this instance's metric prefix are considered, so a 
table named after a component
+  ///     (`broker`) cannot match the prefix itself.
+  ///   - The table name must occupy whole `.`-delimited segments, never part 
of one -- `foo` does not match
+  ///     `foobar`, and a database-qualified `db.tbl_OFFLINE` matches only as 
a unit.
+  ///   - A sibling [AbstractMetrics] sharing this registry and prefix keeps 
its **gauges**. The ownership check
+  ///     below -- re-deriving the key under this instance's class -- is exact 
only where the registry key carries
+  ///     the owning class; yammer's does, dropwizard's discards it. What 
protects the case that actually matters,
+  ///     on every implementation, is the vocabulary check above: a sibling's 
gauge name is absent from this
+  ///     instance's [#getGauges()], so `<siblingGauge>.<table>` reads as 
meter-shaped and the table is not at
+  ///     offset 0, so it cannot match. Gauges are the only kind with a 
re-registration gate ([#_gaugeValues]), so
+  ///     dropping one from under its owner would silence it for the life of 
the process. A sibling's meter or
+  ///     timer may be dropped early where the key cannot distinguish owners; 
that is harmless -- they carry no
+  ///     gate and re-register on the next emission. Every instance should 
still run its own sweep, since that is
+  ///     what clears its own [#_gaugeValues].
+  ///
+  /// A table folded into the shared `allTables` aggregate is safe without a 
special case: no registered name
+  /// contains its name, so nothing matches. Passing `allTables` itself is 
rejected for the same reason it would be
+  /// a disaster -- it would delete the aggregate for every table at once.
+  ///
+  /// Two residual false positives are accepted: a workload or remote-cluster 
name exactly equal to a table name
+  /// sits in the same slot and would be swept. Both re-register on next use, 
so the cost is one counter reset.
+  ///
+  /// Two things this deliberately does **not** reach, both of which need 
their owner to clean up:
+  ///
+  ///   - Series a component registers outside any [AbstractMetrics] -- 
[ValidationMetrics] composes its own
+  ///     `pinot.controller.<table>.<gauge>` names against its own class and 
keeps its own value map, so the
+  ///     ownership check above skips it. Dropping its registry entries from 
here would strand that map and retire
+  ///     those gauges for the life of the process.
+  ///   - Names where the table is not followed by a path separator, such as 
the consumer client id form
+  ///     `<gauge>.<table>-<topic>-<partition>`. Matching those would mean 
accepting any prefix match, which is
+  ///     what makes `tbl` match `tbl_OFFLINE` and `db.tbl`.
+  ///
+  /// @param tableName the table to sweep, in whichever name form its emitters 
used (raw or with type)
+  /// @return the number of series removed
+  public int removeTableMetrics(String tableName) {
+    return removeTableMetrics(List.of(tableName));
+  }
+
+  /// Like [#removeTableMetrics(String)], for several tables at once. Prefer 
this when sweeping a batch: the
+  /// registry is scanned once per call, and yammer and dropwizard both 
materialise a fresh map on every
+  /// `allMetrics()`.
+  public int removeTableMetrics(Collection<String> tableNames) {
+    Set<String> targets = tableNames.stream().filter(t -> 
!ALL_TABLES.equals(t)).collect(Collectors.toSet());
+    if (targets.isEmpty()) {
+      return 0;
+    }
+    Set<String> gaugeNames =
+        
Arrays.stream(getGauges()).map(Gauge::getGaugeName).collect(Collectors.toCollection(HashSet::new));
+    int removed = 0;
+    // Snapshot the keys before mutating: the compound registry hands back its 
live map.
+    for (PinotMetricName registeredName : new 
ArrayList<>(_metricsRegistry.allMetrics().keySet())) {
+      String name = registeredName.getName();
+      if (!name.startsWith(_metricPrefix)
+          || !matchesAnyTable(name.substring(_metricPrefix.length()), targets, 
gaugeNames)) {
+        continue;
+      }
+      // Re-deriving the key under this class is the ownership test: an 
identically named series registered by a
+      // sibling AbstractMetrics is a different key, so it compares unequal 
and is left for that instance to sweep.
+      if (registeredName.equals(PinotMetricUtils.makePinotMetricName(_clazz, 
name))) {
+        PinotMetricUtils.removeMetric(_metricsRegistry, registeredName);
+        removed++;
+      }
+    }
+    // The deprecated gauge paths gate re-registration on _gaugeValues, so an 
entry left here would stop a removed
+    // gauge from ever coming back. Swept from this instance's own map rather 
than from what matched above, so it
+    // stays correct even where the registry cannot tell two instances' series 
apart.
+    synchronized (_gaugeValues) {
+      _gaugeValues.keySet().removeIf(gaugeName -> matchesAnyTable(gaugeName, 
targets, gaugeNames));
+    }
+    return removed;
+  }
+
+  /// Whether the prefix-stripped metric name names one of the given tables.
+  ///
+  /// The table sits at exactly one offset, decided by the shape: gauges 
compose `<gauge>.<table>[.<key>]`, while
+  /// meters, timers and query phases compose `<table>.<rest>`. Which one 
applies is settled by asking whether the
+  /// leading segment is a known gauge name -- and that question is what keeps 
a bare `tbl_OFFLINE` from matching
+  /// `db.tbl_OFFLINE`, a genuinely different table whose series must survive. 
A free search for the name anywhere
+  /// in the string cannot tell those two apart.
+  private static boolean matchesAnyTable(String name, Set<String> tableNames, 
Set<String> gaugeNames) {
+    int firstDot = name.indexOf('.');
+    int start = firstDot > 0 && gaugeNames.contains(name.substring(0, 
firstDot)) ? firstDot + 1 : 0;
+    for (String tableName : tableNames) {
+      int end = start + tableName.length();
+      if (name.startsWith(tableName, start) && (end == name.length() || 
name.charAt(end) == '.')) {

Review Comment:
   [P2 / MAJOR] Preserve exact table identity when matching registered names
   
   The delimiter and gauge-name heuristics can delete unrelated metrics as well 
as miss the requested table. I reproduced these cases against the compiled PR 
implementation:
   
   - Register a meter and gauge for `db.foo_OFFLINE`, then call 
`removeTableMetrics("db")`: both are removed, although they belong to a 
different, database-qualified table.
   - Register `ControllerGauge.REALTIME_TABLE_COUNT`, then sweep 
`realtimeTableCount`: the global gauge is removed.
   - Register a meter for `numberOfReplicas.foo_OFFLINE`: sweeping that table 
removes nothing, while sweeping unrelated `foo_OFFLINE` removes the meter 
because the database name is interpreted as a gauge name.
   
   The API explicitly accepts raw table names, so these inputs are within its 
contract. Please preserve table identity and metric scope at registration, or 
otherwise make matching unambiguous, and add regression coverage for these 
collisions. A dot boundary alone cannot distinguish database qualification from 
metric suffixes.



##########
pinot-common/src/main/java/org/apache/pinot/common/metrics/AbstractMetrics.java:
##########
@@ -711,6 +718,106 @@ public String composePluginGaugeName(String pluginName, 
Gauge gauge) {
     return gauge.getGaugeName() + "." + pluginName;
   }
 
+  /// Removes every series this instance registered for the given table.
+  ///
+  /// Unlike the targeted `removeTable*` methods, this does not rebuild names 
from the rules used to emit them -- it
+  /// scans what is actually registered. That is the whole point. A series 
emitted with an extra key, or with a
+  /// composite table name, embeds a segment no caller can rediscover from the 
table name alone, so a sweep built on
+  /// reconstruction strands exactly those series and keeps stranding each new 
one that gets added.
+  ///
+  /// Matching is deliberately narrow:
+  ///
+  ///   - Only names under this instance's metric prefix are considered, so a 
table named after a component
+  ///     (`broker`) cannot match the prefix itself.
+  ///   - The table name must occupy whole `.`-delimited segments, never part 
of one -- `foo` does not match
+  ///     `foobar`, and a database-qualified `db.tbl_OFFLINE` matches only as 
a unit.
+  ///   - A sibling [AbstractMetrics] sharing this registry and prefix keeps 
its **gauges**. The ownership check
+  ///     below -- re-deriving the key under this instance's class -- is exact 
only where the registry key carries
+  ///     the owning class; yammer's does, dropwizard's discards it. What 
protects the case that actually matters,
+  ///     on every implementation, is the vocabulary check above: a sibling's 
gauge name is absent from this
+  ///     instance's [#getGauges()], so `<siblingGauge>.<table>` reads as 
meter-shaped and the table is not at
+  ///     offset 0, so it cannot match. Gauges are the only kind with a 
re-registration gate ([#_gaugeValues]), so
+  ///     dropping one from under its owner would silence it for the life of 
the process. A sibling's meter or
+  ///     timer may be dropped early where the key cannot distinguish owners; 
that is harmless -- they carry no
+  ///     gate and re-register on the next emission. Every instance should 
still run its own sweep, since that is
+  ///     what clears its own [#_gaugeValues].
+  ///
+  /// A table folded into the shared `allTables` aggregate is safe without a 
special case: no registered name
+  /// contains its name, so nothing matches. Passing `allTables` itself is 
rejected for the same reason it would be
+  /// a disaster -- it would delete the aggregate for every table at once.
+  ///
+  /// Two residual false positives are accepted: a workload or remote-cluster 
name exactly equal to a table name
+  /// sits in the same slot and would be swept. Both re-register on next use, 
so the cost is one counter reset.
+  ///
+  /// Two things this deliberately does **not** reach, both of which need 
their owner to clean up:
+  ///
+  ///   - Series a component registers outside any [AbstractMetrics] -- 
[ValidationMetrics] composes its own
+  ///     `pinot.controller.<table>.<gauge>` names against its own class and 
keeps its own value map, so the
+  ///     ownership check above skips it. Dropping its registry entries from 
here would strand that map and retire
+  ///     those gauges for the life of the process.
+  ///   - Names where the table is not followed by a path separator, such as 
the consumer client id form
+  ///     `<gauge>.<table>-<topic>-<partition>`. Matching those would mean 
accepting any prefix match, which is
+  ///     what makes `tbl` match `tbl_OFFLINE` and `db.tbl`.
+  ///
+  /// @param tableName the table to sweep, in whichever name form its emitters 
used (raw or with type)
+  /// @return the number of series removed
+  public int removeTableMetrics(String tableName) {
+    return removeTableMetrics(List.of(tableName));
+  }
+
+  /// Like [#removeTableMetrics(String)], for several tables at once. Prefer 
this when sweeping a batch: the
+  /// registry is scanned once per call, and yammer and dropwizard both 
materialise a fresh map on every
+  /// `allMetrics()`.
+  public int removeTableMetrics(Collection<String> tableNames) {
+    Set<String> targets = tableNames.stream().filter(t -> 
!ALL_TABLES.equals(t)).collect(Collectors.toSet());
+    if (targets.isEmpty()) {
+      return 0;
+    }
+    Set<String> gaugeNames =
+        
Arrays.stream(getGauges()).map(Gauge::getGaugeName).collect(Collectors.toCollection(HashSet::new));
+    int removed = 0;
+    // Snapshot the keys before mutating: the compound registry hands back its 
live map.
+    for (PinotMetricName registeredName : new 
ArrayList<>(_metricsRegistry.allMetrics().keySet())) {
+      String name = registeredName.getName();
+      if (!name.startsWith(_metricPrefix)
+          || !matchesAnyTable(name.substring(_metricPrefix.length()), targets, 
gaugeNames)) {
+        continue;
+      }
+      // Re-deriving the key under this class is the ownership test: an 
identically named series registered by a
+      // sibling AbstractMetrics is a different key, so it compares unequal 
and is left for that instance to sweep.
+      if (registeredName.equals(PinotMetricUtils.makePinotMetricName(_clazz, 
name))) {

Review Comment:
   [P2 / MAJOR] Preserve ValidationMetrics ownership on Dropwizard
   
   `DropwizardMetricName` discards the owning class and compares only the name 
string, so re-deriving the key with `_clazz` does not establish ownership on 
that backend. With a shared Dropwizard registry, I reproduced:
   
   1. `ValidationMetrics.updateMissingSegmentCountGauge("foo_OFFLINE", 1)` 
registers one gauge.
   2. `ControllerMetrics.removeTableMetrics("foo_OFFLINE")` removes it.
   3. Updating the validation gauge again leaves the registry empty: 
`ValidationMetrics` still has its private `_gaugeValues` entry and skips 
re-registration.
   
   This contradicts the documented exclusion of `ValidationMetrics` and can 
leave its series absent across subsequent updates. Please preserve ownership 
independently of backend key equality, or explicitly exclude these foreign 
registrations, with a Dropwizard regression that verifies the validation gauge 
survives and remains updateable.



##########
pinot-common/src/main/java/org/apache/pinot/common/metrics/AbstractMetrics.java:
##########
@@ -711,6 +718,106 @@ public String composePluginGaugeName(String pluginName, 
Gauge gauge) {
     return gauge.getGaugeName() + "." + pluginName;
   }
 
+  /// Removes every series this instance registered for the given table.
+  ///
+  /// Unlike the targeted `removeTable*` methods, this does not rebuild names 
from the rules used to emit them -- it
+  /// scans what is actually registered. That is the whole point. A series 
emitted with an extra key, or with a
+  /// composite table name, embeds a segment no caller can rediscover from the 
table name alone, so a sweep built on
+  /// reconstruction strands exactly those series and keeps stranding each new 
one that gets added.
+  ///
+  /// Matching is deliberately narrow:
+  ///
+  ///   - Only names under this instance's metric prefix are considered, so a 
table named after a component
+  ///     (`broker`) cannot match the prefix itself.
+  ///   - The table name must occupy whole `.`-delimited segments, never part 
of one -- `foo` does not match
+  ///     `foobar`, and a database-qualified `db.tbl_OFFLINE` matches only as 
a unit.
+  ///   - A sibling [AbstractMetrics] sharing this registry and prefix keeps 
its **gauges**. The ownership check
+  ///     below -- re-deriving the key under this instance's class -- is exact 
only where the registry key carries
+  ///     the owning class; yammer's does, dropwizard's discards it. What 
protects the case that actually matters,
+  ///     on every implementation, is the vocabulary check above: a sibling's 
gauge name is absent from this
+  ///     instance's [#getGauges()], so `<siblingGauge>.<table>` reads as 
meter-shaped and the table is not at
+  ///     offset 0, so it cannot match. Gauges are the only kind with a 
re-registration gate ([#_gaugeValues]), so
+  ///     dropping one from under its owner would silence it for the life of 
the process. A sibling's meter or
+  ///     timer may be dropped early where the key cannot distinguish owners; 
that is harmless -- they carry no
+  ///     gate and re-register on the next emission. Every instance should 
still run its own sweep, since that is
+  ///     what clears its own [#_gaugeValues].
+  ///
+  /// A table folded into the shared `allTables` aggregate is safe without a 
special case: no registered name
+  /// contains its name, so nothing matches. Passing `allTables` itself is 
rejected for the same reason it would be
+  /// a disaster -- it would delete the aggregate for every table at once.
+  ///
+  /// Two residual false positives are accepted: a workload or remote-cluster 
name exactly equal to a table name
+  /// sits in the same slot and would be swept. Both re-register on next use, 
so the cost is one counter reset.
+  ///
+  /// Two things this deliberately does **not** reach, both of which need 
their owner to clean up:
+  ///
+  ///   - Series a component registers outside any [AbstractMetrics] -- 
[ValidationMetrics] composes its own
+  ///     `pinot.controller.<table>.<gauge>` names against its own class and 
keeps its own value map, so the
+  ///     ownership check above skips it. Dropping its registry entries from 
here would strand that map and retire
+  ///     those gauges for the life of the process.
+  ///   - Names where the table is not followed by a path separator, such as 
the consumer client id form
+  ///     `<gauge>.<table>-<topic>-<partition>`. Matching those would mean 
accepting any prefix match, which is
+  ///     what makes `tbl` match `tbl_OFFLINE` and `db.tbl`.
+  ///
+  /// @param tableName the table to sweep, in whichever name form its emitters 
used (raw or with type)
+  /// @return the number of series removed
+  public int removeTableMetrics(String tableName) {
+    return removeTableMetrics(List.of(tableName));
+  }
+
+  /// Like [#removeTableMetrics(String)], for several tables at once. Prefer 
this when sweeping a batch: the
+  /// registry is scanned once per call, and yammer and dropwizard both 
materialise a fresh map on every
+  /// `allMetrics()`.
+  public int removeTableMetrics(Collection<String> tableNames) {
+    Set<String> targets = tableNames.stream().filter(t -> 
!ALL_TABLES.equals(t)).collect(Collectors.toSet());
+    if (targets.isEmpty()) {
+      return 0;
+    }
+    Set<String> gaugeNames =
+        
Arrays.stream(getGauges()).map(Gauge::getGaugeName).collect(Collectors.toCollection(HashSet::new));
+    int removed = 0;
+    // Snapshot the keys before mutating: the compound registry hands back its 
live map.
+    for (PinotMetricName registeredName : new 
ArrayList<>(_metricsRegistry.allMetrics().keySet())) {
+      String name = registeredName.getName();
+      if (!name.startsWith(_metricPrefix)
+          || !matchesAnyTable(name.substring(_metricPrefix.length()), targets, 
gaugeNames)) {
+        continue;
+      }
+      // Re-deriving the key under this class is the ownership test: an 
identically named series registered by a
+      // sibling AbstractMetrics is a different key, so it compares unequal 
and is left for that instance to sweep.
+      if (registeredName.equals(PinotMetricUtils.makePinotMetricName(_clazz, 
name))) {
+        PinotMetricUtils.removeMetric(_metricsRegistry, registeredName);
+        removed++;
+      }
+    }
+    // The deprecated gauge paths gate re-registration on _gaugeValues, so an 
entry left here would stop a removed
+    // gauge from ever coming back. Swept from this instance's own map rather 
than from what matched above, so it
+    // stays correct even where the registry cannot tell two instances' series 
apart.
+    synchronized (_gaugeValues) {
+      _gaugeValues.keySet().removeIf(gaugeName -> matchesAnyTable(gaugeName, 
targets, gaugeNames));

Review Comment:
   [P2 / MAJOR] Keep registry removal and gauge-value cleanup atomic
   
   A gauge can be registered after the registry snapshot above but before this 
`removeIf`. It is then absent from the removal snapshot, so its registry entry 
survives, while this code deletes its backing `_gaugeValues` entry. The 
supplier installed by `setValueOfGauge` calls 
`_gaugeValues.get(gaugeName).get()` and throws `NullPointerException` on scrape 
until another update.
   
   I reproduced this deterministically with latches around 
snapshot/registration: the sweep returned `0`, the registry retained one gauge, 
its backing value was `null`, and reading the gauge threw at 
`AbstractMetrics.java:436`.
   
   Please coordinate the snapshot, registry removal, and backing-value cleanup 
with the same monitor used by gauge registration, as the existing targeted 
`removeGauge` does for its two removals. Add a barrier-controlled regression 
for registration during the sweep.



-- 
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]

Reply via email to