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

  1. Ensure the write side never splits an update pair across epoch boundaries/barriers — flush pairs atomically.
  2. Check serialization ordering so UpdateDelete and UpdateInsert rows are stamped with the same barrier epoch.
  3. Inspect the seq ids around the pair in the KV store to detect truncation or partial writes.
  4. 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

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


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