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
- Inspect the dumped plan in the error to find where the Exchange sits
- Check for frontend/meta version mismatch and align binaries
- Reproduce with the offending query and file/upgrade a planner bug
- 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
- Keep frontend and meta binaries version-aligned
- Run planner tests when adding exchange-affecting rewrite rules
- Include the dumped plan node when reporting this error
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
- Row sequential scan should not have input executor!
- Source should not have input executor!
- Row sequential scan should not have input executor!
- GenerateSeriesExecutor should not have child!
- ValuesExecutor should have no child!
AI-assisted analysis of risingwavelabs/risingwave@6469eb736d (2026-09-11).
Data as JSON: /api/errors/d9a7f3125506c893.
Report an issue: GitHub.