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
- Check whether the job was recovered from a checkpoint across a version change; re-create the MV/query to rebuild clean state.
- Look for recent changes to provisional-match bookkeeping or dead-prefix pruning and bisect the regression.
- 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
- Ensure provisional matches are dropped whenever their rows are pruned/evicted.
- Keep recovery re-feed order stable (key order) as the pruning binary search assumes.
- Add invariant tests: every provisional match's start_seq must exist in run.rows.
- Rebuild state cleanly after cross-version recovery rather than resuming stale provisional matches.
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
- MATCH_RECOGNIZE SUM measure slot has no kernel
- AFTER MATCH SKIP TO FIRST/LAST missing its target variable
- All valid CDC connectors should have returned by now
- BatchMatchRecognize is not implemented yet
- BatchPosixFsReader should not be used
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 howView on GitHub (pinned to 6469eb736d)