Compare commits

...
77 Commits
Author SHA1 Message Date
anand76 a6ac302081 Revert "Fix Windows build issue in trie_index_factory.h"
This reverts commit dfd356fa10.
2026-10-15 10:41:13 -07:00
Anand Ananthabhotla 108d3bf062 Make InjectedErrorLog::Entry 256 bytes on 32-bit targets (#15388)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15388

`InjectedErrorLog::Entry` in `utilities/fault_injection_fs.h` is written to the injected error log in its native layout, and a `static_assert` requires `sizeof(Entry) == 256` so the record size stays stable. That assert fails on i386 builds.

Root cause: the fields add up to only 252 bytes. On targets where `alignof(uint64_t)` is 8 (x86_64, aarch64, armv7), the compiler adds 4 bytes of implicit tail padding, so the size is 256. On i386, `uint64_t` members are 4-byte aligned inside a struct, so there is no tail padding and the size is 252.

Fix: add an explicit trailing `uint8_t padding[4]` field so the size no longer depends on `uint64_t` alignment.

Compatibility: on 64-bit targets the new field takes the place of the old implicit tail padding, so every field offset and the 256-byte record layout are unchanged. `Record()` value-initializes the entry (`Entry entry{}`), so the padding bytes are always written as zero. No decoder in the repo depends on the padding bytes.

___

landed-with-radar-review

Differential Revision: D124873369

fbshipit-source-id: f5c6f43ca85013b0e769853ad13b8d4f3f89b958
2026-10-13 10:26:01 -07:00
xingbowang d6cd1fc884 Fix big-endian builds for the fault-injection filesystem (#15297)
Summary:
The persistent injected-error log writes `InjectedErrorLog::Entry` objects using their native in-memory layout, while the log format's consumer expects little-endian multi-byte fields. The existing `static_assert(port::kLittleEndian)` protected that format by rejecting every big-endian build that included the fault-injection filesystem, even though the log is an optional diagnostic.

This change keeps RocksDB and the fault-injection filesystem available on supported big-endian platforms while disabling only the incompatible persistent log. Log-file creation now returns an inactive descriptor when `port::kLittleEndian` is false, which makes `Record()` and `Flush()` remain safe no-ops. The fixed 256-byte entry-size assertion remains in place, and format-dependent tests are bypassed on big-endian targets.

The change also adds a release note and documents big-endian support plus the preferred capability-check pattern for optional architecture-specific features.

## Compatibility and limitations

- Little-endian behavior and the on-disk log layout are unchanged.
- On big-endian platforms, configuring an injected-error log path does not create or populate a log file; the rest of fault injection remains available.
- Windows already leaves this POSIX-backed diagnostic disabled.
- This does not introduce a portable log encoding. Supporting persistent injected-error logs on big-endian platforms would require a format change.

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

Test Plan:
- `build_tools/rockstest.sh fault_injection_fs_test '--gtest_filter=InjectedErrorLogTest.*'` (little-endian macOS): all 5 tests passed.
- `make check-sources`
- `git diff --check upstream/main...2026_10_01_endian_fix`

Native big-endian build and test execution were not available locally; that path relies on the compile-time `port::kLittleEndian` capability and should be covered by big-endian CI.

Reviewed By: jaykorean

Differential Revision: D122880760

Pulled By: xingbowang

fbshipit-source-id: 59dae9661a47608aa12f41078ad1b3ef0fa7231e
2026-10-13 10:25:48 -07:00
anand76 dfd356fa10 Fix Windows build issue in trie_index_factory.h 2026-10-13 10:24:46 -07:00
Xingbo Wang 60d6f470e8 Update 11.10.0 release date to include the reverted commit 2026-08-28 09:47:49 -07:00
Anand Ananthabhotla ea16e73a71 Revert PR 15106 (Ignore OPTIONS files for manifest recovery advance) (#15158)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15158

Reverts `https://github.com/facebook/rocksdb/pull/15106`, which ignored `OPTIONS-*` and temp files when deciding whether recovery must advance `next_file_number_`.

Crash-test failures show backup/checkpoint failing to copy the current `OPTIONS-*` file after options cleanup, because `DeleteObsoleteOptionsFiles()` ignores `next_file_number_` and `options_file_number_` when choosing which files to keep. The failing commands use `optimize_manifest_for_recovery=1`, so restore the previous conservative file-number reservation behavior while we investigate a narrower fix.

Reviewed By: xingbowang

Differential Revision: D117807052

fbshipit-source-id: 89f8656f045be4ac36d2ec851980573ed83370fe
2026-08-28 04:44:28 -07:00
rocksdb-release-bot 4c5486efec Update HISTORY.md for 11.10.0 release 2026-08-27 16:03:42 -07:00
Omkar Gawde d699ffbd5c Support partial atomic range replacement (#15154)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15154

**What**: Let `IngestExternalFileArg::atomic_replace_range` replace a half-open `[start, limit)` range that only partially overlaps existing SSTs, by committing a generated range tombstone alongside the replacement files.

**How**:
- Tombstone: `NumFilesToPrepare` reserves one extra file number and `PrepareAtomicReplaceRangeTombstone` writes the SST only when the pinned `Version` holds a straddling file (`HasPartialOverlap`), so the no-straddle case pays no extra write; run rescans and returns `TryAgain` if a straddler appeared since. On activation it is rotated to the front of `files_to_ingest_` so one `VersionEdit` lands `old < tombstone < data` seqnos, straddlers are retained, contained files metadata-deleted, and `generated_for_ingestion` keeps `move_files` from unlinking it.
- Half-open plumbing: `RangesOverlapWithMemtables`, `RangeOverlapWithCompaction`, `OverlapProximalLevelOutputRange`, and `IsRangeOverlapped` take a required limit-exclusivity argument, and `largest_internal_key` now comes from `RangeTombstone::SerializeEndKey()`. `OverlapWithLevelIterator` stays inclusive: over-counting can only push the tombstone one level shallower, which is safe.
- Eligibility: `SupportsAtomicReplaceRangeTombstone` requires universal compaction, a delete-range-capable table factory, `allow_global_seqno`, `!allow_db_generated_files`, `!snapshot_consistency`, and no UDT. `fail_if_not_bottommost_level=true` rejects partial overlap because the retained file forces the tombstone and replacements above Lmax. The `atomic_replace_range` doc comment and a new `unreleased_history/public_api_changes/` note state the half-open contract.
- `ResetTableReader` passes `kNoFileChecksumFuncName` in `FileOptions` for external files, making "no checksum metadata" explicit rather than empty.
- Stress: new `ingest_external_file_atomic_replace_one_in` gflag. `PickAtomicReplaceRange` picks a run of overwriteable keys strictly inside an existing SST so every replacement straddles, writes even-indexed keys and marks the interleaved odd-indexed keys deleted so the tombstone must clear the holes, and rolls back expected values on `TryAgain`. `db_stress_tool.cc` and `db_crashtest.py` gate the flag on the same eligibility set and force `acquire_snapshot_one_in=0`.

**Why**: Atomic replacement previously required range bounds to align with SST bounds, and the public doc admitted the upper bound "may be interpreted as inclusive or exclusive". The tombstone overlay removes the alignment restriction and pins the contract to half-open without slowing the Nimble path or the no-straddle case.

Reviewed By: pdillinger

Differential Revision: D117155307

fbshipit-source-id: 65f45463e8082a7db7a9d44c8dc50debe95e36e7
2026-08-26 21:52:40 -07:00
Peter Dillinger e4a7136d3e Preferred-phase scheduling for periodic (time-based) compaction
Summary:
`periodic_compaction_seconds` softly guarantees every SST is rewritten at least
once per interval. Across a fleet of similarly-configured DBs, many events line
those intervals up in lockstep -- a shared enable, a coordinated restart (e.g.
DC power-outage recovery), or just similar write history -- so the periodic
compactions all come due together, a recurring "thundering herd" that is
especially painful with remote compaction (SEV S698635).

The obvious fixes trade one problem for another. Spreading the herd out means
some files get compacted off their natural schedule, which is wasted work; and a
clumsy spread just relocates the herd or spawns a new one. So the real goal is
narrow: **de-herd while keeping "badness" -- extra and/or late compaction --
small, and without inducing fresh herding at any step.**

This change adds an EXPERIMENTAL, opt-in mechanism that gives each `(DB, column
family)` a stable, well-spread *preferred phase* within the interval and steers
periodic compactions toward it. It is randomized and *uncoordinated* (DBs never
talk to each other), which both keeps it simple and, as it turns out, de-herds
far more cheaply than adding jitter to the interval. It is controlled by a single
mutable DBOption and is **off by default**:

- `periodic_compaction_phase_recovery_percent` (default `0` = disabled / kill
  switch; recommended `33`; clamped to `[0, 100]`) -- how aggressively each
  trigger converges toward the phase, as a percent of the remaining gap, only
  ever pulling the trigger *earlier*. `0` reproduces the classic scheduler
  exactly; `100` jumps to the phase in one cycle; lower values approach gradually
  with much less extra work.

The preferred phase itself needs no tuning: it is derived from the DB ID (stable
across re-open and physical cloning/migration, but distinct across DBs) and each
column family is offset from the DB's base phase, so the CFs of one DB don't fire
together. (An earlier draft exposed a `compaction_schedule_seed` string option
for choosing the phase source; it was over-engineered for the one behavior anyone
needed, and as a mutable string it also broke OPTIONS-file round-trip
verification in crash testing. It has been removed in favor of always using the
DB ID.)

## The two kinds of badness

"Badness" here is the fraction of compaction work that is ideally unnecessary:
over-compaction (rewriting a file earlier than the interval required) plus a
penalty for lateness (going overdue past the interval). Steady state is 0; both
"never compact" and "compact twice as often as needed" approach 1. De-herding
inherently spends a little badness to spread load out; every design choice below
exists to bound one of those two costs *without* re-clustering the fleet.

The scenario names cited in the design bullets below (`upgrade_dynamic_r33`,
`naive_upgrade_r25`, `burst_r*`, `turndown_r33`, `turnup_r33`, ...) all come from
the out-of-tree simulator; see its results page (the "Simulator and
visualization" section below, with an interactive plot per scenario) for the
evidence behind each claim.

## Design

- **A stable per-(DB, CF) preferred phase = one fixed attractor per DB.** Each DB
  gets a base phase from its DB ID; each CF is offset from it. Because the target
  is fixed (not re-rolled each cycle) and unique per DB, the fleet converges to a
  uniform spread and then compacts *on time* -- badness decays toward ~0. This is
  the crucial contrast with naive per-cycle jitter, which de-herds but never stops
  jittering and so pays a permanent over-compaction cost (see
  `naive_upgrade_r25`: flat ~12-14%, only diffusive spreading) vs.
  `upgrade_dynamic_r33`, whose badness decays to ~0 for the same anti-herding.

- **Converge gradually, and only ever earlier.** Each trigger closes a
  `recovery_percent` *fraction* of the gap to the phase, reaching it over a few
  cycles rather than in one burst, and never later than
  `periodic_compaction_seconds` in steady state. A lower rate does less extra
  work, which matters when the phase keeps moving (write-driven / bursty
  workloads), where a high rate wastefully "chases" it -- the burst sweep shows
  `burst_r25 < burst_r33 < burst_r50 < burst_r100` in sustained badness. `33` is
  the default balance: quick spreading, low extra work.

- **Fractional recovery (not "snap") is latency-tolerant and doesn't re-herd.**
  Closing a fraction of the gap gives a single stable convergence point, so
  real-world scheduling/queueing latency -- the delay between a file becoming due
  and its compaction actually running -- merely shifts the whole fleet uniformly
  instead of re-clustering it. A tried "snap onto the phase when in range" variant
  converged a bit faster with no latency but re-herded around the snap
  threshold under latency; it was rejected. Graceful degradation beat peak speed.

- **Spread a suddenly-due cohort instead of bursting it (bounded lateness).**
  Enabling the feature, turning `periodic_compaction_seconds` down, or catching up
  after downtime can leave a whole cohort past due at the same instant. Firing it
  all at once would just be the herd again, so such a cohort is spread over the
  first quarter-interval (~N/4) after the phasing anchor on an *absolute*,
  epoch-aligned per-(DB, CF) grid. This trades a bounded lateness (<= ~N/4 past
  the deadline, so the interval is a *soft* upper bound during these transients)
  for anti-herding, and the absolute grid keeps a rapidly restarting DB from being
  perpetually re-deferred and starved. `turndown_r33` shows the past-due cohort
  smeared over N/4 rather than bursting; `turnup_r33` confirms a turn-up creates
  no past-due cohort (deadlines only grow) and so no burst. For immediate
  enforcement at such a transition, trigger a manual compaction.

- **Off by default, with a kill switch.** Inert unless
  `periodic_compaction_phase_recovery_percent > 0`; at `0` behavior is identical
  to the classic scheduler.

## Implementation notes

- Phase math is integer-only (`Hash64` + `FastRange64`); floating point is avoided
  for its pitfalls. The per-CF offset is a golden-ratio (Weyl/Kronecker) additive
  sequence -- low-discrepancy for any number of CFs.
- Logic lives in `db/periodic_compaction_phaser.{h,cc}`
  (`PeriodicCompactionPhaser`), which owns the recovery percent and the phasing
  anchor and exposes the stateless trigger-time math; `VersionSet` holds one and
  caches the per-CF result on each `Version`. The anchor is (re)set to now on DB
  open, on a recovery-percent change, and on any `periodic_compaction_seconds`
  change -- the three events that can create a past-due cohort.
- Plumbed through the C API, `db_bench`, `db_stress`, and `db_crashtest`.

## Simulator and visualization (out-of-tree, not part of this change)

A standalone simulator drives 40 real RocksDB instances through a real
periodic-task scheduler, records every compaction (periodic vs write-driven), and
scores the badness metric above across ~17 scenarios: dynamic/restart enable,
interval turn-down/turn-up via `SetOptions`, on-cadence writes, random write
bursts with a recovery-rate sweep, and the naive-jitter baseline. It is the basis
for the numbers cited above. Interactive HTML (**NOTE** fb-internal URL sanitization might require you to copy-paste the URL yourself):
https://htmlpreview.github.io/?https://github.com/facebook/rocksdb/blob/5759c6a62d9ff36cecf030b80453cc819b2f66ae/pc_phase_sim.html

Reviewed By: jaykorean

Differential Revision: D117487278

fbshipit-source-id: a62439548b7dd6fe7dfdbbe21b69531d8b57a1e8
2026-08-26 20:14:26 -07:00
Peter Dillinger f7b791b213 Fix bottommost-files infinite compaction loop with preserve/preclude (#15146)
Summary:
Leveled compaction combined with preserve_internal_time_seconds or preclude_last_level_data_seconds is a known-broken combination, but it is easy to enable by accident -- for example a fleet-wide config rollout that turns on the preserve time for CFs that happen to be leveled. Rather than degrading gracefully, it triggers an infinite kBottommostFiles compaction loop: a bottommost file gets marked, rewritten with no progress, and immediately re-marked, continuously rewriting the file and growing the MANIFEST so the file set never quiesces. This change makes such a misconfiguration a harmless no-op instead.

Before this change, the marking logic
(ComputeBottommostFilesMarkedForCompaction) marked a bottommost file whenever its largest seqno was below the oldest snapshot, without checking whether a compaction could actually zero that seqno. Recently written seqnos stay within the seqno->time "preserve" window and cannot be zeroed, so the rewrite never made progress. The same futile marking also occurred with user-defined timestamps when full_history_ts_low is unset. The underlying issue is that the "can this bottommost seqno be zeroed?" question was answered independently by the marking logic and by the compaction output path (CompactionIterator::PrepareOutput), and the two had drifted apart.

With this change, both paths answer that question through one shared predicate (BottommostSeqnoCanBeZeroed), and the preserve-window boundary that marking needs is plumbed into VersionStorageInfo from DBImpl, mirroring the existing oldest-snapshot plumbing. Snapshot visibility stays a per-site check. On CFs without preserve/preclude the predicate is a no-op. Confirmed that caller invariants ensure that relocating the `ikey_.sequence <= preserve_seqno_after_` check in CompactionIterator::PrepareOutput() will not trip the ROCKSDB_LOG_FATAL.

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

Test Plan:
- compaction_picker_test: new CompactionPickerTest BottommostFileNotMarkedWithinPreserveWindow / BottommostFileMarkedBelowPreserveWindow cover the preserve-window boundary; new CompactionPickerU64TsTest BottommostNotMarkedWithEmptyFullHistoryTsLowAndUnknownMaxTs covers the UDT edge case.
- tiered_compaction_test: new PrecludeBottommostLoopTest LeveledPreserveTimeNoBottommostLoop is an end-to-end regression for the loop and the DBImpl plumbing: a bottommost file with a nonzero largest seqno inside the preserve window, made a marking candidate via a snapshot release, must not be marked. It reproduces the loop (hangs) when the plumbing is neutralized and passes with the fix.

Reviewed By: xingbowang

Differential Revision: D117299979

Pulled By: pdillinger

fbshipit-source-id: 51e8e2d4e2116984d16d3c7ef54a9635d5aded74
2026-08-26 19:44:47 -07:00
Josh Kang 6dbb6f30e6 Avoid restoring coroutine stats on foreign threads (#15145)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15145

## Summary

RocksDB coroutine statistics are stored in `RequestContext`, but their `RequestData` is thread-affine. A filesystem implementation may propagate the context while completing `SubmitReadAsync` on another thread. Installing that context invokes `CoroutineStatsRequestData::onSet()`, whose `AssertSameThread()` detects the thread hop and aborts.

```
Before:

Thread A                                      Thread B
RequestContext
├── caller tracing/attribution data
└── RocksDB coroutine stats ── propagated ──> setContext()
                                                │
                                                ▼
                                      CoroutineStatsRequestData::onSet()
                                                │
                                                ▼
                                      AssertSameThread(A != B)
                                                │
                                                ▼
                                              abort
```

Keep propagating the complete `RequestContext`, including the coroutine-stats token, so filesystem callbacks retain caller tracing and attribution. `CoroutineStatsRequestData` now records its owner using `Env::GetThreadID()` and ignores `onSet()` and `onUnset()` on foreign threads. When `ViaIfAsync` schedules the continuation back onto the RocksDB read EventBase, it restores the saved context on the owner thread before the coroutine continues, so the stats are safely loaded back into RocksDB TLS.

```
After:

Thread A                                      Thread B
caller RequestContext
├── caller data ───────── propagated ───────> caller data
└── RocksDB coroutine stats ─ propagated ───> onSet()/onUnset()
                                                │
                                                ▼
                                      owner thread != current thread
                                                │
                                                ▼
                                               no-op

continuation queued back onto Thread A
        │
        ▼
saved RequestContext restored on owner thread
        │
        ▼
RocksDB coroutine stats reinstalled in TLS
```

Reviewed By: xingbowang

Differential Revision: D117274942

fbshipit-source-id: 9995cc3053a0a1d3a63d0cb2f4dd9320ab553005
2026-08-25 13:00:55 -07:00
Kunal Pathak bd16531142 Hoist top-level filter-partition index iterator out of MultiGet per-key loop (#15137)
Summary: Pull Request resolved: https://github.com/facebook/rocksdb/pull/15137

Reviewed By: pdillinger

Differential Revision: D115489931

fbshipit-source-id: 9d5b607c834a6e8754d55619309ec3705094cc76
2026-08-25 09:13:49 -07:00
Xingbo Wang e6a2ee0bd2 Fix subset checkpoints with incomplete MANIFEST tails (#15123)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15123

Subset checkpoint post-processing appends column-family drop records to a copied MANIFEST. Recovery can tolerate an incomplete atomic group at the tail while retaining the reader high-water size, so appending at that size can produce a checkpoint that fails recovery.

Track the valid logical append boundary together with its MANIFEST file number, refresh it after recovery and successful writes, reset it across reactive MANIFEST switches, and trim only subset-checkpoint copies before appending drop records. Whole-DB checkpoints and the source MANIFEST remain unchanged.

Reviewed By: anand1976

Differential Revision: D116449394

fbshipit-source-id: a162dd6a2558f0f9ec40d878ee83956a89df698b
2026-08-22 12:17:33 -07:00
Andrei Preda ad81b654a4 Prune resolved 2PC recovery state on secondaries (#15121)
Summary:
Previously, during WAL replay, a secondary stored each 2PC prepare in
`recovered_transactions_`. Replaying the matching commit or rollback marker
deletes it. If the primary purges that marker's WAL before the secondary reads
it, the prepared `WriteBatch` and its tracker entry remain until the secondary
closes. This retention case was flagged by xingbowang in https://github.com/facebook/rocksdb/issues/15066. Even when
the marker is replayed, the secondary records completion but previously did not
prune the completed prefix from `logs_with_prep_tracker_`.

This change deletes recovered transactions whose prepare WALs are all below
`min_log_number_to_keep` after opening every discovered WAL reader but before
replaying records. The primary keeps that watermark at or below every
unresolved prepare and every prepare with unflushed committed data, so an entry
below it is committed and flushed, or rolled back. Deleting before replay lets
a reused transaction name start a new generation instead of merging into stale
state. If a reader cannot be opened, no recovered transaction is deleted.

After a round successfully replays every discovered WAL, the secondary also
prunes the completed prefix from `logs_with_prep_tracker_`. This covers
completions from normal commit or rollback replay and from the watermark-based
cleanup. The oldest outstanding prepare stops the pruning scan.

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

Test Plan:
Added coverage for missed-marker cleanup, transaction-name reuse before replay,
the exact watermark boundary, a catch-up round that reports success after
replaying no WALs, and tracker pruning after normal commit replay. The
missed-marker case verifies cleanup of both retained structures.

Reviewed By: pdillinger

Differential Revision: D116620460

Pulled By: xingbowang

fbshipit-source-id: baca7faf53f414507957f2afdcc4ca67133c5669
2026-08-21 11:39:01 -07:00
Peter Dillinger 8f492b2f47 Store common user-key prefix once per block (format_version 8) (#15117)
Summary:
Building on D116105558, this change finishes and publishes `format_version = 8` as
production-ready.

The headline feature is a tweaked block encoding that stores the common user-key
prefix shared by all keys in a block **once**, in the block's leading bytes
`[0, restarts[0])`, instead of repeating it at every restart point. This shrinks
prefix-heavy blocks (and their block-cache footprint) and, for bytewise-ordered
keys, speeds up `Seek` by comparing prefix-stripped suffixes. It is applied to
**data blocks** and **every index tier** (leaf/partition index, the
partitioned-index top-level, and the partitioned-filter top-level).

Motivation: MyRocks/ZippyDB/timeseries-style keys carry a long shared prefix
(table/index id, partition, etc.). Repeating it at each restart wastes space and
cache, and every Seek re-compares those bytes. Profiles show index-block seek CPU
is comparable to data-block seek CPU (partitioned index does two index seeks per
data seek), so index blocks are a first-class target, not just data blocks.

### Format (self-describing, no new footer bit)

- The common prefix occupies the block's leading bytes; a non-zero `restarts[0]`
  (the offset of the first entry, i.e. the prefix length) self-signals its
  presence. `restarts[0]` is 0 in every block that does not use the feature (all
  versions, all block types), so a single fv8 reader transparently handles any
  mix of stripped and unstripped blocks -- no footer bit is consumed.
- Previous versions would misinterpret this encoding by quietly ignoring the
  common prefix, which (along with the lack of extensibility in data-block
  metadata) is why it is gated behind format_version 8.
- Only restart-point entries change on disk; non-restart entries are byte
  identical to fv7, so the writer produces the exact same bytes regardless of
  implementation.

### Public option

New `BlockBasedTableOptions::optimize_key_common_prefix`
(`enum class OptimizeKeyCommonPrefix`), default `kIfFastSeek`:

- `kDisabled` -- never use it.
- `kIfFastSeek` (default) -- use it only where it also enables the Seek speedup:
  format_version >= 8 with a built-in (reverse-)bytewise comparator.
- `kEnabled` -- use it for all comparators at fv8; (reverse-)bytewise get space +
  fast seek, other comparators get the space savings only (the reader
  reconstructs full keys and compares with the real comparator).

Expressed as a criteria so it need not be toggled per CF based on the comparator
(which can vary by CF). It additionally requires `use_delta_encoding` (the
feature builds on delta encoding) and `ts_sz == 0`; when `use_delta_encoding` is
false -- the setting that also preserves zero-copy key pinning -- it is a no-op.
Default format_version is unchanged (still 7); the feature is opt-in via fv8.

### Scope: every index tier

The reader handles all index-like blocks uniformly (a non-zero `restarts[0]`
self-signals a stripped block regardless of block type), so the writer strips the
common prefix from every tier:

- **Data blocks** and **leaf / partition index blocks**: enabled.
- **Partitioned-index top-level block**: enabled. Under the default
  `index_shortening = kShortenSeparatorsAndSuccessor` the file's last separator
  would otherwise be a short successor sharing no leading bytes, collapsing the
  single-block top-level's common prefix to 0. When common-prefix is on the
  builder suppresses that one successor (keeps the full last key -- i.e.
  `kShortenSeparators` behavior for that one entry), so the top-level strips
  uniformly under every shortening mode; the full last key is also a tighter file
  upper bound, so it is not worse.
- **Partitioned-filter top-level index**: enabled (same uniform treatment). Its
  space/CPU effect is small at default partitioning (the top-level filter index
  is tiny) but grows with many small partitions -- a filter miss short-circuits
  at, and is dominated by, the top-level filter-index seek (see Performance).
  Included for a uniform on-disk surface across tiers.

Bloom filter *contents* still hash full user keys, so prefix/whole-key filtering
is unaffected.

### Implementation

- Writer (`block_builder.{h,cc}`): keys are encoded incrementally in the fv7
  layout while the running common prefix is tracked cheaply (reusing the delta
  `shared`); at `Finish()` only the restart-point entries are rewritten in place
  with the prefix stripped, and the prefix is written once at the block start.
  The rewrite is generalized to the value-delta (fv4) index layout used by index
  blocks (no `value_size` header; bulk-copy the inline BlockHandle and
  non-restart entries verbatim).
- Reader (`block.{h,cc}`, `dbformat.h`): `Block` decodes the prefix from
  `restarts[0]`; `BlockIter::ParseNextKey` prepends it on a restart point
  (`shared == 0`). The suffix-seek helpers (strip target once, compare shortened
  suffixes) live in the templated `BlockIter` base and are shared by data and
  index iterators (binary search, templated on `DecodeKey`/`DecodeKeyV4`); for
  non-bytewise comparators (data) and the hash `PrefixSeek` refine step, the
  reader reconstructs the probed restart key and compares full keys.
- Gating (`block_based_table_builder.cc`, `index_builder.{h,cc}`,
  `partitioned_filter_block.{h,cc}`): the criteria is threaded to the data-block
  builder and to every index tier -- leaf/partition index, the partitioned-index
  top-level (`use_common_prefix_top`), and the partitioned-filter top-level --
  together with the last-separator successor suppression
  (`ShortenedIndexBuilder::GetSeparatorWithSeq`) that lets the top-level strip
  under the default shortening mode. The index base gate also requires
  `super_block_alignment_size == 0` (block alignment can trigger
  `skip_delta_encoding`, producing non-restart `shared == 0` entries the reader
  could not distinguish from stripped restart keys).
- Memory: the per-block/per-iterator prefix is stored as a `uint32_t` length
  (the prefix is always at offset 0) rather than a full `Slice`, and `Block` /
  `BlockBuilder` members were repacked so the feature adds little or no struct
  size.
- Java: a new `OptimizeKeyCommonPrefix` enum and a `BlockBasedTableConfig`
  getter/setter, plumbed through JNI in both directions (the
  `newTableFactoryHandle` writer and the options-reconstruction constructor).

### Whole-file (table-level) prefix: investigated and rejected

A natural extension is to store the whole-file common prefix once per *file*
(when an `SstPartitioner` guarantees every key in the file shares a fixed prefix)
instead of once per *block* -- e.g. the MyRocks index/table id, or a future
128-bit global id. It was prototyped end-to-end (partitioner-provided prefix
captured at file open, stored in a `user_key_common_prefix` table property, and
stripped from every data/index block; the reader re-prepends it) and then
dropped. The `user_key_common_prefix` table property reserved in the
previous diff is dropped in this one. The rationale, in case it is revisited:

- Marginal benefit over per-block. Per-block already strips the full shared
  prefix from every key; the only thing whole-file removes on top is the single
  per-block copy of that prefix, i.e. `~= prefix_len / block_size` of the
  uncompressed (block-cache) footprint, with no meaningful read-CPU change
  (per-block already delivers strip-once + suffix compares). Measured at
  prefix_len=12, 4 KB blocks this was ~-0.3% incremental space; at the 8-16 KB
  data blocks typical of the space-constrained target (MyRocks/UDB) it is
  ~0.1-0.2% even with a 16-byte global id -- a fraction of a percent, seen only
  by partitioner users. Small block-size workloads are becoming more
  niche, and even where they exist, they are also CPU-sensitive (see next
  paragraph).

- Real, if small, CPU tax on the *per-block* path. Reconstructing a key from two
  non-contiguous parts (file prefix + block residual) cost ~0.5-1.0% additional
  retired instructions on the per-block read path (deterministic across gcc and
  clang), wall-clock-neutral in cache-resident tests but present even when no
  whole-file prefix is configured. Attempts to remove it did not pan out: a
  compile-time specialization of the reconstruction *increased* instructions
  (+2.9%) by defeating inlining; a per-block combined-prefix buffer trades the
  CPU cost for a memory cost on every (default) block; and a per-iterator buffer
  was the original larger regression. Only invasive iterator-level
  monomorphization would truly zero it, which is not justified on this hot path.

- Decision. A fraction-of-a-percent, partitioner-only space win does not justify
  a permanent per-block-read-path CPU tax plus additional on-disk format surface
  (a table property that changes whole-file interpretation). Dropped. The generic
  `SstPartitioner::ShouldPartitionByPrefix` hook is kept (cheap, and useful
  independently), and the reserved `user_key_common_prefix` table property is
  removed since no writer will emit it.

### Performance (db_bench, release/native, cache-warm, best-of-3, pinned)

Three arms: (A) fv7 on the base/main branch, (B) fv7 with this change (should be
neutral / byte-identical writing), (C) fv8 with this change and
`optimize_key_common_prefix=enabled` (the optimization is active). Partitioned
index + filters; `compression_type=none`, so SST bytes are the uncompressed
block-cache footprint. `int` = db_bench default keys (variation-early, near worst
case); `pfxN` = an N-byte shared prefix + varying suffix (MyRocks-like).
Setup: `num=3,000,000`, `reads=1,500,000`, single thread,
`metadata_block_size=4096`, cache holds the working set, pinned to one core.

| shape (key/val) | arm | SST bytes | readrandom us/op | seekrandom us/op |
|---|---|---:|---:|---:|
| int (32/8)    | A fv7 main | 138,983,658 | 3.484 | 4.995 |
|               | B fv7 ours | 138,983,146 | 3.518 | 5.024 |
|               | C fv8 ours | 137,681,420 | 3.490 | 4.986 |
| pfx24 (32/8)  | A fv7 main |  71,697,531 | 3.417 | 5.561 |
|               | B fv7 ours |  71,698,043 | 3.448 | 5.579 |
|               | C fv8 ours |  65,578,236 | 3.284 | 5.386 |
| pfx56 (64/16) | A fv7 main | 102,942,266 | 3.624 | 5.830 |
|               | B fv7 ours | 102,942,266 | 3.678 | 5.835 |
|               | C fv8 ours |  90,554,642 | 3.444 | 5.626 |

fv8 vs fv7-main (C vs A) -- space / readrandom / seekrandom:

- int (32/8):    **-0.9%**  / ~neutral  / ~neutral
- pfx24 (32/8):  **-8.5%**  / **-3.9%** / **-3.1%**
- pfx56 (64/16): **-12.0%** / **-5.0%** / **-3.5%**

fv7-ours vs fv7-main (B vs A): SST bytes byte-identical (within 512 B; the fv8
paths are gated off), read/seek us/op within run-to-run noise (~1%, best-of-3 on
a shared host). Net: fv8 is a space **and** read/seek-CPU win on prefix-heavy
data and neutral on variation-early (`int`) data, so `kIfFastSeek` is safe on by
default at fv8.

### Space by tier (compression=none, num=3M, mdb=4096; fv7 -> fv8)

Where the win comes from -- the index tiers strip the most:

| shape | data | leaf/partition index | top-level index | filter |
|---|---:|---:|---:|---:|
| int   | -0.8%  | -29% | -45% | -0.2% |
| pfx24 | -8.3%  | -69% | -89% | -0.8% |
| pfx56 | -11.1% | -83% | -96% | -1.6% |

Data bytes dominate the absolute total, but every index tier is stripped:
leaf/partition index -29% to -83% and the partitioned-index top-level -45% to
-96%. Filter *blocks* barely move (-0.2% to -1.6%) because filter bits hash full
user keys; only the small top-level filter index is stripped.

### Point-miss lookups (readmissing)

A partitioned-filter miss short-circuits at the filter (no data-block read), so
it is dominated by the top-level filter-index seek plus the partition bloom. At a
typical `metadata_block_size=4096` (few partitions, tiny top-level filter index)
fv8 is neutral on misses (int +0.9%, pfx24 -1.0%, pfx56 -0.7% us/op -- all
noise). The top-level-filter stripping pays only with many small partitions: at
`metadata_block_size=256` it is pfx24 1.166->1.138 (**-2.4%**) and pfx56
1.273->1.188 (**-6.7%**) us/op.

### Backward / forward compatibility

- Default `format_version` remains 7; the feature is inert unless a CF opts into
  fv8. Older RocksDB rejects fv8 files at footer decode (fail-safe).
- Within fv8, the stripped-block set a writer can emit is fixed and every fv8
  reader can read it (the `restarts[0]` signal plus uniform reader handling), so
  the on-disk surface is self-consistent across builds/downgrade.

### Follow-ups (not in this change)

- Suffix-aware interpolation / hash index seek (currently correct via the
  reconstruction fallback, not perf-tuned).

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

Test Plan:
- New focused unit tests (`table_test`), run at the full `format_version` matrix
  including the now-supported fv8:
  `GeneralTableTest.OptimizeKeyCommonPrefix` (data-block space+read+Seek
  correctness: existing and missing keys straddling the prefix, before/after-all,
  SeekForPrev), `OptimizeKeyCommonPrefixNonBytewise` (reconstruction path), and
  `IndexBlockCommonPrefixEquivalence` across `{kBinarySearch,
  kBinarySearchWithFirstKey, kHashSearch, kTwoLevelIndexSearch}` x
  `{partitioned filters}` x `{bytewise, reverse-bytewise}` (so both top-level
  tiers -- partitioned-index and partitioned-filter -- are exercised), asserting
  identical reads and a shrunken fv8 index.
- The randomized `ParameterizedHarnessTest` now runs at fv8 (reverse-bytewise,
  restart intervals `{1,16,1024}`, special/empty keys fuzzed against the feature).
- `db_crashtest.py` randomizes `format_version` including 8 and
  `optimize_key_common_prefix` in {0,1,2}.
- Removed the reserved-property carve-out: dropped the `ReservedUserKeyCommonPrefix`
  reader test and the C API getter for the property; C API regenerated via
  `tools/c_api_gen/` (`make regen-c-api`), and the intentional public-C-API
  removal is recorded in `tools/c_api_gen/api_compatibility_allowlist.json`.
- Java: `BlockBasedTableConfigTest.optimizeKeyCommonPrefix` (default
  `kIfFastSeek`, plus set/get of every value).
- `table_test` `FooterTests` updated for the now-published fv8 footer, whose
  metaindex-gap encoding stores the metaindex block size in 16 bits (the test
  caps that size for fv >= 8).
- Manual verification: the db_bench runs in Performance above (three-arm
  space + read/seek CPU, per-tier byte breakdown via `sst_dump`, and
  `readmissing`); and `readrandom`/`seekrandom` with
  `--block_protection_bytes_per_key=8` find 100% of keys at both fv7 and fv8
  (reconstructed keys pass per-key checksum).

Reviewed By: xingbowang

Differential Revision: D116365016

Pulled By: pdillinger

fbshipit-source-id: 66f4b5c60ea4fe0ff48d34c98d191ba8ebdf0665
2026-08-20 17:32:41 -07:00
Peter Dillinger e8554f1b11 Faster and safer MANIFEST recovery for remote compaction (OpenAndCompact) (#15133)
Summary:
Faster and safer MANIFEST recovery for remote compaction (OpenAndCompact)
`DB::OpenAndCompact` (remote/distributed compaction) opens the target DB as a
secondary and reconstructs the LSM state from the MANIFEST before running the
requested compaction. Previously, point-in-time recovery would stat/open the
SST (and blob) files referenced by the MANIFEST to classify them found/missing
and materialize candidate versions, and eagerly load their table handlers.
That is (a) a lot of unnecessary file operations and open latency, especially
on remote/distributed storage, and (b) fragile: when a file referenced by the
MANIFEST but not an input to the compaction was momentarily unavailable to the
worker -- e.g. an obsolete file the primary had already deleted -- recovery
could fail the job (surfacing as an incomplete "no valid result") or roll back
to an earlier version, even though the compaction only needs its own input
files, which the primary keeps referenced for the job's duration. This was seen
as a flaky failure of CompactionServiceTest.BasicCompactions on aarch64, and
reproduces on x86 under COERCE_CONTEXT_SWITCH.

Efficiency + reliability: recovery for the OpenAndCompact path now trusts the
MANIFEST. It reconstructs the current version from FileMetaData without
stat-ing/opening files to classify them and without eagerly opening
candidate-version table handlers, so it does far fewer file operations and
never fails or rolls back over a file the compaction does not need. The
compaction's input files are opened (and unique-id verified) on demand as it
reads them.

Correctness hardening: trusting the MANIFEST means the worker's own (possibly
eventually-consistent) view of CURRENT/MANIFEST drives the reconstructed LSM
shape, which feeds correctness-critical decisions (Compaction::IsBottommostLevel
/ KeyNotExistsBeyondOutputLevel). A worker that read a stale CURRENT (older
MANIFEST), a truncated MANIFEST, or a MANIFEST with a corrupt/torn tail could
reconstruct against an older LSM shape and silently drop keys that should be
kept. To prevent this, the primary records the MANIFEST position (file number
and size) it scheduled the compaction from and passes it in
CompactionServiceInput as a floor. After recovery the worker compares the size
of the maximal *valid* prefix it reconstructed -- which stops at any corrupt or
torn record and at a partial atomic group at EOF, so tolerated tail garbage
cannot inflate it -- against that floor, and refuses to compact against an older
view (an older file number, or the same file number whose valid prefix is
shorter), returning Status::Incomplete so the CompactionService can fall back to
a local compaction. Accept-equal-or-later is safe by monotonicity: while the
inputs stay locked (being_compacted), a later view can only have moved data
further down, never resurrecting a shadowed value, so only an older view is
unsafe.

Single kill switch: both behaviors are bundled behind one signal and one
option. Presence of the floor on the request (min_manifest_file_number != 0)
couples on both trust-manifest recovery and the floor check in the worker;
absence reverts the worker to the original point-in-time recovery with no floor
check. A single mutable DBOption, remote_compaction_manifest_floor (default
on), gates whether the primary populates the floor, so it is the one escape
hatch that disables the entire new recovery path on a running DB via
SetDBOptions if it ever misbehaves. The new CompactionServiceInput fields are
wire-compatible in both directions (0 = not provided), so an old worker or an
old primary simply behaves as if the switch were off.

Follow-up: recovery is not yet strictly "open only the compaction's inputs". A
limited number of non-input files are still opened by the bounded file-property
sampling in Version::PrepareAppend -> UpdateAccumulatedStats (tolerating any
that are missing), which today populates input FileMetaData::num_entries for
compaction input-record-count verification. A natural follow-up is to record
the needed per-file info (e.g. num_entries) in the MANIFEST so the worker can
skip these auxiliary opens entirely; alternatively, source input num_entries
from the worker's own read of its input table properties. Relatedly, the floor
comparand is the recovered "maximal valid prefix" size, which today equals the
MANIFEST file size but is defined to stay correct if a future MANIFEST format
writes trailing padding/footer past the last record.

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

Test Plan:
Added tests in compaction_service_test.cc:
- RemoteCompactionMissingLowerFileTrustsManifest: a non-input file referenced
  by the MANIFEST is unavailable during a remote compaction; the job completes
  with correct results (fails without trust-manifest recovery). The underlying
  race also reproduces under COERCE_CONTEXT_SWITCH.
- RemoteCompactionManifestFloorRejectsStaleView: inflate the floor so the
  worker's recovered MANIFEST is below it; assert the worker rejects, the job
  falls back to local, and a below-output deleted key stays deleted (no silent
  resurrection).
- RemoteCompactionManifestFloorSendsPosition: a non-zero floor is attached and
  the worker accepts its equal-or-later view.
- RemoteCompactionManifestFloorKillSwitchOff: with the option off, no floor is
  sent, the worker uses the original recovery, and a transiently-missing
  non-input file makes the job fail/roll back (the escape hatch reverts
  behavior).
- RemoteCompactionManifestFloorMutable: SetDBOptions flips the option; the
  floor stops being attached and the worker skips the check.
options_settable_test covers the new mutable DBOption.

Crash test: remote compaction is already exercised (db_crashtest randomly
enables remote_compaction_worker_threads, driving DB::OpenAndCompact), so with
remote_compaction_manifest_floor defaulting on, the new recovery path is
covered by the crash test by default -- no new crash-test plumbing is needed.
The kill switch is intentionally left un-randomized (always on): turning it off
re-enables the original, occasionally-broken recovery, which would only add
noisy failures for no benefit.

UPDATE: crash test is not well covered because of `"remote_compaction_failure_fall_back_to_local": 1` which is masking other existing failures. Addressing this is a high-priority follow-up item. (Not practical to fix in this diff.)

Reviewed By: jaykorean

Differential Revision: D116738823

Pulled By: pdillinger

fbshipit-source-id: c02e0a437d62fc526dbbfcf70edf43434dfc7c1f
2026-08-20 16:07:48 -07:00
rocksdb-release-bot 330ded5129 Update version to 11.10.0 for 11.9 release (#15135)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15135

- Bump version.h from 11.9.0 to 11.10.0
- Add `11.9.fb` to `check_format_compatible.sh`
- Sync HISTORY.md `## 11.9.` section from `11.9.fb`
- Delete 17 consumed `unreleased_history/` note file(s) from main

Part of 11.9 release workflow.

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

Reviewed By: anand1976

Differential Revision: D116833037

Pulled By: joshkang97

fbshipit-source-id: 04ebda8db5b2c4fdae71a2cf107784b6abdc0f3c
2026-08-20 15:13:31 -07:00
Anand Ananthabhotla 4b41ccd2a7 Fix full_history_ts_low race for long-running snapshots in db_stress (#15129)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15129

When `persist_user_defined_timestamps=false` and `long_running_snapshots=1`, `AssertSame` in `MaybeReleaseSnapshots` reuses the timestamp saved at snapshot acquisition time. That timestamp can become stale because concurrent flushes advance `full_history_ts_low`, and long-running snapshots can be held much longer than the 10-second headroom added by `GetReadTimestamp()`.

The snapshot validation is intended to check that a held snapshot still exposes the same sequence-number view. It should not require the acquisition-time read timestamp to remain valid after history below `full_history_ts_low` has been collapsed.

The fix keeps the timestamped read API contract intact. Column families with user-defined timestamps still require `ReadOptions.timestamp`, even when timestamps are memtable-only. For `persist_user_defined_timestamps=false`, `AssertSame` now uses a fresh validation timestamp so the read remains above `full_history_ts_low` while the snapshot sequence number continues to pin data visibility. For persisted timestamps, it continues reusing the original timestamp to validate the exact timestamped snapshot view.

Reviewed By: joshkang97

Differential Revision: D116110124

fbshipit-source-id: 9cf6cae6e7123f8453b7065565e9e2e0eb34321a
2026-08-20 12:32:59 -07:00
Ranjan Banerjee 2846163d9e Fix remote compaction input record count mismatch due to uninitialized FileMetaData stats (#15130)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15130

## Summary

Fix a corruption error in remote compaction where `FilterInputsForCompactionIterator` produces different results on the primary host vs. the remote worker.

**Root Cause:**

When a standalone range tombstone file completely shadows another input file, `FilterInputsForCompactionIterator()` removes the shadowed file from `input_levels_`. This filtering relies on `FileIsStandAloneRangeTombstone()`, which checks `num_entries == num_range_deletions == 1`.

On the primary host, these `FileMetaData` fields are always populated (set during flush/compaction when the file was created). On the remote worker, however, these fields are NOT persisted in the MANIFEST and are only lazily loaded from table properties via `UpdateAccumulatedStats()` during DB open, which is capped at 20 files (`kMaxInitCount`).

If the standalone range tombstone file is not among those first 20 initialized files, its `num_entries` remains 0 on the remote worker. This causes `FileIsStandAloneRangeTombstone()` to return false, so the remote worker does NOT filter the shadowed file and processes all its keys. Meanwhile the primary's expected record count is computed from its (filtered) view, leading to:

```
Corruption: Compaction number of input keys does not match number of keys
processed. Expected 0 but processed 12.
```

**Fix:**

Before constructing the `Compaction` object on the remote worker, explicitly initialize `FileMetaData` stats from table properties for all input files. This ensures `FilterInputsForCompactionIterator` produces consistent filtering results on both sides.

**Introduced by:** D83375779 (2025-09-29), which enabled `FilterInputsForCompactionIterator` on the remote worker by passing `earliest_snapshot` to `PickCompactionForCompactFiles`.

Reviewed By: xingbowang

Differential Revision: D116252973

fbshipit-source-id: 2cb2d61dae1f3982edeaf41f9a0f39d5aef1e789
2026-08-20 10:54:16 -07:00
Josh Kang 03b8d1afa0 Prevent normal flushes from starving error recovery (#15132)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15132

This fixes a liveness bug in background-error recovery. Retryable soft errors allow foreground writes to continue, but recovery must wait for every scheduled background flush worker to exit before it can submit the recovery flush.

While recovery was active, normal flush requests could still enter `flush_queue_`. Atomic flush requests bypass per-column-family deduplication, so writes could accumulate a backlog of normal requests.

For each request, the background worker:

  1. Pops and drops the normal request because BG work is stopped
  2. Sleeps for the one-second background-error backoff
  3. Decrements `bg_flush_scheduled_`: 1 -> 0
  4. Calls `MaybeScheduleFlushOrCompaction()`
  5. Schedules the next queued request: 0 -> 1
  6. Only then signals the recovery thread

The rescheduling happens under the DB mutex before recovery is signaled:

  Queued requests: R1, R2, R3, ...

  Flush worker                      Recovery thread
  ------------                      ---------------
  drop R1
  sleep 1 second                    waiting for scheduled == 0
  scheduled: 1 -> 0
  schedule R2: 0 -> 1
  signal -------------------------> wake, observe scheduled == 1
                                    wait again

  drop R2
  sleep 1 second
  scheduled: 1 -> 0
  schedule R3: 0 -> 1
  signal -------------------------> wake, observe scheduled == 1
                                    wait again

The dropped request is not requeued. Instead, the worker exit path guarantees that the next queued request is scheduled while `unscheduled_flushes_ > 0`.

A finite backlog therefore delays recovery by roughly one second per request. If writes keep producing requests as quickly as workers discard them, the queue may never drain and recovery is starved indefinitely. Recovery can complete after writes become quiescent, so this is a liveness failure rather than a lock deadlock or data-corruption bug.

There is also a queue-accounting edge. Scheduling consumes `unscheduled_flushes_`, but a worker does not own its `FlushRequest` until it pops `flush_queue_`. A worker can exit on the background error before popping, leaving:

  flush_queue_.size()  > 0
  unscheduled_flushes_ == 0

Clearing only unscheduled requests therefore misses scheduled-but-unclaimed requests.

The fix:

- Reject new non-recovery requests while background work is stopped.
- Allow error-recovery flush reasons to bypass that guard.
- Clear the entire flush queue before recovery waits for scheduled workers.
- Retain the worker-side reason check for requests queued before the error.
- Rebuild recovery flushes and schedule a catch-up flush for writes made during recovery.

Reviewed By: pdillinger

Differential Revision: D116708546

fbshipit-source-id: 6e29fd3653ef72d047c64ca89d311a7bee51ab84
2026-08-20 10:54:15 -07:00
Xingbo Wang 42fca1bb39 Enable core dumps for sanitizer crash tests (#15131)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15131

ASAN and other sanitizer runtimes can disable coredumps or terminate without a core after the test driver sets its shell core limit. This leaves sanitizer stress-test failures with an LLDB stack but no core for the DB artifact.

Configure ASAN_OPTIONS, TSAN_OPTIONS, and UBSAN_OPTIONS during sanitizer crash-test steps so reports terminate with a core-generating abort and core dumping remains enabled. Preserve caller-provided options and the existing Make fallback semantics. Keep the behavior scoped to test steps and crash targets.

Enabling sanitizer cores raises a concern that the generated core-dump artifacts can be large because they include the process working set. Reduce RocksDB's cache and memtable contribution to the core size without replacing the normal randomized test configuration. Add a db_crashtest cache/write-buffer size multiplier that is applied after each iteration chooses its values, and pass 0.5 from ASAN, TSAN, and UBSAN crash lanes. For the simple profile, cache_size remains randomized with 8/32 MiB becoming 4/16 MiB, while the 32 MiB write buffer becomes 16 MiB. Preserve RocksDB's 64 KiB minimum write-buffer size and leave non-sanitizer lanes unchanged.

Reviewed By: rban1

Differential Revision: D116666171

fbshipit-source-id: aa157cfa255bb637d4a68e54ea2039db779fdc53
2026-08-20 04:59:40 -07:00
Peter Dillinger d3a82f28ce Optimize SstPartitioner overhead in compaction via ShouldPartitionByPrefix() (#15118)
Summary:
When a column family configures an SstPartitioner, compaction paid a per-key cost for every key-value written to an output SST: a full user-key copy (into `last_key_for_partitioner_`) plus a virtual `SstPartitioner::ShouldPartition()` call (which also builds a PartitionerRequest and truncates/compares two Slices).

This is motivated by an internal user whose custom SstPartitioner parses a variable-length prefix (a prefix code) out of each key to decide partition boundaries; today that prefix parsing runs on every key written during compaction, on top of the copy and virtual call above.

This wires up the `SstPartitioner::ShouldPartitionByPrefix()` API (added, but unused, alongside the format_version=8 groundwork) so compaction can skip that per-key work. When a partitioner can express its next boundary as a key prefix, `CompactionOutputs` caches that prefix once per output file (re-derived from the file's first key) and replaces the per-key copy + virtual call with an inlined `Slice::starts_with()` check. That also lets a partitioner like the one above do its prefix parsing once per output file instead of once per key. When the partitioner returns no prefix (ambiguous, e.g. `SstPartitionerFixedPrefix` for keys shorter than its length), compaction falls back to the previous per-key `ShouldPartition()` path. Output file boundaries are byte-for-byte identical to before. `SstPartitionerFixedPrefix` implements `ShouldPartitionByPrefix()`; the base-class default returns "no prefix", so existing custom partitioners are unaffected until they opt in.

Also:
- db_bench: new `--sst_partitioner_fixed_prefix_len` to configure a fixed-prefix partitioner (enables benchmarking; no default behavior change).
- `ShouldPartitionByPrefix()` API doc updated now that it is used and recommended for compaction CPU efficiency (any prefix code works, not just fixed-size).

No new option and no behavior or format change.

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

Test Plan:
Added db_compaction_test cases covering the new fast path and its fallbacks: many keys sharing a prefix (one output file per prefix, no spurious cuts), keys shorter than the fixed prefix length (fallback to ShouldPartition), the zero-length-prefix "never partition" case, and short<->long key transitions across output-file boundaries. Existing SstPartitioner tests and the range tombstone + partitioner tests (db_range_del_test) provide regression coverage that file boundaries are unchanged, including the fast path with range tombstones. The crash test already exercises the partitioner via `enable_sst_partitioner_factory`.

Benchmark (db_bench `compact` of a pre-built 50M-key DB; 16B keys / 8B values, 8-byte shared key prefixes with FixedPrefix length 8, no compression, single subcompaction). A/B compares this change against the same binary with only the compaction hot-path change reverted. Means of 6 trials, optimized faster on every trial and every metric; both variants produced identical output (50 files, i.e. identical partition boundaries):

| Metric                          | Baseline | Optimized | Delta  |
|---------------------------------|----------|-----------|--------|
| Instructions (whole process)    | 92.46 B  | 90.09 B   | -2.6%  |
| CPU cycles                      | 46.20 B  | 45.28 B   | -2.0%  |
| CPU time (task-clock)           | 18.74 s  | 18.39 s   | -1.9%  |
| `compact` wall time             | 20.58 s  | 20.08 s   | -2.4%  |

DB-internal compaction stats corroborate: CompMergeCPU 15.84s -> 15.42s (-2.6%), compaction write throughput 38.9 -> 39.9 MB/s. The relative gain shrinks with larger values or compression enabled, where the partitioner is a smaller fraction of total compaction work; it is a pure win whenever a prefix partitioner is configured.

Reviewed By: rban1

Differential Revision: D116372363

Pulled By: pdillinger

fbshipit-source-id: 63de190329112aa2ce5b48da9fc77ef386494558
2026-08-19 11:43:10 -07:00
Xingbo Wang d8536d3274 Enable continuous verification for secondary stress tests (#15114)
Summary:
PR https://github.com/facebook/rocksdb/issues/15066 fixed the bug that has caused this combination fails in the past. Re-enable this combination in the stress test.

db_crashtest.py currently disables continuous_verification_interval whenever test_secondary=1. This change removes that quarantine and enables continuous verification for secondary DB stress tests.

The disable predates auto_refresh_iterator_with_snapshot. When auto-refresh was later added, its stress-test integration mechanically added snapshot setup to several iterator paths, including the secondary-
only NonBatchedOpsStressTest::ContinuouslyVerifyDb() path:

snapshot = std::make_unique<ManagedSnapshot>(db_);
read_opts.snapshot = snapshot->snapshot();

This creates a snapshot from the primary db_ and passes it to an iterator created from secondary_db_. That was never supported: a ReadOptions snapshot must belong to the DB being read, and secondary DB
iterators explicitly reject non-null snapshots.

The incompatibility went unnoticed because the existing sanitizer made this combination unreachable in recurring crash tests. It was therefore a stress-test integration bug, not evidence that snapshot-
preserving auto-refresh was supported by secondary DBs.

This change:

- Stops forcing continuous_verification_interval=0 for secondary tests.
- Avoids attaching a primary-owned snapshot to secondary iterators.
- Continues exercising auto_refresh_iterator_with_snapshot through primary iterator paths.

Secondary iterators still support ordinary Iterator::Refresh(), which advances them to the latest caught-up secondary state. Snapshot-preserving automatic refresh remains unsupported.

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

Test Plan:
- python3 tools/db_crashtest_test.py
- make check-sources
- AUTO_CLEAN=1 make -j192 db_stress
- Targeted blackbox crash test with:
- test_secondary=1
- continuous_verification_interval=1000
- auto_refresh_iterator_with_snapshot=1

Reviewed By: jaykorean

Differential Revision: D116381630

Pulled By: xingbowang

fbshipit-source-id: a162ab357a537c2e5047c1eda85d0c2138b56ed1
2026-08-19 11:34:01 -07:00
zaidoon 136d0d1fa1 Add UDI index mode API vocabulary (#14959)
Summary:
Part 2 of 13 in the UDI split.

Stack order:
1. https://github.com/facebook/rocksdb/issues/14954 Add IndexFactory compatibility names
2. https://github.com/facebook/rocksdb/issues/14959 Add UDI index mode API vocabulary
3. https://github.com/facebook/rocksdb/issues/14960 Add optional UDI builder protocols
4. https://github.com/facebook/rocksdb/issues/14961 Add built-in index factory wrappers
5. https://github.com/facebook/rocksdb/issues/14969 Add built-in index factory tests
6. https://github.com/facebook/rocksdb/issues/14962 Promote IndexFactory as UDI SPI
7. https://github.com/facebook/rocksdb/issues/14963 Wire IndexFactory through block-based tables
8. https://github.com/facebook/rocksdb/issues/14964 Add IndexFactory table routing tests
9. https://github.com/facebook/rocksdb/issues/14970 Add IndexFactory parallel routing tests
10. https://github.com/facebook/rocksdb/issues/14965 Account UDI blocks as index blocks
11. https://github.com/facebook/rocksdb/issues/14966 Support parallel AddIndexEntry in trie index
12. https://github.com/facebook/rocksdb/issues/14967 Cover trie index modes
13. https://github.com/facebook/rocksdb/issues/14968 Add IndexFactory stress and benchmark flags

Previous: https://github.com/facebook/rocksdb/issues/14954.
Next: https://github.com/facebook/rocksdb/issues/14960.

Depends on https://github.com/facebook/rocksdb/issues/14954. Until https://github.com/facebook/rocksdb/issues/14954 lands, GitHub shows the cumulative diff against main. After that lands, the intended review diff is:

`zaidoon/udi-01-index-factory-compat-shim..zaidoon/udi-02-index-mode-api`

That final review delta is 4 files with 52 insertions.

What changed:
- Add inert IndexMode and ReadIndex vocabulary to the public options headers.
- Block those experimental enum fields from automatic C API generation until the explicit C API mapping lands later in the stack.
- Keep behavior unchanged. Runtime wiring comes in later PRs.

Validation:
- `git diff --check`
- `make check-sources`
- `python3 tools/c_api_gen/verify_generated_up_to_date.py`
- `AUTO_CLEAN=1 make check-c-api-gen`
- `./c_test`

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

Reviewed By: jaykorean

Differential Revision: D116211178

Pulled By: xingbowang

fbshipit-source-id: 42edd3f0991d90b0b5c90232e5023de6e3740dac
2026-08-19 10:31:27 -07:00
Anand Ananthabhotla 49faa51e11 Fix crash test Restore() failure with empty trace when replay_write_ops == 0 (#14840)
Summary:
`ExpectedState::Restore()` always attempted to open and replay the post-`saved_seqno_` trace. In the crash-test failure, recovery had already returned the DB exactly to `saved_seqno_`, so `replay_write_ops == 0` and the trace was legitimately empty. `FileTraceReader` reported `Status::Incomplete()` on that empty trace, which incorrectly failed restore.

Skip the trace replay work when `replay_write_ops == 0`, but still run the normal state promotion, cleanup, and `saved_seqno_` reset path so later saves continue to work.

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

Reviewed By: xingbowang

Differential Revision: D107968797

fbshipit-source-id: b4024e1ff329aa3bd47f0307e67d9a4f224b66d4
2026-08-18 21:09:30 -07:00
Anand Ananthabhotla 7d18ffe316 Ignore OPTIONS files for manifest recovery advance (#15106)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15106

When `optimize_manifest_for_recovery` is enabled, `MaybeUpdateNextFileNumber` uses on-disk files to decide whether recovery must append a `SetNextFile` edit. `OPTIONS-*` and temp options files are written as sidecar option snapshots during open, after recovery, so their presence alone should not force MANIFEST growth on a later clean reopen.

Exclude `kOptionsFile` and `kTempFile` from the trigger that requires a MANIFEST file-number advance. If any real file requires an advance, the code still advances past the largest observed file number, including OPTIONS files.

Also tighten db_stress strict MANIFEST verification so fatal zero-growth checks only run when all fault injection is disabled. Open write/sync fault injection can legitimately make `reuse_manifest_on_open` fall back to a fresh MANIFEST.

Reviewed By: xingbowang

Differential Revision: D116092705

fbshipit-source-id: 65b57b0b7f57b2cef42efc4df353fb2218688d34
2026-08-18 17:53:22 -07:00
Xingbo Wang db1adf47bb Add bootstrap configuration for RocksDB external tables
Summary:
Add an opaque external_table_config option to the external table adapter. RocksDB persists the string in OPTIONS files and passes it to ExternalTableFactory::Configure during bootstrap, allowing implementations such as Nimble to parse implementation-specific settings without adding RocksDB-specific fields. The option is immutable after DB open, and attempted runtime updates fail safely for non-clonable table factories. A reopen test verifies that callers can supply a different valid configuration between DB opens. Initialize the adapter option metadata lazily as const to avoid namespace-scope dynamic initialization and mutable-global lint issues.

PR https://github.com/facebook/rocksdb/pull/15119

Reviewed By: anand1976

Differential Revision: D116362444

fbshipit-source-id: 225a4aedc455f320b5c90ac2f0862917da477ccb
2026-08-18 10:12:47 -07:00
Xingbo Wang bbdcd2825f Add remote storage liveness crash test signal (#15102)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15102

Add a dedicated Skycastle signal that invokes liveness through the existing remote-storage test-driver path.

For remote DB runs, double default wrapper/no-progress timeouts while preserving explicit overrides. Default remote liveness `expected_values_dir` to local storage so `UniqueIdVerifier` never treats a remote DB path as a local path; local liveness behavior remains unchanged.

Normalize both equals and spaced `--env_uri`/`--fs_uri` forms once so remote detection, workload execution, and cleanup share the same arguments. Track the crash harness and internal release scripts as CI source triggers, scoped so release-script changes select only the remote-storage signal. Add command-level regression coverage and failure-safe expected-values cleanup. Use generic remote DB/storage terminology throughout public RocksDB code and comments.

Reviewed By: pdillinger

Differential Revision: D115936164

fbshipit-source-id: e4997bf19eec18e4914626d479690b78c6cd3ad2
2026-08-17 19:30:37 -07:00
Peter Dillinger 495c21dc5c Unify lazy wide-column blob read verify policy; tool coverage (#15115)
Summary:
Follow-ups to the lazy wide-column read API (DB::GetEntityLazy / MultiGetEntityLazy). Three related, low-risk changes under one theme; no public API change.

1. Internal read-API cleanup (retires TODO(lazy-blob-resolution-cleanup)). The lazy byte-range work had left a whole/range/force-verify method fan-out on the blob read path: VersionBlobFetcherBase::FetchBlobRange plus a separate FetchBlobForceVerify, and SameFileBlobReader::GetSameFileBlob paired with GetSameFileBlobRange, with the (verify_checksums, force_verify) decision duplicated across branches. This folds them into a single 3-valued BlobVerifyPolicy (kVerifyIfPresent / kVerifyIfNoAmplification / kSkip, added to db/blob/blob_constants.h) derived once at the resolver boundary and threaded down. FetchBlobRange and GetSameFileBlob now take a range descriptor (range_length == kWholeBlobLength means the whole value) plus the policy; FetchBlobForceVerify and the *Range twins are removed. The public LazyColumnReadRequest::force_verify bool is unchanged. The widely-called lower-layer whole-value primitives (BlobFileReader / BlobSource / Version GetBlob) are intentionally left as delegated primitives to bound blast radius; folding those is a later cleanup.

2. Embedded block-cache-tier quirk fix (retires TODO(lazy-blob-resolution-quirks)). ValidateEmbeddedBlobIndex rejected read_tier == kBlockCacheTier before the blob-cache probe, so a same-file (embedded) column already in the blob cache returned Incomplete under kBlockCacheTier -- unlike the separate-file path, which probes the cache first. The up-front rejection moves to the two no-cache paths (ResolveEmbeddedBlob / ResolveEmbeddedBlobPinned); the cache-probing paths now serve a cached record under kBlockCacheTier, matching separate-file behavior. This is a shared lower-layer change: the non-lazy Get / GetEntity and iterator embedded read paths (get_context / MaybeResolveEmbeddedValue -> ResolveEmbeddedBlobCached) get the same corrected behavior, not just the lazy resolver.

3. Coverage. db_bench gains a readrandomentitylazy benchmark (GetEntityLazy + MultiResolve of --lazy_entity_read_length bytes per column) for measuring byte-range (partial) blob read savings; it requires open_files == -1. The crash test now also fuzzes LazyColumnReadRequest::force_verify in the lazy-vs-eager differential (force_verify affects only verification, not the returned bytes, so the comparison is unchanged).

This also sets up the next planned step -- cross-key coalescing for MultiGetEntityLazy / LazyWideColumnsBatch::MultiResolve. The internal BlobReadRequest already carries a {offset, len} range descriptor, and with the verify decision now a single BlobVerifyPolicy rather than a bool plus a separate force-verify method, a batched resolve can group a vector of range+policy requests per (CF, Version, blob file) and coalesce them onto the existing MultiGetBlob machinery -- without re-introducing whole/range/force-verify method twins in the multi-read path.

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

Test Plan:
- New unit test DBLazyEntityTest.EmbeddedBlockCacheTierServedFromCachedValue: a cached embedded column serves whole- and sub-range reads under kBlockCacheTier instead of returning Incomplete. Existing force_verify / BlockCacheTier tests are unchanged (public behavior preserved).
- New unit test DBBlobIndexTest.EmbeddedBlobWideColumnGetEntityBlockCacheTierServesCached: covers the non-lazy GetEntity path -- a cached embedded column is served under kBlockCacheTier; the existing ...GetEntityBlockCacheTier test still asserts a genuine miss yields Incomplete.
- Crash test: extended the existing lazy differential to fuzz force_verify (reuses --lazy_entity_read_one_in).
- db_bench: readrandomentitylazy over 2000 entities x 2 uncompressed embedded blob columns (4000 B each), reading 100 B per column:

| metric | value |
|---|---|
| rocksdb.blobdb.lazy.partial.read.count | 4000 |
| rocksdb.blobdb.lazy.read.bytes | 400000 |
| rocksdb.blobdb.lazy.partial.bytes.saved | 15600000 |

i.e. 100 B read per column instead of the 4000 B whole-column value.

Reviewed By: xingbowang

Differential Revision: D116334969

Pulled By: pdillinger

fbshipit-source-id: cebe90456b23be0f5be60cdc4bff85c8a37b5442
2026-08-17 17:06:32 -07:00
Anand Ananthabhotla 575b6df1b8 Force immediate creation of a new MANIFEST if the current one cannot be reused (#15103)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15103

The crash artifact showed `MANIFEST-000005` was physically well-formed but contained an incomplete atomic group that had been tolerated at EOF. `reuse_manifest_on_open` then appended ordinary records after that tail, turning the tolerated crash tail into mid-file `corrupted atomic group` corruption seen by OpenAndCompact and backup restore.

Record whether MANIFEST recovery ended with a pending atomic group. When that marker is set, skip `ReopenManifestForAppend()` so the next manifest write creates a fresh MANIFEST instead of appending after the partial group. The existing physical-size guard remains as a separate torn-tail defense.

This replaces the earlier keep-size preallocation hypothesis with the artifact-backed root cause.

Reviewed By: xingbowang

Differential Revision: D116062618

fbshipit-source-id: fb55ce2891a6fd50fa2c670dc720e91bad27e94d
2026-08-17 16:18:06 -07:00
Peter Dillinger f0c1e3d7d9 Fix cleanup-on-failure and flaky liveness check in point_lock_manager_stress_test (#15116)
Summary:
The point_lock_manager_stress_test (via PointLockValidationTestRunner) was seen flaky under USE_CLANG=1 COMPILE_WITH_ASAN=1 COMPILE_WITH_UBSAN=1, manifesting as a heap-use-after-free plus "terminate called without an active exception" / SIGABRT. There were two distinct problems.

Cleanup on failure: run() uses gtest ASSERT_* macros, which return early from run() on failure. That early return skipped the shutdown flag set and the join of the worker threads, so the worker threads kept running and accessed members of the (now destroyed) test runner object (heap-use-after-free), and destroying the still-joinable threads_ vector called std::terminate. Fixed by adding a destructor plus a StopAndJoinThreads() helper that always signals shutdown and joins the worker threads before any member is destroyed, no matter how run() exits. Copy/move are deleted since the object owns worker threads.

Underlying flaky failure: the per-second "No locks were acquired in the last 1 second" liveness assertion was too strict. Under sanitizer builds combined with very short lock timeout/expiration and post-acquisition sleeps, all threads can occasionally fail to acquire a single lock within a given one-second window without the lock manager actually being stuck. The per-thread variant of this check was already disabled for this reason; the aggregate one is now relaxed to tolerate a few consecutive no-progress seconds, which still reliably detects a genuinely stuck (dead/livelocked) lock manager. Also fixed the end-of-run check from `>= 0` (always true) to `> 0` so it actually catches a fully stuck manager.

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

Test Plan:
Verified the cleanup path by temporarily forcing the liveness assertion to fail while worker threads were active: the test now reports a normal gtest failure with clean teardown, with no heap-use-after-free and no std::terminate/SIGABRT.

I don't have a consistent reproducer for the failure but ran
* 3x `USE_CLANG=1 COMPILE_WITH_ASAN=1 COMPILE_WITH_UBSAN=1 make check`
* `USE_CLANG=1 COMPILE_WITH_ASAN=1 COMPILE_WITH_UBSAN=1 rocksptest point_lock_manager_stress_test -r100`

Reviewed By: xingbowang

Differential Revision: D116349420

Pulled By: pdillinger

fbshipit-source-id: 104784c5c59770414224ac4751eabf69f65e0958
2026-08-17 16:01:35 -07:00
Andrei Preda b616b203b8 Drop flushed WAL data from the secondary's active memtable (#15066)
Summary:
Fix https://github.com/facebook/rocksdb/issues/15051: a secondary serves stale reads because entries replayed from a WAL the primary has since flushed are never evicted from the active memtable. During TryCatchUpWithPrimary(), seal the active memtable once the installed Version provably covers everything it holds, so the following RemoveOldMemTables() call drops it. Every memtable-dropping decision takes that same watermark, so a file set the secondary cannot read never turns the stale read into a vanished key.

## Why and How

A secondary replays the primary's WALs into its active memtable and stops replaying a WAL once the primary flushes it. Entries already replayed stayed behind, and since reads consult the memtable first, they shadowed the newer flushed values indefinitely. Iterators go stale too once bottommost compaction rewrites the flushed entry's sequence number to 0; until then Get() and an iterator disagree.

Seal the active memtable during catch-up, gated on the installed Version: each point-in-time Version now carries the column family's log number it reflects, exposed via the new
ReactiveVersionSet::GetInstalledVersionLogNumber(). Sealing requires cf_id_to_current_log_[cfd->GetID()] < that number, which guarantees the primary flushed everything the memtable holds (unflushed WAL data is kept) and that the flushed data is readable through the installed Version. The gate asks the Version directly because proxies fail: the log number alone advances even when no Version was installed, and "no missing files" misses atomic_flush groups parked by a sibling's missing file and corrupt-but-present files with
verify_sst_unique_id_in_manifest=false. In those cases the memtable holds the only readable copy, so dropping it would turn a stale read into a vanished key; the gate fails closed and keeps it.

Reconcile every initialized column family, not just cfds_changed: a round that failed after advancing the log number leaves a stale memtable that later rounds see no new MANIFEST record for. It can also leave a collectible immutable memtable, so the loop consults the new O(1) MemTableList::HasOldMemTablesToRemove() before skipping a column family. The predicate keeps the skip precise: reconciling any column family with a non-empty immutable list would be correct but wasteful, installing a fresh SuperVersion every catch-up call for memtables legitimately waiting on a primary flush and costing readers a mutex acquisition each time. With it, no extra super versions are installed in the steady state. That predicate and the RemoveOldMemTables() cutoff take the installed Version's log number as well, not cfd->GetLogNumber(): that advances as soon as the flush record is read, so an immutable memtable sealed on a WAL switch could otherwise be freed while the flushed file superseding it is unreadable. A sealed memtable is stamped with cf_id_to_current_log_[cfd->GetID()] + 1, the log number following its contents.

cf_id_to_current_log_ is keyed by column family id rather than ColumnFamilyData*, because ids are never reused while a dropped column family's pointer can be recycled, and it is recorded before WriteBatchInternal::InsertInto() rather than after: a batch stops at its first failure with earlier entries already inserted, so recording afterwards could leave the map naming an older WAL than the memtable holds. While the watermark lags and uncollectible memtables accumulate, the secondary warns, since it has no write path to enforce max_write_buffer_number.

The seal sequence, shared with RecoverLogFiles() and the follower's TryCatchUpWithLeader(), is extracted into
DBImplSecondary::SealActiveMemtable(). The follower is refactored onto it with no behaviour change: it keeps cfd->GetLogNumber() as its cutoff because that catch-up path never replays WALs, so its memtables hold nothing that only a not-yet-installed Version could make readable.

## Testing

New tests in db_secondary_test cover: the stale read via both Get() and an iterator, before and after bottommost compaction; unflushed WAL-only data surviving sealing, including two column families sharing a WAL; no data loss with a missing, atomic_flush-sibling-missing, or corrupt flushed file; an immutable memtable sealed on a WAL switch surviving an unreadable flushed file; a batch rejected partway through replay leaving the recorded WAL no older than the memtable's contents; and reconciliation after a failed round (injected via FaultInjectionTestFS at a sync point). Each mechanism was individually neutralized and confirmed to fail its test.

Verified with build_tools/rocksptest.sh over db_secondary_test, db_follower_test, memtable_list_test, version_set_test and version_edit_test (all pass, also under ASSERT_STATUS_CHECKED=1), plus db_basic_test, corruption_test, repair_test and db_flush_test; the secondary tests also ran 20x under COERCE_CONTEXT_SWITCH=1 with no flakes. make format-auto and make check-sources are clean.

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

Reviewed By: jaykorean, anand1976

Differential Revision: D115317523

Pulled By: xingbowang

fbshipit-source-id: a1d4615eb951d75e69ed242731336485c0dfa137
2026-08-17 13:47:30 -07:00
Anand Ananthabhotla 92a6308300 Verify compressed block bytes after checksum computation (#15098)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15098

Compute the base block checksum in `CompressAndVerifyBlock()` before `verify_compression` decompresses the compressed output. Parallel compression stores that base checksum on the block rep so worker threads can keep the CPU-heavy checksum and verification work off the ordered write path; the writer only applies the offset context modifier before writing the trailer.

Update the regression test to cover both serial and parallel compression paths.

Reviewed By: pdillinger

Differential Revision: D115950785

fbshipit-source-id: b5e47f7d7c41680e7e7907443fc805bc0d043d6e
2026-08-17 12:34:23 -07:00
Peter Dillinger 021374c62c Start format_version=8 development toward new features (#15104)
Summary:
format_version background: SST files are shared across RocksDB versions. format_version is a critical tool in ensuring prior (and later) versioned readers do not misinterpret a file created by some version. Opt-in features can sometimes rely on previously-rejected corners of the schema space, but not always.

This change: lands much of the format_version=8 (fv8) format surface as an internal-only draft -- readable and testable but not user-writable -- so upcoming features can build on a stable base. fv8 is NOT published: kLatestBbtFormatVersion stays 7, and reading or writing fv8 is gated behind the existing TEST-only TEST_AllowUnsupportedFormatVersion() hook. Publishing fv8 (raising the ceiling) is deferred to a later change.

The pieces of fv8 added here, and what each unlocks:

- Index value-delta escape. Existing behavior: index blocks (format_version >= 4) store each block handle as a delta against the previous entry's handle, and signal "this value is a full handle instead" by reusing the key sentinel `shared == 0`. Because the two are coupled, an index entry whose handle is not contiguous with the previous one must be forced to a restart, costing a full key as well as a full value. fv8 decouples them with an in-value escape codepoint: varint 0 means "a full BlockHandle follows," and genuine size deltas are remapped to zigzag(delta)+1 so that 0 stays free. A non-contiguous entry can then remain an ordinary key-delta-encoded, on-cadence entry. The escape applies to every index-shaped block, which includes the top-level index of index partitions and the index of filter partitions, not just the index over data blocks. Unlocks: the upcoming optimize_key_common_prefix index feature (removes its need to special-case super_block_alignment) and value-delta encoding for embedded-blob SSTs (previously disabled outright because interleaved blob records break handle contiguity). Guarded strictly on fv8; format_version <= 7 encoding and decoding are byte-for-byte unchanged. Note that restart points must not delta-encode, so they still have full values without a sentinel.

- Data block footer bit 30 is reserved as an "extended metadata present" escape. Decode rejects a set bit as unsupported/corrupt at all versions. Unlocks: adding block-level metadata in the future through the one shared footer decoder, without another format_version bump or plumbing format_version into every block parser. (Older reader versions relied on this bit being zero for some integrity/consistency checking; format_version bump now makes it safe to use.)

- Footer metaindex gap. For fv8, the top 16 bits of the footer's 4-byte metaindex-size field record the gap (in bytes) between the end of the metaindex block and the start of the footer; the low 16 bits hold the metaindex block size. Unlocks: a space very near the end of the file to save full file checksum state, for a "files carry their own checksums" feature that doesn't require SST parsing to extract the checksum states (to run them to completion on the trivial file tail).

- Reserved "user_key_common_prefix" table property. The reader returns NotSupported when it is present and non-empty. Unlocks: a future whole-file optimization that stores the common prefix of all user keys once instead of in every restart key; until implemented, fv8 readers refuse such a file rather than mis-read it.
  - Because of the problem of inferring such a prefix at the start of constructing an SST file, we expect it will rely on SstPartitioner. Here we've added SstPartitioner::ShouldPartitionByPrefix() (not yet used) that will enable this optimization, and avoid the overhead of a virtual callback to SstPartitioner on every key written to an SST file.

Supporting cleanups: the Footer's "unset" format_version sentinel is named (kInvalidFormatVersion) and the FormatVersionUses*() predicates assert against it, as each is a `version >= N` test that an unpopulated Footer would silently satisfy. Index block reader entry points (Block::NewIndexIterator() and friends) drop their defaulted trailing arguments so each caller states which codec it expects, and BlockBuilder::Add() takes delta_value by const reference rather than a pointer whose null case no caller used.

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

Test Plan:
New unit tests, all exercising fv8 through TEST_AllowUnsupportedFormatVersion():

- block_test: value-delta escape codec round-trip (delta 0, negative, large, and the escape; asserts format_version <= 7 bytes match the legacy encoding and a legacy 0x00 still decodes as delta 0), and rejection of footer bit 30 at all versions.

- db_flush_test: fv7-vs-fv8 read equivalence for super_block_alignment, including the separate_key_value_in_data_block intersection, confirming padding events (hence escape entries) occur and fv8 index values are delta encoded.

- sst_file_reader_test: fv7-vs-fv8 read equivalence for embedded-blob SSTs, confirming fv8 (unlike fv7) delta-encodes their index values and actually emits escape entries (counted through a sync point).

- block_based_table_reader_test: a builder+reader test that injects a footer metaindex gap via a sync point (verifies the gap bytes are written and the reader locates the metaindex via the recorded gap, and that too large a gap surfaces as a builder error), and that a non-empty reserved user_key_common_prefix property makes the reader return NotSupported.

- table_test: two-level index with partitioned filters and index_block_restart_interval > 1, the configuration in which both top-level indexes delta-encode their values; verifies scan and point lookup round trips at each tested format_version including fv8.

- (Not strictly fv8) Basic SstPartitionerFixedPrefix tests in slice_test (ShouldPartition, ShouldPartitionByPrefix, their mutual consistency, and CanDoTrivialMove), which had no non-DB coverage before.

fv8 is also added to the table_test harness (GenerateArgList) and the shared footer version set (test::kFooterFormatVersionsToTest), so the existing table-level and DB-level property/checksum suites now also run at fv8. And added to crash test (minor revisions to db_stress).

## Performance details
Release db_bench (PORTABLE=0, -march=native) A/B/C on the read/write fast path,
comparing (a) parent, (b) this commit @ fv7, (c) this commit @ fv8. All DBs on
tmpfs, 20M keys, block_size=2048, compression=none. Note index_block_restart_
interval=16 (not the default 1) is required to actually exercise the fv7<->fv8
index value-delta codec -- with restart interval 1 every index entry is a full-
handle restart and the changed path never runs (confirmed delta-value=1 via
sst_dump). Reads single-thread + core-pinned, warm cache, interleaved/rotated
rounds, paired deltas.

Reads (readrandom + seekrandom, non-partitioned single ~1.9MB index block and
partitioned index+filters): fv7->fv8 in the *same* binary is within noise on all
four workloads (largest -1.0%, CIs cross zero). before->after (fv7 path) is also
within noise; perf shows a deterministic +0.4-0.6% instructions/op from the added
branches (the format_version>=8 value_delta_escape guard + refactored data-block
footer decode), which is below the throughput noise floor.

Writes (fillseq + compact total CPU): fv7->fv8 null. compact (which rewrites all
index blocks) is null before->after. fillseq showed +3.5% CPU before->after, but
perf attributes it entirely to cycles/IPC with *identical* instructions/op
(+0.01-0.04%) -- a code-layout effect on the untouched memtable-insert loop, not
this change.

Sanity: any single throughput/CPU delta <~1% is within the build-to-build noise
floor, measured directly by comparing two byte-identical binaries (swings up to
~0.9% reads; the pre-native/PORTABLE=1 build showed a spurious -1.85% that
vanished under -march=native).

Net: no measurable regression from fv8 draft; fv7 encoding/decoding byte-for-byte
unchanged, only the noted sub-1% instruction bump on the shared read path.

Reviewed By: xingbowang

Differential Revision: D116105558

Pulled By: pdillinger

fbshipit-source-id: ead776cdd396d497d47872c0956ea8c651c0d13f
2026-08-17 12:08:59 -07:00
Josh Kang 9475798205 Refresh recovery plan after error escalation (#15111)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15111

T284514290 exposed an online-recovery race between the automatic recovery coordinator and a foreground synchronous write. A retryable no-WAL flush error selected lightweight retry-flush recovery. While that recovery attempt waited for the failed background flush, `Delete(sync=true)` appended and synced sequence N to the active WAL, then failed while persisting WAL-tracking metadata to the MANIFEST. Because memtable insertion and sequence publication happen after that MANIFEST step, the record was durable but logically unpublished.

OLD: the concurrent error triggered another attempt, but the attempt reused a stale recovery plan. That preserved both ingredients needed for a duplicate: the first `Delete(key, N)` remained in the active WAL, while the published sequence remained `N-1`.

```
Recovery coordinator                         Foreground sync-write thread
------------------------------------         --------------------------------
[db_mutex held]
plan = RetryFlush
(copied once before the retry loop)

attempt 1: ResumeImpl(plan)
  WaitForBackgroundWork()
  bg_cv_.Wait()
  [atomically releases db_mutex]  ---------> assign sequence N
                                             append + sync Delete(key, N)
                                             [db_mutex held]
                                             MANIFEST WAL edit fails
                                             no memtable insert
                                             no LastSequence publication
                                             write returns I/O error

                                             recovery_error_ = Hard
                                               (requests another attempt)
                                             bg_error_ = Hard
                                             recover_context_ eventually
                                               becomes FullRecovery
                                             [db_mutex released]
  [db_mutex reacquired]          <---------
  ClearBGError() sees recovery_error_
  attempt 1 cannot clear the error

attempt 2:
  recovery_error_ = OK
  ResumeImpl(plan)
             ^
             +-- still the cached RetryFlush plan

  RetryFlushesForErrorRecovery()
    retries the failed immutable flush only
    does not call SwitchMemtable()
    does not switch the active WAL
  ClearBGError() succeeds
                                             caller retries Delete(key)
                                             LastSequence is still N-1
                                             => assigns sequence N again
                                             appends Delete(key, N) to same WAL
                                             memtable insert succeeds
                                             publishes LastSequence = N

Reopen / WAL replay:
  first  (key, N, deletion) -> memtable insert succeeds
  second (key, N, deletion) -> TryAgain("key+seq exists")
  normal WAL recovery has seq_per_batch_ == false
  assert(seq_per_batch_) fails
```

The cached argument to `ResumeImpl()` is therefore the direct bridge to the assertion: lightweight retry-flush recovery can report success without retiring the WAL that contains the unpublished record. Since it also does not publish sequence N, the successful caller retry writes the identical internal key into that same WAL.

NEW: the same recovery coordinator consumes the concurrent error through a mutex-ordered handoff.

```
Recovery coordinator                         Foreground sync-write thread
------------------------------------         --------------------------------
[db_mutex held]
attempt 1:
  plan = recover_context_  // RetryFlush
  ResumeImpl(plan)
  WaitForBackgroundWork()
  bg_cv_.Wait()
  [atomically releases db_mutex]  ---------> append + sync Delete(key, seq=N)
                                             [db_mutex held]
                                             MANIFEST WAL edit fails
                                             CheckAndSet...(Hard, FullRecovery):
                                               recovery_error_ = Hard
                                               bg_error_ = Hard
                                               recover_context_ = FullRecovery
                                             [db_mutex released]
  [db_mutex reacquired]          <---------
  ClearBGError() sees recovery_error_
  current attempt cannot clear the error

next retry, with db_mutex held:
  recovery_error_ = OK
  plan = recover_context_  // FullRecovery
  ResumeImpl(plan)
    SwitchMemtable()
    switch to a new WAL
    clear the background error
```

The foreground unlock of `db_mutex_` happens before the recovery coordinator reacquires it and snapshots the next plan. Publishing `bg_error_` and `recover_context_` together under that mutex prevents the next attempt from observing a new severity with an old recovery requirement. `recovery_error_` is the per-attempt latch: a concurrent I/O error cannot be cleared as part of the in-flight attempt; it either stops recovery or forces another attempt to consume the updated context. If another error arrives while `ResumeImpl()` releases the mutex again, the same handoff repeats. No second recovery coordinator is needed.

Manual `Resume()` now uses the retained recovery context instead of reconstructing one from the background-work scheduling flag, and the soft-error clear-only shortcut is allowed only when that flag shows there is no pending recovery work. The write ordering and `assert(seq_per_batch_)` detector are unchanged.

Reviewed By: anand1976, xingbowang

Differential Revision: D116106177

fbshipit-source-id: a78450c0924a326b5302ae6709e367f07bd3d5a6
2026-08-17 09:04:06 -07:00
Josh Kang 323d915dbc Prevent late async-read stats context restoration (#15100)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15100

Fix the TSAN race in T284274705 where `CoroutineStatsRequestData::onSet()` can access its borrowed `Env*` after `AsyncCallback::OnComplete()` allows the caller to destroy that environment.

When `co_await baton` suspends, the Task's implicit `ViaIfAsync` saves the read's statistics `RequestContext` (`S`) as the context needed by the real RocksDB continuation. The old explicit `executor->add()` also ran the Baton bridge under `S`. Consequently, `baton.post()` resumed the Via trampoline under `S`, and the trampoline's own `executor_->add()` caused EventBase to capture `S` a second time as the queued continuation's outer context. After the read removed its stats scope and called `OnComplete()`, the Via guard restored that outer `S`, triggering a late `onSet()` that dereferenced the potentially destroyed `Env*`.

Remove the redundant outer executor hop and call `baton.post()` directly under an explicitly null `RequestContext`. If the read is suspended, `post()` resumes only the Via trampoline; the trampoline still queues the real continuation on the bound read executor. The Via promise retains its saved `S`, so `executeContinuation()` installs `S` while RocksDB executes, but EventBase now captures null as the outer context restored afterward. If filesystem completion happens before `co_await baton`, `post()` only marks the Baton ready and the read continues inline under its existing context.

Thread Race:

`E` = temporary backup `Env`
`S` = statistics `RequestContext` containing `CoroutineStatsRequestData{env_ = E}`
`P` = parent context without RocksDB statistics
`B` = old explicit Baton bridge
`C` = `ViaIfAsync` callback that resumes the real read

```
Caller / stress thread                 RocksDB read EventBase thread
----------------------                 -----------------------------

BackupInfo owns shared_ptr<Env> E

Call MultiGetAsync()
Wait for OnComplete()                  Start the RocksDB read coroutine
                                       Install S:
                                         P -> S
                                         S.onSet()

                                       Call SubmitReadAsync()

                                       No io_uring is available, so:
                                         Read() executes synchronously
                                         filesystem callback runs inline
                                         callback is still under S

                                       Old code calls executor->add(B)
                                       EventBase records:
                                         B.outer_context = S

                                       Reach co_await baton
                                       B has not executed, so Baton is not ready

                                       ViaIfAsync:
                                         saves execution_context = S
                                         gives trampoline V to Baton
                                       Suspend the real read
                                       Leave S:
                                         S -> P
                                         S.onUnset()

                                       Later, EventBase runs B
                                       Install B.outer_context:
                                         P -> S
                                         S.onSet()

                                       B calls baton.post()
                                         -> V resumes inline
                                         -> V queues callback C
                                         -> EventBase records:
                                              C.outer_context = S

                                       Later, EventBase runs C
                                       Install C.outer_context:
                                         P -> S
                                         S.onSet()

                                       Via executeContinuation():
                                         remembers previous context = S
                                         installs saved execution context S
                                         resumes the real RocksDB read

                                       Read finishes
                                       Statistics scope removes S:
                                         S -> P
                                         S.onUnset()

                                       Call OnComplete()
Wait returns          <--------------- OnComplete signals caller

================ PUBLIC READ IS COMPLETE ================

Destroy restored DB                    C is still unwinding
Release BackupInfo                     Via guard restores previous context:
Last shared_ptr<Env> disappears          P -> S
Begin Env E destruction                  S.onSet()
                                           -> E->GetThreadID()

Env::~Env()          <--------------- TSAN race ---------------> S.onSet()
```

The same context was captured in two roles:

```
Via execution_context = S   required while the real read executes
C.outer_context       = S   accidentally restored after the read completes
```

The fix removes bridge `B` and posts under null. For an outstanding asynchronous read, the Via trampoline therefore queues `C` with `outer_context = null`, while retaining `execution_context = S`. The real read still executes under `S`, but after `OnComplete()` the Via guard restores null instead of reactivating `S`.

Reviewed By: anand1976

Differential Revision: D115967513

fbshipit-source-id: 87d744ecdbad3421c4d2a1a7be6423a3f0373573
2026-08-13 22:52:30 -07:00
Peter Dillinger 2dc6bc51b4 Fix TSAN data race on speculative auto_skip_bypassed read (#15094)
Summary:
The parallel-compression auto-skip feature (https://github.com/facebook/rocksdb/issues/15079) introduced a data race that TSAN reports in block_based_table_reader_test. In StateTransition(), the auto-skip loop reads
ring_buffer[cslot].auto_skip_bypassed speculatively, from a not-yet-committed atomic_state snapshot, before the CAS that would validate that snapshot. Unlike every other BlockRep field (touched only after a successful CAS grants exclusive slot ownership), a stale snapshot can point that read at a physical ring-buffer slot the emit thread is concurrently recycling for a new block in EmitBlockForParallel, racing the plain write there. The speculative result is discarded when the CAS fails, so there is no logical corruption, but the plain read/write is a data race (UB) and is flagged by TSAN.

Make auto_skip_bypassed a RelaxedAtomic<bool> so the speculative read and the emit-thread write are atomic. Relaxed ordering suffices: correctness of the auto-skip decision is still gated by the CAS, and when the CAS succeeds the value was published by the emit thread through the existing acquire/release ordering on atomic_state and is not modified elsewhere.

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

Test Plan: Covered by existing block_based_table_reader_test parameterizations that build with parallel compression (parallel_threads > 1) under TSAN, which is where the race was originally reported.

Reviewed By: joshkang97

Differential Revision: D115777206

Pulled By: pdillinger

fbshipit-source-id: 7ee969ec2d95890255d2e8b76eceaa5876b82170
2026-08-12 15:06:31 -07:00
Peter Dillinger 7217bf34a7 Add SST build context: is_remote_compaction, db_name, kSstFileWriter (#15085)
Summary:
Custom filter and compression strategies decide what to build per SST from a FilterBuildingContext (shared by FilterPolicy::GetBuilderWithContext and CompressionManager::GetCompressorForSST). This adds more context so those strategies can adapt to where and how a file is being produced.

FilterBuildingContext gains two fields:
* is_remote_compaction: true only when the file is produced by an offloaded CompactionService worker. Detected with a new virtual CompactionJob::IsRemoteCompaction() (default false) overridden by CompactionServiceCompactionJob, mirroring the existing GetTableFileName hook. Local/fallback compactions and flushes are false.
* db_name: a non-owning Slice naming the DB, populated for flush, compaction, recovery, and repair, and empty only where there is no DB, as with SstFileWriter. Under remote compaction it is the DB as opened by the worker, which can differ from the primary's.

Both are threaded through TableBuilderOptions and populated in BlockBasedTableBuilder::Rep, whose "is this file in the LSM" test becomes a switch with no default case, so that a future TableFileCreationReason has to be classified there.

Separately, TableFileCreationReason is now backed by uint8_t and gains a kSstFileWriter value, which SstFileWriter reports instead of the catch-all kMisc. It is appended after kMisc so existing enumerator values are unchanged; those values are public API, surfaced to EventListener and mirrored by the Java binding (updated here). Note that SstFileWriter neither notifies listeners nor consults CompactionFilterFactory, so for now the new value is only observable through FilterBuildingContext.

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

Test Plan:
* New CompactionServiceTest.IsRemoteCompactionInCompressorContext installs a custom CompressionManager and asserts GetCompressorForSST sees is_remote_compaction == true for the offloaded worker's output files and false for the primary's flushes in the same run, so the flag is shown to be per-file rather than global. IsRemoteCompactionFalseForLocalCompaction covers the same manager with no compaction_service configured.
* DBBloomFilterTest.ContextCustomFilterPolicy now reports db_name as a token (expected / none / UNEXPECTED:<value>), so the value rather than just its presence is checked for flush and compaction, and expects kSstFileWriter instead of kMisc for the external SstFileWriter file.
* bloom_test's "Like SST file writer" case uses kSstFileWriter so that it keeps modelling SstFileWriter.
* Java EventListenerTest references TableFileCreationReason.MISC.getValue() instead of a hardcoded enum byte, so it stays correct across enum changes.

Reviewed By: jaykorean

Differential Revision: D115603616

Pulled By: pdillinger

fbshipit-source-id: ad73fbc391a5377ace406725238ae6eede46c8d9
2026-08-12 14:31:47 -07:00
Xingbo Wang 58d835d951 Add db_stress liveness profile and fix lost flush request (#14830)
Summary:
Add a db_crashtest liveness mode that runs db_stress with a watchdog for
  foreground progress. The profile disables fault injection and uses longer runs
  so failures are more likely to represent real forward-progress bugs instead of
  expected injected I/O errors. The watchdog tracks completed operations, active
  operation type/start time, and per-operation completion counts to distinguish
  stuck writes from long admin operations.

  Bug: The new mode exposed a latent flush scheduling bug. A flush request can be queued
  with an effective max memtable id, then a newer immutable memtable can be added
  before the background flush picks work. PickMemtablesToFlush() could pick only
  the older memtables and still clear flush_requested_. A later queued flush for
  the newer memtable would then be skipped because the column family no longer
  looked flush-pending, leaving the immutable memtable permanently unflushed. In
  stress this surfaced as IngestExternalFile() waiting in WaitForFlushMemTables(),
  with writes eventually blocked behind the same unflushed memtable.

  Fix: Keep flush_requested_ set until all unstarted immutable memtables have been
  picked. Add a direct MemTableList regression for the partial-pick case and a
  DB-level regression covering the ingest/get-live-files ordering that reproduced
  the lost request.

## Testing

- CI
- Local liveness mode 32 full 20-minute runs passed, 0 failures

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

Reviewed By: joshkang97

Differential Revision: D107895052

Pulled By: xingbowang

fbshipit-source-id: ca67c5f9376899eefdc9af7bad4af06200b52446
2026-08-11 18:05:08 -07:00
Michael C Huang 276495843d Add blob file writer buffer size option (#15061)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15061

Adds mutable ColumnFamilyOptions field `blob_file_writable_file_max_buffer_size` to cap blob-file WritableFileWriter buffers independently from DBOptions::writable_file_max_buffer_size.

The default value is 0, which preserves existing behavior by inheriting the DB-wide writer buffer cap. Non-zero values apply to both flush/compaction blob writers and direct-write blob writers. Direct-write updates are propagated through DBImpl::SetOptions to newly opened blob files without requiring a DB restart.

Also wires option parsing, dumping, db_bench, db_stress, crashtest, and release-note coverage.

Reviewed By: xingbowang

Differential Revision: D114979265

fbshipit-source-id: 3f764b797628eee0ce379110c1a1f8a16beb3fa2
2026-08-11 17:38:34 -07:00
Peter Dillinger ec5106b3b8 Fix AI code review hiding details, stale+misleading driver (#15087)
Summary:
The Claude/Codex CI reviewer was posting PR comments like "the review has been written to review-findings.md" instead of the review itself, and the comment footer cited claude_md/code_review.md as the review methodology even though CI stopped feeding that file to the model in https://github.com/facebook/rocksdb/issues/14507 (it switched to claude_md/ci_review_prompt.md). code_review.md had been orphaned from CI for months while still advertising itself as the CI methodology, so edits to it (e.g. the "post directly in your response" note) had no effect on CI. Separately, an over-long review exceeded GitHub's per-comment character limit and was rejected outright, losing the whole review.

Changes:
- ci_review_prompt.md: make the final response the review -- it becomes the PR comment verbatim -- and forbid ending the turn with a meta-summary such as "written to review-findings.md". review-findings.md is demoted to a recovery backstop; the per-phase incremental writes and the turn-limit recovery path are unchanged, so both the normal and recovery comment paths still produce a real review.
- Repoint the "Review methodology" footer in parse-claude-review.js and parse-codex-review.js to claude_md/ci_review_prompt.md (the file CI actually uses), so the citation no longer points at a deleted file.
- build-ai-review-comment.js: cap the assembled comment at GitHub's 65536-char limit, trimming only the model response (the sole unbounded input) and appending a pointer to the execution-log artifact, so an oversized review is truncated cleanly instead of rejected by GitHub. The header, footer, and <details> scaffolding are always preserved.
- Delete claude_md/code_review.md (CI-dead since https://github.com/facebook/rocksdb/issues/14507; superseded locally).
- Add claude_md/manual_code_review.md: a lightweight, single-reviewer local wrapper around ci_review_prompt.md (no multi-agent team/debate, no scratch files, inline output) for running a modest review by hand. It sources its methodology from ci_review_prompt.md so it cannot drift.

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

Test Plan:
- The workflow scripts have no CI unit coverage; verified both parse-*-review.js still parse, and that no reference to code_review.md remains anywhere.
- Dry-ran manual_code_review.md by reviewing the current commit locally; it produced a right-sized single-reviewer report without spawning agents or writing files, and helped find+fix the comment limit issue.
- Exercised the end-to-end GitHub CI path (per https://github.com/facebook/rocksdb/issues/14696, review-workflow changes not yet on main can only be tested from a branch on facebook/rocksdb): pushed these changes to the fix_hidden_review_details_test branch and ran the Claude Code Review workflow (claude-review.yml) via "Run workflow" against PR https://github.com/facebook/rocksdb/issues/15079, confirming the posted comment carries the full review inline (not a review-findings.md summary) with the ci_review_prompt.md footer. https://github.com/facebook/rocksdb/actions/runs/31537781229 https://github.com/facebook/rocksdb/pull/15079#issuecomment-5259127037

Reviewed By: xingbowang

Differential Revision: D115635558

Pulled By: pdillinger

fbshipit-source-id: d3ca7188162a086cd42d7b8bd8e60f3fc3df3bad
2026-08-11 17:07:45 -07:00
Andrew Chang f9e4c5c51f Add stat for number of bottom-pri compactions. (#15071)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15071

__Why__: Give operators visibility into currently running bottom-priority compactions. Low-priority running compactions can be derived from `rocksdb.num-running-compactions - rocksdb.num-running-bottom-compactions`.

__What__: Expose `rocksdb.num-running-bottom-compactions` as a new statistics property.

__How__: We already record `num_running_bottom_compactions_`, so this feeds that value through the DB property path.

Reviewed By: xingbowang

Differential Revision: D114989532

fbshipit-source-id: efa19ef3a69eaecf4cbbbc9e4fc9cd55fce09ca5
2026-08-11 16:08:45 -07:00
Peter Dillinger fcf382c95c Integrated and ratio-aware compression auto-skip (#15079)
Summary:
Auto-skip compression (stop attempting compression on data that isn't compressing well, to save CPU) was previously an experimental CompressionManager wrapper (CreateAutoSkipCompressionManager) whose predictor lived in each per-thread working area. Besides being fragmented across parallel workers and reset on a tumbling window, it decided based on how OFTEN blocks failed to compress, ignoring how MUCH they compress -- so on mixed data it could give up substantial compression to save a little CPU (see performance results).

This makes auto-skip a first-class, optional, ratio-aware feature of the block-based table builder, controlled by two new CompressionOptions fields:
 - auto_skip (default false): the on/off switch. There is no separate threshold knob; the decision reuses max_compressed_bytes_per_kb (the existing per-block "minimum compression worth keeping" bar) as the bar for whether attempting compression on the stream is worthwhile.
 - auto_skip_min_sample_every: sampling interval that bounds the local impact around a transition back to compressible data. 0 selects an internal default (currently 64 skipped data blocks between forced samples).

The emit thread makes the skip/attempt decision once per data block (serial and parallel paths alike; data blocks only), so a skipped block is never dispatched to a compression worker. It is driven by a continuously updated, shared estimate q_hat of the aggregate stored/uncompressed ratio the builder would achieve by attempting -- each block counted at its achieved ratio, or 1.0 when stored raw: attempt while q_hat is at or below the bar, otherwise skip.

Because the estimate is payoff-weighted, a stream whose blocks each save little -- or where only a few blocks compress well -- does not clear the bar and is skipped, while a stream that compresses substantially is attempted. The compression given up per KB is bounded by (1024 - max_compressed_bytes_per_kb).

q_hat uses an asymmetric exponentially weighted moving average (EWMA), slightly faster toward better compression than toward worse; the fast side is deliberately gentle so a single marginally-compressible sample does not flip a whole file back to attempting. It is kept on its own cache line to avoid false sharing. Under parallel compression q_hat is updated with relaxed atomics, and the emit thread can decide from a value that lags the learning it drives by the in-flight depth -- tolerable concessions of accuracy and responsiveness for a coarse heuristic.

The first data block of each file is always attempted (the sample countdown starts at 0), and while skipping the builder keeps sampling (one block after a randomized gap) so a return to compressible data is detected. q_hat is carried across the files a flush/compaction thread emits via thread_local state keyed by preferred compression type, so a sustained region does not re-learn on every file.

The old CompressionManager-based AutoSkip is removed (CostAware is retained); db_stress, db_bench, and db_crashtest are migrated to the new option. db_bench is extended so it can generate the relevant regimes: --compressible_data_fraction and --compressible_data_run_length produce a mix of compressible/incompressible blocks -- i.i.d. per value (run length 1) or in alternating regions (large run length). The default (fraction 1.0) reproduces the historical single-pool behavior.

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

Test Plan:
New/updated unit tests in `util/compression_test.cc` (`DBAutoSkipTest`):
- skips sustained incompressible data;
- never skips compressible data;
- disabled by default never bypasses a block;
- 4-thread parallel compression round-trips all data across alternating compressible/incompressible regions.

**Performance** (`db_bench fillseq`, release, zstd level 3; 100k values of 16 KiB each, roughly one value per data block; WAL and auto-compaction off; median of 5 runs on a quiet host). Auto-skip trades one cost for another, so each cell reports two costs, both lower-is-better: CPU cost as process CPU time (user+sys) per GB of uncompressed input (s/GB; ~1.64 GB uncompressed here), and storage cost as the resulting DB size. Auto-skip does not appear to change parallelizability, so wall-clock s/GB tracks CPU s/GB (the two agree here); CPU s/GB is reported. Percentages are relative to the no-auto-skip baseline: negative CPU = cheaper, positive size = more storage (shown only where storage grows).

| Workload | baseline (off) | (a) old wrapper | (b) draft | (c) this version |
|---|---|---|---|---|
| Incompressible | 2.33 s/GB, 1568 MB | 0.96 (-59%), 1568 MB | 0.82 (-65%), 1568 MB | 0.82 (-65%), 1568 MB |
| Compressible | 3.55 s/GB, 710 MB | 3.54 (~0%), 710 MB | 3.56 (~0%), 710 MB | 3.55 (~0%), 710 MB |
| Marginal-payoff mix | 3.80 s/GB, 1376 MB | 1.54 (-59%), 1523 MB (+11%) | 3.81 (~0%), 1376 MB | 1.43 (-62%), 1531 MB (+11%) |
| High-payoff mix | 2.20 s/GB, 929 MB | 1.14 (-48%), 1417 MB (+53%) | 2.16 (~0%), 929 MB | 2.17 (~0%), 929 MB |

A strategy wins by cutting CPU cost without paying much storage cost. On incompressible data all three cut CPU ~60% at no storage cost; on compressible data none skip (within run-to-run noise). The two mixes (50% incompressible, 50% compressible per block) separate the strategies:
 - Marginal-payoff mix: the compressible half barely clears the per-block bar, so the stream is not worth compressing -- skipping is right. (c) and (a) cut CPU ~60% for ~11% more storage. That extra storage is compression (c) intentionally declines because its aggregate benefit is below the configured worthwhile threshold (max_compressed_bytes_per_kb, ~12.5% per KB); it stays within the (1024 - max_compressed_bytes_per_kb) bound. The ratio-unaware draft (b) cannot see the low payoff, keeps compressing, and saves no CPU.
 - High-payoff mix: the compressible half compresses hard, so compressing is right. (c) and (b) keep full compression at baseline storage; the old wrapper (a) cuts CPU 48% but inflates storage +53% -- far beyond that bound -- giving up worthwhile compression.

Only (c) makes the right call on both mixes: it never gives up worthwhile compression (unlike (a)) and does not waste CPU on low-payoff data (unlike (b)).

Reviewed By: xingbowang

Differential Revision: D115444567

Pulled By: pdillinger

fbshipit-source-id: 05e122d25de95ddcd0edeb0dd07c9d25bead7293
2026-08-11 14:29:51 -07:00
Xingbo Wang 66d7035708 Do not test GetEntityLazy on read-only primary handle (#15084)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15084

MaybeOpenReadOnlyOnPrimary is intended to exercise opening and closing a read-only DB on the primary live directory, then verify that read-only close does not delete files still live in the primary.

D115278311 diagnosed T283967366 as a race between GetEntityLazy on that read-only handle and the primary deleting SST files. After checking the call path, the ENOENT is from GetEntityLazy's initial point lookup opening an SST through TableCache, not from deferred lazy column resolution. Other read APIs are not exercised on this read-only-on-primary handle, so adding GetEntityLazy there made this old open/close harness test also perform a live-directory point read.

Remove the read-only-handle GetEntityLazy exercise and keep GetEntityLazy coverage in the normal stress read paths, consistent with the other read operations.

Reviewed By: pdillinger

Differential Revision: D115569968

fbshipit-source-id: fcef30d09903525c2a7897b9c52bc9ef951bed6a
2026-08-11 13:30:57 -07:00
Andrew Chang 9381544cd9 Add num-running-remote-compactions DB property (#15074)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15074

Why: We want to be able to monitor how many remote compaction jobs are running on a given server. Currently we know the total number of running compactions but not the local vs remote breakdown.

What: Add a new `rocksdb.num-running-remote-compaction-jobs` statistic. This measures in-progress remote compaction jobs.

Note that remote compaction work is scheduled per subcompaction, so one RocksDB compaction may contribute more than one running remote compaction job.

Reviewed By: jaykorean

Differential Revision: D115324149

fbshipit-source-id: 9397e39acd696c88ce7499ecb0da68de215bf1f3
2026-08-11 10:10:41 -07:00
Xingbo Wang f8c601b91c Fix io_activity assertion in stress test after MultiGetEntityLazy (#15076)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15076

Fix the `io_activity == Env::IOActivity::kUnknown || io_activity == options.io_activity`
assertion failure in the crash test. The crash occurs because `TestMultiGetEntity` in
`no_batched_ops_stress.cc` changes the thread operation to `OP_GETENTITY` during its
consistency-check phase (the `check_results` lambda calls `SetThreadOperation(OP_GETENTITY)`
before calling `GetEntity` for comparison). When `MaybeTestMultiGetEntityLazy` is called
afterward, it invokes `MultiGetEntityLazy` which correctly sets
`read_options.io_activity = kMultiGetEntity`, but the thread operation is still stale at
`OP_GETENTITY`. The debug assertion in `DbStressRandomAccessFileWrapper::Read` then sees
`TEST_GetExpectedIOActivity(OP_GETENTITY) = kGetEntity != kMultiGetEntity` and aborts.

The fix restores `SetThreadOperation(OP_MULTIGETENTITY)` before each call to
`MaybeTestMultiGetEntityLazy` in `TestMultiGetEntity`.

Triggered by commit cb73fc845fe0 which added the `MaybeTestMultiGetEntityLazy` calls.
The `--lazy_entity_read_one_in=100` flag (non-default) enables this code path.

Reviewed By: pdillinger

Differential Revision: D115021723

fbshipit-source-id: e3eed600db4986cab27d7b1924282b1dfb8b0a7a
2026-08-11 08:12:46 -07:00
Josh Kang 5dfde1c262 Add coroutine reads to stackable RocksDB utilities (#15080)
Summary:
## Summary

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

Introduce reusable `CoroStackableDBBase` forwarding and public `CoroDB::CoGet()` / `CoMultiGet()` entry points matching the `DB::Get()` / `MultiGet()` overloads. These helpers schedule native coroutine reads on the read executor and use the synchronous DB API when coroutine support or a read executor is unavailable.

Use the stackable coroutine hooks for TTL and WritePrepared transaction reads. TTL preserves expiration decoding and timestamp behavior, while WritePrepared preserves transaction visibility.

TTL keeps a single `DB` inheritance path while adding `CoroDB`:

```
DBWithTTLImpl
`-- CoroStackableDBBase<DBWithTTL>
    |-- DBWithTTL -> StackableDB -> DB
    `-- CoroDB
```

`CoroDB::CoGet()` dispatches through the stackable coroutine hook and schedules the operation on the read EventBase. `CoMultiGet()` follows the same path:

```
CoroDB::CoGet(db, ...)
|-- db->GetCoroDB() -> DBWithTTLImpl
|-- no CoroDB or read executor -> db->Get()
`-- schedule on read EventBase
    `-- virtual CoroDB::GetCoroutine()
        `-- DBWithTTLImpl::GetCoroutine()
            |-- CoroStackableDBBase<DBWithTTL>::GetCoroutine()
            |   `-- wrapped CoroDB::GetCoroutine()
            `-- strip TTL timestamp
```

Reviewed By: xingbowang

Differential Revision: D115232510

fbshipit-source-id: 59e5090db28d15bcff1a8b9b8c0b30ca7eeb742d
2026-08-10 20:28:02 -07:00
Xingbo Wang 44e0294c1d Fix remote compaction filter test flake (#15075)
Summary:
- Disable automatic compactions in `CompactionServiceTest.CompactionFilter`.
- Keep the test focused on the explicit `CompactRange()` path that verifies remote compaction filter behavior.
- Avoid a flaky race where background remote compaction can obsolete an input SST before the manual compaction runs.

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

Test Plan:
- `make format-auto`
- `AUTO_CLEAN=1 COERCE_CONTEXT_SWITCH=1 make -j32 compaction_service_test`
- `tools/gtest_parallel_repro.py --binary ./compaction_service_test --gtest_filter=CompactionServiceTest.CompactionFilter --processes-per-iteration 32 --iteration-count 30 --cpu-count 4 --timeout 120 --stop-on-failure --out /tmp/ws28_compaction_service_flake_fixed_20260809_1`
- `git diff --check`
- `AUTO_CLEAN=1 make check-sources`

Reviewed By: jaykorean

Differential Revision: D115357593

Pulled By: xingbowang

fbshipit-source-id: 9c640b807b8d0c204a23fb805e511b39db69acbc
2026-08-10 15:27:15 -07:00
Xingbo Wang 3f4d840e1a Fix multi-DB checkpoints in legacy regression test (#15078)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15078

The legacy regression test runner attempted to discover multi-DB origin directories by running find through run_remote, then assigned dirs=$?, which captured only find's exit code. For NUM_MULTI_DB > 1 this meant the checkpoint loop operated on the value 0 instead of the discovered sub-DB directories.

For NUM_MULTI_DB > 1, build checkpoints for the deterministic numeric sub-DB directories 0..NUM_MULTI_DB-1 and explicitly fail if any expected origin DB is missing. This avoids rediscovering DBs through find output, so checkpoint creation no longer depends on pipeline exit status or whitespace-sensitive word splitting.

Reviewed By: joshkang97

Differential Revision: D115380183

fbshipit-source-id: 24adfa37f872dd7b8ee4ee986cf4a7a4e58562d0
2026-08-10 14:29:58 -07:00
Andrew Chang 3949208acf Make max_open_files configurable via options_map in OpenAndCompact (#15077)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15077

__Why__: Allow offload compaction workers to configure  `max_open_files` via `options_map`.

__Problem__: `OpenAndCompact` hard-codes `max_open_files` to -1. This overwrites any value set via the `options_map` and prevents configuration in the remote compaction workers.

__Fix__: Remove the hard-coding assignment.

The value now comes from:
1. OPTIONS file via `LoadOptionsFromFile`
2. `override_options.options_map` via `GetDBOptionsFromMap`

The default value of `max_open_files` was already -1, so existing behavior is preserved.

Reviewed By: jaykorean

Differential Revision: D115373592

fbshipit-source-id: fa742397246092544ab6f9004678be7ae938d051
2026-08-10 10:20:08 -07:00
Josh Kang 88752298ff Add native coroutine read dispatch to RocksDB (#15073)
Summary:
## Summary

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

The callback-based async read APIs make coroutine-native implementations and StackableDB composition awkward because wrappers must bridge callbacks back into tasks. This change introduces `CoroDB` as a side capability exposing lazy `GetCoroutine` and `MultiGetCoroutine` tasks. `DBImpl` and `CompactedDBImpl` implement this capability directly, while `DB::GetAsync` and `DB::MultiGetAsync` remain available as compatibility entry points with synchronous fallback.

The native interface should also be more efficient because callers directly await the DB coroutine. It avoids the callback/Baton adapter, callback context handoff, and explicit rescheduling required for each request. Native tasks must run on the DB filesystem read executor's EventBase; debug builds verify this affinity.

Public API documentation, a release note, and native-coroutine support in `db_bench` and `db_stress` are included.

## Dispatch

  DB::GetAsync
    -> GetCoroDB() on the concrete DB
    -> virtual CoroDB::GetCoroutine(...)
       -> DBImpl::GetCoroutine or CompactedDBImpl::GetCoroutine
    -> AsyncCallback::OnComplete(...)

Virtual dispatch selects the coroutine implementation belonging to the concrete DB while preserving the existing callback API.

## Statistics

Coroutine reads preserve RocksDB's TLS statistics behavior while keeping concurrent requests isolated. Each top-level task copies only the caller's statistics configuration—`PerfLevel`, per-level perf context enablement, and IO statistics enablement—into fresh request-local `PerfContext` and `IOStatsContext` objects. Existing counters are not inherited. If both perf and IO statistics are disabled, no request statistics context is installed.

Folly `RequestContext` preserves these contexts across suspension and resumption on the read executor's EventBase. At completion, RocksDB publishes the request's collected values to TLS, allowing the awaiting caller to use `get_perf_context()` and `get_iostats_context()` as after a synchronous read. Callers must consume or copy them before leaving that EventBase. Statistics scopes are installed only by top-level coroutine reads and cannot be nested.

## Benchmarks

Compared the previous `GetAsync` callback bridge with native `GetCoroutine`. Both variants used optimized builds, the same compacted DB, a fixed seed, eight executor threads, ten jobs per thread, and 2.56M reads per invocation. Results are the mean of three independent invocations using the default RocksDB block cache. A sequential read warmed the OS page cache before the non-direct-read measurements.

| Workload | `GetAsync` bridge | Native `GetCoroutine` | Change |
| Direct I/O | 237,710 ops/s | 243,588 ops/s | +2.47% |
| Warm OS page cache | 783,859 ops/s | 891,531 ops/s | +13.74% |

The direct-I/O difference is within observed run-to-run variation. When reads are served from the OS page cache and API overhead is a larger portion of latency, the native interface improves throughput by 13.74%. Every invocation completed all requested reads.

```bash
db_bench \
  --benchmarks=readrandomcoroutine \
  --db=$CORO_BENCH_DB \
  --use_existing_db=true \
  --num=2000000 \
  --value_size=1024 \
  --compression_type=none \
  --threads=8 \
  --coro_jobs_per_thread=10 \
  --reads=320000 \
  --use_direct_reads=<true|false> \
  --perf_level=1 \
  --statistics=true \
  --seed=20260807
```

Reviewed By: xingbowang

Differential Revision: D115232511

fbshipit-source-id: 30b48da2a798094d9080178f5430aae26e6c5954
2026-08-10 09:43:05 -07:00
Andrew Chang 1dfa729793 Expose number of unscheduled compactions as a statistic (#15060)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15060

Expose the `rocksdb.num-unscheduled-compactions`  DB statistic to help troubleshoot high L0 counts.

The metric captures  the raw `unscheduled_compactions_` count. It measures compaction work waiting in the DB internal compaction queue, but not yet assigned to a background job.

This complements `rocksdb.compaction-pending`, which tells us "some compaction work is needed," and `rocksdb.num-running-compactions`, which tells us how many background compactions are executing.

`rocksdb.compaction-pending` is just a boolean value so we can't infer too much from it. `rocksdb.num-unscheduled-compactions` can be used to indicate that we may need to increase the max number of background jobs.

Reviewed By: xingbowang

Differential Revision: D114539448

fbshipit-source-id: 3657951bd9d8a4f4a7bc884513f9a06a87892b50
2026-08-07 13:34:58 -07:00
Peter Dillinger 8e1569b351 Lazy wide-column byte-range (partial) blob reads (#15067)
Summary:
Builds on the lazy wide-column read API (DB::GetEntityLazy / DB::MultiGetEntityLazy) to serve true byte-range reads: resolving a sub-range of an uncompressed blob column now reads only the requested bytes from storage instead of fetching and slicing the whole column. This covers blobs in separate blob files and embedded (same-file) blobs in the SST, so a caller that knows a large value's layout can pull just the parts it needs.

A partial read skips whole-record checksum verification (a strict sub-range cannot cover the checksum) and does not populate the blob cache (the cache entry holds the whole record). A compressed column, a whole-column read, a cache hit, or a read with LazyColumnReadRequest::force_verify still resolves (and verifies) the whole column; force_verify forces verification even when ReadOptions::verify_checksums is off.

Mechanics: new range primitives mirror the whole-value read paths -- BlobFileReader::GetBlobRange, BlobSource::GetBlobRange, Version::GetBlobRange, and VersionBlobFetcherBase::FetchBlobRange for separate-file blobs; ReadSimpleGen2BlobRange, BlobSource::GetSimpleGen2BlobRange, and SameFileBlobReader::GetSameFileBlobRange (implemented by BlockBasedTable) for embedded blobs. ReadPathBlobResolver::ResolveColumnRange owns the partial-vs-whole decision and routes same-file references through the current SST's reader; the whole-column path also gained
VersionBlobFetcherBase::FetchBlobForceVerify to honor force_verify.

Observability: deferred resolve reads are attributed to a new Env::IOActivity::kLazyResolve (distinct from the kGetEntity / kMultiGetEntity of the initial entity read), and lazy resolution's storage reads are counted by new statistics rocksdb.blobdb.lazy.read.count / .read.bytes (all lazy reads) and rocksdb.blobdb.lazy.partial.read.count / .partial.bytes.saved (the byte-range subset and the bytes it avoided). The whole-column read sites gate recording on kLazyResolve so non-lazy reads are excluded.

Also fixes a latent bug in LazyWideColumnsBatch::MultiResolve where an empty/default-constructed batch (null rep_) would accept a foreign standalone column (null owning_batch_rep_) through a nullptr == nullptr comparison instead of returning InvalidArgument.

Follow-ups (in a local plan): unifying the blob-read API surface -- folding the whole/range method twins and the verify-policy flags (verify_checksums x force_verify) into one internal BlobReadRequest primitive (a range descriptor plus a 3-valued verify policy), which retires FetchBlobForceVerify and the whole/range method pairs; this is deferred for feature velocity and is not public-API affecting (the public force_verify bool stays). Marked in code with TODO(lazy-blob-resolution-cleanup) in db/blob/blob_fetcher.h and db/wide/read_path_blob_resolver.cc. Also cross-key coalescing (Phase 3) and async execution (Phase 4).

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

Test Plan:
Extended db/wide/db_lazy_entity_test.cc: separate-file and embedded partial reads return the exact sub-span and read only the requested bytes; a partial read does not fill the blob cache and a cache hit slices with no I/O; a compressed column resolves whole and slices; force_verify does a full verified read and is detected via a tampered record even with verify_checksums off; the lazy statistics distinguish whole vs partial reads and are not touched by non-lazy reads; deferred reads carry kLazyResolve (verified with a FileSystem wrapper); and a regression test for the empty-batch ownership check. Added an IOActivityToString case in env/env_test.cc and registered the new tickers in the statistics name map.

Crash test: extended the db_stress lazy-vs-eager differential (db_stress_tool/db_stress_test_base.cc) to fuzz per-column byte ranges. Each resolved column now randomly picks a whole-column read or a partial (offset, length) sub-range -- occasionally with an offset past the end or a length beyond the remainder to exercise clamping -- for both GetEntityLazy and MultiGetEntityLazy; when an eager reference is available the resolved bytes are compared against the same clamped sub-range of it. No new flags (reuses --lazy_entity_read_one_in); force_verify fuzzing remains a follow-up.

Reviewed By: xingbowang

Differential Revision: D115130388

Pulled By: pdillinger

fbshipit-source-id: 454368dea8fae069d9de649c2c51611469d3c809
2026-08-07 12:08:03 -07:00
Darkhan Imangaliyev 5fe225357a Extend parallel checkpointing and db_stress coverage for column-family-subset checkpoints (#15059)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15059

**What**:
 1. Extends the parallel `CheckpointEngine` so it can also checkpoint a subset of column families.
 2. Adds `db_stress` and `db_crashtest.py` coverage for subset checkpoints, over both the serial and the parallel API.

**How**:

 - **Engine API.** `CreateCheckpointOptions` gains a `std::vector<ColumnFamilyHandle*> column_families` field (empty, the default, still means all column families). This goes on the existing per-operation options
 truct rather than adding a second pure virtual to `CheckpointEngineBase`: it avoids overload-resolution hazards with the defaulted `sequence_number_ptr`, and it is the pattern the engine API already uses.
`CreateCheckpointOptions` has no users outside the engine API, so adding a field breaks nothing.
  - **Shared validation.** The handle checks that were inline in `CheckpointImpl::CreateCheckpoint` -- reject null handles, handles belonging to another DB, and handles for already-dropped families; coalesce ids;
always add the default CF -- move to `static CheckpointImpl::ResolveIncludeColumnFamilyIds()`, now called by both APIs. Rejecting a foreign handle matters because `handle->GetID()` alone would silently coerce a
foreign id onto whichever family in this DB happens to share it. Validation runs before any staging directory is created, so a rejected request leaves nothing behind.
  - **Almost no new plumbing.** `CheckpointImpl::CreateCheckpointImpl()` already accepted `include_cf_ids` and is shared by the serial and parallel movers, and the MANIFEST `kColumnFamilyDrop` rewrite runs after
`mover->Finish()`, so it was already mover-agnostic. The engine change is just resolving the ids and forwarding them.
  - **db_stress.** New `subset_cf_checkpoint_one_in` knob (mirrors `parallel_checkpoint_one_in`): each checkpoint operation covers a random proper subset with probability 1/N. It is a no-op when the DB has only the
default column family, and it is independent of `parallel_checkpoint_one_in`, so subset checkpoints get exercised through both the serial API and the engine. Subset selection includes each non-default family
with probability 1/2 and then force-excludes one, so every subset checkpoint exercises the MANIFEST rewrite instead of degenerating into a whole-DB checkpoint; the default handle is passed in only half the time,
covering the "default is always included whether or not you pass it" contract on both sides.
  - **db_stress validation.** `DB::ListColumnFamilies()` on the checkpoint must return exactly the requested set -- the direct check on the MANIFEST post-processing, catching both an excluded family that was not
recorded as dropped and a requested family that went missing. The checkpoint is then opened read-only with exactly those descriptors, proving the reduced MANIFEST and reduced file set are self-consistent under
the default `paranoid_checks`. The pre-existing per-key comparison against `SharedState::Exists()` still runs, restricted to the families actually present.
  - **Incompatibility.** `clear_column_family_one_in > 0` is rejected together with the new knob, the same way D113103899 did for `abort_and_resume_cf_compactions_one_in`: a subset checkpoint hands live handles
from `column_families_` to the API and validates against `column_family_names_`, and neither is stable while families are dropped and recreated underneath. `db_crashtest.py` already pins
`clear_column_family_one_in` to 0.
  - **Test cleanup.** The subset tests shared a lot of setup and verification, so `PopulateFourCfsForSubsetCheckpoint()`, `VerifyFourCfsIntact()` and `VerifyDefaultAndTwoSubsetCheckpoint()` were extracted into the
test file's anonymous namespace and are reused by the pre-existing `CheckpointSubsetOfCF` as well as the four new engine tests.

This extends `StressTest::TestCheckpoint` with a mode rather than adding a separate operation, which is how `parallel_checkpoint_one_in` was integrated.

Reviewed By: rban1

Differential Revision: D114832267

fbshipit-source-id: f3f843819614477497cc1466910547922cafc51d
2026-08-06 16:17:04 -07:00
Xingbo Wang e8bbe08760 Make ARM nightly crash test swap setup idempotent (#15065)
Summary:
- make the ARM nightly crash-test swap setup skip `/swapfile` creation when it is already active
- keep the existing 4 GiB swap creation path for hosts without active `/swapfile`
- print the active swap devices after setup for easier workflow diagnostics

## Testing

- `git diff --check upstream/main...2026_08_06_arm_failure`

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

Reviewed By: archang19

Differential Revision: D115045941

Pulled By: xingbowang

fbshipit-source-id: ff2248b8501c82f5f9e7561bd77937d4823bc67f
2026-08-06 08:23:22 -07:00
Peter Dillinger fd7399004f Add lazy wide-column read API (GetEntityLazy / MultiGetEntityLazy) (#15012)
Summary:
Adds an experimental **lazy wide-column read API** that lets callers enumerate a
wide-column entity and read back only the columns (and, in a later phase, the
byte ranges) they actually need, instead of eagerly materializing every
blob-backed column like `GetEntity()` / `MultiGetEntity()`.

New public entry points (`include/rocksdb/db.h`):

- `DB::GetEntityLazy(read_options, cf, key, LazyWideColumns* result)`
- `DB::MultiGetEntityLazy(read_options, cf, num_keys, keys, LazyWideColumnsBatch* result, statuses, sorted_input)`

Both leave a wide-column entity's **inline** columns materialized up front but
its **blob-backed** columns as *unresolved references*, so blobs for columns
that are never pulled are never read.

### Result types (`include/rocksdb/lazy_wide_columns.h`)

- **`LazyWideColumn`** — the lazy analogue of `WideColumn`: `name()`, `index()`,
  `inline_value()` (an `OptSlice`; empty ⇒ unresolved reference),
  `is_reference()`, and `logical_size()` (`std::optional<uint64_t>`; known for
  inline and uncompressed references, empty for compressed ones). All no-I/O.
- **`LazyWideColumns`** — a move-only result that is **indexable and iterable
  like a vector** of `LazyWideColumn` (`size()`, `operator[]`, range `begin/end`).
  Unlike `PinnableWideColumns`, it may be used after the producing call returns:
  it **pins the SuperVersion** (like an iterator) so referenced blob files / SST
  readers stay valid for deferred reads.
- **`LazyWideColumnsBatch`** — the `MultiGetEntityLazy` result; vector-like over
  its per-key `LazyWideColumns`.

### Resolution

- **`LazyColumnReadRequest { const LazyWideColumn* column; uint64_t offset;
  size_t length; bool force_verify; PinnableSlice* result; Status* status; }`** —
  a read names a **typed column pointer**, not an untyped index. Because a
  column identifies its owning result (and, for a batch, its owning entity),
  `MultiResolve` routes each read correctly and **rejects a null or foreign
  column** with `InvalidArgument` on that read's per-read status — cross-result
  misuse can't be silently mis-indexed.
- **`LazyWideColumns::MultiResolve` / `LazyWideColumnsBatch::MultiResolve`** take
  the same request type (pointer+count and `std::vector` sugar overloads); the
  batch form resolves reads spanning any entities of the batch in one call.
- **`ResolveColumn(column, result)` / `ResolveColumnRange(column, offset, length,
  result)`** — per-column sugar over a one-entry `MultiResolve`.
- A column's blob is fetched **at most once** over the result's lifetime (bytes
  are cached in the result), so repeated reads of the same column reuse that
  fetch. Resolution honors the `ReadOptions` captured at `GetEntityLazy()` time
  (e.g. `read_tier`, `verify_checksums`); the resolve methods intentionally take
  no per-resolve `ReadOptions`.

### Where it works

- Primary, **read-only**, and **secondary** DB instances (each `GetImpl`
  override forwards the lazy signal and transfers a SuperVersion pin via the
  shared `DBImpl::TransferSuperVersionPin` helper).
- Forwarded through `StackableDB` wrappers (e.g. `TransactionDB`) rather than
  falling through to the `NotSupported` base default.
- Requires `max_open_files == -1` (immortal table readers) so same-file
  ("embedded") references stay resolvable lazily; otherwise the call returns
  `InvalidArgument`.

## Implementation notes

- Read path: `GetContext` gains a lazy mode that leaves blob references
  unresolved and captures the SST's `SameFileBlobReader`; `Version::Get` forwards
  the new (defaulted) `lazy_columns_same_file_reader` out-param.
- `ReadPathBlobResolver` performs on-demand, cached blob resolution bound to the
  pinned `Version` + decoded blob references.
- Internal identity is by heap-stable `Rep` pointers, so column/result/batch
  pointers stay valid across moves with no move-time fix-ups.

## Scope / current limitations

- Resolves **whole columns**; byte ranges are served by slicing the whole column
  for now (so `length`/`offset`/`force_verify` don't yet save I/O or skip
  checksums). Single column family. Synchronous. `kBlockCacheTier` on the
  originating call yields `Incomplete` on a blob-cache miss.
- User-defined timestamps: entities aren't writable under UDT, so `GetEntityLazy`
  is at parity with `GetEntity` (same read-timestamp validation; plain values
  return a single default column).
- Planned follow-ups: partial (byte-range) blob reads that skip checksum/cache-
  fill, cross-key coalescing, and asynchronous execution.

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

Test Plan:
- **`db/wide/db_lazy_entity_test.cc` (15 tests, all passing):** enumeration with
  no blob I/O; single range and whole-column resolution; `MultiResolve` of many
  blobs and mixed ranges/columns (incl. the `std::vector` overload); cross-key
  batch resolution; result-outlives-call / pin survives move; unpulled columns
  never read; `max_open_files == -1` requirement; `kBlockCacheTier` ⇒
  `Incomplete`; foreign/null column ⇒ `InvalidArgument` (single-entity and
  batch); UDT parity with `GetEntity`; `StackableDB` forwarding; and read-only
  and secondary instances.
- **db_stress:** `GetEntityLazy` / `MultiGetEntityLazy` run as a differential
  against eager `GetEntity` / `MultiGetEntity` (gated by `--lazy_entity_read_one_in`,
  which only takes effect under `--open_files=-1`), including coverage on
  read-only (`--open_read_only_one_in`) and secondary (`--test_secondary`)
  instances. Smoke-run (`EXIT=0`, "Verification successful") with integrated blob
  files + `--use_put_entity_one_in`.
- `make format-auto` clean; builds `ASSERT_STATUS_CHECKED`-clean.

Reviewed By: xingbowang

Differential Revision: D113792365

Pulled By: pdillinger

fbshipit-source-id: cb73fc845fe0e20ed352d697e552e7b47b2eb9cf
2026-08-05 22:21:40 -07:00
Josh Kang d1fbdffdae Log failed Status values in db_stress assertions (#15054)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15054

`db_stress` often terminates on raw `assert(status.ok())` checks, which discard the underlying error and make crash-test failures difficult to diagnose.

Log the expression and full status before terminating. A message variant preserves operation-specific context and variadic formatting. In debug builds, assert after logging to retain assertion diagnostics and stack traces. In optimized builds, where assertions are disabled, exit with status 1.

Reviewed By: jaykorean

Differential Revision: D114756034

fbshipit-source-id: 5a0d4409bac69820b6560c8200cc401b7bfc65d6
2026-08-05 19:51:14 -07:00
Josh Kang dc4006480b Tolerate InvalidArgument from full_history_ts_low race when persist_user_defined_timestamps=false (#15052)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15052

When `persist_user_defined_timestamps=false`, `full_history_ts_low` is automatically advanced by flushes to just above the newest timestamp in the flushed memtable. This creates the following race:

```
Reader                              Concurrent writer / flush
------                              -------------------------
T0: Sample read timestamp:
    892779959679761

                                    T1: Write with a newer timestamp:
                                        892779963289806

                                    T2: Flush the memtable.

                                    T3: Advance full_history_ts_low:
                                        892779963289807

T4: Validate the read:

    892779959679761
      < 892779963289807

    Result: InvalidArgument
```

This is expected RocksDB behavior because history below `full_history_ts_low` is no longer guaranteed to be complete. However, the stress test intends these operations to read the latest value.

This change adds ten seconds of headroom to read timestamps when user-defined timestamps are not persisted. This makes it substantially less likely that a concurrent flush overtakes a read timestamp. Unlike tolerating every `InvalidArgument`, this preserves reporting for genuine timestamp errors. Persisted read timestamps and write timestamps remain unchanged.

The triggering non-default options are `persist_user_defined_timestamps=false` and `user_timestamp_size=8`.

Reviewed By: xingbowang

Differential Revision: D114565978

fbshipit-source-id: 6939df9163fd97fefec2fa185bc2ca72231ab04f
2026-08-05 17:06:48 -07:00
Peter Dillinger af0eae4de3 Rename TransactionLogIterator to WalIterator and document its behavior (#15056)
Summary:
`TransactionLogIterator` iterates the write-ahead log. It does not return transactions, does not require `TransactionDB`, and in fact returns `Status::NotSupported()` for two of the three `TransactionDB` write policies (WritePrepared and WriteUnprepared). The name predates `TransactionDB` and comes from "transaction log" being an old synonym for WAL. Everything else in this header was already renamed away from that terminology -- `LogFile` -> `WalFile`, `VectorLogPtr` -> `VectorWalPtr` -- and the C API has long used `rocksdb_wal_iterator_t` and `rocksdb_wal_iter_*`. This finishes the job.

The public class is now `WalIterator` in `include/rocksdb/wal_iterator.h`. `include/rocksdb/transaction_log.h` remains as a forwarding header and `TransactionLogIterator` remains as an alias, both marked deprecated by comment only (not `[[deprecated]]`) so existing users compiling with -Werror are unaffected, matching how `LogFile` and `VectorLogPtr` were retired. `DB::GetUpdatesSince()` keeps its name.

More importantly, the documentation was incomplete in ways that could cost a user data, so this states plainly what the API has always done:

- It only sees the WAL. Writes made with `WriteOptions::disableWAL`, and sequence numbers consumed by `IngestExternalFile()`, advance the DB sequence number without producing a WAL record. They leave permanent holes that end iteration. This was not documented anywhere, so it was unreasonable to expect a user to know those features silently break WAL tailing.
- The three iterator states, in particular that `!Valid()` with an OK `status()` means caught up rather than finished, and that calling `Next()` again is the intended way to tail a DB. The old comment on `Next()` said "REQUIRES: Valid() to be true", which forbids the primary use case.
- `Status::TryAgain`, which a caller must handle but which appeared nowhere in the public headers.
- That `GetUpdatesSince(seq)` silently starts later when `seq` is unavailable. Combined with the above this is the sharp edge: recovering from a spent iterator means calling `GetUpdatesSince()` again, so the recovery step is exactly where data can be skipped unnoticed. A consumer applying these batches to a copy of the DB diverges permanently and undetectably if it misses one, so the header now says such consumers must verify continuity themselves using `BatchResult::sequence` and `WriteBatch::Count()`.

Almost all of this diff is mechanical: renaming the type and its files, updating `#include` paths, rewriting comments, and updating build lists (`src.mk`, `CMakeLists.txt`, `BUCK`, `java/Makefile`, `java/CMakeLists.txt`). `db/c.cc` is regenerated from `tools/c_api_gen/c_base.cc`. Reviewers should focus on the four places that are not:

1. `WalIteratorImpl::UpdateCurrentWriteBatch()` in `db/wal_iterator_impl.cc` (renamed from `db/transaction_log_impl.cc`) drops dead code. On a sequence discontinuity it set `current_status_` to `NotFound` and then called `SeekToStartSequence()`, which returns immediately when `current_status_` is not OK. That guard and the early return in `Next()` were both added by fe63899d1 ("Add checks to GetUpdatesSince", https://github.com/facebook/rocksdb/issues/9459), which made the reseek unreachable without mentioning it. The surviving behavior -- a discontinuity ends the run with `NotFound` -- is what the class has always documented, so this deletes the unreachable reseek and the comment claiming it can restore an OK status rather than resurrecting it. There is no observable change: `Valid()` is `started_ && is_valid_`, `is_valid_` is false either way, and a non-OK `current_status_` already makes `Next()` a no-op.
2. `WalIteratorImpl::IsBatchExpected()` in the same file drops "Log iterator will reseek the correct batch." from its info log message, since it no longer does.
3. `org.rocksdb.WalIterator` now holds the implementation and the nested `BatchResult`, and `org.rocksdb.TransactionLogIterator` becomes a subclass of it. Existing Java code keeps compiling, `RocksDB.getUpdatesSince()` keeps its return type, and `TransactionLogIterator.BatchResult` still resolves through inheritance. Because the native methods are now declared on `WalIterator`, the JNI entry points in `java/rocksjni/wal_iterator.cc` (renamed from `transaction_log.cc`) become `Java_org_rocksdb_WalIterator_*`.
4. `BatchResultJni::getJClass()` in `java/rocksjni/portal.h` looks up `org/rocksdb/WalIterator$BatchResult` to match.

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

Test Plan:
Adds three tests to `db_log_iter_test.cc` that pin the semantics now being documented, none of which had coverage:

- `CaughtUpThenResumes` -- an iterator that has run out of records is `!Valid()` with an OK `status()`, and a later write to the same WAL is picked up by calling `Next()` again. This is the tailing pattern the old `Next()` comment prohibited.
- `GapFromDisableWAL` -- a `disableWAL` write between two normal writes ends the run with `NotFound` instead of skipping to the later sequence number, and the iterator stays spent across further `Next()` calls.
- `SilentlySkipsUnavailableStart` -- `GetUpdatesSince(1)` where sequence 1 never reached the WAL returns OK and positions at sequence 2, documenting the silent skip.

`IngestExternalFile()` is documented as the other source of holes but is not covered by a test; it needs `SstFileWriter` setup and is worth a follow-up.

The Java rename is verified by `WalIteratorTest`, which declares `WalIterator` while `RocksDB.getUpdatesSince()` still returns `TransactionLogIterator`, so the compatibility subclass is exercised on every use. It is otherwise an identifier-only rename of the previous test.

Renames the C++ test fixture to `DBWalIteratorTest`, drops the now-redundant `TransactionLogIterator` prefix from its test names, and updates the comments in `wal_manager.cc`, `sync_point.h` and `fs_posix.cc` that refer to those tests by name. Those comments already pointed at the wrong fixture and file (`DBTest` in `db_test.cc`) before this change.

Reviewed By: xingbowang

Differential Revision: D114809792

Pulled By: pdillinger

fbshipit-source-id: d3cfd23867b4738b62a096db1dbc895bada069b7
2026-08-05 13:21:37 -07:00
Omkar Gawde dbf9328c71 Add per-column-family compaction abort and resume (#14982)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14982

**What**: Add reference-counted `DB::AbortCompactions(cf)` / `DB::ResumeCompactions(cf)` to abort and resume compactions for a single column family without stopping unrelated column families. They compose with the DB-wide `AbortAllCompactions()` / `ResumeAllCompactions()` and are no-ops once DB shutdown has begun.

**How**:
- **Per-CF abort flag.** `ColumnFamilyData` gains `compaction_aborted_`, wrapped in `CacheAlignedWrapper` so the lock-free reads done by compaction workers do not false-share with the DB-mutex-written fields packed around it. A new `IsCompactionAborted(cfd)` helper checks it alongside the DB-wide counter at every entry point (`CompactRange()`, `RunManualCompaction()`, `CompactFilesImpl()`), and `CompactionJob::ProcessKeyValue()` checks both on entry and in its periodic cron check.
- **Parking.** Automatic work for an aborted CF is preserved rather than dropped. `PickCompactionFromQueue()` now returns `std::variant<CompactionQueuePicked, CompactionQueueThrottled, CompactionQueueParked>` and moves an aborted CF into `parked_compaction_cfds_` only once it reaches the front of the queue, so CFs behind it keep their place and the scheduler credit is not restored (which would cause a hot re-pick loop). `EnqueuePendingCompaction()` parks newly needed work for an aborted CF and now early-returns for dropped CFs. `RestoreParkedCompaction()` re-enqueues on the final resume; a CF dropped while parked is handled in `DropColumnFamily()`, and one dropped before its aborted queue entry is reached is handled by a fast path in `PickCompactionFromQueue()`.
- **Knowing when it is safe to return.** `ColumnFamilyData::num_in_flight_compactions_` counts actually-picked work, and `Defer` balances `BeginInFlightCompaction()` / `EndInFlightCompaction()` on every `BackgroundCompaction()` and `CompactFiles()` exit path. That covers the `NotifyOnCompactionCompleted()` window that `CompactionPicker::compactions_in_progress_` does not, since the picker set is cleared during `Install`'s manifest write. `MustWaitForCompaction()` combines the counter with the picker set (filtered to skip `kExternalSstIngestion` / `kRefitLevel` reservations, which are not real jobs) because only the picker set covers the LOW-to-BOTTOM priority handoff, where the counter briefly dips to 0.
- **Waking the waiter.** `AbortCompactions()` cancels matching `manual_compaction_dequeue_` entries and then waits on `bg_cv_`; `per_cf_compaction_abort_waiters_` forces the compaction completion paths to signal, because the picker's `UnregisterCompaction()` does not. `CloseHelper()` drains `parked_compaction_cfds_` and releases their references.
- **API surface.** The two methods are new pure virtuals on `DB`, so `StackableDB` forwards them and the `db_test.cc` DB wrapper stubs them. `db.h` documents how the per-CF API composes with DB-wide abort, `DisableManualCompaction()`, `PauseBackgroundWork()`, shutdown, ingestion reservations, level refit and remote compaction, plus the parked-work caveat on `WaitForCompact()`. Comments in `listener.h`, `statistics.h` and `status.h` now name both abort APIs.
- **Coverage.** 10 new cases in `db_compaction_abort_test.cc` (CF scoping, counter composition, parked-work restore, CF drop both while parked and before parking, trivial-move rejection, waiting for the completion callback, post-close no-op). `db_stress` gains `abort_and_resume_cf_compactions_one_in` and `TestAbortAndResumeCfCompactions()`, rejected in combination with `clear_column_family_one_in` because that would drop the column family out from under an active abort. Also a `db_crashtest.py` knob and one `new_features` release note.
- **Known gap.** A `TODO` records the pre-existing post-pick stale compaction-score problem: aborting after `PickCompaction()` releases the inputs without recomputing the score, so the resume can observe a stale sub-trigger score and re-enqueue nothing. `ResumeAllCompactions()` has the same gap and carries a pointer to the same TODO; fixing it is left to a separate change covering both APIs.

**Why**: Atomic-replace bulk loads only conflict with their target column family, but the DB-wide abort suppresses compaction for every column family colocated in the same RocksDB instance. Scoping the abort keeps unrelated column families compacting for the duration of a bulk load.

Reviewed By: pdillinger

Differential Revision: D113103899

fbshipit-source-id: 9bef924132676ed118a37aee86da6ee8bcefef09
2026-08-04 13:36:03 -07:00
zaidoon 751bde0f7f Add IndexFactory compatibility names (#14954)
Summary:
Part 1 of 13 in the UDI split.

Stack order:
1. https://github.com/facebook/rocksdb/issues/14954 Add IndexFactory compatibility names
2. https://github.com/facebook/rocksdb/issues/14959 Add UDI index mode API vocabulary
3. https://github.com/facebook/rocksdb/issues/14960 Add optional UDI builder protocols
4. https://github.com/facebook/rocksdb/issues/14961 Add built-in index factory wrappers
5. https://github.com/facebook/rocksdb/issues/14969 Add built-in index factory tests
6. https://github.com/facebook/rocksdb/issues/14962 Promote IndexFactory as UDI SPI
7. https://github.com/facebook/rocksdb/issues/14963 Wire IndexFactory through block-based tables
8. https://github.com/facebook/rocksdb/issues/14964 Add IndexFactory table routing tests
9. https://github.com/facebook/rocksdb/issues/14970 Add IndexFactory parallel routing tests
10. https://github.com/facebook/rocksdb/issues/14965 Account UDI blocks as index blocks
11. https://github.com/facebook/rocksdb/issues/14966 Support parallel AddIndexEntry in trie index
12. https://github.com/facebook/rocksdb/issues/14967 Cover trie index modes
13. https://github.com/facebook/rocksdb/issues/14968 Add IndexFactory stress and benchmark flags

Previous: none.
Next: https://github.com/facebook/rocksdb/issues/14959.

This PR adds `include/rocksdb/index_factory.h` as a source-compatible set of new names over the current experimental UDI API:

- `IndexFactoryBuilder` -> `UserDefinedIndexBuilder`
- `IndexFactoryIterator` -> `UserDefinedIndexIterator`
- `IndexFactoryReader` -> `UserDefinedIndexReader`
- `IndexFactoryOptions` -> `UserDefinedIndexOption`
- `IndexFactory` -> `UserDefinedIndexFactory`
- `kIndexFactoryMetaPrefix` -> `kUserDefinedIndexPrefix`

This intentionally does not change table building, table reading, SST format, OPTIONS parsing, or the C API. Existing `UserDefinedIndexFactory` subclasses that only implement the old `NewBuilder()` and `NewReader(Slice&)` shape still compile and work through the new `IndexFactory` name.

Validation:
- `build_tools/rockstest.sh table_test --gtest_filter='IndexFactoryCompatibilityTest.*'`
- `make check-sources`
- GitHub Actions PR matrix passed on the current head

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

Reviewed By: jaykorean

Differential Revision: D114358164

Pulled By: xingbowang

fbshipit-source-id: 5c830dc1f9dfcf11373423a03103a595fd8ad921
2026-08-04 12:26:35 -07:00
Josh Kang 14ecf7f13c Route single-blob reads through BlobFetcher (#15046)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15046

`BlobFetcher` is the policy boundary for single-blob reads, but
`Version::Get()` and primary direct-write postprocessing still called
lower-level backends directly. This duplicated backend selection and could
bypass future fetcher policy or decorators.

Route those paths through their existing `VersionBlobFetcher` and remove the
redundant Slice-based `Version::GetBlob()` overload. This is behavior-preserving:
each fetcher delegates to the same Version or direct-write backend with the same
`ReadOptions`, cache, and lifetime context.

Reviewed By: xingbowang

Differential Revision: D114616978

fbshipit-source-id: 0d94cc389138f27791de636e2cca585e51cb142a
2026-08-03 12:59:37 -07:00
Ranjan Banerjee b1b50fe102 Parallel checkpointing stress test (#15037)
Summary:
Adds db_stress / crash-test coverage for the parallel `CheckpointEngine` so it
is exercised alongside existing features (atomic flush, sync fault injection,
cf_consistency, backups).

- Opens one shared `CheckpointEngine` per db_stress process, configured by
  `--checkpoint_engine_max_background_operations` and
  `--checkpoint_engine_use_link_file_when_available`.
- `--parallel_checkpoint_one_in=N`: each checkpoint independently uses the
  parallel engine with probability 1/N, otherwise the legacy serial
  `Checkpoint` API, so serial and parallel paths run concurrently across threads.
- `db_crashtest.py` randomizes all three per run.

This coverage surfaced the missing destination fsync in the parallel copy path
fixed by the parent diff.

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

Reviewed By: xingbowang

Differential Revision: D114414578

Pulled By: rban1

fbshipit-source-id: 48e7920a62b9b663a1c6054fc60b5765667bcbe5
2026-08-03 11:11:46 -07:00
Ranjan Banerjee cda4a73a19 Fsync destination files in parallel CheckpointEngine copies (#15038)
Summary:
Parallel checkpoint copies were never fsynced. `CopyEngine::CopyOrCreateFile`
gated the destination sync on `use_fsync` (wired to `db_options.use_fsync`) --
but `use_fsync` means "fsync vs fdatasync", not "whether to sync". With the
default `use_fsync=false`, the parallel path skipped the sync, so a copied
MANIFEST/SST stayed in the unsynced buffer and was dropped by
`TestFSWritableFile::Close` under `sync_fault_injection`, leaving a 0-byte file
and checkpoint corruption:

  Corruption: no log_file_number, next_file_number, last_sequence entry in MANIFEST
  Corruption: Sst file size mismatch between expected N and file system 0

Serial `CopyFile` always syncs (using `use_fsync` only to pick the flavor), so
it never hit this.

Fix: split `WorkItem` / `CopyOrCreateFile` into `sync` (whether to sync) and
`use_fsync` (fsync vs fdatasync). CheckpointEngine now passes `sync=true,
use_fsync=db_options.use_fsync`, matching serial. BackupEngine is unchanged --
`sync=options_.sync` with the new `use_fsync=false` default reproduces the prior
behavior.

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

Reviewed By: xingbowang

Differential Revision: D114414620

Pulled By: rban1

fbshipit-source-id: e0f1f3dbadcd48c5a5c2a11111a266dbed623f86
2026-08-03 10:38:26 -07:00
Josh Kang 00e73b99a2 Centralize db_stress validation errors (#15034)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15034

`db_stress` startup validation had accumulated multiple error-reporting paths, including direct `fprintf`/`exit(1)` pairs. This made invalid configurations inconsistent and bypassed normal unwinding.

Centralize validation failures through `ReturnValidationError`, including diagnostics with runtime values, while leaving operational I/O failures on their existing paths. The variadic stream interface keeps the existing messages without `printf` casts.

Reviewed By: jaykorean

Differential Revision: D114384751

fbshipit-source-id: f396f8e7d099ef87c72aaf19455b777f2125f4df
2026-08-03 10:33:36 -07:00
Xingbo Wang 460aea1523 Fix C API compatibility baseline selection in CI (#15039)
Summary:
The C API compatibility job always fetched `main` as its baseline. On release branches, that can report false API removals for symbols that were added to `main` after the release branch was created.

Select the compatibility baseline from the triggering event instead:

- Pull requests use the merge target commit.
- Pushes use the branch's pre-push commit.
- Newly created branches or tags use `HEAD`, since they have no prior ref state to preserve.

Fetch the selected commit by SHA, fail explicitly for unsupported events or missing event data, and pass the resolved baseline to `check-c-api-gen`. Also update the Makefile documentation to describe the PR and push behavior.

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

Test Plan: CI

Reviewed By: pdillinger

Differential Revision: D114437175

Pulled By: xingbowang

fbshipit-source-id: ee66f54bcabba238477d07a69ec7dcb42219c088
2026-08-03 06:42:26 -07:00
zaidoon 4b35e9966c Add batch-owned pinned MultiGet C API (#14988)
Summary:
- add `rocksdb_batched_multi_get_pinned_cf` for batched reads using `rocksdb_slice_t` keys
- return one `rocksdb_pinnable_multi_get_t` owner for all values and per-key errors
- preserve input ordering, duplicates, misses, and sorted-input behavior
- expose indexed borrowed results plus count and destroy functions

## Why

`rocksdb_batched_multi_get_cf_slice` returns one separately allocated `rocksdb_pinnableslice_t` wrapper for every found key. Language bindings that want to retain a full batch have to manage and destroy each wrapper independently.

The new API keeps successful `PinnableSlice` values in one owner. It stores only compact value and error payloads, while a result index maps them back to the original key order. Found values and error messages remain borrowed until the batch is destroyed.

The batch must be destroyed before closing the DB that created it because pinned values can retain DB-owned cache state.

## Compatibility

This is additive. Existing MultiGet APIs and ownership rules are unchanged.

## Testing

`c_test` covers:

- found and missing keys
- duplicate keys and input ordering
- empty values
- per-key merge errors
- sorted and unsorted input
- empty batches
- out-of-bounds access
- borrowed value and error lifetimes

Validation run locally:

- `make -j8 check-c-api-c_test`
- `CXX=clang++ make check-c-api-gen CHECK_C_API_GEN_STRICT=1 CLANG_FORMAT_BINARY=clang-format`
- C API link-completeness and backward-compatibility checks

This API is intended to replace a temporary compatibility extension in `rust-rocksdb` once it is available in a RocksDB release.

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

Reviewed By: jaykorean

Differential Revision: D114357453

Pulled By: xingbowang

fbshipit-source-id: a35785f7bb4d097b391932e2b9f96fc177d1ba1c
2026-08-02 14:13:23 -07:00
Peter Dillinger b7b6d7c8c0 Mirror CI third-party build deps on a GitHub release instead of S3 (#15041)
Summary:
CI/build downloaded pinned third-party binaries (compression source tarballs, Java test jars, and clang-format-diff.py) from an S3 bucket. Move the mirror to assets on an unlisted "third-party-deps" GitHub release, so the files live alongside the code with no separate bucket to maintain and no per-repo bandwidth quota (unlike Git LFS). Recent releases on the RocksDB release page are not polluted (https://github.com/facebook/rocksdb/releases/, far down the list probably from tagging a very old release).

The compression download variables are refactored from *_DOWNLOAD_BASE (a directory URL) to *_DOWNLOAD_PREFIX (directory + "<project>-", or "...archive/v"), so each release asset can carry a project-name prefix in the otherwise flat release namespace while the upstream defaults still resolve to byte-identical URLs. The Java DEPS_URL defaults (Makefile and CMake) and the clang-format-diff.py download are repointed at the release. A comment at each dependency site links to the release edit page for adding/updating assets.

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

Test Plan:
CI and...

Downloaded all 11 assets from the release and confirmed their SHA256 sums match the files previously served from S3 (tarball/jar sums are also enforced by the existing Makefile checksum checks). Verified the refactored Makefile upstream defaults expand to the same URLs as before, and that the CI *_DOWNLOAD_PREFIX overrides plus DEPS_URL resolve to the uploaded asset names. Covered by the normal PR/nightly/weekly jobs that build compression from source and run the Java tests (Make and CMake).

Reviewed By: xingbowang

Differential Revision: D114443886

Pulled By: pdillinger

fbshipit-source-id: 8c59435e7064f0d9931cba103c8120fe113b0d6d
2026-08-01 14:47:28 -07:00
Peter Dillinger afc922bf5f Make ReadOptions::table_filter a caller-owned pointer for cheaper copies (#15005)
Summary:
ReadOptions is copied frequently on read paths, and its only non-trivially-copyable member, table_filter, is a std::function that is almost always empty yet still gets deep-copied on every ReadOptions copy (and can heap-allocate). This changes table_filter to a pointer to a caller-owned std::function instead of storing one by value: when non-nullptr, the caller is responsible for keeping the pointed-to function alive for the duration of the read (like the other pointer fields such as iterate_upper_bound). With this change every ReadOptions member is trivially copyable, so copying ReadOptions no longer touches a std::function at all. That trivial-copyability is locked in with a static_assert in options_test.cc (kept out of the public header).

This is inspired by D113109635, which targeted the same ReadOptions-copy hotspot by relocating table_filter to the end of the struct so the trivial members memcpy in one contiguous block. This is intended to be a superior solution: rather than only defragmenting the copy, it removes the std::function copy entirely.

This is a breaking API change, tolerated because table_filter is a relatively obscure option. A nullptr, or a pointer to an empty std::function, means no filtering (preserving the previous empty-function behavior). A shared internal helper HasTableFilter(const ReadOptions&) in options_helper.h centralizes the nullptr + empty-function check used on the read paths. The C API and Java bindings now own the std::function and hand ReadOptions a pointer to it; their public signatures are unchanged, and the C API rocksdb_readoptions_t clears the owned function when the callback is null.

Making ReadOptions trivially copyable also let
-Werror=unused-but-set-variable fire on a pre-existing dead local in db_bench's MultiScan: a ReadOptions that set async_io/readahead_size but was never passed to NewMultiScan (the warning had been suppressed while ReadOptions had a non-trivial member). That local was in fact inert for multiscan -- BlockBasedTableIterator::Prepare overrides ReadOptions::async_io with MultiScanArgs::use_async_io for the coalesced prefetch, so multiscan async I/O is driven by --multiscan_use_async_io, not ReadOptions::async_io. Removed the dead local; MultiScan now uses read_options_ directly, consistent with MultiScanRandom.

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

Test Plan:
Existing table_filter coverage was adapted to the new pointer API and still exercises the behavior end to end: db_iterator_test.TableFilter (callback invoked once per table; returning false hides a table) and its range-conversion rejection case; db_bloom_filter_test SstQueryFilter/FixedWidthSegments range queries; and trie_index_db_test's range-query iterator (which returns iterators outliving the filter, exercising the caller-owned lifetime). db/c_test covers the C API set/clear/has_table_filter path. options_test asserts std::is_trivially_copyable_v<ReadOptions>.

Manually verified the db_bench MultiScan async behavior with a ~200MB DB and --use_direct_reads: with ReadOptions::async_io held true, async reads tracked only --multiscan_use_async_io (rocksdb.read.async.micros count 329511 with use_async_io=true vs 0 with false), confirming ReadOptions::async_io does not drive multiscan async and the removed local had no effect.

Reviewed By: anand1976

Differential Revision: D113613525

Pulled By: pdillinger

fbshipit-source-id: 38d73b607500a5c7ca2b89dd8f4187ef43940aa4
2026-07-31 10:20:25 -07:00
anand1976 6e73763b8a Update version to 11.9.0 for 11.8 release (#15023)
Summary:
- Bump version.h from 11.8.0 to 11.9.0
- Add `11.8.fb` to `check_format_compatible.sh`
- Sync HISTORY.md `## 11.8.` section from `11.8.fb`
- Delete 5 consumed `unreleased_history/` note file(s) from main

Part of 11.8 release workflow.

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

Reviewed By: xingbowang

Differential Revision: D114304679

Pulled By: anand1976

fbshipit-source-id: 6fecd5c5175140dfa1f6ba16aec42748eee91172
2026-07-31 04:30:52 -07:00
Darkhan Imangaliyev f9df618e4d Api to checkpoint only selected set of column families (#15019)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15019

RocksDB's Checkpoint API currently copies all column families.
For use cases that only need a subset, this is wasteful.

This diff adds a new `CreateCheckpoint(dir, column_families, ...)` overload that:
 - Validates input handles (rejects null/foreign handles, coalesces duplicates, always includes default CF).
 - Filters GetLiveFilesStorageInfo to capture only the requested CFs' table/blob files.
 - Post-processes the copied MANIFEST by appending kColumnFamilyDrop records for excluded CFs, so the checkpoint opens without referencing missing files.
 - An empty column_families vector falls through to the existing whole-DB path.

Reviewed By: xingbowang, pdillinger

Differential Revision: D113963616

fbshipit-source-id: d1becbbb9528dd13c2dc314165880b76123757f0
2026-07-30 16:16:58 -07:00
Peter Dillinger 026f63de6c Parallelize the non-unit-test checks in make check (#15028)
Summary:
`make check` ran a series of extra checks serially, one after another, after the GNU-parallel unit tests: the Python syntax check, ldb/db_crashtest/ rocksdb_dump tests, and the format/buck/sources/workflow/c-api-gen source checks. These had accumulated into a non-trivial single-threaded tail on every `make check`.

Each extra check is now a make target collected in a single CHECK_EXTRA_TARGETS list, and gen_check_scripts emits a `t/run-<target>` wrapper per enabled check. check_0 already runs every `t/run-*` as a gnu_parallel job, so the checks now run alongside the unit tests, hiding their cost behind the (dominant) test phase. The slow ones (check-c-api-gen, check-format) are front-loaded via slow_test_regexp, and under CI sharding the checks are distributed round-robin so each runs on exactly one shard. The J=1 / no-gnu-parallel fallback runs them serially. New check-all-python and rocksdb_dump_test targets wrap the two steps that were previously inlined; the guards match the old serial tail exactly.

Each wrapper frames its result with a gtest-style
`[ RUN ]` / `[ OK ]` / `[ FAILED ] >>> make <check> <<<` line so a non-unit-test failure is easy to distinguish and re-run.

The wrappers spawn sub-makes concurrently, and each make invocation otherwise re-runs build_detect_platform, which rewrites the shared make_config.mk non-atomically -- concurrently that corrupts it ("missing separator"). The wrappers pass a new ROCKSDB_RECURSIVE_MAKE_SAME_CONFIG=1, which reuses the parent's already-generated make_config.mk (guarded on it existing, so normal builds still detect) and also silences the parse-time banners for clean output.

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

Test Plan: `make check` runs the same set of auxiliary checks as before, now as parallel jobs. Verified that gen_check_scripts emits one wrapper per enabled check, that the SKIP_FORMAT_BUCK_CHECKS / ASSERT_STATUS_CHECKED / AIX guards still gate the set, that CI_TOTAL_SHARDS distributes the checks one-per-shard (union across shards is the full set), and that many concurrent wrappers no longer corrupt make_config.mk. A seeded check failure surfaces framed as `[ FAILED ] >>> make <check> <<<`.

Reviewed By: xingbowang

Differential Revision: D114245857

Pulled By: pdillinger

fbshipit-source-id: 519f78ccdcfbfdd88509980f78944e527a572331
2026-07-30 15:49:09 -07:00
Anand Ananthabhotla e2ccb78cf2 Avoid deletefile test purge/flush deadlock (#15027)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15027

`BackgroundPurgeCFDropTest` blocks `DBImpl::BGWorkPurge:start` to verify dropped-CF files remain until background purge is released. Runtime OPTIONS-file cleanup can also schedule a purge on the HIGH pool when `avoid_unnecessary_blocking_io` is enabled, so installing the sync-point dependency before test setup can park that OPTIONS purge before the foreground `Flush()` has a chance to run.

This does not need a broader product change. Runtime OPTIONS-file writes are rare, and the hard deadlock is a test artifact caused by the sync-point dependency. Outside the test, the observable impact would be limited to a foreground `Flush()` API call waiting longer if its flush is queued behind the OPTIONS-file purge.

Scope the sync-point dependency to the behavior under test by installing it only for the background-purge case, after the setup flush has produced the SST and just before `DropColumnFamily()`. This keeps setup-side OPTIONS-file purge work outside the blocked window while still verifying that dropped-CF file deletion waits for background purge.

Reviewed By: joshkang97

Differential Revision: D114241457

fbshipit-source-id: 21c5f4242a50590e161b3720077a9dfe5e77aa08
2026-07-30 11:25:22 -07:00
Josh Kang c64f682946 Fix IoUringBackend crash in ReadIOExecutor worker threads (#15020)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15020

Each read executor worker creates its own `IoUringBackend`, and Folly's I/O thread pool creates its worker threads and EventBases lazily. A successful calling-thread probe or `DB::Open()` therefore does not validate every worker. A later read can start a new worker; previously, an `IoUringBackend` initialization exception on that worker could escape startup and terminate the process. The previous 512-submit setup also allowed Folly's capacity retry to reduce the completion queue to 512 while retaining a 1024-entry submission queue, which the kernel rejects with `EINVAL`.

Use a 256-submit limit so the 512-entry retry remains valid. Configure the shared read executor during `DB::Open()` through `DBOptions::read_io_executor_threads`, defaulting to one thread. The backend factory catches per-worker io_uring initialization failures and uses Folly's default backend. Each worker's thread-local availability marker starts false and is set true only after `IoUringBackend` construction succeeds. When construction fails, the marker remains false and `SubmitReadAsync()` performs the read synchronously. The executor's maximum thread capacity can be raised or lowered; `db_stress` requests eight threads for async DB APIs.

To expose this degradation, `FSRandomAccessFile::SubmitReadAsync()` now reports whether the asynchronous path was used. `RandomAccessFileReader` increments the `FILE_SUBMIT_ASYNC_READ_FALLBACK` ticker for every submission that falls back to synchronous I/O, making the fallback visible through the DB's `Statistics`.

Reviewed By: xingbowang

Differential Revision: D114031018

fbshipit-source-id: 1b7f6c1cea693e5dd743ea1ada0fca1cf757b3f9
2026-07-30 11:18:08 -07:00
Josh Kang 359d57be96 Fix blob file path misidentification in SstFileManager during compaction (#15021)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15021

The warm storage crash failure in T282626155 exposed a RocksDB bookkeeping bug, not a transient WS error. The regression was introduced by D91480994 / upstream RocksDB PR https://github.com/facebook/rocksdb/pull/14227 (“Support abort background compaction jobs”). That change routed `BlobFileBuilder` output tracking through `CompactionOutputs::output_file_paths_`, which contains both blob and SST paths, while blob completion still assumed `.back()` identified the current blob.

Failure timeline:

1. Compaction job 225 opens `000455.blob`, producing `[000455.blob]`.
2. It opens `000456.sst`, producing `[000455.blob, 000456.sst]`.
3. Blob 455 finishes, but `OnBlobFileCompleted()` receives `.back()`, which is `000456.sst`.
4. `SstFileManager` stats the still-open SST. WS correctly returns non-retryable `NOT_FOUND` because the output is not pathname-visible until close.
5. Compaction fails before installing its outputs in the MANIFEST. Cleanup then deletes `000455.blob` and `000456.sst`; those deletions are consequences of the failure, not its cause.

This change derives callback paths from the blob file number instead of the shared cleanup vector, renames the vector accordingly, and adds a deterministic regression test verifying that `SstFileManager` tracks every blob created during compaction.

Reviewed By: pdillinger

Differential Revision: D114002145

fbshipit-source-id: 472b563f3fb731753f286a40feece7428928bc20
2026-07-30 10:38:04 -07:00
generatedunixname1395027625275998 ba81144d8d Force-inline xxph3 mix helpers on the NPHash64/Hash64 hot path (#14981)
Summary: Pull Request resolved: https://github.com/facebook/rocksdb/pull/14981

Reviewed By: pdillinger

Differential Revision: D112965953

fbshipit-source-id: 9645d9287a9aba987bf1b37752df7f9dcb3a9669
2026-07-29 14:21:15 -07:00
Anand Ananthabhotla 4b9070bccb Defer obsolete OPTIONS file deletion on open (#15004)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15004

This change reduces DB open latency on remote storage by avoiding foreground deletion of obsolete `OPTIONS-*` files during open when `avoid_unnecessary_blocking_io` is enabled. DB open now avoids foreground deletion of obsolete `OPTIONS-*` files on the pre-open `WriteOptionsFile()` path. `RenameTempFileToOptionsFile()` records the new OPTIONS file and chooses an explicit cleanup mode: skip while open is still in progress, schedule deletion when background work is available, or delete immediately when blocking IO is allowed or background jobs are rejected.

`DBImpl::Open()` makes the deferred cleanup decision once final open status is known. A successful open schedules obsolete OPTIONS files on the purge queue, while a failed open deletes them synchronously before tearing down the partially opened DB. The default synchronous behavior remains unchanged when `avoid_unnecessary_blocking_io` is disabled, and the latest-two OPTIONS retention policy is preserved.

Reviewed By: pdillinger

Differential Revision: D113593680

fbshipit-source-id: 8aee99b3f38a729647d0bcb644d212181fe86895
2026-07-29 12:44:27 -07:00
311 changed files with 22454 additions and 3542 deletions

No files matched your search

+11 -5
View File
@@ -20,9 +20,15 @@ runs:
echo "GTEST_COLOR=1" >> "$GITHUB_ENV"
echo "CTEST_OUTPUT_ON_FAILURE=1" >> "$GITHUB_ENV"
echo "CTEST_TEST_TIMEOUT=300" >> "$GITHUB_ENV"
echo "ZLIB_DOWNLOAD_BASE=https://rocksdb-deps.s3.us-west-2.amazonaws.com/pkgs/zlib" >> "$GITHUB_ENV"
echo "BZIP2_DOWNLOAD_BASE=https://rocksdb-deps.s3.us-west-2.amazonaws.com/pkgs/bzip2" >> "$GITHUB_ENV"
echo "SNAPPY_DOWNLOAD_BASE=https://rocksdb-deps.s3.us-west-2.amazonaws.com/pkgs/snappy" >> "$GITHUB_ENV"
echo "LZ4_DOWNLOAD_BASE=https://rocksdb-deps.s3.us-west-2.amazonaws.com/pkgs/lz4" >> "$GITHUB_ENV"
echo "ZSTD_DOWNLOAD_BASE=https://rocksdb-deps.s3.us-west-2.amazonaws.com/pkgs/zstd" >> "$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
# 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"
shell: bash
+33 -16
View File
@@ -5,23 +5,40 @@
// return build({ icon, headerTitle, triggerLine, responseBody, footerLines
// });
// GitHub rejects issue comments longer than this many characters. The model
// response (responseBody) is the only unbounded input, so we trim it to fit
// while keeping the fixed scaffolding (header, trigger, footer) intact.
const MAX_COMMENT_LENGTH = 65536;
const TRUNCATION_NOTE =
'\n\n*[Truncated — full output in execution log artifact]*';
module.exports = function buildAiReviewComment(
{icon, headerTitle, triggerLine, responseBody, footerLines}) {
return [
`## ${icon} ${headerTitle}`,
'',
triggerLine,
'',
'---',
'',
responseBody,
'',
'---',
'',
'<details>',
'<summary>ℹ️ About this response</summary>',
'',
...footerLines,
'</details>',
const assemble = (body) => [`## ${icon} ${headerTitle}`,
'',
triggerLine,
'',
'---',
'',
body,
'',
'---',
'',
'<details>',
'<summary>ℹ️ About this response</summary>',
'',
...footerLines,
'</details>',
].join('\n');
const body = responseBody || '';
const comment = assemble(body);
if (comment.length <= MAX_COMMENT_LENGTH) {
return comment;
}
// Drop only as much of the body as needed to fit, then re-assemble so the
// footer/<details> block is preserved rather than cut off mid-comment.
const overflow = comment.length - MAX_COMMENT_LENGTH;
const keep = Math.max(0, body.length - overflow - TRUNCATION_NOTE.length);
return assemble(body.substring(0, keep) + TRUNCATION_NOTE);
};
+1 -1
View File
@@ -103,7 +103,7 @@ module.exports = function parseClaude({executionFile, conclusion, meta}) {
responseBody,
footerLines: [
'Generated by [Claude Code](https://github.com/anthropics/claude-code-action).',
'Review methodology: `claude_md/code_review.md`',
'Review methodology: `claude_md/ci_review_prompt.md`',
'',
'**Limitations:**',
'- Claude may miss context from files not in the diff',
+1 -1
View File
@@ -83,7 +83,7 @@ module.exports = function parseCodex(
responseBody,
footerLines: [
'Generated by Codex CLI.',
'Review methodology: `claude_md/code_review.md`',
'Review methodology: `claude_md/ci_review_prompt.md`',
'',
'**Limitations:**',
'- Codex may miss context from files not in the diff',
+11 -1
View File
@@ -140,7 +140,17 @@ jobs:
- uses: "./.github/actions/pre-steps"
- run: sudo apt-get update && sudo apt-get install -y build-essential libgflags-dev libsnappy-dev zlib1g-dev libbz2-dev liblz4-dev libzstd-dev
- run: sudo mount -o remount,size=16G /dev/shm
- run: sudo dd bs=1048576 count=4096 if=/dev/zero of=/swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile
- name: Ensure swap space
run: |-
if sudo swapon --show=NAME --noheadings --raw | grep -Fxq /swapfile; then
echo "/swapfile is already active"
else
sudo dd bs=1048576 count=4096 if=/dev/zero of=/swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
fi
sudo swapon --show
- 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
+43 -4
View File
@@ -65,7 +65,9 @@ jobs:
pip install https://files.pythonhosted.org/packages/fb/ac/3c04772acc0257f5730e83adb542b2603c1a62d1315010ab593a980af404/clang_format-21.1.2-py2.py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
clang-format --version
- name: Download clang-format-diff.py
run: wget https://rocksdb-deps.s3.us-west-2.amazonaws.com/llvm/llvm-project/release/12.x/clang/tools/clang-format/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
- name: Check format
run: VERBOSE_CHECK=1 make check-format
- name: Compare buckify output
@@ -320,16 +322,46 @@ jobs:
# Run the make target in STRICT mode so this core CI job HARD-FAILS rather
# than silently skipping if a prerequisite (clang++, clang-format, or the
# compat baseline ref) is ever missing. See `make check-c-api-gen`.
env:
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PUSH_BEFORE_SHA: ${{ github.event.before }}
run: |
# The container checkout is owned by a different user, so git refuses to
# operate on it ("dubious ownership") -- mark it safe for the `git fetch`
# and the `git show` the compat checker runs.
git config --global --add safe.directory "$GITHUB_WORKSPACE"
git config --global --add safe.directory '*'
git fetch --no-tags --depth=1 origin main
# A PR must preserve the API of its merge target, while a push must
# preserve the API of the branch before the push. Comparing every event
# with main produces false removals when a release branch predates APIs
# added to main.
case "$GITHUB_EVENT_NAME" in
pull_request) api_compat_ref="$PR_BASE_SHA" ;;
push) api_compat_ref="$PUSH_BEFORE_SHA" ;;
*)
echo "ERROR: unsupported event for C API compatibility: $GITHUB_EVENT_NAME" >&2
exit 1
;;
esac
if [ -z "$api_compat_ref" ]; then
echo "ERROR: C API compatibility baseline is empty" >&2
exit 1
fi
zero_sha=0000000000000000000000000000000000000000
if [ "$api_compat_ref" = "$zero_sha" ]; then
# A newly created branch or tag has no prior ref state to preserve.
api_compat_ref=HEAD
else
git fetch --no-tags --depth=1 origin "$api_compat_ref"
api_compat_ref=FETCH_HEAD
fi
CXX=clang++-21 make check-c-api-gen \
CHECK_C_API_GEN_STRICT=1 \
API_COMPAT_REF=FETCH_HEAD \
API_COMPAT_REF="$api_compat_ref" \
CLANG_FORMAT_BINARY=clang-format-21
- uses: "./.github/actions/teardown-ccache"
if: always()
@@ -394,13 +426,20 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/cache-getdeps-downloads"
- uses: "./.github/actions/setup-folly"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: mini-crashtest
- uses: "./.github/actions/cache-folly"
id: cache-folly
- uses: "./.github/actions/build-folly"
with:
cache-hit: ${{ steps.cache-folly.outputs.cache-hit }}
- run: |
ulimit -S -n $(ulimit -Hn) # Raise open files soft limit to hard limit
echo "Updated ulimit -Hn: $(ulimit -Hn) -Sn: $(ulimit -Sn)"
make V=1 -j8 CRASH_TEST_EXT_ARGS='--duration=${{ matrix.crash_duration }} --max_key=2500000' ${{ matrix.crash_test_target }}
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"
+10 -1
View File
@@ -59,6 +59,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"db/compaction/sst_partitioner.cc",
"db/compaction/subcompaction_state.cc",
"db/convenience.cc",
"db/coro_db.cc",
"db/db_filesnapshot.cc",
"db/db_impl/compacted_db_impl.cc",
"db/db_impl/db_impl.cc",
@@ -95,6 +96,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"db/merge_operator.cc",
"db/multi_scan.cc",
"db/output_validator.cc",
"db/periodic_compaction_phaser.cc",
"db/periodic_task_scheduler.cc",
"db/range_del_aggregator.cc",
"db/range_tombstone_fragmenter.cc",
@@ -103,7 +105,6 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"db/snapshot_impl.cc",
"db/table_cache.cc",
"db/table_properties_collector.cc",
"db/transaction_log_impl.cc",
"db/trim_history_scheduler.cc",
"db/version_builder.cc",
"db/version_edit.cc",
@@ -111,7 +112,9 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"db/version_set.cc",
"db/version_util.cc",
"db/wal_edit.cc",
"db/wal_iterator_impl.cc",
"db/wal_manager.cc",
"db/wide/lazy_wide_columns.cc",
"db/wide/read_path_blob_resolver.cc",
"db/wide/wide_column_serialization.cc",
"db/wide/wide_columns.cc",
@@ -4947,6 +4950,12 @@ cpp_unittest_wrapper(name="db_kv_checksum_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_lazy_entity_test",
srcs=["db/wide/db_lazy_entity_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_log_iter_test",
srcs=["db/db_log_iter_test.cc"],
deps=[":rocksdb_test_lib"],
+30 -6
View File
@@ -8,12 +8,18 @@ This document provides guidance for generating and reviewing code in the RocksDB
### Code Quality and Maintainability
**Clarity and Readability:** Write clear, self-documenting code. Use meaningful variable names, add comments for complex logic, and structure code to minimize cognitive load. Avoid clever tricks that sacrifice readability for marginal performance gains unless absolutely necessary. Avoid static_cast, reinterpret_cast, and C-style casts; static_cast_with_check, up_cast, and lossless_cast from cast_util.h are preferred.
**Clarity and Readability:** Write clear, self-documenting code. Use meaningful variable names, add comments for complex logic, and structure code to minimize cognitive load. Avoid clever tricks that sacrifice readability for marginal performance gains unless absolutely necessary.
**Consistent Style:** Follow existing code style conventions. RocksDB uses `.clang-format` for formatting, specific naming conventions, and structural patterns. Deviations from these patterns are frequently flagged in reviews.
**Error Handling:** Ensure robust error handling throughout the codebase. Use RocksDB's `Status` type consistently, propagate errors appropriately, and avoid silently ignoring failures. Reviewers pay close attention to edge cases and failure modes.
**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 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).
### Testing Philosophy
**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.
@@ -32,7 +38,7 @@ This document provides guidance for generating and reviewing code in the RocksDB
**Memory Copy:** Avoid unnecessary memory copies. Use move semantics, `std::string_view`, `Slice`, and pass-by-reference where appropriate. Be aware of implicit copies in STL containers and function returns. Prefer in-place operations over copy-and-modify patterns.
**CPU Cache Efficiency:** Design data structures and access patterns to be cache-friendly. Keep frequently accessed data together (data locality). Prefer sequential memory access over random access. Be mindful of cache line sizes (typically 64 bytes) and avoid false sharing in concurrent code. Consider struct packing and field ordering to improve cache utilization.
**CPU Cache Efficiency:** Design data structures and access patterns to be cache-friendly. Keep frequently accessed data together (data locality). Prefer sequential memory access over random access. Be mindful of cache line sizes (CACHE_LINE_SIZE, typically 64 bytes) and avoid false sharing in concurrent code (see CacheAlignedWrapper). Especially when adding new data members, be mindful of ordering within structs and classes to reduce unnecessary padding (special care with bool members) and avoid larger-than-necessary types (e.g. prefer OptSlice to std::optional<Slice>).
**Loop Optimization:** Look for opportunities to collapse nested loops, reduce loop overhead, and minimize branch mispredictions. Hoist invariant computations out of loops. Consider loop unrolling for tight inner loops. Batch operations when possible to amortize per-operation overhead.
@@ -47,7 +53,7 @@ This document provides guidance for generating and reviewing code in the RocksDB
- **Cold path** (executed rarely, e.g., DB open, configuration parsing, error handling): Maintainability and clarity are more important. Prefer readable code over micro-optimizations. Complex optimizations here add maintenance burden with negligible performance benefit.
- **Warm path** (moderate frequency): Balance both concerns. Use profiling data to guide optimization decisions.
**Avoid Premature Optimization:** While performance is critical, focus on correctness first, then optimize based on profiling data. However, be performance-aware from the start—choosing the right algorithm and data structure upfront is not premature optimization. Use the hot path analysis above to decide how much optimization effort is warranted.
**Avoid Premature Optimization:** Performance is known critical in places like inner loops of compaction, read path and write path. Elsewhere, try to find a "holy trinity" solution of *correct, simple, and reasonably efficient*. More complex solutions are only appropriate when profiling/benchmarking data shows a measurable payoff.
### API Design and Compatibility
@@ -55,7 +61,7 @@ This document provides guidance for generating and reviewing code in the RocksDB
**API Consistency:** New APIs should be consistent with existing patterns. Use similar naming conventions, parameter ordering, and return types. Reviewers will suggest changes to improve consistency with the broader codebase.
**Documentation:** Public APIs must be thoroughly documented. Include usage examples, parameter descriptions, and notes on thread safety, performance characteristics, and compatibility considerations.
**Documentation:** Public APIs must be thoroughly documented, without unnecessary embelishment nor dwelling on implementation details nor project planning. When non-obvious, include usage examples, parameter descriptions, known bugs or limitations, notes on thread safety, performance characteristics, and compatibility considerations. Re-read comments for ambiguous terminology and phrasing, such as ambiguously re-purposed programming jargon.
---
@@ -73,7 +79,7 @@ The database core handles write-ahead logging (WAL), memtables, compaction, and
**Testing:** Database core changes require extensive testing, including unit tests, integration tests, and stress tests. Test with various configurations, compaction styles, and concurrent workloads.
### Public Headers (`include`)
### Public Headers / "Public API" (`include/rocksdb/`)
Public headers define RocksDB's API surface. Changes here have the highest compatibility impact.
@@ -259,6 +265,10 @@ will automatically clean object files if the build parameters/flavor have
changed. The `build_tools/rockstest.sh` / `rocksptest.sh` helpers described
below set `AUTO_CLEAN=1` for you.
`AUTO_CLEAN=1` does not fix Make failures associated with stale .d files
referring to removed files. Resolve that manually or with `make clean`
without complaining to the user.
### Source checks
* Run `make check-sources` before committing. This catches non-ASCII
characters in source files and other source-level issues that CI will
@@ -298,6 +308,7 @@ build (and where noted, run tests) across:
| Axis | Must support |
|------|--------------|
| OS | Linux (x86_64 + ARM), macOS, Windows |
| Architecture | Little-endian and big-endian targets, including Linux on s390x |
| Compiler | GCC, Clang (libstdc++ **and** libc++), AppleClang, **MSVC (VS2022)**, MinGW (Linux cross-compile, build-only, no gflags) |
| Build system | Make, CMake, and BUCK (internal) -- keep all in sync (see "Build system" above) |
| Config | release (`-fno-rtti`), `ASSERT_STATUS_CHECKED`, ASAN/UBSAN/TSAN, folly, unity build, JNI/Java |
@@ -312,6 +323,12 @@ abstractions; otherwise guard with `#ifdef OS_WIN` (POSIX `<unistd.h>` ->
Windows `<process.h>`). Because libc++ is also tested, include what you use
rather than relying on libstdc++ transitive includes.
Do not use `static_assert` to reject a supported architecture for an optional
feature. Check an existing `port::` capability such as `port::kLittleEndian`
and disable only the unsupported feature so the rest of RocksDB still builds.
Use a portable representation when the feature itself must work across
architectures.
### Unit Test
* After all of the unit tests are added, review them and try to extract common
reusable utility functions to reduce code duplication due to copy past between
@@ -389,6 +406,11 @@ rather than relying on libstdc++ transitive includes.
### Adding release note
* Release note should be kept short at high level for external user consumption.
Release notes identify what users might care about most in a release. They
are not exhaustive and are not a guide. PLEASE learn from past agents who
ried to build elaborate release notes with implementation details and
elsewhere-documented nuance. That wastes time. Fight the bias that
"my change" is important so must be worthy of release note mention.
* If more than single markdown line, consider how their formatting will be
integrated into HISTORY.md.
@@ -402,7 +424,9 @@ rather than relying on libstdc++ transitive includes.
multiple minutes.
* Run `AUTO_CLEAN=1 ASSERT_STATUS_CHECKED=1 make check` to verify all Status
objects are properly checked. This catches missing error handling that can
lead to silent data corruption.
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.
### Monitoring make check progress
* Use `make check-progress` to get machine-parseable JSON progress while
+5 -1
View File
@@ -801,6 +801,7 @@ set(SOURCES
db/compaction/sst_partitioner.cc
db/compaction/subcompaction_state.cc
db/convenience.cc
db/coro_db.cc
db/db_filesnapshot.cc
db/db_impl/compacted_db_impl.cc
db/db_impl/db_impl.cc
@@ -837,6 +838,7 @@ set(SOURCES
db/merge_operator.cc
db/multi_scan.cc
db/output_validator.cc
db/periodic_compaction_phaser.cc
db/periodic_task_scheduler.cc
db/range_del_aggregator.cc
db/range_tombstone_fragmenter.cc
@@ -845,7 +847,6 @@ set(SOURCES
db/snapshot_impl.cc
db/table_cache.cc
db/table_properties_collector.cc
db/transaction_log_impl.cc
db/trim_history_scheduler.cc
db/version_builder.cc
db/version_edit.cc
@@ -853,7 +854,9 @@ set(SOURCES
db/version_set.cc
db/version_util.cc
db/wal_edit.cc
db/wal_iterator_impl.cc
db/wal_manager.cc
db/wide/lazy_wide_columns.cc
db/wide/read_path_blob_resolver.cc
db/wide/wide_column_serialization.cc
db/wide/wide_columns.cc
@@ -1560,6 +1563,7 @@ if(WITH_TESTS)
db/version_set_test.cc
db/wal_manager_test.cc
db/wal_edit_test.cc
db/wide/db_lazy_entity_test.cc
db/wide/db_wide_basic_test.cc
db/wide/db_wide_blob_direct_write_test.cc
db/wide/wide_column_serialization_test.cc
+60
View File
@@ -1,6 +1,66 @@
# Rocksdb Change Log
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
## 11.10.0 (08/28/2026)
### New Features
* Added `BlockBasedTableOptions::optimize_key_common_prefix` as part of new `format_version=8`. When enabled, each data block and index block stores the common user-key prefix of its keys once instead of at every restart point, reducing block size and block-cache footprint for prefix-heavy data and speeding up Seek for bytewise-ordered keys.
* A new experimental feature spreads out periodic (time-based) compaction trigger times across a fleet of similarly-configured DBs to avoid recurring "thundering herd" triggers, especially with remote compaction. See the new experimental DB option `periodic_compaction_phase_recovery_percent`.
### Public API Changes
* `IngestExternalFileArg::atomic_replace_range` now supports a range that only partially overlaps existing files, instead of failing. Requires universal compaction and a table factory that supports range deletion; other configurations still fail on partial overlap. The range is also now specified as half-open (`start` inclusive, `limit` exclusive), resolving a previously documented ambiguity in the upper bound.
* Added an immutable `external_table_config` option that is persisted in OPTIONS files and passed to external table factories during bootstrap.
### Bug Fixes
* Fixed a liveness bug where RocksDB could remain indefinitely in recovery from a soft background error when concurrent writes continuously generated normal flush requests.
* Fixed a race in online error recovery where a concurrent higher-severity I/O error could leave a stale retry-flush plan active, prevent the required WAL switch, and create duplicate internal WAL history detected on DB reopen.
* Fix "bottommost files" infinite compaction loops (`kBottommostFiles`). This happens with the unsupported combination of leveled compaction and `preserve_internal_time_seconds` / `preclude_last_level_data_seconds`, and in an edge case with user-defined timestamps when `full_history_ts_low` is unset. The underlying logic is now more unified to reduce risk of a similar divergence in logic in the future.
* Fixed `reuse_manifest_on_open` to create a fresh MANIFEST instead of appending after recovery sees an incomplete atomic group at the MANIFEST tail.
* Starting remote compaction (`CompactionService` / `DB::OpenAndCompact`) is now more efficient and more reliable, by avoiding unnecessary file operations (latency) while using more accurate consistency / precondition checks, both avoiding some spurious failures and rejecting more legitimate inconsistencies. New DBOption `remote_compaction_manifest_floor` can be disabled in case of unexpected problems with the change.
* Fixed unbounded retention of recovered transaction state by a long-lived secondary opened with `allow_2pc`.
* Fixed stale reads on secondary instances, where entries replayed from a WAL that the primary had already flushed could shadow the newer values in the flushed files indefinitely. Also fixed a case where a secondary could stop returning such a key altogether while the primary's flushed file holding it was unreadable.
* Fixed subset checkpoints to discard tolerated MANIFEST tail corruption before appending column-family drop records.
### Performance Improvements
* Improved compaction CPU efficiency for column families using an `SstPartitioner` (such as the built-in `SstPartitionerFixedPrefix`): when the partitioner can express its next boundary as a key prefix via the new `SstPartitioner::ShouldPartitionByPrefix()` API, compaction now checks that cached prefix inline instead of copying every user key and making a virtual `ShouldPartition()` call for each key-value written to an SST.
## 11.9.0 (08/14/2026)
### New Features
* Added an integrated and improved compression auto-skip feature (new CompressionOptions, still experimental) that saves CPU by skipping compression when and where the overall ratio is inadequate. This improves on the previous auto-skip compression manager by (a) taking achieved compression ratios into account, (b) using a continuously evolving payoff estimate, and (c) combining natively with parallel compression.
* Added the `blob_file_writable_file_max_buffer_size` mutable column family option. When set to a non-zero value, blob-file writers use it as their `WritableFileWriter` max buffer size instead of inheriting `DBOptions::writable_file_max_buffer_size`.
* Added an experimental lazy wide-column read API, `DB::GetEntityLazy()` / `DB::MultiGetEntityLazy()` (see `include/rocksdb/lazy_wide_columns.h`). The returned `LazyWideColumns` leaves blob-backed columns as unresolved references and, like an iterator, stays valid after the call returns, so the caller resolves only the columns -- and only the byte ranges -- it needs; a byte-range read of an uncompressed blob (separate-file or embedded) reads just those bytes from storage. Adds `Env::IOActivity::kLazyResolve` and `rocksdb.blobdb.lazy.*` statistics. Requires `max_open_files == -1`.
* Added DB property `rocksdb.num-running-remote-compactions`, reporting the number of compaction service jobs currently waiting in `CompactionService::Wait()`.
* Added DB property `rocksdb.num-unscheduled-compactions`, reporting compactions waiting in the DB's internal compaction queue but not yet assigned to a background job.
* Added `DB::AbortCompactions()` and `DB::ResumeCompactions()` for per-column-family compaction abort and resume, with the same active-abort semantics as the DB-wide `AbortAllCompactions()`/`ResumeAllCompactions()` but scoped to a single column family. Calls are reference-counted independently per column family and compose with the DB-wide abort.
### Public API Changes
* Add IndexFactory compatibility names for the existing user-defined index API.
* Added the experimental `CoroDB` capability, exposed by `DB::GetCoroDB()`, with lazy native coroutine variants of `Get` and `MultiGet`. The existing `DB::GetAsync()` and `DB::MultiGetAsync()` APIs dispatch through it.
* Added `rocksdb_batched_multi_get_pinned_cf()` to return batch-owned pinned MultiGet values and per-key errors without allocating one wrapper per found value.
* Add `DBOptions::read_io_executor_threads` to increase the maximum thread count of the shared filesystem read I/O executor used for asynchronous reads when opening a DB. Opening a DB never reduces the shared executor thread count. Change the experimental `FSRandomAccessFile::SubmitReadAsync()` API to return whether the asynchronous path was used, and add the `FILE_SUBMIT_ASYNC_READ_FALLBACK` ticker to count calls that fall back to synchronous reads.
* `ReadOptions::table_filter` is now a *pointer* to a caller-owned `std::function` instead of a `std::function` value, to improve efficiency in copying and destroying `ReadOptions` objects (which RocksDB does internally). This is a breaking change for users of this relatively obscure option, who will now need to declare a `std::function` that outlives the read. As a side effect of `ReadOptions` being trivially copyable, new but legitimate compiler warnings may surface.
* Removed the experimental `CreateAutoSkipCompressionManager()`. Auto-skip compression is now a first-class feature of the block-based table builder, controlled by the new experimental `CompressionOptions::auto_skip`.
* Added `CoroStackableDB` for implementing wrappers over native coroutine reads and used it to add coroutine `Get` and `MultiGet` support to `DBWithTTL` and WritePrepared `TransactionDB`. A stackable wrapper advertises coroutine support only when its wrapped DB supports it; TTL value decoding and transaction visibility semantics are preserved.
* Added a `kSstFileWriter` value to `TableFileCreationReason`, which `SstFileWriter` now reports instead of the catch-all `kMisc`. This, along with new `db_name` and `is_remote_compaction` fields in `FilterBuildingContext`, improves the contextual information available to custom filter and compression strategies.
* Renamed `TransactionLogIterator` to `WalIterator` (and `include/rocksdb/transaction_log.h` to `include/rocksdb/wal_iterator.h`), because it iterates the write-ahead log and is unrelated to `TransactionDB`. The old names still work. `DB::GetUpdatesSince()` is unchanged. In Java, `WalIterator` is the new name and `TransactionLogIterator` remains as a subclass. Also documented existing behavior that was previously unstated: only writes that reached the WAL are visible (so `WriteOptions::disableWAL` and `IngestExternalFile()` leave holes that end iteration), what each iterator state means, and that `GetUpdatesSince()` silently starts later when the requested sequence number is unavailable.
### Bug Fixes
* Fixed a bug where `BlobFileBuilder` passed an SST path to `OnBlobFileCompleted()` instead of the completed blob path during BlobDB garbage-collection compactions, causing incorrect `SstFileManager` accounting and compaction failures on filesystems where the SST was not yet pathname-visible.
### Performance Improvements
* When `DBOptions::avoid_unnecessary_blocking_io` is true, obsolete `OPTIONS-*` files found during DB open are deleted by background purge instead of synchronously on the opening thread.
## 11.8.0 (07/28/2026)
### Public API Changes
* Add callback-based asynchronous read APIs, `DB::GetAsync()` and `DB::MultiGetAsync()`. The idea is that when IO is required, RocksDB can suspend its internal coroutine read path, allowing the read executor thread to do other work. When the IO is complete, RocksDB invokes the user callback. This requires filesystem support for full performance benefits. A new filesystem API `FSRandomAccessFile::SubmitReadAsync()` is introduced for this. Unlike `ReadAsync`, the filesystem is responsible for eventually calling the callback. `FileSystem::GetReadExecutor()` returns the IO executor whose EventBases run coroutine read processing. In the Posix filesystem, an IO-uring based event loop is used to complete the IO.
* Added `FlushOptions::listener_wait` (default `false`). When set together with `FlushOptions::wait == true`, `DB::Flush()` will not return until the registered `EventListener::OnFlushCompleted` callbacks for the flushed memtables have finished running. By default (`false`), `Flush(wait=true)` may return as soon as the flush result is committed, which can be before (or while) the `OnFlushCompleted` callbacks execute on the background flush thread. Also added the corresponding C API `rocksdb_flushoptions_set_listener_wait()` / `rocksdb_flushoptions_get_listener_wait()`.
* Deprecated `PinnableWideColumns::serialized_size()` in favor of the new `PinnableWideColumns::payload_size()`, which returns the total size of the columns' names and values (for a plain value this equals the value size). Relatedly, wide-column read accounting changed subtly: read statistics for entities (`BYTES_READ`, `BYTES_PER_READ`, and the per-key byte counts for `GetEntity`/`MultiGetEntity`) and the `ReadOptions::value_size_soft_limit` threshold now measure entities by this payload size instead of the serialized entity size. This drops serialization framing bytes from those measurements (so reported/limited sizes for entity reads are slightly smaller than before) and makes plain-value and wide-column accounting consistent.
### Behavior Changes
* `ReadOptions::value_size_soft_limit` now also bounds the blob values resolved from pre-flush blob direct write references during `MultiGet`/`MultiGetEntity` (previously these were resolved after the limit check and read without bound). Enforcement is "always make progress": at least one key is read even if its value alone exceeds the limit, and only once the returned size exceeds the limit do subsequent keys get `Status::Aborted` -- so a caller retrying the aborted keys cannot loop forever on a single value that by itself exceeds the limit.
### Performance Improvements
* Reduced copying when reading wide-column entities that have blob-referenced columns, whether stored in separate blob files or embedded (same-file) as blob records in the same SST. On the point-lookup (`GetEntity`/`MultiGetEntity`) and blob-backed memtable/direct-write read paths, resolving these columns no longer re-serializes the whole entity into a fresh buffer: inline columns are referenced in place in the pinned entity and each resolved blob value is referenced in place from its fetched buffer. This is enabled by an internal change to `PinnableWideColumns` that lets its columns reference multiple backing buffers.
## 11.7.0 (07/16/2026)
### Public API Changes
* Added `RangeLockManagerHandle::SetIsKilledCallback()`, a `std::function<bool()>` predicate polled during range-lock waits. When it returns true, a blocked `GetRangeLock()` wait returns promptly with `Status::Aborted()` instead of waiting for the lock timeout. Range-lock users that install no callback keep the original wait-until-grant-or-timeout behavior.
+107 -36
View File
@@ -86,11 +86,20 @@ else
LIB_MODE?=shared
endif
$(info $$DEBUG_LEVEL is $(DEBUG_LEVEL), $$LIB_MODE is $(LIB_MODE))
# Set by the gen_check_scripts wrappers to mark a recursive make reusing the
# parent's make_config.mk: silence the parse-time banners and skip
# build_detect_platform (below).
$(if $(ROCKSDB_RECURSIVE_MAKE_SAME_CONFIG),,$(info $$DEBUG_LEVEL is $(DEBUG_LEVEL), $$LIB_MODE is $(LIB_MODE)))
# Detect what platform we're building on.
# Export some common variables that might have been passed as Make variables
# instead of environment variables.
#
# Skipped by the gen_check_scripts wrappers (which run concurrently under
# gnu_parallel): each would otherwise rewrite the shared make_config.mk
# non-atomically and a concurrent reader could catch it mid-write ("missing
# separator"). Requires an existing make_config.mk, so a normal build still detects.
ifeq ($(and $(ROCKSDB_RECURSIVE_MAKE_SAME_CONFIG),$(wildcard make_config.mk)),)
dummy := $(shell (export ROCKSDB_ROOT="$(CURDIR)"; \
export CXXFLAGS="$(EXTRA_CXXFLAGS)"; \
export LDFLAGS="$(EXTRA_LDFLAGS)"; \
@@ -104,8 +113,9 @@ dummy := $(shell (export ROCKSDB_ROOT="$(CURDIR)"; \
export ROCKSDB_CXX_STANDARD="$(ROCKSDB_CXX_STANDARD)"; \
export USE_FOLLY="$(USE_FOLLY)"; \
export USE_FOLLY_LITE="$(USE_FOLLY_LITE)"; \
"$(CURDIR)/build_tools/build_detect_platform" "$(CURDIR)/make_config.mk"))
# this file is generated by the previous line to set build flags and sources
"$(CURDIR)/build_tools/build_detect_platform" "$(CURDIR)/make_config.mk" $(if $(ROCKSDB_RECURSIVE_MAKE_SAME_CONFIG),2>/dev/null)))
endif
# this file (generated above, or reused when skipped) sets build flags and sources
include make_config.mk
# Figure out optimize level.
@@ -181,7 +191,7 @@ ifeq ($(filter -DROCKSDB_ASSERT_STATUS_CHECKED,$(OPT)),)
endif
endif
$(warning Warning: Compiling in debug mode. Don't use the resulting binary in production)
$(if $(ROCKSDB_RECURSIVE_MAKE_SAME_CONFIG),,$(warning Warning: Compiling in debug mode. Don't use the resulting binary in production))
endif
# `USE_LTO=1` enables link-time optimizations. Among other things, this enables
@@ -579,9 +589,12 @@ TESTS += $(PLUGIN_TESTS)
ifneq ($(filter check-headers, $(MAKECMDGOALS)),)
# TODO: add/support JNI headers
DEV_HEADER_DIRS := $(sort include/ $(dir $(ALL_SOURCES)))
FOLLY_DEPENDENT_HEADERS := \
include/rocksdb/coro_db.h \
include/rocksdb/utilities/coro_stackable_db.h
# Some headers like in port/ are platform-specific
DEV_HEADERS_TO_CHECK := $(shell $(FIND) $(DEV_HEADER_DIRS) -type f -name '*.h' | grep -E -v 'port/|plugin/|range_tree/|secondary_index/')
PUBLIC_HEADERS_TO_CHECK := $(shell $(FIND) include/ -type f -name '*.h')
DEV_HEADERS_TO_CHECK := $(filter-out $(FOLLY_DEPENDENT_HEADERS), $(shell $(FIND) $(DEV_HEADER_DIRS) -type f -name '*.h' | grep -E -v 'port/|plugin/|range_tree/|secondary_index/'))
PUBLIC_HEADERS_TO_CHECK := $(filter-out $(FOLLY_DEPENDENT_HEADERS), $(shell $(FIND) include/ -type f -name '*.h'))
else
DEV_HEADERS_TO_CHECK :=
PUBLIC_HEADERS_TO_CHECK :=
@@ -949,6 +962,57 @@ gen_parallel_tests:
$(AM_V_at)$(FIND) t -type f -name 'run-*' -exec rm -f {} \;
$(MAKE) $(parallel_tests)
# Auxiliary (non-unit-test) checks that are part of `make check`. Kept as a
# single list so the same set can run as parallel jobs alongside the C++ unit
# tests (see gen_check_scripts + check_0) and serially in the J=1 fallback.
CHECK_EXTRA_TARGETS =
ifneq ($(PLATFORM), OS_AIX)
CHECK_EXTRA_TARGETS += check-all-python
ifndef ASSERT_STATUS_CHECKED # not yet working with these tests
CHECK_EXTRA_TARGETS += ldb_tests db_crashtest_tests rocksdb_dump_test
endif
endif
ifndef SKIP_FORMAT_BUCK_CHECKS
CHECK_EXTRA_TARGETS += check-format check-buck-targets check-sources check-workflow-yaml check-c-api-gen
endif
# Generate a t/run-<target> wrapper per enabled auxiliary check; check_0's
# `$(FIND) t -name 'run-*'` then runs them as gnu_parallel jobs alongside the
# unit tests, each framed with a gtest-style pass/fail line. Under CI sharding,
# round-robin across shards so each check runs on exactly one shard. Kept
# separate from gen_parallel_tests (which cleans t/run-* first) so the valgrind_*
# targets, which also glob t/run-*, don't pick these up.
.PHONY: gen_check_scripts
gen_check_scripts:
$(AM_V_at)mkdir -p t; \
idx=0; \
for tgt in $(CHECK_EXTRA_TARGETS); do \
if [ -z "$(CI_TOTAL_SHARDS)" ] || [ $$(( idx % $(CI_TOTAL_SHARDS) )) -eq $(CI_SHARD_INDEX) ]; then \
s=t/run-$$tgt; \
printf '%s\n' \
'#!/bin/sh' \
'# Generated by `make gen_check_scripts`.' \
"t=$$tgt" \
'# Drop the parent jobserver from MAKEFLAGS (its fds are lost through' \
'# gnu_parallel, causing a spurious warning); keep overrides after "--".' \
'MAKEFLAGS=$$(printf "%s" "$$MAKEFLAGS" | sed -e "s/ --jobserver-auth=[^ ]*//g" -e "s/ --jobserver-fds=[^ ]*//g" -e "s/ -j[0-9]*//g")' \
'export MAKEFLAGS' \
'echo; echo "[----------]"; echo "[ RUN ] >>> make $$t <<<"' \
'$(MAKE) --no-print-directory ROCKSDB_RECURSIVE_MAKE_SAME_CONFIG=1 "$$t"' \
'rc=$$?' \
'if [ "$$rc" = 0 ]; then' \
' echo "[ OK ] >>> make $$t <<<"' \
'else' \
' echo "[ FAILED ] >>> make $$t <<<"' \
'fi' \
'echo "[----------]"' \
'exit $$rc' \
> $$s; \
chmod a=rx $$s; \
fi; \
idx=$$((idx + 1)); \
done
# Reorder input lines (one per test or per shard) so the longest-running
# tests/shards start first under "make check" parallel scheduling. The trick
# is to prefix matched names with a fake duration (100), sort numerically
@@ -976,7 +1040,8 @@ gen_parallel_tests:
slow_test_regexp = \
^.*point_lock_manager_stress_test.*$$|^.*db_test.*$$|^.*external_sst_file_test.*$$|^.*compaction_service_test.*$$|^.*corruption_test.*$$|^.*comparator_db_test.*$$|^.*external_sst_file_basic_test.*$$|^.*rate_limiter_test.*$$|^.*db_compaction_test.*$$|^.*write_prepared_transaction_test.*$$|^.*db_merge_operator_test.*$$|\
^.*db_dynamic_level_test.*$$|^.*db_bloom_filter_test.*$$|^.*error_handler_fs_test.*$$|^.*merge_helper_test.*$$|^.*transaction_test.*$$|^.*db_kv_checksum_test.*$$|^.*inlineskiplist_test.*$$|\
^.*db_with_timestamp_basic_test.*$$|^.*table_test.*$$|^.*db_wal_test.*$$|^.*block_based_table_reader_test.*$$|^.*block_test.*$$
^.*db_with_timestamp_basic_test.*$$|^.*table_test.*$$|^.*db_wal_test.*$$|^.*block_based_table_reader_test.*$$|^.*block_test.*$$|\
^.*run-check-c-api-gen.*$$|^.*run-check-format.*$$
prioritize_long_running_tests = \
perl -pe 's,($(slow_test_regexp)),100 $$1,' \
| sort -k1,1gr \
@@ -1108,27 +1173,17 @@ check: all
&& (build_tools/gnu_parallel --gnu --help 2>/dev/null) | \
grep -q 'GNU Parallel'; \
then \
$(MAKE) gen_check_scripts; \
$(MAKE) T="$$t" check_0; \
else \
for t in $(TESTS); do \
echo "===== Running $$t (`date`)"; ./$$t || exit 1; done; \
for t in $(CHECK_EXTRA_TARGETS); do \
echo "===== Running $$t (`date`)"; $(MAKE) $$t || exit 1; done; \
fi
rm -rf $(TEST_TMPDIR)
ifneq ($(PLATFORM), OS_AIX)
$(PYTHON) tools/check_all_python.py
ifndef ASSERT_STATUS_CHECKED # not yet working with these tests
$(PYTHON) tools/ldb_test.py
$(PYTHON) tools/db_crashtest_test.py
sh tools/rocksdb_dump_test.sh
endif
endif
ifndef SKIP_FORMAT_BUCK_CHECKS
$(MAKE) check-format
$(MAKE) check-buck-targets
$(MAKE) check-sources
$(MAKE) check-workflow-yaml
$(MAKE) check-c-api-gen
endif
# NOTE: ^^^^^^^^ don't add new auxiliary checks here, but instead to
# CHECK_EXTRA_TARGETS
# Check that the auto-generated C API files are up to date. It regenerates the
# fragments and the inlined c.h/c.cc and compares them against a snapshot of the
@@ -1149,8 +1204,8 @@ endif
# Any non-empty value other than 0/no/false enables strict mode.
CLANG_FORMAT_BINARY ?=
# Backward-compatibility baseline for the C API (signature-level) check. CI
# overrides this with the PR's merge target; locally it falls back to main /
# origin/main and is skipped if neither resolves.
# overrides this with the PR's merge target or the branch's pre-push tip;
# locally it falls back to main or origin/main and is skipped if neither resolves.
API_COMPAT_REF ?= main
CHECK_C_API_GEN_STRICT ?=
check-c-api-gen:
@@ -1235,6 +1290,14 @@ ldb_tests: ldb
db_crashtest_tests:
$(PYTHON) tools/db_crashtest_test.py
.PHONY: check-all-python
check-all-python:
$(PYTHON) tools/check_all_python.py
.PHONY: rocksdb_dump_test
rocksdb_dump_test: rocksdb_dump rocksdb_undump
sh tools/rocksdb_dump_test.sh
include crash_test.mk
asan_check: clean
@@ -1670,6 +1733,9 @@ db_readonly_with_timestamp_test: $(OBJ_DIR)/db/db_readonly_with_timestamp_test.o
db_wide_basic_test: $(OBJ_DIR)/db/wide/db_wide_basic_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
db_lazy_entity_test: $(OBJ_DIR)/db/wide/db_lazy_entity_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
db_wide_blob_direct_write_test: $(OBJ_DIR)/db/wide/db_wide_blob_direct_write_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
@@ -2374,22 +2440,27 @@ ROCKSDB_JAVADOCS_JAR = rocksdbjni-$(ROCKSDB_JAVA_VERSION)-javadoc.jar
ROCKSDB_SOURCES_JAR = rocksdbjni-$(ROCKSDB_JAVA_VERSION)-sources.jar
SHA256_CMD = sha256sum
# The *_DOWNLOAD_PREFIX defaults below point at the upstream projects; CI
# overrides them to a mirror on the "third-party-deps" GitHub release (see
# .github/actions/pre-steps/action.yml). When bumping a version, see
# instructions in CI config comments for how to update that release.
ZLIB_VER ?= 1.3.1
ZLIB_SHA256 ?= 9a93b2b7dfdac77ceba5a558a580e74667dd6fede4585b91eefb60f03b72df23
ZLIB_DOWNLOAD_BASE ?= http://zlib.net
ZLIB_DOWNLOAD_PREFIX ?= http://zlib.net/zlib-
BZIP2_VER ?= 1.0.8
BZIP2_SHA256 ?= ab5a03176ee106d3f0fa90e381da478ddae405918153cca248e682cd0c4a2269
BZIP2_DOWNLOAD_BASE ?= http://sourceware.org/pub/bzip2
BZIP2_DOWNLOAD_PREFIX ?= http://sourceware.org/pub/bzip2/bzip2-
SNAPPY_VER ?= 1.2.2
SNAPPY_SHA256 ?= 90f74bc1fbf78a6c56b3c4a082a05103b3a56bb17bca1a27e052ea11723292dc
SNAPPY_DOWNLOAD_BASE ?= https://github.com/google/snappy/archive
SNAPPY_DOWNLOAD_PREFIX ?= https://github.com/google/snappy/archive/
LZ4_VER ?= 1.10.0
LZ4_SHA256 ?= 537512904744b35e232912055ccf8ec66d768639ff3abe5788d90d792ec5f48b
LZ4_DOWNLOAD_BASE ?= https://github.com/lz4/lz4/archive
LZ4_DOWNLOAD_PREFIX ?= https://github.com/lz4/lz4/archive/v
ZSTD_VER ?= 1.5.7
ZSTD_SHA256 ?= 37d7284556b20954e56e1ca85b80226768902e2edabd3b649e9e72c0c9012ee3
ZSTD_DOWNLOAD_BASE ?= https://github.com/facebook/zstd/archive
CURL_SSL_OPTS ?= --tlsv1
ZSTD_DOWNLOAD_PREFIX ?= https://github.com/facebook/zstd/archive/v
# curl with the common options used to fetch the pinned third-party sources above
CURL_DOWNLOAD = curl --fail --location --retry 3 --retry-all-errors
ifeq ($(PLATFORM), OS_MACOSX)
ifeq (,$(findstring librocksdbjni-osx,$(ROCKSDBJNILIB)))
@@ -2468,7 +2539,7 @@ BUILD_SIG_NONBUILD_GOALS := \
check-sources check-workflow-yaml check-progress clang-tidy \
tags tags0 package jclean checkout_folly build_folly \
watch-log dump-log suggest-slow-tests list_all_tests gen-pc \
gen_parallel_tests check-c-api-gen \
gen_parallel_tests gen_check_scripts check-c-api-gen check-all-python \
setup-hooks install-hooks uninstall-hooks uninstall db_crashtest_tests
# Goals that clean before building (depend on or invoke `clean`): they manage
# their own freshness, so the check must not block them (it would error before
@@ -2530,7 +2601,7 @@ endif
endif
zlib-$(ZLIB_VER).tar.gz:
curl --fail --output zlib-$(ZLIB_VER).tar.gz --location ${ZLIB_DOWNLOAD_BASE}/zlib-$(ZLIB_VER).tar.gz
$(CURL_DOWNLOAD) --output zlib-$(ZLIB_VER).tar.gz ${ZLIB_DOWNLOAD_PREFIX}$(ZLIB_VER).tar.gz
ZLIB_SHA256_ACTUAL=`$(SHA256_CMD) zlib-$(ZLIB_VER).tar.gz | cut -d ' ' -f 1`; \
if [ "$(ZLIB_SHA256)" != "$$ZLIB_SHA256_ACTUAL" ]; then \
echo zlib-$(ZLIB_VER).tar.gz checksum mismatch, expected=\"$(ZLIB_SHA256)\" actual=\"$$ZLIB_SHA256_ACTUAL\"; \
@@ -2548,7 +2619,7 @@ libz.a: zlib-$(ZLIB_VER).tar.gz
cp zlib-$(ZLIB_VER)/libz.a .
bzip2-$(BZIP2_VER).tar.gz:
curl --fail --output bzip2-$(BZIP2_VER).tar.gz --location ${CURL_SSL_OPTS} ${BZIP2_DOWNLOAD_BASE}/bzip2-$(BZIP2_VER).tar.gz
$(CURL_DOWNLOAD) --output bzip2-$(BZIP2_VER).tar.gz ${BZIP2_DOWNLOAD_PREFIX}$(BZIP2_VER).tar.gz
BZIP2_SHA256_ACTUAL=`$(SHA256_CMD) bzip2-$(BZIP2_VER).tar.gz | cut -d ' ' -f 1`; \
if [ "$(BZIP2_SHA256)" != "$$BZIP2_SHA256_ACTUAL" ]; then \
echo bzip2-$(BZIP2_VER).tar.gz checksum mismatch, expected=\"$(BZIP2_SHA256)\" actual=\"$$BZIP2_SHA256_ACTUAL\"; \
@@ -2562,7 +2633,7 @@ libbz2.a: bzip2-$(BZIP2_VER).tar.gz
cp bzip2-$(BZIP2_VER)/libbz2.a .
snappy-$(SNAPPY_VER).tar.gz:
curl --fail --output snappy-$(SNAPPY_VER).tar.gz --location ${CURL_SSL_OPTS} ${SNAPPY_DOWNLOAD_BASE}/$(SNAPPY_VER).tar.gz
$(CURL_DOWNLOAD) --output snappy-$(SNAPPY_VER).tar.gz ${SNAPPY_DOWNLOAD_PREFIX}$(SNAPPY_VER).tar.gz
SNAPPY_SHA256_ACTUAL=`$(SHA256_CMD) snappy-$(SNAPPY_VER).tar.gz | cut -d ' ' -f 1`; \
if [ "$(SNAPPY_SHA256)" != "$$SNAPPY_SHA256_ACTUAL" ]; then \
echo snappy-$(SNAPPY_VER).tar.gz checksum mismatch, expected=\"$(SNAPPY_SHA256)\" actual=\"$$SNAPPY_SHA256_ACTUAL\"; \
@@ -2577,7 +2648,7 @@ libsnappy.a: snappy-$(SNAPPY_VER).tar.gz
cp snappy-$(SNAPPY_VER)/build/libsnappy.a .
lz4-$(LZ4_VER).tar.gz:
curl --fail --output lz4-$(LZ4_VER).tar.gz --location ${CURL_SSL_OPTS} ${LZ4_DOWNLOAD_BASE}/v$(LZ4_VER).tar.gz
$(CURL_DOWNLOAD) --output lz4-$(LZ4_VER).tar.gz ${LZ4_DOWNLOAD_PREFIX}$(LZ4_VER).tar.gz
LZ4_SHA256_ACTUAL=`$(SHA256_CMD) lz4-$(LZ4_VER).tar.gz | cut -d ' ' -f 1`; \
if [ "$(LZ4_SHA256)" != "$$LZ4_SHA256_ACTUAL" ]; then \
echo lz4-$(LZ4_VER).tar.gz checksum mismatch, expected=\"$(LZ4_SHA256)\" actual=\"$$LZ4_SHA256_ACTUAL\"; \
@@ -2591,7 +2662,7 @@ liblz4.a: lz4-$(LZ4_VER).tar.gz
cp lz4-$(LZ4_VER)/lib/liblz4.a .
zstd-$(ZSTD_VER).tar.gz:
curl --fail --output zstd-$(ZSTD_VER).tar.gz --location ${CURL_SSL_OPTS} ${ZSTD_DOWNLOAD_BASE}/v$(ZSTD_VER).tar.gz
$(CURL_DOWNLOAD) --output zstd-$(ZSTD_VER).tar.gz ${ZSTD_DOWNLOAD_PREFIX}$(ZSTD_VER).tar.gz
ZSTD_SHA256_ACTUAL=`$(SHA256_CMD) zstd-$(ZSTD_VER).tar.gz | cut -d ' ' -f 1`; \
if [ "$(ZSTD_SHA256)" != "$$ZSTD_SHA256_ACTUAL" ]; then \
echo zstd-$(ZSTD_VER).tar.gz checksum mismatch, expected=\"$(ZSTD_SHA256)\" actual=\"$$ZSTD_SHA256_ACTUAL\"; \
@@ -141,6 +141,15 @@ int rocksdb_options_get_max_file_opening_threads(rocksdb_options_t* opt) {
return opt->rep.max_file_opening_threads;
}
void rocksdb_options_set_read_io_executor_threads(rocksdb_options_t* opt,
int v) {
opt->rep.read_io_executor_threads = v;
}
int rocksdb_options_get_read_io_executor_threads(rocksdb_options_t* opt) {
return opt->rep.read_io_executor_threads;
}
void rocksdb_options_set_open_files_async(rocksdb_options_t* opt,
unsigned char v) {
opt->rep.open_files_async = v;
@@ -807,6 +816,16 @@ const char* rocksdb_options_get_db_host_id(rocksdb_options_t* opt,
return opt->rep.db_host_id.data();
}
void rocksdb_options_set_remote_compaction_manifest_floor(
rocksdb_options_t* opt, unsigned char v) {
opt->rep.remote_compaction_manifest_floor = v;
}
unsigned char rocksdb_options_get_remote_compaction_manifest_floor(
rocksdb_options_t* opt) {
return opt->rep.remote_compaction_manifest_floor;
}
void rocksdb_options_set_lowest_used_cache_tier(rocksdb_options_t* opt, int v) {
opt->rep.lowest_used_cache_tier =
static_cast<decltype(opt->rep.lowest_used_cache_tier)>(v);
@@ -847,6 +866,16 @@ uint64_t rocksdb_options_get_max_compaction_trigger_wakeup_seconds(
return opt->rep.max_compaction_trigger_wakeup_seconds;
}
void rocksdb_options_set_periodic_compaction_phase_recovery_percent(
rocksdb_options_t* opt, int v) {
opt->rep.periodic_compaction_phase_recovery_percent = v;
}
int rocksdb_options_get_periodic_compaction_phase_recovery_percent(
rocksdb_options_t* opt) {
return opt->rep.periodic_compaction_phase_recovery_percent;
}
void rocksdb_options_set_follower_refresh_catchup_period_ms(
rocksdb_options_t* opt, uint64_t v) {
opt->rep.follower_refresh_catchup_period_ms = v;
@@ -1296,6 +1325,16 @@ uint64_t rocksdb_options_get_blob_file_size(rocksdb_options_t* opt) {
return opt->rep.blob_file_size;
}
void rocksdb_options_set_blob_file_writable_file_max_buffer_size(
rocksdb_options_t* opt, uint64_t v) {
opt->rep.blob_file_writable_file_max_buffer_size = v;
}
uint64_t rocksdb_options_get_blob_file_writable_file_max_buffer_size(
rocksdb_options_t* opt) {
return opt->rep.blob_file_writable_file_max_buffer_size;
}
void rocksdb_options_set_blob_compression_type(rocksdb_options_t* opt, int v) {
opt->rep.blob_compression_type =
static_cast<decltype(opt->rep.blob_compression_type)>(v);
@@ -2495,6 +2534,17 @@ double rocksdb_block_based_options_get_uniform_cv_threshold(
return opt->rep.uniform_cv_threshold;
}
void rocksdb_block_based_options_set_optimize_key_common_prefix(
rocksdb_block_based_table_options_t* opt, int v) {
opt->rep.optimize_key_common_prefix =
static_cast<decltype(opt->rep.optimize_key_common_prefix)>(v);
}
int rocksdb_block_based_options_get_optimize_key_common_prefix(
rocksdb_block_based_table_options_t* opt) {
return static_cast<int>(opt->rep.optimize_key_common_prefix);
}
void rocksdb_block_based_options_set_enable_index_compression(
rocksdb_block_based_table_options_t* opt, unsigned char v) {
opt->rep.enable_index_compression = v;
@@ -55,6 +55,12 @@ rocksdb_options_set_verify_sst_unique_id_in_manifest(rocksdb_options_t* opt,
extern ROCKSDB_LIBRARY_API unsigned char
rocksdb_options_get_verify_sst_unique_id_in_manifest(rocksdb_options_t* opt);
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_read_io_executor_threads(
rocksdb_options_t* opt, int v);
extern ROCKSDB_LIBRARY_API int rocksdb_options_get_read_io_executor_threads(
rocksdb_options_t* opt);
extern ROCKSDB_LIBRARY_API const char* rocksdb_options_get_db_log_dir(
rocksdb_options_t* opt, size_t* size);
@@ -241,6 +247,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_remote_compaction_manifest_floor(rocksdb_options_t* opt,
unsigned char v);
extern ROCKSDB_LIBRARY_API unsigned char
rocksdb_options_get_remote_compaction_manifest_floor(rocksdb_options_t* opt);
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_lowest_used_cache_tier(
rocksdb_options_t* opt, int v);
@@ -261,6 +274,14 @@ extern ROCKSDB_LIBRARY_API const char*
rocksdb_options_get_daily_offpeak_time_utc(rocksdb_options_t* opt,
size_t* size);
extern ROCKSDB_LIBRARY_API void
rocksdb_options_set_periodic_compaction_phase_recovery_percent(
rocksdb_options_t* opt, int v);
extern ROCKSDB_LIBRARY_API int
rocksdb_options_get_periodic_compaction_phase_recovery_percent(
rocksdb_options_t* opt);
extern ROCKSDB_LIBRARY_API void
rocksdb_options_set_follower_refresh_catchup_period_ms(rocksdb_options_t* opt,
uint64_t v);
@@ -381,6 +402,14 @@ rocksdb_options_set_preserve_internal_time_seconds(rocksdb_options_t* opt,
extern ROCKSDB_LIBRARY_API uint64_t
rocksdb_options_get_preserve_internal_time_seconds(rocksdb_options_t* opt);
extern ROCKSDB_LIBRARY_API void
rocksdb_options_set_blob_file_writable_file_max_buffer_size(
rocksdb_options_t* opt, uint64_t v);
extern ROCKSDB_LIBRARY_API uint64_t
rocksdb_options_get_blob_file_writable_file_max_buffer_size(
rocksdb_options_t* opt);
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_enable_blob_direct_write(
rocksdb_options_t* opt, unsigned char v);
@@ -1156,6 +1185,14 @@ extern ROCKSDB_LIBRARY_API double
rocksdb_block_based_options_get_uniform_cv_threshold(
rocksdb_block_based_table_options_t* opt);
extern ROCKSDB_LIBRARY_API void
rocksdb_block_based_options_set_optimize_key_common_prefix(
rocksdb_block_based_table_options_t* opt, int v);
extern ROCKSDB_LIBRARY_API int
rocksdb_block_based_options_get_optimize_key_common_prefix(
rocksdb_block_based_table_options_t* opt);
extern ROCKSDB_LIBRARY_API void
rocksdb_block_based_options_set_enable_index_compression(
rocksdb_block_based_table_options_t* opt, unsigned char v);
@@ -151,6 +151,12 @@ static void rocksdb_roundtrip_test_rocksdb_block_based_options(void) {
rocksdb_block_based_options_set_optimize_filters_for_memory(obj, 0);
CheckCondition(
rocksdb_block_based_options_get_optimize_filters_for_memory(obj) == 0);
rocksdb_block_based_options_set_optimize_key_common_prefix(obj, 1);
CheckCondition(
rocksdb_block_based_options_get_optimize_key_common_prefix(obj) == 1);
rocksdb_block_based_options_set_optimize_key_common_prefix(obj, 0);
CheckCondition(
rocksdb_block_based_options_get_optimize_key_common_prefix(obj) == 0);
rocksdb_block_based_options_set_partition_filters(obj, 1);
CheckCondition(rocksdb_block_based_options_get_partition_filters(obj) == 1);
rocksdb_block_based_options_set_partition_filters(obj, 0);
@@ -647,6 +653,12 @@ static void rocksdb_roundtrip_test_rocksdb_options(void) {
CheckCondition(rocksdb_options_get_blob_direct_write_partitions(obj) == 1);
rocksdb_options_set_blob_direct_write_partitions(obj, 0);
CheckCondition(rocksdb_options_get_blob_direct_write_partitions(obj) == 0);
rocksdb_options_set_blob_file_writable_file_max_buffer_size(obj, 1);
CheckCondition(
rocksdb_options_get_blob_file_writable_file_max_buffer_size(obj) == 1);
rocksdb_options_set_blob_file_writable_file_max_buffer_size(obj, 0);
CheckCondition(
rocksdb_options_get_blob_file_writable_file_max_buffer_size(obj) == 0);
rocksdb_options_set_block_protection_bytes_per_key(obj, 1);
CheckCondition(rocksdb_options_get_block_protection_bytes_per_key(obj) == 1);
rocksdb_options_set_block_protection_bytes_per_key(obj, 0);
@@ -827,6 +839,12 @@ static void rocksdb_roundtrip_test_rocksdb_options(void) {
CheckCondition(rocksdb_options_get_paranoid_memory_checks(obj) == 1);
rocksdb_options_set_paranoid_memory_checks(obj, 0);
CheckCondition(rocksdb_options_get_paranoid_memory_checks(obj) == 0);
rocksdb_options_set_periodic_compaction_phase_recovery_percent(obj, 1);
CheckCondition(
rocksdb_options_get_periodic_compaction_phase_recovery_percent(obj) == 1);
rocksdb_options_set_periodic_compaction_phase_recovery_percent(obj, 0);
CheckCondition(
rocksdb_options_get_periodic_compaction_phase_recovery_percent(obj) == 0);
rocksdb_options_set_persist_stats_to_disk(obj, 1);
CheckCondition(rocksdb_options_get_persist_stats_to_disk(obj) == 1);
rocksdb_options_set_persist_stats_to_disk(obj, 0);
@@ -849,6 +867,16 @@ static void rocksdb_roundtrip_test_rocksdb_options(void) {
CheckCondition(rocksdb_options_get_preserve_internal_time_seconds(obj) == 1);
rocksdb_options_set_preserve_internal_time_seconds(obj, 0);
CheckCondition(rocksdb_options_get_preserve_internal_time_seconds(obj) == 0);
rocksdb_options_set_read_io_executor_threads(obj, 1);
CheckCondition(rocksdb_options_get_read_io_executor_threads(obj) == 1);
rocksdb_options_set_read_io_executor_threads(obj, 0);
CheckCondition(rocksdb_options_get_read_io_executor_threads(obj) == 0);
rocksdb_options_set_remote_compaction_manifest_floor(obj, 1);
CheckCondition(rocksdb_options_get_remote_compaction_manifest_floor(obj) ==
1);
rocksdb_options_set_remote_compaction_manifest_floor(obj, 0);
CheckCondition(rocksdb_options_get_remote_compaction_manifest_floor(obj) ==
0);
rocksdb_options_set_reuse_manifest_on_open(obj, 1);
CheckCondition(rocksdb_options_get_reuse_manifest_on_open(obj) == 1);
rocksdb_options_set_reuse_manifest_on_open(obj, 0);
+31 -13
View File
@@ -4,12 +4,19 @@
Thorough multi-agent code review for RocksDB commits following CLAUDE.md guidelines,
with structured codebase exploration, inter-agent debate, and verification.
IMPORTANT — Incremental output: After completing EACH major phase (Setup,
Codebase Context, Initial Review, Debate, Synthesis), append your findings
to review-findings.md using the Write tool. This ensures partial results are
saved even if the review is interrupted by the turn limit. After the final
synthesis, write the complete review to review-findings.md, replacing
previous content. Also output the final review as your response text.
IMPORTANT — Where the review goes: Your FINAL RESPONSE TEXT is the review.
The PR comment is generated verbatim from your final response, so the
complete, polished review MUST appear there in full. Do NOT end your turn with
a meta-summary such as "the review has been written to review-findings.md" —
paste the entire review as your response.
review-findings.md is a RECOVERY BACKSTOP, not the primary output: after
completing EACH major phase (Setup, Codebase Context, Initial Review, Debate,
Synthesis), append your current findings to it with the Write tool, and after
the final synthesis overwrite it with the complete review. This preserves
partial results if the turn limit interrupts the run (a recovery step reads
this file); it does NOT substitute for outputting the full review as your
final response.
## Workflow Steps
@@ -466,7 +473,9 @@ Team lead synthesizes the debate into a consensus document:
- Include detailed bug analysis (root cause, vulnerable code paths)
- Include suggested fixes
- Note which findings were debated and the outcome
- Write the complete review to review-findings.md and output as response
- Output the complete review as your final response text (this becomes the PR
comment verbatim), and also overwrite review-findings.md with the identical
content as a recovery backstop
**Final report quality rules:**
- The final report must be CLEAN and POLISHED. No stream-of-consciousness,
@@ -483,9 +492,10 @@ Team lead synthesizes the debate into a consensus document:
**REQUIRED output structure (so the PR page stays scrollable):**
The final response (and contents of `review-findings.md`) MUST follow this
exact structure. The summary appears first so reviewers can see HIGH findings
at a glance; everything else is hidden behind a `<details>` block.
Your final response — which becomes the PR comment verbatim — (and the
identical contents of `review-findings.md`) MUST follow this exact structure.
The summary appears first so reviewers can see HIGH findings at a glance;
everything else is hidden behind a `<details>` block.
```markdown
## Summary
@@ -533,6 +543,9 @@ Rules for this structure:
- Do NOT nest a `<details>` inside another `<details>`.
- Do NOT add any text after the closing `</details>` — the comment-builder
appends its own footer.
- Your final response MUST be the review itself in this structure — never a
meta-summary like "the review has been written to review-findings.md". The
PR comment is generated verbatim from your final response.
- If the review is partial (recovery path), still produce the summary block
first, and put whatever was salvaged inside the `<details>` block.
@@ -571,9 +584,14 @@ Rules for this structure:
- [ ] All callers enumerated with parameter range table
## Output Structure
Write all review artifacts to the working directory root:
- `review-findings.md` — Incremental findings (appended after each phase),
then replaced with the final synthesized review at the end
Your **final response text is the primary output** — it becomes the PR comment
verbatim. The files below are working artifacts and recovery backstops only;
they are NOT a substitute for outputting the full review in your final response.
Write these artifacts to the working directory root:
- `review-findings.md` — Recovery backstop: incremental findings appended after
each phase, overwritten with the final synthesized review at the end. Used
only if the turn limit interrupts the run.
- `context.md` — Codebase context (call chains, invariants)
- `findings-*.md` — Per-agent findings (design, correctness, cross-component,
invariant-adversary, caller-audit, performance, api, serialization, tests)
-104
View File
@@ -1,104 +0,0 @@
# Code Review Skill
This document defines the methodology for performing thorough, multi-perspective
code reviews on RocksDB changes. It is used both by CI (GitHub Actions) and
local review workflows.
## Prerequisites
Before starting a review:
- Read `CLAUDE.md` in the repository root for project-specific guidelines
- Read any files in `claude_md/` for architecture context
- Identify the commit/diff/PR to review and parse the changed files
## Review Methodology
Conduct the review from multiple perspectives, then synthesize findings into a
single report. Each perspective catches different classes of bugs.
### Perspective 1: Codebase Context & Call-Chain Analysis
Before reviewing the diff itself, build deep context:
- For each changed function/method, trace the call chain UPWARD 3-5 levels.
Who consumes the changed behavior? What invariants do callers rely on?
- Trace DOWNWARD into callees. What side effects do they have? What shared
state do they mutate (counters, sequence numbers, flags, metadata)?
- Identify the "sibling" or "reference" implementation that handles the same
scenario the standard way. Compare behaviors.
- For data written to shared structures (memtable, SST block, cache entry),
find ALL readers and verify their visibility rules match the writer.
### Perspective 2: Correctness & Edge Cases
- Thread safety and concurrency (lock ordering, data races)
- Error handling and Status propagation
- Edge cases: empty inputs, overflow, boundary conditions
- Data corruption scenarios
- Behavioral contract changes: do return value semantics match callers?
### Perspective 3: Cross-Component & Adversarial Analysis
Check the change in ALL execution contexts:
| Context | Key difference |
|---------|---------------|
| WritePreparedTxnDB | read_callback_ controls visibility |
| ReadOnly DB / SecondaryInstance | No mutable memtable |
| CompactionService / Remote compaction | Different process |
| User-defined timestamps | Extra key comparison dimension |
| MemPurge | Memtable-to-memtable path |
| BlobDB | Values in blob files, not inline |
| Old snapshots | Seqno far behind current |
| Concurrent writers | Lock-free vs locked paths |
| FIFO / Universal compaction | Different invariants |
| Prefix seek / total order seek | Different iterator behavior |
When the change claims a property ("logically redundant", "safe because X"),
systematically try to break it: list preconditions, construct counterexamples.
### Perspective 4: Performance
RocksDB is a high-performance storage engine.
- Memory allocation on hot paths
- Unnecessary copies (prefer move semantics, Slice, string_view)
- Cache efficiency and data locality
- Loop optimization, branch prediction (LIKELY/UNLIKELY)
### Perspective 5: API, Compatibility & Testing
- Public API backwards compatibility
- Serialization format correctness and versioning
- Test coverage for edge cases, failure modes, and system-level interactions
## How to Explore the Codebase
Use available tools to explore deeply — do NOT just review the diff in isolation.
The most critical bugs hide at component boundaries.
- Read the header files for changed implementations
- Search for callers of changed functions (3-5 levels up)
- Read existing tests to understand guaranteed behaviors
- Check related implementations for conventions and patterns
## Output Format
### Summary
Brief overall assessment (1-2 sentences).
### Issues Found
Categorize by severity:
- :red_circle: **Critical**: Must fix (correctness, data corruption, security)
- :yellow_circle: **Suggestion**: Should consider (performance, edge cases)
- :green_circle: **Nitpick**: Minor style issues
For each issue include:
- File and line reference
- Which review perspective found it
- Description with root cause analysis
- Suggested fix
### Cross-Component Analysis
Execution context checks and assumption stress-test results.
### Positive Observations
Good patterns, clever optimizations, or improvements.
+67
View File
@@ -0,0 +1,67 @@
# Manual (Local) Code Review — Lightweight Wrapper
## What this is
A resource-light, **single-reviewer** variant of `claude_md/ci_review_prompt.md`
for running a review by hand in a local Claude Code / Devmate session. It reuses
the full CI review *methodology* but drops the multi-agent orchestration and the
CI-only file-persistence machinery, to approximate the heavyweight result at a
fraction of the cost.
**Scope: LOCAL manual review only. This file is NOT used by CI.** CI uses
`claude_md/ci_review_prompt.md`.
## How to use
Read `claude_md/ci_review_prompt.md` for the complete methodology — the analysis
techniques (caller-chain, callee side-effects, cross-component data consumers,
alternative execution contexts, assumption stress-testing), the per-perspective
reviewer instructions, the review checklist, and the anti-patterns table.
Then apply that methodology with the OVERRIDES below. **Where this file
conflicts with `ci_review_prompt.md`, this file wins.**
## Overrides (this is what makes it lightweight)
### 1. Single reviewer — no team, no subagents
- Do NOT create a team, spawn subagents, or run parallel/background agents.
- Do NOT run the round-robin Debate phase or the Consensus phase.
- Instead, YOU personally apply each reviewer role from `ci_review_prompt.md`
(Design, Correctness, Cross-Component & Adversarial, Invariant Adversary,
Caller-Context, Performance, API/Compat, Serialization, Test Coverage) as
sequential *lenses* in one pass. Spend a few focused minutes per lens. Skip
lenses that plainly don't apply (e.g., Serialization when there is no on-disk
or wire-format change) and note the skip in one line.
### 2. No intermediate files
- Do NOT write `context.md`, `findings-*.md`, `consensus.md`, or
`review-findings.md`. Keep working notes in your reasoning, not on disk.
- Rationale: those files exist only so CI can recover partial results when an
ephemeral runner hits the turn limit — a local session has neither problem.
`ci_review_prompt.md` already outputs the review as its final response, so
skipping the file loses nothing here.
### 3. Right-size the effort
- Do the Codebase Context phase, but proportionally: trace the 2-3 most
load-bearing call chains and the highest-risk cross-component data consumers,
rather than exhaustively enumerating every caller 3-5 levels up.
- Prioritize the highest-signal lenses for RocksDB: **Correctness**,
**Cross-Component & Adversarial**, and **Assumption Stress-Testing**. These
catch the bugs that matter most; give them the most attention.
- Escalation clause: if a thread looks dangerous, go deep on that specific
thread even though the overall pass is modest. Modest is the default, not a cap.
### 4. Identify the review target locally
- Unlike CI, the diff is NOT pre-pasted into this prompt. Determine what to
review from the user's request against the local worktree — e.g. the current
commit (`git show`), uncommitted changes (`git diff` / `git diff --staged`),
a named commit range, or a PR — and read the changed files plus their
surrounding context directly from the repo. When in doubt, include all changes
not yet upstream, as in
`git diff "$(git merge-base upstream/main HEAD 2> /dev/null || git merge-base origin/main HEAD 2> /dev/null)"`
plus any untracked files (`git status`) that seem relevant.
## Reminder
The goal is a thorough *single* reviewer, not a 9-agent debate: depth where it
matters, brevity everywhere else.
+4
View File
@@ -25,6 +25,7 @@ CRASHTEST_PY=$(PYTHON) -u tools/db_crashtest.py --stress_cmd=$(DB_STRESS_CMD) --
crash_test_with_multiops_wup_txn \
crash_test_with_optimistic_txn \
crash_test_with_tiered_storage \
liveness_crash_test \
blackbox_crash_test blackbox_crash_test_with_atomic_flush \
blackbox_crash_test_with_wc_txn blackbox_crash_test_with_wp_txn \
blackbox_crash_test_with_wup_txn \
@@ -94,6 +95,9 @@ crash_test_with_multiops_wp_txn: $(DB_STRESS_CMD)
crash_test_with_multiops_wup_txn: $(DB_STRESS_CMD)
$(CRASHTEST_MAKE) blackbox_crash_test_with_multiops_wup_txn
liveness_crash_test: $(DB_STRESS_CMD)
$(CRASHTEST_PY) liveness $(CRASH_TEST_EXT_ARGS)
blackbox_crash_test: $(DB_STRESS_CMD)
$(CRASHTEST_PY) --simple blackbox $(CRASH_TEST_EXT_ARGS)
$(CRASHTEST_PY) blackbox $(CRASH_TEST_EXT_ARGS)
+41
View File
@@ -5,7 +5,9 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <limits>
#include "rocksdb/rocksdb_namespace.h"
@@ -31,4 +33,43 @@ constexpr uint64_t kInvalidBlobFileNumber = 0;
constexpr uint64_t kCurrentFileBlobIndexFileNumber = kInvalidBlobFileNumber;
static_assert(kCurrentFileBlobIndexFileNumber == kInvalidBlobFileNumber);
// Sentinel range length meaning "the whole blob value" for the unified
// blob-read primitives, which take a range descriptor {range_offset,
// range_length}; range_length == kWholeBlobLength selects the whole value and
// any other value selects the strict sub-range
// [range_offset, range_offset + range_length).
//
// This is the internal counterpart of the public kLazyWholeColumn
// (include/rocksdb/lazy_wide_columns.h); both are the maximum size_t. The lazy
// resolver does not forward the public value as a range descriptor -- it
// normalizes a whole-column request to kWholeBlobLength -- so the two are not
// required to be equal, but both rely on the sentinel exceeding any real
// blob/column size so a "whole" request is never mistaken for a strict
// sub-range. Keep them in sync.
inline constexpr size_t kWholeBlobLength = std::numeric_limits<size_t>::max();
// How aggressively a blob read verifies the record's checksum. Derived once at
// the lazy read-path boundary (ReadPathBlobResolver) from
// (ReadOptions::verify_checksums, LazyColumnReadRequest::force_verify) and
// threaded down through the blob-read primitives, replacing the earlier mix of
// a verify_checksums bool and a separate force-verify method. (Not literally a
// constant, but kept here as a small shared blob-read enum rather than its own
// header.)
//
// - kVerifyIfPresent: verify the whole-record checksum whenever the format
// provides one, even if ReadOptions::verify_checksums is off. A byte-range
// request under this policy is escalated to a whole-record read (a strict
// sub-range cannot cover the checksum). This is the public force_verify.
// - kVerifyIfNoAmplification: verify when the checksummable unit is already
// being read -- a whole-value read verifies (the check is "free"), while a
// strict sub-range read skips verification rather than amplify the read to
// the whole record. Corresponds to verify_checksums on, force_verify off.
// - kSkip: never verify. Corresponds to verify_checksums off, force_verify
// off.
enum class BlobVerifyPolicy : uint8_t {
kVerifyIfPresent,
kVerifyIfNoAmplification,
kSkip,
};
} // namespace ROCKSDB_NAMESPACE
+47 -3
View File
@@ -44,6 +44,43 @@ Status VersionBlobFetcherBase::FetchBlob(const Slice& user_key,
prefetch_buffer, blob_value, bytes_read);
}
Status VersionBlobFetcherBase::FetchBlobRange(
const Slice& user_key, const BlobIndex& blob_index, uint64_t range_offset,
size_t range_length, BlobVerifyPolicy verify_policy,
PinnableSlice* blob_value, uint64_t* bytes_read) const {
if (range_length == kWholeBlobLength) {
// Whole-value read. Delegate to FetchBlob (which handles the direct-write
// fallback). For kVerifyIfPresent, force whole-record checksum verification
// even when ReadOptions::verify_checksums is off, via a transient fetcher
// over a verify-enabled copy of the read options (both locals outlive the
// borrowed-options VersionBlobFetcher).
constexpr FilePrefetchBuffer* prefetch_buffer = nullptr;
if (verify_policy == BlobVerifyPolicy::kVerifyIfPresent &&
!read_options().verify_checksums) {
ReadOptions verify_read_options = read_options();
verify_read_options.verify_checksums = true;
VersionBlobFetcher verifying_fetcher(version_, verify_read_options,
blob_file_cache_,
allow_write_path_fallback_);
return verifying_fetcher.FetchBlob(user_key, blob_index, prefetch_buffer,
blob_value, bytes_read);
}
return FetchBlob(user_key, blob_index, prefetch_buffer, blob_value,
bytes_read);
}
// Strict sub-range read: value bytes only, no whole-record checksum, no cache
// fill. Only ever chosen when SupportsRangeRead() holds (a Version to read
// through, no direct-write fallback) and verification is not forced (a strict
// sub-range cannot cover the whole-record checksum).
assert(SupportsRangeRead());
assert(version_);
assert(verify_policy != BlobVerifyPolicy::kVerifyIfPresent);
return version_->GetBlobRange(read_options(), user_key, blob_index,
range_offset, range_length, blob_value,
bytes_read);
}
Status EmbeddedAwareBlobFetcher::FetchBlob(const Slice& user_key,
const BlobIndex& blob_index,
FilePrefetchBuffer* prefetch_buffer,
@@ -55,9 +92,16 @@ Status EmbeddedAwareBlobFetcher::FetchBlob(const Slice& user_key,
// Same-file ("embedded") blob records live in the current SST and are read
// by the SameFileBlobReader (the BlockBasedTable). A separate blob-file
// prefetch buffer and bytes_read counter do not apply here (embedded reads
// account their own BLOB_DB_* stats via BlobSource).
return same_file_reader_->GetSameFileBlob(read_options(), blob_index,
blob_value);
// account their own BLOB_DB_* stats via BlobSource). This is a whole-value
// read; the verify policy follows read_options().verify_checksums (the
// decorator carries no separate force-verify signal).
const BlobVerifyPolicy verify_policy =
read_options().verify_checksums
? BlobVerifyPolicy::kVerifyIfNoAmplification
: BlobVerifyPolicy::kSkip;
return same_file_reader_->GetSameFileBlob(
read_options(), blob_index,
/*range_offset=*/0, kWholeBlobLength, verify_policy, blob_value);
}
if (base_ == nullptr) {
+28
View File
@@ -7,6 +7,7 @@
#include <utility>
#include "db/blob/blob_constants.h"
#include "rocksdb/options.h"
#include "rocksdb/status.h"
@@ -63,6 +64,26 @@ class VersionBlobFetcherBase : public BlobFetcher {
PinnableSlice* blob_value,
uint64_t* bytes_read) const override;
// Fetches the value of a separate-file blob reference into *blob_value,
// applying `verify_policy` (see BlobVerifyPolicy). range_length ==
// kWholeBlobLength reads the whole value (delegating to FetchBlob, which also
// handles the direct-write fallback, and forcing whole-record checksum
// verification for kVerifyIfPresent even when ReadOptions::verify_checksums
// is off). Any other length reads only the sub-range [range_offset,
// range_offset + range_length) directly from the blob file, reading just
// those bytes on a cache miss -- no record-header/key read, no whole-record
// checksum, no cache fill (see Version::GetBlobRange /
// BlobSource::GetBlobRange). A strict sub-range is only valid when
// SupportsRangeRead() is true and verify_policy != kVerifyIfPresent
// (verifying a strict sub-range would have to amplify the read to the whole
// record); callers check that first and take the whole-value path otherwise.
// Not routed through the same-file (embedded) decorator -- same-file
// references are resolved by the caller through a SameFileBlobReader.
Status FetchBlobRange(const Slice& user_key, const BlobIndex& blob_index,
uint64_t range_offset, size_t range_length,
BlobVerifyPolicy verify_policy,
PinnableSlice* blob_value, uint64_t* bytes_read) const;
// True if this fetcher has enough context to resolve a (non-inlined,
// non-same-file) blob reference: either a Version to read through, or the
// direct-write fallback (a blob file cache) enabled. Callers that may hold a
@@ -73,6 +94,13 @@ class VersionBlobFetcherBase : public BlobFetcher {
(allow_write_path_fallback_ && blob_file_cache_ != nullptr);
}
// True if this fetcher can serve an I/O-saving byte-range read: it reads
// through a Version (FetchBlobRange goes to Version::GetBlobRange) and is not
// in direct-write fallback mode (whose reads resolve whole records instead).
bool SupportsRangeRead() const {
return version_ != nullptr && !allow_write_path_fallback_;
}
protected:
VersionBlobFetcherBase(const Version* version, BlobFileCache* blob_file_cache,
bool allow_write_path_fallback)
+30 -19
View File
@@ -41,14 +41,14 @@ BlobFileBuilder::BlobFileBuilder(
const std::shared_ptr<IOTracer>& io_tracer,
BlobFileCompletionCallback* blob_callback,
BlobFileCreationReason creation_reason,
std::vector<std::string>* blob_file_paths,
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,
io_tracer, blob_callback, creation_reason,
blob_file_paths, blob_file_additions) {}
output_file_paths, blob_file_additions) {}
BlobFileBuilder::BlobFileBuilder(
std::function<uint64_t()> file_number_generator, FileSystem* fs,
@@ -60,13 +60,15 @@ BlobFileBuilder::BlobFileBuilder(
const std::shared_ptr<IOTracer>& io_tracer,
BlobFileCompletionCallback* blob_callback,
BlobFileCreationReason creation_reason,
std::vector<std::string>* blob_file_paths,
std::vector<std::string>* output_file_paths,
std::vector<BlobFileAddition>* blob_file_additions)
: file_number_generator_(std::move(file_number_generator)),
fs_(fs),
immutable_options_(immutable_options),
min_blob_size_(mutable_cf_options->min_blob_size),
blob_file_size_(mutable_cf_options->blob_file_size),
blob_file_writable_file_max_buffer_size_(
mutable_cf_options->blob_file_writable_file_max_buffer_size),
blob_compression_type_(mutable_cf_options->blob_compression_type),
// TODO with schema change: support custom compression manager and options
// such as max_compressed_bytes_per_kb
@@ -88,7 +90,7 @@ BlobFileBuilder::BlobFileBuilder(
io_tracer_(io_tracer),
blob_callback_(blob_callback),
creation_reason_(creation_reason),
blob_file_paths_(blob_file_paths),
output_file_paths_(output_file_paths),
blob_file_additions_(blob_file_additions),
blob_count_(0),
blob_bytes_(0) {
@@ -97,14 +99,21 @@ BlobFileBuilder::BlobFileBuilder(
assert(immutable_options_);
assert(file_options_);
assert(write_options_);
assert(blob_file_paths_);
assert(blob_file_paths_->empty());
assert(output_file_paths_);
assert(output_file_paths_->empty());
assert(blob_file_additions_);
assert(blob_file_additions_->empty());
}
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);
}
Status BlobFileBuilder::Add(const Slice& key, const Slice& value,
std::string* blob_index) {
assert(blob_index);
@@ -186,10 +195,7 @@ Status BlobFileBuilder::OpenBlobFileIfNeeded() {
assert(file_number_generator_);
const uint64_t blob_file_number = file_number_generator_();
assert(immutable_options_);
assert(!immutable_options_->cf_paths.empty());
std::string blob_file_path =
BlobFileName(immutable_options_->cf_paths.front().path, blob_file_number);
std::string blob_file_path = GetBlobFilePath(blob_file_number);
if (blob_callback_) {
blob_callback_->OnBlobFileCreationStarted(
@@ -204,6 +210,10 @@ Status BlobFileBuilder::OpenBlobFileIfNeeded() {
fo_copy.open_contract = FileOpenContract::kNoReopenForWrite |
FileOpenContract::kNoReadersWhileOpenForWrite;
fo_copy.write_hint = write_hint_;
if (blob_file_writable_file_max_buffer_size_ > 0) {
fo_copy.writable_file_max_buffer_size =
blob_file_writable_file_max_buffer_size_;
}
Status s = NewWritableFile(fs_, blob_file_path, &file, fo_copy);
TEST_SYNC_POINT_CALLBACK(
@@ -214,11 +224,11 @@ Status BlobFileBuilder::OpenBlobFileIfNeeded() {
}
}
// Note: files get added to blob_file_paths_ right after the open, so they
// can be cleaned up upon failure. Contrast this with blob_file_additions_,
// which only contains successfully written files.
assert(blob_file_paths_);
blob_file_paths_->emplace_back(std::move(blob_file_path));
// Note: blob files get added to output_file_paths_ right after the open, so
// they can be cleaned up upon failure. Contrast this with
// blob_file_additions_, which only contains successfully written files.
assert(output_file_paths_);
output_file_paths_->emplace_back(std::move(blob_file_path));
assert(file);
file->SetIOPriority(write_options_->rate_limiter_priority);
@@ -228,7 +238,7 @@ Status BlobFileBuilder::OpenBlobFileIfNeeded() {
FileTypeSet tmp_set = immutable_options_->checksum_handoff_file_types;
Statistics* const statistics = immutable_options_->stats;
std::unique_ptr<WritableFileWriter> file_writer(new WritableFileWriter(
std::move(file), blob_file_paths_->back(), *file_options_,
std::move(file), output_file_paths_->back(), fo_copy,
immutable_options_->clock, io_tracer_, statistics,
Histograms::BLOB_DB_BLOB_FILE_WRITE_MICROS, immutable_options_->listeners,
immutable_options_->file_checksum_gen_factory.get(),
@@ -341,7 +351,7 @@ Status BlobFileBuilder::CloseBlobFile() {
if (blob_callback_) {
s = blob_callback_->OnBlobFileCompleted(
blob_file_paths_->back(), column_family_name_, job_id_,
GetBlobFilePath(blob_file_number), column_family_name_, job_id_,
blob_file_number, creation_reason_, s, checksum_value, checksum_method,
blob_count_, blob_bytes_);
}
@@ -385,9 +395,10 @@ void BlobFileBuilder::Abandon(const Status& s) {
if (blob_callback_) {
// BlobFileBuilder::Abandon() is called because of error while writing to
// Blob files. So we can ignore the below error.
const uint64_t blob_file_number = writer_->get_log_number();
blob_callback_
->OnBlobFileCompleted(blob_file_paths_->back(), column_family_name_,
job_id_, writer_->get_log_number(),
->OnBlobFileCompleted(GetBlobFilePath(blob_file_number),
column_family_name_, job_id_, blob_file_number,
creation_reason_, s, "", "", blob_count_,
blob_bytes_)
.PermitUncheckedError();
+5 -3
View File
@@ -48,7 +48,7 @@ class BlobFileBuilder {
const std::shared_ptr<IOTracer>& io_tracer,
BlobFileCompletionCallback* blob_callback,
BlobFileCreationReason creation_reason,
std::vector<std::string>* blob_file_paths,
std::vector<std::string>* output_file_paths,
std::vector<BlobFileAddition>* blob_file_additions);
BlobFileBuilder(std::function<uint64_t()> file_number_generator,
@@ -63,7 +63,7 @@ class BlobFileBuilder {
const std::shared_ptr<IOTracer>& io_tracer,
BlobFileCompletionCallback* blob_callback,
BlobFileCreationReason creation_reason,
std::vector<std::string>* blob_file_paths,
std::vector<std::string>* output_file_paths,
std::vector<BlobFileAddition>* blob_file_additions);
BlobFileBuilder(const BlobFileBuilder&) = delete;
@@ -76,6 +76,7 @@ class BlobFileBuilder {
void Abandon(const Status& s);
private:
std::string GetBlobFilePath(uint64_t blob_file_number) const;
bool IsBlobFileOpen() const;
Status OpenBlobFileIfNeeded();
Status CompressBlobIfNeeded(Slice* blob,
@@ -93,6 +94,7 @@ class BlobFileBuilder {
const ImmutableOptions* immutable_options_;
uint64_t min_blob_size_;
uint64_t blob_file_size_;
uint64_t blob_file_writable_file_max_buffer_size_;
CompressionType blob_compression_type_;
std::unique_ptr<Compressor> blob_compressor_;
mutable Compressor::ManagedWorkingArea blob_compressor_wa_;
@@ -108,7 +110,7 @@ class BlobFileBuilder {
std::shared_ptr<IOTracer> io_tracer_;
BlobFileCompletionCallback* blob_callback_;
BlobFileCreationReason creation_reason_;
std::vector<std::string>* blob_file_paths_;
std::vector<std::string>* output_file_paths_;
std::vector<BlobFileAddition>* blob_file_additions_;
std::unique_ptr<BlobLogWriter> writer_;
uint64_t blob_count_;
+8 -1
View File
@@ -100,7 +100,8 @@ BlobFilePartitionManager::BlobFilePartitionManager(
FileNumberAllocator file_number_allocator, FileSystem* fs,
SystemClock* clock, Statistics* statistics, const FileOptions& file_options,
std::string db_path, std::string column_family_name,
uint64_t blob_file_size, bool use_fsync, BlobFileCache* blob_file_cache,
uint64_t blob_file_size, bool use_fsync,
uint64_t blob_writer_max_buffer_size, BlobFileCache* blob_file_cache,
BlobFileCompletionCallback* blob_callback,
const std::vector<std::shared_ptr<EventListener>>& listeners,
FileChecksumGenFactory* file_checksum_gen_factory,
@@ -120,6 +121,7 @@ BlobFilePartitionManager::BlobFilePartitionManager(
column_family_name_(std::move(column_family_name)),
blob_file_size_(blob_file_size),
use_fsync_(use_fsync),
blob_writer_max_buffer_size_(blob_writer_max_buffer_size),
blob_file_cache_(blob_file_cache),
blob_callback_(blob_callback),
listeners_(listeners),
@@ -202,6 +204,11 @@ Status BlobFilePartitionManager::OpenNewBlobFile(Partition* partition,
std::unique_ptr<FSWritableFile> file;
FileOptions writer_file_options = file_options_;
writer_file_options.open_contract = FileOpenContract::kNoReopenForWrite;
const uint64_t max_buffer_size =
blob_writer_max_buffer_size_.load(std::memory_order_relaxed);
if (max_buffer_size > 0) {
writer_file_options.writable_file_max_buffer_size = max_buffer_size;
}
Status s = NewWritableFile(fs_, blob_file_path, &file, writer_file_options);
if (!s.ok()) {
RemoveFilePartitionMapping(blob_file_number);
+9 -1
View File
@@ -4,6 +4,7 @@
// (found in the LICENSE.Apache file in the root directory).
#pragma once
#include <atomic>
#include <cstdint>
#include <deque>
#include <functional>
@@ -61,7 +62,8 @@ class BlobFilePartitionManager {
SystemClock* clock, Statistics* statistics,
const FileOptions& file_options, std::string db_path,
std::string column_family_name, uint64_t blob_file_size, bool use_fsync,
BlobFileCache* blob_file_cache, BlobFileCompletionCallback* blob_callback,
uint64_t blob_writer_max_buffer_size, BlobFileCache* blob_file_cache,
BlobFileCompletionCallback* blob_callback,
const std::vector<std::shared_ptr<EventListener>>& listeners,
FileChecksumGenFactory* file_checksum_gen_factory,
const FileTypeSet& checksum_handoff_file_types,
@@ -129,6 +131,11 @@ class BlobFilePartitionManager {
// protected by a live memtable / old SuperVersion.
bool IsTrackedBlobFileNumber(uint64_t file_number) const;
void SetBlobWriterMaxBufferSize(uint64_t blob_writer_max_buffer_size) {
blob_writer_max_buffer_size_.store(blob_writer_max_buffer_size,
std::memory_order_relaxed);
}
// Increments / decrements memtable-held protection on sealed blob files.
void ProtectSealedBlobFileNumbers(const std::vector<uint64_t>& file_numbers);
void UnprotectSealedBlobFileNumbers(
@@ -260,6 +267,7 @@ class BlobFilePartitionManager {
// File creation policy and shared blob-file infrastructure.
uint64_t blob_file_size_;
bool use_fsync_;
std::atomic<uint64_t> blob_writer_max_buffer_size_;
BlobFileCache* blob_file_cache_;
BlobFileCompletionCallback* blob_callback_;
std::vector<std::shared_ptr<EventListener>> listeners_;
+71
View File
@@ -416,6 +416,77 @@ Status BlobFileReader::GetBlob(
return Status::OK();
}
Status BlobFileReader::GetBlobRange(const ReadOptions& read_options,
const Slice& user_key, uint64_t offset,
uint64_t value_size, uint64_t range_offset,
size_t range_length,
MemoryAllocator* allocator,
std::unique_ptr<BlobContents>* result,
uint64_t* bytes_read) const {
assert(result);
// Partial reads are only meaningful for uncompressed blobs: a strict
// sub-range of a compressed record cannot be decompressed in isolation.
// Callers (BlobSource::GetBlobRange) must ensure this; enforce it here too.
if (compression_type_ != kNoCompression) {
return Status::Corruption("Cannot range-read a compressed blob");
}
const uint64_t key_size = user_key.size();
// Validate that the full value region is within the file (same check GetBlob
// performs); this guards the sub-range read below.
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_,
has_footer_)) {
return Status::Corruption("Invalid blob offset");
}
// The requested sub-range must lie within the value.
if (range_offset > value_size || range_length > value_size - range_offset) {
return Status::InvalidArgument("Blob range out of bounds");
}
// Read only the requested bytes, at the value's file position plus the range
// offset. Unlike GetBlob there is no record-header adjustment (we do not read
// the key/header) and no whole-record checksum verification (a strict
// sub-range cannot cover it -- callers that require verification take the
// full-read path instead).
const uint64_t read_offset = offset + range_offset;
const size_t read_size = range_length;
Slice range_slice;
Buffer buf;
AlignedBuffer direct_io_buffer;
TEST_SYNC_POINT("BlobFileReader::GetBlobRange:ReadFromFile");
PERF_COUNTER_ADD(blob_read_count, 1);
PERF_COUNTER_ADD(blob_read_byte, read_size);
PERF_TIMER_GUARD(blob_read_time);
{
const Status s =
ReadFromFile(file_reader_.get(), read_options, read_offset, read_size,
statistics_, &range_slice, &buf, &direct_io_buffer);
if (!s.ok()) {
return s;
}
}
TEST_SYNC_POINT_CALLBACK("BlobFileReader::GetBlobRange:TamperWithResult",
&range_slice);
// The blob is uncompressed, so the bytes read are the requested value bytes;
// copy them into an owned BlobContents (no decompression).
BlobContentsCreator::Create(result, /*out_charge=*/nullptr, range_slice,
kNoCompression, allocator);
if (bytes_read) {
*bytes_read = read_size;
}
return Status::OK();
}
void BlobFileReader::MultiGetBlob(
const ReadOptions& read_options, MemoryAllocator* allocator,
autovector<std::pair<BlobReadRequest*, std::unique_ptr<BlobContents>>>&
+17
View File
@@ -64,6 +64,23 @@ class BlobFileReader {
std::unique_ptr<BlobContents>* result,
uint64_t* bytes_read) const;
// Reads a byte sub-range [range_offset, range_offset + range_length) of an
// *uncompressed* blob's value directly from the file, reading only those
// bytes. Unlike GetBlob it does not read the record header/key, does not
// verify the whole-record checksum (a strict sub-range cannot cover it), and
// never decompresses -- so it is only valid when this reader's blob file is
// uncompressed (checked). `offset` and `value_size` are the blob value's file
// offset and full size (from the BlobIndex); the caller must ensure
// range_offset + range_length <= value_size. On success `*result` owns a copy
// of the requested bytes and `*bytes_read` (when non-null) is the number of
// bytes read from the file.
Status GetBlobRange(const ReadOptions& read_options, const Slice& user_key,
uint64_t offset, uint64_t value_size,
uint64_t range_offset, size_t range_length,
MemoryAllocator* allocator,
std::unique_ptr<BlobContents>* result,
uint64_t* bytes_read) const;
// offsets must be sorted in ascending order by caller.
void MultiGetBlob(
const ReadOptions& read_options, MemoryAllocator* allocator,
+48
View File
@@ -76,6 +76,54 @@ Status ReadAndVerifySimpleGen2BlobRecord(
return Status::OK();
}
Status ReadSimpleGen2BlobRange(const ReadOptions& read_options,
RandomAccessFileReader* file,
uint64_t record_offset, size_t payload_size,
uint64_t range_offset, size_t range_length,
CompressionType expected_compression,
char* buf) {
assert(file != nullptr);
assert(buf != nullptr);
// A strict sub-range of a compressed payload cannot be decompressed in
// isolation; callers resolve such payloads whole and slice instead.
if (expected_compression != kNoCompression) {
return Status::Corruption("Cannot range-read a compressed blob");
}
// The requested sub-range must lie within the payload (the trailer that
// follows the payload is never part of a range read).
if (range_offset > payload_size ||
range_length > payload_size - range_offset) {
return Status::InvalidArgument("Blob range out of bounds");
}
Slice result;
IOOptions opts;
IODebugContext dbg;
Status s = file->PrepareIOOptions(read_options, opts, &dbg);
if (s.ok()) {
s = file->Read(opts, record_offset + range_offset, range_length, &result,
buf, nullptr, &dbg);
}
if (!s.ok()) {
return s;
}
if (result.size() != range_length) {
return Status::Corruption("Could not read complete blob range");
}
// With mmap reads the data lands outside `buf`; copy it in so the caller can
// rely on `buf` owning the bytes (the only copy on the mmap path).
if (result.data() != buf) {
memcpy(buf, result.data(), range_length);
}
// No trailer read and no checksum verification: a strict sub-range cannot
// cover the record's checksum (callers that require it read the whole
// record).
return Status::OK();
}
IOStatus WriteSimpleGen2BlobRecord(WritableFileWriter* file,
const WriteOptions& write_options,
ChecksumType checksum_type,
+18
View File
@@ -73,6 +73,24 @@ Status ReadAndVerifySimpleGen2BlobRecord(
ChecksumType checksum_type, uint32_t base_context_checksum,
CompressionType expected_compression, char* buf);
// Reads a byte sub-range [range_offset, range_offset + range_length) of an
// *uncompressed* SimpleGen2Blob payload directly into `buf` (capacity >=
// range_length), reading only those bytes. Unlike
// ReadAndVerifySimpleGen2BlobRecord it does not read the trailer and does not
// verify the record's checksum (a strict sub-range cannot cover it); callers
// that require verification read the whole record instead. `payload_size` is
// the full payload size (from the blob index); the caller must ensure
// range_offset + range_length <= payload_size. `expected_compression` must be
// kNoCompression (a strict sub-range of a compressed payload cannot be
// decompressed in isolation).
//
// On success, buf[0, range_length) holds the requested payload bytes.
Status ReadSimpleGen2BlobRange(const ReadOptions& read_options,
RandomAccessFileReader* file,
uint64_t record_offset, size_t payload_size,
uint64_t range_offset, size_t range_length,
CompressionType expected_compression, char* buf);
// Writes a SimpleGen2Blob record for `payload` at the current end of `file`,
// which the caller asserts is byte offset `record_offset`. Appends the payload
// bytes followed by the 5-byte trailer (compression marker + context-modified
+287 -49
View File
@@ -36,6 +36,102 @@ Status AppendBlobRefreshRetryFailure(const Status& stale_status,
stale_status, "; refresh retry failed: ", retry_status.ToString());
}
// Runs read(reader) against the blob-file reader for `file_number`. On a
// Corruption -- which a stale cached reader (e.g. the physical file was
// replaced) can surface -- evicts the cached reader, reopens it uncached,
// retries read() once, and refreshes the cache on success. Shared by the
// single-blob read paths (GetBlob / GetBlobRange) so the stale-file recovery
// lives in one place.
//
// `read` is invoked with a BlobFileReader* and returns a Status; because it may
// run twice, it must re-initialize any output it populates on each call (and is
// taken by const reference rather than forwarded, since it is not moved-from).
template <typename ReadFn>
Status ReadBlobWithReaderRetry(BlobFileCache* blob_file_cache,
const ReadOptions& read_options,
uint64_t file_number, const ReadFn& read) {
CacheHandleGuard<BlobFileReader> blob_file_reader;
Status s = blob_file_cache->GetBlobFileReader(read_options, file_number,
&blob_file_reader);
if (!s.ok()) {
return s;
}
assert(blob_file_reader.GetValue());
s = read(blob_file_reader.GetValue());
if (!s.IsCorruption()) {
return s;
}
const Status stale_status = s;
blob_file_reader.Reset();
blob_file_cache->Evict(file_number);
std::unique_ptr<BlobFileReader> fresh_reader;
s = blob_file_cache->OpenBlobFileReaderUncached(
read_options, file_number, &fresh_reader,
/*allow_footer_skip_retry=*/false);
if (!s.ok()) {
return AppendBlobRefreshRetryFailure(stale_status, s);
}
s = read(fresh_reader.get());
if (!s.ok()) {
return AppendBlobRefreshRetryFailure(stale_status, s);
}
CacheHandleGuard<BlobFileReader> ignored_reader;
blob_file_cache
->RefreshBlobFileReader(file_number, &fresh_reader, &ignored_reader)
.PermitUncheckedError();
return s;
}
// Records lazy wide-column read metrics for one actual storage read issued
// while resolving a lazy result (attributed via Env::IOActivity::kLazyResolve):
// `bytes_read` bytes were read from storage. Covers both whole-column and
// partial reads; not called for cache hits (no storage read).
void RecordLazyRead(Statistics* statistics, uint64_t bytes_read) {
RecordTick(statistics, BLOB_DB_LAZY_READ_COUNT);
RecordTick(statistics, BLOB_DB_LAZY_READ_BYTES, bytes_read);
}
// Additionally records the partial-read metrics for one actual partial
// (byte-range) read: `bytes_read` bytes were fetched instead of the column's
// full `value_size`. Call alongside RecordLazyRead (a partial read is also a
// lazy read).
void RecordLazyPartialRead(Statistics* statistics, uint64_t bytes_read,
uint64_t value_size) {
RecordTick(statistics, BLOB_DB_LAZY_PARTIAL_READ_COUNT);
if (value_size > bytes_read) {
RecordTick(statistics, BLOB_DB_LAZY_PARTIAL_BYTES_SAVED,
value_size - bytes_read);
}
}
// Pins the sub-range [range_offset, range_offset + range_length) of a cache-hit
// whole blob value into *value (clamped: an offset at/past the end yields
// empty, length is clamped to the remainder), keeping the cached bytes alive by
// transferring the cache handle into *value. Shared by the range-read cache-hit
// paths (GetBlobRange / GetSimpleGen2BlobRange).
void PinCacheHitSubRange(CacheHandleGuard<BlobContents>* blob_handle,
uint64_t range_offset, size_t range_length,
PinnableSlice* value) {
const Slice full = blob_handle->GetValue()->data();
Slice sub;
if (range_offset < full.size()) {
const size_t off = static_cast<size_t>(range_offset);
const size_t avail = full.size() - off;
const size_t len = range_length > avail ? avail : range_length;
sub = Slice(full.data() + off, len);
} // else: offset at/past end -> empty (not an error)
value->Reset();
constexpr Cleanable* cleanable = nullptr;
value->PinSlice(sub, cleanable);
blob_handle->TransferTo(value);
}
} // namespace
BlobSource::BlobSource(const ImmutableOptions& immutable_options,
@@ -228,66 +324,37 @@ Status BlobSource::GetBlob(const ReadOptions& read_options,
std::unique_ptr<BlobContents> blob_contents;
{
CacheHandleGuard<BlobFileReader> blob_file_reader;
s = blob_file_cache_->GetBlobFileReader(read_options, file_number,
&blob_file_reader);
if (!s.ok()) {
return s;
}
assert(blob_file_reader.GetValue());
if (compression_type != blob_file_reader.GetValue()->GetCompressionType()) {
return Status::Corruption("Compression type mismatch when reading blob");
}
MemoryAllocator* const allocator =
(blob_cache_ && read_options.fill_cache)
? blob_cache_.get()->memory_allocator()
: nullptr;
uint64_t read_size = 0;
s = blob_file_reader.GetValue()->GetBlob(
read_options, user_key, offset, value_size, compression_type,
prefetch_buffer, allocator, &blob_contents, &read_size);
if (s.IsCorruption()) {
const Status stale_status = s;
blob_file_reader.Reset();
blob_file_cache_->Evict(file_number);
std::unique_ptr<BlobFileReader> fresh_reader;
s = blob_file_cache_->OpenBlobFileReaderUncached(
read_options, file_number, &fresh_reader,
/*allow_footer_skip_retry=*/false);
if (!s.ok()) {
return AppendBlobRefreshRetryFailure(stale_status, s);
}
if (compression_type != fresh_reader->GetCompressionType()) {
return Status::Corruption(
"Compression type mismatch when reading blob");
}
blob_contents.reset();
read_size = 0;
s = fresh_reader->GetBlob(read_options, user_key, offset, value_size,
compression_type, prefetch_buffer, allocator,
&blob_contents, &read_size);
if (!s.ok()) {
s = AppendBlobRefreshRetryFailure(stale_status, s);
} else {
CacheHandleGuard<BlobFileReader> ignored_reader;
blob_file_cache_
->RefreshBlobFileReader(file_number, &fresh_reader, &ignored_reader)
.PermitUncheckedError();
}
}
s = ReadBlobWithReaderRetry(
blob_file_cache_, read_options, file_number,
[&](BlobFileReader* reader) {
if (compression_type != reader->GetCompressionType()) {
return Status::Corruption(
"Compression type mismatch when reading blob");
}
blob_contents.reset();
read_size = 0;
return reader->GetBlob(read_options, user_key, offset, value_size,
compression_type, prefetch_buffer, allocator,
&blob_contents, &read_size);
});
if (!s.ok()) {
return s;
}
if (bytes_read) {
*bytes_read = read_size;
}
// Whole-column read on the lazy resolve path (partial reads go through
// GetBlobRange). Counts the storage read; partial reads are not counted
// here.
if (read_options.io_activity == Env::IOActivity::kLazyResolve) {
RecordLazyRead(statistics_, read_size);
}
}
if (blob_cache_ && read_options.fill_cache) {
@@ -308,6 +375,93 @@ Status BlobSource::GetBlob(const ReadOptions& read_options,
return s;
}
Status BlobSource::GetBlobRange(const ReadOptions& read_options,
const Slice& user_key, uint64_t file_number,
uint64_t offset, uint64_t file_size,
uint64_t value_size,
CompressionType compression_type,
uint64_t range_offset, size_t range_length,
PinnableSlice* value, uint64_t* bytes_read) {
assert(value);
// Partial reads are for uncompressed blobs only; the caller decides this (a
// compressed column takes the whole-value GetBlob path and slices).
assert(compression_type == kNoCompression);
// Range reads are (currently) only issued while resolving a lazy result, so
// the lazy read stats below are recorded unconditionally (unlike GetBlob,
// which gates on the activity). Enforce that invariant here.
assert(read_options.io_activity == Env::IOActivity::kLazyResolve);
Status s;
const CacheKey cache_key = GetCacheKey(file_number, file_size, offset);
CacheHandleGuard<BlobContents> blob_handle;
// First, probe the blob cache for the whole value. On a hit, slice the
// requested sub-range out of the cached value while pinning the cache handle
// (zero-copy, no disk read). A partial read never inserts into the cache.
if (blob_cache_) {
Slice key = cache_key.AsSlice();
s = GetBlobFromCache(key, &blob_handle);
if (s.ok()) {
PinCacheHitSubRange(&blob_handle, range_offset, range_length, value);
if (bytes_read) {
*bytes_read = 0; // served from cache; no disk read
}
return s;
}
}
assert(blob_handle.IsEmpty());
const bool no_io = read_options.read_tier == kBlockCacheTier;
if (no_io) {
s = Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
return s;
}
// Cache miss: read only the requested sub-range from the file. The result is
// never inserted into the blob cache (see the header comment).
std::unique_ptr<BlobContents> blob_contents;
{
// No cache-fill allocator: a partial value is never inserted into the
// cache.
constexpr MemoryAllocator* allocator = nullptr;
uint64_t read_size = 0;
// Reuses the stale-reader retry shared with GetBlob. A range read skips
// whole-record checksum verification, so the retry handles the stale-file
// case (offset/size mismatch surfacing as Corruption), not a payload
// checksum failure.
s = ReadBlobWithReaderRetry(
blob_file_cache_, read_options, file_number,
[&](BlobFileReader* reader) {
if (compression_type != reader->GetCompressionType()) {
return Status::Corruption(
"Compression type mismatch when reading blob");
}
blob_contents.reset();
read_size = 0;
return reader->GetBlobRange(read_options, user_key, offset,
value_size, range_offset, range_length,
allocator, &blob_contents, &read_size);
});
if (!s.ok()) {
return s;
}
if (bytes_read) {
*bytes_read = read_size;
}
RecordLazyRead(statistics_, read_size);
RecordLazyPartialRead(statistics_, read_size, value_size);
}
PinOwnedBlob(&blob_contents, value);
assert(s.ok());
return s;
}
Status BlobSource::GetSimpleGen2Blob(
const ReadOptions& read_options, const OffsetableCacheKey& base_cache_key,
RandomAccessFileReader* file, uint64_t record_offset, uint64_t payload_size,
@@ -348,7 +502,8 @@ Status BlobSource::GetSimpleGen2Blob(
const bool no_io = read_options.read_tier == kBlockCacheTier;
if (no_io) {
return Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
s = Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
return s;
}
// Cache miss (or no cache configured). Read the record into a buffer
@@ -379,6 +534,11 @@ Status BlobSource::GetSimpleGen2Blob(
if (bytes_read) {
*bytes_read = record_size;
}
// Whole-column read on the lazy resolve path (partial reads go through
// GetSimpleGen2BlobRange).
if (read_options.io_activity == Env::IOActivity::kLazyResolve) {
RecordLazyRead(statistics_, record_size);
}
if (blob_cache_ && read_options.fill_cache) {
// If filling cache is allowed and a cache is configured, try to put the
@@ -398,6 +558,84 @@ Status BlobSource::GetSimpleGen2Blob(
return s;
}
Status BlobSource::GetSimpleGen2BlobRange(
const ReadOptions& read_options, const OffsetableCacheKey& base_cache_key,
RandomAccessFileReader* file, uint64_t record_offset, uint64_t payload_size,
ChecksumType /*checksum_type*/, uint32_t /*base_context_checksum*/,
CompressionType expected_compression, uint64_t range_offset,
size_t range_length, PinnableSlice* value, uint64_t* bytes_read) {
assert(value);
assert(file);
// Partial reads are for uncompressed payloads only; the caller decides this
// (a compressed column takes the whole-payload GetSimpleGen2Blob path +
// slice).
assert(expected_compression == kNoCompression);
// Range reads are (currently) only issued while resolving a lazy result, so
// the lazy read stats below are recorded unconditionally (unlike
// GetSimpleGen2Blob, which gates on the activity). Enforce that invariant
// here.
assert(read_options.io_activity == Env::IOActivity::kLazyResolve);
const CacheKey cache_key =
GetSimpleGen2BlobCacheKey(base_cache_key, record_offset);
Status s;
CacheHandleGuard<BlobContents> blob_handle;
// First, probe the blob cache for the whole payload. On a hit, slice the
// requested sub-range out of the cached payload while pinning the cache
// handle (zero-copy, no disk read). A partial read never inserts into the
// cache.
if (blob_cache_) {
Slice key = cache_key.AsSlice();
s = GetBlobFromCache(key, &blob_handle);
if (s.ok()) {
PinCacheHitSubRange(&blob_handle, range_offset, range_length, value);
if (bytes_read) {
*bytes_read = 0; // served from cache; no disk read
}
return s;
}
}
assert(blob_handle.IsEmpty());
const bool no_io = read_options.read_tier == kBlockCacheTier;
if (no_io) {
s = Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
return s;
}
// Cache miss: read only the requested sub-range from the file. The result is
// never inserted into the blob cache (a partial payload cannot represent the
// whole-record cache entry).
CacheAllocationPtr buf = AllocateBlock(range_length, /*allocator=*/nullptr);
s = ReadSimpleGen2BlobRange(read_options, file, record_offset,
static_cast<size_t>(payload_size), range_offset,
range_length, expected_compression, buf.get());
if (!s.ok()) {
return s;
}
std::unique_ptr<BlobContents> blob_contents(
new BlobContents(std::move(buf), range_length));
RecordTick(statistics_, BLOB_DB_BLOB_FILE_BYTES_READ, range_length);
PERF_COUNTER_ADD(blob_read_count, 1);
PERF_COUNTER_ADD(blob_read_byte, range_length);
RecordLazyRead(statistics_, range_length);
RecordLazyPartialRead(statistics_, range_length, payload_size);
if (bytes_read) {
*bytes_read = range_length;
}
PinOwnedBlob(&blob_contents, value);
assert(s.ok());
return s;
}
void BlobSource::MultiGetBlob(const ReadOptions& read_options,
autovector<BlobFileReadRequests>& blob_reqs,
uint64_t* bytes_read) {
+50
View File
@@ -61,6 +61,31 @@ class BlobSource {
FilePrefetchBuffer* prefetch_buffer, PinnableSlice* value,
uint64_t* bytes_read);
// Reads a byte sub-range [range_offset, range_offset + range_length) of an
// *uncompressed* blob's value, reading only those bytes on a cache miss.
//
// First probes the blob cache for the whole value. On a hit, pins the cache
// handle into *value and points it at the requested sub-range (zero-copy, no
// disk I/O). On a miss, reads only the requested bytes from the blob file
// (via BlobFileReader::GetBlobRange -- no record-header/key read and no
// whole-record checksum verification) and pins the owned buffer into *value.
//
// Unlike GetBlob, a partial read never inserts into the blob cache: the cache
// entry is keyed per (file, offset) and holds the *whole* BlobContents, so a
// partial value would violate that invariant. `compression_type` must be
// kNoCompression (a strict sub-range of a compressed blob cannot be
// decompressed in isolation); callers that need a compressed column, a
// whole-column read, or checksum verification use GetBlob and slice instead.
//
// The caller must ensure range_offset + range_length <= value_size. On a miss
// *bytes_read (when non-null) is the number of bytes read from the file; on a
// hit it is 0.
Status GetBlobRange(const ReadOptions& read_options, const Slice& user_key,
uint64_t file_number, uint64_t offset, uint64_t file_size,
uint64_t value_size, CompressionType compression_type,
uint64_t range_offset, size_t range_length,
PinnableSlice* value, uint64_t* bytes_read);
// Reads a SimpleGen2Blob payload (see db/blob/blob_gen2_format.h) through the
// blob value cache and BLOB_DB_* statistics. This is the counterpart to
// GetBlob() for the second-generation blob record format, which is read
@@ -96,6 +121,31 @@ class BlobSource {
CompressionType expected_compression,
PinnableSlice* value, uint64_t* bytes_read);
// Reads a byte sub-range [range_offset, range_offset + range_length) of an
// *uncompressed* SimpleGen2Blob payload, reading only those bytes on a cache
// miss. This is the embedded (same-file) counterpart of GetBlobRange().
//
// First probes the blob cache for the whole payload (keyed exactly as
// GetSimpleGen2Blob). On a hit, pins the cache handle into *value and points
// it at the requested sub-range (zero-copy, no disk I/O). On a miss, reads
// only the requested bytes from the file (via ReadSimpleGen2BlobRange -- no
// trailer read and no checksum verification) and pins the owned buffer.
//
// Like GetBlobRange, a partial read never inserts into the blob cache (the
// cache entry holds the whole payload). `expected_compression` must be
// kNoCompression. The caller must ensure range_offset + range_length <=
// payload_size. On a miss *bytes_read (when non-null) is the number of bytes
// read from the file; on a hit it is 0.
Status GetSimpleGen2BlobRange(const ReadOptions& read_options,
const OffsetableCacheKey& base_cache_key,
RandomAccessFileReader* file,
uint64_t record_offset, uint64_t payload_size,
ChecksumType checksum_type,
uint32_t base_context_checksum,
CompressionType expected_compression,
uint64_t range_offset, size_t range_length,
PinnableSlice* value, uint64_t* bytes_read);
// Read multiple blobs from the underlying cache or blob file(s).
//
// If successful, returns ok and sets "result" in the elements of "blob_reqs"
+71
View File
@@ -5,6 +5,8 @@
#include <algorithm>
#include <array>
#include <atomic>
#include <cstdint>
#include <set>
#include <sstream>
#include <string>
@@ -63,6 +65,75 @@ TEST_F(DBBlobBasicTest, GetBlob) {
.IsIncomplete());
}
TEST_F(DBBlobBasicTest, BlobFileWritableFileMaxBufferSize) {
constexpr uint64_t kBlobWriterBufferSize = 128 * 1024;
Options options = GetDefaultOptions();
options.enable_blob_files = true;
options.min_blob_size = 0;
options.writable_file_max_buffer_size = 1024 * 1024;
options.blob_file_writable_file_max_buffer_size = kBlobWriterBufferSize;
Reopen(options);
std::atomic<int> matching_blob_writer_count{0};
SyncPoint::GetInstance()->ClearAllCallBacks();
SyncPoint::GetInstance()->SetCallBack(
"WritableFileWriter::WritableFileWriter:0", [&](void* arg) {
const uint64_t max_buffer_size =
static_cast<uint64_t>(reinterpret_cast<uintptr_t>(arg));
if (max_buffer_size == kBlobWriterBufferSize) {
++matching_blob_writer_count;
}
});
SyncPoint::GetInstance()->EnableProcessing();
ASSERT_OK(Put("key", std::string(1024, 'v')));
ASSERT_OK(Flush());
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
ASSERT_GE(matching_blob_writer_count.load(), 1);
}
TEST_F(DBBlobBasicTest,
DirectWriteBlobFileWritableFileMaxBufferSizeSetOptions) {
constexpr uint64_t kBlobWriterBufferSize = 64 * 1024;
Options options = GetDefaultOptions();
options.enable_blob_files = true;
options.enable_blob_direct_write = true;
options.allow_concurrent_memtable_write = false;
options.min_blob_size = 0;
options.blob_file_size = 200;
options.writable_file_max_buffer_size = 1024 * 1024;
Reopen(options);
std::atomic<int> matching_blob_writer_count{0};
SyncPoint::GetInstance()->ClearAllCallBacks();
SyncPoint::GetInstance()->SetCallBack(
"WritableFileWriter::WritableFileWriter:0", [&](void* arg) {
const uint64_t max_buffer_size =
static_cast<uint64_t>(reinterpret_cast<uintptr_t>(arg));
if (max_buffer_size == kBlobWriterBufferSize) {
++matching_blob_writer_count;
}
});
SyncPoint::GetInstance()->EnableProcessing();
ASSERT_OK(Put("first", std::string(300, 'a')));
ASSERT_OK(db_->SetOptions({{"blob_file_writable_file_max_buffer_size",
std::to_string(kBlobWriterBufferSize)}}));
ASSERT_OK(Put("second", std::string(300, 'b')));
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
ASSERT_GE(matching_blob_writer_count.load(), 1);
}
TEST_F(DBBlobBasicTest, EmptyValueNotStoredAsBlob) {
// Regression test for crash when empty blob value is evicted to
// CompressedSecondaryCache (T261142690). Empty values should always be
+52 -1
View File
@@ -15,6 +15,7 @@
#include <vector>
#include "db/arena_wrapped_db_iter.h"
#include "db/blob/blob_fetcher.h"
#include "db/blob/blob_index.h"
#include "db/column_family.h"
#include "db/db_iter.h"
@@ -138,8 +139,11 @@ class DBBlobIndexTest : public DBTestBase {
bool resolve_direct_write_value, const Version* current,
PinnableSlice* value, PinnableWideColumns* columns, Status* s,
bool* is_blob_index, bool* value_found = nullptr) {
VersionBlobFetcher blob_fetcher(current, read_options,
cfd()->blob_file_cache(),
/*allow_write_path_fallback=*/true);
return DBImpl::MaybeResolveDirectWriteValue(
read_options, key, resolve_direct_write_value, current, cfd(), value,
read_options, key, resolve_direct_write_value, blob_fetcher, value,
columns, s, is_blob_index, value_found);
}
@@ -1028,6 +1032,53 @@ TEST_F(DBBlobIndexTest, EmbeddedBlobWideColumnGetEntityBlockCacheTier) {
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
}
// Companion to the test above (genuine miss -> Incomplete): with a blob cache
// configured, a warm read populates the embedded record into the blob cache, so
// a subsequent kBlockCacheTier GetEntity serves it from cache instead of
// returning Incomplete. This exercises the non-lazy Get/GetEntity embedded path
// (get_context -> EmbeddedAwareBlobFetcher -> GetSameFileBlob whole ->
// ResolveEmbeddedBlobCached), which shares the block-cache-tier behavior with
// the lazy resolver (a same-file column already in the blob cache is served).
TEST_F(DBBlobIndexTest,
EmbeddedBlobWideColumnGetEntityBlockCacheTierServesCached) {
Options options = GetTestOptions();
options.create_if_missing = true;
LRUCacheOptions co;
co.capacity = 8 << 20;
options.blob_cache = NewLRUCache(co);
DestroyAndReopen(options);
const std::string sst_path = dbname_ + "/embedded_wc_blockcache_hit.sst";
SstFileWriterEmbeddedBlobOptions embedded_blob_options;
embedded_blob_options.min_blob_size = 8;
const std::string big1(1024, 'a');
const WideColumns columns{{kDefaultWideColumnName, "d"}, {"big", big1}};
SstFileWriter writer(EnvOptions(), options);
ASSERT_OK(writer.OpenWithEmbeddedBlobs(sst_path, embedded_blob_options));
ASSERT_OK(writer.PutEntity("k", columns));
ASSERT_OK(writer.Finish());
ASSERT_OK(db_->IngestExternalFile({sst_path}, IngestExternalFileOptions()));
// Warm both the block cache (data block) and the blob cache (embedded record)
// with a full read.
{
PinnableWideColumns result;
ASSERT_OK(db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), "k",
&result));
ASSERT_EQ(result.columns(), columns);
}
// The embedded record is now cached, so a block-cache-only read serves it.
ReadOptions read_options;
read_options.read_tier = kBlockCacheTier;
PinnableWideColumns result;
ASSERT_OK(
db_->GetEntity(read_options, db_->DefaultColumnFamily(), "k", &result));
ASSERT_EQ(result.columns(), columns);
}
// A corrupt embedded blob record backing a wide-column column must surface an
// error on GetEntity, not expose the raw same-file BlobIndex.
// A failure resolving an embedded blob record backing a wide-column column
+23 -4
View File
@@ -5,6 +5,10 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include "db/blob/blob_constants.h"
#include "rocksdb/rocksdb_namespace.h"
#include "rocksdb/status.h"
@@ -27,12 +31,27 @@ class SameFileBlobReader {
public:
virtual ~SameFileBlobReader() = default;
// Resolves a same-file blob reference (one for which blob_index.IsSameFile()
// is true) into `value`, pinning the payload zero-copy (from the blob cache
// or an owned buffer). read_tier == kBlockCacheTier without a cached record
// yields Status::Incomplete; a bad record yields Status::Corruption.
// Resolves a same-file ("embedded") blob reference (one for which
// blob_index.IsSameFile() is true) into `value`, pinning the payload
// zero-copy (from the blob cache or an owned buffer).
//
// range_length == kWholeBlobLength resolves the whole value. Any other length
// resolves only the sub-range [range_offset, range_offset + range_length),
// reading just those bytes on a cache miss -- skipping whole-record checksum
// verification (a strict sub-range cannot cover it) and never populating the
// blob cache; valid only for an uncompressed record and a verify_policy other
// than kVerifyIfPresent, with range_offset + range_length <=
// blob_index.size().
//
// verify_policy controls whole-record checksum verification (see
// BlobVerifyPolicy); a whole read under kVerifyIfPresent verifies even when
// ReadOptions::verify_checksums is off. read_tier == kBlockCacheTier without
// a cached record yields Status::Incomplete; a bad record yields
// Status::Corruption.
virtual Status GetSameFileBlob(const ReadOptions& read_options,
const BlobIndex& blob_index,
uint64_t range_offset, size_t range_length,
BlobVerifyPolicy verify_policy,
PinnableSlice* value) const = 0;
};
+158 -7
View File
@@ -14,6 +14,7 @@
#include <cstdlib>
#include <iterator>
#include <limits>
#include <map>
#include <memory>
#include <unordered_set>
@@ -182,7 +183,6 @@ using ROCKSDB_NAMESPACE::TraceReader;
using ROCKSDB_NAMESPACE::Transaction;
using ROCKSDB_NAMESPACE::TransactionDB;
using ROCKSDB_NAMESPACE::TransactionDBOptions;
using ROCKSDB_NAMESPACE::TransactionLogIterator;
using ROCKSDB_NAMESPACE::TransactionOptions;
using ROCKSDB_NAMESPACE::UserCollectedProperties;
using ROCKSDB_NAMESPACE::UserDefinedIndexFactory;
@@ -190,6 +190,7 @@ using ROCKSDB_NAMESPACE::VectorWalPtr;
using ROCKSDB_NAMESPACE::WaitForCompactOptions;
using ROCKSDB_NAMESPACE::WalFile;
using ROCKSDB_NAMESPACE::WalFilter;
using ROCKSDB_NAMESPACE::WalIterator;
using ROCKSDB_NAMESPACE::WALRecoveryMode;
using ROCKSDB_NAMESPACE::WritableFile;
using ROCKSDB_NAMESPACE::WriteBatch;
@@ -412,12 +413,16 @@ struct rocksdb_readoptions_t {
void* table_filter_state = nullptr;
void (*table_filter_destructor)(void*) = nullptr;
rocksdb_readoptions_table_filter_cb table_filter_callback = nullptr;
// Owns the std::function that rep.table_filter points to. rep.table_filter is
// a pointer to a caller-owned std::function, and this struct is that owner.
std::function<bool(const TableProperties&)> table_filter_func;
std::shared_ptr<UserDefinedIndexFactory> table_index_factory;
~rocksdb_readoptions_t() { ClearTableFilter(); }
void ClearTableFilter() {
rep.table_filter = {};
rep.table_filter = nullptr;
table_filter_func = {};
if (table_filter_destructor != nullptr) {
(*table_filter_destructor)(table_filter_state);
}
@@ -435,11 +440,12 @@ struct rocksdb_readoptions_t {
if (callback != nullptr) {
auto cb = table_filter_callback;
auto filter_state = table_filter_state;
rep.table_filter = [cb, filter_state](const TableProperties& props) {
table_filter_func = [cb, filter_state](const TableProperties& props) {
return cb(filter_state,
reinterpret_cast<const rocksdb_table_properties_t*>(
&props)) != 0;
};
rep.table_filter = &table_filter_func;
}
}
};
@@ -480,10 +486,10 @@ struct rocksdb_writablefile_t {
WritableFile* rep;
};
struct rocksdb_wal_iterator_t {
TransactionLogIterator* rep;
WalIterator* rep;
};
struct rocksdb_wal_readoptions_t {
TransactionLogIterator::ReadOptions rep;
WalIterator::ReadOptions rep;
};
struct rocksdb_wal_files_t {
@@ -594,6 +600,63 @@ struct rocksdb_perfcontext_t {
struct rocksdb_pinnableslice_t {
PinnableSlice rep;
};
struct rocksdb_pinnable_multi_get_t {
static constexpr size_t kNotFound = std::numeric_limits<size_t>::max();
explicit rocksdb_pinnable_multi_get_t(size_t result_count)
: result_indexes(result_count, kNotFound) {}
void ReservePayloads(const std::vector<Status>& statuses) {
size_t value_count = 0;
size_t error_count = 0;
for (const Status& status : statuses) {
value_count += status.ok();
error_count += !status.ok() && !status.IsNotFound();
}
values.reserve(value_count);
errors.reserve(error_count);
}
void StoreResults(std::vector<PinnableSlice>* multi_get_values,
const std::vector<Status>& statuses) {
ReservePayloads(statuses);
for (size_t i = 0; i < statuses.size(); ++i) {
if (statuses[i].ok()) {
result_indexes[i] = values.size();
values.emplace_back(std::move((*multi_get_values)[i]));
} else if (!statuses[i].IsNotFound()) {
result_indexes[i] = kNotFound - errors.size() - 1;
errors.emplace_back(statuses[i].ToString());
}
}
}
int GetResult(size_t index, const char** value, size_t* value_size,
const char** error, size_t* error_size) const {
if (index >= result_indexes.size()) {
return rocksdb_pinnable_multi_get_out_of_bounds;
}
const size_t result_index = result_indexes[index];
if (result_index == kNotFound) {
return rocksdb_pinnable_multi_get_not_found;
}
if (result_index < values.size()) {
*value = values[result_index].data();
*value_size = values[result_index].size();
return rocksdb_pinnable_multi_get_found;
}
const std::string& message = errors[kNotFound - result_index - 1];
*error = message.c_str();
*error_size = message.size();
return rocksdb_pinnable_multi_get_error;
}
// Found entries store a value index. Error entries count back from
// kNotFound - 1, leaving kNotFound to represent a missing key.
std::vector<size_t> result_indexes;
std::vector<PinnableSlice> values;
std::vector<std::string> errors;
};
struct rocksdb_transactiondb_options_t {
TransactionDBOptions rep;
};
@@ -2862,6 +2925,44 @@ void rocksdb_batched_multi_get_cf_slice(
delete[] statuses;
}
rocksdb_pinnable_multi_get_t* rocksdb_batched_multi_get_pinned_cf(
rocksdb_t* db, const rocksdb_readoptions_t* options,
rocksdb_column_family_handle_t* column_family, size_t num_keys,
const rocksdb_slice_t* keys, unsigned char sorted_input) {
auto results = std::make_unique<rocksdb_pinnable_multi_get_t>(num_keys);
if (num_keys == 0) {
return results.release();
}
const Slice* key_slices = reinterpret_cast<const Slice*>(keys);
std::vector<PinnableSlice> values(num_keys);
std::vector<Status> statuses(num_keys);
db->rep->MultiGet(options->rep, column_family->rep, num_keys, key_slices,
values.data(), statuses.data(), sorted_input != 0);
results->StoreResults(&values, statuses);
return results.release();
}
size_t rocksdb_pinnable_multi_get_count(
const rocksdb_pinnable_multi_get_t* multi_get) {
return multi_get->result_indexes.size();
}
int rocksdb_pinnable_multi_get_result(
const rocksdb_pinnable_multi_get_t* multi_get, size_t index,
const char** value, size_t* value_size, const char** error,
size_t* error_size) {
*value = nullptr;
*value_size = 0;
*error = nullptr;
*error_size = 0;
return multi_get->GetResult(index, value, value_size, error, error_size);
}
void rocksdb_pinnable_multi_get_destroy(
rocksdb_pinnable_multi_get_t* multi_get) {
delete multi_get;
}
unsigned char rocksdb_key_may_exist(rocksdb_t* db,
const rocksdb_readoptions_t* options,
const char* key, size_t key_len,
@@ -2921,8 +3022,8 @@ rocksdb_iterator_t* rocksdb_create_iterator(
rocksdb_wal_iterator_t* rocksdb_get_updates_since(
rocksdb_t* db, uint64_t seq_number,
const rocksdb_wal_readoptions_t* options, char** errptr) {
std::unique_ptr<TransactionLogIterator> iter;
TransactionLogIterator::ReadOptions ro;
std::unique_ptr<WalIterator> iter;
WalIterator::ReadOptions ro;
if (options != nullptr) {
ro = options->rep;
}
@@ -9107,6 +9208,15 @@ int rocksdb_options_get_max_file_opening_threads(rocksdb_options_t* opt) {
return opt->rep.max_file_opening_threads;
}
void rocksdb_options_set_read_io_executor_threads(rocksdb_options_t* opt,
int v) {
opt->rep.read_io_executor_threads = v;
}
int rocksdb_options_get_read_io_executor_threads(rocksdb_options_t* opt) {
return opt->rep.read_io_executor_threads;
}
void rocksdb_options_set_open_files_async(rocksdb_options_t* opt,
unsigned char v) {
opt->rep.open_files_async = v;
@@ -9773,6 +9883,16 @@ const char* rocksdb_options_get_db_host_id(rocksdb_options_t* opt,
return opt->rep.db_host_id.data();
}
void rocksdb_options_set_remote_compaction_manifest_floor(
rocksdb_options_t* opt, unsigned char v) {
opt->rep.remote_compaction_manifest_floor = v;
}
unsigned char rocksdb_options_get_remote_compaction_manifest_floor(
rocksdb_options_t* opt) {
return opt->rep.remote_compaction_manifest_floor;
}
void rocksdb_options_set_lowest_used_cache_tier(rocksdb_options_t* opt, int v) {
opt->rep.lowest_used_cache_tier =
static_cast<decltype(opt->rep.lowest_used_cache_tier)>(v);
@@ -9813,6 +9933,16 @@ uint64_t rocksdb_options_get_max_compaction_trigger_wakeup_seconds(
return opt->rep.max_compaction_trigger_wakeup_seconds;
}
void rocksdb_options_set_periodic_compaction_phase_recovery_percent(
rocksdb_options_t* opt, int v) {
opt->rep.periodic_compaction_phase_recovery_percent = v;
}
int rocksdb_options_get_periodic_compaction_phase_recovery_percent(
rocksdb_options_t* opt) {
return opt->rep.periodic_compaction_phase_recovery_percent;
}
void rocksdb_options_set_follower_refresh_catchup_period_ms(
rocksdb_options_t* opt, uint64_t v) {
opt->rep.follower_refresh_catchup_period_ms = v;
@@ -10262,6 +10392,16 @@ uint64_t rocksdb_options_get_blob_file_size(rocksdb_options_t* opt) {
return opt->rep.blob_file_size;
}
void rocksdb_options_set_blob_file_writable_file_max_buffer_size(
rocksdb_options_t* opt, uint64_t v) {
opt->rep.blob_file_writable_file_max_buffer_size = v;
}
uint64_t rocksdb_options_get_blob_file_writable_file_max_buffer_size(
rocksdb_options_t* opt) {
return opt->rep.blob_file_writable_file_max_buffer_size;
}
void rocksdb_options_set_blob_compression_type(rocksdb_options_t* opt, int v) {
opt->rep.blob_compression_type =
static_cast<decltype(opt->rep.blob_compression_type)>(v);
@@ -11461,6 +11601,17 @@ double rocksdb_block_based_options_get_uniform_cv_threshold(
return opt->rep.uniform_cv_threshold;
}
void rocksdb_block_based_options_set_optimize_key_common_prefix(
rocksdb_block_based_table_options_t* opt, int v) {
opt->rep.optimize_key_common_prefix =
static_cast<decltype(opt->rep.optimize_key_common_prefix)>(v);
}
int rocksdb_block_based_options_get_optimize_key_common_prefix(
rocksdb_block_based_table_options_t* opt) {
return static_cast<int>(opt->rep.optimize_key_common_prefix);
}
void rocksdb_block_based_options_set_enable_index_compression(
rocksdb_block_based_table_options_t* opt, unsigned char v) {
opt->rep.enable_index_compression = v;
+178
View File
@@ -189,6 +189,57 @@ static void CheckMultiGetValues(size_t num_keys, char** values,
}
}
static void CheckPinnableMultiGetFound(
const rocksdb_pinnable_multi_get_t* multi_get, size_t index,
const char* expected, size_t expected_size) {
const char* value;
const char* error;
size_t value_size;
size_t error_size;
int result = rocksdb_pinnable_multi_get_result(
multi_get, index, &value, &value_size, &error, &error_size);
CheckCondition(result == rocksdb_pinnable_multi_get_found);
CheckCondition(value_size == expected_size);
CheckCondition(error == NULL);
CheckCondition(error_size == 0);
if (expected_size > 0) {
CheckCondition(value != NULL);
CheckCondition(memcmp(value, expected, expected_size) == 0);
}
}
static void CheckPinnableMultiGetNotFound(
const rocksdb_pinnable_multi_get_t* multi_get, size_t index) {
const char* value;
const char* error;
size_t value_size;
size_t error_size;
int result = rocksdb_pinnable_multi_get_result(
multi_get, index, &value, &value_size, &error, &error_size);
CheckCondition(result == rocksdb_pinnable_multi_get_not_found);
CheckCondition(value == NULL);
CheckCondition(value_size == 0);
CheckCondition(error == NULL);
CheckCondition(error_size == 0);
}
static void CheckPinnableMultiGetError(
const rocksdb_pinnable_multi_get_t* multi_get, size_t index,
const char* expected) {
const char* value;
const char* error;
size_t value_size;
size_t error_size;
int result = rocksdb_pinnable_multi_get_result(
multi_get, index, &value, &value_size, &error, &error_size);
CheckCondition(result == rocksdb_pinnable_multi_get_error);
CheckCondition(value == NULL);
CheckCondition(value_size == 0);
CheckCondition(error != NULL);
CheckCondition(error_size == strlen(error));
CheckCondition(strstr(error, expected) != NULL);
}
static void CheckIter(rocksdb_iterator_t* iter, const char* key,
const char* val) {
size_t len;
@@ -834,6 +885,39 @@ static char* MergeOperatorPartialMerge(void* arg, const char* key,
return result;
}
static char* FailingMergeOperatorFullMerge(
void* arg, const char* key, size_t key_length, const char* existing_value,
size_t existing_value_length, const char* const* operands_list,
const size_t* operands_list_length, int num_operands,
unsigned char* success, size_t* new_value_length) {
(void)arg;
(void)key;
(void)key_length;
(void)existing_value;
(void)existing_value_length;
(void)operands_list;
(void)operands_list_length;
(void)num_operands;
*success = 0;
*new_value_length = 0;
return NULL;
}
static char* FailingMergeOperatorPartialMerge(
void* arg, const char* key, size_t key_length,
const char* const* operands_list, const size_t* operands_list_length,
int num_operands, unsigned char* success, size_t* new_value_length) {
(void)arg;
(void)key;
(void)key_length;
(void)operands_list;
(void)operands_list_length;
(void)num_operands;
*success = 0;
*new_value_length = 0;
return NULL;
}
static void CheckTxnGet(rocksdb_transaction_t* txn,
const rocksdb_readoptions_t* options, const char* key,
const char* expected) {
@@ -2639,12 +2723,17 @@ int main(int argc, char** argv) {
// use dbpathname2 as the cf_path for "cf1"
rocksdb_dbpath_t* dbpath2;
char dbpathname2[200];
rocksdb_mergeoperator_t* failing_merge_operator;
snprintf(dbpathname2, sizeof(dbpathname2),
"%s/rocksdb_c_test-%" PRIu64 "-dbpath2", GetTempDir(),
GetTestId());
dbpath2 = rocksdb_dbpath_create(dbpathname2, 1024 * 1024);
const rocksdb_dbpath_t* cf_paths[1] = {dbpath2};
rocksdb_options_set_cf_paths(cf_options_2, cf_paths, 1);
failing_merge_operator = rocksdb_mergeoperator_create(
NULL, MergeOperatorDestroy, FailingMergeOperatorFullMerge,
FailingMergeOperatorPartialMerge, NULL, MergeOperatorName);
rocksdb_options_set_merge_operator(cf_options_2, failing_merge_operator);
const char* cf_names[2] = {"default", "cf1"};
const rocksdb_options_t* cf_opts[2] = {cf_options_1, cf_options_2};
@@ -3167,6 +3256,95 @@ int main(int argc, char** argv) {
}
}
{
rocksdb_slice_t pinned_keys[6] = {{"box", 3}, {"missing", 7},
{"empty", 5}, {"merge-error", 11},
{"box", 3}, {"owned", 5}};
rocksdb_pinnable_multi_get_t* multi_get;
const char* value;
const char* error;
size_t value_size;
size_t error_size;
int result;
rocksdb_put_cf(db, woptions, handles[1], "empty", 5, "", 0, &err);
CheckNoError(err);
rocksdb_put_cf(db, woptions, handles[1], "merge-error", 11, "base", 4,
&err);
CheckNoError(err);
rocksdb_merge_cf(db, woptions, handles[1], "merge-error", 11, "operand",
7, &err);
CheckNoError(err);
rocksdb_put_cf(db, woptions, handles[1], "owned", 5, "before", 6, &err);
CheckNoError(err);
multi_get = rocksdb_batched_multi_get_pinned_cf(db, roptions, handles[1],
6, pinned_keys, 0);
CheckCondition(rocksdb_pinnable_multi_get_count(multi_get) == 6);
CheckPinnableMultiGetFound(multi_get, 0, "c", 1);
CheckPinnableMultiGetNotFound(multi_get, 1);
CheckPinnableMultiGetFound(multi_get, 2, "", 0);
CheckPinnableMultiGetError(multi_get, 3, "Merge operator failed");
CheckPinnableMultiGetFound(multi_get, 4, "c", 1);
CheckPinnableMultiGetFound(multi_get, 5, "before", 6);
value = "unchanged";
error = "unchanged";
value_size = 9;
error_size = 9;
result = rocksdb_pinnable_multi_get_result(
multi_get, 6, &value, &value_size, &error, &error_size);
CheckCondition(result == rocksdb_pinnable_multi_get_out_of_bounds);
CheckCondition(value == NULL);
CheckCondition(value_size == 0);
CheckCondition(error == NULL);
CheckCondition(error_size == 0);
result = rocksdb_pinnable_multi_get_result(
multi_get, 5, &value, &value_size, &error, &error_size);
CheckCondition(result == rocksdb_pinnable_multi_get_found);
const char* borrowed_value = value;
size_t borrowed_value_size = value_size;
result = rocksdb_pinnable_multi_get_result(
multi_get, 3, &value, &value_size, &error, &error_size);
CheckCondition(result == rocksdb_pinnable_multi_get_error);
const char* borrowed_error = error;
size_t borrowed_error_size = error_size;
rocksdb_put_cf(db, woptions, handles[1], "owned", 5, "after", 5, &err);
CheckNoError(err);
CheckCondition(borrowed_value_size == 6);
CheckCondition(memcmp(borrowed_value, "before", 6) == 0);
CheckCondition(borrowed_error_size == strlen(borrowed_error));
CheckCondition(strstr(borrowed_error, "Merge operator failed") != NULL);
rocksdb_pinnable_multi_get_destroy(multi_get);
}
{
rocksdb_slice_t sorted_keys[4] = {
{"box", 3}, {"box", 3}, {"empty", 5}, {"missing", 7}};
rocksdb_pinnable_multi_get_t* multi_get =
rocksdb_batched_multi_get_pinned_cf(db, roptions, handles[1], 4,
sorted_keys, 1);
CheckCondition(rocksdb_pinnable_multi_get_count(multi_get) == 4);
CheckPinnableMultiGetFound(multi_get, 0, "c", 1);
CheckPinnableMultiGetFound(multi_get, 1, "c", 1);
CheckPinnableMultiGetFound(multi_get, 2, "", 0);
CheckPinnableMultiGetNotFound(multi_get, 3);
rocksdb_pinnable_multi_get_destroy(multi_get);
multi_get = rocksdb_batched_multi_get_pinned_cf(db, roptions, handles[1],
0, sorted_keys, 1);
CheckCondition(rocksdb_pinnable_multi_get_count(multi_get) == 0);
rocksdb_pinnable_multi_get_destroy(multi_get);
}
rocksdb_delete_cf(db, woptions, handles[1], "empty", 5, &err);
CheckNoError(err);
rocksdb_delete_cf(db, woptions, handles[1], "merge-error", 11, &err);
CheckNoError(err);
rocksdb_delete_cf(db, woptions, handles[1], "owned", 5, &err);
CheckNoError(err);
{
unsigned char value_found = 0;
+11 -8
View File
@@ -1281,15 +1281,15 @@ Compaction* ColumnFamilyData::PickCompaction(
}
bool ColumnFamilyData::RangeOverlapWithCompaction(
const Slice& smallest_user_key, const Slice& largest_user_key,
int level) const {
const Slice& smallest_user_key, const Slice& largest_user_key, int level,
bool range_limit_exclusive) const {
return compaction_picker_->RangeOverlapWithCompaction(
smallest_user_key, largest_user_key, level);
smallest_user_key, largest_user_key, level, range_limit_exclusive);
}
Status ColumnFamilyData::RangesOverlapWithMemtables(
const autovector<UserKeyRange>& ranges, SuperVersion* super_version,
bool allow_data_in_errors, bool* overlap) {
bool allow_data_in_errors, bool* overlap, bool range_limit_exclusive) {
assert(overlap != nullptr);
*overlap = false;
// Create an InternalIterator over all unflushed memtables
@@ -1334,12 +1334,15 @@ Status ColumnFamilyData::RangesOverlapWithMemtables(
}
if (status.ok()) {
const int limit_cmp = memtable_iter->Valid()
? ucmp->CompareWithoutTimestamp(
seek_result.user_key, ranges[i].limit)
: 1;
if (memtable_iter->Valid() &&
ucmp->CompareWithoutTimestamp(seek_result.user_key,
ranges[i].limit) <= 0) {
(range_limit_exclusive ? limit_cmp < 0 : limit_cmp <= 0)) {
*overlap = true;
} else if (range_del_agg.IsRangeOverlapped(ranges[i].start,
ranges[i].limit)) {
} else if (range_del_agg.IsRangeOverlapped(
ranges[i].start, ranges[i].limit, range_limit_exclusive)) {
*overlap = true;
}
}
+49 -5
View File
@@ -29,6 +29,7 @@
#include "trace_replay/block_cache_tracer.h"
#include "util/cast_util.h"
#include "util/hash_containers.h"
#include "util/mutexlock.h"
#include "util/thread_local.h"
namespace ROCKSDB_NAMESPACE {
@@ -444,8 +445,8 @@ class ColumnFamilyData {
// Check if the passed range overlap with any running compactions.
// REQUIRES: DB mutex held
bool RangeOverlapWithCompaction(const Slice& smallest_user_key,
const Slice& largest_user_key,
int level) const;
const Slice& largest_user_key, int level,
bool range_limit_exclusive) const;
// Check if the passed ranges overlap with any unflushed memtables
// (immutable or mutable).
@@ -456,7 +457,8 @@ class ColumnFamilyData {
// Thread-safe
Status RangesOverlapWithMemtables(const autovector<UserKeyRange>& ranges,
SuperVersion* super_version,
bool allow_data_in_errors, bool* overlap);
bool allow_data_in_errors, bool* overlap,
bool range_limit_exclusive);
// A flag to tell a manual compaction is to compact all levels together
// instead of a specific level.
@@ -520,6 +522,32 @@ class ColumnFamilyData {
bool queued_for_flush() { return queued_for_flush_; }
bool queued_for_compaction() { return queued_for_compaction_; }
int compaction_aborted() const {
return compaction_aborted_.obj_.load(std::memory_order_acquire);
}
void inc_compaction_aborted(InstrumentedMutex* db_mutex) {
db_mutex->AssertHeld();
compaction_aborted_.obj_.fetch_add(1, std::memory_order_release);
}
int dec_compaction_aborted(InstrumentedMutex* db_mutex) {
db_mutex->AssertHeld();
return compaction_aborted_.obj_.fetch_sub(1, std::memory_order_acq_rel) - 1;
}
int num_in_flight_compactions(InstrumentedMutex* db_mutex) const {
db_mutex->AssertHeld();
return num_in_flight_compactions_;
}
void inc_num_in_flight_compactions(InstrumentedMutex* db_mutex) {
db_mutex->AssertHeld();
++num_in_flight_compactions_;
}
void dec_num_in_flight_compactions(InstrumentedMutex* db_mutex) {
db_mutex->AssertHeld();
assert(num_in_flight_compactions_ > 0);
--num_in_flight_compactions_;
}
static std::pair<WriteStallCondition, WriteStallCause>
GetWriteStallConditionAndCause(
int num_unflushed_memtables, int num_l0_files,
@@ -711,10 +739,26 @@ class ColumnFamilyData {
// If true --> this ColumnFamily is currently present in DBImpl::flush_queue_
bool queued_for_flush_;
// If true --> this ColumnFamily is currently present in
// DBImpl::compaction_queue_
// If true, this ColumnFamily is in DBImpl::compaction_queue_ or its parked
// compaction set.
bool queued_for_compaction_;
// Read outside the DB mutex by compaction workers (acquire-loaded roughly
// every 1024 records in ProcessKeyValueCompaction). Given its own cache line
// via CacheAlignedWrapper to avoid false sharing with the DB-mutex-written
// fields packed around it.
CacheAlignedWrapper<std::atomic<int>> compaction_aborted_{};
// Per-CF count of compactions "in flight" for AbortCompactions(): a picked
// Compaction (or one CompactFiles call), tracked from
// BeginInFlightCompaction() until EndInFlightCompaction() at the end of
// BackgroundCompaction, so it covers the NotifyOnCompactionCompleted window
// that CompactionPicker's compactions_in_progress_ set does not. NOT the same
// as DBImpl::num_running_compactions_ (which counts BackgroundCallCompaction
// worker invocations, backs the public rocksdb.num-running-compactions
// property, and excludes CompactFiles). Protected by the DB mutex.
int num_in_flight_compactions_{0};
uint64_t prev_compaction_needed_bytes_;
// if the database was opened with 2pc enabled
+5 -3
View File
@@ -491,7 +491,8 @@ int Compaction::GetProximalLevel() const { return proximal_level_; }
// smallest_key and largest_key include timestamps if user-defined timestamp is
// enabled.
bool Compaction::OverlapProximalLevelOutputRange(
const Slice& smallest_key, const Slice& largest_key) const {
const Slice& smallest_key, const Slice& largest_key,
bool range_limit_exclusive) const {
if (!SupportsPerKeyPlacement()) {
return false;
}
@@ -506,10 +507,11 @@ bool Compaction::OverlapProximalLevelOutputRange(
const Comparator* ucmp =
input_vstorage_->InternalComparator()->user_comparator();
const int largest_cmp = ucmp->CompareWithoutTimestamp(
largest_key, proximal_level_smallest_.user_key());
return ucmp->CompareWithoutTimestamp(
smallest_key, proximal_level_largest_.user_key()) <= 0 &&
ucmp->CompareWithoutTimestamp(
largest_key, proximal_level_smallest_.user_key()) >= 0;
(range_limit_exclusive ? largest_cmp > 0 : largest_cmp >= 0);
}
// key includes timestamp if user-defined timestamp is enabled.
+2 -1
View File
@@ -397,7 +397,8 @@ class Compaction {
// Both smallest_key and largest_key include timestamps if user-defined
// timestamp is enabled.
bool OverlapProximalLevelOutputRange(const Slice& smallest_key,
const Slice& largest_key) const;
const Slice& largest_key,
bool range_limit_exclusive) const;
// For testing purposes, check that a key is within proximal level
// output range for per_key_placement feature, which is safe to place the key
+24 -15
View File
@@ -1909,8 +1909,7 @@ void CompactionIterator::PrepareOutput() {
// KeyNotExistsBeyondOutputLevel() return true?
if (Valid() && bottommost_level_ &&
DefinitelyInSnapshot(ikey_.sequence, earliest_snapshot_) &&
ikey_.type != kTypeMerge && current_key_committed_ &&
ikey_.sequence <= preserve_seqno_after_ && !is_range_del_) {
ikey_.type != kTypeMerge && current_key_committed_ && !is_range_del_) {
assert(compaction_ != nullptr && !compaction_->allow_ingest_behind());
if (ikey_.type == kTypeDeletion ||
(ikey_.type == kTypeSingleDeletion && timestamp_size_ == 0)) {
@@ -1932,19 +1931,29 @@ void CompactionIterator::PrepareOutput() {
}
bool zeroed_seqno = false;
if (!timestamp_size_) {
current_key_.UpdateInternalKey(0, ikey_.type);
zeroed_seqno = true;
} else if (full_history_ts_low_ && cmp_with_history_ts_low_ < 0) {
// For UDT, the seqno and timestamp could only be zeroed out after the
// key is below history_ts_low_.
// For the same user key (excluding timestamp), the timestamp-based
// history can be collapsed to save some space if the timestamp is
// older than *full_history_ts_low_.
const std::string kTsMin(timestamp_size_, static_cast<char>(0));
const Slice ts_slice = kTsMin;
ikey_.SetTimestamp(ts_slice);
current_key_.UpdateInternalKey(0, ikey_.type, &ts_slice);
// Whether the sequence number can actually be zeroed out here is decided
// by the shared BottommostSeqnoCanBeZeroed() predicate, which the
// bottommost-file marking logic
// (VersionStorageInfo::ComputeBottommostFilesMarkedForCompaction) also
// consults. Keeping both on one predicate ensures a file is only marked
// for a compaction that can make progress; otherwise the file would be
// re-marked forever (infinite compaction loop).
if (BottommostSeqnoCanBeZeroed(
ikey_.sequence, preserve_seqno_after_, timestamp_size_,
full_history_ts_low_ != nullptr, cmp_with_history_ts_low_ < 0)) {
if (!timestamp_size_) {
current_key_.UpdateInternalKey(0, ikey_.type);
} else {
// For UDT, the seqno and timestamp could only be zeroed out after the
// key is below history_ts_low_.
// For the same user key (excluding timestamp), the timestamp-based
// history can be collapsed to save some space if the timestamp is
// older than *full_history_ts_low_.
const std::string kTsMin(timestamp_size_, static_cast<char>(0));
const Slice ts_slice = kTsMin;
ikey_.SetTimestamp(ts_slice);
current_key_.UpdateInternalKey(0, ikey_.type, &ts_slice);
}
zeroed_seqno = true;
}
+43 -6
View File
@@ -152,7 +152,8 @@ CompactionJob::CompactionJob(
const std::atomic<int>& compaction_aborted, const std::string& db_id,
const std::string& db_session_id, std::string full_history_ts_low,
std::string trim_ts, BlobFileCompletionCallback* blob_callback,
int* bg_compaction_scheduled, int* bg_bottom_compaction_scheduled)
int* bg_compaction_scheduled, int* bg_bottom_compaction_scheduled,
std::atomic<int>* num_running_remote_compactions)
: compact_(new CompactionState(compaction)),
internal_stats_(compaction->compaction_reason(), 1),
db_options_(db_options),
@@ -198,7 +199,8 @@ CompactionJob::CompactionJob(
blob_callback_(blob_callback),
extra_num_subcompaction_threads_reserved_(0),
bg_compaction_scheduled_(bg_compaction_scheduled),
bg_bottom_compaction_scheduled_(bg_bottom_compaction_scheduled) {
bg_bottom_compaction_scheduled_(bg_bottom_compaction_scheduled),
num_running_remote_compactions_(num_running_remote_compactions) {
assert(job_stats_ != nullptr);
assert(log_buffer_ != nullptr);
assert(job_context);
@@ -393,6 +395,24 @@ void CompactionJob::Prepare(
std::min(preclude_last_level_min_seqno, preserve_time_min_seqno);
#endif
// For a bottommost-file compaction, which exists only because the marker
// (VersionStorageInfo::ComputeBottommostFilesMarkedForCompaction) decided the
// file's largest sequence number could be zeroed, honor that same
// version-carried preserve boundary here. The marker reads DBImpl's live,
// DB-wide seqno->time mapping, whereas the value computed above is derived
// only from the selected file's persisted mapping, which can be sparse or
// empty (e.g. a file written before preserve/preclude was enabled). Taking
// the max ensures this compaction can zero at least the sequence numbers the
// marker considered zeroable, so a marked file makes progress instead of
// being re-marked forever (infinite compaction loop). Other compaction
// reasons keep the per-input-file boundary so their seqno-zeroing behavior is
// unchanged. When preserve/preclude is inactive the version value is
// kMaxSequenceNumber, making this a no-op.
if (c->compaction_reason() == CompactionReason::kBottommostFiles) {
preserve_time_min_seqno = std::max(preserve_time_min_seqno,
storage_info->GetPreserveTimeMinSeqno());
}
// Preserve sequence numbers for preserved write times and snapshots, though
// the specific sequence number of the earliest snapshot can be zeroed.
preserve_seqno_after_ =
@@ -421,6 +441,17 @@ void CompactionJob::Prepare(
c->GetKeepInLastLevelThroughSeqno());
options_file_number_ = versions_->options_file_number();
// For remote compaction hardening: capture the current MANIFEST position as a
// floor for the worker's recovery. manifest_file_size() is a physical byte
// offset that VersionSet::ProcessManifestWrites records under the DB mutex
// only after the whole version-edit batch is written and sync'd, so it is a
// durable, complete-batch offset -- never mid-record or mid-atomic-group --
// at or past the compaction's (already committed) input version.
if (mutable_db_options_copy_.remote_compaction_manifest_floor) {
min_manifest_file_number_ = versions_->manifest_file_number();
min_manifest_file_size_ = versions_->manifest_file_size();
}
}
void CompactionJob::MaybeAssignCompactionProgressAndWriter(
@@ -1668,8 +1699,12 @@ Status CompactionJob::ProcessKeyValue(
const uint64_t kCronEveryMask = (1 << 10) - 1;
[[maybe_unused]] const std::optional<const Slice> end = sub_compact->end;
// Check for abort signal before starting key processing
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
// Check for abort signal before starting key processing. The DB-wide
// (AbortAllCompactions) and per-CF (AbortCompactions) counters compose here;
// either aborts this job. See DB::AbortCompactions() in db.h for how the
// abort knobs relate to each other and to the other compaction controls.
if (compaction_aborted_.load(std::memory_order_acquire) > 0 ||
cfd->compaction_aborted() > 0) {
return Status::Incomplete(Status::SubCode::kCompactionAborted);
}
@@ -1691,7 +1726,8 @@ Status CompactionJob::ProcessKeyValue(
// Periodic cron operations: stats update, abort check.
if ((num_records & kCronEveryMask) == kCronEveryMask) {
// Check for abort signal periodically
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
if (compaction_aborted_.load(std::memory_order_acquire) > 0 ||
cfd->compaction_aborted() > 0) {
status = Status::Incomplete(Status::SubCode::kCompactionAborted);
break;
}
@@ -2613,7 +2649,8 @@ Status CompactionJob::OpenCompactionOutputFile(SubcompactionState* sub_compact,
bottommost_level_, TableFileCreationReason::kCompaction,
0 /* oldest_key_time */, current_time, db_id_, db_session_id_,
sub_compact->compaction->max_output_file_size(), file_number,
proximal_after_seqno_ /*last_level_inclusive_max_seqno_threshold*/);
proximal_after_seqno_ /*last_level_inclusive_max_seqno_threshold*/,
dbname_ /*db_name*/, IsRemoteCompaction() /*is_remote_compaction*/);
outputs.NewBuilder(tboptions);
+33 -2
View File
@@ -40,7 +40,7 @@
#include "rocksdb/db.h"
#include "rocksdb/env.h"
#include "rocksdb/memtablerep.h"
#include "rocksdb/transaction_log.h"
#include "rocksdb/wal_iterator.h"
#include "util/autovector.h"
#include "util/stop_watch.h"
#include "util/thread_local.h"
@@ -166,7 +166,8 @@ class CompactionJob {
std::string full_history_ts_low = "", std::string trim_ts = "",
BlobFileCompletionCallback* blob_callback = nullptr,
int* bg_compaction_scheduled = nullptr,
int* bg_bottom_compaction_scheduled = nullptr);
int* bg_bottom_compaction_scheduled = nullptr,
std::atomic<int>* num_running_remote_compactions = nullptr);
virtual ~CompactionJob();
@@ -500,6 +501,12 @@ class CompactionJob {
int* bg_compaction_scheduled_;
int* bg_bottom_compaction_scheduled_;
// Stores the pointer to DBImpl::num_running_remote_compactions_, backing the
// public rocksdb.num-running-remote-compactions property. Null when the job
// 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_;
// Stores the sequence number to time mapping gathered from all input files
// it also collects the smallest_seqno -> oldest_ancester_time from the SST.
SeqnoToTimeMapping seqno_to_time_mapping_;
@@ -517,12 +524,23 @@ class CompactionJob {
// Setting this requires DBMutex.
uint64_t options_file_number_ = 0;
// MANIFEST position (file number and size, in bytes) captured in Prepare()
// (mutex held) to send to the remote worker as a floor. Zero when
// DBOptions::remote_compaction_manifest_floor is off (or this is not a remote
// compaction), meaning no floor is sent. Setting these requires DBMutex.
uint64_t min_manifest_file_number_ = 0;
uint64_t min_manifest_file_size_ = 0;
// 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);
// True if this job builds output files as a remote (offloaded)
// CompactionService worker. Overridden by CompactionServiceCompactionJob.
virtual bool IsRemoteCompaction() const { return false; }
// 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.
@@ -592,6 +610,18 @@ struct CompactionServiceInput {
uint64_t options_file_number = 0;
// MANIFEST position (file number and size, in bytes) the primary scheduled
// this compaction from, used as a floor by the remote worker: it refuses to
// reconstruct the compaction against an older MANIFEST view (an older file
// number, or the same file number read to a smaller size) and falls back to
// local compaction. A non-zero min_manifest_file_number also signals the
// worker to use "trust the MANIFEST" recovery. Zero means the primary did not
// provide a floor (kill switch off, or an older primary); the worker then
// uses the original recovery with no floor check.
// See DBOptions::remote_compaction_manifest_floor.
uint64_t min_manifest_file_number = 0;
uint64_t min_manifest_file_size = 0;
// serialization interface to read and write the object
static Status Read(const std::string& data_str, CompactionServiceInput* obj);
Status Write(std::string* output);
@@ -730,6 +760,7 @@ class CompactionServiceCompactionJob : private CompactionJob {
private:
// Get table file name in output_path
std::string GetTableFileName(uint64_t file_number) override;
bool IsRemoteCompaction() const override { return true; }
// Specific the compaction output path, otherwise it uses default DB path
const std::string output_path_;
+34 -7
View File
@@ -267,11 +267,22 @@ bool CompactionOutputs::ShouldStopBefore(const CompactionIterator& c_iter) {
return true;
}
// If there's user defined partitioner, check that first
if (partitioner_ && partitioner_->ShouldPartition(PartitionerRequest(
last_key_for_partitioner_, c_iter.user_key(),
current_output_file_size_)) == kRequired) {
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;
}
}
}
// files output to Level 0 won't be split
@@ -401,11 +412,27 @@ Status CompactionOutputs::AddToOutput(
if (!s.ok()) {
return s;
}
// A new output file starts a fresh partition-tracking window. Establish the
// partitioner fast path from this file's first key: if the partitioner can
// express its next boundary as "first key not sharing this prefix", cache
// that prefix; otherwise fall back to per-key ShouldPartition(). This is
// the only place the prefix needs to be (re)derived, since a partition
// boundary always forces a file cut, and re-deriving on non-partition cuts
// just recomputes the same still-valid prefix.
if (partitioner_) {
OptSlice prefix =
partitioner_->ShouldPartitionByPrefix(c_iter.user_key());
partitioner_prefix_valid_ = prefix.has_value();
if (partitioner_prefix_valid_) {
partitioner_prefix_.assign(prefix->data(), prefix->size());
}
}
}
// c_iter may emit range deletion keys, so update `last_key_for_partitioner_`
// here before returning below when `is_range_del` is true
if (partitioner_) {
// here before returning below when `is_range_del` is true. Only needed in the
// fallback mode; the prefix fast path does not use it.
if (partitioner_ && !partitioner_prefix_valid_) {
last_key_for_partitioner_.assign(c_iter.user_key().data_,
c_iter.user_key().size_);
}
+12 -1
View File
@@ -349,8 +349,19 @@ class CompactionOutputs {
const bool is_proximal_level_;
// partitioner information
std::string last_key_for_partitioner_;
std::unique_ptr<SstPartitioner> partitioner_;
// Previous user key, only maintained (copied per key) in the fallback mode
// (see below).
std::string last_key_for_partitioner_;
// Fast path for partitioners that support ShouldPartitionByPrefix(): when
// partitioner_prefix_valid_ is true, all keys in the current output file
// share partitioner_prefix_, and a partition boundary is requested at the
// first future key that does not start with that prefix. This is (re)derived
// once per output file from its first key, avoiding a per-key copy and
// virtual ShouldPartition() call. When false, we fall back to comparing
// against last_key_for_partitioner_ via ShouldPartition() on every key.
std::string partitioner_prefix_;
bool partitioner_prefix_valid_ = false;
// A flag determines if this subcompaction has been split by the cursor
// for RoundRobin compaction
+13 -9
View File
@@ -319,21 +319,22 @@ bool CompactionPicker::ExpandInputsToCleanCut(const std::string& /*cf_name*/,
}
bool CompactionPicker::RangeOverlapWithCompaction(
const Slice& smallest_user_key, const Slice& largest_user_key,
int level) const {
const Slice& smallest_user_key, const Slice& largest_user_key, int level,
bool range_limit_exclusive) const {
const Comparator* ucmp = icmp_->user_comparator();
for (Compaction* c : compactions_in_progress_) {
const int largest_cmp = ucmp->CompareWithoutTimestamp(
largest_user_key, c->GetSmallestUserKey());
if (c->output_level() == level &&
ucmp->CompareWithoutTimestamp(smallest_user_key,
c->GetLargestUserKey()) <= 0 &&
ucmp->CompareWithoutTimestamp(largest_user_key,
c->GetSmallestUserKey()) >= 0) {
(range_limit_exclusive ? largest_cmp > 0 : largest_cmp >= 0)) {
// Overlap
return true;
}
if (c->SupportsPerKeyPlacement()) {
if (c->OverlapProximalLevelOutputRange(smallest_user_key,
largest_user_key)) {
if (c->OverlapProximalLevelOutputRange(
smallest_user_key, largest_user_key, range_limit_exclusive)) {
return true;
}
}
@@ -364,7 +365,8 @@ bool CompactionPicker::FilesRangeOverlapWithCompaction(
if (proximal_level != Compaction::kInvalidLevel) {
if (ioptions_.compaction_style == kCompactionStyleUniversal) {
if (RangeOverlapWithCompaction(smallest.user_key(), largest.user_key(),
proximal_level)) {
proximal_level,
/*range_limit_exclusive=*/false)) {
return true;
}
} else {
@@ -372,14 +374,16 @@ bool CompactionPicker::FilesRangeOverlapWithCompaction(
GetRange(inputs, &proximal_smallest, &proximal_largest, level);
if (RangeOverlapWithCompaction(proximal_smallest.user_key(),
proximal_largest.user_key(),
proximal_level)) {
proximal_level,
/*range_limit_exclusive=*/false)) {
return true;
}
}
}
return RangeOverlapWithCompaction(smallest.user_key(), largest.user_key(),
level);
level,
/*range_limit_exclusive=*/false);
}
// Returns true if any one of specified files are being compacted
+2 -2
View File
@@ -154,8 +154,8 @@ class CompactionPicker {
// Return true if the passed key range overlap with a compaction output
// that is currently running.
bool RangeOverlapWithCompaction(const Slice& smallest_user_key,
const Slice& largest_user_key,
int level) const;
const Slice& largest_user_key, int level,
bool range_limit_exclusive) const;
// Stores the minimal range that covers all entries in inputs in
// *smallest, *largest.
+139 -4
View File
@@ -90,7 +90,8 @@ class CompactionPickerTestBase : public testing::Test {
&icmp_, ucmp_, options_.num_levels, style, nullptr, false,
EpochNumberRequirement::kMustPresent, ioptions_.clock,
options_.bottommost_file_compaction_delay,
OffpeakTimeOption(mutable_db_options_.daily_offpeak_time_utc)));
OffpeakTimeOption(mutable_db_options_.daily_offpeak_time_utc),
PeriodicCompactionPhaseParams{}));
vstorage_->PrepareForVersionAppend(ioptions_, mutable_cf_options_);
}
@@ -101,7 +102,8 @@ class CompactionPickerTestBase : public testing::Test {
&icmp_, ucmp_, options_.num_levels, ioptions_.compaction_style,
vstorage_.get(), false, EpochNumberRequirement::kMustPresent,
ioptions_.clock, options_.bottommost_file_compaction_delay,
OffpeakTimeOption(mutable_db_options_.daily_offpeak_time_utc)));
OffpeakTimeOption(mutable_db_options_.daily_offpeak_time_utc),
PeriodicCompactionPhaseParams{}));
}
void DeleteVersionStorage() {
@@ -4162,6 +4164,18 @@ TEST_F(CompactionPickerU64TsTest, Overlap) {
CompactionOptions(), input_files, level, vstorage_.get(),
mutable_cf_options_, mutable_db_options_, /*output_path_id=*/0));
{
std::string range_start("400");
PutFixed64(&range_start, ts_of_smallest);
std::string range_limit(smallest);
PutFixed64(&range_limit, ts_of_smallest);
ASSERT_FALSE(level_compaction_picker.RangeOverlapWithCompaction(
range_start, range_limit, level, /*range_limit_exclusive=*/true));
ASSERT_TRUE(level_compaction_picker.RangeOverlapWithCompaction(
range_start, range_limit, level,
/*range_limit_exclusive=*/false));
}
{
// [600, ts=50000] to [600, ts=50000] is the range to check.
// ucmp->Compare(smallest_user_key, c->GetLargestUserKey()) > 0, but
@@ -4173,7 +4187,8 @@ TEST_F(CompactionPickerU64TsTest, Overlap) {
std::string user_key_with_ts2(largest);
PutFixed64(&user_key_with_ts2, ts_of_largest - 1);
ASSERT_TRUE(level_compaction_picker.RangeOverlapWithCompaction(
user_key_with_ts1, user_key_with_ts2, level));
user_key_with_ts1, user_key_with_ts2, level,
/*range_limit_exclusive=*/false));
}
{
// [500, ts=60000] to [500, ts=60000] is the range to check.
@@ -4186,7 +4201,8 @@ TEST_F(CompactionPickerU64TsTest, Overlap) {
std::string user_key_with_ts2(smallest);
PutFixed64(&user_key_with_ts2, ts_of_smallest + 1);
ASSERT_TRUE(level_compaction_picker.RangeOverlapWithCompaction(
user_key_with_ts1, user_key_with_ts2, level));
user_key_with_ts1, user_key_with_ts2, level,
/*range_limit_exclusive=*/false));
}
}
@@ -5058,6 +5074,41 @@ TEST_F(CompactionPickerTest, StandaloneRangeDeletionOnlyPicksOlderFiles) {
ASSERT_EQ(10U, compaction->input(1, 0)->fd.GetNumber());
}
// Tests for the seqno->time preserve window controlling bottommost file
// marking. A bottommost file whose largest seqno is still within the preserve
// window cannot have its seqno zeroed, so it must not be marked (otherwise the
// rewrite makes no progress and the file is re-marked forever -- an infinite
// loop).
TEST_F(CompactionPickerTest, BottommostFileNotMarkedWithinPreserveWindow) {
NewVersionStorage(6, kCompactionStyleLevel);
Add(5, 1U, "100", "200", /*file_size=*/1000, /*path_id=*/0,
/*smallest_seq=*/90, /*largest_seq=*/100);
// preserve_time_min_seqno = 100 => preserve_seqno_after = 99; largest_seqno
// 100 is within the window (> 99) and cannot be zeroed.
vstorage_->SetPreserveTimeMinSeqno(100);
UpdateVersionStorageInfo();
vstorage_->UpdateOldestSnapshot(/*oldest_snapshot_seqnum=*/200,
/*allow_ingest_behind=*/false, /*ucmp=*/ucmp_,
/*full_history_ts_low=*/"");
ASSERT_EQ(0U, vstorage_->BottommostFilesMarkedForCompaction().size());
}
TEST_F(CompactionPickerTest, BottommostFileMarkedBelowPreserveWindow) {
NewVersionStorage(6, kCompactionStyleLevel);
Add(5, 1U, "100", "200", /*file_size=*/1000, /*path_id=*/0,
/*smallest_seq=*/90, /*largest_seq=*/100);
// preserve_time_min_seqno = 150 => preserve_seqno_after = 149; largest_seqno
// 100 <= 149 so it can be zeroed and the file is marked as usual.
vstorage_->SetPreserveTimeMinSeqno(150);
UpdateVersionStorageInfo();
vstorage_->UpdateOldestSnapshot(/*oldest_snapshot_seqnum=*/200,
/*allow_ingest_behind=*/false, /*ucmp=*/ucmp_,
/*full_history_ts_low=*/"");
ASSERT_EQ(1U, vstorage_->BottommostFilesMarkedForCompaction().size());
ASSERT_EQ(1U, vstorage_->BottommostFilesMarkedForCompaction()[0]
.second->fd.GetNumber());
}
// Tests for full_history_ts_low parameter in compaction picker.
// The full_history_ts_low parameter is used to control bottommost file marking
// for compaction when user-defined timestamps (UDT) are enabled.
@@ -5137,6 +5188,90 @@ TEST_F(CompactionPickerU64TsTest,
ASSERT_EQ(0U, vstorage_->BottommostFilesMarkedForCompaction().size());
}
TEST_F(CompactionPickerU64TsTest,
BottommostNotMarkedWithEmptyFullHistoryTsLowAndUnknownMaxTs) {
// Regression test for an infinite bottommost compaction loop. A bottommost
// UDT file whose max_timestamp was not recorded in its FileMetaData (e.g. a
// file written by an older version, or one whose timestamp property is
// otherwise absent) must NOT be marked for compaction when
// full_history_ts_low is unset. Under UDT the only progress a bottommost
// compaction can make here is zeroing sequence numbers, which
// CompactionIterator does only when full_history_ts_low is set. With it unset
// the compaction cannot advance the file, so the rewritten output
// re-qualifies forever.
NewVersionStorage(6, kCompactionStyleLevel);
std::string ts_small = MakeU64Timestamp(50);
std::string ts_large = MakeU64Timestamp(100);
// Add a bottommost file with seqno < oldest_snapshot.
Add(5, 1U, "100", "200", /*file_size=*/1000, /*path_id=*/0,
/*smallest_seq=*/10, /*largest_seq=*/40,
/*compensated_file_size=*/1000,
/*marked_for_compact=*/false, Temperature::kUnknown,
kUnknownOldestAncesterTime, kUnknownNewestKeyTime, ts_small, ts_large);
// Simulate a file whose min/max timestamp was never recorded.
ASSERT_EQ(1U, vstorage_->LevelFiles(5).size());
FileMetaData* f = vstorage_->LevelFiles(5)[0];
f->min_timestamp.clear();
f->max_timestamp.clear();
UpdateVersionStorageInfo();
vstorage_->UpdateOldestSnapshot(
/*oldest_snapshot_seqnum=*/50,
/*allow_ingest_behind=*/false,
/*ucmp=*/ucmp_,
/*full_history_ts_low=*/"");
// No bottommost file can make progress with empty full_history_ts_low, so
// nothing is marked even though max_timestamp is unknown. Before the fix this
// file was marked, producing an infinite compaction loop.
ASSERT_EQ(0U, vstorage_->BottommostFilesMarkedForCompaction().size());
}
TEST_F(CompactionPickerU64TsTest,
BottommostMarkedWithSetFullHistoryTsLowAndUnknownMaxTs) {
// A bottommost UDT file whose max_timestamp was not recorded (e.g. written by
// an older version) must still be marked for a one-time backfill compaction
// when full_history_ts_low IS set: that compaction can collapse timestamp
// history / zero seqnos and record max_timestamp, so it makes progress
// (bounded, not a loop). This complements
// BottommostNotMarkedWithEmptyFullHistoryTsLowAndUnknownMaxTs, which covers
// the unset case where no progress is possible.
NewVersionStorage(6, kCompactionStyleLevel);
std::string ts_small = MakeU64Timestamp(50);
std::string ts_large = MakeU64Timestamp(100);
Add(5, 1U, "100", "200", /*file_size=*/1000, /*path_id=*/0,
/*smallest_seq=*/10, /*largest_seq=*/40,
/*compensated_file_size=*/1000,
/*marked_for_compact=*/false, Temperature::kUnknown,
kUnknownOldestAncesterTime, kUnknownNewestKeyTime, ts_small, ts_large);
// Simulate a file whose min/max timestamp was never recorded.
ASSERT_EQ(1U, vstorage_->LevelFiles(5).size());
FileMetaData* f = vstorage_->LevelFiles(5)[0];
f->min_timestamp.clear();
f->max_timestamp.clear();
UpdateVersionStorageInfo();
std::string full_history_ts_low = MakeU64Timestamp(500);
vstorage_->UpdateOldestSnapshot(
/*oldest_snapshot_seqnum=*/50,
/*allow_ingest_behind=*/false,
/*ucmp=*/ucmp_, full_history_ts_low);
// With full_history_ts_low set, the unknown-max_timestamp file is marked so a
// bounded compaction can backfill its metadata and reclaim obsolete data.
ASSERT_EQ(1U, vstorage_->BottommostFilesMarkedForCompaction().size());
ASSERT_EQ(1U, vstorage_->BottommostFilesMarkedForCompaction()[0]
.second->fd.GetNumber());
}
TEST_F(CompactionPickerU64TsTest, LevelPickCompactionWithFullHistoryTsLow) {
// Test that level compaction correctly passes full_history_ts_low
// and picks compaction appropriately
+27 -8
View File
@@ -15,6 +15,7 @@
#include "monitoring/thread_status_util.h"
#include "options/options_helper.h"
#include "rocksdb/utilities/options_type.h"
#include "util/defer.h"
namespace ROCKSDB_NAMESPACE {
class SubcompactionState;
@@ -49,6 +50,8 @@ CompactionJob::ProcessKeyValueCompactionWithCompactionService(
compaction_input.end =
compaction_input.has_end ? sub_compact->end->ToString() : "";
compaction_input.options_file_number = options_file_number_;
compaction_input.min_manifest_file_number = min_manifest_file_number_;
compaction_input.min_manifest_file_size = min_manifest_file_size_;
TEST_SYNC_POINT_CALLBACK(
"CompactionServiceJob::ProcessKeyValueCompactionWithCompactionService",
@@ -114,9 +117,6 @@ CompactionJob::ProcessKeyValueCompactionWithCompactionService(
break;
}
std::string debug_str_before_wait =
compaction->input_version()->DebugString(/*hex=*/true);
// TODO: Update CompactionService API to support abort and resume
// functionality. Currently, remote compaction jobs cannot be aborted via
// AbortAllCompactions() because the CompactionService interface lacks methods
@@ -129,18 +129,29 @@ CompactionJob::ProcessKeyValueCompactionWithCompactionService(
"[%s] [JOB %d] Waiting for remote compaction...",
compaction->column_family_data()->GetName().c_str(), job_id_);
std::string compaction_result_binary;
CompactionServiceJobStatus compaction_status =
db_options_.compaction_service->Wait(response.scheduled_job_id,
&compaction_result_binary);
CompactionServiceJobStatus compaction_status;
{
// Increment rocksdb.num-running-remote-compactions while this compaction
// service job is waiting in CompactionService::Wait().
std::atomic<int>* const num_running = num_running_remote_compactions_;
if (num_running != nullptr) {
num_running->fetch_add(1, std::memory_order_relaxed);
}
Defer decrement_num_running([num_running]() {
if (num_running != nullptr) {
num_running->fetch_sub(1, std::memory_order_relaxed);
}
});
compaction_status = db_options_.compaction_service->Wait(
response.scheduled_job_id, &compaction_result_binary);
}
if (compaction_status != CompactionServiceJobStatus::kSuccess) {
ROCKS_LOG_ERROR(
db_options_.info_log,
"[%s] [JOB %d] Wait() status is not kSuccess. "
"\nDebugString Before Wait():\n%s"
"\nDebugString After Wait():\n%s",
compaction->column_family_data()->GetName().c_str(), job_id_,
debug_str_before_wait.c_str(),
compaction->input_version()->DebugString(/*hex=*/true).c_str());
}
@@ -555,6 +566,14 @@ static std::unordered_map<std::string, OptionTypeInfo> cs_input_type_info = {
{offsetof(struct CompactionServiceInput, options_file_number),
OptionType::kUInt64T, OptionVerificationType::kNormal,
OptionTypeFlags::kNone}},
{"min_manifest_file_number",
{offsetof(struct CompactionServiceInput, min_manifest_file_number),
OptionType::kUInt64T, OptionVerificationType::kNormal,
OptionTypeFlags::kNone}},
{"min_manifest_file_size",
{offsetof(struct CompactionServiceInput, min_manifest_file_size),
OptionType::kUInt64T, OptionVerificationType::kNormal,
OptionTypeFlags::kNone}},
};
static std::unordered_map<std::string, OptionTypeInfo>
+510 -2
View File
@@ -3,11 +3,15 @@
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#include <atomic>
#include <memory>
#include "db/db_test_util.h"
#include "file/file_util.h"
#include "port/stack_trace.h"
#include "rocksdb/advanced_compression.h"
#include "rocksdb/filter_policy.h"
#include "rocksdb/utilities/object_registry.h"
#include "rocksdb/utilities/options_util.h"
#include "table/unique_id_impl.h"
#include "utilities/merge_operators/string_append/stringappend.h"
@@ -159,6 +163,14 @@ class MyTestCompactionService : public CompactionService {
compaction_num_.fetch_add(1);
if (s.ok()) {
return CompactionServiceJobStatus::kSuccess;
} else if (s.IsIncomplete() && !s.IsManualCompactionPaused()) {
// The remote worker declined the job (e.g. the MANIFEST floor check
// rejected an older/stale filesystem view). Mapping that decline to
// kUseLocal (retry locally) vs. kFailure (surface it) is the integrator's
// choice; this example picks kUseLocal to exercise the fallback path,
// while DbStressCompactionService deliberately picks kFailure. Real
// cancellation (kManualCompactionPaused) is handled as a failure.
return CompactionServiceJobStatus::kUseLocal;
} else {
return CompactionServiceJobStatus::kFailure;
}
@@ -360,6 +372,7 @@ class CompactionServiceTest : public DBTestBase {
TEST_F(CompactionServiceTest, BasicCompactions) {
Options options = CurrentOptions();
options.max_open_files = -1;
ReopenWithCompactionService(&options);
Statistics* primary_statistics = GetPrimaryStatistics();
@@ -394,8 +407,12 @@ TEST_F(CompactionServiceTest, BasicCompactions) {
ASSERT_EQ(primary_statistics->getTickerCount(REMOTE_COMPACT_WRITE_BYTES),
compactor_statistics->getTickerCount(COMPACT_WRITE_BYTES));
ASSERT_GE(primary_statistics->getTickerCount(REMOTE_COMPACT_READ_BYTES), 1);
ASSERT_GT(primary_statistics->getTickerCount(COMPACT_READ_BYTES),
primary_statistics->getTickerCount(REMOTE_COMPACT_READ_BYTES));
// The remote worker reads the full compaction inputs on demand, while the
// primary only re-reads the (smaller) output files for verify_table(), so
// remote read bytes dominate. (The worker no longer eagerly opens every file
// during secondary recovery, so its input reads are attributed accurately.)
ASSERT_GT(primary_statistics->getTickerCount(REMOTE_COMPACT_READ_BYTES),
primary_statistics->getTickerCount(COMPACT_READ_BYTES));
// compactor is already the remote side, which doesn't have remote
ASSERT_EQ(compactor_statistics->getTickerCount(REMOTE_COMPACT_READ_BYTES), 0);
ASSERT_EQ(compactor_statistics->getTickerCount(REMOTE_COMPACT_WRITE_BYTES),
@@ -480,6 +497,113 @@ TEST_F(CompactionServiceTest, BasicCompactions) {
SyncPoint::GetInstance()->DisableProcessing();
}
namespace {
// A CompressionManager that records, per GetCompressorForSST() call, whether
// the FilterBuildingContext reports the SST as being built by a remote
// (offloaded) compaction worker. Observations go into process-global counters
// because the remote worker reconstructs its own manager instance from the
// persisted OPTIONS file (same process in this test).
struct RemoteAwareObservations {
std::atomic<int> num_remote{0};
std::atomic<int> num_non_remote{0};
void Reset() {
num_remote.store(0);
num_non_remote.store(0);
}
};
RemoteAwareObservations& GetRemoteAwareObservations() {
static RemoteAwareObservations obs;
return obs;
}
class RemoteAwareCompressionManager : public CompressionManagerWrapper {
public:
RemoteAwareCompressionManager()
: CompressionManagerWrapper(GetBuiltinV2CompressionManager()) {}
static const char* kClassName() { return "RemoteAwareCompressionManager"; }
const char* Name() const override { return kClassName(); }
std::unique_ptr<Compressor> GetCompressorForSST(
const FilterBuildingContext& context, const CompressionOptions& opts,
CompressionType preferred) override {
if (context.is_remote_compaction) {
GetRemoteAwareObservations().num_remote.fetch_add(1);
} else {
GetRemoteAwareObservations().num_non_remote.fetch_add(1);
}
return CompressionManagerWrapper::GetCompressorForSST(context, opts,
preferred);
}
};
} // namespace
TEST_F(CompactionServiceTest, IsRemoteCompactionInCompressorContext) {
// Register so the remote worker's DB::OpenAndCompact can reconstruct the
// custom CompressionManager from the persisted OPTIONS file
// (CompactionServiceOptionsOverride does not carry a compression_manager).
ObjectLibrary::Default()->AddFactory<CompressionManager>(
RemoteAwareCompressionManager::kClassName(),
[](const std::string& /*uri*/, std::unique_ptr<CompressionManager>* guard,
std::string* /*errmsg*/) {
*guard = std::make_unique<RemoteAwareCompressionManager>();
return guard->get();
});
auto& obs = GetRemoteAwareObservations();
obs.Reset();
Options options = CurrentOptions();
options.compression_manager =
std::make_shared<RemoteAwareCompressionManager>();
ReopenWithCompactionService(&options);
GenerateTestData();
ASSERT_OK(dbfull()->TEST_WaitForCompact());
VerifyTestData();
auto my_cs = GetCompactionService();
ASSERT_GE(my_cs->GetCompactionNum(), 1);
// The offloaded worker built the compaction output SSTs and therefore must
// have observed is_remote_compaction == true in GetCompressorForSST().
ASSERT_GT(obs.num_remote.load(), 0);
// The primary's own flushes, seen by the same manager, must report false
ASSERT_GT(obs.num_non_remote.load(), 0);
Close();
}
TEST_F(CompactionServiceTest, IsRemoteCompactionFalseForLocalCompaction) {
// Same custom manager, but no compaction_service: everything runs locally,
// so GetCompressorForSST() must never see is_remote_compaction == true.
auto& obs = GetRemoteAwareObservations();
obs.Reset();
Options options = CurrentOptions();
options.compression_manager =
std::make_shared<RemoteAwareCompressionManager>();
DestroyAndReopen(options);
Random rnd(301);
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 100; j++) {
ASSERT_OK(Put(Key(i * 100 + j), rnd.RandomString(64)));
}
ASSERT_OK(Flush());
}
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
// Local flushes and compactions must all report not-remote.
ASSERT_GT(obs.num_non_remote.load(), 0);
ASSERT_EQ(obs.num_remote.load(), 0);
Close();
}
TEST_F(CompactionServiceTest, SkipWALRecoveryInOpenAndCompact) {
// Verify that OpenAndCompact skips WAL recovery when opening the secondary
// instance. WAL replay is unnecessary for remote compaction since it only
@@ -1761,6 +1885,109 @@ TEST_F(CompactionServiceTest, SubCompaction) {
ASSERT_GE(compaction_num, 2);
}
class PropertySamplingCompactionService : public MyTestCompactionService {
public:
using MyTestCompactionService::MyTestCompactionService;
void SetPrimaryDB(DB* db) { db_ = db; }
CompactionServiceJobStatus Wait(const std::string& scheduled_job_id,
std::string* result) override {
EXPECT_TRUE(
db_->GetIntProperty(DB::Properties::kNumRunningRemoteCompactions,
&num_running_while_waiting_));
return MyTestCompactionService::Wait(scheduled_job_id, result);
}
uint64_t GetNumRunningWhileWaiting() const {
return num_running_while_waiting_;
}
private:
DB* db_ = nullptr;
uint64_t num_running_while_waiting_ = 0;
};
TEST_F(CompactionServiceTest, NumRunningRemoteCompactionsProperty) {
Options options = CurrentOptions();
options.disable_auto_compactions = true;
options.env = env_;
auto statistics = CreateDBStatistics();
auto compaction_service = std::make_shared<PropertySamplingCompactionService>(
dbname_, options, statistics,
std::vector<std::shared_ptr<EventListener>>{},
std::vector<std::shared_ptr<TablePropertiesCollectorFactory>>{});
options.compaction_service = compaction_service;
DestroyAndReopen(options);
compaction_service->SetPrimaryDB(db_.get());
// Overlapping key ranges across files so the compaction is not a trivial
// move, which would bypass the compaction service entirely.
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 10; j++) {
ASSERT_OK(Put(Key(j), "value" + std::to_string(i * 10 + j)));
}
ASSERT_OK(Flush());
}
// No remote compaction is in flight yet.
uint64_t num_running = 0;
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningRemoteCompactions,
&num_running));
ASSERT_EQ(num_running, 0U);
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
ASSERT_GE(compaction_service->GetCompactionNum(), 1);
ASSERT_GE(compaction_service->GetNumRunningWhileWaiting(), 1U);
// The counter is back to 0 once the remote jobs are installed.
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningRemoteCompactions,
&num_running));
ASSERT_EQ(num_running, 0U);
}
TEST_F(CompactionServiceTest, NumRunningRemoteCompactionsPropertyLocalOnly) {
// Without a compaction_service configured, the property stays 0.
Options options = CurrentOptions();
options.disable_auto_compactions = true;
DestroyAndReopen(options);
// Overlapping key ranges across files so the compaction is not a trivial
// move, which would bypass the compaction service entirely.
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 10; j++) {
ASSERT_OK(Put(Key(j), "value" + std::to_string(i * 10 + j)));
}
ASSERT_OK(Flush());
}
std::atomic<int> property_samples{0};
std::atomic_bool observed_non_zero{false};
SyncPoint::GetInstance()->SetCallBack(
"CompactionJob::ProcessKeyValueCompaction()::Processing",
[&](void* /*arg*/) {
uint64_t num_running = 0;
EXPECT_TRUE(db_->GetIntProperty(
DB::Properties::kNumRunningRemoteCompactions, &num_running));
property_samples.fetch_add(1);
if (num_running != 0) {
observed_non_zero.store(true);
}
});
SyncPoint::GetInstance()->EnableProcessing();
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
ASSERT_GT(property_samples.load(), 0);
ASSERT_FALSE(observed_non_zero.load());
uint64_t num_running = 0;
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningRemoteCompactions,
&num_running));
ASSERT_EQ(num_running, 0U);
}
class PartialDeleteCompactionFilter : public CompactionFilter {
public:
CompactionFilter::Decision FilterV2(
@@ -1779,6 +2006,7 @@ class PartialDeleteCompactionFilter : public CompactionFilter {
TEST_F(CompactionServiceTest, CompactionFilter) {
Options options = CurrentOptions();
options.disable_auto_compactions = true;
std::unique_ptr<CompactionFilter> delete_comp_filter(
new PartialDeleteCompactionFilter());
options.compaction_filter = delete_comp_filter.get();
@@ -1953,6 +2181,286 @@ TEST_F(CompactionServiceTest, ConcurrentCompaction) {
ASSERT_EQ(FilesPerLevel(), "0,0,10");
}
TEST_F(CompactionServiceTest, RemoteCompactionMissingLowerFileTrustsManifest) {
// A remote compaction reconstructs its view of the LSM (which files sit below
// the output level) from the secondary's recovered MANIFEST version. That
// view drives correctness-critical decisions like whether a deletion
// tombstone can be dropped (Compaction::KeyNotExistsBeyondOutputLevel /
// IsBottommostLevel, which read input_version_'s VersionStorageInfo).
//
// If a non-input file that is still referenced by the MANIFEST is momentarily
// missing (as on a live primary), the OLD point-in-time recovery rolls back
// to an earlier version. Here that rollback drops the (later, whole-DB)
// manifest record entirely, so the remote worker can no longer find the
// compaction input and the job fails ("cannot find matched SST files") --
// i.e. it fails safe rather than mis-compacting. The fix makes recovery TRUST
// the manifest: it keeps the current version, the job completes, and the
// deleted key correctly stays deleted (the below-output tombstone is
// retained).
Options options = CurrentOptions();
options.num_levels = 7;
// -1 keeps the primary's descriptor for the (unlinked) lower file valid, so
// the final read below is served from the primary regardless of the deletion.
options.max_open_files = -1;
options.level0_file_num_compaction_trigger = 100; // no auto compaction
ReopenWithCompactionService(&options);
// Old value for "k", pushed down to L2 (below the compaction output level).
ASSERT_OK(Put("k", "OLD"));
ASSERT_OK(Flush());
MoveFilesToLevel(2); // L2 file contains k=OLD
// Newer tombstone for "k" at L0 (the compaction input).
ASSERT_OK(Delete("k"));
ASSERT_OK(Flush()); // L0 file contains Delete(k)
// Reopen so the MANIFEST is rewritten as current state before we perturb it.
ReopenWithColumnFamilies({kDefaultColumnFamilyName, "cf_1", "cf_2", "cf_3"},
options);
// Closing on reopen cancels pending remote jobs; re-arm the service.
GetCompactionService()->SetCanceled(false);
ColumnFamilyMetaData meta;
db_->GetColumnFamilyMetaData(&meta); // default CF
ASSERT_EQ(meta.levels[0].files.size(), 1);
ASSERT_EQ(meta.levels[2].files.size(), 1);
const auto& f_del = meta.levels[0].files[0];
const auto& f_old = meta.levels[2].files[0];
// Make the lower (L2) file momentarily unavailable to a fresh open, as a live
// primary would if it had compacted it away. It is NOT a compaction input.
ASSERT_OK(env_->DeleteFile(f_old.db_path + "/" + f_old.name));
// Remote-compact the L0 tombstone into L1. L2 (with k=OLD) is below the
// output level, so a correct compaction must retain the tombstone. Without
// the fix, recovery rolls back and this job fails; with the fix it completes.
std::string fname = f_del.db_path + "/" + f_del.name;
ASSERT_OK(db_->CompactFiles(CompactionOptions(), {fname}, 1));
// Deleted key stays deleted (tombstone retained above the still-present L2
// value, which the primary can still read via its open descriptor).
ASSERT_EQ("NOT_FOUND", Get("k"));
}
TEST_F(CompactionServiceTest, RemoteCompactionManifestFloorSendsPosition) {
// By default (remote_compaction_manifest_floor=true), the primary attaches a
// non-zero MANIFEST floor to every remote compaction request, and the worker
// runs its floor check. Here the worker recovers the same-or-later MANIFEST
// position the primary scheduled from, so the check accepts (the
// later-manifest positive case).
Options options = CurrentOptions();
options.disable_auto_compactions = true;
ReopenWithCompactionService(&options);
ASSERT_TRUE(db_->GetDBOptions().remote_compaction_manifest_floor);
GenerateTestData();
uint64_t floor_number = 0;
uint64_t floor_size = 0;
int floor_check_count = 0;
bool floor_check_rejected = false;
SyncPoint::GetInstance()->SetCallBack(
"CompactionServiceJob::ProcessKeyValueCompactionWithCompactionService",
[&](void* arg) {
auto* input = static_cast<CompactionServiceInput*>(arg);
floor_number = input->min_manifest_file_number;
floor_size = input->min_manifest_file_size;
});
SyncPoint::GetInstance()->SetCallBack(
"DBImplSecondary::OpenAndCompact::ManifestFloorCheck", [&](void* arg) {
floor_check_count++;
if (*static_cast<bool*>(arg)) {
floor_check_rejected = true;
}
});
SyncPoint::GetInstance()->EnableProcessing();
std::string start_str = Key(15);
std::string end_str = Key(45);
Slice start(start_str);
Slice end(end_str);
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), &start, &end));
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
// A floor was populated and the worker accepted its (later-or-equal) view.
ASSERT_GT(floor_number, 0U);
ASSERT_GT(floor_size, 0U);
ASSERT_GE(floor_check_count, 1);
ASSERT_FALSE(floor_check_rejected);
VerifyTestData();
}
TEST_F(CompactionServiceTest, RemoteCompactionManifestFloorRejectsStaleView) {
// If the worker recovers an OLDER MANIFEST view than the primary scheduled
// from (simulated here by inflating the floor above the real position), the
// floor check rejects the job and the primary falls back to a correct local
// compaction rather than compacting against a stale/older LSM shape.
Options options = CurrentOptions();
options.num_levels = 7;
options.level0_file_num_compaction_trigger = 100; // no auto compaction
ReopenWithCompactionService(&options);
// Old value for "k", pushed down to L2 (below the compaction output level).
ASSERT_OK(Put("k", "OLD"));
ASSERT_OK(Flush());
MoveFilesToLevel(2); // L2: k=OLD
// Newer tombstone for "k" at L0 (the compaction input).
ASSERT_OK(Delete("k"));
ASSERT_OK(Flush()); // L0: Delete(k)
int floor_check_rejected = 0;
SyncPoint::GetInstance()->SetCallBack(
"CompactionServiceJob::ProcessKeyValueCompactionWithCompactionService",
[&](void* arg) {
auto* input = static_cast<CompactionServiceInput*>(arg);
// Simulate the worker seeing an older MANIFEST than the primary
// scheduled from by raising the floor above the real position.
input->min_manifest_file_number += 1000000;
});
SyncPoint::GetInstance()->SetCallBack(
"DBImplSecondary::OpenAndCompact::ManifestFloorCheck", [&](void* arg) {
if (*static_cast<bool*>(arg)) {
floor_check_rejected++;
}
});
SyncPoint::GetInstance()->EnableProcessing();
ColumnFamilyMetaData meta;
db_->GetColumnFamilyMetaData(&meta); // default CF
ASSERT_EQ(meta.levels[0].files.size(), 1);
const auto& f_del = meta.levels[0].files[0];
std::string fname = f_del.db_path + "/" + f_del.name;
// The worker rejects the stale view -> local fallback -> the compaction still
// completes correctly (CompactFiles returns OK).
ASSERT_OK(db_->CompactFiles(CompactionOptions(), {fname}, 1));
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
ASSERT_GE(floor_check_rejected, 1);
// Deleted key stays deleted: the local fallback retained the below-output
// tombstone. A silent mis-compaction against the older view would resurrect
// "OLD".
ASSERT_EQ("NOT_FOUND", Get("k"));
}
TEST_F(CompactionServiceTest, RemoteCompactionManifestFloorKillSwitchOff) {
// With the kill switch off, the primary sends no floor, so the worker uses
// the original point-in-time recovery (no trust-manifest, no floor check). A
// transiently-missing non-input file then rolls recovery back and fails the
// job -- confirming the escape hatch reverts to the pre-fix behavior.
Options options = CurrentOptions();
options.num_levels = 7;
options.max_open_files = -1;
options.level0_file_num_compaction_trigger = 100; // no auto compaction
options.remote_compaction_manifest_floor = false; // kill switch OFF
ReopenWithCompactionService(&options);
ASSERT_OK(Put("k", "OLD"));
ASSERT_OK(Flush());
MoveFilesToLevel(2); // L2: k=OLD
ASSERT_OK(Delete("k"));
ASSERT_OK(Flush()); // L0: Delete(k)
// Reopen so the MANIFEST is rewritten as current state before we perturb it.
ReopenWithColumnFamilies({kDefaultColumnFamilyName, "cf_1", "cf_2", "cf_3"},
options);
GetCompactionService()->SetCanceled(false);
bool floor_provided_seen = true;
int floor_check_count = 0;
SyncPoint::GetInstance()->SetCallBack(
"CompactionServiceJob::ProcessKeyValueCompactionWithCompactionService",
[&](void* arg) {
auto* input = static_cast<CompactionServiceInput*>(arg);
floor_provided_seen = (input->min_manifest_file_number != 0);
});
SyncPoint::GetInstance()->SetCallBack(
"DBImplSecondary::OpenAndCompact::ManifestFloorCheck",
[&](void* /*arg*/) { floor_check_count++; });
SyncPoint::GetInstance()->EnableProcessing();
ColumnFamilyMetaData meta;
db_->GetColumnFamilyMetaData(&meta);
ASSERT_EQ(meta.levels[0].files.size(), 1);
ASSERT_EQ(meta.levels[2].files.size(), 1);
const auto& f_del = meta.levels[0].files[0];
const auto& f_old = meta.levels[2].files[0];
// Make the lower (L2) non-input file momentarily unavailable, as a live
// primary would if it had compacted it away.
ASSERT_OK(env_->DeleteFile(f_old.db_path + "/" + f_old.name));
std::string fname = f_del.db_path + "/" + f_del.name;
Status s = db_->CompactFiles(CompactionOptions(), {fname}, 1);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
// No floor was sent and the worker skipped the floor check; the original
// recovery rolled back and failed the job (reverted behavior).
ASSERT_FALSE(floor_provided_seen);
ASSERT_EQ(0, floor_check_count);
ASSERT_NOK(s);
}
TEST_F(CompactionServiceTest, RemoteCompactionManifestFloorMutable) {
// remote_compaction_manifest_floor is a mutable DB option: flipping it via
// SetDBOptions changes whether the primary attaches a floor to subsequent
// remote compaction requests (and thus whether the worker runs the floor
// check).
Options options = CurrentOptions();
options.disable_auto_compactions = true;
ReopenWithCompactionService(&options);
GenerateTestData();
bool floor_provided = false;
int floor_check_count = 0;
SyncPoint::GetInstance()->SetCallBack(
"CompactionServiceJob::ProcessKeyValueCompactionWithCompactionService",
[&](void* arg) {
auto* input = static_cast<CompactionServiceInput*>(arg);
floor_provided = (input->min_manifest_file_number != 0);
});
SyncPoint::GetInstance()->SetCallBack(
"DBImplSecondary::OpenAndCompact::ManifestFloorCheck",
[&](void* /*arg*/) { floor_check_count++; });
SyncPoint::GetInstance()->EnableProcessing();
std::string start_str = Key(15);
std::string end_str = Key(45);
Slice start(start_str);
Slice end(end_str);
// Default on: floor attached, floor check runs.
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), &start, &end));
ASSERT_TRUE(floor_provided);
ASSERT_GE(floor_check_count, 1);
// Turn the kill switch off dynamically.
ASSERT_OK(db_->SetDBOptions({{"remote_compaction_manifest_floor", "false"}}));
ASSERT_FALSE(db_->GetDBOptions().remote_compaction_manifest_floor);
floor_provided = true;
floor_check_count = 0;
// Create a fresh compaction target overlapping the range and re-compact.
ASSERT_OK(Put(Key(20), "v"));
ASSERT_OK(Flush());
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), &start, &end));
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
// Now no floor is attached and the worker skips the floor check.
ASSERT_FALSE(floor_provided);
ASSERT_EQ(0, floor_check_count);
}
TEST_F(CompactionServiceTest, CompactionInfo) {
Options options = CurrentOptions();
ReopenWithCompactionService(&options);
+12
View File
@@ -25,6 +25,18 @@ SstPartitionerFixedPrefixFactory::SstPartitionerFixedPrefixFactory(size_t len)
RegisterOptions("Length", &len_, &sst_fixed_prefix_type_info);
}
OptSlice SstPartitionerFixedPrefix::ShouldPartitionByPrefix(
const Slice& user_key) {
if (user_key.size() >= len_) {
return {user_key.data(), len_};
} else {
// ShouldPartition() cuts a partition for EVERY UNIQUE KEY outside the
// prefix length domain, for better or worse. We can't directly represent
// that so we defer to ShouldPartition().
return {};
}
}
PartitionerResult SstPartitionerFixedPrefix::ShouldPartition(
const PartitionerRequest& request) {
Slice last_key_fixed(*request.prev_user_key);
+189 -3
View File
@@ -1339,9 +1339,13 @@ TEST_F(TieredCompactionTest, SequenceBasedTieredStorageLevel) {
// This test was essentially for a hacked-up version on future functionality.
// It can be resurrected if/when a form of range-based tiering is properly
// implemented.
// FIXME: aside from that, this test reproduces a near-endless compaction
// cycle that needs to be reproduced independently and fixed before
// leveled compaction can be used with the preclude feature in production.
// The near-endless compaction cycle this used to reproduce (a bottommost file
// whose seqno could not be zeroed within the preserve window being re-marked
// forever) is now prevented by sharing the seqno-zeroability decision between
// the compaction output path and bottommost-file marking (see
// BottommostSeqnoCanBeZeroed and CompactionPickerTest.BottommostFile*
// PreserveWindow). This test remains disabled only because range-based tiering
// is not implemented.
TEST_F(TieredCompactionTest, DISABLED_RangeBasedTieredStorageLevel) {
const int kNumTrigger = 4;
const int kNumLevels = 7;
@@ -1558,6 +1562,188 @@ class PrecludeLastLevelTest : public PrecludeLastLevelTestBase,
}
};
// Regression test for an infinite kBottommostFiles compaction loop on a
// leveled column family that has the seqno->time preserve feature enabled. A
// bottommost file whose largest sequence number is still within the preserve
// window cannot have its seqno zeroed by compaction, so it must not be marked
// for bottommost compaction; otherwise each rewrite makes no progress and the
// file is re-marked forever. This exercises the DBImpl plumbing that feeds
// VersionStorageInfo::preserve_time_min_seqno_ (DBImpl::
// MaybeUpdatePreserveTimeMinSeqno) end to end, complementing the isolated
// marking-predicate coverage in compaction_picker_test.cc.
class PrecludeBottommostLoopTest : public PrecludeLastLevelTestBase {};
TEST_F(PrecludeBottommostLoopTest, LeveledPreserveTimeNoBottommostLoop) {
const int kNumLevels = 7;
Options options = CurrentOptions();
options.compaction_style = kCompactionStyleLevel;
options.preserve_internal_time_seconds = 10000;
options.env = mock_env_.get();
options.num_levels = kNumLevels;
// Prevent the extra L0 file below from auto-triggering an L0 compaction.
options.level0_file_num_compaction_trigger = 4;
SyncPoint::GetInstance()->EnableProcessing();
DestroyAndReopen(options);
// Populate the seqno->time mapping with recent samples so the preserve window
// maps the file's (recent) largest seqno as non-zeroable. Total elapsed mock
// time stays far below preserve_internal_time_seconds, so nothing ages out.
for (int i = 0; i < 10; i++) {
ASSERT_OK(Put("infinite", "v" + std::to_string(i)));
dbfull()->TEST_WaitForPeriodicTaskRun(
[&] { mock_clock_->MockSleepForSeconds(10); });
}
ASSERT_OK(Flush());
// Push the file with the hot key down to the bottommost level.
MoveFilesToLevel(kNumLevels - 1);
ASSERT_EQ(1, NumTableFilesAtLevel(kNumLevels - 1));
// Confirm the bottommost file's largest seqno was not zeroed (it is within
// the preserve window), so it is a genuine marking candidate that only the
// preserve-window guard should protect -- this keeps the test from passing
// vacuously.
{
std::vector<LiveFileMetaData> files;
db_->GetLiveFilesMetaData(&files);
bool found_bottommost = false;
for (const auto& f : files) {
if (f.level == kNumLevels - 1) {
ASSERT_GT(f.largest_seqno, 0U);
found_bottommost = true;
}
}
ASSERT_TRUE(found_bottommost);
}
// Bump the sequence number above the bottommost file so that releasing a
// snapshot advances oldest_snapshot_seqnum_ past the file's largest seqno,
// which makes the file a candidate for bottommost marking (the snapshot gate
// passes). Only the preserve-window guard should keep it from being marked.
ASSERT_OK(Put("bumpseqnum", ""));
ASSERT_OK(Flush());
std::atomic<int> bottommost_compactions{0};
SyncPoint::GetInstance()->SetCallBack(
"LevelCompactionPicker::PickCompaction:Return", [&](void* arg) {
auto* c = static_cast<Compaction*>(arg);
if (c != nullptr &&
c->compaction_reason() == CompactionReason::kBottommostFiles) {
bottommost_compactions.fetch_add(1);
}
});
const Snapshot* snapshot = db_->GetSnapshot();
db_->ReleaseSnapshot(snapshot);
// The bottommost file's largest seqno is within the preserve window, so it is
// not marked and the DB quiesces. Before the fix the file was marked and the
// rewrite re-qualified it forever (this wait would hang).
ASSERT_OK(dbfull()->TEST_WaitForCompact());
ASSERT_EQ(0, bottommost_compactions.load());
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
// Close explicitly because mock_env_ is destroyed before the DB otherwise.
Close();
}
// Regression test for the empty-mapping variant of the same loop: a bottommost
// file that predates a preserve/preclude rollout has an EMPTY persisted
// seqno->time mapping. Once the live, DB-wide mapping ages the file out of the
// preserve window, the marker (which reads the live mapping) marks it. The
// compaction that rewrites it must use the same version-carried preserve
// boundary rather than the file's own empty mapping; otherwise it computes
// preserve_seqno_after_ = 0, zeroes nothing, and re-marks the file forever.
// With the fix the file's seqno is zeroed in one bounded compaction.
TEST_F(PrecludeBottommostLoopTest, EmptyMappingAgedOutNoBottommostLoop) {
const int kNumLevels = 7;
Options options = CurrentOptions();
options.compaction_style = kCompactionStyleLevel;
options.env = mock_env_.get();
options.num_levels = kNumLevels;
options.level0_file_num_compaction_trigger = 4;
// Start with preserve/preclude disabled so the bottommost file below is
// written with an EMPTY persisted seqno->time mapping, mimicking a file that
// predates a preserve/preclude rollout.
options.preserve_internal_time_seconds = 0;
options.preclude_last_level_data_seconds = 0;
SyncPoint::GetInstance()->EnableProcessing();
DestroyAndReopen(options);
// Hold a snapshot below the "aged" key so moving it to the bottommost level
// does not zero its seqno (there is no preserve window yet to protect it).
ASSERT_OK(Put("keeplow", "v"));
const Snapshot* protect = db_->GetSnapshot();
ASSERT_OK(Put("aged", "v"));
ASSERT_OK(Flush());
MoveFilesToLevel(kNumLevels - 1);
ASSERT_EQ(1, NumTableFilesAtLevel(kNumLevels - 1));
SequenceNumber aged_seqno = 0;
{
std::vector<LiveFileMetaData> files;
db_->GetLiveFilesMetaData(&files);
for (const auto& f : files) {
if (f.level == kNumLevels - 1) {
aged_seqno = f.largest_seqno;
}
}
}
ASSERT_GT(aged_seqno, 0U);
// Roll out the preserve feature dynamically and let the live, DB-wide
// seqno->time mapping accumulate samples while mock time advances well past
// preserve_internal_time_seconds, so the pre-existing file's seqno ages out
// of the preserve window per the live mapping (even though the file's own
// persisted mapping is empty). The protecting snapshot keeps the file from
// being marked during this loop (oldest_snapshot_seqnum_ stays below it).
ASSERT_OK(db_->SetOptions({{"preserve_internal_time_seconds", "10000"}}));
for (int i = 0; i < 100; i++) {
ASSERT_OK(Put("hot", "v" + std::to_string(i)));
dbfull()->TEST_WaitForPeriodicTaskRun(
[&] { mock_clock_->MockSleepForSeconds(200); });
}
std::atomic<int> bottommost_compactions{0};
SyncPoint::GetInstance()->SetCallBack(
"LevelCompactionPicker::PickCompaction:Return", [&](void* arg) {
auto* c = static_cast<Compaction*>(arg);
if (c != nullptr &&
c->compaction_reason() == CompactionReason::kBottommostFiles) {
bottommost_compactions.fetch_add(1);
}
});
// Release the protecting snapshot so oldest_snapshot_seqnum_ advances past
// the file's largest seqno, making it a bottommost marking candidate. The
// marker (using the live mapping) marks it; the compaction that rewrites it
// must use the same version-carried preserve boundary (not the file's empty
// mapping) to actually zero the seqno, otherwise it makes no progress and
// loops.
db_->ReleaseSnapshot(protect);
// With the fix the file's seqno is zeroed in a bounded number of bottommost
// compactions and the DB quiesces. Before the fix this wait would hang.
ASSERT_OK(dbfull()->TEST_WaitForCompact());
{
std::vector<LiveFileMetaData> files;
db_->GetLiveFilesMetaData(&files);
for (const auto& f : files) {
if (f.level == kNumLevels - 1) {
ASSERT_EQ(0U, f.largest_seqno);
}
}
}
// The file was genuinely marked and rewritten (not vacuously skipped), and
// the rewrite made progress instead of looping.
ASSERT_GE(bottommost_compactions.load(), 1);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
Close();
}
TEST_P(PrecludeLastLevelTest, MigrationFromPreserveTimeManualCompaction) {
const int kNumTrigger = 4;
const int kNumLevels = 7;
+160
View File
@@ -0,0 +1,160 @@
// 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).
#include "rocksdb/rocksdb_namespace.h"
#if USE_COROUTINES
#include <algorithm>
#include <array>
#include <cassert>
#include <vector>
#include "folly/Executor.h"
#include "folly/coro/Nothrow.h"
#include "folly/executors/IOExecutor.h"
#include "folly/io/async/EventBase.h"
#include "rocksdb/coro_db.h"
#include "rocksdb/file_system.h"
#include "table/multiget_context.h"
namespace ROCKSDB_NAMESPACE {
folly::coro::Task<Status> CoroDB::CoGet(DB* db, const ReadOptions& options,
ColumnFamilyHandle* column_family,
const Slice& key, PinnableSlice* value,
std::string* timestamp) {
assert(db != nullptr);
CoroDB* coro_db = db->GetCoroDB();
auto* file_system = db->GetFileSystem();
auto* read_executor =
file_system == nullptr ? nullptr : file_system->GetReadExecutor();
auto* read_event_base =
read_executor == nullptr ? nullptr : read_executor->getEventBase();
Status status;
if (coro_db == nullptr || read_event_base == nullptr) {
status = db->Get(options, column_family, key, value, timestamp);
} else {
status = co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
folly::Executor::getKeepAliveToken(read_event_base),
coro_db->GetCoroutine(options, column_family, key, value, timestamp)));
}
co_return status;
}
folly::coro::Task<Status> CoroDB::CoGet(DB* db, const ReadOptions& options,
ColumnFamilyHandle* column_family,
const Slice& key, std::string* value,
std::string* timestamp) {
assert(value != nullptr);
PinnableSlice pinnable_value(value);
assert(!pinnable_value.IsPinned());
Status status = co_await folly::coro::co_nothrow(
CoGet(db, options, column_family, key, &pinnable_value, timestamp));
if (status.ok() && pinnable_value.IsPinned()) {
value->assign(pinnable_value.data(), pinnable_value.size());
}
co_return status;
}
folly::coro::Task<Status> CoroDB::CoGet(DB* db, const ReadOptions& options,
const Slice& key, std::string* value) {
assert(db != nullptr);
return CoGet(db, options, db->DefaultColumnFamily(), key, value);
}
folly::coro::Task<Status> CoroDB::CoGet(DB* db, const ReadOptions& options,
const Slice& key, std::string* value,
std::string* timestamp) {
assert(db != nullptr);
return CoGet(db, options, db->DefaultColumnFamily(), key, value, timestamp);
}
folly::coro::Task<std::vector<Status>> CoroDB::CoMultiGet(
DB* db, const ReadOptions& options,
const std::vector<ColumnFamilyHandle*>& column_families,
const std::vector<Slice>& keys, std::vector<std::string>* values,
std::vector<std::string>* timestamps) {
const size_t num_keys = keys.size();
std::vector<Status> statuses(num_keys);
std::vector<PinnableSlice> pinnable_values(num_keys);
values->resize(num_keys);
if (timestamps != nullptr) {
timestamps->resize(num_keys);
}
co_await folly::coro::co_nothrow(CoMultiGet(
db, options, num_keys,
const_cast<ColumnFamilyHandle**>(column_families.data()), keys.data(),
pinnable_values.data(),
timestamps == nullptr ? nullptr : timestamps->data(), statuses.data(),
/*sorted_input=*/false));
for (size_t i = 0; i < num_keys; ++i) {
if (statuses[i].ok()) {
(*values)[i].assign(pinnable_values[i].data(), pinnable_values[i].size());
}
}
co_return statuses;
}
folly::coro::Task<std::vector<Status>> CoroDB::CoMultiGet(
DB* db, const ReadOptions& options, const std::vector<Slice>& keys,
std::vector<std::string>* values, std::vector<std::string>* timestamps) {
assert(db != nullptr);
std::vector<ColumnFamilyHandle*> column_families(keys.size(),
db->DefaultColumnFamily());
co_return co_await folly::coro::co_nothrow(
CoMultiGet(db, options, column_families, keys, values, timestamps));
}
folly::coro::Task<void> CoroDB::CoMultiGet(
DB* db, const ReadOptions& options, size_t num_keys,
ColumnFamilyHandle** column_families, const Slice* keys,
PinnableSlice* values, std::string* timestamps, Status* statuses,
bool sorted_input) {
assert(db != nullptr);
CoroDB* coro_db = db->GetCoroDB();
auto* file_system = db->GetFileSystem();
auto* read_executor =
file_system == nullptr ? nullptr : file_system->GetReadExecutor();
auto* read_event_base =
read_executor == nullptr ? nullptr : read_executor->getEventBase();
if (coro_db == nullptr || read_event_base == nullptr) {
db->MultiGet(options, num_keys, column_families, keys, values, timestamps,
statuses, sorted_input);
} else {
co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
folly::Executor::getKeepAliveToken(read_event_base),
coro_db->MultiGetCoroutine(options, num_keys, column_families, keys,
values, timestamps, statuses,
sorted_input)));
}
}
folly::coro::Task<void> CoroDB::CoMultiGet(
DB* db, const ReadOptions& options, ColumnFamilyHandle* column_family,
size_t num_keys, const Slice* keys, PinnableSlice* values,
std::string* timestamps, Status* statuses, bool sorted_input) {
if (num_keys > MultiGetContext::MAX_BATCH_SIZE) {
std::vector<ColumnFamilyHandle*> column_families(num_keys, column_family);
co_await folly::coro::co_nothrow(
CoMultiGet(db, options, num_keys, column_families.data(), keys, values,
timestamps, statuses, sorted_input));
} else {
std::array<ColumnFamilyHandle*, MultiGetContext::MAX_BATCH_SIZE>
column_families;
std::fill(column_families.begin(), column_families.begin() + num_keys,
column_family);
co_await folly::coro::co_nothrow(
CoMultiGet(db, options, num_keys, column_families.data(), keys, values,
timestamps, statuses, sorted_input));
}
}
} // namespace ROCKSDB_NAMESPACE
#endif // USE_COROUTINES
+97 -35
View File
@@ -11,6 +11,7 @@
#include "db/db_test_util.h"
#include "db/log_writer.h"
#include "db/manifest_ops.h"
#include "db/version_edit.h"
#include "file/writable_file_writer.h"
#include "options/options_helper.h"
@@ -20,10 +21,13 @@
#include "rocksdb/merge_operator.h"
#include "rocksdb/perf_context.h"
#include "rocksdb/table.h"
#include "rocksdb/utilities/checkpoint.h"
#include "rocksdb/utilities/debug.h"
#include "table/block_based/block_based_table_reader.h"
#include "table/block_based/block_builder.h"
#include "table/format.h"
#include "test_util/sync_point.h"
#include "util/defer.h"
#include "util/file_checksum_helper.h"
#include "util/random.h"
#include "utilities/counted_fs.h"
@@ -141,6 +145,7 @@ struct RecoveryOptimizationCounters {
sp->ClearAllCallBacks();
}
};
} // namespace
// optimize_manifest_for_recovery=true: a clean reopen of a flushed DB must
@@ -351,6 +356,45 @@ TEST_F(DBBasicTest,
synthetic_number);
}
TEST_F(DBBasicTest,
OptimizeManifestForRecoveryKeepsCurrentOptionsFileWithStaleOptions) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
options.avoid_unnecessary_blocking_io = false;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
const uint64_t next_before_close =
dbfull()->GetVersionSet()->current_next_file_number();
Close();
const uint64_t stale_options_number = next_before_close + 100;
ASSERT_OK(WriteStringToFile(
env_, "" /*data*/, dbname_ + "/" + OptionsFileName(stale_options_number),
/*should_sync=*/true));
ASSERT_OK(WriteStringToFile(
env_, "" /*data*/,
dbname_ + "/" + OptionsFileName(stale_options_number + 1),
/*should_sync=*/true));
Reopen(options);
const uint64_t current_options_number =
dbfull()->GetVersionSet()->options_file_number();
ASSERT_GT(current_options_number, 0u);
ASSERT_OK(env_->FileExists(OptionsFileName(dbname_, current_options_number)));
const std::string checkpoint_dir = dbname_ + "_checkpoint";
ASSERT_OK(DestroyDir(env_, checkpoint_dir));
Checkpoint* checkpoint = nullptr;
ASSERT_OK(Checkpoint::Create(db_.get(), &checkpoint));
std::unique_ptr<Checkpoint> checkpoint_guard(checkpoint);
ASSERT_OK(checkpoint_guard->CreateCheckpoint(checkpoint_dir));
ASSERT_OK(DestroyDir(env_, checkpoint_dir));
ASSERT_EQ("v", Get("k"));
}
// optimize_manifest_for_recovery=true: after a clean Put + Flush + Close, the
// next Open's min_log_number_to_keep equals (max_wal_before_close + 1),
// proving the close-time MANIFEST write took effect.
@@ -978,27 +1022,19 @@ TEST_F(DBBasicTest, WritableFileWriterInitialFileSizeAdoptsExistingSize) {
TEST_F(DBBasicTest, ReuseManifestOnOpenSkipsOnTailCorruption) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
options.reuse_manifest_on_open = true;
options.write_dbid_to_manifest = false;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
Close();
// Find the MANIFEST file and append garbage to it.
std::string manifest_path;
{
std::vector<std::string> files;
ASSERT_OK(env_->GetChildren(dbname_, &files));
for (const auto& f : files) {
uint64_t number;
FileType type;
if (ParseFileName(f, &number, &type) && type == kDescriptorFile) {
manifest_path = dbname_ + "/" + f;
break;
}
}
}
ASSERT_FALSE(manifest_path.empty());
uint64_t manifest_file_number = 0;
ASSERT_OK(GetCurrentManifestPath(dbname_, env_->GetFileSystem().get(),
/*is_retry=*/false, &manifest_path,
&manifest_file_number));
{
std::string contents;
ASSERT_OK(ReadFileToString(env_, manifest_path, &contents));
@@ -1007,9 +1043,13 @@ TEST_F(DBBasicTest, ReuseManifestOnOpenSkipsOnTailCorruption) {
}
std::atomic<int> reopened{0};
std::atomic<int> new_manifest{0};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"VersionSet::ReopenManifestForAppend:Reopened",
[&](void* /*arg*/) { reopened.fetch_add(1); });
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"VersionSet::ProcessManifestWrites:BeforeNewManifest",
[&](void* /*arg*/) { new_manifest.fetch_add(1); });
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
Reopen(options);
@@ -1018,6 +1058,12 @@ TEST_F(DBBasicTest, ReuseManifestOnOpenSkipsOnTailCorruption) {
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
ASSERT_EQ(0, reopened.load());
ASSERT_EQ(1, new_manifest.load());
std::string manifest_path_after;
ASSERT_OK(GetCurrentManifestPath(dbname_, env_->GetFileSystem().get(),
/*is_retry=*/false, &manifest_path_after,
&manifest_file_number));
ASSERT_NE(manifest_path, manifest_path_after);
ASSERT_EQ("v", Get("k"));
}
@@ -1030,27 +1076,19 @@ TEST_F(DBBasicTest, ReuseManifestOnOpenSkipsOnTailCorruption) {
TEST_F(DBBasicTest, ReuseManifestOnOpenIncompleteAtomicGroupAtTail) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
options.reuse_manifest_on_open = true;
options.write_dbid_to_manifest = false;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
Close();
// Find the MANIFEST file.
std::string manifest_path;
{
std::vector<std::string> files;
ASSERT_OK(env_->GetChildren(dbname_, &files));
for (const auto& f : files) {
uint64_t number;
FileType type;
if (ParseFileName(f, &number, &type) && type == kDescriptorFile) {
manifest_path = dbname_ + "/" + f;
break;
}
}
}
ASSERT_FALSE(manifest_path.empty());
uint64_t manifest_file_number = 0;
ASSERT_OK(GetCurrentManifestPath(dbname_, env_->GetFileSystem().get(),
/*is_retry=*/false, &manifest_path,
&manifest_file_number));
// Append an incomplete atomic group (2 records of a 3-member group) to the
// MANIFEST. These are valid log records with correct CRCs but the atomic
@@ -1088,11 +1126,29 @@ TEST_F(DBBasicTest, ReuseManifestOnOpenIncompleteAtomicGroupAtTail) {
// Deliberately NOT writing the third record (remaining_entries=0)
}
// Reopen: recovery should succeed (incomplete trailing group is ignored).
// With the fix, ReopenManifestForAppend will NOT reuse the MANIFEST because
// manifest_last_valid_record_end_ < physical_size (the incomplete group
// records are excluded from the valid end).
std::atomic<int> reopened{0};
std::atomic<int> new_manifest{0};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"VersionSet::ReopenManifestForAppend:Reopened",
[&](void* /*arg*/) { reopened.fetch_add(1); });
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"VersionSet::ProcessManifestWrites:BeforeNewManifest",
[&](void* /*arg*/) { new_manifest.fetch_add(1); });
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
// Reopen: recovery should succeed, but ReopenManifestForAppend must not
// reuse a MANIFEST ending with an incomplete atomic group.
Reopen(options);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
ASSERT_EQ(0, reopened.load());
ASSERT_EQ(1, new_manifest.load());
std::string manifest_path_after;
ASSERT_OK(GetCurrentManifestPath(dbname_, env_->GetFileSystem().get(),
/*is_retry=*/false, &manifest_path_after,
&manifest_file_number));
ASSERT_NE(manifest_path, manifest_path_after);
ASSERT_EQ("v", Get("k"));
// Write new data to create new MANIFEST records.
@@ -2227,6 +2283,9 @@ INSTANTIATE_TEST_CASE_P(FormatVersions, DBBlockChecksumTest,
TEST_P(DBBlockChecksumTest, BlockChecksumTest) {
BlockBasedTableOptions table_options;
table_options.format_version = GetParam();
// kFooterFormatVersionsToTest includes the unpublished draft format_version
// 8; writing it requires this opt-in.
SaveAndRestore<bool> allow_draft(&TEST_AllowUnsupportedFormatVersion(), true);
Options options = CurrentOptions();
const int kNumPerFile = 2;
@@ -4020,7 +4079,8 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL0) {
AssertMultiGetIOBatchSize(3, 1);
}
#else // ROCKSDB_IOURING_PRESENT
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 0);
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT),
UseCoroutineRead() ? 3 : 0);
#endif // ROCKSDB_IOURING_PRESENT
}
@@ -4047,7 +4107,8 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL1) {
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT),
UseCoroutineRead() ? 6 : 3);
#else // ROCKSDB_IOURING_PRESENT
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 0);
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT),
UseCoroutineRead() ? 6 : 0);
#endif // ROCKSDB_IOURING_PRESENT
}
@@ -4200,7 +4261,8 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL2WithRangeOverlapL0L1) {
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT),
UseCoroutineRead() ? 3 : 2);
#else // ROCKSDB_IOURING_PRESENT
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 0);
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT),
UseCoroutineRead() ? 3 : 0);
#endif // ROCKSDB_IOURING_PRESENT
}
+30 -9
View File
@@ -1800,6 +1800,7 @@ static std::map<TableFileCreationReason, std::string>
{TableFileCreationReason::kFlush, "kFlush"},
{TableFileCreationReason::kMisc, "kMisc"},
{TableFileCreationReason::kRecovery, "kRecovery"},
{TableFileCreationReason::kSstFileWriter, "kSstFileWriter"},
};
class TestingContextCustomFilterPolicy
@@ -1814,6 +1815,15 @@ class TestingContextCustomFilterPolicy
const FilterBuildingContext& context) const override {
test_report_ += "cf=";
test_report_ += context.column_family_name;
// Token rather than path, so expectations don't depend on test directory
test_report_ += ",db=";
if (context.db_name.empty()) {
test_report_ += "none";
} else if (context.db_name == Slice(expected_db_name_)) {
test_report_ += "expected";
} else {
test_report_ += "UNEXPECTED:" + context.db_name.ToString();
}
test_report_ += ",s=";
test_report_ +=
OptionsHelper::compaction_style_to_string[context.compaction_style];
@@ -1836,8 +1846,13 @@ class TestingContextCustomFilterPolicy
return rv;
}
void SetExpectedDbName(std::string db_name) {
expected_db_name_ = std::move(db_name);
}
private:
mutable std::string test_report_;
std::string expected_db_name_;
};
} // anonymous namespace
@@ -1859,6 +1874,13 @@ TEST_F(DBBloomFilterTest, ContextCustomFilterPolicy) {
ASSERT_OK(TryReopen(options));
CreateAndReopenWithCF({fifo ? "abe" : "bob"}, options);
policy->SetExpectedDbName(dbname_);
const char* expected_flush_report =
fifo
? "cf=abe,db=expected,s=kCompactionStyleFIFO,n=7,l=0,b=0,r=kFlush\n"
: "cf=bob,db=expected,s=kCompactionStyleLevel,n=7,l=0,b=0,r="
"kFlush\n";
const int maxKey = 10000;
for (int i = 0; i < maxKey / 2; i++) {
@@ -1867,17 +1889,13 @@ TEST_F(DBBloomFilterTest, ContextCustomFilterPolicy) {
// Add a large key to make the file contain wide range
ASSERT_OK(Put(1, Key(maxKey + 55555), Key(maxKey + 55555)));
ASSERT_OK(Flush(1));
EXPECT_EQ(policy->DumpTestReport(),
fifo ? "cf=abe,s=kCompactionStyleFIFO,n=7,l=0,b=0,r=kFlush\n"
: "cf=bob,s=kCompactionStyleLevel,n=7,l=0,b=0,r=kFlush\n");
EXPECT_EQ(policy->DumpTestReport(), expected_flush_report);
for (int i = maxKey / 2; i < maxKey; i++) {
ASSERT_OK(Put(1, Key(i), Key(i)));
}
ASSERT_OK(Flush(1));
EXPECT_EQ(policy->DumpTestReport(),
fifo ? "cf=abe,s=kCompactionStyleFIFO,n=7,l=0,b=0,r=kFlush\n"
: "cf=bob,s=kCompactionStyleLevel,n=7,l=0,b=0,r=kFlush\n");
EXPECT_EQ(policy->DumpTestReport(), expected_flush_report);
// Check that they can be found
for (int i = 0; i < maxKey; i++) {
@@ -1904,7 +1922,8 @@ TEST_F(DBBloomFilterTest, ContextCustomFilterPolicy) {
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), handles_[1], nullptr,
nullptr));
EXPECT_EQ(policy->DumpTestReport(),
"cf=bob,s=kCompactionStyleLevel,n=7,l=1,b=1,r=kCompaction\n");
"cf=bob,db=expected,s=kCompactionStyleLevel,n=7,l=1,b=1,r="
"kCompaction\n");
// Check that we now have one filter, about 9.2% FP rate (5 bits per key)
for (int i = 0; i < maxKey; i++) {
@@ -1926,7 +1945,8 @@ TEST_F(DBBloomFilterTest, ContextCustomFilterPolicy) {
}
// Note: kCompactionStyleLevel is default, ignored if num_levels == -1
EXPECT_EQ(policy->DumpTestReport(),
"cf=abe,s=kCompactionStyleLevel,n=-1,l=-1,b=0,r=kMisc\n");
"cf=abe,db=none,s=kCompactionStyleLevel,n=-1,l=-1,b=0,r="
"kSstFileWriter\n");
}
// Destroy
@@ -3978,7 +3998,8 @@ static std::vector<std::string> RangeQueryKeys(
ReadOptions ro;
ro.iterate_lower_bound = &lb;
ro.iterate_upper_bound = &ub;
ro.table_filter = factory.GetTableFilterForRangeQuery(lb, ub);
auto filter = factory.GetTableFilterForRangeQuery(lb, ub);
ro.table_filter = &filter;
auto it = db.NewIterator(ro);
std::vector<std::string> ret;
for (it->Seek(lb); it->Valid(); it->Next()) {
+494
View File
@@ -4,6 +4,9 @@
// (found in the LICENSE.Apache file in the root directory).
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <set>
#include <thread>
#include <unordered_map>
@@ -14,6 +17,7 @@
#include "options/options_helper.h"
#include "port/stack_trace.h"
#include "rocksdb/db.h"
#include "rocksdb/listener.h"
#include "rocksdb/sst_file_writer.h"
#include "test_util/sync_point.h"
#include "test_util/testharness.h"
@@ -91,6 +95,19 @@ class AbortSynchronizer {
}
}
void TriggerAbort(DBImpl* db, ColumnFamilyHandle* column_family) {
if (abort_triggered_.load() && abort_completed_.load()) {
Reset();
}
if (!abort_triggered_.exchange(true)) {
abort_thread_ = std::thread([this, db, column_family]() {
db->AbortCompactions(column_family);
SignalAbortCompleted();
});
}
}
// Wait for the abort thread to complete.
// Call this AFTER compaction returns to ensure the abort thread has finished
// before proceeding with Resume or other operations.
@@ -459,6 +476,483 @@ TEST_F(DBCompactionAbortTest, AbortScheduledAutomaticCompactionBeforePick) {
VerifyDataIntegrity(/*num_keys=*/100);
}
TEST_F(DBCompactionAbortTest, AbortCompactionsIsScopedToColumnFamily) {
Options options = CurrentOptions();
options.level0_file_num_compaction_trigger = 100;
options.max_background_compactions = 2;
options.max_subcompactions = 1;
options.disable_auto_compactions = true;
CreateAndReopenWithCF({"target", "sibling"}, options);
Random rnd(301);
for (int cf = 1; cf <= 2; ++cf) {
for (int file = 0; file < 4; ++file) {
for (int key = 0; key < 100; ++key) {
ASSERT_OK(Put(cf, Key(key), rnd.RandomString(1000)));
}
ASSERT_OK(Flush(cf));
}
}
AbortSynchronizer abort_sync;
std::atomic<int> compactions_started{0};
std::mutex sibling_mutex;
std::condition_variable sibling_cv;
bool sibling_started = false;
bool allow_sibling_to_finish = false;
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
{"DBImpl::AbortCompactions:FlagSet",
"DBCompactionAbortTest::ScopedAbort:WaitForAbort"},
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"CompactionJob::ProcessKeyValueCompaction:Start", [&](void* /*arg*/) {
if (compactions_started.fetch_add(1) == 0) {
std::unique_lock<std::mutex> lock(sibling_mutex);
sibling_started = true;
sibling_cv.notify_all();
sibling_cv.wait(lock, [&]() { return allow_sibling_to_finish; });
return;
}
abort_sync.TriggerAbort(dbfull(), handles_[1]);
TEST_SYNC_POINT_CALLBACK(
"DBCompactionAbortTest::ScopedAbort:WaitForAbort", nullptr);
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
Status sibling_status;
std::thread sibling_thread([&]() {
sibling_status = dbfull()->CompactRange(CompactRangeOptions(), handles_[2],
nullptr, nullptr);
});
{
std::unique_lock<std::mutex> lock(sibling_mutex);
sibling_cv.wait(lock, [&]() { return sibling_started; });
}
Status target_status = dbfull()->CompactRange(CompactRangeOptions(),
handles_[1], nullptr, nullptr);
abort_sync.WaitForAbortCompletion();
{
std::lock_guard<std::mutex> guard(sibling_mutex);
allow_sibling_to_finish = true;
}
sibling_cv.notify_all();
sibling_thread.join();
CleanupSyncPoints();
ASSERT_TRUE(target_status.IsIncomplete());
ASSERT_TRUE(target_status.IsCompactionAborted());
ASSERT_OK(sibling_status);
target_status = dbfull()->CompactRange(CompactRangeOptions(), handles_[1],
nullptr, nullptr);
ASSERT_TRUE(target_status.IsIncomplete());
ASSERT_TRUE(target_status.IsCompactionAborted());
dbfull()->ResumeCompactions(handles_[1]);
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), handles_[1], nullptr,
nullptr));
}
TEST_F(DBCompactionAbortTest, PerColumnFamilyAndGlobalAbortCountersCompose) {
Options options = CurrentOptions();
options.level0_file_num_compaction_trigger = 100;
options.disable_auto_compactions = true;
CreateAndReopenWithCF({"target", "sibling"}, options);
for (int cf = 1; cf <= 2; ++cf) {
for (int file = 0; file < 2; ++file) {
ASSERT_OK(Put(cf, Key(file), "value"));
ASSERT_OK(Flush(cf));
}
}
dbfull()->AbortAllCompactions();
dbfull()->AbortCompactions(handles_[1]);
dbfull()->AbortCompactions(handles_[1]);
dbfull()->ResumeCompactions(handles_[1]);
Status sibling_status = dbfull()->CompactRange(CompactRangeOptions(),
handles_[2], nullptr, nullptr);
ASSERT_TRUE(sibling_status.IsIncomplete());
ASSERT_TRUE(sibling_status.IsCompactionAborted());
dbfull()->ResumeAllCompactions();
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), handles_[2], nullptr,
nullptr));
Status target_status = dbfull()->CompactRange(CompactRangeOptions(),
handles_[1], nullptr, nullptr);
ASSERT_TRUE(target_status.IsIncomplete());
ASSERT_TRUE(target_status.IsCompactionAborted());
dbfull()->ResumeCompactions(handles_[1]);
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), handles_[1], nullptr,
nullptr));
}
TEST_F(DBCompactionAbortTest, AbortCompactionsRejectsCompactFilesTrivialMove) {
Options options = CurrentOptions();
options.disable_auto_compactions = true;
CreateAndReopenWithCF({"target"}, options);
ASSERT_OK(Put(1, "key", "value"));
ASSERT_OK(Flush(1));
ColumnFamilyMetaData metadata;
dbfull()->GetColumnFamilyMetaData(handles_[1], &metadata);
ASSERT_EQ(metadata.levels[0].files.size(), 1);
const std::vector<std::string> input_files = {
metadata.levels[0].files.front().name};
CompactionOptions compaction_options;
compaction_options.allow_trivial_move = true;
dbfull()->AbortCompactions(handles_[1]);
Status status =
dbfull()->CompactFiles(compaction_options, handles_[1], input_files, 1);
ASSERT_TRUE(status.IsIncomplete());
ASSERT_TRUE(status.IsCompactionAborted());
dbfull()->ResumeCompactions(handles_[1]);
ASSERT_OK(
dbfull()->CompactFiles(compaction_options, handles_[1], input_files, 1));
}
TEST_F(DBCompactionAbortTest, ResumeRestoresParkedAutomaticCompaction) {
constexpr int kCompactionTrigger = 4;
Options options = CurrentOptions();
options.level0_file_num_compaction_trigger = kCompactionTrigger;
options.max_background_compactions = 1;
options.disable_auto_compactions = false;
CreateAndReopenWithCF({"target", "sibling"}, options);
AbortSynchronizer abort_sync;
std::mutex parked_mutex;
std::condition_variable parked_cv;
bool compaction_parked = false;
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
{"DBImpl::AbortCompactions:FlagSet",
"DBCompactionAbortTest::ParkedCompaction:WaitForAbort"},
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"BackgroundCallCompaction:0", [&](void* /*arg*/) {
abort_sync.TriggerAbort(dbfull(), handles_[1]);
TEST_SYNC_POINT_CALLBACK(
"DBCompactionAbortTest::ParkedCompaction:WaitForAbort", nullptr);
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"DBImpl::PickCompactionFromQueue:Parked", [&](void* /*arg*/) {
std::lock_guard<std::mutex> guard(parked_mutex);
compaction_parked = true;
parked_cv.notify_all();
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
Random rnd(301);
for (int file = 0; file < kCompactionTrigger; ++file) {
for (int key = 0; key < 100; ++key) {
ASSERT_OK(Put(1, Key(key), rnd.RandomString(1000)));
}
ASSERT_OK(Flush(1));
}
{
std::unique_lock<std::mutex> lock(parked_mutex);
parked_cv.wait(lock, [&]() { return compaction_parked; });
}
CleanupSyncPoints();
abort_sync.WaitForAbortCompletion();
const int l0_files_before_resume = NumTableFilesAtLevel(0, 1);
ASSERT_GE(l0_files_before_resume, kCompactionTrigger);
WaitForCompactOptions wait_options;
wait_options.timeout = std::chrono::milliseconds(1);
ASSERT_TRUE(dbfull()->WaitForCompact(wait_options).IsTimedOut());
for (int file = 0; file < kCompactionTrigger; ++file) {
for (int key = 0; key < 100; ++key) {
ASSERT_OK(Put(2, Key(key), rnd.RandomString(1000)));
}
ASSERT_OK(Flush(2));
}
ASSERT_OK(dbfull()->TEST_WaitForBackgroundWork());
ASSERT_LT(NumTableFilesAtLevel(0, 2), kCompactionTrigger);
ASSERT_EQ(NumTableFilesAtLevel(0, 1), l0_files_before_resume);
std::atomic<bool> target_reenqueued{false};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"DBImpl::RestoreParkedCompaction:cfd", [&](void* arg) {
auto* cfd = static_cast<ColumnFamilyData*>(arg);
if (cfd->GetName() == "target") {
target_reenqueued.store(true);
}
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
dbfull()->ResumeCompactions(handles_[1]);
ASSERT_TRUE(target_reenqueued.load());
CleanupSyncPoints();
ASSERT_OK(dbfull()->TEST_WaitForCompact());
ASSERT_LT(NumTableFilesAtLevel(0, 1), l0_files_before_resume);
}
TEST_F(DBCompactionAbortTest, DropColumnFamilyReleasesParkedCompaction) {
constexpr int kCompactionTrigger = 4;
Options options = CurrentOptions();
options.level0_file_num_compaction_trigger = kCompactionTrigger;
options.max_background_compactions = 1;
options.disable_auto_compactions = false;
CreateAndReopenWithCF({"target"}, options);
AbortSynchronizer abort_sync;
std::mutex parked_mutex;
std::condition_variable parked_cv;
bool compaction_parked = false;
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
{"DBImpl::AbortCompactions:FlagSet",
"DBCompactionAbortTest::DropParkedCompaction:WaitForAbort"},
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"BackgroundCallCompaction:0", [&](void* /*arg*/) {
abort_sync.TriggerAbort(dbfull(), handles_[1]);
TEST_SYNC_POINT_CALLBACK(
"DBCompactionAbortTest::DropParkedCompaction:WaitForAbort",
nullptr);
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"DBImpl::PickCompactionFromQueue:Parked", [&](void* /*arg*/) {
std::lock_guard<std::mutex> guard(parked_mutex);
compaction_parked = true;
parked_cv.notify_all();
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
Random rnd(301);
for (int file = 0; file < kCompactionTrigger; ++file) {
for (int key = 0; key < 100; ++key) {
ASSERT_OK(Put(1, Key(key), rnd.RandomString(1000)));
}
ASSERT_OK(Flush(1));
}
{
std::unique_lock<std::mutex> lock(parked_mutex);
parked_cv.wait(lock, [&]() { return compaction_parked; });
}
CleanupSyncPoints();
abort_sync.WaitForAbortCompletion();
ASSERT_OK(dbfull()->DropColumnFamily(handles_[1]));
WaitForCompactOptions wait_options;
wait_options.timeout = std::chrono::seconds(10);
ASSERT_OK(dbfull()->WaitForCompact(wait_options));
dbfull()->ResumeCompactions(handles_[1]);
}
TEST_F(DBCompactionAbortTest, DropColumnFamilyBeforeAbortedQueueEntryIsParked) {
constexpr int kCompactionTrigger = 4;
Options options = CurrentOptions();
options.level0_file_num_compaction_trigger = kCompactionTrigger;
options.max_background_compactions = 1;
options.disable_auto_compactions = false;
CreateAndReopenWithCF({"target"}, options);
std::mutex worker_mutex;
std::condition_variable worker_cv;
bool worker_started = false;
bool allow_worker_to_pick = false;
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"BackgroundCallCompaction:0", [&](void* /*arg*/) {
std::unique_lock<std::mutex> lock(worker_mutex);
worker_started = true;
worker_cv.notify_all();
worker_cv.wait(lock, [&]() { return allow_worker_to_pick; });
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
for (int file = 0; file < kCompactionTrigger; ++file) {
ASSERT_OK(Put(1, Key(file), "value"));
ASSERT_OK(Flush(1));
}
{
std::unique_lock<std::mutex> lock(worker_mutex);
worker_cv.wait(lock, [&]() { return worker_started; });
}
dbfull()->AbortCompactions(handles_[1]);
const Status drop_status = dbfull()->DropColumnFamily(handles_[1]);
{
std::lock_guard<std::mutex> guard(worker_mutex);
allow_worker_to_pick = true;
}
worker_cv.notify_all();
WaitForCompactOptions wait_options;
wait_options.timeout = std::chrono::seconds(10);
const Status wait_status = dbfull()->WaitForCompact(wait_options);
CleanupSyncPoints();
dbfull()->ResumeCompactions(handles_[1]);
ASSERT_OK(drop_status);
ASSERT_OK(wait_status);
}
TEST_F(DBCompactionAbortTest, NewCompactionWorkWhileAbortedIsParked) {
constexpr int kCompactionTrigger = 4;
Options options = CurrentOptions();
options.level0_file_num_compaction_trigger = kCompactionTrigger;
options.max_background_compactions = 1;
options.disable_auto_compactions = false;
CreateAndReopenWithCF({"target"}, options);
dbfull()->AbortCompactions(handles_[1]);
for (int file = 0; file < kCompactionTrigger; ++file) {
ASSERT_OK(Put(1, Key(file), "value"));
ASSERT_OK(Flush(1));
}
const int l0_files_before_resume = NumTableFilesAtLevel(0, 1);
WaitForCompactOptions wait_options;
wait_options.timeout = std::chrono::milliseconds(1);
const Status wait_status = dbfull()->WaitForCompact(wait_options);
dbfull()->ResumeCompactions(handles_[1]);
ASSERT_TRUE(wait_status.IsTimedOut());
ASSERT_GE(l0_files_before_resume, kCompactionTrigger);
ASSERT_OK(dbfull()->TEST_WaitForCompact());
ASSERT_LT(NumTableFilesAtLevel(0, 1), l0_files_before_resume);
}
TEST_F(DBCompactionAbortTest, AbortWaitsForCompactionCompletionCallback) {
constexpr int kCompactionTrigger = 4;
Options options = CurrentOptions();
options.level0_file_num_compaction_trigger = kCompactionTrigger;
options.max_background_compactions = 1;
options.disable_auto_compactions = false;
options.listeners.emplace_back(std::make_shared<EventListener>());
CreateAndReopenWithCF({"target"}, options);
std::mutex callback_mutex;
std::condition_variable callback_cv;
bool callback_started = false;
bool allow_callback_to_finish = false;
enum class AbortState { kNotObserved, kWaiting, kComplete };
AbortState abort_state = AbortState::kNotObserved;
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"DBImpl::NotifyOnCompactionCompleted::UnlockMutex", [&](void* /*arg*/) {
std::unique_lock<std::mutex> lock(callback_mutex);
callback_started = true;
callback_cv.notify_all();
callback_cv.wait(lock, [&]() { return allow_callback_to_finish; });
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"DBImpl::AbortCompactions:Waiting", [&](void* /*arg*/) {
std::lock_guard<std::mutex> guard(callback_mutex);
abort_state = AbortState::kWaiting;
callback_cv.notify_all();
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"DBImpl::AbortCompactions:Complete", [&](void* /*arg*/) {
std::lock_guard<std::mutex> guard(callback_mutex);
abort_state = AbortState::kComplete;
callback_cv.notify_all();
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
for (int file = 0; file < kCompactionTrigger; ++file) {
ASSERT_OK(Put(1, Key(file), "value"));
ASSERT_OK(Flush(1));
}
{
std::unique_lock<std::mutex> lock(callback_mutex);
callback_cv.wait(lock, [&]() { return callback_started; });
}
std::thread abort_thread([&]() { dbfull()->AbortCompactions(handles_[1]); });
bool returned_while_callback_blocked = false;
{
std::unique_lock<std::mutex> lock(callback_mutex);
callback_cv.wait(lock,
[&]() { return abort_state != AbortState::kNotObserved; });
returned_while_callback_blocked = abort_state == AbortState::kComplete;
}
{
std::lock_guard<std::mutex> guard(callback_mutex);
allow_callback_to_finish = true;
}
callback_cv.notify_all();
abort_thread.join();
CleanupSyncPoints();
dbfull()->ResumeCompactions(handles_[1]);
ASSERT_FALSE(returned_while_callback_blocked);
}
TEST_F(DBCompactionAbortTest,
DroppedColumnFamilyFilesPurgedAfterTrackedCompaction) {
constexpr int kCompactionTrigger = 4;
Options options = CurrentOptions();
options.level0_file_num_compaction_trigger = kCompactionTrigger;
options.max_background_compactions = 1;
options.disable_auto_compactions = false;
CreateAndReopenWithCF({"target"}, options);
std::mutex compaction_mutex;
std::condition_variable compaction_cv;
bool compaction_started = false;
bool allow_compaction_to_finish = false;
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"CompactionJob::Run():Start", [&](void* /*arg*/) {
std::unique_lock<std::mutex> lock(compaction_mutex);
compaction_started = true;
compaction_cv.notify_all();
compaction_cv.wait(lock, [&]() { return allow_compaction_to_finish; });
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
Random rnd(301);
for (int file = 0; file < kCompactionTrigger; ++file) {
for (int key = 0; key < 100; ++key) {
ASSERT_OK(Put(1, Key(key), rnd.RandomString(1000)));
}
ASSERT_OK(Flush(1));
}
{
std::unique_lock<std::mutex> lock(compaction_mutex);
compaction_cv.wait(lock, [&]() { return compaction_started; });
}
const int sst_files_before_drop = GetSstFileCount(dbname_);
const Status drop_status = dbfull()->DropColumnFamily(handles_[1]);
Status destroy_handle_status;
if (drop_status.ok()) {
destroy_handle_status = dbfull()->DestroyColumnFamilyHandle(handles_[1]);
if (destroy_handle_status.ok()) {
handles_.erase(handles_.begin() + 1);
}
}
{
std::lock_guard<std::mutex> guard(compaction_mutex);
allow_compaction_to_finish = true;
}
compaction_cv.notify_all();
const Status wait_status = dbfull()->TEST_WaitForCompact();
CleanupSyncPoints();
ASSERT_OK(drop_status);
ASSERT_OK(destroy_handle_status);
ASSERT_OK(wait_status);
ASSERT_GT(sst_files_before_drop, 0);
ASSERT_EQ(0, GetSstFileCount(dbname_));
}
TEST_F(DBCompactionAbortTest, ResumeCompactionsAfterCloseIsNoOp) {
auto* const default_column_family = dbfull()->DefaultColumnFamily();
dbfull()->AbortCompactions(default_column_family);
ASSERT_OK(dbfull()->Close());
dbfull()->ResumeCompactions(default_column_family);
}
TEST_F(DBCompactionAbortTest, AbortAndVerifyNoOutputFiles) {
Options options = CurrentOptions();
options.level0_file_num_compaction_trigger = 4;
+325 -2
View File
@@ -14,6 +14,7 @@
#include "db/blob/blob_index.h"
#include "db/db_test_util.h"
#include "db/dbformat.h"
#include "db/periodic_compaction_phaser.h"
#include "db/table_cache.h"
#include "env/mock_env.h"
#include "file/filename.h"
@@ -29,6 +30,8 @@
#include "test_util/sync_point.h"
#include "test_util/testutil.h"
#include "util/concurrent_task_limiter_impl.h"
#include "util/fastrange.h"
#include "util/hash.h"
#include "util/random.h"
#include "utilities/fault_injection_env.h"
#include "utilities/fault_injection_fs.h"
@@ -813,6 +816,23 @@ TEST_F(DBCompactionTest, CompactRangeBottomPri) {
int low_pri_count = 0;
int bottom_pri_count = 0;
bool bottom_running_seen = false;
std::atomic<bool> bottom_property_seen = false;
SyncPoint::GetInstance()->SetCallBack(
"CompactionJob::Run():Start", [&](void*) {
uint64_t total = 0;
uint64_t bottom = 0;
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningCompactions,
&total));
ASSERT_TRUE(db_->GetIntProperty(
DB::Properties::kNumRunningBottomCompactions, &bottom));
ASSERT_GE(total, bottom);
ASSERT_GE(total, 1);
ASSERT_LE(total, 2);
ASSERT_LE(bottom, 1);
if (bottom > 0) {
bottom_property_seen = true;
}
});
SyncPoint::GetInstance()->SetCallBack(
"BackgroundCallCompaction:1", [&](void*) {
if (dbfull()->TEST_NumRunningBottomCompactions() > 0) {
@@ -838,6 +858,7 @@ TEST_F(DBCompactionTest, CompactRangeBottomPri) {
ASSERT_EQ(1, low_pri_count);
ASSERT_EQ(1, bottom_pri_count);
ASSERT_TRUE(bottom_running_seen);
ASSERT_TRUE(bottom_property_seen);
ASSERT_EQ("0,0,2", FilesPerLevel(0));
// Recompact bottom most level uses bottom pool
@@ -1316,6 +1337,118 @@ TEST_F(DBCompactionTest, CompactionSstPartitionerNonTrivial) {
ASSERT_EQ("B", Get("bbbb1"));
}
TEST_F(DBCompactionTest, CompactionSstPartitionerPrefixFastPath) {
// Exercises the ShouldPartitionByPrefix() fast path in CompactionOutputs:
// multiple keys sharing a fixed prefix must stay together in one output file
// (no spurious cuts), with boundaries landing exactly at prefix changes.
Options options = CurrentOptions();
options.compaction_style = kCompactionStyleLevel;
options.level0_file_num_compaction_trigger = 3;
options.sst_partitioner_factory = NewSstPartitionerFixedPrefixFactory(4);
DestroyAndReopen(options);
// Three prefixes with multiple keys each, spread across two flushed files so
// that a non-trivial compaction merges them.
ASSERT_OK(Put("aaaa1", "v"));
ASSERT_OK(Put("aaaa3", "v"));
ASSERT_OK(Put("bbbb1", "v"));
ASSERT_OK(Put("cccc2", "v"));
ASSERT_OK(Flush());
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
ASSERT_OK(Put("aaaa2", "v"));
ASSERT_OK(Put("bbbb2", "v"));
ASSERT_OK(Put("cccc1", "v"));
ASSERT_OK(Flush());
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), nullptr, nullptr));
// Exactly one output file per distinct 4-byte prefix.
std::vector<LiveFileMetaData> files;
dbfull()->GetLiveFilesMetaData(&files);
ASSERT_EQ(3, files.size());
// Each file holds a whole prefix group (multiple keys => the fast path
// correctly did NOT cut within a shared prefix).
for (const auto& f : files) {
ASSERT_EQ(f.smallestkey.substr(0, 4), f.largestkey.substr(0, 4));
}
for (const std::string k :
{"aaaa1", "aaaa2", "aaaa3", "bbbb1", "bbbb2", "cccc1", "cccc2"}) {
ASSERT_EQ("v", Get(k));
}
}
TEST_F(DBCompactionTest, CompactionSstPartitionerPrefixShortKeyFallback) {
// Keys shorter than the fixed prefix length cannot be represented as a
// prefix, so ShouldPartitionByPrefix() returns no value and CompactionOutputs
// falls back to per-key ShouldPartition(). This also exercises short->long
// and long->short mode transitions across output file boundaries.
Options options = CurrentOptions();
options.compaction_style = kCompactionStyleLevel;
options.level0_file_num_compaction_trigger = 3;
options.sst_partitioner_factory = NewSstPartitionerFixedPrefixFactory(4);
DestroyAndReopen(options);
// "a" and "bb" are shorter than len 4 (fallback, each in its own file);
// "cccc1"/"cccc2" share prefix "cccc" (fast path, one file); "dd" is short
// again (fallback, own file).
ASSERT_OK(Put("a", "v"));
ASSERT_OK(Put("bb", "v"));
ASSERT_OK(Put("cccc1", "v"));
ASSERT_OK(Put("dd", "v"));
ASSERT_OK(Flush());
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
// Second file overlaps to force a non-trivial (rewriting) compaction.
ASSERT_OK(Put("cccc2", "v"));
ASSERT_OK(Put("a", "v2"));
ASSERT_OK(Flush());
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), nullptr, nullptr));
// "a" | "bb" | {"cccc1","cccc2"} | "dd" => 4 files.
std::vector<LiveFileMetaData> files;
dbfull()->GetLiveFilesMetaData(&files);
ASSERT_EQ(4, files.size());
ASSERT_EQ("v2", Get("a"));
for (const std::string k : {"bb", "cccc1", "cccc2", "dd"}) {
ASSERT_EQ("v", Get(k));
}
}
TEST_F(DBCompactionTest, CompactionSstPartitionerZeroLenNoSplit) {
// A zero-length fixed prefix yields an empty (but present) prefix from
// ShouldPartitionByPrefix(), which means "no further partitions": every key
// "starts with" the empty prefix, so the fast path never cuts.
Options options = CurrentOptions();
options.compaction_style = kCompactionStyleLevel;
options.level0_file_num_compaction_trigger = 3;
options.sst_partitioner_factory = NewSstPartitionerFixedPrefixFactory(0);
DestroyAndReopen(options);
ASSERT_OK(Put("aaaa1", "v1"));
ASSERT_OK(Put("bbbb1", "v1"));
ASSERT_OK(Flush());
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
// Overlapping key forces a non-trivial compaction (not a trivial move).
ASSERT_OK(Put("aaaa1", "v2"));
ASSERT_OK(Put("cccc1", "v2"));
ASSERT_OK(Flush());
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), nullptr, nullptr));
std::vector<LiveFileMetaData> files;
dbfull()->GetLiveFilesMetaData(&files);
ASSERT_EQ(1, files.size());
ASSERT_EQ("v2", Get("aaaa1"));
ASSERT_EQ("v1", Get("bbbb1"));
ASSERT_EQ("v2", Get("cccc1"));
}
TEST_F(DBCompactionTest, ZeroSeqIdCompaction) {
Options options = CurrentOptions();
options.compaction_style = kCompactionStyleLevel;
@@ -5353,6 +5486,7 @@ TEST_F(DBCompactionTest, LevelPeriodicCompaction) {
for (bool if_open_all_files : {false, true}) {
Options options = CurrentOptions();
options.periodic_compaction_seconds = 48 * 60 * 60; // 2 days
options.periodic_compaction_phase_recovery_percent = 0;
if (if_open_all_files) {
options.max_open_files = -1; // needed for ttl compaction
} else {
@@ -5448,6 +5582,169 @@ TEST_F(DBCompactionTest, LevelPeriodicCompaction) {
}
}
TEST_F(DBCompactionTest, PeriodicCompactionPhaseTriggerTime) {
// Unit-tests PeriodicCompactionPhaser::TriggerTime, the core of
// preferred-phase periodic compaction (see
// DBOptions::periodic_compaction_phase_recovery_percent).
using Params = PeriodicCompactionPhaseParams;
const uint64_t kN = 1000; // periodic_compaction_seconds
// The trigger-time helper treats seed_hash as an opaque value; use a fixed
// constant and derive the resulting preferred-phase target the same way
// production does (FastRange64 of the seed over the interval).
const uint64_t kSeed = 0x9e3779b97f4a7c15ULL;
const uint64_t target = FastRange64(kSeed, kN);
ASSERT_LT(target, kN);
// recovery_percent == 0 => classic behavior: trigger at file_mod + N.
{
Params p;
p.seed_hash = kSeed;
p.recovery_percent = 0;
for (uint64_t fm : {uint64_t{1}, uint64_t{7}, uint64_t{12345}}) {
ASSERT_EQ(PeriodicCompactionPhaser::TriggerTime(fm, kN, p), fm + kN);
}
}
// Steady state (anchor in the distant past): trigger is pulled earlier by
// recovery_percent% of the phase offset, and never later than the deadline.
{
Params p;
p.seed_hash = kSeed;
p.recovery_percent = 50;
p.anchor_time = 0;
const uint64_t fm = 500;
const uint64_t natural = fm + kN;
const uint64_t offset = (fm % kN + kN - target) % kN;
const uint64_t expected = natural - offset * 50 / 100;
ASSERT_EQ(PeriodicCompactionPhaser::TriggerTime(fm, kN, p), expected);
ASSERT_LE(expected, natural);
}
// Geometric convergence: repeatedly re-stamp the file at its trigger time and
// confirm the phase error is non-increasing and converges to (near) zero.
for (int recovery_percent : {50, 100}) {
Params p;
p.seed_hash = kSeed;
p.recovery_percent = recovery_percent;
p.anchor_time = 0;
uint64_t fm = 123;
uint64_t prev_error = kN;
for (int cycle = 0; cycle < 64; ++cycle) {
const uint64_t trig = PeriodicCompactionPhaser::TriggerTime(fm, kN, p);
ASSERT_LE(trig, fm + kN); // never later than the deadline
const uint64_t error = (trig % kN + kN - target) % kN;
ASSERT_LE(error, prev_error);
prev_error = error;
fm = trig;
}
// rp=100 pulls the whole gap so it reaches the phase exactly; smaller rates
// decay geometrically and leave a tiny integer-rounding residue.
if (recovery_percent == 100) {
ASSERT_EQ(prev_error, uint64_t{0});
} else {
ASSERT_LE(prev_error, uint64_t{1});
}
}
// Already past the hard deadline when phasing took effect (e.g. the DB was
// down, or periodic_compaction_seconds was turned down): instead of firing
// the whole cohort immediately, spread it over the first quarter-period after
// the anchor on an absolute (epoch-aligned) grid of period N/4.
{
Params p;
p.seed_hash = kSeed;
p.recovery_percent = 50;
p.anchor_time = 200;
const uint64_t n = 100;
const uint64_t grid = n / 4; // 25
const uint64_t phase = FastRange64(kSeed, grid);
// natural deadline (10 + 100 = 110) is before the anchor (200).
const uint64_t expected = 200 + (phase + grid - 200 % grid) % grid;
const uint64_t trig = PeriodicCompactionPhaser::TriggerTime(10, n, p);
ASSERT_EQ(trig, expected);
ASSERT_GE(trig, uint64_t{200}); // never before the anchor
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.
{
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 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);
}
}
TEST_F(DBCompactionTest, PeriodicCompactionPhaseCfSpread) {
// Unit-tests PeriodicCompactionPhaser::CfPhaseSeedHash: a DB's column
// families are spread quasi-uniformly around the DB base phase via the
// golden-ratio recurrence, for any base and any number of CFs.
using Phaser = PeriodicCompactionPhaser;
// cf_id 0 leaves the base phase unchanged; distinct cf_ids -> distinct
// phases.
for (uint64_t base : {uint64_t{0}, uint64_t{0x9e3779b97f4a7c15ULL},
uint64_t{12345678901234567ULL}, ~uint64_t{0}}) {
ASSERT_EQ(Phaser::CfPhaseSeedHash(base, 0), base);
std::vector<uint64_t> hs;
for (uint32_t id = 0; id < 64; ++id) {
hs.push_back(Phaser::CfPhaseSeedHash(base, id));
}
std::sort(hs.begin(), hs.end());
for (size_t i = 1; i < hs.size(); ++i) {
ASSERT_NE(hs[i], hs[i - 1]) << "duplicate CF phase seed at base=" << base;
}
}
// Low-discrepancy: for N CFs the phases over an interval are well spread --
// no gap larger than ~2x the average (a hash would allow chance clustering).
for (uint64_t base : {uint64_t{0}, uint64_t{0xdeadbeefcafef00dULL}}) {
for (uint32_t N : {uint32_t{3}, uint32_t{8}, uint32_t{16}, uint32_t{40}}) {
const uint64_t n = 100000;
std::vector<uint64_t> phases;
for (uint32_t id = 0; id < N; ++id) {
phases.push_back(FastRange64(Phaser::CfPhaseSeedHash(base, id), n));
}
std::sort(phases.begin(), phases.end());
uint64_t max_gap = n - phases.back() + phases.front(); // wrap-around gap
for (size_t i = 1; i < phases.size(); ++i) {
max_gap = std::max(max_gap, phases[i] - phases[i - 1]);
}
ASSERT_LE(max_gap, 2 * (n / N))
<< "base=" << base << " N=" << N << " max_gap=" << max_gap;
}
}
}
TEST_F(DBCompactionTest, PeriodicCompactionPhaseOptions) {
// Verifies periodic_compaction_phase_recovery_percent is plumbed and
// dynamically settable.
Options options = CurrentOptions();
options.periodic_compaction_phase_recovery_percent = 25;
DestroyAndReopen(options);
ASSERT_EQ(dbfull()->GetDBOptions().periodic_compaction_phase_recovery_percent,
25);
ASSERT_OK(dbfull()->SetDBOptions(
{{"periodic_compaction_phase_recovery_percent", "0"}}));
ASSERT_EQ(dbfull()->GetDBOptions().periodic_compaction_phase_recovery_percent,
0);
}
TEST_F(DBCompactionTest, LevelPeriodicCompactionOffpeak) {
// This test simply checks if offpeak adjustment works in Leveled
// Compactions. For testing offpeak periodic compactions in various
@@ -5465,6 +5762,7 @@ TEST_F(DBCompactionTest, LevelPeriodicCompactionOffpeak) {
Options options = CurrentOptions();
options.ttl = 0;
options.periodic_compaction_seconds = 5 * kSecondsPerDay; // 5 days
options.periodic_compaction_phase_recovery_percent = 0;
// In the case where all files are opened and doing DB restart
// forcing the file creation time in manifest file to be 0 to
// simulate the case of reading from an old version.
@@ -5612,6 +5910,7 @@ TEST_F(DBCompactionTest, LevelPeriodicCompactionWithOldDB) {
// Forward the clock by 2 days.
env_->MockSleepForSeconds(2 * 24 * 60 * 60);
options.periodic_compaction_seconds = 1 * 24 * 60 * 60; // 1 day
options.periodic_compaction_phase_recovery_percent = 0;
Reopen(options);
ASSERT_OK(dbfull()->TEST_WaitForCompact());
@@ -5630,6 +5929,7 @@ TEST_F(DBCompactionTest, LevelPeriodicAndTtlCompaction) {
Options options = CurrentOptions();
options.ttl = 10 * 60 * 60; // 10 hours
options.periodic_compaction_seconds = 48 * 60 * 60; // 2 days
options.periodic_compaction_phase_recovery_percent = 0;
options.max_open_files = -1; // needed for both periodic and ttl compactions
env_->SetMockSleep();
options.env = env_;
@@ -5712,6 +6012,7 @@ TEST_F(DBCompactionTest, LevelTtlBooster) {
Options options = CurrentOptions();
options.ttl = 10 * 60 * 60; // 10 hours
options.periodic_compaction_seconds = 480 * 60 * 60; // very long
options.periodic_compaction_phase_recovery_percent = 0;
options.level0_file_num_compaction_trigger = 2;
options.max_bytes_for_level_base = 5 * uint64_t{kNumKeysPerFile * kValueSize};
options.max_open_files = -1; // needed for both periodic and ttl compactions
@@ -6156,6 +6457,7 @@ TEST_F(DBCompactionTest, CompactRangeFlushOverlappingMemtable) {
TEST_F(DBCompactionTest, CompactionStatsTest) {
Options options = CurrentOptions();
options.level0_file_num_compaction_trigger = 2;
options.max_background_compactions = 1;
CompactionStatsCollector* collector = new CompactionStatsCollector();
options.listeners.emplace_back(collector);
DestroyAndReopen(options);
@@ -6163,9 +6465,13 @@ TEST_F(DBCompactionTest, CompactionStatsTest) {
// Verify that the internal statistics for num_running_compactions and
// num_running_compaction_sorted_runs start and end at valid states
uint64_t num_running_compactions = 0;
uint64_t num_running_bottom_compactions = 0;
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningCompactions,
&num_running_compactions));
ASSERT_EQ(num_running_compactions, 0);
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningBottomCompactions,
&num_running_bottom_compactions));
ASSERT_EQ(0, num_running_compactions);
ASSERT_EQ(0, num_running_bottom_compactions);
uint64_t num_running_compaction_sorted_runs = 0;
ASSERT_TRUE(
db_->GetIntProperty(DB::Properties::kNumRunningCompactionSortedRuns,
@@ -6174,6 +6480,18 @@ TEST_F(DBCompactionTest, CompactionStatsTest) {
// Check that the stat actually gets changed some time between the start and
// end of compaction
std::atomic<bool> sorted_runs_count_incremented = false;
std::atomic<bool> running_compaction_seen = false;
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"CompactionJob::Run():Start", [&](void*) {
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningCompactions,
&num_running_compactions));
ASSERT_TRUE(
db_->GetIntProperty(DB::Properties::kNumRunningBottomCompactions,
&num_running_bottom_compactions));
ASSERT_EQ(1, num_running_compactions);
ASSERT_EQ(0, num_running_bottom_compactions);
running_compaction_seen = true;
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"CompactionMergingIterator::UpdateInternalStats",
[&](void*) { sorted_runs_count_incremented = true; });
@@ -6197,11 +6515,15 @@ TEST_F(DBCompactionTest, CompactionStatsTest) {
// to process
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningCompactions,
&num_running_compactions));
ASSERT_EQ(num_running_compactions, 0);
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningBottomCompactions,
&num_running_bottom_compactions));
ASSERT_EQ(0, num_running_compactions);
ASSERT_EQ(0, num_running_bottom_compactions);
ASSERT_TRUE(
db_->GetIntProperty(DB::Properties::kNumRunningCompactionSortedRuns,
&num_running_compaction_sorted_runs));
ASSERT_EQ(num_running_compaction_sorted_runs, 0);
ASSERT_TRUE(running_compaction_seen);
ASSERT_TRUE(sorted_runs_count_incremented);
SyncPoint::GetInstance()->DisableProcessing();
@@ -12240,6 +12562,7 @@ TEST_F(DBCompactionTest, PeriodicTask) {
options.statistics = CreateDBStatistics();
int kPeriodicCompactionSeconds = 7 * 24 * 60 * 60; // 1 week
options.periodic_compaction_seconds = kPeriodicCompactionSeconds;
options.periodic_compaction_phase_recovery_percent = 0;
options.num_levels = 50;
auto listener = std::make_shared<PeriodicCompactionListener>();
options.listeners.push_back(listener);
+79 -2
View File
@@ -8,6 +8,7 @@
#include <cstdint>
#include <memory>
#include <string>
#include <unordered_set>
#include <vector>
#include "db/db_impl/db_impl.h"
@@ -20,8 +21,8 @@
#include "rocksdb/db.h"
#include "rocksdb/env.h"
#include "rocksdb/metadata.h"
#include "rocksdb/transaction_log.h"
#include "rocksdb/types.h"
#include "rocksdb/wal_iterator.h"
#include "test_util/sync_point.h"
#include "util/file_checksum_helper.h"
#include "util/mutexlock.h"
@@ -195,14 +196,64 @@ Status DBImpl::GetCurrentWalFile(std::unique_ptr<WalFile>* current_wal_file) {
return wal_manager_.GetLiveWalFile(current_logfile_number, current_wal_file);
}
Status DBImpl::AppendColumnFamilyDropsToManifest(
const std::string& manifest_path, uint64_t manifest_size,
const std::vector<uint32_t>& cf_ids) {
const WriteOptions write_options;
// Snapshot manifest_preallocation_size_ under the DB mutex per VersionSet's
// contract (the field is mutated by SetDBOptions under this mutex).
uint64_t preallocation_size = 0;
{
InstrumentedMutexLock l(&mutex_);
preallocation_size = versions_->manifest_preallocation_size_;
}
return versions_->AppendColumnFamilyDropsToManifest(
manifest_path, manifest_size, cf_ids, write_options, preallocation_size);
}
Status DBImpl::GetLiveFilesStorageInfo(
const LiveFilesStorageInfoOptions& opts,
std::vector<LiveFileStorageInfo>* files) {
return GetLiveFilesStorageInfoImpl(opts, /*include_cf_ids=*/{}, files,
/*excluded_cf_ids=*/nullptr);
}
Status DBImpl::GetLiveFilesStorageInfoForSubsetCheckpoint(
const LiveFilesStorageInfoOptions& opts,
const std::vector<uint32_t>& include_cf_ids,
std::vector<LiveFileStorageInfo>* files,
std::vector<uint32_t>* excluded_cf_ids) {
// Internal contract: caller must coalesce ids and always include the default
// CF (it cannot be dropped, so a subset checkpoint without it would produce
// an unopenable DB). CheckpointImpl enforces this before calling here.
assert(!include_cf_ids.empty());
assert(std::find(include_cf_ids.begin(), include_cf_ids.end(),
default_cf_handle_->GetID()) != include_cf_ids.end());
assert(excluded_cf_ids != nullptr);
return GetLiveFilesStorageInfoImpl(opts, include_cf_ids, files,
excluded_cf_ids);
}
Status DBImpl::GetLiveFilesStorageInfoImpl(
const LiveFilesStorageInfoOptions& opts,
const std::vector<uint32_t>& include_cf_ids,
std::vector<LiveFileStorageInfo>* files,
std::vector<uint32_t>* excluded_cf_ids) {
// To avoid returning partial results, only move results to files on success.
assert(files);
files->clear();
std::vector<LiveFileStorageInfo> results;
const bool cf_subset = !include_cf_ids.empty();
assert(!cf_subset || excluded_cf_ids != nullptr);
const std::unordered_set<uint32_t> include_cf_id_set(include_cf_ids.begin(),
include_cf_ids.end());
// Tracks which requested ids remain to be observed as live under the DB
// mutex; any id still present after the enumeration was either dropped
// before or during the checkpoint (race) and must not silently disappear
// from the checkpoint.
std::unordered_set<uint32_t> unseen_include_cf_ids = include_cf_id_set;
// NOTE: This implementation was largely migrated from Checkpoint.
VectorWalPtr live_wal_files;
@@ -282,6 +333,14 @@ Status DBImpl::GetLiveFilesStorageInfo(
if (cfd->IsDropped()) {
continue;
}
if (cf_subset &&
include_cf_id_set.find(cfd->GetID()) == include_cf_id_set.end()) {
excluded_cf_ids->push_back(cfd->GetID());
continue;
}
if (cf_subset) {
unseen_include_cf_ids.erase(cfd->GetID());
}
VersionStorageInfo& vsi = *cfd->current()->storage_info();
auto& cf_paths = cfd->ioptions().cf_paths;
@@ -343,9 +402,27 @@ Status DBImpl::GetLiveFilesStorageInfo(
}
}
// Release-build validation: every id the subset-checkpoint caller asked for
// must have been observed as live under the mutex. If any is missing, it was
// dropped before we ran (stale handle) or lost a race with a concurrent
// drop; either way the checkpoint would silently omit it, violating the
// caller's contract that the checkpoint contains the requested CFs.
if (cf_subset && !unseen_include_cf_ids.empty()) {
mutex_.Unlock();
return Status::InvalidArgument(
"Requested column family id was not live at checkpoint time");
}
// Capture some final info before releasing mutex
const uint64_t manifest_number = versions_->manifest_file_number();
const uint64_t manifest_size = versions_->manifest_file_size();
uint64_t manifest_size = versions_->manifest_file_size();
if (cf_subset && !excluded_cf_ids->empty()) {
Status manifest_s = versions_->GetManifestAppendBoundary(&manifest_size);
if (!manifest_s.ok()) {
mutex_.Unlock();
return manifest_s;
}
}
const uint64_t options_number = versions_->options_file_number();
const uint64_t options_size = versions_->options_file_size_;
const uint64_t min_log_num = MinLogNumberToKeep();
+210
View File
@@ -20,9 +20,11 @@
#include "port/stack_trace.h"
#include "rocksdb/utilities/transaction_db.h"
#include "table/block_based/block_based_table_builder.h"
#include "table/format.h"
#include "test_util/sync_point.h"
#include "test_util/testutil.h"
#include "util/cast_util.h"
#include "util/defer.h"
#include "util/mutexlock.h"
#include "utilities/fault_injection_env.h"
#include "utilities/fault_injection_fs.h"
@@ -3723,6 +3725,108 @@ TEST_F(DBFlushTest, NonAtomicNormalFlushAbortWhenBGError) {
SyncPoint::GetInstance()->DisableProcessing();
}
TEST_F(DBFlushTest, NonRecoveryFlushDoesNotStarveErrorRecovery) {
constexpr uint64_t kRecoveryTimeoutMicros = 15 * 1000 * 1000;
constexpr int kLateWriteCount = 32;
Options opts = CurrentOptions();
opts.atomic_flush = true;
opts.memtable_factory.reset(test::NewSpecialSkipListFactory(1));
opts.max_write_buffer_number = 64;
opts.max_background_flushes = 1;
DestroyAndReopen(opts);
env_->SetBackgroundThreads(1, Env::HIGH);
std::atomic_int flush_write_table_count{0};
port::Mutex wait_mutex;
port::CondVar wait_cv(&wait_mutex);
bool recovery_wait_started = false;
bool recovery_succeeded = false;
SyncPoint::GetInstance()->ClearAllCallBacks();
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->SetCallBack(
"FlushJob::WriteLevel0Table:s", [&](void* s_ptr) {
if (flush_write_table_count.fetch_add(1) == 0) {
Status* s = static_cast<Status*>(s_ptr);
IOStatus io_error = IOStatus::IOError("injected foobar");
io_error.SetRetryable(true);
*s = io_error;
TEST_SYNC_POINT(
"DBFlushTest::NonRecoveryFlushDoesNotStarveErrorRecovery:"
"FirstFlushFailed");
TEST_SYNC_POINT(
"DBFlushTest::NonRecoveryFlushDoesNotStarveErrorRecovery:"
"ContinueFirstFlush");
}
});
SyncPoint::GetInstance()->SetCallBack(
"DBImpl::ResumeImpl:BeforeWaitForBackgroundWork", [&](void*) {
MutexLock l(&wait_mutex);
recovery_wait_started = true;
wait_cv.SignalAll();
});
SyncPoint::GetInstance()->SetCallBack(
"RecoverFromRetryableBGIOError:RecoverSuccess", [&](void*) {
MutexLock l(&wait_mutex);
recovery_succeeded = true;
wait_cv.SignalAll();
});
SyncPoint::GetInstance()->LoadDependency(
{{"DBFlushTest::NonRecoveryFlushDoesNotStarveErrorRecovery:"
"FirstFlushFailed",
"DBFlushTest::NonRecoveryFlushDoesNotStarveErrorRecovery:"
"FailureObserved"},
{"DBFlushTest::NonRecoveryFlushDoesNotStarveErrorRecovery:"
"ReleaseFirstFlush",
"DBFlushTest::NonRecoveryFlushDoesNotStarveErrorRecovery:"
"ContinueFirstFlush"}});
SyncPoint::GetInstance()->EnableProcessing();
auto wait_until = [&](const auto& predicate) {
const uint64_t abs_time =
SystemClock::Default()->NowMicros() + kRecoveryTimeoutMicros;
MutexLock l(&wait_mutex);
while (!predicate()) {
if (wait_cv.TimedWait(abs_time)) {
return false;
}
}
return true;
};
ASSERT_OK(Put(Key(1), "val1"));
ASSERT_OK(Put(Key(2), "val2"));
TEST_SYNC_POINT(
"DBFlushTest::NonRecoveryFlushDoesNotStarveErrorRecovery:"
"FailureObserved");
TEST_SYNC_POINT(
"DBFlushTest::NonRecoveryFlushDoesNotStarveErrorRecovery:"
"ReleaseFirstFlush");
const bool recovery_wait_reached =
wait_until([&] { return recovery_wait_started; });
Status late_write_status;
if (recovery_wait_reached) {
for (int key = 3; key < 3 + kLateWriteCount && late_write_status.ok();
++key) {
late_write_status = Put(Key(key), "val");
}
}
const bool recovered = wait_until([&] { return recovery_succeeded; });
if (!recovered) {
IOStatus cleanup_error = IOStatus::IOError("stop stuck error recovery");
dbfull()->TEST_SetBGError(cleanup_error, BackgroundErrorReason::kFlush);
dbfull()->TEST_SignalAllBgCv();
}
SyncPoint::GetInstance()->ClearAllCallBacks();
SyncPoint::GetInstance()->DisableProcessing();
ASSERT_TRUE(recovery_wait_reached);
ASSERT_OK(late_write_status);
ASSERT_TRUE(recovered);
}
TEST_F(DBFlushTest, DBStuckAfterAtomicFlushError) {
// Test for a bug with atomic flush where DB can become stuck
// after a flush error. A repro timeline:
@@ -4007,6 +4111,112 @@ INSTANTIATE_TEST_CASE_P(
// larger than the max allowed padding size
testing::Values(4, kLowSpaceOverheadRatio)));
// super_block_alignment pads data blocks, giving the padded block's index entry
// a non-contiguous handle. At format_version <= 7 the writer forces that index
// entry to shared==0 (full key AND full value). format_version 8 instead uses
// the index value-delta escape (see IndexValue::EncodeTo), keeping the key
// delta-encoded and the restart cadence intact. This verifies that fv8 returns
// byte-identical read results to fv7 for the same keys (also with
// separate_key_value_in_data_block, the intersection that motivated the
// escape), that padding -- and hence the escape -- is actually exercised, and
// that fv8 index values are delta encoded.
TEST_F(DBFlushTest, SuperBlockAlignmentValueDeltaEscape) {
// Writing the unpublished draft format_version 8 requires this opt-in.
SaveAndRestore<bool> allow_draft(&TEST_AllowUnsupportedFormatVersion(), true);
constexpr int kKeyCount = 5000;
auto format_key = [](int i) {
char buf[16];
snprintf(buf, sizeof(buf), "%010d", i);
return std::string(buf);
};
// Deterministic values so fv7 and fv8 see identical input.
Random rnd(test::RandomSeed());
std::vector<std::string> values;
values.reserve(kKeyCount);
for (int i = 0; i < kKeyCount; ++i) {
values.push_back(rnd.RandomString(static_cast<int>(rnd.Uniform(1000))));
}
auto build_and_read = [&](uint32_t format_version, bool separate_kv,
std::vector<std::string>* got_get,
std::vector<std::string>* got_iter, int* pad_count,
bool* index_delta_encoded) {
Options options = CurrentOptions();
options.compression = kNoCompression;
BlockBasedTableOptions bbto;
bbto.format_version = format_version;
bbto.index_block_restart_interval = 3; // > 1 so value delta encoding is on
bbto.super_block_alignment_size = 16 * 1024;
bbto.super_block_alignment_space_overhead_ratio = 8;
bbto.separate_key_value_in_data_block = separate_kv;
options.table_factory.reset(NewBlockBasedTableFactory(bbto));
DestroyAndReopen(options);
int local_pad = 0;
SyncPoint::GetInstance()->SetCallBack(
"BlockBasedTableBuilder::WriteMaybeCompressedBlock:SuperBlockAlignment",
[&local_pad](void*) { local_pad++; });
SyncPoint::GetInstance()->EnableProcessing();
for (int i = 0; i < kKeyCount; ++i) {
ASSERT_OK(Put(format_key(i), values[i]));
}
ASSERT_OK(Flush());
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
*pad_count = local_pad;
got_get->clear();
for (int i = 0; i < kKeyCount; ++i) {
PinnableSlice v;
ASSERT_OK(Get(format_key(i), &v));
got_get->push_back(v.ToString());
}
got_iter->clear();
{
std::unique_ptr<Iterator> it(db_->NewIterator(ReadOptions()));
for (it->SeekToFirst(); it->Valid(); it->Next()) {
ASSERT_OK(it->status());
got_iter->push_back(it->value().ToString());
}
ASSERT_OK(it->status());
}
// Confirm the index value encoding of the produced SST(s).
TablePropertiesCollection props;
ASSERT_OK(db_->GetPropertiesOfAllTables(&props));
ASSERT_FALSE(props.empty());
*index_delta_encoded = true;
for (const auto& kv : props) {
if (kv.second->index_value_is_delta_encoded == 0) {
*index_delta_encoded = false;
}
}
};
for (bool separate_kv : {false, true}) {
SCOPED_TRACE("separate_kv=" + std::to_string(separate_kv));
std::vector<std::string> get7, iter7, get8, iter8;
int pad7 = 0, pad8 = 0;
bool delta7 = false, delta8 = false;
build_and_read(7, separate_kv, &get7, &iter7, &pad7, &delta7);
build_and_read(/*format_version=*/8, separate_kv, &get8, &iter8, &pad8,
&delta8);
// Reads must be identical across versions.
ASSERT_EQ(get7, get8);
ASSERT_EQ(iter7, iter8);
ASSERT_EQ(static_cast<int>(iter8.size()), kKeyCount);
// Padding (the non-contiguity that drives the escape) actually happened.
EXPECT_GT(pad8, 0);
// fv8 index values are delta encoded, so the escape path was exercised.
EXPECT_TRUE(delta8);
}
}
// Test that when the table builder's io_status becomes bad during flush
// (simulating write fault injection), BuildTable properly propagates the
// builder's IO error instead of producing a misleading Corruption from the
@@ -6,6 +6,10 @@
#include "util/coro_utils.h"
#if defined(USE_COROUTINES) && defined(WITH_COROUTINES)
#include "util/coro_stats_util.h"
#endif // USE_COROUTINES && WITH_COROUTINES
#if defined(WITHOUT_COROUTINES) || \
(defined(USE_COROUTINES) && defined(WITH_COROUTINES))
@@ -14,6 +18,10 @@ namespace ROCKSDB_NAMESPACE {
DEFINE_SYNC_AND_ASYNC(Status, CompactedDBImpl::Get)
(const ReadOptions& _read_options, ColumnFamilyHandle*, const Slice& key,
PinnableSlice* value, std::string* timestamp) {
#ifdef WITH_COROUTINES
INSTALL_COROUTINE_STATS_CONTEXT_SCOPE(
immutable_db_options_.fs->GetReadExecutor(), immutable_db_options_.env);
#endif
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
_read_options.io_activity != Env::IOActivity::kGet) {
CO_RETURN Status::InvalidArgument(
@@ -102,6 +110,10 @@ DEFINE_SYNC_AND_ASYNC(void, CompactedDBImpl::MultiGet)
ColumnFamilyHandle** /*column_families*/, const Slice* keys,
PinnableSlice* values, std::string* timestamps, Status* statuses,
const bool /*sorted_input*/) {
#ifdef WITH_COROUTINES
INSTALL_COROUTINE_STATS_CONTEXT_SCOPE(
immutable_db_options_.fs->GetReadExecutor(), immutable_db_options_.env);
#endif
assert(user_comparator_);
Status s;
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
+462 -140
View File
@@ -15,13 +15,6 @@
#include <alloca.h>
#endif
#if USE_COROUTINES
#include "folly/Executor.h"
#include "folly/coro/Invoke.h"
#include "folly/executors/IOExecutor.h"
#include "folly/io/async/EventBase.h"
#endif // USE_COROUTINES
#include <cinttypes>
#include <cstdio>
#include <deque>
@@ -67,8 +60,9 @@
#include "db/range_tombstone_fragmenter.h"
#include "db/table_cache.h"
#include "db/table_properties_collector.h"
#include "db/transaction_log_impl.h"
#include "db/version_set.h"
#include "db/wal_iterator_impl.h"
#include "db/wide/lazy_wide_columns_helper.h"
#include "db/wide/wide_column_serialization.h"
#include "db/wide/wide_columns_helper.h"
#include "db/write_batch_internal.h"
@@ -91,6 +85,7 @@
#include "options/cf_options.h"
#include "options/options_helper.h"
#include "options/options_parser.h"
#include "rocksdb/lazy_wide_columns.h"
#include "util/udt_util.h"
#ifdef ROCKSDB_JEMALLOC
#include "port/jemalloc_helper.h"
@@ -135,6 +130,15 @@
#include "util/udt_util.h"
#include "utilities/trace/replayer_impl.h"
#if USE_COROUTINES
#include "folly/Executor.h"
#include "folly/coro/Nothrow.h"
#include "folly/coro/Task.h"
#include "folly/executors/IOExecutor.h"
#include "folly/io/async/EventBase.h"
#include "rocksdb/coro_db.h"
#endif // USE_COROUTINES
namespace ROCKSDB_NAMESPACE {
const std::string kDefaultColumnFamilyName("default");
@@ -244,6 +248,11 @@ DBImpl::DBImpl(const DBOptions& options, const std::string& dbname,
blob_callback_(immutable_db_options_.sst_file_manager.get(), &mutex_,
&error_handler_, &event_logger_,
immutable_db_options_.listeners, dbname_) {
#if USE_COROUTINES
immutable_db_options_.fs->SetReadIOExecutorThreads(
immutable_db_options_.read_io_executor_threads);
#endif // USE_COROUTINES
// !batch_per_trx_ implies seq_per_batch_ because it is only unset for
// WriteUnprepared, which should use seq_per_batch_.
assert(batch_per_txn_ || seq_per_batch_);
@@ -338,6 +347,22 @@ Status DBImpl::ResumeImpl(DBRecoverContext context,
const ReadOptions read_options(io_activity);
const WriteOptions write_options(io_activity);
assert(static_cast<size_t>(unscheduled_flushes_) <= flush_queue_.size());
// Recovery rebuilds flush requests for every column family with pending
// immutable data after scheduled workers exit, so every existing request is
// redundant and safe to remove. A worker can exit on the background error
// without popping a request, leaving it out of unscheduled_flushes_.
while (!flush_queue_.empty()) {
FlushRequest flush_req = PopFirstFromFlushQueue();
for (const auto& item : flush_req.cfd_to_max_mem_id_to_persist) {
ColumnFamilyData* cfd = item.first;
assert(cfd);
cfd->UnrefAndTryDelete();
}
}
unscheduled_flushes_ = 0;
TEST_SYNC_POINT("DBImpl::ResumeImpl:BeforeWaitForBackgroundWork");
WaitForBackgroundWork();
TEST_SYNC_POINT("DBImpl::ResumeImpl:Start");
@@ -465,9 +490,8 @@ Status DBImpl::ResumeImpl(DBRecoverContext context,
s = Status::ShutdownInProgress();
}
if (s.ok() && context.flush_after_recovery) {
// Since we drop all non-recovery flush requests during recovery,
// and new memtable may fill up during recovery,
// schedule one more round of flush.
// Normal flush requests are discarded during recovery, and a new memtable
// may fill while recovery releases the DB mutex. Schedule a catch-up flush.
Status status = RetryFlushesForErrorRecovery(
FlushReason::kCatchUpAfterErrorRecovery, false /* wait */);
if (!status.ok()) {
@@ -665,8 +689,9 @@ void DBImpl::MaybeInitBlobDirectWriteColumnFamily(
[vs = versions_.get()]() { return vs->NewFileNumber(); }, fs_.get(),
immutable_db_options_.clock, stats_, file_options_, dbname_,
column_family_name, cf_options.blob_file_size,
immutable_db_options_.use_fsync, cfd->blob_file_cache(), &blob_callback_,
immutable_db_options_.listeners,
immutable_db_options_.use_fsync,
cf_options.blob_file_writable_file_max_buffer_size,
cfd->blob_file_cache(), &blob_callback_, immutable_db_options_.listeners,
immutable_db_options_.file_checksum_gen_factory.get(),
immutable_db_options_.checksum_handoff_file_types, io_tracer_, db_id_,
db_session_id_, immutable_db_options_.info_log.get());
@@ -878,6 +903,13 @@ Status DBImpl::CloseHelper() {
cfd->UnrefAndTryDelete();
}
for (auto* cfd : parked_compaction_cfds_) {
assert(cfd->queued_for_compaction());
cfd->set_queued_for_compaction(false);
cfd->UnrefAndTryDelete();
}
parked_compaction_cfds_.clear();
if (default_cf_handle_ != nullptr || persist_stats_cf_handle_ != nullptr) {
// we need to delete handle outside of lock because it does its own locking
mutex_.Unlock();
@@ -1645,6 +1677,21 @@ Status DBImpl::SetOptions(
VersionEdit dummy_edit;
dummy_edit.MarkNoManifestWriteDummy();
TEST_SYNC_POINT_CALLBACK("DBImpl::SetOptions:dummy_edit", &dummy_edit);
// If any CF is changing periodic_compaction_seconds, (re)anchor phasing to
// now BEFORE the new Version is built below, so the new Version's scoring
// spreads a turn-down's newly past-due cohort over the phase grid (within
// ~N/4 of now) instead of firing it all at once (a herd). Anchoring is
// DB-level and benign to CFs that are not changing their interval.
bool changing_periodic_compaction_seconds = false;
for (const auto& cfd_opts : column_family_datas) {
if (cfd_opts.second->count("periodic_compaction_seconds") > 0) {
changing_periodic_compaction_seconds = true;
break;
}
}
if (changing_periodic_compaction_seconds) {
versions_->ReanchorCompactionPhase();
}
for (const auto& cfd_opts : column_family_datas) {
auto* cfd = cfd_opts.first;
const auto* options_map_ptr = cfd_opts.second;
@@ -1675,7 +1722,13 @@ Status DBImpl::SetOptions(
// options to file, otherwise there will be a deadlock with writer
// thread.
for (const auto& cfd_opts : column_family_datas) {
InstallSuperVersionForConfigChange(cfd_opts.first, &sv_context);
auto* cfd = cfd_opts.first;
InstallSuperVersionForConfigChange(cfd, &sv_context);
if (auto* blob_partition_manager = cfd->blob_partition_manager()) {
blob_partition_manager->SetBlobWriterMaxBufferSize(
cfd->GetLatestMutableCFOptions()
.blob_file_writable_file_max_buffer_size);
}
}
persist_options_status =
@@ -2532,6 +2585,17 @@ static void CleanupSuperVersionHandle(void* arg1, void* /*arg2*/) {
delete sv_handle;
}
void DBImpl::TransferSuperVersionPin(SuperVersion* super_version,
Cleanable* pin) {
assert(super_version != nullptr);
assert(pin != nullptr);
super_version->Ref();
SuperVersionHandle* sv_handle = new SuperVersionHandle(
this, &mutex_, super_version,
immutable_db_options_.avoid_unnecessary_blocking_io);
pin->RegisterCleanup(CleanupSuperVersionHandle, sv_handle, nullptr);
}
struct GetMergeOperandsState {
GetMergeOperandsState(MergeContext _merge_context,
PinnedIteratorsManager _pinned_iters_mgr)
@@ -2702,6 +2766,163 @@ Status DBImpl::GetEntity(const ReadOptions& _read_options,
return GetImpl(read_options, key, get_impl_options);
}
Status DBImpl::GetEntityLazyImpl(const ReadOptions& read_options,
ColumnFamilyHandle* column_family,
const Slice& key, LazyWideColumns* result) {
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(column_family);
auto cfd = cfh->cfd();
// Point-lookup output: the entity buffer owned by the result. GetImpl fills
// it with the (unresolved) entity and hands back the resolution context.
PinnableWideColumns* entity_buffer =
LazyWideColumnsHelper::EntityBuffer(result);
const SameFileBlobReader* same_file_reader = nullptr;
const Version* version = nullptr;
Cleanable pin;
GetImplOptions get_impl_options;
get_impl_options.column_family = column_family;
get_impl_options.columns = entity_buffer;
get_impl_options.lazy_columns_pin = &pin;
get_impl_options.lazy_columns_version = &version;
get_impl_options.lazy_columns_same_file_reader = &same_file_reader;
Status s = GetImpl(read_options, key, get_impl_options);
if (!s.ok()) {
result->Reset();
return s;
}
s = LazyWideColumnsHelper::Finalize(
result, key, version, read_options, cfd->blob_file_cache(),
/*allow_write_path_fallback=*/cfd->blob_partition_manager() != nullptr,
same_file_reader, std::move(pin));
if (!s.ok()) {
result->Reset();
}
return s;
}
Status DBImpl::GetEntityLazy(const ReadOptions& _read_options,
ColumnFamilyHandle* column_family,
const Slice& key, LazyWideColumns* result) {
// Start from an empty result so a reused LazyWideColumns is left empty (not
// stale) on every early-return / error path below.
if (result != nullptr) {
result->Reset();
}
if (!column_family) {
return Status::InvalidArgument(
"Cannot call GetEntityLazy without a column family handle");
}
if (!result) {
return Status::InvalidArgument(
"Cannot call GetEntityLazy without a LazyWideColumns object");
}
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
_read_options.io_activity != Env::IOActivity::kGetEntity) {
return Status::InvalidArgument(
"Can only call GetEntityLazy with `ReadOptions::io_activity` set to "
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kGetEntity`");
}
// Lazy resolution keeps blob references (including same-file/embedded ones)
// resolvable after the call returns; that relies on immortal, pinned table
// readers, which the DB only guarantees when max_open_files == -1.
if (mutable_db_options_.max_open_files != -1) {
return Status::InvalidArgument(
"GetEntityLazy requires the DB to be opened with max_open_files == -1");
}
ReadOptions read_options(_read_options);
if (read_options.io_activity == Env::IOActivity::kUnknown) {
read_options.io_activity = Env::IOActivity::kGetEntity;
}
return GetEntityLazyImpl(read_options, column_family, key, result);
}
void DBImpl::MultiGetEntityLazy(const ReadOptions& _read_options,
ColumnFamilyHandle* column_family,
size_t num_keys, const Slice* keys,
LazyWideColumnsBatch* result, Status* statuses,
bool /* sorted_input */) {
// Start from an empty batch so a reused LazyWideColumnsBatch is left empty
// (not stale) on every early-return / error path below.
if (result != nullptr) {
result->Reset();
}
if (num_keys == 0) {
return;
}
Status arg_status;
if (!column_family) {
arg_status = Status::InvalidArgument(
"Cannot call MultiGetEntityLazy without a column family handle");
} else if (!result) {
arg_status = Status::InvalidArgument(
"Cannot call MultiGetEntityLazy without a LazyWideColumnsBatch object");
} else if (_read_options.io_activity != Env::IOActivity::kUnknown &&
_read_options.io_activity != Env::IOActivity::kMultiGetEntity) {
arg_status = Status::InvalidArgument(
"Can only call MultiGetEntityLazy with `ReadOptions::io_activity` set "
"to `Env::IOActivity::kUnknown` or `Env::IOActivity::kMultiGetEntity`");
} else if (mutable_db_options_.max_open_files != -1) {
arg_status = Status::InvalidArgument(
"MultiGetEntityLazy requires the DB to be opened with max_open_files "
"== "
"-1");
}
if (!arg_status.ok()) {
for (size_t i = 0; i < num_keys; ++i) {
statuses[i] = arg_status;
}
return;
}
LazyWideColumnsHelper::InitBatch(result, num_keys);
ReadOptions read_options(_read_options);
if (read_options.io_activity == Env::IOActivity::kUnknown) {
read_options.io_activity = Env::IOActivity::kMultiGetEntity;
}
// The current phase implements MultiGetEntityLazy as a loop of single-key
// lazy lookups. Each iteration acquires (and hands back to the result) its
// own SuperVersion reference, and each single-key lookup independently
// derives its read sequence number. To still present one consistent
// point-in-time view across all keys (as MultiGet guarantees), pin an
// explicit snapshot for the duration of the loop when the caller did not
// supply one: it fixes the read sequence number for every key and prevents
// compaction from dropping a version some later key still needs to observe.
//
// TODO(lazy-blob-resolution-phase3): replace this loop with a genuinely
// batched read that acquires a single SuperVersion and one consistent
// (implicit) sequence number for the whole batch -- the mechanism batched
// MultiGet already uses -- and holds one shared pin per column family instead
// of one per key. That removes the need for an explicit snapshot here (which
// takes the DB mutex and adds a snapshot list entry) and enables
// coalescing/parallelizing the per-key work.
const Snapshot* snapshot = read_options.snapshot;
const bool own_snapshot = snapshot == nullptr;
if (own_snapshot) {
snapshot = GetSnapshot();
read_options.snapshot = snapshot;
}
for (size_t i = 0; i < num_keys; ++i) {
statuses[i] =
GetEntityLazyImpl(read_options, column_family, keys[i], &(*result)[i]);
}
// Link the populated entities back to the batch so batch reads can validate
// column ownership.
LazyWideColumnsHelper::FinalizeBatch(result);
if (own_snapshot) {
ReleaseSnapshot(snapshot);
}
}
Status DBImpl::GetEntity(const ReadOptions& _read_options, const Slice& key,
PinnableAttributeGroups* result) {
if (!result) {
@@ -2822,9 +3043,10 @@ static Status DecodeDirectWriteBlobIndex(const Slice& blob_index_slice,
return status;
}
Status DBImpl::ResolveDirectWritePlainValue(
const ReadOptions& read_options, const Slice& key, const Version* current,
ColumnFamilyData* cfd, PinnableSlice* value, PinnableWideColumns* columns) {
Status DBImpl::ResolveDirectWritePlainValue(const Slice& key,
const BlobFetcher& blob_fetcher,
PinnableSlice* value,
PinnableWideColumns* columns) {
Slice blob_index_slice;
std::string blob_index_storage;
if (value != nullptr) {
@@ -2862,9 +3084,9 @@ Status DBImpl::ResolveDirectWritePlainValue(
BlobIndex blob_idx;
Status status = DecodeDirectWriteBlobIndex(blob_index_slice, &blob_idx);
if (status.ok()) {
status = BlobFilePartitionManager::ResolveBlobDirectWriteIndex(
read_options, key, blob_idx, current, cfd->blob_file_cache(),
nullptr /* prefetch_buffer */, target, nullptr /* bytes_read */);
status =
blob_fetcher.FetchBlob(key, blob_idx, nullptr /* prefetch_buffer */,
target, nullptr /* bytes_read */);
if (status.ok() && columns != nullptr) {
columns->SetPlainValue(std::move(*target));
}
@@ -2872,10 +3094,8 @@ Status DBImpl::ResolveDirectWritePlainValue(
return status;
}
Status DBImpl::ResolveDirectWriteWideColumns(const ReadOptions& read_options,
const Slice& key,
const Version* current,
ColumnFamilyData* cfd,
Status DBImpl::ResolveDirectWriteWideColumns(const Slice& key,
const BlobFetcher& blob_fetcher,
PinnableWideColumns* columns) {
assert(columns != nullptr);
@@ -2919,9 +3139,8 @@ Status DBImpl::ResolveDirectWriteWideColumns(const ReadOptions& read_options,
} else {
extra_buffers.emplace_front();
PinnableSlice& blob_value = extra_buffers.front();
s = BlobFilePartitionManager::ResolveBlobDirectWriteIndex(
read_options, key, blob_idx, current, cfd->blob_file_cache(),
nullptr /* prefetch_buffer */, &blob_value, nullptr /* bytes_read */);
s = blob_fetcher.FetchBlob(key, blob_idx, nullptr /* prefetch_buffer */,
&blob_value, nullptr /* bytes_read */);
if (!s.ok()) {
return s;
}
@@ -3117,9 +3336,9 @@ Status DBImpl::PostprocessMemtableValueRead(
bool DBImpl::MaybeResolveDirectWriteValue(
const ReadOptions& read_options, const Slice& key,
bool resolve_direct_write_value, const Version* current,
ColumnFamilyData* cfd, PinnableSlice* value, PinnableWideColumns* columns,
Status* s, bool* is_blob_index, bool* value_found) {
bool resolve_direct_write_value, const BlobFetcher& blob_fetcher,
PinnableSlice* value, PinnableWideColumns* columns, Status* s,
bool* is_blob_index, bool* value_found) {
if (!s->ok() || !resolve_direct_write_value || (!value && !columns)) {
return false;
}
@@ -3164,15 +3383,14 @@ bool DBImpl::MaybeResolveDirectWriteValue(
}
if (needs_plain_value_resolution) {
*s = ResolveDirectWritePlainValue(read_options, key, current, cfd, value,
columns);
*s = ResolveDirectWritePlainValue(key, blob_fetcher, value, columns);
assert(is_blob_index != nullptr);
*is_blob_index = false;
return true;
}
assert(columns != nullptr);
*s = ResolveDirectWriteWideColumns(read_options, key, current, cfd, columns);
*s = ResolveDirectWriteWideColumns(key, blob_fetcher, columns);
if (is_blob_index != nullptr) {
*is_blob_index = false;
@@ -3184,15 +3402,16 @@ bool DBImpl::MaybeResolveDirectWriteValue(
void DBImpl::PostprocessDirectWriteValueRead(
const ReadOptions& read_options, const Slice& key,
const std::string* timestamp, bool resolve_direct_write_value,
const Version* current, ColumnFamilyData* cfd, PinnableSlice* value,
const BlobFetcher* blob_fetcher, PinnableSlice* value,
PinnableWideColumns* columns, Status* s, bool* is_blob_index,
bool* value_found) {
if (resolve_direct_write_value) {
assert(blob_fetcher != nullptr);
std::string blob_lookup_key_storage;
const bool value_resolved = MaybeResolveDirectWriteValue(
read_options,
GetBlobLookupUserKey(key, timestamp, &blob_lookup_key_storage),
resolve_direct_write_value, current, cfd, value, columns, s,
resolve_direct_write_value, *blob_fetcher, value, columns, s,
is_blob_index, value_found);
if (!value_resolved && value != nullptr) {
value->PinSelf();
@@ -4011,6 +4230,9 @@ Status DBImpl::DropColumnFamilyImpl(ColumnFamilyHandle* column_family) {
if (s.ok() && cfd->blob_partition_manager() != nullptr) {
UnregisterBlobDirectWriteColumnFamily();
}
if (s.ok()) {
RestoreParkedCompaction(cfd);
}
}
if (s.ok()) {
auto& moptions = cfd->GetLatestMutableCFOptions();
@@ -5207,9 +5429,9 @@ void DBImpl::ReleaseOptionsFileNumber(
}
}
Status DBImpl::GetUpdatesSince(
SequenceNumber seq, std::unique_ptr<TransactionLogIterator>* iter,
const TransactionLogIterator::ReadOptions& read_options) {
Status DBImpl::GetUpdatesSince(SequenceNumber seq,
std::unique_ptr<WalIterator>* iter,
const WalIterator::ReadOptions& read_options) {
RecordTick(stats_, GET_UPDATES_SINCE_CALLS);
if (seq_per_batch_) {
return Status::NotSupported(
@@ -5830,7 +6052,7 @@ void DeleteOptionsFilesHelper(const std::map<uint64_t, std::string>& filenames,
}
} // namespace
Status DBImpl::DeleteObsoleteOptionsFiles() {
Status DBImpl::DeleteObsoleteOptionsFiles(bool schedule_only) {
std::vector<std::string> filenames;
// use ordered map to store keep the filenames sorted from the newest
// to the oldest.
@@ -5855,8 +6077,23 @@ Status DBImpl::DeleteObsoleteOptionsFiles() {
// Keeps the latest 2 Options file
const size_t kNumOptionsFilesKept = 2;
DeleteOptionsFilesHelper(options_filenames, kNumOptionsFilesKept,
immutable_db_options_.info_log, GetEnv());
if (options_filenames.size() > kNumOptionsFilesKept) {
if (schedule_only) {
InstrumentedMutexLock l(&mutex_);
for (auto iter =
std::next(options_filenames.begin(), kNumOptionsFilesKept);
iter != options_filenames.end(); ++iter) {
const uint64_t file_number =
std::numeric_limits<uint64_t>::max() - iter->first;
SchedulePendingPurge(iter->second, GetName(), kOptionsFile, file_number,
/*job_id=*/0);
}
SchedulePurge();
} else {
DeleteOptionsFilesHelper(options_filenames, kNumOptionsFilesKept,
immutable_db_options_.info_log, GetEnv());
}
}
return Status::OK();
}
@@ -5896,18 +6133,42 @@ Status DBImpl::RenameTempFileToOptionsFile(const std::string& file_name,
}
if (s.ok()) {
int my_disable_delete_obsolete_files;
enum class ObsoleteOptionsFileCleanup {
kSkip,
kDeleteNow,
kSchedule,
};
ObsoleteOptionsFileCleanup obsolete_options_file_cleanup =
ObsoleteOptionsFileCleanup::kSkip;
{
InstrumentedMutexLock l(&mutex_);
versions_->options_file_number_ = options_file_number;
versions_->options_file_size_ = options_file_size;
my_disable_delete_obsolete_files = disable_delete_obsolete_files_;
if (!disable_delete_obsolete_files_ && !is_remote_compaction_enabled) {
if (immutable_db_options_.avoid_unnecessary_blocking_io &&
!reject_new_background_jobs_) {
// DB::Open() sets `opened_successfully_` after WriteOptionsFile()
// returns, then schedules the deferred OPTIONS-file purge.
obsolete_options_file_cleanup =
opened_successfully_ ? ObsoleteOptionsFileCleanup::kSchedule
: ObsoleteOptionsFileCleanup::kSkip;
} else {
obsolete_options_file_cleanup =
ObsoleteOptionsFileCleanup::kDeleteNow;
}
}
}
if (!my_disable_delete_obsolete_files && !is_remote_compaction_enabled) {
// TODO: Should we check for errors here?
DeleteObsoleteOptionsFiles().PermitUncheckedError();
if (obsolete_options_file_cleanup != ObsoleteOptionsFileCleanup::kSkip) {
Status obsolete_options_status =
DeleteObsoleteOptionsFiles(obsolete_options_file_cleanup ==
ObsoleteOptionsFileCleanup::kSchedule);
if (!obsolete_options_status.ok()) {
ROCKS_LOG_WARN(immutable_db_options_.info_log,
"Unable to delete obsolete OPTIONS files: %s",
obsolete_options_status.ToString().c_str());
}
}
}
@@ -6307,12 +6568,23 @@ Status DBImpl::PrepareFileIngestion(
}
}
std::vector<ExternalSstFileIngestionJob> ingestion_jobs;
ingestion_jobs.reserve(num_cfs);
for (const auto& arg : args) {
auto* cfd = static_cast<ColumnFamilyHandleImpl*>(arg.column_family)->cfd();
ingestion_jobs.emplace_back(versions_.get(), cfd, immutable_db_options_,
mutable_db_options_, file_options_, &snapshots_,
arg.options, &directories_, &event_logger_,
io_tracer_);
}
// TODO (yanqin) maybe handle the case in which column_families have
// duplicates
std::unique_ptr<std::list<uint64_t>::iterator> pending_output_elem;
size_t total = 0;
for (const auto& arg : args) {
total += arg.external_files.size();
for (size_t i = 0; i != num_cfs; ++i) {
total += ingestion_jobs[i].NumFilesToPrepare(args[i].external_files.size(),
args[i].atomic_replace_range);
}
uint64_t next_file_number = 0;
Status status = ReserveFileNumbersBeforeIngestion(
@@ -6324,20 +6596,11 @@ Status DBImpl::PrepareFileIngestion(
return status;
}
std::vector<ExternalSstFileIngestionJob> ingestion_jobs;
ingestion_jobs.reserve(num_cfs);
for (const auto& arg : args) {
auto* cfd = static_cast<ColumnFamilyHandleImpl*>(arg.column_family)->cfd();
ingestion_jobs.emplace_back(versions_.get(), cfd, immutable_db_options_,
mutable_db_options_, file_options_, &snapshots_,
arg.options, &directories_, &event_logger_,
io_tracer_);
}
// TODO(yanqin) maybe make jobs run in parallel
uint64_t start_file_number = next_file_number;
for (size_t i = 1; i != num_cfs; ++i) {
start_file_number += args[i - 1].external_files.size();
start_file_number += ingestion_jobs[i - 1].NumFilesToPrepare(
args[i - 1].external_files.size(), args[i - 1].atomic_replace_range);
SuperVersion* super_version =
ingestion_jobs[i].GetColumnFamilyData()->GetReferencedSuperVersion(
this);
@@ -7457,6 +7720,7 @@ void DBImpl::RecordSeqnoToTimeMapping() {
new_seqno_to_time_mapping->CopyFrom(seqno_to_time_mapping_);
// Update in SV of all applicable CFs
bool enqueued_any = false;
for (ColumnFamilyData* cfd : *versions_->GetColumnFamilySet()) {
if (cfd->IsDropped()) {
continue;
@@ -7466,8 +7730,26 @@ void DBImpl::RecordSeqnoToTimeMapping() {
sv_context.NewSuperVersion();
cfd->InstallSuperVersion(&sv_context, &mutex_,
new_seqno_to_time_mapping);
// Recording a new sample can move the preserve-window boundary, aging a
// bottommost file out of it. When that happens, recompute bottommost
// marking and enqueue now: on a quiet DB nothing else would create a
// new Version to pick up the change, so the compaction would otherwise
// be delayed until unrelated activity.
if (MaybeUpdatePreserveTimeMinSeqno(cfd) && !cfd->AllowIngestBehind()) {
VersionStorageInfo* vstorage = cfd->current()->storage_info();
vstorage->ComputeBottommostFilesMarkedForCompaction(
/*allow_ingest_behind=*/false, cfd->ioptions().user_comparator,
cfd->GetFullHistoryTsLow());
if (!vstorage->BottommostFilesMarkedForCompaction().empty()) {
EnqueuePendingCompaction(cfd);
enqueued_any = true;
}
}
}
}
if (enqueued_any) {
MaybeScheduleFlushOrCompaction();
}
bg_cv_.SignalAll();
}
@@ -7475,6 +7757,32 @@ void DBImpl::RecordSeqnoToTimeMapping() {
sv_context.Clean();
}
bool DBImpl::MaybeUpdatePreserveTimeMinSeqno(ColumnFamilyData* cfd) {
mutex_.AssertHeld();
VersionStorageInfo* vstorage = cfd->current()->storage_info();
const SequenceNumber prev = vstorage->GetPreserveTimeMinSeqno();
const MutableCFOptions& mopts = cfd->GetLatestMutableCFOptions();
MinAndMaxPreserveSeconds preserve_info{mopts};
if (!preserve_info.IsEnabled()) {
// Preserve/preclude disabled: no restriction on bottommost seqno zeroing.
vstorage->SetPreserveTimeMinSeqno(kMaxSequenceNumber);
return prev != kMaxSequenceNumber;
}
int64_t current_time = 0;
if (!immutable_db_options_.clock->GetCurrentTime(&current_time).ok()) {
// Leave the previous value in place; being stale is safe (a hot key's
// largest seqno stays above any past boundary and remains unmarked).
return false;
}
SequenceNumber preserve_time_min_seqno = kMaxSequenceNumber;
seqno_to_time_mapping_.GetCurrentTieringCutoffSeqnos(
static_cast<uint64_t>(current_time), mopts.preserve_internal_time_seconds,
mopts.preclude_last_level_data_seconds, &preserve_time_min_seqno,
/*preclude_last_level_min_seqno=*/nullptr);
vstorage->SetPreserveTimeMinSeqno(preserve_time_min_seqno);
return prev != preserve_time_min_seqno;
}
void DBImpl::TriggerPeriodicCompaction() {
TEST_SYNC_POINT("DBImpl::TriggerPeriodicCompaction:StartRunning");
{
@@ -7550,9 +7858,24 @@ const IOStatsContext* CurrentIOStatsContextForAsyncCallback(
}
#if USE_COROUTINES
void InstallCoroutineStatsConfigToTLS(
const CoroutineStatsConfig& stats_config) {
#ifndef NPERF_CONTEXT
if (stats_config.per_level_perf_context_enabled) {
get_perf_context()->EnablePerLevelPerfContext();
} else {
get_perf_context()->per_level_perf_context_enabled = false;
}
#endif
#ifndef NIOSTATS_CONTEXT
get_iostats_context()->disable_iostats = stats_config.iostats_disabled;
#endif
SetPerfLevel(stats_config.perf_level);
}
std::optional<CoroutineStatsConfig> CaptureCoroutineStatsConfigForCallback(
bool stats_enabled) {
if (!stats_enabled) {
if (!stats_enabled || !IsCoroutineStatsEnabled()) {
return std::nullopt;
}
return CaptureCoroutineStatsConfig();
@@ -7562,50 +7885,49 @@ std::optional<CoroutineStatsConfig> CaptureCoroutineStatsConfigForCallback(
} // namespace
void DBImpl::GetAsync(const ReadOptions& options,
ColumnFamilyHandle* column_family, const Slice& key,
PinnableSlice* value, std::string* timestamp,
Status& status, AsyncCallback& callback) {
void DB::GetAsync(const ReadOptions& options, ColumnFamilyHandle* column_family,
const Slice& key, PinnableSlice* value,
std::string* timestamp, Status& status,
AsyncCallback& callback) {
const bool stats_enabled = callback.EnableStats();
#if USE_COROUTINES
auto* read_executor =
immutable_db_options_.env->GetFileSystem()->GetReadExecutor();
if (read_executor != nullptr) {
auto* read_event_base = read_executor->getEventBase();
assert(read_event_base != nullptr);
auto task =
[](std::optional<CoroutineStatsConfig> task_stats_config, DBImpl* db,
ReadOptions task_options, ColumnFamilyHandle* task_column_family,
Slice task_key, PinnableSlice* task_value,
std::string* task_timestamp, Status& task_status,
AsyncCallback& task_callback) mutable -> folly::coro::Task<void> {
std::optional<CoroutineStatsContextScope> coroutine_stats_scope;
if (task_stats_config.has_value()) {
coroutine_stats_scope.emplace(std::move(*task_stats_config),
db->immutable_db_options_.env);
}
task_status = co_await folly::coro::co_nothrow(
db->GetCoroutine(task_options, task_column_family, task_key,
task_value, task_timestamp));
if (coroutine_stats_scope.has_value()) {
coroutine_stats_scope->Finalize();
}
task_callback.OnComplete(
CurrentPerfContextForAsyncCallback(coroutine_stats_scope.has_value()),
CurrentIOStatsContextForAsyncCallback(
coroutine_stats_scope.has_value()));
}(CaptureCoroutineStatsConfigForCallback(stats_enabled), this, options,
column_family, key, value,
timestamp, status, callback);
folly::coro::co_withExecutor(
folly::Executor::getKeepAliveToken(read_event_base), std::move(task))
.start();
return;
CoroDB* coro_db = GetCoroDB();
if (coro_db != nullptr) {
auto* read_executor = GetFileSystem()->GetReadExecutor();
if (read_executor != nullptr) {
auto* read_event_base = read_executor->getEventBase();
assert(read_event_base != nullptr);
auto stats_config = CaptureCoroutineStatsConfigForCallback(stats_enabled);
auto task =
[](std::optional<CoroutineStatsConfig> task_stats_config,
CoroDB* task_db, ReadOptions task_options,
ColumnFamilyHandle* task_column_family, Slice task_key,
PinnableSlice* task_value, std::string* task_timestamp,
Status& task_status,
AsyncCallback& task_callback) mutable -> folly::coro::Task<void> {
if (task_stats_config.has_value()) {
InstallCoroutineStatsConfigToTLS(*task_stats_config);
}
task_status = co_await folly::coro::co_nothrow(
task_db->GetCoroutine(task_options, task_column_family, task_key,
task_value, task_timestamp));
const bool task_stats_enabled = task_stats_config.has_value();
task_callback.OnComplete(
CurrentPerfContextForAsyncCallback(task_stats_enabled),
CurrentIOStatsContextForAsyncCallback(task_stats_enabled));
}(std::move(stats_config), coro_db, options, column_family, key, value,
timestamp, status, callback);
folly::coro::co_withExecutor(
folly::Executor::getKeepAliveToken(read_event_base), std::move(task))
.start();
return;
}
}
#endif // USE_COROUTINES
if (stats_enabled) {
// Mirror behavior of coroutine version where each coroutine gets a clean
// context
// Match coroutine reads: callback stats describe only this request.
ResetThreadLocalStatsForAsyncCallback();
}
status = Get(options, column_family, key, value, timestamp);
@@ -7613,52 +7935,52 @@ void DBImpl::GetAsync(const ReadOptions& options,
CurrentIOStatsContextForAsyncCallback(stats_enabled));
}
void DBImpl::MultiGetAsync(const ReadOptions& options, const size_t num_keys,
ColumnFamilyHandle** column_families,
const Slice* keys, PinnableSlice* values,
std::string* timestamps, Status* statuses,
const bool sorted_input, AsyncCallback& callback) {
void DB::MultiGetAsync(const ReadOptions& options, const size_t num_keys,
ColumnFamilyHandle** column_families, const Slice* keys,
PinnableSlice* values, std::string* timestamps,
Status* statuses, const bool sorted_input,
AsyncCallback& callback) {
const bool stats_enabled = callback.EnableStats();
#if USE_COROUTINES
auto* read_executor =
immutable_db_options_.env->GetFileSystem()->GetReadExecutor();
if (read_executor != nullptr) {
auto* read_event_base = read_executor->getEventBase();
assert(read_event_base != nullptr);
auto task = [](std::optional<CoroutineStatsConfig> task_stats_config,
DBImpl* db, ReadOptions task_options, size_t task_num_keys,
ColumnFamilyHandle** task_column_families,
const Slice* task_keys, PinnableSlice* task_values,
std::string* task_timestamps, Status* task_statuses,
bool task_sorted_input, AsyncCallback& task_callback) mutable
-> folly::coro::Task<void> {
std::optional<CoroutineStatsContextScope> coroutine_stats_scope;
if (task_stats_config.has_value()) {
coroutine_stats_scope.emplace(std::move(*task_stats_config),
db->immutable_db_options_.env);
}
co_await folly::coro::co_nothrow(db->MultiGetCoroutine(
task_options, task_num_keys, task_column_families, task_keys,
task_values, task_timestamps, task_statuses, task_sorted_input));
if (coroutine_stats_scope.has_value()) {
coroutine_stats_scope->Finalize();
}
task_callback.OnComplete(
CurrentPerfContextForAsyncCallback(coroutine_stats_scope.has_value()),
CurrentIOStatsContextForAsyncCallback(
coroutine_stats_scope.has_value()));
}(CaptureCoroutineStatsConfigForCallback(stats_enabled), this, options,
num_keys, column_families, keys, values, timestamps, statuses,
sorted_input, callback);
folly::coro::co_withExecutor(
folly::Executor::getKeepAliveToken(read_event_base), std::move(task))
.start();
return;
CoroDB* coro_db = GetCoroDB();
if (coro_db != nullptr) {
auto* read_executor = GetFileSystem()->GetReadExecutor();
if (read_executor != nullptr) {
auto* read_event_base = read_executor->getEventBase();
assert(read_event_base != nullptr);
auto stats_config = CaptureCoroutineStatsConfigForCallback(stats_enabled);
auto task =
[](std::optional<CoroutineStatsConfig> task_stats_config,
CoroDB* task_db, ReadOptions task_options, size_t task_num_keys,
ColumnFamilyHandle** task_column_families, const Slice* task_keys,
PinnableSlice* task_values, std::string* task_timestamps,
Status* task_statuses, bool task_sorted_input,
AsyncCallback& task_callback) mutable -> folly::coro::Task<void> {
if (task_stats_config.has_value()) {
InstallCoroutineStatsConfigToTLS(*task_stats_config);
}
co_await folly::coro::co_nothrow(task_db->MultiGetCoroutine(
task_options, task_num_keys, task_column_families, task_keys,
task_values, task_timestamps, task_statuses, task_sorted_input));
const bool task_stats_enabled = task_stats_config.has_value();
task_callback.OnComplete(
CurrentPerfContextForAsyncCallback(task_stats_enabled),
CurrentIOStatsContextForAsyncCallback(task_stats_enabled));
}(std::move(stats_config), coro_db, options, num_keys, column_families,
keys, values, timestamps,
statuses, sorted_input,
callback);
folly::coro::co_withExecutor(
folly::Executor::getKeepAliveToken(read_event_base), std::move(task))
.start();
return;
}
}
#endif // USE_COROUTINES
if (stats_enabled) {
// Mirror behavior of coroutine version where each coroutine gets a clean
// context
// Match coroutine reads: callback stats describe only this request.
ResetThreadLocalStatsForAsyncCallback();
}
MultiGet(options, num_keys, column_families, keys, values, timestamps,
+213 -55
View File
@@ -21,6 +21,7 @@
#include <string>
#include <unordered_map>
#include <utility>
#include <variant>
#include <vector>
#include "db/column_family.h"
@@ -53,6 +54,7 @@
#include "memtable/wbwi_memtable.h"
#include "monitoring/instrumented_mutex.h"
#include "options/db_options.h"
#include "options/options_helper.h"
#include "port/port.h"
#include "rocksdb/attribute_groups.h"
#include "rocksdb/db.h"
@@ -60,10 +62,10 @@
#include "rocksdb/memtablerep.h"
#include "rocksdb/status.h"
#include "rocksdb/trace_reader_writer.h"
#include "rocksdb/transaction_log.h"
#include "rocksdb/user_write_callback.h"
#include "rocksdb/utilities/replayer.h"
#include "rocksdb/utilities/write_batch_with_index.h"
#include "rocksdb/wal_iterator.h"
#include "rocksdb/write_buffer_manager.h"
#include "table/merging_iterator.h"
#include "util/autovector.h"
@@ -73,11 +75,17 @@
#include "util/stop_watch.h"
#include "util/thread_local.h"
#if USE_COROUTINES
#include "rocksdb/coro_db.h"
#endif
namespace ROCKSDB_NAMESPACE {
class Arena;
class ArenaWrappedDBIter;
class Cleanable;
class FileIngestionHandleImpl;
class SameFileBlobReader;
class InMemoryStatsHistoryIterator;
class MemTable;
class PersistentStatsHistoryIterator;
@@ -196,14 +204,22 @@ struct DBOpenLogRecordReadReporter : public log::Reader::Reporter {
//
// Since it's a very large class, the definition of the functions is
// divided in several db_impl_*.cc files, besides db_impl.cc.
class DBImpl : public DB {
#if USE_COROUTINES
class DBImpl : public DB,
public CoroDB
#else
class DBImpl : public DB
#endif
{
public:
DBImpl(const DBOptions& options, const std::string& dbname,
const bool seq_per_batch = false, const bool batch_per_txn = true,
bool read_only = false);
// No copying allowed
// No copying or moving allowed
DBImpl(const DBImpl&) = delete;
void operator=(const DBImpl&) = delete;
DBImpl(DBImpl&&) = delete;
void operator=(DBImpl&&) = delete;
virtual ~DBImpl();
@@ -269,14 +285,10 @@ class DBImpl : public DB {
bool HasAnyBlobDirectWriteColumnFamily();
using DB::Get;
using DB::GetAsync;
DECLARE_SYNC_ASYNC_AND_CALLBACK(
Status, Get, GetAsync,
(const ReadOptions& options, ColumnFamilyHandle* column_family,
const Slice& key, PinnableSlice* value, std::string* timestamp,
Status& status, AsyncCallback& callback),
const ReadOptions& _read_options, ColumnFamilyHandle* column_family,
const Slice& key, PinnableSlice* value, std::string* timestamp);
DECLARE_SYNC_AND_ASYNC_OVERRIDE(Status, Get, const ReadOptions& _read_options,
ColumnFamilyHandle* column_family,
const Slice& key, PinnableSlice* value,
std::string* timestamp);
using DB::GetEntity;
Status GetEntity(const ReadOptions& options,
@@ -285,6 +297,10 @@ class DBImpl : public DB {
Status GetEntity(const ReadOptions& options, const Slice& key,
PinnableAttributeGroups* result) override;
Status GetEntityLazy(const ReadOptions& options,
ColumnFamilyHandle* column_family, const Slice& key,
LazyWideColumns* result) override;
using DB::GetMergeOperands;
Status GetMergeOperands(const ReadOptions& options,
ColumnFamilyHandle* column_family, const Slice& key,
@@ -301,7 +317,6 @@ class DBImpl : public DB {
}
using DB::MultiGet;
using DB::MultiGetAsync;
// This MultiGet is a batched version, which may be faster than calling Get
// multiple times, especially if the keys have some spatial locality that
// enables them to be queried in the same SST files/set of files. The larger
@@ -309,16 +324,13 @@ class DBImpl : public DB {
// The values and statuses parameters are arrays with number of elements
// equal to keys.size(). This allows the storage for those to be alloacted
// by the caller on the stack for small batches
DECLARE_SYNC_ASYNC_AND_CALLBACK(
void, MultiGet, MultiGetAsync,
(const ReadOptions& options, const size_t num_keys,
ColumnFamilyHandle** column_families, const Slice* keys,
PinnableSlice* values, std::string* timestamps, Status* statuses,
const bool sorted_input, AsyncCallback& callback),
const ReadOptions& _read_options, const size_t num_keys,
ColumnFamilyHandle** column_families, const Slice* keys,
PinnableSlice* values, std::string* timestamps, Status* statuses,
const bool sorted_input = false);
DECLARE_SYNC_AND_ASYNC_OVERRIDE(void, MultiGet,
const ReadOptions& _read_options,
const size_t num_keys,
ColumnFamilyHandle** column_families,
const Slice* keys, PinnableSlice* values,
std::string* timestamps, Status* statuses,
const bool sorted_input = false);
void MultiGetWithCallback(
const ReadOptions& _read_options, ColumnFamilyHandle* column_family,
@@ -340,6 +352,11 @@ class DBImpl : public DB {
const Slice* keys,
PinnableAttributeGroups* results) override;
void MultiGetEntityLazy(const ReadOptions& options,
ColumnFamilyHandle* column_family, size_t num_keys,
const Slice* keys, LazyWideColumnsBatch* result,
Status* statuses, bool sorted_input) override;
void MultiGetEntityWithCallback(
const ReadOptions& read_options, ColumnFamilyHandle* column_family,
ReadCallback* callback,
@@ -477,6 +494,8 @@ class DBImpl : public DB {
void DisableManualCompaction() override;
void AbortAllCompactions() override;
void ResumeAllCompactions() override;
void AbortCompactions(ColumnFamilyHandle* column_family) override;
void ResumeCompactions(ColumnFamilyHandle* column_family) override;
using DB::SetOptions;
Status SetOptions(
@@ -573,10 +592,10 @@ class DBImpl : public DB {
Status GetCurrentWalFile(std::unique_ptr<WalFile>* current_wal_file) override;
Status GetCreationTimeOfOldestFile(uint64_t* creation_time) override;
Status GetUpdatesSince(
SequenceNumber seq_number, std::unique_ptr<TransactionLogIterator>* iter,
const TransactionLogIterator::ReadOptions& read_options =
TransactionLogIterator::ReadOptions()) override;
Status GetUpdatesSince(SequenceNumber seq_number,
std::unique_ptr<WalIterator>* iter,
const WalIterator::ReadOptions& read_options =
WalIterator::ReadOptions()) override;
Status DeleteFilesInRanges(ColumnFamilyHandle* column_family,
const RangeOpt* ranges, size_t n,
bool include_end = true);
@@ -589,6 +608,35 @@ class DBImpl : public DB {
const LiveFilesStorageInfoOptions& opts,
std::vector<LiveFileStorageInfo>* files) override;
// Variant of GetLiveFilesStorageInfo used by subset-CF Checkpoint. Only
// table/blob files of the given column families are captured; the rest are
// reported in excluded_cf_ids so the caller can reconcile the copied
// MANIFEST (typically via AppendColumnFamilyDropsToManifest below). Caller
// must supply a non-empty include_cf_ids containing the default CF id and a
// non-null excluded_cf_ids; both are internal-contract preconditions.
Status GetLiveFilesStorageInfoForSubsetCheckpoint(
const LiveFilesStorageInfoOptions& opts,
const std::vector<uint32_t>& include_cf_ids,
std::vector<LiveFileStorageInfo>* files,
std::vector<uint32_t>* excluded_cf_ids);
// Shared body of GetLiveFilesStorageInfo and its subset-checkpoint variant.
// include_cf_ids empty -> no filter; otherwise records skipped CF ids in
// *excluded_cf_ids (which must be non-null in that case).
Status GetLiveFilesStorageInfoImpl(
const LiveFilesStorageInfoOptions& opts,
const std::vector<uint32_t>& include_cf_ids,
std::vector<LiveFileStorageInfo>* files,
std::vector<uint32_t>* excluded_cf_ids);
// Appends kColumnFamilyDrop records to the MANIFEST file at manifest_path for
// the given column family ids. Used by Checkpoint to make a subset-CF
// checkpoint's copied MANIFEST consistent with the reduced file set. Operates
// only on the given file; does not mutate this DB's live state.
Status AppendColumnFamilyDropsToManifest(const std::string& manifest_path,
uint64_t manifest_size,
const std::vector<uint32_t>& cf_ids);
Status GetPreparedFileInfoForExternalSstIngestion(
const std::string& file_path,
std::shared_ptr<const PreparedFileInfo>* file_info) override;
@@ -729,6 +777,24 @@ class DBImpl : public DB {
PinnableSlice* merge_operands = nullptr;
GetMergeOperandsOptions* get_merge_operands_options = nullptr;
int* number_of_operands = nullptr;
// Lazy wide-column mode (used by GetEntityLazy / MultiGetEntityLazy). When
// `lazy_columns_pin` is non-null, the SST point-lookup path leaves a
// wide-column entity's blob references *unresolved* (see
// GetContext::SaveWideColumnEntityToColumns), so the caller can resolve
// them on demand later. On a successful lookup GetImpl then:
// - sets `*lazy_columns_version` to the Version the entity was read from
// (the resolution context for blob references), and
// - transfers an extra SuperVersion reference by registering a cleanup on
// `*lazy_columns_pin`, so the result may outlive this call (as an
// iterator's result does).
// The SameFileBlobReader for the SST that held the entity (if any) is
// stored in `*lazy_columns_same_file_reader` by the read path, so same-file
// ("embedded") references stay resolvable later. All three must be set
// together. `columns` must also be set (the entity output).
Cleanable* lazy_columns_pin = nullptr;
const Version** lazy_columns_version = nullptr;
const SameFileBlobReader** lazy_columns_same_file_reader = nullptr;
};
DECLARE_SYNC_AND_ASYNC(Status, GetImpl, const ReadOptions& read_options,
@@ -750,6 +816,17 @@ class DBImpl : public DB {
const Slice& key,
GetImplOptions& get_impl_options);
// Shared implementation of GetEntityLazy / MultiGetEntityLazy for a single
// key: performs a lazy point lookup for `key` (leaving blob references
// unresolved) into `*result`, which takes ownership of a SuperVersion pin so
// it may outlive this call. `read_options` must already be finalized (e.g.
// io_activity set). Returns OK on success, NotFound (with an empty `*result`)
// if there is no entry, or another non-OK status on error. REQUIRES:
// max_open_files == -1 has already been validated by the caller.
Status GetEntityLazyImpl(const ReadOptions& read_options,
ColumnFamilyHandle* column_family, const Slice& key,
LazyWideColumns* result);
// If `snapshot` == kMaxSequenceNumber, set a recent one inside the file.
ArenaWrappedDBIter* NewIteratorImpl(const ReadOptions& options,
ColumnFamilyHandleImpl* cfh,
@@ -901,6 +978,13 @@ class DBImpl : public DB {
void CleanupIteratorSuperVersion(SuperVersion* super_version,
bool background_purge);
// Transfer an extra reference to `super_version` into `pin` (via a cleanup
// callback), so `pin`'s owner keeps the SuperVersion -- and thus the data it
// references -- alive after the current call returns, as an iterator does.
// Shared by the lazy wide-column read path (GetEntityLazy) across the
// primary, read-only, and secondary GetImpl overrides.
void TransferSuperVersionPin(SuperVersion* super_version, Cleanable* pin);
LogsWithPrepTracker* logs_with_prep_tracker() {
return &logs_with_prep_tracker_;
}
@@ -1072,10 +1156,12 @@ class DBImpl : public DB {
}
};
using RecoveredTransactionMap =
std::unordered_map<std::string, RecoveredTransaction*>;
bool allow_2pc() const { return immutable_db_options_.allow_2pc; }
std::unordered_map<std::string, RecoveredTransaction*>
recovered_transactions() {
RecoveredTransactionMap recovered_transactions() {
return recovered_transactions_;
}
@@ -1106,16 +1192,24 @@ class DBImpl : public DB {
logs_with_prep_tracker_.MarkLogAsContainingPrepSection(log);
}
void DeleteRecoveredTransaction(const std::string& name) {
auto it = recovered_transactions_.find(name);
// Deletes the recovered transaction `it` points to and returns the iterator
// following it, like std::unordered_map::erase().
RecoveredTransactionMap::iterator DeleteRecoveredTransaction(
RecoveredTransactionMap::iterator it) {
assert(it != recovered_transactions_.end());
auto* trx = it->second;
recovered_transactions_.erase(it);
RecoveredTransactionMap::iterator next = recovered_transactions_.erase(it);
for (const auto& info : trx->batches_) {
logs_with_prep_tracker_.MarkLogAsHavingPrepSectionFlushed(
info.second.log_number_);
}
delete trx;
return next;
}
void DeleteRecoveredTransaction(const std::string& name) {
RecoveredTransactionMap::iterator it = recovered_transactions_.find(name);
DeleteRecoveredTransaction(it);
}
void DeleteAllRecoveredTransactions() {
@@ -1234,6 +1328,8 @@ class DBImpl : public DB {
// Get the background error status
Status TEST_GetBGError();
void TEST_SetBGError(const IOStatus& error, BackgroundErrorReason reason);
bool TEST_IsRecoveryInProgress();
Status TEST_ResumeImpl(DBRecoverContext context);
@@ -1357,6 +1453,20 @@ class DBImpl : public DB {
// REQUIRES: DB mutex held or during open
void EnsureSeqnoToTimeMapping(const MinAndMaxPreserveSeconds& preserve_secs);
// Computes the seqno->time preserve-window lower bound from
// seqno_to_time_mapping_ and stores it on cfd's current version, so
// bottommost file marking does not mark files whose largest seqno cannot be
// zeroed yet (which would loop). No-op for column families without
// preserve/preclude enabled. Returns true if the stored value changed, so
// callers can recompute bottommost marking when the boundary moves.
// Note: right after opening an existing DB, seqno_to_time_mapping_ may not be
// fully reconstructed, so this bound can be imprecise until the first
// periodic RecordSeqnoToTimeMapping. That is safe: CompactionJob folds this
// same bound into kBottommostFiles compactions, so any marked file still
// makes progress (never loops), and the periodic task self-corrects the
// bound. REQUIRES: DB mutex held
bool MaybeUpdatePreserveTimeMinSeqno(ColumnFamilyData* cfd);
// Only called during open
void PrepopulateSeqnoToTimeMapping(
const MinAndMaxPreserveSeconds& preserve_secs);
@@ -1418,6 +1528,10 @@ class DBImpl : public DB {
bool seq_per_batch() const { return seq_per_batch_; }
#if USE_COROUTINES
CoroDB* GetCoroDB() override { return this; }
#endif
protected:
const std::string dbname_;
const bool read_only_;
@@ -1437,8 +1551,7 @@ class DBImpl : public DB {
FileSystemPtr fs_;
MutableDBOptions mutable_db_options_;
Statistics* stats_;
std::unordered_map<std::string, RecoveredTransaction*>
recovered_transactions_;
RecoveredTransactionMap recovered_transactions_;
std::unique_ptr<Tracer> tracer_;
InstrumentedMutex trace_mutex_;
BlockCacheTracer block_cache_tracer_;
@@ -1539,6 +1652,15 @@ class DBImpl : public DB {
edit_lists_[i].emplace_back(new VersionEdit(edit));
}
bool HasVersionEdits() const {
for (const auto& edit_list : edit_lists_) {
if (!edit_list.empty()) {
return true;
}
}
return false;
}
std::unordered_map<uint32_t, uint32_t> map_; // cf_id to index;
autovector<ColumnFamilyData*> cfds_;
autovector<autovector<VersionEdit*>> edit_lists_;
@@ -1562,7 +1684,7 @@ class DBImpl : public DB {
// 2. db_mutex is NOT held
Status RenameTempFileToOptionsFile(const std::string& file_name,
bool is_remote_compaction_enabled);
Status DeleteObsoleteOptionsFiles();
Status DeleteObsoleteOptionsFiles(bool schedule_only);
void NotifyOnManualFlushScheduled(autovector<ColumnFamilyData*> cfds,
FlushReason flush_reason);
@@ -1908,7 +2030,7 @@ class DBImpl : public DB {
// LogAndApplyForRecovery should be called only once during recovery and it
// should be called when RocksDB writes to a first new MANIFEST since this
// recovery.
Status LogAndApplyForRecovery(const RecoveryContext& recovery_ctx);
Status LogAndApplyForRecovery(RecoveryContext& recovery_ctx);
// Schedule background work to open and validate SST files asynchronously.
// Called when open_files_async is enabled.
@@ -2686,7 +2808,11 @@ class DBImpl : public DB {
// equal to this per-column-family specified value, this flush request is
// considered to have completed its work of flushing this column family.
// After completing the work for all column families in this request, this
// flush is considered complete.
// flush is considered complete. EnqueuePendingFlush() acquires one
// reference for each CFD when it successfully queues this request.
// PopFirstFromFlushQueue() transfers responsibility for those references
// to its caller, which must release each one with UnrefAndTryDelete() after
// processing or discarding the request.
std::unordered_map<ColumnFamilyData*, uint64_t>
cfd_to_max_mem_id_to_persist;
@@ -2801,9 +2927,24 @@ class DBImpl : public DB {
ColumnFamilyData* PopFirstFromCompactionQueue();
FlushRequest PopFirstFromFlushQueue();
struct CompactionQueueThrottled {};
struct CompactionQueueParked {};
struct CompactionQueuePicked {
ColumnFamilyData* cfd;
};
using CompactionQueuePickResult =
std::variant<CompactionQueuePicked, CompactionQueueThrottled,
CompactionQueueParked>;
// Pick the first unthrottled compaction with task token from queue.
ColumnFamilyData* PickCompactionFromQueue(
CompactionQueuePickResult PickCompactionFromQueue(
std::unique_ptr<TaskLimiterToken>* token, LogBuffer* log_buffer);
bool IsCompactionAborted(ColumnFamilyData* cfd) const;
bool MustWaitForCompaction(ColumnFamilyData* cfd);
bool HasPendingManualCompaction(ColumnFamilyData* cfd);
bool RestoreParkedCompaction(ColumnFamilyData* cfd);
void BeginInFlightCompaction(ColumnFamilyData* cfd);
bool EndInFlightCompaction(ColumnFamilyData* cfd);
IOStatus SyncWalImpl(bool include_current_wal,
const WriteOptions& write_options,
@@ -3084,39 +3225,34 @@ class DBImpl : public DB {
// Resolves a plain value whose current payload is an encoded direct-write
// blob index, either through `value` directly or through the default-column
// view layered on `columns`.
static Status ResolveDirectWritePlainValue(const ReadOptions& read_options,
const Slice& key,
const Version* current,
ColumnFamilyData* cfd,
static Status ResolveDirectWritePlainValue(const Slice& key,
const BlobFetcher& blob_fetcher,
PinnableSlice* value,
PinnableWideColumns* columns);
// Resolves each unresolved direct-write blob-valued column in `columns` and
// rebuilds the serialized wide-column entity in place.
static Status ResolveDirectWriteWideColumns(const ReadOptions& read_options,
const Slice& key,
const Version* current,
ColumnFamilyData* cfd,
static Status ResolveDirectWriteWideColumns(const Slice& key,
const BlobFetcher& blob_fetcher,
PinnableWideColumns* columns);
// Dispatches between plain-value and wide-column direct-write resolution for
// read results observed before flush makes the corresponding blob file
// visible through normal Version metadata.
static bool MaybeResolveDirectWriteValue(
const ReadOptions& read_options, const Slice& key,
bool resolve_direct_write_value, const Version* current,
ColumnFamilyData* cfd, PinnableSlice* value, PinnableWideColumns* columns,
Status* s, bool* is_blob_index, bool* value_found = nullptr);
bool resolve_direct_write_value, const BlobFetcher& blob_fetcher,
PinnableSlice* value, PinnableWideColumns* columns, Status* s,
bool* is_blob_index, bool* value_found = nullptr);
// Completes primary memtable hits that can still carry direct-write blob
// references before the blob file is visible in Version metadata.
static void PostprocessDirectWriteValueRead(
const ReadOptions& read_options, const Slice& key,
const std::string* timestamp, bool resolve_direct_write_value,
const Version* current, ColumnFamilyData* cfd, PinnableSlice* value,
const BlobFetcher* blob_fetcher, PinnableSlice* value,
PinnableWideColumns* columns, Status* s, bool* is_blob_index,
bool* value_found = nullptr);
// Resolves memtable read results that still carry blob references through
// either a raw blob-index payload in `value` or unresolved blob columns in
// `columns`. Unlike the direct-write helper above, this path only depends on
// a BlobFetcher and therefore works for read-only/secondary DBs. Sets
// `columns`. This generic path is also used by read-only/secondary DBs. Sets
// *did_resolve to true iff there was a reference to resolve -- in which case
// this function has finalized `value`/`columns` (populated on success,
// cleared on error) -- and false iff there was nothing to resolve (the caller
@@ -3190,6 +3326,10 @@ class DBImpl : public DB {
// DB mutex in compaction code paths.
std::atomic<int> compaction_aborted_ = 0;
// Number of AbortCompactions() callers waiting for a targeted compaction to
// finish. Protected by the DB mutex.
int per_cf_compaction_abort_waiters_ = 0;
// This condition variable is signaled on these conditions:
// * whenever bg_compaction_scheduled_ goes down to 0
// * if AnyManualCompaction, whenever a compaction finishes, even if it hasn't
@@ -3399,7 +3539,7 @@ class DBImpl : public DB {
// cfd->imm()->IsFlushPending()
// A column family is inserted into compaction_queue_ when it satisfied
// condition cfd->NeedsCompaction()
// Column families in this list are all Ref()-erenced
// Column families in these collections are all Ref()-erenced.
// TODO(icanadi) Provide some kind of ReferencedColumnFamily class that will
// do RAII on ColumnFamilyData
// Column families are in this queue when they need to be flushed or
@@ -3414,9 +3554,23 @@ class DBImpl : public DB {
// invariant(column family present in flush_queue_ <==>
// ColumnFamilyData::pending_flush_ == true)
std::deque<FlushRequest> flush_queue_;
// invariant(column family present in compaction_queue_ <==>
// ColumnFamilyData::pending_compaction_ == true)
// invariant((column family in compaction_queue_ or
// column family in parked_compaction_cfds_)
// <=> queued_for_compaction() == true)
//
// "Parked" means a column family whose compaction was aborted via
// AbortCompactions() while it was queued. When an aborted CF reaches the
// front of compaction_queue_, PickCompactionFromQueue() moves it to
// parked_compaction_cfds_ instead of scheduling it. The CF does not
// immediately lose its place -- only when it reaches the front of the
// queue is it parked. This preserves fairness for CFs behind it and
// avoids restoring the scheduler credit (which would cause a hot re-pick
// loop). On the final ResumeCompactions(), RestoreParkedCompaction()
// re-enqueues the CF (or releases its ref if it no longer needs
// compaction). Each parked CF carries exactly one Ref() from the
// original AddToCompactionQueue() call.
std::deque<ColumnFamilyData*> compaction_queue_;
std::unordered_set<ColumnFamilyData*> parked_compaction_cfds_;
// A map to store file numbers and filenames of the files to be purged
std::unordered_map<uint64_t, PurgeFileInfo> purge_files_;
@@ -3447,6 +3601,10 @@ class DBImpl : public DB {
// stores the number of BOTTOM-priority compactions currently running
int num_running_bottom_compactions_ = 0;
// Number of compaction service jobs currently waiting in
// CompactionService::Wait(), counted per executed subcompaction.
std::atomic<int> num_running_remote_compactions_ = 0;
// number of background memtable flush jobs, submitted to the HIGH pool
int bg_flush_scheduled_ = 0;
@@ -3770,7 +3928,7 @@ inline Status DBImpl::FailIfTsMismatchCf(ColumnFamilyHandle* column_family,
inline Status DBImpl::FailIfTableFilterWithRangeConversion(
const ReadOptions& read_options,
const MutableCFOptions& mutable_cf_options) const {
if (read_options.table_filter &&
if (HasTableFilter(read_options) &&
mutable_cf_options.min_tombstones_for_range_conversion > 0) {
return Status::InvalidArgument(
"ReadOptions::table_filter is not supported when "
+280 -46
View File
@@ -6,6 +6,7 @@
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// 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 <cinttypes>
#include <deque>
#include <unordered_map>
@@ -1195,11 +1196,15 @@ Status DBImpl::CompactRange(const CompactRangeOptions& options,
ColumnFamilyHandle* column_family,
const Slice* begin_without_ts,
const Slice* end_without_ts) {
auto* cfd =
static_cast_with_check<ColumnFamilyHandleImpl>(column_family)->cfd();
assert(cfd);
if (manual_compaction_paused_.load(std::memory_order_acquire) > 0) {
return Status::Incomplete(Status::SubCode::kManualCompactionPaused);
}
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
if (IsCompactionAborted(cfd)) {
return Status::Incomplete(Status::SubCode::kCompactionAborted);
}
@@ -1334,7 +1339,7 @@ Status DBImpl::CompactRangeInternal(const CompactRangeOptions& options,
SuperVersion* super_version = cfd->GetReferencedSuperVersion(this);
s = cfd->RangesOverlapWithMemtables(
{range}, super_version, immutable_db_options_.allow_data_in_errors,
&flush_needed);
&flush_needed, /*range_limit_exclusive=*/false);
CleanupSuperVersion(super_version);
}
@@ -1610,6 +1615,21 @@ Status DBImpl::CompactFiles(const CompactionOptions& compact_options,
static_cast_with_check<ColumnFamilyHandleImpl>(column_family)->cfd();
assert(cfd);
{
InstrumentedMutexLock l(&mutex_);
BeginInFlightCompaction(cfd);
}
Defer end_in_flight_compaction([&]() {
InstrumentedMutexLock l(&mutex_);
// The caller holds a live handle to this CF, so this can never be the final
// unref (the return can only be true when the last reference is dropped).
[[maybe_unused]] const bool cfd_deleted = EndInFlightCompaction(cfd);
assert(!cfd_deleted);
if (per_cf_compaction_abort_waiters_ > 0) {
bg_cv_.SignalAll();
}
});
Status s;
JobContext job_context(next_job_id_.fetch_add(1), true);
LogBuffer log_buffer(InfoLogLevel::INFO_LEVEL,
@@ -1738,8 +1758,7 @@ Status DBImpl::CompactFilesImpl(
return Status::ShutdownInProgress();
}
// triggered by AbortAllCompactions
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
if (IsCompactionAborted(cfd)) {
return Status::Incomplete(Status::SubCode::kCompactionAborted);
}
@@ -1866,7 +1885,7 @@ Status DBImpl::CompactFilesImpl(
c.reset();
bg_compaction_scheduled_--;
if (bg_compaction_scheduled_ == 0) {
if (bg_compaction_scheduled_ == 0 || per_cf_compaction_abort_waiters_ > 0) {
bg_cv_.SignalAll();
}
MaybeScheduleFlushOrCompaction();
@@ -1896,7 +1915,7 @@ Status DBImpl::CompactFilesImpl(
kManualCompactionCanceledFalse_, compaction_aborted_, db_id_,
db_session_id_, c->column_family_data()->GetFullHistoryTsLow(),
c->trim_ts(), &blob_callback_, &bg_compaction_scheduled_,
&bg_bottom_compaction_scheduled_);
&bg_bottom_compaction_scheduled_, &num_running_remote_compactions_);
// Creating a compaction influences the compaction score because the score
// takes running compactions into account (by skipping files that are already
@@ -2001,7 +2020,7 @@ Status DBImpl::CompactFilesImpl(
c.reset();
bg_compaction_scheduled_--;
if (bg_compaction_scheduled_ == 0) {
if (bg_compaction_scheduled_ == 0 || per_cf_compaction_abort_waiters_ > 0) {
bg_cv_.SignalAll();
}
MaybeScheduleFlushOrCompaction();
@@ -2205,8 +2224,8 @@ Status DBImpl::ReFitLevel(ColumnFamilyData* cfd, int level, int target_level) {
"Levels between source and target are not empty for a move.");
}
if (cfd->RangeOverlapWithCompaction(refit_level_smallest.user_key(),
refit_level_largest.user_key(),
l)) {
refit_level_largest.user_key(), l,
/*range_limit_exclusive=*/false)) {
refitting_level_ = false;
return Status::NotSupported(
"Levels between source and target "
@@ -2229,8 +2248,8 @@ Status DBImpl::ReFitLevel(ColumnFamilyData* cfd, int level, int target_level) {
"Levels between source and target are not empty for a move.");
}
if (cfd->RangeOverlapWithCompaction(refit_level_smallest.user_key(),
refit_level_largest.user_key(),
l)) {
refit_level_largest.user_key(), l,
/*range_limit_exclusive=*/false)) {
refitting_level_ = false;
return Status::NotSupported(
"Levels between source and target "
@@ -2471,12 +2490,10 @@ Status DBImpl::RunManualCompaction(
return manual.status;
}
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
// All compactions are being aborted. Return immediately.
int counter = compaction_aborted_.load(std::memory_order_acquire);
ROCKS_LOG_INFO(
immutable_db_options_.info_log,
"RunManualCompaction: Aborting due to compaction_aborted_=%d", counter);
if (IsCompactionAborted(cfd)) {
ROCKS_LOG_INFO(immutable_db_options_.info_log,
"[%s] RunManualCompaction: aborting compaction",
cfd->GetName().c_str());
manual.status = Status::Incomplete(Status::SubCode::kCompactionAborted);
manual.done = true;
return manual.status;
@@ -2506,7 +2523,7 @@ Status DBImpl::RunManualCompaction(
// and `CompactRangeOptions::canceled` might not work well together.
while (bg_bottom_compaction_scheduled_ > 0 ||
bg_compaction_scheduled_ > 0) {
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
if (IsCompactionAborted(cfd)) {
// Pretend the error came from compaction so the below cleanup/error
// handling code can process it.
manual.done = true;
@@ -2542,6 +2559,11 @@ Status DBImpl::RunManualCompaction(
// true.
while (!manual.done) {
assert(HasPendingManualCompaction());
if (IsCompactionAborted(cfd) && !scheduled && !manual.in_progress) {
manual.done = true;
manual.status = Status::Incomplete(Status::SubCode::kCompactionAborted);
break;
}
manual_conflict = false;
Compaction* compaction = nullptr;
if (ShouldntRunManualCompaction(&manual) || (manual.in_progress == true) ||
@@ -2631,7 +2653,7 @@ Status DBImpl::RunManualCompaction(
if (!scheduled) {
// There is nothing scheduled to wait on, so any cancellation can end the
// manual now.
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
if (IsCompactionAborted(cfd)) {
// Stop waiting since it was canceled. Pretend the error came from
// compaction so the below cleanup/error handling code can process it.
manual.done = true;
@@ -3299,6 +3321,52 @@ void DBImpl::EnableManualCompaction() {
manual_compaction_paused_.fetch_sub(1, std::memory_order_release);
}
bool DBImpl::IsCompactionAborted(ColumnFamilyData* cfd) const {
return compaction_aborted_.load(std::memory_order_acquire) > 0 ||
cfd->compaction_aborted() > 0;
}
bool DBImpl::MustWaitForCompaction(ColumnFamilyData* cfd) {
mutex_.AssertHeld();
// num_in_flight_compactions_ and the picker's compactions_in_progress_ set
// cover DIFFERENT windows and neither subsumes the other:
// - the counter covers a picked Compaction from pick through the end of
// BackgroundCompaction (including NotifyOnCompactionCompleted), which the
// picker set does not -- it is cleared during Install's manifest write.
// - the picker set covers the bottom-priority handoff: when a LOW thread
// forwards a bottom compaction, its EndInFlightCompaction() dips the
// counter to 0 while the compaction is still registered and queued for the
// BOTTOM pool, and the picker set keeps AbortCompactions() waiting across
// that gap. (The re-registered "intended" bottom compaction inherits the
// original compaction_reason(), so it is not filtered out below.)
if (cfd->num_in_flight_compactions(&mutex_) > 0) {
return true;
}
for (const auto* compaction :
*cfd->compaction_picker()->compactions_in_progress()) {
const auto reason = compaction->compaction_reason();
// These are picker reservations for conflict tracking, not CompactionJobs,
// so they never observe the abort atomic in ProcessKeyValue().
if (reason != CompactionReason::kExternalSstIngestion &&
reason != CompactionReason::kRefitLevel) {
return true;
}
}
return false;
}
void DBImpl::BeginInFlightCompaction(ColumnFamilyData* cfd) {
mutex_.AssertHeld();
cfd->Ref();
cfd->inc_num_in_flight_compactions(&mutex_);
}
bool DBImpl::EndInFlightCompaction(ColumnFamilyData* cfd) {
mutex_.AssertHeld();
cfd->dec_num_in_flight_compactions(&mutex_);
return cfd->UnrefAndTryDelete();
}
void DBImpl::AbortAllCompactions() {
InstrumentedMutexLock l(&mutex_);
@@ -3325,6 +3393,83 @@ void DBImpl::AbortAllCompactions() {
}
}
void DBImpl::AbortCompactions(ColumnFamilyHandle* column_family) {
InstrumentedMutexLock l(&mutex_);
if (shutting_down_.load(std::memory_order_acquire)) {
return;
}
assert(column_family != nullptr);
auto* cfh = static_cast_with_check<ColumnFamilyHandleImpl>(column_family);
assert(cfh->db() == this);
auto* cfd = cfh->cfd();
assert(cfd != nullptr);
cfd->inc_compaction_aborted(&mutex_);
TEST_SYNC_POINT("DBImpl::AbortCompactions:FlagSet");
for (const auto* manual_compaction : manual_compaction_dequeue_) {
if (manual_compaction->cfd == cfd) {
manual_compaction->canceled.store(true, std::memory_order_release);
}
}
bg_cv_.SignalAll();
// Wait for this CF's running compactions to finish or abort. If the DB
// is shutting down, return early -- the counter stays incremented but the
// CFD and DB will be torn down anyway. Callers that use SCOPE_EXIT for
// the matching ResumeCompactions() are safe: the resume is a no-op on a
// DB that is already closed.
++per_cf_compaction_abort_waiters_;
while (!shutting_down_.load(std::memory_order_acquire) &&
(MustWaitForCompaction(cfd) || HasPendingManualCompaction(cfd))) {
TEST_SYNC_POINT("DBImpl::AbortCompactions:Waiting");
bg_cv_.Wait();
}
--per_cf_compaction_abort_waiters_;
TEST_SYNC_POINT("DBImpl::AbortCompactions:Complete");
}
void DBImpl::ResumeCompactions(ColumnFamilyHandle* column_family) {
InstrumentedMutexLock l(&mutex_);
if (shutting_down_.load(std::memory_order_acquire)) {
return;
}
assert(column_family != nullptr);
auto* cfh = static_cast_with_check<ColumnFamilyHandleImpl>(column_family);
assert(cfh->db() == this);
auto* cfd = cfh->cfd();
assert(cfd != nullptr);
const int before = cfd->compaction_aborted();
if (before <= 0) {
ROCKS_LOG_WARN(immutable_db_options_.info_log,
"ResumeCompactions called without prior AbortCompactions "
"for column family %s (counter=%d)",
cfd->GetName().c_str(), before);
return;
}
const int current = cfd->dec_compaction_aborted(&mutex_);
ROCKS_LOG_INFO(immutable_db_options_.info_log,
"[%s] ResumeCompactions: counter %d -> %d",
cfd->GetName().c_str(), before, current);
if (current == 0) {
// TODO: Aborting an automatic compaction after PickCompaction()
// releases its inputs without recomputing the compaction score, so this
// EnqueuePendingCompaction() can observe a stale sub-trigger score and
// queue nothing, leaving the CF idle until the next flush. Pre-existing
// (ResumeAllCompactions() has the same gap); fix separately for both APIs
// by recomputing the score on the abort path.
if (!RestoreParkedCompaction(cfd) && !cfd->IsDropped()) {
EnqueuePendingCompaction(cfd);
}
MaybeScheduleFlushOrCompaction();
bg_cv_.SignalAll();
}
}
void DBImpl::ResumeAllCompactions() {
InstrumentedMutexLock l(&mutex_);
int before = compaction_aborted_.load(std::memory_order_acquire);
@@ -3350,6 +3495,7 @@ void DBImpl::ResumeAllCompactions() {
// If this is the last resume call (abort counter back to 0), schedule
// compactions that may have been waiting
if (current == 0) {
// The stale-score TODO in ResumeCompactions() also applies here.
MaybeScheduleFlushOrCompaction();
}
}
@@ -3366,14 +3512,9 @@ void DBImpl::MaybeScheduleFlushOrCompaction() {
return;
} else if (error_handler_.IsBGWorkStopped() &&
!error_handler_.IsRecoveryInProgress()) {
// There has been a hard error and this call is not part of the recovery
// sequence. Bail out here so we don't get into an endless loop of
// scheduling BG work which will again call this function
//
// Note that a non-recovery flush can still be scheduled if
// error_handler_.IsRecoveryInProgress() returns true. We rely on
// BackgroundCallFlush() to check flush reason and drop non-recovery
// flushes.
// Recovery must be able to schedule its flushes while background work is
// stopped. New non-recovery requests are rejected by EnqueuePendingFlush,
// and BackgroundFlush drops any request queued before the error.
return;
} else if (shutting_down_.load(std::memory_order_acquire)) {
// DB is being deleted; no more background compactions
@@ -3576,6 +3717,27 @@ void DBImpl::AddToCompactionQueue(ColumnFamilyData* cfd) {
++unscheduled_compactions_;
}
bool DBImpl::RestoreParkedCompaction(ColumnFamilyData* cfd) {
mutex_.AssertHeld();
auto it = parked_compaction_cfds_.find(cfd);
if (it == parked_compaction_cfds_.end()) {
return false;
}
parked_compaction_cfds_.erase(it);
assert(cfd->queued_for_compaction());
if (!cfd->IsDropped() && cfd->NeedsCompaction()) {
TEST_SYNC_POINT_CALLBACK("DBImpl::RestoreParkedCompaction:cfd",
static_cast<void*>(cfd));
compaction_queue_.push_back(cfd);
++unscheduled_compactions_;
} else {
cfd->set_queued_for_compaction(false);
cfd->UnrefAndTryDelete();
}
return true;
}
ColumnFamilyData* DBImpl::PopFirstFromCompactionQueue() {
assert(!compaction_queue_.empty());
auto cfd = *compaction_queue_.begin();
@@ -3603,22 +3765,37 @@ DBImpl::FlushRequest DBImpl::PopFirstFromFlushQueue() {
return flush_req;
}
ColumnFamilyData* DBImpl::PickCompactionFromQueue(
DBImpl::CompactionQueuePickResult DBImpl::PickCompactionFromQueue(
std::unique_ptr<TaskLimiterToken>* token, LogBuffer* log_buffer) {
assert(!compaction_queue_.empty());
assert(*token == nullptr);
autovector<ColumnFamilyData*> throttled_candidates;
ColumnFamilyData* cfd = nullptr;
CompactionQueuePickResult result = CompactionQueueThrottled{};
while (!compaction_queue_.empty()) {
auto first_cfd = *compaction_queue_.begin();
compaction_queue_.pop_front();
assert(first_cfd->queued_for_compaction());
if (first_cfd->IsDropped()) {
first_cfd->set_queued_for_compaction(false);
result = CompactionQueuePicked{first_cfd};
break;
}
if (first_cfd->compaction_aborted() > 0) {
// Parking consumes this worker's credit. Remaining queue entries retain
// their own credits and are scheduled when this worker returns.
[[maybe_unused]] const bool inserted =
parked_compaction_cfds_.insert(first_cfd).second;
assert(inserted);
TEST_SYNC_POINT("DBImpl::PickCompactionFromQueue:Parked");
result = CompactionQueueParked{};
break;
}
if (!RequestCompactionToken(first_cfd, false, token, log_buffer)) {
throttled_candidates.push_back(first_cfd);
continue;
}
cfd = first_cfd;
cfd->set_queued_for_compaction(false);
first_cfd->set_queued_for_compaction(false);
result = CompactionQueuePicked{first_cfd};
break;
}
// Add throttled compaction candidates back to queue in the original order.
@@ -3626,7 +3803,7 @@ ColumnFamilyData* DBImpl::PickCompactionFromQueue(
iter != throttled_candidates.rend(); ++iter) {
compaction_queue_.push_front(*iter);
}
return cfd;
return result;
}
bool DBImpl::EnqueuePendingFlush(const FlushRequest& flush_req) {
@@ -3635,6 +3812,10 @@ bool DBImpl::EnqueuePendingFlush(const FlushRequest& flush_req) {
if (reject_new_background_jobs_) {
return enqueued;
}
if (error_handler_.IsBGWorkStopped() &&
!IsRecoveryFlush(flush_req.flush_reason)) {
return enqueued;
}
if (flush_req.cfd_to_max_mem_id_to_persist.empty()) {
return enqueued;
}
@@ -3673,13 +3854,21 @@ bool DBImpl::EnqueuePendingFlush(const FlushRequest& flush_req) {
void DBImpl::EnqueuePendingCompaction(ColumnFamilyData* cfd) {
mutex_.AssertHeld();
if (reject_new_background_jobs_) {
if (reject_new_background_jobs_ || cfd->IsDropped()) {
return;
}
if (!cfd->queued_for_compaction() && cfd->NeedsCompaction()) {
TEST_SYNC_POINT_CALLBACK("EnqueuePendingCompaction::cfd",
static_cast<void*>(cfd));
AddToCompactionQueue(cfd);
if (cfd->compaction_aborted() > 0) {
cfd->Ref();
cfd->set_queued_for_compaction(true);
[[maybe_unused]] const bool inserted =
parked_compaction_cfds_.insert(cfd).second;
assert(inserted);
} else {
AddToCompactionQueue(cfd);
}
}
}
@@ -3783,8 +3972,8 @@ Status DBImpl::BackgroundFlush(bool* made_progress, JobContext* job_context,
Status status;
*reason = FlushReason::kOthers;
// If BG work is stopped due to an error, but a recovery is in progress,
// that means this flush is part of the recovery. So allow it to go through
// During recovery, allow flush workers to inspect the queue. Non-recovery
// requests are dropped below.
if (!error_handler_.IsBGWorkStopped()) {
if (shutting_down_.load(std::memory_order_acquire)) {
status = Status::ShutdownInProgress();
@@ -3806,12 +3995,9 @@ Status DBImpl::BackgroundFlush(bool* made_progress, JobContext* job_context,
FlushRequest flush_req = PopFirstFromFlushQueue();
FlushReason flush_reason = flush_req.flush_reason;
if (!error_handler_.GetBGError().ok() && error_handler_.IsBGWorkStopped() &&
flush_reason != FlushReason::kErrorRecovery &&
flush_reason != FlushReason::kErrorRecoveryRetryFlush) {
// Stop non-recovery flush when bg work is stopped
// Note that we drop the flush request here.
// Recovery thread should schedule further flushes after bg error
// is cleared.
!IsRecoveryFlush(flush_reason)) {
// A request queued before the error can reach a worker before recovery
// clears the queue. Drop it; recovery rebuilds flush work for all CFs.
status = error_handler_.GetBGError();
assert(!status.ok());
ROCKS_LOG_BUFFER(log_buffer,
@@ -4139,7 +4325,7 @@ void DBImpl::BackgroundCallCompaction(PrepickedCompaction* prepicked_compaction,
prepicked_compaction->task_token.reset();
}
if (made_progress ||
if (made_progress || per_cf_compaction_abort_waiters_ > 0 ||
(bg_compaction_scheduled_ == 0 &&
bg_bottom_compaction_scheduled_ == 0) ||
HasPendingManualCompaction() || unscheduled_compactions_ == 0) {
@@ -4181,7 +4367,29 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
prepicked_compaction->compaction != nullptr) {
c.reset(prepicked_compaction->compaction);
}
// Track this CF as having an in-flight compaction for the duration of this
// BackgroundCompaction (which covers NotifyOnCompactionCompleted). The Defer
// ends tracking on every return path. BeginInFlightCompaction()'s Ref keeps a
// dropped CF alive across any c.reset() until then; if this ends the CF's
// last reference, the FindObsoleteFiles() pass in BackgroundCallCompaction
// cleans up its files.
ColumnFamilyData* in_flight_cfd = nullptr;
Defer end_in_flight_compaction([&]() {
if (in_flight_cfd != nullptr) {
EndInFlightCompaction(in_flight_cfd);
}
});
if (c != nullptr) {
in_flight_cfd = c->column_family_data();
BeginInFlightCompaction(in_flight_cfd);
}
bool is_prepicked = is_manual || c;
ColumnFamilyData* prepicked_cfd = nullptr;
if (c) {
prepicked_cfd = c->column_family_data();
} else if (manual_compaction != nullptr) {
prepicked_cfd = manual_compaction->cfd;
}
// (manual_compaction->in_progress == false);
bool trivial_move_disallowed =
@@ -4209,6 +4417,9 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
// can schedule the still-queued work.
unscheduled_compactions_++;
}
} else if (prepicked_cfd != nullptr &&
prepicked_cfd->compaction_aborted() > 0) {
status = Status::Incomplete(Status::SubCode::kCompactionAborted);
} else if (is_manual &&
manual_compaction->canceled.load(std::memory_order_acquire)) {
status = Status::Incomplete(Status::SubCode::kManualCompactionPaused);
@@ -4313,13 +4524,18 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
ColumnFamilyData* cfd = nullptr;
if (!need_repick) {
cfd = PickCompactionFromQueue(&task_token, log_buffer);
if (cfd == nullptr) {
auto pick_result = PickCompactionFromQueue(&task_token, log_buffer);
if (std::holds_alternative<CompactionQueueThrottled>(pick_result)) {
// Can't find any executable task from the compaction queue.
// All tasks have been throttled by compaction thread limiter.
++unscheduled_compactions_;
return Status::Busy();
}
if (std::holds_alternative<CompactionQueueParked>(pick_result)) {
return Status::Incomplete(Status::SubCode::kCompactionAborted);
}
cfd = std::get<CompactionQueuePicked>(pick_result).cfd;
assert(cfd != nullptr);
// We unreference here because the following code will take a Ref() on
// this cfd if it is going to use it (Compaction class holds a
@@ -4360,6 +4576,10 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
mutable_cf_options, mutable_db_options_, job_context->snapshot_seqs,
job_context->snapshot_checker, log_buffer,
thread_pri == Env::Priority::BOTTOM /* require_max_output_level */));
if (c != nullptr && in_flight_cfd == nullptr) {
in_flight_cfd = cfd;
BeginInFlightCompaction(in_flight_cfd);
}
if (thread_pri == Env::Priority::LOW) {
TEST_SYNC_POINT("DBImpl::BackgroundCompaction():AfterPickCompaction");
} else if (thread_pri == Env::Priority::BOTTOM) {
@@ -4840,7 +5060,7 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
compaction_aborted_, db_id_, db_session_id_,
c->column_family_data()->GetFullHistoryTsLow(), c->trim_ts(),
&blob_callback_, &bg_compaction_scheduled_,
&bg_bottom_compaction_scheduled_);
&bg_bottom_compaction_scheduled_, &num_running_remote_compactions_);
compaction_job.Prepare(std::nullopt /*subcompact to be computed*/);
std::unique_ptr<std::list<uint64_t>::iterator> min_options_file_number_elem;
@@ -5082,6 +5302,14 @@ bool DBImpl::HasPendingManualCompaction() {
return (!manual_compaction_dequeue_.empty());
}
bool DBImpl::HasPendingManualCompaction(ColumnFamilyData* cfd) {
return std::any_of(manual_compaction_dequeue_.begin(),
manual_compaction_dequeue_.end(),
[cfd](const ManualCompactionState* manual) {
return manual->cfd == cfd;
});
}
void DBImpl::AddManualCompaction(DBImpl::ManualCompactionState* m) {
assert(manual_compaction_paused_ == 0);
manual_compaction_dequeue_.push_back(m);
@@ -5293,6 +5521,11 @@ void DBImpl::InstallSuperVersionAndScheduleWork(
cfd->InstallSuperVersion(sv_context, &mutex_,
std::move(new_seqno_to_time_mapping));
// Refresh the seqno->time preserve-window bound used to gate bottommost file
// marking, so a file whose largest seqno is still within the preserve window
// is not marked for a compaction that cannot zero it out (infinite loop).
MaybeUpdatePreserveTimeMinSeqno(cfd);
// There may be a small data race here. The snapshot tricking bottommost
// compaction may already be released here. But assuming there will always be
// newer snapshot created and released frequently, the compaction will be
@@ -5432,6 +5665,7 @@ Status DBImpl::WaitForCompact(
}
if ((bg_bottom_compaction_scheduled_ || bg_compaction_scheduled_ ||
bg_flush_scheduled_ || unscheduled_compactions_ ||
!parked_compaction_cfds_.empty() ||
(wait_for_compact_options.wait_for_purge && bg_purge_scheduled_) ||
unscheduled_flushes_ || error_handler_.IsRecoveryInProgress()) &&
(error_handler_.GetBGError().ok())) {
+6
View File
@@ -225,6 +225,12 @@ Status DBImpl::TEST_GetBGError() {
return error_handler_.GetBGError();
}
void DBImpl::TEST_SetBGError(const IOStatus& error,
BackgroundErrorReason reason) {
InstrumentedMutexLock l(&mutex_);
error_handler_.SetBGError(error, reason);
}
bool DBImpl::TEST_IsRecoveryInProgress() {
InstrumentedMutexLock l(&mutex_);
return error_handler_.IsRecoveryInProgress();
+2
View File
@@ -55,6 +55,8 @@ Env::IOActivity GetCurrentThreadIOActivityForMetadataRead() {
case ThreadStatus::OperationType::
OP_GET_FILE_CHECKSUMS_FROM_CURRENT_MANIFEST:
return Env::IOActivity::kGetFileChecksumsFromCurrentManifest;
case ThreadStatus::OperationType::OP_LAZY_RESOLVE:
return Env::IOActivity::kLazyResolve;
default:
return Env::IOActivity::kUnknown;
}
+13 -13
View File
@@ -131,21 +131,21 @@ Status DBImplFollower::TryCatchUpWithLeader() {
for (auto cfd : cfds_changed) {
if (cfd->mem()->GetEarliestSequenceNumber() <
versions_->LastSequence()) {
// Construct a new memtable with earliest sequence number set to the
// last sequence number in the VersionSet. This matters when
// DBImpl::MultiCFSnapshot tries to get consistent references
// to super versions in a lock free manner, it checks the earliest
// sequence number to detect if there was a change in version in
// the meantime.
MemTable* new_mem = cfd->ConstructNewMemtable(
cfd->GetLatestMutableCFOptions(), versions_->LastSequence());
cfd->mem()->SetNextLogNumber(cfd->GetLogNumber());
cfd->mem()->ConstructFragmentedRangeTombstones();
cfd->imm()->Add(cfd->mem(), &job_context.memtables_to_free);
new_mem->Ref();
cfd->SetMemtable(new_mem);
// Seal the active memtable so that its replacement gets its earliest
// sequence number set to the last sequence number in the VersionSet.
// This matters when DBImpl::MultiCFSnapshot tries to get consistent
// references to super versions in a lock free manner, it checks the
// earliest sequence number to detect if there was a change in
// version in the meantime.
SealActiveMemtable(cfd, cfd->GetLogNumber(),
versions_->LastSequence(), &job_context);
}
// `cfd->GetLogNumber()` is a safe watermark here, unlike in
// DBImplSecondary::TryCatchUpWithPrimary(): this catch-up path never
// replays WALs, so the memtables it collects hold nothing that only a
// not-yet-installed Version could make readable.
//
// This will check if the old memtable is still referenced
cfd->imm()->RemoveOldMemTables(cfd->GetLogNumber(),
&job_context.memtables_to_free);
+35 -2
View File
@@ -243,6 +243,11 @@ Status DBImpl::ValidateOptions(
}
Status DBImpl::ValidateOptions(const DBOptions& db_options) {
if (db_options.read_io_executor_threads <= 0) {
return Status::InvalidArgument(
"read_io_executor_threads must be greater than zero");
}
if (db_options.db_paths.size() > 4) {
return Status::NotSupported(
"More than four DB paths are not supported yet. ");
@@ -1029,7 +1034,7 @@ Status DBImpl::InitPersistStatsColumnFamily() {
return s;
}
Status DBImpl::LogAndApplyForRecovery(const RecoveryContext& recovery_ctx) {
Status DBImpl::LogAndApplyForRecovery(RecoveryContext& recovery_ctx) {
mutex_.AssertHeld();
// descriptor_log_ is normally null after Recover, but when
// reuse_manifest_on_open is set VersionSet::Recover may have already
@@ -1039,9 +1044,21 @@ Status DBImpl::LogAndApplyForRecovery(const RecoveryContext& recovery_ctx) {
const ReadOptions read_options(Env::IOActivity::kDBOpen);
const WriteOptions write_options(Env::IOActivity::kDBOpen);
if (versions_->force_new_manifest_on_open_ &&
!recovery_ctx.HasVersionEdits()) {
VersionEdit edit;
ColumnFamilyData* default_cfd =
versions_->GetColumnFamilySet()->GetDefault();
assert(default_cfd);
recovery_ctx.UpdateVersionEdits(default_cfd, edit);
}
Status s = versions_->LogAndApply(recovery_ctx.cfds_, read_options,
write_options, recovery_ctx.edit_lists_,
&mutex_, directories_.GetDbDir());
if (s.ok()) {
versions_->force_new_manifest_on_open_ = false;
}
return s;
}
@@ -2171,7 +2188,7 @@ Status DBImpl::WriteLevel0TableForRecovery(int job_id, ColumnFamilyData* cfd,
false /* is_bottommost */, TableFileCreationReason::kRecovery,
0 /* oldest_key_time */, 0 /* file_creation_time */, db_id_,
db_session_id_, 0 /* target_file_size */, meta.fd.GetNumber(),
kMaxSequenceNumber);
kMaxSequenceNumber, dbname_ /* db_name */);
Version* version = cfd->current();
version->Ref();
TableProperties temp_table_proerties;
@@ -2872,11 +2889,16 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
}
TEST_SYNC_POINT("DBImpl::Open:Opened");
Status persist_options_status;
bool cleanup_obsolete_options_files = false;
if (s.ok()) {
// Persist RocksDB Options before scheduling the compaction.
// The WriteOptionsFile() will release and lock the mutex internally.
persist_options_status =
impl->WriteOptionsFile(write_options, true /*db_mutex_already_held*/);
cleanup_obsolete_options_files =
impl->immutable_db_options_.avoid_unnecessary_blocking_io &&
impl->immutable_db_options_.compaction_service == nullptr &&
!impl->disable_delete_obsolete_files_;
impl->opened_successfully_ = true;
} else {
persist_options_status.PermitUncheckedError();
@@ -2966,6 +2988,17 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
s = impl->RegisterRecordSeqnoTimeWorker();
}
impl->options_mutex_.Unlock();
if (cleanup_obsolete_options_files) {
Status obsolete_options_status =
impl->DeleteObsoleteOptionsFiles(/*schedule_only=*/s.ok());
if (!obsolete_options_status.ok()) {
ROCKS_LOG_WARN(impl->immutable_db_options_.info_log,
"Unable to %s obsolete OPTIONS files%s: %s",
s.ok() ? "schedule deletion of" : "delete",
s.ok() ? "" : " after DB open failure",
obsolete_options_status.ToString().c_str());
}
}
if (s.ok()) {
*dbptr = std::move(impl);
} else {
+13 -1
View File
@@ -128,7 +128,8 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplReadOnly::GetImpl)
/*value_found*/ nullptr,
/*key_exists*/ nullptr, /*seq*/ nullptr, &read_cb,
/*is_blob*/ nullptr,
/*do_merge=*/get_impl_options.get_value);
/*do_merge=*/get_impl_options.get_value,
get_impl_options.lazy_columns_same_file_reader);
RecordTick(stats_, MEMTABLE_MISS);
}
{
@@ -156,6 +157,17 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplReadOnly::GetImpl)
PERF_COUNTER_ADD(get_read_bytes, size);
}
}
if (get_impl_options.lazy_columns_pin != nullptr && s.ok()) {
// Lazy result (GetEntityLazy): hand back the Version the entity's blob
// references must be resolved against, and transfer a SuperVersion pin into
// the result so it -- and deferred blob reads -- stay valid after this call
// (as an iterator's pin does). Read-only borrows the current SuperVersion
// without a reference, so the pin takes its own.
if (get_impl_options.lazy_columns_version != nullptr) {
*get_impl_options.lazy_columns_version = super_version->current;
}
TransferSuperVersionPin(super_version, get_impl_options.lazy_columns_pin);
}
CO_RETURN s;
}
+365 -49
View File
@@ -7,6 +7,7 @@
#include <cinttypes>
#include <optional>
#include <unordered_set>
#include "db/arena_wrapped_db_iter.h"
#include "db/blob/blob_fetcher.h"
@@ -81,7 +82,9 @@ Status DBImplSecondary::FindAndRecoverLogFiles(
Status s;
std::vector<uint64_t> logs;
s = FindNewLogNumbers(&logs);
if (s.ok() && !logs.empty()) {
if (s.ok()) {
// Empty recovery rounds still prune resolved transactions and completed
// prep tracking.
SequenceNumber next_sequence(kMaxSequenceNumber);
s = RecoverLogFiles(logs, &next_sequence, cfds_changed, job_context);
}
@@ -204,6 +207,8 @@ Status DBImplSecondary::RecoverLogFiles(
assert(reader != nullptr);
}
DeleteResolvedRecoveredTransactions();
const UnorderedMap<uint32_t, size_t>& running_ts_sz =
versions_->GetRunningColumnFamiliesTimestampSize();
for (auto log_number : log_numbers) {
@@ -220,6 +225,10 @@ Status DBImplSecondary::RecoverLogFiles(
std::string scratch;
Slice record;
WriteBatch batch;
// Hoisted alongside `scratch`, `record` and `batch`: the loop below runs
// once per replayed WAL record, and clear() keeps the bucket array that a
// set constructed there would allocate again for every record.
std::unordered_set<uint32_t> selected_cf_ids;
while (reader->ReadRecord(&record, &scratch,
immutable_db_options_.wal_recovery_mode) &&
@@ -253,42 +262,62 @@ Status DBImplSecondary::RecoverLogFiles(
continue;
}
cfds_changed->insert(cfd);
// The return value is what the seal below compares against
// `log_number`, so the call has to stay. The write also names a WAL
// for column families whose inserts are skipped as already flushed
// and never reach the set recorded after the insert.
const std::optional<uint64_t> prev_log_number =
RecordCurrentLog(id, log_number);
const std::vector<FileMetaData*>& l0_files =
cfd->current()->storage_info()->LevelFiles(0);
SequenceNumber seq =
l0_files.empty() ? 0 : l0_files.back()->fd.largest_seqno;
// If the write batch's sequence number is smaller than the last
// sequence number of the largest sequence persisted for this column
// family, then its data must reside in an SST that has already been
// added in the prior MANIFEST replay.
// `l0_files` is ordered newest first, so `seq` is the largest
// sequence number in the oldest L0 file. A write batch at or below it
// must reside in an SST that a prior MANIFEST replay already added.
if (seq_of_batch <= seq) {
continue;
}
auto curr_log_num = std::numeric_limits<uint64_t>::max();
if (cfd_to_current_log_.count(cfd) > 0) {
curr_log_num = cfd_to_current_log_[cfd];
}
// If the active memtable contains records added by replaying an
// earlier WAL, then we need to seal the memtable, add it to the
// immutable memtable list and create a new active memtable.
if (!cfd->mem()->IsEmpty() &&
(curr_log_num == std::numeric_limits<uint64_t>::max() ||
curr_log_num != log_number)) {
MemTable* new_mem = cfd->ConstructNewMemtable(
cfd->GetLatestMutableCFOptions(), seq_of_batch);
cfd->mem()->SetNextLogNumber(log_number);
cfd->mem()->ConstructFragmentedRangeTombstones();
cfd->imm()->Add(cfd->mem(), &job_context->memtables_to_free);
new_mem->Ref();
cfd->SetMemtable(new_mem);
// immutable memtable list and create a new active memtable, so that
// the sealed memtable can be dropped once the primary has flushed
// that earlier WAL.
//
// This is best effort: the sequence number check above can skip the
// seal without skipping the insert that follows, leaving the active
// memtable holding entries from more than one WAL.
// MaybeSealFullyFlushedActiveMemtable() tolerates that because
// `cf_id_to_current_log_[id]` is the newest of them.
if (!cfd->mem()->IsEmpty() && prev_log_number != log_number) {
SealActiveMemtable(cfd, log_number, seq_of_batch, job_context);
}
}
bool has_valid_writes = false;
selected_cf_ids.clear();
status = WriteBatchInternal::InsertInto(
&batch, column_family_memtables_.get(),
nullptr /* flush_scheduler */, nullptr /* trim_history_scheduler*/,
true, log_number, this, false /* concurrent_memtable_writes */,
next_sequence, &has_valid_writes, seq_per_batch_, batch_per_txn_);
next_sequence, &has_valid_writes, seq_per_batch_, batch_per_txn_,
&selected_cf_ids);
// A 2PC commit marker names no column family yet replays its prepared
// batch into the memtables, so the recorded WAL would lag the committed
// data and the gate would drop it. Mark them changed too, or a
// commit-replay-only round skips the super version install and the
// retention warning.
//
// The seal loop above runs over named ids, so a commit marker takes no
// seal decision: its data joins the memtable's current WAL generation,
// which then stays until the primary flushes the commit's WAL.
for (const uint32_t id : selected_cf_ids) {
RecordCurrentLog(id, log_number);
ColumnFamilyData* cfd =
versions_->GetColumnFamilySet()->GetColumnFamily(id);
if (cfd != nullptr) {
cfds_changed->insert(cfd);
}
}
}
// If column family was not found, it might mean that the WAL write
// batch references to the column family that was dropped after the
@@ -298,20 +327,6 @@ Status DBImplSecondary::RecoverLogFiles(
// passing null flush_scheduler will disable memtable flushing which is
// needed for secondary instances
if (status.ok()) {
for (const auto id : column_family_ids) {
ColumnFamilyData* cfd =
versions_->GetColumnFamilySet()->GetColumnFamily(id);
if (cfd == nullptr) {
continue;
}
std::unordered_map<ColumnFamilyData*, uint64_t>::iterator iter =
cfd_to_current_log_.find(cfd);
if (iter == cfd_to_current_log_.end()) {
cfd_to_current_log_.insert({cfd, log_number});
} else if (log_number > iter->second) {
iter->second = log_number;
}
}
auto last_sequence = *next_sequence - 1;
if ((*next_sequence != kMaxSequenceNumber) &&
(versions_->LastSequence() <= last_sequence)) {
@@ -336,6 +351,176 @@ Status DBImplSecondary::RecoverLogFiles(
return status;
}
void DBImplSecondary::SealActiveMemtable(ColumnFamilyData* cfd,
uint64_t next_log_number,
SequenceNumber new_mem_earliest_seq,
JobContext* job_context) {
mutex_.AssertHeld();
assert(cfd != nullptr);
assert(job_context != nullptr);
MemTable* new_mem = cfd->ConstructNewMemtable(
cfd->GetLatestMutableCFOptions(), new_mem_earliest_seq);
cfd->mem()->SetNextLogNumber(next_log_number);
cfd->mem()->ConstructFragmentedRangeTombstones();
cfd->imm()->Add(cfd->mem(), &job_context->memtables_to_free);
new_mem->Ref();
cfd->SetMemtable(new_mem);
}
std::optional<uint64_t> DBImplSecondary::RecordCurrentLog(uint32_t cf_id,
uint64_t log_number) {
mutex_.AssertHeld();
const auto [log_iter, inserted] =
cf_id_to_current_log_.try_emplace(cf_id, log_number);
if (inserted) {
return std::nullopt;
}
const uint64_t prev_log_number = log_iter->second;
if (log_number > prev_log_number) {
log_iter->second = log_number;
}
return prev_log_number;
}
bool DBImplSecondary::MaybeSealFullyFlushedActiveMemtable(
ColumnFamilyData* cfd, uint64_t installed_log_number,
JobContext* job_context) {
mutex_.AssertHeld();
assert(cfd != nullptr);
assert(job_context != nullptr);
assert(installed_log_number <= cfd->GetLogNumber());
if (cfd->mem()->IsEmpty()) {
return false;
}
const auto log_iter = cf_id_to_current_log_.find(cfd->GetID());
if (log_iter == cf_id_to_current_log_.end()) {
return false;
}
// Keep the memtable unless the installed Version covers everything it holds:
// only the WALs older than `installed_log_number` are readable from the files
// that Version references. The loop in TryCatchUpWithPrimary() explains why
// the watermark has to come from the Version.
if (log_iter->second >= installed_log_number) {
return false;
}
// The file set is now the authority for these entries, so keeping them would
// be incorrect rather than merely redundant: point lookups stop at the
// memtable, and bottommost compaction rewriting the flushed entry's sequence
// number to 0 makes the shadowing permanent.
ROCKS_LOG_INFO(immutable_db_options_.info_log,
"[%s] Sealing active memtable replayed from WAL %" PRIu64
", flushed by the primary and readable as of log number "
"%" PRIu64 "; its first sequence number is %" PRIu64,
cfd->GetName().c_str(), log_iter->second, installed_log_number,
static_cast<uint64_t>(cfd->mem()->GetFirstSequenceNumber()));
// The memtable holds no entries from a WAL newer than `log_iter->second`, so
// + 1 is the log number following its contents, and the check above puts that
// at or below `installed_log_number`. The older immutable memtables that
// RemoveOldMemTables() examines first were themselves sealed on a WAL switch
// and stamped no higher, so nothing stops its scan short of this memtable and
// it is collected in the same round.
//
// The replacement memtable's earliest sequence number is the VersionSet's
// last sequence number, as in DBImplFollower::TryCatchUpWithLeader():
// DBImpl::MultiCFSnapshot() compares it against the last sequence number to
// detect a version change while collecting super version references without
// the mutex. That can be above the sequence number replayed next, so it is
// not yet the lower bound MemTable::SetEarliestSequenceNumber() documents;
// MemTable::Add() corrects it downward on the first insert, and until then
// the memtable holds no key a too-high bound could hide.
SealActiveMemtable(cfd, log_iter->second + 1, versions_->LastSequence(),
job_context);
cf_id_to_current_log_.erase(log_iter);
return true;
}
void DBImplSecondary::MaybeWarnAboutRetainedMemtables(
ColumnFamilyData* cfd, uint64_t installed_log_number) {
mutex_.AssertHeld();
assert(cfd != nullptr);
// Tested before the count below because it is false whenever the secondary
// is keeping up with the primary, which is the normal case.
if (installed_log_number >= cfd->GetLogNumber()) {
cf_id_to_retention_warning_.erase(cfd->GetID());
return;
}
const int retained = cfd->imm()->NumNotFlushed();
if (retained == 0) {
cf_id_to_retention_warning_.erase(cfd->GetID());
return;
}
// Warn only when either number changes. The condition lasts until the
// primary's flushed files become readable, and TryCatchUpWithPrimary() is
// called as often as the application chooses, so reporting it every round
// would bury everything else in the log. Both values move only as the
// condition worsens, making this one line per newly retained memtable.
const std::pair<uint64_t, int> warning{installed_log_number, retained};
const auto [warning_iter, inserted] =
cf_id_to_retention_warning_.insert({cfd->GetID(), warning});
if (!inserted) {
if (warning_iter->second == warning) {
return;
}
warning_iter->second = warning;
}
// ColumnFamilyData::RecalculateWriteStallConditions() also counts this as a
// memtable limit stop, but attributes it to a flush that is not coming and to
// a `max_write_buffer_number` that no writer of this instance's own can hit,
// so name the actual cause.
ROCKS_LOG_WARN(immutable_db_options_.info_log,
"[%s] Retaining %d immutable memtable(s): the primary has "
"flushed up to log number %" PRIu64
" but no Version past log number %" PRIu64
" could be installed, so they may hold the only readable copy "
"of what was flushed",
cfd->GetName().c_str(), retained, cfd->GetLogNumber(),
installed_log_number);
}
// The primary keeps min_log_number_to_keep at or below every unresolved prepare
// and every prepare with unflushed committed data. A recovered transaction
// entirely below it is therefore committed and flushed, or rolled back; see
// PrecomputeMinLogNumberToKeep2PC(). FindNewLogNumbers() uses the same
// threshold, so no marker below it can be replayed. If a resolution marker
// appears in a later WAL, its handler tolerates missing recovery state.
// Committed writes are skipped because the column family's log number is
// already past their WAL.
//
// Drop before replay so a reused name starts a new generation instead of
// merging with stale state. Open every reader first so a round that cannot
// replay drops nothing.
void DBImplSecondary::DeleteResolvedRecoveredTransactions() {
mutex_.AssertHeld();
if (recovered_transactions_.empty()) {
return;
}
const uint64_t min_log_number_to_keep = versions_->min_log_number_to_keep();
size_t deleted = 0;
for (RecoveredTransactionMap::iterator it = recovered_transactions_.begin();
it != recovered_transactions_.end();) {
assert(!it->second->batches_.empty());
bool all_batches_below_watermark = true;
for (const auto& batch_info : it->second->batches_) {
if (batch_info.second.log_number_ >= min_log_number_to_keep) {
all_batches_below_watermark = false;
break;
}
}
if (all_batches_below_watermark) {
it = DeleteRecoveredTransaction(it);
++deleted;
} else {
++it;
}
}
if (deleted > 0) {
ROCKS_LOG_DEBUG(immutable_db_options_.info_log,
"Dropped %" ROCKSDB_PRIszt
" recovered transaction(s) prepared before WAL %" PRIu64,
deleted, min_log_number_to_keep);
}
}
Iterator* DBImplSecondary::NewIterator(const ReadOptions& _read_options,
ColumnFamilyHandle* column_family) {
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
@@ -496,10 +681,12 @@ Status DBImplSecondary::TryCatchUpWithPrimary() {
{
InstrumentedMutexLock lock_guard(&mutex_);
assert(manifest_reader_.get() != nullptr);
s = static_cast_with_check<ReactiveVersionSet>(versions_.get())
->ReadAndApply(&mutex_, &manifest_reader_,
manifest_reader_status_.get(), &cfds_changed,
/*files_to_delete=*/nullptr);
auto* reactive_versions =
static_cast_with_check<ReactiveVersionSet>(versions_.get());
s = reactive_versions->ReadAndApply(&mutex_, &manifest_reader_,
manifest_reader_status_.get(),
&cfds_changed,
/*files_to_delete=*/nullptr);
ROCKS_LOG_INFO(immutable_db_options_.info_log, "Last sequence is %" PRIu64,
static_cast<uint64_t>(versions_->LastSequence()));
@@ -517,8 +704,10 @@ Status DBImplSecondary::TryCatchUpWithPrimary() {
// list wal_dir to discover new WALs and apply new changes to the secondary
// instance
bool replayed_every_wal_found = false;
if (s.ok()) {
s = FindAndRecoverLogFiles(&cfds_changed, &job_context);
replayed_every_wal_found = s.ok();
if (s.IsPathNotFound()) {
ROCKS_LOG_INFO(
immutable_db_options_.info_log,
@@ -528,13 +717,65 @@ Status DBImplSecondary::TryCatchUpWithPrimary() {
}
}
if (s.ok()) {
for (auto cfd : cfds_changed) {
cfd->imm()->RemoveOldMemTables(cfd->GetLogNumber(),
// Iterate over every column family rather than only `cfds_changed`: a
// stale memtable left behind because an earlier round failed after
// ReadAndApply() had already advanced the log number must still be
// dropped, and that column family may have no new MANIFEST record in this
// round. Column families dropped in this round are skipped, unlike with
// `cfds_changed`, which can still name a column family that a later
// record in the same round made VersionEditHandler::DestroyCfAndCleanup()
// drop. Column families that neither changed nor have anything to collect
// are skipped below, so this installs no extra super versions.
for (ColumnFamilyData* cfd : *versions_->GetColumnFamilySet()) {
if (cfd->IsDropped()) {
// Nothing more can ever be replayed into a dropped column family, so
// drop its entry too: it had no removal path before. Ids are never
// reused, so a leftover entry could only waste space, and one is
// still left behind by a column family destroyed before this loop
// next runs.
cf_id_to_current_log_.erase(cfd->GetID());
cf_id_to_retention_warning_.erase(cfd->GetID());
continue;
}
if (!cfd->initialized()) {
// There is no memtable to reconcile yet, and any entry the column
// family already has still names the WAL a later round must gate on.
continue;
}
// Drop memtables against the log number of the Version most recently
// installed, never `cfd->GetLogNumber()`: that advances as soon as the
// primary's flush record is read, even when the flushed files cannot be
// opened or an incomplete atomic group parks the new Version. Until
// such a Version is installed the memtable can hold the only readable
// copy of what was flushed, so dropping it against that number would
// turn a stale read into a vanished key.
const uint64_t installed_log_number =
reactive_versions->GetInstalledVersionLogNumber(cfd->GetID());
const bool sealed = MaybeSealFullyFlushedActiveMemtable(
cfd, installed_log_number, &job_context);
// A round that failed after RecoverLogFiles() had sealed on a WAL
// switch can also leave a collectible immutable memtable behind, so
// check for one instead of relying on this round having sealed.
const bool needs_super_version =
sealed ||
cfd->imm()->HasOldMemTablesToRemove(installed_log_number) ||
cfds_changed.count(cfd) > 0;
if (!needs_super_version) {
continue;
}
cfd->imm()->RemoveOldMemTables(installed_log_number,
&job_context.memtables_to_free);
MaybeWarnAboutRetainedMemtables(cfd, installed_log_number);
auto& sv_context = job_context.superversion_contexts.back();
cfd->InstallSuperVersion(&sv_context, &mutex_);
sv_context.NewSuperVersion();
}
if (replayed_every_wal_found) {
// Secondary catch-up does not run the primary paths that prune the
// completed tracker prefix. Replay and pre-replay cleanup add
// completions; an outstanding prepare stops the scan.
(void)logs_with_prep_tracker_.FindMinLogContainingOutstandingPrep();
}
}
}
job_context.Clean();
@@ -581,7 +822,7 @@ Status DB::OpenAsSecondary(
std::vector<ColumnFamilyHandle*>* handles, std::unique_ptr<DB>* dbptr) {
return DBImplSecondary::OpenAsSecondaryImpl(
db_options, dbname, secondary_path, column_families, handles, dbptr,
/*recover_wal=*/true);
/*recover_wal=*/true, /*trust_manifest_recovery=*/false);
}
Status DBImplSecondary::OpenAsSecondaryImpl(
@@ -589,7 +830,7 @@ Status DBImplSecondary::OpenAsSecondaryImpl(
const std::string& secondary_path,
const std::vector<ColumnFamilyDescriptor>& column_families,
std::vector<ColumnFamilyHandle*>* handles, std::unique_ptr<DB>* dbptr,
bool recover_wal) {
bool recover_wal, bool trust_manifest_recovery) {
*dbptr = nullptr;
DBOptions tmp_opts(db_options);
@@ -630,6 +871,8 @@ Status DBImplSecondary::OpenAsSecondaryImpl(
impl->file_options_, impl->table_cache_.get(),
impl->write_buffer_manager_, &impl->write_controller_, impl->io_tracer_,
impl->db_id_, impl->db_session_id_));
static_cast_with_check<ReactiveVersionSet>(impl->versions_.get())
->SetTrustManifestRecovery(trust_manifest_recovery);
impl->column_family_memtables_.reset(
new ColumnFamilyMemTablesImpl(impl->versions_->GetColumnFamilySet()));
impl->wal_in_db_path_ = impl->immutable_db_options_.IsWalDirSameAsDBPath();
@@ -1239,6 +1482,22 @@ Status DBImplSecondary::CompactWithoutInstallation(
/*managed_snapshot=*/nullptr,
kMaxSequenceNumber, std::move(snapshots));
// Ensure FileMetaData stats (num_entries, num_range_deletions) are
// initialized for all input files. These stats are not persisted in the
// MANIFEST and are loaded lazily from table properties. The Compaction
// constructor's FilterInputsForCompactionIterator() relies on
// FileIsStandAloneRangeTombstone() which needs these fields to be populated.
// Without this, the remote worker may not filter the same files as the
// primary host, leading to input record count verification failures.
{
const ReadOptions read_options(Env::IOActivity::kCompaction);
for (const auto& level_files : input_files) {
for (FileMetaData* file_meta : level_files.files) {
version->MaybeInitializeFileMetaData(read_options, file_meta);
}
}
}
// TODO - consider serializing the entire Compaction object and using it as
// input instead of recreating it in the remote worker
std::unique_ptr<Compaction> c;
@@ -1362,9 +1621,6 @@ Status DB::OpenAndCompact(
db_options.statistics = override_options.statistics;
db_options.listeners = override_options.listeners;
db_options.compaction_service = nullptr;
// We will close the DB after the compaction anyway.
// Open as many files as needed for the compaction.
db_options.max_open_files = -1;
db_options.info_log = override_options.info_log;
// 4. Filter CFs that are needed for OpenAndCompact()
@@ -1407,12 +1663,21 @@ Status DB::OpenAndCompact(
// 5. Open db As Secondary (skip WAL recovery -- remote compaction only
// needs LSM state from MANIFEST, not memtable data from WAL replay)
//
// A non-zero min_manifest_file_number means the primary provided a MANIFEST
// floor (DBOptions::remote_compaction_manifest_floor was on when it scheduled
// the job). Its presence couples on both (a) "trust the MANIFEST" recovery
// and (b) the floor check below. Absence -> the worker uses the original
// point-in-time recovery with no floor check (kill switch off, or an older
// primary).
const bool floor_provided = compaction_input.min_manifest_file_number != 0;
std::unique_ptr<DB> db;
std::vector<ColumnFamilyHandle*> handles;
const uint64_t db_open_start_micros = db_options.env->NowMicros();
s = DBImplSecondary::OpenAsSecondaryImpl(db_options, name, output_directory,
column_families, &handles, &db,
/*recover_wal=*/false);
s = DBImplSecondary::OpenAsSecondaryImpl(
db_options, name, output_directory, column_families, &handles, &db,
/*recover_wal=*/false,
/*trust_manifest_recovery=*/floor_provided);
RecordTimeToHistogram(db_options.statistics.get(),
OPEN_AND_COMPACT_DB_OPEN_MICROS,
db_options.env->NowMicros() - db_open_start_micros);
@@ -1424,6 +1689,57 @@ Status DB::OpenAndCompact(
TEST_SYNC_POINT_CALLBACK(
"DBImplSecondary::OpenAndCompact::AfterOpenAsSecondary:0", db.get());
// 5b. Manifest floor check. Refuse to reconstruct the compaction against an
// older MANIFEST view than the primary scheduled from (e.g. an
// eventually-consistent filesystem returning a stale CURRENT, or a truncated
// MANIFEST): an older LSM shape could wrongly drop keys that should be kept.
// Accept-equal-or-later is safe by monotonicity while the inputs stay locked;
// only an older view is unsafe. On rejection, return Status::Incomplete so
// the CompactionService implementation can fall back to a local compaction
// (kUseLocal) rather than install a possibly-incorrect result.
if (floor_provided) {
VersionSet* recovered_versions =
static_cast_with_check<DBImplSecondary>(db.get())->GetVersionSet();
const uint64_t recovered_number =
recovered_versions->manifest_file_number();
// Compare the maximal valid prefix of the recovered MANIFEST (not the
// reader's read-to-EOF high-water mark, which can include tolerated tail
// garbage: a corrupt/torn trailing record or a partial atomic group at
// EOF). This way a tolerated corrupt tail is fine as long as the
// actually-installed valid prefix reaches the floor. Both sides are byte
// sizes of the same append-only, uniquely-numbered MANIFEST, so equal
// number + recovered_valid_size >= min means the worker installed a
// same-or-newer view; anything short (older/truncated/torn/partial) is
// below the floor and rejected.
const uint64_t recovered_valid_size =
recovered_versions->manifest_recovery_maximal_valid_size();
bool below_floor =
recovered_number < compaction_input.min_manifest_file_number ||
(recovered_number == compaction_input.min_manifest_file_number &&
recovered_valid_size < compaction_input.min_manifest_file_size);
TEST_SYNC_POINT_CALLBACK(
"DBImplSecondary::OpenAndCompact::ManifestFloorCheck", &below_floor);
if (below_floor) {
ROCKS_LOG_WARN(
db_options.info_log,
"Remote compaction recovered an older MANIFEST view (file number "
"%" PRIu64 " valid size %" PRIu64
") than the primary scheduled from "
"(min file number %" PRIu64 " size %" PRIu64
"); declining so the job can fall back to local compaction.",
recovered_number, recovered_valid_size,
compaction_input.min_manifest_file_number,
compaction_input.min_manifest_file_size);
for (auto& handle : handles) {
delete handle;
}
db.reset();
return Status::Incomplete(
"Remote compaction worker recovered an older MANIFEST view than the "
"primary scheduled from");
}
}
// 6. Find the handle of the Column Family that this will compact
ColumnFamilyHandle* cfh = nullptr;
for (auto* handle : handles) {
+88 -4
View File
@@ -5,7 +5,10 @@
#pragma once
#include <optional>
#include <string>
#include <unordered_set>
#include <utility>
#include <vector>
#include "db/db_impl/db_impl.h"
@@ -287,6 +290,23 @@ class DBImplSecondary : public DBImpl {
return false;
}
// Seals `cfd`'s active memtable: adds it to the immutable memtable list and
// puts a fresh, empty active memtable in its place. This is the counterpart
// of DBImpl::SwitchMemtable(), which cannot be used here because there is no
// writer queue and no WAL of this instance's own.
//
// `next_log_number` is recorded on the sealed memtable. A subsequent
// MemTableList::RemoveOldMemTables(log_number) call considers the sealed
// memtable collectible once `next_log_number <= log_number`, so pass a log
// number above every WAL whose entries the memtable holds.
// `new_mem_earliest_seq` becomes the replacement memtable's earliest
// sequence number.
//
// REQUIRES: mutex_ held
void SealActiveMemtable(ColumnFamilyData* cfd, uint64_t next_log_number,
SequenceNumber new_mem_earliest_seq,
JobContext* job_context);
std::unique_ptr<log::FragmentBufferedReader> manifest_reader_;
std::unique_ptr<log::Reader::Reporter> manifest_reporter_;
std::unique_ptr<Status> manifest_reader_status_;
@@ -304,13 +324,63 @@ class DBImplSecondary : public DBImpl {
std::unordered_set<ColumnFamilyData*>* cfds_changed,
JobContext* job_context);
Status FindNewLogNumbers(std::vector<uint64_t>* logs);
// After manifest recovery, replay WALs and refresh log_readers_ if necessary
// Replays WALs after manifest recovery, refreshes log_readers_ as needed, and
// drops recovered transactions the primary resolved. Cleanup runs even when
// `log_numbers` is empty.
// REQUIRES: log_numbers are sorted in ascending order
Status RecoverLogFiles(const std::vector<uint64_t>& log_numbers,
SequenceNumber* next_sequence,
std::unordered_set<ColumnFamilyData*>* cfds_changed,
JobContext* job_context);
// Records `log_number` as the newest WAL whose entries `cf_id`'s active
// memtable may hold, never lowering what is already recorded. Returns what
// was recorded before this call, or nullopt if nothing was.
//
// REQUIRES: mutex_ held
std::optional<uint64_t> RecordCurrentLog(uint32_t cf_id, uint64_t log_number);
// Seals the column family's active memtable if everything it holds is
// already readable through the installed Version, so that a following
// RemoveOldMemTables(installed_log_number) call drops it. Returns whether the
// memtable was sealed; in the common case it is not, and this is a no-op.
//
// `installed_log_number` is the column family's log number as of the Version
// most recently installed for it, from
// ReactiveVersionSet::GetInstalledVersionLogNumber(). It gates the seal, and
// the sealed memtable's next log number is at most that value.
//
// REQUIRES: mutex_ held
// REQUIRES: installed_log_number <= cfd->GetLogNumber()
// REQUIRES: cf_id_to_current_log_[cfd->GetID()] is at least as new as the
// newest WAL whose entries the active memtable holds, i.e. any WAL replay for
// this round has run. Too low a value drops a memtable the installed Version
// does not cover; too high only retains it for longer. RecoverLogFiles()
// records both before and after each insert to keep that true; see there.
bool MaybeSealFullyFlushedActiveMemtable(ColumnFamilyData* cfd,
uint64_t installed_log_number,
JobContext* job_context);
// Warns while `installed_log_number` lags behind cfd->GetLogNumber() and the
// column family is holding immutable memtables that RemoveOldMemTables()
// cannot collect as a result. Nothing on a secondary flushes them, so their
// number grows by one per WAL switch until the primary's flushed files become
// readable or the instance is reopened.
//
// Warns only when what it would report has changed, since the condition can
// persist across arbitrarily many catch-up calls.
//
// REQUIRES: mutex_ held
void MaybeWarnAboutRetainedMemtables(ColumnFamilyData* cfd,
uint64_t installed_log_number);
// Drops recovered transactions the primary resolved and flushed.
//
// REQUIRES: mutex_ held
// REQUIRES: every WAL reader for the round opened successfully
// REQUIRES: called before replaying records from the round
void DeleteResolvedRecoveredTransactions();
// Run compaction without installation, the output files will be placed in the
// secondary DB path. The LSM tree won't be changed, the secondary DB is still
// in read-only mode.
@@ -404,19 +474,33 @@ class DBImplSecondary : public DBImpl {
// MANIFEST only. When recover_wal is true, WAL files are also replayed
// (needed by DB::OpenAsSecondary). When false, WAL replay is skipped
// (used by DB::OpenAndCompact which only needs LSM state).
//
// When trust_manifest_recovery is true (only DB::OpenAndCompact), MANIFEST
// recovery trusts the manifest and does not stat/open SST or blob files, so
// only the compaction's (deletion-protected) input files are opened later, on
// demand. Must be false for normal secondary opens.
static Status OpenAsSecondaryImpl(
const DBOptions& db_options, const std::string& dbname,
const std::string& secondary_path,
const std::vector<ColumnFamilyDescriptor>& column_families,
std::vector<ColumnFamilyHandle*>* handles, std::unique_ptr<DB>* dbptr,
bool recover_wal);
bool recover_wal, bool trust_manifest_recovery);
// Cache log readers for each log number, used for continue WAL replay
// after recovery
std::map<uint64_t, std::unique_ptr<LogReaderContainer>> log_readers_;
// Current WAL number replayed for each column family.
std::unordered_map<ColumnFamilyData*, uint64_t> cfd_to_current_log_;
// The newest WAL whose entries each column family's active memtable may
// hold, keyed by column family id. An upper bound, not necessarily a WAL the
// memtable took entries from. Column family ids are never reused, so an entry
// left behind by a dropped column family can never be mistaken for a new
// column family's.
std::unordered_map<uint32_t, uint64_t> cf_id_to_current_log_;
// The installed Version's log number and retained memtable count last
// reported by MaybeWarnAboutRetainedMemtables() for each column family.
std::unordered_map<uint32_t, std::pair<uint64_t, int>>
cf_id_to_retention_warning_;
const std::string secondary_path_;
+12 -1
View File
@@ -185,11 +185,22 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplSecondary::GetImpl)
/*value_found*/ nullptr,
/*key_exists*/ nullptr, /*seq*/ nullptr, &read_cb,
/*is_blob*/ nullptr,
/*do_merge=*/get_impl_options.get_value);
/*do_merge=*/get_impl_options.get_value,
get_impl_options.lazy_columns_same_file_reader);
RecordTick(stats_, MEMTABLE_MISS);
}
{
PERF_TIMER_GUARD(get_post_process_time);
if (get_impl_options.lazy_columns_pin != nullptr && s.ok()) {
// Lazy result (GetEntityLazy): hand back the resolution Version and take
// a SuperVersion pin for the result before the borrowed reference below
// is released, so the result -- and deferred blob reads -- stay valid
// after this call (as an iterator's pin does).
if (get_impl_options.lazy_columns_version != nullptr) {
*get_impl_options.lazy_columns_version = super_version->current;
}
TransferSuperVersionPin(super_version, get_impl_options.lazy_columns_pin);
}
#if defined(WITH_COROUTINES)
CleanupSuperVersion(super_version);
#else
+41 -39
View File
@@ -7,8 +7,7 @@
#include "util/coro_utils.h"
#if defined(USE_COROUTINES) && defined(WITH_COROUTINES)
#include "folly/executors/IOExecutor.h"
#include "folly/io/async/EventBase.h"
#include "util/coro_stats_util.h"
#endif // USE_COROUTINES && WITH_COROUTINES
#if defined(WITHOUT_COROUTINES) || \
@@ -26,6 +25,10 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
DEFINE_SYNC_AND_ASYNC(Status, DBImpl::Get)
(const ReadOptions& _read_options, ColumnFamilyHandle* column_family,
const Slice& key, PinnableSlice* value, std::string* timestamp) {
#ifdef WITH_COROUTINES
INSTALL_COROUTINE_STATS_CONTEXT_SCOPE(
immutable_db_options_.fs->GetReadExecutor(), immutable_db_options_.env);
#endif
assert(value != nullptr);
value->Reset();
@@ -41,19 +44,8 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::Get)
read_options.io_activity = Env::IOActivity::kGet;
}
#ifdef WITH_COROUTINES
auto* read_executor =
immutable_db_options_.env->GetFileSystem()->GetReadExecutor();
if (read_executor != nullptr) {
auto* read_event_base = read_executor->getEventBase();
assert(read_event_base != nullptr);
Status s = co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
folly::Executor::getKeepAliveToken(read_event_base),
GetImplCoroutine(read_options, column_family, key, value, timestamp)));
co_return s;
}
#endif
CO_RETURN GetImpl(read_options, column_family, key, value, timestamp);
CO_RETURN CO_AWAIT(GetImpl, read_options, column_family, key, value,
timestamp);
}
DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
@@ -250,8 +242,9 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
done = true;
PostprocessDirectWriteValueRead(
read_options, key, timestamp, resolve_direct_write_value,
sv->current, cfd, get_impl_options.value, get_impl_options.columns,
&s, &is_blob_index, get_impl_options.value_found);
memtable_blob_fetcher_ptr, get_impl_options.value,
get_impl_options.columns, &s, &is_blob_index,
get_impl_options.value_found);
RecordTick(stats_, MEMTABLE_HIT);
} else if ((s.ok() || s.IsMergeInProgress()) &&
@@ -266,8 +259,9 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
done = true;
PostprocessDirectWriteValueRead(
read_options, key, timestamp, resolve_direct_write_value,
sv->current, cfd, get_impl_options.value, get_impl_options.columns,
&s, &is_blob_index, get_impl_options.value_found);
memtable_blob_fetcher_ptr, get_impl_options.value,
get_impl_options.columns, &s, &is_blob_index,
get_impl_options.value_found);
RecordTick(stats_, MEMTABLE_HIT);
}
@@ -307,14 +301,16 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
nullptr, nullptr,
get_impl_options.get_value ? get_impl_options.callback : nullptr,
get_impl_options.get_value ? is_blob_ptr : nullptr,
get_impl_options.get_value);
get_impl_options.get_value,
get_impl_options.lazy_columns_same_file_reader);
if (get_impl_options.get_value && resolve_direct_write_value) {
assert(memtable_blob_fetcher_ptr != nullptr);
std::string blob_lookup_key_storage;
MaybeResolveDirectWriteValue(
read_options,
GetBlobLookupUserKey(key, timestamp, &blob_lookup_key_storage),
resolve_direct_write_value, sv->current, cfd, get_impl_options.value,
get_impl_options.columns, &s, &is_blob_index,
resolve_direct_write_value, *memtable_blob_fetcher_ptr,
get_impl_options.value, get_impl_options.columns, &s, &is_blob_index,
get_impl_options.value_found);
}
RecordTick(stats_, MEMTABLE_MISS);
@@ -407,6 +403,19 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
PERF_COUNTER_ADD(get_read_bytes, size);
}
if (get_impl_options.lazy_columns_pin != nullptr && s.ok()) {
// Lazy result (GetEntityLazy): the entity's blob references were left
// unresolved. Hand back the Version they must be resolved against and
// transfer an extra SuperVersion reference into the result's pin, so the
// result -- and thus deferred blob reads -- stay valid after this call
// returns (as an iterator's SuperVersion pin does). The reference is
// released when the pin's cleanup runs (result destruction); the
// borrowed reference for this call is dropped by the sv_cleanup Defer.
if (get_impl_options.lazy_columns_version != nullptr) {
*get_impl_options.lazy_columns_version = sv->current;
}
TransferSuperVersionPin(sv, get_impl_options.lazy_columns_pin);
}
RecordInHistogram(stats_, BYTES_PER_READ, size);
}
CO_RETURN s;
@@ -574,12 +583,13 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::MultiGetImpl)
key->is_blob_index = false;
*key->s = Status::Aborted();
} else {
assert(memtable_blob_fetcher_ptr != nullptr);
std::string blob_lookup_key_storage;
MaybeResolveDirectWriteValue(
read_options,
GetBlobLookupUserKey(*key->key, key->timestamp,
&blob_lookup_key_storage),
/*resolve_direct_write_value=*/true, super_version->current, cfd,
/*resolve_direct_write_value=*/true, *memtable_blob_fetcher_ptr,
key->value, key->columns, key->s, &key->is_blob_index);
}
}
@@ -791,6 +801,10 @@ DEFINE_SYNC_AND_ASYNC(void, DBImpl::MultiGet)
(const ReadOptions& _read_options, const size_t num_keys,
ColumnFamilyHandle** column_families, const Slice* keys, PinnableSlice* values,
std::string* timestamps, Status* statuses, const bool sorted_input) {
#ifdef WITH_COROUTINES
INSTALL_COROUTINE_STATS_CONTEXT_SCOPE(
immutable_db_options_.fs->GetReadExecutor(), immutable_db_options_.env);
#endif
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
_read_options.io_activity != Env::IOActivity::kMultiGet) {
Status s = Status::InvalidArgument(
@@ -807,22 +821,10 @@ DEFINE_SYNC_AND_ASYNC(void, DBImpl::MultiGet)
if (read_options.io_activity == Env::IOActivity::kUnknown) {
read_options.io_activity = Env::IOActivity::kMultiGet;
}
#ifdef WITH_COROUTINES
auto* read_executor =
immutable_db_options_.env->GetFileSystem()->GetReadExecutor();
if (read_executor != nullptr) {
auto* read_event_base = read_executor->getEventBase();
assert(read_event_base != nullptr);
co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
folly::Executor::getKeepAliveToken(read_event_base),
MultiGetCommonCoroutine(
read_options, num_keys, column_families, keys, values,
/* columns */ nullptr, timestamps, statuses, sorted_input)));
co_return;
}
#endif
MultiGetCommon(read_options, num_keys, column_families, keys, values,
/* columns */ nullptr, timestamps, statuses, sorted_input);
CO_AWAIT(MultiGetCommon, read_options, num_keys, column_families, keys,
values,
/* columns */ nullptr, timestamps, statuses, sorted_input);
CO_RETURN;
}
} // namespace ROCKSDB_NAMESPACE
+4 -3
View File
@@ -26,6 +26,7 @@
#include "logging/logging.h"
#include "memory/arena.h"
#include "monitoring/perf_context_imp.h"
#include "options/options_helper.h"
#include "port/likely.h"
#include "rocksdb/env.h"
#include "rocksdb/io_dispatcher.h"
@@ -78,8 +79,8 @@ DBIter::DBIter(Env* _env, const ReadOptions& read_options,
merge_operator_(ioptions.merge_operator.get()),
iter_(iter),
// Enable the direct-write blob fallback only when this CF has a
// write-path partition manager; otherwise the fetcher stays on the plain
// Version::GetBlob fast path.
// write-path partition manager; otherwise the fetcher stays on the
// version-backed path.
blob_state_(version, read_options, cfd ? cfd->blob_file_cache() : nullptr,
cfd != nullptr && cfd->blob_partition_manager() != nullptr),
read_callback_(read_callback),
@@ -122,7 +123,7 @@ DBIter::DBIter(Env* _env, const ReadOptions& read_options,
// prefix_same_as_start do not guarantee complete scans, so conversion
// must stay disabled for the iterator lifetime.
min_tombstones_for_range_conversion_(
active_mem != nullptr && !read_options.table_filter &&
active_mem != nullptr && !HasTableFilter(read_options) &&
(expect_total_order_inner_iter_ || prefix_same_as_start_) &&
HasFullTimestampVisibility(read_options)
? mutable_cf_options.min_tombstones_for_range_conversion
+18 -16
View File
@@ -3230,16 +3230,18 @@ TEST_P(DBIteratorTest, TableFilter) {
{
std::set<uint64_t> unseen{1, 2, 3};
ReadOptions opts;
opts.table_filter = [&](const TableProperties& props) {
auto it = unseen.find(props.num_entries);
if (it == unseen.end()) {
ADD_FAILURE() << "saw table properties with an unexpected "
<< props.num_entries << " entries";
} else {
unseen.erase(it);
}
return true;
};
std::function<bool(const TableProperties&)> filter =
[&](const TableProperties& props) {
auto it = unseen.find(props.num_entries);
if (it == unseen.end()) {
ADD_FAILURE() << "saw table properties with an unexpected "
<< props.num_entries << " entries";
} else {
unseen.erase(it);
}
return true;
};
opts.table_filter = &filter;
auto iter = NewIterator(opts);
iter->SeekToFirst();
ASSERT_EQ(IterStatus(iter), "a->1");
@@ -3264,9 +3266,9 @@ TEST_P(DBIteratorTest, TableFilter) {
// during iteration.
{
ReadOptions opts;
opts.table_filter = [](const TableProperties& props) {
return props.num_entries != 2;
};
std::function<bool(const TableProperties&)> filter =
[](const TableProperties& props) { return props.num_entries != 2; };
opts.table_filter = &filter;
auto iter = NewIterator(opts);
iter->SeekToFirst();
ASSERT_EQ(IterStatus(iter), "a->1");
@@ -7151,9 +7153,9 @@ TEST_P(ReadPathRangeTombstoneTest, TableFilterNotAllowed) {
{
ReadOptions filtered_ro;
filtered_ro.table_filter = [](const TableProperties& props) {
return props.num_entries != 2;
};
std::function<bool(const TableProperties&)> filter =
[](const TableProperties& props) { return props.num_entries != 2; };
filtered_ro.table_filter = &filter;
// Hiding the two-delete SST would otherwise leave this iterator with a
// partial SST view plus the previously converted memtable tombstone,
// allowing hidden SST state to affect the filtered read result.
+104 -28
View File
@@ -18,14 +18,12 @@
namespace ROCKSDB_NAMESPACE {
class DBTestXactLogIterator : public DBTestBase {
class DBWalIteratorTest : public DBTestBase {
public:
DBTestXactLogIterator()
: DBTestBase("db_log_iter_test", /*env_do_fsync=*/true) {}
DBWalIteratorTest() : DBTestBase("db_log_iter_test", /*env_do_fsync=*/true) {}
std::unique_ptr<TransactionLogIterator> OpenTransactionLogIter(
const SequenceNumber seq) {
std::unique_ptr<TransactionLogIterator> iter;
std::unique_ptr<WalIterator> OpenWalIter(const SequenceNumber seq) {
std::unique_ptr<WalIterator> iter;
Status status = dbfull()->GetUpdatesSince(seq, &iter);
EXPECT_OK(status);
EXPECT_TRUE(iter->Valid());
@@ -34,8 +32,8 @@ class DBTestXactLogIterator : public DBTestBase {
};
namespace {
SequenceNumber ReadRecords(std::unique_ptr<TransactionLogIterator>& iter,
int& count, bool expect_ok = true) {
SequenceNumber ReadRecords(std::unique_ptr<WalIterator>& iter, int& count,
bool expect_ok = true) {
count = 0;
SequenceNumber lastSequence = 0;
BatchResult res;
@@ -56,14 +54,14 @@ SequenceNumber ReadRecords(std::unique_ptr<TransactionLogIterator>& iter,
}
void ExpectRecords(const int expected_no_records,
std::unique_ptr<TransactionLogIterator>& iter) {
std::unique_ptr<WalIterator>& iter) {
int num_records;
ReadRecords(iter, num_records);
ASSERT_EQ(num_records, expected_no_records);
}
} // anonymous namespace
TEST_F(DBTestXactLogIterator, TransactionLogIterator) {
TEST_F(DBWalIteratorTest, Basic) {
do {
Options options = OptionsForLogIterTest();
DestroyAndReopen(options);
@@ -73,7 +71,7 @@ TEST_F(DBTestXactLogIterator, TransactionLogIterator) {
ASSERT_OK(Put(1, "key2", DummyString(1024)));
ASSERT_EQ(dbfull()->GetLatestSequenceNumber(), 3U);
{
auto iter = OpenTransactionLogIter(0);
auto iter = OpenWalIter(0);
ExpectRecords(3, iter);
}
ReopenWithColumnFamilies({"default", "pikachu"}, options);
@@ -84,14 +82,14 @@ TEST_F(DBTestXactLogIterator, TransactionLogIterator) {
ASSERT_OK(Put(0, "key6", DummyString(1024)));
}
{
auto iter = OpenTransactionLogIter(0);
auto iter = OpenWalIter(0);
ExpectRecords(6, iter);
}
} while (ChangeCompactOptions());
}
#ifndef NDEBUG // sync point is not included with DNDEBUG build
TEST_F(DBTestXactLogIterator, TransactionLogIteratorRace) {
TEST_F(DBWalIteratorTest, Race) {
static const int LOG_ITERATOR_RACE_TEST_COUNT = 2;
static const char* sync_points[LOG_ITERATOR_RACE_TEST_COUNT][4] = {
{"WalManager::GetSortedWalFiles:1", "WalManager::PurgeObsoleteFiles:1",
@@ -123,7 +121,7 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorRace) {
ASSERT_OK(dbfull()->FlushWAL(false));
{
auto iter = OpenTransactionLogIter(0);
auto iter = OpenWalIter(0);
ExpectRecords(4, iter);
}
@@ -140,14 +138,14 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorRace) {
ASSERT_OK(dbfull()->FlushWAL(false));
{
// this iter would miss "key4" if not fixed
auto iter = OpenTransactionLogIter(0);
auto iter = OpenWalIter(0);
ExpectRecords(5, iter);
}
} while (ChangeCompactOptions());
}
}
TEST_F(DBTestXactLogIterator, TransactionLogIteratorCheckWhenArchive) {
TEST_F(DBWalIteratorTest, CheckWhenArchive) {
RelaxedAtomic<bool> callback_hit{};
do {
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearTrace();
@@ -173,7 +171,7 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorCheckWhenArchive) {
callback_hit.StoreRelaxed(false);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"WalManager::PurgeObsoleteFiles:1", [&](void*) {
auto iter = OpenTransactionLogIter(0);
auto iter = OpenWalIter(0);
ExpectRecords(4, iter);
callback_hit.StoreRelaxed(true);
});
@@ -192,12 +190,12 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorCheckWhenArchive) {
}
#endif
TEST_F(DBTestXactLogIterator, TransactionLogIteratorStallAtLastRecord) {
TEST_F(DBWalIteratorTest, StallAtLastRecord) {
do {
Options options = OptionsForLogIterTest();
DestroyAndReopen(options);
ASSERT_OK(Put("key1", DummyString(1024)));
auto iter = OpenTransactionLogIter(0);
auto iter = OpenWalIter(0);
ASSERT_OK(iter->status());
ASSERT_TRUE(iter->Valid());
iter->Next();
@@ -210,7 +208,7 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorStallAtLastRecord) {
} while (ChangeCompactOptions());
}
TEST_F(DBTestXactLogIterator, TransactionLogIteratorCheckAfterRestart) {
TEST_F(DBWalIteratorTest, CheckAfterRestart) {
do {
Options options = OptionsForLogIterTest();
DestroyAndReopen(options);
@@ -218,12 +216,12 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorCheckAfterRestart) {
ASSERT_OK(Put("key2", DummyString(1023)));
ASSERT_OK(dbfull()->Flush(FlushOptions()));
Reopen(options);
auto iter = OpenTransactionLogIter(0);
auto iter = OpenWalIter(0);
ExpectRecords(2, iter);
} while (ChangeCompactOptions());
}
TEST_F(DBTestXactLogIterator, TransactionLogIteratorCorruptedLog) {
TEST_F(DBWalIteratorTest, CorruptedLog) {
do {
Options options = OptionsForLogIterTest();
DestroyAndReopen(options);
@@ -254,18 +252,18 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorCorruptedLog) {
// Try to read from the beginning. Should stop before the gap and read less
// than 1025 entries
auto iter = OpenTransactionLogIter(0);
auto iter = OpenWalIter(0);
int count = 0;
SequenceNumber last_sequence_read = ReadRecords(iter, count, false);
ASSERT_LT(last_sequence_read, 1025U);
// Try to read past the gap, should be able to seek to key1025
auto iter2 = OpenTransactionLogIter(last_sequence_read + 1);
auto iter2 = OpenWalIter(last_sequence_read + 1);
ExpectRecords(1, iter2);
} while (ChangeCompactOptions());
}
TEST_F(DBTestXactLogIterator, TransactionLogIteratorBatchOperations) {
TEST_F(DBWalIteratorTest, BatchOperations) {
do {
Options options = OptionsForLogIterTest();
DestroyAndReopen(options);
@@ -280,12 +278,12 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorBatchOperations) {
ASSERT_OK(Flush(0));
ReopenWithColumnFamilies({"default", "pikachu"}, options);
ASSERT_OK(Put(1, "key4", DummyString(1024)));
auto iter = OpenTransactionLogIter(3);
auto iter = OpenWalIter(3);
ExpectRecords(2, iter);
} while (ChangeCompactOptions());
}
TEST_F(DBTestXactLogIterator, TransactionLogIteratorBlobs) {
TEST_F(DBWalIteratorTest, Blobs) {
Options options = OptionsForLogIterTest();
DestroyAndReopen(options);
CreateAndReopenWithCF({"pikachu"}, options);
@@ -301,7 +299,7 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorBlobs) {
ReopenWithColumnFamilies({"default", "pikachu"}, options);
}
auto res = OpenTransactionLogIter(0)->GetBatch();
auto res = OpenWalIter(0)->GetBatch();
struct Handler : public WriteBatch::Handler {
std::string seen;
Status PutCF(uint32_t cf, const Slice& key, const Slice& value) override {
@@ -332,6 +330,84 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorBlobs) {
"Delete(0, key2)",
handler.seen);
}
// The tests below pin the semantics documented on WalIterator and
// DB::GetUpdatesSince, including the sharp edges, so that changing any of
// them is a deliberate and visible decision rather than an accident.
// A caught-up iterator is usable, not finished: Next() is how a consumer
// tails a DB.
TEST_F(DBWalIteratorTest, CaughtUpThenResumes) {
Options options = OptionsForLogIterTest();
DestroyAndReopen(options);
ASSERT_OK(Put("key1", DummyString(128)));
auto iter = OpenWalIter(0);
ASSERT_TRUE(iter->Valid());
iter->Next();
// Caught up: !Valid(), but status() is OK and the iterator is not spent.
ASSERT_TRUE(!iter->Valid());
ASSERT_OK(iter->status());
// A later write to the same WAL is picked up by calling Next() again,
// without rebuilding the iterator.
ASSERT_OK(Put("key2", DummyString(128)));
iter->Next();
ASSERT_TRUE(iter->Valid()) << iter->status().ToString();
ASSERT_OK(iter->status());
ASSERT_EQ(2U, iter->GetBatch().sequence);
}
// Writes that bypass the WAL still consume sequence numbers, so they leave
// holes that this API surfaces as the end of the run.
TEST_F(DBWalIteratorTest, GapFromDisableWAL) {
Options options = OptionsForLogIterTest();
DestroyAndReopen(options);
WriteOptions no_wal;
no_wal.disableWAL = true;
ASSERT_OK(Put("key1", DummyString(128))); // seq 1, in the WAL
ASSERT_OK(Put("key2", DummyString(128), no_wal)); // seq 2, not in the WAL
ASSERT_OK(Put("key3", DummyString(128))); // seq 3, in the WAL
auto iter = OpenWalIter(0);
ASSERT_TRUE(iter->Valid());
ASSERT_EQ(1U, iter->GetBatch().sequence);
// The run stops at the hole rather than silently jumping to seq 3.
iter->Next();
ASSERT_TRUE(!iter->Valid());
ASSERT_TRUE(iter->status().IsNotFound()) << iter->status().ToString();
// And the iterator is spent: further Next() calls do nothing and the
// status never changes.
iter->Next();
iter->Next();
ASSERT_TRUE(!iter->Valid());
ASSERT_TRUE(iter->status().IsNotFound());
}
// GetUpdatesSince is permissive about its starting point: if the requested
// sequence number is not in the WAL it silently starts later and still
// reports OK. This is the path by which a consumer recovering from a spent
// iterator can lose data without noticing.
TEST_F(DBWalIteratorTest, SilentlySkipsUnavailableStart) {
Options options = OptionsForLogIterTest();
DestroyAndReopen(options);
WriteOptions no_wal;
no_wal.disableWAL = true;
ASSERT_OK(Put("key1", DummyString(128), no_wal)); // seq 1, not in the WAL
ASSERT_OK(Put("key2", DummyString(128))); // seq 2, in the WAL
std::unique_ptr<WalIterator> iter;
ASSERT_OK(dbfull()->GetUpdatesSince(1, &iter));
ASSERT_OK(iter->status());
ASSERT_TRUE(iter->Valid());
// Asked for seq 1, silently given seq 2, with no error anywhere.
ASSERT_EQ(2U, iter->GetBatch().sequence);
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+19
View File
@@ -566,6 +566,25 @@ TEST_F(DBOptionsTest, SetOptionsAndReopen) {
ASSERT_OK(TryReopen(options));
}
TEST_F(DBOptionsTest, SetBlobFileWritableFileMaxBufferSize) {
constexpr uint64_t kBlobWriterBufferSize = 128 * 1024;
Options options;
options.env = env_;
options.create_if_missing = true;
options.enable_blob_files = true;
options.min_blob_size = 0;
Reopen(options);
ASSERT_EQ(db_->GetOptions().blob_file_writable_file_max_buffer_size, 0U);
ASSERT_OK(db_->SetOptions({{"blob_file_writable_file_max_buffer_size",
std::to_string(kBlobWriterBufferSize)}}));
ASSERT_EQ(db_->GetOptions().blob_file_writable_file_max_buffer_size,
kBlobWriterBufferSize);
}
TEST_F(DBOptionsTest, EnableAutoCompactionAndTriggerStall) {
const std::string kValue(1024, 'v');
for (int method_type = 0; method_type < 2; method_type++) {
+54
View File
@@ -1148,6 +1148,60 @@ TEST_F(DBPropertiesTest, EstimatePendingCompBytes) {
ASSERT_EQ(int_num, 0U);
}
TEST_F(DBPropertiesTest, NumUnscheduledCompactions) {
if (env_ == nullptr) {
ADD_FAILURE() << "Missing test environment";
return;
}
auto& env = *env_;
env.SetBackgroundThreads(1, Env::HIGH);
env.SetBackgroundThreads(1, Env::LOW);
Options options = CurrentOptions();
WriteOptions writeOpt = WriteOptions();
writeOpt.disableWAL = true;
options.compaction_style = kCompactionStyleLevel;
options.level0_file_num_compaction_trigger = 2;
options.max_background_compactions = 1;
options.max_background_flushes = 1;
options.write_buffer_size = 1000000;
DestroyAndReopen(options);
CreateAndReopenWithCF({"one"}, options);
uint64_t int_num;
ASSERT_TRUE(dbfull()->GetIntProperty(
DB::Properties::kNumUnscheduledCompactions, &int_num));
ASSERT_EQ(int_num, 0U);
// Keep one scheduled compaction from completing.
test::SleepingBackgroundTask sleeping_task_low;
env.Schedule(&test::SleepingBackgroundTask::DoSleepTask, &sleeping_task_low,
Env::Priority::LOW);
sleeping_task_low.WaitUntilSleeping();
std::string big_value(1000000 * 2, 'x');
for (int cf = 0; cf < 2; cf++) {
for (int i = 0; i < 4; i++) {
ASSERT_OK(dbfull()->Put(writeOpt, handles_[cf], "k" + std::to_string(i),
big_value));
ASSERT_OK(Flush(cf));
}
}
ASSERT_TRUE(dbfull()->GetIntProperty(
DB::Properties::kNumUnscheduledCompactions, &int_num));
ASSERT_GT(int_num, 0U);
sleeping_task_low.WakeUp();
sleeping_task_low.WaitUntilDone();
ASSERT_OK(dbfull()->TEST_WaitForCompact());
ASSERT_TRUE(dbfull()->GetIntProperty(
DB::Properties::kNumUnscheduledCompactions, &int_num));
ASSERT_EQ(int_num, 0U);
}
TEST_F(DBPropertiesTest, EstimateCompressionRatio) {
if (!Snappy_Supported()) {
return;
+736 -12
View File
@@ -13,11 +13,14 @@
#include "db/db_with_timestamp_test_util.h"
#include "db/wide/wide_column_test_util.h"
#include "db/write_batch_internal.h"
#include "env/composite_env_wrapper.h"
#include "file/filename.h"
#include "port/stack_trace.h"
#include "rocksdb/utilities/transaction_db.h"
#include "test_util/sync_point.h"
#include "test_util/testutil.h"
#include "util/defer.h"
#include "utilities/fault_injection_fs.h"
#include "utilities/merge_operators/string_append/stringappend2.h"
namespace ROCKSDB_NAMESPACE {
@@ -63,6 +66,149 @@ class DBSecondaryTestBase : public DBBasicTestWithTimestampBase {
db_secondary_.reset();
}
// Reopens the primary with `options`, opens a secondary on
// `secondary_options` and catches it up after writing "foo"="v1" to the
// primary's WAL without flushing, so that the value is readable only from the
// secondary's WAL-replayed active memtable. An earlier flush makes the column
// family's log number equal the WAL that the secondary then replays, which is
// the boundary case for deciding whether that WAL has been flushed.
void OpenSecondaryOnWalOnlyPut(const Options& options,
const Options& secondary_options) {
Reopen(options);
ASSERT_OK(Put("seed", "s"));
ASSERT_OK(Flush());
OpenSecondary(secondary_options);
ASSERT_OK(Put("foo", "v1"));
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
VerifySecondaryValue("foo", "v1");
}
// Destroys the primary and recreates it as a TransactionDB, so that its WAL
// carries 2PC markers. `db_` takes ownership; `*txn_db` is a borrowed view of
// it for the transaction API. Call under ASSERT_NO_FATAL_FAILURE: on a failed
// open `*txn_db` is left null.
void RecreatePrimaryAsTransactionDB(const Options& options,
TransactionDB** txn_db) {
Close();
Destroy(options, true);
*txn_db = nullptr;
TransactionDBOptions txn_db_opts;
ASSERT_OK(TransactionDB::Open(options, txn_db_opts, dbname_, txn_db));
ASSERT_NE(nullptr, *txn_db);
db_.reset(*txn_db);
}
// Opens a secondary that can replay the primary's 2PC markers.
// OpenAsSecondary() does not force allow_2pc the way TransactionDB::Open()
// does for the primary, and without it MarkBeginPrepare() rejects the
// prepared section with NotSupported.
void OpenSecondaryFor2PC(const Options& options) {
Options secondary_options = options;
secondary_options.max_open_files = -1;
secondary_options.allow_2pc = true;
OpenSecondary(secondary_options);
}
// Prepares a transaction named `name` on the primary and catches the
// secondary up to it, so that `*txn` awaits resolution and the secondary
// holds its prepared batch as a recovered transaction. Call under
// ASSERT_NO_FATAL_FAILURE.
void PrepareTransactionAndCatchUp(TransactionDB* txn_db,
const std::string& name,
const std::string& key,
const std::string& value,
std::unique_ptr<Transaction>* txn) {
txn->reset(txn_db->BeginTransaction(WriteOptions(), TransactionOptions()));
ASSERT_NE(nullptr, txn->get());
ASSERT_OK((*txn)->SetName(name));
ASSERT_OK((*txn)->Put(key, value));
ASSERT_OK((*txn)->Prepare());
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
ASSERT_NE(nullptr, db_secondary_full()->GetRecoveredTransaction(name));
}
// Commits `*txn` on the primary and flushes, then purges the WAL holding its
// commit marker, so that no later round can replay that marker. Reports the
// purged WAL's log number in `*commit_log_number`. Call under
// ASSERT_NO_FATAL_FAILURE.
void CommitAndPurgeMarkerWal(TransactionDB* txn_db,
std::unique_ptr<Transaction>* txn,
uint64_t* commit_log_number) {
DBImpl* primary = static_cast_with_check<DBImpl>(txn_db->GetRootDB());
ASSERT_OK((*txn)->Commit());
txn->reset();
*commit_log_number = primary->TEST_GetCurrentLogNumber();
ASSERT_OK(db_->Flush(FlushOptions()));
ASSERT_OK(db_->Put(WriteOptions(), "spacer", "s"));
ASSERT_OK(db_->Flush(FlushOptions()));
primary->TEST_DeleteObsoleteFiles();
ASSERT_OK(primary->TEST_WaitForPurge());
ASSERT_TRUE(env_->FileExists(LogFileName(dbname_, *commit_log_number))
.IsNotFound());
}
// Checks that the secondary reads `expected` for `key` through both Get() and
// an iterator. The two paths order the active memtable against the file set
// differently, so they can disagree on a stale memtable entry.
void VerifySecondaryValue(const std::string& key,
const std::string& expected) {
VerifySecondaryValue(db_secondary_->DefaultColumnFamily(), key, expected);
}
void VerifySecondaryValue(ColumnFamilyHandle* cfh, const std::string& key,
const std::string& expected) {
SCOPED_TRACE("key=" + key);
ReadOptions ropts;
std::string value;
ASSERT_OK(db_secondary_->Get(ropts, cfh, key, &value));
ASSERT_EQ(expected, value);
std::unique_ptr<Iterator> iter(db_secondary_->NewIterator(ropts, cfh));
iter->Seek(key);
ASSERT_OK(iter->status());
ASSERT_TRUE(iter->Valid());
ASSERT_EQ(key, iter->key().ToString());
ASSERT_EQ(expected, iter->value().ToString());
}
// Returns the path of the primary's newest table file for `cf_name`.
std::string GetNewestTableFilePath(const std::string& cf_name) {
std::vector<LiveFileMetaData> live_files;
db_->GetLiveFilesMetaData(&live_files);
std::string path;
uint64_t newest_file_number = 0;
for (const auto& md : live_files) {
if (md.column_family_name == cf_name &&
md.file_number > newest_file_number) {
newest_file_number = md.file_number;
path = md.directory + "/" + md.relative_filename;
}
}
EXPECT_FALSE(path.empty());
return path;
}
// Moves `path` aside so that opening it fails, and returns where it went.
// Every DB holding the file open must be closed first, because not every
// platform allows renaming an open file. The caller must move it back before
// the fixture is destroyed - DestroyDB() does not recognize the new name, so
// it would otherwise be left behind for the next test sharing `dbname_` - and
// must do so with a Defer, since a failing assertion leaves the test body
// early.
std::string MoveFileAside(const std::string& path) {
const std::string aside_path = path + ".aside";
EXPECT_OK(env_->RenameFile(path, aside_path));
return aside_path;
}
// Returns the secondary's ColumnFamilyData behind `cfh`, for the tests that
// assert on memtable state directly rather than through a read.
static ColumnFamilyData* GetSecondaryCfd(ColumnFamilyHandle* cfh) {
return static_cast_with_check<ColumnFamilyHandleImpl>(cfh)->cfd();
}
DBImplSecondary* db_secondary_full() {
return static_cast<DBImplSecondary*>(db_secondary_.get());
}
@@ -749,8 +895,7 @@ TEST_F(DBSecondaryTest, OptionsOverrideTest) {
"blob_compaction_readahead_size=4194304;"
"some_invalid_option=ignore_me;"
"env=this_should_not_fail;"
"max_open_files=100;", // this should be always overriden as
// -1 in remote compaction
"max_open_files=100;",
&override_options.options_map));
bool verified = false;
@@ -761,7 +906,7 @@ TEST_F(DBSecondaryTest, OptionsOverrideTest) {
auto secondary_db_options = secondary_db->GetOptions();
// DBOption
ASSERT_EQ(secondary_db_options.compaction_readahead_size, 8388608);
ASSERT_EQ(secondary_db_options.max_open_files, -1);
ASSERT_EQ(secondary_db_options.max_open_files, 100);
// CFOption
ASSERT_EQ(secondary_db_options.blob_compaction_readahead_size, 4194304);
verified = true;
@@ -998,6 +1143,593 @@ TEST_F(DBSecondaryTest, SecondaryTailingBug_ISSUE_8467) {
}
}
// Fixture for the catch-up rounds that fail partway through. The
// fault-injecting Env is a member, and the destructor closes the secondary
// before that member is destroyed, so the Env outlives the DB opened on it even
// when an ASSERT returns from a test early.
class DBSecondaryCatchUpFaultTest : public DBSecondaryTestBase {
public:
DBSecondaryCatchUpFaultTest()
: DBSecondaryTestBase("db_secondary_catch_up_fault_test"),
fault_fs_(
std::make_shared<FaultInjectionTestFS>(env_->GetFileSystem())),
fault_env_(new CompositeEnvWrapper(env_, fault_fs_)) {}
~DBSecondaryCatchUpFaultTest() override {
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
fault_fs_->SetFilesystemActive(true);
CloseSecondary();
}
protected:
std::shared_ptr<FaultInjectionTestFS> fault_fs_;
std::unique_ptr<Env> fault_env_;
};
// A catch-up round can fail after ReadAndApply() has already advanced the
// column family's log number, here because WAL discovery fails. Later rounds
// see no new MANIFEST record for that column family, so they must still
// reconcile its active memtable against the installed Version rather than
// considering only the column families that changed in the round.
TEST_F(DBSecondaryCatchUpFaultTest, ReconcilesMemtableAfterFailedRound) {
Options options;
options.env = env_;
options.disable_auto_compactions = true;
Options secondary_options;
secondary_options.env = fault_env_.get();
secondary_options.max_open_files = -1;
OpenSecondaryOnWalOnlyPut(options, secondary_options);
ASSERT_OK(Put("foo", "v2"));
ASSERT_OK(Flush());
// The round that would have reconciled the memtable fails right after the
// MANIFEST tail has been applied, which is where the log number advances.
SyncPoint::GetInstance()->SetCallBack(
"DBImplSecondary::FindAndRecoverLogFiles:Begin", [this](void* /*arg*/) {
fault_fs_->SetFilesystemActive(
false, IOStatus::IOError("injected WAL discovery failure"));
});
SyncPoint::GetInstance()->EnableProcessing();
ASSERT_NOK(db_secondary_->TryCatchUpWithPrimary());
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
fault_fs_->SetFilesystemActive(true);
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
VerifySecondaryValue("foo", "v2");
}
// A round whose WAL discovery reports a purged path replays nothing, yet
// reports success. A marker still eligible for replay has not had its chance to
// resolve its prepared section, so that round must drop no recovered
// transaction, however far the watermark has moved past its prepare.
TEST_F(DBSecondaryCatchUpFaultTest,
KeepsRecoveredTransactionAfterUnreplayedRound) {
Options options = CurrentOptions();
options.disable_auto_compactions = true;
TransactionDB* txn_db = nullptr;
ASSERT_NO_FATAL_FAILURE(RecreatePrimaryAsTransactionDB(options, &txn_db));
Options secondary_options = options;
secondary_options.env = fault_env_.get();
OpenSecondaryFor2PC(secondary_options);
std::unique_ptr<Transaction> txn;
ASSERT_NO_FATAL_FAILURE(
PrepareTransactionAndCatchUp(txn_db, "t4", "k3", "v3", &txn));
// Leave the transaction resolved and its marker's WAL purged, so any round
// that replayed every WAL it found would drop it.
uint64_t commit_log_number = 0;
ASSERT_NO_FATAL_FAILURE(
CommitAndPurgeMarkerWal(txn_db, &txn, &commit_log_number));
// Discovery fails the way a WAL purged mid-round fails, which the round
// reports as success after replaying nothing.
SyncPoint::GetInstance()->SetCallBack(
"DBImplSecondary::FindAndRecoverLogFiles:Begin", [this](void* /*arg*/) {
fault_fs_->SetFilesystemActive(
false, IOStatus::PathNotFound("injected purged WAL"));
});
SyncPoint::GetInstance()->EnableProcessing();
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
fault_fs_->SetFilesystemActive(true);
ASSERT_GT(db_secondary_full()->GetVersionSet()->min_log_number_to_keep(),
commit_log_number);
ASSERT_NE(nullptr, db_secondary_full()->GetRecoveredTransaction("t4"));
// The next round replays every WAL it finds, so it may drop the transaction.
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
ASSERT_EQ(nullptr, db_secondary_full()->GetRecoveredTransaction("t4"));
}
// The column family's log number advances as soon as the flush record is read,
// even when the flushed file cannot be opened and no Version reflecting it can
// be installed. The active memtable then holds the only readable copy of what
// was flushed, so dropping it would turn a stale read into data loss.
TEST_F(DBSecondaryTest, KeepsMemtableWhenFlushedFileIsMissing) {
Options options;
options.env = env_;
options.disable_auto_compactions = true;
Options secondary_options;
secondary_options.env = env_;
secondary_options.max_open_files = -1;
OpenSecondaryOnWalOnlyPut(options, secondary_options);
// The primary flushes "v2", then its file goes away before the secondary
// reads the MANIFEST record that adds it.
ASSERT_OK(Put("foo", "v2"));
ASSERT_OK(Flush());
const std::string table_file =
GetNewestTableFilePath(kDefaultColumnFamilyName);
Close();
const std::string aside_file = MoveFileAside(table_file);
Defer restore_file(
[&] { EXPECT_OK(env_->RenameFile(aside_file, table_file)); });
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
// "v1" is stale but the newest value readable here: "v2"'s file is absent
// from the installed Version and its WAL is obsolete. Reaching "v2" needs a
// reopen. What must not happen is the key disappearing.
VerifySecondaryValue("foo", "v1");
}
// Same hazard reached without any missing file of the column family's own: an
// atomic flush whose group is incomplete parks the new Version of every column
// family in the group, including those whose files are all readable, while
// their log numbers have already advanced.
TEST_F(DBSecondaryTest, KeepsMemtableWhenAtomicFlushSiblingFileIsMissing) {
Options options;
options.env = env_;
options.disable_auto_compactions = true;
options.atomic_flush = true;
CreateAndReopenWithCF({"cf1"}, options);
Options secondary_options;
secondary_options.env = env_;
secondary_options.max_open_files = -1;
secondary_options.atomic_flush = true;
OpenSecondaryWithColumnFamilies({"cf1"}, secondary_options);
ASSERT_EQ(2, handles_secondary_.size());
ASSERT_OK(Put(0, "foo", "v1"));
ASSERT_OK(Put(1, "bar", "w1"));
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
VerifySecondaryValue(handles_secondary_[0], "foo", "v1");
VerifySecondaryValue(handles_secondary_[1], "bar", "w1");
// Both column families are flushed in one atomic group, but only the default
// column family's file stays readable.
ASSERT_OK(Flush({0, 1}));
const std::string cf1_table_file = GetNewestTableFilePath("cf1");
Close();
const std::string aside_file = MoveFileAside(cf1_table_file);
Defer restore_file(
[&] { EXPECT_OK(env_->RenameFile(aside_file, cf1_table_file)); });
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
// Neither column family may lose its key. The default one has no missing
// file of its own, but its Version is parked while the group is incomplete,
// leaving its memtable the only source of "foo"; assert that directly too.
ASSERT_FALSE(GetSecondaryCfd(handles_secondary_[0])->mem()->IsEmpty());
VerifySecondaryValue(handles_secondary_[0], "foo", "v1");
VerifySecondaryValue(handles_secondary_[1], "bar", "w1");
}
// Same hazard for a flushed file that is present and the right size but
// unreadable. With verify_sst_unique_id_in_manifest disabled nothing records it
// missing, yet opening it fails while the Version is built and that failure is
// swallowed so replay can continue, so again no Version covers the flush.
TEST_F(DBSecondaryTest, KeepsMemtableWhenFlushedFileIsCorrupt) {
Options options;
options.env = env_;
options.disable_auto_compactions = true;
Options secondary_options;
secondary_options.env = env_;
secondary_options.max_open_files = -1;
secondary_options.verify_sst_unique_id_in_manifest = false;
OpenSecondaryOnWalOnlyPut(options, secondary_options);
ASSERT_OK(Put("foo", "v2"));
ASSERT_OK(Flush());
const std::string table_file =
GetNewestTableFilePath(kDefaultColumnFamilyName);
Close();
// Corrupting the footer in place keeps the file size that the MANIFEST
// records, which is the only thing verified without unique id verification.
ASSERT_OK(test::CorruptFile(env_, table_file, /*offset=*/-8,
/*bytes_to_corrupt=*/8,
/*verify_checksum=*/false));
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
VerifySecondaryValue("foo", "v1");
}
// The same hazard for an *immutable* memtable. RecoverLogFiles() stamps a
// memtable sealed on a WAL switch with the newer WAL, and the column family's
// log number reaches that WAL as soon as the flush record is read, so the
// watermark collecting it has to come from the installed Version too.
TEST_F(DBSecondaryTest, KeepsImmutableMemtableWhenFlushedFileIsMissing) {
Options options;
options.env = env_;
options.disable_auto_compactions = true;
Options secondary_options;
secondary_options.env = env_;
secondary_options.max_open_files = -1;
OpenSecondaryOnWalOnlyPut(options, secondary_options);
// Only "foo" is overwritten later, so "bar" ends up readable from nowhere
// but this memtable once the flushed file is gone.
ASSERT_OK(Put("bar", "w1"));
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
VerifySecondaryValue("foo", "v1");
VerifySecondaryValue("bar", "w1");
// "v2" goes through the WAL, so closing the primary leaves it there for the
// secondary to replay after the flush record. The flushed file disappears
// before the secondary reads the MANIFEST record adding it.
ASSERT_OK(Flush());
const std::string table_file =
GetNewestTableFilePath(kDefaultColumnFamilyName);
ASSERT_OK(Put("foo", "v2"));
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
Close();
const std::string aside_file = MoveFileAside(table_file);
Defer restore_file(
[&] { EXPECT_OK(env_->RenameFile(aside_file, table_file)); });
// This round advances the log number and seals the memtable holding both
// keys, stamped with the new WAL. No Version covers the flush, so it must
// survive: "v2" supersedes "foo", but nothing supersedes "bar".
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
// "bar" survives in an immutable memtable, unlike the test above.
ASSERT_EQ(1, GetSecondaryCfd(db_secondary_->DefaultColumnFamily())
->imm()
->NumNotFlushed());
VerifySecondaryValue("foo", "v2");
VerifySecondaryValue("bar", "w1");
}
// WriteBatch::Iterate() stops at its first failure with earlier entries already
// inserted, so a batch rejected partway leaves entries from the newer WAL in
// the active memtable. The recorded WAL has to cover them, or the gate passes
// as soon as the installed Version reaches the newer WAL and drops them.
TEST_F(DBSecondaryTest, KeepsMemtableAfterPartialReplayFailure) {
Options options;
options.env = env_;
options.disable_auto_compactions = true;
CreateAndReopenWithCF({"cf1"}, options);
// Rejecting writes to "cf1" on the secondary only is what fails a batch
// partway: the default column family's entry is inserted, then "cf1" stops
// it. The primary allows both, so it can write such a batch.
Options secondary_options;
secondary_options.env = env_;
secondary_options.max_open_files = -1;
Options secondary_cf1_options = secondary_options;
secondary_cf1_options.disallow_memtable_writes = true;
std::vector<ColumnFamilyDescriptor> cf_descs;
cf_descs.emplace_back(kDefaultColumnFamilyName, secondary_options);
cf_descs.emplace_back("cf1", secondary_cf1_options);
ASSERT_OK(DB::OpenAsSecondary(secondary_options, dbname_, secondary_path_,
cf_descs, &handles_secondary_, &db_secondary_));
ASSERT_EQ(2, handles_secondary_.size());
// The hazard needs an older WAL already recorded: with no entry at all the
// gate keeps the memtable regardless.
ASSERT_OK(Put(0, "keep", "v1"));
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
VerifySecondaryValue(handles_secondary_[0], "keep", "v1");
// "keep" becomes file-readable and the primary moves to the WAL the rejected
// batch below lands in.
ASSERT_OK(Flush(0));
{
WriteBatch batch;
ASSERT_OK(batch.Put(handles_[0], "lost", "w1"));
ASSERT_OK(batch.Put(handles_[1], "x", "y"));
ASSERT_OK(db_->Write(WriteOptions(), &batch));
}
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
ASSERT_NOK(db_secondary_->TryCatchUpWithPrimary());
// Both column families, so that WAL goes obsolete: until it does, the reader
// the failed round poisoned fails every later round before the gate. The new
// file then goes missing, so no Version covers "lost".
ASSERT_OK(Flush({0, 1}));
const std::string table_file =
GetNewestTableFilePath(kDefaultColumnFamilyName);
Close();
const std::string aside_file = MoveFileAside(table_file);
Defer restore_file(
[&]() { EXPECT_OK(env_->RenameFile(aside_file, table_file)); });
// "lost" is readable only from the memtable the rejected batch wrote to, so
// that memtable must survive.
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
VerifySecondaryValue(handles_secondary_[0], "keep", "v1");
VerifySecondaryValue(handles_secondary_[0], "lost", "w1");
}
// A 2PC commit record names no column family, so
// CollectColumnFamilyIdsFromWriteBatch() reports nothing, yet MarkCommit()
// replays the prepared batch into the memtables. The recorded WAL has to
// advance anyway, or the gate drops committed data no file covers yet.
TEST_F(DBSecondaryTest, KeepsMemtableAfterTwoPhaseCommitReplay) {
Options options = CurrentOptions();
options.disable_auto_compactions = true;
// Recreate the primary as a TransactionDB so its WAL carries 2PC markers.
TransactionDB* txn_db = nullptr;
ASSERT_NO_FATAL_FAILURE(RecreatePrimaryAsTransactionDB(options, &txn_db));
// Flush so the WAL the prepared section lands in is not the one the column
// family's log number already names.
ASSERT_OK(db_->Put(WriteOptions(), "seed", "s"));
ASSERT_OK(db_->Flush(FlushOptions()));
OpenSecondaryFor2PC(options);
// The prepared batch names the default column family, so the secondary
// records this WAL for it, but buffers the entries instead of inserting.
std::unique_ptr<Transaction> txn;
ASSERT_NO_FATAL_FAILURE(
PrepareTransactionAndCatchUp(txn_db, "t1", "x", "2", &txn));
ASSERT_TRUE(
GetSecondaryCfd(db_secondary_->DefaultColumnFamily())->mem()->IsEmpty());
// No catch-up between this write and its flush: the secondary then applies
// the flush record first and skips replaying "z" as already flushed, which
// advances the installed Version while leaving its memtable empty.
ASSERT_OK(db_->Put(WriteOptions(), "z", "1"));
ASSERT_OK(db_->Flush(FlushOptions()));
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
// Had "z" been replayed instead of skipped, this round would seal and the
// commit below would record its own WAL, leaving nothing to get wrong.
ColumnFamilyData* cfd_secondary =
GetSecondaryCfd(db_secondary_->DefaultColumnFamily());
ASSERT_TRUE(cfd_secondary->mem()->IsEmpty());
ASSERT_EQ(0, cfd_secondary->imm()->NumNotFlushed());
// Replaying the marker inserts "x", which no file covers.
ASSERT_OK(txn->Commit());
txn.reset();
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
VerifySecondaryValue("x", "2");
}
// A secondary that observes a prepare but never its commit marker holds the
// prepared batch until close: DeleteRecoveredTransaction() runs only from
// MarkCommit() and MarkRollback(). Once the primary commits, flushes, and
// purges the WAL holding the marker nothing can resolve the batch, so a
// long-lived secondary accumulates them without bound while serving correct
// reads from the SST.
TEST_F(DBSecondaryTest, DropsRecoveredTransactionAfterCommitWalIsPurged) {
Options options = CurrentOptions();
options.disable_auto_compactions = true;
TransactionDB* txn_db = nullptr;
ASSERT_NO_FATAL_FAILURE(RecreatePrimaryAsTransactionDB(options, &txn_db));
OpenSecondaryFor2PC(options);
std::unique_ptr<Transaction> txn;
ASSERT_NO_FATAL_FAILURE(
PrepareTransactionAndCatchUp(txn_db, "t2", "k1", "v2", &txn));
// The primary commits and flushes, then the WAL holding the marker is purged
// before the secondary catches up again, so the marker is never replayed.
uint64_t commit_log_number = 0;
ASSERT_NO_FATAL_FAILURE(
CommitAndPurgeMarkerWal(txn_db, &txn, &commit_log_number));
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
VerifySecondaryValue("k1", "v2");
// The drop reads the secondary's own watermark, replicated by the MANIFEST.
ASSERT_GT(db_secondary_full()->GetVersionSet()->min_log_number_to_keep(),
commit_log_number);
ASSERT_EQ(nullptr, db_secondary_full()->GetRecoveredTransaction("t2"));
ASSERT_EQ(0, db_secondary_full()->TEST_LogsWithPrepSize());
ASSERT_EQ(0, db_secondary_full()->TEST_PreparedSectionCompletedSize());
}
// A primary can reuse a transaction name after commit unregisters it. If the
// secondary missed the old marker, it must remove that generation before
// replaying a new prepare with the same name.
TEST_F(DBSecondaryTest, ReusesTransactionNameAfterResolvedBatchIsPurged) {
Options options = CurrentOptions();
options.disable_auto_compactions = true;
TransactionDB* txn_db = nullptr;
ASSERT_NO_FATAL_FAILURE(RecreatePrimaryAsTransactionDB(options, &txn_db));
OpenSecondaryFor2PC(options);
std::unique_ptr<Transaction> old_txn;
ASSERT_NO_FATAL_FAILURE(
PrepareTransactionAndCatchUp(txn_db, "t6", "reuse", "v1", &old_txn));
uint64_t old_commit_log_number = 0;
ASSERT_NO_FATAL_FAILURE(
CommitAndPurgeMarkerWal(txn_db, &old_txn, &old_commit_log_number));
std::unique_ptr<Transaction> new_txn;
ASSERT_NO_FATAL_FAILURE(
PrepareTransactionAndCatchUp(txn_db, "t6", "reuse", "v2", &new_txn));
// The drop, not a replayed marker, is what removed the old generation.
ASSERT_GT(db_secondary_full()->GetVersionSet()->min_log_number_to_keep(),
old_commit_log_number);
DBImpl::RecoveredTransaction* recovered =
db_secondary_full()->GetRecoveredTransaction("t6");
ASSERT_NE(nullptr, recovered);
ASSERT_EQ(1, recovered->batches_.size());
VerifySecondaryValue("reuse", "v1");
ASSERT_OK(new_txn->Commit());
new_txn.reset();
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
ASSERT_EQ(nullptr, db_secondary_full()->GetRecoveredTransaction("t6"));
VerifySecondaryValue("reuse", "v2");
ASSERT_EQ(0, db_secondary_full()->TEST_LogsWithPrepSize());
ASSERT_EQ(0, db_secondary_full()->TEST_PreparedSectionCompletedSize());
}
// Commit replay deletes the recovered transaction and records its prepare WAL
// as complete. A secondary never runs the primary flush and write paths that
// normally prune that tracking state, so catch-up must do it.
TEST_F(DBSecondaryTest, PrunesRecoveredTransactionTrackingAfterCommitReplay) {
Options options = CurrentOptions();
options.disable_auto_compactions = true;
TransactionDB* txn_db = nullptr;
ASSERT_NO_FATAL_FAILURE(RecreatePrimaryAsTransactionDB(options, &txn_db));
OpenSecondaryFor2PC(options);
std::unique_ptr<Transaction> txn;
ASSERT_NO_FATAL_FAILURE(
PrepareTransactionAndCatchUp(txn_db, "t5", "k3", "v3", &txn));
ASSERT_EQ(1, db_secondary_full()->TEST_LogsWithPrepSize());
ASSERT_EQ(0, db_secondary_full()->TEST_PreparedSectionCompletedSize());
ASSERT_OK(txn->Commit());
txn.reset();
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
ASSERT_EQ(nullptr, db_secondary_full()->GetRecoveredTransaction("t5"));
ASSERT_EQ(0, db_secondary_full()->TEST_LogsWithPrepSize());
ASSERT_EQ(0, db_secondary_full()->TEST_PreparedSectionCompletedSize());
}
// A transaction whose prepare is still outstanding must be kept, no matter how
// much unrelated data the primary flushes and purges.
TEST_F(DBSecondaryTest, KeepsRecoveredTransactionWhilePrepareIsOutstanding) {
Options options = CurrentOptions();
options.disable_auto_compactions = true;
TransactionDB* txn_db = nullptr;
ASSERT_NO_FATAL_FAILURE(RecreatePrimaryAsTransactionDB(options, &txn_db));
DBImpl* primary = static_cast_with_check<DBImpl>(db_->GetRootDB());
OpenSecondaryFor2PC(options);
std::unique_ptr<Transaction> txn;
ASSERT_NO_FATAL_FAILURE(
PrepareTransactionAndCatchUp(txn_db, "t3", "k2", "w2", &txn));
const uint64_t prepare_log_number = primary->TEST_GetCurrentLogNumber();
for (int i = 0; i < 3; ++i) {
ASSERT_OK(db_->Put(WriteOptions(), "spacer" + std::to_string(i), "s"));
ASSERT_OK(db_->Flush(FlushOptions()));
}
primary->TEST_DeleteObsoleteFiles();
ASSERT_OK(primary->TEST_WaitForPurge());
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
// The watermark the drop reads stops exactly at the prepare's WAL, which is
// what makes comparing against it with `>=` load-bearing.
ASSERT_EQ(prepare_log_number,
db_secondary_full()->GetVersionSet()->min_log_number_to_keep());
ASSERT_NE(nullptr, db_secondary_full()->GetRecoveredTransaction("t3"));
// MarkCommit() resolves it and applies the batch, so an early drop would have
// lost "k2".
ASSERT_OK(txn->Commit());
txn.reset();
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
ASSERT_EQ(nullptr, db_secondary_full()->GetRecoveredTransaction("t3"));
VerifySecondaryValue("k2", "w2");
}
// Once the primary flushes and drops a WAL, its files are the authority for
// that WAL's contents, but entries the secondary already replayed linger in the
// active memtable and are consulted first, shadowing the newer file value.
// Bottommost compaction rewrites the file entry's sequence number to 0, which
// makes the shadowing permanent even for a seqno-aware read path.
TEST_F(DBSecondaryTest, StaleActiveMemtableDoesNotShadowFlushedData) {
Options options;
options.env = env_;
options.disable_auto_compactions = true;
Options secondary_options;
secondary_options.env = env_;
secondary_options.max_open_files = -1;
OpenSecondaryOnWalOnlyPut(options, secondary_options);
// "v2" is appended to the same WAL and flushed, and that WAL is deleted
// before the secondary reads its tail, so the memtable is stuck at "v1".
// Before the fix Get() returned "v1" here while an iterator returned "v2",
// which is why both read paths are checked.
ASSERT_OK(Put("foo", "v2"));
ASSERT_OK(Flush());
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
VerifySecondaryValue("foo", "v2");
// A second flush gives the bottommost compaction below more than one input
// file, so it has to rewrite rather than trivially move.
ASSERT_OK(Put("foo", "v3"));
ASSERT_OK(Flush());
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
VerifySecondaryValue("foo", "v3");
// Rewriting the surviving entry's sequence number to 0 puts it below any
// residual memtable entry, which used to make the iterator go stale too.
CompactRangeOptions cro;
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
std::vector<LiveFileMetaData> live_files;
db_->GetLiveFilesMetaData(&live_files);
ASSERT_EQ(1, live_files.size());
ASSERT_EQ(0, live_files[0].largest_seqno);
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
VerifySecondaryValue("foo", "v3");
}
// Complements the test above: dropping the active memtable on catch-up is only
// safe for the column families whose contents the primary has flushed. A column
// family whose data still lives solely in a WAL must keep its memtable, even
// when another column family in the same WAL gets flushed.
TEST_F(DBSecondaryTest, CatchUpKeepsUnflushedWalData) {
Options options;
options.env = env_;
options.disable_auto_compactions = true;
CreateAndReopenWithCF({"cf1"}, options);
Options secondary_options;
secondary_options.env = env_;
secondary_options.max_open_files = -1;
OpenSecondaryWithColumnFamilies({"cf1"}, secondary_options);
ASSERT_EQ(2, handles_secondary_.size());
// Both column families write to the same WAL and neither is flushed, so the
// secondary can only serve them from its WAL-replayed memtables.
ASSERT_OK(Put(0, "foo", "v1"));
ASSERT_OK(Put(1, "bar", "v1"));
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
VerifySecondaryValue(handles_secondary_[0], "foo", "v1");
VerifySecondaryValue(handles_secondary_[1], "bar", "v1");
// Flushing only "cf1" advances that column family's log number while the WAL
// is retained for the default column family, whose memtable must survive.
ASSERT_OK(Flush(1));
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
VerifySecondaryValue(handles_secondary_[0], "foo", "v1");
VerifySecondaryValue(handles_secondary_[1], "bar", "v1");
}
TEST_F(DBSecondaryTest, RefreshIterator) {
Options options;
options.env = env_;
@@ -1583,16 +2315,8 @@ TEST_F(DBSecondaryTest, OpenWithTransactionDB) {
Options options = CurrentOptions();
options.create_if_missing = true;
// Destroy the DB to recreate as a TransactionDB.
Close();
Destroy(options, true);
// Create a TransactionDB.
TransactionDB* txn_db = nullptr;
TransactionDBOptions txn_db_opts;
ASSERT_OK(TransactionDB::Open(options, txn_db_opts, dbname_, &txn_db));
ASSERT_NE(txn_db, nullptr);
db_.reset(txn_db);
ASSERT_NO_FATAL_FAILURE(RecreatePrimaryAsTransactionDB(options, &txn_db));
std::vector<std::string> cfs = {"new_CF"};
CreateColumnFamilies(cfs, options);
+38
View File
@@ -701,6 +701,44 @@ TEST_F(DBSSTTest, DBWithSstFileManagerForBlobFilesWithGC) {
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_F(DBSSTTest, SstFileManagerTracksMultipleCompactionBlobOutputs) {
constexpr uint64_t kBlobFileSize = 1024;
constexpr size_t kValueSize = 600;
std::shared_ptr<SstFileManager> sst_file_manager(NewSstFileManager(env_));
auto* sfm = static_cast<SstFileManagerImpl*>(sst_file_manager.get());
Options options = CurrentOptions();
options.sst_file_manager = sst_file_manager;
options.enable_blob_files = true;
options.min_blob_size = 0;
options.blob_file_size = kBlobFileSize;
options.blob_compression_type = kNoCompression;
options.disable_auto_compactions = true;
options.enable_blob_garbage_collection = true;
options.blob_garbage_collection_age_cutoff = 1.0;
options.blob_garbage_collection_force_threshold = 0.0;
DestroyAndReopen(options);
const std::string value(kValueSize, 'a');
ASSERT_OK(Put("key1", value));
ASSERT_OK(Put("key3", value));
ASSERT_OK(Flush());
ASSERT_OK(Put("key2", value));
ASSERT_OK(Put("key4", value));
ASSERT_OK(Flush());
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
ASSERT_OK(dbfull()->TEST_WaitForCompact());
sfm->WaitForEmptyTrash();
ASSERT_EQ(2U, GetBlobFileNumbers().size());
std::unordered_map<std::string, uint64_t> files_in_db;
ASSERT_OK(GetAllDataFiles(kTableFile, &files_in_db));
ASSERT_OK(GetAllDataFiles(kBlobFile, &files_in_db));
ASSERT_EQ(files_in_db, sfm->GetTrackedFiles());
}
class DBSSTTestRateLimit : public DBSSTTest,
public ::testing::WithParamInterface<bool> {
public:
+6 -5
View File
@@ -3725,6 +3725,8 @@ class ModelDB : public DB {
void DisableManualCompaction() override {}
void AbortAllCompactions() override {}
void ResumeAllCompactions() override {}
void AbortCompactions(ColumnFamilyHandle* /*column_family*/) override {}
void ResumeCompactions(ColumnFamilyHandle* /*column_family*/) override {}
Status WaitForCompact(
const WaitForCompactOptions& /* wait_for_compact_options */) override {
@@ -3804,11 +3806,10 @@ class ModelDB : public DB {
return Status::NotSupported();
}
Status GetUpdatesSince(
ROCKSDB_NAMESPACE::SequenceNumber,
std::unique_ptr<ROCKSDB_NAMESPACE::TransactionLogIterator>*,
const TransactionLogIterator::ReadOptions& /*read_options*/ =
TransactionLogIterator::ReadOptions()) override {
Status GetUpdatesSince(ROCKSDB_NAMESPACE::SequenceNumber,
std::unique_ptr<ROCKSDB_NAMESPACE::WalIterator>*,
const WalIterator::ReadOptions& /*read_options*/ =
WalIterator::ReadOptions()) override {
return Status::NotSupported("Not supported in Model DB");
}
+16 -12
View File
@@ -25,6 +25,7 @@
#if USE_COROUTINES
#include "folly/coro/BlockingWait.h"
#include "rocksdb/coro_db.h"
#endif // USE_COROUTINES
namespace ROCKSDB_NAMESPACE {
@@ -872,8 +873,8 @@ std::string DBTestBase::Get(const std::string& k, const Snapshot* snapshot,
#if USE_COROUTINES
if (use_coroutine) {
PinnableSlice pinnable_value(&result);
s = folly::coro::blockingWait(dbfull()->GetCoroutine(
options, dbfull()->DefaultColumnFamily(), k, &pinnable_value,
s = folly::coro::blockingWait(CoroDB::CoGet(
dbfull(), options, dbfull()->DefaultColumnFamily(), k, &pinnable_value,
/*timestamp=*/nullptr));
if (s.ok() && pinnable_value.IsPinned()) {
result.assign(pinnable_value.data(), pinnable_value.size());
@@ -903,8 +904,9 @@ std::string DBTestBase::Get(int cf, const std::string& k,
#if USE_COROUTINES
if (use_coroutine) {
PinnableSlice pinnable_value(&result);
s = folly::coro::blockingWait(dbfull()->GetCoroutine(
options, handles_[cf], k, &pinnable_value, /*timestamp=*/nullptr));
s = folly::coro::blockingWait(CoroDB::CoGet(dbfull(), options, handles_[cf],
k, &pinnable_value,
/*timestamp=*/nullptr));
if (s.ok() && pinnable_value.IsPinned()) {
result.assign(pinnable_value.data(), pinnable_value.size());
}
@@ -956,9 +958,10 @@ std::vector<std::string> DBTestBase::MultiGet(std::vector<int> cfs,
s.resize(cfs.size());
#if USE_COROUTINES
if (use_coroutine) {
folly::coro::blockingWait(dbfull()->MultiGetCoroutine(
options, cfs.size(), handles.data(), keys.data(), pin_values.data(),
/*timestamps=*/nullptr, s.data(), /*sorted_input=*/false));
folly::coro::blockingWait(CoroDB::CoMultiGet(
dbfull(), options, cfs.size(), handles.data(), keys.data(),
pin_values.data(), /*timestamps=*/nullptr, s.data(),
/*sorted_input=*/false));
} else
#else
(void)use_coroutine;
@@ -1005,9 +1008,10 @@ std::vector<std::string> DBTestBase::MultiGet(const std::vector<std::string>& k,
if (use_coroutine) {
std::vector<ColumnFamilyHandle*> cfs(keys.size(),
dbfull()->DefaultColumnFamily());
folly::coro::blockingWait(dbfull()->MultiGetCoroutine(
options, keys.size(), cfs.data(), keys.data(), pin_values.data(),
/*timestamps=*/nullptr, statuses.data(), /*sorted_input=*/false));
folly::coro::blockingWait(CoroDB::CoMultiGet(
dbfull(), options, keys.size(), cfs.data(), keys.data(),
pin_values.data(), /*timestamps=*/nullptr, statuses.data(),
/*sorted_input=*/false));
} else
#else
(void)use_coroutine;
@@ -1038,8 +1042,8 @@ Status DBTestBase::Get(const std::string& k, PinnableSlice* v,
#if USE_COROUTINES
if (use_coroutine) {
return folly::coro::blockingWait(
dbfull()->GetCoroutine(options, dbfull()->DefaultColumnFamily(), k, v,
/*timestamp=*/nullptr));
CoroDB::CoGet(dbfull(), options, dbfull()->DefaultColumnFamily(), k, v,
/*timestamp=*/nullptr));
}
#else
(void)use_coroutine;
+87
View File
@@ -4101,6 +4101,93 @@ TEST_F(DBBasicTestWithTimestamp, FullHistoryTsLowSanityCheckFail) {
Close();
}
// Verifies that reading with a snapshot and a FRESH timestamp succeeds even
// after full_history_ts_low advances past the snapshot's original timestamp.
// This models the db_stress scenario where a long-running snapshot's saved
// timestamp becomes stale due to concurrent flushes advancing
// full_history_ts_low (persist_user_defined_timestamps=false).
TEST_F(DBBasicTestWithTimestamp,
SnapshotReadWithFreshTimestampAfterTsLowAdvances) {
Options options = CurrentOptions();
options.env = env_;
options.comparator = test::BytewiseComparatorWithU64TsWrapper();
options.persist_user_defined_timestamps = false;
options.allow_concurrent_memtable_write = false;
DestroyAndReopen(options);
// Write a key at timestamp 100.
std::string write_ts;
PutFixed64(&write_ts, 100);
ASSERT_OK(db_->Put(WriteOptions(), "key1", write_ts, "value1"));
// Take a snapshot (pins the data at this sequence number).
const Snapshot* snap = db_->GetSnapshot();
ASSERT_NE(snap, nullptr);
// Simulate what happens in the stress test: the original read timestamp at
// snapshot time would have been ~100. Now advance full_history_ts_low well
// past 100 by writing more data and flushing.
std::string write_ts2;
PutFixed64(&write_ts2, 500);
ASSERT_OK(db_->Put(WriteOptions(), "key1", write_ts2, "value2"));
// Advance full_history_ts_low to 400 (past the original timestamp 100).
std::string ts_low;
PutFixed64(&ts_low, 400);
ASSERT_OK(db_->IncreaseFullHistoryTsLow(db_->DefaultColumnFamily(), ts_low));
ASSERT_OK(Flush());
// Omitting ReadOptions.timestamp is not a valid way to bypass the stale
// timestamp check because this column family enables user-defined timestamps.
{
ReadOptions read_opts;
read_opts.snapshot = snap;
std::string value;
Status s = db_->Get(read_opts, "key1", &value);
ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString();
}
// With persist_user_defined_timestamps=false and the flush,
// full_history_ts_low auto-advances past 500 (the highest write). Reading
// with the snapshot but the OLD (stale) timestamp of 100, which is below the
// auto-advanced threshold, should fail with InvalidArgument because
// read_ts < full_history_ts_low.
{
ReadOptions read_opts;
read_opts.snapshot = snap;
std::string stale_ts;
PutFixed64(&stale_ts, 100);
Slice stale_ts_slice = stale_ts;
read_opts.timestamp = &stale_ts_slice;
std::string value;
Status s = db_->Get(read_opts, "key1", &value);
ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString();
}
// Reading with the snapshot but a FRESH timestamp (>= full_history_ts_low)
// should succeed. The snapshot pins data visibility by sequence number, so
// the fresh timestamp only needs to pass the full_history_ts_low check.
// When persist_user_defined_timestamps=false and we flush,
// full_history_ts_low automatically advances to just above the newest write
// timestamp in the flushed memtable (i.e. just above 500). So we use 1000 to
// have comfortable margin above 500.
{
ReadOptions read_opts;
read_opts.snapshot = snap;
std::string fresh_ts;
PutFixed64(&fresh_ts, 1000);
Slice fresh_ts_slice = fresh_ts;
read_opts.timestamp = &fresh_ts_slice;
std::string value;
Status s = db_->Get(read_opts, "key1", &value);
ASSERT_OK(s);
ASSERT_EQ(value, "value1");
}
db_->ReleaseSnapshot(snap);
Close();
}
TEST_F(DBBasicTestWithTimestamp,
GCPreserveRangeTombstoneWhenNoOrSmallFullHistoryLow) {
Options options = CurrentOptions();
+50
View File
@@ -131,6 +131,43 @@ static const SequenceNumber kMaxSequenceNumber = ((0x1ull << 56) - 1);
static const SequenceNumber kDisableGlobalSequenceNumber =
std::numeric_limits<uint64_t>::max();
// Single source of truth for whether a bottommost entry's sequence number can
// be zeroed out (the "seqno squashing" compaction optimization), for the two
// conditions that gate it independently of snapshot visibility: the seqno->time
// "preserve" window and the user-defined-timestamp (UDT) history cutoff.
//
// Callers still apply their own snapshot-visibility gate (which differs: the
// compaction iterator is transaction/snapshot-checker aware, the version marker
// only compares against the oldest snapshot). This shared predicate exists so
// the entry-level decision in CompactionIterator::PrepareOutput and the
// file-level decision in
// VersionStorageInfo::ComputeBottommostFilesMarkedForCompaction cannot drift
// apart -- a mismatch causes a bottommost file to be marked for a compaction
// that can make no progress, i.e. an infinite compaction loop.
//
// - `seq`: the entry's sequence number (or a file's largest sequence number).
// - `preserve_seqno_after`: max seqno that may be zeroed given the preserve
// window; a seqno above it is too recent to zero.
// - `ts_sz`: user-defined timestamp size (0 means UDT disabled).
// - `full_history_ts_low_set`: whether full_history_ts_low has ever been set.
// - `ts_below_full_history_ts_low`: whether the entry's (or file's max) user
// timestamp is strictly below full_history_ts_low. Only consulted when UDT is
// enabled; callers pass false when the timestamp is unknown so an
// indeterminate file is treated as not-zeroable (conservative, no loop).
inline bool BottommostSeqnoCanBeZeroed(SequenceNumber seq,
SequenceNumber preserve_seqno_after,
size_t ts_sz,
bool full_history_ts_low_set,
bool ts_below_full_history_ts_low) {
if (seq > preserve_seqno_after) {
return false;
}
if (ts_sz == 0) {
return true;
}
return full_history_ts_low_set && ts_below_full_history_ts_low;
}
constexpr uint64_t kNumInternalBytes = 8;
// Defined in dbformat.cc
@@ -649,6 +686,19 @@ class IterKey {
key_size_ = total_size;
}
// Sets the key to `prefix` immediately followed by `suffix`, always copying
// into buf_. Used by the data-block common-prefix feature to reconstruct a
// restart-point key from the block's stored common user-key prefix plus the
// key's stored (prefix-stripped) bytes. is_user_key_ is left unchanged.
void SetKeyPrependingPrefix(const Slice& prefix, const Slice& suffix) {
const size_t total_size = prefix.size() + suffix.size();
EnlargeBufferIfNeeded(total_size);
memcpy(buf_, prefix.data(), prefix.size());
memcpy(buf_ + prefix.size(), suffix.data(), suffix.size());
key_ = buf_;
key_size_ = total_size;
}
// A version of `TrimAppend` assuming the last bytes of length `ts_sz` in the
// user key part of `key_` is not counted towards shared bytes. And the
// decoded key needed a min timestamp of length `ts_sz` pad to the user key.
+12 -10
View File
@@ -21,7 +21,7 @@
#include "port/stack_trace.h"
#include "rocksdb/db.h"
#include "rocksdb/env.h"
#include "rocksdb/transaction_log.h"
#include "rocksdb/wal_iterator.h"
#include "test_util/sync_point.h"
#include "test_util/testharness.h"
#include "test_util/testutil.h"
@@ -262,8 +262,8 @@ TEST_F(DeleteFileTest, BackgroundPurgeIteratorTest) {
// Regression test for a use-after-free where an obsolete-file purge started
// DURING DB close -- after CloseHelper's early purge drain -- continued using
// DBImpl after ~DBImpl destroyed mutex_. Seen as a vhost-user-blk SIGABRT when
// many RocksDB instances closed concurrently under a Warm Storage fault, with
// avoid_unnecessary_blocking_io=true on a shared, process-wide Env.
// many RocksDB instances closed concurrently under a remote file-system fault
// while using avoid_unnecessary_blocking_io=true on a shared, process-wide Env.
//
// Here we make the straggler deterministic: keep a dropped column family handle
// alive, delete it only after CloseHelper has passed its early purge drain,
@@ -418,6 +418,15 @@ TEST_F(DeleteFileTest, BackgroundPurgeCFDropTest) {
// Expect 1 sst file.
CheckFileTypeCounts(dbname_, 0, 1, 1);
if (bg_purge) {
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
SyncPoint::GetInstance()->LoadDependency(
{{"DeleteFileTest::BackgroundPurgeCFDropTest:1",
"DBImpl::BGWorkPurge:start"}});
SyncPoint::GetInstance()->EnableProcessing();
}
ASSERT_OK(db_->DropColumnFamily(cfh));
// Still 1 file, it won't be deleted while ColumnFamilyHandle is alive.
CheckFileTypeCounts(dbname_, 0, 1, 1);
@@ -447,13 +456,6 @@ TEST_F(DeleteFileTest, BackgroundPurgeCFDropTest) {
Reopen(options);
ASSERT_OK(dbfull()->TEST_WaitForPurge());
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
SyncPoint::GetInstance()->LoadDependency(
{{"DeleteFileTest::BackgroundPurgeCFDropTest:1",
"DBImpl::BGWorkPurge:start"}});
SyncPoint::GetInstance()->EnableProcessing();
{
SCOPED_TRACE("avoid_unnecessary_blocking_io = true");
do_test(true);
+22 -28
View File
@@ -433,13 +433,12 @@ void ErrorHandler::SetBGError(const Status& bg_status,
// it can directly overwrite any existing bg_error_.
bool auto_recovery = false;
Status bg_err(new_bg_io_err, Status::Severity::kUnrecoverableError);
CheckAndSetRecoveryAndBGError(bg_err);
CheckAndSetRecoveryAndBGError(bg_err, context);
ROCKS_LOG_INFO(
db_options_.info_log,
"ErrorHandler: Set background IO error as unrecoverable error\n");
EventHelpers::NotifyOnBackgroundError(db_options_.listeners, reason,
&bg_err, db_mutex_, &auto_recovery);
recover_context_ = context;
return;
}
if (wal_related) {
@@ -457,13 +456,12 @@ void ErrorHandler::SetBGError(const Status& bg_status,
// from WAL write failures.
bool auto_recovery = false;
Status bg_err(new_bg_io_err, Status::Severity::kFatalError);
CheckAndSetRecoveryAndBGError(bg_err);
CheckAndSetRecoveryAndBGError(bg_err, context);
ROCKS_LOG_WARN(db_options_.info_log,
"ErrorHandler: A potentially WAL error happened, set "
"background IO error as fatal error\n");
EventHelpers::NotifyOnBackgroundError(db_options_.listeners, reason,
&bg_err, db_mutex_, &auto_recovery);
recover_context_ = context;
return;
}
@@ -519,8 +517,7 @@ void ErrorHandler::SetBGError(const Status& bg_status,
severity = Status::Severity::kHardError;
}
Status bg_err(new_bg_io_err, severity);
CheckAndSetRecoveryAndBGError(bg_err);
recover_context_ = context;
CheckAndSetRecoveryAndBGError(bg_err, context);
bool auto_recovery = db_options_.max_bgerror_resume_count > 0;
EventHelpers::NotifyOnBackgroundError(db_options_.listeners, reason,
&new_bg_io_err, db_mutex_,
@@ -632,7 +629,7 @@ Status ErrorHandler::ClearBGError() {
Status ErrorHandler::RecoverFromBGError(bool is_manual) {
InstrumentedMutexLock l(db_mutex_);
bool no_bg_work_original_flag = soft_error_no_bg_work_;
const bool soft_error_no_bg_work_on_entry = soft_error_no_bg_work_;
if (is_manual) {
// If its a manual recovery and there's a background recovery in progress
// return busy status
@@ -640,25 +637,15 @@ Status ErrorHandler::RecoverFromBGError(bool is_manual) {
return Status::Busy("Recovery already in progress");
}
recovery_in_prog_ = true;
// In manual resume, we allow the bg work to run. If it is a auto resume,
// the bg work should follow this tag.
soft_error_no_bg_work_ = false;
// In manual resume, if the bg error is a soft error and also requires
// no bg work, the error must be recovered by call the flush with
// flush reason: kErrorRecoveryRetryFlush. In other case, the flush
// reason is set to kErrorRecovery.
if (no_bg_work_original_flag) {
recover_context_.flush_reason = FlushReason::kErrorRecoveryRetryFlush;
} else {
recover_context_.flush_reason = FlushReason::kErrorRecovery;
}
}
// kErrorRecovery can be the default context for a generic soft error that
// needs no flush. It can also remain from an earlier error after a later
// no-WAL soft error sets soft_error_no_bg_work_. Only the first case may be
// cleared directly.
if (bg_error_.severity() == Status::Severity::kSoftError &&
recover_context_.flush_reason == FlushReason::kErrorRecovery) {
// Simply clear the background error and return
recover_context_.flush_reason == FlushReason::kErrorRecovery &&
!soft_error_no_bg_work_on_entry) {
recovery_error_ = IOStatus::OK();
return ClearBGError();
}
@@ -668,11 +655,13 @@ Status ErrorHandler::RecoverFromBGError(bool is_manual) {
// can generate background errors should be the flush operations
recovery_error_ = IOStatus::OK();
recovery_error_.PermitUncheckedError();
Status s = db_->ResumeImpl(recover_context_, Env::IOActivity::kFlush);
DBRecoverContext context = recover_context_;
context.flush_after_recovery = true;
Status s = db_->ResumeImpl(context, Env::IOActivity::kFlush);
if (s.ok()) {
soft_error_no_bg_work_ = false;
} else {
soft_error_no_bg_work_ = no_bg_work_original_flag;
soft_error_no_bg_work_ = soft_error_no_bg_work_on_entry;
}
// For manual recover, shutdown, and fatal error cases, set
@@ -746,8 +735,6 @@ void ErrorHandler::RecoverFromRetryableBGIOError() {
recovery_in_prog_ = false;
return;
}
DBRecoverContext context = recover_context_;
context.flush_after_recovery = true;
int resume_count = db_options_.max_bgerror_resume_count;
uint64_t wait_interval = db_options_.bgerror_resume_retry_interval;
uint64_t retry_count = 0;
@@ -763,6 +750,9 @@ void ErrorHandler::RecoverFromRetryableBGIOError() {
TEST_SYNC_POINT("RecoverFromRetryableBGIOError:BeforeResume0");
TEST_SYNC_POINT("RecoverFromRetryableBGIOError:BeforeResume1");
recovery_error_ = IOStatus::OK();
// An error during the previous attempt can require stronger recovery.
DBRecoverContext context = recover_context_;
context.flush_after_recovery = true;
retry_count++;
Status s = db_->ResumeImpl(context, Env::IOActivity::kFlush);
RecordStats({ERROR_HANDLER_AUTORESUME_RETRY_TOTAL_COUNT},
@@ -822,12 +812,16 @@ void ErrorHandler::RecoverFromRetryableBGIOError() {
{{ERROR_HANDLER_AUTORESUME_RETRY_COUNT, retry_count}});
}
void ErrorHandler::CheckAndSetRecoveryAndBGError(const Status& bg_err) {
void ErrorHandler::CheckAndSetRecoveryAndBGError(
const Status& bg_err, const DBRecoverContext& context) {
assert(context.flush_reason != FlushReason::kErrorRecoveryRetryFlush ||
bg_err.severity() == Status::Severity::kSoftError);
if (recovery_in_prog_ && recovery_error_.ok()) {
recovery_error_ = status_to_io_status(Status(bg_err));
}
if (bg_err.severity() > bg_error_.severity()) {
bg_error_ = bg_err;
recover_context_ = context;
}
if (bg_error_.severity() >= Status::Severity::kHardError) {
is_db_stopped_.store(true, std::memory_order_release);
+4 -4
View File
@@ -156,10 +156,10 @@ class ErrorHandler {
void RecoverFromNoSpace();
void StartRecoverFromRetryableBGIOError(const IOStatus& io_error);
void RecoverFromRetryableBGIOError();
// First, if it is in recovery and the recovery_error is ok. Set the
// recovery_error_ to bg_err. Second, if the severity is higher than the
// current bg_error_, overwrite it.
void CheckAndSetRecoveryAndBGError(const Status& bg_err);
// If recovery is in progress, record the first new error for the current
// attempt. If this error is more severe, retain it and its recovery context.
void CheckAndSetRecoveryAndBGError(const Status& bg_err,
const DBRecoverContext& context);
};
} // namespace ROCKSDB_NAMESPACE
+136
View File
@@ -7,6 +7,9 @@
// 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 <atomic>
#include <chrono>
#include <future>
#include <memory>
#include "db/db_test_util.h"
@@ -1999,6 +2002,139 @@ TEST_F(DBErrorHandlingFSTest, FlushWritNoWALRetryableErrorAutoRecover2) {
Destroy(options);
}
TEST_F(DBErrorHandlingFSTest, AutoRecoveryEscalationSwitchesWAL) {
Options options = GetDefaultOptions();
options.env = fault_env_.get();
options.create_if_missing = true;
options.disable_auto_compactions = true;
options.track_and_verify_wals_in_manifest = true;
options.max_bgerror_resume_count = 3;
options.bgerror_resume_retry_interval = 1000;
options.avoid_flush_during_shutdown = true;
DestroyAndReopen(options);
struct ErrorInjectionState {
std::atomic<bool> fail_flush{true};
std::atomic<bool> fail_manifest{false};
std::atomic<bool> manifest_error_injected{false};
std::atomic<bool> recovery_notified{false};
std::promise<void> recovery_finished;
};
auto state = std::make_shared<ErrorInjectionState>();
auto recovery_finished = state->recovery_finished.get_future();
// Keep the failed flush counted as background work while the first recovery
// attempt waits without the DB mutex. This lets the foreground write safely
// escalate the retained recovery context.
SyncPoint::GetInstance()->LoadDependency(
{{"AutoRecoveryEscalationSwitchesWAL:ReleaseFailedFlush",
"DBImpl::BackgroundCallFlush:FilesFound"},
{"RecoverFromRetryableBGIOError:BeforeResume0",
"AutoRecoveryEscalationSwitchesWAL:RecoveryStarted"}});
SyncPoint::GetInstance()->SetCallBack(
"FlushJob::Run:PostBuildTable", [state](void* arg) {
if (state->fail_flush.exchange(false)) {
IOStatus error = IOStatus::IOError("injected retryable flush error");
error.SetRetryable(true);
*static_cast<Status*>(arg) = error;
}
});
SyncPoint::GetInstance()->SetCallBack(
"VersionSet::ProcessManifestWrites:AfterSyncManifest",
[state](void* arg) {
if (state->fail_manifest.exchange(false)) {
IOStatus error =
IOStatus::IOError("injected retryable MANIFEST error");
error.SetRetryable(true);
*static_cast<IOStatus*>(arg) = error;
state->manifest_error_injected.store(true);
}
});
SyncPoint::GetInstance()->SetCallBack(
"RecoverFromRetryableBGIOError:RecoverSuccess", [state](void*) {
if (!state->recovery_notified.exchange(true)) {
state->recovery_finished.set_value();
}
});
SyncPoint::GetInstance()->EnableProcessing();
ASSERT_OK(Put(Key(0), "value"));
const uint64_t initial_wal = dbfull()->TEST_GetCurrentLogNumber();
Status flush_status = Flush();
ASSERT_EQ(Status::Severity::kSoftError, flush_status.severity());
ASSERT_FALSE(state->fail_flush.load());
const uint64_t failed_write_wal = dbfull()->TEST_GetCurrentLogNumber();
ASSERT_GT(failed_write_wal, initial_wal);
TEST_SYNC_POINT("AutoRecoveryEscalationSwitchesWAL:RecoveryStarted");
const SequenceNumber sequence_before_delete = db_->GetLatestSequenceNumber();
state->fail_manifest.store(true);
WriteOptions write_options;
write_options.sync = true;
Status delete_status = db_->Delete(write_options, Key(0));
ASSERT_TRUE(delete_status.IsIOError()) << delete_status.ToString();
ASSERT_TRUE(state->manifest_error_injected.load());
ASSERT_EQ(sequence_before_delete, db_->GetLatestSequenceNumber());
ASSERT_EQ(Status::Severity::kHardError,
dbfull()->TEST_GetBGError().severity());
ASSERT_TRUE(dbfull()->TEST_IsRecoveryInProgress());
TEST_SYNC_POINT("AutoRecoveryEscalationSwitchesWAL:ReleaseFailedFlush");
ASSERT_EQ(std::future_status::ready,
recovery_finished.wait_for(std::chrono::seconds(10)))
<< "automatic recovery did not finish";
const uint64_t wal_after_recovery = dbfull()->TEST_GetCurrentLogNumber();
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
ASSERT_GT(wal_after_recovery, failed_write_wal)
<< "hard MANIFEST recovery must switch away from the WAL containing the "
"failed synced write";
ASSERT_OK(db_->Delete(write_options, Key(0)));
ASSERT_EQ("NOT_FOUND", Get(Key(0)));
Reopen(options);
ASSERT_EQ("NOT_FOUND", Get(Key(0)));
Destroy(options);
}
TEST_F(DBErrorHandlingFSTest, ManualResumePreservesStrongerRecoveryContext) {
auto listener = std::make_shared<ErrorHandlerFSListener>();
Options options = GetDefaultOptions();
options.env = fault_env_.get();
options.create_if_missing = true;
options.listeners.emplace_back(listener);
options.max_bgerror_resume_count = 0;
options.avoid_flush_during_shutdown = true;
listener->EnableAutoRecovery(false);
DestroyAndReopen(options);
ASSERT_OK(Put(Key(0), "value"));
const uint64_t wal_before_recovery = dbfull()->TEST_GetCurrentLogNumber();
IOStatus soft_error = IOStatus::IOError("injected soft error");
dbfull()->TEST_SetBGError(soft_error, BackgroundErrorReason::kAsyncFileOpen);
ASSERT_EQ(Status::Severity::kSoftError,
dbfull()->TEST_GetBGError().severity());
IOStatus retryable_error = IOStatus::IOError("injected retryable error");
retryable_error.SetRetryable(true);
dbfull()->TEST_SetBGError(retryable_error,
BackgroundErrorReason::kFlushNoWAL);
ASSERT_EQ(Status::Severity::kSoftError,
dbfull()->TEST_GetBGError().severity());
ASSERT_OK(dbfull()->Resume());
const uint64_t wal_after_recovery = dbfull()->TEST_GetCurrentLogNumber();
ASSERT_GT(wal_after_recovery, wal_before_recovery)
<< "manual resume must execute the retained stronger recovery context";
Reopen(options);
ASSERT_EQ("value", Get(Key(0)));
Destroy(options);
}
// Auto resume fromt the flush retryable IO error. Activate the FS before the
// first resume. Resume is successful
TEST_F(DBErrorHandlingFSTest, FlushWritRetryableErrorAutoRecover1) {
+82 -7
View File
@@ -2306,6 +2306,86 @@ TEST_F(ExternalSSTFileBasicTest,
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
}
TEST_F(ExternalSSTFileBasicTest,
AtomicReplaceRangeGeneratesTombstoneForPartialOverlap) {
Options options = CurrentOptions();
options.compaction_style = CompactionStyle::kCompactionStyleUniversal;
DestroyAndReopen(options);
SstFileWriter sst_file_writer(EnvOptions(), options);
std::vector<std::string> old_files;
for (const auto& file_contents :
std::vector<std::vector<std::pair<std::string, std::string>>>{
{{"a", "old-a"}, {"c", "old-c"}},
{{"d", "old-d"}},
{{"e", "old-e"}, {"f", "old-f"}, {"h", "old-h"}}}) {
std::string file_path =
sst_files_dir_ + "old-" + std::to_string(old_files.size()) + ".sst";
ASSERT_OK(sst_file_writer.Open(file_path));
for (const auto& [key, value] : file_contents) {
ASSERT_OK(sst_file_writer.Put(key, value));
}
ASSERT_OK(sst_file_writer.Finish());
old_files.push_back(std::move(file_path));
}
ASSERT_OK(db_->IngestExternalFile(old_files, IngestExternalFileOptions()));
ASSERT_EQ("0,0,0,0,0,0,3", FilesPerLevel());
const std::string replacement_file = sst_files_dir_ + "replacement.sst";
ASSERT_OK(sst_file_writer.Open(replacement_file));
ASSERT_OK(sst_file_writer.Put("b", "new-b"));
ASSERT_OK(sst_file_writer.Put("e", "new-e"));
ASSERT_OK(sst_file_writer.Finish());
IngestExternalFileArg arg;
arg.column_family = db_->DefaultColumnFamily();
arg.external_files = {replacement_file};
arg.options.snapshot_consistency = false;
arg.atomic_replace_range = {{"b", "f"}};
arg.options.allow_global_seqno = false;
Status status = db_->IngestExternalFiles({arg});
ASSERT_TRUE(status.IsInvalidArgument()) << status.ToString();
arg.options.allow_global_seqno = true;
arg.options.fail_if_not_bottommost_level = true;
status = db_->IngestExternalFiles({arg});
ASSERT_TRUE(status.IsTryAgain()) << status.ToString();
arg.options.fail_if_not_bottommost_level = false;
const Snapshot* snapshot = db_->GetSnapshot();
const SequenceNumber seqno_before_ingestion = db_->GetLatestSequenceNumber();
ASSERT_OK(db_->IngestExternalFiles({arg}));
ASSERT_EQ(seqno_before_ingestion + 2, db_->GetLatestSequenceNumber());
ASSERT_EQ("old-a", Get("a"));
ASSERT_EQ("new-b", Get("b"));
ASSERT_EQ("NOT_FOUND", Get("c"));
ASSERT_EQ("NOT_FOUND", Get("d"));
ASSERT_EQ("new-e", Get("e"));
ASSERT_EQ("old-f", Get("f"));
ASSERT_EQ("old-h", Get("h"));
ASSERT_EQ("0,0,0,0,1,1,2", FilesPerLevel());
ASSERT_OK(dbfull()->TEST_WaitForCompact());
ASSERT_EQ("0,0,0,0,1,1,2", FilesPerLevel());
ReadOptions snapshot_read_options;
snapshot_read_options.snapshot = snapshot;
std::string value;
ASSERT_OK(db_->Get(snapshot_read_options, "c", &value));
ASSERT_EQ("old-c", value);
ASSERT_OK(db_->Get(snapshot_read_options, "e", &value));
ASSERT_EQ("old-e", value);
db_->ReleaseSnapshot(snapshot);
ASSERT_OK(dbfull()->TEST_WaitForCompact());
ASSERT_EQ("0,0,0,0,1,0,1", FilesPerLevel());
ASSERT_EQ("NOT_FOUND", Get("c"));
ASSERT_EQ("new-e", Get("e"));
ASSERT_EQ("old-f", Get("f"));
}
TEST_F(ExternalSSTFileBasicTest,
PartiallyReplaceDataWithMultipleStandaloneRangeDeletions) {
Options options = CurrentOptions();
@@ -2963,14 +3043,9 @@ TEST_F(ExternalSSTFileBasicTest, FailIfNotBottommostLevelAndDisallowMemtable) {
s = db_->IngestExternalFiles({arg});
ASSERT_EQ(s.code(), Status::Code::kInvalidArgument);
// FIXME: upper bound should be exclusive (DeleteRange semantics).
// currently rejected because of documented bug
// The upper bound is exclusive, so the memtable key "e" does not
// overlap the replacement range.
arg.atomic_replace_range = {{"a", "e"}};
s = db_->IngestExternalFiles({arg});
ASSERT_EQ(s.code(), Status::Code::kInvalidArgument);
// work-around ensuring no memtable overlap
arg.atomic_replace_range = {{"a", "d2"}};
ASSERT_OK(db_->IngestExternalFiles({arg}));
ASSERT_EQ(Get(1, "e"), "5");
+213 -15
View File
@@ -18,6 +18,7 @@
#include "file/random_access_file_reader.h"
#include "logging/logging.h"
#include "monitoring/statistics_impl.h"
#include "options/options_helper.h"
#include "table/merging_iterator.h"
#include "table/prepared_file_info.h"
#include "table/sst_file_writer_collectors.h"
@@ -28,6 +29,129 @@
namespace ROCKSDB_NAMESPACE {
bool ExternalSstFileIngestionJob::SupportsAtomicReplaceRangeTombstone() const {
return cfd_->is_delete_range_supported() &&
cfd_->ioptions().compaction_style == kCompactionStyleUniversal &&
ingestion_options_.allow_global_seqno &&
!ingestion_options_.allow_db_generated_files &&
!ingestion_options_.snapshot_consistency &&
ucmp_->timestamp_size() == 0;
}
bool ExternalSstFileIngestionJob::CanUseAtomicReplaceRangeTombstone(
const std::optional<RangeOpt>& atomic_replace_range) const {
return atomic_replace_range.has_value() &&
atomic_replace_range->start.has_value() &&
atomic_replace_range->limit.has_value() &&
SupportsAtomicReplaceRangeTombstone() &&
!ingestion_options_.fail_if_not_bottommost_level;
}
bool ExternalSstFileIngestionJob::HasPartialOverlap(
const VersionStorageInfo* vstorage) const {
assert(vstorage != nullptr);
assert(atomic_replace_range_.has_value());
assert(!atomic_replace_range_->unset());
for (int level = 0; level < cfd_->NumberLevels(); ++level) {
for (const auto* file : vstorage->LevelFiles(level)) {
if (file_range_checker_.Overlaps(*atomic_replace_range_, file->smallest,
file->largest) &&
!file_range_checker_.Contains(*atomic_replace_range_, file->smallest,
file->largest)) {
return true;
}
}
}
return false;
}
size_t ExternalSstFileIngestionJob::NumFilesToPrepare(
size_t num_external_files,
const std::optional<RangeOpt>& atomic_replace_range) const {
return num_external_files +
static_cast<size_t>(
CanUseAtomicReplaceRangeTombstone(atomic_replace_range));
}
Status ExternalSstFileIngestionJob::PrepareAtomicReplaceRangeTombstone(
const Slice& start, const Slice& limit, uint64_t file_number,
SuperVersion* super_version) {
assert(cfd_->is_delete_range_supported());
assert(cfd_->ioptions().compaction_style == kCompactionStyleUniversal);
assert(ingestion_options_.allow_global_seqno);
assert(!ingestion_options_.allow_db_generated_files);
assert(!ingestion_options_.fail_if_not_bottommost_level);
assert(!ingestion_options_.snapshot_consistency);
assert(ucmp_->timestamp_size() == 0);
const uint32_t path_id = 0;
const std::string file_path =
TableFileName(cfd_->ioptions().cf_paths, file_number, path_id);
ExternalSstFileInfo external_file_info;
Status status;
{
Options options(
BuildDBOptions(db_options_, mutable_db_options_),
BuildColumnFamilyOptions(cfd_->initial_cf_options(),
super_version->mutable_cf_options));
SstFileWriter writer(env_options_, options, ucmp_);
status = writer.Open(
file_path, super_version->mutable_cf_options.default_write_temperature);
if (status.ok()) {
status = writer.DeleteRange(start, limit);
}
if (status.ok()) {
status = writer.Finish(&external_file_info);
}
}
if (!status.ok()) {
fs_->DeleteFile(file_path, IOOptions(), nullptr).PermitUncheckedError();
return status;
}
IngestedFileInfo tombstone;
tombstone.file_temperature =
super_version->mutable_cf_options.default_write_temperature;
tombstone.prefetch_lmax_index_and_filter_blocks =
ingestion_options_.prefetch_lmax_index_and_filter_blocks;
const PreparedFileInfo* prepared_file_info =
ingestion_options_.write_global_seqno
? nullptr
: external_file_info.prepared_file_info.get();
status = GetIngestedFileInfo(file_path, file_number, prepared_file_info,
&tombstone, super_version);
if (!status.ok()) {
fs_->DeleteFile(file_path, IOOptions(), nullptr).PermitUncheckedError();
return status;
}
tombstone.internal_file_path = file_path;
tombstone.copy_file = true;
tombstone.generated_for_ingestion = true;
tombstone.file_checksum = external_file_info.file_checksum;
tombstone.file_checksum_func_name =
external_file_info.file_checksum_func_name;
atomic_replace_range_tombstone_.emplace(std::move(tombstone));
return Status::OK();
}
void ExternalSstFileIngestionJob::ActivateAtomicReplaceRangeTombstone() {
assert(atomic_replace_range_tombstone_.has_value());
// Prepare rejects an empty external file list and verifies every input file
// is contained in the replacement range. The covering tombstone must
// therefore overlap at least one input file.
assert(!files_to_ingest_.empty());
file_batches_to_ingest_.clear();
files_to_ingest_.emplace_back(std::move(*atomic_replace_range_tombstone_));
std::rotate(files_to_ingest_.begin(), files_to_ingest_.end() - 1,
files_to_ingest_.end());
atomic_replace_range_tombstone_.reset();
atomic_replace_range_tombstone_active_ = true;
files_overlap_ = ComputeFilesOverlap(files_to_ingest_);
assert(files_overlap_);
DivideInputFilesIntoBatches();
}
Status ExternalSstFileIngestionJob::Prepare(
const std::vector<std::string>& external_files_paths,
const std::vector<std::string>& files_checksums,
@@ -92,6 +216,11 @@ Status ExternalSstFileIngestionJob::Prepare(
atomic_replace_range_.emplace();
if (atomic_replace_range->start && atomic_replace_range->limit) {
if (ucmp_->CompareWithoutTimestamp(*atomic_replace_range->start,
*atomic_replace_range->limit) >= 0) {
return Status::InvalidArgument(
"Atomic replace range limit must be greater than start");
}
// User keys to internal keys (with timestamps)
const size_t ts_sz = ucmp_->timestamp_size();
std::string start_with_ts, limit_with_ts;
@@ -102,8 +231,8 @@ Status ExternalSstFileIngestionJob::Prepare(
assert(limit.has_value());
atomic_replace_range_->smallest_internal_key.Set(
*start, kMaxSequenceNumber, kValueTypeForSeek);
atomic_replace_range_->largest_internal_key.Set(
*limit, kMaxSequenceNumber, kValueTypeForSeek);
atomic_replace_range_->largest_internal_key =
RangeTombstone(*start, *limit, kMaxSequenceNumber).SerializeEndKey();
// Check files to ingest against replace range
for (size_t i = 0; i < num_files; i++) {
if (!file_range_checker_.Contains(*atomic_replace_range_,
@@ -232,6 +361,20 @@ Status ExternalSstFileIngestionJob::Prepare(
ingestion_path_ids.insert(f.fd.GetPathId());
}
if (status.ok() && CanUseAtomicReplaceRangeTombstone(atomic_replace_range) &&
HasPartialOverlap(sv->current->storage_info())) {
assert(atomic_replace_range->start.has_value());
assert(atomic_replace_range->limit.has_value());
status = PrepareAtomicReplaceRangeTombstone(*atomic_replace_range->start,
*atomic_replace_range->limit,
next_file_number, sv);
if (status.ok()) {
assert(atomic_replace_range_tombstone_.has_value());
ingestion_path_ids.insert(
atomic_replace_range_tombstone_->fd.GetPathId());
}
}
TEST_SYNC_POINT("ExternalSstFileIngestionJob::BeforeSyncDir");
if (status.ok()) {
for (auto path_id : ingestion_path_ids) {
@@ -493,9 +636,7 @@ Status ExternalSstFileIngestionJob::NeedsFlush(bool* flush_needed,
assert(!atomic_replace_range_->smallest_internal_key.unset());
assert(!atomic_replace_range_->largest_internal_key.unset());
// NOTE: we already checked in Prepare() that the atomic_replace_range
// covers all the files_to_ingest
// FIXME: need to make upper bound key exclusive (not easy here because
// the existing internal APIs deal in inclusive upper bound user keys)
// covers all the files_to_ingest.
ranges.emplace_back(
atomic_replace_range_->smallest_internal_key.user_key(),
atomic_replace_range_->largest_internal_key.user_key());
@@ -507,7 +648,8 @@ Status ExternalSstFileIngestionJob::NeedsFlush(bool* flush_needed,
}
}
status = cfd_->RangesOverlapWithMemtables(
ranges, super_version, db_options_.allow_data_in_errors, flush_needed);
ranges, super_version, db_options_.allow_data_in_errors, flush_needed,
atomic_replace_range_.has_value() && !atomic_replace_range_->unset());
if (!status.ok()) {
ROCKS_LOG_WARN(db_options_.info_log,
"Failed to check ranges overlap with memtables: %s",
@@ -586,10 +728,12 @@ Status ExternalSstFileIngestionJob::Run() {
} else {
assert(!atomic_replace_range_->smallest_internal_key.unset());
assert(!atomic_replace_range_->largest_internal_key.unset());
bool has_partial_overlap = false;
for (int lvl = 0; lvl < cfd_->NumberLevels(); lvl++) {
if (cfd_->RangeOverlapWithCompaction(
atomic_replace_range_->smallest_internal_key.user_key(),
atomic_replace_range_->largest_internal_key.user_key(), lvl)) {
atomic_replace_range_->largest_internal_key.user_key(), lvl,
/*range_limit_exclusive=*/true)) {
return Status::InvalidArgument(
"Atomic replace range overlaps with pending compaction");
}
@@ -601,20 +745,36 @@ Status ExternalSstFileIngestionJob::Run() {
// Set up to delete file to be replaced
edit_.DeleteFile(lvl, file->fd.GetNumber());
} else {
// TODO: generate and ingest a tombstone file also
return Status::InvalidArgument(
"Atomic replace range partially overlaps with existing file");
has_partial_overlap = true;
}
}
}
}
if (has_partial_overlap) {
if (ingestion_options_.fail_if_not_bottommost_level &&
SupportsAtomicReplaceRangeTombstone()) {
return Status::TryAgain(
"Atomic replace range partially overlaps with an existing file, "
"so replacement files cannot all be ingested to Lmax");
}
if (!atomic_replace_range_tombstone_.has_value()) {
if (SupportsAtomicReplaceRangeTombstone()) {
return Status::TryAgain(
"Atomic replace range acquired a partial overlap after "
"preparation; retry ingestion");
}
return Status::InvalidArgument(
"Atomic replace range partially overlaps with existing file");
}
ActivateAtomicReplaceRangeTombstone();
}
}
}
// Find levels to ingest into
std::optional<int> prev_batch_uppermost_level;
// batches at the front of file_batches_to_ingest_ contains older updates and
// are placed in smaller levels.
// Batches at the front contain older updates and are placed deeper in the
// LSM tree than later overlapping batches.
for (auto& batch : file_batches_to_ingest_) {
int batch_uppermost_level = 0;
status = AssignLevelsForOneBatch(batch, super_version, force_global_seqno,
@@ -927,9 +1087,27 @@ void ExternalSstFileIngestionJob::Cleanup(const Status& status) {
// remove all the files we copied
DeleteInternalFiles();
files_overlap_ = false;
} else if (status.ok() && ingestion_options_.move_files) {
} else {
if (atomic_replace_range_tombstone_.has_value()) {
Status s =
fs_->DeleteFile(atomic_replace_range_tombstone_->internal_file_path,
io_opts, nullptr);
if (!s.ok()) {
ROCKS_LOG_WARN(
db_options_.info_log,
"Failed to remove unused atomic replace range tombstone %s: %s",
atomic_replace_range_tombstone_->internal_file_path.c_str(),
s.ToString().c_str());
}
atomic_replace_range_tombstone_.reset();
}
}
if (status.ok() && ingestion_options_.move_files) {
// The files were moved and added successfully, remove original file links
for (IngestedFileInfo& f : files_to_ingest_) {
if (f.generated_for_ingestion) {
continue;
}
Status s = fs_->DeleteFile(f.external_file_path, io_opts, nullptr);
if (!s.ok()) {
ROCKS_LOG_WARN(
@@ -955,6 +1133,19 @@ void ExternalSstFileIngestionJob::DeleteInternalFiles() {
f.internal_file_path.c_str(), s.ToString().c_str());
}
}
if (atomic_replace_range_tombstone_.has_value() &&
!atomic_replace_range_tombstone_->internal_file_path.empty()) {
Status s = fs_->DeleteFile(
atomic_replace_range_tombstone_->internal_file_path, io_opts, nullptr);
if (!s.ok()) {
ROCKS_LOG_WARN(
db_options_.info_log,
"AddFile() clean up for generated tombstone file %s failed : %s",
atomic_replace_range_tombstone_->internal_file_path.c_str(),
s.ToString().c_str());
}
atomic_replace_range_tombstone_.reset();
}
}
Status ExternalSstFileIngestionJob::ResetTableReader(
@@ -965,6 +1156,7 @@ Status ExternalSstFileIngestionJob::ResetTableReader(
std::unique_ptr<FSRandomAccessFile> sst_file;
FileOptions fo{env_options_};
fo.temperature = file_to_ingest->file_temperature;
fo.file_checksum_func_name = kNoFileChecksumFuncName;
Status status =
fs_->NewRandomAccessFile(external_file, fo, &sst_file, nullptr);
if (!status.ok()) {
@@ -1511,17 +1703,23 @@ Status ExternalSstFileIngestionJob::AssignLevelAndSeqnoForIngestedFile(
ingestion_options_.allow_db_generated_files
? cfd_->NumberLevels()
: assigned_level_exclusive_end;
const bool is_generated_range_tombstone =
atomic_replace_range_tombstone_active_ &&
file_to_ingest->generated_for_ingestion;
for (int lvl = 0; lvl < overlap_checking_exclusive_end; lvl++) {
if (lvl > 0 && lvl < vstorage->base_level()) {
continue;
}
if (lvl < assigned_level_exclusive_end &&
atomic_replace_range_.has_value()) {
atomic_replace_range_.has_value() &&
!atomic_replace_range_tombstone_active_) {
target_level = lvl;
continue;
}
if (cfd_->RangeOverlapWithCompaction(file_to_ingest->start_ukey,
file_to_ingest->limit_ukey, lvl)) {
file_to_ingest->limit_ukey, lvl,
/*range_limit_exclusive=*/
is_generated_range_tombstone)) {
// We must use L0 or any level higher than `lvl` to be able to overwrite
// the compaction output keys that we overlap with in this level, We also
// need to assign this file a seqno to overwrite the compaction output
+27 -2
View File
@@ -82,12 +82,12 @@ class ExternalFileRangeChecker {
range2_largest) <= 0;
}
bool Contains(const KeyRangeInfo& range1, const KeyRangeInfo& range2) {
bool Contains(const KeyRangeInfo& range1, const KeyRangeInfo& range2) const {
return Contains(range1, range2.smallest_internal_key,
range2.largest_internal_key);
}
bool Contains(const KeyRangeInfo& range1, const InternalKey& range2_smallest,
const InternalKey& range2_largest) {
const InternalKey& range2_largest) const {
bool any_unset =
range1.unset() || range2_smallest.unset() || range2_largest.unset();
if (any_unset) {
@@ -184,6 +184,11 @@ struct IngestedFileInfo : public KeyRangeInfo {
// Whether Lmax commit-time table opening should prefetch index/filter blocks.
bool prefetch_lmax_index_and_filter_blocks = true;
// Whether RocksDB generated this file as part of the ingestion job. Such a
// file is already in the DB directory and must not be unlinked as a caller
// input when move_files is enabled.
bool generated_for_ingestion = false;
SequenceNumber largest_seqno = kMaxSequenceNumber;
SequenceNumber smallest_seqno = kMaxSequenceNumber;
};
@@ -311,6 +316,10 @@ class ExternalSstFileIngestionJob {
return ingestion_options_.file_opening_threads;
}
size_t NumFilesToPrepare(
size_t num_external_files,
const std::optional<RangeOpt>& atomic_replace_range) const;
// Merge another already-Prepare()d job for the SAME column family into this
// one so both sets of files are committed by a single Run(). The other job's
// files are appended after this job's, so for any overlapping keys the other
@@ -320,6 +329,20 @@ class ExternalSstFileIngestionJob {
Status MergeForSameColumnFamily(ExternalSstFileIngestionJob* other);
private:
bool SupportsAtomicReplaceRangeTombstone() const;
bool CanUseAtomicReplaceRangeTombstone(
const std::optional<RangeOpt>& atomic_replace_range) const;
bool HasPartialOverlap(const VersionStorageInfo* vstorage) const;
Status PrepareAtomicReplaceRangeTombstone(const Slice& start,
const Slice& limit,
uint64_t file_number,
SuperVersion* super_version);
void ActivateAtomicReplaceRangeTombstone();
Status ResetTableReader(const std::string& external_file,
uint64_t new_file_number,
bool user_defined_timestamps_persisted,
@@ -451,6 +474,8 @@ class ExternalSstFileIngestionJob {
std::vector<FileBatchInfo> file_batches_to_ingest_;
const IngestExternalFileOptions ingestion_options_;
std::optional<KeyRangeInfo> atomic_replace_range_;
std::optional<IngestedFileInfo> atomic_replace_range_tombstone_;
bool atomic_replace_range_tombstone_active_{false};
Directories* directories_;
EventLogger* event_logger_;
VersionEdit edit_;
+293
View File
@@ -18,8 +18,10 @@
#include "port/stack_trace.h"
#include "rocksdb/sst_file_reader.h"
#include "rocksdb/sst_file_writer.h"
#include "table/format.h"
#include "table/prepared_file_info.h"
#include "test_util/testutil.h"
#include "util/defer.h"
#include "util/random.h"
#include "util/thread_guard.h"
#include "utilities/fault_injection_env.h"
@@ -431,6 +433,47 @@ TEST_F(ExternalSSTFileTest, PrepareThenCommit) {
ASSERT_EQ("v3", Get("k3"));
}
TEST_F(ExternalSSTFileTest,
AtomicReplaceRangeRetriesIfPartialOverlapAppearsAfterPrepare) {
Options options = CurrentOptions();
options.compaction_style = kCompactionStyleUniversal;
options.disable_auto_compactions = true;
DestroyAndReopen(options);
ASSERT_OK(
GenerateAndAddExternalFile(options, {{"c", "old-c"}, {"d", "old-d"}}));
std::string replacement_file;
ASSERT_OK(GenerateExternalFileOnly(options, {{"c", "new-c"}, {"d", "new-d"}},
&replacement_file));
IngestExternalFileArg arg;
arg.column_family = db_->DefaultColumnFamily();
arg.external_files = {replacement_file};
arg.options.allow_global_seqno = true;
arg.options.snapshot_consistency = false;
arg.atomic_replace_range = {{"b", "e"}};
std::unique_ptr<FileIngestionHandle> handle;
ASSERT_OK(db_->PrepareFileIngestion({arg}, &handle));
ASSERT_OK(
GenerateAndAddExternalFile(options, {{"a", "old-a"}, {"b", "old-b"}}));
Status status = db_->CommitFileIngestionHandle(std::move(handle));
ASSERT_TRUE(status.IsTryAgain()) << status.ToString();
ASSERT_EQ("old-a", Get("a"));
ASSERT_EQ("old-b", Get("b"));
ASSERT_EQ("old-c", Get("c"));
ASSERT_EQ("old-d", Get("d"));
ASSERT_OK(db_->IngestExternalFiles({arg}));
ASSERT_EQ("old-a", Get("a"));
ASSERT_EQ("NOT_FOUND", Get("b"));
ASSERT_EQ("new-c", Get("c"));
ASSERT_EQ("new-d", Get("d"));
}
TEST_F(ExternalSSTFileTest, ParallelFileOpenWithFileOpeningThreads) {
// Ingest many non-overlapping files with file_opening_threads > 1 so commit
// opens table readers with multiple threads. max_open_files == -1 makes
@@ -2415,6 +2458,253 @@ TEST_F(ExternalSSTFileTest, SstFileWriterNonSharedKeys) {
ASSERT_OK(DeprecatedAddFile({file_path}));
}
TEST_F(ExternalSSTFileTest, IngestFlushRequestNotLostBehindQueuedFlush) {
// These waits only prevent broken test paths from hanging forever. Keep them
// large enough to avoid treating slow CI scheduling as a regression.
constexpr uint64_t kHangGuardMicros = 30 * 1000 * 1000;
constexpr uint64_t kSetupWaitMicros = kHangGuardMicros;
constexpr uint64_t kIngestWaitMicros = kHangGuardMicros;
constexpr uint64_t kPollMicros = 1000;
Options options = CurrentOptions();
options.atomic_flush = false;
options.disable_auto_compactions = true;
options.write_buffer_size = 1024;
options.max_write_buffer_number = 8;
options.min_write_buffer_number_to_merge = 2;
env_->SetBackgroundThreads(1, Env::HIGH);
Reopen(options);
std::string external_file_path;
std::vector<std::pair<std::string, std::string>> external_file_data = {
{"ingest_key", "ingested_value"}};
ASSERT_OK(GenerateOneExternalFile(options, nullptr, external_file_data, -1,
true /* sort_data */, &external_file_path,
nullptr /* true_data */));
auto sleeping_task = std::make_shared<test::SleepingBackgroundTask>();
env_->Schedule(&test::SleepingBackgroundTask::DoSleepTask,
sleeping_task.get(), Env::Priority::HIGH);
sleeping_task->WaitUntilSleeping();
std::atomic<int> atomic_flush_scheduled{0};
std::atomic<int> get_live_files_done{0};
std::atomic<bool> ingest_waiting_for_flush{false};
std::atomic<bool> ingest_done{false};
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
SyncPoint::GetInstance()->SetCallBack(
"DBImpl::AtomicFlushMemTables:AfterScheduleFlush", [&](void*) {
atomic_flush_scheduled.fetch_add(1, std::memory_order_acq_rel);
});
SyncPoint::GetInstance()->SetCallBack(
"DBImpl::FlushMemTable:BeforeWaitForBgFlush", [&](void*) {
ingest_waiting_for_flush.store(true, std::memory_order_release);
});
SyncPoint::GetInstance()->EnableProcessing();
auto wait_until = [&](const std::function<bool()>& predicate,
uint64_t timeout_micros) {
for (uint64_t waited_micros = 0; waited_micros < timeout_micros;
waited_micros += kPollMicros) {
if (predicate()) {
return true;
}
env_->SleepForMicroseconds(kPollMicros);
}
return predicate();
};
auto wake_sleeping_task = [&] {
if (!sleeping_task->WokenUp()) {
sleeping_task->WakeUp();
}
return !sleeping_task->TimedWaitUntilDone(kSetupWaitMicros);
};
auto wait_or_cancel_background_work =
[&](const std::function<bool()>& predicate, uint64_t timeout_micros) {
if (wait_until(predicate, timeout_micros)) {
return true;
}
dbfull()->CancelAllBackgroundWork(false /* wait */);
if (wait_until(predicate, timeout_micros)) {
return true;
}
dbfull()->CancelAllBackgroundWork(true /* wait */);
return wait_until(predicate, timeout_micros);
};
ASSERT_OK(Put("atomic_flush_key", "value"));
Status get_live_files_status[2];
std::vector<LiveFileStorageInfo> live_files[2];
ThreadGuard get_live_files_thread_1(port::Thread([&] {
LiveFilesStorageInfoOptions lfsi_opts;
lfsi_opts.wal_size_for_flush = 0;
lfsi_opts.atomic_flush = true;
get_live_files_status[0] =
db_->GetLiveFilesStorageInfo(lfsi_opts, &live_files[0]);
get_live_files_done.fetch_add(1, std::memory_order_acq_rel);
}));
ThreadGuard get_live_files_thread_2(port::Thread([&] {
LiveFilesStorageInfoOptions lfsi_opts;
lfsi_opts.wal_size_for_flush = 0;
lfsi_opts.atomic_flush = true;
get_live_files_status[1] =
db_->GetLiveFilesStorageInfo(lfsi_opts, &live_files[1]);
get_live_files_done.fetch_add(1, std::memory_order_acq_rel);
}));
ThreadGuard ingest_thread;
auto get_live_files_finished = [&] {
return get_live_files_done.load(std::memory_order_acquire) == 2;
};
auto ingest_finished = [&] {
return ingest_done.load(std::memory_order_acquire);
};
auto wait_for_get_live_files_finished = [&] {
return wait_or_cancel_background_work(get_live_files_finished,
kSetupWaitMicros);
};
auto wait_for_ingest_finished = [&](uint64_t timeout_micros) {
return wait_or_cancel_background_work(ingest_finished, timeout_micros);
};
auto join_get_live_files_threads = [&] {
if (get_live_files_thread_1.GetThread().joinable()) {
get_live_files_thread_1.GetThread().join();
}
if (get_live_files_thread_2.GetThread().joinable()) {
get_live_files_thread_2.GetThread().join();
}
};
auto join_ingest_thread = [&] {
if (ingest_thread.GetThread().joinable()) {
ingest_thread.GetThread().join();
}
};
class CleanupGuard {
public:
explicit CleanupGuard(const std::function<void()>& cleanup)
: cleanup_(cleanup) {}
~CleanupGuard() {
if (armed_) {
cleanup_();
}
}
void Disarm() { armed_ = false; }
private:
std::function<void()> cleanup_;
bool armed_ = true;
};
CleanupGuard cleanup_guard([&] {
wake_sleeping_task();
wait_for_get_live_files_finished();
if (ingest_thread.GetThread().joinable()) {
wait_for_ingest_finished(kSetupWaitMicros);
}
join_get_live_files_threads();
join_ingest_thread();
});
const bool saw_both_atomic_flushes = wait_until(
[&] {
return atomic_flush_scheduled.load(std::memory_order_acquire) >= 2;
},
kSetupWaitMicros);
if (!saw_both_atomic_flushes) {
const bool sleeping_task_done = wake_sleeping_task();
const bool get_live_files_finished_after_cleanup =
wait_for_get_live_files_finished();
join_get_live_files_threads();
EXPECT_OK(get_live_files_status[0]);
EXPECT_OK(get_live_files_status[1]);
EXPECT_TRUE(get_live_files_finished_after_cleanup);
ASSERT_TRUE(saw_both_atomic_flushes)
<< "Timed out waiting for both GetLiveFilesStorageInfo calls to "
"schedule their flushes. scheduled="
<< atomic_flush_scheduled.load(std::memory_order_acquire)
<< " get_live_files_done="
<< get_live_files_done.load(std::memory_order_acquire)
<< " sleeping_task_done=" << sleeping_task_done;
}
ASSERT_OK(Put("ingest_key", "memtable_value"));
Status ingest_status;
ingest_thread = ThreadGuard(port::Thread([&] {
IngestExternalFileOptions ifo;
ingest_status = db_->IngestExternalFile({external_file_path}, ifo);
ingest_done.store(true, std::memory_order_release);
}));
const bool saw_ingest_wait = wait_until(
[&] {
return ingest_waiting_for_flush.load(std::memory_order_acquire) ||
ingest_done.load(std::memory_order_acquire);
},
kSetupWaitMicros);
const bool ingest_finished_before_wait =
ingest_done.load(std::memory_order_acquire);
if (!saw_ingest_wait || ingest_finished_before_wait) {
const bool sleeping_task_done = wake_sleeping_task();
const bool get_live_files_finished_after_cleanup =
wait_for_get_live_files_finished();
join_get_live_files_threads();
const bool ingest_finished_after_cleanup =
wait_for_ingest_finished(kSetupWaitMicros);
join_ingest_thread();
EXPECT_OK(get_live_files_status[0]);
EXPECT_OK(get_live_files_status[1]);
EXPECT_OK(ingest_status);
EXPECT_TRUE(get_live_files_finished_after_cleanup);
EXPECT_TRUE(ingest_finished_after_cleanup);
ASSERT_TRUE(sleeping_task_done);
ASSERT_TRUE(saw_ingest_wait)
<< "Timed out waiting for IngestExternalFile to enter its flush wait";
ASSERT_FALSE(ingest_finished_before_wait)
<< "IngestExternalFile finished before entering the expected flush "
"wait";
}
const bool sleeping_task_done = wake_sleeping_task();
join_get_live_files_threads();
const bool ingest_completed_without_unblock =
wait_until(ingest_finished, kIngestWaitMicros);
if (!ingest_completed_without_unblock) {
FlushOptions unblock_flush_options;
unblock_flush_options.wait = false;
const Status unblock_flush_status = db_->Flush(unblock_flush_options);
const bool ingest_finished_after_unblock =
wait_for_ingest_finished(kIngestWaitMicros);
join_ingest_thread();
ASSERT_TRUE(sleeping_task_done);
ASSERT_OK(get_live_files_status[0]);
ASSERT_OK(get_live_files_status[1]);
ASSERT_OK(unblock_flush_status);
ASSERT_TRUE(ingest_finished_after_unblock);
FAIL() << "IngestExternalFile remained blocked waiting for a flush that "
"was deduped behind an older queued flush request";
}
join_ingest_thread();
ASSERT_TRUE(sleeping_task_done);
ASSERT_OK(get_live_files_status[0]);
ASSERT_OK(get_live_files_status[1]);
ASSERT_OK(ingest_status);
ASSERT_EQ("ingested_value", Get("ingest_key"));
cleanup_guard.Disarm();
}
TEST_F(ExternalSSTFileTest, WithUnorderedWrite) {
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->LoadDependency(
@@ -3281,6 +3571,9 @@ INSTANTIATE_TEST_CASE_P(FormatVersions, ExternalSSTBlockChecksumTest,
TEST_P(ExternalSSTBlockChecksumTest, DISABLED_HugeBlockChecksum) {
BlockBasedTableOptions table_options;
table_options.format_version = GetParam();
// kFooterFormatVersionsToTest includes the unpublished draft format_version
// 8; writing it requires this opt-in.
SaveAndRestore<bool> allow_draft(&TEST_AllowUnsupportedFormatVersion(), true);
for (auto t : GetSupportedChecksums()) {
table_options.checksum = t;
Options options = CurrentOptions();
+2 -1
View File
@@ -1042,7 +1042,8 @@ Status FlushJob::WriteLevel0Table() {
meta_.fd.GetNumber(),
preclude_last_level_min_seqno_ == kMaxSequenceNumber
? preclude_last_level_min_seqno_
: std::min(earliest_snapshot_, preclude_last_level_min_seqno_));
: std::min(earliest_snapshot_, preclude_last_level_min_seqno_),
dbname_ /*db_name*/);
s = BuildTable(
dbname_, versions_, db_options_, tboptions, file_options_,
cfd_->table_cache(), iter.get(), std::move(range_del_iters), &meta_,
+1 -1
View File
@@ -38,7 +38,7 @@
#include "rocksdb/env.h"
#include "rocksdb/listener.h"
#include "rocksdb/memtablerep.h"
#include "rocksdb/transaction_log.h"
#include "rocksdb/wal_iterator.h"
#include "util/autovector.h"
#include "util/stop_watch.h"
#include "util/thread_local.h"
+2 -1
View File
@@ -196,7 +196,8 @@ Status ImportColumnFamilyJob::Run() {
nullptr /* src_vstorage */, cfd_->ioptions().force_consistency_checks,
EpochNumberRequirement::kMightMissing, cfd_->ioptions().clock,
cfd_->GetLatestMutableCFOptions().bottommost_file_compaction_delay,
cfd_->current()->version_set()->offpeak_time_option());
cfd_->current()->version_set()->offpeak_time_option(),
PeriodicCompactionPhaseParams{});
for (size_t j = 0; s.ok() && j < files_to_import_[i].size(); ++j) {
const auto& f = files_to_import_[i][j];
const auto& file_metadata = *metadatas_[i][j];
+46
View File
@@ -308,9 +308,15 @@ static const std::string aggregated_table_properties =
static const std::string aggregated_table_properties_at_level =
aggregated_table_properties + "-at-level";
static const std::string num_running_compactions = "num-running-compactions";
static const std::string num_running_remote_compactions =
"num-running-remote-compactions";
static const std::string num_running_bottom_compactions =
"num-running-bottom-compactions";
static const std::string num_running_compaction_sorted_runs =
"num-running-compaction-sorted-runs";
static const std::string compaction_abort_count = "compaction-abort-count";
static const std::string num_unscheduled_compactions =
"num-unscheduled-compactions";
static const std::string num_running_flushes = "num-running-flushes";
static const std::string actual_delayed_write_rate =
"actual-delayed-write-rate";
@@ -361,10 +367,16 @@ const std::string DB::Properties::kCompactionPending =
rocksdb_prefix + compaction_pending;
const std::string DB::Properties::kNumRunningCompactions =
rocksdb_prefix + num_running_compactions;
const std::string DB::Properties::kNumRunningRemoteCompactions =
rocksdb_prefix + num_running_remote_compactions;
const std::string DB::Properties::kNumRunningBottomCompactions =
rocksdb_prefix + num_running_bottom_compactions;
const std::string DB::Properties::kNumRunningCompactionSortedRuns =
rocksdb_prefix + num_running_compaction_sorted_runs;
const std::string DB::Properties::kCompactionAbortCount =
rocksdb_prefix + compaction_abort_count;
const std::string DB::Properties::kNumUnscheduledCompactions =
rocksdb_prefix + num_unscheduled_compactions;
const std::string DB::Properties::kNumRunningFlushes =
rocksdb_prefix + num_running_flushes;
const std::string DB::Properties::kBackgroundErrors =
@@ -594,12 +606,21 @@ const UnorderedMap<std::string, DBPropertyInfo>
{DB::Properties::kNumRunningCompactions,
{false, nullptr, &InternalStats::HandleNumRunningCompactions, nullptr,
nullptr}},
{DB::Properties::kNumRunningRemoteCompactions,
{false, nullptr, &InternalStats::HandleNumRunningRemoteCompactions,
nullptr, nullptr}},
{DB::Properties::kNumRunningBottomCompactions,
{false, nullptr, &InternalStats::HandleNumRunningBottomCompactions,
nullptr, nullptr}},
{DB::Properties::kNumRunningCompactionSortedRuns,
{false, nullptr, &InternalStats::HandleNumRunningCompactionSortedRuns,
nullptr, nullptr}},
{DB::Properties::kCompactionAbortCount,
{false, nullptr, &InternalStats::HandleCompactionAbortCount, nullptr,
nullptr}},
{DB::Properties::kNumUnscheduledCompactions,
{false, nullptr, &InternalStats::HandleNumUnscheduledCompactions,
nullptr, nullptr}},
{DB::Properties::kActualDelayedWriteRate,
{false, nullptr, &InternalStats::HandleActualDelayedWriteRate, nullptr,
nullptr}},
@@ -1286,6 +1307,23 @@ bool InternalStats::HandleNumRunningCompactions(uint64_t* value, DBImpl* db,
return true;
}
bool InternalStats::HandleNumRunningRemoteCompactions(uint64_t* value,
DBImpl* db,
Version* /*version*/) {
const int num_running =
db->num_running_remote_compactions_.load(std::memory_order_relaxed);
assert(num_running >= 0);
*value = static_cast<uint64_t>(num_running);
return true;
}
bool InternalStats::HandleNumRunningBottomCompactions(uint64_t* value,
DBImpl* db,
Version* /*version*/) {
*value = static_cast<uint64_t>(db->num_running_bottom_compactions_);
return true;
}
bool InternalStats::HandleNumRunningCompactionSortedRuns(uint64_t* value,
DBImpl* db,
Version* /*version*/) {
@@ -1305,6 +1343,14 @@ bool InternalStats::HandleCompactionAbortCount(uint64_t* value, DBImpl* db,
return true;
}
bool InternalStats::HandleNumUnscheduledCompactions(uint64_t* value, DBImpl* db,
Version* /*version*/) {
db->mutex()->AssertHeld();
assert(db->unscheduled_compactions_ >= 0);
*value = static_cast<uint64_t>(db->unscheduled_compactions_);
return true;
}
bool InternalStats::HandleBackgroundErrors(uint64_t* value, DBImpl* /*db*/,
Version* /*version*/) {
// Accumulated number of errors in background flushes or compactions.
+6
View File
@@ -850,10 +850,16 @@ class InternalStats {
bool HandleCompactionPending(uint64_t* value, DBImpl* db, Version* version);
bool HandleNumRunningCompactions(uint64_t* value, DBImpl* db,
Version* version);
bool HandleNumRunningRemoteCompactions(uint64_t* value, DBImpl* db,
Version* version);
bool HandleNumRunningBottomCompactions(uint64_t* value, DBImpl* db,
Version* version);
bool HandleNumRunningCompactionSortedRuns(uint64_t* value, DBImpl* db,
Version* version);
bool HandleCompactionAbortCount(uint64_t* value, DBImpl* db,
Version* version);
bool HandleNumUnscheduledCompactions(uint64_t* value, DBImpl* db,
Version* version);
bool HandleBackgroundErrors(uint64_t* value, DBImpl* db, Version* version);
bool HandleCurSizeActiveMemTable(uint64_t* value, DBImpl* db,
Version* version);
+6 -5
View File
@@ -498,7 +498,6 @@ void MemTableList::PickMemtablesToFlush(uint64_t max_memtable_id,
AutoThreadOperationStageUpdater stage_updater(
ThreadStatus::STAGE_PICK_MEMTABLES_TO_FLUSH);
const auto& memlist = current_->memlist_;
bool atomic_flush = false;
// Note: every time MemTableList::Add(mem) is called, it adds the new mem
// at the FRONT of the memlist (memlist.push_front(mem)). Therefore, by
@@ -509,9 +508,6 @@ void MemTableList::PickMemtablesToFlush(uint64_t max_memtable_id,
auto it = memlist.rbegin();
for (; it != memlist.rend(); ++it) {
ReadOnlyMemTable* m = *it;
if (!atomic_flush && m->atomic_flush_seqno_ != kMaxSequenceNumber) {
atomic_flush = true;
}
if (m->GetID() > max_memtable_id) {
break;
}
@@ -542,7 +538,12 @@ void MemTableList::PickMemtablesToFlush(uint64_t max_memtable_id,
// since they map to the same WAL and have the same NextLogNumber().
assert(strcmp((*it)->Name(), "WBWIMemTable") != 0);
}
if (!atomic_flush || num_flush_not_started_ == 0) {
// The flush request is complete only after every unstarted immutable
// memtable has been picked. A request can be older than a newer immutable
// memtable; clearing flush_requested_ after a partial or empty pick would
// make the queued newer request look unnecessary and leave that memtable
// unflushed.
if (num_flush_not_started_ == 0) {
flush_requested_ = false; // start-flush request is complete
}
}
+22 -9
View File
@@ -402,11 +402,11 @@ class MemTableList {
// Returns an estimate of the timestamp of the earliest key.
uint64_t ApproximateOldestKeyTime() const;
// Request a flush of all existing memtables to storage. This will
// cause future calls to IsFlushPending() to return true if this list is
// non-empty (regardless of the min_write_buffer_number_to_merge
// parameter). This flush request will persist until the next time
// PickMemtablesToFlush() is called.
// Request a flush of all existing memtables to storage. This will cause
// future calls to IsFlushPending() to return true if this list is non-empty
// (regardless of the min_write_buffer_number_to_merge parameter). This flush
// request will persist until PickMemtablesToFlush() has picked all unstarted
// memtables.
void FlushRequested() {
flush_requested_ = true;
// If there are some memtables stored in imm() that don't trigger
@@ -510,13 +510,26 @@ class MemTableList {
}
}
// Used only by DBImplSecondary during log replay.
// Remove memtables whose data were written before the WAL with log_number
// was created, i.e. mem->GetNextLogNumber() <= log_number. The memtables are
// not freed, but put into a vector for future deref and reclamation.
// Used only by secondary and follower instances, which collect memtables by
// the log number that follows their contents rather than by flushing them.
// Scanning from the oldest memtable, removes those with
// mem->GetNextLogNumber() <= log_number and stops at the first one without,
// so a memtable is kept while an older one is. The memtables are not freed,
// but put into a vector for future deref and reclamation.
void RemoveOldMemTables(uint64_t log_number,
autovector<ReadOnlyMemTable*>* to_delete);
// Returns whether RemoveOldMemTables(log_number) would remove anything, so
// that a caller can skip the call and the bookkeeping that follows it. O(1):
// the scan stops at the oldest memtable that does not qualify, so only the
// oldest one can decide this.
//
// REQUIRES: db mutex held.
bool HasOldMemTablesToRemove(uint64_t log_number) const {
const auto& memlist = current_->memlist_;
return !memlist.empty() && memlist.back()->GetNextLogNumber() <= log_number;
}
// This API is only used by atomic date replacement. To get an edit for
// dropping the current `MemTableListVersion`.
VersionEdit GetEditForDroppingCurrentVersion(
+107 -2
View File
@@ -890,8 +890,8 @@ TEST_F(MemTableListTest, FlushPendingTest) {
ASSERT_TRUE(to_flush5.empty());
ASSERT_EQ(1, list.NumNotFlushed());
ASSERT_TRUE(list.imm_flush_needed.load(std::memory_order_acquire));
ASSERT_FALSE(list.IsFlushPending());
ASSERT_FALSE(list.HasFlushRequested());
ASSERT_TRUE(list.IsFlushPending());
ASSERT_TRUE(list.HasFlushRequested());
// Pick tables to flush. The tables to pick must have ID smaller than or
// equal to 5. Therefore, only tables[5] will be selected.
@@ -920,6 +920,111 @@ TEST_F(MemTableListTest, FlushPendingTest) {
to_delete.clear();
}
TEST_F(MemTableListTest, FlushRequestPersistsAfterPartialPick) {
SequenceNumber seq = 1;
auto factory = std::make_shared<SkipListFactory>();
options.memtable_factory = factory;
ImmutableOptions ioptions(options);
InternalKeyComparator cmp(BytewiseComparator());
WriteBufferManager wb(options.db_write_buffer_size);
MutableCFOptions mutable_cf_options(options);
autovector<ReadOnlyMemTable*> to_delete;
MemTableList list(10 /* min_write_buffer_number_to_merge */,
0 /* max_write_buffer_size_to_maintain */);
for (uint64_t memtable_id = 0; memtable_id < 2; ++memtable_id) {
MemTable* mem = new MemTable(cmp, ioptions, mutable_cf_options, &wb,
kMaxSequenceNumber, 0 /* column_family_id */);
mem->SetID(memtable_id);
mem->Ref();
ASSERT_OK(mem->Add(++seq, kTypeValue, "key" + std::to_string(memtable_id),
"value", nullptr /* kv_prot_info */));
list.Add(mem, &to_delete);
}
list.FlushRequested();
ASSERT_TRUE(list.IsFlushPending());
autovector<ReadOnlyMemTable*> to_flush;
list.PickMemtablesToFlush(0 /* max_memtable_id */, &to_flush);
ASSERT_EQ(1, to_flush.size());
ASSERT_TRUE(list.IsFlushPending());
ASSERT_TRUE(list.HasFlushRequested());
autovector<ReadOnlyMemTable*> to_flush2;
list.PickMemtablesToFlush(1 /* max_memtable_id */, &to_flush2);
ASSERT_EQ(1, to_flush2.size());
ASSERT_FALSE(list.IsFlushPending());
ASSERT_FALSE(list.HasFlushRequested());
list.current()->Unref(&to_delete);
ASSERT_EQ(2, to_delete.size());
for (ReadOnlyMemTable* m : to_delete) {
delete m;
}
}
TEST_F(MemTableListTest, RemoveOldMemTablesTest) {
// Covers the predicate that lets a caller skip RemoveOldMemTables(), and
// checks that it agrees with what the removal itself does. Both are used only
// by the secondary instance, which collects memtables by the log number that
// follows their contents rather than by flushing them.
auto factory = std::make_shared<SkipListFactory>();
options.memtable_factory = factory;
ImmutableOptions ioptions(options);
InternalKeyComparator cmp(BytewiseComparator());
WriteBufferManager wb(options.db_write_buffer_size);
MutableCFOptions mutable_cf_options(options);
autovector<ReadOnlyMemTable*> to_delete;
MemTableList list(/*min_write_buffer_number_to_merge=*/1,
/*max_write_buffer_size_to_maintain=*/0);
// An empty list has nothing to remove, whatever the log number.
ASSERT_FALSE(list.HasOldMemTablesToRemove(0));
ASSERT_FALSE(
list.HasOldMemTablesToRemove(std::numeric_limits<uint64_t>::max()));
// Two sealed memtables, the older one followed by WAL 10 and the newer one by
// WAL 20.
SequenceNumber seq = 1;
for (uint64_t next_log_number : {10, 20}) {
MemTable* mem = new MemTable(cmp, ioptions, mutable_cf_options, &wb,
kMaxSequenceNumber, 0 /* column_family_id */);
mem->Ref();
ASSERT_OK(mem->Add(++seq, kTypeValue, "key", "value",
nullptr /* kv_prot_info */));
mem->SetNextLogNumber(next_log_number);
mem->ConstructFragmentedRangeTombstones();
list.Add(mem, &to_delete);
}
ASSERT_EQ(2, list.NumNotFlushed());
// Neither memtable is superseded by a WAL older than the one following it.
ASSERT_FALSE(list.HasOldMemTablesToRemove(9));
list.RemoveOldMemTables(9, &to_delete);
ASSERT_EQ(2, list.NumNotFlushed());
// The older memtable is collected at exactly its own next log number.
ASSERT_TRUE(list.HasOldMemTablesToRemove(10));
list.RemoveOldMemTables(10, &to_delete);
ASSERT_EQ(1, list.NumNotFlushed());
ASSERT_FALSE(list.HasOldMemTablesToRemove(19));
ASSERT_TRUE(list.HasOldMemTablesToRemove(20));
list.RemoveOldMemTables(20, &to_delete);
ASSERT_EQ(0, list.NumNotFlushed());
ASSERT_FALSE(
list.HasOldMemTablesToRemove(std::numeric_limits<uint64_t>::max()));
list.current()->Unref(&to_delete);
for (ReadOnlyMemTable* m : to_delete) {
delete m;
}
}
TEST_F(MemTableListTest, EmptyAtomicFlushTest) {
autovector<MemTableList*> lists;
autovector<uint32_t> cf_ids;
+1 -1
View File
@@ -24,7 +24,7 @@
#include "port/stack_trace.h"
#include "rocksdb/db.h"
#include "rocksdb/env.h"
#include "rocksdb/transaction_log.h"
#include "rocksdb/wal_iterator.h"
#include "test_util/sync_point.h"
#include "test_util/testharness.h"
#include "test_util/testutil.h"
Loaded 100 of 311 files, more files were not shown because too many files have changed in this diff. Show more