Compare commits

..
86 Commits
Author SHA1 Message Date
Xingbo Wang 0e768876e5 Don't let error recovery wait forever behind a writer stalled on the failed flush (#15605)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15605

**Problem:** automatic error recovery can hang forever, with all writes stopped, when a writer is waiting on a write stop (for example the `max_write_buffer_number` memtable limit) as the flush that would end it fails. Only a restart clears it.

**Why:** the stalled writer leads the write queue. It waits in `DelayWrite()` for the stop to end, and keeps waiting while recovery is in progress, so any recovery step that enters the write queue waits behind it. Two such steps exist:
1. A retryable SST write error is a soft error, and recovery retries the flush of the immutable memtables, which does not need the write queue. But with `two_write_queues = true`, `ResumeImpl()` first enters both write queues to sync `LastSequence()` with the allocated sequence numbers.
2. A hard error with auto recovery, for example a retryable error syncing the closed WALs during the flush of a DB with two column families, is recovered by an atomic flush of all column families. Its memtable switch enters the write queue in every configuration. New writes already fail with the background error at this point (`PreprocessWrite()` checks `IsDBStopped()`); only a writer that was already waiting keeps waiting.

**Fix:**
- The sequence sync at recovery start no longer waits for a write queue whose leader is in a write stall. That leader stalls before it allocates sequence numbers, and it cannot leave the stall while recovery holds the DB mutex, so the sync stays exact. If the stall ends while recovery waits for the other queue, the sync retries. Queues without a stall are still drained first, and the recovery flushes that switch memtables keep their own full fence.
- `DelayWrite()` stops waiting on a write stop once the DB is stopped, and returns the background error, like a new write does.

**Tests:**
- In `error_handler_fs_test`:
  - `RetryableFlushErrorWithWriterStalledOnFlush` (with and without `two_write_queues`): the first flush fails with a retryable SST write error after the writer stops on its memtables; the writer finishes after recovery.
  - `FlushWALSyncErrorWithWriterStalledOnFlush`: with two column families, the closed WAL sync fails after the writer stops; the writer gets the hard error, recovery completes, and the next write succeeds.
- In `write_prepared_transaction_seqno_test`:
  - `RecoverySeqnoSyncRetriesWhenStallEndsDuringNonmemJoin`: the main queue stall ends while recovery joins the nonmem queue, and the writer allocates before recovery syncs; recovery retries and syncs only after the writer publishes.
  - `RecoverySeqnoSyncSkipsStalledWalOnlyCommit`: a WAL-only commit is stalled on the nonmem queue; recovery syncs and finishes while the commit still waits.

Reviewed By: archang19

Differential Revision: D127129804

fbshipit-source-id: 88ed4938be39d7222ceb7c28437187989885b5d0
2026-11-05 17:25:23 -08:00
Xingbo Wang 9cbdbcfce8 Update RocksDB main for 11.14 release (#15604)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15604

Why: keep internal RocksDB main ready for 11.15.0 development while the 11.14 release is cut.
Previous behavior: Conveyor opened a GitHub pull request for this update, which could be missed and was not stacked with the internal release diff.
New behavior: Conveyor owns an internal Phabricator diff that the release-import diff stacks on top of. The release manager exports this diff manually with ExportIt.

Implementation:
- Bump version.h from 11.14.0 to 11.15.0
- Add `11.14.fb` to `check_format_compatible.sh`
- Sync HISTORY.md `## 11.14.` section from `11.14.fb`
- Delete 53 consumed `unreleased_history/` note file(s) from main

Safety and unsupported cases:
- Only version.h, check_format_compatible.sh, HISTORY.md, and consumed release-note files are allowed.
- Open legacy GitHub update-main PRs block this flow; this automation never modifies, exports, or lands them.
- Concurrent direct CLI invocations are unsupported; Conveyor serializes the Release Cut node to one active run.
- The tool refuses unrelated commits, mismatched release metadata, and non-canonical GitHub repositories.

Known limitations:
- ExportIt remains manual. Bot create permission for internal_repo_rocksdb/repo is a rollout prerequisite.

Reviewed By: omkarhgawde, archang19

Differential Revision: D129984171

fbshipit-source-id: 2e9612c1c3b68f69c6252f191c24975a6ab26210
2026-11-05 13:34:42 -08:00
cm-sally-mate a4e298049a FileIngestionHandle::GetPreparedFiles: metadata of the files a prepared ingestion makes visible (#15593)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15593

**What:** `FileIngestionHandle` gets `GetPreparedFiles()`. It returns an `SstFileMetaData` for each file that the prepared ingestion makes visible on commit. Set fields: `directory`, `relative_filename`, `name`, `db_path`, `file_number`, `size`, `temperature`, `file_checksum`, `file_checksum_func_name`, `smallestkey`, `largestkey`, `num_entries` and `num_deletions`. With `allow_db_generated_files`, `smallest_seqno` and `largest_seqno` too, since commit keeps the file's own seqnos; otherwise commit assigns a global seqno and they stay 0. Commit also decides `epoch_number`, and `file_creation_time`/`oldest_ancestor_time` are the commit-time clock, so those keep their defaults. The list is empty after the handle is committed or aborted.

**Why:** a caller can read or verify the prepared files before commit makes them visible. Commit keeps each file's path, number, size and checksum: `MergeForSameColumnFamily` moves each `IngestedFileInfo` unchanged, and commit only edits the MANIFEST.

**How:** one new pure virtual on `FileIngestionHandle`. `FileIngestionHandleImpl` builds the entries from `files_to_ingest()` of each job, using the existing public `SstFileMetaData` struct. The directory comes from the column family's `cf_paths` and the file name from `MakeTableFileName`, the same way `TableFileName` builds the path.

**Not listed:** the range-tombstone SST that an `atomic_replace_range` prepare writes on a universal-compaction column family. Commit makes it live only on a partial overlap and deletes it otherwise, so a listed entry could name a file that never goes live.

**Risk:** low. Additive API; no behavior change for existing callers.

Reviewed By: archang19, joshkang97

Differential Revision: D129771271

fbshipit-source-id: be6e89d14bb710f787ad0514a839883f23731d3d
2026-11-05 13:02:47 -08:00
cm-sally-mate a9bcab5e75 Fix flaky ExternalTableTest.FactoryWithoutTableBuilderIsNeverCompacted (#15594)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15594

**Why:** `ExternalTableTest.FactoryWithoutTableBuilderIsNeverCompacted` fails intermittently, mostly in TSAN builds: `table_test.cc` reports `ingested[cf].size()` is 0, expected 1.

**Old behavior:** the test ingests one file into a FIFO column family whose `max_table_files_size` is 1, so any ingested file exceeds the limit. Ingestion schedules that column family's compaction right away. The test then reads the column family's live files to check that the ingestion added one. If the background FIFO compaction runs first, it has already deleted the file, and the check fails. This is a race in the test only; RocksDB behaves as intended.

**New behavior:** the test calls `PauseBackgroundWork()` before the two ingestions and `ContinueBackgroundWork()` after it checks them. Then it waits for compaction as before. The writable column family still loses its file to FIFO compaction, and the column family without a table builder still keeps its file. The test checks the same things, in a fixed order.

**Safety:** test-only change. The memtables are empty, so ingestion does not need a flush while background work is paused.

Reviewed By: joshkang97, archang19

Differential Revision: D129959521

fbshipit-source-id: 4f781efe2122f4bd40aef4da82854c5ca28f5499
2026-11-05 10:42:38 -08:00
Robert Pan a24f8f8785 Honor SstPartitioner before a bottommost range tombstone (#15590)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15590

Why the change is required:
`IngestExternalFiles()` with `fail_if_not_bottommost_level=true` can fail with `TryAgain` indefinitely when a range tombstone stretches an existing bottommost file across an `SstPartitioner` boundary. Recompacting the range does not help, because the same input produces the same bracketing file. MyRocks hits this in its tmp-rdb copy ALTER, which compacts the destination range with a partitioner that cuts at the new index boundaries and then ingests into Lmax. The ALTER fails with error 525 when another DDL creates and drops an index in the same column family while the ALTER runs, and a snapshot older than the drop is open. This can happen on any server that runs DDL concurrently, a primary or a replica whose applier runs DDL in parallel; it was first seen on replicas with `replica_parallel_type=DATABASE`.

Previous behavior:
At the bottommost level, `CompactionOutputs::AddToOutput()` returned early for a range-del sentinel key, before `ShouldStopBefore()`. As a result, the partitioner was never consulted for that key. At the bottommost level a range tombstone is kept only while a snapshot older than it exists. When it is kept, `AddRangeDels()` adds it to the current output file and extends that file's largest key to the tombstone's end key, even if the partitioner would have required a cut between the last point key and the tombstone start.

Triggering conditions, all of which are required:
- A partitioner returns `kRequired` at a boundary B.
- A bottommost compaction outputs point keys below B, followed by a range tombstone starting at or above B, with no point key in between.
- A snapshot keeps the tombstone alive.

The resulting file spans B, so no file whose smallest key is at or after B and below the tombstone end fits in the bottommost level.

In MyRocks, the tombstone is the `[id, id+1)` DeleteRange written by the drop-index thread for a dropped index whose id is above the ALTER's newly reserved index ids.

New behavior:
A bottommost range-del sentinel cuts the output file when the partitioner returns `kRequired` for the sentinel's start key, unless the open file's largest key has the same user key. The tombstone then starts its own file, and the file holding the preceding point keys ends below B.

Implementation:
- `AddToOutput()` keeps returning early for a bottommost sentinel unless a file is open, its largest user key differs from the sentinel's, and the partitioner requires a cut. Each branch now decides `cut_before` explicitly. On a cut, it closes the current file exactly as the non-bottommost sentinel path does, including setting `range_tombstone_lower_bound_` from the sentinel key.
- The partitioner check is factored out of `ShouldStopBefore()` into `PartitionerRequiresCut()`, so both paths use the same fast prefix path and per-key callback. It is only meaningful while an output file is open and asserts `HasBuilder()`. `ShouldStopBefore()` behavior is unchanged.
- The same-user-key guard is needed because a bottommost point key can have its seqno zeroed (`k@0`), which sorts after a sentinel `k@s` for an older tombstone starting at the same user key. Cutting there would close the file with a next-file key below its own largest key: debug builds assert in `AddRangeDels()`, and with a newer snapshot-pinned tombstone in another input run, release builds install two overlapping bottommost files and fail with Corruption. No partition boundary can fall inside one user key, so skipping the cut loses nothing. Prefix partitioners and the MyRocks index-boundary partitioner never cut between equal keys; a partitioner that cuts at every key, or by file size, can.

Safety, compatibility, and limitations:
- Only bottommost compactions with a configured `SstPartitioner` are affected. Without a partitioner, `PartitionerRequiresCut()` returns false and output is unchanged. Grandparent-overlap and file-size cuts still never trigger on a bottommost sentinel.
- Such compactions can now produce one more, smaller output file per cut. The cut is decided before `AddRangeDels()` decides whether to drop the tombstone, so it also happens when the tombstone is about to be dropped (the common no-snapshot case). The file opened for it then usually ends up empty: it is written, synced and closed, then deleted by the existing empty-output cleanup in `FinishCompactionOutputFile()` before the compaction installs its outputs, so it never enters the LSM. Listeners see `OnTableFileCreated` with `Aborted("Empty SST file not kept")`, and the output-file count includes it. Predicting tombstone survival in `CompactionOutputs` would avoid this but add complexity, so the cost is accepted. In a microbenchmark with a range delete over every other one of 2000 prefixes, it added 999 such files and raised `CompactRange` from 797 ms to 1155 ms. In MyRocks, the index-boundary partitioner only cuts at index ids registered for an in-flight bulk load or tmp-rdb ALTER, so the extra file appears only in compactions within such a window.
- The same applies to the proximal-level output of per-key placement (tiered storage), which shares the bottommost compaction. A snapshot-pinned tombstone there no longer shares a file with the preceding hot key when the partitioner requires a cut; `TieredCompactionTest.LevelOutofBoundaryRangeDelete` and `UniversalRangeDelete` asserted the old layout (one proximal file holding a data key and the tombstone) and now expect the tombstone in a file of its own.
- No API, option, or on-disk format change.
- Not addressed: a partitioner cut inside a single tombstone's range. A tombstone still cannot be split across files at the bottommost level, as before.

Reviewed By: pdillinger

Differential Revision: D129661896

fbshipit-source-id: 1c38b00b1fd80c679a094b124db6f097b122b5af
2026-11-05 08:40:29 -08:00
Peter Dillinger 9d3733d9d8 Accurate SST file times, plus DB ownership metadata for SST files (#15583)
Summary:
Ideally, each age-based feature acts on the time that matches its purpose, which requires tracking three different times for each SST file:
* How old its data is (when its keys were written, as bounded by the oldest ancestor time and the `newest_key_time` table property), so that TTL expires data based on its actual age.
* When the file's contents were built, i.e. when its data last went through a flush or compaction (`file_creation_time`). Periodic compaction (commonly used so that all data regularly goes through a compaction filter) and `GetCreationTimeOfOldestFile()` should act on this.
* When this DB began holding the file. `bottommost_file_compaction_delay` should act on this: it limits how soon a recently written bottommost file is rewritten alone, after snapshot releases, to zero its sequence numbers or drop data no longer protected, so that a series of snapshot releases does not rewrite the same data repeatedly. External storage accounting (how long each file occupies storage) also needs this time.

Knowing how the DB got each file (built it, or ingested or imported it) is also useful, e.g. for storage accounting.

Previous behavior:
A file's age for periodic compaction comes from its MANIFEST `file_creation_time`, then (only if the table reader is pinned) the `file_creation_time` table property, then the oldest ancestor time, then the file's mtime (unchanged by this commit). However:
* Files written by `SstFileWriter` had table property `file_creation_time` = 0 (unknown).
* Files flushed during WAL recovery had `file_creation_time` = 0 in both the table properties and the MANIFEST.
* `RepairDB` restored each file's oldest ancestor time from its table properties but not its `file_creation_time`, so every repaired file lost its MANIFEST creation time.
* `IngestExternalFile()` and `CreateColumnFamilyWithImport()` set both `file_creation_time` and the oldest ancestor time to the ingest/import time, ignoring times carried by the file's table properties or the export metadata. Data written or last compacted long before ingestion was therefore not due for periodic compaction (or TTL) until a full period after ingestion. Also, `Checkpoint::ExportColumnFamily()` left the time fields of its export metadata unset.
* A FIFO temperature change done as a trivial copy (with `allow_trivial_copy_when_change_temperature`) recorded the copy time as the new file's MANIFEST `file_creation_time`, although its contents and table properties are the original's.
* FIFO TTL compaction picks expired files oldest first, judging each by the `newest_key_time` table property or else the oldest ancestor time table property. `SstFileWriter` files have neither, so an ingested `SstFileWriter` file stopped the scan: neither it nor any newer file was ever deleted by TTL.
* Nothing recorded when the DB began holding a file, or whether it was ingested, imported, or built by this DB. (For ingested and imported files, `file_creation_time` happened to be the time they were added, which `bottommost_file_compaction_delay` used.)

New behavior, for when contents were built:
* The block-based table builder now records the `file_creation_time` property itself, reading the clock when it finishes the file, so that the time is no earlier than when any entry was added. So every block-based SST file carries one (unless reading the clock fails), including `SstFileWriter` files, whose entries can be added over a long time. (For flush and compaction outputs, the MANIFEST value is still read when the output file is started, so it can be slightly earlier than the property.) The WAL recovery flush now also sets the MANIFEST value, and `RepairDB` restores it from the table property.
* Ingestion and import keep the creation and oldest ancestor times carried by the file when known: for ingestion, from its table properties; for import, from the export metadata, or else the file's table properties. A carried time later than the current time suggests an unreliable clock, so it is logged as a WARNING and replaced with the current time; an unknown time becomes the current time. When no oldest ancestor time is carried (true of all `SstFileWriter` files), it falls back to the resolved creation time, an upper bound on when the data was written, so TTL never fires early. `ExportColumnFamily()` now fills in both time fields.
* A FIFO temperature-change copy keeps the original's `file_creation_time`.
* FIFO TTL compaction falls back to `file_creation_time` when a file has neither of the table properties above. Every key in a file was written no later than the file's contents were built, so this never deletes early.
* New option `reject_too_old_file` (default false) in `IngestExternalFileOptions` and `ImportColumnFamilyOptions` fails the whole ingestion/import with `InvalidArgument`, before any file is linked or copied, if any file is already too old for the column family to keep without recompaction or deletion: for leveled and universal compaction, if its known creation time is older than the effective `periodic_compaction_seconds`; for FIFO compaction, if its newest data (the `newest_key_time` table property when known, else its creation time) is older than `ttl`. Such data should already have been recompacted (e.g. through a compaction filter) or deleted. Leveled `ttl` is not considered because it does not limit how long data can be kept, only how long it stays above the last level. Files without the needed time are not rejected. C and Java bindings included.

New behavior, for when the DB began holding a file:
* New `SstFileMetaData::db_ownership_start_time` (also in `LiveFileMetaData` and export metadata), backed by a new MANIFEST field: the time this DB began holding the physical file (file number). That is when the DB started writing it (flush or compaction output, local or remote), when ingestion linked or copied it into the DB (in the prepare phase, for two-phase ingestion), when import did, or when a FIFO temperature-change copy wrote it. Moves between levels preserve it. A long compaction's outputs start when they are created, not when the compaction finishes.
* New `SstFileMetaData::db_ownership_info`, decoded by the new public struct `DbOwnershipInfo`, backed by another MANIFEST field: how the DB got the contents (flush, compaction, ingestion, or import) plus sticky flags for things that have happened to the file without its contents being rewritten (moved between levels; copied to a new temperature; temperature updated from the file system by `UpdateManifestForFilesState()`; recorded by `RepairDB`).
* `bottommost_file_compaction_delay` is now measured from `db_ownership_start_time` (falling back to `file_creation_time` when unknown). For files built by the DB that is the same as before; ingested files again get the delay from when they were ingested, rather than skipping it because of old contents (which only matters for ingested DB-generated files, since normally ingested `SstFileWriter` files have sequence number 0 and are never marked).

Neither new field is a table property: they describe one DB's relationship to a file, and the same file contents can be added to several DBs. Times not recorded, for lack of a consumer: when the DB first held the contents across temperature copies, when a file became visible (MANIFEST commit time), and when a file arrived at its current level.

Implementation notes:
* `TableBuilderOptions::file_creation_time` (and its positional constructor parameter) is removed, since the builder now reads the clock itself. All callers passing arguments past that position were updated.
* `ResolveExternalFileTimes()` and `CheckExternalFileNotTooOld()` are shared by ingestion and import.
* The new MANIFEST fields use new safe-to-ignore `NewFile` tags, written only when known. Their packed bit layout is defined next to the tags; unknown bits are ignored on read and preserved when a file's record is carried over.
* New `VersionEdit::AddExistingFile()` re-adds a file under the same file number with all of its persisted metadata. It replaces field-by-field copies into the positional `AddFile()` at the five places that move files (trivial move, `CompactRange()` with `change_level`, `PromoteL0()`, the level migration at open, and `UpdateManifestForFilesState()`) and in the MANIFEST snapshot writer, so MANIFEST-only fields, including future ones, carry over. The WAL recovery flush, `RepairDB`, and import now use the `FileMetaData` overload of `AddFile()`; as a side effect, recovery flush outputs now also record min/max timestamps and file open metadata in the MANIFEST, as regular flush outputs do.

Compatibility and limitations:
* Behavior change: ingesting or importing old data can make it due for periodic compaction or TTL soon afterward rather than a full period later, so bulk-ingesting old SSTs can cause a burst of compactions. Users who must not publish such data can set `reject_too_old_file`.
* Files already in a DB keep their MANIFEST entries; the new fields are unknown (0) for them. For files ingested, imported, or temperature-copied by older versions, `file_creation_time` was the ingestion, import, or copy time, so the `bottommost_file_compaction_delay` fallback is accurate for them. Files from older `SstFileWriter` versions carry no creation time and still get the ingestion time.
* Older RocksDB versions ignore the new MANIFEST fields and drop them when rewriting the MANIFEST. A DB opened from a checkpoint or backup keeps the values recorded by the DB it was copied from. `RepairDB` loses them for files that existed before the repair.
* Plain and cuckoo table builders still record no time properties.
* The times recorded for remote compaction outputs come from the remote worker's clock.
* Possible follow-ups: letting `SstFileWriter` callers supply an authentic oldest ancestor time; and avoiding bottommost-files compactions that rewrite an ingested `SstFileWriter` file only to zero a global sequence number that exists only in the MANIFEST (found by code reading, not yet confirmed by a test).

Pull Request resolved: https://github.com/meta/rocksdb/pull/15583

Test Plan:
New tests, using mock clocks where time matters:
* `DBTablePropertiesTest.FileCreationTimeAlwaysSet`: `SstFileWriter` file plus ingestion, WAL recovery flush, and `RepairDB` all record a creation time, checked in both table properties and the raw MANIFEST `FileMetaData` (`GetLiveFilesMetaData()` would mask a missing MANIFEST value by falling back on pinned table properties).
* `ExternalSSTFileTimeTest.*` (external_sst_file_test): `KeepRecordedTimes` covers recorded, unknown, and future creation times; `KeepDBGeneratedFileTimes` checks that `allow_db_generated_files` keeps both times from the source DB; `PeriodicCompactionFromWriteTime` checks that periodic compaction triggers relative to when an `SstFileWriter` file was written, not when it was ingested; `RejectTooOldFile` covers leveled, universal, and FIFO: a batch containing a too-old file is rejected with the DB unchanged, a file just under the limit and a file with unknown creation time are accepted, and the option off accepts the too-old file; `RejectFIFOFileWithOldNewestKey` checks that for FIFO, a recently built file whose newest key time is older than `ttl` is rejected; `CreationTimeIsWhenFileIsFinished` checks that an `SstFileWriter` file's creation time is when it was finished, not opened; `FIFOTtlDeletesIngestedFile` checks that an ingested `SstFileWriter` file and a newer flushed file expire by TTL; `RecordsDbOwnership` checks the ownership start time and origin for one-phase ingestion, two-phase ingestion (from the prepare phase, not the commit), and ingestion of a DB-generated file; `BottommostCompactionDelayFromIngestion` checks that an ingested DB-generated file with old contents is not compacted as a bottommost file until the delay has passed since ingestion.
* `ImportColumnFamilyTest.ImportKeepsExportedFileTimes` and `ImportFileTimesAndRejectTooOldFile`: same coverage for export/import, plus falling back on the file's table properties when the import metadata has no time, and ownership metadata in the export and after import.
* `DBOwnershipTest.*` (db_etc3_test): ownership start time and origin of flush, WAL recovery flush, and compaction outputs (including a compaction delayed after creating its output), and in public metadata; preservation, with the "moved" flag, through `PromoteL0()`, trivial move, `CompactRange()` with `change_level`, the level migration at open, and reopens (MANIFEST replay and snapshot); FIFO temperature-change copy (same creation time, new ownership start time, "temperature copied" flag); `UpdateManifestForFilesState()`; and `RepairDB` (unknown ownership for pre-existing tables, flush origin for tables built from WALs).
* `CompactionServiceTest.BasicCompactions`: remote compaction outputs record ownership like local ones.
* `VersionEditTest.*`: MANIFEST encoding of the new fields (round trip, preserved reserved bits, written only when known, safe to ignore, debug output), the persisted `DbOwnershipInfo` bit layout, `AddExistingFile()`, and the fallback for an unknown ownership start time.
* Java getter/setter tests for the new option. The JNI code was compiled against `javac`-generated headers, but the Java tests were not run locally.

`FIFOTtlDeletesIngestedFile` was verified to fail before the FIFO TTL fix. `BottommostCompactionDelayFromIngestion`, `FIFOTemperatureChangeCopy`, `MovesPreserveOwnership`, `TemperatureUpdatedFromFileSystem`, and `AddExistingFile` were verified to fail with, respectively, the delay measured from `file_creation_time`, the copy time used as creation time, and `AddExistingFile()` dropping the ownership start time. `CreationTimeIsWhenFileIsFinished`, `RejectFIFOFileWithOldNewestKey`, and `ImportFileTimesAndRejectTooOldFile` were verified to fail with, respectively, the creation time read when the builder is created, FIFO rejection based only on creation time, and import ignoring the file's table properties. The creation-time tests were verified to fail with the new logic stubbed out to the old behavior before rebasing onto current main, and `ImportKeepsExportedFileTimes` also fails with only the `ExportColumnFamily()` fix reverted.

Adjusted `ExternalSSTFileBasicTest.AtomicReplaceRangeAvoidsRedundantTimestampScans`: it models legacy files with no recorded creation time and relied on `SstFileWriter` files having a zero `file_creation_time` property (pinned readers fall back on it). It now writes those files with a writer clock that reads 0.

Reviewed By: xingbowang

Differential Revision: D129668538

Pulled By: pdillinger

fbshipit-source-id: 1e8ad8e9738d7b0427c408d693bc22c46deca913
2026-11-04 16:31:52 -08:00
cm-sally-mate 8437f701ce Fix MultiGet with empty prefix filter partitions (#15589)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15589

Why the change is required:
With partitioned filters and `whole_key_filtering=false`, filter partitions hold only prefixes. Keys outside the prefix extractor's domain (for example keys shorter than N with `fixed:N`) are never added. When filter partitions are cut together with index partitions (`decouple_partitioned_filters=false`, which is also how files from older versions are laid out), a partition that covers only such keys is empty, and its handle has size 0. `MultiGet` dropped every key mapped to such a partition, so it returned NotFound for keys that exist.

Previous behavior:
`PartitionedFilterBlockReader::MayMatch(MultiGetRange*)` looks up the filter partition of every key in the batch before any per-key domain check. For a size-0 handle it called `SkipKey()`, after `assert(false)` with the comment "Not reachable". So:
- Debug builds: `MultiGet` fails the assertion.
- Release builds: `MultiGet` returns NotFound for existing out-of-domain keys. In a release-build repro, `get a` returned the value while `multi_get` of the same 10 short keys reported 9 of them as not found.
`Get` is not affected: it checks `InDomain()` before it consults a prefix filter.

New behavior:
A key mapped to an empty filter partition is ruled out only when the filter could have contained it: in whole-key mode, or when the key is in the prefix extractor's domain. Out-of-domain keys stay in the batch, as with `Get`.

Implementation:
The size-0 handle branch in `PartitionedFilterBlockReader::MayMatch(MultiGetRange*)` now checks `prefix_extractor->InDomain()` before `SkipKey()`, and the unreachable-path assertion is removed.

Safety, compatibility, and limitations:
Read-side only, no format change. Existing files with empty filter partitions read correctly after this change. Decoupled filter partitions (the current default) are cut by filter size, so new files rarely have empty partitions.

Reviewed By: pdillinger

Differential Revision: D126811019

fbshipit-source-id: d154acc42ded8d1fe9637e03e9d52bbd9ec3177e
2026-11-04 16:14:22 -08:00
cm-rockee c9fcb564f8 Distinguish captured compaction pool metadata from the DB option (#15587)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15587

`use_separate_remote_compaction_pool` is a mutable DB setting, while the same name on job info structs records the pool selected for an already scheduled job. Using an option-style name for that snapshot makes callback metadata look like another configuration knob.

Rename the captured field to `separate_remote_compaction_pool_selected` in `CompactionJobInfo`, `SubcompactionJobInfo`, and `CompactionServiceJobInfo`, together with the backing `Compaction` field and its accessors. The selection is still captured at the same points, stays stable across later DB option updates, and remains selected when execution falls back to local workers. The DB option, runtime option string, defaults, serialization policy, and scheduling behavior are unchanged.

Regenerate the C bindings to expose getters using the metadata name. Retain the previous C getter names as compatibility aliases, preserving existing exported signatures.

Requested by: Ranjan Banerjee (rbanerjee2)

Reviewed By: pdillinger

Differential Revision: D129730425

fbshipit-source-id: 48cc35f35b6479a37a0b94aeef9f09b87d158644
2026-11-04 15:14:48 -08:00
Chaitanya Datye 78eb4900c8 Let table factories declare that they cannot build tables (#15586)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15586

This adds `TableFactory::IsTableBuilderSupported()` and `ExternalTableFactory::IsTableBuilderSupported()`, so a table factory that only reads files produced elsewhere can say so, and makes RocksDB keep every compaction away from a column family that uses one.

Some external table factories read a format that RocksDB cannot write: their files are produced by an outside pipeline and only ever ingested, and `NewTableBuilder()` returns a builder that fails. Today nothing tells RocksDB this, so any flush or compaction that reaches such a factory fails in the background, and the resulting background error stops writes to the whole DB, not just to that column family. Flushes can already be ruled out with `disallow_memtable_writes`, but a manual compaction cannot: with a single ingested file on the last level, a default `CompactRange()` is a no-op, while `CompactRange()` with `BottommostLevelCompaction::kForce` rewrites the file, fails with the builder's error, and leaves the next `Put()` to the default column family failing with the same status. Automatic compactions have the same problem unless the user remembers `disable_auto_compactions`.

With this change, a factory that returns false gets four guarantees. Opening or creating a column family with it fails with `InvalidArgument` unless `disallow_memtable_writes` is set, so nothing can reach a flush. `ColumnFamilyData::NeedsCompaction()` returns false, so automatic compactions are never scheduled, whatever the compaction style and `disable_auto_compactions`. `CompactRange()` and `CompactFiles()` return `NotSupported` before doing any work, so a caller gets a foreground error and the DB stays writable. And its L0 files and pending compaction bytes never stall or stop writes, the same as for a column family with `disable_auto_compactions`: since nothing can compact it, counting that backlog would let one such column family stop writes to the whole DB for good, for example after an overlapping ingestion lands in L0 of a column family whose L0 stop trigger it reaches. This follows `IsDeleteRangeSupported()`: the column family caches the capability when it is created, and the external table adapter caches the wrapped factory's answer so that RocksDB never calls external code while holding the DB mutex.

The default is true, so existing table factories behave as before. Blocking automatic compactions also blocks compactions that need no builder, such as FIFO deletions and trivial moves; a factory that cannot build tables should not rely on them. `OpenAndTrimHistory()` calls `CompactRangeInternal()` directly, but only for column families with user-defined timestamps, which external tables reject.

Changes:
1. `TableFactory::IsTableBuilderSupported()` and `ExternalTableFactoryBase::IsTableBuilderSupported()` default to true; `ExternalTableFactoryAdapter` caches and forwards the wrapped factory's value, including after `PrepareOptions()`.
2. `ColumnFamilyData` caches the capability, `NeedsCompaction()` returns false without it, and `ColumnFamilyData::ValidateOptions()` requires `disallow_memtable_writes` without it.
3. `GetWriteStallConditionAndCause()` takes the capability and, like `disable_auto_compactions`, ignores L0 file counts and pending compaction bytes without it; its three callers pass the column family's cached value.
4. `DBImpl::CompactRange()` and `DBImpl::CompactFiles()` return `NotSupported` for such a column family.
5. `ExternalTableTest.FactoryWithoutTableBuilderIsNeverCompacted` compares two FIFO column families holding the same ingested file, one with a factory that can build tables and one without, and checks creation-time validation, that only the first is compacted automatically, that `CompactRange()` with and without `kForce` and `CompactFiles()` return `NotSupported` for the second, and that the DB stays writable and the file readable.
6. `ExternalTableTest.FactoryWithoutTableBuilderDoesNotStallWrites` ingests two overlapping files into a two-level column family without a builder, so one lands in L0 and reaches every L0 trigger, and checks that writes to the default column family are neither stopped nor delayed.

Reviewed By: xingbowang

Differential Revision: D128259405

fbshipit-source-id: f52221321a1f3ae20725d41632a3c61bde0738f5
2026-11-04 13:05:08 -08:00
Jay Huh 15951afbb5 Experimental live tuning: measure, recommend, apply, watch, keep or revert option changes (#15563)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15563

Why:
Tuning RocksDB options for a workload is manual. LLMs can now reason about RocksDB's stats and workload well enough to propose option changes, and RocksDB already has the signals (DB properties, statistics, the query tracer) and can change mutable options online. This adds the loop that connects them: an on-demand tuning cycle inside RocksDB that measures the DB, asks an advisor for changes, applies them, watches the result and keeps or reverts them, with every step pluggable and logged.

What (EXPERIMENTAL; nothing runs unless the application calls `StartLiveTuneCycle()`):
- One cycle: baseline metrics from the DB's own properties -> a short query trace (`DB::StartTrace()`) and its analysis -> a recommendation -> checks (allowed options, bounds, `max_changes`, `max_step_ratio`) -> `SetOptions()` / `SetDBOptions()` (`skip_apply` stops before this) -> each following window compared with the baseline and decided on -> kept or reverted.
- API (`include/rocksdb/live_tune.h`): `StartLiveTuneCycle()`, `GetLiveTuneStatus()`, `StopLiveTuneCycle()`, `GetLiveTuneAppliedOptions()`, and `LiveTuneOptions`: the intent (a goal in plain words, allowed options with bounds, optional constraints), window and limit settings, `skip_apply`, `allow_compaction`.
  - `GetLiveTuneAppliedOptions()` returns the options live tuning has changed and not reverted, with their values: the running cycle's changes (from just before they are applied until they are reverted) and the changes kept by cycles since the DB was opened. An application that also sets options, e.g. by re-applying its own configuration periodically, uses it to leave those options alone instead of undoing the cycle's change.
- Changes that take effect only on SST files written after them (e.g. `block_size`): the advisor says so with `LiveTuneRecommendation::requires_compaction`. With `allow_compaction` the cycle compacts the column family (`CompactRange()`) after applying them and before watching; without it, such a recommendation is rejected. Table-property metrics (`avg_data_block_bytes`, `index_bytes`, `filter_bytes`) show their effect.
- With a `Statistics` object, the metrics also come from the DB's tickers: `db_keys_read` (Get and MultiGet keys), `db_iterator_seeks`, `db_iterator_steps`, `db_block_cache_data_hits` and `db_block_cache_data_misses`, with derived `sst_file_reads_per_key` (the column family's SST file reads per key read, sought or stepped) and `db_block_cache_data_hit_ratio`. Read efficiency can then be judged per key rather than on raw counts that follow traffic, with no help from the application. Tickers cover every column family, and every DB sharing the `Statistics` object, so their metrics carry a `db_` prefix; the public header lists which metrics are per column family and which are for the whole DB.
- Pluggable:
  - `LiveTuneAdvisor` (required): recommends changes, e.g. by asking an LLM, with the objectives and constraints to judge them by.
  - `LiveTuneTraceAnalyzer`: summarizes the trace; the built-in one uses RocksDB's `TraceAnalyzer` (parent change).
  - `LiveTuneVerdict`: `GetWindowDecision()` after every watch window: keep watching, keep the changes or revert them. By default a broken constraint reverts, and a change is kept once a one-sided 95% Wilson lower bound of winning windows reaches `min_confidence`. An override can call the default and add its own checks, e.g. revert a change the default would keep.
  - `LiveTuneMetricsProvider`: the application's own metrics.
  - `LiveTuneEventListener`: every step as a JSON event, and `OnCycleDone()` with the final result, e.g. to roll a kept change out to other DBs.
- Every event also goes to the info LOG as `LIVE_TUNE {json}`.

Safety and limits:
- Only allowed options within bounds change, through `SetOptions()` / `SetDBOptions()`; they are reverted on a broken constraint, a revert decision, `StopLiveTuneCycle()` or DB close. A change that mixes column family and DB options is checked before anything is set, and if `SetDBOptions()` still fails after `SetOptions()` succeeded, the column family options are restored, so nothing stays half applied. Because `SetOptions()` and `SetDBOptions()` can fail with the new values already in effect (for example when the OPTIONS file write fails), the values are read back after an error to decide what is in effect. A revert restores the values read right before apply, after the advisor call, so it doesn't undo an option the application changed during the trace or the advisor call; if a change is still in effect after a revert, the outcome ends with "; revert failed: <status>" and `GetLiveTuneAppliedOptions()` still lists it. One cycle per DB at a time. Any DB opened read-only (read-only, compacted, secondary or follower) returns NotSupported.
- Lifecycle: an event listener may stop the cycle from its callbacks (the stop is only requested), and starting the next cycle from a callback returns Busy; start it from another thread, e.g. once the "done" event is seen. Starting a new cycle joins the previous cycle's thread without holding the live tune mutex. The advisor's `CancelRecommendation()` is called only while its `GetRecommendation()` is in progress.
- Option values are read and written with RocksDB's own options serde: single options by name through `CFOptionsAsConfigurable()` / `DBOptionsAsConfigurable()` and `Configurable::GetOption()`, including dotted fields of a struct or of the table factory (`compaction_options_universal.size_ratio`, `block_based_table_factory.block_size`), which `SetOptions()` also takes directly, keeping the block cache and filter. Recommended values are rewritten the way RocksDB writes them (e.g. `32K` as `32768`) by parsing them as their option, so checks and read-back comparisons are type-aware. They are parsed into a copy of the table factory and its filter policy, because the table factory parser sets a mutable field under it (a ribbon filter's `filter_policy.bloom_before_level`) on the existing object, so checking a recommendation never changes the DB; a value the option doesn't accept is rejected ("rejected: invalid value: ...") before anything changes, and like any rejected recommendation it is logged with its changes and recorded in the history the next cycle's advisor gets. Numbers are parsed with `strtod`, like RocksDB's own parsing of double options.
- `max_step_ratio` bounds every numeric change; a move to or from 0 or a negative value (often "disabled" or "no limit" in RocksDB) is accepted only within explicit intent bounds.
- If the current values of the allowed options cannot be read (an option fails to serialize), the cycle ends as "rejected: cannot read the current option values: <status>" before anything changes, rather than recommending from incomplete values. If applying fails, the previous values are restored the same way as a revert, read back, and the outcome adds "; revert failed: <status>" only for a change still in effect. An allowed option that neither the column family nor the DB options have makes `StartLiveTuneCycle()` return InvalidArgument, before any baseline.
- Objectives and constraints must be on measured metrics: a recommendation with changes that names an unmeasured one is rejected (an answer with no changes is "no change" whatever objectives it lists), and an enforced constraint (the intent's, or `default_constraints` when the intent has none) on an unmeasured metric ends the cycle right after the baseline ("rejected: intent constraint metric <metric> is not measured", or "default constraint metric"), before any trace or advisor call, instead of never being checked.
- Each window is compared with a baseline taken before the change (no control group). If the application already has a trace running (`DB::StartTrace()`), the cycle leaves it alone and goes on without a trace (the "traced" event has status Busy); if the application starts one while the cycle traces, the cycle's trace ends and the application's keeps running. This uses two internal `DBImpl` methods, `StartTraceIfNone()` and `EndTraceIfCurrent()`; `DB::StartTrace()` itself still replaces a running trace. A trace file left by a cycle that ended with the process (e.g. a crash) is deleted when the next cycle on the DB starts. SST read metrics come from the file read histograms read without a lock, so they are approximate. Table-property metrics come from `GetPropertiesOfAllTables()`, which reads the files' properties without holding the DB mutex. Not yet supported in db_bench, db_stress, Java or the C API.

Reviewed By: xingbowang

Differential Revision: D128710591

fbshipit-source-id: 06b7af65841593870e9c2325c0f305bca94c7787
2026-11-03 17:37:21 -08:00
Michael C Huang a51c5e21ec Repair missing blob references during manual compaction (#15538)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15538

What changed?

- Adds `CompactRangeOptions::skip_missing_blob_files`, default `false`, for manual repair when referenced blob files are absent from the compaction's input Version.

https://pxl.cl/j8V47

- Rewrites selected SSTs. Lost scalar values become deletions. Wide entities keep surviving columns.
- Manual `CompactRange` only. Runs locally; compaction services are bypassed. FIFO is unsupported.

Why?

Best efforts recovery can leave dangling blob references in SSTs. These can cause later compactions to fail. Offline repair prepares the DB for normal reopening.

Reviewed By: xingbowang

Differential Revision: D128836898

fbshipit-source-id: 3acdf3b2deb3193dd181e748522d85e8efd64d97
2026-11-03 16:15:46 -08:00
Steph Pontikes 8fa3ee8fdc Fan out prefetched block decompression on a bounded executor (#15409)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15409

X-link: https://github.com/facebook/rocksdb/pull/15409

**WHAT**

- Add an optional fixed-size worker pool to the scan I/O dispatcher.
- Poll asynchronous reads and materialize their blocks, including decompression, off the foreground iterator thread.
- Fan out blocks from each coalesced read across bounded workers and join them with an atomic counter.
- Publish status and blocks through an acquire/release completion state, parking only when the sole `ReadSet` consumer arrives early.
- Preserve caller-thread processing when the configured worker count is zero

This change is purely additive; if a decompression thread pool is not specified via read options, the behavior is the same as previously with decompression happening on the calling thread.

**WHY**

Decompression can often become the bottleneck during async-io iteration. async io with rolling prefetching is meant to pipeline the application level iteration with io happening underneath. however, if the application level iteration (which involves decompressing fetched blocks) is far slower than the IO, the pipeline stalls, and throughput gets worse.

to this end, we can fork decompression tasks onto an executor, which can speed up the application layer part of the pipeline. for full table scans, this can see ~2x throughput improvements

Reviewed By: xingbowang

Differential Revision: D125249244

fbshipit-source-id: 99b80d7fb4f38ad6297d455ef0340f203d05aeb6
2026-11-03 15:59:08 -08:00
Steph Pontikes d0e6a4ca27 Add a dedicated READ Env thread pool (#15576)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15576

Add `Env::Priority::READ` as a schedulable pool and keep `USER` as a metrics-only priority category. Propagate the new pool through POSIX, Windows, thread-status, and Java/JNI APIs.

Reviewed By: xingbowang

Differential Revision: D129424000

fbshipit-source-id: 9dcf7c25ac6be6ec8931072dba2ad0bab8146300
2026-11-03 15:59:08 -08:00
Jay Huh 3993edbf34 Keep a secondary's view when catch-up switches to a MANIFEST that lists deleted files (#15571)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15571

## Problem

`TryCatchUpWithPrimary()` on a secondary (and follower catch-up) can install a column family with no files and still return OK. It happens when catch-up switches to a MANIFEST the primary just rolled, and the primary deletes a file that MANIFEST lists before catch-up checks it, recording the deletion in a newer MANIFEST. The secondary then serves the column family as empty until a later catch-up installs a new state. NeoSQL reproduced it deterministically with `branch_gc_catch_up_test` in D129307821 (T296537643).

## Cause

On a MANIFEST switch, `ManifestTailer::Initialize()` (default column family) and `ManifestTailer::OnColumnFamilyAdd()` (other column families) reset each builder to the column family's retained empty initial `Version`, so that the new MANIFEST's snapshot can add every live file again. The builder treats that empty base as a valid point in time, and the log number, next file number and last sequence are already known from the previous MANIFEST. When the snapshot lists a file that is gone, applying it is a valid-to-invalid edge, so `VersionEditHandlerPointInTime::MaybeCreateVersionBeforeApplyEdit()` builds a `Version` from the state before the snapshot, which is empty, and `CheckIterationResult()` installs it.

## Fix

`ManifestTailer` tracks the column families whose builder was reset for a new MANIFEST and that have not yet applied the record listing their files. `VersionEditHandlerPointInTime` now asks `BuilderHoldsPointInTime()` before creating a `Version`, and `ManifestTailer` answers false for those column families, so no `Version` is built from the empty base, neither on a negative edge nor on a forced create. A column family whose snapshot is incomplete keeps its previously installed `Version`. It gets a new one when a later record in the same MANIFEST completes the state, or from the next MANIFEST switch.

`VersionSet::WriteCurrentStateToManifest()` writes each column family's files in one record that also sets the column family's log number, and the records before it (DB id, comparator) set none. So the first record for the column family that sets a log number, other than a column family add, ends the wait.

A column family can also be left waiting when catch-up stops partway through a MANIFEST, after the record adding the column family and before the one listing its files. If the primary then drops it, no later MANIFEST lists it, and while it waits it holds back every atomic group. So a column family still waiting when catch-up switches to a new MANIFEST, and that the new snapshot does not add again, stops waiting at the first record after that snapshot. That is the first record setting the next file number: `LogAndApply()` sets it on every edit, and `WriteCurrentStateToManifest()` never does.

Because no `Version` is installed, `GetInstalledVersionLogNumber()` does not advance, and the secondary keeps memtables against the previous view, consistent with it.

Other paths are unchanged: the hook returns true by default, and only catch-up resets fill the set. Opening a secondary on such a MANIFEST is a separate case and also unchanged: initial recovery builds no `Version` from the snapshot, so the column family keeps its empty initial `Version` (see D127928276).

Reviewed By: xingbowang

Differential Revision: D129395958

fbshipit-source-id: 765ec6c60f842618fb2e3a691dcef61bbed02b51
2026-11-03 14:48:35 -08:00
Richard Barnes a303d02bb9 Use folly::coro::coroutine_handle in AsyncFileReader (#15580)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15580

Why: `AsyncFileReader::ReadAwaiter` names `folly::coro::impl::coroutine_handle`. `folly::coro::impl` is an internal alias that let folly switch between `std` and `std::experimental` coroutines. folly now requires C++20 and is dropping the alias, which would break RocksDB builds with `USE_COROUTINES`.

Change: use `folly::coro::coroutine_handle`, the public name folly exports for the same type. This works with both current and older folly releases, so no folly version check is needed. Behavior is unchanged.

Scope: only affects builds with `USE_COROUTINES=1` (folly coroutine support).

___

landed-with-radar-review

Differential Revision: D129467132

fbshipit-source-id: 81e87588a61ce7e73426336a2e91ec80f7ad1ba6
2026-11-03 14:44:18 -08:00
cm-rockee e15742470e Use ImmediateExit for db_stress MANIFEST verification failures (#15575)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15575

**Why:** when db_stress's MANIFEST-avoidance check fails on reopen, the crash test can report a misleading `Assertion 'property_info != nullptr' failed` in `DBImpl::DumpStats()` instead of the real failure. T287969622 collected four such hits (Sep 9 to Nov 1). Each started with a `FATAL: MANIFEST file grew by N bytes ...` or `FATAL: MANIFEST file was recreated ...` line from `StressTest::VerifyManifestNotRewritten()`. The task title, and the signature the automation groups by, came from the secondary assert. When the race does not happen, the same failure is filed under its own title instead (for example T288683405).

**Previous behavior:** `StressTest::Reopen()` calls `VerifyManifestNotRewritten()` right after `Open()`, so the DB is open with live background threads, including the stats-dump `Timer`. The three STRICT-mode failure paths in that function (MANIFEST recreated, MANIFEST grew, CURRENT updated) called `exit(1)`. `exit()` runs static destructors. If the stats timer fires during that teardown, `DBImpl::DumpStats()` looks up a DB property in an already-destroyed static map and asserts, so the process aborts with signal 6. The Sep 9 analysis on T287969622 identified this race.

**New behavior:** those three paths call `port::ImmediateExit(1)`, which wraps `_exit()`. The process prints the same FATAL message on stderr and exits with code 1 without running static destructors, so the stats timer can no longer turn it into a `DumpStats` assert. The crash test now reports the real failure.

**Implementation:** replace `exit(1)` with `port::ImmediateExit(1)` at the three STRICT-mode failure sites in `VerifyManifestNotRewritten()`. D108298839 added `port::ImmediateExit()` for this kind of failure: an exit after DB open with background threads running. Other post-open db_stress exits already use it (`FinishInitDb()`, `TrackExpectedState()` and the open-error paths in `Open()`).

**Safety and limitations:** db_stress only; no engine, API or format change. The remaining `exit(1)` calls in `db_stress_test_base.cc` are option-validation paths that run before the DB is opened, so they are left alone. `_exit()` skips flushing stdio buffers, so any buffered stdout from that run can be lost. The FATAL text goes to unbuffered stderr, as with the existing `ImmediateExit` sites. This does not fix the MANIFEST-avoidance failures themselves. T287969622 tracks those: one is fixed by D127204400, one (leftover `async_wal_precreate` WAL) still looks open, and the "MANIFEST recreated" variant is unexplained.

Requested by: Peter Dillinger (peterd)

Reviewed By: pdillinger

Differential Revision: D129421184

fbshipit-source-id: b3ab9f60200fd785b3ecfbf9bac35093ff7f4212
2026-11-03 11:37:19 -08:00
Peter Dillinger 2a2e19f51b Remove Codex review support from GitHub CI (#15570)
Summary:
Before this change, public GitHub CI supported both Claude and Codex AI code reviews through shared reusable workflows. Codex auto-review on PRs was already disabled (https://github.com/meta/rocksdb/issues/14933), but manual Codex reviews were still available via `/codex-review` and `/codex-query` PR comments.

We don't expect to need Codex as an additional review layer, largely because internal reviews are more convenient, and Claude reviews are sufficient for public CI. This change removes the Codex wrapper workflows, the Codex comment parser script, and all Codex-specific steps from the shared review workflows. The shared workflows otherwise keep their provider-parameterized structure.

If Codex review support is ever resurrected, it will need a security audit.

Repo admins can delete the OPENAI_API_KEY secret after this lands.

Pull Request resolved: https://github.com/meta/rocksdb/pull/15570

Test Plan: These workflows run from the base/default branch, so this PR's own CI does not exercise them; the first Claude review after merge will.

Reviewed By: archang19

Differential Revision: D129398405

Pulled By: pdillinger

fbshipit-source-id: 0628d20a61f6e9c5466d59443e422b71f00cfaf2
2026-11-03 10:01:53 -08:00
Omkar Gawde 6bc72c508b Fix the empty-level assert and wrong level labels in compaction resume (#15568)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15568

Why the change is required:
When a resumable remote compaction (`OpenAndCompactOptions::allow_resumption`) resumes, `MaybeResumeSubcompactionProgressOnInputIterator()` restores the saved output files of the output level and, with per-key placement, of the proximal output level. It reads their table properties with `CompactionJob::ReadOutputFilesTableProperties()`. With per-key placement, a debug build aborted on resume when one of the two levels had no output files, and a failure on a proximal level file was logged as an output level failure.

Fix: allow a level with no output files. A level can be empty when the progress is saved, for example when every key so far goes to the last level. `ReadOutputFilesTableProperties()` then failed `assert(!output_files.empty())`. Release builds compile the assert out, and the rest of the restore path already handles an empty list: there are no properties to read, no files or output records to restore, and no file numbers to skip. The assert is removed, with a comment that the list can be empty.

Fix: label the level correctly. `level_type` was a function-level `static`, so the first call in the process fixed the label for every later call. The only caller also did not pass `is_proximal_level`, so it always took its default of `false`. The caller's own log printed "output levelfiles", with a missing space. `level_type` is now a normal local variable, the caller passes `is_proximal_level`, and the parameter default is removed so that a later caller cannot omit it.

Safety and limitations:
Release behavior does not change, except for the log and status text. The empty-level case needs per-key placement (the compaction outputs to the last level and `preclude_last_level_data_seconds` is set) and saved progress with files at only one of the two levels. Progress is not saved while per-key placement writes blob files. A related case, where the other level still has an open file when the progress is saved, is fixed separately in D128238210.

Reviewed By: jaykorean

Differential Revision: D128228840

fbshipit-source-id: 59981a38e2d8a0964bfa659cc3f8bfa1a7c3954a
2026-11-03 09:41:27 -08:00
Chaitanya Datye 75e965d2a6 Validate table factory options when creating a column family (#15514)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15514

This makes `CreateColumnFamily()` and `CreateColumnFamilies()` run the same table factory option validation that `DB::Open()` runs, and makes the external table adapter forward that validation to the factory it wraps.

`DB::Open()` validates each column family twice: `ColumnFamilyData::ValidateOptions()` checks the plain column family options, and `ValidateOptions()` in `options_helper` walks the configurable objects, which is where `TableFactory::ValidateOptions()` runs. `CreateColumnFamiliesImpl()` only ran the first. A column family whose options the table factory rejects, for example a block-based table with a hash index and no prefix extractor, was therefore created and persisted, and the next `DB::Open()` failed on it. Validating at creation turns that into an `InvalidArgument` from `CreateColumnFamily()`.

The external table adapter returned by `NewExternalTableFactory()` did not override `ValidateOptions()`, and `Configurable::ValidateOptions()` only checks registered options, not `Inner()`, so an external table factory had no way to reject a column family it cannot serve. This matters for ingestion-only factories whose builders fail every write: in a column family without `disallow_memtable_writes`, one stray `Put` makes the next flush fail with a background error that stops writes to the whole DB. Such a factory can now override `ValidateOptions()` to require `disallow_memtable_writes`, and with the first change the check covers column families created at runtime as well as those opened with the DB.

This is a behavior change for callers that create column families with options the table factory rejects. Those calls used to succeed and leave a DB that failed to reopen; they now fail up front. A release note is included.

Changes:
1. `CreateColumnFamiliesImpl()` calls `ValidateOptions(db_options, cf_options)` after `ColumnFamilyData::ValidateOptions()`.
2. `ExternalTableFactoryAdapter::ValidateOptions()` validates its own options and then calls the wrapped factory's `ValidateOptions()`.
3. `ColumnFamilyTest.CreateRejectsOptionsThatOpenRejects` checks that a block-based hash index without a prefix extractor is rejected at creation and that the DB still reopens.
4. RocksJava's `OptionsUtilTest` created a column family whose block-based options `DB::Open()` rejects (`cache_index_and_filter_blocks` and `pin_l0_filter_and_index_blocks_in_cache` with `no_block_cache`, and `block_align` with compression and a 35 KiB block size); it only round-trips the options file, so it never noticed. It now uses a block cache, a 32 KiB block size and no compression for that column family, keeping every other non-default table option it verifies.
5. `ExternalTableTest.WrappedFactoryValidatesColumnFamilyOptions` checks that an external factory's `ValidateOptions()` is reached directly, through `CreateColumnFamily()` and through `DB::Open()`.

Reviewed By: xingbowang

Differential Revision: D128151632

fbshipit-source-id: 348e7820aa3995ddfa798b02bb38dddb8528e76c
2026-11-02 23:11:33 -08:00
Peter Dillinger e189b2724b Disable test flaky in GitHub Actions (#15567)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15567

EnvTest.GenerateRawUniqueIdTrackRandomDeviceOnly, to revisit in a few
months.

Differential Revision: D129271441

fbshipit-source-id: 3b9d7ec54f186bd8433249387f6ede81d83810c9
2026-11-02 21:20:49 -08:00
cm-rockee cba23b972b Fix remote compaction pool test flakes from 10 s wait timeouts (#15566)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15566

**Why:** The tests added with the separate remote compaction pool (D124139026) failed in the RocksDB contrun `check` job twice on Oct 30 (T265642089). The same code passed when the job retried at 14:07.

The failed tests:
- `DedicatedCompactionSubmissionExceptionTest.DrainsAcceptedSubcompactionsBeforeReturning/3,4,5`
- `RemoteCompactionPoolGateTest.RuntimeSwitchPreservesScheduledJobs/0,1`
- `DedicatedCompactionFallbackTest.CompactFilesFromLowWorker/0,2`

**Cause:** These tests wait for background progress, such as a remote compaction reaching the service's `Wait()`, a local subcompaction starting, or a manual compaction returning. Each wait is on a condition variable that the background work signals through sync point callbacks, the test compaction service or a listener, and it gave up after 10 s, failing the test. In the failing jobs each test took 15-125 s instead of about 1 s. The job runs 253 test processes at once with `TEST_TMPDIR` on `/tmp`, because `/dev/shm` is only about 100 GB on these hosts, so every flush and remote compaction output fsyncs a busy shared disk.

**Fix:**
- All 25 of these waits, including three `WaitForCompactOptions::timeout` values, now use a 10 minute `kProgressTimeout` instead of 10 s. A wait still returns as soon as its event happens, so a passing run takes as long as before; the timeout only makes a hang fail the test instead of blocking it. The one intentionally short timeout, a 10 ms `WaitForCompact()` that is expected to time out while remote compactions are held, is unchanged.
- `DedicatedCompactionPoolTest`, the fixture of all these tests, sets `SpecialEnv::skip_fsync_`, as `column_family_test` does. These tests check scheduling, routing and draining, not durability, and skipping fsync keeps them fast on a busy disk.

The waits are not converted to sync point dependencies: they already wait for events, and several wait for counts (such as N remote `Wait()` calls in progress), which a sync point dependency cannot express.

Test only.

Requested by: Peter Dillinger (peterd)

Reviewed By: pdillinger

Differential Revision: D128766650

fbshipit-source-id: 2f065678fe072cb6d9b64d47bcdeb76e6bf978ff
2026-11-02 20:31:45 -08:00
cm-rockee 47570c7418 Fix FlushLargestAcrossDBsPicksAtomicFlushDB flake on loaded hosts (#15565)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15565

**Why:** `DBWriteBufferManagerTest.FlushLargestAcrossDBsPicksAtomicFlushDB/0` failed in the RocksDB contrun `clang_check` job on Oct 25 (T265642089). It expected that only `atomic_db` gets flushed, but `db_` was flushed too.

**Cause:** with `kFlushLargestAcrossDBs`, the coordinator gives the DB it picks a 20 ms lease (`kFlushWorkCycleMicros`), starting when the handoff worker runs. If the lease expires while the WBM still needs to flush, the coordinator moves on to the next largest DB. That is intentional: it is how it bypasses stalled candidates. On a heavily loaded host, the handoff worker can take longer than 20 ms to get `atomic_db` to switch its memtables, so the coordinator also flushes `db_`.

I reproduced this with 150 concurrent copies of the test next to 200 CPU-burning processes on a 160-core host, plus logging in the WBM:
- the coordinator submitted `atomic_db` (626 KB flushable);
- the lease expired 20 ms later, with mutable memory still 989 KB;
- it then submitted `db_` (362 KB), and `db_` flushed first;
- `atomic_db`'s handoff ran 8 ms after that, and its flush followed.

**Fix:** a new `WriteBufferManager::ExpireFlushHandoffLease:Expired` sync point. The test asserts that `db_` was not flushed only when no lease expired. `atomic_db` must still flush both of its column families in either case.

This keeps the test's check of the ranking for the normal case. If the lease is not supposed to start before the chosen DB has switched its memtables, the fix belongs in the WBM instead; this diff does not change WBM behavior.

Requested by: Peter Dillinger (peterd)

Reviewed By: pdillinger

Differential Revision: D128766672

fbshipit-source-id: 746ebf858e5e31657bfe3d4ea4176252919b9af9
2026-11-02 20:26:10 -08:00
Jay Huh b07a47dd5b Check every Status in the trace analyzer's statistics passes (#15564)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15564

`TraceAnalyzerTest.LibraryResultWithoutOutputFiles` aborts with "Failed to check Status" in ASSERT_STATUS_CHECKED builds. `MakeStatistics()`, `MakeStatisticQPS()` and `ReProcessing()` start with a default `Status s` that is assigned only when an output file is set, so a library run without output files destroys it unchecked. `Analyze()` also leaves the `EndProcessing()` status unchecked when an earlier step failed.

Each status is now declared where it is assigned, and `Analyze()` marks the `EndProcessing()` status checked when it returns the earlier error.

___

landed-with-radar-review

Differential Revision: D129241122

fbshipit-source-id: 7e8c1d0f5a7042f1eee7263d3d2af63bf669606c
2026-11-02 18:29:21 -08:00
cm-wang 49a8f7d762 Handle a failed blob direct write seal: roll back picked memtables and keep the unsealed blob file (#15482)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15482

X-link: https://github.com/facebook/rocksdb/pull/15482

With blob direct write, `FlushMemTableToOutputFile()` and `AtomicFlushMemTablesToOutputFiles()` pick the memtables and then seal the write-path blob files with `PrepareFlushAdditions()` before running the flush job. A seal can fail with ENOSPC or an I/O error while writing or syncing the footer. Two bugs follow.

**Stranded memtables.** The flush job is cancelled without running. `FlushJob::Cancel()` only releases the base version, so the picked memtables stay marked as flush in progress. No later flush picks them up (`rocksdb.mem-table-flush-pending` reads 0), and error recovery, which flushes before it clears the error, waits for them forever. The comment in that path already expects a later flush to retry and publish the same blob file additions.

**Dropped blob file.** When the seal itself fails, `PrepareFlushAdditions()` has already popped the file from its generation, and `SealDeferredFile()` stops tracking it. `WriteBlob()` does the same when sealing a full active file fails. The memtable still points into that file. Once the memtables are flushable again, a retried flush finds no file to add and installs a table file whose blob file is not in the Version or the MANIFEST. The file is also untracked, so the next full obsolete-file scan, or the next open, deletes it, and the values are lost. In debug builds, `Close()` asserts on the cached, untracked file.

Fix:
- Roll back the picked memtables of a flush job that did not run with `RollbackMemtableFlush()`, in both the single column family and the atomic flush paths. Such a job never got an output file number, so this is the plain rollback.
- Keep a blob file whose seal failed tracked and in its generation, with the seal status. The writer cannot be finalized again, so every later flush of that generation fails with that status instead of publishing table files without the file. A failed active file joins the next generation, and its partition opens a new file for later writes.

Reviewed By: xingbowang

Differential Revision: D126837498

fbshipit-source-id: f80bcefbd6a4435f6ca7451c2a14071c537da9af
2026-11-02 13:58:31 -08:00
cm-wang 4500871744 Keep a sealed blob file when the space limit is reached, and report it outside the blob manager mutex (#15483)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15483

X-link: https://github.com/facebook/rocksdb/pull/15483

With blob direct write and an `SstFileManager` space limit, sealing a write-path blob file reports it through `BlobFileCompletionCallback::OnBlobFileCompleted()`. Once the limit is reached, that returns `Status::SpaceLimit` and sets the background error under the DB mutex. Two bugs follow.

**Data loss.** `FinalizeBlobFile()` returned that status as a sealing failure, although the footer was already written and the file was complete. `PrepareFlushAdditions()` (flush) and `WriteBlob()` (active file full) then dropped the sealed file: it was no longer tracked by the partition manager and never reached the MANIFEST, so the obsolete-file purge deleted it while the memtable still held blob indexes pointing into it. Reads of those keys returned `IO error: No such file or directory`, and the values were gone for good, including after error recovery flushed the memtable.

**Deadlock.** `OnBlobFileCompleted()` took the DB mutex while its caller held the partition manager's `mutex_`. `SwitchMemtable()` holds the DB mutex and calls `RotateCurrentGeneration()`, which takes `mutex_`. A memtable switch racing with the space-limit report deadlocked both threads, and everything else that needs the DB mutex (writes, `DumpStats()`) hung behind them.

Fix: blob direct write handles the space limit the way an SST flush does. The blob bytes are already on disk when a file is sealed, so failing the seal saves no space.
- A seal that reaches the limit still succeeds. The partition manager completes the file through a new `OnKeptBlobFileCompleted()`, which registers it with the `SstFileManager` at the size its `BlobLogWriter` wrote (so the registration cannot fail), reports its creation to listeners as successful, and only returns whether the limit is reached. It never takes the DB mutex.
- A write that rolls over a full file succeeds, then sets `SpaceLimit` as a `kFlush` background error after `mutex_` is released. A flush that seals a file commits it normally, and the flush's existing post-commit `SstFileManager` check sets the same error; setting it before the commit would make the flush roll back. Either way writes stop, reads keep working, and `SstFileManager` recovery resumes the DB once space is freed.
- So the space limit is soft for blob direct write. A direct-write blob file is counted only once it is sealed, and after the error the memtables waiting for a flush are not flushed, so usage can exceed the limit by up to about `blob_file_size` × `blob_direct_write_partitions` for each memtable not flushed yet (up to `max_write_buffer_number`). This is documented on `enable_blob_direct_write`. Blob files written by `BlobFileBuilder` (flush and compaction) still fail their job and are deleted.
- `MaybeTransformBatchForBlobDirectWrite()` keeps a failed batch's partial rollback records, as `TransformBatch()` promises. Blobs that a failed batch already wrote are counted as garbage, so their sealed file can still be dropped.

Reviewed By: archang19, xingbowang

Differential Revision: D126837497

fbshipit-source-id: c06241899f108fbbf5ee0125d191214bbfee8e0f
2026-11-02 13:58:31 -08:00
Jay Huh b4d0865a93 Make the trace analyzer a library API (#15561)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15561

Why the change is required:
RocksDB's query trace analyzer (`tools/trace_analyzer_tool.cc`, described in the "RocksDB Trace, Replay, Analyzer, and Workload Generation" wiki) can only be run as the `trace_analyzer` command line tool. Code inside RocksDB, such as experimental live tuning, cannot use it: it is built only into the tools and test libraries, it reads its configuration from global gflags, it writes its results to files, and on bad input it prints and calls `exit(1)`. Live tuning therefore carried its own small trace summarizer that computes much less (no QPS, no size statistics, no per-type key statistics, no top keys).

Previous behavior:
`TraceAnalyzer` lived in `tools/trace_analyzer_tool.{h,cc}` (ANALYZER_LIB_SOURCES), under `#ifdef GFLAGS`, configured by 29 `FLAGS_*` globals. Results went to stdout and to files under `--output_dir`; with no `--output_dir`, the files were opened under "/". `ta_[kPutEntity]` was never initialized, so whether PutEntity statistics were collected was undefined.

New behavior:
- `TraceAnalyzer` is in the RocksDB library (`trace_replay/trace_analyzer.{h,cc}`, in LIB_SOURCES), with no gflags. It takes a `TraceAnalyzerOptions` whose fields are the tool's flags, and either a trace path or an open `TraceReader`.
- `Analyze()` runs the whole analysis; `GetResult()` returns the statistics in memory (per operation type and column family: count, unique keys, key and value size sums, peak and average QPS, access count distribution, top keys; plus the trace's totals) without draining anything; `PrintStatistics()` prints them as the tool does.
- With an empty `output_dir`, no output files are written and the statistics are only kept in memory. The overall QPS peak and average are computed even without the QPS file.
- Errors are returned as `Status` instead of `exit(1)`: a bad `print_correlation`, a `value_interval` of 0 or less (which used to divide by zero), a file that cannot be created, a bad key in a whole key space file.
- New `analyze_put_entity` option and `--analyze_put_entity` flag (off by default), which also fixes the uninitialized PutEntity setting.
- `tools/trace_analyzer_tool.cc` is now a thin gflags front end that fills `TraceAnalyzerOptions` and runs `TraceAnalyzer`; its flags and output are unchanged.

Implementation:
- `tools/trace_analyzer_tool.{h,cc}` are moved (with history) to `trace_replay/trace_analyzer.{h,cc}`; `FLAGS_x` became `options_.x`; the `LDBCommand` hex helpers (tools-only) are replaced by local ones with the same output; the option-name maps are file-local and const.
- File writes are skipped when their file is not open, so statistics that were computed only on the way to a file (e.g. prefix top-k) are still computed without files.
- `EndProcessing()` closes the output files (including the QPS files) and no longer prints; the tool prints before it, unless `--no_print`.

Safety, compatibility, and limitations:
- The `trace_analyzer` tool's flags, defaults and output files are unchanged; `trace_analyzer_test`, which drives the tool through argv, passes unchanged.
- `TraceAnalyzer` is an internal API (not under include/), for code inside RocksDB.
- The analyzer keeps every accessed key in memory per type and column family; callers that analyze long traces should sample (`sample_ratio`, or the tracer's sampling).
- Diagnostic `fprintf`s on errors that also return a Status are kept as they were.

Reviewed By: xingbowang

Differential Revision: D129139885

fbshipit-source-id: a3eec6ce76b8199352d7bf52c3f109cf2f37d02b
2026-11-02 12:30:38 -08:00
Omkar Gawde 5cabe9439d Fix uninitialized and out-of-bounds reads from a damaged remote compaction result (#15562)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15562

Why the change is required:
With a damaged or hand-made remote compaction result, the primary could use a status built partly from uninitialized bytes as the compaction outcome, install an output SST in the wrong level under per-key placement, or read out of bounds in a release build when it logs the remote status. Results that RocksDB writes are not affected.

Previous behavior:
`CompactionServiceResult::Read()` parses with `OptionTypeInfo`, which sets only the fields present in the string.
- `StatusSerializationAdapter` did not initialize `code`, `subcode`, or `severity`. A missing field gave the status an uninitialized byte, and on the success path the primary used that status.
- `CompactionServiceOutputFile` did not initialize `marked_for_compaction` or `is_proximal_level_output`, and `ParseVector` default-initializes each element. A missing flag was indeterminate when the primary checked and installed the file.
- Parsed status values were not range-checked. With a non-OK code, a subcode at or above `kMaxSubCode` makes `Status::ToString()` read past its message table in release builds. Debug builds assert.
- A failed status parse, such as bad hex in the message, still replaced the caller's `status` with a half-parsed one. The primary does not use the result after a failed `Read()`.

New behavior:
A missing status field takes its `Status::OK()` value, and a missing output-file flag reads as false. A code, subcode, or severity outside its enum makes `Read()` return `InvalidArgument`, which the primary handles like any other parse error. A failed status parse leaves the caller's `status` unchanged. Complete results with in-range values parse as before.

Implementation:
The five members get initializers. `StatusSerializationAdapter::IsValid()` checks the enum ranges, and the status parse lambda returns on any error before it assigns the status.

Safety, compatibility, and limitations:
The serialized format does not change. A result from a newer worker with a status value that this primary does not know now fails to parse: after a `kSuccess` wait the primary falls back to local compaction, and after a `kFailure` wait it reports `Incomplete`. Before, it built an invalid `Status`. `Read()` still parses in place, so when a later field fails, the fields parsed before it stay written.

Reviewed By: jaykorean

Differential Revision: D128231672

fbshipit-source-id: 8335decb90ae452ade71ce2cd011bfed200d3945
2026-11-02 12:28:04 -08:00
Xingbo Wang 24282f7418 Support merge-before-filter compaction (#15517)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15517

## Why

Some useful compaction filters only make sense on merged results. For example, a filter that removes counters below a threshold must see the counter's total, not each `+N` operand. With the current order, the compaction filter runs before the merge operator, so it sees (and can remove) individual operands and never sees the merge result.

Important limit: compaction can only merge what is in its own inputs. If a key's base value or older operands are in files outside the compaction, the filter sees a partial merge result (or single operands), not the logical value of the key.

## Behavior

- **Default (unchanged):** `CompactionFilter::MergeOrder::kFilterBeforeMerge`. Operands are filtered one by one before merging.
- **New opt-in:** a filter or factory returns `kMergeBeforeFilter` from the new `GetMergeOrder()`. During compaction the merge operator runs first, then the filter sees:
  - the full merge result as `kValue` / `kWideColumnEntity`, if the inputs include the base (Put or tombstone), or the compaction is bottommost with no older entries;
  - otherwise the operands as `kMergeOperand`: one partial result from `PartialMergeMulti`, or each operand by itself if partial merge declines.
- A filter result is not merged again in the same compaction. Filters that only implement FilterV2/V3 also work.
- A merge result is offered only if no snapshot can see any of its operands. Plain values are filtered as before, regardless of snapshots.
- `kPurge` on a value is treated as `kRemove` (ordinary Delete), because SingleDelete does not mix with Merge history. `kPurge` and `kChangeWideColumnEntity` on an operand fail the compaction with NotSupported.
- `kRemoveAndSkipUntil` on any operand of an uncombined batch drops the whole batch, the same result as the default order.
- `COMPACTION_KEY_DROP_USER` keeps the same meaning in both orders (skipped ranges are not counted).
- The new order applies only to compaction. Flush, recovery, MemPurge, and repair keep the default order.

### Example

Counter merge operator (add) with a filter that removes counters below 10. The compaction input for one key is `Merge(+3)`, `Merge(+5)`, `Put(4)`. Before: the filter sees the raw operands `+3` and `+5`, removes both, and the output is `Put(4)`; it never sees the merged value. After: the merge runs first (4 + 5 + 3 = 12), the filter sees `kValue` "12", and keeps it. If the base is not in the compaction, partial merge gives `+8` and the filter sees it as `kMergeOperand`.

## Implementation

- `MergeHelper` carries the merge order and records each merge outcome (full, partial, uncombined) and whether it can be filtered under snapshots.
- `CompactionIterator` reads the order from `MergeHelper` and filters after the merge. A full result changed to Delete goes through the normal `ProcessDeletion()` path. Uncombined operand batches are filtered as a whole before any of them is output, so a skip decision can drop the full batch.
- Order checks (`GetCompactionFilterMergeOrder()`) run outside the DB mutex. A factory's non-null filters must declare the same order; otherwise the compaction fails with InvalidArgument. A factory that returns a null filter means no filtering, which is valid with either order.
- **Remote compaction:** `CompactionService::SupportsMergeBeforeFilter()` (default false) lets a service receive new-order jobs; otherwise they run locally. The request carries `merge_before_filter`, and the worker acknowledges the order it actually used. A missing or wrong acknowledgment (for example, from an older worker) falls back to local compaction before any output is imported. Real compaction errors stay errors. A worker with no filter accepts either order.
- **Early rejection:** DB open and CF creation reject a new-order declaration with user-defined timestamps, WritePrepared/WriteUnprepared transactions, stacked BlobDB, or a factory that selects non-compaction table file creation. This avoids a background error that would stop writes at the first compaction.
- `ShouldFilterTableFileCreation()` checks use an exhaustive switch over `TableFileCreationReason`, so a new reason must be classified.
- **Bug fix included:** `MemPurge()` returned with the DB mutex unlocked when the flush filter failed a check (for example, `IgnoreSnapshots() == false`). The new order check adds another early return on the same path, so the re-lock is fixed here, with a `bug_fixes` release note.
- **Stress test:** with `merge_before_compaction_filter`, the db_stress filter now makes real decisions that never change what a reader sees: `kChangeValue` with the same bytes for about 1 in N records (`merge_before_filter_change_one_in`); with `merge_before_filter_remove_deleted`, `kRemove` for keys deleted in the expected state, and `kRemoveAndSkipUntil` over just that key in full compactions (where every version of the key is an input). It also aborts if a value it sees is not a well-formed stress value for its key, or if the new-order factory is asked to filter a non-compaction file. It prints per-type decision counts at the end of a run. The crash test picks these modes randomly and uses merges in about 3/4 of new-order runs; the remove/skip mode uses the same no-snapshot settings as the default-order filter.
- `Repairer` only marks `BuildTable()`'s `IOStatus` as checked, because `BuildTable()` can now return early before reading it. Repair behavior does not change.

## Safety and limitations

- The default order and existing filters behave as before. There is no SST format change, no new CF option, and no C/Java binding.
- Not supported in the new order: user-defined timestamps, WritePrepared/WriteUnprepared transactions (SnapshotChecker), and stacked BlobDB. Integrated BlobDB is supported.
- Do not use SingleDelete on keys handled by a new-order filter, even after switching back to the default order.
- How operands are grouped into partial results can differ between compactions, and SST files keep no record of earlier groupings. Filter decisions must be correct for any grouping.
- **Stress test limits:** the stress merge operators keep only the newest operand, and every stress filter decision is chosen so that reads do not change. So both orders give the same visible result, and the oracle checks robustness of the new paths (wrong output key or sequence, lost or returning data, buffer and pinning errors, crash recovery, remote fallback), not whether the filter saw the right input. A same-bytes rewrite also cannot detect a change that RocksDB ignores. Skips only run in full compactions, which are rare in leveled runs. The decision counters print only when db_stress exits normally (not in blackbox kills).
- **Unit tests cover the rest:** which input the filter sees (full, partial, uncombined), the snapshot rule, changed values in the output (`MergeBeforeFilterFullReplacementAndFailure`, `MergeBeforeFilterPartialAndDecline`, remote full/partial tests), skip and removal results, kPurge handling, unsupported decisions, early rejection, remote routing and fallback, and BlobDB.
- Validation: the full suites of the touched test binaries pass, and local db_stress and crash-test runs with the new modes pass. See the Test Plan for details and the benchmark results.

Reviewed By: pdillinger, jaykorean

Differential Revision: D128313536

fbshipit-source-id: 996dd31a78636f7968005938365ce94d11cbe8b3
2026-11-02 11:05:34 -08:00
Peter Dillinger ea5db0c142 Fix format_version=8 common prefix extending past a user key (#15556)
Summary:
## Background / Motivation
With format_version=8 (not yet the default) and optimize_key_common_prefix, each data and index block stores one common prefix. The design and code intend that prefix to be a prefix of every user key in the block. Seek and SeekForPrev for bytewise and reverse bytewise comparators rely on that: they strip the prefix from the target and from the block's keys, then compare what remains (the "prefix-optimized seek"). That preserves ordering only if the stripped bytes are user key bytes.

Before this change, BlockBuilder limited the prefix to the user key of the first key and of each restart key. For other keys, it limited the prefix only to the internal key bytes shared with the previous key. ReverseBytewiseComparator orders a user key after longer user keys that extend it, so those shared bytes could reach past a user key into its internal key footer. For example, in a block with `ab\x01c` followed by a Put of `ab`, whose internal key begins `ab\x01` because the first footer byte is the value type, the stored prefix was `ab\x01`. Because internal keys are not bytewise comparable, the prefix-stripped comparisons then gave wrong results:
* Get, MultiGet and seeks could miss such a key. A missed deletion would expose an older value.
* Iteration could skip such a key or fail a debug assertion, because iterators seek internally after skipping many versions of a key and when changing direction.
* A compaction with subcompactions seeks to the start key of each subcompaction, and could skip versions of such a key there. The check of the number of input records (compaction_verify_record_count, on by default) then fails the compaction with Corruption; without that check, the skipped versions would likely be dropped from the compaction output.

Index blocks of internal keys with index_block_restart_interval > 1 could be affected the same way, theoretically including filter partition indexes. RocksDB 11.10 through 11.13 wrote such blocks.

Other comparators were not buggy:
* With BytewiseComparator, the prefix cannot extend past a user key, because a user key sorts before every longer user key that extends it.
* With other comparators (optimize_key_common_prefix=kEnabled), a prefix extending past a user key is apparently benign, at least for now, because their reads reconstruct full keys before comparing.

This bug was found by an agent in D126805261, but this solution is less deeply intrusive in the most performance-critical areas and generally improves performance with a bundled optimization.

## This Change
This change fixes the writer, makes the reader handle "bad" blocks that have already been written, and bundles an optimization of the prefix-optimized seek that offsets the cost of the reader fix.

### Write Path
BlockBuilder now also limits the prefix to the user key of each non-restart key, so new blocks store only a common user key prefix, with any comparator.

There is no format change. Blocks written with this change meet the invariant that all format_version=8 readers assume, so older versions read them correctly.

### Read Path
When a data or index block (including a filter partition index block) of a table opened with ReverseBytewiseComparator is loaded, the reader checks whether the stored prefix extends past a user key. If it does, seeks within that block use the existing full-key search (as for other comparators) instead of the prefix-optimized seek. BlockBasedTable::Open() enables the check; blocks inserted into the block cache while building a table (prepopulate_block_cache) come from the fixed writer and are not checked.

Checking only the block's last entry is enough. A user key shorter than the stored prefix is extended by every other user key in the block, so in reverse bytewise order its versions are the block's last entries. None of them can be a restart key, since the prefix was limited to each restart key's user key, so they all follow the last restart point. The check therefore costs:
* for a data block, decoding the entry headers of the last restart interval;
* for an index block of internal keys, reading the last entry with an iterator (a single entry with the default index_block_restart_interval of 1);
* for an index block of user keys, nothing: its prefix was computed from user key bytes.

The result is a new Block flag. Block's flags are now bit-fields so that sizeof(Block), allocated for every cached block, stays 96 bytes (one more bool would have made it 104).

Two other approaches to reading existing blocks were considered:
* Handling affected blocks within the prefix-stripped comparisons, as D126805261 did, adds work to every (reverse-)bytewise seek: 63 to 98 more instructions per block seek, and up to 12x slower seeks for reverse bytewise targets that sort outside a block's prefix.
* Rejecting affected blocks as corruption would keep the seek code unchanged. But reads that work today, such as lookups of other keys and most scans and compactions, would then fail. That includes the compactions by a fixed version that would rewrite the affected files without the problem (compaction by an unfixed version can write the same layout again).

### Bundled optimization: comparing without virtual calls
The reader fix needs a per-iterator decision of whether to use the prefix-optimized seek, which already requires knowing that the comparator is BytewiseComparator or ReverseBytewiseComparator. Each block iterator now records that decision, made once when it is initialized, as a PrefixOptimizedSeek value (none, bytewise, or reverse bytewise). Previously, every seek compared the comparator with BytewiseComparator() and ReverseBytewiseComparator() (out-of-line calls), and then compared keys through InternalKeyComparator and the virtual Comparator::Compare(), although the order was known to be bytewise or reverse bytewise.

Because the same value tells which of the two orders applies, the prefix-optimized seek now compares keys directly (BlockIter::CompareKeyForPrefixOptimizedSeek(): a byte comparison of the user key parts, negated for reverse bytewise, then the internal key footer, with the file's global sequence number applied as in CompareKey()). It still counts user_key_comparison_count.

This is bundled, rather than a separate change, so that the fix "pays as it goes": the reader fix alone added a small cost to seeks (and grew Block until the bit-field change), while this optimization reuses the state the fix introduces and more than offsets that cost in most measurements (below).

Pull Request resolved: https://github.com/meta/rocksdb/pull/15556

Test Plan:
## Unit Tests
New tests in db_etc3_test, all using ReverseBytewiseComparator and format_version=8. A test sync point in BlockBuilder restores the old prefix computation so that tests can write affected blocks. A sync point at detection lets tests confirm that affected blocks were detected.
* CommonKeyPrefixWithShorterKeyAfterLongerKey: the fixed writer with Put and Delete forms of the problematic key order; no block is detected as affected.
* CommonKeyPrefixReadsBlocksWithShorterKey: Get, MultiGet, Seek and SeekForPrev on a data block whose stored prefix extends past a user key, before and after a compaction with two subcompactions.
* CommonKeyPrefixIteratesBlocksWithShorterKey: forward and reverse scans, with and without a snapshot, that seek internally into such a block.
* CommonKeyPrefixIndexBlockWithShorterKey: an affected index block of internal keys.
* CommonKeyPrefixRandomizedReverse: random DBs whose user keys often extend each other, written with the old or new prefix computation and with varied block sizes, restart intervals, index types (including partitioned, with partitioned Bloom filters), separated KV storage, data block hash index, block_protection_bytes_per_key, cache_index_and_filter_blocks, prepopulate_block_cache (with the new writer), and snapshots, sometimes including an ingested file with a global sequence number, checked against a model before and after compaction. With the new writer, it also checks that no block is flagged for the full-key search.

I confirmed that the last four tests fail with the BlockBuilder change but without the reader changes, with missing or wrong results or debug assertion failures. The randomized test also fails without the filter partition index check (debug assertion in the comparison) and if blocks are checked while building a table (index blocks of user keys flagged). The direct comparisons are also covered by existing tests of format_version=8 tables and ingested files (e.g. table_test, block_based_table_reader_test, external_sst_file_test).

## Performance
Release builds from GCC 11 and clang 21, each with the default (-march=haswell) and with PORTABLE=0 (-march=native, here znver3 or newer); single thread pinned to one CPU; interleaved runs; median of paired time ratios vs. base (lower is better); user-space instructions per operation.

Block seeks (4KB blocks, 14-byte common prefix, 92 keys per data block, 128 entries per index block; ranges are over the two builds per compiler). "Re-initialized" means the block iterator is initialized before each seek, as for each block that a point lookup visits.

| Block seek | GCC: instructions, time | clang: instructions, time |
|---|---|---|
| Index seek | +49 to +50, 0.989 to 1.003 | -83, 0.974 |
| Index seek, re-initialized | +67 to +68, 0.997 to 1.009 | -38, 0.978 to 1.006 |
| Data seek | -241 to -259, 0.936 to 0.940 | -250, 0.913 to 0.926 |
| Data seek, re-initialized | -221 to -241, 0.946 to 0.956 | -206, 0.933 to 0.952 |
| Data SeekForPrev | -344 to -369, 0.925 to 0.941 | -290, 0.934 to 0.944 |
| Reverse bytewise index seek, re-initialized | +58 to +59, 1.003 to 1.009 | -45, 0.998 to 1.002 |
| Reverse bytewise data seek, re-initialized | -227 to -247, 0.944 to 0.969 | -216, 0.936 to 0.952 |

With GCC, the direct comparison is inlined into data block seeks but called out of line (three call sites) from IndexBlockIter::SeekImpl(), hence the small instruction increase there, with no measurable time change.

db_bench (10M entries, 16-byte keys, 100-byte values, fully cached), time:

| db_bench | GCC | clang |
|---|---|---|
| readrandom, bytewise | 0.975 to 1.013 | 1.001 |
| readrandom, reverse bytewise | 0.985 to 0.986 | 0.995 to 1.006 |
| seekrandom, bytewise | 1.007 to 1.018 | 0.980 to 0.983 |
| seekrandom, reverse bytewise | 1.005 to 1.023 | 0.971 to 0.973 |

Per operation, readrandom ran 47 to 81 fewer instructions with GCC and 261 to 275 fewer with clang; seekrandom ran 12 to 81 fewer with GCC and 188 to 315 fewer with clang.

With GCC, seekrandom (a new DB iterator per operation) was 0.3% to 3.4% slower than base across repeated runs (median about 1.8%), while running fewer instructions; with clang it was 2-3% faster. GCC seekrandom is front-end bound, with about 1300 L1 instruction cache misses per operation, and with GCC this change (and earlier versions of it, without the bundled optimization) adds 5-6% to those misses. Shifting code by 16 bytes to 3 KB left each build's miss count unchanged, and this change's hot functions are no larger in total than base's. So the GCC seekrandom difference is not from added work. It looks like an effect of GCC code generation and layout: a change with no behavior change, adding an unused field to BlockIter, moved base's misses by +3% (and its seekrandom time by -0.8%), though this change's effect is somewhat larger.

Parsing a 4KB data block on load (excluding I/O, checksum verification, decompression, and cache insertion), GCC default build: +8 instructions with bytewise (no check); with reverse bytewise, +415 instructions, time 1.194 (about +37 ns) at block_restart_interval=16 and +145 instructions, time 1.049 at block_restart_interval=4.

Reviewed By: xingbowang

Differential Revision: D129007823

Pulled By: pdillinger

fbshipit-source-id: d9ffa16ed31384dbd8fd5f883b5080b986f7539e
2026-11-02 10:55:17 -08:00
Omkar Gawde d375bbbbd0 Skip SetOptions when no option value changes (#15552)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15552

Why the change is required:
`DB::SetOptions()` does the full update on every call, also when the request changes no value. It installs a new Version and SuperVersion, writes a new OPTIONS file, and deletes obsolete ones. Callers that apply their full configuration on each refresh pay this cost every time. `SetDBOptions()` already skips a request that changes nothing.

New behavior:
`SetOptions()` returns OK and does nothing when each column family in the request is not dropped and the request parses, validates, and changes no value. The full update still runs for:
- A request that names an object option (`table_factory` or one of its fields, `prefix_extractor`, `compression_manager`). For example, `table_factory.filter_policy.bloom_before_level` changes the shared filter policy in place, and a `MutableCFOptions` comparison cannot see that.
- A request after an earlier update left the OPTIONS file or the seqno-to-time worker behind: an OPTIONS file write or a worker update failed, a multi-CF call failed after it changed some column families, or, with `track_options_file_number_in_manifest`, a column family create or drop batch stopped part way. While the OPTIONS file is behind, an unchanged `SetDBOptions()` request also writes it.

Implementation:
- `MutableCFOptionsUnchangedByMap()` parses the request over the current options and compares the result with `MutableCFOptions::operator==`, so doubles compare exactly. It returns false for a request that names an object option.
- `ColumnFamilyData::options_install_pending_` marks a column family that a failed multi-CF call changed but did not install.
- `DBImpl::options_file_stale_` is set by a failed `WriteOptionsFile()` and cleared by a successful one. `DBImpl::seqno_time_worker_stale_` does the same for `RegisterRecordSeqnoTimeWorker()`. A multi-CF call that fails after it changes some column families sets both. A call that fails before it changes anything sets neither, so a request with a bad option does not turn off the skip for later requests. Both flags are protected by `options_mutex_`.
- With `track_options_file_number_in_manifest`, column family create and drop do not call `WriteOptionsFile()`. Instead, the first create or the last drop of a batch commits an OPTIONS snapshot through the MANIFEST. So `CreateColumnFamiliesImpl()` sets `options_file_stale_` unless every create of the batch succeeds. `DropColumnFamilyWithOptionsFile()` sets it after a drop that commits no snapshot and clears it after a drop that does. The drop records the flag right after its MANIFEST commit, before a later seqno-to-time worker update can fail. A snapshot holds the options in effect, so it also clears a flag that an earlier failed OPTIONS file write set.

Safety and compatibility:
- Every failed `WriteOptionsFile()` sets the flag, also in `SetDBOptions()`, `CreateColumnFamily()`, and `DropColumnFamily()`. So the next `SetOptions()` or `SetDBOptions()` call writes the OPTIONS file again. Before this change, the next `SetOptions()` call did this because it always wrote the file.
- A tracked snapshot that lists a dropped or never-created column family was not a correctness problem, because `LoadLatestOptions()` filters a tracked snapshot by MANIFEST membership. Before this change, the next `SetOptions()` call replaced such a snapshot, and the new flags keep that result.
- A skipped request does not re-anchor periodic-compaction phasing or schedule flushes and compactions, and it cannot fail on an OPTIONS file write. For example, `EnableAutoCompaction()` on a column family with auto compaction already on now does nothing.
- `MergeTest.MergeWithCompactionAndFlush` sent an unchanged `SetOptions()` request to reach `LogAndApply()`, so it hung with this change. It now sets `report_bg_io_stats`, which changes a value.
- A release note is in `unreleased_history/behavior_changes/`.

Reviewed By: xingbowang

Differential Revision: D128887760

fbshipit-source-id: 2a095713e67f79600d9f5ef5588180f03c0e2f8e
2026-11-02 10:30:14 -08:00
Jay Huh 9bfe67777a Make JSONWriter escape strings and write non-finite numbers as null (#15559)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15559

`JSONWriter` (`logging/event_logger.h`) wrote keys and string values between quotes without escaping, and wrote floating-point values through `std::ostream`, which prints `nan` and `inf`. A string containing a quote, backslash, or control character, or a non-finite double, produced invalid JSON in `EVENT_LOG_v1` lines and in `VersionEdit::DebugJSON()` output (for example, a non-hex user key with binary bytes).

`JSONWriter` now escapes `"`, `\`, and control characters (using `\b`, `\f`, `\n`, `\r`, `\t`, or `\u00XX`) in keys and string values, and writes non-finite floating-point values as `null`. Output that was already valid JSON is unchanged.

This lets other code that builds JSON log lines use `JSONWriter` directly instead of carrying its own escaping helpers.

Reviewed By: xingbowang

Differential Revision: D129106883

fbshipit-source-id: 8097d91ae1a2bc88fbbc93fd7cbdc7f00f7aa504
2026-11-02 09:58:04 -08:00
zaidoon 8fd33c2fac Stress and benchmark all IndexFactory modes; add docs and release notes (#14968)
Summary:
**Why:** The `IndexFactory` series (https://github.com/meta/rocksdb/issues/14963, https://github.com/meta/rocksdb/issues/14965, https://github.com/meta/rocksdb/issues/14966) added five index modes and a per-read selector, but the stress and benchmark tools could reach only part of that surface. Its public API also shipped with no release note, and the `CompressionOptions::parallel_threads` comment still said a custom index forces single-threaded compression.

**Previous behavior:**

- `db_stress` had only `--use_udi_as_primary_index`, which selects `kStandardDefault` or `kCustomDefault`. `kCustomOnly`, `kStandardRequired`, and the `kBuiltin`/`kPreferCustom` read selectors were never stress tested. Reads chose the trie through the legacy `ReadOptions::table_index_factory` pointer, and the two read paths disagreed: after a reopen, the pointer was also set in primary mode.
- `db_bench --use_trie_index` could only build the trie as the secondary index, so the trie could not be benchmarked as the default read index or as the only index.

**New behavior:**

- `db_stress` and `db_bench` take `--index_mode` (0-4, defaulting to the library default `kStandardDefault`) and `--read_index` (-1 = auto, or `kDefault`/`kBuiltin`/`kPreferCustom`). Auto keeps today's behavior of reading through the trie when it is the secondary index. Both tools reject combinations that cannot work: modes 2-4 or a non-default selector without `--use_trie_index`, and `kBuiltin` with `kCustomOnly`. `--use_udi_as_primary_index` is removed from `db_stress`, and `--index_mode=2` replaces it.
- The crashtest draws `index_mode` once per DB together with `use_trie_index`, so every SST in a DB has matching table options, and draws `read_index` on every invocation. It disables only what cannot work in a mode:
  - `kCustomDefault`/`kCustomOnly`: partitioned index and filters (rejected by option validation); backup, because the restored DB rebuilds options from strings that do not carry the factory; and the secondary instance, which main already disables for primary mode.
  - `kCustomOnly`: `format_version` raised to 6, and `kBuiltin` reads dropped.
  - After all other sanitizers, custom modes and selectors are reset when something (for example user-defined timestamps) has turned the trie off.
- New `docs/components/index_factory/` component doc, and two release notes: one for the `IndexFactory` API, one for parallel compression with a custom index.

## Changes since the previous revision

This branch now contains only https://github.com/meta/rocksdb/issues/14968's own work on top of main, which includes https://github.com/meta/rocksdb/issues/14966. I re-checked each piece for value and redundancy:

- **Dropped `--index_mode=-1` ("auto")** from both tools. With no factory, the default `kStandardDefault` already builds only the standard index, so auto behaved identically to the default and needed a helper in each tool.
- **Kept remote compaction on in the custom modes.** The previous revision disabled it because it was "not exercised yet", but main already runs primary mode with remote compaction, and remote workers rebuild the trie factory from its registered name. The crashtest runs below cover it in `kCustomOnly` and `kCustomDefault`.
- **Dropped disabling write faults for `cf_consistency`** in the custom modes. Main runs that combination in primary mode today, and the targeted `cf_consistency` + `kCustomOnly` + write-fault run below passed.
- **Kept backup and secondary on for `kStandardRequired`.** It has a full standard index, so a restored or secondary DB can always read it.
- **Release notes cut from eight to two.** Dropped one that duplicated main's existing user-defined-timestamp note, and one that described `kPreferCustom`'s fallback as a behavior change. Before https://github.com/meta/rocksdb/issues/14963, `table_index_factory` was only consulted for SSTs that had a custom index, so SSTs without one already used the standard index.
- Replaced eleven per-case crashtest tests with four table-driven ones, and dropped an unrelated `(void)argc` edit in `db_bench_tool_test.cc`.

## Safety and compatibility

- Tool defaults are unchanged: without `--use_trie_index`, both tools build only the standard index. With it, `db_bench` and `db_stress` still read through the trie as before.
- Removing `--use_udi_as_primary_index` from `db_stress` breaks any external command line that passes it. The in-tree crashtest is updated.
- The header and docs changes are comments only.
- The component doc notes known gaps rather than fixing them: `sst_dump`, and `ldb` without the DB's OPTIONS file, cannot open `kCustomOnly` files, and there is no Java API.

Pull Request resolved: https://github.com/meta/rocksdb/pull/14968

Test Plan:
All on this branch rebased onto main at a7ae6cd0a (macOS ARM64, Apple Clang, debug):

- `tools/db_crashtest_test.py`: 76 tests pass. Each of the four new tests fails when its sanitizer rule is removed: the `format_version` bump, the `kBuiltin` drop for `kCustomOnly`, the reset when the trie is off, and keeping remote compaction on (verified by re-adding the old disable).
- Flag validation: every invalid combination listed above exits with status 1 and a clear message in both tools.
- `db_bench` smoke runs: fillseq, compact, readrandom, and seekrandom with `--use_trie_index=1` in all 19 valid mode and selector combinations, with every key found.
- Blackbox crashtests (`db_crashtest.py blackbox`, 300 s, killed every 30 s, `db_stress` built with gflags). Runs A, B, and D predate the last `read_index` fix, so they always used auto. Each run completed 10 kill/restart cycles, and its final crash-recovery verification passed:

| Run | Index mode | What it adds | Result |
|---|---|---|---|
| A | `kCustomOnly` | Remote compaction in 4 of 11 runs, `format_version` 6-8, 1-16 compression threads | Passed |
| A2 | `kCustomOnly` | Write faults (1 in 128) in all 11 runs. Remote compaction in 7 runs: 289 remote jobs, none fell back to local. `read_index` auto, `kDefault`, `kPreferCustom` | Passed |
| D | `kCustomOnly` | `--cf_consistency` | Passed |
| D2 | `kCustomOnly` | `--cf_consistency` with write faults in all 11 runs. Remote compaction in 6 runs: 1,710 remote jobs; 18 fell back to local after the remote job failed (`db_stress` falls back only for injected faults or when configured to, and fails the run on any other remote error) | Passed |
| B | `kStandardRequired` | Remote compaction and backup in every run, write faults in 4 runs | Passed |
| B2 | `kStandardRequired` | Secondary instance in every run, 2 DBs, all four `read_index` values including `kBuiltin` | Passed |
| E | `kCustomDefault` | Remote compaction in 9 runs: 1,472 remote jobs, none fell back to local. All four `read_index` values | Passed |

- `make format-auto` and `make check-sources` pass.

Not run locally: `db_bench_tool_test` (this Mac's gflags setup pulls in a conflicting gtest; CI runs it), full `make check`, Linux, Windows, sanitizers, and a release build.

## Series

Builds on https://github.com/meta/rocksdb/issues/14963, https://github.com/meta/rocksdb/issues/14965, and https://github.com/meta/rocksdb/issues/14966, all on main. This is the last PR in the series.

Reviewed By: jaykorean

Differential Revision: D128807954

Pulled By: xingbowang

fbshipit-source-id: b4faa1e94959f63ea56fd8d69f64b1763c8aacce
2026-11-02 09:37:26 -08:00
Yedidya Feldblum ee13e2a640 Use owning EventBase acquisition in coroutine benchmarks (#15527)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15527

Prefer owning EventBase acquisition in both coroutine benchmark families without breaking RocksDB's public build against pinned Folly 7ba92f0e29d24b453cee8ab27d117b1c7cc19589, which lacks `IOExecutor::getEventBaseKeepAlive()`. A private function template uses a dependent C++20 `requires` expression and `if constexpr`: use the owning getter when available, otherwise retain the previous raw selection plus token acquisition. Exceptions propagate without switching to the fallback.

Acquisition remains at each job's existing launch point; executors, parameters, futures, teardown, non-coroutine builds, API/ABI and stored formats are unchanged. The legacy branch preserves existing behavior rather than providing the newer API's selection-window safety. The Folly pin is not advanced here; a separate cleanup will remove this temporary fallback once the pin includes the owning API.

Validation: native owner builds and both 64-record coroutine benchmarks passed; five C++20 compile/selection/exception checks cover both API shapes. Formatting, default lint and 3/3 applicable sanity checks passed; 72 whole-file clang-tidy warnings match the exact unmodified baseline. Full Make and status-checked Make runs were attempted but are not green: Git-only checks fail in this Sapling checkout and unchanged I/O-dispatcher tests fail/abort outside the modified benchmark library. The standalone indexed source snapshot passed `make check-sources`. The actual pinned-Folly public build and other platforms remain unverified locally.

Reviewed By: r-barnes

Differential Revision: D128119434

fbshipit-source-id: d5b6bfbb0b782d334423b876717d20d0c829cc22
2026-11-01 17:05:25 -08:00
Omkar Gawde a87e2a0e43 Report OnSubcompactionCompleted when a resumed subcompaction fails to restore its progress (#15530)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15530

`CompactionJob::ProcessKeyValueCompaction()` calls `NotifyOnSubcompactionBegin()` before `MaybeResumeSubcompactionProgressOnInputIterator()`. When the resume failed, the function set `sub_compact->status` and returned without a call to `NotifyOnSubcompactionCompleted()`. The resume fails when a saved output file cannot be read, when the key to resume from is outside the input, when the input iterator fails at that key, or when the saved blob progress cannot be restored. A listener then got `OnSubcompactionBegin()` with no matching `OnSubcompactionCompleted()`, so it could not close the event or see the failure status.

Only `DB::OpenAndCompact()` with `OpenAndCompactOptions::allow_resumption` loads saved progress, so only the listeners of such a worker are affected. When there is no progress to resume, the function returns `NotFound`, and the subcompaction starts from the first key as before.

The early return now calls `NotifyOnSubcompactionCompleted()`, and the event carries the resume error in `SubcompactionJobInfo::status`. `NotifyOnSubcompactionCompleted()` keeps its checks: it sends nothing when `NotifyOnSubcompactionBegin()` sent no event, or when the DB is shutting down. The resume error and what follows it do not change: the worker's compaction fails with that status, and the `CompactionService` implementation decides what the primary does.

The diff also adds a release note under `unreleased_history/bug_fixes/`.

Reviewed By: jaykorean

Differential Revision: D128236290

fbshipit-source-id: 2b2a0e97b65556c1cd52f7c494636ae923cd639e
2026-11-01 12:38:57 -08:00
Andrew Chang 02815ee01c Don't recycle a WAL until its old writer is closed (#15458)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15458

X-link: https://github.com/facebook/rocksdb/pull/15458

**Bug:** with WAL recycling on (`recycle_log_file_num > 0`), RocksDB can lose writes that were already acknowledged, including `sync` writes.

**Why:** `FindObsoleteFiles()` puts an old WAL A on the recycle list and only closes A's writer later, after releasing the mutexes. If a memtable switch happens in that window, A is renamed and reused as the new WAL C, so A's still-open writer and C point at the same file. When the old writer finally closes, `PosixWritableFile::Close()` truncates the file to A's old size and wipes out what was written to C. The next reopen recovers from the truncated WAL, and those writes are gone. In the test, C went from 65,589 bytes to 32.

With `background_close_inactive_wals = true`, two more paths (`FindObsoleteFiles()` and `MarkLogsSynced()`) take A out of `logs_` before its writer is closed, so the same reuse can happen.

**Fix:** never reuse a WAL whose writer may still be open.
- `SwitchMemtable()` skips recycling a WAL that is still in `logs_` (`IsWalInLogs()`) and creates a fresh WAL that time.
- With recycling on, `FindObsoleteFiles()` and `MarkLogsSynced()` close a WAL's writer before removing it from `logs_`.

Reviewed By: xingbowang

Differential Revision: D126882379

fbshipit-source-id: ecaf3af9d89947d4236a07097bbd9cddaa962b71
2026-10-31 14:27:50 -07:00
Vitalii Kalinkin 563d4b14e0 Skip column families dropped during a whole-DB atomic flush (#15529)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15529

With `atomic_flush=true`, `DB::CompactRange()`, `WaitForCompact(flush=true)` and DB close flush all live column families through `AtomicFlushMemTables()` with internally generated candidates. If an unrelated column family is dropped while that flush waits, the flush returns `ColumnFamilyDropped`:
- `WaitUntilFlushWouldNotStallWrites()` returns it for a candidate dropped while waiting for write-stall conditions to clear, before any memtable is switched. `CompactRange()` then fails although its target is live, and `FlushAllColumnFamilies()` maps the error to OK without flushing the remaining column families.
- `WaitForFlushMemTables()` returns it when the only selected column family is dropped while its flush is pending, for example on `CompactRange(cf, nullptr, nullptr)` with an empty target and one other column family with data. This happens with `allow_write_stall=true` as well.

A dropped column family has nothing left to flush, so for generated candidates `AtomicFlushMemTablesImpl()` now skips it in the stall wait and treats `ColumnFamilyDropped` from `WaitForFlushMemTables()` as OK. Callers that pass explicit column families, such as `DB::Flush(handles)`, still get the error. `CompactRange()` returns `ColumnFamilyDropped` when its own target was dropped, and `FlushAllColumnFamilies()` no longer masks the status. There are no API, option or file format changes.

Reviewed By: xingbowang

Differential Revision: D128611460

fbshipit-source-id: 2e4d6c9204ca108eb7757bb8d39c3596348dee4d
2026-10-31 06:46:58 -07:00
Ranjan Banerjee 7e61be9ecc Preserve compaction context across worker handoffs (#15450)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15450

X-link: https://github.com/facebook/rocksdb/pull/15450

The separate remote compaction pool moves coordinators, sibling subcompactions, and local fallback work between threads. Consumers need the execution mode selected for each job so listener and compaction-service callbacks can preserve the correct context even when DB options change at runtime.

Capture `use_separate_remote_compaction_pool` on `Compaction` for background/manual compaction and `CompactFiles`. Expose the captured value in `CompactionJobInfo`, `SubcompactionJobInfo`, and `CompactionServiceJobInfo`, and populate it in parent/subcompaction callbacks and compaction-service scheduling requests. Default the metadata fields to false and keep each accepted job's value independent of later option updates.

Include the parent/subcompaction C API getters and their generated definitions. Add regressions for gate changes during a job and for metadata propagation through `CompactFiles` with multiple subcompactions.

Keep these library changes in `internal_repo_rocksdb/repo` so the normal RocksDB release carries all consumer prerequisites into fbcode. ZippyDB then only needs its gated context handling and tests; copying released RocksDB sources remains the responsibility of the separate release diff.

Reviewed By: xingbowang

Differential Revision: D126716904

fbshipit-source-id: fc6d377cd139fb2a81684f9f47daabc26d3095d5
2026-10-30 16:08:44 -07:00
Josh Kang a7ae6cd0a8 Remove dead code (#15407)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15407

X-link: https://github.com/facebook/rocksdb/pull/15407

## Summary

A full clang-tidy scan of RocksDB reports 65 dead-code findings in
existing code:
- `clang-analyzer-deadcode.DeadStores` (20)
- `-Wunused-member-function` (24)
- `-Wunreachable-code-return` / `-Wunreachable-code-loop-increment` (6)
- `-Wmissing-noreturn` (9)
- `-Wextra-semi` (6)

Every PR that touches nearby lines gets flagged for them, and some hide
real mistakes (an unchecked result, a loop that can only run once). This
diff fixes 57 of them.

What changed:
- Unused private and file-local member functions are removed, along with
  members that only fed them and local classes that are never
  instantiated. Examples: `EmptyIterator` in the hash memtable reps,
  `PersistentCacheFromCache` in `db_block_cache_test.cc`, and
  `LegacyFileSystemWrapper::target()`. Each one was confirmed unused in
  every build configuration and is file-local or private.
- Unreachable code after `return`, after a `throw`, or after an infinite
  loop is removed, but only where every compiler still sees every path
  returning. A loop that always returned on its first pass is now an
  `if`. A trailing return after `#ifdef` branches is now an
  `#else`/`#endif`.
- Dead stores are removed. Google Benchmark loop variables are marked
  `[[maybe_unused]]` (`for ([[maybe_unused]] auto _ : state)`).
- `[[noreturn]]` is added to functions that never return, such as
  `ErrorExit` in `db_bench`.
- Stray `;` after member function bodies are removed.

Not changed: `[[noreturn]]` was not added to `NoopDelete` helpers whose
body is `assert(false)`, because they return in release builds. It was
also not added to `MemTableRep::InsertConcurrently` (public virtual) or to
`LDBTool::Run` (public, deprecated), and lambdas have no portable C++20
syntax for it.

Reviewed By: xingbowang

Differential Revision: D123186311

fbshipit-source-id: 378348c02a494aef6786827e3d18b44cbd280a04
2026-10-30 15:45:12 -07:00
zaidoon 5f39d7b8ea Support parallel AddIndexEntry in trie index (#14966)
Summary:
**Why:** With `CompressionOptions::parallel_threads > 1`, the block-based table builder runs parallel compression only if every index builder implements the two-phase `PrepareAddEntry` / `FinishAddEntry` protocol. The trie builder declined it, so any SST built with the trie `IndexFactory` fell back to single-threaded compression (with a LOG warning), in every index mode.

**New behavior:** The trie builder opts into the protocol. `PrepareAddEntry` runs on the emit thread: it computes the separator and updates the size estimate. `FinishAddEntry` runs on the writer thread in block order: it attaches the block handle, resolves same-user-key boundaries against the previously committed entry, and buffers the entry. The serialized trie is byte-identical to the serial `AddIndexEntry` output.

**Implementation:**

- **Split state by thread.** The estimate (`estimated_trie_edges_`, `estimated_num_entries_`, and an emit-owned copy of the previous separator for shared-prefix accounting) is written only on the emit thread. `buffered_entries_` is owned by the writer. `EstimatedSize()` is called on the emit thread, from compaction's `ShouldStopBefore()` via `EstimatedTailSize()`, concurrently with `FinishAddEntry()`, so it never reads writer-owned state.
- **Mirror the serial path.** `FinishAddEntry` follows the serial branches and comparison primitives, including the unshortened-separator collision with the previous entry and the last-block check.
- **Fail on a protocol violation.** Finishing an entry that was never prepared, or finishing one twice, makes `Finish()` return `InvalidArgument`. Previously it was skipped silently, so the build succeeded with a trie that omitted that data block and seeks for its keys landed on the next block.
- **Crashtest.** `db_crashtest.py` no longer forces `compression_parallel_threads=1` when `use_trie_index=1`, so crashtest randomizes the thread count with the trie in both modes.
- **Renames (no behavior change).** The trie code and tests move from `UserDefinedIndex*` to the `IndexFactory*` names and from `ReadOptions::table_index_factory` to `read_index`, and tests set `index_mode` explicitly. The always-true `must_use_separator_with_seq_` flag becomes `!buffered_entries_.empty()`. The old public names remain as aliases on main.

## Safety and compatibility

- The table builder decides on parallel compression once, when it is constructed, so serial and parallel callbacks never mix within one SST. `Finish()` runs only after the worker threads join.
- The serialized trie format and readers are unchanged. Index builders that do not support the protocol still force serial compression for that SST.
- The table builder only finishes entries it prepared (an existing table test asserts equal prepare and finish counts), so the new `InvalidArgument` path cannot fire in a supported build.
- Users who configure the trie with `parallel_threads > 1` now get multi-threaded compression for those SSTs, and therefore more CPU threads per build. No throughput gain is claimed; this was not benchmarked.
- The release note for parallel compression with a custom index is in https://github.com/meta/rocksdb/issues/14968.
- The existing 32-bit block offset/size limit of the trie encoding is unchanged.

Pull Request resolved: https://github.com/meta/rocksdb/pull/14966

Test Plan:
Unit (`trie_index_test`):

- `ParallelMatchesSerialOutput`: serial and parallel builds produce byte-identical output, including a four-block run of one user key and the last block.
- `ParallelPipelineOverlapsPrepareAndFinish`: separate emit and writer threads with a bounded handoff queue, output identical to a serial build.
- `ParallelEstimatedSizeStaysMonotonic`: the estimate grows on `PrepareAddEntry` and is unchanged by `FinishAddEntry`. Verified to fail if `EstimatedSize()` is changed back to read writer-owned `buffered_entries_`.
- `ParallelFinishWithoutPrepareFailsBuild`: covers a never-prepared entry and an entry finished twice. Verified to fail without the fix: `Finish()` returned OK and a seek for a key in the skipped block returned the next block's offset.

DB (`trie_index_db_test`):

- `ParallelCompressionWithTrieIndex` (all three modes), `ParallelCompressionWithHashStandardIndexAndTrieUdi`, and `SharedPrefixWithParallelCompression` now also require the parallel pipeline to have started (via the `MaybeStartParallelCompression:Started` sync point). Verified: with the trie builder forced to decline the protocol, all 7 previously passed and now fail.
- Under TSAN, `SharedPrefixWithParallelCompression` reports a data race between `EstimatedSize()` (compaction thread) and `FinishAddEntry()` (writer thread) if `EstimatedSize()` reads `buffered_entries_` again, and the process exits non-zero. It is clean with this PR.

Crashtest (`tools/db_crashtest_test.py`):

- `test_finalize_keeps_parallel_compression_with_trie_index`: with `use_trie_index=1` and `compression_parallel_threads=4`, sanitizing keeps 4 threads in both modes. Verified to fail before the change (it returned 1).

Runs on this branch rebased onto main at 160a3171f (macOS ARM64, Apple Clang):

- Debug: all of `trie_index_test` (156) and `trie_index_db_test` (260) pass.
- `ASSERT_STATUS_CHECKED=1 COERCE_CONTEXT_SWITCH=1`: both suites pass, and the 12 parallel tests pass 100 times each (1,200 runs).
- TSAN (`COMPILE_WITH_TSAN=1`): both suites pass with no reports.
- No compression libraries (`ROCKSDB_DISABLE_{SNAPPY,ZLIB,BZIP,LZ4,ZSTD}=1`, as in `build-linux-encrypted_env-no_compression`): both suites pass, and the 7 Zlib-dependent parallel tests skip.
- `db_stress` (debug) with `--use_trie_index=1 --compression_type=zlib --compression_parallel_threads=4`, 512-byte blocks, 256 KiB memtables and target files, 8 threads x 200,000 ops, and 10 reopens, in both `--use_udi_as_primary_index=0` and `=1`: final verification passed with 0 errors, and the LOG has no parallel-compression fallback warning.
- Blackbox crashtest (`db_crashtest.py blackbox`, 300 s, killed every 30 s) with `--use_trie_index=1 --compression_parallel_threads=4` and all other parameters randomized by the script, once per index mode. Each run completed 10 kill/restart cycles. In secondary mode, the script's final crash-recovery verification passed. In primary mode, I reran the final verification on a copy of the DB with `--read_fault_one_in=0`, and it passed. The script's own run with read faults on spent its time retrying compactions inside `WaitForCompact`, because `db_stress` injects read faults into every subcompaction. That injection doesn't depend on the index.
- Source and format checks pass.

Not run locally: full `make check`, Linux, Windows, ASAN, UBSAN, or a release (`-fno-rtti`) build. Hosted CI covers those.

## Series

Builds on https://github.com/meta/rocksdb/issues/14963 and https://github.com/meta/rocksdb/issues/14965, both already on main. This branch contains only this PR's commits. Next: https://github.com/meta/rocksdb/issues/14968.

Reviewed By: jaykorean

Differential Revision: D128658113

Pulled By: xingbowang

fbshipit-source-id: 3e3090565f92401274e27c112edeef3cac1bb398
2026-10-30 15:22:06 -07:00
Ranjan Banerjee fcb42ecdb2 Synchronize manual compaction cancellation during close (#15528)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15528

Closing a DB can race with a manual-compaction caller finishing canceled work. `CloseHelper()` reads `manual_compaction_dequeue_` through `HasPendingManualCompaction()` without holding `mutex_`, while the caller removes its entry under that mutex. The concurrent-close regression `CloseQueuedManualBottomSubcompactions` exposed this as a TSAN data race.

Acquire the DB mutex before checking for pending manual work during close. If work is pending, release the lock before calling `DisableManualCompaction()`, then reacquire it before teardown continues. An empty queue skips cancellation, preserving the existing `manual_compaction_paused_` count. The public helper retains its contract of pausing future manual requests even when its queue is empty.

Require the mutex in both `HasPendingManualCompaction()` overloads with `AssertHeld()`. Add regressions for idle manual disable, pause-count preservation when closing without pending work, and repeated close.

Reviewed By: xingbowang

Differential Revision: D128672217

fbshipit-source-id: 4169c44ff568d5750f33af37b3f5f03cec120867
2026-10-30 14:51:20 -07:00
Josh Kang bc11adb4ba Add buffer-based internal key encoding (#15495)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15495

X-link: https://github.com/facebook/rocksdb/pull/15495

## Summary

`EncodeEntry()` currently requires a `std::string`, so callers using preallocated storage must stage encoded keys in a string or reproduce the encoding themselves. Add `GetEncodedEntrySize()` and a `char*` overload that writes directly into caller-owned storage without allocating on success.

Callers provide at least `GetEncodedEntrySize(entry)` writable bytes. Input slices must not overlap the written region; debug builds assert this. The overload uses the existing internal key format and validates sequence and type before writing, leaving the buffer unchanged on error. The string overload retains its append implementation.

Reviewed By: xingbowang

Differential Revision: D127866846

fbshipit-source-id: efcd489aeb487a2ce17f2d396fe50dc68e1161c4
2026-10-30 14:50:31 -07:00
Josh Kang feb6fb8377 Fix atomic replace ingestion racing with compaction on replaced files (#15392)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15392

X-link: https://github.com/facebook/rocksdb/pull/15392

## Summary

Fix atomic range replacement racing with a background compaction that selects an SST the replacement is deleting. During `LogAndApply()`'s unlocked MANIFEST write, the old SST remained in the current LSM version and appeared available to the picker. If ingestion committed first, compaction later tried to delete the same SST and raised a `VersionBuilder` corruption, leaving the DB in a background error state that fails subsequent writes. Registration of the new SST output ranges did not protect the old deletion inputs; universal compaction with `num_levels=1` permits overlapping L0 outputs.

Reserve the actual old SSTs with `being_compacted`, checking eligibility and registering under one DB mutex hold. Cleanup releases only reservations acquired by the job, on success or failure. Full-column-family and bounded replacement reject already-reserved inputs, including `DeleteFilesInRanges()` reservations that have no registered compaction. Atomic range replacement now returns `NotSupported` with FIFO compaction, before preparing or reserving files. Existing partial-overlap handling and snapshot restrictions are unchanged, and there are no persisted format changes.

Replacement now selects files from a pinned `cfd->current()`, and ingestion level assignment probes `cfd->current()` as well, matching the Version it already uses for level fit. The published SuperVersion can lag after a grouped MANIFEST write in which a follower compaction released its inputs before installing its own SuperVersion. Selecting from it could delete an SST that is no longer in the LSM tree, and probing it could place a replacement range tombstone below the compaction output it must cover. Reserving files filters cached compaction candidates; successful commits use the normal Version refresh. Failed commits recompute compaction eligibility before rescheduling.

### Race outcomes

| Ordering under the DB mutex | Winner | Expected behavior |
| Compaction selects the SST before ingestion reserves it. | Compaction | Ingestion returns `InvalidArgument` for the pending-compaction conflict without committing the replacement. Compaction continues; the caller can retry ingestion. |
| Ingestion checks and reserves the SST before compaction selects it. | Ingestion | The picker skips the reserved SST and may use other eligible inputs or find no work. The reservation covers MANIFEST I/O; successful replacement removes the SST before cleanup clears its flag. |
| DeleteFilesInRanges reserves the SST before ingestion checks it. | File deletion | Full-column-family replacement returns `InvalidArgument` without committing or clearing deletion's reservation. Deletion completes; the caller can retry ingestion. |
| Ingestion reserves the SST before DeleteFilesInRanges checks it. | Ingestion | `DeleteFilesInRanges()` skips the reserved SST and can return success without deleting it. Successful replacement removes the old SST before releasing its reservation. |

Reviewed By: xingbowang

Differential Revision: D124614960

fbshipit-source-id: b85030cf7609e97df2ea0e34e30db2bd6789ce16
2026-10-30 14:12:54 -07:00
Erin Gao d2d4c9b44c Expose batched_multi_get_cf on pessimistic TransactionDB (#15520)
Summary:
PR adds two TransactionDB C APIs:
- `rocksdb_transactiondb_batched_multi_get_cf`
- `rocksdb_transactiondb_batched_multi_get_cf_slice`

They mirror the existing base-DB wrappers, return pinned values, preserve `sorted_input`, and call TransactionDB directly to retain transaction visibility rules.

## Motivation
We are using the pessimistic DB and would not like to unwrap to the base db, which is what the current API does. The other option which uses TransactionDB doesn't return the DBPinnableSlice or have the ability to specify whether inputs are sorted.

Pull Request resolved: https://github.com/meta/rocksdb/pull/15520

Reviewed By: anand1976

Differential Revision: D128633853

Pulled By: xingbowang

fbshipit-source-id: f84ac90bcf5a1e13d6516db90127c280879839a7
2026-10-30 14:10:24 -07:00
Josh Kang 800b5741f2 Limit prepared ingestion SST protection to reserved files (#15490)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15490

X-link: https://github.com/facebook/rocksdb/pull/15490

## Summary

Prepared external ingestions held a `pending_outputs_` watermark until commit or abort, preventing reclamation of unrelated SSTs created later and compacted away. Prepared ingestions now protect only their reserved SST numbers through `pending_ingestion_outputs_` (`first_file_number` -> `num_files`), copied into full-purge `sst_live` snapshots. This preserves staged inputs and generated tombstones while allowing unrelated obsolete SSTs to be deleted.

Successful manifest completion releases reservations under the DB mutex after all Versions are installed, including grouped commits performed by another writer. Waiting for the ingestion caller to resume could otherwise let purge claim a compacted input and retain it through the stale reservation, leaving its grabbed marker behind. Failure and abort retain protection through cleanup. Public APIs and persisted formats are unchanged, existing background-output protection remains in place, and handles still retain other resources until consumed.

## Example

Keep a prepared handle open for SST `100`, containing key `z`. Meanwhile, normal DB writes update another key, `a`. The SST numbers are illustrative.

```text
Prepare ingestion:
  SST 100: key z (staged, awaiting commit)

Unrelated DB activity:
  flush a=old --> SST 101 --+
                            +--> compaction --> SST 103 (live)
  flush a=new --> SST 102 --+
                  101 and 102 become obsolete

Files remaining after full purge, before commit/abort:
  SST   State      Before fix   After fix
  100   staged     kept         kept
  101   obsolete   kept         deleted
  102   obsolete   kept         deleted
  103   live       kept         kept
```

`100` is not yet visible to reads or compaction. After compaction releases its input references, the old watermark still protects obsolete `101` and `102`. The new reservation protects only staged `100`; `103` remains because it is live.

Reviewed By: xingbowang

Differential Revision: D127694250

fbshipit-source-id: c89ecc94c96ff0a2043a2673cc2484c68323f275
2026-10-30 13:57:20 -07:00
Ranjan Banerjee 160a3171f2 Exercise the remote compaction pool in stress tools
Summary:
The stress and benchmark tools need a way to select the separate remote compaction pool when exercising a compaction service.

Expose `use_separate_remote_compaction_pool` as a default-false flag in `db_stress` and `db_bench`, and pass it into their DB options. Randomize the flag in `db_crashtest.py` to cover both scheduling modes. The option takes effect when a compaction service is configured.

X-link: https://github.com/facebook/rocksdb/pull/15406

Reviewed By: xingbowang

Differential Revision: D125484658

fbshipit-source-id: 1fb3f0b92ff156f86d035f11b14e2c5316c60d19
2026-10-30 12:47:10 -07:00
Ranjan Banerjee e6d1ec007f Remote Compaction to not take local slots but run in a separate remote pool
Summary:
Remote compaction coordinators currently occupy LOW/BOTTOM workers and local compaction credits while scheduling and waiting for remote execution. Slow remote work can delay local compactions, including jobs with multiple subcompactions.

Add the default-off, runtime mutable `use_separate_remote_compaction_pool` DB option. When enabled with a `CompactionService`, coordinators and sibling subcompactions use one process-wide elastic pool, including DBs with different Env wrappers. Sharing the pool avoids multiplying its baseline across shard DBs. The baseline is the largest combined LOW/BOTTOM thread count among participating DBs, and global outstanding work determines growth. After 30 seconds of inactivity, the pool shrinks to the enabled clients' baseline, or zero if none remain. Name its workers `rocksdb:remote`.

Local fallback acquires a DB credit when needed and executes in LOW/BOTTOM. Caller-driven `CompactFiles` retains inline local execution. Each DB drains its own accepted tasks before closing; only the last pool reference joins the shared workers. Preserve completion and submission exception handling and each coordinator's captured gate across runtime updates.

Support parsing, runtime updates, and the generated C API, while omitting the option from serialized OPTIONS so older remote workers can parse requests. Gate-only mutable updates remain effective. Expose the process-wide worker count through `DB::Properties::kNumRemoteCompactionPoolThreads`; collectors must sample it once rather than sum it across DBs.

X-link: https://github.com/facebook/rocksdb/pull/15361

Reviewed By: xingbowang

Differential Revision: D124139026

fbshipit-source-id: a46de1740db5aa2068525bb6b4fd18f808cdf864
2026-10-30 12:19:34 -07:00
Xingbo Wang e6961ad337 Fix resumable remote compaction losing restored outputs on repeated resume (#15526)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15526

CONTEXT:
Resumable remote compaction can lose output data when the same job is resumed more than once.

`DBImplSecondary::FinalizeCompactionProgressWriter()` sets up the progress file for a resumed job. It wrote the initial progress record (the list of restored output SSTs and the record count) into a temporary progress file, renamed that file to the final name, and then reopened the final file with `CreateCompactionProgressWriter()`. The reopen uses `FileSystem::NewWritableFile()`, which truncates the file, so the initial record was erased.

Before 7790768fe8a2 ("Support BlobDB in remote compaction") this was hidden: `last_persisted_output_files_count_` stayed 0 after the initial persist, so the first checkpoint of the resumed job wrote all restored output files again together with the new ones. 7790768fe8a2 made `PersistInitialCompactionProgress()` call `UpdateLastPersistedOutputFilesCount()` (and the blob-addition equivalent). Now the first checkpoint only writes the files added after resumption, together with the cumulative `num_processed_output_records`, so the restored files are in no progress record on disk.

Previous behavior: on the next resume of the same job (third attempt), the worker loads a progress whose file list lacks the restored files but whose record count still includes them. `CleanupPhysicalCompactionOutputFiles()` deletes those SSTs as untracked, and the worker returns `num_output_records` larger than the entries in the returned files. The primary's `VerifyOutputRecordCount()` then fails with "Number of keys in compaction output SST files does not match number of keys added" (count > entries). With `compaction_verify_record_count=false`, the data is silently dropped.

The stress test hits this with `allow_resumption_one_in=1` and `remote_compaction_worker_threads=8`: the first attempt is always canceled and later attempts are canceled 1 in 10, so some jobs resume more than once. Seen in T295182746 (JOB 1086 and JOB 1069: each final result has one restored file followed by a file-number jump) and T295084500.

WHAT:
New behavior: the initial progress record is written and synced through the final writer, after the final progress file is opened. The record stays in the same file that later checkpoints append to, so the progress file always lists all restored SSTs and the delta encoding from 7790768fe8a2 stays correct. Repeated resumes keep all restored outputs, and the output record count matches the output files.

Implementation:
- `FinalizeCompactionProgressWriter()`: move the `PersistInitialCompactionProgress()` call from the temporary writer to the final writer, after `CreateCompactionProgressWriter(final_file_path)`. If it fails, clean up the final file (the temporary file is already renamed). Also fix the "copmaction" typo in the log message.
- Test: add `ResumableCompactionService::SetSecondCancelAtKey()`, which runs an extra resume-then-cancel phase between the first canceled phase and the final phase, and add `ResumableCompactionServiceTest.RepeatedCancelKeepsOutputRecordCountConsistent`.

Safety boundaries and unsupported cases:
- No change to the progress record format, public API, or DB mutex use. Fresh (not resumed) attempts write nothing extra.
- A crash after the final file is created and before the initial record is synced leaves an empty latest progress file. As before, the next attempt then starts fresh. This window is shorter than before, when the file stayed empty until the first checkpoint.
- Progress files already written by binaries that have 7790768fe8a2 but not this fix are not repaired. Resuming such a job again can still fail record count verification.
- The restored progress is now durable. If a resumed attempt fails every time while restoring outputs (for example, a tracked SST cannot be read), later resumes return the same error instead of accidentally starting fresh. This matches the documented `allow_resumption` contract.

Validation: the new unit test cancels, resumes and cancels again, then resumes to completion with record count verification on primary and worker. It fails without the fix and passes with it; the full `compaction_service_test` passes. Exact commands and results are in the Test Plan.

Reviewed By: pdillinger

Differential Revision: D128126725

fbshipit-source-id: 6321a1a69c03f925d820a617ed25883be97ea74a
2026-10-30 11:40:18 -07:00
Andrei Preda 7fb0d8e94a Fix MultiScan Prepare crashes with L0 files (#15502)
Summary:
Problem: L0 table iterators receive Iterator::Prepare() directly from
the MergingIterator, bypassing LevelIterator, which exposes two bugs
in BlockBasedTableIterator::Prepare():

1. A single scan range without a limit reaches it as Prepare(nullptr),
   which CollectBlockHandles() dereferences (SIGSEGV).

2. On an already-positioned iterator (Seek, SeekToFirst, SeekToLast,
   SeekForPrev, Next), Prepare() marks the data block unloaded without
   invalidating it, so the next Seek() re-initializes a live block
   iterator (assertion in BlockIter::InitializeBase() in debug builds).

Memtable-only DBs, files in L1+, and Prepare() as the first call on an
iterator are unaffected.

Fix:
1. Prepare(nullptr) returns early, as in LevelIterator.
   InternalIterator::Prepare() now documents the nullptr case.

2. Prepare() resets the data iterator before repositioning the index.
   Since that releases the block key() and value() point into,
   Prepare() also invalidates the iterator until it is positioned
   again; it was already documented to be followed by Seek().

Pull Request resolved: https://github.com/meta/rocksdb/pull/15502

Test Plan:
New PrepareWithMultiScanAfterPositioning covers each positioning call,
a reverse MultiScan, pin_data and first-key indexes, and checks that
MultiScan ran, Valid() is false after Prepare(), and the scanned keys
and values. PrepareWithMultiScanAllowsSingleUnboundedRange now
includes an L0 file. Reverting each change makes a test fail: SIGSEGV
in CollectBlockHandles(), the InitializeBase() assertion, or Valid()
after Prepare(). make check passes (2264 shards).

Reviewed By: anand1976

Differential Revision: D128181531

Pulled By: xingbowang

fbshipit-source-id: 4b662046a2b5790362a7c832b0b7c3eec6237f41
2026-10-30 10:15:38 -07:00
zaidoon 4ef117052f Account UDI blocks as index blocks
Summary:
Custom-index blocks previously lacked entries in the cache-helper tables and were omitted from the index-specific block-read counters. Register their cache helpers and count their reads and bytes as index-block activity, making custom-index work visible in the same statistics as built-in indexes.

Preserve the ownership contract established in https://github.com/facebook/rocksdb/issues/14963: charge the serialized block once, either to the table reader or to the block cache. A parsed `IndexFactoryReader` reports only its own allocations, excluding the borrowed serialized bytes.

## Safety and compatibility

This changes cache helper registration and statistics, without changing the SST format or index selection. The existing cache enablement and lifetime rules still apply. The accounting regression allows small allocator-size-class differences but rejects a missing or duplicate raw-block charge.

X-link: https://github.com/facebook/rocksdb/pull/14965

Reviewed By: jaykorean

Differential Revision: D126949386

Pulled By: xingbowang

fbshipit-source-id: d7b8d6cbaf939c20adbbbd24a4ff6f26d155cb5f
2026-10-30 10:14:42 -07:00
Jay Huh ce00781b64 Trace decoder ignores query record fields from newer versions
Summary:
X-link: https://github.com/facebook/rocksdb/pull/15486

Why: query records (Write, Get, MultiGet, Seek/SeekForPrev) list their fields in a 64-bit `payload_map`, and a newer RocksDB can add fields under new bits, as https://github.com/facebook/rocksdb/pull/15460 did with `TraceOptions::include_read_write_options` (bits 11-14). `TracerHelper::DecodeTraceRecord()` hit `assert(false)` on any bit it did not know, so a debug build could not read a trace from a newer version; release builds already went on and decoded the known fields. A review of https://github.com/facebook/rocksdb/pull/15453 raised this for traces recorded with the new flag.

Previous behavior: on an unknown bit, debug builds abort. Release builds skip the bit without consuming its bytes; because fields are encoded in bit order, every known field was already decoded by then and the record is used without the new field.

New behavior: at the first unknown bit, the decoder stops reading the record's fields, in debug and release builds alike, and returns the record with the fields it knows. A later field it would know is never placed after an unknown one, because encoders write fields in bit order and new fields get higher bits.

Compatibility: no format change. Release-build behavior is unchanged; traces this version writes decode as before. Versions without this change still assert in debug builds on traces written with `include_read_write_options` (documented on that option).

Reviewed By: xingbowang

Differential Revision: D127652082

fbshipit-source-id: 01a34d705e57a4b1429ad74d8602502357153dc6
2026-10-30 09:50:05 -07:00
cm-sally-mate 87d442749a Let async_io MultiGet read direct-IO files asynchronously (#15493)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15493

X-link: https://github.com/facebook/rocksdb/pull/15493

**What:** with `ReadOptions::async_io`, MultiGet now reads direct-IO files asynchronously too. Before, `RetrieveMultipleBlocks` sent them to the synchronous `MultiRead`, so a MultiGet over direct-IO files got no read overlap at all. Async reads are now also charged to the rate limiter, like sync reads.

**How:**
- `AsyncFileReader::MultiReadAsyncImpl`: for a direct-IO request without a scratch buffer, `RandomAccessFileReader::ReadAsync` allocates the aligned buffer and hands it back in the request's `fs_scratch`, with `result` pointing into it. That uses the existing `aligned_buf` support in `ReadAsync`, so the file system only sees aligned requests. If `ReadAsync` reports NotSupported, the synchronous fallback reads through an `AlignedBufferAllocationContext` and moves that buffer into `fs_scratch` the same way.
- `RandomAccessFileReader::ReadAsync` charges the read rate limiter at `rate_limiter_priority` for every read it submits: the aligned length for direct IO, the request length otherwise. It charges once the file system accepts the read, because every caller falls back to `Read` on NotSupported and `Read` charges for itself. `MultiRead` and `ReadAsync` share the charging loop (`ChargeRateLimiterForRead`).
- `RetrieveMultipleBlocks`: direct-IO files now take `MultiReadAsync` like buffered files. The requests (and so their `fs_scratch`) live until the blocks are parsed.

**Behavior changes:**
- Direct IO: each block request is aligned and read on its own; requests aren't merged. Adjacent blocks of one file can read an overlapping aligned page twice.
- Async reads (async_io MultiGet and iterator prefetching), buffered or direct, are now charged to `DBOptions::rate_limiter`. Before, `ReadAsync` charged nothing.

Reviewed By: archang19, anand1976

Differential Revision: D127688099

fbshipit-source-id: cfab475b6a27cb2a78b610144f9f8436c3b8a807
2026-10-29 17:28:27 -07:00
Daniel Bradleigh Crockett f216185228 Emit fbsource//xplat/folly deps from the RocksDB buckifier (#15521)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15521

The fbcode copy of folly is a dirsync mirror of xplat/folly that is being removed; once it is gone, `fbcode//folly` targets will no longer resolve. Hand-written BUCK files are migrated by `migrate_fbcode_folly.py`; generated BUCK files have to be fixed in their generator.

Rewrites the 16 `rocksdb_lib` folly deps in `internal_repo_rocksdb/repo/buckifier/buckify_rocksdb.py` to `fbsource//xplat/folly...` and regenerates `internal_repo_rocksdb/repo/BUCK` with `python3 buckifier/buckify_rocksdb.py`; only the 16 labels change.

`fbcode/rocksdb/src/BUCK` is a release snapshot updated by the periodic RocksDB release process and is not changed here.

___

landed-with-radar-review

Differential Revision: D128219553

fbshipit-source-id: 259fd54bf827a75e389c7d9be8275145f55515a8
2026-10-29 17:27:19 -07:00
Josh Kang f14b3508d4 Allow the current Version as a secondary replay base (#15499)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15499

X-link: https://github.com/facebook/rocksdb/pull/15499

## Summary

A secondary can successfully open without installing a new `Version` when the SSTs referenced by its initial MANIFEST snapshot disappear during primary compaction. On a later MANIFEST switch, catch-up selects the original empty `Version` as its replay base. That `Version` is still `current()`, so an overly strict assertion in `BaseReferencedVersionBuilder` aborts assertion-enabled builds.

Allow that valid base with one explicit-base `BaseReferencedVersionBuilder` constructor that takes and releases its own reference. Ordinary updates pass `current()`; MANIFEST replay passes the retained empty `Version`. This replaces the overload-specific inequality assertion and moves reference acquisition into the constructor, before the old builder is released. Replay still constructs a separate candidate `Version`; reference counts, release-build replay behavior, public APIs, and persisted formats are unchanged.

## Example

In this example, `V0` is the secondary’s initial empty `Version`, and `A`/`B` are SST files. A MANIFEST begins with a live-file snapshot, followed by later edits. The log names and SST layout below are simplified.

```text
Primary                                      Secondary
MANIFEST-1 snapshot: AddFile(A)               opens MANIFEST-1;
                                             current = V0 (empty)
roll to MANIFEST-2:
  snapshot: AddFile(A)
compact A -> B:
  append ONE edit to MANIFEST-2:
    DeleteFile(A), AddFile(B)
  purge A                                    replay MANIFEST-1 snapshot:
                                               A is missing
                                               no candidate installed
                                               current still V0

Catch-up sees CURRENT -> MANIFEST-2:
  replay base = V0 (empty); current() = V0
  old constructor: assert(version_ != cfd->current()) -> abort

With the unified constructor:
  Ref(V0), then replay MANIFEST-2 from the beginning:
    snapshot AddFile(A)                   -> A missing
    edit {DeleteFile(A), AddFile(B)}      -> live file set {B}
    SaveTo() a separate V1, then install  -> current() = V1 {B}
```

The new MANIFEST is rebuilt from the retained empty `V0`. Here `V0` is also current because initial recovery installed no candidate. The builder keeps `V0` referenced while replay saves the reconstructed file set into a separate `V1`, then installs `V1`.

## CI test fix

Also reorder `SyncPointAbortHelper::CleanupAndWait()` to call `WaitForAbortCompletion()` before `CleanupSyncPoints()`, fixing a pre-existing race exposed by CI. This keeps the abort dependency active until the worker has observed the abort and exited.

Reviewed By: pdillinger

Differential Revision: D127928276

fbshipit-source-id: 3e437328ed5afc8c9bf53bb97cbce0dfe2560097
2026-10-29 16:45:54 -07:00
Peter Dillinger 92c99cb6aa Some Status testing advice in CLAUDE.md and status.h
Summary:
as title, after observing bad agent behavior

X-link: https://github.com/facebook/rocksdb/pull/15476

Reviewed By: xingbowang

Differential Revision: D127492952

Pulled By: pdillinger

fbshipit-source-id: 94cfe2766433a7e59c05a79e189d53b46facc292
2026-10-29 14:53:15 -07:00
Omkar Gawde 42ed9b6a09 Reject malformed hex in encoded string fields (#15512)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15512

`OptionTypeInfo` parses an `OptionType::kEncodedString` field by hex-decoding its value, but `ParseOptionHelper()` ignored the result of `Slice::DecodeHex()` and always reported success. A value with an odd number of digits left the field unchanged, which is empty in a freshly constructed struct. A value with a non-hex digit kept only the bytes decoded before that digit. In both cases `Parse()` returned OK.

Only serialized formats that RocksDB itself writes as hex use `kEncodedString`: the remote compaction input and result (`CompactionServiceInput::Read()` and `CompactionServiceResult::Read()`), and `TableProperties::Parse()`, which the compaction result and the `PreparedFileInfo` deserialization use. So a corrupted or truncated serialized string was accepted with an empty or shortened value in fields such as the column family name, an input file name, a compaction range key, an output file key, or a file checksum, with no sign that the input was bad. The custom blob-metadata parser in `compaction_service_job.cc` already rejects bad hex with `InvalidArgument`.

Fix: decode into a local string and return false when `DecodeHex()` fails, so `OptionTypeInfo::Parse()` returns `InvalidArgument("Error parsing:", <field>)`. The field stays unchanged on failure, as with the `StringMap` parser.

Compatibility: `Serialize()` always writes valid hex, and valid hex in either letter case parses as before. Only malformed input changes behavior: it now fails instead of yielding an empty or partial value. `CompactionServiceInput::Read()` and `CompactionServiceResult::Read()` still ignore unknown fields.

Reviewed By: jaykorean

Differential Revision: D128131523

fbshipit-source-id: 49e2840758b96e24c910355b0d969092e760a222
2026-10-29 13:35:20 -07:00
Rajiv Harlalka 609269e0ae Fix GetCharge on uncharged standalone LRU entries
Summary:
X-link: https://github.com/facebook/rocksdb/pull/15501

Why the change is required:
`Cache::CreateStandalone` documents that with `strict_capacity_limit` and no
room, `allow_uncharged=true` creates the entry anyway with `GetCharge() == 0`.
The secondary cache adapter relies on this whenever it promotes an entry into
a full strict primary cache. Under `kFullChargeCacheMetadata` (the default),
such entries violated the public API contract.

Previous behavior:
`LRUCacheShard::CreateStandalone` sets `total_charge = 0` for an uncharged
entry, but `LRUHandle::GetCharge` unconditionally subtracted the metadata
charge. Debug builds aborted on `assert(total_charge >= meta_charge)`;
release builds reported enormous per-handle charges from unsigned underflow,
corrupting block-cache byte statistics through `GetUsage(handle)` in
`UpdateCacheHitMetrics`. The trigger requires an LRU cache with
`strict_capacity_limit`, full metadata charging, pinned entries preventing
sufficient eviction, and `allow_uncharged=true`. `kDontChargeCacheMetadata`
and HyperClockCache are unaffected.

New behavior:
Uncharged standalone handles report `GetCharge() == 0`, preventing the debug
assertion failure and incorrect release-build byte statistics.
`GetUsage(handle)` and `ApplyToHandle` also receive the correct charge. Total
cache usage accounting is unchanged: uncharged handles add no usage on
creation and subtract none on release.

Implementation:
`LRUHandle::GetCharge` returns zero for standalone handles with the named
zero-charge sentinel, retaining the existing assertion for other handles.
Added a release note and simplified the direct API regression test.
`FullCapacityTest` now exercises both metadata charge policies and verifies
zero per-handle usage when strict capacity forces secondary-cache promotion
to use an uncharged standalone handle. Its 700-byte values leave room for
metadata while ensuring two entries still exceed the 1024-byte capacity.

Safety, compatibility, and limitations:
Only the previously broken standalone, uncharged case changes its returned
charge. There are no API signature, ABI, or serialized-format changes.
The added check runs on every LRU `GetCharge` call, for every handle,
including block-cache hits through `UpdateCacheHitMetrics` and
`GetUsage(handle)`. Expected overhead is negligible because `im_flags` is
stored near `total_charge` and the branch is predictable; this is an
assessment of the code, with no performance benchmark performed.

Reviewed By: xingbowang

Differential Revision: D126972213

fbshipit-source-id: 595d8771b0e57f20c12314e00c2aa2d7d2965a34
2026-10-29 12:47:48 -07:00
Peter Dillinger 4750c1cce3 Move macOS CMake tests to nightly; remove unused post-steps action (#15518)
Summary:
CI cost reduction and cleanup in the GitHub Actions configuration:

* Move `build-macos-cmake` (four test shards on `macos-15-xlarge`) from pr-jobs to nightly. These jobs are among the most expensive per run and rarely catch anything not caught elsewhere. PRs still run `build-macos` (a build without tests) and the macOS Java jobs. The job is unchanged except for using nightly's owner check (it can no longer be selected through pr-jobs' `ONLY_JOB`; use workflow_dispatch on nightly instead).

* Remove the `post-steps` composite action and its 36 call sites. It was invoked without an `if:` condition, so GitHub skipped it whenever an earlier step had failed, meaning it only ever ran after successful jobs. Its failure-oriented steps (copying `t/` test logs, uploading core dumps) therefore never did anything. On success it uploaded the gnu_parallel job log (misleadingly named "db-log-file") and gtest XML results, neither of which is consumed by anything, and the macOS Java jobs that call it produce neither. Failing test output is already printed in the job console by `make check`. The `GTEST_OUTPUT` setting in pre-steps existed only to produce the uploaded XML, so it is removed too. Rather than fixing post-steps to run on failure, this change drops it because what it would collect is redundant with the console output.

* Bonus: the pre-steps diagnostics step now logs OS and CPU details alongside the existing ulimits, to help diagnose failures that depend on the runner environment.

Pull Request resolved: https://github.com/meta/rocksdb/pull/15518

Test Plan: CI on this PR validates the workflow YAML (`make check-workflow-yaml` in check-format-and-targets) and exercises pre-steps and the remaining jobs without post-steps. The relocated `build-macos-cmake` won't run before merge; parsing the workflows confirmed the nightly job is identical to the removed pr-jobs job apart from its owner-check condition. A manual workflow_dispatch of nightly after merge can confirm it runs.

Reviewed By: xingbowang

Differential Revision: D128353844

Pulled By: pdillinger

fbshipit-source-id: d066e6f60bf433ab6dc70aea7e1cd22d7207c8d1
2026-10-29 11:42:28 -07:00
Alex Zhavnerchik a12aafaf69 Track newer snapshot versions in entity metadata reads (#15398)
Summary:
Pull Request resolved: https://github.com/meta/rocksdb/pull/15398

X-link: https://github.com/facebook/rocksdb/pull/15398

Why: Applications that populate a cache from explicit-snapshot wide-column reads (`GetEntity` / `MultiGetEntity`) need to know whether a returned entity has already been superseded by a newer committed write, without issuing a second lookup. `D104182397` added `DB::GetWithMetadata()` and `DB::MultiGetWithMetadata()` for plain values, but left entity reads without a metadata counterpart.

Previous behavior: `DB::GetEntity()` and `DB::MultiGetEntity()` returned the snapshot-visible `PinnableWideColumns` or `PinnableAttributeGroups`, but always passed `newer_version_present = nullptr` into `DBImpl::GetImpl` and `DBImpl::MultiGetCommon` and could not report a newer point write or covering range deletion.

New behavior: experimental C++ `DB::GetEntityWithMetadata()` (virtual column-family overload plus default-CF helper) and `DB::MultiGetEntityWithMetadata()` (multi-CF `PinnableWideColumns*` virtual, single-CF helper, and `PinnableAttributeGroups*` attribute-group virtual) report newer committed `PutEntity`, `Put`, `Delete`, `Merge`, `SingleDelete`, and covering `DeleteRange` operations after an explicit snapshot. As with `GetWithMetadata()` / `MultiGetWithMetadata()`, ordinary primary-DB writes completed before the read starts are guaranteed to be reported, racing writes remain best-effort, and `true` has no false positives. For the attribute-group overload, `MultiGetOutputMetadata` vectors are sized to the total number of attribute groups across `results[0..num_keys-1]` in `(key, attribute_group)` order.

Implementation: newer-version tracking in `DBImpl::GetImpl` and `DBImpl::MultiGetCommon` is already result-type agnostic (gated on the read-callback metadata bounds inside memtable, SST, range-tombstone, and row-cache lookups rather than `value` vs `columns`). `DBImpl::GetEntityWithMetadata` and `DBImpl::MultiGetEntityWithMetadata` validate inputs, initialize `OutputMetadata` / `MultiGetOutputMetadata` via shared helpers in `db/db_impl/db_impl_metadata.h`, and pass the requested `newer_version_present` pointer or vector alongside `columns` into `GetImpl` and `MultiGetCommon` while keeping metadata-only state off passive `PinnableWideColumns` and `PinnableAttributeGroup` layouts. Existing `GetEntity()` and `MultiGetEntity()` paths are unchanged.

Safety and unsupported cases: tracking is opt-in (`WantNewerVersionPresent()`) and requires an explicit `read_options.snapshot`; when `snapshot == nullptr` or tracking is not requested, reads behave like `GetEntity()` / `MultiGetEntity()` and any engaged `newer_version_present` output stays `false`. Entity reads do not return user-defined timestamps, so `WantTimestamp()` / `WantTimestamps()` returns `Status::NotSupported` on all DB types. With an explicit snapshot and tracking requested, `ReadOptions::read_tier == kPersistedTier`, `DBImplSecondary`, `WritePreparedTxnDB` / `WriteUnpreparedTxnDB` (custom visibility callbacks), and `DB` subclasses that do not override the methods return `Status::NotSupported`, whereas `DBImplReadOnly` and `CompactedDBImpl` return `false` because their in-process views are static. `StackableDB` forwards to the wrapped `DB`.

Tests and limitations: 1,328 Buck tests across `db_wide_basic_test`, `db_basic_test`, `db_secondary_test`, and `write_prepared_transaction_test` passed, all 6 per-guard counterfactual mutations fail at least one targeted test, and `db_stress`, `db_bench`, and `db_test` build cleanly.

Reviewed By: xingbowang

Differential Revision: D125108470

fbshipit-source-id: 1ef367d5696ee6f8217391f64698229f48675d68
2026-10-29 11:29:17 -07:00
Peter Dillinger 7e0c4d8e11 Spread periodic compaction catch-up evenly after a phasing re-anchor (#15505)
Summary:
`periodic_compaction_phase_recovery_percent` (experimental, since 11.10) de-herds periodic compactions across a fleet of DBs sharing their `periodic_compaction_seconds` settings and writing slowly enough, on average, to actually reach it: instead of letting every (DB, column family) come due together, it steers each one toward a stable "preferred phase" within that period. Phasing (re)anchors at DB open and whenever that option or `periodic_compaction_seconds` changes. The "anchor" is the earliest time phasing may pull a compaction to so that enabling phasing cannot stampede a backlog of already-old files all at once.

**The problem.** Enabling the option across a production use case showed that the catch-up right after an anchor was still too bunched up. In one deployment (7-day `periodic_compaction_seconds`, recovery percent 33), enabling phasing in a single region pushed the remote periodic compaction rate to about 5.7x normal in the first hour and kept it 3-4x elevated for about 6 hours. (The intended long-term de-herding did work.)

**Root cause.** Before this change, a file whose preferred-phase target landed before the anchor but whose natural deadline landed after it was given a stable per-(DB, CF) trigger time picked uniformly in [anchor, natural deadline]. Any one such file is spread out fine, but every file's window *starts* at the anchor, so the windows stack up there. Summed over a fleet whose file ages are spread across the period, the trigger density at time t after the anchor grows like ln(n/t) -- about 3.8x the steady rate in the first 1% of a period, and elevated for a large fraction of a period. Within a single DB that is minor, but a fleet-wide option change (e.g. a dynamic SetDBOptions from a config push) lines up every DB's anchor and so turns each DB's small front-load into one fleet-wide burst. The original simulator missed this because no scenario enabled phasing on a fleet whose files already had ages spread across the period: the upgrade scenarios start from a lockstep herd, and the staggered scenario enables phasing at DB open, before any file exists.

**The fix.** Phasing fires a file early by some `early_pull`, at `natural deadline - early_pull`. The old code computed it two ways: a normal file used

    early_pull = q * offset          (q = recovery_percent / 100)

while a near-anchor file (preferred phase before the anchor, deadline after) ignored that and was instead dropped at a uniform point in [anchor, natural deadline] -- the spreading branch that caused the spike. This change deletes the spreading branch and computes every file the same way:

    early_pull = offset * min(q, k * r / n)

with `offset` the gap from the natural deadline back to the preferred phase, `n = periodic_compaction_seconds`, `r = natural deadline - anchor`, and `k = 1/2` (`TriggerTime` carries the full derivation). The `min` is the crux: for a file due well after the anchor (large r) it selects `q`, reproducing the old steady-state pull exactly; for a file due soon after the anchor (small r) it scales the pull down toward 0, so near-anchor files stay close to their own deadlines instead of all jumping toward the anchor. For example, with k = 1/2 a file due only 0.1n after the anchor is pulled by at most 0.05 of its offset (it barely moves), whereas a file due a full period or more out gets the full q pull (for the recommended q <= 1/2). Resulting behavior:

- **Steady state is unchanged.** The full q pull applies whenever r >= q*n/k, which for q <= 1/2 is every file written at or after the anchor.
- **Never before the anchor, never after the natural deadline.**
- **Catch-up density is bounded** at about -ln(1-k)/k = 1.39x the steady rate right after an anchor on a uniform-age fleet, for any q.
- **Herds stay spread.** DBs sharing one deadline get pulls proportional to their own (differing) offsets, so they don't re-collapse. Capping the pull at q*r instead would hand a herd identical pulls and clump much of it onto one instant.

Files already past due at the anchor are unaffected and keep the existing n/4 grid.

`k` is fixed at 1/2 rather than tracking `q`. k = max(q, 1/2) is identical for q <= 1/2 (including the recommended 33), but at q = 100 it gives k = 1, which loses the density bound entirely; k = q was rejected because it noticeably slows the first de-herding cycle of a lockstep fleet. The price of fixing k = 1/2 is that for q > 1/2 the first cycle after an anchor (including DB open) spreads only as fast as q = 1/2.

Phasing now applies only to `periodic_compaction_seconds` up to 2^31 (~68 years), which keeps the scaled-pull arithmetic within 64 bits (asserted statically); longer periods use the classic deadline.

No effect when phasing is disabled (the default). No API or file-format changes.

Pull Request resolved: https://github.com/meta/rocksdb/pull/15505

Test Plan:
New/changed unit tests in db_compaction_test.cc, both of which fail against the previous implementation:
- PeriodicCompactionPhaseTriggerTime: replaced the subcase that asserted the old spread-to-deadline result with hand-computed results near the anchor for recovery percent 10 to 100, including the threshold where the full pull resumes, plus long-period cases: exact results up to the 2^31 limit (including the largest intermediate product) and the classic deadline beyond it. The geometric-convergence subcase is unchanged.
- PeriodicCompactionPhaseTriggerTimeRandomized (new, time-seeded): over random (n, recovery percent, phase, file time, anchor) it checks anchor < trigger <= natural deadline and trigger >= the steady-state trigger, with exactly the steady-state result for files written after the anchor (a full period after, for recovery percent > 50). With many files sharing one anchor and uniform ages, no 1%-of-period bin after the anchor exceeds 1.6x its share (expected 1.39x) for recovery percent 10, 33, 50, 67, 100. A herd sharing one deadline 0.62n after the anchor stays spread over >= 0.3n with no clump.

Simulation (out-of-tree harness, not part of this change): 1000 real RocksDB instances per scenario, `periodic_compaction_seconds` N = 100s, a real periodic-task scheduler, every compaction recorded; the previous code, k = max(q, 1/2), and k = 1/2 run concurrently with identical per-DB phases, plus a replicate of the previous code as a noise reference. New `midlife_dynamic_r*` scenarios model the production case: file ages uniformly spread, phasing enabled via SetDBOptions at 2.4N. Full interactive HTML (plots per scenario plus a side-by-side table; **fb-internal URL sanitization may require copy-pasting the URL manually**):
https://htmlpreview.github.io/?https://github.com/pdillinger/rocksdb/blob/38ba095d48e12b85c9a0ccc397c68a486c07a63a/pc_phase_switchover.html

Periodic compaction load relative to an even rate, in `midlife_dynamic_r*`:

| recovery % | old, first 0.05N | old, 0.1N-0.2N | new, 0.05N-0.4N | |---|---|---|---|
| 33 | 2.8x | 1.2x | ~1.4x |
| 50 | 2.7x | 1.8x | ~1.4x |
| 100 | 3.2x | 2.2x | ~1.4x |

Peak load there (max periodic compactions per DB per N, in N/40 bins) dropped from 3.5 / 3.3 / 3.7 to 2.0 / 1.7 / 1.8 for recovery percent 33 / 50 / 100.

The tradeoffs are all in first-cycle de-herding of a lockstep fleet: peak load rose from 3.3 to 3.8 (`upgrade_restart_r33`), 2.6 to 3.8 (`upgrade_dynamic_r50`), and 3.0 to 3.8 (`upgrade_dynamic_r100`), comparable to the 3.6 to 3.8 of `upgrade_dynamic_r33`; convergence at 50 and 100 finishes somewhat later. Cumulative badness was unchanged within noise, or better, in every scenario. Scenarios the change should not affect (`staggered_phased_r33`, `turnup_r33`, `natural_*`, `burst_r0` through `burst_r50`) matched the old code within run-to-run noise.

Reviewed By: jaykorean

Differential Revision: D128151011

Pulled By: pdillinger

fbshipit-source-id: b1b34d532647cd5e2280b9c613e659efb79da20b
2026-10-29 11:09:22 -07:00
Andrew Chang 898ace2b02 Make remote compaction honor preclude_last_level_data_seconds and preserve_internal_time_seconds
Summary:
X-link: https://github.com/facebook/rocksdb/pull/15457

**Problem:** a remote compaction (`CompactionService` / `DB::OpenAndCompact`) ignores `preclude_last_level_data_seconds` and `preserve_internal_time_seconds`.
- With tiered storage, every remote compaction into the last level writes recent (hot-window) data to the last level, which is the cold tier. A local compaction keeps that data in the proximal level.
- With `preserve_internal_time_seconds`, a remote compaction zeroes sequence numbers that a local compaction keeps, so their write-time information is lost.
- Remote outputs also get no `seqno_to_time_mapping` table property. Later compactions, local ones included, take write times only from their input files, so that data is treated as recent unless other inputs cover it. Files written by remote compactions before this fix stay that way after upgrading.

**Root cause:** the worker opens the primary's DB as a secondary. `SanitizeCfOptions()` treats the secondary as read-only and sets both options to 0 ("ignored in read-only DB"). `DBImplSecondary::CompactWithoutInstallation()` builds the compaction from these sanitized options, so `Compaction::EvaluateProximalLevel()` turns off per-key placement and `CompactionJob::Prepare()` computes no preserve window. The existing `CompactionServiceTest.PrecludeLastLevel` forces per-key placement and the cutoff through sync points, so it did not catch this.

**Fix:**
- `CompactionServiceInput` carries the primary's `preserve_internal_time_seconds` and `preclude_last_level_data_seconds`. The worker sets them on its copy of the CF options before it picks the compaction.
- Restoring the preserve window alone would bring back the kBottommostFiles infinite compaction loop for remote compactions. The worker does not know the compaction reason or the primary's DB-wide seqno->time mapping, so it cannot apply the version-carried preserve boundary that a local bottommost-files compaction applies. `CompactionServiceInput` therefore also carries that boundary (`preserve_time_min_seqno_floor`, set only for kBottommostFiles). The worker takes the max with it, the same as a local compaction does.
- The options are sent in `CompactionServiceInput` rather than re-read from the OPTIONS file (which the worker already loads before sanitizing) so that they always arrive together with the boundary. With the OPTIONS-file approach, a new worker paired with an old primary would get a preserve window but no boundary, which brings back the loop.
- With per-key placement on, the primary and the worker each decide it, and the worker's MANIFEST view can be newer than the primary's input version. The primary now also sends its proximal level, proximal output range type and `keep_in_last_level_through_seqno`. The worker returns `Status::Incomplete` on any mismatch, so the `CompactionService` can fall back to local compaction (the same pattern as the MANIFEST floor check). As a backstop, the primary falls back to local compaction if a result has proximal-level outputs while its own compaction has no proximal level.
- Compatibility: the new fields default to 0, and readers ignore unknown fields. So an old primary or an old worker keeps today's behavior. The fix takes effect when both are upgraded.

Reviewed By: pdillinger

Differential Revision: D126826333

fbshipit-source-id: cb59f0f75601e1cbb1233cb52c173d05dc127b92
2026-10-29 09:00:12 -07:00
Steph Pontikes b6582c84bc Add rolling MultiScan prefetch windows
Summary:
X-link: https://github.com/facebook/rocksdb/pull/15408

Add bounded rolling MultiScan prefetch windows to `IODispatcher`.

Invariants:

- Rolling prefetch is effective only for forward async scans with verified file-offset-ordered handles, a nonzero window that fits every uncached block, and no global dispatcher memory limit; otherwise it disables itself and preserves ordinary iterator and prefetch error semantics.
- Each SST partitions uncached data blocks into ordered, variable windows whose physical I/O span, including coalesced gaps, is at most `max_prefetch_size`; a gap beyond `io_coalesce_threshold` ends the current window early.
- `Prepare` submits the first window, first use of a block in the current window submits the next window, and reaching a queued window waits for that whole window instead of issuing an individual block read while rolling remains enabled.
- Rolling consumption is monotonic: forward jumps discard obsolete queued windows, successful portions of partial dispatches are removed from pending bookkeeping, benign absent or drained pending groups are treated as already handled, and failed speculation falls back without duplicate I/O or a rolling-specific status.

Reviewed By: anand1976

Differential Revision: D121817105

fbshipit-source-id: 1af83d23634b20cc2938a4c5ad6137fa700a0c7b
2026-10-28 19:09:55 -07:00
Steph Pontikes d27327384d Extract I/O prefetch scheduling policy
Summary:
X-link: https://github.com/facebook/rocksdb/pull/15448

- Move the existing global prefetch-memory policy and pending queue into a dedicated scheduler module.
- Make every returned `ReadSet` own a configured scheduler.
- Keep I/O submission, buffers, completion, and block construction in `IODispatcher`.
- Preserve the existing default scheduling and coalescing behavior.

Reviewed By: anand1976, xingbowang

Differential Revision: D126191480

fbshipit-source-id: 515bbe66dafc26908d26e0e7c7c772d952e56f3f
2026-10-28 19:09:55 -07:00
Peter Dillinger 0adcea5ea4 Update for GitHub repository move to meta/rocksdb (#15508)
Summary:
The RocksDB GitHub repository moved from github.com/facebook/rocksdb to github.com/meta/rocksdb. Nearly all GitHub Actions jobs are gated on `github.repository_owner == 'facebook'` (to keep them from running on forks), so after the move those jobs were skipped on new PRs and pushes, and in the nightly and weekly schedules.

This change updates the repository for the new location:
* Fix: All `repository_owner` checks in the workflows now compare against 'meta', so the PR, nightly, weekly, and clang-tidy jobs run again.
* Change: CI container images now come from ghcr.io/meta/rocksdb_ubuntu, and the Dockerfile build/push instructions match.
* Change: Workflow names facebook/rocksdb/* are now meta/rocksdb/*. The Claude review workflow triggers on the pr-jobs workflow by name, so its `workflow_run` trigger is updated to match. (Required status checks are keyed by job name, not workflow name.)
* Change: Every github.com/facebook/rocksdb URL in the repo now uses github.com/meta/rocksdb. This covers URLs used by CI and build scripts (third-party-deps release downloads, including DEPS_URL for Java builds; git remotes in setup-upstream and check_format_compatible.sh), the docs site config (`ghrepo`) and navigation, Maven SCM metadata, and links in code comments, docs, HISTORY.md, and blog posts. Historical links are included because GitHub's redirect from the old URL is not dependable for every link: some old issue URLs return 404 while the same issue under meta/rocksdb works.
* Change: build_tools/format-diff.sh finds the upstream remote by matching either meta/rocksdb.git or facebook/rocksdb.git, because existing clones still use the old remote URL (which GitHub redirects for git operations).
* Remove: The stale CircleCI status badge from README.md. CircleCI is no longer used.
* Add: A final step in nightly's build-format-compatible job that fails if https://github.com/facebook/rocksdb stops redirecting to https://github.com/meta/rocksdb (or the old name is taken by another repo). Existing releases (e.g. DEPS_URL in java/Makefile as of v11.12), existing clones, and external links still depend on that redirect, so this alerts the RocksDB team if it breaks. The step runs even when the compatibility test fails.

Not addressed here: runner labels (16-core-ubuntu, etc.) and secrets are configured per GitHub org, outside the repo. Any that are missing in the meta org need to be set up there.

Pull Request resolved: https://github.com/meta/rocksdb/pull/15508

Test Plan:
CI on this PR exercises the main fix: jobs should run instead of being skipped.

Manual checks from an unrestricted network:
* https://github.com/facebook/rocksdb returns a single 301 to https://github.com/meta/rocksdb, then 200, and `git ls-remote` via the old URL works. Old pull, wiki, blob, and release-download URLs redirect, but old issue URLs for https://github.com/meta/rocksdb/issues/9275, https://github.com/meta/rocksdb/issues/10080, and https://github.com/meta/rocksdb/issues/13293 returned 404 (https://github.com/meta/rocksdb/issues/12038 redirected) even though all four exist under meta/rocksdb.
* A CI log showed ghcr.io/meta/rocksdb_ubuntu:22.1 pulling successfully.

The new nightly step won't run before merge. The exact curl command it uses printed the expected `200 https://github.com/meta/rocksdb` when run manually. Separately, its script was extracted from the parsed workflow YAML and run against a stub curl: it passes for a redirect to meta, fails for a 404 and for a 200 without redirect, and reports a network failure distinctly. After merge, a manual workflow_dispatch of the nightly workflow can confirm it in CI.

Reviewed By: archang19

Differential Revision: D128195050

Pulled By: pdillinger

fbshipit-source-id: 3d42aa271b706eec2ef6c1586643056084fc866f
2026-10-28 17:23:50 -07:00
Andrei Preda 9457606301 Stop pinning iterators over immortal mmap blocks
Summary:
With `ReadOptions::pin_data`, `LevelIterator` pins every table iterator
it leaves and `BlockBasedTableIterator::ResetDataIter()` delegates every
data block it leaves to the `PinnedIteratorsManager`, until the `DBIter`
is destroyed. For an immortal table (read-only with `max_open_files=-1`)
whose uncompressed blocks are read through mmap, the returned slices
point into the mapping, which the reader keeps alive for the iterator's
Version. The pinned iterators and `Block` objects keep nothing alive and
only grow memory with every seek.

Add `InternalIterator::IsSafeToDropWhilePinned()` (default false), true
for a `BlockBasedTableIterator` whose returned slices all point into an
immortal source. `LevelIterator` now deletes such an iterator instead
of pinning it, and `ResetDataIter()` frees such a block instead of
delegating it. Owned and cached blocks are delegated as before.

X-link: https://github.com/facebook/rocksdb/pull/15459

Reviewed By: anand1976

Differential Revision: D127012316

Pulled By: xingbowang

fbshipit-source-id: e29957fb615915abd98a40f98815205bad14c696
2026-10-28 16:27:13 -07:00
Omkar Gawde 302eaa8b1f Share GetMergeOperands finalization across DB modes and reset only filled output slots
Summary:
X-link: https://github.com/facebook/rocksdb/pull/15503

Why the change is required:
After D123382954, the primary, read-only, and secondary `GetMergeOperands` paths still each have their own copy of output setup and post-processing, so they can drift apart again. Read-only and secondary also copy every operand, even when the result is large enough for the primary's zero-copy bundle (the TODO for PR #10458). Separately, xbw pointed out in review that the up-front output reset scales with the caller's capacity rather than the result.

Previous behavior:
- Each mode set up `MergeContext` and published operands with its own code. Only the primary used the shared cleanup bundle, which pins a SuperVersion and the iterator pins for large results. Read-only and secondary always called `PinSelf` on each operand.
- All three modes called `Reset()` on all `expected_max_number_of_operands` output slots before the lookup, even when the caller had already reset them or only a few operands existed. A caller passing a capacity of 1000 to read 3 operands touched 1000 `PinnableSlice` objects on every call.

New behavior:
- All three modes publish operands the same way. Read-only and secondary now return large results pinned through the shared bundle, just as the primary does.
- `GetMergeOperands` resets only the slots it fills, each one just before writing it, so the cost scales with the result instead of the capacity. Slots after `*number_of_operands`, and the whole array on `Incomplete`, `NotFound`, or an error, are left as the caller passed them. Previously they were cleared.

Implementation:
- `DBImpl::PrepareGetMergeOperands` now only forwards `GetMergeOperandsOptions` into `MergeContext`.
- `DBImpl::FinalizeGetMergeOperands` reports the count, checks capacity before writing anything, then resets and fills each slot. Large results move into the existing `GetMergeOperandsState` bundle, with a new SuperVersion reference and the moved `PinnedIteratorsManager`; small results are copied. The per-slot `Reset()` is still required because a reused slot can hold an earlier pin, and `PinSlice`/`PinSelf` assert the slot is unpinned.
- All three `GetImpl` variants call the shared pair. Callers must invoke finalization while their SuperVersion reference and pins are still held; secondary releases its reference afterwards, as in D123382954. The primary keeps the `OkMergeOperandThresholdExceeded` status from `merge_operand_count_threshold` unless finalization fails.

Safety, compatibility, and limitations:
The reset change is visible through the public API in every mode, including the primary. It still satisfies the documented contract: `db.h` says `merge_operands` is not populated on `Incomplete` and that callers own `Reset()` of the entries. A caller that inspected slots after a failed call, or past `*number_of_operands`, will now see its own earlier contents instead of empty slices. A pin left in a slot this call doesn't touch stays alive until the caller resets it. This diff adds a behavior-change release note. Pinned results from read-only and secondary carry the same requirement as the primary's: the DB must outlive them. There is no ABI or file-format change, and snapshot acquisition and the memtable/SST lookups are unchanged.

Reviewed By: jaykorean

Differential Revision: D123475366

fbshipit-source-id: 615e09ce0658c2ef86235810ff59dbfc3b54c289
2026-10-28 11:47:00 -07:00
Omkar Gawde a8a626ba8b Enforce GetMergeOperands contract and operand lifetime in secondary modes (#15332)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15332

Why the change is required:
Read-only and secondary DBs use their own `GetImpl` paths, which had drifted from the primary `GetMergeOperands` contract. A caller passing an output array sized by `expected_max_number_of_operands` could get writes past the end of that array. Separately, both paths could copy operands out of memory they had already released.

Previous behavior:
- Both modes pinned every accumulated operand without checking `expected_max_number_of_operands`. More operands than capacity meant writes beyond the caller's array instead of `Incomplete`.
- Output slots were not cleared first, so a reused array could expose stale values.
- `MergeContext` never received `GetMergeOperandsOptions`, so `continue_cb` was ignored.
- Both modes destroyed the SST lookup's `PinnedIteratorsManager` before copying operands into the caller's slices. SST operands borrow block-cache memory that is freed once unpinned under cache pressure, so the copy in `PinSelf` could read freed memory.
- Secondary only: the SuperVersion reference was also released before the copy. Memtable operands are borrowed slices into the memtable, and a `TryCatchUpWithPrimary` that completes in between can retire that memtable, with the same heap use-after-free.

New behavior:
Both modes now follow the primary contract. They clear every advertised output slot, report the required operand count, and return `Incomplete` with `KMergeOperandsInsufficientCapacity` before writing anything to an undersized array. `continue_cb` stops collection and returns the collected newest suffix. Both modes copy operands while the block pins are still held, and secondary also holds its SuperVersion until the copy is done.

Implementation:
- Reset the advertised output range up front, forward the options into `MergeContext`, and check capacity before the copy loop. Both match `DBImpl::GetImpl`: the reset is skipped when no options are passed, while the capacity check dereferences them unconditionally.
- Both modes: hoist `PinnedIteratorsManager` to function scope, as `DBImpl::GetImpl` does. Secondary also moves the SuperVersion release after the copy.

Safety, compatibility, and limitations:
Only `GetMergeOperands` results change in read-only and secondary modes, and the primary path is untouched. For `Get` and `GetEntity`, the only change is that the block pins, and on secondary the SuperVersion, are released a few statements later. Read-only needs no SuperVersion change because it never installs a new SuperVersion. There is no API, ABI, or file-format impact. Callers that relied on the old behavior were writing past their declared capacity. Large-result zero-copy pinning for these modes is still a TODO (PR #10458) and is handled in the follow-up diff.

Reviewed By: jaykorean

Differential Revision: D123382954

fbshipit-source-id: 7b0c49b5e96f464a3b8c974940713261a9c0e15e
2026-10-28 11:09:41 -07:00
Josh Kang 4c38c45ac1 Avoid redundant executor scheduling in CoroDB::CoGet (#15488)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15488

`CoroDB::CoGet` queues its child read task onto the selected read EventBase even when the caller already uses that executor. The DB and `SstFileReader` overloads now await the child directly when `co_current_executor` matches that EventBase, avoiding redundant queues on task entry and return. Calls from another executor keep the existing dispatch.

This applies to reads that perform I/O as well as cached reads. Asynchronous I/O retains its existing suspension and resumption behavior on the read executor; cancellation and statistics handling are preserved.

Reviewed By: pdillinger

Differential Revision: D127665852

fbshipit-source-id: 73d969d0abeefc68cb5b34b1164f3a2da943516e
2026-10-27 17:34:39 -07:00
Jay Huh 5d9da10d28 Fix ASSERT_STATUS_CHECKED and MSVC build failures from BlobDB remote compaction (#15498)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15498

Why: "Support BlobDB in remote compaction" (#15496) broke four GitHub CI jobs on `main`: `build-linux-static_lib-alt_namespace-status_checked` and the three `build-windows-msvc` jobs. The `main` run just before it passed all four; every run that includes it, on `main` and on unrelated PRs (including reruns), fails them.

Previous behavior:
- `ASSERT_STATUS_CHECKED` builds abort with "Failed to check Status" in `compaction_job_test` and `compaction_service_test`:
  - `CompactionOutputs::AddToOutput()` gained an early `return c_iter.status();` after `PrepareOutputIfNeeded()`. It leaves the local `s` unchecked when nothing assigned it earlier in the call. This is production code; `ResumableCompactionServiceTest.BlobCompactionResumeRangeExhaustionFallsBackLocally` reaches it.
  - New tests never check `CompactionServiceResult::status`: `CompactionJobTest.ResultSerialization` (`deserialized_old_result`), `CompactionServiceTest.RemoteBlobWorkerDisablesBlobOutputSafely`, a sync point callback in a blob-file rollback test, and every test that reads all results through `MyTestCompactionService::GetResults()` but checks only the one it looks for.
- MSVC builds with `/W4 /WX`. Two new lines in `compaction_service_test.cc` pass `'a' + i` (an unbounded `int`) to the `char` parameter of `std::string(size_t, char)`. That narrowing is a warning MSVC reports and `/WX` turns into an error. The GitHub logs are not readable without repo admin rights, so this cause is inferred, not read from the log: clang's `-Wimplicit-int-conversion` flags exactly these two lines and nothing else the commit added, and the existing `'a' + (i % 26)` form, which compilers can prove fits in a `char`, already builds on Windows in `compact_files_test`.

New behavior:
- `AddToOutput()` assigns `c_iter.status()` to `s` and returns `s`, like its other exits. The returned status is unchanged.
- The tests check or explicitly permit each result status. `GetResults()` permits every result's status, the way `GetResult()` and `GetInput()` already permit their read status; tests that care still assert on the result they select.
- The two conversions use `static_cast<char>`; `i < 20`, so the values are unchanged.

No format, API, or behavior change.

___

landed-with-radar-review

Differential Revision: D127938230

fbshipit-source-id: 0c5b05e28b07898067a2038e9376225940e910ed
2026-10-27 17:12:11 -07:00
Bikash Chandra 83747eb79f Support BlobDB in remote compaction (#15496)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15496

Remote compaction previously transferred only SST inputs and output metadata. A worker could not reliably read blob-backed values for a compaction filter, output SST metadata did not preserve references to existing blob files, and blob garbage collection could derive a different cutoff from the worker view.

This change adds BlobDB support to the existing remote-compaction path. Existing `enable_blob_files` and `blob_file_starting_level` settings determine whether eligible output may create blobs; compactions whose input SSTs reference blobs transport those dependencies regardless of new-blob eligibility.

Each subcompaction transports the live blob-file suffix referenced by overlapping input SSTs. For blob-eligible output, the primary atomically reserves a half-open blob-file-number range. The worker uses native `CompactionJob`, `BlobFileBuilder`, and `BlobGarbageMeter` machinery and returns `BlobFileAddition`, `BlobFileGarbage`, and SST dependency metadata. When blob GC is enabled, the primary computes the cutoff from its authoritative input Version and serializes it in `CompactionServiceInput`; the worker consumes that cutoff rather than recomputing from its local view.

Workers advertise result-side BlobDB and GC-cutoff protocol markers. The primary declines old or non-capable results before installation. It validates reserved additions, transported-input garbage, output dependencies, cross-subcompaction aggregate garbage, staged file existence, optional content checksums, and each staged blob footer count plus the overflow-safe header+record-bytes+footer physical size. Metadata or structural corruption declines the result and reruns locally, while genuine validation I/O errors remain `kFailure`. Blob files move before SSTs and register through `BlobFileCompletionCallback`; rollback physically deletes each installed blob before removing its `SstFileManager` entry, and a cleanup error remains a failure with the tracking entry preserved.

Resumable workers checkpoint blob additions, cumulative garbage, and the next reserved number. Checkpoints cut SST and blob outputs before preparing the next key. `CompactionIterator` records when processing has already consumed an earlier input entry of the current user key, and progress is not checkpointed at that unsafe boundary. Resume therefore does not seek back over already-counted input, fixing both the baseline record-count mismatch and blob-garbage double counting. At safe boundaries, the raw pre-filter inflow for the current entry is subtracted from the blob-garbage checkpoint; per-entry inflow metering exists only for resumable jobs.

`REMOTE_COMPACT_INPUT_BLOB_FILE_COUNT` counts each distinct transported blob file once per compaction job. `REMOTE_COMPACT_BLOB_RESULT_DECLINED_COUNT` counts primary-side blob-result declines only; service-side `kUseLocal` and pre-dispatch reservation fallback do not increment it. Both tickers are exposed through C++ only; JNI and Java bindings are intentionally omitted because the Java ticker jbyte space is exhausted.

Compatibility and failure behavior:
New fields deserialize with safe defaults. A new primary declines an old worker when blob inputs, blob-eligible output, or a primary-provided GC cutoff requires the new protocol. A new worker receiving an old input without the cutoff field disables blob GC for that job, avoiding a worker-local cutoff. Primary-side declines rerun locally, so `CompactRange` and `CompactFiles` succeed without a background error. The `CompactionService` contract is unchanged: a service returning `kFailure` preserves the worker status, including `Status::Incomplete`; mapping a worker decline to `kUseLocal` remains the integrator's choice. ZippyDB production additionally maps service failures to local fallback through `HandleFailure(FALLBACK_LOCAL)`.

Reviewed By: jaykorean

Differential Revision: D122097088

fbshipit-source-id: 7790768fe8a25dd2451a5d69d809d356899cabab
2026-10-27 13:11:50 -07:00
cm-wang 6d03b3df45 Match blob direct write generations to the memtables a flush picked (#15481)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15481

With blob direct write, each memtable switch moves the column family's open blob files into a new generation, queued in `BlobFilePartitionManager::pending_generations_`. A flush then seals and publishes the generations of the memtables it flushes.

`PrepareFlushAdditions(n)` always used the first `n` queued generations, and `CommitPreparedGenerations(n)` always popped the first `n`. That is only right if flushes pick, and finish, the oldest memtables in order. With more than one flush running for a column family (`max_background_flushes` > 1, or `max_background_jobs` >= 8), a second flush skips the memtable the first one is still flushing and picks a newer one, but prepares the oldest generation, which belongs to the first flush. Both flush edits then add the same blob file, and installing them fails with `Corruption: VersionBuilder: Blob file #N already added`, which stops the database. The newer memtable's own blob files are never published, and committing the second flush pops a generation it did not own. Atomic flush prepares before it waits to install in order, so it has the same problem.

Removing generations by the flush's own memtables is not enough either. A newer flush that finishes first only marks its memtable completed: the older flush installs both, or rolls the newer one back too if it fails. A generation removed by the newer flush is then gone when the retried flush looks for it (`Corruption: Missing blob direct write generation metadata for flush`).

Fix:
- Tag each generation with the ID of the memtable whose switch created it (`RotateCurrentGeneration(memtable_id)`). `PrepareFlushAdditions()` takes the IDs of the memtables the flush picked and seals exactly those generations, in both flush paths.
- Remove generations only once installed: after a successful flush, `CommitFlushedGenerations()` drops the generations of the memtables older than the earliest one still in the immutable list (memtables leave it only by installation, in order), and stops tracking their sealed files as manager-owned.

Reviewed By: xingbowang, archang19

Differential Revision: D126852567

fbshipit-source-id: 910bf466674cf8bc89bc3ccbbcf08c684968ad72
2026-10-27 12:49:41 -07:00
cm-andrew f0434b6972 Let async reads record get_cpu_nanos without the CPU-time perf level (#15489)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15489

**Adds `SetAsyncReadCPUTimeEnabled()`, so an async read can report the CPU it used without turning on every perf timer.**

- **Problem:** a coroutine read runs on the read executor, not the caller's thread. RocksDB sums its CPU in `PerfContext::get_cpu_nanos`, but only at `kEnableTimeAndCPUTimeExceptForMutex`, which also turns on every wall-time timer.
- **Change:** with the new opt-in set and the perf level at least `kEnableCount`, an async read times only its running slices into `get_cpu_nanos`. The setting is thread-local, like the other async-read stats settings, so there are no option or C API changes. It resets after each read.
- **Unchanged:** reads without the opt-in, sync reads, and async reads that run synchronously on the caller's thread.

Reviewed By: archang19, joshkang97

Differential Revision: D127495339

fbshipit-source-id: 1b0ab016552f216409a1a2c99ed1733269e5dbc3
2026-10-27 08:34:10 -07:00
Jay Huh 0c79400bf7 Add range-based DB split and merge utilities (#15240)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15240

Why:
Shard splits and merges currently require scanning ranges and rewriting them through the write path. These utilities transfer selected half-open `[begin_key, end_key)` ranges through checkpoints, clipping compactions, and SST ingestion. Boundary-only clipping is deferred; kept keys are still rewritten by compaction.

Behavior:
- `SplitDB` builds and verifies a staged destination, rewrites its OPTIONS file with the source CF options without reopening it, and publishes it by rename and directory fsync. The destination is not opened with the source options; the caller's first open validates it, with sync or async file opening as the caller chooses. Source data remains available. After switching traffic, callers run retryable `DeleteSplitRanges`; combining those steps would leave a failure ambiguous after source deletion.
- `MergeDB` clips a source checkpoint and ingests the latest range contents into empty destination ranges in one crash-atomic multi-CF MANIFEST update. It transfers source sequence numbers rather than snapshot history.
- `db_split_merge` provides `split`, `delete-ranges`, and `merge` commands with hex-encoded bounds and strict OPTIONS loading.

Safety and compatibility:
- Callers must stop writes to affected ranges, reserve the temporary paths, and avoid destination snapshots during merge. Existing destination snapshots are rejected. Merge visibility across CFs is not atomic for concurrent readers.
- Clipping disables source filters, callbacks, and the WAL filter; source flushes and subsequent source deletion retain their normal filter behavior. Checkpoint opens use strict recovery, and failed splits remove partial outputs.
- Support is limited to plain writable DBs with compatible BlockBasedTable CFs, leveled/universal compaction, and colocated SSTs. Transaction/TTL/blob wrappers, timestamps, ingest-behind, and other incompatible options are rejected or documented as unsupported in `db_split_merge.h`. Merge requires the same FileSystem object and rejects destination tombstone conversion; old iterators created with conversion enabled must first be retired.

Review fixes:
Live blob detection now reads `kNumBlobFiles` without copying SST metadata. `ClipColumnFamily` now checks surviving file presence instead of treating empty boundary strings as an empty CF: previously, a lone empty-string key in L0 caused split/merge of an excluding range to fail verification. Depends on D123306376 for the separate begin-boundary fix.

Validation:
See the test plan for regression failures without the fixes, sanitizer runs, stress repetitions, full Make checks, and platform/tooling limitations.

Reviewed By: xingbowang

Differential Revision: D120757697

fbshipit-source-id: df04ea49100eb102ba04321fc23fdca48fc379e5
2026-10-26 17:58:13 -07:00
Anand Ananthabhotla 935cdad34d Bypass permanently inapplicable trie-index modes (#15484)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15484

Four `TrieIndexDBTest` mode combinations are permanently inapplicable: `MigrationFullPath`, `RollbackFromPrimaryWithoutCompactSucceeds`, and `EstimatedSizeNonZero` in `kCustomOnly`, and `PrefetchWithCustomIndexWrapper` in `kStandardDefault`. Using `ROCKSDB_GTEST_SKIP` for these intentional omissions generated eight GTest/TSan skipping issues in T294331850.

Change exactly those four calls to `ROCKSDB_GTEST_BYPASS`, matching the permanent-parameter-omission contract documented in `test_util/testharness.h` and already used elsewhere in this test file. Keep the original guards, `return`s, fixture, names, and all 253 registrations unchanged. BYPASS prints its reason and records GTest success instead of a skip; these inapplicable combinations still do not execute the test assertions. All eight applicable cases and every other byte of the test file are preserved.

No derived fixtures or suite splits remain. This changes test reporting only, not database behavior, API/ABI, persisted formats, or performance. Unrelated capability-dependent skips and existing bypasses are untouched. TestX recovery must be audited after landing; passing bypass cases are not evidence of additional functional coverage. Public GitHub export remains a separate prerequisite and is not authorized by this revision request.

___

`Trail`: https://trail.nest.x2p.facebook.net/c/da4fc690-13ac-410e-b1ee-faf2b010e677

___

landed-with-radar-review

Differential Revision: D127646540

fbshipit-source-id: 7159e20c88e358c68b1873008ff4134ceae29162
2026-10-26 14:02:41 -07:00
cm-rockee c631d45bb6 Notify listeners of FIFO temperature-change trivial copies (#15474)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15474

**Why:** With `CompactionOptionsFIFO::allow_trivial_copy_when_change_temperature`, a temperature change copies an SST byte for byte to a new file number, but nothing reported the new file. There was no `OnTableFileCreationStarted()` or `OnTableFileCreated()` call, no `table_file_creation` event in the LOG, and `CompactionJobInfo::table_properties` had no entry for the output. Anything that tracks SST files through listener events misses these files. Two `TODO (mikechuang)` comments in `DBImpl::BackgroundCompaction()` left this undone because reporting the input's properties "will break some existing stress tests".

**The stress test problem:** D73295404's history shows the attempts. It first reported empty `TableProperties`, then partly synthetic ones, then the input's exact properties with the reason `kCompaction`, and finally removed the calls.
- Empty or synthetic properties fail the `data_size`/`raw_key_size`/`num_entries > 0` asserts in `DbStressListener::OnTableFileCreated()`.
- The exact properties are correct, but a copy keeps its input's SST unique ID. The ID is derived from `db_id`, `db_session_id` and `orig_file_number` in the copied properties, and the copy's MANIFEST entry already reuses `in_file->unique_id`. `DbStressListener` asserts that every created file has a never-seen unique ID, so it aborts with "Duplicate partial unique ID found". With this diff's notifications but without the stress-listener change below, a FIFO tiered db_stress run aborts that way within seconds.
- Two more constraints apply. Every `OnTableFileCreationStarted()` needs a matching `OnTableFileCreated()`, including on failure, because `DbStressListener`'s destructor asserts that no creations are pending. And listeners must not be called with the DB mutex held, which this code path holds.

**New behavior:** For each file a trivial copy creates:
- `OnTableFileCreationStarted()` before the file is created, with reason `kCompaction`.
- `OnTableFileCreated()` and the `table_file_creation` LOG event once the copy is written, before the version edit is committed, the same timing as other compaction outputs. They carry the input file's table properties unchanged. Those properties exactly describe the copied bytes, including `orig_file_number` and therefore the unique ID. If the copy fails, `OnTableFileCreated()` gets the failure status with empty properties, as `CompactionJob` reports a failed output open.
- `CompactionJobInfo::table_properties` (in `OnCompactionPreCommit()`/`OnCompactionCompleted()`) includes the output file with those properties, as for other compaction outputs.

**Implementation:**
- In the trivial copy branch, the input's properties come from `Compaction::GetOrInitInputTableProperties()`, which `NotifyOnCompactionBegin()` has usually already loaded. The branch also records them with `Compaction::SetOutputTableProperties()`.
- The DB mutex is released while loading the properties and around each listener/event-log call, as `NotifyOnCompactionBegin()`/`NotifyOnCompactionPreCommit()` already do in this branch. The new file number is covered by the pending outputs captured in `BackgroundCallCompaction()`, and the input file by `being_compacted`.
- `DbStressListener::OnTableFileCreated()` checks for a new unique ID only when `orig_file_number` equals the file's own number. A file whose properties name another original file number is a copy, and its ID was checked when the original was created.

**Safety, compatibility, limitations:**
- No public API, option or file format change. Only FIFO compaction with `file_temperature_age_thresholds` and `allow_trivial_copy_when_change_temperature = true` is affected.
- Listener-visible change: these copies now produce file creation events, and their properties repeat the original file's unique ID. A listener that assumes every created SST has a new unique ID (as `DbStressListener` did) needs the same adjustment.
- If the input's properties cannot be loaded, the copy proceeds and is reported with empty properties.
- Unchanged and out of scope: the copy itself still runs with the DB mutex held.

Requested by: Peter Dillinger (peterd)

Reviewed By: pdillinger

Differential Revision: D127283628

fbshipit-source-id: 418051286e3527583874839afbd2c309b06d4096
2026-10-26 11:30:46 -07:00
Richard Barnes ff044b05ae Fix -Wunreachable-code-break findings in fbcode/internal_repo_rocksdb (#15480)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15480

D127336061 enables `-Wunreachable-code-break` through the PACKAGE files of `fbcode/internal_audit_infrastructure`, `fbcode/internal_chub`, `fbcode/internal_repo_rocksdb`, `fbcode/internal_tool_authorization_service`, `fbcode/internal_tree_sitter_tests`. This diff fixes the code that warning flags there, so the PACKAGE change builds cleanly on top of it.

Fixes the new warning in `fbcode/internal_repo_rocksdb/repo/db/c_test.c`: in the `CheckPutCF`, `CheckDelCF` and `CheckMergeCF` callbacks, the `default:` case is `CheckCondition(false);`, whose macro expansion always calls `abort()`, so the `break;` after it was unreachable and is removed. Test-only change; behavior is unchanged.

Reviewed By: dtolnay

Differential Revision: D127368328

fbshipit-source-id: c46ef678e13545b032ba4e5935c7864dc3e95c71
2026-10-26 09:54:17 -07:00
Xingbo Wang a5c1c345b2 Stress column family lifecycle recovery (#15434)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15434

Why the change is required: A filesystem write failure during column family creation can persist only one side of the MANIFEST and OPTIONS metadata update. Existing crash profiles did not repeatedly change column family topology or deliberately fault both metadata files, so this inconsistency could escape stress coverage.

Previous behavior: db_crashtest could randomize OPTIONS tracking across independent runs, but it had no column family lifecycle profile, no targeted OPTIONS or MANIFEST write faults during CreateColumnFamily, and no coverage gate proving repeated drop/create recovery. Unsafe verification and configuration combinations could also turn retained database contents into false positives after restart.

New behavior: --cf_lifecycle runs a four-phase blackbox schedule: an OPTIONS write fault with tracking enabled, clean recovery with tracking disabled, a MANIFEST write fault with tracking enabled, and clean repeated column family recreation with tracking enabled. Each writable open checks exact persisted OPTIONS column family membership against the MANIFEST view. The lifecycle wrapper requires observed OPTIONS and MANIFEST fault exits plus at least two successful lifecycle operations before final verification.

Implementation: add lifecycle flags and validation to db_stress, target only kWrite operations for the selected metadata file type, exit immediately with a dedicated status after an exact target marker, and preserve ordinary non-lifecycle column family replacement ordering. The Python driver owns lifecycle activation and appends authoritative phase, recovery-state, and sanitizer lock-bound flags after flagfile or forwarded overrides. It rejects arguments and modes that could bypass the schedule or invalidate the recovery oracle. Lifecycle-only completion checks enforce successful final verification; ordinary blackbox runs retain their historical final-verifier timeout handling and a single stabilization delay.

Safety and unsupported cases: this is limited to a local blackbox stress profile with two column families, one foreground thread, one database, and no persistent expected-state directory. It rejects remote filesystems, remote compaction, transactions, BlobDB and dynamic blob options, TTL, secondary/read-only modes, MANIFEST reuse, reopen, unrelated fault injection, and verification modes that can report retained keys as mismatches. Fault coverage is intentionally limited to selected OPTIONS and MANIFEST kWrite operations; it is not a general filesystem-fault API. Strict fault-observation, coverage, and final-verification exit checks are limited to --cf_lifecycle; ordinary blackbox behavior is unchanged.

Validation: focused Python tests cover scheduling, authoritative arguments including recovery-state and lock-bound precedence, exact marker counting, repeated-success requirements, and lifecycle-only final-verification failure handling. Normal, ASAN, and TSAN builds exercised both targeted metadata failures, the tracking transition, repeated lifecycle changes, and final recovery. The exact ordinary mini-crash CI profile also passes locally. Stress timing remains probabilistic, and local make check-sources is unavailable in a Sapling checkout; CI covers the Git-only source check.

Reviewed By: rban1

Differential Revision: D125875013

fbshipit-source-id: 895f1fb6ab66cddc5b4ad8ac1c3e017c3a59dd2d
2026-10-26 09:52:21 -07:00
Jay Huh ea31360536 Optionally record ReadOptions and WriteOptions in query traces (#15460)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15460

Why: a query trace keeps only each call's arguments (the write batch, keys, iterator bounds), and replay runs every call with default ReadOptions and WriteOptions. A workload that writes with `disableWAL` or reads with `fill_cache=false` therefore replays differently from how it ran: WAL writes and block cache fills that never happened live. That matters to anyone who evaluates option changes by replaying a trace, such as auto-tuning (D123483160).

New behavior:
- Add `TraceOptions::include_read_write_options` (default false). When set, every Write, Get, MultiGet, Seek and SeekForPrev record also holds the options of its call:
  - WriteOptions: `sync`, `disableWAL`, `no_slowdown`, `rate_limiter_priority`;
  - ReadOptions: `verify_checksums`, `fill_cache`, `ignore_range_deletions`, `async_io`, `optimize_multiget_for_io`, `tailing`, `total_order_seek`, `auto_prefix_mode`, `prefix_same_as_start`, `pin_data`, `adaptive_readahead`, `background_purge_on_iterator_cleanup`, `auto_readahead_size`, `allow_unprepared_value`, `read_tier`, `rate_limiter_priority`, `readahead_size`, `max_skippable_internal_keys`, `value_size_soft_limit`.
- Add `WriteQueryTraceRecord::GetWriteOptions()` and `GetReadOptions()` on the Get, MultiGet and iterator records (nullptr when not recorded). The default replayer uses them instead of default options.
- With recorded options, a replayed call can fail the way the live call could. The replayer reports such a status in the record's result, as it does NotFound, and goes on: for writes, `Incomplete` with `no_slowdown` and `InvalidArgument` with `sync` and `disableWAL` (rejected after tracing); for reads, `Incomplete` when `read_tier` is not `kReadAllTier`, `Aborted` with `value_size_soft_limit`, `NotSupported` with `kPersistedTier`, and for seeks also `Incomplete` with `max_skippable_internal_keys` and `NotSupported` with `tailing` (a tailing iterator fails `SeekForPrev()` and seeks over range tombstones). Every other failure (e.g. a column family or an option the replay DB lacks, or a replay DB that cannot create tailing iterators at all, such as a secondary) still stops the replay, and records without recorded options are handled as before.
- A captured seek result holds the value: with a recorded `allow_unprepared_value`, the replayer calls `PrepareValue()` before copying it (a blob load error becomes the seek's status). Without a result callback the value stays unprepared, as in the live call.
- Fix: `Replay()` returned OK when a record's execution returned `Incomplete`, taking it for the end of the trace, and the rest of the trace was not replayed. Only the trace reader's `Incomplete` now means trace end.
- C API: `rocksdb_trace_options_{set,get}_include_read_write_options`.

Trace format (version-2 query records; fields are encoded in payload_map bit order):
```
bit 0..10   existing fields                         unchanged
bit 11      kWriteOptions         \
bit 12      kGetReadOptions        |  only with include_read_write_options:
bit 13      kIterReadOptions       |  one length-prefixed blob, varint32 flags
bit 14      kMultiGetReadOptions  /   then varint scalars; later fields append
```

Implementation:
- `TracedWriteOptions` / `TracedReadOptions` (trace_replay.h) hold the recorded fields and encode/decode the blob; the decoder ignores bytes it does not know at the blob's end, and rejects out-of-range enums.
- `DBIter` keeps a `TracedReadOptions` built from its ReadOptions for the seeks it traces. Grouped writers' options come from `WriteThread::Writer`. `IteratorSeek` and `IteratorSeekForPrev` share one implementation.

Compatibility and limitations:
- Off by default: traces are byte-for-byte unchanged and every existing reader keeps working.
- With it on, the new fields come after all existing ones. Older decoders iterate the set bits; in release builds they skip unknown bits, in debug builds they assert. This version reads traces written without the flag as before (the accessors return nullptr).
- Not recorded: `low_pri` (not kept on grouped writers), `ignore_missing_column_families`, snapshots, timestamps, deadlines and `io_timeout`, callbacks and other pointers. Iterator `Next()`/`Prev()` are still not traced.
- The generated C bindings were added by hand, mirroring `preserve_write_order` (no clang++ on the devserver to run `regen_all.py`); CI's generated-code check confirms them.

Reviewed By: xingbowang

Differential Revision: D126954597

fbshipit-source-id: 50e128fcde6cb9de6e5cf7dd1e8d71bc33c86134
2026-10-23 16:01:05 -07:00
Anand Ananthabhotla e4cacf667f Fix false MANIFEST-growth failures in db_stress reopen verification (#15470)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15470

**Why the change is required:** `rocksdb-contrun-fbcode_crash_test_with_tiered_storage` (Sandcastle workflow 3256102530611462228) failed with `FATAL: MANIFEST file grew by 146 bytes after reopen despite ALL conditions for complete avoidance being met`, followed by the known secondary `assert(property_info != nullptr)` in `DBImpl::DumpStats` from the stats timer racing `exit(1)`. The engine did not violate the `optimize_manifest_for_recovery` + `reuse_manifest_on_open` guarantee; the db_stress check measured the wrong thing.

**Previous behavior:** `StressTest::Reopen()` records the MANIFEST size, calls `Open()`, then runs `VerifyManifestNotRewritten()`. `DB::Open()` ends by scheduling flush/compaction, so any compaction that is already eligible at open (periodic compaction, compaction TTL, files marked for compaction, universal size amplification) can `LogAndApply` before the size check. In the CI failure, `DB::Open()` returned at 20:15:57.963622, a delete-triggered universal compaction on CF "3" was picked 29us later and installed around 57.988, and decoding the MANIFEST bytes appended after the pre-reopen size with `ldb manifest_dump` shows exactly that one compaction edit (delete 0/350, 0/377, 5/340; add 5/385). Recovery itself wrote nothing. This race has existed since the check was added (5baa65680fdd). Separately, STRICT mode did not account for `enforce_write_buffer_manager_during_recovery`: with a WriteBufferManager limit (here `db_write_buffer_size=1048576` across 10 CFs), WAL replay can legitimately force recovery flushes even with `avoid_flush_during_recovery=1`, which STRICT mode also reported as a fatal engine bug.

**New behavior:** MANIFEST verification on reopen only measures writes made by `DB::Open()` itself. STRICT (fatal) mode is used only when no recovery flush is possible; when a WriteBufferManager limit is enforced during recovery, the check runs in NO_WRITE mode and prints a warning instead of exiting.

**Implementation:**
- Fix: when a reopen will be verified and auto compactions are not already disabled, `Open()` opens with `disable_auto_compactions=true`; `Reopen()` re-enables them with `EnableAutoCompaction()` and restores `options_` right after `VerifyManifestNotRewritten()`. This follows the existing `preserve_unverified_changes` pattern.
- Fix: `RecordManifestStateBeforeReopen()` treats `enforce_write_buffer_manager_during_recovery` with `db_write_buffer_size > 0` or a configured `write_buffer_manager` as "MANIFEST writes possible", and the STRICT-mode comment lists the new condition.

**Safety and limitations:** db_stress-only change; no engine, API, or format change. Compactions are held off only between the start of a verified reopen and the check, then resume immediately; when verification is off or auto compactions are already disabled, nothing changes. The `exit(1)`-time shutdown race that turns the FATAL into a `DumpStats` assert or hang is pre-existing and not addressed here.

Reviewed By: xingbowang

Differential Revision: D127204400

fbshipit-source-id: 0542597b8b6164945c70b3b8d8ddc3e0ee3206a0
2026-10-23 12:40:15 -07:00
Anand Ananthabhotla 58d929bc85 Update RocksDB main for 11.13 release (#15471)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15471

Why: keep internal RocksDB main ready for 11.14.0 development while the 11.13 release is cut.
Previous behavior: Conveyor opened a GitHub pull request for this update, which could be missed and was not stacked with the internal release diff.
New behavior: Conveyor owns an internal Phabricator diff that the release-import diff stacks on top of. The release manager exports this diff manually with ExportIt.

Implementation:
- Bump version.h from 11.13.0 to 11.14.0
- Add `11.13.fb` to `check_format_compatible.sh`
- Sync HISTORY.md `## 11.13.` section from `11.13.fb`
- Delete 10 consumed `unreleased_history/` note file(s) from main

Safety and unsupported cases:
- Only version.h, check_format_compatible.sh, HISTORY.md, and consumed release-note files are allowed.
- Open legacy GitHub update-main PRs block this flow; this automation never modifies, exports, or lands them.
- Concurrent direct CLI invocations are unsupported; Conveyor serializes the Release Cut node to one active run.
- The tool refuses unrelated commits, mismatched release metadata, and non-canonical GitHub repositories.

Known limitations:
- ExportIt remains manual. Bot create permission for internal_repo_rocksdb/repo is a rollout prerequisite.

Reviewed By: omkarhgawde

Differential Revision: D126887226

fbshipit-source-id: 60e5f13913912a5582199183ca29e0150e431665
2026-10-23 11:52:54 -07:00
Xingbo Wang e38f68d6ce Shard the snapshot list so snapshots avoid the DB mutex (#15402)
Summary:
Move primary/read-only snapshot creation and all snapshot release operations
from the DB mutex to a bounded set of snapshot-list shard locks. Keep exact
DB-wide snapshot maintenance on existing DB-mutex-protected paths, and retain
the global column-family threshold optimization so most maintenance scans do
not walk column families. Maintenance aggregates shard-local release counts so
ordinary writes on an empty snapshot list cannot masquerade as snapshot
release progress and spuriously schedule compaction.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/15402

Motivation and previous behavior:

`DB::GetSnapshot()` and `DB::ReleaseSnapshot()` previously serialized on the
DB mutex. Applications that use an explicit snapshot as a consistency token
across several reads therefore contended with flush, compaction, MANIFEST, and
other DB-wide work. This contention materially increased CPU cost and reduced
throughput at high concurrency.

New behavior and implementation:

- `ShardedSnapshotList` owns 16 cache-aligned shards. Snapshot creation selects
  a shard from the current CPU (or a per-thread relaxed round-robin fallback),
  and each snapshot records its shard so release remains correct after thread
  or coroutine migration.
- Primary and read-only snapshot creation uses one shard lock. Secondary and
  follower creation retains the DB mutex because catch-up can publish a new
  sequence before installing its matching SuperVersion; their clock read and
  allocation occur before that lock, while sequence/view capture and shard
  insertion occur under it. External-file ingestion can also gate creation and
  retry under the DB mutex to preserve its existing serialization contract.
  Atomic multi-CF ingestion checks gated shards one at a time and defers its
  exact scan until its arbitrary-size per-CF lock set has been released.
- Release removes the snapshot and increments a shard-local release count under
  its existing shard lock. It does not acquire the DB mutex, inspect column
  families, schedule work, perform I/O, or add an atomic read-modify-write.
- Existing DB-mutex-protected paths lock all shards in index order when they
  need an exact oldest snapshot and aggregate the per-shard release counts.
  They advance snapshot-dependent compaction state only after an actual
  release, so ordinary writes on an empty snapshot list do not spuriously
  schedule cleanup. If the oldest sequence then crosses a DB-wide compaction
  threshold, they reconcile affected column families and schedule newly
  eligible work. Triggers include SuperVersion installation, flush/compaction
  snapshot loading, WritePrepared snapshot loading, the trigger-compaction
  periodic task, and `WaitForCompact()`.
- `GetAll()` copies each sorted shard independently and performs a bounded
  S-way merge, preserving ordering, deduplication, maximum-sequence filtering,
  and write-conflict-boundary semantics. Statistics-only queries do not trigger
  maintenance.
- Timestamped snapshot insertion remains O(1). Equal-sequence ordering follows
  list insertion order; `GetOldestSnapshotTime()` returns the minimum shard-head
  Unix time and is documented as approximate when wall-clock time moves
  backward or snapshots are concurrently created.
- Secondary stress verification now reads each column family through a managed
  secondary snapshot after catch-up.

Alternatives evaluated:

- A reader/writer lock does not help because both creation and release mutate
  the list. A lock-free list would additionally require ordered insertion,
  lifetime management, and safe reclamation.
- Running maintenance from release would reintroduce the DB mutex on the read
  path. Running it only from flush would delay cleanup when a DB is otherwise
  idle. Reusing existing exact DB-mutex-protected scans keeps release cheap and
  provides periodic eventual progress.
- Moving `GetAll()` merging outside the DB mutex would require callers to pin
  and revalidate surrounding flush, compaction, and transaction state. It is
  deferred without evidence that the bounded merge is a material critical-
  section cost.
- Shard-count measurements did not justify a public option. At 64 workers, 32
  shards were 10.5% slower than 16 in the existing snapshot-backed
  `randomwithverify` workload. A 64-shard setting cannot be supported because
  maintenance holds the DB mutex plus all shard locks, exceeding GCC TSAN's
  fixed 64-lock set. The count is therefore an internal constant of 16.

Safety boundaries and limitations:

- This changes neither file format nor compaction-picking policy. Shard locks
  are leaf locks under the DB mutex, and multi-shard operations always acquire
  them in index order.
- Atomic multi-CF ingestion never adds all 16 shard locks to its arbitrary-size
  per-CF lock set. Its installed Versions first use the last exact oldest
  sequence, which is conservative, and one exact scan runs after those locks
  are released.
- Read-only, secondary, and follower instances do not run release-driven
  compaction maintenance because they cannot schedule that work.
- Per-shard release counts are updated under the same shard lock as removal.
  Exact readers aggregate them while already holding every shard, avoiding a
  shared counter on the release hot path.
- Release-driven compaction eligibility is eventually observed by the next
  exact maintenance trigger. With default statistics scheduling this is
  normally within 600 seconds; if both statistics tasks are disabled and no
  column family has a shorter time-based trigger, trigger-compaction provides
  a 12-hour fallback.
- `GetAll()` remains O(N log 16) under its caller's DB mutex. Snapshot-time
  statistics are approximate under wall-clock rollback.
- Local benchmarks are closed-loop and do not model remote latency. The
  existing `randomwithverify` benchmark performs three reads under each
  explicit snapshot; it has no directly comparable no-snapshot mode, so no
  no-snapshot ceiling is claimed.

Performance evidence:

Release-build, CPU-pinned, order-balanced trials compared an unsharded parent
with this change. The existing `randomwithverify` workload takes one explicit
snapshot per read operation and performs three point Gets under it; writes and
deletes atomically update the corresponding three keys. The fully cached
workload used 98% reads, 1% overwrites, 1% deletes, 100,000 distinct keys,
256-byte values, a 1 GiB write buffer and block cache, disabled compression and
automatic compaction, and fixed seed 8675309. Each variant and trial started
from a fresh database, warmed up for 200,000 operations per worker, and then
ran two million measured operations per worker. Five paired trials produced
these median throughputs:

| Workers | Parent | Change | Improvement |
| ---: | ---: | ---: | ---: |
| 1 | 0.132 Mops/s | 0.133 Mops/s | +1.1% |
| 2 | 0.254 Mops/s | 0.265 Mops/s | +4.3% |
| 4 | 0.492 Mops/s | 0.592 Mops/s | +20.2% |
| 16 | 0.586 Mops/s | 2.956 Mops/s | +404.2% |

The throughput medians are non-regressing at every worker count; 17 of 20
individual pairings improved, with three low-thread losses inside the observed
run-to-run spread.

A separate three-pair CPU/contention experiment used the same configuration
and fresh-DB setup at 1, 2, 4, 16, and 64 workers, with two million measured
operations per worker. All counters cover the measured process invocation,
including DB open/close; prefill and warmup are separate. One operation is a
benchmark operation, not one individual point Get. The following are medians
per variant, with aggregate CPU time from `task-clock`:

| Workers | Parent/Change Mops/s | Parent/Change cycles/op | Cycle change | Parent/Change CPU us/op | Parent/Change context switches/Mop |
| ---: | ---: | ---: | ---: | ---: | ---: |
| 1 | 0.129 / 0.130 | 22,647 / 22,425 | -1.0% | 7.78 / 7.69 | 551.0 / 561.0 |
| 2 | 0.246 / 0.266 | 22,566 / 21,760 | -3.6% | 8.02 / 7.45 | 4,530.5 / 328.5 |
| 4 | 0.383 / 0.594 | 23,495 / 19,242 | -18.1% | 9.77 / 6.56 | 29,709.3 / 134.4 |
| 16 | 0.629 / 2.924 | 33,924 / 15,088 | -55.5% | 22.84 / 5.14 | 190,879.1 / 74.8 |
| 64 | 0.635 / 5.582 | 240,076 / 22,858 | -90.5% | 93.66 / 10.53 | 200,175.0 / 46,537.8 |

At 16 and 64 workers, throughput improved 364.6% and 779.0%, while cycles/op
fell 55.5% and 90.5%. Aggregate CPU time/op fell 77.5% and 88.8%, and context
switches/op fell 99.96% and 76.75%. CPU time/op excludes blocked time; these
results show better CPU efficiency, not merely higher CPU utilization.
Single-worker cycles/op was nearly flat (-1.0%); an independent earlier series
measured +4.1%, so a small single-worker regression cannot be ruled out.
The 64-worker change throughput ranged from 4.178 to 5.611 Mops/s across the
three trials; these closed-loop VM results are not a production guarantee.

Separate futex-instrumented runs used 250,000 operations per worker and three
order-balanced pairs. Counts cover all futexes in the benchmark process, not
only snapshot mutexes. `FUTEX_WAIT` entry-to-exit duration includes sleep and
syscall time, including fast failed waits; it is not CPU time.

| Workers | Parent/Change FUTEX_WAIT calls/Mop | Parent/Change FUTEX_WAKE calls/Mop | Parent/Change summed WAIT us/op | WAIT duration reduction |
| ---: | ---: | ---: | ---: | ---: |
| 16 | 1,351,313.00 / 21.75 | 1,328,722.25 / 181.25 | 9.42994 / 0.00393 | 99.96% |
| 64 | 840,136.56 / 449,760.56 | 1,005,333.44 / 625,624.44 | 36.75790 / 0.73084 | 98.01% |

These measurements support less mutex blocking and scheduler overhead under
contention. At 64 workers, 16 shards still have residual contention: WAIT
calls/op fell 46.5%, but their summed duration fell 98.0%. User-cycle sampling
also showed residual pthread lock work; it cannot assign a precise fraction
of saved cycles to snapshot mutexes versus writes or allocation.
Generic cache misses/op fell 33.7% and 44.5% at 16 and 64 workers, respectively,
but cache misses are not a measurement of cache-line bouncing. This KVM guest
does not expose the memory-sampling PMU required by `perf c2c`, so cache-line
bouncing remains an explanation to test on suitable hardware, not a measured
claim. Context switches, futex duration, and cycles are not additive parts of
a CPU-cost breakdown.

The shard-count decision used the existing `randomwithverify` workload with
three reads per snapshot, fixed seed 8675309, one million operations per
worker, a 1 GiB write buffer, disabled compression and automatic compaction,
and three order-balanced 64-worker trials. The 16-shard trials measured 4.953,
5.049, and 4.634 Mops/s; the 32-shard trials measured 4.201, 4.433, and 4.540
Mops/s. The medians favor the fixed 16-shard count by 11.7% (or equivalently,
32 shards were 10.5% slower).

Validation coverage:

Focused tests cover shard selection and merging, timestamped ordering, exact
oldest-snapshot maintenance, global threshold gating,
SuperVersion-install release races, read-only/secondary behavior, external-file
ingestion gates, and concurrent snapshot use. The WritePrepared snapshot
matrix and format/lint checks also pass. Full diff CI supplies the broad build,
platform, sanitizer, and test-suite coverage.

Reviewed By: pdillinger

Differential Revision: D123507967

fbshipit-source-id: 2a067dbadc5382cf998cc4920d8aedd81136ac5f
2026-10-23 11:32:02 -07:00
cm-sally-mate 94c7f76da0 Keep read-path tombstone conversion from deleting ingested files an older iterator cannot see (#15461)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15461

**What:** With `min_tombstones_for_range_conversion` set, an iterator whose view predates an ingestion can delete the ingested keys for good.

**Why it matters:** Silent, permanent data loss. `Get` returns the ingested key right after ingestion. After one scan from the older iterator, `Get` returns NotFound, also after flush and reopen. Every MyRocks bulk load with the option set takes this path: MyRocks ingests with `allow_global_seqno=false` and `snapshot_consistency=false` (seqno 0).

**Root cause:**
- The older iterator does not see the ingested file. It sees the deleted keys on both sides of the file as one run, and inserts a range tombstone at its read sequence number. The tombstone covers the ingested keys.
- `ingest_seqno_barrier_` (D102044512) refuses a conversion only when the iterator's sequence number is below the ingested seqno. That misses two cases:
  1. The file gets no new seqno: no overlap (seqno 0), `ingest_behind`, or `allow_db_generated_files`. Ingestion does not move the barrier at all (`assigned > 0`).
  2. `NewIterator()` references the SuperVersion before it reads the sequence number. If an ingestion and a write happen in between, the iterator reads at a sequence number above the ingested file, with a SuperVersion that does not have it. This also loses files ingested with a new seqno.
- Ingestions that flush the memtable are safe: the older iterator's memtable becomes immutable.

**Fix:** Barrier on the Version, not only on the sequence number.
- After `LogAndApply()`, ingestion records the new Version number on the active memtable (`ingest_version_barrier_`), while it still holds the per-CF `ingest_sst_lock`.
- `DBIter` passes the number of the Version it reads. `AddLogicallyRedundantRangeTombstone()` refuses the conversion if that number is below the barrier.
- Version numbers only go up, and every later Version of the column family has the ingested files. So the check is true exactly when the iterator cannot see an ingestion into this memtable. Iterators created after the ingestion convert as before.
- The seqno barrier stays: it still covers old explicit snapshots that read a newer Version.

**Related:** D126796014 (`Disable tombstone conversion for kMemtableTier iterators`) fixes a different trigger of the same feature.

Reviewed By: joshkang97, archang19

Differential Revision: D126875720

fbshipit-source-id: 5767b866c6174d791f9ddae78311816300ac49f3
2026-10-22 15:31:52 -07:00
Yoshinori Matsunobu d6a7847d37 Add a DB option to log long-held global mutexes and release sites (#15436)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15436

## Why
Long-held global mutexes can stall foreground and background work. Existing mutex statistics measure acquisition waits, not continuous ownership. This adds opt-in logging to identify the mutex, owning thread, operation and release site.

## Enable for an investigation
Provide an INFO logger and shared `Statistics` object when opening the DB, then:
- Set the shared statistics level to `StatsLevel::kAll`.
- Set `mutex_hold_log_threshold_us` through `DB::SetDBOptions`; `100000` means 100 ms.
- Restore threshold 0 and the previous statistics level afterward.

The threshold defaults to **0 (disabled)**. `kAll` alone does not enable these hold probes; threshold 0 leaves existing statistics timers unchanged.

## Output
Illustrative records, not production observations:
```
SLOW_GLOBAL_MUTEX mutex=DBImpl::mutex_ mutex_addr=0x12340000 owner_thread_id=12345 thread_operation=Compaction hold_us=200000 threshold_us=100000 observed_end_us=1792205400000000 buffer_dropped_records=0 unlock=db_impl_compaction_flush.cc:4660 BackgroundCallCompaction()
SLOW_GLOBAL_MUTEX mutex=DBImpl::wal_write_mutex_ mutex_addr=0x12340100 owner_thread_id=12346 thread_operation= hold_us=35000 threshold_us=25000 observed_end_us=1792205401000000 buffer_dropped_records=0 scope_exit=db_impl_write.cc:2250 PreprocessWrite()
```
The first proves a completed 200 ms DB-mutex ownership interval on a thread tagged `Compaction`. The second uses a 25 ms threshold; an empty operation tag means none was available. Neither record reports acquisition wait or proves the cause of a stall.

Release attribution uses compiler-embedded strings available in optimized, stripped builds, with a GCC/Clang fallback where standard source locations are unavailable. Direct unlock and condition wait identify their call sites; scope exit and temporary unlock identify guard declarations, not closing braces. No stack unwinding or symbol lookup.

## Scope and safety
- Track six selected mutexes: DB, WAL, point-lock wait graph, two prepare-log bookkeeping mutexes and transaction name map. This is not comprehensive synchronization tracing.
- Acquisition waits and condition-variable release gaps are excluded from hold duration. Configuration is pre-first-use; repeated setup retains the first valid context.
- Sample at most once per mutex per second, with eight deferred records per thread. `buffer_dropped_records` counts capacity overflow only; rate limiting, pause and emission contention can omit samples.
- Emit after covered outer lock scopes release. Emission is nonblocking best-effort; shutdown drains/pauses emission and stops diagnostics before the owned logger closes. Closing from that logger's own callback is unsupported.
- No data-file format change. The mutable option is persisted in OPTIONS and wired into bench/stress tools. Typed C binding is deferred; string DB-option APIs are available.

Disabled bookkeeping still has a cost. Release A/B measurements found about 3 ns added per selected mutex pair at level 1, and about 70 ns for hold timing over `kAll`/0. Get and reused-iterator Seek showed no resolved small probe effect; fresh iterator+Seek at 16 threads showed +8.7% CPU/query and -9.4% QPS. No logs were emitted in those benchmarks; logging cost and broader workload qualification remain open.

Reviewed By: xingbowang

Differential Revision: D125790955

fbshipit-source-id: 1096163f3bd25ffee585dc6ec8ebfd7f1759851a
2026-10-22 09:50:20 -07:00
Andrew Chang 431ce83b9b Disable tombstone conversion for kMemtableTier iterators (#15451)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15451

Fix a data loss bug in read-path range tombstone conversion.

Why the change is required:
With `min_tombstones_for_range_conversion` > 0, a DBIter that scans a run of contiguous point tombstones inserts a range tombstone `[first_tombstone_key, next_live_key)` at its read sequence number into the active memtable. This is only logically redundant if the iterator observed every live key inside that range. An iterator created with `ReadOptions::read_tier = kMemtableTier` reads only the mutable and immutable memtables (`DBImpl::NewInternalIterator` adds no SST iterators for that tier), but DBIter still enabled conversion for it: the gates were the active memtable, `table_filter`, prefix mode, and timestamp visibility, not the read tier.

Previous behavior:
A live key that exists only in SST files and sorts between memtable tombstones is invisible to a memtable-only scan, so the scan treats the tombstones around it as contiguous and inserts a range tombstone covering it. The key is then deleted for all reads at or above the iterator's sequence number, and the deletion persists through flush and compaction. Example with `min_tombstones_for_range_conversion = 2`: `Put(c)`, `Flush()`, `Put(a)`, `Delete(b)`, `Delete(d)`, `Put(e)`; a `kMemtableTier` scan returns `a, e` and inserts `[b, e)`; afterwards `Get(c)` returns NotFound. Both forward and reverse scans trigger it.

Affected: column families with `min_tombstones_for_range_conversion` > 0 (EXPERIMENTAL, default 0) that create iterators with `read_tier = kMemtableTier`. Not affected: `kBlockCacheTier` (the scan stops with `Incomplete` before a run can be flushed, already covered by `NoInsertionOnBlockCacheTierIncomplete`), `kPersistedTier` (not supported for iterators), and all other iterators.

New behavior:
Conversion runs only for iterators with `read_tier = kReadAllTier`, and stays disabled for the iterator lifetime for every other tier. This also turns it off for `kBlockCacheTier` scans that are served fully from cache, which could convert before. The option documentation now lists reads with `read_tier` other than `kReadAllTier` among the configurations where conversion is skipped.

Implementation:
One more condition, `read_tier == kReadAllTier`, in the DBIter constructor's `min_tombstones_for_range_conversion_` gate, next to the existing `table_filter` and timestamp-visibility checks. Release note added.

Safety, compatibility, and limitations:
No API, format, or default changes. The change only drops a logically redundant optimization for read tiers that cannot guarantee a complete scan. A new test checks that a range tombstone inserted by a full-tier scan does not change what later `kMemtableTier` and `kBlockCacheTier` reads return. The only difference it found: with a cold block cache, a `kBlockCacheTier` `Get` of a covered key returns NotFound (the full-tier answer) instead of `Incomplete`.

Reviewed By: joshkang97, jaykorean

Differential Revision: D126796014

fbshipit-source-id: 70f5184dbf5bb9eff765c232ac130ab43379a814
2026-10-22 09:40:43 -07:00
Peter Dillinger 539532377d Remove Solaris support (#15449)
Summary:
Solaris support was contributed by outside users starting with d78c6b28c (2016), and no project CI has ever run on Solaris. The last Solaris-motivated change was c76688745 (https://github.com/facebook/rocksdb/issues/4012, June 2018). Four months later, d6ec28870 (https://github.com/facebook/rocksdb/issues/4226, 5.18) gave PerfContext a user-declared destructor while the Solaris-only definition was still `__thread PerfContext perf_context_`, which GCC rejects, so the Solaris build has not compiled since. fd00f39f9 (https://github.com/facebook/rocksdb/issues/8117, 2021) then renamed that definition to `perf_context` while keeping
`#define perf_context (*get_perf_context())` in perf_context_imp.h, so the definition expands to `thread_local PerfContext (*get_perf_context());`, an error still present today. The Solaris-only paths are dead and untestable, yet still attract new carve-outs.

Because Solaris support is apparently unused and complicating things, this change removes all OS_SOLARIS / SunOS handling from the build (Make, CMake, build_detect_platform, INSTALL.md), the portability layer and Env (endianness, gethrtime, directio, stack trace), the perf_context macro and destructor carve-out, Solaris-only test workarounds and exclusions, and RocksJava's Environment.isSolaris() and Solaris JNI library naming.

Not Solaris-specific and kept: SPARC unaligned-access handling in util/coding.h, sparc64 in Makefile machine lists (Linux/OpenBSD), and sparcv9 in RocksJava Environment.is64Bit(). Third-party sources are untouched.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/15449

Test Plan: On non-Solaris platforms every removed condition was either an OS_SOLARIS-only branch or a `!defined(OS_SOLARIS)` term that was always true, so the code compiled is unchanged and the CI matrix covers it. The Solaris breakage was confirmed by syntax-checking monitoring/perf_context.cc with -DOS_SOLARIS (GCC 11, stub sys/isa_defs.h) at each commit touching the perf_context files from c31979205 through fd00f39f9: clean before d6ec28870, failing at every commit from d6ec28870 on.

Reviewed By: archang19

Differential Revision: D126714985

Pulled By: pdillinger

fbshipit-source-id: 40d22fcd5a462375c9d2c93f52407df651286ef7
2026-10-22 08:25:11 -07:00
zaidoon 553b52b4a7 Wire IndexFactory through block-based tables (#14963)
Summary:
Build block-based table indexes through `IndexFactory`, with modes for writing both indexes or only the custom index and a per-read index selector. Built-in reads retain their existing cache and prefetch paths.

The review fixes preserve the existing UDI behavior at the migration and rollback boundaries:

- Reopening an SST with index caching enabled reuses the serialized custom block. Each table reader constructs its own factory reader over those bytes and passes `NewReader()` a `Slice` view stored in the table reader. That view stays valid for the reader's lifetime, so a legacy reader can advance it or keep a pointer to it without changing the cached block. The view and the raw block are destroyed after the custom reader.
- `IndexFactoryReader::ApproximateMemoryUsage()` keeps its documented meaning: the reader's own allocations, excluding the serialized block. `MemoryUsageIncludesIndexBlock()` defaults to false. A reader whose estimate includes the block opts in, and the wrapper subtracts the block size from that estimate.
- Copying factory options and clearing a legacy UDI flag now rolls back that flag's policy. Legacy-derived options keep the requested default and resolve their effective mode without changing the inputs. Explicit factory modes retain precedence through cloning, failed updates, reconstruction, and the C API, including an explicitly selected `kStandardDefault`.
- OK empty custom-index output still produces a zero-size meta block and reads through the full standard index. This covers point data and files containing only range tombstones. Custom-only mode rejects empty output because it has no standard fallback.
- Default primary reads retain the table factory's precedence over a legacy per-read factory pointer. Explicit `kPreferCustom` still checks a supplied factory name; `kBuiltin` ignores that pointer.
- OPTIONS files write equivalent legacy booleans for `kStandardDefault`, `kStandardRequired`, and `kCustomDefault`. Older releases retain those policies when loading newer files with `ignore_unknown_options=true`; the explicit-mode marker remains authoritative for current readers.

Custom-only SSTs carry a footer feature bit so older readers reject them. Reject unsupported filter/index combinations before constructing filters. Trie size estimates account for shared prefixes, and block offsets or sizes beyond the 32-bit format limit return `NotSupported` instead of truncating.

## Safety and compatibility

- Custom-only files require format version 6 or later and the factory. Rewrite every custom-only SST with a full standard index before removing the factory or downgrading. The other modes have no equivalent legacy configuration beyond the three listed above.
- A raw C++ options struct cannot distinguish an explicitly assigned `kStandardDefault` from its default value. Configure the factory to make that choice explicit. `GetEffectiveIndexMode()` reports the resolved policy while preserving legacy inputs for copy-and-clear rollback.
- A custom factory with user-defined timestamps is rejected in every mode except `kStandardOnly`, which ignores the factory.
- Trie construction rejects a data-block offset or size greater than `UINT32_MAX`, including in `kStandardDefault`. That can fail flush, compaction, or `SstFileWriter::Finish()`; total SST size above 4 GiB alone is not the rejection condition. File-size targets do not split L0 or `SstFileWriter` outputs.
- Custom-index cache hits, misses, and adds are reported under `BLOCK_CACHE_INDEX_*` instead of `BLOCK_CACHE_DATA_*`. The existing cache setting still controls ownership and cache use.
- A table reader now charges a custom index block it owns (`cache_index_and_filter_blocks=false`); before, that block was not charged anywhere. Cached blocks are still charged only to the block cache. A reader whose `ApproximateMemoryUsage()` includes the serialized block, such as IntegerTrie or TimeSeries, must override `MemoryUsageIncludesIndexBlock()` to return true, or that block is charged twice.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14963

Test Plan:
For this commit, on macOS: `table_test`, `trie_index_test`, `trie_index_db_test`, and `block_based_table_reader_test` pass on the full stack (11,416 cases), and the full `table_test` passes at this PR's head alone (6,969 cases). The four custom-reader compatibility tests pass 100 times with `ASSERT_STATUS_CHECKED=1` and `COERCE_CONTEXT_SWITCH=1`, and 84 UDI table cases pass under ASAN with stack-use-after-return detection. Release-mode syntax checks with `-DNDEBUG -fno-rtti` on the changed sources, source checks, formatting checks, and `git diff --check` pass.

Two new regressions cover the latest review, and both were checked failing before the fix:

- `RetainedReaderSliceOutlivesTableOpen`: a reader keeps a pointer to the `Slice` it received, the stack is overwritten after open, and the reader then serves a seek and a full scan. It covers all three custom-index modes with owned and cached blocks, including a cached reopen. Without the fix, the retained `Slice` no longer matched the block.
- `AuxiliaryReaderMemoryIsFullyCharged`: a reader that implements only the older reader methods reports a fixed allocation of half and twice the raw block size. Table usage must grow by that amount with owned and cached blocks, and a strict cache sized just below the required reservation must reject the open with `MemoryLimit`. Without the fix, usage fell short by `min(allocation, raw block size)` and the strict-cache open succeeded.

The consuming-reader and strict-cache accounting tests are kept, now on shared helpers.

From the previous revision: 498 focused C++ cases passed (82 table/factory, 156 trie, and 260 trie DB), along with the C API test and generated C API verification. Coverage includes both comparators, copied-options rollback, C factory reconstruction, explicit modes, OPTIONS serialization, SST Get/iteration, point and range-only flushes, compaction, reopen, cache disabled/enabled/evicted, and legacy factory precedence. The rollback, empty-output, primary-selector, and OPTIONS regressions were checked failing before their fixes. The cache test measures file reads at the custom block's actual offset: one on a cold open, zero on cached reopens, and one after eviction or with caching disabled. Disabling custom-block cache reuse makes that assertion fail.

The full suite and Linux/Windows/TSAN coverage are left to CI. Earlier full-suite runs had failures/timeouts (173/2095 jobs normally and 152/2088 with Status checking); that broad suite was not rerun for this commit.

## Series

Previous: https://github.com/facebook/rocksdb/issues/14962 (landed through import). Next: https://github.com/facebook/rocksdb/issues/14965.

Reviewed By: jaykorean

Differential Revision: D122796629

Pulled By: xingbowang

fbshipit-source-id: 4c9d6f3f548c588a37b08fd94de25e8c0b5ef743
2026-10-21 20:20:38 -07:00
Xingbo Wang 81369fc170 Fix async secondary catch-up on deleted SSTs (#15438)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15438

Fix a RocksDB secondary with `open_files_async = true` latching on `PathNotFound` during `TryCatchUpWithPrimary()`.

**Why:** With `open_files_async = true` and `max_open_files = -1`, a secondary's catch-up could fail with `PathNotFound` on a table that was live at open time (most reachably in a non-default column family) but which the primary compacted away before the background async opener pinned it. The error latched in the tailer's status, so the secondary never advanced again and had to be closed and reopened. With `open_files_async = false` the same sequence caught up fine. This blocks enabling async secondary open where it would otherwise cut cold-open time substantially.

**Previous behavior:** In `VersionEditHandlerPointInTime::MaybeCreateVersionBeforeApplyEdit`, a negative-edge `LoadTableHandlers()` failure skipped the unrecoverable point-in-time only when the status was `Corruption`; a `PathNotFound` was returned as-is and latched. During a secondary's catch-up, `open_files_async` leaves recovery-time tables unpinned until the background opener reaches them, so `LoadTableHandlers()` could try to open a file the primary had already deleted.

**New behavior:** That negative-edge failure path now also treats `PathNotFound` as a missing file (skip this point-in-time and continue) when `no_error_if_files_missing_` is set, mirroring the tolerance `VersionEditHandler::LoadTables()` already applies. Catch-up continues past the transient state and installs the current version, so the secondary advances instead of latching.

**Implementation:**
- `db/version_edit_handler.cc`: broaden the negative-edge skip condition in `VersionEditHandlerPointInTime::MaybeCreateVersionBeforeApplyEdit` from `s.IsCorruption()` to also include `s.IsPathNotFound() && no_error_if_files_missing_`.
- `db/db_secondary_test.cc`: add `DBSecondaryTest.OpenFilesAsyncNonDefaultCfCatchUp`, a regression test that opens an async secondary with a non-default column family, holds the background opener back so the open-time table stays unpinned, has the primary flush and `CompactRange(kForce)` to replace and delete that table, then asserts `TryCatchUpWithPrimary()` succeeds and reads the new value.

**Safety boundaries:** The change only affects point-in-time handlers that opted into missing-file tolerance (`no_error_if_files_missing_`, set by secondary/follower tailing and best-efforts recovery); it does not relax error handling for strict recovery. It does not change the `trust_manifest_recovery_` / `defer_sst_file_opening_` paths, which already skip opening.

**Validation and limitations:** The regression test covers async secondary catch-up after deletion of an open-time SST in a non-default column family. Validation covers async opening, catch-up, memtable retention, and repeated scheduling changes. Strict recovery, API and file formats are unchanged. The full supported platform matrix has not been run locally.

Reviewed By: jaykorean

Differential Revision: D125378257

fbshipit-source-id: 3dd23c2f763e4e744c9f38fb77dfdfb3f554ee7f
2026-10-21 17:51:53 -07:00
460 changed files with 50922 additions and 7455 deletions

No files matched your search

-38
View File
@@ -1,38 +0,0 @@
name: post-steps
description: Steps that are taken after a RocksDB job
inputs:
artifact-prefix:
description: Prefix to append to the name of artifacts that are uploaded
required: true
default: "${{ github.job }}"
runs:
using: composite
steps:
- name: Upload Test Results artifact
uses: actions/upload-artifact@v4.0.0
with:
name: "${{ inputs.artifact-prefix }}-test-results"
path: "${{ runner.temp }}/test-results/**"
- name: Upload DB LOG file artifact
uses: actions/upload-artifact@v4.0.0
with:
name: "${{ inputs.artifact-prefix }}-db-log-file"
path: LOG
- name: Copy Test Logs (on Failure)
if: ${{ failure() }}
run: |
mkdir -p ${{ runner.temp }}/failure-test-logs
cp -r t/* ${{ runner.temp }}/failure-test-logs
shell: bash
- name: Upload Test Logs (on Failure) artifact
uses: actions/upload-artifact@v4.0.0
with:
name: "${{ inputs.artifact-prefix }}-failure-test-logs"
path: ${{ runner.temp }}/failure-test-logs/**
if-no-files-found: ignore
- name: Upload Core Dumps artifact
uses: actions/upload-artifact@v4.0.0
with:
name: "${{ inputs.artifact-prefix }}-core-dumps"
path: "core.*"
if-no-files-found: ignore
+16 -8
View File
@@ -2,12 +2,21 @@ name: pre-steps
runs:
using: composite
steps:
- name: Dump ulimits for diagnostics
- name: Dump system info for diagnostics
run: |
echo "=== ulimits (soft) ==="
ulimit -a -S || true
echo "=== ulimits (hard) ==="
ulimit -a -H || true
echo "=== OS ==="
uname -a || true
echo "=== CPU ==="
if [ "$RUNNER_OS" = "macOS" ]; then
sysctl machdep.cpu.brand_string hw.ncpu || true
else
lscpu || true
grep -m1 '^microcode' /proc/cpuinfo || true
fi
shell: bash
- name: Install lld linker for faster builds
run: apt-get update -y && apt-get install -y lld 2>/dev/null || true
@@ -15,20 +24,19 @@ runs:
- name: Setup Environment Variables
run: |-
echo "GTEST_THROW_ON_FAILURE=0" >> "$GITHUB_ENV"
echo "GTEST_OUTPUT=\"xml:${{ runner.temp }}/test-results/\"" >> "$GITHUB_ENV"
echo "SKIP_FORMAT_BUCK_CHECKS=1" >> "$GITHUB_ENV"
echo "GTEST_COLOR=1" >> "$GITHUB_ENV"
echo "CTEST_OUTPUT_ON_FAILURE=1" >> "$GITHUB_ENV"
echo "CTEST_TEST_TIMEOUT=300" >> "$GITHUB_ENV"
# These third-party sources are mirrored on the "third-party-deps" GitHub
# release. To add one, upload the asset here (intentionally hidden):
# https://github.com/facebook/rocksdb/releases/edit/third-party-deps
# https://github.com/meta/rocksdb/releases/edit/third-party-deps
# Note how file names are generated below (vs. from the original source)
# before uploading. Removing or modifying files will break builds for
# older releases/revisions (not found or checksum mismatch).
echo "ZLIB_DOWNLOAD_PREFIX=https://github.com/facebook/rocksdb/releases/download/third-party-deps/zlib-" >> "$GITHUB_ENV"
echo "BZIP2_DOWNLOAD_PREFIX=https://github.com/facebook/rocksdb/releases/download/third-party-deps/bzip2-" >> "$GITHUB_ENV"
echo "SNAPPY_DOWNLOAD_PREFIX=https://github.com/facebook/rocksdb/releases/download/third-party-deps/snappy-" >> "$GITHUB_ENV"
echo "LZ4_DOWNLOAD_PREFIX=https://github.com/facebook/rocksdb/releases/download/third-party-deps/lz4-" >> "$GITHUB_ENV"
echo "ZSTD_DOWNLOAD_PREFIX=https://github.com/facebook/rocksdb/releases/download/third-party-deps/zstd-" >> "$GITHUB_ENV"
echo "ZLIB_DOWNLOAD_PREFIX=https://github.com/meta/rocksdb/releases/download/third-party-deps/zlib-" >> "$GITHUB_ENV"
echo "BZIP2_DOWNLOAD_PREFIX=https://github.com/meta/rocksdb/releases/download/third-party-deps/bzip2-" >> "$GITHUB_ENV"
echo "SNAPPY_DOWNLOAD_PREFIX=https://github.com/meta/rocksdb/releases/download/third-party-deps/snappy-" >> "$GITHUB_ENV"
echo "LZ4_DOWNLOAD_PREFIX=https://github.com/meta/rocksdb/releases/download/third-party-deps/lz4-" >> "$GITHUB_ENV"
echo "ZSTD_DOWNLOAD_PREFIX=https://github.com/meta/rocksdb/releases/download/third-party-deps/zstd-" >> "$GITHUB_ENV"
shell: bash
+1 -1
View File
@@ -8,7 +8,7 @@ runs:
run: chown `whoami` . || true
shell: bash
- name: Set upstream
run: git remote add upstream https://github.com/facebook/rocksdb.git
run: git remote add upstream https://github.com/meta/rocksdb.git
shell: bash
- name: Fetch upstream
run: git fetch upstream
-98
View File
@@ -1,98 +0,0 @@
// Parse Codex review artifacts and produce a markdown review comment.
//
// Usage from actions/github-script:
// const parse = require('./.github/scripts/parse-codex-review.js');
// const markdown = parse({ responseFile, recoveryFile, findingsFile, logFile,
// exitCode, meta });
//
// Parameters:
// responseFile - path to Codex final response output
// recoveryFile - path to recovery output formatted from review-findings.md
// findingsFile - path to incremental findings file written during review
// logFile - path to Codex stdout/stderr log
// exitCode - Codex process exit code
// meta - { trigger, autoMode, headSha, reviewer, isQuery, isPartial }
const fs = require('fs');
const buildComment = require('./build-ai-review-comment.js');
module.exports = function parseCodex(
{responseFile, recoveryFile, findingsFile, logFile, exitCode, meta}) {
function getTriggerLine() {
if (meta.trigger !== 'auto') {
return `*Requested by @${meta.reviewer}*`;
}
const shortSha = meta.headSha ? meta.headSha.substring(0, 7) : 'unknown';
if (meta.autoMode === 'early') {
return `*Auto-triggered after CI reached the early-review threshold — reviewing commit ${
shortSha}*`;
}
return `*Auto-triggered after CI passed — reviewing commit ${shortSha}*`;
}
function readIfPresent(path) {
if (!path || !fs.existsSync(path)) {
return '';
}
const text = fs.readFileSync(path, 'utf8').trim();
return text;
}
function tailFile(path, maxChars = 12000) {
const text = readIfPresent(path);
if (!text) {
return '';
}
if (text.length <= maxChars) {
return text;
}
return text.slice(text.length - maxChars);
}
let responseBody = '';
const recovered = readIfPresent(recoveryFile);
const direct = readIfPresent(responseFile);
const findings = readIfPresent(findingsFile);
if (recovered) {
responseBody = recovered;
} else if (direct) {
responseBody = direct;
} else if (findings) {
responseBody =
'⚠️ **Review incomplete — Codex did not produce a final response.**\n\n' +
'Below are the incremental findings recovered from `review-findings.md`.\n\n---\n\n' +
findings;
} else {
const logTail = tailFile(logFile);
responseBody = logTail ?
`❌ **Codex review failed before producing findings.**\n\n\`\`\`\n${
logTail}\n\`\`\`` :
'❌ Codex review failed before producing any output.';
}
const isPartial = !!meta.isPartial;
const code = Number.parseInt(exitCode || '1', 10);
const icon = isPartial ? '🟡' : (code === 0 ? '✅' : '⚠️');
const triggerLine = getTriggerLine();
return buildComment({
icon,
headerTitle: meta.isQuery ? 'Codex Response' : 'Codex Code Review',
triggerLine,
responseBody,
footerLines: [
'Generated by Codex CLI.',
'Review methodology: `claude_md/ci_review_prompt.md`',
'',
'**Limitations:**',
'- Codex may miss context from files not in the diff',
'- Large PRs may be truncated',
'- Always apply human judgment to AI suggestions',
'',
'**Commands:**',
'- `/codex-review [context]` — Request a code review',
'- `/codex-query <question>` — Ask about the PR or codebase',
],
});
};
+1 -1
View File
@@ -117,7 +117,7 @@ function assertObsoleteComment(comment, originalBody) {
const context = {
repo: {
owner: 'facebook',
owner: 'meta',
repo: 'rocksdb',
},
};
+5 -418
View File
@@ -1,18 +1,8 @@
# Shared analysis workflow for AI PR reviews.
#
# This workflow is invoked by provider-specific wrappers (Claude/Codex) so the
# event triggers and workflow names stay user-friendly while the implementation
# lives in one place.
#
# Security note for Codex:
# Codex runs in non-interactive CI, so the Codex CLI steps below pass
# `--dangerously-bypass-approvals-and-sandbox`. That disables Codex's
# approval prompts and sandbox protections, so treat those steps as
# arbitrary command execution on the runner. This file therefore keeps Codex
# in the read-only analysis workflow, restricts the pull_request_target path
# to same-repo PRs, and leaves write-scoped PR commenting in a separate
# workflow. Re-evaluate that trust boundary before expanding Codex auto
# review to broader trigger sources.
# This workflow is invoked by provider-specific wrappers (such as
# claude-review.yml) so the event triggers and workflow names stay
# user-friendly while the implementation lives in one place.
name: AI Review Analysis
@@ -20,7 +10,7 @@ on:
workflow_call:
inputs:
provider:
description: Provider key, for example "claude" or "codex"
description: Provider key, for example "claude"
required: true
type: string
display_name:
@@ -119,7 +109,7 @@ jobs:
const FAIL_CONCLUSIONS = new Set([
'failure', 'cancelled', 'timed_out', 'action_required'
]);
const AI_CHECK_TOKENS = ['claude', 'codex'];
const AI_CHECK_TOKENS = ['claude'];
const MIN_CHECKS = 10;
const THRESHOLD = 0.5;
const POLL_INTERVAL_MS = 90 * 1000;
@@ -419,24 +409,6 @@ jobs:
fetch-depth: 0
persist-credentials: false
- name: Setup Node.js
if: >-
steps.pr_info.outputs.skip != 'true' &&
steps.check_existing.outputs.skip != 'true' &&
inputs.provider == 'codex'
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install Codex CLI
if: >-
steps.pr_info.outputs.skip != 'true' &&
steps.check_existing.outputs.skip != 'true' &&
inputs.provider == 'codex'
run: |
npm install -g @openai/codex
codex --version
- name: Generate diff and prompt
if: steps.pr_info.outputs.skip != 'true' && steps.check_existing.outputs.skip != 'true'
env:
@@ -541,72 +513,6 @@ jobs:
core.setOutput('label', label);
core.setOutput('tokens', budget);
- name: Classify PR complexity (Codex)
if: >-
steps.pr_info.outputs.skip != 'true' &&
steps.check_existing.outputs.skip != 'true' &&
inputs.provider == 'codex'
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
CODEX_HOME: /tmp/codex-home
CODEX_MODEL: ${{ inputs.default_model }}
run: |
if [ -z "${OPENAI_API_KEY}" ]; then
echo "OPENAI_API_KEY not configured, skipping Codex review"
printf 'complex\n' > codex-complexity-output.txt
exit 0
fi
mkdir -p "${CODEX_HOME}"
set +e
codex exec \
-m "${CODEX_MODEL}" \
-c model_reasoning_effort="medium" \
--dangerously-bypass-approvals-and-sandbox \
--output-last-message codex-complexity-output.txt \
- < /tmp/complexity_prompt.txt > codex-complexity.log 2>&1
exit_code=$?
set -e
if [ "${exit_code}" -ne 0 ]; then
echo "Codex complexity classification failed with exit code ${exit_code}"
echo "Defaulting to complex review budget"
if [ -s codex-complexity.log ]; then
echo "Last 100 lines of codex-complexity.log:"
tail -100 codex-complexity.log
fi
printf 'complex\n' > codex-complexity-output.txt
fi
- name: Pick thinking budget (Codex)
if: >-
steps.pr_info.outputs.skip != 'true' &&
steps.check_existing.outputs.skip != 'true' &&
inputs.provider == 'codex'
id: budget_codex
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
let label = 'complex';
try {
const text = fs.readFileSync(
'codex-complexity-output.txt',
'utf8'
).trim().toLowerCase();
if (/\bsimple\b/.test(text) && !/\bcomplex\b/.test(text)) {
label = 'simple';
}
} catch (error) {
core.warning(
`Could not parse classifier output: ${error.message}`);
}
const effort = label === 'simple' ? 'high' : 'xhigh';
core.info(
`Complexity classified as: ${label} ` +
`-> model_reasoning_effort=${effort}`
);
core.setOutput('label', label);
core.setOutput('effort', effort);
- name: Run Claude
if: >-
steps.pr_info.outputs.skip != 'true' &&
@@ -703,99 +609,6 @@ jobs:
});
fs.writeFileSync(process.env.COMMENT_FILE, body);
- name: Run Codex
if: >-
steps.pr_info.outputs.skip != 'true' &&
steps.check_existing.outputs.skip != 'true' &&
inputs.provider == 'codex'
id: codex
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
CODEX_HOME: /tmp/codex-home
CODEX_MODEL: ${{ inputs.default_model }}
CODEX_REASONING_EFFORT: ${{ steps.budget_codex.outputs.effort }}
run: |
if [ -z "${OPENAI_API_KEY}" ]; then
echo "OPENAI_API_KEY not configured, skipping Codex review"
exit 0
fi
mkdir -p "${CODEX_HOME}"
set +e
codex exec \
-m "${CODEX_MODEL}" \
-c model_reasoning_effort="${CODEX_REASONING_EFFORT}" \
--dangerously-bypass-approvals-and-sandbox \
--output-last-message codex-review-output.md \
- < /tmp/prompt.txt > codex-review-execution.log 2>&1
exit_code=$?
set -e
echo "exit_code=${exit_code}" >> "${GITHUB_OUTPUT}"
echo "${exit_code}" > codex-review-exit-code.txt
- name: Recover partial review (Codex)
if: >-
steps.pr_info.outputs.skip != 'true' &&
steps.check_existing.outputs.skip != 'true' &&
inputs.provider == 'codex' &&
steps.codex.outputs.exit_code != '0' &&
hashFiles('review-findings.md') != ''
id: recover_codex
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
CODEX_HOME: /tmp/codex-home
CODEX_MODEL: ${{ inputs.default_model }}
CODEX_REASONING_EFFORT: ${{ steps.budget_codex.outputs.effort }}
run: |
set +e
codex exec \
-m "${CODEX_MODEL}" \
-c model_reasoning_effort="${CODEX_REASONING_EFFORT}" \
--dangerously-bypass-approvals-and-sandbox \
--output-last-message codex-review-recovery.md \
- < claude_md/ci_recovery_prompt.md > codex-review-recovery.log 2>&1
exit_code=$?
set -e
echo "exit_code=${exit_code}" >> "${GITHUB_OUTPUT}"
- name: Build review comment (Codex)
if: >-
steps.pr_info.outputs.skip != 'true' &&
steps.check_existing.outputs.skip != 'true' &&
inputs.provider == 'codex'
env:
RESPONSE_FILE: codex-review-output.md
RECOVERY_FILE: codex-review-recovery.md
FINDINGS_FILE: review-findings.md
LOG_FILE: codex-review-execution.log
EXIT_CODE: ${{ steps.codex.outputs.exit_code }}
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
AUTO_MODE: ${{ steps.pr_info.outputs.auto_mode }}
USED_RECOVERY: ${{ steps.recover_codex.outputs.exit_code == '0' && 'true' || 'false' }}
COMMENT_FILE: ${{ inputs.comment_file }}
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const parse = require('./.github/scripts/parse-codex-review.js');
const body = parse({
responseFile: process.env.RESPONSE_FILE,
recoveryFile: process.env.RECOVERY_FILE,
findingsFile: process.env.FINDINGS_FILE,
logFile: process.env.LOG_FILE,
exitCode: process.env.EXIT_CODE,
meta: {
trigger: 'auto',
autoMode: process.env.AUTO_MODE,
headSha: process.env.HEAD_SHA,
reviewer: '',
isQuery: false,
isPartial: process.env.USED_RECOVERY === 'true' ||
(!fs.existsSync(process.env.RESPONSE_FILE) &&
fs.existsSync(process.env.FINDINGS_FILE)),
}
});
fs.writeFileSync(process.env.COMMENT_FILE, body);
- name: Save PR number
if: steps.pr_info.outputs.skip != 'true' && steps.check_existing.outputs.skip != 'true'
run: |
@@ -846,27 +659,6 @@ jobs:
retention-days: 7
if-no-files-found: ignore
- name: Upload review logs (Codex)
if: >-
always() &&
steps.pr_info.outputs.skip != 'true' &&
steps.check_existing.outputs.skip != 'true' &&
inputs.provider == 'codex'
uses: actions/upload-artifact@v4
with:
name: codex-review-logs
path: |
codex-review-execution.log
codex-complexity.log
codex-complexity-output.txt
codex-review-recovery.log
codex-review-output.md
codex-review-recovery.md
review-findings.md
codex-review-exit-code.txt
retention-days: 7
if-no-files-found: ignore
manual-review:
name: Manual ${{ inputs.display_name }} Review
runs-on: ubuntu-latest
@@ -990,18 +782,6 @@ jobs:
fetch-depth: 0
persist-credentials: false
- name: Setup Node.js
if: steps.request.outputs.skip != 'true' && inputs.provider == 'codex'
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install Codex CLI
if: steps.request.outputs.skip != 'true' && inputs.provider == 'codex'
run: |
npm install -g @openai/codex
codex --version
- name: Generate diff and prompt
if: steps.request.outputs.skip != 'true'
env:
@@ -1153,89 +933,6 @@ jobs:
core.setOutput('label', label);
core.setOutput('tokens', budget);
- name: Classify PR complexity (Codex)
if: >-
steps.request.outputs.skip != 'true' &&
steps.request.outputs.type == 'review' &&
inputs.provider == 'codex'
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
CODEX_HOME: /tmp/codex-home
CODEX_MODEL: ${{ inputs.selected_model != '' && inputs.selected_model || inputs.default_model }}
run: |
if [ -z "${OPENAI_API_KEY}" ]; then
echo "OPENAI_API_KEY not configured, skipping Codex review"
printf 'complex\n' > codex-complexity-output.txt
exit 0
fi
mkdir -p "${CODEX_HOME}"
set +e
codex exec \
-m "${CODEX_MODEL}" \
-c model_reasoning_effort="medium" \
--dangerously-bypass-approvals-and-sandbox \
--output-last-message codex-complexity-output.txt \
- < /tmp/complexity_prompt.txt > codex-complexity.log 2>&1
exit_code=$?
set -e
if [ "${exit_code}" -ne 0 ]; then
echo "Codex complexity classification failed with exit code ${exit_code}"
echo "Defaulting to complex review budget"
if [ -s codex-complexity.log ]; then
echo "Last 100 lines of codex-complexity.log:"
tail -100 codex-complexity.log
fi
printf 'complex\n' > codex-complexity-output.txt
fi
- name: Pick thinking budget (Codex)
if: steps.request.outputs.skip != 'true' && inputs.provider == 'codex'
id: manual_budget_codex
env:
OVERRIDE: ${{ inputs.thinking_budget }}
COMMAND_TYPE: ${{ steps.request.outputs.type }}
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
if (process.env.COMMAND_TYPE !== 'review') {
core.setOutput('label', 'query');
core.setOutput('effort', 'medium');
return;
}
const override = (process.env.OVERRIDE || '').trim();
if (override && /^\d+$/.test(override)) {
const budget = parseInt(override, 10);
const effort = budget >= 32000 ? 'xhigh' : 'high';
core.info(
`Using thinking_budget override: ${override} ` +
`-> model_reasoning_effort=${effort}`
);
core.setOutput('label', 'override');
core.setOutput('effort', effort);
return;
}
let label = 'complex';
try {
const text = fs.readFileSync(
'codex-complexity-output.txt',
'utf8'
).trim().toLowerCase();
if (/\bsimple\b/.test(text) && !/\bcomplex\b/.test(text)) {
label = 'simple';
}
} catch (error) {
core.warning(
`Could not parse classifier output: ${error.message}`);
}
const effort = label === 'simple' ? 'high' : 'xhigh';
core.info(
`Complexity classified as: ${label} ` +
`-> model_reasoning_effort=${effort}`
);
core.setOutput('label', label);
core.setOutput('effort', effort);
- name: Run Claude
if: steps.request.outputs.skip != 'true' && inputs.provider == 'claude'
id: manual_claude
@@ -1328,99 +1025,6 @@ jobs:
});
fs.writeFileSync(process.env.COMMENT_FILE, body);
- name: Run Codex
if: steps.request.outputs.skip != 'true' && inputs.provider == 'codex'
id: manual_codex
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
CODEX_HOME: /tmp/codex-home
CODEX_MODEL: ${{ inputs.selected_model != '' && inputs.selected_model || inputs.default_model }}
CODEX_REASONING_EFFORT: ${{ steps.manual_budget_codex.outputs.effort }}
COMMAND_TYPE: ${{ steps.request.outputs.type }}
run: |
mkdir -p "${CODEX_HOME}"
set +e
if [ "${COMMAND_TYPE}" = "query" ]; then
codex exec \
-m "${CODEX_MODEL}" \
-c model_reasoning_effort="${CODEX_REASONING_EFFORT}" \
--dangerously-bypass-approvals-and-sandbox \
--output-last-message codex-review-output.md \
- < /tmp/prompt.txt > codex-review-execution.log 2>&1
else
codex exec \
-m "${CODEX_MODEL}" \
-c model_reasoning_effort="${CODEX_REASONING_EFFORT}" \
--dangerously-bypass-approvals-and-sandbox \
--output-last-message codex-review-output.md \
- < /tmp/prompt.txt > codex-review-execution.log 2>&1
fi
exit_code=$?
set -e
echo "exit_code=${exit_code}" >> "${GITHUB_OUTPUT}"
echo "${exit_code}" > codex-review-exit-code.txt
- name: Recover partial review (Codex)
if: >-
steps.request.outputs.skip != 'true' &&
inputs.provider == 'codex' &&
steps.request.outputs.type == 'review' &&
steps.manual_codex.outputs.exit_code != '0' &&
hashFiles('review-findings.md') != ''
id: manual_recover_codex
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
CODEX_HOME: /tmp/codex-home
CODEX_MODEL: ${{ inputs.selected_model != '' && inputs.selected_model || inputs.default_model }}
CODEX_REASONING_EFFORT: ${{ steps.manual_budget_codex.outputs.effort }}
run: |
set +e
codex exec \
-m "${CODEX_MODEL}" \
-c model_reasoning_effort="${CODEX_REASONING_EFFORT}" \
--dangerously-bypass-approvals-and-sandbox \
--output-last-message codex-review-recovery.md \
- < claude_md/ci_recovery_prompt.md > codex-review-recovery.log 2>&1
exit_code=$?
set -e
echo "exit_code=${exit_code}" >> "${GITHUB_OUTPUT}"
- name: Build review comment (Codex)
if: steps.request.outputs.skip != 'true' && inputs.provider == 'codex'
env:
RESPONSE_FILE: codex-review-output.md
RECOVERY_FILE: codex-review-recovery.md
FINDINGS_FILE: review-findings.md
LOG_FILE: codex-review-execution.log
EXIT_CODE: ${{ steps.manual_codex.outputs.exit_code }}
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
REVIEWER: ${{ steps.request.outputs.reviewer }}
COMMAND_TYPE: ${{ steps.request.outputs.type }}
USED_RECOVERY: ${{ steps.manual_recover_codex.outputs.exit_code == '0' && 'true' || 'false' }}
COMMENT_FILE: ${{ inputs.comment_file }}
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const parse = require('./.github/scripts/parse-codex-review.js');
const body = parse({
responseFile: process.env.RESPONSE_FILE,
recoveryFile: process.env.RECOVERY_FILE,
findingsFile: process.env.FINDINGS_FILE,
logFile: process.env.LOG_FILE,
exitCode: process.env.EXIT_CODE,
meta: {
trigger: 'manual',
headSha: process.env.HEAD_SHA,
reviewer: process.env.REVIEWER,
isQuery: process.env.COMMAND_TYPE !== 'review',
isPartial: process.env.USED_RECOVERY === 'true' ||
(!fs.existsSync(process.env.RESPONSE_FILE) &&
fs.existsSync(process.env.FINDINGS_FILE)),
}
});
fs.writeFileSync(process.env.COMMENT_FILE, body);
- name: Save PR number
if: steps.request.outputs.skip != 'true'
run: |
@@ -1474,20 +1078,3 @@ jobs:
path: ${{ steps.manual_format_recovery_claude.outputs.execution_file }}
retention-days: 7
if-no-files-found: ignore
- name: Upload review logs (Codex)
if: always() && steps.request.outputs.skip != 'true' && inputs.provider == 'codex'
uses: actions/upload-artifact@v4
with:
name: codex-review-logs
path: |
codex-review-execution.log
codex-complexity.log
codex-complexity-output.txt
codex-review-recovery.log
codex-review-output.md
codex-review-recovery.md
review-findings.md
codex-review-exit-code.txt
retention-days: 7
if-no-files-found: ignore
+3 -2
View File
@@ -6,7 +6,7 @@ on:
workflow_call:
inputs:
provider:
description: Provider key, for example "claude" or "codex"
description: Provider key, for example "claude"
required: true
type: string
result_artifact_name:
@@ -71,7 +71,8 @@ jobs:
const post = require('./.github/scripts/post-pr-comment.js');
const provider = process.env.PROVIDER;
const providerTitle = provider === 'codex' ? 'Codex' : 'Claude';
const providerTitle =
provider.charAt(0).toUpperCase() + provider.slice(1);
const body = fs.readFileSync(commentFile, 'utf8');
const prNumber = parseInt(
fs.readFileSync(prNumberFile, 'utf8').trim(),
+2 -2
View File
@@ -1,4 +1,4 @@
name: facebook/rocksdb/benchmark-linux
name: meta/rocksdb/benchmark-linux
on: workflow_dispatch
permissions: {}
# FIXME: Disabled temporarily
@@ -6,7 +6,7 @@ permissions: {}
# - cron: 7 */2 * * * # At minute 7 past every 2nd hour
jobs:
benchmark-linux:
if: ${{ github.repository_owner == 'facebook' }}
if: ${{ github.repository_owner == 'meta' }}
runs-on: ubuntu-latest # FIXME: change this back to self-hosted when ready
steps:
- uses: actions/checkout@v4.1.0
+2 -2
View File
@@ -7,11 +7,11 @@ permissions: {}
jobs:
clang-tidy:
if: github.repository_owner == 'facebook'
if: github.repository_owner == 'meta'
runs-on:
labels: 4-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
image: ghcr.io/meta/rocksdb_ubuntu:24.1
options: --shm-size=8gb
steps:
- uses: actions/checkout@v4.1.0
+1 -1
View File
@@ -8,7 +8,7 @@ name: Claude Code Review
on:
workflow_run:
workflows: ["facebook/rocksdb/pr-jobs"]
workflows: ["meta/rocksdb/pr-jobs"]
types: [completed]
# The early pull_request_target path is limited to same-repo PRs by the job
@@ -1,24 +0,0 @@
# Codex Code Review — Comment Posting Workflow
#
# Thin wrapper around .github/workflows/ai-review-comment.yml so the provider
# specific workflow name stays stable while the shared implementation lives in
# one reusable workflow.
name: Post Codex Review Comment
on:
workflow_run:
workflows: ["Codex Code Review"]
types: [completed]
permissions:
pull-requests: write
issues: write
jobs:
review-comment:
uses: ./.github/workflows/ai-review-comment.yml
with:
provider: codex
result_artifact_name: codex-review-result
comment_file: codex-review-comment.md
-63
View File
@@ -1,63 +0,0 @@
# Codex Code Review — Analysis Workflow
#
# Thin wrapper around .github/workflows/ai-review-analysis.yml so provider
# specific triggers and workflow_dispatch inputs stay stable while the shared
# implementation lives in one reusable workflow.
name: Codex Code Review
on:
# Automatic PR reviews are temporarily disabled while Codex authentication is
# repaired. Manual review requests remain available through issue comments
# and workflow_dispatch.
issue_comment:
types: [created]
workflow_dispatch:
inputs:
pr_number:
description: PR number to review
required: true
type: number
model:
description: Codex model to use
required: false
type: choice
options:
- gpt-5.5
- gpt-5.3-codex
- gpt-5.2-codex
default: gpt-5.5
thinking_budget:
description: Override MAX_THINKING_TOKENS (blank = auto-classify)
required: false
type: string
default: ""
permissions:
contents: read
# The shared analysis workflow polls check state, inspects prior runs, and
# reads PR metadata. Keep those permissions read-only in the caller.
actions: read
checks: read
pull-requests: read
jobs:
review:
if: >-
github.event_name != 'pull_request_target' ||
github.event.pull_request.head.repo.full_name == github.repository
uses: ./.github/workflows/ai-review-analysis.yml
with:
provider: codex
display_name: Codex
review_command: /codex-review
query_command: /codex-query
result_artifact_name: codex-review-result
comment_file: codex-review-comment.md
default_model: gpt-5.5
selected_model: ${{ github.event_name == 'workflow_dispatch' && inputs.model || '' }}
thinking_budget: ${{ github.event_name == 'workflow_dispatch' && inputs.thinking_budget || '' }}
dispatch_pr_number: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || '' }}
secrets: inherit
+2 -3
View File
@@ -1,11 +1,11 @@
name: facebook/rocksdb/nightly-candidate
name: meta/rocksdb/nightly-candidate
on: workflow_dispatch
permissions: {}
jobs:
# These jobs would be in nightly but are failing or otherwise broken for
# some reason.
build-linux-arm-test-full:
if: ${{ github.repository_owner == 'facebook' }}
if: ${{ github.repository_owner == 'meta' }}
runs-on:
labels: arm64large
container:
@@ -16,4 +16,3 @@ jobs:
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/install-gflags"
- run: make V=1 J=4 -j4 check
- uses: "./.github/actions/post-steps"
+73 -30
View File
@@ -1,4 +1,4 @@
name: facebook/rocksdb/nightly
name: meta/rocksdb/nightly
on:
schedule:
- cron: 0 9 * * *
@@ -6,11 +6,11 @@ on:
permissions: {}
jobs:
build-format-compatible:
if: ${{ github.repository_owner == 'facebook' }}
if: ${{ github.repository_owner == 'meta' }}
runs-on:
labels: 16-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
image: ghcr.io/meta/rocksdb_ubuntu:22.1
options: --shm-size=32gb
steps:
- uses: actions/checkout@v4.1.0
@@ -26,13 +26,30 @@ jobs:
mkdir /dev/shm/rocksdb
git config --global --add safe.directory /__w/rocksdb/rocksdb
tools/check_format_compatible.sh
- uses: "./.github/actions/post-steps"
- name: Check that old facebook/rocksdb URLs still redirect
# Alerts the RocksDB team if GitHub stops redirecting the pre-transfer
# repo URL to its current location (or the old name gets taken by
# another repo). Existing releases (e.g. dependency download URLs in
# java/Makefile), existing clones, and external links still use the old
# URL, so they would all break.
if: ${{ !cancelled() }}
run: |-
if ! result=$(curl -sSL --retry 3 --retry-all-errors -o /dev/null \
-w '%{http_code} %{url_effective}' https://github.com/facebook/rocksdb); then
echo "::error::Failed to fetch https://github.com/facebook/rocksdb (network problem?)"
exit 1
fi
echo "Old URL resolved to: $result"
if [ "$result" != "200 https://github.com/meta/rocksdb" ]; then
echo "::error::https://github.com/facebook/rocksdb no longer redirects to https://github.com/meta/rocksdb. Builds of existing releases, existing clones, and external links that use the old URL are likely broken."
exit 1
fi
build-linux-non-shm:
if: ${{ github.repository_owner == 'facebook' }}
if: ${{ github.repository_owner == 'meta' }}
runs-on:
labels: 16-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
image: ghcr.io/meta/rocksdb_ubuntu:22.1
options: --shm-size=32gb
env:
TEST_TMPDIR: "/tmp/rocksdb_test_tmp"
@@ -40,13 +57,12 @@ jobs:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- run: make V=1 -j32 check
- uses: "./.github/actions/post-steps"
build-linux-clang-21-asan-ubsan-with-folly:
if: ${{ github.repository_owner == 'facebook' }}
if: ${{ github.repository_owner == 'meta' }}
runs-on:
labels: 16-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
image: ghcr.io/meta/rocksdb_ubuntu:24.1
options: --shm-size=32gb
env:
CC: clang-21
@@ -77,13 +93,12 @@ jobs:
make build_folly
shell: bash
- run: LIB_MODE=static USE_CLANG=1 USE_FOLLY=1 COMPILE_WITH_UBSAN=1 COMPILE_WITH_ASAN=1 make -j32 check
- uses: "./.github/actions/post-steps"
build-linux-cmake-with-folly:
if: ${{ github.repository_owner == 'facebook' }}
if: ${{ github.repository_owner == 'meta' }}
runs-on:
labels: 16-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
image: ghcr.io/meta/rocksdb_ubuntu:22.1
options: --shm-size=32gb
steps:
- uses: actions/checkout@v4.1.0
@@ -96,13 +111,12 @@ jobs:
with:
cache-hit: ${{ steps.cache-folly.outputs.cache-hit }}
- run: "(mkdir build && cd build && cmake -DUSE_FOLLY=1 -DWITH_GFLAGS=1 -DROCKSDB_BUILD_SHARED=0 .. && make VERBOSE=1 -j20 && ctest -j20)"
- uses: "./.github/actions/post-steps"
build-linux-release-with-folly:
if: ${{ github.repository_owner == 'facebook' }}
if: ${{ github.repository_owner == 'meta' }}
runs-on:
labels: 16-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
image: ghcr.io/meta/rocksdb_ubuntu:22.1
options: --shm-size=32gb
steps:
- uses: actions/checkout@v4.1.0
@@ -112,17 +126,51 @@ jobs:
- run: "DEBUG_LEVEL=0 make -j20 build_folly"
- run: "USE_FOLLY=1 LIB_MODE=static DEBUG_LEVEL=0 V=1 make -j20 release"
- run: "(mkdir build && cd build && cmake -DUSE_FOLLY=1 -DWITH_GFLAGS=1 -DROCKSDB_BUILD_SHARED=0 -DCMAKE_BUILD_TYPE=Release .. && make VERBOSE=1 -j20 && ctest -j20)"
- uses: "./.github/actions/post-steps"
build-windows-msvc-avx2:
if: ${{ github.repository_owner == 'facebook' }}
if: ${{ github.repository_owner == 'meta' }}
runs-on: windows-8-core
env:
CMAKE_PORTABLE: AVX2
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/windows-build-steps"
build-macos-cmake:
if: ${{ github.repository_owner == 'meta' }}
runs-on: macos-15-xlarge
strategy:
matrix:
run_sharded_tests: [0, 1, 2, 3]
steps:
- uses: actions/checkout@v4.1.0
- uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: 16.4.0
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: macos-cmake
- uses: "./.github/actions/increase-max-open-files-on-macos"
- uses: "./.github/actions/install-gflags-on-macos"
- uses: "./.github/actions/pre-steps-macos"
- name: cmake generate project file
run: ulimit -S -n `ulimit -H -n` && mkdir build && cd build && cmake -DWITH_GFLAGS=1 ..
- name: Build tests
run: cd build && make VERBOSE=1 -j8
- name: Run shard 0 out of 4 test shards
run: ulimit -S -n `ulimit -H -n` && cd build && ctest -j8 -I 0,,4
if: ${{ matrix.run_sharded_tests == 0 }}
- name: Run shard 1 out of 4 test shards
run: ulimit -S -n `ulimit -H -n` && cd build && ctest -j8 -I 1,,4
if: ${{ matrix.run_sharded_tests == 1 }}
- name: Run shard 2 out of 4 test shards
run: ulimit -S -n `ulimit -H -n` && cd build && ctest -j8 -I 2,,4
if: ${{ matrix.run_sharded_tests == 2 }}
- name: Run shard 3 out of 4 test shards
run: ulimit -S -n `ulimit -H -n` && cd build && ctest -j8 -I 3,,4
if: ${{ matrix.run_sharded_tests == 3 }}
- uses: "./.github/actions/teardown-ccache"
if: always()
build-linux-arm-test-full:
if: ${{ github.repository_owner == 'facebook' }}
if: ${{ github.repository_owner == 'meta' }}
runs-on:
labels: 4-core-ubuntu-arm
steps:
@@ -130,9 +178,8 @@ jobs:
- uses: "./.github/actions/pre-steps"
- run: sudo apt-get update && sudo apt-get install -y build-essential libgflags-dev
- run: make V=1 J=4 -j4 check
- uses: "./.github/actions/post-steps"
build-linux-arm-crashtest:
if: ${{ github.repository_owner == 'facebook' }}
if: ${{ github.repository_owner == 'meta' }}
runs-on:
labels: 4-core-ubuntu-arm
steps:
@@ -154,26 +201,24 @@ jobs:
- run: ulimit -S -n `ulimit -H -n` && make V=1 -j8 CRASH_TEST_EXT_ARGS='--duration=1800 --max_key=2500000' blackbox_crash_test_with_atomic_flush
- run: rm -rf /dev/shm/rocksdb.*
- run: ulimit -S -n `ulimit -H -n` && make V=1 -j8 CRASH_TEST_EXT_ARGS='--duration=1800 --max_key=2500000' blackbox_crash_test_with_multiops_wc_txn
- uses: "./.github/actions/post-steps"
build-examples:
if: ${{ github.repository_owner == 'facebook' }}
if: ${{ github.repository_owner == 'meta' }}
runs-on:
labels: 4-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
image: ghcr.io/meta/rocksdb_ubuntu:22.1
options: --shm-size=8gb
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- name: Build examples
run: make V=1 -j4 static_lib && cd examples && make V=1 -j4
- uses: "./.github/actions/post-steps"
build-fuzzers:
if: ${{ github.repository_owner == 'facebook' }}
if: ${{ github.repository_owner == 'meta' }}
runs-on:
labels: 4-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
image: ghcr.io/meta/rocksdb_ubuntu:24.1
options: --shm-size=8gb
steps:
- uses: actions/checkout@v4.1.0
@@ -182,13 +227,12 @@ jobs:
run: CC=clang-21 CXX=clang++-21 USE_CLANG=1 make -j4 static_lib
- name: Build fuzzers
run: cd fuzz && make sst_file_writer_fuzzer db_fuzzer db_map_fuzzer
- uses: "./.github/actions/post-steps"
build-linux-cmake-with-folly-lite:
if: ${{ github.repository_owner == 'facebook' }}
if: ${{ github.repository_owner == 'meta' }}
runs-on:
labels: 16-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
image: ghcr.io/meta/rocksdb_ubuntu:22.1
options: --shm-size=32gb
steps:
- uses: actions/checkout@v4.1.0
@@ -196,4 +240,3 @@ jobs:
- uses: "./.github/actions/cache-getdeps-downloads"
- uses: "./.github/actions/setup-folly"
- run: "(mkdir build && cd build && cmake -DUSE_FOLLY_LITE=1 -DWITH_GFLAGS=1 -DCMAKE_CXX_FLAGS=-DGLOG_USE_GLOG_EXPORT .. && make VERBOSE=1 -j20 && ctest -j20)"
- uses: "./.github/actions/post-steps"
+3 -5
View File
@@ -1,4 +1,4 @@
name: facebook/rocksdb/pr-jobs-candidate
name: meta/rocksdb/pr-jobs-candidate
on: workflow_dispatch
permissions: {}
jobs:
@@ -6,7 +6,7 @@ jobs:
# some reason.
# =========================== ARM Jobs ============================ #
build-linux-arm:
if: ${{ github.repository_owner == 'facebook' }}
if: ${{ github.repository_owner == 'meta' }}
runs-on:
labels: arm64large # GitHub hosted ARM runners do not yet exist
steps:
@@ -14,9 +14,8 @@ jobs:
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/install-gflags"
- run: ROCKSDBTESTS_PLATFORM_DEPENDENT=only make V=1 J=4 -j4 all_but_some_tests check_some
- uses: "./.github/actions/post-steps"
build-linux-arm-cmake-no_test_run:
if: ${{ github.repository_owner == 'facebook' }}
if: ${{ github.repository_owner == 'meta' }}
runs-on:
labels: arm64large # GitHub hosted ARM runners do not yet exist
env:
@@ -44,4 +43,3 @@ jobs:
cd build
cmake -DJNI=1 -DCMAKE_BUILD_TYPE=Release -DWITH_GFLAGS=1 ..
make -j4 rocksdb rocksdbjni
- uses: "./.github/actions/post-steps"
+49 -115
View File
@@ -1,9 +1,9 @@
name: facebook/rocksdb/pr-jobs
name: meta/rocksdb/pr-jobs
on: [push, pull_request]
permissions: {}
env:
# Set to a job name to run only that job (on any repo), or leave empty for
# normal behavior (all jobs on facebook repo only).
# normal behavior (all jobs on meta/rocksdb repo only).
ONLY_JOB: ''
jobs:
config:
@@ -19,7 +19,7 @@ jobs:
# workflow minimizes the problem because it will be suspicious if there are
# no GHA results.
#
# The if: ${{ github.repository_owner == 'facebook' }} lines prevent the
# The if: ${{ github.repository_owner == 'meta' }} lines prevent the
# jobs from attempting to run on repo forks, because of a few problems:
# * runs-on labels are repository (owner) specific, so the job might wait
# for days waiting for a runner that simply isn't available.
@@ -45,7 +45,7 @@ jobs:
# ======================== Fast Initial Checks ====================== #
check-format-and-targets:
if: needs.config.outputs.only_job == 'check-format-and-targets' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
if: needs.config.outputs.only_job == 'check-format-and-targets' || (needs.config.outputs.only_job == '' && github.repository_owner == 'meta')
needs: config
runs-on: ubuntu-24.04
steps:
@@ -66,8 +66,8 @@ jobs:
clang-format --version
- name: Download clang-format-diff.py
# Mirrored on the "third-party-deps" GitHub release; add/update via
# https://github.com/facebook/rocksdb/releases/edit/third-party-deps
run: wget --tries=3 --retry-on-host-error https://github.com/facebook/rocksdb/releases/download/third-party-deps/clang-format-diff.py
# https://github.com/meta/rocksdb/releases/edit/third-party-deps
run: wget --tries=3 --retry-on-host-error https://github.com/meta/rocksdb/releases/download/third-party-deps/clang-format-diff.py
- name: Check format
run: VERBOSE_CHECK=1 make check-format
- name: Compare buckify output
@@ -86,12 +86,12 @@ jobs:
SANITY_CHECK=1 LONG_TEST=1 tools/check_format_compatible.sh
# ========================= Linux With Tests ======================== #
build-linux:
if: needs.config.outputs.only_job == 'build-linux' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
if: needs.config.outputs.only_job == 'build-linux' || (needs.config.outputs.only_job == '' && github.repository_owner == 'meta')
needs: config
runs-on:
labels: 16-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
image: ghcr.io/meta/rocksdb_ubuntu:22.1
options: --shm-size=32gb
steps:
- uses: actions/checkout@v4.1.0
@@ -102,14 +102,13 @@ jobs:
- run: make V=1 J=32 -j32 check
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-linux-cmake-mingw:
if: needs.config.outputs.only_job == 'build-linux-cmake-mingw' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
if: needs.config.outputs.only_job == 'build-linux-cmake-mingw' || (needs.config.outputs.only_job == '' && github.repository_owner == 'meta')
needs: config
runs-on:
labels: 4-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
image: ghcr.io/meta/rocksdb_ubuntu:24.1
options: --shm-size=8gb
steps:
- uses: actions/checkout@v4.1.0
@@ -127,14 +126,13 @@ jobs:
mkdir build && cd build && cmake -DJNI=1 -DWITH_GFLAGS=OFF .. -DCMAKE_C_COMPILER=x86_64-w64-mingw32-gcc -DCMAKE_CXX_COMPILER=x86_64-w64-mingw32-g++ -DCMAKE_SYSTEM_NAME=Windows && make -j4 rocksdb rocksdbjni
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-linux-make-with-folly:
if: needs.config.outputs.only_job == 'build-linux-make-with-folly' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
if: needs.config.outputs.only_job == 'build-linux-make-with-folly' || (needs.config.outputs.only_job == '' && github.repository_owner == 'meta')
needs: config
runs-on:
labels: 32-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
image: ghcr.io/meta/rocksdb_ubuntu:22.1
options: --shm-size=64gb
steps:
- uses: actions/checkout@v4.1.0
@@ -152,14 +150,13 @@ jobs:
- run: USE_FOLLY=1 LIB_MODE=static V=1 make -j64 check
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-linux-make-with-folly-lite-no-test:
if: needs.config.outputs.only_job == 'build-linux-make-with-folly-lite-no-test' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
if: needs.config.outputs.only_job == 'build-linux-make-with-folly-lite-no-test' || (needs.config.outputs.only_job == '' && github.repository_owner == 'meta')
needs: config
runs-on:
labels: 16-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
image: ghcr.io/meta/rocksdb_ubuntu:22.1
options: --shm-size=32gb
steps:
- uses: actions/checkout@v4.1.0
@@ -172,14 +169,13 @@ jobs:
- run: USE_FOLLY_LITE=1 EXTRA_CXXFLAGS=-DGLOG_USE_GLOG_EXPORT V=1 make -j32 all
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-linux-cmake-with-folly-coroutines:
if: needs.config.outputs.only_job == 'build-linux-cmake-with-folly-coroutines' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
if: needs.config.outputs.only_job == 'build-linux-cmake-with-folly-coroutines' || (needs.config.outputs.only_job == '' && github.repository_owner == 'meta')
needs: config
runs-on:
labels: 32-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
image: ghcr.io/meta/rocksdb_ubuntu:22.1
options: --shm-size=64gb
steps:
- uses: actions/checkout@v4.1.0
@@ -197,9 +193,8 @@ jobs:
- run: "(mkdir build && cd build && cmake -DUSE_COROUTINES=1 -DWITH_GFLAGS=1 -DROCKSDB_BUILD_SHARED=0 -DPORTABLE=ON .. && make VERBOSE=1 -j64 && ctest -j64)"
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-linux-cmake-with-benchmark-no-thread-status:
if: needs.config.outputs.only_job == 'build-linux-cmake-with-benchmark-no-thread-status' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
if: needs.config.outputs.only_job == 'build-linux-cmake-with-benchmark-no-thread-status' || (needs.config.outputs.only_job == '' && github.repository_owner == 'meta')
needs: config
runs-on:
labels: 16-core-ubuntu
@@ -208,7 +203,7 @@ jobs:
matrix:
shard: [0, 1, 2, 3]
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
image: ghcr.io/meta/rocksdb_ubuntu:22.1
options: --shm-size=32gb
steps:
- uses: actions/checkout@v4.1.0
@@ -222,16 +217,13 @@ jobs:
run: cd build && ctest -j20 -I ${{ matrix.shard }},,4
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
with:
artifact-prefix: "${{ github.job }}-shard${{ matrix.shard }}"
build-linux-encrypted_env-no_compression:
if: needs.config.outputs.only_job == 'build-linux-encrypted_env-no_compression' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
if: needs.config.outputs.only_job == 'build-linux-encrypted_env-no_compression' || (needs.config.outputs.only_job == '' && github.repository_owner == 'meta')
needs: config
runs-on:
labels: 16-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
image: ghcr.io/meta/rocksdb_ubuntu:22.1
options: --shm-size=32gb
steps:
- uses: actions/checkout@v4.1.0
@@ -243,15 +235,14 @@ jobs:
- run: "./sst_dump --help | grep -E -q 'Supported built-in compression types: kNoCompression$' # Verify no compiled in compression\n"
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
# ======================== Linux No Test Runs ======================= #
build-linux-release:
if: needs.config.outputs.only_job == 'build-linux-release' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
if: needs.config.outputs.only_job == 'build-linux-release' || (needs.config.outputs.only_job == '' && github.repository_owner == 'meta')
needs: config
runs-on:
labels: 16-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
image: ghcr.io/meta/rocksdb_ubuntu:22.1
options: --shm-size=32gb
steps:
- uses: actions/checkout@v4.1.0
@@ -276,14 +267,13 @@ jobs:
- run: if ./trace_analyzer --version; then false; else true; fi
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-linux-clang-13-no_test_run:
if: needs.config.outputs.only_job == 'build-linux-clang-13-no_test_run' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
if: needs.config.outputs.only_job == 'build-linux-clang-13-no_test_run' || (needs.config.outputs.only_job == '' && github.repository_owner == 'meta')
needs: config
runs-on:
labels: 8-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
image: ghcr.io/meta/rocksdb_ubuntu:22.1
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
@@ -298,14 +288,13 @@ jobs:
- run: CC=clang-13 CXX=clang++-13 USE_CLANG=1 EXTRA_CXXFLAGS=-stdlib=libc++ EXTRA_LDFLAGS=-stdlib=libc++ DEBUG_LEVEL=0 make -j32 shared_lib
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-linux-clang-21-no_test_run:
if: needs.config.outputs.only_job == 'build-linux-clang-21-no_test_run' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
if: needs.config.outputs.only_job == 'build-linux-clang-21-no_test_run' || (needs.config.outputs.only_job == '' && github.repository_owner == 'meta')
needs: config
runs-on:
labels: 16-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
image: ghcr.io/meta/rocksdb_ubuntu:24.1
options: --shm-size=32gb
steps:
- uses: actions/checkout@v4.1.0
@@ -365,14 +354,13 @@ jobs:
CLANG_FORMAT_BINARY=clang-format-21
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-linux-gcc-14-no_test_run:
if: needs.config.outputs.only_job == 'build-linux-gcc-14-no_test_run' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
if: needs.config.outputs.only_job == 'build-linux-gcc-14-no_test_run' || (needs.config.outputs.only_job == '' && github.repository_owner == 'meta')
needs: config
runs-on:
labels: 16-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
image: ghcr.io/meta/rocksdb_ubuntu:24.1
options: --shm-size=32gb
steps:
- uses: actions/checkout@v4.1.0
@@ -383,12 +371,11 @@ jobs:
- run: CC=gcc-14 CXX=g++-14 V=1 make -j32 all microbench
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
# ======================== Linux Other Checks ======================= #
build-linux-unity-and-headers:
if: needs.config.outputs.only_job == 'build-linux-unity-and-headers' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
if: needs.config.outputs.only_job == 'build-linux-unity-and-headers' || (needs.config.outputs.only_job == '' && github.repository_owner == 'meta')
needs: config
runs-on:
labels: 4-core-ubuntu
@@ -406,9 +393,8 @@ jobs:
- run: make V=1 -j8 -k check-headers
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-linux-mini-crashtest:
if: needs.config.outputs.only_job == 'build-linux-mini-crashtest' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
if: needs.config.outputs.only_job == 'build-linux-mini-crashtest' || (needs.config.outputs.only_job == '' && github.repository_owner == 'meta')
needs: config
runs-on:
labels: 4-core-ubuntu
@@ -421,7 +407,7 @@ jobs:
- crash_test_target: blackbox_crash_test
crash_duration: 240
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
image: ghcr.io/meta/rocksdb_ubuntu:22.1
options: --shm-size=8gb --ulimit nofile=1048576:1048576
steps:
- uses: actions/checkout@v4.1.0
@@ -442,12 +428,9 @@ jobs:
USE_COROUTINES=1 USE_FOLLY=1 LIB_MODE=static make V=1 -j8 CRASH_TEST_EXT_ARGS='--duration=${{ matrix.crash_duration }} --max_key=2500000' ${{ matrix.crash_test_target }}
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
with:
artifact-prefix: "${{ github.job }}-${{ matrix.crash_test_target }}"
# ======================= Linux with Sanitizers ===================== #
build-linux-clang21-asan-ubsan:
if: needs.config.outputs.only_job == 'build-linux-clang21-asan-ubsan' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
if: needs.config.outputs.only_job == 'build-linux-clang21-asan-ubsan' || (needs.config.outputs.only_job == '' && github.repository_owner == 'meta')
needs: config
runs-on:
labels: 32-core-ubuntu
@@ -456,7 +439,7 @@ jobs:
matrix:
shard: [0, 1, 2]
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
image: ghcr.io/meta/rocksdb_ubuntu:24.1
options: --shm-size=64gb
steps:
- uses: actions/checkout@v4.1.0
@@ -470,11 +453,8 @@ jobs:
CI_TOTAL_SHARDS: 3
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
with:
artifact-prefix: "${{ github.job }}-shard${{ matrix.shard }}"
build-linux-clang21-mini-tsan:
if: needs.config.outputs.only_job == 'build-linux-clang21-mini-tsan' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
if: needs.config.outputs.only_job == 'build-linux-clang21-mini-tsan' || (needs.config.outputs.only_job == '' && github.repository_owner == 'meta')
needs: config
runs-on:
labels: 32-core-ubuntu
@@ -483,7 +463,7 @@ jobs:
matrix:
shard: [0, 1, 2]
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
image: ghcr.io/meta/rocksdb_ubuntu:24.1
options: --shm-size=64gb
steps:
- uses: actions/checkout@v4.1.0
@@ -497,16 +477,13 @@ jobs:
CI_TOTAL_SHARDS: 3
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
with:
artifact-prefix: "${{ github.job }}-shard${{ matrix.shard }}"
build-linux-static_lib-alt_namespace-status_checked:
if: needs.config.outputs.only_job == 'build-linux-static_lib-alt_namespace-status_checked' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
if: needs.config.outputs.only_job == 'build-linux-static_lib-alt_namespace-status_checked' || (needs.config.outputs.only_job == '' && github.repository_owner == 'meta')
needs: config
runs-on:
labels: 16-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
image: ghcr.io/meta/rocksdb_ubuntu:22.1
options: --shm-size=32gb
steps:
- uses: actions/checkout@v4.1.0
@@ -517,10 +494,10 @@ jobs:
- run: ASSERT_STATUS_CHECKED=1 TEST_UINT128_COMPAT=1 ROCKSDB_MODIFY_NPHASH=1 LIB_MODE=static OPT="-DROCKSDB_USE_STD_SEMAPHORES -DROCKSDB_NAMESPACE=alternative_rocksdb_ns" make V=1 -j24 check
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
# ========================= MacOS build only ======================== #
# NOTE: build-macos-cmake (with tests) is in "nightly" to save resources
build-macos:
if: needs.config.outputs.only_job == 'build-macos' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
if: needs.config.outputs.only_job == 'build-macos' || (needs.config.outputs.only_job == '' && github.repository_owner == 'meta')
needs: config
runs-on: macos-15-xlarge
env:
@@ -540,49 +517,10 @@ jobs:
run: ulimit -S -n `ulimit -H -n` && make V=1 J=16 -j8 all
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
# ========================= MacOS with Tests ======================== #
build-macos-cmake:
if: needs.config.outputs.only_job == 'build-macos-cmake' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on: macos-15-xlarge
strategy:
matrix:
run_sharded_tests: [0, 1, 2, 3]
steps:
- uses: actions/checkout@v4.1.0
- uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: 16.4.0
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: macos-cmake
- uses: "./.github/actions/increase-max-open-files-on-macos"
- uses: "./.github/actions/install-gflags-on-macos"
- uses: "./.github/actions/pre-steps-macos"
- name: cmake generate project file
run: ulimit -S -n `ulimit -H -n` && mkdir build && cd build && cmake -DWITH_GFLAGS=1 ..
- name: Build tests
run: cd build && make VERBOSE=1 -j8
- name: Run shard 0 out of 4 test shards
run: ulimit -S -n `ulimit -H -n` && cd build && ctest -j8 -I 0,,4
if: ${{ matrix.run_sharded_tests == 0 }}
- name: Run shard 1 out of 4 test shards
run: ulimit -S -n `ulimit -H -n` && cd build && ctest -j8 -I 1,,4
if: ${{ matrix.run_sharded_tests == 1 }}
- name: Run shard 2 out of 4 test shards
run: ulimit -S -n `ulimit -H -n` && cd build && ctest -j8 -I 2,,4
if: ${{ matrix.run_sharded_tests == 2 }}
- name: Run shard 3 out of 4 test shards
run: ulimit -S -n `ulimit -H -n` && cd build && ctest -j8 -I 3,,4
if: ${{ matrix.run_sharded_tests == 3 }}
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
# ======================== Windows with Tests ======================= #
# NOTE: some windows jobs are in "nightly" to save resources
build-windows-msvc:
if: needs.config.outputs.only_job == 'build-windows-msvc' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
if: needs.config.outputs.only_job == 'build-windows-msvc' || (needs.config.outputs.only_job == '' && github.repository_owner == 'meta')
needs: config
runs-on: windows-8-core
strategy:
@@ -608,12 +546,12 @@ jobs:
run-java: ${{ matrix.run_java }}
# ============================ Java Jobs ============================ #
build-linux-java:
if: needs.config.outputs.only_job == 'build-linux-java' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
if: needs.config.outputs.only_job == 'build-linux-java' || (needs.config.outputs.only_job == '' && github.repository_owner == 'meta')
needs: config
runs-on:
labels: 4-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
image: ghcr.io/meta/rocksdb_ubuntu:22.1
options: --shm-size=8gb
steps:
- uses: actions/checkout@v4.1.0
@@ -631,12 +569,12 @@ jobs:
- uses: "./.github/actions/teardown-ccache"
if: always()
build-linux-java-static:
if: needs.config.outputs.only_job == 'build-linux-java-static' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
if: needs.config.outputs.only_job == 'build-linux-java-static' || (needs.config.outputs.only_job == '' && github.repository_owner == 'meta')
needs: config
runs-on:
labels: 4-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
image: ghcr.io/meta/rocksdb_ubuntu:22.1
options: --shm-size=8gb
steps:
- uses: actions/checkout@v4.1.0
@@ -654,7 +592,7 @@ jobs:
- uses: "./.github/actions/teardown-ccache"
if: always()
build-macos-java:
if: needs.config.outputs.only_job == 'build-macos-java' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
if: needs.config.outputs.only_job == 'build-macos-java' || (needs.config.outputs.only_job == '' && github.repository_owner == 'meta')
needs: config
runs-on: macos-15-xlarge
env:
@@ -680,9 +618,8 @@ jobs:
run: make V=1 J=16 -j16 jtest
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-macos-java-static:
if: needs.config.outputs.only_job == 'build-macos-java-static' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
if: needs.config.outputs.only_job == 'build-macos-java-static' || (needs.config.outputs.only_job == '' && github.repository_owner == 'meta')
needs: config
runs-on: macos-15-xlarge
steps:
@@ -706,9 +643,8 @@ jobs:
run: make V=1 J=16 -j16 rocksdbjavastaticosx
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-macos-java-static-universal:
if: needs.config.outputs.only_job == 'build-macos-java-static-universal' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
if: needs.config.outputs.only_job == 'build-macos-java-static-universal' || (needs.config.outputs.only_job == '' && github.repository_owner == 'meta')
needs: config
runs-on: macos-15-xlarge
steps:
@@ -732,9 +668,8 @@ jobs:
run: make V=1 J=16 -j16 rocksdbjavastaticosx_ub
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-linux-java-pmd:
if: needs.config.outputs.only_job == 'build-linux-java-pmd' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
if: needs.config.outputs.only_job == 'build-linux-java-pmd' || (needs.config.outputs.only_job == '' && github.repository_owner == 'meta')
needs: config
runs-on:
labels: 4-core-ubuntu
@@ -761,7 +696,7 @@ jobs:
name: maven-site
path: "${{ github.workspace }}/java/target/site"
build-linux-arm:
if: needs.config.outputs.only_job == 'build-linux-arm' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
if: needs.config.outputs.only_job == 'build-linux-arm' || (needs.config.outputs.only_job == '' && github.repository_owner == 'meta')
needs: config
runs-on:
labels: 4-core-ubuntu-arm
@@ -777,4 +712,3 @@ jobs:
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
+3 -4
View File
@@ -1,4 +1,4 @@
name: facebook/rocksdb/weekly
name: meta/rocksdb/weekly
on:
schedule:
- cron: 0 9 * * 0
@@ -6,15 +6,14 @@ on:
permissions: {}
jobs:
build-linux-valgrind:
if: ${{ github.repository_owner == 'facebook' }}
if: ${{ github.repository_owner == 'meta' }}
runs-on:
labels: 16-core-ubuntu
timeout-minutes: 840
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
image: ghcr.io/meta/rocksdb_ubuntu:22.1
options: --shm-size=32gb
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- run: make V=1 -j20 valgrind_test
- uses: "./.github/actions/post-steps"
+35 -16
View File
@@ -259,6 +259,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"tools/sst_dump_tool.cc",
"trace_replay/block_cache_tracer.cc",
"trace_replay/io_tracer.cc",
"trace_replay/trace_analyzer.cc",
"trace_replay/trace_record.cc",
"trace_replay/trace_record_handler.cc",
"trace_replay/trace_record_result.cc",
@@ -281,6 +282,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"util/file_checksum_helper.cc",
"util/hash.cc",
"util/io_dispatcher_imp.cc",
"util/io_prefetch_scheduler.cc",
"util/murmurhash.cc",
"util/random.cc",
"util/rate_limiter.cc",
@@ -314,6 +316,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"utilities/convenience/info_log_finder.cc",
"utilities/copy_engine/copy_engine.cc",
"utilities/counted_fs.cc",
"utilities/db_split_merge.cc",
"utilities/debug.cc",
"utilities/env_mirror.cc",
"utilities/env_timed.cc",
@@ -321,6 +324,8 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"utilities/fault_injection_fs.cc",
"utilities/fault_injection_secondary_cache.cc",
"utilities/leveldb_options/leveldb_options.cc",
"utilities/live_tune/live_tune_options_helper.cc",
"utilities/live_tune/live_tuner.cc",
"utilities/memory/memory_util.cc",
"utilities/merge_operators.cc",
"utilities/merge_operators/bytesxor.cc",
@@ -385,22 +390,22 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"utilities/write_batch_with_index/write_batch_with_index.cc",
"utilities/write_batch_with_index/write_batch_with_index_internal.cc",
], deps=[
"//folly/container:f14_hash",
"//folly/coro:baton",
"//folly/coro:blocking_wait",
"//folly/coro:collect",
"//folly/coro:coroutine",
"//folly/coro:nothrow",
"//folly/coro:task",
"//folly/executors/thread_factory:named_thread_factory",
"//folly/executors:io_executor",
"//folly/executors:io_thread_pool_executor",
"//folly/io/async:async_base",
"//folly/io/async:event_base_manager",
"//folly/io/async:io_uring_backend",
"//folly/io/async:request_context",
"//folly/synchronization:distributed_mutex",
"//folly:executor",
"fbsource//xplat/folly/container:f14_hash",
"fbsource//xplat/folly/coro:baton",
"fbsource//xplat/folly/coro:blocking_wait",
"fbsource//xplat/folly/coro:collect",
"fbsource//xplat/folly/coro:coroutine",
"fbsource//xplat/folly/coro:nothrow",
"fbsource//xplat/folly/coro:task",
"fbsource//xplat/folly/executors/thread_factory:named_thread_factory",
"fbsource//xplat/folly/executors:io_executor",
"fbsource//xplat/folly/executors:io_thread_pool_executor",
"fbsource//xplat/folly/io/async:async_base",
"fbsource//xplat/folly/io/async:event_base_manager",
"fbsource//xplat/folly/io/async:io_uring_backend",
"fbsource//xplat/folly/io/async:request_context",
"fbsource//xplat/folly/synchronization:distributed_mutex",
"fbsource//xplat/folly:executor",
], headers=glob(["**/*.h"]), link_whole=False, extra_test_libs=False)
cpp_library_wrapper(name="rocksdb_whole_archive_lib", srcs=[], deps=[":rocksdb_lib"], headers=[], link_whole=True, extra_test_libs=False)
@@ -528,6 +533,8 @@ rocks_cpp_library_wrapper(name="rocksdb_stress_lib", srcs=[
cpp_binary_wrapper(name="ldb", srcs=["tools/ldb.cc"], deps=[":rocksdb_tools_lib"], extra_preprocessor_flags=[], extra_bench_libs=False)
cpp_binary_wrapper(name="db_split_merge", srcs=["tools/db_split_merge.cc"], deps=[":rocksdb_lib"], extra_preprocessor_flags=[], extra_bench_libs=False)
cpp_binary_wrapper(name="db_stress", srcs=["db_stress_tool/db_stress.cc"], deps=[":rocksdb_stress_lib"], extra_preprocessor_flags=[], extra_bench_libs=False)
cpp_binary_wrapper(name="db_bench", srcs=["tools/db_bench.cc"], deps=[":rocksdb_tools_lib"], extra_preprocessor_flags=[], extra_bench_libs=False)
@@ -5088,6 +5095,12 @@ cpp_unittest_wrapper(name="db_secondary_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_split_merge_test",
srcs=["utilities/db_split_merge_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_sst_test",
srcs=["db/db_sst_test.cc"],
deps=[":rocksdb_test_lib"],
@@ -5384,6 +5397,12 @@ cpp_unittest_wrapper(name="listener_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="live_tuner_test",
srcs=["utilities/live_tune/live_tuner_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="log_test",
srcs=["db/log_test.cc"],
deps=[":rocksdb_test_lib"],
+5 -3
View File
@@ -16,7 +16,7 @@ This document provides guidance for generating and reviewing code in the RocksDB
**Refactoring traps:** At least in production code, avoid constructs that could allow existing code to unexpectedly, quietly change meaning.
Specifically:
* Avoid new defaulted parameters. This is the #1 trap on refactoring!
* Avoid new defaulted parameters. This is the #1 trap on refactoring! (Permitted exception: optional outputs that are generally safe to ignore.)
* Avoid static_cast, reinterpret_cast, and C-style casts; static_cast_with_check, up_cast, and lossless_cast from cast_util.h are preferred.
* Avoid declared type `auto` (`auto&`, `auto*` OK).
@@ -24,7 +24,8 @@ Specifically:
**Comprehensive Coverage:** Every change should include appropriate test coverage. This includes unit tests for isolated functionality, integration tests for component interactions, and stress tests for concurrency and performance validation. Reviewers will ask for additional tests if coverage is insufficient.
**Edge Cases and Failure Modes:** Tests should explicitly cover edge cases, boundary conditions, and potential failure scenarios. This is especially important for changes affecting core database operations, compaction, or recovery logic.
**Edge Cases and Failure Modes:** Tests should explicitly cover edge cases, boundary conditions, and potential failure scenarios. This is especially important for changes affecting core database operations, compaction, or recovery logic. For non-OK Statuses, prefer comparing code() rather than using ASSERT_TRUE
which is bad for debugging.
**Platform-Specific Testing:** RocksDB supports multiple platforms (Linux, Windows, macOS) and compilers (GCC, Clang, MSVC). Changes should be tested across relevant platforms, particularly when touching platform-specific code or using compiler-specific features.
@@ -436,7 +437,8 @@ architectures.
objects are properly checked. This catches missing error handling that can
lead to silent data corruption. Merely building with ASSERT_STATUS_CHECKED=1
accomplishes nothing; it enables a runtime check. New unit tests missing
Status checks is a common failure point, even from agents.
Status checks is a common failure point, even from agents. Avoid new uses
of PermitUncheckedError(). That's the lazy solution.
### Monitoring make check progress
* Use `make check-progress` to get machine-parseable JSON progress while
+7 -2
View File
@@ -522,8 +522,6 @@ elseif(CMAKE_SYSTEM_NAME MATCHES "Linux")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fuse-ld=lld")
endif()
endif()
elseif(CMAKE_SYSTEM_NAME MATCHES "SunOS")
add_definitions(-DOS_SOLARIS)
elseif(CMAKE_SYSTEM_NAME MATCHES "kFreeBSD")
add_definitions(-DOS_GNU_KFREEBSD)
elseif(CMAKE_SYSTEM_NAME MATCHES "FreeBSD")
@@ -1002,6 +1000,7 @@ set(SOURCES
tools/trace_analyzer_tool.cc
trace_replay/block_cache_tracer.cc
trace_replay/io_tracer.cc
trace_replay/trace_analyzer.cc
trace_replay/trace_record_handler.cc
trace_replay/trace_record_result.cc
trace_replay/trace_record.cc
@@ -1022,6 +1021,7 @@ set(SOURCES
util/dynamic_bloom.cc
util/hash.cc
util/io_dispatcher_imp.cc
util/io_prefetch_scheduler.cc
util/murmurhash.cc
util/random.cc
util/rate_limiter.cc
@@ -1051,7 +1051,10 @@ set(SOURCES
utilities/cassandra/format.cc
utilities/cassandra/merge_operator.cc
utilities/checkpoint/checkpoint_impl.cc
utilities/live_tune/live_tune_options_helper.cc
utilities/live_tune/live_tuner.cc
utilities/compaction_filters.cc
utilities/db_split_merge.cc
utilities/sorted_run_builder/sorted_run_builder.cc
utilities/compaction_filters/remove_emptyvalue_compactionfilter.cc
utilities/counted_fs.cc
@@ -1508,6 +1511,7 @@ if(WITH_TESTS)
db/db_compaction_test.cc
db/db_compaction_abort_test.cc
db/db_clip_test.cc
utilities/db_split_merge_test.cc
db/db_dynamic_level_test.cc
db/db_encryption_test.cc
db/db_etc2_test.cc
@@ -1658,6 +1662,7 @@ if(WITH_TESTS)
utilities/cassandra/cassandra_row_merge_test.cc
utilities/cassandra/cassandra_serialize_test.cc
utilities/checkpoint/checkpoint_test.cc
utilities/live_tune/live_tuner_test.cc
utilities/copy_engine/copy_engine_test.cc
utilities/sorted_run_builder/sorted_run_builder_test.cc
utilities/env_timed_test.cc
+88 -22
View File
@@ -1,15 +1,80 @@
# Rocksdb Change Log
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
## 11.13.2 (10/28/2026)
### Bug Fixes
* Fixed a secondary instance with `open_files_async = true` getting stuck after `TryCatchUpWithPrimary()` returned `PathNotFound` for a table file that the primary deleted before the background opener reached it.
* Fixed blob direct write with more than one concurrent flush of a column family: a later flush sealed and published the blob files of the oldest pending memtable instead of its own, so flushes failed with `Corruption: VersionBuilder: Blob file #N already added` and the database stopped.
## 11.14.0 (11/05/2026)
### New Features
* Added experimental `SetAsyncReadCPUTimeEnabled()`: the next async or coroutine read started on the thread records its CPU time in `PerfContext::get_cpu_nanos` at any perf level except `kDisable`, without turning on the other timers of `kEnableTimeAndCPUTimeExceptForMutex`.
* Added `SplitDB()`, `DeleteSplitRanges()`, and `MergeDB()` in `rocksdb/utilities/db_split_merge.h`, plus a `db_split_merge` command-line tool, for moving selected column-family key ranges into a new DB and copying ranges into existing column families.
* Added opt-in sampled logging of long-held DB-wide mutexes under `StatsLevel::kAll`, with a runtime-changeable `mutex_hold_log_threshold_us` DB option defaulting to 0 (disabled).
* Add experimental live tuning of a DB's options (`StartLiveTuneCycle()`, `GetLiveTuneStatus()`, `StopLiveTuneCycle()` in `include/rocksdb/live_tune.h`): one call runs a cycle that measures the DB, asks an application-provided `LiveTuneAdvisor` for option changes, applies them, and reverts them when they break the application's constraints or do not prove better.
* Added `CompactionFilter::GetMergeOrder()` and `CompactionFilterFactory::GetMergeOrder()` to optionally run the merge operator before the compaction filter during compaction, so that the filter sees merge results. The default behavior is unchanged.
* Parallel compression (`CompressionOptions::parallel_threads > 1`) now works with a `user_defined_index_factory` whose builders support it, including the built-in trie index. Previously a custom index always forced single-threaded compression.
* Added option `reject_too_old_file` to `IngestExternalFileOptions` and `ImportColumnFamilyOptions` to reject files whose data is already older than the column family's `periodic_compaction_seconds` (or `ttl` for FIFO compaction).
* Added BlobDB support for remote compaction. Blob garbage collection uses the
cutoff computed by the primary; workers receiving legacy input without a cutoff
safely disable blob garbage collection for that job. When the primary rejects a
remote blob result because its metadata, files, or capabilities fail validation,
it reruns the compaction locally, so `CompactRange` and `CompactFiles` succeed.
`CompactionService` `kFailure` semantics are unchanged; ZippyDB production also
maps service failures to local fallback through `HandleFailure(FALLBACK_LOCAL)`.
* `CompactRangeOptions::skip_missing_blob_files` enables offline repair that deletes values referencing integrated external blob files absent from Version metadata, preserves available wide entity columns, and rewrites selected SSTs locally, bypassing `DBOptions::compaction_service`. It defaults to false; FIFO compaction and legacy BlobDB are unsupported.
* Added `db_ownership_start_time` and `data_file_provenance` (decoded by `DataFileProvenance`) to `SstFileMetaData`, recording when the DB began holding each SST file and how it got the file (e.g. flush, compaction, ingestion, or import).
* Added `TableFactory::IsTableBuilderSupported()` and `ExternalTableFactory::IsTableBuilderSupported()` for table factories that only read ingested or imported files. A column family whose factory returns false must set `disallow_memtable_writes`, is never scheduled for automatic compaction, does not stall writes for L0 files or pending compaction bytes, and returns `NotSupported` from `CompactRange()` and `CompactFiles()` instead of failing a background compaction.
* trace_analyzer: new `--analyze_put_entity` flag analyzes PutEntity queries (previously their statistics were neither enabled nor disabled explicitly).
* Added `TraceOptions::include_read_write_options`: query traces can record the main `ReadOptions` and `WriteOptions` of each traced call (e.g. `fill_cache`, `disableWAL`), which replay then uses instead of default options.
### Public API Changes
* Added `GetEncodedEntrySize()` and an `EncodeEntry()` overload that writes internal keys into caller-owned buffers without allocating on success.
* Added `Env::Priority::READ` and `ThreadStatus::READ_PRIORITY` for a dedicated Env-managed read-work thread pool. `Env::Priority::USER` remains reserved for internal statistics and is not backed by a thread pool.
* Added experimental `MultiScanArgs::use_parallel_async_io_block_decompression` for materializing asynchronously prefetched data blocks, including decompression, in parallel on the `Env::Priority::READ` thread pool. A zero-sized pool preserves inline materialization.
* Added the experimental `IndexFactory` interface (`include/rocksdb/index_factory.h`) for pluggable SST indexes; the `UserDefinedIndex*` names remain as aliases. `BlockBasedTableOptions::index_mode` chooses which indexes an SST is built with and which one reads use by default, superseding the now-deprecated `use_udi_as_primary_index` and `fail_if_no_udi_on_open`. `ReadOptions::read_index` overrides that choice per read, and the `standard_index_is_stub` table property marks SSTs built with `kCustomOnly`. Both options are also available in the C API.
* Added experimental `DB::GetEntityWithMetadata()` and `DB::MultiGetEntityWithMetadata()`, the wide-column entity counterparts of `DB::GetWithMetadata()` and `DB::MultiGetWithMetadata()` with opt-in explicit-snapshot newer-version detection. Requesting key timestamps returns `NotSupported`.
* Removed support for building on Solaris, which had not compiled since 5.18. Removed RocksJava `Environment.isSolaris()`.
### Behavior Changes
* With `ReadOptions::async_io`, `MultiGet` now reads data blocks of direct-IO files with asynchronous reads (`FSRandomAccessFile::ReadAsync`) instead of a synchronous `MultiRead`. Each block read is aligned separately rather than merged with adjacent reads. The bytes read are still charged to `DBOptions::rate_limiter` at `ReadOptions::rate_limiter_priority`.
* Asynchronous file reads (`FSRandomAccessFile::ReadAsync`, used with `ReadOptions::async_io` by `MultiGet` and iterator prefetching) are now charged to `DBOptions::rate_limiter` at `ReadOptions::rate_limiter_priority`, like synchronous reads. A read is charged once the file system accepts it.
* `CreateColumnFamily()` and `CreateColumnFamilies()` now run the table factory option validation that `DB::Open()` runs, so they return `InvalidArgument` instead of creating a column family that the next `DB::Open()` would reject. External table factories wrapped by `NewExternalTableFactory()` now receive `ValidateOptions()` calls.
* With `CompactionOptionsFIFO::allow_trivial_copy_when_change_temperature`, each file copied for a temperature change is now reported to `EventListener::OnTableFileCreationStarted()` and `OnTableFileCreated()` (with the original file's table properties) and included in `CompactionJobInfo::table_properties`.
* `DB::GetMergeOperands()` now resets only the `merge_operands` entries it fills, instead of all `expected_max_number_of_operands` entries. Entries past `*number_of_operands`, and the whole array on a non-OK result, are left as the caller passed them.
* The JSON written to the info LOG by `EventLogger` (`EVENT_LOG_v1` lines) and by `VersionEdit::DebugJSON()` now escapes special characters in keys and string values, and writes non-finite floating-point values as `null`, so the output is always valid JSON.
* Improved how `periodic_compaction_phase_recovery_percent` spreads catch-up periodic compactions after it is enabled or changed (or the DB is reopened). This reduces a transient burst in periodic compactions when many DBs make such a change at once.
* The new DB option `use_separate_remote_compaction_pool` defaults to false and can be changed through `SetDBOptions()` for newly scheduled jobs. When enabled with CompactionService, remote jobs use a dedicated pool and each local subcompaction uses LOW/BOTTOM and counts against the DB compaction limit. Existing jobs finish using their original scheduling model.
* `DB::SetOptions()` now returns OK without writing a new OPTIONS file or installing a new SuperVersion when the call changes no option value, as `DB::SetDBOptions()` already does. Calls that set an object option, such as `table_factory` or one of its fields, are still applied in full. After a failed OPTIONS file write, the next `SetOptions()` or `SetDBOptions()` call writes the file even if no option value changes.
* Custom index factories are rejected with user-defined timestamps in every index mode except `kStandardOnly`, which ignores the factory. Custom-index block-cache hits, misses, and adds now contribute to `BLOCK_CACHE_INDEX_*` instead of `BLOCK_CACHE_DATA_*`.
Trie index construction returns `NotSupported` when a data-block offset or size exceeds `UINT32_MAX`, including when the trie is optional in `kStandardDefault`. Flush, compaction, and `SstFileWriter::Finish()` can therefore fail for such output; an SST larger than 4 GiB is not itself the rejection condition. File-size targets do not split L0 outputs or `SstFileWriter` outputs. Bound those outputs or disable trie construction with `kStandardOnly`.
## 11.13.1 (10/23/2026)
### Bug Fixes
* Fixed a race where `IngestExternalFiles()` with `atomic_replace_range` could delete an existing SST file that a concurrently picked compaction (e.g. a universal compaction outputting to L0) used as input, causing the compaction to fail with "Corruption: VersionBuilder: Cannot delete table file ... since it is not in the LSM tree" and putting the DB into read-only mode. Atomic range replacement now returns `NotSupported` with FIFO compaction.
* Fixed blob direct write when sealing a blob file fails during a flush or a write: the flush left its memtables marked as being flushed, so no later flush picked them up and error recovery waited for them forever, and the blob file was dropped from tracking, so a retried flush could install table files whose blob file was never added to the MANIFEST.
* Fixed blob direct write with more than one concurrent flush of a column family: a later flush sealed and published the blob files of the oldest pending memtable instead of its own, so flushes failed with `Corruption: VersionBuilder: Blob file #N already added` and the database stopped.
* Fixed blob direct write when the `SstFileManager` space limit is reached while a blob file is sealed: the sealed blob file was deleted while memtables still referenced it, so reads of its values failed and the values were lost, and reporting the error could deadlock with a concurrent memtable switch. The seal and the write or flush now succeed and keep the file, and writes then stop with a `SpaceLimit` background error, so for blob direct write the limit is soft: usage can exceed it by up to about `blob_file_size` × `blob_direct_write_partitions` for each memtable not flushed yet. Also fixed a write batch that failed after writing some of its values to blob files: those blobs are now counted as garbage, so their blob file can still be deleted.
* Values that retain external blob references now keep their original timestamps during history compaction, preventing blob key verification failures.
* Fixed `DB::ClipColumnFamily()` leaving an empty-string key outside the kept range when it is the only key in an L0 SST file.
* Fixed `format_version=8` with `ReverseBytewiseComparator`: a block's stored common key prefix could extend past a user key (one following longer user keys that extend it), so reads and iteration could miss that key (or its deletion, exposing an older value), and compactions could fail with `Corruption`. Files written by affected versions (11.10 through 11.13) are now read correctly, and new files no longer have this problem.
* Fixed SST file times that could defer periodic compaction and TTL. Files written by `SstFileWriter`, flushed during WAL recovery, or recovered by `RepairDB` now record a file creation time. Files added by `IngestExternalFile()` or `CreateColumnFamilyWithImport()` now keep the creation and oldest ancestor times from the file or export metadata (capped at the current time) instead of taking the time they were added, so ingesting or importing old data can make it due for periodic compaction or TTL soon afterward. FIFO TTL compaction now also deletes ingested `SstFileWriter` files, which previously blocked TTL deletion of themselves and all newer files. Files copied by FIFO compaction for a temperature change now keep the original file's creation time.
* Fixed a debug-build assertion failure and incorrect cache charge and block-cache byte statistics in release builds when a full, strict-capacity LRU cache with metadata charging creates an uncharged standalone handle, including during secondary-cache promotion.
* Fixed atomic flushes of all column families when another column family is dropped while they wait: `CompactRange()` no longer fails with a "Column family dropped" error, and `WaitForCompact()` and DB close no longer return OK without flushing the remaining column families.
* Fixed a bug where MemPurge returned with the DB mutex unlocked when the compaction filter created for flush did not ignore snapshots.
* Fixed a crash in MultiScan `Iterator::Prepare()` with a single scan range without a limit when the DB has L0 files, and an assertion failure in debug builds when `Prepare()` is called on an already-positioned iterator. `Prepare()` now leaves the iterator invalid until it is positioned again (e.g. by `Seek()`).
* Fixed `MultiGet` returning NotFound for existing keys that are outside the prefix extractor's domain, with partitioned filters, `whole_key_filtering=false`, and filter partitions cut together with index partitions (`decouple_partitioned_filters=false`, also the layout of files written by older versions). A filter partition holding only such keys is empty, and `MultiGet` treated every key mapped to it as absent. `Get` was not affected.
* Fixed prepared external file ingestions retaining unrelated obsolete SSTs until commit or abort. Prepared ingestions now protect only their staged files from deletion.
* Fixed data loss with `min_tombstones_for_range_conversion` and external file ingestion. An iterator whose view predated the ingestion could convert the deleted keys around the ingested keys into a range tombstone that deleted them. This happened when the file got no new sequence number (sequence number zero, `ingest_behind`, or `allow_db_generated_files`), and when an iterator was created during the ingestion.
* Fixed a data loss bug where an iterator with `ReadOptions::read_tier = kMemtableTier` could, with `min_tombstones_for_range_conversion` enabled, insert a range tombstone that deleted live keys stored only in SST files. Read-path range tombstone conversion now runs only for iterators with `read_tier = kReadAllTier`.
* Fixed a race with `recycle_log_file_num > 0` where an obsolete WAL could be reused for a new WAL while its old writer was still open. Closing the old writer then truncated the reused file to the old WAL's size, so writes in the new WAL past that point, including acknowledged `sync` writes, were lost on the next reopen.
* Fixed remote compaction ignoring `preclude_last_level_data_seconds` and `preserve_internal_time_seconds`, which caused recent data to be written to the last level and recent sequence numbers to be zeroed. Takes effect once both the primary and the compaction worker are upgraded.
* Fixed `DB::OpenAndCompact()` with `OpenAndCompactOptions::allow_resumption` not calling `EventListener::OnSubcompactionCompleted()` when the compaction fails to resume from its saved progress.
* Fixed MANIFEST catch-up on secondary instances (`TryCatchUpWithPrimary()`) and follower instances installing a column family with no files and still returning OK. This happened when catch-up switched to a new MANIFEST and the primary deleted a file that MANIFEST lists before catch-up checked it, recording the deletion in a newer MANIFEST. Catch-up now keeps the column family's previous view until a complete state from the new MANIFEST is available.
* Custom index blocks now reuse cached contents on SST reopen, and deprecated UDI boolean options can be cleared without overriding an explicitly configured index mode.
Legacy UDI options copied from a factory can now be cleared before constructing a new factory without retaining the derived mode. The C API preserves both that rollback behavior and explicitly selected modes. OPTIONS files serialize equivalent legacy booleans for `kStandardDefault`, `kStandardRequired`, and `kCustomDefault` so older readers retain routing.
Empty custom-index output retains the standard-index fallback in modes that write both indexes; custom-only output must remain nonempty. Default primary reads retain the table factory's precedence over a legacy per-read factory pointer, while explicit custom selectors still check its name.
* Preserve legacy UDI routing when loading old OPTIONS files, and reject invalid index modes in standalone SST readers and writers. Table readers now charge the custom index blocks they own; a custom reader whose `ApproximateMemoryUsage()` already includes that block should override `IndexFactoryReader::MemoryUsageIncludesIndexBlock()` to return true so the block is not charged twice.
Keep cached custom-index Slice views intact when a reader consumes its input. Register custom-index cache helpers without requiring a later change. Trie index size estimates now account for shared prefixes, preventing premature compaction file cuts, and reject data-block offsets or sizes exceeding the 32-bit on-disk limit instead of truncating them.
### Performance Improvements
* With `ReadOptions::pin_data`, iterators over read-only DBs opened with `max_open_files = -1` and `allow_mmap_reads = true` no longer accumulate memory as they move across SST files.
* On a read-write primary, `DB::GetSnapshot()` and `DB::ReleaseSnapshot()` no longer acquire the DB mutex in the common case. Secondary and follower snapshot creation retains the DB mutex needed to synchronize with view installation, while release remains sharded. The snapshot list is now split into 16 per-CPU shards, each with its own lock, so primary threads creating and releasing snapshots no longer serialize on one lock or wait behind flush, compaction, or other work holding the DB mutex. Snapshot-release maintenance is deferred to existing flush, compaction, and periodic maintenance paths. Primary snapshot creation serializes with the DB-mutex-held phases of external file ingestion but can proceed while the MANIFEST write temporarily releases that mutex.
## 11.13.0 (10/21/2026)
### New Features
@@ -46,6 +111,7 @@ checkpoint intentionally dropped from its copied MANIFEST.
### Performance Improvements
* Perf context counters and timers no longer call the thread-local init function of `perf_context` on every update. With `kEnableCount`, a counter update costs about 60% less.
## 11.12.0 (10/07/2026)
### New Features
* Added `FullExternalTableFactory` and related full-mode external table APIs that preserve RocksDB internal keys, enabling external tables to support nonzero sequence numbers, point and single deletions, merge operands, blob indexes, wide-column entities, snapshots, live DB writes, and multi-level compactions.
@@ -1090,7 +1156,7 @@ want to continue to use force enabling, they need to explicitly pass a `true` to
* Overload the API CreateColumnFamilyWithImport() to support creating ColumnFamily by importing multiple ColumnFamilies It requires that CFs should not overlap in user key range.
### Behavior Changes
* Change the default value for option `level_compaction_dynamic_level_bytes` to true. This affects users who use leveled compaction and do not set this option explicitly. These users may see additional background compactions following DB open. These compactions help to shape the LSM according to `level_compaction_dynamic_level_bytes` such that the size of each level Ln is approximately size of Ln-1 * `max_bytes_for_level_multiplier`. Turning on this option has other benefits too: see more detail in wiki: https://github.com/facebook/rocksdb/wiki/Leveled-Compaction#option-level_compaction_dynamic_level_bytes-and-levels-target-size and in option comment in advanced_options.h (#11525).
* Change the default value for option `level_compaction_dynamic_level_bytes` to true. This affects users who use leveled compaction and do not set this option explicitly. These users may see additional background compactions following DB open. These compactions help to shape the LSM according to `level_compaction_dynamic_level_bytes` such that the size of each level Ln is approximately size of Ln-1 * `max_bytes_for_level_multiplier`. Turning on this option has other benefits too: see more detail in wiki: https://github.com/meta/rocksdb/wiki/Leveled-Compaction#option-level_compaction_dynamic_level_bytes-and-levels-target-size and in option comment in advanced_options.h (#11525).
* For Leveled Compaction users, `CompactRange()` will now always try to compact to the last non-empty level. (#11468)
For Leveled Compaction users, `CompactRange()` with `bottommost_level_compaction = BottommostLevelCompaction::kIfHaveCompactionFilter` will behave similar to `kForceOptimized` in that it will skip files created during this manual compaction when compacting files in the bottommost level. (#11468)
* RocksDB will try to drop range tombstones during non-bottommost compaction when it is safe to do so. (#11459)
@@ -1135,7 +1201,7 @@ For Leveled Compaction users, `CompactRange()` with `bottommost_level_compaction
* Remove unnecessary constructor for CompressionOptions.
### Behavior changes
* Changed default block cache size from an 8MB to 32MB LRUCache, which increases the default number of cache shards from 16 to 64. This change is intended to minimize cache mutex contention under stress conditions. See https://github.com/facebook/rocksdb/wiki/Block-Cache for more information.
* Changed default block cache size from an 8MB to 32MB LRUCache, which increases the default number of cache shards from 16 to 64. This change is intended to minimize cache mutex contention under stress conditions. See https://github.com/meta/rocksdb/wiki/Block-Cache for more information.
* For level compaction with `level_compaction_dynamic_level_bytes=true`, RocksDB now trivially moves levels down to fill LSM starting from bottommost level during DB open. See more in comments for option `level_compaction_dynamic_level_bytes` (#11321).
* User-provided `ReadOptions` take effect for more reads of non-`CacheEntryRole::kDataBlock` blocks.
* For level compaction with `level_compaction_dynamic_level_bytes=true`, RocksDB now drains unnecessary levels through background compaction automatically (#11340). This together with #11321 makes it automatic to migrate other compaction settings to level compaction with `level_compaction_dynamic_level_bytes=true`. In addition, a live DB that becomes smaller will now have unnecessary levels drained which can help to reduce read and space amp.
@@ -1299,7 +1365,7 @@ For Leveled Compaction users, `CompactRange()` with `bottommost_level_compaction
* Only periodic stats dumper triggered by options.stats_dump_period_sec will update stats interval. Ones triggered by DB::GetProperty() will not update stats interval and will report based on an interval since the last time stats dump period.
### Public API changes
* Make kXXH3 checksum the new default, because it is faster on common hardware, especially with kCRC32c affected by a performance bug in some versions of clang (https://github.com/facebook/rocksdb/issues/9891). DBs written with this new setting can be read by RocksDB 6.27 and newer.
* Make kXXH3 checksum the new default, because it is faster on common hardware, especially with kCRC32c affected by a performance bug in some versions of clang (https://github.com/meta/rocksdb/issues/9891). DBs written with this new setting can be read by RocksDB 6.27 and newer.
* Refactor the classes, APIs and data structures for block cache tracing to allow a user provided trace writer to be used. Introduced an abstract BlockCacheTraceWriter class that takes a structured BlockCacheTraceRecord. The BlockCacheTraceWriter implementation can then format and log the record in whatever way it sees fit. The default BlockCacheTraceWriterImpl does file tracing using a user provided TraceWriter. More details in rocksdb/includb/block_cache_trace_writer.h.
## 7.7.0 (09/18/2022)
@@ -1419,7 +1485,7 @@ For Leveled Compaction users, `CompactRange()` with `bottommost_level_compaction
* Fix a bug in which backup/checkpoint can include a WAL deleted by RocksDB.
* Fix a bug where concurrent compactions might cause unnecessary further write stalling. In some cases, this might cause write rate to drop to minimum.
* Fix a bug in Logger where if dbname and db_log_dir are on different filesystems, dbname creation would fail wrt to db_log_dir path returning an error and fails to open the DB.
* Fix a CPU and memory efficiency issue introduce by https://github.com/facebook/rocksdb/pull/8336 which made InternalKeyComparator configurable as an unintended side effect.
* Fix a CPU and memory efficiency issue introduce by https://github.com/meta/rocksdb/pull/8336 which made InternalKeyComparator configurable as an unintended side effect.
## Behavior Change
* In leveled compaction with dynamic levelling, level multiplier is not anymore adjusted due to oversized L0. Instead, compaction score is adjusted by increasing size level target by adding incoming bytes from upper levels. This would deprioritize compactions from upper levels if more data from L0 is coming. This is to fix some unnecessary full stalling due to drastic change of level targets, while not wasting write bandwidth for compaction while writes are overloaded.
@@ -1458,7 +1524,7 @@ For Leveled Compaction users, `CompactRange()` with `bottommost_level_compaction
* Add User Defined Timestamp in C API.
* `rocksdb_comparator_with_ts_create` to create timestamp aware comparator
* Put, Get, Delete, SingleDelete, MultiGet APIs has corresponding timestamp aware APIs with suffix `with_ts`
* And Add C API's for Transaction, SstFileWriter, Compaction as mentioned [here](https://github.com/facebook/rocksdb/wiki/User-defined-Timestamp-(Experimental))
* And Add C API's for Transaction, SstFileWriter, Compaction as mentioned [here](https://github.com/meta/rocksdb/wiki/User-defined-Timestamp-(Experimental))
* The contract for implementations of Comparator::IsSameLengthImmediateSuccessor has been updated to work around a design bug in `auto_prefix_mode`.
* The API documentation for `auto_prefix_mode` now notes some corner cases in which it returns different results than `total_order_seek`, due to design bugs that are not easily fixed. Users using built-in comparators and keys at least the size of a fixed prefix length are not affected.
* Obsoleted the NUM_DATA_BLOCKS_READ_PER_LEVEL stat and introduced the NUM_LEVEL_READ_PER_MULTIGET and MULTIGET_COROUTINE_COUNT stats
@@ -1689,7 +1755,7 @@ cache at some point (i.e, causing a cache full under `LRUCacheOptions::strict_ca
* Experimental: Add support for new APIs ReadAsync in FSRandomAccessFile that reads the data asynchronously and Poll API in FileSystem that checks if requested read request has completed or not. ReadAsync takes a callback function. Poll API checks for completion of read IO requests and should call callback functions to indicate completion of read requests.
## 6.29.0 (01/21/2022)
Note: The next release will be major release 7.0. See https://github.com/facebook/rocksdb/issues/9390 for more info.
Note: The next release will be major release 7.0. See https://github.com/meta/rocksdb/issues/9390 for more info.
### Public API change
* Added values to `TraceFilterType`: `kTraceFilterIteratorSeek`, `kTraceFilterIteratorSeekForPrev`, and `kTraceFilterMultiGet`. They can be set in `TraceOptions` to filter out the operation types after which they are named.
* Added `TraceOptions::preserve_write_order`. When enabled it guarantees write records are traced in the same order they are logged to WAL and applied to the DB. By default it is disabled (false) to match the legacy behavior and prevent regression.
@@ -1838,7 +1904,7 @@ Note: The next release will be major release 7.0. See https://github.com/faceboo
### Java API Changes
* Add Java API bindings for new integrated BlobDB options
* `keyMayExist()` supports ByteBuffer.
* Fix multiget throwing Null Pointer Exception for num of keys > 70k (https://github.com/facebook/rocksdb/issues/8039).
* Fix multiget throwing Null Pointer Exception for num of keys > 70k (https://github.com/meta/rocksdb/issues/8039).
## 6.25.0 (2021-09-20)
### Bug Fixes
@@ -2525,7 +2591,7 @@ Note: The next release will be major release 7.0. See https://github.com/faceboo
* Errors related to the retrieval of the compression dictionary are now propagated to the user.
* db_bench adds a "benchmark" stats_history, which prints out the whole stats history.
* Overload GetAllKeyVersions() to support non-default column family.
* Added new APIs ExportColumnFamily() and CreateColumnFamilyWithImport() to support export and import of a Column Family. https://github.com/facebook/rocksdb/issues/3469
* Added new APIs ExportColumnFamily() and CreateColumnFamilyWithImport() to support export and import of a Column Family. https://github.com/meta/rocksdb/issues/3469
* ldb sometimes uses a string-append merge operator if no merge operator is passed in. This is to allow users to print keys from a DB with a merge operator.
* Replaces old Registra with ObjectRegistry to allow user to create custom object from string, also add LoadEnv() to Env.
* Added new overload of GetApproximateSizes which gets SizeApproximationOptions object and returns a Status. The older overloads are redirecting their calls to this new method and no longer assert if the include_flags doesn't have either of INCLUDE_MEMTABLES or INCLUDE_FILES bits set. It's recommended to use the new method only, as it is more type safe and returns a meaningful status in case of errors.
@@ -3112,7 +3178,7 @@ if set to something > 0 user will see 2 changes in iterators behavior 1) only ke
## 4.8.0 (2016-05-02)
### Public API Change
* Allow preset compression dictionary for improved compression of block-based tables. This is supported for zlib, zstd, and lz4. The compression dictionary's size is configurable via CompressionOptions::max_dict_bytes.
* Delete deprecated classes for creating backups (BackupableDB) and restoring from backups (RestoreBackupableDB). Now, BackupEngine should be used for creating backups, and BackupEngineReadOnly should be used for restorations. For more details, see https://github.com/facebook/rocksdb/wiki/How-to-backup-RocksDB%3F
* Delete deprecated classes for creating backups (BackupableDB) and restoring from backups (RestoreBackupableDB). Now, BackupEngine should be used for creating backups, and BackupEngineReadOnly should be used for restorations. For more details, see https://github.com/meta/rocksdb/wiki/How-to-backup-RocksDB%3F
* Expose estimate of per-level compression ratio via DB property: "rocksdb.compression-ratio-at-levelN".
* Added EventListener::OnTableFileCreationStarted. EventListener::OnTableFileCreated will be called on failure case. User can check creation status via TableFileCreationInfo::status.
@@ -3122,7 +3188,7 @@ if set to something > 0 user will see 2 changes in iterators behavior 1) only ke
## 4.7.0 (2016-04-08)
### Public API Change
* rename options compaction_measure_io_stats to report_bg_io_stats and include flush too.
* Change some default options. Now default options will optimize for server-workloads. Also enable slowdown and full stop triggers for pending compaction bytes. These changes may cause sub-optimal performance or significant increase of resource usage. To avoid these risks, users can open existing RocksDB with options extracted from RocksDB option files. See https://github.com/facebook/rocksdb/wiki/RocksDB-Options-File for how to use RocksDB option files. Or you can call Options.OldDefaults() to recover old defaults. DEFAULT_OPTIONS_HISTORY.md will track change history of default options.
* Change some default options. Now default options will optimize for server-workloads. Also enable slowdown and full stop triggers for pending compaction bytes. These changes may cause sub-optimal performance or significant increase of resource usage. To avoid these risks, users can open existing RocksDB with options extracted from RocksDB option files. See https://github.com/meta/rocksdb/wiki/RocksDB-Options-File for how to use RocksDB option files. Or you can call Options.OldDefaults() to recover old defaults. DEFAULT_OPTIONS_HISTORY.md will track change history of default options.
## 4.6.0 (2016-03-10)
### Public API Changes
@@ -3419,7 +3485,7 @@ Because of that, Options::disable_seek_compaction is now obsolete. It is still a
### Public API changes
* Added _LEVEL to all InfoLogLevel enums
* Deprecated ReadOptions.prefix and ReadOptions.prefix_seek. Seek() defaults to prefix-based seek when Options.prefix_extractor is supplied. More detail is documented in https://github.com/facebook/rocksdb/wiki/Prefix-Seek-API-Changes
* Deprecated ReadOptions.prefix and ReadOptions.prefix_seek. Seek() defaults to prefix-based seek when Options.prefix_extractor is supplied. More detail is documented in https://github.com/meta/rocksdb/wiki/Prefix-Seek-API-Changes
* MemTableRepFactory::CreateMemTableRep() takes info logger as an extra parameter.
### New Features
@@ -3470,8 +3536,8 @@ Because of that, Options::disable_seek_compaction is now obsolete. It is still a
### New Features
* Added [BackupableDB](https://github.com/facebook/rocksdb/wiki/How-to-backup-RocksDB%3F)
* Implemented [TailingIterator](https://github.com/facebook/rocksdb/wiki/Tailing-Iterator), a special type of iterator that
* Added [BackupableDB](https://github.com/meta/rocksdb/wiki/How-to-backup-RocksDB%3F)
* Implemented [TailingIterator](https://github.com/meta/rocksdb/wiki/Tailing-Iterator), a special type of iterator that
doesn't create a snapshot (can be used to read newly inserted data)
and is optimized for doing sequential reads.
* Added property block for table, which allows (1) a table to store
@@ -3488,8 +3554,8 @@ Because of that, Options::disable_seek_compaction is now obsolete. It is still a
* Huge benchmark performance improvements by multiple efforts. For example, increase in readonly QPS from about 530k in 2.6 release to 1.1 million in 2.7 [1]
* Speeding up a way RocksDB deleted obsolete files - no longer listing the whole directory under a lock -- decrease in p99
* Use raw pointer instead of shared pointer for statistics: [5b825d](https://github.com/facebook/rocksdb/commit/5b825d6964e26ec3b4bb6faa708ebb1787f1d7bd) -- huge increase in performance -- shared pointers are slow
* Optimized locking for `Get()` -- [1fdb3f](https://github.com/facebook/rocksdb/commit/1fdb3f7dc60e96394e3e5b69a46ede5d67fb976c) -- 1.5x QPS increase for some workloads
* Cache speedup - [e8d40c3](https://github.com/facebook/rocksdb/commit/e8d40c31b3cca0c3e1ae9abe9b9003b1288026a9)
* Implemented autovector, which allocates first N elements on stack. Most of vectors in RocksDB are small. Also, we never want to allocate heap objects while holding a mutex. -- [c01676e4](https://github.com/facebook/rocksdb/commit/c01676e46d3be08c3c140361ef1f5884f47d3b3c)
* Use raw pointer instead of shared pointer for statistics: [5b825d](https://github.com/meta/rocksdb/commit/5b825d6964e26ec3b4bb6faa708ebb1787f1d7bd) -- huge increase in performance -- shared pointers are slow
* Optimized locking for `Get()` -- [1fdb3f](https://github.com/meta/rocksdb/commit/1fdb3f7dc60e96394e3e5b69a46ede5d67fb976c) -- 1.5x QPS increase for some workloads
* Cache speedup - [e8d40c3](https://github.com/meta/rocksdb/commit/e8d40c31b3cca0c3e1ae9abe9b9003b1288026a9)
* Implemented autovector, which allocates first N elements on stack. Most of vectors in RocksDB are small. Also, we never want to allocate heap objects while holding a mutex. -- [c01676e4](https://github.com/meta/rocksdb/commit/c01676e46d3be08c3c140361ef1f5884f47d3b3c)
* Lots of efforts to move malloc, memcpy and IO outside of locks
+2 -13
View File
@@ -153,7 +153,7 @@ most processors made since roughly 2013.
* Build RocksDB from source:
cd ~
git clone https://github.com/facebook/rocksdb.git
git clone https://github.com/meta/rocksdb.git
cd rocksdb
gmake static_lib
@@ -174,7 +174,7 @@ most processors made since roughly 2013.
```bash
cd ~
git clone https://github.com/facebook/rocksdb.git
git clone https://github.com/meta/rocksdb.git
cd rocksdb
gmake static_lib
```
@@ -211,14 +211,3 @@ most processors made since roughly 2013.
export LIBPATH=/opt/freeware/lib
export JAVA_HOME=/usr/java8_64
export PATH=/opt/freeware/bin:$PATH
* **Solaris Sparc**
* Install GCC 11 and higher.
* Use these environment variables:
export CC=gcc
export EXTRA_CFLAGS=-m64
export EXTRA_CXXFLAGS=-m64
export EXTRA_LDFLAGS=-m64
export PORTABLE=1
export PLATFORM_LDFLAGS="-static-libstdc++ -static-libgcc"
+1 -1
View File
@@ -1,6 +1,6 @@
This is the list of all known third-party language bindings for RocksDB. If something is missing, please open a pull request to add it.
* Java - https://github.com/facebook/rocksdb/tree/main/java
* Java - https://github.com/meta/rocksdb/tree/main/java
* Python
* https://github.com/rocksdict/RocksDict
* http://python-rocksdb.readthedocs.io/en/latest/ (unmaintained)
+11 -13
View File
@@ -355,10 +355,6 @@ ARFLAGS = -X64 rs
STRIPFLAGS = -X64 -x
endif
ifeq ($(PLATFORM), OS_SOLARIS)
PLATFORM_CXXFLAGS += -D _GLIBCXX_USE_C99
endif
ifeq ($(LIB_MODE),shared)
# So that binaries are executable from build location, in addition to install location
EXEC_LDFLAGS += -Wl,-rpath -Wl,'$$ORIGIN'
@@ -664,6 +660,7 @@ TESTS_PLATFORM_DEPENDENT := \
dynamic_bloom_test \
c_test \
checkpoint_test \
live_tuner_test \
sorted_run_builder_test \
crc32c_test \
coding_test \
@@ -1896,6 +1893,12 @@ backup_engine_test: $(OBJ_DIR)/utilities/backup/backup_engine_test.o $(TEST_LIBR
checkpoint_test: $(OBJ_DIR)/utilities/checkpoint/checkpoint_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
live_tuner_test: $(OBJ_DIR)/utilities/live_tune/live_tuner_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
db_split_merge_test: $(OBJ_DIR)/utilities/db_split_merge_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
copy_engine_test: $(OBJ_DIR)/utilities/copy_engine/copy_engine_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
@@ -2208,6 +2211,9 @@ ldb_cmd_test: $(OBJ_DIR)/tools/ldb_cmd_test.o $(TOOLS_LIBRARY) $(TEST_LIBRARY) $
ldb: $(OBJ_DIR)/tools/ldb.o $(TOOLS_LIBRARY) $(LIBRARY)
$(AM_LINK)
db_split_merge: $(OBJ_DIR)/tools/db_split_merge.o $(LIBRARY)
$(AM_LINK)
iostats_context_test: $(OBJ_DIR)/monitoring/iostats_context_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_V_CCLD)$(CXX) $^ $(EXEC_LDFLAGS) -o $@ $(LDFLAGS)
@@ -2407,9 +2413,7 @@ gen-pc:
# Jni stuff
# ---------------------------------------------------------------------------
JAVA_INCLUDE = -I$(JAVA_HOME)/include/ -I$(JAVA_HOME)/include/linux
ifeq ($(PLATFORM), OS_SOLARIS)
ARCH := $(shell isainfo -b)
else ifeq ($(PLATFORM), OS_OPENBSD)
ifeq ($(PLATFORM), OS_OPENBSD)
ifneq (,$(filter amd64 ppc64 ppc64le s390x arm64 aarch64 riscv64 sparc64 loongarch64, $(MACHINE)))
ARCH := 64
else
@@ -2490,12 +2494,6 @@ ifeq ($(PLATFORM), OS_FREEBSD)
ROCKSDBJNILIB = librocksdbjni-freebsd$(ARCH).so
ROCKSDB_JAR = rocksdbjni-$(ROCKSDB_JAVA_VERSION)-freebsd$(ARCH).jar
endif
ifeq ($(PLATFORM), OS_SOLARIS)
ROCKSDBJNILIB = librocksdbjni-solaris$(ARCH).so
ROCKSDB_JAR = rocksdbjni-$(ROCKSDB_MAJOR).$(ROCKSDB_MINOR).$(ROCKSDB_PATCH)-solaris$(ARCH).jar
JAVA_INCLUDE = -I$(JAVA_HOME)/include/ -I$(JAVA_HOME)/include/solaris
SHA256_CMD = digest -a sha256
endif
ifeq ($(PLATFORM), OS_AIX)
JAVA_INCLUDE = -I$(JAVA_HOME)/include/ -I$(JAVA_HOME)/include/aix
ROCKSDBJNILIB = librocksdbjni-aix.so
+2 -4
View File
@@ -1,7 +1,5 @@
## RocksDB: A Persistent Key-Value Store for Flash and RAM Storage
[![CircleCI Status](https://circleci.com/gh/facebook/rocksdb.svg?style=svg)](https://circleci.com/gh/facebook/rocksdb)
RocksDB is developed and maintained by Facebook Database Engineering Team.
It is built on earlier work on [LevelDB](https://github.com/google/leveldb) by Sanjay Ghemawat (sanjay@google.com)
and Jeff Dean (jeff@google.com)
@@ -14,9 +12,9 @@ and Space-Amplification-Factor (SAF). It has multi-threaded compactions,
making it especially suitable for storing multiple terabytes of data in a
single database.
Start with example usage here: https://github.com/facebook/rocksdb/tree/main/examples
Start with example usage here: https://github.com/meta/rocksdb/tree/main/examples
See the [github wiki](https://github.com/facebook/rocksdb/wiki) for more explanation.
See the [github wiki](https://github.com/meta/rocksdb/wiki) for more explanation.
The public interface is in `include/`. Callers should not include or
rely on the details of any other header files in this package. Those
+19 -16
View File
@@ -188,22 +188,22 @@ def generate_buck(repo_path, deps_map):
# always add range_tree, it's only excluded on ppc64, which we don't use internally
src_mk["RANGE_TREE_SOURCES"] + src_mk["TOOL_LIB_SOURCES"],
deps=[
"//folly:executor",
"//folly/container:f14_hash",
"//folly/coro:baton",
"//folly/coro:blocking_wait",
"//folly/coro:collect",
"//folly/coro:coroutine",
"//folly/coro:nothrow",
"//folly/coro:task",
"//folly/executors:io_executor",
"//folly/executors:io_thread_pool_executor",
"//folly/executors/thread_factory:named_thread_factory",
"//folly/io/async:async_base",
"//folly/io/async:event_base_manager",
"//folly/io/async:io_uring_backend",
"//folly/io/async:request_context",
"//folly/synchronization:distributed_mutex",
"fbsource//xplat/folly:executor",
"fbsource//xplat/folly/container:f14_hash",
"fbsource//xplat/folly/coro:baton",
"fbsource//xplat/folly/coro:blocking_wait",
"fbsource//xplat/folly/coro:collect",
"fbsource//xplat/folly/coro:coroutine",
"fbsource//xplat/folly/coro:nothrow",
"fbsource//xplat/folly/coro:task",
"fbsource//xplat/folly/executors:io_executor",
"fbsource//xplat/folly/executors:io_thread_pool_executor",
"fbsource//xplat/folly/executors/thread_factory:named_thread_factory",
"fbsource//xplat/folly/io/async:async_base",
"fbsource//xplat/folly/io/async:event_base_manager",
"fbsource//xplat/folly/io/async:io_uring_backend",
"fbsource//xplat/folly/io/async:request_context",
"fbsource//xplat/folly/synchronization:distributed_mutex",
],
headers=LiteralValue("glob([\"**/*.h\"])")
)
@@ -299,6 +299,9 @@ def generate_buck(repo_path, deps_map):
BUCK.add_binary(
"ldb", ["tools/ldb.cc"], [":rocksdb_tools_lib"]
)
BUCK.add_binary(
"db_split_merge", ["tools/db_split_merge.cc"], [":rocksdb_lib"]
)
# db_stress binary
BUCK.add_binary(
"db_stress", ["db_stress_tool/db_stress.cc"], [":rocksdb_stress_lib"]
-6
View File
@@ -230,12 +230,6 @@ case "$TARGET_OS" in
PLATFORM_LDFLAGS="$PLATFORM_LDFLAGS -lpthread -lrt -ldl"
# PORT_FILES=port/linux/linux_specific.cc
;;
SunOS)
PLATFORM=OS_SOLARIS
COMMON_FLAGS="$COMMON_FLAGS -fno-builtin-memcmp -D_REENTRANT -DOS_SOLARIS -m64"
PLATFORM_LDFLAGS="$PLATFORM_LDFLAGS -lpthread -lrt -static-libstdc++ -static-libgcc -m64"
# PORT_FILES=port/sunos/sunos_specific.cc
;;
AIX)
PLATFORM=OS_AIX
CC=gcc
+4 -3
View File
@@ -129,12 +129,13 @@ uncommitted_code=`git diff HEAD`
# If there's no uncommitted changes, we assume user are doing post-commit
# format check, in which case we'll try to check the modified lines vs. the
# facebook/rocksdb.git main branch. Otherwise, we'll check format of the
# meta/rocksdb.git main branch. Otherwise, we'll check format of the
# uncommitted code only.
if [ -z "$uncommitted_code" ]
then
# Attempt to get name of facebook/rocksdb.git remote.
[ "$FORMAT_REMOTE" ] || FORMAT_REMOTE="$(LC_ALL=POSIX LANG=POSIX git remote -v | grep 'facebook/rocksdb.git' | head -n 1 | cut -f 1)"
# Attempt to get name of meta/rocksdb.git remote. Older clones might still
# use the pre-transfer facebook/rocksdb.git URL, which GitHub redirects.
[ "$FORMAT_REMOTE" ] || FORMAT_REMOTE="$(LC_ALL=POSIX LANG=POSIX git remote -v | grep -E '[:/](meta|facebook)/rocksdb\.git' | head -n 1 | cut -f 1)"
# Fall back on 'origin' if that fails
[ "$FORMAT_REMOTE" ] || FORMAT_REMOTE=origin
# Use main branch from that remote
+1 -1
View File
@@ -23,7 +23,7 @@ mkdir -pv /usr/local/rocksdb-${ROCKSDB_VERSION}
ln -sfT /usr/local/rocksdb-${ROCKSDB_VERSION} /usr/local/rocksdb
wget -qO /tmp/zstd-${ZSTD_VERSION}.tar.gz https://github.com/facebook/zstd/archive/v${ZSTD_VERSION}.tar.gz
wget -qO /tmp/rocksdb-${ROCKSDB_VERSION}.tar.gz https://github.com/facebook/rocksdb/archive/v${ROCKSDB_VERSION}.tar.gz
wget -qO /tmp/rocksdb-${ROCKSDB_VERSION}.tar.gz https://github.com/meta/rocksdb/archive/v${ROCKSDB_VERSION}.tar.gz
cd /tmp
+2 -2
View File
@@ -17,8 +17,8 @@
# Then in the build_tools/ubuntu22_image directory, (bump minor version for
# random docker file updates, major version tracks Ubuntu release)
#
# $ docker build -t ghcr.io/facebook/rocksdb_ubuntu:22.2
# $ docker push ghcr.io/facebook/rocksdb_ubuntu:22.2
# $ docker build -t ghcr.io/meta/rocksdb_ubuntu:22.2
# $ docker push ghcr.io/meta/rocksdb_ubuntu:22.2
#
# Might need to change visibility to public through
# https://github.com/orgs/facebook/packages/container/rocksdb_ubuntu/settings
+2 -2
View File
@@ -17,8 +17,8 @@
# Then in the build_tools/ubuntu24_image directory, (bump minor version for
# random docker file updates, major version tracks Ubuntu release)
#
# $ docker build -t ghcr.io/facebook/rocksdb_ubuntu:24.1
# $ docker push ghcr.io/facebook/rocksdb_ubuntu:24.1
# $ docker build -t ghcr.io/meta/rocksdb_ubuntu:24.1
# $ docker push ghcr.io/meta/rocksdb_ubuntu:24.1
#
# Might need to change visibility to public through
# https://github.com/orgs/facebook/packages/container/rocksdb_ubuntu/settings
+12
View File
@@ -109,6 +109,12 @@ int rocksdb_compactionjobinfo_job_id(const rocksdb_compactionjobinfo_t* info) {
return info->rep.job_id;
}
unsigned char
rocksdb_compactionjobinfo_separate_remote_compaction_pool_selected(
const rocksdb_compactionjobinfo_t* info) {
return info->rep.separate_remote_compaction_pool_selected;
}
int rocksdb_compactionjobinfo_num_l0_files(
const rocksdb_compactionjobinfo_t* info) {
return info->rep.num_l0_files;
@@ -172,6 +178,12 @@ int rocksdb_subcompactionjobinfo_job_id(
return info->rep.job_id;
}
unsigned char
rocksdb_subcompactionjobinfo_separate_remote_compaction_pool_selected(
const rocksdb_subcompactionjobinfo_t* info) {
return info->rep.separate_remote_compaction_pool_selected;
}
int rocksdb_subcompactionjobinfo_subcompaction_job_id(
const rocksdb_subcompactionjobinfo_t* info) {
return info->rep.subcompaction_job_id;
+8
View File
@@ -78,6 +78,10 @@ rocksdb_compactionjobinfo_thread_id(const rocksdb_compactionjobinfo_t* info);
extern ROCKSDB_LIBRARY_API int rocksdb_compactionjobinfo_job_id(
const rocksdb_compactionjobinfo_t* info);
extern ROCKSDB_LIBRARY_API unsigned char
rocksdb_compactionjobinfo_separate_remote_compaction_pool_selected(
const rocksdb_compactionjobinfo_t* info);
extern ROCKSDB_LIBRARY_API int rocksdb_compactionjobinfo_num_l0_files(
const rocksdb_compactionjobinfo_t* info);
@@ -117,6 +121,10 @@ extern ROCKSDB_LIBRARY_API uint64_t rocksdb_subcompactionjobinfo_thread_id(
extern ROCKSDB_LIBRARY_API int rocksdb_subcompactionjobinfo_job_id(
const rocksdb_subcompactionjobinfo_t* info);
extern ROCKSDB_LIBRARY_API unsigned char
rocksdb_subcompactionjobinfo_separate_remote_compaction_pool_selected(
const rocksdb_subcompactionjobinfo_t* info);
extern ROCKSDB_LIBRARY_API int
rocksdb_subcompactionjobinfo_subcompaction_job_id(
const rocksdb_subcompactionjobinfo_t* info);
@@ -67,6 +67,11 @@ uint64_t rocksdb_table_properties_udi_is_primary_index(
return props->rep.udi_is_primary_index;
}
uint64_t rocksdb_table_properties_standard_index_is_stub(
const rocksdb_table_properties_t* props) {
return props->rep.standard_index_is_stub;
}
uint64_t rocksdb_table_properties_filter_size(
const rocksdb_table_properties_t* props) {
return props->rep.filter_size;
@@ -54,6 +54,10 @@ extern ROCKSDB_LIBRARY_API uint64_t
rocksdb_table_properties_udi_is_primary_index(
const rocksdb_table_properties_t* props);
extern ROCKSDB_LIBRARY_API uint64_t
rocksdb_table_properties_standard_index_is_stub(
const rocksdb_table_properties_t* props);
extern ROCKSDB_LIBRARY_API uint64_t
rocksdb_table_properties_filter_size(const rocksdb_table_properties_t* props);
@@ -826,6 +826,16 @@ const char* rocksdb_options_get_db_host_id(rocksdb_options_t* opt,
return opt->rep.db_host_id.data();
}
void rocksdb_options_set_use_separate_remote_compaction_pool(
rocksdb_options_t* opt, unsigned char v) {
opt->rep.use_separate_remote_compaction_pool = v;
}
unsigned char rocksdb_options_get_use_separate_remote_compaction_pool(
rocksdb_options_t* opt) {
return opt->rep.use_separate_remote_compaction_pool;
}
void rocksdb_options_set_remote_compaction_manifest_floor(
rocksdb_options_t* opt, unsigned char v) {
opt->rep.remote_compaction_manifest_floor = v;
@@ -1919,6 +1929,16 @@ unsigned char rocksdb_trace_options_get_preserve_write_order(
return opt->rep.preserve_write_order;
}
void rocksdb_trace_options_set_include_read_write_options(
rocksdb_trace_options_t* opt, unsigned char v) {
opt->rep.include_read_write_options = v;
}
unsigned char rocksdb_trace_options_get_include_read_write_options(
rocksdb_trace_options_t* opt) {
return opt->rep.include_read_write_options;
}
/* BlockCacheTraceOptions */
void rocksdb_block_cache_trace_options_set_sampling_frequency(
@@ -1998,6 +2018,16 @@ unsigned char rocksdb_compaction_options_get_allow_trivial_move(
/* CompactRangeOptions */
void rocksdb_compactoptions_set_skip_missing_blob_files(
rocksdb_compactoptions_t* opt, unsigned char v) {
opt->rep.skip_missing_blob_files = v;
}
unsigned char rocksdb_compactoptions_get_skip_missing_blob_files(
rocksdb_compactoptions_t* opt) {
return opt->rep.skip_missing_blob_files;
}
void rocksdb_compactoptions_set_blob_garbage_collection_policy(
rocksdb_compactoptions_t* opt, int v) {
opt->rep.blob_garbage_collection_policy =
@@ -2145,6 +2175,16 @@ int rocksdb_ingestexternalfileoptions_get_file_opening_threads(
return opt->rep.file_opening_threads;
}
void rocksdb_ingestexternalfileoptions_set_reject_too_old_file(
rocksdb_ingestexternalfileoptions_t* opt, unsigned char v) {
opt->rep.reject_too_old_file = v;
}
unsigned char rocksdb_ingestexternalfileoptions_get_reject_too_old_file(
rocksdb_ingestexternalfileoptions_t* opt) {
return opt->rep.reject_too_old_file;
}
/* OpenAndCompactOptions */
void rocksdb_open_and_compact_options_set_max_secondary_open_retries(
@@ -2169,6 +2209,16 @@ unsigned char rocksdb_import_column_family_options_get_move_files(
return opt->rep.move_files;
}
void rocksdb_import_column_family_options_set_reject_too_old_file(
rocksdb_import_column_family_options_t* opt, unsigned char v) {
opt->rep.reject_too_old_file = v;
}
unsigned char rocksdb_import_column_family_options_get_reject_too_old_file(
rocksdb_import_column_family_options_t* opt) {
return opt->rep.reject_too_old_file;
}
/* SizeApproximationOptions */
void rocksdb_size_approximation_options_set_include_memtables(
@@ -2463,26 +2513,6 @@ unsigned char rocksdb_block_based_options_get_use_delta_encoding(
return opt->rep.use_delta_encoding;
}
void rocksdb_block_based_options_set_use_udi_as_primary_index(
rocksdb_block_based_table_options_t* opt, unsigned char v) {
opt->rep.use_udi_as_primary_index = v;
}
unsigned char rocksdb_block_based_options_get_use_udi_as_primary_index(
rocksdb_block_based_table_options_t* opt) {
return opt->rep.use_udi_as_primary_index;
}
void rocksdb_block_based_options_set_fail_if_no_udi_on_open(
rocksdb_block_based_table_options_t* opt, unsigned char v) {
opt->rep.fail_if_no_udi_on_open = v;
}
unsigned char rocksdb_block_based_options_get_fail_if_no_udi_on_open(
rocksdb_block_based_table_options_t* opt) {
return opt->rep.fail_if_no_udi_on_open;
}
void rocksdb_block_based_options_set_whole_key_filtering(
rocksdb_block_based_table_options_t* opt, unsigned char v) {
opt->rep.whole_key_filtering = v;
+39 -16
View File
@@ -255,6 +255,13 @@ extern ROCKSDB_LIBRARY_API void rocksdb_options_set_db_host_id(
extern ROCKSDB_LIBRARY_API const char* rocksdb_options_get_db_host_id(
rocksdb_options_t* opt, size_t* size);
extern ROCKSDB_LIBRARY_API void
rocksdb_options_set_use_separate_remote_compaction_pool(rocksdb_options_t* opt,
unsigned char v);
extern ROCKSDB_LIBRARY_API unsigned char
rocksdb_options_get_use_separate_remote_compaction_pool(rocksdb_options_t* opt);
extern ROCKSDB_LIBRARY_API void
rocksdb_options_set_remote_compaction_manifest_floor(rocksdb_options_t* opt,
unsigned char v);
@@ -708,6 +715,14 @@ extern ROCKSDB_LIBRARY_API void rocksdb_trace_options_set_preserve_write_order(
extern ROCKSDB_LIBRARY_API unsigned char
rocksdb_trace_options_get_preserve_write_order(rocksdb_trace_options_t* opt);
extern ROCKSDB_LIBRARY_API void
rocksdb_trace_options_set_include_read_write_options(
rocksdb_trace_options_t* opt, unsigned char v);
extern ROCKSDB_LIBRARY_API unsigned char
rocksdb_trace_options_get_include_read_write_options(
rocksdb_trace_options_t* opt);
/* BlockCacheTraceOptions */
extern ROCKSDB_LIBRARY_API void
@@ -770,6 +785,14 @@ rocksdb_compaction_options_get_allow_trivial_move(
/* CompactRangeOptions */
extern ROCKSDB_LIBRARY_API void
rocksdb_compactoptions_set_skip_missing_blob_files(
rocksdb_compactoptions_t* opt, unsigned char v);
extern ROCKSDB_LIBRARY_API unsigned char
rocksdb_compactoptions_get_skip_missing_blob_files(
rocksdb_compactoptions_t* opt);
extern ROCKSDB_LIBRARY_API void
rocksdb_compactoptions_set_blob_garbage_collection_policy(
rocksdb_compactoptions_t* opt, int v);
@@ -884,6 +907,14 @@ extern ROCKSDB_LIBRARY_API int
rocksdb_ingestexternalfileoptions_get_file_opening_threads(
rocksdb_ingestexternalfileoptions_t* opt);
extern ROCKSDB_LIBRARY_API void
rocksdb_ingestexternalfileoptions_set_reject_too_old_file(
rocksdb_ingestexternalfileoptions_t* opt, unsigned char v);
extern ROCKSDB_LIBRARY_API unsigned char
rocksdb_ingestexternalfileoptions_get_reject_too_old_file(
rocksdb_ingestexternalfileoptions_t* opt);
/* OpenAndCompactOptions */
extern ROCKSDB_LIBRARY_API void
@@ -904,6 +935,14 @@ extern ROCKSDB_LIBRARY_API unsigned char
rocksdb_import_column_family_options_get_move_files(
rocksdb_import_column_family_options_t* opt);
extern ROCKSDB_LIBRARY_API void
rocksdb_import_column_family_options_set_reject_too_old_file(
rocksdb_import_column_family_options_t* opt, unsigned char v);
extern ROCKSDB_LIBRARY_API unsigned char
rocksdb_import_column_family_options_get_reject_too_old_file(
rocksdb_import_column_family_options_t* opt);
/* SizeApproximationOptions */
extern ROCKSDB_LIBRARY_API void
@@ -1130,22 +1169,6 @@ extern ROCKSDB_LIBRARY_API unsigned char
rocksdb_block_based_options_get_use_delta_encoding(
rocksdb_block_based_table_options_t* opt);
extern ROCKSDB_LIBRARY_API void
rocksdb_block_based_options_set_use_udi_as_primary_index(
rocksdb_block_based_table_options_t* opt, unsigned char v);
extern ROCKSDB_LIBRARY_API unsigned char
rocksdb_block_based_options_get_use_udi_as_primary_index(
rocksdb_block_based_table_options_t* opt);
extern ROCKSDB_LIBRARY_API void
rocksdb_block_based_options_set_fail_if_no_udi_on_open(
rocksdb_block_based_table_options_t* opt, unsigned char v);
extern ROCKSDB_LIBRARY_API unsigned char
rocksdb_block_based_options_get_fail_if_no_udi_on_open(
rocksdb_block_based_table_options_t* opt);
extern ROCKSDB_LIBRARY_API void
rocksdb_block_based_options_set_whole_key_filtering(
rocksdb_block_based_table_options_t* opt, unsigned char v);
+35 -12
View File
@@ -87,12 +87,6 @@ static void rocksdb_roundtrip_test_rocksdb_block_based_options(void) {
rocksdb_block_based_options_set_enable_index_compression(obj, 0);
CheckCondition(
rocksdb_block_based_options_get_enable_index_compression(obj) == 0);
rocksdb_block_based_options_set_fail_if_no_udi_on_open(obj, 1);
CheckCondition(rocksdb_block_based_options_get_fail_if_no_udi_on_open(obj) ==
1);
rocksdb_block_based_options_set_fail_if_no_udi_on_open(obj, 0);
CheckCondition(rocksdb_block_based_options_get_fail_if_no_udi_on_open(obj) ==
0);
rocksdb_block_based_options_set_format_version(obj, 1);
CheckCondition(rocksdb_block_based_options_get_format_version(obj) == 1);
rocksdb_block_based_options_set_format_version(obj, 0);
@@ -223,12 +217,6 @@ static void rocksdb_roundtrip_test_rocksdb_block_based_options(void) {
CheckCondition(rocksdb_block_based_options_get_use_delta_encoding(obj) == 1);
rocksdb_block_based_options_set_use_delta_encoding(obj, 0);
CheckCondition(rocksdb_block_based_options_get_use_delta_encoding(obj) == 0);
rocksdb_block_based_options_set_use_udi_as_primary_index(obj, 1);
CheckCondition(
rocksdb_block_based_options_get_use_udi_as_primary_index(obj) == 1);
rocksdb_block_based_options_set_use_udi_as_primary_index(obj, 0);
CheckCondition(
rocksdb_block_based_options_get_use_udi_as_primary_index(obj) == 0);
rocksdb_block_based_options_set_verify_compression(obj, 1);
CheckCondition(rocksdb_block_based_options_get_verify_compression(obj) == 1);
rocksdb_block_based_options_set_verify_compression(obj, 0);
@@ -321,6 +309,10 @@ static void rocksdb_roundtrip_test_rocksdb_compactoptions(void) {
rocksdb_compactoptions_set_blob_garbage_collection_policy(obj, 0);
CheckCondition(
rocksdb_compactoptions_get_blob_garbage_collection_policy(obj) == 0);
rocksdb_compactoptions_set_skip_missing_blob_files(obj, 1);
CheckCondition(rocksdb_compactoptions_get_skip_missing_blob_files(obj) == 1);
rocksdb_compactoptions_set_skip_missing_blob_files(obj, 0);
CheckCondition(rocksdb_compactoptions_get_skip_missing_blob_files(obj) == 0);
rocksdb_compactoptions_destroy(obj);
}
@@ -493,6 +485,18 @@ static void rocksdb_roundtrip_test_rocksdb_flushwaloptions(void) {
rocksdb_flushwaloptions_destroy(obj);
}
static void rocksdb_roundtrip_test_rocksdb_import_column_family_options(void) {
rocksdb_import_column_family_options_t* obj =
rocksdb_import_column_family_options_create();
rocksdb_import_column_family_options_set_reject_too_old_file(obj, 1);
CheckCondition(
rocksdb_import_column_family_options_get_reject_too_old_file(obj) == 1);
rocksdb_import_column_family_options_set_reject_too_old_file(obj, 0);
CheckCondition(
rocksdb_import_column_family_options_get_reject_too_old_file(obj) == 0);
rocksdb_import_column_family_options_destroy(obj);
}
static void rocksdb_roundtrip_test_rocksdb_ingestexternalfileoptions(void) {
rocksdb_ingestexternalfileoptions_t* obj =
rocksdb_ingestexternalfileoptions_create();
@@ -534,6 +538,12 @@ static void rocksdb_roundtrip_test_rocksdb_ingestexternalfileoptions(void) {
CheckCondition(
rocksdb_ingestexternalfileoptions_get_prefetch_lmax_index_and_filter_blocks(
obj) == 0);
rocksdb_ingestexternalfileoptions_set_reject_too_old_file(obj, 1);
CheckCondition(
rocksdb_ingestexternalfileoptions_get_reject_too_old_file(obj) == 1);
rocksdb_ingestexternalfileoptions_set_reject_too_old_file(obj, 0);
CheckCondition(
rocksdb_ingestexternalfileoptions_get_reject_too_old_file(obj) == 0);
rocksdb_ingestexternalfileoptions_set_verify_checksums_before_ingest(obj, 1);
CheckCondition(
rocksdb_ingestexternalfileoptions_get_verify_checksums_before_ingest(
@@ -937,6 +947,12 @@ static void rocksdb_roundtrip_test_rocksdb_options(void) {
CheckCondition(rocksdb_options_get_use_fsync(obj) == 1);
rocksdb_options_set_use_fsync(obj, 0);
CheckCondition(rocksdb_options_get_use_fsync(obj) == 0);
rocksdb_options_set_use_separate_remote_compaction_pool(obj, 1);
CheckCondition(rocksdb_options_get_use_separate_remote_compaction_pool(obj) ==
1);
rocksdb_options_set_use_separate_remote_compaction_pool(obj, 0);
CheckCondition(rocksdb_options_get_use_separate_remote_compaction_pool(obj) ==
0);
rocksdb_options_set_use_session_tmp_dir_for_remote_compaction(obj, 1);
CheckCondition(
rocksdb_options_get_use_session_tmp_dir_for_remote_compaction(obj) == 1);
@@ -1124,6 +1140,12 @@ static void rocksdb_roundtrip_test_rocksdb_trace_options(void) {
CheckCondition(rocksdb_trace_options_get_filter(obj) == 1);
rocksdb_trace_options_set_filter(obj, 0);
CheckCondition(rocksdb_trace_options_get_filter(obj) == 0);
rocksdb_trace_options_set_include_read_write_options(obj, 1);
CheckCondition(rocksdb_trace_options_get_include_read_write_options(obj) ==
1);
rocksdb_trace_options_set_include_read_write_options(obj, 0);
CheckCondition(rocksdb_trace_options_get_include_read_write_options(obj) ==
0);
rocksdb_trace_options_set_max_trace_file_size(obj, 1);
CheckCondition(rocksdb_trace_options_get_max_trace_file_size(obj) == 1);
rocksdb_trace_options_set_max_trace_file_size(obj, 0);
@@ -1399,6 +1421,7 @@ static void rocksdb_run_generated_roundtrip_tests(void) {
rocksdb_roundtrip_test_rocksdb_fifo_compaction_options();
rocksdb_roundtrip_test_rocksdb_flushoptions();
rocksdb_roundtrip_test_rocksdb_flushwaloptions();
rocksdb_roundtrip_test_rocksdb_import_column_family_options();
rocksdb_roundtrip_test_rocksdb_ingestexternalfileoptions();
rocksdb_roundtrip_test_rocksdb_livefiles_storage_info_options();
rocksdb_roundtrip_test_rocksdb_open_and_compact_options();
+1 -1
View File
@@ -180,7 +180,7 @@ DEFINE_uint32(
// than random." Even with this setting, file offsets are not randomized.
DEFINE_bool(sck_randomize, false,
"(-stress_cache_key) Randomize (hash) cache key");
// See https://github.com/facebook/rocksdb/pull/9058
// See https://github.com/meta/rocksdb/pull/9058
DEFINE_bool(sck_footer_unique_id, false,
"(-stress_cache_key) Simulate using proposed footer unique id");
// ## END stress_cache_key sub-tool options ##
+19
View File
@@ -948,6 +948,25 @@ TEST_P(CacheTest, GetChargeAndDeleter) {
cache_->Release(h1);
}
TEST_P(LRUCacheTest, UnchargedStandaloneGetCharge) {
auto cache = NewCache(1000, 0, true, kFullChargeCacheMetadata);
Cache::Handle* pinned = nullptr;
ASSERT_OK(
cache->Insert(EncodeKey(1), EncodeValue(1), &kHelper, 800, &pinned));
ASSERT_EQ(nullptr, cache->CreateStandalone(EncodeKey(2), EncodeValue(2),
&kHelper, 500, false));
Cache::Handle* h = cache->CreateStandalone(EncodeKey(2), EncodeValue(2),
&kHelper, 500, true);
ASSERT_NE(h, nullptr);
EXPECT_EQ(0, cache->GetCharge(h));
EXPECT_EQ(0, cache->GetUsage(h));
cache->Release(h);
cache->Release(pinned);
}
namespace {
bool AreTwoCacheKeysOrdered(Cache* cache) {
std::vector<std::string> keys;
+1 -1
View File
@@ -574,7 +574,7 @@ LRUHandle* LRUCacheShard::CreateStandalone(const Slice& key, uint32_t hash,
if (strict_capacity_limit_ && (usage_ + e->total_charge) > capacity_) {
if (allow_uncharged) {
e->total_charge = 0;
e->total_charge = LRUHandle::kUnchargedStandaloneTotalCharge;
} else {
free(e);
e = nullptr;
+8
View File
@@ -87,6 +87,10 @@ struct LRUHandle : public Cache::Handle {
IM_IS_STANDALONE = (1 << 2),
};
// total_charge sentinel for a standalone entry that added nothing to usage_
// (strict cache full, created with allow_uncharged).
static constexpr size_t kUnchargedStandaloneTotalCharge = 0;
// Beginning of the key (MUST BE THE LAST FIELD IN THIS STRUCT!)
char key_data[1];
@@ -196,6 +200,10 @@ struct LRUHandle : public Cache::Handle {
inline size_t GetCharge(
CacheMetadataChargePolicy metadata_charge_policy) const {
if (IsStandalone() && total_charge == kUnchargedStandaloneTotalCharge) {
// Documented to report 0; no metadata was charged.
return 0;
}
size_t meta_charge = CalcuMetaCharge(metadata_charge_policy);
assert(total_charge >= meta_charge);
return total_charge - meta_charge;
+71 -58
View File
@@ -1474,68 +1474,81 @@ TEST_P(BasicSecondaryCacheTest, CreateFailTest) {
}
TEST_P(BasicSecondaryCacheTest, FullCapacityTest) {
for (bool strict_capacity_limit : {false, true}) {
std::shared_ptr<TestSecondaryCache> secondary_cache =
std::make_shared<TestSecondaryCache>(2048, true);
std::shared_ptr<Cache> cache =
NewCache(1024 /* capacity */, 0 /* num_shard_bits */,
strict_capacity_limit, secondary_cache);
CacheKey k1 = CacheKey::CreateUniqueForCacheLifetime(cache.get());
CacheKey k2 = CacheKey::CreateUniqueForCacheLifetime(cache.get());
const size_t kValueSize = 700;
estimated_value_size_ = kValueSize;
for (CacheMetadataChargePolicy charge_policy :
{kDontChargeCacheMetadata, kFullChargeCacheMetadata}) {
SCOPED_TRACE(charge_policy);
for (bool strict_capacity_limit : {false, true}) {
SCOPED_TRACE(strict_capacity_limit);
std::shared_ptr<TestSecondaryCache> secondary_cache =
std::make_shared<TestSecondaryCache>(2048, true);
std::shared_ptr<Cache> cache =
NewCache(1024, [&](ShardedCacheOptions& opts) {
opts.num_shard_bits = 0;
opts.strict_capacity_limit = strict_capacity_limit;
opts.metadata_charge_policy = charge_policy;
opts.secondary_cache = secondary_cache;
});
CacheKey k1 = CacheKey::CreateUniqueForCacheLifetime(cache.get());
CacheKey k2 = CacheKey::CreateUniqueForCacheLifetime(cache.get());
Random rnd(301);
std::string str1 = rnd.RandomString(1020);
TestItem* item1 = new TestItem(str1.data(), str1.length());
ASSERT_OK(cache->Insert(k1.AsSlice(), item1, GetHelper(), str1.length()));
std::string str2 = rnd.RandomString(1020);
TestItem* item2 = new TestItem(str2.data(), str2.length());
// k1 should be demoted to NVM
ASSERT_OK(cache->Insert(k2.AsSlice(), item2, GetHelper(), str2.length()));
Random rnd(301);
std::string str1 = rnd.RandomString(kValueSize);
TestItem* item1 = new TestItem(str1.data(), str1.length());
ASSERT_OK(cache->Insert(k1.AsSlice(), item1, GetHelper(), str1.length()));
std::string str2 = rnd.RandomString(kValueSize);
TestItem* item2 = new TestItem(str2.data(), str2.length());
// k1 should be demoted to NVM
ASSERT_OK(cache->Insert(k2.AsSlice(), item2, GetHelper(), str2.length()));
Cache::Handle* handle2;
handle2 = cache->Lookup(k2.AsSlice(), GetHelper(),
/*context*/ this, Cache::Priority::LOW);
ASSERT_NE(handle2, nullptr);
// k1 lookup fails without secondary cache support
Cache::Handle* handle1;
handle1 = cache->Lookup(
k1.AsSlice(),
GetHelper(CacheEntryRole::kDataBlock, /*secondary_compatible=*/false),
/*context*/ this, Cache::Priority::LOW);
ASSERT_EQ(handle1, nullptr);
// k1 promotion can fail with strict_capacity_limit=true, but Lookup still
// succeeds using a standalone handle
handle1 = cache->Lookup(k1.AsSlice(), GetHelper(),
/*context*/ this, Cache::Priority::LOW);
ASSERT_NE(handle1, nullptr);
ASSERT_EQ(secondary_cache->num_inserts(), 1u);
ASSERT_EQ(secondary_cache->num_lookups(), 1u);
// Releasing k2's handle first, k2 is evicted from primary iff k1 promotion
// was charged to the cache (except HCC doesn't erase in Release() over
// capacity)
// FIXME: Insert to secondary from Release disabled
cache->Release(handle2);
cache->Release(handle1);
handle2 = cache->Lookup(
k2.AsSlice(),
GetHelper(CacheEntryRole::kDataBlock, /*secondary_compatible=*/false),
/*context*/ this, Cache::Priority::LOW);
if (strict_capacity_limit || IsHyperClock()) {
Cache::Handle* handle2;
handle2 = cache->Lookup(k2.AsSlice(), GetHelper(),
/*context*/ this, Cache::Priority::LOW);
ASSERT_NE(handle2, nullptr);
cache->Release(handle2);
ASSERT_EQ(secondary_cache->num_inserts(), 1u);
} else {
ASSERT_EQ(handle2, nullptr);
// FIXME: Insert to secondary from Release disabled
// ASSERT_EQ(secondary_cache->num_inserts(), 2u);
ASSERT_EQ(secondary_cache->num_inserts(), 1u);
}
// k1 lookup fails without secondary cache support
Cache::Handle* handle1;
handle1 = cache->Lookup(
k1.AsSlice(),
GetHelper(CacheEntryRole::kDataBlock, /*secondary_compatible=*/false),
/*context*/ this, Cache::Priority::LOW);
ASSERT_EQ(handle1, nullptr);
cache.reset();
secondary_cache.reset();
// k1 promotion can fail with strict_capacity_limit=true, but Lookup still
// succeeds using a standalone handle
handle1 = cache->Lookup(k1.AsSlice(), GetHelper(),
/*context*/ this, Cache::Priority::LOW);
ASSERT_NE(handle1, nullptr);
ASSERT_EQ(strict_capacity_limit ? 0 : str1.size(),
cache->GetUsage(handle1));
ASSERT_EQ(secondary_cache->num_inserts(), 1u);
ASSERT_EQ(secondary_cache->num_lookups(), 1u);
// Releasing k2's handle first, k2 is evicted from primary iff k1
// promotion was charged to the cache (except HCC doesn't erase in
// Release() over capacity)
// FIXME: Insert to secondary from Release disabled
cache->Release(handle2);
cache->Release(handle1);
handle2 = cache->Lookup(
k2.AsSlice(),
GetHelper(CacheEntryRole::kDataBlock, /*secondary_compatible=*/false),
/*context*/ this, Cache::Priority::LOW);
if (strict_capacity_limit || IsHyperClock()) {
ASSERT_NE(handle2, nullptr);
cache->Release(handle2);
ASSERT_EQ(secondary_cache->num_inserts(), 1u);
} else {
ASSERT_EQ(handle2, nullptr);
// FIXME: Insert to secondary from Release disabled
// ASSERT_EQ(secondary_cache->num_inserts(), 2u);
ASSERT_EQ(secondary_cache->num_inserts(), 1u);
}
cache.reset();
secondary_cache.reset();
}
}
}
+1 -1
View File
@@ -951,7 +951,7 @@ TEST_F(DBTieredSecondaryCacheTest, FSBufferTest) {
IODebugContext* dbg) override {
for (size_t i = 0; i < num_reqs; ++i) {
FSReadRequest& req = reqs[i];
// See https://github.com/facebook/rocksdb/pull/13195 for why we
// See https://github.com/meta/rocksdb/pull/13195 for why we
// want to set up our test implementation for FSAllocationPtr this
// way.
char* internalData = new char[req.len];
+3 -3
View File
@@ -87,15 +87,15 @@ to scan.
## Reference Pull Request Summaries
- [PR #15216](https://github.com/facebook/rocksdb/pull/15216) is a strong
- [PR #15216](https://github.com/meta/rocksdb/pull/15216) is a strong
focused bug-fix example. It traces stale iterator state to readahead behavior
and dropped keys, identifies the exact prerequisites and unaffected mmap
case, gives before/after measurements, and explains how the regression test
fails when the fix is reverted.
- [PR #15218](https://github.com/facebook/rocksdb/pull/15218) is a concise
- [PR #15218](https://github.com/meta/rocksdb/pull/15218) is a concise
example of explicit `Why`, `Previous behavior`, `New behavior`,
`Implementation`, and `Safety boundaries and unsupported cases` sections.
- Peter Dillinger's [PR #15231](https://github.com/facebook/rocksdb/pull/15231)
- Peter Dillinger's [PR #15231](https://github.com/meta/rocksdb/pull/15231)
is a strong recent multi-part example. Its `Fix`/`Add`/`Change` bullets
separate the behaviors, its compatibility sentence explains how old files
read back, and its test plan maps each new persisted property to concrete
+30 -1
View File
@@ -6,6 +6,7 @@
#pragma once
#include <cassert>
#include <optional>
#include "db/blob/blob_garbage_meter.h"
#include "rocksdb/rocksdb_namespace.h"
@@ -21,10 +22,14 @@ namespace ROCKSDB_NAMESPACE {
class BlobCountingIterator : public InternalIterator {
public:
BlobCountingIterator(InternalIterator* iter,
BlobGarbageMeter* blob_garbage_meter)
BlobGarbageMeter* blob_garbage_meter,
bool track_current_key_inflow)
: iter_(iter), blob_garbage_meter_(blob_garbage_meter) {
assert(iter_);
assert(blob_garbage_meter_);
if (track_current_key_inflow) {
current_key_inflow_.emplace();
}
UpdateAndCountBlobIfNeeded();
}
@@ -127,23 +132,47 @@ class BlobCountingIterator : public InternalIterator {
return iter_->IsDeleteRangeSentinelKey();
}
const BlobGarbageMeter& GetCurrentKeyInflow() const {
assert(current_key_inflow_.has_value());
return *current_key_inflow_;
}
bool TracksCurrentKeyInflow() const {
return current_key_inflow_.has_value();
}
private:
void UpdateAndCountBlobIfNeeded() {
assert(!iter_->Valid() || iter_->status().ok());
if (!iter_->Valid()) {
status_ = iter_->status();
if (current_key_inflow_.has_value()) {
current_key_inflow_->Clear();
}
return;
}
TEST_SYNC_POINT(
"BlobCountingIterator::UpdateAndCountBlobIfNeeded:ProcessInFlow");
if (current_key_inflow_.has_value()) {
current_key_inflow_->Clear();
TEST_SYNC_POINT(
"BlobCountingIterator::UpdateAndCountBlobIfNeeded:CurrentKeyInflow");
status_ = current_key_inflow_->ProcessInFlow(key(), value());
if (!status_.ok()) {
return;
}
}
TEST_SYNC_POINT(
"BlobCountingIterator::UpdateAndCountBlobIfNeeded:AggregateInflow");
status_ = blob_garbage_meter_->ProcessInFlow(key(), value());
}
InternalIterator* iter_;
BlobGarbageMeter* blob_garbage_meter_;
std::optional<BlobGarbageMeter> current_key_inflow_;
Status status_;
};
+56 -2
View File
@@ -71,7 +71,8 @@ TEST(BlobCountingIteratorTest, CountBlobs) {
VectorIterator input(keys, values);
BlobGarbageMeter blob_garbage_meter;
BlobCountingIterator blob_counter(&input, &blob_garbage_meter);
BlobCountingIterator blob_counter(&input, &blob_garbage_meter,
/*track_current_key_inflow=*/false);
constexpr uint64_t first_expected_bytes =
first_size +
@@ -301,6 +302,58 @@ TEST(BlobCountingIteratorTest, CountBlobs) {
5 * second_expected_bytes);
}
TEST(BlobCountingIteratorTest, TracksCurrentKeyOnlyForResumableCompaction) {
constexpr size_t kNumEntries = 1000;
std::vector<std::string> keys;
std::vector<std::string> values;
keys.reserve(kNumEntries);
values.reserve(kNumEntries);
for (size_t i = 0; i < kNumEntries; ++i) {
keys.push_back(
test::KeyStr("key" + std::to_string(i), i + 1, kTypeBlobIndex));
BlobIndex::EncodeBlob(&values.emplace_back(), /*file_number=*/7,
/*offset=*/i * 100, /*size=*/100, kNoCompression);
}
for (const bool track_current_key : {false, true}) {
VectorIterator input(keys, values);
BlobGarbageMeter blob_garbage_meter;
size_t aggregate_parse_count = 0;
size_t current_key_parse_count = 0;
SyncPoint::GetInstance()->SetCallBack(
"BlobCountingIterator::UpdateAndCountBlobIfNeeded:AggregateInflow",
[&](void*) { ++aggregate_parse_count; });
SyncPoint::GetInstance()->SetCallBack(
"BlobCountingIterator::UpdateAndCountBlobIfNeeded:CurrentKeyInflow",
[&](void*) { ++current_key_parse_count; });
SyncPoint::GetInstance()->EnableProcessing();
BlobCountingIterator blob_counter(&input, &blob_garbage_meter,
track_current_key);
EXPECT_EQ(blob_counter.TracksCurrentKeyInflow(), track_current_key);
const BlobGarbageMeter* current_key_meter =
track_current_key ? &blob_counter.GetCurrentKeyInflow() : nullptr;
for (blob_counter.SeekToFirst(); blob_counter.Valid();
blob_counter.Next()) {
if (track_current_key) {
// This checks reuse of the optional meter object, not allocations made
// by its internal map while processing each key.
EXPECT_EQ(&blob_counter.GetCurrentKeyInflow(), current_key_meter);
}
}
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
ASSERT_OK(blob_counter.status());
EXPECT_EQ(aggregate_parse_count, kNumEntries);
EXPECT_EQ(current_key_parse_count, track_current_key ? kNumEntries : 0);
const auto flow = blob_garbage_meter.flows().find(7);
ASSERT_NE(flow, blob_garbage_meter.flows().end());
EXPECT_EQ(flow->second.GetInFlow().GetCount(), kNumEntries);
}
}
TEST(BlobCountingIteratorTest, CorruptBlobIndex) {
const std::vector<std::string> keys{
test::KeyStr("user_key", 1, kTypeBlobIndex)};
@@ -311,7 +364,8 @@ TEST(BlobCountingIteratorTest, CorruptBlobIndex) {
VectorIterator input(keys, values);
BlobGarbageMeter blob_garbage_meter;
BlobCountingIterator blob_counter(&input, &blob_garbage_meter);
BlobCountingIterator blob_counter(&input, &blob_garbage_meter,
/*track_current_key_inflow=*/false);
blob_counter.SeekToFirst();
ASSERT_FALSE(blob_counter.Valid());
+15 -12
View File
@@ -40,15 +40,16 @@ BlobFileBuilder::BlobFileBuilder(
const std::string& column_family_name, Env::WriteLifeTimeHint write_hint,
const std::shared_ptr<IOTracer>& io_tracer,
BlobFileCompletionCallback* blob_callback,
BlobFileCreationReason creation_reason,
BlobFileCreationReason creation_reason, std::string blob_output_path,
std::vector<std::string>* output_file_paths,
std::vector<BlobFileAddition>* blob_file_additions)
: BlobFileBuilder([versions]() { return versions->NewFileNumber(); }, fs,
immutable_options, mutable_cf_options, file_options,
write_options, db_id, db_session_id, job_id,
column_family_id, column_family_name, write_hint,
write_options, std::move(db_id), std::move(db_session_id),
job_id, column_family_id, column_family_name, write_hint,
io_tracer, blob_callback, creation_reason,
output_file_paths, blob_file_additions) {}
std::move(blob_output_path), output_file_paths,
blob_file_additions) {}
BlobFileBuilder::BlobFileBuilder(
std::function<uint64_t()> file_number_generator, FileSystem* fs,
@@ -59,7 +60,7 @@ BlobFileBuilder::BlobFileBuilder(
const std::string& column_family_name, Env::WriteLifeTimeHint write_hint,
const std::shared_ptr<IOTracer>& io_tracer,
BlobFileCompletionCallback* blob_callback,
BlobFileCreationReason creation_reason,
BlobFileCreationReason creation_reason, std::string blob_output_path,
std::vector<std::string>* output_file_paths,
std::vector<BlobFileAddition>* blob_file_additions)
: file_number_generator_(std::move(file_number_generator)),
@@ -90,6 +91,7 @@ BlobFileBuilder::BlobFileBuilder(
io_tracer_(io_tracer),
blob_callback_(blob_callback),
creation_reason_(creation_reason),
blob_output_path_(std::move(blob_output_path)),
output_file_paths_(output_file_paths),
blob_file_additions_(blob_file_additions),
blob_count_(0),
@@ -102,16 +104,14 @@ BlobFileBuilder::BlobFileBuilder(
assert(output_file_paths_);
assert(output_file_paths_->empty());
assert(blob_file_additions_);
assert(blob_file_additions_->empty());
// Resumable compactions restore finalized additions before creating a new
// builder. Any newly generated blob files are appended to that state.
}
BlobFileBuilder::~BlobFileBuilder() = default;
std::string BlobFileBuilder::GetBlobFilePath(uint64_t blob_file_number) const {
assert(immutable_options_);
assert(!immutable_options_->cf_paths.empty());
return BlobFileName(immutable_options_->cf_paths.front().path,
blob_file_number);
return BlobFileName(blob_output_path_, blob_file_number);
}
Status BlobFileBuilder::Add(const Slice& key, const Slice& value,
@@ -194,6 +194,9 @@ Status BlobFileBuilder::OpenBlobFileIfNeeded() {
assert(file_number_generator_);
const uint64_t blob_file_number = file_number_generator_();
if (blob_file_number == kInvalidBlobFileNumber) {
return Status::Incomplete("Blob file number range exhausted");
}
std::string blob_file_path = GetBlobFilePath(blob_file_number);
@@ -353,7 +356,7 @@ Status BlobFileBuilder::CloseBlobFile() {
s = blob_callback_->OnBlobFileCompleted(
GetBlobFilePath(blob_file_number), column_family_name_, job_id_,
blob_file_number, creation_reason_, s, checksum_value, checksum_method,
blob_count_, blob_bytes_);
blob_count_, blob_bytes_, /*set_bg_error_on_space_limit=*/true);
}
assert(blob_file_additions_);
@@ -400,7 +403,7 @@ void BlobFileBuilder::Abandon(const Status& s) {
->OnBlobFileCompleted(GetBlobFilePath(blob_file_number),
column_family_name_, job_id_, blob_file_number,
creation_reason_, s, "", "", blob_count_,
blob_bytes_)
blob_bytes_, /*set_bg_error_on_space_limit=*/true)
.PermitUncheckedError();
}
+3
View File
@@ -48,6 +48,7 @@ class BlobFileBuilder {
const std::shared_ptr<IOTracer>& io_tracer,
BlobFileCompletionCallback* blob_callback,
BlobFileCreationReason creation_reason,
std::string blob_output_path,
std::vector<std::string>* output_file_paths,
std::vector<BlobFileAddition>* blob_file_additions);
@@ -63,6 +64,7 @@ class BlobFileBuilder {
const std::shared_ptr<IOTracer>& io_tracer,
BlobFileCompletionCallback* blob_callback,
BlobFileCreationReason creation_reason,
std::string blob_output_path,
std::vector<std::string>* output_file_paths,
std::vector<BlobFileAddition>* blob_file_additions);
@@ -112,6 +114,7 @@ class BlobFileBuilder {
std::shared_ptr<IOTracer> io_tracer_;
BlobFileCompletionCallback* blob_callback_;
BlobFileCreationReason creation_reason_;
const std::string blob_output_path_;
std::vector<std::string>* output_file_paths_;
std::vector<BlobFileAddition>* blob_file_additions_;
std::unique_ptr<BlobLogWriter> writer_;
+14 -7
View File
@@ -148,7 +148,8 @@ TEST_F(BlobFileBuilderTest, BuildAndCheckOneFile) {
&file_options_, &write_options_, "" /*db_id*/, "" /*db_session_id*/,
job_id, column_family_id, column_family_name, write_hint,
nullptr /*IOTracer*/, nullptr /*BlobFileCompletionCallback*/,
BlobFileCreationReason::kFlush, &blob_file_paths, &blob_file_additions);
BlobFileCreationReason::kFlush, immutable_options.cf_paths.front().path,
&blob_file_paths, &blob_file_additions);
std::vector<std::pair<std::string, std::string>> expected_key_value_pairs(
number_of_blobs);
@@ -232,7 +233,8 @@ TEST_F(BlobFileBuilderTest, BuildAndCheckMultipleFiles) {
&file_options_, &write_options_, "" /*db_id*/, "" /*db_session_id*/,
job_id, column_family_id, column_family_name, write_hint,
nullptr /*IOTracer*/, nullptr /*BlobFileCompletionCallback*/,
BlobFileCreationReason::kFlush, &blob_file_paths, &blob_file_additions);
BlobFileCreationReason::kFlush, immutable_options.cf_paths.front().path,
&blob_file_paths, &blob_file_additions);
std::vector<std::pair<std::string, std::string>> expected_key_value_pairs(
number_of_blobs);
@@ -319,7 +321,8 @@ TEST_F(BlobFileBuilderTest, InlinedValues) {
&file_options_, &write_options_, "" /*db_id*/, "" /*db_session_id*/,
job_id, column_family_id, column_family_name, write_hint,
nullptr /*IOTracer*/, nullptr /*BlobFileCompletionCallback*/,
BlobFileCreationReason::kFlush, &blob_file_paths, &blob_file_additions);
BlobFileCreationReason::kFlush, immutable_options.cf_paths.front().path,
&blob_file_paths, &blob_file_additions);
for (size_t i = 0; i < number_of_blobs; ++i) {
const std::string key = std::to_string(i);
@@ -373,7 +376,8 @@ TEST_F(BlobFileBuilderTest, Compression) {
&file_options_, &write_options_, "" /*db_id*/, "" /*db_session_id*/,
job_id, column_family_id, column_family_name, write_hint,
nullptr /*IOTracer*/, nullptr /*BlobFileCompletionCallback*/,
BlobFileCreationReason::kFlush, &blob_file_paths, &blob_file_additions);
BlobFileCreationReason::kFlush, immutable_options.cf_paths.front().path,
&blob_file_paths, &blob_file_additions);
const std::string key("1");
const std::string uncompressed_value(value_size, 'x');
@@ -452,7 +456,8 @@ TEST_F(BlobFileBuilderTest, CompressionError) {
&file_options_, &write_options_, "" /*db_id*/, "" /*db_session_id*/,
job_id, column_family_id, column_family_name, write_hint,
nullptr /*IOTracer*/, nullptr /*BlobFileCompletionCallback*/,
BlobFileCreationReason::kFlush, &blob_file_paths, &blob_file_additions);
BlobFileCreationReason::kFlush, immutable_options.cf_paths.front().path,
&blob_file_paths, &blob_file_additions);
SyncPoint::GetInstance()->SetCallBack(
"LegacyForceBuiltinCompression:TamperWithStatus", [](void* arg) {
@@ -532,7 +537,8 @@ TEST_F(BlobFileBuilderTest, Checksum) {
&file_options_, &write_options_, "" /*db_id*/, "" /*db_session_id*/,
job_id, column_family_id, column_family_name, write_hint,
nullptr /*IOTracer*/, nullptr /*BlobFileCompletionCallback*/,
BlobFileCreationReason::kFlush, &blob_file_paths, &blob_file_additions);
BlobFileCreationReason::kFlush, immutable_options.cf_paths.front().path,
&blob_file_paths, &blob_file_additions);
const std::string key("1");
const std::string value("deadbeef");
@@ -631,7 +637,8 @@ TEST_P(BlobFileBuilderIOErrorTest, IOError) {
&file_options_, &write_options_, "" /*db_id*/, "" /*db_session_id*/,
job_id, column_family_id, column_family_name, write_hint,
nullptr /*IOTracer*/, nullptr /*BlobFileCompletionCallback*/,
BlobFileCreationReason::kFlush, &blob_file_paths, &blob_file_additions);
BlobFileCreationReason::kFlush, immutable_options.cf_paths.front().path,
&blob_file_paths, &blob_file_additions);
SyncPoint::GetInstance()->SetCallBack(sync_point_, [this](void* arg) {
Status* const s = static_cast<Status*>(arg);
+62 -15
View File
@@ -12,6 +12,7 @@
#include "db/event_helpers.h"
#include "file/sst_file_manager_impl.h"
#include "rocksdb/status.h"
#include "util/cast_util.h"
namespace ROCKSDB_NAMESPACE {
@@ -38,14 +39,12 @@ class BlobFileCompletionCallback {
job_id, creation_reason);
}
Status OnBlobFileCompleted(const std::string& file_name,
const std::string& column_family_name, int job_id,
uint64_t file_number,
BlobFileCreationReason creation_reason,
const Status& report_status,
const std::string& checksum_value,
const std::string& checksum_method,
uint64_t blob_count, uint64_t blob_bytes) {
Status OnBlobFileCompleted(
const std::string& file_name, const std::string& column_family_name,
int job_id, uint64_t file_number, BlobFileCreationReason creation_reason,
const Status& report_status, const std::string& checksum_value,
const std::string& checksum_method, uint64_t blob_count,
uint64_t blob_bytes, bool set_bg_error_on_space_limit) {
Status s;
auto sfm = static_cast<SstFileManagerImpl*>(sst_file_manager_);
@@ -56,24 +55,72 @@ class BlobFileCompletionCallback {
s = Status::SpaceLimit("Max allowed space was reached");
TEST_SYNC_POINT(
"BlobFileCompletionCallback::CallBack::MaxAllowedSpaceReached");
InstrumentedMutexLock l(mutex_);
error_handler_->SetBGError(s, BackgroundErrorReason::kFlush);
if (set_bg_error_on_space_limit) {
// This callback historically reports space limits as flush errors.
SetBGError(s);
}
}
}
// Notify the listeners.
NotifyCreationFinished(
file_name, column_family_name, job_id, file_number, creation_reason,
(!report_status.ok() ? report_status : s), checksum_value,
checksum_method, blob_count, blob_bytes);
return s;
}
// For a complete blob file that is kept even when the space limit is
// reached. The file is registered with the SstFileManager at `file_size`,
// which cannot fail, and its creation is reported as successful. Returns
// whether the max allowed space is reached; the caller sets the background
// error, because this does not acquire the DB mutex.
bool OnKeptBlobFileCompleted(
const std::string& file_name, const std::string& column_family_name,
int job_id, uint64_t file_number, BlobFileCreationReason creation_reason,
const std::string& checksum_value, const std::string& checksum_method,
uint64_t blob_count, uint64_t blob_bytes, uint64_t file_size) {
bool space_limit_reached = false;
SstFileManagerImpl* sfm =
static_cast_with_check<SstFileManagerImpl>(sst_file_manager_);
if (sfm) {
sfm->OnAddFile(file_name, file_size).PermitUncheckedError();
space_limit_reached = sfm->IsMaxAllowedSpaceReached();
if (space_limit_reached) {
TEST_SYNC_POINT(
"BlobFileCompletionCallback::CallBack::MaxAllowedSpaceReached");
}
}
NotifyCreationFinished(file_name, column_family_name, job_id, file_number,
creation_reason, Status::OK(), checksum_value,
checksum_method, blob_count, blob_bytes);
return space_limit_reached;
}
void SetBGError(const Status& s) {
InstrumentedMutexLock l(mutex_);
error_handler_->SetBGError(s, BackgroundErrorReason::kFlush);
}
private:
void NotifyCreationFinished(const std::string& file_name,
const std::string& column_family_name, int job_id,
uint64_t file_number,
BlobFileCreationReason creation_reason,
const Status& status,
const std::string& checksum_value,
const std::string& checksum_method,
uint64_t blob_count, uint64_t blob_bytes) {
EventHelpers::LogAndNotifyBlobFileCreationFinished(
event_logger_, listeners_, dbname_, column_family_name, file_name,
job_id, file_number, creation_reason,
(!report_status.ok() ? report_status : s),
job_id, file_number, creation_reason, status,
(checksum_value.empty() ? kUnknownFileChecksum : checksum_value),
(checksum_method.empty() ? kUnknownFileChecksumFuncName
: checksum_method),
blob_count, blob_bytes);
return s;
}
private:
SstFileManager* sst_file_manager_;
InstrumentedMutex* mutex_;
ErrorHandler* error_handler_;
+73 -31
View File
@@ -27,6 +27,7 @@
#include "rocksdb/file_system.h"
#include "util/aligned_buffer.h"
#include "util/compression.h"
#include "util/defer.h"
#include "util/mutexlock.h"
namespace ROCKSDB_NAMESPACE {
@@ -158,6 +159,12 @@ BlobFilePartitionManager::~BlobFilePartitionManager() {
}
}
BlobFilePartitionManager::DeferredFile::~DeferredFile() {
// Only a later flush of the generation reads `seal_status`, and the DB may
// close before one runs.
seal_status.PermitUncheckedError();
}
void BlobFilePartitionManager::ResetPartitionState(Partition* partition) {
partition->writer.reset();
partition->file_number = 0;
@@ -171,6 +178,19 @@ void BlobFilePartitionManager::ResetPartitionState(Partition* partition) {
partition->sync_required = false;
}
BlobFilePartitionManager::DeferredFile
BlobFilePartitionManager::DetachPartitionFile(Partition* partition) {
DeferredFile deferred;
deferred.writer = std::move(partition->writer);
deferred.file_number = partition->file_number;
deferred.blob_count = partition->blob_count;
deferred.total_blob_bytes = partition->total_blob_bytes;
deferred.garbage_blob_count = partition->garbage_blob_count;
deferred.garbage_blob_bytes = partition->garbage_blob_bytes;
ResetPartitionState(partition);
return deferred;
}
void BlobFilePartitionManager::AddFilePartitionMapping(uint64_t file_number,
uint32_t partition_idx) {
WriteLock lock(&file_partition_mutex_);
@@ -255,7 +275,7 @@ Status BlobFilePartitionManager::OpenNewBlobFile(Partition* partition,
Status BlobFilePartitionManager::SealActiveBlobFile(
const WriteOptions& write_options, Partition* partition,
SealedFile* sealed_file) {
SealedFile* sealed_file, bool* space_limit_reached) {
assert(partition != nullptr);
assert(partition->writer);
assert(sealed_file != nullptr);
@@ -264,10 +284,8 @@ Status BlobFilePartitionManager::SealActiveBlobFile(
Status s =
FinalizeBlobFile(write_options, partition->writer.get(), file_number,
partition->blob_count, partition->total_blob_bytes,
&sealed_file->addition);
&sealed_file->addition, space_limit_reached);
if (!s.ok()) {
RemoveFilePartitionMapping(file_number);
ResetPartitionState(partition);
return s;
}
@@ -284,11 +302,15 @@ Status BlobFilePartitionManager::SealDeferredFile(
assert(deferred->writer);
assert(sealed_file != nullptr);
Status s = FinalizeBlobFile(
write_options, deferred->writer.get(), deferred->file_number,
deferred->blob_count, deferred->total_blob_bytes, &sealed_file->addition);
// As for its SST file, the flush sets a reached space limit as the
// background error after it commits. An error set earlier would make the
// flush roll back instead.
bool space_limit_reached = false;
Status s = FinalizeBlobFile(write_options, deferred->writer.get(),
deferred->file_number, deferred->blob_count,
deferred->total_blob_bytes,
&sealed_file->addition, &space_limit_reached);
if (!s.ok()) {
RemoveFilePartitionMapping(deferred->file_number);
return s;
}
@@ -301,9 +323,10 @@ Status BlobFilePartitionManager::SealDeferredFile(
Status BlobFilePartitionManager::FinalizeBlobFile(
const WriteOptions& write_options, BlobLogWriter* writer,
uint64_t file_number, uint64_t blob_count, uint64_t total_blob_bytes,
BlobFileAddition* addition) {
BlobFileAddition* addition, bool* space_limit_reached) {
assert(writer != nullptr);
assert(addition != nullptr);
assert(space_limit_reached != nullptr);
BlobLogFooter footer;
footer.blob_count = blob_count;
@@ -317,13 +340,13 @@ Status BlobFilePartitionManager::FinalizeBlobFile(
}
if (blob_callback_ != nullptr) {
// `mutex_` is held, and the DB mutex must not be acquired under it.
const std::string blob_file_path = BlobFileName(db_path_, file_number);
s = blob_callback_->OnBlobFileCompleted(
blob_file_path, column_family_name_, /*job_id=*/0, file_number,
BlobFileCreationReason::kFlush, s, checksum_value, checksum_method,
blob_count, total_blob_bytes);
if (!s.ok()) {
return s;
if (blob_callback_->OnKeptBlobFileCompleted(
blob_file_path, column_family_name_, /*job_id=*/0, file_number,
BlobFileCreationReason::kFlush, checksum_value, checksum_method,
blob_count, total_blob_bytes, writer->get_file_size())) {
*space_limit_reached = true;
}
}
@@ -477,6 +500,15 @@ Status BlobFilePartitionManager::WriteBlob(
value) %
num_partitions_);
bool space_limit_reached = false;
// Setting the background error takes the DB mutex, so it must run after
// `mutex_` is released.
Defer set_space_limit_error([&]() {
if (space_limit_reached) {
blob_callback_->SetBGError(
Status::SpaceLimit("Max allowed space was reached"));
}
});
{
MutexLock lock(&mutex_);
Partition* partition = partitions_[selected_partition_idx].get();
@@ -486,11 +518,16 @@ Status BlobFilePartitionManager::WriteBlob(
return Status::OK();
}
SealedFile sealed_file;
Status s = SealActiveBlobFile(write_options, partition, &sealed_file);
if (s.ok()) {
current_generation_sealed_files_.push_back(std::move(sealed_file));
Status s = SealActiveBlobFile(write_options, partition, &sealed_file,
&space_limit_reached);
if (!s.ok()) {
DeferredFile failed = DetachPartitionFile(partition);
failed.seal_status = s;
current_generation_failed_files_.push_back(std::move(failed));
return s;
}
return s;
current_generation_sealed_files_.push_back(std::move(sealed_file));
return Status::OK();
};
const uint64_t record_size =
@@ -550,21 +587,16 @@ void BlobFilePartitionManager::RotateCurrentGeneration(uint64_t memtable_id) {
batch.memtable_id = memtable_id;
batch.sealed_files = std::move(current_generation_sealed_files_);
current_generation_sealed_files_.clear();
for (auto& failed : current_generation_failed_files_) {
batch.deferred_files.emplace_back(std::move(failed));
}
current_generation_failed_files_.clear();
for (auto& partition : partitions_) {
if (!partition->writer) {
continue;
}
DeferredFile deferred;
deferred.writer = std::move(partition->writer);
deferred.file_number = partition->file_number;
deferred.blob_count = partition->blob_count;
deferred.total_blob_bytes = partition->total_blob_bytes;
deferred.garbage_blob_count = partition->garbage_blob_count;
deferred.garbage_blob_bytes = partition->garbage_blob_bytes;
ResetPartitionState(partition.get());
batch.deferred_files.emplace_back(std::move(deferred));
batch.deferred_files.emplace_back(DetachPartitionFile(partition.get()));
}
pending_generations_.emplace_back(std::move(batch));
@@ -603,14 +635,18 @@ Status BlobFilePartitionManager::PrepareFlushAdditions(
}
GenerationBatch& batch = *batch_ptr;
while (!batch.deferred_files.empty()) {
DeferredFile deferred = std::move(batch.deferred_files.front());
batch.deferred_files.pop_front();
DeferredFile& deferred = batch.deferred_files.front();
if (!deferred.seal_status.ok()) {
return deferred.seal_status;
}
SealedFile sealed_file;
Status s = SealDeferredFile(write_options, &deferred, &sealed_file);
if (!s.ok()) {
deferred.seal_status = s;
return s;
}
batch.deferred_files.pop_front();
// Keep each successfully sealed file attached to the generation
// immediately. If a later seal or the flush job itself fails, retry must
// reuse these exact on-disk files instead of finalizing replacements.
@@ -658,6 +694,12 @@ Status BlobFilePartitionManager::MarkBlobWriteAsGarbage(uint64_t file_number,
return Status::OK();
}
for (auto& failed : current_generation_failed_files_) {
if (MarkDeferredFileGarbage(&failed, file_number, blob_count, blob_bytes)) {
return Status::OK();
}
}
for (auto& batch : pending_generations_) {
for (auto& deferred : batch.deferred_files) {
if (MarkDeferredFileGarbage(&deferred, file_number, blob_count,
+20 -3
View File
@@ -183,6 +183,11 @@ class BlobFilePartitionManager {
};
struct DeferredFile {
DeferredFile() = default;
DeferredFile(DeferredFile&&) = default;
DeferredFile& operator=(DeferredFile&&) = default;
~DeferredFile();
// Blob writer moved out of an active partition at memtable rotation.
std::unique_ptr<BlobLogWriter> writer;
// Metadata preserved until flush preparation seals the file.
@@ -192,6 +197,10 @@ class BlobFilePartitionManager {
// Failed transformed writes already appended before the file is sealed.
uint64_t garbage_blob_count = 0;
uint64_t garbage_blob_bytes = 0;
// Set once sealing fails. The writer cannot be finalized again, and
// memtables still reference the file, so every flush of its generation
// fails with this status.
Status seal_status;
};
struct SealedFile {
@@ -216,17 +225,22 @@ class BlobFilePartitionManager {
void ResetPartitionState(Partition* partition);
// Returns the queued generation of `memtable_id`, or null.
GenerationBatch* FindPendingGeneration(uint64_t memtable_id);
// Moves a partition's open file out of the partition and resets it.
DeferredFile DetachPartitionFile(Partition* partition);
// Marks a blob file as manager-owned until it is committed or abandoned.
void AddFilePartitionMapping(uint64_t file_number, uint32_t partition_idx);
// Stops tracking a blob file after open/seal failure.
// Stops tracking a blob file after open failure.
void RemoveFilePartitionMapping(uint64_t file_number);
// Opens a new active blob file for one partition.
Status OpenNewBlobFile(Partition* partition, uint32_t column_family_id,
CompressionType compression, uint32_t partition_idx);
// Finalizes one active partition file and returns the MANIFEST addition.
// Sets `*space_limit_reached` if the SstFileManager's max allowed space is
// reached; the seal still succeeds.
Status SealActiveBlobFile(const WriteOptions& write_options,
Partition* partition, SealedFile* sealed_file);
Partition* partition, SealedFile* sealed_file,
bool* space_limit_reached);
// Finalizes one deferred file that was carried over to flush preparation.
Status SealDeferredFile(const WriteOptions& write_options,
DeferredFile* deferred, SealedFile* sealed_file);
@@ -235,7 +249,8 @@ class BlobFilePartitionManager {
Status FinalizeBlobFile(const WriteOptions& write_options,
BlobLogWriter* writer, uint64_t file_number,
uint64_t blob_count, uint64_t total_blob_bytes,
BlobFileAddition* addition);
BlobFileAddition* addition,
bool* space_limit_reached);
// Appends one initial-garbage record to the flush output if any failed
// transformed writes targeted the sealed blob file.
static void AddSealedFileGarbage(const SealedFile& sealed_file,
@@ -294,6 +309,8 @@ class BlobFilePartitionManager {
mutable port::Mutex mutex_;
// Files already sealed while the current mutable memtable was active.
std::vector<SealedFile> current_generation_sealed_files_;
// Files whose seal failed while the current mutable memtable was active.
std::vector<DeferredFile> current_generation_failed_files_;
// FIFO immutable memtable generations waiting to be flushed.
std::deque<GenerationBatch> pending_generations_;
+23
View File
@@ -59,6 +59,14 @@ class BlobGarbageMeter {
out_flow_.Add(bytes);
assert(IsValid());
}
void AddOutFlow(uint64_t count, uint64_t bytes) {
out_flow_.Add(count, bytes);
assert(IsValid());
}
void AddGarbage(uint64_t count, uint64_t bytes) {
in_flow_.Add(count, bytes);
assert(IsValid());
}
const BlobStats& GetInFlow() const { return in_flow_; }
const BlobStats& GetOutFlow() const { return out_flow_; }
@@ -90,6 +98,21 @@ class BlobGarbageMeter {
Status ProcessInFlow(const Slice& key, const Slice& value);
Status ProcessOutFlow(const Slice& key, const Slice& value);
void ProcessOutFlow(const BlobGarbageMeter& inflow) {
for (const auto& [file_number, flow] : inflow.flows()) {
const BlobStats& stats = flow.GetInFlow();
flows_[file_number].AddOutFlow(stats.GetCount(), stats.GetBytes());
}
}
// Imports a garbage delta computed by another compaction process.
void AddGarbage(uint64_t blob_file_number, uint64_t count, uint64_t bytes) {
// A garbage delta is equivalent to inflow with no matching outflow.
flows_[blob_file_number].AddGarbage(count, bytes);
}
void Clear() { flows_.clear(); }
const std::unordered_map<uint64_t, BlobInOutFlow>& flows() const {
return flows_;
}
+3 -1
View File
@@ -106,7 +106,9 @@ class BlobIndex {
Status DecodeFrom(Slice slice) {
const char* kErrorMessage = "Error while decoding blob index";
assert(slice.size() > 0);
if (slice.empty()) {
return Status::Corruption(kErrorMessage, "Empty blob index");
}
type_ = static_cast<Type>(*slice.data());
if (type_ >= Type::kUnknown) {
return Status::Corruption(kErrorMessage,
+2
View File
@@ -70,6 +70,8 @@ class BlobLogWriter {
uint64_t get_log_number() const { return log_number_; }
uint64_t get_file_size() const { return block_offset_; }
Status Sync(const WriteOptions& write_options);
private:
+569
View File
@@ -41,6 +41,73 @@ class DBBlobBasicTest : public DBTestBase {
protected:
DBBlobBasicTest()
: DBTestBase("db_blob_basic_test", /* env_do_fsync */ false) {}
Options MissingBlobRepairOptions() {
Options options = GetDefaultOptions();
options.create_if_missing = true;
options.enable_blob_files = true;
options.min_blob_size = 0;
options.enable_blob_garbage_collection = false;
options.disable_auto_compactions = true;
options.num_levels = 3;
options.level_compaction_dynamic_level_bytes = false;
return options;
}
void AssertBlobReference(const Slice& key, uint64_t blob_file_number) {
PinnableSlice value;
bool is_blob_index = false;
DBImpl::GetImplOptions get_options;
get_options.column_family = db_->DefaultColumnFamily();
get_options.value = &value;
get_options.is_blob_index = &is_blob_index;
ASSERT_OK(dbfull()->GetImpl(ReadOptions(), key, get_options));
ASSERT_TRUE(is_blob_index);
BlobIndex blob_index;
ASSERT_OK(blob_index.DecodeFrom(value));
ASSERT_EQ(blob_index.file_number(), blob_file_number);
}
void RecoverMissingNonOldestBlob(const Options& options,
bool preserve_older_value,
uint64_t* missing_blob_file) {
DestroyAndReopen(options);
ASSERT_OK(Put("a", "va"));
ASSERT_OK(Put("c", "vc"));
if (preserve_older_value) {
ASSERT_OK(Put("b", "old"));
}
ASSERT_OK(Flush());
const Snapshot* snapshot =
preserve_older_value ? db_->GetSnapshot() : nullptr;
{
Defer release_snapshot([this, snapshot]() {
if (snapshot) {
db_->ReleaseSnapshot(snapshot);
}
});
ASSERT_OK(Put("b", "vb"));
ASSERT_OK(Put("d", "vd"));
ASSERT_OK(Flush());
const std::vector<uint64_t> blob_files = GetBlobFileNumbers();
ASSERT_EQ(blob_files.size(), 2);
*missing_blob_file = blob_files.back();
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
ASSERT_EQ(TotalTableFiles(), 1);
}
Close();
ASSERT_OK(env_->DeleteFile(BlobFileName(dbname_, *missing_blob_file)));
Options recovery_options = options;
recovery_options.best_efforts_recovery = true;
Reopen(recovery_options);
const std::vector<uint64_t> live_blob_files = GetBlobFileNumbers();
ASSERT_EQ(live_blob_files.size(), 1);
ASSERT_NE(live_blob_files.front(), *missing_blob_file);
ASSERT_EQ(TotalTableFiles(), 1);
ASSERT_NO_FATAL_FAILURE(AssertBlobReference("b", *missing_blob_file));
ASSERT_NO_FATAL_FAILURE(AssertBlobReference("d", *missing_blob_file));
}
};
class BlobFileChecksumCapturingFS : public FileSystemWrapper {
@@ -1483,6 +1550,335 @@ TEST_F(DBBlobBasicTest, BestEffortsRecovery_MissingNewestBlobFile) {
ASSERT_EQ("value" + std::to_string(kNumTableFiles - 2), value);
}
TEST_F(DBBlobBasicTest, RepairMissingNonOldestBlobAfterBestEffortsRecovery) {
Options options = MissingBlobRepairOptions();
options.statistics = CreateDBStatistics();
uint64_t missing_blob_file = 0;
ASSERT_NO_FATAL_FAILURE(
RecoverMissingNonOldestBlob(options, false, &missing_blob_file));
const std::vector<uint64_t> healthy_blob_files = GetBlobFileNumbers();
ColumnFamilyMetaData before;
db_->GetColumnFamilyMetaData(db_->DefaultColumnFamily(), &before);
CompactRangeOptions repair;
repair.skip_missing_blob_files = true;
repair.bottommost_level_compaction = BottommostLevelCompaction::kSkip;
repair.max_subcompactions = 2;
repair.blob_garbage_collection_policy = BlobGarbageCollectionPolicy::kDisable;
ASSERT_OK(options.statistics->Reset());
ASSERT_OK(db_->CompactRange(repair, nullptr, nullptr));
ASSERT_GT(options.statistics->getTickerCount(COMPACT_READ_BYTES), 0);
ASSERT_EQ(options.statistics->getTickerCount(BLOB_DB_BLOB_FILE_BYTES_READ),
0);
ColumnFamilyMetaData after;
db_->GetColumnFamilyMetaData(db_->DefaultColumnFamily(), &after);
std::set<std::string> input_files;
for (const LevelMetaData& before_level : before.levels) {
for (const SstFileMetaData& input : before_level.files) {
input_files.insert(input.name);
}
}
for (const LevelMetaData& after_level : after.levels) {
for (const SstFileMetaData& output : after_level.files) {
ASSERT_EQ(input_files.count(output.name), 0);
}
}
ASSERT_EQ(GetBlobFileNumbers(), healthy_blob_files);
ASSERT_EQ(Get("a"), "va");
ASSERT_EQ(Get("c"), "vc");
std::string value;
ASSERT_EQ(db_->Get(ReadOptions(), "b", &value).code(), Status::kNotFound);
ASSERT_EQ(db_->Get(ReadOptions(), "d", &value).code(), Status::kNotFound);
Reopen(options);
ASSERT_EQ(Get("a"), "va");
ASSERT_EQ(Get("c"), "vc");
ASSERT_EQ(db_->Get(ReadOptions(), "b", &value).code(), Status::kNotFound);
ASSERT_EQ(db_->Get(ReadOptions(), "d", &value).code(), Status::kNotFound);
}
TEST_F(DBBlobBasicTest, MissingBlobRepairIsScopedToCompactRange) {
Options options = MissingBlobRepairOptions();
options.paranoid_checks = false;
uint64_t missing_blob_file = 0;
ASSERT_NO_FATAL_FAILURE(
RecoverMissingNonOldestBlob(options, false, &missing_blob_file));
ASSERT_OK(Put("b", "replacement"));
ASSERT_OK(Flush());
const Status normal_compaction =
db_->CompactRange(CompactRangeOptions(), nullptr, nullptr);
ASSERT_EQ(normal_compaction.code(), Status::kCorruption);
ASSERT_NE(normal_compaction.ToString().find("unknown blob file"),
std::string::npos);
CompactRangeOptions repair;
repair.skip_missing_blob_files = true;
ASSERT_OK(db_->CompactRange(repair, nullptr, nullptr));
ASSERT_EQ(Get("b"), "replacement");
ASSERT_EQ(Get("a"), "va");
std::string value;
ASSERT_EQ(db_->Get(ReadOptions(), "d", &value).code(), Status::kNotFound);
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
Reopen(options);
ASSERT_EQ(Get("b"), "replacement");
const uint64_t later_missing_blob = GetBlobFileNumbers().back();
Close();
ASSERT_OK(env_->DeleteFile(BlobFileName(dbname_, later_missing_blob)));
Reopen(options);
const Status later_corruption = db_->Get(ReadOptions(), "b", &value);
ASSERT_FALSE(later_corruption.ok());
ASSERT_TRUE(later_corruption.IsCorruption() || later_corruption.IsIOError() ||
later_corruption.IsNotFound())
<< later_corruption.ToString();
}
TEST_F(DBBlobBasicTest, RepairMissingBlobsAcrossSubcompactions) {
Options options = MissingBlobRepairOptions();
options.statistics = CreateDBStatistics();
options.compression = kNoCompression;
options.target_file_size_base = 1024 * 1024;
BlockBasedTableOptions table_options;
table_options.block_size = 256;
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
DestroyAndReopen(options);
constexpr int kNumKeys = 512;
for (int key = 0; key < kNumKeys; key += 2) {
ASSERT_OK(Put(Key(key), "healthy"));
}
ASSERT_OK(Flush());
const uint64_t healthy_blob_file = GetBlobFileNumbers().back();
for (int key = 1; key < kNumKeys; key += 2) {
ASSERT_OK(Put(Key(key), "lost"));
}
ASSERT_OK(Flush());
const uint64_t missing_blob_file = GetBlobFileNumbers().back();
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
ASSERT_EQ(TotalTableFiles(), 1);
Close();
ASSERT_OK(env_->DeleteFile(BlobFileName(dbname_, missing_blob_file)));
Options recovery_options = options;
recovery_options.best_efforts_recovery = true;
Reopen(recovery_options);
ASSERT_EQ(GetBlobFileNumbers(), std::vector<uint64_t>{healthy_blob_file});
ASSERT_NO_FATAL_FAILURE(AssertBlobReference(Key(1), missing_blob_file));
ASSERT_OK(db_->SetOptions({{"target_file_size_base", "1024"}}));
CompactRangeOptions repair;
repair.skip_missing_blob_files = true;
repair.max_subcompactions = 4;
ASSERT_OK(options.statistics->Reset());
ASSERT_OK(db_->CompactRange(repair, nullptr, nullptr));
HistogramData subcompactions;
options.statistics->histogramData(NUM_SUBCOMPACTIONS_SCHEDULED,
&subcompactions);
ASSERT_GT(subcompactions.max, 1);
ASSERT_EQ(options.statistics->getTickerCount(BLOB_DB_BLOB_FILE_BYTES_READ),
0);
std::string value;
for (int key = 0; key < kNumKeys; ++key) {
if (key % 2 == 0) {
ASSERT_OK(db_->Get(ReadOptions(), Key(key), &value));
ASSERT_EQ(value, "healthy");
} else {
ASSERT_EQ(db_->Get(ReadOptions(), Key(key), &value).code(),
Status::kNotFound);
}
}
}
TEST_F(DBBlobBasicTest, RepairPropagatesKnownBlobFileIoFailureDuringGc) {
Options options = MissingBlobRepairOptions();
DestroyAndReopen(options);
ASSERT_OK(Put("key", "value"));
ASSERT_OK(Flush());
const std::vector<uint64_t> blob_files = GetBlobFileNumbers();
ASSERT_EQ(blob_files.size(), 1);
Close();
ASSERT_OK(env_->DeleteFile(BlobFileName(dbname_, blob_files.front())));
Reopen(options);
ASSERT_EQ(GetBlobFileNumbers(), blob_files);
ASSERT_NO_FATAL_FAILURE(AssertBlobReference("key", blob_files.front()));
std::atomic<size_t> blob_file_opens{0};
SyncPoint::GetInstance()->SetCallBack(
"BlobFileReader::OpenFile:NewRandomAccessFile",
[&blob_file_opens](void*) { ++blob_file_opens; });
Defer clear_sync_points([]() {
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
});
SyncPoint::GetInstance()->EnableProcessing();
CompactRangeOptions repair;
repair.skip_missing_blob_files = true;
repair.blob_garbage_collection_policy = BlobGarbageCollectionPolicy::kForce;
repair.blob_garbage_collection_age_cutoff = 1.0;
const Status status = db_->CompactRange(repair, nullptr, nullptr);
ASSERT_FALSE(status.ok());
ASSERT_TRUE(status.IsIOError() || status.IsCorruption() ||
status.IsNotFound())
<< status.ToString();
ASSERT_GT(blob_file_opens.load(), 0);
ASSERT_EQ(GetBlobFileNumbers(), blob_files);
ASSERT_NO_FATAL_FAILURE(AssertBlobReference("key", blob_files.front()));
}
TEST_F(DBBlobBasicTest, RepairMissingBlobPreservesSnapshotDeletionSemantics) {
for (const bool use_merge : {false, true}) {
SCOPED_TRACE("use_merge=" + std::to_string(use_merge));
Options options = MissingBlobRepairOptions();
if (use_merge) {
options.merge_operator = MergeOperators::CreateStringAppendOperator();
}
uint64_t missing_blob_file = 0;
ASSERT_NO_FATAL_FAILURE(
RecoverMissingNonOldestBlob(options, true, &missing_blob_file));
{
const Snapshot* snapshot = db_->GetSnapshot();
Defer release_snapshot(
[this, snapshot]() { db_->ReleaseSnapshot(snapshot); });
if (use_merge) {
ASSERT_OK(Merge("b", "replacement"));
} else {
ASSERT_OK(Put("b", "replacement"));
}
ASSERT_OK(Flush());
CompactRangeOptions repair;
repair.skip_missing_blob_files = true;
ASSERT_OK(db_->CompactRange(repair, nullptr, nullptr));
ASSERT_EQ(Get("b"), "replacement");
ReadOptions read;
read.snapshot = snapshot;
std::string value;
ASSERT_EQ(db_->Get(read, "b", &value).code(), Status::kNotFound);
ASSERT_OK(db_->Get(read, "a", &value));
ASSERT_EQ(value, "va");
}
Reopen(options);
ASSERT_EQ(Get("b"), "replacement");
}
}
TEST_F(DBBlobBasicTest, RepairMissingBlobPairedWithSingleDelete) {
Options options = MissingBlobRepairOptions();
uint64_t missing_blob_file = 0;
ASSERT_NO_FATAL_FAILURE(
RecoverMissingNonOldestBlob(options, false, &missing_blob_file));
{
const Snapshot* snapshot = db_->GetSnapshot();
Defer release_snapshot(
[this, snapshot]() { db_->ReleaseSnapshot(snapshot); });
ASSERT_OK(SingleDelete("b"));
ASSERT_OK(Flush());
CompactRangeOptions repair;
repair.skip_missing_blob_files = true;
ASSERT_OK(db_->CompactRange(repair, nullptr, nullptr));
std::string value;
ASSERT_EQ(db_->Get(ReadOptions(), "b", &value).code(), Status::kNotFound);
ReadOptions read;
read.snapshot = snapshot;
ASSERT_EQ(db_->Get(read, "b", &value).code(), Status::kNotFound);
ASSERT_OK(db_->Get(read, "a", &value));
ASSERT_EQ(value, "va");
}
CompactRangeOptions normal;
normal.bottommost_level_compaction = BottommostLevelCompaction::kForce;
ASSERT_OK(db_->CompactRange(normal, nullptr, nullptr));
Reopen(options);
std::string value;
ASSERT_EQ(db_->Get(ReadOptions(), "b", &value).code(), Status::kNotFound);
ASSERT_EQ(db_->Get(ReadOptions(), "d", &value).code(), Status::kNotFound);
ASSERT_EQ(Get("a"), "va");
ASSERT_EQ(Get("c"), "vc");
}
TEST_F(DBBlobBasicTest, RepairMissingBlobWithNoLiveBlobMetadata) {
Options options = MissingBlobRepairOptions();
uint64_t missing_blob_file = 0;
ASSERT_NO_FATAL_FAILURE(
RecoverMissingNonOldestBlob(options, false, &missing_blob_file));
ASSERT_OK(Delete("a"));
ASSERT_OK(Delete("c"));
ASSERT_OK(Flush());
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
ASSERT_TRUE(GetBlobFileNumbers().empty());
ASSERT_NO_FATAL_FAILURE(AssertBlobReference("b", missing_blob_file));
ASSERT_NO_FATAL_FAILURE(AssertBlobReference("d", missing_blob_file));
std::string value;
ASSERT_EQ(db_->Get(ReadOptions(), "b", &value).code(), Status::kCorruption);
CompactRangeOptions repair;
repair.skip_missing_blob_files = true;
ASSERT_OK(db_->CompactRange(repair, nullptr, nullptr));
ASSERT_EQ(db_->Get(ReadOptions(), "b", &value).code(), Status::kNotFound);
ASSERT_EQ(db_->Get(ReadOptions(), "d", &value).code(), Status::kNotFound);
Reopen(options);
ASSERT_EQ(db_->Get(ReadOptions(), "b", &value).code(), Status::kNotFound);
ASSERT_EQ(db_->Get(ReadOptions(), "d", &value).code(), Status::kNotFound);
}
TEST_F(DBBlobBasicTest, RepairRetainsHealthyBlobsReferencedByUntouchedSsts) {
Options options = MissingBlobRepairOptions();
options.sst_partitioner_factory = NewSstPartitionerFixedPrefixFactory(1);
DestroyAndReopen(options);
ASSERT_OK(Put("a_anchor", "anchor"));
ASSERT_OK(Put("z_anchor", "anchor"));
ASSERT_OK(Flush());
ASSERT_OK(Put("a_lost", "lost"));
ASSERT_OK(Flush());
const uint64_t missing_blob_file = GetBlobFileNumbers().back();
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
ASSERT_EQ(TotalTableFiles(), 2);
Close();
ASSERT_OK(env_->DeleteFile(BlobFileName(dbname_, missing_blob_file)));
Options recovery_options = options;
recovery_options.best_efforts_recovery = true;
Reopen(recovery_options);
ASSERT_OK(Put("a_live", "healthy"));
ASSERT_OK(Flush());
const uint64_t healthy_blob_file = GetBlobFileNumbers().back();
ASSERT_OK(Delete("a_anchor"));
ASSERT_OK(Flush());
const Slice a_begin("a");
const Slice a_end("b");
CompactRangeOptions rewrite;
rewrite.bottommost_level_compaction = BottommostLevelCompaction::kForce;
ASSERT_OK(db_->CompactRange(rewrite, &a_begin, &a_end));
ASSERT_EQ(Get("a_live"), "healthy");
ASSERT_OK(Delete("z_anchor"));
ASSERT_OK(Flush());
const Slice z_begin("z");
CompactRangeOptions repair;
repair.skip_missing_blob_files = true;
ASSERT_OK(db_->CompactRange(repair, &z_begin, nullptr));
ASSERT_NO_FATAL_FAILURE(AssertBlobReference("a_lost", missing_blob_file));
ASSERT_NO_FATAL_FAILURE(AssertBlobReference("a_live", healthy_blob_file));
ASSERT_EQ(Get("a_live"), "healthy");
const std::vector<uint64_t> retained_blob_files = GetBlobFileNumbers();
ASSERT_NE(std::find(retained_blob_files.begin(), retained_blob_files.end(),
healthy_blob_file),
retained_blob_files.end());
std::string value;
ASSERT_EQ(db_->Get(ReadOptions(), "a_lost", &value).code(),
Status::kCorruption);
ASSERT_OK(db_->CompactRange(repair, &a_begin, &a_end));
ASSERT_EQ(db_->Get(ReadOptions(), "a_lost", &value).code(),
Status::kNotFound);
ASSERT_EQ(Get("a_live"), "healthy");
Reopen(options);
ASSERT_EQ(db_->Get(ReadOptions(), "a_lost", &value).code(),
Status::kNotFound);
ASSERT_EQ(Get("a_live"), "healthy");
}
TEST_F(DBBlobBasicTest, GetMergeBlobWithPut) {
Options options = GetDefaultOptions();
options.merge_operator = MergeOperators::CreateStringAppendOperator();
@@ -2315,6 +2711,179 @@ class DBBlobWithTimestampTest : public DBBasicTestWithTimestampBase {
: DBBasicTestWithTimestampBase("db_blob_with_timestamp_test") {}
};
TEST_F(DBBlobWithTimestampTest, RepairPreservesTimestampHistoryAfterRecovery) {
Options options = GetDefaultOptions();
options.create_if_missing = true;
options.enable_blob_files = true;
options.min_blob_size = 0;
options.enable_blob_garbage_collection = false;
options.disable_auto_compactions = true;
options.num_levels = 3;
options.level_compaction_dynamic_level_bytes = false;
TestComparator test_cmp(Timestamp(0, 0).size());
options.comparator = &test_cmp;
DestroyAndReopen(options);
Defer close_db([this]() { Close(); });
const std::string old_timestamp = Timestamp(1, 0);
const std::string middle_timestamp = Timestamp(2, 0);
const std::string lost_timestamp = Timestamp(3, 0);
const std::string latest_timestamp = Timestamp(4, 0);
const Slice old_ts(old_timestamp);
const Slice middle_ts(middle_timestamp);
const Slice lost_ts(lost_timestamp);
const Slice latest_ts(latest_timestamp);
ReadOptions healthy_read;
healthy_read.verify_checksums = true;
healthy_read.timestamp = &latest_ts;
std::string healthy_value;
ASSERT_OK(db_->Put(WriteOptions(), "a", old_ts, "healthy"));
ASSERT_OK(db_->Put(WriteOptions(), "b", old_ts, "older"));
const SequenceNumber older_sequence = db_->GetLatestSequenceNumber();
ASSERT_OK(Flush());
const uint64_t healthy_blob_file = GetBlobFileNumbers().back();
ASSERT_OK(db_->Get(healthy_read, "a", &healthy_value));
ASSERT_EQ(healthy_value, "healthy");
ASSERT_OK(db_->Put(WriteOptions(), "b", lost_ts, "lost"));
const SequenceNumber lost_sequence = db_->GetLatestSequenceNumber();
ASSERT_OK(db_->Put(WriteOptions(), "d", lost_ts, "lost"));
const SequenceNumber other_lost_sequence = db_->GetLatestSequenceNumber();
ASSERT_OK(Flush());
const uint64_t missing_blob_file = GetBlobFileNumbers().back();
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
ASSERT_EQ(TotalTableFiles(), 1);
Close();
ASSERT_OK(env_->DeleteFile(BlobFileName(dbname_, missing_blob_file)));
Options recovery_options = options;
recovery_options.best_efforts_recovery = true;
Reopen(recovery_options);
ASSERT_EQ(TotalTableFiles(), 1);
ASSERT_EQ(GetBlobFileNumbers(), std::vector<uint64_t>{healthy_blob_file});
ASSERT_OK(db_->Get(healthy_read, "a", &healthy_value));
ASSERT_EQ(healthy_value, "healthy");
{
ReadOptions read;
read.timestamp = &lost_ts;
PinnableSlice value;
bool is_blob_index = false;
DBImpl::GetImplOptions get_options;
get_options.column_family = db_->DefaultColumnFamily();
get_options.value = &value;
get_options.is_blob_index = &is_blob_index;
ASSERT_OK(dbfull()->GetImpl(read, "b", get_options));
ASSERT_TRUE(is_blob_index);
BlobIndex blob_index;
ASSERT_OK(blob_index.DecodeFrom(value));
ASSERT_EQ(blob_index.file_number(), missing_blob_file);
}
CompactRangeOptions repair;
repair.skip_missing_blob_files = true;
repair.full_history_ts_low = &old_ts;
ASSERT_OK(db_->CompactRange(repair, nullptr, nullptr));
for (int reopen = 0; reopen < 2; ++reopen) {
SCOPED_TRACE(reopen);
if (reopen != 0) {
Reopen(options);
}
std::string full_history_ts_low;
ASSERT_OK(db_->GetFullHistoryTsLow(db_->DefaultColumnFamily(),
&full_history_ts_low));
ASSERT_EQ(full_history_ts_low, old_timestamp);
ReadOptions read;
read.verify_checksums = true;
std::string value;
read.timestamp = &old_ts;
ASSERT_OK(db_->Get(read, "b", &value));
ASSERT_EQ(value, "older");
read.timestamp = &middle_ts;
ASSERT_OK(db_->Get(read, "b", &value));
ASSERT_EQ(value, "older");
read.timestamp = &lost_ts;
ASSERT_EQ(db_->Get(read, "b", &value).code(), Status::kNotFound);
ASSERT_EQ(db_->Get(read, "d", &value).code(), Status::kNotFound);
read.timestamp = &latest_ts;
ASSERT_EQ(db_->Get(read, "b", &value).code(), Status::kNotFound);
ASSERT_OK(db_->Get(read, "a", &value));
ASSERT_EQ(value, "healthy");
read.iter_start_ts = &old_ts;
std::unique_ptr<Iterator> history(db_->NewIterator(read));
history->Seek("b");
ASSERT_NO_FATAL_FAILURE(CheckIterEntry(history.get(), "b", lost_sequence,
kTypeDeletionWithTimestamp, Slice(),
lost_ts));
history->Next();
ASSERT_NO_FATAL_FAILURE(CheckIterEntry(history.get(), "b", older_sequence,
kTypeBlobIndex, "older", old_ts));
ASSERT_EQ(history->value(), "older");
history->Next();
ASSERT_NO_FATAL_FAILURE(
CheckIterEntry(history.get(), "d", other_lost_sequence,
kTypeDeletionWithTimestamp, Slice(), lost_ts));
history->Next();
ASSERT_FALSE(history->Valid());
ASSERT_OK(history->status());
}
CompactRangeOptions normal;
normal.bottommost_level_compaction = BottommostLevelCompaction::kForce;
normal.full_history_ts_low = &latest_ts;
ASSERT_OK(db_->Get(healthy_read, "a", &healthy_value));
ASSERT_EQ(healthy_value, "healthy");
ASSERT_OK(db_->CompactRange(normal, nullptr, nullptr));
ASSERT_OK(db_->Get(healthy_read, "a", &healthy_value));
ASSERT_EQ(healthy_value, "healthy");
Reopen(options);
ReadOptions read;
read.verify_checksums = true;
read.timestamp = &latest_ts;
std::string value;
ASSERT_EQ(db_->Get(read, "b", &value).code(), Status::kNotFound);
ASSERT_EQ(db_->Get(read, "d", &value).code(), Status::kNotFound);
ASSERT_OK(db_->Get(read, "a", &value));
ASSERT_EQ(value, "healthy");
read.timestamp = &old_ts;
ASSERT_EQ(db_->Get(read, "b", &value).code(), Status::kInvalidArgument);
}
TEST_F(DBBlobWithTimestampTest, HistoryCompactionPreservesHealthyBlobKey) {
Options options = GetDefaultOptions();
options.create_if_missing = true;
options.enable_blob_files = true;
options.min_blob_size = 0;
options.enable_blob_garbage_collection = false;
options.disable_auto_compactions = true;
options.num_levels = 3;
options.level_compaction_dynamic_level_bytes = false;
TestComparator test_cmp(Timestamp(0, 0).size());
options.comparator = &test_cmp;
DestroyAndReopen(options);
Defer close_db([this]() { Close(); });
const std::string old_timestamp = Timestamp(1, 0);
const std::string history_floor = Timestamp(4, 0);
const Slice old_ts(old_timestamp);
const Slice history_floor_ts(history_floor);
ASSERT_OK(db_->Put(WriteOptions(), "a", old_ts, "healthy"));
ASSERT_OK(Flush());
ASSERT_EQ(GetBlobFileNumbers().size(), 1);
ReadOptions read;
read.verify_checksums = true;
read.timestamp = &history_floor_ts;
std::string value;
ASSERT_OK(db_->Get(read, "a", &value));
ASSERT_EQ(value, "healthy");
CompactRangeOptions normal;
normal.bottommost_level_compaction = BottommostLevelCompaction::kForce;
normal.full_history_ts_low = &history_floor_ts;
ASSERT_OK(db_->CompactRange(normal, nullptr, nullptr));
ASSERT_OK(db_->Get(read, "a", &value));
ASSERT_EQ(value, "healthy");
Reopen(options);
ASSERT_OK(db_->Get(read, "a", &value));
ASSERT_EQ(value, "healthy");
}
TEST_F(DBBlobWithTimestampTest, GetBlob) {
Options options = GetDefaultOptions();
options.create_if_missing = true;
+399
View File
@@ -18,6 +18,7 @@
#include <set>
#include <sstream>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
@@ -37,6 +38,7 @@
#include "port/stack_trace.h"
#include "rocksdb/convenience.h"
#include "rocksdb/file_system.h"
#include "rocksdb/sst_file_manager.h"
#include "rocksdb/trace_reader_writer.h"
#include "rocksdb/trace_record.h"
#include "rocksdb/utilities/replayer.h"
@@ -2004,6 +2006,403 @@ TEST_F(DBBlobDirectWriteTest, RetryAfterOlderOverlappingFlushFails) {
ASSERT_EQ(value, Get("key2"));
}
class BlobFileCreationStatusListener : public EventListener {
public:
void OnBlobFileCreated(const BlobFileCreationInfo& info) override {
std::lock_guard<std::mutex> lock(mutex_);
statuses_.push_back(info.status.ToString());
}
void OnErrorRecoveryBegin(BackgroundErrorReason /*reason*/, Status bg_error,
bool* /*auto_recovery*/) override {
bg_error.PermitUncheckedError();
}
std::vector<std::string> GetStatuses() {
std::lock_guard<std::mutex> lock(mutex_);
return statuses_;
}
private:
std::mutex mutex_;
std::vector<std::string> statuses_;
};
TEST_F(DBBlobDirectWriteTest, SpaceLimitWhileSealingKeepsBlobFile) {
Options options = GetDirectWriteOptions();
std::shared_ptr<SstFileManager> sst_file_manager(NewSstFileManager(env_));
options.sst_file_manager = sst_file_manager;
auto listener = std::make_shared<BlobFileCreationStatusListener>();
options.listeners.emplace_back(listener);
DestroyAndReopen(options);
const std::string value(1024, 'v');
ASSERT_OK(Put("key", value));
// The flush seals the blob file holding "key" and reaches the space limit.
// It still commits the file, and then writes stop. As for an SST file,
// Flush() returns the background error set after the commit.
sst_file_manager->SetMaxAllowedSpaceUsage(1);
ASSERT_EQ(Status::kSpaceLimit, Flush().subcode());
ASSERT_EQ(Status::kSpaceLimit, dbfull()->TEST_GetBGError().subcode());
ASSERT_NOK(Put("key2", "x"));
ColumnFamilyMetaData cf_meta;
db_->GetColumnFamilyMetaData(&cf_meta);
ASSERT_EQ(1U, cf_meta.blob_files.size());
ASSERT_EQ(value, Get("key"));
ASSERT_EQ(std::vector<std::string>{"OK"}, listener->GetStatuses());
}
TEST_F(DBBlobDirectWriteTest,
SpaceLimitWhileSealingDoesNotBlockMemtableSwitch) {
Options options = GetDirectWriteOptions();
options.max_write_buffer_number = 4;
std::shared_ptr<SstFileManager> sst_file_manager(NewSstFileManager(env_));
options.sst_file_manager = sst_file_manager;
DestroyAndReopen(options);
const std::string value(1024, 'v');
ASSERT_OK(Put("key1", value));
// While the flush reports the space limit for the blob file holding "key1",
// a concurrent memtable switch holds the DB mutex and rotates the blob
// generation.
port::Thread switch_thread;
std::atomic<bool> switch_started{false};
std::promise<void> switch_holds_db_mutex;
SyncPoint::GetInstance()->LoadDependency(
{{"DBBlobDirectWriteTest::SpaceLimitWhileSealing:SecondPut",
"DBImpl::BackgroundCallFlush:start"}});
SyncPoint::GetInstance()->SetCallBack(
"DBImpl::SwitchMemtable:BeforeRotateBlobGeneration", [&](void*) {
if (switch_started.exchange(false)) {
switch_holds_db_mutex.set_value();
}
});
SyncPoint::GetInstance()->SetCallBack(
"BlobFileCompletionCallback::CallBack::MaxAllowedSpaceReached",
[&](void*) {
if (switch_thread.joinable()) {
return;
}
switch_started = true;
switch_thread = port::Thread([&]() {
FlushOptions no_wait;
no_wait.wait = false;
no_wait.allow_write_stall = true;
db_->Flush(no_wait).PermitUncheckedError();
});
switch_holds_db_mutex.get_future().wait();
});
SyncPoint::GetInstance()->EnableProcessing();
sst_file_manager->SetMaxAllowedSpaceUsage(1);
FlushOptions no_wait;
no_wait.wait = false;
ASSERT_OK(db_->Flush(no_wait));
ASSERT_OK(Put("key2", value));
TEST_SYNC_POINT("DBBlobDirectWriteTest::SpaceLimitWhileSealing:SecondPut");
dbfull()->TEST_WaitForBackgroundWork().PermitUncheckedError();
ASSERT_TRUE(switch_thread.joinable());
switch_thread.join();
dbfull()->TEST_WaitForBackgroundWork().PermitUncheckedError();
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
ASSERT_EQ(value, Get("key1"));
ASSERT_EQ(value, Get("key2"));
}
class IgnoreFlushNoWALErrorListener : public EventListener {
public:
void OnBackgroundError(BackgroundErrorReason reason,
Status* bg_error) override {
std::lock_guard<std::mutex> lock(mutex_);
reasons_.push_back(reason);
if (reason == BackgroundErrorReason::kFlushNoWAL) {
*bg_error = Status::OK();
}
}
void OnErrorRecoveryBegin(BackgroundErrorReason /*reason*/, Status bg_error,
bool* /*auto_recovery*/) override {
bg_error.PermitUncheckedError();
}
std::vector<BackgroundErrorReason> GetReasons() {
std::lock_guard<std::mutex> lock(mutex_);
return reasons_;
}
private:
std::mutex mutex_;
std::vector<BackgroundErrorReason> reasons_;
};
TEST_F(DBBlobDirectWriteTest, SpaceLimitWhileSealingStopsWrites) {
Options options = GetDirectWriteOptions();
std::shared_ptr<SstFileManager> sst_file_manager(NewSstFileManager(env_));
options.sst_file_manager = sst_file_manager;
auto listener = std::make_shared<IgnoreFlushNoWALErrorListener>();
options.listeners.emplace_back(listener);
DestroyAndReopen(options);
const std::string value(1024, 'v');
ASSERT_OK(Put("key1", value));
// The space limit is reported for the blob file the flush seals, with the
// reason kFlush, so a listener that ignores kFlushNoWAL errors still stops
// writes.
sst_file_manager->SetMaxAllowedSpaceUsage(1);
FlushOptions no_wait;
no_wait.wait = false;
ASSERT_OK(db_->Flush(no_wait));
dbfull()->TEST_WaitForBackgroundWork().PermitUncheckedError();
const std::vector<BackgroundErrorReason> reasons = listener->GetReasons();
ASSERT_GT(
std::count(reasons.begin(), reasons.end(), BackgroundErrorReason::kFlush),
0);
ASSERT_NOK(Put("key2", value));
}
TEST_F(DBBlobDirectWriteTest, SpaceLimitWhileWriteSealsBlobFile) {
Options options = GetDirectWriteOptions();
options.blob_file_size = 2048;
std::shared_ptr<SstFileManager> sst_file_manager(NewSstFileManager(env_));
options.sst_file_manager = sst_file_manager;
auto listener = std::make_shared<BlobFileCreationStatusListener>();
options.listeners.emplace_back(listener);
DestroyAndReopen(options);
const std::string value(1024, 'v');
ASSERT_OK(Put("key1", value));
// "key2" does not fit in the blob file holding "key1", so the write seals
// that file and reaches the space limit. The write still succeeds, and then
// writes stop.
sst_file_manager->SetMaxAllowedSpaceUsage(1);
ASSERT_OK(Put("key2", value));
ASSERT_EQ(Status::kSpaceLimit, dbfull()->TEST_GetBGError().subcode());
ASSERT_NOK(Put("key3", "x"));
dbfull()->TEST_DeleteObsoleteFiles();
ASSERT_EQ(2U, CountBlobFiles());
ASSERT_EQ(value, Get("key1"));
ASSERT_EQ(value, Get("key2"));
ASSERT_EQ(std::vector<std::string>{"OK"}, listener->GetStatuses());
}
class FailNthBlobFileOpenFileSystem : public FileSystemWrapper {
public:
FailNthBlobFileOpenFileSystem(const std::shared_ptr<FileSystem>& fs,
int failing_open)
: FileSystemWrapper(fs), failing_open_(failing_open) {}
static const char* kClassName() { return "FailNthBlobFileOpenFileSystem"; }
const char* Name() const override { return kClassName(); }
IOStatus NewWritableFile(const std::string& fname,
const FileOptions& file_opts,
std::unique_ptr<FSWritableFile>* result,
IODebugContext* dbg) override {
if (IsBlobFilePath(fname) && ++blob_file_opens_ == failing_open_) {
return IOStatus::IOError("Injected blob file open error");
}
return target()->NewWritableFile(fname, file_opts, result, dbg);
}
private:
const int failing_open_;
std::atomic<int> blob_file_opens_{0};
};
TEST_F(DBBlobDirectWriteTest, FailedBatchAfterSealCountsGarbageInSealedFile) {
auto fs = std::make_shared<FailNthBlobFileOpenFileSystem>(
env_->GetFileSystem(), /*failing_open=*/3);
std::unique_ptr<Env> env(new CompositeEnvWrapper(env_, fs));
Options options = GetDirectWriteOptions();
options.env = env.get();
options.blob_file_size = 2048;
Defer close_db([this]() { Close(); });
DestroyAndReopen(options);
const std::string value(1024, 'v');
// An SST refers to the first blob file, so a later blob file that no SST
// refers to is dropped only once all its blobs are garbage.
ASSERT_OK(Put("x", value));
ASSERT_OK(Flush());
// "b" does not fit in the blob file holding "a" from the same batch, so the
// batch seals that file and then fails to open the next one. "a" is garbage
// in the sealed file, so the flush drops that file.
WriteBatch batch;
ASSERT_OK(batch.Put("a", value));
ASSERT_OK(batch.Put("b", value));
ASSERT_TRUE(db_->Write(WriteOptions(), &batch).IsIOError());
ASSERT_OK(Put("c", value));
ASSERT_OK(Flush());
ColumnFamilyMetaData cf_meta;
db_->GetColumnFamilyMetaData(&cf_meta);
ASSERT_EQ(2U, cf_meta.blob_files.size());
ASSERT_EQ("NOT_FOUND", Get("a"));
ASSERT_EQ(value, Get("x"));
ASSERT_EQ(value, Get("c"));
}
class FailBlobFileSizeFileSystem : public FileSystemWrapper {
public:
explicit FailBlobFileSizeFileSystem(const std::shared_ptr<FileSystem>& fs)
: FileSystemWrapper(fs) {}
static const char* kClassName() { return "FailBlobFileSizeFileSystem"; }
const char* Name() const override { return kClassName(); }
IOStatus GetFileSize(const std::string& fname, const IOOptions& options,
uint64_t* file_size, IODebugContext* dbg) override {
if (IsBlobFilePath(fname)) {
return IOStatus::IOError("Injected blob file size error");
}
return target()->GetFileSize(fname, options, file_size, dbg);
}
};
TEST_F(DBBlobDirectWriteTest, SealedBlobFilesTrackedWhenSizeLookupFails) {
Options options = GetDirectWriteOptions();
options.blob_file_size = 2048;
// Only the SstFileManager fails to look up blob file sizes.
std::shared_ptr<SstFileManager> sst_file_manager(NewSstFileManager(
env_,
std::make_shared<FailBlobFileSizeFileSystem>(env_->GetFileSystem())));
options.sst_file_manager = sst_file_manager;
DestroyAndReopen(options);
const std::string value(1024, 'v');
// "key2" does not fit in the blob file holding "key1", so the write seals
// that file, and the flush seals the one holding "key2".
ASSERT_OK(Put("key1", value));
ASSERT_OK(Put("key2", value));
ASSERT_OK(Flush());
ASSERT_EQ(2U, CountBlobFiles());
std::unordered_map<std::string, uint64_t> blob_file_sizes;
std::vector<std::string> files;
ASSERT_OK(env_->GetChildren(dbname_, &files));
for (const auto& file : files) {
const std::string path = dbname_ + "/" + file;
if (IsBlobFilePath(path)) {
ASSERT_OK(env_->GetFileSize(path, &blob_file_sizes[path]));
}
}
std::unordered_map<std::string, uint64_t> tracked_blob_file_sizes;
for (const auto& [path, size] : sst_file_manager->GetTrackedFiles()) {
if (IsBlobFilePath(path)) {
tracked_blob_file_sizes[path] = size;
}
}
ASSERT_EQ(blob_file_sizes, tracked_blob_file_sizes);
Reopen(options);
ASSERT_EQ(value, Get("key1"));
ASSERT_EQ(value, Get("key2"));
}
class DisableAutoRecoveryListener : public EventListener {
public:
void OnErrorRecoveryBegin(BackgroundErrorReason /*reason*/, Status bg_error,
bool* auto_recovery) override {
bg_error.PermitUncheckedError();
*auto_recovery = false;
}
};
TEST_F(DBBlobDirectWriteTest, FailedBlobSealLeavesMemtableFlushable) {
std::unique_ptr<FaultInjectionTestEnv> fault_env(
new FaultInjectionTestEnv(env_));
Options options = GetDirectWriteOptions();
options.env = fault_env.get();
options.listeners.emplace_back(
std::make_shared<DisableAutoRecoveryListener>());
for (bool atomic_flush : {false, true}) {
SCOPED_TRACE("atomic_flush=" + std::to_string(atomic_flush));
options.atomic_flush = atomic_flush;
DestroyAndReopen(options);
Defer close_db([this]() { Close(); });
const std::string value(1024, 'v');
ASSERT_OK(Put("key", value));
// The flush fails to sync the footer of the blob file holding "key".
SyncPoint::GetInstance()->SetCallBack("BlobLogWriter::Sync", [&](void*) {
fault_env->SetFilesystemActive(false, Status::NoSpace("injected"));
});
SyncPoint::GetInstance()->EnableProcessing();
ASSERT_NOK(Flush());
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
fault_env->SetFilesystemActive(true);
uint64_t flush_pending = 0;
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kMemTableFlushPending,
&flush_pending));
ASSERT_EQ(1U, flush_pending);
}
}
TEST_F(DBBlobDirectWriteTest, FailedBlobFooterSyncFailsRetriedFlush) {
std::unique_ptr<FaultInjectionTestEnv> fault_env(
new FaultInjectionTestEnv(env_));
Options options = GetDirectWriteOptions();
options.env = fault_env.get();
options.listeners.emplace_back(
std::make_shared<DisableAutoRecoveryListener>());
DestroyAndReopen(options);
Defer close_db([this]() { Close(); });
const std::string value(1024, 'v');
ASSERT_OK(Put("key", value));
// The flush fails to sync the footer of the blob file holding "key".
SyncPoint::GetInstance()->SetCallBack("BlobLogWriter::Sync", [&](void*) {
fault_env->SetFilesystemActive(false, Status::NoSpace("injected"));
});
SyncPoint::GetInstance()->EnableProcessing();
ASSERT_NOK(Flush());
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
fault_env->SetFilesystemActive(true);
// That blob file can no longer be sealed, so retrying the flush must fail
// rather than install a table file that references it.
ASSERT_NOK(dbfull()->Resume());
ASSERT_EQ(0, NumTableFilesAtLevel(0));
ASSERT_EQ(value, Get("key"));
}
TEST_F(DBBlobDirectWriteTest, CloseAfterFailedBlobSeal) {
std::unique_ptr<FaultInjectionTestEnv> fault_env(
new FaultInjectionTestEnv(env_));
Options options = GetDirectWriteOptions();
options.env = fault_env.get();
options.blob_file_size = 2048;
options.listeners.emplace_back(
std::make_shared<DisableAutoRecoveryListener>());
for (bool seal_on_write : {false, true}) {
SCOPED_TRACE("seal_on_write=" + std::to_string(seal_on_write));
DestroyAndReopen(options);
Defer close_db([this]() { Close(); });
const std::string value(1024, 'v');
ASSERT_OK(Put("key1", value));
// Sealing the blob file holding "key1" fails, either in the write that
// does not fit in it or in the flush. The DB closes without retrying.
SyncPoint::GetInstance()->SetCallBack("BlobLogWriter::Sync", [&](void*) {
fault_env->SetFilesystemActive(false, Status::NoSpace("injected"));
});
SyncPoint::GetInstance()->EnableProcessing();
ASSERT_NOK(seal_on_write ? Put("key2", value) : Flush());
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
fault_env->SetFilesystemActive(true);
}
}
class DBBlobDirectWriteWithTimestampTest : public DBBasicTestWithTimestampBase {
protected:
DBBlobDirectWriteWithTimestampTest()
+5
View File
@@ -57,6 +57,11 @@ TEST(BlobIndexTest, SameFileBlobIndex) {
EXPECT_NE(blob_index.DebugString(false).find("file:same"), std::string::npos);
}
TEST(BlobIndexTest, EmptyBlobIndexIsCorruption) {
BlobIndex blob_index;
ASSERT_EQ(blob_index.DecodeFrom(Slice()).code(), Status::Code::kCorruption);
}
// kTypeBlobIndex is a value type used by BlobDB only. The base rocksdb
// should accept the value type on write, and report not supported value
// for reads, unless caller request for it explicitly. The base rocksdb
+284
View File
@@ -0,0 +1,284 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#pragma once
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <string>
#include <utility>
#include <vector>
#include "db/blob/blob_index.h"
#include "db/version_set.h"
#include "db/wide/wide_column_serialization.h"
#include "table/internal_iterator.h"
namespace ROCKSDB_NAMESPACE {
// Replaces references absent from an immutable input Version with deletions
// or entities containing the surviving columns. Input values must already be
// prepared, as they are for compaction input. Neither input is owned.
class MissingBlobRepairIterator : public InternalIterator {
public:
MissingBlobRepairIterator(InternalIterator* input,
const VersionStorageInfo* input_storage,
size_t timestamp_size)
: input_(input),
input_storage_(input_storage),
timestamp_size_(timestamp_size) {
assert(input_);
assert(input_storage_);
Update();
}
bool Valid() const override { return input_->Valid() && status_.ok(); }
void SeekToFirst() override {
input_->SeekToFirst();
Update();
}
void SeekToLast() override {
input_->SeekToLast();
Update();
}
void Seek(const Slice& target) override {
// A rewritten type can sort after its original type at the same sequence.
const std::string target_copy = target.ToString();
ParsedInternalKey parsed;
status_ = ParseInternalKey(target_copy, &parsed, false);
if (!status_.ok()) {
return;
}
InternalKey seek_key(parsed.user_key, parsed.sequence, kValueTypeForSeek);
input_->Seek(seek_key.Encode());
Update();
while (Valid() && input_storage_->InternalComparator()->Compare(
key(), target_copy) < 0) {
Next();
}
}
void SeekForPrev(const Slice& target) override {
const std::string target_copy = target.ToString();
input_->SeekForPrev(target_copy);
Update();
while (Valid() && input_storage_->InternalComparator()->Compare(
key(), target_copy) > 0) {
Prev();
}
}
void Next() override {
assert(Valid());
input_->Next();
Update();
}
void Prev() override {
assert(Valid());
input_->Prev();
Update();
}
Slice key() const override {
assert(Valid());
return repaired_key_.empty() ? input_->key() : Slice(repaired_key_);
}
Slice value() const override {
assert(Valid());
return value_rewritten_ ? Slice(repaired_value_) : input_->value();
}
uint64_t write_unix_time() const override {
return input_->write_unix_time();
}
Status status() const override { return status_; }
bool PrepareValue() override {
assert(Valid());
return true;
}
void SetPinnedItersMgr(PinnedIteratorsManager* manager) override {
input_->SetPinnedItersMgr(manager);
}
bool IsKeyPinned() const override {
return repaired_key_.empty() && input_->IsKeyPinned();
}
bool IsValuePinned() const override {
return !value_rewritten_ && input_->IsValuePinned();
}
bool IsSafeToDropWhilePinned() const override {
return !value_rewritten_ && input_->IsSafeToDropWhilePinned();
}
bool IsDeleteRangeSentinelKey() const override {
return input_->IsDeleteRangeSentinelKey();
}
bool MayBeOutOfLowerBound() override {
return input_->MayBeOutOfLowerBound();
}
IterBoundCheck UpperBoundCheckResult() override {
return input_->UpperBoundCheckResult();
}
void SetRangeDelReadSeqno(SequenceNumber sequence) override {
input_->SetRangeDelReadSeqno(sequence);
}
void GetReadaheadState(ReadaheadFileInfo* state) override {
input_->GetReadaheadState(state);
}
void SetReadaheadState(ReadaheadFileInfo* state) override {
input_->SetReadaheadState(state);
}
Status GetProperty(std::string name, std::string* value) override {
return input_->GetProperty(std::move(name), value);
}
bool IsRepairDeletion() const { return repair_deletion_; }
private:
// Only external, non-TTL indexes use the input Version's blob file set.
// Absence is checked in metadata; inline, same-file and legacy TTL indexes
// retain their existing resolution rules.
bool IsMissing(const BlobIndex& index) const {
if (index.IsInlined() || index.HasTTL() || index.IsSameFile()) {
return false;
}
const VersionStorageInfo::BlobFiles::const_iterator it =
input_storage_->GetBlobFileMetaDataLB(index.file_number());
return it == input_storage_->GetBlobFiles().end() ||
(*it)->GetBlobFileNumber() != index.file_number();
}
// Preserve the sequence and user-key timestamp so the lost value continues
// to hide older versions, including versions retained for snapshots.
void SetDeletion(const ParsedInternalKey& parsed) {
repaired_key_ = input_->key().ToString();
const ValueType type =
timestamp_size_ == 0 ? kTypeDeletion : kTypeDeletionWithTimestamp;
UpdateInternalKey(&repaired_key_, parsed.sequence, type);
value_rewritten_ = true;
repair_deletion_ = true;
}
// Inspect column indexes without resolving blob payloads. Removing columns
// changes the positions of surviving blob indexes, so rebuild those positions
// before serialization. Only loss of every column becomes a deletion.
Status RepairEntity(const ParsedInternalKey& parsed) {
columns_.clear();
blob_columns_.clear();
Status s = WideColumnSerialization::Deserialize(input_->value(), columns_,
&blob_columns_);
if (!s.ok()) {
return s;
}
bool has_missing = false;
for (const std::pair<size_t, BlobIndex>& column : blob_columns_) {
has_missing |= IsMissing(column.second);
}
if (!has_missing) {
return Status::OK();
}
surviving_columns_.clear();
surviving_blobs_.clear();
size_t blob_position = 0;
for (size_t i = 0; i < columns_.size(); ++i) {
const BlobIndex* index = nullptr;
if (blob_position < blob_columns_.size() &&
blob_columns_[blob_position].first == i) {
index = &blob_columns_[blob_position++].second;
}
if (index && IsMissing(*index)) {
continue;
}
if (index) {
surviving_blobs_.emplace_back(surviving_columns_.size(), *index);
}
surviving_columns_.push_back(columns_[i]);
}
if (surviving_columns_.empty()) {
SetDeletion(parsed);
return Status::OK();
}
s = surviving_blobs_.empty()
? WideColumnSerialization::Serialize(surviving_columns_,
repaired_value_)
: WideColumnSerialization::SerializeV2(
surviving_columns_, surviving_blobs_, repaired_value_);
if (s.ok()) {
value_rewritten_ = true;
}
return s;
}
// Reset rewritten buffers on every movement. Preserve input errors and range
// deletion sentinels before decoding point values; malformed indexes fail.
void Update() {
repaired_key_.clear();
repaired_value_.clear();
value_rewritten_ = false;
repair_deletion_ = false;
status_ = input_->status();
if (!status_.ok() || !input_->Valid() ||
input_->IsDeleteRangeSentinelKey()) {
return;
}
ParsedInternalKey parsed;
status_ = ParseInternalKey(input_->key(), &parsed, false);
if (!status_.ok() || (parsed.type != kTypeBlobIndex &&
parsed.type != kTypeWideColumnEntity)) {
return;
}
BlobIndex index;
if (parsed.user_key.size() < timestamp_size_) {
status_ =
Status::Corruption("Blob reference key has an invalid timestamp");
} else if (parsed.type == kTypeWideColumnEntity) {
status_ = RepairEntity(parsed);
} else {
status_ = index.DecodeFrom(input_->value());
}
if (!status_.ok()) {
return;
}
if (parsed.type == kTypeBlobIndex && IsMissing(index)) {
SetDeletion(parsed);
}
}
InternalIterator* input_;
const VersionStorageInfo* input_storage_;
size_t timestamp_size_;
std::string repaired_key_;
std::string repaired_value_;
WideColumns columns_;
std::vector<std::pair<size_t, BlobIndex>> blob_columns_;
WideColumns surviving_columns_;
std::vector<std::pair<size_t, BlobIndex>> surviving_blobs_;
Status status_;
bool value_rewritten_ = false;
bool repair_deletion_ = false;
};
} // namespace ROCKSDB_NAMESPACE
+14 -4
View File
@@ -116,7 +116,8 @@ Status BuildTable(
// compaction path so the manifest learns about the new garbage.
blob_garbage_meter.reset(new BlobGarbageMeter());
blob_counting_iter.reset(
new BlobCountingIterator(iter, blob_garbage_meter.get()));
new BlobCountingIterator(iter, blob_garbage_meter.get(),
/*track_current_key_inflow=*/false));
input = blob_counting_iter.get();
}
@@ -161,6 +162,13 @@ Status BuildTable(
context.reason = tboptions.reason;
compaction_filter =
ioptions.compaction_filter_factory->CreateCompactionFilter(context);
CompactionFilter::MergeOrder order;
s = GetCompactionFilterMergeOrder(
compaction_filter.get(), ioptions.compaction_filter_factory.get(),
tboptions.reason, &order);
if (!s.ok()) {
return s;
}
if (compaction_filter != nullptr &&
!compaction_filter->IgnoreSnapshots()) {
s.PermitUncheckedError();
@@ -216,7 +224,8 @@ Status BuildTable(
auto ucmp = tboptions.internal_comparator.user_comparator();
MergeHelper merge(
env, ucmp, ioptions.merge_operator.get(), compaction_filter.get(),
ioptions.logger, true /* internal key corruption is not ok */,
CompactionFilter::MergeOrder::kFilterBeforeMerge, ioptions.logger,
true /* internal key corruption is not ok */,
snapshots.empty() ? 0 : snapshots.back(), snapshot_checker);
std::unique_ptr<BlobFileBuilder> blob_file_builder(
@@ -229,7 +238,8 @@ Status BuildTable(
&(tboptions.write_options), tboptions.db_id,
tboptions.db_session_id, job_id, tboptions.column_family_id,
tboptions.column_family_name, write_hint, io_tracer,
blob_callback, blob_creation_reason, &blob_file_paths,
blob_callback, blob_creation_reason,
ioptions.cf_paths.front().path, &blob_file_paths,
blob_file_additions)
: nullptr);
@@ -390,7 +400,7 @@ Status BuildTable(
std::min(meta->fd.smallest_seqno, smallest_preferred_seqno),
meta->fd.largest_seqno);
relevant_mapping.SetCapacity(kMaxSeqnoTimePairsPerSST);
relevant_mapping.Enforce(tboptions.file_creation_time);
relevant_mapping.Enforce(meta->file_creation_time);
}
builder->SetSeqnoTimeTableProperties(
relevant_mapping,
+222 -20
View File
@@ -58,6 +58,7 @@
#include "rocksdb/wal_filter.h"
#include "rocksdb/write_batch.h"
#include "rocksdb/write_buffer_manager.h"
#include "util/cast_util.h"
#include "util/stderr_logger.h"
#include "utilities/merge_operators.h"
@@ -474,6 +475,23 @@ struct rocksdb_compactoptions_t {
struct rocksdb_block_based_table_options_t {
BlockBasedTableOptions rep;
std::shared_ptr<UserDefinedIndexFactory> user_defined_index_factory;
bool use_udi_as_primary_index = false;
bool fail_if_no_udi_on_open = false;
// Set once rocksdb_block_based_options_set_index_mode() has been called.
// Mirrors the C++ precedence rule in GetEffectiveIndexMode(): an explicit
// index_mode wins over the deprecated bools, so a stale
// use_udi_as_primary_index cannot silently escalate a rollback back into a
// custom-index mode.
bool index_mode_explicit = false;
void SyncIndexModeFromLegacyUdiOptions() {
if (index_mode_explicit) {
return;
}
rep.index_mode = BlockBasedTableOptions::IndexMode::kStandardDefault;
rep.use_udi_as_primary_index = use_udi_as_primary_index;
rep.fail_if_no_udi_on_open = fail_if_no_udi_on_open;
}
};
struct rocksdb_block_cache_trace_options_t {
BlockCacheTraceOptions rep;
@@ -4767,12 +4785,60 @@ const char* rocksdb_block_based_options_get_user_defined_index_factory_name(
return name;
}
void rocksdb_block_based_options_set_use_udi_as_primary_index(
rocksdb_block_based_table_options_t* opt, unsigned char v) {
opt->use_udi_as_primary_index = v != 0;
opt->SyncIndexModeFromLegacyUdiOptions();
}
unsigned char rocksdb_block_based_options_get_use_udi_as_primary_index(
rocksdb_block_based_table_options_t* opt) {
return opt->use_udi_as_primary_index;
}
void rocksdb_block_based_options_set_fail_if_no_udi_on_open(
rocksdb_block_based_table_options_t* opt, unsigned char v) {
opt->fail_if_no_udi_on_open = v != 0;
opt->SyncIndexModeFromLegacyUdiOptions();
}
unsigned char rocksdb_block_based_options_get_fail_if_no_udi_on_open(
rocksdb_block_based_table_options_t* opt) {
return opt->fail_if_no_udi_on_open;
}
void rocksdb_block_based_options_set_index_mode(
rocksdb_block_based_table_options_t* opt, int v) {
opt->rep.index_mode = static_cast<BlockBasedTableOptions::IndexMode>(v);
opt->index_mode_explicit = true;
opt->rep.use_udi_as_primary_index = false;
opt->rep.fail_if_no_udi_on_open = false;
}
int rocksdb_block_based_options_get_index_mode(
rocksdb_block_based_table_options_t* opt) {
return ROCKSDB_NAMESPACE::lossless_cast<int>(
opt->rep.GetEffectiveIndexMode());
}
void rocksdb_options_set_block_based_table_factory(
rocksdb_options_t* opt,
rocksdb_block_based_table_options_t* table_options) {
if (table_options) {
opt->rep.table_factory.reset(
ROCKSDB_NAMESPACE::NewBlockBasedTableFactory(table_options->rep));
if (table_options->index_mode_explicit &&
table_options->rep.index_mode ==
BlockBasedTableOptions::IndexMode::kStandardDefault) {
// The default struct value cannot carry an explicit selection. Preserve
// that selection when transferring the C wrapper into a C++ factory.
ConfigOptions config;
config.invoke_prepare_options = false;
Status s = opt->rep.table_factory->ConfigureOption(config, "index_mode",
"kStandardDefault");
assert(s.ok());
s.PermitUncheckedError();
}
}
}
@@ -4886,6 +4952,12 @@ int rocksdb_compactionjobinfo_job_id(const rocksdb_compactionjobinfo_t* info) {
return info->rep.job_id;
}
unsigned char
rocksdb_compactionjobinfo_separate_remote_compaction_pool_selected(
const rocksdb_compactionjobinfo_t* info) {
return info->rep.separate_remote_compaction_pool_selected;
}
int rocksdb_compactionjobinfo_num_l0_files(
const rocksdb_compactionjobinfo_t* info) {
return info->rep.num_l0_files;
@@ -4949,6 +5021,12 @@ int rocksdb_subcompactionjobinfo_job_id(
return info->rep.job_id;
}
unsigned char
rocksdb_subcompactionjobinfo_separate_remote_compaction_pool_selected(
const rocksdb_subcompactionjobinfo_t* info) {
return info->rep.separate_remote_compaction_pool_selected;
}
int rocksdb_subcompactionjobinfo_subcompaction_job_id(
const rocksdb_subcompactionjobinfo_t* info) {
return info->rep.subcompaction_job_id;
@@ -5187,6 +5265,11 @@ uint64_t rocksdb_table_properties_udi_is_primary_index(
return props->rep.udi_is_primary_index;
}
uint64_t rocksdb_table_properties_standard_index_is_stub(
const rocksdb_table_properties_t* props) {
return props->rep.standard_index_is_stub;
}
uint64_t rocksdb_table_properties_filter_size(
const rocksdb_table_properties_t* props) {
return props->rep.filter_size;
@@ -5652,6 +5735,16 @@ uint64_t rocksdb_blob_file_garbage_info_garbage_blob_bytes(
}
// END generated: c_generated_metadata_structs_auto.cc.inc
unsigned char rocksdb_compactionjobinfo_use_separate_remote_compaction_pool(
const rocksdb_compactionjobinfo_t* info) {
return info->rep.separate_remote_compaction_pool_selected;
}
unsigned char rocksdb_subcompactionjobinfo_use_separate_remote_compaction_pool(
const rocksdb_subcompactionjobinfo_t* info) {
return info->rep.separate_remote_compaction_pool_selected;
}
rocksdb_compactionjobinfo_t* rocksdb_compactionjobinfo_create() {
return new rocksdb_compactionjobinfo_t;
}
@@ -7513,6 +7606,7 @@ void rocksdb_readoptions_set_table_index_factory_from_string(
char** errptr) {
opt->table_index_factory.reset();
opt->rep.table_index_factory = nullptr;
opt->rep.read_index = ReadOptions::ReadIndex::kDefault;
if (value == nullptr) {
return;
}
@@ -7524,11 +7618,21 @@ void rocksdb_readoptions_set_table_index_factory_from_string(
}
opt->table_index_factory = std::move(factory);
opt->rep.table_index_factory = opt->table_index_factory.get();
opt->rep.read_index = ReadOptions::ReadIndex::kPreferCustom;
}
void rocksdb_readoptions_clear_table_index_factory(rocksdb_readoptions_t* opt) {
opt->table_index_factory.reset();
opt->rep.table_index_factory = nullptr;
opt->rep.read_index = ReadOptions::ReadIndex::kDefault;
}
void rocksdb_readoptions_set_read_index(rocksdb_readoptions_t* opt, int v) {
opt->rep.read_index = static_cast<ReadOptions::ReadIndex>(v);
}
int rocksdb_readoptions_get_read_index(const rocksdb_readoptions_t* opt) {
return static_cast<int>(opt->rep.read_index);
}
const char* rocksdb_readoptions_get_table_index_factory_name(
@@ -10176,6 +10280,16 @@ const char* rocksdb_options_get_db_host_id(rocksdb_options_t* opt,
return opt->rep.db_host_id.data();
}
void rocksdb_options_set_use_separate_remote_compaction_pool(
rocksdb_options_t* opt, unsigned char v) {
opt->rep.use_separate_remote_compaction_pool = v;
}
unsigned char rocksdb_options_get_use_separate_remote_compaction_pool(
rocksdb_options_t* opt) {
return opt->rep.use_separate_remote_compaction_pool;
}
void rocksdb_options_set_remote_compaction_manifest_floor(
rocksdb_options_t* opt, unsigned char v) {
opt->rep.remote_compaction_manifest_floor = v;
@@ -11269,6 +11383,16 @@ unsigned char rocksdb_trace_options_get_preserve_write_order(
return opt->rep.preserve_write_order;
}
void rocksdb_trace_options_set_include_read_write_options(
rocksdb_trace_options_t* opt, unsigned char v) {
opt->rep.include_read_write_options = v;
}
unsigned char rocksdb_trace_options_get_include_read_write_options(
rocksdb_trace_options_t* opt) {
return opt->rep.include_read_write_options;
}
/* BlockCacheTraceOptions */
void rocksdb_block_cache_trace_options_set_sampling_frequency(
@@ -11348,6 +11472,16 @@ unsigned char rocksdb_compaction_options_get_allow_trivial_move(
/* CompactRangeOptions */
void rocksdb_compactoptions_set_skip_missing_blob_files(
rocksdb_compactoptions_t* opt, unsigned char v) {
opt->rep.skip_missing_blob_files = v;
}
unsigned char rocksdb_compactoptions_get_skip_missing_blob_files(
rocksdb_compactoptions_t* opt) {
return opt->rep.skip_missing_blob_files;
}
void rocksdb_compactoptions_set_blob_garbage_collection_policy(
rocksdb_compactoptions_t* opt, int v) {
opt->rep.blob_garbage_collection_policy =
@@ -11495,6 +11629,16 @@ int rocksdb_ingestexternalfileoptions_get_file_opening_threads(
return opt->rep.file_opening_threads;
}
void rocksdb_ingestexternalfileoptions_set_reject_too_old_file(
rocksdb_ingestexternalfileoptions_t* opt, unsigned char v) {
opt->rep.reject_too_old_file = v;
}
unsigned char rocksdb_ingestexternalfileoptions_get_reject_too_old_file(
rocksdb_ingestexternalfileoptions_t* opt) {
return opt->rep.reject_too_old_file;
}
/* OpenAndCompactOptions */
void rocksdb_open_and_compact_options_set_max_secondary_open_retries(
@@ -11519,6 +11663,16 @@ unsigned char rocksdb_import_column_family_options_get_move_files(
return opt->rep.move_files;
}
void rocksdb_import_column_family_options_set_reject_too_old_file(
rocksdb_import_column_family_options_t* opt, unsigned char v) {
opt->rep.reject_too_old_file = v;
}
unsigned char rocksdb_import_column_family_options_get_reject_too_old_file(
rocksdb_import_column_family_options_t* opt) {
return opt->rep.reject_too_old_file;
}
/* SizeApproximationOptions */
void rocksdb_size_approximation_options_set_include_memtables(
@@ -11813,26 +11967,6 @@ unsigned char rocksdb_block_based_options_get_use_delta_encoding(
return opt->rep.use_delta_encoding;
}
void rocksdb_block_based_options_set_use_udi_as_primary_index(
rocksdb_block_based_table_options_t* opt, unsigned char v) {
opt->rep.use_udi_as_primary_index = v;
}
unsigned char rocksdb_block_based_options_get_use_udi_as_primary_index(
rocksdb_block_based_table_options_t* opt) {
return opt->rep.use_udi_as_primary_index;
}
void rocksdb_block_based_options_set_fail_if_no_udi_on_open(
rocksdb_block_based_table_options_t* opt, unsigned char v) {
opt->rep.fail_if_no_udi_on_open = v;
}
unsigned char rocksdb_block_based_options_get_fail_if_no_udi_on_open(
rocksdb_block_based_table_options_t* opt) {
return opt->rep.fail_if_no_udi_on_open;
}
void rocksdb_block_based_options_set_whole_key_filtering(
rocksdb_block_based_table_options_t* opt, unsigned char v) {
opt->rep.whole_key_filtering = v;
@@ -13405,6 +13539,74 @@ void rocksdb_transactiondb_multi_get_cf(
}
}
void rocksdb_transactiondb_batched_multi_get_cf(
rocksdb_transactiondb_t* txn_db, const rocksdb_readoptions_t* options,
rocksdb_column_family_handle_t* column_family, size_t num_keys,
const char* const* keys_list, const size_t* keys_list_sizes,
rocksdb_pinnableslice_t** values, char** errs, const bool sorted_input) {
Slice* key_slices = new Slice[num_keys];
PinnableSlice* value_slices = new PinnableSlice[num_keys];
Status* statuses = new Status[num_keys];
for (size_t i = 0; i < num_keys; ++i) {
key_slices[i] = Slice(keys_list[i], keys_list_sizes[i]);
}
txn_db->rep->MultiGet(options->rep, column_family->rep, num_keys, key_slices,
value_slices, statuses, sorted_input);
for (size_t i = 0; i < num_keys; ++i) {
if (statuses[i].ok()) {
values[i] = new (rocksdb_pinnableslice_t);
values[i]->rep = std::move(value_slices[i]);
errs[i] = nullptr;
} else {
values[i] = nullptr;
if (!statuses[i].IsNotFound()) {
errs[i] = strdup(statuses[i].ToString().c_str());
} else {
errs[i] = nullptr;
}
}
}
delete[] key_slices;
delete[] value_slices;
delete[] statuses;
}
void rocksdb_transactiondb_batched_multi_get_cf_slice(
rocksdb_transactiondb_t* txn_db, const rocksdb_readoptions_t* options,
rocksdb_column_family_handle_t* column_family, size_t num_keys,
const rocksdb_slice_t* keys_list, rocksdb_pinnableslice_t** values,
char** errs, const bool sorted_input) {
PinnableSlice* value_slices = new PinnableSlice[num_keys];
Status* statuses = new Status[num_keys];
// Cast rocksdb_slice_t* to Slice* - they have identical memory layout
const Slice* key_slices = reinterpret_cast<const Slice*>(keys_list);
txn_db->rep->MultiGet(options->rep, column_family->rep, num_keys, key_slices,
value_slices, statuses, sorted_input);
for (size_t i = 0; i < num_keys; ++i) {
if (statuses[i].ok()) {
values[i] = new (rocksdb_pinnableslice_t);
values[i]->rep = std::move(value_slices[i]);
errs[i] = nullptr;
} else {
values[i] = nullptr;
if (!statuses[i].IsNotFound()) {
errs[i] = strdup(statuses[i].ToString().c_str());
} else {
errs[i] = nullptr;
}
}
}
delete[] value_slices;
delete[] statuses;
}
void rocksdb_transaction_set_commit_timestamp(rocksdb_transaction_t* txn,
uint64_t commit_timestamp) {
txn->rep->SetCommitTimestamp(commit_timestamp);
+189 -3
View File
@@ -240,6 +240,138 @@ static void CheckPinnableMultiGetError(
CheckCondition(strstr(error, expected) != NULL);
}
static void CheckTxnDBBatchedMultiGet(rocksdb_transactiondb_t* db,
const rocksdb_readoptions_t* options,
rocksdb_column_family_handle_t* cf,
const char* expected_a) {
for (int sorted = 0; sorted <= 1; ++sorted) {
const char* keys[4] = {"b", "a", "b", "missing"};
const size_t sizes[4] = {1, 1, 1, 7};
const char* expected[4] = {"", expected_a, "", NULL};
if (sorted) {
keys[0] = "a";
keys[1] = "b";
expected[0] = expected_a;
expected[1] = "";
}
for (int slices = 0; slices <= 1; ++slices) {
rocksdb_pinnableslice_t* values[4];
char* errs[4];
if (slices) {
rocksdb_slice_t key_slices[4];
for (size_t i = 0; i < 4; ++i) {
key_slices[i].data = keys[i];
key_slices[i].size = sizes[i];
}
rocksdb_transactiondb_batched_multi_get_cf_slice(
db, options, cf, 4, key_slices, values, errs, sorted);
} else {
rocksdb_transactiondb_batched_multi_get_cf(db, options, cf, 4, keys,
sizes, values, errs, sorted);
}
for (size_t i = 0; i < 4; ++i) {
CheckNoError(errs[i]);
if (expected[i] == NULL) {
CheckCondition(values[i] == NULL);
} else {
size_t size;
CheckCondition(values[i] != NULL);
const char* value = rocksdb_pinnableslice_value(values[i], &size);
CheckEqual(expected[i], value, size);
}
rocksdb_pinnableslice_destroy(values[i]);
}
}
}
rocksdb_transactiondb_batched_multi_get_cf(db, options, cf, 0, NULL, NULL,
NULL, NULL, false);
rocksdb_transactiondb_batched_multi_get_cf_slice(db, options, cf, 0, NULL,
NULL, NULL, true);
}
static void TestTxnDBBatchedMultiGet(void) {
rocksdb_options_t* options = rocksdb_options_create();
rocksdb_readoptions_t* ro = rocksdb_readoptions_create();
rocksdb_writeoptions_t* wo = rocksdb_writeoptions_create();
rocksdb_transaction_options_t* to = rocksdb_transaction_options_create();
rocksdb_transactiondb_options_t* dbo = rocksdb_transactiondb_options_create();
char path[200];
char* err = NULL;
snprintf(path, sizeof(path), "%s/rocksdb_c_test-batched-txn-%" PRIu64,
GetTempDir(), GetTestId());
rocksdb_options_set_create_if_missing(options, 1);
rocksdb_transaction_options_set_skip_prepare(to, 0);
for (int policy = rocksdb_txndb_write_policy_write_committed;
policy <= rocksdb_txndb_write_policy_write_prepared; ++policy) {
rocksdb_destroy_db(options, path, &err);
CheckNoError(err);
rocksdb_transactiondb_options_set_write_policy(dbo, policy);
rocksdb_transactiondb_t* db =
rocksdb_transactiondb_open(options, dbo, path, &err);
CheckNoError(err);
rocksdb_column_family_handle_t* cf =
rocksdb_transactiondb_create_column_family(db, options, "batched",
&err);
CheckNoError(err);
rocksdb_transactiondb_put_cf(db, wo, cf, "a", 1, "old", 3, &err);
CheckNoError(err);
rocksdb_transactiondb_put_cf(db, wo, cf, "b", 1, "", 0, &err);
CheckNoError(err);
CheckTxnDBBatchedMultiGet(db, ro, cf, "old");
rocksdb_transaction_t* txn = rocksdb_transaction_begin(db, wo, to, NULL);
rocksdb_transaction_set_name(txn, "batched", 7, &err);
CheckNoError(err);
rocksdb_transaction_put_cf(txn, cf, "a", 1, "new", 3, &err);
CheckNoError(err);
rocksdb_transaction_prepare(txn, &err);
CheckNoError(err);
// WRITE_PREPARED puts the new value in the base DB before commit. Reads
// must go through TransactionDB to keep that value invisible.
CheckTxnDBBatchedMultiGet(db, ro, cf, "old");
const rocksdb_snapshot_t* snapshot =
rocksdb_transactiondb_create_snapshot(db);
rocksdb_transaction_commit(txn, &err);
CheckNoError(err);
CheckTxnDBBatchedMultiGet(db, ro, cf, "new");
rocksdb_readoptions_set_snapshot(ro, snapshot);
CheckTxnDBBatchedMultiGet(db, ro, cf, "old");
rocksdb_readoptions_set_snapshot(ro, NULL);
rocksdb_transactiondb_release_snapshot(db, snapshot);
// A timestamp on a non-timestamped column family must return an error,
// rather than being reported as a missing key.
rocksdb_readoptions_set_timestamp(ro, "timestamp", 9);
for (int slices = 0; slices <= 1; ++slices) {
const char* keys[1] = {"a"};
const size_t sizes[1] = {1};
const rocksdb_slice_t key_slices[1] = {{"a", 1}};
rocksdb_pinnableslice_t* values[1];
char* errs[1];
if (slices) {
rocksdb_transactiondb_batched_multi_get_cf_slice(
db, ro, cf, 1, key_slices, values, errs, true);
} else {
rocksdb_transactiondb_batched_multi_get_cf(db, ro, cf, 1, keys, sizes,
values, errs, true);
}
CheckCondition(values[0] == NULL);
CheckCondition(errs[0] != NULL);
rocksdb_free(errs[0]);
}
rocksdb_readoptions_set_timestamp(ro, NULL, 0);
rocksdb_transaction_destroy(txn);
rocksdb_column_family_handle_destroy(cf);
rocksdb_transactiondb_close(db);
rocksdb_destroy_db(options, path, &err);
CheckNoError(err);
}
rocksdb_transactiondb_options_destroy(dbo);
rocksdb_transaction_options_destroy(to);
rocksdb_writeoptions_destroy(wo);
rocksdb_readoptions_destroy(ro);
rocksdb_options_destroy(options);
}
static void CheckIter(rocksdb_iterator_t* iter, const char* key,
const char* val) {
size_t len;
@@ -324,7 +456,6 @@ static void CheckPutCF(void* ptr, uint32_t cfid, const char* k, size_t klen,
break;
default:
CheckCondition(false);
break;
}
(*state)++;
}
@@ -343,7 +474,6 @@ static void CheckDelCF(void* ptr, uint32_t cfid, const char* k, size_t klen) {
break;
default:
CheckCondition(false);
break;
}
(*state)++;
}
@@ -365,7 +495,6 @@ static void CheckMergeCF(void* ptr, uint32_t cfid, const char* k, size_t klen,
break;
default:
CheckCondition(false);
break;
}
(*state)++;
}
@@ -3630,6 +3759,42 @@ int main(int argc, char** argv) {
bbo, &factory_name_len);
CheckCondition(factory_name == NULL);
CheckCondition(factory_name_len == 0);
CheckCondition(
rocksdb_block_based_options_get_use_udi_as_primary_index(bbo) == 0);
rocksdb_block_based_options_set_use_udi_as_primary_index(bbo, 1);
CheckCondition(
rocksdb_block_based_options_get_use_udi_as_primary_index(bbo) == 1);
rocksdb_block_based_options_set_use_udi_as_primary_index(bbo, 0);
CheckCondition(
rocksdb_block_based_options_get_use_udi_as_primary_index(bbo) == 0);
CheckCondition(
rocksdb_block_based_options_get_fail_if_no_udi_on_open(bbo) == 0);
rocksdb_block_based_options_set_fail_if_no_udi_on_open(bbo, 1);
CheckCondition(
rocksdb_block_based_options_get_fail_if_no_udi_on_open(bbo) == 1);
rocksdb_block_based_options_set_fail_if_no_udi_on_open(bbo, 0);
CheckCondition(
rocksdb_block_based_options_get_fail_if_no_udi_on_open(bbo) == 0);
// index_mode defaults to standard_default and tracks the deprecated
// bools until it is set explicitly; after that an explicit value wins no
// matter what order the deprecated setters are called in.
CheckCondition(rocksdb_block_based_table_index_mode_standard_default ==
rocksdb_block_based_options_get_index_mode(bbo));
rocksdb_block_based_options_set_use_udi_as_primary_index(bbo, 1);
CheckCondition(rocksdb_block_based_table_index_mode_custom_default ==
rocksdb_block_based_options_get_index_mode(bbo));
rocksdb_block_based_options_set_index_mode(
bbo, rocksdb_block_based_table_index_mode_custom_only);
CheckCondition(rocksdb_block_based_table_index_mode_custom_only ==
rocksdb_block_based_options_get_index_mode(bbo));
rocksdb_block_based_options_set_index_mode(
bbo, rocksdb_block_based_table_index_mode_standard_only);
rocksdb_block_based_options_set_fail_if_no_udi_on_open(bbo, 1);
CheckCondition(rocksdb_block_based_table_index_mode_standard_only ==
rocksdb_block_based_options_get_index_mode(bbo));
rocksdb_block_based_options_set_use_udi_as_primary_index(bbo, 0);
rocksdb_block_based_options_set_fail_if_no_udi_on_open(bbo, 0);
rocksdb_block_based_options_destroy(bbo);
}
@@ -4677,6 +4842,24 @@ int main(int argc, char** argv) {
CheckCondition(table_index_factory_name == NULL);
CheckCondition(table_index_factory_name_len == 0);
// read_index: set_table_index_factory_from_string implies prefer_custom;
// clearing it returns to default; the explicit setter reaches builtin,
// which the legacy factory shim cannot express.
CheckCondition(rocksdb_readoptions_read_index_default ==
rocksdb_readoptions_get_read_index(ro));
rocksdb_readoptions_set_read_index(ro,
rocksdb_readoptions_read_index_builtin);
CheckCondition(rocksdb_readoptions_read_index_builtin ==
rocksdb_readoptions_get_read_index(ro));
rocksdb_readoptions_set_table_index_factory_from_string(ro, "trie_index",
10, &err);
CheckNoError(err);
CheckCondition(rocksdb_readoptions_read_index_prefer_custom ==
rocksdb_readoptions_get_read_index(ro));
rocksdb_readoptions_clear_table_index_factory(ro);
CheckCondition(rocksdb_readoptions_read_index_default ==
rocksdb_readoptions_get_read_index(ro));
// optimize_multiget_for_io defaults to true (1); flip to 0 to verify the
// setter is wired through to the underlying C++ ReadOptions field.
CheckCondition(1 == rocksdb_readoptions_get_optimize_multiget_for_io(ro));
@@ -5587,6 +5770,9 @@ int main(int argc, char** argv) {
CheckNoError(err);
}
StartPhase("transactiondb_batched_multi_get");
TestTxnDBBatchedMultiGet();
StartPhase("transactions");
{
rocksdb_close(db);
+34 -7
View File
@@ -475,6 +475,9 @@ ColumnFamilyOptions SanitizeCfOptions(const ImmutableDBOptions& db_options,
result.preclude_last_level_data_seconds > 0)) {
// With no writes coming in, we don't need periodic SeqnoToTime entries.
// Existing SST files may or may not have that info associated with them.
// Remote compaction runs in a secondary (read-only) instance but writes
// files, so it restores both from the primary. See
// DBImplSecondary::CompactWithoutInstallation().
ROCKS_LOG_WARN(
db_options.info_log.get(),
"preserve_internal_time_seconds and preclude_last_level_data_seconds "
@@ -632,6 +635,8 @@ ColumnFamilyData::ColumnFamilyData(
mutable_cf_options_(initial_cf_options_),
is_delete_range_supported_(
cf_options.table_factory->IsDeleteRangeSupported()),
is_table_builder_supported_(
cf_options.table_factory->IsTableBuilderSupported()),
write_buffer_manager_(write_buffer_manager),
mem_(nullptr),
imm_(ioptions_.min_write_buffer_number_to_merge,
@@ -1022,13 +1027,16 @@ ColumnFamilyData::GetWriteStallConditionAndCause(
int num_unflushed_memtables, int num_l0_files,
uint64_t num_compaction_needed_bytes,
const MutableCFOptions& mutable_cf_options,
const ImmutableCFOptions& immutable_cf_options) {
const ImmutableCFOptions& immutable_cf_options,
bool table_builder_supported) {
const bool auto_compactions =
table_builder_supported && !mutable_cf_options.disable_auto_compactions;
if (num_unflushed_memtables >= mutable_cf_options.max_write_buffer_number) {
return {WriteStallCondition::kStopped, WriteStallCause::kMemtableLimit};
} else if (!mutable_cf_options.disable_auto_compactions &&
} else if (auto_compactions &&
num_l0_files >= mutable_cf_options.level0_stop_writes_trigger) {
return {WriteStallCondition::kStopped, WriteStallCause::kL0FileCountLimit};
} else if (!mutable_cf_options.disable_auto_compactions &&
} else if (auto_compactions &&
mutable_cf_options.hard_pending_compaction_bytes_limit > 0 &&
num_compaction_needed_bytes >=
mutable_cf_options.hard_pending_compaction_bytes_limit) {
@@ -1040,12 +1048,12 @@ ColumnFamilyData::GetWriteStallConditionAndCause(
num_unflushed_memtables - 1 >=
immutable_cf_options.min_write_buffer_number_to_merge) {
return {WriteStallCondition::kDelayed, WriteStallCause::kMemtableLimit};
} else if (!mutable_cf_options.disable_auto_compactions &&
} else if (auto_compactions &&
mutable_cf_options.level0_slowdown_writes_trigger >= 0 &&
num_l0_files >=
mutable_cf_options.level0_slowdown_writes_trigger) {
return {WriteStallCondition::kDelayed, WriteStallCause::kL0FileCountLimit};
} else if (!mutable_cf_options.disable_auto_compactions &&
} else if (auto_compactions &&
mutable_cf_options.soft_pending_compaction_bytes_limit > 0 &&
num_compaction_needed_bytes >=
mutable_cf_options.soft_pending_compaction_bytes_limit) {
@@ -1067,7 +1075,7 @@ WriteStallCondition ColumnFamilyData::RecalculateWriteStallConditions(
auto write_stall_condition_and_cause = GetWriteStallConditionAndCause(
imm()->NumNotFlushed(), vstorage->l0_delay_trigger_count(),
vstorage->estimated_compaction_needed_bytes(), mutable_cf_options,
ioptions());
ioptions(), is_table_builder_supported_);
write_stall_condition = write_stall_condition_and_cause.first;
auto write_stall_cause = write_stall_condition_and_cause.second;
@@ -1273,7 +1281,8 @@ void ColumnFamilyData::CreateNewMemtable(SequenceNumber earliest_seq) {
}
bool ColumnFamilyData::NeedsCompaction() const {
return !mutable_cf_options_.disable_auto_compactions &&
return is_table_builder_supported_ &&
!mutable_cf_options_.disable_auto_compactions &&
compaction_picker_->NeedsCompaction(current_->storage_info());
}
@@ -1538,6 +1547,13 @@ Status ColumnFamilyData::ValidateOptions(
return s;
}
if (!cf_options.table_factory->IsTableBuilderSupported() &&
!cf_options.disallow_memtable_writes) {
return Status::InvalidArgument(
"A table factory that cannot build tables requires "
"disallow_memtable_writes");
}
if (cf_options.ttl > 0 && cf_options.ttl != kDefaultTtl) {
if (!cf_options.table_factory->IsInstanceOf(
TableFactory::kBlockBasedTableName())) {
@@ -1738,10 +1754,21 @@ Status ColumnFamilyData::SetOptions(
if (s.ok()) {
mutable_cf_options_ = MutableCFOptions(cf_opts);
mutable_cf_options_.RefreshDerivedOptions(ioptions_);
options_install_pending_ = true;
}
return s;
}
bool ColumnFamilyData::SetOptionsIsNoOp(
const DBOptions& db_opts,
const std::unordered_map<std::string, std::string>& options_map) const {
// SetOptions() validates the resulting options, so a request that changes
// nothing must pass the same check to return the same status.
return !IsDropped() && !options_install_pending_ &&
MutableCFOptionsUnchangedByMap(mutable_cf_options_, options_map) &&
ValidateOptions(db_opts, GetLatestCFOptions()).ok();
}
Status ColumnFamilyData::AddDirectories(
std::map<std::string, std::shared_ptr<FSDirectory>>* created_dirs) {
Status s;
+33 -5
View File
@@ -368,6 +368,12 @@ class ColumnFamilyData {
bool is_delete_range_supported() { return is_delete_range_supported_; }
// False when the table factory cannot write tables, so the column family is
// never compacted.
bool is_table_builder_supported() const {
return is_table_builder_supported_;
}
// Validate CF options against DB options
static Status ValidateOptions(const DBOptions& db_options,
const ColumnFamilyOptions& cf_options);
@@ -375,6 +381,19 @@ class ColumnFamilyData {
Status SetOptions(
const DBOptions& db_options,
const std::unordered_map<std::string, std::string>& options_map);
// REQUIRES: DB mutex held
// Returns true if the column family is not dropped, SetOptions(db_options,
// options_map) would succeed without changing any option, and options from
// an earlier SetOptions() are not still waiting for MarkOptionsInstalled().
// Can return false for a request that changes nothing, for example one that
// sets a table factory field.
bool SetOptionsIsNoOp(
const DBOptions& db_options,
const std::unordered_map<std::string, std::string>& options_map) const;
// REQUIRES: DB mutex held
// Call after the options from SetOptions() are in the current SuperVersion
// and every other user of them is updated.
void MarkOptionsInstalled() { options_install_pending_ = false; }
InternalStats* internal_stats() { return internal_stats_.get(); }
@@ -549,12 +568,15 @@ class ColumnFamilyData {
--num_in_flight_compactions_;
}
// L0 files and pending compaction bytes stall writes only when the column
// family can be compacted automatically, which requires
// 'table_builder_supported' and no disable_auto_compactions.
static std::pair<WriteStallCondition, WriteStallCause>
GetWriteStallConditionAndCause(
int num_unflushed_memtables, int num_l0_files,
uint64_t num_compaction_needed_bytes,
const MutableCFOptions& mutable_cf_options,
const ImmutableCFOptions& immutable_cf_options);
GetWriteStallConditionAndCause(int num_unflushed_memtables, int num_l0_files,
uint64_t num_compaction_needed_bytes,
const MutableCFOptions& mutable_cf_options,
const ImmutableCFOptions& immutable_cf_options,
bool table_builder_supported);
// Recalculate some stall conditions, which are changed only during
// compaction, adding new memtable and/or recalculation of compaction score.
@@ -692,6 +714,12 @@ class ColumnFamilyData {
MutableCFOptions mutable_cf_options_;
const bool is_delete_range_supported_;
const bool is_table_builder_supported_;
// True from a successful SetOptions() until MarkOptionsInstalled(). A
// DB::SetOptions() call that fails on a later column family leaves it set
// for the column families it already changed.
bool options_install_pending_ = false;
std::unique_ptr<TableCache> table_cache_;
std::unique_ptr<BlobFileCache> blob_file_cache_;
+20 -1
View File
@@ -468,7 +468,6 @@ class ColumnFamilyTestBase : public testing::Test {
}
}
return ret;
return 0;
}
void AssertCountLiveLogFiles(int value) {
@@ -806,6 +805,26 @@ TEST_P(ColumnFamilyTest, EmptyNameRejected) {
ASSERT_TRUE(handles.empty());
}
TEST_P(ColumnFamilyTest, CreateRejectsOptionsThatOpenRejects) {
// CreateColumnFamily() used to skip the table factory checks of DB::Open, so
// it persisted a column family that made the next DB::Open fail.
Open();
ColumnFamilyOptions hash_index_options = column_family_options_;
BlockBasedTableOptions table_options;
table_options.index_type = BlockBasedTableOptions::kHashSearch;
hash_index_options.table_factory.reset(
NewBlockBasedTableFactory(table_options));
hash_index_options.prefix_extractor.reset();
ColumnFamilyHandle* handle = nullptr;
Status s = db_->CreateColumnFamily(hash_index_options, "hash_index", &handle);
ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString();
ASSERT_EQ(handle, nullptr);
Close();
AssertColumnFamilyNames({kDefaultColumnFamilyName});
ASSERT_OK(TryOpen({kDefaultColumnFamilyName}));
}
TEST_P(ColumnFamilyTest, BulkAddDrop) {
constexpr int kNumCF = 1000;
ColumnFamilyOptions cf_options;
+4
View File
@@ -555,6 +555,10 @@ bool Compaction::InputCompressionMatchesOutput() const {
}
bool Compaction::IsTrivialMove() const {
if (skip_missing_blob_files_) {
return false;
}
// Avoid a move if there is lots of overlapping grandparent data.
// Otherwise, the move could create a parent file that will require
// a very expensive merge later on.
+18 -1
View File
@@ -151,6 +151,14 @@ class Compaction {
// Returns input version of the compaction
Version* input_version() const { return input_version_; }
bool skip_missing_blob_files() const { return skip_missing_blob_files_; }
// Set before registering or running this compaction.
void SetSkipMissingBlobFiles(bool skip_missing_blob_files) {
assert(is_manual_compaction_ || !skip_missing_blob_files);
skip_missing_blob_files_ = skip_missing_blob_files;
}
// Returns the ColumnFamilyData associated with the compaction.
ColumnFamilyData* column_family_data() const { return cfd_; }
@@ -271,6 +279,13 @@ class Compaction {
// Does this compaction include all sst files?
bool is_full_compaction() const { return is_full_compaction_; }
bool separate_remote_compaction_pool_selected() const {
return separate_remote_compaction_pool_selected_;
}
void SetSeparateRemoteCompactionPoolSelected(bool selected) {
separate_remote_compaction_pool_selected_ = selected;
}
// Was this compaction triggered manually by the client?
bool is_manual_compaction() const { return is_manual_compaction_; }
@@ -531,7 +546,7 @@ class Compaction {
// Get the atomic file boundaries for all files in the compaction. Necessary
// in order to avoid the scenario described in
// https://github.com/facebook/rocksdb/pull/4432#discussion_r221072219 and
// https://github.com/meta/rocksdb/pull/4432#discussion_r221072219 and
// plumb down appropriate key boundaries to RangeDelAggregator during
// compaction.
static std::vector<CompactionInputFiles> PopulateWithAtomicBoundaries(
@@ -609,9 +624,11 @@ class Compaction {
const bool bottommost_level_;
// Does this compaction include all sst files?
const bool is_full_compaction_;
bool separate_remote_compaction_pool_selected_ = false;
// Is this compaction requested by the client?
const bool is_manual_compaction_;
bool skip_missing_blob_files_ = false;
// The data with timestamp > trim_ts_ will be removed
const std::string trim_ts_;
+432 -92
View File
@@ -5,6 +5,7 @@
#include "db/compaction/compaction_iterator.h"
#include <cassert>
#include <iterator>
#include <limits>
#include <memory>
@@ -12,6 +13,7 @@
#include "db/blob/blob_fetcher.h"
#include "db/blob/blob_file_builder.h"
#include "db/blob/blob_index.h"
#include "db/blob/missing_blob_repair_iterator.h"
#include "db/blob/prefetch_buffer_collection.h"
#include "db/snapshot_checker.h"
#include "db/wide/blob_column_resolver_util.h"
@@ -139,7 +141,8 @@ CompactionIterator::CompactionIterator(
const std::shared_ptr<Logger> info_log,
const std::string* full_history_ts_low,
std::optional<SequenceNumber> preserve_seqno_min,
const Version* input_version, Env::IOActivity blob_read_io_activity)
const Version* input_version, Env::IOActivity blob_read_io_activity,
std::optional<uint64_t> blob_gc_cutoff_file_number)
: CompactionIterator(
input, cmp, merge_helper, last_sequence, snapshots, earliest_snapshot,
earliest_write_conflict_snapshot, job_snapshot, snapshot_checker, env,
@@ -149,7 +152,7 @@ CompactionIterator::CompactionIterator(
compaction ? std::make_unique<RealCompaction>(compaction) : nullptr,
must_count_input_entries, compaction_filter, shutting_down, info_log,
full_history_ts_low, preserve_seqno_min, input_version,
blob_read_io_activity) {}
blob_read_io_activity, blob_gc_cutoff_file_number) {}
CompactionIterator::CompactionIterator(
InternalIterator* input, const Comparator* cmp, MergeHelper* merge_helper,
@@ -168,7 +171,8 @@ CompactionIterator::CompactionIterator(
const std::shared_ptr<Logger> info_log,
const std::string* full_history_ts_low,
std::optional<SequenceNumber> preserve_seqno_min,
const Version* input_version, Env::IOActivity blob_read_io_activity)
const Version* input_version, Env::IOActivity blob_read_io_activity,
std::optional<uint64_t> blob_gc_cutoff_file_number)
: input_(input, cmp, must_count_input_entries),
cmp_(cmp),
merge_helper_(merge_helper),
@@ -183,6 +187,9 @@ CompactionIterator::CompactionIterator(
blob_file_builder_(blob_file_builder),
compaction_(std::move(compaction)),
compaction_filter_(compaction_filter),
merge_order_(merge_helper
? merge_helper->merge_order()
: CompactionFilter::MergeOrder::kFilterBeforeMerge),
filter_supports_v4_(
compaction_filter_ ? compaction_filter_->SupportsFilterV4() : false),
shutting_down_(shutting_down),
@@ -202,7 +209,10 @@ CompactionIterator::CompactionIterator(
current_user_key_snapshot_(0),
merge_out_iter_(merge_helper_),
blob_garbage_collection_cutoff_file_number_(
ComputeBlobGarbageCollectionCutoffFileNumber(compaction_.get())),
blob_gc_cutoff_file_number.has_value()
? *blob_gc_cutoff_file_number
: ComputeBlobGarbageCollectionCutoffFileNumber(
compaction_.get())),
blob_fetcher_(CreateBlobFetcherIfNeeded(compaction_.get(), input_version,
blob_read_io_activity)),
prefetch_buffers_(
@@ -212,8 +222,20 @@ CompactionIterator::CompactionIterator(
level_(compaction_ == nullptr ? 0 : compaction_->level()),
preserve_seqno_after_(preserve_seqno_min.value_or(earliest_snapshot)) {
assert(snapshots_ != nullptr);
if (compaction_filter_ &&
merge_order_ == CompactionFilter::MergeOrder::kMergeBeforeFilter &&
(snapshot_checker_ != nullptr || timestamp_size_ != 0 ||
compaction_filter_->IsStackedBlobDbInternalCompactionFilter())) {
status_ = Status::NotSupported(
"Merge-before-filter does not support SnapshotChecker, timestamps, "
"or stacked BlobDB");
}
assert(preserve_seqno_after_ <= earliest_snapshot_);
TEST_SYNC_POINT_CALLBACK(
"CompactionIterator::BlobGarbageCollectionCutoffFileNumber",
&blob_garbage_collection_cutoff_file_number_);
if (compaction_ != nullptr) {
level_ptrs_ = std::vector<size_t>(compaction_->number_levels(), 0);
}
@@ -268,18 +290,41 @@ void CompactionIterator::ResetRecordCounts() {
}
void CompactionIterator::SeekToFirst() {
SeekToFirstWithoutPreparingOutput();
PrepareOutputIfNeeded();
}
void CompactionIterator::SeekToFirstWithoutPreparingOutput() {
if (!status_.ok()) {
return;
}
NextFromInput();
PrepareOutput();
}
void CompactionIterator::Next() {
NextImpl();
PrepareOutputIfNeeded();
}
void CompactionIterator::NextWithoutPreparingOutput() { NextImpl(); }
void CompactionIterator::NextImpl() {
if (!status_.ok()) {
return;
}
output_prepared_ = false;
// If there is a merge output, return it before continuing to process the
// input.
if (merge_out_iter_.Valid()) {
merge_out_iter_.Next();
// Check if we returned all records of the merge output.
if (merge_out_iter_.Valid()) {
if (merge_out_iter_.Valid() &&
merge_order_ == CompactionFilter::MergeOrder::kMergeBeforeFilter) {
if (!SelectMergeOutput(ValidContext::kMerge1) && status_.ok()) {
NextFromInput();
}
} else if (merge_out_iter_.Valid()) {
key_ = merge_out_iter_.key();
value_ = merge_out_iter_.value();
Status s = ParseInternalKey(key_, &ikey_, allow_data_in_errors_);
@@ -339,25 +384,33 @@ void CompactionIterator::Next() {
// Record that we've outputted a record for the current key.
has_outputted_key_ = true;
}
}
PrepareOutput();
void CompactionIterator::PrepareOutputIfNeeded() {
if (!output_prepared_) {
PrepareOutput();
output_prepared_ = true;
}
}
bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
Slice* skip_until) {
Slice* skip_until,
bool merge_output) {
if (!compaction_filter_) {
return true;
}
if (ikey_.type != kTypeValue && ikey_.type != kTypeBlobIndex &&
ikey_.type != kTypeWideColumnEntity) {
ikey_.type != kTypeWideColumnEntity &&
!(merge_output && ikey_.type == kTypeMerge)) {
return true;
}
CompactionFilter::Decision decision =
CompactionFilter::Decision::kUndetermined;
CompactionFilter::ValueType value_type =
ikey_.type == kTypeValue ? CompactionFilter::ValueType::kValue
ikey_.type == kTypeValue ? CompactionFilter::ValueType::kValue
: ikey_.type == kTypeMerge ? CompactionFilter::ValueType::kMergeOperand
: ikey_.type == kTypeBlobIndex
? CompactionFilter::ValueType::kBlobIndex
: CompactionFilter::ValueType::kWideColumnEntity;
@@ -590,6 +643,45 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
return false;
}
if (merge_order_ == CompactionFilter::MergeOrder::kMergeBeforeFilter) {
const bool operand = ikey_.type == kTypeMerge;
switch (decision) {
case CompactionFilter::Decision::kKeep:
case CompactionFilter::Decision::kRemove:
case CompactionFilter::Decision::kChangeValue:
case CompactionFilter::Decision::kRemoveAndSkipUntil:
break;
case CompactionFilter::Decision::kPurge:
if (!operand) {
// Use ordinary removal for both input values and merge results, so
// later compactions apply the same policy. SingleDelete cannot be
// mixed with Merge or ordinary Delete on affected keys.
decision = CompactionFilter::Decision::kRemove;
break;
}
[[fallthrough]];
case CompactionFilter::Decision::kChangeWideColumnEntity:
if (!operand) {
break;
}
// Partial results and untouched operands must remain valid merge
// operands. Neither SingleDelete nor an entity replacement represents
// an operand; kRemove drops its effect and kChangeValue replaces it.
[[fallthrough]];
case CompactionFilter::Decision::kChangeBlobIndex:
case CompactionFilter::Decision::kIOError:
case CompactionFilter::Decision::kUndetermined:
// kChangeBlobIndex/kIOError belong to the old stacked BlobDB protocol.
// Integrated BlobDB changes use kChangeValue and report resolver errors
// through Status. kUndetermined is only a FilterBlobByKey fallback,
// never a final FilterV2/V3/V4 decision.
status_ = Status::NotSupported(
"Invalid decision for merge-before-filter value type");
validity_info_.Invalidate();
return false;
}
}
if (decision == CompactionFilter::Decision::kRemoveAndSkipUntil &&
cmp_->Compare(*compaction_filter_skip_until_.rep(), ikey_.user_key) <=
0) {
@@ -599,6 +691,11 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
}
if (decision == CompactionFilter::Decision::kRemove) {
if (ikey_.type == kTypeMerge) {
++iter_stats_.num_record_drop_user;
validity_info_.Invalidate();
return true;
}
// convert the current key to a delete; key_ is pointing into
// current_key_ at this point, so updating current_key_ updates key()
ikey_.type = kTypeDeletion;
@@ -615,7 +712,7 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
value_.clear();
iter_stats_.num_record_drop_user++;
} else if (decision == CompactionFilter::Decision::kChangeValue) {
if (ikey_.type != kTypeValue) {
if (ikey_.type != kTypeValue && ikey_.type != kTypeMerge) {
ikey_.type = kTypeValue;
current_key_.UpdateInternalKey(ikey_.sequence, kTypeValue);
}
@@ -697,6 +794,221 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
return true;
}
bool CompactionIterator::PrefilterUncombinedOutputs(uint8_t valid_context) {
uncombined_prefiltered_ = true;
uncombined_decisions_.clear();
uncombined_index_ = 0;
MergeOutputIterator it(merge_helper_);
for (it.SeekToFirst(); it.Valid(); it.Next()) {
key_ = it.key();
value_ = it.value();
status_ = ParseInternalKey(key_, &ikey_, allow_data_in_errors_);
if (!status_.ok()) {
validity_info_.Invalidate();
merge_out_iter_.Invalidate();
return false;
}
current_key_.UpdateInternalKey(ikey_.sequence, ikey_.type);
key_ = current_key_.GetInternalKey();
ikey_.user_key = current_key_.GetUserKey();
entity_deserialized_ = false;
blob_value_.Reset();
validity_info_.SetValid(valid_context);
UncombinedDecision decision;
if (merge_helper_->OperandEligible(ikey_.sequence)) {
bool need_skip = false;
Slice skip_until;
if (!InvokeFilterIfNeeded(&need_skip, &skip_until, true)) {
merge_out_iter_.Invalidate();
return false;
}
if (need_skip) {
// Newer operands of this batch were not output yet, so the skip drops
// the whole batch.
merge_out_iter_.Invalidate();
validity_info_.Invalidate();
pinned_iters_mgr_.ReleasePinnedData();
SkipUntil(skip_until);
return false;
}
decision.keep = Valid();
if (decision.keep && value_.data() == compaction_filter_value_.data()) {
decision.changed = true;
decision.value = compaction_filter_value_;
}
}
uncombined_decisions_.push_back(std::move(decision));
}
return true;
}
bool CompactionIterator::SelectMergeOutput(uint8_t valid_context) {
const bool prefiltered =
compaction_filter_ != nullptr &&
merge_helper_->outcome() == MergeHelper::Outcome::kUncombined;
if (prefiltered && !uncombined_prefiltered_ &&
!PrefilterUncombinedOutputs(valid_context)) {
return false;
}
while (merge_out_iter_.Valid() && !IsShuttingDown() &&
!IsPausingManualCompaction()) {
key_ = merge_out_iter_.key();
value_ = merge_out_iter_.value();
status_ = ParseInternalKey(key_, &ikey_, allow_data_in_errors_);
if (!status_.ok()) {
validity_info_.Invalidate();
merge_out_iter_.Invalidate();
return false;
}
current_key_.UpdateInternalKey(ikey_.sequence, ikey_.type);
key_ = current_key_.GetInternalKey();
ikey_.user_key = current_key_.GetUserKey();
entity_deserialized_ = false;
blob_value_.Reset();
validity_info_.SetValid(valid_context);
if (prefiltered) {
if (uncombined_index_ < uncombined_decisions_.size()) {
const UncombinedDecision& decision =
uncombined_decisions_[uncombined_index_];
if (!decision.keep) {
validity_info_.Invalidate();
} else if (decision.changed) {
value_ = decision.value;
}
}
++uncombined_index_;
} else if (merge_helper_->aggregate_eligible() && compaction_filter_) {
bool need_skip = false;
Slice skip_until;
if (!InvokeFilterIfNeeded(&need_skip, &skip_until, true)) {
merge_out_iter_.Invalidate();
return false;
}
if (need_skip) {
merge_out_iter_.Invalidate();
validity_info_.Invalidate();
pinned_iters_mgr_.ReleasePinnedData();
SkipUntil(skip_until);
return false;
}
if (Valid() && ikey_.type == kTypeDeletion) {
SequenceNumber prev_snapshot = 0;
current_user_key_sequence_ = ikey_.sequence;
current_user_key_snapshot_ =
visible_at_tip_
? earliest_snapshot_
: findEarliestVisibleSnapshot(ikey_.sequence, &prev_snapshot);
ProcessDeletion(prev_snapshot, true);
}
}
if (Valid()) {
return true;
}
merge_out_iter_.Next();
}
if (merge_until_status_.IsMergeInProgress()) {
has_current_user_key_ = false;
}
pinned_iters_mgr_.ReleasePinnedData();
return false;
}
bool CompactionIterator::ProcessDeletion(SequenceNumber prev_snapshot,
bool input_already_advanced) {
if (compaction_ != nullptr &&
(ikey_.type == kTypeDeletion ||
(ikey_.type == kTypeDeletionWithTimestamp &&
cmp_with_history_ts_low_ < 0)) &&
!compaction_->allow_ingest_behind() &&
DefinitelyInSnapshot(ikey_.sequence, earliest_snapshot_) &&
compaction_->KeyNotExistsBeyondOutputLevel(ikey_.user_key,
&level_ptrs_)) {
// TODO(noetzli): This is the only place where we use compaction_
// (besides the constructor). We should probably get rid of this
// dependency and find a way to do similar filtering during flushes.
//
// For this user key:
// (1) there is no data in higher levels
// (2) data in lower levels will have larger sequence numbers
// (3) data in layers that are being compacted here and have
// smaller sequence numbers will be dropped in the next
// few iterations of this loop (by rule (A) above).
// Therefore this deletion marker is obsolete and can be dropped.
//
// Note: Dropping this Delete will not affect TransactionDB
// write-conflict checking since it is earlier than any snapshot.
//
// It seems that we can also drop deletion later than earliest snapshot
// given that:
// (1) The deletion is earlier than earliest_write_conflict_snapshot, and
// (2) No value exist earlier than the deletion.
//
// Note also that a deletion marker of type kTypeDeletionWithTimestamp
// will be treated as a different user key unless the timestamp is older
// than *full_history_ts_low_.
validity_info_.Invalidate();
++iter_stats_.num_record_drop_obsolete;
if (!bottommost_level_) {
++iter_stats_.num_optimized_del_drop_obsolete;
}
if (!input_already_advanced) {
AdvanceInputIterWithoutOutput();
}
} else if ((ikey_.type == kTypeDeletion ||
(ikey_.type == kTypeDeletionWithTimestamp &&
cmp_with_history_ts_low_ < 0)) &&
bottommost_level_) {
assert(compaction_);
assert(!compaction_->allow_ingest_behind()); // bottommost_level_ is true
// Handle the case where we have a delete key at the bottom most level
// We can skip outputting the key iff there are no subsequent puts for
// this key
assert(compaction_->KeyNotExistsBeyondOutputLevel(ikey_.user_key,
&level_ptrs_));
validity_info_.Invalidate();
ParsedInternalKey next_ikey;
if (!input_already_advanced) {
AdvanceInputIterWithoutOutput();
}
#ifndef NDEBUG
const Compaction* c =
compaction_ ? compaction_->real_compaction() : nullptr;
#endif
TEST_SYNC_POINT_CALLBACK(
"CompactionIterator::NextFromInput:BottommostDelete:1",
const_cast<Compaction*>(c));
// Skip over all versions of this key that happen to occur in the same
// snapshot range as the delete.
//
// Note that a deletion marker of type kTypeDeletionWithTimestamp will be
// considered to have a different user key unless the timestamp is older
// than *full_history_ts_low_.
//
// Range tombstone start keys are skipped as they are not "real" keys.
while (!IsPausingManualCompaction() && !IsShuttingDown() &&
input_.Valid() &&
(ParseInternalKey(input_.key(), &next_ikey, allow_data_in_errors_)
.ok()) &&
cmp_->EqualWithoutTimestamp(ikey_.user_key, next_ikey.user_key) &&
(prev_snapshot == 0 || input_.IsDeleteRangeSentinelKey() ||
DefinitelyNotInSnapshot(next_ikey.sequence, prev_snapshot))) {
AdvanceInputIterWithoutOutput();
}
// If you find you still need to output a row with this key, we need to
// output the delete too
if (input_.Valid() &&
(ParseInternalKey(input_.key(), &next_ikey, allow_data_in_errors_)
.ok()) &&
cmp_->EqualWithoutTimestamp(ikey_.user_key, next_ikey.user_key)) {
validity_info_.SetValid(ValidContext::kKeepDel);
at_next_ = true;
}
} else {
return false;
}
return true;
}
void CompactionIterator::NextFromInput() {
at_next_ = false;
validity_info_.Invalidate();
@@ -722,6 +1034,12 @@ void CompactionIterator::NextFromInput() {
status_ = pik_status;
return;
}
if (has_consumed_earlier_entries_of_current_key_ &&
!cmp_->EqualWithoutTimestamp(ikey_.user_key, current_user_key_)) {
// current_user_key_ remains usable when lookahead or merge handling
// clears has_current_user_key_.
has_consumed_earlier_entries_of_current_key_ = false;
}
TEST_SYNC_POINT_CALLBACK("CompactionIterator:ProcessKV", &ikey_);
if (is_range_del_) {
validity_info_.SetValid(kRangeDeletion);
@@ -807,7 +1125,7 @@ void CompactionIterator::NextFromInput() {
// Apply the compaction filter to the first committed version of the user
// key.
if (current_key_committed_ &&
!InvokeFilterIfNeeded(&need_skip, &skip_until)) {
!InvokeFilterIfNeeded(&need_skip, &skip_until, false)) {
break;
}
} else {
@@ -829,7 +1147,7 @@ void CompactionIterator::NextFromInput() {
// Apply the compaction filter to the first committed version of the
// user key.
if (current_key_committed_ &&
!InvokeFilterIfNeeded(&need_skip, &skip_until)) {
!InvokeFilterIfNeeded(&need_skip, &skip_until, false)) {
break;
}
}
@@ -952,14 +1270,14 @@ void CompactionIterator::NextFromInput() {
(full_history_ts_low_ && cmp_with_history_ts_low_ < 0));
ParsedInternalKey next_ikey;
AdvanceInputIter();
AdvanceInputIterWithoutOutput();
while (input_.Valid() && input_.IsDeleteRangeSentinelKey() &&
ParseInternalKey(input_.key(), &next_ikey, allow_data_in_errors_)
.ok() &&
cmp_->EqualWithoutTimestamp(ikey_.user_key, next_ikey.user_key)) {
// skip range tombstone start keys with the same user key
// since they are not "real" point keys.
AdvanceInputIter();
AdvanceInputIterWithoutOutput();
}
// Check whether the next key exists, is not corrupt, and is the same key
@@ -976,13 +1294,27 @@ void CompactionIterator::NextFromInput() {
TEST_SYNC_POINT_CALLBACK(
"CompactionIterator::NextFromInput:SingleDelete:1",
const_cast<Compaction*>(c));
if (last_key_seq_zeroed_) {
if (missing_blob_repair_iterator_ &&
missing_blob_repair_iterator_->IsRepairDeletion()) {
ikey_.type =
timestamp_size_ == 0 ? kTypeDeletion : kTypeDeletionWithTimestamp;
current_key_.UpdateInternalKey(ikey_.sequence, ikey_.type);
value_.clear();
if (DeletionIsObsolete()) {
++iter_stats_.num_record_drop_obsolete;
if (!bottommost_level_) {
++iter_stats_.num_optimized_del_drop_obsolete;
}
} else {
validity_info_.SetValid(ValidContext::kKeepDel);
}
} else if (last_key_seq_zeroed_) {
// Drop SD and the next key since they are both in the last
// snapshot (since last key has seqno zeroed).
++iter_stats_.num_record_drop_hidden;
++iter_stats_.num_record_drop_obsolete;
assert(bottommost_level_);
AdvanceInputIter();
AdvanceInputIterWithoutOutput();
} else if (prev_snapshot == 0 ||
DefinitelyNotInSnapshot(next_ikey.sequence, prev_snapshot)) {
// Check whether the next key belongs to the same snapshot as the
@@ -1054,7 +1386,7 @@ void CompactionIterator::NextFromInput() {
++iter_stats_.num_record_drop_obsolete;
// Already called input_.Next() once. Call it a second time to
// skip past the second key.
AdvanceInputIter();
AdvanceInputIterWithoutOutput();
} else {
// Found a matching value, but we cannot drop both keys since
// there is an earlier snapshot and we need to leave behind a record
@@ -1149,15 +1481,8 @@ void CompactionIterator::NextFromInput() {
}
++iter_stats_.num_record_drop_hidden;
AdvanceInputIter();
} else if (compaction_ != nullptr &&
(ikey_.type == kTypeDeletion ||
(ikey_.type == kTypeDeletionWithTimestamp &&
cmp_with_history_ts_low_ < 0)) &&
!compaction_->allow_ingest_behind() &&
DefinitelyInSnapshot(ikey_.sequence, earliest_snapshot_) &&
compaction_->KeyNotExistsBeyondOutputLevel(ikey_.user_key,
&level_ptrs_)) {
AdvanceInputIterWithoutOutput();
} else if (DeletionIsObsolete()) {
// TODO(noetzli): This is the only place where we use compaction_
// (besides the constructor). We should probably get rid of this
// dependency and find a way to do similar filtering during flushes.
@@ -1185,53 +1510,11 @@ void CompactionIterator::NextFromInput() {
if (!bottommost_level_) {
++iter_stats_.num_optimized_del_drop_obsolete;
}
AdvanceInputIter();
AdvanceInputIterWithoutOutput();
} else if ((ikey_.type == kTypeDeletion ||
(ikey_.type == kTypeDeletionWithTimestamp &&
cmp_with_history_ts_low_ < 0)) &&
bottommost_level_) {
assert(compaction_);
assert(!compaction_->allow_ingest_behind()); // bottommost_level_ is true
// Handle the case where we have a delete key at the bottom most level
// We can skip outputting the key iff there are no subsequent puts for
// this key
assert(compaction_->KeyNotExistsBeyondOutputLevel(ikey_.user_key,
&level_ptrs_));
ParsedInternalKey next_ikey;
AdvanceInputIter();
#ifndef NDEBUG
const Compaction* c =
compaction_ ? compaction_->real_compaction() : nullptr;
#endif
TEST_SYNC_POINT_CALLBACK(
"CompactionIterator::NextFromInput:BottommostDelete:1",
const_cast<Compaction*>(c));
// Skip over all versions of this key that happen to occur in the same
// snapshot range as the delete.
//
// Note that a deletion marker of type kTypeDeletionWithTimestamp will be
// considered to have a different user key unless the timestamp is older
// than *full_history_ts_low_.
//
// Range tombstone start keys are skipped as they are not "real" keys.
while (!IsPausingManualCompaction() && !IsShuttingDown() &&
input_.Valid() &&
(ParseInternalKey(input_.key(), &next_ikey, allow_data_in_errors_)
.ok()) &&
cmp_->EqualWithoutTimestamp(ikey_.user_key, next_ikey.user_key) &&
(prev_snapshot == 0 || input_.IsDeleteRangeSentinelKey() ||
DefinitelyNotInSnapshot(next_ikey.sequence, prev_snapshot))) {
AdvanceInputIter();
}
// If you find you still need to output a row with this key, we need to
// output the delete too
if (input_.Valid() &&
(ParseInternalKey(input_.key(), &next_ikey, allow_data_in_errors_)
.ok()) &&
cmp_->EqualWithoutTimestamp(ikey_.user_key, next_ikey.user_key)) {
validity_info_.SetValid(ValidContext::kKeepDel);
at_next_ = true;
}
ProcessDeletion(prev_snapshot, false)) {
} else if (ikey_.type == kTypeValuePreferredSeqno &&
DefinitelyInSnapshot(ikey_.sequence, earliest_snapshot_) &&
(bottommost_level_ ||
@@ -1267,7 +1550,7 @@ void CompactionIterator::NextFromInput() {
key_, RangeDelPositioningMode::kForwardTraversal)) {
++iter_stats_.num_record_drop_hidden;
++iter_stats_.num_record_drop_range_del;
AdvanceInputIter();
AdvanceInputIterWithoutOutput();
} else {
InternalKey ikey_after_swap(ikey_.user_key,
std::min(preferred_seqno, ikey_.sequence),
@@ -1306,36 +1589,45 @@ void CompactionIterator::NextFromInput() {
// have hit (A)
// We encapsulate the merge related state machine in a different
// object to minimize change to the existing flow.
has_consumed_earlier_entries_of_current_key_ = true;
merge_until_status_ = merge_helper_->MergeUntil(
&input_, range_del_agg_, prev_snapshot, bottommost_level_,
allow_data_in_errors_, blob_fetcher_.get(), full_history_ts_low_,
prefetch_buffers_.get(), &iter_stats_);
merge_out_iter_.SeekToFirst();
uncombined_prefiltered_ = false;
if (!merge_until_status_.ok() &&
!merge_until_status_.IsMergeInProgress()) {
status_ = merge_until_status_;
return;
} else if (merge_out_iter_.Valid()) {
// NOTE: key, value, and ikey_ refer to old entries.
// These will be correctly set below.
key_ = merge_out_iter_.key();
value_ = merge_out_iter_.value();
pik_status = ParseInternalKey(key_, &ikey_, allow_data_in_errors_);
// MergeUntil stops when it encounters a corrupt key and does not
// include them in the result, so we expect the keys here to valid.
if (!pik_status.ok()) {
ROCKS_LOG_FATAL(
info_log_, "Invalid key %s in compaction. %s",
allow_data_in_errors_ ? key_.ToString(true).c_str() : "hidden",
pik_status.getState());
assert(false);
if (merge_order_ == CompactionFilter::MergeOrder::kMergeBeforeFilter) {
SelectMergeOutput(ValidContext::kMerge2);
if (!status_.ok()) {
return;
}
} else {
// NOTE: key, value, and ikey_ refer to old entries.
// These will be correctly set below.
key_ = merge_out_iter_.key();
value_ = merge_out_iter_.value();
pik_status = ParseInternalKey(key_, &ikey_, allow_data_in_errors_);
// MergeUntil stops when it encounters a corrupt key and does not
// include them in the result, so we expect the keys here to valid.
if (!pik_status.ok()) {
ROCKS_LOG_FATAL(
info_log_, "Invalid key %s in compaction. %s",
allow_data_in_errors_ ? key_.ToString(true).c_str() : "hidden",
pik_status.getState());
assert(false);
}
// Keep current_key_ in sync.
current_key_.UpdateInternalKey(ikey_.sequence, ikey_.type);
key_ = current_key_.GetInternalKey();
ikey_.user_key = current_key_.GetUserKey();
validity_info_.SetValid(ValidContext::kMerge2);
}
// Keep current_key_ in sync.
current_key_.UpdateInternalKey(ikey_.sequence, ikey_.type);
key_ = current_key_.GetInternalKey();
ikey_.user_key = current_key_.GetUserKey();
validity_info_.SetValid(ValidContext::kMerge2);
} else {
// all merge operands were filtered out. reset the user key, since the
// batch consumed by the merge operator should not shadow any keys
@@ -1364,7 +1656,7 @@ void CompactionIterator::NextFromInput() {
if (should_delete) {
++iter_stats_.num_record_drop_hidden;
++iter_stats_.num_record_drop_range_del;
AdvanceInputIter();
AdvanceInputIterWithoutOutput();
} else {
validity_info_.SetValid(ValidContext::kNewUserKey);
}
@@ -1387,6 +1679,17 @@ void CompactionIterator::NextFromInput() {
}
}
bool CompactionIterator::DeletionIsObsolete() {
return compaction_ &&
(ikey_.type == kTypeDeletion ||
(ikey_.type == kTypeDeletionWithTimestamp &&
cmp_with_history_ts_low_ < 0)) &&
!compaction_->allow_ingest_behind() &&
DefinitelyInSnapshot(ikey_.sequence, earliest_snapshot_) &&
compaction_->KeyNotExistsBeyondOutputLevel(ikey_.user_key,
&level_ptrs_);
}
bool CompactionIterator::ExtractLargeValueIfNeededImpl() {
if (!blob_file_builder_) {
return false;
@@ -1855,6 +2158,31 @@ void CompactionIterator::GarbageCollectEntityBlobsIfNeeded() {
fetched_blob_values, new_blob_columns);
}
Status CompactionIterator::OutputHasExternalBlobReferences(
bool* has_external_refs) const {
assert(has_external_refs);
*has_external_refs = false;
if (ikey_.type == kTypeBlobIndex) {
BlobIndex index;
const Status s = index.DecodeFrom(value_);
if (!s.ok()) {
return s;
}
*has_external_refs = !index.IsInlined() && !index.IsSameFile();
return Status::OK();
}
if (ikey_.type == kTypeWideColumnEntity) {
return WideColumnSerialization::ForEachBlobFileNumber(
value_, [has_external_refs](const BlobIndex& index) {
if (!index.IsInlined() && !index.IsSameFile()) {
*has_external_refs = true;
}
return Status::OK();
});
}
return Status::OK();
}
void CompactionIterator::PrepareOutput() {
if (Valid()) {
if (LIKELY(!is_range_del_)) {
@@ -1941,7 +2269,19 @@ void CompactionIterator::PrepareOutput() {
if (BottommostSeqnoCanBeZeroed(
ikey_.sequence, preserve_seqno_after_, timestamp_size_,
full_history_ts_low_ != nullptr, cmp_with_history_ts_low_ < 0)) {
if (!timestamp_size_) {
bool has_external_blob_refs = false;
if (timestamp_size_) {
const Status s =
OutputHasExternalBlobReferences(&has_external_blob_refs);
if (!s.ok()) {
status_ = s;
(void)status_.ok();
validity_info_.Invalidate();
return;
}
}
// External blob records include the timestamp in their stored user key.
if (!timestamp_size_ || has_external_blob_refs) {
current_key_.UpdateInternalKey(0, ikey_.type);
} else {
// For UDT, the seqno and timestamp could only be zeroed out after the
+77 -6
View File
@@ -5,11 +5,16 @@
#pragma once
#include <algorithm>
#include <atomic>
#include <cassert>
#include <cinttypes>
#include <cstddef>
#include <cstdint>
#include <deque>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "db/blob/blob_index.h"
@@ -29,6 +34,7 @@ class BlobFileBuilder;
class BlobFileCache;
class BlobFetcher;
class Version;
class MissingBlobRepairIterator;
class PrefetchBufferCollection;
class FilePrefetchBuffer;
class Version;
@@ -274,7 +280,8 @@ class CompactionIterator {
const std::string* full_history_ts_low = nullptr,
std::optional<SequenceNumber> preserve_seqno_min = {},
const Version* input_version = nullptr,
Env::IOActivity blob_read_io_activity = Env::IOActivity::kCompaction);
Env::IOActivity blob_read_io_activity = Env::IOActivity::kCompaction,
std::optional<uint64_t> blob_gc_cutoff_file_number = {});
// Constructor with custom CompactionProxy, used for tests.
CompactionIterator(
@@ -296,7 +303,8 @@ class CompactionIterator {
const std::string* full_history_ts_low = nullptr,
std::optional<SequenceNumber> preserve_seqno_min = {},
const Version* input_version = nullptr,
Env::IOActivity blob_read_io_activity = Env::IOActivity::kCompaction);
Env::IOActivity blob_read_io_activity = Env::IOActivity::kCompaction,
std::optional<uint64_t> blob_gc_cutoff_file_number = {});
~CompactionIterator();
@@ -309,16 +317,36 @@ class CompactionIterator {
Env::IOActivity io_activity,
bool allow_write_path_fallback);
// Install before the first seek; the normalizer must outlive this iterator.
void SetMissingBlobRepairIterator(const MissingBlobRepairIterator* iterator) {
assert(key_.empty());
missing_blob_repair_iterator_ = iterator;
}
static uint64_t ComputeBlobGarbageCollectionCutoffFileNumber(
const CompactionProxy* compaction);
// Seek to the beginning of the compaction iterator output.
//
// REQUIRED: Call only once.
void SeekToFirst();
// Positions at the first output without extracting/relocating blobs yet.
// CompactionJob uses this when an SST checkpoint may need to be persisted
// before the current key is added to a blob file.
void SeekToFirstWithoutPreparingOutput();
// Produces the next record in the compaction.
//
// REQUIRED: SeekToFirst() has been called.
void Next();
// Advances without extracting/relocating blobs for the new current key.
void NextWithoutPreparingOutput();
// Completes output preparation once for the current key.
void PrepareOutputIfNeeded();
// Getters
const Slice& key() const { return key_; }
const Slice& value() const { return value_; }
@@ -348,6 +376,15 @@ class CompactionIterator {
return at_next_ || merge_out_iter_.Valid();
}
// Returns true if an earlier input entry for the current valid user key was
// consumed without being output.
//
// REQUIRED: Valid() must be true
bool HasConsumedEarlierEntriesOfCurrentKey() const {
assert(Valid());
return has_consumed_earlier_entries_of_current_key_;
}
Status InputStatus() const { return input_.status(); }
bool IsDeleteRangeSentinelKey() const { return is_range_del_; }
@@ -356,6 +393,8 @@ class CompactionIterator {
// Processes the input stream to find the next output
void NextFromInput();
void NextImpl();
// Do final preparations before presenting the output to the callee.
void PrepareOutput();
@@ -421,7 +460,16 @@ class CompactionIterator {
// Invoke compaction filter if needed.
// Return true on success, false on failures (e.g.: kIOError).
bool InvokeFilterIfNeeded(bool* need_skip, Slice* skip_until);
bool InvokeFilterIfNeeded(bool* need_skip, Slice* skip_until,
bool merge_output);
bool SelectMergeOutput(uint8_t valid_context);
bool PrefilterUncombinedOutputs(uint8_t valid_context);
bool ProcessDeletion(SequenceNumber prev_snapshot,
bool input_already_advanced);
bool DeletionIsObsolete();
Status OutputHasExternalBlobReferences(bool* has_external_refs) const;
// Given a sequence number, return the sequence number of the
// earliest snapshot that this sequence number is visible in.
@@ -456,8 +504,6 @@ class CompactionIterator {
}
}
static uint64_t ComputeBlobGarbageCollectionCutoffFileNumber(
const CompactionProxy* compaction);
static std::unique_ptr<BlobFetcher> CreateBlobFetcherIfNeeded(
const CompactionProxy* compaction, const Version* input_version,
Env::IOActivity blob_read_io_activity);
@@ -465,6 +511,7 @@ class CompactionIterator {
CreatePrefetchBufferCollectionIfNeeded(const CompactionProxy* compaction);
SequenceIterWrapper input_;
const MissingBlobRepairIterator* missing_blob_repair_iterator_ = nullptr;
const Comparator* cmp_;
MergeHelper* merge_helper_;
const std::vector<SequenceNumber>* snapshots_;
@@ -484,6 +531,7 @@ class CompactionIterator {
BlobFileBuilder* blob_file_builder_;
std::unique_ptr<CompactionProxy> compaction_;
const CompactionFilter* compaction_filter_;
const CompactionFilter::MergeOrder merge_order_;
const bool filter_supports_v4_;
const std::atomic<bool>* shutting_down_;
const std::atomic<bool>& manual_compaction_canceled_;
@@ -557,6 +605,8 @@ class CompactionIterator {
// If false, the iterator holds a copy of the current compaction iterator
// output (or current key in the underlying iterator during NextFromInput()).
bool at_next_ = false;
// Whether an earlier input entry of the current user key was not output.
bool has_consumed_earlier_entries_of_current_key_ = false;
// A copy of the current internal key.
IterKey current_key_;
@@ -574,6 +624,17 @@ class CompactionIterator {
bool clear_and_output_next_key_ = false;
MergeOutputIterator merge_out_iter_;
// Merge-before-filter decisions for the current batch of uncombined
// operands. They are made before any operand is output, so
// kRemoveAndSkipUntil drops the whole batch, as in the legacy order.
struct UncombinedDecision {
bool keep = true;
bool changed = false;
std::string value;
};
std::vector<UncombinedDecision> uncombined_decisions_;
size_t uncombined_index_ = 0;
bool uncombined_prefiltered_ = false;
Status merge_until_status_;
// PinnedIteratorsManager used to pin input_ Iterator blocks while reading
// merge operands and then releasing them after consuming them.
@@ -614,6 +675,7 @@ class CompactionIterator {
// it) and PrepareOutput() can skip redundant work. Reset for each candidate
// input record and whenever the filter rewrites the entity value.
bool entity_deserialized_{false};
bool output_prepared_{false};
// "level_ptrs" holds indices that remember which file of an associated
// level we were last checking during the last call to compaction->
// KeyNotExistsBeyondOutputLevel(). This allows future calls to the function
@@ -641,7 +703,16 @@ class CompactionIterator {
void AdvanceInputIter() { input_.Next(); }
void SkipUntil(const Slice& skip_until) { input_.Seek(skip_until); }
// Advances past an input entry that will not be emitted as output.
void AdvanceInputIterWithoutOutput() {
has_consumed_earlier_entries_of_current_key_ = true;
input_.Next();
}
void SkipUntil(const Slice& skip_until) {
has_consumed_earlier_entries_of_current_key_ = true;
input_.Seek(skip_until);
}
bool IsShuttingDown() {
// This is a best-effort facility, so memory_order_relaxed is sufficient.
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+162 -14
View File
@@ -14,9 +14,11 @@
#include <limits>
#include <set>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#include "db/blob/blob_counting_iterator.h"
#include "db/blob/blob_file_completion_callback.h"
#include "db/column_family.h"
#include "db/compaction/compaction_iterator.h"
@@ -51,6 +53,7 @@ class Arena;
class CompactionState;
class ErrorHandler;
class MemTable;
class MissingBlobRepairIterator;
class SnapshotChecker;
class SystemClock;
class TableCache;
@@ -171,6 +174,20 @@ class CompactionJob {
virtual ~CompactionJob();
using RemoteCompactionExecutor = std::function<void(std::function<void()>)>;
using LocalCompactionExecutor =
std::function<Status(std::function<void()>, Env::Priority)>;
// Set before Prepare(). Remote submissions are asynchronous: returning
// accepts the task exactly once, and throwing must leave it unsubmitted.
// Local execution blocks until the task finishes or is canceled. Both
// executors must outlive Run(). Service workers leave these unset.
void SetCompactionExecutors(RemoteCompactionExecutor remote_executor,
LocalCompactionExecutor local_executor) {
remote_compaction_executor_ = std::move(remote_executor);
local_compaction_executor_ = std::move(local_executor);
}
// no copy/move
CompactionJob(CompactionJob&& job) = delete;
CompactionJob(const CompactionJob& job) = delete;
@@ -223,6 +240,7 @@ class CompactionJob {
// Iterate through input and compact the kv-pairs.
void ProcessKeyValueCompaction(SubcompactionState* sub_compact);
void ProcessLocalKeyValueCompaction(SubcompactionState* sub_compact);
CompactionState* compact_;
InternalStats::CompactionStatsFull internal_stats_;
@@ -240,6 +258,10 @@ class CompactionJob {
CompactionJobStats* job_stats_;
// Lower bound for the minimal sequence number whose write time is
// preserved. See Prepare().
SequenceNumber preserve_time_min_seqno_floor_ = 0;
private:
friend class CompactionJobTestBase;
@@ -307,6 +329,7 @@ class CompactionJob {
void InitializeCompactionRun();
void RunSubcompactions();
void RecordRemoteCompactionInputBlobFileCount();
void UpdateTimingStats(uint64_t start_micros);
void RemoveEmptyOutputs();
void CleanupAbortedSubcompactions();
@@ -326,7 +349,8 @@ class CompactionJob {
uint64_t num_input_range_del);
CompactionServiceJobStatus ProcessKeyValueCompactionWithCompactionService(
SubcompactionState* sub_compact);
SubcompactionState* sub_compact,
CompactionFilter::MergeOrder merge_order);
struct CompactionIOStatsSnapshot {
PerfLevel prev_perf_level = PerfLevel::kEnableTime;
@@ -370,8 +394,9 @@ class CompactionJob {
struct SubcompactionInternalIterators {
std::unique_ptr<InternalIterator> raw_input;
std::unique_ptr<InternalIterator> clip;
std::unique_ptr<InternalIterator> blob_counter;
std::unique_ptr<BlobCountingIterator> blob_counter;
std::unique_ptr<InternalIterator> trim_history_iter;
std::unique_ptr<MissingBlobRepairIterator> missing_blob_repair;
};
bool ShouldUseLocalCompaction(SubcompactionState* sub_compact);
@@ -380,7 +405,8 @@ class CompactionJob {
SubcompactionState* sub_compact,
const CompactionFilter* configured_compaction_filter,
const CompactionFilter*& compaction_filter,
std::unique_ptr<CompactionFilter>& compaction_filter_from_factory);
std::unique_ptr<CompactionFilter>& compaction_filter_from_factory,
CompactionFilter::MergeOrder& merge_order);
void InitializeReadOptionsAndBoundaries(
size_t ts_sz, ReadOptions& read_options,
SubcompactionKeyBoundaries& boundaries);
@@ -398,11 +424,14 @@ class CompactionJob {
MergeHelper& merge, std::unique_ptr<BlobFileBuilder>& blob_file_builder,
const WriteOptions& write_options);
std::pair<CompactionFileOpenFunc, CompactionFileCloseFunc> CreateFileHandlers(
SubcompactionState* sub_compact, SubcompactionKeyBoundaries& boundaries);
SubcompactionState* sub_compact, SubcompactionKeyBoundaries& boundaries,
BlobFileBuilder* blob_file_builder,
const BlobCountingIterator* blob_counter);
Status ProcessKeyValue(SubcompactionState* sub_compact, ColumnFamilyData* cfd,
CompactionIterator* c_iter,
const CompactionFileOpenFunc& open_file_func,
const CompactionFileCloseFunc& close_file_func,
bool defer_blob_output_preparation,
uint64_t& prev_cpu_micros);
void UpdateSubcompactionJobStatsIncrementally(
CompactionIterator* c_iter, CompactionJobStats* compaction_job_stats,
@@ -431,15 +460,17 @@ class CompactionJob {
uint64_t prev_cpu_micros,
const CompactionIOStatsSnapshot& io_stats);
// update the thread status for starting a compaction.
void ReportStartedCompaction(Compaction* compaction);
void InitializeThreadStatus();
Status FinishCompactionOutputFile(
const Status& input_status,
const ParsedInternalKey& prev_iter_output_internal_key,
const Slice& next_table_min_key, const Slice* comp_start_user_key,
const Slice* comp_end_user_key, const CompactionIterator* c_iter,
SubcompactionState* sub_compact, CompactionOutputs& outputs);
bool post_prepare_retry, SubcompactionState* sub_compact,
BlobFileBuilder* blob_file_builder,
const BlobCountingIterator* blob_counter, CompactionOutputs& outputs);
Status InstallCompactionResults(bool* compaction_released);
Status OpenCompactionOutputFile(SubcompactionState* sub_compact,
CompactionOutputs& outputs);
@@ -452,6 +483,8 @@ class CompactionJob {
void NotifyOnSubcompactionCompleted(SubcompactionState* sub_compact);
uint32_t job_id_;
// Limits unsafe resume-boundary diagnostics to one per compaction job.
mutable std::atomic<bool> logged_unsafe_resume_boundary_{false};
// DBImpl state
const std::string& dbname_;
@@ -507,6 +540,8 @@ class CompactionJob {
// has no owning DBImpl to report to (compaction service workers, unit tests).
// Updated without the DB mutex held.
std::atomic<int>* num_running_remote_compactions_;
RemoteCompactionExecutor remote_compaction_executor_;
LocalCompactionExecutor local_compaction_executor_;
// Stores the sequence number to time mapping gathered from all input files
// it also collects the smallest_seqno -> oldest_ancestor_time from the SST.
@@ -532,16 +567,45 @@ class CompactionJob {
uint64_t min_manifest_file_number_ = 0;
uint64_t min_manifest_file_size_ = 0;
// Remote subcompactions run concurrently and validate their blob garbage
// before moving output into the primary. This aggregate prevents each
// subcompaction from independently claiming up to the same blob-file total.
port::Mutex remote_blob_garbage_mutex_;
std::unordered_map<uint64_t, BlobGarbageMeter::BlobStats>
remote_blob_garbage_;
// Minimum blob-file dependency among subcompactions successfully dispatched
// to a remote worker. Recorded once for the whole job after they finish.
std::atomic<uint64_t> remote_compaction_min_blob_file_number_{
std::numeric_limits<uint64_t>::max()};
// Writer for persisting compaction progress during compaction
log::Writer* compaction_progress_writer_ = nullptr;
// Get table file name in where it's outputting to, which should also be in
// `output_directory_`.
virtual std::string GetTableFileName(uint64_t file_number);
virtual const std::string& GetBlobOutputPath() const;
virtual uint64_t NewBlobFileNumber();
virtual uint64_t GetNextBlobFileNumber() const;
virtual Status RestoreNextBlobFileNumber(
const SubcompactionProgress& subcompaction_progress);
virtual std::optional<uint64_t> GetBlobGarbageCollectionCutoffFileNumber()
const {
return std::nullopt;
}
// True if this job builds output files as a remote (offloaded)
// CompactionService worker. Overridden by CompactionServiceCompactionJob.
virtual bool IsRemoteCompaction() const { return false; }
// Checks the actual filter order against what the job requires before any
// input is processed. A remote worker compares it with the primary's
// request and records the acknowledgment or decline in its result.
virtual Status ValidateCompactionFilterMergeOrder(
const CompactionFilter* /*compaction_filter*/,
CompactionFilter::MergeOrder /*merge_order*/) {
return Status::OK();
}
// The rate limiter priority (io_priority) is determined dynamically here.
// The Compaction Read and Write priorities are the same for different
// scenarios, such as write stalled.
@@ -555,7 +619,7 @@ class CompactionJob {
const ReadOptions& read_options,
std::vector<std::shared_ptr<const TableProperties>>&
output_files_table_properties,
bool is_proximal_level = false);
bool is_proximal_level);
Status ReadTablePropertiesDirectly(
const ImmutableOptions& ioptions, const MutableCFOptions& moptions,
@@ -572,12 +636,22 @@ class CompactionJob {
bool ShouldUpdateSubcompactionProgress(
const SubcompactionState* sub_compact, const CompactionIterator* c_iter,
const ParsedInternalKey& prev_iter_output_internal_key,
const Slice& next_table_min_internal_key, const FileMetaData* meta) const;
const Slice& next_table_min_internal_key, const FileMetaData* meta,
const BlobFileBuilder* blob_file_builder,
const BlobCountingIterator* blob_counter, bool post_prepare_retry) const;
void UpdateSubcompactionProgress(const CompactionIterator* c_iter,
const Slice next_table_min_key,
SubcompactionState* sub_compact);
Status RestoreBlobCompactionProgress(
SubcompactionState* sub_compact,
const SubcompactionProgress& subcompaction_progress);
void UpdateBlobCompactionProgress(
SubcompactionState* sub_compact,
SubcompactionProgress& subcompaction_progress);
Status PersistSubcompactionProgress(SubcompactionState* sub_compact);
void UpdateSubcompactionProgressPerLevel(
@@ -585,6 +659,12 @@ class CompactionJob {
SubcompactionProgress& subcompaction_progress);
};
// Version of the blob-aware remote compaction result produced by this binary.
// Zero means the worker did not advertise support.
constexpr uint64_t kCompactionServiceBlobVersion = 1;
constexpr uint64_t kCompactionServiceBlobGCCutoffVersion = 1;
constexpr uint64_t kCompactionServiceMergeBeforeFilterVersion = 1;
// CompactionServiceInput is used the pass compaction information between two
// db instances. It contains the information needed to do a compaction. It
// doesn't contain the LSM tree information, which is passed though MANIFEST
@@ -592,6 +672,11 @@ class CompactionJob {
struct CompactionServiceInput {
std::string cf_name;
// Requests merge-before-filter. Old workers ignore this field and return no
// applied-order acknowledgment, so the primary falls back to local compaction
// before importing their output.
bool merge_before_filter = false;
// Snapshot sequence numbers in strictly increasing order.
std::vector<SequenceNumber> snapshots;
@@ -599,6 +684,23 @@ struct CompactionServiceInput {
// files needed for this compaction, for both input level files and output
// level files.
std::vector<std::string> input_files;
// Blob files potentially referenced by input_files. CompactionService
// implementations can use this list to make the files available to the
// remote worker.
std::vector<std::string> input_blob_files;
// When present, the remote worker uses this primary-computed cutoff instead
// of deriving one from its reconstructed Version. An absent field disables
// blob GC on the worker for compatibility with older primaries.
bool has_blob_gc_cutoff_file_number = false;
uint64_t blob_gc_cutoff_file_number = 0;
// The primary reserves this half-open file-number range before scheduling
// the remote compaction. A worker that creates blob files must allocate them
// consecutively from this range.
uint64_t blob_file_number_start = kInvalidBlobFileNumber;
uint64_t blob_file_number_limit = kInvalidBlobFileNumber;
int output_level = 0;
// db_id is used to generate unique id of sst on the remote compactor
@@ -624,6 +726,24 @@ struct CompactionServiceInput {
uint64_t min_manifest_file_number = 0;
uint64_t min_manifest_file_size = 0;
// The primary's values. The worker opens the DB as a secondary, which
// sanitizes them to 0 as a read-only DB. They are sent here rather than
// re-read from the OPTIONS file so that they always arrive together with
// preserve_time_min_seqno_floor: a preserve window without that floor would
// bring back the kBottommostFiles compaction loop.
uint64_t preserve_internal_time_seconds = 0;
uint64_t preclude_last_level_data_seconds = 0;
// The primary's lower bound for the minimal sequence number whose write time
// is preserved: the version-carried preserve boundary for a kBottommostFiles
// compaction, 0 otherwise.
SequenceNumber preserve_time_min_seqno_floor = 0;
// The primary's per-key placement decisions. The worker declines the job if
// its own Compaction differs. proximal_level is Compaction::kInvalidLevel
// when per-key placement is off, and 0 only from an older primary.
int proximal_level = 0;
int proximal_output_range_type = 0;
SequenceNumber keep_in_last_level_through_seqno = 0;
// serialization interface to read and write the object
static Status Read(const std::string& data_str, CompactionServiceInput* obj);
Status Write(std::string* output);
@@ -634,7 +754,9 @@ struct CompactionServiceInput {
#endif // NDEBUG
};
// CompactionServiceOutputFile is the metadata for the output SST file
// CompactionServiceOutputFile is the metadata for the output SST file. Parsing
// a serialized result sets only the fields present in it, so every member
// needs a default value.
struct CompactionServiceOutputFile {
std::string file_name;
uint64_t file_size{};
@@ -642,25 +764,26 @@ struct CompactionServiceOutputFile {
SequenceNumber largest_seqno{};
std::string smallest_internal_key;
std::string largest_internal_key;
uint64_t oldest_blob_file_number = kInvalidBlobFileNumber;
uint64_t oldest_ancestor_time = kUnknownOldestAncestorTime;
uint64_t file_creation_time = kUnknownFileCreationTime;
uint64_t epoch_number = kUnknownEpochNumber;
std::string file_checksum = kUnknownFileChecksum;
std::string file_checksum_func_name = kUnknownFileChecksumFuncName;
uint64_t paranoid_hash{};
bool marked_for_compaction;
bool marked_for_compaction = false;
UniqueId64x2 unique_id{};
TableProperties table_properties;
bool is_proximal_level_output;
bool is_proximal_level_output = false;
Temperature file_temperature = Temperature::kUnknown;
CompactionServiceOutputFile() = default;
CompactionServiceOutputFile(
const std::string& name, uint64_t size, SequenceNumber smallest,
SequenceNumber largest, std::string _smallest_internal_key,
std::string _largest_internal_key, uint64_t _oldest_ancestor_time,
uint64_t _file_creation_time, uint64_t _epoch_number,
const std::string& _file_checksum,
std::string _largest_internal_key, uint64_t _oldest_blob_file_number,
uint64_t _oldest_ancestor_time, uint64_t _file_creation_time,
uint64_t _epoch_number, const std::string& _file_checksum,
const std::string& _file_checksum_func_name, uint64_t _paranoid_hash,
bool _marked_for_compaction, UniqueId64x2 _unique_id,
const TableProperties& _table_properties, bool _is_proximal_level_output,
@@ -671,6 +794,7 @@ struct CompactionServiceOutputFile {
largest_seqno(largest),
smallest_internal_key(std::move(_smallest_internal_key)),
largest_internal_key(std::move(_largest_internal_key)),
oldest_blob_file_number(_oldest_blob_file_number),
oldest_ancestor_time(_oldest_ancestor_time),
file_creation_time(_file_creation_time),
epoch_number(_epoch_number),
@@ -689,7 +813,19 @@ struct CompactionServiceOutputFile {
// permission is able to install the result to the DB.
struct CompactionServiceResult {
Status status;
// Nonzero only after validating the actual worker filter/factory order.
uint64_t merge_before_filter_version = 0;
// The worker declined an order mismatch before processing input records.
bool merge_order_mismatch = false;
std::vector<CompactionServiceOutputFile> output_files;
// Blob metadata generated by the remote compaction. Blob file additions
// identify files in output_path that must be installed before the output
// SSTs. Garbage records apply to blob files already known to the primary.
std::vector<BlobFileAddition> blob_file_additions;
std::vector<BlobFileGarbage> blob_file_garbages;
uint64_t blob_compaction_version = 0;
uint64_t blob_gc_cutoff_version = 0;
int output_level = 0;
// location of the output files
@@ -762,13 +898,25 @@ class CompactionServiceCompactionJob : private CompactionJob {
private:
// Get table file name in output_path
std::string GetTableFileName(uint64_t file_number) override;
const std::string& GetBlobOutputPath() const override;
uint64_t NewBlobFileNumber() override;
uint64_t GetNextBlobFileNumber() const override;
Status RestoreNextBlobFileNumber(
const SubcompactionProgress& subcompaction_progress) override;
std::optional<uint64_t> GetBlobGarbageCollectionCutoffFileNumber()
const override;
bool IsRemoteCompaction() const override { return true; }
Status ValidateCompactionFilterMergeOrder(
const CompactionFilter* compaction_filter,
CompactionFilter::MergeOrder merge_order) override;
// Specific the compaction output path, otherwise it uses default DB path
const std::string output_path_;
// Compaction job input
const CompactionServiceInput& compaction_input_;
uint64_t next_blob_file_number_;
// Compaction job result
CompactionServiceResult* compaction_result_;
};
+261 -1
View File
@@ -196,6 +196,42 @@ class MockTestFileSystem : public FileSystemWrapper {
enum TableTypeForTest : uint8_t { kMockTable = 0, kBlockBasedTable = 1 };
// Replaces the first scalar `name=value;` entry after `scope` in a serialized
// CompactionServiceResult with `replacement`. An empty `replacement` removes
// the entry. Returns false if there is no such entry.
bool ReplaceResultEntry(std::string* serialized, const std::string& scope,
const std::string& name,
const std::string& replacement) {
const size_t scope_pos = serialized->find(scope);
if (scope_pos == std::string::npos) {
return false;
}
// Match the whole name, so that "code" does not match "subcode".
size_t pos = std::string::npos;
for (const char delimiter : {'{', ';'}) {
pos = std::min(pos, serialized->find(delimiter + name + "=", scope_pos));
}
if (pos == std::string::npos) {
return false;
}
const size_t end = serialized->find(';', pos + 1);
if (end == std::string::npos) {
return false;
}
serialized->replace(pos + 1, end - pos, replacement);
return true;
}
// Formats every field of `s`. A failed gtest comparison prints the `Status`
// enums as raw characters, so tests compare these strings instead.
std::string StatusFields(const Status& s) {
const char* state = s.getState();
return "code=" + std::to_string(s.code()) +
" subcode=" + std::to_string(s.subcode()) +
" severity=" + std::to_string(s.severity()) +
" message=" + (state != nullptr ? state : "");
}
} // namespace
class CompactionJobTestBase : public testing::Test {
@@ -787,6 +823,20 @@ TEST_F(CompactionJobTest, Simple) {
RunCompaction({files}, {input_level}, {expected_results});
}
TEST_F(CompactionJobTest, AtomicallyAllocatesFileNumberRange) {
NewDB();
const uint64_t next = versions_->current_next_file_number();
uint64_t first = 0;
ASSERT_TRUE(versions_->TryAllocateFileNumberRange(3, &first));
ASSERT_EQ(first, next);
ASSERT_EQ(versions_->current_next_file_number(), next + 3);
ASSERT_FALSE(versions_->TryAllocateFileNumberRange(0, &first));
ASSERT_FALSE(versions_->TryAllocateFileNumberRange(1, nullptr));
ASSERT_EQ(versions_->current_next_file_number(), next + 3);
}
TEST_F(CompactionJobTest, DISABLED_SimpleCorrupted) {
NewDB();
@@ -1583,6 +1633,14 @@ TEST_F(CompactionJobTest, InputSerialization) {
rnd.Uniform(kStrMaxLen - 1) +
1)); // input file name should have at least one character
}
do {
input.input_blob_files.emplace_back(
rnd.RandomString(rnd.Uniform(kStrMaxLen - 1) + 1));
} while (!rnd.OneIn(10));
input.has_blob_gc_cutoff_file_number = true;
input.blob_gc_cutoff_file_number = rnd64.Uniform(UINT64_MAX);
input.blob_file_number_start = 123;
input.blob_file_number_limit = 456;
input.output_level = 4;
input.has_begin = rnd.OneIn(2);
if (input.has_begin) {
@@ -1592,6 +1650,12 @@ TEST_F(CompactionJobTest, InputSerialization) {
if (input.has_end) {
input.end = rnd.RandomBinaryString(rnd.Uniform(kStrMaxLen));
}
input.preserve_internal_time_seconds = rnd64.Uniform(UINT64_MAX);
input.preclude_last_level_data_seconds = rnd64.Uniform(UINT64_MAX);
input.preserve_time_min_seqno_floor = rnd64.Uniform(UINT64_MAX);
input.proximal_level = static_cast<int>(rnd.Uniform(8)) - 1;
input.proximal_output_range_type = static_cast<int>(rnd.Uniform(4));
input.keep_in_last_level_through_seqno = rnd64.Uniform(UINT64_MAX);
std::string output;
ASSERT_OK(input.Write(&output));
@@ -1600,6 +1664,7 @@ TEST_F(CompactionJobTest, InputSerialization) {
CompactionServiceInput deserialized1;
ASSERT_OK(CompactionServiceInput::Read(output, &deserialized1));
ASSERT_TRUE(deserialized1.TEST_Equals(&input));
ASSERT_EQ(deserialized1.input_blob_files, input.input_blob_files);
// Test mismatch
deserialized1.output_level += 10;
@@ -1616,6 +1681,54 @@ TEST_F(CompactionJobTest, InputSerialization) {
ASSERT_OK(CompactionServiceInput::Read(output, &deserialized2));
ASSERT_TRUE(deserialized2.TEST_Equals(&input));
std::string old_input = output;
auto remove_field = [](const std::string& field, std::string* serialized) {
const size_t pos = serialized->find(field + "=");
ASSERT_NE(pos, std::string::npos);
const size_t end = serialized->find(';', pos);
ASSERT_NE(end, std::string::npos);
serialized->erase(pos, end - pos + 1);
};
remove_field("input_blob_files", &old_input);
remove_field("has_blob_gc_cutoff_file_number", &old_input);
remove_field("blob_gc_cutoff_file_number", &old_input);
remove_field("blob_file_number_start", &old_input);
remove_field("blob_file_number_limit", &old_input);
CompactionServiceInput deserialized_old_input;
deserialized_old_input.input_blob_files.emplace_back("stale.blob");
deserialized_old_input.has_blob_gc_cutoff_file_number = true;
deserialized_old_input.blob_gc_cutoff_file_number = 123;
deserialized_old_input.blob_file_number_start = 1;
deserialized_old_input.blob_file_number_limit = 2;
ASSERT_OK(CompactionServiceInput::Read(old_input, &deserialized_old_input));
ASSERT_TRUE(deserialized_old_input.input_blob_files.empty());
ASSERT_FALSE(deserialized_old_input.has_blob_gc_cutoff_file_number);
ASSERT_EQ(deserialized_old_input.blob_gc_cutoff_file_number, 0);
ASSERT_EQ(deserialized_old_input.blob_file_number_start,
kInvalidBlobFileNumber);
ASSERT_EQ(deserialized_old_input.blob_file_number_limit,
kInvalidBlobFileNumber);
CompactionServiceInput invalid_range_input = input;
invalid_range_input.blob_file_number_start = 456;
invalid_range_input.blob_file_number_limit = 123;
std::string invalid_range;
ASSERT_OK(invalid_range_input.Write(&invalid_range));
CompactionServiceInput parsed_invalid_range;
ASSERT_TRUE(CompactionServiceInput::Read(invalid_range, &parsed_invalid_range)
.IsInvalidArgument());
// Test malformed hex in an encoded string field
const std::string cf_name_field = "cf_name=";
std::string malformed_hex;
ASSERT_OK(input.Write(&malformed_hex));
const size_t cf_name_pos = malformed_hex.find(cf_name_field);
ASSERT_NE(cf_name_pos, std::string::npos);
malformed_hex.insert(cf_name_pos + cf_name_field.size(), "Z");
CompactionServiceInput parsed_malformed_hex;
ASSERT_TRUE(CompactionServiceInput::Read(malformed_hex, &parsed_malformed_hex)
.IsInvalidArgument());
// Test missing field
CompactionServiceInput deserialized3;
deserialized3.output_level = 0;
@@ -1704,6 +1817,7 @@ TEST_F(CompactionJobTest, ResultSerialization) {
rnd.Uniform(kStrMaxLen)) /* smallest_internal_key */,
rnd.RandomBinaryString(
rnd.Uniform(kStrMaxLen)) /* largest_internal_key */,
rnd64.Uniform(UINT64_MAX - 1) + 1 /* oldest_blob_file_number */,
rnd64.Uniform(UINT64_MAX) /* oldest_ancestor_time */,
rnd64.Uniform(UINT64_MAX) /* file_creation_time */,
rnd64.Uniform(UINT64_MAX) /* epoch_number */,
@@ -1713,6 +1827,15 @@ TEST_F(CompactionJobTest, ResultSerialization) {
rnd.OneIn(2) /* marked_for_compaction */, id /* unique_id */, tp,
false /* is_proximal_level_output */, Temperature::kHot);
}
result.blob_file_additions.emplace_back(123, 2, 4096, "checksum",
rnd.RandomBinaryString(16));
result.blob_file_additions.emplace_back(0, 0, 0, "", "");
result.blob_file_additions.emplace_back();
result.blob_file_garbages.emplace_back(123, 1, 2048);
result.blob_file_garbages.emplace_back(0, 0, 0);
result.blob_file_garbages.emplace_back();
result.blob_compaction_version = kCompactionServiceBlobVersion;
result.blob_gc_cutoff_version = kCompactionServiceBlobGCCutoffVersion;
result.output_level = rnd.Uniform(10);
result.output_path = rnd.RandomString(rnd.Uniform(kStrMaxLen));
result.stats.num_output_records = rnd64.Uniform(UINT64_MAX);
@@ -1731,6 +1854,14 @@ TEST_F(CompactionJobTest, ResultSerialization) {
CompactionServiceResult deserialized1;
ASSERT_OK(CompactionServiceResult::Read(output, &deserialized1));
ASSERT_TRUE(deserialized1.TEST_Equals(&result));
ASSERT_EQ(deserialized1.blob_file_additions.size(), 3);
ASSERT_EQ(deserialized1.blob_file_additions[1].GetBlobFileNumber(), 0);
ASSERT_EQ(deserialized1.blob_file_additions[2].GetBlobFileNumber(),
kInvalidBlobFileNumber);
ASSERT_EQ(deserialized1.blob_file_garbages.size(), 3);
ASSERT_EQ(deserialized1.blob_file_garbages[1].GetBlobFileNumber(), 0);
ASSERT_EQ(deserialized1.blob_file_garbages[2].GetBlobFileNumber(),
kInvalidBlobFileNumber);
for (size_t i = 0; i < result.output_files.size(); i++) {
for (const auto& prop :
@@ -1778,11 +1909,44 @@ TEST_F(CompactionJobTest, ResultSerialization) {
ASSERT_OK(CompactionServiceResult::Read(output, &deserialized2));
ASSERT_TRUE(deserialized2.TEST_Equals(&result));
std::string old_result = output;
size_t pos = old_result.find("blob_file_additions=");
ASSERT_NE(pos, std::string::npos);
size_t end = old_result.find(';', pos);
ASSERT_NE(end, std::string::npos);
old_result.erase(pos, end - pos + 1);
pos = old_result.find("blob_file_garbages=");
ASSERT_NE(pos, std::string::npos);
end = old_result.find(';', pos);
ASSERT_NE(end, std::string::npos);
old_result.erase(pos, end - pos + 1);
pos = old_result.find("blob_compaction_version=");
ASSERT_NE(pos, std::string::npos);
end = old_result.find(';', pos);
ASSERT_NE(end, std::string::npos);
old_result.erase(pos, end - pos + 1);
pos = old_result.find("blob_gc_cutoff_version=");
ASSERT_NE(pos, std::string::npos);
end = old_result.find(';', pos);
ASSERT_NE(end, std::string::npos);
old_result.erase(pos, end - pos + 1);
CompactionServiceResult deserialized_old_result;
deserialized_old_result.blob_file_additions.emplace_back(321, 1, 1024, "",
"");
deserialized_old_result.blob_file_garbages.emplace_back(321, 1, 1024);
ASSERT_OK(
CompactionServiceResult::Read(old_result, &deserialized_old_result));
ASSERT_TRUE(deserialized_old_result.blob_file_additions.empty());
ASSERT_TRUE(deserialized_old_result.blob_file_garbages.empty());
ASSERT_EQ(deserialized_old_result.blob_compaction_version, 0);
ASSERT_EQ(deserialized_old_result.blob_gc_cutoff_version, 0);
deserialized_old_result.status.PermitUncheckedError();
// Test missing field
CompactionServiceResult deserialized3;
deserialized3.bytes_read = 0;
std::string to_remove = "bytes_read=123;";
size_t pos = output.find(to_remove);
pos = output.find(to_remove);
ASSERT_TRUE(pos != std::string::npos);
output.erase(pos, to_remove.length());
ASSERT_OK(CompactionServiceResult::Read(output, &deserialized3));
@@ -1811,6 +1975,102 @@ TEST_F(CompactionJobTest, ResultSerialization) {
}
}
TEST_F(CompactionJobTest, ResultSerializationMissingOrInvalidFields) {
// Each status field differs between the written status, the destination's
// earlier status, and `Status::OK()`, so every check below can tell which of
// the three a parsed field came from.
CompactionServiceResult result;
result.status = Status(Status::kIOError, Status::kNoSpace, Status::kHardError,
"disk full");
CompactionServiceOutputFile file;
file.file_name = "000123.sst";
file.marked_for_compaction = true;
file.is_proximal_level_output = true;
result.output_files.push_back(file);
std::string serialized;
ASSERT_OK(result.Write(&serialized));
const Status earlier(Status::kTimedOut, Status::kMutexTimeout,
Status::kSoftError, "earlier");
CompactionServiceResult parsed;
ASSERT_OK(CompactionServiceResult::Read(serialized, &parsed));
ASSERT_EQ(StatusFields(parsed.status), StatusFields(result.status));
ASSERT_EQ(parsed.output_files.size(), 1U);
ASSERT_TRUE(parsed.output_files[0].marked_for_compaction);
ASSERT_TRUE(parsed.output_files[0].is_proximal_level_output);
// A missing status field takes its value in `Status::OK()`, and the other
// fields keep their written values.
struct MissingFieldCase {
const char* field;
Status::Code code;
Status::SubCode subcode;
Status::Severity severity;
const char* message;
};
const MissingFieldCase missing_field_cases[] = {
{"code", Status::kOk, Status::kNoSpace, Status::kHardError, "disk full"},
{"subcode", Status::kIOError, Status::kNone, Status::kHardError,
"disk full"},
{"severity", Status::kIOError, Status::kNoSpace, Status::kNoError,
"disk full"},
{"message", Status::kIOError, Status::kNoSpace, Status::kHardError, ""},
};
for (const MissingFieldCase& c : missing_field_cases) {
SCOPED_TRACE(c.field);
std::string missing_field = serialized;
ASSERT_TRUE(ReplaceResultEntry(&missing_field, "status={", c.field, ""));
CompactionServiceResult parsed_missing_field;
parsed_missing_field.status = earlier;
ASSERT_OK(
CompactionServiceResult::Read(missing_field, &parsed_missing_field));
const Status expected(c.code, c.subcode, c.severity, c.message);
ASSERT_EQ(StatusFields(parsed_missing_field.status),
StatusFields(expected));
}
// A malformed or out-of-range status field fails the parse, and the
// destination keeps its earlier status.
struct InvalidFieldCase {
const char* field;
std::string entry;
};
const InvalidFieldCase invalid_field_cases[] = {
{"message", "message=Z;"},
{"code", "code=" + std::to_string(Status::kMaxCode) + ";"},
{"subcode", "subcode=" + std::to_string(Status::kMaxSubCode) + ";"},
{"severity", "severity=" + std::to_string(Status::kMaxSeverity) + ";"},
};
for (const InvalidFieldCase& c : invalid_field_cases) {
SCOPED_TRACE(c.entry);
std::string invalid_field = serialized;
ASSERT_TRUE(
ReplaceResultEntry(&invalid_field, "status={", c.field, c.entry));
CompactionServiceResult parsed_invalid_field;
parsed_invalid_field.status = earlier;
const Status s =
CompactionServiceResult::Read(invalid_field, &parsed_invalid_field);
ASSERT_EQ(s.code(), Status::kInvalidArgument) << s.ToString();
ASSERT_EQ(StatusFields(parsed_invalid_field.status), StatusFields(earlier));
}
// A missing output-file flag reads as false.
std::string missing_flags = serialized;
ASSERT_TRUE(ReplaceResultEntry(&missing_flags,
"output_files=", "marked_for_compaction", ""));
ASSERT_TRUE(ReplaceResultEntry(
&missing_flags, "output_files=", "is_proximal_level_output", ""));
CompactionServiceResult parsed_missing_flags;
ASSERT_OK(
CompactionServiceResult::Read(missing_flags, &parsed_missing_flags));
ASSERT_EQ(StatusFields(parsed_missing_flags.status),
StatusFields(result.status));
ASSERT_EQ(parsed_missing_flags.output_files.size(), 1U);
ASSERT_EQ(parsed_missing_flags.output_files[0].file_name, "000123.sst");
ASSERT_FALSE(parsed_missing_flags.output_files[0].marked_for_compaction);
ASSERT_FALSE(parsed_missing_flags.output_files[0].is_proximal_level_output);
}
TEST_F(CompactionJobTest, CutForMaxCompactionBytes) {
// dynamic_file_size option should have no impact on cutting for max
// compaction bytes.
+68 -37
View File
@@ -235,6 +235,32 @@ uint64_t CompactionOutputs::GetCurrentKeyGrandparentOverlappedBytes(
return overlapped_bytes;
}
bool CompactionOutputs::PartitionerRequiresCut(
const CompactionIterator& c_iter) const {
assert(HasBuilder());
if (!partitioner_) {
return false;
}
if (partitioner_prefix_valid_) {
// Fast path: no per-key copy or virtual call. A partition is requested at
// the first key that no longer shares the established prefix. An empty
// prefix means no further partitions (every key "starts with" it).
return UNLIKELY(!c_iter.user_key().starts_with(partitioner_prefix_));
}
// Slow path: per-key callback (and copy elsewhere)
return UNLIKELY(partitioner_->ShouldPartition(PartitionerRequest(
last_key_for_partitioner_, c_iter.user_key(),
current_output_file_size_)) == kRequired);
}
bool CompactionOutputs::LargestUserKeyEquals(const Slice& user_key) {
const InternalKey& largest = current_output().meta.largest;
return largest.size() > 0 &&
compaction_->column_family_data()
->user_comparator()
->EqualWithoutTimestamp(largest.user_key(), user_key);
}
bool CompactionOutputs::ShouldStopBefore(const CompactionIterator& c_iter) {
assert(c_iter.Valid());
const Slice& internal_key = c_iter.key();
@@ -270,22 +296,8 @@ bool CompactionOutputs::ShouldStopBefore(const CompactionIterator& c_iter) {
return true;
}
if (partitioner_) {
if (partitioner_prefix_valid_) {
// Fast path: no per-key copy or virtual call. A partition is requested at
// the first key that no longer shares the established prefix. An empty
// prefix means no further partitions (every key "starts with" it).
if (UNLIKELY(!c_iter.user_key().starts_with(partitioner_prefix_))) {
return true;
}
} else {
// Slow path: per-key callback (and copy elsewhere)
if (UNLIKELY(partitioner_->ShouldPartition(PartitionerRequest(
last_key_for_partitioner_, c_iter.user_key(),
current_output_file_size_)) == kRequired)) {
return true;
}
}
if (PartitionerRequiresCut(c_iter)) {
return true;
}
// files output to Level 0 won't be split
@@ -395,38 +407,56 @@ void CompactionOutputs::PrepareNewOutputForFirstKey(
}
Status CompactionOutputs::AddToOutput(
const CompactionIterator& c_iter,
const CompactionFileOpenFunc& open_file_func,
CompactionIterator& c_iter, const CompactionFileOpenFunc& open_file_func,
const CompactionFileCloseFunc& close_file_func,
const ParsedInternalKey& prev_iter_output_internal_key) {
Status s;
bool is_range_del = c_iter.IsDeleteRangeSentinelKey();
bool cut_before;
if (is_range_del && compaction_->bottommost_level()) {
// We don't consider range tombstone for bottommost level since:
// 1. there is no grandparent and hence no overlap to consider
// 2. range tombstone may be dropped at bottommost level.
return s;
// At the bottommost level, a range tombstone is not by itself a reason to
// cut: there are no grandparents to limit overlap with, and the tombstone
// might be dropped. However, a boundary required by the SstPartitioner
// must still be honored; otherwise AddRangeDels() would extend the
// current file across it. No boundary can fall inside one user key, and
// a point key there may have its seqno zeroed, so that it sorts after a
// tombstone starting at the same user key.
if (!HasBuilder() || LargestUserKeyEquals(c_iter.user_key()) ||
!PartitionerRequiresCut(c_iter)) {
return s;
}
cut_before = true;
} else {
cut_before = ShouldStopBefore(c_iter) && HasBuilder();
}
const Slice& key = c_iter.key();
if (ShouldStopBefore(c_iter) && HasBuilder()) {
if (cut_before) {
const Slice next_table_min_key = c_iter.key();
s = close_file_func(c_iter.InputStatus(), prev_iter_output_internal_key,
key, &c_iter, *this);
next_table_min_key, &c_iter,
/*post_prepare_retry=*/false, *this);
if (!s.ok()) {
return s;
}
// reset grandparent information
grandparent_boundary_switched_num_ = 0;
grandparent_overlapped_bytes_ =
GetCurrentKeyGrandparentOverlappedBytes(key);
GetCurrentKeyGrandparentOverlappedBytes(next_table_min_key);
if (UNLIKELY(is_range_del)) {
// lower bound for this new output file, this is needed as the lower bound
// does not come from the smallest point key in this case.
range_tombstone_lower_bound_.DecodeFrom(key);
range_tombstone_lower_bound_.DecodeFrom(next_table_min_key);
} else {
range_tombstone_lower_bound_.Clear();
}
}
c_iter.PrepareOutputIfNeeded();
if (!c_iter.status().ok() || !c_iter.Valid()) {
s = c_iter.status();
return s;
}
const Slice& key = c_iter.key();
// Open output file if necessary
if (!HasBuilder()) {
s = open_file_func(*this);
@@ -457,7 +487,7 @@ Status CompactionOutputs::AddToOutput(
}
TableBuilderAddContext add_context;
TableBuilderAddResult add_result =
const TableBuilderAddResult add_result =
builder_->TryAdd(key, value, &add_context);
if (add_result == TableBuilderAddResult::kError) {
@@ -479,6 +509,10 @@ Status CompactionOutputs::AddToOutput(
if (!s.ok()) {
return s;
}
const char* const new_table_reason =
add_context.new_table_reason == TableBuilderNewTableReason::kNone
? "Unspecified"
: TableBuilderNewTableReasonToString(add_context.new_table_reason);
const Compaction* compaction = compaction_;
if (compaction == nullptr) {
return Status::Corruption("Compaction output is missing compaction");
@@ -495,9 +529,8 @@ Status CompactionOutputs::AddToOutput(
if (compaction->output_level() == 0) {
const std::string detail =
"cf: " + cfd->GetName() + ", file_number: " +
std::to_string(current_output().meta.fd.GetNumber()) + ", reason: " +
TableBuilderNewTableReasonToString(add_context.new_table_reason) +
", key: " + key_for_diagnostics;
std::to_string(current_output().meta.fd.GetNumber()) +
", reason: " + new_table_reason + ", key: " + key_for_diagnostics;
return Status::Corruption(
"TableBuilder requested a new table while producing level 0 output",
detail);
@@ -505,9 +538,8 @@ Status CompactionOutputs::AddToOutput(
if (retried_after_new_table || builder_->IsEmpty()) {
const std::string detail =
"cf: " + cfd->GetName() + ", file_number: " +
std::to_string(current_output().meta.fd.GetNumber()) + ", reason: " +
TableBuilderNewTableReasonToString(add_context.new_table_reason) +
", key: " + key_for_diagnostics;
std::to_string(current_output().meta.fd.GetNumber()) +
", reason: " + new_table_reason + ", key: " + key_for_diagnostics;
return Status::Corruption(
"TableBuilder requested a new table without accepting any rows",
detail);
@@ -518,11 +550,10 @@ Status CompactionOutputs::AddToOutput(
"[%s] Compaction output table builder requested a new table before "
"key %s for file #%" PRIu64 ". reason: %s",
cfd->GetName().c_str(), key_for_diagnostics.c_str(),
current_output().meta.fd.GetNumber(),
TableBuilderNewTableReasonToString(add_context.new_table_reason));
current_output().meta.fd.GetNumber(), new_table_reason);
s = close_file_func(c_iter.InputStatus(), prev_iter_output_internal_key,
key, &c_iter, *this);
key, &c_iter, /*post_prepare_retry=*/true, *this);
if (!s.ok()) {
return s;
}
+13 -3
View File
@@ -22,7 +22,7 @@ class CompactionOutputs;
using CompactionFileOpenFunc = std::function<Status(CompactionOutputs&)>;
using CompactionFileCloseFunc =
std::function<Status(const Status&, const ParsedInternalKey&, const Slice&,
const CompactionIterator*, CompactionOutputs&)>;
const CompactionIterator*, bool, CompactionOutputs&)>;
// Files produced by subcompaction, most of the functions are used by
// compaction_job Open/Close compaction file functions.
@@ -245,6 +245,15 @@ class CompactionOutputs {
// before processing the current key in compaction iterator.
bool ShouldStopBefore(const CompactionIterator& c_iter);
// Returns true iff the SST partitioner requires a new output file to start
// at the current key. Only meaningful while an output file is open: the
// partitioner state describes the current file.
bool PartitionerRequiresCut(const CompactionIterator& c_iter) const;
// Returns true iff the current output file has a largest key and its user
// key equals `user_key`, ignoring timestamps.
bool LargestUserKeyEquals(const Slice& user_key);
// Re-establishes per-file state that depends on the first key accepted by a
// freshly opened builder. This keeps caller-requested file cuts equivalent to
// RocksDB's built-in file-boundary decisions for partition tracking.
@@ -278,7 +287,7 @@ class CompactionOutputs {
// Add current key from compaction_iterator to the output file. If needed
// close and open new compaction output with the functions provided.
Status AddToOutput(const CompactionIterator& c_iter,
Status AddToOutput(CompactionIterator& c_iter,
const CompactionFileOpenFunc& open_file_func,
const CompactionFileCloseFunc& close_file_func,
const ParsedInternalKey& prev_iter_output_internal_key);
@@ -302,7 +311,8 @@ class CompactionOutputs {
const ParsedInternalKey empty_internal_key{};
const Slice empty_key{};
Status s = close_file_func(status, empty_internal_key, empty_key,
nullptr /* c_iter */, *this);
nullptr /* c_iter */,
/*post_prepare_retry=*/false, *this);
if (!s.ok() && status.ok()) {
status = s;
}
+8 -4
View File
@@ -764,6 +764,7 @@ Compaction* CompactionPicker::PickCompactionForCompactRange(
compact_range_options.blob_garbage_collection_policy,
compact_range_options.blob_garbage_collection_age_cutoff);
c->SetSkipMissingBlobFiles(compact_range_options.skip_missing_blob_files);
RegisterCompaction(c);
vstorage->ComputeCompactionScore(ioptions_, mutable_cf_options,
full_history_ts_low);
@@ -837,7 +838,8 @@ Compaction* CompactionPicker::PickCompactionForCompactRange(
// for BOTTOM LEVEL compaction only, use max_file_num_to_ignore to filter out
// files that are created during the current compaction.
if ((compact_range_options.bottommost_level_compaction ==
if ((compact_range_options.skip_missing_blob_files ||
compact_range_options.bottommost_level_compaction ==
BottommostLevelCompaction::kForceOptimized ||
compact_range_options.bottommost_level_compaction ==
BottommostLevelCompaction::kIfHaveCompactionFilter) &&
@@ -860,9 +862,6 @@ Compaction* CompactionPicker::PickCompactionForCompactRange(
if (inputs_shrunk.empty()) {
return nullptr;
}
if (inputs.size() != inputs_shrunk.size()) {
inputs.files.swap(inputs_shrunk);
}
// set covering_the_whole_range to false if there is any file that need to
// be compacted in the range of inputs[skip_input_index+1, inputs.size())
for (size_t i = skip_input_index + 1; i < inputs.size(); ++i) {
@@ -870,6 +869,9 @@ Compaction* CompactionPicker::PickCompactionForCompactRange(
covering_the_whole_range = false;
}
}
if (inputs.size() != inputs_shrunk.size()) {
inputs.files.swap(inputs_shrunk);
}
}
InternalKey key_storage;
@@ -956,6 +958,8 @@ Compaction* CompactionPicker::PickCompactionForCompactRange(
compact_range_options.blob_garbage_collection_policy,
compact_range_options.blob_garbage_collection_age_cutoff);
compaction->SetSkipMissingBlobFiles(
compact_range_options.skip_missing_blob_files);
TEST_SYNC_POINT_CALLBACK("CompactionPicker::CompactRange:Return", compaction);
RegisterCompaction(compaction);
+7
View File
@@ -110,6 +110,13 @@ Compaction* FIFOCompactionPicker::PickTTLCompaction(
uint64_t est_newest_key_time = newest_key_time == kUnknownNewestKeyTime
? oldest_ancestor_time
: newest_key_time;
if (est_newest_key_time == kUnknownNewestKeyTime) {
// E.g. a file written by SstFileWriter. Every key in the file was
// written no later than the file's contents were built.
est_newest_key_time = f->TryGetFileCreationTime();
}
// CheckExternalFileNotTooOld() (reject_too_old_file) mirrors this
// estimate
if (est_newest_key_time == kUnknownNewestKeyTime ||
est_newest_key_time >= (current_time - mutable_cf_options.ttl)) {
break;
+1 -1
View File
@@ -13,7 +13,7 @@
namespace ROCKSDB_NAMESPACE {
// Picking compactions for leveled compaction. See wiki page
// https://github.com/facebook/rocksdb/wiki/Leveled-Compaction
// https://github.com/meta/rocksdb/wiki/Leveled-Compaction
// for description of Leveled compaction.
class LevelCompactionPicker : public CompactionPicker {
public:
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+17 -1
View File
@@ -106,7 +106,7 @@ Slice SubcompactionState::LargestUserKey() const {
}
Status SubcompactionState::AddToOutput(
const CompactionIterator& iter, bool use_proximal_output,
CompactionIterator& iter, bool use_proximal_output,
const CompactionFileOpenFunc& open_file_func,
const CompactionFileCloseFunc& close_file_func,
const ParsedInternalKey& prev_iter_output_internal_key) {
@@ -117,4 +117,20 @@ Status SubcompactionState::AddToOutput(
prev_iter_output_internal_key);
}
Status SubcompactionState::UpdateBlobGarbageCheckpoint(
const BlobGarbageMeter* next_key_inflow) {
const BlobGarbageMeter* const meter =
compaction_outputs_.GetBlobGarbageMeter();
if (meter == nullptr) {
blob_garbage_checkpoint_.reset();
return Status::OK();
}
blob_garbage_checkpoint_ = std::make_unique<BlobGarbageMeter>(*meter);
if (next_key_inflow == nullptr || next_key_inflow->flows().empty()) {
return Status::OK();
}
blob_garbage_checkpoint_->ProcessOutFlow(*next_key_inflow);
return Status::OK();
}
} // namespace ROCKSDB_NAMESPACE
+9 -2
View File
@@ -163,7 +163,8 @@ class SubcompactionState {
sub_job_id(state.sub_job_id),
compaction_outputs_(std::move(state.compaction_outputs_)),
proximal_level_outputs_(std::move(state.proximal_level_outputs_)),
range_del_agg_(std::move(state.range_del_agg_)) {
range_del_agg_(std::move(state.range_del_agg_)),
blob_garbage_checkpoint_(std::move(state.blob_garbage_checkpoint_)) {
current_outputs_ = state.current_outputs_ == &state.proximal_level_outputs_
? &proximal_level_outputs_
: &compaction_outputs_;
@@ -238,11 +239,16 @@ class SubcompactionState {
}
// Add compaction_iterator key/value to the `Current` output group.
Status AddToOutput(const CompactionIterator& iter, bool use_proximal_output,
Status AddToOutput(CompactionIterator& iter, bool use_proximal_output,
const CompactionFileOpenFunc& open_file_func,
const CompactionFileCloseFunc& close_file_func,
const ParsedInternalKey& prev_iter_output_internal_key);
Status UpdateBlobGarbageCheckpoint(const BlobGarbageMeter* next_key_inflow);
const BlobGarbageMeter* GetBlobGarbageCheckpoint() const {
return blob_garbage_checkpoint_.get();
}
// Close all compaction output files, both output_to_proximal_level outputs
// and normal outputs.
Status CloseCompactionFiles(const Status& curr_status,
@@ -271,6 +277,7 @@ class SubcompactionState {
CompactionOutputs proximal_level_outputs_;
CompactionOutputs* current_outputs_ = &compaction_outputs_;
std::unique_ptr<CompactionRangeDelAggregator> range_del_agg_;
std::unique_ptr<BlobGarbageMeter> blob_garbage_checkpoint_;
SubcompactionProgress subcompaction_progress_;
};
+15 -9
View File
@@ -843,19 +843,20 @@ TEST_F(TieredCompactionTest, LevelOutofBoundaryRangeDelete) {
ASSERT_OK(Flush());
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
ASSERT_EQ("0,2,10",
FilesPerLevel()); // one file is at the proximal level
// which only contains a range delete
ASSERT_EQ("0,3,10",
FilesPerLevel()); // the partitioner cuts before and after the
// range delete, so it has a file of its own
// at the proximal level
std::vector<LiveFileMetaData> live_file_meta;
db_->GetLiveFilesMetaData(&live_file_meta);
bool found_sst_with_del = false;
uint64_t sst_with_del_num = 0;
for (const auto& meta : live_file_meta) {
if (meta.num_deletions > 0) {
// found SST with del, which has 2 entries, one for data one for range del
// found SST with del, which only contains the range del
ASSERT_EQ(meta.level,
kNumLevels - 2); // output to proximal level
ASSERT_EQ(meta.num_entries, 2);
ASSERT_EQ(meta.num_entries, 1);
ASSERT_EQ(meta.num_deletions, 1);
found_sst_with_del = true;
sst_with_del_num = meta.file_number;
@@ -974,7 +975,9 @@ TEST_F(TieredCompactionTest, UniversalRangeDelete) {
ASSERT_OK(Flush());
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
ASSERT_EQ("0,0,0,0,0,2,8", FilesPerLevel());
// the partitioner cuts before and after the range del, so it has a file of
// its own at the proximal level
ASSERT_EQ("0,0,0,0,0,3,8", FilesPerLevel());
// find the SST with range del
std::vector<LiveFileMetaData> live_file_meta;
db_->GetLiveFilesMetaData(&live_file_meta);
@@ -982,10 +985,10 @@ TEST_F(TieredCompactionTest, UniversalRangeDelete) {
uint64_t sst_with_del_num = 0;
for (const auto& meta : live_file_meta) {
if (meta.num_deletions > 0) {
// found SST with del, which has 2 entries, one for data one for range del
// found SST with del, which only contains the range del
ASSERT_EQ(meta.level,
kNumLevels - 2); // output_to_proximal_level level
ASSERT_EQ(meta.num_entries, 2);
ASSERT_EQ(meta.num_entries, 1);
ASSERT_EQ(meta.num_deletions, 1);
found_sst_with_del = true;
sst_with_del_num = meta.file_number;
@@ -1313,6 +1316,9 @@ TEST_F(TieredCompactionTest, SequenceBasedTieredStorageLevel) {
options.statistics->getTickerCount(COMPACTION_RANGE_DEL_DROP_OBSOLETE),
0);
// Keep bottommost cleanup from erasing the sequence metadata used by the
// following manual-compaction placement assertions.
ASSERT_OK(dbfull()->SetOptions({{"disable_auto_compactions", "true"}}));
db_->ReleaseSnapshot(snap);
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
@@ -2109,7 +2115,7 @@ class PrecludeWithCompactStyleTest : public PrecludeLastLevelTestBase,
TEST_P(PrecludeWithCompactStyleTest, RangeTombstoneSnapshotMigrateFromLast) {
// Reproducer for issue originally described in
// https://github.com/facebook/rocksdb/pull/9964/files#r1024449523
// https://github.com/meta/rocksdb/pull/9964/files#r1024449523
const bool universal = GetParam();
const int kNumLevels = 7;
auto options = CurrentOptions();
+29 -16
View File
@@ -14,6 +14,7 @@
#include <vector>
#include "folly/Executor.h"
#include "folly/coro/CurrentExecutor.h"
#include "folly/coro/Nothrow.h"
#include "folly/executors/IOExecutor.h"
#include "folly/io/async/EventBase.h"
@@ -41,8 +42,8 @@ folly::coro::Task<Status> CoroDB::CoGet(DB* db, const ReadOptions& options,
Status status;
if (coro_db == nullptr || read_event_base == nullptr) {
CoroutineStatsContextScope stats_scope(std::move(stats_config),
db->GetEnv());
CoroutineStatsContextScope stats_scope(
WithoutAsyncReadCPUTime(std::move(stats_config)), db->GetEnv());
status = db->Get(options, column_family, key, value, timestamp);
} else {
auto task = [](CoroutineStatsConfig task_stats_config, Env* task_env,
@@ -57,8 +58,14 @@ folly::coro::Task<Status> CoroDB::CoGet(DB* db, const ReadOptions& options,
task_value, task_timestamp));
}(std::move(stats_config), db->GetEnv(), coro_db, options, column_family,
key, value, timestamp);
status = co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
folly::Executor::getKeepAliveToken(read_event_base), std::move(task)));
auto* current_executor = co_await folly::coro::co_current_executor;
if (current_executor == read_event_base) {
status = co_await folly::coro::co_nothrow(std::move(task));
} else {
status = co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
folly::Executor::getKeepAliveToken(read_event_base),
std::move(task)));
}
}
co_return status;
}
@@ -104,8 +111,8 @@ folly::coro::Task<Status> CoroDB::CoGet(SstFileReader* reader,
Status status;
if (read_event_base == nullptr) {
CoroutineStatsContextScope stats_scope(std::move(stats_config),
rep->options.env);
CoroutineStatsContextScope stats_scope(
WithoutAsyncReadCPUTime(std::move(stats_config)), rep->options.env);
status = reader->Get(options, key, value);
} else {
auto task = [](CoroutineStatsConfig task_stats_config, Env* task_env,
@@ -117,8 +124,14 @@ folly::coro::Task<Status> CoroDB::CoGet(SstFileReader* reader,
co_return co_await folly::coro::co_nothrow(
task_reader->rep_->GetCoroutine(task_options, task_key, task_value));
}(std::move(stats_config), rep->options.env, reader, options, key, value);
status = co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
folly::Executor::getKeepAliveToken(read_event_base), std::move(task)));
auto* current_executor = co_await folly::coro::co_current_executor;
if (current_executor == read_event_base) {
status = co_await folly::coro::co_nothrow(std::move(task));
} else {
status = co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
folly::Executor::getKeepAliveToken(read_event_base),
std::move(task)));
}
}
co_return status;
}
@@ -153,8 +166,8 @@ folly::coro::Task<Status> CoroDB::CoGetEntity(DB* db,
Status status;
if (coro_db == nullptr || read_event_base == nullptr) {
CoroutineStatsContextScope stats_scope(std::move(stats_config),
db->GetEnv());
CoroutineStatsContextScope stats_scope(
WithoutAsyncReadCPUTime(std::move(stats_config)), db->GetEnv());
status = db->GetEntity(options, column_family, key, columns);
} else {
auto task =
@@ -197,8 +210,8 @@ folly::coro::Task<Status> CoroDB::CoGetEntity(DB* db,
Status status;
if (coro_db == nullptr || read_event_base == nullptr) {
CoroutineStatsContextScope stats_scope(std::move(stats_config),
db->GetEnv());
CoroutineStatsContextScope stats_scope(
WithoutAsyncReadCPUTime(std::move(stats_config)), db->GetEnv());
status = db->GetEntity(options, key, result);
} else {
auto task =
@@ -265,8 +278,8 @@ folly::coro::Task<std::vector<Status>> CoroDB::CoMultiGet(
std::vector<Status> statuses;
if (read_event_base == nullptr) {
CoroutineStatsContextScope stats_scope(std::move(stats_config),
rep->options.env);
CoroutineStatsContextScope stats_scope(
WithoutAsyncReadCPUTime(std::move(stats_config)), rep->options.env);
statuses = reader->MultiGet(options, keys, values);
} else {
auto task = [](CoroutineStatsConfig task_stats_config, Env* task_env,
@@ -316,8 +329,8 @@ folly::coro::Task<void> CoroDB::CoMultiGet(
CoroutineStatsConfig stats_config = CaptureAndDisableCoroutineStatsConfig();
if (coro_db == nullptr || read_event_base == nullptr) {
CoroutineStatsContextScope stats_scope(std::move(stats_config),
db->GetEnv());
CoroutineStatsContextScope stats_scope(
WithoutAsyncReadCPUTime(std::move(stats_config)), db->GetEnv());
db->MultiGet(options, num_keys, column_families, keys, values, timestamps,
statuses, sorted_input);
} else {
+80 -17
View File
@@ -1560,6 +1560,44 @@ TEST_F(DBBasicTest, CompactedDB) {
ASSERT_FALSE((*multiget_output_metadata.newer_version_present)[0]);
ASSERT_TRUE(metadata_statuses[1].IsNotFound());
ASSERT_FALSE((*multiget_output_metadata.newer_version_present)[1]);
{
const std::string expected_aaa_value = DummyString(kFileSize / 2, 'a');
const WideColumns expected_aaa_columns{
{kDefaultWideColumnName, expected_aaa_value}};
PinnableWideColumns get_entity_result;
OutputMetadata get_entity_output_metadata = NewerVersionOutputMetadata();
ASSERT_OK(db_->GetEntityWithMetadata(metadata_read_options, "aaa",
&get_entity_result,
&get_entity_output_metadata));
ASSERT_EQ(get_entity_result.columns(), expected_aaa_columns);
ASSERT_FALSE(*get_entity_output_metadata.newer_version_present);
std::array<PinnableWideColumns, 2> entity_results;
std::array<Status, 2> entity_statuses;
MultiGetOutputMetadata entity_output_metadata =
NewerVersionMultiGetOutputMetadata();
db_->MultiGetEntityWithMetadata(
metadata_read_options, db_->DefaultColumnFamily(), metadata_keys.size(),
metadata_keys.data(), entity_results.data(), entity_statuses.data(),
&entity_output_metadata);
ASSERT_OK(entity_statuses[0]);
ASSERT_EQ(entity_results[0].columns(), expected_aaa_columns);
ASSERT_TRUE(entity_statuses[1].IsNotFound());
ASSERT_EQ(*entity_output_metadata.newer_version_present,
(std::vector<uint8_t>{0, 0}));
std::array<PinnableAttributeGroups, 2> attribute_group_results;
attribute_group_results[0].emplace_back(db_->DefaultColumnFamily());
attribute_group_results[1].emplace_back(db_->DefaultColumnFamily());
db_->MultiGetEntityWithMetadata(
metadata_read_options, metadata_keys.size(), metadata_keys.data(),
attribute_group_results.data(), &entity_output_metadata);
ASSERT_OK(attribute_group_results[0][0].status());
ASSERT_EQ(attribute_group_results[0][0].columns(), expected_aaa_columns);
ASSERT_TRUE(attribute_group_results[1][0].status().IsNotFound());
ASSERT_EQ(*entity_output_metadata.newer_version_present,
(std::vector<uint8_t>{0, 0}));
}
db_->ReleaseSnapshot(metadata_snapshot);
ASSERT_TRUE(db_->GetWithMetadata(ReadOptions(), db_->DefaultColumnFamily(),
@@ -2176,6 +2214,35 @@ TEST_F(DBBasicTest, ReadOnlyNewerVersionPresentReturnsFalse) {
ASSERT_EQ(1, multiget_output_metadata.newer_version_present->size());
ASSERT_FALSE((*multiget_output_metadata.newer_version_present)[0]);
const WideColumns expected_columns{{kDefaultWideColumnName, "value"}};
PinnableWideColumns entity_result;
OutputMetadata get_entity_output_metadata = NewerVersionOutputMetadata();
ASSERT_OK(db_->GetEntityWithMetadata(read_options, handles_[1], "key",
&entity_result,
&get_entity_output_metadata));
ASSERT_EQ(entity_result.columns(), expected_columns);
ASSERT_FALSE(*get_entity_output_metadata.newer_version_present);
Status entity_status;
MultiGetOutputMetadata entity_output_metadata =
NewerVersionMultiGetOutputMetadata();
db_->MultiGetEntityWithMetadata(read_options, handles_[1], keys.size(),
keys.data(), &entity_result, &entity_status,
&entity_output_metadata);
ASSERT_OK(entity_status);
ASSERT_EQ(entity_result.columns(), expected_columns);
ASSERT_EQ(*entity_output_metadata.newer_version_present,
std::vector<uint8_t>{0});
PinnableAttributeGroups attribute_groups;
attribute_groups.emplace_back(handles_[1]);
db_->MultiGetEntityWithMetadata(read_options, keys.size(), keys.data(),
&attribute_groups, &entity_output_metadata);
ASSERT_OK(attribute_groups[0].status());
ASSERT_EQ(attribute_groups[0].columns(), expected_columns);
ASSERT_EQ(*entity_output_metadata.newer_version_present,
std::vector<uint8_t>{0});
db_->ReleaseSnapshot(snapshot);
}
@@ -4609,12 +4676,13 @@ INSTANTIATE_TEST_CASE_P(DBMultiGetTestWithParam, DBMultiGetTestWithParam,
#if USE_COROUTINES
class DBMultiGetAsyncIOTest
: public DBBasicTest,
public ::testing::WithParamInterface<std::tuple<bool, bool>> {
public ::testing::WithParamInterface<std::tuple<bool, bool, bool>> {
public:
DBMultiGetAsyncIOTest()
: DBBasicTest(),
optimize_multiget_for_io_(std::get<0>(GetParam())),
use_coroutine_(std::get<1>(GetParam())),
use_direct_io_(std::get<2>(GetParam())),
statistics_(ROCKSDB_NAMESPACE::CreateDBStatistics()) {
BlockBasedTableOptions bbto;
bbto.filter_policy.reset(NewBloomFilterPolicy(10));
@@ -4623,6 +4691,7 @@ class DBMultiGetAsyncIOTest
options_.statistics = statistics_;
options_.table_factory.reset(NewBlockBasedTableFactory(bbto));
options_.env = Env::Default();
options_.use_direct_reads = use_direct_io_ && IsDirectIOSupported();
Reopen(options_);
int num_keys = 0;
@@ -4686,6 +4755,12 @@ class DBMultiGetAsyncIOTest
EXPECT_EQ(0, num_keys);
}
void SetUp() override {
if (use_direct_io_ && !options_.use_direct_reads) {
ROCKSDB_GTEST_SKIP("Direct IO is not supported");
}
}
const std::shared_ptr<Statistics>& statistics() { return statistics_; }
protected:
@@ -4720,6 +4795,7 @@ class DBMultiGetAsyncIOTest
const bool optimize_multiget_for_io_;
const bool use_coroutine_;
const bool use_direct_io_;
private:
std::shared_ptr<Statistics> statistics_;
@@ -5169,7 +5245,8 @@ TEST_P(DBMultiGetAsyncIOTest, MixedReadAsyncFallbackPropagatesPollError) {
#endif // ROCKSDB_IOURING_PRESENT
INSTANTIATE_TEST_CASE_P(DBMultiGetAsyncIOTest, DBMultiGetAsyncIOTest,
testing::Combine(testing::Bool(), testing::Bool()));
testing::Combine(testing::Bool(), testing::Bool(),
testing::Bool()));
#endif // USE_COROUTINES
TEST_F(DBBasicTest, MultiGetStats) {
@@ -6216,7 +6293,7 @@ class DBBasicTestTrackWal : public DBTestBase,
VectorLogPtr log_files;
EXPECT_OK(dbfull()->GetSortedWalFiles(log_files));
return static_cast<int>(log_files.size());
};
}
};
TEST_P(DBBasicTestTrackWal, DoNotTrackObsoleteWal) {
@@ -6586,7 +6663,6 @@ TEST_P(DBBasicTestWithParallelIO, MultiGet) {
TEST_P(DBBasicTestWithParallelIO, MultiGetDirectIO) {
class FakeDirectIOEnv : public EnvWrapper {
class FakeDirectIOSequentialFile;
class FakeDirectIORandomAccessFile;
public:
@@ -6610,19 +6686,6 @@ TEST_P(DBBasicTestWithParallelIO, MultiGetDirectIO) {
}
private:
class FakeDirectIOSequentialFile : public SequentialFileWrapper {
public:
FakeDirectIOSequentialFile(std::unique_ptr<SequentialFile>&& file)
: SequentialFileWrapper(file.get()), file_(std::move(file)) {}
~FakeDirectIOSequentialFile() override {}
bool use_direct_io() const override { return true; }
size_t GetRequiredBufferAlignment() const override { return 1; }
private:
std::unique_ptr<SequentialFile> file_;
};
class FakeDirectIORandomAccessFile : public RandomAccessFileWrapper {
public:
FakeDirectIORandomAccessFile(std::unique_ptr<RandomAccessFile>&& file)
-48
View File
@@ -20,7 +20,6 @@
#include "db/db_test_util.h"
#include "env/unique_id_gen.h"
#include "port/stack_trace.h"
#include "rocksdb/persistent_cache.h"
#include "rocksdb/statistics.h"
#include "rocksdb/table.h"
#include "rocksdb/table_properties.h"
@@ -240,53 +239,6 @@ TEST_F(DBBlockCacheTest, TestWithoutCompressedBlockCache) {
#ifdef SNAPPY
namespace {
class PersistentCacheFromCache : public PersistentCache {
public:
PersistentCacheFromCache(std::shared_ptr<Cache> cache, bool read_only)
: cache_(cache), read_only_(read_only) {}
Status Insert(const Slice& key, const char* data,
const size_t size) override {
if (read_only_) {
return Status::NotSupported();
}
std::unique_ptr<char[]> copy{new char[size]};
std::copy_n(data, size, copy.get());
Status s = cache_.Insert(key, copy.get(), size);
if (s.ok()) {
copy.release();
}
return s;
}
Status Lookup(const Slice& key, std::unique_ptr<char[]>* data,
size_t* size) override {
auto handle = cache_.Lookup(key);
if (handle) {
char* ptr = cache_.Value(handle);
*size = cache_.get()->GetCharge(handle);
data->reset(new char[*size]);
std::copy_n(ptr, *size, data->get());
cache_.Release(handle);
return Status::OK();
} else {
return Status::NotFound();
}
}
bool IsCompressed() override { return false; }
StatsType Stats() override { return StatsType(); }
std::string GetPrintableOptions() const override { return ""; }
uint64_t NewId() override { return cache_.get()->NewId(); }
private:
BasicTypedSharedCacheInterface<char[], CacheEntryRole::kMisc> cache_;
bool read_only_;
};
class ReadOnlyCacheWrapper : public CacheWrapper {
public:
using CacheWrapper::CacheWrapper;
+37
View File
@@ -928,6 +928,43 @@ INSTANTIATE_TEST_CASE_P(
BlockBasedTableOptions::kLatestSupportedFormatVersion)));
#endif // !defined(ROCKSDB_VALGRIND_RUN) || defined(ROCKSDB_FULL_VALGRIND_RUN)
TEST_F(DBBloomFilterTest, PartitionedPrefixFilterMultiGetKeysOutOfDomain) {
for (bool decouple : {false, true}) {
SCOPED_TRACE("decouple_partitioned_filters=" + std::to_string(decouple));
Options options = CurrentOptions();
options.prefix_extractor.reset(NewFixedPrefixTransform(4));
BlockBasedTableOptions bbto;
bbto.filter_policy.reset(NewBloomFilterPolicy(10));
bbto.whole_key_filtering = false;
bbto.partition_filters = true;
bbto.decouple_partitioned_filters = decouple;
bbto.index_type = BlockBasedTableOptions::kTwoLevelIndexSearch;
bbto.metadata_block_size = 1;
bbto.block_size = 1;
options.table_factory.reset(NewBlockBasedTableFactory(bbto));
DestroyAndReopen(options);
std::vector<std::string> keys;
std::vector<std::string> expected;
for (char c = 'a'; c <= 'j'; ++c) {
keys.emplace_back(1, c);
expected.push_back("v_" + keys.back());
ASSERT_OK(Put(keys.back(), expected.back()));
}
ASSERT_OK(Put("kkkk1", "v_kkkk1"));
ASSERT_OK(Flush());
keys.emplace_back("aaaa");
expected.emplace_back("NOT_FOUND");
keys.emplace_back("kkkk1");
expected.emplace_back("v_kkkk1");
for (size_t i = 0; i < keys.size(); ++i) {
ASSERT_EQ(expected[i], Get(keys[i]));
}
ASSERT_EQ(expected, MultiGet(keys));
}
}
TEST_F(DBBloomFilterTest, BloomFilterRate) {
while (ChangeFilterOptions()) {
Options options = CurrentOptions();
+44 -1
View File
@@ -195,6 +195,49 @@ TEST_F(DBClipTest, TestClipRange) {
}
}
TEST_F(DBClipTest, SingleEmptyUserKey) {
struct TestCase {
const Comparator* comparator;
const char* begin_key;
const char* end_key;
bool keep_empty_key;
};
const TestCase cases[] = {
{BytewiseComparator(), "b", "d", false},
{BytewiseComparator(), "", "z", true},
{ReverseBytewiseComparator(), "d", "", false},
};
for (const TestCase& test_case : cases) {
for (int level : {0, 1}) {
SCOPED_TRACE(test_case.comparator->Name());
SCOPED_TRACE(test_case.begin_key);
SCOPED_TRACE(level);
Options options = CurrentOptions();
options.comparator = test_case.comparator;
options.disable_auto_compactions = true;
DestroyAndReopen(options);
ASSERT_OK(db_->ClipColumnFamily(db_->DefaultColumnFamily(),
test_case.begin_key, test_case.end_key));
ASSERT_EQ("NOT_FOUND", Get(""));
ASSERT_OK(Put("", "empty-key-value"));
ASSERT_OK(Flush());
if (level != 0) {
MoveFilesToLevel(level);
}
ASSERT_EQ(1, NumTableFilesAtLevel(level));
ASSERT_OK(db_->ClipColumnFamily(db_->DefaultColumnFamily(),
test_case.begin_key, test_case.end_key));
const std::string expected =
test_case.keep_empty_key ? "empty-key-value" : "NOT_FOUND";
ASSERT_EQ(expected, Get(""));
Reopen(options);
ASSERT_EQ(expected, Get(""));
}
}
}
TEST_F(DBClipTest, KeepFileEndingAtBeginKey) {
Options options = CurrentOptions();
options.disable_auto_compactions = true;
@@ -222,4 +265,4 @@ int main(int argc, char** argv) {
ROCKSDB_NAMESPACE::port::InstallStackTraceHandler();
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
}
+4 -1
View File
@@ -188,8 +188,11 @@ class SyncPointAbortHelper {
// Clean up sync points and wait for abort completion in one call
void CleanupAndWait() {
CleanupSyncPoints();
// We hold no DB mutex, and the wait releases abort_mutex_. Keeping sync
// points enabled lets the worker observe the abort and exit, so the abort
// thread can finish before cleanup.
WaitForAbortCompletion();
CleanupSyncPoints();
}
private:
+571
View File
@@ -8,7 +8,12 @@
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include "db/db_test_util.h"
#include "options/options_helper.h"
#include "port/stack_trace.h"
#include "rocksdb/utilities/transaction_db.h"
#include "test_util/merge_filter_test_util.h"
#include "util/defer.h"
#include "utilities/fault_injection_env.h"
namespace ROCKSDB_NAMESPACE {
@@ -23,6 +28,17 @@ class DBTestCompactionFilter : public DBTestBase {
public:
DBTestCompactionFilter()
: DBTestBase("db_compaction_filter_test", /*env_do_fsync=*/true) {}
Status CompactAllMergeFilterFiles() {
std::vector<LiveFileMetaData> files;
db_->GetLiveFilesMetaData(&files);
std::vector<std::string> names;
names.reserve(files.size());
for (const auto& file : files) {
names.push_back(file.relative_filename);
}
return db_->CompactFiles(CompactionOptions(), names, 1);
}
};
// Param variant of DBTestBase::ChangeCompactOptions
@@ -1048,6 +1064,561 @@ TEST_F(DBTestCompactionFilter, DropKeyWithSingleDelete) {
}
}
TEST_F(DBTestCompactionFilter, MergeBeforeFilterModeDeclarations) {
using Order = CompactionFilter::MergeOrder;
test::MergeFilterTrace trace;
test::RecordingMergeFilter legacy(Order::kFilterBeforeMerge, &trace);
test::RecordingMergeFilter post(Order::kMergeBeforeFilter, &trace);
test::RecordingMergeFilterFactory legacy_factory(Order::kFilterBeforeMerge,
&trace);
test::RecordingMergeFilterFactory post_factory(Order::kMergeBeforeFilter,
&trace);
for (int i = 0; i < 3; ++i) {
ASSERT_EQ(Order::kFilterBeforeMerge, legacy.GetMergeOrder());
ASSERT_EQ(Order::kMergeBeforeFilter, post.GetMergeOrder());
ASSERT_EQ(Order::kMergeBeforeFilter, post_factory.GetMergeOrder());
}
Order order;
ASSERT_OK(GetCompactionFilterMergeOrder(
nullptr, nullptr, TableFileCreationReason::kCompaction, &order));
ASSERT_EQ(Order::kFilterBeforeMerge, order);
ASSERT_OK(GetCompactionFilterMergeOrder(
&post, &post_factory, TableFileCreationReason::kCompaction, &order));
ASSERT_EQ(Order::kMergeBeforeFilter, order);
ASSERT_TRUE(
GetCompactionFilterMergeOrder(
&legacy, &post_factory, TableFileCreationReason::kCompaction, &order)
.IsInvalidArgument());
ASSERT_TRUE(
GetCompactionFilterMergeOrder(
&post, &legacy_factory, TableFileCreationReason::kCompaction, &order)
.IsInvalidArgument());
for (TableFileCreationReason reason :
{TableFileCreationReason::kFlush, TableFileCreationReason::kRecovery,
TableFileCreationReason::kMisc,
TableFileCreationReason::kSstFileWriter}) {
ASSERT_OK(GetCompactionFilterMergeOrder(&legacy, &legacy_factory, reason,
&order));
ASSERT_TRUE(
GetCompactionFilterMergeOrder(&post, &post_factory, reason, &order)
.IsInvalidArgument());
}
}
TEST_F(DBTestCompactionFilter, MergeBeforeFilterNullFactory) {
test::MergeFilterTrace trace;
auto factory = std::make_shared<test::RecordingMergeFilterFactory>(
CompactionFilter::MergeOrder::kMergeBeforeFilter, &trace);
factory->return_null = true;
Options options = CurrentOptions();
options.disable_auto_compactions = true;
options.compaction_filter_factory = factory;
Reopen(options);
ASSERT_OK(Put("a", "value"));
ASSERT_OK(Flush());
ASSERT_OK(CompactAllMergeFilterFiles());
ASSERT_EQ("value", Get("a"));
ASSERT_EQ(1, factory->creations.load());
ASSERT_TRUE(trace.Events().empty());
Close();
}
TEST_F(DBTestCompactionFilter, MergeBeforeFilterSharedCreationRuntimeGuard) {
for (bool recovery : {false, true}) {
test::MergeFilterTrace trace;
auto factory = std::make_shared<test::RecordingMergeFilterFactory>(
CompactionFilter::MergeOrder::kFilterBeforeMerge, &trace);
factory->filter_other_reason = true;
factory->other_reason = recovery ? TableFileCreationReason::kRecovery
: TableFileCreationReason::kFlush;
Options options = CurrentOptions();
options.disable_auto_compactions = true;
options.compaction_filter_factory = factory;
DestroyAndReopen(options);
ASSERT_OK(Put("a", "durable"));
factory->instance_order = CompactionFilter::MergeOrder::kMergeBeforeFilter;
ASSERT_OK(ValidateCompactionFilterOptions(options));
if (recovery) {
Close();
ASSERT_TRUE(TryReopen(options).IsInvalidArgument());
} else {
ASSERT_TRUE(Flush().IsInvalidArgument());
ASSERT_EQ(0, NumTableFilesAtLevel(0));
ASSERT_EQ("durable", Get("a"));
Close();
}
ASSERT_EQ(1, factory->creations.load());
ASSERT_TRUE(trace.Events().empty());
factory->instance_order = CompactionFilter::MergeOrder::kFilterBeforeMerge;
Reopen(options);
ASSERT_EQ("durable", Get("a"));
Close();
}
}
TEST_F(DBTestCompactionFilter, MergeBeforeFilterRepairRuntimeGuard) {
test::MergeFilterTrace trace;
auto factory = std::make_shared<test::RecordingMergeFilterFactory>(
CompactionFilter::MergeOrder::kFilterBeforeMerge, &trace);
factory->filter_other_reason = true;
factory->other_reason = TableFileCreationReason::kRecovery;
factory->instance_order = CompactionFilter::MergeOrder::kMergeBeforeFilter;
Options options = CurrentOptions();
options.disable_auto_compactions = true;
DestroyAndReopen(options);
ASSERT_OK(Put("sst", "preserved"));
ASSERT_OK(Flush());
ASSERT_OK(Put("wal", "not-installed"));
Close();
options.compaction_filter_factory = factory;
ASSERT_OK(ValidateCompactionFilterOptions(options));
// Repair retains its best-effort policy: a failed WAL conversion is logged
// and archived, while existing SST files are still recovered.
ASSERT_OK(RepairDB(dbname_, options));
ASSERT_EQ(1, factory->creations.load());
// The filter was created but never saw a key, and the WAL data is absent.
ASSERT_TRUE(trace.Events().empty());
options.compaction_filter_factory.reset();
Reopen(options);
ASSERT_EQ("preserved", Get("sst"));
ASSERT_EQ("NOT_FOUND", Get("wal"));
ASSERT_EQ(1, TotalTableFiles());
Close();
}
TEST_F(DBTestCompactionFilter, MergeBeforeFilterRepeatedPurgeAndRollback) {
test::MergeFilterTrace trace;
test::RecordingMergeFilter post(
CompactionFilter::MergeOrder::kMergeBeforeFilter, &trace);
test::RecordingMergeFilter legacy(
CompactionFilter::MergeOrder::kFilterBeforeMerge, &trace);
auto merge = std::make_shared<test::RecordingMergeOperator>(&trace);
Options options = CurrentOptions();
options.disable_auto_compactions = true;
options.merge_operator = merge;
options.compaction_filter = &post;
Reopen(options);
ASSERT_OK(Put("a", "A"));
ASSERT_OK(Flush());
ASSERT_OK(Merge("a", "B"));
ASSERT_OK(Flush());
trace.Clear();
ASSERT_OK(CompactAllMergeFilterFiles());
ASSERT_EQ((std::vector<std::string>{"full:a:AB", "value:a:AB"}),
trace.Events());
ASSERT_EQ("AB", Get("a"));
const Snapshot* snapshot = db_->GetSnapshot();
ASSERT_OK(Merge("a", "C"));
ASSERT_OK(Flush());
trace.Clear();
ASSERT_OK(CompactAllMergeFilterFiles());
ASSERT_EQ((std::vector<std::string>{"operand:a:C", "value:a:AB"}),
trace.Events());
ASSERT_EQ("AB", Get("a", snapshot));
db_->ReleaseSnapshot(snapshot);
trace.Clear();
ASSERT_OK(CompactAllMergeFilterFiles());
ASSERT_EQ((std::vector<std::string>{"full:a:ABC", "value:a:ABC"}),
trace.Events());
trace.Clear();
post.value_decision = CompactionFilter::Decision::kPurge;
ASSERT_OK(CompactAllMergeFilterFiles());
ASSERT_EQ("NOT_FOUND", Get("a"));
ASSERT_EQ((std::vector<std::string>{"value:a:ABC"}), trace.Events());
// Startup selection changes ordering, retaining ordinary removal for history.
Close();
legacy.value_decision = CompactionFilter::Decision::kRemove;
options.compaction_filter = &legacy;
Reopen(options);
ASSERT_EQ("NOT_FOUND", Get("a"));
ASSERT_OK(Put("a", "new"));
ASSERT_OK(Flush());
ASSERT_OK(CompactAllMergeFilterFiles());
ASSERT_EQ("NOT_FOUND", Get("a"));
Close();
}
TEST_F(DBTestCompactionFilter,
MergeBeforeFilterRemovalOutsideInputsAndExactSkip) {
for (bool skip : {false, true}) {
test::MergeFilterTrace trace;
test::RecordingMergeFilter filter(
CompactionFilter::MergeOrder::kMergeBeforeFilter, &trace);
filter.action_key = "a";
Options options = CurrentOptions();
options.disable_auto_compactions = true;
options.compaction_filter = &filter;
options.merge_operator =
std::make_shared<test::RecordingMergeOperator>(&trace);
DestroyAndReopen(options);
ASSERT_OK(Put("a", "older"));
ASSERT_OK(Flush());
MoveFilesToLevel(options.num_levels - 1);
const Snapshot* snapshot = db_->GetSnapshot();
ASSERT_OK(Put("a", "base"));
ASSERT_OK(Put("b", "middle"));
ASSERT_OK(Put("c", "next"));
ASSERT_OK(Flush());
ASSERT_OK(Merge("a", "operand"));
ASSERT_OK(Flush());
filter.value_decision =
skip ? CompactionFilter::Decision::kRemoveAndSkipUntil
: CompactionFilter::Decision::kPurge;
filter.skip_target = "c";
trace.Clear();
std::vector<LiveFileMetaData> files;
db_->GetLiveFilesMetaData(&files);
std::vector<std::string> inputs;
for (const auto& file : files) {
if (file.level == 0) {
inputs.push_back(file.relative_filename);
}
}
ASSERT_EQ(2, inputs.size());
ASSERT_OK(db_->CompactFiles(CompactionOptions(), inputs, 1));
ASSERT_EQ(skip ? "older" : "NOT_FOUND", Get("a"));
ASSERT_EQ("older", Get("a", snapshot));
ASSERT_EQ(skip ? "NOT_FOUND" : "middle", Get("b"));
ASSERT_EQ("next", Get("c"));
ASSERT_EQ(
skip ? (std::vector<std::string>{"full:a:baseoperand",
"value:a:baseoperand", "value:c:next"})
: (std::vector<std::string>{"full:a:baseoperand",
"value:a:baseoperand",
"value:b:middle", "value:c:next"}),
trace.Events());
db_->ReleaseSnapshot(snapshot);
if (!skip) {
CompactRangeOptions compact;
compact.bottommost_level_compaction = BottommostLevelCompaction::kForce;
ASSERT_OK(db_->CompactRange(compact, nullptr, nullptr));
ASSERT_EQ("NOT_FOUND", Get("a"));
}
Close();
Reopen(options);
ASSERT_EQ(skip ? "older" : "NOT_FOUND", Get("a"));
Close();
}
}
TEST_F(DBTestCompactionFilter, MergeBeforeFilterWideEntityAndBlobBase) {
for (bool blobs : {false, true}) {
test::MergeFilterTrace trace;
test::RecordingMergeFilter filter(
CompactionFilter::MergeOrder::kMergeBeforeFilter, &trace);
auto merge = std::make_shared<test::RecordingMergeOperator>(&trace);
Options options = CurrentOptions();
options.disable_auto_compactions = true;
options.compaction_filter = &filter;
options.merge_operator = merge;
options.enable_blob_files = blobs;
options.min_blob_size = 0;
DestroyAndReopen(options);
ASSERT_OK(db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), "a",
{{"", "A"}, {"extra", "payload"}}));
ASSERT_OK(Put("b", "base"));
ASSERT_OK(Flush());
ASSERT_OK(Merge("a", "B"));
ASSERT_OK(Merge("b", "operand"));
ASSERT_OK(Flush());
trace.Clear();
ASSERT_OK(CompactAllMergeFilterFiles());
ASSERT_EQ(
(std::vector<std::string>{"full:a:AB", "entity:a:AB",
"full:b:baseoperand", "value:b:baseoperand"}),
trace.Events());
PinnableWideColumns columns;
ASSERT_OK(db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), "a",
&columns));
ASSERT_EQ((WideColumns{{"", "AB"}, {"extra", "payload"}}),
columns.columns());
ASSERT_EQ("baseoperand", Get("b"));
filter.value_decision = CompactionFilter::Decision::kPurge;
filter.blob_decision = CompactionFilter::Decision::kPurge;
trace.Clear();
ASSERT_OK(CompactAllMergeFilterFiles());
ASSERT_EQ("NOT_FOUND", Get("a"));
ASSERT_EQ("NOT_FOUND", Get("b"));
if (blobs) {
ASSERT_GT(filter.blob_key_calls.load(), 0);
}
Close();
Reopen(options);
ASSERT_EQ("NOT_FOUND", Get("a"));
ASSERT_EQ("NOT_FOUND", Get("b"));
Close();
}
}
TEST_F(DBTestCompactionFilter, MergeBeforeFilterBlobReadFailures) {
class ResolvingFilter : public test::RecordingMergeFilter {
public:
explicit ResolvingFilter(test::MergeFilterTrace* trace)
: RecordingMergeFilter(MergeOrder::kMergeBeforeFilter, trace) {}
Decision FilterV4(
int level, const Slice& key, ValueType type, const Slice* value,
const WideColumns* columns, std::string* new_value,
std::vector<std::pair<std::string, std::string>>* new_columns,
std::string* skip_until,
WideColumnBlobResolver* resolver) const override {
if (resolver != nullptr) {
Slice resolved;
Status s = resolver->ResolveColumn(0, &resolved);
if (!s.ok()) {
resolve_failed.store(true);
return Decision::kKeep;
}
}
return RecordingMergeFilter::FilterV4(level, key, type, value, columns,
new_value, new_columns, skip_until,
resolver);
}
mutable std::atomic<bool> resolve_failed{false};
};
for (int history : {0, 1, 2}) {
FaultInjectionTestEnv fault_env(env_);
test::MergeFilterTrace trace;
ResolvingFilter filter(&trace);
Defer cleanup([&]() {
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
fault_env.SetFilesystemActive(true);
Close();
});
Options options = CurrentOptions();
options.env = &fault_env;
options.disable_auto_compactions = true;
options.compaction_filter = &filter;
options.merge_operator =
std::make_shared<test::RecordingMergeOperator>(&trace);
options.enable_blob_files = true;
options.min_blob_size = 0;
options.blob_compaction_readahead_size = 0;
DestroyAndReopen(options);
if (history == 0) {
ASSERT_OK(Put("a", "base"));
} else {
ASSERT_OK(db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), "a",
{{"", "base"}, {"extra", "payload"}}));
}
ASSERT_OK(Flush());
if (history != 2) {
ASSERT_OK(Merge("a", "operand"));
ASSERT_OK(Flush());
}
trace.Clear();
std::vector<LiveFileMetaData> before;
db_->GetLiveFilesMetaData(&before);
SyncPoint::GetInstance()->SetCallBack(
"BlobFileReader::GetBlob:ReadFromFile", [&](void*) {
fault_env.SetFilesystemActive(false, Status::IOError("blob read"));
});
SyncPoint::GetInstance()->EnableProcessing();
Status s = CompactAllMergeFilterFiles();
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
fault_env.SetFilesystemActive(true);
ASSERT_TRUE(s.IsIOError()) << s.ToString();
ASSERT_TRUE(trace.Events().empty());
ASSERT_EQ(history == 2, filter.resolve_failed.load());
std::vector<LiveFileMetaData> after;
db_->GetLiveFilesMetaData(&after);
ASSERT_EQ(before.size(), after.size());
for (size_t i = 0; i < before.size(); ++i) {
ASSERT_EQ(before[i].file_number, after[i].file_number);
}
ASSERT_EQ(history == 2 ? "base" : "baseoperand", Get("a"));
Close();
Reopen(options);
ASSERT_EQ(history == 2 ? "base" : "baseoperand", Get("a"));
}
}
TEST_F(DBTestCompactionFilter, MergeBeforeFilterEarlyFactoryValidation) {
for (TableFileCreationReason reason :
{TableFileCreationReason::kFlush, TableFileCreationReason::kRecovery,
TableFileCreationReason::kMisc,
TableFileCreationReason::kSstFileWriter}) {
test::MergeFilterTrace trace;
auto factory = std::make_shared<test::RecordingMergeFilterFactory>(
CompactionFilter::MergeOrder::kMergeBeforeFilter, &trace);
factory->filter_other_reason = true;
factory->other_reason = reason;
test::RecordingMergeFilter direct(
CompactionFilter::MergeOrder::kMergeBeforeFilter, &trace);
Options invalid = CurrentOptions();
invalid.compaction_filter_factory = factory;
invalid.compaction_filter = &direct;
ASSERT_TRUE(ValidateCompactionFilterOptions(invalid).IsInvalidArgument());
ColumnFamilyHandle* handle = nullptr;
ASSERT_TRUE(db_->CreateColumnFamily(invalid, "rejected", &handle)
.IsInvalidArgument());
ASSERT_EQ(nullptr, handle);
ASSERT_EQ(0, factory->creations.load());
std::vector<ColumnFamilyHandle*> handles;
Status batch = db_->CreateColumnFamilies(
{ColumnFamilyDescriptor("accepted", CurrentOptions()),
ColumnFamilyDescriptor("rejected", invalid)},
&handles);
ASSERT_TRUE(batch.IsInvalidArgument());
ASSERT_EQ(1, handles.size());
ASSERT_EQ("accepted", handles[0]->GetName());
ASSERT_OK(db_->DropColumnFamily(handles[0]));
ASSERT_OK(db_->DestroyColumnFamilyHandle(handles[0]));
Close();
ASSERT_TRUE(TryReopen(invalid).IsInvalidArgument());
ASSERT_EQ(0, factory->creations.load());
ASSERT_TRUE(trace.Events().empty());
Reopen(CurrentOptions());
}
}
TEST_F(DBTestCompactionFilter, MergeBeforeFilterUnsupportedConfigsFailEarly) {
test::MergeFilterTrace trace;
test::RecordingMergeFilter post(
CompactionFilter::MergeOrder::kMergeBeforeFilter, &trace);
test::RecordingMergeFilter legacy(
CompactionFilter::MergeOrder::kFilterBeforeMerge, &trace);
// User-defined timestamps.
Options ts_options = CurrentOptions();
ts_options.comparator = test::BytewiseComparatorWithU64TsWrapper();
ts_options.compaction_filter = &post;
ASSERT_TRUE(ValidateCompactionFilterOptions(ts_options).IsNotSupported());
ColumnFamilyHandle* handle = nullptr;
ASSERT_TRUE(
db_->CreateColumnFamily(ts_options, "ts", &handle).IsNotSupported());
ASSERT_EQ(nullptr, handle);
ts_options.compaction_filter = &legacy;
ASSERT_OK(ValidateCompactionFilterOptions(ts_options));
// WritePrepared and WriteUnprepared transactions use a SnapshotChecker.
const std::string path = test::PerThreadDBPath(env_, "mbf_txn");
for (TxnDBWritePolicy policy :
{TxnDBWritePolicy::WRITE_COMMITTED, TxnDBWritePolicy::WRITE_PREPARED,
TxnDBWritePolicy::WRITE_UNPREPARED}) {
SCOPED_TRACE(static_cast<int>(policy));
Options options = CurrentOptions();
options.create_if_missing = true;
options.compaction_filter = &post;
TransactionDBOptions txn_options;
txn_options.write_policy = policy;
ASSERT_OK(DestroyDB(path, options));
TransactionDB* txn_db = nullptr;
Status s = TransactionDB::Open(options, txn_options, path, &txn_db);
if (policy == TxnDBWritePolicy::WRITE_COMMITTED) {
ASSERT_OK(s);
delete txn_db;
continue;
}
ASSERT_TRUE(s.IsNotSupported()) << s.ToString();
ASSERT_EQ(nullptr, txn_db);
// Creating a merge-before-filter CF in such a DB fails too.
options.compaction_filter = &legacy;
ASSERT_OK(TransactionDB::Open(options, txn_options, path, &txn_db));
Options cf_options = options;
cf_options.compaction_filter = &post;
ASSERT_TRUE(txn_db->CreateColumnFamily(cf_options, "post", &handle)
.IsNotSupported());
ASSERT_EQ(nullptr, handle);
ASSERT_OK(txn_db->Put(WriteOptions(), "a", "value"));
delete txn_db;
}
ASSERT_OK(DestroyDB(path, CurrentOptions()));
}
TEST_F(DBTestCompactionFilter,
MergeBeforeFilterSkipOnOlderOperandMatchesLegacy) {
for (CompactionFilter::MergeOrder order :
{CompactionFilter::MergeOrder::kFilterBeforeMerge,
CompactionFilter::MergeOrder::kMergeBeforeFilter}) {
SCOPED_TRACE(static_cast<int>(order));
test::MergeFilterTrace trace;
test::RecordingMergeFilter filter(order, &trace);
filter.action_value = "M1";
filter.operand_decision = CompactionFilter::Decision::kRemoveAndSkipUntil;
filter.skip_target = "b";
auto merge = std::make_shared<test::RecordingMergeOperator>(&trace);
merge->partial_success = false;
Options options = CurrentOptions();
options.disable_auto_compactions = true;
options.compaction_filter = &filter;
options.merge_operator = merge;
DestroyAndReopen(options);
ASSERT_OK(Put("a", "base"));
ASSERT_OK(Flush());
MoveFilesToLevel(options.num_levels - 1);
ASSERT_OK(Merge("a", "M1"));
ASSERT_OK(Flush());
ASSERT_OK(Merge("a", "M2"));
ASSERT_OK(Flush());
std::vector<LiveFileMetaData> files;
db_->GetLiveFilesMetaData(&files);
std::vector<std::string> inputs;
for (const auto& file : files) {
if (file.level == 0) {
inputs.push_back(file.relative_filename);
}
}
ASSERT_EQ(2, inputs.size());
ASSERT_OK(db_->CompactFiles(CompactionOptions(), inputs, 1));
// Both orders drop every operand of the batch, exposing the older base.
ASSERT_EQ("base", Get("a"));
Close();
}
}
TEST_F(DBTestCompactionFilter, MergeBeforeFilterRuntimeDeclarationAndErrors) {
for (bool paranoid : {false, true}) {
test::MergeFilterTrace trace;
auto factory = std::make_shared<test::RecordingMergeFilterFactory>(
CompactionFilter::MergeOrder::kMergeBeforeFilter, &trace);
Options options = CurrentOptions();
options.disable_auto_compactions = true;
options.paranoid_checks = paranoid;
options.merge_operator =
std::make_shared<test::RecordingMergeOperator>(&trace);
options.compaction_filter_factory = factory;
DestroyAndReopen(options);
ASSERT_OK(Merge("a", "A"));
ASSERT_OK(Flush());
trace.Clear();
factory->instance_order = CompactionFilter::MergeOrder::kFilterBeforeMerge;
CompactRangeOptions compact;
compact.bottommost_level_compaction = BottommostLevelCompaction::kForce;
compact.change_level = true;
compact.target_level = options.num_levels - 1;
ASSERT_TRUE(
db_->CompactRange(compact, nullptr, nullptr).IsInvalidArgument());
ASSERT_EQ(1, factory->creations.load());
ASSERT_TRUE(trace.Events().empty());
ASSERT_EQ("A", Get("a"));
Close();
factory->instance_order = CompactionFilter::MergeOrder::kMergeBeforeFilter;
Reopen(options);
ASSERT_OK(db_->CompactRange(compact, nullptr, nullptr));
factory->operand_decision = CompactionFilter::Decision::kPurge;
// Keep the base in a lower level outside this compaction's inputs.
ASSERT_OK(Merge("a", "B"));
ASSERT_OK(Flush());
std::vector<LiveFileMetaData> files;
db_->GetLiveFilesMetaData(&files);
ASSERT_FALSE(files.empty());
const LiveFileMetaData* newest = &files.front();
for (const auto& file : files) {
if (file.file_number > newest->file_number) {
newest = &file;
}
}
Status invalid =
db_->CompactFiles(CompactionOptions(), {newest->relative_filename}, 1);
ASSERT_TRUE(invalid.IsNotSupported()) << invalid.ToString();
ASSERT_EQ("AB", Get("a"));
Close();
}
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+614 -37
View File
@@ -7,11 +7,15 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include <algorithm>
#include <atomic>
#include <chrono>
#include <limits>
#include <memory>
#include <string>
#include <tuple>
#include <utility>
#include <vector>
#include "compaction/compaction_picker_universal.h"
#include "db/blob/blob_index.h"
@@ -37,6 +41,7 @@
#include "util/concurrent_task_limiter_impl.h"
#include "util/fastrange.h"
#include "util/hash.h"
#include "util/mutexlock.h"
#include "util/random.h"
#include "utilities/fault_injection_env.h"
#include "utilities/fault_injection_fs.h"
@@ -141,6 +146,19 @@ class DBCompactionTest : public DBTestBase {
}
protected:
std::vector<std::pair<std::string, std::string>> TableFileNamesByKey() {
ColumnFamilyMetaData metadata;
db_->GetColumnFamilyMetaData(&metadata);
std::vector<std::pair<std::string, std::string>> files;
for (const auto& level : metadata.levels) {
for (const auto& file : level.files) {
files.emplace_back(file.smallestkey, file.name);
}
}
std::sort(files.begin(), files.end());
return files;
}
/*
* Verifies compaction stats of cfd are valid.
*
@@ -591,8 +609,6 @@ class FlushedFileCollector : public EventListener {
return result;
}
void ClearFlushedFiles() { flushed_files_.clear(); }
private:
std::vector<std::string> flushed_files_;
std::mutex mutex_;
@@ -4411,6 +4427,150 @@ static std::string ShortKey(int i) {
return std::string(buf);
}
TEST_F(DBCompactionTest, MissingBlobRepairDisablesTrivialMovesPerCall) {
Options options = CurrentOptions();
options.disable_auto_compactions = true;
options.num_levels = 3;
options.level_compaction_dynamic_level_bytes = false;
options.compression = kNoCompression;
DestroyAndReopen(options);
ASSERT_OK(Put("a", "value-a"));
ASSERT_OK(Flush());
const auto before = TableFileNamesByKey();
ASSERT_EQ(before.size(), 1U);
std::atomic<int> trivial_moves{0};
SyncPoint::GetInstance()->SetCallBack(
"DBImpl::BackgroundCompaction:TrivialMove",
[&](void*) { ++trivial_moves; });
SyncPoint::GetInstance()->EnableProcessing();
CompactRangeOptions compact_options;
compact_options.skip_missing_blob_files = true;
compact_options.bottommost_level_compaction =
BottommostLevelCompaction::kSkip;
ASSERT_OK(db_->CompactRange(compact_options, nullptr, nullptr));
const auto repaired = TableFileNamesByKey();
ASSERT_EQ(repaired.size(), 1U);
ASSERT_NE(before[0].second, repaired[0].second);
ASSERT_EQ(trivial_moves.load(), 0);
ASSERT_OK(Put("z", "value-z"));
ASSERT_OK(Flush());
const auto before_default = TableFileNamesByKey();
ASSERT_EQ(before_default.size(), 2U);
const Slice key("z");
compact_options.skip_missing_blob_files = false;
ASSERT_OK(db_->CompactRange(compact_options, &key, &key));
ASSERT_EQ(TableFileNamesByKey(), before_default);
ASSERT_EQ(trivial_moves.load(), 1);
ASSERT_EQ(Get("a"), "value-a");
ASSERT_EQ(Get("z"), "value-z");
SyncPoint::GetInstance()->DisableProcessing();
}
TEST_F(DBCompactionTest, MissingBlobRepairRewritesSelectedBottommostFiles) {
Options options = CurrentOptions();
options.disable_auto_compactions = true;
options.num_levels = 3;
options.level_compaction_dynamic_level_bytes = false;
options.compression = kNoCompression;
DestroyAndReopen(options);
for (const char* key : {"a", "m", "z"}) {
ASSERT_OK(Put(key, key));
ASSERT_OK(Flush());
}
CompactRangeOptions compact_options;
compact_options.change_level = true;
compact_options.target_level = 2;
ASSERT_OK(db_->CompactRange(compact_options, nullptr, nullptr));
ASSERT_EQ(NumTableFilesAtLevel(2), 3);
const auto before = TableFileNamesByKey();
ASSERT_EQ(before.size(), 3U);
compact_options = CompactRangeOptions();
compact_options.skip_missing_blob_files = true;
compact_options.bottommost_level_compaction =
BottommostLevelCompaction::kSkip;
const Slice key("m");
ASSERT_OK(db_->CompactRange(compact_options, &key, &key));
const auto repaired = TableFileNamesByKey();
ASSERT_EQ(repaired.size(), 3U);
ASSERT_EQ(before[0], repaired[0]);
ASSERT_NE(before[1].second, repaired[1].second);
ASSERT_EQ(before[2], repaired[2]);
compact_options.skip_missing_blob_files = false;
ASSERT_OK(db_->CompactRange(compact_options, nullptr, nullptr));
ASSERT_EQ(TableFileNamesByKey(), repaired);
for (const char* retained : {"a", "m", "z"}) {
ASSERT_EQ(Get(retained), retained);
}
}
TEST_F(DBCompactionTest, MissingBlobRepairKeepsBottommostProgress) {
class KeepFilter : public CompactionFilter {
public:
const char* Name() const override { return "KeepFilter"; }
bool Filter(int, const Slice&, const Slice&, std::string*,
bool*) const override {
return false;
}
} keep_filter;
for (bool repair : {false, true}) {
SCOPED_TRACE(repair);
Options options = CurrentOptions();
options.disable_auto_compactions = true;
options.num_levels = 3;
options.level_compaction_dynamic_level_bytes = false;
options.compression = kNoCompression;
const auto collector = std::make_shared<CompactionStatsCollector>();
options.listeners.push_back(collector);
DestroyAndReopen(options);
for (const char* key : {"a", "z"}) {
ASSERT_OK(Put(key, key));
ASSERT_OK(Flush());
}
CompactRangeOptions compact_options;
compact_options.change_level = true;
compact_options.target_level = 2;
ASSERT_OK(db_->CompactRange(compact_options, nullptr, nullptr));
ASSERT_EQ(NumTableFilesAtLevel(2), 2);
const auto before = TableFileNamesByKey();
ASSERT_EQ(before.size(), 2U);
options.compaction_filter = &keep_filter;
Reopen(options);
ASSERT_OK(Put("m", "m"));
ASSERT_OK(Flush());
const int compactions_before =
collector->NumberOfCompactions(CompactionReason::kManualCompaction);
compact_options = CompactRangeOptions();
compact_options.skip_missing_blob_files = repair;
compact_options.bottommost_level_compaction =
repair ? BottommostLevelCompaction::kSkip
: BottommostLevelCompaction::kForceOptimized;
ASSERT_OK(db_->CompactRange(compact_options, nullptr, nullptr));
const auto after = TableFileNamesByKey();
ASSERT_EQ(after.size(), 3U);
ASSERT_NE(before[0].second, after[0].second);
ASSERT_NE(before[1].second, after[2].second);
ASSERT_EQ(
collector->NumberOfCompactions(CompactionReason::kManualCompaction) -
compactions_before,
4);
for (const char* retained : {"a", "m", "z"}) {
ASSERT_EQ(Get(retained), retained);
}
Close();
}
}
TEST_P(DBCompactionTestWithParam, ForceBottommostLevelCompaction) {
int32_t trivial_move = 0;
int32_t non_trivial_move = 0;
@@ -4961,17 +5121,31 @@ TEST_F(DBCompactionTest, DeleteFilesInRangeConflictWithCompaction) {
{{"VersionSet::LogAndApply:WriteManifestStart",
"BackgroundCallCompaction:0"},
{"DBImpl::BackgroundCompaction:Finish",
"VersionSet::LogAndApply:WriteManifestDone"}});
"VersionSet::LogAndApply:WriteManifestDone"},
{"DBImpl::WaitForCompact:StartWaiting",
"DBCompactionTest::DeleteFilesInRangeConflictWithCompaction:"
"WaitStarted"}});
SyncPoint::GetInstance()->EnableProcessing();
// release snapshot which mark bottommost file for compaction
// Release no longer schedules work directly. WaitForCompact refreshes the
// oldest snapshot and schedules the compaction that this test races against
// DeleteFilesInRange().
db_->ReleaseSnapshot(snapshot);
Status wait_status;
port::Thread wait_thread(
[&]() { wait_status = dbfull()->TEST_WaitForCompact(); });
TEST_SYNC_POINT(
"DBCompactionTest::DeleteFilesInRangeConflictWithCompaction:"
"WaitStarted");
std::string begin_string = Key(0);
std::string end_string = Key(kMaxKey + 1);
Slice begin(begin_string);
Slice end(end_string);
ASSERT_OK(
DeleteFilesInRange(db_.get(), db_->DefaultColumnFamily(), &begin, &end));
Status delete_status =
DeleteFilesInRange(db_.get(), db_->DefaultColumnFamily(), &begin, &end);
wait_thread.join();
ASSERT_OK(delete_status);
ASSERT_OK(wait_status);
SyncPoint::GetInstance()->DisableProcessing();
}
@@ -5016,10 +5190,9 @@ TEST_F(DBCompactionTest, CompactBottomLevelFilesWithDeletions) {
std::vector<LiveFileMetaData> pre_release_metadata;
std::vector<LiveFileMetaData> post_release_metadata;
db_->GetLiveFilesMetaData(&pre_release_metadata);
// just need to bump seqnum so ReleaseSnapshot knows the newest key in the SST
// files does not need to be preserved in case of a future snapshot.
// Bump the published sequence so a maintenance refresh after the release can
// advance beyond the newest key in the SST files.
ASSERT_OK(Put(Key(0), "val"));
ASSERT_NE(kMaxSequenceNumber, dbfull()->bottommost_files_mark_threshold_);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"LevelCompactionPicker::PickCompaction:Return", [&](void* arg) {
Compaction* compaction = static_cast<Compaction*>(arg);
@@ -5027,11 +5200,9 @@ TEST_F(DBCompactionTest, CompactBottomLevelFilesWithDeletions) {
CompactionReason::kBottommostFiles);
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
// release snapshot and wait for compactions to finish. Single-file
// compactions should be triggered, which reduce the size of each bottom-level
// file without changing file count.
// Release does not schedule work. WaitForCompact refreshes the oldest
// snapshot before waiting and triggers the single-file compactions.
db_->ReleaseSnapshot(snapshot);
ASSERT_EQ(kMaxSequenceNumber, dbfull()->bottommost_files_mark_threshold_);
ASSERT_OK(dbfull()->TEST_WaitForCompact());
db_->GetLiveFilesMetaData(&post_release_metadata);
ASSERT_EQ(pre_release_metadata.size(), post_release_metadata.size());
@@ -5048,6 +5219,113 @@ TEST_F(DBCompactionTest, CompactBottomLevelFilesWithDeletions) {
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
}
TEST_F(DBCompactionTest, SnapshotReleaseDuringSuperVersionInstall) {
// A snapshot release can race with SuperVersion installation. The installer
// must observe the exact oldest-snapshot sequence after the release and
// schedule newly eligible bottommost work.
Options options = CurrentOptions();
options.compression = kNoCompression;
options.level0_file_num_compaction_trigger = 100;
CreateAndReopenWithCF({"one"}, options);
constexpr int kNumKeys = 100;
for (int i = 0; i < kNumKeys; ++i) {
ASSERT_OK(Put(1, Key(i), std::string(100, 'v')));
}
const Snapshot* snapshot = db_->GetSnapshot();
for (int i = 0; i < kNumKeys; i += 2) {
ASSERT_OK(Delete(1, Key(i)));
}
// Advance the published sequence beyond the largest sequence in CF "one".
ASSERT_OK(Put("advance", "value"));
bool released_during_install = false;
std::atomic_int bottommost_compactions{0};
SyncPoint::GetInstance()->SetCallBack(
"DBImpl::InstallSuperVersionAndScheduleWork:"
"BeforeSnapshotRefresh",
[&](void*) {
if (snapshot != nullptr) {
db_->ReleaseSnapshot(snapshot);
snapshot = nullptr;
released_during_install = true;
}
});
SyncPoint::GetInstance()->SetCallBack(
"LevelCompactionPicker::PickCompaction:Return", [&](void* arg) {
auto* compaction = static_cast<Compaction*>(arg);
if (compaction->compaction_reason() ==
CompactionReason::kBottommostFiles) {
++bottommost_compactions;
}
});
SyncPoint::GetInstance()->EnableProcessing();
ASSERT_OK(Flush(1));
ASSERT_TRUE(released_during_install);
ASSERT_EQ(snapshot, nullptr);
// The exact scan must observe the release and schedule newly eligible work.
ASSERT_OK(dbfull()->TEST_WaitForCompact());
ASSERT_GT(bottommost_compactions.load(), 0);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_F(DBCompactionTest,
StandaloneRangeTombstoneSnapshotAdvanceRequeuesScheduling) {
Options options = CurrentOptions();
options.compaction_style = kCompactionStyleUniversal;
options.num_levels = 2;
options.level0_file_num_compaction_trigger = 100;
DestroyAndReopen(options);
ASSERT_OK(Put("old", "value"));
ASSERT_OK(Flush());
ASSERT_OK(Put("new", "value"));
ASSERT_OK(Flush());
DBImpl* impl = dbfull();
InstrumentedMutexLock lock(&impl->mutex_);
ColumnFamilyData* cfd =
impl->GetVersionSet()->GetColumnFamilySet()->GetDefault();
VersionStorageInfo* vstorage = cfd->current()->storage_info();
ASSERT_EQ(vstorage->LevelFiles(0).size(), 2);
// Only the scheduling metadata matters here. Simulate an L0 standalone
// range tombstone that a prior Universal picker can defer.
FileMetaData* file = vstorage->LevelFiles(0).front();
file->marked_for_compaction = true;
file->num_entries = 1;
file->num_range_deletions = 1;
ASSERT_TRUE(file->FileIsStandAloneRangeTombstone());
vstorage->ComputeFilesMarkedForCompaction(
vstorage->MaxOutputLevel(cfd->AllowIngestBehind()));
const SequenceNumber threshold =
vstorage->standalone_range_tombstone_files_mark_threshold();
ASSERT_EQ(threshold, file->fd.smallest_seqno);
ASSERT_TRUE(impl->compaction_queue_.empty());
const SequenceNumber oldest_snapshot =
std::max(threshold, vstorage->oldest_snapshot_seqnum() + 1);
auto consume_queued_attempt = [&]() {
ASSERT_EQ(impl->compaction_queue_.size(), 1);
ColumnFamilyData* queued_cfd = impl->PopFirstFromCompactionQueue();
ASSERT_EQ(queued_cfd, cfd);
ASSERT_FALSE(queued_cfd->UnrefAndTryDelete());
};
ASSERT_TRUE(
impl->ReconcileOldestSnapshotForColumnFamily(cfd, oldest_snapshot));
consume_queued_attempt();
// The prior attempt was skipped and consumed its queue entry. A later
// snapshot advance must enqueue another attempt even though the threshold
// was already crossed.
ASSERT_TRUE(
impl->ReconcileOldestSnapshotForColumnFamily(cfd, oldest_snapshot + 1));
consume_queued_attempt();
}
TEST_F(DBCompactionTest, DelayCompactBottomLevelFilesWithDeletions) {
// bottom-level files may contain deletions due to snapshots protecting the
// deleted keys. Once the snapshot is released and the files are old enough,
@@ -5084,13 +5362,11 @@ TEST_F(DBCompactionTest, DelayCompactBottomLevelFilesWithDeletions) {
compaction_count++;
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
// just need to bump seqnum so ReleaseSnapshot knows the newest key in the SST
// files does not need to be preserved in case of a future snapshot.
// Bump the published sequence so a later maintenance refresh can advance.
ASSERT_OK(Put(Key(0), "val"));
ASSERT_NE(kMaxSequenceNumber, dbfull()->bottommost_files_mark_threshold_);
// release snapshot will not trigger compaction.
// Release and the first maintenance refresh do not trigger compaction while
// the bottommost file is still too young.
db_->ReleaseSnapshot(snapshot);
ASSERT_EQ(kMaxSequenceNumber, dbfull()->bottommost_files_mark_threshold_);
ASSERT_OK(dbfull()->TEST_WaitForCompact());
ASSERT_EQ(0, compaction_count);
// Now the file is old enough for compaction.
@@ -5875,6 +6151,8 @@ TEST_F(DBCompactionTest, PeriodicCompactionPhaseTriggerTime) {
const uint64_t kSeed = 0x9e3779b97f4a7c15ULL;
const uint64_t target = FastRange64(kSeed, kN);
ASSERT_LT(target, kN);
// Preferred-phase target for kSeed with a given periodic_compaction_seconds.
const auto target_for = [&](uint64_t n) { return FastRange64(kSeed, n); };
// recovery_percent == 0 => classic behavior: trigger at file_mod + N.
{
@@ -5947,25 +6225,212 @@ TEST_F(DBCompactionTest, PeriodicCompactionPhaseTriggerTime) {
ASSERT_LT(trig, 200 + grid); // within N/4 of the anchor
}
// Past due against the recovery target but not yet at the deadline => spread
// across [anchor, deadline] using the phase; never later than the deadline.
// Natural deadline shortly after the anchor (0 < r = natural - anchor):
// the pull toward the phase is scaled down to offset * min(q, k * r / n)
// with k = 1/2, so the file triggers after the anchor and no earlier than
// with the steady-state pull. Once r >= q * n / k, the steady-state pull
// (offset * q) applies unchanged.
{
const uint64_t n = 100;
const uint64_t small_target = FastRange64(kSeed, n);
const uint64_t fm = small_target + 50 + 3 * n; // phase offset == 50
const uint64_t fm = 3 * n + (target_for(n) + 50) % n;
ASSERT_EQ((fm % n + n - target_for(n)) % n, uint64_t{50}); // offset
const uint64_t natural = fm + n;
const uint64_t anchor =
natural - 25; // recovery target (natural-50) < anchor
Params p;
p.seed_hash = kSeed;
p.recovery_percent = 100;
p.anchor_time = anchor;
ASSERT_EQ((fm % n + n - small_target) % n, uint64_t{50});
const uint64_t expected = anchor + FastRange64(kSeed, natural - anchor);
const uint64_t trig = PeriodicCompactionPhaser::TriggerTime(fm, n, p);
ASSERT_EQ(trig, expected);
ASSERT_GE(trig, anchor);
ASSERT_LT(trig, natural);
struct Case {
int rp;
uint64_t r;
uint64_t pull;
};
for (const Case& c : {
Case{100, 25, 6}, // 50 * 0.5 * 25/100 = 6.25
Case{100, 150, 37}, // 50 * 0.5 * 150/100 = 37.5 (steady: 50)
Case{100, 200, 50}, // r == q * n / k: steady-state 50 * 1.0
Case{67, 100, 25}, // 50 * 0.5 * 100/100 = 25 (steady: 33)
Case{67, 134, 33}, // r == q * n / k: steady-state 50 * 0.67
Case{50, 1, 0}, // 50 * 0.5 * 1/100 = 0.25
Case{50, 99, 24}, // 50 * 0.5 * 99/100 = 24.75
Case{50, 100, 25}, // r == q * n / k: steady-state 50 * 0.5
Case{33, 25, 6}, // 50 * 0.5 * 25/100 = 6.25
Case{33, 60, 15}, // 50 * 0.5 * 60/100 = 15 (steady: 16)
Case{33, 66, 16}, // r == q * n / k: steady-state 50 * 0.33
Case{33, 70, 16}, // steady-state
Case{10, 19, 4}, // 50 * 0.5 * 19/100 = 4.75
Case{10, 20, 5}, // r == q * n / k: steady-state 50 * 0.1
}) {
Params p;
p.seed_hash = kSeed;
p.recovery_percent = c.rp;
p.anchor_time = natural - c.r;
const uint64_t trig = PeriodicCompactionPhaser::TriggerTime(fm, n, p);
ASSERT_EQ(trig, natural - c.pull) << "rp=" << c.rp << " r=" << c.r;
ASSERT_GT(trig, p.anchor_time);
}
}
// Long periodic_compaction_seconds. Up to kMaxPhasedPeriod the pull is
// computed exactly, without overflow, including the largest intermediate
// product (inputs chosen so the exact result is easy to derive by hand).
// Beyond it, phasing is not applied (pull 0: the classic deadline).
{
const uint64_t kMaxPhased = PeriodicCompactionPhaser::kMaxPhasedPeriod;
struct Case {
uint64_t n;
uint64_t offset;
uint64_t r;
int rp;
uint64_t pull;
};
for (const Case& c : {
// 30 days: 1296000 * 0.5 * 1/4
Case{30 * 86400, 15 * 86400, 30 * 86400 / 4, 33, 162000},
// Largest product: (n - 1) * (2n - 1) / (2n), rounded down
Case{kMaxPhased, kMaxPhased - 1, 2 * kMaxPhased - 1, 100,
kMaxPhased - 2},
// Steady state at kMaxPhased: (n - 1) * 0.33, rounded down
Case{kMaxPhased, kMaxPhased - 1, kMaxPhased - 1, 33,
(kMaxPhased - 1) * 33 / 100},
// Beyond kMaxPhasedPeriod: classic deadline
Case{kMaxPhased + 1, kMaxPhased / 2, kMaxPhased / 4, 33, 0},
Case{uint64_t{1} << 40, uint64_t{1} << 39, uint64_t{1} << 38, 100,
0},
Case{std::numeric_limits<uint64_t>::max() / 8, 12345, 678, 50, 0},
}) {
const uint64_t fm = 3 * c.n + (target_for(c.n) + c.offset) % c.n;
ASSERT_EQ((fm % c.n + c.n - target_for(c.n)) % c.n, c.offset);
Params p;
p.seed_hash = kSeed;
p.recovery_percent = c.rp;
p.anchor_time = fm + c.n - c.r;
ASSERT_EQ(PeriodicCompactionPhaser::TriggerTime(fm, c.n, p),
fm + c.n - c.pull)
<< "n=" << c.n;
}
}
}
TEST_F(DBCompactionTest, PeriodicCompactionPhaseTriggerTimeRandomized) {
// Randomized properties of PeriodicCompactionPhaser::TriggerTime, in
// particular around the anchor (when phasing took effect).
using Params = PeriodicCompactionPhaseParams;
const uint32_t seed = static_cast<uint32_t>(
std::chrono::system_clock::now().time_since_epoch().count());
SCOPED_TRACE("seed=" + std::to_string(seed));
Random64 rnd(seed);
// Bounds sweep over (n, recovery_percent, phase, file time, anchor),
// including n at and beyond kMaxPhasedPeriod.
{
const uint64_t kMaxPhased = PeriodicCompactionPhaser::kMaxPhasedPeriod;
const uint64_t kNs[] = {1,
2,
3,
4,
7,
100,
86400,
30 * 86400,
kMaxPhased,
kMaxPhased + 1,
uint64_t{1} << 40,
std::numeric_limits<uint64_t>::max() / 16};
const int kRps[] = {1, 10, 25, 33, 50, 67, 99, 100};
for (int i = 0; i < 100000; ++i) {
const uint64_t n = kNs[rnd.Uniform(std::size(kNs))];
const int rp = kRps[rnd.Uniform(std::size(kRps))];
Params p;
p.seed_hash = rnd.Next();
p.recovery_percent = rp;
p.anchor_time = rnd.Uniform(6 * n);
const uint64_t fm = rnd.Uniform(4 * n);
const uint64_t natural = fm + n;
const uint64_t trig = PeriodicCompactionPhaser::TriggerTime(fm, n, p);
if (n > kMaxPhased) {
ASSERT_EQ(trig, natural) << "n=" << n << " rp=" << rp;
continue;
}
const uint64_t offset = (fm % n + n - FastRange64(p.seed_hash, n)) % n;
const uint64_t steady_trigger = natural - offset * rp / 100;
if (natural > p.anchor_time) {
// Never before the anchor, never after the natural deadline, never
// earlier than with the steady-state pull.
ASSERT_GT(trig, p.anchor_time) << "n=" << n << " rp=" << rp;
ASSERT_LE(trig, natural) << "n=" << n << " rp=" << rp;
ASSERT_GE(trig, steady_trigger) << "n=" << n << " rp=" << rp;
if (fm >= p.anchor_time + (rp <= 50 ? 0 : n)) {
// Written at or after the anchor (at least a period after it for
// rp > 50): exactly the steady-state pull.
ASSERT_EQ(trig, steady_trigger) << "n=" << n << " rp=" << rp;
}
} else {
// Already past due at the anchor: within a quarter period after it.
ASSERT_GE(trig, p.anchor_time) << "n=" << n << " rp=" << rp;
ASSERT_LT(trig - p.anchor_time, std::max(n / 4, uint64_t{1}))
<< "n=" << n << " rp=" << rp;
}
}
}
// Bounded trigger density after the anchor. Many files (random phases)
// share one anchor and have file ages spread uniformly over a period at the
// anchor, so their natural deadlines are uniform over the period after it.
// Without phasing, each 1% of that period would get 1% of the triggers. The
// scaled pull should give about 1.39x throughout (k = 1/2) for any
// recovery_percent, so no 1% bin may exceed 1.6x. (Choosing each trigger
// uniformly between the anchor and the natural deadline gave ~3.8x in the
// first 1%.)
{
const uint64_t n = 1000000;
const uint64_t anchor = 10 * n + 12345;
const int kFiles = 200000;
for (int rp : {10, 33, 50, 67, 100}) {
std::vector<int> bins(100);
for (int i = 0; i < kFiles; ++i) {
Params p;
p.seed_hash = rnd.Next();
p.recovery_percent = rp;
p.anchor_time = anchor;
// Natural deadline in (anchor, anchor + n]
const uint64_t fm = anchor - n + 1 + rnd.Uniform(n);
const uint64_t trig = PeriodicCompactionPhaser::TriggerTime(fm, n, p);
ASSERT_GT(trig, anchor);
ASSERT_LE(trig, fm + n);
bins[(trig - anchor - 1) * 100 / n]++;
}
for (size_t b = 0; b < bins.size(); ++b) {
ASSERT_LE(bins[b], kFiles / 100 * 16 / 10) << "rp=" << rp << " b=" << b;
}
}
}
// A herd: many DBs (random phases) share one natural deadline 0.62 * n after
// the anchor, as with a lockstep fleet when phasing is enabled. Triggers
// must stay spread over most of [natural - q * n, natural] (about 0.31 * n
// here), with no clump: no bin of width n/1000 may hold more than 1% of the
// herd (versus ~0.32% expected).
{
const uint64_t n = 1000000;
const uint64_t anchor = 10 * n;
const uint64_t natural = anchor + n * 62 / 100;
const uint64_t fm = natural - n;
const int kHerd = 100000;
for (int rp : {33, 50, 100}) {
std::vector<int> bins(1000);
uint64_t earliest = natural;
for (int i = 0; i < kHerd; ++i) {
Params p;
p.seed_hash = rnd.Next();
p.recovery_percent = rp;
p.anchor_time = anchor;
const uint64_t trig = PeriodicCompactionPhaser::TriggerTime(fm, n, p);
ASSERT_GT(trig, anchor);
ASSERT_LE(trig, natural);
earliest = std::min(earliest, trig);
bins[(trig - anchor) * 1000 / n]++;
}
ASSERT_GE(natural - earliest, n * 30 / 100) << "rp=" << rp;
for (size_t b = 0; b < bins.size(); ++b) {
ASSERT_LE(bins[b], kHerd / 100) << "rp=" << rp << " b=" << b;
}
}
}
}
@@ -6620,6 +7085,119 @@ TEST_F(DBCompactionTest, CompactRangeShutdownWhileDelayed) {
}
}
TEST_F(DBCompactionTest, CompactRangeAtomicFlushOtherColumnFamilyDropped) {
Options options = CurrentOptions();
options.atomic_flush = true;
options.level0_stop_writes_trigger = 2;
options.level0_slowdown_writes_trigger = 2;
options.level0_file_num_compaction_trigger = -1;
for (bool drop_target : {false, true}) {
SCOPED_TRACE(drop_target);
DestroyAndReopen(options);
CreateAndReopenWithCF({"target", "obsolete"}, options);
test::SleepingBackgroundTask sleeping_task;
env_->SetBackgroundThreads(1, Env::LOW);
env_->Schedule(&test::SleepingBackgroundTask::DoSleepTask, &sleeping_task,
Env::LOW);
sleeping_task.WaitUntilSleeping();
ASSERT_OK(Put(2, "old", "value"));
ASSERT_OK(Flush(2));
ASSERT_OK(Put(2, "pending", "value"));
ASSERT_OK(Put(1, "key", "value"));
SyncPoint::GetInstance()->LoadDependency(
{{"DBImpl::WaitUntilFlushWouldNotStallWrites:StallWait",
"DBCompactionTest::AtomicFlushOtherColumnFamilyDropped:Drop"}});
SyncPoint::GetInstance()->EnableProcessing();
Status status;
port::Thread compact([&] {
status = db_->CompactRange(CompactRangeOptions(), handles_[1], nullptr,
nullptr);
});
TEST_SYNC_POINT(
"DBCompactionTest::AtomicFlushOtherColumnFamilyDropped:Drop");
if (drop_target) {
ASSERT_OK(db_->DropColumnFamily(handles_[1]));
}
ASSERT_OK(db_->DropColumnFamily(handles_[2]));
sleeping_task.WakeUp();
compact.join();
if (drop_target) {
ASSERT_EQ(Status::kColumnFamilyDropped, status.code());
} else {
ASSERT_OK(status);
ASSERT_EQ("value", Get(1, "key"));
ASSERT_GT(TotalTableFiles(1), 0);
ASSERT_EQ(0, NumTableFilesAtLevel(0, 1));
}
SyncPoint::GetInstance()->DisableProcessing();
}
}
TEST_F(DBCompactionTest, CompactRangeAtomicFlushSelectedColumnFamilyDropped) {
// The target has nothing to flush, so the atomic flush selects only the
// other column family, which is dropped while the flush is pending.
Options options = CurrentOptions();
options.atomic_flush = true;
DestroyAndReopen(options);
CreateAndReopenWithCF({"target", "obsolete"}, options);
ASSERT_OK(Put(2, "key", "value"));
SyncPoint::GetInstance()->LoadDependency(
{{"DBImpl::AtomicFlushMemTables:BeforeWaitForBgFlush",
"DBCompactionTest::AtomicFlushSelectedColumnFamilyDropped:Drop"},
{"DBCompactionTest::AtomicFlushSelectedColumnFamilyDropped:Dropped",
"DBImpl::BackgroundCallFlush:start"}});
SyncPoint::GetInstance()->EnableProcessing();
CompactRangeOptions cro;
cro.allow_write_stall = true;
Status status;
port::Thread compact(
[&] { status = db_->CompactRange(cro, handles_[1], nullptr, nullptr); });
TEST_SYNC_POINT(
"DBCompactionTest::AtomicFlushSelectedColumnFamilyDropped:Drop");
ASSERT_OK(db_->DropColumnFamily(handles_[2]));
TEST_SYNC_POINT(
"DBCompactionTest::AtomicFlushSelectedColumnFamilyDropped:Dropped");
compact.join();
ASSERT_OK(status);
SyncPoint::GetInstance()->DisableProcessing();
}
TEST_F(DBCompactionTest, WaitForCompactAtomicFlushOtherColumnFamilyDropped) {
// The flush waits for a write stall on another column family that is
// dropped during the wait. The live column family must still be flushed.
Options options = CurrentOptions();
options.atomic_flush = true;
options.level0_stop_writes_trigger = 2;
options.level0_slowdown_writes_trigger = 2;
options.level0_file_num_compaction_trigger = -1;
DestroyAndReopen(options);
CreateAndReopenWithCF({"live", "obsolete"}, options);
ASSERT_OK(Put(2, "old", "value"));
ASSERT_OK(Flush(2));
ASSERT_OK(Put(2, "pending", "value"));
ASSERT_OK(Put(1, "key", "value"));
SyncPoint::GetInstance()->LoadDependency(
{{"DBImpl::WaitUntilFlushWouldNotStallWrites:StallWait",
"DBCompactionTest::WaitForCompactOtherColumnFamilyDropped:Drop"}});
SyncPoint::GetInstance()->EnableProcessing();
Status status;
port::Thread wait([&] {
WaitForCompactOptions wait_for_compact_options;
wait_for_compact_options.flush = true;
status = db_->WaitForCompact(wait_for_compact_options);
});
TEST_SYNC_POINT(
"DBCompactionTest::WaitForCompactOtherColumnFamilyDropped:Drop");
ASSERT_OK(db_->DropColumnFamily(handles_[2]));
wait.join();
ASSERT_OK(status);
ASSERT_EQ(1, NumTableFilesAtLevel(0, 1));
SyncPoint::GetInstance()->DisableProcessing();
}
TEST_F(DBCompactionTest, CompactRangeSkipFlushAfterDelay) {
// Verify that, when `CompactRangeOptions::allow_write_stall == false`,
// CompactRange skips its flush if the delay is long enough that the memtables
@@ -7408,8 +7986,7 @@ TEST_F(DBCompactionTest,
// The NewRandomAccessFile:O_DIRECT sync point in env/fs_posix.cc fires once
// per fresh open with the O_DIRECT flag, so this proves the kernel path, not
// just the FileOptions. Only runs on platforms that take the O_DIRECT path.
#if !defined(OS_MACOSX) && !defined(OS_OPENBSD) && !defined(OS_SOLARIS) && \
!defined(OS_WIN)
#if !defined(OS_MACOSX) && !defined(OS_OPENBSD) && !defined(OS_WIN)
TEST_F(DBCompactionTest, UseDirectIoForCompactionReadsEndToEnd) {
if (!IsDirectIOSupported()) {
ROCKSDB_GTEST_BYPASS("Direct IO not supported");
@@ -12474,7 +13051,7 @@ TEST_F(DBCompactionTest, VerifyOutputRecordCountPlainTable) {
TEST_F(DBCompactionTest, ErrorWhenReadFileHead) {
// This is to test a bug that is fixed in
// https://github.com/facebook/rocksdb/pull/11782.
// https://github.com/meta/rocksdb/pull/11782.
//
// Ingest error when reading from a file with offset = 0,
// See if compaction handles it correctly.
+1175 -6
View File
File diff suppressed because it is too large. Load diff
+754
View File
@@ -5,8 +5,10 @@
#include "db/db_test_util.h"
#include "rocksdb/convenience.h"
#include "rocksdb/experimental.h"
#include "rocksdb/metadata.h"
#include "rocksdb/sst_file_writer.h"
#include "util/defer.h"
namespace ROCKSDB_NAMESPACE {
@@ -15,6 +17,463 @@ class DBEtc3Test : public DBTestBase {
DBEtc3Test() : DBTestBase("db_etc3_test", /*env_do_fsync=*/true) {}
};
namespace {
// The configuration in which RocksDB 11.10 through 11.13 could write a common
// prefix extending past a user key: format_version=8 (with the default
// optimize_key_common_prefix) and ReverseBytewiseComparator, which orders a
// user key after longer user keys that extend it.
Options PreviouslyBuggyCommonPrefixOptions(
Options options, BlockBasedTableOptions table_options) {
options.comparator = ReverseBytewiseComparator();
options.disable_auto_compactions = true;
table_options.format_version = 8;
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
return options;
}
// Makes BlockBuilder compute the common prefix as RocksDB 11.10 through 11.13
// did, which could extend it past a user key.
void SimulateOldCommonPrefixWriter() {
SyncPoint::GetInstance()->SetCallBack(
"BlockBuilder::AddWithLastKeyImpl:CommonPrefixUserKeySize",
[](void* arg) {
*static_cast<size_t*>(arg) = std::numeric_limits<size_t>::max();
});
}
void StopSimulatingOldCommonPrefixWriter() {
SyncPoint::GetInstance()->ClearCallBack(
"BlockBuilder::AddWithLastKeyImpl:CommonPrefixUserKeySize");
}
// Data and index blocks found on load to have a last user key shorter than
// their common prefix.
struct ShortLastUserKeyBlockCounts {
std::atomic<int> data{0};
std::atomic<int> index{0};
};
void CountShortLastUserKeyBlocks(ShortLastUserKeyBlockCounts* counts) {
SyncPoint::GetInstance()->SetCallBack(
"Block::InitializeDataBlockCommonPrefixCheck:LastUserKeyShorter",
[counts](void*) { counts->data.fetch_add(1); });
SyncPoint::GetInstance()->SetCallBack(
"Block::InitializeIndexBlockCommonPrefixCheck:LastUserKeyShorter",
[counts](void*) { counts->index.fetch_add(1); });
}
const std::string kPutLonger(
"ab\x01"
"c",
4);
} // namespace
TEST_F(DBEtc3Test, CommonKeyPrefixWithShorterKeyAfterLongerKey) {
DestroyAndReopen(PreviouslyBuggyCommonPrefixOptions(
CurrentOptions(), BlockBasedTableOptions()));
ShortLastUserKeyBlockCounts counts;
CountShortLastUserKeyBlocks(&counts);
SyncPoint::GetInstance()->EnableProcessing();
ASSERT_OK(Put("xy", "old"));
ASSERT_OK(Flush());
const std::string del_long(
"xy\x00"
"c",
4);
ASSERT_OK(Put(del_long, "long"));
ASSERT_OK(Delete("xy"));
ASSERT_OK(Flush());
EXPECT_EQ("long", Get(del_long));
EXPECT_EQ("NOT_FOUND", Get("xy"));
ASSERT_OK(Put(kPutLonger, "long"));
ASSERT_OK(Put("ab", "short"));
ASSERT_OK(Flush());
EXPECT_EQ("long", Get(kPutLonger));
EXPECT_EQ("short", Get("ab"));
std::unique_ptr<Iterator> iter(db_->NewIterator(ReadOptions()));
iter->Seek("ab");
ASSERT_TRUE(iter->Valid());
EXPECT_EQ("ab", iter->key().ToString());
ASSERT_OK(iter->status());
EXPECT_EQ(0, counts.data.load());
EXPECT_EQ(0, counts.index.load());
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_F(DBEtc3Test, CommonKeyPrefixReadsBlocksWithShorterKey) {
Options options = CurrentOptions();
options.compression = kNoCompression;
options.max_subcompactions = 2;
options.target_file_size_base = 1024;
BlockBasedTableOptions table_options;
table_options.block_size = 1000;
table_options.block_size_deviation = 0;
DestroyAndReopen(PreviouslyBuggyCommonPrefixOptions(options, table_options));
ShortLastUserKeyBlockCounts counts;
CountShortLastUserKeyBlocks(&counts);
SimulateOldCommonPrefixWriter();
SyncPoint::GetInstance()->EnableProcessing();
// Data block [kPutLonger, "ab"] with common prefix "ab\x01".
const std::string big(2000, 'v');
ASSERT_OK(Put("c", big));
ASSERT_OK(Put(kPutLonger, "x"));
ASSERT_OK(Put("ab", big));
ASSERT_OK(Put("aa", big));
ASSERT_OK(Flush());
ASSERT_OK(Put("b", "y"));
ASSERT_OK(Flush());
StopSimulatingOldCommonPrefixWriter();
auto verify = [&]() {
EXPECT_EQ(big, Get("ab"));
EXPECT_EQ("x", Get(kPutLonger));
EXPECT_EQ(std::vector<std::string>({big, "x"}),
MultiGet({"ab", kPutLonger}));
std::unique_ptr<Iterator> iter(db_->NewIterator(ReadOptions()));
// "ab\x01" equals the common prefix, so it can be compared by suffix.
for (const Slice& target :
{Slice("ab"), Slice("ab\x00", 3), Slice("ab\x01", 3)}) {
iter->Seek(target);
ASSERT_TRUE(iter->Valid());
EXPECT_EQ("ab", iter->key().ToString());
}
iter->SeekForPrev("ab");
ASSERT_TRUE(iter->Valid());
EXPECT_EQ("ab", iter->key().ToString());
iter->SeekForPrev(Slice("ab\x01", 3));
ASSERT_TRUE(iter->Valid());
EXPECT_EQ(kPutLonger, iter->key().ToString());
ASSERT_OK(iter->status());
};
verify();
EXPECT_GT(counts.data.load(), 0);
// With subcompactions, a compaction starts by seeking to a key.
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
verify();
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_F(DBEtc3Test, CommonKeyPrefixIteratesBlocksWithShorterKey) {
// Iteration seeks internally after skipping many versions of a key
// (max_sequential_skip_in_iterations) and when reversing.
Options options = CurrentOptions();
options.compression = kNoCompression;
options.statistics = CreateDBStatistics();
DestroyAndReopen(
PreviouslyBuggyCommonPrefixOptions(options, BlockBasedTableOptions()));
ShortLastUserKeyBlockCounts counts;
CountShortLastUserKeyBlocks(&counts);
SimulateOldCommonPrefixWriter();
SyncPoint::GetInstance()->EnableProcessing();
ASSERT_OK(Put("c", "vc"));
ASSERT_OK(Put("aa", "va"));
ASSERT_OK(Flush());
// Data block [kPutLonger, "ab"] with common prefix "ab\x01".
ASSERT_OK(Put(kPutLonger, "vl"));
ASSERT_OK(Put("ab", "v0"));
ASSERT_OK(Flush());
StopSimulatingOldCommonPrefixWriter();
const Snapshot* snap = db_->GetSnapshot();
for (int i = 1; i <= 10; ++i) {
ASSERT_OK(Put("ab", "v" + std::to_string(i)));
ASSERT_OK(Flush());
}
auto scan = [&](const Snapshot* s, bool forward) {
ReadOptions ro;
ro.snapshot = s;
std::unique_ptr<Iterator> it(db_->NewIterator(ro));
std::string out;
for (forward ? it->SeekToFirst() : it->SeekToLast(); it->Valid();
forward ? it->Next() : it->Prev()) {
out +=
it->key().ToString(/*hex=*/true) + "=" + it->value().ToString() + " ";
}
EXPECT_OK(it->status());
return out;
};
// Hex keys: "c", kPutLonger, "ab", "aa".
EXPECT_EQ("63=vc 61620163=vl 6162=v10 6161=va ", scan(nullptr, true));
EXPECT_EQ("63=vc 61620163=vl 6162=v0 6161=va ", scan(snap, true));
EXPECT_EQ("6161=va 6162=v10 61620163=vl 63=vc ", scan(nullptr, false));
EXPECT_EQ("6161=va 6162=v0 61620163=vl 63=vc ", scan(snap, false));
EXPECT_GT(TestGetTickerCount(options, NUMBER_OF_RESEEKS_IN_ITERATION), 0);
EXPECT_GT(counts.data.load(), 0);
db_->ReleaseSnapshot(snap);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_F(DBEtc3Test, CommonKeyPrefixIndexBlockWithShorterKey) {
// One key per data block. Two versions of "ab" in adjacent data blocks make
// the index keys internal keys, so the old writer can store a common prefix
// in the index block (with more than one key per restart interval) that
// extends past a user key.
Options options = CurrentOptions();
options.compression = kNoCompression;
BlockBasedTableOptions table_options;
table_options.block_size = 1;
table_options.index_block_restart_interval = 16;
DestroyAndReopen(PreviouslyBuggyCommonPrefixOptions(options, table_options));
ShortLastUserKeyBlockCounts counts;
CountShortLastUserKeyBlocks(&counts);
SimulateOldCommonPrefixWriter();
SyncPoint::GetInstance()->EnableProcessing();
ASSERT_OK(Put(kPutLonger, "x"));
ASSERT_OK(Put("ab", "v1"));
const Snapshot* snap = db_->GetSnapshot();
ASSERT_OK(Put("ab", "v2"));
ASSERT_OK(Flush());
StopSimulatingOldCommonPrefixWriter();
auto verify = [&]() {
EXPECT_EQ("v2", Get("ab"));
EXPECT_EQ("v1", Get("ab", snap));
EXPECT_EQ("x", Get(kPutLonger));
EXPECT_EQ(std::vector<std::string>({"v2", "x"}),
MultiGet({"ab", kPutLonger}));
for (const Snapshot* s : {static_cast<const Snapshot*>(nullptr), snap}) {
ReadOptions ro;
ro.snapshot = s;
std::unique_ptr<Iterator> iter(db_->NewIterator(ro));
for (const Slice& target : {Slice("ab"), Slice("ab\x01", 3)}) {
iter->Seek(target);
ASSERT_TRUE(iter->Valid());
EXPECT_EQ("ab", iter->key().ToString());
EXPECT_EQ(s ? "v1" : "v2", iter->value().ToString());
}
iter->SeekForPrev(Slice("ab\x01", 3));
ASSERT_TRUE(iter->Valid());
EXPECT_EQ(kPutLonger, iter->key().ToString());
ASSERT_OK(iter->status());
}
};
verify();
EXPECT_GT(counts.index.load(), 0);
EXPECT_EQ(0, counts.data.load());
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
verify();
db_->ReleaseSnapshot(snap);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_F(DBEtc3Test, CommonKeyPrefixRandomizedReverse) {
// Random keys, often prefixes of each other, from both the old and the
// current writer, checked against a model.
struct RevLess {
bool operator()(const std::string& a, const std::string& b) const {
return Slice(a).compare(Slice(b)) > 0;
}
};
const uint32_t base_seed = static_cast<uint32_t>(env_->NowMicros());
ShortLastUserKeyBlockCounts counts;
for (int round = 0; round < 60; ++round) {
const uint32_t seed = base_seed + round;
SCOPED_TRACE("seed=" + std::to_string(seed));
Random rnd(seed);
const bool old_writer = rnd.OneIn(2);
Options options = CurrentOptions();
options.compression = kNoCompression;
options.max_subcompactions = 1 + rnd.Uniform(3);
const uint8_t kProtectionBytes[] = {0, 1, 2, 4, 8};
options.block_protection_bytes_per_key = kProtectionBytes[rnd.Uniform(5)];
BlockBasedTableOptions table_options;
// Small blocks make it more likely that the old writer's prefix extends
// past a user key: all keys of the block must extend that user key.
table_options.block_size = 64 + rnd.Uniform(rnd.OneIn(2) ? 64 : 512);
table_options.block_size_deviation = 0;
const int kRestartIntervals[] = {1, 2, 4, 16};
table_options.block_restart_interval = kRestartIntervals[rnd.Uniform(4)];
table_options.index_block_restart_interval =
kRestartIntervals[rnd.Uniform(4)];
table_options.filter_policy.reset(NewBloomFilterPolicy(10));
// Small index partitions make it more likely that the old writer's prefix
// extends past a user key in index blocks.
switch (rnd.Uniform(4)) {
case 0:
break;
case 1:
table_options.index_type =
BlockBasedTableOptions::kBinarySearchWithFirstKey;
break;
default:
table_options.index_type = BlockBasedTableOptions::kTwoLevelIndexSearch;
table_options.metadata_block_size = 64 + rnd.Uniform(64);
table_options.partition_filters = true;
table_options.decouple_partitioned_filters = rnd.OneIn(2);
break;
}
table_options.cache_index_and_filter_blocks = rnd.OneIn(2);
// Warming the cache while building a table does not check blocks (only
// blocks from the old writer need it), so only with the current writer.
if (!old_writer && rnd.OneIn(2)) {
table_options.prepopulate_block_cache =
rnd.OneIn(2)
? BlockBasedTableOptions::PrepopulateBlockCache::kFlushOnly
: BlockBasedTableOptions::PrepopulateBlockCache::
kFlushAndCompaction;
}
table_options.separate_key_value_in_data_block = rnd.OneIn(3);
if (rnd.OneIn(3)) {
table_options.data_block_index_type =
BlockBasedTableOptions::kDataBlockBinaryAndHash;
}
const Options db_options =
PreviouslyBuggyCommonPrefixOptions(options, table_options);
DestroyAndReopen(db_options);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
CountShortLastUserKeyBlocks(&counts);
if (old_writer) {
SimulateOldCommonPrefixWriter();
}
SyncPoint::GetInstance()->EnableProcessing();
const int flagged_before = counts.data.load() + counts.index.load();
auto rand_key = [&](bool allow_no_prefix) {
static const char kAlphabet[] = {'\x00', '\x01', '\x02', 'a', 'b'};
std::string k;
if (!allow_no_prefix || !rnd.OneIn(4)) {
k = "pp";
}
const int len = static_cast<int>(rnd.Uniform(6));
for (int i = 0; i < len; ++i) {
k.push_back(kAlphabet[rnd.Uniform(5)]);
}
return k;
};
std::map<std::string, std::string, RevLess> model;
std::vector<std::string> keys;
// Snapshots keep older versions, which can make index keys internal keys.
std::vector<const Snapshot*> snapshots;
Defer release_snapshots([&]() {
for (const Snapshot* s : snapshots) {
db_->ReleaseSnapshot(s);
}
});
const int num_files = 1 + static_cast<int>(rnd.Uniform(3));
for (int f = 0; f < num_files; ++f) {
const int n = 20 + static_cast<int>(rnd.Uniform(300));
for (int i = 0; i < n; ++i) {
std::string k = rand_key(/*allow_no_prefix=*/false);
keys.push_back(k);
if (rnd.OneIn(5)) {
ASSERT_OK(Delete(k));
model.erase(k);
} else {
std::string v = "v" + std::to_string(rnd.Next());
ASSERT_OK(Put(k, v));
model[k] = v;
}
if (rnd.OneIn(20)) {
snapshots.push_back(db_->GetSnapshot());
}
}
ASSERT_OK(Flush());
}
// Sometimes also ingest a file whose keys overlap the DB, so it is
// assigned a global sequence number (applied to block keys when seeking).
if (rnd.OneIn(2)) {
std::map<std::string, std::string, RevLess> file_kvs;
const int n = 10 + static_cast<int>(rnd.Uniform(100));
for (int i = 0; i < n; ++i) {
file_kvs[rand_key(/*allow_no_prefix=*/false)] =
"i" + std::to_string(rnd.Next());
}
const std::string file = dbname_ + "/ingest.sst";
SstFileWriter writer(EnvOptions(), db_options);
ASSERT_OK(writer.Open(file));
for (const auto& kv : file_kvs) {
ASSERT_OK(writer.Put(kv.first, kv.second));
keys.push_back(kv.first);
model[kv.first] = kv.second;
}
ASSERT_OK(writer.Finish());
IngestExternalFileOptions ifo;
ifo.allow_global_seqno = true;
ASSERT_OK(db_->IngestExternalFile({file}, ifo));
}
auto verify = [&]() {
for (const auto& k : keys) {
auto it = model.find(k);
const std::string expected =
it == model.end() ? "NOT_FOUND" : it->second;
ASSERT_EQ(expected, Get(k)) << "key=" << Slice(k).ToString(true);
}
std::unique_ptr<Iterator> iter(db_->NewIterator(ReadOptions()));
auto mit = model.begin();
for (iter->SeekToFirst(); iter->Valid(); iter->Next(), ++mit) {
ASSERT_TRUE(mit != model.end());
ASSERT_EQ(Slice(mit->first).ToString(true), iter->key().ToString(true));
}
ASSERT_OK(iter->status());
ASSERT_TRUE(mit == model.end());
auto rit = model.rbegin();
for (iter->SeekToLast(); iter->Valid(); iter->Prev(), ++rit) {
ASSERT_TRUE(rit != model.rend());
ASSERT_EQ(Slice(rit->first).ToString(true), iter->key().ToString(true));
}
ASSERT_OK(iter->status());
ASSERT_TRUE(rit == model.rend());
for (int p = 0; p < 400; ++p) {
const std::string target = rand_key(/*allow_no_prefix=*/true);
SCOPED_TRACE("target=" + Slice(target).ToString(true));
iter->Seek(target);
auto lb = model.lower_bound(target);
if (lb == model.end()) {
ASSERT_FALSE(iter->Valid());
} else {
ASSERT_TRUE(iter->Valid());
ASSERT_EQ(Slice(lb->first).ToString(true),
iter->key().ToString(true));
}
ASSERT_OK(iter->status());
iter->SeekForPrev(target);
auto ub = model.upper_bound(target);
if (ub == model.begin()) {
ASSERT_FALSE(iter->Valid());
} else {
--ub;
ASSERT_TRUE(iter->Valid());
ASSERT_EQ(Slice(ub->first).ToString(true),
iter->key().ToString(true));
}
ASSERT_OK(iter->status());
}
};
verify();
if (HasFatalFailure()) {
return;
}
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
verify();
if (HasFatalFailure()) {
return;
}
if (!old_writer) {
// Blocks from the current writer never need the check to flag them.
ASSERT_EQ(flagged_before, counts.data.load() + counts.index.load());
}
}
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
// Some blocks from the old writer needed the full-key search.
EXPECT_GT(counts.data.load() + counts.index.load(), 0);
}
TEST_F(DBEtc3Test, ManifestRollOver) {
do {
Options options;
@@ -371,6 +830,301 @@ TEST_F(DBEtc3Test, AutoTuneManifestSize) {
}
}
// Tests for the DB ownership metadata recorded for SST files
// (FileMetaData::db_ownership_start_time and data_file_provenance)
class DBOwnershipTest : public DBEtc3Test {
protected:
uint64_t Now() {
int64_t now = 0;
EXPECT_OK(env_->GetCurrentTime(&now));
return static_cast<uint64_t>(now);
}
// The MANIFEST record and level of the file in the default column family
// whose smallest user key is `key`
std::pair<FileMetaData, int> FileStartingWith(const std::string& key) {
std::vector<std::vector<FileMetaData>> metadata;
dbfull()->TEST_GetFilesMetaData(db_->DefaultColumnFamily(), &metadata);
for (size_t level = 0; level < metadata.size(); ++level) {
for (const auto& f : metadata[level]) {
if (f.smallest.user_key() == key) {
return {f, static_cast<int>(level)};
}
}
}
ADD_FAILURE() << "No file starting with " << key;
return {FileMetaData(), -1};
}
static DataFileProvenance Decode(const FileMetaData& f) {
return DataFileProvenance::DecodeFrom(f.data_file_provenance);
}
static DataFileProvenance OriginOnly(DataFileProvenance::Origin origin) {
DataFileProvenance info;
info.origin = origin;
return info;
}
// Expects `after` to be the same file as `before`, with the same contents
// and DB ownership, plus DataFileProvenance flag bits `added_flags`
static void ExpectSameFile(const FileMetaData& before,
const FileMetaData& after, uint32_t added_flags) {
EXPECT_EQ(after.fd.GetNumber(), before.fd.GetNumber());
EXPECT_EQ(after.file_creation_time, before.file_creation_time);
EXPECT_EQ(after.db_ownership_start_time, before.db_ownership_start_time);
EXPECT_EQ(after.data_file_provenance,
before.data_file_provenance | added_flags);
}
};
TEST_F(DBOwnershipTest, FlushAndCompactionOutputs) {
Options options = CurrentOptions();
options.disable_auto_compactions = true;
env_->SetMockSleep();
DestroyAndReopen(options);
const uint64_t start = Now();
ASSERT_OK(Put("a", "1"));
ASSERT_OK(Flush());
const FileMetaData flushed = FileStartingWith("a").first;
EXPECT_GE(flushed.db_ownership_start_time, start);
EXPECT_EQ(flushed.db_ownership_start_time, flushed.file_creation_time);
EXPECT_EQ(Decode(flushed), OriginOnly(DataFileProvenance::Origin::kFlush));
// Reported in public metadata
std::vector<LiveFileMetaData> live_files;
db_->GetLiveFilesMetaData(&live_files);
ASSERT_EQ(live_files.size(), 1U);
EXPECT_EQ(live_files[0].db_ownership_start_time,
flushed.db_ownership_start_time);
EXPECT_EQ(live_files[0].data_file_provenance, flushed.data_file_provenance);
ColumnFamilyMetaData cf_meta;
db_->GetColumnFamilyMetaData(&cf_meta);
ASSERT_EQ(cf_meta.levels[0].files.size(), 1U);
EXPECT_EQ(cf_meta.levels[0].files[0].db_ownership_start_time,
flushed.db_ownership_start_time);
EXPECT_EQ(cf_meta.levels[0].files[0].data_file_provenance,
flushed.data_file_provenance);
// Flushed during WAL recovery
ASSERT_OK(Put("b", "1"));
Reopen(options);
const FileMetaData recovered = FileStartingWith("b").first;
EXPECT_GE(recovered.db_ownership_start_time, start);
EXPECT_EQ(recovered.db_ownership_start_time, recovered.file_creation_time);
EXPECT_EQ(Decode(recovered), OriginOnly(DataFileProvenance::Origin::kFlush));
// Compactions that take a long time to finish after creating their output
ASSERT_OK(Put("a", "2"));
ASSERT_OK(Flush());
SyncPoint::GetInstance()->SetCallBack(
"DBImpl::BackgroundCompaction:NonTrivial:AfterRun",
[&](void*) { env_->MockSleepForSeconds(1000); });
SyncPoint::GetInstance()->EnableProcessing();
CompactRangeOptions cro;
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
// The DB has held the output since creating it
const FileMetaData compacted = FileStartingWith("a").first;
EXPECT_EQ(compacted.db_ownership_start_time, compacted.file_creation_time);
EXPECT_LE(compacted.db_ownership_start_time + 1000, Now());
EXPECT_EQ(Decode(compacted),
OriginOnly(DataFileProvenance::Origin::kCompaction));
}
TEST_F(DBOwnershipTest, MovesPreserveOwnership) {
Options options = CurrentOptions();
options.disable_auto_compactions = true;
options.level_compaction_dynamic_level_bytes = true;
env_->SetMockSleep();
CompactRangeOptions force_compact;
force_compact.bottommost_level_compaction = BottommostLevelCompaction::kForce;
{
SCOPED_TRACE("PromoteL0");
DestroyAndReopen(options);
ASSERT_OK(Put("a", "1"));
ASSERT_OK(Flush());
const FileMetaData before = FileStartingWith("a").first;
env_->MockSleepForSeconds(1000);
ASSERT_OK(
experimental::PromoteL0(db_.get(), db_->DefaultColumnFamily(), 2));
const auto& [after, level] = FileStartingWith("a");
EXPECT_EQ(level, 2);
ExpectSameFile(before, after, kProvenanceMovedBetweenLevels);
}
{
SCOPED_TRACE("Trivial move");
DestroyAndReopen(options);
ASSERT_OK(Put("a", "1"));
ASSERT_OK(Flush());
const FileMetaData before = FileStartingWith("a").first;
env_->MockSleepForSeconds(1000);
std::atomic<int> trivial_moves{0};
SyncPoint::GetInstance()->SetCallBack(
"DBImpl::BackgroundCompaction:TrivialMove",
[&](void*) { ++trivial_moves; });
SyncPoint::GetInstance()->EnableProcessing();
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
EXPECT_GT(trivial_moves.load(), 0);
const auto& [after, level] = FileStartingWith("a");
EXPECT_GT(level, 0);
ExpectSameFile(before, after, kProvenanceMovedBetweenLevels);
}
{
SCOPED_TRACE("CompactRange with change_level, and reopen");
DestroyAndReopen(options);
// A compaction output that has not been moved
ASSERT_OK(Put("a", "1"));
ASSERT_OK(Flush());
ASSERT_OK(Put("a", "2"));
ASSERT_OK(Flush());
ASSERT_OK(db_->CompactRange(force_compact, nullptr, nullptr));
const auto& [before, before_level] = FileStartingWith("a");
ASSERT_EQ(Decode(before),
OriginOnly(DataFileProvenance::Origin::kCompaction));
ASSERT_GT(before_level, 1);
env_->MockSleepForSeconds(1000);
CompactRangeOptions refit;
refit.change_level = true;
refit.target_level = 1;
ASSERT_OK(db_->CompactRange(refit, nullptr, nullptr));
const auto& [after, level] = FileStartingWith("a");
EXPECT_EQ(level, 1);
ExpectSameFile(before, after, kProvenanceMovedBetweenLevels);
// Preserved by replaying the MANIFEST and then by the new MANIFEST written
// on open
for (int i = 0; i < 2; ++i) {
Reopen(options);
ExpectSameFile(after, FileStartingWith("a").first, 0);
}
}
{
SCOPED_TRACE("Level migration on open");
options.level_compaction_dynamic_level_bytes = false;
DestroyAndReopen(options);
ASSERT_OK(Put("a", "1"));
ASSERT_OK(Flush());
ASSERT_OK(Put("a", "2"));
ASSERT_OK(Flush());
ASSERT_OK(db_->CompactRange(force_compact, nullptr, nullptr));
const auto& [before, before_level] = FileStartingWith("a");
ASSERT_EQ(Decode(before),
OriginOnly(DataFileProvenance::Origin::kCompaction));
ASSERT_LT(before_level, options.num_levels - 1);
env_->MockSleepForSeconds(1000);
options.level_compaction_dynamic_level_bytes = true;
// Required for the migration
options.disable_auto_compactions = false;
Reopen(options);
const auto& [after, level] = FileStartingWith("a");
EXPECT_EQ(level, options.num_levels - 1);
ExpectSameFile(before, after, kProvenanceMovedBetweenLevels);
}
}
TEST_F(DBOwnershipTest, FIFOTemperatureChangeCopy) {
Options options = CurrentOptions();
options.compaction_style = kCompactionStyleFIFO;
options.num_levels = 1;
options.max_open_files = -1;
options.level0_file_num_compaction_trigger = 2;
CompactionOptionsFIFO fifo_options;
fifo_options.file_temperature_age_thresholds = {{Temperature::kCold, 1000}};
fifo_options.max_table_files_size = 100000000;
fifo_options.allow_trivial_copy_when_change_temperature = true;
options.compaction_options_fifo = fifo_options;
env_->SetMockSleep();
DestroyAndReopen(options);
ASSERT_OK(Put("a", "1"));
ASSERT_OK(Flush());
ASSERT_OK(Put("b", "1"));
ASSERT_OK(Flush());
ASSERT_OK(dbfull()->TEST_WaitForCompact());
const FileMetaData before = FileStartingWith("a").first;
ASSERT_NE(before.temperature, Temperature::kCold);
env_->MockSleepForSeconds(2000);
ASSERT_OK(Put("c", "1"));
ASSERT_OK(Flush());
ASSERT_OK(dbfull()->TEST_WaitForCompact());
const FileMetaData after = FileStartingWith("a").first;
ASSERT_EQ(after.temperature, Temperature::kCold);
EXPECT_NE(after.fd.GetNumber(), before.fd.GetNumber());
// Same contents...
EXPECT_EQ(after.file_creation_time, before.file_creation_time);
// ...in a new file for the DB to hold
EXPECT_GE(after.db_ownership_start_time,
before.db_ownership_start_time + 2000);
EXPECT_EQ(after.data_file_provenance,
before.data_file_provenance | kProvenanceTemperatureChangedByCopy);
}
TEST_F(DBOwnershipTest, TemperatureUpdatedFromFileSystem) {
auto test_fs = std::make_shared<FileTemperatureTestFS>(env_->GetFileSystem());
std::unique_ptr<Env> env(new CompositeEnvWrapper(env_, test_fs));
Options options = CurrentOptions();
options.disable_auto_compactions = true;
options.env = env.get();
DestroyAndReopen(options);
ASSERT_OK(Put("a", "1"));
ASSERT_OK(Flush());
const FileMetaData before = FileStartingWith("a").first;
ASSERT_NE(before.temperature, Temperature::kCold);
test_fs->OverrideSstFileTemperature(before.fd.GetNumber(),
Temperature::kCold);
Close();
experimental::UpdateManifestForFilesStateOptions update_opts;
update_opts.update_temperatures = true;
ASSERT_OK(experimental::UpdateManifestForFilesState(
options, dbname_,
{ColumnFamilyDescriptor(kDefaultColumnFamilyName, options)},
update_opts));
Reopen(options);
const FileMetaData after = FileStartingWith("a").first;
EXPECT_EQ(after.temperature, Temperature::kCold);
ExpectSameFile(before, after, kProvenanceTemperatureUpdatedFromFileSystem);
Close();
}
TEST_F(DBOwnershipTest, Repair) {
Options options = CurrentOptions();
options.disable_auto_compactions = true;
DestroyAndReopen(options);
ASSERT_OK(Put("a", "1"));
ASSERT_OK(Flush());
const FileMetaData flushed = FileStartingWith("a").first;
// Left in the WAL, for repair to write to a table
ASSERT_OK(Put("b", "1"));
Close();
ASSERT_OK(RepairDB(dbname_, options));
Reopen(options);
// Ownership of tables from before the repair was recorded in the lost
// MANIFEST
const FileMetaData repaired = FileStartingWith("a").first;
// Restored from the table property, recorded when the flush finished
EXPECT_GE(repaired.file_creation_time, flushed.file_creation_time);
EXPECT_EQ(repaired.db_ownership_start_time, kUnknownDbOwnershipStartTime);
DataFileProvenance expected;
expected.recorded_by_repair = true;
EXPECT_EQ(Decode(repaired), expected);
const FileMetaData from_wal = FileStartingWith("b").first;
EXPECT_GT(from_wal.file_creation_time, 0U);
EXPECT_EQ(from_wal.db_ownership_start_time, from_wal.file_creation_time);
expected.origin = DataFileProvenance::Origin::kFlush;
EXPECT_EQ(Decode(from_wal), expected);
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+1 -1
View File
@@ -3898,7 +3898,7 @@ TEST_F(DBFlushTest, DBStuckAfterAtomicFlushError) {
// Resume starts and waits for background job to finish, i.e., Flush1
// Fill memtable again, trigger Flush2 for mem0
// Flush2 will get error status, and not rollback mem0, see code in
// https://github.com/facebook/rocksdb/blob/b927ba5936216861c2c35ab68f50ba4a78e65747/db/db_impl/db_impl_compaction_flush.cc#L725
// https://github.com/meta/rocksdb/blob/b927ba5936216861c2c35ab68f50ba4a78e65747/db/db_impl/db_impl_compaction_flush.cc#L725
//
// DB is stuck since mem0 can never be picked now
//
+66
View File
@@ -407,6 +407,72 @@ TEST_F(DBFollowerTest, RetryCatchupManifestRollover) {
SyncPoint::GetInstance()->DisableProcessing();
}
TEST_F(DBFollowerTest, CatchUpKeepsViewWhenNewManifestListsDeletedFiles) {
Options opts = CurrentOptions();
opts.disable_auto_compactions = true;
opts.max_manifest_file_size = 1;
opts.max_manifest_space_amp_pct = 0;
Reopen(opts);
ASSERT_OK(Put("foo", "v1"));
ASSERT_OK(Flush());
SyncPoint::GetInstance()->LoadDependency({
{"DBImplFollower::TryCatchupWithLeader:Begin1", "Leader::Start"},
{"Leader::Done", "DBImplFollower::TryCatchupWithLeader:Begin2"},
{"DBImplFollower::TryCatchupWithLeader:End",
"Follower::WaitForCatchup:1"},
});
SyncPoint::GetInstance()->EnableProcessing();
ASSERT_OK(OpenAsFollower());
ASSERT_EQ(FollowerGet("foo"), "v1");
std::atomic<bool> compacted{false};
SyncPoint::GetInstance()->SetCallBack(
"VersionEditHandlerPointInTime::MaybeCreateVersionBeforeApplyEdit:Begin1",
[&](void*) {
if (compacted.exchange(true)) {
return;
}
std::vector<LiveFileMetaData> tables;
db_->GetLiveFilesMetaData(&tables);
CompactRangeOptions compact_options;
compact_options.bottommost_level_compaction =
BottommostLevelCompaction::kForce;
ASSERT_OK(db_->CompactRange(compact_options, nullptr, nullptr));
ASSERT_OK(dbfull()->TEST_WaitForPurge());
for (const auto& table : tables) {
const Status table_status =
env_->FileExists(MakeTableFileName(dbname_, table.file_number));
ASSERT_TRUE(table_status.IsNotFound()) << table_status.ToString();
}
});
TEST_SYNC_POINT("Leader::Start");
// The second flush rolls the MANIFEST again, so that the newest one lists
// the file of the first flush in its snapshot.
ASSERT_OK(Put("bar", "v1"));
ASSERT_OK(Flush());
ASSERT_OK(Put("baz", "v1"));
ASSERT_OK(Flush());
TEST_SYNC_POINT("Leader::Done");
TEST_SYNC_POINT("Follower::WaitForCatchup:1");
ASSERT_TRUE(compacted.load());
ASSERT_EQ(FollowerGet("foo"), "v1");
SyncPoint::GetInstance()->LoadDependency({
{"DBImplFollower::TryCatchupWithLeader:End",
"Follower::WaitForCatchup:2"},
});
TEST_SYNC_POINT("Follower::WaitForCatchup:2");
ASSERT_EQ(FollowerGet("foo"), "v1");
ASSERT_EQ(FollowerGet("bar"), "v1");
ASSERT_EQ(FollowerGet("baz"), "v1");
SyncPoint::GetInstance()->ClearAllCallBacks();
SyncPoint::GetInstance()->DisableProcessing();
}
// This test creates 4 L0 files and compacts them. The follower, during catchup,
// successfully instantiates 4 Versions corresponding to the 4 files (but
// doesn't install them yet), followed by deleting those 4 and adding a new
+46
View File
@@ -5,6 +5,8 @@
#include "db/db_impl/compacted_db_impl.h"
#include <algorithm>
#include "db/blob/blob_fetcher.h"
#include "db/db_impl/db_impl.h"
#include "db/db_impl/db_impl_metadata.h"
@@ -51,6 +53,18 @@ Status CompactedDBImpl::GetWithMetadata(const ReadOptions& options,
return Get(options, column_family, key, value, timestamp);
}
Status CompactedDBImpl::GetEntityWithMetadata(const ReadOptions& options,
ColumnFamilyHandle* column_family,
const Slice& key,
PinnableWideColumns* columns,
OutputMetadata* output_metadata) {
Status s = InitGetEntityOutputMetadata(output_metadata);
if (!s.ok()) {
return s;
}
return GetEntity(options, column_family, key, columns);
}
void CompactedDBImpl::MultiGetWithMetadata(
const ReadOptions& options, size_t num_keys,
ColumnFamilyHandle* const* column_families, const Slice* keys,
@@ -75,6 +89,38 @@ void CompactedDBImpl::MultiGetWithMetadata(
sorted_input);
}
void CompactedDBImpl::MultiGetEntityWithMetadata(
const ReadOptions& options, size_t num_keys,
ColumnFamilyHandle* const* column_families, const Slice* keys,
PinnableWideColumns* results, Status* statuses,
MultiGetOutputMetadata* output_metadata, bool sorted_input) {
const Status s = InitMultiGetEntityOutputMetadata(num_keys, output_metadata);
if (!s.ok()) {
std::fill_n(statuses, num_keys, s);
return;
}
MultiGetEntityWithoutNewerVersionTracking(this, options, num_keys,
column_families, keys, results,
statuses, sorted_input);
}
void CompactedDBImpl::MultiGetEntityWithMetadata(
const ReadOptions& options, size_t num_keys, const Slice* keys,
PinnableAttributeGroups* results, MultiGetOutputMetadata* output_metadata) {
const size_t total_count = CountAttributeGroups(num_keys, results);
const Status s =
InitMultiGetEntityOutputMetadata(total_count, output_metadata);
if (!results) {
s.PermitUncheckedError();
return;
}
if (!s.ok()) {
SetAttributeGroupsStatus(num_keys, results, s);
return;
}
MultiGetEntity(options, num_keys, keys, results);
}
Status CompactedDBImpl::Init(const Options& options) {
SuperVersionContext sv_context(/* create_superversion */ true);
mutex_.Lock();
+19
View File
@@ -39,6 +39,12 @@ class CompactedDBImpl : public DBImpl {
PinnableSlice* value,
OutputMetadata* output_metadata) override;
using DB::GetEntityWithMetadata;
Status GetEntityWithMetadata(const ReadOptions& options,
ColumnFamilyHandle* column_family,
const Slice& key, PinnableWideColumns* columns,
OutputMetadata* output_metadata) override;
using DB::MultiGet;
// Note that CompactedDBImpl::MultiGet is not the optimized version of
// MultiGet to use.
@@ -58,6 +64,19 @@ class CompactedDBImpl : public DBImpl {
MultiGetOutputMetadata* output_metadata,
const bool sorted_input) override;
using DB::MultiGetEntityWithMetadata;
void MultiGetEntityWithMetadata(const ReadOptions& options, size_t num_keys,
ColumnFamilyHandle* const* column_families,
const Slice* keys,
PinnableWideColumns* results,
Status* statuses,
MultiGetOutputMetadata* output_metadata,
bool sorted_input) override;
void MultiGetEntityWithMetadata(
const ReadOptions& options, size_t num_keys, const Slice* keys,
PinnableAttributeGroups* results,
MultiGetOutputMetadata* output_metadata) override;
using DBImpl::Put;
Status Put(const WriteOptions& /*options*/,
ColumnFamilyHandle* /*column_family*/, const Slice& /*key*/,
+1073 -261
View File
File diff suppressed because it is too large. Load diff
+230 -22
View File
@@ -8,6 +8,7 @@
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#pragma once
#include <array>
#include <atomic>
#include <chrono>
#include <cstdint>
@@ -61,6 +62,7 @@
#include "rocksdb/attribute_groups.h"
#include "rocksdb/db.h"
#include "rocksdb/env.h"
#include "rocksdb/live_tune.h"
#include "rocksdb/memtablerep.h"
#include "rocksdb/status.h"
#include "rocksdb/trace_reader_writer.h"
@@ -73,9 +75,11 @@
#include "util/autovector.h"
#include "util/coro_utils.h"
#include "util/hash.h"
#include "util/hash_containers.h"
#include "util/repeatable_thread.h"
#include "util/stop_watch.h"
#include "util/thread_local.h"
#include "util/threadpool_imp.h"
#if USE_COROUTINES
#include "rocksdb/coro_db.h"
@@ -89,6 +93,7 @@ class Cleanable;
class FileIngestionHandleImpl;
class SameFileBlobReader;
class InMemoryStatsHistoryIterator;
class LiveTuner;
class MemTable;
class PersistentStatsHistoryIterator;
class TableCache;
@@ -320,6 +325,12 @@ class DBImpl : public DB
const Slice& key,
PinnableAttributeGroups* result);
using DB::GetEntityWithMetadata;
Status GetEntityWithMetadata(const ReadOptions& _read_options,
ColumnFamilyHandle* column_family,
const Slice& key, PinnableWideColumns* columns,
OutputMetadata* output_metadata) override;
Status GetEntityLazy(const ReadOptions& options,
ColumnFamilyHandle* column_family, const Slice& key,
LazyWideColumns* result) override;
@@ -384,6 +395,20 @@ class DBImpl : public DB
const Slice* keys,
PinnableAttributeGroups* results) override;
using DB::MultiGetEntityWithMetadata;
void MultiGetEntityWithMetadata(const ReadOptions& _read_options,
size_t num_keys,
ColumnFamilyHandle* const* column_families,
const Slice* keys,
PinnableWideColumns* results,
Status* statuses,
MultiGetOutputMetadata* output_metadata,
bool sorted_input = false) override;
void MultiGetEntityWithMetadata(
const ReadOptions& _read_options, size_t num_keys, const Slice* keys,
PinnableAttributeGroups* results,
MultiGetOutputMetadata* output_metadata) override;
void MultiGetEntityLazy(const ReadOptions& options,
ColumnFamilyHandle* column_family, size_t num_keys,
const Slice* keys, LazyWideColumnsBatch* result,
@@ -541,6 +566,12 @@ class DBImpl : public DB
Status SetDBOptions(
const std::unordered_map<std::string, std::string>& options_map) override;
// See rocksdb/live_tune.h.
Status StartLiveTuneCycle(const LiveTuneOptions& options);
Status GetLiveTuneStatus(LiveTuneStatus* status);
Status GetLiveTuneAppliedOptions(std::map<std::string, std::string>* options);
Status StopLiveTuneCycle();
using DB::NumberLevels;
int NumberLevels(ColumnFamilyHandle* column_family) override;
using DB::Level0StopWriteTrigger;
@@ -765,6 +796,17 @@ class DBImpl : public DB
using DB::EndTrace;
Status EndTrace() override;
// Like StartTrace(), but returns Busy rather than replacing a trace that is
// already running. `*trace_id` identifies the started trace for
// EndTraceIfCurrent().
Status StartTraceIfNone(const TraceOptions& options,
std::unique_ptr<TraceWriter>&& trace_writer,
uint64_t* trace_id);
// Ends the trace `trace_id` from StartTraceIfNone() if it is still the one
// running. Incomplete if a later StartTrace() replaced it or EndTrace()
// ended it.
Status EndTraceIfCurrent(uint64_t trace_id);
using DB::NewDefaultReplayer;
Status NewDefaultReplayer(const std::vector<ColumnFamilyHandle*>& handles,
std::unique_ptr<TraceReader>&& reader,
@@ -986,10 +1028,12 @@ class DBImpl : public DB
bool* is_blob_index);
Status TraceIteratorSeek(const uint32_t& cf_id, const Slice& key,
const Slice& lower_bound, const Slice upper_bound);
const Slice& lower_bound, const Slice upper_bound,
const TracedReadOptions& read_options);
Status TraceIteratorSeekForPrev(const uint32_t& cf_id, const Slice& key,
const Slice& lower_bound,
const Slice upper_bound);
const Slice upper_bound,
const TracedReadOptions& read_options);
// Similar to GetSnapshot(), but also lets the db know that this snapshot
// will be used for transaction write-conflict checking. The DB can then
@@ -1083,6 +1127,12 @@ class DBImpl : public DB
uint64_t MinLogNumberToRecycle();
// Whether the WAL is still in logs_, i.e. its writer may still be open. Such
// a WAL must not be recycled yet: closing the old writer truncates the file
// to its old size.
// REQUIRES: wal_write_mutex_ not held
bool IsWalInLogs(uint64_t wal_number);
// Returns the lower bound file number for SSTs that won't be deleted, even if
// they're obsolete. This lower bound is used internally to prevent newly
// created flush/compaction output files from being deleted before they're
@@ -1115,17 +1165,25 @@ class DBImpl : public DB
// Schedule a background job to actually delete obsolete files.
void SchedulePurge();
const SnapshotList& snapshots() const { return snapshots_; }
// Thread-safe; does not require any DB lock.
const ShardedSnapshotList& snapshots() const { return snapshots_; }
// load list of snapshots to `snap_vector` that is no newer than `max_seq`
// in ascending order.
// Loads snapshots no newer than `max_seq` into `snap_vector` in ascending
// order. Returns an exact oldest live snapshot sequence, or the last
// published sequence if there are no snapshots, from a separate all-shard
// scan. `snapshot_release_count` receives the release count observed by that
// exact scan when non-null.
// `oldest_write_conflict_snapshot` is filled with the oldest snapshot
// which satisfies SnapshotImpl.is_write_conflict_boundary_ = true.
void LoadSnapshots(std::vector<SequenceNumber>* snap_vector,
SequenceNumber* oldest_write_conflict_snapshot,
const SequenceNumber& max_seq) const {
InstrumentedMutexLock l(mutex());
// Thread-safe; callers that use the return values for DB-wide maintenance
// must hold mutex_.
SequenceNumber LoadSnapshots(std::vector<SequenceNumber>* snap_vector,
SequenceNumber* oldest_write_conflict_snapshot,
const SequenceNumber& max_seq,
uint64_t* snapshot_release_count) const {
snapshots().GetAll(snap_vector, oldest_write_conflict_snapshot, max_seq);
return snapshots().OldestOr([this]() { return GetLastPublishedSequence(); },
snapshot_release_count);
}
const ImmutableDBOptions& immutable_db_options() const {
@@ -1319,6 +1377,12 @@ class DBImpl : public DB
void SetRecoverableStatePreReleaseCallback(PreReleaseCallback* callback);
InstrumentedMutex* mutex() const { return &mutex_; }
const std::atomic<uint64_t>* GetMutexHoldLogThreshold() const {
return &mutex_hold_log_threshold_us_;
}
const std::shared_ptr<SlowMutexLoggingContext>& GetSlowMutexLogging() const {
return slow_mutex_logging_;
}
// Initialize a brand new DB. The DB directory is expected to be empty before
// calling it. Push new manifest file name into `new_filenames`.
@@ -1673,8 +1737,12 @@ class DBImpl : public DB
FileSystemPtr fs_;
MutableDBOptions mutable_db_options_;
Statistics* stats_;
std::atomic<uint64_t> mutex_hold_log_threshold_us_;
std::shared_ptr<SlowMutexLoggingContext> slow_mutex_logging_;
RecoveredTransactionMap recovered_transactions_;
std::unique_ptr<Tracer> tracer_;
// Incremented by every trace started; guarded by trace_mutex_.
uint64_t trace_id_ = 0;
InstrumentedMutex trace_mutex_;
BlockCacheTracer block_cache_tracer_;
@@ -2235,9 +2303,12 @@ class DBImpl : public DB
friend class CompactedDBImpl;
friend class DBImplFollower;
#ifndef NDEBUG
friend class DedicatedCompactionPoolTest;
friend class DBTest_ConcurrentFlushWAL_Test;
friend class DBTest_MixedSlowdownOptionsStop_Test;
friend class DBCompactionTest_CompactBottomLevelFilesWithDeletions_Test;
friend class
DBCompactionTest_StandaloneRangeTombstoneSnapshotAdvanceRequeuesScheduling_Test;
friend class DBCompactionTest_CompactionDuringShutdown_Test;
friend class DBCompactionTest_DelayCompactBottomLevelFilesWithDeletions_Test;
friend class DBCompactionTest_DisableCompactBottomLevelFiles_Test;
@@ -2500,6 +2571,7 @@ class DBImpl : public DB
// background compaction takes ownership of `prepicked_compaction`.
PrepickedCompaction* prepicked_compaction;
Env::Priority compaction_pri_;
bool separate_remote_pool = false;
};
static bool IsRecoveryFlush(FlushReason flush_reason) {
@@ -2828,6 +2900,12 @@ class DBImpl : public DB
// fence that is about to switch memtables.
void MaybeSyncLastSequenceWithAllocatedForRecovery(FlushReason flush_reason);
// Like the memtable-switch fence, but does not wait for a write queue whose
// leader is in a write stall: that leader has not allocated sequence numbers
// yet, and it can be waiting for this recovery.
// REQUIRES: mutex locked, two_write_queues
void SyncLastSequenceWithAllocatedAtRecoveryStart();
Status RetryFlushesForErrorRecovery(FlushReason flush_reason, bool wait);
// Wait until flushing this column family won't stall writes
@@ -3031,6 +3109,48 @@ class DBImpl : public DB
bool track);
void MaybeScheduleFlushOrCompaction();
bool UseSeparateRemoteCompactionPool() const {
mutex_.AssertHeld();
return immutable_db_options_.compaction_service != nullptr &&
mutable_db_options_.use_separate_remote_compaction_pool;
}
int LocalCompactionsScheduled() const {
mutex_.AssertHeld();
return bg_compaction_scheduled_ + bg_bottom_compaction_scheduled_;
}
int CompactionsScheduled() const {
mutex_.AssertHeld();
return LocalCompactionsScheduled() + remote_coordinators_scheduled_;
}
Status ScheduleCompaction(CompactionArg* arg, void* tag);
class RemoteCompactionCompletionGuard;
void ScheduleRemoteCompaction(std::function<void()> work);
void ScheduleRemoteCompactionLocked(std::function<void()> work);
int RemoteCompactionPoolBaseline() const;
void ShrinkRemoteCompactionPool();
void ReleaseRemoteCompactionPool();
void ConfigureCompactionExecutors(CompactionJob* job, ColumnFamilyData* cfd,
const std::atomic<bool>& canceled,
bool is_manual, bool separate_remote_pool);
Status LocalCompactionStatus(ColumnFamilyData* cfd,
const std::atomic<bool>& canceled);
Status RunLocalCompaction(std::function<void()> work, Env::Priority priority,
ColumnFamilyData* cfd,
const std::atomic<bool>& canceled, bool is_manual);
IOStatus CopyTrivialCompactionFiles(Compaction* c, LogBuffer* log_buffer,
std::vector<FileMetaData>* out_files,
int job_id, bool db_mutex_held);
struct LocalCompactionTask {
DBImpl* db;
ColumnFamilyData* cfd;
const std::atomic<bool>* canceled;
std::function<void()> work;
Env::Priority priority;
Status status;
bool done = false;
};
static void BGWorkLocalCompaction(void* arg);
static void UnscheduleLocalCompaction(void* arg);
BackgroundJobPressure CaptureBackgroundJobPressure() const;
void NotifyOnBackgroundJobPressureChanged();
@@ -3098,7 +3218,8 @@ class DBImpl : public DB
static void UnscheduleCompactionCallback(void* arg);
static void UnscheduleFlushCallback(void* arg);
void BackgroundCallCompaction(PrepickedCompaction* prepicked_compaction,
Env::Priority thread_pri);
Env::Priority thread_pri,
bool separate_remote_pool);
void BackgroundCallFlush(Env::Priority thread_pri);
void BackgroundCallPurge();
// Recursively removes all children of the DB session temporary directory.
@@ -3107,7 +3228,9 @@ class DBImpl : public DB
Status BackgroundCompaction(bool* madeProgress, JobContext* job_context,
LogBuffer* log_buffer,
PrepickedCompaction* prepicked_compaction,
Env::Priority thread_pri);
Env::Priority thread_pri,
bool separate_remote_pool,
int* deferred_compactions);
Status BackgroundFlush(bool* madeProgress, JobContext* job_context,
LogBuffer* log_buffer, FlushReason* reason,
bool* flush_rescheduled_to_retain_udt,
@@ -3198,8 +3321,23 @@ class DBImpl : public DB
// WALs with log number up to up_to are not synced successfully.
void MarkLogsNotSynced(uint64_t up_to);
enum class SnapshotLockMode {
kTryWithoutDBMutex,
kDBMutexHeld,
kAcquireDBMutex,
};
SnapshotImpl* GetSnapshotImpl(bool is_write_conflict_boundary,
bool lock = true);
SnapshotLockMode lock_mode);
// Updates compaction state after the oldest snapshot advances and also
// reconciles `installed_cfd`, when non-null, if its Version has not processed
// the current oldest snapshot yet. Requires mutex_.
void UpdateOldestSnapshotAndScheduleWork(SequenceNumber oldest_snapshot,
uint64_t snapshot_release_count,
ColumnFamilyData* installed_cfd);
bool ReconcileOldestSnapshotForColumnFamily(ColumnFamilyData* cfd,
SequenceNumber oldest_snapshot);
void UpdateSnapshotCompactionThresholds();
// If snapshot_seq != kMaxSequenceNumber, then this function can only be
// called from the write thread that publishes sequence numbers to readers.
@@ -3235,6 +3373,7 @@ class DBImpl : public DB
// See also InstallSuperVersionForConfigChange().
void InstallSuperVersionAndScheduleWork(
ColumnFamilyData* cfd, SuperVersionContext* sv_context,
bool defer_snapshot_refresh,
std::optional<std::shared_ptr<SeqnoToTimeMapping>>
new_seqno_to_time_mapping = {});
@@ -3268,10 +3407,8 @@ class DBImpl : public DB
// Reserve the next 'num' file numbers for to-be-ingested external SST files,
// and return the current file_number in 'next_file_number'.
// Write a version edit to the MANIFEST.
Status ReserveFileNumbersBeforeIngestion(
ColumnFamilyData* cfd, uint64_t num,
std::unique_ptr<std::list<uint64_t>::iterator>& pending_output_elem,
uint64_t* next_file_number);
Status ReserveFileNumbersBeforeIngestion(ColumnFamilyData* cfd, uint64_t num,
uint64_t* next_file_number);
bool ShouldPurge(uint64_t file_number) const;
void MarkAsGrabbedForPurge(uint64_t file_number);
@@ -3464,6 +3601,24 @@ class DBImpl : public DB
Status IncreaseFullHistoryTsLowImpl(ColumnFamilyData* cfd,
std::string ts_low);
// Passes GetMergeOperands collection options to every lookup layer through
// merge_context. The caller's output array is not touched here.
static void PrepareGetMergeOperands(const GetImplOptions& get_impl_options,
MergeContext* merge_context);
// Publishes a successful GetMergeOperands lookup into the first
// *number_of_operands output slots; later slots, and the whole array on an
// Incomplete result, are left as the caller passed them. The output either
// owns a copy of every operand or shares one cleanup bundle that keeps all
// backing resources alive until the last PinnableSlice is reset. Returns
// Incomplete when the result exceeds the caller's capacity and a plain OK
// otherwise, so a caller must not let that OK replace a subcode it set.
Status FinalizeGetMergeOperands(const GetImplOptions& get_impl_options,
SuperVersion* super_version,
MergeContext* merge_context,
PinnedIteratorsManager* pinned_iters_manager,
size_t* bytes_read);
bool ShouldReferenceSuperVersion(const MergeContext& merge_context);
// Resolves a plain value whose current payload is an encoded direct-write
// blob index, either through `value` directly or through the default-column
@@ -3543,9 +3698,38 @@ class DBImpl : public DB
// Always acquired *before* DB mutex when this one is applicable.
InstrumentedMutex options_mutex_;
// True when the OPTIONS file can lag the options or column families in
// effect: the last OPTIONS file write failed, SetOptions() changed options
// and then failed before it wrote the file, or a column family batch with
// track_options_file_number_in_manifest stopped part way. Then SetOptions()
// and SetDBOptions() write the file even for a request that changes no
// option. A successful WriteOptionsFile() clears it, and so does a tracked
// column family change that commits a current snapshot. Protected by
// options_mutex_.
bool options_file_stale_ = false;
// True when the seqno-to-time worker can lag the time options of the column
// families: the last RegisterRecordSeqnoTimeWorker() failed, or SetOptions()
// changed options and then failed before it updated the worker. Then
// SetOptions() updates the worker even for a request that changes no option.
// A successful RegisterRecordSeqnoTimeWorker() clears it. Protected by
// options_mutex_.
bool seqno_time_worker_stale_ = false;
// Guards reads and writes to in-memory stats_history_.
InstrumentedMutex stats_history_mutex_;
// Guards live_tuner_ (the running or last live tune cycle) and
// live_tune_closed_. Never held while stopping a cycle (which joins its
// thread, and the cycle calls into the DB) nor while holding the DB mutex.
std::mutex live_tune_mutex_;
// The tuner holds a ColumnFamilyHandle, whose destructor locks the DB mutex,
// so it must be destroyed while the DB is still open: CloseHelper() takes
// it out and destroys it first. Never hand out this pointer, which would
// let the tuner outlive the DB.
std::shared_ptr<LiveTuner> live_tuner_;
bool live_tune_closed_ = false;
// In addition to mutex_, wal_write_mutex_ protects writes to logs_ and
// cur_wal_number_. With two_write_queues it also protects alive_wal_files_,
// and wal_empty_. Refer to the definition of each variable below for more
@@ -3736,7 +3920,8 @@ class DBImpl : public DB
// threads. Protected by wal_write_mutex_.
autovector<log::Writer*> wals_to_free_;
bool is_snapshot_supported_ = true;
// Written with mutex_ held. Read without mutex_ when creating a snapshot.
std::atomic<bool> is_snapshot_supported_{true};
std::map<uint64_t, std::map<std::string, uint64_t>> stats_history_;
@@ -3766,7 +3951,13 @@ class DBImpl : public DB
TrimHistoryScheduler trim_history_scheduler_;
SnapshotList snapshots_;
// Shard locks inside snapshots_ are leaf locks: they may be acquired while
// holding mutex_, but mutex_ must never be acquired while holding one.
// GetSnapshotImpl() reads a snapshot's sequence number inside the shard lock
// that adds it. Flush and compaction combine that ordering with their
// DB-mutex-protected input boundary and, when required, a managed snapshot or
// published-sequence pin in InitSnapshotContext().
ShardedSnapshotList snapshots_;
TimestampedSnapshotList timestamped_snapshots_;
@@ -3781,6 +3972,11 @@ class DBImpl : public DB
// State is protected with db mutex.
std::list<uint64_t> pending_outputs_;
// First file number -> reserved file count for external file ingestions.
// Full-scan purge snapshots protect these SSTs until commit or cleanup.
// State is protected with db mutex.
UnorderedMap<uint64_t, uint64_t> pending_ingestion_outputs_;
// Similar to pending_outputs_, FindObsoleteFiles()/PurgeObsoleteFiles() never
// deletes any OPTIONS file that has number bigger than any of the file number
// in min_options_file_numbers_.
@@ -3848,6 +4044,11 @@ class DBImpl : public DB
// count how many background compactions are running or have been scheduled
int bg_compaction_scheduled_ = 0;
std::shared_ptr<RemoteCompactionThreadPool> remote_compaction_pool_;
int remote_compaction_pool_jobs_ = 0;
int remote_coordinators_scheduled_ = 0;
std::array<int, 2> local_compactions_running_by_priority_{};
// stores the number of compactions are currently running
int num_running_compactions_ = 0;
@@ -3972,15 +4173,22 @@ class DBImpl : public DB
// Guard against multiple concurrent refitting
bool refitting_level_ = false;
// The min threshold to triggere bottommost compaction for removing
// garbages, among all column families.
// The minimum threshold for triggering bottommost compaction to remove
// garbage across all column families. Protected by mutex_.
SequenceNumber bottommost_files_mark_threshold_ = kMaxSequenceNumber;
// The min threshold to trigger compactions for standalone range deletion
// files that are marked for compaction.
// The minimum threshold for triggering compactions of standalone range
// deletion files across all column families. Protected by mutex_.
SequenceNumber standalone_range_deletion_files_mark_threshold_ =
kMaxSequenceNumber;
// Largest exact oldest-snapshot sequence processed. Protected by mutex_.
SequenceNumber last_processed_oldest_snapshot_ = 0;
// Sum of the per-shard snapshot release counts at the last exact refresh.
// Protected by mutex_.
uint64_t last_processed_snapshot_release_count_ = 0;
LogsWithPrepTracker logs_with_prep_tracker_;
// Callback for compaction to check if a key is visible to a snapshot.
Loaded 100 of 460 files, more files were not shown because too many files have changed in this diff. Show more