Skip to content

Commit 050f2bf

Browse files
mch2bkhishor
authored andcommitted
analytics-engine: dedicated reduce thread pool (opensearch-project#21800)
* analytics-engine: dedicated reduce thread pool Add a fixed analytics_reduce pool (size=max(2, processors/2), queue=100) for coordinator reduce drains. The reduce blocks for the entire query duration via a synchronous FFM call (streamNext), so sharing the SEARCH pool causes deadlock: concurrent reduces occupy all search threads, preventing local shard fragments from executing and feeding the reduce. The reduce pool is separate from both SEARCH (which runs shard fragments) and analytics_scheduler (which orchestrates stage lifecycle). Pool size is adjustable via thread_pool.analytics_reduce.size cluster setting. Signed-off-by: Marc Handalian <marc.handalian@gmail.com>
1 parent 044cc2b commit 050f2bf

4 files changed

Lines changed: 23 additions & 5 deletions

File tree

sandbox/plugins/analytics-engine/src/main/java/org/opensearch/analytics/AnalyticsPlugin.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,17 @@ public class AnalyticsPlugin extends Plugin implements ExtensiblePlugin, ActionP
6969
public static final String SCHEDULER_THREAD_POOL_NAME = "analytics_scheduler";
7070
private static final int SCHEDULER_QUEUE_SIZE = 200;
7171

72+
public static final String REDUCE_THREAD_POOL_NAME = "analytics_reduce";
73+
74+
// The reduce pool exists to isolate coordinator-reduce drains from the SEARCH
75+
// pool, preventing deadlock when reduces and local shard fragments compete for
76+
// the same threads. Each reduce thread blocks on a synchronous FFM call
77+
// (streamNext) with negligible CPU usage — memory is the real constraint,
78+
// bounded by the DataFusion pool and phantom reservations. Size is generous
79+
// so the thread pool isn't the throughput bottleneck.
80+
private static final int REDUCE_POOL_SIZE = Math.max(8, Runtime.getRuntime().availableProcessors() * 4);
81+
private static final int REDUCE_QUEUE_SIZE = 200;
82+
7283
public static final Setting<Long> COORDINATOR_BUFFER_LIMIT = Setting.longSetting(
7384
"analytics.coordinator.buffer_limit",
7485
256L * 1024 * 1024,
@@ -152,7 +163,10 @@ public List<Setting<?>> getSettings() {
152163
@Override
153164
public List<ExecutorBuilder<?>> getExecutorBuilders(Settings settings) {
154165
int poolSize = schedulerPoolSize();
155-
return List.of(new FixedExecutorBuilder(settings, SCHEDULER_THREAD_POOL_NAME, poolSize, SCHEDULER_QUEUE_SIZE, "analytics"));
166+
return List.of(
167+
new FixedExecutorBuilder(settings, SCHEDULER_THREAD_POOL_NAME, poolSize, SCHEDULER_QUEUE_SIZE, "analytics"),
168+
new FixedExecutorBuilder(settings, REDUCE_THREAD_POOL_NAME, REDUCE_POOL_SIZE, REDUCE_QUEUE_SIZE, "analytics_reduce")
169+
);
156170
}
157171

158172
static int schedulerPoolSize() {

sandbox/plugins/analytics-engine/src/main/java/org/opensearch/analytics/exec/QueryContext.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ public Executor schedulerExecutor() {
8383
return threadPool != null ? threadPool.executor(AnalyticsPlugin.SCHEDULER_THREAD_POOL_NAME) : Runnable::run;
8484
}
8585

86+
public Executor reduceExecutor() {
87+
return threadPool != null ? threadPool.executor(AnalyticsPlugin.REDUCE_THREAD_POOL_NAME) : Runnable::run;
88+
}
89+
8690
public AnalyticsQueryTask parentTask() {
8791
return parentTask;
8892
}

sandbox/plugins/analytics-engine/src/main/java/org/opensearch/analytics/exec/stage/coordinator/ReduceStageExecution.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@ public final class ReduceStageExecution extends AbstractStageExecution implement
3838

3939
private final ReducingExchangeSink backendSink;
4040
private final ExchangeSink downstream;
41-
private final Executor searchExecutor;
41+
private final Executor reduceExecutor;
4242

4343
public ReduceStageExecution(Stage stage, QueryContext config, ReducingExchangeSink backendSink, ExchangeSink downstream) {
4444
super(stage, config.queryId(), config.operationListeners(), config.parentTask());
4545
this.backendSink = backendSink;
4646
this.downstream = downstream;
47-
this.searchExecutor = config.searchExecutor();
47+
this.reduceExecutor = config.reduceExecutor();
4848
this.runner = new LocalTaskRunner(config.schedulerExecutor());
4949
}
5050

@@ -81,7 +81,7 @@ public ExchangeSource outputSource() {
8181
@Override
8282
protected List<StageTask> materializeTasks() {
8383
return List.of(new LocalStageTask(new StageTaskId(getStageId(), 0), listener -> {
84-
searchExecutor.execute(() -> {
84+
reduceExecutor.execute(() -> {
8585
try {
8686
backendSink.reduce(listener);
8787
} catch (Exception e) {

sandbox/plugins/analytics-engine/src/test/java/org/opensearch/analytics/exec/stage/ReduceStageExecutionTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ private static ExecutorService inlineExecutor() {
257257

258258
private static QueryContext mockContext() {
259259
QueryContext ctx = mock(QueryContext.class);
260-
when(ctx.searchExecutor()).thenReturn(inlineExecutor());
260+
when(ctx.reduceExecutor()).thenReturn(inlineExecutor());
261261
when(ctx.schedulerExecutor()).thenReturn(inlineExecutor());
262262
when(ctx.parentTask()).thenReturn(mock(AnalyticsQueryTask.class));
263263
return ctx;

0 commit comments

Comments
 (0)