Summary
WT-18406 made cursor traversal skip a disaggregated internal page that is still on disk and whose address aggregate shows the whole subtree is deleted. That stopped a read/re-dirty livelock, but it also removed the only trigger that ever frees the subtree's blocks.
_wt_ref_block_free() is reached only from reconciliation and split (rec_child.c, rec_write.c, bt_split.c), and reconciliation needs the page in cache – checkpoint's tree walk is cache-only (WT_READ_CACHE in bt_sync.c, and _wt_page_in returns WT_NOTFOUND for a WT_REF_DISK ref). A page that is never read is never reconciled, so no plh_discard is ever issued for its children.
The page service does not compensate. It performs no reachability collection: the plh_get_page_ids callback is not implemented by the SLS page log extension and there is no equivalent RPC. Page deletion is driven entirely by explicit tombstone/supersession signals plus retention windows. A subtree we skip therefore stays resident in both the hot (RocksDB) and cold (object storage) tiers until the table is dropped or the log is deleted.
Proposal
Stop skipping once the truncate is globally visible.
The skip is only needed during the window where the deletion is committed but not yet globally visible – that is exactly when a read re-dirties the page and the livelock forms. Once the deletion is globally visible, reading the page once is both safe and sufficient:
- read-in recreates the WT_REF_DELETED children from the WT_CELL_ADDR_DEL cells and dirties the page (guarded by btree->modified);
- _rec_child_deleted() then takes its globally-visible branch – WTI_CHILD_IGNORE plus _wt_ref_block_free(session, ref, true) – so every child block is discarded and no cell is written;
- the page reconciles to zero entries, yielding WT_PM_REC_EMPTY; eviction turns its ref into a WT_REF_DELETED ref with no page_del, and the parent's next reconciliation drops that ref and frees the internal page itself.
Nothing is left to re-dirty, so the read happens once per subtree and the livelock cannot recur.
The selected_for_write stickiness in __rec_child_deleted() does not block this: the flag is not persisted, and page_del is rebuilt from the cell at read-in.
Scope
- Add the globally-visible condition to the internal-page skip in __wt_btcur_skip_page(), next to the existing WT_REF_DISK condition.
- Add a statistic distinguishing "skipped, reclamation still pending" from the existing skip counter, so the pending-reclamation backlog is observable.
- Update test_disagg_fast_truncate03: its exit step currently asserts the eviction-blocked counter does not move after the deletions become globally visible. Under this change it moves once (the one read that re-dirties the page). Assert instead that the subtree is reclaimed – the page is read exactly once and the children are discarded.
Out of scope
Reclaiming without reading. __sync_obsolete_disk_cleanup() in bt_sync_obsolete.c already flips an on-disk ref to deleted from its aggregate alone, and its internal-ref and WT_ADDR_LEAF_NO guards could be relaxed. But the descendants' page ids exist only inside the internal page's image, so that path would discard the internal page and strand everything beneath it – strictly worse than today.
- is related to
-
WT-18406 Skip reading an internal page when its time aggregate shows the whole subtree is deleted and visible
-
- Closed
-
- related to
-
WT-18406 Skip reading an internal page when its time aggregate shows the whole subtree is deleted and visible
-
- Closed
-
-
WT-18524 Evaluate skipping emptied internal pages on non-cursor walk and descent paths
-
- Needs Scheduling
-
-
WT-18525 Evaluate skipping emptied internal pages for the local block manager
-
- Needs Scheduling
-
-
WT-18577 test_disagg_fast_truncate03: a skipped internal page did not reconcile to empty
-
- Closed
-