feat(cubestore): tolerant job reads + cleanup of unknown job types#11087
Conversation
|
Claude finished @waralexrom's task in 1m 9s —— View job Code Review — tolerant job reads + Unknown cleanupVerdict: LGTM. Approach is sound, scope is tight, and tests cover the surprising parts (data-carrying unknown variants, nested-in-struct decode, index rebuild). A few low/medium notes below — nothing blocking. Review details (4 notes, 0 high)Medium1. Cleanup sweep runs an unconditional 2. Low3. Index rebuild runs in a second 4. Nits
|
|
Claude finished @waralexrom's task in 43s —— View job Re-review — follow-up commit
|
| Prior note | Fix in f1a51fd |
Status |
|---|---|---|
#1 unconditional all_rows() every reconcile |
delete_unknown_jobs now does an read_operation_out_of_queue scan first and only enters write_operation when an Unknown is observed (metastore/mod.rs:4346-4358). Steady state no longer takes the writer queue. |
Fixed |
#2 add_job silently accepting Unknown |
debug_assert! on JobType::Unknown at the top of add_job (metastore/mod.rs:4233-4240); the test updated to insert directly via JobRocksTable::insert to simulate a newer-binary write. |
Fixed |
| #3 delete + rebuild split across two write txns | Folded into a single write_operation (metastore/mod.rs:4361-4387), so the cleanup commits atomically — no window where the unique-index entry is dangling. Early unknown.is_empty() short-circuit handles the TOCTOU between the gate scan and the write. |
Fixed |
#4 route_job Unknown arm look unreachable |
One-line comment added at cluster/ingestion/job_runner.rs:400 explaining it's defense-in-depth, not a live path. |
Fixed |
Small things still worth noting (non-blocking)
- The out-of-queue gate scan still does a full
all_rows()on Jobs, but it's lock-free and Jobs are small — the original concern was the write-queue contention, which is gone. Fine as-is. - The
debug_assertis debug-only; in release builds anUnknowninsert would still succeed silently. That's deliberate (asserts are the right tool for "this binary's own code never does X"), but worth being aware of if you ever wireadd_jobto anything that proxies external input.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #11087 +/- ##
=======================================
Coverage 58.50% 58.50%
=======================================
Files 216 216
Lines 17270 17270
Branches 3524 3524
=======================================
Hits 10103 10103
Misses 6652 6652
Partials 515 515
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:
|
Make the worker's job read path forward-compatible across latest/release channel switches: an unknown JobType variant (written by a newer binary) no longer fails the whole job-shard scan. - Add `#[serde(other)] Unknown` to JobType so foreign variants decode to Unknown instead of erroring (flexbuffers honors serde(other), incl. data-carrying variants). Stable index id (11) and lowest priority. - start_processing_job filters Unknown out silently; route_job rejects it cleanly instead of panicking. - Add MetaStore::delete_unknown_jobs: scans, deletes Unknown jobs and rebuilds the Jobs RowReference index (a plain delete would leave the unique-index entry dangling, since its key was written over the real variant). Wired into the scheduler reconcile loop; warns per removed job and bumps cs.jobs.unknown.deleted.
- Gate delete_unknown_jobs with an out-of-queue scan so the common steady state (no unknown jobs) doesn't take the single-writer queue every reconcile tick. - Fold the RowReference index rebuild into the same write_operation as the delete so the cleanup commits atomically (no window that leaves a dangling unique-index entry). - Reject JobType::Unknown in add_job via debug_assert; this binary must never create one. Test injects Unknown via a direct insert instead. - Note the route_job Unknown arm is defense-in-depth (not a live path).
f1a51fd to
2b1ad41
Compare
|
Claude finished @waralexrom's task in 46s —— View job Re-review — commit
|
| Prior note | Fix in 2b1ad41 |
Status |
|---|---|---|
#1 unconditional all_rows() every reconcile |
delete_unknown_jobs now gates on a read_operation_out_of_queue scan (metastore/mod.rs:4365-4376) and only enters write_operation when an Unknown is observed. Steady state no longer takes the writer queue. |
Fixed |
#2 add_job silently accepting Unknown |
debug_assert! at metastore/mod.rs:4254-4258; the test now injects Unknown via a direct JobRocksTable::insert to simulate a newer-binary write (metastore/mod.rs:7592-7605). |
Fixed |
| #3 delete + rebuild split across two write txns | Folded into a single write_operation (metastore/mod.rs:4378-4406); the unknown.is_empty() early-return handles the TOCTOU between the gate scan and the write. Cleanup now commits atomically. |
Fixed |
#4 route_job Unknown arm looking unreachable |
One-line comment added (cluster/ingestion/job_runner.rs) noting it is defense-in-depth, not a live path. |
Fixed |
Non-blocking observations
- The out-of-queue gate scan still does a full
all_rows()materialize before.any(...)— it can't short-circuit becauseall_rows()returns aVec. Fine: Jobs is small and this path runs outside the writer queue, which was the real concern. debug_assert!is release-stripped by design — correct choice for "this binary's own code never does X." Worth being aware of ifadd_jobis ever wired to anything that proxies external input.- Atomic delete+rebuild closes the crash-window correctly; the
unknown.is_empty()guard inside the write is the right belt-and-braces for the gate→write race.
Summary
Make the CubeStore worker's job read path forward-compatible across
latest/releasechannel switches. Today, a job row carrying aJobTypevariant this binary doesn't know fails to deserialize, and because the shard scan doesget_row(id)?in a loop, the whole scan errors — the worker stops picking up any of its jobs until upgraded. This makes it unsafe to ever add a newJobType, since a deployment can move between channels in both directions. This PR is the groundwork that makes futureJobTypeadditions channel-switch safe.Changes
JobType::Unknownfallback (metastore/job.rs): add#[serde(other)] Unknownso a foreign serialized variant decodes toUnknowninstead of erroring. Verified flexbuffers honors#[serde(other)]— including for data-carrying unknown variants (payload is skipped, noErr) — both bare and nested in theJobstruct. Stable index id (11), lowest priority.start_processing_jobselection;route_jobrejects it with a cleanErrinstead of a non-exhaustive-match panic.process_separate_jobalready had a catch-all.MetaStore::delete_unknown_jobs: scans, deletesUnknownjobs, then rebuilds the JobsRowReferenceindex. The rebuild is required becausedeleterecomputes the unique-index key from the (nowUnknown)job_type, while the stored key was written by the newer binary over the real variant — a plain delete would leave a dangling unique-index entry and cause a phantomadd_jobdedup on a later upgrade. Wired into the schedulerreconcileloop (runs on the router, ~30s); warns per removed job and bumpscs.jobs.unknown.deleted.This lets a leftover unknown job stay in the metastore without blocking the worker from picking up its other jobs (the success bar), while the sweep removes them so they don't pile up on a long downgrade.
Testing
metastore/job.rs: flexbuffers round-trip of unknown unit/data variants →Unknown(bare + nested inJob), and known variants still round-trip.start_processing_job_skips_unknowninmetastore/mod.rs: a known sibling job is selected over anUnknownone, theUnknownjob is never picked, scan paths still succeed, and the cleanup sweep removes it (count 1 then 0) leaving the known job + a consistent index (re-add for the freed reference succeeds).cargo build -p cubestoreclean;cargo test -p cubestore --lib metastore35 passed;cargo test -p cubestore --lib scheduler4 passed;cargo fmtapplied.