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

   ## Intent
   
   `ExternalViewReader.getTableToBrokersMap()` builds the table-to-broker 
routing map that
   `DynamicBrokerSelector` refreshes from, and it re-reads the same data from 
ZooKeeper many
   times over.
   
   The broker resource lives in a single znode, but it names brokers by Helix 
instance ID. Turning an
   instance ID into a connectable address means reading that broker's own 
config znode under
   `/CONFIGS/PARTICIPANT`. The method walks the broker resource table by table, 
and resolves the
   address inline for every table/broker pair it meets — so a broker serving a 
thousand tables has its
   config read a thousand times, and every read after the first returns the 
same bytes.
   
   A broker's address is a property of the broker. It does not depend on which 
table is being examined,
   so all but one of those reads is waste. For N tables each served by M 
brokers the method performs
   N x M reads where M would do, and each one is a separate sequential round 
trip to the
   ZooKeeper ensemble.
   
   This matters because of when it happens. The map is rebuilt on every 
broker-resource watch fire —
   that is, precisely when routing has just changed and a client most needs to 
converge quickly. On a
   large cluster the redundant reads add seconds to that convergence, during 
which the client is still
   routing on the old map.
   
   ## What this changes
   
   `getTableToBrokersMap()` now resolves each distinct broker's address once 
per call and reuses it for
   the remaining tables that broker serves. Reads drop from N x M to M.
   
   The map used for this is created per call and passed in, rather than held as 
a field. That is
   deliberate on two counts. The method is called concurrently, so a shared 
field would need
   synchronising. And an address cached across calls would have to be 
invalidated whenever a broker
   changed host or port, which would mean watching a second znode; a per-call 
map cannot go stale,
   because the next refresh resolves everything again from scratch.
   
   ## Behaviour
   
   The returned map is unchanged — same keys, same addresses, same ordering 
semantics. The loop still
   visits every table/broker pair and still collects into the same structures; 
only the number of
   ZooKeeper reads differs.
   
   There is exactly one case where old and new observably differ. If a broker's 
instance config changes
   *while* a single traversal is in progress, the previous code would resolve 
that broker to its old
   address for tables visited before the change and its new address for tables 
visited after, producing
   a map in which one broker holds two different addresses. Reusing the first 
resolution gives one
   address per broker for the whole snapshot. This is a more coherent result, 
and either way the next
   refresh picks up the new address.
   
   Exception handling is unaffected: a ZooKeeper failure still propagates out 
of the resolution to the
   same enclosing handler and aborts the traversal exactly as before, and no 
partial entry is recorded
   when it does.
   
   ## Scope
   
   `getLiveBrokers()` on the same class walks the broker resource the same way 
and carries the same
   redundancy, and is fixed here too. It currently has no callers in the 
repository, so this is not
   where the win is — but the defect is identical and sits twenty lines from 
the one that matters, and
   leaving one of the two behind would only invite the same change again later 
against a method whose
   behaviour is then harder to reason about.
   
   Its result is deliberately untouched. `getLiveBrokers()` returns a `List` 
and has always emitted one
   entry per table/broker pair rather than one per broker, so a broker serving 
many tables appears many
   times. Resolving the address once must not become an excuse to de-duplicate 
that list: it is public
   API, and a caller choosing at random would see its weighting change. Whether 
the duplicates are
   desirable is a separate question and not one this change answers.
   
   ## Test plan
   
   Five tests added to `ExternalViewReaderTest`. The existing fixtures hold a 
single table and a single
   broker, where N x M and M are both 1, so none of them could observe this; 
the new ones use two
   tables served by the same two brokers — four pairs over two distinct brokers.
   
   - `testGetTableToBrokersMapReadsEachInstanceConfigOncePerCall` — each 
broker's config znode is read
     exactly once, not once per table.
   - `testGetLiveBrokersReadsEachInstanceConfigOncePerCall` — the same, for the 
other method.
   - `testGetTableToBrokersMapResultIsUnchangedByReusingResolvedAddresses` — 
both tables still map to
     both broker addresses.
   - `testGetLiveBrokersStillReturnsOneEntryPerTableBrokerPair` — the returned 
list keeps all four
     entries, guarding the de-duplication trap described above.
   - `testResolvedAddressesAreNotRetainedAcrossCalls` — a second call re-reads, 
so nothing can be
     served stale.
   
   Results:
   
   - Full `pinot-java-client` suite: **190 tests, 0 failures**, including 
`DynamicBrokerSelectorTest`
     and `BrokerCacheTest` — the consumers of this method, not just the 
reader's own tests.
   - All five new tests were confirmed load-bearing: restoring the inline 
resolution fails the
     read-count tests, and de-duplicating the list fails the pair-count test.
   - `checkstyle` and `license` pass on `pinot-clients/pinot-java-client`.
   
   ### Measured against a live cluster
   
   A quickstart was extended to 76 table resources served by 2 brokers — 152 
table/broker pairs over 2
   distinct brokers — and `getTableToBrokersMap()` was timed against its 
ZooKeeper, before and after,
   from the same harness. Both runs warm up first and report the median of 15 
iterations.
   
   | Build | median | min | mean |
   |---|---|---|---|
   | before | 28.3 ms | 27.3 ms | 28.1 ms |
   | after | 1.3 ms | 1.0 ms | 1.3 ms |
   
   The harness also prints a canonical digest of the returned map. It is 
identical across both builds
   (76 tables, same digest), so the result is unchanged and only the time 
differs.
   
   Note this is a ZooKeeper on loopback, where a read is well under a 
millisecond. The saving is one
   round trip per redundant read, so on a cluster whose client is a network hop 
from the ZooKeeper
   ensemble the absolute difference is larger, and it grows with the number of 
tables.
   


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