risingwavelabs/risingwave · error

invalid backfill state: backfill_finished

Error message

invalid backfill state: backfill_finished

What it means

Thrown by CdcBackfillStateV2::restore_state when element [1] of the persisted state row is not a Bool, so the backfill_finished flag cannot be decoded. It guards against reading a state row written with a different layout or corrupted value.

Source

Thrown at src/stream/src/executor/backfill/cdc/state_v2.rs:70

    pub async fn init_epoch(&mut self, epoch: EpochPair) -> StreamExecutorResult<()> {
        self.state_table.init_epoch(epoch).await
    }

    /// Restore the backfill state from storage
    pub async fn restore_state(&mut self, split_id: i64) -> StreamExecutorResult<CdcStateRecord> {
        let key = Some(split_id);
        match self
            .state_table
            .get_row(row::once(key.map(ScalarImpl::from)))
            .await?
        {
            Some(row) => {
                tracing::info!("restored cdc backfill state: {:?}", row);
                let state = row.into_inner().into_vec();
                let is_finished = match state[1] {
                    Some(ScalarImpl::Bool(val)) => val,
                    _ => return Err(anyhow!("invalid backfill state: backfill_finished").into()),
                };
                let row_count = match state[2] {
                    Some(ScalarImpl::Int64(val)) => val,
                    _ => return Err(anyhow!("invalid backfill state: row_count").into()),
                };
                let (cdc_offset_low, cdc_offset_high) = if !self.is_legacy_state {
                    let cdc_offset_low = match state[3] {
                        Some(ScalarImpl::Jsonb(ref jsonb)) => {
                            serde_json::from_value(jsonb.clone().take()).unwrap()
                        }
                        None => None,
                        _ => return Err(anyhow!("invalid backfill state: cdc_offset_low").into()),
                    };
                    let cdc_offset_high = match state[4] {
                        Some(ScalarImpl::Jsonb(ref jsonb)) => {
                            serde_json::from_value(jsonb.clone().take()).unwrap()
                        }
                        None => None,

View on GitHub (pinned to 6469eb736d)

Solutions

  1. Cancel and recreate the streaming job so the CDC backfill is rebuilt with a fresh state row
  2. Verify the writer/reader versions match; downgrade to the version that wrote the state and let backfill finish before upgrading
  3. Inspect the internal state table row to confirm the field type mismatch
  4. Restore the state table from backup if the row was corrupted
Defensive patterns

Strategy: validation

Validate before calling

let is_finished = matches!(state[STATE_FINISHED_IDX], Some(ScalarImpl::Bool(_)));
if !is_finished { /* treat state as incompatible; recreate job */ }

Prevention

When it happens

Trigger: Restoring a v2 CDC backfill state where state[1] is None or holds a non-Bool ScalarImpl (e.g. Int64/Bytea from a legacy or foreign row layout).

Common situations: Version upgrade/migration where the state schema changed; a row written by the legacy no-shuffle backfill being read by the v2 state reader; corrupted persisted state after a crash mid-write.

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


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