Find and fix three persistence event-gap leak sites - #99
Conversation
`scripts/ci-local.sh` runs `cargo fmt --all --check` as its first step and CI did not, which made the local script stricter than CI rather than equal to it, so formatting drift could reach master unnoticed. Same command, same arguments. Measured before adding it: the tree already passes, exit 0 with no diffs, so this job is green as written rather than a red first run to clean up.
The log carried beta.18 and beta.19 and nothing before them, so the history a consumer needs in order to judge an upgrade was only in the commits. Entries are derived from this repository's own history.
`BatchOperation::validate` refuses an event stream with a hole in it. A hole is normally transient, and one that survives the whole deferral budget is not, but the stall message could not tell the two apart: it reported the range and nothing else. Two very different bugs produce that line. Either an id was assigned by the index and its event was dropped instead of queued, in which case nothing will ever deliver it, or the operation was queued and batch collection keeps assembling batches that exclude it. Distinguishing them needs a record of what was queued. Assignment happens inside the index and cannot be hooked from here, but every event reaching persistence passes through `Queue`, so an id present in the stream and absent from the ledger was assigned and never queued. That is the leak signature. Producers are named by `std::panic::Location`, which is why `push_at` takes the caller location and why the old `push` and `push_many` wrappers are deleted rather than kept: calling them loses exactly the location the ledger exists to record. This observes and does not fix. `docs/TODO.md` records the three leak sites the instrument points at, all in generated code: the `NotFound` arm of the update query returns without building an acknowledge while its `AlreadyExists` sibling builds one, and two `res?;` sites in update and delete propagate before acknowledging. The TODO's congee section is corrected while here. The crossbeam-epoch port was already done in congee-wt 0.4.4, and it was a design change rather than a rename: per-tree `Domain`, batched retires, guard provenance checked, and a new `Guard` that is `!Send` and tree-scoped, which constrains future callers.
The ledger from the previous commit points at three sites in generated persisted query code where an event the index had already assigned an id to was dropped instead of queued. A dropped event is a hole `BatchOperation::validate` refuses forever, so the table stalls and the message names a range rather than a cause. All three now do what the rollback arms already did: build an `Acknowledge` carrying the orphaned events and apply it before propagating. The `IndexError::NotFound` arm of the update insert path acknowledges the events its sibling `AlreadyExists` arm was already acknowledging, and the two bare `res?` sites, in `gen_process_diffs_remove_on_index` and after `delete_row_cdc`, became `if let Err(e) = res` blocks that acknowledge and then return. The events are moved into the acknowledge rather than cloned, and that is load-bearing rather than a preference. Cloning does not compile: the events type is still an inference variable at that point, pinned only by the `op.extend_secondary_key_events` call further down, and method resolution for `.clone()` needs the type known where the call is written. It fails as an `E0282` reported against the `worktable!` invocation with no inner span, which is expensive to diagnose. The comment in `docs/TODO.md` records that so nobody pays for it twice. The write failures are not forcible through the public API, so the wiring is pinned on the emitted tokens the way the surrounding tests already do it. Both assertions check the acknowledge is emitted *before* the return or the extend, not merely that one appears somewhere in the output. The in-memory generator has the same two `NotFound` arms and is deliberately untouched: there is no persistence stream behind them to gap.
|
CI does not cover this PR. Verified locally instead, on the merge state of this branch: The new formatting job in the first commit is included in that and is green. Note also that the formatting job this PR adds will not run on this PR, for the same reason. |
Collapses what was PR #102 onto the arctic, event-ledger and page-list work already on this branch, so WorkTable carries one pull request. #97, #99 and #100 are folded in; #58 is not, being 224 commits behind master and already conflicted, which is its own job. `fsx` and every persistence signature go through `nagoya::io`, so the production path names no runtime. A persisted table can set `page_size`, which was refused before because the seeks computed offsets from a crate constant while the generated table threaded the configured one. Four places where the folded branches disagreed, each resolved by keeping both rather than either: * `arctic` and `ps-reclaim` keep #97's raised version floors and gain the `default-features = false` the no_std work needs. * `allocated_bytes` keeps the page list as #100 left it and takes `core::mem` from the no_std work; the `.load()` in the older branch belonged to a shape #100 replaced. * `batch.rs` and `task.rs` take the `core`/`alloc` imports, plus the `Arc` and `Location` the no_std lists had dropped and the code still uses. * The s3 test goes back to tokio's extension traits. It talks to a tokio `TcpStream` it starts itself, so the sweep that took the storage path off tokio should never have touched it. `event_ledger` is new here and was written against `std`. Its bookkeeping moves to `core` and `alloc`; only the two parts that genuinely need an operating system are gated, the backtrace capture and reading `WT_EVENT_LEDGER`, so without `std` the ledger is simply never enabled. `futures/std` goes in this crate's own `std` feature rather than on the dependency line, where a `--no-default-features` build would still have turned it on. cargo test --workspace --all-targets 933 passed ... --all-features 935 passed ... --features versioned-row-publication 1029 passed cargo clippy --workspace --all-targets [--all-features] clean cargo check --no-default-features clean
|
Folded into #102, which now carries this work plus the tokio::fs removal and tunable page stride, rebased linear with no merge commit. WorkTable carries one PR for this chain. |
Based on
deps/arctic-0.1.11, not on master. The seven files here do not overlap the three that branch changes, but this was written and tested against its dependency bump, so it is stacked rather than rebased. Merge that one first.The event gap
BatchOperation::validaterefuses an event stream with a hole in it. A hole is normally transient: the operation carrying the missing id has been produced but not yet batched. A hole that survives the whole deferral budget is not transient, and the stall message could not tell the two cases apart. It reported the range and nothing else:Two very different bugs produce that line. Either an id was assigned by the index and its event was dropped instead of queued, so nothing will ever deliver it and the stream is permanently gapped, or the operation was queued and batch collection keeps assembling batches that exclude it.
Distinguishing them needs a record of what was queued. Assignment happens inside the index, which this crate cannot hook, but every event reaching persistence passes through
Queue, so an id present in the stream and absent from the ledger was assigned and never queued. That is the leak signature and it is whatgap_reportreports.Producers are named by
std::panic::Location. That is whypush_attakes the caller location, and why the oldpushandpush_manywrappers are deleted rather than left in place:push_at's own documentation warns that callingpushloses exactly the location the ledger exists to record, so keeping the wrapper is keeping a trap.The ledger found three leak sites, all in generated persisted query code, and the second commit fixes all three: the
NotFoundarm of the update insert path, and the two bareres?sites ingen_process_diffs_remove_on_indexand afterdelete_row_cdc. Each now builds anAcknowledgecarrying the orphaned events and applies it before propagating, which is what the rollback arms already did.The events are moved into the acknowledge rather than cloned, and that is load-bearing. Cloning does not compile: the events type is still an inference variable there, pinned only by the
op.extend_secondary_key_eventscall further down, so method resolution for.clone()has nothing to resolve against. It fails as anE0282against theworktable!invocation with no inner span.The write failures are not forcible through the public API, so both regression assertions pin the emitted tokens and check the acknowledge comes before the return or the extend, not merely that one appears. The in-memory generator has the same two
NotFoundarms and is deliberately untouched: no persistence stream sits behind them.The formatting job
scripts/ci-local.shrunscargo fmt --all --checkas its first step and CI did not, so the local script was stricter than CI rather than equal to it. Measured before adding the job: the tree already passes, exit 0, no diffs. The job is green as written.The change log
The log carried beta.18 and beta.19 and nothing before them. Backfilled to 0.3.10 from this repository's own history.
Correction carried in
docs/TODO.mdThe congee crossbeam-epoch port was already done, in congee-wt 0.4.4, and the TODO was wrong about both the state and the nature of it. It was a design change rather than a rename: per-tree
Domain, batched retires, guard provenance checked. The newGuardis!Sendand tree-scoped, which constrains future callers.Verification