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

  1. Check that the query is scheduled through the correct path (distributed vs local) for its plan shape.
  2. Inspect the generated batch plan (`EXPLAIN (TRACE)`) to see why an exchange appears where single-stage execution was assumed.
  3. Retry with a simpler query shape or disable the optimization producing the unexpected exchange.
  4. 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

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


AI-assisted analysis of risingwavelabs/risingwave@6469eb736d (2026-09-11). Data as JSON: /api/errors/869ab106ddd53f7d. Report an issue: GitHub.