fix(namespace): fence stale table deregistration - #8605
Conversation
|
Important This PR touches the Lance format specification. Substantive changes to the format specification — the If this is a meaningful format change:
|
There was a problem hiding this comment.
❌ Gate recommendation: request changes.
The async-drop safety contract must be catalog-wide: once durable drop fencing is enabled, every instance that can physically drop a table must use the tombstone-and-epoch transition.
Persist and enforce that mode in the manifest, or fail closed on the legacy route, so a process-local default cannot bypass fencing during rollout.
| // Delete from manifest first | ||
| self.delete_from_manifest(&object_id).boxed().await?; | ||
| self.ensure_manifest_writable().await?; | ||
| let tombstone_id = if self.async_drop_enabled { |
There was a problem hiding this comment.
This branch makes the data-loss fence process-local. After an async-enabled instance has activated the catalog, a peer opened with the default async_drop_enabled=false still takes the else path: it removes T@L without a tombstone or drop-epoch advance. Another peer can then register U@L before this method reaches remove_dir_all(L), so the cleanup deletes U's data while the catalog still points to it.
Persist the async-drop mode in the manifest and require every physical drop to honor it (or reject legacy drops after activation), so mixed rollout cannot bypass the tombstone/epoch transition.
Reproducer run against this head
I added and ran a detached unit regression that:
- opens two
ManifestNamespaces on one root, one enabled and one disabled; - creates and completes an async tombstone, leaving drop epoch
1; - registers
oldatL; - lets the disabled instance run
delete_from_manifest_if_location("old", Some(L), true); - registers
replacementatLthrough the enabled instance; and - runs the disabled instance's
remove_dir_all(L).
cargo test -p lance-namespace-impls mixed_async_drop_modes_allow_legacy_cleanup_to_delete_reregistered_location --lib --quiet passed with assertions that replacement remains in the manifest and its data file has been deleted.
Make manifest-backed deregistration conditional on the physical location observed by the caller and re-check that generation inside every manifest rewrite attempt. Expected-generation deregistration writes an out-of-prefix sibling tombstone so the old location cannot be re-registered until physical cleanup finishes.
With
async_drop_enabled=true, generated table paths use UUID-sized incarnation IDs and overwrite publishes a fresh physical generation with a conditional manifest swap. The default overwrite path retains its existing version-history semantics.Also paginate DynamoDB external-manifest deletion so asynchronous physical cleanup removes every manifest entry for the dropped table.