risingwavelabs/risingwave · error
Exchange executor should not have children!
Error message
Exchange executor should not have children!
What it means
Same monotonicity rule as the buffered-path truncation, but for the historical-data path: when the requested truncation offset is a `TruncateOffset::Barrier { epoch }` covering pre-current-epoch data, the reader rejects truncating at or before the previously recorded truncate offset. Historical truncation is applied at barrier/epoch granularity via `rx.truncate_historical(epoch)`.
Source
Thrown at src/batch/executors/src/executor/generic_exchange.rs:154
.inspect_err(|e| {
if matches!(e, BatchError::RpcError(_)) {
mask_failed_serving_worker()
}
})?,
))
}
}
}
pub struct GenericExchangeExecutorBuilder {}
impl BoxedExecutorBuilder for GenericExchangeExecutorBuilder {
async fn new_boxed_executor(
source: &ExecutorBuilder<'_>,
inputs: Vec<BoxedExecutor>,
) -> Result<BoxedExecutor> {
ensure!(
inputs.is_empty(),
"Exchange executor should not have children!"
);
let node = try_match_expand!(
source.plan_node().get_node_body().unwrap(),
NodeBody::Exchange
)?;
let sequential = node.get_sequential();
ensure!(!node.get_sources().is_empty());
let proto_sources: Vec<PbExchangeSource> = node.get_sources().clone();
let source_creators =
vec![DefaultCreateSource::new(source.context().client_pool()); proto_sources.len()];
let input_schema: Vec<NodeField> = node.get_input_schema().clone();
let fields = input_schema.iter().map(Field::from).collect::<Vec<Field>>();
Ok(Box::new(ExchangeExecutor {
proto_sources,View on GitHub (pinned to 6469eb736d)
Solutions
- Ensure the caller tracks the last truncated barrier epoch and skips repeats or regressions.
- Persist/restore truncate progress correctly across restarts so the first post-recovery truncate is newer.
- Audit the epoch source (barrier manager / Hummock watermark) for out-of-order barrier delivery.
- If the regression is expected (e.g. testing), reset or recreate the reader instead of re-truncating.
Example fix
// before
reader.truncate(TruncateOffset::Barrier { epoch });
// after
if !matches!(reader.last_truncate_offset(), Some(prev) if TruncateOffset::Barrier { epoch } <= prev) {
reader.truncate(TruncateOffset::Barrier { epoch });
} Defensive patterns
Strategy: validation
Validate before calling
// rust
fn safe_truncate_barrier(reader: &mut KvLogStoreReader, epoch: u64) -> anyhow::Result<()> {
let offset = TruncateOffset::Barrier { epoch };
if let Some(prev) = reader.last_truncate_offset() {
anyhow::ensure!(offset > prev, "skip historical truncate {:?} (prev {:?})", offset, prev);
}
reader.truncate(offset)
} Prevention
- Only issue historical truncations for strictly newer barrier epochs.
- Persist the last truncated barrier epoch with actor state.
- Monitor barrier epoch ordering from the barrier manager.
When it happens
Trigger: Calling `KvLogStoreReader::truncate(TruncateOffset::Barrier { epoch })` while `offset <= self.truncate_offset` and `offset.epoch() < first_write_epoch` (historical region). Thrown at reader.rs:543.
Common situations: Restarted stream actor replays an old barrier epoch; epoch watermark regression after meta failover; mismatch between the epoch the executor believes it has consumed and what the reader last truncated.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Filter can only receive bool array
- unable to send barrier
- Iceberg metadata scan received a non-Iceberg connector
- Iceberg source should not have input executor!
- missing lagging barriers for direct log-store start from sna
AI-assisted analysis of risingwavelabs/risingwave@6469eb736d (2026-09-11).
Data as JSON: /api/errors/73cfaf10cf96d335.
Report an issue: GitHub.