You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug](cloud) Packed file correction can delete a just-recycled rowset's slice: recycle/tmp and formal rowset keys are read in different transactions #69861
I had searched in the issues and found no similar issues.
Version
4.1.4 (cloud mode, packed files). master is the same: cloud/src/recycler/recycler.cpp:1487 reads the recycle and tmp rowset keys in one transaction (check_recycle_and_tmp_rowset_exists), and :1503 reads the formal rowset key in another (check_rowset_exists).
Description
When the recycler corrects a packed file, it decides for each small file (slice) whether its rowset is still alive:
if (check_recycle_and_tmp_rowset_exists(tablet_id, rowset_id, txn_id, &recycle_exists, &tmp_exists) != 0) ...
...
} else { // neither recycle nor tmp key seenbool rowset_exists = false;
if (check_rowset_exists(tablet_id, rowset_id, &rowset_exists, stats) != 0) ...
if (!rowset_exists) { small_file->set_deleted(true); small_file->set_corrected(true); ... }
The two checks are separate transactions. If a compaction that consumes the rowset commits between them, the first read sees the formal key (no recycle key yet) and the second sees neither the formal key (removed by the commit) nor looks again at the recycle key (just written). The live, just-recycled rowset is taken as gone, its slice is marked deleted, and the packed file can be deleted before the compacted rowset's retention ends. Readers still on the old version (or anything relying on the retention window) then hit missing data.
PR #68153 makes the packed-file checker read within one snapshot; this recycler correction path is separate.
Solution
Read the recycle, tmp and formal rowset keys of a slice in one transaction / snapshot (e.g. one check_rowset_keys(...) that returns all three), so a concurrent compaction commit is seen either entirely before or entirely after.
How to Reproduce?
With packed files enabled, inject a sync point between check_recycle_and_tmp_rowset_exists and check_rowset_exists in packed file correction, commit a compaction whose input includes the slice's rowset there, and observe the slice marked deleted while the rowset's recycle key exists.
Anything Else?
The race window is small, but the result is premature deletion of data still inside its retention window.
The reported interleaving is supported by the code and warrants high-priority cloud/recycler correctness triage because it can make retained data eligible for premature physical deletion. This is a source-level finding; I have not run the proposed race reproduction or verified an actual missing-data incident. The issue currently has no labels.
I checked the public 4.1.4 tag (ad35a140c7fd) and upstream master at 2a4191c1dc94. Both contain the split reads. In particular:
check_recycle_and_tmp_rowset_exists() creates its own transaction. check_rowset_exists() scans formal rowsets through txn_get(), which creates another transaction for each scan page. The outer transaction in process_single_packed_file() is not passed into either helper. See the 4.1.4 helpers and current master correction path.
For the non-versioned metadata write path, compaction puts the input rowset's recycle record and removes its formal metadata in the same transaction, committed by _finish_tablet_job(). Thus the writer is atomic, but the correction reader can combine observations from opposite sides of that commit. See compaction input recycling and commit.
The failing schedule is:
An eligible, uncorrected slice belongs to a formal rowset; its recycle and tmp keys are absent.
Correction reads recycle/tmp absence at snapshot A. Strictly speaking, this first helper does not read the formal key; that key merely still exists at this point.
Compaction atomically removes the formal rowset and creates its recycle record.
The subsequent formal-rowset scan at snapshot B cannot find that rowset ID. Correction marks the slice deleted=true, corrected=true, and excludes it from the recomputed reference count and remaining bytes.
Physical deletion is conditional: correction must be due, the slice must not already be corrected/deleted, the packed-metadata update must commit, and the resulting packed-file reference count must reach zero. Other live slices can postpone object deletion, but the incorrect slice accounting remains. With zero references, correction sets RECYCLING, and process_single_packed_file() can delete the object in the same call. Its later KV reread checks packed-file state/count after object deletion; it does not recheck rowset retention. The packed-key conflict retry is not sufficient because the input rowset's formal-to-recycle transition does not itself update that packed key. See the correction and deletion sequence. The ordinary rowset expiration calculation applies the compacted-rowset retention interval, but this orphan-classification path bypasses that check. See retention calculation.
The proposed shared-transaction approach addresses this interleaving. It must cover recycle/tmp reads and every page of the formal-rowset scan, with the whole existence decision restarted on transaction expiry/read failure. Merely putting the existing helpers behind one wrapper, or setting snapshot=true on independent transactions, would preserve the problem. Keep the packed-metadata write conflict/retry protection and the existing semantics that preserve recycle references without marking them corrected. PR #68153 is still open/unmerged at review time; its checker changes do not modify this recycler correction implementation.
Suggested validation and remaining evidence:
Add a deterministic regression beside recycle_packed_files_respect_recycle_and_tmp_refs in cloud/test/recycler_test.cpp. Pause after recycle/tmp absence, commit the formal-to-recycle transition, then resume correction. Use a packed file whose only remaining reference is the target slice; keep force_immediate_recycle=false, positive rowset retention, and an old enough packed-file creation time to make correction due. Assert that the unexpired recycle key, slice reference/count/bytes, and object survive. Exercise both commit orderings and a multi-page formal scan; also verify genuine orphans still recycle.
Please provide the exact deployed MetaService/recycler build SHA and whether versioned metadata reads/writes are enabled. The atomic transition traced above is specifically the non-versioned write path; versioned operation-log handling needs separate coverage before extending the conclusion to that mode.
To establish observed impact, attach a sanitized reproducer/test result, before/after PackedFileInfoPB, the matching formal/tmp/recycle metadata with recycle creation time, and effective packed_file_correction_delay_seconds, compacted_rowset_retention_seconds, retention_seconds, and force_immediate_recycle. Correlate compaction commit with packed slice correction status, corrected packed file ref count, and deleted packed file logs. Any claimed reader failure additionally needs the corresponding BE missing-object error and timestamp. A query profile is not needed to establish this metadata race.
No source code was modified and no fix or regression-test pass is claimed.
Activity
Doris-Breakwater commentedon Oct 29, 2026
The reported interleaving is supported by the code and warrants high-priority cloud/recycler correctness triage because it can make retained data eligible for premature physical deletion. This is a source-level finding; I have not run the proposed race reproduction or verified an actual missing-data incident. The issue currently has no labels.
I checked the public
4.1.4tag (ad35a140c7fd) and upstreammasterat2a4191c1dc94. Both contain the split reads. In particular:check_recycle_and_tmp_rowset_exists()creates its own transaction.check_rowset_exists()scans formal rowsets throughtxn_get(), which creates another transaction for each scan page. The outer transaction inprocess_single_packed_file()is not passed into either helper. See the 4.1.4 helpers and current master correction path._finish_tablet_job(). Thus the writer is atomic, but the correction reader can combine observations from opposite sides of that commit. See compaction input recycling and commit.The failing schedule is:
deleted=true,corrected=true, and excludes it from the recomputed reference count and remaining bytes.Physical deletion is conditional: correction must be due, the slice must not already be corrected/deleted, the packed-metadata update must commit, and the resulting packed-file reference count must reach zero. Other live slices can postpone object deletion, but the incorrect slice accounting remains. With zero references, correction sets
RECYCLING, andprocess_single_packed_file()can delete the object in the same call. Its later KV reread checks packed-file state/count after object deletion; it does not recheck rowset retention. The packed-key conflict retry is not sufficient because the input rowset's formal-to-recycle transition does not itself update that packed key. See the correction and deletion sequence. The ordinary rowset expiration calculation applies the compacted-rowset retention interval, but this orphan-classification path bypasses that check. See retention calculation.The proposed shared-transaction approach addresses this interleaving. It must cover recycle/tmp reads and every page of the formal-rowset scan, with the whole existence decision restarted on transaction expiry/read failure. Merely putting the existing helpers behind one wrapper, or setting
snapshot=trueon independent transactions, would preserve the problem. Keep the packed-metadata write conflict/retry protection and the existing semantics that preserve recycle references without marking them corrected. PR #68153 is still open/unmerged at review time; its checker changes do not modify this recycler correction implementation.Suggested validation and remaining evidence:
recycle_packed_files_respect_recycle_and_tmp_refsincloud/test/recycler_test.cpp. Pause after recycle/tmp absence, commit the formal-to-recycle transition, then resume correction. Use a packed file whose only remaining reference is the target slice; keepforce_immediate_recycle=false, positive rowset retention, and an old enough packed-file creation time to make correction due. Assert that the unexpired recycle key, slice reference/count/bytes, and object survive. Exercise both commit orderings and a multi-page formal scan; also verify genuine orphans still recycle.PackedFileInfoPB, the matching formal/tmp/recycle metadata with recycle creation time, and effectivepacked_file_correction_delay_seconds,compacted_rowset_retention_seconds,retention_seconds, andforce_immediate_recycle. Correlate compaction commit withpacked slice correction status,corrected packed file ref count, anddeleted packed filelogs. Any claimed reader failure additionally needs the corresponding BE missing-object error and timestamp. A query profile is not needed to establish this metadata race.No source code was modified and no fix or regression-test pass is claimed.
Breakwater-GitHub-Analysis-Slot: slot_3c21b68379d1