jineshparakh opened a new pull request, #19503:
URL: https://github.com/apache/pinot/pull/19503

   ## Intent
   
   Delete a table and its metric series keep being exported by the brokers, 
servers and controllers that
   were serving it — frozen at whatever value they last held — until each of 
those processes is restarted.
   
   That is not merely untidy:
   
   * **Dashboards lie.** A panel filtered by table shows a row for a table that 
no longer exists, holding
     its final measurement forever.
   * **Alerts fire on ghosts.** Gauges such as `percentOfReplicas`, 
`segmentsInErrorState` or
     `percentSegmentsAvailable` keep their last value. If a table was degraded 
at the moment it was
     dropped — which is common, since deletion drives replicas offline — an 
alert can fire *after* the
     table is gone and never clear.
   * **Cardinality only grows.** In a cluster where tables are created and 
dropped routinely, the metric
     registry, the JMX MBean set and everything downstream of it accumulate 
monotonically for the life of
     the process.
   
   The goal is that deleting a table stops its metrics, everywhere, without 
waiting for a restart.
   
   This PR does not do that on its own. It adds the one capability all three 
components need, and the one
   SPI accessor that capability requires. It is deliberately 
**behaviour-neutral** — nothing calls it yet —
   so that the API can be discussed separately from the three behaviour changes 
that will build on it.
   
   ## Why the cleanup we have today doesn't achieve it
   
   Removal is retrofitted per component, and every implementation reconstructs 
metric names by replaying
   the composition rules in `AbstractMetrics`. Reconstruction can only reach a 
name that is *fully
   derivable from the table name*. Several names are not, so we have three 
hand-rolled sweeps, each
   incomplete in a different way: the controller and server iterate their 
metric enums and so miss
   anything carrying an extra segment between the table and the metric name, 
and the broker removes three
   gauges and nothing else — no query counters, no phase timings, no time 
boundary, no quota gauges.
   
   Two current examples of the blind spot:
   
   **`tableRebalanceExecutionTimeMs`** is registered as 
`<table>.<jobStatus>.<timer>`, because
   `TableRebalancer` passes a synthetic table name. The sweep composes 
`<table>.<timer>`. It never matches,
   for any of the seven `RebalanceResult.Status` values. 
`cronSchedulerJobExecutionTimeMs` has the same
   shape with a task type in place of the status.
   
   **`OPEN_STRUCT_LAST_SEGMENT_KEY_DOC_COUNT`** is registered as 
`<gauge>.<table>.<column>$<key>`. The
   server's deletion handler already documents this exact failure, works around 
it by recovering keys from
   the table config, and concedes in its own javadoc that the workaround is 
partial: *"Gauges for
   discovered keys survive until the server restarts."*
   
   Both are the same defect. **Emission is modelled; removal is not.** Every 
new keyed metric silently
   re-opens the hole, and nothing tells the author who added it.
   
   ## What this PR adds
   
   A way to remove a table's series based on **what is actually registered**, 
rather than on names we can
   reconstruct — so keyed, composite and future name shapes are all reachable 
without anyone having to
   remember them.
   
   Doing that needs one thing from the SPI: the ability to read a registered 
metric's name back as a
   string. `PinotMetricName` exposes only `Object getMetricName()`, and 
`toString()` is not portable —
   yammer renders a JMX object name there, dropwizard renders the bare name. So 
the PR adds a single
   `default String getName()` and overrides it in the in-tree implementations. 
As a `default` method it is
   source- and binary-compatible: a third-party metrics plugin keeps working, 
and at worst the sweep
   matches nothing for it, which is exactly today's behaviour.
   
   Arguably this accessor is one the interface should have had regardless; the 
sweep is just its first
   consumer.
   
   ## The flow this unlocks
   
   The follow-up PRs attach the sweep to each component's **existing** 
table-deletion signal — no new
   messages, no new protocol, no new ZooKeeper writes.
   
   ```
                                 DELETE /tables/{table}
                                           │
                           PinotHelixResourceManager.deleteTable
                                           │
      
┌────────────────────────────────────┼────────────────────────────────────┐
      │ ① removed from brokerResource      │ ② TableDeletionMessage             
│ ③ table-config znode
      │    (happens FIRST)                 │    → every server                  
│    removed (LAST)
      ▼                                    ▼                                    
▼
   BROKER                               SERVER                              
CONTROLLER (every one)
   Helix ONLINE→OFFLINE/DROPPED         Helix USER_DEFINE_MSG                ZK 
child watch /CONFIGS/TABLE
      │                                    │                                    
│
      ▼                                    ▼                                    
▼
   BaseBrokerRoutingManager             SegmentMessageHandlerFactory         
(new) TableMetricsCleaner
     .removeRoutingInternal              .TableDeletionMessageHandler
      │                                    │                                    
│
      
└────────────────┬───────────────────┴────────────────────────────────────┘
                       ▼
           AbstractMetrics.removeTableMetrics(...)      ← this PR
   ```
   
   
   Two ordering facts the follow-ups have to respect, recorded here because 
they are not obvious and each
   cost a debugging cycle to find:
   
   * **The table config is removed last.** A "does the table still exist in 
ZooKeeper?" guard is therefore
     useless at broker and server time — the config is still present when the 
deletion becomes observable
     there. Only the controller's watch can use the ZK snapshot that reported 
the deletion, which *is*
     authoritative at that instant.
   * **Some series key off the raw table name**, not the name with type — the 
deep store timers and byte
     counters, and the broker's whole query path. Those are shared by the 
OFFLINE and REALTIME halves of a
     hybrid table, so they may only be swept once neither half remains.
   
   Scope note: this is deletion-driven. A table that still exists but has 
stopped being reported on —
   disabled, ideal state unreadable, or leadership moved — remains 
`SegmentStatusChecker`'s business, since
   only it knows it stopped reporting.
   
   ## Approaches considered and rejected
   
   **Fix the two known offenders.** Add a keyed timer remover and loop over 
`RebalanceResult.Status`, then
   do the same for the cron timer. Rejected as symptom-level: it leaves the 
defect class intact, so the
   next keyed metric leaks again and the person adding it gets no signal.
   
   **Extend the enum-driven sweep properly** — add an abstract `getTimers()` to 
`AbstractMetrics`, add
   keyed `removeTableTimer` / `removeTableMeter` overloads, and have each 
component iterate every key it
   knows about. This was the first design. Rejected because it inherits the 
very defect it is fixing: it
   can still only remove names it can reconstruct, so it is complete only for 
as long as someone keeps it
   in sync with every emission site. It also makes `getTimers()` a compile 
break for every subclass.
   
   **Clean up inside `PinotHelixResourceManager#deleteTable`.** Superficially 
the obvious place. Rejected
   because deletion happens in *one* process while the series live in nearly 
all of them: that controller
   holds one metric registry out of N and cannot reach any broker's, any 
server's, or any other
   controller's. It is also edge-triggered in the worst way — a missed call 
leaks forever, and it races a
   delete-then-recreate.
   
   **Parse `toString()` instead of adding an SPI method.** Rejected: the format 
is implementation-specific
   (a JMX object name on yammer, the bare name on dropwizard), so this would 
mean implementation-specific
   parsing inside a generic base class, silently wrong for any plugin we do not 
know about.
   
   **Reach the name reflectively through the existing `Object 
getMetricName()`.** Technically workable for
   the in-tree implementations. Rejected as an undocumented contract that would 
break silently rather than
   at compile time.
   
   **Have `AbstractMetrics` index the names it registers** and sweep that 
index, avoiding the SPI change
   entirely. Genuinely attractive — ownership becomes exact by construction and 
sweeps get cheaper, since
   `allMetrics()` materialises a fresh map on some implementations. Rejected on 
hot-path cost: it puts a
   map write on every metric emission, and `addMeteredTableValue` runs several 
times per query, to save one
   `default` method. It also introduces a second source of truth that can drift 
from the registry. The
   registry scan pays its cost only on deletion, which is rare and 
operator-driven.
   
   ## Backward compatibility
   
   * **No wire surface.** `PinotMetricName` is confined to the metrics 
packages, is not `Serializable`, and
     appears in no message class, ZooKeeper znode or request/response type. 
Metric names leave a process
     only as JMX MBeans.
   * **No emitted name changes**, so every exporter rule, dashboard and alert 
keeps matching exactly what
     it matched before.
   * **`default` method**, so third-party metrics plugins remain source- and 
binary-compatible.
   * **Mixed versions are independent.** Each component has its own JVM, 
registry and copy of
     `pinot-common`; there is no shared metric state and nothing to negotiate. 
In a partially upgraded
     cluster each component simply behaves per its own version.
   * **No behaviour change at all in this PR** — nothing calls the new method 
yet.
   
   ## Testing
   
   `AbstractMetricsTest` is abstract with three concrete subclasses, so the new 
cases run against the
   **fake**, **yammer** and **dropwizard** registries. They cover the names 
reconstruction cannot reach
   (including a keyed timer registered exactly as `TableRebalancer` emits it), 
that a swept gauge can be
   re-registered afterwards, whole-segment matching so neighbouring and 
database-qualified tables are not
   caught, that global series and other tables survive, that the shared 
`allTables` aggregate cannot be
   deleted, and that a second `AbstractMetrics` sharing the same registry and 
prefix keeps its gauges.
   
   ```
   Pinot SPI ................ SUCCESS
   Pinot Common ............. SUCCESS    46 tests
   Pinot Yammer Metrics ..... SUCCESS    20 tests
   Pinot Dropwizard Metrics . SUCCESS    20 tests
   Pinot Compound Metrics ... SUCCESS
   checkstyle:check + license:check ... clean
   ```
   


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