risingwavelabs/risingwave · error
Chunk size can't be zero!
Error message
Chunk size can't be zero!
What it means
While merging a decoded `UpdateDelete` row with its paired `UpdateInsert` row, the two halves must share the same epoch. The decoder found `row_meta.epoch` of the UpdateDelete differs from the epoch of the following UpdateInsert, meaning the update pair was split across epochs — an invariant the merge logic cannot safely handle, so it errors out.
Source
Thrown at src/batch/executors/src/executor/join/chunked_data.rs:101
chunk_id: self.chunk_id,
row_id: self.row_id + 1,
}
}
}
}
impl<V> ChunkedData<V> {
pub(super) fn with_chunk_sizes<C>(chunk_sizes: C) -> Result<Self>
where
C: IntoIterator<Item = usize>,
V: Default,
{
let chunk_sizes = chunk_sizes.into_iter();
let mut chunk_offsets = Vec::with_capacity(chunk_sizes.size_hint().0 + 1);
let mut cur = 0usize;
chunk_offsets.push(0);
for chunk_size in chunk_sizes {
ensure!(chunk_size > 0, "Chunk size can't be zero!");
cur += chunk_size;
chunk_offsets.push(cur);
}
let mut data = Vec::with_capacity(cur);
data.resize_with(cur, V::default);
Ok(Self {
data,
chunk_offsets,
})
}
fn index_in_data(&self, index: RowId) -> usize {
self.chunk_offsets[index.chunk_id()] + index.row_id()
}
pub(super) fn all_row_ids(&self) -> impl Iterator<Item = RowId> + '_ {View on GitHub (pinned to 6469eb736d)
Solutions
- Ensure the write side never splits an update pair across epoch boundaries/barriers — flush pairs atomically.
- Check serialization ordering so UpdateDelete and UpdateInsert rows are stamped with the same barrier epoch.
- Inspect the seq ids around the pair in the KV store to detect truncation or partial writes.
- If it appears only after failover, verify replay does not re-write one half of a pair with a new epoch.
Example fix
// before // writer: buffer delete and insert halves independently, may flush across barriers // after // buffer both halves of an update together and flush them under one epoch buffer.push((delete_half, insert_half)); // single epoch, single flush
Defensive patterns
Strategy: validation
Validate before calling
// rust
// on the write side, verify the pair before buffering to the log store
anyhow::ensure!(delete_meta.epoch == insert_meta.epoch,
"update pair spans epochs {} != {}", delete_meta.epoch, insert_meta.epoch); Try / catch
// rust
match reader.next_op().await {
Ok(op) => handle(op),
Err(e) if e.to_string().contains("UpdateDelete epoch") => {
tracing::error!("update pair split across epochs in log store: {e:#}");
// fatal: investigate writer flush boundaries / corruption
}
Err(e) => return Err(e),
} Prevention
- Flush update delete/insert pairs atomically under one barrier epoch.
- Add a write-side invariant that a pair never crosses a flush boundary.
- After failover, verify replay does not re-stamp one half of a pair with a new epoch.
When it happens
Trigger: Reading/decoding a buffered update pair where the stored `LogStoreOp::Row { op: Op::UpdateDelete }` and the subsequent `LogStoreOp::Row { op: Op::UpdateInsert }` have differing `row_meta.epoch`. Thrown at serde.rs:812.
Common situations: A barrier flushed between the delete and insert halves of an update; a writer bug that lets epoch change mid-pair; store corruption or partial write dropping one half and substituting a row from another epoch.
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
- Iceberg metadata scan should not have input executors
- Iceberg metadata type is unspecified
- GetChannelDeltaStatsExecutor should have no child!
- Iceberg metadata scan received a non-Iceberg connector
- file_scan_tasks must be Some
AI-assisted analysis of risingwavelabs/risingwave@6469eb736d (2026-09-11).
Data as JSON: /api/errors/eaa1fef370e0b9a2.
Report an issue: GitHub.