gortiz commented on code in PR #18947:
URL: https://github.com/apache/pinot/pull/18947#discussion_r3896511336


##########
pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/StrictReplicaGroupInstanceSelector.java:
##########
@@ -18,44 +18,52 @@
  */
 package org.apache.pinot.broker.routing.instanceselector;
 
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import org.apache.helix.model.ExternalView;
 import org.apache.helix.model.IdealState;
+import 
org.apache.pinot.broker.routing.adaptiveserverselector.ServerSelectionContext;
+import org.apache.pinot.common.utils.config.QueryOptionsUtils;
+
 
 /// Instance selector for strict replica-group routing strategy.
 ///
-/// ```
-/// The strict replica-group routing strategy always routes the query to the 
instances within the same replica-group.
-/// (Note that the replica-group information is derived from the ideal state 
of the table, where the instances are
-/// sorted alphabetically in the instance state map, so the replica-groups in 
the instance selector might not match the
-/// replica-groups in the instance partitions). The goal of this algorithm is 
to ensure that segments from the same
-/// partition are never served from multiple different instances. The 
instances in a replica-group should have all the
-/// online segments (segments with ONLINE/CONSUMING instances in the ideal 
state and selected by the pre-selector)
-/// available (ONLINE/CONSUMING in the external view) in order to serve 
queries. If any segment is unavailable in the
-/// replica-group, we mark the whole replica-group down and not serve queries 
with this replica-group.
-///
-/// The selection algorithm is the same as {@link 
ReplicaGroupInstanceSelector}, and will always evenly distribute the
-/// traffic to all replica-groups that have all online segments available.
+/// The strict replica-group routing strategy always routes same-partition 
segments to the same instance. During
+/// routing state construction, [#updateSegmentMapsForUpsertTable(IdealState, 
ExternalView, Set, Map)] removes from
+/// every segment in a partition any replica that is unavailable for any old 
segment in that partition. Consequently,
+/// all same-partition segments have identical, ordered candidate identities.
 ///
-/// The algorithm relies on the mirror segment assignment from replica-group 
segment assignment strategy. With mirror
-/// segment assignment, any server in one replica-group will always have a 
corresponding server in other replica-groups
-/// that have the same segments assigned. For example, if S1 is a server in 
replica-group 1, and it has mirror server S2
-/// in replica-group 2 and S3 in replica-group 3. All segments assigned to S1 
will also be assigned to S2 and S3. In
-/// stable scenario (external view matches ideal state), all segments assigned 
to S1 will have the same enabled
-/// instances of [S1, S2, S3] sorted (in alphabetical order). If we always 
pick the same index of enabled instances for
-/// all segments, only one of S1, S2, S3 will be picked, and all the segments 
are processed by the same server. In
-/// transitioning/error scenario (external view does not match ideal state), 
if a segment is down on S1, we mark all
-/// segments with the same assignment ([S1, S2, S3]) down on S1 to ensure that 
we always route the segments to the same
-/// replica-group.
+/// Adaptive routing preserves that guarantee without explicit partition or 
mirror-set metadata. The inherited

Review Comment:
   The rework is the right call, and this javadoc does a good job of stating 
why it works. Two assumptions are now load-bearing and worth naming explicitly 
here, because neither lives in this file:
   
   1. **One ranking snapshot per query.** If the parent ever re-ranked per 
segment, same-partition segments could diverge.
   2. **The strictly-less-than tie-break** in 
`ReplicaGroupInstanceSelector.selectServers()` (see my comment there).
   
   I verified the argument itself: for old segments in one instance set, 
`candidates = idealState - unavailableInstances` — the online filter is 
implied, since `unavailableInstances` is the union of non-online instances over 
every old segment in that set — so all old segments in a partition get 
identical, identically-ordered lists. New segments get the same instance list 
with per-segment online flags, and selection keys off instance identity, not 
the flag. `refreshSegmentStates()` then filters by `_enabledInstances` 
uniformly. It holds; I'd just like the two dependencies written down so a 
future refactor of the parent doesn't quietly break the subclass with no test 
in this file failing.



##########
pom.xml:
##########
@@ -2061,7 +2061,7 @@
         <plugin>
           <groupId>com.diffplug.spotless</groupId>
           <artifactId>spotless-maven-plugin</artifactId>
-          <version>2.46.1</version>
+          <version>3.0.0</version>

Review Comment:
   Thanks for dropping the cleanthat engine — a version bump is the right shape 
of fix. But this is still a build-wide change riding along in a routing PR, and 
a major version bump is a bigger one than the engine switch it replaces: 
spotless 3.0.0 can change formatting defaults and tool requirements across all 
~100 modules, and if it misbehaves it'll get bisected to this PR.
   
   I'd land it separately, or stay on 2.46.1 and pin `googleJavaFormat`'s 
version inside `removeUnusedImports` — the minimal change that unblocks the 
record. Not blocking the approval, but I don't think this PR should be the 
thing that moves the repo's formatter.



##########
pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/StrictReplicaGroupInstanceSelector.java:
##########
@@ -18,44 +18,52 @@
  */
 package org.apache.pinot.broker.routing.instanceselector;
 
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import org.apache.helix.model.ExternalView;
 import org.apache.helix.model.IdealState;
+import 
org.apache.pinot.broker.routing.adaptiveserverselector.ServerSelectionContext;
+import org.apache.pinot.common.utils.config.QueryOptionsUtils;
+
 
 /// Instance selector for strict replica-group routing strategy.
 ///
-/// ```
-/// The strict replica-group routing strategy always routes the query to the 
instances within the same replica-group.
-/// (Note that the replica-group information is derived from the ideal state 
of the table, where the instances are
-/// sorted alphabetically in the instance state map, so the replica-groups in 
the instance selector might not match the
-/// replica-groups in the instance partitions). The goal of this algorithm is 
to ensure that segments from the same
-/// partition are never served from multiple different instances. The 
instances in a replica-group should have all the
-/// online segments (segments with ONLINE/CONSUMING instances in the ideal 
state and selected by the pre-selector)
-/// available (ONLINE/CONSUMING in the external view) in order to serve 
queries. If any segment is unavailable in the
-/// replica-group, we mark the whole replica-group down and not serve queries 
with this replica-group.
-///
-/// The selection algorithm is the same as {@link 
ReplicaGroupInstanceSelector}, and will always evenly distribute the
-/// traffic to all replica-groups that have all online segments available.
+/// The strict replica-group routing strategy always routes same-partition 
segments to the same instance. During
+/// routing state construction, [#updateSegmentMapsForUpsertTable(IdealState, 
ExternalView, Set, Map)] removes from
+/// every segment in a partition any replica that is unavailable for any old 
segment in that partition. Consequently,
+/// all same-partition segments have identical, ordered candidate identities.
 ///
-/// The algorithm relies on the mirror segment assignment from replica-group 
segment assignment strategy. With mirror
-/// segment assignment, any server in one replica-group will always have a 
corresponding server in other replica-groups
-/// that have the same segments assigned. For example, if S1 is a server in 
replica-group 1, and it has mirror server S2
-/// in replica-group 2 and S3 in replica-group 3. All segments assigned to S1 
will also be assigned to S2 and S3. In
-/// stable scenario (external view matches ideal state), all segments assigned 
to S1 will have the same enabled
-/// instances of [S1, S2, S3] sorted (in alphabetical order). If we always 
pick the same index of enabled instances for
-/// all segments, only one of S1, S2, S3 will be picked, and all the segments 
are processed by the same server. In
-/// transitioning/error scenario (external view does not match ideal state), 
if a segment is down on S1, we mark all
-/// segments with the same assignment ([S1, S2, S3]) down on S1 to ensure that 
we always route the segments to the same
-/// replica-group.
+/// Adaptive routing preserves that guarantee without explicit partition or 
mirror-set metadata. The inherited
+/// selector takes one ranking snapshot for the query and deterministically 
chooses the best candidate from each
+/// segment's ordered list. Identical filtered candidate lists, the same 
ranking snapshot, and deterministic list-order
+/// tie-breaking therefore produce identical selections for every segment in a 
partition. Different partitions may
+/// independently choose different replicas.
 ///
-/// Note that new segments won't be used to exclude instances from serving 
when the segment is unavailable.
-/// ```
+/// New segments do not exclude a candidate when that segment is unavailable; 
they remain optional so that the broker or
+/// server can skip them if necessary.
 public class StrictReplicaGroupInstanceSelector extends 
ReplicaGroupInstanceSelector {
 
   @Override
   void updateSegmentMaps(IdealState idealState, ExternalView externalView, 
Set<String> onlineSegments,
       Map<String, Long> newSegmentCreationTimeMap) {
     super.updateSegmentMapsForUpsertTable(idealState, externalView, 
onlineSegments, newSegmentCreationTimeMap);
   }
+
+  @Override
+  public InstanceMapping select(List<String> segments, int requestId,
+      SegmentStates segmentStates, Map<String, String> queryOptions) {
+    ServerSelectionContext ctx = new ServerSelectionContext(queryOptions, 
_config);
+    if (_adaptiveServerSelector != null && _priorityPoolInstanceSelector != 
null) {
+      if (ctx.isUseFixedReplica()) {
+        throw new IllegalArgumentException(

Review Comment:
   This throws for the config-level setting too, not just the query option — 
`ServerSelectionContext` falls back to `InstanceSelectorConfig`, which 
`InstanceSelectorFactory` populates from `pinot.broker.use.fixed.replica` and 
`routingConfig.getUseFixedReplica()`.
   
   So an operator who set either of those (default is `false`, so this is 
opt-in, but it's a documented feature) plus adaptive routing gets an 
`IllegalArgumentException` on **every** query against a strict-RG table. 
`calculateRoutes()` isn't wrapped in a try at 
`BaseSingleStageBrokerRequestHandler:782`, so it surfaces as an internal error 
rather than a `QueryErrorCode`, and the only remedy is a config change plus a 
restart. Before this PR that combination worked — fixed replica simply won.
   
   And fixed replica is *safe* under your new argument: with identical 
per-partition candidate lists, `candidates.get((tableNameHash + replicaOffset) 
% numCandidates)` yields the same index for every segment in a partition, so 
the strict invariant holds.
   
   Suggestion: handle it in `InstanceSelectorFactory` exactly like the legacy 
upsert/dedup block you added below — if strict RG + adaptive + 
`useFixedReplica`, log a warning and null out 
`effectiveAdaptiveServerSelector`. If you specifically want to reject the 
*query option*, it should be a `BadQueryRequestException`/`QueryErrorCode` and 
must not fire for the config or table-level setting. The test name 
(`…RejectsFixedReplicaQueryOption`) reads like the query-option case is what 
was actually intended.



##########
pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/ReplicaGroupInstanceSelector.java:
##########
@@ -124,13 +126,19 @@ private Pair<Map<String, String>, Map<String, String>> 
selectServers(List<String
       } else if (MapUtils.isNotEmpty(serverRankMap)) {
         // Adaptive Server Selection is enabled.
         // Use the instance with the best rank if all servers have stats 
populated, else use the round-robin selected
-        // instance
-        selectedInstance = candidates.stream()
-            .anyMatch(candidate -> 
!serverRankMap.containsKey(candidate.getInstance()))
-            ? selectedInstance
-            : candidates.stream()
-                .min(Comparator.comparingInt(candidate -> 
serverRankMap.get(candidate.getInstance())))
-                .orElse(selectedInstance);
+        // instance. As of 8 July 2026, this fallback is unreachable, but new 
implementations could require it.
+        int bestRank = Integer.MAX_VALUE;
+        for (SegmentInstanceCandidate candidate : candidates) {
+          Integer rank = serverRankMap.get(candidate.getInstance());
+          if (rank == null) {
+            selectedInstance = candidates.get(instanceIdx);
+            break;
+          }
+          if (rank < bestRank) {

Review Comment:
   The rewrite is a good change on its own — no streams, no double traversal, 
and the semantics are preserved (any unranked candidate falls back to the 
round-robin index, same as the old `anyMatch` guard).
   
   Worth a comment saying this loop is now load-bearing for 
`StrictReplicaGroupInstanceSelector`: `rank < bestRank` means the first 
candidate in list order wins a tie, and candidate lists are identical per 
partition, which is precisely what makes every segment of a partition resolve 
to the same server. Changing `<` to `<=`, reordering candidates, or 
reintroducing a shuffle would silently break the strict guarantee, and nothing 
in `ReplicaGroupSelectorTest` would point at this line. Same for the `rank == 
null` branch reusing `instanceIdx` rather than a per-candidate fallback.



##########
pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/StrictReplicaGroupInstanceSelector.java:
##########
@@ -18,44 +18,52 @@
  */
 package org.apache.pinot.broker.routing.instanceselector;
 
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import org.apache.helix.model.ExternalView;
 import org.apache.helix.model.IdealState;
+import 
org.apache.pinot.broker.routing.adaptiveserverselector.ServerSelectionContext;
+import org.apache.pinot.common.utils.config.QueryOptionsUtils;
+
 
 /// Instance selector for strict replica-group routing strategy.
 ///
-/// ```
-/// The strict replica-group routing strategy always routes the query to the 
instances within the same replica-group.
-/// (Note that the replica-group information is derived from the ideal state 
of the table, where the instances are
-/// sorted alphabetically in the instance state map, so the replica-groups in 
the instance selector might not match the
-/// replica-groups in the instance partitions). The goal of this algorithm is 
to ensure that segments from the same
-/// partition are never served from multiple different instances. The 
instances in a replica-group should have all the
-/// online segments (segments with ONLINE/CONSUMING instances in the ideal 
state and selected by the pre-selector)
-/// available (ONLINE/CONSUMING in the external view) in order to serve 
queries. If any segment is unavailable in the
-/// replica-group, we mark the whole replica-group down and not serve queries 
with this replica-group.
-///
-/// The selection algorithm is the same as {@link 
ReplicaGroupInstanceSelector}, and will always evenly distribute the
-/// traffic to all replica-groups that have all online segments available.
+/// The strict replica-group routing strategy always routes same-partition 
segments to the same instance. During
+/// routing state construction, [#updateSegmentMapsForUpsertTable(IdealState, 
ExternalView, Set, Map)] removes from
+/// every segment in a partition any replica that is unavailable for any old 
segment in that partition. Consequently,
+/// all same-partition segments have identical, ordered candidate identities.
 ///
-/// The algorithm relies on the mirror segment assignment from replica-group 
segment assignment strategy. With mirror
-/// segment assignment, any server in one replica-group will always have a 
corresponding server in other replica-groups
-/// that have the same segments assigned. For example, if S1 is a server in 
replica-group 1, and it has mirror server S2
-/// in replica-group 2 and S3 in replica-group 3. All segments assigned to S1 
will also be assigned to S2 and S3. In
-/// stable scenario (external view matches ideal state), all segments assigned 
to S1 will have the same enabled
-/// instances of [S1, S2, S3] sorted (in alphabetical order). If we always 
pick the same index of enabled instances for
-/// all segments, only one of S1, S2, S3 will be picked, and all the segments 
are processed by the same server. In
-/// transitioning/error scenario (external view does not match ideal state), 
if a segment is down on S1, we mark all
-/// segments with the same assignment ([S1, S2, S3]) down on S1 to ensure that 
we always route the segments to the same
-/// replica-group.
+/// Adaptive routing preserves that guarantee without explicit partition or 
mirror-set metadata. The inherited
+/// selector takes one ranking snapshot for the query and deterministically 
chooses the best candidate from each
+/// segment's ordered list. Identical filtered candidate lists, the same 
ranking snapshot, and deterministic list-order
+/// tie-breaking therefore produce identical selections for every segment in a 
partition. Different partitions may
+/// independently choose different replicas.
 ///
-/// Note that new segments won't be used to exclude instances from serving 
when the segment is unavailable.
-/// ```
+/// New segments do not exclude a candidate when that segment is unavailable; 
they remain optional so that the broker or
+/// server can skip them if necessary.
 public class StrictReplicaGroupInstanceSelector extends 
ReplicaGroupInstanceSelector {
 
   @Override
   void updateSegmentMaps(IdealState idealState, ExternalView externalView, 
Set<String> onlineSegments,
       Map<String, Long> newSegmentCreationTimeMap) {
     super.updateSegmentMapsForUpsertTable(idealState, externalView, 
onlineSegments, newSegmentCreationTimeMap);
   }
+
+  @Override
+  public InstanceMapping select(List<String> segments, int requestId,
+      SegmentStates segmentStates, Map<String, String> queryOptions) {
+    ServerSelectionContext ctx = new ServerSelectionContext(queryOptions, 
_config);
+    if (_adaptiveServerSelector != null && _priorityPoolInstanceSelector != 
null) {
+      if (ctx.isUseFixedReplica()) {
+        throw new IllegalArgumentException(
+            "useFixedReplica cannot be used when adaptive routing is enabled 
for StrictReplicaGroupInstanceSelector");
+      }
+      if (QueryOptionsUtils.getNumReplicaGroupsToQuery(ctx.getQueryOptions()) 
!= null) {

Review Comment:
   These two guards look inverted to me: the option that's rejected above is 
safe, and the one preserved here is the one that breaks the invariant this PR 
exists to protect.
   
   With `numReplicaGroupsToQuery` set, `selectServers()` cycles `replicaOffset` 
per segment, so consecutive segments deliberately land in different replica 
groups — including segments of the same partition. 
`testStrictReplicaGroupAdaptivePreservesNumReplicaGroupsToQuery` asserts 
exactly that: in the fixture `seg0` and `seg1` share the candidate list 
(`server_a`@rg0, `server_c`@rg1), i.e. same partition, and the expected result 
routes `seg0` to `server_a` and `seg1` to `server_c`. That's the 
cross-replica-group read the PR description says can miss or duplicate upsert 
keys.
   
   It's pre-existing behavior so I'm not asking you to fix it here, but the 
comment calls it intentional and a test now blesses it, which reads oddly next 
to a hard throw on the safe option. At minimum I'd reword this to say the path 
is knowingly unsafe for upsert tables, and consider rejecting or ignoring 
`numReplicaGroupsToQuery` for strict RG as a follow-up.



##########
pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/InstanceSelector.java:
##########
@@ -72,27 +71,45 @@ void init(TableConfig tableConfig, 
ZkHelixPropertyStore<ZNRecord> propertyStore,
   /// Returns the enabled server instances currently serving the table.
   Set<String> getServingInstances();
 
+  /// Holds the result of an instance selection: `segmentToInstanceMap` maps 
each segment to its selected server
+  /// instance, `optionalSegmentToInstanceMap` maps segments not yet fully 
online that the server may skip, and
+  /// `unavailableSegments` lists segments that have candidates but could not 
be routed. These unavailable segments are
+  /// merged into the query's unavailable-segment list and surfaced as a 
`BROKER_SEGMENT_UNAVAILABLE` error.
+  record InstanceMapping(Map<String, String> segmentToInstanceMap,
+      Map<String, String> optionalSegmentToInstanceMap,
+      List<String> unavailableSegments) {

Review Comment:
   After the rework, nothing in production populates `unavailableSegments`. 
Grepping the PR head, the 3-arg form is constructed exactly twice: line 81 
(`EMPTY`) and `InstanceSelectorTest:791` (the fake selector added to test the 
dedup path). Every real selector uses the 2-arg convenience constructor.
   
   So the third component, the `LinkedHashSet` dedup in `BaseInstanceSelector`, 
and the javadoc sentence promising a `BROKER_SEGMENT_UNAVAILABLE` error all 
describe a path that can't be reached today. Since the dropped-segment behavior 
is gone, I'd drop the third component and the dedup and keep the record at two 
— it makes the refactor commit purely mechanical, which is easier to backport. 
Keeping it as an extension point for custom selectors is defensible too, but 
then the javadoc should say that's what it is; as written it reads as though 
some selector does this.



##########
pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/InstanceSelectorFactory.java:
##########
@@ -136,8 +145,15 @@ public static InstanceSelector 
getInstanceSelector(TableConfig tableConfig,
     if (instanceSelector == null) {
       instanceSelector = new BalancedInstanceSelector();
     }
+    if (instanceSelector.getClass() == ReplicaGroupInstanceSelector.class

Review Comment:
   Good catch closing the legacy upsert/dedup path. Two notes on the mechanics:
   
   - `getClass() == ReplicaGroupInstanceSelector.class` correctly excludes the 
Strict subclass, but an exact-class test also excludes any plugin-provided 
subclass. That's probably what you want; it's unusual enough to deserve a 
comment saying the exclusion is deliberate.
   - Unlike the strict path, this disable isn't gated by the new config flag, 
so an operator has no escape hatch if it regresses their latency. Reasonable 
default for a correctness fix, but it's a silent behavior change for those 
tables — worth a line in the PR description and release notes next to the flag.



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