risingwavelabs/risingwave · error

provisional match references seq

Error message

provisional match references seq {:?} not present in the row buffer

What it means

While emitting a ready provisional match, `emit_ready` binary-searches the row buffer for the match's starting sequence number and it is not found. A provisional match referencing an unfed seq is a matcher-invariant violation; the executor fails loudly because breaking quietly would make the partition never emit or evict again while its state grows unbounded.

Solutions

  1. Check whether the job was recovered from a checkpoint across a version change; re-create the MV/query to rebuild clean state.
  2. Look for recent changes to provisional-match bookkeeping or dead-prefix pruning and bisect the regression.
  3. Capture the partition state (run rows vs provisional matches) and file a bug with the fragment id — this is an internal invariant failure.
Defensive patterns

Strategy: try-catch

Try / catch

// Fail the actor with context so the invariant violation is diagnosable:
let Ok(start) = run.rows.binary_search_by_key(&start_seq.0, |r| r.seq) else {
    return Err(anyhow!("provisional match seq {:?} missing from buffer; restarting partition", start_seq).into());
};

Prevention

When it happens

Trigger: emit_ready encounters a provisional match whose start_seq is absent from run.rows — e.g. rows were pruned/evicted while a provisional match still references them, or recovery re-fed rows in a different order than the invariant assumes.

Common situations: Bugs in dead-prefix pruning leaving stale provisional matches; state recovery (checkpoint/restore) reordering feeds; state table corruption from version skew.

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


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

Appendix: source

Thrown at src/stream/src/executor/match_recognize/executor.rs:782

            .map(|m| (m.start_seq, m.labels.len()))
        {
            // `end_seq` is a synthetic exclusive bound (last row's seq + 1), not a real row's
            // seq; the span length is the label count (one label per matched row). The scan runs
            // from 0, NOT from the matcher's resume position: `provisional()` leads with FROZEN
            // but not-yet-emitted matches, whose starts sit before the resume position (it points
            // past the LAST frozen match).
            let resume_pos = run.matcher.resume_pos().min(run.rows.len());
            // The gap check below walks `[resume_pos, start)`; positions the freeze already proved
            // dead (`dead_prefix_end`, monotone under appends) need no walk, so start it past them.
            let gap_from = resume_pos.max(run.matcher.dead_prefix_end());
            // `seq` is strictly increasing in buffer position — rows are appended in mint order and
            // the recovery rebuild re-feeds them in key order — the same invariant the dead-prefix
            // prune already binary-searches on.
            let Ok(start) = run.rows.binary_search_by_key(&start_seq.0, |r| r.seq) else {
                // A provisional match referencing an unfed seq is a matcher-invariant violation.
                // Fail loud: breaking here instead would re-hit the same match on every visit —
                // the partition would silently never emit or evict again while its state grows.
                return Err(anyhow::anyhow!(
                    "provisional match references seq {:?} not present in the row buffer",
                    start_seq
                )
                .into());
            };
            let end = start + labels_len;
            debug_assert!(end <= run.rows.len());

            let within_final = if let Some(w) = watermark {
                run.rows[start].deadline.closed_at(w)
            } else {
                false
            };
            // A spent budget cannot decide a STRUCTURAL hold: every walk short-circuits to
            // "undecided", which the gate must read as hold. A WITHIN-final match is different —
            // its window has closed, so `match_is_final` returns FINAL for it before spending
            // anything, and it needs no walk at all. Those MUST still be drained: leaving one
            // withheld while `prune_dead_prefix` treats its window-closed start row as dead is how

View on GitHub (pinned to 6469eb736d)