Compare commits

...
49 Commits
Author SHA1 Message Date
Richard Barnes fa616f0e79 Fix string-conversion issue in internal_repo_rocksdb/repo/utilities/persistent_cache/volatile_tier_impl.cc +5 (#14296)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14296

This could is triggering `-Wstring-conversion`, which presents as:
```
warning: implicit conversion turns string literal into bool: A to B
```
This is often a bug and what was intended. The most frequent cause is the code was:
```
void foo(bool) { ... }
void foo(std::string) { ... }
foo("this gets interpreted as a bool");
```

It is also possible the issue is innocuous as part of an assert:
```
assert(!"this string is true, so the assertion is false");
EXPECT_FALSE("this string is true, so the expect fails");
```
in these cases the use is to "cute", so we modify the code to make it more obvious.
```
assert(false && "the compiler recognizes and doesn't complain about this pattern");
FAIL() << "much more obvious";
```

Reviewed By: anand1976

Differential Revision: D92013593

fbshipit-source-id: 0b4e00339bef3f76fc5b9ad35e2383c5e4f828f9
2026-02-25 10:30:50 -08:00
Richard Barnes 96db15c6e6 Fix string-conversion issue in internal_repo_rocksdb/repo/table/block_based/index_builder.cc +1
Summary:
This could is triggering `-Wstring-conversion`, which presents as:
```
warning: implicit conversion turns string literal into bool: A to B
```
This is often a bug and what was intended. The most frequent cause is the code was:
```
void foo(bool) { ... }
void foo(std::string) { ... }
foo("this gets interpreted as a bool");
```

It is also possible the issue is innocuous as part of an assert:
```
assert(false && "this string is true, so the assertion is false");
EXPECT_FALSE("this string is true, so the expect fails");
```
in these cases the use is to "cute", so we modify the code to make it more obvious.
```
assert(false && "the compiler recognizes and doesn't complain about this pattern");
FAIL() << "much more obvious";
```

Differential Revision: D92886041

fbshipit-source-id: 6adfaa102f12e293491cc579ec92b48834d1d0a8
2026-02-25 10:30:50 -08:00
Richard Barnes 912596031a Fix string-conversion issue in internal_repo_rocksdb/repo/utilities/persistent_cache/block_cache_tier_file.cc +2 (#14312)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14312

This could is triggering `-Wstring-conversion`, which presents as:
```
warning: implicit conversion turns string literal into bool: A to B
```
This is often a bug and what was intended. The most frequent cause is the code was:
```
void foo(bool) { ... }
void foo(std::string) { ... }
foo("this gets interpreted as a bool");
```

It is also possible the issue is innocuous as part of an assert:
```
assert(!"this string is true, so the assertion is false");
EXPECT_FALSE("this string is true, so the expect fails");
```
in these cases the use is to "cute", so we modify the code to make it more obvious.
```
assert(false && "the compiler recognizes and doesn't complain about this pattern");
FAIL() << "much more obvious";
```

Reviewed By: dmm-fb

Differential Revision: D92528316

fbshipit-source-id: 93fbb624e8731c4cdb559746b44c1aa71d786304
2026-02-25 10:30:50 -08:00
Andrew Chang f4fe40845a Update HISTORY and version to 10.11.2 2026-02-25 10:30:29 -08:00
Andrew Chang 40a5f27f6e Check io_uring probe result in SupportedOps (#14355)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14355

SupportedOps advertised kAsyncIO based only on the IsIOUringEnabled() weak symbol check, without verifying that the constructor's io_uring probe actually succeeded. Add a thread_local_async_read_io_urings_ null check so kAsyncIO is only reported when the probe passed. Also update the constructor to probe with the same IORING_SETUP_SINGLE_ISSUER | IORING_SETUP_DEFER_TASKRUN flags that ReadAsync and MultiRead use at runtime.

Reviewed By: anand1976

Differential Revision: D93780065

fbshipit-source-id: 6f51f544b267cb39d09b49949a9485f55eeae12e
2026-02-24 16:36:14 -08:00
Andrew Chang 9676ce2f84 Propagate file_checksum through FileOptions on NewRandomAccessFile (#14321)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14321

Add file_checksum and file_checksum_func_name fields to FileOptions so that downstream FileSystem implementations can access per-file checksum metadata when SST files are opened. The fields are populated from FileMetaData at all call sites where SST files are opened via NewRandomAccessFile: TableCache::GetTableReader, Version::GetTableProperties, and CompactionJob::ReadTablePropertiesDirectly. Also fixes the fallback path in TableCache::GetTableReader to use the local fopts (with temperature and checksum) instead of the original file_options.

Added a kNoFileChecksumFuncName which is distinct from  kUnknownFileChecksumFuncName:

 - kUnknownFileChecksumFuncName ("Unknown"): We have FileMetaData for this file, and the metadata says no checksum was computed (no factory was configured when the file was written). This is a property of the file itself.
- kNoFileChecksumFuncName ("Unavailable"): We don't even have FileMetaData — we're opening this file in a context where there's no checksum metadata to propagate at all (e.g., SstFileDumper, SstFileReader, checksum generation). It's a property of the call site, not the file.

So the assertion file_checksum.empty() is correct for both, but for different reasons — one says "the file has no checksum," the other says "we have no idea about this file's checksum."

Reviewed By: pdillinger

Differential Revision: D92728944

fbshipit-source-id: 8fd34ea22ca87090b26d0a55c921f354f97f1ffc
2026-02-24 16:36:06 -08:00
Peter Dillinger e506aa7154 Fix format compatibility issues, extend test (#14323)
Summary:
See https://github.com/facebook/rocksdb/issues/14240 which brought this to my attention. Here I've added range deletions and compactions to the format compatible test, and fixed or worked-around compatibility issues (likely longstanding).

The first fix was in Version::MaybeInitializeFileMetaData for an assertion failure simply from adding range deletions from some 5.x version.

The second fix is a broader work-around for older SST files with unreliable num_entries/num_range_deletions/num_deletions statistics in their table properties. We depend on them only for some paranoid checks for compaction, so in my assessment the best way to deal with those files is to exclude the paranoid checks when dealing with the files with unrelaible data. (Details in code comments.) The important part is that compacting old files is exceptionally rare, so we aren't really interefering with the paranoid checks doing thier job on an ongoing basis.

This depends on https://github.com/facebook/rocksdb/issues/14315 (just landed) because there is a remaining undiagnosed problem with some very early releases, but I'm not fixing that because its support is being dropped.

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

Test Plan: test extended (ran locally excluding some releases)

Reviewed By: xingbowang

Differential Revision: D93032653

Pulled By: pdillinger

fbshipit-source-id: f90b32f30ba4764692e68d23705f42c778e0dc1d
2026-02-16 11:13:06 -08:00
Anand Ananthabhotla 6d4c2c3297 Fix racy assertion in AbortIOPartialHandlesBug test (#14319)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14319

The test asserted h1->is_finished and h2->is_finished immediately after AbortIO({H0}), before calling Poll. This is invalid because AbortIO only guarantees that handles in its abort set are finalized. Non-aborted handles' CQEs may or may not be consumed during AbortIO depending on io_uring completion ordering. If H0's two CQEs (original read + cancel) arrive before H1/H2's CQEs, AbortIO breaks out of its wait loop without processing them. Move the H1/H2 is_finished assertions to after Poll, which correctly handles either case. Also remove the racy req_count checks for non-aborted handles since Poll does not increment req_count.

Reviewed By: jaykorean

Differential Revision: D92848827

fbshipit-source-id: 0c09b44ceada99877e8311cff799fa94f1056545
2026-02-16 11:13:06 -08:00
Anand Ananthabhotla 2ceb3ef4d2 Add a cleanup target to crash_test.mk (#14286)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14286

Add the db_c leanup target which can be used by CI test scripts to delete the db on failure. The db_crashtest.py doesn't automatically delete on error.

Reviewed By: jaykorean

Differential Revision: D91912877

fbshipit-source-id: d36ec0896fba64faaafe055d8673e437e85d0c3a
2026-02-06 12:20:35 -08:00
anand76 edc76202a2 Update HISTORY and version to 10.11.1 2026-02-06 12:20:35 -08:00
Andrew Chang b44c70adcf Fix crash in GetLiveFilesStorageInfo on read-only DB (#14306)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14306

GetLiveFilesStorageInfo crashes when called on a read-only RocksDB because
it calls FlushWAL(), which accesses logs_.back() on an empty deque.

Root cause: DBImplReadOnly overrides SyncWAL() to return NotSupported, but
does NOT override FlushWAL(). Read-only DBs have an empty logs_ deque
because they don't create WAL writers during recovery - there's nothing to
write, so no WAL infrastructure is initialized.

The reason SyncWAL was originally marked NotSupported is that these WAL
operations (SyncWAL syncs buffer to disk, FlushWAL flushes to OS buffer)
require an active WAL writer at logs_.back().writer. Since read-only DBs:
1. Cannot perform writes
2. Don't create WAL files for writing
3. Have an empty logs_ deque

...there's no WAL writer to sync or flush. The operations are semantically
meaningless, not just "forbidden write operations."

The fix adds a FlushWAL override matching the SyncWAL pattern. The caller
in db_filesnapshot.cc:403-405 already handles IsNotSupported() gracefully:
  if (s.IsNotSupported()) { s = Status::OK(); }

Reviewed By: pdillinger

Differential Revision: D92419557

fbshipit-source-id: 7079071209b3c7be41a2c98c9b691e68bc031595
2026-02-06 12:20:35 -08:00
Andrew Chang 71eb5a1646 Propagate Poll errors in FilePrefetchBuffer::PollIfNeeded (#14282)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14282

Previously, `PollIfNeeded` returned `void` and silently ignored errors from
`fs_->Poll()` by calling `PermitUncheckedError()`. This could lead to silent
data corruption or unexpected behavior when Poll operations fail.

This diff changes `PollIfNeeded` to return `Status` and properly propagate
Poll errors to callers. When Poll fails:
1. The IO handle is cleaned up via `DestroyAndClearIOHandle`
2. The error status is returned to the caller
3. Callers (`HandleOverlappingAsyncData` and `PrefetchInternal`) now check
   and propagate this error

Also adds a `TEST_SYNC_POINT_CALLBACK` to allow tests to inject Poll errors.

Reviewed By: anand1976

Differential Revision: D91624185

fbshipit-source-id: 8dd0ee6588ed1ce4bf080bcf857b778c5140ccf5
2026-02-06 12:20:35 -08:00
Anand Ananthabhotla 63a256d768 Fix AbortIO consuming completions for non-aborted handles (#14301)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14301

When AbortIO was called with a subset of outstanding async read handles,
it would consume io_uring completions for handles NOT in the abort set
but fail to finalize them. This caused subsequent Poll calls on those
handles to hang forever waiting for completions that had already been
consumed.

The fix adds an `is_being_aborted` flag to Posix_IOHandle that is set
when submitting the cancel request. When processing completions in
AbortIO, handles with this flag wait for req_count==2 (original + cancel),
while handles without the flag are finalized immediately at req_count==1.

Also refactored the completion finalization logic into a shared
FinalizeAsyncRead() helper function used by both Poll and AbortIO.

Reviewed By: mszeszko-meta, archang19

Differential Revision: D92230883

fbshipit-source-id: e6d11e009a4930e5608459771990f6cf7d46d827
2026-02-06 11:47:45 -08:00
mszeszko-meta 87d4b0d579 Merge pull request #14289 from facebook/cherry_pick_14280_to_10_11
Cherry picks into 10.11.fb
2026-02-02 10:47:06 -08:00
Adam Retter 09bc20b16e Fixes the Windows VS 2022 build (#14280)
Summary:
When building a Release on Windows RTTI is not available, so asserts that use dynamic_cast need to be disabled

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

Reviewed By: nmk70

Differential Revision: D91807791

Pulled By: mszeszko-meta

fbshipit-source-id: e29c19c757bcd076a1f09ed40b306bb50ba9e882
2026-01-30 16:36:19 -08:00
Peter Dillinger dd9a806edd Update HISTORY for 10.11.0 2026-01-26 12:42:18 -08:00
Peter Dillinger ad218cacf6 Temporary disable multiscan_use_async_io in crash test (#14263)
Summary:
Seeing many errors like this

```
Iterator diverged from control iterator which has value ...
iterator is not valid with status: IO error: Req failed: Unknown error -14
VerifyIterator failed. Control CF default
```

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

Test Plan: CI

Reviewed By: archang19

Differential Revision: D91478886

Pulled By: pdillinger

fbshipit-source-id: 94b955b6ecdb7a3cab39dac8e7b0d1047d49a0bb
2026-01-26 09:06:33 -08:00
Peter Dillinger 6a79e02ebd Support pre-defined compression dictionaries (#14253)
Summary:
... in addition to those derived from samples. This could be useful when trade-offs favor an offline trained dictionary that's good for the whole work load, which can involve heavy-weight training, vs. on-the-fly training on samples for each file, which has limitations.

This involves some breaking changes to some deeper parts of the new compression API. I'm not concerned about performance because this doesn't touch the per-block parts of the API, just the per-file parts.

Bonus: change to
CompressionManagerWrapper::FindCompatibleCompressionManager to implement what is likely the preferred behavior.

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

Test Plan: unit test included

Reviewed By: hx235

Differential Revision: D91082208

Pulled By: pdillinger

fbshipit-source-id: 1442db65e15c9435437204c19787c96f7a40a207
2026-01-23 10:01:50 -08:00
Peter Dillinger a9906f0dd0 A better approach to clearing DBs for crash test (#14254)
Summary:
Clearing DB dir for crash test is currently a hodgepodge of
1. Caller of db_crashtest.py maybe tries to clear the dir
2. db_crashtest.py tries to clear the dir in get_dbname() (but ignoring failure)
3. db_crashtest.py passes --destroy_db_initially to some db_stress calls as needed
4. db_crashtest.py tries to clear the dir between some db_stress calls
5. db_crashtest.py tries to clear the dir after everything is done and successful (no artifacts to investigate or save) (but ignoring failure)
6. Try to add more uniqueness to the directory from https://github.com/facebook/rocksdb/issues/14249

This change reverts or replaces 2, 4, 5, and 6 by doubling-down on (expanding) 3 and a small variant of it:

* crash_test.mk passes --destroy_db_initially=1 so that the first run of db_stress clears the db dir.
* After each db_stress invocation, db_crashtest.py resets destroy_db_initially=0 so that the next invocation reuses the same DB, except in cases where there is an incompatibility that requires a fresh DB (from cases 3 and 4 above).
* On success, uses new `db_stress --destroy_db_and_exit` option to clean up the DB dir without needing a custom cleanup_cmd (now ignored)

Note that although case 1 is likely obsolete, it is out of control of an open source PR.

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

Test Plan: some manual runs

Reviewed By: xingbowang

Differential Revision: D91164731

Pulled By: pdillinger

fbshipit-source-id: 0a66c8c0e130c9eeacc55af411a18a09bc9debdf
2026-01-22 11:48:06 -08:00
Anand Ananthabhotla b89d290c20 Add MultiScan statistics (#14248)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14248

### Overview

This diff introduces the addition of multi-scan statistics to RocksDB, enhancing the database's ability to monitor and analyze performance during multi-scan operations.

### Key Changes

#### Implemented Multi-Scan Statistics

The following statistics were implemented to provide deeper insights into multi-scan operations:

- **MULTISCAN_PREPARE_MICROS**: Measures the time (in microseconds) spent preparing for multi-scan operations.
- **MULTISCAN_BLOCKS_PER_PREPARE**: Tracks the number of blocks processed per multi-scan prepare operation.
- **Wasted Prefetch Blocks Count**: Counts the number of prefetched blocks that were not used (i.e., wasted) if the iterator is abandoned before accessing them.
- **MULTISCAN_TOTAL_BLOCKS_SCANNED**: Tracks the total number of blocks scanned during all multi-scan operations.
- **MULTISCAN_TOTAL_KEYS_SCANNED**: Measures the total number of keys scanned across all multi-scan operations.
- **MULTISCAN_TOTAL_MICROS**: Captures the total time (in microseconds) spent in multi-scan operations.
- **MULTISCAN_PREFETCHED_BLOCKS**: Counts the number of blocks that were prefetched during multi-scan operations.
- **MULTISCAN_USED_PREFETCH_BLOCKS**: Tracks the number of prefetched blocks that were actually used during multi-scan operations.

### Impact

This diff provides more fine-grained statistics for multi-scan operations, allowing developers and users to better understand and optimize the performance of their RocksDB instances.

Reviewed By: krhancoc

Differential Revision: D91053297

fbshipit-source-id: 7158741b9f026c0b5ce8ba1264dbd137e7fe985d
2026-01-21 23:23:38 -08:00
Andrew Chang f84351de98 Fix AbortIO hang when aborting multiple io_uring handles (#14252)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14252

Fixed a bug in PosixFileSystem::AbortIO that could cause an infinite hang
when aborting multiple concurrent async IO handles.

The bug occurred in the completion processing loop: when an io_uring
completion arrived for a handle other than the one currently being waited
for (io_handles[i]), the code would increment that handle's req_count but
only mark it as finished if it also matched io_handles[i]. This meant
completions for other handles were consumed but those handles were never
marked as finished.

Later, when iterating to those handles, the code would enter
io_uring_wait_cqe expecting more completions, but they had already been
consumed - causing an infinite hang.

The fix aligns AbortIO's completion handling with what Poll() already does:
mark handles as finished whenever their completions arrive, regardless of
which handle we're currently waiting for in the outer loop. Only the break
statement remains conditional on matching io_handles[i].

Reviewed By: anand1976

Differential Revision: D91070044

fbshipit-source-id: 47faf5f0df3e26a2aa83444bbac623f43f560933
2026-01-21 19:17:05 -08:00
Andrew Chang f312633aff Fix AbortIO documentation to match actual behavior (#14251)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14251

The AbortIO API documentation incorrectly stated that the callback
should NOT be called for aborted io_handles. However, the actual
implementation in fs_posix.cc does invoke the callback with
IOStatus::Aborted() status after cancelling requests:

```
// fs_posix.cc:1252-1260
if (posix_handle->req_count == 2 &&
    static_cast<Posix_IOHandle*>(io_handles[i]) == posix_handle) {
  posix_handle->is_finished = true;
  FSReadRequest req;
  req.status = IOStatus::Aborted();
  posix_handle->cb(req, posix_handle->cb_arg);

  break;
}
```

This change corrects the documentation to match the actual behavior
in RocksDB.

Reviewed By: anand1976

Differential Revision: D91073466

fbshipit-source-id: 47ae14a09e9386cc68049ca272d6b712f5a9bed7
2026-01-21 16:26:06 -08:00
Peter Dillinger a6af317476 Use format_version=7 by default, fix perf bug (#14239)
Summary:
Since it's been > 6 months and we have production uses, migrate to fv=7 by default. One unit test needed an update for the change to table properties with fv=7.

On making this change, PresetCompressionDictTest tests detected extra memory usage by decompressing LZ4 with dictionary compression. This turned out to be a bug in `std::find` usage that led to using the ZSTD-optimized decompressor (with digested dictionary usage) in cases where it is not needed. I've fixed the bug and improved the unit tests that found the bug.

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

Test Plan: existing tests, including format compatible CI job (updated, and run locally with SHORT_TEST=1)

Reviewed By: hx235

Differential Revision: D90728697

Pulled By: pdillinger

fbshipit-source-id: 8f1a0e9ca59a88c18eaa4cdfdea00309175ce30a
2026-01-21 09:28:06 -08:00
Xingbo Wang eb5e1a2d1f Use unique DB directory when TEST_TMPDIR is set (#14249)
Summary:
Some of the stress tests script run tests multiple times with TEST_TMPDIR set. When TEST_TMPDIR is set, the db directory is a fixed string. This caused the same DB directory was reused across db_crashtest.py script run. Typically, the DB folder is cleaned up after db_crashtest.py complete. But sometimes, the clean up command could fail. This caused the DB folder to be reused across different db_crashtest.py runs. Meantime, each db_crashtest.py run would randomize some of the parameters. This caused different parameters to be used with same DB directory, violating some of the assumption such as use_put_entity_one_in parameter to be not changed between runs. This change added a suffix to DB directory, so that each db_crashtest.py script run would generate a unique DB directory, which prevents the clean up failure issue causing test flaky.

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

Test Plan:
Stress test local run

```
TEST_TMPDIR=/tmp/aaa /usr/local/bin/python3 -u tools/db_crashtest.py --stress_cmd=./db_stress --cleanup_cmd='' --simple blackbox  --duration 15 --interval 10

>>> Running db_stress with pid=113810: ./db_stress ... --db=/tmp/aaa/rocksdb_crashtest_blackbox_6967584463401575611 ...
```

Reviewed By: hx235

Differential Revision: D91069655

Pulled By: xingbowang

fbshipit-source-id: 327fc3cd0d8e3ef4b49e182e21bcd91a10647710
2026-01-20 17:31:28 -08:00
Xingbo Wang ea5e649225 Fix an infinite compaction loop bug with udt (#14228)
Summary:
Problem

The TEST_WaitForCompact in
TimestampCompatibleCompactionTest.UdtTombstoneCollapsingTest would sometimes
run forever, indicating an infinite compaction loop.
Issue https://github.com/facebook/rocksdb/issues/14223

Root Cause

In ComputeBottommostFilesMarkedForCompaction(), files were marked for
bottommost compaction based only on the condition largest_seqno <
oldest_snapshot_seqnum. However, for User-Defined Timestamps (UDT) columns,
compaction can only zero sequence numbers when the file's maximum timestamp is
below full_history_ts_low.

When timestamps were above this threshold:
1. File gets marked for compaction (seqno condition met)
2. Compaction runs but cannot zero seqno (timestamp condition not met)
3. Output file immediately gets re-marked for compaction
4. Infinite loop

Solution

Added timestamp range tracking to FileMetaData and updated the marking logic to
check timestamps before marking files.

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

Test Plan: Unit test

Reviewed By: pdillinger

Differential Revision: D90586045

Pulled By: xingbowang

fbshipit-source-id: addfa4f988db8c87fb513a1bf58ee54623a6c210
2026-01-20 14:10:41 -08:00
anand76 2a7a6a6d72 Don't assert on async_read.status in MultiScan code path (#14244)
Summary:
Surface async read errors instead of asserting on them. This makes it easier to debug stress test failures. Async reads can fail for legitimate reasons, such as fs errors.

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

Reviewed By: hx235

Differential Revision: D90878515

Pulled By: anand1976

fbshipit-source-id: 6335d4b06ddf250b26842ce94e3f5263356b2695
2026-01-20 12:18:19 -08:00
Peter Dillinger 88aff40c97 New io stats for unknown file temperature last vs. non-last (#14243)
Summary:
These will be useful for qualifying non-tiered workloads for tiered storage.

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

Test Plan:
unit test included

I'm not concerned about performance because this fits pretty nicely into some existing code and only adds overhead when (expensive) IOs are done.

Reviewed By: jaykorean

Differential Revision: D90870348

Pulled By: pdillinger

fbshipit-source-id: 984411123bcd54c249a949da813ff04fedacc6a4
2026-01-16 11:01:37 -08:00
Peter Dillinger c6d08d3efe Use new compression APIs in db_bench (#14241)
Summary:
To move away from OLD_CompressData / OLD_UncompressData. Also improved some error/warning messages.

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

Test Plan: manual tests showing similar performance, runs with ASAN/UBSAN to check for issues

Reviewed By: hx235

Differential Revision: D90793708

Pulled By: pdillinger

fbshipit-source-id: e0655f7bed8d85e5ea110167dca73c6664f7465b
2026-01-16 10:01:42 -08:00
Peter Dillinger a1af6f9f64 Use new compression APIs internally for sample_for_compression (#14230)
Summary:
Trying to get rid of uses of OLD_CompressData / OLD_UncompressData. Some performance optimizations and corrections for better accounting also.

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

Test Plan:
* exanded unit test to be more complete / rigorous
* manual before-and-after db_bench runs with the option, seeing table properties as expected

Reviewed By: hx235

Differential Revision: D90545476

Pulled By: pdillinger

fbshipit-source-id: 2f7c577574bcc4b2acafa002761ec1cad7fdb093
2026-01-14 09:35:54 -08:00
Peter Dillinger 57036b68d9 Migrate blob handling to new compression APIs (#14234)
Summary:
as part of the effort to get rid of OLD_CompressData and OLD_UncompressData and the old implementations in compression.h.

It's unfortunate the the existing blob file schema doesn't allow storing blobs uncompressed when the compressed version is larger, so we have to work around that.

Note that use of GrowableBuffer in place of std::string is intended to avoid the potential performance overhead of zeroing out memory before overwriting it.

Also includes some cleanup of includes

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

Test Plan:
some unit test updates as needed. Crash test covers integrated blob support.

I'm not too concerned about performance, as until a future schema change, this code is committing the grave performance error of storing compressed data larger than uncompressed.

Reviewed By: mszeszko-meta, hx235

Differential Revision: D90544049

Pulled By: pdillinger

fbshipit-source-id: 2f2ed16de63990b797cc06c8dad36b5869dac302
2026-01-14 09:35:16 -08:00
Xingbo Wang 256838180e Fix stress test deadlock failure in TestPut (#14235)
Summary:
Deadlock or timeout is possible in TestPut, when TestMultiGet was executed at the same time, because it executes MaybeAddKeyToTxnForRYW, which writes to the same key space but does not acquire stress test level mutex. Therefore, RocksDB could return deadlock error.

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

Test Plan: Stress test

Reviewed By: hx235

Differential Revision: D90621772

Pulled By: xingbowang

fbshipit-source-id: eb808193ded06b69a8161320f88d5ba4e20b4901
2026-01-14 06:44:01 -08:00
Xingbo Wang 2893c25ca2 Support printing block checksum in sst_dump (#14222)
Summary:
Support printing block checksum in sst_dump

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

Test Plan:
manual test, sample output

```
Data Block # 1 @ 00B021
   Data block checksum type: 4  checksum value: 0x40614fb3  offset: 0  size: 4272  compression type: 0
```

Reviewed By: jaykorean

Differential Revision: D90286789

Pulled By: xingbowang

fbshipit-source-id: 71324e04549bea070d80b45a81b562ad331a7840
2026-01-10 05:05:18 -08:00
Xingbo Wang a6325e9564 Add block type to corruption error message (#14225)
Summary:
Add block type to corruption error message

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

Test Plan: Unit test

Reviewed By: jaykorean

Differential Revision: D90329899

Pulled By: xingbowang

fbshipit-source-id: 6fa925d1704c7c19c98d6067628b73a7c0904c3e
2026-01-09 14:37:46 -08:00
Xingbo Wang 4bcec5ae89 Fix autoconf download failure in folly build (#14226)
Summary:
Folly download dependencies directly from external source. Sometimes, this could fail due to external website instability. To solve this, we added github cache to cache the dependencies. We also added a python script to try different sources during download to reduce the chance of failure.

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

Test Plan: github CI

Reviewed By: krhancoc, archang19

Differential Revision: D90343051

Pulled By: xingbowang

fbshipit-source-id: 3faad6aaa6c1bfd361b9e405c298856cd64bf457
2026-01-09 10:43:54 -08:00
Josh Kang 4bf2bcdbb3 Allow setting options for multiple column families (#14201)
Summary:
Currently to set options for multiple CFs, the caller must repeatedly call SetOptions() for each CF. This in turn serializes the entire options file each time. This PR exposes a new API that allows SetOptions to be called on multiple CFs at once, thus only paying the OPTIONS file serialization once.

Also added a new unit test for SetOptions.

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

Reviewed By: pdillinger

Differential Revision: D89735181

Pulled By: joshkang97

fbshipit-source-id: 9b7a721b7e8769b653243b1581678ffd05d038e8
2026-01-08 15:15:31 -08:00
Ryan Hancock 2b28885c80 Introducing IO Dispatcher (#14135)
Summary:
This diff introduces the IO Dispatcher, which will be used to simplify the code path for MultiScan, while further providing a centralized place to enact policy on how MultiScan is done (i.e., limit memory usage and pinned buffers for example). Right now this diff only encapsulates the functionality done during the Prepare of MultiScan.

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

Reviewed By: anand1976

Differential Revision: D87837261

Pulled By: krhancoc

fbshipit-source-id: 2698910ade02bc3d182413ae07ce69fe7abb7ec5
2026-01-07 10:34:21 -08:00
zaidoon 429b36c22d Add C API for block_align option in BlockBasedTableOptions (#14153)
Summary: Pull Request resolved: https://github.com/facebook/rocksdb/pull/14153

Reviewed By: archang19

Differential Revision: D90211012

Pulled By: jaykorean

fbshipit-source-id: fd87d3d74664f75fbe47946764b1d25aa731c020
2026-01-06 19:03:18 -08:00
Xingbo Wang 6f03c3dfea Fix a flaky unit test UdtTombstoneCollapsingTest (#14220)
Summary:
As compaction scheduling is not deterministic, the existing check is too strict sometimes, causing test to be flaky.

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

Test Plan: Unit test

Reviewed By: pdillinger

Differential Revision: D90143556

Pulled By: xingbowang

fbshipit-source-id: 6780423c63324a4b20fc8b8ccac2051a094c9f4a
2026-01-06 14:29:02 -08:00
Xingbo Wang a7c1acbe9f Mark RateLimiter::GetMode method as const (#14221)
Summary:
Mark RateLimiter::GetMode method as const

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

Test Plan: existing unit test

Reviewed By: jaykorean

Differential Revision: D90182630

Pulled By: xingbowang

fbshipit-source-id: 119f3cf0082e285a84ecdca224535f03f2afbf12
2026-01-06 10:30:34 -08:00
Peter Dillinger 4e10e0bcac Fix cases of db dir going missing in db_crashtest.py (#14219)
Summary:
My PR https://github.com/facebook/rocksdb/issues/14195 regressed a case in which db_crashtest.py calling db_stress with --destroy_db_initially=1 could lead to dbname directory being nonexistant for subsequent calls to gen_cmd -> finalize_and_sanitize -> is_direct_io_supported which would fail in creating a temporary file. Fix this (and clean up existing related code) using os.makedirs.

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

Test Plan: I don't have a good reproducer for the error but some manual testing indicates this change is at least safe

Reviewed By: virajthakur

Differential Revision: D90138248

Pulled By: pdillinger

fbshipit-source-id: 0ed6524cd50f8632346a8583f26bf1f4941817ce
2026-01-06 10:09:07 -08:00
Peter Dillinger 387cb4aae7 Clarify/rename atomic wrapper stuff + blog post (#14213)
Summary:
* Some existing commentary and motivation around my atomic wrappers in atomic.h was based on a misreading of documentation. seq_cst *is* a safe substitute for acq_rel in all cases. I still like having a distinct type for RelaxedAtomic (as folly does) and a wrapper also for other cases to avoid readability traps like implicit conversion and implicit memory order. This PR is only comment changes and renaming.
* Create a blog post about bit fields API to help with lock-free (and low-lock) programming.

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

Test Plan: esiting tests

Reviewed By: xingbowang

Differential Revision: D89971581

Pulled By: pdillinger

fbshipit-source-id: 9bd1181d692258d668189c2da8bd0e5d98fd6230
2026-01-05 20:47:46 -08:00
Pierre Moulon 8b6f98cdb4 Fixing typos in comments and documentation (#14205)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14205

Fixed various spelling errors throughout RocksDB codebase including:
- assiciated → associated
- disucssion → discussion
- satisifed → satisfied
- supoort → support
- capacit_limit → capacity_limit
- direclty → directly
- diable → disable
- opeartions → operations
- paylaod → payload
- happenning/happended → happening/happened
- intialized/initiallized → initialized
- asynchronosuly → asynchronously
- exisiting → existing
- persitence → persistence
- and several others

These changes are in comments, test code, and documentation only.

Reviewed By: pdillinger

Differential Revision: D89800154

fbshipit-source-id: 1681ec95a687b038c2bad48856f1abb4dbeb42cf
2026-01-02 17:25:57 -08:00
Peter Dillinger 1cc1df8dab Finish migrating HCC to BitFields API (#14154)
Summary:
This change builds on https://github.com/facebook/rocksdb/issues/14027 and https://github.com/facebook/rocksdb/issues/13965 to complete migration
of the HyperClockCache implementation to using the hygienic BitFields API.
No semantic change in the implementation details is intended, just
greatly improving readability and safety of the code while maintaining
the same performance.

In more detail,
* Refactor the main metadata atomic for each slot in an HCC table into
SlotMeta using BitFields.
* Extended BitFields APIs with some additional features, and renamed
  BlahTransform classes to BlahTransformer to resolve potential naming
  conflicts with member functions to create them.

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

Test Plan:
for correctness, mostly existing tests. but also added tests
for new BitFields features. I especially ran local TSAN whitebox crash
test extensively which caught a couple of refactoring errors.

For performance, I verified with release builds of cache_bench, using
default options, that there was no noticeable/consistent difference
after all these HCC migrations vs. backing them out. That test was with
GCC 11 and -O2, which is a reasonable baseline for expected compiler
optimizations.

Reviewed By: xingbowang

Differential Revision: D87960540

Pulled By: pdillinger

fbshipit-source-id: e0257b7fea8a5c7709daef18911959201ce4e0f3
2025-12-29 17:13:50 -08:00
Xingbo Wang 3818cc1aca Fix a bug in seqno zeroing logic with UDT (#14207)
Summary:
This bug caused seqno to be incorrectly zeroed when UDT is enabled. This is one of the contributing factor that caused tombstones to be accumulated at bottommost level, causing high space amp.

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

Test Plan: Unit test

Reviewed By: pdillinger

Differential Revision: D89826564

Pulled By: xingbowang

fbshipit-source-id: 62ab1e37c36ae1ed95f26213c97a591a17e962a6
2025-12-29 10:53:58 -08:00
zaidoon 276721cd10 eliminate per-iterator heap allocation by constructing InternalKeyComparator in-place (#14044)
Summary:
resolve [13951](https://github.com/facebook/rocksdb/issues/13951)

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

Reviewed By: xingbowang

Differential Revision: D86217603

Pulled By: jaykorean

fbshipit-source-id: 8ed62503cfcfdfb26f7af7b0a5641cd47dd9e54c
2025-12-29 10:32:36 -08:00
nsaji-stripe 45690d0f6a CompactionServiceOptionsOverride setters C API (#14183)
Summary:
## Context
1. OpenAndCompact required CompactionServiceOptionsOverride
2. Currently there are no C APIs to create CompactionServiceOptionsOverride

## Changes
1. Create C API for compactionServiceOptionsOverride
2. Create helper function to create compactionServiceOptionsOverride from Options.  This was added in because The C API lacks getter methods for non-serializable options (comparator, table_factory, etc.). Without this, users would need to maintain separate references to all these options just to pass them to the override. If the user need to create a new comparator or table factory then C API for compactionServiceOptionsOverride already as the setters for the same.

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

Reviewed By: hx235

Differential Revision: D89690005

Pulled By: jaykorean

fbshipit-source-id: efe8211feec9d144b32be0f5e66c8cf8bde8dac0
2025-12-29 10:15:38 -08:00
Maciej Szeszko e77ba4bc95 Start 10.11.0 development (#14192)
Summary:
10.10.0 branch has been cut.

### Updated:

HISTORY.md
include/rocksdb/version.h
tools/check_format_compatible.sh

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

Reviewed By: virajthakur

Differential Revision: D89736225

Pulled By: pdillinger

fbshipit-source-id: d7fc592b33a5c60dc2b53aa72ceafaa507730f20
2025-12-23 12:18:44 -08:00
Peter Dillinger 7e9f54d56b Remove remaining pieces of Lua integration (#14200)
Summary:
Going from deprecated and partly removed to fully removed

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

Test Plan: existing tests

Reviewed By: archang19

Differential Revision: D89697543

Pulled By: pdillinger

fbshipit-source-id: bbf6161c04322e9756a9479758488cac6d03473a
2025-12-23 09:10:12 -08:00
Peter Dillinger 41beb1422f Improve db_crashtest.py for remote DB (#14195)
Summary:
Let db_crashtest.py work with TEST_TMPDIR on remote filesystem, by infering whether it's remote from the env_uri argument. Note that some other paths passed to db_stress are local paths and we can't reuse TEST_TMPDIR for those cases when it's remote.

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

Test Plan: public and private CI

Reviewed By: archang19

Differential Revision: D89590246

Pulled By: pdillinger

fbshipit-source-id: db6eb9c16d4e76617183780747353c798cc9bef6
2025-12-22 13:03:00 -08:00
165 changed files with 7224 additions and 1483 deletions

No files matched your search

@@ -0,0 +1,21 @@
name: cache-getdeps-downloads
description: Cache getdeps downloads to avoid unreliable mirrors and speed up builds
outputs:
cache-hit:
description: Whether the cache was hit
value: ${{ steps.cache-downloads.outputs.cache-hit }}
runs:
using: composite
steps:
- name: Cache getdeps downloads
id: cache-downloads
uses: actions/cache@v4
with:
# Use a fixed path that we control - folly.mk will sync with getdeps downloads dir
path: /tmp/rocksdb-getdeps-cache
# Use a rolling cache key - the cache accumulates downloads over time
# The key includes a weekly timestamp to ensure periodic refresh
key: getdeps-downloads-${{ runner.os }}-${{ runner.arch }}-week-${{ github.run_id }}
restore-keys: |
getdeps-downloads-${{ runner.os }}-${{ runner.arch }}-week-
getdeps-downloads-${{ runner.os }}-${{ runner.arch }}-
+4
View File
@@ -54,6 +54,7 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/cache-getdeps-downloads"
- uses: "./.github/actions/setup-folly"
- uses: "./.github/actions/build-folly"
- run: LIB_MODE=static USE_CLANG=1 USE_FOLLY=1 COMPILE_WITH_UBSAN=1 COMPILE_WITH_ASAN=1 make -j32 check
@@ -68,6 +69,7 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/cache-getdeps-downloads"
- uses: "./.github/actions/setup-folly"
- uses: "./.github/actions/cache-folly"
id: cache-folly
@@ -86,6 +88,7 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/cache-getdeps-downloads"
- uses: "./.github/actions/setup-folly"
- run: "DEBUG_LEVEL=0 make -j20 build_folly"
- run: "USE_FOLLY=1 LIB_MODE=static DEBUG_LEVEL=0 V=1 make -j20 release"
@@ -162,6 +165,7 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/cache-getdeps-downloads"
- uses: "./.github/actions/setup-folly"
- run: "(mkdir build && cd build && cmake -DUSE_FOLLY_LITE=1 -DWITH_GFLAGS=1 -DCMAKE_CXX_FLAGS=-DGLOG_USE_GLOG_EXPORT .. && make VERBOSE=1 -j20 && ctest -j20)"
- uses: "./.github/actions/post-steps"
+3
View File
@@ -102,6 +102,7 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/cache-getdeps-downloads"
- uses: "./.github/actions/setup-folly"
- uses: "./.github/actions/cache-folly"
id: cache-folly
@@ -120,6 +121,7 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/cache-getdeps-downloads"
- uses: "./.github/actions/setup-folly"
- run: USE_FOLLY_LITE=1 EXTRA_CXXFLAGS=-DGLOG_USE_GLOG_EXPORT V=1 make -j32 all
- uses: "./.github/actions/post-steps"
@@ -133,6 +135,7 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/cache-getdeps-downloads"
- uses: "./.github/actions/setup-folly"
- uses: "./.github/actions/cache-folly"
id: cache-folly
+7
View File
@@ -268,6 +268,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"util/dynamic_bloom.cc",
"util/file_checksum_helper.cc",
"util/hash.cc",
"util/io_dispatcher_imp.cc",
"util/murmurhash.cc",
"util/random.cc",
"util/rate_limiter.cc",
@@ -5214,6 +5215,12 @@ cpp_unittest_wrapper(name="interval_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="io_dispatcher_test",
srcs=["util/io_dispatcher_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="io_posix_test",
srcs=["env/io_posix_test.cc"],
deps=[":rocksdb_test_lib"],
+4
View File
@@ -219,6 +219,10 @@ if(MSVC)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zi /nologo /EHsc /GS /Gd /GR /GF /fp:precise /Zc:wchar_t /Zc:forScope /errorReport:queue")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /FC /d2Zi+ /W4 /wd4127 /wd4996 /wd4100 /wd4324")
endif()
if(CMAKE_BUILD_TYPE STREQUAL "Release")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /DNDEBUG")
message(STATUS "Setting /DNDEBUG as CMAKE_BUILD_TYPE is set to ${CMAKE_BUILD_TYPE}")
endif()
else()
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -W -Wextra -Wall -pthread")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wsign-compare -Wshadow -Wno-unused-parameter -Wno-unused-variable -Woverloaded-virtual -Wnon-virtual-dtor -Wno-missing-field-initializers -Wno-strict-aliasing -Wno-invalid-offsetof")
+27
View File
@@ -1,6 +1,33 @@
# Rocksdb Change Log
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
## 10.11.2 (02/25/2026)
### Bug Fixes
* Fix longstanding failures that can arise from reading and/or compacting old DB dirs with range deletions (likely from version < 5.19.0) in many newer versions.
## 10.11.1 (02/06/2026)
### Bug Fixes
* Fixed a bug in PosixFS AbortIO that could cause it to ignore an IO completion and a subsequent Poll to hang
## 10.11.0 (01/23/2026)
### Public API Changes
* New SetOptions API that allows setting options for multiple CFs, avoiding the need to reserialize OPTIONS file for each CF
* Remove remaining pieces of Lua integration
### Behavior Changes
* The new default for `BlockBasedTableOptions::format_version` is 7, which has been supported since RocksDB 10.4.0 and is required in order to use CompressionManagers supporting custom compression types.
### Bug Fixes
* Fixed a small performance bug with `format_version=7` when decompressing formats other than Snappy and ZSTD.
* Fixed an infinite compaction loop bug with User-Defined Timestamps (UDT) where bottommost files were repeatedly marked for compaction even though their timestamp could not be collapsed.
* Bugfix for persisted UDT record sequence number zeroing logic.
## 10.10.0 (12/16/2025)
### Bug Fixes
* Fixed a bug in best-efforts recovery that causes use-after-free crashes when accessing SST files that were cached during the recovery.
* Fix resumable compaction incorrectly allowing resumption from a truncated range deletion that is not well handled currently.
* Fixed a bug in `PosixRandomFileAccess` IO uring submission queue ownership & management. Fix eliminates the false positive 'Bad cqe data' IO errors in `PosixRandomFileAccess::MultiRead` when interleaved with `PosixRandomFileAccess::ReadAsync` on the same thread.
## 10.9.0 (11/21/2025)
### New Features
* Added an auto-tuning feature for DB manifest file size that also (by default) improves the safety of existing configurations in case `max_manifest_file_size` is repeatedly exceeded. The new recommendation is to set `max_manifest_file_size` to something small like 1MB and tune `max_manifest_space_amp_pct` as needed to balance write amp and space amp in the manifest. Refer to comments on those options in `DBOptions` for details. Both options are (now) mutable.
+5 -30
View File
@@ -390,8 +390,6 @@ ifdef COMPILE_WITH_TSAN
# Turn off -pg when enabling TSAN testing, because that induces
# a link failure. TODO: find the root cause
PROFILING_FLAGS =
# LUA is not supported under TSAN
LUA_PATH =
# Limit keys for crash test under TSAN to avoid error:
# "ThreadSanitizer: DenseSlabAllocator overflow. Dying."
CRASH_TEST_EXT_ARGS += --max_key=1000000
@@ -508,32 +506,6 @@ ifndef DISABLE_WARNING_AS_ERROR
endif
ifdef LUA_PATH
ifndef LUA_INCLUDE
LUA_INCLUDE=$(LUA_PATH)/include
endif
LUA_INCLUDE_FILE=$(LUA_INCLUDE)/lualib.h
ifeq ("$(wildcard $(LUA_INCLUDE_FILE))", "")
# LUA_INCLUDE_FILE does not exist
$(error Cannot find lualib.h under $(LUA_INCLUDE). Try to specify both LUA_PATH and LUA_INCLUDE manually)
endif
LUA_FLAGS = -I$(LUA_INCLUDE) -DLUA -DLUA_COMPAT_ALL
CFLAGS += $(LUA_FLAGS)
CXXFLAGS += $(LUA_FLAGS)
ifndef LUA_LIB
LUA_LIB = $(LUA_PATH)/lib/liblua.a
endif
ifeq ("$(wildcard $(LUA_LIB))", "") # LUA_LIB does not exist
$(error $(LUA_LIB) does not exist. Try to specify both LUA_PATH and LUA_LIB manually)
endif
EXEC_LDFLAGS += $(LUA_LIB)
endif
ifeq ($(NO_THREEWAY_CRC32C), 1)
CXXFLAGS += -DNO_THREEWAY_CRC32C
endif
@@ -604,8 +576,8 @@ ifneq ($(filter check-headers, $(MAKECMDGOALS)),)
# TODO: add/support JNI headers
DEV_HEADER_DIRS := $(sort include/ $(dir $(ALL_SOURCES)))
# Some headers like in port/ are platform-specific
DEV_HEADERS_TO_CHECK := $(shell $(FIND) $(DEV_HEADER_DIRS) -type f -name '*.h' | grep -E -v 'port/|plugin/|lua/|range_tree/|secondary_index/')
PUBLIC_HEADERS_TO_CHECK := $(shell $(FIND) include/ -type f -name '*.h' | grep -E -v 'lua/')
DEV_HEADERS_TO_CHECK := $(shell $(FIND) $(DEV_HEADER_DIRS) -type f -name '*.h' | grep -E -v 'port/|plugin/|range_tree/|secondary_index/')
PUBLIC_HEADERS_TO_CHECK := $(shell $(FIND) include/ -type f -name '*.h')
else
DEV_HEADERS_TO_CHECK :=
PUBLIC_HEADERS_TO_CHECK :=
@@ -1950,6 +1922,9 @@ blob_source_test: $(OBJ_DIR)/db/blob/blob_source_test.o $(TEST_LIBRARY) $(LIBRAR
blob_garbage_meter_test: $(OBJ_DIR)/db/blob/blob_garbage_meter_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
io_dispatcher_test: $(OBJ_DIR)/util/io_dispatcher_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
timer_test: $(OBJ_DIR)/util/timer_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
-1
View File
@@ -831,7 +831,6 @@ fi
if test -n "$WITH_JEMALLOC_FLAG"; then
echo "WITH_JEMALLOC_FLAG=$WITH_JEMALLOC_FLAG" >> "$OUTPUT"
fi
echo "LUA_PATH=$LUA_PATH" >> "$OUTPUT"
if test -n "$USE_FOLLY"; then
echo "USE_FOLLY=$USE_FOLLY" >> "$OUTPUT"
fi
+2 -2
View File
@@ -8,7 +8,7 @@ BAD=""
# Look for potential for ODR violations caused by public headers depending on
# build parameters that could vary between RocksDB build and application build.
# * Cases like LUA, ROCKSDB_NAMESPACE, and ROCKSDB_ASSERT_STATUS_CHECKED are
# * Cases like ROCKSDB_NAMESPACE, and ROCKSDB_ASSERT_STATUS_CHECKED are
# intentional, hard to avoid. (We expect definitions to change and the user
# should also.)
# * Cases like _WIN32, OS_WIN, and __cplusplus are essentially ODR-safe.
@@ -19,7 +19,7 @@ BAD=""
# that should not cause ODR violations can be exempted with the ODR-SAFE
# marker recognized here.
grep -nHE '^#if' -- "$@" | grep -vE 'ROCKSDB_NAMESPACE|ROCKSDB_ASSERT_STATUS_CHECKED|LUA|_WIN32|OS_WIN|ODR-SAFE|__cplusplus|ROCKSDB_DLL|ROCKSDB_LIBRARY_EXPORTS'
grep -nHE '^#if' -- "$@" | grep -vE 'ROCKSDB_NAMESPACE|ROCKSDB_ASSERT_STATUS_CHECKED|_WIN32|OS_WIN|ODR-SAFE|__cplusplus|ROCKSDB_DLL|ROCKSDB_LIBRARY_EXPORTS'
if [ "$?" != "1" ]; then
echo "^^^^^ #if in public API could cause an ODR violation."
echo " Add // ODR-SAFE if verified safe."
-1
View File
@@ -19,4 +19,3 @@ BENCHMARK_BASE=/mnt/gvfs/third-party2/benchmark/780c7a0f9cf0967961e69ad08e61cddd
KERNEL_HEADERS_BASE=/mnt/gvfs/third-party2/kernel-headers/624a2f8f6c93c3c1df8aa4a6255d8202631a6c80/fb/platform010/da39a3e
BINUTILS_BASE=/mnt/gvfs/third-party2/binutils/39579e8603b48b3540f8b0633f43adf29acccb8b/2.37/centos8-native/da39a3e
VALGRIND_BASE=/mnt/gvfs/third-party2/valgrind/cd9cc656d49ecb53797ce4d055e49fde29fd57ff/3.19.0/platform010/76ebdda
LUA_BASE=/mnt/gvfs/third-party2/lua/363787fa5cac2a8aa20638909210443278fa138e/5.3.4/platform010/9079c97
+1 -9
View File
@@ -164,12 +164,4 @@ EXEC_LDFLAGS_SHARED="$SNAPPY_LIBS $ZLIB_LIBS $BZIP_LIBS $LZ4_LIBS $ZSTD_LIBS $GF
VALGRIND_VER="$VALGRIND_BASE/bin/"
LUA_PATH="$LUA_BASE"
if test -z $PIC_BUILD; then
LUA_LIB=" $LUA_PATH/lib/liblua.a"
else
LUA_LIB=" $LUA_PATH/lib/liblua_pic.a"
fi
export CC CXX AR CFLAGS CXXFLAGS EXEC_LDFLAGS EXEC_LDFLAGS_SHARED VALGRIND_VER JEMALLOC_LIB JEMALLOC_INCLUDE CLANG_ANALYZER CLANG_SCAN_BUILD LUA_PATH LUA_LIB
export CC CXX AR CFLAGS CXXFLAGS EXEC_LDFLAGS EXEC_LDFLAGS_SHARED VALGRIND_VER JEMALLOC_LIB JEMALLOC_INCLUDE CLANG_ANALYZER CLANG_SCAN_BUILD
+1 -1
View File
@@ -172,4 +172,4 @@ EXEC_LDFLAGS_SHARED="$SNAPPY_LIBS $ZLIB_LIBS $BZIP_LIBS $LZ4_LIBS $ZSTD_LIBS $GF
VALGRIND_VER="$VALGRIND_BASE/bin/"
export CC CXX AR AS CFLAGS CXXFLAGS EXEC_LDFLAGS EXEC_LDFLAGS_SHARED VALGRIND_VER JEMALLOC_LIB JEMALLOC_INCLUDE CLANG_ANALYZER CLANG_SCAN_BUILD LUA_PATH LUA_LIB
export CC CXX AR AS CFLAGS CXXFLAGS EXEC_LDFLAGS EXEC_LDFLAGS_SHARED VALGRIND_VER JEMALLOC_LIB JEMALLOC_INCLUDE CLANG_ANALYZER CLANG_SCAN_BUILD
+123
View File
@@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""
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
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''):
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()
try:
config.read(manifest_path)
if 'download' in config:
return {
'url': config['download'].get('url', ''),
'sha256': config['download'].get('sha256', ''),
}
except Exception:
pass
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():
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
def main():
if len(sys.argv) != 4:
print(f"Usage: {sys.argv[0]} <download_dir> <cache_dir> <manifests_dir>")
sys.exit(1)
download_dir, cache_dir, manifests_dir = sys.argv[1], sys.argv[2], sys.argv[3]
# Packages known to have unreliable mirrors
packages_to_check = ["autoconf", "automake", "libtool"]
for package in packages_to_check:
manifest_path = os.path.join(manifests_dir, package)
if not os.path.exists(manifest_path):
continue
info = parse_manifest(manifest_path)
if not info or not info['url'] or not info['sha256']:
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)")
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)
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)
if not downloaded:
print(f" {filename}: WARNING - all mirrors failed")
if __name__ == "__main__":
main()
-1
View File
@@ -101,6 +101,5 @@ get_lib_base benchmark LATEST platform010
get_lib_base kernel-headers fb platform010
get_lib_base binutils LATEST centos8-native
get_lib_base valgrind LATEST platform010
get_lib_base lua 5.3.4 platform010
git diff $OUTPUT
+1 -1
View File
@@ -644,7 +644,7 @@ using TypedHandle = SharedCache::TypedHandle;
TEST_P(CacheTest, SetCapacity) {
if (IsHyperClock()) {
// TODO: update test & code for limited supoort
// TODO: update test & code for limited support
ROCKSDB_GTEST_BYPASS(
"HyperClockCache doesn't support arbitrary capacity "
"adjustments.");
+258 -294
View File
@@ -38,13 +38,11 @@ namespace ROCKSDB_NAMESPACE {
namespace clock_cache {
namespace {
inline uint64_t GetRefcount(uint64_t meta) {
return ((meta >> ClockHandle::kAcquireCounterShift) -
(meta >> ClockHandle::kReleaseCounterShift)) &
ClockHandle::kCounterMask;
}
using SlotMeta = ClockHandle::SlotMeta;
using AcquireCounter = SlotMeta::AcquireCounter;
using ReleaseCounter = SlotMeta::ReleaseCounter;
inline uint64_t GetInitialCountdown(Cache::Priority priority) {
inline uint32_t GetInitialCountdown(Cache::Priority priority) {
// Set initial clock data from priority
// TODO: configuration parameters for priority handling and clock cycle
// count?
@@ -65,11 +63,11 @@ inline uint64_t GetInitialCountdown(Cache::Priority priority) {
inline void MarkEmpty(ClockHandle& h) {
#ifndef NDEBUG
// Mark slot as empty, with assertion
uint64_t meta = h.meta.Exchange(0);
assert(meta >> ClockHandle::kStateShift == ClockHandle::kStateConstruction);
auto old_meta = h.meta.Exchange({});
assert(old_meta.IsUnderConstruction());
#else
// Mark slot as empty
h.meta.Store(0);
h.meta.Store({});
#endif
}
@@ -85,18 +83,20 @@ inline void FreeDataMarkEmpty(ClockHandle& h, MemoryAllocator* allocator) {
// Called to undo the effect of referencing an entry for internal purposes,
// so it should not be marked as having been used.
inline void Unref(const ClockHandle& h, uint64_t count = 1) {
inline void Unref(const ClockHandle& h, uint32_t count = 1) {
// Pretend we never took the reference
// WART: there's a tiny chance we release last ref to invisible
// entry here. If that happens, we let eviction take care of it.
uint64_t old_meta = h.meta.FetchSub(ClockHandle::kAcquireIncrement * count);
assert(GetRefcount(old_meta) != 0);
SlotMeta old_meta;
h.meta.Apply(AcquireCounter::MinusTransformPromiseNoUnderflow(count),
&old_meta);
assert(old_meta.GetRefcount() != 0);
(void)old_meta;
}
inline bool ClockUpdate(ClockHandle& h, BaseClockTable::EvictionData* data,
bool* purgeable = nullptr) {
uint64_t meta;
SlotMeta meta;
if (purgeable) {
assert(*purgeable == false);
// In AutoHCC, our eviction process follows the chain structure, so we
@@ -110,46 +110,40 @@ inline bool ClockUpdate(ClockHandle& h, BaseClockTable::EvictionData* data,
meta = h.meta.LoadRelaxed();
}
if (((meta >> ClockHandle::kStateShift) & ClockHandle::kStateShareableBit) ==
0) {
if (!meta.IsShareable()) {
// Only clock update Shareable entries
if (purgeable) {
*purgeable = true;
// AutoHCC only: make sure we only attempt to update non-empty slots
assert((meta >> ClockHandle::kStateShift) &
ClockHandle::kStateOccupiedBit);
assert(!meta.IsEmpty());
}
return false;
}
uint64_t acquire_count =
(meta >> ClockHandle::kAcquireCounterShift) & ClockHandle::kCounterMask;
uint64_t release_count =
(meta >> ClockHandle::kReleaseCounterShift) & ClockHandle::kCounterMask;
uint32_t acquire_count = meta.GetAcquireCounter();
uint32_t release_count = meta.GetReleaseCounter();
if (acquire_count != release_count) {
// Only clock update entries with no outstanding refs
data->seen_pinned_count++;
return false;
}
if ((meta >> ClockHandle::kStateShift == ClockHandle::kStateVisible) &&
acquire_count > 0) {
if (meta.IsVisible() && acquire_count > 0) {
// Decrement clock
uint64_t new_count =
std::min(acquire_count - 1, uint64_t{ClockHandle::kMaxCountdown} - 1);
uint32_t new_count =
std::min(acquire_count - 1, uint32_t{ClockHandle::kMaxCountdown} - 1);
// Compare-exchange in the decremented clock info, but
// not aggressively
uint64_t new_meta =
(uint64_t{ClockHandle::kStateVisible} << ClockHandle::kStateShift) |
(meta & ClockHandle::kHitBitMask) |
(new_count << ClockHandle::kReleaseCounterShift) |
(new_count << ClockHandle::kAcquireCounterShift);
SlotMeta new_meta = meta;
new_meta.SetReleaseCounter(new_count);
new_meta.SetAcquireCounter(new_count);
h.meta.CasStrongRelaxed(meta, new_meta);
return false;
}
// Otherwise, remove entry (either unreferenced invisible or
// unreferenced and expired visible).
if (h.meta.CasStrong(meta, (uint64_t{ClockHandle::kStateConstruction}
<< ClockHandle::kStateShift) |
(meta & ClockHandle::kHitBitMask))) {
SlotMeta construction_meta;
construction_meta.SetUnderConstruction();
construction_meta.SetHit(meta.GetHit());
if (h.meta.CasStrong(meta, construction_meta)) {
// Took ownership.
data->freed_charge += h.GetTotalCharge();
data->freed_count += 1;
@@ -215,39 +209,39 @@ inline bool ClockUpdate(ClockHandle& h, BaseClockTable::EvictionData* data,
// counter to reach "high" state again and bumped back to "medium." (This
// motivates only checking for release counter in high state, not both in high
// state.)
inline void CorrectNearOverflow(uint64_t old_meta,
AcqRelAtomic<uint64_t>& meta) {
inline void CorrectNearOverflow(SlotMeta old_meta,
BitFieldsAtomic<SlotMeta>& meta) {
// We clear both top-most counter bits at the same time.
constexpr uint64_t kCounterTopBit = uint64_t{1}
<< (ClockHandle::kCounterNumBits - 1);
constexpr uint64_t kClearBits =
(kCounterTopBit << ClockHandle::kAcquireCounterShift) |
(kCounterTopBit << ClockHandle::kReleaseCounterShift);
// A simple check that allows us to initiate clearing the top bits for
// a large portion of the "high" state space on release counter.
constexpr uint64_t kCheckBits =
(kCounterTopBit | (ClockHandle::kMaxCountdown + 1))
<< ClockHandle::kReleaseCounterShift;
constexpr uint32_t kCounterTopBit = uint32_t{1}
<< (SlotMeta::kCounterNumBits - 1);
// The threshold for correcting "near overflow" is to ensure
// (a) the value has a top bit set that can be cleared
// (b) when we clear the top bit, the eviction state will be preserved
// (everything >= kMaxCountdown is treated equivalently)
// As mentioned above, we only check the release count.
constexpr uint32_t kThreshold = kCounterTopBit + ClockHandle::kMaxCountdown;
if (UNLIKELY(old_meta & kCheckBits)) {
meta.FetchAndRelaxed(~kClearBits);
if (UNLIKELY(old_meta.GetReleaseCounter() > kThreshold)) {
auto clear_transform = AcquireCounter::AndTransform(kCounterTopBit - 1) +
ReleaseCounter::AndTransform(kCounterTopBit - 1);
meta.ApplyRelaxed(clear_transform);
}
}
inline bool BeginSlotInsert(const ClockHandleBasicData& proto, ClockHandle& h,
uint64_t initial_countdown, bool* already_matches) {
uint32_t initial_countdown, bool* already_matches) {
assert(*already_matches == false);
// Optimistically transition the slot from "empty" to
// "under construction" (no effect on other states)
uint64_t old_meta = h.meta.FetchOr(uint64_t{ClockHandle::kStateOccupiedBit}
<< ClockHandle::kStateShift);
uint64_t old_state = old_meta >> ClockHandle::kStateShift;
auto set_occupied = SlotMeta::OccupiedFlag::SetTransform();
SlotMeta old_meta;
h.meta.Apply(set_occupied, &old_meta);
if (old_state == ClockHandle::kStateEmpty) {
if (old_meta.IsEmpty()) {
// We've started inserting into an available slot, and taken
// ownership.
return true;
} else if (old_state != ClockHandle::kStateVisible) {
} else if (!old_meta.IsVisible()) {
// Slot not usable / touchable now
return false;
}
@@ -255,15 +249,17 @@ inline bool BeginSlotInsert(const ClockHandleBasicData& proto, ClockHandle& h,
// But first, we need to acquire a ref to read it. In fact, number of
// refs for initial countdown, so that we boost the clock state if
// this is a match.
old_meta =
h.meta.FetchAdd(ClockHandle::kAcquireIncrement * initial_countdown);
auto add_acquire =
AcquireCounter::PlusTransformPromiseNoOverflow(initial_countdown);
h.meta.Apply(add_acquire, &old_meta);
// Like Lookup
if ((old_meta >> ClockHandle::kStateShift) == ClockHandle::kStateVisible) {
if (old_meta.IsVisible()) {
// Acquired a read reference
if (h.hashed_key == proto.hashed_key) {
// Match. Release in a way that boosts the clock state
old_meta =
h.meta.FetchAdd(ClockHandle::kReleaseIncrement * initial_countdown);
auto add_release =
ReleaseCounter::PlusTransformPromiseNoOverflow(initial_countdown);
h.meta.Apply(add_release, &old_meta);
// Correct for possible (but rare) overflow
CorrectNearOverflow(old_meta, h.meta);
// Insert detached instead (only if return handle needed)
@@ -273,8 +269,7 @@ inline bool BeginSlotInsert(const ClockHandleBasicData& proto, ClockHandle& h,
// Mismatch.
Unref(h, initial_countdown);
}
} else if (UNLIKELY((old_meta >> ClockHandle::kStateShift) ==
ClockHandle::kStateInvisible)) {
} else if (UNLIKELY(old_meta.IsInvisible())) {
// Pretend we never took the reference
Unref(h, initial_countdown);
} else {
@@ -286,25 +281,23 @@ inline bool BeginSlotInsert(const ClockHandleBasicData& proto, ClockHandle& h,
}
inline void FinishSlotInsert(const ClockHandleBasicData& proto, ClockHandle& h,
uint64_t initial_countdown, bool keep_ref) {
uint32_t initial_countdown, bool keep_ref) {
// Save data fields
ClockHandleBasicData* h_alias = &h;
*h_alias = proto;
// Transition from "under construction" state to "visible" state
uint64_t new_meta = uint64_t{ClockHandle::kStateVisible}
<< ClockHandle::kStateShift;
SlotMeta new_meta;
new_meta.SetVisible();
// Maybe with an outstanding reference
new_meta |= initial_countdown << ClockHandle::kAcquireCounterShift;
new_meta |= (initial_countdown - keep_ref)
<< ClockHandle::kReleaseCounterShift;
new_meta.SetAcquireCounter(initial_countdown);
new_meta.SetReleaseCounter(initial_countdown - (keep_ref ? 1 : 0));
#ifndef NDEBUG
// Save the state transition, with assertion
uint64_t old_meta = h.meta.Exchange(new_meta);
assert(old_meta >> ClockHandle::kStateShift ==
ClockHandle::kStateConstruction);
auto old_meta = h.meta.Exchange(new_meta);
assert(old_meta.IsUnderConstruction());
#else
// Save the state transition
h.meta.Store(new_meta);
@@ -312,7 +305,7 @@ inline void FinishSlotInsert(const ClockHandleBasicData& proto, ClockHandle& h,
}
bool TryInsert(const ClockHandleBasicData& proto, ClockHandle& h,
uint64_t initial_countdown, bool keep_ref,
uint32_t initial_countdown, bool keep_ref,
bool* already_matches) {
bool b = BeginSlotInsert(proto, h, initial_countdown, already_matches);
if (b) {
@@ -326,35 +319,32 @@ template <class HandleImpl, class Func>
void ConstApplyToEntriesRange(const Func& func, const HandleImpl* begin,
const HandleImpl* end,
bool apply_if_will_be_deleted) {
uint64_t check_state_mask = ClockHandle::kStateShareableBit;
if (!apply_if_will_be_deleted) {
check_state_mask |= ClockHandle::kStateVisibleBit;
}
for (const HandleImpl* h = begin; h < end; ++h) {
// Note: to avoid using compare_exchange, we have to be extra careful.
uint64_t old_meta = h->meta.LoadRelaxed();
SlotMeta old_meta = h->meta.LoadRelaxed();
// Check if it's an entry visible to lookups
if ((old_meta >> ClockHandle::kStateShift) & check_state_mask) {
// Increment acquire counter. Note: it's possible that the entry has
// completely changed since we loaded old_meta, but incrementing acquire
// count is always safe. (Similar to optimistic Lookup here.)
old_meta = h->meta.FetchAdd(ClockHandle::kAcquireIncrement);
// Check whether we actually acquired a reference.
if ((old_meta >> ClockHandle::kStateShift) &
ClockHandle::kStateShareableBit) {
// Apply func if appropriate
if ((old_meta >> ClockHandle::kStateShift) & check_state_mask) {
func(*h);
if (apply_if_will_be_deleted || old_meta.IsVisible()) {
if (old_meta.IsShareable()) {
// Increment acquire counter. Note: it's possible that the entry has
// completely changed since we loaded old_meta, but incrementing acquire
// count is always safe. (Similar to optimistic Lookup here.)
auto add_acquire = AcquireCounter::PlusTransformPromiseNoOverflow(1);
h->meta.Apply(add_acquire, &old_meta);
// Check whether we actually acquired a reference.
if (old_meta.IsShareable()) {
// Apply func if appropriate
if (apply_if_will_be_deleted || old_meta.IsVisible()) {
func(*h);
}
// Pretend we never took the reference
Unref(*h);
// No net change, so don't need to check for overflow
} else {
// For other states, incrementing the acquire counter has no effect
// so we don't need to undo it. Furthermore, we cannot safely undo
// it because we did not acquire a read reference to lock the
// entry in a Shareable state.
}
// Pretend we never took the reference
Unref(*h);
// No net change, so don't need to check for overflow
} else {
// For other states, incrementing the acquire counter has no effect
// so we don't need to undo it. Furthermore, we cannot safely undo
// it because we did not acquire a read reference to lock the
// entry in a Shareable state.
}
}
}
@@ -399,9 +389,9 @@ HandleImpl* BaseClockTable::StandaloneInsert(
h->SetStandalone();
// Single reference (standalone entries only created if returning a refed
// Handle back to user)
uint64_t meta = uint64_t{ClockHandle::kStateInvisible}
<< ClockHandle::kStateShift;
meta |= uint64_t{1} << ClockHandle::kAcquireCounterShift;
SlotMeta meta;
meta.SetInvisible();
meta.SetAcquireCounter(1);
h->meta.Store(meta);
// Keep track of how much of usage is standalone
standalone_usage_.FetchAddRelaxed(proto.GetTotalCharge());
@@ -564,11 +554,10 @@ void BaseClockTable::TrackAndReleaseEvictedEntry(ClockHandle* h) {
if (eviction_callback_) {
// For key reconstructed from hash
UniqueId64x2 unhashed;
took_value_ownership =
eviction_callback_(ClockCacheShard<FixedHyperClockTable>::ReverseHash(
h->GetHash(), &unhashed, hash_seed_),
static_cast<Cache::Handle*>(h),
h->meta.LoadRelaxed() & ClockHandle::kHitBitMask);
took_value_ownership = eviction_callback_(
ClockCacheShard<FixedHyperClockTable>::ReverseHash(
h->GetHash(), &unhashed, hash_seed_),
static_cast<Cache::Handle*>(h), h->meta.LoadRelaxed().GetHit());
}
if (!took_value_ownership) {
h->FreeData(allocator_);
@@ -648,7 +637,7 @@ Status BaseClockTable::Insert(const ClockHandleBasicData& proto,
// * Have to insert into a suboptimal location (more probes) so that the
// old entry can be kept around as well.
uint64_t initial_countdown = GetInitialCountdown(priority);
uint32_t initial_countdown = GetInitialCountdown(priority);
assert(initial_countdown > 0);
HandleImpl* e =
@@ -693,34 +682,34 @@ Status BaseClockTable::Insert(const ClockHandleBasicData& proto,
void BaseClockTable::Ref(ClockHandle& h) {
// Increment acquire counter
uint64_t old_meta = h.meta.FetchAdd(ClockHandle::kAcquireIncrement);
SlotMeta old_meta;
h.meta.Apply(AcquireCounter::PlusTransformPromiseNoOverflow(1), &old_meta);
assert((old_meta >> ClockHandle::kStateShift) &
ClockHandle::kStateShareableBit);
assert(old_meta.IsShareable());
// Must have already had a reference
assert(GetRefcount(old_meta) > 0);
assert(old_meta.GetRefcount() > 0);
(void)old_meta;
}
#ifndef NDEBUG
void BaseClockTable::TEST_RefN(ClockHandle& h, size_t n) {
void BaseClockTable::TEST_RefN(ClockHandle& h, uint32_t n) {
// Increment acquire counter
uint64_t old_meta = h.meta.FetchAdd(n * ClockHandle::kAcquireIncrement);
SlotMeta old_meta;
h.meta.Apply(AcquireCounter::PlusTransformPromiseNoOverflow(n), &old_meta);
assert((old_meta >> ClockHandle::kStateShift) &
ClockHandle::kStateShareableBit);
assert(old_meta.IsShareable());
(void)old_meta;
}
void BaseClockTable::TEST_ReleaseNMinus1(ClockHandle* h, size_t n) {
void BaseClockTable::TEST_ReleaseNMinus1(ClockHandle* h, uint32_t n) {
assert(n > 0);
// Like n-1 Releases, but assumes one more will happen in the caller to take
// care of anything like erasing an unreferenced, invisible entry.
uint64_t old_meta =
h->meta.FetchAdd((n - 1) * ClockHandle::kReleaseIncrement);
assert((old_meta >> ClockHandle::kStateShift) &
ClockHandle::kStateShareableBit);
SlotMeta old_meta;
h->meta.Apply(ReleaseCounter::PlusTransformPromiseNoOverflow(n - 1),
&old_meta);
assert(old_meta.IsShareable());
(void)old_meta;
}
#endif
@@ -754,23 +743,20 @@ FixedHyperClockTable::~FixedHyperClockTable() {
// in the table.
for (size_t i = 0; i < GetTableSize(); i++) {
HandleImpl& h = array_[i];
switch (h.meta.LoadRelaxed() >> ClockHandle::kStateShift) {
case ClockHandle::kStateEmpty:
// noop
break;
case ClockHandle::kStateInvisible: // rare but possible
case ClockHandle::kStateVisible:
assert(GetRefcount(h.meta.LoadRelaxed()) == 0);
h.FreeData(allocator_);
SlotMeta meta = h.meta.LoadRelaxed();
if (meta.IsShareable()) {
// NOTE: Reaching here invisible is rare but possible
assert(meta.GetRefcount() == 0);
h.FreeData(allocator_);
#ifndef NDEBUG
Rollback(h.hashed_key, &h);
ReclaimEntryUsage(h.GetTotalCharge());
Rollback(h.hashed_key, &h);
ReclaimEntryUsage(h.GetTotalCharge());
#endif
break;
// otherwise
default:
assert(false);
break;
} else {
// Should be no transient "under construction" states unless a thread
// was killed or we are being destructed while another thread is still
// operating on the structure
assert(meta.IsEmpty());
}
}
@@ -792,7 +778,7 @@ bool FixedHyperClockTable::GrowIfNeeded(size_t new_occupancy, InsertState&) {
}
FixedHyperClockTable::HandleImpl* FixedHyperClockTable::DoInsert(
const ClockHandleBasicData& proto, uint64_t initial_countdown,
const ClockHandleBasicData& proto, uint32_t initial_countdown,
bool keep_ref, InsertState&) {
bool already_matches = false;
HandleImpl* e = FindSlot(
@@ -843,47 +829,46 @@ FixedHyperClockTable::HandleImpl* FixedHyperClockTable::Lookup(
HandleImpl* e = FindSlot(
hashed_key,
[&](HandleImpl* h) {
SlotMeta old_meta;
// Mostly branch-free version (similar performance)
/*
uint64_t old_meta = h->meta.FetchAdd(ClockHandle::kAcquireIncrement,
std::memory_order_acquire);
bool Shareable = (old_meta >> (ClockHandle::kStateShift + 1)) & 1U;
bool visible = (old_meta >> ClockHandle::kStateShift) & 1U;
bool match = (h->key == key) & visible;
h->meta.FetchSub(static_cast<uint64_t>(Shareable & !match) <<
ClockHandle::kAcquireCounterShift); return
match;
h->meta.Apply(AcquireCounter::PlusTransformPromiseNoOverflow(1),
&old_meta);
bool shareable = old_meta.IsShareable();
bool visible = old_meta.IsVisible();
bool match = (h->hashed_key == hashed_key) & visible;
h->meta.Apply(AcquireCounter::MinusTransformPromiseNoUnderflow(
uint32_t{shareable} & uint32_t{!match}));
h->meta.Apply(SlotMeta::HitFlag::Or(match));
return match;
*/
// Optimistic lookup should pay off when the table is relatively
// sparse.
constexpr bool kOptimisticLookup = true;
uint64_t old_meta;
if (!kOptimisticLookup) {
old_meta = h->meta.Load();
if ((old_meta >> ClockHandle::kStateShift) !=
ClockHandle::kStateVisible) {
if (!old_meta.IsVisible()) {
return false;
}
}
// (Optimistically) increment acquire counter
old_meta = h->meta.FetchAdd(ClockHandle::kAcquireIncrement);
h->meta.Apply(AcquireCounter::PlusTransformPromiseNoOverflow(1),
&old_meta);
// Check if it's an entry visible to lookups
if ((old_meta >> ClockHandle::kStateShift) ==
ClockHandle::kStateVisible) {
if (old_meta.IsVisible()) {
// Acquired a read reference
if (h->hashed_key == hashed_key) {
// Match
// Update the hit bit
if (eviction_callback_) {
h->meta.FetchOrRelaxed(uint64_t{1} << ClockHandle::kHitBitShift);
h->meta.ApplyRelaxed(SlotMeta::HitFlag::SetTransform());
}
return true;
} else {
// Mismatch. Pretend we never took the reference
Unref(*h);
}
} else if (UNLIKELY((old_meta >> ClockHandle::kStateShift) ==
ClockHandle::kStateInvisible)) {
} else if (UNLIKELY(old_meta.IsInvisible())) {
// Pretend we never took the reference
Unref(*h);
} else {
@@ -907,53 +892,49 @@ bool FixedHyperClockTable::Release(HandleImpl* h, bool useful,
// is only freed up by EvictFromClock (called by Insert when space is needed)
// and Erase. We do this to avoid an extra atomic read of the variable usage_.
uint64_t old_meta;
SlotMeta old_meta;
if (useful) {
// Increment release counter to indicate was used
old_meta = h->meta.FetchAdd(ClockHandle::kReleaseIncrement);
auto add_release = ReleaseCounter::PlusTransformPromiseNoOverflow(1);
h->meta.Apply(add_release, &old_meta);
} else {
// Decrement acquire counter to pretend it never happened
old_meta = h->meta.FetchSub(ClockHandle::kAcquireIncrement);
auto sub_acquire = AcquireCounter::MinusTransformPromiseNoUnderflow(1);
h->meta.Apply(sub_acquire, &old_meta);
}
assert((old_meta >> ClockHandle::kStateShift) &
ClockHandle::kStateShareableBit);
assert(old_meta.IsShareable());
// No underflow
assert(((old_meta >> ClockHandle::kAcquireCounterShift) &
ClockHandle::kCounterMask) !=
((old_meta >> ClockHandle::kReleaseCounterShift) &
ClockHandle::kCounterMask));
assert(old_meta.GetAcquireCounter() != old_meta.GetReleaseCounter());
if (erase_if_last_ref || UNLIKELY(old_meta >> ClockHandle::kStateShift ==
ClockHandle::kStateInvisible)) {
if (erase_if_last_ref || UNLIKELY(old_meta.IsInvisible())) {
// FIXME: There's a chance here that another thread could replace this
// entry and we end up erasing the wrong one.
// Update for last FetchAdd op
// Update for last Apply op
if (useful) {
old_meta += ClockHandle::kReleaseIncrement;
old_meta.SetReleaseCounter(old_meta.GetReleaseCounter() + 1);
} else {
old_meta -= ClockHandle::kAcquireIncrement;
old_meta.SetAcquireCounter(old_meta.GetAcquireCounter() - 1);
}
// Take ownership if no refs
SlotMeta construction_meta;
construction_meta.SetUnderConstruction();
do {
if (GetRefcount(old_meta) != 0) {
if (old_meta.GetRefcount() != 0) {
// Not last ref at some point in time during this Release call
// Correct for possible (but rare) overflow
CorrectNearOverflow(old_meta, h->meta);
return false;
}
if ((old_meta & (uint64_t{ClockHandle::kStateShareableBit}
<< ClockHandle::kStateShift)) == 0) {
if (!old_meta.IsShareable()) {
// Someone else took ownership
return false;
}
// Note that there's a small chance that we release, another thread
// replaces this entry with another, reaches zero refs, and then we end
// up erasing that other entry. That's an acceptable risk / imprecision.
} while (
!h->meta.CasWeak(old_meta, uint64_t{ClockHandle::kStateConstruction}
<< ClockHandle::kStateShift));
} while (!h->meta.CasWeak(old_meta, construction_meta));
// Took ownership
size_t total_charge = h->GetTotalCharge();
if (UNLIKELY(h->IsStandalone())) {
@@ -976,7 +957,7 @@ bool FixedHyperClockTable::Release(HandleImpl* h, bool useful,
}
#ifndef NDEBUG
void FixedHyperClockTable::TEST_ReleaseN(HandleImpl* h, size_t n) {
void FixedHyperClockTable::TEST_ReleaseN(HandleImpl* h, uint32_t n) {
if (n > 0) {
// Do n-1 simple releases first
TEST_ReleaseNMinus1(h, n);
@@ -993,30 +974,29 @@ void FixedHyperClockTable::Erase(const UniqueId64x2& hashed_key) {
[&](HandleImpl* h) {
// Could be multiple entries in rare cases. Erase them all.
// Optimistically increment acquire counter
uint64_t old_meta = h->meta.FetchAdd(ClockHandle::kAcquireIncrement);
auto add_acquire = AcquireCounter::PlusTransformPromiseNoOverflow(1);
SlotMeta old_meta, meta;
h->meta.Apply(add_acquire, &old_meta, &meta);
// Check if it's an entry visible to lookups
if ((old_meta >> ClockHandle::kStateShift) ==
ClockHandle::kStateVisible) {
if (meta.IsVisible()) {
// Acquired a read reference
if (h->hashed_key == hashed_key) {
// Match. Set invisible.
old_meta =
h->meta.FetchAnd(~(uint64_t{ClockHandle::kStateVisibleBit}
<< ClockHandle::kStateShift));
// Apply update to local copy
old_meta &= ~(uint64_t{ClockHandle::kStateVisibleBit}
<< ClockHandle::kStateShift);
// Match. Take ownership if no other refs, or set invisible other
// refs exist.
for (;;) {
uint64_t refcount = GetRefcount(old_meta);
uint32_t refcount = meta.GetRefcount();
assert(refcount > 0);
if (refcount > 1) {
// Not last ref at some point in time during this Erase call
// Pretend we never took the reference
// Set invisible
h->meta.Apply(SlotMeta::VisibleFlag::ClearTransform());
// And pretend we never took the reference
Unref(*h);
break;
} else if (h->meta.CasWeak(
old_meta, uint64_t{ClockHandle::kStateConstruction}
<< ClockHandle::kStateShift)) {
}
SlotMeta construction_meta;
construction_meta.SetUnderConstruction();
if (h->meta.CasWeak(meta, construction_meta)) {
// Took ownership
assert(hashed_key == h->hashed_key);
size_t total_charge = h->GetTotalCharge();
@@ -1032,8 +1012,7 @@ void FixedHyperClockTable::Erase(const UniqueId64x2& hashed_key) {
// Mismatch. Pretend we never took the reference
Unref(*h);
}
} else if (UNLIKELY((old_meta >> ClockHandle::kStateShift) ==
ClockHandle::kStateInvisible)) {
} else if (UNLIKELY(old_meta.IsInvisible())) {
// Pretend we never took the reference
Unref(*h);
} else {
@@ -1050,17 +1029,17 @@ void FixedHyperClockTable::EraseUnRefEntries() {
for (size_t i = 0; i <= this->length_bits_mask_; i++) {
HandleImpl& h = array_[i];
uint64_t old_meta = h.meta.LoadRelaxed();
if (old_meta & (uint64_t{ClockHandle::kStateShareableBit}
<< ClockHandle::kStateShift) &&
GetRefcount(old_meta) == 0 &&
h.meta.CasStrong(old_meta, uint64_t{ClockHandle::kStateConstruction}
<< ClockHandle::kStateShift)) {
// Took ownership
size_t total_charge = h.GetTotalCharge();
Rollback(h.hashed_key, &h);
FreeDataMarkEmpty(h, allocator_);
ReclaimEntryUsage(total_charge);
SlotMeta old_meta = h.meta.LoadRelaxed();
if (old_meta.IsShareable() && old_meta.GetRefcount() == 0) {
SlotMeta construction_meta;
construction_meta.SetUnderConstruction();
if (h.meta.CasStrong(old_meta, construction_meta)) {
// Took ownership
size_t total_charge = h.GetTotalCharge();
Rollback(h.hashed_key, &h);
FreeDataMarkEmpty(h, allocator_);
ReclaimEntryUsage(total_charge);
}
}
}
}
@@ -1320,12 +1299,12 @@ bool ClockCacheShard<Table>::Release(HandleImpl* handle, bool useful,
#ifndef NDEBUG
template <class Table>
void ClockCacheShard<Table>::TEST_RefN(HandleImpl* h, size_t n) {
void ClockCacheShard<Table>::TEST_RefN(HandleImpl* h, uint32_t n) {
table_.TEST_RefN(*h, n);
}
template <class Table>
void ClockCacheShard<Table>::TEST_ReleaseN(HandleImpl* h, size_t n) {
void ClockCacheShard<Table>::TEST_ReleaseN(HandleImpl* h, uint32_t n) {
table_.TEST_ReleaseN(h, n);
}
#endif
@@ -1373,8 +1352,8 @@ size_t ClockCacheShard<Table>::GetPinnedUsage() const {
metadata_charge_policy_ == kFullChargeCacheMetadata;
ConstApplyToEntriesRange(
[&table_pinned_usage, charge_metadata](const HandleImpl& h) {
uint64_t meta = h.meta.LoadRelaxed();
uint64_t refcount = GetRefcount(meta);
SlotMeta meta = h.meta.LoadRelaxed();
uint32_t refcount = meta.GetRefcount();
// Holding one ref for ConstApplyToEntriesRange
assert(refcount > 0);
if (refcount > 1) {
@@ -1494,7 +1473,7 @@ void AddShardEvaluation(const FixedHyperClockCache::Shard& shard,
}
bool IsSlotOccupied(const ClockHandle& h) {
return (h.meta.LoadRelaxed() >> ClockHandle::kStateShift) != 0;
return !h.meta.LoadRelaxed().IsEmpty();
}
} // namespace
@@ -1759,12 +1738,12 @@ inline bool MatchAndRef(const UniqueId64x2* hashed_key, const ClockHandle& h,
// Must be at least something to match
assert(hashed_key || shift > 0);
uint64_t old_meta;
SlotMeta old_meta, new_meta;
// (Optimistically) increment acquire counter.
old_meta = h.meta.FetchAdd(ClockHandle::kAcquireIncrement);
auto add_acquire = AcquireCounter::PlusTransformPromiseNoOverflow(1);
h.meta.Apply(add_acquire, &old_meta, &new_meta);
// Check if it's a referencable (sharable) entry
if ((old_meta & (uint64_t{ClockHandle::kStateShareableBit}
<< ClockHandle::kStateShift)) == 0) {
if (!old_meta.IsShareable()) {
// For non-sharable states, incrementing the acquire counter has no effect
// so we don't need to undo it. Furthermore, we cannot safely undo
// it because we did not acquire a read reference to lock the
@@ -1775,10 +1754,9 @@ inline bool MatchAndRef(const UniqueId64x2* hashed_key, const ClockHandle& h,
return false;
}
// Else acquired a read reference
assert(GetRefcount(old_meta + ClockHandle::kAcquireIncrement) > 0);
assert(new_meta.GetRefcount() > 0);
if (hashed_key && h.hashed_key == *hashed_key &&
LIKELY(old_meta & (uint64_t{ClockHandle::kStateVisibleBit}
<< ClockHandle::kStateShift))) {
LIKELY(old_meta.IsVisible())) {
// Match on full key, visible
if (full_match_or_unknown) {
*full_match_or_unknown = true;
@@ -1946,7 +1924,7 @@ class AutoHyperClockTable::ChainRewriteLock {
}
}
AcqRelBitFieldsAtomic<NextWithShift>* head_ptr_;
BitFieldsAtomic<NextWithShift>* head_ptr_;
NextWithShift saved_head_;
};
@@ -2051,7 +2029,7 @@ AutoHyperClockTable::~AutoHyperClockTable() {
HandleImpl::kUnusedMarker);
assert(array_[i].chain_next_with_shift.LoadRelaxed() ==
HandleImpl::kUnusedMarker);
assert(array_[i].meta.LoadRelaxed() == 0);
assert(array_[i].meta.LoadRelaxed() == SlotMeta{});
}
#endif // MUST_FREE_HEAP_ALLOCATIONS
#ifndef NDEBUG // Extra invariant checking
@@ -2060,30 +2038,27 @@ AutoHyperClockTable::~AutoHyperClockTable() {
#endif // !NDEBUG
for (size_t i = 0; i < used_end; i++) {
HandleImpl& h = array_[i];
switch (h.meta.LoadRelaxed() >> ClockHandle::kStateShift) {
case ClockHandle::kStateEmpty:
// noop
break;
case ClockHandle::kStateInvisible: // rare but possible
case ClockHandle::kStateVisible:
assert(GetRefcount(h.meta.LoadRelaxed()) == 0);
h.FreeData(allocator_);
SlotMeta meta = h.meta.LoadRelaxed();
if (meta.IsShareable()) {
// NOTE: Reaching here invisible is rare but possible
assert(meta.GetRefcount() == 0);
h.FreeData(allocator_);
#ifndef NDEBUG // Extra invariant checking
usage_.FetchSubRelaxed(h.total_charge);
occupancy_.FetchSubRelaxed(1U);
was_populated[i] = true;
if (!h.chain_next_with_shift.LoadRelaxed().IsEnd()) {
assert(!h.chain_next_with_shift.LoadRelaxed().IsLocked());
size_t next = h.chain_next_with_shift.LoadRelaxed().GetNext();
assert(!was_pointed_to[next]);
was_pointed_to[next] = true;
}
usage_.FetchSubRelaxed(h.total_charge);
occupancy_.FetchSubRelaxed(1U);
was_populated[i] = true;
if (!h.chain_next_with_shift.LoadRelaxed().IsEnd()) {
assert(!h.chain_next_with_shift.LoadRelaxed().IsLocked());
size_t next = h.chain_next_with_shift.LoadRelaxed().GetNext();
assert(!was_pointed_to[next]);
was_pointed_to[next] = true;
}
#endif // !NDEBUG
break;
// otherwise
default:
assert(false);
break;
} else {
// Should be no transient "under construction" states unless a thread
// was killed or we are being destructed while another thread is still
// operating on the structure
assert(meta.IsEmpty());
}
#ifndef NDEBUG // Extra invariant checking
if (!h.head_next_with_shift.LoadRelaxed().IsEnd()) {
@@ -2691,20 +2666,17 @@ void AutoHyperClockTable::PurgeImplLocked(OpData* op_data,
op_data->push_back(h);
// Entries for eviction become purgeable
purgeable = true;
assert((h->meta.Load() >> ClockHandle::kStateShift) ==
ClockHandle::kStateConstruction);
assert(h->meta.Load().IsUnderConstruction());
}
} else {
(void)op_data;
(void)data;
purgeable = ((h->meta.Load() >> ClockHandle::kStateShift) &
ClockHandle::kStateShareableBit) == 0;
purgeable = !h->meta.Load().IsShareable();
}
}
if (purgeable) {
assert((h->meta.Load() >> ClockHandle::kStateShift) ==
ClockHandle::kStateConstruction);
assert(h->meta.Load().IsUnderConstruction());
pending_purge = true;
} else if (pending_purge) {
if (prev_to_keep) {
@@ -2864,7 +2836,7 @@ void AutoHyperClockTable::PurgeImpl(OpData* op_data, size_t home,
}
AutoHyperClockTable::HandleImpl* AutoHyperClockTable::DoInsert(
const ClockHandleBasicData& proto, uint64_t initial_countdown,
const ClockHandleBasicData& proto, uint32_t initial_countdown,
bool take_ref, InsertState& state) {
size_t home;
int orig_home_shift;
@@ -3149,14 +3121,14 @@ AutoHyperClockTable::HandleImpl* AutoHyperClockTable::Lookup(
#endif
if (probably_equal) {
// Increment acquire counter for definitive check
uint64_t old_meta = h->meta.FetchAdd(ClockHandle::kAcquireIncrement);
auto add_acquire = AcquireCounter::PlusTransformPromiseNoOverflow(1);
SlotMeta old_meta, new_meta;
h->meta.Apply(add_acquire, &old_meta, &new_meta);
// Check if it's a referencable (sharable) entry
if (LIKELY(old_meta & (uint64_t{ClockHandle::kStateShareableBit}
<< ClockHandle::kStateShift))) {
assert(GetRefcount(old_meta + ClockHandle::kAcquireIncrement) > 0);
if (LIKELY(old_meta.IsShareable())) {
assert(new_meta.GetRefcount() > 0);
if (LIKELY(h->hashed_key == hashed_key) &&
LIKELY(old_meta & (uint64_t{ClockHandle::kStateVisibleBit}
<< ClockHandle::kStateShift))) {
LIKELY(old_meta.IsVisible())) {
return h;
} else {
Unref(*h);
@@ -3277,7 +3249,7 @@ AutoHyperClockTable::HandleImpl* AutoHyperClockTable::Lookup(
}
// Update the hit bit
if (eviction_callback_) {
h->meta.FetchOrRelaxed(uint64_t{1} << ClockHandle::kHitBitShift);
h->meta.ApplyRelaxed(SlotMeta::HitFlag::SetTransform());
}
// All done.
return h;
@@ -3317,8 +3289,7 @@ AutoHyperClockTable::HandleImpl* AutoHyperClockTable::Lookup(
}
void AutoHyperClockTable::Remove(HandleImpl* h) {
assert((h->meta.Load() >> ClockHandle::kStateShift) ==
ClockHandle::kStateConstruction);
assert(h->meta.Load().IsUnderConstruction());
const HandleImpl& c_h = *h;
PurgeImpl(&c_h.hashed_key);
@@ -3326,26 +3297,23 @@ void AutoHyperClockTable::Remove(HandleImpl* h) {
bool AutoHyperClockTable::TryEraseHandle(HandleImpl* h, bool holding_ref,
bool mark_invisible) {
uint64_t meta;
if (mark_invisible) {
// Set invisible
meta = h->meta.FetchAnd(
~(uint64_t{ClockHandle::kStateVisibleBit} << ClockHandle::kStateShift));
// To local variable also
meta &=
~(uint64_t{ClockHandle::kStateVisibleBit} << ClockHandle::kStateShift);
} else {
meta = h->meta.Load();
}
SlotMeta meta = h->meta.Load();
assert(!holding_ref || meta.IsShareable());
// Take ownership if no other refs
// Take ownership if no other refs, or set invisible if other refs exist (and
// mark_invisible is set).
SlotMeta construction_meta;
construction_meta.SetUnderConstruction();
do {
if (GetRefcount(meta) != uint64_t{holding_ref}) {
if (meta.GetRefcount() != uint32_t{holding_ref}) {
// Not last ref at some point in time during this call
if (mark_invisible) {
// Set invisible
h->meta.Apply(SlotMeta::VisibleFlag::ClearTransform());
}
return false;
}
if ((meta & (uint64_t{ClockHandle::kStateShareableBit}
<< ClockHandle::kStateShift)) == 0) {
if (!meta.IsShareable()) {
// Someone else took ownership
return false;
}
@@ -3353,8 +3321,7 @@ bool AutoHyperClockTable::TryEraseHandle(HandleImpl* h, bool holding_ref,
// another thread replaces this entry with another, reaches zero refs, and
// then we end up erasing that other entry. That's an acceptable risk /
// imprecision.
} while (!h->meta.CasWeak(meta, uint64_t{ClockHandle::kStateConstruction}
<< ClockHandle::kStateShift));
} while (!h->meta.CasWeak(meta, construction_meta));
// Took ownership
// TODO? Delay freeing?
h->FreeData(allocator_);
@@ -3381,27 +3348,24 @@ bool AutoHyperClockTable::Release(HandleImpl* h, bool useful,
// is needed) and Erase. We do this to avoid an extra atomic read of the
// variable usage_.
uint64_t old_meta;
SlotMeta old_meta;
if (useful) {
// Increment release counter to indicate was used
old_meta = h->meta.FetchAdd(ClockHandle::kReleaseIncrement);
auto add_release = ReleaseCounter::PlusTransformPromiseNoOverflow(1);
h->meta.Apply(add_release, &old_meta);
// Correct for possible (but rare) overflow
CorrectNearOverflow(old_meta, h->meta);
} else {
// Decrement acquire counter to pretend it never happened
old_meta = h->meta.FetchSub(ClockHandle::kAcquireIncrement);
auto sub_acquire = AcquireCounter::MinusTransformPromiseNoUnderflow(1);
h->meta.Apply(sub_acquire, &old_meta);
}
assert((old_meta >> ClockHandle::kStateShift) &
ClockHandle::kStateShareableBit);
assert(old_meta.IsShareable());
// No underflow
assert(((old_meta >> ClockHandle::kAcquireCounterShift) &
ClockHandle::kCounterMask) !=
((old_meta >> ClockHandle::kReleaseCounterShift) &
ClockHandle::kCounterMask));
assert(old_meta.GetAcquireCounter() != old_meta.GetReleaseCounter());
if ((erase_if_last_ref || UNLIKELY(old_meta >> ClockHandle::kStateShift ==
ClockHandle::kStateInvisible))) {
if ((erase_if_last_ref || UNLIKELY(old_meta.IsInvisible()))) {
// FIXME: There's a chance here that another thread could replace this
// entry and we end up erasing the wrong one.
return TryEraseHandle(h, /*holding_ref=*/false, /*mark_invisible=*/false);
@@ -3411,7 +3375,7 @@ bool AutoHyperClockTable::Release(HandleImpl* h, bool useful,
}
#ifndef NDEBUG
void AutoHyperClockTable::TEST_ReleaseN(HandleImpl* h, size_t n) {
void AutoHyperClockTable::TEST_ReleaseN(HandleImpl* h, uint32_t n) {
if (n > 0) {
// Do n-1 simple releases first
TEST_ReleaseNMinus1(h, n);
@@ -3441,20 +3405,20 @@ void AutoHyperClockTable::EraseUnRefEntries() {
for (size_t i = 0; i < usable_size; i++) {
HandleImpl& h = array_[i];
uint64_t old_meta = h.meta.LoadRelaxed();
if (old_meta & (uint64_t{ClockHandle::kStateShareableBit}
<< ClockHandle::kStateShift) &&
GetRefcount(old_meta) == 0 &&
h.meta.CasStrong(old_meta, uint64_t{ClockHandle::kStateConstruction}
<< ClockHandle::kStateShift)) {
// Took ownership
h.FreeData(allocator_);
usage_.FetchSubRelaxed(h.total_charge);
// NOTE: could be more efficient with a dedicated variant of
// PurgeImpl, but this is not a common operation
Remove(&h);
MarkEmpty(h);
occupancy_.FetchSub(1U);
SlotMeta old_meta = h.meta.LoadRelaxed();
if (old_meta.IsShareable() && old_meta.GetRefcount() == 0) {
SlotMeta construction_meta;
construction_meta.SetUnderConstruction();
if (h.meta.CasStrong(old_meta, construction_meta)) {
// Took ownership
h.FreeData(allocator_);
usage_.FetchSubRelaxed(h.total_charge);
// NOTE: could be more efficient with a dedicated variant of
// PurgeImpl, but this is not a common operation
Remove(&h);
MarkEmpty(h);
occupancy_.FetchSub(1U);
}
}
}
}
+93 -44
View File
@@ -317,40 +317,89 @@ struct ClockHandle : public ClockHandleBasicData {
// | acquire counter | release counter | hit bit | state marker |
// -----------------------------------------------------------------------
// For reading or updating counters in meta word.
static constexpr uint8_t kCounterNumBits = 30;
static constexpr uint64_t kCounterMask = (uint64_t{1} << kCounterNumBits) - 1;
struct SlotMeta : public BitFields<uint64_t, SlotMeta> {
// For reading or updating counters in meta word.
static constexpr uint8_t kCounterNumBits = 30;
// Number of times the a reference has been acquired (or attempted)
// since last reset by eviction processing
using AcquireCounter =
UnsignedBitField<SlotMeta, kCounterNumBits, NoPrevBitField>;
// Number of times the a reference has been released (or attempted)
// since last reset by eviction processing
using ReleaseCounter =
UnsignedBitField<SlotMeta, kCounterNumBits, AcquireCounter>;
// Metadata bit in support of secondary cache
using HitFlag = BoolBitField<SlotMeta, ReleaseCounter>;
// Occupied means any state other than empty
using OccupiedFlag = BoolBitField<SlotMeta, HitFlag>;
// Shareable means the entry is reference counted (visible or invisible)
// (only set if also occupied)
using ShareableFlag = BoolBitField<SlotMeta, OccupiedFlag>;
// Visible is only set if also shareable (invisible can't be found by
// Lookup)
using VisibleFlag = BoolBitField<SlotMeta, ShareableFlag>;
static constexpr uint8_t kAcquireCounterShift = 0;
static constexpr uint64_t kAcquireIncrement = uint64_t{1}
<< kAcquireCounterShift;
static constexpr uint8_t kReleaseCounterShift = kCounterNumBits;
static constexpr uint64_t kReleaseIncrement = uint64_t{1}
<< kReleaseCounterShift;
// Convenience functions
uint32_t GetAcquireCounter() const { return Get<AcquireCounter>(); }
void SetAcquireCounter(uint32_t val) { Set<AcquireCounter>(val); }
uint32_t GetReleaseCounter() const { return Get<ReleaseCounter>(); }
void SetReleaseCounter(uint32_t val) { Set<ReleaseCounter>(val); }
uint32_t GetRefcount() const {
return Get<AcquireCounter>() - Get<ReleaseCounter>();
}
bool GetHit() const { return Get<HitFlag>(); }
void SetHit(bool val) { Set<HitFlag>(val); }
// For setting the hit bit
static constexpr uint8_t kHitBitShift = 2U * kCounterNumBits;
static constexpr uint64_t kHitBitMask = uint64_t{1} << kHitBitShift;
// Some distinct states for the various state flags
bool IsEmpty() const {
bool rv = !Get<OccupiedFlag>();
if (rv) {
assert(!Get<ShareableFlag>());
assert(!Get<VisibleFlag>());
}
return rv;
}
// For reading or updating the state marker in meta word
static constexpr uint8_t kStateShift = kHitBitShift + 1;
bool IsUnderConstruction() const {
bool rv = Get<OccupiedFlag>() && !Get<ShareableFlag>();
if (rv) {
assert(!Get<VisibleFlag>());
}
return rv;
}
void SetUnderConstruction() {
Set<OccupiedFlag>(true);
Set<ShareableFlag>(false);
Set<VisibleFlag>(false);
}
// Bits contribution to state marker.
// Occupied means any state other than empty
static constexpr uint8_t kStateOccupiedBit = 0b100;
// Shareable means the entry is reference counted (visible or invisible)
// (only set if also occupied)
static constexpr uint8_t kStateShareableBit = 0b010;
// Visible is only set if also shareable
static constexpr uint8_t kStateVisibleBit = 0b001;
bool IsShareable() const { return Get<ShareableFlag>(); }
bool IsInvisible() const {
bool rv = Get<ShareableFlag>() && !Get<VisibleFlag>();
if (rv) {
assert(Get<OccupiedFlag>());
}
return rv;
}
void SetInvisible() {
Set<OccupiedFlag>(true);
Set<ShareableFlag>(true);
Set<VisibleFlag>(false);
}
// Complete state markers (not shifted into full word)
static constexpr uint8_t kStateEmpty = 0b000;
static constexpr uint8_t kStateConstruction = kStateOccupiedBit;
static constexpr uint8_t kStateInvisible =
kStateOccupiedBit | kStateShareableBit;
static constexpr uint8_t kStateVisible =
kStateOccupiedBit | kStateShareableBit | kStateVisibleBit;
bool IsVisible() const {
bool rv = Get<ShareableFlag>() && Get<VisibleFlag>();
if (rv) {
assert(Get<OccupiedFlag>());
}
return rv;
}
void SetVisible() {
Set<OccupiedFlag>(true);
Set<ShareableFlag>(true);
Set<VisibleFlag>(true);
}
};
// Constants for initializing the countdown clock. (Countdown clock is only
// in effect with zero refs, acquire counter == release counter, and in that
@@ -364,7 +413,7 @@ struct ClockHandle : public ClockHandleBasicData {
// TODO: make these coundown values tuning parameters for eviction?
// See above. Mutable for read reference counting.
mutable AcqRelAtomic<uint64_t> meta{};
mutable BitFieldsAtomic<SlotMeta> meta{};
}; // struct ClockHandle
class BaseClockTable {
@@ -431,9 +480,9 @@ class BaseClockTable {
bool IsEvictionEffortExceeded(const BaseClockTable::EvictionData& data) const;
#ifndef NDEBUG
// Acquire N references
void TEST_RefN(ClockHandle& handle, size_t n);
void TEST_RefN(ClockHandle& handle, uint32_t n);
// Helper for TEST_ReleaseN
void TEST_ReleaseNMinus1(ClockHandle* handle, size_t n);
void TEST_ReleaseNMinus1(ClockHandle* handle, uint32_t n);
#endif
private: // fns
@@ -489,13 +538,13 @@ class BaseClockTable {
// TODO: is this separation needed if we don't do background evictions?
ALIGN_AS(CACHE_LINE_SIZE)
// Number of elements in the table.
AcqRelAtomic<size_t> occupancy_{};
Atomic<size_t> occupancy_{};
// Memory usage by entries tracked by the cache (including standalone)
AcqRelAtomic<size_t> usage_{};
Atomic<size_t> usage_{};
// Part of usage by standalone entries (not in table)
AcqRelAtomic<size_t> standalone_usage_{};
Atomic<size_t> standalone_usage_{};
// Maximum total charge of all elements stored in the table.
// (Relaxed: eventual consistency/update is OK)
@@ -586,7 +635,7 @@ class FixedHyperClockTable : public BaseClockTable {
bool GrowIfNeeded(size_t new_occupancy, InsertState& state);
HandleImpl* DoInsert(const ClockHandleBasicData& proto,
uint64_t initial_countdown, bool take_ref,
uint32_t initial_countdown, bool take_ref,
InsertState& state);
// Runs the clock eviction algorithm trying to reclaim at least
@@ -614,7 +663,7 @@ class FixedHyperClockTable : public BaseClockTable {
}
// Release N references
void TEST_ReleaseN(HandleImpl* handle, size_t n);
void TEST_ReleaseN(HandleImpl* handle, uint32_t n);
#endif
// The load factor p is a real number in (0, 1) such that at all
@@ -850,8 +899,8 @@ class AutoHyperClockTable : public BaseClockTable {
// See above. The head pointer is logically independent of the rest of
// the entry, including the chain next pointer.
AcqRelBitFieldsAtomic<NextWithShift> head_next_with_shift{kUnusedMarker};
AcqRelBitFieldsAtomic<NextWithShift> chain_next_with_shift{kUnusedMarker};
BitFieldsAtomic<NextWithShift> head_next_with_shift{kUnusedMarker};
BitFieldsAtomic<NextWithShift> chain_next_with_shift{kUnusedMarker};
// For supporting CreateStandalone and some fallback cases.
inline bool IsStandalone() const {
@@ -897,7 +946,7 @@ class AutoHyperClockTable : public BaseClockTable {
bool GrowIfNeeded(size_t new_occupancy, InsertState& state);
HandleImpl* DoInsert(const ClockHandleBasicData& proto,
uint64_t initial_countdown, bool take_ref,
uint32_t initial_countdown, bool take_ref,
InsertState& state);
// Runs the clock eviction algorithm trying to reclaim at least
@@ -925,7 +974,7 @@ class AutoHyperClockTable : public BaseClockTable {
}
// Release N references
void TEST_ReleaseN(HandleImpl* handle, size_t n);
void TEST_ReleaseN(HandleImpl* handle, uint32_t n);
#endif
// Maximum ratio of number of occupied slots to number of usable slots. The
@@ -1007,7 +1056,7 @@ class AutoHyperClockTable : public BaseClockTable {
// To maximize parallelization of Grow() operations, this field is only
// updated opportunistically after Grow() operations and in DoInsert() where
// it is found to be out-of-date. See CatchUpLengthInfoNoWait().
AcqRelAtomic<uint64_t> length_info_;
Atomic<uint64_t> length_info_;
// An already-computed version of the usable length times the max load
// factor. Could be slightly out of date but GrowIfNeeded()/Grow() handle
@@ -1130,8 +1179,8 @@ class ALIGN_AS(CACHE_LINE_SIZE) ClockCacheShard final : public CacheShardBase {
return table_.TEST_MutableOccupancyLimit();
}
// Acquire/release N references
void TEST_RefN(HandleImpl* handle, size_t n);
void TEST_ReleaseN(HandleImpl* handle, size_t n);
void TEST_RefN(HandleImpl* handle, uint32_t n);
void TEST_ReleaseN(HandleImpl* handle, uint32_t n);
#endif
private: // data
+7 -7
View File
@@ -1503,7 +1503,7 @@ TEST_P(BasicSecondaryCacheTest, FullCapacityTest) {
/*context*/ this, Cache::Priority::LOW);
ASSERT_EQ(handle1, nullptr);
// k1 promotion can fail with strict_capacit_limit=true, but Lookup still
// k1 promotion can fail with strict_capacity_limit=true, but Lookup still
// succeeds using a standalone handle
handle1 = cache->Lookup(k1.AsSlice(), GetHelper(),
/*context*/ this, Cache::Priority::LOW);
@@ -1680,7 +1680,7 @@ TEST_P(DBSecondaryCacheTest, TestSecondaryCacheCorrectness2) {
// After Flush is successful, RocksDB will do the paranoid check for the new
// SST file. Meta blocks are always cached in the block cache and they
// will not be evicted. When block_2 is cache miss and read out, it is
// inserted to the block cache. Thefore, block_1 is evicted from block
// inserted to the block cache. Therefore, block_1 is evicted from block
// cache and successfully inserted to the secondary cache. Here are 2
// lookups in the secondary cache for block_1 and block_2.
ASSERT_EQ(secondary_cache->num_inserts(), 1u);
@@ -1721,7 +1721,7 @@ TEST_P(DBSecondaryCacheTest, TestSecondaryCacheCorrectness2) {
v = Get(Key(0));
ASSERT_EQ(1007, v.size());
// This Get needs to access block_1, since block_1 is not in block cache
// there is one econdary cache lookup. Then, block_1 is cached in the
// there is one secondary cache lookup. Then, block_1 is cached in the
// block cache.
ASSERT_EQ(secondary_cache->num_inserts(), 2u);
ASSERT_EQ(secondary_cache->num_lookups(), 5u);
@@ -1785,7 +1785,7 @@ TEST_P(DBSecondaryCacheTest, NoSecondaryCacheInsertion) {
std::string v = Get(Key(0));
ASSERT_EQ(1000, v.size());
// Since the block cache is large enough, all the blocks are cached. we
// do not need to lookup the seondary cache.
// do not need to lookup the secondary cache.
ASSERT_EQ(secondary_cache->num_inserts(), 0u);
ASSERT_EQ(secondary_cache->num_lookups(), 2u);
@@ -2150,7 +2150,7 @@ TEST_P(DBSecondaryCacheTest, LRUCacheDumpLoadBasic) {
ASSERT_OK(Flush());
Compact("a", "z");
// do th eread for all the key value pairs, so all the blocks should be in
// do the read for all the key value pairs, so all the blocks should be in
// cache
uint32_t start_insert = cache->GetInsertCount();
uint32_t start_lookup = cache->GetLookupcount();
@@ -2464,7 +2464,7 @@ TEST_P(DBSecondaryCacheTest, TestSecondaryCacheOptionBasic) {
std::string v = Get(Key(0));
ASSERT_EQ(1007, v.size());
// Check the data in first block. Cache miss, direclty read from SST file.
// Check the data in first block. Cache miss, directly read from SST file.
ASSERT_EQ(secondary_cache->num_inserts(), 0u);
ASSERT_EQ(secondary_cache->num_lookups(), 0u);
@@ -2598,7 +2598,7 @@ TEST_P(DBSecondaryCacheTest, TestSecondaryCacheOptionChange) {
}
// Two DB test. We create 2 DBs sharing the same block cache and secondary
// cache. We diable the secondary cache option for DB2.
// cache. We disable the secondary cache option for DB2.
TEST_P(DBSecondaryCacheTest, TestSecondaryCacheOptionTwoDB) {
if (IsHyperClock()) {
ROCKSDB_GTEST_BYPASS("Test depends on LRUCache-specific behaviors");
+5 -1
View File
@@ -8,7 +8,7 @@ DB_STRESS_CMD?=./db_stress
include common.mk
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)'
CRASHTEST_PY=$(PYTHON) -u tools/db_crashtest.py --stress_cmd=$(DB_STRESS_CMD) --cleanup_cmd='$(DB_CLEANUP_CMD)' --destroy_db_initially=1
.PHONY: crash_test crash_test_with_atomic_flush crash_test_with_txn \
crash_test_with_wc_txn crash_test_with_wp_txn crash_test_with_wup_txn \
@@ -34,6 +34,7 @@ CRASHTEST_PY=$(PYTHON) -u tools/db_crashtest.py --stress_cmd=$(DB_STRESS_CMD) --
whitebox_crash_test_with_txn whitebox_crash_test_with_ts \
whitebox_crash_test_with_optimistic_txn \
whitebox_crash_test_with_tiered_storage \
crash_test_db_cleanup \
crash_test: $(DB_STRESS_CMD)
# Do not parallelize
@@ -161,6 +162,9 @@ whitebox_crash_test_with_optimistic_txn: $(DB_STRESS_CMD)
$(CRASHTEST_PY) --optimistic_txn whitebox --random_kill_odd \
$(CRASH_TEST_KILL_ODD) $(CRASH_TEST_EXT_ARGS)
crash_test_db_cleanup: $(DB_STRESS_CMD)
$(DB_STRESS_CMD) --delete_dir_and_exit=$(TEST_TMPDIR)
# Old names DEPRECATED
crash_test_with_txn: crash_test_with_wc_txn
whitebox_crash_test_with_txn: whitebox_crash_test_with_wc_txn
+23 -22
View File
@@ -67,6 +67,16 @@ BlobFileBuilder::BlobFileBuilder(
min_blob_size_(mutable_cf_options->min_blob_size),
blob_file_size_(mutable_cf_options->blob_file_size),
blob_compression_type_(mutable_cf_options->blob_compression_type),
// TODO: support most CompressionOptions with a new CF option
// blob_compression_opts
// TODO with schema change: support custom compression manager and options
// such as max_compressed_bytes_per_kb
// NOTE: returns nullptr for kNoCompression
blob_compressor_(GetBuiltinV2CompressionManager()->GetCompressor(
CompressionOptions{}, blob_compression_type_)),
blob_compressor_wa_(blob_compressor_
? blob_compressor_->ObtainWorkingArea()
: Compressor::ManagedWorkingArea{}),
prepopulate_blob_cache_(mutable_cf_options->prepopulate_blob_cache),
file_options_(file_options),
write_options_(write_options),
@@ -113,7 +123,7 @@ Status BlobFileBuilder::Add(const Slice& key, const Slice& value,
}
Slice blob = value;
std::string compressed_blob;
GrowableBuffer compressed_blob;
{
const Status s = CompressBlobIfNeeded(&blob, &compressed_blob);
@@ -254,36 +264,27 @@ Status BlobFileBuilder::OpenBlobFileIfNeeded() {
}
Status BlobFileBuilder::CompressBlobIfNeeded(
Slice* blob, std::string* compressed_blob) const {
Slice* blob, GrowableBuffer* compressed_blob) const {
assert(blob);
assert(compressed_blob);
assert(compressed_blob->empty());
assert(immutable_options_);
if (blob_compression_type_ == kNoCompression) {
if (!blob_compressor_) {
assert(blob_compression_type_ == kNoCompression);
return Status::OK();
}
assert(blob_compression_type_ != kNoCompression);
// TODO: allow user CompressionOptions, including max_compressed_bytes_per_kb
CompressionOptions opts;
CompressionContext context(blob_compression_type_, opts);
// WART: always stored as compressed even when that increases the size.
CompressionInfo info(opts, context, CompressionDict::GetEmptyDict(),
blob_compression_type_);
constexpr uint32_t compression_format_version = 2;
bool success = false;
{
StopWatch stop_watch(immutable_options_->clock, immutable_options_->stats,
BLOB_DB_COMPRESSION_MICROS);
success = OLD_CompressData(*blob, info, compression_format_version,
compressed_blob);
}
if (!success) {
return Status::Corruption("Error compressing blob");
Status s;
StopWatch stop_watch(immutable_options_->clock, immutable_options_->stats,
BLOB_DB_COMPRESSION_MICROS);
s = LegacyForceBuiltinCompression(*blob_compressor_, &blob_compressor_wa_,
*blob, compressed_blob);
if (!s.ok()) {
return s;
}
*blob = Slice(*compressed_blob);
+6 -1
View File
@@ -10,12 +10,14 @@
#include <string>
#include <vector>
#include "rocksdb/advanced_compression.h"
#include "rocksdb/advanced_options.h"
#include "rocksdb/compression_type.h"
#include "rocksdb/env.h"
#include "rocksdb/options.h"
#include "rocksdb/rocksdb_namespace.h"
#include "rocksdb/types.h"
#include "util/aligned_buffer.h"
namespace ROCKSDB_NAMESPACE {
@@ -76,7 +78,8 @@ class BlobFileBuilder {
private:
bool IsBlobFileOpen() const;
Status OpenBlobFileIfNeeded();
Status CompressBlobIfNeeded(Slice* blob, std::string* compressed_blob) const;
Status CompressBlobIfNeeded(Slice* blob,
GrowableBuffer* compressed_blob) const;
Status WriteBlobToFile(const Slice& key, const Slice& blob,
uint64_t* blob_file_number, uint64_t* blob_offset);
Status CloseBlobFile();
@@ -91,6 +94,8 @@ class BlobFileBuilder {
uint64_t min_blob_size_;
uint64_t blob_file_size_;
CompressionType blob_compression_type_;
std::unique_ptr<Compressor> blob_compressor_;
mutable Compressor::ManagedWorkingArea blob_compressor_wa_;
PrepopulateBlobCache prepopulate_blob_cache_;
const FileOptions* file_options_;
const WriteOptions* write_options_;
+7 -6
View File
@@ -457,11 +457,12 @@ TEST_F(BlobFileBuilderTest, CompressionError) {
nullptr /*IOTracer*/, nullptr /*BlobFileCompletionCallback*/,
BlobFileCreationReason::kFlush, &blob_file_paths, &blob_file_additions);
SyncPoint::GetInstance()->SetCallBack("CompressData:TamperWithReturnValue",
[](void* arg) {
bool* ret = static_cast<bool*>(arg);
*ret = false;
});
SyncPoint::GetInstance()->SetCallBack(
"LegacyForceBuiltinCompression:TamperWithStatus", [](void* arg) {
Status* ret = static_cast<Status*>(arg);
ASSERT_OK(*ret);
*ret = Status::Corruption("Tampered result");
});
SyncPoint::GetInstance()->EnableProcessing();
constexpr char key[] = "1";
@@ -469,7 +470,7 @@ TEST_F(BlobFileBuilderTest, CompressionError) {
std::string blob_index;
ASSERT_TRUE(builder.Add(key, value, &blob_index).IsCorruption());
ASSERT_EQ(builder.Add(key, value, &blob_index).code(), Status::kCorruption);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
+37 -25
View File
@@ -17,10 +17,10 @@
#include "rocksdb/file_system.h"
#include "rocksdb/slice.h"
#include "rocksdb/status.h"
#include "table/format.h"
#include "table/multiget_context.h"
#include "test_util/sync_point.h"
#include "util/compression.h"
#include "util/crc32c.h"
#include "util/stop_watch.h"
namespace ROCKSDB_NAMESPACE {
@@ -69,9 +69,16 @@ Status BlobFileReader::Create(
}
}
blob_file_reader->reset(
new BlobFileReader(std::move(file_reader), file_size, compression_type,
immutable_options.clock, statistics));
std::shared_ptr<Decompressor> decompressor;
if (compression_type != kNoCompression) {
// The blob format has always used compression format 2
decompressor = GetBuiltinV2CompressionManager()->GetDecompressorOptimizeFor(
compression_type);
}
blob_file_reader->reset(new BlobFileReader(
std::move(file_reader), file_size, compression_type,
std::move(decompressor), immutable_options.clock, statistics));
return Status::OK();
}
@@ -282,11 +289,13 @@ Status BlobFileReader::ReadFromFile(const RandomAccessFileReader* file_reader,
BlobFileReader::BlobFileReader(
std::unique_ptr<RandomAccessFileReader>&& file_reader, uint64_t file_size,
CompressionType compression_type, SystemClock* clock,
CompressionType compression_type,
std::shared_ptr<Decompressor> decompressor, SystemClock* clock,
Statistics* statistics)
: file_reader_(std::move(file_reader)),
file_size_(file_size),
compression_type_(compression_type),
decompressor_(std::move(decompressor)),
clock_(clock),
statistics_(statistics) {
assert(file_reader_);
@@ -375,8 +384,9 @@ Status BlobFileReader::GetBlob(
const Slice value_slice(record_slice.data() + adjustment, value_size);
{
const Status s = UncompressBlobIfNeeded(
value_slice, compression_type, allocator, clock_, statistics_, result);
const Status s = UncompressBlobIfNeeded(value_slice, compression_type,
decompressor_.get(), allocator,
clock_, statistics_, result);
if (!s.ok()) {
return s;
}
@@ -524,9 +534,9 @@ void BlobFileReader::MultiGetBlob(
// Uncompress blob if needed
Slice value_slice(record_slice.data() + adjustments[i], req->len);
*req->status =
UncompressBlobIfNeeded(value_slice, compression_type_, allocator,
clock_, statistics_, &blob_reqs[i].second);
*req->status = UncompressBlobIfNeeded(
value_slice, compression_type_, decompressor_.get(), allocator, clock_,
statistics_, &blob_reqs[i].second);
if (req->status->ok()) {
total_bytes += record_slice.size();
}
@@ -583,8 +593,8 @@ Status BlobFileReader::VerifyBlob(const Slice& record_slice,
Status BlobFileReader::UncompressBlobIfNeeded(
const Slice& value_slice, CompressionType compression_type,
MemoryAllocator* allocator, SystemClock* clock, Statistics* statistics,
std::unique_ptr<BlobContents>* result) {
Decompressor* decompressor, MemoryAllocator* allocator, SystemClock* clock,
Statistics* statistics, std::unique_ptr<BlobContents>* result) {
assert(result);
if (compression_type == kNoCompression) {
@@ -593,31 +603,33 @@ Status BlobFileReader::UncompressBlobIfNeeded(
return Status::OK();
}
UncompressionContext context(compression_type);
UncompressionInfo info(context, UncompressionDict::GetEmptyDict(),
compression_type);
assert(decompressor);
size_t uncompressed_size = 0;
constexpr uint32_t compression_format_version = 2;
Decompressor::Args args;
args.compression_type = compression_type;
args.compressed_data = value_slice;
CacheAllocationPtr output;
Status s = decompressor->ExtractUncompressedSize(args);
if (!s.ok()) {
return Status::Corruption(s.ToString());
}
CacheAllocationPtr output = AllocateBlock(args.uncompressed_size, allocator);
{
PERF_TIMER_GUARD(blob_decompress_time);
StopWatch stop_watch(clock, statistics, BLOB_DB_DECOMPRESSION_MICROS);
output = OLD_UncompressData(info, value_slice.data(), value_slice.size(),
&uncompressed_size, compression_format_version,
allocator);
s = decompressor->DecompressBlock(args, output.get());
}
TEST_SYNC_POINT_CALLBACK(
"BlobFileReader::UncompressBlobIfNeeded:TamperWithResult", &output);
"BlobFileReader::UncompressBlobIfNeeded:TamperWithResult", &s);
if (!output) {
return Status::Corruption("Unable to uncompress blob");
if (!s.ok()) {
return Status::Corruption(s.ToString());
}
result->reset(new BlobContents(std::move(output), uncompressed_size));
result->reset(new BlobContents(std::move(output), args.uncompressed_size));
return Status::OK();
}
+5 -1
View File
@@ -10,6 +10,7 @@
#include "db/blob/blob_read_request.h"
#include "file/random_access_file_reader.h"
#include "rocksdb/advanced_compression.h"
#include "rocksdb/compression_type.h"
#include "rocksdb/rocksdb_namespace.h"
#include "util/autovector.h"
@@ -64,7 +65,8 @@ class BlobFileReader {
private:
BlobFileReader(std::unique_ptr<RandomAccessFileReader>&& file_reader,
uint64_t file_size, CompressionType compression_type,
SystemClock* clock, Statistics* statistics);
std::shared_ptr<Decompressor> decompressor, SystemClock* clock,
Statistics* statistics);
static Status OpenFile(const ImmutableOptions& immutable_options,
const FileOptions& file_opts,
@@ -96,6 +98,7 @@ class BlobFileReader {
static Status UncompressBlobIfNeeded(const Slice& value_slice,
CompressionType compression_type,
Decompressor* decompressor,
MemoryAllocator* allocator,
SystemClock* clock,
Statistics* statistics,
@@ -104,6 +107,7 @@ class BlobFileReader {
std::unique_ptr<RandomAccessFileReader> file_reader_;
uint64_t file_size_;
CompressionType compression_type_;
std::shared_ptr<Decompressor> decompressor_;
SystemClock* clock_;
Statistics* statistics_;
};
+15 -18
View File
@@ -65,7 +65,7 @@ void WriteBlobFile(const ImmutableOptions& immutable_options,
ASSERT_OK(blob_log_writer.WriteHeader(WriteOptions(), header));
std::vector<std::string> compressed_blobs(num);
std::vector<GrowableBuffer> compressed_blobs(num);
std::vector<Slice> blobs_to_write(num);
if (kNoCompression == compression) {
for (size_t i = 0; i < num; ++i) {
@@ -73,16 +73,13 @@ void WriteBlobFile(const ImmutableOptions& immutable_options,
blob_sizes[i] = blobs[i].size();
}
} else {
CompressionOptions opts;
CompressionContext context(compression, opts);
CompressionInfo info(opts, context, CompressionDict::GetEmptyDict(),
compression);
constexpr uint32_t compression_format_version = 2;
auto compressor =
GetBuiltinV2CompressionManager()->GetCompressor({}, compression);
for (size_t i = 0; i < num; ++i) {
ASSERT_TRUE(OLD_CompressData(blobs[i], info, compression_format_version,
&compressed_blobs[i]));
ASSERT_OK(LegacyForceBuiltinCompression(*compressor,
/*working_area=*/nullptr,
blobs[i], &compressed_blobs[i]));
blobs_to_write[i] = compressed_blobs[i];
blob_sizes[i] = compressed_blobs[i].size();
}
@@ -809,11 +806,10 @@ TEST_F(BlobFileReaderTest, UncompressionError) {
SyncPoint::GetInstance()->SetCallBack(
"BlobFileReader::UncompressBlobIfNeeded:TamperWithResult", [](void* arg) {
CacheAllocationPtr* const output =
static_cast<CacheAllocationPtr*>(arg);
assert(output);
auto* result = static_cast<Status*>(arg);
assert(result);
output->reset();
*result = Status::Corruption("Injected result");
});
SyncPoint::GetInstance()->EnableProcessing();
@@ -824,11 +820,12 @@ TEST_F(BlobFileReaderTest, UncompressionError) {
std::unique_ptr<BlobContents> value;
uint64_t bytes_read = 0;
ASSERT_TRUE(reader
->GetBlob(ReadOptions(), key, blob_offset, blob_size,
kSnappyCompression, prefetch_buffer, allocator,
&value, &bytes_read)
.IsCorruption());
ASSERT_EQ(reader
->GetBlob(ReadOptions(), key, blob_offset, blob_size,
kSnappyCompression, prefetch_buffer, allocator,
&value, &bytes_read)
.code(),
Status::Code::kCorruption);
ASSERT_EQ(value, nullptr);
ASSERT_EQ(bytes_read, 0);
+6 -9
View File
@@ -67,7 +67,7 @@ void WriteBlobFile(const ImmutableOptions& immutable_options,
ASSERT_OK(blob_log_writer.WriteHeader(WriteOptions(), header));
std::vector<std::string> compressed_blobs(num);
std::vector<GrowableBuffer> compressed_blobs(num);
std::vector<Slice> blobs_to_write(num);
if (kNoCompression == compression) {
for (size_t i = 0; i < num; ++i) {
@@ -75,16 +75,13 @@ void WriteBlobFile(const ImmutableOptions& immutable_options,
blob_sizes[i] = blobs[i].size();
}
} else {
CompressionOptions opts;
CompressionContext context(compression, opts);
CompressionInfo info(opts, context, CompressionDict::GetEmptyDict(),
compression);
constexpr uint32_t compression_format_version = 2;
auto compressor =
GetBuiltinV2CompressionManager()->GetCompressor({}, compression);
for (size_t i = 0; i < num; ++i) {
ASSERT_TRUE(OLD_CompressData(blobs[i], info, compression_format_version,
&compressed_blobs[i]));
ASSERT_OK(LegacyForceBuiltinCompression(*compressor,
/*working_area=*/nullptr,
blobs[i], &compressed_blobs[i]));
blobs_to_write[i] = compressed_blobs[i];
blob_sizes[i] = compressed_blobs[i].size();
}
+13
View File
@@ -56,6 +56,18 @@ TableBuilder* NewTableBuilder(const TableBuilderOptions& tboptions,
return tboptions.moptions.table_factory->NewTableBuilder(tboptions, file);
}
void ExtractTimestampFromTableProperties(const TableProperties& tp,
FileMetaData* meta) {
auto min_ts_iter = tp.user_collected_properties.find("rocksdb.timestamp_min");
if (min_ts_iter != tp.user_collected_properties.end()) {
meta->min_timestamp = min_ts_iter->second;
}
auto max_ts_iter = tp.user_collected_properties.find("rocksdb.timestamp_max");
if (max_ts_iter != tp.user_collected_properties.end()) {
meta->max_timestamp = max_ts_iter->second;
}
}
Status BuildTable(
const std::string& dbname, VersionSet* versions,
const ImmutableDBOptions& db_options, const TableBuilderOptions& tboptions,
@@ -355,6 +367,7 @@ Status BuildTable(
assert(meta->fd.GetFileSize() > 0);
tp = builder
->GetTableProperties(); // refresh now that builder is finished
ExtractTimestampFromTableProperties(tp, meta);
if (memtable_payload_bytes != nullptr &&
memtable_garbage_bytes != nullptr) {
const CompactionIterationStats& ci_stats = c_iter.iter_stats();
+7
View File
@@ -41,6 +41,13 @@ class BlobFileCompletionCallback;
TableBuilder* NewTableBuilder(const TableBuilderOptions& tboptions,
WritableFileWriter* file);
// Extract min/max timestamps from table properties and populate FileMetaData.
// This is used by both flush (BuildTable) and compaction (CompactionOutputs)
// to populate timestamp range in FileMetaData from the TimestampTableProperties
// collector output.
void ExtractTimestampFromTableProperties(const TableProperties& tp,
FileMetaData* meta);
// Build a Table file from the contents of *iter. The generated file
// will be named according to number specified in meta. On success, the rest of
// *meta will be filled with metadata about the generated table.
+224
View File
@@ -89,10 +89,12 @@ using ROCKSDB_NAMESPACE::EnvOptions;
using ROCKSDB_NAMESPACE::EventListener;
using ROCKSDB_NAMESPACE::ExportImportFilesMetaData;
using ROCKSDB_NAMESPACE::ExternalFileIngestionInfo;
using ROCKSDB_NAMESPACE::FileChecksumGenFactory;
using ROCKSDB_NAMESPACE::FileLock;
using ROCKSDB_NAMESPACE::FilterPolicy;
using ROCKSDB_NAMESPACE::FlushJobInfo;
using ROCKSDB_NAMESPACE::FlushOptions;
using ROCKSDB_NAMESPACE::GetFileChecksumGenCrc32cFactory;
using ROCKSDB_NAMESPACE::HistogramData;
using ROCKSDB_NAMESPACE::HyperClockCacheOptions;
using ROCKSDB_NAMESPACE::ImportColumnFamilyOptions;
@@ -112,6 +114,7 @@ using ROCKSDB_NAMESPACE::NewCompactOnDeletionCollectorFactory;
using ROCKSDB_NAMESPACE::NewGenericRateLimiter;
using ROCKSDB_NAMESPACE::NewLRUCache;
using ROCKSDB_NAMESPACE::NewRibbonFilterPolicy;
using ROCKSDB_NAMESPACE::NewSstPartitionerFixedPrefixFactory;
using ROCKSDB_NAMESPACE::OpenAndCompactOptions;
using ROCKSDB_NAMESPACE::OptimisticTransactionDB;
using ROCKSDB_NAMESPACE::OptimisticTransactionOptions;
@@ -133,9 +136,11 @@ using ROCKSDB_NAMESPACE::Snapshot;
using ROCKSDB_NAMESPACE::SstFileManager;
using ROCKSDB_NAMESPACE::SstFileMetaData;
using ROCKSDB_NAMESPACE::SstFileWriter;
using ROCKSDB_NAMESPACE::SstPartitionerFactory;
using ROCKSDB_NAMESPACE::Status;
using ROCKSDB_NAMESPACE::StderrLogger;
using ROCKSDB_NAMESPACE::SubcompactionJobInfo;
using ROCKSDB_NAMESPACE::TableFactory;
using ROCKSDB_NAMESPACE::TablePropertiesCollectorFactory;
using ROCKSDB_NAMESPACE::Transaction;
using ROCKSDB_NAMESPACE::TransactionDB;
@@ -235,6 +240,15 @@ struct rocksdb_filelock_t {
struct rocksdb_logger_t {
std::shared_ptr<Logger> rep;
};
struct rocksdb_file_checksum_gen_factory_t {
std::shared_ptr<FileChecksumGenFactory> rep;
};
struct rocksdb_sst_partitioner_factory_t {
std::shared_ptr<SstPartitionerFactory> rep;
};
struct rocksdb_table_properties_collector_factory_t {
std::shared_ptr<TablePropertiesCollectorFactory> rep;
};
struct rocksdb_lru_cache_options_t {
LRUCacheOptions rep;
};
@@ -920,6 +934,38 @@ rocksdb_compaction_service_options_override_create() {
return new rocksdb_compaction_service_options_override_t;
}
rocksdb_compaction_service_options_override_t*
rocksdb_compaction_service_options_override_create_from_options(
rocksdb_options_t* options) {
if (!options) {
return nullptr;
}
rocksdb_compaction_service_options_override_t* override_opts =
new rocksdb_compaction_service_options_override_t;
// Copy all relevant options from rocksdb_options_t
override_opts->rep.env = options->rep.env;
override_opts->rep.file_checksum_gen_factory =
options->rep.file_checksum_gen_factory;
override_opts->rep.comparator = options->rep.comparator;
override_opts->rep.merge_operator = options->rep.merge_operator;
override_opts->rep.compaction_filter = options->rep.compaction_filter;
override_opts->rep.compaction_filter_factory =
options->rep.compaction_filter_factory;
override_opts->rep.prefix_extractor = options->rep.prefix_extractor;
override_opts->rep.table_factory = options->rep.table_factory;
override_opts->rep.sst_partitioner_factory =
options->rep.sst_partitioner_factory;
override_opts->rep.listeners = options->rep.listeners;
override_opts->rep.statistics = options->rep.statistics;
override_opts->rep.info_log = options->rep.info_log;
override_opts->rep.table_properties_collector_factories =
options->rep.table_properties_collector_factories;
return override_opts;
}
void rocksdb_compaction_service_options_override_destroy(
rocksdb_compaction_service_options_override_t* override_options) {
if (override_options) {
@@ -943,6 +989,111 @@ void rocksdb_compaction_service_options_override_set_comparator(
}
}
void rocksdb_compaction_service_options_override_set_merge_operator(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_mergeoperator_t* merge_operator) {
if (override_options && merge_operator) {
override_options->rep.merge_operator =
std::shared_ptr<MergeOperator>(merge_operator);
}
}
void rocksdb_compaction_service_options_override_set_compaction_filter(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_compactionfilter_t* compaction_filter) {
if (override_options && compaction_filter) {
override_options->rep.compaction_filter = compaction_filter;
}
}
void rocksdb_compaction_service_options_override_set_compaction_filter_factory(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_compactionfilterfactory_t* compaction_filter_factory) {
if (override_options && compaction_filter_factory) {
override_options->rep.compaction_filter_factory =
std::shared_ptr<CompactionFilterFactory>(compaction_filter_factory);
}
}
void rocksdb_compaction_service_options_override_set_prefix_extractor(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_slicetransform_t* prefix_extractor) {
if (override_options && prefix_extractor) {
override_options->rep.prefix_extractor =
std::shared_ptr<const SliceTransform>(prefix_extractor);
}
}
void rocksdb_compaction_service_options_override_set_block_based_table_factory(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_block_based_table_options_t* table_options) {
if (override_options && table_options) {
override_options->rep.table_factory = std::shared_ptr<TableFactory>(
NewBlockBasedTableFactory(table_options->rep));
}
}
void rocksdb_compaction_service_options_override_set_cuckoo_table_factory(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_cuckoo_table_options_t* table_options) {
if (override_options && table_options) {
override_options->rep.table_factory = std::shared_ptr<TableFactory>(
NewCuckooTableFactory(table_options->rep));
}
}
// Note: add_event_listener is defined later after rocksdb_eventlistener_t
// struct
void rocksdb_compaction_service_options_override_set_statistics(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_options_t* options) {
if (override_options && options) {
override_options->rep.statistics = options->rep.statistics;
}
}
void rocksdb_compaction_service_options_override_set_info_log(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_logger_t* logger) {
if (override_options && logger) {
override_options->rep.info_log = logger->rep;
}
}
void rocksdb_compaction_service_options_override_set_option(
rocksdb_compaction_service_options_override_t* override_options,
const char* key, const char* value) {
if (override_options && key && value) {
override_options->rep.options_map[std::string(key)] = std::string(value);
}
}
void rocksdb_compaction_service_options_override_set_file_checksum_gen_factory(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_file_checksum_gen_factory_t* factory) {
if (override_options && factory) {
override_options->rep.file_checksum_gen_factory = factory->rep;
}
}
void rocksdb_compaction_service_options_override_set_sst_partitioner_factory(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_sst_partitioner_factory_t* factory) {
if (override_options && factory) {
override_options->rep.sst_partitioner_factory = factory->rep;
}
}
void rocksdb_compaction_service_options_override_add_table_properties_collector_factory(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_table_properties_collector_factory_t* factory) {
if (override_options && factory) {
override_options->rep.table_properties_collector_factories.push_back(
factory->rep);
}
}
// Atomic bool management for cancellation
unsigned char* rocksdb_open_and_compact_canceled_create() {
return reinterpret_cast<unsigned char*>(new std::atomic<bool>(false));
@@ -3612,6 +3763,11 @@ void rocksdb_block_based_options_set_unpartitioned_pinning_tier(
static_cast<ROCKSDB_NAMESPACE::PinningTier>(v);
}
void rocksdb_block_based_options_set_block_align(
rocksdb_block_based_table_options_t* options, unsigned char v) {
options->rep.block_align = v;
}
/* FlushJobInfo */
const char* rocksdb_flushjobinfo_cf_name(const rocksdb_flushjobinfo_t* info,
@@ -3967,6 +4123,15 @@ void rocksdb_options_add_eventlistener(rocksdb_options_t* opt,
opt->rep.listeners.emplace_back(std::shared_ptr<EventListener>(t));
}
void rocksdb_compaction_service_options_override_add_event_listener(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_eventlistener_t* event_listener) {
if (override_options && event_listener) {
override_options->rep.listeners.emplace_back(
std::shared_ptr<EventListener>(event_listener));
}
}
rocksdb_cuckoo_table_options_t* rocksdb_cuckoo_options_create() {
return new rocksdb_cuckoo_table_options_t;
}
@@ -4178,6 +4343,65 @@ rocksdb_logger_t* rocksdb_logger_create_callback_logger(
void rocksdb_logger_destroy(rocksdb_logger_t* logger) { delete logger; }
/* File Checksum Gen Factory */
rocksdb_file_checksum_gen_factory_t*
rocksdb_file_checksum_gen_crc32c_factory_create() {
rocksdb_file_checksum_gen_factory_t* factory =
new rocksdb_file_checksum_gen_factory_t;
factory->rep = GetFileChecksumGenCrc32cFactory();
return factory;
}
void rocksdb_file_checksum_gen_factory_destroy(
rocksdb_file_checksum_gen_factory_t* factory) {
delete factory;
}
void rocksdb_options_set_file_checksum_gen_factory(
rocksdb_options_t* opt, rocksdb_file_checksum_gen_factory_t* factory) {
if (opt && factory) {
opt->rep.file_checksum_gen_factory = factory->rep;
}
}
/* SST Partitioner Factory */
rocksdb_sst_partitioner_factory_t*
rocksdb_sst_partitioner_fixed_prefix_factory_create(size_t prefix_len) {
rocksdb_sst_partitioner_factory_t* factory =
new rocksdb_sst_partitioner_factory_t;
factory->rep = NewSstPartitionerFixedPrefixFactory(prefix_len);
return factory;
}
void rocksdb_sst_partitioner_factory_destroy(
rocksdb_sst_partitioner_factory_t* factory) {
delete factory;
}
void rocksdb_options_set_sst_partitioner_factory(
rocksdb_options_t* opt, rocksdb_sst_partitioner_factory_t* factory) {
if (opt && factory) {
opt->rep.sst_partitioner_factory = factory->rep;
}
}
/* Table Properties Collector Factory */
void rocksdb_table_properties_collector_factory_destroy(
rocksdb_table_properties_collector_factory_t* factory) {
delete factory;
}
void rocksdb_options_add_table_properties_collector_factory(
rocksdb_options_t* opt,
rocksdb_table_properties_collector_factory_t* factory) {
if (opt && factory) {
opt->rep.table_properties_collector_factories.push_back(factory->rep);
}
}
void rocksdb_options_set_env(rocksdb_options_t* opt, rocksdb_env_t* env) {
opt->rep.env = (env ? env->rep : nullptr);
}
+77
View File
@@ -4717,9 +4717,86 @@ int main(int argc, char** argv) {
rocksdb_compaction_service_options_override_set_comparator(override_opts,
cmp);
// Test file checksum gen factory
rocksdb_file_checksum_gen_factory_t* checksum_factory =
rocksdb_file_checksum_gen_crc32c_factory_create();
CheckCondition(checksum_factory != NULL);
rocksdb_compaction_service_options_override_set_file_checksum_gen_factory(
override_opts, checksum_factory);
// Test SST partitioner factory
rocksdb_sst_partitioner_factory_t* partitioner_factory =
rocksdb_sst_partitioner_fixed_prefix_factory_create(4);
CheckCondition(partitioner_factory != NULL);
rocksdb_compaction_service_options_override_set_sst_partitioner_factory(
override_opts, partitioner_factory);
// Test merge operator
rocksdb_compaction_service_options_override_set_merge_operator(
override_opts, NULL);
// Test compaction filter
rocksdb_compaction_service_options_override_set_compaction_filter(
override_opts, NULL);
// Test prefix extractor
rocksdb_compaction_service_options_override_set_prefix_extractor(
override_opts, NULL);
// Test table factory - block based
rocksdb_block_based_table_options_t* table_opts =
rocksdb_block_based_options_create();
rocksdb_compaction_service_options_override_set_block_based_table_factory(
override_opts, table_opts);
rocksdb_block_based_options_destroy(table_opts);
// Test statistics via options
rocksdb_options_t* stats_opts = rocksdb_options_create();
rocksdb_options_enable_statistics(stats_opts);
rocksdb_compaction_service_options_override_set_statistics(override_opts,
stats_opts);
rocksdb_options_destroy(stats_opts);
// Test info log
rocksdb_logger_t* logger =
rocksdb_logger_create_stderr_logger(1, "test_prefix");
rocksdb_compaction_service_options_override_set_info_log(override_opts,
logger);
rocksdb_logger_destroy(logger);
// Test options map
rocksdb_compaction_service_options_override_set_option(
override_opts, "max_bytes_for_level_base", "67108864");
// Cleanup
rocksdb_file_checksum_gen_factory_destroy(checksum_factory);
rocksdb_sst_partitioner_factory_destroy(partitioner_factory);
rocksdb_compaction_service_options_override_destroy(override_opts);
}
StartPhase("factory_options_on_regular_options");
{
// Test that the new factory types work with regular rocksdb_options_t
rocksdb_options_t* test_opts = rocksdb_options_create();
// Test file checksum gen factory on regular options
rocksdb_file_checksum_gen_factory_t* checksum_factory =
rocksdb_file_checksum_gen_crc32c_factory_create();
CheckCondition(checksum_factory != NULL);
rocksdb_options_set_file_checksum_gen_factory(test_opts, checksum_factory);
// Test SST partitioner factory on regular options
rocksdb_sst_partitioner_factory_t* partitioner_factory =
rocksdb_sst_partitioner_fixed_prefix_factory_create(8);
CheckCondition(partitioner_factory != NULL);
rocksdb_options_set_sst_partitioner_factory(test_opts, partitioner_factory);
// Cleanup
rocksdb_file_checksum_gen_factory_destroy(checksum_factory);
rocksdb_sst_partitioner_factory_destroy(partitioner_factory);
rocksdb_options_destroy(test_opts);
}
StartPhase("remote_compaction_null_callback_handling");
{
// Test that NULL callback returns are handled gracefully
+2 -2
View File
@@ -1248,7 +1248,7 @@ Compaction* ColumnFamilyData::PickCompaction(
auto* result = compaction_picker_->PickCompaction(
GetName(), mutable_options, mutable_db_options, existing_snapshots,
snapshot_checker, current_->storage_info(), log_buffer,
require_max_output_level);
GetFullHistoryTsLow(), require_max_output_level);
if (result != nullptr) {
result->FinalizeInputInfo(current_);
}
@@ -1336,7 +1336,7 @@ Compaction* ColumnFamilyData::CompactRange(
GetName(), mutable_cf_options, mutable_db_options,
current_->storage_info(), input_level, output_level,
compact_range_options, begin, end, compaction_end, conflict,
max_file_num_to_ignore, trim_ts);
max_file_num_to_ignore, trim_ts, GetFullHistoryTsLow());
if (result != nullptr) {
result->FinalizeInputInfo(current_);
}
+3 -2
View File
@@ -433,8 +433,9 @@ TEST_F(CompactFilesTest, SentinelCompressionType) {
ROCKSDB_NAMESPACE::TablePropertiesCollection all_tables_props;
ASSERT_OK(db->GetPropertiesOfAllTables(&all_tables_props));
for (const auto& name_and_table_props : all_tables_props) {
ASSERT_EQ(CompressionTypeToString(CompressionType::kZlibCompression),
name_and_table_props.second->compression_name);
// As of format_version 7, more elaborate information is encoded into the
// compression_name property
ASSERT_EQ("BuiltinV2;02;", name_and_table_props.second->compression_name);
}
delete db;
}
+13 -5
View File
@@ -1310,14 +1310,14 @@ void CompactionIterator::PrepareOutput() {
validity_info_.rep);
assert(false);
}
ikey_.sequence = 0;
last_key_seq_zeroed_ = true;
TEST_SYNC_POINT_CALLBACK("CompactionIterator::PrepareOutput:ZeroingSeq",
&ikey_);
bool zeroed_seqno = false;
if (!timestamp_size_) {
current_key_.UpdateInternalKey(0, ikey_.type);
zeroed_seqno = true;
} else if (full_history_ts_low_ && cmp_with_history_ts_low_ < 0) {
// We can also zero out timestamp for better compression.
// For UDT, the seqno and timestamp could only be zeroed out after the
// key is below history_ts_low_.
// For the same user key (excluding timestamp), the timestamp-based
// history can be collapsed to save some space if the timestamp is
// older than *full_history_ts_low_.
@@ -1325,6 +1325,14 @@ void CompactionIterator::PrepareOutput() {
const Slice ts_slice = kTsMin;
ikey_.SetTimestamp(ts_slice);
current_key_.UpdateInternalKey(0, ikey_.type, &ts_slice);
zeroed_seqno = true;
}
if (zeroed_seqno) {
ikey_.sequence = 0;
last_key_seq_zeroed_ = true;
TEST_SYNC_POINT_CALLBACK("CompactionIterator::PrepareOutput:ZeroingSeq",
&ikey_);
}
}
}
+30 -1
View File
@@ -2438,6 +2438,32 @@ bool CompactionJob::UpdateInternalStatsFromInputFiles(
bool has_error = false;
const ReadOptions read_options(Env::IOActivity::kCompaction);
const auto& input_table_properties = compaction->GetInputTableProperties();
// Check all input files for old block-based SST format_version. Why? Old
// block-based SST files from roughly version 5.0 to 5.18 could produce
// inaccurate num_entries counts due to the evolution of its handling along
// with num_range_deletions. We have to disable some paranoid checks when
// compacting files from such an old release. However, we don't have great
// information to identify those files, so we heuristically over-approximate
// that set of files using
// (a) format_version < 5, which will be true for any files from RocksDB <
// 6.6.0 and should not be true for any recent production files
// (b) to avoid including non-block-based SST files (which still use older
// format_version markers, and do not support DeleteRange), we also require
// the presence of the user property "rocksdb.block.based.table.index.type",
// which was added in RocksDB 2.8 and is always present in block-based tables.
for (const auto& tp_pair : input_table_properties) {
if (tp_pair.second && tp_pair.second->format_version < 5) {
// Check for block-based table by looking for its index type property
const auto& user_props = tp_pair.second->user_collected_properties;
if (user_props.find(BlockBasedTablePropertyNames::kIndexType) !=
user_props.end()) {
job_stats_->has_accurate_num_input_records = false;
break;
}
}
}
for (int input_level = 0;
input_level < static_cast<int>(compaction->num_input_levels());
++input_level) {
@@ -2682,7 +2708,10 @@ Status CompactionJob::ReadTablePropertiesDirectly(
std::shared_ptr<const TableProperties>* tp) {
std::unique_ptr<FSRandomAccessFile> file;
std::string file_name = GetTableFileName(file_meta->fd.GetNumber());
Status s = ioptions.fs->NewRandomAccessFile(file_name, file_options_, &file,
FileOptions fopts = file_options_;
fopts.file_checksum = file_meta->file_checksum;
fopts.file_checksum_func_name = file_meta->file_checksum_func_name;
Status s = ioptions.fs->NewRandomAccessFile(file_name, fopts, &file,
nullptr /* dbg */);
if (!s.ok()) {
return s;
+4
View File
@@ -250,6 +250,10 @@ class CompactionJob {
// @param num_input_range_del if non-null, will be set to the number of range
// deletion entries in this compaction input.
//
// If any input file has potentially unreliable num_entries count (old SST
// files - details in implementation),
// job_stats_->has_accurate_num_input_records is set to false.
//
// Returns true iff internal_stats_.output_level_stats.num_input_records and
// num_input_range_del are calculated successfully.
//
+4 -2
View File
@@ -49,8 +49,10 @@ Status CompactionOutputs::Finish(
meta->fd.file_size = current_bytes;
meta->tail_size = builder_->GetTailSize();
meta->marked_for_compaction = builder_->NeedCompact();
meta->user_defined_timestamps_persisted = static_cast<bool>(
builder_->GetTableProperties().user_defined_timestamps_persisted);
const TableProperties& tp = builder_->GetTableProperties();
meta->user_defined_timestamps_persisted =
static_cast<bool>(tp.user_defined_timestamps_persisted);
ExtractTimestampFromTableProperties(tp, meta);
}
current_output().finished = true;
stats_.bytes_written += current_bytes;
+6 -3
View File
@@ -611,7 +611,8 @@ Compaction* CompactionPicker::PickCompactionForCompactRange(
int input_level, int output_level,
const CompactRangeOptions& compact_range_options, const InternalKey* begin,
const InternalKey* end, InternalKey** compaction_end, bool* manual_conflict,
uint64_t max_file_num_to_ignore, const std::string& trim_ts) {
uint64_t max_file_num_to_ignore, const std::string& trim_ts,
const std::string& full_history_ts_low) {
// CompactionPickerFIFO has its own implementation of compact range
assert(ioptions_.compaction_style != kCompactionStyleFIFO);
@@ -690,7 +691,8 @@ Compaction* CompactionPicker::PickCompactionForCompactRange(
compact_range_options.blob_garbage_collection_age_cutoff);
RegisterCompaction(c);
vstorage->ComputeCompactionScore(ioptions_, mutable_cf_options);
vstorage->ComputeCompactionScore(ioptions_, mutable_cf_options,
full_history_ts_low);
return c;
}
@@ -887,7 +889,8 @@ Compaction* CompactionPicker::PickCompactionForCompactRange(
// takes running compactions into account (by skipping files that are already
// being compacted). Since we just changed compaction score, we recalculate it
// here
vstorage->ComputeCompactionScore(ioptions_, mutable_cf_options);
vstorage->ComputeCompactionScore(ioptions_, mutable_cf_options,
full_history_ts_low);
return compaction;
}
+8 -5
View File
@@ -65,7 +65,8 @@ class CompactionPicker {
const MutableDBOptions& mutable_db_options,
const std::vector<SequenceNumber>& existing_snapshots,
const SnapshotChecker* snapshot_checker, VersionStorageInfo* vstorage,
LogBuffer* log_buffer, bool require_max_output_level) = 0;
LogBuffer* log_buffer, const std::string& full_history_ts_low,
bool require_max_output_level = false) = 0;
// The returned Compaction might not include the whole requested range.
// In that case, compaction_end will be set to the next key that needs
@@ -82,7 +83,8 @@ class CompactionPicker {
const CompactRangeOptions& compact_range_options,
const InternalKey* begin, const InternalKey* end,
InternalKey** compaction_end, bool* manual_conflict,
uint64_t max_file_num_to_ignore, const std::string& trim_ts);
uint64_t max_file_num_to_ignore, const std::string& trim_ts,
const std::string& full_history_ts_low);
// The maximum allowed output level. Default value is NumberLevels() - 1.
virtual int MaxOutputLevel() const { return NumberLevels() - 1; }
@@ -284,7 +286,8 @@ class NullCompactionPicker : public CompactionPicker {
const std::vector<SequenceNumber>& /*existing_snapshots*/,
const SnapshotChecker* /*snapshot_checker*/,
VersionStorageInfo* /*vstorage*/, LogBuffer* /* log_buffer */,
bool /*require_max_output_level*/ = false) override {
const std::string& /*full_history_ts_low*/,
bool /*require_max_output_level*/) override {
return nullptr;
}
@@ -298,8 +301,8 @@ class NullCompactionPicker : public CompactionPicker {
const CompactRangeOptions& /*compact_range_options*/,
const InternalKey* /*begin*/, const InternalKey* /*end*/,
InternalKey** /*compaction_end*/, bool* /*manual_conflict*/,
uint64_t /*max_file_num_to_ignore*/,
const std::string& /*trim_ts*/) override {
uint64_t /*max_file_num_to_ignore*/, const std::string& /*trim_ts*/,
const std::string& /*full_history_ts_low*/) override {
return nullptr;
}
+14 -6
View File
@@ -419,12 +419,19 @@ Compaction* FIFOCompactionPicker::PickTemperatureChangeCompaction(
return c;
}
// The full_history_ts_low parameter is used to control bottommost file marking
// for compaction when user-defined timestamps (UDT) are enabled.
// TODO leverage full_history_ts_low for FIFO compaction, by trigggerring
// compaction early for data that has already expired to achieve the goal of TTL
// enforced compliance.
Compaction* FIFOCompactionPicker::PickCompaction(
const std::string& cf_name, const MutableCFOptions& mutable_cf_options,
const MutableDBOptions& mutable_db_options,
const std::vector<SequenceNumber>& /* existing_snapshots */,
const SnapshotChecker* /* snapshot_checker */, VersionStorageInfo* vstorage,
LogBuffer* log_buffer, bool /* require_max_output_level*/) {
LogBuffer* log_buffer, const std::string& /* full_history_ts_low */,
bool /* require_max_output_level*/) {
Compaction* c = nullptr;
if (mutable_cf_options.ttl > 0) {
c = PickTTLCompaction(cf_name, mutable_cf_options, mutable_db_options,
@@ -449,7 +456,8 @@ Compaction* FIFOCompactionPicker::PickCompactionForCompactRange(
const CompactRangeOptions& /*compact_range_options*/,
const InternalKey* /*begin*/, const InternalKey* /*end*/,
InternalKey** compaction_end, bool* /*manual_conflict*/,
uint64_t /*max_file_num_to_ignore*/, const std::string& /*trim_ts*/) {
uint64_t /*max_file_num_to_ignore*/, const std::string& /*trim_ts*/,
const std::string& full_history_ts_low) {
#ifdef NDEBUG
(void)input_level;
(void)output_level;
@@ -458,10 +466,10 @@ Compaction* FIFOCompactionPicker::PickCompactionForCompactRange(
assert(output_level == 0);
*compaction_end = nullptr;
LogBuffer log_buffer(InfoLogLevel::INFO_LEVEL, ioptions_.logger);
Compaction* c =
PickCompaction(cf_name, mutable_cf_options, mutable_db_options,
/*existing_snapshots*/ {}, /*snapshot_checker*/ nullptr,
vstorage, &log_buffer);
Compaction* c = PickCompaction(
cf_name, mutable_cf_options, mutable_db_options,
/*existing_snapshots*/ {}, /*snapshot_checker*/ nullptr, vstorage,
&log_buffer, full_history_ts_low, /* require_max_output_level */ false);
log_buffer.FlushBufferToLog();
return c;
}
+3 -1
View File
@@ -24,6 +24,7 @@ class FIFOCompactionPicker : public CompactionPicker {
const std::vector<SequenceNumber>& /* existing_snapshots */,
const SnapshotChecker* /* snapshot_checker */,
VersionStorageInfo* version, LogBuffer* log_buffer,
const std::string& /* full_history_ts_low */,
bool /* require_max_output_level*/ = false) override;
Compaction* PickCompactionForCompactRange(
@@ -33,7 +34,8 @@ class FIFOCompactionPicker : public CompactionPicker {
const CompactRangeOptions& compact_range_options,
const InternalKey* begin, const InternalKey* end,
InternalKey** compaction_end, bool* manual_conflict,
uint64_t max_file_num_to_ignore, const std::string& trim_ts) override;
uint64_t max_file_num_to_ignore, const std::string& trim_ts,
const std::string& full_history_ts_low) override;
// The maximum allowed output level. Always returns 0.
int MaxOutputLevel() const override { return 0; }
+10 -5
View File
@@ -61,14 +61,16 @@ class LevelCompactionBuilder {
LogBuffer* log_buffer,
const MutableCFOptions& mutable_cf_options,
const ImmutableOptions& ioptions,
const MutableDBOptions& mutable_db_options)
const MutableDBOptions& mutable_db_options,
const std::string& full_history_ts_low)
: cf_name_(cf_name),
vstorage_(vstorage),
compaction_picker_(compaction_picker),
log_buffer_(log_buffer),
mutable_cf_options_(mutable_cf_options),
ioptions_(ioptions),
mutable_db_options_(mutable_db_options) {}
mutable_db_options_(mutable_db_options),
full_history_ts_low_(full_history_ts_low) {}
// Pick and return a compaction.
Compaction* PickCompaction();
@@ -155,6 +157,7 @@ class LevelCompactionBuilder {
const MutableCFOptions& mutable_cf_options_;
const ImmutableOptions& ioptions_;
const MutableDBOptions& mutable_db_options_;
const std::string& full_history_ts_low_;
// Pick a path ID to place a newly generated file, with its level
static uint32_t GetPathId(const ImmutableCFOptions& ioptions,
const MutableCFOptions& mutable_cf_options,
@@ -571,7 +574,8 @@ Compaction* LevelCompactionBuilder::GetCompaction() {
// takes running compactions into account (by skipping files that are already
// being compacted). Since we just changed compaction score, we recalculate it
// here
vstorage_->ComputeCompactionScore(ioptions_, mutable_cf_options_);
vstorage_->ComputeCompactionScore(ioptions_, mutable_cf_options_,
full_history_ts_low_);
return c;
}
@@ -976,10 +980,11 @@ Compaction* LevelCompactionPicker::PickCompaction(
const MutableDBOptions& mutable_db_options,
const std::vector<SequenceNumber>& /*existing_snapshots */,
const SnapshotChecker* /*snapshot_checker*/, VersionStorageInfo* vstorage,
LogBuffer* log_buffer, bool /* require_max_output_level*/) {
LogBuffer* log_buffer, const std::string& full_history_ts_low,
bool /* require_max_output_level*/) {
LevelCompactionBuilder builder(cf_name, vstorage, this, log_buffer,
mutable_cf_options, ioptions_,
mutable_db_options);
mutable_db_options, full_history_ts_low);
return builder.PickCompaction();
}
} // namespace ROCKSDB_NAMESPACE
+1
View File
@@ -26,6 +26,7 @@ class LevelCompactionPicker : public CompactionPicker {
const std::vector<SequenceNumber>& /* existing_snapshots */,
const SnapshotChecker* /* snapshot_checker */,
VersionStorageInfo* vstorage, LogBuffer* log_buffer,
const std::string& full_history_ts_low,
bool /*require_max_output_level*/ = false) override;
bool NeedsCompaction(const VersionStorageInfo* vstorage) const override;
File diff suppressed because it is too large. Load diff
+12 -5
View File
@@ -39,7 +39,7 @@ class UniversalCompactionBuilder {
const std::vector<SequenceNumber>& existing_snapshots,
const SnapshotChecker* snapshot_checker, VersionStorageInfo* vstorage,
UniversalCompactionPicker* picker, LogBuffer* log_buffer,
bool require_max_output_level)
bool require_max_output_level, const std::string& full_history_ts_low)
: ioptions_(ioptions),
icmp_(icmp),
cf_name_(cf_name),
@@ -50,7 +50,8 @@ class UniversalCompactionBuilder {
log_buffer_(log_buffer),
require_max_output_level_(require_max_output_level),
allow_ingest_behind_(ioptions.cf_allow_ingest_behind ||
ioptions.allow_ingest_behind) {
ioptions.allow_ingest_behind),
full_history_ts_low_(full_history_ts_low) {
assert(icmp_);
const auto* ucmp = icmp_->user_comparator();
assert(ucmp);
@@ -450,6 +451,7 @@ class UniversalCompactionBuilder {
std::map<uint64_t, size_t> file_marked_for_compaction_to_sorted_run_index_;
bool require_max_output_level_;
bool allow_ingest_behind_;
const std::string& full_history_ts_low_;
std::vector<UniversalCompactionBuilder::SortedRun> CalculateSortedRuns(
const VersionStorageInfo& vstorage, int last_level,
@@ -595,16 +597,20 @@ bool UniversalCompactionPicker::NeedsCompaction(
return false;
}
// TODO leverage full_history_ts_low in universal compaction picking. It could
// help reduce the same infinite compaction loop issue found in level
// compaction.
Compaction* UniversalCompactionPicker::PickCompaction(
const std::string& cf_name, const MutableCFOptions& mutable_cf_options,
const MutableDBOptions& mutable_db_options,
const std::vector<SequenceNumber>& existing_snapshots,
const SnapshotChecker* snapshot_checker, VersionStorageInfo* vstorage,
LogBuffer* log_buffer, bool require_max_output_level) {
LogBuffer* log_buffer, const std::string& full_history_ts_low,
bool require_max_output_level) {
UniversalCompactionBuilder builder(
ioptions_, icmp_, cf_name, mutable_cf_options, mutable_db_options,
existing_snapshots, snapshot_checker, vstorage, this, log_buffer,
require_max_output_level);
require_max_output_level, full_history_ts_low);
return builder.PickCompaction();
}
@@ -825,7 +831,8 @@ Compaction* UniversalCompactionBuilder::PickCompaction() {
RecordInHistogram(ioptions_.stats, NUM_FILES_IN_SINGLE_COMPACTION, num_files);
picker_->RegisterCompaction(c);
vstorage_->ComputeCompactionScore(ioptions_, mutable_cf_options_);
vstorage_->ComputeCompactionScore(ioptions_, mutable_cf_options_,
full_history_ts_low_);
TEST_SYNC_POINT_CALLBACK("UniversalCompactionBuilder::PickCompaction:Return",
c);
+2 -1
View File
@@ -26,7 +26,8 @@ class UniversalCompactionPicker : public CompactionPicker {
const MutableDBOptions& mutable_db_options,
const std::vector<SequenceNumber>& existing_snapshots,
const SnapshotChecker* snapshot_checker, VersionStorageInfo* vstorage,
LogBuffer* log_buffer, bool require_max_output_level = false) override;
LogBuffer* log_buffer, const std::string& full_history_ts_low,
bool require_max_output_level = false) override;
int MaxOutputLevel() const override { return NumberLevels() - 1; }
bool NeedsCompaction(const VersionStorageInfo* vstorage) const override;
+4 -4
View File
@@ -65,7 +65,7 @@ Status VerifySstFileChecksum(const Options& options,
}
Status VerifySstFileChecksumInternal(const Options& options,
const EnvOptions& env_options,
const FileOptions& file_options,
const ReadOptions& read_options,
const std::string& file_path,
const SequenceNumber& largest_seqno) {
@@ -74,8 +74,8 @@ Status VerifySstFileChecksumInternal(const Options& options,
InternalKeyComparator internal_comparator(options.comparator);
ImmutableOptions ioptions(options);
Status s = ioptions.fs->NewRandomAccessFile(
file_path, FileOptions(env_options), &file, nullptr);
Status s =
ioptions.fs->NewRandomAccessFile(file_path, file_options, &file, nullptr);
if (s.ok()) {
s = ioptions.fs->GetFileSize(file_path, IOOptions(), &file_size, nullptr);
} else {
@@ -94,7 +94,7 @@ Status VerifySstFileChecksumInternal(const Options& options,
const bool kImmortal = true;
auto reader_options = TableReaderOptions(
ioptions, options.prefix_extractor, options.compression_manager.get(),
env_options, internal_comparator, options.block_protection_bytes_per_key,
file_options, internal_comparator, options.block_protection_bytes_per_key,
false /* skip_filters */, !kImmortal, false /* force_direct_prefetch */,
-1 /* level */);
reader_options.largest_seqno = largest_seqno;
+2 -1
View File
@@ -5,10 +5,11 @@
#pragma once
#include "rocksdb/db.h"
#include "rocksdb/file_system.h"
namespace ROCKSDB_NAMESPACE {
Status VerifySstFileChecksumInternal(const Options& options,
const EnvOptions& env_options,
const FileOptions& file_options,
const ReadOptions& read_options,
const std::string& file_path,
const SequenceNumber& largest_seqno = 0);
+125
View File
@@ -242,6 +242,43 @@ TEST_F(DBBasicTest, ReadOnlyDB) {
Status::Code::kNotSupported);
}
TEST_F(DBBasicTest, ReadOnlyDBFlushWAL) {
// Test that FlushWAL returns NotSupported on read-only DB, and that
// GetLiveFilesStorageInfo works correctly even with manual_wal_flush=true.
// This is a regression test for a bug where GetLiveFilesStorageInfo would
// crash on read-only DBs with manual_wal_flush=true because FlushWAL
// accessed logs_.back() on an empty deque.
auto options = CurrentOptions();
options.manual_wal_flush = true;
DestroyAndReopen(options);
ASSERT_OK(Put("foo", "v1"));
ASSERT_OK(Put("bar", "v2"));
ASSERT_OK(Flush());
ASSERT_OK(Put("baz", "v3")); // Unflushed data in WAL
Close();
// Reopen as read-only
ASSERT_OK(ReadOnlyReopen(options));
ASSERT_EQ("v1", Get("foo"));
ASSERT_EQ("v2", Get("bar"));
ASSERT_EQ("v3", Get("baz"));
// FlushWAL should return NotSupported (not crash)
ASSERT_EQ(db_->FlushWAL(/*sync=*/false).code(), Status::Code::kNotSupported);
ASSERT_EQ(db_->FlushWAL(/*sync=*/true).code(), Status::Code::kNotSupported);
// GetLiveFilesStorageInfo should succeed (previously crashed with
// manual_wal_flush=true because it called FlushWAL which accessed
// logs_.back() on empty deque)
LiveFilesStorageInfoOptions lfsi_opts;
lfsi_opts.wal_size_for_flush = 0;
std::vector<LiveFileStorageInfo> files;
ASSERT_OK(db_->GetLiveFilesStorageInfo(lfsi_opts, &files));
ASSERT_GT(files.size(), 0);
Close();
}
TEST_F(DBBasicTest, ReadOnlyDBWithWriteDBIdToManifestSet) {
auto options = CurrentOptions();
options.write_dbid_to_manifest = false;
@@ -5437,6 +5474,94 @@ INSTANTIATE_TEST_CASE_P(DBBasicTestDeadline, DBBasicTestDeadline,
::testing::Values(std::make_tuple(true, false),
std::make_tuple(false, true),
std::make_tuple(true, true)));
// FileSystemWrapper that captures FileOptions passed to NewRandomAccessFile
// for .sst files, so we can verify file_checksum fields are populated.
class ChecksumCapturingFS : public FileSystemWrapper {
public:
explicit ChecksumCapturingFS(const std::shared_ptr<FileSystem>& base)
: FileSystemWrapper(base) {}
static const char* kClassName() { return "ChecksumCapturingFS"; }
const char* Name() const override { return kClassName(); }
IOStatus NewRandomAccessFile(const std::string& fname,
const FileOptions& opts,
std::unique_ptr<FSRandomAccessFile>* result,
IODebugContext* dbg) override {
if (fname.find(".sst") != std::string::npos) {
std::lock_guard<std::mutex> lock(mu_);
captured_file_checksum_ = opts.file_checksum;
captured_file_checksum_func_name_ = opts.file_checksum_func_name;
capture_count_++;
}
return target()->NewRandomAccessFile(fname, opts, result, dbg);
}
std::string GetCapturedFileChecksum() {
std::lock_guard<std::mutex> lock(mu_);
return captured_file_checksum_;
}
std::string GetCapturedFileChecksumFuncName() {
std::lock_guard<std::mutex> lock(mu_);
return captured_file_checksum_func_name_;
}
int GetCaptureCount() {
std::lock_guard<std::mutex> lock(mu_);
return capture_count_;
}
void Reset() {
std::lock_guard<std::mutex> lock(mu_);
captured_file_checksum_.clear();
captured_file_checksum_func_name_.clear();
capture_count_ = 0;
}
private:
std::mutex mu_;
std::string captured_file_checksum_;
std::string captured_file_checksum_func_name_;
int capture_count_ = 0;
};
TEST_F(DBBasicTest, FileChecksumInFileOptions) {
// Verify that file_checksum and file_checksum_func_name from FileMetaData
// are propagated through FileOptions when opening SST files.
auto capturing_fs =
std::make_shared<ChecksumCapturingFS>(env_->GetFileSystem());
std::unique_ptr<Env> env(new CompositeEnvWrapper(env_, capturing_fs));
Options options = GetDefaultOptions();
options.create_if_missing = true;
options.env = env.get();
options.file_checksum_gen_factory = GetFileChecksumGenCrc32cFactory();
DestroyAndReopen(options);
// Write data and flush to create an SST with a file checksum.
ASSERT_OK(Put("key1", "value1"));
ASSERT_OK(Flush());
// Reset captures, then reopen to trigger TableCache SST open.
capturing_fs->Reset();
Reopen(options);
// Read to trigger SST open through TableCache::GetTableReader.
ASSERT_EQ("value1", Get("key1"));
// Verify that checksum fields were populated.
ASSERT_GT(capturing_fs->GetCaptureCount(), 0);
ASSERT_FALSE(capturing_fs->GetCapturedFileChecksum().empty());
ASSERT_NE(capturing_fs->GetCapturedFileChecksumFuncName(),
capturing_fs->GetCapturedFileChecksum());
ASSERT_EQ(capturing_fs->GetCapturedFileChecksumFuncName(),
"FileChecksumCrc32c");
Close();
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+7 -7
View File
@@ -101,7 +101,7 @@ TEST_F(DBFlushTest, SyncFail) {
TEST_SYNC_POINT("DBFlushTest::SyncFail:2");
fault_injection_env->SetFilesystemActive(true);
// Now the background job will do the flush; wait for it.
// Returns the IO error happend during flush.
// Returns the IO error happened during flush.
ASSERT_NOK(dbfull()->TEST_WaitForFlushMemTable());
ASSERT_EQ("", FilesPerLevel()); // flush failed.
Destroy(options);
@@ -518,11 +518,11 @@ TEST_F(DBFlushTest, StatisticsGarbageInsertAndDeletes) {
// Note : one set of delete for KEY1, KEY2, KEY3 is written to
// SSTable to propagate the delete operations to K-V pairs
// that could have been inserted into the database during past Flush
// opeartions.
// operations.
EXPECTED_MEMTABLE_GARBAGE_BYTES_AT_FLUSH -=
KEY1.size() + KEY2.size() + KEY3.size() + 3 * sizeof(uint64_t);
// Additional useful paylaod.
// Additional useful payload.
ASSERT_OK(Delete(KEY4));
ASSERT_OK(Delete(KEY5));
ASSERT_OK(Delete(KEY6));
@@ -614,7 +614,7 @@ TEST_F(DBFlushTest, StatisticsGarbageRangeDeletes) {
// Note : one set of deleteRange for (KEY1, KEY2) and (KEY2, KEY3) is written
// to SSTable to propagate the deleteRange operations to K-V pairs that could
// have been inserted into the database during past Flush opeartions.
// have been inserted into the database during past Flush operations.
EXPECTED_MEMTABLE_GARBAGE_BYTES_AT_FLUSH -=
(KEY1.size() + KEY2.size() + sizeof(uint64_t)) +
(KEY2.size() + KEY3.size() + sizeof(uint64_t));
@@ -842,7 +842,7 @@ TEST_F(DBFlushTest, FixFlushReasonRaceFromConcurrentFlushes) {
ASSERT_OK(Put(1, Key(idx), std::string(1, 'v')));
}
// To coerce a manual flush happenning in the middle of GetLiveFiles's flush,
// To coerce a manual flush happening in the middle of GetLiveFiles's flush,
// we need to pause background flush thread and enable it later.
std::shared_ptr<test::SleepingBackgroundTask> sleeping_task =
std::make_shared<test::SleepingBackgroundTask>();
@@ -851,7 +851,7 @@ TEST_F(DBFlushTest, FixFlushReasonRaceFromConcurrentFlushes) {
sleeping_task.get(), Env::Priority::HIGH);
sleeping_task->WaitUntilSleeping();
// Coerce a manual flush happenning in the middle of GetLiveFiles's flush
// Coerce a manual flush happening in the middle of GetLiveFiles's flush
bool get_live_files_paused_at_sync_point = false;
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"DBImpl::AtomicFlushMemTables:AfterScheduleFlush", [&](void* /* arg */) {
@@ -1428,7 +1428,7 @@ TEST_F(DBFlushTest, MemPurgeDeleteAndDeleteRange) {
Close();
}
// Create a Compaction Fitler that will be invoked
// Create a Compaction Filter that will be invoked
// at flush time and will update the value of a KV pair
// if the key string is "lower" than the filter_key_ string.
class ConditionalUpdateFilter : public CompactionFilter {
+110 -50
View File
@@ -1177,23 +1177,38 @@ FSDirectory* DBImpl::GetDataDir(ColumnFamilyData* cfd, size_t path_id) const {
}
Status DBImpl::SetOptions(
ColumnFamilyHandle* column_family,
const std::unordered_map<std::string, std::string>& options_map) {
const std::unordered_map<ColumnFamilyHandle*,
std::unordered_map<std::string, std::string>>&
column_families_opts_map) {
// TODO: plumb Env::IOActivity, Env::IOPriority
const ReadOptions read_options;
const WriteOptions write_options;
auto* cfd =
static_cast_with_check<ColumnFamilyHandleImpl>(column_family)->cfd();
if (options_map.empty()) {
ROCKS_LOG_WARN(immutable_db_options_.info_log,
"SetOptions() on column family [%s], empty input",
cfd->GetName().c_str());
return Status::InvalidArgument("empty input");
if (column_families_opts_map.empty()) {
return Status::OK();
}
for (const auto& cf_opts : column_families_opts_map) {
if (cf_opts.second.empty()) {
ROCKS_LOG_WARN(immutable_db_options_.info_log,
"SetOptions() on column family [%s], empty input",
cf_opts.first->GetName().c_str());
return Status::InvalidArgument("empty input");
}
}
autovector<std::pair<ColumnFamilyData*,
const std::unordered_map<std::string, std::string>*>>
column_family_datas;
for (const auto& cf_opts : column_families_opts_map) {
column_family_datas.push_back(
{static_cast_with_check<ColumnFamilyHandleImpl>(cf_opts.first)->cfd(),
&cf_opts.second});
}
InstrumentedMutexLock ol(&options_mutex_);
MutableCFOptions new_options_copy; // For logging outside of DB mutex
autovector<MutableCFOptions>
new_options_copy; // For logging outside of DB mutex
Status s;
Status persist_options_status;
SuperVersionContext sv_context(/* create_superversion */ true);
@@ -1216,68 +1231,104 @@ Status DBImpl::SetOptions(
// Thus aren't releasing the DB mutex from LogAndApply calling pre_cb,
// through installing the new Version until the end of this block, after
// installing the new SuperVersion.
auto pre_cb = [&]() -> Status {
Status cb_s = cfd->SetOptions(db_options, options_map);
if (cb_s.ok()) {
new_options_copy = cfd->GetLatestMutableCFOptions();
}
return cb_s;
};
VersionEdit dummy_edit;
dummy_edit.MarkNoManifestWriteDummy();
TEST_SYNC_POINT_CALLBACK("DBImpl::SetOptions:dummy_edit", &dummy_edit);
s = versions_->LogAndApply(
cfd, read_options, write_options, &dummy_edit, &mutex_,
directories_.GetDbDir(), false /*new_descriptor_log=*/,
nullptr /*new_opts*/, {} /*manifest_wcb*/, pre_cb);
if (!versions_->io_status().ok()) {
assert(!s.ok());
error_handler_.SetBGError(versions_->io_status(),
BackgroundErrorReason::kManifestWrite);
for (const auto& cfd_opts : column_family_datas) {
auto* cfd = cfd_opts.first;
const auto* options_map_ptr = cfd_opts.second;
auto pre_cb = [&]() -> Status {
Status cb_s = cfd->SetOptions(db_options, *options_map_ptr);
if (cb_s.ok()) {
new_options_copy.emplace_back(cfd->GetLatestMutableCFOptions());
}
return cb_s;
};
s = versions_->LogAndApply(
cfd, read_options, write_options, &dummy_edit, &mutex_,
directories_.GetDbDir(), false /*new_descriptor_log=*/,
nullptr /*new_opts*/, {} /*manifest_wcb*/, pre_cb);
if (!versions_->io_status().ok()) {
assert(!s.ok());
error_handler_.SetBGError(versions_->io_status(),
BackgroundErrorReason::kManifestWrite);
}
if (!s.ok()) {
break;
}
}
if (s.ok()) {
// Trigger possible flush/compactions. This has to be before we persist
// options to file, otherwise there will be a deadlock with writer
// thread.
InstallSuperVersionForConfigChange(cfd, &sv_context);
for (const auto& cfd_opts : column_family_datas) {
InstallSuperVersionForConfigChange(cfd_opts.first, &sv_context);
}
persist_options_status =
WriteOptionsFile(write_options, true /*db_mutex_already_held*/);
bg_cv_.SignalAll();
assert(new_options_copy == cfd->GetLatestMutableCFOptions());
assert(cfd->GetLatestMutableCFOptions() ==
cfd->GetCurrentMutableCFOptions());
assert(cfd->GetCurrentMutableCFOptions() ==
cfd->current()->GetMutableCFOptions());
#ifndef NDEBUG
for (size_t i = 0; i < column_family_datas.size(); ++i) {
auto* cfd = column_family_datas[i].first;
assert(new_options_copy[i] == cfd->GetLatestMutableCFOptions());
assert(cfd->GetLatestMutableCFOptions() ==
cfd->GetCurrentMutableCFOptions());
assert(cfd->GetCurrentMutableCFOptions() ==
cfd->current()->GetMutableCFOptions());
}
#endif
}
}
sv_context.Clean();
if (s.ok() && (options_map.count("preserve_internal_time_seconds") > 0 ||
options_map.count("preclude_last_level_data_seconds") > 0)) {
s = RegisterRecordSeqnoTimeWorker();
if (s.ok()) {
bool needs_seqno_worker = false;
for (const auto& cf_opts : column_families_opts_map) {
if (cf_opts.second.count("preserve_internal_time_seconds") > 0 ||
cf_opts.second.count("preclude_last_level_data_seconds") > 0) {
needs_seqno_worker = true;
break;
}
}
if (needs_seqno_worker) {
s = RegisterRecordSeqnoTimeWorker();
}
}
ROCKS_LOG_INFO(
immutable_db_options_.info_log,
"SetOptions() on column family [%s], inputs:", cfd->GetName().c_str());
for (const auto& o : options_map) {
ROCKS_LOG_INFO(immutable_db_options_.info_log, "%s: %s\n", o.first.c_str(),
o.second.c_str());
ROCKS_LOG_INFO(immutable_db_options_.info_log,
"SetOptions() on [%zu] column families, inputs:",
column_family_datas.size());
for (size_t i = 0; i < column_family_datas.size(); ++i) {
const auto* cfd = column_family_datas[i].first;
const auto* options_map_ptr = column_family_datas[i].second;
ROCKS_LOG_INFO(immutable_db_options_.info_log,
"Set options on column family [%s] (%zu/%zu), inputs:",
cfd->GetName().c_str(), i, column_family_datas.size());
for (const auto& o : *options_map_ptr) {
ROCKS_LOG_INFO(immutable_db_options_.info_log, "%s: %s\n",
o.first.c_str(), o.second.c_str());
}
}
if (s.ok()) {
ROCKS_LOG_INFO(immutable_db_options_.info_log,
"[%s] SetOptions() succeeded", cfd->GetName().c_str());
new_options_copy.Dump(immutable_db_options_.info_log.get());
for (size_t i = 0; i < column_family_datas.size(); ++i) {
const auto* cfd = column_family_datas[i].first;
ROCKS_LOG_INFO(immutable_db_options_.info_log,
"Set options on column family [%s] (%zu/%zu) succeeded, "
"updated CF options:",
cfd->GetName().c_str(), i, column_family_datas.size());
new_options_copy[i].Dump(immutable_db_options_.info_log.get());
}
if (!persist_options_status.ok()) {
// NOTE: WriteOptionsFile already logs on failure
s = persist_options_status;
}
} else {
persist_options_status.PermitUncheckedError(); // less important
ROCKS_LOG_WARN(immutable_db_options_.info_log, "[%s] SetOptions() failed",
cfd->GetName().c_str());
ROCKS_LOG_WARN(immutable_db_options_.info_log, "SetOptions() failed: %s",
s.ToString().c_str());
}
LogFlush(immutable_db_options_.info_log);
return s;
@@ -4351,7 +4402,8 @@ void DBImpl::ReleaseSnapshot(const Snapshot* s) {
for (auto* cfd : *versions_->GetColumnFamilySet()) {
if (!cfd->AllowIngestBehind()) {
cfd->current()->storage_info()->UpdateOldestSnapshot(
oldest_snapshot, /*allow_ingest_behind=*/false);
oldest_snapshot, /*allow_ingest_behind=*/false,
cfd->ioptions().user_comparator, cfd->GetFullHistoryTsLow());
if (!cfd->current()
->storage_info()
->BottommostFilesMarkedForCompaction()
@@ -4987,7 +5039,8 @@ Status DBImpl::DeleteFilesInRanges(ColumnFamilyHandle* column_family,
}
if (!deleted_files.empty()) {
vstorage->ComputeCompactionScore(cfd->ioptions(),
cfd->GetLatestMutableCFOptions());
cfd->GetLatestMutableCFOptions(),
cfd->GetFullHistoryTsLow());
}
if (edit.GetDeletedFiles().empty()) {
job_context.Clean();
@@ -6418,8 +6471,11 @@ Status DBImpl::VerifyChecksumInternal(const ReadOptions& read_options,
fmeta->file_checksum_func_name, fname,
read_options);
} else {
FileOptions fopts = file_options_;
fopts.file_checksum = fmeta->file_checksum;
fopts.file_checksum_func_name = fmeta->file_checksum_func_name;
s = ROCKSDB_NAMESPACE::VerifySstFileChecksumInternal(
opts, file_options_, read_options, fname, fd.largest_seqno);
opts, fopts, read_options, fname, fd.largest_seqno);
}
RecordTick(stats_, VERIFY_CHECKSUM_READ_BYTES,
IOSTATS(bytes_read) - prev_bytes_read);
@@ -6487,12 +6543,15 @@ Status DBImpl::VerifyFullFileChecksum(const std::string& file_checksum_expected,
}
std::string file_checksum;
std::string func_name;
FileOptions fopts;
fopts.file_checksum = file_checksum_expected;
fopts.file_checksum_func_name = func_name_expected;
s = ROCKSDB_NAMESPACE::GenerateOneFileChecksum(
fs_.get(), fname, immutable_db_options_.file_checksum_gen_factory.get(),
func_name_expected, &file_checksum, &func_name,
read_options.readahead_size, immutable_db_options_.allow_mmap_reads,
io_tracer_, immutable_db_options_.rate_limiter.get(), read_options,
immutable_db_options_.stats, immutable_db_options_.clock);
immutable_db_options_.stats, immutable_db_options_.clock, fopts);
if (s.ok()) {
assert(func_name_expected == func_name);
if (file_checksum != file_checksum_expected) {
@@ -6851,7 +6910,8 @@ void DBImpl::TriggerPeriodicCompaction() {
if (cfd->GetLatestCFOptions().periodic_compaction_seconds &&
!cfd->queued_for_compaction()) {
cfd->current()->storage_info()->ComputeCompactionScore(
cfd->ioptions(), cfd->GetLatestMutableCFOptions());
cfd->ioptions(), cfd->GetLatestMutableCFOptions(),
cfd->GetFullHistoryTsLow());
EnqueuePendingCompaction(cfd);
if (cfd->queued_for_compaction()) {
ROCKS_LOG_INFO(immutable_db_options_.info_log,
+3 -2
View File
@@ -458,8 +458,9 @@ class DBImpl : public DB {
using DB::SetOptions;
Status SetOptions(
ColumnFamilyHandle* column_family,
const std::unordered_map<std::string, std::string>& options_map) override;
const std::unordered_map<ColumnFamilyHandle*,
std::unordered_map<std::string, std::string>>&
column_families_opts_map) override;
Status SetDBOptions(
const std::unordered_map<std::string, std::string>& options_map) override;
+18 -19
View File
@@ -1449,7 +1449,8 @@ Status DBImpl::PerformTrivialMove(Compaction& c, LogBuffer* log_buffer,
f->file_creation_time, f->epoch_number,
f->file_checksum, f->file_checksum_func_name,
f->unique_id, f->compensated_range_deletion_size,
f->tail_size, f->user_defined_timestamps_persisted);
f->tail_size, f->user_defined_timestamps_persisted,
f->min_timestamp, f->max_timestamp);
moved_bytes += static_cast<size_t>(c.input(l, i)->fd.GetFileSize());
ROCKS_LOG_BUFFER(
log_buffer, "[%s] Moved #%" PRIu64 " to level-%d %" PRIu64 " bytes\n",
@@ -1645,8 +1646,8 @@ Status DBImpl::CompactFilesImpl(
// takes running compactions into account (by skipping files that are already
// being compacted). Since we just changed compaction score, we recalculate it
// here.
version->storage_info()->ComputeCompactionScore(cfd->ioptions(),
c->mutable_cf_options());
version->storage_info()->ComputeCompactionScore(
cfd->ioptions(), c->mutable_cf_options(), cfd->GetFullHistoryTsLow());
compaction_job.Prepare(std::nullopt /*subcompact to be computed*/);
@@ -1971,7 +1972,8 @@ Status DBImpl::ReFitLevel(ColumnFamilyData* cfd, int level, int target_level) {
f->oldest_ancester_time, f->file_creation_time, f->epoch_number,
f->file_checksum, f->file_checksum_func_name, f->unique_id,
f->compensated_range_deletion_size, f->tail_size,
f->user_defined_timestamps_persisted);
f->user_defined_timestamps_persisted, f->min_timestamp,
f->max_timestamp);
}
ROCKS_LOG_DEBUG(immutable_db_options_.info_log,
"[%s] Apply version edit:\n%s", cfd->GetName().c_str(),
@@ -2889,16 +2891,8 @@ Status DBImpl::WaitForFlushMemTables(
Status DBImpl::EnableAutoCompaction(
const std::vector<ColumnFamilyHandle*>& column_family_handles) {
Status s;
for (auto cf_ptr : column_family_handles) {
Status status =
this->SetOptions(cf_ptr, {{"disable_auto_compactions", "false"}});
if (!status.ok()) {
s = status;
}
}
return s;
return SetOptions(column_family_handles,
{{"disable_auto_compactions", "false"}});
}
// NOTE: Calling DisableManualCompaction() may overwrite the
@@ -3839,7 +3833,8 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
->current()
->storage_info()
->ComputeCompactionScore(c->immutable_options(),
c->mutable_cf_options());
c->mutable_cf_options(),
cfd->GetFullHistoryTsLow());
EnqueuePendingCompaction(cfd);
c.reset();
@@ -4096,7 +4091,9 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
in_file->unique_id,
in_file->compensated_range_deletion_size,
in_file->tail_size,
in_file->user_defined_timestamps_persisted};
in_file->user_defined_timestamps_persisted,
in_file->min_timestamp,
in_file->max_timestamp};
out_files.push_back(std::move(out_file_metadata));
}
@@ -4401,7 +4398,8 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
->current()
->storage_info()
->ComputeCompactionScore(c->immutable_options(),
c->mutable_cf_options());
c->mutable_cf_options(),
cfd->GetFullHistoryTsLow());
EnqueuePendingCompaction(cfd);
}
}
@@ -4507,7 +4505,7 @@ Compaction* DBImpl::CreateIntendedCompactionForwardedToBottomPriorityPool(
nullptr /* snapshot_checker */, c->compaction_reason());
cfd->compaction_picker()->RegisterCompaction(intended_compaction);
vstorage->ComputeCompactionScore(io, mo);
vstorage->ComputeCompactionScore(io, mo, cfd->GetFullHistoryTsLow());
intended_compaction->FinalizeInputInfo(cfd->current());
return intended_compaction;
@@ -4879,7 +4877,8 @@ void DBImpl::ResetBottomPriCompactionIntent(ColumnFamilyData* cfd,
std::unique_ptr<Compaction>& c) {
c->ReleaseCompactionFiles(Status::OK());
cfd->current()->storage_info()->ComputeCompactionScore(
c->immutable_options(), c->mutable_cf_options());
c->immutable_options(), c->mutable_cf_options(),
cfd->GetFullHistoryTsLow());
c.reset();
}
+4 -2
View File
@@ -46,7 +46,8 @@ Status DBImpl::SuggestCompactRange(ColumnFamilyHandle* column_family,
// Since we have some more files to compact, we should also recompute
// compaction score
vstorage->ComputeCompactionScore(cfd->ioptions(),
cfd->GetLatestMutableCFOptions());
cfd->GetLatestMutableCFOptions(),
cfd->GetFullHistoryTsLow());
EnqueuePendingCompaction(cfd);
MaybeScheduleFlushOrCompaction();
}
@@ -143,7 +144,8 @@ Status DBImpl::PromoteL0(ColumnFamilyHandle* column_family, int target_level) {
f->file_creation_time, f->epoch_number, f->file_checksum,
f->file_checksum_func_name, f->unique_id,
f->compensated_range_deletion_size, f->tail_size,
f->user_defined_timestamps_persisted);
f->user_defined_timestamps_persisted, f->min_timestamp,
f->max_timestamp);
}
status = versions_->LogAndApply(cfd, read_options, write_options, &edit,
+2 -1
View File
@@ -657,7 +657,8 @@ Status DBImpl::Recover(
f->file_creation_time, f->epoch_number,
f->file_checksum, f->file_checksum_func_name,
f->unique_id, f->compensated_range_deletion_size,
f->tail_size, f->user_defined_timestamps_persisted);
f->tail_size, f->user_defined_timestamps_persisted,
f->min_timestamp, f->max_timestamp);
ROCKS_LOG_WARN(immutable_db_options_.info_log,
"[%s] Moving #%" PRIu64
" from from_level-%d to from_level-%d %" PRIu64
+5
View File
@@ -121,6 +121,11 @@ class DBImplReadOnly : public DBImpl {
return Status::NotSupported("Not supported operation in read only mode.");
}
using DBImpl::FlushWAL;
Status FlushWAL(const FlushWALOptions& /*options*/) override {
return Status::NotSupported("Not supported operation in read only mode.");
}
using DB::IngestExternalFile;
Status IngestExternalFile(
ColumnFamilyHandle* /*column_family*/,
+3 -3
View File
@@ -216,9 +216,9 @@ class DBImplSecondary : public DBImpl {
using DBImpl::SetOptions;
Status SetOptions(
ColumnFamilyHandle* /*cfd*/,
const std::unordered_map<std::string, std::string>& /*options_map*/)
override {
const std::unordered_map<ColumnFamilyHandle*,
std::unordered_map<std::string, std::string>>&
/*column_families_opts_map*/) override {
// Currently not supported because changing certain options may cause
// flush/compaction and/or write to MANIFEST.
return Status::NotSupported("Not supported operation in secondary mode.");
+146
View File
@@ -5027,6 +5027,152 @@ TEST_P(DBMultiScanIteratorTest, AsyncPrefetchWithExternalFileIngestion) {
ASSERT_EQ(total_keys, 400);
iter.reset();
}
TEST_P(DBMultiScanIteratorTest, StatisticsTest) {
// Test that multi scan statistics are properly recorded
auto options = CurrentOptions();
options.statistics = CreateDBStatistics();
// Use small block size to ensure multiple blocks
BlockBasedTableOptions table_options;
table_options.block_size = 256;
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
DestroyAndReopen(options);
// Create data across multiple blocks
for (int i = 0; i < 100; ++i) {
std::stringstream ss;
ss << std::setw(3) << std::setfill('0') << i;
// Use larger values to ensure multiple blocks
ASSERT_OK(Put("k" + ss.str(), std::string(100, 'v')));
}
ASSERT_OK(Flush());
// Reset stats before multi scan
ASSERT_OK(options.statistics->Reset());
// Set up two scan ranges
std::vector<std::string> key_ranges({"k010", "k030", "k060", "k080"});
ReadOptions ro;
ro.fill_cache = GetParam();
MultiScanArgs scan_options(BytewiseComparator());
scan_options.insert(key_ranges[0], key_ranges[1]);
scan_options.insert(key_ranges[2], key_ranges[3]);
ColumnFamilyHandle* cfh = dbfull()->DefaultColumnFamily();
std::unique_ptr<MultiScan> iter =
dbfull()->NewMultiScan(ro, cfh, scan_options);
// Iterate through all ranges
int count = 0;
try {
for (auto range : *iter) {
for (auto it : range) {
(void)it;
count++;
}
}
} catch (MultiScanException& ex) {
ASSERT_NOK(ex.status());
std::cerr << "Iterator returned status " << ex.what();
abort();
}
ASSERT_EQ(count, 40); // 20 keys per range
iter.reset();
// Check statistics
// MULTISCAN_PREPARE_CALLS should be at least 1
ASSERT_GE(TestGetTickerCount(options, MULTISCAN_PREPARE_CALLS), 1);
// MULTISCAN_PREPARE_ERRORS should be 0
ASSERT_EQ(TestGetTickerCount(options, MULTISCAN_PREPARE_ERRORS), 0);
// MULTISCAN_SEEK_ERRORS should be 0
ASSERT_EQ(TestGetTickerCount(options, MULTISCAN_SEEK_ERRORS), 0);
// Blocks should be prefetched or from cache
uint64_t blocks_prefetched =
TestGetTickerCount(options, MULTISCAN_BLOCKS_PREFETCHED);
uint64_t blocks_from_cache =
TestGetTickerCount(options, MULTISCAN_BLOCKS_FROM_CACHE);
ASSERT_GT(blocks_prefetched + blocks_from_cache, 0);
// If blocks were prefetched, prefetch bytes and IO requests should be > 0
if (blocks_prefetched > 0) {
ASSERT_GT(TestGetTickerCount(options, MULTISCAN_PREFETCH_BYTES), 0);
uint64_t io_requests = TestGetTickerCount(options, MULTISCAN_IO_REQUESTS);
ASSERT_GT(io_requests, 0);
ASSERT_LE(io_requests, blocks_prefetched);
}
// Wasted blocks should be 0 since we iterated through everything
ASSERT_EQ(TestGetTickerCount(options, MULTISCAN_PREFETCH_BLOCKS_WASTED), 0);
}
TEST_P(DBMultiScanIteratorTest, StatisticsWastedBlocksTest) {
// Test that wasted blocks are tracked when iteration is abandoned early
auto options = CurrentOptions();
options.statistics = CreateDBStatistics();
// Use small block size to ensure multiple blocks
BlockBasedTableOptions table_options;
table_options.block_size = 256;
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
DestroyAndReopen(options);
// Create data across multiple blocks
for (int i = 0; i < 100; ++i) {
std::stringstream ss;
ss << std::setw(3) << std::setfill('0') << i;
ASSERT_OK(Put("k" + ss.str(), std::string(100, 'v')));
}
ASSERT_OK(Flush());
// Reset stats before multi scan
ASSERT_OK(options.statistics->Reset());
// Set up a large scan range
ReadOptions ro;
ro.fill_cache = GetParam();
MultiScanArgs scan_options(BytewiseComparator());
scan_options.insert("k000", "k099");
ColumnFamilyHandle* cfh = dbfull()->DefaultColumnFamily();
std::unique_ptr<MultiScan> iter =
dbfull()->NewMultiScan(ro, cfh, scan_options);
// Only iterate through a few keys, then abandon
int count = 0;
try {
for (auto range : *iter) {
for (auto it : range) {
(void)it;
count++;
if (count >= 5) {
break; // Abandon iteration early
}
}
if (count >= 5) {
break;
}
}
} catch (MultiScanException& ex) {
ASSERT_NOK(ex.status());
std::cerr << "Iterator returned status " << ex.what();
abort();
}
ASSERT_EQ(count, 5);
// Destroy iterator to trigger wasted blocks counting
iter.reset();
uint64_t blocks_prefetched =
TestGetTickerCount(options, MULTISCAN_BLOCKS_PREFETCHED);
// If blocks were prefetched, some should be wasted since we abandoned early
if (blocks_prefetched > 1) {
// We only read a few keys, so there should be wasted blocks
ASSERT_GT(TestGetTickerCount(options, MULTISCAN_PREFETCH_BLOCKS_WASTED), 0);
}
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+40
View File
@@ -1689,6 +1689,46 @@ TEST_F(DBOptionsTest, SetOptionsNoManifestWrite) {
ASSERT_EQ(Get("x"), "x");
}
TEST_F(DBOptionsTest, SetOptionsMultipleColumnFamilies) {
Options options;
options.create_if_missing = true;
options.env = CurrentOptions().env;
options.disable_auto_compactions = true;
Reopen(options);
// Create two additional column families
CreateColumnFamilies({"cf1", "cf2"}, options);
ReopenWithColumnFamilies({"default", "cf1", "cf2"}, options);
// Verify initial state - auto compaction should be disabled
ASSERT_TRUE(dbfull()->GetOptions(handles_[0]).disable_auto_compactions);
ASSERT_TRUE(dbfull()->GetOptions(handles_[1]).disable_auto_compactions);
ASSERT_TRUE(dbfull()->GetOptions(handles_[2]).disable_auto_compactions);
// Set options on multiple column families at once
ASSERT_OK(dbfull()->SetOptions({handles_[1], handles_[2]},
{{"disable_auto_compactions", "false"}}));
ASSERT_TRUE(
dbfull()->GetOptions(handles_[0]).disable_auto_compactions); // unchanged
ASSERT_FALSE(
dbfull()->GetOptions(handles_[1]).disable_auto_compactions); // changed
ASSERT_FALSE(
dbfull()->GetOptions(handles_[2]).disable_auto_compactions); // changed
std::unordered_map<ColumnFamilyHandle*,
std::unordered_map<std::string, std::string>>
options_map;
options_map[handles_[0]] = {{"disable_auto_compactions", "false"}};
options_map[handles_[1]] = {{"disable_auto_compactions", "true"}};
options_map[handles_[2]] = {{"disable_auto_compactions", "true"}};
ASSERT_OK(dbfull()->SetOptions(options_map));
ASSERT_FALSE(dbfull()->GetOptions(handles_[0]).disable_auto_compactions);
ASSERT_TRUE(dbfull()->GetOptions(handles_[1]).disable_auto_compactions);
ASSERT_TRUE(dbfull()->GetOptions(handles_[2]).disable_auto_compactions);
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+29 -10
View File
@@ -1519,16 +1519,14 @@ TEST_F(DBPropertiesTest, NeedCompactHintPersistentTest) {
// Excluded from RocksDB lite tests due to `GetPropertiesOfAllTables()` usage.
TEST_F(DBPropertiesTest, BlockAddForCompressionSampling) {
// Sampled compression requires at least one of the following four types.
if (!Snappy_Supported() && !Zlib_Supported() && !LZ4_Supported() &&
!ZSTD_Supported()) {
return;
}
Options options = CurrentOptions();
options.disable_auto_compactions = true;
options.table_properties_collector_factories.emplace_back(
std::make_shared<BlockCountingTablePropertiesCollectorFactory>());
options.compression = kNoCompression;
bool fast_sampling_supported = Snappy_Supported() || LZ4_Supported();
bool slow_sampling_supported = ZSTD_Supported() || Zlib_Supported();
for (bool sample_for_compression : {false, true}) {
// For simplicity/determinism, sample 100% when enabled, or 0% when disabled
@@ -1542,10 +1540,11 @@ TEST_F(DBPropertiesTest, BlockAddForCompressionSampling) {
// L1_0 ["a", "b"]
//
// L0_0 was created by flush. L1_0 was created by compaction. Each file
// contains one data block.
// contains one data block with enough data to be compressible.
for (int i = 0; i < 3; ++i) {
ASSERT_OK(Put("a", "val"));
ASSERT_OK(Put("b", "val"));
for (int j = 0; j < 50; ++j) {
ASSERT_OK(Put(std::to_string(j), "thisismyvalue"));
}
ASSERT_OK(Flush());
if (i == 1) {
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
@@ -1558,13 +1557,33 @@ TEST_F(DBPropertiesTest, BlockAddForCompressionSampling) {
ASSERT_OK(db_->GetPropertiesOfAllTables(&file_to_props));
ASSERT_EQ(2, file_to_props.size());
for (const auto& file_and_props : file_to_props) {
auto& user_props = file_and_props.second->user_collected_properties;
auto& props = *file_and_props.second;
auto& user_props = props.user_collected_properties;
ASSERT_TRUE(user_props.find(BlockCountingTablePropertiesCollector::
kNumSampledBlocksPropertyName) !=
user_props.end());
ASSERT_EQ(user_props.at(BlockCountingTablePropertiesCollector::
kNumSampledBlocksPropertyName),
std::to_string(sample_for_compression ? 1 : 0));
if (sample_for_compression) {
EXPECT_GT(props.fast_compression_estimated_data_size, 0);
EXPECT_GT(props.slow_compression_estimated_data_size, 0);
if (fast_sampling_supported) {
EXPECT_LT(props.fast_compression_estimated_data_size,
props.data_size);
if (slow_sampling_supported) {
EXPECT_LT(props.slow_compression_estimated_data_size,
props.fast_compression_estimated_data_size);
}
}
if (slow_sampling_supported) {
EXPECT_LT(props.slow_compression_estimated_data_size,
props.data_size);
}
} else {
EXPECT_EQ(props.fast_compression_estimated_data_size, 0);
EXPECT_EQ(props.slow_compression_estimated_data_size, 0);
}
}
}
}
+81
View File
@@ -6544,6 +6544,9 @@ TEST_F(DBTest2, LastLevelStatistics) {
DestroyAndReopen(options);
get_iostats_context()->Reset();
IOStatsContext* iostats = get_iostats_context();
// generate 1 sst on level 0
ASSERT_OK(Put("foo1", "bar"));
ASSERT_OK(Put("bar", "bar"));
@@ -6644,9 +6647,87 @@ TEST_F(DBTest2, LastLevelStatistics) {
// Control
ASSERT_NE(options.statistics->getTickerCount(LAST_LEVEL_READ_COUNT),
options.statistics->getTickerCount(NON_LAST_LEVEL_READ_COUNT));
// Control: unknown temperature iostats should be zero since files have
// explicit temperatures (mapped or written)
EXPECT_EQ(
iostats->file_io_stats_by_temperature.unknown_non_last_level_bytes_read,
0);
EXPECT_EQ(
iostats->file_io_stats_by_temperature.unknown_non_last_level_read_count,
0);
EXPECT_EQ(
iostats->file_io_stats_by_temperature.unknown_last_level_bytes_read, 0);
EXPECT_EQ(
iostats->file_io_stats_by_temperature.unknown_last_level_read_count, 0);
}
}
// Test the iostats for files with Temperature::kUnknown that is not mapped
// to another temperature. These stats are used to indicate which non-tiered
// workloads are most promising for tiering (so this test doesn't set
// temperatures).
TEST_F(DBTest2, UnknownLastLevelStatistics) {
Options options = CurrentOptions();
options.statistics = CreateDBStatistics();
BlockBasedTableOptions bbto;
bbto.no_block_cache = true;
options.table_factory.reset(NewBlockBasedTableFactory(bbto));
DestroyAndReopen(options);
get_iostats_context()->Reset();
IOStatsContext* iostats = get_iostats_context();
// Generate 1 sst file on level 0 with kUnknown temperature
ASSERT_OK(Put("foo", "bar"));
ASSERT_OK(Flush());
// Read from the kUnknown file on non-last level
ASSERT_EQ("bar", Get("foo"));
// Verify unknown_non_last_level stats are populated
EXPECT_GT(
iostats->file_io_stats_by_temperature.unknown_non_last_level_bytes_read,
0);
EXPECT_GT(
iostats->file_io_stats_by_temperature.unknown_non_last_level_read_count,
0);
// No reads from last level yet
EXPECT_EQ(iostats->file_io_stats_by_temperature.unknown_last_level_bytes_read,
0);
EXPECT_EQ(iostats->file_io_stats_by_temperature.unknown_last_level_read_count,
0);
// Compact to the last level (level 6) explicitly using MoveFilesToLevel
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
MoveFilesToLevel(6);
// Reopen DB to ensure table cache is cleared and files are re-opened
// with correct is_last_level flag
Reopen(options);
// Reset iostats to measure only the following reads
get_iostats_context()->Reset();
// Read from the file now on last level (still kUnknown since
// last_level_temperature is not set)
ASSERT_EQ("bar", Get("foo"));
// Verify unknown_last_level stats are populated
EXPECT_GT(iostats->file_io_stats_by_temperature.unknown_last_level_bytes_read,
0);
EXPECT_GT(iostats->file_io_stats_by_temperature.unknown_last_level_read_count,
0);
// No new reads from non-last level
EXPECT_EQ(
iostats->file_io_stats_by_temperature.unknown_non_last_level_bytes_read,
0);
EXPECT_EQ(
iostats->file_io_stats_by_temperature.unknown_non_last_level_read_count,
0);
}
TEST_F(DBTest2, CheckpointFileTemperature) {
class NoLinkTestFS : public FileTemperatureTestFS {
using FileTemperatureTestFS::FileTemperatureTestFS;
+1 -1
View File
@@ -1613,7 +1613,7 @@ TEST_F(DBWALTest, SyncWalPartialFailure) {
return s;
}
AcqRelAtomic<uint32_t> syncs_before_failure_{UINT32_MAX};
Atomic<uint32_t> syncs_before_failure_{UINT32_MAX};
protected:
class MyTestWritableFile : public FSWritableFileOwnerWrapper {
+499
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 <set>
#include "db/column_family.h"
#include "db/compaction/compaction.h"
#include "db/db_test_util.h"
#include "port/stack_trace.h"
#include "rocksdb/sst_file_reader.h"
#include "test_util/testutil.h"
namespace ROCKSDB_NAMESPACE {
@@ -48,6 +52,122 @@ class TimestampCompatibleCompactionTest : public DBTestBase {
}
return value;
}
// Helper to get all files with their level and timestamps
std::vector<std::tuple<int, std::string, std::string>>
GetAllFileTimestamps() {
std::vector<std::tuple<int, std::string, std::string>> results;
ColumnFamilyHandle* cfh = db_->DefaultColumnFamily();
auto* cfd = static_cast_with_check<ColumnFamilyHandleImpl>(cfh)->cfd();
auto* vstorage = cfd->current()->storage_info();
for (int level = 0; level < cfd->NumberLevels(); level++) {
for (auto* file : vstorage->LevelFiles(level)) {
results.emplace_back(level, file->min_timestamp, file->max_timestamp);
}
}
return results;
}
// Helper to compute overall min/max timestamps across all files
// Returns {min_ts, max_ts} as uint64_t values
// Asserts that all files have non-empty timestamps
std::pair<uint64_t, uint64_t> GetOverallTimestampRange() {
auto files = GetAllFileTimestamps();
EXPECT_GE(files.size(), 1U);
uint64_t overall_min = UINT64_MAX;
uint64_t overall_max = 0;
for (const auto& [level, min_ts, max_ts] : files) {
EXPECT_FALSE(min_ts.empty()) << "min_timestamp empty at level " << level;
EXPECT_FALSE(max_ts.empty()) << "max_timestamp empty at level " << level;
if (!min_ts.empty() && !max_ts.empty()) {
uint64_t file_min = DecodeFixed64(min_ts.data());
uint64_t file_max = DecodeFixed64(max_ts.data());
overall_min = std::min(overall_min, file_min);
overall_max = std::max(overall_max, file_max);
}
}
return {overall_min, overall_max};
}
// Helper to verify timestamp range matches expected values, including after
// reopen
void VerifyTimestampRangeWithPersistence(const Options& options,
uint64_t expected_min,
uint64_t expected_max) {
// Verify before reopen
auto [min_ts, max_ts] = GetOverallTimestampRange();
ASSERT_EQ(expected_min, min_ts);
ASSERT_EQ(expected_max, max_ts);
size_t file_count_before = GetAllFileTimestamps().size();
// Verify manifest persistence by reopening
Reopen(options);
// Verify after reopen
auto [reopened_min_ts, reopened_max_ts] = GetOverallTimestampRange();
ASSERT_EQ(expected_min, reopened_min_ts);
ASSERT_EQ(expected_max, reopened_max_ts);
ASSERT_EQ(file_count_before, GetAllFileTimestamps().size());
}
// Helper to create common options for UDT tests with level compaction
Options CreateTimestampOptions(bool disable_auto_compactions = false) {
Options options = CurrentOptions();
options.env = env_;
options.compaction_style = kCompactionStyleLevel;
options.num_levels = 4;
options.persist_user_defined_timestamps = true;
options.comparator = test::BytewiseComparatorWithU64TsWrapper();
options.disable_auto_compactions = disable_auto_compactions;
return options;
}
// Helper to write test data with alternating timestamps in a range
// Writes keys [start_key, end_key) with timestamps alternating between
// min_ts and max_ts
void WriteDataWithTimestampRange(int start_key, int end_key, uint64_t min_ts,
uint64_t max_ts) {
std::string ts_buf;
for (int i = start_key; i < end_key; i++) {
ts_buf.clear();
uint64_t ts = (i % 2 == 0) ? min_ts : max_ts;
PutFixed64(&ts_buf, ts);
ASSERT_OK(db_->Put(WriteOptions(), Key(i), ts_buf,
"value" + std::to_string(i)));
}
}
// Helper to check if any file has the expected timestamp range
bool HasFileWithTimestampRange(uint64_t expected_min, uint64_t expected_max) {
auto file_timestamps = GetAllFileTimestamps();
for (const auto& [level, min_ts, max_ts] : file_timestamps) {
if (!min_ts.empty() && !max_ts.empty()) {
uint64_t file_min = DecodeFixed64(min_ts.data());
uint64_t file_max = DecodeFixed64(max_ts.data());
if (file_min == expected_min && file_max == expected_max) {
return true;
}
}
}
return false;
}
// Helper to verify data is readable with a given timestamp
void VerifyDataReadable(int key, const std::string& expected_value,
uint64_t read_ts) {
std::string value;
std::string ts_buf;
PutFixed64(&ts_buf, read_ts);
ReadOptions read_opts;
Slice ts_slice(ts_buf);
read_opts.timestamp = &ts_slice;
ASSERT_OK(db_->Get(read_opts, Key(key), &value));
ASSERT_EQ(expected_value, value);
}
};
TEST_F(TimestampCompatibleCompactionTest, UserKeyCrossFileBoundary) {
@@ -344,6 +464,385 @@ TEST_F(TimestampCompatibleCompactionTest, EmptyCompactionOutput) {
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
}
TEST_F(TimestampCompatibleCompactionTest, SeqnoZeroingWithUDT) {
// This test validates that seqno is only zeroed when the timestamp is older
// than full_history_ts_low_. Before the fix, seqno was incorrectly zeroed
// even when UDT was enabled but timestamp wasn't old enough.
Options options = CurrentOptions();
options.env = env_;
options.comparator = test::BytewiseComparatorWithU64TsWrapper();
options.disable_auto_compactions = true;
DestroyAndReopen(options);
// Track seqno zeroing events and which keys are zeroed
std::set<std::string> zeroed_keys;
SyncPoint::GetInstance()->SetCallBack(
"CompactionIterator::PrepareOutput:ZeroingSeq", [&](void* arg) {
auto* ikey = static_cast<ParsedInternalKey*>(arg);
ASSERT_EQ(0, ikey->sequence);
// Extract user key without timestamp (last 8 bytes)
Slice user_key_with_ts = ikey->user_key;
std::string user_key =
user_key_with_ts.ToString().substr(0, user_key_with_ts.size() - 8);
zeroed_keys.insert(user_key);
});
SyncPoint::GetInstance()->EnableProcessing();
// Case 1: Test that seqno is NOT zeroed when full_history_ts_low is not set
// Write a key with timestamp 100
std::string ts_str = Timestamp(100);
ASSERT_OK(db_->Put(WriteOptions(), "key1", ts_str, "value1"));
ASSERT_OK(Flush());
zeroed_keys.clear();
{
CompactRangeOptions cro;
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
}
// With UDT enabled and no full_history_ts_low, seqno should NOT be zeroed
ASSERT_TRUE(zeroed_keys.empty());
// Case 2: Test that seqno IS zeroed when timestamp < full_history_ts_low
// Write a new key with timestamp 200
ts_str = Timestamp(200);
ASSERT_OK(db_->Put(WriteOptions(), "key2", ts_str, "value2"));
ASSERT_OK(Flush());
zeroed_keys.clear();
{
// Set full_history_ts_low to 300, so ts < 300 should be zeroed
std::string full_history_ts_low = Timestamp(300);
Slice ts_slice = full_history_ts_low;
CompactRangeOptions cro;
cro.full_history_ts_low = &ts_slice;
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
}
// key1 (ts=100) and key2 (ts=200) both have ts < 300, so both should be
// zeroed
ASSERT_EQ(2u, zeroed_keys.size());
ASSERT_TRUE(zeroed_keys.count("key1") > 0);
ASSERT_TRUE(zeroed_keys.count("key2") > 0);
// Case 3: Write a new key with timestamp >= full_history_ts_low
// and verify it is NOT zeroed while old keys are re-zeroed
ts_str = Timestamp(500);
ASSERT_OK(db_->Put(WriteOptions(), "key3", ts_str, "value3"));
ASSERT_OK(Flush());
zeroed_keys.clear();
{
// Set full_history_ts_low to 400
// key1 (ts=100) and key2 (ts=200) have ts < 400, will be re-processed
// key3 (ts=500) has ts >= 400, should NOT be zeroed
std::string full_history_ts_low = Timestamp(400);
Slice ts_slice = full_history_ts_low;
CompactRangeOptions cro;
cro.full_history_ts_low = &ts_slice;
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
}
// key3 should NOT appear in zeroed_keys since ts=500 >= 400
ASSERT_TRUE(zeroed_keys.count("key3") == 0);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
// Verify data is still readable
std::string value;
ts_str = Timestamp(600);
Slice read_ts = ts_str;
ReadOptions read_opts;
read_opts.timestamp = &read_ts;
ASSERT_OK(db_->Get(read_opts, "key1", &value));
ASSERT_EQ("value1", value);
ASSERT_OK(db_->Get(read_opts, "key2", &value));
ASSERT_EQ("value2", value);
ASSERT_OK(db_->Get(read_opts, "key3", &value));
ASSERT_EQ("value3", value);
}
// Test that files with max_timestamp >= full_history_ts_low are not marked
// for bottommost compaction, which prevents infinite compaction loops.
TEST_F(TimestampCompatibleCompactionTest,
BottommostCompactionRespectsFullHistoryTsLow) {
Options options = CreateTimestampOptions();
options.level0_file_num_compaction_trigger = 4;
DestroyAndReopen(options);
// Write some data with timestamps 100-199
std::string ts_buf;
for (int i = 0; i < 100; i++) {
ts_buf.clear();
PutFixed64(&ts_buf, 100 + i);
ASSERT_OK(
db_->Put(WriteOptions(), Key(i), ts_buf, "value" + std::to_string(i)));
}
ASSERT_OK(Flush());
// Compact to the bottommost level
CompactRangeOptions cro;
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
// Set full_history_ts_low to 150 - files with max_ts >= 150 should NOT be
// marked for bottommost compaction since seqno cannot be zeroed
ts_buf.clear();
PutFixed64(&ts_buf, 150);
ASSERT_OK(db_->IncreaseFullHistoryTsLow(db_->DefaultColumnFamily(), ts_buf));
// Release a snapshot to potentially trigger bottommost file marking
// but files should NOT be marked because max_ts (199) >= full_history_ts_low
// (150)
const Snapshot* snap = db_->GetSnapshot();
db_->ReleaseSnapshot(snap);
// Wait for any scheduled compactions - should complete without infinite loop
// Use a reasonable timeout to detect infinite loops
WaitForCompactOptions wfc_options;
wfc_options.timeout = std::chrono::microseconds(5000000); // 5 seconds
Status s = dbfull()->WaitForCompact(wfc_options);
// Should succeed without timeout (no infinite compaction loop)
ASSERT_TRUE(s.ok() || s.IsTimedOut());
if (s.IsTimedOut()) {
// If timeout, the fix is not working - this should not happen
FAIL() << "WaitForCompact timed out - possible infinite compaction loop";
}
// Now set full_history_ts_low beyond max timestamp in the file (200+)
// This should allow the file to be properly marked and compacted
ts_buf.clear();
PutFixed64(&ts_buf, 300);
ASSERT_OK(db_->IncreaseFullHistoryTsLow(db_->DefaultColumnFamily(), ts_buf));
// Trigger another snapshot release to potentially mark files
snap = db_->GetSnapshot();
db_->ReleaseSnapshot(snap);
// Now compaction should clean up the file.
ASSERT_OK(dbfull()->TEST_WaitForCompact());
}
// Test that files are NOT marked for bottommost compaction when UDT is enabled
// and full_history_ts_low has never been set (empty).
TEST_F(TimestampCompatibleCompactionTest,
BottommostCompactionSkipsWhenFullHistoryTsLowNotSet) {
Options options = CreateTimestampOptions();
DestroyAndReopen(options);
// Write some data with timestamps 100-199
std::string ts_buf;
for (int i = 0; i < 100; i++) {
ts_buf.clear();
PutFixed64(&ts_buf, 100 + i);
ASSERT_OK(
db_->Put(WriteOptions(), Key(i), ts_buf, "value" + std::to_string(i)));
}
ASSERT_OK(Flush());
// Compact to the bottommost level without setting full_history_ts_low
CompactRangeOptions cro;
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
// Verify files have valid max_timestamp
auto file_timestamps = GetAllFileTimestamps();
ASSERT_GE(file_timestamps.size(), 1U);
for (const auto& [level, min_ts, max_ts] : file_timestamps) {
ASSERT_FALSE(max_ts.empty()) << "max_timestamp should not be empty";
}
// full_history_ts_low is NOT set (empty), so files should NOT be marked
// for bottommost compaction even after releasing a snapshot.
// This tests the branch: if (full_history_ts_low.empty()) { continue; }
const Snapshot* snap = db_->GetSnapshot();
db_->ReleaseSnapshot(snap);
// Wait for any scheduled compactions
ASSERT_OK(dbfull()->TEST_WaitForCompact());
// Now set full_history_ts_low to a value > max_timestamp (199) in the file
// This should allow the file to be properly marked and compacted
ts_buf.clear();
PutFixed64(&ts_buf, 300);
ASSERT_OK(db_->IncreaseFullHistoryTsLow(db_->DefaultColumnFamily(), ts_buf));
// Trigger another snapshot release to potentially mark files
snap = db_->GetSnapshot();
db_->ReleaseSnapshot(snap);
// Now compaction should be able to proceed since full_history_ts_low is set
ASSERT_OK(dbfull()->TEST_WaitForCompact());
// Verify data is still readable
VerifyDataReadable(0, "value0", 250);
}
// Test that ingested SST files created with UDT have their min/max timestamps
// properly extracted from table properties and populated in FileMetaData.
// This verifies the fix in external_sst_file_ingestion_job.cc that calls
// ExtractTimestampFromTableProperties after creating FileMetaData.
TEST_F(TimestampCompatibleCompactionTest,
IngestedFileTimestampsExtractedFromTableProperties) {
Options options = CreateTimestampOptions();
DestroyAndReopen(options);
// Create an SST file WITH timestamps using SstFileWriter
std::string sst_file = dbname_ + "/ingested_udt_file.sst";
const uint64_t kMinTs = 100;
const uint64_t kMaxTs = 200;
{
SstFileWriter sst_file_writer(EnvOptions(), options);
ASSERT_OK(sst_file_writer.Open(sst_file));
std::string ts_buf;
for (int i = 0; i < 10; i++) {
// Alternate between min and max timestamps
uint64_t ts = (i % 2 == 0) ? kMinTs : kMaxTs;
ts_buf.clear();
PutFixed64(&ts_buf, ts);
// SstFileWriter with UDT comparator requires key with timestamp
ASSERT_OK(
sst_file_writer.Put(Key(i), ts_buf, "value" + std::to_string(i)));
}
ASSERT_OK(sst_file_writer.Finish());
}
// Verify the SST file has timestamp properties before ingestion
{
std::unique_ptr<SstFileReader> reader(new SstFileReader(options));
ASSERT_OK(reader->Open(sst_file));
auto props = reader->GetTableProperties();
auto& user_collected = props->user_collected_properties;
ASSERT_TRUE(user_collected.find("rocksdb.timestamp_min") !=
user_collected.end())
<< "SST file should have rocksdb.timestamp_min property";
ASSERT_TRUE(user_collected.find("rocksdb.timestamp_max") !=
user_collected.end())
<< "SST file should have rocksdb.timestamp_max property";
}
// Ingest the SST file
IngestExternalFileOptions ifo;
ifo.move_files = false;
ASSERT_OK(db_->IngestExternalFile({sst_file}, ifo));
// Verify the ingested file has proper timestamps in FileMetaData
ASSERT_TRUE(HasFileWithTimestampRange(kMinTs, kMaxTs))
<< "Ingested file should have min_timestamp=" << kMinTs
<< " and max_timestamp=" << kMaxTs << " in FileMetaData";
// Verify timestamps persist after reopen
Reopen(options);
ASSERT_TRUE(HasFileWithTimestampRange(kMinTs, kMaxTs))
<< "Ingested file timestamps should persist after reopen";
// Verify data is readable
VerifyDataReadable(0, "value0", kMaxTs);
// Clean up
ASSERT_OK(env_->DeleteFile(sst_file));
}
// Test that min/max timestamps are correctly tracked in FileMetaData and
// persisted in the manifest during flush.
TEST_F(TimestampCompatibleCompactionTest, TimestampRangePersistenceFlush) {
Options options = CreateTimestampOptions();
DestroyAndReopen(options);
// Expected timestamp range
const uint64_t kMinTs = 100;
const uint64_t kMaxTs = 200;
// Write data with specific timestamp range
WriteDataWithTimestampRange(0, 50, kMinTs, kMaxTs);
ASSERT_OK(Flush());
// First verify table properties have the timestamps
// (this confirms TimestampTablePropertiesCollector is working)
TablePropertiesCollection props;
ASSERT_OK(db_->GetPropertiesOfAllTables(&props));
ASSERT_EQ(1U, props.size());
for (const auto& item : props) {
auto& user_collected = item.second->user_collected_properties;
ASSERT_TRUE(user_collected.find("rocksdb.timestamp_min") !=
user_collected.end());
ASSERT_TRUE(user_collected.find("rocksdb.timestamp_max") !=
user_collected.end());
// Verify the collected timestamps match expected values
std::string collected_min_ts = user_collected.at("rocksdb.timestamp_min");
std::string collected_max_ts = user_collected.at("rocksdb.timestamp_max");
ASSERT_EQ(kMinTs, DecodeFixed64(collected_min_ts.data()));
ASSERT_EQ(kMaxTs, DecodeFixed64(collected_max_ts.data()));
}
// Verify FileMetaData timestamps and persistence through reopen
VerifyTimestampRangeWithPersistence(options, kMinTs, kMaxTs);
// Verify we can still read the data
VerifyDataReadable(0, "value0", kMaxTs);
}
// Test that min/max timestamps are correctly merged during compaction
// and persisted in the manifest.
TEST_F(TimestampCompatibleCompactionTest, TimestampRangePersistenceCompaction) {
Options options = CreateTimestampOptions(true /* disable_auto_compactions */);
DestroyAndReopen(options);
// Create multiple L0 files with different timestamp ranges
// File 1: timestamps 100-150
const uint64_t kFile1MinTs = 100;
const uint64_t kFile1MaxTs = 150;
WriteDataWithTimestampRange(0, 10, kFile1MinTs, kFile1MaxTs);
ASSERT_OK(Flush());
// File 2: timestamps 50-80 (earlier range)
const uint64_t kFile2MinTs = 50;
const uint64_t kFile2MaxTs = 80;
WriteDataWithTimestampRange(10, 20, kFile2MinTs, kFile2MaxTs);
ASSERT_OK(Flush());
// File 3: timestamps 200-300 (later range)
const uint64_t kFile3MinTs = 200;
const uint64_t kFile3MaxTs = 300;
WriteDataWithTimestampRange(20, 30, kFile3MinTs, kFile3MaxTs);
ASSERT_OK(Flush());
// Expected combined range: min=50, max=300
const uint64_t kExpectedMinTs = 50;
const uint64_t kExpectedMaxTs = 300;
// Verify we have 3 L0 files before compaction with valid timestamps
auto files_before = GetAllFileTimestamps();
ASSERT_EQ(3U, files_before.size());
for (const auto& [level, min_ts, max_ts] : files_before) {
ASSERT_EQ(0, level); // All files should be in L0
ASSERT_FALSE(min_ts.empty());
ASSERT_FALSE(max_ts.empty());
}
// Trigger compaction
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
ASSERT_OK(dbfull()->TEST_WaitForCompact());
// Verify timestamp range and persistence through reopen
VerifyTimestampRangeWithPersistence(options, kExpectedMinTs, kExpectedMaxTs);
// Verify data is still readable
VerifyDataReadable(0, "value0", kExpectedMaxTs);
VerifyDataReadable(15, "value15", kExpectedMaxTs);
VerifyDataReadable(25, "value25", kExpectedMaxTs);
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+3 -3
View File
@@ -741,7 +741,7 @@ TEST_P(DBWriteTest, LockWALConcurrentRecursive) {
ExternalSstFileInfo external_info;
ASSERT_OK(sst_file_writer.Finish(&external_info));
}
AcqRelAtomic<bool> parallel_ingest_completed{false};
Atomic<bool> parallel_ingest_completed{false};
port::Thread parallel_ingest{[&]() {
IngestExternalFileOptions ingest_opts;
ingest_opts.move_files = true; // faster than copy
@@ -750,7 +750,7 @@ TEST_P(DBWriteTest, LockWALConcurrentRecursive) {
parallel_ingest_completed.Store(true);
}};
AcqRelAtomic<bool> flush_completed{false};
Atomic<bool> flush_completed{false};
port::Thread parallel_flush{[&]() {
FlushOptions flush_opts;
// NB: Flush with wait=false case is tested above in LockWALInEffect
@@ -762,7 +762,7 @@ TEST_P(DBWriteTest, LockWALConcurrentRecursive) {
flush_completed.Store(true);
}};
AcqRelAtomic<bool> parallel_put_completed{false};
Atomic<bool> parallel_put_completed{false};
port::Thread parallel_put{[&]() {
// This can make certain failure scenarios more likely:
// sleep(1);
+11 -9
View File
@@ -158,15 +158,17 @@ Status UpdateManifestForFilesState(
// Current state inconsistent with manifest
++files_updated;
edit.DeleteFile(level, number);
edit.AddFile(
level, number, lf->fd.GetPathId(), lf->fd.GetFileSize(),
lf->smallest, lf->largest, lf->fd.smallest_seqno,
lf->fd.largest_seqno, lf->marked_for_compaction, temp,
lf->oldest_blob_file_number, lf->oldest_ancester_time,
lf->file_creation_time, lf->epoch_number, lf->file_checksum,
lf->file_checksum_func_name, lf->unique_id,
lf->compensated_range_deletion_size, lf->tail_size,
lf->user_defined_timestamps_persisted);
edit.AddFile(level, lf->fd.GetNumber(), lf->fd.GetPathId(),
lf->fd.GetFileSize(), lf->smallest, lf->largest,
lf->fd.smallest_seqno, lf->fd.largest_seqno,
lf->marked_for_compaction, temp,
lf->oldest_blob_file_number,
lf->oldest_ancester_time, lf->file_creation_time,
lf->epoch_number, lf->file_checksum,
lf->file_checksum_func_name, lf->unique_id,
lf->compensated_range_deletion_size, lf->tail_size,
lf->user_defined_timestamps_persisted,
lf->min_timestamp, lf->max_timestamp);
}
}
} else {
+22 -3
View File
@@ -11,6 +11,7 @@
#include <unordered_set>
#include <vector>
#include "db/builder.h"
#include "db/db_impl/db_impl.h"
#include "db/version_edit.h"
#include "file/file_util.h"
@@ -299,6 +300,18 @@ Status ExternalSstFileIngestionJob::Prepare(
// ingestion.
// TODO: plumb Env::IOActivity
ReadOptions ro;
// Pass user-provided checksums through FileOptions when available.
// The caller may not have provided checksums at all (empty vectors),
// so we guard with a bounds check.
FileOptions fopts;
if (i < files_checksums.size()) {
fopts.file_checksum = files_checksums[i];
}
if (i < files_checksum_func_names.size()) {
fopts.file_checksum_func_name = files_checksum_func_names[i];
} else {
fopts.file_checksum_func_name = kNoFileChecksumFuncName;
}
IOStatus io_s = GenerateOneFileChecksum(
fs_.get(), files_to_ingest_[i].internal_file_path,
db_options_.file_checksum_gen_factory.get(),
@@ -307,7 +320,7 @@ Status ExternalSstFileIngestionJob::Prepare(
ingestion_options_.verify_checksums_readahead_size,
db_options_.allow_mmap_reads, io_tracer_,
db_options_.rate_limiter.get(), ro, db_options_.stats,
db_options_.clock);
db_options_.clock, fopts);
if (!io_s.ok()) {
status = io_s;
ROCKS_LOG_WARN(db_options_.info_log,
@@ -699,9 +712,13 @@ Status ExternalSstFileIngestionJob::AssignLevelsForOneBatch(
? kReservedEpochNumberForFileIngestedBehind
: cfd_->NewEpochNumber(), // orders files ingested to L0
file->file_checksum, file->file_checksum_func_name, file->unique_id, 0,
tail_size, file->user_defined_timestamps_persisted);
tail_size, file->user_defined_timestamps_persisted, "", "");
f_metadata.temperature = file->file_temperature;
f_metadata.marked_for_compaction = marked_for_compaction;
// Extract min/max timestamps from table properties for UDT support.
// This ensures ingested files have proper timestamp ranges in FileMetaData,
// similar to files created by flush and compaction.
ExtractTimestampFromTableProperties(file->table_properties, &f_metadata);
edit_.AddFile(file->picked_level, f_metadata);
*batch_uppermost_level =
@@ -1477,13 +1494,15 @@ IOStatus ExternalSstFileIngestionJob::GenerateChecksumForIngestedFile(
// TODO: rate limit file reads for checksum calculation during file ingestion.
// TODO: plumb Env::IOActivity
ReadOptions ro;
FileOptions gen_fopts;
gen_fopts.file_checksum_func_name = kNoFileChecksumFuncName;
IOStatus io_s = GenerateOneFileChecksum(
fs_.get(), file_to_ingest->internal_file_path,
db_options_.file_checksum_gen_factory.get(), requested_checksum_func_name,
&file_checksum, &file_checksum_func_name,
ingestion_options_.verify_checksums_readahead_size,
db_options_.allow_mmap_reads, io_tracer_, db_options_.rate_limiter.get(),
ro, db_options_.stats, db_options_.clock);
ro, db_options_.stats, db_options_.clock, gen_fopts);
if (!io_s.ok()) {
ROCKS_LOG_WARN(
db_options_.info_log, "Failed to generate checksum for %s: %s",
+2 -1
View File
@@ -1095,7 +1095,8 @@ Status FlushJob::WriteLevel0Table() {
meta_.file_creation_time, meta_.epoch_number,
meta_.file_checksum, meta_.file_checksum_func_name,
meta_.unique_id, meta_.compensated_range_deletion_size,
meta_.tail_size, meta_.user_defined_timestamps_persisted);
meta_.tail_size, meta_.user_defined_timestamps_persisted,
meta_.min_timestamp, meta_.max_timestamp);
edit_->SetBlobFileAdditions(std::move(blob_file_additions));
}
// Piggyback FlushJobInfo on the first first flushed memtable.
+4 -2
View File
@@ -310,8 +310,10 @@ Status ImportColumnFamilyJob::GetIngestedFileInfo(
std::unique_ptr<FSRandomAccessFile> sst_file;
std::unique_ptr<RandomAccessFileReader> sst_file_reader;
status =
fs_->NewRandomAccessFile(external_file, env_options_, &sst_file, nullptr);
FileOptions fo{env_options_};
fo.file_checksum = file_meta.file_checksum;
fo.file_checksum_func_name = file_meta.file_checksum_func_name;
status = fs_->NewRandomAccessFile(external_file, fo, &sst_file, nullptr);
if (!status.ok()) {
return status;
}
+10 -10
View File
@@ -704,17 +704,17 @@ class Repairer {
VersionEdit dummy_edit;
for (const auto* table : cf_id_and_tables.second) {
// TODO(opt): separate out into multiple levels
const auto& meta = table->meta;
dummy_edit.AddFile(
0, table->meta.fd.GetNumber(), table->meta.fd.GetPathId(),
table->meta.fd.GetFileSize(), table->meta.smallest,
table->meta.largest, table->meta.fd.smallest_seqno,
table->meta.fd.largest_seqno, table->meta.marked_for_compaction,
table->meta.temperature, table->meta.oldest_blob_file_number,
table->meta.oldest_ancester_time, table->meta.file_creation_time,
table->meta.epoch_number, table->meta.file_checksum,
table->meta.file_checksum_func_name, table->meta.unique_id,
table->meta.compensated_range_deletion_size, table->meta.tail_size,
table->meta.user_defined_timestamps_persisted);
0, meta.fd.GetNumber(), meta.fd.GetPathId(), meta.fd.GetFileSize(),
meta.smallest, meta.largest, meta.fd.smallest_seqno,
meta.fd.largest_seqno, meta.marked_for_compaction, meta.temperature,
meta.oldest_blob_file_number, meta.oldest_ancester_time,
meta.file_creation_time, meta.epoch_number, meta.file_checksum,
meta.file_checksum_func_name, meta.unique_id,
meta.compensated_range_deletion_size, meta.tail_size,
meta.user_defined_timestamps_persisted, meta.min_timestamp,
meta.max_timestamp);
}
s = dummy_version_builder.Apply(&dummy_edit);
if (s.ok()) {
+3 -2
View File
@@ -98,6 +98,8 @@ Status TableCache::GetTableReader(
std::unique_ptr<FSRandomAccessFile> file;
FileOptions fopts = file_options;
fopts.temperature = file_temperature;
fopts.file_checksum = file_meta.file_checksum;
fopts.file_checksum_func_name = file_meta.file_checksum_func_name;
Status s = PrepareIOFromReadOptions(ro, ioptions_.clock, fopts.io_options);
TEST_SYNC_POINT_CALLBACK("TableCache::GetTableReader:BeforeOpenFile",
const_cast<Status*>(&s));
@@ -113,8 +115,7 @@ Status TableCache::GetTableReader(
Status temp_s =
PrepareIOFromReadOptions(ro, ioptions_.clock, fopts.io_options);
if (temp_s.ok()) {
temp_s = ioptions_.fs->NewRandomAccessFile(fname, file_options, &file,
nullptr);
temp_s = ioptions_.fs->NewRandomAccessFile(fname, fopts, &file, nullptr);
}
if (temp_s.ok()) {
RecordTick(ioptions_.stats, NO_FILE_OPENS);
+2 -1
View File
@@ -76,7 +76,8 @@ class VersionBuilderTest : public testing::Test {
oldest_blob_file_number, kUnknownOldestAncesterTime,
kUnknownFileCreationTime, epoch_number, kUnknownFileChecksum,
kUnknownFileChecksumFuncName, kNullUniqueId64x2, 0, 0,
/* user_defined_timestamps_persisted */ true);
/* user_defined_timestamps_persisted */ true, /* min timestamp */ "",
/* max timestamp */ "");
f->compensated_file_size = file_size;
f->num_entries = num_entries;
f->num_deletions = num_deletions;
+15
View File
@@ -304,6 +304,15 @@ void VersionEdit::EncodeToNewFile4(const FileMetaData& f, int level,
char p = static_cast<char>(0);
PutLengthPrefixedSlice(dst, Slice(&p, 1));
}
// Encode min/max timestamp if they are non-empty
if (!f.min_timestamp.empty()) {
PutVarint32(dst, NewFileCustomTag::kMinTimestamp);
PutLengthPrefixedSlice(dst, Slice(f.min_timestamp));
}
if (!f.max_timestamp.empty()) {
PutVarint32(dst, NewFileCustomTag::kMaxTimestamp);
PutLengthPrefixedSlice(dst, Slice(f.max_timestamp));
}
TEST_SYNC_POINT_CALLBACK("VersionEdit::EncodeTo:NewFile4:CustomizeFields",
dst);
@@ -443,6 +452,12 @@ const char* VersionEdit::DecodeNewFile4From(Slice* input, int& max_level,
}
f.user_defined_timestamps_persisted = (field[0] == 1);
break;
case kMinTimestamp:
f.min_timestamp = field.ToString();
break;
case kMaxTimestamp:
f.max_timestamp = field.ToString();
break;
default:
if ((custom_tag & kCustomTagNonSafeIgnoreMask) != 0) {
// Should not proceed if cannot understand it
+26 -11
View File
@@ -276,6 +276,14 @@ struct FileMetaData {
// false, it's explicitly written to Manifest.
bool user_defined_timestamps_persisted = true;
// Minimum user-defined timestamp in the file. Empty if no UDT or unknown.
// This is populated from the table properties "rocksdb.timestamp_min".
std::string min_timestamp;
// Maximum user-defined timestamp in the file. Empty if no UDT or unknown.
// This is populated from the table properties "rocksdb.timestamp_max".
std::string max_timestamp;
FileMetaData() = default;
FileMetaData(uint64_t file, uint32_t file_path_id, uint64_t file_size,
@@ -288,7 +296,9 @@ struct FileMetaData {
const std::string& _file_checksum_func_name,
UniqueId64x2 _unique_id,
const uint64_t _compensated_range_deletion_size,
uint64_t _tail_size, bool _user_defined_timestamps_persisted)
uint64_t _tail_size, bool _user_defined_timestamps_persisted,
const std::string& _min_timestamp,
const std::string& _max_timestamp)
: fd(file, file_path_id, file_size, smallest_seq, largest_seq),
smallest(smallest_key),
largest(largest_key),
@@ -303,7 +313,9 @@ struct FileMetaData {
file_checksum_func_name(_file_checksum_func_name),
unique_id(std::move(_unique_id)),
tail_size(_tail_size),
user_defined_timestamps_persisted(_user_defined_timestamps_persisted) {
user_defined_timestamps_persisted(_user_defined_timestamps_persisted),
min_timestamp(_min_timestamp),
max_timestamp(_max_timestamp) {
TEST_SYNC_POINT_CALLBACK("FileMetaData::FileMetaData", this);
}
@@ -386,7 +398,8 @@ struct FileMetaData {
usage += sizeof(*this);
#endif // ROCKSDB_MALLOC_USABLE_SIZE
usage += smallest.size() + largest.size() + file_checksum.size() +
file_checksum_func_name.size();
file_checksum_func_name.size() + min_timestamp.size() +
max_timestamp.size();
return usage;
}
@@ -737,17 +750,19 @@ class VersionEdit {
const std::string& file_checksum_func_name,
const UniqueId64x2& unique_id,
const uint64_t compensated_range_deletion_size,
uint64_t tail_size, bool user_defined_timestamps_persisted) {
uint64_t tail_size, bool user_defined_timestamps_persisted,
const std::string& min_timestamp = "",
const std::string& max_timestamp = "") {
assert(smallest_seqno <= largest_seqno);
new_files_.emplace_back(
level,
FileMetaData(file, file_path_id, file_size, smallest, largest,
smallest_seqno, largest_seqno, marked_for_compaction,
temperature, oldest_blob_file_number, oldest_ancester_time,
file_creation_time, epoch_number, file_checksum,
file_checksum_func_name, unique_id,
compensated_range_deletion_size, tail_size,
user_defined_timestamps_persisted));
FileMetaData(
file, file_path_id, file_size, smallest, largest, smallest_seqno,
largest_seqno, marked_for_compaction, temperature,
oldest_blob_file_number, oldest_ancester_time, file_creation_time,
epoch_number, file_checksum, file_checksum_func_name, unique_id,
compensated_range_deletion_size, tail_size,
user_defined_timestamps_persisted, min_timestamp, max_timestamp));
files_to_quarantine_.push_back(file);
if (!HasLastSequence() || largest_seqno > GetLastSequence()) {
SetLastSequence(largest_seqno);
+47 -12
View File
@@ -1820,8 +1820,10 @@ Status Version::GetTableProperties(const ReadOptions& read_options,
file_name = TableFileName(ioptions.cf_paths, file_meta->fd.GetNumber(),
file_meta->fd.GetPathId());
}
s = ioptions.fs->NewRandomAccessFile(file_name, file_options_, &file,
nullptr);
FileOptions fopts = file_options_;
fopts.file_checksum = file_meta->file_checksum;
fopts.file_checksum_func_name = file_meta->file_checksum_func_name;
s = ioptions.fs->NewRandomAccessFile(file_name, fopts, &file, nullptr);
if (!s.ok()) {
return s;
}
@@ -3444,7 +3446,7 @@ bool Version::MaybeInitializeFileMetaData(const ReadOptions& read_options,
// Ensure new invariants on old files
file_meta->num_deletions =
std::max(tp->num_deletions, tp->num_range_deletions);
file_meta->num_entries = std::max(tp->num_entries, tp->num_deletions);
file_meta->num_entries = std::max(tp->num_entries, file_meta->num_deletions);
return true;
}
@@ -3737,7 +3739,8 @@ bool ShouldChangeFileTemperature(const ImmutableOptions& ioptions,
void VersionStorageInfo::ComputeCompactionScore(
const ImmutableOptions& immutable_options,
const MutableCFOptions& mutable_cf_options) {
const MutableCFOptions& mutable_cf_options,
const std::string& full_history_ts_low) {
double total_downcompact_bytes = 0.0;
// Historically, score is defined as actual bytes in a level divided by
// the level's target size, and 1.0 is the threshold for triggering
@@ -3936,7 +3939,8 @@ void VersionStorageInfo::ComputeCompactionScore(
ComputeFilesMarkedForCompaction(max_output_level);
ComputeBottommostFilesMarkedForCompaction(
immutable_options.cf_allow_ingest_behind ||
immutable_options.allow_ingest_behind);
immutable_options.allow_ingest_behind,
immutable_options.user_comparator, full_history_ts_low);
ComputeExpiredTtlFiles(immutable_options, mutable_cf_options.ttl);
ComputeFilesMarkedForPeriodicCompaction(
immutable_options, mutable_cf_options.periodic_compaction_seconds,
@@ -4527,17 +4531,20 @@ void VersionStorageInfo::GenerateFileLocationIndex() {
}
}
void VersionStorageInfo::UpdateOldestSnapshot(SequenceNumber seqnum,
bool allow_ingest_behind) {
void VersionStorageInfo::UpdateOldestSnapshot(
SequenceNumber seqnum, bool allow_ingest_behind, const Comparator* ucmp,
const std::string& full_history_ts_low) {
assert(seqnum >= oldest_snapshot_seqnum_);
oldest_snapshot_seqnum_ = seqnum;
if (oldest_snapshot_seqnum_ > bottommost_files_mark_threshold_) {
ComputeBottommostFilesMarkedForCompaction(allow_ingest_behind);
ComputeBottommostFilesMarkedForCompaction(allow_ingest_behind, ucmp,
full_history_ts_low);
}
}
void VersionStorageInfo::ComputeBottommostFilesMarkedForCompaction(
bool allow_ingest_behind) {
bool allow_ingest_behind, const Comparator* ucmp,
const std::string& full_history_ts_low) {
bottommost_files_marked_for_compaction_.clear();
bottommost_files_mark_threshold_ = kMaxSequenceNumber;
if (allow_ingest_behind) {
@@ -4558,12 +4565,39 @@ void VersionStorageInfo::ComputeBottommostFilesMarkedForCompaction(
current_time - static_cast<int64_t>(bottommost_file_compaction_delay_);
}
// For UDT, we need to check if the file's max timestamp is below
// full_history_ts_low. If not, the compaction won't be able to collapse the
// timestamp to clean up the tombstone , so marking the file would be futile
// and could cause an infinite compaction loop.
const bool has_udt = ucmp && ucmp->timestamp_size() > 0;
for (auto& level_and_file : bottommost_files_) {
if (!level_and_file.second->being_compacted &&
level_and_file.second->fd.largest_seqno != 0) {
// largest_seqno might be nonzero due to containing the final key in an
// earlier compaction, whose seqnum we didn't zero out.
if (level_and_file.second->fd.largest_seqno < oldest_snapshot_seqnum_) {
if (has_udt) {
const std::string& max_ts = level_and_file.second->max_timestamp;
// If max_timestamp is empty, the file could come from very old
// version which does not have timestamp. In that case, we should pick
// the file for compaction. After compaction, the file will have
// max_timestamp set propertly.
if (!max_ts.empty()) {
// If full_history_ts_low is empty, it means it was never set, which
// means its value is 0. Therefore, it would be always smaller than
// max_timestamp
if (full_history_ts_low.empty()) {
continue;
}
// If max timestamp >= full_history_ts_low, skip this file
if (ucmp->CompareTimestamp(Slice(max_ts), full_history_ts_low) >=
0) {
continue;
}
}
}
if (!needs_delay) {
bottommost_files_marked_for_compaction_.push_back(level_and_file);
} else if (creation_time_ub > 0) {
@@ -5639,7 +5673,8 @@ void VersionSet::AppendVersion(ColumnFamilyData* column_family_data,
// compute new compaction score
v->storage_info()->ComputeCompactionScore(
column_family_data->ioptions(),
column_family_data->GetLatestMutableCFOptions());
column_family_data->GetLatestMutableCFOptions(),
column_family_data->GetFullHistoryTsLow());
// Mark v finalized
v->storage_info_.SetFinalized();
@@ -7102,7 +7137,6 @@ Status VersionSet::WriteCurrentStateToManifest(
for (const auto& f : level_files) {
assert(f);
edit.AddFile(level, f->fd.GetNumber(), f->fd.GetPathId(),
f->fd.GetFileSize(), f->smallest, f->largest,
f->fd.smallest_seqno, f->fd.largest_seqno,
@@ -7111,7 +7145,8 @@ Status VersionSet::WriteCurrentStateToManifest(
f->file_creation_time, f->epoch_number, f->file_checksum,
f->file_checksum_func_name, f->unique_id,
f->compensated_range_deletion_size, f->tail_size,
f->user_defined_timestamps_persisted);
f->user_defined_timestamps_persisted, f->min_timestamp,
f->max_timestamp);
}
}
+12 -3
View File
@@ -200,7 +200,8 @@ class VersionStorageInfo {
// REQUIRES: db_mutex held!!
// TODO find a better way to pass compaction_options_fifo.
void ComputeCompactionScore(const ImmutableOptions& immutable_options,
const MutableCFOptions& mutable_cf_options);
const MutableCFOptions& mutable_cf_options,
const std::string& full_history_ts_low);
// Estimate est_comp_needed_bytes_
void EstimateCompactionBytesNeeded(
@@ -230,8 +231,15 @@ class VersionStorageInfo {
// oldest snapshot changes as that is when bottom-level files can become
// eligible for compaction.
//
// For columns with User Defined Timestamps (UDT), also checks that the
// file's largest timestamp is below full_history_ts_low before marking,
// since compaction can only collapse timestamp when it is below this
// threshold.
//
// REQUIRES: DB mutex held
void ComputeBottommostFilesMarkedForCompaction(bool allow_ingest_behind);
void ComputeBottommostFilesMarkedForCompaction(
bool allow_ingest_behind, const Comparator* ucmp,
const std::string& full_history_ts_low);
// This computes files_marked_for_forced_blob_gc_ and is called by
// ComputeCompactionScore()
@@ -248,7 +256,8 @@ class VersionStorageInfo {
// files marked for compaction.
// REQUIRES: DB mutex held
void UpdateOldestSnapshot(SequenceNumber oldest_snapshot_seqnum,
bool allow_ingest_behind);
bool allow_ingest_behind, const Comparator* ucmp,
const std::string& full_history_ts_low);
int MaxInputLevel() const;
int MaxOutputLevel(bool allow_ingest_behind) const;
+18 -9
View File
@@ -56,7 +56,8 @@ class GenerateLevelFilesBriefTest : public testing::Test {
kInvalidBlobFileNumber, kUnknownOldestAncesterTime,
kUnknownFileCreationTime, kUnknownEpochNumber, kUnknownFileChecksum,
kUnknownFileChecksumFuncName, kNullUniqueId64x2, 0, 0,
/* user_defined_timestamps_persisted */ true);
/* user_defined_timestamps_persisted */ true, /* min timestamp */ "",
/* max timestamp */ "");
files_.push_back(f);
}
@@ -172,7 +173,8 @@ class VersionStorageInfoTestBase : public testing::Test {
kUnknownOldestAncesterTime, kUnknownFileCreationTime,
kUnknownEpochNumber, kUnknownFileChecksum, kUnknownFileChecksumFuncName,
kNullUniqueId64x2, compensated_range_deletion_size, 0,
/* user_defined_timestamps_persisted */ true);
/* user_defined_timestamps_persisted */ true, /* min timestamp */ "",
/* max timestamp */ "");
vstorage_.AddFile(level, f);
}
@@ -391,7 +393,8 @@ TEST_F(VersionStorageInfoTest, MaxBytesForLevelDynamicWithLargeL0_1) {
ASSERT_EQ(51450U, vstorage_.MaxBytesForLevel(3));
ASSERT_EQ(257250U, vstorage_.MaxBytesForLevel(4));
vstorage_.ComputeCompactionScore(ioptions_, mutable_cf_options_);
vstorage_.ComputeCompactionScore(ioptions_, mutable_cf_options_,
/*full_history_ts_low=*/"");
// Only L0 hits compaction.
ASSERT_EQ(vstorage_.CompactionScoreLevel(0), 0);
}
@@ -421,7 +424,8 @@ TEST_F(VersionStorageInfoTest, MaxBytesForLevelDynamicWithLargeL0_2) {
ASSERT_EQ(51450U, vstorage_.MaxBytesForLevel(3));
ASSERT_EQ(257250U, vstorage_.MaxBytesForLevel(4));
vstorage_.ComputeCompactionScore(ioptions_, mutable_cf_options_);
vstorage_.ComputeCompactionScore(ioptions_, mutable_cf_options_,
/*full_history_ts_low=*/"");
// Although L2 and l3 have higher unadjusted compaction score, considering
// a relatively large L0 being compacted down soon, L4 is picked up for
// compaction.
@@ -453,7 +457,8 @@ TEST_F(VersionStorageInfoTest, MaxBytesForLevelDynamicWithLargeL0_3) {
ASSERT_EQ(2, vstorage_.base_level());
ASSERT_EQ(20000U, vstorage_.MaxBytesForLevel(2));
vstorage_.ComputeCompactionScore(ioptions_, mutable_cf_options_);
vstorage_.ComputeCompactionScore(ioptions_, mutable_cf_options_,
/*full_history_ts_low=*/"");
// Although L2 has higher unadjusted compaction score, considering
// a relatively large L0 being compacted down soon, L3 is picked up for
// compaction.
@@ -483,7 +488,8 @@ TEST_F(VersionStorageInfoTest, DrainUnnecessaryLevel) {
ASSERT_EQ(1, vstorage_.base_level());
ASSERT_EQ(1000, vstorage_.MaxBytesForLevel(1));
ASSERT_EQ(10100, vstorage_.MaxBytesForLevel(3));
vstorage_.ComputeCompactionScore(ioptions_, mutable_cf_options_);
vstorage_.ComputeCompactionScore(ioptions_, mutable_cf_options_,
/*full_history_ts_low=*/"");
// Tests that levels 1 and 3 are eligible for compaction.
// Levels 1 and 3 are much smaller than target size,
@@ -1346,7 +1352,8 @@ class VersionSetTestBase {
Temperature::kUnknown, info.oldest_blob_file_number, 0, 0,
info.epoch_number, kUnknownFileChecksum, kUnknownFileChecksumFuncName,
kNullUniqueId64x2, 0, 0,
/* user_defined_timestamps_persisted */ true);
/* user_defined_timestamps_persisted */ true, /* min timestamp */ "",
/* max timestamp */ "");
if (info.file_missing) {
ASSERT_OK(fs_->DeleteFile(fname, IOOptions(), nullptr));
}
@@ -3940,7 +3947,8 @@ TEST_F(VersionSetTestMissingFiles, ManifestFarBehindSst) {
largest_ikey, 0, 0, false, Temperature::kUnknown, 0, 0, 0,
file_num /* epoch_number */, kUnknownFileChecksum,
kUnknownFileChecksumFuncName, kNullUniqueId64x2, 0, 0,
/* user_defined_timestamps_persisted */ true);
/* user_defined_timestamps_persisted */ true, /* min timestamp */ "",
/* max timestamp */ "");
added_files.emplace_back(0, meta);
}
WriteFileAdditionAndDeletionToManifest(
@@ -4001,7 +4009,8 @@ TEST_F(VersionSetTestMissingFiles, ManifestAheadofSst) {
largest_ikey, 0, 0, false, Temperature::kUnknown, 0, 0, 0,
file_num /* epoch_number */, kUnknownFileChecksum,
kUnknownFileChecksumFuncName, kNullUniqueId64x2, 0, 0,
/* user_defined_timestamps_persisted */ true);
/* user_defined_timestamps_persisted */ true, /* min timestamp */ "",
/* max timestamp */ "");
added_files.emplace_back(0, meta);
}
WriteFileAdditionAndDeletionToManifest(
+19
View File
@@ -877,5 +877,24 @@ Status DestroyUnverifiedSubdir(const std::string& dirname) {
return s;
}
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;
// 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());
} else {
s = DestroyDB(db_path, options);
}
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);
}
} // namespace ROCKSDB_NAMESPACE
#endif // GFLAGS
+7
View File
@@ -100,6 +100,8 @@ DECLARE_bool(enable_pipelined_write);
DECLARE_bool(verify_before_write);
DECLARE_bool(histogram);
DECLARE_bool(destroy_db_initially);
DECLARE_bool(destroy_db_and_exit);
DECLARE_string(delete_dir_and_exit);
DECLARE_bool(verbose);
DECLARE_bool(progress_reports);
DECLARE_uint64(db_write_buffer_size);
@@ -820,5 +822,10 @@ Status SaveFilesInDirectory(const std::string& src_dirname,
const std::string& dst_dirname);
Status DestroyUnverifiedSubdir(const std::string& dirname);
Status InitUnverifiedSubdir(const std::string& dirname);
// Destroy the DB at the given path under the env configured for db_stress.
// Handles both regular DB and BlobDB, and cleans and removes the entire dir.
Status DbStressDestroyDb(const std::string& db_path);
} // namespace ROCKSDB_NAMESPACE
#endif // GFLAGS
+32
View File
@@ -9,8 +9,11 @@
#ifdef GFLAGS
#pragma once
#include "db_stress_tool/db_stress_common.h"
#include "file/filename.h"
#include "monitoring/thread_status_util.h"
#include "rocksdb/file_checksum.h"
namespace ROCKSDB_NAMESPACE {
namespace {
@@ -173,6 +176,35 @@ class DbStressFSWrapper : public FileSystemWrapper {
const FileOptions& file_opts,
std::unique_ptr<FSRandomAccessFile>* r,
IODebugContext* dbg) override {
// verify that file checksums are propagated through FileOptions
// for SST file opens.
std::string basename = f.substr(f.rfind('/') + 1);
uint64_t file_number;
FileType file_type;
if (ParseFileName(basename, &file_number, &file_type) &&
file_type == kTableFile) {
// file_checksum_func_name must always be populated to be sure each call
// site within RocksDB is intentional about populating the fields with the
// best available information:
// - kNoFileChecksumFuncName: no checksum context available
// (e.g., SstFileDumper, SstFileReader, checksum generation),
// always paired with empty checksum
// - kUnknownFileChecksumFuncName: file created without a
// checksum factory (from MANIFEST), always paired with
// empty checksum
// - a real name (e.g., "FileChecksumCrc32c"): checksum exists
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
assert(file_opts.file_checksum.empty());
} else {
// A real checksum function — checksum value must be present
assert(!file_opts.file_checksum.empty());
}
}
std::unique_ptr<FSRandomAccessFile> file;
IOStatus s = target()->NewRandomAccessFile(f, file_opts, &file, dbg);
if (s.ok()) {
+8
View File
@@ -135,6 +135,14 @@ DEFINE_bool(histogram, false, "Print histogram of operation timings");
DEFINE_bool(destroy_db_initially, true,
"Destroys the database dir before start if this is true");
DEFINE_bool(destroy_db_and_exit, false,
"Destroys the database dir and exits. Useful for cleanup without "
"running stress test. Other options are mostly ignored.");
DEFINE_string(delete_dir_and_exit, "",
"Recursively deletes the specified directory and exits. "
"Useful for cleaning up TEST_TMPDIR after crash tests.");
DEFINE_bool(verbose, false, "Verbose");
DEFINE_bool(progress_reports, true,
+1 -16
View File
@@ -77,22 +77,7 @@ StressTest::StressTest()
secondary_db_(nullptr),
is_db_stopped_(false) {
if (FLAGS_destroy_db_initially) {
std::vector<std::string> files;
db_stress_env->GetChildren(FLAGS_db, &files);
for (unsigned int i = 0; i < files.size(); i++) {
if (Slice(files[i]).starts_with("heap-")) {
db_stress_env->DeleteFile(FLAGS_db + "/" + files[i]);
}
}
Options options;
options.env = db_stress_env;
// Remove files without preserving manfiest files
const Status s = !FLAGS_use_blob_db
? DestroyDB(FLAGS_db, options)
: blob_db::DestroyBlobDB(FLAGS_db, options,
blob_db::BlobDBOptions());
const Status s = DbStressDestroyDb(FLAGS_db);
if (!s.ok()) {
fprintf(stderr, "Cannot destroy original db: %s\n", s.ToString().c_str());
exit(1);
+31 -2
View File
@@ -94,10 +94,39 @@ int db_stress_tool(int argc, char** argv) {
raw_env = fault_env_guard.get();
}
env_wrapper_guard = std::make_shared<CompositeEnvWrapper>(
raw_env, std::make_shared<DbStressFSWrapper>(raw_env->GetFileSystem()));
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
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;
}
}
// Handle --delete_dir_and_exit early, before other option validation
if (!FLAGS_delete_dir_and_exit.empty()) {
s = DestroyDir(raw_env, FLAGS_delete_dir_and_exit);
if (s.ok()) {
fprintf(stdout, "Successfully deleted directory %s\n",
FLAGS_delete_dir_and_exit.c_str());
return 0;
} else {
fprintf(stderr, "Failed to delete directory %s: %s\n",
FLAGS_delete_dir_and_exit.c_str(), s.ToString().c_str());
return 1;
}
}
FLAGS_rep_factory = StringToRepFactory(FLAGS_memtablerep.c_str());
// The number of background threads should be at least as much the
+11
View File
@@ -1904,6 +1904,17 @@ class NonBatchedOpsStressTest : public StressTest {
} while (!s.ok() && IsErrorInjectedAndRetryable(s) &&
initial_wal_write_may_succeed);
if ((s.IsDeadlock() || s.IsTimedOut()) &&
(FLAGS_use_multiget || FLAGS_use_multi_get_entity)) {
// Deadlock or timeout is ok, when multi get is tested. Because multi get
// tests execute MaybeAddKeyToTxnForRYW function which writes to the
// same key space but does not acquire stress test level mutex. So it is
// possible RocksDB returns deadlock or timeout. Return OK() for these
// cases
pending_expected_value.Rollback();
return Status::OK();
}
if (!s.ok()) {
pending_expected_value.Rollback();
if (IsErrorInjectedAndRetryable(s)) {
@@ -0,0 +1,279 @@
---
title: "BitFields API: Type-Safe Bit Packing for Lock-Free Data Structures"
layout: post
author: pdillinger
category: blog
---
Modern concurrent data structures increasingly rely on [atomic operations](https://en.cppreference.com/w/cpp/atomic/atomic) to avoid the overhead of locking. A valuable but under-utilized technique for maximizing the effectiveness of atomic operations is [bit packing](https://en.wikipedia.org/wiki/Bit_field)---fitting multiple logical fields into a single atomic variable for algorithmic simplicity and efficiency. However, language support for bit packing does not guarantee dense packing, and manually managing bit manipulation quickly becomes error-prone, especially when dealing with complex state machines.
To address this in RocksDB, we have developed a reusable **BitFields API**, a type-safe, zero-overhead abstraction for bit packing in C++. This works in conjunction with clean wrappers for `std::atomic` for powerful and relatively safe bit-packing of atomic data. For broader use, a [variant of the code](https://github.com/facebook/folly/pull/2549) has been proposed for adding to folly.
## The Problem: Managing Packed Bit Fields
Consider HyperClockCache, an essentially lock-free cache implementation in RocksDB, which was [refactored to use this BitFields API](https://github.com/facebook/rocksdb/pull/14154). It is a hash table built on *slots* that can each hold a cache entry and relevant metadata. For atomic simplicity and efficiency, all the essential metadata for each slot is packed into a single 64-bit value:
- The reference count and eviction metadata are together encoded into *acquire* and *release* counters, 30 bits each.
- The possible states of {*empty*, *under construction/destruction*, *occupied+visible*, and *occupied+invisible*} are encoded into three state bits (instead of two, for easier decoding and manipulation).
- A *hit* bit is used for secondary cache integration.
Traditionally, you might write code like this:
```cpp
// Old approach: manual bit manipulation
constexpr uint64_t kAcquireCounterShift = 0;
constexpr uint64_t kReleaseCounterShift = 30;
constexpr uint64_t kCounterMask = 0x3FFFFFFF;
constexpr uint64_t kHitBitShift = 60;
constexpr uint64_t kOccupiedShift = 61;
constexpr uint64_t kShareableShift = 62;
constexpr uint64_t kVisibleShift = 63;
constexpr uint64_t kStateShift = kOccupiedShift;
std::atomic<uint64_t> meta_;
bool IsUnderConstruction(uint64_t meta) const {
return (meta & (uint64_t{1} << kOccupiedShift)) && !(meta & (uint64_t{1} << kShareableShift));
}
// Getting fields
uint64_t meta = meta_.load(std::memory_order_acquire);
if (IsUnderConstruction(meta)) {
// ...
} else if ((meta >> kVisibleShift) & 1) {
uint32_t refcount =
static_cast<uint32_t>(((meta >> kAcquireCounterShift) -
(meta >> kReleaseCounterShift)) & kCounterMask);
// ...
}
// Setting fields
// Set the hit bit (relaxed)
meta_.fetch_or(uint64_t{1} << kHitBitShift, std::memory_order_relaxed);
// Set both counters to `new_count` (as in eviction processing)
uint64_t meta = meta_.load(std::memory_order_relaxed);
uint64_t new_meta =
(meta & ((uint64_t{1} << kHitBitShift) | (uint64_t{7} << kStateShift))) |
(new_count << kReleaseCounterShift) |
(new_count << kAcquireCounterShift);
bool success = meta_.compare_exchange_strong(meta, new_meta,
std::memory_order_acq_rel);
// Increment acquire counter by initial_countdown
old_meta = meta_.fetch_add((uint64_t{1} << kAcquireCounterShift) * initial_countdown,
std::memory_order_acq_rel);
```
This approach has several problems:
1. **Error-prone**: Easy to get masks and shifts wrong
2. **Maintenance burden**: Changes to field sizes require updating multiple constants
3. **Abstraction challenges**: Even if writing a full set of well-tested getters and setters to hide all the details, details can leak in to do things like update multiple fields in one non-CAS (compare-and-swap) atomic operation.
## New Solution: BitFields API
The BitFields API provides a declarative, type-safe way to define bit-packed structures. Here's how the same example looks with BitFields:
```cpp
// New approach: declarative bit fields. (Each field must reference the
// previous, so that the declaration machinery is simply stateless.)
struct SlotMeta : public BitFields<uint64_t, SlotMeta> {
using AcquireCounter = UnsignedBitField<SlotMeta, 30, NoPrevBitField>;
using ReleaseCounter = UnsignedBitField<SlotMeta, 30, AcquireCounter>;
using HitFlag = BoolBitField<SlotMeta, ReleaseCounter>;
using OccupiedFlag = BoolBitField<SlotMeta, HitFlag>;
using ShareableFlag = BoolBitField<SlotMeta, OccupiedFlag>;
using VisibleFlag = BoolBitField<SlotMeta, ShareableFlag>;
// Convenience helpers
bool IsUnderConstruction() const {
return Get<OccupiedFlag>() && !Get<ShareableFlag>();
}
};
BitFieldsAtomic<SlotMeta> meta_;
// Getting fields
SlotMeta state = meta_.Load();
if (state.IsUnderConstruction()) {
// ...
} else if (state.Get<SlotMeta::VisibleFlag>()) {
uint32_t refcount = state.Get<SlotMeta::AcquireCounter>() -
state.Get<SlotMeta::ReleaseCounter>();
// ...
}
// Setting fields
// Set the hit bit (relaxed)
meta_.ApplyRelaxed(SlotMeta::HitFlag::SetTransform());
// Set both counters to `new_count` (as in eviction processing)
SlotMeta meta = meta_.LoadRelaxed();
SlotMeta new_meta = meta;
new_meta.Set<SlotMeta::ReleaseCounter>(new_count);
new_meta.Set<SlotMeta::AcquireCounter>(new_count);
meta_.CasStrongRelaxed(meta, new_meta);
// Increment acquire counter by initial_countdown
auto add_acquire =
AcquireCounter::PlusTransformPromiseNoOverflow(initial_countdown);
meta_.Apply(add_acquire, &old_meta);
// Bonus: Atomic multi-field updates without compare-exchange
auto transform = AcquireCounter::PlusTransformPromiseNoOverflow(1) +
ReleaseCounter::PlusTransformPromiseNoOverflow(1);
meta_.Apply(transform);
```
## Key Features
### Type Safety and Self-Documentation
Each field has a specific type (`bool` for `BoolBitField`, appropriately-sized unsigned int for `UnsignedBitField`) and clear semantic meaning. The field definitions are self-documenting: you can immediately see how many bits each field occupies and in what order.
### [Zero Overhead](https://en.cppreference.com/w/cpp/language/Zero-overhead_principle)
Because of heavy use of templates and constexpr operations and the ability to satisfy multiple field reads or writes from a single atomic operation, we have seen no runtime overhead vs. hand-written bit manipulation, in RocksDB. In one case, we verified the assembly code was identical.
[For folly's LifoSem](https://github.com/facebook/folly/pull/2550), there was one case where an optimization hack with detected overflow from one field to another couldn't be replicated as efficiently with the BitFields API because it would violate overflow checking. For that case I dove into the underlying representation to bypass the BitFields overflow check.
### Atomic Operations with Transforms
One of the most powerful features is the ability to combine multiple field updates into a single atomic operation using "transforms", if they are all either (a) some combination of addition and subtraction, (b) bitwise-and, or (c) bitwise-or. For example:
```cpp
// Clear several but not all fields atomically
auto and_transform = Field1::AndTransform(0) +
Field2::ClearTransform() +
Field4::ClearTransform();
atomic_bitfields.Apply(and_transform, &old_state, &new_state);
...
// Set more than one boolean field atomically
auto or_transform = Field2::SetTransform() +
Field4::SetTransform();
atomic_bitfields.Apply(or_transform, &old_state, &new_state);
...
auto add_transform = Field1::PlusTransformPromiseNoOverflow(1) +
Field3::MinusTransformPromiseNoUnderflow(1);
atomic_bitfields.Apply(add_transform, &old_state, &new_state);
```
Each `Apply()` generates a single atomic operation (e.g., `fetch_add` or `fetch_or`) that updates all the specified fields, and optionally returns both the old and new values. This enables a number of hacks for atomic updates without CAS.
### Overflow Protection
The API includes built-in overflow detection in debug builds:
```cpp
// An assertion will fail in debug builds if the counter overflows
auto transform = Counter::PlusTransformPromiseNoOverflow(value);
atomic.Apply(transform);
```
For fields at the top of the underlying representation (where overflow doesn't affect other fields), overflow is explicitly ignored. (A compile time error is generated if you try to use `PlusTransformPromiseNoOverflow` on a field at the top of the representation or `PlusTransformIgnoreOverflow` on a field not at the top of the representation.)
```cpp
// For wraparound counters
auto transform = Counter::PlusTransformIgnoreOverflow(value);
```
This capability is used in a folly data structure called LifoSem, which [I have proposed to refactor](https://github.com/facebook/folly/pull/2550) to a proposed BitFields API variant for folly.
### Compare-and-Swap (CAS) Support
The atomic wrappers provide full CAS support for lock-free algorithms:
```cpp
SlotMeta expected = current_state;
SlotMeta desired = expected.With<Field1>(new_value).With<Field2>(true);
if (meta_.CasStrong(expected, desired)) {
// Successfully updated
...
}
```
### Atomic wrappers
The BitFields API includes two atomic wrappers: `RelaxedBitFieldsAtomic` and `BitFieldsAtomic`. However, RocksDB also has versions of these wrappers for regular `std::atomic` variables that help with memory ordering discipline: `RelaxedAtomic` and `Atomic` in `util/atomic.h`.
These wrappers help in a couple of ways:
* **Self-document intended memory order**: An atomic field generally has a single memory order that all or most operations should use, typically either `std::memory_order_relaxed` or `std::memory_order_acq_rel`.
* **More intentional memory orders and atomic operations**: The standard library's implicit conversions and default memory ordering (`memory_order_seq_cst`) make it easy to accidentally use sequential consistency with acquire/release ordering or even relaxed, which could hurt performance, and tend to hide where atomic operations are actually happening (e.g. implicit vs. explicit load).
For example, instead of writing:
```cpp
std::atomic<uint64_t> stat_counter;
stat_counter++; // Uses memory_order_seq_cst implicitly - maybe inefficient
```
You write:
```cpp
RelaxedAtomic<uint64_t> stat_counter;
stat_counter.FetchAddRelaxed(1); // Explicitly relaxed - appropriate for a diagnostic counter
```
Or for data providing synchronization:
```cpp
Atomic<size_t> refcount;
refcount.FetchAdd(1); // Standard acquire-release semantics for coordinating with other threads
```
These wrappers complement the BitFields atomic wrappers by providing the same ordering discipline for non-packed atomic variables throughout much of RocksDB, creating a more readable and less clunky approach to concurrent programming. Migrating remaining uses of `std::atomic` is an ongoing effort.
## Real-World Usage in RocksDB
The BitFields API was developed along with the revamped parallel compression in RocksDB, but with the intention to also clean up the HyperClockCache (HCC) implementation. With that migration complete, we can see the benefits. Specifically, **by packing more of the state machine into a single atomic value, the parallel algorithms became both simpler and more efficient.** Concurrent algorithms that could have blown up in their state space with elaborate interleavings between threads trying not to block each other, e.g. because of multi-step consensus on work assignments, were instead able to quickly and more easily make progress, e.g. with atomically clear work assignments.
### Before: Manual Bit Manipulation
The old HCC code was difficult to read and maintain. Many of the common read and update operations had manually written helper functions, but it was not practical to develop the full set of functions needed for rare cases. Consider this code that clears the "visible" flag on a slot when an entry is erased from subsequent lookups but might still be referenced:
```cpp
// Old HCC code, without atomic wrappers
uint64_t old_meta =
h->meta.fetch_and(~(uint64_t{ClockHandle::kStateVisibleBit}
<< ClockHandle::kStateShift), std::memory_order_acq_rel);
// Apply update to local copy
uint64_t new_meta = old_meta & ~(uint64_t{ClockHandle::kStateVisibleBit}
<< ClockHandle::kStateShift);
// New HCC code
SlotMeta old_meta, new_meta;
h->meta.Apply(SlotMeta::VisibleFlag::ClearTransform(), &old_meta, &new_meta);
```
Or this assertion that the acquire and release counters are different:
```cpp
// Old HCC code
uint64_t old_meta = ...;
assert(((old_meta >> ClockHandle::kAcquireCounterShift) &
ClockHandle::kCounterMask) !=
((old_meta >> ClockHandle::kReleaseCounterShift) &
ClockHandle::kCounterMask));
// New HCC code without single-purpose helper functions
SlotMeta old_meta = ...;
assert(old_meta.Get<SlotMeta::AcquireCounter>() !=
old_meta.Get<SlotMeta::ReleaseCounter>());
// New HCC code, with single-purpose helper functions
SlotMeta old_meta = ...;
assert(old_meta.GetAcquireCounter() != old_meta.GetReleaseCounter());
```
Some hand-written helper functions or using directives are still useful for brevity, but even without them all the bit manipulation details are hidden in the BitFields implementation.
## Future Directions
We hope the proposed folly version is accepted to make the BitFields API available for broader usage. Additionally, some quality-of-life improvements are likely possible, perhaps including easier declaration and usage syntax, hopefully without delving into boost-like macro hell. Better runtime and compile time checks might also be possible.
## Conclusion
The BitFields API demonstrates that zero-overhead abstractions can significantly improve code quality without sacrificing performance. By providing type safety, self-documentation, and convenience features around bit manipulation and atomic operations, it makes lock-free programming more accessible and maintainable. Bit-packed atomics are arguably essential for *slaying the complexity dragon* of efficient lock-free and low-lock algorithms, because they reduce explosion in algorithm states.
For RocksDB specifically, the migration to BitFields has made the HyperClockCache implementation substantially easier to understand and modify, while maintaining the same high-performance characteristics. Combined with the recent [parallel compression revamp](/blog/2025/10/08/parallel-compression-revamp.html), these improvements showcase our ongoing commitment to writing clean, efficient, and maintainable code.
The BitFields API is available in RocksDB's util/bit_fields.h and can be adapted for use in other projects requiring efficient, type-safe bit packing. For those building high-performance concurrent systems, it offers a compelling alternative to manual bit manipulation—proving that safe abstractions and peak performance are not mutually exclusive.
+363
View File
@@ -41,6 +41,9 @@
#include "env/env_chroot.h"
#include "env/env_encryption_ctr.h"
#include "env/fs_readonly.h"
#if defined(ROCKSDB_IOURING_PRESENT)
#include "env/io_posix.h"
#endif
#include "env/mock_env.h"
#include "env/unique_id_gen.h"
#include "logging/log_buffer.h"
@@ -3725,6 +3728,366 @@ TEST_F(TestAsyncRead, InterleavingIOUringOperations) {
#endif
}
// Helper function to run AbortIO test with parameterized read requests.
// Each request is specified as {offset, length}.
// use_direct_io: if true, opens the file with O_DIRECT to bypass page cache.
// iterations: number of times to repeat the test (useful for race conditions).
void TestAbortIOWithRequests(
Env* env, size_t file_size,
const std::vector<std::pair<uint64_t, size_t>>& read_specs,
bool use_direct_io = false, int iterations = 1) {
#if defined(ROCKSDB_IOURING_PRESENT)
fprintf(stderr,
"TestAbortIOWithRequests: file_size=%zu, num_reads=%zu, "
"direct_io=%d, iterations=%d\n",
file_size, read_specs.size(), use_direct_io, iterations);
std::shared_ptr<FileSystem> fs = env->GetFileSystem();
std::string fname = test::PerThreadDBPath(env, "testfile_abortio");
// 1. Create test file once (content doesn't change between iterations)
{
std::unique_ptr<FSWritableFile> wfile;
FileOptions file_opts;
file_opts.use_direct_writes = true;
ASSERT_OK(fs->NewWritableFile(fname, file_opts, &wfile, nullptr));
// Query the file's required buffer alignment (logical block size)
// instead of hardcoding 4096, to support devices with different
// sector sizes.
size_t sector_size = wfile->GetRequiredBufferAlignment();
// Round up to full sectors for direct IO writes
size_t num_sectors = (file_size + sector_size - 1) / sector_size;
for (size_t i = 0; i < num_sectors; ++i) {
auto data = NewAligned(sector_size, static_cast<char>(i + 1));
Slice slice(data.get(), sector_size);
ASSERT_OK(wfile->Append(slice, IOOptions(), nullptr));
}
// Truncate to exact file size if not aligned to sector boundary
if (file_size % sector_size != 0) {
ASSERT_OK(wfile->Truncate(file_size, IOOptions(), nullptr));
}
ASSERT_OK(wfile->Close(IOOptions(), nullptr));
}
for (int iter = 0; iter < iterations; iter++) {
// 2. Submit ReadAsync requests and immediately abort
{
FileOptions file_opts;
file_opts.use_direct_reads = use_direct_io;
std::unique_ptr<FSRandomAccessFile> file;
ASSERT_OK(fs->NewRandomAccessFile(fname, file_opts, &file, nullptr));
const size_t num_reads = read_specs.size();
IOOptions opts;
std::vector<void*> io_handles(num_reads);
std::vector<FSReadRequest> reqs(num_reads);
std::vector<std::unique_ptr<char, Deleter>> data;
std::vector<size_t> vals;
IOHandleDeleter del_fn;
std::atomic<int> callbacks_invoked{0};
// Initialize read requests from specs
for (size_t i = 0; i < num_reads; i++) {
reqs[i].offset = read_specs[i].first;
reqs[i].len = read_specs[i].second;
data.emplace_back(NewAligned(reqs[i].len, 0));
reqs[i].scratch = data.back().get();
vals.push_back(i);
}
// Callback
std::function<void(FSReadRequest&, void*)> callback =
[&](FSReadRequest& req, void* cb_arg) {
size_t i = *(reinterpret_cast<size_t*>(cb_arg));
reqs[i].status = req.status;
callbacks_invoked++;
};
// Submit all ReadAsync requests
for (size_t i = 0; i < num_reads; i++) {
void* cb_arg = static_cast<void*>(&(vals[i]));
IOStatus s = file->ReadAsync(reqs[i], opts, callback, cb_arg,
&(io_handles[i]), &del_fn, nullptr);
if (s.IsNotSupported()) {
// io_uring not supported, clean up and skip
fprintf(stderr,
"WARNING: io_uring not supported, skipping test: %s\n",
s.ToString().c_str());
for (size_t j = 0; j < i; j++) {
if (io_handles[j]) {
del_fn(io_handles[j]);
}
}
ASSERT_OK(fs->DeleteFile(fname, IOOptions(), nullptr));
return;
}
ASSERT_OK(s);
}
// Immediately call AbortIO - this should NOT hang
ASSERT_OK(fs->AbortIO(io_handles));
// Verify all handles are finished and all callbacks were invoked.
// Since all handles are passed to AbortIO, every handle is guaranteed
// to be finalized (either completed or cancelled).
for (size_t i = 0; i < num_reads; i++) {
Posix_IOHandle* h = static_cast<Posix_IOHandle*>(io_handles[i]);
ASSERT_TRUE(h->is_finished);
}
ASSERT_EQ(callbacks_invoked.load(), static_cast<int>(num_reads));
// Clean up handles
for (size_t i = 0; i < num_reads; i++) {
if (io_handles[i]) {
del_fn(io_handles[i]);
}
}
}
}
ASSERT_OK(fs->DeleteFile(fname, IOOptions(), nullptr));
fprintf(stderr, "TestAbortIOWithRequests: completed %d iterations\n",
iterations);
#else
fprintf(stderr,
"TestAbortIOWithRequests: SKIPPED (ROCKSDB_IOURING_PRESENT not "
"defined)\n");
(void)env;
(void)file_size;
(void)read_specs;
(void)use_direct_io;
(void)iterations;
#endif
}
// Test overlapping reads at aligned offsets (multiples of 4KB)
TEST_F(TestAsyncRead, AbortIOOverlappingAligned) {
// 4 reads of 16KB each, overlapping by 8KB, all at 4KB-aligned offsets
// Read 0: [0, 16KB), Read 1: [8KB, 24KB), Read 2: [16KB, 32KB), Read 3:
// [24KB, 40KB)
std::vector<std::pair<uint64_t, size_t>> specs = {
{0, 16384},
{8192, 16384},
{16384, 16384},
{24576, 16384},
};
TestAbortIOWithRequests(env_, 64 * 1024, specs);
}
// Test reads at unaligned offsets (not multiples of 4KB)
TEST_F(TestAsyncRead, AbortIOUnalignedOffsets) {
// Reads starting at non-4KB-aligned offsets
std::vector<std::pair<uint64_t, size_t>> specs = {
{1000, 8192}, // starts at 1000 (unaligned)
{5000, 12288}, // starts at 5000 (unaligned), spans multiple sectors
{15000, 8192}, // starts at 15000 (unaligned)
{25500, 16384}, // starts at 25500 (unaligned)
};
TestAbortIOWithRequests(env_, 64 * 1024, specs);
}
// Test mix of aligned and unaligned, various sizes
TEST_F(TestAsyncRead, AbortIOMixedOffsets) {
std::vector<std::pair<uint64_t, size_t>> specs = {
{0, 4096}, // aligned, 1 sector
{1500, 8192}, // unaligned, 2 sectors
{4096, 20480}, // aligned, 5 sectors
{7000, 4096}, // unaligned, spans 2 sectors
{16384, 32768}, // aligned, 8 sectors
{50000, 8192}, // unaligned
};
TestAbortIOWithRequests(env_, 128 * 1024, specs);
}
// Stress test with many concurrent handles
TEST_F(TestAsyncRead, AbortIOStress) {
std::vector<std::pair<uint64_t, size_t>> specs;
// 16 overlapping reads with mixed alignment
for (int i = 0; i < 16; i++) {
uint64_t offset = i * 4000; // Not aligned to 4KB
size_t len = 8192 + (i % 4) * 4096; // 8KB to 20KB
specs.emplace_back(offset, len);
}
TestAbortIOWithRequests(env_, 256 * 1024, specs);
}
// Regression test for a fixed bug in AbortIO where out-of-order io_uring
// completions could cause an infinite hang. The bug occurred when completions
// for a different handle arrived while waiting for the current handle - the
// code would consume those completions but not mark the handle as finished,
// causing a hang when later iterating to that handle.
//
// Uses a large read (1MB) followed by a small read (4KB) with Direct I/O to
// maximize the chance of out-of-order completions. Runs 100 iterations to
// increase the likelihood of triggering the race condition.
TEST_F(TestAsyncRead, AbortIOReversedHandles) {
// Request 0: LARGE (1MB) at offset 0
// Request 1: SMALL (4KB) at offset 1MB
std::vector<std::pair<uint64_t, size_t>> specs = {
{0, 1024 * 1024}, // 1MB read
{1024 * 1024, 4096}, // 4KB read at 1MB offset
};
// 2MB file, Direct I/O enabled, 100 iterations
TestAbortIOWithRequests(env_, 2 * 1024 * 1024, specs,
/*use_direct_io=*/true, /*iterations=*/100);
}
// Test for bug fix: AbortIO with partial handles should correctly handle
// completions for non-aborted handles.
//
// Previously, AbortIO would consume completions for non-aborted handles but
// not set is_finished (since it expected req_count==2 for all handles).
// This caused subsequent Poll calls to hang forever.
//
// The fix correctly detects handles not in the abort set and finalizes them
// immediately when their completion arrives (at req_count==1).
TEST_F(TestAsyncRead, AbortIOPartialHandlesBug) {
#if defined(ROCKSDB_IOURING_PRESENT)
std::shared_ptr<FileSystem> fs = env_->GetFileSystem();
std::string fname = test::PerThreadDBPath(env_, "testfile_abortio_partial");
constexpr size_t kSectorSize = 4096;
constexpr size_t kFileSize = 2 * 1024 * 1024; // 2MB
// 1. Create test file with direct I/O
{
std::unique_ptr<FSWritableFile> wfile;
FileOptions file_opts;
file_opts.use_direct_writes = true;
ASSERT_OK(fs->NewWritableFile(fname, file_opts, &wfile, nullptr));
size_t num_sectors = kFileSize / kSectorSize;
for (size_t i = 0; i < num_sectors; ++i) {
auto data = NewAligned(kSectorSize, static_cast<char>(i + 1));
Slice slice(data.get(), kSectorSize);
ASSERT_OK(wfile->Append(slice, IOOptions(), nullptr));
}
ASSERT_OK(wfile->Close(IOOptions(), nullptr));
}
// 2. Submit 3 ReadAsync requests, abort only the first one, then Poll the
// rest
{
FileOptions file_opts;
file_opts.use_direct_reads = true;
std::unique_ptr<FSRandomAccessFile> file;
ASSERT_OK(fs->NewRandomAccessFile(fname, file_opts, &file, nullptr));
IOOptions opts;
constexpr size_t kNumReads = 3;
std::vector<void*> io_handles(kNumReads);
std::vector<FSReadRequest> reqs(kNumReads);
std::vector<std::unique_ptr<char, Deleter>> data;
std::vector<size_t> vals;
IOHandleDeleter del_fn;
std::atomic<int> callbacks_invoked{0};
// H0: 1MB read, H1: 4KB read, H2: 4KB read
std::vector<std::pair<uint64_t, size_t>> read_specs = {
{0, 1024 * 1024}, // H0: 1MB at offset 0
{1024 * 1024, 4096}, // H1: 4KB at offset 1MB
{1024 * 1024 + 4096, 4096}, // H2: 4KB at offset 1MB+4KB
};
for (size_t i = 0; i < kNumReads; i++) {
reqs[i].offset = read_specs[i].first;
reqs[i].len = read_specs[i].second;
data.emplace_back(NewAligned(reqs[i].len, 0));
reqs[i].scratch = data.back().get();
vals.push_back(i);
}
std::function<void(FSReadRequest&, void*)> callback =
[&](FSReadRequest& req, void* cb_arg) {
size_t i = *(reinterpret_cast<size_t*>(cb_arg));
reqs[i].status = req.status;
callbacks_invoked++;
};
// Submit all ReadAsync requests
for (size_t i = 0; i < kNumReads; i++) {
void* cb_arg = static_cast<void*>(&(vals[i]));
IOStatus s = file->ReadAsync(reqs[i], opts, callback, cb_arg,
&(io_handles[i]), &del_fn, nullptr);
if (s.IsNotSupported()) {
// io_uring not supported, clean up and skip
for (size_t j = 0; j < i; j++) {
if (io_handles[j]) {
del_fn(io_handles[j]);
}
}
ASSERT_OK(fs->DeleteFile(fname, IOOptions(), nullptr));
return;
}
ASSERT_OK(s);
}
// Wait for reads to complete in io_uring (completions in queue but not
// consumed). 5 seconds should be plenty for direct I/O reads to complete.
std::this_thread::sleep_for(std::chrono::seconds(5));
// Abort ONLY H0 - this will consume all completions but should correctly
// finalize H1 and H2 (since they're not in the abort set).
std::vector<void*> abort_handles = {io_handles[0]};
ASSERT_OK(fs->AbortIO(abort_handles));
// Verify H0 is finished (aborted)
Posix_IOHandle* h0 = static_cast<Posix_IOHandle*>(io_handles[0]);
ASSERT_TRUE(h0->is_finished);
ASSERT_EQ(h0->req_count, 2u); // original + cancel
// Note: H1 and H2 may or may not be finished at this point. AbortIO
// finalizes non-aborted handles whose CQEs arrive while waiting for
// aborted handles, but CQE ordering is non-deterministic. If H0's
// completions arrived first, H1/H2's CQEs are still in the queue.
// Poll handles either case correctly.
// Poll on H1, H2 - completes them if not already finalized by AbortIO
std::vector<void*> poll_handles = {io_handles[1], io_handles[2]};
// Use a watchdog to detect hang (regression test for the original bug
// where AbortIO consumed non-aborted CQEs without finalizing them)
std::atomic<bool> poll_completed{false};
std::thread watchdog([&]() {
for (int i = 0; i < 500; i++) { // 5 seconds timeout
std::this_thread::sleep_for(std::chrono::milliseconds(10));
if (poll_completed) return;
}
// Bug regression: Poll hung
_exit(1);
});
fs->Poll(poll_handles, poll_handles.size());
poll_completed = true;
watchdog.join();
// After Poll, H1 and H2 must be finished
Posix_IOHandle* h1 = static_cast<Posix_IOHandle*>(io_handles[1]);
Posix_IOHandle* h2 = static_cast<Posix_IOHandle*>(io_handles[2]);
ASSERT_TRUE(h1->is_finished);
ASSERT_TRUE(h2->is_finished);
// Verify all callbacks were invoked
ASSERT_EQ(callbacks_invoked.load(), 3);
// Clean up handles
for (size_t i = 0; i < kNumReads; i++) {
if (io_handles[i]) {
del_fn(io_handles[i]);
}
}
}
ASSERT_OK(fs->DeleteFile(fname, IOOptions(), nullptr));
#else
(void)env_; // Suppress unused variable warning
#endif
}
struct StaticDestructionTester {
bool activated = false;
~StaticDestructionTester() {
+29 -28
View File
@@ -1129,25 +1129,7 @@ class PosixFileSystem : public FileSystem {
// Reset cqe data to catch any stray reuse of it
static_cast<struct io_uring_cqe*>(cqe)->user_data = 0xd5d5d5d5d5d5d5d5;
FSReadRequest req;
req.scratch = posix_handle->scratch;
req.offset = posix_handle->offset;
req.len = posix_handle->len;
size_t finished_len = 0;
size_t bytes_read = 0;
bool read_again = false;
UpdateResult(cqe, "", req.len, posix_handle->iov.iov_len,
true /*async_read*/, posix_handle->use_direct_io,
posix_handle->alignment, finished_len, &req, bytes_read,
read_again);
posix_handle->is_finished = true;
io_uring_cqe_seen(iu, cqe);
posix_handle->cb(req, posix_handle->cb_arg);
(void)finished_len;
(void)bytes_read;
(void)read_again;
FinalizeAsyncRead(iu, cqe, posix_handle);
if (static_cast<Posix_IOHandle*>(io_handles[i]) == posix_handle) {
break;
@@ -1188,6 +1170,11 @@ class PosixFileSystem : public FileSystem {
return IOStatus::IOError("");
}
// Mark this handle as being aborted. This is used when processing
// completions to distinguish between aborted handles (expect 2
// completions: original + cancel) and non-aborted handles (expect 1).
posix_handle->is_being_aborted = true;
// Prepare the cancel request.
struct io_uring_sqe* sqe;
sqe = io_uring_get_sqe(iu);
@@ -1234,6 +1221,14 @@ class PosixFileSystem : public FileSystem {
}
posix_handle->req_count++;
if (!posix_handle->is_being_aborted) {
// This is a completion for a handle NOT being aborted.
// It only has 1 outstanding request (the original read), so we
// should finalize it now.
FinalizeAsyncRead(iu, cqe, posix_handle);
continue;
}
// Reset cqe data to catch any stray reuse of it
static_cast<struct io_uring_cqe*>(cqe)->user_data = 0xd5d5d5d5d5d5d5d5;
io_uring_cqe_seen(iu, cqe);
@@ -1247,16 +1242,23 @@ class PosixFileSystem : public FileSystem {
// - And finally, if the request to cancel wasn't
// found, the cancel request is completed with -ENOENT.
//
// Every handle has to wait for 2 requests completion: original one and
// the cancel request which is tracked by PosixHandle::req_count.
if (posix_handle->req_count == 2 &&
static_cast<Posix_IOHandle*>(io_handles[i]) == posix_handle) {
// Every handle being aborted has to wait for 2 requests completion:
// original one and the cancel request which is tracked by
// PosixHandle::req_count.
// Note: We must mark is_finished and invoke the callback for ANY handle
// that reaches req_count == 2, not just the one we're currently waiting
// for (io_handles[i]). Otherwise, if completions arrive out of order,
// we consume another handle's completions without marking it finished,
// causing an infinite hang when we later wait for that handle.
if (posix_handle->req_count == 2) {
posix_handle->is_finished = true;
FSReadRequest req;
req.status = IOStatus::Aborted();
posix_handle->cb(req, posix_handle->cb_arg);
break;
if (static_cast<Posix_IOHandle*>(io_handles[i]) == posix_handle) {
break;
}
}
}
}
@@ -1272,7 +1274,7 @@ class PosixFileSystem : public FileSystem {
void SupportedOps(int64_t& supported_ops) override {
supported_ops = 0;
#if defined(ROCKSDB_IOURING_PRESENT)
if (IsIOUringEnabled()) {
if (IsIOUringEnabled() && thread_local_async_read_io_urings_) {
// Underlying FS supports async_io
supported_ops |= (1 << FSSupportedOps::kAsyncIO);
}
@@ -1338,9 +1340,8 @@ PosixFileSystem::PosixFileSystem()
page_size_(getpagesize()),
allow_non_owner_access_(true) {
#if defined(ROCKSDB_IOURING_PRESENT)
// Test whether IOUring is supported, and if it does, create a managing
// object for thread local point so that in the future thread-local
// io_uring can be created.
// Test whether IOUring is supported with the same flags that ReadAsync and
// MultiRead will use at runtime.
struct io_uring* new_io_uring = CreateIOUring();
if (new_io_uring != nullptr) {
thread_local_async_read_io_urings_.reset(new ThreadLocalPtr(DeleteIOUring));
+2 -8
View File
@@ -755,10 +755,7 @@ IOStatus PosixRandomAccessFile::MultiRead(FSReadRequest* reqs, size_t num_reqs,
iu = static_cast<struct io_uring*>(
thread_local_multi_read_io_urings_->Get());
if (iu == nullptr) {
unsigned int flags = 0;
flags |= IORING_SETUP_SINGLE_ISSUER;
flags |= IORING_SETUP_DEFER_TASKRUN;
iu = CreateIOUring(flags);
iu = CreateIOUring();
if (iu != nullptr) {
thread_local_multi_read_io_urings_->Reset(iu);
}
@@ -1090,10 +1087,7 @@ IOStatus PosixRandomAccessFile::ReadAsync(
iu = static_cast<struct io_uring*>(
thread_local_async_read_io_urings_->Get());
if (iu == nullptr) {
unsigned int flags = 0;
flags |= IORING_SETUP_SINGLE_ISSUER;
flags |= IORING_SETUP_DEFER_TASKRUN;
iu = CreateIOUring(flags);
iu = CreateIOUring();
if (iu != nullptr) {
thread_local_async_read_io_urings_->Reset(iu);
}
+30 -1
View File
@@ -127,6 +127,7 @@ struct Posix_IOHandle {
use_direct_io(_use_direct_io),
alignment(_alignment),
is_finished(false),
is_being_aborted(false),
req_count(0) {}
struct iovec iov;
@@ -139,6 +140,10 @@ struct Posix_IOHandle {
bool use_direct_io;
size_t alignment;
bool is_finished;
// is_being_aborted is set by AbortIO when a cancel request is submitted.
// Used to distinguish between aborted handles (expect 2 completions) and
// non-aborted handles (expect 1 completion) when processing completions.
bool is_being_aborted;
// req_count is used by AbortIO API to keep track of number of requests.
uint32_t req_count;
};
@@ -197,6 +202,27 @@ inline void UpdateResult(struct io_uring_cqe* cqe, const std::string& file_name,
(void)len;
#endif
}
// Finalize a completed async read request.
// Processes the CQE result, marks the handle as finished, and invokes the
// callback. This is shared between Poll and AbortIO (for non-aborted handles).
inline void FinalizeAsyncRead(struct io_uring* iu, struct io_uring_cqe* cqe,
Posix_IOHandle* posix_handle) {
FSReadRequest req;
req.scratch = posix_handle->scratch;
req.offset = posix_handle->offset;
req.len = posix_handle->len;
size_t finished_len = 0;
size_t bytes_read = 0;
bool read_again = false;
UpdateResult(cqe, "", req.len, posix_handle->iov.iov_len, true /*async_read*/,
posix_handle->use_direct_io, posix_handle->alignment,
finished_len, &req, bytes_read, read_again);
posix_handle->is_finished = true;
io_uring_cqe_seen(iu, cqe);
posix_handle->cb(req, posix_handle->cb_arg);
}
#endif
#ifdef OS_LINUX
@@ -307,8 +333,11 @@ inline void DeleteIOUring(void* p) {
delete iu;
}
inline struct io_uring* CreateIOUring(unsigned int flags = 0) {
inline struct io_uring* CreateIOUring() {
struct io_uring* new_io_uring = new struct io_uring;
unsigned int flags = 0;
flags |= IORING_SETUP_SINGLE_ISSUER;
flags |= IORING_SETUP_DEFER_TASKRUN;
int ret = io_uring_queue_init(kIoUringDepth, new_io_uring, flags);
if (ret) {
delete new_io_uring;
+20 -4
View File
@@ -351,7 +351,7 @@ void FilePrefetchBuffer::ClearOutdatedData(uint64_t offset, size_t length) {
assert(IsBufferQueueEmpty() || buf->IsOffsetInBuffer(offset));
}
void FilePrefetchBuffer::PollIfNeeded(uint64_t offset, size_t length) {
Status FilePrefetchBuffer::PollIfNeeded(uint64_t offset, size_t length) {
BufferInfo* buf = GetFirstBuffer();
if (buf->async_read_in_progress_ && fs_ != nullptr) {
@@ -362,7 +362,16 @@ void FilePrefetchBuffer::PollIfNeeded(uint64_t offset, size_t length) {
std::vector<void*> handles;
handles.emplace_back(buf->io_handle_);
StopWatch sw(clock_, stats_, POLL_WAIT_MICROS);
fs_->Poll(handles, 1).PermitUncheckedError();
IOStatus io_s = fs_->Poll(handles, 1);
// Allow tests to inject Poll errors
TEST_SYNC_POINT_CALLBACK("FilePrefetchBuffer::PollIfNeeded:IOStatus",
&io_s);
if (!io_s.ok()) {
// On Poll failure, clean up the handle and abort.
// DestroyAndClearIOHandle also sets async_read_in_progress_ to false.
DestroyAndClearIOHandle(buf);
return io_s;
}
}
// Reset and Release io_handle after the Poll API as request has been
@@ -373,6 +382,7 @@ void FilePrefetchBuffer::PollIfNeeded(uint64_t offset, size_t length) {
// Always call outdated data after Poll as Buffers might be out of sync w.r.t
// offset and length.
ClearOutdatedData(offset, length);
return Status::OK();
}
// ReadAheadSizeTuning API calls readaheadsize_cb_
@@ -511,7 +521,10 @@ Status FilePrefetchBuffer::HandleOverlappingAsyncData(
// by Seek, but the next access is at another offset.
if (buf->async_read_in_progress_ &&
buf->IsOffsetInBufferWithAsyncProgress(offset)) {
PollIfNeeded(offset, length);
Status poll_status = PollIfNeeded(offset, length);
if (!poll_status.ok()) {
return poll_status;
}
}
if (IsBufferQueueEmpty() || NumBuffersAllocated() == 1) {
@@ -646,7 +659,10 @@ Status FilePrefetchBuffer::PrefetchInternal(const IOOptions& opts,
return s;
}
} else {
PollIfNeeded(tmp_offset, tmp_length);
Status poll_status = PollIfNeeded(tmp_offset, tmp_length);
if (!poll_status.ok()) {
return poll_status;
}
}
AllocateBufferIfEmpty();
+4 -4
View File
@@ -93,8 +93,8 @@ struct BufferInfo {
//
// For example - if end offset of previous buffer was 100 and because of
// readahead_size optimization, end_offset was trimmed to 60. Then for next
// prefetch call, start_offset should be intialized to 100 i.e start_offset =
// buf->initial_end_offset_.
// prefetch call, start_offset should be initialized to 100 i.e start_offset
// = buf->initial_end_offset_.
uint64_t initial_end_offset_ = 0;
bool IsDataBlockInBuffer(uint64_t offset, size_t length) {
@@ -155,7 +155,7 @@ enum class FilePrefetchBufferUsage {
// When reusing the file system allocated buffer, overlap_buf_ is used if the
// main buffer only contains part of the requested data. It is returned to
// the caller after the remaining data is fetched.
// If num_buffers_ > 1, then the data is prefetched asynchronosuly in the
// If num_buffers_ > 1, then the data is prefetched asynchronously in the
// buffers whenever the data is consumed from the buffers and that buffer is
// freed.
// If num_buffers > 1, then requested data can be overlapping between 2 buffers.
@@ -431,7 +431,7 @@ class FilePrefetchBuffer {
void ClearOutdatedData(uint64_t offset, size_t len);
// It calls Poll API to check for any pending asynchronous request.
void PollIfNeeded(uint64_t offset, size_t len);
Status PollIfNeeded(uint64_t offset, size_t len);
Status PrefetchInternal(const IOOptions& opts, RandomAccessFileReader* reader,
uint64_t offset, size_t length, size_t readahead_size,
+8 -2
View File
@@ -178,7 +178,8 @@ IOStatus GenerateOneFileChecksum(
std::string* file_checksum_func_name,
size_t verify_checksums_readahead_size, bool /*allow_mmap_reads*/,
std::shared_ptr<IOTracer>& io_tracer, RateLimiter* rate_limiter,
const ReadOptions& read_options, Statistics* stats, SystemClock* clock) {
const ReadOptions& read_options, Statistics* stats, SystemClock* clock,
const FileOptions& file_options) {
if (checksum_factory == nullptr) {
return IOStatus::InvalidArgument("Checksum factory is invalid");
}
@@ -218,7 +219,12 @@ IOStatus GenerateOneFileChecksum(
std::unique_ptr<RandomAccessFileReader> reader;
{
std::unique_ptr<FSRandomAccessFile> r_file;
io_s = fs->NewRandomAccessFile(file_path, FileOptions(), &r_file, nullptr);
FileOptions fopts = file_options;
if (fopts.file_checksum.empty()) {
// No expected checksum is known — this is a from-scratch computation.
fopts.file_checksum_func_name = kNoFileChecksumFuncName;
}
io_s = fs->NewRandomAccessFile(file_path, fopts, &r_file, nullptr);
if (!io_s.ok()) {
return io_s;
}
+2 -1
View File
@@ -83,7 +83,8 @@ IOStatus GenerateOneFileChecksum(
std::string* file_checksum_func_name,
size_t verify_checksums_readahead_size, bool allow_mmap_reads,
std::shared_ptr<IOTracer>& io_tracer, RateLimiter* rate_limiter,
const ReadOptions& read_options, Statistics* stats, SystemClock* clock);
const ReadOptions& read_options, Statistics* stats, SystemClock* clock,
const FileOptions& file_options);
inline IOStatus PrepareIOFromReadOptions(const ReadOptions& ro,
SystemClock* clock, IOOptions& opts,
+260 -10
View File
@@ -669,7 +669,7 @@ TEST_P(PrefetchTest, ConfigureAutoMaxReadaheadSize) {
MoveFilesToLevel(level);
}
Close();
std::vector<int> buff_prefectch_level_count = {0, 0, 0};
std::vector<int> buff_prefetch_level_count = {0, 0, 0};
ASSERT_OK(TryReopen(options));
{
auto iter = std::unique_ptr<Iterator>(db_->NewIterator(ReadOptions()));
@@ -707,7 +707,7 @@ TEST_P(PrefetchTest, ConfigureAutoMaxReadaheadSize) {
iter->Next();
}
buff_prefectch_level_count[level] = buff_prefetch_count;
buff_prefetch_level_count[level] = buff_prefetch_count;
if (support_prefetch && !use_direct_io) {
if (level == 0) {
ASSERT_FALSE(fs->IsPrefetchCalled());
@@ -728,7 +728,7 @@ TEST_P(PrefetchTest, ConfigureAutoMaxReadaheadSize) {
}
if (!support_prefetch) {
ASSERT_GT(buff_prefectch_level_count[1], buff_prefectch_level_count[2]);
ASSERT_GT(buff_prefetch_level_count[1], buff_prefetch_level_count[2]);
}
SyncPoint::GetInstance()->DisableProcessing();
@@ -814,7 +814,7 @@ TEST_P(PrefetchTest, ConfigureInternalAutoReadaheadSize) {
"{initial_auto_readahead_size=0;}"}}));
break;
case 1:
// intial_auto_readahead_size and max_auto_readahead_size are set
// initial_auto_readahead_size and max_auto_readahead_size are set
// same so readahead_size remains same.
ASSERT_OK(db_->SetOptions({{"block_based_table_factory",
"{initial_auto_readahead_size=4096;max_"
@@ -1081,7 +1081,7 @@ TEST_P(PrefetchTest, PrefetchWhenReseek) {
}
{
/*
* Reesek keys from Single Data Block.
* Reseek keys from Single Data Block.
*/
auto iter = std::unique_ptr<Iterator>(db_->NewIterator(ReadOptions()));
iter->Seek(BuildKey(0));
@@ -1116,9 +1116,8 @@ TEST_P(PrefetchTest, PrefetchWhenReseek) {
ASSERT_TRUE(iter->Valid());
iter->Seek(BuildKey(1008));
ASSERT_TRUE(iter->Valid());
iter->Seek(
BuildKey(996)); // Reseek won't prefetch any data and
// readahead_size will be initiallized to 8*1024.
iter->Seek(BuildKey(996)); // Reseek won't prefetch any data and
// readahead_size will be initialized to 8*1024.
ASSERT_TRUE(iter->Valid());
iter->Seek(BuildKey(992));
ASSERT_TRUE(iter->Valid());
@@ -1590,7 +1589,7 @@ INSTANTIATE_TEST_CASE_P(
::testing::Combine(
// Params are as follows -
// Param 0 - TableOptions::index_shortening
// Param 2 - ReadOptinos::auto_readahead_size
// Param 2 - ReadOptions::auto_readahead_size
::testing::Values(
BlockBasedTableOptions::IndexShorteningMode::kNoShortening,
BlockBasedTableOptions::IndexShorteningMode::kShortenSeparators,
@@ -2518,6 +2517,187 @@ TEST_P(PrefetchTest1, SeekParallelizationTest) {
Close();
}
TEST_P(PrefetchTest1, PollErrorRecoveryDuringIteration) {
// This end-to-end test verifies that Poll() errors during async prefetching
// are properly propagated to the iterator. When Poll() fails, the iterator
// should stop and return an IOError status.
//
// With error injection on the 3rd Poll call, the iterator reads ~231 keys
// (out of 500) before encountering the error.
if (mem_env_ || encrypted_env_) {
ROCKSDB_GTEST_SKIP("Test requires non-mem or non-encrypted environment");
return;
}
const int kNumKeys = 500;
std::shared_ptr<MockFS> fs = std::make_shared<MockFS>(
FileSystem::Default(), /*support_prefetch=*/false);
std::unique_ptr<Env> env(new CompositeEnvWrapper(env_, fs));
bool use_direct_io = GetParam();
Options options;
SetGenericOptions(env.get(), use_direct_io, options);
options.statistics = CreateDBStatistics();
BlockBasedTableOptions table_options;
SetBlockBasedTableOptions(table_options);
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
Status s = TryReopen(options);
if (use_direct_io && (s.IsNotSupported() || s.IsInvalidArgument())) {
ROCKSDB_GTEST_SKIP("Direct IO not supported");
return;
}
ASSERT_OK(s);
// Write keys with known values so we can verify correctness
std::map<std::string, std::string> expected_data;
{
WriteBatch batch;
for (int i = 0; i < kNumKeys; i++) {
std::string key = BuildKey(i);
std::string value = "value_" + std::to_string(i) + "_" +
std::string(100, 'x'); // Make values ~110 bytes
ASSERT_OK(batch.Put(key, value));
expected_data[key] = value;
}
ASSERT_OK(db_->Write(WriteOptions(), &batch));
ASSERT_OK(Flush());
}
std::string start_key = BuildKey(0);
std::string end_key = BuildKey(kNumKeys - 1);
Slice least(start_key.data(), start_key.size());
Slice greatest(end_key.data(), end_key.size());
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), &least, &greatest));
// Set up callbacks to track async IO and inject Poll errors
std::atomic<int> poll_call_count{0};
std::atomic<int> poll_error_injected_count{0};
bool read_async_called = false;
SyncPoint::GetInstance()->SetCallBack(
"FilePrefetchBuffer::PollIfNeeded:IOStatus", [&](void* arg) {
poll_call_count++;
int current_count = poll_call_count.load();
// Inject error on the third Poll call to allow some keys to be read
// first
if (current_count == 3) {
IOStatus* io_s = static_cast<IOStatus*>(arg);
*io_s = IOStatus::IOError("Injected Poll error for e2e testing");
poll_error_injected_count++;
std::cout << "PollErrorRecoveryDuringIteration: Injected error on "
"Poll call #"
<< current_count << std::endl;
}
});
SyncPoint::GetInstance()->SetCallBack(
"UpdateResults::io_uring_result",
[&](void* /*arg*/) { read_async_called = true; });
SyncPoint::GetInstance()->EnableProcessing();
// Iterate through all keys with async IO enabled
ReadOptions ro;
ro.async_io = true;
ro.adaptive_readahead = true;
int keys_read = 0;
int data_mismatches = 0;
Status iter_status;
{
auto iter = std::unique_ptr<Iterator>(db_->NewIterator(ro));
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
std::string key = iter->key().ToString();
std::string value = iter->value().ToString();
auto it = expected_data.find(key);
if (it == expected_data.end()) {
std::cout << "PollErrorRecoveryDuringIteration: Unexpected key: " << key
<< std::endl;
data_mismatches++;
} else if (it->second != value) {
std::cout << "PollErrorRecoveryDuringIteration: Value mismatch for key "
<< key << std::endl;
data_mismatches++;
}
keys_read++;
}
iter_status = iter->status();
}
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
// Log results
std::cout << "PollErrorRecoveryDuringIteration: " << "read_async_called="
<< read_async_called << ", poll_calls=" << poll_call_count.load()
<< ", poll_errors_injected=" << poll_error_injected_count.load()
<< ", keys_read=" << keys_read << ", expected_keys=" << kNumKeys
<< ", data_mismatches=" << data_mismatches
<< ", iter_status=" << iter_status.ToString() << std::endl;
// Verify no data mismatches occurred for keys that were read
ASSERT_EQ(data_mismatches, 0)
<< "Found " << data_mismatches << " data mismatches";
if (read_async_called) {
// Async IO was used - verify Poll error was injected and propagated
ASSERT_EQ(poll_call_count.load(), 3)
<< "Expected exactly 3 Poll calls when error injected on 3rd call";
ASSERT_EQ(poll_error_injected_count.load(), 1)
<< "Expected exactly 1 Poll error to be injected";
// The iterator should have stopped with an error status
ASSERT_TRUE(iter_status.IsIOError())
<< "Expected iterator to report IOError after Poll failure, got: "
<< iter_status.ToString();
std::cout << "PollErrorRecoveryDuringIteration: Successfully verified "
"Poll error was injected and propagated to iterator"
<< std::endl;
} else {
// Async IO not supported - iterator should complete successfully
ASSERT_OK(iter_status);
ASSERT_EQ(keys_read, kNumKeys);
std::cout << "PollErrorRecoveryDuringIteration: Async IO (io_uring) not "
"supported on this platform, verified data correctness"
<< std::endl;
}
// Retry iteration without error injection - verify all data is still readable
// This confirms the Poll error didn't corrupt state
{
int retry_keys_read = 0;
int retry_data_mismatches = 0;
auto iter = std::unique_ptr<Iterator>(db_->NewIterator(ro));
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
std::string key = iter->key().ToString();
std::string value = iter->value().ToString();
auto it = expected_data.find(key);
if (it == expected_data.end()) {
retry_data_mismatches++;
} else if (it->second != value) {
retry_data_mismatches++;
}
retry_keys_read++;
}
ASSERT_OK(iter->status())
<< "Retry iteration failed: " << iter->status().ToString();
ASSERT_EQ(retry_keys_read, kNumKeys)
<< "Retry should read all " << kNumKeys << " keys";
ASSERT_EQ(retry_data_mismatches, 0)
<< "Retry found " << retry_data_mismatches << " data mismatches";
std::cout << "PollErrorRecoveryDuringIteration: Retry succeeded, read all "
<< retry_keys_read << " keys correctly" << std::endl;
}
Close();
}
namespace {
#ifdef GFLAGS
const int kMaxArgCount = 100;
@@ -3303,7 +3483,7 @@ TEST_F(FilePrefetchBufferTest, SyncReadaheadStats) {
ASSERT_EQ(stats->getAndResetTickerCount(PREFETCH_HITS), 1);
ASSERT_EQ(stats->getAndResetTickerCount(PREFETCH_BYTES_USEFUL), 8192);
// Now read some data with length doesn't align with aligment and it needs
// Now read some data with length doesn't align with alignment and it needs
// prefetching. Read from 16000 with length 10000 (i.e. requested end offset -
// 26000).
ASSERT_TRUE(
@@ -3377,6 +3557,76 @@ TEST_F(FilePrefetchBufferTest, ForCompaction) {
0);
}
TEST_F(FilePrefetchBufferTest, PollErrorPropagation) {
// This test verifies that Poll() errors in PollIfNeeded are properly
// propagated rather than being silently ignored.
std::string fname = "poll-error-test";
Random rand(0);
std::string content = rand.RandomString(32768);
Write(fname, content);
FileOptions opts;
std::unique_ptr<RandomAccessFileReader> r;
Read(fname, opts, &r);
// Set up readahead params for async prefetching
ReadaheadParams readahead_params;
readahead_params.initial_readahead_size = 16384;
readahead_params.max_readahead_size = 16384;
FilePrefetchBuffer fpb(readahead_params, /*enable=*/true,
/*track_min_offset=*/false, fs());
Slice result;
// Start an async prefetch to set up async_read_in_progress_ state
Status s = fpb.PrefetchAsync(IOOptions(), r.get(), 0, 4096, &result);
// Skip test on platforms that don't support async IO
if (s.IsNotSupported()) {
ROCKSDB_GTEST_SKIP("Async IO not supported on this platform");
return;
}
ASSERT_TRUE(s.IsTryAgain());
std::cout << "PollErrorPropagation: Async IO supported, proceeding with test"
<< std::endl;
// Set up SyncPoint to inject Poll error
SyncPoint::GetInstance()->SetCallBack(
"FilePrefetchBuffer::PollIfNeeded:IOStatus", [&](void* arg) {
IOStatus* io_s = static_cast<IOStatus*>(arg);
*io_s = IOStatus::IOError("Injected Poll error for testing");
});
SyncPoint::GetInstance()->EnableProcessing();
// TryReadFromCache will call PollIfNeeded to complete the async read
IOOptions io_opts;
io_opts.rate_limiter_priority = Env::IOPriority::IO_LOW;
Status read_status;
bool found =
fpb.TryReadFromCache(io_opts, r.get(), 0, 4096, &result, &read_status);
// When PollIfNeeded fails:
// 1. PrefetchInternal returns the error status
// 2. TryReadFromCacheUntracked sets *status to the error and returns false
// Therefore: found should be false, and read_status should contain the error
ASSERT_FALSE(found) << "Expected TryReadFromCache to return false on Poll "
"error, but it returned true";
ASSERT_TRUE(read_status.IsIOError())
<< "Expected IOError status, got: " << read_status.ToString();
ASSERT_TRUE(read_status.ToString().find("Injected Poll error") !=
std::string::npos)
<< "Expected error message to contain 'Injected Poll error', got: "
<< read_status.ToString();
std::cout << "PollErrorPropagation: Poll error correctly propagated - "
<< "found=" << found << ", status=" << read_status.ToString()
<< std::endl;
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
}
class FSBufferPrefetchTest
: public testing::Test,
public ::testing::WithParamInterface<std::tuple<bool, bool>> {
+47 -35
View File
@@ -66,41 +66,53 @@ inline void RecordIOStats(Statistics* stats, Temperature file_temperature,
}
// record for temperature file
if (file_temperature != Temperature::kUnknown) {
switch (file_temperature) {
case Temperature::kHot:
IOSTATS_ADD(file_io_stats_by_temperature.hot_file_bytes_read, size);
IOSTATS_ADD(file_io_stats_by_temperature.hot_file_read_count, 1);
RecordTick(stats, HOT_FILE_READ_BYTES, size);
RecordTick(stats, HOT_FILE_READ_COUNT, 1);
break;
case Temperature::kWarm:
IOSTATS_ADD(file_io_stats_by_temperature.warm_file_bytes_read, size);
IOSTATS_ADD(file_io_stats_by_temperature.warm_file_read_count, 1);
RecordTick(stats, WARM_FILE_READ_BYTES, size);
RecordTick(stats, WARM_FILE_READ_COUNT, 1);
break;
case Temperature::kCool:
IOSTATS_ADD(file_io_stats_by_temperature.cool_file_bytes_read, size);
IOSTATS_ADD(file_io_stats_by_temperature.cool_file_read_count, 1);
RecordTick(stats, COOL_FILE_READ_BYTES, size);
RecordTick(stats, COOL_FILE_READ_COUNT, 1);
break;
case Temperature::kCold:
IOSTATS_ADD(file_io_stats_by_temperature.cold_file_bytes_read, size);
IOSTATS_ADD(file_io_stats_by_temperature.cold_file_read_count, 1);
RecordTick(stats, COLD_FILE_READ_BYTES, size);
RecordTick(stats, COLD_FILE_READ_COUNT, 1);
break;
case Temperature::kIce:
IOSTATS_ADD(file_io_stats_by_temperature.ice_file_bytes_read, size);
IOSTATS_ADD(file_io_stats_by_temperature.ice_file_read_count, 1);
RecordTick(stats, ICE_FILE_READ_BYTES, size);
RecordTick(stats, ICE_FILE_READ_COUNT, 1);
break;
default:
break;
}
switch (file_temperature) {
case Temperature::kHot:
IOSTATS_ADD(file_io_stats_by_temperature.hot_file_bytes_read, size);
IOSTATS_ADD(file_io_stats_by_temperature.hot_file_read_count, 1);
RecordTick(stats, HOT_FILE_READ_BYTES, size);
RecordTick(stats, HOT_FILE_READ_COUNT, 1);
break;
case Temperature::kWarm:
IOSTATS_ADD(file_io_stats_by_temperature.warm_file_bytes_read, size);
IOSTATS_ADD(file_io_stats_by_temperature.warm_file_read_count, 1);
RecordTick(stats, WARM_FILE_READ_BYTES, size);
RecordTick(stats, WARM_FILE_READ_COUNT, 1);
break;
case Temperature::kCool:
IOSTATS_ADD(file_io_stats_by_temperature.cool_file_bytes_read, size);
IOSTATS_ADD(file_io_stats_by_temperature.cool_file_read_count, 1);
RecordTick(stats, COOL_FILE_READ_BYTES, size);
RecordTick(stats, COOL_FILE_READ_COUNT, 1);
break;
case Temperature::kCold:
IOSTATS_ADD(file_io_stats_by_temperature.cold_file_bytes_read, size);
IOSTATS_ADD(file_io_stats_by_temperature.cold_file_read_count, 1);
RecordTick(stats, COLD_FILE_READ_BYTES, size);
RecordTick(stats, COLD_FILE_READ_COUNT, 1);
break;
case Temperature::kIce:
IOSTATS_ADD(file_io_stats_by_temperature.ice_file_bytes_read, size);
IOSTATS_ADD(file_io_stats_by_temperature.ice_file_read_count, 1);
RecordTick(stats, ICE_FILE_READ_BYTES, size);
RecordTick(stats, ICE_FILE_READ_COUNT, 1);
break;
case Temperature::kUnknown:
if (is_last_level) {
IOSTATS_ADD(file_io_stats_by_temperature.unknown_last_level_bytes_read,
size);
IOSTATS_ADD(file_io_stats_by_temperature.unknown_last_level_read_count,
1);
} else {
IOSTATS_ADD(
file_io_stats_by_temperature.unknown_non_last_level_bytes_read,
size);
IOSTATS_ADD(
file_io_stats_by_temperature.unknown_non_last_level_read_count, 1);
}
break;
default:
break;
}
}
+20 -3
View File
@@ -98,7 +98,7 @@ endif # FMT_SOURCE_PATH
PLATFORM_LDFLAGS += -lglog
endif
FOLLY_COMMIT_HASH = abe68f7e917e8b7a0ee2fe066c972dc98fd35aa1
FOLLY_COMMIT_HASH = 94a8e82cf16a0e229fc4fc89140219434ba78fa2
# For public CI runs, checkout folly in a way that can build with RocksDB.
# This is mostly intended as a test-only simulation of Meta-internal folly
@@ -116,10 +116,27 @@ checkout_folly:
perl -pi -e 's/(#include <atomic>)/$$1\n#include <cstring>/' third-party/folly/folly/lang/Exception.h
@# const mismatch
perl -pi -e 's/: environ/: (const char**)(environ)/' third-party/folly/folly/Subprocess.cpp
@# Use gnu.org mirrors to improve download speed (ftp.gnu.org is often super slow)
cd third-party/folly && perl -pi -e 's/ftp.gnu.org/ftpmirror.gnu.org/' `git grep -l ftp.gnu.org` README.md
@# Restore cached downloads and handle unreliable mirrors with fallback
@cd third-party/folly && \
DOWNLOAD_DIR=`$(PYTHON) build/fbcode_builder/getdeps.py show-inst-dir | sed 's|/installed/.*|/downloads|'` && \
mkdir -p "$$DOWNLOAD_DIR" && \
CACHE_DIR="/tmp/rocksdb-getdeps-cache" && \
mkdir -p "$$CACHE_DIR" && \
echo "Restoring cached downloads..." && \
if ls "$$CACHE_DIR"/*.tar.gz "$$CACHE_DIR"/*.tar.xz "$$CACHE_DIR"/*.zip >/dev/null 2>&1; then \
cp -n "$$CACHE_DIR"/*.tar.gz "$$CACHE_DIR"/*.tar.xz "$$CACHE_DIR"/*.zip "$$DOWNLOAD_DIR/" 2>/dev/null || true; \
fi && \
echo "Handling known unreliable downloads with fallback mirrors..." && \
$(PYTHON) ../../build_tools/getdeps_fallback_mirror.py "$$DOWNLOAD_DIR" "$$CACHE_DIR" build/fbcode_builder/manifests
@# NOTE: boost and fmt source will be needed for any build including `USE_FOLLY_LITE` builds as those depend on those headers
cd third-party/folly && GETDEPS_USE_WGET=1 $(PYTHON) build/fbcode_builder/getdeps.py fetch boost && GETDEPS_USE_WGET=1 $(PYTHON) build/fbcode_builder/getdeps.py fetch fmt
@# Update cache with any new downloads
@cd third-party/folly && \
DOWNLOAD_DIR=`$(PYTHON) build/fbcode_builder/getdeps.py show-inst-dir | sed 's|/installed/.*|/downloads|'` && \
CACHE_DIR="/tmp/rocksdb-getdeps-cache" && \
if ls "$$DOWNLOAD_DIR"/*.tar.gz "$$DOWNLOAD_DIR"/*.tar.xz "$$DOWNLOAD_DIR"/*.zip >/dev/null 2>&1; then \
cp -n "$$DOWNLOAD_DIR"/*.tar.gz "$$DOWNLOAD_DIR"/*.tar.xz "$$DOWNLOAD_DIR"/*.zip "$$CACHE_DIR/" 2>/dev/null || true; \
fi
CXX_M_FLAGS = $(filter -m%, $(CXXFLAGS))
+1 -1
View File
@@ -318,7 +318,7 @@ class Cache : public Customizable {
// REQUIRES: handle must have been returned by a method on *this.
virtual bool Release(Handle* handle, bool erase_if_last_ref = false) = 0;
// Return the object assiciated with a handle returned by a successful
// Return the object associated with a handle returned by a successful
// Lookup(). For historical reasons, this is also known at the "value"
// associated with the key.
// REQUIRES: handle must not have been released yet.
+100 -54
View File
@@ -11,6 +11,8 @@
#pragma once
#include <variant>
#include "rocksdb/cache.h"
#include "rocksdb/compression_type.h"
#include "rocksdb/data_structure.h"
@@ -56,7 +58,64 @@ class Decompressor;
// because RocksDB is not exception-safe. This could cause undefined behavior
// including data loss, unreported corruption, deadlocks, and more.
class Compressor {
public:
public: // Auxiliary types
// No dictionary should be used (for a given block type).
struct DictDisabled {};
// A recommendation for dictionary compression by collecting samples from
// blocks. The caller should collect up to `max_sample_bytes` of sample data
// and pass it to MaybeCloneSpecialized() to create a specialized compressor.
struct DictSampling {
// Maximum total bytes of sample data to collect from blocks.
// This controls how much data is buffered before dictionary training.
size_t max_sample_bytes = 0;
};
// A pre-defined dictionary that is recommended or specified for direct use
// with MaybeCloneSpecialized(), without any sampling.
struct DictPreDefined {
// The owned raw/serialized dictionary bytes. Recommend std::move to
// MaybeCloneSpecialized()
std::string dict_data;
};
// The result type for GetDictGuidance() - indicates how dictionary
// compression should be configured for a given block type.
using DictConfig = std::variant<DictDisabled, DictSampling, DictPreDefined>;
// Sample data collected from blocks for dictionary training.
struct DictSamples {
// All the sample input blocks stored contiguously
std::string sample_data;
// The lengths of each of the sample blocks in `sample_data`
std::vector<size_t> sample_lens;
bool empty() const { return sample_data.empty(); }
bool Verify() const {
size_t total_len = 0;
for (auto len : sample_lens) {
total_len += len;
}
return total_len == sample_data.size();
}
};
// Arguments for MaybeCloneSpecialized() - provides either samples, a
// pre-defined dictionary, or indicates no dictionary should be used.
// NOTE: DictPreDefined here is the same type as above, allowing the
// pre-defined dictionary from GetDictGuidance() to be passed through.
using DictConfigArgs =
std::variant<DictDisabled, DictSamples, DictPreDefined>;
// A WorkingArea is an optional structure (both for callers and
// implementations) that can enable optimizing repeated compressions by
// reusing working space or thread-local tracking of statistics or trends.
// This enables use of ZSTD context, for example.
//
// EXTENSIBLE or reinterpret_cast-able by custom Compressor implementations
struct WorkingArea {};
public: // Functions
Compressor() = default;
virtual ~Compressor() = default;
@@ -69,15 +128,17 @@ class Compressor {
return id;
}
// Returns the max total bytes of for all sampled blocks for creating the data
// dictionary, or zero indicating dictionary compression should not be
// used/configured. This will typically be called after
// CompressionManager::GetCompressor() to see if samples should be accumulated
// and passed to MaybeCloneSpecialized().
virtual size_t GetMaxSampleSizeIfWantDict(CacheEntryRole block_type) const {
// Returns the recommended dictionary configuration for the given block type.
// See the comments on DictConfig and variants for details.
//
// NOTE: This may be called on the "base" Compressor returned by
// CompressionManager, which is not yet configured with a dictionary,
// or it can be skipped by callers not intending to handle dictionary
// compression.
virtual DictConfig GetDictGuidance(CacheEntryRole block_type) const {
// Default implementation: no dictionary
(void)block_type;
return 0;
return DictDisabled{};
}
// Returns the serialized form of the data dictionary associated with this
@@ -94,52 +155,32 @@ class Compressor {
// needed to implement MaybeCloneSpecialized() in wrapper compressors.
virtual std::unique_ptr<Compressor> Clone() const = 0;
// Utility struct for providing sample data for the compression dictionary.
// Potentially extensible by callers of Compressor (but not recommended)
struct DictSampleArgs {
// All the sample input blocks stored contiguously
std::string sample_data;
// The lengths of each of the sample blocks in `sample_data`
std::vector<size_t> sample_lens;
bool empty() { return sample_data.empty(); }
bool Verify() {
size_t total_len = 0;
for (auto len : sample_lens) {
total_len += len;
}
return total_len == sample_data.size();
}
};
// Create potential variants of the same Compressor that might be
// (a) optimized for a particular block type (does not affect correct
// decompression), and/or
// (b) configured to use a compression dictionary, based on the given
// samples (decompression must provide the dictionary from
// GetSerializedDict())
// (b) configured to use a compression dictionary based on the provided
// configuration (samples or pre-defined dictionary). See the comments on
// DictConfigArgs and its variants for detail.
//
// Return of nullptr indicates no specialization exists or was attempted
// and the caller is best to use the current Compressor for the desired
// scenario. Using CacheEntryRole:kMisc for block_type generally means
// "unspecified", and both parameters are merely suggestions. The exact
// dictionary associated with a returned compressor must be read from
// GetSerializedDict().
// and the caller should use the current Compressor for the desired scenario.
// Using CacheEntryRole::kMisc for block_type generally means "unspecified".
//
// The exact dictionary associated with a returned compressor must be read
// from GetSerializedDict().
virtual std::unique_ptr<Compressor> MaybeCloneSpecialized(
CacheEntryRole block_type, DictSampleArgs&& dict_samples) const {
CacheEntryRole block_type, DictConfigArgs&& dict_config) const {
// Default implementation: no specialization
(void)block_type;
(void)dict_samples;
// Caller should have checked GetMaxSampleSizeIfWantDict before attempting
// to provide dictionary samples
assert(dict_samples.empty());
(void)dict_config;
return nullptr;
}
// A convenience function when a clone is needed and may or may not be
// specialized.
std::unique_ptr<Compressor> CloneMaybeSpecialized(
CacheEntryRole block_type, DictSampleArgs&& dict_samples) const {
auto clone = MaybeCloneSpecialized(block_type, std::move(dict_samples));
CacheEntryRole block_type, DictConfigArgs&& dict_config) const {
auto clone = MaybeCloneSpecialized(block_type, std::move(dict_config));
if (clone == nullptr) {
clone = Clone();
assert(clone != nullptr);
@@ -147,14 +188,6 @@ class Compressor {
return clone;
}
// A WorkingArea is an optional structure (both for callers and
// implementations) that can enable optimizing repeated compressions by
// reusing working space or thread-local tracking of statistics or trends.
// This enables use of ZSTD context, for example.
//
// EXTENSIBLE or reinterpret_cast-able by custom Compressor implementations
struct WorkingArea {};
// To allow for flexible re-use / reclaimation, we have explicit Get and
// Release functions, and usually wrap in a special RAII smart pointer.
// For example, a WorkingArea could be saved/recycled in thread-local or
@@ -423,6 +456,12 @@ class CompressionManager
// which is valid at the discretion of the CompressionManager. Returning
// nullptr should normally be the result if preferred == kNoCompression.
//
// Compressors returned here are configured WITHOUT a dictionary, so that
// it's always possible to get correct compression->decompression results
// if not opting-in to dictionary handling. The compressors may recommend
// dictionary usage via GetDictGuidance() and creating a modified Compressor
// for that. See Compressor::GetDictGuidance() etc. for details.
//
// These functions must be thread-safe.
// Get a compressor for an SST file.
@@ -477,8 +516,8 @@ class CompressorWrapper : public Compressor {
CompressorWrapper(const CompressorWrapper&) = delete;
CompressorWrapper& operator=(const CompressorWrapper&) = delete;
size_t GetMaxSampleSizeIfWantDict(CacheEntryRole block_type) const override {
return wrapped_->GetMaxSampleSizeIfWantDict(block_type);
DictConfig GetDictGuidance(CacheEntryRole block_type) const override {
return wrapped_->GetDictGuidance(block_type);
}
Slice GetSerializedDict() const override {
@@ -496,9 +535,9 @@ class CompressorWrapper : public Compressor {
// when the wrapped Compressor uses the default implementation of
// MaybeCloneSpecialized(). This needs to be overridden if not.
std::unique_ptr<Compressor> MaybeCloneSpecialized(
CacheEntryRole block_type, DictSampleArgs&& dict_samples) const override {
CacheEntryRole block_type, DictConfigArgs&& dict_config) const override {
auto clone =
wrapped_->MaybeCloneSpecialized(block_type, std::move(dict_samples));
wrapped_->MaybeCloneSpecialized(block_type, std::move(dict_config));
// Assert default no-op MaybeCloneSpecialized()
assert(clone == nullptr);
return clone;
@@ -592,7 +631,14 @@ class CompressionManagerWrapper : public CompressionManager {
std::shared_ptr<CompressionManager> FindCompatibleCompressionManager(
Slice compatibility_name) override {
return wrapped_->FindCompatibleCompressionManager(compatibility_name);
// NOTE: We expect that the wrapped CompressionManager will generally
// be preferred if compatible, so the default implementation here does
// not purely defer to the wrapped instance
if (compatibility_name == CompatibilityName()) {
return shared_from_this();
} else {
return wrapped_->FindCompatibleCompressionManager(compatibility_name);
}
}
bool SupportsCompressionType(CompressionType type) const override {
+102
View File
@@ -86,6 +86,12 @@ typedef struct rocksdb_compactionfiltercontext_t
rocksdb_compactionfiltercontext_t;
typedef struct rocksdb_compactionfilterfactory_t
rocksdb_compactionfilterfactory_t;
typedef struct rocksdb_file_checksum_gen_factory_t
rocksdb_file_checksum_gen_factory_t;
typedef struct rocksdb_sst_partitioner_factory_t
rocksdb_sst_partitioner_factory_t;
typedef struct rocksdb_table_properties_collector_factory_t
rocksdb_table_properties_collector_factory_t;
typedef struct rocksdb_comparator_t rocksdb_comparator_t;
typedef struct rocksdb_dbpath_t rocksdb_dbpath_t;
typedef struct rocksdb_env_t rocksdb_env_t;
@@ -1225,6 +1231,8 @@ rocksdb_block_based_options_set_partition_pinning_tier(
extern ROCKSDB_LIBRARY_API void
rocksdb_block_based_options_set_unpartitioned_pinning_tier(
rocksdb_block_based_table_options_t*, int);
extern ROCKSDB_LIBRARY_API void rocksdb_block_based_options_set_block_align(
rocksdb_block_based_table_options_t*, unsigned char);
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_write_buffer_manager(
rocksdb_options_t* opt, rocksdb_write_buffer_manager_t* wbm);
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_sst_file_manager(
@@ -1471,6 +1479,31 @@ rocksdb_logger_create_callback_logger(int log_level,
void* priv);
extern ROCKSDB_LIBRARY_API void rocksdb_logger_destroy(
rocksdb_logger_t* logger);
/* File Checksum Gen Factory */
extern ROCKSDB_LIBRARY_API rocksdb_file_checksum_gen_factory_t*
rocksdb_file_checksum_gen_crc32c_factory_create(void);
extern ROCKSDB_LIBRARY_API void rocksdb_file_checksum_gen_factory_destroy(
rocksdb_file_checksum_gen_factory_t* factory);
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_file_checksum_gen_factory(
rocksdb_options_t*, rocksdb_file_checksum_gen_factory_t*);
/* SST Partitioner Factory */
extern ROCKSDB_LIBRARY_API rocksdb_sst_partitioner_factory_t*
rocksdb_sst_partitioner_fixed_prefix_factory_create(size_t prefix_len);
extern ROCKSDB_LIBRARY_API void rocksdb_sst_partitioner_factory_destroy(
rocksdb_sst_partitioner_factory_t* factory);
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_sst_partitioner_factory(
rocksdb_options_t*, rocksdb_sst_partitioner_factory_t*);
/* Table Properties Collector Factory */
extern ROCKSDB_LIBRARY_API void
rocksdb_table_properties_collector_factory_destroy(
rocksdb_table_properties_collector_factory_t* factory);
extern ROCKSDB_LIBRARY_API void
rocksdb_options_add_table_properties_collector_factory(
rocksdb_options_t*, rocksdb_table_properties_collector_factory_t*);
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_write_buffer_size(
rocksdb_options_t*, size_t);
extern ROCKSDB_LIBRARY_API size_t
@@ -3631,6 +3664,10 @@ extern ROCKSDB_LIBRARY_API void rocksdb_options_set_compaction_service(
extern ROCKSDB_LIBRARY_API rocksdb_compaction_service_options_override_t*
rocksdb_compaction_service_options_override_create(void);
extern ROCKSDB_LIBRARY_API rocksdb_compaction_service_options_override_t*
rocksdb_compaction_service_options_override_create_from_options(
rocksdb_options_t* option);
extern ROCKSDB_LIBRARY_API void
rocksdb_compaction_service_options_override_destroy(
rocksdb_compaction_service_options_override_t* override_options);
@@ -3645,6 +3682,71 @@ rocksdb_compaction_service_options_override_set_comparator(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_comparator_t* comparator);
extern ROCKSDB_LIBRARY_API void
rocksdb_compaction_service_options_override_set_merge_operator(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_mergeoperator_t* merge_operator);
extern ROCKSDB_LIBRARY_API void
rocksdb_compaction_service_options_override_set_compaction_filter(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_compactionfilter_t* compaction_filter);
extern ROCKSDB_LIBRARY_API void
rocksdb_compaction_service_options_override_set_compaction_filter_factory(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_compactionfilterfactory_t* compaction_filter_factory);
extern ROCKSDB_LIBRARY_API void
rocksdb_compaction_service_options_override_set_prefix_extractor(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_slicetransform_t* prefix_extractor);
extern ROCKSDB_LIBRARY_API void
rocksdb_compaction_service_options_override_set_block_based_table_factory(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_block_based_table_options_t* table_options);
extern ROCKSDB_LIBRARY_API void
rocksdb_compaction_service_options_override_set_cuckoo_table_factory(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_cuckoo_table_options_t* table_options);
extern ROCKSDB_LIBRARY_API void
rocksdb_compaction_service_options_override_add_event_listener(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_eventlistener_t* event_listener);
extern ROCKSDB_LIBRARY_API void
rocksdb_compaction_service_options_override_set_statistics(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_options_t* options);
extern ROCKSDB_LIBRARY_API void
rocksdb_compaction_service_options_override_set_info_log(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_logger_t* logger);
extern ROCKSDB_LIBRARY_API void
rocksdb_compaction_service_options_override_set_option(
rocksdb_compaction_service_options_override_t* override_options,
const char* key, const char* value);
extern ROCKSDB_LIBRARY_API void
rocksdb_compaction_service_options_override_set_file_checksum_gen_factory(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_file_checksum_gen_factory_t* factory);
extern ROCKSDB_LIBRARY_API void
rocksdb_compaction_service_options_override_set_sst_partitioner_factory(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_sst_partitioner_factory_t* factory);
extern ROCKSDB_LIBRARY_API void
rocksdb_compaction_service_options_override_add_table_properties_collector_factory(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_table_properties_collector_factory_t* factory);
// Atomic bool management for cancellation
// Creates an atomic bool that can be used for cancellation.
// User must call rocksdb_open_and_compact_canceled_destroy() to free it.
+27 -3
View File
@@ -1628,15 +1628,39 @@ class DB {
// s = db->SetOptions(cfh, {{"block_based_table_factory",
// "{prepopulate_block_cache=kDisable;}"}});
virtual Status SetOptions(
ColumnFamilyHandle* /*column_family*/,
const std::unordered_map<std::string, std::string>& /*opts_map*/) {
return Status::NotSupported("Not implemented");
ColumnFamilyHandle* column_family,
const std::unordered_map<std::string, std::string>& opts_map) {
return SetOptions(std::vector<ColumnFamilyHandle*>{column_family},
opts_map);
}
// Shortcut for SetOptions on the default column family handle.
virtual Status SetOptions(
const std::unordered_map<std::string, std::string>& new_options) {
return SetOptions(DefaultColumnFamily(), new_options);
}
// Shortcut where you want to apply the same options to multiple column
// families. Beneficial for avoiding reserialization of OPTIONS file.
virtual Status SetOptions(
const std::vector<ColumnFamilyHandle*>& column_families,
const std::unordered_map<std::string, std::string>& opts_map) {
std::unordered_map<ColumnFamilyHandle*,
std::unordered_map<std::string, std::string>>
column_families_opts_map;
column_families_opts_map.reserve(column_families.size());
for (auto* cf : column_families) {
column_families_opts_map[cf] = opts_map;
}
return SetOptions(column_families_opts_map);
}
// SetOptions with potentially different options per column family. It is
// typically better to batch all option changes together as the OPTIONS file
// is written to once per SetOptions call.
virtual Status SetOptions(
const std::unordered_map<ColumnFamilyHandle*,
std::unordered_map<std::string, std::string>>&
/*column_families_opts_map*/) {
return Status::NotSupported("Not implemented");
}
// Like SetOptions but for DBOptions, including the same caveats for
// usability, reliability, and performance. See GetDBOptionsFromMap() (and
+3 -3
View File
@@ -86,7 +86,7 @@ Status UpdateManifestForFilesState(
// keys in a category to return an empty sequence of segments.
//
// To eliminate a confusing distinction between a segment that is empty vs.
// "not present" for a particular key, each key is logically assiciated with
// "not present" for a particular key, each key is logically associated with
// an infinite sequence of segments, including some infinite tail of 0-length
// segments. In practice, we only represent a finite sequence that (at least)
// covers the non-trivial segments.
@@ -220,7 +220,7 @@ Status UpdateManifestForFilesState(
// whole key.
// * Range query - Whether there {definitely isn't, might be} any entries
// within a lower and upper key bound, in an SST file (or partition, etc.).
// NOTE: For this disucssion, we ignore the detail of inclusive vs.
// NOTE: For this discussion, we ignore the detail of inclusive vs.
// exclusive bounds by assuming a generalized notion of "bound" (vs. key)
// that conveniently represents spaces between keys. For details, see
// https://github.com/facebook/rocksdb/pull/11434
@@ -300,7 +300,7 @@ Status UpdateManifestForFilesState(
// * Keys x and z are in categories in category set s, and
// * Key y is ordered x < y < z according to the CF comparator,
// then both
// * The common segment prefix property is satisifed through ordinal i-1
// * The common segment prefix property is satisfied through ordinal i-1
// and with category set s
// * x_i..j <= y_i..j <= z_i..j according to segment comparator c, where
// x_i..j is the concatenation of segments i through j of key x (etc.).
+5
View File
@@ -22,7 +22,12 @@ namespace ROCKSDB_NAMESPACE {
// The unknown file checksum.
constexpr char kUnknownFileChecksum[] = "";
// The unknown sst file checksum function name.
// Indicates that the file metadata says that no checksum factory was configured
// when the file was written.
constexpr char kUnknownFileChecksumFuncName[] = "Unknown";
// Used when opening a file and there is no file checksum metadata to propagate
// at all.
constexpr char kNoFileChecksumFuncName[] = "Unavailable";
// The standard DB file checksum function name.
// This is the name of the checksum function returned by
// GetFileChecksumGenCrc32cFactory();
Loaded 100 of 165 files, more files were not shown because too many files have changed in this diff. Show more