Jackie-Jiang commented on code in PR #19178: URL: https://github.com/apache/pinot/pull/19178#discussion_r4251524516
########## pinot-server/src/main/java/org/apache/pinot/server/starter/helix/BrokerRoutingReadyChecker.java: ########## @@ -0,0 +1,371 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.pinot.server.starter.helix; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.io.IOException; +import java.net.URI; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BooleanSupplier; +import java.util.function.LongSupplier; +import java.util.function.Predicate; +import java.util.function.Supplier; +import javax.annotation.Nullable; +import org.apache.hc.core5.http.Header; +import org.apache.helix.HelixAdmin; +import org.apache.helix.HelixManager; +import org.apache.helix.model.ExternalView; +import org.apache.helix.model.InstanceConfig; +import org.apache.pinot.common.auth.AuthProviderUtils; +import org.apache.pinot.common.auth.NullAuthProvider; +import org.apache.pinot.common.utils.SimpleHttpResponse; +import org.apache.pinot.common.utils.config.InstanceUtils; +import org.apache.pinot.common.utils.helix.HelixHelper; +import org.apache.pinot.common.utils.http.HttpClient; +import org.apache.pinot.common.utils.http.HttpClientConfig; +import org.apache.pinot.common.utils.tls.TlsUtils; +import org.apache.pinot.spi.auth.AuthProvider; +import org.apache.pinot.spi.utils.CommonConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/// Checks broker routing state while a server starts. A single background thread polls every online broker until all +/// of them report the server as routable, then caches success. Readiness requests only read the cached state and never +/// perform network I/O. Broker requests are sequential and have bounded connect, pool-checkout and response timeouts, +/// so an unavailable broker cannot create unbounded tasks or threads. +public class BrokerRoutingReadyChecker implements AutoCloseable { + private static final Logger LOGGER = LoggerFactory.getLogger(BrokerRoutingReadyChecker.class); + private static final long CHECK_INTERVAL_MS = 1_000L; + private static final long REQUEST_TIMEOUT_MS = 5_000L; + private static final int MAX_RESPONSE_LENGTH = 1_024; + + private final String _serverInstanceId; + private final Supplier<Set<String>> _onlineBrokersSupplier; + private final BrokersReadyEvaluator _allBrokersReady; + @Nullable + private final ScheduledExecutorService _checkExecutor; + private final RoutingStatusClient _routingStatusClient; + private final LongSupplier _currentTimeMs; + private final long _deadlineMs; + private final boolean _failOpen; + private final AuthProvider _authProvider; + private final AtomicReference<State> _state; + private boolean _timeoutLogged; + + public BrokerRoutingReadyChecker(HelixManager helixManager, long timeoutMs, boolean failOpen, + AuthProvider authProvider) { + this(createProductionContext(helixManager, authProvider), timeoutMs, failOpen, authProvider); + } + + @VisibleForTesting + BrokerRoutingReadyChecker(HelixManager helixManager, long timeoutMs, boolean failOpen, AuthProvider authProvider, + RoutingStatusClient routingStatusClient) { + this(helixManager, timeoutMs, failOpen, authProvider, routingStatusClient, System::currentTimeMillis); + } + + @VisibleForTesting + BrokerRoutingReadyChecker(HelixManager helixManager, long timeoutMs, boolean failOpen, AuthProvider authProvider, + RoutingStatusClient routingStatusClient, LongSupplier currentTimeMs) { + this(createContext(helixManager, routingStatusClient, null, authProvider), timeoutMs, failOpen, currentTimeMs, + authProvider); + } + + private BrokerRoutingReadyChecker(ProductionContext context, long timeoutMs, boolean failOpen, + AuthProvider authProvider) { + this(context, timeoutMs, failOpen, System::currentTimeMillis, authProvider); + } + + private BrokerRoutingReadyChecker(ProductionContext context, long timeoutMs, boolean failOpen, + LongSupplier currentTimeMs, AuthProvider authProvider) { + this(context._serverInstanceId, context._onlineBrokersSupplier, context._allBrokersReady, + context._checkExecutor, context._routingStatusClient, timeoutMs, failOpen, currentTimeMs, + authProvider); + } + + @VisibleForTesting + BrokerRoutingReadyChecker(String serverInstanceId, Supplier<Set<String>> onlineBrokersSupplier, + Predicate<Set<String>> allBrokersReady) { + this(serverInstanceId, onlineBrokersSupplier, allBrokersReady, Long.MAX_VALUE, false, () -> 0L); + } + + @VisibleForTesting + BrokerRoutingReadyChecker(String serverInstanceId, Supplier<Set<String>> onlineBrokersSupplier, + Predicate<Set<String>> allBrokersReady, long timeoutMs, boolean failOpen, LongSupplier currentTimeMs) { + this(serverInstanceId, onlineBrokersSupplier, (brokers, shouldStop) -> allBrokersReady.test(brokers), null, + RoutingStatusClient.NOOP, timeoutMs, failOpen, currentTimeMs, new NullAuthProvider()); + } + + private BrokerRoutingReadyChecker(String serverInstanceId, Supplier<Set<String>> onlineBrokersSupplier, + BrokersReadyEvaluator allBrokersReady, @Nullable ScheduledExecutorService checkExecutor, + RoutingStatusClient routingStatusClient, long timeoutMs, boolean failOpen, LongSupplier currentTimeMs, + AuthProvider authProvider) { + _serverInstanceId = serverInstanceId; + _onlineBrokersSupplier = onlineBrokersSupplier; + _allBrokersReady = allBrokersReady; + _checkExecutor = checkExecutor; + _routingStatusClient = routingStatusClient; + _currentTimeMs = currentTimeMs; + long nowMs = currentTimeMs.getAsLong(); + _deadlineMs = timeoutMs >= Long.MAX_VALUE - nowMs ? Long.MAX_VALUE : nowMs + Math.max(timeoutMs, 0L); + _failOpen = failOpen; + _authProvider = authProvider; + _state = new AtomicReference<>(State.CHECKING); + if (_checkExecutor != null) { + _checkExecutor.scheduleWithFixedDelay(this::check, 0L, CHECK_INTERVAL_MS, TimeUnit.MILLISECONDS); + } + } + + public boolean isReady() { + transitionToReadyOnFailOpenTimeout(); + return _state.get() == State.READY; + } + + @VisibleForTesting + AuthProvider getAuthProvider() { + return _authProvider; + } + + @VisibleForTesting + synchronized void check() { + if (_state.get() != State.CHECKING || transitionToReadyOnFailOpenTimeout()) { + return; + } + try { + Set<String> onlineBrokers = _onlineBrokersSupplier.get(); + if (!onlineBrokers.isEmpty() && _allBrokersReady.test(onlineBrokers, this::shouldStopChecking) + && _state.get() == State.CHECKING + // Do not mark the server ready if broker membership changed while acknowledgements were collected. + && onlineBrokers.equals(_onlineBrokersSupplier.get())) { + if (_state.compareAndSet(State.CHECKING, State.READY)) { + LOGGER.info("All online brokers report server {} as routable: {}", _serverInstanceId, onlineBrokers); + stopChecking(); + } + return; + } + } catch (Exception e) { + LOGGER.debug("Failed to check broker routing readiness for server {}", _serverInstanceId, e); + } + + if (transitionToReadyOnFailOpenTimeout()) { + return; + } + if (_currentTimeMs.getAsLong() >= _deadlineMs) { + if (!_timeoutLogged) { + LOGGER.warn("Timed out waiting for all online brokers to report server {} as routable; failOpen={}", + _serverInstanceId, _failOpen); + _timeoutLogged = true; + } + } + } + + private boolean shouldStopChecking() { + return _state.get() != State.CHECKING || Thread.currentThread().isInterrupted() + || transitionToReadyOnFailOpenTimeout(); + } + + private boolean transitionToReadyOnFailOpenTimeout() { + if (!_failOpen || _currentTimeMs.getAsLong() < _deadlineMs) { + return false; + } + if (_state.compareAndSet(State.CHECKING, State.READY)) { + LOGGER.warn("Timed out waiting for all online brokers to report server {} as routable; failOpen=true", + _serverInstanceId); + stopChecking(); + } + return _state.get() != State.CHECKING; + } + + private void stopChecking() { + if (_checkExecutor != null) { + _checkExecutor.shutdown(); + } + } + + private static ProductionContext createProductionContext(HelixManager helixManager, AuthProvider authProvider) { + HttpClientConfig httpClientConfig = HttpClientConfig.newBuilder() + .withMaxConns(1) + .withMaxConnsPerRoute(1) + .withConnectionTimeoutMs((int) REQUEST_TIMEOUT_MS) + .withFollowRedirects(false) + .build(); + RoutingStatusClient routingStatusClient = new SecureRoutingStatusClient( + new HttpRoutingStatusClient(new HttpClient(httpClientConfig, TlsUtils.getSslContext(), true))); + ScheduledExecutorService checkExecutor = Executors.newSingleThreadScheduledExecutor( + new ThreadFactoryBuilder().setNameFormat("broker-routing-ready-check-%d").setDaemon(true).build()); + return createContext(helixManager, routingStatusClient, checkExecutor, authProvider); + } + + private static ProductionContext createContext(HelixManager helixManager, RoutingStatusClient routingStatusClient, + @Nullable ScheduledExecutorService checkExecutor, AuthProvider authProvider) { + String serverInstanceId = helixManager.getInstanceName(); + HelixAdmin helixAdmin = helixManager.getClusterManagmentTool(); + String clusterName = helixManager.getClusterName(); + Supplier<Set<String>> onlineBrokersSupplier = () -> { + ExternalView brokerResource = helixAdmin.getResourceExternalView(clusterName, + CommonConstants.Helix.BROKER_RESOURCE_INSTANCE); + return HelixHelper.getOnlineInstanceFromExternalView(brokerResource); Review Comment: **[P2] Discover live brokers independently of table assignments** The brokerResource ExternalView represents table assignments, so a live broker with no assigned tables does not appear in this set. HelixSetupUtils creates brokerResource without partitions, and broker registration only adds the broker to existing table partitions. In a fresh cluster with healthy upgraded brokers but no tables, enabling this gate with the default failOpen=false leaves servers unready indefinitely: check() rejects the empty set, although the existing local service-status check accepts zero table resources. Provisioning that waits for server readiness before creating tables cannot proceed. Discover live broker instances independently of table assignments, or explicitly distinguish the no-table case from missing/failed discovery while retaining the membership-stability check. A discovery-level regression should include a live broker and an empty brokerResource; the current empty-supplier test does not distinguish this from having no brokers. ########## pinot-broker/src/main/java/org/apache/pinot/broker/routing/manager/BaseBrokerRoutingManager.java: ########## @@ -1376,6 +1569,14 @@ public Set<String> getServingInstances(String tableNameWithType) { return routingEntry._instanceSelector.getServingInstances(); } + /// Returns whether the broker sees the server as routable. + public boolean isServerRoutable(String instanceId) { + // Read the map first. A new entry is inserted only after the server is marked pending, and removing the pending + // marker publishes all routing-entry updates that precede it. + return _routableServerInstanceMap.containsKey(instanceId) + && !_serversPendingRoutingUpdate.containsKey(instanceId); Review Comment: **[P2] Read readiness from a consistent routing state** These two reads can acknowledge a server that remains unready throughout an exclusion. Start with S present in the routable map but still pending after a failed table update. The reader observes map membership; a concurrent exclusion then publishes the map without S and clears its pending marker; the reader observes no marker and returns true. A failing MultiStageReplicaGroupSelector instance-partitions fetch provides a real path to the retained pending state. The routing endpoint can return 200 and the server checker can permanently cache that false acknowledgement. Disabling a pending server has the same shape. Use a consistent readiness snapshot, or verify that the captured routable map is still current after checking the pending marker. Holding the global read lock across both checks is another correctness option, with request-latency tradeoffs. Merely reversing the reads is insufficient. Add a controlled interleaving test that pauses between the reads and excludes a server with a retained pending update. ########## pinot-broker/src/main/java/org/apache/pinot/broker/routing/manager/BaseBrokerRoutingManager.java: ########## @@ -531,6 +570,163 @@ private void processInstanceConfigChangeInternal() { newDisabledServers, _excludedServers); } + /// Initializes an empty publication marker with the tables whose primary selector can route to the server. A + /// nonempty marker belongs to an earlier pass and retains its completed acknowledgements across retries. Must run + /// before publishing the routable map so the lock-free readiness check observes the pending marker first. + @GuardedBy("_globalLock.writeLock()") + private void markServersPendingRelevantTableUpdates(List<String> servers) { + for (String server : servers) { + if (!_serversPendingRoutingUpdate.get(server).isEmpty()) { + continue; + } + Set<String> relevantTables = new HashSet<>(); + for (RoutingEntry routingEntry : _routingEntryMap.values()) { + if (routingEntry.isServerAssigned(server)) { + relevantTables.add(routingEntry.getTableNameWithType()); + } + } + _serversPendingRoutingUpdate.put(server, relevantTables); + } + } + + /// Applies the current instance change plus any retained failed work independently to each table. Successful tables + /// acknowledge only the servers they can route; failed tables retain their complete change set for the retry. + @GuardedBy("_globalLock.writeLock()") + private void updateRoutingEntriesOnInstancesChange(List<String> changedServers) { + Map<String, List<String>> pendingServersByTable = new HashMap<>(); + _serversPendingRoutingUpdate.forEach((server, tables) -> { + for (String table : tables) { + pendingServersByTable.computeIfAbsent(table, ignored -> new ArrayList<>()).add(server); + } + }); + for (RoutingEntry routingEntry : _routingEntryMap.values()) { + String tableNameWithType = routingEntry.getTableNameWithType(); + List<String> tableChangedServers = new ArrayList<>(changedServers); + List<String> pendingAcknowledgements = pendingServersByTable.get(tableNameWithType); + if (pendingAcknowledgements != null) { + for (String pendingServer : pendingAcknowledgements) { + if (!tableChangedServers.contains(pendingServer)) { Review Comment: **[P3] Use a set to combine the per-table server lists** Optional simplification: combine changedServers, pendingAcknowledgements, and the retained failed servers in a set, then convert once to the List required by the selector API. These repeated ArrayList.contains() calls add quadratic membership work per table in the worst case during bulk enablement, when changedServers and pendingAcknowledgements contain the same large server set. The work runs under the global write lock. A set union also removes the duplicated membership/add loops. This is an update-path improvement; no measured performance regression is claimed. ########## pinot-server/src/main/java/org/apache/pinot/server/starter/helix/BrokerRoutingReadyChecker.java: ########## @@ -0,0 +1,371 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.pinot.server.starter.helix; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.io.IOException; +import java.net.URI; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BooleanSupplier; +import java.util.function.LongSupplier; +import java.util.function.Predicate; +import java.util.function.Supplier; +import javax.annotation.Nullable; +import org.apache.hc.core5.http.Header; +import org.apache.helix.HelixAdmin; +import org.apache.helix.HelixManager; +import org.apache.helix.model.ExternalView; +import org.apache.helix.model.InstanceConfig; +import org.apache.pinot.common.auth.AuthProviderUtils; +import org.apache.pinot.common.auth.NullAuthProvider; +import org.apache.pinot.common.utils.SimpleHttpResponse; +import org.apache.pinot.common.utils.config.InstanceUtils; +import org.apache.pinot.common.utils.helix.HelixHelper; +import org.apache.pinot.common.utils.http.HttpClient; +import org.apache.pinot.common.utils.http.HttpClientConfig; +import org.apache.pinot.common.utils.tls.TlsUtils; +import org.apache.pinot.spi.auth.AuthProvider; +import org.apache.pinot.spi.utils.CommonConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/// Checks broker routing state while a server starts. A single background thread polls every online broker until all +/// of them report the server as routable, then caches success. Readiness requests only read the cached state and never +/// perform network I/O. Broker requests are sequential and have bounded connect, pool-checkout and response timeouts, +/// so an unavailable broker cannot create unbounded tasks or threads. +public class BrokerRoutingReadyChecker implements AutoCloseable { + private static final Logger LOGGER = LoggerFactory.getLogger(BrokerRoutingReadyChecker.class); + private static final long CHECK_INTERVAL_MS = 1_000L; + private static final long REQUEST_TIMEOUT_MS = 5_000L; + private static final int MAX_RESPONSE_LENGTH = 1_024; + + private final String _serverInstanceId; + private final Supplier<Set<String>> _onlineBrokersSupplier; + private final BrokersReadyEvaluator _allBrokersReady; + @Nullable + private final ScheduledExecutorService _checkExecutor; + private final RoutingStatusClient _routingStatusClient; + private final LongSupplier _currentTimeMs; + private final long _deadlineMs; + private final boolean _failOpen; + private final AuthProvider _authProvider; + private final AtomicReference<State> _state; + private boolean _timeoutLogged; + + public BrokerRoutingReadyChecker(HelixManager helixManager, long timeoutMs, boolean failOpen, + AuthProvider authProvider) { + this(createProductionContext(helixManager, authProvider), timeoutMs, failOpen, authProvider); + } + + @VisibleForTesting + BrokerRoutingReadyChecker(HelixManager helixManager, long timeoutMs, boolean failOpen, AuthProvider authProvider, + RoutingStatusClient routingStatusClient) { + this(helixManager, timeoutMs, failOpen, authProvider, routingStatusClient, System::currentTimeMillis); + } + + @VisibleForTesting + BrokerRoutingReadyChecker(HelixManager helixManager, long timeoutMs, boolean failOpen, AuthProvider authProvider, + RoutingStatusClient routingStatusClient, LongSupplier currentTimeMs) { + this(createContext(helixManager, routingStatusClient, null, authProvider), timeoutMs, failOpen, currentTimeMs, + authProvider); + } + + private BrokerRoutingReadyChecker(ProductionContext context, long timeoutMs, boolean failOpen, + AuthProvider authProvider) { + this(context, timeoutMs, failOpen, System::currentTimeMillis, authProvider); + } + + private BrokerRoutingReadyChecker(ProductionContext context, long timeoutMs, boolean failOpen, + LongSupplier currentTimeMs, AuthProvider authProvider) { + this(context._serverInstanceId, context._onlineBrokersSupplier, context._allBrokersReady, + context._checkExecutor, context._routingStatusClient, timeoutMs, failOpen, currentTimeMs, + authProvider); + } + + @VisibleForTesting + BrokerRoutingReadyChecker(String serverInstanceId, Supplier<Set<String>> onlineBrokersSupplier, + Predicate<Set<String>> allBrokersReady) { + this(serverInstanceId, onlineBrokersSupplier, allBrokersReady, Long.MAX_VALUE, false, () -> 0L); + } + + @VisibleForTesting + BrokerRoutingReadyChecker(String serverInstanceId, Supplier<Set<String>> onlineBrokersSupplier, + Predicate<Set<String>> allBrokersReady, long timeoutMs, boolean failOpen, LongSupplier currentTimeMs) { + this(serverInstanceId, onlineBrokersSupplier, (brokers, shouldStop) -> allBrokersReady.test(brokers), null, + RoutingStatusClient.NOOP, timeoutMs, failOpen, currentTimeMs, new NullAuthProvider()); + } + + private BrokerRoutingReadyChecker(String serverInstanceId, Supplier<Set<String>> onlineBrokersSupplier, + BrokersReadyEvaluator allBrokersReady, @Nullable ScheduledExecutorService checkExecutor, + RoutingStatusClient routingStatusClient, long timeoutMs, boolean failOpen, LongSupplier currentTimeMs, + AuthProvider authProvider) { + _serverInstanceId = serverInstanceId; + _onlineBrokersSupplier = onlineBrokersSupplier; + _allBrokersReady = allBrokersReady; + _checkExecutor = checkExecutor; + _routingStatusClient = routingStatusClient; + _currentTimeMs = currentTimeMs; + long nowMs = currentTimeMs.getAsLong(); + _deadlineMs = timeoutMs >= Long.MAX_VALUE - nowMs ? Long.MAX_VALUE : nowMs + Math.max(timeoutMs, 0L); + _failOpen = failOpen; + _authProvider = authProvider; + _state = new AtomicReference<>(State.CHECKING); + if (_checkExecutor != null) { + _checkExecutor.scheduleWithFixedDelay(this::check, 0L, CHECK_INTERVAL_MS, TimeUnit.MILLISECONDS); + } + } + + public boolean isReady() { + transitionToReadyOnFailOpenTimeout(); + return _state.get() == State.READY; + } + + @VisibleForTesting + AuthProvider getAuthProvider() { + return _authProvider; + } + + @VisibleForTesting + synchronized void check() { + if (_state.get() != State.CHECKING || transitionToReadyOnFailOpenTimeout()) { + return; + } + try { + Set<String> onlineBrokers = _onlineBrokersSupplier.get(); + if (!onlineBrokers.isEmpty() && _allBrokersReady.test(onlineBrokers, this::shouldStopChecking) + && _state.get() == State.CHECKING + // Do not mark the server ready if broker membership changed while acknowledgements were collected. + && onlineBrokers.equals(_onlineBrokersSupplier.get())) { + if (_state.compareAndSet(State.CHECKING, State.READY)) { + LOGGER.info("All online brokers report server {} as routable: {}", _serverInstanceId, onlineBrokers); + stopChecking(); + } + return; + } + } catch (Exception e) { + LOGGER.debug("Failed to check broker routing readiness for server {}", _serverInstanceId, e); + } + + if (transitionToReadyOnFailOpenTimeout()) { + return; + } + if (_currentTimeMs.getAsLong() >= _deadlineMs) { + if (!_timeoutLogged) { + LOGGER.warn("Timed out waiting for all online brokers to report server {} as routable; failOpen={}", + _serverInstanceId, _failOpen); + _timeoutLogged = true; + } + } + } + + private boolean shouldStopChecking() { + return _state.get() != State.CHECKING || Thread.currentThread().isInterrupted() + || transitionToReadyOnFailOpenTimeout(); + } + + private boolean transitionToReadyOnFailOpenTimeout() { + if (!_failOpen || _currentTimeMs.getAsLong() < _deadlineMs) { + return false; + } + if (_state.compareAndSet(State.CHECKING, State.READY)) { + LOGGER.warn("Timed out waiting for all online brokers to report server {} as routable; failOpen=true", + _serverInstanceId); + stopChecking(); + } + return _state.get() != State.CHECKING; + } + + private void stopChecking() { + if (_checkExecutor != null) { + _checkExecutor.shutdown(); + } + } + + private static ProductionContext createProductionContext(HelixManager helixManager, AuthProvider authProvider) { + HttpClientConfig httpClientConfig = HttpClientConfig.newBuilder() + .withMaxConns(1) + .withMaxConnsPerRoute(1) + .withConnectionTimeoutMs((int) REQUEST_TIMEOUT_MS) + .withFollowRedirects(false) + .build(); Review Comment: **[P2] Disable automatic HTTP retries for the dedicated polling client** This client disables redirects but retains Apache's automatic retries for 429/503. In the pinned HttpClient 5.6.4, HttpRequestRetryExec gracefully closes the first retryable response before returning control to Pinot, draining its body before the 1,024-byte reader can enforce its limit. A chunked 503 that keeps supplying data can therefore occupy this single worker and consume an arbitrarily large response. The same retry path honors Retry-After: 3600 with an hour-long sleep: DefaultHttpRequestRetryStrategy compares only its default one-second delay against the response timeout, then calculates the header-derived delay separately. With the default fail-closed behavior, the checker cannot observe broker recovery during that drain/sleep period. Fail-open health checks still evaluate their deadline independently. Disable automatic retries for this dedicated client, preserving existing defaults for other callers; the scheduled checker already handles retries. Extend testBoundedResponseDoesNotDrainChunkedRemainder to cover 429/503 and add a Retry-After caseāthe current slow-body test covers only 200. -- 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]
