Skip to content

compute: delay retracting a dropped export's lifecycle rows [deferred, unsound] - #38417

Draft
antiguru wants to merge 2 commits into
claude/hydration-visibility-compute-js1ycmfrom
claude/lifecycle-retraction-delay-js1ycm
Draft

compute: delay retracting a dropped export's lifecycle rows [deferred, unsound]#38417
antiguru wants to merge 2 commits into
claude/hydration-visibility-compute-js1ycmfrom
claude/lifecycle-retraction-delay-js1ycm

Conversation

@antiguru

@antiguru antiguru commented Aug 22, 2026

Copy link
Copy Markdown
Member

Deferred. Not ready, and not close to ready. Do not pick this up without reading the blocker below — the two-dyncfg design in this PR does not survive it.

Blocker: delayed retraction is unsound while ids can be reused

The delay retracts an export's rows at a future timestamp while removing its ExportState immediately. Nothing records that a retraction is pending, so a new export with the same GlobalId inside the window inserts a fresh row and the relation ends up holding two rows with the same (export_id, worker_id, event) from different incarnations. That breaks the at-most-once property the relation documents and that test/testdrive/compute-lifecycle-events.td asserts under max-tries=1, and it corrupts any duration a reader computes with max(occurred_at) FILTER (...).

Transient ids are reused because TransientIdGen::new() starts at 1 per process (src/repr/src/global_id.rs:123-129), so a plain environmentd restart — replicas survive and reconcile, transient dataflows are dropped, new generation reissues t1… — collides inside the window.

Non-transient ids can also be reused during reconciliation, so the window is not a transient-only problem. That kills the obvious fix: excluding transient exports from the delay does not make this sound, and the split between compute_lifecycle_retraction_delay and compute_lifecycle_retraction_delay_transient is not the axis that matters. Any future attempt needs to make retraction and re-insertion of the same id ordered — for example a pending_retractions map flushed at the current timestamp when handle_export sees an id whose retraction is still outstanding — before the delay is worth having at all.

Note the base PR is unaffected: it retracts at the drop timestamp, so no window exists in which two incarnations of an id coexist.

Two further findings, unaddressed

  • An unclamped compute_lifecycle_retraction_delay panics the replica in ts_at's expect("must fit"). There is a lower clamp to the logging interval but no upper one, and ALTER SYSTEM SET reaches it.
  • The delay makes the relation carry rows whose export_id is absent from mz_compute_exports_per_worker for up to the delay. The ontology description was written before dropped existed and still implies a lifecycle row means a live export, and the design doc and the testdrive orphan check disagree about whether an orphaned row is expected.

Original motivation

The lifecycle log retracts an export's rows the moment it is dropped, so an object's history vanishes with the object. That loses exactly the episodes worth looking at: a dataflow dropped before it hydrated, or one whose hydration is the reason someone opened the log. A short-lived dataflow can also come and go inside one introspection interval and leave no trace.

Part of CPU-226.

What is in the diff

Two commits on top of #38403: the retraction delay with a dropped stage and two dyncfgs, and an unrelated design-doc correction about catalog_server_explain.slt.

The dropped stage is the part worth keeping from this attempt. Without it a lingering row says only that an export reached some stage, not whether it still exists, so "hydrated but never written" cannot be told apart from "dropped before it wrote".

The storage reasoning also stands independently of the soundness problem: a transient export exists per peek and per subscribe, so at eight workers and two hundred peeks per second a five-minute window holds around 1.4 million rows, hundreds of megabytes, while a user object is dropped by DDL and a thousand drops inside the window is under ten megabytes. Whatever replaces this will still need to treat those two populations differently, just not by delay alone.

@linear-code

linear-code Bot commented Aug 22, 2026

Copy link
Copy Markdown

CPU-226

@antiguru
antiguru force-pushed the claude/lifecycle-retraction-delay-js1ycm branch 3 times, most recently from c56501a to 5981d21 Compare August 24, 2026 12:14
claude added 2 commits August 24, 2026 13:02
The lifecycle log retracts an export's rows the moment it is dropped, so an
object's history vanishes with the object. That loses exactly the episodes worth
looking at: a dataflow that was dropped before it hydrated, or one whose
hydration is the reason someone is reading the log at all. It also means a short
lived dataflow can come and go inside one introspection interval and leave no
trace.

Delay the retraction instead, and record the drop as a stage of its own.

Two delays, because the two populations churn at completely different rates. A
transient export is created per peek and per subscribe, so retaining those for
minutes costs hundreds of megabytes on a busy replica: at eight workers and two
hundred peeks per second, a five minute window holds around 1.4 million rows.
A few seconds is enough for a reader to observe them and costs single digit
megabytes. A user object is dropped by DDL, so even a thousand drops inside the
window is under ten megabytes, and there the history is worth keeping.

    compute_lifecycle_retraction_delay            default 5 min
    compute_lifecycle_retraction_delay_transient  default 5 s

Both are floored at the logging interval in code rather than by convention. The
demux rounds update timestamps up to that interval, so a shorter delay can round
to the same timestamp as the insertion and leave the rows never separately
visible, which would defeat the point.

The delays are read per batch rather than at construction, so an
`UpdateConfiguration` command takes effect without recreating the logging
dataflow, and a per-replica override applies to a replica under investigation
without touching the rest.

The `dropped` stage is what makes the delay readable. Without it a lingering row
says only that an export reached some stage, not whether it still exists, so
"hydrated but never written" could not be told apart from "dropped before it
wrote". With it the object's last event names its fate and the delay is pure
retention, and a row whose export id no longer appears in `mz_objects` is
explained by its own `dropped` event rather than reading as a leak.

Part of CPU-226

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ZVCMBSLdxzGus78ZKWhZz
The design doc says `catalog_server_explain.slt` needs no change when a builtin
log is added, on the grounds that its query filters `o.id NOT LIKE 'si%'` and so
never enumerates a per-replica introspection index. The filter is real, but the
conclusion does not follow. The plans already in the file embed the inlined
builtin `VALUES` sets as `Constant (N rows)` nodes, so every count over a catalog
relation that gained a row moves, and adding an ontology entity and link moves two
more.

Record the question that catches this: not whether a new plan appears, but whether
the existing plans change.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ZVCMBSLdxzGus78ZKWhZz
@antiguru
antiguru force-pushed the claude/lifecycle-retraction-delay-js1ycm branch from 5981d21 to 4541a11 Compare August 24, 2026 13:02
@antiguru antiguru changed the title compute: delay retracting a dropped export's lifecycle rows compute: delay retracting a dropped export's lifecycle rows [deferred, unsound] Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants