risingwavelabs/risingwave · error · SinkError
pk-index merger flush before start_seed
Error message
pk-index merger flush before start_seed
What it means
await_ready resolves the position-delete handler's inner state; it only exists once the handler transitions out of Unseeded via start_seed. This error means a flush was attempted while the merger's staging was never seeded, i.e. the flush path ran before start_seed populated HandlerInner::Ready. It is an internal lifecycle-ordering violation: flush_inner should never be called in Unseeded state.
Solutions
- Ensure start_seed is invoked before any flush_inner call in the handler's lifecycle.
- Check logs for a failed/aborted start_seed (e.g. the epoch-wait error) that left the handler Unseeded.
- If flushing must be optional pre-seed, guard the caller to skip flush when inner is Unseeded.
- Report as a bug with the executor's message sequence if the normal barrier flow triggers it.
Example fix
// before
if matches!(self.inner, HandlerInner::Unseeded) {
self.await_ready().await?;
}
// after
if matches!(self.inner, HandlerInner::Unseeded) {
return Ok(()); // nothing to flush before seeding
} Defensive patterns
Strategy: validation
Validate before calling
// before flushing, confirm the handler is seeded
if matches!(handler.state(), HandlerState::Unseeded) {
// skip flush or trigger start_seed first
} Type guard
fn is_seeded(inner: &HandlerInner) -> bool {
matches!(inner, HandlerInner::Ready(_) | HandlerInner::Seeding(_))
} Try / catch
match flush_inner(...).await {
Err(SinkError::Iceberg(e)) if format!("{:e}", e).contains("flush before start_seed") => {
warn!("handler not seeded yet; skipping flush");
}
other => other?,
} Prevention
- Always run start_seed before enabling the flush path.
- Log handler state transitions (Unseeded → Seeding → Ready) for diagnosis.
- Treat this error as an ordering bug: audit any code path that can flush during bootstrap.
When it happens
Trigger: Raised in await_ready (called by flush_inner) when self.inner is HandlerInner::Unseeded — a flush request arrives before start_seed has waited for the sink commit epoch and loaded/seeded the table.
Common situations: A code path invoking flush during initial bootstrap before the merger finished waiting for the sink's commit epoch; recovery/replay invoking flush earlier than expected; a race where flush is triggered by backpressure before seeding completes.
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
- iceberg catalog did not reflect committed pk-index snapshot
- Iceberg compaction maintenance was cleared for sink
- iceberg pk-index writer
- iceberg pk-index writer input must be a merge after actor…
- iceberg pk-index writer
AI-assisted analysis of risingwavelabs/risingwave@6469eb736d (2026-09-11).
Data as JSON: /api/errors/aa653b11bc541eb6.
Report an issue: GitHub.
Appendix: source
Thrown at src/stream/src/executor/iceberg_with_pk_index/position_delete_handler_impl.rs:139
/// Await the background seed, transitioning `Seeding -> Ready`. Errors if called before
/// `start_seed`.
async fn await_ready(&mut self) -> SinkResult<&mut SeededState> {
if matches!(self.inner, HandlerInner::Seeding(_)) {
// Take the handle out before awaiting so an errored seed leaves `Unseeded` (a clean error
// on any later flush) rather than a `Seeding` holding a consumed `JoinHandle` that would
// panic if re-polled.
let HandlerInner::Seeding(handle) =
std::mem::replace(&mut self.inner, HandlerInner::Unseeded)
else {
unreachable!("guarded by matches! above");
};
let state = handle.await.map_err(|e| SinkError::Iceberg(anyhow!(e)))??;
self.inner = HandlerInner::Ready(state);
}
match &mut self.inner {
HandlerInner::Ready(state) => Ok(state),
HandlerInner::Unseeded => Err(SinkError::Iceberg(anyhow!(
"pk-index merger flush before start_seed"
))),
HandlerInner::Seeding(_) => unreachable!("just transitioned out of Seeding"),
}
}
async fn flush_inner(&mut self) -> SinkResult<Option<SinkMetadata>> {
if self.pending.is_empty() {
return Ok(None);
}
let config = self.config.clone();
let pending = std::mem::take(&mut self.pending);
let state = self.await_ready().await?;
let mut delete_files = Vec::with_capacity(pending.len());
let mut overwrite_files = Vec::new();
View on GitHub (pinned to 6469eb736d)