feat: end-to-end truncation - #3132
Conversation
91c351f to
1be6962
Compare
jgraettinger
left a comment
There was a problem hiding this comment.
Great! We talked over VC about re-structuring the journal carrier (folding into ACKs with causal hints). A few other comments below.
1be6962 to
23983ca
Compare
|
Addressed the comments so far, and also rebased & pushed a couple other substantial changes:
Ran a fresh claude review and manual E2E tests on these pushed changes as well. |
|
Ran the combiner benchmarks before and after this change.
|
jgraettinger
left a comment
There was a problem hiding this comment.
A few minor comments below (plus our Slack thread re: the combiner, which is my only real concern). Everything else makes sense!
| // propagation of flush and progress reporting. | ||
| let is_append = is_append | ||
| && *clock >= binding.not_before | ||
| && *clock >= read_state.truncated_at |
There was a problem hiding this comment.
confirming my understanding: binding.not_before is (only) the built spec's not-before, and does not updated with the journals' truncation label (carried only in read_state.truncated_at). Thus we must check both.
There was a problem hiding this comment.
Yep that's right. They are separate, with the not_before being a property of the binding and the truncated_at being a proper of the journal.
| stall timeout: it fires only when no progress at all occurs between | ||
| two consecutive ticks. | ||
|
|
||
| A peek also carries `latest_backfill_begin` eagerly (cloned from |
| // caller. | ||
| for cas_retries in 0..10 { | ||
| let listing = retry_transient("list partitions", || { | ||
| client.list(broker::ListRequest { |
There was a problem hiding this comment.
It'd be more idiomatic to use a list_watch or list_watch_with here, which can be polled again to retry, already handles back-off internally, and will return a restatement of the journals after every Ok change. You'd use its items to drive convergence retries.
There was a problem hiding this comment.
Switched to using list_watch, much better.
| anyhow = { workspace = true } | ||
| base64 = { workspace = true } | ||
| bytes = { workspace = true } | ||
| chrono = { workspace = true } |
There was a problem hiding this comment.
nit: I'm not actually seeing chrono in use in publisher changes ?
There was a problem hiding this comment.
Left over from some prior iterations...removed.
| persist_fut: Option<BoxFuture<'static, anyhow::Result<(crate::shard::RocksDB, Vec<String>)>>>, | ||
| labels_apply_fut: Option<BoxFuture<'static, (P, BTreeMap<u32, u64>, tonic::Result<()>)>>, | ||
| persist_fut: Option< | ||
| BoxFuture< |
There was a problem hiding this comment.
nit: factor out a type alias here and below?
| // sub-range shard that inherited `active_backfills` (e.g. a split | ||
| // mid-backfill) must not re-apply labels it can never clear — it | ||
| // never receives the BackfillComplete that removes them. | ||
| if !self.is_single_shard || !self.labels_dirty || self.active_backfills.is_empty() { |
There was a problem hiding this comment.
I'm wondering if we should make the rule that shard zero, alone, manages truncation signals. that would allow a CDC capture to scale out by having shard zero manage the WAL, while other shards perform backfill fetches.
There was a problem hiding this comment.
Seems reasonable, I could see that working out. Updated.
| })?; | ||
| memtable.add(binding_index as u16, doc, true)?; | ||
|
|
||
| // Classify by the loaded document's UUID clock. As a fall-back for |
There was a problem hiding this comment.
Should we make this fail faster? A concern is that materialization tasks will appear to work fine, and then blow up on a first source truncation, right?
There was a problem hiding this comment.
Yeah, we probably should, I was thinking not right away though. With it like this, pre-existing "no flow document" materializations that don't load a _meta/uuid will keep working until captures start to emit truncations. I think the implementation on the connector side will need to do at least materializations first, with the "no flow document" cases always emitting a clock, and then we can harden this.
|
Pushed a commit that includes a different strategy for the combiner changes, instead of adding document clock to the meta. It adds a |
|
Also pushed an additional commit to incorporate the review feedback. I'll aim to do one final rebase prior to merging that will amend the two |
jgraettinger
left a comment
There was a problem hiding this comment.
Changes look great! All of my comments are resolved. Ready to stamp after you rebase.
| } else if !entry.meta.front() { | ||
| return false; | ||
| } | ||
| entry.meta.set_stale(); |
There was a problem hiding this comment.
Nice, I hadn't fully considered the implications of a connector that streams Loaded responses on demand, which you caught here and have handled 👍
I'm expecting this means we need to still surface front() documents of cut-off segments for purposes of the Exists flag. (confirmed)
Backfill truncation lets a capture re-backfill a binding and mark the
previously-captured data as stale. It needs new protocol surface:
capture BackfillBegin / BackfillComplete responses, materialize Flush
backfill notifications, runtime Recover.active_backfills and a Persist
active-backfill change, and shuffle Frontier latest_backfill_{begin,
complete} clocks keyed by binding index.
A reader must skip the stale pre-backfill prefix of a journal once its binding is re-backfilled. Record the boundary as an `estuary.dev/truncated-at` label: a fixed-width hex Gazette clock that sorts lexically by value and only ever advances. dekaf honors it by raising its effective not_before to the max truncated-at of its journals.
A materialization that loads a stale row — one published before its binding's backfill boundary — must not reduce it into the fresh, current document for the same key, or the superseded value would leak forward. Expose an explicit Accumulator::truncate(binding): the live MemTable drops the binding's pre-boundary sources and flags its Loaded fronts stale, while already-spilled segments are fenced by a per-binding ordinal cutoff. Staleness is a single Meta flag bit, keeping HeapEntry at 32 bytes and the spill entry header at 24.
Fold the begin/complete clocks of backfill markers into the checkpoint frontier, keyed by binding index. The markers ride the bodies of immediately-committed ACK documents, so they never participate in causal-hint sequencing; their clocks are instead max-reduced per binding across a checkpoint delta and surface on a resolved NextCheckpoint.
…d-at labels A backfill marker rides the body of a transaction's ACK documents, broadcast to every partition journal of the binding under a single shared clock, so even a constrained reader observes it. Un-upgraded readers skip ACKs, so the marker is invisible to them by construction and commits atomically with the connector checkpoint. Also apply the `estuary.dev/truncated-at` label to a binding's journals.
Backfill state must survive transaction rotation and leader restart. Add key families, split by task type: `AB:` holds a capture's in-progress active backfills (the clocks that drive the truncated-at journal label), while `BB:`/`BC:` (and hinted `HB:`/`HC:`) hold a materialization's cumulative begin/complete boundary keyed by state_key.
A capture connector may signal BackfillBegin / BackfillComplete for a binding. Each signal must stand alone in its connector checkpoint, so isolate it in its own transaction, publish the marker on that transaction's ACK documents — the begin's assigned clock is the authoritative truncated_at — and stage the active-backfill change atomically with the connector checkpoint.
Carry the per-binding truncation boundary from the frontier into the shard. The scan drops source documents older than the boundary, and a loaded row older than it is classified prior-generation so the combiner discards it instead of reducing stale state forward — its current-generation sibling is then stored as an update. The leader broadcasts each begin/complete notification to every shard's connector.
Regenerating protobufs reformats the checked-in bindings; run `cargo fmt --all` afterward so the diff carries only real changes.
07dcc7a to
b652d77
Compare
|
re: the agents.md change, fyi I added cargo fmt to the build:rust-protobufs task a bit ago |
## What's Changed * docs: surface MCP and agent skills for coding agents by @jwhartley in estuary/flow#3093 * dekaf: serve the document containing a mid-document fetch offset by @jshearer in estuary/flow#3150 * runtime: let docker atomically assign published connector host ports by @dgreer-dev in estuary/flow#3162 * runtime-next: discard hinted frontier when committed frontier is rebuilt by @jgraettinger in estuary/flow#3165 * docs/source-postgres: Sync statement_timeout change by @willdonnelly in estuary/flow#3168 * Provide capture/materializations created_at date by @dgreer-dev in estuary/flow#3160 * docs: add Processing order section for materialization binding priority by @jwhartley in estuary/flow#3171 * docs: reorganize agent skills page by plugin, add derivations and schema by @jwhartley in estuary/flow#3170 * go.mod: bump gazette to latest by @williamhbaker in estuary/flow#3180 * Service Accounts by @GregorShear in estuary/flow#3058 * Bound too-large error messages over RPC by @dgreer-dev in estuary/flow#3182 * control-plane: guarded, auto-expiring temporary support access to restricted tenants by @skord in estuary/flow#3115 * private links: model links as rows with controller-observed status by @jshearer in estuary/flow#3063 * Adding support for an stripe web hook endpoint. by @bbartman in estuary/flow#3174 * docs: Dekaf consumer behaviors + Spark Structured Streaming guidance by @jwhartley in estuary/flow#3094 * shuffle: log canonical journal name in slice read events by @jgraettinger in estuary/flow#3190 * runtime: persist the max-key fail-safe sentinel at adoption by @jgraettinger in estuary/flow#3191 * local: make flow-plane-link robust to control-plane agent start-up by @jgraettinger in estuary/flow#3158 * gazette: surface fragment store diagnostic on FRAGMENT_STORE_UNHEALTHY by @jgraettinger in estuary/flow#3159 * docs: add warning about source-stripe-native's events API version by @nicolaslazo in estuary/flow#3166 * agent-api: Add `STRIPE_WEBHOOK_SECRET` to cloud run config by @jshearer in estuary/flow#3196 * shuffle, gazette: guard against wedged reads by @jgraettinger in estuary/flow#3193 * runtime-next: gate task-log stream by configured log level by @williamhbaker in estuary/flow#3198 * flowctl: preview-next appends a final drain session by @mdibaiee in estuary/flow#3195 * Allow localhost:3000 origin for the local control-plane agent by @GregorShear in estuary/flow#3206 * control-plane-api: don't lock injected ops collections during publications by @skord in estuary/flow#3164 * runtime-next: force txn close at any usage ceiling, bypassing min duration by @jgraettinger in estuary/flow#3203 * shuffle: replay gapped producers on restart by @jgraettinger in estuary/flow#3202 * docs: source-zuora by @Alex-Bair in estuary/flow#3204 * dekaf: only read back when a fetch offset lands inside a document by @mdibaiee in estuary/flow#3205 * mise: dev-VM zone configurability (create fallback, zone-agnostic SSH, vm:move-gcp) by @skord in estuary/flow#3208 * docs/postgres: Discovery Filters by @willdonnelly in estuary/flow#3215 * docs: document materialize-s3-iceberg nanosecond_timestamps option by @jacobmarble in estuary/flow#3199 * runtime v2 flag for new captures and derivations by @williamhbaker in estuary/flow#3219 * runtime-next: converge startup reconciliation via rescan Persists by @jgraettinger in estuary/flow#3218 * build(deps): bump fast-uri from 3.1.0 to 3.1.4 in /site by @dependabot[bot] in estuary/flow#3223 * build(deps): bump svgo from 3.3.3 to 3.3.4 in /site by @dependabot[bot] in estuary/flow#3224 * build(deps): bump dompurify from 3.3.3 to 3.4.12 in /site by @dependabot[bot] in estuary/flow#3222 * build(deps): bump shell-quote from 1.8.3 to 1.10.0 in /site by @dependabot[bot] in estuary/flow#3213 * build(deps): bump body-parser from 1.20.4 to 1.20.6 in /site by @dependabot[bot] in estuary/flow#3212 * build(deps): bump webpack-dev-server from 5.2.2 to 5.2.6 in /site by @dependabot[bot] in estuary/flow#3211 * build(deps): bump websocket-driver from 0.7.4 to 0.7.5 in /site by @dependabot[bot] in estuary/flow#3183 * Docs: MySQL discovery filters by @aeluce in estuary/flow#3217 * docs: source-smartsheet by @nicolaslazo in estuary/flow#3228 * control-plane-api: surface per-store health diagnostics in storage-mapping mutations by @GregorShear in estuary/flow#3181 * async-process: handle reaping-task JoinError in Child::wait() without panicking by @jgraettinger in estuary/flow#3225 * feat: end-to-end truncation by @williamhbaker in estuary/flow#3132 * docs: source-zuora uses the AQuA API now by @Alex-Bair in estuary/flow#3237 * Improve storage mapping GraphQL results by @GregorShear in estuary/flow#3200 * control-plane: add `closed` flag to data planes by @GregorShear in estuary/flow#3175 **Full Changelog**: estuary/flow@v0.6.11...v0.6.12 Co-authored-by: mdibaiee <mdibaiee@users.noreply.github.com>
## What's Changed * docs: surface MCP and agent skills for coding agents by @jwhartley in estuary/flow#3093 * dekaf: serve the document containing a mid-document fetch offset by @jshearer in estuary/flow#3150 * runtime: let docker atomically assign published connector host ports by @dgreer-dev in estuary/flow#3162 * runtime-next: discard hinted frontier when committed frontier is rebuilt by @jgraettinger in estuary/flow#3165 * docs/source-postgres: Sync statement_timeout change by @willdonnelly in estuary/flow#3168 * Provide capture/materializations created_at date by @dgreer-dev in estuary/flow#3160 * docs: add Processing order section for materialization binding priority by @jwhartley in estuary/flow#3171 * docs: reorganize agent skills page by plugin, add derivations and schema by @jwhartley in estuary/flow#3170 * go.mod: bump gazette to latest by @williamhbaker in estuary/flow#3180 * Service Accounts by @GregorShear in estuary/flow#3058 * Bound too-large error messages over RPC by @dgreer-dev in estuary/flow#3182 * control-plane: guarded, auto-expiring temporary support access to restricted tenants by @skord in estuary/flow#3115 * private links: model links as rows with controller-observed status by @jshearer in estuary/flow#3063 * Adding support for an stripe web hook endpoint. by @bbartman in estuary/flow#3174 * docs: Dekaf consumer behaviors + Spark Structured Streaming guidance by @jwhartley in estuary/flow#3094 * shuffle: log canonical journal name in slice read events by @jgraettinger in estuary/flow#3190 * runtime: persist the max-key fail-safe sentinel at adoption by @jgraettinger in estuary/flow#3191 * local: make flow-plane-link robust to control-plane agent start-up by @jgraettinger in estuary/flow#3158 * gazette: surface fragment store diagnostic on FRAGMENT_STORE_UNHEALTHY by @jgraettinger in estuary/flow#3159 * docs: add warning about source-stripe-native's events API version by @nicolaslazo in estuary/flow#3166 * agent-api: Add `STRIPE_WEBHOOK_SECRET` to cloud run config by @jshearer in estuary/flow#3196 * shuffle, gazette: guard against wedged reads by @jgraettinger in estuary/flow#3193 * runtime-next: gate task-log stream by configured log level by @williamhbaker in estuary/flow#3198 * flowctl: preview-next appends a final drain session by @mdibaiee in estuary/flow#3195 * Allow localhost:3000 origin for the local control-plane agent by @GregorShear in estuary/flow#3206 * control-plane-api: don't lock injected ops collections during publications by @skord in estuary/flow#3164 * runtime-next: force txn close at any usage ceiling, bypassing min duration by @jgraettinger in estuary/flow#3203 * shuffle: replay gapped producers on restart by @jgraettinger in estuary/flow#3202 * docs: source-zuora by @Alex-Bair in estuary/flow#3204 * dekaf: only read back when a fetch offset lands inside a document by @mdibaiee in estuary/flow#3205 * mise: dev-VM zone configurability (create fallback, zone-agnostic SSH, vm:move-gcp) by @skord in estuary/flow#3208 * docs/postgres: Discovery Filters by @willdonnelly in estuary/flow#3215 * docs: document materialize-s3-iceberg nanosecond_timestamps option by @jacobmarble in estuary/flow#3199 * runtime v2 flag for new captures and derivations by @williamhbaker in estuary/flow#3219 * runtime-next: converge startup reconciliation via rescan Persists by @jgraettinger in estuary/flow#3218 * build(deps): bump fast-uri from 3.1.0 to 3.1.4 in /site by @dependabot[bot] in estuary/flow#3223 * build(deps): bump svgo from 3.3.3 to 3.3.4 in /site by @dependabot[bot] in estuary/flow#3224 * build(deps): bump dompurify from 3.3.3 to 3.4.12 in /site by @dependabot[bot] in estuary/flow#3222 * build(deps): bump shell-quote from 1.8.3 to 1.10.0 in /site by @dependabot[bot] in estuary/flow#3213 * build(deps): bump body-parser from 1.20.4 to 1.20.6 in /site by @dependabot[bot] in estuary/flow#3212 * build(deps): bump webpack-dev-server from 5.2.2 to 5.2.6 in /site by @dependabot[bot] in estuary/flow#3211 * build(deps): bump websocket-driver from 0.7.4 to 0.7.5 in /site by @dependabot[bot] in estuary/flow#3183 * Docs: MySQL discovery filters by @aeluce in estuary/flow#3217 * docs: source-smartsheet by @nicolaslazo in estuary/flow#3228 * control-plane-api: surface per-store health diagnostics in storage-mapping mutations by @GregorShear in estuary/flow#3181 * async-process: handle reaping-task JoinError in Child::wait() without panicking by @jgraettinger in estuary/flow#3225 * feat: end-to-end truncation by @williamhbaker in estuary/flow#3132 * docs: source-zuora uses the AQuA API now by @Alex-Bair in estuary/flow#3237 * Improve storage mapping GraphQL results by @GregorShear in estuary/flow#3200 * control-plane: add `closed` flag to data planes by @GregorShear in estuary/flow#3175 **Full Changelog**: estuary/flow@v0.6.11...v0.6.12 Co-authored-by: mdibaiee <mdibaiee@users.noreply.github.com>
Description:
When a capture re-backfills a binding it re-snapshots the source, so the previously-captured documents in the collection become stale. This adds an end-to-end truncation boundary so downstream consumers skip or reset that superseded data instead of carrying it forward.
A capture connector signals
BackfillBegin/BackfillCompleteper binding. The capture publishes each as aCONTROLdocument into the collection's journals and stamps anestuary.dev/truncated-atlabel (the begin clock) on them. Every reader (V1 and V2 alike) treatsCONTROLdocs as metadata rather than content, and a fresh reader skips the stale pre-boundary prefix via the label. Shuffle carries the begin/complete clocks through the checkpoint frontier, and the runtime persists them so the boundary survives transaction rotation and restart. Materializations use the boundary to drop stale source documents, reset stale stored rows, and notify the connector so it can delete superseded destination rows.Scoped to single-shard, full-key-range captures; multi-shard synchronized backfills are future work.
Tested on a local stack through various scenarios with a single shard capture and materialization processing sequences of
BackfillBegin/BackfillCompletemessages.Closes #2821
Workflow steps:
(How does one use this feature, and how has it changed)
Documentation links affected:
(list any documentation links that you created, or existing ones that you've identified as needing updates, along with a brief description)
Notes for reviewers:
There's a constant stream of merge conflicting things landing on master. I've rebased and fixed these more times than I care to count at this point. I'll do one final pass just before this is ready to merge (and the requisite re-approvals).