Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a downstream patch to RocksDB that adds the blob_dir option to AdvancedColumnFamilyOptions, allowing blob files to be stored on a different volume than SST files. Feedback on the patch includes preventing duplicate paths in the directory creation list to avoid disabling NoSpace() recovery, ensuring the AdvancedColumnFamilyOptions constructor copies the new field, and using std::move for safer smart pointer ownership transfer.
|
Is this something we can PR with the RocksDB repo so we don't have to maintain this patch? |
Yes, I believe so, it seems like a very reasonable PR to submit. |
|
@cb1kenobi Should I go ahead and merge this, per the discussion above? |
Adds AdvancedColumnFamilyOptions::blob_dir so blob files can be placed on a different volume than the SST files. db_paths/cf_paths distribute SST files by level, but every blob file path is derived from cf_paths.front(), so large values cannot be tiered at all without this. That makes it impossible to keep the LSM tree (and all its compaction write traffic) on fast local storage while large values live on cheaper attached storage. All blob path derivation now goes through a single ImmutableCFOptions:: GetBlobDir() accessor, so the option and its cf_paths.front() default cannot drift apart. That includes the two places a first pass missed: a blob directory outside cf_paths has no handle in Directories, so its directory entries were never fsynced after flush or compaction (a crash could leave a durable SST referencing a blob file with no directory entry), and the common two-argument DestroyDB overload never collected blob_dir because collection lived only in the column_families loop. blob_dir is the last field of AdvancedColumnFamilyOptions on purpose: inserting mid-struct shifts every following field's offset and silently mismatches code compiled against stock headers. ROCKSDB_HAS_CF_BLOB_DIR lets downstream code feature-detect and still compile against an unpatched RocksDB. Authored against v11.8.1, the version rocksdb-js pins. 11.8.1's new direct-write blob partition manager was an additional path-derivation site, so the header carries a note to re-audit them on every version bump. Verified: applies cleanly to a fresh v11.8.1 tarball with patch -p1, builds clean, and the resulting library passes @harperfast/rocksdb-js's blob_dir tests — blobs written to a separate volume, read back across reopen, relocated, and applied to named column families. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Running the upstream suite against the patched tree (which I had skipped) turned up both: - ColumnFamilyData::GetDbPaths() called GetBlobDir() unconditionally, whose assert fires when cf_paths is empty. Unit tests build ImmutableCFOptions that never went through SanitizeCfOptions, so memtable_list_test aborted. It now reads the raw blob_dir field: an unset one resolves to cf_paths.front(), which the loop above already covers. - The options_settable_test exclusion entry was out of offset order. FillWithSpecialChar walks that list sequentially and computes pair.first - offset, so a misplaced entry underflows the length and the memset clobbers the very field the entry exists to protect — turning a pre-existing assertion failure on this toolchain into a segfault. blob_dir sits at 664, after blob_direct_write_partition_strategy at 616. memtable_list_test now passes. options_settable_test still fails, but identically to a pristine v11.8.1 tree on this toolchain (unset_bytes_base 102 vs 126, a GCC 15 padding artifact) — verified by building the same test from an unpatched tarball. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copy blob_dir through AdvancedColumnFamilyOptions, avoid duplicate open paths, and transfer the blob directory handle without a raw-pointer round trip. Add a focused constructor regression test. Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Drop a whitespace-only advanced_options.h hunk so future -F0 rebases have one fewer anchor to maintain. Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Review asked us to reject backup/checkpoint while blob_dir is non-empty, because a backup that flattens blob files into the DB directory restores into a database that cannot read its large values. The flattening half of that does not happen: CreateCustomCheckpoint -- the shared implementation behind both Checkpoint and BackupEngine -- already refuses any DB whose non-WAL files span more than one directory, and reporting the real blob directory from GetLiveFilesStorageInfo is what puts a tiered DB in that category. But it only puts it there once a blob file exists to report, and the gap is worse than the flattening would have been. Backing up a tiered DB that has not written a blob file yet succeeds. The copied OPTIONS file carries blob_dir, so restoring that backup elsewhere and opening it produces a second DB pointed at the *source's* blob directory -- and its obsolete-file scan deletes the source's live blob files. Measured on a patched v11.8.1 build: source blob files 1 before, 0 after opening the restore, and the source DB then fails its own read with "No such file or directory". So the rejection has to key on the option, not on the files reported. CreateCustomCheckpoint now also asks DBImpl::HasBlobDirSet(), which is true for any non-empty blob_dir rather than only one outside the DB directory: blob_dir is an absolute path, so a copy of the DB opened anywhere resolves its blob files back to the original directory whatever that directory is. This is upstream's hazard, not one blob_dir introduces -- the same sequence with cf_paths pointing at an external directory corrupts the source DB on stock v11.8.1, verified the same way. It is closed here because blob_dir is the option this patch owns, and because a tiered DB spends its early life blob-free, which is exactly the window that backs up cleanly and restores into a landmine. Also: the file-spanning message now names blob_dir, so someone who set only blob_dir does not read "db_paths / cf_paths not supported" as a bug, and the option's doc comment states the restriction. Verified with blob_dir unset: checkpoint, backup and restore unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Co-Authored-By: GPT-5 Codex <noreply@openai.com>
The `cf_paths[0].path` half of the audit regex could never match: `cf_paths\.` requires a literal dot, which `cf_paths[0]` does not have. Moving the dot inside the alternation surfaces seven previously invisible call sites, all of which derive SST paths rather than blob paths — recorded in the inventory so a future one that does derive a blob path shows up as a diff. AtomicFlushMemTablesToOutputFiles enters its output-directory sync block on `s.IsShutdownInProgress()`, a state reachable from SyncClosedWals before the PickMemTable loop runs. FlushJob::edit_ is null until PickMemTable assigns it, so the blob-directory addition dereferenced null on a shutdown flush. Guard on `pick_status[i]`, the same condition the surrounding cancel loop already uses. The PR gate resolved `releases/latest` at run time while the inventories pin upstream line numbers, so an upstream release would have failed every open pull request. It now applies and tests the release in `patched-version.txt`; the nightly audit still runs against the latest release, which is where drift belongs. `options_settable_test` joins the gate's test set — it is the upstream test that covers the patch's ColumnFamilyOptions registration. Verified on the patched v11.8.1 build: the blob_dir cases in db_basic_test (4), db_flush_test (1), checkpoint_test (3), and backup_engine_test (1) all pass. `options_settable_test.ColumnFamilyOptionsAllFieldsSettable` fails on GCC 15 with the identical numbers (unset_bytes_base 102 vs 126) on a pristine v11.8.1 tree, so the patch neither causes nor masks it. The regenerated patch applies to a fresh tarball at `patch -p1 -F0` and `git apply --check`, and reproduces all 28 patched files byte-for-byte against the tree that was built and tested. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011TPPxptQi3DTZk7UPDFsFQ
Guarding the call site on pick_status[i] was not enough. pick_status[i] records that PickMemTable() ran, not that it picked anything: PickMemtablesToFlush() returns an empty set when every not-yet-flushed memtable is already claimed by an overlapping flush or sits above the request's max_memtable_id, and PickMemTable() then returns before assigning edit_. Run() accepts that state and returns OK, so the atomic-flush sync block is reached with pick_status[i] true and edit_ null. edit_ is null exactly when nothing was picked, and a job that picked nothing wrote no blob files, so the accessor answers false rather than making every call site prove it picked memtables. The call-site guard goes away with it. Also trims the added comments down to the invariants they carry — the Env path registration reading the raw field, the data_dirs_ gap that blob_dir_ fills, the NoSpace() interaction — dropping the sentences that narrate the code beside them. Verified on the patched v11.8.1 build: db_basic_test blob_dir cases 4/4, db_flush_test 1/1, checkpoint_test 42/42, backup_engine_test 108/108, options_test 79/79. The regenerated patch applies at patch -p1 -F0 and git apply --check and reproduces all 28 patched files byte-for-byte against the tree that was built and tested; both inventories are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011TPPxptQi3DTZk7UPDFsFQ
The validate job downloads a RocksDB release, applies every patch and runs upstream test binaries; on a 90-minute timeout that is not something to spend on a pull request that cannot change the result. Restrict the pull_request trigger to the overlay directory and this workflow, the same `paths` idiom validate-experimental-patches.yml already uses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011TPPxptQi3DTZk7UPDFsFQ
28e4696 to
729c24a
Compare
|
Blocked by PR #24. |
Checked this rather than taking it on faith, and it holds — nothing on this PR needs to change for it, and the move is cheaper than it looks:
So the ordering is right, and I've updated item 7 of the description — it had the placement question as still open, and your comment answers it. What is left is on #24 rather than here: two review threads are still unresolved against
The one thing #24 does not settle is the consumer: once the patch is opt-in, official prebuilds stop carrying Nice call on the move, by the way — the per-patch — Claude Opus 5 |
cb1kenobi
left a comment
There was a problem hiding this comment.
The blob_dir patch's earlier blockers are closed: portable capture is latched under the DB mutex, backup and checkpoint refuse a configured blob_dir, and directory fsync plus call-site audit coverage match the stated contract. Remaining items in the PR body are documented C++ layering tradeoffs that rocksdb-js already enforces, not unfixed defects on this diff. No new blocking issue on the changed lines at 729c24a.
—
Reviewed 729c24a
Adds a downstream RocksDB patch introducing
AdvancedColumnFamilyOptions::blob_dir, so blob files can be placed on a different volume than the SST files.Stock RocksDB derives every blob file path from
cf_paths.front().path. That meansdb_paths/cf_pathscannot tier large values at all — they distribute SST files by level while every blob file stays put. Without this, there is no way to keep the LSM tree (and all of its compaction write traffic) on fast local storage while large values live on cheaper attached storage.All blob path derivation now routes through a single
ImmutableCFOptions::GetBlobDir(), so the option and itscf_paths.front()default cannot drift apart.ROCKSDB_HAS_CF_BLOB_DIRlets downstream code feature-detect and still compile against an unpatched RocksDB. Consumed by@harperfast/rocksdb-jsvia its newblobs.diroption.The patch is authored against v11.8.1, the version
rocksdb-jspins, and applied withpatch -p1 -F0so a version bump fails loudly instead of fuzzy-matching a hunk into a plausible-but-wrong site.Where to look
GetBlobDir()is the whole contract; most of the rest is call sites. The parts worth reading closely are the ones review found rather than the ones the first pass wrote.blob_dirDB.CreateCustomCheckpoint— the shared implementation behind both — asks for a portable capture throughLiveFilesStorageInfoOptions::require_portable_files, andGetLiveFilesStorageInfoevaluates it undermutex_before the flush and again after, becauseFlushForGetLiveFiles()releases the mutex. The state is a monotone latch (has_ever_had_blob_dir_) set at column-family creation and never cleared, so dropping the onlyblob_dirCF does not re-open the hole. Reasoning below.FlushJob::HasBlobFileAdditions()is total, not asserted. It answers false for a nulledit_.edit_is null exactly whenPickMemTable()picked nothing — reachable when an overlapping flush already claimed the memtables, or when they all sit above the request'smax_memtable_id— andRun()accepts that state and returns OK. A job that picked nothing wrote no blob files, so the predicate has a correct answer; guarding the call site instead would have been wrong, becausepick_status[i]records thatPickMemTable()ran, not that it picked.cf_pathshas noFSDirectoryhandle inDirectories, so its entries were never fsynced after flush, compaction, or WAL recovery — a crash could leave a durable SST referencing a blob file with no directory entry.ColumnFamilyDatanow owns a handle, created through the sharedcreated_dirsmap so no directory is opened twice.cf_paths(\.front\(\)|\[0\])\.path— the dot used to sit before the alternation, socf_paths[0].path(which has no dot aftercf_paths) could never match and the guard silently covered half of what it claimed. Fixing it surfaced seven call sites; all seven derive SST paths, and they are now pinned incf-blob-path-inventory.txtso one that later derives a blob path shows up as a diff.Checkpoint and backup
Two review threads asked for backup/checkpoint to be rejected while
blob_diris non-empty. Building the patched tree and measuring says the stated failure mode does not happen, but the conclusion is right anyway, for a worse reason.Flattening is already refused.
CreateCustomCheckpointrejects any DB whose non-WAL files span more than one directory, and reporting the real blob directory fromGetLiveFilesStorageInfois what puts a tiered DB in that category. Measured: withblob_dirset and one live blob file,CreateCheckpointandCreateNewBackupboth returnNotSupportedand leave no partial artifact.But only once a blob file exists to report. A DB with
blob_dirset that has not written one yet backs up cleanly — and that backup is a landmine. The copied OPTIONS file carriesblob_dir, an absolute path, so restoring elsewhere and opening produces a second DB pointed at the source's blob directory, whose obsolete-file scan deletes the source's live blob files. Measured on the patched build: source blob files 1 before, 0 after opening the restore, and the source DB then fails its own read withNo such file or directory.So the refusal keys on the option being set at all, not on the files reported and not on it differing from
dbname_— the path is absolute either way.This is upstream's hazard, not one
blob_dirintroduces. The same sequence withcf_pathspointing at an external directory corrupts the source DB on stock v11.8.1, verified by building an unpatched tree. It is closed here becauseblob_diris the option this patch owns, and because a tiered DB spends its early life blob-free — exactly the window that backs up cleanly and restores into a landmine.Rebased onto the experimental-patches pipeline
This PR had been conflicting with
mainsince #15 merged, which is why no PR workflow had ever run on it — GitHub cannot compute a merge commit for a conflicting PR, so the gate this branch adds had never once executed. Rebased; three conflicts, all inbuild.yml, all resolved in favour of keeping both sides:patch -F0invocationmain— it had already adopted-F0and a rationale comment, so this branch's intent was already thereEXPERIMENTAL_DIRSloopauditjobNo RocksDB patch content changed in the rebase;
vcpkg-overlays/is byte-identical to the pre-rebase tree.CI
A PR gate applies all patches at zero fuzz, runs the call-site audit, then builds and runs a focused set of upstream tests. It resolves the release from
patched-version.txtrather thanreleases/latest: the inventories pin upstream line numbers, so resolving "latest" at run time would have turned every upstream release day into a red gate on every open PR. The nightly audit still runs against the latest release, which is where drift belongs and where it must block the publishing build.Verification
All on a patched v11.8.1 tree built from this patch (
make, debug, GCC 15).db_basic_test*PortableLiveFileCapture*,*RecoveryBlobDirSynced*db_flush_test.BlobDirIsFsyncedForOrdinaryAndAtomicFlushcheckpoint_test(full)backup_engine_test(full)options_test(full)options_settable_testColumnFamilyOptionsAllFieldsSettablefails — identically on a pristine v11.8.1 tree (unset_bytes_base102 vs 126, both trees), so it is a GCC 15 artifact the patch neither causes nor masksThe PR gate this branch adds is green on its first-ever run (run 34369386485): the pinned download, the zero-fuzz apply, the call-site audit, and all five test binaries —
options_settable_testincluded, so the GCC 15 failure above does not reproduce onubuntu-latest.The regenerated patch applies to a fresh v11.8.1 tarball at
patch -p1 -F0andgit apply --check, and the tree it produces is byte-identical across all 28 patched files to the tree that was built and tested.The inventory audit passes against that freshly reproduced tree.
RocksDB's own
make checkwas run against the patched tree in an earlier round and found two real defects, both fixed here (a186bc7).Behavior of the checkpoint/backup guard, measured against the patched library rather than traced:
CreateCheckpoint/CreateNewBackupblob_dirunsetblob_dirset, one live blob fileNotSupported, no partial artifactblob_dirset, no blob file yetNotSupported(the case the guard adds)blob_dirset to the DB directory itselfNotSupported— still an absolute path that would not follow a copyThe resulting library passes
@harperfast/rocksdb-js's tiered-storage suite 19/19. That predates this round; it does not exercise checkpoint or backup.For the human reviewer
Reviewer sign-off at
729c24a. cb1kenobi's review closes the earlier blockers — "portable capture is latched under the DB mutex, backup and checkpoint refuse a configuredblob_dir, and directory fsync plus call-site audit coverage match the stated contract" — and rules the items below "documented C++ layering tradeoffs that rocksdb-js already enforces, not unfixed defects on this diff." So none of these blocks merge. Items 1, 4 and 6 remain live only as offers to do more than this diff does; the only thing actually gating merge is the ordering in item 7.blob_dirdatabase is a real operational constraint, and it is the decision most worth disagreeing with. It is strictly more conservative than the status quo — it only refuses backups that were already dangerous — but a tiered database can never be checkpointed or backed up through RocksDB's own tooling, at any point in its life. Say if you would rather have documentation only and accept the footgun.blob_dir, against a DB whose OPTIONS file still names one, admits a capture that copies that OPTIONS file — and restoring it can again aim obsolete-file cleanup at the source volume.rocksdb-jsrefuses such an open; the C++ API does not. Closing it in the engine means validating the captured OPTIONS artifact, which is the same layering question as item 3. Raised by review, left open deliberately.rocksdb-js, which compares against the persistedblob_dir. The layering is worth agreeing on.HasBlobFileAdditions()fix is total, so the crash is gone, but nothing pins it. Reaching that window from a test needs a second flush to claim the memtables inside theSyncClosedWalsmutex-release window, and any second flush re-enters the same sync point, so the usualLoadDependencyidiom deadlocks — forcing it means adding a newTEST_SYNC_POINTto upstreamdb_impl_compaction_flush.ccand widening the vendored patch. Say if you want that; it is a small change, just not a free one.cf_pathshole upstream is left open. Same sequence, same corruption, stock v11.8.1. Fixing it would change behavior for configurations this patch does not own.dbname_regardless of blob placement; it now honorsblob_dironly when explicitly set, so the default path is byte-for-byte unchanged. Two review legs argued it should unconditionally useGetBlobDir()— that is a real upstream inconsistency (withblob_dirunset andcf_paths.front() != dbname_, direct-write blobs are written to one and read from the other), but fixing it changes stock behavior on a path this patch does not own. Say if you would rather it were fixed here.experimental-patches/. Add opt-in experimental patches pipeline #15 landed the opt-in patch layer after this branch was written, and I had left the question open here because making the patch opt-in means the default prebuild loses the feature. #24 "Move the cf-blob-dir patch into experimental-patches/" settles it: it targets this branch, notmain, so it lands here first and this PR then merges tomainwith the patch already in the opt-in slot. It is a strict fast-forward of this branch, the patch file is byte-identical across the move (sha256 931b8b0a…), and its gate reproduces this PR's full test result under the new pipeline, so the move costs no coverage. The one thing that move does not settle is the consumer: official prebuilds will no longer carryROCKSDB_HAS_CF_BLOB_DIR, so rocksdb-js #767 must either pin an experimental prebuild or wait forblob_dirto graduate into the official overlay. That decision is tracked on Move the cf-blob-dir patch into experimental-patches/ #24, not here.patched-version.txtnow pins what the inventories were generated from. If that burden looks wrong, the alternative is proposingblob_dirupstream and waiting.Review findings declined this round with the evidence that refuted them, so you do not have to re-derive it: the
SstFileManager/DeleteSchedulertrash and space-accounting findings (CollectAllDBPaths()already includesGetBlobDir(), and bothdb_impl_open.cctrash cleanup anddb_impl_files.ccexisting-file tracking iterate it); aRepairDBfinding (repair.cchas noBlobFileNamesite and passesnullptrblob additions in stock 11.8.1, so tiering changes nothing there); a WAL-deadlock blocker on the second portability check (UnlockWALappears nowhere indb_filesnapshot.cc;wal_lockedonly observes whether another caller holds the lock, and upstream's own early returns in that block have the identical shape); and a recovery null-check (the unconditionaldata_dirderef one line above is verbatim upstream v11.8.1).Generated by Claude Opus 5.
🤖 Generated with Claude Code
https://claude.ai/code/session_011TPPxptQi3DTZk7UPDFsFQ
Complexity: complicated
Review-Coverage: authored=claude; ran=codex,gemini; declined=cursor-grok,cursor-composer,domain; rounds=5 @ 729c24a
Human-Review-Need: 4 @ 729c24a