risingwavelabs/risingwave · error
Unexpected exchange detected. We are either converting a sin
Error message
Unexpected exchange detected. We are either converting a single stage plan or converting the second stage of the plan.
What it means
Raised in `DistributedQueryStageScheduler`/local conversion (`convert_plan_node`, src/frontend/src/scheduler/local.rs:276) when a `BatchExchange` node is encountered while `second_stages` is `None`. Exchanges are only valid during the second-stage conversion pass, so this indicates the plan-to-executor converter saw an exchange in a context where it expected a single-stage plan (or already consumed the second stages).
Source
Thrown at src/frontend/src/scheduler/local.rs:276
}
fn convert_plan_node<'a>(
&'a self,
execution_plan_node: &ExecutionPlanNode,
second_stages: &mut Option<HashMap<StageId, &'a QueryStage>>,
partition: Option<PartitionInfo>,
next_executor_id: Arc<AtomicU32>,
) -> SchedulerResult<PbPlanNode> {
let identity = format!(
"{:?}-{}",
execution_plan_node.plan_node_type,
next_executor_id.fetch_add(1, Ordering::Relaxed)
);
match execution_plan_node.plan_node_type {
BatchPlanNodeType::BatchExchange => {
let exchange_source_stage_id = execution_plan_node
.source_stage_id
.expect("We expect stage id for Exchange Operator");
let Some(second_stages) = second_stages.as_mut() else {
bail!(
"Unexpected exchange detected. We are either converting a single stage plan or converting the second stage of the plan."
)
};
let second_stage = second_stages.remove(&exchange_source_stage_id).expect(
"We expect child stage fragment for Exchange Operator running in the frontend",
);
let mut node_body = execution_plan_node.node.clone();
let sources = match &mut node_body {
NodeBody::Exchange(exchange_node) => &mut exchange_node.sources,
NodeBody::MergeSortExchange(merge_sort_exchange_node) => {
&mut merge_sort_exchange_node
.exchange
.as_mut()
.expect("MergeSortExchangeNode must have a exchange node")
.sources
}View on GitHub (pinned to 6469eb736d)
Solutions
- Check that the query is scheduled through the correct path (distributed vs local) for its plan shape.
- Inspect the generated batch plan (`EXPLAIN (TRACE)`) to see why an exchange appears where single-stage execution was assumed.
- Retry with a simpler query shape or disable the optimization producing the unexpected exchange.
- If reproducible on a supported plan, report with the EXPLAIN output — it is an invariant violation in stage splitting.
Defensive patterns
Strategy: try-catch
Type guard
// Ensure second_stages exists before processing exchanges
fn require_second_stages(second_stages: &Option<HashMap<u32, _>>) -> Result<&HashMap<u32, _>> {
second_stages.as_ref().ok_or_else(|| anyhow!("no second stages supplied for exchange-containing plan"))
} Try / catch
match result {
Err(e) if e.to_string().contains("Unexpected exchange detected") => {
log::error!("stage splitting invariant broken; retry via distributed scheduler");
}
r => r?,
} Prevention
- Run exchange-containing plans through the distributed scheduler path, not the local single-stage path.
- Re-run EXPLAIN (TRACE) after planner changes to check plan shape.
- Cover stage-splitting invariants with unit tests.
When it happens
Trigger: Calling `convert_plan_node` via `create_plan_fragment` (including recursive conversion) with a batch plan that contains a `BatchExchange` node but no second-stage map supplied, or after all second stages have been removed and another exchange appears.
Common situations: Internal scheduler bugs after plan-shape changes (new operators introducing exchanges), or running the local/single-node executor path with a plan built for distributed execution.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- broken broadcast_channel
- {0}
- Task got killed because compute node running out of memory
- BatchError
- The stage has single distribution, but contains a source ope
AI-assisted analysis of risingwavelabs/risingwave@6469eb736d (2026-09-11).
Data as JSON: /api/errors/869ab106ddd53f7d.
Report an issue: GitHub.