6666import org .apache .pinot .spi .exception .TerminationException ;
6767import org .apache .pinot .spi .query .QueryThreadContext ;
6868import org .apache .pinot .spi .utils .CommonConstants .Broker .Request .QueryOptionValue ;
69+ import org .apache .pinot .spi .utils .CommonConstants .MultiStageQueryRunner .TimeoutOverflowMode ;
6970import org .slf4j .Logger ;
7071import org .slf4j .LoggerFactory ;
7172
@@ -81,6 +82,8 @@ public class LeafOperator extends MultiStageOperator {
8182 ErrorMseBlock .fromError (QueryErrorCode .QUERY_CANCELLATION , "Cancelled while waiting for leaf results" );
8283 private static final ErrorMseBlock TIMEOUT_BLOCK =
8384 ErrorMseBlock .fromError (QueryErrorCode .EXECUTION_TIMEOUT , "Timed out waiting for leaf results" );
85+ public static final String LEAF_ACTIVE_DEADLINE_METADATA_KEY = "leafActiveDeadlineMs" ;
86+ public static final String LEAF_PASSIVE_DEADLINE_METADATA_KEY = "leafPassiveDeadlineMs" ;
8487
8588 // Use a special results block to indicate that this is the last results block
8689 @ VisibleForTesting
@@ -98,10 +101,14 @@ public class LeafOperator extends MultiStageOperator {
98101 // Use a limit-sized BlockingQueue to store the results blocks and apply back pressure to the single-stage threads
99102 @ VisibleForTesting
100103 final BlockingQueue <BaseResultsBlock > _blockingQueue ;
104+ private final TimeoutOverflowMode _timeoutOverflowMode ;
101105
102106 @ Nullable
103107 private volatile Future <?> _executionFuture ;
104108 private volatile boolean _terminated ;
109+ private volatile boolean _timeoutOverflowTriggered ;
110+ private final long _customActiveDeadlineMs ;
111+ private final long _customPassiveDeadlineMs ;
105112
106113 public LeafOperator (OpChainExecutionContext context , List <ServerQueryRequest > requests , DataSchema dataSchema ,
107114 QueryExecutor queryExecutor , ExecutorService executorService ) {
@@ -118,6 +125,12 @@ public LeafOperator(OpChainExecutionContext context, List<ServerQueryRequest> re
118125 Integer maxStreamingPendingBlocks = QueryOptionsUtils .getMaxStreamingPendingBlocks (context .getOpChainMetadata ());
119126 _blockingQueue = new ArrayBlockingQueue <>(maxStreamingPendingBlocks != null ? maxStreamingPendingBlocks
120127 : QueryOptionValue .DEFAULT_MAX_STREAMING_PENDING_BLOCKS );
128+ TimeoutOverflowMode timeoutOverflowMode =
129+ QueryOptionsUtils .getTimeoutOverflowMode (context .getOpChainMetadata ());
130+ _timeoutOverflowMode = timeoutOverflowMode != null ? timeoutOverflowMode : TimeoutOverflowMode .THROW ;
131+ Map <String , String > metadata = context .getOpChainMetadata ();
132+ _customActiveDeadlineMs = parseDeadline (metadata .get (LEAF_ACTIVE_DEADLINE_METADATA_KEY ));
133+ _customPassiveDeadlineMs = parseDeadline (metadata .get (LEAF_PASSIVE_DEADLINE_METADATA_KEY ));
121134 }
122135
123136 public List <ServerQueryRequest > getRequests () {
@@ -168,15 +181,15 @@ protected MseBlock getNextBlock() {
168181 BaseResultsBlock resultsBlock ;
169182 try {
170183 // Here we use passive deadline because we end up waiting for the SSE operators which can timeout by their own.
171- resultsBlock =
172- _blockingQueue .poll (_context . getPassiveDeadlineMs () - System .currentTimeMillis (), TimeUnit .MILLISECONDS );
184+ long deadlineMs = getWaitDeadlineMs ();
185+ resultsBlock = _blockingQueue .poll (Math . max ( 0L , deadlineMs - System .currentTimeMillis () ), TimeUnit .MILLISECONDS );
173186 } catch (InterruptedException e ) {
174187 terminateAndClearResultsBlocks ();
175188 return replaceWithTerminateExceptionIfAvailable (CANCELLED_BLOCK );
176189 }
177190 if (resultsBlock == null ) {
178191 terminateAndClearResultsBlocks ();
179- return replaceWithTerminateExceptionIfAvailable (TIMEOUT_BLOCK );
192+ return handleTimeoutOverflow (TIMEOUT_BLOCK , "waiting for leaf results" );
180193 }
181194 // Terminate when there is error block
182195 ErrorMseBlock errorBlock = getErrorBlock ();
@@ -201,6 +214,42 @@ private MseBlock replaceWithTerminateExceptionIfAvailable(ErrorMseBlock errorBlo
201214 terminateException .getMessage ()) : errorBlock ;
202215 }
203216
217+ private MseBlock handleTimeoutOverflow (ErrorMseBlock defaultBlock , String reason ) {
218+ if (recordTimeoutOverflow (reason )) {
219+ return SuccessMseBlock .INSTANCE ;
220+ }
221+ return replaceWithTerminateExceptionIfAvailable (defaultBlock );
222+ }
223+
224+ private boolean recordTimeoutOverflow (String reason ) {
225+ if (_timeoutOverflowMode != TimeoutOverflowMode .BREAK ) {
226+ return false ;
227+ }
228+ if (!_timeoutOverflowTriggered ) {
229+ LOGGER .warn ("Leaf operator for table '{}' returning partial results after timeout: {}" , _tableName , reason );
230+ }
231+ _timeoutOverflowTriggered = true ;
232+ _statMap .merge (StatKey .TIMEOUT_OVERFLOW_REACHED , true );
233+ return true ;
234+ }
235+
236+ private long getActiveDeadlineMsOverride () {
237+ return _customActiveDeadlineMs != Long .MAX_VALUE ? _customActiveDeadlineMs : _context .getActiveDeadlineMs ();
238+ }
239+
240+ private long getPassiveDeadlineMsOverride () {
241+ return _customPassiveDeadlineMs != Long .MAX_VALUE ? _customPassiveDeadlineMs : _context .getPassiveDeadlineMs ();
242+ }
243+
244+ private static long parseDeadline (@ Nullable String value ) {
245+ return value != null ? Long .parseLong (value ) : Long .MAX_VALUE ;
246+ }
247+
248+ private long getWaitDeadlineMs () {
249+ return _timeoutOverflowMode == TimeoutOverflowMode .BREAK ? getActiveDeadlineMsOverride ()
250+ : getPassiveDeadlineMsOverride ();
251+ }
252+
204253 public ExplainedNode explain () {
205254 if (_executionFuture == null ) {
206255 _executionFuture = startExecution ();
@@ -214,8 +263,9 @@ public ExplainedNode explain() {
214263 BaseResultsBlock resultsBlock ;
215264 try {
216265 // Here we use passive deadline because we end up waiting for the SSE operators which can timeout by their own.
217- resultsBlock =
218- _blockingQueue .poll (_context .getPassiveDeadlineMs () - System .currentTimeMillis (), TimeUnit .MILLISECONDS );
266+ long deadlineMs = getWaitDeadlineMs ();
267+ long waitMs = Math .max (0L , deadlineMs - System .currentTimeMillis ());
268+ resultsBlock = _blockingQueue .poll (waitMs , TimeUnit .MILLISECONDS );
219269 } catch (InterruptedException e ) {
220270 terminateAndClearResultsBlocks ();
221271 checkTerminateException ();
@@ -470,8 +520,11 @@ void execute() {
470520 });
471521 }
472522 try {
473- if (!latch .await (_context .getPassiveDeadlineMs () - System .currentTimeMillis (), TimeUnit .MILLISECONDS )) {
474- setErrorBlock (TIMEOUT_BLOCK );
523+ long deadlineMs = getWaitDeadlineMs ();
524+ if (!latch .await (Math .max (0L , deadlineMs - System .currentTimeMillis ()), TimeUnit .MILLISECONDS )) {
525+ if (!recordTimeoutOverflow ("waiting for hybrid leaf responses" )) {
526+ setErrorBlock (TIMEOUT_BLOCK );
527+ }
475528 }
476529 } catch (InterruptedException e ) {
477530 setErrorBlock (CANCELLED_BLOCK );
@@ -510,7 +563,9 @@ private void executeOneRequest(ServerQueryRequest request, @Nullable Runnable on
510563 } catch (InterruptedException e ) {
511564 setErrorBlock (CANCELLED_BLOCK );
512565 } catch (TimeoutException e ) {
513- setErrorBlock (TIMEOUT_BLOCK );
566+ if (!recordTimeoutOverflow ("adding results block" )) {
567+ setErrorBlock (TIMEOUT_BLOCK );
568+ }
514569 } catch (Exception e ) {
515570 if (!(e instanceof EarlyTerminationException )) {
516571 LOGGER .warn ("Failed to add results block" , e );
@@ -526,8 +581,9 @@ void addResultsBlock(BaseResultsBlock resultsBlock)
526581 if (_terminated ) {
527582 throw new EarlyTerminationException ("Query has been terminated" );
528583 }
529- if (!_blockingQueue .offer (resultsBlock , _context .getPassiveDeadlineMs () - System .currentTimeMillis (),
530- TimeUnit .MILLISECONDS )) {
584+ long deadlineMs = getWaitDeadlineMs ();
585+ long waitMs = Math .max (0L , deadlineMs - System .currentTimeMillis ());
586+ if (!_blockingQueue .offer (resultsBlock , waitMs , TimeUnit .MILLISECONDS )) {
531587 throw new TimeoutException ("Timed out waiting to add results block" );
532588 }
533589 }
@@ -704,6 +760,7 @@ public long merge(long value1, long value2) {
704760 GROUPS_TRIMMED (StatMap .Type .BOOLEAN ),
705761 NUM_GROUPS_LIMIT_REACHED (StatMap .Type .BOOLEAN ),
706762 NUM_GROUPS_WARNING_LIMIT_REACHED (StatMap .Type .BOOLEAN ),
763+ TIMEOUT_OVERFLOW_REACHED (StatMap .Type .BOOLEAN , BrokerResponseNativeV2 .StatKey .TIMEOUT_OVERFLOW_REACHED ),
707764 NUM_RESIZES (StatMap .Type .INT , null ),
708765 RESIZE_TIME_MS (StatMap .Type .LONG , null ),
709766 THREAD_CPU_TIME_NS (StatMap .Type .LONG , null ),
0 commit comments