fix(cubestore): stop reporting remote files nothing references - #11601
fix(cubestore): stop reporting remote files nothing references#11601waralexrom wants to merge 7 commits into
Conversation
A download that fails because the object is gone is not a transient failure, but the only signal callers had was CorruptData, which they cannot act on: Cube Cloud retries such a download ten times with exponential backoff, and the startup warmup logs it at ERROR. A single file that compaction removed while the warmup walks its snapshot therefore costs ~51s of sleeps, 20 remote calls and 10 log lines, which slows the pass down enough to make the rest of the snapshot staler. Give it CubeErrorCauseType::FileNotFound so the process holding the remote fs can tell the two apart. A listing stays the only classifier, since object stores answer 404 both for a missing key and for a missing bucket. Nothing acts on the new cause yet, so behaviour is unchanged: it still counts as corrupt data for is_corrupt_data(), and it keeps the name and index of CorruptData everywhere it leaves the process - the wire, which a node of any version has to be able to read, and Display, whose output the scheduler matches on to deactivate a table whose import job failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deleting a file that is already gone is the state the caller asked for, and S3 reports it that way: it answers 204 for a missing key. GCS answers 404 instead, so every GC task and every cleanup pass that raced with an earlier deletion logged an error nobody could act on. On one cluster those lines were 76% of all ERROR output, enough on their own to fire the error-rate alert. Report an absent object as a successful delete, and keep dropping the local copy so callers see the same end state on either driver. The check is a function of its own so a test can pin it to the payload a bucket really answers with: the reason mapping lives in the client crate, and if it ever changes the flood comes back silently. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ng warmup The startup warmup takes one metastore snapshot and then walks it one download at a time, which on a large node runs for hours. Everything compaction replaces in the meantime is gone from remote storage by the time the pass asks for it, and every one of those was logged at ERROR, so a worker kept reporting errors about files nothing referenced any more for as long as the pass lasted. Ask the metastore what the file's row looks like now. A row that is inactive or gone means compaction replaced it, which is routine and belongs in debug output. A row that is still active means the data is really missing, which is the case worth an error and worth a metric of its own - it is the one this alert was supposed to be about. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The warmup check read one row per absent file and treated any failure to read it as proof the row was gone. Both halves were wrong in the same direction: warmup runs at startup, when a select worker reaches the metastore over an RPC link that may still be coming up, so a flaky link made every missing file look routine and counted it as such - fail-silent, in a check whose whole purpose is to report data that is really missing. And a node whose snapshot compaction had long moved on issued a router point-get per stale file, with every warming worker doing it at once, exactly while the router was busy recovering. Re-read the rows in one batch per partition with the out of queue readers, which leave out ids they no longer hold. That gives the classification its third answer: an id missing from the result is a deleted row, while an error is a check that did not happen and is now reported as such instead of being folded into either verdict. Also note in the GCS delete path that a missing bucket carries the same reason code as a missing object and that the client crate keeps the detail private, so the two cannot be told apart there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The check ran per partition, which on the path the pass hits most is still one or two requests to the main node for every group of files compaction has replaced. A worker returning after a long absence finds its whole snapshot replaced, so the count follows the number of partitions it holds, and it pays them while the main node is busy with the rest of the cluster coming up. Slowing the pass down is also what makes files go stale in the first place, so the check was working against the fix it belongs to. Collect absent files as the pass goes and look them up once per 256, which is the same verdict for a fraction of the round trips. A cancelled pass drops what it has not looked up: the next start walks the whole list again. Report per batch as well. A table whose objects an operator or a lifecycle rule really removed would otherwise produce an error line per chunk, which is the flood this check exists to remove wearing a different message. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @waralexrom's task in 5m 35s —— View job Review: no blocking issues — 0 high, 1 medium, 3 low (4 inline comments)The compatibility story checks out: I verified Full review — findings, verification notes, and checklistChecklist
Verification notesThe stale/active classification is correct, including a subtlety worth recording. Recheck-by-id is only valid because a row's file name is stable:
GCS. Treating an absent object as a completed delete matches S3's 204 and still drops the local copy. The missing-bucket conflation is real but correctly argued away in the PR body — a wrong bucket fails every upload and download loudly. Tests. Good coverage of the parts that are easy to get wrong later: byte-equality of the flexbuffer against Gaps: Findings
Not verifiedI did not compile or run the test suite — cubestore has no prebuilt Security: nothing relevant — no new inputs, no auth paths, no user-controlled data. Performance: two extra out-of-queue metastore reads per 256 absent files, against one download each, so negligible; memory is bounded by the batch plus one partition's chunks. |
Three ways the reporting lost the one thing an operator needs, the name of the file: A recheck the metastore could not answer dropped the batch with a count and no ids, and this runs at startup, when the link to the main node is likeliest to blip. There is no second pass, so the ids were the only trace of files that may be the genuinely missing ones. They are bounded by the batch, so log them. A download that failed for any other reason was reported as the bare error, which carries the path only when the error happens to be about the path - a size check does, a transport error may not. The caller knows it either way. A listing that failed while probing whether the object exists replaced the download error with its own, so the reason the download failed was lost and the classification the warmup now depends on turned into a guess. Let the download error stand and note the failed probe separately. Also name the predicate for what it does, since it reports as well as answers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests.
Additional details and impacted files@@ Coverage Diff @@
## master #11601 +/- ##
===========================================
- Coverage 79.58% 59.37% -20.22%
===========================================
Files 480 229 -251
Lines 99544 18247 -81297
Branches 3636 3682 +46
===========================================
- Hits 79224 10834 -68390
+ Misses 19801 6861 -12940
- Partials 519 552 +33
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The pass was asking the metastore whether each absent file was still named, so that it could tell a file compaction had replaced from one that is really gone. That distinction is worth having, but not here: the query that needs a missing file reports it anyway, and it is better placed to judge, since by then there is no snapshot to be behind. A whole-cluster answer is also nearly free where the cleanup loop already holds both the remote listing and the metastore filenames, and it covers every file rather than one node's share of one snapshot. So the pass reports what it can actually tell: a file that is not there is a file it did not warm, at debug, with a counter that says how far behind the snapshot it ran. Everything else stays an error. This drops the batching, the id bookkeeping and the two metastore reads that came with the classification. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @waralexrom's task in 3m 41s —— View job Re-review: no blocking issues — 0 high, 1 medium, 2 low (1 inline comment; all 4 prior threads resolved as addressed/obsolete)The change is much smaller than when I last looked: the batched metastore recheck is gone ( Full review — checklist, verification notes, findingsChecklist
Prior threads — all resolved
Verification notesCompatibility shim ( Worth stating explicitly since the tests pin it: the cause does not survive the wire — The GCS. FindingsMedium — the new metric can't distinguish the case worth alerting on ( Low — the classification is not pinned by a test. Posted inline on Low — the PR description no longer matches the code. It still describes the batched 256-file metastore recheck, Pre-existing, not introduced here: Security: nothing relevant — no new inputs, no auth paths. Performance: two extra Not verifiedI did not compile or run the suite — no prebuilt |
| // the download error to speak for both, rather than replacing it. | ||
| match self.remote_fs.list(file.clone()).await { | ||
| Ok(listing) if listing.is_empty() => { | ||
| return Err(CubeError::file_not_found(format!( |
There was a problem hiding this comment.
This is the only place in the tree that ever constructs a FileNotFound, so the whole warmup change hangs off it — but the two tests that exercise it (queue_download_missing_file at L620, queue_download_wrong_file_size at L660) both still assert only is_corrupt_data(), which was already true before this PR. Nothing would fail if a later refactor swapped this back to corrupt_data(...); the warmup would just quietly return to logging an ERROR per compacted file.
Tightening the two existing asserts pins it at zero cost:
// queue_download_missing_file
Err(e) => assert!(e.is_file_not_found()),
// queue_download_wrong_file_size — a truncated file is present, not absent
Err(e) => assert!(e.is_corrupt_data() && !e.is_file_not_found()),
Summary
Two paths log an ERROR for a file that was removed legitimately. The startup warmup takes one metastore snapshot and then walks it one download at a time, so on a node with many partitions the pass runs long enough that whatever compaction replaces meanwhile is already gone from remote storage by the time the pass asks for it. And
GCTask::RemoveRemoteFilelogs an error whenever the object is already gone, which on GCS is a 404 where S3 answers 204. Neither says anything is wrong, and both drown out the errors that do.The warmup carried a TODO for exactly this — "propagate 'not found' and log in debug mode. Compaction might remove files, so they are not errors most of the time."
Changes
CubeErrorCauseType::FileNotFoundfor an absent remote object, so a caller can tell it from a remote it could not reach. It still counts as corrupt data foris_corrupt_data(), and it keeps the name and index ofCorruptDataeverywhere it leaves the process: the wire, which a node of any version has to be able to read, andDisplay, whose outputschedulermatches on to deactivate a table whose import job failed. A listing stays the only classifier, since object stores answer 404 both for a missing key and for a missing bucket, and mistaking the second for the first would deactivate every table. A listing that fails leaves the download error to speak for both rather than replacing it.cs.warmup.missingto say how far behind its snapshot the pass ran. Every other failure stays an error, now naming the file. Whether such a file is merely replaced or really lost is not something the pass can judge — the query that needs it reports it anyway, and by then there is no snapshot to be behind.Testing
cargo test -p cubestore --lib— 319 pass.Displaycompatibility of the new cause, since both have consumers that would fail quietly; two pin the GCS detection against the payload a bucket really answers with, because the reason mapping lives in the client crate and a change there would bring the flood back silently.Heads up for CI:
metastore::tests::delete_old_snapshotsandsql::tests::decimal_partition_pruningflake on repeated parallel runs without this branch too.