This is an automated email from the ASF dual-hosted git repository.

kfaraz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/master by this push:
     new 4e15d6ef99d fix: fix query context precedence layer for 
datasource-level per-segment timeout (#19654)
4e15d6ef99d is described below

commit 4e15d6ef99d58ebea27e1bb25291aab1ace3867d
Author: jtuglu1 <[email protected]>
AuthorDate: Fri Jul 31 03:43:17 2026 -0700

    fix: fix query context precedence layer for datasource-level per-segment 
timeout (#19654)
    
    Currently, per-datasource, per-segment timeout can be not set if there's a 
hardcoded
    static default query context 
(`druid.query.default.context.perSegmentTimeout`).
    
    This patch ensures the evaluation precedence for the per-segment timeout is 
as follows
    (from highest to lowest):
    
    1. Query-specified context values (passed in the query payload)
    2. Dynamic configs (broker per-datasource per-segment timeout, broker 
dynamic config, etc.).
    Former beats the latter for this particular config.
    3. Static defaults specified at process boot 
(`druid.query.default.context.perSegmentTimeout`)
    4. Default values specified in the application code.
    
    Also adds the perSegmentTimeout context key so it's easy to auto-complete 
in the UI.
---
 .../apache/druid/query/QueryConfigProvider.java    |   2 +-
 .../druid/client/BrokerViewOfBrokerConfig.java     |  45 +++++--
 .../org/apache/druid/server/QueryLifecycle.java    | 107 +++++++--------
 .../apache/druid/server/QueryLifecycleFactory.java |  23 +---
 .../druid/server/broker/BrokerDynamicConfig.java   |  36 +++++
 .../druid/server/broker/QueryConfigSnapshot.java   |  91 +++++++++++++
 .../druid/client/BrokerViewOfBrokerConfigTest.java |  52 ++++++++
 .../server/PerSegmentTimeoutInjectionTest.java     | 146 +++++++++------------
 .../apache/druid/server/QueryLifecycleTest.java    |  20 +--
 .../org/apache/druid/server/QueryResourceTest.java |  13 +-
 .../server/broker/BrokerDynamicConfigTest.java     |  45 +++++++
 .../druid/sql/calcite/run/NativeQueryMaker.java    |   4 +-
 .../query-context-completions.ts                   |   4 +
 13 files changed, 398 insertions(+), 190 deletions(-)

diff --git 
a/processing/src/main/java/org/apache/druid/query/QueryConfigProvider.java 
b/processing/src/main/java/org/apache/druid/query/QueryConfigProvider.java
index 6b533ab6a37..1559beb0732 100644
--- a/processing/src/main/java/org/apache/druid/query/QueryConfigProvider.java
+++ b/processing/src/main/java/org/apache/druid/query/QueryConfigProvider.java
@@ -22,7 +22,7 @@ package org.apache.druid.query;
 import java.util.Map;
 
 /**
- * Provides the default query config applied to all incoming queries before 
per-query overrides are merged in.
+ * Provides the default query context applied to all incoming queries before 
per-query overrides are merged in.
  *
  * <p>On non-broker nodes this is backed by static runtime properties ({@link 
DefaultQueryConfig}).
  * On brokers, it is backed by {@code BrokerViewOfBrokerConfig}, which merges 
the static defaults with
diff --git 
a/server/src/main/java/org/apache/druid/client/BrokerViewOfBrokerConfig.java 
b/server/src/main/java/org/apache/druid/client/BrokerViewOfBrokerConfig.java
index b67b4bc46da..0c6f52805bb 100644
--- a/server/src/main/java/org/apache/druid/client/BrokerViewOfBrokerConfig.java
+++ b/server/src/main/java/org/apache/druid/client/BrokerViewOfBrokerConfig.java
@@ -36,6 +36,7 @@ import org.apache.druid.rpc.ServiceClientFactory;
 import org.apache.druid.rpc.ServiceLocator;
 import org.apache.druid.rpc.StandardRetryPolicy;
 import org.apache.druid.server.broker.BrokerDynamicConfig;
+import org.apache.druid.server.broker.QueryConfigSnapshot;
 
 import javax.validation.constraints.NotNull;
 import java.util.Map;
@@ -54,11 +55,13 @@ public class BrokerViewOfBrokerConfig extends 
BaseBrokerViewOfConfig<BrokerDynam
   private final DefaultQueryConfig defaultQueryConfig;
 
   /**
-   * Pre-computed merge of {@link DefaultQueryConfig#getContext()} and
+   * The dynamic config plus the merge of {@link 
DefaultQueryConfig#getContext()} and
    * {@link BrokerDynamicConfig#getQueryContext()}, recomputed on each config 
sync.
-   * Dynamic config values override static defaults. {@link QueryContext} 
provides immutability.
+   *
+   * <p>volatile, not synchronized: read on the query hot path, see {@link 
BaseBrokerViewOfConfig}. Both halves
+   * live in one field so a query cannot observe them from different 
generations.
    */
-  private volatile QueryContext resolvedDefaultQueryContext;
+  private volatile QueryConfigSnapshot querySnapshot;
 
   @Inject
   public BrokerViewOfBrokerConfig(
@@ -69,7 +72,7 @@ public class BrokerViewOfBrokerConfig extends 
BaseBrokerViewOfConfig<BrokerDynam
   )
   {
     this.defaultQueryConfig = defaultQueryConfig;
-    this.resolvedDefaultQueryContext = 
QueryContext.of(defaultQueryConfig.getContext());
+    this.querySnapshot = new 
QueryConfigSnapshot(QueryContext.of(defaultQueryConfig.getContext()).asMap(), 
null);
     this.coordinatorClient =
         new CoordinatorClientImpl(
             clientFactory.makeClient(
@@ -89,7 +92,7 @@ public class BrokerViewOfBrokerConfig extends 
BaseBrokerViewOfConfig<BrokerDynam
   {
     this.coordinatorClient = coordinatorClient;
     this.defaultQueryConfig = defaultQueryConfig;
-    this.resolvedDefaultQueryContext = 
QueryContext.of(defaultQueryConfig.getContext());
+    this.querySnapshot = new 
QueryConfigSnapshot(QueryContext.of(defaultQueryConfig.getContext()).asMap(), 
null);
   }
 
   @Override
@@ -109,13 +112,16 @@ public class BrokerViewOfBrokerConfig extends 
BaseBrokerViewOfConfig<BrokerDynam
    * resolved default query context by merging static defaults with dynamic 
overrides.
    */
   @Override
-  public synchronized void setDynamicConfig(@NotNull BrokerDynamicConfig 
updatedConfig)
+  public void setDynamicConfig(@NotNull BrokerDynamicConfig updatedConfig)
   {
     super.setDynamicConfig(updatedConfig);
-    resolvedDefaultQueryContext = QueryContext.of(QueryContexts.override(
-        defaultQueryConfig.getContext(),
-        updatedConfig.getQueryContext().asMap()
-    ));
+    querySnapshot = new QueryConfigSnapshot(
+        QueryContext.of(QueryContexts.override(
+            defaultQueryConfig.getContext(),
+            updatedConfig.getQueryContext().asMap()
+        )).asMap(),
+        updatedConfig
+    );
   }
 
   /**
@@ -125,6 +131,23 @@ public class BrokerViewOfBrokerConfig extends 
BaseBrokerViewOfConfig<BrokerDynam
   @Override
   public Map<String, Object> getContext()
   {
-    return resolvedDefaultQueryContext.asMap();
+    return querySnapshot.getResolvedDefaultQueryContext();
+  }
+
+  /**
+   * Snapshot for a single query to resolve its context and blocklist against, 
instead of re-reading the live config.
+   */
+  public QueryConfigSnapshot snapshotForQuery()
+  {
+    return querySnapshot;
+  }
+
+  /**
+   * Reads through {@link #querySnapshot} so this and {@link 
#snapshotForQuery()} always agree.
+   */
+  @Override
+  public BrokerDynamicConfig getDynamicConfig()
+  {
+    return querySnapshot.getDynamicConfig();
   }
 }
diff --git a/server/src/main/java/org/apache/druid/server/QueryLifecycle.java 
b/server/src/main/java/org/apache/druid/server/QueryLifecycle.java
index fc7d896df83..0f970a7fe0c 100644
--- a/server/src/main/java/org/apache/druid/server/QueryLifecycle.java
+++ b/server/src/main/java/org/apache/druid/server/QueryLifecycle.java
@@ -37,9 +37,7 @@ import org.apache.druid.query.BaseQuery;
 import org.apache.druid.query.DruidMetrics;
 import org.apache.druid.query.GenericQueryMetricsFactory;
 import org.apache.druid.query.Query;
-import org.apache.druid.query.QueryConfigProvider;
 import org.apache.druid.query.QueryContext;
-import org.apache.druid.query.QueryContexts;
 import org.apache.druid.query.QueryInterruptedException;
 import org.apache.druid.query.QueryMetrics;
 import org.apache.druid.query.QueryPlus;
@@ -49,7 +47,7 @@ import org.apache.druid.query.QueryTimeoutException;
 import org.apache.druid.query.QueryToolChest;
 import org.apache.druid.query.context.ResponseContext;
 import org.apache.druid.query.policy.PolicyEnforcer;
-import org.apache.druid.server.broker.PerSegmentTimeoutConfig;
+import org.apache.druid.server.broker.QueryConfigSnapshot;
 import org.apache.druid.server.log.RequestLogger;
 import org.apache.druid.server.security.Action;
 import org.apache.druid.server.security.AuthConfig;
@@ -64,7 +62,6 @@ import 
org.checkerframework.checker.nullness.qual.MonotonicNonNull;
 
 import javax.annotation.Nullable;
 import javax.servlet.http.HttpServletRequest;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -99,11 +96,9 @@ public class QueryLifecycle
   private final ServiceEmitter emitter;
   private final RequestLogger requestLogger;
   private final AuthorizerMapper authorizerMapper;
-  private final QueryConfigProvider queryConfigProvider;
   private final AuthConfig authConfig;
   private final PolicyEnforcer policyEnforcer;
-  private final List<QueryBlocklistRule> queryBlocklist;
-  private final Map<String, PerSegmentTimeoutConfig> perSegmentTimeoutConfig;
+  private final QueryConfigSnapshot configSnapshot;
   private final long startMs;
   private final long startNs;
 
@@ -113,8 +108,11 @@ public class QueryLifecycle
 
   @MonotonicNonNull
   private Query<?> baseQuery;
+  /**
+   * Context keys as received, i.e. the candidate set for {@link 
AuthConfig#contextKeysToAuthorize}.
+   */
   @MonotonicNonNull
-  private Set<String> userContextKeys;
+  private Set<String> queryContextKeysToAuthorize;
 
   public QueryLifecycle(
       final QueryRunnerFactoryConglomerate conglomerate,
@@ -123,11 +121,9 @@ public class QueryLifecycle
       final ServiceEmitter emitter,
       final RequestLogger requestLogger,
       final AuthorizerMapper authorizerMapper,
-      final QueryConfigProvider queryConfigProvider,
       final AuthConfig authConfig,
       final PolicyEnforcer policyEnforcer,
-      final List<QueryBlocklistRule> queryBlocklist,
-      final Map<String, PerSegmentTimeoutConfig> perSegmentTimeoutConfig,
+      final QueryConfigSnapshot configSnapshot,
       final long startMs,
       final long startNs
   )
@@ -138,11 +134,9 @@ public class QueryLifecycle
     this.emitter = emitter;
     this.requestLogger = requestLogger;
     this.authorizerMapper = authorizerMapper;
-    this.queryConfigProvider = queryConfigProvider;
     this.authConfig = authConfig;
     this.policyEnforcer = policyEnforcer;
-    this.queryBlocklist = queryBlocklist;
-    this.perSegmentTimeoutConfig = perSegmentTimeoutConfig;
+    this.configSnapshot = configSnapshot;
     this.startMs = startMs;
     this.startNs = startNs;
   }
@@ -166,7 +160,21 @@ public class QueryLifecycle
       final AuthorizationResult authorizationResult
   )
   {
-    initialize(query);
+    return runSimple(query, authenticationResult, authorizationResult, null);
+  }
+
+  /**
+   * As {@link #runSimple(Query, AuthenticationResult, AuthorizationResult)}, 
but takes the context keys the client
+   * actually set. See {@link #initialize(Query, Set)}.
+   */
+  public <T> QueryResponse<T> runSimple(
+      final Query<T> query,
+      final AuthenticationResult authenticationResult,
+      final AuthorizationResult authorizationResult,
+      @Nullable final Set<String> clientProvidedQueryContextKeys
+  )
+  {
+    initialize(query, clientProvidedQueryContextKeys);
 
     final Sequence<T> results;
 
@@ -212,61 +220,40 @@ public class QueryLifecycle
    * @throws DruidException if the current state is not NEW, which indicates a 
bug
    */
   public void initialize(final Query<?> baseQuery)
+  {
+    initialize(baseQuery, null);
+  }
+
+  /**
+   * As {@link #initialize(Query)}, but takes the context keys the client 
actually set. Pass {@code null} to treat the
+   * whole context as client-set. The SQL layer must pass the real keys, since 
it merges defaults into the context
+   * and those should still be overridable by dynamic config.
+   *
+   * @throws DruidException if the current state is not NEW, which indicates a 
bug
+   */
+  public void initialize(final Query<?> baseQuery, @Nullable final Set<String> 
clientProvidedQueryContextKeys)
   {
     transition(State.NEW, State.INITIALIZED);
 
-    userContextKeys = new HashSet<>(baseQuery.getContext().keySet());
+    final Map<String, Object> baseContext = baseQuery.getContext();
+    queryContextKeysToAuthorize = new HashSet<>(baseContext.keySet());
+
+    final Set<String> effectiveClientProvidedQueryContextKeys =
+        clientProvidedQueryContextKeys != null ? 
clientProvidedQueryContextKeys : baseContext.keySet();
+
     String queryId = baseQuery.getId();
     if (Strings.isNullOrEmpty(queryId)) {
       queryId = UUID.randomUUID().toString();
     }
 
-    // Start with system defaults, apply per-datasource override, then user 
context wins
-    Map<String, Object> contextWithDefaults = new 
HashMap<>(queryConfigProvider.getContext());
-    applyPerDatasourcePerSegmentTimeout(baseQuery, contextWithDefaults, 
queryId);
-    Map<String, Object> finalContext = 
QueryContexts.override(contextWithDefaults, baseQuery.getContext());
+    final Map<String, Object> finalContext =
+        configSnapshot.resolveContext(baseQuery, 
effectiveClientProvidedQueryContextKeys);
     finalContext.put(BaseQuery.QUERY_ID, queryId);
 
     this.baseQuery = baseQuery.withOverriddenContext(finalContext);
     this.toolChest = conglomerate.getToolChest(this.baseQuery);
   }
 
-  /**
-   * If a per-datasource per-segment timeout is configured, injects it into 
the context defaults.
-   * User context (applied later via {@link QueryContexts#override}) will 
override this if set explicitly.
-   * In monitorOnly mode, logs the configured timeout but does not inject it.
-   *
-   * For queries involving multiple datasources (e.g., joins or unions), the 
timeout from the first matching datasource is applied
-   * since getTableNames() returns a Set, the match order is non-deterministic.
-   */
-  private void applyPerDatasourcePerSegmentTimeout(
-      final Query<?> query,
-      final Map<String, Object> contextWithDefaults,
-      final String queryId
-  )
-  {
-    if (perSegmentTimeoutConfig.isEmpty()) {
-      return;
-    }
-
-    for (String tableName : query.getDataSource().getTableNames()) {
-      PerSegmentTimeoutConfig dsConfig = 
perSegmentTimeoutConfig.get(tableName);
-      if (dsConfig != null) {
-        if (dsConfig.isMonitorOnly()) {
-          log.debug(
-              "Per-segment timeout [%d ms] configured for datasource [%s] in 
monitorOnly mode (not enforced) for query [%s].",
-              dsConfig.getPerSegmentTimeoutMs(),
-              tableName,
-              queryId
-          );
-        } else {
-          contextWithDefaults.put(QueryContexts.PER_SEGMENT_TIMEOUT_KEY, 
dsConfig.getPerSegmentTimeoutMs());
-        }
-        return;
-      }
-    }
-  }
-
   /**
    * Returns {@link AuthorizationResult} based on {@code 
DRUID_AUTHENTICATION_RESULT} in the given request, base query
    * would be transformed with restrictions on the AuthorizationResult.
@@ -290,7 +277,7 @@ public class QueryLifecycle
             AuthorizationUtils.DATASOURCE_READ_RA_GENERATOR
         ),
         Iterables.transform(
-            authConfig.contextKeysToAuthorize(userContextKeys),
+            authConfig.contextKeysToAuthorize(queryContextKeysToAuthorize),
             contextParam -> new ResourceAction(new Resource(contextParam, 
ResourceType.QUERY_CONTEXT), Action.WRITE)
         )
     );
@@ -328,7 +315,7 @@ public class QueryLifecycle
             AuthorizationUtils.DATASOURCE_READ_RA_GENERATOR
         ),
         Iterables.transform(
-            authConfig.contextKeysToAuthorize(userContextKeys),
+            authConfig.contextKeysToAuthorize(queryContextKeysToAuthorize),
             contextParam -> new ResourceAction(new Resource(contextParam, 
ResourceType.QUERY_CONTEXT), Action.WRITE)
         )
     );
@@ -364,7 +351,9 @@ public class QueryLifecycle
    */
   private void checkQueryBlocklist()
   {
-    if (queryBlocklist == null || queryBlocklist.isEmpty()) {
+    final List<QueryBlocklistRule> queryBlocklist = 
configSnapshot.getQueryBlocklist();
+
+    if (queryBlocklist.isEmpty()) {
       return;
     }
 
diff --git 
a/server/src/main/java/org/apache/druid/server/QueryLifecycleFactory.java 
b/server/src/main/java/org/apache/druid/server/QueryLifecycleFactory.java
index 57fcd1b47e9..d22d24ed3ef 100644
--- a/server/src/main/java/org/apache/druid/server/QueryLifecycleFactory.java
+++ b/server/src/main/java/org/apache/druid/server/QueryLifecycleFactory.java
@@ -28,15 +28,12 @@ import org.apache.druid.query.QueryConfigProvider;
 import org.apache.druid.query.QueryRunnerFactoryConglomerate;
 import org.apache.druid.query.QuerySegmentWalker;
 import org.apache.druid.query.policy.PolicyEnforcer;
-import org.apache.druid.server.broker.PerSegmentTimeoutConfig;
+import org.apache.druid.server.broker.QueryConfigSnapshot;
 import org.apache.druid.server.log.RequestLogger;
 import org.apache.druid.server.security.AuthConfig;
 import org.apache.druid.server.security.AuthorizerMapper;
 
 import javax.annotation.Nullable;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
 
 @LazySingleton
 public class QueryLifecycleFactory
@@ -80,15 +77,11 @@ public class QueryLifecycleFactory
 
   public QueryLifecycle factorize()
   {
-    final List<QueryBlocklistRule> queryBlocklist;
-    final Map<String, PerSegmentTimeoutConfig> perSegmentTimeoutConfig;
-    if (brokerViewOfBrokerConfig != null && 
brokerViewOfBrokerConfig.getDynamicConfig() != null) {
-      queryBlocklist = 
brokerViewOfBrokerConfig.getDynamicConfig().getQueryBlocklist();
-      perSegmentTimeoutConfig = 
brokerViewOfBrokerConfig.getDynamicConfig().getPerSegmentTimeoutConfig();
-    } else {
-      queryBlocklist = Collections.emptyList();
-      perSegmentTimeoutConfig = Collections.emptyMap();
-    }
+    // Read once per query so the whole lifecycle sees one config, even if it 
is swapped mid-query.
+    final QueryConfigSnapshot configSnapshot =
+        brokerViewOfBrokerConfig == null
+        ? new QueryConfigSnapshot(queryConfigProvider.getContext(), null)
+        : brokerViewOfBrokerConfig.snapshotForQuery();
 
     return new QueryLifecycle(
         conglomerate,
@@ -97,11 +90,9 @@ public class QueryLifecycleFactory
         emitter,
         requestLogger,
         authorizerMapper,
-        queryConfigProvider,
         authConfig,
         policyEnforcer,
-        queryBlocklist,
-        perSegmentTimeoutConfig,
+        configSnapshot,
         System.currentTimeMillis(),
         System.nanoTime()
     );
diff --git 
a/server/src/main/java/org/apache/druid/server/broker/BrokerDynamicConfig.java 
b/server/src/main/java/org/apache/druid/server/broker/BrokerDynamicConfig.java
index 57200e729d3..37634048de2 100644
--- 
a/server/src/main/java/org/apache/druid/server/broker/BrokerDynamicConfig.java
+++ 
b/server/src/main/java/org/apache/druid/server/broker/BrokerDynamicConfig.java
@@ -22,7 +22,10 @@ package org.apache.druid.server.broker;
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import org.apache.druid.common.config.Configs;
+import org.apache.druid.java.util.common.logger.Logger;
+import org.apache.druid.query.Query;
 import org.apache.druid.query.QueryContext;
+import org.apache.druid.query.QueryContexts;
 import org.apache.druid.server.QueryBlocklistRule;
 
 import javax.annotation.Nullable;
@@ -40,6 +43,8 @@ import java.util.Objects;
  */
 public class BrokerDynamicConfig
 {
+  private static final Logger log = new Logger(BrokerDynamicConfig.class);
+
   public static final String CONFIG_KEY = "broker.config";
 
   /**
@@ -91,6 +96,37 @@ public class BrokerDynamicConfig
     return perSegmentTimeoutConfig;
   }
 
+  /**
+   * Query context overrides (e.g. per-segment timeout) for the datasources 
the query targets. With multiple
+   * datasources the first match wins, in non-deterministic order.
+   */
+  public QueryContext getContextOverridesForQuery(Query<?> query)
+  {
+    if (perSegmentTimeoutConfig.isEmpty()) {
+      return QueryContext.empty();
+    }
+
+    for (String tableName : query.getDataSource().getTableNames()) {
+      PerSegmentTimeoutConfig dataSourceTimeoutConfig = 
perSegmentTimeoutConfig.get(tableName);
+      if (dataSourceTimeoutConfig != null) {
+        if (dataSourceTimeoutConfig.isMonitorOnly()) {
+          // monitorOnly is documented as "logged but not enforced", so this 
log is its only effect.
+          log.debug(
+              "Per-segment timeout[%d ms] configured for datasource[%s] in 
monitorOnly mode (not enforced) for query[%s].",
+              dataSourceTimeoutConfig.getPerSegmentTimeoutMs(),
+              tableName,
+              query.getId()
+          );
+          return QueryContext.empty();
+        }
+        return QueryContext.of(
+            Map.of(QueryContexts.PER_SEGMENT_TIMEOUT_KEY, 
dataSourceTimeoutConfig.getPerSegmentTimeoutMs())
+        );
+      }
+    }
+    return QueryContext.empty();
+  }
+
   @Override
   public boolean equals(Object o)
   {
diff --git 
a/server/src/main/java/org/apache/druid/server/broker/QueryConfigSnapshot.java 
b/server/src/main/java/org/apache/druid/server/broker/QueryConfigSnapshot.java
new file mode 100644
index 00000000000..882a64543f6
--- /dev/null
+++ 
b/server/src/main/java/org/apache/druid/server/broker/QueryConfigSnapshot.java
@@ -0,0 +1,91 @@
+/*
+ * 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.druid.server.broker;
+
+import org.apache.druid.query.Query;
+import org.apache.druid.query.QueryContexts;
+import org.apache.druid.server.QueryBlocklistRule;
+
+import javax.annotation.Nullable;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A snapshot of the {@link BrokerDynamicConfig} (null on non-Broker nodes)
+ * and the resolved default query context that is used for the entire {@code 
QueryLifecycle}
+ * of a single query.
+ */
+public class QueryConfigSnapshot
+{
+  /** Already resolved against {@link BrokerDynamicConfig#getQueryContext()}. 
*/
+  private final Map<String, Object> resolvedDefaultQueryContext;
+  @Nullable
+  private final BrokerDynamicConfig dynamicConfig;
+
+  public QueryConfigSnapshot(
+      Map<String, Object> resolvedDefaultQueryContext,
+      @Nullable BrokerDynamicConfig dynamicConfig
+  )
+  {
+    this.resolvedDefaultQueryContext = resolvedDefaultQueryContext;
+    this.dynamicConfig = dynamicConfig;
+  }
+
+  public Map<String, Object> getResolvedDefaultQueryContext()
+  {
+    return resolvedDefaultQueryContext;
+  }
+
+  @Nullable
+  public BrokerDynamicConfig getDynamicConfig()
+  {
+    return dynamicConfig;
+  }
+
+  /**
+   * The final query context for the given query. Precedence, highest to 
lowest:
+   * <ol>
+   *   <li>Keys the client set on the query payload</li>
+   *   <li>Per-query overrides from {@link 
BrokerDynamicConfig#getContextOverridesForQuery}</li>
+   *   <li>Remaining keys on the query context (defaults merged in by the SQL 
layer)</li>
+   *   <li>{@link #resolvedDefaultQueryContext}, i.e. runtime properties 
overridden by
+   *       {@link BrokerDynamicConfig#getQueryContext()}</li>
+   * </ol>
+   */
+  public Map<String, Object> resolveContext(Query<?> query, Set<String> 
clientProvidedQueryContextKeys)
+  {
+    final Map<String, Object> result = 
QueryContexts.override(resolvedDefaultQueryContext, query.getContext());
+    if (dynamicConfig != null) {
+      for (Map.Entry<String, Object> override : 
dynamicConfig.getContextOverridesForQuery(query).asMap().entrySet()) {
+        if (!clientProvidedQueryContextKeys.contains(override.getKey())) {
+          result.put(override.getKey(), override.getValue());
+        }
+      }
+    }
+    return result;
+  }
+
+  public List<QueryBlocklistRule> getQueryBlocklist()
+  {
+    return dynamicConfig == null ? Collections.emptyList() : 
dynamicConfig.getQueryBlocklist();
+  }
+}
diff --git 
a/server/src/test/java/org/apache/druid/client/BrokerViewOfBrokerConfigTest.java
 
b/server/src/test/java/org/apache/druid/client/BrokerViewOfBrokerConfigTest.java
index c4877220ffc..54318f75590 100644
--- 
a/server/src/test/java/org/apache/druid/client/BrokerViewOfBrokerConfigTest.java
+++ 
b/server/src/test/java/org/apache/druid/client/BrokerViewOfBrokerConfigTest.java
@@ -19,12 +19,16 @@
 
 package org.apache.druid.client;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.util.concurrent.Futures;
 import org.apache.druid.client.coordinator.CoordinatorClient;
 import org.apache.druid.query.DefaultQueryConfig;
 import org.apache.druid.query.QueryContext;
+import org.apache.druid.server.DefaultQueryBlocklistRule;
 import org.apache.druid.server.broker.BrokerDynamicConfig;
+import org.apache.druid.server.broker.QueryConfigSnapshot;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
@@ -82,4 +86,52 @@ public class BrokerViewOfBrokerConfigTest
     target.setDynamicConfig(BrokerDynamicConfig.builder().build());
     Assert.assertEquals(defaultQueryConfig.getContext(), target.getContext());
   }
+
+  @Test
+  public void testSnapshotBeforeFirstSyncHasStaticDefaultsAndNoDynamicConfig()
+  {
+    final QueryConfigSnapshot snapshot = target.snapshotForQuery();
+    Assert.assertEquals(defaultQueryConfig.getContext(), 
snapshot.getResolvedDefaultQueryContext());
+    Assert.assertTrue(snapshot.getQueryBlocklist().isEmpty());
+  }
+
+  @Test
+  public void testSnapshotAndGetContextStayInSync()
+  {
+    final BrokerDynamicConfig dynamicConfig =
+        BrokerDynamicConfig.builder()
+                           
.withQueryContext(QueryContext.of(ImmutableMap.of("priority", 5)))
+                           .withQueryBlocklist(ImmutableList.of(
+                               new DefaultQueryBlocklistRule("block-ds", 
ImmutableSet.of("ds"), null, null)
+                           ))
+                           .build();
+    target.setDynamicConfig(dynamicConfig);
+
+    final QueryConfigSnapshot snapshot = target.snapshotForQuery();
+    Assert.assertSame(target.getContext(), 
snapshot.getResolvedDefaultQueryContext());
+    Assert.assertEquals(dynamicConfig.getQueryBlocklist(), 
snapshot.getQueryBlocklist());
+    Assert.assertEquals(5, 
snapshot.getResolvedDefaultQueryContext().get("priority"));
+    Assert.assertSame(snapshot.getDynamicConfig(), target.getDynamicConfig());
+  }
+
+  @Test
+  public void testSnapshotIsUnaffectedByLaterConfigUpdate()
+  {
+    // A query holds its snapshot for the whole lifecycle, so a later swap 
must not change it.
+    target.setDynamicConfig(
+        BrokerDynamicConfig.builder()
+                           
.withQueryContext(QueryContext.of(ImmutableMap.of("priority", 5)))
+                           .build()
+    );
+    final QueryConfigSnapshot snapshot = target.snapshotForQuery();
+
+    target.setDynamicConfig(
+        BrokerDynamicConfig.builder()
+                           
.withQueryContext(QueryContext.of(ImmutableMap.of("priority", 9)))
+                           .build()
+    );
+
+    Assert.assertEquals(5, 
snapshot.getResolvedDefaultQueryContext().get("priority"));
+    Assert.assertEquals(9, target.getContext().get("priority"));
+  }
 }
diff --git 
a/server/src/test/java/org/apache/druid/server/PerSegmentTimeoutInjectionTest.java
 
b/server/src/test/java/org/apache/druid/server/PerSegmentTimeoutInjectionTest.java
index 440f065ba48..8b3aeec828b 100644
--- 
a/server/src/test/java/org/apache/druid/server/PerSegmentTimeoutInjectionTest.java
+++ 
b/server/src/test/java/org/apache/druid/server/PerSegmentTimeoutInjectionTest.java
@@ -23,7 +23,6 @@ import org.apache.druid.java.util.common.Intervals;
 import org.apache.druid.java.util.emitter.service.ServiceEmitter;
 import org.apache.druid.query.Druids;
 import org.apache.druid.query.GenericQueryMetricsFactory;
-import org.apache.druid.query.QueryConfigProvider;
 import org.apache.druid.query.QueryContexts;
 import org.apache.druid.query.QueryRunnerFactoryConglomerate;
 import org.apache.druid.query.QuerySegmentWalker;
@@ -31,7 +30,9 @@ import org.apache.druid.query.QueryToolChest;
 import org.apache.druid.query.aggregation.CountAggregatorFactory;
 import org.apache.druid.query.policy.NoopPolicyEnforcer;
 import org.apache.druid.query.timeseries.TimeseriesQuery;
+import org.apache.druid.server.broker.BrokerDynamicConfig;
 import org.apache.druid.server.broker.PerSegmentTimeoutConfig;
+import org.apache.druid.server.broker.QueryConfigSnapshot;
 import org.apache.druid.server.log.RequestLogger;
 import org.apache.druid.server.security.AuthConfig;
 import org.apache.druid.server.security.AuthorizerMapper;
@@ -44,10 +45,17 @@ import org.junit.Test;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
+/**
+ * Precedence of the per-query dynamic overrides at the {@link QueryLifecycle} 
level: an override beats a
+ * non-client-provided value in the context, but a value the client set wins. 
The datasource-matching logic itself is
+ * tested in {@code org.apache.druid.server.broker.BrokerDynamicConfigTest}.
+ */
 public class PerSegmentTimeoutInjectionTest
 {
   private static final String DATASOURCE = "my_datasource";
+  private static final String KEY = QueryContexts.PER_SEGMENT_TIMEOUT_KEY;
 
   private QueryRunnerFactoryConglomerate conglomerate;
   private QuerySegmentWalker texasRanger;
@@ -55,7 +63,6 @@ public class PerSegmentTimeoutInjectionTest
   private ServiceEmitter emitter;
   private RequestLogger requestLogger;
   private AuthorizerMapper authzMapper;
-  private QueryConfigProvider queryConfig;
   private QueryToolChest toolChest;
 
   private final TimeseriesQuery baseQuery = Druids.newTimeseriesQueryBuilder()
@@ -68,142 +75,111 @@ public class PerSegmentTimeoutInjectionTest
   public void setUp()
   {
     conglomerate = EasyMock.createMock(QueryRunnerFactoryConglomerate.class);
-    texasRanger = EasyMock.createMock(QuerySegmentWalker.class);
-    metricsFactory = EasyMock.createMock(GenericQueryMetricsFactory.class);
-    emitter = EasyMock.createMock(ServiceEmitter.class);
+    texasRanger = EasyMock.createNiceMock(QuerySegmentWalker.class);
+    metricsFactory = EasyMock.createNiceMock(GenericQueryMetricsFactory.class);
+    emitter = EasyMock.createNiceMock(ServiceEmitter.class);
     requestLogger = EasyMock.createNiceMock(RequestLogger.class);
     authzMapper = EasyMock.createNiceMock(AuthorizerMapper.class);
-    queryConfig = EasyMock.createMock(QueryConfigProvider.class);
     toolChest = EasyMock.createNiceMock(QueryToolChest.class);
   }
 
   @After
   public void tearDown()
   {
-    EasyMock.verify(conglomerate, queryConfig);
+    EasyMock.verify(conglomerate);
   }
 
   @Test
-  public void testPerDatasourceTimeout_applied()
+  public void testDynamicOverrideAppliedWhenClientDidNotSet()
   {
-    Map<String, PerSegmentTimeoutConfig> config = Map.of(
-        DATASOURCE, new PerSegmentTimeoutConfig(5000, false)
-    );
-
-    expectDefaults();
-
-    QueryLifecycle lifecycle = createLifecycle(config);
+    QueryLifecycle lifecycle = createLifecycle(perSegmentTimeout(5000));
     lifecycle.initialize(baseQuery);
 
     Assert.assertEquals(5000L, 
lifecycle.getQuery().context().getPerSegmentTimeout());
   }
 
   @Test
-  public void testPerDatasourceTimeout_userOverrideWins()
+  public void testDynamicOverridesNonClientValueInContext()
   {
-    Map<String, PerSegmentTimeoutConfig> config = Map.of(
-        DATASOURCE, new PerSegmentTimeoutConfig(5000, false)
-    );
+    // SQL path: a default was merged into the context but the client did not 
set it, so the dynamic override wins.
+    TimeseriesQuery query = baseQuery.withOverriddenContext(Map.of(KEY, "0"));
 
-    expectDefaults();
+    QueryLifecycle lifecycle = createLifecycle(perSegmentTimeout(5000));
+    lifecycle.initialize(query, Collections.emptySet());
 
-    TimeseriesQuery queryWithUserTimeout = baseQuery.withOverriddenContext(
-        Map.of(QueryContexts.PER_SEGMENT_TIMEOUT_KEY, 2000L)
-    );
+    Assert.assertEquals(5000L, 
lifecycle.getQuery().context().getPerSegmentTimeout());
+  }
 
-    QueryLifecycle lifecycle = createLifecycle(config);
-    lifecycle.initialize(queryWithUserTimeout);
+  @Test
+  public void testClientProvidedValueWins()
+  {
+    TimeseriesQuery query = baseQuery.withOverriddenContext(Map.of(KEY, 
2000L));
+
+    QueryLifecycle lifecycle = createLifecycle(perSegmentTimeout(5000));
+    lifecycle.initialize(query, Set.of(KEY));
 
     Assert.assertEquals(2000L, 
lifecycle.getQuery().context().getPerSegmentTimeout());
   }
 
   @Test
-  public void testPerDatasourceTimeout_monitorOnlyDoesNotInject()
+  public void testDynamicOverrideBeatsStaticDefault()
   {
-    // monitorOnly=true: config exists but should NOT be enforced
-    Map<String, PerSegmentTimeoutConfig> config = Map.of(
-        DATASOURCE, new PerSegmentTimeoutConfig(5000, true)
-    );
-
-    expectDefaults();
-
-    QueryLifecycle lifecycle = createLifecycle(config);
+    // The bug being fixed: the static default used to shadow the 
per-datasource dynamic config.
+    QueryLifecycle lifecycle = createLifecycle(Map.of(KEY, 100L), 
perSegmentTimeout(5000));
     lifecycle.initialize(baseQuery);
 
-    Assert.assertFalse(
-        "monitorOnly should not inject perSegmentTimeout",
-        lifecycle.getQuery().context().usePerSegmentTimeout()
-    );
+    Assert.assertEquals(5000L, 
lifecycle.getQuery().context().getPerSegmentTimeout());
   }
 
   @Test
-  public void testPerDatasourceTimeout_noMatchingDatasource()
+  public void testStaticDefaultUsedWhenNoDatasourceOverride()
   {
-    Map<String, PerSegmentTimeoutConfig> config = Map.of(
-        "other_datasource", new PerSegmentTimeoutConfig(5000, false)
-    );
-
-    expectDefaults();
-
-    QueryLifecycle lifecycle = createLifecycle(config);
+    QueryLifecycle lifecycle = createLifecycle(Map.of(KEY, 100L), 
BrokerDynamicConfig.builder().build());
     lifecycle.initialize(baseQuery);
 
-    Assert.assertFalse(lifecycle.getQuery().context().usePerSegmentTimeout());
+    Assert.assertEquals(100L, 
lifecycle.getQuery().context().getPerSegmentTimeout());
   }
 
   @Test
-  public void testPerDatasourceTimeout_overridesSystemDefault()
+  public void testMonitorOnlyIsNotEnforced()
   {
-    Map<String, PerSegmentTimeoutConfig> config = Map.of(
-        DATASOURCE, new PerSegmentTimeoutConfig(5000, false)
-    );
+    BrokerDynamicConfig dynamicConfig =
+        BrokerDynamicConfig.builder()
+                           .withPerSegmentTimeoutConfig(Map.of(DATASOURCE, new 
PerSegmentTimeoutConfig(5000L, true)))
+                           .build();
 
-    // System default sets perSegmentTimeout to 10000
-    EasyMock.expect(queryConfig.getContext())
-            .andReturn(Map.of(QueryContexts.PER_SEGMENT_TIMEOUT_KEY, 10000L))
-            .anyTimes();
-    
EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())).andReturn(toolChest).once();
-    EasyMock.replay(conglomerate, queryConfig);
-
-    QueryLifecycle lifecycle = createLifecycle(config);
+    QueryLifecycle lifecycle = createLifecycle(dynamicConfig);
     lifecycle.initialize(baseQuery);
 
-    Assert.assertEquals(5000L, 
lifecycle.getQuery().context().getPerSegmentTimeout());
+    Assert.assertFalse(lifecycle.getQuery().context().usePerSegmentTimeout());
   }
 
   @Test
-  public void testPrecedence_userOverridesPerDatasourceOverridesSystemDefault()
+  public void testNoDynamicConfigMeansNoInjection()
   {
-    // System default: 10000, per-datasource: 5000, user: 2000 — user should 
win
-    Map<String, PerSegmentTimeoutConfig> config = Map.of(
-        DATASOURCE, new PerSegmentTimeoutConfig(5000, false)
-    );
-
-    EasyMock.expect(queryConfig.getContext())
-            .andReturn(Map.of(QueryContexts.PER_SEGMENT_TIMEOUT_KEY, 10000L))
-            .anyTimes();
-    
EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())).andReturn(toolChest).once();
-    EasyMock.replay(conglomerate, queryConfig);
-
-    TimeseriesQuery queryWithUserTimeout = baseQuery.withOverriddenContext(
-        Map.of(QueryContexts.PER_SEGMENT_TIMEOUT_KEY, 2000L)
-    );
+    QueryLifecycle lifecycle = createLifecycle(null);
+    lifecycle.initialize(baseQuery);
 
-    QueryLifecycle lifecycle = createLifecycle(config);
-    lifecycle.initialize(queryWithUserTimeout);
+    Assert.assertFalse(lifecycle.getQuery().context().usePerSegmentTimeout());
+  }
 
-    Assert.assertEquals(2000L, 
lifecycle.getQuery().context().getPerSegmentTimeout());
+  private static BrokerDynamicConfig perSegmentTimeout(long timeoutMs)
+  {
+    return BrokerDynamicConfig.builder()
+                              .withPerSegmentTimeoutConfig(Map.of(DATASOURCE, 
new PerSegmentTimeoutConfig(timeoutMs, false)))
+                              .build();
   }
 
-  private void expectDefaults()
+  private QueryLifecycle createLifecycle(BrokerDynamicConfig dynamicConfig)
   {
-    EasyMock.expect(queryConfig.getContext()).andReturn(Map.of()).anyTimes();
-    
EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())).andReturn(toolChest).once();
-    EasyMock.replay(conglomerate, queryConfig);
+    return createLifecycle(Collections.emptyMap(), dynamicConfig);
   }
 
-  private QueryLifecycle createLifecycle(Map<String, PerSegmentTimeoutConfig> 
perSegmentTimeoutConfig)
+  private QueryLifecycle createLifecycle(Map<String, Object> 
resolvedDefaultQueryContext, BrokerDynamicConfig dynamicConfig)
   {
+    
EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())).andReturn(toolChest).once();
+    EasyMock.replay(conglomerate);
+
     return new QueryLifecycle(
         conglomerate,
         texasRanger,
@@ -211,11 +187,9 @@ public class PerSegmentTimeoutInjectionTest
         emitter,
         requestLogger,
         authzMapper,
-        queryConfig,
         new AuthConfig(),
         NoopPolicyEnforcer.instance(),
-        Collections.emptyList(),
-        perSegmentTimeoutConfig,
+        new QueryConfigSnapshot(resolvedDefaultQueryContext, dynamicConfig),
         System.currentTimeMillis(),
         System.nanoTime()
     );
diff --git 
a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java 
b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java
index 5f8da0d21a3..3691f80ce27 100644
--- a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java
+++ b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java
@@ -59,6 +59,8 @@ import org.apache.druid.query.policy.PolicyEnforcer;
 import org.apache.druid.query.policy.RestrictAllTablesPolicyEnforcer;
 import org.apache.druid.query.policy.RowFilterPolicy;
 import org.apache.druid.query.timeseries.TimeseriesQuery;
+import org.apache.druid.server.broker.BrokerDynamicConfig;
+import org.apache.druid.server.broker.QueryConfigSnapshot;
 import org.apache.druid.server.log.RequestLogger;
 import org.apache.druid.server.security.Access;
 import org.apache.druid.server.security.Action;
@@ -80,9 +82,7 @@ import org.junit.rules.ExpectedException;
 
 import javax.annotation.Nullable;
 import javax.servlet.http.HttpServletRequest;
-import java.util.Collections;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 
@@ -848,7 +848,6 @@ public class QueryLifecycleTest
     replayAll();
 
     // Create lifecycle with blocklist
-    List<QueryBlocklistRule> queryBlocklist = ImmutableList.of(rule);
     QueryLifecycle lifecycle = new QueryLifecycle(
         conglomerate,
         texasRanger,
@@ -856,11 +855,12 @@ public class QueryLifecycleTest
         emitter,
         requestLogger,
         authzMapper,
-        queryConfig,
         authConfig,
         policyEnforcer,
-        queryBlocklist,
-        Collections.emptyMap(),
+        new QueryConfigSnapshot(
+            ImmutableMap.of(),
+            
BrokerDynamicConfig.builder().withQueryBlocklist(ImmutableList.of(rule)).build()
+        ),
         System.currentTimeMillis(),
         System.nanoTime()
     );
@@ -900,7 +900,6 @@ public class QueryLifecycleTest
     replayAll();
 
     // Create lifecycle with blocklist
-    List<QueryBlocklistRule> queryBlocklist = ImmutableList.of(rule);
     QueryLifecycle lifecycle = new QueryLifecycle(
         conglomerate,
         texasRanger,
@@ -908,11 +907,12 @@ public class QueryLifecycleTest
         emitter,
         requestLogger,
         authzMapper,
-        queryConfig,
         authConfig,
         policyEnforcer,
-        queryBlocklist,
-        Collections.emptyMap(),
+        new QueryConfigSnapshot(
+            ImmutableMap.of(),
+            
BrokerDynamicConfig.builder().withQueryBlocklist(ImmutableList.of(rule)).build()
+        ),
         System.currentTimeMillis(),
         System.nanoTime()
     );
diff --git 
a/server/src/test/java/org/apache/druid/server/QueryResourceTest.java 
b/server/src/test/java/org/apache/druid/server/QueryResourceTest.java
index 7922588cf77..e01e6fe073a 100644
--- a/server/src/test/java/org/apache/druid/server/QueryResourceTest.java
+++ b/server/src/test/java/org/apache/druid/server/QueryResourceTest.java
@@ -79,6 +79,7 @@ import org.apache.druid.query.policy.NoopPolicyEnforcer;
 import org.apache.druid.query.policy.RowFilterPolicy;
 import org.apache.druid.query.timeboundary.TimeBoundaryResultValue;
 import org.apache.druid.server.broker.BrokerDynamicConfig;
+import org.apache.druid.server.broker.QueryConfigSnapshot;
 import org.apache.druid.server.initialization.ServerConfig;
 import org.apache.druid.server.log.TestRequestLogger;
 import org.apache.druid.server.metrics.NoopServiceEmitter;
@@ -772,11 +773,9 @@ public class QueryResourceTest
                 emitter,
                 testRequestLogger,
                 AuthTestUtils.TEST_AUTHORIZER_MAPPER,
-                overrideConfig,
                 new AuthConfig(),
                 NoopPolicyEnforcer.instance(),
-                null,
-                Collections.emptyMap(),
+                new QueryConfigSnapshot(overrideConfig.getContext(), null),
                 System.currentTimeMillis(),
                 System.nanoTime()
             )
@@ -1979,10 +1978,12 @@ public class QueryResourceTest
 
   private QueryResource createQueryResourceWithBlocklist(ServerConfig 
serverConfig, QueryBlocklistRule... rules)
   {
+    final BrokerDynamicConfig dynamicConfig =
+        new 
BrokerDynamicConfig.Builder().withQueryBlocklist(Arrays.asList(rules)).build();
     final BrokerViewOfBrokerConfig brokerViewOfBrokerConfig = 
Mockito.mock(BrokerViewOfBrokerConfig.class);
-    Mockito.when(brokerViewOfBrokerConfig.getDynamicConfig()).thenReturn(
-        new 
BrokerDynamicConfig.Builder().withQueryBlocklist(Arrays.asList(rules)).build()
-    );
+    
Mockito.when(brokerViewOfBrokerConfig.getDynamicConfig()).thenReturn(dynamicConfig);
+    Mockito.when(brokerViewOfBrokerConfig.snapshotForQuery())
+           .thenReturn(new QueryConfigSnapshot(Map.of(), dynamicConfig));
 
     return createQueryResource(
         new QueryLifecycleFactory(
diff --git 
a/server/src/test/java/org/apache/druid/server/broker/BrokerDynamicConfigTest.java
 
b/server/src/test/java/org/apache/druid/server/broker/BrokerDynamicConfigTest.java
index 6a466bcd269..5d8e2b948d8 100644
--- 
a/server/src/test/java/org/apache/druid/server/broker/BrokerDynamicConfigTest.java
+++ 
b/server/src/test/java/org/apache/druid/server/broker/BrokerDynamicConfigTest.java
@@ -24,7 +24,11 @@ import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import nl.jqno.equalsverifier.EqualsVerifier;
+import org.apache.druid.java.util.common.Intervals;
+import org.apache.druid.query.Druids;
 import org.apache.druid.query.QueryContext;
+import org.apache.druid.query.aggregation.CountAggregatorFactory;
+import org.apache.druid.query.timeseries.TimeseriesQuery;
 import org.apache.druid.segment.TestHelper;
 import org.apache.druid.server.DefaultQueryBlocklistRule;
 import org.apache.druid.server.QueryBlocklistRule;
@@ -204,4 +208,45 @@ public class BrokerDynamicConfigTest
                   .usingGetClass()
                   .verify();
   }
+
+  @Test
+  public void 
testContextOverridesInjectsPerSegmentTimeoutForMatchingDatasource()
+  {
+    BrokerDynamicConfig config = perSegmentTimeout("ds", new 
PerSegmentTimeoutConfig(5000, false));
+    Assert.assertEquals(5000L, 
config.getContextOverridesForQuery(query("ds")).getPerSegmentTimeout());
+  }
+
+  @Test
+  public void testContextOverridesEmptyForMonitorOnly()
+  {
+    BrokerDynamicConfig config = perSegmentTimeout("ds", new 
PerSegmentTimeoutConfig(5000, true));
+    
Assert.assertTrue(config.getContextOverridesForQuery(query("ds")).isEmpty());
+  }
+
+  @Test
+  public void testContextOverridesEmptyForNonMatchingDatasource()
+  {
+    BrokerDynamicConfig config = perSegmentTimeout("other", new 
PerSegmentTimeoutConfig(5000, false));
+    
Assert.assertTrue(config.getContextOverridesForQuery(query("ds")).isEmpty());
+  }
+
+  @Test
+  public void testContextOverridesEmptyWhenNoPerSegmentTimeoutConfigured()
+  {
+    
Assert.assertTrue(BrokerDynamicConfig.builder().build().getContextOverridesForQuery(query("ds")).isEmpty());
+  }
+
+  private static BrokerDynamicConfig perSegmentTimeout(String datasource, 
PerSegmentTimeoutConfig timeoutConfig)
+  {
+    return 
BrokerDynamicConfig.builder().withPerSegmentTimeoutConfig(Map.of(datasource, 
timeoutConfig)).build();
+  }
+
+  private static TimeseriesQuery query(String datasource)
+  {
+    return Druids.newTimeseriesQueryBuilder()
+                 .dataSource(datasource)
+                 .intervals(List.of(Intervals.ETERNITY))
+                 .aggregators(new CountAggregatorFactory("count"))
+                 .build();
+  }
 }
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/run/NativeQueryMaker.java 
b/sql/src/main/java/org/apache/druid/sql/calcite/run/NativeQueryMaker.java
index 8020b50f6c2..f0fb59254a0 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/run/NativeQueryMaker.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/run/NativeQueryMaker.java
@@ -186,7 +186,9 @@ public class NativeQueryMaker implements QueryMaker
     final QueryResponse<T> results = queryLifecycle.runSimple(
         (Query<T>) query,
         authenticationResult,
-        authorizationResult
+        authorizationResult,
+        // The user-set keys, as distinct from the defaults SQL merged into 
the context.
+        plannerContext.authContextKeys()
     );
 
     return mapResultSequence(
diff --git 
a/web-console/src/dialogs/edit-context-dialog/query-context-completions.ts 
b/web-console/src/dialogs/edit-context-dialog/query-context-completions.ts
index d7e67b7f492..68392e6a6dd 100644
--- a/web-console/src/dialogs/edit-context-dialog/query-context-completions.ts
+++ b/web-console/src/dialogs/edit-context-dialog/query-context-completions.ts
@@ -25,6 +25,10 @@ export const QUERY_CONTEXT_COMPLETIONS: JsonCompletionRule[] 
= [
     isObject: true,
     completions: [
       { value: 'timeout', documentation: 'Query timeout in milliseconds' },
+      {
+        value: 'perSegmentTimeout',
+        documentation: 'Per-segment processing timeout in milliseconds',
+      },
       {
         value: 'priority',
         documentation: 'Query priority (higher = more important)',


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to