risingwavelabs/risingwave · error · MetaError

there should be no ExchangeNode on the top of the plan node:

Error message

there should be no ExchangeNode on the top of the plan node: {:#?}

What it means

In actor rewriting (rewrite_inner), an Exchange node may only appear at the bottom of a plan tree (depth 0, where it becomes the merge of an upstream fragment). Finding one at depth > 0 means the streamed plan is malformed — exchanges must have been rewritten/hoisted before this stage.

Source

Thrown at src/meta/src/stream/stream_graph/actor.rs:54

impl FragmentActorBuilder {
    /// Rewrite the actor body.
    ///
    /// During this process, the following things will be done:
    /// 1. Replace the logical `Exchange` in node's input with `Merge`, which can be executed on the
    ///    compute nodes.
    fn rewrite(&self) -> MetaResult<StreamNode> {
        self.rewrite_inner(&self.node, 0)
    }

    fn rewrite_inner(&self, stream_node: &StreamNode, depth: usize) -> MetaResult<StreamNode> {
        match stream_node.get_node_body()? {
            // Leaf node `Exchange`.
            NodeBody::Exchange(exchange) => {
                // The exchange node should always be the bottom of the plan node. If we find one
                // when the depth is 0, it means that the plan node is not well-formed.
                if depth == 0 {
                    bail!(
                        "there should be no ExchangeNode on the top of the plan node: {:#?}",
                        stream_node
                    )
                }
                assert!(!stream_node.get_fields().is_empty());
                assert!(stream_node.input.is_empty());

                // Index the upstreams by the an internal edge ID.
                let (upstream_fragment_id, _) = &self.upstreams[&EdgeId::Internal {
                    link_id: stream_node.get_operator_id().as_raw_id(),
                }];

                let upstream_fragment_id = upstream_fragment_id.as_global_id();

                Ok(StreamNode {
                    node_body: Some(NodeBody::Merge(Box::new({
                        MergeNode {
                            upstream_fragment_id,

View on GitHub (pinned to 6469eb736d)

Solutions

  1. Inspect the dumped plan in the error to find where the Exchange sits
  2. Check for frontend/meta version mismatch and align binaries
  3. Reproduce with the offending query and file/upgrade a planner bug
  4. Restart and rebuild the MV/job to regenerate the plan
Defensive patterns

Strategy: validation

Validate before calling

// before graph build, assert plan well-formedness: exchanges only at depth 0
fn exchange_only_at_root(node: &StreamNode, depth: usize) -> bool {
    let is_exchange = matches!(node.get_node_body(), Ok(NodeBody::Exchange(_)));
    !(is_exchange && depth > 0)
        && node.get_input().iter().all(|c| exchange_only_at_root(c, depth + 1))
}

Try / catch

match rewrite(stream_node) {
    Err(e) if e.to_string().contains("ExchangeNode on the top") => {
        // plan malformed: capture the dumped plan for a planner bug report
    }
    r => r?,
}

Prevention

When it happens

Trigger: Building the actor graph from a stream plan whose tree contains an Exchange node above other operators, i.e. planner/frontend emitted an exchange at a non-leaf position.

Common situations: Frontend/planner bug after a new plan node type or rewrite rule; custom internal development; version skew between frontend and meta binary.

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/d9a7f3125506c893. Report an issue: GitHub.