Compare commits

...
Author SHA1 Message Date
Jay Huh e9d2ad782e Update version and HISTORY.md for 11.4.3 release 2026-06-25 13:10:48 -07:00
Josh Kang b0001a9ca7 Do not allow read only DBs to delete obsolete files (#14881)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14881

Read-only DB instances can share a directory with a live writer, so their view of live files is only a snapshot from the time they opened. After read-only DB open began setting `opened_successfully_` for its intended open-success semantics, that state had an unintended side effect: the common close path treated a successful read-only open like a read-write open and ran obsolete-file cleanup. If the writer created or made files live after the read-only handle opened, that cleanup could use the stale read-only live set and delete files still needed by the writer.

This change records whether a `DBImpl` was opened read-only and skips close-time obsolete-file cleanup for read-only DBs instead of simply keeping `opened_successfully_=false`, which could be confusing and easily mistaken in the future again.

For completeness I also updated other callsites of `opened_successfully_`, but those should not be real bugs as ReadOnly DBs do not run flushes/compactions or write to WALs.

Reviewed By: xingbowang

Differential Revision: D109622445

fbshipit-source-id: d7be4b20fce86ccb218a63ac6f5b707b316aac79
2026-06-25 13:05:38 -07:00
anand76 cebb2ba1bf Update HISTORY and version for 11.4.2 2026-06-10 14:43:26 -07:00
Xingbo Wang f39e9a94f0 Add read-scoped block buffers for scan reads (#14806)
Summary:
Add read-scoped block buffers for scan reads. Introduce an experimental read-scoped block buffer provider API, configured through ReadOptions::read_scoped_block_buffer_provider, so supported block-based table iterator scans and MultiScan data-block reads can use caller-provided read-scoped storage for final data-block contents.

When configured, supported provider-backed scan data-block reads bypass the data-block cache while preserving normal index/filter block-cache behavior. Known-uncompressed reads can attach provider cleanup to provider-backed read buffers without copying. Compressed reads decompress directly into provider-backed output, while maybe-compressed reads that turn out to be uncompressed copy once into provider-backed final contents. mmap reads ignore the provider.

Extend AlignedBuffer and RandomAccessFileReader direct-I/O paths to support external aligned allocations, then use that support for read-scoped iterator, async I/O, and MultiRead scratch buffers. Centralize read-scoped I/O policy, keep coalesced async reads safe when blocks are released before completion, and validate provider lease contracts.

Add focused coverage for read-scoped ownership, compressed and uncompressed blocks, direct I/O, data-block cache bypass behavior, invalid provider leases, async release handling, and stress-test provider invariants. Add public API release notes for read-scoped block buffers.

Bonus change: Fixed a flaky test in ReserveThread

## Testing

- CI

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

Reviewed By: anand1976

Differential Revision: D106999951

Pulled By: xingbowang

fbshipit-source-id: b1f23d4bab6318b6373ba2ca99a5c4d6a842dc5a
2026-06-10 14:03:51 -07:00
Josh Kang 1805da4fec No-op 11.4.1 patch 2026-06-05 15:57:56 -07:00
anand1976 b5e3c7056d Update HISTORY.md for 11.4.0 release 2026-06-04 15:51:59 -07:00
Peter Dillinger 3883a8d05e Rename db_test2 -> db_etc2_test (#14218)
Summary:
When searching/grepping through unit tests, it's convenient to use test.cc suffix to match all unit tests, or to exclude test.cc when excluding unit tests from a code search. (I've even seen my AI assistant grep through `*test.cc`.) So I'm renaming db_test2.cc (as planned in https://github.com/facebook/rocksdb/issues/14076)

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

Test Plan: existing tests + CI

Reviewed By: xingbowang

Differential Revision: D90140624

Pulled By: pdillinger

fbshipit-source-id: 78aa099290670bbb4093b2c7c02bc47ab3bf5f8e
2026-06-02 16:18:05 -07:00
Josh Kang 768de6e50d Fix false-positive corruption error with remote compaction (#14808)
Summary:
Fix a false-positive compaction corruption error in the remote compaction path. Remote workers can mark `CompactionJobStats::num_input_records` as unreliable when input iteration uses seek/skip behavior, but the remote result serialization was dropping `has_accurate_num_input_records`. The primary then deserialized the flag as its default `true`, trusted an unreliable zero input-record count, and raised `Compaction number of input keys does not match number of keys processed`.

This change serializes the accuracy flag with the rest of `CompactionJobStats` so the primary preserves the remote worker's "do not verify this count" signal. It also adds a targeted regression test and a release note for the bug fix.

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

Test Plan: New regression test that triggers corruption error without the fix by modifying `has_accurate_num_input_records=false`

Reviewed By: xingbowang

Differential Revision: D107128357

Pulled By: joshkang97

fbshipit-source-id: 3243c9c2ab18534652737f7410fe3c51724db549
2026-06-02 11:13:54 -07:00
Peter Dillinger 62f05627be Reduce manifest rotation for foreground metadata ops (#14797)
Summary:
Async WAL precreation in https://github.com/facebook/rocksdb/pull/14738 / D105020559 was motivated by slow file creation time on remote storage. MANIFEST does not need the same precreation treatment as WAL because most MANIFEST writes come from background flush and compaction work, but user-facing metadata operations can still pay MANIFEST rotation file creation latency inline. File ingestion performance is a particular concern to some Meta users.

Relax the effective MANIFEST rotation limit by 25% for MANIFEST write batches containing any foreground VersionEdit, while keeping background-only flush/compaction batches on the configured or auto-tuned limit. This covers column family manipulation, external file ingestion and import, and DeleteFilesInRange(s). SetOptions remains expected to avoid MANIFEST writes; the test keeps a regression guard for that behavior.

The relaxation is intentionally bounded. It reduces the chance that foreground metadata operations create a new MANIFEST inline, while still allowing foreground operations to rotate once the current MANIFEST is beyond the relaxed threshold. Heavier blocking operations like manual Flush or CompactRange already trigger additional file creation and do not get this treatment here, though that could be reconsidered later.

This should reduce a potential latency hazard of manifest file size auto-tuning: more frequent MANIFEST rotations. With this change, rotation latency is shifted toward background-only MANIFEST batches when possible.

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

Test Plan:
Expanded DBEtc3Test.AutoTuneManifestSize to cover the foreground threshold behavior and the original auto-tuning behavior in separate phases:
- verifies foreground-only CreateColumnFamily writes get only bounded 25% headroom by asserting the first four large-CF additions do not rotate and the fifth does;
- verifies auto-tuned background thresholds still prevent excessive rotation;
- verifies foreground operations stay below the relaxed threshold for CreateColumnFamily, IngestExternalFile, CreateColumnFamilyWithImport, and DeleteFilesInRanges;
- verifies SetOptions still does not write to MANIFEST;
- verifies a following background flush still rotates at the normal threshold;
- preserves the persisted compacted manifest size close/reopen coverage.

Reviewed By: xingbowang

Differential Revision: D106578771

Pulled By: pdillinger

fbshipit-source-id: f8e274032cd9e7f50e95b685c949242f95351498
2026-06-01 18:13:29 -07:00
Peter Dillinger 9ef369c606 Make StringToMap entries self-contained for direct round-trip (#14805)
Summary:
StringToMap previously stripped the outer braces from nested values,
making single-entry round-trip via `key=value;` (e.g. SetOptions)
silently corrupt values that contain ';'. For example, a filter_policy
with sub-options serialized as
  filter_policy={id=ribbonfilter:10:-1;bloom_before_level=-1;}
parsed to map[filter_policy] = "id=ribbonfilter:10:-1;bloom_before_level=-1;",
and embedding that map entry directly into another `key=value;` string
re-exposed the inner ';' as a top-level delimiter.

This change is a step toward the "essential view": the outer `{...}` of
a nested value is part of the value's identity. StringToMap preserves it, so
each map entry is in self-contained form (a simple value or a single
balanced `{...}` block) and can be embedded directly without further
escaping by the caller. However some permissiveness is kept for
compatibility: list-style typed parsers (ParseVector, ParseArray,
kStringMap, the listener parser) and scalar dispatch in
OptionTypeInfo::Parse accept either the bare or the wrapped form via a
new OptionTypeInfo::StripOuterBraces helper that peels at most one
outer-pair-matched layer. So braced scalar input like `key={42};` and
`key={true};` still parses, and `key={};` denotes an empty list,
distinct from `key={{}};` which denotes a one-element list with an
empty element.

A new public MapToString is added to convenience.h as the symmetric
inverse of StringToMap: a trivial `key=value;` joiner that relies on
each value already being self-contained (which StringToMap guarantees).

Also fixed and refactored our trim() function because it would do the wrong
thing for a single space. This problem was detected by expanding the
StringToMapRandomTest based on code review feedback, and should only
improve existing callers to trim().

The OPTIONS-file serializer code is untouched, so the on-disk format
is byte-for-byte identical and format-compatibility is preserved.

Bonus: fixes / clarifications to CLAUDE.md's build-system note.

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

Test Plan:
db_bloom_filter_test extends MutableFilterPolicy with the OPTIONS-file
format for filter_policy to confirm that form round-trips through
SetOptions.

options_test updates StringToMapTest expectations for the new
brace-preserving behavior, and adds:
- MapToStringTest covering the new joiner.
- StringToMapMapToStringRoundTripTest including a single-entry
  pull-and-embed scenario that previously corrupted on round-trip.
- FullOptionsStringToMapRoundTripTest that drives a populated
  DBOptions and CFOptions through GetStringFromXxx -> StringToMap ->
  per-entry single round-trip -> MapToString -> GetXxxFromString ->
  VerifyXxxOptions, catching any custom serializer that emits a
  value with ';' without enclosing it in '{}'.
- EmptyBracedVectorAndBracedScalarTest covering `key={};` (empty
  list) and braced scalar input like `key={42};`, `key={true};`.
- Expanded StringToMapRandomTest with round-tripping.

Some explicit trim() tests added to string_util_test.cc

Format compatibility verified with
  SHORT_TEST=1 tools/check_format_compatible.sh

Other existing tests in options_test, customizable_test, options_settable_test,
listener_test, options_file_test, options_util_test, db_options_test,
and compaction_service_test have extensive coverage of serialization /
deserialization logic.

Reviewed By: hx235, xingbowang

Differential Revision: D106855466

Pulled By: pdillinger

fbshipit-source-id: 872aac8d819c7b90c807b92bab85569b48fa2aa0
2026-06-01 17:07:11 -07:00
Xingbo Wang 30dba7f41a Fix compaction abort rescheduling before queue pick (#14800)
Summary:
- AbortAllCompactions can race with an already-scheduled automatic compaction before the background worker calls PickCompactionFromQueue(). MaybeScheduleFlushOrCompaction() has already consumed one unscheduled_compactions_ credit when it scheduled the worker, but the CF is still present in compaction_queue_ with queued_for_compaction=true. If the worker returns kCompactionAborted before popping the CF, ResumeAllCompactions() can see unscheduled_compactions_ == 0 and fail to schedule the still-queued work, leaving compaction permanently stalled until DB restart.

- Restore the unscheduled compaction credit in the non-prepicked abort path, matching the existing BG-work-stopped handling for the same scheduled-before-pick state. Prepicked/manual compactions are not adjusted because they do not represent an unpopped automatic compaction_queue_ entry whose scheduling credit was consumed.

- Add AbortScheduledAutomaticCompactionBeforePick to deterministically reproduce the lost-credit race with a sync point at BackgroundCallCompaction:0. The test verifies that ResumeAllCompactions() schedules the still-queued automatic compaction by checking COMPACT_WRITE_BYTES advances and L0 file count drops.

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

Test Plan: - Without the implementation fix, `DBCompactionAbortTest.AbortScheduledAutomaticCompactionBeforePick` fails because `COMPACT_WRITE_BYTES` remains unchanged after `ResumeAllCompactions()`.

Reviewed By: pdillinger

Differential Revision: D106684155

Pulled By: xingbowang

fbshipit-source-id: 6dacea473ef481eb3122eb15e296e5b69635413f
2026-06-01 16:11:22 -07:00
Xingbo Wang 023fbb074a Optimize MultiScan dispatch for sorted blocks (#14783)
Summary:
- Propagate validated, sorted MultiScan range state from `DBIter::Prepare()` through `MultiScanArgs`.
- Mark block-based table IO jobs as already sorted when the public MultiScan ranges have been validated.
- Keep `IODispatcher::SubmitJob()` as the normalization boundary for unsorted callers, while allowing sorted callers to skip the defensive block-handle sort.
- Update private dispatcher coalescing helpers to consume sorted block indices and add debug assertions for that precondition.

## Testing
CI, new unit test

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

Reviewed By: anand1976

Differential Revision: D106301516

Pulled By: xingbowang

fbshipit-source-id: 99b7ffcaecbf27cb79f15feb4af8680ff1e422d9
2026-06-01 15:36:38 -07:00
Josh Kang 26a501b5d1 Fix tiered compaction incorrectly moving range tombstones upwards (#14795)
Summary:
Fixes an off-by-one bug in how sequence numbers are handled when splitting range tombstones across output levels in per-key-placement (tiered) compaction. The bug was either introduced or propagated in https://github.com/facebook/rocksdb/issues/13256.

Point entries move to proximal output only when their sequence number is strictly greater than `proximal_after_seqno_`, but range tombstones were previously split using an inclusive lower bound at that same seqno. That allowed a range tombstone at the boundary to be emitted to the proximal level while point keys at the same seqno stayed in the last level, which could create overlapping files in the proximal level (caught by `force_consistency_checks` as `L<n> has overlapping ranges`).

This matters when `proximal_output_range_type_` is `kNonLastRange`: the compaction only owns the selected proximal-level input range, so existing last-level data at the split boundary must stay in the last level. In `kFullRange`, the compaction owns the relevant proximal-level range, so newer last-level data can be safely emitted to proximal output.

The fix splits range tombstones at `proximal_after_seqno_ + 1` (saturating at `kMaxSequenceNumber`), so the half-open `[lower, upper)` tombstone filter lands on the same boundary as the strict `seqno > proximal_after_seqno_` rule used for point keys.

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

Test Plan:
New regression test `PrecludeLastLevelTestBase.RangeDelAtProximalSeqnoBoundaryStaysInLastLevel` uses a targeted manual compaction to exercise the `kNonLastRange` case directly, verifying a boundary range tombstone stays in the last level instead of widening the proximal output range.

Worked example:

```text
Initial state:

  L6 (last level):   Put(Key 2)s1,  Put(Key 12)s2,  RangeDel[Key 2, Key 12)s3
  L5 (proximal):     file A [Key 0 .. Key 4]s4-s5     file B [Key 5 .. Key 9]s6-s7

  preclude_last_level_min_seqno is forced to 0 via sync point.

Manual CompactFiles selects only L5 file B + the L6 file (output level 6).
Only part of the proximal level is selected, so this is the kNonLastRange case:

  max_last_level_seqno  = 3
  proximal_after_seqno_ = max(0, 3) = 3

  Point keys Key 5 (s6) and Key 9 (s7): both seqno > 3 -> proximal output (L5)   OK

  Range tombstone s3:
    Old (buggy):  proximal keep range [3, MAX) includes s3, so the tombstone is
                  emitted to the proximal output. The output is built from L5 input
                  file B [Key 5 .. Key 9], but the tombstone covers [Key 2, Key 12),
                  so the proximal output file starts at Key 2 and spills past Key 5
                  into the existing, untouched L5 file A [Key 0 .. Key 4].
                  Two overlapping files in L5 -> Corruption.

    New (fixed):  split at proximal_after_seqno_ + 1 = 4.
                  proximal keep [4, MAX) excludes s3; last-level keep [0, 4) includes
                  s3 -> tombstone stays in the last level (L6).   OK
```

Verification (debug build, `make -j64 tiered_compaction_test`):

- Without the fix, the test fails at the `CompactFiles` call:
  ```
  tiered_compaction_test.cc:2940: Failure
  Corruption: force_consistency_checks(DEBUG): VersionBuilder: L5 has overlapping ranges:
    file https://github.com/facebook/rocksdb/issues/11 largest key: Key(4) seq:5, type:1 (Put) vs.
    file https://github.com/facebook/rocksdb/issues/17 smallest key: Key(2) seq:3, type:15 (range deletion)
  ```
- With the fix, the test passes (range deletions absent from L5, still present in L6).

Reviewed By: pdillinger

Differential Revision: D106528471

Pulled By: joshkang97

fbshipit-source-id: a5f99b426d3a7a6253bc1972cf8cb60d1cb85089
2026-05-29 13:10:21 -07:00
Xingbo Wang e492562651 Prune MultiScan blocks using first internal key (#14784)
Summary:
- Use `IndexValue::first_internal_key` from `kBinarySearchWithFirstKey` index entries to decide whether the final bounded MultiScan candidate block starts at or beyond the scan limit.
- Skip that block when its first user key compares greater than or equal to the range limit with `CompareWithoutTimestamp`.
- Preserve existing conservative behavior for unbounded ranges, index entries without first-key metadata, and normal `kBinarySearch` indexes.
- Add parameterized coverage for boundary limits, in-block limits, bytewise and reverse comparators, and stripped/persisted user-defined timestamp modes.

## Testing

CI

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

Reviewed By: pdillinger

Differential Revision: D106302205

Pulled By: xingbowang

fbshipit-source-id: 1acebdf48bf7c18d35a781ca41c7bfd5c4ab8f47
2026-05-29 05:01:54 -07:00
Hui Xiao a0db079fdd Fall back to local compaction and report kUseLocal on remote result parse failure (#14799)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14799

When `CompactionService::Wait()` returns `kSuccess` but `CompactionServiceResult::Read()` fails before the primary renames any remote output file from `CompactionServiceResult::output_path` into the DB directory, fall back to local compaction for the same job and notify the service with `OnInstallation(..., kUseLocal)`.

At that point the remote SSTs are still in the service-managed output directory recorded in `CompactionServiceResult::output_path`, and the primary has not installed any of them into the DB yet.

Update `CompactionServiceTest.InvalidResultFallsBackToLocal` to verify the fallback completes successfully, preserves the data, and invokes `OnInstallation()` exactly once with `kUseLocal`.

Reviewed By: jaykorean

Differential Revision: D106321319

fbshipit-source-id: 39d9206f0e3f62612a52c03462bd1bee69020b80
2026-05-28 19:38:58 -07:00
Michael Huang 9270dc8149 Add blob_cache_read_byte perf context counter (#14792)
Summary:
Added blob_cache_read_byte in the rocksdb::PerContextBase to expose the blob cache read bytes when blob cache is enabled.

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

Reviewed By: xingbowang

Differential Revision: D106528572

Pulled By: mikechuangmeta

fbshipit-source-id: 555b2f01785bb819e62ed834ee45f0436dfb2875
2026-05-28 17:46:45 -07:00
xingbowang 638354e766 Fix getdeps fallback mirror downloads (#14763)
Summary:
- parse folly getdeps manifests with bare package entries so fallback prefetching actually runs
- validate and remove bad cached/downloaded archives before trying fallback mirrors
- download through temporary files and include libiberty in the GNU toolchain fallback set

Context:

Nightly test failed with dependency download failure in folly.

```
Assessing autoconf...
Download with https://ftpmirror.gnu.org/gnu/autoconf/autoconf-2.69.tar.gz -> /tmp/fbcode_builder_getdeps-Z__wZrocksdbZrocksdbZthird-partyZfollyZbuildZfbcode_builder-root/downloads/autoconf-autoconf-2.69.tar.gz ...
 [Complete in 136.616022 seconds]
    raise Exception(
Exception: https://ftpmirror.gnu.org/gnu/autoconf/autoconf-2.69.tar.gz: expected sha256 954bd69b391edc12d6a4a51a2dd1476543da5c6bbf05a95b59dc0dd6fd4c2969 but got e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
make: *** [folly.mk:152: build_folly] Error 1
##[error]Process completed with exit code 2.
```

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

Test Plan:
1. Connection failure:
      - Forced first mirror to http://127.0.0.1:1/...
      - It logged connection refused.
      - It then tried https://mirrors.kernel.org/gnu/...
      - Download succeeded, size 1927468, SHA matched 954bd69b...
  2. Empty file / bad hash:
      - Ran a local HTTP server returning a zero-byte autoconf-2.69.tar.gz
      - Script logged mismatch with actual=e3b0c442... size=0
      - It removed the bad download and fell back to mirrors.kernel.org
      - Download succeeded with the expected SHA.
  3. Existing zero-byte cache:
      - Seeded cache with an empty tarball.
      - Script removed invalid cache and downloaded a verified copy.

Reviewed By: mszeszko-meta

Differential Revision: D105859558

Pulled By: xingbowang

fbshipit-source-id: ff1f20f87debad561610271ce99b8b8de2d4264f
2026-05-28 07:19:20 -07:00
Hui Xiao c724aeb67e Add multi-DB stress testing support (--num_dbs flag) (#14749)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14749

Add `--num_dbs` flag to run N independent DB instances in parallel. Each `StressTest` instance has its own DB with isolated fault injection (from D104959945). `db_crashtest.py` defaults to `num_dbs=1`.

For `num_dbs=1`: `--db` and `--expected_values_dir` are paths used as-is.
For `num_dbs>1`: they are parent directories; C++ creates `db_0/`, `db_1/`, ... subdirs underneath.

Path ownership: C++ owns DB and secondary dir creation (supports remote env). Python owns EV dir creation (always local). C++ also creates EV dirs as fallback for direct CLI usage. `DestroyAllDbs` cleans up subdirs and the parent dir.

Per-DB: `threads`, `max_key`, `ops_per_thread`, `reopen`, `column_families`, and all DB options.
Shared: background env threads (compaction, flush pool), `block_cache`, `write_buffer_manager`, `compressed_secondary_cache`, `rate_limiter`, `compaction_thread_pool_adjust_interval`.

Reviewed By: anand1976

Differential Revision: D104959942

fbshipit-source-id: 3d0d60101e7f2e600306e5a9c4018686bf649658
2026-05-27 11:53:11 -07:00
Xingbo Wang 364eb88151 Keep prepared transactions rollbackable after commit write failure (#14778)
Summary:
- Restore prepared transactions to `PREPARED` state when writing the commit marker fails, so callers can still roll them back.
- Preserve `PREPARED` state when rollback of a prepared transaction hits a retryable write error, allowing rollback to be retried after `DB::Resume()`.
- Update `db_stress` to clean up prepared transactions after failed commits and report detailed rollback cleanup failure diagnostics.
- Add a WritePrepared regression test covering retryable commit write failure, retryable rollback write failure, successful rollback retry, and DB reopen.

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

Test Plan: CI

Reviewed By: pdillinger

Differential Revision: D106202437

Pulled By: xingbowang

fbshipit-source-id: b0b52e1d14f39b023b9692dd8fc44060fa35c446
2026-05-27 05:25:59 -07:00
Xingbo Wang d7afd3bb3b Sync recovery SST directory before reused MANIFEST append (#14780)
Summary:
- When `reuse_manifest_on_open` reuses the current MANIFEST, `DB::Open` recovery can flush WAL data into a new L0 SST and append the corresponding `VersionEdit` to that already-current MANIFEST.
- If open later fails and the process crashes, the MANIFEST edit can be durable while the recovered SST directory entry is not, leaving the DB pointing at a missing SST.
- Fsync the recovered SST's data directory before adding the file to the recovery edit when appending to a reused MANIFEST.
- Add a regression test that injects failure after MANIFEST sync, simulates crash cleanup of files created after the last directory sync, and verifies the recovered key remains readable.

## Task
- T272584339

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

Test Plan: CI

Reviewed By: anand1976

Differential Revision: D106201774

Pulled By: xingbowang

fbshipit-source-id: a44a7d1263d5bc1d82b995c90eef1a825eab4182
2026-05-27 04:42:27 -07:00
Xingbo Wang ae30c71c6b Fix secondary WAL tailing with precreated future WAL (#14781)
Summary:
- Fix secondary catch-up WAL discovery to retain existing WAL readers until MANIFEST replay advances min_log_number_to_keep past them.
- Do not treat a higher-number WAL appearing in the directory as proof that a lower-number current WAL is obsolete; async WAL precreation can expose that shape while the lower-number WAL is still growing.
- Continue scanning from the smallest retained reader so later appends to the current WAL are replayed, and add a regression test that precreates an empty future WAL before secondary catch-up.

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

Test Plan:
- make -j128 db_secondary_test
- ./db_secondary_test --gtest_filter=DBSecondaryTest.CatchUpTailsCurrentWalWhenFutureWalExists
- ./db_secondary_test
- make check-sources

Reviewed By: hx235

Differential Revision: D106296411

Pulled By: xingbowang

fbshipit-source-id: acc850a177c02968372981d1407721540bc164f5
2026-05-26 11:55:32 -07:00
zaidoon 91b31112ed Expose AdvancedColumnFamilyOptions::memtable_batch_lookup_optimization in the C API (#14776)
Summary:
`AdvancedColumnFamilyOptions::memtable_batch_lookup_optimization` ([advanced_options.h](https://github.com/facebook/rocksdb/blob/main/include/rocksdb/advanced_options.h)) gates the skip-list memtable's batch-lookup optimization for `MultiGet`. When enabled, the search path is cached between consecutive keys, reducing per-key cost from `O(log N)` to `O(log d)` where `d` is the distance between consecutive keys.

The C++ field exists; the C API setter does not. This PR adds the missing pair, mirroring the existing `rocksdb_options_{set,get}_memtable_huge_page_size` shape exactly — the closest sibling on both axes:

- C API: adjacent memtable knob, same `rocksdb_options_t*` receiver.
- C++: same `AdvancedColumnFamilyOptions` parent struct, same immutability semantics.

## Motivation

Without this setter, C API consumers and downstream bindings cannot opt into the batch-lookup optimization. Non-skip-list memtable implementations fall back to per-key lookups, so the flag is a no-op for them.

The field is immutable on the C++ side, so calling the setter on options that are already in use by an open DB has no effect on that DB — same constraint as the underlying C++ field. This matches the behavior of every other immutable-options setter in the C API.

No change to the C++ API.

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

Reviewed By: joshkang97

Differential Revision: D106364224

Pulled By: xingbowang

fbshipit-source-id: 90946af498fba51581a1e7d493c9e5c9b98472a2
2026-05-26 09:11:35 -07:00
zaidoon 7f32e2cabc Expose BlockBasedTableOptions::uniform_cv_threshold and BlockSearchType::kAuto in the C API (#14775)
Summary:
The block-based table format gained an "auto" index-block search mode ([table.h](https://github.com/facebook/rocksdb/blob/main/include/rocksdb/table.h)) that selects binary vs interpolation search per index block based on key uniformity. The C++ surface exposes this as two coupled knobs:

- `BlockSearchType::kAuto = 0x02` selects the per-block adaptive search at read time.
- `BlockBasedTableOptions::uniform_cv_threshold` (default `-1`, i.e. disabled) is the coefficient-of-variation threshold checked on the write path to set the per-block `is_uniform` footer bit that `kAuto` reads.

This PR adds the missing C API coverage for both:

1. **`rocksdb_block_based_table_index_block_search_type_auto = 2`** enum constant. The existing setter `rocksdb_block_based_options_set_index_block_search_type` already does `static_cast<BlockSearchType>(v)`, so `kAuto = 2` was reachable today by passing the raw int — only the named constant was missing.

2. **`rocksdb_block_based_options_set_uniform_cv_threshold(...)` setter**. The field had no C wrapper, so C/binding users could select `kAuto` but the `is_uniform` bit was never set on the write path, making `kAuto` degenerate to `kBinary`.

No getter is added: the surrounding `rocksdb_block_based_options_set_*` functions in `c.h` do not expose getters either, so adding one only here would be inconsistent with the local style.

## Motivation

Without both pieces, `kAuto` is effectively unreachable from C. This matters for binding consumers (Rust, Go, Java-via-JNI shim, etc.) who want to opt index-block search into the per-block adaptive mode.

The setter mirrors the existing `rocksdb_block_based_options_set_data_block_hash_ratio` shape exactly (same struct, same `double` payload, same naming pattern), so review surface is minimal.

No change to the C++ API.

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

Reviewed By: joshkang97

Differential Revision: D106364288

Pulled By: xingbowang

fbshipit-source-id: bb532eac6d4c04d032a7235f25ac29ab74f636f2
2026-05-26 09:09:24 -07:00
Xingbo Wang 81fa943ca0 Avoid retryable io_uring wait_cqe stderr in crash tests (#14779)
Summary:
- Suppress retryable `io_uring_wait_cqe()` errors in `PosixFileSystem::Poll()` and `AbortIO()` before they reach stderr during crash-test SIGTERM timeout handling.
- Keep terminal `wait_cqe` failures logged and fatal.
- Extend the `db_crashtest.py` SIGTERM stderr filter only for retryable `Poll`/`AbortIO` wait_cqe errors and add regression coverage.

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

Test Plan:
CI

## Related
- T272682963

Reviewed By: hx235

Differential Revision: D106201579

Pulled By: xingbowang

fbshipit-source-id: 2331d5cdca064ad901f6af7341b4e8f15b418663
2026-05-25 05:30:49 -07:00
zaidoon a3ba3e8a6e Expose ReadOptions::optimize_multiget_for_io in the C API (#14752)
Summary:
The async MultiGet support introduced two `ReadOptions` flags: `async_io` and `optimize_multiget_for_io`. The setter/getter for `async_io` was exposed in the C API in [ff04fb154](https://github.com/facebook/rocksdb/commit/ff04fb154bd74fd0681baa83b478095207e2719d); the corresponding pair for `optimize_multiget_for_io` was not.

This PR adds `rocksdb_readoptions_set_optimize_multiget_for_io` and `rocksdb_readoptions_get_optimize_multiget_for_io`, mirroring the existing `async_io` pattern exactly.

## Motivation

The flag is consulted in `db/version_set.cc` only inside the `#if USE_COROUTINES` guard, so this setter has no behavioral effect in non-coroutine builds. It matters for:

1. **API parity** with the C++ surface and with the existing `async_io` C API. The two flags were introduced together as part of the async MultiGet feature; only exposing one is an oversight.

2. **CPU/latency tuning in `USE_COROUTINES` builds.** Per the [Asynchronous IO in RocksDB blog post](https://rocksdb.org/blog/2022/10/07/asynchronous-io-in-rocksdb.html), `async_io=true` with `optimize_multiget_for_io=false` (single-level parallel reads) ran 775 μs/op vs 508 μs/op with `optimize_multiget_for_io=true` (multi-level), with the latter incurring additional CPU overhead from coroutine scheduling. Without this setter, coroutine-enabled builds cannot reach the single-level configuration from C.

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

Reviewed By: mszeszko-meta

Differential Revision: D106080144

Pulled By: xingbowang

fbshipit-source-id: 28f12f07f29660392ba6ef7840b22804dab3567b
2026-05-23 04:38:23 -07:00
Peter Dillinger e82af29adb Makefile fix and speed up 'clean' (#14767)
Summary:
* Fix Makefile default target (was ordered after a folly target)
* Improved the speed of `make clean` by using just one `find` and by pruning "hidden" .* and third-party directories that should not be modified anyway.
* Reduce excessive output from `make clean`

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

Test Plan: manual

Reviewed By: mszeszko-meta

Differential Revision: D105972958

Pulled By: pdillinger

fbshipit-source-id: 2c0f6097c74c3129b815450f23c19ef07bfbe656
2026-05-22 17:34:53 -07:00
Peter Dillinger acfa68ccff Fix flaky preallocation tests on btrfs, zfs, tmpfs, and overlayfs (#14744)
Summary:
Fix flaky test failures in EnvPosixTestWithParam.AllocateTest and DBWALTest.TruncateLastLogAfterRecoverWithFlush that occur when running on filesystems where preallocated space is not reliably reflected in st_blocks.

The tests use stat() to check st_blocks and verify that fallocate with FALLOC_FL_KEEP_SIZE actually preallocates disk space. However, on certain filesystems, this check is unreliable:

1. btrfs and zfs: Copy-on-write filesystems where preallocated extents are not reliably reflected in st_blocks, especially under load.

2. tmpfs: Memory filesystem that may not report preallocated blocks in st_blocks.

3. overlayfs: Union filesystem common in containers that may not pass through fallocate properly or report preallocated space.

4. Any filesystem where FALLOC_FL_KEEP_SIZE is not supported: The preallocation will fail silently (error ignored with PermitUncheckedError), leaving only the written data in st_blocks.

Changes made:

env/env_test.cc:
- Added filesystem magic number definitions for TMPFS_MAGIC, OVERLAYFS_SUPER_MAGIC, and ZFS_SUPER_MAGIC
- Extended the AllocateTest to skip block count checks on zfs, tmpfs, and overlayfs in addition to btrfs
- Added runtime fallback: if st_blocks is less than expected, print a warning and skip the check instead of failing. This handles unknown filesystems or configurations where preallocation isn't supported.

db/db_wal_test.cc:
- Added includes and filesystem magic number definitions
- Added ShouldSkipAllocationCheck() helper function to detect problematic filesystems
- Modified TruncateLastLogAfterRecoverWithoutFlush, TruncateLastLogAfterRecoverWithFlush, TruncateLastLogAfterRecoverWALEmpty, and ReadOnlyRecoveryNoTruncate tests to skip allocation checks on problematic filesystems
- Added runtime fallback checks similar to env_test.cc

These changes make the tests robust against filesystem differences while still validating preallocation behavior on filesystems where it works correctly (ext4, xfs).

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

Test Plan: Many local 'make -j100 check' runs that would previously fail with good probability.

Reviewed By: hx235

Differential Revision: D105331256

Pulled By: pdillinger

fbshipit-source-id: 862a1512da1466cb037af15342404939b677c02a
2026-05-22 14:43:23 -07:00
Xingbo Wang 8bf167a194 Notify listeners before DB shutdown begins (#14769)
Summary:
### Notify listeners before DB shutdown begins

db_stress listener bookkeeping correlates compaction callbacks with file deletion callbacks. DBImpl intentionally skips some compaction listener callbacks once shutdown starts, but file deletion callbacks can still be delivered. That mismatch is normally tolerable for production listeners, but db_stress uses listener-local tracking to detect callback consistency and can report a false positive when shutdown interrupts a compaction callback sequence.

The failed-open case is the important gap. DB::Open() can create a DBImpl, recover or flush files, schedule background compaction, and then fail during late open work such as persisting OPTIONS or waiting for open-time compaction under fault injection. At that point DB::Open() tears down the internal DBImpl before returning an error to StressTest::Open(). If OnCompactionBegin already recorded an input file or job in DbStressListener, DBImpl shutdown can suppress the later OnCompactionPreCommit/OnCompactionCompleted callbacks that would normally clear that state. Since control has not returned to the stress harness yet, StressTest::CleanUp()/Reopen() cannot notify the listener in time. A later shutdown-time callback, or the next open retry reusing the same listener, can then observe stale tracking and abort even though RocksDB did not compact the same SST concurrently.

Add EventListener::OnDBShutdownBegin and fire it once from DBImpl::CancelAllBackgroundWork() before publishing shutting_down_. The callback also covers cleanup of a failed DB::Open() attempt, where the DB pointer refers to the internal DBImpl that was never returned to the caller. Track shutdown_notification_sent_ separately from shutting_down_ because listeners are invoked with mutex_ released, and a concurrent or reentrant cancellation must not deliver the callback twice.

Update DbStressListener to consume the DBImpl-driven shutdown notification instead of relying on StressTest::CleanUp()/Reopen() to manually notify it. This lets db_stress mark itself as shutting down before DBImpl starts skipping shutdown-sensitive compaction notifications, including during failed-open cleanup.

### Also keep listener state scoped to one db_stress open attempt.

Initialize listeners at the top of each non-transactional open retry before enabling open fault injection, so retry attempts get fresh listener state without widening open fault injection to listener construction. Factor the open fault setup into a small helper to keep the retry loop readable. The transaction open path still initializes listeners once because it does not use this open-fault retry loop.

### Also fix multi-ops transaction listener checks during shutdown

A TSAN race was reported where MultiOpsTxnsStressListener::OnCompactionCompleted called VerifyPkSkFast on a background compaction thread while the main thread was destroying the transaction DB wrapper in StressTest::CleanUp(). The reported read was a virtual call through the DB object and the write was the WritePreparedTxnDB/WriteUnpreparedTxnDB destructor updating the vptr.

The vulnerable ordering is that DBImpl can already be inside NotifyOnCompactionCompleted before shutdown is requested. It unlocks db mutex while iterating listeners; another listener such as DbStressListener can spend time in its callback, giving the main thread time to enter CleanUp()/Close(). The DB object is still shutting down, but MultiOpsTxnsStressListener may be invoked later in the same callback iteration and call VerifyPkSkFast through stress_test_->db_aptr_, racing with DB wrapper destruction.

With DBImpl-owned EventListener::OnDBShutdownBegin callback, we have MultiOpsTxnsStressListener consume that callback directly. Once DBImpl begins shutdown, the listener skips both flush-completed and compaction-completed verification callbacks, avoiding DB access during teardown. This also covers failed-open cleanup without name-based downcasts in StressTest.

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

Test Plan: CI

Reviewed By: pdillinger

Differential Revision: D106011452

Pulled By: xingbowang

fbshipit-source-id: 768838ddcd9910de5d1b5204c990a4d88dbc850c
2026-05-22 12:32:11 -07:00
Hui Xiao 97551b72d7 Per-StressTest fault injection with env/fs cleanup (#14757)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14757

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

Prerequisite for multi-DB stress test support where each StressTest instance owns one DB. Moves fault injection from a single global to per-StressTest instance so each DB gets isolated fault injection; errors injected into one DB do not leak to others.

FS/Env architecture change:

  Before (upstream):
    raw_fs → FaultInjectionTestFS (global) → DbStressFSWrapper → db_stress_env → DB::Open

  After (this diff):
    Global: raw_env (no wrappers)
      Used outside StressTest: non-FS ops (threads, time, sleep), test framework FS setup (dirs), DB destruction
      Used inside StressTest: cleanup ops that must succeed (external file delete)

    Per StressTest:
      raw_fs → DbStressFSWrapper (db_stress_fs_, always)
                → FaultInjectionTestFS (db_fault_injection_fs_, optional)
                      → CompositeEnvWrapper (db_env_, always) → DB::Open

    Expected values state: Env::Default() (always local PosixEnv even when raw_env is remote)

FS layer order swapped (DbStressFSWrapper now innermost). Safe because:
- Error injection returns early; inner wrapper never executes (same behavior)
- DbStressFSWrapper assertions do not modify data (checksum validation, IOActivity checks)
- MANIFEST rename tracking slightly better for crash simulation in new order

Stored members (per StressTest):
- db_stress_fs_: DbStressFSWrapper. Always active regardless of fault injection flags.
- db_fault_injection_fs_: FaultInjectionTestFS. Only when fault injection flags set. Direct access for Enable/Disable/SetThreadLocal.
- db_env_: CompositeEnvWrapper. Always present. THE env for all DB I/O (options_.env).

Eliminated globals: db_stress_env → renamed to raw_env (no wrappers, DbStressFSWrapper moved to per-StressTest); db_stress_listener_env, db_stress_raw_fs removed; fault_fs_guard, fault_env_guard moved to per-StressTest.

Other changes:
- CleanupOutputDirectory simplified (uses raw_env, no disable/enable needed)
- SstFileManager recreated with per-StressTest env in Open()
- Remote compaction override env uses options.env directly (fixes pre-existing silent bug)
- Comments added: Env::Default() always local, DbStressDestroyDb MANIFEST explanation, fault injection log path in TEST_TMPDIR
- TestFSWritableFile::Close() now mirrors the production FSWritableFile close boundary. After the first Close() attempt, later explicit or destructor Close() calls are wrapper-level no-ops, while FaultInjectionTestFS still records the first close attempt for crash/recovery simulation.
- Added targeted fault_injection_fs_test coverage for injected metadata-close failures to ensure FaultInjectionTestFS does not retry the inner Close() path.

Reviewed By: anand1976

Differential Revision: D104959945

fbshipit-source-id: 7cf9bb494dec2b372528d5f119c023b6d392ffca
2026-05-21 18:39:18 -07:00
Maciej Szeszko d1ab4bf12c Guard db_bench DB lifetime with RWMutex against shutdown race (#14754)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14754

**Problem**

When db_bench shuts down (fatal IO error, unknown benchmark name, etc.), ErrorExit() or ~Benchmark() destroys db_/multi_dbs_ while worker threads may still be inside benchmark methods holding raw DBWithColumnFamilies pointers returned by SelectDBWithCfh(). In single-DB mode, db_.db becomes nullptr and multi_dbs_ is empty, so workers evaluate rand_int % 0 and SIGFPE. The cascade across all workers masks whatever originally triggered the shutdown. Observed in the reliable_volumes crash-fault test at ~5 SIGFPE events per hour.

**Approach**

Guard DB lifetime at the worker-thread boundary with a reader-writer lock (port::RWMutex): workers acquire a read lock for the duration of their benchmark method; mutators acquire the write lock (which waits for active readers to drain) before destroying DBs. This ensures raw pointers returned by SelectDBWithCfh() remain valid for their entire usage scope.

**Fix**

- Workers hold a DbUseGuard (RAII wrapper over a read lock on db_lifecycle_rwlock_) for the entire benchmark method in ThreadBody. Cost: one rdlock/unlock pair per worker lifetime, zero hot-path overhead.

- Every DB mutation path (ErrorExit, ~Benchmark, fresh-DB reopen) takes DbStateMutationGuard, which acquires the write lock and then stops the secondary update thread before mutation may proceed.

- ErrorExit() routes to std::_Exit(1) when called from any thread that cannot safely run the mutation cleanup path: a DbUseGuard holder (would self-wait on the write lock) or the secondary update thread (would self-join via StopSecondaryUpdateThread). Tracked via two thread-locals: holds_db_use_guard_ and is_secondary_update_thread_. Main thread takes DbStateMutationGuard, cleans up, dispatches through db_bench_exit() / ToolHooks::Exit.

- StopSecondaryUpdateThread() resets secondary_update_stopped_ to 0 after joining, so a replacement thread created by a subsequent Open() is not immediately killed.

- Protocol is mechanically enforced in debug builds:
    * SelectDBWithCfh asserts holds_db_use_guard_  (read-side ownership)
    * DeleteDBs        asserts holds_db_state_mutation_guard_ (write-side ownership)
    * DbUseGuard and DbStateMutationGuard ctor/dtor assert correct imbalance (no nested or stray release).
    * DbStateMutationGuard ctor additionally asserts !holds_db_use_guard_ and !is_secondary_update_thread_, catching the new deadlock modes the single-lock design makes reachable (WriteLock while holding ReadLock; secondary thread self-joining via StopSecondaryUpdateThread).
  These convert "trust me" invariants into "the assert fires if you break it." Direct field accesses (e.g. db_.db->NewIterator inside a benchmark method, multi_dbs_.clear() in the reopen branch) are protected by the enclosing guard scope but are not per-site asserted.

**Caveat**

port::RWMutex is pthread_rwlock_t with default attrs -> reader-preferred on Linux/glibc. The current call graph has no concurrent worker spawn during mutator wait (mutator paths run only from the main thread, not concurrently with RunBenchmark spawning workers), so writer starvation is not reachable. Documented as a constraint; revisit if that invariant changes. The port layer doesn't expose pthread_rwlockattr_setkind_np cross-platform.

**Alternatives considered**

- std::_Exit(1) on every shutdown path, skip cleanup entirely. Loses ToolHooks::Exit dispatch, flushed traces, and end-of-run stats -- those matter for the RV crash-fault test image which consumes db_bench output. Rejected.

- shared_ptr<DBWithColumnFamilies> from SelectDBWithCfh, let DB lifetime extend naturally to the last reader. Adds a per-call atomic refcount bump in the hot path; the RWMutex approach is per-method-call instead of per-op, making the hot-path cost zero. Rejected for hot-path neutrality.

Reviewed By: xingbowang

Differential Revision: D104974784

fbshipit-source-id: 3a04d9e1c0b5042436d690f573cf369de6b4c9df
2026-05-21 11:49:59 -07:00
Xingbo Wang e42af37ea7 Avoid reusing db_stress listeners across open retries (#14765)
Summary:
- Rebuild db_stress event listeners through a shared `InitializeListenersForOpen()` helper.
- Reinitialize listeners before retrying `DB::Open()` after injected open or open-compaction failures.

## Context
- A stress test failed with "Concurrent compaction of SST file detected".
- Root cause: StressTest::Open() built DbStressListener once before its DB::Open() retry loop. With open fault injection, a DB::Open() attempt can create a DBImpl, schedule background compaction, and then fail during late open work such as persisting OPTIONS. During teardown of that failed DBImpl, DBImpl's shutdown flag can suppress later compaction callbacks, leaving listener-local compaction bookkeeping stale.
- Diagnosis: Sandcastle DB LOGs showed file 16821 flushed, then a failed open attempt with an injected read error and a background compaction picking 16821. The crash was in DbStressListener::OnCompactionBegin, so this was stale db_stress listener state across open attempts rather than DBImpl allowing a real concurrent compaction of the same SST.
- Fix: factor listener construction into InitializeListenersForOpen() and call it before each DB::Open() attempt, including the retry path after open/open-compaction failure. Each DBImpl open attempt now gets fresh listener state.
- Verification: make clean; make db_stress -j192; make check-sources; git diff --check.

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

Test Plan:
- `make clean`
- `make db_stress -j192`
- `make check-sources`
- `git diff --check`

Reviewed By: pdillinger

Differential Revision: D105969381

Pulled By: xingbowang

fbshipit-source-id: 759e2be7e1215a498ed449ab36f13e8c7975f4a4
2026-05-21 10:21:02 -07:00
xingbowang 54d13c0af9 Fix DBTest cleanup for alternate log dirs (#14764)
Summary:
- Clean DBTestBase's fixture-owned alternate WAL and db_log_dir paths during setup and teardown.
- Prevent kDBLogDir option tests from leaving dbname_/db_log_dir behind and polluting later DBTest cases in the same gtest shard.
- Preserve production DestroyDB() behavior while making the test fixture cleanup complete.

Context:
- The ARM nightly failure was exposed by the 32-shard db_test layout running DBTest.GetPicksCorrectFile before DBTest.PurgeInfoLogs in the same process.
- GetPicksCorrectFile can use kDBLogDir, which creates logs under dbname_/db_log_dir. DestroyDB() intentionally ignores DeleteDir() failures when unknown children remain, so the next fixture could observe dbname_ still existing.

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

Test Plan:
- make -j14 db_test
- TEST_TMPDIR=/tmp/rocksdb_arm_fix_test ./db_test --gtest_filter=DBTest.GetPicksCorrectFile:DBTest.PurgeInfoLogs
- TEST_TMPDIR=/tmp/rocksdb_arm_fix_test_shard GTEST_TOTAL_SHARDS=32 GTEST_SHARD_INDEX=18 ./db_test

Reviewed By: mszeszko-meta

Differential Revision: D105860476

Pulled By: xingbowang

fbshipit-source-id: b2685063ca6c4eedec589697b439fe4aee4eda1a
2026-05-21 09:09:17 -07:00
Hui Xiao 2904bc64dc Encapsulate path access in StressTest via accessors (#14756)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14756

Pure mechanical refactor: replace all direct FLAGS_db / FLAGS_expected_values_dir / FLAGS_secondaries_base reads with accessor methods on StressTest. No new flags, no parameters, no behavior change. Prepares for multi-DB stress test where each StressTest instance has its own DB.

Changes:
- GetDbPath(), GetExpectedValuesDir(), GetSecondariesBase() accessors return the corresponding FLAGS values directly
- Replace ~15 FLAGS_db references with GetDbPath() in db_stress_test_base.cc
- Move SharedState constructor from .h to .cc (needs full StressTest type for GetExpectedValuesDir())
- Move DbStressListener constructor from .h to .cc (same reason)
- Replace FLAGS_db / FLAGS_expected_values_dir in db_stress_driver.cc with accessor calls
- NO changes to db_crashtest.py or db_stress_gflags.cc

Reviewed By: anand1976

Differential Revision: D104959943

fbshipit-source-id: d7ef6a39d4c2ed467b2960417629c09f3988faf5
2026-05-20 16:52:40 -07:00
Anand Ananthabhotla 88d7d2df75 Enable MANIFEST optimization options in db_crashtest.py and add db_stress verification (#14742)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14742

This change enables testing of the two new MANIFEST optimization options introduced in D103568447:
1. optimize_manifest_for_recovery - Skips unnecessary MANIFEST edits during recovery
2. reuse_manifest_on_open - Reuses existing MANIFEST file on DB open

Changes:
- tools/db_crashtest.py: Add both options with 20% probability to default_params
- db_stress_tool/db_stress_test_base.h: Add ManifestVerifyMode enum and member variables for tracking MANIFEST state
- db_stress_tool/db_stress_test_base.cc: Implement RecordManifestStateBeforeReopen() and VerifyManifestNotRewritten() methods to validate MANIFEST reuse on DB reopen

The verification logic handles 4 combinations:
- Both disabled: No verification (baseline)
- Only optimize enabled: No verification (hard to measure without sync points)
- Only reuse enabled: Verify MANIFEST file is reused
- Both enabled: Verify MANIFEST reused AND CURRENT unchanged

Verification is warning-only (not fatal) to account for legitimate fallback cases like corruption or size limits.

Reviewed By: hx235

Differential Revision: D105224068

fbshipit-source-id: 5baa65680fdd639674d87ff1e9187b743e691bc1
2026-05-20 11:27:27 -07:00
Hui Xiao 367f2b0fdf Resumable Remote Compaction Blog Post (#14759)
Summary:
**Summary:** as titled

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

Test Plan:
local server rendering test
<img width="914" height="764" alt="Screenshot 2026-05-19 at 2 18 41 PM" src="https://github.com/user-attachments/assets/0e5353d4-df41-476c-8902-28d7975330d6" />
<img width="914" height="764" alt="Screenshot 2026-05-19 at 2 18 59 PM" src="https://github.com/user-attachments/assets/a0f8692d-6ca8-4c67-b7a0-b47146080c57" />
<img width="895" height="477" alt="Screenshot 2026-05-19 at 2 19 20 PM" src="https://github.com/user-attachments/assets/7479dd54-5bc4-41da-99f3-f081a714ba7c" />

Reviewed By: jaykorean

Differential Revision: D105665739

Pulled By: hx235

fbshipit-source-id: 0701a627eb2b18b9bfd3dd22397aeae9553f1903
2026-05-19 14:35:43 -07:00
Peter Dillinger 07a5a0a804 Fix "too many open files" failures in GitHub CI (#14755)
Summary:
seen several times in the build-linux-mini-crashtest job. Raise ulimit in container spec.

Task: T271298423

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

Test Plan: look at reported ulimits, watch CI

Reviewed By: hx235

Differential Revision: D105664569

Pulled By: pdillinger

fbshipit-source-id: 6f7f3976bd73ecc86509ac955d5e190316e98ba3
2026-05-18 23:15:12 -07:00
Peter Dillinger 01d8c7720f Fix crash test failures in OnCompactionPreCommit listener checks (#14753)
Summary:
NotifyOnCompactionPreCommit initially omitted the shutting_down_ guard to avoid a false positive abort in the db_stress OnTableFileDeleted check, where stale compacting_files_ entries from skipped notifications would be mistaken for a bug. The better fix is to add the shutting_down_ guard for consistency with Begin and Completed, and instead make OnTableFileDeleted tolerate stale tracking during shutdown by checking a new atomic bool in the listener intended to track DBImpl's shutting_down_.

Also fix lint: release mutex before RandomSleep() in PreCommit; use char literal for find_last_of; avoid unnecessary string copy.

Document WART in listener.h: all three compaction callbacks are skipped during DB shutdown, so a committed compaction may go unobserved.

Bonus: update CLAUDE.md with instructions on avoiding non-ASCII characters

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

Test Plan: manually trigger many crash test runs

Reviewed By: hx235

Differential Revision: D105591913

Pulled By: pdillinger

fbshipit-source-id: 32029fea4c2571d88f645eb325db2e25a94e0d26
2026-05-18 22:31:46 -07:00
Hui Xiao cba33621bd Fix BackgroundJobPressure flakiness with TEST_WaitForBackgroundWork (#14708)
Summary:
BackgroundJobPressure test was flaky because Phase 3 used TEST_WaitForCompact() which waits for bg_compaction_scheduled_ but NOT bg_pressure_callback_in_progress_. The final "healthy" pressure callback could still be in-flight when the test checked snapshots.back(), causing compaction_scheduled=1 instead of 0.

Fix: replace TEST_WaitForCompact() with TEST_WaitForBackgroundWork() which explicitly checks bg_pressure_callback_in_progress_ (db_impl.cc:478-484). Also add TEST_WaitForBackgroundWork() before Phase 1 and Phase 2 snapshot checks for consistency, ensuring all pressure callbacks are delivered before assertions.

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

Reviewed By: jaykorean

Differential Revision: D103784101

fbshipit-source-id: 275802b5bb70094af62486bde26b599a292e71fa
2026-05-18 17:05:57 -07:00
Hui Xiao 554a123295 Clean up failed regression test workdirs (#14751)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14751

`regression_test.sh` already cleans the current run's `TEST_PATH` on normal success and on the early-exit path when a recent `db_bench` is still running. But a hard benchmark failure goes through `exit_on_error`, which exits before the end-of-`main()` cleanup runs.

This change adds an `EXIT` trap and a single-shot finalizer so the current invocation still cleans its own `TEST_PATH` on hard failure. It does not reintroduce any sibling-directory cleanup, and it preserves the existing success-path cleanup, early-exit cleanup, and debug preservation behavior.

| Scenario | Previous code | New code |
| --- | --- | --- |
| Normal success | Cleans current `TEST_PATH` at end of `main()` | Still cleans current `TEST_PATH` |
| Hard benchmark failure via `exit_on_error` | Can leave current `TEST_PATH` behind | `EXIT` trap cleans current `TEST_PATH` |
| Early exit because recent `db_bench` exists | Cleans current `TEST_PATH`, exits `2` | Same behavior |
| Debug mode / `DELETE_TEST_PATH=0` | Preserves artifacts | Same behavior |

Reviewed By: jaykorean

Differential Revision: D105411220

fbshipit-source-id: 37a335b87faaeee86d44ef2e24bebf1b7b9626d6
2026-05-18 16:24:56 -07:00
Maciej Szeszko 459236341c Start development 11.4 (#14747)
Summary:
* Release notes from 11.3 branch
* Update version.h
* Add [11.3.fb](https://github.com/facebook/rocksdb/tree/11.3.fb) (to check_format_compatible.sh)
* Update folly commit hash to: https://github.com/facebook/folly/releases/tag/v2026.05.11.00 (see [comment](https://github.com/facebook/rocksdb/pull/14747#issuecomment-4480217173) for more)
* Copyright headers refresh
* Decouple 11.1.fb from 11.2.fb in `db_forward_with_options_refs`
* Add validation step in `check_format_compatible.sh` to prevent gluey release strings

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

Reviewed By: xingbowang

Differential Revision: D105443941

Pulled By: mszeszko-meta

fbshipit-source-id: 8499b18cc2ff118d805b3865463ad2a999868de4
2026-05-18 13:35:11 -07:00
Peter Dillinger c48b020e92 Speed up parallel 'make check' scheduling (#14745)
Summary:
Reduce wall-clock time of parallel 'make check' by improving the scheduling and granularity of slow test binaries.

Three Makefile changes:

1) Refresh slow_test_regexp with current observed bottlenecks. Adds
   binaries (point_lock_manager_stress_test, compaction_service_test,
   corruption_test, comparator_db_test, external_sst_file_basic_test,
   rate_limiter_test, db_compaction_test, db_merge_operator_test,
   db_dynamic_level_test, db_bloom_filter_test, error_handler_fs_test,
   merge_helper_test, db_kv_checksum_test, inlineskiplist_test) whose
   shards take >=15s but were not being front-loaded for early
   queueing. Also drops stale FIXME comments that no longer apply
   and adds tier annotations + a maintenance recipe.

2) Add SHARD_SIZE_OVERRIDES, a per-binary override of GTEST_SHARD_SIZE,
   so binaries with slow individual tests (e.g.
   point_lock_manager_stress_test where each test is ~10s) can be
   chopped into more, smaller shards. The default of 10 stays for
   everything else. Each shard's effective size is reported in the
   'Generating ... shards for ...' line.

3) Add 'make suggest-slow-tests' to print a per-binary aggregation of
   the most recent LOG (max single-shard time, total time, shard
   count) for any binary worth attention. Used to maintain the regex
   and override list above.

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

Test Plan:
Two runs each of 'make -j166 check', before and after this change (all compilation already finished):
Before: 197s and 198s
After: 123s and 125s
Reduction: 37%

Reviewed By: xingbowang

Differential Revision: D105332444

Pulled By: pdillinger

fbshipit-source-id: 1d1c2f89a32647e6651e2ffeb72da9d51bcc004f
2026-05-18 09:07:08 -07:00
Maciej Szeszko 3daabe2db3 Fix SeekForPrev key pinning regression in FindValueForCurrentKeyUsingSeek (#14746)
Summary:
In PR https://github.com/facebook/rocksdb/issues/13531, we added a `saved_key_.SetUserKey(ikey.user_key)` call in `FindValueForCurrentKeyUsingSeek` to fix unprepared-value reverse iteration. The default `copy`=`true` parameter unconditionally copies the key into the internal buffer, breaking is-key-pinned when `pin_data`=`true`. This path triggers only when a key has more versions than `max_sequential_skip_in_iterations` (default 8 ), making the bug rare and hard to repro deterministically.

**Fix:** pass `!pin_thru_lifetime_ || !iter_.iter()->IsKeyPinned()` as the copy parameter, matching every other `SetUserKey` call site in `DBIter`.

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

Test Plan: New test `SeekForPrevKeyPinnedWithManyVersions`: writes 20 versions of the same key, confirms `FindValueForCurrentKeyUsingSeek` is taken via `NUMBER_OF_RESEEKS_IN_ITERATION`, asserts is-key-pinned == "1" after `SeekForPrev`. Fails without fix, passes with.

Reviewed By: xingbowang

Differential Revision: D105397906

Pulled By: mszeszko-meta

fbshipit-source-id: 5581105e9d929bb4c582c52dd6a7ae3e8dd9da72
2026-05-16 11:17:05 -07:00
Peter Dillinger 805a476a5f Add OnCompactionPreCommit listener callback (#14740)
Summary:
Adds a new `EventListener::OnCompactionPreCommit` callback that fires after a compaction job's output files are written but *before* the manifest write commits the new Version. At that point input files still have `FileMetaData::being_compacted == true`, so listeners that maintain bookkeeping of "files currently being compacted" can clean up that state without racing the compaction picker.

A check implemented by a Meta-internal RocksDB user crashes when the same file appears as input to two concurrent compactions. Tracking that set in `OnCompactionBegin` / `OnCompactionCompleted` produces false positives because `Compaction::ReleaseCompactionFiles()` flips `being_compacted` back to false before `OnCompactionCompleted` fires, so another thread can pick the same file and trigger `OnCompactionBegin` before the previous compaction's `Completed` callback runs. Doing the cleanup in `OnCompactionPreCommit` closes that race. Default implementation is a no-op, so no API break.

A trivial refactoring to split PerformTrivialMove ensures data is populated for the new callback, while calling back before the trivial move compaction is committed.

Bonus: `CLAUDE.md` update for when to call `make clean` as I've recently had it get thoroughly confused TWICE mixing build modes.

Task: T269479969

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

Test Plan:
- New unit test in `db/listener_test.cc` checks that `OnCompactionPreCommit` fires strictly between `OnCompactionBegin` and `OnCompactionCompleted` for the same compaction, and that input files still have `being_compacted == true` at the time it fires.
- Crash test: adds a concurrent-compaction sanity check to `db_stress`'s listener resembling the Meta-internal intended usage: tracks input file numbers from `OnCompactionBegin` to `OnCompactionPreCommit`, aborting if the same file appears as input to two concurrent compactions. Also checks other ordering constraints and checks for "leaks" from possible failure to call OnCompactionPreCommit(). Exercises all compaction styles and trivial-move/FIFO paths under load.

Reviewed By: hx235

Differential Revision: D105065326

Pulled By: pdillinger

fbshipit-source-id: 8f606a7c0fac899574e7340c600b71fed902394a
2026-05-15 16:08:23 -07:00
Xingbo Wang 21723bbbef Add async WAL precreation
Summary:
- Add experimental immutable `DBOptions::async_wal_precreate` to reserve and open one future WAL on a background HIGH-priority task, with sanitization that disables the optimization when WAL recycling is configured.
- Split WAL creation into open/preallocate and start phases so `SwitchMemtable()` can consume a prepared WAL after writing normal WAL metadata, wait for in-flight precreation, fall back to synchronous creation, and delete an unstarted prepared WAL on start failure.
- Keep WAL numbering, close, recovery, and read-only open safe for empty future WAL files left by async precreation; `error_if_wal_file_exists=true` now rejects non-empty WALs while tolerating empty WALs.
- Add public option plumbing for the C API, options parsing/stringification, random option testing, `db_bench`, `db_stress`, and crash-test configuration.
- Add WAL precreate statistics counters plus Java `TickerType`/JNI mappings, and update C++, C, and Java read-only-open documentation for the empty-WAL behavior.
- Add focused WAL/option/C/Java tests for async precreate ready/wait/failure/recovery paths, read-only WAL detection, option sanitization, and API plumbing, plus write-flow docs and unreleased history entries for the new feature and behavior change.

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

Reviewed By: pdillinger

Differential Revision: D105020559

fbshipit-source-id: 5059b424702e021abb8de65ceeb6d3b975280ffc
2026-05-15 10:59:47 -07:00
Hui Xiao a2c96df7d7 Add listener_uri stress test flag for pluggable EventListener (#14741)
Summary:
**Summary:**
Adds a --listener_uri flag to db_stress that creates an EventListener via ObjectLibrary from the given URI and attaches it to the DB options.

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

Test Plan:
- Compilation
- e2e test will be done next internally with a customized listener

Reviewed By: anand1976

Differential Revision: D104750476

Pulled By: hx235

fbshipit-source-id: cdc00191de6b7434e4b373db30769ff34d99b80d
2026-05-14 17:33:29 -07:00
Peter Dillinger 87c554b492 Persist compacted manifest size for auto-tuning across DB::Open (#14725)
Summary:
last_compacted_manifest_file_size_ drives TuneMaxManifestFileSize() to compute the manifest rotation threshold, but it started at 0 on every DB::Open and was only populated after the first manifest rotation. This is really only a problem with reuse_manifest_on_open, because no fresh manifest is created on open.

Add a new forward-compatible (safe-to-ignore) MANIFEST tag kLastCompactedManifestFileSize that records the approximate compacted manifest size at the end of WriteCurrentStateToManifest. During recovery, the value is loaded and used to immediately tune the rotation threshold.

The record includes a rough estimate of its own overhead (~15 bytes) and must be the last record written by WriteCurrentStateToManifest for accurate estimation.

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

Test Plan:
Extended AutoTuneManifestSize in db_etc3_test to close and reopen with reuse_manifest_on_open after establishing a known auto-tuning state. Verifies that the manifest file number is preserved (no spurious rotation) and that subsequent CF additions don't trigger rotation -- proving the persisted compacted size keeps the tuned threshold correct. Verified the test fails when the recovery loading is disabled.

Relax a fragile Java test that was dependent on the exact size of the manifest file.

SHORT_TEST=1 ./tools/check_format_compatible.sh

Reviewed By: anand1976

Differential Revision: D104464522

Pulled By: pdillinger

fbshipit-source-id: 4f5d22d2e149bd40a523ee11780e5e3344803c19
2026-05-13 18:31:49 -07:00
Xingbo Wang cbd61a3165 Add parser for raw table iterator keys (#14726)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14726

Add SstFileReader::ParseTableIteratorKey() so callers of NewTableIterator() have a public way to decode raw table keys without duplicating RocksDB internal-key layout. The implementation delegates to the existing public ParsedEntryInfo parser using the reader comparator.

Reviewed By: pdillinger

Differential Revision: D104584393

fbshipit-source-id: 98e21c4d6676fbba69e533376b3da67539dd8fad
2026-05-12 16:26:18 -07:00
Peter Dillinger 59633e36ed Bug fix: Reject empty string as a column family name (#14732)
Summary:
Previously, calling `DB::CreateColumnFamily(opts, "", &handle)` returned `Status::OK()` with a usable handle, but the column family was not persisted in the manifest. Any data written to the empty-named CF was silently lost on DB reopen, and `ListColumnFamilies` would not show it.

The empty string is also reserved as a sentinel meaning "no/unknown column family" in various RocksDB APIs and serialization formats (e.g. `TablePropertiesCollectorFactory::Context::kUnknownColumnFamily` and table properties), so allowing it as a real CF name is ambiguous in addition to being broken.

This change rejects an empty CF name with `Status::InvalidArgument` at the top of `DBImpl::CreateColumnFamilyImpl`, which covers the single-CF `CreateColumnFamily` API as well as both `CreateColumnFamilies` overloads (by-names and by-descriptors).

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

Test Plan: * Added `ColumnFamilyTest.EmptyNameRejected` covering all three Create entry points; verifies `IsInvalidArgument()` and that no spurious handles are returned.

Reviewed By: hx235

Differential Revision: D104753911

Pulled By: pdillinger

fbshipit-source-id: e52d792830b965484a618f4e55981eee4eb6f515
2026-05-12 16:06:58 -07:00
Xingbo Wang 4707775ae9 Fix GetContext status propagation and blob-backed wide-column merge operands (#14640)
Summary:
- propagate lower-level read and merge failures through `GetContext` via `read_status`, so `Get` and `GetEntity` preserve the original error instead of synthesizing `Corruption` when blob-backed reads or merge resolution fail
- teach `GetMergeOperands` to resolve blob-backed default columns from wide-column entities, covering both the direct base-value path and the merge-plus-base path
- add regression coverage for blob-read IO errors during `Get`/`GetEntity` merge resolution and for `GetMergeOperands` on blob-backed wide-column entities
- fix the `DBFlushTest.MemPurgeCorrectLogNumberAndSSTFileCreation` test race by waiting for flush callbacks and cleaning up sync points

## Testing

- `make db_blob_basic_test -j14`
- `/usr/bin/perl -e 'alarm shift; exec ARGV' 60 ./db_blob_basic_test --gtest_filter='DBBlobBasicTest/DBBlobBasicIOErrorTest.GetBlob_IOError/*:DBBlobBasicTest/DBBlobBasicIOErrorTest.GetEntityMergeWithBlobBaseIOError/*'`

## Task
T265824017, T265415808

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

Reviewed By: anand1976

Differential Revision: D101690700

Pulled By: xingbowang

fbshipit-source-id: 2b6fc357b37a01efa72a2d54dcff55be8992f42a
2026-05-12 15:29:27 -07:00
Maciej Szeszko 46ce1a03a9 Cap Claude thinking budget (#14731)
Summary:
### Motivation
Claude's auto review workflow classifies the PR as complex and sets `MAX_THINKING_TOKENS`=**32000** in `.github/workflows/ai-review-analysis.yml:534`. The Claude action then sends a request where `thinking.budget_tokens` is **32000**, but the request `max_tokens` is not greater than that, so Anthropic rejects it before the review starts ([example](https://github.com/facebook/rocksdb/actions/runs/25691112276/job/75427435133)).

### Fix
Reduce the "complex" thinking budget from **32000** to **24000** tokens and clamps manual overrides to the same ceiling, preventing the thinking budget from exceeding or equalling the action's effective `max_tokens`. 24k is still generous — enough for deep reasoning on complex PRs while guaranteeing the model can emit the formatted review.

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

Reviewed By: xingbowang

Differential Revision: D104749426

Pulled By: mszeszko-meta

fbshipit-source-id: bf94d0f50e7c2c9bc9e4d4cdffd61087d739018a
2026-05-11 17:45:43 -07:00
Peter Dillinger 795f3bd61f Fix check-sources.sh non-ASCII check and remove non-ASCII from sources (#14729)
Summary:
The non-ASCII character check in check-sources.sh used git grep -P (Perl regex), which requires git compiled with PCRE support. On systems without it, the command fails with exit code 128, which is != 1 (no match), so the check always reported a violation -- effectively dead.

Even in CI where git has PCRE2 support, the check was silently broken: git grep -P uses PCRE2 in UTF mode by default, which interprets [\x80-\xFF] as a Unicode codepoint range (U+0080 to U+00FF). Characters like em-dash (U+2014), arrows (U+2192), and math symbols (U+2248, etc.) fall outside that range and were not detected. Only Latin-1 Supplement characters (U+0080-U+00FF) would have been caught.

Replace with LC_ALL=C git grep using bash $'[\x80-\xff]' literal byte range, which works with basic regex in the C locale, and replace all non-ASCII characters in non-excluded source files:
- em-dash to --
- arrow to ->
- math symbols to ASCII equivalents (~=, <=, >=)
- box-drawing characters to ASCII art

Also exclude .github/ from the check, as scripts there can use non-ascii without disrupting RocksDB builds on non-UTF-8 systems.

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

Test Plan: manual / CI (make check-sources passes clean)

Reviewed By: hx235

Differential Revision: D104692574

Pulled By: pdillinger

fbshipit-source-id: 1d884c21056dcd83558b825a04b867f1c08e3f45
2026-05-11 17:02:22 -07:00
Maciej Szeszko 330962bff6 CI: Make Codex complexity classification non-fatal (#14721)
Summary:
Previously, `Classify PR complexity (Codex)` ran under `bash -e`, so any `codex exec` failure aborted the entire Codex review before the real review step could run. The classifier only selects the review budget, so on failure we now log the classifier output tail, default to the `complex` review budget, and continue. This keeps actual Codex review failures visible through the existing review exit-code/log handling while preventing the auxiliary classifier from blocking review generation.

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

Reviewed By: xingbowang

Differential Revision: D104326081

Pulled By: mszeszko-meta

fbshipit-source-id: c17388bbf576f71ff85ded320e0740f89072f8c1
2026-05-11 10:59:26 -07:00
Josh Kang e07ccc3528 block GetCreationTimeOfOldestFile on async file open completion (#14723)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14723

### Context

`GetCreationTimeOfOldestFile()` assumed `max_open_files` = -1 meant every live SST had a pinned table reader. That is not true with `open_files_async`: recovery intentionally skips loading table files, `DB::Open()` returns, and `BGWorkAsyncFileOpen()` pins readers later. Any caller — e.g. fb_rocksdb's daily report at `FbRocksDb.cpp:1025` — invoking the API in the window between `DB::Open` returning and the background opener completing trips a debug assert. Reported in https://fb.workplace.com/groups/rocksdb/permalink/31668956482726231/.

Removing the assert alone is insufficient. For legacy DBs whose manifest does not carry `file_creation_time`, `FileMetaData::TryGetFileCreationTime()` falls back to the pinned reader; with no reader, it returns `kUnknownFileCreationTime` and the function silently returns 0 (the "info unavailable" sentinel). The caller cannot distinguish "no info" from "raced with async open."

### Changes

- Remove the invalid debug assert in `Version::GetCreationTimeOfOldestFile`.
- Add a private helper `DBImpl::WaitForAsyncFileOpen()` that blocks on `bg_cv_` while `bg_async_file_open_state_ == kScheduled`. The synchronization machinery (`bg_async_file_open_state_` + `bg_cv_`) already exists — the destructor wait loop in `db_impl.cc:674-687` uses the same pattern. The helper is a no-op when `open_files_async = false`, and bails on `shutting_down_` so `DB::Close()` is not blocked by an in-flight caller.
- Call `WaitForAsyncFileOpen()` at the top of `DBImpl::GetCreationTimeOfOldestFile()` (inside the `max_open_files == -1` branch).
- Document the blocking behavior in `include/rocksdb/db.h`.
- Replace the regression test with one that uses a `DBImpl::WaitForAsyncFileOpen::BeforeWait` sync point: spawn a thread that calls `GetCreationTimeOfOldestFile`, deterministically confirm it blocks inside the wait, release async open, confirm the caller wakes with the real value.

### Potential Followups (not included here)

- Apply the same wait to `GetLiveFilesMetaData` and `GetColumnFamilyMetaData` — both zero out `oldest_ancester_time` / `file_creation_time` in the SST metadata they return during the async-open window (`db/version_set.cc:7877-7878`, `2090-2091`, `2172-2173`).
- Address compaction-picker effects: TTL/periodic file selection (`db/version_set.cc:4039`, `4092-4094`), bottommost over-marking (`db/version_set.cc:4700-4701`), FIFO TTL/temperature pickers (`db/compaction/compaction_picker_fifo.cc:105-107`, `167-170`, `401-411`), and tiered-compaction output time inheritance (`db/compaction/compaction.cc:981`, `1000`).
- Harden `FbRocksDb.cpp:1025` to check `status.ok()` instead of `status.code() != kNotSupported`.

Reviewed By: mszeszko-meta

Differential Revision: D104285992

fbshipit-source-id: ea46375ea1b3ba77fe6b548071aee1101ac0da77
2026-05-08 19:13:20 -07:00
xingbowang 224e849e8d env: suppress liburing TSAN false positives (#14710)
Summary:
- Use the liburing TSAN suppressions in `tools/tsan_suppressions.txt` instead of defining the process-wide `__tsan_default_suppressions()` hook, avoiding conflicts with downstream applications.
- Wire RocksDB TSAN make and crash-test flows to use that suppressions file by default without overriding caller-provided `TSAN_OPTIONS`.
- Cover direct `db_crashtest.py` launches by passing the default suppressions to `db_stress` subprocesses.
- Fix the GCC 16 unity-build warning in `CacheItemHelper` by directly initializing the no-secondary-cache helper fields instead of delegating with `this`.

Imported from D101303486.

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

Test Plan:
- `COMPILE_WITH_TSAN=1 make -j128 env_test`
- `timeout 60s ./env_test --gtest_filter=EnvPosixTest.IOUringAddressReuseNoTsanFalsePositive`
- `python3 tools/db_crashtest_test.py`
- `python3 -m py_compile tools/db_crashtest.py tools/db_crashtest_test.py`
- CI: `build-linux-unity-and-headers` passed after the `CacheItemHelper` fix

Reviewed By: hx235

Differential Revision: D104103800

Pulled By: xingbowang

fbshipit-source-id: 6066d9abe02a3c44d75f9ce449889468c927ce56
2026-05-07 16:40:56 -07:00
Anand Ananthabhotla c734b7cc60 Add reuse_manifest_on_open DBOption (#14704)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14704

Add an immutable DBOption `reuse_manifest_on_open` (default false). When enabled, `DB::Open` can keep using the recovered MANIFEST for the first post-open metadata update instead of rebuilding a fresh MANIFEST, which can reduce warm-open latency for DBs whose MANIFEST is expensive to regenerate.

Reuse is still best-effort. If RocksDB cannot safely resume appending to the recovered MANIFEST, it falls back to the existing fresh-MANIFEST path. The option is also disabled under `best_efforts_recovery`.

This diff also teaches the reopened MANIFEST writer to adopt the existing file size before appending, documents the small-`max_manifest_file_size` caveat for the reused path, and keeps the full warm-reopen composition working with `optimize_manifest_for_recovery`.

Reviewed By: hx235, pdillinger

Differential Revision: D103568447

fbshipit-source-id: f4f5c35ea3ef0b80a0d52d94be40c6bd11505999
2026-05-07 13:10:59 -07:00
Anand Ananthabhotla 60b34f30c4 Extend optimize_manifest_for_recovery through DB::Close (#14703)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14703

Extend `optimize_manifest_for_recovery` so a clean `DB::Close` can persist up-to-date WAL recovery markers when that can be done safely. Combined with the recovery-side optimization in the previous diff, a clean close/reopen can avoid recovery-time MANIFEST appends.

This remains best-effort: if the close-time write is disabled, skipped, or fails, RocksDB falls back to the standard recovery path on the next open. The option stays mutable so it can be turned off before close to suppress the optimization without restarting the DB.

The close-time path respects the existing recovery constraints for 2PC, non-empty column families, dropped column families, and WAL tracking, and preserves the existing file-number invariants.

Reviewed By: pdillinger, hx235

Differential Revision: D103568449

fbshipit-source-id: ae62867507a8a87640a2c140bea852b7c608cb66
2026-05-07 12:30:54 -07:00
Anand Ananthabhotla 02a2b3501d Add optimize_manifest_for_recovery DBOption (#14702)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14702

Add a mutable DBOption `optimize_manifest_for_recovery` (default false) as a temporary rollout / kill switch for warm-reopen MANIFEST optimizations.

In this diff, enabling the option lets recovery skip MANIFEST updates during `DB::Open` when the recovered state is already reflected on disk, which reduces metadata appends after a clean shutdown and can lower warm-reopen latency on storage where MANIFEST appends are expensive.

If the option is disabled, RocksDB follows the existing recovery path unchanged. The optimization is disabled under `best_efforts_recovery`, where recovery intentionally rewrites metadata as part of salvage, and the option is mutable so later diffs in this stack can share the same rollout knob.

Reviewed By: pdillinger, hx235

Differential Revision: D103568448

fbshipit-source-id: 9ec930343e434f1bee6130bcdbd7738dddd92b6d
2026-05-07 11:44:37 -07:00
generatedunixname3846135475516776 1dc4813d97 Rocksdb Crash Test failed: assertion failed - cached_file_is_live_or_quar (#14717)
Summary: Pull Request resolved: https://github.com/facebook/rocksdb/pull/14717

Reviewed By: pdillinger

Differential Revision: D103953026

fbshipit-source-id: 4b81853dad792d77fe6dbc1ab7255a9f04834078
2026-05-07 11:21:18 -07:00
Josh Kang 4af61efaf4 Add blog post for interpolation search (#14701)
Summary:
title

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

Reviewed By: pdillinger

Differential Revision: D103709672

Pulled By: joshkang97

fbshipit-source-id: 09cd8644cfa139ed59287ca1e508df6ce470f9e9
2026-05-07 10:39:42 -07:00
xingbowang e0d549fbba Keep remote compaction stats serialization compatible with 11.1 (#14712)
Summary:
- Reapply c4941760c9 so remote compaction serializes InternalStats::CompactionStats::counts with the pre-11.2 compaction-reason count.
- Keep remote compaction result metadata compatible with 11.1 readers and writers until the stats serialization path has version-aware array handling.

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

Test Plan:
- make format-auto
- make -j$(sysctl -n hw.ncpu) compaction_job_test compaction_service_test
- /usr/local/bin/timeout 60 ./compaction_job_test --gtest_filter=CompactionJobTest.ResultSerialization
- /usr/local/bin/timeout 60 ./compaction_service_test --gtest_filter=CompactionServiceTest.VerifyStats

Reviewed By: joshkang97, jaykorean

Differential Revision: D104130456

Pulled By: xingbowang

fbshipit-source-id: e9e92cb044253edbec0fc79b92c3efff53f01d8b
2026-05-07 01:06:56 -07:00
Maciej Szeszko c25dcbccba Fix GCC 16 warning in CacheItemHelper constructor (#14713)
Summary:
This fixes a GCC 16 unity-build failure in `Cache::CacheItemHelper`. The no-secondary-cache constructor delegated to the full constructor while passing this as `without_secondary_compat`. GCC 16 reports that pattern as `-Wmaybe-uninitialized` because the delegated constructor receives a pointer to the object under construction and its debug assertions dereference it. Since RocksDB builds with `-Werror`, the warning is promoted to a build error. We see it now because the unity job uses `gcc:latest`, which now has `GCC 16.1.0`. The code pattern itself dates back to PR https://github.com/facebook/rocksdb/issues/11299 / ccaa3225b from 2023.

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

Reviewed By: xingbowang

Differential Revision: D104162382

Pulled By: mszeszko-meta

fbshipit-source-id: d6cb579e37bed0d833c6427c3b0541c857425d42
2026-05-06 20:11:57 -07:00
Peter Dillinger c99d5ec06e Add prefix varint codec and tests (#14692)
Summary:
Add MLIR-compatible PrefixVarint32/64 helpers in util/prefix_varint.h. Document the format, split decode API, and hot paths, and cover the codec in coding_test with round-trip, disk-read, overflow, and truncation cases.

I'm intending to use this in the new blog file format because you only need to read the first byte to know how many varint bytes to read total, and it might be more CPU efficient in other uses as well (to be determined in follow-up work).

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

Test Plan: Unit tests included

Reviewed By: joshkang97

Differential Revision: D103245079

Pulled By: pdillinger

fbshipit-source-id: dca435beccb6e666d864a31d8683ad19e2121c34
2026-05-06 19:27:14 -07:00
Josh Kang a4045b0a5f ExternalTable Iterator Prefer IterKey over InternalKey (#14695)
Summary:
Reduces per-step heap allocations in the external table iterator's hot path
by replacing the `InternalKey` member of `ExternalTableIteratorAdapter` with
`IterKey`. `InternalKey` stores its bytes in a `std::string`, which
heap-allocates whenever a key exceeds the small-string threshold. `IterKey`
keeps a 39-byte inline buffer and only spills to the heap for larger keys.
`UpdateKey` runs on every `Next` / `Prev` / `Seek*`, so this swap removes a
recurring per-step allocation for typical key sizes.

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

Test Plan: CI

Reviewed By: anand1976

Differential Revision: D103440780

Pulled By: joshkang97

fbshipit-source-id: deb0a8ea04110e3dff0a1bcad767e7136192fdc6
2026-05-06 10:48:44 -07:00
Josh Kang b874d2546e Preserve published-boundary visibility during flush/compaction (#14681) (#14681)
Summary:
See SC crash test failure 2729181374202803470.

Flush/compaction retention was protecting only explicit snapshots, while readers without an explicit snapshot were still bounded by GetLastPublishedSequence(). That mismatch becomes observable in WRITE_COMMITTED when commit_bypass_memtable runs: a newer version can already be installed into WBWI-backed immutable memtables before SetLastSequence() publishes it. This can happen with two-write-queue enabled OR disabled.

Example setup:
- K@P = "old_value" is the current published version of key K.
- K@U = "new_value" is written later with U > P.
- commit_bypass_memtable ingests K@U into immutable memtables, but publication is still at P.
- flush starts in that window and builds its snapshot context.

Before this change, if there was no explicit snapshot at P, flush/compaction was free to collapse K@P under K@U. A reader at the published boundary P then saw neither version: K@P had been discarded, while K@U was still too new, so Get(K) returned NotFound.

A simple solution is to add a "fake" snapshot boundary for pessimistic write committed txns.

NOTE: A behavioral side effect is that this will prevent a merge operand at snapshot seqno from being filtered via compaction filter because of the new managed snapshot. But this keeps it aligned with the behavior that write prepared and write unprepared have.

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

Test Plan: Regression test that fails without fix, specifically K@P gets deleted and a snapshot read returns NotFound, when it should exist.

Reviewed By: hx235

Differential Revision: D102856943

Pulled By: joshkang97

fbshipit-source-id: 46cb3936e9d4e771966d39fd0d15436335bf6ccf
2026-05-05 14:58:01 -07:00
anand76 c1dbe0a6ac Disable MultiScanAsyncIOTest.MultiScanPrepare test (#14707)
Summary:
This test is failing in CI. Disable until root caused.

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

Reviewed By: mszeszko-meta

Differential Revision: D103888323

Pulled By: anand1976

fbshipit-source-id: d2639ddda10a8caa2f36ae2ba529d65cb8bd2ea9
2026-05-05 11:33:13 -07:00
Peter Dillinger 52d8574629 Fix AI review workflows broken by cc8d9ea04 (#14696)
Summary:
This commit fixes several potential issues introduced by cc8d9ea04 (CI: AI review workflow improvements) that broke the agent review jobs in GitHub CI.

1. Codex workflows fail when OPENAI_API_KEY is not configured:
   - Added Codex review workflows fail with exit code 1 if the API key secret is missing
   - Added script-level checks to skip Codex steps gracefully when OPENAI_API_KEY is unset, avoiding CI noise

2. Missing pull-requests: read permission in manual-review job:
   - The manual-review job calls github.rest.pulls.get() but lacked the required permission
   - Added 'pull-requests: read' to the manual-review job permissions

3. Potential crash when headSha is undefined:
   - parse-claude-review.js and parse-codex-review.js accessed meta.headSha.substring() without null check
   - Added defensive checks to handle undefined headSha gracefully

4. Claude model claude-opus-4-7 incompatible with thinking API:
   - The commit upgraded default model to claude-opus-4-7, but this model doesn't support the 'thinking.type.enabled' API used by the Claude Code action
   - Error: 'thinking.type.enabled' is not supported for this model. Use 'thinking.type.adaptive' and 'output_config.effort' to control thinking behavior
   - Reverted default model to claude-opus-4-6 and removed claude-opus-4-7 from options

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

Test Plan: Created a draft PR with this change and draft production code changes, including a branch on facebook/rocksdb. Go to https://github.com/facebook/rocksdb/actions/workflows/claude-review.yml and choose "Run workflow" with that branch and PR. (Changes in the PR do are not picked up for the agent code review workflows; the only way to test what is outside of main is with a branch on facebook/rocksdb)

Reviewed By: joshkang97

Differential Revision: D103453631

Pulled By: pdillinger

fbshipit-source-id: 15a82257b5762e9e0b1b393dc45c4b343c866f7a
2026-05-01 16:35:45 -07:00
223 changed files with 13208 additions and 2154 deletions

No files matched your search

+7
View File
@@ -2,6 +2,13 @@ name: pre-steps
runs:
using: composite
steps:
- name: Dump ulimits for diagnostics
run: |
echo "=== ulimits (soft) ==="
ulimit -a -S || true
echo "=== ulimits (hard) ==="
ulimit -a -H || true
shell: bash
- name: Install lld linker for faster builds
run: apt-get update -y && apt-get install -y lld 2>/dev/null || true
shell: bash
@@ -3,7 +3,7 @@ inputs:
suite-run:
description: Comma-separated list of test suites to run (empty to skip C++ tests)
required: false
default: arena_test,db_basic_test,db_test,db_test2,db_merge_operand_test,bloom_test,c_test,coding_test,crc32c_test,dynamic_bloom_test,env_basic_test,env_test,hash_test,random_test
default: arena_test,db_basic_test,db_test,db_etc2_test,db_merge_operand_test,bloom_test,c_test,coding_test,crc32c_test,dynamic_bloom_test,env_basic_test,env_test,hash_test,random_test
run-java:
description: Whether to run Java tests
required: false
+1 -1
View File
@@ -18,7 +18,7 @@ module.exports = function parseClaude({executionFile, conclusion, meta}) {
if (meta.trigger !== 'auto') {
return `*Requested by @${meta.reviewer}*`;
}
const shortSha = meta.headSha.substring(0, 7);
const shortSha = meta.headSha ? meta.headSha.substring(0, 7) : 'unknown';
if (meta.autoMode === 'early') {
return `*Auto-triggered after CI reached the early-review threshold — reviewing commit ${
shortSha}*`;
+1 -1
View File
@@ -22,7 +22,7 @@ module.exports = function parseCodex(
if (meta.trigger !== 'auto') {
return `*Requested by @${meta.reviewer}*`;
}
const shortSha = meta.headSha.substring(0, 7);
const shortSha = meta.headSha ? meta.headSha.substring(0, 7) : 'unknown';
if (meta.autoMode === 'early') {
return `*Auto-triggered after CI reached the early-review threshold — reviewing commit ${
shortSha}*`;
+57 -4
View File
@@ -531,7 +531,9 @@ jobs:
core.warning(
`Could not parse classifier output: ${error.message}`);
}
const budget = label === 'simple' ? '16000' : '32000';
// Keep Claude's thinking budget below the action's effective
// max_tokens so the API has room for the final review text.
const budget = label === 'simple' ? '16000' : '24000';
core.info(
`Complexity classified as: ${label} ` +
`-> MAX_THINKING_TOKENS=${budget}`
@@ -549,13 +551,30 @@ jobs:
CODEX_HOME: /tmp/codex-home
CODEX_MODEL: ${{ inputs.default_model }}
run: |
if [ -z "${OPENAI_API_KEY}" ]; then
echo "OPENAI_API_KEY not configured, skipping Codex review"
printf 'complex\n' > codex-complexity-output.txt
exit 0
fi
mkdir -p "${CODEX_HOME}"
set +e
codex exec \
-m "${CODEX_MODEL}" \
-c model_reasoning_effort="medium" \
--dangerously-bypass-approvals-and-sandbox \
--output-last-message codex-complexity-output.txt \
- < /tmp/complexity_prompt.txt > codex-complexity.log 2>&1
exit_code=$?
set -e
if [ "${exit_code}" -ne 0 ]; then
echo "Codex complexity classification failed with exit code ${exit_code}"
echo "Defaulting to complex review budget"
if [ -s codex-complexity.log ]; then
echo "Last 100 lines of codex-complexity.log:"
tail -100 codex-complexity.log
fi
printf 'complex\n' > codex-complexity-output.txt
fi
- name: Pick thinking budget (Codex)
if: >-
@@ -698,6 +717,10 @@ jobs:
BASE_SHA: ${{ steps.pr_info.outputs.base_sha }}
PR_TITLE: ${{ steps.pr_info.outputs.pr_title }}
run: |
if [ -z "${OPENAI_API_KEY}" ]; then
echo "OPENAI_API_KEY not configured, skipping Codex review"
exit 0
fi
mkdir -p "${CODEX_HOME}"
set +e
codex exec review \
@@ -853,6 +876,7 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
if: >-
github.event_name == 'workflow_dispatch' ||
@@ -1093,9 +1117,19 @@ jobs:
}
const override = (process.env.OVERRIDE || '').trim();
if (override && /^\d+$/.test(override)) {
core.info(`Using thinking_budget override: ${override}`);
const maxSafeBudget = 24000;
const requested = parseInt(override, 10);
const budget = Math.min(requested, maxSafeBudget).toString();
if (budget !== override) {
core.info(
`Clamping thinking_budget override from ${override} ` +
`to ${budget} so max_tokens can exceed thinking tokens.`
);
} else {
core.info(`Using thinking_budget override: ${budget}`);
}
core.setOutput('label', 'override');
core.setOutput('tokens', override);
core.setOutput('tokens', budget);
return;
}
let label = 'complex';
@@ -1113,7 +1147,9 @@ jobs:
core.warning(
`Could not parse classifier output: ${error.message}`);
}
const budget = label === 'simple' ? '16000' : '32000';
// Keep Claude's thinking budget below the action's effective
// max_tokens so the API has room for the final review text.
const budget = label === 'simple' ? '16000' : '24000';
core.info(
`Complexity classified as: ${label} ` +
`-> MAX_THINKING_TOKENS=${budget}`
@@ -1131,13 +1167,30 @@ jobs:
CODEX_HOME: /tmp/codex-home
CODEX_MODEL: ${{ inputs.selected_model != '' && inputs.selected_model || inputs.default_model }}
run: |
if [ -z "${OPENAI_API_KEY}" ]; then
echo "OPENAI_API_KEY not configured, skipping Codex review"
printf 'complex\n' > codex-complexity-output.txt
exit 0
fi
mkdir -p "${CODEX_HOME}"
set +e
codex exec \
-m "${CODEX_MODEL}" \
-c model_reasoning_effort="medium" \
--dangerously-bypass-approvals-and-sandbox \
--output-last-message codex-complexity-output.txt \
- < /tmp/complexity_prompt.txt > codex-complexity.log 2>&1
exit_code=$?
set -e
if [ "${exit_code}" -ne 0 ]; then
echo "Codex complexity classification failed with exit code ${exit_code}"
echo "Defaulting to complex review budget"
if [ -s codex-complexity.log ]; then
echo "Last 100 lines of codex-complexity.log:"
tail -100 codex-complexity.log
fi
printf 'complex\n' > codex-complexity-output.txt
fi
- name: Pick thinking budget (Codex)
if: steps.request.outputs.skip != 'true' && inputs.provider == 'codex'
+3 -4
View File
@@ -30,12 +30,11 @@ on:
required: false
type: choice
options:
- claude-opus-4-7
- claude-opus-4-6
- claude-sonnet-4-6
default: claude-opus-4-7
default: claude-opus-4-6
thinking_budget:
description: Override MAX_THINKING_TOKENS (blank = auto-classify)
description: Override MAX_THINKING_TOKENS (blank = auto-classify, capped at 24000)
required: false
type: string
default: ""
@@ -61,7 +60,7 @@ jobs:
query_command: /claude-query
result_artifact_name: claude-review-result
comment_file: claude-review-comment.md
default_model: claude-opus-4-7
default_model: claude-opus-4-6
selected_model: ${{ github.event_name == 'workflow_dispatch' && inputs.model || '' }}
classifier_model: claude-sonnet-4-6
recovery_model: claude-sonnet-4-6
+6 -3
View File
@@ -373,14 +373,17 @@ jobs:
crash_duration: 240
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
options: --shm-size=16gb
options: --shm-size=16gb --ulimit nofile=1048576:1048576
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: mini-crashtest
- run: ulimit -S -n `ulimit -H -n` && make V=1 -j8 CRASH_TEST_EXT_ARGS='--duration=${{ matrix.crash_duration }} --max_key=2500000' ${{ matrix.crash_test_target }}
- 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 }}
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
@@ -534,7 +537,7 @@ jobs:
suite_run: db_test
run_java: "false"
- test_shard: other
suite_run: arena_test,db_basic_test,db_test2,db_merge_operand_test,bloom_test,c_test,coding_test,crc32c_test,dynamic_bloom_test,env_basic_test,env_test,hash_test,random_test
suite_run: arena_test,db_basic_test,db_etc2_test,db_merge_operand_test,bloom_test,c_test,coding_test,crc32c_test,dynamic_bloom_test,env_basic_test,env_test,hash_test,random_test
run_java: "false"
- test_shard: java
suite_run: ""
+1 -1
View File
@@ -52,7 +52,7 @@ GRTAGS
GTAGS
rocksdb_dump
rocksdb_undump
db_test2
db_etc2_test
trace_analyzer
block_cache_trace_analyzer
io_tracer_parser
+6 -6
View File
@@ -4868,6 +4868,12 @@ cpp_unittest_wrapper(name="db_encryption_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_etc2_test",
srcs=["db/db_etc2_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_etc3_test",
srcs=["db/db_etc3_test.cc"],
deps=[":rocksdb_test_lib"],
@@ -5024,12 +5030,6 @@ cpp_unittest_wrapper(name="db_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_test2",
srcs=["db/db_test2.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_universal_compaction_test",
srcs=["db/db_universal_compaction_test.cc"],
deps=[":rocksdb_test_lib"],
+37 -3
View File
@@ -209,14 +209,48 @@ The following patterns emerged as frequent sources of review feedback:
## Important tips
### Build system
* There are 3 build system. Make, CMake, BUCK(meta internal).
* There are 3 build system. Make for git clones, BUCK (meta internal) for hg
clones, and CMake for some special cases.
* When a new .cc file is added, update Makefile, CMakeLists.txt, src.mk, BUCK.
* Don't manually edit BUCK file, after updating src.mk, run
/usr/local/bin/python3 buckifier/buckify_rocksdb.py to update it
* Use make to build and run the test. CMake and BUCK are not used locally.
* Use `make dbg` command to build all of the unit test in debug mode.
* For -j in make command, use the number of CPU cores to decide it.
### When to run `make clean` (avoid mixing build modes)
The Makefile does **not** track build mode, so object files from a prior
build are silently reused even when compiled with different flags, leading
to confusing linker errors, sanitizer false negatives, ODR violations, or
"phantom" bugs.
Run `make clean` before switching any of these:
* **`ASSERT_STATUS_CHECKED=1` ↔ unset** — changes the `Status` class layout (ABI break).
* **Sanitizer builds** — toggling any of `COMPILE_WITH_ASAN=1`,
`COMPILE_WITH_UBSAN=1`, `COMPILE_WITH_TSAN=1` on/off.
* `DEBUG_LEVEL=0` (release) ↔ `DEBUG_LEVEL=1` (debug, default for `make dbg`).
* Different compilers, `OPT` levels, or other flags affecting codegen/ABI.
**Notable exception:** `DEBUG_LEVEL=2` can be safely mixed with
`DEBUG_LEVEL=1` — rebuild a subset of files with `DEBUG_LEVEL=2` to get
extra/more accurate runtime checks for those files without a full clean.
When in doubt, `make clean` is cheap insurance compared to chasing a
phantom bug.
### Source checks
* Run `make check-sources` before committing. This catches non-ASCII
characters in source files and other source-level issues that CI will
reject. In particular, **do not use Unicode characters** (em dashes,
smart quotes, etc.) in comments or strings -- use ASCII equivalents
(`--` instead of em dash, `'` instead of smart quote, etc.).
### RTTI and dynamic_cast
* Production code and `db_stress` must build in **release mode
(`-fno-rtti`)**. Do not use `dynamic_cast` anywhere except unit tests.
Use `static_cast_with_check` from `util/cast_util.h` (validates with
`dynamic_cast` in debug builds, plain `static_cast` in release).
* Unit tests (`*_test.cc`) are built in debug mode with RTTI enabled.
### 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
+39 -1
View File
@@ -654,6 +654,8 @@ if(USE_FOLLY)
FMT_INST_PATH)
exec_program(ls ARGS -d ${FOLLY_INST_PATH}/../gflags* OUTPUT_VARIABLE
GFLAGS_INST_PATH)
exec_program(ls ARGS -d ${FOLLY_INST_PATH}/../glog* OUTPUT_VARIABLE
GLOG_INST_PATH)
set(Boost_DIR ${BOOST_INST_PATH}/lib/cmake/Boost-1.83.0)
if(EXISTS ${FMT_INST_PATH}/lib64)
set(fmt_DIR ${FMT_INST_PATH}/lib64/cmake/fmt)
@@ -661,6 +663,42 @@ if(USE_FOLLY)
set(fmt_DIR ${FMT_INST_PATH}/lib/cmake/fmt)
endif()
set(gflags_DIR ${GFLAGS_INST_PATH}/lib/cmake/gflags)
if(EXISTS ${GLOG_INST_PATH}/lib64/cmake/glog)
set(GLOG_CMAKE_DIR ${GLOG_INST_PATH}/lib64/cmake/glog)
elseif(EXISTS ${GLOG_INST_PATH}/lib/cmake/glog)
set(GLOG_CMAKE_DIR ${GLOG_INST_PATH}/lib/cmake/glog)
endif()
if(NOT GLOG_CMAKE_DIR)
find_library(GETDEPS_GLOG_LIBRARY NAMES glogd glog
PATHS ${GLOG_INST_PATH}/lib64 ${GLOG_INST_PATH}/lib
NO_DEFAULT_PATH)
if(NOT GETDEPS_GLOG_LIBRARY OR
NOT EXISTS ${GLOG_INST_PATH}/include/glog/logging.h)
message(FATAL_ERROR "Could not find getdeps glog under "
"${GLOG_INST_PATH}")
endif()
set(GLOG_CMAKE_DIR ${CMAKE_CURRENT_BINARY_DIR}/getdeps-glog-cmake)
file(MAKE_DIRECTORY ${GLOG_CMAKE_DIR})
file(WRITE ${GLOG_CMAKE_DIR}/glog-config.cmake
"if(NOT TARGET glog::glog)\n"
" add_library(glog::glog UNKNOWN IMPORTED)\n"
" set_target_properties(glog::glog PROPERTIES\n"
" IMPORTED_LOCATION \"${GETDEPS_GLOG_LIBRARY}\"\n"
" INTERFACE_INCLUDE_DIRECTORIES \"${GLOG_INST_PATH}/include\")\n"
"endif()\n"
"set(glog_FOUND TRUE)\n"
"set(Glog_FOUND TRUE)\n"
"set(GLOG_FOUND TRUE)\n"
"set(GLOG_LIBRARY \"${GETDEPS_GLOG_LIBRARY}\")\n"
"set(GLOG_LIBRARIES \"${GETDEPS_GLOG_LIBRARY}\")\n"
"set(GLOG_INCLUDE_DIR \"${GLOG_INST_PATH}/include\")\n")
endif()
set(glog_DIR ${GLOG_CMAKE_DIR})
set(Glog_DIR ${GLOG_CMAKE_DIR})
# Unlike gflags, glog may also be installed system-wide. Pre-resolve the
# getdeps copy so folly-config.cmake cannot fall back to another version.
find_package(glog CONFIG REQUIRED PATHS ${GLOG_CMAKE_DIR}
NO_DEFAULT_PATH)
exec_program(sed ARGS -i 's/gflags_shared//g'
${FOLLY_INST_PATH}/lib/cmake/folly/folly-targets.cmake)
@@ -1453,6 +1491,7 @@ if(WITH_TESTS)
db/db_clip_test.cc
db/db_dynamic_level_test.cc
db/db_encryption_test.cc
db/db_etc2_test.cc
db/db_etc3_test.cc
db/db_flush_test.cc
db/db_inplace_update_test.cc
@@ -1476,7 +1515,6 @@ if(WITH_TESTS)
db/db_table_properties_test.cc
db/db_tailing_iter_test.cc
db/db_test.cc
db/db_test2.cc
db/db_logical_block_size_cache_test.cc
db/db_universal_compaction_test.cc
db/db_wal_test.cc
+57
View File
@@ -1,6 +1,63 @@
# Rocksdb Change Log
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
## 11.4.3 (06/24/2026)
### Bug Fixes
* Fixed a bug where closing a read-only DB instance could delete live SST files created by a concurrent read-write DB sharing the same directory.
## 11.4.2 (06/08/2026)
### Public API Changes
* Added experimental read-scoped block buffer provider API, configured through `ReadOptions::read_scoped_block_buffer_provider`, for supported block-based table iterator scans and MultiScan reads to use caller-provided read-scoped storage for final data-block contents. When configured, supported provider-backed scan data-block reads bypass the data-block cache. The provider is ignored when mmap reads are enabled. RocksDB may still use ordinary temporary scratch for serialized block bytes, such as when a block may be compressed. Get and MultiGet do not currently provide API guarantees for this provider.
### Bug Fixes
* Fixed a use after free bug in Multiscan when async IO is used
## 11.4.1 (06/05/2026)
* No-op patch used to sync with internal fb version.
## 11.4.0 (06/02/2026)
### Public API Changes
* Added `rocksdb_options_set_memtable_batch_lookup_optimization()` and `rocksdb_options_get_memtable_batch_lookup_optimization()` to the C API, exposing the existing `AdvancedColumnFamilyOptions::memtable_batch_lookup_optimization` field. This allows C API users (and downstream language bindings) to enable the skip-list memtable's batch-lookup optimization for `MultiGet`, which caches the search path between consecutive keys and reduces per-key cost from O(log N) to O(log d) where d is the distance between consecutive keys.
* Added `rocksdb_readoptions_set_optimize_multiget_for_io()` and `rocksdb_readoptions_get_optimize_multiget_for_io()` to the C API, exposing the existing `ReadOptions::optimize_multiget_for_io` field. This allows C API users (and downstream language bindings) to opt out of the multi-level parallel MultiGet path, which only takes effect when the library is built with `USE_COROUTINES`.
* Added `rocksdb_block_based_table_index_block_search_type_auto` enum constant and the `rocksdb_block_based_options_set_uniform_cv_threshold()` setter to the C API. The constant exposes `BlockSearchType::kAuto`, and the setter exposes `BlockBasedTableOptions::uniform_cv_threshold`. Both are required for `kAuto` index-block search to take effect from the C API: without setting `uniform_cv_threshold >= 0`, the per-block `is_uniform` footer bit is never written, and `kAuto` falls back to binary search at read time.
* Added `EventListener::OnDBShutdownBegin`, a callback that fires once when a DB begins shutdown, including when RocksDB cleans up a failed `DB::Open()` attempt.
* Added a new PerfContext counter `blob_cache_read_byte` for blob cache bytes read.
### Behavior Changes
* Remote compaction now falls back to local compaction when the primary cannot deserialize a successful `CompactionServiceResult` before installing any remote output files.
* StringToMap (rocksdb/convenience.h) now preserves the outer braces of nested values so each map entry is in self-contained form and can be embedded directly into another `key=value;` string (e.g. SetOptions) without losing inner ';' delimiters. A new symmetric MapToString utility is provided.
### Bug Fixes
* Fixed a bug where `AbortAllCompactions()` could leave automatic compaction work unscheduled when aborting before the background worker picked it, causing compaction and write stalls until DB restart.
* Fix a bug where db had a false positive compaction corruption error due to remote compaction service result with `has_accurate_num_input_records=false` not being serialized.
* Fixed WritePrepared TransactionDB cleanup after retryable commit or rollback write failures so prepared transactions remain rollbackable instead of leaving unresolved prepared state.
* Fix a rare corruption bug for tiered compaction that incorrectly moved last level range tombstones into proximal level. This corruption error is surfaced when force_consistency_checks is enabled.
### Performance Improvements
* Reduce the likelihood of user-facing metadata operations blocking on MANIFEST rotation when file creation is slow, such as on remote storage. MANIFEST write batches containing foreground edits from `CreateColumnFamily()`, `DropColumnFamily()`, `CreateColumnFamilyWithImport()`, `IngestExternalFile()`, and `DeleteFilesInRanges()` now get a relaxed file size threshold for triggering MANIFEST rotation; background-only MANIFEST write batches, such as compaction and flush, continue to use the normal threshold. This change should make auto-tuning manifest file size more attractive (see `max_manifest_file_size` and `max_manifest_space_amp_pct` options).
## 11.3.0 (05/15/2026)
### New Features
* Add experimental DB option `async_wal_precreate` to precreate the next WAL file in a background thread and reduce foreground WAL rotation latency. The option is sanitized to false when WAL recycling is enabled.
* Added a new `EventListener::OnCompactionPreCommit` callback that fires after a compaction job finishes but before its input files are released (i.e. while `FileMetaData::being_compacted` is still true). Listeners that maintain bookkeeping of which files are currently being compacted can clean up such state in this new callback to avoid races with concurrent compaction picking, where another thread might pick up the same files for a new compaction immediately after `being_compacted` is flipped back to false but before `OnCompactionCompleted` fires. The default implementation is a no-op so this is not a breaking change.
* Add mutable DBOption `optimize_manifest_for_recovery` (default false). When enabled, RocksDB can reduce recovery work after a clean shutdown, which may lower DB::Open latency on warm reopens.
* Added public utility APIs `ParseCompressionNameForDisplay()` to convert `TableProperties::compression_name` into a human-readable compression name for both legacy and format_version 7+ SST metadata, including custom `CompressionManager`-provided display names for custom compression types.
* Add `reuse_manifest_on_open` DBOption (default false). When enabled, DB::Open reuses the existing MANIFEST file for append instead of creating a fresh one, avoiding the cost of serializing the entire database state into a new MANIFEST on the first post-open write. To prevent this feature from interfering with manifest file size auto-tuning, an extra forward-compatible field is now always added to the MANIFEST (to track the last "compacted" size).
### Behavior Changes
* Read-only open with `error_if_wal_file_exists=true` now tolerates empty WAL files so empty precreated WALs do not prevent inspection.
* WriteCommitted TransactionDB now matches WritePrepared and WriteUnprepared compaction filtering in both single- and two-write-queue modes: a compaction filter's FilterMergeOperand will not be invoked on merge operands at or below the latest published sequence number.
### Bug Fixes
* Fixed blob-backed wide-column merge reads to preserve correct status
propagation and resolution across memtable, read-only, and secondary DB
paths.
* Fixed a bug where `DB::GetCreationTimeOfOldestFile()` could return inaccurate results instead of the real creation time when called shortly after opening a legacy DB (one whose manifest lacks `file_creation_time`) with `open_files_async = true`. The API now waits for background SST file loading to complete only when needed; modern DBs are unaffected.
* Fixed merge reads against wide-column/blob-backed base values to preserve precise failure statuses, including `GetMergeOperands()` and direct-write memtable reads.
* Fix bug in range tombstone synthesis that covers live keys added during an IngestExternalFile
* Reject the empty string as a column family name in `DB::CreateColumnFamily` / `DB::CreateColumnFamilies`. Previously such calls returned OK and a usable handle, but the column family was not persisted in the manifest, so any data written to it was silently lost on DB reopen.
* Fixed a bug where a WriteCommitted TransactionDB using commit-bypass WBWI ingestion could drop an entry that is still visible at the published sequence boundary.
## 11.2.0 (04/18/2026)
### New Features
* Added experimental `DBOptions::fast_sst_open` option. When enabled, RocksDB retrieves opaque file system metadata for SST files after flush, compaction, and external file ingestion, persists it in the MANIFEST, and passes it back to the file system on subsequent file opens to accelerate DB open time.
+94 -31
View File
@@ -327,6 +327,9 @@ missing_make_config_paths := $(shell \
$(foreach path, $(missing_make_config_paths), \
$(warning Warning: $(path) does not exist))
# This (the first rule) must depend on "all".
default: all
ifeq ($(PLATFORM), OS_AIX)
# no debug info
else ifneq ($(PLATFORM), IOS)
@@ -384,6 +387,10 @@ endif
# TSAN doesn't work well with jemalloc. If we're compiling with TSAN, we should use regular malloc.
ifdef COMPILE_WITH_TSAN
DISABLE_JEMALLOC=1
# Use a suppressions file instead of the process-wide TSAN default
# suppressions hook, which belongs to the final application.
TSAN_OPTIONS?=suppressions=$(CURDIR)/tools/tsan_suppressions.txt
export TSAN_OPTIONS
EXEC_LDFLAGS += -fsanitize=thread
PLATFORM_CCFLAGS += -fsanitize=thread -fPIC -DFOLLY_SANITIZE_THREAD
PLATFORM_CXXFLAGS += -fsanitize=thread -fPIC -DFOLLY_SANITIZE_THREAD
@@ -481,9 +488,6 @@ ifdef ROCKSDB_MODIFY_NPHASH
PLATFORM_CXXFLAGS += -DROCKSDB_MODIFY_NPHASH=1
endif
# This (the first rule) must depend on "all".
default: all
WARNING_FLAGS = -W -Wextra -Wall -Wsign-compare -Wshadow \
-Wunused-parameter
@@ -883,23 +887,43 @@ coverage: clean
parallel_tests = $(patsubst %,parallel_%,$(PARALLEL_TEST))
.PHONY: gen_parallel_tests $(parallel_tests)
# Shard size controls how many test cases run per process. The actual number
# of shards per binary is: min(ceil(test_count / GTEST_SHARD_SIZE), NCORES * 8)
# of shards per binary is: min(ceil(test_count / shard_size), NCORES * 8)
# This adapts to machine size: many small shards on beefy machines, fewer
# larger shards on CI (typically 2-4 cores).
GTEST_SHARD_SIZE ?= 10
NCORES ?= $(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)
# Per-binary overrides for GTEST_SHARD_SIZE, used to chop up binaries whose
# individual test cases are slow enough that the default of 10 leaves one
# shard on the critical path of "make check". Format: whitespace-separated
# tokens "BINARY:SIZE". Sizes were tuned from `make suggest-slow-tests`
# output to keep max shard time under ~20s on a beefy box. Reducing too far
# adds per-shard fixed overhead, so don't use 1 unless individual tests are
# >5s.
SHARD_SIZE_OVERRIDES ?= \
point_lock_manager_stress_test:1 \
external_sst_file_test:1 \
external_sst_file_basic_test:2 \
db_test:3 \
compaction_service_test:1
$(parallel_tests):
$(AM_V_at)TEST_BINARY=$(patsubst parallel_%,%,$@); \
TEST_COUNT=` \
(./$$TEST_BINARY --gtest_list_tests 2>/dev/null || echo " list_failure") \
| grep -c '^ '`; \
if [ "$$TEST_COUNT" -le 0 ]; then TEST_COUNT=1; fi; \
SHARD_SIZE=$(GTEST_SHARD_SIZE); \
for o in $(SHARD_SIZE_OVERRIDES); do \
case "$$o" in \
"$$TEST_BINARY":*) SHARD_SIZE=$${o#*:} ;; \
esac; \
done; \
MAX_SHARDS=$$(( $(NCORES) * 8 )); \
NUM_SHARDS=$$(( (TEST_COUNT + $(GTEST_SHARD_SIZE) - 1) / $(GTEST_SHARD_SIZE) )); \
NUM_SHARDS=$$(( (TEST_COUNT + SHARD_SIZE - 1) / SHARD_SIZE )); \
if [ "$$NUM_SHARDS" -gt "$$MAX_SHARDS" ]; then NUM_SHARDS=$$MAX_SHARDS; fi; \
if [ "$$NUM_SHARDS" -le 0 ]; then NUM_SHARDS=1; fi; \
echo " Generating $$NUM_SHARDS shards for $$TEST_BINARY ($$TEST_COUNT tests)"; \
echo " Generating $$NUM_SHARDS shards for $$TEST_BINARY ($$TEST_COUNT tests, shard_size=$$SHARD_SIZE)"; \
SHARD_IDX=0; \
while [ "$$SHARD_IDX" -lt "$$NUM_SHARDS" ]; do \
if [ -n "$(CI_TOTAL_SHARDS)" ] && [ $$(( $$SHARD_IDX % $(CI_TOTAL_SHARDS) )) -ne $(CI_SHARD_INDEX) ]; then \
@@ -925,31 +949,64 @@ gen_parallel_tests:
$(AM_V_at)$(FIND) t -type f -name 'run-*' -exec rm -f {} \;
$(MAKE) $(parallel_tests)
# Reorder input lines (which are one per test) so that the
# longest-running tests appear first in the output.
# Do this by prefixing each selected name with its duration,
# sort the resulting names, and remove the leading numbers.
# FIXME: the "100" we prepend is a fake time, for now.
# FIXME: squirrel away timings from each run and use them
# (when present) on subsequent runs to order these tests.
# 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
# descending, then strip the prefix. Slow-matched tests therefore come ahead
# of all unmatched tests; ordering within each group is unspecified.
#
# Without this reordering, these two tests would happen to start only
# after almost all other tests had completed, thus adding 100 seconds
# to the duration of parallel "make check". That's the difference
# between 4 minutes (old) and 2m20s (new).
# Why this matters: gnu_parallel hands out jobs in input order. If a slow
# test starts only after most others have finished, the run is bottlenecked
# on its tail. Front-loading slow tests overlaps them with the bulk of
# faster ones for much better wall-clock time.
#
# 152.120 PASS t/DBTest.FileCreationRandomFailure
# 107.816 PASS t/DBTest.EncodeDecompressedBlockSizeTest
# Maintenance:
# 1) Run `make check` so LOG is up-to-date.
# 2) Run `make suggest-slow-tests` to see candidate slow binaries.
# 3) Add binaries with high per-shard time (slow critical path) or high
# total time across many shards (need early queueing for fan-out).
# Each alternative is wrapped in `^.*NAME.*$$` so the *whole input line* is
# captured into $$1; then `s,(...),100 $$1,` prepends "100 " to the line.
#
# With sharded test execution, prioritize binaries known to be slow.
# These generate many shards and should start early for good load balancing.
# Tiers below are based on observed timings (see suggest-slow-tests).
# Tier 1: max single-shard time >= 30s (critical-path bottlenecks).
# Tier 2: max single-shard time 15-30s.
# Tier 3: huge total time across many tiny shards; front-loading them keeps
# the tail of the run busy while big shards finish.
slow_test_regexp = \
^.*block_based_table_reader_test.*$$|^.*table_test.*$$|^.*block_test.*$$|^.*write_prepared_transaction_test.*$$|^.*transaction_test.*$$|^.*external_sst_file_test.*$$|^.*db_wal_test.*$$|^.*db_with_timestamp_basic_test.*$$|^.*db_test-.*$$
^.*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.*$$
prioritize_long_running_tests = \
perl -pe 's,($(slow_test_regexp)),100 $$1,' \
| sort -k1,1gr \
| sed 's/^[.0-9]* //'
# Helper: print binaries observed to be slow in the last `make check` run.
# Use this to decide whether `slow_test_regexp` above needs updating.
# Aggregates per-binary across shards; flags any binary whose max single-shard
# time is >= 20s OR whose total time is >= 200s.
.PHONY: suggest-slow-tests
suggest-slow-tests:
@if [ ! -s LOG ]; then \
echo "No (or empty) LOG file. Run 'make check' first." >&2; exit 1; \
fi
@bash -c '$(quoted_perl_command)' < LOG | awk '\
{ \
t=$$1; name=$$3; \
sub(/^t\/run-/, "", name); \
sub(/-shard-[0-9]+$$/, "", name); \
total[name] += t; \
if (t > max[name]) max[name] = t; \
count[name]++; \
} \
END { \
printf "%8s %8s %5s %s\n", "TOTAL_s", "MAX_s", "SHRDS", "BINARY"; \
for (n in total) \
if (max[n] >= 20 || total[n] >= 200) \
printf "%8.1f %8.1f %5d %s\n", total[n], max[n], count[n], n; \
}' | (read header; echo "$$header"; sort -k2,2gr)
# "make check" uses
# Run with "make J=1 check" to disable parallelism in "make check".
# Run with "make J=200% check" to run two parallel jobs per core.
@@ -986,7 +1043,7 @@ check_0:
awk -v s=$(CI_SHARD_INDEX) -v n=$(CI_TOTAL_SHARDS) '(NR-1)%n==s'; \
else cat; fi; \
fi; \
find t -name 'run-*' -print; \
$(FIND) t -name 'run-*' -print; \
} \
| $(prioritize_long_running_tests) \
| grep -E '$(tests-regexp)' \
@@ -1008,7 +1065,7 @@ valgrind_check_0:
' run "make watch-log" in a separate window' ''; \
{ \
printf './%s\n' $(filter-out $(PARALLEL_TEST) %skiplist_test options_settable_test, $(TESTS)); \
find t -name 'run-*' -print; \
$(FIND) t -name 'run-*' -print; \
} \
| $(prioritize_long_running_tests) \
| grep -E '$(tests-regexp)' \
@@ -1226,11 +1283,17 @@ clean-not-downloaded: clean-ext-libraries-bin clean-rocks clean-not-downloaded-r
clean-rocks:
# Not practical to exactly match all versions/variants in naming (e.g. debug or not)
rm -f ${LIBNAME}*.so* ${LIBNAME}*.a
rm -f $(BENCHMARKS) $(TOOLS) $(TESTS) $(PARALLEL_TEST) $(MICROBENCHS)
rm -rf $(CLEAN_FILES) ios-x86 ios-arm scan_build_report
$(FIND) . -name "*.[oda]" -exec rm -f {} \;
$(FIND) . -type f \( -name "*.gcda" -o -name "*.gcno" \) -exec rm -f {} \;
@echo Removing link targets
@rm -f ${LIBNAME}*.so* ${LIBNAME}*.a $(BENCHMARKS) $(TOOLS) $(TESTS) $(PARALLEL_TEST) $(MICROBENCHS)
@echo Finding and cleaning other files
@rm -rf $(CLEAN_FILES) ios-x86 ios-arm scan_build_report
@if $(FIND) Makefile -regextype awk &> /dev/null; then \
$(FIND) . -regextype awk \
-regex '.*/([.].*|third-party)' -prune -o \
-type f -regex '.*[.]([oda]|gcda|gcno)' -exec rm -f {} \; ; \
else \
$(FIND) . -name "*.[oda]" -exec rm -f {} \; ; \
fi
clean-rocksjava: clean-rocks
rm -rf jl jls
@@ -1243,7 +1306,7 @@ clean-ext-libraries-all:
rm -rf bzip2* snappy* zlib* lz4* zstd*
clean-ext-libraries-bin:
find . -maxdepth 1 -type d \( -name bzip2\* -or -name snappy\* -or -name zlib\* -or -name lz4\* -or -name zstd\* \) -prune -exec rm -rf {} \;
$(FIND) . -maxdepth 1 -type d \( -name bzip2\* -or -name snappy\* -or -name zlib\* -or -name lz4\* -or -name zstd\* \) -prune -exec rm -rf {} \;
tags:
ctags -R .
@@ -1521,7 +1584,7 @@ db_open_with_config_test: $(OBJ_DIR)/db/db_open_with_config_test.o $(TEST_LIBRAR
db_test: $(OBJ_DIR)/db/db_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
db_test2: $(OBJ_DIR)/db/db_test2.o $(TEST_LIBRARY) $(LIBRARY)
db_etc2_test: $(OBJ_DIR)/db/db_etc2_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
db_etc3_test: $(OBJ_DIR)/db/db_etc3_test.o $(TEST_LIBRARY) $(LIBRARY)
+1 -1
View File
@@ -37,7 +37,7 @@ if [ "$?" != "1" ]; then
BAD=1
fi
git grep -n -P "[\x80-\xFF]" -- ':!docs' ':!*.md'
LC_ALL=C git grep -n $'[\x80-\xff]' -- ':!docs' ':!*.md' ':!.github'
if [ "$?" != "1" ]; then
echo '^^^^ Use only ASCII characters in source files'
BAD=1
+178 -73
View File
@@ -1,62 +1,188 @@
#!/usr/bin/env python3
# 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 the Apache 2.0 License (found in the LICENSE.Apache file in the root directory).
"""
Pre-download packages with unreliable mirrors using fallback mirrors.
Reads package info from folly's getdeps manifest files.
"""
import sys
import os
import hashlib
import subprocess
import configparser
import hashlib
import os
import shutil
import sys
import urllib.request
DOWNLOAD_TIMEOUT_SECONDS = 120
DOWNLOAD_CHUNK_BYTES = 64 * 1024
MAX_DOWNLOAD_BYTES = 50 * 1024 * 1024
MIRROR_FALLBACKS = {
"ftpmirror.gnu.org/gnu/": [
"https://mirrors.kernel.org/gnu/",
"https://ftpmirror.gnu.org/gnu/",
"https://ftp.gnu.org/gnu/",
],
"ftp.gnu.org/gnu/": [
"https://mirrors.kernel.org/gnu/",
"https://ftpmirror.gnu.org/gnu/",
"https://ftp.gnu.org/gnu/",
],
}
# These packages must have URLs matching MIRROR_FALLBACKS; other packages are
# left for getdeps.py's normal download path.
PACKAGES_TO_CHECK = ("autoconf", "automake", "libtool", "libiberty")
def sha256_file(path):
"""Calculate SHA256 hash of a file."""
h = hashlib.sha256()
try:
with open(path, 'rb') as f:
for chunk in iter(lambda: f.read(65536), b''):
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
h.update(chunk)
return h.hexdigest()
except Exception:
return None
def parse_manifest(manifest_path):
"""Parse a getdeps manifest file to extract download info."""
config = configparser.ConfigParser()
# folly manifests can contain bare keys in sections unrelated to downloads.
config = configparser.ConfigParser(allow_no_value=True, interpolation=None)
try:
config.read(manifest_path)
if 'download' in config:
return {
'url': config['download'].get('url', ''),
'sha256': config['download'].get('sha256', ''),
}
except Exception:
pass
with open(manifest_path, encoding="utf-8") as manifest_file:
config.read_file(manifest_file)
except Exception as ex:
print(f" {os.path.basename(manifest_path)}: WARNING - parse failed: {ex}")
return None
if "download" in config:
return {
"url": config["download"].get("url", ""),
"sha256": config["download"].get("sha256", ""),
}
return None
def file_size(path):
try:
return os.path.getsize(path)
except Exception:
return None
def get_fallback_mirrors(url):
"""Get fallback mirror URLs for a given URL."""
# Fallback mirror patterns for known unreliable hosts
mirror_fallbacks = {
"ftp.gnu.org/gnu/": [
"https://mirrors.kernel.org/gnu/",
"https://ftpmirror.gnu.org/gnu/",
"https://ftp.gnu.org/gnu/",
],
"ftpmirror.gnu.org/gnu/": [
"https://mirrors.kernel.org/gnu/",
"https://ftpmirror.gnu.org/gnu/",
"https://ftp.gnu.org/gnu/",
],
}
for pattern, mirrors in mirror_fallbacks.items():
for pattern, mirrors in MIRROR_FALLBACKS.items():
if pattern in url:
# Extract the path after the pattern
path_start = url.find(pattern) + len(pattern)
path = url[path_start:]
return [mirror + path for mirror in mirrors]
return [url] # No fallback, use original
return []
def download_url(url, filepath):
"""Download URL to filepath without leaving partial files behind."""
tmp_filepath = filepath + ".tmp"
if os.path.exists(tmp_filepath):
os.remove(tmp_filepath)
request = urllib.request.Request(
url, headers={"User-Agent": "rocksdb-getdeps-fallback/1.0"}
)
try:
with urllib.request.urlopen(
request, timeout=DOWNLOAD_TIMEOUT_SECONDS
) as response, open(tmp_filepath, "wb") as output:
copied = 0
while True:
chunk = response.read(DOWNLOAD_CHUNK_BYTES)
if not chunk:
break
copied += len(chunk)
if copied > MAX_DOWNLOAD_BYTES:
raise Exception(
f"download exceeds {MAX_DOWNLOAD_BYTES} bytes"
)
output.write(chunk)
os.replace(tmp_filepath, filepath)
finally:
if os.path.exists(tmp_filepath):
os.remove(tmp_filepath)
def prepare_download(package, info, download_dir, cache_dir):
url = info["url"]
expected_sha256 = info["sha256"]
mirrors = get_fallback_mirrors(url)
if not mirrors:
return False
if not expected_sha256:
print(f" {package}: WARNING - skipped fallback without sha256")
return False
# getdeps uses format: {package}-{filename}
filename = f"{package}-{os.path.basename(url)}"
filepath = os.path.join(download_dir, filename)
cache_path = os.path.join(cache_dir, filename)
# Check if already valid.
actual_sha256 = sha256_file(filepath) if os.path.exists(filepath) else None
if actual_sha256 == expected_sha256:
print(f" {filename}: OK (already downloaded)")
return True
if actual_sha256 is not None:
print(
f" {filename}: WARNING - removing invalid download "
f"sha256={actual_sha256}"
)
os.remove(filepath)
# The cache is only an opportunistic single-build accelerator; callers
# should not share it across concurrent builds without external locking.
actual_sha256 = sha256_file(cache_path) if os.path.exists(cache_path) else None
if actual_sha256 == expected_sha256:
print(f" {filename}: OK (from cache)")
shutil.copy2(cache_path, filepath)
return True
if actual_sha256 is not None:
print(
f" {filename}: WARNING - removing invalid cache "
f"sha256={actual_sha256}"
)
os.remove(cache_path)
# Try fallback mirrors.
for mirror_url in mirrors:
print(f" {filename}: trying {mirror_url}...")
try:
download_url(mirror_url, filepath)
except Exception as ex:
print(f" {filename}: WARNING - download failed: {ex}")
continue
actual_sha256 = sha256_file(filepath)
if actual_sha256 == expected_sha256:
size = file_size(filepath)
print(f" {filename}: OK (downloaded, {size} bytes)")
shutil.copy2(filepath, cache_path)
return True
size = file_size(filepath)
print(
f" {filename}: WARNING - sha256 mismatch from {mirror_url}: "
f"expected={expected_sha256} actual={actual_sha256} size={size}"
)
os.remove(filepath)
print(f" {filename}: WARNING - all mirrors failed")
return False
def main():
if len(sys.argv) != 4:
@@ -64,60 +190,39 @@ def main():
sys.exit(1)
download_dir, cache_dir, manifests_dir = sys.argv[1], sys.argv[2], sys.argv[3]
os.makedirs(download_dir, exist_ok=True)
os.makedirs(cache_dir, exist_ok=True)
# Packages known to have unreliable mirrors
packages_to_check = ["autoconf", "automake", "libtool"]
for package in packages_to_check:
checked = 0
ready = 0
for package in PACKAGES_TO_CHECK:
manifest_path = os.path.join(manifests_dir, package)
if not os.path.exists(manifest_path):
if not os.path.isfile(manifest_path):
continue
info = parse_manifest(manifest_path)
if not info or not info['url'] or not info['sha256']:
if not info or not info["url"]:
continue
# Determine filename from URL
url = info['url']
expected_sha256 = info['sha256']
url_filename = os.path.basename(url)
# getdeps uses format: {package}-{filename}
filename = f"{package}-{url_filename}"
filepath = os.path.join(download_dir, filename)
cache_path = os.path.join(cache_dir, filename)
# Check if already valid
if os.path.exists(filepath) and sha256_file(filepath) == expected_sha256:
print(f" {filename}: OK (already downloaded)")
if not info["sha256"]:
print(f" {package}: WARNING - skipped fallback without sha256")
continue
# Check cache
if os.path.exists(cache_path) and sha256_file(cache_path) == expected_sha256:
print(f" {filename}: OK (from cache)")
subprocess.run(['cp', cache_path, filepath], check=True)
if not get_fallback_mirrors(info["url"]):
print(
f" {package}: WARNING - skipped fallback without known mirror "
f"for {info['url']}"
)
continue
# Try fallback mirrors
mirrors = get_fallback_mirrors(url)
downloaded = False
for mirror_url in mirrors:
print(f" {filename}: trying {mirror_url}...")
try:
subprocess.run(['wget', '-q', '-O', filepath, mirror_url], check=True, timeout=120)
if sha256_file(filepath) == expected_sha256:
print(f" {filename}: OK (downloaded)")
subprocess.run(['cp', filepath, cache_path], check=False)
downloaded = True
break
else:
os.remove(filepath)
except Exception:
if os.path.exists(filepath):
os.remove(filepath)
checked += 1
try:
if prepare_download(package, info, download_dir, cache_dir):
ready += 1
except Exception as ex:
print(f" {package}: WARNING - fallback preparation failed: {ex}")
if not downloaded:
print(f" {filename}: WARNING - all mirrors failed")
print(f" fallback mirror downloads ready: {ready}/{checked}")
if __name__ == "__main__":
main()
+7
View File
@@ -7,6 +7,13 @@ DB_STRESS_CMD?=./db_stress
include common.mk
ifdef COMPILE_WITH_TSAN
# Keep direct `make -f crash_test.mk COMPILE_WITH_TSAN=1 ...` runs
# aligned with the main Makefile's TSAN runtime options.
TSAN_OPTIONS?=suppressions=$(CURDIR)/tools/tsan_suppressions.txt
export TSAN_OPTIONS
endif
CRASHTEST_MAKE=$(MAKE) -f crash_test.mk
CRASHTEST_PY=$(PYTHON) -u tools/db_crashtest.py --stress_cmd=$(DB_STRESS_CMD) --cleanup_cmd='$(DB_CLEANUP_CMD)' --destroy_db_initially=1
+10 -4
View File
@@ -786,13 +786,19 @@ Status BlobFilePartitionManager::ResolveBlobDirectWriteIndex(
}
}
Status s = version != nullptr ? Status::Corruption("Invalid blob file number")
: Status::NotFound();
if (blob_file_cache == nullptr) {
return s;
return version != nullptr ? Status::Corruption("Invalid blob file number")
: Status::NotFound();
}
if (read_options.read_tier == kBlockCacheTier) {
// The direct-write fallback below may need to open the blob file reader,
// which `kBlockCacheTier` forbids. Keep the normal Version-backed path
// above eligible for cache-only hits.
return Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
}
Status s;
CacheHandleGuard<BlobFileReader> reader;
s = blob_file_cache->GetBlobFileReader(read_options, blob_idx.file_number(),
&reader,
+16 -13
View File
@@ -20,6 +20,7 @@
#include "table/format.h"
#include "table/multiget_context.h"
#include "test_util/sync_point.h"
#include "util/aligned_buffer.h"
#include "util/compression.h"
#include "util/stop_watch.h"
@@ -165,7 +166,7 @@ Status BlobFileReader::ReadHeader(const RandomAccessFileReader* file_reader,
Slice header_slice;
Buffer buf;
AlignedBuf aligned_buf;
AlignedBuffer direct_io_buffer;
{
TEST_SYNC_POINT("BlobFileReader::ReadHeader:ReadFromFile");
@@ -175,7 +176,7 @@ Status BlobFileReader::ReadHeader(const RandomAccessFileReader* file_reader,
const Status s =
ReadFromFile(file_reader, read_options, read_offset, read_size,
statistics, &header_slice, &buf, &aligned_buf);
statistics, &header_slice, &buf, &direct_io_buffer);
if (!s.ok()) {
return s;
}
@@ -216,7 +217,7 @@ Status BlobFileReader::ReadFooter(const RandomAccessFileReader* file_reader,
Slice footer_slice;
Buffer buf;
AlignedBuf aligned_buf;
AlignedBuffer direct_io_buffer;
{
TEST_SYNC_POINT("BlobFileReader::ReadFooter:ReadFromFile");
@@ -226,7 +227,7 @@ Status BlobFileReader::ReadFooter(const RandomAccessFileReader* file_reader,
const Status s =
ReadFromFile(file_reader, read_options, read_offset, read_size,
statistics, &footer_slice, &buf, &aligned_buf);
statistics, &footer_slice, &buf, &direct_io_buffer);
if (!s.ok()) {
return s;
}
@@ -257,10 +258,11 @@ Status BlobFileReader::ReadFromFile(const RandomAccessFileReader* file_reader,
const ReadOptions& read_options,
uint64_t read_offset, size_t read_size,
Statistics* statistics, Slice* slice,
Buffer* buf, AlignedBuf* aligned_buf) {
Buffer* buf,
AlignedBuffer* direct_io_buffer) {
assert(slice);
assert(buf);
assert(aligned_buf);
assert(direct_io_buffer);
assert(file_reader);
@@ -277,15 +279,15 @@ Status BlobFileReader::ReadFromFile(const RandomAccessFileReader* file_reader,
if (file_reader->use_direct_io()) {
constexpr char* scratch = nullptr;
AlignedBufferAllocationContext direct_io_context{direct_io_buffer};
s = file_reader->Read(io_options, read_offset, read_size, slice, scratch,
aligned_buf, &dbg);
&direct_io_context, &dbg);
} else {
buf->reset(new char[read_size]);
constexpr AlignedBuf* aligned_scratch = nullptr;
s = file_reader->Read(io_options, read_offset, read_size, slice, buf->get(),
aligned_scratch, &dbg);
nullptr, &dbg);
}
if (!s.ok()) {
@@ -349,7 +351,7 @@ Status BlobFileReader::GetBlob(
Slice record_slice;
Buffer buf;
AlignedBuf aligned_buf;
AlignedBuffer direct_io_buffer;
bool prefetched = false;
@@ -379,7 +381,7 @@ Status BlobFileReader::GetBlob(
const Status s =
ReadFromFile(file_reader_.get(), read_options, record_offset,
static_cast<size_t>(record_size), statistics_,
&record_slice, &buf, &aligned_buf);
&record_slice, &buf, &direct_io_buffer);
if (!s.ok()) {
return s;
}
@@ -477,7 +479,7 @@ void BlobFileReader::MultiGetBlob(
}
Buffer buf;
AlignedBuf aligned_buf;
AlignedBuffer direct_io_buffer;
Status s;
bool direct_io = file_reader_->use_direct_io();
@@ -500,8 +502,9 @@ void BlobFileReader::MultiGetBlob(
IODebugContext dbg;
s = file_reader_->PrepareIOOptions(read_options, opts, &dbg);
if (s.ok()) {
AlignedBufferAllocationContext direct_io_context{&direct_io_buffer};
s = file_reader_->MultiRead(opts, read_reqs.data(), read_reqs.size(),
direct_io ? &aligned_buf : nullptr, &dbg);
&direct_io_context, &dbg);
}
if (!s.ok()) {
for (auto& req : read_reqs) {
+1 -1
View File
@@ -108,7 +108,7 @@ class BlobFileReader {
const ReadOptions& read_options,
uint64_t read_offset, size_t read_size,
Statistics* statistics, Slice* slice, Buffer* buf,
AlignedBuf* aligned_buf);
AlignedBuffer* direct_io_buffer);
static Status VerifyBlob(const Slice& record_slice, const Slice& user_key,
uint64_t value_size);
+1
View File
@@ -74,6 +74,7 @@ Status BlobSource::GetBlobFromCache(
assert(cached_blob->GetValue());
PERF_COUNTER_ADD(blob_cache_hit_count, 1);
PERF_COUNTER_ADD(blob_cache_read_byte, cached_blob->GetValue()->size());
RecordTick(statistics_, BLOB_DB_CACHE_HIT);
RecordTick(statistics_, BLOB_DB_CACHE_BYTES_READ,
cached_blob->GetValue()->size());
+10
View File
@@ -229,6 +229,7 @@ TEST_F(BlobSourceTest, GetBlobsFromCache) {
// Retrieved the blob cache num_blobs * 3 times via TEST_BlobInCache,
// GetBlob, and TEST_BlobInCache.
ASSERT_EQ((int)get_perf_context()->blob_cache_hit_count, 0);
ASSERT_EQ((int)get_perf_context()->blob_cache_read_byte, 0);
ASSERT_EQ((int)get_perf_context()->blob_read_count, num_blobs);
ASSERT_EQ((int)get_perf_context()->blob_read_byte, total_bytes);
ASSERT_GE((int)get_perf_context()->blob_checksum_time, 0);
@@ -262,6 +263,8 @@ TEST_F(BlobSourceTest, GetBlobsFromCache) {
blob_bytes += blob_sizes[i];
total_bytes += bytes_read;
ASSERT_EQ((int)get_perf_context()->blob_cache_hit_count, i);
ASSERT_EQ((int)get_perf_context()->blob_cache_read_byte,
blob_bytes - blob_sizes[i]);
ASSERT_EQ((int)get_perf_context()->blob_read_count, i + 1);
ASSERT_EQ((int)get_perf_context()->blob_read_byte, total_bytes);
@@ -269,11 +272,13 @@ TEST_F(BlobSourceTest, GetBlobsFromCache) {
blob_offsets[i]));
ASSERT_EQ((int)get_perf_context()->blob_cache_hit_count, i + 1);
ASSERT_EQ((int)get_perf_context()->blob_cache_read_byte, blob_bytes);
ASSERT_EQ((int)get_perf_context()->blob_read_count, i + 1);
ASSERT_EQ((int)get_perf_context()->blob_read_byte, total_bytes);
}
ASSERT_EQ((int)get_perf_context()->blob_cache_hit_count, num_blobs);
ASSERT_EQ((int)get_perf_context()->blob_cache_read_byte, blob_bytes);
ASSERT_EQ((int)get_perf_context()->blob_read_count, num_blobs);
ASSERT_EQ((int)get_perf_context()->blob_read_byte, total_bytes);
@@ -312,6 +317,7 @@ TEST_F(BlobSourceTest, GetBlobsFromCache) {
// Retrieved the blob cache num_blobs * 3 times via TEST_BlobInCache,
// GetBlob, and TEST_BlobInCache.
ASSERT_EQ((int)get_perf_context()->blob_cache_hit_count, num_blobs * 3);
ASSERT_EQ((int)get_perf_context()->blob_cache_read_byte, blob_bytes * 3);
ASSERT_EQ((int)get_perf_context()->blob_read_count, 0); // without i/o
ASSERT_EQ((int)get_perf_context()->blob_read_byte, 0); // without i/o
@@ -690,6 +696,8 @@ TEST_F(BlobSourceTest, MultiGetBlobsFromMultiFiles) {
// TEST_BlobInCache.
ASSERT_EQ((int)get_perf_context()->blob_cache_hit_count,
num_blobs * blob_files);
ASSERT_EQ((int)get_perf_context()->blob_cache_read_byte,
blob_value_bytes * blob_files);
ASSERT_EQ((int)get_perf_context()->blob_read_count,
num_blobs * blob_files); // blocking i/o
ASSERT_EQ((int)get_perf_context()->blob_read_byte,
@@ -750,6 +758,8 @@ TEST_F(BlobSourceTest, MultiGetBlobsFromMultiFiles) {
// via MultiGetBlob and TEST_BlobInCache.
ASSERT_EQ((int)get_perf_context()->blob_cache_hit_count,
num_blobs * blob_files * 2);
ASSERT_EQ((int)get_perf_context()->blob_cache_read_byte,
blob_value_bytes * blob_files * 2);
ASSERT_EQ((int)get_perf_context()->blob_read_count,
0); // blocking i/o
ASSERT_EQ((int)get_perf_context()->blob_read_byte,
+1 -1
View File
@@ -278,7 +278,7 @@ Status BlobWriteBatchTransformer::MergeCF(uint32_t column_family_id,
Status BlobWriteBatchTransformer::PutBlobIndexCF(uint32_t column_family_id,
const Slice& key,
const Slice& value) {
// Already a blob index — pass through unchanged.
// Already a blob index -- pass through unchanged.
return WriteBatchInternal::PutBlobIndex(output_batch_, column_family_id, key,
value);
}
+1 -1
View File
@@ -35,7 +35,7 @@ struct BlobDirectWriteSettings {
CompressionType compression_type = kNoCompression;
// Compression options for newly written blob records.
CompressionOptions compression_opts;
// Raw pointer — the Cache is owned by ColumnFamilyOptions and outlives all
// Raw pointer -- the Cache is owned by ColumnFamilyOptions and outlives all
// settings snapshots. Using raw avoids 2 atomic ref-count ops per Put().
Cache* blob_cache = nullptr;
// Blob-cache prepopulation policy for direct-write records.
+41
View File
@@ -1620,6 +1620,47 @@ TEST_P(DBBlobBasicIOErrorTest, GetBlob_IOError) {
SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_P(DBBlobBasicIOErrorTest, GetEntityMergeWithBlobBaseIOError) {
// Goal: verify GetEntity preserves injected blob-read IOErrors when merge
// reads a blob-backed base value, instead of laundering them into Corruption.
// The test writes a blob-backed base value plus a merge operand, then injects
// an IOError at blob read time and checks both GetEntity and Get see it.
Options options;
options.env = fault_injection_env_.get();
options.enable_blob_files = true;
options.min_blob_size = 0;
options.merge_operator = MergeOperators::CreateStringAppendOperator();
Reopen(options);
constexpr char key[] = "key";
constexpr char base_value[] = "base_value";
ASSERT_OK(Put(key, base_value));
ASSERT_OK(Flush());
ASSERT_OK(Merge(key, "merge_operand"));
ASSERT_OK(Flush());
SyncPoint::GetInstance()->SetCallBack(sync_point_, [this](void* /* arg */) {
fault_injection_env_->SetFilesystemActive(false,
Status::IOError(sync_point_));
});
SyncPoint::GetInstance()->EnableProcessing();
PinnableWideColumns entity_result;
Status s = db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), key,
&entity_result);
ASSERT_TRUE(s.IsIOError()) << "Expected IOError but got: " << s.ToString();
PinnableSlice get_result;
s = db_->Get(ReadOptions(), db_->DefaultColumnFamily(), key, &get_result);
ASSERT_TRUE(s.IsIOError()) << "Expected IOError but got: " << s.ToString();
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_P(DBBlobBasicIOErrorMultiGetTest, MultiGetBlobs_IOError) {
Options options = GetDefaultOptions();
options.env = fault_injection_env_.get();
+60 -1
View File
@@ -128,6 +128,16 @@ class DBBlobIndexTest : public DBTestBase {
columns, s, is_blob_index, value_found);
}
bool MaybeResolveMemtableBlobValueForTest(const Slice& key,
const BlobFetcher* blob_fetcher,
PinnableSlice* value,
PinnableWideColumns* columns,
Status* s, bool* is_blob_index,
bool* value_found = nullptr) {
return DBImpl::MaybeResolveMemtableBlobValue(
key, blob_fetcher, value, columns, s, is_blob_index, value_found);
}
Options GetTestOptions() {
Options options;
options.env = CurrentOptions().env;
@@ -271,6 +281,55 @@ TEST_F(DBBlobIndexTest,
ASSERT_EQ(static_cast<char>(BlobIndex::Type::kUnknown), blob_index.front());
}
TEST_F(DBBlobIndexTest,
MaybeResolveMemtableBlobValueWithoutFetcherFailsClosed) {
// Goal: if a readonly/secondary memtable hit produces a blob-backed payload
// but no BlobFetcher is available, the helper must fail closed instead of
// handing raw blob-index bytes back to the caller as if they were the value.
std::string blob_index;
BlobIndex::EncodeBlob(&blob_index, /*file_number=*/123, /*offset=*/456,
/*size=*/789, kNoCompression);
PinnableSlice value;
value.GetSelf()->assign(blob_index.data(), blob_index.size());
value.PinSelf();
Status s = Status::OK();
bool is_blob_index = true;
ASSERT_TRUE(MaybeResolveMemtableBlobValueForTest(
Slice("key"), /*blob_fetcher=*/nullptr, &value, /*columns=*/nullptr, &s,
&is_blob_index));
ASSERT_TRUE(s.IsNotSupported()) << s.ToString();
ASSERT_TRUE(value.empty());
ASSERT_FALSE(is_blob_index);
}
TEST_F(DBBlobIndexTest, ReadOnlyGetImplReturnsBlobIndexWhenRequested) {
// Goal: cover the internal read-only GetImpl contract when the caller
// explicitly asks for raw blob-index bytes via `is_blob_index`. Recovery
// keeps the blob index in the memtable, and the read-only path must preserve
// the encoded index instead of eagerly resolving or rejecting it.
Options options = GetTestOptions();
DestroyAndReopen(options);
std::string blob_index;
BlobIndex::EncodeInlinedTTL(&blob_index, /*expiration=*/9876543210, "blob");
WriteBatch batch;
ASSERT_OK(PutBlobIndex(&batch, "blob_key", blob_index));
ASSERT_OK(Write(&batch));
Close();
options.avoid_flush_during_recovery = true;
ASSERT_OK(ReadOnlyReopen(options));
bool is_blob_index = false;
ASSERT_EQ(blob_index, GetImpl("blob_key", &is_blob_index));
ASSERT_TRUE(is_blob_index);
}
class PlainBlobValueFilterV3 : public CompactionFilter {
public:
PlainBlobValueFilterV3(std::atomic<int>* filter_call_count,
@@ -2138,7 +2197,7 @@ TEST_F(DBBlobIndexTest, EntityBlobFilterV3Remove) {
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
ASSERT_OK(Flush());
// Compact to trigger the filter — entity should be removed
// Compact to trigger the filter -- entity should be removed
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
// Key should be gone
+36
View File
@@ -3771,6 +3771,11 @@ void rocksdb_block_based_options_set_index_block_search_type(
static_cast<BlockBasedTableOptions::BlockSearchType>(v);
}
void rocksdb_block_based_options_set_uniform_cv_threshold(
rocksdb_block_based_table_options_t* options, double v) {
options->rep.uniform_cv_threshold = v;
}
void rocksdb_block_based_options_set_data_block_hash_ratio(
rocksdb_block_based_table_options_t* options, double v) {
options->rep.data_block_hash_table_util_ratio = v;
@@ -5286,6 +5291,15 @@ size_t rocksdb_options_get_recycle_log_file_num(rocksdb_options_t* opt) {
return opt->rep.recycle_log_file_num;
}
void rocksdb_options_set_async_wal_precreate(rocksdb_options_t* opt,
unsigned char v) {
opt->rep.async_wal_precreate = v;
}
unsigned char rocksdb_options_get_async_wal_precreate(rocksdb_options_t* opt) {
return opt->rep.async_wal_precreate;
}
void rocksdb_options_set_soft_pending_compaction_bytes_limit(
rocksdb_options_t* opt, size_t v) {
opt->rep.soft_pending_compaction_bytes_limit = v;
@@ -5389,6 +5403,16 @@ size_t rocksdb_options_get_memtable_huge_page_size(rocksdb_options_t* opt) {
return opt->rep.memtable_huge_page_size;
}
void rocksdb_options_set_memtable_batch_lookup_optimization(
rocksdb_options_t* opt, unsigned char v) {
opt->rep.memtable_batch_lookup_optimization = v;
}
unsigned char rocksdb_options_get_memtable_batch_lookup_optimization(
rocksdb_options_t* opt) {
return opt->rep.memtable_batch_lookup_optimization;
}
void rocksdb_options_set_hash_skip_list_rep(rocksdb_options_t* opt,
size_t bucket_count,
int32_t skiplist_height,
@@ -5820,6 +5844,8 @@ uint64_t rocksdb_perfcontext_metric(rocksdb_perfcontext_t* context,
return rep->number_async_seek;
case rocksdb_blob_cache_hit_count:
return rep->blob_cache_hit_count;
case rocksdb_blob_cache_read_byte:
return rep->blob_cache_read_byte;
case rocksdb_blob_read_count:
return rep->blob_read_count;
case rocksdb_blob_read_byte:
@@ -6238,6 +6264,16 @@ unsigned char rocksdb_readoptions_get_async_io(rocksdb_readoptions_t* opt) {
return opt->rep.async_io;
}
void rocksdb_readoptions_set_optimize_multiget_for_io(
rocksdb_readoptions_t* opt, unsigned char v) {
opt->rep.optimize_multiget_for_io = v;
}
unsigned char rocksdb_readoptions_get_optimize_multiget_for_io(
rocksdb_readoptions_t* opt) {
return opt->rep.optimize_multiget_for_io;
}
void rocksdb_readoptions_set_timestamp(rocksdb_readoptions_t* opt,
const char* ts, size_t tslen) {
if (ts == nullptr) {
+23
View File
@@ -888,6 +888,9 @@ int main(int argc, char** argv) {
rocksdb_block_based_options_set_block_cache(table_options, cache);
rocksdb_block_based_options_set_data_block_index_type(table_options, 1);
rocksdb_block_based_options_set_data_block_hash_ratio(table_options, 0.75);
rocksdb_block_based_options_set_index_block_search_type(
table_options, rocksdb_block_based_table_index_block_search_type_auto);
rocksdb_block_based_options_set_uniform_cv_threshold(table_options, 0.2);
rocksdb_block_based_options_set_top_level_index_pinning_tier(table_options,
1);
rocksdb_block_based_options_set_partition_pinning_tier(table_options, 2);
@@ -2812,6 +2815,15 @@ int main(int argc, char** argv) {
rocksdb_options_set_memtable_huge_page_size(o, 25);
CheckCondition(25 == rocksdb_options_get_memtable_huge_page_size(o));
// memtable_batch_lookup_optimization defaults to 0; flip to 1 to verify
// the setter is wired through to the underlying C++ ColumnFamilyOptions
// field.
CheckCondition(0 ==
rocksdb_options_get_memtable_batch_lookup_optimization(o));
rocksdb_options_set_memtable_batch_lookup_optimization(o, 1);
CheckCondition(1 ==
rocksdb_options_get_memtable_batch_lookup_optimization(o));
rocksdb_options_set_max_successive_merges(o, 26);
CheckCondition(26 == rocksdb_options_get_max_successive_merges(o));
@@ -2869,6 +2881,9 @@ int main(int argc, char** argv) {
rocksdb_options_set_track_and_verify_wals_in_manifest(o, 42);
CheckCondition(1 ==
rocksdb_options_get_track_and_verify_wals_in_manifest(o));
CheckCondition(0 == rocksdb_options_get_async_wal_precreate(o));
rocksdb_options_set_async_wal_precreate(o, 1);
CheckCondition(1 == rocksdb_options_get_async_wal_precreate(o));
/* Blob Options */
rocksdb_options_set_enable_blob_files(o, 1);
@@ -2995,6 +3010,8 @@ int main(int argc, char** argv) {
rocksdb_options_get_memtable_prefix_bloom_size_ratio(copy));
CheckCondition(24 == rocksdb_options_get_max_compaction_bytes(copy));
CheckCondition(25 == rocksdb_options_get_memtable_huge_page_size(copy));
CheckCondition(
1 == rocksdb_options_get_memtable_batch_lookup_optimization(copy));
CheckCondition(26 == rocksdb_options_get_max_successive_merges(copy));
CheckCondition(27 == rocksdb_options_get_bloom_locality(copy));
CheckCondition(1 == rocksdb_options_get_inplace_update_support(copy));
@@ -3429,6 +3446,12 @@ int main(int argc, char** argv) {
rocksdb_readoptions_set_async_io(ro, 1);
CheckCondition(1 == rocksdb_readoptions_get_async_io(ro));
// optimize_multiget_for_io defaults to true (1); flip to 0 to verify the
// setter is wired through to the underlying C++ ReadOptions field.
CheckCondition(1 == rocksdb_readoptions_get_optimize_multiget_for_io(ro));
rocksdb_readoptions_set_optimize_multiget_for_io(ro, 0);
CheckCondition(0 == rocksdb_readoptions_get_optimize_multiget_for_io(ro));
rocksdb_readoptions_destroy(ro);
}
+36
View File
@@ -734,6 +734,42 @@ TEST_P(ColumnFamilyTest, AddDrop) {
std::vector<std::string>({"default", "four", "three"}));
}
TEST_P(ColumnFamilyTest, EmptyNameRejected) {
// Creating a column family with an empty-string name should be rejected with
// InvalidArgument. Empty strings are reserved by various RocksDB APIs and
// serialization formats to mean "unknown/unspecified column family" (e.g.
// TablePropertiesCollectorFactory::Context::kUnknownColumnFamily, table
// properties).
//
// Prior behavior was silently broken: CreateColumnFamily("") would return
// OK and a usable handle, but the CF was not persisted in the manifest --
// data written to it was lost on DB reopen.
Open();
ColumnFamilyHandle* handle = nullptr;
Status s =
db_->CreateColumnFamily(column_family_options_, std::string(), &handle);
ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString();
ASSERT_EQ(handle, nullptr);
// Same via the bulk-by-names API. The first valid name should still get
// created; the failure should come on the empty one.
std::vector<ColumnFamilyHandle*> handles;
s = db_->CreateColumnFamilies(column_family_options_,
{"ok_name", "", "another"}, &handles);
ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString();
// Clean up the partially-created handle ("ok_name") before the failure.
ASSERT_EQ(handles.size(), 1U);
ASSERT_OK(db_->DropColumnFamily(handles[0]));
ASSERT_OK(db_->DestroyColumnFamilyHandle(handles[0]));
handles.clear();
// Same via the bulk-by-descriptor API.
s = db_->CreateColumnFamilies(
{ColumnFamilyDescriptor("", column_family_options_)}, &handles);
ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString();
ASSERT_TRUE(handles.empty());
}
TEST_P(ColumnFamilyTest, BulkAddDrop) {
constexpr int kNumCF = 1000;
ColumnFamilyOptions cf_options;
+18
View File
@@ -454,6 +454,18 @@ class Compaction {
return notify_on_compaction_completion_;
}
// Called by DBImpl::NotifyOnCompactionPreCommit to ensure the
// OnCompactionPreCommit listener callback fires at most once per
// compaction lifecycle, even though the notify helper is invoked at multiple
// potential ReleaseCompactionFiles() sites for safety.
void SetNotifyOnCompactionPreCommitCalled() {
notify_on_compaction_pre_commit_called_ = true;
}
bool WasNotifyOnCompactionPreCommitCalled() const {
return notify_on_compaction_pre_commit_called_;
}
static constexpr int kInvalidLevel = -1;
// Evaluate proximal output level. If the compaction supports
@@ -626,6 +638,12 @@ class Compaction {
// begin.
bool notify_on_compaction_completion_;
// Tracks whether OnCompactionPreCommit has already fired for this
// compaction. The notify helper is called from multiple potential
// ReleaseCompactionFiles() call sites; this flag guarantees that listeners
// observe the callback exactly once per compaction.
bool notify_on_compaction_pre_commit_called_ = false;
// Enable/disable GC collection for blobs during compaction.
bool enable_blob_garbage_collection_;
+1 -1
View File
@@ -75,7 +75,7 @@ class CompactionBlobResolver : public WideColumnBlobResolver {
bool track_resolve_error_ = false;
// Cache for resolved blob values to avoid re-fetching. Uses a vector of
// (column_index, PinnableSlice) pairs — typical entities have few blob
// (column_index, PinnableSlice) pairs -- typical entities have few blob
// columns (<5), making linear scan cheaper than hash map overhead.
std::vector<std::pair<size_t, std::unique_ptr<PinnableSlice>>>
resolved_cache_;
+10 -2
View File
@@ -2065,10 +2065,18 @@ Status CompactionJob::FinishCompactionOutputFile(
std::pair<SequenceNumber, SequenceNumber> keep_seqno_range{
0, kMaxSequenceNumber};
if (sub_compact->compaction->SupportsPerKeyPlacement()) {
// Point entries are routed to proximal output only when their seqno is
// strictly greater than `proximal_after_seqno_`. Range tombstones use a
// [lower, upper) filter, so split them at the next seqno to preserve the
// same boundary.
SequenceNumber range_del_split_seqno = proximal_after_seqno_;
if (range_del_split_seqno < kMaxSequenceNumber) {
range_del_split_seqno++;
}
if (outputs.IsProximalLevel()) {
keep_seqno_range.first = proximal_after_seqno_;
keep_seqno_range.first = range_del_split_seqno;
} else {
keep_seqno_range.second = proximal_after_seqno_;
keep_seqno_range.second = range_del_split_seqno;
}
}
CompactionIterationStats range_del_out_stats;
+9 -9
View File
@@ -251,7 +251,7 @@ Compaction* FIFOCompactionPicker::PickSizeCompaction(
// When using blob-aware sizing, use proportional estimation (same
// principle as EstimateTotalDataForSST): each SST "owns"
// effective_size / num_files of total data. This is an approximation
// — individual SSTs may reference different amounts of blob data,
// -- individual SSTs may reference different amounts of blob data,
// but uniform distribution is a reasonable estimate for FIFO dropping.
uint64_t remaining_size = effective_size;
const uint64_t num_files = last_level_files.size();
@@ -477,12 +477,12 @@ Compaction* FIFOCompactionPicker::PickIntraL0Compaction(
if (fifo_opts.max_data_files_size == 0) {
ROCKS_LOG_BUFFER(
log_buffer,
"[%s] FIFO kv-ratio compaction: skipping — "
"[%s] FIFO kv-ratio compaction: skipping -- "
"max_data_files_size is 0, cannot compute target file size. ",
cf_name.c_str());
} else if (fifo_opts.max_data_files_size < fifo_opts.max_table_files_size) {
ROCKS_LOG_BUFFER(log_buffer,
"[%s] FIFO kv-ratio compaction: skipping — "
"[%s] FIFO kv-ratio compaction: skipping -- "
"max_data_files_size (%" PRIu64
") < max_table_files_size "
"(%" PRIu64 ").",
@@ -547,7 +547,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
for (int level = 1; level < vstorage->num_levels(); ++level) {
if (!vstorage->LevelFiles(level).empty()) {
ROCKS_LOG_BUFFER(log_buffer,
"[%s] FIFO kv-ratio compaction: skipping — non-L0 "
"[%s] FIFO kv-ratio compaction: skipping -- non-L0 "
"level %d still has %" ROCKSDB_PRIszt
" files (migration in progress)",
cf_name.c_str(), level,
@@ -587,7 +587,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
// once per flush or compaction completion, so no caching is needed.
uint64_t target = 0;
if (mutable_cf_options.max_compaction_bytes > 0) {
// User explicitly set max_compaction_bytes — use it as target
// User explicitly set max_compaction_bytes -- use it as target
target = mutable_cf_options.max_compaction_bytes;
} else {
// Auto-calculate from capacity and observed SST/blob ratio
@@ -642,7 +642,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
// of linear) write amplification.
// Build tier boundaries from smallest to largest.
// Stop at 10KB minimum — SST files of most workloads are larger than
// Stop at 10KB minimum -- SST files of most workloads are larger than
// this, so lower boundaries would only waste CPU scanning L0 files.
// Files smaller than the lowest boundary simply merge at that boundary.
static constexpr uint64_t kMinTierBoundary = 10 * 1024; // 10KB
@@ -651,7 +651,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
boundaries.push_back(b);
}
if (boundaries.empty()) {
// target itself is below kMinTierBoundary — use target as the
// target itself is below kMinTierBoundary -- use target as the
// sole boundary so we can still compact at the target size.
boundaries.push_back(target);
}
@@ -668,7 +668,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
continue;
}
// Found a file < boundary — collect contiguous batch
// Found a file < boundary -- collect contiguous batch
std::vector<FileMetaData*> batch;
uint64_t accumulated = 0;
size_t pos = scan;
@@ -713,7 +713,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
return c;
}
// This batch wasn't enough — advance past it
// This batch wasn't enough -- advance past it
scan = pos;
}
}
+1 -1
View File
@@ -1101,7 +1101,7 @@ TEST_F(CompactionPickerTest, ReadTriggeredSkipsLastLevel) {
Add(0, 1U, "150", "200", kFileSize, 0, 500, 550);
Add(4, 3U, "301", "350", kFileSize, 0, 101, 150);
// File 3 is at the last non-empty level — should NOT be marked for
// File 3 is at the last non-empty level -- should NOT be marked for
// read-triggered compaction. Bottommost file cleanup is handled
// separately by ComputeBottommostFilesMarkedForCompaction().
file_map_[3U].first->stats.num_collapsible_entry_reads_sampled.store(
+29 -10
View File
@@ -189,14 +189,24 @@ CompactionJob::ProcessKeyValueCompactionWithCompactionService(
return compaction_status;
}
// CompactionServiceJobStatus::kSuccess was returned, but somehow we failed to
// read the result. Consider this as an installation failure
if (!s.ok()) {
sub_compact->status = s;
// Wait() returned kSuccess, but the primary host could not deserialize the
// remote result before importing any output files into this DB. The remote
// output is still isolated in the service-managed staging directory, so it
// is safe to fall back to local compaction for the same job and notify the
// service via OnInstallation(kUseLocal).
assert(sub_compact->status.ok());
assert(sub_compact->GetMutableCompactionOutputs().empty());
assert(sub_compact->GetMutableProximalOutputs().empty());
ROCKS_LOG_WARN(db_options_.info_log,
"[%s] [JOB %d] Failed to parse remote compaction result "
"(%s), falling back to local compaction",
compaction->column_family_data()->GetName().c_str(), job_id_,
s.ToString().c_str());
compaction_result.status.PermitUncheckedError();
db_options_.compaction_service->OnInstallation(
response.scheduled_job_id, CompactionServiceJobStatus::kFailure);
return CompactionServiceJobStatus::kFailure;
response.scheduled_job_id, CompactionServiceJobStatus::kUseLocal);
return CompactionServiceJobStatus::kUseLocal;
}
sub_compact->status = compaction_result.status;
@@ -641,6 +651,10 @@ static std::unordered_map<std::string, OptionTypeInfo>
{"cpu_micros",
{offsetof(struct CompactionJobStats, cpu_micros), OptionType::kUInt64T,
OptionVerificationType::kNormal, OptionTypeFlags::kNone}},
{"has_accurate_num_input_records",
{offsetof(struct CompactionJobStats, has_accurate_num_input_records),
OptionType::kBoolean, OptionVerificationType::kNormal,
OptionTypeFlags::kNone}},
{"num_input_records",
{offsetof(struct CompactionJobStats, num_input_records),
OptionType::kUInt64T, OptionVerificationType::kNormal,
@@ -846,11 +860,16 @@ static std::unordered_map<std::string, OptionTypeInfo>
{offsetof(struct InternalStats::CompactionStats, count),
OptionType::kUInt64T, OptionVerificationType::kNormal,
OptionTypeFlags::kNone}},
{"counts", OptionTypeInfo::Array<
int, static_cast<int>(CompactionReason::kNumOfReasons)>(
offsetof(struct InternalStats::CompactionStats, counts),
OptionVerificationType::kNormal, OptionTypeFlags::kNone,
{0, OptionType::kInt})},
{"counts",
OptionTypeInfo::Array<
/* In release 11.2, a new compaction reason was added. This broken
* the reader and writer. To unblock release 11.1, we temporarily
* reduce the count array size to the old one. TODO add a proper
* serialization and deserialization method. */
int, static_cast<int>(CompactionReason::kNumOfReasons) - 1>(
offsetof(struct InternalStats::CompactionStats, counts),
OptionVerificationType::kNormal, OptionTypeFlags::kNone,
{0, OptionType::kInt})},
};
static std::unordered_map<std::string, OptionTypeInfo>
+45 -3
View File
@@ -127,6 +127,7 @@ class MyTestCompactionService : public CompactionService {
void OnInstallation(const std::string& /*scheduled_job_id*/,
CompactionServiceJobStatus status) override {
installation_callback_count_.fetch_add(1);
final_updated_status_ = status;
}
@@ -167,6 +168,8 @@ class MyTestCompactionService : public CompactionService {
return final_updated_status_.load();
}
int GetOnInstallationCount() { return installation_callback_count_.load(); }
protected:
InstrumentedMutex mutex_;
const std::string db_path_;
@@ -195,6 +198,7 @@ class MyTestCompactionService : public CompactionService {
std::vector<std::shared_ptr<EventListener>> listeners_;
std::vector<std::shared_ptr<TablePropertiesCollectorFactory>>
table_properties_collector_factories_;
std::atomic_int installation_callback_count_{0};
std::atomic<CompactionServiceJobStatus> final_updated_status_{
CompactionServiceJobStatus::kUseLocal};
@@ -939,6 +943,37 @@ TEST_F(CompactionServiceTest, VerifyInputRecordCount) {
SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_F(CompactionServiceTest, InaccurateInputRecordCount) {
Options options = CurrentOptions();
options.disable_auto_compactions = true;
ReopenWithCompactionService(&options);
GenerateTestData();
auto my_cs = GetCompactionService();
std::string start_str = Key(15);
std::string end_str = Key(45);
Slice start(start_str);
Slice end(end_str);
uint64_t comp_num = my_cs->GetCompactionNum();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"CompactionServiceCompactionJob::Run:0", [&](void* arg) {
CompactionServiceResult* compaction_result =
*(static_cast<CompactionServiceResult**>(arg));
ASSERT_TRUE(compaction_result != nullptr);
compaction_result->stats.has_accurate_num_input_records = false;
compaction_result->stats.num_input_records = 0;
});
SyncPoint::GetInstance()->EnableProcessing();
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), &start, &end));
ASSERT_GE(my_cs->GetCompactionNum(), comp_num + 1);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_F(CompactionServiceTest, EmptyResult) {
Options options = CurrentOptions();
options.disable_auto_compactions = true;
@@ -1448,7 +1483,7 @@ TEST_F(CompactionServiceTest, FailedToStart) {
ASSERT_TRUE(s.IsIncomplete());
}
TEST_F(CompactionServiceTest, InvalidResult) {
TEST_F(CompactionServiceTest, InvalidResultFallsBackToLocal) {
Options options = CurrentOptions();
options.disable_auto_compactions = true;
ReopenWithCompactionService(&options);
@@ -1456,15 +1491,22 @@ TEST_F(CompactionServiceTest, InvalidResult) {
GenerateTestData();
auto my_cs = GetCompactionService();
ASSERT_EQ(0, my_cs->GetOnInstallationCount());
my_cs->OverrideWaitResult("Invalid Str");
int compaction_num_before = my_cs->GetCompactionNum();
std::string start_str = Key(15);
std::string end_str = Key(45);
Slice start(start_str);
Slice end(end_str);
// The remote result is malformed before any installation starts, so the
// primary should recover by rerunning the compaction locally for this job.
Status s = db_->CompactRange(CompactRangeOptions(), &start, &end);
ASSERT_FALSE(s.ok());
ASSERT_EQ(CompactionServiceJobStatus::kFailure,
ASSERT_OK(s);
ASSERT_GE(my_cs->GetCompactionNum(), compaction_num_before + 1);
VerifyTestData();
ASSERT_EQ(1, my_cs->GetOnInstallationCount());
ASSERT_EQ(CompactionServiceJobStatus::kUseLocal,
my_cs->GetFinalCompactionServiceJobStatus());
}
+55
View File
@@ -2897,6 +2897,61 @@ TEST_P(PrecludeLastLevelTest, RangeDelsCauseFileEndpointsToOverlap) {
Close();
}
TEST_F(PrecludeLastLevelTestBase,
RangeDelAtProximalSeqnoBoundaryStaysInLastLevel) {
constexpr int kNumLevels = 7;
Options options = CurrentOptions();
options.env = mock_env_.get();
options.num_levels = kNumLevels;
options.disable_auto_compactions = true;
DestroyAndReopen(options);
Defer close_db([&] { Close(); });
ASSERT_OK(Put(Key(2), "old"));
ASSERT_OK(Put(Key(12), "old"));
ManagedSnapshot snapshot(db_.get());
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(2),
Key(12)));
ASSERT_OK(Flush());
MoveFilesToLevel(6);
const int l5_file_keys[][2] = {{0, 4}, {5, 9}};
for (const auto& l5_file : l5_file_keys) {
ASSERT_OK(Put(Key(l5_file[0]), "hot"));
ASSERT_OK(Put(Key(l5_file[1]), "hot"));
ASSERT_OK(Flush());
MoveFilesToLevel(5);
}
ASSERT_EQ("0,0,0,0,0,2,1", FilesPerLevel());
SyncPoint::GetInstance()->SetCallBack(
"CompactionJob::PrepareTimes():preclude_last_level_min_seqno",
[](void* arg) { *static_cast<SequenceNumber*>(arg) = 0; });
SyncPoint::GetInstance()->EnableProcessing();
ASSERT_OK(db_->SetOptions({{"preclude_last_level_data_seconds", "10000"}}));
auto l5_files = GetLevelFileMetadatas(5);
auto l6_files = GetLevelFileMetadatas(6);
ASSERT_EQ(2, l5_files.size());
ASSERT_EQ(1, l6_files.size());
ASSERT_OK(db_->CompactFiles(CompactionOptions(),
{MakeTableFileName(l5_files[1]->fd.GetNumber()),
MakeTableFileName(l6_files[0]->fd.GetNumber())},
6));
uint64_t l6_range_deletions = 0;
for (auto* f : GetLevelFileMetadatas(5)) {
ASSERT_EQ(0, f->num_range_deletions);
}
for (auto* f : GetLevelFileMetadatas(6)) {
l6_range_deletions += f->num_range_deletions;
}
ASSERT_GT(l6_range_deletions, 0);
}
// Tests DBIter::GetProperty("rocksdb.iterator.write-time") return a data's
// approximate write unix time.
class IteratorWriteTimeTest
+856
View File
@@ -102,6 +102,862 @@ TEST_F(DBBasicTest, OpenWhenOpen) {
ASSERT_TRUE(strstr(s.getState(), "lock ") != nullptr);
}
namespace {
// Helper that captures per-branch SkippedNoopEdit counts for tests.
struct RecoveryOptimizationCounters {
std::atomic<int> setup_dbid{0};
std::atomic<int> per_cf{0};
std::atomic<int> wal_deletion{0};
std::atomic<int> next_file_number{0};
void Install() {
auto* sp = ROCKSDB_NAMESPACE::SyncPoint::GetInstance();
sp->SetCallBack("DBImpl::Recovery:SkippedNoopEdit:SetupDBId",
[this](void*) { setup_dbid.fetch_add(1); });
sp->SetCallBack("DBImpl::Recovery:SkippedNoopEdit:PerCF",
[this](void*) { per_cf.fetch_add(1); });
sp->SetCallBack("DBImpl::Recovery:SkippedNoopEdit:WalDeletion",
[this](void*) { wal_deletion.fetch_add(1); });
sp->SetCallBack("DBImpl::Recovery:SkippedNoopEdit:NextFileNumber",
[this](void*) { next_file_number.fetch_add(1); });
sp->EnableProcessing();
}
void Uninstall() {
auto* sp = ROCKSDB_NAMESPACE::SyncPoint::GetInstance();
sp->DisableProcessing();
sp->ClearAllCallBacks();
}
};
} // namespace
// optimize_manifest_for_recovery=true: a clean reopen of a flushed DB must
// append fewer individual records to the MANIFEST than the default-off path.
// Verified by counting AddRecord calls (one per VersionEdit written to the
// MANIFEST log).
TEST_F(DBBasicTest,
OptimizeManifestForRecoveryReducesManifestWritesOnCleanReopen) {
Options options = CurrentOptions();
options.create_if_missing = true;
auto* sp = ROCKSDB_NAMESPACE::SyncPoint::GetInstance();
// Measure MANIFEST records with optimize_manifest_for_recovery OFF (default).
DestroyAndReopen(options);
ASSERT_OK(Put("k1", "v1"));
ASSERT_OK(Flush());
Close();
std::atomic<int> records_off{0};
std::atomic<int> next_file_skips_off{0};
sp->SetCallBack("VersionSet::ProcessManifestWrites:AddRecord",
[&](void*) { records_off.fetch_add(1); });
sp->SetCallBack("DBImpl::Recovery:SkippedNoopEdit:NextFileNumber",
[&](void*) { next_file_skips_off.fetch_add(1); });
sp->EnableProcessing();
Reopen(options);
sp->DisableProcessing();
sp->ClearAllCallBacks();
ASSERT_EQ("v1", Get("k1"));
int off_count = records_off.load();
ASSERT_GT(off_count, 0);
ASSERT_EQ(0, next_file_skips_off.load());
Close();
// Measure MANIFEST records with optimize_manifest_for_recovery ON.
options.optimize_manifest_for_recovery = true;
DestroyAndReopen(options);
ASSERT_OK(Put("k2", "v2"));
ASSERT_OK(Flush());
Close();
std::atomic<int> records_on{0};
std::atomic<int> next_file_skips_on{0};
sp->SetCallBack("VersionSet::ProcessManifestWrites:AddRecord",
[&](void*) { records_on.fetch_add(1); });
sp->SetCallBack("DBImpl::Recovery:SkippedNoopEdit:NextFileNumber",
[&](void*) { next_file_skips_on.fetch_add(1); });
sp->EnableProcessing();
Reopen(options);
sp->DisableProcessing();
sp->ClearAllCallBacks();
ASSERT_EQ("v2", Get("k2"));
int on_count = records_on.load();
ASSERT_GT(next_file_skips_on.load(), 0);
ASSERT_LT(on_count, off_count);
}
// When the per-CF log_number actually advances, the per-CF skip must NOT
// fire -- the edit carries real information and must be emitted.
TEST_F(DBBasicTest, OptimizeManifestForRecoveryEmitsPerCFWhenLogAdvances) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
// Write data and close WITHOUT flushing -- the WAL has un-replayed
// records, so on reopen recovery flushes the memtable and the per-CF
// edit's log_number must advance past the prior WAL.
Close();
RecoveryOptimizationCounters counters;
counters.Install();
Reopen(options);
counters.Uninstall();
ASSERT_EQ(0, counters.per_cf.load());
ASSERT_EQ("v", Get("k"));
}
// With track_and_verify_wals_in_manifest=true, the wal_deletion edit
// must STILL be emitted (the DeleteWalsBefore record is required).
TEST_F(DBBasicTest, OptimizeManifestForRecoveryPreservesWalTracking) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
options.track_and_verify_wals_in_manifest = true;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
Close();
RecoveryOptimizationCounters counters;
counters.Install();
Reopen(options);
counters.Uninstall();
ASSERT_EQ(0, counters.wal_deletion.load());
}
// best_efforts_recovery requires a fresh MANIFEST + CURRENT to be
// produced on every open (the salvage contract). Even with the option
// on, none of the SkippedNoopEdit branches must fire under
// best_efforts_recovery -- otherwise CURRENT can be left missing or
// stale (regression caught by DBBasicTest.RecoverWithNoCurrentFile and
// DBTest2.BestEffortsRecoveryWithSstUniqueIdVerification under an
// option-on default).
TEST_F(DBBasicTest, OptimizeManifestForRecoveryDisabledByBestEffortsRecovery) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
options.best_efforts_recovery = true;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
Close();
RecoveryOptimizationCounters counters;
counters.Install();
Reopen(options);
counters.Uninstall();
ASSERT_EQ(0, counters.setup_dbid.load());
ASSERT_EQ(0, counters.per_cf.load());
ASSERT_EQ(0, counters.wal_deletion.load());
ASSERT_EQ(0, counters.next_file_number.load());
ASSERT_EQ("v", Get("k"));
}
// Multi-column-family coverage: with one CF flushed and another not,
// the un-flushed CF's edit must still be emitted.
TEST_F(DBBasicTest, OptimizeManifestForRecoveryMultiCF) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
CreateAndReopenWithCF({"pikachu", "raichu"}, options);
ASSERT_OK(Put(0, "k", "v0"));
ASSERT_OK(Put(1, "k", "v1"));
ASSERT_OK(Put(2, "k", "v2"));
ASSERT_OK(Flush(0));
// CF 1 and 2 keep dirty memtables -- recovery must emit their per-CF
// edits (log_number advance from WAL replay).
Close();
RecoveryOptimizationCounters counters;
counters.Install();
ReopenWithColumnFamilies({"default", "pikachu", "raichu"}, options);
counters.Uninstall();
ASSERT_EQ("v0", Get(0, "k"));
ASSERT_EQ("v1", Get(1, "k"));
ASSERT_EQ("v2", Get(2, "k"));
}
// MaybeUpdateNextFileNumber's seed value (next_file_number_) makes the
// post-loop comparison trivially true on every clean recovery, causing a
// no-op SetNextFile edit to be appended to the MANIFEST. With
// optimize_manifest_for_recovery=true, that emission is gated and
// the SkippedNoopEdit:NextFileNumber sync point fires in its place.
TEST_F(DBBasicTest, OptimizeManifestForRecoverySkipsNextFileNumber) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
Close();
RecoveryOptimizationCounters counters;
counters.Install();
Reopen(options);
counters.Uninstall();
ASSERT_EQ(1, counters.next_file_number.load());
}
// With option=true and a synthetic on-disk file whose number is at or
// above next_file_number_, MaybeUpdateNextFileNumber MUST emit the
// SetNextFile edit and advance the counter past the synthetic number.
TEST_F(DBBasicTest,
OptimizeManifestForRecoveryEmitsNextFileNumberWhenJustified) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
const uint64_t next_before_close =
dbfull()->GetVersionSet()->current_next_file_number();
Close();
// Drop an empty .sst with a number well above next_file_number into
// the DB dir. MaybeUpdateNextFileNumber must observe it and advance.
const uint64_t synthetic_number = next_before_close + 100;
const std::string synthetic_sst =
dbname_ + "/" + MakeTableFileName("", synthetic_number);
ASSERT_OK(WriteStringToFile(env_, "" /*data*/, synthetic_sst,
/*should_sync=*/true));
RecoveryOptimizationCounters counters;
counters.Install();
Reopen(options);
counters.Uninstall();
ASSERT_EQ(0, counters.next_file_number.load());
ASSERT_GT(dbfull()->GetVersionSet()->current_next_file_number(),
synthetic_number);
}
// 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.
TEST_F(DBBasicTest, OptimizeManifestForRecoveryAdvancesWalMarkersOnClose) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
uint64_t max_wal_before_close = 0;
auto* sp = ROCKSDB_NAMESPACE::SyncPoint::GetInstance();
sp->SetCallBack("DBImpl::CloseHelper:CapturedMaxWal", [&](void* arg) {
max_wal_before_close = *static_cast<uint64_t*>(arg);
});
sp->EnableProcessing();
Close();
sp->DisableProcessing();
sp->ClearAllCallBacks();
ASSERT_GT(max_wal_before_close, 0u);
Reopen(options);
ASSERT_EQ(max_wal_before_close + 1,
dbfull()->GetVersionSet()->min_log_number_to_keep());
ASSERT_EQ("v", Get("k"));
}
// Shared-option matrix: default-off and disable-before-close should both fall
// back to recovery-time MANIFEST work, while enable-through-close should avoid
// MANIFEST appends on a clean reopen.
TEST_F(DBBasicTest, OptimizeManifestForRecoveryCleanReopenMatrix) {
struct TestCase {
const char* name;
bool enable_on_open;
bool disable_before_close;
bool expect_close_write;
};
const TestCase test_cases[] = {
{"default_off", false, false, false},
{"enabled", true, false, true},
{"disabled_before_close", true, true, false},
};
auto* sp = ROCKSDB_NAMESPACE::SyncPoint::GetInstance();
for (const auto& test_case : test_cases) {
SCOPED_TRACE(test_case.name);
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = test_case.enable_on_open;
DestroyAndReopen(options);
ASSERT_OK(Put("k", test_case.name));
ASSERT_OK(Flush());
if (test_case.disable_before_close) {
ASSERT_OK(dbfull()->SetDBOptions(
{{"optimize_manifest_for_recovery", "false"}}));
}
std::atomic<int> entered{0};
sp->SetCallBack("DBImpl::CloseHelper:WriteWalMarkersOnCloseEntered",
[&](void*) { entered.fetch_add(1); });
sp->EnableProcessing();
Close();
sp->DisableProcessing();
sp->ClearAllCallBacks();
std::atomic<int> records{0};
RecoveryOptimizationCounters counters;
counters.Install();
sp->SetCallBack("VersionSet::ProcessManifestWrites:AddRecord",
[&](void*) { records.fetch_add(1); });
Reopen(options);
counters.Uninstall();
if (test_case.expect_close_write) {
ASSERT_EQ(1, entered.load());
ASSERT_EQ(0, records.load());
ASSERT_GT(counters.per_cf.load(), 0);
} else {
ASSERT_EQ(0, entered.load());
ASSERT_GT(records.load(), 0);
}
ASSERT_EQ(test_case.name, Get("k"));
Close();
}
}
// allow_2pc=true: even with the option on, the close-time write must NOT
// advance MinLogNumberToKeep / DeleteWalsBefore (2pc requires the WAL to
// remain replayable for uncommitted prepared transactions).
TEST_F(DBBasicTest, OptimizeManifestForRecoveryRespectsTwoPC) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
options.allow_2pc = true;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
const uint64_t min_log_before_close =
dbfull()->GetVersionSet()->min_log_number_to_keep();
Close();
Reopen(options);
ASSERT_EQ(min_log_before_close,
dbfull()->GetVersionSet()->min_log_number_to_keep());
ASSERT_EQ("v", Get("k"));
}
// Mixed-CF emptiness: on reopen, recovery should still have MANIFEST work to
// do for the dirty CF while skipping per-CF recovery edits for the empty CFs
// whose markers were persisted at close.
TEST_F(DBBasicTest, OptimizeManifestForRecoverySkipsNonEmptyCF) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
CreateAndReopenWithCF({"empty_cf", "dirty_cf"}, options);
ASSERT_OK(Put(1, "k", "v1"));
ASSERT_OK(Flush(1));
ASSERT_OK(Put(2, "k", "v2"));
Close();
RecoveryOptimizationCounters counters;
std::atomic<int> records{0};
auto* sp = ROCKSDB_NAMESPACE::SyncPoint::GetInstance();
counters.Install();
sp->SetCallBack("VersionSet::ProcessManifestWrites:AddRecord",
[&](void*) { records.fetch_add(1); });
ReopenWithColumnFamilies({"default", "empty_cf", "dirty_cf"}, options);
counters.Uninstall();
ASSERT_GT(counters.per_cf.load(), 0);
ASSERT_GT(records.load(), 0);
ASSERT_EQ("v1", Get(1, "k"));
ASSERT_EQ("v2", Get(2, "k"));
}
// track_and_verify_wals_in_manifest=true: the close-time write must leave
// recovery with only the mandatory WAL-tracking MANIFEST work. The per-CF
// recovery edits should still skip.
TEST_F(DBBasicTest, OptimizeManifestForRecoveryEmitsWalDeletionWhenTracking) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
options.track_and_verify_wals_in_manifest = true;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
Close();
RecoveryOptimizationCounters counters;
std::atomic<int> records{0};
auto* sp = ROCKSDB_NAMESPACE::SyncPoint::GetInstance();
counters.Install();
sp->SetCallBack("VersionSet::ProcessManifestWrites:AddRecord",
[&](void*) { records.fetch_add(1); });
Reopen(options);
counters.Uninstall();
ASSERT_GT(counters.per_cf.load(), 0);
ASSERT_EQ(1, records.load());
ASSERT_GT(dbfull()->GetVersionSet()->GetWalSet().GetMinWalNumberToKeep(), 0u);
ASSERT_EQ("v", Get("k"));
}
// Regression for the close-time marker path: if a new WAL is created while an
// otherwise-empty user CF stays empty, Close() must reserve the next file
// number before persisting SetLogNumber(cur_wal + 1) for that CF.
TEST_F(DBBasicTest, OptimizeManifestForRecoveryImmediateCloseAfterWarmReopen) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
CreateAndReopenWithCF({"empty_cf"}, options);
ASSERT_OK(Put(0, "k", "v"));
const uint64_t wal_before_switch = dbfull()->TEST_LogfileNumber();
ASSERT_OK(dbfull()->TEST_SwitchMemtable());
ASSERT_GT(dbfull()->TEST_LogfileNumber(), wal_before_switch);
const uint64_t expected_new_log_num = dbfull()->TEST_LogfileNumber() + 1;
ASSERT_EQ(expected_new_log_num,
dbfull()->GetVersionSet()->current_next_file_number());
Close();
ReopenWithColumnFamilies({"default", "empty_cf"}, options);
ASSERT_GT(dbfull()->GetVersionSet()->current_next_file_number(),
expected_new_log_num);
ASSERT_EQ("v", Get(0, "k"));
}
// Dropped-CF safety: dropped column families must NOT have markers written for
// them at close time (the !IsDropped() guard protects against attaching the
// global edit to a dropped CF, which would later trip MANIFEST replay
// assertions).
TEST_F(DBBasicTest, OptimizeManifestForRecoverySkipsDroppedCF) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
CreateAndReopenWithCF({"to_drop", "keeper"}, options);
ASSERT_OK(Put(1, "k", "v1"));
ASSERT_OK(Put(2, "k", "v2"));
ASSERT_OK(Flush(1));
ASSERT_OK(Flush(2));
ASSERT_OK(db_->DropColumnFamily(handles_[1]));
Close();
ReopenWithColumnFamilies({"default", "keeper"}, options);
ASSERT_EQ("v2", Get(1, "k"));
}
// reuse_manifest_on_open=true: the next LogAndApply after Recover must
// append to the existing MANIFEST file instead of allocating a fresh
// one. Force a write after reopen and verify the MANIFEST file number
// stays unchanged.
TEST_F(DBBasicTest, ReuseManifestOnOpenAppendsToExistingFile) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.reuse_manifest_on_open = true;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
Close();
Reopen(options);
const uint64_t manifest_after_reopen =
dbfull()->TEST_Current_Manifest_FileNo();
ASSERT_OK(Put("k2", "v2"));
ASSERT_OK(Flush());
const uint64_t manifest_after_flush =
dbfull()->TEST_Current_Manifest_FileNo();
EXPECT_EQ(manifest_after_reopen, manifest_after_flush);
EXPECT_EQ("v", Get("k"));
EXPECT_EQ("v2", Get("k2"));
}
// Default off: ReopenManifestForAppend must NOT be invoked.
TEST_F(DBBasicTest, ReuseManifestOnOpenDefaultOffSkipsReopen) {
Options options = CurrentOptions();
options.create_if_missing = true;
// reuse_manifest_on_open defaults to false
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
Close();
std::atomic<int> reopened{0};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"VersionSet::ReopenManifestForAppend:Reopened",
[&](void* /*arg*/) { reopened.fetch_add(1); });
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
Reopen(options);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
ASSERT_EQ(0, reopened.load());
}
// Regression for the WritableFileWriter::filesize_=0 bug fixed via
// constructor-time initial_file_size: after ReopenManifestForAppend binds a
// writer to the existing MANIFEST, GetFileSize() must return the on-disk
// size, not 0.
// (If it returned 0, the size-limit check in ProcessManifestWrites would
// compare against 0 and Close-time Truncate could shrink the file.)
TEST_F(DBBasicTest, ReuseManifestOnOpenAdoptsOnDiskSize) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.reuse_manifest_on_open = true;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
Close();
// Capture the on-disk MANIFEST size before reopen.
Reopen(options);
const uint64_t manifest_no = dbfull()->TEST_Current_Manifest_FileNo();
const std::string manifest_path = DescriptorFileName(dbname_, manifest_no);
uint64_t on_disk_size = 0;
ASSERT_OK(env_->GetFileSize(manifest_path, &on_disk_size));
ASSERT_GT(on_disk_size, 0u);
// Force a write to flush the writer's buffer; verify Close doesn't
// shrink the file (which would happen if Truncate(filesize_=0) ran).
ASSERT_OK(Put("k2", "v2"));
ASSERT_OK(Flush());
Close();
uint64_t after_close_size = 0;
ASSERT_OK(env_->GetFileSize(manifest_path, &after_close_size));
EXPECT_GE(after_close_size, on_disk_size);
}
// MANIFEST rotation continues to work under reuse: when the file grows
// past tuned_max_manifest_file_size_, ProcessManifestWrites must rotate
// to a fresh MANIFEST (same as legacy). Use the rotation SyncPoint to
// observe rotation directly, since tuned_max_manifest_file_size_ is
// auto-derived and not directly = max_manifest_file_size.
TEST_F(DBBasicTest, ReuseManifestOnOpenStillRotatesOnSizeCap) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.reuse_manifest_on_open = true;
options.max_manifest_file_size = 1;
options.max_manifest_space_amp_pct = 0; // disable amp-based tuning
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
Close();
std::atomic<int> rotations{0};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"VersionSet::ProcessManifestWrites:BeforeNewManifest",
[&](void* /*arg*/) { rotations.fetch_add(1); });
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
Reopen(options);
ASSERT_OK(Put("k2", "v2"));
ASSERT_OK(Flush());
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
// The Flush after Reopen must have triggered a rotation given the
// tiny size cap -- proves the size-driven rotation path still runs
// when descriptor_log_ is bound by reuse.
EXPECT_GT(rotations.load(), 0);
EXPECT_EQ("v", Get("k"));
EXPECT_EQ("v2", Get("k2"));
}
// Multi-CF reuse: append-mode MANIFEST must correctly handle edits
// from multiple CFs after reopen.
TEST_F(DBBasicTest, ReuseManifestOnOpenMultiCF) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.reuse_manifest_on_open = true;
CreateAndReopenWithCF({"alpha", "beta"}, options);
ASSERT_OK(Put(0, "k", "v0"));
ASSERT_OK(Put(1, "k", "v1"));
ASSERT_OK(Put(2, "k", "v2"));
ASSERT_OK(Flush(0));
ASSERT_OK(Flush(1));
ASSERT_OK(Flush(2));
Close();
ReopenWithColumnFamilies({"default", "alpha", "beta"}, options);
// Trigger more writes post-reopen on each CF -- these get appended to
// the reused MANIFEST.
ASSERT_OK(Put(0, "k2", "v0b"));
ASSERT_OK(Put(1, "k2", "v1b"));
ASSERT_OK(Put(2, "k2", "v2b"));
ASSERT_OK(Flush(0));
ASSERT_OK(Flush(1));
ASSERT_OK(Flush(2));
EXPECT_EQ("v0", Get(0, "k"));
EXPECT_EQ("v1", Get(1, "k"));
EXPECT_EQ("v2", Get(2, "k"));
EXPECT_EQ("v0b", Get(0, "k2"));
EXPECT_EQ("v1b", Get(1, "k2"));
EXPECT_EQ("v2b", Get(2, "k2"));
}
// Disabled under best_efforts_recovery: that mode rebuilds CURRENT and
// MANIFEST as the side-effect of LogAndApplyForRecovery emitting an
// edit; reusing the prior MANIFEST contradicts the salvage contract.
TEST_F(DBBasicTest, ReuseManifestOnOpenDisabledByBestEffortsRecovery) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.reuse_manifest_on_open = true;
options.best_efforts_recovery = true;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
Close();
std::atomic<int> reopened{0};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"VersionSet::ReopenManifestForAppend:Reopened",
[&](void* /*arg*/) { reopened.fetch_add(1); });
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
Reopen(options);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
ASSERT_EQ(0, reopened.load());
ASSERT_EQ("v", Get("k"));
}
// Full-stack composition: optimize_manifest_for_recovery plus
// reuse_manifest_on_open. Verifies that Close writes recovery markers,
// Reopen skips clean-recovery MANIFEST edits, and the MANIFEST is reused
// instead of recreated for the next metadata update.
TEST_F(DBBasicTest, ReuseManifestOnOpenFullStackComposition) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
options.reuse_manifest_on_open = true;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
Close();
// Capture the MANIFEST file number before reopen.
std::atomic<int> reopened{0};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"VersionSet::ReopenManifestForAppend:Reopened",
[&](void* /*arg*/) { reopened.fetch_add(1); });
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
Reopen(options);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
// All three composing: reuse fired, data is intact, no fresh MANIFEST.
EXPECT_EQ(1, reopened.load());
EXPECT_EQ("v", Get("k"));
}
// Regression test for WAL recovery while publishing a fresh MANIFEST. The test
// stores SSTs in a separate DB path, injects failure after CURRENT points at
// the new MANIFEST, and simulates crash cleanup; the recovered SST must survive
// because the synced MANIFEST references it.
TEST_F(DBBasicTest, RecoverySstDirSyncedBeforeFreshManifestPublish) {
auto fault_fs = std::make_shared<FaultInjectionTestFS>(env_->GetFileSystem());
std::unique_ptr<Env> fault_env(NewCompositeEnv(fault_fs));
Options options = CurrentOptions();
options.create_if_missing = true;
options.disable_auto_compactions = true;
options.env = fault_env.get();
options.reuse_manifest_on_open = false;
options.db_paths.emplace_back(dbname_ + "_2", 1ULL << 30);
DestroyAndReopen(options);
ASSERT_OK(Put("base", "value"));
ASSERT_OK(Flush());
ASSERT_OK(Put("recovered", "value"));
ASSERT_OK(db_->FlushWAL(true));
Close();
fault_fs->ResetState();
std::atomic<bool> fail_after_current_publish{true};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"VersionSet::ProcessManifestWrites:AfterSetCurrentFile", [&](void* arg) {
if (fail_after_current_publish.exchange(false)) {
ASSERT_NE(nullptr, arg);
IOStatus* io_s = static_cast<IOStatus*>(arg);
ASSERT_OK(*io_s);
*io_s = IOStatus::IOError("injected current publish aftermath");
}
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
Status s = TryReopen(options);
ASSERT_TRUE(s.IsIOError()) << s.ToString();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
ASSERT_OK(fault_fs->DeleteFilesCreatedAfterLastDirSync(IOOptions(), nullptr));
s = TryReopen(options);
ASSERT_OK(s);
ASSERT_EQ("value", Get("base"));
ASSERT_EQ("value", Get("recovered"));
Close();
}
// Regression test for WAL recovery while appending to a reused MANIFEST. The
// first reopen forces recovery to create an SST and then fail after MANIFEST
// sync. The simulated crash cleanup deletes files without a prior directory
// sync; the recovered SST must survive because the synced MANIFEST references
// it.
TEST_F(DBBasicTest,
ReuseManifestOnOpenSyncsRecoverySstDirBeforeManifestAppend) {
auto fault_fs = std::make_shared<FaultInjectionTestFS>(env_->GetFileSystem());
std::unique_ptr<Env> fault_env(NewCompositeEnv(fault_fs));
Options options = CurrentOptions();
options.create_if_missing = true;
options.disable_auto_compactions = true;
options.env = fault_env.get();
options.reuse_manifest_on_open = true;
DestroyAndReopen(options);
ASSERT_OK(Put("base", "value"));
ASSERT_OK(Flush());
ASSERT_OK(Put("recovered", "value"));
ASSERT_OK(db_->FlushWAL(true));
Close();
fault_fs->ResetState();
std::atomic<bool> fail_after_manifest_sync{true};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"VersionSet::ProcessManifestWrites:AfterSyncManifest", [&](void* arg) {
if (fail_after_manifest_sync.exchange(false)) {
ASSERT_NE(nullptr, arg);
IOStatus* io_s = static_cast<IOStatus*>(arg);
ASSERT_OK(*io_s);
*io_s = IOStatus::IOError("injected manifest sync aftermath");
}
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
Status s = TryReopen(options);
ASSERT_TRUE(s.IsIOError()) << s.ToString();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
ASSERT_OK(fault_fs->DeleteFilesCreatedAfterLastDirSync(IOOptions(), nullptr));
s = TryReopen(options);
ASSERT_OK(s);
ASSERT_EQ("value", Get("base"));
ASSERT_EQ("value", Get("recovered"));
Close();
}
// Direct unit test for WritableFileWriter's initial_file_size parameter:
// verifies the visible size accessors report the existing on-disk size
// immediately, rather than the constructor's zero default.
TEST_F(DBBasicTest, WritableFileWriterInitialFileSizeAdoptsExistingSize) {
Env* env = Env::Default();
std::string fname = test::PerThreadDBPath("set_file_size_test");
ASSERT_OK(env->CreateDirIfMissing(test::TmpDir(env)));
// Create a file with some bytes, close it.
{
std::unique_ptr<WritableFile> raw;
ASSERT_OK(env->NewWritableFile(fname, &raw, EnvOptions()));
ASSERT_OK(raw->Append("hello world"));
ASSERT_OK(raw->Close());
}
// Reopen and wrap in a WritableFileWriter without initial_file_size --
// GetFileSize() should be 0 (the constructor's default).
std::unique_ptr<FSWritableFile> fs_file;
ASSERT_OK(env->GetFileSystem()->ReopenWritableFile(
fname, FileOptions(), &fs_file, /*dbg=*/nullptr));
std::unique_ptr<WritableFileWriter> writer(
new WritableFileWriter(std::move(fs_file), fname, FileOptions()));
EXPECT_EQ(0u, writer->GetFileSize());
// Reopen again and seed the writer's size accounting from the existing
// bytes. GetFileSize and GetFlushedSize must reflect it immediately.
ASSERT_OK(env->GetFileSystem()->ReopenWritableFile(
fname, FileOptions(), &fs_file, /*dbg=*/nullptr));
writer.reset(new WritableFileWriter(
std::move(fs_file), fname, FileOptions(),
/*clock=*/nullptr, /*io_tracer=*/nullptr, /*stats=*/nullptr,
Histograms::HISTOGRAM_ENUM_MAX, /*listeners=*/{},
/*file_checksum_gen_factory=*/nullptr,
/*perform_data_verification=*/false,
/*buffered_data_with_checksum=*/false,
/*initial_file_size=*/11));
EXPECT_EQ(11u, writer->GetFileSize());
EXPECT_EQ(11u, writer->GetFlushedSize());
ASSERT_OK(env->DeleteFile(fname));
}
// Tail corruption: appending garbage bytes to the MANIFEST after a
// clean close must prevent reuse -- the physical size exceeds the
// last valid record end.
TEST_F(DBBasicTest, ReuseManifestOnOpenSkipsOnTailCorruption) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.reuse_manifest_on_open = true;
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());
{
std::string contents;
ASSERT_OK(ReadFileToString(env_, manifest_path, &contents));
contents.append("garbage!");
ASSERT_OK(WriteStringToFile(env_, contents, manifest_path));
}
std::atomic<int> reopened{0};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"VersionSet::ReopenManifestForAppend:Reopened",
[&](void* /*arg*/) { reopened.fetch_add(1); });
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
Reopen(options);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
ASSERT_EQ(0, reopened.load());
ASSERT_EQ("v", Get("k"));
}
TEST_F(DBBasicTest, EnableDirectIOWithZeroBuf) {
if (!IsDirectIOSupported()) {
ROCKSDB_GTEST_BYPASS("Direct IO not supported");
+1 -1
View File
@@ -542,7 +542,7 @@ TEST_F(DBBlockCacheTest, WarmCacheWithDataBlocksDuringCompaction) {
EXPECT_TRUE(tracking_cache->HasPriority(Cache::Priority::BOTTOM));
EXPECT_FALSE(tracking_cache->HasPriority(Cache::Priority::LOW));
// Compaction output is in cache — reads should have zero misses.
// Compaction output is in cache -- reads should have zero misses.
auto data_miss_before =
options.statistics->getTickerCount(BLOCK_CACHE_DATA_MISS);
ASSERT_EQ(value + "2", Get("key"));
+5 -1
View File
@@ -2022,7 +2022,11 @@ TEST_F(DBBloomFilterTest, MutableFilterPolicy) {
double expected_bpk = 10.0;
// Other configs to try
std::vector<std::pair<std::string, double>> configs = {
{"ribbonfilter:10:-1", 7.0}, {"bloomfilter:5", 5.0}, {"nullptr", 0.0}};
{"ribbonfilter:10:-1", 7.0},
{"bloomfilter:5", 5.0},
{"nullptr", 0.0},
// As serialized in OPTIONS file
{"{id=ribbonfilter:12:-1;bloom_before_level=-1;}", 8.4}};
table_options.cache_index_and_filter_blocks = true;
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
+32
View File
@@ -427,6 +427,38 @@ TEST_F(DBCompactionAbortTest, AbortAutomaticCompaction) {
}
}
TEST_F(DBCompactionAbortTest, AbortScheduledAutomaticCompactionBeforePick) {
// The goal is to cover an automatic compaction that has been scheduled, but
// aborts before the worker picks and removes a column family from the
// compaction queue. This pins the scheduler bookkeeping invariant that
// ResumeAllCompactions() must be able to schedule that still-queued work.
constexpr int kCompactionTrigger = 4;
constexpr int kNumL0Files = kCompactionTrigger + 1;
Options options = GetOptionsWithStats();
options.level0_file_num_compaction_trigger = kCompactionTrigger;
options.max_background_compactions = 1;
options.max_subcompactions = 1;
options.disable_auto_compactions = false;
Reopen(options);
SyncPointAbortHelper helper("BackgroundCallCompaction:0");
helper.Setup(dbfull());
PopulateData(/*num_files=*/kNumL0Files, /*keys_per_file=*/100,
/*value_size=*/1000);
helper.CleanupAndWait();
const uint64_t compact_write_bytes_before_resume =
stats_->getTickerCount(COMPACT_WRITE_BYTES);
dbfull()->ResumeAllCompactions();
ASSERT_OK(dbfull()->TEST_WaitForCompact());
ASSERT_GT(stats_->getTickerCount(COMPACT_WRITE_BYTES),
compact_write_bytes_before_resume);
ASSERT_LT(NumTableFilesAtLevel(0), kNumL0Files);
VerifyDataIntegrity(/*num_keys=*/100);
}
TEST_F(DBCompactionAbortTest, AbortAndVerifyNoOutputFiles) {
Options options = CurrentOptions();
options.level0_file_num_compaction_trigger = 4;
+88 -6
View File
@@ -11833,7 +11833,7 @@ TEST_F(DBCompactionTest, RoundRobinCleanCutWithSharedBoundary) {
// 2. A post-verification step fails (injected here via sync point), setting
// compact_->status to error while each subcompaction's status stays OK.
// 3. SubcompactionState::Cleanup checks individual status (OK) and skips
// ReleaseObsolete — the cache entries leak.
// ReleaseObsolete -- the cache entries leak.
// 4. FaultInjectionTestFS injects metadata read errors, causing GetChildren
// to fail in FindObsoleteFiles.
// 5. Close()'s FindObsoleteFiles also fails to find the orphan for the same
@@ -11864,7 +11864,7 @@ TEST_F(DBCompactionTest, LeakedTableCacheEntryOnCompactionFailure) {
// fail while individual subcompaction statuses stay OK (so Cleanup skips
// ReleaseObsolete). The filesystem deactivation makes GetChildren fail
// in FindObsoleteFiles, preventing the backstop from evicting the leaked
// cache entries — matching the crash test's metadata read fault injection.
// cache entries -- matching the crash test's metadata read fault injection.
std::atomic<bool> inject_error{true};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"CompactionJob::Run():AfterVerifyOutputFiles", [&](void* arg) {
@@ -11876,7 +11876,7 @@ TEST_F(DBCompactionTest, LeakedTableCacheEntryOnCompactionFailure) {
// Enable metadata read fault injection on the bg compaction thread after
// the compaction job finishes but before FindObsoleteFiles runs. This
// makes GetChildren fail (metadata read), matching crash test's
// --open_metadata_read_fault_one_in=8. Only metadata reads fail —
// --open_metadata_read_fault_one_in=8. Only metadata reads fail --
// logging and other IO operations continue normally.
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"BackgroundCallCompaction:1", [&](void*) {
@@ -11889,7 +11889,7 @@ TEST_F(DBCompactionTest, LeakedTableCacheEntryOnCompactionFailure) {
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
// Trigger compaction — fails after VerifyOutputFiles.
// Trigger compaction -- fails after VerifyOutputFiles.
Status s = dbfull()->TEST_CompactRange(0, nullptr, nullptr);
ASSERT_NOK(s);
@@ -11971,7 +11971,7 @@ TEST_F(DBCompactionTest, LeakedTableCacheEntryOnInstallFailure) {
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
// Trigger compaction — Run() succeeds but Install() fails.
// Trigger compaction -- Run() succeeds but Install() fails.
Status s = dbfull()->TEST_CompactRange(0, nullptr, nullptr);
ASSERT_NOK(s);
@@ -12032,7 +12032,7 @@ TEST_F(DBCompactionTest, ObsoleteFileTableCacheEntryWithConcurrentRef) {
TableCache::Lookup(table_cache, target_file_number);
ASSERT_NE(concurrent_handle, nullptr);
// Call ReleaseObsolete directly — this is what PurgeObsoleteFiles calls
// Call ReleaseObsolete directly -- this is what PurgeObsoleteFiles calls
// when a file becomes obsolete. Pass nullptr for the handle to make
// ReleaseObsolete do its own Lookup internally.
TableCache::ReleaseObsolete(table_cache, target_file_number,
@@ -12049,6 +12049,88 @@ TEST_F(DBCompactionTest, ObsoleteFileTableCacheEntryWithConcurrentRef) {
ASSERT_EQ(leaked, nullptr);
}
// Regression test for the atomic flush path leaking table cache entries when
// InstallMemtableAtomicFlushResults (MANIFEST write) fails.
// 1. atomic_flush is enabled with two column families.
// 2. Both CFs have data, a flush is triggered.
// 3. FlushJob::Run() succeeds for both (files are added to table cache by
// BuildTable), but write_manifest=false so no install happens in Run().
// 4. The combined MANIFEST write (InstallMemtableAtomicFlushResults) fails
// due to injected I/O error.
// 5. Without the fix, the files remain in the table cache but are not in
// any Version, causing TEST_VerifyNoObsoleteFilesCached to fire during
// Close() (especially when metadata_read_fault_one_in disables the
// FindObsoleteFiles backstop).
TEST_F(DBCompactionTest, LeakedTableCacheEntryOnAtomicFlushInstallFailure) {
auto fault_fs = std::make_shared<FaultInjectionTestFS>(env_->GetFileSystem());
std::unique_ptr<Env> fault_env(NewCompositeEnv(fault_fs));
Options options = CurrentOptions();
options.env = fault_env.get();
options.atomic_flush = true;
options.create_if_missing = true;
options.disable_auto_compactions = true;
// Enough buffer so auto-flush doesn't trigger prematurely.
options.write_buffer_size = 64 << 20;
DestroyAndReopen(options);
// Create a second column family.
CreateAndReopenWithCF({"pikachu"}, options);
ASSERT_EQ(2, handles_.size());
// Write data to both CFs.
ASSERT_OK(Put(0, "key0", std::string(1024, 'a')));
ASSERT_OK(Put(1, "key1", std::string(1024, 'b')));
// Inject a write error during the combined MANIFEST write that happens
// inside InstallMemtableAtomicFlushResults (via LogAndApply).
std::atomic<bool> inject_error{true};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"VersionSet::LogAndApply:WriteManifest", [&](void*) {
if (inject_error.exchange(false)) {
fault_fs->SetThreadLocalErrorContext(
FaultInjectionIOType::kWrite, /*seed=*/0, /*one_in=*/1,
/*retryable=*/false, /*has_data_loss=*/false);
fault_fs->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kWrite);
}
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
// Trigger atomic flush - Run() succeeds but MANIFEST write fails.
FlushOptions flush_opts;
flush_opts.wait = true;
Status s = db_->Flush(flush_opts, handles_);
ASSERT_NOK(s);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
// Disable the write fault so Close() can proceed.
fault_fs->DisableThreadLocalErrorInjection(FaultInjectionIOType::kWrite);
// Enable metadata read fault injection on the main thread so that
// Close()'s FindObsoleteFiles full-scan backstop cannot find the orphan
// files (simulating metadata_read_fault_one_in from the stress test).
fault_fs->SetThreadLocalErrorContext(
FaultInjectionIOType::kMetadataRead, /*seed=*/0, /*one_in=*/1,
/*retryable=*/false, /*has_data_loss=*/false);
fault_fs->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
// Destroy column family handles before closing.
for (auto h : handles_) {
ASSERT_OK(db_->DestroyColumnFamilyHandle(h));
}
handles_.clear();
// Close the DB. Without the fix, TEST_VerifyNoObsoleteFilesCached fires.
s = db_->Close();
ASSERT_OK(s);
db_ = nullptr;
}
TEST_F(DBCompactionTest, VerifyFileChecksumOnCompactionOutput) {
Options options = CurrentOptions();
options.disable_auto_compactions = true;
+63 -4
View File
@@ -35,7 +35,7 @@ namespace ROCKSDB_NAMESPACE {
class DBTest2 : public DBTestBase {
public:
DBTest2() : DBTestBase("db_test2", /*env_do_fsync=*/true) {}
DBTest2() : DBTestBase("db_etc2_test", /*env_do_fsync=*/true) {}
};
TEST_F(DBTest2, OpenForReadOnly) {
@@ -107,7 +107,7 @@ TEST_F(DBTest2, ReadOnlyDBWalInDbPathInitialized) {
ASSERT_OK(Put("key2", "value2"));
Close();
// Reopen as read-only — wal_in_db_path_ must be properly initialized.
// Reopen as read-only -- wal_in_db_path_ must be properly initialized.
// Before the fix, closing this DB would read an uninitialized bool in
// DeleteObsoleteFileImpl, which UBSan catches as undefined behavior.
std::unique_ptr<DB> db_ptr;
@@ -115,7 +115,7 @@ TEST_F(DBTest2, ReadOnlyDBWalInDbPathInitialized) {
std::string value;
ASSERT_OK(db_ptr->Get(ReadOptions(), "key1", &value));
ASSERT_EQ("value1", value);
// Close the read-only DB — this triggers PurgeObsoleteFiles which reads
// Close the read-only DB -- this triggers PurgeObsoleteFiles which reads
// wal_in_db_path_. Under UBSan, an uninitialized bool here would fail.
db_ptr.reset();
@@ -7510,6 +7510,65 @@ TEST_F(DBTest2, GetLatestSeqAndTsForKey) {
ASSERT_EQ(0, options.statistics->getTickerCount(GET_HIT_L0));
}
TEST_F(DBTest2,
GetLatestSequenceForKeyFromHistoryWithBlobBackedWideColumnEntity) {
// Goal: exercise the memtable history lookup in GetLatestSequenceForKey()
// after blob direct write stores a blob-backed V2 entity in a flushed
// memtable. Using cache_only=true ensures the lookup succeeds only if the
// history path can resolve the wide-column base value under the newer merge.
Destroy(last_options_);
Options options = CurrentOptions();
options.create_if_missing = true;
options.enable_blob_files = true;
options.enable_blob_direct_write = true;
options.min_blob_size = 50;
options.allow_concurrent_memtable_write = false;
options.blob_direct_write_partitions = 1;
options.max_write_buffer_size_to_maintain = 64 << 10;
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
Reopen(options);
const std::string key = "history_blob_entity";
const std::string default_value(100, 'd');
const std::string merge_operand = "suffix";
WideColumns columns{{kDefaultWideColumnName, default_value},
{"meta", "inline"}};
ASSERT_OK(
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), key,
merge_operand));
const SequenceNumber expected_seq = dbfull()->GetLatestSequenceNumber();
ASSERT_OK(Flush());
ASSERT_FALSE(GetBlobFileNumbers().empty());
uint64_t num_immutable_memtables = 0;
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumImmutableMemTable,
&num_immutable_memtables));
ASSERT_EQ(num_immutable_memtables, 0);
auto* cfhi = static_cast_with_check<ColumnFamilyHandleImpl>(
dbfull()->DefaultColumnFamily());
assert(cfhi);
assert(cfhi->cfd());
SuperVersion* sv = cfhi->cfd()->GetSuperVersion();
SequenceNumber seq = kMaxSequenceNumber;
bool found_record_for_key = false;
bool is_blob_index = false;
const Status s = dbfull()->GetLatestSequenceForKey(
sv, key, /*cache_only=*/true, /*lower_bound_seq=*/0, &seq,
/*timestamp=*/nullptr, &found_record_for_key, &is_blob_index);
ASSERT_OK(s);
ASSERT_TRUE(found_record_for_key);
ASSERT_EQ(expected_seq, seq);
ASSERT_FALSE(is_blob_index);
}
#if defined(ZSTD)
TEST_F(DBTest2, ZSTDChecksum) {
// Verify that corruption during decompression is caught.
@@ -8173,7 +8232,7 @@ TEST_F(DBTest2, FastSstOpenDisableAfterMetadataPersisted) {
ASSERT_EQ(1, test_fs->GetMetadataRetrievedCount());
db.reset();
// Reopen with fast_sst_open DISABLED — metadata is in the MANIFEST
// Reopen with fast_sst_open DISABLED -- metadata is in the MANIFEST
// but should NOT be passed to the filesystem
options.fast_sst_open = false;
test_fs->ResetCounters();
+253 -34
View File
@@ -1,9 +1,12 @@
// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
// 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 "db/db_test_util.h"
#include "rocksdb/convenience.h"
#include "rocksdb/metadata.h"
#include "rocksdb/sst_file_writer.h"
namespace ROCKSDB_NAMESPACE {
@@ -43,50 +46,98 @@ TEST_F(DBEtc3Test, AutoTuneManifestSize) {
ASSERT_EQ(DBOptions{}.max_manifest_space_amp_pct, 500);
Options options = CurrentOptions();
ASSERT_OK(db_->SetOptions({{"level0_file_num_compaction_trigger", "20"}}));
// Test setup: create many flushed files. Keep level compaction semantics so
// DeleteFilesInRanges can remove non-L0 files, but prevent automatic
// compactions and write stalls from adding unrelated behavior.
options.disable_auto_compactions = true;
options.level0_slowdown_writes_trigger = 100;
options.level0_stop_writes_trigger = 200;
// Use large column family names to essentially control the amount of payload
// data needed for the manifest file. Drop manifest entries don't include the
// CF name so are small.
// Test strategy: use large column family names to control the rough amount
// of payload added to the MANIFEST. Drop manifest entries do not include the
// CF name, so they are small.
//
// Most CF helper calls piggy-back a background manifest write so the main
// auto-tuning phases continue to test the unrelaxed background threshold
// even though CF manipulation itself is foreground. Phase-specific foreground
// checks disable that piggy-backed background write.
uint64_t prev_manifest_num = 0, cur_manifest_num = 0;
std::deque<ColumnFamilyHandle*> handles;
int counter = 5;
auto AddCfFn = [&]() {
auto UpdateManifestNumsFrom = [&](uint64_t before_manifest_num) {
prev_manifest_num = before_manifest_num;
cur_manifest_num = dbfull()->TEST_Current_Manifest_FileNo();
};
auto BackgroundManifestWriteFn = [&]() {
uint64_t before_manifest_num = cur_manifest_num;
ASSERT_OK(Put("x", std::to_string(counter++)));
ASSERT_OK(Flush());
UpdateManifestNumsFrom(before_manifest_num);
};
auto AddCfFn = [&](bool include_background_manifest_write = true) {
uint64_t before_manifest_num = cur_manifest_num;
std::string name = "cf" + std::to_string(counter++);
name.resize(1000, 'a');
ASSERT_OK(db_->CreateColumnFamily(options, name, &handles.emplace_back()));
prev_manifest_num = cur_manifest_num;
cur_manifest_num = dbfull()->TEST_Current_Manifest_FileNo();
if (include_background_manifest_write) {
BackgroundManifestWriteFn();
}
UpdateManifestNumsFrom(before_manifest_num);
};
auto DropCfFn = [&]() {
auto DropCfFn = [&](bool include_background_manifest_write = true) {
uint64_t before_manifest_num = cur_manifest_num;
ASSERT_OK(db_->DropColumnFamily(handles.front()));
ASSERT_OK(db_->DestroyColumnFamilyHandle(handles.front()));
handles.pop_front();
prev_manifest_num = cur_manifest_num;
cur_manifest_num = dbfull()->TEST_Current_Manifest_FileNo();
};
auto TrivialManifestWriteFn = [&]() {
ASSERT_OK(Put("x", std::to_string(counter++)));
ASSERT_OK(Flush());
prev_manifest_num = cur_manifest_num;
cur_manifest_num = dbfull()->TEST_Current_Manifest_FileNo();
if (include_background_manifest_write) {
BackgroundManifestWriteFn();
}
UpdateManifestNumsFrom(before_manifest_num);
};
// ---- Phase 1: foreground threshold relaxation is bounded ----
//
// Foreground operations should only get about 25% extra headroom, not an
// unbounded threshold. With 1000-byte CF names and a 3000-byte normal limit,
// the relaxed limit should allow the first four foreground-only CF additions
// but require rotation on the fifth.
options.max_manifest_file_size = 3000;
options.max_manifest_space_amp_pct = 0;
DestroyAndReopen(options);
cur_manifest_num = dbfull()->TEST_Current_Manifest_FileNo();
prev_manifest_num = cur_manifest_num;
for (int i = 1; i <= 4; ++i) {
AddCfFn(/*include_background_manifest_write=*/false);
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
}
AddCfFn(/*include_background_manifest_write=*/false);
ASSERT_LT(prev_manifest_num, cur_manifest_num);
while (!handles.empty()) {
ASSERT_OK(db_->DestroyColumnFamilyHandle(handles.front()));
handles.pop_front();
}
// ---- Phase 2: no auto-tuning means frequent rotation ----
//
options.max_manifest_file_size = 1000000;
options.max_manifest_space_amp_pct = 0; // no auto-tuning yet
DestroyAndReopen(options);
cur_manifest_num = dbfull()->TEST_Current_Manifest_FileNo();
prev_manifest_num = cur_manifest_num;
// With the generous (minimum) maximum manifest size, should not be rotated
// With the generous minimum manifest size, should not be rotated.
AddCfFn();
AddCfFn();
AddCfFn();
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
// Change options for small max and (still) no auto-tuning
// Lower the minimum while still disabling auto-tuning.
ASSERT_OK(db_->SetDBOptions({{"max_manifest_file_size", "3000"}}));
// Takes effect on the next manifest write
TrivialManifestWriteFn();
BackgroundManifestWriteFn();
ASSERT_LT(prev_manifest_num, cur_manifest_num);
// Now we have to rewrite the whole manifest on each write because the
@@ -97,28 +148,31 @@ TEST_F(DBEtc3Test, AutoTuneManifestSize) {
ASSERT_LT(prev_manifest_num, cur_manifest_num);
AddCfFn();
ASSERT_LT(prev_manifest_num, cur_manifest_num);
TrivialManifestWriteFn();
BackgroundManifestWriteFn();
ASSERT_LT(prev_manifest_num, cur_manifest_num);
// ---- Phase 3: auto-tuning raises the background threshold ----
//
// Enabling auto-tuning should fix this, immediately for next manifest writes.
// This will allow up to double-ish the size of the compacted manifest,
// which last should have been 4000 + some bytes.
// This will allow up to roughly double the size of the compacted manifest,
// which now includes CF entries plus the piggy-backed background writes.
ASSERT_EQ(handles.size(), 4U);
ASSERT_OK(db_->SetDBOptions({{"max_manifest_space_amp_pct", "105"}}));
ASSERT_OK(db_->SetDBOptions({{"max_manifest_space_amp_pct", "95"}}));
// After 9 CF names should be enough to rotate the manifest
for (int i = 1; i <= 5; ++i) {
// Auto-tuning lets several more CF names accumulate in the MANIFEST before
// the piggy-backed background write crosses the threshold.
for (int i = 1; i <= 3; ++i) {
if ((i % 2) == 1) {
DropCfFn();
}
AddCfFn();
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
}
TrivialManifestWriteFn();
AddCfFn();
ASSERT_LT(prev_manifest_num, cur_manifest_num);
// We now have a different last compacted manifest size, should be
// able to go beyond 9 CFs named in manifest this time.
// We now have a different last compacted manifest size, so the next
// threshold should be based on the newly compacted MANIFEST.
ASSERT_EQ(handles.size(), 6U);
DropCfFn();
@@ -128,11 +182,10 @@ TEST_F(DBEtc3Test, AutoTuneManifestSize) {
AddCfFn();
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
}
// We've written 10 named CFs to the manifest. We should be able to
// dynamically change the auto-tuning still based on the last "compacted"
// manifest size of 7000 + some bytes.
// We should be able to dynamically change the auto-tuning still based on
// the last "compacted" manifest size.
ASSERT_OK(db_->SetDBOptions({{"max_manifest_space_amp_pct", "51"}}));
TrivialManifestWriteFn();
BackgroundManifestWriteFn();
ASSERT_LT(prev_manifest_num, cur_manifest_num);
// And the "compacted" manifest size has reset again, so should be changed
// again sooner.
@@ -141,13 +194,179 @@ TEST_F(DBEtc3Test, AutoTuneManifestSize) {
AddCfFn();
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
}
// Enough for manifest change
AddCfFn();
// ---- Phase 4: foreground operations use relaxed threshold ----
//
// The current MANIFEST is now large enough for the next background manifest
// write to rotate it, but still small enough for foreground operations to use
// their 25% extra headroom. Assert that each foreground operation stays on
// the same MANIFEST, then verify the next background write rotates.
const std::string external_files_dir = dbname_ + "/external_files";
ASSERT_OK(env_->CreateDirIfMissing(external_files_dir));
auto WriteExternalSstFile = [&](const std::string& file_name,
const std::string& key,
const std::string& value) {
const std::string file_path = external_files_dir + "/" + file_name;
Status ignored = env_->DeleteFile(file_path);
ignored.PermitUncheckedError();
SstFileWriter sst_file_writer(EnvOptions(), options);
ASSERT_OK(sst_file_writer.Open(file_path));
ASSERT_OK(sst_file_writer.Put(key, value));
ASSERT_OK(sst_file_writer.Finish());
};
auto IngestExternalFileForegroundManifestWriteFn =
[&](std::string* ingested_key) {
uint64_t before_manifest_num = cur_manifest_num;
const std::string key = "z" + std::to_string(counter++);
const std::string value = "v" + std::to_string(counter++);
const std::string file_name =
"ingest_" + std::to_string(counter++) + ".sst";
const std::string file_path = external_files_dir + "/" + file_name;
WriteExternalSstFile(file_name, key, value);
ASSERT_OK(
db_->IngestExternalFile({file_path}, IngestExternalFileOptions()));
ASSERT_EQ(value, Get(key));
*ingested_key = key;
Status ignored = env_->DeleteFile(file_path);
ignored.PermitUncheckedError();
UpdateManifestNumsFrom(before_manifest_num);
};
auto CreateColumnFamilyWithImportForegroundManifestWriteFn = [&]() {
uint64_t before_manifest_num = cur_manifest_num;
const std::string cf_name = "import_cf" + std::to_string(counter++);
const std::string key = "import" + std::to_string(counter++);
const std::string value = "v" + std::to_string(counter++);
const std::string file_name =
"import_" + std::to_string(counter++) + ".sst";
const std::string file_path = external_files_dir + "/" + file_name;
WriteExternalSstFile(file_name, key, value);
LiveFileMetaData file_metadata;
file_metadata.name = file_name;
file_metadata.db_path = external_files_dir;
file_metadata.smallest_seqno = 0;
file_metadata.largest_seqno = 0;
file_metadata.level = 0;
ExportImportFilesMetaData metadata;
metadata.files.push_back(file_metadata);
metadata.db_comparator_name = options.comparator->Name();
ColumnFamilyHandle* import_handle = nullptr;
ASSERT_OK(db_->CreateColumnFamilyWithImport(options, cf_name,
ImportColumnFamilyOptions(),
metadata, &import_handle));
ASSERT_NE(import_handle, nullptr);
handles.push_back(import_handle);
std::string result;
ASSERT_OK(db_->Get(ReadOptions(), import_handle, key, &result));
ASSERT_EQ(value, result);
Status ignored = env_->DeleteFile(file_path);
ignored.PermitUncheckedError();
UpdateManifestNumsFrom(before_manifest_num);
};
auto DeleteFilesInRangesForegroundManifestWriteFn =
[&](const std::string& key) {
uint64_t before_manifest_num = cur_manifest_num;
const std::string limit = key + "\xff";
std::vector<RangeOpt> ranges;
ranges.emplace_back(key, limit);
ASSERT_OK(DeleteFilesInRanges(db_.get(), db_->DefaultColumnFamily(),
ranges.data(), ranges.size(),
/*include_end=*/false));
std::string result;
ASSERT_TRUE(db_->Get(ReadOptions(), key, &result).IsNotFound());
UpdateManifestNumsFrom(before_manifest_num);
};
// Column family manipulation.
AddCfFn(/*include_background_manifest_write=*/false);
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
// SetOptions should not write to the MANIFEST. If that regresses, the write
// should be treated as a background write and rotate here.
{
ASSERT_OK(db_->SetOptions({{"level0_slowdown_writes_trigger", "101"}}));
UpdateManifestNumsFrom(cur_manifest_num);
}
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
// External file ingestion.
std::string ingested_key;
IngestExternalFileForegroundManifestWriteFn(&ingested_key);
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
// Imported column family creation.
CreateColumnFamilyWithImportForegroundManifestWriteFn();
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
// Physical file deletion by range.
DeleteFilesInRangesForegroundManifestWriteFn(ingested_key);
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
// Background flush.
BackgroundManifestWriteFn();
ASSERT_LT(prev_manifest_num, cur_manifest_num);
// ---- Phase 5: persisted compacted manifest size survives close/reopen ----
// Close with CFs still live. Reopen with reuse_manifest_on_open so the
// manifest is NOT rewritten from scratch. The persisted compacted size
// should be loaded and used for auto-tuning.
// At this point we have 8 CF handles plus default.
ASSERT_EQ(handles.size(), 8U);
// Collect CF names for reopen, then release handles (Close needs this)
std::vector<std::string> cf_names = {"default"};
for (auto* h : handles) {
cf_names.push_back(h->GetName());
}
for (auto* h : handles) {
ASSERT_OK(db_->DestroyColumnFamilyHandle(h));
}
handles.clear();
Close();
// Use a max_manifest_file_size below the reused manifest size. Auto-tuning
// with the persisted compacted size at 200% amp keeps the tuned threshold
// high enough. Without persistence, the threshold would be
// max(max_manifest_file_size, 0 * anything) = max_manifest_file_size.
//
// With max_manifest_file_size set to 3000, missing persisted compacted size
// would keep tuned = 3000 and the first AddCf would rotate because the
// reused manifest is already larger. With persisted compacted size loaded,
// the tuned threshold is based on the compacted size, so no rotation until
// the manifest grows well beyond the minimum.
options.max_manifest_file_size = 3000;
options.max_manifest_space_amp_pct = 200;
options.reuse_manifest_on_open = true;
uint64_t manifest_num_before_reopen = cur_manifest_num;
ReopenWithColumnFamilies(cf_names, options);
cur_manifest_num = dbfull()->TEST_Current_Manifest_FileNo();
// With persistence: the manifest file number should NOT have changed
// during reopen, because the persisted compacted size keeps the tuned
// threshold high enough. Without persistence, last_compacted = 0, so
// tuned = max_manifest_file_size = 3000, and the first LogAndApply
// during Open rotates the manifest because it is already larger than 3000.
ASSERT_EQ(manifest_num_before_reopen, cur_manifest_num);
// Adding CFs should still not trigger rotation because the tuned threshold
// from the persisted compacted size exceeds the current manifest size.
for (int i = 1; i <= 4; ++i) {
AddCfFn();
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
}
for (int i = 1; i <= 4; ++i) {
AddCfFn();
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
}
// Wrap up
while (!handles.empty()) {
DropCfFn();
DropCfFn(/*include_background_manifest_write=*/false);
}
}
+8 -1
View File
@@ -1852,6 +1852,8 @@ TEST_F(DBFlushTest, MemPurgeCorrectLogNumberAndSSTFileCreation) {
}
}
ASSERT_OK(WaitForFlushCallbacks());
// Check that there was at least one mempurge
uint32_t expected_min_mempurge_count = 1;
// Check that there was no SST files created during flush.
@@ -1872,6 +1874,8 @@ TEST_F(DBFlushTest, MemPurgeCorrectLogNumberAndSSTFileCreation) {
ASSERT_EQ(Get(key), value);
}
ASSERT_OK(WaitForFlushCallbacks());
// Check that there was at least one SST files created during flush.
expected_sst_count = 1;
EXPECT_GE(sst_count.load(), expected_sst_count);
@@ -1891,6 +1895,9 @@ TEST_F(DBFlushTest, MemPurgeCorrectLogNumberAndSSTFileCreation) {
// Extra check of database consistency.
ASSERT_EQ(Get(key), value);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
Close();
}
@@ -3883,7 +3890,7 @@ TEST_F(DBFlushTest, LeakedTableCacheEntryOnFlushInstallFailure) {
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
// Trigger flush — BuildTable succeeds but LogAndApply fails.
// Trigger flush -- BuildTable succeeds but LogAndApply fails.
Status s = Flush();
ASSERT_NOK(s);
+384 -25
View File
@@ -15,6 +15,7 @@
#include <cinttypes>
#include <cstdio>
#include <deque>
#include <map>
#include <memory>
#include <optional>
@@ -29,6 +30,7 @@
#include "db/arena_wrapped_db_iter.h"
#include "db/attribute_group_iterator_impl.h"
#include "db/blob/blob_fetcher.h"
#include "db/blob/blob_file_partition_manager.h"
#include "db/blob/blob_index.h"
#include "db/builder.h"
@@ -173,6 +175,7 @@ DBImpl::DBImpl(const DBOptions& options, const std::string& dbname,
const bool seq_per_batch, const bool batch_per_txn,
bool read_only)
: dbname_(dbname),
read_only_(read_only),
own_info_log_(options.info_log == nullptr),
initial_db_options_(SanitizeOptions(dbname, options, read_only,
&init_logger_creation_s_)),
@@ -481,6 +484,33 @@ void DBImpl::WaitForBackgroundWork() {
}
}
void DBImpl::WaitForAsyncFileOpen() {
if (!immutable_db_options_.open_files_async) {
return;
}
InstrumentedMutexLock l(&mutex_);
TEST_SYNC_POINT("DBImpl::WaitForAsyncFileOpen::BeforeWait");
while (bg_async_file_open_state_ == AsyncFileOpenState::kScheduled &&
!shutting_down_.load(std::memory_order_acquire)) {
bg_cv_.Wait();
}
}
void DBImpl::NotifyOnDBShutdownBegin() {
mutex_.AssertHeld();
if (shutdown_notification_sent_ || immutable_db_options_.listeners.empty()) {
return;
}
shutdown_notification_sent_ = true;
// release lock while notifying events
mutex_.Unlock();
for (const auto& listener : immutable_db_options_.listeners) {
listener->OnDBShutdownBegin(this);
}
mutex_.Lock();
}
// Will lock the mutex_, will wait for completion if wait is true
void DBImpl::CancelAllBackgroundWork(bool wait) {
ROCKS_LOG_INFO(immutable_db_options_.info_log,
@@ -527,6 +557,9 @@ void DBImpl::CancelAllBackgroundWork(bool wait) {
immutable_db_options_.compaction_service->CancelAwaitingJobs();
}
if (!already_shutting_down) {
NotifyOnDBShutdownBegin();
}
shutting_down_.store(true, std::memory_order_release);
bg_cv_.SignalAll();
if (!wait) {
@@ -636,6 +669,106 @@ void DBImpl::UnregisterBlobDirectWriteColumnFamily() {
assert(false);
}
Status DBImpl::MaybeWriteWalMarkersToManifestOnClose() {
mutex_.AssertHeld();
if (!mutable_db_options_.optimize_manifest_for_recovery ||
!opened_successfully_ || read_only_ || versions_ == nullptr ||
logs_.empty()) {
return Status::OK();
}
TEST_SYNC_POINT("DBImpl::CloseHelper:WriteWalMarkersOnCloseEntered");
const ReadOptions read_options(Env::IOActivity::kUnknown);
const WriteOptions write_options(Env::IOActivity::kUnknown);
uint64_t max_wal_number = logs_.back().number;
TEST_SYNC_POINT_CALLBACK("DBImpl::CloseHelper:CapturedMaxWal",
&max_wal_number);
const uint64_t new_log_num = max_wal_number + 1;
// std::deque keeps VersionEdit pointers stable across emplace_back.
std::deque<VersionEdit> edits;
autovector<ColumnFamilyData*> cfds;
autovector<autovector<VersionEdit*>> edit_lists;
VersionEdit global_edit;
bool persist_global_edit = false;
bool reserved_log_number = false;
bool all_cfs_empty = true;
for (auto* cfd : *versions_->GetColumnFamilySet()) {
if (cfd->IsDropped()) {
continue;
}
// cfd->IsEmpty() checks both mem and imm. An immutable memtable queued for
// flush still pins an older WAL; advancing past it would drop unflushed
// records on the next open.
if (!cfd->IsEmpty()) {
all_cfs_empty = false;
continue;
}
if (new_log_num > cfd->GetLogNumber()) {
if (!reserved_log_number) {
const uint64_t next_file_number = versions_->current_next_file_number();
if (next_file_number <= new_log_num) {
versions_->FetchAddFileNumber(new_log_num + 1 - next_file_number);
global_edit.SetNextFile(versions_->current_next_file_number());
persist_global_edit = true;
}
reserved_log_number = true;
}
edits.emplace_back();
VersionEdit& edit = edits.back();
edit.SetLogNumber(new_log_num);
edit.SetColumnFamily(cfd->GetID());
cfds.push_back(cfd);
edit_lists.emplace_back();
edit_lists.back().push_back(&edit);
}
}
if (all_cfs_empty && !allow_2pc()) {
if (new_log_num > versions_->min_log_number_to_keep()) {
global_edit.SetMinLogNumberToKeep(new_log_num);
persist_global_edit = true;
}
// Mirror the monotonicity guard in db_impl_files.cc:
// PrecomputeWalDeletionEdit emits DeleteWalsBefore only when the
// marker actually advances the WalSet's existing min. Skipping
// this guard would let a stale-replay edit regress WalSet state.
if (immutable_db_options_.track_and_verify_wals_in_manifest &&
new_log_num > versions_->GetWalSet().GetMinWalNumberToKeep()) {
global_edit.DeleteWalsBefore(new_log_num);
persist_global_edit = true;
}
}
if (persist_global_edit) {
ColumnFamilyData* default_cfd =
versions_->GetColumnFamilySet()->GetDefault();
if (default_cfd && !default_cfd->IsDropped()) {
bool attached_to_existing = false;
for (size_t i = 0; i < cfds.size(); ++i) {
if (cfds[i] == default_cfd) {
edit_lists[i].push_back(&global_edit);
attached_to_existing = true;
break;
}
}
if (!attached_to_existing) {
cfds.push_back(default_cfd);
edit_lists.emplace_back();
edit_lists.back().push_back(&global_edit);
}
}
}
if (cfds.empty()) {
return Status::OK();
}
return versions_->LogAndApply(cfds, read_options, write_options, edit_lists,
&mutex_, directories_.GetDbDir(),
/*new_descriptor_log=*/false);
}
Status DBImpl::CloseHelper() {
// Guarantee that there is no background error recovery in progress before
// continuing with the shutdown
@@ -676,12 +809,27 @@ Status DBImpl::CloseHelper() {
bg_flush_scheduled_ || bg_purge_scheduled_ ||
bg_pressure_callback_in_progress_ ||
bg_async_file_open_state_ == AsyncFileOpenState::kScheduled ||
async_wal_precreate_state_ == AsyncWALPrecreateState::kScheduled ||
pending_purge_obsolete_files_ ||
error_handler_.IsRecoveryInProgress()) {
TEST_SYNC_POINT("DBImpl::~DBImpl:WaitJob");
bg_cv_.Wait();
}
// Release any opened-but-unpublished WAL writer after the in-flight worker
// has published its result. Clear the DB-owned async slot while holding
// mutex_, but destroy the detached writer after dropping mutex_ because
// log::Writer / WritableFileWriter destruction can flush and close the file.
// The file itself can be left behind as an empty future WAL; recovery already
// tolerates it and marks its file number used if observed.
UnpublishedWAL unused_async_wal = std::move(async_wal_precreate_wal_);
async_wal_precreate_state_ = AsyncWALPrecreateState::kNotScheduled;
if (unused_async_wal.writer) {
mutex_.Unlock();
unused_async_wal.Reset();
mutex_.Lock();
}
// Ensure subclasses don't forget to schedule async file opening
assert(!immutable_db_options_.open_files_async || !opened_successfully_ ||
bg_async_file_open_state_ != AsyncFileOpenState::kNotScheduled);
@@ -727,7 +875,7 @@ Status DBImpl::CloseHelper() {
// manifest file), it is not able to identify live files correctly. As a
// result, all "live" files can get deleted by accident. However, corrupted
// manifest is recoverable by RepairDB().
if (opened_successfully_) {
if (opened_successfully_ && !read_only_) {
JobContext job_context(next_job_id_.fetch_add(1));
FindObsoleteFiles(&job_context, true);
@@ -740,6 +888,17 @@ Status DBImpl::CloseHelper() {
job_context.Clean();
mutex_.Lock();
}
// Best-effort warm-reopen optimization: after obsolete-file cleanup and
// before logs_.clear(), persist close-time WAL markers that can reduce
// recovery work on the next open. This matters most when some CFs have no
// unflushed writes and/or the newest WAL is empty, since recovery would
// otherwise append marker-only MANIFEST edits on reopen.
Status manifest_s = MaybeWriteWalMarkersToManifestOnClose();
if (!manifest_s.ok()) {
ROCKS_LOG_WARN(immutable_db_options_.info_log,
"optimize_manifest_for_recovery close-time write failed: %s",
manifest_s.ToString().c_str());
}
{
InstrumentedMutexLock lock(&wal_write_mutex_);
for (auto l : wals_to_free_) {
@@ -2748,6 +2907,148 @@ Status DBImpl::ResolveDirectWriteWideColumns(const ReadOptions& read_options,
return status;
}
bool DBImpl::MaybeResolveMemtableBlobValue(const Slice& key,
const BlobFetcher* blob_fetcher,
PinnableSlice* value,
PinnableWideColumns* columns,
Status* s, bool* is_blob_index,
bool* value_found) {
if (!s->ok() || (!value && !columns)) {
return false;
}
auto reset_outputs = [&]() {
if (value != nullptr) {
value->Reset();
}
if (columns != nullptr) {
columns->Reset();
}
};
auto clear_blob_state = [&]() {
if (is_blob_index != nullptr) {
*is_blob_index = false;
}
};
const bool needs_plain_value_resolution =
is_blob_index != nullptr && *is_blob_index;
const bool needs_wide_column_resolution =
columns != nullptr && !columns->unresolved_blob_column_indices_.empty();
if (!needs_plain_value_resolution && !needs_wide_column_resolution) {
return false;
}
if (blob_fetcher == nullptr) {
reset_outputs();
*s = Status::NotSupported(
"Encountered blob-backed memtable value without blob fetcher.");
clear_blob_state();
return true;
}
if (needs_plain_value_resolution) {
Slice blob_index_slice;
std::string blob_index_storage;
if (value != nullptr) {
if (value->size() > 0) {
blob_index_slice = Slice(value->data(), value->size());
} else {
blob_index_slice = Slice(*(value->GetSelf()));
}
} else {
assert(columns != nullptr);
const WideColumns& plain_value_columns = columns->columns();
assert(plain_value_columns.size() == 1);
assert(plain_value_columns.front().name() == kDefaultWideColumnName);
blob_index_slice = plain_value_columns.front().value();
}
PinnableSlice resolved_value;
PinnableSlice* target = value != nullptr ? value : &resolved_value;
if (value != nullptr) {
// BlobIndex::DecodeFrom can retain Slices into the encoded bytes for
// inlined blob indices, so take an owned copy before resetting `value`
// and reusing the same PinnableSlice as the output target.
blob_index_storage.assign(blob_index_slice.data(),
blob_index_slice.size());
blob_index_slice = Slice(blob_index_storage);
value->Reset();
}
*s = blob_fetcher->FetchBlob(key, blob_index_slice,
nullptr /* prefetch_buffer */, target,
nullptr /* bytes_read */);
if (s->ok() && columns != nullptr) {
columns->SetPlainValue(std::move(*target));
} else if (!s->ok()) {
reset_outputs();
if (s->IsIncomplete() && value_found != nullptr) {
*value_found = false;
}
}
clear_blob_state();
return true;
}
assert(columns != nullptr);
std::string resolved_entity;
bool resolved = false;
*s = WideColumnSerialization::ResolveEntityBlobColumns(
columns->value_, key, blob_fetcher, nullptr /* prefetch_buffers */,
resolved_entity, resolved, nullptr /* total_bytes_read */,
nullptr /* num_blobs_resolved */);
if (s->ok()) {
assert(resolved);
if (resolved) {
*s = columns->SetWideColumnValue(std::move(resolved_entity));
}
}
if (!s->ok()) {
reset_outputs();
if (s->IsIncomplete() && value_found != nullptr) {
*value_found = false;
}
}
clear_blob_state();
return true;
}
void DBImpl::PostprocessMemtableValueRead(
const Slice& key, const std::string* timestamp,
bool resolve_blob_backed_memtable_value,
const BlobFetcher* memtable_blob_fetcher, PinnableSlice* value,
PinnableWideColumns* columns, Status* s, bool* is_blob_index,
bool* value_found) {
if (resolve_blob_backed_memtable_value) {
std::string blob_lookup_key_storage;
const bool value_resolved = MaybeResolveMemtableBlobValue(
GetBlobLookupUserKey(key, timestamp, &blob_lookup_key_storage),
memtable_blob_fetcher, value, columns, s, is_blob_index, value_found);
if (!value_resolved && value != nullptr && s->ok()) {
value->PinSelf();
}
return;
}
if (s->ok()) {
if (value != nullptr) {
value->PinSelf();
}
} else {
if (value != nullptr) {
value->Reset();
}
if (columns != nullptr) {
columns->Reset();
}
}
}
bool DBImpl::MaybeResolveDirectWriteValue(
const ReadOptions& read_options, const Slice& key,
bool resolve_direct_write_value, const Version* current,
@@ -2963,6 +3264,14 @@ Status DBImpl::GetImpl(const ReadOptions& read_options, const Slice& key,
}
const bool resolve_direct_write_value =
partition_mgr != nullptr && (is_blob_ptr == &is_blob_index);
std::optional<BlobFetcher> memtable_blob_fetcher;
if (partition_mgr != nullptr) {
memtable_blob_fetcher.emplace(sv->current, read_options,
cfd->blob_file_cache(),
/*allow_write_path_fallback=*/true);
}
const BlobFetcher* memtable_blob_fetcher_ptr =
memtable_blob_fetcher ? &*memtable_blob_fetcher : nullptr;
std::string blob_lookup_key_storage;
auto get_blob_lookup_key = [&]() -> Slice {
return GetBlobLookupUserKey(key, timestamp, &blob_lookup_key_storage);
@@ -2990,19 +3299,21 @@ Status DBImpl::GetImpl(const ReadOptions& read_options, const Slice& key,
get_impl_options.columns, timestamp, &s, &merge_context,
&max_covering_tombstone_seq, read_options,
false /* immutable_memtable */,
get_impl_options.callback, is_blob_ptr)) {
get_impl_options.callback, is_blob_ptr,
/*do_merge=*/true, memtable_blob_fetcher_ptr)) {
done = true;
maybe_resolve_memtable_value();
RecordTick(stats_, MEMTABLE_HIT);
} else if ((s.ok() || s.IsMergeInProgress()) &&
sv->imm->Get(
lkey,
get_impl_options.value ? get_impl_options.value->GetSelf()
: nullptr,
get_impl_options.columns, timestamp, &s, &merge_context,
&max_covering_tombstone_seq, read_options,
get_impl_options.callback, is_blob_ptr)) {
sv->imm->Get(lkey,
get_impl_options.value
? get_impl_options.value->GetSelf()
: nullptr,
get_impl_options.columns, timestamp, &s,
&merge_context, &max_covering_tombstone_seq,
read_options, get_impl_options.callback,
is_blob_ptr, memtable_blob_fetcher_ptr)) {
done = true;
maybe_resolve_memtable_value();
@@ -3014,14 +3325,14 @@ Status DBImpl::GetImpl(const ReadOptions& read_options, const Slice& key,
if (sv->mem->Get(lkey, /*value=*/nullptr, /*columns=*/nullptr,
/*timestamp=*/nullptr, &s, &merge_context,
&max_covering_tombstone_seq, read_options,
false /* immutable_memtable */, nullptr, nullptr,
false)) {
false /* immutable_memtable */, nullptr, nullptr, false,
memtable_blob_fetcher_ptr)) {
done = true;
RecordTick(stats_, MEMTABLE_HIT);
} else if ((s.ok() || s.IsMergeInProgress()) &&
sv->imm->GetMergeOperands(lkey, &s, &merge_context,
&max_covering_tombstone_seq,
read_options)) {
sv->imm->GetMergeOperands(
lkey, &s, &merge_context, &max_covering_tombstone_seq,
read_options, memtable_blob_fetcher_ptr)) {
done = true;
RecordTick(stats_, MEMTABLE_HIT);
}
@@ -3717,6 +4028,14 @@ Status DBImpl::MultiGetImpl(
(*sorted_keys)[start_key]->column_family);
ColumnFamilyData* cfd = cfh->cfd();
auto* partition_mgr = cfd->blob_partition_manager();
std::optional<BlobFetcher> memtable_blob_fetcher;
if (partition_mgr != nullptr) {
memtable_blob_fetcher.emplace(super_version->current, read_options,
cfd->blob_file_cache(),
/*allow_write_path_fallback=*/true);
}
const BlobFetcher* memtable_blob_fetcher_ptr =
memtable_blob_fetcher ? &*memtable_blob_fetcher : nullptr;
// Clear the timestamps for returning results so that we can distinguish
// between tombstone or key that has never been written
for (size_t i = start_key; i < start_key + num_keys; ++i) {
@@ -3763,9 +4082,11 @@ Status DBImpl::MultiGetImpl(
has_unpersisted_data_.load(std::memory_order_relaxed));
if (!skip_memtable) {
super_version->mem->MultiGet(read_options, &range, callback,
false /* immutable_memtable */);
false /* immutable_memtable */,
memtable_blob_fetcher_ptr);
if (!range.empty()) {
super_version->imm->MultiGet(read_options, &range, callback);
super_version->imm->MultiGet(read_options, &range, callback,
memtable_blob_fetcher_ptr);
}
if (!range.empty()) {
uint64_t left = range.KeysLeft();
@@ -4142,9 +4463,21 @@ Status DBImpl::CreateColumnFamilyImpl(const ReadOptions& read_options,
const std::string& column_family_name,
ColumnFamilyHandle** handle) {
options_mutex_.AssertHeld();
Status s;
*handle = nullptr;
// The empty string is reserved as a "no/unknown column family name" sentinel
// in various APIs and serialization formats (e.g.
// TablePropertiesCollectorFactory::Context::kUnknownColumnFamily, table
// properties), so reject it to avoid ambiguity. Note: prior to this check,
// CreateColumnFamily("") would silently "succeed" but the CF was not
// persisted in the manifest, causing data loss across DB reopens.
if (column_family_name.empty()) {
return Status::InvalidArgument(
"Column family name cannot be the empty string");
}
Status s;
DBOptions db_options =
BuildDBOptions(immutable_db_options_, mutable_db_options_);
s = ColumnFamilyData::ValidateOptions(db_options, cf_options);
@@ -5522,6 +5855,7 @@ Status DBImpl::DeleteFilesInRanges(ColumnFamilyHandle* column_family,
}
VersionEdit edit;
edit.MarkForegroundOperation();
std::set<FileMetaData*> deleted_files;
JobContext job_context(next_job_id_.fetch_add(1), true);
{
@@ -6200,12 +6534,20 @@ Status DBImpl::GetLatestSequenceForKey(
*seq = kMaxSequenceNumber;
*found_record_for_key = false;
std::optional<BlobFetcher> memtable_blob_fetcher;
if (cfd->blob_partition_manager() != nullptr) {
memtable_blob_fetcher.emplace(sv->current, read_options,
cfd->blob_file_cache(),
/*allow_write_path_fallback=*/true);
}
const BlobFetcher* memtable_blob_fetcher_ptr =
memtable_blob_fetcher ? &*memtable_blob_fetcher : nullptr;
// Check if there is a record for this key in the latest memtable
sv->mem->Get(lkey, /*value=*/nullptr, /*columns=*/nullptr, timestamp, &s,
&merge_context, &max_covering_tombstone_seq, seq, read_options,
false /* immutable_memtable */, nullptr /*read_callback*/,
is_blob_index);
is_blob_index, /*do_merge=*/true, memtable_blob_fetcher_ptr);
if (!(s.ok() || s.IsNotFound() || s.IsMergeInProgress())) {
// unexpected error reading memtable.
@@ -6238,7 +6580,8 @@ Status DBImpl::GetLatestSequenceForKey(
// Check if there is a record for this key in the immutable memtables
sv->imm->Get(lkey, /*value=*/nullptr, /*columns=*/nullptr, timestamp, &s,
&merge_context, &max_covering_tombstone_seq, seq, read_options,
nullptr /*read_callback*/, is_blob_index);
nullptr /*read_callback*/, is_blob_index,
memtable_blob_fetcher_ptr);
if (!(s.ok() || s.IsNotFound() || s.IsMergeInProgress())) {
// unexpected error reading memtable.
@@ -6271,7 +6614,7 @@ Status DBImpl::GetLatestSequenceForKey(
sv->imm->GetFromHistory(lkey, /*value=*/nullptr, /*columns=*/nullptr,
timestamp, &s, &merge_context,
&max_covering_tombstone_seq, seq, read_options,
is_blob_index);
is_blob_index, memtable_blob_fetcher_ptr);
if (!(s.ok() || s.IsNotFound() || s.IsMergeInProgress())) {
// unexpected error reading memtable.
@@ -6623,7 +6966,9 @@ Status DBImpl::IngestExternalFiles(
assert(!cfd->IsDropped());
cfds_to_commit.push_back(cfd);
autovector<VersionEdit*> edit_list;
edit_list.push_back(ingestion_jobs[i].edit());
auto* edit = ingestion_jobs[i].edit();
edit->MarkForegroundOperation();
edit_list.push_back(edit);
edit_lists.push_back(edit_list);
++num_entries;
}
@@ -6638,7 +6983,6 @@ Status DBImpl::IngestExternalFiles(
}
status =
versions_->LogAndApply(cfds_to_commit, read_options, write_options,
edit_lists, &mutex_, directories_.GetDbDir());
// It is safe to update VersionSet last seqno here after LogAndApply since
// LogAndApply persists last sequence number from VersionEdits,
@@ -6771,6 +7115,7 @@ Status DBImpl::CreateColumnFamilyWithImport(
SuperVersionContext dummy_sv_ctx(/* create_superversion */ true);
VersionEdit dummy_edit;
dummy_edit.MarkForegroundOperation();
uint64_t next_file_number = 0;
std::unique_ptr<std::list<uint64_t>::iterator> pending_output_elem;
{
@@ -6828,9 +7173,10 @@ Status DBImpl::CreateColumnFamilyWithImport(
// Install job edit [Mutex will be unlocked here]
if (status.ok()) {
status = versions_->LogAndApply(cfd, read_options, write_options,
import_job.edit(), &mutex_,
directories_.GetDbDir());
auto* edit = import_job.edit();
edit->MarkForegroundOperation();
status = versions_->LogAndApply(cfd, read_options, write_options, edit,
&mutex_, directories_.GetDbDir());
if (status.ok()) {
InstallSuperVersionForConfigChange(cfd, &sv_context);
}
@@ -7261,6 +7607,7 @@ Status DBImpl::ReserveFileNumbersBeforeIngestion(
// reuse the file number that has already assigned to the internal file,
// and this will overwrite the external file. To protect the external
// file, we have to make sure the file number will never being reused.
dummy_edit.MarkForegroundOperation();
s = versions_->LogAndApply(cfd, read_options, write_options, &dummy_edit,
&mutex_, directories_.GetDbDir());
if (s.ok()) {
@@ -7273,6 +7620,7 @@ Status DBImpl::ReserveFileNumbersBeforeIngestion(
Status DBImpl::GetCreationTimeOfOldestFile(uint64_t* creation_time) {
if (mutable_db_options_.max_open_files == -1) {
uint64_t oldest_time = std::numeric_limits<uint64_t>::max();
std::once_flag waited_for_async_open;
for (auto cfd : *versions_->GetColumnFamilySet()) {
if (!cfd->IsDropped()) {
uint64_t ctime;
@@ -7280,6 +7628,17 @@ Status DBImpl::GetCreationTimeOfOldestFile(uint64_t* creation_time) {
SuperVersion* sv = GetAndRefSuperVersion(cfd);
Version* version = sv->current;
version->GetCreationTimeOfOldestFile(&ctime);
// For modern DBs, manifest carries file_creation_time and the
// first call returns the real value. We only need to wait for
// BGWorkAsyncFileOpen on legacy DBs whose manifest lacks
// file_creation_time -- those rely on the pinned reader, which is
// null until async file open completes.
if (ctime == 0 && immutable_db_options_.open_files_async) {
std::call_once(waited_for_async_open, [&]() {
WaitForAsyncFileOpen();
version->GetCreationTimeOfOldestFile(&ctime);
});
}
ReturnAndCleanupSuperVersion(cfd, sv);
}
+152 -5
View File
@@ -15,6 +15,8 @@
#include <limits>
#include <list>
#include <map>
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <unordered_map>
@@ -1096,6 +1098,7 @@ class DBImpl : public DB {
superversions_to_free_queue_.push_back(sv);
}
void EnableTrackPublishedSeqInSnapshotContext();
void SetSnapshotChecker(SnapshotChecker* snapshot_checker);
// Fill JobContext with snapshot information needed by flush and compaction.
@@ -1371,6 +1374,7 @@ class DBImpl : public DB {
protected:
const std::string dbname_;
const bool read_only_;
// TODO(peterd): unify with VersionSet::db_id_
std::string db_id_;
// db_session_id_ is an identifier that gets reset
@@ -1452,6 +1456,13 @@ class DBImpl : public DB {
std::atomic<int> next_job_id_ = 1;
std::atomic<bool> shutting_down_ = false;
// Protected by mutex_. This is separate from shutting_down_ because
// OnDBShutdownBegin must fire before shutting_down_ is published, and the
// notification releases mutex_ while invoking listeners. Marking that the
// notification has started prevents a concurrent or reentrant
// CancelAllBackgroundWork() call from firing it again during that unlocked
// callback window.
bool shutdown_notification_sent_ = false;
// No new background jobs can be queued if true. This is used to prevent new
// background jobs from being queued after WaitForCompact() completes waiting
@@ -1526,6 +1537,16 @@ class DBImpl : public DB {
const Status& st,
const CompactionJobStats& job_stats,
int job_id);
// Fires the OnCompactionPreCommit listener callback. Mirrors
// NotifyOnCompactionCompleted but is invoked before ReleaseCompactionFiles()
// (i.e. while input files still have being_compacted == true). Idempotent:
// safe to call from multiple potential release sites; only fires once per
// compaction, and only if NotifyOnCompactionBegin previously fired.
void NotifyOnCompactionPreCommit(ColumnFamilyData* cfd, Compaction* c,
const Status& st,
const CompactionJobStats& job_stats,
int job_id);
void NotifyOnDBShutdownBegin();
void NotifyOnMemTableSealed(ColumnFamilyData* cfd,
const MemTableInfo& mem_table_info);
@@ -1854,6 +1875,11 @@ class DBImpl : public DB {
// Background work function for async file opening.
static void BGWorkAsyncFileOpen(void* arg);
// Block the until any in-flight async file open work has
// completed. No-op when open_files_async is false. Returns early if
// shutdown begins.
void WaitForAsyncFileOpen();
void InvokeWalFilterIfNeededOnColumnFamilyToWalNumberMap();
// Return true to proceed with current WAL record whose content is stored in
@@ -1869,6 +1895,7 @@ class DBImpl : public DB {
private:
friend class DB;
friend class DBImplReadOnly;
friend class DBImplSecondary;
friend class ErrorHandler;
friend class InternalStats;
@@ -2567,10 +2594,15 @@ class DBImpl : public DB {
// Helper function to perform trivial move by updating manifest metadata
// without rewriting data files. This is called when IsTrivialMove() is true.
// REQUIRES: mutex held
// Returns: Status of the trivial move operation
Status PerformTrivialMove(Compaction& c, LogBuffer* log_buffer,
bool& compaction_released, size_t& moved_files,
size_t& moved_bytes);
// Populates the Compaction's edit with DeleteFile/AddFile entries for
// a trivial move (no data rewriting). Does not commit the edit.
void PrepareTrivialMoveEdit(Compaction& c, LogBuffer* log_buffer,
size_t& moved_files, size_t& moved_bytes);
// REQUIRES: mutex held, PrepareTrivialMoveEdit already called
// Commits the trivial move by calling LogAndApply on the prepared edit.
// Returns: Status of the manifest write
Status CommitTrivialMove(Compaction& c, bool& compaction_released);
// REQUIRES: mutex unlocked
void TrackOrUntrackFiles(const std::vector<std::string>& existing_data_files,
@@ -2743,6 +2775,8 @@ class DBImpl : public DB {
Status MaybeReleaseTimestampedSnapshotsAndCheck();
Status MaybeWriteWalMarkersToManifestOnClose();
Status CloseHelper();
void WaitForBackgroundWork();
@@ -2801,7 +2835,68 @@ class DBImpl : public DB {
size_t GetWalPreallocateBlockSize(uint64_t write_buffer_size) const;
Env::WriteLifeTimeHint CalculateWALWriteHint() { return Env::WLTH_SHORT; }
IOStatus CreateWAL(const WriteOptions& write_options, uint64_t log_file_num,
// Returns true when async WAL precreation is enabled and compatible with the
// active WAL strategy. WAL recycling already avoids file creation latency, so
// precreation is disabled when recycle_log_file_num is non-zero.
bool AsyncWALPrecreateEnabled() const;
// A WAL file that has a reserved file number and may have an opened writer,
// but has not been added to DBImpl's in-memory logical WAL tracking lists
// (logs_ and alive_wal_files_).
struct UnpublishedWAL {
uint64_t log_number = 0;
std::unique_ptr<log::Writer> writer;
UnpublishedWAL() = default;
UnpublishedWAL(const UnpublishedWAL&) = delete;
UnpublishedWAL& operator=(const UnpublishedWAL&) = delete;
UnpublishedWAL(UnpublishedWAL&& other) noexcept {
*this = std::move(other);
}
UnpublishedWAL& operator=(UnpublishedWAL&& other) noexcept {
if (this != &other) {
log_number = other.log_number;
writer = std::move(other.writer);
other.Reset();
}
return *this;
}
void Reset() {
log_number = 0;
writer.reset();
}
};
// Reserves the next WAL file number and schedules a HIGH-priority background
// task to precreate that WAL file. A precreated WAL is not a logical WAL
// until a foreground WAL rotation consumes it.
void MaybeScheduleAsyncWALPrecreate(size_t preallocate_block_size);
// Background task for opening the reserved future WAL and publishing the
// result under mutex_.
static void BGWorkAsyncWALPrecreate(void* arg);
// Waits for an in-flight async WAL precreation and returns a prepared WAL if
// one is available. If precreation failed, returns an empty WAL and lets the
// foreground rotation create the WAL synchronously. Caller must hold mutex_.
UnpublishedWAL WaitForAsyncWALPrecreate();
// Opens and preallocates a WAL writer without writing logical WAL records.
// Used by async WAL precreation and by synchronous WAL creation.
IOStatus CreateWALWriter(const DBOptions& db_options, uint64_t log_file_num,
uint64_t recycle_log_number,
size_t preallocate_block_size,
UnpublishedWAL* new_wal);
// Starts an opened WAL file by writing the initial records required before it
// can be installed as the current WAL for foreground writes.
IOStatus StartWALFile(const WriteOptions& write_options,
const PredecessorWALInfo& predecessor_wal_info,
log::Writer* new_log);
IOStatus CreateWAL(const DBOptions& db_options,
const WriteOptions& write_options, uint64_t log_file_num,
uint64_t recycle_log_number, size_t preallocate_block_size,
const PredecessorWALInfo& predecessor_wal_info,
log::Writer** new_log);
@@ -2944,6 +3039,29 @@ class DBImpl : public DB {
bool resolve_direct_write_value, const Version* current,
ColumnFamilyData* cfd, 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.
static bool MaybeResolveMemtableBlobValue(const Slice& key,
const BlobFetcher* blob_fetcher,
PinnableSlice* value,
PinnableWideColumns* columns,
Status* s, bool* is_blob_index,
bool* value_found = nullptr);
// Completes read-only/secondary memtable Get()/GetEntity() hits by resolving
// blob-backed payloads when `resolve_blob_backed_memtable_value` is true,
// pinning plain values on success, and clearing outputs on error. When the
// caller explicitly requested raw blob indices via
// `GetImplOptions::is_blob_index`, this helper leaves that payload
// untouched. `memtable_blob_fetcher` may be null when blob support is
// disabled for the column family.
static void PostprocessMemtableValueRead(
const Slice& key, const std::string* timestamp,
bool resolve_blob_backed_memtable_value,
const BlobFetcher* memtable_blob_fetcher, PinnableSlice* value,
PinnableWideColumns* columns, Status* s, bool* is_blob_index,
bool* value_found = nullptr);
template <typename IterType, typename ImplType,
typename ErrorIteratorFuncType>
@@ -3274,6 +3392,28 @@ class DBImpl : public DB {
AsyncFileOpenState bg_async_file_open_state_ =
AsyncFileOpenState::kNotScheduled;
// State machine for the single async WAL precreation slot protected by
// mutex_. Background precreation failure returns to kNotScheduled; foreground
// rotation handles it the same as no prepared WAL and creates one
// synchronously. kScheduled owns a reserved file number; kReady owns an
// opened writer that has not been started or added to logical WAL tracking.
enum class AsyncWALPrecreateState : uint8_t {
kNotScheduled = 0, // No WAL precreate work is in-flight or ready.
kScheduled, // Background task owns creation of the reserved WAL.
kReady, // Reserved WAL writer is open but not logically live.
};
// Protected by mutex_. Tracks at most one background precreated WAL. A
// precreated WAL is only reserved empty storage until SwitchMemtable()
// consumes it and installs it in DBImpl's in-memory logical WAL tracking
// lists (logs_ and alive_wal_files_).
AsyncWALPrecreateState async_wal_precreate_state_ =
AsyncWALPrecreateState::kNotScheduled;
// Reserved in-flight/ready precreated WAL. The writer is populated only while
// state is kReady.
UnpublishedWAL async_wal_precreate_wal_;
std::deque<ManualCompactionState*> manual_compaction_dequeue_;
// shall we disable deletion of obsolete files
@@ -3348,6 +3488,13 @@ class DBImpl : public DB {
// Callback for compaction to check if a key is visible to a snapshot.
// REQUIRES: mutex held
std::unique_ptr<SnapshotChecker> snapshot_checker_;
// When set, InitSnapshotContext() appends GetLastPublishedSequence() to the
// job's snapshot_seqs (if not already present) so that flush/compaction
// preserves the published-sequence boundary even when no explicit user
// snapshot exists there. Unlike taking a real ManagedSnapshot, this just
// pins the seqno into the snapshot list: no allocation, no snapshot list
// mutation, and no SnapshotChecker.
bool track_published_seq_in_snapshot_context_ = false;
// Callback for when the cached_recoverable_state_ is written to memtable
// Only to be set during initialization
+122 -12
View File
@@ -874,6 +874,24 @@ Status DBImpl::AtomicFlushMemTablesToOutputFiles(
directories_.GetDbDir(), log_buffer);
}
if (!s.ok()) {
// If the atomic flush's combined MANIFEST write failed, the output files
// are cached in the table cache (added by BuildTable during FlushJob::Run)
// but never installed into any Version. Evict them now so that
// TEST_VerifyNoObsoleteFilesCached does not fire during Close() when the
// FindObsoleteFiles full-scan backstop is disabled by
// metadata_read_fault_one_in.
for (int i = 0; i != num_cfs; ++i) {
if (exec_status[i].first && exec_status[i].second.ok() &&
!switched_to_mempurge[i] && file_meta[i].fd.GetFileSize() > 0) {
TableCache::ReleaseObsolete(
cfds[i]->table_cache()->get_cache().get(),
file_meta[i].fd.GetNumber(), nullptr /*handle*/,
all_mutable_cf_options[i].uncache_aggressiveness);
}
}
}
for (int i = 0; i != num_cfs; ++i) {
auto* mgr = cfds[i]->blob_partition_manager();
if (mgr == nullptr || prepared_blob_generations[i] == 0) {
@@ -1563,9 +1581,8 @@ Status DBImpl::CompactFiles(const CompactionOptions& compact_options,
return s;
}
Status DBImpl::PerformTrivialMove(Compaction& c, LogBuffer* log_buffer,
bool& compaction_released,
size_t& moved_files, size_t& moved_bytes) {
void DBImpl::PrepareTrivialMoveEdit(Compaction& c, LogBuffer* log_buffer,
size_t& moved_files, size_t& moved_bytes) {
mutex_.AssertHeld();
ROCKS_LOG_BUFFER(log_buffer, "[%s] Moving %d files to level-%d\n",
@@ -1598,6 +1615,10 @@ Status DBImpl::PerformTrivialMove(Compaction& c, LogBuffer* log_buffer,
}
moved_files += c.num_input_files(l);
}
}
Status DBImpl::CommitTrivialMove(Compaction& c, bool& compaction_released) {
mutex_.AssertHeld();
// Install the new version
const ReadOptions read_options(Env::IOActivity::kCompaction);
@@ -1722,8 +1743,8 @@ Status DBImpl::CompactFilesImpl(
bool compaction_released = false;
size_t moved_files = 0;
size_t moved_bytes = 0;
Status status = PerformTrivialMove(
*c.get(), log_buffer, compaction_released, moved_files, moved_bytes);
PrepareTrivialMoveEdit(*c.get(), log_buffer, moved_files, moved_bytes);
Status status = CommitTrivialMove(*c.get(), compaction_released);
if (status.ok()) {
InstallSuperVersionAndScheduleWork(
@@ -1991,6 +2012,46 @@ void DBImpl::NotifyOnCompactionCompleted(
// flush process.
}
void DBImpl::NotifyOnCompactionPreCommit(
ColumnFamilyData* cfd, Compaction* c, const Status& st,
const CompactionJobStats& compaction_job_stats, const int job_id) {
if (immutable_db_options_.listeners.size() == 0U) {
return;
}
mutex_.AssertHeld();
if (shutting_down_.load(std::memory_order_acquire)) {
return;
}
// Only fire if OnCompactionBegin has fired for this compaction.
if (c->ShouldNotifyOnCompactionCompleted() == false) {
return;
}
// Idempotency: ensure listeners observe at most one
// OnCompactionPreCommit per compaction lifecycle, even though the
// notify helper is invoked at multiple potential ReleaseCompactionFiles()
// sites for safety.
if (c->WasNotifyOnCompactionPreCommitCalled()) {
return;
}
c->SetNotifyOnCompactionPreCommitCalled();
int num_l0_files = cfd->current()->storage_info()->NumLevelFiles(0);
// release lock while notifying events
mutex_.Unlock();
TEST_SYNC_POINT("DBImpl::NotifyOnCompactionPreCommit::UnlockMutex");
{
CompactionJobInfo info{};
info.num_l0_files = num_l0_files;
BuildCompactionJobInfo(cfd, c, st, compaction_job_stats, job_id, &info);
for (const auto& listener : immutable_db_options_.listeners) {
listener->OnCompactionPreCommit(this, info);
}
info.status.PermitUncheckedError();
}
mutex_.Lock();
}
// REQUIREMENT: block all background work by calling PauseBackgroundWork()
// before calling this function
// TODO (hx235): Replace Status::NotSupported() with Status::Aborted() for
@@ -3162,7 +3223,7 @@ void DBImpl::ResumeAllCompactions() {
void DBImpl::MaybeScheduleFlushOrCompaction() {
mutex_.AssertHeld();
TEST_SYNC_POINT("DBImpl::MaybeScheduleFlushOrCompaction:Start");
if (!opened_successfully_) {
if (!opened_successfully_ || read_only_) {
// Compaction may introduce data race to DB open
return;
}
@@ -4006,6 +4067,13 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
status = Status::ShutdownInProgress();
} else if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
status = Status::Incomplete(Status::SubCode::kCompactionAborted);
if (!is_prepicked) {
// This automatic compaction was scheduled from compaction_queue_, but
// abort happened before PickCompactionFromQueue() could remove the
// queued CF. Restore the unscheduled count so ResumeAllCompactions()
// can schedule the still-queued work.
unscheduled_compactions_++;
}
} else if (is_manual &&
manual_compaction->canceled.load(std::memory_order_acquire)) {
status = Status::Incomplete(Status::SubCode::kManualCompactionPaused);
@@ -4013,10 +4081,14 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
} else {
status = error_handler_.GetBGError();
// If we get here, it means a hard error happened after this compaction
// was scheduled by MaybeScheduleFlushOrCompaction(), but before it got
// a chance to execute. Since we didn't pop a cfd from the compaction
// queue, increment unscheduled_compactions_
unscheduled_compactions_++;
// was scheduled by MaybeScheduleFlushOrCompaction(), but before it got a
// chance to execute. Since non-prepicked work consumed an unscheduled
// compaction credit without popping a cfd from the compaction queue,
// restore the credit here. Prepicked work was scheduled directly and did
// not consume unscheduled_compactions_.
if (!is_prepicked) {
unscheduled_compactions_++;
}
}
if (!status.ok()) {
@@ -4237,6 +4309,8 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
for (const auto& f : *c->inputs(0)) {
c->edit()->DeleteFile(c->level(), f->fd.GetNumber());
}
NotifyOnCompactionPreCommit(c->column_family_data(), c.get(), status,
compaction_job_stats, job_context->job_id);
status = versions_->LogAndApply(
c->column_family_data(), read_options, write_options, c->edit(),
&mutex_, directories_.GetDbDir(),
@@ -4455,6 +4529,8 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
++out_file_metadata_it;
}
NotifyOnCompactionPreCommit(c->column_family_data(), c.get(), status,
compaction_job_stats, job_context->job_id);
status = versions_->LogAndApply(
c->column_family_data(), read_options, write_options, c->edit(),
&mutex_, directories_.GetDbDir(),
@@ -4531,8 +4607,12 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
// Perform the trivial move
size_t moved_files = 0;
size_t moved_bytes = 0;
status = PerformTrivialMove(*c.get(), log_buffer, compaction_released,
moved_files, moved_bytes);
// Populate the edit first so that CompactionJobInfo has output file
// info when OnCompactionPreCommit fires.
PrepareTrivialMoveEdit(*c.get(), log_buffer, moved_files, moved_bytes);
NotifyOnCompactionPreCommit(c->column_family_data(), c.get(), status,
compaction_job_stats, job_context->job_id);
status = CommitTrivialMove(*c.get(), compaction_released);
io_s = versions_->io_status();
InstallSuperVersionAndScheduleWork(
c->column_family_data(), job_context->superversion_contexts.data());
@@ -4655,6 +4735,10 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
ReleaseOptionsFileNumber(min_options_file_number_elem);
}
// Fire OnCompactionPreCommit before compaction_job.Install runs
// ReleaseCompactionFiles in its manifest write callback.
NotifyOnCompactionPreCommit(c->column_family_data(), c.get(), status,
compaction_job_stats, job_context->job_id);
status = compaction_job.Install(&compaction_released);
io_s = compaction_job.io_status();
if (status.ok()) {
@@ -4674,6 +4758,14 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
if (c != nullptr) {
if (!compaction_released) {
// Safety net: if any of the paths above did not fire the
// OnCompactionPreCommit notification (e.g. status was not OK and
// an early-out skipped the LogAndApply / Install), still fire the
// notify here while files are about to be released. The notify is
// idempotent and is a no-op if it has already fired (or if Begin never
// fired).
NotifyOnCompactionPreCommit(c->column_family_data(), c.get(), status,
compaction_job_stats, job_context->job_id);
c->ReleaseCompactionFiles(status);
} else {
#ifndef NDEBUG
@@ -5114,6 +5206,11 @@ void DBImpl::MarkAsGrabbedForPurge(uint64_t file_number) {
files_grabbed_for_purge_.insert(file_number);
}
void DBImpl::EnableTrackPublishedSeqInSnapshotContext() {
InstrumentedMutexLock l(&mutex_);
track_published_seq_in_snapshot_context_ = true;
}
void DBImpl::SetSnapshotChecker(SnapshotChecker* snapshot_checker) {
InstrumentedMutexLock l(&mutex_);
// snapshot_checker_ should only set once. If we need to set it multiple
@@ -5147,6 +5244,19 @@ void DBImpl::InitSnapshotContext(JobContext* job_context) {
SequenceNumber earliest_write_conflict_snapshot = kMaxSequenceNumber;
std::vector<SequenceNumber> snapshot_seqs =
snapshots_.GetAll(&earliest_write_conflict_snapshot);
// The SnapshotChecker path above already adds a managed snapshot to
// snapshot_seqs.
if (track_published_seq_in_snapshot_context_ && !snapshot_checker) {
// Pin the published-sequence boundary into snapshot_seqs without taking
// a real snapshot. This prevents flush/compaction from collapsing a version
// visible at the published boundary into a newer unpublished seqno.
const SequenceNumber published_seq = GetLastPublishedSequence();
if (snapshot_seqs.empty() || snapshot_seqs.back() < published_seq) {
snapshot_seqs.push_back(published_seq);
}
}
TEST_SYNC_POINT("DBImpl::InitSnapshotContext:BeforeInit");
job_context->InitSnapshotContext(
snapshot_checker, std::move(managed_snapshot),
earliest_write_conflict_snapshot, std::move(snapshot_seqs));
+36 -12
View File
@@ -1186,8 +1186,9 @@ std::set<std::string> DBImpl::CollectAllDBPaths() {
Status DBImpl::MaybeUpdateNextFileNumber(RecoveryContext* recovery_ctx) {
mutex_.AssertHeld();
uint64_t next_file_number = versions_->current_next_file_number();
uint64_t largest_file_number = next_file_number;
const uint64_t prev_next_file_number = versions_->current_next_file_number();
uint64_t largest_file_number = prev_next_file_number;
bool on_disk_file_advanced_counter = false;
Status s;
for (const auto& path : CollectAllDBPaths()) {
std::vector<std::string> files;
@@ -1201,7 +1202,15 @@ Status DBImpl::MaybeUpdateNextFileNumber(RecoveryContext* recovery_ctx) {
if (!ParseFileName(fname, &number, &type)) {
continue;
}
const std::string normalized_fpath = path + kFilePathSeparator + fname;
std::string normalized_fpath = path;
normalized_fpath += kFilePathSeparator;
normalized_fpath.append(fname);
// Use >= so a crashed-mid-allocation file at exactly
// prev_next_file_number triggers the advance -- otherwise the next
// NewFileNumber() would collide with it.
if (number >= prev_next_file_number) {
on_disk_file_advanced_counter = true;
}
largest_file_number = std::max(largest_file_number, number);
if ((type == kTableFile || type == kBlobFile)) {
recovery_ctx->existing_data_files_.push_back(normalized_fpath);
@@ -1212,16 +1221,31 @@ Status DBImpl::MaybeUpdateNextFileNumber(RecoveryContext* recovery_ctx) {
return s;
}
if (largest_file_number >= next_file_number) {
versions_->next_file_number_.store(largest_file_number + 1);
}
// Legacy bug: initializing largest_file_number from prev_next_file_number
// made the post-loop check trivially true and emitted a noop SetNextFile
// on every recovery.
// Preserved verbatim by default (some tests pin file numbers); skipped
// only when the option is on AND we're not in salvage mode (which
// requires the MANIFEST rewrite this emit triggers).
const bool optimize_manifest_for_recovery =
mutable_db_options_.optimize_manifest_for_recovery &&
!immutable_db_options_.best_efforts_recovery;
VersionEdit edit;
edit.SetNextFile(versions_->next_file_number_.load());
assert(versions_->GetColumnFamilySet());
ColumnFamilyData* default_cfd = versions_->GetColumnFamilySet()->GetDefault();
assert(default_cfd);
recovery_ctx->UpdateVersionEdits(default_cfd, edit);
if (!optimize_manifest_for_recovery || on_disk_file_advanced_counter) {
if (largest_file_number >= prev_next_file_number) {
versions_->next_file_number_.store(largest_file_number + 1);
}
TEST_SYNC_POINT("DBImpl::MaybeUpdateNextFileNumber:EmitEdit");
VersionEdit edit;
edit.SetNextFile(versions_->next_file_number_.load());
assert(versions_->GetColumnFamilySet());
ColumnFamilyData* default_cfd =
versions_->GetColumnFamilySet()->GetDefault();
assert(default_cfd);
recovery_ctx->UpdateVersionEdits(default_cfd, edit);
} else {
TEST_SYNC_POINT("DBImpl::Recovery:SkippedNoopEdit:NextFileNumber");
}
return s;
}
} // namespace ROCKSDB_NAMESPACE
+2 -1
View File
@@ -295,7 +295,8 @@ Status DB::OpenAsFollower(
impl->versions_.reset(new ReactiveVersionSet(
dbname, &impl->immutable_db_options_, impl->mutable_db_options_,
impl->file_options_, impl->table_cache_.get(),
impl->write_buffer_manager_, &impl->write_controller_, impl->io_tracer_));
impl->write_buffer_manager_, &impl->write_controller_, impl->io_tracer_,
impl->db_id_, impl->db_session_id_));
impl->column_family_memtables_.reset(
new ColumnFamilyMemTablesImpl(impl->versions_->GetColumnFamilySet()));
impl->wal_in_db_path_ = impl->immutable_db_options_.IsWalDirSameAsDBPath();
+250 -33
View File
@@ -21,6 +21,7 @@
#include "file/writable_file_writer.h"
#include "logging/logging.h"
#include "monitoring/persistent_stats_history.h"
#include "monitoring/statistics_impl.h"
#include "monitoring/thread_status_util.h"
#include "options/options_helper.h"
#include "rocksdb/options.h"
@@ -122,6 +123,16 @@ DBOptions SanitizeOptions(const std::string& dbname, const DBOptions& src,
result.recycle_log_file_num = 0;
}
if (result.async_wal_precreate && result.recycle_log_file_num != 0) {
// Async WAL precreation reserves a future log number, while WAL recycling
// chooses from old WAL files. Keep the recycling behavior and disable only
// the async optimization.
result.async_wal_precreate = false;
ROCKS_LOG_WARN(result.info_log,
"async_wal_precreate is disabled since "
"recycle_log_file_num is non-zero");
}
if (result.db_paths.size() == 0) {
result.db_paths.emplace_back(dbname, std::numeric_limits<uint64_t>::max());
} else if (result.wal_dir.empty()) {
@@ -680,8 +691,18 @@ Status DBImpl::Recover(
} else if (immutable_db_options_.write_dbid_to_manifest && recovery_ctx) {
VersionEdit edit;
s = SetupDBId(write_options, read_only, is_new_db, is_retry, &edit);
recovery_ctx->UpdateVersionEdits(
versions_->GetColumnFamilySet()->GetDefault(), edit);
// best_efforts_recovery rebuilds CURRENT/MANIFEST as the side-effect
// of LogAndApplyForRecovery emitting an edit, so the optimization is
// disabled there (see optimize_manifest_for_recovery doc).
const bool optimize_manifest_for_recovery =
mutable_db_options_.optimize_manifest_for_recovery &&
!immutable_db_options_.best_efforts_recovery;
if (!optimize_manifest_for_recovery || edit.HasDbId()) {
recovery_ctx->UpdateVersionEdits(
versions_->GetColumnFamilySet()->GetDefault(), edit);
} else {
TEST_SYNC_POINT("DBImpl::Recovery:SkippedNoopEdit:SetupDBId");
}
} else {
s = SetupDBId(write_options, read_only, is_new_db, is_retry, nullptr);
}
@@ -770,6 +791,10 @@ Status DBImpl::Recover(
// otherwise, in the future, if WAL tracking is enabled again,
// since the WALs deleted when WAL tracking is disabled are not persisted
// into MANIFEST, WAL check may fail.
//
// Intentionally NOT gated by optimize_manifest_for_recovery:
// this edit is safety-critical for a future re-enable of WAL tracking,
// even though it can look noop-ish under the current session.
VersionEdit edit;
WalNumber max_wal_number =
versions_->GetWalSet().GetWals().rbegin()->first;
@@ -785,9 +810,19 @@ Status DBImpl::Recover(
if (!wal_files.empty()) {
if (error_if_wal_file_exists) {
return Status::Corruption(
"The db was opened in readonly mode with error_if_wal_file_exists"
"flag but a WAL file already exists");
for (const auto& wal_file : wal_files) {
uint64_t bytes;
s = env_->GetFileSize(wal_file.second, &bytes);
if (!s.ok()) {
return s;
}
if (bytes > 0) {
return Status::Corruption(
"The db was opened in readonly mode with "
"error_if_wal_file_exists flag but a non-empty WAL file "
"already exists");
}
}
} else if (error_if_data_exists_in_wals) {
for (auto& wal_file : wal_files) {
uint64_t bytes;
@@ -983,7 +1018,11 @@ Status DBImpl::InitPersistStatsColumnFamily() {
Status DBImpl::LogAndApplyForRecovery(const RecoveryContext& recovery_ctx) {
mutex_.AssertHeld();
assert(versions_->descriptor_log_ == nullptr);
// descriptor_log_ is normally null after Recover, but when
// reuse_manifest_on_open is set VersionSet::Recover may have already
// bound a log::Writer to the existing MANIFEST for append.
assert(versions_->descriptor_log_ == nullptr ||
immutable_db_options_.reuse_manifest_on_open);
const ReadOptions read_options(Env::IOActivity::kDBOpen);
const WriteOptions write_options(Env::IOActivity::kDBOpen);
@@ -1870,25 +1909,47 @@ Status DBImpl::MaybeFlushFinalMemtableOrRestoreActiveLogFiles(
versions_->MarkFileNumberUsed(max_wal_number + 1);
assert(recovery_ctx != nullptr);
const bool optimize_manifest_for_recovery =
mutable_db_options_.optimize_manifest_for_recovery &&
!immutable_db_options_.best_efforts_recovery;
for (auto* cfd : *versions_->GetColumnFamilySet()) {
auto iter = version_edits->find(cfd->GetID());
assert(iter != version_edits->end());
recovery_ctx->UpdateVersionEdits(cfd, iter->second);
const VersionEdit& cf_edit = iter->second;
if (!optimize_manifest_for_recovery ||
cf_edit.ShouldEmitPerColumnFamilyRecoveryEdit(
cfd->GetLogNumber())) {
recovery_ctx->UpdateVersionEdits(cfd, cf_edit);
} else {
TEST_SYNC_POINT("DBImpl::Recovery:SkippedNoopEdit:PerCF");
}
}
if (flushed || !data_seen) {
const uint64_t new_min_log = max_wal_number + 1;
VersionEdit wal_deletion;
bool emit_wal_deletion = false;
if (immutable_db_options_.track_and_verify_wals_in_manifest) {
wal_deletion.DeleteWalsBefore(max_wal_number + 1);
// Determining whether DeleteWalsBefore actually shrinks WalSet
// membership requires WalSet state outside this site, so emit
// unconditionally (pre-existing behavior).
wal_deletion.DeleteWalsBefore(new_min_log);
emit_wal_deletion = true;
}
if (!allow_2pc()) {
// In non-2pc mode, flushing the memtables of the column families
// means we can advance min_log_number_to_keep.
wal_deletion.SetMinLogNumberToKeep(max_wal_number + 1);
const bool min_log_advances =
new_min_log > versions_->min_log_number_to_keep();
if (!allow_2pc() &&
(!optimize_manifest_for_recovery || min_log_advances)) {
wal_deletion.SetMinLogNumberToKeep(new_min_log);
emit_wal_deletion = true;
}
assert(versions_->GetColumnFamilySet() != nullptr);
recovery_ctx->UpdateVersionEdits(
versions_->GetColumnFamilySet()->GetDefault(), wal_deletion);
if (!optimize_manifest_for_recovery || emit_wal_deletion) {
recovery_ctx->UpdateVersionEdits(
versions_->GetColumnFamilySet()->GetDefault(), wal_deletion);
} else {
TEST_SYNC_POINT("DBImpl::Recovery:SkippedNoopEdit:WalDeletion");
}
}
}
}
@@ -2173,6 +2234,16 @@ Status DBImpl::WriteLevel0TableForRecovery(int job_id, ColumnFamilyData* cfd,
constexpr int level = 0;
if (s.ok() && has_output) {
// Before publishing any recovery edit, make the new SST's directory entry
// durable. Reused MANIFESTs have no CURRENT update, and fresh MANIFEST
// publication should not be relied on to order the recovered SST entry.
s = GetDataDir(cfd, meta.fd.GetPathId())
->FsyncWithDirOptions(
IOOptions(), nullptr,
DirFsyncOptions(DirFsyncOptions::FsyncReason::kNewFileSynced));
}
if (s.ok() && has_output) {
edit->AddFile(level, meta.fd.GetNumber(), meta.fd.GetPathId(),
meta.fd.GetFileSize(), meta.smallest, meta.largest,
@@ -2337,16 +2408,17 @@ Status DB::OpenAndTrimHistory(
return s;
}
IOStatus DBImpl::CreateWAL(const WriteOptions& write_options,
uint64_t log_file_num, uint64_t recycle_log_number,
size_t preallocate_block_size,
const PredecessorWALInfo& predecessor_wal_info,
log::Writer** new_log) {
IOStatus DBImpl::CreateWALWriter(const DBOptions& db_options,
uint64_t log_file_num,
uint64_t recycle_log_number,
size_t preallocate_block_size,
UnpublishedWAL* new_wal) {
assert(new_wal);
assert(new_wal->log_number == 0);
assert(new_wal->writer == nullptr);
IOStatus io_s;
std::unique_ptr<FSWritableFile> lfile;
DBOptions db_options =
BuildDBOptions(immutable_db_options_, mutable_db_options_);
FileOptions opt_file_options =
fs_->OptimizeForLogWrite(file_options_, db_options);
opt_file_options.write_hint = CalculateWALWriteHint();
@@ -2384,21 +2456,162 @@ IOStatus DBImpl::CreateWAL(const WriteOptions& write_options,
Histograms::HISTOGRAM_ENUM_MAX /* hist_type */, listeners, nullptr,
tmp_set.Contains(FileType::kWalFile),
tmp_set.Contains(FileType::kWalFile)));
*new_log = new log::Writer(std::move(file_writer), log_file_num,
immutable_db_options_.recycle_log_file_num > 0,
immutable_db_options_.manual_wal_flush,
immutable_db_options_.wal_compression,
immutable_db_options_.track_and_verify_wals);
io_s = (*new_log)->AddCompressionTypeRecord(write_options);
if (io_s.ok()) {
io_s = (*new_log)->MaybeAddPredecessorWALInfo(write_options,
predecessor_wal_info);
}
new_wal->log_number = log_file_num;
new_wal->writer = std::make_unique<log::Writer>(
std::move(file_writer), log_file_num,
immutable_db_options_.recycle_log_file_num > 0,
immutable_db_options_.manual_wal_flush,
immutable_db_options_.wal_compression,
immutable_db_options_.track_and_verify_wals);
}
return io_s;
}
IOStatus DBImpl::StartWALFile(const WriteOptions& write_options,
const PredecessorWALInfo& predecessor_wal_info,
log::Writer* new_log) {
assert(new_log);
IOStatus io_s = new_log->AddCompressionTypeRecord(write_options);
TEST_SYNC_POINT_CALLBACK("DBImpl::StartWALFile:AfterCompressionTypeRecord",
&io_s);
if (io_s.ok()) {
io_s = new_log->MaybeAddPredecessorWALInfo(write_options,
predecessor_wal_info);
}
return io_s;
}
IOStatus DBImpl::CreateWAL(const DBOptions& db_options,
const WriteOptions& write_options,
uint64_t log_file_num, uint64_t recycle_log_number,
size_t preallocate_block_size,
const PredecessorWALInfo& predecessor_wal_info,
log::Writer** new_log) {
assert(new_log);
assert(*new_log == nullptr);
UnpublishedWAL wal;
IOStatus io_s = CreateWALWriter(db_options, log_file_num, recycle_log_number,
preallocate_block_size, &wal);
if (io_s.ok()) {
io_s = StartWALFile(write_options, predecessor_wal_info, wal.writer.get());
}
if (io_s.ok()) {
*new_log = wal.writer.release();
}
return io_s;
}
bool DBImpl::AsyncWALPrecreateEnabled() const {
return immutable_db_options_.async_wal_precreate &&
immutable_db_options_.recycle_log_file_num == 0;
}
struct AsyncWALPrecreateContext {
DBImpl* db = nullptr;
DBOptions db_options;
uint64_t log_number = 0;
size_t preallocate_block_size = 0;
};
void DBImpl::MaybeScheduleAsyncWALPrecreate(size_t preallocate_block_size) {
mutex_.AssertHeld();
if (!AsyncWALPrecreateEnabled() ||
shutting_down_.load(std::memory_order_acquire) ||
error_handler_.IsBGWorkStopped() ||
async_wal_precreate_state_ != AsyncWALPrecreateState::kNotScheduled) {
return;
}
async_wal_precreate_wal_.Reset();
async_wal_precreate_wal_.log_number = versions_->NewFileNumber();
async_wal_precreate_state_ = AsyncWALPrecreateState::kScheduled;
auto* ctx = new AsyncWALPrecreateContext();
ctx->db = this;
ctx->db_options = BuildDBOptions(immutable_db_options_, mutable_db_options_);
ctx->log_number = async_wal_precreate_wal_.log_number;
ctx->preallocate_block_size = preallocate_block_size;
env_->Schedule(&DBImpl::BGWorkAsyncWALPrecreate, ctx, Env::Priority::HIGH,
nullptr);
}
DBImpl::UnpublishedWAL DBImpl::WaitForAsyncWALPrecreate() {
mutex_.AssertHeld();
UnpublishedWAL result;
if (!AsyncWALPrecreateEnabled()) {
return result;
}
TEST_SYNC_POINT("DBImpl::WaitForAsyncWALPrecreate:Begin");
bool waited = false;
uint64_t wait_start_micros = 0;
while (async_wal_precreate_state_ == AsyncWALPrecreateState::kScheduled) {
TEST_SYNC_POINT("DBImpl::WaitForAsyncWALPrecreate:BeforeWait");
if (!waited) {
waited = true;
wait_start_micros = immutable_db_options_.clock->NowMicros();
RecordTick(stats_, WAL_PRECREATE_WAITED);
}
bg_cv_.Wait();
}
if (waited) {
RecordTick(stats_, WAL_PRECREATE_WAIT_MICROS,
immutable_db_options_.clock->NowMicros() - wait_start_micros);
}
if (async_wal_precreate_state_ == AsyncWALPrecreateState::kReady) {
result = std::move(async_wal_precreate_wal_);
async_wal_precreate_state_ = AsyncWALPrecreateState::kNotScheduled;
RecordTick(stats_, WAL_PRECREATE_HIT);
return result;
}
assert(async_wal_precreate_state_ == AsyncWALPrecreateState::kNotScheduled);
RecordTick(stats_, WAL_PRECREATE_MISS);
return result;
}
void DBImpl::BGWorkAsyncWALPrecreate(void* arg) {
TEST_SYNC_POINT("DBImpl::BGWorkAsyncWALPrecreate:Start");
std::unique_ptr<AsyncWALPrecreateContext> ctx(
static_cast<AsyncWALPrecreateContext*>(arg));
DBImpl* db = ctx->db;
UnpublishedWAL new_wal;
IOStatus io_s = db->CreateWALWriter(ctx->db_options, ctx->log_number,
0 /* recycle_log_number */,
ctx->preallocate_block_size, &new_wal);
if (!io_s.ok()) {
RecordTick(db->stats_, WAL_PRECREATE_FAILED);
ROCKS_LOG_WARN(db->immutable_db_options_.info_log,
"Async WAL precreate failed for #%" PRIu64 ": %s",
ctx->log_number, io_s.ToString().c_str());
}
TEST_SYNC_POINT("DBImpl::BGWorkAsyncWALPrecreate:BeforePublish");
TEST_SYNC_POINT("DBImpl::BGWorkAsyncWALPrecreate:Publish");
{
InstrumentedMutexLock l(&db->mutex_);
if (db->async_wal_precreate_state_ == AsyncWALPrecreateState::kScheduled &&
db->async_wal_precreate_wal_.log_number == ctx->log_number) {
if (io_s.ok()) {
db->async_wal_precreate_wal_ = std::move(new_wal);
db->async_wal_precreate_state_ = AsyncWALPrecreateState::kReady;
} else {
// Async WAL precreation is best-effort. Clear the slot so foreground
// rotation falls back to normal synchronous WAL creation.
db->async_wal_precreate_wal_.Reset();
db->async_wal_precreate_state_ = AsyncWALPrecreateState::kNotScheduled;
}
}
db->bg_cv_.SignalAll();
}
TEST_SYNC_POINT("DBImpl::BGWorkAsyncWALPrecreate:Done");
}
void DBImpl::TrackExistingDataFiles(
const std::vector<std::string>& existing_data_files) {
TrackOrUntrackFiles(existing_data_files, /*track=*/true);
@@ -2494,8 +2707,10 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
// created during DB open with predecessor WALs from previous DB session due
// to `avoid_flush_during_recovery == true`. This can protect the last WAL
// recovered.
s = impl->CreateWAL(write_options, new_log_number, 0 /*recycle_log_number*/,
preallocate_block_size,
const DBOptions db_options_snapshot =
BuildDBOptions(impl->immutable_db_options_, impl->mutable_db_options_);
s = impl->CreateWAL(db_options_snapshot, write_options, new_log_number,
0 /*recycle_log_number*/, preallocate_block_size,
PredecessorWALInfo() /* predecessor_wal_info */,
&new_log);
if (s.ok()) {
@@ -2700,6 +2915,8 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
if (impl->immutable_db_options_.open_files_async) {
impl->ScheduleAsyncFileOpening();
}
impl->MaybeScheduleAsyncWALPrecreate(
impl->GetWalPreallocateBlockSize(max_write_buffer_size));
impl->mutex_.Unlock();
}
+35 -7
View File
@@ -5,7 +5,10 @@
#include "db/db_impl/db_impl_readonly.h"
#include <optional>
#include "db/arena_wrapped_db_iter.h"
#include "db/blob/blob_fetcher.h"
#include "db/db_impl/compacted_db_impl.h"
#include "db/db_impl/db_impl.h"
#include "db/manifest_ops.h"
@@ -63,13 +66,25 @@ Status DBImplReadOnly::GetImpl(const ReadOptions& read_options,
const Comparator* ucmp = get_impl_options.column_family->GetComparator();
assert(ucmp);
std::string* ts =
ucmp->timestamp_size() > 0 ? get_impl_options.timestamp : nullptr;
SequenceNumber snapshot = versions_->LastSequence();
GetWithTimestampReadCallback read_cb(snapshot);
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(
get_impl_options.column_family);
auto cfd = cfh->cfd();
bool is_blob_index = false;
bool* is_blob_ptr = get_impl_options.is_blob_index;
std::string timestamp_storage;
std::string* ts = nullptr;
if (ucmp->timestamp_size() > 0) {
ts = get_impl_options.timestamp != nullptr
? get_impl_options.timestamp
: (get_impl_options.get_value ? &timestamp_storage : nullptr);
}
if (!is_blob_ptr && get_impl_options.get_value) {
is_blob_ptr = &is_blob_index;
}
const bool resolve_blob_backed_memtable_value =
get_impl_options.get_value && (is_blob_ptr == &is_blob_index);
if (tracer_) {
InstrumentedMutexLock lock(&trace_mutex_);
if (tracer_) {
@@ -95,6 +110,18 @@ Status DBImplReadOnly::GetImpl(const ReadOptions& read_options,
SequenceNumber max_covering_tombstone_seq = 0;
LookupKey lkey(key, snapshot, read_options.timestamp);
PERF_TIMER_STOP(get_snapshot_time);
std::optional<BlobFetcher> memtable_blob_fetcher;
if (cfd->ioptions().enable_blob_direct_write ||
cfd->GetLatestMutableCFOptions().enable_blob_files) {
// Recovered memtables can still contain older blob references after
// mutable blob-file settings change, so keep blob resolution available
// whenever either blob knob indicates it may be needed.
memtable_blob_fetcher.emplace(super_version->current, read_options,
cfd->blob_file_cache(),
/*allow_write_path_fallback=*/true);
}
const BlobFetcher* memtable_blob_fetcher_ptr =
memtable_blob_fetcher ? &*memtable_blob_fetcher : nullptr;
// Look up starts here
if (super_version->mem->Get(
@@ -102,11 +129,12 @@ Status DBImplReadOnly::GetImpl(const ReadOptions& read_options,
get_impl_options.value ? get_impl_options.value->GetSelf() : nullptr,
get_impl_options.columns, ts, &s, &merge_context,
&max_covering_tombstone_seq, read_options,
false /* immutable_memtable */, &read_cb,
/*is_blob_index=*/nullptr, /*do_merge=*/get_impl_options.get_value)) {
if (get_impl_options.value) {
get_impl_options.value->PinSelf();
}
false /* immutable_memtable */, &read_cb, is_blob_ptr,
/*do_merge=*/get_impl_options.get_value, memtable_blob_fetcher_ptr)) {
DBImpl::PostprocessMemtableValueRead(
key, ts, resolve_blob_backed_memtable_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_HIT);
} else {
PERF_TIMER_GUARD(get_from_output_files_time);
+67 -28
View File
@@ -6,8 +6,10 @@
#include "db/db_impl/db_impl_secondary.h"
#include <cinttypes>
#include <optional>
#include "db/arena_wrapped_db_iter.h"
#include "db/blob/blob_fetcher.h"
#include "db/log_reader.h"
#include "db/log_writer.h"
#include "db/merge_context.h"
@@ -103,10 +105,22 @@ Status DBImplSecondary::FindNewLogNumbers(std::vector<uint64_t>* logs) {
return s;
}
// if log_readers_ is non-empty, it means we have applied all logs with log
// numbers smaller than the smallest log in log_readers_, so there is no
// need to pass these logs to RecoverLogFiles
uint64_t log_number_min = 0;
// Drop readers only after MANIFEST replay says the WAL is obsolete. The
// primary can create a higher-number empty WAL before it stops appending to a
// lower-number current WAL, so seeing a higher-number WAL is not sufficient.
const uint64_t min_log_number_to_keep = versions_->min_log_number_to_keep();
for (auto iter = log_readers_.begin(); iter != log_readers_.end();) {
if (iter->first < min_log_number_to_keep) {
iter = log_readers_.erase(iter);
} else {
break;
}
}
// If log_readers_ is non-empty, it means we have applied all logs with log
// numbers smaller than the smallest retained log reader. Otherwise, manifest
// recovery tells us which older logs can be skipped.
uint64_t log_number_min = min_log_number_to_keep;
if (!log_readers_.empty()) {
log_number_min = log_readers_.begin()->first;
}
@@ -319,12 +333,6 @@ Status DBImplSecondary::RecoverLogFiles(
return status;
}
}
// remove logreaders from map after successfully recovering the WAL
if (log_readers_.size() > 1) {
auto erase_iter = log_readers_.begin();
std::advance(erase_iter, log_readers_.size() - 1);
log_readers_.erase(log_readers_.begin(), erase_iter);
}
return status;
}
@@ -363,13 +371,25 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
const Comparator* ucmp = get_impl_options.column_family->GetComparator();
assert(ucmp);
std::string* ts =
ucmp->timestamp_size() > 0 ? get_impl_options.timestamp : nullptr;
SequenceNumber snapshot = versions_->LastSequence();
GetWithTimestampReadCallback read_cb(snapshot);
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(
get_impl_options.column_family);
auto cfd = cfh->cfd();
bool is_blob_index = false;
bool* is_blob_ptr = get_impl_options.is_blob_index;
std::string timestamp_storage;
std::string* ts = nullptr;
if (ucmp->timestamp_size() > 0) {
ts = get_impl_options.timestamp != nullptr
? get_impl_options.timestamp
: (get_impl_options.get_value ? &timestamp_storage : nullptr);
}
if (!is_blob_ptr && get_impl_options.get_value) {
is_blob_ptr = &is_blob_index;
}
const bool resolve_blob_backed_memtable_value =
get_impl_options.get_value && (is_blob_ptr == &is_blob_index);
if (tracer_) {
InstrumentedMutexLock lock(&trace_mutex_);
if (tracer_) {
@@ -395,6 +415,18 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
LookupKey lkey(key, snapshot, read_options.timestamp);
PERF_TIMER_STOP(get_snapshot_time);
bool done = false;
std::optional<BlobFetcher> memtable_blob_fetcher;
if (cfd->ioptions().enable_blob_direct_write ||
cfd->GetLatestMutableCFOptions().enable_blob_files) {
// Catch-up can rebuild older blob references into memtables after mutable
// blob-file settings change, so keep blob resolution available whenever
// either blob knob indicates it may be needed.
memtable_blob_fetcher.emplace(super_version->current, read_options,
cfd->blob_file_cache(),
/*allow_write_path_fallback=*/true);
}
const BlobFetcher* memtable_blob_fetcher_ptr =
memtable_blob_fetcher ? &*memtable_blob_fetcher : nullptr;
// Look up starts here
if (get_impl_options.get_value) {
@@ -404,12 +436,14 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
: nullptr,
get_impl_options.columns, ts, &s, &merge_context,
&max_covering_tombstone_seq, read_options,
false /* immutable_memtable */, &read_cb,
/*is_blob_index=*/nullptr, /*do_merge=*/true)) {
false /* immutable_memtable */, &read_cb, is_blob_ptr,
/*do_merge=*/true, memtable_blob_fetcher_ptr)) {
done = true;
if (get_impl_options.value) {
get_impl_options.value->PinSelf();
}
DBImpl::PostprocessMemtableValueRead(
key, ts, resolve_blob_backed_memtable_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_HIT);
} else if ((s.ok() || s.IsMergeInProgress()) &&
super_version->imm->Get(
@@ -417,11 +451,14 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
get_impl_options.value ? get_impl_options.value->GetSelf()
: nullptr,
get_impl_options.columns, ts, &s, &merge_context,
&max_covering_tombstone_seq, read_options, &read_cb)) {
&max_covering_tombstone_seq, read_options, &read_cb,
is_blob_ptr, memtable_blob_fetcher_ptr)) {
done = true;
if (get_impl_options.value) {
get_impl_options.value->PinSelf();
}
DBImpl::PostprocessMemtableValueRead(
key, ts, resolve_blob_backed_memtable_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_HIT);
}
} else {
@@ -433,13 +470,14 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
get_impl_options.columns, ts, &s, &merge_context,
&max_covering_tombstone_seq, read_options,
false /* immutable_memtable */, &read_cb,
/*is_blob_index=*/nullptr, /*do_merge=*/false)) {
/*is_blob_index=*/nullptr, /*do_merge=*/false,
memtable_blob_fetcher_ptr)) {
done = true;
RecordTick(stats_, MEMTABLE_HIT);
} else if ((s.ok() || s.IsMergeInProgress()) &&
super_version->imm->GetMergeOperands(lkey, &s, &merge_context,
&max_covering_tombstone_seq,
read_options)) {
super_version->imm->GetMergeOperands(
lkey, &s, &merge_context, &max_covering_tombstone_seq,
read_options, memtable_blob_fetcher_ptr)) {
done = true;
RecordTick(stats_, MEMTABLE_HIT);
}
@@ -457,7 +495,7 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
ts, &s, &merge_context, &max_covering_tombstone_seq, &pinned_iters_mgr,
/*value_found*/ nullptr,
/*key_exists*/ nullptr, /*seq*/ nullptr, &read_cb, /*is_blob*/ nullptr,
/*do_merge*/ true);
/*do_merge=*/get_impl_options.get_value);
RecordTick(stats_, MEMTABLE_MISS);
}
{
@@ -777,7 +815,8 @@ Status DBImplSecondary::OpenAsSecondaryImpl(
impl->versions_.reset(new ReactiveVersionSet(
dbname, &impl->immutable_db_options_, impl->mutable_db_options_,
impl->file_options_, impl->table_cache_.get(),
impl->write_buffer_manager_, &impl->write_controller_, impl->io_tracer_));
impl->write_buffer_manager_, &impl->write_controller_, impl->io_tracer_,
impl->db_id_, impl->db_session_id_));
impl->column_family_memtables_.reset(
new ColumnFamilyMemTablesImpl(impl->versions_->GetColumnFamilySet()));
impl->wal_in_db_path_ = impl->immutable_db_options_.IsWalDirSameAsDBPath();
@@ -1553,7 +1592,7 @@ Status DB::OpenAndCompact(
}
}
// 5. Open db As Secondary (skip WAL recovery — remote compaction only
// 5. Open db As Secondary (skip WAL recovery -- remote compaction only
// needs LSM state from MANIFEST, not memtable data from WAL replay)
std::unique_ptr<DB> db;
std::vector<ColumnFamilyHandle*> handles;
+43 -4
View File
@@ -16,6 +16,7 @@
#include "db/error_handler.h"
#include "db/event_helpers.h"
#include "db/wide/wide_columns_helper.h"
#include "file/filename.h"
#include "logging/logging.h"
#include "memtable/wbwi_memtable.h"
#include "monitoring/perf_context_imp.h"
@@ -1546,6 +1547,10 @@ Status DBImpl::WriteImpl(
// Note: if we are to resume after non-OK statuses we need to revisit how
// we react to non-OK statuses here.
if (w.status.ok()) { // Don't publish a partial batch write
TEST_SYNC_POINT(
"DBImpl::WriteImpl:AfterWBWIIngestBeforeSetLastSequence:pause");
TEST_SYNC_POINT(
"DBImpl::WriteImpl:AfterWBWIIngestBeforeSetLastSequence:resume");
versions_->SetLastSequence(last_sequence);
}
}
@@ -3103,6 +3108,7 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context,
const WriteOptions write_options;
log::Writer* new_log = nullptr;
UnpublishedWAL prepared_wal;
MemTable* new_mem = nullptr;
IOStatus io_s;
@@ -3130,8 +3136,16 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context,
!wal_recycle_files_.empty() && IsFileDeletionsEnabled()) {
recycle_log_number = wal_recycle_files_.front();
}
uint64_t new_log_number =
creating_new_log ? versions_->NewFileNumber() : cur_wal_number_;
uint64_t new_log_number = cur_wal_number_;
if (creating_new_log) {
prepared_wal = WaitForAsyncWALPrecreate();
if (prepared_wal.writer) {
new_log_number = prepared_wal.log_number;
} else {
assert(prepared_wal.log_number == 0);
new_log_number = versions_->NewFileNumber();
}
}
// For use outside of holding DB mutex
const MutableCFOptions mutable_cf_options_copy =
cfd->GetLatestMutableCFOptions();
@@ -3152,6 +3166,8 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context,
// Log this later after lock release. It may be outdated, e.g., if background
// flush happens before logging, but that should be ok.
int num_imm_unflushed = cfd->imm()->NumNotFlushed();
const DBOptions db_options_snapshot =
BuildDBOptions(immutable_db_options_, mutable_db_options_);
const auto preallocate_block_size =
GetWalPreallocateBlockSize(mutable_cf_options_copy.write_buffer_size);
mutex_.Unlock();
@@ -3167,13 +3183,35 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context,
wal_write_mutex_.Unlock();
// TODO: Write buffer size passed in should be max of all CF's instead
// of mutable_cf_options.write_buffer_size.
io_s = CreateWAL(write_options, new_log_number, recycle_log_number,
preallocate_block_size, info, &new_log);
if (prepared_wal.writer) {
io_s = StartWALFile(write_options, info, prepared_wal.writer.get());
if (io_s.ok()) {
new_log = prepared_wal.writer.release();
}
} else {
io_s =
CreateWAL(db_options_snapshot, write_options, new_log_number,
recycle_log_number, preallocate_block_size, info, &new_log);
}
TEST_SYNC_POINT_CALLBACK("DBImpl::SwitchMemtable:AfterCreateWAL", &io_s);
if (s.ok()) {
s = io_s;
}
}
if (!s.ok() && prepared_wal.writer) {
const uint64_t prepared_wal_number = prepared_wal.log_number;
const std::string prepared_wal_fname =
LogFileName(immutable_db_options_.GetWalDir(), prepared_wal_number);
prepared_wal.Reset();
Status delete_s = env_->DeleteFile(prepared_wal_fname);
if (!delete_s.ok() && !delete_s.IsNotFound()) {
ROCKS_LOG_WARN(immutable_db_options_.info_log,
"Failed to delete unstarted async precreated WAL #%" PRIu64
": %s",
prepared_wal_number, delete_s.ToString().c_str());
}
delete_s.PermitUncheckedError();
}
if (s.ok()) {
// FIXME: from the comment for GetEarliestSequenceNumber(), any key with
// seqno >= earliest_seqno should be in this or later memtable. This means
@@ -3355,6 +3393,7 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context,
new_mem->Ref();
cfd->SetMemtable(new_mem);
InstallSuperVersionAndScheduleWork(cfd, &context->superversion_context);
MaybeScheduleAsyncWALPrecreate(preallocate_block_size);
// Notify client that memtable is sealed, now that we have successfully
// installed a new memtable
+7 -5
View File
@@ -623,7 +623,7 @@ bool DBIter::FindNextUserEntryInternal(bool skipping_saved_key) {
PERF_COUNTER_ADD(internal_delete_skipped_count, 1);
MarkMemtableForFlushForPerOpTrigger(mem_hidden_op_scanned);
// Track contiguous tombstones for range conversion.
// Skip if outside seek prefix — the top-of-loop check
// Skip if outside seek prefix -- the top-of-loop check
// flushed any pending run, but we must also avoid starting
// a new run outside the prefix.
if (min_tombstones_for_range_conversion_ > 0 &&
@@ -1097,7 +1097,7 @@ void DBIter::PrevInternal() {
if (min_tombstones_for_range_conversion_ > 0 &&
range_tomb_end_key_.Size() > 0 && timestamp_lb_ == nullptr) {
if (!valid_ && found_visible && PrefixCheck(saved_key_without_ts)) {
// Key was deleted and is within the seek prefix — track it.
// Key was deleted and is within the seek prefix -- track it.
TrackContiguousTombstone(saved_key_.GetUserKey(),
/*always_update_first_key=*/true);
} else if (valid_) {
@@ -1141,7 +1141,7 @@ void DBIter::PrevInternal() {
// Sets ikey_ to the last visible entry's internal key. When found_visible
// is false, ikey_ is not updated and may contain stale data.
// Sets found_visible to true if at least one entry passed the IsVisible()
// check (seqno <= snapshot). When false, no entry was visible — the key
// check (seqno <= snapshot). When false, no entry was visible -- the key
// does not exist at this snapshot and should not be treated as a tombstone.
// Returns false if an error occurred, and !status().ok() and !valid_.
//
@@ -1196,7 +1196,7 @@ bool DBIter::FindValueForCurrentKey(bool& found_visible) {
break;
}
// Entry survived the visibility check — at least one visible version
// Entry survived the visibility check -- at least one visible version
// exists for this user key.
found_visible = true;
@@ -1480,7 +1480,9 @@ bool DBIter::FindValueForCurrentKeyUsingSeek() {
if (timestamp_lb_ != nullptr) {
saved_key_.SetInternalKey(ikey);
} else {
saved_key_.SetUserKey(ikey.user_key);
saved_key_.SetUserKey(
ikey.user_key,
!pin_thru_lifetime_ || !iter_.iter()->IsKeyPinned() /* copy */);
}
valid_ = true;
+1 -1
View File
@@ -589,7 +589,7 @@ class DBIter final : public Iterator {
// If enough contiguous tombstones have been tracked, insert a range
// tombstone [first_key, end_key) into the mutable memtable.
// end_key is the exclusive upper bound — typically the next live key.
// end_key is the exclusive upper bound -- typically the next live key.
void MaybeInsertRangeTombstone(const Slice& end_key);
void ResetContiguousTombstoneTracking() {
contiguous_tombstone_count_ = 0;
+54
View File
@@ -216,12 +216,19 @@ class TestIterator : public InternalIterator {
bool IsKeyPinned() const override { return true; }
bool IsValuePinned() const override { return true; }
void Prepare(const MultiScanArgs* /*scan_opts*/) override {
++prepare_call_count_;
}
size_t prepare_call_count() const { return prepare_call_count_; }
private:
bool initialized_;
bool valid_;
size_t sequence_number_;
size_t iter_;
size_t steps_ = 0;
size_t prepare_call_count_ = 0;
InternalKeyComparator cmp;
std::vector<std::pair<std::string, std::string>> data_;
@@ -243,6 +250,53 @@ class DBIteratorTest : public testing::Test {
DBIteratorTest() : env_(Env::Default()) {}
};
TEST_F(DBIteratorTest, PrepareForwardsValidatedScanRanges) {
Options options;
ImmutableOptions ioptions = ImmutableOptions(options);
MutableCFOptions mutable_cf_options = MutableCFOptions(options);
TestIterator* internal_iter = new TestIterator(BytewiseComparator());
internal_iter->AddPut("a", "val_a");
internal_iter->AddPut("b", "val_b");
internal_iter->Finish();
ReadOptions ro;
std::unique_ptr<Iterator> db_iter(DBIter::NewIter(
env_, ro, ioptions, mutable_cf_options, BytewiseComparator(),
internal_iter, nullptr /* version */, 10 /* sequence */,
nullptr /* read_callback */, /*active_mem=*/nullptr));
MultiScanArgs scan_opts(BytewiseComparator());
scan_opts.insert("a", "c");
db_iter->Prepare(scan_opts);
ASSERT_OK(db_iter->status());
EXPECT_EQ(internal_iter->prepare_call_count(), 1);
}
TEST_F(DBIteratorTest, PrepareDoesNotForwardInvalidScanRanges) {
Options options;
ImmutableOptions ioptions = ImmutableOptions(options);
MutableCFOptions mutable_cf_options = MutableCFOptions(options);
TestIterator* internal_iter = new TestIterator(BytewiseComparator());
internal_iter->AddPut("a", "val_a");
internal_iter->AddPut("b", "val_b");
internal_iter->Finish();
ReadOptions ro;
std::unique_ptr<Iterator> db_iter(DBIter::NewIter(
env_, ro, ioptions, mutable_cf_options, BytewiseComparator(),
internal_iter, nullptr /* version */, 10 /* sequence */,
nullptr /* read_callback */, /*active_mem=*/nullptr));
MultiScanArgs scan_opts(BytewiseComparator());
scan_opts.insert("b", "d");
scan_opts.insert("a", "c");
db_iter->Prepare(scan_opts);
EXPECT_TRUE(db_iter->status().IsInvalidArgument());
EXPECT_EQ(internal_iter->prepare_call_count(), 0);
}
TEST_F(DBIteratorTest, DBIteratorPrevNext) {
Options options;
ImmutableOptions ioptions = ImmutableOptions(options);
+133 -22
View File
@@ -4151,7 +4151,7 @@ TEST_P(DBIteratorTest, PrefixSameAsStartSeekToNonInDomainKey) {
ASSERT_OK(Put("abc2", "v2"));
ASSERT_OK(Put("abc3", "v3"));
// Seek to "ab" (2 bytes) — out-of-domain for FixedPrefixTransform(3).
// Seek to "ab" (2 bytes) -- out-of-domain for FixedPrefixTransform(3).
// ShouldSetPrefix returns false for out-of-domain targets, so no prefix
// constraint is set. The seek should find "abc1" and iteration should
// proceed without prefix_same_as_start enforcement.
@@ -4395,6 +4395,71 @@ TEST_P(DBMultiScanIteratorTest, RangeAcrossFiles) {
iter.reset();
}
TEST_P(DBMultiScanIteratorTest, SortedRangesSkipIODispatcherSort) {
auto options = CurrentOptions();
options.target_file_size_base = 100 << 10;
options.compaction_style = kCompactionStyleUniversal;
options.num_levels = 50;
options.compression = kNoCompression;
DestroyAndReopen(options);
auto key = [](int i) {
std::stringstream ss;
ss << "k" << std::setw(5) << std::setfill('0') << i;
return ss.str();
};
auto rnd = Random::GetTLSInstance();
for (int i = 0; i < 100; ++i) {
ASSERT_OK(Put(key(i), rnd->RandomString(2 << 10)));
}
ASSERT_OK(Flush());
ASSERT_OK(db_->CompactRange({}, nullptr, nullptr));
ASSERT_EQ(2, NumTableFilesAtLevel(49));
int sort_count = 0;
SyncPoint::GetInstance()->SetCallBack(
"IODispatcherImpl::SubmitJob:SortBlockHandles",
[&](void* /*arg*/) { ++sort_count; });
SyncPoint::GetInstance()->EnableProcessing();
auto tracking_dispatcher = std::make_shared<TrackingIODispatcher>();
std::vector<std::string> key_ranges({key(10), key(90)});
MultiScanArgs scan_options(BytewiseComparator());
scan_options.io_dispatcher = tracking_dispatcher;
scan_options.use_async_io = false;
scan_options.insert(key_ranges[0], key_ranges[1]);
ReadOptions ro;
ro.fill_cache = GetParam();
ColumnFamilyHandle* cfh = dbfull()->DefaultColumnFamily();
std::unique_ptr<MultiScan> iter =
dbfull()->NewMultiScan(ro, cfh, scan_options);
try {
int i = 10;
for (auto range : *iter) {
for (auto it : range) {
ASSERT_EQ(it.first.ToString(), key(i));
++i;
}
}
ASSERT_EQ(i, 90);
} catch (MultiScanException& ex) {
ASSERT_NOK(ex.status());
std::cerr << "Iterator returned status " << ex.what();
abort();
} catch (std::logic_error& ex) {
std::cerr << "Iterator returned logic error " << ex.what();
abort();
}
ASSERT_GT(tracking_dispatcher->GetReadSets().size(), 0);
EXPECT_EQ(sort_count, 0);
iter.reset();
}
TEST_P(DBMultiScanIteratorTest, FailureTest) {
auto options = CurrentOptions();
options.compression = kNoCompression;
@@ -5820,13 +5885,13 @@ TEST_P(ReadPathRangeTombstoneTest, ExhaustedIteratorWithBounds) {
// Both directions encounter two tombstone runs (a-d and g-j).
ASSERT_EQ(attempted_insert_ranges_.size(), 2);
if (Forward()) {
// Forward: sees a-d tombstones first → live e terminates → [a, e).
// Then g-j → exhaustion past j, saved_key_="j" fallback → [g, j),
// Forward: sees a-d tombstones first -> live e terminates -> [a, e).
// Then g-j -> exhaustion past j, saved_key_="j" fallback -> [g, j),
// covering g,h,i. j remains a point tombstone.
AssertRange(0, "a", "e");
AssertRange(1, "g", "j");
} else {
// Reverse: sees j-g tombstones first, then f,e live, then d-a → exhausts.
// Reverse: sees j-g tombstones first, then f,e live, then d-a -> exhausts.
AssertRange(0, "g", "z");
AssertRange(1, "a", "e");
}
@@ -5863,8 +5928,8 @@ TEST_P(ReadPathRangeTombstoneTest, ExhaustedIteratorNoBounds) {
VerifyIteration({"e", "f"});
if (Forward()) {
// Forward: a-d run terminates at live e → [a, e). g-j run exhausts
// past j → saved_key_ fallback → [g, j), covering g,h,i with j as a
// Forward: a-d run terminates at live e -> [a, e). g-j run exhausts
// past j -> saved_key_ fallback -> [g, j), covering g,h,i with j as a
// point tombstone.
ASSERT_EQ(attempted_insert_ranges_.size(), 2);
AssertRange(0, "a", "e");
@@ -6061,7 +6126,7 @@ TEST_P(ReadPathRangeTombstoneTest, PrefixFilterDefaultReadOptions) {
}
TEST_P(ReadPathRangeTombstoneTest, PrefixFilterTotalOrderSeek) {
// total_order_seek=true disables prefix filtering — all files visible.
// total_order_seek=true disables prefix filtering -- all files visible.
// The delete run spans from prefix 'b' into prefix 'c', terminated by
// live key "cb" from L0. The tombstone [ba, cb) crosses prefix boundaries,
// which is safe because total_order_seek makes all files visible.
@@ -6103,7 +6168,7 @@ TEST_P(ReadPathRangeTombstoneTest, PrefixFilterTotalOrderSeek) {
ASSERT_OK(Flush());
// Memtable: contiguous deletes spanning prefixes 'b' and 'c'.
// No live key between "bb" and "cb" — the run crosses prefix boundaries.
// No live key between "bb" and "cb" -- the run crosses prefix boundaries.
ASSERT_OK(put("aa", "below"));
ASSERT_OK(del("ba"));
ASSERT_OK(del("bb"));
@@ -6311,7 +6376,7 @@ TEST_P(ReadPathRangeTombstoneTest, PrefixFilterOutOfDomainSeek) {
ro.prefix_same_as_start = true;
auto it = std::unique_ptr<Iterator>(db_->NewIterator(ro));
if (Forward()) {
// Seek with a 1-byte key — out-of-domain for FixedPrefixTransform(4).
// Seek with a 1-byte key -- out-of-domain for FixedPrefixTransform(4).
// prefix_same_as_start cannot set a prefix bound for this target.
it->Seek("b");
while (it->Valid()) {
@@ -6674,7 +6739,7 @@ TEST_P(ReadPathRangeTombstoneTest, ExhaustedWithUDT) {
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
if (Forward()) {
// Forward exhaustion past h: saved_key_="h"+ts fallback → [e+ts, h+ts).
// Forward exhaustion past h: saved_key_="h"+ts fallback -> [e+ts, h+ts).
AssertRange(0, std::string("e") + ts, std::string("h") + ts);
} else {
// Reverse exhaustion: range is [a+ts, e+ts).
@@ -6687,9 +6752,9 @@ TEST_P(ReadPathRangeTombstoneTest, ExhaustedWithUDT) {
TEST_P(ReadPathRangeTombstoneTest, SeekForPrevTombstone) {
// SeekForPrev lands directly on tombstones. No upper bound. Forward
// exhaustion past h falls back to saved_key_="h" → [e, h), covering
// exhaustion past h falls back to saved_key_="h" -> [e, h), covering
// e,f,g with h as a point tombstone.
// Reverse: SeekForPrev("h") → Delete(h,g,f,e) → live "d" → range [e, h).
// Reverse: SeekForPrev("h") -> Delete(h,g,f,e) -> live "d" -> range [e, h).
for (bool use_udt : {false, true}) {
SCOPED_TRACE(use_udt ? "with UDT" : "without UDT");
Options options = CurrentOptions();
@@ -6727,7 +6792,7 @@ TEST_P(ReadPathRangeTombstoneTest, SeekForPrevTombstone) {
keys.push_back(iter->key().ToString());
}
} else {
// SeekForPrev("h") lands on Delete(h), traverses g,f,e → finds "d".
// SeekForPrev("h") lands on Delete(h), traverses g,f,e -> finds "d".
for (iter->SeekForPrev("h"); iter->Valid(); iter->Prev()) {
keys.push_back(iter->key().ToString());
}
@@ -6738,7 +6803,7 @@ TEST_P(ReadPathRangeTombstoneTest, SeekForPrevTombstone) {
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
if (Forward()) {
// Forward exhausts past h → saved_key_="h" fallback → [e, h),
// Forward exhausts past h -> saved_key_="h" fallback -> [e, h),
// covering e,f,g with h as a point tombstone.
if (use_udt) {
AssertRange(0, std::string("e") + ts, std::string("h") + ts);
@@ -6761,11 +6826,11 @@ TEST_P(ReadPathRangeTombstoneTest, SeekForPrevTombstone) {
TEST_P(ReadPathRangeTombstoneTest, UpperBoundTombstone) {
// iterate_upper_bound lands past the data. Both directions see tombstones
// e-h. Forward exhausts naturally past h (no key in DB ≥ upper) so the
// e-h. Forward exhausts naturally past h (no key in DB >= upper) so the
// forward path falls back to saved_key_ ("h") as the exclusive end_key,
// covering n-1 of n deletes — [e, h). The remaining tombstone for h
// covering n-1 of n deletes -- [e, h). The remaining tombstone for h
// stays as a point delete. Reverse captures the live key "d" before the
// run and uses it via range_tomb_end_key_ — [e, i) when SeekToLast
// run and uses it via range_tomb_end_key_ -- [e, i) when SeekToLast
// delegates to SeekForPrev("i").
for (bool use_udt : {false, true}) {
SCOPED_TRACE(use_udt ? "with UDT" : "without UDT");
@@ -6837,8 +6902,8 @@ TEST_P(ReadPathRangeTombstoneTest, UpperBoundTombstone) {
TEST_P(ReadPathRangeTombstoneTest, LowerBoundTruncatesReverse) {
// Keys a-j, delete a-h. Lower bound "e" truncates reverse iteration
// mid-tombstone-run. Forward: tombstones e-h ended by live key i → [e, i).
// Reverse: tombstones h,g,f,e then lower_bound hit → flush [e, i).
// mid-tombstone-run. Forward: tombstones e-h ended by live key i -> [e, i).
// Reverse: tombstones h,g,f,e then lower_bound hit -> flush [e, i).
for (bool use_udt : {false, true}) {
SCOPED_TRACE(use_udt ? "with UDT" : "without UDT");
Options options = CurrentOptions();
@@ -6975,14 +7040,14 @@ TEST_P(ReadPathRangeTombstoneTest, InvisibleKeysDontBreakTombstoneRun) {
ASSERT_OK(Put("f", "vf"));
ASSERT_OK(Flush());
// Delete b, d — visible tombstones.
// Delete b, d -- visible tombstones.
ASSERT_OK(Delete("b"));
ASSERT_OK(Delete("d"));
// Snapshot S. At S: a(live), b(del), d(del), f(live).
const Snapshot* snap = db_->GetSnapshot();
// Write c, e AFTER the snapshot — invisible at S.
// Write c, e AFTER the snapshot -- invisible at S.
// At S the iterator sees: a(live), b(del), c(invis), d(del),
// e(invis), f(live).
ASSERT_OK(Put("c", "vc"));
@@ -6994,7 +7059,7 @@ TEST_P(ReadPathRangeTombstoneTest, InvisibleKeysDontBreakTombstoneRun) {
VerifyIteration({"a", "f"}, ro);
// Invisible keys c and e should not break the tombstone run.
// 2 tombstones (b, d) ≥ threshold → range [b, f).
// 2 tombstones (b, d) >= threshold -> range [b, f).
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
AssertRange(0, "b", "f");
@@ -7181,6 +7246,52 @@ TEST_P(ReadPathRangeTombstoneTest,
db_->ReleaseSnapshot(snap);
}
// SeekForPrev uses the reverse path: PrevInternal -> FindValueForCurrentKey.
// When a key has more versions than max_sequential_skip_in_iterations,
// FindValueForCurrentKey delegates to FindValueForCurrentKeyUsingSeek which
// re-seeks forward to the newest visible version. That function must preserve
// key pinning by passing the correct `copy` parameter to
// saved_key_.SetUserKey().
TEST_P(DBIteratorTest, SeekForPrevKeyPinnedWithManyVersions) {
Options options = CurrentOptions();
options.max_sequential_skip_in_iterations = 8;
options.statistics = CreateDBStatistics();
DestroyAndReopen(options);
// 20 versions > max_sequential_skip (8), forcing the reseek path.
// No flush: keeps all versions alive in the memtable.
for (int i = 0; i < 20; i++) {
ASSERT_OK(Put("key1", "value" + std::to_string(i)));
}
ReadOptions ro;
ro.pin_data = true;
std::unique_ptr<Iterator> iter(NewIterator(ro));
std::string prop_value;
// Seek (forward) correctly reports key as pinned.
iter->Seek("key1");
ASSERT_TRUE(iter->Valid());
ASSERT_OK(iter->GetProperty("rocksdb.iterator.is-key-pinned", &prop_value));
ASSERT_EQ("1", prop_value);
uint64_t reseeks_before =
options.statistics->getTickerCount(NUMBER_OF_RESEEKS_IN_ITERATION);
iter->SeekForPrev("key1");
ASSERT_TRUE(iter->Valid());
ASSERT_EQ("key1", iter->key().ToString());
// Confirm FindValueForCurrentKeyUsingSeek was actually taken.
uint64_t reseeks_after =
options.statistics->getTickerCount(NUMBER_OF_RESEEKS_IN_ITERATION);
ASSERT_GT(reseeks_after, reseeks_before);
ASSERT_OK(iter->GetProperty("rocksdb.iterator.is-key-pinned", &prop_value));
ASSERT_EQ("1", prop_value);
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+34
View File
@@ -102,6 +102,40 @@ TEST_F(DBOptionsTest, ImmutableTrackAndVerifyWalsInManifest) {
ASSERT_FALSE(s.ok());
}
TEST_F(DBOptionsTest, ImmutableAsyncWalPrecreate) {
Options options;
options.env = env_;
options.async_wal_precreate = true;
ImmutableDBOptions db_options(options);
ASSERT_TRUE(db_options.async_wal_precreate);
Reopen(options);
ASSERT_TRUE(dbfull()->GetDBOptions().async_wal_precreate);
Status s = dbfull()->SetDBOptions({{"async_wal_precreate", "false"}});
ASSERT_FALSE(s.ok());
}
TEST_F(DBOptionsTest, SanitizeAsyncWalPrecreateWithWalRecycle) {
// This checks the option-level contract for the incompatible WAL paths:
// SanitizeOptions() preserves WAL recycling and disables only the
// opportunistic async precreation path, so the immutable options reflect
// effective behavior.
Options options;
options.env = env_;
options.async_wal_precreate = true;
options.recycle_log_file_num = 1;
Options sanitized_options = SanitizeOptions(dbname_, options);
ASSERT_FALSE(sanitized_options.async_wal_precreate);
ASSERT_EQ(1U, sanitized_options.recycle_log_file_num);
options.recycle_log_file_num = 0;
sanitized_options = SanitizeOptions(dbname_, options);
ASSERT_TRUE(sanitized_options.async_wal_precreate);
}
TEST_F(DBOptionsTest, ImmutableVerifySstUniqueIdInManifest) {
Options options;
options.env = env_;
+238 -2
View File
@@ -7,9 +7,13 @@
// 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 "db/blob/blob_index.h"
#include "db/db_impl/db_impl_secondary.h"
#include "db/db_test_util.h"
#include "db/db_with_timestamp_test_util.h"
#include "db/wide/wide_column_test_util.h"
#include "db/write_batch_internal.h"
#include "file/filename.h"
#include "port/stack_trace.h"
#include "rocksdb/utilities/transaction_db.h"
#include "test_util/sync_point.h"
@@ -357,8 +361,7 @@ TEST_F(DBSecondaryTest, GetMergeOperands) {
const Status s = db_secondary_->GetMergeOperands(
ReadOptions(), cfh, "k1", values.data(), &merge_operands_info,
&number_of_operands);
ASSERT_NOK(s);
ASSERT_TRUE(s.IsMergeInProgress());
ASSERT_OK(s);
ASSERT_EQ(number_of_operands, 4);
ASSERT_EQ(values[0].ToString(), "v1");
@@ -367,6 +370,197 @@ TEST_F(DBSecondaryTest, GetMergeOperands) {
ASSERT_EQ(values[3].ToString(), "v4");
}
TEST_F(DBSecondaryTest, GetMergeOperandsWithBlobBackedEntityDefaultColumn) {
// Goal: exercise the secondary read path with a blob-backed V2 entity base
// in SST and a newer merge operand applied through catch-up. The secondary
// DB must resolve the blob-backed default column for both Get() and
// GetMergeOperands() while combining the newer memtable merge with the older
// SST-backed base entity.
Options options = GetDefaultOptions();
options.create_if_missing = true;
options.enable_blob_files = true;
options.min_blob_size = 50;
options.disable_auto_compactions = true;
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
options.env = env_;
Reopen(options);
const std::string key = "secondary_blob_entity";
const std::string default_value(100, 'd');
const std::string large_value(120, 'l');
const std::string merge_operand = "suffix";
const std::string expected_merged = default_value + "|" + merge_operand;
WideColumns columns{{kDefaultWideColumnName, default_value},
{"col_large", large_value},
{"meta", "inline"}};
ASSERT_OK(
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
ASSERT_OK(Flush());
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
options.max_open_files = -1;
OpenSecondary(options);
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), key,
merge_operand));
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
auto* cfh = db_secondary_->DefaultColumnFamily();
{
PinnableSlice result;
ASSERT_OK(db_secondary_->Get(ReadOptions(), cfh, key, &result));
ASSERT_EQ(result, expected_merged);
}
{
GetMergeOperandsOptions get_merge_opts;
get_merge_opts.expected_max_number_of_operands = 2;
std::array<PinnableSlice, 2> merge_operands;
int number_of_operands = 0;
ASSERT_OK(db_secondary_->GetMergeOperands(
ReadOptions(), cfh, key, merge_operands.data(), &get_merge_opts,
&number_of_operands));
ASSERT_EQ(number_of_operands, 2);
ASSERT_EQ(merge_operands[0], default_value);
ASSERT_EQ(merge_operands[1], merge_operand);
}
}
TEST_F(DBSecondaryTest, GetImplReturnsBlobIndexWhenRequested) {
// Goal: cover the internal secondary GetImpl contract when the caller
// explicitly asks for raw blob-index bytes via `is_blob_index`. Catch-up
// rebuilds the blob index into the secondary memtable, and this path must
// preserve the encoded index instead of eagerly resolving or rejecting it.
Options options = GetDefaultOptions();
options.create_if_missing = true;
options.env = env_;
Reopen(options);
options.max_open_files = -1;
OpenSecondary(options);
std::string blob_index;
BlobIndex::EncodeInlinedTTL(&blob_index, /*expiration=*/9876543210, "blob");
WriteBatch batch;
ASSERT_OK(WriteBatchInternal::PutBlobIndex(
&batch, db_->DefaultColumnFamily()->GetID(), "blob_key", blob_index));
ASSERT_OK(db_->Write(WriteOptions(), &batch));
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
PinnableSlice value;
bool is_blob_index = false;
DBImpl::GetImplOptions get_impl_options;
get_impl_options.column_family = db_secondary_->DefaultColumnFamily();
get_impl_options.value = &value;
get_impl_options.is_blob_index = &is_blob_index;
ASSERT_OK(db_secondary_full()->GetImpl(ReadOptions(), Slice("blob_key"),
get_impl_options));
ASSERT_TRUE(is_blob_index);
ASSERT_EQ(value, blob_index);
}
TEST_F(DBSecondaryTest,
GetAndGetEntityWithBlobBackedDefaultColumnDirectWriteMemtable) {
// Goal: cover the secondary memtable path after catch-up replays a blob
// direct-write entity from the primary WAL. The test checks both `Get()`,
// which must resolve the blob-backed default column, and `GetEntity()`,
// which must eagerly resolve all unresolved blob columns instead of exposing
// encoded blob indices to the caller.
Options options =
wide_column_test_util::GetDirectWriteOptions(GetDefaultOptions());
options.create_if_missing = true;
options.min_blob_size = 50;
options.env = env_;
Reopen(options);
options.max_open_files = -1;
OpenSecondary(options);
const std::string key = "secondary_direct_write_memtable_entity";
const std::string default_value =
wide_column_test_util::GenerateLargeValue(100, 'd');
const std::string large_value =
wide_column_test_util::GenerateLargeValue(120, 'l');
const std::string small_value = wide_column_test_util::GenerateSmallValue();
WideColumns columns{{kDefaultWideColumnName, default_value},
{"col_large", large_value},
{"col_small", small_value}};
ASSERT_OK(
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
auto* cfh = db_secondary_->DefaultColumnFamily();
{
PinnableSlice result;
ASSERT_OK(db_secondary_->Get(ReadOptions(), cfh, key, &result));
ASSERT_EQ(result, default_value);
}
{
PinnableWideColumns result;
ASSERT_OK(db_secondary_->GetEntity(ReadOptions(), cfh, key, &result));
ASSERT_EQ(result.columns(), columns);
}
}
TEST_F(DBSecondaryTest, SecondaryDirectWriteMemtableBlobBlockCacheTier) {
// Goal: cover the secondary memtable path under kBlockCacheTier. Catch-up
// rebuilds the blob-backed direct-write entity from WAL, so serving Get() or
// GetEntity() would require blob I/O through the memtable resolution path,
// which must return Incomplete instead of issuing that I/O.
Options options =
wide_column_test_util::GetDirectWriteOptions(GetDefaultOptions());
options.create_if_missing = true;
options.min_blob_size = 50;
options.env = env_;
Reopen(options);
options.max_open_files = -1;
OpenSecondary(options);
const std::string key = "secondary_direct_write_memtable_block_cache_tier";
const std::string default_value =
wide_column_test_util::GenerateLargeValue(100, 'd');
const std::string large_value =
wide_column_test_util::GenerateLargeValue(120, 'l');
const std::string small_value = wide_column_test_util::GenerateSmallValue();
WideColumns columns{{kDefaultWideColumnName, default_value},
{"col_large", large_value},
{"col_small", small_value}};
ASSERT_OK(
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
auto* cfh = db_secondary_->DefaultColumnFamily();
ReadOptions read_opts;
read_opts.read_tier = kBlockCacheTier;
{
PinnableSlice result;
const Status s = db_secondary_->Get(read_opts, cfh, key, &result);
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
ASSERT_TRUE(result.empty());
}
{
PinnableWideColumns result;
const Status s = db_secondary_->GetEntity(read_opts, cfh, key, &result);
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
ASSERT_TRUE(result.columns().empty());
}
}
TEST_F(DBSecondaryTest, InternalCompactionCompactedFiles) {
Options options;
options.env = env_;
@@ -729,6 +923,48 @@ TEST_F(DBSecondaryTest, OpenAsSecondaryWALTailing) {
verify_db_func("new_foo_value_1", "new_bar_value");
}
TEST_F(DBSecondaryTest, CatchUpTailsCurrentWalWhenFutureWalExists) {
// Goal: secondary catch-up must keep tailing the current WAL even when a
// higher-number empty WAL is present. This reproduces the async WAL
// precreation shape by creating a future WAL before opening the secondary,
// then appending another write to the current WAL and catching up again.
Options options;
options.env = env_;
Reopen(options);
ASSERT_OK(Put("foo", "value0"));
ASSERT_OK(Flush());
ASSERT_OK(Put("foo", "value1"));
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
std::unique_ptr<WalFile> current_wal;
ASSERT_OK(db_->GetCurrentWalFile(&current_wal));
ASSERT_NE(nullptr, current_wal);
const uint64_t future_wal_number = current_wal->LogNumber() + 1000;
std::unique_ptr<WritableFile> future_wal_file;
ASSERT_OK(env_->NewWritableFile(LogFileName(dbname_, future_wal_number),
&future_wal_file, EnvOptions()));
ASSERT_OK(future_wal_file->Close());
Options secondary_options;
secondary_options.env = env_;
secondary_options.max_open_files = -1;
OpenSecondary(secondary_options);
ReadOptions read_options;
std::string value;
ASSERT_OK(db_secondary_->Get(read_options, "foo", &value));
ASSERT_EQ("value1", value);
ASSERT_OK(Put("foo", "value2"));
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
ASSERT_OK(db_secondary_->Get(read_options, "foo", &value));
ASSERT_EQ("value2", value);
}
TEST_F(DBSecondaryTest, SecondaryTailingBug_ISSUE_8467) {
Options options;
options.env = env_;
+127
View File
@@ -8071,6 +8071,41 @@ INSTANTIATE_TEST_CASE_P(OpenFilesAsync, OpenFilesAsyncTest,
::testing::Values(-1, 10),
::testing::Bool()));
TEST_F(DBTest, ReadOnlyCloseDoesNotDeleteWriterFiles) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.disable_auto_compactions = true;
DestroyAndReopen(options);
ASSERT_OK(Put("before", "value"));
ASSERT_OK(Flush());
std::unique_ptr<DB> read_only_db;
ASSERT_OK(DB::OpenForReadOnly(options, dbname_, &read_only_db));
ASSERT_OK(Put("after", "value"));
ASSERT_OK(Flush());
std::vector<LiveFileMetaData> live_files;
db_->GetLiveFilesMetaData(&live_files);
ASSERT_GE(live_files.size(), 2);
std::string newest_sst_path;
uint64_t newest_file_number = 0;
for (const auto& live_file : live_files) {
if (live_file.file_number > newest_file_number) {
newest_file_number = live_file.file_number;
newest_sst_path = live_file.directory + "/" + live_file.relative_filename;
}
}
ASSERT_FALSE(newest_sst_path.empty());
ASSERT_OK(env_->FileExists(newest_sst_path));
read_only_db.reset();
ASSERT_OK(env_->FileExists(newest_sst_path));
}
// Test mix of races with async file open, reads, compactions
TEST_P(OpenFilesAsyncTest, ConcurrentFileAccess) {
Options options = CurrentOptions();
@@ -8203,6 +8238,98 @@ TEST_P(OpenFilesAsyncTest, BeforeRead) {
}
}
// For modern DBs (manifest carries file_creation_time),
// GetCreationTimeOfOldestFile returns the real value without waiting for async
// file open.
TEST_P(OpenFilesAsyncTest, GetCreationTimeOfOldestFileSkipsWaitForModernDB) {
if (max_open_files_ != -1) {
return;
}
Options options = CurrentOptions();
ASSERT_NO_FATAL_FAILURE(SetupData(options));
// If WaitForAsyncFileOpen is entered, the test fails -- modern DBs must
// never need the wait.
std::atomic<bool> waited{false};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"DBImpl::WaitForAsyncFileOpen::BeforeWait",
[&](void* /*arg*/) { waited.store(true); });
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
OpenTestDB(options);
uint64_t creation_time = std::numeric_limits<uint64_t>::max();
ASSERT_OK(dbfull()->GetCreationTimeOfOldestFile(&creation_time));
EXPECT_GT(creation_time, 0);
EXPECT_LT(creation_time, std::numeric_limits<uint64_t>::max());
EXPECT_FALSE(waited.load());
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
Close();
}
// For legacy DBs (manifest lacks file_creation_time, value comes from the
// pinned reader), GetCreationTimeOfOldestFile must block until
// BGWorkAsyncFileOpen pins the readers; otherwise it returns 0 (the "info
// unavailable" sentinel) instead of the real value.
TEST_P(OpenFilesAsyncTest,
GetCreationTimeOfOldestFileBlocksOnAsyncOpenForLegacyDB) {
if (max_open_files_ != -1) {
return;
}
Options options = CurrentOptions();
ASSERT_NO_FATAL_FAILURE(SetupData(options));
// Simulate a legacy DB: force TryGetFileCreationTime to return
// kUnknownFileCreationTime until BGWorkAsyncFileOpen completes (i.e., what
// would happen if the manifest had no file_creation_time and the pinned
// reader was the only source).
std::atomic<bool> async_open_done{false};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"DBImpl::BGWorkAsyncFileOpen:Done",
[&](void* /*arg*/) { async_open_done.store(true); });
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"Version::GetCreationTimeOfOldestFile::FileCreationTime", [&](void* arg) {
if (!async_open_done.load()) {
*static_cast<uint64_t*>(arg) = kUnknownFileCreationTime;
}
});
// Dependency chain: caller's first pass returns 0, enters
// WaitForAsyncFileOpen -> main thread confirms wait -> main thread releases
// async open -> background worker completes -> caller wakes -> second pass
// sees the real value.
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
{{"DBImpl::WaitForAsyncFileOpen::BeforeWait",
"OpenFilesAsyncTest::CreationTime::CallerBlocked"},
{"OpenFilesAsyncTest::CreationTime::CallerBlocked",
"OpenFilesAsyncTest::CreationTime::ReleaseAsyncOpen"},
{"OpenFilesAsyncTest::CreationTime::ReleaseAsyncOpen",
"DBImpl::BGWorkAsyncFileOpen::Start"}});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
OpenTestDB(options);
uint64_t creation_time = std::numeric_limits<uint64_t>::max();
port::Thread caller([&]() {
ASSERT_OK(dbfull()->GetCreationTimeOfOldestFile(&creation_time));
});
TEST_SYNC_POINT("OpenFilesAsyncTest::CreationTime::CallerBlocked");
TEST_SYNC_POINT("OpenFilesAsyncTest::CreationTime::ReleaseAsyncOpen");
caller.join();
EXPECT_GT(creation_time, 0);
EXPECT_LT(creation_time, std::numeric_limits<uint64_t>::max());
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
Close();
}
TEST_P(OpenFilesAsyncTest, Shutdown) {
Options options = CurrentOptions();
ASSERT_NO_FATAL_FAILURE(SetupData(options));
+5
View File
@@ -13,6 +13,7 @@
#include "db/forward_iterator.h"
#include "env/fs_readonly.h"
#include "env/mock_env.h"
#include "file/file_util.h"
#include "port/lang.h"
#include "rocksdb/cache.h"
#include "rocksdb/convenience.h"
@@ -90,6 +91,8 @@ DBTestBase::DBTestBase(const std::string path, bool env_do_fsync)
dbname_ = test::PerThreadDBPath(env_, path);
alternative_wal_dir_ = dbname_ + "/wal";
alternative_db_log_dir_ = dbname_ + "/db_log_dir";
EXPECT_OK(DestroyDir(env_, alternative_wal_dir_));
EXPECT_OK(DestroyDir(env_, alternative_db_log_dir_));
auto options = CurrentOptions();
options.env = env_;
auto delete_options = options;
@@ -117,6 +120,8 @@ DBTestBase::~DBTestBase() {
if (getenv("KEEP_DB")) {
printf("DB is still at %s\n", dbname_.c_str());
} else {
EXPECT_OK(DestroyDir(env_, alternative_wal_dir_));
EXPECT_OK(DestroyDir(env_, alternative_db_log_dir_));
EXPECT_OK(DestroyDB(dbname_, options));
}
// Ensure SstFileManager (and its DeleteScheduler background thread) is
+443 -14
View File
@@ -7,6 +7,8 @@
// 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 "db/db_test_util.h"
#include "db/db_with_timestamp_test_util.h"
#include "options/options_helper.h"
@@ -19,12 +21,62 @@
#include "utilities/fault_injection_env.h"
#include "utilities/fault_injection_fs.h"
#if defined(OS_LINUX)
#include <sys/statfs.h>
#endif
#if defined(OS_LINUX) && !defined(BTRFS_SUPER_MAGIC)
#define BTRFS_SUPER_MAGIC 0x9123683E
#endif
#if defined(OS_LINUX) && !defined(TMPFS_MAGIC)
#define TMPFS_MAGIC 0x01021994
#endif
#if defined(OS_LINUX) && !defined(OVERLAYFS_SUPER_MAGIC)
#define OVERLAYFS_SUPER_MAGIC 0x794c7630
#endif
#if defined(OS_LINUX) && !defined(ZFS_SUPER_MAGIC)
#define ZFS_SUPER_MAGIC 0x2fc12fc1
#endif
namespace ROCKSDB_NAMESPACE {
class DBWALTestBase : public DBTestBase {
protected:
explicit DBWALTestBase(const std::string& dir_name)
: DBTestBase(dir_name, /*env_do_fsync=*/true) {}
std::vector<uint64_t> ListWalNumbers() {
std::vector<uint64_t> wal_numbers;
std::vector<std::string> files;
EXPECT_OK(env_->GetChildren(dbname_, &files));
for (const auto& file : files) {
uint64_t number = 0;
FileType type = kWalFile;
if (ParseFileName(file, &number, &type) && type == kWalFile) {
wal_numbers.push_back(number);
}
}
std::sort(wal_numbers.begin(), wal_numbers.end());
return wal_numbers;
}
void CreateEmptyWal(uint64_t wal_number) {
std::unique_ptr<WritableFile> file;
ASSERT_OK(env_->NewWritableFile(LogFileName(dbname_, wal_number), &file,
EnvOptions()));
ASSERT_OK(file->Close());
}
void CreateWalWithContents(uint64_t wal_number, const Slice& contents) {
std::unique_ptr<WritableFile> file;
ASSERT_OK(env_->NewWritableFile(LogFileName(dbname_, wal_number), &file,
EnvOptions()));
ASSERT_OK(file->Append(contents));
ASSERT_OK(file->Close());
}
#if defined(ROCKSDB_PLATFORM_POSIX)
public:
#if defined(ROCKSDB_FALLOCATE_PRESENT)
@@ -57,6 +109,31 @@ class DBWALTestBase : public DBTestBase {
assert(err == 0);
return sbuf.st_blocks * 512;
}
#if defined(ROCKSDB_FALLOCATE_PRESENT)
bool ShouldSkipAllocationCheck(const std::string& file_name) {
(void)file_name;
#if defined(OS_LINUX)
struct statfs fs_stat;
if (statfs(file_name.c_str(), &fs_stat) == 0) {
if (fs_stat.f_type ==
static_cast<decltype(fs_stat.f_type)>(BTRFS_SUPER_MAGIC)) {
return true;
} else if (fs_stat.f_type ==
static_cast<decltype(fs_stat.f_type)>(ZFS_SUPER_MAGIC)) {
return true;
} else if (fs_stat.f_type ==
static_cast<decltype(fs_stat.f_type)>(TMPFS_MAGIC)) {
return true;
} else if (fs_stat.f_type ==
static_cast<decltype(fs_stat.f_type)>(OVERLAYFS_SUPER_MAGIC)) {
return true;
}
}
#endif
return false;
}
#endif // ROCKSDB_FALLOCATE_PRESENT
#endif // ROCKSDB_PLATFORM_POSIX
};
@@ -2752,16 +2829,35 @@ TEST_F(DBWALTest, TruncateLastLogAfterRecoverWithoutFlush) {
auto& file_before = log_files_before[0];
ASSERT_LT(file_before->SizeFileBytes(), 1 * kKB);
// The log file has preallocated space.
ASSERT_GE(GetAllocatedFileSize(dbname_ + file_before->PathName()),
preallocated_size);
{
std::string fname = dbname_ + file_before->PathName();
uint64_t allocated = GetAllocatedFileSize(fname);
if (ShouldSkipAllocationCheck(fname)) {
fprintf(stderr,
"Skipping preallocation check on this filesystem for %s\n",
fname.c_str());
} else if (allocated < preallocated_size) {
fprintf(stderr,
"Warning: allocated size (%lu) less than preallocated size (%lu) "
"for %s, skipping check. This may indicate the filesystem does "
"not support preallocation or does not report it.\n",
allocated, preallocated_size, fname.c_str());
} else {
ASSERT_GE(allocated, preallocated_size);
}
}
Reopen(options);
VectorLogPtr log_files_after;
ASSERT_OK(dbfull()->GetSortedWalFiles(log_files_after));
ASSERT_EQ(1, log_files_after.size());
ASSERT_LT(log_files_after[0]->SizeFileBytes(), 1 * kKB);
// The preallocated space should be truncated.
ASSERT_LT(GetAllocatedFileSize(dbname_ + file_before->PathName()),
preallocated_size);
{
std::string fname = dbname_ + file_before->PathName();
if (!ShouldSkipAllocationCheck(fname)) {
ASSERT_LT(GetAllocatedFileSize(fname), preallocated_size);
}
}
}
// Tests that we will truncate the preallocated space of the last log from
// previous.
@@ -2788,8 +2884,23 @@ TEST_F(DBWALTest, TruncateLastLogAfterRecoverWithFlush) {
ASSERT_EQ(1, log_files_before.size());
auto& file_before = log_files_before[0];
ASSERT_LT(file_before->SizeFileBytes(), 1 * kKB);
ASSERT_GE(GetAllocatedFileSize(dbname_ + file_before->PathName()),
preallocated_size);
{
std::string fname = dbname_ + file_before->PathName();
uint64_t allocated = GetAllocatedFileSize(fname);
if (ShouldSkipAllocationCheck(fname)) {
fprintf(stderr,
"Skipping preallocation check on this filesystem for %s\n",
fname.c_str());
} else if (allocated < preallocated_size) {
fprintf(stderr,
"Warning: allocated size (%lu) less than preallocated size (%lu) "
"for %s, skipping check. This may indicate the filesystem does "
"not support preallocation or does not report it.\n",
allocated, preallocated_size, fname.c_str());
} else {
ASSERT_GE(allocated, preallocated_size);
}
}
// The log file has preallocated space.
Close();
@@ -2807,8 +2918,12 @@ TEST_F(DBWALTest, TruncateLastLogAfterRecoverWithFlush) {
// if the process is in a crash loop, the log file may not get
// deleted and thte preallocated space will keep accumulating. So we need
// to ensure it gets trtuncated.
EXPECT_LT(GetAllocatedFileSize(dbname_ + file_before->PathName()),
preallocated_size);
{
std::string fname = dbname_ + file_before->PathName();
if (!ShouldSkipAllocationCheck(fname)) {
EXPECT_LT(GetAllocatedFileSize(fname), preallocated_size);
}
}
TEST_SYNC_POINT(
"DBWALTest::TruncateLastLogAfterRecoverWithFlush:AfterTruncate");
reopen_thread.join();
@@ -2863,14 +2978,31 @@ TEST_F(DBWALTest, TruncateLastLogAfterRecoverWALEmpty) {
log_file->PrepareWrite(0, 4096);
log_file.reset();
ASSERT_GE(GetAllocatedFileSize(last_log), preallocated_size);
{
uint64_t allocated = GetAllocatedFileSize(last_log);
if (ShouldSkipAllocationCheck(last_log)) {
fprintf(stderr,
"Skipping preallocation check on this filesystem for %s\n",
last_log.c_str());
} else if (allocated < preallocated_size) {
fprintf(stderr,
"Warning: allocated size (%lu) less than preallocated size (%lu) "
"for %s, skipping check. This may indicate the filesystem does "
"not support preallocation or does not report it.\n",
allocated, preallocated_size, last_log.c_str());
} else {
ASSERT_GE(allocated, preallocated_size);
}
}
port::Thread reopen_thread([&]() { Reopen(options); });
TEST_SYNC_POINT(
"DBWALTest::TruncateLastLogAfterRecoverWithFlush:AfterRecover");
// The preallocated space should be truncated.
EXPECT_LT(GetAllocatedFileSize(last_log), preallocated_size);
if (!ShouldSkipAllocationCheck(last_log)) {
EXPECT_LT(GetAllocatedFileSize(last_log), preallocated_size);
}
TEST_SYNC_POINT(
"DBWALTest::TruncateLastLogAfterRecoverWithFlush:AfterTruncate");
reopen_thread.join();
@@ -2912,8 +3044,23 @@ TEST_F(DBWALTest, ReadOnlyRecoveryNoTruncate) {
auto& file_before = log_files_before[0];
ASSERT_LT(file_before->SizeFileBytes(), 1 * kKB);
// The log file has preallocated space.
auto db_size = GetAllocatedFileSize(dbname_ + file_before->PathName());
ASSERT_GE(db_size, preallocated_size);
std::string fname = dbname_ + file_before->PathName();
auto db_size = GetAllocatedFileSize(fname);
{
if (ShouldSkipAllocationCheck(fname)) {
fprintf(stderr,
"Skipping preallocation check on this filesystem for %s\n",
fname.c_str());
} else if (db_size < preallocated_size) {
fprintf(stderr,
"Warning: allocated size (%lu) less than preallocated size (%lu) "
"for %s, skipping check. This may indicate the filesystem does "
"not support preallocation or does not report it.\n",
db_size, preallocated_size, fname.c_str());
} else {
ASSERT_GE(db_size, preallocated_size);
}
}
Close();
// enable truncate and open DB as readonly, the file should not be truncated
@@ -2927,8 +3074,12 @@ TEST_F(DBWALTest, ReadOnlyRecoveryNoTruncate) {
ASSERT_EQ(log_files_after[0]->PathName(), file_before->PathName());
// The preallocated space should NOT be truncated.
// the DB size is almost the same.
ASSERT_NEAR(GetAllocatedFileSize(dbname_ + file_before->PathName()), db_size,
db_size / 100);
{
std::string fname2 = dbname_ + file_before->PathName();
if (!ShouldSkipAllocationCheck(fname2)) {
ASSERT_NEAR(GetAllocatedFileSize(fname2), db_size, db_size / 100);
}
}
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
}
@@ -3071,6 +3222,284 @@ TEST_F(DBWALTest, EmptyWalReopenTest) {
}
}
TEST_F(DBWALTest, AsyncWalPrecreateWaitsForPendingFile) {
Options options = CurrentOptions();
options.env = env_;
options.statistics = CreateDBStatistics();
options.async_wal_precreate = true;
options.recycle_log_file_num = 0;
options.write_buffer_size = 1024;
options.max_write_buffer_number = 4;
// Goal: cover the race where foreground WAL rotation reaches the switch
// before the background precreate publishes its result. The sync points hold
// the async worker after it has created the file but before it marks it
// ready, then verify the foreground writer waits instead of allocating a
// higher WAL number.
SyncPoint::GetInstance()->LoadDependency({
{"DBImpl::BGWorkAsyncWALPrecreate:BeforePublish",
"DBWALTest::AsyncWalPrecreateWaitsForPendingFile:WorkerBlocked"},
{"DBWALTest::AsyncWalPrecreateWaitsForPendingFile:AllowPublish",
"DBImpl::BGWorkAsyncWALPrecreate:Publish"},
{"DBImpl::WaitForAsyncWALPrecreate:BeforeWait",
"DBWALTest::AsyncWalPrecreateWaitsForPendingFile:ForegroundWaiting"},
});
SyncPoint::GetInstance()->EnableProcessing();
Defer cleanup_sync_point([] {
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
});
Reopen(options);
TEST_SYNC_POINT(
"DBWALTest::AsyncWalPrecreateWaitsForPendingFile:WorkerBlocked");
ASSERT_OK(Put("fill", "value"));
Status writer_status;
port::Thread writer(
[&]() { writer_status = dbfull()->TEST_SwitchMemtable(); });
TEST_SYNC_POINT(
"DBWALTest::AsyncWalPrecreateWaitsForPendingFile:ForegroundWaiting");
TEST_SYNC_POINT(
"DBWALTest::AsyncWalPrecreateWaitsForPendingFile:AllowPublish");
writer.join();
ASSERT_OK(writer_status);
ASSERT_OK(Put("trigger", "value"));
ASSERT_EQ("value", Get("trigger"));
ASSERT_EQ(1, options.statistics->getTickerCount(WAL_PRECREATE_HIT));
ASSERT_EQ(1, options.statistics->getTickerCount(WAL_PRECREATE_WAITED));
ASSERT_EQ(0, options.statistics->getTickerCount(WAL_PRECREATE_MISS));
}
TEST_F(DBWALTest, AsyncWalPrecreateConsumesReadyFile) {
Options options = CurrentOptions();
options.env = env_;
options.statistics = CreateDBStatistics();
options.async_wal_precreate = true;
options.recycle_log_file_num = 0;
options.write_buffer_size = 1024;
options.max_write_buffer_number = 4;
// Goal: exercise the no-wait consumption path. The background task publishes
// a prepared future WAL before the foreground memtable switch, and the switch
// must install exactly that reserved WAL number instead of allocating
// another.
SyncPoint::GetInstance()->LoadDependency({
{"DBImpl::BGWorkAsyncWALPrecreate:Done",
"DBWALTest::AsyncWalPrecreateConsumesReadyFile:PrecreateDone"},
});
SyncPoint::GetInstance()->EnableProcessing();
Defer cleanup_sync_point([] {
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
});
Reopen(options);
TEST_SYNC_POINT(
"DBWALTest::AsyncWalPrecreateConsumesReadyFile:PrecreateDone");
const std::vector<uint64_t> wal_numbers = ListWalNumbers();
ASSERT_GE(wal_numbers.size(), 2);
const uint64_t prepared_wal_number = wal_numbers.back();
ASSERT_NE(prepared_wal_number, dbfull()->TEST_GetCurrentLogNumber());
ASSERT_OK(Put("fill", "value"));
ASSERT_OK(dbfull()->TEST_SwitchMemtable());
ASSERT_EQ(prepared_wal_number, dbfull()->TEST_GetCurrentLogNumber());
ASSERT_EQ(1, options.statistics->getTickerCount(WAL_PRECREATE_HIT));
ASSERT_EQ(0, options.statistics->getTickerCount(WAL_PRECREATE_MISS));
ASSERT_EQ(0, options.statistics->getTickerCount(WAL_PRECREATE_WAITED));
}
TEST_F(DBWALTest, AsyncWalPrecreateRecoveryToleratesActualFutureWal) {
Options options = CurrentOptions();
options.env = env_;
options.async_wal_precreate = true;
options.recycle_log_file_num = 0;
options.track_and_verify_wals = true;
options.track_and_verify_wals_in_manifest = true;
options.avoid_flush_during_shutdown = true;
options.avoid_flush_during_recovery = true;
// Goal: cover the actual async precreation path, not a hand-forged empty WAL.
// The background task opens the future WAL, clean close releases the
// unpublished writer and leaves the empty future WAL behind, and recovery
// must tolerate that file while replaying the preceding WAL with real user
// data.
SyncPoint::GetInstance()->LoadDependency({
{"DBImpl::BGWorkAsyncWALPrecreate:Done",
"DBWALTest::AsyncWalPrecreateRecoveryToleratesActualFutureWal:"
"PrecreateDone"},
});
SyncPoint::GetInstance()->EnableProcessing();
Defer cleanup_sync_point([] {
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
});
DestroyAndReopen(options);
TEST_SYNC_POINT(
"DBWALTest::AsyncWalPrecreateRecoveryToleratesActualFutureWal:"
"PrecreateDone");
const std::vector<uint64_t> wal_numbers = ListWalNumbers();
ASSERT_GE(wal_numbers.size(), 2);
const uint64_t future_wal_number = wal_numbers.back();
uint64_t future_wal_size = 0;
ASSERT_OK(env_->GetFileSize(LogFileName(dbname_, future_wal_number),
&future_wal_size));
ASSERT_EQ(0, future_wal_size);
ASSERT_OK(Put("key", "value"));
Close();
ASSERT_OK(TryReopen(options));
ASSERT_EQ("value", Get("key"));
ASSERT_OK(Put("after-recovery", "value2"));
ASSERT_EQ("value2", Get("after-recovery"));
}
TEST_F(DBWALTest, AsyncWalPrecreateDeletesFileOnStartFailure) {
Options options = CurrentOptions();
options.env = env_;
options.statistics = CreateDBStatistics();
options.async_wal_precreate = true;
options.recycle_log_file_num = 0;
options.write_buffer_size = 1024;
options.max_write_buffer_number = 4;
// Goal: force failure after consuming a prepared WAL but before it becomes a
// logical WAL. The failed, unpublished file must be deleted and its writer
// destroyed outside mutex_.
SyncPoint::GetInstance()->LoadDependency({
{"DBImpl::BGWorkAsyncWALPrecreate:Done",
"DBWALTest::AsyncWalPrecreateDeletesFileOnStartFailure:PrecreateDone"},
});
SyncPoint::GetInstance()->EnableProcessing();
Defer cleanup_sync_point([] {
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
});
Reopen(options);
TEST_SYNC_POINT(
"DBWALTest::AsyncWalPrecreateDeletesFileOnStartFailure:PrecreateDone");
SyncPoint::GetInstance()->SetCallBack(
"DBImpl::StartWALFile:AfterCompressionTypeRecord", [](void* arg) {
*static_cast<IOStatus*>(arg) =
IOStatus::IOError("injected async WAL start failure");
});
const std::vector<uint64_t> wal_numbers = ListWalNumbers();
ASSERT_GE(wal_numbers.size(), 2);
const uint64_t prepared_wal_number = wal_numbers.back();
ASSERT_OK(Put("fill", "value"));
ASSERT_NOK(dbfull()->TEST_SwitchMemtable());
ASSERT_TRUE(
env_->FileExists(LogFileName(dbname_, prepared_wal_number)).IsNotFound());
ASSERT_EQ(1, options.statistics->getTickerCount(WAL_PRECREATE_HIT));
}
TEST_F(DBWALTest, AsyncWalPrecreateFailureFallsBackToSyncCreation) {
Options options = CurrentOptions();
options.env = env_;
options.statistics = CreateDBStatistics();
options.async_wal_precreate = true;
options.recycle_log_file_num = 0;
options.write_buffer_size = 1024;
options.max_write_buffer_number = 4;
// Goal: fail the background precreate file open after normal DB open has
// created the current WAL. The foreground switch should observe no prepared
// WAL, create one synchronously, and expose both failure and miss counters.
SyncPoint::GetInstance()->LoadDependency({
{"DBImpl::BGWorkAsyncWALPrecreate:Done",
"DBWALTest::AsyncWalPrecreateFailureFallsBackToSyncCreation:"
"PrecreateDone"},
});
SyncPoint::GetInstance()->SetCallBack(
"DBImpl::Open:AfterDeleteFiles",
[&](void*) { env_->non_writable_count_ = 1; });
SyncPoint::GetInstance()->EnableProcessing();
Defer cleanup_sync_point([&] {
env_->non_writable_count_ = 0;
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
});
Reopen(options);
TEST_SYNC_POINT(
"DBWALTest::AsyncWalPrecreateFailureFallsBackToSyncCreation:"
"PrecreateDone");
ASSERT_EQ(1, options.statistics->getTickerCount(WAL_PRECREATE_FAILED));
ASSERT_OK(Put("fill", "value"));
ASSERT_OK(dbfull()->TEST_SwitchMemtable());
ASSERT_EQ(1, options.statistics->getTickerCount(WAL_PRECREATE_MISS));
}
TEST_F(DBWALTest, ReadOnlyErrorIfWalFileExistsIgnoresEmptyWals) {
Options options = CurrentOptions();
options.env = env_;
DestroyAndReopen(options);
Close();
// Goal: preserve error_if_wal_file_exists for WALs containing data while
// allowing empty future WALs left by async precreation. This creates empty
// WAL files directly so the test isolates the read-only open contract.
const std::vector<uint64_t> existing_wal_numbers = ListWalNumbers();
const uint64_t empty_wal_number =
existing_wal_numbers.empty() ? 1 : existing_wal_numbers.back() + 1;
CreateEmptyWal(empty_wal_number);
DBOptions db_options(options);
std::vector<ColumnFamilyDescriptor> column_families;
column_families.emplace_back(kDefaultColumnFamilyName,
ColumnFamilyOptions(options));
std::vector<ColumnFamilyHandle*> handles;
std::unique_ptr<DB> read_only_db;
ASSERT_OK(DB::OpenForReadOnly(db_options, dbname_, column_families, &handles,
&read_only_db,
true /* error_if_wal_file_exists */));
for (ColumnFamilyHandle* handle : handles) {
ASSERT_OK(read_only_db->DestroyColumnFamilyHandle(handle));
}
read_only_db.reset();
handles.clear();
CreateWalWithContents(empty_wal_number + 1, Slice("not empty"));
ASSERT_NOK(DB::OpenForReadOnly(db_options, dbname_, column_families, &handles,
&read_only_db,
true /* error_if_wal_file_exists */));
}
TEST_F(DBWALTest, RecoveryToleratesEmptyFutureWal) {
Options options = CurrentOptions();
options.env = env_;
options.track_and_verify_wals = true;
options.track_and_verify_wals_in_manifest = true;
options.avoid_flush_during_recovery = true;
DestroyAndReopen(options);
ASSERT_OK(Put("key", "value"));
Close();
const std::vector<uint64_t> wal_numbers = ListWalNumbers();
ASSERT_FALSE(wal_numbers.empty());
const uint64_t future_wal_number = wal_numbers.back() + 1;
CreateEmptyWal(future_wal_number);
// Goal: model a crash after async WAL precreation but before consumption.
// Recovery should process the zero-byte future WAL, mark its file number
// used, and keep replaying the real preceding WAL without reporting a WAL
// chain or MANIFEST-tracking error.
ASSERT_OK(TryReopen(options));
ASSERT_EQ("value", Get("key"));
ASSERT_OK(Put("after-recovery", "value2"));
ASSERT_EQ("value2", Get("after-recovery"));
}
TEST_F(DBWALTest, RecoveryFlushSwitchWALOnEmptyMemtable) {
Options options = CurrentOptions();
auto fault_fs = std::make_shared<FaultInjectionTestFS>(FileSystem::Default());
+2 -2
View File
@@ -218,7 +218,7 @@ TEST_P(IterKeySwapTest, SwapAndDestroy) {
b_use_secondary] = GetParam();
std::string expected_a, expected_b;
// Storage for pinned keys — must outlive the IterKeys.
// Storage for pinned keys -- must outlive the IterKeys.
std::string a_storage, b_storage;
IterKey a;
expected_a =
@@ -236,7 +236,7 @@ TEST_P(IterKeySwapTest, SwapAndDestroy) {
// After swap: a has b's old data, b has a's old data.
ASSERT_EQ(a.GetUserKey().ToString(), expected_b);
ASSERT_EQ(b.GetUserKey().ToString(), expected_a);
} // b destroyed here — must not corrupt a's data
} // b destroyed here -- must not corrupt a's data
// a must still hold valid data after b's destruction.
ASSERT_EQ(a.GetUserKey().ToString(), expected_b);
+193 -4
View File
@@ -31,6 +31,7 @@
#include "util/mutexlock.h"
#include "util/rate_limiter_impl.h"
#include "util/string_util.h"
#include "utilities/fault_injection_fs.h"
#include "utilities/merge_operators.h"
namespace ROCKSDB_NAMESPACE {
@@ -201,6 +202,194 @@ TEST_F(EventListenerTest, OnSingleDBCompactionTest) {
}
}
// Listener that asserts OnCompactionPreCommit fires strictly between
// OnCompactionBegin and OnCompactionCompleted, and that input files'
// being_compacted flag is still true at that point.
class TestCompactionPreCommitListener : public EventListener {
public:
explicit TestCompactionPreCommitListener(EventListenerTest* test)
: test_(test) {}
void OnCompactionBegin(DB* /*db*/, const CompactionJobInfo& ci) override {
std::lock_guard<std::mutex> lock(mutex_);
++begin_count_;
last_begin_job_id_ = ci.job_id;
EXPECT_EQ(begin_count_, pre_commit_count_ + 1);
EXPECT_EQ(begin_count_, completed_count_ + 1);
}
void OnCompactionPreCommit(DB* db, const CompactionJobInfo& ci) override {
std::lock_guard<std::mutex> lock(mutex_);
++pre_commit_count_;
// Must fire after Begin and before Completed for this compaction.
EXPECT_EQ(pre_commit_count_, begin_count_);
EXPECT_EQ(pre_commit_count_, completed_count_ + 1);
EXPECT_EQ(ci.job_id, last_begin_job_id_);
EXPECT_GT(ci.input_files.size(), 0U);
// Verify input files are still marked being_compacted.
std::vector<std::vector<FileMetaData>> files_by_level;
test_->dbfull()->TEST_GetFilesMetaData(test_->handles_[ci.cf_id],
&files_by_level);
EXPECT_EQ(test_->db_.get(), db);
for (const auto& info : ci.input_file_infos) {
bool found = false;
for (const auto& level_files : files_by_level) {
for (const auto& meta : level_files) {
if (meta.fd.GetNumber() == info.file_number) {
found = true;
EXPECT_TRUE(meta.being_compacted)
<< "input file " << info.file_number
<< " should still be being_compacted in "
"OnCompactionPreCommit";
}
}
}
EXPECT_TRUE(found) << "input file " << info.file_number
<< " not found in DB";
}
}
void OnCompactionCompleted(DB* /*db*/, const CompactionJobInfo& ci) override {
std::lock_guard<std::mutex> lock(mutex_);
++completed_count_;
// Must fire after the matching PreCommit.
EXPECT_EQ(completed_count_, pre_commit_count_);
EXPECT_EQ(completed_count_, begin_count_);
EXPECT_EQ(ci.job_id, last_begin_job_id_);
}
size_t BeginCount() {
std::lock_guard<std::mutex> lock(mutex_);
return begin_count_;
}
size_t PreCommitCount() {
std::lock_guard<std::mutex> lock(mutex_);
return pre_commit_count_;
}
size_t CompletedCount() {
std::lock_guard<std::mutex> lock(mutex_);
return completed_count_;
}
private:
EventListenerTest* test_;
std::mutex mutex_;
size_t begin_count_ = 0;
size_t pre_commit_count_ = 0;
size_t completed_count_ = 0;
int last_begin_job_id_ = -1;
};
TEST_F(EventListenerTest, OnCompactionPreCommitOrdering) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.compaction_style = kCompactionStyleLevel;
options.compression = kNoCompression;
options.level0_file_num_compaction_trigger = 4;
auto* listener = new TestCompactionPreCommitListener(this);
options.listeners.emplace_back(listener);
// Verify sync-point ordering: Begin -> PreCommit -> Completed.
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
{{"DBImpl::NotifyOnCompactionBegin::UnlockMutex",
"DBImpl::NotifyOnCompactionPreCommit::UnlockMutex"},
{"DBImpl::NotifyOnCompactionPreCommit::UnlockMutex",
"DBImpl::NotifyOnCompactionCompleted::UnlockMutex"}});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
CreateAndReopenWithCF({"pikachu"}, options);
Random rnd(301);
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 10; j++) {
ASSERT_OK(Put(1, rnd.RandomString(10), rnd.RandomString(10)));
}
ASSERT_OK(Flush(1));
}
ASSERT_OK(dbfull()->TEST_WaitForCompact());
EXPECT_GT(listener->BeginCount(), 0U);
EXPECT_EQ(listener->PreCommitCount(), listener->BeginCount());
EXPECT_EQ(listener->CompletedCount(), listener->BeginCount());
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
}
class TestDBShutdownBeginListener : public EventListener {
public:
void OnDBShutdownBegin(DB* db) override {
std::lock_guard<std::mutex> lock(mutex_);
++shutdown_count_;
last_db_ = db;
}
int ShutdownCount() const {
std::lock_guard<std::mutex> lock(mutex_);
return shutdown_count_;
}
DB* LastDB() const {
std::lock_guard<std::mutex> lock(mutex_);
return last_db_;
}
private:
mutable std::mutex mutex_;
int shutdown_count_ = 0;
DB* last_db_ = nullptr;
};
TEST_F(EventListenerTest, OnDBShutdownBeginOnceForCancelAndClose) {
Options options = CurrentOptions();
options.create_if_missing = true;
auto listener = std::make_shared<TestDBShutdownBeginListener>();
options.listeners.emplace_back(listener);
DestroyAndReopen(options);
DB* db = db_.get();
dbfull()->CancelAllBackgroundWork(false);
EXPECT_EQ(1, listener->ShutdownCount());
EXPECT_EQ(db, listener->LastDB());
Close();
EXPECT_EQ(1, listener->ShutdownCount());
}
TEST_F(EventListenerTest, OnDBShutdownBeginOnFailedOpen) {
Close();
std::shared_ptr<FaultInjectionTestFS> fs(
new FaultInjectionTestFS(env_->GetFileSystem()));
std::unique_ptr<Env> env(NewCompositeEnv(fs));
Options options = CurrentOptions();
options.env = env.get();
options.create_if_missing = true;
auto listener = std::make_shared<TestDBShutdownBeginListener>();
options.listeners.emplace_back(listener);
SyncPoint::GetInstance()->SetCallBack(
"PersistRocksDBOptions:create",
[&](void* /*arg*/) { fs->SetFilesystemActive(false); });
SyncPoint::GetInstance()->SetCallBack(
"PersistRocksDBOptions:written",
[&](void* /*arg*/) { fs->SetFilesystemActive(true); });
SyncPoint::GetInstance()->EnableProcessing();
std::unique_ptr<DB> db;
Status s = DB::Open(options, dbname_, &db);
ASSERT_TRUE(s.IsIOError()) << s.ToString();
EXPECT_EQ(1, listener->ShutdownCount());
EXPECT_NE(nullptr, listener->LastDB());
EXPECT_EQ(nullptr, db.get());
fs->SetFilesystemActive(true);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
}
// This simple Listener can only handle one flush at a time.
class TestFlushListener : public EventListener {
public:
@@ -1696,7 +1885,7 @@ TEST_F(EventListenerTest, BackgroundJobPressure) {
Env::Priority::LOW);
sleeping_task.WaitUntilSleeping();
// Phase 1: No pressure — 3 SST files (below slowdown trigger=4).
// Phase 1: No pressure -- 3 SST files (below slowdown trigger=4).
for (int i = 0; i < 3; i++) {
ASSERT_OK(Put("k" + std::to_string(i), std::string(100, 'x')));
ASSERT_OK(Flush());
@@ -1714,7 +1903,7 @@ TEST_F(EventListenerTest, BackgroundJobPressure) {
// MaybeScheduleFlushOrCompaction() runs before the pressure callback and
// may schedule new flush work, making flush counts non-deterministic.
// Phase 2: Build pressure — flush past slowdown trigger (4 L0 SST files).
// Phase 2: Build pressure -- flush past slowdown trigger (4 L0 SST files).
// Compaction is blocked, so L0 SST files pile up.
listener->Reset();
{
@@ -1750,11 +1939,11 @@ TEST_F(EventListenerTest, BackgroundJobPressure) {
ASSERT_TRUE(found_compaction_scheduled);
ASSERT_TRUE(found_high_proximity);
// Phase 3: Relieve pressure — unblock compaction, wait for completion.
// Phase 3: Relieve pressure -- unblock compaction, wait for completion.
listener->Reset();
sleeping_task.WakeUp();
sleeping_task.WaitUntilDone();
ASSERT_OK(dbfull()->TEST_WaitForCompact());
ASSERT_OK(dbfull()->TEST_WaitForBackgroundWork());
snapshots = listener->GetSnapshots();
CheckInvariants(snapshots);
+3 -2
View File
@@ -22,9 +22,10 @@ namespace ROCKSDB_NAMESPACE::log {
Writer::Writer(std::unique_ptr<WritableFileWriter>&& dest, uint64_t log_number,
bool recycle_log_files, bool manual_flush,
CompressionType compression_type, bool track_and_verify_wals)
CompressionType compression_type, bool track_and_verify_wals,
size_t initial_block_offset)
: dest_(std::move(dest)),
block_offset_(0),
block_offset_(initial_block_offset),
log_number_(log_number),
recycle_log_files_(recycle_log_files),
// Header size varies depending on whether we are recycling or not.
+8 -3
View File
@@ -75,15 +75,20 @@ namespace log {
class Writer {
public:
// Create a writer that will append data to "*dest".
// "*dest" must be initially empty.
// "*dest" must remain live while this Writer is in use.
// "*dest" must remain live while this Writer is in use. By default
// "*dest" is expected to be empty (initial_block_offset = 0). When
// resuming append into an existing log file (e.g.
// VersionSet::ReopenManifestForAppend), pass the offset within the
// current 32 KiB block at which writes should resume so that record
// framing aligns to the existing block layout.
// TODO(hx235): separate WAL related parameters from general `Reader`
// parameters
explicit Writer(std::unique_ptr<WritableFileWriter>&& dest,
uint64_t log_number, bool recycle_log_files,
bool manual_flush = false,
CompressionType compressionType = kNoCompression,
bool track_and_verify_wals = false);
bool track_and_verify_wals = false,
size_t initial_block_offset = 0);
// No copying allowed
Writer(const Writer&) = delete;
void operator=(const Writer&) = delete;
+80 -22
View File
@@ -15,6 +15,7 @@
#include <memory>
#include <optional>
#include "db/blob/blob_fetcher.h"
#include "db/blob/blob_file_partition_manager.h"
#include "db/blob/blob_index.h"
#include "db/dbformat.h"
@@ -70,6 +71,67 @@ Status GetDefaultColumnBlobIndexSlice(Slice entity, Slice* blob_index_slice) {
return status;
}
Status PushWideColumnEntityDefaultOperand(const Slice& user_key,
const Slice& entity,
MergeContext* merge_context,
bool operand_pinned,
const BlobFetcher* blob_fetcher) {
assert(merge_context != nullptr);
Slice entity_ref = entity;
Slice value_of_default;
Status status = WideColumnSerialization::GetValueOfDefaultColumn(
entity_ref, value_of_default);
if (status.ok()) {
merge_context->PushOperand(value_of_default, operand_pinned);
return status;
}
if (!status.IsNotSupported()) {
return status;
}
if (blob_fetcher == nullptr) {
return Status::Corruption(
"Cannot resolve blob-backed default column without a blob fetcher");
}
PinnableSlice resolved_default;
bool resolved = false;
status = WideColumnSerialization::GetValueOfDefaultColumnResolvingBlobs(
entity, user_key, blob_fetcher, resolved_default, resolved);
if (status.ok()) {
// Resolved blob values are backed by this stack-local PinnableSlice, so
// copy them into MergeContext instead of pinning their storage.
merge_context->PushOperand(Slice(resolved_default), false);
}
return status;
}
Status MergeWithWideColumnEntityBaseValue(
const Slice& user_key, const Slice& entity,
const MergeOperator* merge_operator, MergeContext* merge_context,
Logger* logger, Statistics* statistics, SystemClock* clock,
std::string* value, PinnableWideColumns* columns,
const BlobFetcher* blob_fetcher) {
assert(merge_context != nullptr);
std::string resolved_entity;
Slice effective_entity;
Status status = WideColumnSerialization::ResolveEntityForMerge(
entity, user_key, blob_fetcher, nullptr /* prefetch_buffers */,
resolved_entity, effective_entity);
if (!status.ok()) {
return status;
}
// `op_failure_scope` (an output parameter) is not provided (set to nullptr)
// since a failure must be propagated regardless of its value.
return MergeHelper::TimedFullMerge(
merge_operator, user_key, MergeHelper::kWideBaseValue, effective_entity,
merge_context->GetOperands(), logger, statistics, clock,
/* update_num_ops_stats */ true, /* op_failure_scope */ nullptr, value,
columns);
}
} // namespace
ImmutableMemTableOptions::ImmutableMemTableOptions(
@@ -1254,6 +1316,7 @@ struct Saver {
bool inplace_update_support;
bool do_merge;
SystemClock* clock;
const BlobFetcher* blob_fetcher;
ReadCallback* callback_;
bool* is_blob_index;
@@ -1424,27 +1487,17 @@ static bool SaveValue(void* arg, const char* entry) {
// Preserve the value with the goal of returning it as part of
// raw merge operands to the user
Slice value_of_default;
*(s->status) = WideColumnSerialization::GetValueOfDefaultColumn(
v, value_of_default);
if (s->status->ok()) {
merge_context->PushOperand(
value_of_default,
s->inplace_update_support == false /* operand_pinned */);
}
*(s->status) = PushWideColumnEntityDefaultOperand(
s->key->user_key(), v, merge_context,
s->inplace_update_support == false /* operand_pinned */,
s->blob_fetcher);
} else if (*(s->merge_in_progress)) {
assert(s->do_merge);
if (s->value || s->columns) {
// `op_failure_scope` (an output parameter) is not provided (set
// to nullptr) since a failure must be propagated regardless of
// its value.
*(s->status) = MergeHelper::TimedFullMerge(
merge_operator, s->key->user_key(), MergeHelper::kWideBaseValue,
v, merge_context->GetOperands(), s->logger, s->statistics,
s->clock, /* update_num_ops_stats */ true,
/* op_failure_scope */ nullptr, s->value, s->columns);
*(s->status) = MergeWithWideColumnEntityBaseValue(
s->key->user_key(), v, merge_operator, merge_context, s->logger,
s->statistics, s->clock, s->value, s->columns, s->blob_fetcher);
}
} else if (s->value) {
Slice value_of_default;
@@ -1520,7 +1573,8 @@ bool MemTable::Get(const LookupKey& key, std::string* value,
SequenceNumber* max_covering_tombstone_seq,
SequenceNumber* seq, const ReadOptions& read_opts,
bool immutable_memtable, ReadCallback* callback,
bool* is_blob_index, bool do_merge) {
bool* is_blob_index, bool do_merge,
const BlobFetcher* blob_fetcher) {
// The sequence number is updated synchronously in version_set.h
if (IsEmpty()) {
// Avoiding recording stats for speed.
@@ -1578,7 +1632,7 @@ bool MemTable::Get(const LookupKey& key, std::string* value,
}
GetFromTable(key, *max_covering_tombstone_seq, do_merge, callback,
is_blob_index, value, columns, timestamp, s, merge_context,
seq, &found_final_value, &merge_in_progress);
seq, &found_final_value, &merge_in_progress, blob_fetcher);
}
// No change to value, since we have not yet found a Put/Delete
@@ -1601,7 +1655,8 @@ void MemTable::GetFromTable(const LookupKey& key,
PinnableWideColumns* columns,
std::string* timestamp, Status* s,
MergeContext* merge_context, SequenceNumber* seq,
bool* found_final_value, bool* merge_in_progress) {
bool* found_final_value, bool* merge_in_progress,
const BlobFetcher* blob_fetcher) {
Saver saver;
saver.status = s;
saver.found_final_value = found_final_value;
@@ -1619,6 +1674,7 @@ void MemTable::GetFromTable(const LookupKey& key,
saver.inplace_update_support = moptions_.inplace_update_support;
saver.statistics = moptions_.statistics;
saver.clock = clock_;
saver.blob_fetcher = blob_fetcher;
saver.callback_ = callback;
saver.is_blob_index = is_blob_index;
saver.do_merge = do_merge;
@@ -1648,7 +1704,8 @@ Status MemTable::ValidateKey(const char* key, bool allow_data_in_errors) {
}
void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
ReadCallback* callback, bool immutable_memtable) {
ReadCallback* callback, bool immutable_memtable,
const BlobFetcher* blob_fetcher) {
// The sequence number is updated synchronously in version_set.h
if (IsEmpty()) {
// Avoiding recording stats for speed.
@@ -1740,6 +1797,7 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
saver.inplace_update_support = moptions_.inplace_update_support;
saver.statistics = moptions_.statistics;
saver.clock = clock_;
saver.blob_fetcher = blob_fetcher;
saver.callback_ = callback;
saver.is_blob_index = &iter->is_blob_index;
saver.do_merge = true;
@@ -1831,7 +1889,7 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
*(iter->lkey), iter->max_covering_tombstone_seq, true, callback,
&iter->is_blob_index, iter->value ? iter->value->GetSelf() : nullptr,
iter->columns, iter->timestamp, iter->s, &(iter->merge_context),
&dummy_seq, &found_final_value, &merge_in_progress);
&dummy_seq, &found_final_value, &merge_in_progress, blob_fetcher);
if (!found_final_value && merge_in_progress) {
if (iter->s->ok()) {
+14 -8
View File
@@ -39,6 +39,7 @@
namespace ROCKSDB_NAMESPACE {
class BlobFetcher;
class BlobFilePartitionManager;
struct FlushJobInfo;
class Mutex;
@@ -236,25 +237,27 @@ class ReadOnlyMemTable {
SequenceNumber* max_covering_tombstone_seq,
SequenceNumber* seq, const ReadOptions& read_opts,
bool immutable_memtable, ReadCallback* callback = nullptr,
bool* is_blob_index = nullptr, bool do_merge = true) = 0;
bool* is_blob_index = nullptr, bool do_merge = true,
const BlobFetcher* blob_fetcher = nullptr) = 0;
bool Get(const LookupKey& key, std::string* value,
PinnableWideColumns* columns, std::string* timestamp, Status* s,
MergeContext* merge_context,
SequenceNumber* max_covering_tombstone_seq,
const ReadOptions& read_opts, bool immutable_memtable,
ReadCallback* callback = nullptr, bool* is_blob_index = nullptr,
bool do_merge = true) {
bool do_merge = true, const BlobFetcher* blob_fetcher = nullptr) {
SequenceNumber seq;
return Get(key, value, columns, timestamp, s, merge_context,
max_covering_tombstone_seq, &seq, read_opts, immutable_memtable,
callback, is_blob_index, do_merge);
callback, is_blob_index, do_merge, blob_fetcher);
}
// @param immutable_memtable Whether this memtable is immutable. Used
// internally by NewRangeTombstoneIterator(). See comment above
// NewRangeTombstoneIterator() for more detail.
virtual void MultiGet(const ReadOptions& read_options, MultiGetRange* range,
ReadCallback* callback, bool immutable_memtable) = 0;
ReadCallback* callback, bool immutable_memtable,
const BlobFetcher* blob_fetcher = nullptr) = 0;
// Get total number of entries in the mem table.
// REQUIRES: external synchronization to prevent simultaneous
@@ -320,7 +323,7 @@ class ReadOnlyMemTable {
virtual uint64_t ApproximateOldestKeyTime() const = 0;
// Inserts a range tombstone [start_key, end_key) that is logically redundant
// — it is derived from existing point tombstones observed during iteration
// -- it is derived from existing point tombstones observed during iteration
// and does not delete any data that isn't already deleted. This is a
// best-effort optimization. It allows future reads to skip iterating over
// continuous single deletion tombstones.
@@ -688,10 +691,12 @@ class MemTable final : public ReadOnlyMemTable {
SequenceNumber* max_covering_tombstone_seq, SequenceNumber* seq,
const ReadOptions& read_opts, bool immutable_memtable,
ReadCallback* callback = nullptr, bool* is_blob_index = nullptr,
bool do_merge = true) override;
bool do_merge = true,
const BlobFetcher* blob_fetcher = nullptr) override;
void MultiGet(const ReadOptions& read_options, MultiGetRange* range,
ReadCallback* callback, bool immutable_memtable) override;
ReadCallback* callback, bool immutable_memtable,
const BlobFetcher* blob_fetcher = nullptr) override;
// If `key` exists in current memtable with type value_type and the existing
// value is at least as large as the new value, updates it in-place. Otherwise
@@ -991,7 +996,8 @@ class MemTable final : public ReadOnlyMemTable {
std::string* value, PinnableWideColumns* columns,
std::string* timestamp, Status* s,
MergeContext* merge_context, SequenceNumber* seq,
bool* found_final_value, bool* merge_in_progress);
bool* found_final_value, bool* merge_in_progress,
const BlobFetcher* blob_fetcher);
// Always returns non-null and assumes certain pre-checks (e.g.,
// is_range_del_table_empty_) are done. This is only valid during the lifetime
+18 -14
View File
@@ -108,18 +108,19 @@ bool MemTableListVersion::Get(const LookupKey& key, std::string* value,
MergeContext* merge_context,
SequenceNumber* max_covering_tombstone_seq,
SequenceNumber* seq, const ReadOptions& read_opts,
ReadCallback* callback, bool* is_blob_index) {
ReadCallback* callback, bool* is_blob_index,
const BlobFetcher* blob_fetcher) {
return GetFromList(&memlist_, key, value, columns, timestamp, s,
merge_context, max_covering_tombstone_seq, seq, read_opts,
callback, is_blob_index);
callback, is_blob_index, blob_fetcher);
}
void MemTableListVersion::MultiGet(const ReadOptions& read_options,
MultiGetRange* range,
ReadCallback* callback) {
MultiGetRange* range, ReadCallback* callback,
const BlobFetcher* blob_fetcher) {
for (auto memtable : memlist_) {
memtable->MultiGet(read_options, range, callback,
true /* immutable_memtable */);
true /* immutable_memtable */, blob_fetcher);
if (range->empty()) {
return;
}
@@ -128,12 +129,13 @@ void MemTableListVersion::MultiGet(const ReadOptions& read_options,
bool MemTableListVersion::GetMergeOperands(
const LookupKey& key, Status* s, MergeContext* merge_context,
SequenceNumber* max_covering_tombstone_seq, const ReadOptions& read_opts) {
SequenceNumber* max_covering_tombstone_seq, const ReadOptions& read_opts,
const BlobFetcher* blob_fetcher) {
for (ReadOnlyMemTable* memtable : memlist_) {
bool done = memtable->Get(
key, /*value=*/nullptr, /*columns=*/nullptr, /*timestamp=*/nullptr, s,
merge_context, max_covering_tombstone_seq, read_opts,
true /* immutable_memtable */, nullptr, nullptr, false);
true /* immutable_memtable */, nullptr, nullptr, false, blob_fetcher);
if (done) {
return true;
}
@@ -145,10 +147,11 @@ bool MemTableListVersion::GetFromHistory(
const LookupKey& key, std::string* value, PinnableWideColumns* columns,
std::string* timestamp, Status* s, MergeContext* merge_context,
SequenceNumber* max_covering_tombstone_seq, SequenceNumber* seq,
const ReadOptions& read_opts, bool* is_blob_index) {
const ReadOptions& read_opts, bool* is_blob_index,
const BlobFetcher* blob_fetcher) {
return GetFromList(&memlist_history_, key, value, columns, timestamp, s,
merge_context, max_covering_tombstone_seq, seq, read_opts,
nullptr /*read_callback*/, is_blob_index);
nullptr /*read_callback*/, is_blob_index, blob_fetcher);
}
bool MemTableListVersion::GetFromList(
@@ -156,17 +159,18 @@ bool MemTableListVersion::GetFromList(
std::string* value, PinnableWideColumns* columns, std::string* timestamp,
Status* s, MergeContext* merge_context,
SequenceNumber* max_covering_tombstone_seq, SequenceNumber* seq,
const ReadOptions& read_opts, ReadCallback* callback, bool* is_blob_index) {
const ReadOptions& read_opts, ReadCallback* callback, bool* is_blob_index,
const BlobFetcher* blob_fetcher) {
*seq = kMaxSequenceNumber;
for (auto& memtable : *list) {
assert(memtable->IsFragmentedRangeTombstonesConstructed());
SequenceNumber current_seq = kMaxSequenceNumber;
bool done =
memtable->Get(key, value, columns, timestamp, s, merge_context,
max_covering_tombstone_seq, &current_seq, read_opts,
true /* immutable_memtable */, callback, is_blob_index);
bool done = memtable->Get(key, value, columns, timestamp, s, merge_context,
max_covering_tombstone_seq, &current_seq,
read_opts, true /* immutable_memtable */,
callback, is_blob_index, true, blob_fetcher);
if (*seq == kMaxSequenceNumber) {
// Store the most recent sequence number of any operation on this key.
// Since we only care about the most recent change, we only need to
+16 -9
View File
@@ -61,29 +61,33 @@ class MemTableListVersion {
MergeContext* merge_context,
SequenceNumber* max_covering_tombstone_seq, SequenceNumber* seq,
const ReadOptions& read_opts, ReadCallback* callback = nullptr,
bool* is_blob_index = nullptr);
bool* is_blob_index = nullptr,
const BlobFetcher* blob_fetcher = nullptr);
bool Get(const LookupKey& key, std::string* value,
PinnableWideColumns* columns, std::string* timestamp, Status* s,
MergeContext* merge_context,
SequenceNumber* max_covering_tombstone_seq,
const ReadOptions& read_opts, ReadCallback* callback = nullptr,
bool* is_blob_index = nullptr) {
bool* is_blob_index = nullptr,
const BlobFetcher* blob_fetcher = nullptr) {
SequenceNumber seq;
return Get(key, value, columns, timestamp, s, merge_context,
max_covering_tombstone_seq, &seq, read_opts, callback,
is_blob_index);
is_blob_index, blob_fetcher);
}
void MultiGet(const ReadOptions& read_options, MultiGetRange* range,
ReadCallback* callback);
ReadCallback* callback,
const BlobFetcher* blob_fetcher = nullptr);
// Returns all the merge operands corresponding to the key by searching all
// memtables starting from the most recent one.
bool GetMergeOperands(const LookupKey& key, Status* s,
MergeContext* merge_context,
SequenceNumber* max_covering_tombstone_seq,
const ReadOptions& read_opts);
const ReadOptions& read_opts,
const BlobFetcher* blob_fetcher = nullptr);
// Similar to Get(), but searches the Memtable history of memtables that
// have already been flushed. Should only be used from in-memory only
@@ -94,17 +98,19 @@ class MemTableListVersion {
Status* s, MergeContext* merge_context,
SequenceNumber* max_covering_tombstone_seq,
SequenceNumber* seq, const ReadOptions& read_opts,
bool* is_blob_index = nullptr);
bool* is_blob_index = nullptr,
const BlobFetcher* blob_fetcher = nullptr);
bool GetFromHistory(const LookupKey& key, std::string* value,
PinnableWideColumns* columns, std::string* timestamp,
Status* s, MergeContext* merge_context,
SequenceNumber* max_covering_tombstone_seq,
const ReadOptions& read_opts,
bool* is_blob_index = nullptr) {
bool* is_blob_index = nullptr,
const BlobFetcher* blob_fetcher = nullptr) {
SequenceNumber seq;
return GetFromHistory(key, value, columns, timestamp, s, merge_context,
max_covering_tombstone_seq, &seq, read_opts,
is_blob_index);
is_blob_index, blob_fetcher);
}
Status AddRangeTombstoneIterators(const ReadOptions& read_opts, Arena* arena,
@@ -188,7 +194,8 @@ class MemTableListVersion {
SequenceNumber* max_covering_tombstone_seq,
SequenceNumber* seq, const ReadOptions& read_opts,
ReadCallback* callback = nullptr,
bool* is_blob_index = nullptr);
bool* is_blob_index = nullptr,
const BlobFetcher* blob_fetcher = nullptr);
void AddMemTable(ReadOnlyMemTable* m);
+38 -1
View File
@@ -30,7 +30,7 @@ PinnedTableReader& PinnedTableReader::operator=(
TableReader* r = other.reader_.load(std::memory_order_acquire);
// Only read handle_ when reader_ is non-null. Pin() writes handle_ before
// reader_ (with release), so a non-null reader_ guarantees handle_ is stable.
// If reader_ is null, Pin() may be in progress — avoid reading handle_.
// If reader_ is null, Pin() may be in progress -- avoid reading handle_.
handle_ = (r != nullptr) ? other.handle_ : nullptr;
reader_.store(r, std::memory_order_release);
return *this;
@@ -109,6 +109,17 @@ Status FileMetaData::UpdateBoundaries(const Slice& key, const Slice& value,
void VersionEdit::Clear() { *this = VersionEdit(); }
bool VersionEdit::ShouldEmitPerColumnFamilyRecoveryEdit(
uint64_t current_log_number) const {
return (HasLogNumber() && GetLogNumber() > current_log_number) ||
NumEntries() > 0 || HasComparatorName() || HasPrevLogNumber() ||
HasNextFile() || HasMaxColumnFamily() || HasMinLogNumberToKeep() ||
HasLastSequence() || !GetCompactCursors().empty() || HasDbId() ||
IsColumnFamilyManipulation() || IsInAtomicGroup() ||
HasFullHistoryTsLow() || HasPersistUserDefinedTimestamps() ||
HasSubcompactionProgress();
}
bool VersionEdit::EncodeTo(std::string* dst,
std::optional<size_t> ts_sz) const {
assert(!IsNoManifestWriteDummy());
@@ -229,6 +240,13 @@ bool VersionEdit::EncodeTo(std::string* dst,
PutLengthPrefixedSlice(dst, progress_data);
}
if (has_last_compacted_manifest_file_size_) {
PutVarint32(dst, kLastCompactedManifestFileSize);
std::string varint_size;
PutVarint64(&varint_size, last_compacted_manifest_file_size_);
PutLengthPrefixedSlice(dst, Slice(varint_size));
}
return true;
}
@@ -879,6 +897,17 @@ Status VersionEdit::DecodeFrom(const Slice& src) {
break;
}
case kLastCompactedManifestFileSize: {
Slice encoded;
if (GetLengthPrefixedSlice(&input, &encoded) &&
GetVarint64(&encoded, &last_compacted_manifest_file_size_)) {
has_last_compacted_manifest_file_size_ = true;
} else {
msg = "last compacted manifest file size";
}
break;
}
default:
if (tag & kTagSafeIgnoreMask) {
// Tag from future which can be safely ignored.
@@ -1053,6 +1082,10 @@ std::string VersionEdit::DebugString(bool hex_key) const {
r.append("\n SubcompactionProgress: ");
r.append(subcompaction_progress_.ToString());
}
if (has_last_compacted_manifest_file_size_) {
r.append("\n LastCompactedManifestFileSize: ");
AppendNumberTo(&r, last_compacted_manifest_file_size_);
}
r.append("\n}\n");
return r;
}
@@ -1209,6 +1242,10 @@ std::string VersionEdit::DebugJSON(int edit_num, bool hex_key) const {
jw << "SubcompactionProgress" << subcompaction_progress_.ToString();
}
if (has_last_compacted_manifest_file_size_) {
jw << "LastCompactedManifestFileSize" << last_compacted_manifest_file_size_;
}
jw.EndObject();
return jw.Get();
+25
View File
@@ -74,6 +74,7 @@ enum Tag : uint32_t {
kWalDeletion2,
kPersistUserDefinedTimestamps,
kSubcompactionProgress,
kLastCompactedManifestFileSize,
};
enum SubcompactionProgressPerLevelCustomTag : uint32_t {
@@ -977,6 +978,11 @@ class VersionEdit {
bool IsColumnFamilyDrop() const { return is_column_family_drop_; }
void MarkForegroundOperation() { is_foreground_operation_ = true; }
bool IsForegroundOperation() const {
return is_foreground_operation_ || IsColumnFamilyManipulation();
}
void MarkNoManifestWriteDummy() { is_no_manifest_write_dummy_ = true; }
bool IsNoManifestWriteDummy() const { return is_no_manifest_write_dummy_; }
@@ -1016,6 +1022,21 @@ class VersionEdit {
subcompaction_progress_.Clear();
}
void SetLastCompactedManifestFileSize(uint64_t size) {
has_last_compacted_manifest_file_size_ = true;
last_compacted_manifest_file_size_ = size;
}
bool HasLastCompactedManifestFileSize() const {
return has_last_compacted_manifest_file_size_;
}
uint64_t GetLastCompactedManifestFileSize() const {
return last_compacted_manifest_file_size_;
}
// Recovery-time per-column-family edits only need to be written when they
// advance the CF's log number or carry some other manifest state.
bool ShouldEmitPerColumnFamilyRecoveryEdit(uint64_t current_log_number) const;
// return true on success.
// `ts_sz` is the size in bytes for the user-defined timestamp contained in
// a user key. This argument is optional because it's only required for
@@ -1095,6 +1116,7 @@ class VersionEdit {
// it also includes column family name.
bool is_column_family_drop_ = false;
bool is_column_family_add_ = false;
bool is_foreground_operation_ = false;
std::string column_family_name_;
uint32_t remaining_entries_ = 0;
@@ -1107,6 +1129,9 @@ class VersionEdit {
bool has_subcompaction_progress_ = false;
SubcompactionProgress subcompaction_progress_;
bool has_last_compacted_manifest_file_size_ = false;
uint64_t last_compacted_manifest_file_size_ = 0;
// Newly created table files and blob files are eligible for deletion if they
// are not registered as live files after the background jobs creating them
// have finished. In case committing the VersionEdit containing such changes
+7
View File
@@ -35,6 +35,7 @@ void VersionEditHandlerBase::Iterate(log::Reader& reader,
VersionEdit edit;
s = edit.DecodeFrom(record);
if (s.ok()) {
last_valid_record_end_ = reader.LastRecordEnd();
s = read_buffer_.AddEdit(&edit);
}
if (s.ok()) {
@@ -460,6 +461,7 @@ void VersionEditHandler::CheckIterationResult(const log::Reader& reader,
if (s->ok()) {
version_set_->manifest_file_size_ = reader.GetReadOffset();
assert(version_set_->manifest_file_size_ > 0);
version_set_->manifest_last_valid_record_end_ = last_valid_record_end_;
version_set_->next_file_number_.store(version_edit_params_.GetNextFile() +
1);
SequenceNumber last_seq = version_edit_params_.GetLastSequence();
@@ -640,6 +642,11 @@ Status VersionEditHandler::ExtractInfoFromVersionEdit(ColumnFamilyData* cfd,
version_edit_params_.GetLastSequence() <= edit.GetLastSequence());
version_edit_params_.SetLastSequence(edit.GetLastSequence());
}
if (edit.HasLastCompactedManifestFileSize()) {
version_set_->last_compacted_manifest_file_size_ =
edit.GetLastCompactedManifestFileSize();
version_set_->TuneMaxManifestFileSize();
}
if (!version_edit_params_.HasPrevLogNumber()) {
version_edit_params_.SetPrevLogNumber(0);
}
+6
View File
@@ -31,6 +31,8 @@ class VersionEditHandlerBase {
AtomicGroupReadBuffer& GetReadBuffer() { return read_buffer_; }
uint64_t GetLastValidRecordEnd() const { return last_valid_record_end_; }
protected:
explicit VersionEditHandlerBase(const ReadOptions& read_options,
uint64_t max_read_size)
@@ -52,6 +54,10 @@ class VersionEditHandlerBase {
const ReadOptions& read_options_;
// File offset at the end of the last successfully completed and
// decoded logical record.
uint64_t last_valid_record_end_ = 0;
private:
AtomicGroupReadBuffer read_buffer_;
const uint64_t max_manifest_read_size_;
+173 -26
View File
@@ -2243,8 +2243,10 @@ void Version::GetCreationTimeOfOldestFile(uint64_t* creation_time) {
uint64_t oldest_time = std::numeric_limits<uint64_t>::max();
for (int level = 0; level < storage_info_.num_non_empty_levels_; level++) {
for (FileMetaData* meta : storage_info_.LevelFiles(level)) {
assert(meta->fd.pinned_reader.Get() != nullptr);
uint64_t file_creation_time = meta->TryGetFileCreationTime();
TEST_SYNC_POINT_CALLBACK(
"Version::GetCreationTimeOfOldestFile::FileCreationTime",
&file_creation_time);
if (file_creation_time == kUnknownFileCreationTime) {
*creation_time = 0;
return;
@@ -4217,10 +4219,10 @@ void VersionStorageInfo::ComputeFilesMarkedForReadTriggeredCompaction(
return;
}
// Skip files at the last non-empty level — there is no lower level to
// Skip files at the last non-empty level -- there is no lower level to
// compact into. Exception: L0 files are allowed even when L0 is the last
// non-empty level, because in single-level universal or FIFO compaction, L0
// files can be compacted together (L0 → L0).
// files can be compacted together (L0 -> L0).
int last_non_empty = num_non_empty_levels_ - 1;
for (int level = 0; level < num_levels(); level++) {
@@ -5618,6 +5620,7 @@ VersionSet::VersionSet(
prev_log_number_(0),
current_version_number_(0),
manifest_file_size_(0),
manifest_last_valid_record_end_(0),
last_compacted_manifest_file_size_(0),
file_options_(storage_options),
block_cache_tracer_(block_cache_tracer),
@@ -5689,7 +5692,7 @@ Status VersionSet::Close(FSDirectory* db_dir, InstrumentedMutex* mu) {
content_manifest_name, fs_->OptimizeForManifestRead(file_options_),
&manifest_file, nullptr);
if (!content_io_s.ok()) {
// Surface I/O errors to the caller — users who call DB::Close() and
// Surface I/O errors to the caller -- users who call DB::Close() and
// check the status should know about filesystem problems.
s = content_io_s;
ROCKS_LOG_ERROR(db_options_->info_log,
@@ -5741,7 +5744,7 @@ Status VersionSet::Close(FSDirectory* db_dir, InstrumentedMutex* mu) {
corrupt_io_s.PermitUncheckedError();
io_error_info.io_status.PermitUncheckedError();
if (content_check == 0) {
// First check failed — rewrite and verify again
// First check failed -- rewrite and verify again
ROCKS_LOG_ERROR(db_options_->info_log,
"MANIFEST content verification on Close failed, "
"filename %s, rewriting manifest\n",
@@ -5752,7 +5755,7 @@ Status VersionSet::Close(FSDirectory* db_dir, InstrumentedMutex* mu) {
s = LogAndApply(cfd, ReadOptions(), WriteOptions(), &recovery_edit, mu,
db_dir);
} else {
// Rewritten manifest is also corrupt — likely a recurring filesystem
// Rewritten manifest is also corrupt -- likely a recurring filesystem
// issue. Surface it so DB::Close() callers can detect the problem.
ROCKS_LOG_ERROR(db_options_->info_log,
"MANIFEST content verification on Close failed again "
@@ -5827,6 +5830,7 @@ void VersionSet::Reset() {
current_version_number_ = 0;
manifest_writers_.clear();
manifest_file_size_ = 0;
manifest_last_valid_record_end_ = 0;
last_compacted_manifest_file_size_ = 0;
TuneMaxManifestFileSize();
obsolete_files_.clear();
@@ -6107,9 +6111,32 @@ Status VersionSet::ProcessManifestWrites(
uint64_t prev_manifest_file_size = manifest_file_size_;
assert(pending_manifest_file_number_ == 0);
bool has_foreground_operation = false;
for (const VersionEdit* e : batch_edits) {
if (e->IsForegroundOperation()) {
has_foreground_operation = true;
break;
}
}
// For MANIFEST write batches with any foreground operation (external file
// ingestion/import, DeleteFilesInRange(s), and column family manipulations
// like CreateColumnFamily and DropColumnFamily), relax the size limit by 25%
// to reduce the likelihood of a user operation blocking on MANIFEST rotation.
// Background-only batches (flush/compaction) still rotate at the normal
// threshold.
// TODO/future: for workloads like atomic-replace ingestion-only, with zero
// or few flushes and compactions, it might be nice to trigger background
// manifest rotation if we are beyond the soft limit. But the vast majority
// of workloads should have plenty of background manifest ops to avoid
// foreground rotation.
uint64_t enforced_limit = tuned_max_manifest_file_size_;
if (has_foreground_operation) {
uint64_t new_limit = enforced_limit + enforced_limit / 4;
// don't keep in case of overflow
enforced_limit = std::max(enforced_limit, new_limit);
}
if (!skip_manifest_write &&
(!descriptor_log_ ||
prev_manifest_file_size >= tuned_max_manifest_file_size_)) {
(!descriptor_log_ || prev_manifest_file_size >= enforced_limit)) {
TEST_SYNC_POINT("VersionSet::ProcessManifestWrites:BeforeNewManifest");
new_descriptor_log = true;
} else {
@@ -6161,11 +6188,7 @@ Status VersionSet::ProcessManifestWrites(
}
}
} else {
FileOptions opt_file_opts = fs_->OptimizeForManifestWrite(file_options_);
// DB option (in file_options_) takes precedence when not kUnknown
if (file_options_.temperature != Temperature::kUnknown) {
opt_file_opts.temperature = file_options_.temperature;
}
FileOptions opt_file_opts = GetFileOptionsForManifestWrite();
mu->Unlock();
TEST_SYNC_POINT("VersionSet::LogAndApply:WriteManifestStart");
TEST_SYNC_POINT_CALLBACK("VersionSet::LogAndApply:WriteManifest", nullptr);
@@ -6200,16 +6223,9 @@ Status VersionSet::ProcessManifestWrites(
io_s = NewWritableFile(fs_.get(), descriptor_fname, &descriptor_file,
opt_file_opts);
if (io_s.ok()) {
descriptor_file->SetPreallocationBlockSize(manifest_preallocation_size);
FileTypeSet tmp_set = db_options_->checksum_handoff_file_types;
std::unique_ptr<WritableFileWriter> file_writer(new WritableFileWriter(
std::move(descriptor_file), descriptor_fname, opt_file_opts, clock_,
io_tracer_, nullptr, Histograms::HISTOGRAM_ENUM_MAX /* hist_type */,
db_options_->listeners, nullptr,
tmp_set.Contains(FileType::kDescriptorFile),
tmp_set.Contains(FileType::kDescriptorFile)));
new_desc_log_ptr.reset(
new log::Writer(std::move(file_writer), 0, false));
new_desc_log_ptr =
CreateManifestWriter(std::move(descriptor_file), descriptor_fname,
opt_file_opts, manifest_preallocation_size);
raw_desc_log_ptr = new_desc_log_ptr.get();
s = WriteCurrentStateToManifest(write_options, curr_state,
wal_additions, raw_desc_log_ptr, io_s);
@@ -6259,6 +6275,7 @@ Status VersionSet::ProcessManifestWrites(
}
++idx;
#endif /* !NDEBUG */
TEST_SYNC_POINT("VersionSet::ProcessManifestWrites:AddRecord");
io_s = raw_desc_log_ptr->AddRecord(write_options, record);
if (!io_s.ok()) {
s = io_s;
@@ -6290,6 +6307,8 @@ Status VersionSet::ProcessManifestWrites(
io_s = SetCurrentFile(
write_options, fs_.get(), dbname_, pending_manifest_file_number_,
file_options_.temperature, dir_contains_current_file);
TEST_SYNC_POINT_CALLBACK(
"VersionSet::ProcessManifestWrites:AfterSetCurrentFile", &io_s);
if (!io_s.ok()) {
s = io_s;
// Quarantine old manifest file in case new manifest file's CURRENT
@@ -6680,6 +6699,106 @@ Status VersionSet::LogAndApplyHelper(ColumnFamilyData* cfd,
return builder ? builder->Apply(edit) : Status::OK();
}
FileOptions VersionSet::GetFileOptionsForManifestWrite() const {
FileOptions opt_file_opts = fs_->OptimizeForManifestWrite(file_options_);
// DB-configured temperature wins over the FS's default (when not kUnknown).
if (file_options_.temperature != Temperature::kUnknown) {
opt_file_opts.temperature = file_options_.temperature;
}
return opt_file_opts;
}
std::unique_ptr<log::Writer> VersionSet::CreateManifestWriter(
std::unique_ptr<FSWritableFile> file, const std::string& fname,
const FileOptions& opts, uint64_t preallocation_size,
uint64_t initial_file_size) const {
file->SetPreallocationBlockSize(preallocation_size);
FileTypeSet tmp_set = db_options_->checksum_handoff_file_types;
auto file_writer = std::make_unique<WritableFileWriter>(
std::move(file), fname, opts, clock_, io_tracer_,
/*stats=*/nullptr, Histograms::HISTOGRAM_ENUM_MAX, db_options_->listeners,
/*file_checksum_gen_factory=*/nullptr,
tmp_set.Contains(FileType::kDescriptorFile),
tmp_set.Contains(FileType::kDescriptorFile), initial_file_size);
const size_t block_offset = initial_file_size % log::kBlockSize;
return std::make_unique<log::Writer>(
std::move(file_writer), /*log_number=*/0, /*recycle_log_files=*/false,
/*manual_flush=*/false, kNoCompression,
/*track_and_verify_wals=*/false, block_offset);
}
Status VersionSet::ReopenManifestForAppend(const std::string& manifest_path) {
assert(db_options_->reuse_manifest_on_open);
assert(manifest_last_valid_record_end_ > 0);
// Disabled under best_efforts_recovery: that mode rebuilds the
// MANIFEST + CURRENT from scratch via the side-effect of
// LogAndApplyForRecovery emitting an edit. Reusing the (possibly
// stale) prior MANIFEST contradicts the salvage contract.
if (db_options_->best_efforts_recovery) {
return Status::OK();
}
FileOptions opt_file_opts = GetFileOptionsForManifestWrite();
// Bail if the on-disk size diverges from what Recover's Reader
// consumed -- likely a torn tail from a prior crash; mid-block append
// would mis-frame the record stream.
uint64_t physical_size = 0;
IOStatus stat_s = fs_->GetFileSize(manifest_path, IOOptions(), &physical_size,
/*dbg=*/nullptr);
if (!stat_s.ok() || physical_size != manifest_last_valid_record_end_) {
ROCKS_LOG_WARN(db_options_->info_log,
"reuse_manifest_on_open: physical size %" PRIu64
" != last valid record end %" PRIu64
" (tail corruption?); falling back to fresh MANIFEST",
physical_size, manifest_last_valid_record_end_);
return Status::OK();
}
// Direct-writes path is hard to reason about for an already-populated
// file (alignment + tail-pad). POSIX's OptimizeForManifestWrite forces
// it off, but custom FileSystems may not.
if (opt_file_opts.use_direct_writes) {
ROCKS_LOG_WARN(db_options_->info_log,
"reuse_manifest_on_open: direct writes enabled; "
"falling back to fresh MANIFEST");
return Status::OK();
}
std::unique_ptr<FSWritableFile> manifest_file;
IOStatus io_s = fs_->ReopenWritableFile(manifest_path, opt_file_opts,
&manifest_file, /*dbg=*/nullptr);
if (!io_s.ok()) {
ROCKS_LOG_WARN(db_options_->info_log,
"Failed to reopen MANIFEST for append: %s",
io_s.ToString().c_str());
return Status::OK();
}
const uint64_t reopened_size =
manifest_file->GetFileSize(opt_file_opts.io_options, /*dbg=*/nullptr);
if (reopened_size != manifest_last_valid_record_end_) {
ROCKS_LOG_WARN(db_options_->info_log,
"reuse_manifest_on_open: reopened handle size %" PRIu64
" != last valid record end %" PRIu64
"; falling back to fresh MANIFEST",
reopened_size, manifest_last_valid_record_end_);
return Status::OK();
}
descriptor_log_ = CreateManifestWriter(
std::move(manifest_file), manifest_path, opt_file_opts,
manifest_preallocation_size_, reopened_size);
ROCKS_LOG_INFO(db_options_->info_log,
"Reusing existing MANIFEST file: %s (valid data size: %" PRIu64
")",
manifest_path.c_str(), manifest_last_valid_record_end_);
TEST_SYNC_POINT("VersionSet::ReopenManifestForAppend:Reopened");
return Status::OK();
}
Status VersionSet::Recover(
const std::vector<ColumnFamilyDescriptor>& column_families, bool read_only,
std::string* db_id, bool no_error_if_files_missing, bool is_retry,
@@ -6767,6 +6886,11 @@ Status VersionSet::Recover(
}
}
if (s.ok() && !read_only && db_options_->reuse_manifest_on_open &&
manifest_last_valid_record_end_ > 0) {
s = ReopenManifestForAppend(manifest_path);
}
return s;
}
@@ -7397,6 +7521,28 @@ Status VersionSet::WriteCurrentStateToManifest(
}
}
}
// Record the approximate compacted manifest size so it's available at
// recovery time for TuneMaxManifestFileSize(). This record must come last
// in WriteCurrentStateToManifest for an accurate size estimate.
{
// Include a rough estimate of this record's own size (~15 bytes for the
// VersionEdit payload + log record header).
constexpr uint64_t kEstimatedRecordOverhead = 15;
VersionEdit edit;
edit.SetLastCompactedManifestFileSize(log->file()->GetFileSize() +
kEstimatedRecordOverhead);
std::string record;
if (!edit.EncodeTo(&record)) {
return Status::Corruption("Unable to Encode VersionEdit:" +
edit.DebugString(true));
}
io_s = log->AddRecord(write_options, record);
if (!io_s.ok()) {
return io_s;
}
}
return Status::OK();
}
@@ -8064,11 +8210,12 @@ ReactiveVersionSet::ReactiveVersionSet(
const MutableDBOptions& mutable_db_options,
const FileOptions& _file_options, Cache* table_cache,
WriteBufferManager* write_buffer_manager, WriteController* write_controller,
const std::shared_ptr<IOTracer>& io_tracer)
const std::shared_ptr<IOTracer>& io_tracer, const std::string& db_id,
const std::string& db_session_id)
: VersionSet(dbname, imm_db_options, mutable_db_options, _file_options,
table_cache, write_buffer_manager, write_controller,
/*block_cache_tracer=*/nullptr, io_tracer, /*db_id*/ "",
/*db_session_id*/ "", /*daily_offpeak_time_utc*/ "",
/*block_cache_tracer=*/nullptr, io_tracer, db_id,
db_session_id, /*daily_offpeak_time_utc*/ "",
/*error_handler=*/nullptr, /*unchanging=*/false) {}
ReactiveVersionSet::~ReactiveVersionSet() = default;
+41 -1
View File
@@ -1719,6 +1719,33 @@ class VersionSet {
const std::unordered_map<uint32_t, MutableCFState>& curr_state,
const VersionEdit& wal_additions, log::Writer* log, IOStatus& io_s);
// Reopen the existing MANIFEST file for append at the end of Recover()
// when reuse_manifest_on_open is set, so the next LogAndApply appends
// to it instead of creating a fresh MANIFEST. Falls back to OK
// (descriptor_log_ stays null, next LogAndApply creates a fresh
// MANIFEST as before) if ReopenWritableFile fails.
Status ReopenManifestForAppend(const std::string& manifest_path);
// FileOptions for MANIFEST writes -- applies the FS's
// OptimizeForManifestWrite tuning, then re-applies the user-configured
// temperature so a custom FS can't override it.
FileOptions GetFileOptionsForManifestWrite() const;
// Create a log::Writer over the given FSWritableFile with all standard
// MANIFEST setup applied (preallocation block size, checksum-handoff
// classification, listeners, etc.). preallocation_size should be a
// snapshot of manifest_preallocation_size_ taken under the DB mutex
// (the fresh-path call site reads the field before releasing mu).
// When initial_file_size > 0 the writer is treated as resuming over
// an already-populated file: WritableFileWriter is constructed with the
// existing size so size accounting is correct, and the log writer's
// initial_block_offset aligns to the existing file's tail within the
// current 32 KiB block.
std::unique_ptr<log::Writer> CreateManifestWriter(
std::unique_ptr<FSWritableFile> file, const std::string& fname,
const FileOptions& opts, uint64_t preallocation_size,
uint64_t initial_file_size = 0) const;
void AppendVersion(ColumnFamilyData* column_family_data, Version* v);
ColumnFamilyData* CreateColumnFamily(const ColumnFamilyOptions& cf_options,
@@ -1788,6 +1815,17 @@ class VersionSet {
// Current size of manifest file
uint64_t manifest_file_size_;
// File offset at the end of the last successfully completed logical
// record during MANIFEST recovery. Unlike manifest_file_size_ (the
// reader's I/O high-water mark, which includes any tolerated tail
// garbage), this value points to the byte after the last valid record.
// Used by ReopenManifestForAppend to detect intra-block tail
// corruption that doesn't extend the physical file size.
// manifest_file_size_ is kept separate because it is used for
// rotation decisions (ProcessManifestWrites), close-time verification
// (Close), and backup metadata.
uint64_t manifest_last_valid_record_end_;
// Size of the populated manifest file last time it was re-written from
// scratch.
uint64_t last_compacted_manifest_file_size_;
@@ -1860,7 +1898,9 @@ class ReactiveVersionSet : public VersionSet {
const FileOptions& _file_options, Cache* table_cache,
WriteBufferManager* write_buffer_manager,
WriteController* write_controller,
const std::shared_ptr<IOTracer>& io_tracer);
const std::shared_ptr<IOTracer>& io_tracer,
const std::string& db_id,
const std::string& db_session_id);
~ReactiveVersionSet() override;
+6 -6
View File
@@ -1203,7 +1203,7 @@ class VersionSetTestBase {
reactive_versions_ = std::make_shared<ReactiveVersionSet>(
dbname_, &imm_db_options_, mutable_db_options_, env_options_,
table_cache_.get(), &write_buffer_manager_, &write_controller_,
nullptr);
/*io_tracer=*/nullptr, /*db_id=*/"", /*db_session_id=*/"");
imm_db_options_.db_paths.emplace_back(dbname_,
std::numeric_limits<uint64_t>::max());
}
@@ -2440,7 +2440,7 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseClean) {
// Verify content validation actually ran
ASSERT_TRUE(content_validation_ran);
// No corruption — counter should be zero
// No corruption -- counter should be zero
ASSERT_EQ(0, stats->getTickerCount(MANIFEST_VALIDATION_FAILURE_COUNT));
// Manifest path should be unchanged (no rotation)
@@ -2513,7 +2513,7 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseCorruptRecord) {
TEST_F(VersionSetTest, ManifestContentValidationOnCloseDisabled) {
// Default (option disabled), corrupt manifest after writer close,
// verify no rotation occurred — corrupt manifest persists.
// verify no rotation occurred -- corrupt manifest persists.
NewDB();
auto stats = CreateDBStatistics();
imm_db_options_.statistics = stats;
@@ -2538,7 +2538,7 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseDisabled) {
ASSERT_FALSE(content_validation_ran);
// Validation disabled — counter should be zero
// Validation disabled -- counter should be zero
ASSERT_EQ(0, stats->getTickerCount(MANIFEST_VALIDATION_FAILURE_COUNT));
// Manifest path should be unchanged (no rotation since validation is off)
@@ -2585,7 +2585,7 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseSizeCheckFails) {
SyncPoint::GetInstance()->DisableProcessing();
// Size check caught the issue; content validation on rewritten manifest
// should pass — no content validation failure recorded
// should pass -- no content validation failure recorded
ASSERT_EQ(0, stats->getTickerCount(MANIFEST_VALIDATION_FAILURE_COUNT));
// Size check should have triggered rotation
@@ -2697,7 +2697,7 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseOpenFails) {
ASSERT_TRUE(close_s.IsIOError()) << close_s.ToString();
// File couldn't be opened — no content validation ran
// File couldn't be opened -- no content validation ran
ASSERT_EQ(0, stats->getTickerCount(MANIFEST_VALIDATION_FAILURE_COUNT));
}
+398 -2
View File
@@ -30,6 +30,10 @@ class DBWideBasicTest : public DBTestBase {
return wide_column_test_util::GetOptionsForBlobTest(GetDefaultOptions());
}
Options GetDirectWriteOptions() {
return wide_column_test_util::GetDirectWriteOptions(GetDefaultOptions());
}
// Helper: runs the EntityBlobAfterFlush test logic with the given options.
void RunEntityBlobAfterFlush(const Options& options);
@@ -2159,6 +2163,57 @@ TEST_F(DBWideBasicTest, MultiGetEntityWithBlobResolution) {
}
}
TEST_F(DBWideBasicTest,
MultiGetBlobBackedEntityDirectWriteMemtableBatchLookup) {
// Goal: force the memtable batch MultiGet optimization to read a direct-write
// blob-backed entity from the active memtable. One key exercises merge
// resolution against a blob-backed entity base, which requires Saver to carry
// the BlobFetcher through the batched callback path. The second key proves
// the non-merge default-column path still resolves correctly in the same
// batched request.
Options options = GetDirectWriteOptions();
options.min_blob_size = 50;
options.memtable_batch_lookup_optimization = true;
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
DestroyAndReopen(options);
const std::string merge_key = "multiget_direct_write_merge_key";
const std::string plain_key = "multiget_direct_write_plain_key";
const std::string default_value = GenerateLargeValue(100, 'd');
const std::string other_default_value = GenerateLargeValue(110, 'p');
const std::string large_value = GenerateLargeValue(120, 'l');
const std::string small_value = GenerateSmallValue();
const std::string merge_operand = "tail";
WideColumns merge_columns{{kDefaultWideColumnName, default_value},
{"col_large", large_value},
{"col_small", small_value}};
WideColumns plain_columns{{kDefaultWideColumnName, other_default_value},
{"col_large", large_value},
{"col_small", small_value}};
ASSERT_OK(db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(),
merge_key, merge_columns));
ASSERT_OK(db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(),
plain_key, plain_columns));
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), merge_key,
merge_operand));
std::array<Slice, 2> keys{{merge_key, plain_key}};
std::array<PinnableSlice, 2> values;
std::array<Status, 2> statuses;
db_->MultiGet(ReadOptions(), db_->DefaultColumnFamily(), keys.size(),
keys.data(), values.data(), statuses.data());
ASSERT_OK(statuses[0]);
ASSERT_EQ(values[0], default_value + "|" + merge_operand);
ASSERT_OK(statuses[1]);
ASSERT_EQ(values[1], other_default_value);
}
void DBWideBasicTest::RunEntityBlobAfterFlush(const Options& options) {
Reopen(options);
@@ -2776,7 +2831,7 @@ TEST_F(DBWideBasicTest, EntityBlobBlockCacheTierGet) {
// Reopen to clear any caches
Reopen(options);
// Read with kBlockCacheTier — blob is not in cache
// Read with kBlockCacheTier -- blob is not in cache
ReadOptions read_opts;
read_opts.read_tier = kBlockCacheTier;
PinnableSlice result;
@@ -2909,6 +2964,347 @@ TEST_F(DBWideBasicTest, MergeEntityWithBlobColumns) {
}
}
TEST_F(DBWideBasicTest, GetMergeOperandsWithBlobBackedEntityDefaultColumn) {
// Goal: cover both GetMergeOperands code paths that read a compacted V2
// wide-column entity whose default column was moved to a blob file. The
// first read exercises the base-value path directly, then a merge operand is
// added so the second read exercises the merge-plus-base path.
Options options = GetBlobTestOptions();
options.min_blob_size = 50;
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
DestroyAndReopen(options);
const std::string key = "merge_operands_blob_entity";
const std::string default_value = GenerateLargeValue(100, 'd');
const std::string large_value = GenerateLargeValue(120, 'l');
const std::string small_value = GenerateSmallValue();
const std::string merge_operand = "suffix";
WideColumns columns{{kDefaultWideColumnName, default_value},
{"col_large", large_value},
{"col_small", small_value}};
ASSERT_OK(
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
ASSERT_OK(Flush());
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
ASSERT_FALSE(GetBlobFileNumbers().empty());
{
// The compacted V2 entity becomes the first operand when there are no
// newer merge operands above it.
GetMergeOperandsOptions get_merge_opts;
get_merge_opts.expected_max_number_of_operands = 1;
std::array<PinnableSlice, 2> merge_operands;
int number_of_operands = 0;
ASSERT_OK(db_->GetMergeOperands(ReadOptions(), db_->DefaultColumnFamily(),
key, merge_operands.data(), &get_merge_opts,
&number_of_operands));
ASSERT_EQ(number_of_operands, 1);
ASSERT_EQ(merge_operands[0], default_value);
}
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), key,
merge_operand));
{
// After a merge is added, GetMergeOperands must still resolve the blob
// backed base default column while traversing the older base entry below
// the newer merge operand.
GetMergeOperandsOptions get_merge_opts;
get_merge_opts.expected_max_number_of_operands = 2;
std::array<PinnableSlice, 2> merge_operands;
int number_of_operands = 0;
ASSERT_OK(db_->GetMergeOperands(ReadOptions(), db_->DefaultColumnFamily(),
key, merge_operands.data(), &get_merge_opts,
&number_of_operands));
ASSERT_EQ(number_of_operands, 2);
ASSERT_EQ(merge_operands[0], default_value);
ASSERT_EQ(merge_operands[1], merge_operand);
}
}
TEST_F(DBWideBasicTest,
GetMergeOperandsWithBlobBackedEntityDefaultColumnDirectWriteMemtable) {
// Goal: cover the pre-flush memtable path when blob direct write stores a
// blob-backed V2 entity in memory. The first GetMergeOperands call exercises
// the no-merge base-operand path. After adding a merge, DB::Get exercises
// merge resolution against the same memtable entity, and a second
// GetMergeOperands call exercises the merge-plus-base path.
Options options = GetDirectWriteOptions();
options.min_blob_size = 50;
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
DestroyAndReopen(options);
const std::string key = "direct_write_merge_operands_blob_entity";
const std::string default_value = GenerateLargeValue(100, 'd');
const std::string large_value = GenerateLargeValue(120, 'l');
const std::string small_value = GenerateSmallValue();
const std::string merge_operand = "suffix";
const std::string expected_merged = default_value + "|" + merge_operand;
WideColumns columns{{kDefaultWideColumnName, default_value},
{"col_large", large_value},
{"col_small", small_value}};
ASSERT_OK(
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
{
GetMergeOperandsOptions get_merge_opts;
get_merge_opts.expected_max_number_of_operands = 1;
std::array<PinnableSlice, 2> merge_operands;
int number_of_operands = 0;
ASSERT_OK(db_->GetMergeOperands(ReadOptions(), db_->DefaultColumnFamily(),
key, merge_operands.data(), &get_merge_opts,
&number_of_operands));
ASSERT_EQ(number_of_operands, 1);
ASSERT_EQ(merge_operands[0], default_value);
}
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), key,
merge_operand));
{
PinnableSlice result;
ASSERT_OK(
db_->Get(ReadOptions(), db_->DefaultColumnFamily(), key, &result));
ASSERT_EQ(result, expected_merged);
}
{
GetMergeOperandsOptions get_merge_opts;
get_merge_opts.expected_max_number_of_operands = 2;
std::array<PinnableSlice, 2> merge_operands;
int number_of_operands = 0;
ASSERT_OK(db_->GetMergeOperands(ReadOptions(), db_->DefaultColumnFamily(),
key, merge_operands.data(), &get_merge_opts,
&number_of_operands));
ASSERT_EQ(number_of_operands, 2);
ASSERT_EQ(merge_operands[0], default_value);
ASSERT_EQ(merge_operands[1], merge_operand);
}
}
TEST_F(DBWideBasicTest,
GetAndGetEntityWithBlobBackedDefaultColumnDirectWriteMemtableReadOnly) {
// Goal: cover the read-only reopen path after writing a blob-backed
// direct-write entity without manually flushing it first. The test checks
// both `Get()`, which must resolve the default-column blob reference, and
// `GetEntity()`, which must eagerly resolve the entity's unresolved blob
// columns before returning them to the caller.
Options options = GetDirectWriteOptions();
options.min_blob_size = 50;
DestroyAndReopen(options);
const std::string key = "readonly_direct_write_memtable_entity";
const std::string default_value = GenerateLargeValue(100, 'd');
const std::string large_value = GenerateLargeValue(120, 'l');
const std::string small_value = GenerateSmallValue();
WideColumns columns{{kDefaultWideColumnName, default_value},
{"col_large", large_value},
{"col_small", small_value}};
ASSERT_OK(
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
Close();
options.avoid_flush_during_recovery = true;
ASSERT_OK(ReadOnlyReopen(options));
{
PinnableSlice result;
ASSERT_OK(
db_->Get(ReadOptions(), db_->DefaultColumnFamily(), key, &result));
ASSERT_EQ(result, default_value);
}
{
PinnableWideColumns result;
ASSERT_OK(db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), key,
&result));
ASSERT_EQ(result.columns(), columns);
}
}
TEST_F(DBWideBasicTest, ReadOnlyDirectWriteMemtableBlobBlockCacheTier) {
// Goal: cover the read-only memtable path under kBlockCacheTier. The test
// keeps a blob-backed direct-write entity in WAL-backed recovery state so
// resolving either Get() or GetEntity() would require blob I/O, which must
// surface as Incomplete rather than silently reading the blob file.
Options options = GetDirectWriteOptions();
options.min_blob_size = 50;
DestroyAndReopen(options);
const std::string key = "readonly_direct_write_memtable_block_cache_tier";
const std::string default_value = GenerateLargeValue(100, 'd');
const std::string large_value = GenerateLargeValue(120, 'l');
const std::string small_value = GenerateSmallValue();
WideColumns columns{{kDefaultWideColumnName, default_value},
{"col_large", large_value},
{"col_small", small_value}};
ASSERT_OK(
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
Close();
options.avoid_flush_during_recovery = true;
ASSERT_OK(ReadOnlyReopen(options));
ReadOptions read_opts;
read_opts.read_tier = kBlockCacheTier;
{
PinnableSlice result;
const Status s =
db_->Get(read_opts, db_->DefaultColumnFamily(), key, &result);
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
ASSERT_TRUE(result.empty());
}
{
PinnableWideColumns result;
const Status s =
db_->GetEntity(read_opts, db_->DefaultColumnFamily(), key, &result);
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
ASSERT_TRUE(result.columns().empty());
}
}
TEST_F(DBWideBasicTest,
GetMergeOperandsWithBlobBackedEntityDefaultColumnReadOnly) {
// Goal: exercise OpenForReadOnly on a blob-backed V2 entity base in SST with
// a newer merge operand in a separate SST. The read-only DB must resolve the
// blob-backed default column for both Get() and GetMergeOperands() without
// relying on mutable memtable state.
Options options = GetBlobTestOptions();
options.min_blob_size = 50;
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
DestroyAndReopen(options);
const std::string key = "readonly_merge_operands_blob_entity";
const std::string default_value = GenerateLargeValue(100, 'd');
const std::string large_value = GenerateLargeValue(120, 'l');
const std::string small_value = GenerateSmallValue();
const std::string merge_operand = "suffix";
const std::string expected_merged = default_value + "|" + merge_operand;
WideColumns columns{{kDefaultWideColumnName, default_value},
{"col_large", large_value},
{"col_small", small_value}};
ASSERT_OK(
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
ASSERT_OK(Flush());
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
ASSERT_FALSE(GetBlobFileNumbers().empty());
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), key,
merge_operand));
ASSERT_OK(Flush());
Close();
ASSERT_OK(ReadOnlyReopen(options));
{
PinnableSlice result;
ASSERT_OK(
db_->Get(ReadOptions(), db_->DefaultColumnFamily(), key, &result));
ASSERT_EQ(result, expected_merged);
}
{
GetMergeOperandsOptions get_merge_opts;
get_merge_opts.expected_max_number_of_operands = 2;
std::array<PinnableSlice, 2> merge_operands;
int number_of_operands = 0;
ASSERT_OK(db_->GetMergeOperands(ReadOptions(), db_->DefaultColumnFamily(),
key, merge_operands.data(), &get_merge_opts,
&number_of_operands));
ASSERT_EQ(number_of_operands, 2);
ASSERT_EQ(merge_operands[0], default_value);
ASSERT_EQ(merge_operands[1], merge_operand);
}
}
TEST_F(DBWideBasicTest,
GetMergeOperandsWithBlobBackedEntityDefaultColumnBlockCacheTier) {
// Goal: prove the SST-backed GetMergeOperands path propagates Incomplete
// when resolving a blob-backed default column would require I/O. One key
// covers the direct base-entity path, and the other covers the merge-plus-
// base path with the merge operand flushed to a newer SST.
Options options = GetBlobTestOptions();
options.min_blob_size = 50;
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
DestroyAndReopen(options);
const std::string base_key = "merge_operands_blob_entity_cache_base";
const std::string merge_key = "merge_operands_blob_entity_cache_merge";
const std::string default_value = GenerateLargeValue(100, 'd');
const std::string large_value = GenerateLargeValue(120, 'l');
const std::string small_value = GenerateSmallValue();
WideColumns columns{{kDefaultWideColumnName, default_value},
{"col_large", large_value},
{"col_small", small_value}};
ASSERT_OK(db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), base_key,
columns));
ASSERT_OK(db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(),
merge_key, columns));
ASSERT_OK(Flush());
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), merge_key,
"tail"));
ASSERT_OK(Flush());
// Reopen to clear any cached blob readers/values so kBlockCacheTier must
// report Incomplete instead of succeeding through a warm cache.
Reopen(options);
ReadOptions read_opts;
read_opts.read_tier = kBlockCacheTier;
{
GetMergeOperandsOptions get_merge_opts;
get_merge_opts.expected_max_number_of_operands = 1;
std::array<PinnableSlice, 2> merge_operands;
int number_of_operands = 0;
const Status s = db_->GetMergeOperands(
read_opts, db_->DefaultColumnFamily(), base_key, merge_operands.data(),
&get_merge_opts, &number_of_operands);
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
}
{
GetMergeOperandsOptions get_merge_opts;
get_merge_opts.expected_max_number_of_operands = 2;
std::array<PinnableSlice, 2> merge_operands;
int number_of_operands = 0;
const Status s = db_->GetMergeOperands(
read_opts, db_->DefaultColumnFamily(), merge_key, merge_operands.data(),
&get_merge_opts, &number_of_operands);
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
}
}
TEST_F(DBWideBasicTest, MergeEntityWithBlobColumnsNoDefault) {
// Test: Merge on a V2 entity without a default column. The merge result
// should produce a valid entity with all original columns plus a default
@@ -2998,7 +3394,7 @@ TEST_F(DBWideBasicTest, MergeEntityWithBlobColumnsBlockCacheTier) {
// Reopen to clear caches
Reopen(options);
// Read with kBlockCacheTier — blob not in cache, merge needs blob resolution
// Read with kBlockCacheTier -- blob not in cache, merge needs blob resolution
ReadOptions read_opts;
read_opts.read_tier = kBlockCacheTier;
PinnableSlice result;
+1 -1
View File
@@ -59,7 +59,7 @@ Status ReadPathBlobResolver::ResolveColumn(size_t column_index,
blob_resolver_util::FindBlobColumn(blob_columns_, column_index);
if (blob_index_ptr == nullptr) {
// Inline column — return the value directly
// Inline column -- return the value directly
*resolved_value = (*columns_)[column_index].value();
} else {
// Check if already resolved
+4
View File
@@ -153,6 +153,8 @@ class WideColumnSerialization {
// value() is initially the serialized BlobIndex bytes from the entity.
// blob_columns: receives (column_index, blob_index) pairs identifying which
// entries in `columns` are blob references and their decoded BlobIndex data.
// For valid input, this must agree with HasBlobColumns() about whether blob
// column references are present.
static Status DeserializeV2(
Slice& input, std::vector<WideColumn>& columns,
std::vector<std::pair<size_t, BlobIndex>>& blob_columns);
@@ -160,6 +162,8 @@ class WideColumnSerialization {
// Check if the serialized entity has any blob column references.
// Sets *has_blob_columns to true if version >= 2 and at least one column
// has blob type; false otherwise.
// For valid input, this must agree with DeserializeV2() about whether blob
// column references are present.
// Returns Status::Corruption on decode errors.
static Status HasBlobColumns(const Slice& input, bool& has_blob_columns);
+2 -2
View File
@@ -391,7 +391,7 @@ static BlobIndex MakeBlobIndex(uint64_t file_number, uint64_t offset,
return bi;
}
// Helper: V2 serialize → DeserializeV2 round-trip, returning
// Helper: V2 serialize -> DeserializeV2 round-trip, returning
// deserialized columns and blob column info.
static void V2SerializeAndDeserialize(
const std::vector<std::pair<std::string, std::string>>& columns,
@@ -798,7 +798,7 @@ TEST_F(WideColumnSerializationTest, RandomizedSerializeDeserializeRoundTrip) {
}
}
// V2 serialize → DeserializeV2 round-trip
// V2 serialize -> DeserializeV2 round-trip
std::string serialized;
std::vector<WideColumn> deserialized;
std::vector<std::pair<size_t, BlobIndex>> blob_out;
+8 -2
View File
@@ -13,7 +13,9 @@
namespace ROCKSDB_NAMESPACE {
class BatchedOpsStressTest : public StressTest {
public:
BatchedOpsStressTest() = default;
BatchedOpsStressTest(int db_index, const std::string& db_path,
const std::string& ev_path, const std::string& sec_path)
: StressTest(db_index, db_path, ev_path, sec_path) {}
virtual ~BatchedOpsStressTest() = default;
bool IsStateTracked() const override { return false; }
@@ -722,7 +724,11 @@ class BatchedOpsStressTest : public StressTest {
}
};
StressTest* CreateBatchedOpsStressTest() { return new BatchedOpsStressTest(); }
StressTest* CreateBatchedOpsStressTest(int db_index, const std::string& db_path,
const std::string& ev_path,
const std::string& sec_path) {
return new BatchedOpsStressTest(db_index, db_path, ev_path, sec_path);
}
} // namespace ROCKSDB_NAMESPACE
#endif // GFLAGS
+21 -15
View File
@@ -21,7 +21,10 @@
namespace ROCKSDB_NAMESPACE {
class CfConsistencyStressTest : public StressTest {
public:
CfConsistencyStressTest() : batch_id_(0) {}
CfConsistencyStressTest(int db_index, const std::string& db_path,
const std::string& ev_path,
const std::string& sec_path)
: StressTest(db_index, db_path, ev_path, sec_path), batch_id_(0) {}
~CfConsistencyStressTest() override = default;
@@ -227,10 +230,10 @@ class CfConsistencyStressTest : public StressTest {
key, &value0);
// Temporarily disable error injection for verification
if (fault_fs_guard) {
fault_fs_guard->DisableThreadLocalErrorInjection(
if (db_fault_injection_fs_) {
db_fault_injection_fs_->DisableThreadLocalErrorInjection(
FaultInjectionIOType::kRead);
fault_fs_guard->DisableThreadLocalErrorInjection(
db_fault_injection_fs_->DisableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
}
@@ -279,10 +282,10 @@ class CfConsistencyStressTest : public StressTest {
}
// Enable back error injection disabled for verification
if (fault_fs_guard) {
fault_fs_guard->EnableThreadLocalErrorInjection(
if (db_fault_injection_fs_) {
db_fault_injection_fs_->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kRead);
fault_fs_guard->EnableThreadLocalErrorInjection(
db_fault_injection_fs_->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
}
db_->ReleaseSnapshot(snapshot);
@@ -397,10 +400,10 @@ class CfConsistencyStressTest : public StressTest {
&cmp_result);
// Temporarily disable error injection for verification
if (fault_fs_guard) {
fault_fs_guard->DisableThreadLocalErrorInjection(
if (db_fault_injection_fs_) {
db_fault_injection_fs_->DisableThreadLocalErrorInjection(
FaultInjectionIOType::kRead);
fault_fs_guard->DisableThreadLocalErrorInjection(
db_fault_injection_fs_->DisableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
}
@@ -559,10 +562,10 @@ class CfConsistencyStressTest : public StressTest {
}
// Enable back error injection disabled for verification
if (fault_fs_guard) {
fault_fs_guard->EnableThreadLocalErrorInjection(
if (db_fault_injection_fs_) {
db_fault_injection_fs_->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kRead);
fault_fs_guard->EnableThreadLocalErrorInjection(
db_fault_injection_fs_->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
}
}
@@ -1543,8 +1546,11 @@ class CfConsistencyStressTest : public StressTest {
std::deque<DebugEvent> recent_debug_events_;
};
StressTest* CreateCfConsistencyStressTest() {
return new CfConsistencyStressTest();
StressTest* CreateCfConsistencyStressTest(int db_index,
const std::string& db_path,
const std::string& ev_path,
const std::string& sec_path) {
return new CfConsistencyStressTest(db_index, db_path, ev_path, sec_path);
}
} // namespace ROCKSDB_NAMESPACE
+39 -45
View File
@@ -13,16 +13,17 @@
#include <cmath>
#include "db_stress_tool/db_stress_test_base.h"
#include "file/file_util.h"
#include "rocksdb/secondary_cache.h"
#include "util/file_checksum_helper.h"
#include "util/xxhash.h"
ROCKSDB_NAMESPACE::Env* db_stress_listener_env = nullptr;
ROCKSDB_NAMESPACE::Env* db_stress_env = nullptr;
std::shared_ptr<ROCKSDB_NAMESPACE::FaultInjectionTestFS> fault_fs_guard;
ROCKSDB_NAMESPACE::Env* raw_env = nullptr;
std::shared_ptr<ROCKSDB_NAMESPACE::SecondaryCache> compressed_secondary_cache;
std::shared_ptr<ROCKSDB_NAMESPACE::Cache> block_cache;
std::shared_ptr<ROCKSDB_NAMESPACE::WriteBufferManager> wbm;
std::shared_ptr<ROCKSDB_NAMESPACE::RateLimiter> rate_limiter;
enum ROCKSDB_NAMESPACE::CompressionType compression_type_e =
ROCKSDB_NAMESPACE::kSnappyCompression;
enum ROCKSDB_NAMESPACE::CompressionType bottommost_compression_type_e =
@@ -114,10 +115,10 @@ void PoolSizeChangeThread(void* v) {
if (new_thread_pool_size < 1) {
new_thread_pool_size = 1;
}
db_stress_env->SetBackgroundThreads(new_thread_pool_size,
ROCKSDB_NAMESPACE::Env::Priority::LOW);
raw_env->SetBackgroundThreads(new_thread_pool_size,
ROCKSDB_NAMESPACE::Env::Priority::LOW);
// Sleep up to 3 seconds
db_stress_env->SleepForMicroseconds(
raw_env->SleepForMicroseconds(
thread->rand.Next() % FLAGS_compaction_thread_pool_adjust_interval *
1000 +
1);
@@ -144,7 +145,7 @@ void DbVerificationThread(void* v) {
if (!shared->HasVerificationFailedYet()) {
stress_test->ContinuouslyVerifyDb(thread);
}
db_stress_env->SleepForMicroseconds(
raw_env->SleepForMicroseconds(
thread->rand.Next() % FLAGS_continuous_verification_interval * 1000 +
1);
}
@@ -166,7 +167,7 @@ void CompressedCacheSetCapacityThread(void* v) {
return;
}
}
db_stress_env->SleepForMicroseconds(FLAGS_secondary_cache_update_interval);
raw_env->SleepForMicroseconds(FLAGS_secondary_cache_update_interval);
if (FLAGS_compressed_secondary_cache_size > 0) {
Status s = compressed_secondary_cache->SetCapacity(0);
size_t capacity;
@@ -174,7 +175,7 @@ void CompressedCacheSetCapacityThread(void* v) {
s = compressed_secondary_cache->GetCapacity(capacity);
assert(capacity == 0);
}
db_stress_env->SleepForMicroseconds(10 * 1000 * 1000);
raw_env->SleepForMicroseconds(10 * 1000 * 1000);
if (s.ok()) {
s = compressed_secondary_cache->SetCapacity(
FLAGS_compressed_secondary_cache_size);
@@ -201,7 +202,7 @@ void CompressedCacheSetCapacityThread(void* v) {
block_cache->SetCapacity(capacity - adjustment);
fprintf(stdout, "New cache capacity = %zu\n",
block_cache->GetCapacity());
db_stress_env->SleepForMicroseconds(10 * 1000 * 1000);
raw_env->SleepForMicroseconds(10 * 1000 * 1000);
block_cache->SetCapacity(capacity);
} else {
Status s;
@@ -214,7 +215,7 @@ void CompressedCacheSetCapacityThread(void* v) {
s = UpdateTieredCache(block_cache, /*capacity*/ -1,
new_comp_cache_ratio);
if (s.ok()) {
db_stress_env->SleepForMicroseconds(10 * 1000 * 1000);
raw_env->SleepForMicroseconds(10 * 1000 * 1000);
}
if (s.ok()) {
s = UpdateTieredCache(block_cache, /*capacity*/ -1,
@@ -231,36 +232,37 @@ void CompressedCacheSetCapacityThread(void* v) {
#ifndef NDEBUG
static void SetupFaultInjectionForRemoteCompaction(SharedState* shared) {
if (!fault_fs_guard) {
auto fault_fs = shared->GetStressTest()->GetDbFaultInjectionFs();
if (!fault_fs) {
return;
}
fault_fs_guard->SetThreadLocalErrorContext(
fault_fs->SetThreadLocalErrorContext(
FaultInjectionIOType::kRead, shared->GetSeed(), FLAGS_read_fault_one_in,
FLAGS_inject_error_severity == 1 /* retryable */,
FLAGS_inject_error_severity == 2 /* has_data_loss*/);
fault_fs_guard->EnableThreadLocalErrorInjection(FaultInjectionIOType::kRead);
fault_fs->EnableThreadLocalErrorInjection(FaultInjectionIOType::kRead);
fault_fs_guard->SetThreadLocalErrorContext(
fault_fs->SetThreadLocalErrorContext(
FaultInjectionIOType::kWrite, shared->GetSeed(), FLAGS_write_fault_one_in,
FLAGS_inject_error_severity == 1 /* retryable */,
FLAGS_inject_error_severity == 2 /* has_data_loss*/);
fault_fs_guard->EnableThreadLocalErrorInjection(FaultInjectionIOType::kWrite);
fault_fs->EnableThreadLocalErrorInjection(FaultInjectionIOType::kWrite);
fault_fs_guard->SetThreadLocalErrorContext(
fault_fs->SetThreadLocalErrorContext(
FaultInjectionIOType::kMetadataRead, shared->GetSeed(),
FLAGS_metadata_read_fault_one_in,
FLAGS_inject_error_severity == 1 /* retryable */,
FLAGS_inject_error_severity == 2 /* has_data_loss*/);
fault_fs_guard->EnableThreadLocalErrorInjection(
fault_fs->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
fault_fs_guard->SetThreadLocalErrorContext(
fault_fs->SetThreadLocalErrorContext(
FaultInjectionIOType::kMetadataWrite, shared->GetSeed(),
FLAGS_metadata_write_fault_one_in,
FLAGS_inject_error_severity == 1 /* retryable */,
FLAGS_inject_error_severity == 2 /* has_data_loss*/);
fault_fs_guard->EnableThreadLocalErrorInjection(
fault_fs->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataWrite);
}
#endif // NDEBUG
@@ -268,7 +270,7 @@ static void SetupFaultInjectionForRemoteCompaction(SharedState* shared) {
static CompactionServiceOptionsOverride CreateOverrideOptions(
const Options& options, const CompactionServiceJobInfo& job_info) {
CompactionServiceOptionsOverride override_options{
.env = db_stress_env,
.env = options.env,
.file_checksum_gen_factory = options.file_checksum_gen_factory,
.merge_operator = options.merge_operator,
.compaction_filter = options.compaction_filter,
@@ -311,14 +313,8 @@ static CompactionServiceOptionsOverride CreateOverrideOptions(
}
static Status CleanupOutputDirectory(const std::string& output_directory) {
#ifndef NDEBUG
// Temporarily disable fault injection to ensure deletion always succeeds
if (fault_fs_guard) {
fault_fs_guard->DisableAllThreadLocalErrorInjection();
}
#endif // NDEBUG
Status s = DestroyDir(db_stress_env, output_directory);
// Uses raw_env (no fault injection); cleanup must always succeed.
Status s = DestroyDir(raw_env, output_directory);
if (!s.ok()) {
fprintf(stderr,
"Failed to destroy output directory %s when allow_resumption is "
@@ -327,7 +323,7 @@ static Status CleanupOutputDirectory(const std::string& output_directory) {
}
if (s.ok()) {
s = db_stress_env->CreateDir(output_directory);
s = raw_env->CreateDir(output_directory);
if (!s.ok()) {
fprintf(stderr,
"Failed to recreate output directory %s when allow_resumption is "
@@ -336,13 +332,6 @@ static Status CleanupOutputDirectory(const std::string& output_directory) {
}
}
#ifndef NDEBUG
// Re-enable fault injection after deletion
if (fault_fs_guard) {
fault_fs_guard->EnableAllThreadLocalErrorInjection();
}
#endif // NDEBUG
return s;
}
@@ -497,7 +486,7 @@ void RemoteCompactionWorkerThread(void* v) {
shared, stress_test, rand, successful_compaction_end_to_end_micros);
}
db_stress_env->SleepForMicroseconds(
raw_env->SleepForMicroseconds(
thread->rand.Next() % FLAGS_remote_compaction_worker_interval * 1000 +
1);
}
@@ -679,7 +668,7 @@ bool VerifyIteratorAttributeGroups(
}
std::string GetNowNanos() {
uint64_t t = db_stress_env->NowNanos();
uint64_t t = raw_env->NowNanos();
std::string ret;
PutFixed64(&ret, t);
return ret;
@@ -691,7 +680,7 @@ uint64_t GetWriteUnixTime(ThreadState* thread) {
FLAGS_preclude_last_level_data_seconds);
static uint64_t kFallbackTime = std::numeric_limits<uint64_t>::max();
int64_t write_time = 0;
Status s = db_stress_env->GetCurrentTime(&write_time);
Status s = raw_env->GetCurrentTime(&write_time);
uint32_t write_time_mode = thread->rand.Uniform(3);
if (write_time_mode == 0 || !s.ok()) {
return kFallbackTime;
@@ -806,6 +795,8 @@ std::shared_ptr<FileChecksumGenFactory> GetFileChecksumImpl(
return std::make_shared<DbStressChecksumGenFactory>(internal_name);
}
// Expected values state files are always on local filesystem (Python owns
// this dir), so use Env::Default() (PosixEnv) even when raw_env is remote.
Status DeleteFilesInDirectory(const std::string& dirname) {
std::vector<std::string> filenames;
Status s = Env::Default()->GetChildren(dirname, &filenames);
@@ -880,8 +871,12 @@ Status DestroyUnverifiedSubdir(const std::string& dirname) {
Status DbStressDestroyDb(const std::string& db_path) {
Status s;
Options options;
// NOTE: using db_stress_listener_env in order to see obsolete MANIFEST files
options.env = db_stress_listener_env;
// Use raw_env (no DbStressFSWrapper) because DbStressFSWrapper renames
// MANIFEST files to MANIFEST-xxx_renamed_ instead of deleting them during
// normal DB operation (to preserve history for debugging). Using raw_env
// here ensures DestroyDB actually deletes these files. DestroyDir below
// catches any remaining files not known to DestroyDB.
options.env = raw_env;
// Remove DB files in a principled way to avoid issues
if (FLAGS_use_blob_db) {
s = blob_db::DestroyBlobDB(db_path, options, blob_db::BlobDBOptions());
@@ -891,9 +886,8 @@ Status DbStressDestroyDb(const std::string& db_path) {
if (!s.ok()) {
return s;
}
// Remove everything else recursively, only reporting success if able to
// delete everything
return DestroyDir(db_stress_listener_env, db_path);
// Remove everything else recursively (catches MANIFEST_renamed_ files)
return DestroyDir(raw_env, db_path);
}
} // namespace ROCKSDB_NAMESPACE
+31 -8
View File
@@ -49,6 +49,7 @@
#include "rocksdb/advanced_options.h"
#include "rocksdb/cache.h"
#include "rocksdb/env.h"
#include "rocksdb/rate_limiter.h"
#include "rocksdb/slice.h"
#include "rocksdb/slice_transform.h"
#include "rocksdb/statistics.h"
@@ -61,6 +62,7 @@
#include "rocksdb/utilities/transaction.h"
#include "rocksdb/utilities/transaction_db.h"
#include "rocksdb/write_batch.h"
#include "rocksdb/write_buffer_manager.h"
#include "test_util/testutil.h"
#include "util/coding.h"
#include "util/compression.h"
@@ -114,6 +116,7 @@ DECLARE_double(memtable_prefix_bloom_size_ratio);
DECLARE_bool(memtable_whole_key_filtering);
DECLARE_int32(open_files);
DECLARE_bool(open_files_async);
DECLARE_bool(async_wal_precreate);
DECLARE_uint64(compressed_secondary_cache_size);
DECLARE_int32(compressed_secondary_cache_numshardbits);
DECLARE_int32(secondary_cache_update_interval);
@@ -190,6 +193,7 @@ DECLARE_string(db);
DECLARE_string(secondaries_base);
DECLARE_bool(test_secondary);
DECLARE_string(expected_values_dir);
DECLARE_int32(num_dbs);
DECLARE_bool(expected_state_trace_debug);
DECLARE_int64(expected_state_trace_debug_key);
DECLARE_int32(expected_state_trace_debug_max_logs);
@@ -279,7 +283,9 @@ DECLARE_bool(use_full_merge_v1);
DECLARE_int32(sync_wal_one_in);
DECLARE_bool(avoid_unnecessary_blocking_io);
DECLARE_bool(write_dbid_to_manifest);
DECLARE_bool(optimize_manifest_for_recovery);
DECLARE_bool(write_identity_file);
DECLARE_bool(reuse_manifest_on_open);
DECLARE_bool(avoid_flush_during_recovery);
DECLARE_bool(enforce_write_buffer_manager_during_recovery);
DECLARE_uint64(max_write_batch_group_size_bytes);
@@ -460,6 +466,7 @@ DECLARE_uint32(ingest_wbwi_one_in);
DECLARE_bool(universal_reduce_file_locking);
DECLARE_bool(use_multiscan);
DECLARE_bool(multiscan_use_async_io);
DECLARE_bool(read_scoped_block_buffer_provider);
DECLARE_uint64(multiscan_max_prefetch_memory_bytes);
// Compaction deletion trigger declarations for stress testing
@@ -470,18 +477,23 @@ DECLARE_int32(compaction_on_deletion_window_size);
DECLARE_double(compaction_on_deletion_ratio);
DECLARE_double(read_triggered_compaction_threshold);
DECLARE_string(listener_uri);
constexpr long KB = 1024;
constexpr int kRandomValueMaxFactor = 3;
constexpr int kValueMaxLen = 100;
constexpr uint32_t kLargePrimeForCommonFactorSkew = 1872439133;
// wrapped posix environment
extern ROCKSDB_NAMESPACE::Env* db_stress_env;
extern ROCKSDB_NAMESPACE::Env* db_stress_listener_env;
extern std::shared_ptr<ROCKSDB_NAMESPACE::FaultInjectionTestFS> fault_fs_guard;
// Base env from --env_uri/--fs_uri. No wrappers (no DbStressFSWrapper, no
// fault injection). Could be PosixEnv or remote. Used for infrastructure ops
// (threads, time, dirs, cleanup). Per-StressTest env adds fault injection +
// DbStressFSWrapper on top for DB I/O.
extern ROCKSDB_NAMESPACE::Env* raw_env;
extern std::shared_ptr<ROCKSDB_NAMESPACE::SecondaryCache>
compressed_secondary_cache;
extern std::shared_ptr<ROCKSDB_NAMESPACE::Cache> block_cache;
extern std::shared_ptr<ROCKSDB_NAMESPACE::WriteBufferManager> wbm;
extern std::shared_ptr<ROCKSDB_NAMESPACE::RateLimiter> rate_limiter;
extern enum ROCKSDB_NAMESPACE::CompressionType compression_type_e;
extern enum ROCKSDB_NAMESPACE::CompressionType bottommost_compression_type_e;
@@ -826,10 +838,21 @@ AttributeGroups GenerateAttributeGroups(
const std::vector<ColumnFamilyHandle*>& cfhs, uint32_t value_base,
const Slice& slice);
StressTest* CreateCfConsistencyStressTest();
StressTest* CreateBatchedOpsStressTest();
StressTest* CreateNonBatchedOpsStressTest();
StressTest* CreateMultiOpsTxnsStressTest();
StressTest* CreateCfConsistencyStressTest(int db_index,
const std::string& db_path,
const std::string& ev_path,
const std::string& sec_path);
StressTest* CreateBatchedOpsStressTest(int db_index, const std::string& db_path,
const std::string& ev_path,
const std::string& sec_path);
StressTest* CreateNonBatchedOpsStressTest(int db_index,
const std::string& db_path,
const std::string& ev_path,
const std::string& sec_path);
StressTest* CreateMultiOpsTxnsStressTest(int db_index,
const std::string& db_path,
const std::string& ev_path,
const std::string& sec_path);
void CheckAndSetOptionsForMultiOpsTxnStressTest();
void InitializeHotKeyGenerator(double alpha);
int64_t GetOneHotKeyID(double rand_seed, int64_t max_key);
@@ -1,4 +1,4 @@
// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
// 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).
+70 -49
View File
@@ -10,12 +10,14 @@
#include "db_stress_tool/db_stress_shared_state.h"
#ifdef GFLAGS
#include <mutex>
#include "db_stress_tool/db_stress_common.h"
#include "utilities/fault_injection_fs.h"
namespace ROCKSDB_NAMESPACE {
void ThreadBody(void* v) {
ThreadStatusUtil::RegisterThread(db_stress_env, ThreadStatus::USER);
ThreadStatusUtil::RegisterThread(raw_env, ThreadStatus::USER);
ThreadState* thread = static_cast<ThreadState*>(v);
SharedState* shared = thread->shared;
@@ -64,17 +66,21 @@ void ThreadBody(void* v) {
ThreadStatusUtil::UnregisterThread();
}
bool RunStressTestImpl(SharedState* shared) {
SystemClock* clock = db_stress_env->GetSystemClock().get();
SystemClock* clock = raw_env->GetSystemClock().get();
StressTest* stress = shared->GetStressTest();
const std::string db_label =
(FLAGS_num_dbs > 1) ? "[" + stress->GetDbLabel() + "] " : "";
if (shared->ShouldVerifyAtBeginning() && FLAGS_preserve_unverified_changes) {
Status s = InitUnverifiedSubdir(FLAGS_db);
if (s.ok() && !FLAGS_expected_values_dir.empty()) {
s = InitUnverifiedSubdir(FLAGS_expected_values_dir);
const auto& db_path = stress->GetDbPath();
const auto& ev_dir = stress->GetExpectedValuesDir();
Status s = InitUnverifiedSubdir(db_path);
if (s.ok() && !ev_dir.empty()) {
s = InitUnverifiedSubdir(ev_dir);
}
if (!s.ok()) {
fprintf(stderr, "Failed to setup unverified state dir: %s\n",
s.ToString().c_str());
fprintf(stderr, "%sFailed to setup unverified state dir: %s\n",
db_label.c_str(), s.ToString().c_str());
exit(1);
}
}
@@ -84,24 +90,15 @@ bool RunStressTestImpl(SharedState* shared) {
uint32_t n = FLAGS_threads;
uint64_t now = clock->NowMicros();
fprintf(stdout, "%s Initializing worker threads\n",
clock->TimeToString(now / 1000000).c_str());
fprintf(stdout, "%s %sInitializing worker threads\n",
clock->TimeToString(now / 1000000).c_str(), db_label.c_str());
shared->SetThreads(n);
if (FLAGS_compaction_thread_pool_adjust_interval > 0) {
shared->IncBgThreads();
}
if (FLAGS_continuous_verification_interval > 0) {
shared->IncBgThreads();
}
if (FLAGS_compressed_secondary_cache_size > 0 ||
FLAGS_compressed_secondary_cache_ratio > 0.0) {
shared->IncBgThreads();
}
uint32_t remote_compaction_worker_thread_count =
FLAGS_remote_compaction_worker_threads;
if (remote_compaction_worker_thread_count > 0) {
@@ -113,25 +110,39 @@ bool RunStressTestImpl(SharedState* shared) {
std::vector<ThreadState*> threads(n);
for (uint32_t i = 0; i < n; i++) {
threads[i] = new ThreadState(i, shared);
db_stress_env->StartThread(ThreadBody, threads[i]);
raw_env->StartThread(ThreadBody, threads[i]);
}
// Spawn at most one PoolSizeChangeThread globally. All DBs share the
// same raw_env, so one thread resizing the pool is sufficient.
// IncBgThreads inside call_once so only the launching DB's SharedState
// tracks this thread (avoids deadlock for non-first DBs).
ThreadState bg_thread(0, shared);
static std::once_flag pool_size_thread_flag;
if (FLAGS_compaction_thread_pool_adjust_interval > 0) {
db_stress_env->StartThread(PoolSizeChangeThread, &bg_thread);
std::call_once(pool_size_thread_flag, [&]() {
shared->IncBgThreads();
raw_env->StartThread(PoolSizeChangeThread, &bg_thread);
});
}
ThreadState continuous_verification_thread(0, shared);
if (FLAGS_continuous_verification_interval > 0) {
db_stress_env->StartThread(DbVerificationThread,
&continuous_verification_thread);
raw_env->StartThread(DbVerificationThread, &continuous_verification_thread);
}
// Spawn at most one CompressedCacheSetCapacityThread globally. The cache
// is shared across all DBs, and the thread's SetCapacity(0)/assert(==0)
// sequence would race if multiple DBs each spawned their own copy.
ThreadState compressed_cache_set_capacity_thread(0, shared);
static std::once_flag compressed_cache_thread_flag;
if (FLAGS_compressed_secondary_cache_size > 0 ||
FLAGS_compressed_secondary_cache_ratio > 0.0) {
db_stress_env->StartThread(CompressedCacheSetCapacityThread,
&compressed_cache_set_capacity_thread);
std::call_once(compressed_cache_thread_flag, [&]() {
shared->IncBgThreads();
raw_env->StartThread(CompressedCacheSetCapacityThread,
&compressed_cache_set_capacity_thread);
});
}
std::vector<ThreadState*> remote_compaction_worker_threads;
@@ -141,7 +152,7 @@ bool RunStressTestImpl(SharedState* shared) {
for (uint32_t i = 0; i < remote_compaction_worker_thread_count; i++) {
ThreadState* ts = new ThreadState(i, shared);
remote_compaction_worker_threads.push_back(ts);
db_stress_env->StartThread(RemoteCompactionWorkerThread, ts);
raw_env->StartThread(RemoteCompactionWorkerThread, ts);
}
}
@@ -156,16 +167,19 @@ bool RunStressTestImpl(SharedState* shared) {
}
if (shared->ShouldVerifyAtBeginning()) {
if (shared->HasVerificationFailedYet()) {
fprintf(stderr, "Crash-recovery verification failed :(\n");
fprintf(stderr, "%sCrash-recovery verification failed :(\n",
db_label.c_str());
} else {
fprintf(stdout, "Crash-recovery verification passed :)\n");
Status s = DestroyUnverifiedSubdir(FLAGS_db);
if (s.ok() && !FLAGS_expected_values_dir.empty()) {
s = DestroyUnverifiedSubdir(FLAGS_expected_values_dir);
fprintf(stdout, "%sCrash-recovery verification passed :)\n",
db_label.c_str());
const auto& ev_dir = stress->GetExpectedValuesDir();
Status s = DestroyUnverifiedSubdir(stress->GetDbPath());
if (s.ok() && !ev_dir.empty()) {
s = DestroyUnverifiedSubdir(ev_dir);
}
if (!s.ok()) {
fprintf(stderr, "Failed to cleanup unverified state dir: %s\n",
s.ToString().c_str());
fprintf(stderr, "%sFailed to cleanup unverified state dir: %s\n",
db_label.c_str(), s.ToString().c_str());
exit(1);
}
}
@@ -174,15 +188,16 @@ bool RunStressTestImpl(SharedState* shared) {
if (!FLAGS_verification_only) {
// This is after the verification step to avoid making all those `Get()`s
// and `MultiGet()`s contend on the DB-wide trace mutex.
if (!FLAGS_expected_values_dir.empty()) {
if (!stress->GetExpectedValuesDir().empty()) {
stress->TrackExpectedState(shared);
}
if (FLAGS_sync_fault_injection || FLAGS_write_fault_one_in > 0) {
fault_fs_guard->SetFilesystemDirectWritable(false);
fault_fs_guard->SetInjectUnsyncedDataLoss(FLAGS_sync_fault_injection);
auto fault_fs = stress->GetDbFaultInjectionFs();
fault_fs->SetFilesystemDirectWritable(false);
fault_fs->SetInjectUnsyncedDataLoss(FLAGS_sync_fault_injection);
if (FLAGS_exclude_wal_from_write_fault_injection) {
fault_fs_guard->SetFileTypesExcludedFromWriteFaultInjection(
fault_fs->SetFileTypesExcludedFromWriteFaultInjection(
{FileType::kWalFile});
}
}
@@ -190,8 +205,8 @@ bool RunStressTestImpl(SharedState* shared) {
Status s = stress->EnableAutoCompaction();
assert(s.ok());
}
fprintf(stdout, "%s Starting database operations\n",
clock->TimeToString(now / 1000000).c_str());
fprintf(stdout, "%s %sStarting database operations\n",
clock->TimeToString(now / 1000000).c_str(), db_label.c_str());
shared->SetStart();
shared->GetCondVar()->SignalAll();
@@ -201,14 +216,17 @@ bool RunStressTestImpl(SharedState* shared) {
now = clock->NowMicros();
if (FLAGS_test_batches_snapshots) {
fprintf(stdout, "%s Limited verification already done during gets\n",
clock->TimeToString((uint64_t)now / 1000000).c_str());
fprintf(stdout, "%s %sLimited verification already done during gets\n",
clock->TimeToString((uint64_t)now / 1000000).c_str(),
db_label.c_str());
} else if (FLAGS_skip_verifydb) {
fprintf(stdout, "%s Verification skipped\n",
clock->TimeToString((uint64_t)now / 1000000).c_str());
fprintf(stdout, "%s %sVerification skipped\n",
clock->TimeToString((uint64_t)now / 1000000).c_str(),
db_label.c_str());
} else {
fprintf(stdout, "%s Starting verification\n",
clock->TimeToString((uint64_t)now / 1000000).c_str());
fprintf(stdout, "%s %sStarting verification\n",
clock->TimeToString((uint64_t)now / 1000000).c_str(),
db_label.c_str());
}
shared->SetStartVerify();
@@ -230,7 +248,7 @@ bool RunStressTestImpl(SharedState* shared) {
for (unsigned int i = 1; i < n; i++) {
threads[0]->stats.Merge(threads[i]->stats);
}
threads[0]->stats.Report("Stress Test");
threads[0]->stats.Report((db_label + "Stress Test").c_str());
}
for (unsigned int i = 0; i < n; i++) {
@@ -241,8 +259,8 @@ bool RunStressTestImpl(SharedState* shared) {
now = clock->NowMicros();
if (!FLAGS_skip_verifydb && !FLAGS_test_batches_snapshots &&
!shared->HasVerificationFailedYet()) {
fprintf(stdout, "%s Verification successful\n",
clock->TimeToString(now / 1000000).c_str());
fprintf(stdout, "%s %sVerification successful\n",
clock->TimeToString(now / 1000000).c_str(), db_label.c_str());
}
if (!FLAGS_verification_only) {
@@ -260,6 +278,9 @@ bool RunStressTestImpl(SharedState* shared) {
shared->GetCondVar()->Wait();
}
}
now = clock->NowMicros();
fprintf(stdout, "%s %sStress test bg threads finished\n",
clock->TimeToString(now / 1000000).c_str(), db_label.c_str());
assert(remote_compaction_worker_threads.size() ==
remote_compaction_worker_thread_count);
@@ -271,13 +292,13 @@ bool RunStressTestImpl(SharedState* shared) {
}
if (shared->HasVerificationFailedYet()) {
fprintf(stderr, "Verification failed :(\n");
fprintf(stderr, "%sVerification failed :(\n", db_label.c_str());
return false;
}
return true;
}
bool RunStressTest(SharedState* shared) {
ThreadStatusUtil::RegisterThread(db_stress_env, ThreadStatus::USER);
ThreadStatusUtil::RegisterThread(raw_env, ThreadStatus::USER);
bool result = RunStressTestImpl(shared);
ThreadStatusUtil::UnregisterThread();
return result;
+2 -2
View File
@@ -197,10 +197,10 @@ class DbStressFSWrapper : public FileSystemWrapper {
assert(!file_opts.file_checksum_func_name.empty());
if (file_opts.file_checksum_func_name == kUnknownFileChecksumFuncName ||
file_opts.file_checksum_func_name == kNoFileChecksumFuncName) {
// No checksum available — checksum value must be empty
// No checksum available -- checksum value must be empty
assert(file_opts.file_checksum.empty());
} else {
// A real checksum function — checksum value must be present
// A real checksum function -- checksum value must be present
assert(!file_opts.file_checksum.empty());
}
}
+34
View File
@@ -206,6 +206,10 @@ DEFINE_int32(open_files, ROCKSDB_NAMESPACE::Options().max_open_files,
DEFINE_bool(open_files_async, ROCKSDB_NAMESPACE::Options().open_files_async,
"Options.open_files_async");
DEFINE_bool(async_wal_precreate,
ROCKSDB_NAMESPACE::Options().async_wal_precreate,
"Options.async_wal_precreate");
DEFINE_uint64(compressed_secondary_cache_size, 0,
"Number of bytes to use as a cache of compressed data."
" 0 means use default settings.");
@@ -1133,10 +1137,19 @@ DEFINE_bool(write_dbid_to_manifest,
ROCKSDB_NAMESPACE::Options().write_dbid_to_manifest,
"Write DB_ID to manifest");
DEFINE_bool(optimize_manifest_for_recovery,
ROCKSDB_NAMESPACE::Options().optimize_manifest_for_recovery,
"Reduce recovery work after a clean shutdown");
DEFINE_bool(write_identity_file,
ROCKSDB_NAMESPACE::Options().write_identity_file,
"Write DB_ID to IDENTITY file");
DEFINE_bool(reuse_manifest_on_open,
ROCKSDB_NAMESPACE::Options().reuse_manifest_on_open,
"Reopen existing MANIFEST for append after recovery instead of "
"creating a fresh one");
DEFINE_bool(avoid_flush_during_recovery,
ROCKSDB_NAMESPACE::Options().avoid_flush_during_recovery,
"Avoid flush during recovery");
@@ -1655,6 +1668,10 @@ DEFINE_double(read_triggered_compaction_threshold,
ROCKSDB_NAMESPACE::Options().read_triggered_compaction_threshold,
"Sets CF option read_triggered_compaction_threshold.");
DEFINE_string(listener_uri, "",
"URI for an additional EventListener to attach (e.g. "
"auto_tuner://). Empty means none.");
DEFINE_bool(
auto_refresh_iterator_with_snapshot,
ROCKSDB_NAMESPACE::ReadOptions().auto_refresh_iterator_with_snapshot,
@@ -1688,10 +1705,27 @@ DEFINE_bool(use_multiscan, false,
DEFINE_bool(multiscan_use_async_io, false,
"If set, enable async_io for MultiScan operations.");
DEFINE_bool(read_scoped_block_buffer_provider, false,
"If set, configure ReadOptions::read_scoped_block_buffer_provider "
"with a stress-test provider for supported scan reads.");
DEFINE_uint64(multiscan_max_prefetch_memory_bytes, 0,
"If non-zero, sets the max_prefetch_memory_bytes on the "
"IODispatcher used for MultiScan. This limits the total memory "
"used for prefetching data blocks across all concurrent "
"MultiScan ReadSets.");
DEFINE_int32(num_dbs, 1,
"Number of DB instances to run in parallel. "
"For num_dbs=1: --db is the DB path directly (default). "
"For num_dbs>1: --db is a parent directory; db_stress "
"auto-creates db_0/, db_1/, ... subdirs underneath "
"(same for --expected_values_dir and secondaries). "
"Per-DB flags: threads, max_key, ops_per_thread, reopen, "
"column_families, and all DB options. "
"Shared across all instances: background env threads "
"(compaction, flush pool), cache, write_buffer_manager, "
"compressed_secondary_cache, rate_limiter, and "
"compaction_thread_pool_adjust_interval.");
#endif // GFLAGS
+16
View File
@@ -7,6 +7,7 @@
#include <cstdint>
#include "db_stress_tool/db_stress_test_base.h"
#include "file/file_util.h"
#include "rocksdb/file_system.h"
#include "util/coding_lean.h"
@@ -15,6 +16,21 @@ namespace ROCKSDB_NAMESPACE {
#ifdef GFLAGS
DbStressListener::DbStressListener(
const std::string& db_name, const std::vector<DbPath>& db_paths,
const std::vector<ColumnFamilyDescriptor>& column_families,
SharedState* shared)
: db_name_(db_name),
db_paths_(db_paths),
column_families_(column_families),
num_pending_file_creations_(0),
unique_ids_(shared->GetStressTest()->GetExpectedValuesDir().empty()
? db_name
: shared->GetStressTest()->GetExpectedValuesDir()),
shared_(shared),
db_fault_injection_fs_(shared->GetStressTest()->GetDbFaultInjectionFs()) {
}
UniqueIdVerifier::UniqueIdVerifier(const std::string& dir)
: path_(dir + "/.unique_ids") {
// We expect such a small number of files generated during this test
+174 -41
View File
@@ -6,7 +6,9 @@
#ifdef GFLAGS
#pragma once
#include <cinttypes>
#include <mutex>
#include <unordered_map>
#include <unordered_set>
#include "db_stress_tool/db_stress_compaction_service.h"
@@ -19,13 +21,12 @@
#include "rocksdb/listener.h"
#include "rocksdb/table_properties.h"
#include "rocksdb/unique_id.h"
#include "util/atomic.h"
#include "util/gflags_compat.h"
#include "util/random.h"
#include "utilities/fault_injection_fs.h"
DECLARE_int32(compact_files_one_in);
extern std::shared_ptr<ROCKSDB_NAMESPACE::FaultInjectionTestFS> fault_fs_guard;
namespace ROCKSDB_NAMESPACE {
// Verify across process executions that all seen IDs are unique
@@ -55,27 +56,22 @@ class DbStressListener : public EventListener {
DbStressListener(const std::string& db_name,
const std::vector<DbPath>& db_paths,
const std::vector<ColumnFamilyDescriptor>& column_families,
SharedState* shared)
: db_name_(db_name),
db_paths_(db_paths),
column_families_(column_families),
num_pending_file_creations_(0),
unique_ids_(FLAGS_expected_values_dir.empty()
? db_name
: FLAGS_expected_values_dir),
shared_(shared) {}
SharedState* shared);
const char* Name() const override { return kClassName(); }
static const char* kClassName() { return "DBStressListener"; }
~DbStressListener() override { assert(num_pending_file_creations_ == 0); }
void OnDBShutdownBegin(DB* /*db*/) override { shutting_down_.Store(true); }
void OnFlushCompleted(DB* /*db*/, const FlushJobInfo& info) override {
assert(IsValidColumnFamilyName(info.cf_name));
VerifyFilePath(info.file_path);
// pretending doing some work here
RandomSleep();
if (fault_fs_guard) {
fault_fs_guard->DisableAllThreadLocalErrorInjection();
if (db_fault_injection_fs_) {
db_fault_injection_fs_->DisableAllThreadLocalErrorInjection();
}
shared_->SetPersistedSeqno(info.largest_seqno);
}
@@ -83,46 +79,118 @@ class DbStressListener : public EventListener {
void OnFlushBegin(DB* /*db*/,
const FlushJobInfo& /*flush_job_info*/) override {
RandomSleep();
if (fault_fs_guard) {
fault_fs_guard->SetThreadLocalErrorContext(
if (db_fault_injection_fs_) {
db_fault_injection_fs_->SetThreadLocalErrorContext(
FaultInjectionIOType::kRead, static_cast<uint32_t>(FLAGS_seed),
FLAGS_read_fault_one_in,
FLAGS_inject_error_severity == 1 /* retryable */,
FLAGS_inject_error_severity == 2 /* has_data_loss*/);
fault_fs_guard->EnableThreadLocalErrorInjection(
db_fault_injection_fs_->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kRead);
fault_fs_guard->SetThreadLocalErrorContext(
db_fault_injection_fs_->SetThreadLocalErrorContext(
FaultInjectionIOType::kWrite, static_cast<uint32_t>(FLAGS_seed),
FLAGS_write_fault_one_in,
FLAGS_inject_error_severity == 1 /* retryable */,
FLAGS_inject_error_severity == 2 /* has_data_loss*/);
fault_fs_guard->EnableThreadLocalErrorInjection(
db_fault_injection_fs_->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kWrite);
fault_fs_guard->SetThreadLocalErrorContext(
db_fault_injection_fs_->SetThreadLocalErrorContext(
FaultInjectionIOType::kMetadataRead,
static_cast<uint32_t>(FLAGS_seed), FLAGS_metadata_read_fault_one_in,
FLAGS_inject_error_severity == 1 /* retryable */,
FLAGS_inject_error_severity == 2 /* has_data_loss*/);
fault_fs_guard->EnableThreadLocalErrorInjection(
db_fault_injection_fs_->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
fault_fs_guard->SetThreadLocalErrorContext(
db_fault_injection_fs_->SetThreadLocalErrorContext(
FaultInjectionIOType::kMetadataWrite,
static_cast<uint32_t>(FLAGS_seed), FLAGS_metadata_write_fault_one_in,
FLAGS_inject_error_severity == 1 /* retryable */,
FLAGS_inject_error_severity == 2 /* has_data_loss*/);
fault_fs_guard->EnableThreadLocalErrorInjection(
db_fault_injection_fs_->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataWrite);
}
}
void OnTableFileDeleted(const TableFileDeletionInfo& /*info*/) override {
void OnTableFileDeleted(const TableFileDeletionInfo& info) override {
// A file must not be deleted while it is still between
// OnCompactionBegin and OnCompactionPreCommit (i.e. actively being
// compacted with being_compacted == true). (Perhaps more realistically,
// this is checking for failure to call OnCompactionPreCommit.)
//
// During shutdown, compaction notifications are skipped so tracking
// state may be stale -- skip the check.
if (!shutting_down_.Load()) {
std::lock_guard<std::mutex> lock(compacting_files_mu_);
uint64_t file_number = FileNumberFromPath(info.file_path);
if (file_number != 0 &&
compacting_files_.find(file_number) != compacting_files_.end()) {
fprintf(stderr,
"OnTableFileDeleted for file tracked as being compacted "
"(between Begin and PreCommit): file_number=%" PRIu64 "\n",
file_number);
fflush(stderr);
std::abort();
}
}
RandomSleep();
}
void OnCompactionBegin(DB* /*db*/, const CompactionJobInfo& /*ci*/) override {
void OnCompactionBegin(DB* /*db*/, const CompactionJobInfo& ci) override {
// Sanity check inspired by a Meta-internal check: the same input file must
// not be in two concurrent compactions. Inserting input file numbers into a
// shared set on Begin and removing them on PreCommit (rather than on
// Completed) is what exercises the new OnCompactionPreCommit
// callback. Removing on Completed would race with another picker that
// grabs the same file as soon as ReleaseCompactionFiles flips
// FileMetaData::being_compacted back to false; PreCommit runs
// while being_compacted is still true and so closes that race.
{
std::lock_guard<std::mutex> lock(compacting_files_mu_);
for (const auto& info : ci.input_file_infos) {
auto [_, inserted] = compacting_files_.insert(info.file_number);
if (!inserted) {
fprintf(stderr,
"Concurrent compaction of SST file detected: cf=%s "
"file_number=%" PRIu64 "\n",
ci.cf_name.c_str(), info.file_number);
fflush(stderr);
std::abort();
}
}
}
RandomSleep();
}
void OnCompactionPreCommit(DB* /*db*/, const CompactionJobInfo& ci) override {
// Pair with OnCompactionBegin's bookkeeping: move files from
// compacting_files_ and record the job for Completed verification.
{
std::lock_guard<std::mutex> lock(compacting_files_mu_);
std::unordered_set<uint64_t> job_files;
for (const auto& info : ci.input_file_infos) {
size_t erased = compacting_files_.erase(info.file_number);
if (erased != 1) {
fprintf(stderr,
"OnCompactionPreCommit for file not in tracking set: "
"cf=%s file_number=%" PRIu64 "\n",
ci.cf_name.c_str(), info.file_number);
fflush(stderr);
std::abort();
}
job_files.insert(info.file_number);
}
auto [_, inserted] =
precommitted_jobs_.emplace(ci.job_id, std::move(job_files));
if (!inserted) {
fprintf(stderr, "OnCompactionPreCommit: duplicate job_id %d\n",
ci.job_id);
fflush(stderr);
std::abort();
}
}
RandomSleep();
}
@@ -135,49 +203,77 @@ class DbStressListener : public EventListener {
for (const auto& file_path : ci.output_files) {
VerifyFilePath(file_path);
}
// Verify that OnCompactionPreCommit fired before OnCompactionCompleted
// for the same job, with matching input files.
{
std::lock_guard<std::mutex> lock(compacting_files_mu_);
auto it = precommitted_jobs_.find(ci.job_id);
if (it == precommitted_jobs_.end()) {
fprintf(stderr,
"OnCompactionCompleted without prior OnCompactionPreCommit: "
"cf=%s job_id=%d\n",
ci.cf_name.c_str(), ci.job_id);
fflush(stderr);
std::abort();
}
// Verify input file sets match between PreCommit and Completed.
for (const auto& info : ci.input_file_infos) {
if (it->second.find(info.file_number) == it->second.end()) {
fprintf(stderr,
"OnCompactionCompleted: input file %" PRIu64
" not in PreCommit set for job_id=%d\n",
info.file_number, ci.job_id);
fflush(stderr);
std::abort();
}
}
precommitted_jobs_.erase(it);
}
// pretending doing some work here
RandomSleep();
}
void OnSubcompactionBegin(const SubcompactionJobInfo& /* si */) override {
if (fault_fs_guard) {
fault_fs_guard->SetThreadLocalErrorContext(
if (db_fault_injection_fs_) {
db_fault_injection_fs_->SetThreadLocalErrorContext(
FaultInjectionIOType::kRead, static_cast<uint32_t>(FLAGS_seed),
FLAGS_read_fault_one_in,
FLAGS_inject_error_severity == 1 /* retryable */,
FLAGS_inject_error_severity == 2 /* has_data_loss*/);
fault_fs_guard->EnableThreadLocalErrorInjection(
db_fault_injection_fs_->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kRead);
fault_fs_guard->SetThreadLocalErrorContext(
db_fault_injection_fs_->SetThreadLocalErrorContext(
FaultInjectionIOType::kWrite, static_cast<uint32_t>(FLAGS_seed),
FLAGS_write_fault_one_in,
FLAGS_inject_error_severity == 1 /* retryable */,
FLAGS_inject_error_severity == 2 /* has_data_loss*/);
fault_fs_guard->EnableThreadLocalErrorInjection(
db_fault_injection_fs_->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kWrite);
fault_fs_guard->SetThreadLocalErrorContext(
db_fault_injection_fs_->SetThreadLocalErrorContext(
FaultInjectionIOType::kMetadataRead,
static_cast<uint32_t>(FLAGS_seed), FLAGS_metadata_read_fault_one_in,
FLAGS_inject_error_severity == 1 /* retryable */,
FLAGS_inject_error_severity == 2 /* has_data_loss*/);
fault_fs_guard->EnableThreadLocalErrorInjection(
db_fault_injection_fs_->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
fault_fs_guard->SetThreadLocalErrorContext(
db_fault_injection_fs_->SetThreadLocalErrorContext(
FaultInjectionIOType::kMetadataWrite,
static_cast<uint32_t>(FLAGS_seed), FLAGS_metadata_write_fault_one_in,
FLAGS_inject_error_severity == 1 /* retryable */,
FLAGS_inject_error_severity == 2 /* has_data_loss*/);
fault_fs_guard->EnableThreadLocalErrorInjection(
db_fault_injection_fs_->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataWrite);
}
}
void OnSubcompactionCompleted(const SubcompactionJobInfo& /* si */) override {
if (fault_fs_guard) {
fault_fs_guard->DisableAllThreadLocalErrorInjection();
if (db_fault_injection_fs_) {
db_fault_injection_fs_->DisableAllThreadLocalErrorInjection();
}
}
@@ -270,11 +366,12 @@ class DbStressListener : public EventListener {
Status /* bg_error */,
bool* /* auto_recovery */) override {
RandomSleep();
if (FLAGS_error_recovery_with_no_fault_injection && fault_fs_guard) {
fault_fs_guard->DisableAllThreadLocalErrorInjection();
if (FLAGS_error_recovery_with_no_fault_injection &&
db_fault_injection_fs_) {
db_fault_injection_fs_->DisableAllThreadLocalErrorInjection();
// TODO(hx235): only exempt the flush thread during error recovery instead
// of all the flush threads from error injection
fault_fs_guard->SetIOActivitiesExcludedFromFaultInjection(
db_fault_injection_fs_->SetIOActivitiesExcludedFromFaultInjection(
{Env::IOActivity::kFlush});
}
}
@@ -282,9 +379,10 @@ class DbStressListener : public EventListener {
void OnErrorRecoveryEnd(
const BackgroundErrorRecoveryInfo& /*info*/) override {
RandomSleep();
if (FLAGS_error_recovery_with_no_fault_injection && fault_fs_guard) {
fault_fs_guard->EnableAllThreadLocalErrorInjection();
fault_fs_guard->SetIOActivitiesExcludedFromFaultInjection({});
if (FLAGS_error_recovery_with_no_fault_injection &&
db_fault_injection_fs_) {
db_fault_injection_fs_->EnableAllThreadLocalErrorInjection();
db_fault_injection_fs_->SetIOActivitiesExcludedFromFaultInjection({});
}
}
@@ -375,8 +473,43 @@ class DbStressListener : public EventListener {
std::atomic<int> num_pending_file_creations_;
UniqueIdVerifier unique_ids_;
SharedState* shared_;
std::shared_ptr<FaultInjectionTestFS> db_fault_injection_fs_;
mutable std::mutex bg_pressure_mu_;
BackgroundJobPressure last_bg_pressure_;
// Files (by file_number) currently in flight from OnCompactionBegin to
// OnCompactionPreCommit. Used to detect concurrent compaction of
// the same SST file -- the bug fixed by the OnCompactionPreCommit
// callback. Protected by compacting_files_mu_.
std::mutex compacting_files_mu_;
std::unordered_set<uint64_t> compacting_files_;
// Set when DBImpl begins shutdown to suppress false positives from stale
// tracking when compaction callbacks are skipped during shutdown.
Atomic<bool> shutting_down_{false};
// Jobs that have passed OnCompactionPreCommit but not yet
// OnCompactionCompleted. Maps job_id -> input file numbers.
// Used to verify Begin -> PreCommit -> Completed ordering per job.
// Protected by compacting_files_mu_.
std::unordered_map<int, std::unordered_set<uint64_t>> precommitted_jobs_;
// Extract file number from a file path like "/path/to/000123.sst".
// Returns 0 if the path cannot be parsed.
static uint64_t FileNumberFromPath(const std::string& file_path) {
size_t pos = file_path.find_last_of('/');
// Avoid copying file_path when no '/' separator is found
std::string file_name_buf;
if (pos != std::string::npos) {
file_name_buf = file_path.substr(pos + 1);
}
const std::string& file_name =
(pos == std::string::npos) ? file_path : file_name_buf;
uint64_t file_number = 0;
FileType file_type;
if (ParseFileName(file_name, &file_number, &file_type) &&
file_type == kTableFile) {
return file_number;
}
return 0;
}
};
} // namespace ROCKSDB_NAMESPACE
#endif // GFLAGS
+102
View File
@@ -11,7 +11,109 @@
#ifdef GFLAGS
#include "db_stress_tool/db_stress_shared_state.h"
#include "db_stress_tool/db_stress_test_base.h"
namespace ROCKSDB_NAMESPACE {
thread_local bool SharedState::ignore_read_error;
SharedState::SharedState(Env* /*env*/, StressTest* stress_test)
: cv_(&mu_),
seed_(static_cast<uint32_t>(FLAGS_seed)),
max_key_(FLAGS_max_key),
log2_keys_per_lock_(static_cast<uint32_t>(FLAGS_log2_keys_per_lock)),
num_threads_(0),
num_initialized_(0),
num_populated_(0),
vote_reopen_(0),
num_done_(0),
start_(false),
start_verify_(false),
num_bg_threads_(0),
should_stop_bg_thread_(false),
bg_thread_finished_(0),
stress_test_(stress_test),
verification_failure_(false),
should_stop_test_(false),
no_overwrite_ids_(GenerateNoOverwriteIds()),
expected_state_manager_(nullptr),
printing_verification_results_(false),
start_timestamp_(Env::Default()->NowNanos()) {
Status status;
// TODO: We should introduce a way to explicitly disable verification
// during shutdown. When that is disabled and FLAGS_expected_values_dir
// is empty (disabling verification at startup), we can skip tracking
// expected state. Only then should we permit bypassing the below feature
// compatibility checks.
const auto& expected_values_dir = stress_test_->GetExpectedValuesDir();
if (!expected_values_dir.empty()) {
if (!std::atomic<uint32_t>{}.is_lock_free() ||
!std::atomic<uint64_t>{}.is_lock_free()) {
std::ostringstream status_s;
status_s << "Cannot use --expected_values_dir on platforms without "
"lock-free "
<< (!std::atomic<uint32_t>{}.is_lock_free()
? "std::atomic<uint32_t>"
: "std::atomic<uint64_t>");
status = Status::InvalidArgument(status_s.str());
}
if (status.ok() && FLAGS_clear_column_family_one_in > 0) {
status = Status::InvalidArgument(
"Cannot use --expected_values_dir on when "
"--clear_column_family_one_in is greater than zero.");
}
}
if (status.ok()) {
if (expected_values_dir.empty()) {
expected_state_manager_.reset(
new AnonExpectedStateManager(FLAGS_max_key, FLAGS_column_families));
} else {
expected_state_manager_.reset(new FileExpectedStateManager(
FLAGS_max_key, FLAGS_column_families, expected_values_dir));
}
status = expected_state_manager_->Open();
}
if (!status.ok()) {
fprintf(stderr, "Failed setting up expected state with error: %s\n",
status.ToString().c_str());
exit(1); // NOLINT(concurrency-mt-unsafe)
}
if (FLAGS_test_batches_snapshots) {
fprintf(stdout, "No lock creation because test_batches_snapshots set\n");
return;
}
long num_locks = static_cast<long>(max_key_ >> log2_keys_per_lock_);
if (max_key_ & ((1 << log2_keys_per_lock_) - 1)) {
num_locks++;
}
fprintf(stdout, "Creating %ld locks\n", num_locks * FLAGS_column_families);
key_locks_.resize(FLAGS_column_families);
for (int i = 0; i < FLAGS_column_families; ++i) {
key_locks_[i].reset(new port::Mutex[num_locks]);
}
if (FLAGS_read_fault_one_in || FLAGS_metadata_read_fault_one_in) {
#ifdef NDEBUG
// Unsupported in release mode because it relies on
// `IGNORE_STATUS_IF_ERROR` to distinguish faults not expected to lead to
// failure.
fprintf(stderr,
"Cannot set nonzero value for --read_fault_one_in in "
"release mode.");
exit(1); // NOLINT(concurrency-mt-unsafe)
#else // NDEBUG
SyncPoint::GetInstance()->SetCallBack("FaultInjectionIgnoreError",
IgnoreReadErrorCallback);
SyncPoint::GetInstance()->EnableProcessing();
#endif // NDEBUG
}
}
bool SharedState::ShouldVerifyAtBeginning() const {
return !stress_test_->GetExpectedValuesDir().empty();
}
} // namespace ROCKSDB_NAMESPACE
#endif // GFLAGS
+2 -97
View File
@@ -24,7 +24,6 @@ DECLARE_uint64(log2_keys_per_lock);
DECLARE_int32(threads);
DECLARE_int32(column_families);
DECLARE_int32(nooverwritepercent);
DECLARE_string(expected_values_dir);
DECLARE_int32(clear_column_family_one_in);
DECLARE_bool(test_batches_snapshots);
DECLARE_int32(compaction_thread_pool_adjust_interval);
@@ -78,99 +77,7 @@ class SharedState {
// for those calls
static thread_local bool ignore_read_error;
SharedState(Env* /*env*/, StressTest* stress_test)
: cv_(&mu_),
seed_(static_cast<uint32_t>(FLAGS_seed)),
max_key_(FLAGS_max_key),
log2_keys_per_lock_(static_cast<uint32_t>(FLAGS_log2_keys_per_lock)),
num_threads_(0),
num_initialized_(0),
num_populated_(0),
vote_reopen_(0),
num_done_(0),
start_(false),
start_verify_(false),
num_bg_threads_(0),
should_stop_bg_thread_(false),
bg_thread_finished_(0),
stress_test_(stress_test),
verification_failure_(false),
should_stop_test_(false),
no_overwrite_ids_(GenerateNoOverwriteIds()),
expected_state_manager_(nullptr),
printing_verification_results_(false),
start_timestamp_(Env::Default()->NowNanos()) {
Status status;
// TODO: We should introduce a way to explicitly disable verification
// during shutdown. When that is disabled and FLAGS_expected_values_dir
// is empty (disabling verification at startup), we can skip tracking
// expected state. Only then should we permit bypassing the below feature
// compatibility checks.
if (!FLAGS_expected_values_dir.empty()) {
if (!std::atomic<uint32_t>{}.is_lock_free() ||
!std::atomic<uint64_t>{}.is_lock_free()) {
std::ostringstream status_s;
status_s << "Cannot use --expected_values_dir on platforms without "
"lock-free "
<< (!std::atomic<uint32_t>{}.is_lock_free()
? "std::atomic<uint32_t>"
: "std::atomic<uint64_t>");
status = Status::InvalidArgument(status_s.str());
}
if (status.ok() && FLAGS_clear_column_family_one_in > 0) {
status = Status::InvalidArgument(
"Cannot use --expected_values_dir on when "
"--clear_column_family_one_in is greater than zero.");
}
}
if (status.ok()) {
if (FLAGS_expected_values_dir.empty()) {
expected_state_manager_.reset(
new AnonExpectedStateManager(FLAGS_max_key, FLAGS_column_families));
} else {
expected_state_manager_.reset(new FileExpectedStateManager(
FLAGS_max_key, FLAGS_column_families, FLAGS_expected_values_dir));
}
status = expected_state_manager_->Open();
}
if (!status.ok()) {
fprintf(stderr, "Failed setting up expected state with error: %s\n",
status.ToString().c_str());
exit(1);
}
if (FLAGS_test_batches_snapshots) {
fprintf(stdout, "No lock creation because test_batches_snapshots set\n");
return;
}
long num_locks = static_cast<long>(max_key_ >> log2_keys_per_lock_);
if (max_key_ & ((1 << log2_keys_per_lock_) - 1)) {
num_locks++;
}
fprintf(stdout, "Creating %ld locks\n", num_locks * FLAGS_column_families);
key_locks_.resize(FLAGS_column_families);
for (int i = 0; i < FLAGS_column_families; ++i) {
key_locks_[i].reset(new port::Mutex[num_locks]);
}
if (FLAGS_read_fault_one_in || FLAGS_metadata_read_fault_one_in) {
#ifdef NDEBUG
// Unsupported in release mode because it relies on
// `IGNORE_STATUS_IF_ERROR` to distinguish faults not expected to lead to
// failure.
fprintf(stderr,
"Cannot set nonzero value for --read_fault_one_in in "
"release mode.");
exit(1);
#else // NDEBUG
SyncPoint::GetInstance()->SetCallBack("FaultInjectionIgnoreError",
IgnoreReadErrorCallback);
SyncPoint::GetInstance()->EnableProcessing();
#endif // NDEBUG
}
}
SharedState(Env* env, StressTest* stress_test);
~SharedState() {
#ifndef NDEBUG
@@ -430,9 +337,7 @@ class SharedState {
return bg_thread_finished_ == num_bg_threads_;
}
bool ShouldVerifyAtBeginning() const {
return !FLAGS_expected_values_dir.empty();
}
bool ShouldVerifyAtBeginning() const;
bool PrintingVerificationResults() {
bool tmp = false;
+4
View File
@@ -15,6 +15,7 @@
#include "rocksdb/statistics.h"
#include "rocksdb/system_clock.h"
#include "util/gflags_compat.h"
#include "util/mutexlock.h"
#include "util/random.h"
DECLARE_bool(histogram);
@@ -181,6 +182,9 @@ class Stats {
double rate = bytes_mb / elapsed;
double throughput = (double)done_ / elapsed;
// Lock stdout so multi-DB reports don't interleave.
static port::Mutex report_mutex;
MutexLock l(&report_mutex);
fprintf(stdout, "%-12s: ", name);
fprintf(stdout, "%.3f micros/op %ld ops/sec\n", seconds_ * 1e6 / done_,
(long)throughput);
File diff suppressed because it is too large. Load diff
+79 -14
View File
@@ -13,6 +13,7 @@
#include "db_stress_tool/db_stress_common.h"
#include "db_stress_tool/db_stress_shared_state.h"
#include "env/composite_env_wrapper.h"
#include "rocksdb/experimental.h"
#include "rocksdb/user_defined_index.h"
#include "utilities/fault_injection_fs.h"
@@ -22,6 +23,7 @@ class SystemClock;
class Transaction;
class TransactionDB;
class OptimisticTransactionDB;
class DbStressFSWrapper;
struct TransactionDBOptions;
using experimental::SstQueryFilterConfigsManager;
@@ -40,11 +42,26 @@ class StressTest {
// from optimistic transactions when conflict detection retries are exhausted.
static bool IsExpectedTxnError(const Status& s);
StressTest();
StressTest(int db_index, const std::string& db_path,
const std::string& ev_path, const std::string& sec_path);
virtual ~StressTest() {}
std::shared_ptr<Cache> NewCache(size_t capacity, int32_t num_shard_bits);
const std::string& GetDbLabel() const;
const std::string& GetDbPath() const;
const std::string& GetExpectedValuesDir() const;
const std::string& GetSecondariesBase() const;
// See db_fault_injection_fs_ member.
std::shared_ptr<FaultInjectionTestFS> GetDbFaultInjectionFs() const {
return db_fault_injection_fs_;
}
// See db_env_ member.
Env* GetDbEnv() const;
static std::shared_ptr<Cache> NewCache(size_t capacity,
int32_t num_shard_bits);
static std::vector<std::string> GetBlobCompressionTags();
@@ -71,6 +88,11 @@ class StressTest {
Options GetOptions(int cf_id);
void CleanUp();
private:
void InitializeListenersForOpen(
SharedState* shared,
const std::vector<ColumnFamilyDescriptor>& cf_descriptors);
protected:
static int GetMinInjectedErrorCount(int error_count_1, int error_count_2) {
if (error_count_1 > 0 && error_count_2 > 0) {
@@ -84,39 +106,34 @@ class StressTest {
}
}
void UpdateIfInitialWriteFails(Env* db_stress_env, const Status& write_s,
void UpdateIfInitialWriteFails(Env* env, const Status& write_s,
Status* initial_write_s,
bool* initial_wal_write_may_succeed,
uint64_t* wait_for_recover_start_time,
bool commit_bypass_memtable = false) {
assert(db_stress_env && initial_write_s && initial_wal_write_may_succeed &&
assert(env && initial_write_s && initial_wal_write_may_succeed &&
wait_for_recover_start_time);
// Only update `initial_write_s`, `initial_wal_write_may_succeed` when the
// first write fails
if (!write_s.ok() && (*initial_write_s).ok()) {
*initial_write_s = write_s;
// With commit_bypass_memtable, we create a new WAL after WAL write
// succeeds, that wal creation may fail due to injected error. So the
// initial wal write may succeed even if status is failed to write to wal
*initial_wal_write_may_succeed =
commit_bypass_memtable ||
!FaultInjectionTestFS::IsFailedToWriteToWALError(*initial_write_s);
*wait_for_recover_start_time = db_stress_env->NowMicros();
*wait_for_recover_start_time = env->NowMicros();
}
}
void PrintWriteRecoveryWaitTimeIfNeeded(Env* db_stress_env,
void PrintWriteRecoveryWaitTimeIfNeeded(Env* env,
const Status& initial_write_s,
bool initial_wal_write_may_succeed,
uint64_t wait_for_recover_start_time,
const std::string& thread_name) {
assert(db_stress_env);
assert(env);
bool waited_for_recovery = !initial_write_s.ok() &&
IsErrorInjectedAndRetryable(initial_write_s) &&
initial_wal_write_may_succeed;
if (waited_for_recovery) {
uint64_t elapsed_sec =
(db_stress_env->NowMicros() - wait_for_recover_start_time) / 1000000;
(env->NowMicros() - wait_for_recover_start_time) / 1000000;
if (elapsed_sec > 10) {
fprintf(stdout,
"%s thread slept to wait for write recovery for "
@@ -303,6 +320,35 @@ class StressTest {
kLastOpSeekToLast
};
// Enum used to track MANIFEST verification mode during DB reopen
enum ManifestVerifyMode {
MANIFEST_VERIFY_NONE,
// MANIFEST file should be reused (same file number), CURRENT should not
// change. Used when reuse_manifest_on_open=1. Warnings are logged on
// failure but test continues.
MANIFEST_VERIFY_REUSE,
// MANIFEST file should be reused AND no recovery writes should occur.
// Used when both reuse_manifest_on_open=1 and
// optimize_manifest_for_recovery=1. Warnings are logged on failure but test
// continues.
MANIFEST_VERIFY_NO_WRITE,
// Strict verification: MANIFEST file must be reused with ZERO writes
// (complete avoidance). Used when ALL conditions for complete avoidance
// are met. Failures are FATAL and will terminate the test.
// Conditions for STRICT mode:
// - Both reuse_manifest_on_open=1 and optimize_manifest_for_recovery=1
// - Not in best_efforts_recovery mode
// - avoid_flush_during_recovery=true (no flush during recovery)
// - write_dbid_to_manifest=0 (no DB_ID write on open)
// - metadata_write_fault_one_in=0 (no fault injection)
// - open_metadata_write_fault_one_in=0 (no fault injection)
// - MANIFEST not corrupted, not at size limit
// Note: avoid_flush_during_shutdown is NOT required. If it leaves data
// in WAL but avoid_flush_during_recovery=true prevents flushing it,
// MANIFEST still won't be written.
MANIFEST_VERIFY_STRICT
};
// Compare the two iterator, iter and cmp_iter are in the same position,
// unless iter might be made invalidate or undefined because of
// upper or lower bounds, or prefix extractor.
@@ -413,7 +459,20 @@ class StressTest {
void CleanUpColumnFamilies();
std::shared_ptr<Cache> cache_;
void RecordManifestStateBeforeReopen();
void VerifyManifestNotRewritten();
int db_index_;
std::string db_label_;
std::string db_path_;
std::string expected_values_path_;
std::string secondaries_path_;
// Wraps raw_env->GetFileSystem(). See DbStressFSWrapper.
std::shared_ptr<DbStressFSWrapper> db_stress_fs_;
// Wraps db_stress_fs_ when NeedsFaultInjection(). Null otherwise.
std::shared_ptr<FaultInjectionTestFS> db_fault_injection_fs_;
// Wraps the outermost FS above. Set as options_.env for all DB I/O.
std::unique_ptr<CompositeEnvWrapper> db_env_;
std::shared_ptr<Cache> compressed_cache_;
std::shared_ptr<const FilterPolicy> filter_policy_;
std::unique_ptr<DB> db_owner_;
@@ -439,6 +498,12 @@ class StressTest {
std::unique_ptr<DB> secondary_db_;
std::vector<ColumnFamilyHandle*> secondary_cfhs_;
bool is_db_stopped_;
// MANIFEST verification state for reopen
ManifestVerifyMode manifest_verify_mode_;
uint64_t manifest_file_number_before_reopen_;
uint64_t manifest_file_size_before_reopen_;
std::string current_file_content_before_reopen_;
};
// Load options from OPTIONS file and populate `options`.
+213 -106
View File
@@ -22,6 +22,7 @@
#ifdef GFLAGS
#include <iostream>
#include <thread>
#include "db_stress_tool/db_stress_common.h"
#include "db_stress_tool/db_stress_driver.h"
@@ -33,11 +34,79 @@
namespace ROCKSDB_NAMESPACE {
namespace {
static std::shared_ptr<ROCKSDB_NAMESPACE::Env> env_guard;
static std::shared_ptr<ROCKSDB_NAMESPACE::Env> env_wrapper_guard;
static std::shared_ptr<ROCKSDB_NAMESPACE::Env> legacy_env_wrapper_guard;
static std::shared_ptr<ROCKSDB_NAMESPACE::CompositeEnvWrapper>
dbsl_env_wrapper_guard;
static std::shared_ptr<CompositeEnvWrapper> fault_env_guard;
// Raw pointers for signal-safe crash callback. Signal handlers can only
// access file-static/global variables; can't capture StressTest instances.
static std::vector<ROCKSDB_NAMESPACE::FaultInjectionTestFS*>
fault_fs_for_crash_report;
int ValidateNumDbsFlags() {
if (FLAGS_num_dbs < 1) {
fprintf(stderr, "Error: --num_dbs must be >= 1\n");
return 1;
}
if (FLAGS_num_dbs > 1) {
if (FLAGS_clear_column_family_one_in > 0) {
fprintf(stderr,
"Error: --num_dbs > 1 incompatible with "
"--clear_column_family_one_in\n");
return 1;
}
if (FLAGS_test_multi_ops_txns) {
fprintf(stderr,
"Error: --num_dbs > 1 incompatible with "
"--test_multi_ops_txns\n");
return 1;
}
}
return 0;
}
int DestroyAllDbs() {
bool all_ok = true;
const int num_dbs = FLAGS_num_dbs;
auto destroy_one = [&](const std::string& db_path) {
Status s = DbStressDestroyDb(db_path);
if (s.ok()) {
fprintf(stdout, "Successfully destroyed db at %s\n", db_path.c_str());
} else {
fprintf(stderr, "Failed to destroy db at %s: %s\n", db_path.c_str(),
s.ToString().c_str());
all_ok = false;
}
};
if (num_dbs == 1) {
destroy_one(FLAGS_db);
} else {
for (int i = 0; i < num_dbs; i++) {
destroy_one(FLAGS_db + "/db_" + std::to_string(i));
}
DestroyDir(raw_env, FLAGS_db);
}
return all_ok ? 0 : 1;
}
void RegisterCrashCallbacks(
const std::vector<std::unique_ptr<StressTest>>& stress_tests, int num_dbs) {
fault_fs_for_crash_report.resize(num_dbs, nullptr);
bool any_fault_fs = false;
for (int i = 0; i < num_dbs; i++) {
fault_fs_for_crash_report[i] =
stress_tests[i]->GetDbFaultInjectionFs().get();
if (fault_fs_for_crash_report[i]) {
any_fault_fs = true;
}
}
if (any_fault_fs) {
port::RegisterCrashCallback([]() {
for (auto* fs : fault_fs_for_crash_report) {
if (fs) {
fs->PrintRecentInjectedErrors();
}
}
});
}
}
int ReturnFlagValidationError(const char* message) {
std::cerr << "Error: " << message << '\n';
@@ -67,8 +136,6 @@ int db_stress_tool(int argc, char** argv) {
StringToCompressionType(FLAGS_bottommost_compression_type.c_str());
checksum_type_e = StringToChecksumType(FLAGS_checksum_type.c_str());
Env* raw_env;
int env_opts = !FLAGS_env_uri.empty() + !FLAGS_fs_uri.empty();
if (env_opts > 1) {
fprintf(stderr, "Error: --env_uri and --fs_uri are mutually exclusive\n");
@@ -82,57 +149,10 @@ int db_stress_tool(int argc, char** argv) {
s.ToString().c_str());
exit(1);
}
dbsl_env_wrapper_guard = std::make_shared<CompositeEnvWrapper>(raw_env);
db_stress_listener_env = dbsl_env_wrapper_guard.get();
if (FLAGS_open_metadata_read_fault_one_in ||
FLAGS_open_metadata_write_fault_one_in || FLAGS_open_read_fault_one_in ||
FLAGS_open_write_fault_one_in || FLAGS_metadata_read_fault_one_in ||
FLAGS_metadata_write_fault_one_in || FLAGS_read_fault_one_in ||
FLAGS_write_fault_one_in || FLAGS_sync_fault_injection) {
FaultInjectionTestFS* fs =
new FaultInjectionTestFS(raw_env->GetFileSystem());
fault_fs_guard.reset(fs);
// Info logs are debugging artifacts, so exclude them from fault injection
// and keep error accounting focused on DB data and metadata.
fault_fs_guard->SetFileTypesExcludedFromFaultInjection(
{FileType::kInfoLogFile});
// Set it to direct writable here to initially bypass any fault injection
// during DB open This will correspondingly be overwritten in
// StressTest::Open() for open fault injection and in RunStressTestImpl()
// for proper fault injection setup.
fault_fs_guard->SetFilesystemDirectWritable(true);
fault_env_guard =
std::make_shared<CompositeEnvWrapper>(raw_env, fault_fs_guard);
raw_env = fault_env_guard.get();
// Register a crash callback so that recently injected errors are
// printed to stderr when the process crashes (SIGABRT, SIGSEGV, etc.).
// This helps diagnose stress test failures caused by fault injection.
port::RegisterCrashCallback([]() {
if (fault_fs_guard) {
fault_fs_guard->PrintRecentInjectedErrors();
}
});
}
auto db_stress_fs =
std::make_shared<DbStressFSWrapper>(raw_env->GetFileSystem());
env_wrapper_guard =
std::make_shared<CompositeEnvWrapper>(raw_env, db_stress_fs);
db_stress_env = env_wrapper_guard.get();
// Handle --destroy_db_and_exit early, before other option validation
// Handle --destroy_db_and_exit early
if (FLAGS_destroy_db_and_exit) {
s = DbStressDestroyDb(FLAGS_db);
if (s.ok()) {
fprintf(stdout, "Successfully destroyed db at %s\n", FLAGS_db.c_str());
return 0;
} else {
fprintf(stderr, "Failed to destroy db at %s: %s\n", FLAGS_db.c_str(),
s.ToString().c_str());
return 1;
}
return DestroyAllDbs();
}
// Handle --delete_dir_and_exit early, before other option validation
@@ -149,14 +169,21 @@ int db_stress_tool(int argc, char** argv) {
}
}
{
int rc = ValidateNumDbsFlags();
if (rc != 0) {
return rc;
}
}
FLAGS_rep_factory = StringToRepFactory(FLAGS_memtablerep.c_str());
// The number of background threads should be at least as much the
// max number of concurrent compactions.
db_stress_env->SetBackgroundThreads(FLAGS_max_background_compactions,
ROCKSDB_NAMESPACE::Env::Priority::LOW);
db_stress_env->SetBackgroundThreads(FLAGS_num_bottom_pri_threads,
ROCKSDB_NAMESPACE::Env::Priority::BOTTOM);
raw_env->SetBackgroundThreads(FLAGS_max_background_compactions,
ROCKSDB_NAMESPACE::Env::Priority::LOW);
raw_env->SetBackgroundThreads(FLAGS_num_bottom_pri_threads,
ROCKSDB_NAMESPACE::Env::Priority::BOTTOM);
if (FLAGS_prefixpercent > 0 && FLAGS_prefix_size < 0) {
fprintf(stderr,
"Error: prefixpercent is non-zero while prefix_size is "
@@ -356,42 +383,75 @@ int db_stress_tool(int argc, char** argv) {
// Choose a location for the test database if none given with --db=<path>
if (FLAGS_db.empty()) {
std::string default_db_path;
db_stress_env->GetTestDirectory(&default_db_path);
raw_env->GetTestDirectory(&default_db_path);
default_db_path += "/dbstress";
FLAGS_db = default_db_path;
}
// Now that FLAGS_db is resolved, set the fault injection log file path
// so that PrintAll() writes to a file instead of stderr (signal-safe).
// Store the log in TEST_TMPDIR (outside the DB directory) so it survives
// DB reopen (which cleans untracked files) and gets included in the
// sandcastle db.tar.gz artifact for post-failure analysis.
if (fault_fs_guard) {
std::string log_dir;
const char* test_tmpdir = getenv("TEST_TMPDIR");
if (test_tmpdir && test_tmpdir[0] != '\0') {
log_dir = test_tmpdir;
} else {
log_dir = "/tmp";
}
std::string log_path = log_dir + "/fault_injection_" +
std::to_string(getpid()) + "_" +
std::to_string(time(nullptr)) + ".log";
fault_fs_guard->SetInjectedErrorLogPath(log_path);
}
if ((FLAGS_test_secondary || FLAGS_continuous_verification_interval > 0) &&
FLAGS_secondaries_base.empty()) {
std::string default_secondaries_path;
db_stress_env->GetTestDirectory(&default_secondaries_path);
default_secondaries_path += "/dbstress_secondaries";
s = db_stress_env->CreateDirIfMissing(default_secondaries_path);
// For num_dbs=1: --db and --expected_values_dir are paths used as-is.
// For num_dbs>1: they are parent directories; C++ creates db_0/, db_1/, ...
// subdirs underneath. DB::Open(create_if_missing) creates each DB dir.
// Python (db_crashtest.py) also creates EV dirs; C++ creates them here as a
// fallback for direct CLI usage.
const int num_dbs = FLAGS_num_dbs;
if (num_dbs > 1) {
s = raw_env->CreateDirIfMissing(FLAGS_db);
if (!s.ok()) {
fprintf(stderr, "Failed to create directory %s: %s\n",
default_secondaries_path.c_str(), s.ToString().c_str());
fprintf(stderr, "Failed to create directory %s: %s\n", FLAGS_db.c_str(),
s.ToString().c_str());
exit(1);
}
FLAGS_secondaries_base = default_secondaries_path;
}
std::vector<std::string> db_paths;
std::vector<std::string> ev_paths;
for (int i = 0; i < num_dbs; i++) {
std::string suffix = (num_dbs == 1) ? "" : "/db_" + std::to_string(i);
db_paths.push_back(FLAGS_db + suffix);
if (!FLAGS_expected_values_dir.empty()) {
std::string ep = FLAGS_expected_values_dir + suffix;
s = Env::Default()->CreateDirIfMissing(ep);
if (!s.ok()) {
fprintf(stderr, "Failed to create directory %s: %s\n", ep.c_str(),
s.ToString().c_str());
exit(1);
}
ev_paths.push_back(std::move(ep));
}
}
if (ev_paths.empty()) {
ev_paths.resize(num_dbs);
}
// Secondary paths: C++ owns creation entirely.
std::vector<std::string> sec_paths;
if (FLAGS_test_secondary || FLAGS_continuous_verification_interval > 0) {
std::string sec_parent;
if (!FLAGS_secondaries_base.empty()) {
sec_parent = FLAGS_secondaries_base;
} else {
raw_env->GetTestDirectory(&sec_parent);
sec_parent += "/dbstress_secondaries";
}
s = raw_env->CreateDirIfMissing(sec_parent);
if (!s.ok()) {
fprintf(stderr, "Failed to create directory %s: %s\n", sec_parent.c_str(),
s.ToString().c_str());
exit(1);
}
for (int i = 0; i < num_dbs; i++) {
std::string suffix = (num_dbs == 1) ? "" : "/db_" + std::to_string(i);
std::string sec_path = sec_parent + suffix;
s = raw_env->CreateDirIfMissing(sec_path);
if (!s.ok()) {
fprintf(stderr, "Failed to create directory %s: %s\n", sec_path.c_str(),
s.ToString().c_str());
exit(1);
}
sec_paths.push_back(std::move(sec_path));
}
}
if (sec_paths.empty()) {
sec_paths.resize(num_dbs);
}
if (FLAGS_best_efforts_recovery &&
@@ -511,25 +571,72 @@ int db_stress_tool(int argc, char** argv) {
key_gen_ctx.weights.emplace_back(key_gen_ctx.window -
keys_per_level * (levels - 1));
}
std::unique_ptr<ROCKSDB_NAMESPACE::SharedState> shared;
std::unique_ptr<ROCKSDB_NAMESPACE::StressTest> stress;
if (FLAGS_test_cf_consistency) {
stress.reset(CreateCfConsistencyStressTest());
} else if (FLAGS_test_batches_snapshots) {
stress.reset(CreateBatchedOpsStressTest());
} else if (FLAGS_test_multi_ops_txns) {
stress.reset(CreateMultiOpsTxnsStressTest());
} else {
stress.reset(CreateNonBatchedOpsStressTest());
}
// Initialize the Zipfian pre-calculated array
// Initialize shared resources (hot-key generator, block cache, WBM,
// rate limiter) once so that all DB instances share them.
InitializeHotKeyGenerator(FLAGS_hot_key_alpha);
shared.reset(new SharedState(db_stress_env, stress.get()));
bool run_stress_test = RunStressTest(shared.get());
// Close DB in CleanUp() before destructor to prevent race between destructor
// and operations in listener callbacks (e.g. MultiOpsTxnsStressListener).
stress->CleanUp();
return run_stress_test ? 0 : 1;
block_cache =
StressTest::NewCache(FLAGS_cache_size, FLAGS_cache_numshardbits);
if (FLAGS_use_write_buffer_manager) {
wbm = std::make_shared<WriteBufferManager>(FLAGS_db_write_buffer_size,
block_cache);
}
if (FLAGS_rate_limiter_bytes_per_sec > 0) {
rate_limiter.reset(NewGenericRateLimiter(
FLAGS_rate_limiter_bytes_per_sec, 1000 /* refill_period_us */,
10 /* fairness */,
FLAGS_rate_limit_bg_reads ? RateLimiter::Mode::kReadsOnly
: RateLimiter::Mode::kWritesOnly));
}
// Phase 1: Create StressTest instances (one per DB).
std::vector<std::unique_ptr<StressTest>> stress_tests(num_dbs);
for (int i = 0; i < num_dbs; i++) {
if (FLAGS_test_cf_consistency) {
stress_tests[i].reset(CreateCfConsistencyStressTest(
i, db_paths[i], ev_paths[i], sec_paths[i]));
} else if (FLAGS_test_batches_snapshots) {
stress_tests[i].reset(CreateBatchedOpsStressTest(
i, db_paths[i], ev_paths[i], sec_paths[i]));
} else if (FLAGS_test_multi_ops_txns) {
stress_tests[i].reset(CreateMultiOpsTxnsStressTest(
i, db_paths[i], ev_paths[i], sec_paths[i]));
} else {
stress_tests[i].reset(CreateNonBatchedOpsStressTest(
i, db_paths[i], ev_paths[i], sec_paths[i]));
}
}
RegisterCrashCallbacks(stress_tests, num_dbs);
// Phase 2: Create SharedState for every DB before any worker thread starts.
std::vector<std::unique_ptr<SharedState>> shared_states(num_dbs);
for (int i = 0; i < num_dbs; i++) {
shared_states[i].reset(new SharedState(raw_env, stress_tests[i].get()));
}
// Phase 3: Launch each DB's stress test on its own thread.
std::vector<int> results(num_dbs, 0);
std::vector<std::thread> stress_test_runners;
stress_test_runners.reserve(num_dbs);
for (int i = 0; i < num_dbs; i++) {
stress_test_runners.emplace_back([i, &results, &shared_states]() {
results[i] = RunStressTest(shared_states[i].get()) ? 1 : 0;
});
}
// Phase 4: Wait for all DB threads, collect results, clean up.
bool all_passed = true;
for (int i = 0; i < num_dbs; i++) {
stress_test_runners[i].join();
if (!results[i]) {
all_passed = false;
}
stress_tests[i]->CleanUp();
}
for (auto& fs : fault_fs_for_crash_report) {
fs = nullptr;
}
return all_passed ? 0 : 1;
}
} // namespace ROCKSDB_NAMESPACE
+8 -5
View File
@@ -40,7 +40,7 @@ DEFINE_int32(clear_wp_commit_cache_one_in, 0,
"write-unprepared transactions.");
extern "C" bool rocksdb_write_prepared_TEST_ShouldClearCommitCache(void) {
static Random rand(static_cast<uint32_t>(db_stress_env->NowMicros()));
static Random rand(static_cast<uint32_t>(raw_env->NowMicros()));
return FLAGS_clear_wp_commit_cache_one_in > 0 &&
rand.OneIn(FLAGS_clear_wp_commit_cache_one_in);
}
@@ -1413,7 +1413,7 @@ Status MultiOpsTxnsStressTest::CommitAndCreateTimestampedSnapshotIfNeeded(
Status s;
if (FLAGS_create_timestamped_snapshot_one_in > 0 &&
thread->rand.OneInOpt(FLAGS_create_timestamped_snapshot_one_in)) {
uint64_t ts = db_stress_env->NowNanos();
uint64_t ts = raw_env->NowNanos();
std::shared_ptr<const Snapshot> snapshot;
s = txn.CommitAndTryCreateSnapshot(/*notifier=*/nullptr, ts, &snapshot);
} else {
@@ -1428,7 +1428,7 @@ Status MultiOpsTxnsStressTest::CommitAndCreateTimestampedSnapshotIfNeeded(
assert(txn_db_);
if (FLAGS_create_timestamped_snapshot_one_in > 0 &&
thread->rand.OneInOpt(50000)) {
uint64_t now = db_stress_env->NowNanos();
uint64_t now = raw_env->NowNanos();
constexpr uint64_t time_diff = static_cast<uint64_t>(1000) * 1000 * 1000;
txn_db_->ReleaseTimestampedSnapshotsOlderThan(now - time_diff);
}
@@ -1767,8 +1767,11 @@ void MultiOpsTxnsStressTest::ScanExistingDb(SharedState* shared, int threads) {
}
}
StressTest* CreateMultiOpsTxnsStressTest() {
return new MultiOpsTxnsStressTest();
StressTest* CreateMultiOpsTxnsStressTest(int db_index,
const std::string& db_path,
const std::string& ev_path,
const std::string& sec_path) {
return new MultiOpsTxnsStressTest(db_index, db_path, ev_path, sec_path);
}
void CheckAndSetOptionsForMultiOpsTxnStressTest() {
+14 -1
View File
@@ -9,6 +9,7 @@
#ifdef GFLAGS
#include "db_stress_tool/db_stress_common.h"
#include "util/atomic.h"
namespace ROCKSDB_NAMESPACE {
@@ -191,7 +192,10 @@ class MultiOpsTxnsStressTest : public StressTest {
uint32_t c_{0};
};
MultiOpsTxnsStressTest() {}
MultiOpsTxnsStressTest(int db_index, const std::string& db_path,
const std::string& ev_path,
const std::string& sec_path)
: StressTest(db_index, db_path, ev_path, sec_path) {}
~MultiOpsTxnsStressTest() override {}
@@ -423,11 +427,16 @@ class MultiOpsTxnsStressListener : public EventListener {
~MultiOpsTxnsStressListener() override {}
void OnDBShutdownBegin(DB* /*db*/) override { shutting_down_.Store(true); }
void OnFlushCompleted(DB* db, const FlushJobInfo& info) override {
assert(db);
#ifdef NDEBUG
(void)db;
#endif
if (shutting_down_.Load()) {
return;
}
assert(info.cf_id == 0);
const ReadOptions read_options(Env::IOActivity::kFlush);
stress_test_->VerifyPkSkFast(read_options, info.job_id);
@@ -438,6 +447,9 @@ class MultiOpsTxnsStressListener : public EventListener {
#ifdef NDEBUG
(void)db;
#endif
if (shutting_down_.Load()) {
return;
}
assert(info.cf_id == 0);
const ReadOptions read_options(Env::IOActivity::kCompaction);
stress_test_->VerifyPkSkFast(read_options, info.job_id);
@@ -445,6 +457,7 @@ class MultiOpsTxnsStressListener : public EventListener {
private:
MultiOpsTxnsStressTest* const stress_test_ = nullptr;
Atomic<bool> shutting_down_{false};
};
} // namespace ROCKSDB_NAMESPACE
+109 -103
View File
@@ -24,7 +24,10 @@
namespace ROCKSDB_NAMESPACE {
class NonBatchedOpsStressTest : public StressTest {
public:
NonBatchedOpsStressTest() = default;
NonBatchedOpsStressTest(int db_index, const std::string& db_path,
const std::string& ev_path,
const std::string& sec_path)
: StressTest(db_index, db_path, ev_path, sec_path) {}
virtual ~NonBatchedOpsStressTest() = default;
@@ -265,20 +268,20 @@ class NonBatchedOpsStressTest : public StressTest {
std::string from_db;
// Temporarily disable error injection to verify the secondary
if (fault_fs_guard) {
fault_fs_guard->DisableThreadLocalErrorInjection(
if (db_fault_injection_fs_) {
db_fault_injection_fs_->DisableThreadLocalErrorInjection(
FaultInjectionIOType::kRead);
fault_fs_guard->DisableThreadLocalErrorInjection(
db_fault_injection_fs_->DisableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
}
s = secondary_db_->Get(options, secondary_cfhs_[cf], key, &from_db);
// Re-enable error injection after verifying the secondary
if (fault_fs_guard) {
fault_fs_guard->EnableThreadLocalErrorInjection(
if (db_fault_injection_fs_) {
db_fault_injection_fs_->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kRead);
fault_fs_guard->EnableThreadLocalErrorInjection(
db_fault_injection_fs_->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
}
@@ -688,10 +691,10 @@ class NonBatchedOpsStressTest : public StressTest {
bool read_older_ts = MaybeUseOlderTimestampForPointLookup(
thread, read_ts_str, read_ts_slice, read_opts_copy);
if (fault_fs_guard) {
fault_fs_guard->GetAndResetInjectedThreadLocalErrorCount(
if (db_fault_injection_fs_) {
db_fault_injection_fs_->GetAndResetInjectedThreadLocalErrorCount(
FaultInjectionIOType::kRead);
fault_fs_guard->GetAndResetInjectedThreadLocalErrorCount(
db_fault_injection_fs_->GetAndResetInjectedThreadLocalErrorCount(
FaultInjectionIOType::kMetadataRead);
SharedState::ignore_read_error = false;
}
@@ -703,11 +706,11 @@ class NonBatchedOpsStressTest : public StressTest {
thread->shared->Get(rand_column_families[0], rand_keys[0]);
int injected_error_count = 0;
if (fault_fs_guard) {
if (db_fault_injection_fs_) {
injected_error_count = GetMinInjectedErrorCount(
fault_fs_guard->GetAndResetInjectedThreadLocalErrorCount(
db_fault_injection_fs_->GetAndResetInjectedThreadLocalErrorCount(
FaultInjectionIOType::kRead),
fault_fs_guard->GetAndResetInjectedThreadLocalErrorCount(
db_fault_injection_fs_->GetAndResetInjectedThreadLocalErrorCount(
FaultInjectionIOType::kMetadataRead));
if (!SharedState::ignore_read_error && injected_error_count > 0 &&
(s.ok() || s.IsNotFound())) {
@@ -716,9 +719,9 @@ class NonBatchedOpsStressTest : public StressTest {
MutexLock l(thread->shared->GetMutex());
fprintf(stderr, "Didn't get expected error from Get\n");
fprintf(stderr, "Callstack that injected the fault\n");
fault_fs_guard->PrintInjectedThreadLocalErrorBacktrace(
db_fault_injection_fs_->PrintInjectedThreadLocalErrorBacktrace(
FaultInjectionIOType::kRead);
fault_fs_guard->PrintInjectedThreadLocalErrorBacktrace(
db_fault_injection_fs_->PrintInjectedThreadLocalErrorBacktrace(
FaultInjectionIOType::kMetadataRead);
std::terminate();
}
@@ -821,10 +824,10 @@ class NonBatchedOpsStressTest : public StressTest {
std::unique_ptr<Transaction> txn;
if (use_txn) {
// TODO(hx235): test fault injection with MultiGet() with transactions
if (fault_fs_guard) {
fault_fs_guard->DisableThreadLocalErrorInjection(
if (db_fault_injection_fs_) {
db_fault_injection_fs_->DisableThreadLocalErrorInjection(
FaultInjectionIOType::kRead);
fault_fs_guard->DisableThreadLocalErrorInjection(
db_fault_injection_fs_->DisableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
}
WriteOptions wo;
@@ -850,20 +853,20 @@ class NonBatchedOpsStressTest : public StressTest {
int injected_error_count = 0;
if (!use_txn) {
if (fault_fs_guard) {
fault_fs_guard->GetAndResetInjectedThreadLocalErrorCount(
if (db_fault_injection_fs_) {
db_fault_injection_fs_->GetAndResetInjectedThreadLocalErrorCount(
FaultInjectionIOType::kRead);
fault_fs_guard->GetAndResetInjectedThreadLocalErrorCount(
db_fault_injection_fs_->GetAndResetInjectedThreadLocalErrorCount(
FaultInjectionIOType::kMetadataRead);
SharedState::ignore_read_error = false;
}
db_->MultiGet(readoptionscopy, cfh, num_keys, keys.data(), values.data(),
statuses.data());
if (fault_fs_guard) {
if (db_fault_injection_fs_) {
injected_error_count = GetMinInjectedErrorCount(
fault_fs_guard->GetAndResetInjectedThreadLocalErrorCount(
db_fault_injection_fs_->GetAndResetInjectedThreadLocalErrorCount(
FaultInjectionIOType::kRead),
fault_fs_guard->GetAndResetInjectedThreadLocalErrorCount(
db_fault_injection_fs_->GetAndResetInjectedThreadLocalErrorCount(
FaultInjectionIOType::kMetadataRead));
if (injected_error_count > 0) {
@@ -883,9 +886,9 @@ class NonBatchedOpsStressTest : public StressTest {
"num_keys %zu Expected %d errors, seen at least %d\n",
num_keys, injected_error_count, stat_nok_nfound);
fprintf(stderr, "Callstack that injected the fault\n");
fault_fs_guard->PrintInjectedThreadLocalErrorBacktrace(
db_fault_injection_fs_->PrintInjectedThreadLocalErrorBacktrace(
FaultInjectionIOType::kRead);
fault_fs_guard->PrintInjectedThreadLocalErrorBacktrace(
db_fault_injection_fs_->PrintInjectedThreadLocalErrorBacktrace(
FaultInjectionIOType::kMetadataRead);
std::terminate();
}
@@ -952,10 +955,10 @@ class NonBatchedOpsStressTest : public StressTest {
const Status& s,
const std::optional<ExpectedValue>& ryw_expected_value) -> bool {
// Temporarily disable error injection for verification
if (fault_fs_guard) {
fault_fs_guard->DisableThreadLocalErrorInjection(
if (db_fault_injection_fs_) {
db_fault_injection_fs_->DisableThreadLocalErrorInjection(
FaultInjectionIOType::kRead);
fault_fs_guard->DisableThreadLocalErrorInjection(
db_fault_injection_fs_->DisableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
}
@@ -1049,10 +1052,10 @@ class NonBatchedOpsStressTest : public StressTest {
}
// Enable back error injection disbled for checking results
if (fault_fs_guard) {
fault_fs_guard->DisableThreadLocalErrorInjection(
if (db_fault_injection_fs_) {
db_fault_injection_fs_->DisableThreadLocalErrorInjection(
FaultInjectionIOType::kRead);
fault_fs_guard->DisableThreadLocalErrorInjection(
db_fault_injection_fs_->DisableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
}
return check_multiget_res;
@@ -1091,10 +1094,10 @@ class NonBatchedOpsStressTest : public StressTest {
if (use_txn) {
txn->Rollback().PermitUncheckedError();
// Enable back error injection disbled for transactions
if (fault_fs_guard) {
fault_fs_guard->EnableThreadLocalErrorInjection(
if (db_fault_injection_fs_) {
db_fault_injection_fs_->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kRead);
fault_fs_guard->EnableThreadLocalErrorInjection(
db_fault_injection_fs_->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
}
}
@@ -1141,10 +1144,10 @@ class NonBatchedOpsStressTest : public StressTest {
const ExpectedValue pre_read_expected_value =
thread->shared->Get(column_family, key);
if (fault_fs_guard) {
fault_fs_guard->GetAndResetInjectedThreadLocalErrorCount(
if (db_fault_injection_fs_) {
db_fault_injection_fs_->GetAndResetInjectedThreadLocalErrorCount(
FaultInjectionIOType::kRead);
fault_fs_guard->GetAndResetInjectedThreadLocalErrorCount(
db_fault_injection_fs_->GetAndResetInjectedThreadLocalErrorCount(
FaultInjectionIOType::kMetadataRead);
SharedState::ignore_read_error = false;
}
@@ -1164,11 +1167,11 @@ class NonBatchedOpsStressTest : public StressTest {
thread->shared->Get(column_family, key);
int injected_error_count = 0;
if (fault_fs_guard) {
if (db_fault_injection_fs_) {
injected_error_count = GetMinInjectedErrorCount(
fault_fs_guard->GetAndResetInjectedThreadLocalErrorCount(
db_fault_injection_fs_->GetAndResetInjectedThreadLocalErrorCount(
FaultInjectionIOType::kRead),
fault_fs_guard->GetAndResetInjectedThreadLocalErrorCount(
db_fault_injection_fs_->GetAndResetInjectedThreadLocalErrorCount(
FaultInjectionIOType::kMetadataRead));
if (!SharedState::ignore_read_error && injected_error_count > 0 &&
(s.ok() || s.IsNotFound())) {
@@ -1177,9 +1180,9 @@ class NonBatchedOpsStressTest : public StressTest {
MutexLock l(thread->shared->GetMutex());
fprintf(stderr, "Didn't get expected error from GetEntity\n");
fprintf(stderr, "Callstack that injected the fault\n");
fault_fs_guard->PrintInjectedThreadLocalErrorBacktrace(
db_fault_injection_fs_->PrintInjectedThreadLocalErrorBacktrace(
FaultInjectionIOType::kRead);
fault_fs_guard->PrintInjectedThreadLocalErrorBacktrace(
db_fault_injection_fs_->PrintInjectedThreadLocalErrorBacktrace(
FaultInjectionIOType::kMetadataRead);
std::terminate();
}
@@ -1281,10 +1284,10 @@ class NonBatchedOpsStressTest : public StressTest {
if (FLAGS_use_txn) {
// TODO(hx235): test fault injection with MultiGetEntity() with
// transactions
if (fault_fs_guard) {
fault_fs_guard->DisableThreadLocalErrorInjection(
if (db_fault_injection_fs_) {
db_fault_injection_fs_->DisableThreadLocalErrorInjection(
FaultInjectionIOType::kRead);
fault_fs_guard->DisableThreadLocalErrorInjection(
db_fault_injection_fs_->DisableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
}
WriteOptions write_options;
@@ -1318,11 +1321,11 @@ class NonBatchedOpsStressTest : public StressTest {
int injected_error_count = 0;
auto verify_expected_errors = [&](auto get_status) {
assert(fault_fs_guard);
assert(db_fault_injection_fs_);
injected_error_count = GetMinInjectedErrorCount(
fault_fs_guard->GetAndResetInjectedThreadLocalErrorCount(
db_fault_injection_fs_->GetAndResetInjectedThreadLocalErrorCount(
FaultInjectionIOType::kRead),
fault_fs_guard->GetAndResetInjectedThreadLocalErrorCount(
db_fault_injection_fs_->GetAndResetInjectedThreadLocalErrorCount(
FaultInjectionIOType::kMetadataRead));
if (injected_error_count) {
int stat_nok_nfound = 0;
@@ -1344,9 +1347,9 @@ class NonBatchedOpsStressTest : public StressTest {
fprintf(stderr, "num_keys %zu Expected %d errors, seen %d\n",
num_keys, injected_error_count, stat_nok_nfound);
fprintf(stderr, "Call stack that injected the fault\n");
fault_fs_guard->PrintInjectedThreadLocalErrorBacktrace(
db_fault_injection_fs_->PrintInjectedThreadLocalErrorBacktrace(
FaultInjectionIOType::kRead);
fault_fs_guard->PrintInjectedThreadLocalErrorBacktrace(
db_fault_injection_fs_->PrintInjectedThreadLocalErrorBacktrace(
FaultInjectionIOType::kMetadataRead);
std::terminate();
}
@@ -1356,10 +1359,10 @@ class NonBatchedOpsStressTest : public StressTest {
auto check_results = [&](auto get_columns, auto get_status,
auto do_extra_check, auto call_get_entity) {
// Temporarily disable error injection for checking results
if (fault_fs_guard) {
fault_fs_guard->DisableThreadLocalErrorInjection(
if (db_fault_injection_fs_) {
db_fault_injection_fs_->DisableThreadLocalErrorInjection(
FaultInjectionIOType::kRead);
fault_fs_guard->DisableThreadLocalErrorInjection(
db_fault_injection_fs_->DisableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
}
const bool check_get_entity =
@@ -1538,10 +1541,10 @@ class NonBatchedOpsStressTest : public StressTest {
}
}
// Enable back error injection disbled for checking results
if (fault_fs_guard) {
fault_fs_guard->EnableThreadLocalErrorInjection(
if (db_fault_injection_fs_) {
db_fault_injection_fs_->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kRead);
fault_fs_guard->EnableThreadLocalErrorInjection(
db_fault_injection_fs_->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
}
};
@@ -1624,19 +1627,19 @@ class NonBatchedOpsStressTest : public StressTest {
txn->Rollback().PermitUncheckedError();
// Enable back error injection disbled for transactions
if (fault_fs_guard) {
fault_fs_guard->EnableThreadLocalErrorInjection(
if (db_fault_injection_fs_) {
db_fault_injection_fs_->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kRead);
fault_fs_guard->EnableThreadLocalErrorInjection(
db_fault_injection_fs_->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
}
} else if (FLAGS_use_attribute_group) {
// AttributeGroup MultiGetEntity verification
if (fault_fs_guard) {
fault_fs_guard->GetAndResetInjectedThreadLocalErrorCount(
if (db_fault_injection_fs_) {
db_fault_injection_fs_->GetAndResetInjectedThreadLocalErrorCount(
FaultInjectionIOType::kRead);
fault_fs_guard->GetAndResetInjectedThreadLocalErrorCount(
db_fault_injection_fs_->GetAndResetInjectedThreadLocalErrorCount(
FaultInjectionIOType::kMetadataRead);
SharedState::ignore_read_error = false;
}
@@ -1652,7 +1655,7 @@ class NonBatchedOpsStressTest : public StressTest {
db_->MultiGetEntity(read_opts_copy, num_keys, key_slices.data(),
results.data());
if (fault_fs_guard) {
if (db_fault_injection_fs_) {
verify_expected_errors(
[&](size_t i) { return results[i][0].status(); });
}
@@ -1668,10 +1671,10 @@ class NonBatchedOpsStressTest : public StressTest {
} else {
// Non-AttributeGroup MultiGetEntity verification
if (fault_fs_guard) {
fault_fs_guard->GetAndResetInjectedThreadLocalErrorCount(
if (db_fault_injection_fs_) {
db_fault_injection_fs_->GetAndResetInjectedThreadLocalErrorCount(
FaultInjectionIOType::kRead);
fault_fs_guard->GetAndResetInjectedThreadLocalErrorCount(
db_fault_injection_fs_->GetAndResetInjectedThreadLocalErrorCount(
FaultInjectionIOType::kMetadataRead);
SharedState::ignore_read_error = false;
}
@@ -1682,7 +1685,7 @@ class NonBatchedOpsStressTest : public StressTest {
db_->MultiGetEntity(read_opts_copy, cfh, num_keys, key_slices.data(),
results.data(), statuses.data());
if (fault_fs_guard) {
if (db_fault_injection_fs_) {
verify_expected_errors([&](size_t i) { return statuses[i]; });
}
@@ -1733,10 +1736,10 @@ class NonBatchedOpsStressTest : public StressTest {
MaybeUseOlderTimestampForRangeScan(thread, read_ts_str, read_ts_slice,
ro_copy);
if (fault_fs_guard) {
fault_fs_guard->GetAndResetInjectedThreadLocalErrorCount(
if (db_fault_injection_fs_) {
db_fault_injection_fs_->GetAndResetInjectedThreadLocalErrorCount(
FaultInjectionIOType::kRead);
fault_fs_guard->GetAndResetInjectedThreadLocalErrorCount(
db_fault_injection_fs_->GetAndResetInjectedThreadLocalErrorCount(
FaultInjectionIOType::kMetadataRead);
SharedState::ignore_read_error = false;
}
@@ -1798,11 +1801,11 @@ class NonBatchedOpsStressTest : public StressTest {
}
int injected_error_count = 0;
if (fault_fs_guard) {
if (db_fault_injection_fs_) {
injected_error_count = GetMinInjectedErrorCount(
fault_fs_guard->GetAndResetInjectedThreadLocalErrorCount(
db_fault_injection_fs_->GetAndResetInjectedThreadLocalErrorCount(
FaultInjectionIOType::kRead),
fault_fs_guard->GetAndResetInjectedThreadLocalErrorCount(
db_fault_injection_fs_->GetAndResetInjectedThreadLocalErrorCount(
FaultInjectionIOType::kMetadataRead));
if (!SharedState::ignore_read_error && injected_error_count > 0 &&
s.ok()) {
@@ -1811,9 +1814,9 @@ class NonBatchedOpsStressTest : public StressTest {
MutexLock l(thread->shared->GetMutex());
fprintf(stderr, "Didn't get expected error from PrefixScan\n");
fprintf(stderr, "Callstack that injected the fault\n");
fault_fs_guard->PrintInjectedThreadLocalErrorBacktrace(
db_fault_injection_fs_->PrintInjectedThreadLocalErrorBacktrace(
FaultInjectionIOType::kRead);
fault_fs_guard->PrintInjectedThreadLocalErrorBacktrace(
db_fault_injection_fs_->PrintInjectedThreadLocalErrorBacktrace(
FaultInjectionIOType::kMetadataRead);
std::terminate();
}
@@ -1880,10 +1883,10 @@ class NonBatchedOpsStressTest : public StressTest {
if (FLAGS_verify_before_write) {
// Temporarily disable error injection for preparation
if (fault_fs_guard) {
fault_fs_guard->DisableThreadLocalErrorInjection(
if (db_fault_injection_fs_) {
db_fault_injection_fs_->DisableThreadLocalErrorInjection(
FaultInjectionIOType::kRead);
fault_fs_guard->DisableThreadLocalErrorInjection(
db_fault_injection_fs_->DisableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
}
@@ -1894,10 +1897,10 @@ class NonBatchedOpsStressTest : public StressTest {
/* msg_prefix */ "Pre-Put Get verification", from_db, s);
// Enable back error injection disabled for preparation
if (fault_fs_guard) {
fault_fs_guard->EnableThreadLocalErrorInjection(
if (db_fault_injection_fs_) {
db_fault_injection_fs_->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kRead);
fault_fs_guard->EnableThreadLocalErrorInjection(
db_fault_injection_fs_->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
}
if (!res) {
@@ -2000,7 +2003,7 @@ class NonBatchedOpsStressTest : public StressTest {
&commit_bypass_memtable);
}
}
UpdateIfInitialWriteFails(db_stress_env, s, &initial_write_s,
UpdateIfInitialWriteFails(raw_env, s, &initial_write_s,
&initial_wal_write_may_succeed,
&wait_for_recover_start_time);
@@ -2031,7 +2034,7 @@ class NonBatchedOpsStressTest : public StressTest {
}
} else {
PrintWriteRecoveryWaitTimeIfNeeded(
db_stress_env, initial_write_s, initial_wal_write_may_succeed,
raw_env, initial_write_s, initial_wal_write_may_succeed,
wait_for_recover_start_time, "TestPut");
pending_expected_value.Commit();
thread->stats.AddBytesForWrites(1, sz);
@@ -2107,7 +2110,7 @@ class NonBatchedOpsStressTest : public StressTest {
&commit_bypass_memtable);
}
UpdateIfInitialWriteFails(
db_stress_env, s, &initial_write_s, &initial_wal_write_may_succeed,
raw_env, s, &initial_write_s, &initial_wal_write_may_succeed,
&wait_for_recover_start_time, commit_bypass_memtable);
} while (!s.ok() && IsErrorInjectedAndRetryable(s) &&
initial_wal_write_may_succeed);
@@ -2137,7 +2140,7 @@ class NonBatchedOpsStressTest : public StressTest {
}
} else {
PrintWriteRecoveryWaitTimeIfNeeded(
db_stress_env, initial_write_s, initial_wal_write_may_succeed,
raw_env, initial_write_s, initial_wal_write_may_succeed,
wait_for_recover_start_time, "TestDelete");
pending_expected_value.Commit();
thread->stats.AddDeletes(1);
@@ -2179,7 +2182,7 @@ class NonBatchedOpsStressTest : public StressTest {
&commit_bypass_memtable);
}
UpdateIfInitialWriteFails(
db_stress_env, s, &initial_write_s, &initial_wal_write_may_succeed,
raw_env, s, &initial_write_s, &initial_wal_write_may_succeed,
&wait_for_recover_start_time, commit_bypass_memtable);
} while (!s.ok() && IsErrorInjectedAndRetryable(s) &&
initial_wal_write_may_succeed);
@@ -2209,7 +2212,7 @@ class NonBatchedOpsStressTest : public StressTest {
}
} else {
PrintWriteRecoveryWaitTimeIfNeeded(
db_stress_env, initial_write_s, initial_wal_write_may_succeed,
raw_env, initial_write_s, initial_wal_write_may_succeed,
wait_for_recover_start_time, "TestDelete");
pending_expected_value.Commit();
thread->stats.AddSingleDeletes(1);
@@ -2274,7 +2277,7 @@ class NonBatchedOpsStressTest : public StressTest {
} else {
s = db_->DeleteRange(write_opts, cfh, key, end_key);
}
UpdateIfInitialWriteFails(db_stress_env, s, &initial_write_s,
UpdateIfInitialWriteFails(raw_env, s, &initial_write_s,
&initial_wal_write_may_succeed,
&wait_for_recover_start_time);
} while (!s.ok() && IsErrorInjectedAndRetryable(s) &&
@@ -2302,7 +2305,7 @@ class NonBatchedOpsStressTest : public StressTest {
}
} else {
PrintWriteRecoveryWaitTimeIfNeeded(
db_stress_env, initial_write_s, initial_wal_write_may_succeed,
raw_env, initial_write_s, initial_wal_write_may_succeed,
wait_for_recover_start_time, "TestDeleteRange");
for (PendingExpectedValue& pending_expected_value :
pending_expected_values) {
@@ -2325,11 +2328,11 @@ class NonBatchedOpsStressTest : public StressTest {
FLAGS_test_ingest_standalone_range_deletion_one_in);
std::vector<std::string> external_files;
const std::string sst_filename =
FLAGS_db + "/." + std::to_string(thread->tid) + ".sst";
GetDbPath() + "/." + std::to_string(thread->tid) + ".sst";
external_files.push_back(sst_filename);
std::string standalone_rangedel_filename;
if (test_standalone_range_deletion) {
standalone_rangedel_filename = FLAGS_db + "/." +
standalone_rangedel_filename = GetDbPath() + "/." +
std::to_string(thread->tid) +
"_standalone_rangedel.sst";
external_files.push_back(standalone_rangedel_filename);
@@ -2338,28 +2341,28 @@ class NonBatchedOpsStressTest : public StressTest {
std::ostringstream ingest_options_oss;
// Temporarily disable error injection for preparation
if (fault_fs_guard) {
fault_fs_guard->DisableThreadLocalErrorInjection(
if (db_fault_injection_fs_) {
db_fault_injection_fs_->DisableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
fault_fs_guard->DisableThreadLocalErrorInjection(
db_fault_injection_fs_->DisableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataWrite);
}
for (const auto& filename : external_files) {
if (db_stress_env->FileExists(filename).ok()) {
if (raw_env->FileExists(filename).ok()) {
// Maybe we terminated abnormally before, so cleanup to give this file
// ingestion a clean slate
s = db_stress_env->DeleteFile(filename);
s = raw_env->DeleteFile(filename);
}
if (!s.ok()) {
return;
}
}
if (fault_fs_guard) {
fault_fs_guard->EnableThreadLocalErrorInjection(
if (db_fault_injection_fs_) {
db_fault_injection_fs_->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
fault_fs_guard->EnableThreadLocalErrorInjection(
db_fault_injection_fs_->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataWrite);
}
@@ -2900,7 +2903,7 @@ class NonBatchedOpsStressTest : public StressTest {
op_logs += "N";
}
// backward scan — skip when backward iteration is not supported
// backward scan -- skip when backward iteration is not supported
if (FLAGS_test_backward_scan) {
key_str = Key(ub - 1);
iter->SeekForPrev(key_str);
@@ -3485,8 +3488,11 @@ class NonBatchedOpsStressTest : public StressTest {
}
};
StressTest* CreateNonBatchedOpsStressTest() {
return new NonBatchedOpsStressTest();
StressTest* CreateNonBatchedOpsStressTest(int db_index,
const std::string& db_path,
const std::string& ev_path,
const std::string& sec_path) {
return new NonBatchedOpsStressTest(db_index, db_path, ev_path, sec_path);
}
} // namespace ROCKSDB_NAMESPACE
Loaded 100 of 223 files, more files were not shown because too many files have changed in this diff. Show more