{"record":{"id":"101fc69209abecbb","repo":"risingwavelabs/risingwave","slug":"provisional-match-references-seq-not-present","errorCode":null,"errorMessage":"provisional match references seq {:?} not present in the row buffer","messagePattern":"provisional match references seq (.+?) not present in the row buffer","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/stream/src/executor/match_recognize/executor.rs","lineNumber":782,"sourceCode":"            .map(|m| (m.start_seq, m.labels.len()))\n        {\n            // `end_seq` is a synthetic exclusive bound (last row's seq + 1), not a real row's\n            // seq; the span length is the label count (one label per matched row). The scan runs\n            // from 0, NOT from the matcher's resume position: `provisional()` leads with FROZEN\n            // but not-yet-emitted matches, whose starts sit before the resume position (it points\n            // past the LAST frozen match).\n            let resume_pos = run.matcher.resume_pos().min(run.rows.len());\n            // The gap check below walks `[resume_pos, start)`; positions the freeze already proved\n            // dead (`dead_prefix_end`, monotone under appends) need no walk, so start it past them.\n            let gap_from = resume_pos.max(run.matcher.dead_prefix_end());\n            // `seq` is strictly increasing in buffer position — rows are appended in mint order and\n            // the recovery rebuild re-feeds them in key order — the same invariant the dead-prefix\n            // prune already binary-searches on.\n            let Ok(start) = run.rows.binary_search_by_key(&start_seq.0, |r| r.seq) else {\n                // A provisional match referencing an unfed seq is a matcher-invariant violation.\n                // Fail loud: breaking here instead would re-hit the same match on every visit —\n                // the partition would silently never emit or evict again while its state grows.\n                return Err(anyhow::anyhow!(\n                    \"provisional match references seq {:?} not present in the row buffer\",\n                    start_seq\n                )\n                .into());\n            };\n            let end = start + labels_len;\n            debug_assert!(end <= run.rows.len());\n\n            let within_final = if let Some(w) = watermark {\n                run.rows[start].deadline.closed_at(w)\n            } else {\n                false\n            };\n            // A spent budget cannot decide a STRUCTURAL hold: every walk short-circuits to\n            // \"undecided\", which the gate must read as hold. A WITHIN-final match is different —\n            // its window has closed, so `match_is_final` returns FINAL for it before spending\n            // anything, and it needs no walk at all. Those MUST still be drained: leaving one\n            // withheld while `prune_dead_prefix` treats its window-closed start row as dead is how","sourceCodeStart":764,"sourceCodeEnd":800,"githubUrl":"https://github.com/risingwavelabs/risingwave/blob/6469eb736d691e8e9b8a419a57edd6429ca77417/src/stream/src/executor/match_recognize/executor.rs#L764-L800","documentation":"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.","triggerScenarios":"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.","commonSituations":"Bugs in dead-prefix pruning leaving stale provisional matches; state recovery (checkpoint/restore) reordering feeds; state table corruption from version skew.","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."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"// Fail the actor with context so the invariant violation is diagnosable:\nlet Ok(start) = run.rows.binary_search_by_key(&start_seq.0, |r| r.seq) else {\n    return Err(anyhow!(\"provisional match seq {:?} missing from buffer; restarting partition\", start_seq).into());\n};","preventionTips":["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."],"tags":["match-recognize","state-management","internal-invariant"],"backgroundTag":"internal-invariant-violation","analyzedSha":"6469eb736d691e8e9b8a419a57edd6429ca77417","analyzedAt":"2026-09-11T21:06:21.487Z","contentChangedAt":"2026-09-11T21:06:21.487Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}