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

   > **Draft until #19409 and #19410 merge.** This PR is stacked on both. 
GitHub compares against
   > `master`, so the diff shown here also contains their commits. **The change 
under review here is the
   > two commits "Select the stats store by name" and "Purge orphaned 
statistics on request, not at
   > startup" (+1,000).** I will rebase and mark this ready as the parents land.
   
   Contributes to #18740. **Stacked on #19409 (contracts and stores) and #19410 
(collection) — review those first.** Part of the split that replaces #18741.
   
   This PR makes the feature configurable and operable: it selects a store by 
name, wires collection
   into broker startup behind `pinot.broker.stats.enabled`, and adds an 
endpoint to reclaim statistics
   for tables the broker no longer serves. Still no query-behavior change.
   
   ```text
     ┌──────────────────┐   ┌──────────────────┐   ┌──────────────────┐   
┌──────────────┐   ┌──────────────┐
     │ 1 — contracts    │──▶│ 2 — collection   │──▶│ 3 — this PR      │──▶│ 4 — 
planner  │──▶│ 5 — join     │
     │ and the stores   │   │                  │   │ selection +      │   │ 
wiring       │   │ reordering   │
     │                  │   │                  │   │ purge endpoint   │   │     
         │   │              │
     └──────────────────┘   └──────────────────┘   └──────────────────┘   
└──────────────┘   └──────────────┘
   ```
   
   ## Selecting a store
   
   `pinot.broker.stats.store` resolves against `StatsStoreProvider` 
implementations discovered through
   `ServiceLoader`, each declaring its own name — the same pattern as 
`TimeBoundaryStrategyService`.
   
   The value is a **name, not a class name**. A class name in configuration is 
a rename trap: moving or
   renaming an implementation would silently invalidate an operator's config. 
Duplicate names fail
   loudly, and an unknown name fails startup listing the names that do exist, 
rather than quietly
   disabling statistics an operator explicitly asked for.
   
   Discovery walks the plugin realms as well as the context classloader, since 
a provider contributed
   by a plugin is invisible to the context loader alone — which would make the 
documented extension
   seam unusable. The same provider class reached through two classloaders is 
deduplicated by class
   name, so a fat jar plus a plugin realm is not mistaken for a collision.
   
   A provider's own failure is kept distinct from an operator typo: the typo 
fails broker startup,
   while a provider that cannot build a store from the given configuration 
degrades to no-statistics.
   Getting those two the wrong way round inverts the operator-visible outcome.
   
   ## Reclaiming orphaned statistics
   
   A broker drops a table's statistics as soon as its routing entry goes away 
(PR 2). That cannot cover
   a table dropped **while the broker was down**: on restart no routing entry — 
and so no listener — is
   ever created for it, leaving rows nothing would revisit.
   
   This is exposed as `DELETE /statistics/orphaned` rather than run 
automatically, because a startup
   sweep is wrong in the worst way. `BaseBrokerRoutingManager.init()` only 
wires up ZooKeeper accessors;
   routing entries are created later by Helix state transitions. 
`routingExists()` would answer false
   for every table and the sweep would delete the whole store on every boot, 
logging it as tables the
   broker no longer serves — making the store, its schema versioning and the 
crc fast path pointless.
   
   Moving it later does not fix it either. Routing is built asynchronously, so 
before it settles "no
   routing for T" also matches a table that has merely not loaded yet, and 
purging then discards exactly
   the column statistics that are expensive to re-fetch. There *is* a readiness 
signal — service status
   is GOOD only once CurrentState matches IdealState — but consuming it needs a 
background waiter, and
   an operator can lower `minResourcePercentForStart` and quietly break the 
guarantee. An operator
   calling this on a running broker removes the ambiguity entirely.
   
   The liveness test and the purge are not atomic with respect to a routing 
build, which publishes its
   routing entry only after its listener has written that table's rows. A table 
that becomes served in
   that window has just had those rows deleted underneath a listener that 
believes they are still
   there; since a listener only removes segments its mirror knows about, the 
loss would persist until
   restart. The purge re-tests afterwards and hands such a listener a clean 
slate instead.
   
   ## Reviewer notes
   
   - **Off by default** (`pinot.broker.stats.enabled=false`) and broker-local: 
no wire format, no
     mixed-version concern, nothing to coordinate on a rolling upgrade.
   - **New config keys**, all `[EXPERIMENTAL]`: `pinot.broker.stats.enabled`,
     `pinot.broker.stats.store`, `pinot.broker.stats.dir` (the last applies 
only to the `sqlite`
     store). The default directory is 
`<java.io.tmpdir>/<instanceId>/broker-stats` — per-instance,
     because two brokers on one host sharing a single SQLite file would corrupt 
it.
   - **New REST endpoint** `DELETE /statistics/orphaned`, behind a new 
`Actions.Cluster.DELETE_STATISTICS`
     authorization action. Returns 404 when collection is disabled, so it is 
inert on an unconfigured
     broker.
   - `BrokerAdminApiApplication` gains a nullable constructor parameter for the 
stats manager; it is
     bound only when statistics are enabled, and the resource injects it as 
`@Optional`.
   
   ## Testing
   
   - `StatsStoreFactoryTest` covers every configured value, the fail-fast path 
for a typo, duplicate
     names, and the deliberate split between a provider failure (degrade) and 
an operator typo (fail
     startup).
   - `PinotBrokerStatisticsTest` covers the endpoint: the 404 when collection 
is disabled, and that the
     liveness predicate is actually wired to the routing table — an inverted 
one would purge tables the
     broker still serves.
   - `BrokerStatsCollectionIntegrationTest` starts a **real broker** with
     `pinot.broker.stats.enabled=true` against a real controller and ZooKeeper. 
Everything else in this
     stack is unit-tested against hand-built ZooKeeper records, which cannot 
see the wiring: that the
     listener provider is registered before the routing manager initialises, 
that the
     `pinot.broker.stats.*` subset reaches the provider, that the directory 
default resolves, and that
     sqlite-jdbc can load its native library in an assembled broker. Each of 
those fails by silently
     collecting nothing, so no other assertion would notice. It asserts that 
segments announced through
     Helix arrive in the store with the counts ZooKeeper carries, that the time 
boundary of a
     DAYS-typed column resolves to an instant rather than the day number, and 
that
     `DELETE /statistics/orphaned` is reachable and leaves a served table 
alone. It justifies its own
     cluster because the feature is off by default and cannot ride an existing 
broker fixture.
   


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