Skip to content

Commit f5db45c

Browse files
AjayRajNelapudibkhishor
authored andcommitted
Make tokio concurrency settings dynamic (opensearch-project#21817)
* Make tokio concurrency settings dynamic Signed-off-by: Ajay Raj Nelapudi <ajnelapu@amazon.com>
1 parent 91218f9 commit f5db45c

25 files changed

Lines changed: 2916 additions & 116 deletions

sandbox/plugins/analytics-backend-datafusion/rust/src/executor.rs

Lines changed: 457 additions & 6 deletions
Large diffs are not rendered by default.

sandbox/plugins/analytics-backend-datafusion/rust/src/ffm.rs

Lines changed: 167 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,54 @@ pub extern "C" fn df_shutdown_runtime_manager() {
8888
}
8989
}
9090

91+
/// Updates the effective permit count of a named concurrency gate.
92+
/// Gate names: "fragment_executor" (targets DedicatedExecutor gate) or
93+
/// "reduce" (targets RuntimeManager coordinator gate).
94+
///
95+
/// Scale-up is synchronous. Scale-down spawns an async task on the IO
96+
/// runtime to acquire poison permits (may need to wait for in-flight
97+
/// queries to release).
98+
///
99+
/// Java: NativeBridge.updateConcurrencyGate(String, int)
100+
#[ffm_safe]
101+
#[no_mangle]
102+
pub unsafe extern "C" fn df_update_concurrency_gate(
103+
gate_name_ptr: *const u8,
104+
gate_name_len: i64,
105+
new_max_permits: u32,
106+
) -> i64 {
107+
let gate_name = str_from_raw(gate_name_ptr, gate_name_len)
108+
.map_err(|e| format!("df_update_concurrency_gate: {}", e))?;
109+
110+
let mgr = match get_rt_manager() {
111+
Ok(m) => m,
112+
Err(_) => {
113+
warn!("df_update_concurrency_gate called before runtime init");
114+
return Ok(0);
115+
}
116+
};
117+
118+
let gate = match gate_name {
119+
"fragment_executor" => mgr.cpu_executor().concurrency_gate().clone(),
120+
"reduce" => mgr.coordinator_gate().clone(),
121+
other => {
122+
warn!("df_update_concurrency_gate: unknown gate '{}'", other);
123+
return Ok(0);
124+
}
125+
};
126+
127+
let io_runtime = mgr.io_runtime.clone();
128+
let gate_name_owned = gate_name.to_string();
129+
130+
// Spawn the resize on the IO runtime. Scale-up completes immediately;
131+
// scale-down may need to wait for permits to become available.
132+
io_runtime.spawn(async move {
133+
gate.resize(new_max_permits, &gate_name_owned).await;
134+
});
135+
136+
Ok(0)
137+
}
138+
91139
#[ffm_safe]
92140
#[no_mangle]
93141
pub unsafe extern "C" fn df_create_global_runtime(
@@ -1149,8 +1197,8 @@ pub unsafe extern "C" fn df_stats(out_ptr: *mut u8, out_cap: i64) -> i64 {
11491197
query_execution: pack_task_monitor(query_execution_monitor()),
11501198
stream_next: pack_task_monitor(stream_next_monitor()),
11511199
plan_setup: pack_task_monitor(plan_setup_monitor()),
1152-
datanode_gate: pack_partition_gate(mgr.cpu_executor.concurrency_gate()),
1153-
coordinator_gate: pack_partition_gate(mgr.coordinator_gate()),
1200+
fragment_executor_gate: pack_partition_gate(mgr.cpu_executor.concurrency_gate()),
1201+
reduce_executor_gate: pack_partition_gate(mgr.coordinator_gate()),
11541202
};
11551203

11561204
// Copy struct bytes to caller buffer
@@ -1262,4 +1310,121 @@ mod tests {
12621310
assert_eq!(send_outcome_to_code(SendOutcome::ReceiverDropped), SENDER_SEND_RECEIVER_DROPPED);
12631311
assert_eq!(SENDER_SEND_RECEIVER_DROPPED, 1);
12641312
}
1313+
1314+
/// Initialize the global runtime manager for tests.
1315+
/// Uses 2 CPU threads and 1.5 multiplier (default) for both gates.
1316+
fn init_test_runtime() {
1317+
df_init_runtime_manager(2, 1.5, 1.5);
1318+
}
1319+
1320+
/// Shutdown and clear the global runtime manager after tests.
1321+
/// Must be called from a blocking context (not inside an async runtime).
1322+
fn shutdown_test_runtime() {
1323+
df_shutdown_runtime_manager();
1324+
}
1325+
1326+
/// Helper: call df_update_concurrency_gate with a Rust string.
1327+
/// Returns the i64 result (0 = success for the outer call).
1328+
unsafe fn call_update_gate(gate_name: &str, new_max: u32) -> i64 {
1329+
df_update_concurrency_gate(
1330+
gate_name.as_ptr(),
1331+
gate_name.len() as i64,
1332+
new_max,
1333+
)
1334+
}
1335+
1336+
/// Validates: Requirements 2.2, 2.3, 2.4, 2.6
1337+
///
1338+
/// Combined test for FFI gate routing to avoid global state conflicts
1339+
/// between parallel test threads. Tests are run sequentially within this
1340+
/// function since they all share the TOKIO_RUNTIME_MANAGER global.
1341+
///
1342+
/// Covers:
1343+
/// - "fragment_executor" routes to the DedicatedExecutor's gate (Req 2.2)
1344+
/// - "reduce" routes to the RuntimeManager's coordinator gate (Req 2.3)
1345+
/// - Unknown gate name logs warning and returns success (Req 2.4)
1346+
/// - Calling update before runtime init returns success (Req 2.6)
1347+
#[test]
1348+
fn test_ffi_gate_routing() {
1349+
// ── Test 1: update before runtime init returns success (Req 2.6) ──
1350+
shutdown_test_runtime(); // ensure clean state
1351+
let result = unsafe { call_update_gate("fragment_executor", 10) };
1352+
assert_eq!(result, 0, "FFI call should return success even before runtime init");
1353+
1354+
// ── Initialize runtime for remaining tests ──
1355+
init_test_runtime();
1356+
let mgr = get_rt_manager().expect("runtime should be initialized");
1357+
1358+
// ── Test 2: "fragment_executor" routes to CPU executor gate (Req 2.2) ──
1359+
{
1360+
let gate = mgr.cpu_executor().concurrency_gate().clone();
1361+
let initial_max = gate.max_permits();
1362+
let new_max = initial_max + 4;
1363+
1364+
let result = unsafe { call_update_gate("fragment_executor", new_max) };
1365+
assert_eq!(result, 0, "FFI call should return success for 'fragment_executor'");
1366+
1367+
// The resize is spawned on the IO runtime asynchronously.
1368+
// Wait briefly for it to complete.
1369+
std::thread::sleep(std::time::Duration::from_millis(200));
1370+
1371+
assert_eq!(
1372+
gate.max_permits(),
1373+
new_max,
1374+
"fragment_executor gate max_permits should be updated to {}",
1375+
new_max
1376+
);
1377+
}
1378+
1379+
// ── Test 3: "reduce" routes to coordinator gate (Req 2.3) ──
1380+
{
1381+
let gate = mgr.coordinator_gate().clone();
1382+
let initial_max = gate.max_permits();
1383+
let new_max = initial_max + 4;
1384+
1385+
let result = unsafe { call_update_gate("reduce", new_max) };
1386+
assert_eq!(result, 0, "FFI call should return success for 'reduce'");
1387+
1388+
// The resize is spawned on the IO runtime asynchronously.
1389+
// Wait briefly for it to complete.
1390+
std::thread::sleep(std::time::Duration::from_millis(200));
1391+
1392+
assert_eq!(
1393+
gate.max_permits(),
1394+
new_max,
1395+
"reduce gate max_permits should be updated to {}",
1396+
new_max
1397+
);
1398+
}
1399+
1400+
// ── Test 4: unknown gate name returns success without modifying gates (Req 2.4) ──
1401+
{
1402+
let fragment_executor_gate = mgr.cpu_executor().concurrency_gate().clone();
1403+
let reduce_executor_gate = mgr.coordinator_gate().clone();
1404+
1405+
let fragment_executor_max_before = fragment_executor_gate.max_permits();
1406+
let reduce_max_before = reduce_executor_gate.max_permits();
1407+
1408+
let result = unsafe { call_update_gate("unknown_gate", 99) };
1409+
assert_eq!(result, 0, "FFI call should return success even for unknown gate");
1410+
1411+
// Wait briefly to ensure no async resize was spawned
1412+
std::thread::sleep(std::time::Duration::from_millis(100));
1413+
1414+
// Neither gate should have been modified
1415+
assert_eq!(
1416+
fragment_executor_gate.max_permits(),
1417+
fragment_executor_max_before,
1418+
"fragment_executor gate should not be modified for unknown gate name"
1419+
);
1420+
assert_eq!(
1421+
reduce_executor_gate.max_permits(),
1422+
reduce_max_before,
1423+
"reduce gate should not be modified for unknown gate name"
1424+
);
1425+
}
1426+
1427+
// ── Cleanup ──
1428+
shutdown_test_runtime();
1429+
}
12651430
}

sandbox/plugins/analytics-backend-datafusion/rust/src/runtime_manager.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pub struct RuntimeManager {
1919
pub io_monitor: RuntimeMonitor,
2020
pub cpu_monitor: Option<RuntimeMonitor>,
2121
/// Separate concurrency gate for coordinator-reduce execution.
22-
/// Independent from the datanode gate (on DedicatedExecutor) to avoid
22+
/// Independent from the fragment executor gate (on DedicatedExecutor) to avoid
2323
/// deadlock when shard streams and coordinator reduce run concurrently.
2424
coordinator_gate: Arc<ConcurrencyGate>,
2525
}
@@ -51,16 +51,16 @@ impl RuntimeManager {
5151
register_io_runtime(Some(io_handle.clone()));
5252
});
5353

54-
// Datanode concurrency gate: limits concurrent partition tasks from shard scans.
54+
// Fragment executor concurrency gate: limits concurrent partition tasks from shard scans.
5555
let datanode_max_concurrent = (cpu_threads as f64 * datanode_multiplier).max(1.0) as usize;
5656
let cpu_executor = DedicatedExecutor::new("datafusion-cpu", cpu_runtime_builder, datanode_max_concurrent);
5757

5858
let cpu_monitor = cpu_executor
5959
.handle()
6060
.map(|h| RuntimeMonitor::new(&h));
6161

62-
// Coordinator concurrency gate: limits concurrent partition tasks from reduce execution.
63-
// Separate from datanode gate to avoid deadlock (shard streams hold datanode permits
62+
// Reduce concurrency gate: limits concurrent partition tasks from reduce execution.
63+
// Separate from fragment executor gate to avoid deadlock (shard streams hold fragment executor permits
6464
// while coordinator reduce runs concurrently on single-node clusters).
6565
let coordinator_max_concurrent = (cpu_threads as f64 * coordinator_multiplier).max(1.0) as usize;
6666
let coordinator_gate = Arc::new(ConcurrencyGate::new(coordinator_max_concurrent));

sandbox/plugins/analytics-backend-datafusion/rust/src/stats.rs

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
//! Stats packing helpers for the FFM `df_stats()` function.
66
//!
77
//! Packs Tokio runtime metrics and per-operation task monitor metrics
8-
//! into a `#[repr(C)]` `DfStatsBuffer` struct (304 bytes) for efficient
8+
//! into a `#[repr(C)]` `DfStatsBuffer` struct (336 bytes) for efficient
99
//! transfer across the FFM boundary.
1010
//!
1111
//! ## Struct layout
@@ -18,8 +18,8 @@
1818
//! | `query_execution` | `TaskMonitorRepr` | 3 × i64 |
1919
//! | `stream_next` | `TaskMonitorRepr` | 3 × i64 |
2020
//! | `plan_setup` | `TaskMonitorRepr` | 3 × i64 |
21-
//! | `datanode_gate` | `PartitionGateRepr` | 4 × i64 |
22-
//! | `coordinator_gate` | `PartitionGateRepr` | 4 × i64 |
21+
//! | `fragment_executor_gate` | `PartitionGateRepr` | 6 × i64 |
22+
//! | `reduce_executor_gate` | `PartitionGateRepr` | 6 × i64 |
2323
2424
use tokio::runtime::Handle;
2525
use tokio_metrics::{RuntimeMonitor, TaskMonitor};
@@ -68,6 +68,8 @@ pub struct PartitionGateRepr {
6868
pub active_permits: i64,
6969
pub total_wait_duration_ms: i64,
7070
pub total_batches_started: i64,
71+
pub poison_permits: i64,
72+
pub target_max_permits: i64,
7173
}
7274

7375
#[repr(C)]
@@ -78,19 +80,19 @@ pub struct DfStatsBuffer {
7880
pub query_execution: TaskMonitorRepr,
7981
pub stream_next: TaskMonitorRepr,
8082
pub plan_setup: TaskMonitorRepr,
81-
pub datanode_gate: PartitionGateRepr,
82-
pub coordinator_gate: PartitionGateRepr,
83+
pub fragment_executor_gate: PartitionGateRepr,
84+
pub reduce_executor_gate: PartitionGateRepr,
8385
}
8486

8587
const _: () = assert!(std::mem::size_of::<RuntimeMetricsRepr>() == 9 * 8);
8688
const _: () = assert!(std::mem::size_of::<TaskMonitorRepr>() == 3 * 8);
87-
const _: () = assert!(std::mem::size_of::<PartitionGateRepr>() == 4 * 8);
88-
const _: () = assert!(std::mem::size_of::<DfStatsBuffer>() == 38 * 8);
89+
const _: () = assert!(std::mem::size_of::<PartitionGateRepr>() == 6 * 8);
90+
const _: () = assert!(std::mem::size_of::<DfStatsBuffer>() == 42 * 8);
8991

9092
pub mod layout {
9193
use super::*;
9294
pub const BUFFER_BYTE_SIZE: usize = std::mem::size_of::<DfStatsBuffer>();
93-
const _: () = assert!(BUFFER_BYTE_SIZE == 304);
95+
const _: () = assert!(BUFFER_BYTE_SIZE == 336);
9496
}
9597

9698
/// Snapshot a `RuntimeMonitor` and return a populated `RuntimeMetricsRepr`.
@@ -163,12 +165,16 @@ pub fn pack_task_monitor(monitor: &TaskMonitor) -> TaskMonitorRepr {
163165
/// | active_permits | `gate.active_permits()` |
164166
/// | total_wait_duration_ms | `gate.total_wait_ms()` |
165167
/// | total_batches_started | `gate.total_queries_admitted()` |
168+
/// | poison_permits | `gate.poison_permits_held()` |
169+
/// | target_max_permits | `gate.target_max_permits()` |
166170
pub fn pack_partition_gate(gate: &ConcurrencyGate) -> PartitionGateRepr {
167171
PartitionGateRepr {
168172
max_permits: gate.max_permits() as i64,
169173
active_permits: gate.active_permits() as i64,
170174
total_wait_duration_ms: gate.total_wait_ms() as i64,
171175
total_batches_started: gate.total_queries_admitted() as i64,
176+
poison_permits: gate.poison_permits_held() as i64,
177+
target_max_permits: gate.target_max_permits() as i64,
172178
}
173179
}
174180

@@ -253,14 +259,14 @@ mod tests {
253259
query_execution: pack_task_monitor(query_execution_monitor()),
254260
stream_next: pack_task_monitor(stream_next_monitor()),
255261
plan_setup: pack_task_monitor(plan_setup_monitor()),
256-
datanode_gate: pack_partition_gate(mgr.cpu_executor.concurrency_gate()),
257-
coordinator_gate: pack_partition_gate(mgr.coordinator_gate()),
262+
fragment_executor_gate: pack_partition_gate(mgr.cpu_executor.concurrency_gate()),
263+
reduce_executor_gate: pack_partition_gate(mgr.coordinator_gate()),
258264
};
259265

260-
assert_eq!(layout::BUFFER_BYTE_SIZE, 304);
266+
assert_eq!(layout::BUFFER_BYTE_SIZE, 336);
261267
assert!(buf.io_runtime.workers_count > 0, "IO runtime workers_count should be > 0, got {}", buf.io_runtime.workers_count);
262-
assert!(buf.datanode_gate.max_permits > 0, "datanode_gate max_permits should be > 0, got {}", buf.datanode_gate.max_permits);
263-
assert!(buf.coordinator_gate.max_permits > 0, "coordinator_gate max_permits should be > 0, got {}", buf.coordinator_gate.max_permits);
268+
assert!(buf.fragment_executor_gate.max_permits > 0, "fragment_executor_gate max_permits should be > 0, got {}", buf.fragment_executor_gate.max_permits);
269+
assert!(buf.reduce_executor_gate.max_permits > 0, "reduce_executor_gate max_permits should be > 0, got {}", buf.reduce_executor_gate.max_permits);
264270

265271
if mgr.cpu_monitor.is_some() {
266272
assert!(buf.cpu_runtime.workers_count > 0, "CPU runtime workers_count should be > 0, got {}", buf.cpu_runtime.workers_count);
@@ -273,9 +279,9 @@ mod tests {
273279
#[test]
274280
fn test_df_stats_buffer_too_small() {
275281
// Verify that the buffer size assertion holds
276-
assert_eq!(std::mem::size_of::<DfStatsBuffer>(), 304);
277-
assert_eq!(layout::BUFFER_BYTE_SIZE, 304);
278-
// A buffer smaller than 304 bytes should be rejected by df_stats.
282+
assert_eq!(std::mem::size_of::<DfStatsBuffer>(), 336);
283+
assert_eq!(layout::BUFFER_BYTE_SIZE, 336);
284+
// A buffer smaller than 336 bytes should be rejected by df_stats.
279285
// We can't call df_stats directly without a runtime manager,
280286
// but we verify the constant is correct.
281287
assert!(layout::BUFFER_BYTE_SIZE > 0);

sandbox/plugins/analytics-backend-datafusion/src/main/java/org/opensearch/be/datafusion/DataFusionPlugin.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,19 @@ public Collection<Object> createComponents(
420420
clusterService.getClusterSettings()
421421
.addSettingsUpdateConsumer(DATAFUSION_MEMORY_GUARD_EXECUTION_CRITICAL_THRESHOLD, v -> updateMemoryGuardThresholds());
422422

423+
// Wire dynamic concurrency gate multiplier settings
424+
int cpuThreads = DataFusionService.cpuThreadCount();
425+
426+
clusterService.getClusterSettings().addSettingsUpdateConsumer(DatafusionSettings.CONCURRENCY_DATANODE_MULTIPLIER, multiplier -> {
427+
int newMax = Math.max(1, (int) (cpuThreads * multiplier));
428+
NativeBridge.updateConcurrencyGate("fragment_executor", newMax);
429+
});
430+
431+
clusterService.getClusterSettings().addSettingsUpdateConsumer(DatafusionSettings.CONCURRENCY_COORDINATOR_MULTIPLIER, multiplier -> {
432+
int newMax = Math.max(1, (int) (cpuThreads * multiplier));
433+
NativeBridge.updateConcurrencyGate("reduce", newMax);
434+
});
435+
423436
// Apply initial values
424437
NativeBridge.setMinTargetPartitions(DATAFUSION_MIN_TARGET_PARTITIONS.get(settings));
425438
NativeBridge.setReduceTargetPartitions(DATAFUSION_REDUCE_TARGET_PARTITIONS.get(settings));

sandbox/plugins/analytics-backend-datafusion/src/main/java/org/opensearch/be/datafusion/DataFusionService.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ public static Builder builder() {
6363
return new Builder();
6464
}
6565

66+
/**
67+
* Returns the number of available CPU threads used for the dedicated executor.
68+
* This is the value used to compute concurrency gate permit counts (cpu_threads × multiplier).
69+
*/
70+
public static int cpuThreadCount() {
71+
return Runtime.getRuntime().availableProcessors();
72+
}
73+
6674
@Override
6775
protected void doStart() {
6876
logger.debug("Starting DataFusion service");

sandbox/plugins/analytics-backend-datafusion/src/main/java/org/opensearch/be/datafusion/DatafusionSettings.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -191,22 +191,24 @@ public final class DatafusionSettings {
191191

192192
// ── Concurrency gate settings ──
193193

194-
/** Datanode concurrency gate multiplier: max concurrent partition-equivalents = cpu_threads × multiplier. */
194+
/** Fragment executor concurrency gate multiplier: max concurrent partition-equivalents = cpu_threads × multiplier. */
195195
public static final Setting<Double> CONCURRENCY_DATANODE_MULTIPLIER = Setting.doubleSetting(
196-
"datafusion.concurrency.datanode_multiplier",
196+
"datafusion.concurrency.fragment_executor_multiplier",
197197
1.5,
198198
0.1,
199199
10.0,
200-
Setting.Property.NodeScope
200+
Setting.Property.NodeScope,
201+
Setting.Property.Dynamic
201202
);
202203

203-
/** Coordinator concurrency gate multiplier: max concurrent partition-equivalents = cpu_threads × multiplier. */
204+
/** Reduce concurrency gate multiplier: max concurrent partition-equivalents = cpu_threads × multiplier. */
204205
public static final Setting<Double> CONCURRENCY_COORDINATOR_MULTIPLIER = Setting.doubleSetting(
205-
"datafusion.concurrency.coordinator_multiplier",
206+
"datafusion.concurrency.reduce_executor_multiplier",
206207
1.5,
207208
0.1,
208209
10.0,
209-
Setting.Property.NodeScope
210+
Setting.Property.NodeScope,
211+
Setting.Property.Dynamic
210212
);
211213

212214
// Query strategy constants

0 commit comments

Comments
 (0)