Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a391bd9
feat: slow query thread pool
Swiddis Jul 13, 2026
311f751
handle slow query detection when optimization runs in execution step
Swiddis Jul 13, 2026
49e41df
fixes: assorted context propagation issues
Swiddis Jul 13, 2026
657657c
fix remaining integ tests
Swiddis Jul 14, 2026
5b0d7b2
add some more thread & security tests
Swiddis Jul 15, 2026
46c3cbe
code self-review, round 1
Swiddis Jul 16, 2026
6fdd82e
Merge remote-tracking branch 'upstream/main' into feat/slow-query-pool
Swiddis Jul 16, 2026
434021f
use 2x background threads to account for 2x pools
Swiddis Jul 17, 2026
169375f
rename slow -> complex, add pool indication header
Swiddis Jul 17, 2026
9c57cd7
add a failure log for slow pool requests
Swiddis Jul 17, 2026
7da8807
fix profile, add thread pool as part of profile object
Swiddis Jul 17, 2026
559a2d0
remove leftover build.gradle changes from another branch
Swiddis Jul 17, 2026
3ab3b4b
add cancelation polling so ppl cancelation is faster to apply
Swiddis Jul 21, 2026
73c68cf
Add thread pool profile details to doc
Swiddis Jul 22, 2026
21dd714
Move analyze call measurement
Swiddis Jul 22, 2026
9ba8eef
add complex pool IT
Swiddis Jul 22, 2026
009300a
Move calcite context thread copies to dedicated method
Swiddis Jul 22, 2026
cc02907
add attach_pid to gitignore
Swiddis Jul 22, 2026
aaaa8e4
register timeout handler to complex pool on these requests
Swiddis Jul 22, 2026
1053561
fix units
Swiddis Jul 22, 2026
ba6046c
Merge remote-tracking branch 'upstream/main' into feat/slow-query-pool
Swiddis Jul 27, 2026
737d413
remove redundant optimize call from execution engine during execution
Swiddis Jul 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ src/site-server/node_modules
build/
gen/
*.tokens
.attach_pid*

# various IDE files
.vscode
Expand Down Expand Up @@ -59,4 +60,4 @@ http-client.env.json
!.claude/harness/
.claude/settings.local.json
.clinerules
memory-bank
memory-bank
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,10 @@ public enum Key {
ASYNC_QUERY_EXTERNAL_SCHEDULER_INTERVAL(
"plugins.query.executionengine.async_query.external_scheduler.interval"),
STREAMING_JOB_HOUSEKEEPER_INTERVAL(
"plugins.query.executionengine.spark.streamingjobs.housekeeper.interval");
"plugins.query.executionengine.spark.streamingjobs.housekeeper.interval"),

/** Thread Pool Settings. */
SQL_COMPLEX_WORKER_POOL_ENABLED("plugins.sql.complex_worker_pool.enabled");

@Getter private final String keyValue;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ public class CalcitePlanContext {
/** Timewrap series mode: "relative", "short", or "exact". */
public static final ThreadLocal<String> timewrapSeries = new ThreadLocal<>();

/**
* Thread-local tracking which pool executed this query ("sql-worker" or "sql-complex-worker").
*/
public static final ThreadLocal<String> executionPool = new ThreadLocal<>();

/** Thread-local switch that tells whether the current query prefers legacy behavior. */
private static final ThreadLocal<Boolean> legacyPreferredFlag =
ThreadLocal.withInitial(() -> true);
Expand Down Expand Up @@ -245,6 +250,51 @@ public static void clearTimewrapSignals() {
stripNullColumns.set(false);
timewrapUnitName.set(null);
timewrapSeries.set(null);
executionPool.set(null);
}

/**
* Snapshot of all thread-local state in CalcitePlanContext. Used when dispatching queries to the
* complex worker pool — capture state on the caller thread, restore on the worker thread.
*/
public static class ThreadLocalSnapshot {
final boolean skipEncoding;
final boolean stripNullColumns;
final String timewrapUnitName;
final String timewrapSeries;
final String executionPool;

private ThreadLocalSnapshot(
boolean skipEncoding,
boolean stripNullColumns,
String timewrapUnitName,
String timewrapSeries,
String executionPool) {
this.skipEncoding = skipEncoding;
this.stripNullColumns = stripNullColumns;
this.timewrapUnitName = timewrapUnitName;
this.timewrapSeries = timewrapSeries;
this.executionPool = executionPool;
}
}

/** Capture current thread-local state for cross-thread propagation. */
public static ThreadLocalSnapshot snapshotThreadLocals() {
return new ThreadLocalSnapshot(
skipEncoding.get(),
stripNullColumns.get(),
timewrapUnitName.get(),
timewrapSeries.get(),
executionPool.get());
}

/** Restore thread-local state from a snapshot. */
public static void restoreThreadLocals(ThreadLocalSnapshot snapshot) {
skipEncoding.set(snapshot.skipEncoding);
stripNullColumns.set(snapshot.stripNullColumns);
timewrapUnitName.set(snapshot.timewrapUnitName);
timewrapSeries.set(snapshot.timewrapSeries);
executionPool.set(snapshot.executionPool);
}

public void pushForeachBindings(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -516,8 +516,6 @@ private static void enrichErrorsForSpecialCases(ErrorReport.Builder report, SQLE
public static PreparedStatement run(CalcitePlanContext context, RelNode rel) {
ProfileMetric optimizeTime = QueryProfiling.current().getOrCreateMetric(OPTIMIZE);
long startTime = System.nanoTime();
Comment thread
penghuo marked this conversation as resolved.
// Optimize the plan by Calcite's HepPlanner before using VolcanoPlanner in prepareStatement.
rel = CalciteToolsHelper.optimize(rel, context);
final RelShuttle shuttle =
new RelHomogeneousShuttle() {
@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.executor;

import org.apache.calcite.rel.RelNode;
import org.opensearch.sql.calcite.CalcitePlanContext;
import org.opensearch.sql.common.response.ResponseListener;

/**
* Default no-op dispatcher that executes inline on the current thread. Used when complex-pool
* routing is disabled or as a fallback.
*/
public class DirectExecutionDispatcher implements ExecutionDispatcher {

@Override
public void dispatch(
RelNode plan,
CalcitePlanContext context,
ResponseListener<ExecutionEngine.QueryResponse> listener,
ExecutionEngine engine) {
engine.execute(plan, context, listener);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.executor;

import org.apache.calcite.rel.RelNode;
import org.opensearch.sql.calcite.CalcitePlanContext;
import org.opensearch.sql.common.response.ResponseListener;

/**
* Dispatches query execution to an appropriate thread pool based on plan characteristics. After
* query analysis and optimization, the dispatcher inspects the plan and routes execution to either
* the fast worker pool (for queries fully pushed to OpenSearch) or the complex worker pool (for
* queries requiring scripts/table scans).
*/
public interface ExecutionDispatcher {

/**
* Dispatch execution of the given plan via the standard ExecutionEngine.
*
* @param plan the optimized Calcite plan
* @param context the plan context
* @param listener response listener for query results
* @param engine the execution engine to invoke
*/
void dispatch(
RelNode plan,
CalcitePlanContext context,
ResponseListener<ExecutionEngine.QueryResponse> listener,
ExecutionEngine engine);

/**
* Dispatch a task to the appropriate thread pool based on plan characteristics. Use this when the
* execution path differs from the standard ExecutionEngine interface (e.g., analytics engine).
*
* @param plan the optimized Calcite plan used for routing decisions
* @param context the plan context
* @param task the execution task to run
*/
default void dispatchTask(RelNode plan, CalcitePlanContext context, Runnable task) {
task.run();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Nullable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
Expand Down Expand Up @@ -54,6 +53,7 @@
import org.opensearch.sql.calcite.plan.rel.LogicalSystemLimit;
import org.opensearch.sql.calcite.plan.rel.LogicalSystemLimit.SystemLimitType;
import org.opensearch.sql.calcite.utils.CalciteClassLoaderHelper;
import org.opensearch.sql.calcite.utils.CalciteToolsHelper;
import org.opensearch.sql.calcite.utils.CalciteToolsHelper.OpenSearchRelRunners;
import org.opensearch.sql.common.error.ErrorReport;
import org.opensearch.sql.common.error.QueryProcessingStage;
Expand All @@ -78,14 +78,44 @@

/** The low level interface of core engine. */
@RequiredArgsConstructor
@AllArgsConstructor
@Log4j2
public class QueryService {
private final Analyzer analyzer;
private final ExecutionEngine executionEngine;
private final Planner planner;
private DataSourceService dataSourceService;
private Settings settings;
private ExecutionDispatcher executionDispatcher = new DirectExecutionDispatcher();

public QueryService(
Analyzer analyzer,
ExecutionEngine executionEngine,
Planner planner,
DataSourceService dataSourceService,
Settings settings) {
this(
analyzer,
executionEngine,
planner,
dataSourceService,
settings,
new DirectExecutionDispatcher());
}

public QueryService(
Analyzer analyzer,
ExecutionEngine executionEngine,
Planner planner,
DataSourceService dataSourceService,
Settings settings,
ExecutionDispatcher executionDispatcher) {
this.analyzer = analyzer;
this.executionEngine = executionEngine;
this.planner = planner;
this.dataSourceService = dataSourceService;
this.settings = settings;
this.executionDispatcher = executionDispatcher;
}

@Getter(lazy = true)
private final CalciteRelNodeVisitor relNodeVisitor = new CalciteRelNodeVisitor(dataSourceService);
Expand Down Expand Up @@ -199,9 +229,7 @@ public void executeWithCalcite(
convertToCalcitePlan(relNode, context), context),
"while converting the query to an executable plan");

analyzeMetric.set(System.nanoTime() - analyzeStart);

executeCalcitePlan(calcitePlan, context, listener);
executeCalcitePlan(calcitePlan, context, listener, analyzeMetric, analyzeStart);
},
QueryService.class);
} catch (Throwable t) {
Expand All @@ -219,11 +247,20 @@ public void executeWithCalcite(
private void executeCalcitePlan(
RelNode calcitePlan,
CalcitePlanContext context,
ResponseListener<ExecutionEngine.QueryResponse> listener) {
ResponseListener<ExecutionEngine.QueryResponse> listener,
ProfileMetric analyzeMetric,
long analyzeStart) {
try {
// Optimize before dispatch so the dispatcher's ScriptDetector
// sees the post-optimization plan for accurate routing.
RelNode optimizedPlan = CalciteToolsHelper.optimize(calcitePlan, context);
analyzeMetric.set(System.nanoTime() - analyzeStart);

// Wrap execution with EXECUTING stage tracking — dispatch via
// ExecutionDispatcher which may route to a complex worker pool
StageErrorHandler.executeStageVoid(
QueryProcessingStage.EXECUTING,
() -> executionEngine.execute(calcitePlan, context, listener),
() -> executionDispatcher.dispatch(optimizedPlan, context, listener, executionEngine),
Comment thread
penghuo marked this conversation as resolved.
"while running the query");
} catch (RuntimeException e) {
ArithmeticException overflow = findArithmeticOverflow(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import org.opensearch.sql.calcite.CalcitePlanContext;

/** Default implementation that records profiling metrics. */
public class DefaultProfileContext implements ProfileContext {
Expand Down Expand Up @@ -63,7 +64,8 @@ public synchronized QueryProfile finish() {
double totalMillis = ProfileUtils.roundToMillis(endNanos - startNanos);
Object planSnapshot =
enginePlan != null ? enginePlan : (planRoot == null ? null : planRoot.snapshot());
profile = new QueryProfile(totalMillis, snapshot, planSnapshot);
String threadPool = CalcitePlanContext.executionPool.get();
Comment thread
penghuo marked this conversation as resolved.
profile = new QueryProfile(totalMillis, snapshot, planSnapshot, threadPool);
return profile;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,17 @@ public final class QueryProfile {
/** Execution-engine-specific plan profile: a {@link PlanNode} tree, or a pre-rendered object. */
private final Object plan;

@SerializedName("thread_pool")
private final String threadPool;

Comment thread
Swiddis marked this conversation as resolved.
/**
* Create a new query profile snapshot.
*
* @param totalTimeMillis total elapsed milliseconds for the query (rounded to two decimals)
* @param phases metric values keyed by {@link MetricName}
*/
public QueryProfile(double totalTimeMillis, Map<MetricName, Double> phases) {
this(totalTimeMillis, phases, null);
this(totalTimeMillis, phases, null, null);
}

/**
Expand All @@ -42,9 +45,23 @@ public QueryProfile(double totalTimeMillis, Map<MetricName, Double> phases) {
* @param plan plan tree profiling output
*/
public QueryProfile(double totalTimeMillis, Map<MetricName, Double> phases, Object plan) {
this(totalTimeMillis, phases, plan, null);
}

/**
* Create a new query profile snapshot.
*
* @param totalTimeMillis total elapsed milliseconds for the query (rounded to two decimals)
* @param phases metric values keyed by {@link MetricName}
* @param plan plan tree profiling output
* @param threadPool thread pool name that executed the query
*/
public QueryProfile(
double totalTimeMillis, Map<MetricName, Double> phases, Object plan, String threadPool) {
this.summary = new Summary(totalTimeMillis);
this.phases = buildPhases(phases);
this.plan = plan;
this.threadPool = threadPool;
}

private Map<String, Phase> buildPhases(Map<MetricName, Double> phases) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ public static void clear() {
CURRENT.remove();
}

/**
* Set the profiling context for the current thread. Used when propagating context across thread
* boundaries.
*
* @param ctx profiling context to bind
*/
public static void set(ProfileContext ctx) {
CURRENT.set(Objects.requireNonNull(ctx, "ctx"));
}

/**
* Run a supplier with the provided profiling context bound to the current thread.
*
Expand Down
4 changes: 3 additions & 1 deletion docs/user/ppl/interfaces/endpoint.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,8 @@ Expected output (trimmed):
"children": [
{ "node": "CalciteEnumerableIndexScan", "time_ms": 4.12, "rows": 2 }
]
}
},
"thread_pool": "sql-worker"
}
}
```
Expand All @@ -315,6 +316,7 @@ Expected output (trimmed):
- Plan node names use Calcite physical operator names (for example, `EnumerableCalc` or `CalciteEnumerableIndexScan`).
- Plan `time_ms` is inclusive of child operators and represents wall-clock time; overlapping work can make summed plan times exceed `summary.total_time_ms`.
- Scan nodes reflect operator wall-clock time; background prefetch can make scan time smaller than total request latency.
- `thread_pool` indicates which thread pool executed the query. Possible values are `sql-worker` (default, pushdown-only queries) and `sql-complex-worker` (queries requiring in-memory evaluation such as scripted fields).

## Highlight

Expand Down
Loading
Loading