mirror of
https://github.com/facebook/rocksdb.git
synced 2026-11-06 19:12:36 -05:00
Compare commits
152
Commits
11.9.fb
...
11.13.test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21a7975dfd | ||
|
|
bf1a9a1a20 | ||
|
|
163db5ee28 | ||
|
|
533ba40a62 | ||
|
|
834b2c21cb | ||
|
|
c7c3047b90 | ||
|
|
babce11705 | ||
|
|
ee305ac1c3 | ||
|
|
074134d98f | ||
|
|
7454655969 | ||
|
|
a7f6f33ccc | ||
|
|
61831d47be | ||
|
|
8121c7e254 | ||
|
|
54732b0c8f | ||
|
|
a3b0f187ff | ||
|
|
a215c38a8e | ||
|
|
df90749560 | ||
|
|
014e1c43c1 | ||
|
|
956c7042da | ||
|
|
9198b11b46 | ||
|
|
77eef9fa2e | ||
|
|
4b983da055 | ||
|
|
ddac37a780 | ||
|
|
76d7c29389 | ||
|
|
1334e4dd52 | ||
|
|
9fb698e218 | ||
|
|
baa4d373a6 | ||
|
|
4b44c2b4ba | ||
|
|
da82001d81 | ||
|
|
72beaf8dfb | ||
|
|
9b4b980c6a | ||
|
|
928527b869 | ||
|
|
f9b62a0567 | ||
|
|
2e689e3c00 | ||
|
|
78a9d3c98c | ||
|
|
39d56d5ad7 | ||
|
|
dec67990f0 | ||
|
|
9b4c5422f0 | ||
|
|
7292adcc9a | ||
|
|
6b15f1643e | ||
|
|
a38cd3a439 | ||
|
|
45203a867f | ||
|
|
0561153b73 | ||
|
|
1c490a4a4d | ||
|
|
60f090647d | ||
|
|
5b77e34985 | ||
|
|
3ea1ec56df | ||
|
|
f16157cfc5 | ||
|
|
c83a84e98c | ||
|
|
c0512c4c34 | ||
|
|
5f1132b884 | ||
|
|
c2b86f3ec0 | ||
|
|
8585299552 | ||
|
|
866e8e9f06 | ||
|
|
19b6f85266 | ||
|
|
4cd0450f31 | ||
|
|
b58cbcf13c | ||
|
|
4d180247ae | ||
|
|
ded3cb72a5 | ||
|
|
aeebcbded4 | ||
|
|
11516577ab | ||
|
|
9d358e63d6 | ||
|
|
31675ca6d0 | ||
|
|
ff29163e47 | ||
|
|
176d7c7ea1 | ||
|
|
b4e068e1ed | ||
|
|
eed2f96676 | ||
|
|
5fa16200aa | ||
|
|
7510773057 | ||
|
|
5eed79a0e1 | ||
|
|
3d381e1bd4 | ||
|
|
f8d3141e5c | ||
|
|
4052fccde9 | ||
|
|
ac30c4d738 | ||
|
|
dbc2db5228 | ||
|
|
07cbacadb4 | ||
|
|
662cdbc9ec | ||
|
|
ffd4ac31af | ||
|
|
0675830967 | ||
|
|
14a82121d4 | ||
|
|
507206e46b | ||
|
|
3c30d43baf | ||
|
|
96e7ef0269 | ||
|
|
5d6f321b70 | ||
|
|
256a699be3 | ||
|
|
7530287bde | ||
|
|
b03a058d61 | ||
|
|
8108b49e0c | ||
|
|
dd54c5abc9 | ||
|
|
7d721f398b | ||
|
|
bcf8711165 | ||
|
|
34d5074bfa | ||
|
|
79b58b0dd6 | ||
|
|
613a642b5c | ||
|
|
e231c1826c | ||
|
|
1419ae41b1 | ||
|
|
db114777b1 | ||
|
|
7a90dc5ba8 | ||
|
|
a4e298b8c4 | ||
|
|
576dc39899 | ||
|
|
4c2a977aa5 | ||
|
|
88a8fdf849 | ||
|
|
a844cbf5b7 | ||
|
|
3d8e4be2f5 | ||
|
|
7d3150ffce | ||
|
|
91b96f41cf | ||
|
|
dfde6a60b0 | ||
|
|
bcda8c4b1a | ||
|
|
37234200b5 | ||
|
|
ac0f4ae69f | ||
|
|
bec9dcec7c | ||
|
|
897162a6bc | ||
|
|
1ca38e1442 | ||
|
|
da3a0961fa | ||
|
|
96ed085402 | ||
|
|
2c1c7ea2f1 | ||
|
|
fe6406f7b2 | ||
|
|
0df7ed563b | ||
|
|
068aeda9d8 | ||
|
|
a0966fd94e | ||
|
|
fe5123681e | ||
|
|
f9c117e203 | ||
|
|
82dfd6a510 | ||
|
|
80a5a6e4b2 | ||
|
|
d699ffbd5c | ||
|
|
e4a7136d3e | ||
|
|
f7b791b213 | ||
|
|
6dbb6f30e6 | ||
|
|
bd16531142 | ||
|
|
e6a2ee0bd2 | ||
|
|
ad81b654a4 | ||
|
|
8f492b2f47 | ||
|
|
e8554f1b11 | ||
|
|
330ded5129 | ||
|
|
4b41ccd2a7 | ||
|
|
2846163d9e | ||
|
|
03b8d1afa0 | ||
|
|
42fca1bb39 | ||
|
|
d3a82f28ce | ||
|
|
d8536d3274 | ||
|
|
136d0d1fa1 | ||
|
|
49faa51e11 | ||
|
|
7d18ffe316 | ||
|
|
db1adf47bb | ||
|
|
bbdcd2825f | ||
|
|
495c21dc5c | ||
|
|
575b6df1b8 | ||
|
|
f0c1e3d7d9 | ||
|
|
b616b203b8 | ||
|
|
92a6308300 | ||
|
|
021374c62c | ||
|
|
9475798205 |
No files matched your search
@@ -12,6 +12,7 @@ jobs:
|
||||
labels: 4-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
|
||||
options: --shm-size=8gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
with:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: facebook/rocksdb/nightly
|
||||
name: facebook/rocksdb/nightly-candidate
|
||||
on: workflow_dispatch
|
||||
permissions: {}
|
||||
jobs:
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
labels: arm64large
|
||||
container:
|
||||
image: ubuntu-2004:202111-02
|
||||
options: --shm-size=16gb
|
||||
options: --shm-size=8gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
options: --shm-size=32gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
with:
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
options: --shm-size=32gb
|
||||
env:
|
||||
TEST_TMPDIR: "/tmp/rocksdb_test_tmp"
|
||||
steps:
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
|
||||
options: --shm-size=16gb
|
||||
options: --shm-size=32gb
|
||||
env:
|
||||
CC: clang-21
|
||||
CXX: clang++-21
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
options: --shm-size=32gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
@@ -103,7 +103,7 @@ jobs:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
options: --shm-size=32gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
@@ -161,7 +161,7 @@ jobs:
|
||||
labels: 4-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
options: --shm-size=8gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
@@ -174,7 +174,7 @@ jobs:
|
||||
labels: 4-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
|
||||
options: --shm-size=16gb
|
||||
options: --shm-size=8gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
@@ -189,7 +189,7 @@ jobs:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
options: --shm-size=32gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
options: --shm-size=32gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
labels: 4-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
|
||||
options: --shm-size=16gb
|
||||
options: --shm-size=8gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
labels: 32-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
options: --shm-size=64gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
@@ -160,7 +160,7 @@ jobs:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
options: --shm-size=32gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
@@ -180,7 +180,7 @@ jobs:
|
||||
labels: 32-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
options: --shm-size=64gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
@@ -209,7 +209,7 @@ jobs:
|
||||
shard: [0, 1, 2, 3]
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
options: --shm-size=32gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
@@ -232,7 +232,7 @@ jobs:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
options: --shm-size=32gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
@@ -252,7 +252,7 @@ jobs:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
options: --shm-size=32gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
@@ -306,7 +306,7 @@ jobs:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
|
||||
options: --shm-size=16gb
|
||||
options: --shm-size=32gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
@@ -373,7 +373,7 @@ jobs:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
|
||||
options: --shm-size=16gb
|
||||
options: --shm-size=32gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
@@ -394,7 +394,7 @@ jobs:
|
||||
labels: 4-core-ubuntu
|
||||
container:
|
||||
image: gcc:latest
|
||||
options: --shm-size=16gb
|
||||
options: --shm-size=8gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- run: apt-get update -y && apt-get install -y libgflags-dev
|
||||
@@ -422,7 +422,7 @@ jobs:
|
||||
crash_duration: 240
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb --ulimit nofile=1048576:1048576
|
||||
options: --shm-size=8gb --ulimit nofile=1048576:1048576
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
@@ -457,7 +457,7 @@ jobs:
|
||||
shard: [0, 1, 2]
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
|
||||
options: --shm-size=16gb
|
||||
options: --shm-size=64gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
@@ -484,7 +484,7 @@ jobs:
|
||||
shard: [0, 1, 2]
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
|
||||
options: --shm-size=16gb
|
||||
options: --shm-size=64gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
@@ -507,7 +507,7 @@ jobs:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
options: --shm-size=32gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
@@ -614,7 +614,7 @@ jobs:
|
||||
labels: 4-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
options: --shm-size=8gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
@@ -637,7 +637,7 @@ jobs:
|
||||
labels: 4-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
options: --shm-size=8gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
@@ -740,7 +740,7 @@ jobs:
|
||||
labels: 4-core-ubuntu
|
||||
container:
|
||||
image: evolvedbinary/rocksjava:alpine3_x64-be
|
||||
options: --shm-size=16gb
|
||||
options: --shm-size=8gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/install-maven"
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
timeout-minutes: 840
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
options: --shm-size=32gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
|
||||
@@ -96,6 +96,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"db/merge_operator.cc",
|
||||
"db/multi_scan.cc",
|
||||
"db/output_validator.cc",
|
||||
"db/periodic_compaction_phaser.cc",
|
||||
"db/periodic_task_scheduler.cc",
|
||||
"db/range_del_aggregator.cc",
|
||||
"db/range_tombstone_fragmenter.cc",
|
||||
@@ -205,6 +206,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"table/block_based/block_cache.cc",
|
||||
"table/block_based/block_prefetcher.cc",
|
||||
"table/block_based/block_prefix_index.cc",
|
||||
"table/block_based/builtin_index_factory.cc",
|
||||
"table/block_based/data_block_footer.cc",
|
||||
"table/block_based/data_block_hash_index.cc",
|
||||
"table/block_based/filter_block_reader_common.cc",
|
||||
@@ -239,6 +241,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"table/plain/plain_table_index.cc",
|
||||
"table/plain/plain_table_key_coding.cc",
|
||||
"table/plain/plain_table_reader.cc",
|
||||
"table/range_del_block.cc",
|
||||
"table/sst_file_dumper.cc",
|
||||
"table/sst_file_reader.cc",
|
||||
"table/sst_file_writer.cc",
|
||||
@@ -413,13 +416,27 @@ cpp_library_wrapper(name="rocksdb_test_lib", srcs=[
|
||||
"table/mock_table.cc",
|
||||
"test_util/mock_time_env.cc",
|
||||
"test_util/secondary_cache_test_util.cc",
|
||||
"test_util/simple_external_table_factory.cc",
|
||||
"test_util/testharness.cc",
|
||||
"test_util/testutil.cc",
|
||||
"tools/block_cache_analyzer/block_cache_trace_analyzer.cc",
|
||||
"tools/trace_analyzer_tool.cc",
|
||||
"utilities/agg_merge/test_agg_merge.cc",
|
||||
"utilities/cassandra/test_utils.cc",
|
||||
], deps=[":rocksdb_lib"], headers=[], link_whole=False, extra_test_libs=True)
|
||||
], deps=[":rocksdb_lib"], headers=[
|
||||
"db/db_test_util.h",
|
||||
"db/db_with_timestamp_test_util.h",
|
||||
"table/mock_table.h",
|
||||
"test_util/mock_time_env.h",
|
||||
"test_util/secondary_cache_test_util.h",
|
||||
"test_util/simple_external_table_factory.h",
|
||||
"test_util/testharness.h",
|
||||
"test_util/testutil.h",
|
||||
"tools/block_cache_analyzer/block_cache_trace_analyzer.h",
|
||||
"tools/trace_analyzer_tool.h",
|
||||
"utilities/agg_merge/test_agg_merge.h",
|
||||
"utilities/cassandra/test_utils.h",
|
||||
], link_whole=False, extra_test_libs=True)
|
||||
|
||||
cpp_library_wrapper(name="rocksdb_with_faiss_test_lib", srcs=[
|
||||
"db/db_test_util.cc",
|
||||
@@ -427,13 +444,27 @@ cpp_library_wrapper(name="rocksdb_with_faiss_test_lib", srcs=[
|
||||
"table/mock_table.cc",
|
||||
"test_util/mock_time_env.cc",
|
||||
"test_util/secondary_cache_test_util.cc",
|
||||
"test_util/simple_external_table_factory.cc",
|
||||
"test_util/testharness.cc",
|
||||
"test_util/testutil.cc",
|
||||
"tools/block_cache_analyzer/block_cache_trace_analyzer.cc",
|
||||
"tools/trace_analyzer_tool.cc",
|
||||
"utilities/agg_merge/test_agg_merge.cc",
|
||||
"utilities/cassandra/test_utils.cc",
|
||||
], deps=[":rocksdb_with_faiss_lib"], headers=[], link_whole=False, extra_test_libs=True)
|
||||
], deps=[":rocksdb_with_faiss_lib"], headers=[
|
||||
"db/db_test_util.h",
|
||||
"db/db_with_timestamp_test_util.h",
|
||||
"table/mock_table.h",
|
||||
"test_util/mock_time_env.h",
|
||||
"test_util/secondary_cache_test_util.h",
|
||||
"test_util/simple_external_table_factory.h",
|
||||
"test_util/testharness.h",
|
||||
"test_util/testutil.h",
|
||||
"tools/block_cache_analyzer/block_cache_trace_analyzer.h",
|
||||
"tools/trace_analyzer_tool.h",
|
||||
"utilities/agg_merge/test_agg_merge.h",
|
||||
"utilities/cassandra/test_utils.h",
|
||||
], link_whole=False, extra_test_libs=True)
|
||||
|
||||
cpp_library_wrapper(name="rocksdb_tools_lib", srcs=[
|
||||
"test_util/testutil.cc",
|
||||
@@ -442,7 +473,12 @@ cpp_library_wrapper(name="rocksdb_tools_lib", srcs=[
|
||||
"tools/simulated_hybrid_file_system.cc",
|
||||
"tools/tool_hooks.cc",
|
||||
"tools/trace_analyzer_tool.cc",
|
||||
], deps=[":rocksdb_lib"], headers=[], link_whole=False, extra_test_libs=False)
|
||||
], deps=[":rocksdb_lib"], headers=[
|
||||
"test_util/testutil.h",
|
||||
"tools/block_cache_analyzer/block_cache_trace_analyzer.h",
|
||||
"tools/simulated_hybrid_file_system.h",
|
||||
"tools/trace_analyzer_tool.h",
|
||||
], link_whole=False, extra_test_libs=False)
|
||||
|
||||
cpp_library_wrapper(name="rocksdb_cache_bench_tools_lib", srcs=["cache/cache_bench_tool.cc"], deps=[":rocksdb_lib"], headers=[], link_whole=False, extra_test_libs=False)
|
||||
|
||||
@@ -466,10 +502,28 @@ rocks_cpp_library_wrapper(name="rocksdb_stress_lib", srcs=[
|
||||
"db_stress_tool/expected_value.cc",
|
||||
"db_stress_tool/multi_ops_txns_stress.cc",
|
||||
"db_stress_tool/no_batched_ops_stress.cc",
|
||||
"test_util/simple_external_table_factory.cc",
|
||||
"test_util/testutil.cc",
|
||||
"tools/block_cache_analyzer/block_cache_trace_analyzer.cc",
|
||||
"tools/trace_analyzer_tool.cc",
|
||||
], headers=[])
|
||||
], headers=[
|
||||
"db_stress_tool/db_stress_common.h",
|
||||
"db_stress_tool/db_stress_compaction_service.h",
|
||||
"db_stress_tool/db_stress_compression_manager.h",
|
||||
"db_stress_tool/db_stress_driver.h",
|
||||
"db_stress_tool/db_stress_filters.h",
|
||||
"db_stress_tool/db_stress_listener.h",
|
||||
"db_stress_tool/db_stress_shared_state.h",
|
||||
"db_stress_tool/db_stress_test_base.h",
|
||||
"db_stress_tool/db_stress_wide_merge_operator.h",
|
||||
"db_stress_tool/expected_state.h",
|
||||
"db_stress_tool/expected_value.h",
|
||||
"db_stress_tool/multi_ops_txns_stress.h",
|
||||
"test_util/simple_external_table_factory.h",
|
||||
"test_util/testutil.h",
|
||||
"tools/block_cache_analyzer/block_cache_trace_analyzer.h",
|
||||
"tools/trace_analyzer_tool.h",
|
||||
])
|
||||
|
||||
|
||||
cpp_binary_wrapper(name="ldb", srcs=["tools/ldb.cc"], deps=[":rocksdb_tools_lib"], extra_preprocessor_flags=[], extra_bench_libs=False)
|
||||
@@ -4613,6 +4667,12 @@ cpp_unittest_wrapper(name="bloom_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="builtin_index_factory_test",
|
||||
srcs=["table/block_based/builtin_index_factory_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="cache_reservation_manager_test",
|
||||
srcs=["cache/cache_reservation_manager_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
@@ -4753,6 +4813,7 @@ cpp_unittest_wrapper(name="compression_test",
|
||||
|
||||
cpp_unittest_wrapper(name="configurable_test",
|
||||
srcs=["options/configurable_test.cc"],
|
||||
source_headers=["options/configurable_test.h"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
@@ -5457,6 +5518,7 @@ cpp_unittest_wrapper(name="periodic_task_scheduler_test",
|
||||
|
||||
cpp_unittest_wrapper(name="persistent_cache_test",
|
||||
srcs=["utilities/persistent_cache/persistent_cache_test.cc"],
|
||||
source_headers=["utilities/persistent_cache/persistent_cache_test.h"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
@@ -5475,6 +5537,7 @@ cpp_unittest_wrapper(name="point_lock_manager_stress_test",
|
||||
|
||||
cpp_unittest_wrapper(name="point_lock_manager_test",
|
||||
srcs=["utilities/transactions/lock/point/point_lock_manager_test.cc"],
|
||||
source_headers=["utilities/transactions/lock/point/point_lock_manager_test.h"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
@@ -5685,6 +5748,7 @@ cpp_unittest_wrapper(name="trace_analyzer_test",
|
||||
|
||||
cpp_unittest_wrapper(name="transaction_test",
|
||||
srcs=["utilities/transactions/transaction_test.cc"],
|
||||
source_headers=["utilities/transactions/transaction_test.h"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
@@ -8,12 +8,18 @@ This document provides guidance for generating and reviewing code in the RocksDB
|
||||
|
||||
### Code Quality and Maintainability
|
||||
|
||||
**Clarity and Readability:** Write clear, self-documenting code. Use meaningful variable names, add comments for complex logic, and structure code to minimize cognitive load. Avoid clever tricks that sacrifice readability for marginal performance gains unless absolutely necessary. Avoid static_cast, reinterpret_cast, and C-style casts; static_cast_with_check, up_cast, and lossless_cast from cast_util.h are preferred.
|
||||
**Clarity and Readability:** Write clear, self-documenting code. Use meaningful variable names, add comments for complex logic, and structure code to minimize cognitive load. Avoid clever tricks that sacrifice readability for marginal performance gains unless absolutely necessary.
|
||||
|
||||
**Consistent Style:** Follow existing code style conventions. RocksDB uses `.clang-format` for formatting, specific naming conventions, and structural patterns. Deviations from these patterns are frequently flagged in reviews.
|
||||
|
||||
**Error Handling:** Ensure robust error handling throughout the codebase. Use RocksDB's `Status` type consistently, propagate errors appropriately, and avoid silently ignoring failures. Reviewers pay close attention to edge cases and failure modes.
|
||||
|
||||
**Refactoring traps:** At least in production code, avoid constructs that could allow existing code to unexpectedly, quietly change meaning.
|
||||
Specifically:
|
||||
* Avoid new defaulted parameters. This is the #1 trap on refactoring!
|
||||
* Avoid static_cast, reinterpret_cast, and C-style casts; static_cast_with_check, up_cast, and lossless_cast from cast_util.h are preferred.
|
||||
* Avoid declared type `auto` (`auto&`, `auto*` OK).
|
||||
|
||||
### Testing Philosophy
|
||||
|
||||
**Comprehensive Coverage:** Every change should include appropriate test coverage. This includes unit tests for isolated functionality, integration tests for component interactions, and stress tests for concurrency and performance validation. Reviewers will ask for additional tests if coverage is insufficient.
|
||||
@@ -32,7 +38,7 @@ This document provides guidance for generating and reviewing code in the RocksDB
|
||||
|
||||
**Memory Copy:** Avoid unnecessary memory copies. Use move semantics, `std::string_view`, `Slice`, and pass-by-reference where appropriate. Be aware of implicit copies in STL containers and function returns. Prefer in-place operations over copy-and-modify patterns.
|
||||
|
||||
**CPU Cache Efficiency:** Design data structures and access patterns to be cache-friendly. Keep frequently accessed data together (data locality). Prefer sequential memory access over random access. Be mindful of cache line sizes (typically 64 bytes) and avoid false sharing in concurrent code. Consider struct packing and field ordering to improve cache utilization.
|
||||
**CPU Cache Efficiency:** Design data structures and access patterns to be cache-friendly. Keep frequently accessed data together (data locality). Prefer sequential memory access over random access. Be mindful of cache line sizes (CACHE_LINE_SIZE, typically 64 bytes) and avoid false sharing in concurrent code (see CacheAlignedWrapper). Especially when adding new data members, be mindful of ordering within structs and classes to reduce unnecessary padding (special care with bool members) and avoid larger-than-necessary types (e.g. prefer OptSlice to std::optional<Slice>).
|
||||
|
||||
**Loop Optimization:** Look for opportunities to collapse nested loops, reduce loop overhead, and minimize branch mispredictions. Hoist invariant computations out of loops. Consider loop unrolling for tight inner loops. Batch operations when possible to amortize per-operation overhead.
|
||||
|
||||
@@ -47,12 +53,14 @@ This document provides guidance for generating and reviewing code in the RocksDB
|
||||
- **Cold path** (executed rarely, e.g., DB open, configuration parsing, error handling): Maintainability and clarity are more important. Prefer readable code over micro-optimizations. Complex optimizations here add maintenance burden with negligible performance benefit.
|
||||
- **Warm path** (moderate frequency): Balance both concerns. Use profiling data to guide optimization decisions.
|
||||
|
||||
**Avoid Premature Optimization:** While performance is critical, focus on correctness first, then optimize based on profiling data. However, be performance-aware from the start—choosing the right algorithm and data structure upfront is not premature optimization. Use the hot path analysis above to decide how much optimization effort is warranted.
|
||||
**Avoid Premature Optimization:** Performance is known critical in places like inner loops of compaction, read path and write path. Elsewhere, try to find a "holy trinity" solution of *correct, simple, and reasonably efficient*. More complex solutions are only appropriate when profiling/benchmarking data shows a measurable payoff.
|
||||
|
||||
### API Design and Compatibility
|
||||
|
||||
**Backwards Compatibility:** RocksDB maintains strong backwards compatibility guarantees. Breaking changes are rare and require extensive justification. When deprecating features, follow the project's deprecation policy (typically spanning multiple releases).
|
||||
|
||||
**Behavioral Compatibility:** Behavior changes are human judgment calls that must be intentional, well-documented (including release notes), and well researched for potential downstream production impact. Agents and reviewers must call out potential gaps, ambiguity, or adverse impacts in intentionality, documentation, and production impact. For an existing accepted input—including an undocumented, discouraged, or historically ignored value—the analysis should compare old and new observable behavior, including performance and resource impact, identify known direct callers and stored configurations, and add or update regression coverage as appropriate.
|
||||
|
||||
**API Consistency:** New APIs should be consistent with existing patterns. Use similar naming conventions, parameter ordering, and return types. Reviewers will suggest changes to improve consistency with the broader codebase.
|
||||
|
||||
**Documentation:** Public APIs must be thoroughly documented, without unnecessary embelishment nor dwelling on implementation details nor project planning. When non-obvious, include usage examples, parameter descriptions, known bugs or limitations, notes on thread safety, performance characteristics, and compatibility considerations. Re-read comments for ambiguous terminology and phrasing, such as ambiguously re-purposed programming jargon.
|
||||
@@ -73,7 +81,7 @@ The database core handles write-ahead logging (WAL), memtables, compaction, and
|
||||
|
||||
**Testing:** Database core changes require extensive testing, including unit tests, integration tests, and stress tests. Test with various configurations, compaction styles, and concurrent workloads.
|
||||
|
||||
### Public Headers (`include`)
|
||||
### Public Headers / "Public API" (`include/rocksdb/`)
|
||||
|
||||
Public headers define RocksDB's API surface. Changes here have the highest compatibility impact.
|
||||
|
||||
@@ -236,6 +244,11 @@ from an implementation detail instead of an explicit option.
|
||||
|
||||
## Important tips
|
||||
|
||||
### Pull request and diff summaries
|
||||
* Before writing or updating an external RocksDB pull request summary or its
|
||||
corresponding internal Phabricator diff summary, read and follow
|
||||
`claude_md/pull_request_summary_guideline.md`.
|
||||
|
||||
### Build system
|
||||
* There are 3 build system. Make for git clones, BUCK (meta internal) for hg
|
||||
clones, and CMake for some special cases.
|
||||
@@ -302,6 +315,7 @@ build (and where noted, run tests) across:
|
||||
| Axis | Must support |
|
||||
|------|--------------|
|
||||
| OS | Linux (x86_64 + ARM), macOS, Windows |
|
||||
| Architecture | Little-endian and big-endian targets, including Linux on s390x |
|
||||
| Compiler | GCC, Clang (libstdc++ **and** libc++), AppleClang, **MSVC (VS2022)**, MinGW (Linux cross-compile, build-only, no gflags) |
|
||||
| Build system | Make, CMake, and BUCK (internal) -- keep all in sync (see "Build system" above) |
|
||||
| Config | release (`-fno-rtti`), `ASSERT_STATUS_CHECKED`, ASAN/UBSAN/TSAN, folly, unity build, JNI/Java |
|
||||
@@ -316,6 +330,12 @@ abstractions; otherwise guard with `#ifdef OS_WIN` (POSIX `<unistd.h>` ->
|
||||
Windows `<process.h>`). Because libc++ is also tested, include what you use
|
||||
rather than relying on libstdc++ transitive includes.
|
||||
|
||||
Do not use `static_assert` to reject a supported architecture for an optional
|
||||
feature. Check an existing `port::` capability such as `port::kLittleEndian`
|
||||
and disable only the unsupported feature so the rest of RocksDB still builds.
|
||||
Use a portable representation when the feature itself must work across
|
||||
architectures.
|
||||
|
||||
### Unit Test
|
||||
* After all of the unit tests are added, review them and try to extract common
|
||||
reusable utility functions to reduce code duplication due to copy past between
|
||||
@@ -366,6 +386,9 @@ rather than relying on libstdc++ transitive includes.
|
||||
### Adding new public API
|
||||
Refer to claude_md/add_public_api.md
|
||||
|
||||
### Changing existing public API behavior
|
||||
Follow the Behavioral Compatibility requirements above.
|
||||
|
||||
### Adding new option
|
||||
Refer to claude_md/add_option.md
|
||||
|
||||
@@ -395,7 +418,7 @@ rather than relying on libstdc++ transitive includes.
|
||||
* Release note should be kept short at high level for external user consumption.
|
||||
Release notes identify what users might care about most in a release. They
|
||||
are not exhaustive and are not a guide. PLEASE learn from past agents who
|
||||
ried to build elaborate release notes with implementation details and
|
||||
tried to build elaborate release notes with implementation details and
|
||||
elsewhere-documented nuance. That wastes time. Fight the bias that
|
||||
"my change" is important so must be worthy of release note mention.
|
||||
* If more than single markdown line, consider how their formatting will be
|
||||
@@ -411,7 +434,9 @@ rather than relying on libstdc++ transitive includes.
|
||||
multiple minutes.
|
||||
* Run `AUTO_CLEAN=1 ASSERT_STATUS_CHECKED=1 make check` to verify all Status
|
||||
objects are properly checked. This catches missing error handling that can
|
||||
lead to silent data corruption.
|
||||
lead to silent data corruption. Merely building with ASSERT_STATUS_CHECKED=1
|
||||
accomplishes nothing; it enables a runtime check. New unit tests missing
|
||||
Status checks is a common failure point, even from agents.
|
||||
|
||||
### Monitoring make check progress
|
||||
* Use `make check-progress` to get machine-parseable JSON progress while
|
||||
|
||||
@@ -650,6 +650,9 @@ if(USE_FOLLY)
|
||||
exec_program(ls ARGS -d ${FOLLY_INST_PATH}/../glog* OUTPUT_VARIABLE
|
||||
GLOG_INST_PATH)
|
||||
set(Boost_DIR ${BOOST_INST_PATH}/lib/cmake/Boost-1.83.0)
|
||||
# Boost's per-library CMake configs find their dependencies (e.g.
|
||||
# boost_assert) only through the package search path.
|
||||
set(CMAKE_PREFIX_PATH ${BOOST_INST_PATH} ${CMAKE_PREFIX_PATH})
|
||||
if(EXISTS ${FMT_INST_PATH}/lib64)
|
||||
set(fmt_DIR ${FMT_INST_PATH}/lib64/cmake/fmt)
|
||||
else()
|
||||
@@ -696,7 +699,13 @@ if(USE_FOLLY)
|
||||
exec_program(sed ARGS -i 's/gflags_shared//g'
|
||||
${FOLLY_INST_PATH}/lib/cmake/folly/folly-targets.cmake)
|
||||
|
||||
# folly-config.cmake only checks for gflags under a target name that this
|
||||
# install doesn't define, then falls back to our Findgflags.cmake, which
|
||||
# mistakes the gflags_static target name for a library path. gflags is
|
||||
# resolved below instead.
|
||||
set(CMAKE_DISABLE_FIND_PACKAGE_gflags TRUE)
|
||||
include(${FOLLY_INST_PATH}/lib/cmake/folly/folly-config.cmake)
|
||||
unset(CMAKE_DISABLE_FIND_PACKAGE_gflags)
|
||||
|
||||
# Fix gflags library name for debug builds. The getdeps gflags shared
|
||||
# library is versioned (e.g. libgflags_debug.so.2.3), so resolve the
|
||||
@@ -838,6 +847,7 @@ set(SOURCES
|
||||
db/merge_operator.cc
|
||||
db/multi_scan.cc
|
||||
db/output_validator.cc
|
||||
db/periodic_compaction_phaser.cc
|
||||
db/periodic_task_scheduler.cc
|
||||
db/range_del_aggregator.cc
|
||||
db/range_tombstone_fragmenter.cc
|
||||
@@ -937,6 +947,7 @@ set(SOURCES
|
||||
table/block_based/block_cache.cc
|
||||
table/block_based/block_prefetcher.cc
|
||||
table/block_based/block_prefix_index.cc
|
||||
table/block_based/builtin_index_factory.cc
|
||||
table/block_based/data_block_hash_index.cc
|
||||
table/block_based/data_block_footer.cc
|
||||
table/block_based/filter_block_reader_common.cc
|
||||
@@ -970,6 +981,7 @@ set(SOURCES
|
||||
table/plain/plain_table_index.cc
|
||||
table/plain/plain_table_key_coding.cc
|
||||
table/plain/plain_table_reader.cc
|
||||
table/range_del_block.cc
|
||||
table/sst_file_dumper.cc
|
||||
table/sst_file_reader.cc
|
||||
table/sst_file_writer.cc
|
||||
@@ -1594,6 +1606,7 @@ if(WITH_TESTS)
|
||||
options/options_test.cc
|
||||
table/block_based/block_based_table_reader_test.cc
|
||||
table/block_based/block_test.cc
|
||||
table/block_based/builtin_index_factory_test.cc
|
||||
table/block_based/data_block_hash_index_test.cc
|
||||
table/block_based/full_filter_block_test.cc
|
||||
table/block_based/partitioned_filter_block_test.cc
|
||||
@@ -1685,6 +1698,7 @@ if(WITH_TESTS)
|
||||
db/db_with_timestamp_test_util.cc
|
||||
monitoring/thread_status_updater_debug.cc
|
||||
table/mock_table.cc
|
||||
test_util/simple_external_table_factory.cc
|
||||
utilities/agg_merge/test_agg_merge.cc
|
||||
utilities/cassandra/test_utils.cc
|
||||
)
|
||||
|
||||
+159
@@ -1,6 +1,165 @@
|
||||
# Rocksdb Change Log
|
||||
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
|
||||
|
||||
## 11.13.2 (10/21/2026)
|
||||
### Bug Fixes
|
||||
* Fixed blob direct write with more than one concurrent flush of a column family: a later flush sealed and published the blob files of the oldest pending memtable instead of its own, so flushes failed with `Corruption: VersionBuilder: Blob file #N already added` and the database stopped.
|
||||
|
||||
## 11.13.1 (10/23/2026)
|
||||
### Bug Fixes
|
||||
* Fixed data loss with `min_tombstones_for_range_conversion` and external file ingestion. An iterator whose view predated the ingestion could convert the deleted keys around the ingested keys into a range tombstone that deleted them. This happened when the file got no new sequence number (sequence number zero, `ingest_behind`, or `allow_db_generated_files`), and when an iterator was created during the ingestion.
|
||||
* Fixed a data loss bug where an iterator with `ReadOptions::read_tier = kMemtableTier` could, with `min_tombstones_for_range_conversion` enabled, insert a range tombstone that deleted live keys stored only in SST files. Read-path range tombstone conversion now runs only for iterators with `read_tier = kReadAllTier`.
|
||||
|
||||
## 11.13.0 (10/21/2026)
|
||||
### New Features
|
||||
* Added `FileSystem::IsPreallocationSupported()`. A `FileSystem` that never preallocates file space can return false so that DB open skips reopening and truncating the last WAL file.
|
||||
|
||||
### Public API Changes
|
||||
* Added the experimental `DBOptions::track_options_file_number_in_manifest`
|
||||
rollout option (with C and Java bindings). The option is persisted in each
|
||||
OPTIONS file. When enabled, RocksDB durably creates an OPTIONS file and then
|
||||
commits its file number in MANIFEST. Column-family create/drop commits the
|
||||
configuration snapshot and membership in one MANIFEST record. A newer tracked
|
||||
OPTIONS file without a commit is ignored after a crash, while a newer OPTIONS
|
||||
file written with tracking disabled retains the legacy highest-numbered-file
|
||||
behavior. Batched column-family changes publish one snapshot; after partial
|
||||
success it can be a safe superset that persisted-options APIs filter against
|
||||
live MANIFEST membership. Ordinary `DB::Open` continues to use caller-supplied
|
||||
options and does not depend on the operational snapshot.
|
||||
* Added `EncodeEntry()`, `PackSequenceAndType()`, and `UnPackSequenceAndType()` in `rocksdb/utilities/types_util.h` to construct internal keys and pack or unpack their sequence numbers and entry types. `kInternalKeyFooterSize` exposes the footer size in bytes.
|
||||
* Added `ldb update_manifest --rebind_options_file --options_file=OPTIONS-<number>` for offline repair of a committed MANIFEST OPTIONS file number. Operators must select a trusted snapshot that has MANIFEST tracking enabled and provide any external DB/CF allocation roots that are not serialized in OPTIONS.
|
||||
|
||||
### Behavior Changes
|
||||
* With `max_bgerror_resume_count <= 0` (auto resume disabled), a retryable or file-scoped IO error during compaction or flush is now a hard background error that stops writes until `DB::Resume()` is called. Previously, compaction errors were silently retried, and flush errors with a healthy WAL were soft errors that let writes continue while flushes were stopped.
|
||||
|
||||
### Bug Fixes
|
||||
* Fixed two more `SstFileManager` trash accounting leaks: retrying a failed trash deletion immediately (for example with rate limiting disabled) after the file had disappeared, and a trash file that was deleted but whose directory sync then failed. In both cases the file's size stayed in the total trash size and tracked size until restart.
|
||||
* Fixed `SstFileManager` trash accounting leaking the size of a queued trash file that disappeared before deletion (for example because its directory was renamed). The leaked bytes stayed in the total trash size until restart, which could push every later deletion past `max_trash_db_ratio` and make it immediate instead of rate limited.
|
||||
* Fixed `MultiGet` with `ReadOptions::async_io` returning `NotSupported` when io_uring initialization or an asynchronous read submission fails. It now checks async I/O support on the reading thread and falls back to synchronous reads when needed.
|
||||
* Fixed subset checkpoints so their newest OPTIONS file lists only the column
|
||||
families retained by the checkpoint. This applies with both legacy
|
||||
highest-numbered OPTIONS selection and MANIFEST-tracked OPTIONS selection, and
|
||||
prevents `LoadLatestOptions` from recreating column families that the subset
|
||||
checkpoint intentionally dropped from its copied MANIFEST.
|
||||
|
||||
### Performance Improvements
|
||||
* Perf context counters and timers no longer call the thread-local init function of `perf_context` on every update. With `kEnableCount`, a counter update costs about 60% less.
|
||||
|
||||
## 11.12.0 (10/07/2026)
|
||||
### New Features
|
||||
* Added `FullExternalTableFactory` and related full-mode external table APIs that preserve RocksDB internal keys, enabling external tables to support nonzero sequence numbers, point and single deletions, merge operands, blob indexes, wide-column entities, snapshots, live DB writes, and multi-level compactions.
|
||||
|
||||
### Public API Changes
|
||||
* Added experimental coroutine capability interfaces for basic and full `ExternalTableReader` implementations. Coroutine-capable external readers can initiate file reads through `FSRandomAccessFile::SubmitReadAsync()`.
|
||||
* `ExternalTableFactory::NewTableReader()` now receives an already-opened `FSRandomAccessFile` and its file size instead of requiring implementations to reopen `file_path`. The supplied file preserves RocksDB's direct-I/O alignment handling, rate limiting, read statistics and listener notifications, prefetching, and synchronous, asynchronous, and batched reads.
|
||||
* Added `PutRangeDeletionBlock()`, `GetRangeDeletionBlock()`, and `IsDeleteRangeSupported()` to the external table interfaces so formats can opt in to range deletions using RocksDB's raw range-deletion metadata block.
|
||||
* Opaque `PreparedFileInfo` metadata can now be serialized to a checksummed representation with `SerializePreparedFileInfo()` and restored with `DeserializePreparedFileInfo()` for reuse across processes.
|
||||
* Removed the deprecated `IngestExternalFileOptions::write_global_seqno` option and its C, Java, and `ldb` APIs, `TableProperties::external_sst_file_global_seqno_offset`, and the `file_offset` output from `ExternalTableReader::GetPropertiesBlock()`. Existing external SST files whose global sequence number was rewritten without updating the properties-block checksum remain readable.
|
||||
* `TablePropertiesNames` has no apparent use in the public API so has been deprecated there.
|
||||
* `TableProperties::creation_time` is renamed to `oldest_ancestor_time`, which better describes its meaning, along with `TablePropertiesNames::kCreationTime` to `kOldestAncestorTime`. The misspelled `SstFileMetaData::oldest_ancester_time` is renamed to `oldest_ancestor_time`. The old names remain available as deprecated aliases and will be removed in a future release.
|
||||
* Added `WriteBufferFlushPolicy` and `WriteBufferManager` policy selection for flushing the oldest or largest column family, including configurable batched flushing of the largest column families across DBs sharing a manager, with C and Java bindings.
|
||||
|
||||
### Behavior Changes
|
||||
* When `ExternalTableBuilderBase::PutPropertiesBlock()` returns `NotSupported`, RocksDB now overrides `num_entries`, `raw_key_size`, and `raw_value_size` from `ExternalTableBuilderBase::GetTableProperties()` with the logical totals tracked by the external-table adapter. Format-specific properties such as `data_size` remain unchanged.
|
||||
* The `FSWritableFile` passed to `ExternalTableFactory::NewTableBuilder()` now reports logical file size including RocksDB-buffered data and exposes the underlying file's direct-I/O mode and required buffer alignment.
|
||||
* `Iterator::Refresh()` now returns `NotSupported` on iterators from a secondary DB's `DB::NewIterators()` only, because those iterators are pinned to one cross-column-family read view. Iterators from `DB::NewIterator()` are unaffected; to advance a batch, call `DB::NewIterators()` again after `TryCatchUpWithPrimary()`.
|
||||
* `SstFileReader` now reports standard SST read statistics and file I/O listener notifications, and honors the configured read rate limiter.
|
||||
|
||||
### Bug Fixes
|
||||
* Fixed full recovery from retryable background errors to atomically advance WAL replay boundaries for all column families, preventing partial recovery of multi-column-family write batches after a failed recovery flush.
|
||||
* Fixed RocksDB builds on big-endian architectures such as s390x by disabling the optional persistent fault-injection log, whose binary format is little-endian-only.
|
||||
* Fixed `DB::ClipColumnFamily()` deleting a non-L0 SST file whose largest key equals `begin_key`, which lost keys at the start of the kept range.
|
||||
* Fixed swapped `oldest_key_time` and `newest_key_time` values in the `table_file_creation` event log entry.
|
||||
* Fixed `ExternalTableReader::VerifyChecksum()` results not being honored by the external table adapter.
|
||||
* Fixed a bug in manifest handling that could lead to a hang or memory corruption in the case of LSM sanity checks being fatally violated (exceptional; see `force_consistency_checks`).
|
||||
* Fixed `MultiGet()` with `ReadOptions::read_tier == kBlockCacheTier` reading data blocks from disk when `allow_mmap_reads` is false. Keys whose data block is not in the block cache now return `Status::Incomplete()`, the same as `Get()`.
|
||||
* Fixed trace replay (`DB::NewDefaultReplayer()`, db_bench `replay`) failing every read record (Get, MultiGet, iterator seek) on the default column family with `Corruption: Invalid Column Family ID` when the default column family handle was not passed, which is always the case in db_bench with `--num_column_families=1`.
|
||||
* Fixed multi-threaded `Replayer::Replay()` ignoring errors from executing trace records and returning OK. It now stops and returns the error, including `Status::NotSupported()` from executing a record, as single-threaded replay does.
|
||||
* Fixed secondary DB `NewIterators()` returning iterators from different database states or blocking for `TryCatchUpWithPrimary()` by serving the last completed catch-up view.
|
||||
|
||||
### Performance Improvements
|
||||
* Single Get coroutine reads using non-mmap, non-direct I/O on a `kFSBuffer`-capable `FileSystem` now use `FSRandomAccessFile::SubmitReadAsync()` instead of falling back to synchronous `Read()`. When rate-limited, these reads acquire their full token budget before submission to preserve a single filesystem-owned buffer, which can make physical I/O burstier than reads using caller-provided buffers.
|
||||
|
||||
|
||||
## 11.11.0 (09/21/2026)
|
||||
### New Features
|
||||
* External tables now support DB-assigned global sequence numbers when `IngestExternalFileOptions::allow_global_seqno` is enabled, allowing overlapping external tables to be ingested across multiple levels when compactions are disabled. This includes the deprecated `write_global_seqno` path when the reader supplies the properties block's file offset.
|
||||
* Added checksum handoff support for SST file writes when `BlockBasedTableOptions::checksum` is `kCRC32c`, allowing block and trailer CRC32C checksums to be passed from the table builder to the file writer for buffered-data validation and filesystem in-flight write protection.
|
||||
* Enhanced `sst_dump --command=recompress` to be more powerful for benchmarking, including through automation with a new `--json` option for machine-readable output.
|
||||
|
||||
### Public API Changes
|
||||
* Changed the experimental async read statistics API by removing `DB::AsyncCallback::EnableStats()` and changing `OnComplete(const PerfContext*, const IOStatsContext*)` to `OnComplete()`. Callers must configure TLS statistics before each async or coroutine read. `DB::GetAsync()` and `DB::MultiGetAsync()` callbacks must copy per-operation statistics from TLS during `OnComplete()`; coroutine users must copy them from inside a `CoroStackableDB` hook before suspending again. Each operation leaves TLS statistics collection disabled.
|
||||
* Added `Compressor::MaybeOverrideCompressionOptions()` (EXPERIMENTAL), an omnibus hook letting a custom compressor adjust the subset of `CompressionOptions` that the SST builder applies rather than the compressor itself: `parallel_threads`, `max_compressed_bytes_per_kb`, `auto_skip`, and `auto_skip_min_sample_every` (and potentially more in the future). This deprecates `Compressor::GetRecommendedParallelThreads()`, which it subsumes; the default implementation still honors an overridden `GetRecommendedParallelThreads()` for compatibility.
|
||||
* Added experimental `CoroDB::CoGet()` and `CoroDB::CoMultiGet()` overloads for point reads from an `SstFileReader`.
|
||||
* Added experimental `DB::GetWithMetadata()` and `DB::MultiGetWithMetadata()` APIs with opt-in output metadata for key timestamps and explicit-snapshot newer-version detection.
|
||||
* Added a `DB::NewIterators()` overload that accepts parallel vectors of `ReadOptions` and column family handles, allowing each returned iterator to use independent read settings while sharing a consistent database state. The vectors must have the same size, all snapshots must be null or identical, and all `tailing` settings must match.
|
||||
* Added `ReadOptions::custom_context`, an opaque, non-owning pointer passed through table reads so external table implementations can consume application-defined read options.
|
||||
|
||||
### Behavior Changes
|
||||
* `ExternalTableReader::MultiGet()` is now used by RocksDB's `DB::MultiGet()` path. Previously, the external table adapter used `TableReader`'s fallback, which called `ExternalTableReader::Get()` separately for each key.
|
||||
* `DB::OpenAndCompact()` now requires `output_directory` to be a simple name for a new subdirectory when the new `use_session_tmp_dir_for_remote_compaction` DB option is enabled. The option places that subdirectory under the canonical `<dbname>/session_tmp/` root, and `DB::Open()` deletes stale contents under the root after recovery. The disabled default preserves the existing `<dbname>/<output_directory>` layout without automatic cleanup. Previously, remote-compaction output directories were created directly under `<dbname>`, which made it challenging to safely identify and delete obsolete files.
|
||||
* Several block-based SST table properties now better support compression benchmarking and optimization: `uncompressed_data_size` is now persisted in the file (previously it was computed only at write time and read back as 0); a new `lsm_info_at_creation` property records the LSM level and "bottommost" status at the file's creation, distinguishing "not applicable" (e.g. SstFileWriter files) from "unknown" (older files); and the `compression_options` property now records the full compression manager configuration instead of only its name.
|
||||
|
||||
### Bug Fixes
|
||||
* Fixed a usually-minor performance bug where `Cache::ApplyToAllEntries()` used single-entry batches instead of the intended 256-entry default for `ApplyToAllEntriesOptions::average_entries_per_lock`. All of the Cache implementations (LRU and HCC) benefit from the fix, but the disfavored LRU cache was especially impacted by repeated mutex acquisition and release.
|
||||
* Fixed stats-disabled coroutine and asynchronous reads inheriting enabled TLS statistics state when interleaved with stats-enabled reads on the same executor. Each read now uses its captured per-call configuration, while enabled request counters remain isolated across suspensions.
|
||||
* Fixed a bug where a remote compaction (`CompactionService` / `DB::OpenAndCompact`) requested with `OpenAndCompactOptions::allow_resumption=true` but for which resumption was internally disabled (incompatible with `paranoid_file_checks` or output-iteration verification) could fail a retried attempt, because the output directory was not cleaned before starting fresh and output files left by a previously interrupted attempt collided with the reused file numbers.
|
||||
* Fixed a bug where a remote compaction (`CompactionService` / `DB::OpenAndCompact`) could fail when the primary rotated its MANIFEST while the worker was opening the primary's directory read-only. The open reads `CURRENT`, and the primary publishes a new MANIFEST by renaming a freshly written `CURRENT` over the old one; on file systems without POSIX read-after-unlink semantics (remote/object stores) the replaced object is then reported as gone (`PathNotFound`/`NotFound`, or `TryAgain` for the MANIFEST equivalent) instead of remaining readable. `DB::OpenAndCompact()` now retries the secondary open a bounded, configurable number of times for those transient statuses, so a concurrent rotation no longer fails the compaction (which, for a `CompactionService` that does not fall back to a local compaction, became a background error on the primary).
|
||||
* Fixed secondary DB `NewIterators()` returning iterators from different database states when `TryCatchUpWithPrimary()` runs concurrently.
|
||||
* Fixed a bug where a `SeekToFirst()` on an iterator previously used for a `Seek()` could end the scan early and silently omit keys with `ReadOptions::async_io`, or otherwise run with readahead effectively disabled. Affects scans using `ReadOptions::prefix_same_as_start` together with `ReadOptions::auto_readahead_size` (enabled by default); memory-mapped reads are unaffected.
|
||||
|
||||
### Performance Improvements
|
||||
* `DB::MultiGetEntityLazy()` now reads all keys of a batch under a single shared per-column-family SuperVersion pin, and a single `LazyWideColumnsBatch::MultiResolve()` coalesces the resulting blob reads across keys: one read per blob file (and one per SST for embedded blobs), for whole-column and byte-range reads alike, instead of reading each key's blobs separately.
|
||||
|
||||
|
||||
## 11.10.0 (08/28/2026)
|
||||
### New Features
|
||||
* Added `BlockBasedTableOptions::optimize_key_common_prefix` as part of new `format_version=8`. When enabled, each data block and index block stores the common user-key prefix of its keys once instead of at every restart point, reducing block size and block-cache footprint for prefix-heavy data and speeding up Seek for bytewise-ordered keys.
|
||||
* A new experimental feature spreads out periodic (time-based) compaction trigger times across a fleet of similarly-configured DBs to avoid recurring "thundering herd" triggers, especially with remote compaction. See the new experimental DB option `periodic_compaction_phase_recovery_percent`.
|
||||
|
||||
### Public API Changes
|
||||
* `IngestExternalFileArg::atomic_replace_range` now supports a range that only partially overlaps existing files, instead of failing. Requires universal compaction and a table factory that supports range deletion; other configurations still fail on partial overlap. The range is also now specified as half-open (`start` inclusive, `limit` exclusive), resolving a previously documented ambiguity in the upper bound.
|
||||
* Added an immutable `external_table_config` option that is persisted in OPTIONS files and passed to external table factories during bootstrap.
|
||||
|
||||
### Bug Fixes
|
||||
* Fixed a liveness bug where RocksDB could remain indefinitely in recovery from a soft background error when concurrent writes continuously generated normal flush requests.
|
||||
* Fixed a race in online error recovery where a concurrent higher-severity I/O error could leave a stale retry-flush plan active, prevent the required WAL switch, and create duplicate internal WAL history detected on DB reopen.
|
||||
* Fix "bottommost files" infinite compaction loops (`kBottommostFiles`). This happens with the unsupported combination of leveled compaction and `preserve_internal_time_seconds` / `preclude_last_level_data_seconds`, and in an edge case with user-defined timestamps when `full_history_ts_low` is unset. The underlying logic is now more unified to reduce risk of a similar divergence in logic in the future.
|
||||
* Fixed `reuse_manifest_on_open` to create a fresh MANIFEST instead of appending after recovery sees an incomplete atomic group at the MANIFEST tail.
|
||||
* Starting remote compaction (`CompactionService` / `DB::OpenAndCompact`) is now more efficient and more reliable, by avoiding unnecessary file operations (latency) while using more accurate consistency / precondition checks, both avoiding some spurious failures and rejecting more legitimate inconsistencies. New DBOption `remote_compaction_manifest_floor` can be disabled in case of unexpected problems with the change.
|
||||
* Fixed unbounded retention of recovered transaction state by a long-lived secondary opened with `allow_2pc`.
|
||||
* Fixed stale reads on secondary instances, where entries replayed from a WAL that the primary had already flushed could shadow the newer values in the flushed files indefinitely. Also fixed a case where a secondary could stop returning such a key altogether while the primary's flushed file holding it was unreadable.
|
||||
* Fixed subset checkpoints to discard tolerated MANIFEST tail corruption before appending column-family drop records.
|
||||
|
||||
### Performance Improvements
|
||||
* Improved compaction CPU efficiency for column families using an `SstPartitioner` (such as the built-in `SstPartitionerFixedPrefix`): when the partitioner can express its next boundary as a key prefix via the new `SstPartitioner::ShouldPartitionByPrefix()` API, compaction now checks that cached prefix inline instead of copying every user key and making a virtual `ShouldPartition()` call for each key-value written to an SST.
|
||||
|
||||
## 11.9.0 (08/14/2026)
|
||||
### New Features
|
||||
* Added an integrated and improved compression auto-skip feature (new CompressionOptions, still experimental) that saves CPU by skipping compression when and where the overall ratio is inadequate. This improves on the previous auto-skip compression manager by (a) taking achieved compression ratios into account, (b) using a continuously evolving payoff estimate, and (c) combining natively with parallel compression.
|
||||
* Added the `blob_file_writable_file_max_buffer_size` mutable column family option. When set to a non-zero value, blob-file writers use it as their `WritableFileWriter` max buffer size instead of inheriting `DBOptions::writable_file_max_buffer_size`.
|
||||
* Added an experimental lazy wide-column read API, `DB::GetEntityLazy()` / `DB::MultiGetEntityLazy()` (see `include/rocksdb/lazy_wide_columns.h`). The returned `LazyWideColumns` leaves blob-backed columns as unresolved references and, like an iterator, stays valid after the call returns, so the caller resolves only the columns -- and only the byte ranges -- it needs; a byte-range read of an uncompressed blob (separate-file or embedded) reads just those bytes from storage. Adds `Env::IOActivity::kLazyResolve` and `rocksdb.blobdb.lazy.*` statistics. Requires `max_open_files == -1`.
|
||||
* Added DB property `rocksdb.num-running-remote-compactions`, reporting the number of compaction service jobs currently waiting in `CompactionService::Wait()`.
|
||||
* Added DB property `rocksdb.num-unscheduled-compactions`, reporting compactions waiting in the DB's internal compaction queue but not yet assigned to a background job.
|
||||
* Added `DB::AbortCompactions()` and `DB::ResumeCompactions()` for per-column-family compaction abort and resume, with the same active-abort semantics as the DB-wide `AbortAllCompactions()`/`ResumeAllCompactions()` but scoped to a single column family. Calls are reference-counted independently per column family and compose with the DB-wide abort.
|
||||
|
||||
### Public API Changes
|
||||
* Add IndexFactory compatibility names for the existing user-defined index API.
|
||||
* Added the experimental `CoroDB` capability, exposed by `DB::GetCoroDB()`, with lazy native coroutine variants of `Get` and `MultiGet`. The existing `DB::GetAsync()` and `DB::MultiGetAsync()` APIs dispatch through it.
|
||||
* Added `rocksdb_batched_multi_get_pinned_cf()` to return batch-owned pinned MultiGet values and per-key errors without allocating one wrapper per found value.
|
||||
* Add `DBOptions::read_io_executor_threads` to increase the maximum thread count of the shared filesystem read I/O executor used for asynchronous reads when opening a DB. Opening a DB never reduces the shared executor thread count. Change the experimental `FSRandomAccessFile::SubmitReadAsync()` API to return whether the asynchronous path was used, and add the `FILE_SUBMIT_ASYNC_READ_FALLBACK` ticker to count calls that fall back to synchronous reads.
|
||||
* `ReadOptions::table_filter` is now a *pointer* to a caller-owned `std::function` instead of a `std::function` value, to improve efficiency in copying and destroying `ReadOptions` objects (which RocksDB does internally). This is a breaking change for users of this relatively obscure option, who will now need to declare a `std::function` that outlives the read. As a side effect of `ReadOptions` being trivially copyable, new but legitimate compiler warnings may surface.
|
||||
* Removed the experimental `CreateAutoSkipCompressionManager()`. Auto-skip compression is now a first-class feature of the block-based table builder, controlled by the new experimental `CompressionOptions::auto_skip`.
|
||||
* Added `CoroStackableDB` for implementing wrappers over native coroutine reads and used it to add coroutine `Get` and `MultiGet` support to `DBWithTTL` and WritePrepared `TransactionDB`. A stackable wrapper advertises coroutine support only when its wrapped DB supports it; TTL value decoding and transaction visibility semantics are preserved.
|
||||
* Added a `kSstFileWriter` value to `TableFileCreationReason`, which `SstFileWriter` now reports instead of the catch-all `kMisc`. This, along with new `db_name` and `is_remote_compaction` fields in `FilterBuildingContext`, improves the contextual information available to custom filter and compression strategies.
|
||||
* Renamed `TransactionLogIterator` to `WalIterator` (and `include/rocksdb/transaction_log.h` to `include/rocksdb/wal_iterator.h`), because it iterates the write-ahead log and is unrelated to `TransactionDB`. The old names still work. `DB::GetUpdatesSince()` is unchanged. In Java, `WalIterator` is the new name and `TransactionLogIterator` remains as a subclass. Also documented existing behavior that was previously unstated: only writes that reached the WAL are visible (so `WriteOptions::disableWAL` and `IngestExternalFile()` leave holes that end iteration), what each iterator state means, and that `GetUpdatesSince()` silently starts later when the requested sequence number is unavailable.
|
||||
|
||||
### Bug Fixes
|
||||
* Fixed a bug where `BlobFileBuilder` passed an SST path to `OnBlobFileCompleted()` instead of the completed blob path during BlobDB garbage-collection compactions, causing incorrect `SstFileManager` accounting and compaction failures on filesystems where the SST was not yet pathname-visible.
|
||||
|
||||
### Performance Improvements
|
||||
* When `DBOptions::avoid_unnecessary_blocking_io` is true, obsolete `OPTIONS-*` files found during DB open are deleted by background purge instead of synchronously on the opening thread.
|
||||
|
||||
## 11.8.0 (07/28/2026)
|
||||
### Public API Changes
|
||||
* Add callback-based asynchronous read APIs, `DB::GetAsync()` and `DB::MultiGetAsync()`. The idea is that when IO is required, RocksDB can suspend its internal coroutine read path, allowing the read executor thread to do other work. When the IO is complete, RocksDB invokes the user callback. This requires filesystem support for full performance benefits. A new filesystem API `FSRandomAccessFile::SubmitReadAsync()` is introduced for this. Unlike `ReadAsync`, the filesystem is responsible for eventually calling the callback. `FileSystem::GetReadExecutor()` returns the IO executor whose EventBases run coroutine read processing. In the Posix filesystem, an IO-uring based event loop is used to complete the IO.
|
||||
|
||||
@@ -591,6 +591,7 @@ ifneq ($(filter check-headers, $(MAKECMDGOALS)),)
|
||||
DEV_HEADER_DIRS := $(sort include/ $(dir $(ALL_SOURCES)))
|
||||
FOLLY_DEPENDENT_HEADERS := \
|
||||
include/rocksdb/coro_db.h \
|
||||
include/rocksdb/coro_external_table.h \
|
||||
include/rocksdb/utilities/coro_stackable_db.h
|
||||
# Some headers like in port/ are platform-specific
|
||||
DEV_HEADERS_TO_CHECK := $(filter-out $(FOLLY_DEPENDENT_HEADERS), $(shell $(FIND) $(DEV_HEADER_DIRS) -type f -name '*.h' | grep -E -v 'port/|plugin/|range_tree/|secondary_index/'))
|
||||
@@ -2018,6 +2019,9 @@ block_fetcher_test: table/block_fetcher_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
block_test: $(OBJ_DIR)/table/block_based/block_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
builtin_index_factory_test: $(OBJ_DIR)/table/block_based/builtin_index_factory_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
data_block_hash_index_test: $(OBJ_DIR)/table/block_based/data_block_hash_index_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
|
||||
@@ -35,6 +35,49 @@ from util import ColorString
|
||||
# tests to export as libraries for inclusion in other projects
|
||||
_EXPORTED_TEST_LIBS = ["env_basic_test"]
|
||||
|
||||
_TEST_LIB_HEADERS = [
|
||||
"db/db_test_util.h",
|
||||
"db/db_with_timestamp_test_util.h",
|
||||
"table/mock_table.h",
|
||||
"test_util/mock_time_env.h",
|
||||
"test_util/secondary_cache_test_util.h",
|
||||
"test_util/simple_external_table_factory.h",
|
||||
"test_util/testharness.h",
|
||||
"test_util/testutil.h",
|
||||
"tools/block_cache_analyzer/block_cache_trace_analyzer.h",
|
||||
"tools/trace_analyzer_tool.h",
|
||||
"utilities/agg_merge/test_agg_merge.h",
|
||||
"utilities/cassandra/test_utils.h",
|
||||
]
|
||||
|
||||
_TEST_HEADERS = {
|
||||
"configurable_test": ["options/configurable_test.h"],
|
||||
"persistent_cache_test": ["utilities/persistent_cache/persistent_cache_test.h"],
|
||||
"point_lock_manager_test": [
|
||||
"utilities/transactions/lock/point/point_lock_manager_test.h",
|
||||
],
|
||||
"transaction_test": ["utilities/transactions/transaction_test.h"],
|
||||
}
|
||||
|
||||
_STRESS_LIB_HEADERS = [
|
||||
"db_stress_tool/db_stress_common.h",
|
||||
"db_stress_tool/db_stress_compaction_service.h",
|
||||
"db_stress_tool/db_stress_compression_manager.h",
|
||||
"db_stress_tool/db_stress_driver.h",
|
||||
"db_stress_tool/db_stress_filters.h",
|
||||
"db_stress_tool/db_stress_listener.h",
|
||||
"db_stress_tool/db_stress_shared_state.h",
|
||||
"db_stress_tool/db_stress_test_base.h",
|
||||
"db_stress_tool/db_stress_wide_merge_operator.h",
|
||||
"db_stress_tool/expected_state.h",
|
||||
"db_stress_tool/expected_value.h",
|
||||
"db_stress_tool/multi_ops_txns_stress.h",
|
||||
"test_util/simple_external_table_factory.h",
|
||||
"test_util/testutil.h",
|
||||
"tools/block_cache_analyzer/block_cache_trace_analyzer.h",
|
||||
"tools/trace_analyzer_tool.h"
|
||||
]
|
||||
|
||||
# Parse src.mk files as a Dictionary of
|
||||
# VAR_NAME => list of files
|
||||
def parse_src_mk(repo_path):
|
||||
@@ -183,27 +226,34 @@ def generate_buck(repo_path, deps_map):
|
||||
":rocksdb_lib",
|
||||
],
|
||||
)
|
||||
# rocksdb_test_lib
|
||||
BUCK.add_library(
|
||||
"rocksdb_test_lib",
|
||||
test_lib_sources = (
|
||||
src_mk.get("MOCK_LIB_SOURCES", [])
|
||||
+ src_mk.get("TEST_LIB_SOURCES", [])
|
||||
+ src_mk.get("EXP_LIB_SOURCES", [])
|
||||
+ src_mk.get("ANALYZER_LIB_SOURCES", []),
|
||||
+ src_mk.get("ANALYZER_LIB_SOURCES", [])
|
||||
)
|
||||
# Older release source lists may not compile every test helper yet.
|
||||
test_lib_headers = [
|
||||
header for header in _TEST_LIB_HEADERS
|
||||
if header[:-2] + ".cc" in test_lib_sources
|
||||
]
|
||||
# rocksdb_test_lib
|
||||
BUCK.add_library(
|
||||
"rocksdb_test_lib",
|
||||
test_lib_sources,
|
||||
[":rocksdb_lib"],
|
||||
extra_test_libs=True,
|
||||
headers=test_lib_headers,
|
||||
)
|
||||
# rocksdb_with_faiss_test_lib
|
||||
BUCK.add_library(
|
||||
"rocksdb_with_faiss_test_lib",
|
||||
src_mk.get("MOCK_LIB_SOURCES", [])
|
||||
+ src_mk.get("TEST_LIB_SOURCES", [])
|
||||
+ src_mk.get("EXP_LIB_SOURCES", [])
|
||||
+ src_mk.get("ANALYZER_LIB_SOURCES", []),
|
||||
test_lib_sources,
|
||||
deps=[
|
||||
":rocksdb_with_faiss_lib",
|
||||
],
|
||||
extra_test_libs=True,
|
||||
headers=test_lib_headers,
|
||||
)
|
||||
# rocksdb_tools_lib
|
||||
BUCK.add_library(
|
||||
@@ -212,6 +262,12 @@ def generate_buck(repo_path, deps_map):
|
||||
+ src_mk.get("ANALYZER_LIB_SOURCES", [])
|
||||
+ ["test_util/testutil.cc"],
|
||||
[":rocksdb_lib"],
|
||||
headers=[
|
||||
"test_util/testutil.h",
|
||||
"tools/block_cache_analyzer/block_cache_trace_analyzer.h",
|
||||
"tools/simulated_hybrid_file_system.h",
|
||||
"tools/trace_analyzer_tool.h",
|
||||
],
|
||||
)
|
||||
# rocksdb_cache_bench_tools_lib
|
||||
BUCK.add_library(
|
||||
@@ -225,12 +281,19 @@ def generate_buck(repo_path, deps_map):
|
||||
src_mk.get("POINT_LOCK_BENCH_LIB_SOURCES", []),
|
||||
[":rocksdb_lib"],
|
||||
)
|
||||
stress_sources = (
|
||||
src_mk.get("ANALYZER_LIB_SOURCES", [])
|
||||
+ src_mk.get("STRESS_LIB_SOURCES", [])
|
||||
+ ["test_util/testutil.cc"]
|
||||
)
|
||||
# rocksdb_stress_lib
|
||||
BUCK.add_rocksdb_library(
|
||||
"rocksdb_stress_lib",
|
||||
src_mk.get("ANALYZER_LIB_SOURCES", [])
|
||||
+ src_mk.get("STRESS_LIB_SOURCES", [])
|
||||
+ ["test_util/testutil.cc"],
|
||||
stress_sources,
|
||||
headers=[
|
||||
header for header in _STRESS_LIB_HEADERS
|
||||
if header[:-2] + ".cc" in stress_sources
|
||||
],
|
||||
)
|
||||
# ldb binary
|
||||
BUCK.add_binary(
|
||||
@@ -353,6 +416,7 @@ def generate_buck(repo_path, deps_map):
|
||||
test_src,
|
||||
deps=json.dumps(deps["extra_deps"] + [":" + test_library]),
|
||||
extra_compiler_flags=json.dumps(deps["extra_compiler_flags"]),
|
||||
source_headers=_TEST_HEADERS.get(test, []),
|
||||
)
|
||||
else:
|
||||
if with_faiss:
|
||||
@@ -361,6 +425,7 @@ def generate_buck(repo_path, deps_map):
|
||||
test_src,
|
||||
deps=json.dumps(deps["extra_deps"] + [":rocksdb_with_faiss_test_lib"]),
|
||||
extra_compiler_flags=json.dumps(deps["extra_compiler_flags"]),
|
||||
source_headers=_TEST_HEADERS.get(test, []),
|
||||
)
|
||||
else:
|
||||
BUCK.register_test(
|
||||
@@ -368,6 +433,7 @@ def generate_buck(repo_path, deps_map):
|
||||
test_src,
|
||||
deps=json.dumps(deps["extra_deps"] + [":rocksdb_test_lib"]),
|
||||
extra_compiler_flags=json.dumps(deps["extra_compiler_flags"]),
|
||||
source_headers=_TEST_HEADERS.get(test, []),
|
||||
)
|
||||
BUCK.export_file("tools/db_crashtest.py")
|
||||
BUCK.export_file("tools/fault_injection_log_parser.py")
|
||||
|
||||
@@ -162,12 +162,17 @@ add_c_test_wrapper()
|
||||
).encode("utf-8")
|
||||
)
|
||||
|
||||
def register_test(self, test_name, src, deps, extra_compiler_flags):
|
||||
def register_test(self, test_name, src, deps, extra_compiler_flags, source_headers):
|
||||
with open(self.path, "ab") as targets_file:
|
||||
targets_file.write(
|
||||
targets_cfg.unittests_template.format(
|
||||
test_name=test_name,
|
||||
test_cc=str(src),
|
||||
source_headers=(
|
||||
f" source_headers=[{pretty_list(source_headers, 16)}],\n"
|
||||
if source_headers
|
||||
else ""
|
||||
),
|
||||
deps=deps,
|
||||
extra_compiler_flags=extra_compiler_flags,
|
||||
).encode("utf-8")
|
||||
|
||||
@@ -30,7 +30,7 @@ cpp_binary_wrapper(name="{name}", srcs=[{srcs}], deps=[{deps}], extra_preprocess
|
||||
unittests_template = """
|
||||
cpp_unittest_wrapper(name="{test_name}",
|
||||
srcs=["{test_cc}"],
|
||||
deps={deps},
|
||||
{source_headers} deps={deps},
|
||||
extra_compiler_flags={extra_compiler_flags})
|
||||
|
||||
"""
|
||||
|
||||
@@ -142,6 +142,11 @@ uint64_t rocksdb_table_properties_column_family_id(
|
||||
return props->rep.column_family_id;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_table_properties_oldest_ancestor_time(
|
||||
const rocksdb_table_properties_t* props) {
|
||||
return props->rep.oldest_ancestor_time;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_table_properties_creation_time(
|
||||
const rocksdb_table_properties_t* props) {
|
||||
return props->rep.creation_time;
|
||||
@@ -172,11 +177,6 @@ uint64_t rocksdb_table_properties_fast_compression_estimated_data_size(
|
||||
return props->rep.fast_compression_estimated_data_size;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_table_properties_external_sst_file_global_seqno_offset(
|
||||
const rocksdb_table_properties_t* props) {
|
||||
return props->rep.external_sst_file_global_seqno_offset;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_table_properties_tail_start_offset(
|
||||
const rocksdb_table_properties_t* props) {
|
||||
return props->rep.tail_start_offset;
|
||||
@@ -212,6 +212,11 @@ uint64_t rocksdb_table_properties_separate_key_value_in_data_block(
|
||||
return props->rep.separate_key_value_in_data_block;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_table_properties_lsm_info_at_creation(
|
||||
const rocksdb_table_properties_t* props) {
|
||||
return props->rep.lsm_info_at_creation;
|
||||
}
|
||||
|
||||
const char* rocksdb_table_properties_db_id(
|
||||
const rocksdb_table_properties_t* props, size_t* size) {
|
||||
*size = props->rep.db_id.size();
|
||||
|
||||
@@ -102,6 +102,10 @@ rocksdb_table_properties_fixed_key_len(const rocksdb_table_properties_t* props);
|
||||
extern ROCKSDB_LIBRARY_API uint64_t rocksdb_table_properties_column_family_id(
|
||||
const rocksdb_table_properties_t* props);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_table_properties_oldest_ancestor_time(
|
||||
const rocksdb_table_properties_t* props);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_table_properties_creation_time(const rocksdb_table_properties_t* props);
|
||||
|
||||
@@ -122,10 +126,6 @@ extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_table_properties_fast_compression_estimated_data_size(
|
||||
const rocksdb_table_properties_t* props);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_table_properties_external_sst_file_global_seqno_offset(
|
||||
const rocksdb_table_properties_t* props);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint64_t rocksdb_table_properties_tail_start_offset(
|
||||
const rocksdb_table_properties_t* props);
|
||||
|
||||
@@ -151,6 +151,10 @@ extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_table_properties_separate_key_value_in_data_block(
|
||||
const rocksdb_table_properties_t* props);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_table_properties_lsm_info_at_creation(
|
||||
const rocksdb_table_properties_t* props);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API const char* rocksdb_table_properties_db_id(
|
||||
const rocksdb_table_properties_t* props, size_t* size);
|
||||
|
||||
|
||||
@@ -741,6 +741,16 @@ unsigned char rocksdb_options_get_write_dbid_to_manifest(
|
||||
return opt->rep.write_dbid_to_manifest;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_track_options_file_number_in_manifest(
|
||||
rocksdb_options_t* opt, unsigned char v) {
|
||||
opt->rep.track_options_file_number_in_manifest = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_options_get_track_options_file_number_in_manifest(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.track_options_file_number_in_manifest;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_write_identity_file(rocksdb_options_t* opt,
|
||||
unsigned char v) {
|
||||
opt->rep.write_identity_file = v;
|
||||
@@ -816,6 +826,26 @@ const char* rocksdb_options_get_db_host_id(rocksdb_options_t* opt,
|
||||
return opt->rep.db_host_id.data();
|
||||
}
|
||||
|
||||
void rocksdb_options_set_remote_compaction_manifest_floor(
|
||||
rocksdb_options_t* opt, unsigned char v) {
|
||||
opt->rep.remote_compaction_manifest_floor = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_options_get_remote_compaction_manifest_floor(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.remote_compaction_manifest_floor;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_use_session_tmp_dir_for_remote_compaction(
|
||||
rocksdb_options_t* opt, unsigned char v) {
|
||||
opt->rep.use_session_tmp_dir_for_remote_compaction = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_options_get_use_session_tmp_dir_for_remote_compaction(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.use_session_tmp_dir_for_remote_compaction;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_lowest_used_cache_tier(rocksdb_options_t* opt, int v) {
|
||||
opt->rep.lowest_used_cache_tier =
|
||||
static_cast<decltype(opt->rep.lowest_used_cache_tier)>(v);
|
||||
@@ -856,6 +886,16 @@ uint64_t rocksdb_options_get_max_compaction_trigger_wakeup_seconds(
|
||||
return opt->rep.max_compaction_trigger_wakeup_seconds;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_periodic_compaction_phase_recovery_percent(
|
||||
rocksdb_options_t* opt, int v) {
|
||||
opt->rep.periodic_compaction_phase_recovery_percent = v;
|
||||
}
|
||||
|
||||
int rocksdb_options_get_periodic_compaction_phase_recovery_percent(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.periodic_compaction_phase_recovery_percent;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_follower_refresh_catchup_period_ms(
|
||||
rocksdb_options_t* opt, uint64_t v) {
|
||||
opt->rep.follower_refresh_catchup_period_ms = v;
|
||||
@@ -2027,16 +2067,6 @@ unsigned char rocksdb_ingestexternalfileoptions_get_ingest_behind(
|
||||
return opt->rep.ingest_behind;
|
||||
}
|
||||
|
||||
void rocksdb_ingestexternalfileoptions_set_write_global_seqno(
|
||||
rocksdb_ingestexternalfileoptions_t* opt, unsigned char v) {
|
||||
opt->rep.write_global_seqno = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_ingestexternalfileoptions_get_write_global_seqno(
|
||||
rocksdb_ingestexternalfileoptions_t* opt) {
|
||||
return opt->rep.write_global_seqno;
|
||||
}
|
||||
|
||||
void rocksdb_ingestexternalfileoptions_set_verify_checksums_before_ingest(
|
||||
rocksdb_ingestexternalfileoptions_t* opt, unsigned char v) {
|
||||
opt->rep.verify_checksums_before_ingest = v;
|
||||
@@ -2117,6 +2147,16 @@ int rocksdb_ingestexternalfileoptions_get_file_opening_threads(
|
||||
|
||||
/* OpenAndCompactOptions */
|
||||
|
||||
void rocksdb_open_and_compact_options_set_max_secondary_open_retries(
|
||||
rocksdb_open_and_compact_options_t* opt, uint32_t v) {
|
||||
opt->rep.max_secondary_open_retries = v;
|
||||
}
|
||||
|
||||
uint32_t rocksdb_open_and_compact_options_get_max_secondary_open_retries(
|
||||
rocksdb_open_and_compact_options_t* opt) {
|
||||
return opt->rep.max_secondary_open_retries;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_open_and_compact_options_get_allow_resumption(
|
||||
rocksdb_open_and_compact_options_t* opt) {
|
||||
return opt->rep.allow_resumption;
|
||||
@@ -2514,6 +2554,17 @@ double rocksdb_block_based_options_get_uniform_cv_threshold(
|
||||
return opt->rep.uniform_cv_threshold;
|
||||
}
|
||||
|
||||
void rocksdb_block_based_options_set_optimize_key_common_prefix(
|
||||
rocksdb_block_based_table_options_t* opt, int v) {
|
||||
opt->rep.optimize_key_common_prefix =
|
||||
static_cast<decltype(opt->rep.optimize_key_common_prefix)>(v);
|
||||
}
|
||||
|
||||
int rocksdb_block_based_options_get_optimize_key_common_prefix(
|
||||
rocksdb_block_based_table_options_t* opt) {
|
||||
return static_cast<int>(opt->rep.optimize_key_common_prefix);
|
||||
}
|
||||
|
||||
void rocksdb_block_based_options_set_enable_index_compression(
|
||||
rocksdb_block_based_table_options_t* opt, unsigned char v) {
|
||||
opt->rep.enable_index_compression = v;
|
||||
|
||||
@@ -204,6 +204,14 @@ rocksdb_options_set_background_close_inactive_wals(rocksdb_options_t* opt,
|
||||
extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_options_get_background_close_inactive_wals(rocksdb_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_options_set_track_options_file_number_in_manifest(
|
||||
rocksdb_options_t* opt, unsigned char v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_options_get_track_options_file_number_in_manifest(
|
||||
rocksdb_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_prefix_seek_opt_in_only(
|
||||
rocksdb_options_t* opt, unsigned char v);
|
||||
|
||||
@@ -247,6 +255,21 @@ extern ROCKSDB_LIBRARY_API void rocksdb_options_set_db_host_id(
|
||||
extern ROCKSDB_LIBRARY_API const char* rocksdb_options_get_db_host_id(
|
||||
rocksdb_options_t* opt, size_t* size);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_options_set_remote_compaction_manifest_floor(rocksdb_options_t* opt,
|
||||
unsigned char v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_options_get_remote_compaction_manifest_floor(rocksdb_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_options_set_use_session_tmp_dir_for_remote_compaction(
|
||||
rocksdb_options_t* opt, unsigned char v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_options_get_use_session_tmp_dir_for_remote_compaction(
|
||||
rocksdb_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_lowest_used_cache_tier(
|
||||
rocksdb_options_t* opt, int v);
|
||||
|
||||
@@ -267,6 +290,14 @@ extern ROCKSDB_LIBRARY_API const char*
|
||||
rocksdb_options_get_daily_offpeak_time_utc(rocksdb_options_t* opt,
|
||||
size_t* size);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_options_set_periodic_compaction_phase_recovery_percent(
|
||||
rocksdb_options_t* opt, int v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API int
|
||||
rocksdb_options_get_periodic_compaction_phase_recovery_percent(
|
||||
rocksdb_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_options_set_follower_refresh_catchup_period_ms(rocksdb_options_t* opt,
|
||||
uint64_t v);
|
||||
@@ -793,14 +824,6 @@ extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_ingestexternalfileoptions_get_ingest_behind(
|
||||
rocksdb_ingestexternalfileoptions_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_ingestexternalfileoptions_set_write_global_seqno(
|
||||
rocksdb_ingestexternalfileoptions_t* opt, unsigned char v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_ingestexternalfileoptions_get_write_global_seqno(
|
||||
rocksdb_ingestexternalfileoptions_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_ingestexternalfileoptions_set_verify_checksums_before_ingest(
|
||||
rocksdb_ingestexternalfileoptions_t* opt, unsigned char v);
|
||||
@@ -863,6 +886,14 @@ rocksdb_ingestexternalfileoptions_get_file_opening_threads(
|
||||
|
||||
/* OpenAndCompactOptions */
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_open_and_compact_options_set_max_secondary_open_retries(
|
||||
rocksdb_open_and_compact_options_t* opt, uint32_t v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint32_t
|
||||
rocksdb_open_and_compact_options_get_max_secondary_open_retries(
|
||||
rocksdb_open_and_compact_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_open_and_compact_options_get_allow_resumption(
|
||||
rocksdb_open_and_compact_options_t* opt);
|
||||
@@ -1170,6 +1201,14 @@ extern ROCKSDB_LIBRARY_API double
|
||||
rocksdb_block_based_options_get_uniform_cv_threshold(
|
||||
rocksdb_block_based_table_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_block_based_options_set_optimize_key_common_prefix(
|
||||
rocksdb_block_based_table_options_t* opt, int v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API int
|
||||
rocksdb_block_based_options_get_optimize_key_common_prefix(
|
||||
rocksdb_block_based_table_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_block_based_options_set_enable_index_compression(
|
||||
rocksdb_block_based_table_options_t* opt, unsigned char v);
|
||||
|
||||
@@ -151,6 +151,12 @@ static void rocksdb_roundtrip_test_rocksdb_block_based_options(void) {
|
||||
rocksdb_block_based_options_set_optimize_filters_for_memory(obj, 0);
|
||||
CheckCondition(
|
||||
rocksdb_block_based_options_get_optimize_filters_for_memory(obj) == 0);
|
||||
rocksdb_block_based_options_set_optimize_key_common_prefix(obj, 1);
|
||||
CheckCondition(
|
||||
rocksdb_block_based_options_get_optimize_key_common_prefix(obj) == 1);
|
||||
rocksdb_block_based_options_set_optimize_key_common_prefix(obj, 0);
|
||||
CheckCondition(
|
||||
rocksdb_block_based_options_get_optimize_key_common_prefix(obj) == 0);
|
||||
rocksdb_block_based_options_set_partition_filters(obj, 1);
|
||||
CheckCondition(rocksdb_block_based_options_get_partition_filters(obj) == 1);
|
||||
rocksdb_block_based_options_set_partition_filters(obj, 0);
|
||||
@@ -550,12 +556,6 @@ static void rocksdb_roundtrip_test_rocksdb_ingestexternalfileoptions(void) {
|
||||
rocksdb_ingestexternalfileoptions_set_verify_file_checksum(obj, 0);
|
||||
CheckCondition(
|
||||
rocksdb_ingestexternalfileoptions_get_verify_file_checksum(obj) == 0);
|
||||
rocksdb_ingestexternalfileoptions_set_write_global_seqno(obj, 1);
|
||||
CheckCondition(
|
||||
rocksdb_ingestexternalfileoptions_get_write_global_seqno(obj) == 1);
|
||||
rocksdb_ingestexternalfileoptions_set_write_global_seqno(obj, 0);
|
||||
CheckCondition(
|
||||
rocksdb_ingestexternalfileoptions_get_write_global_seqno(obj) == 0);
|
||||
rocksdb_ingestexternalfileoptions_destroy(obj);
|
||||
}
|
||||
|
||||
@@ -586,6 +586,20 @@ static void rocksdb_roundtrip_test_rocksdb_livefiles_storage_info_options(
|
||||
rocksdb_livefiles_storage_info_options_destroy(obj);
|
||||
}
|
||||
|
||||
static void rocksdb_roundtrip_test_rocksdb_open_and_compact_options(void) {
|
||||
rocksdb_open_and_compact_options_t* obj =
|
||||
rocksdb_open_and_compact_options_create();
|
||||
rocksdb_open_and_compact_options_set_max_secondary_open_retries(obj, 1);
|
||||
CheckCondition(
|
||||
rocksdb_open_and_compact_options_get_max_secondary_open_retries(obj) ==
|
||||
1);
|
||||
rocksdb_open_and_compact_options_set_max_secondary_open_retries(obj, 0);
|
||||
CheckCondition(
|
||||
rocksdb_open_and_compact_options_get_max_secondary_open_retries(obj) ==
|
||||
0);
|
||||
rocksdb_open_and_compact_options_destroy(obj);
|
||||
}
|
||||
|
||||
static void rocksdb_roundtrip_test_rocksdb_optimistictransactiondb_options(
|
||||
void) {
|
||||
rocksdb_optimistictransactiondb_options_t* obj =
|
||||
@@ -833,6 +847,12 @@ static void rocksdb_roundtrip_test_rocksdb_options(void) {
|
||||
CheckCondition(rocksdb_options_get_paranoid_memory_checks(obj) == 1);
|
||||
rocksdb_options_set_paranoid_memory_checks(obj, 0);
|
||||
CheckCondition(rocksdb_options_get_paranoid_memory_checks(obj) == 0);
|
||||
rocksdb_options_set_periodic_compaction_phase_recovery_percent(obj, 1);
|
||||
CheckCondition(
|
||||
rocksdb_options_get_periodic_compaction_phase_recovery_percent(obj) == 1);
|
||||
rocksdb_options_set_periodic_compaction_phase_recovery_percent(obj, 0);
|
||||
CheckCondition(
|
||||
rocksdb_options_get_periodic_compaction_phase_recovery_percent(obj) == 0);
|
||||
rocksdb_options_set_persist_stats_to_disk(obj, 1);
|
||||
CheckCondition(rocksdb_options_get_persist_stats_to_disk(obj) == 1);
|
||||
rocksdb_options_set_persist_stats_to_disk(obj, 0);
|
||||
@@ -859,6 +879,12 @@ static void rocksdb_roundtrip_test_rocksdb_options(void) {
|
||||
CheckCondition(rocksdb_options_get_read_io_executor_threads(obj) == 1);
|
||||
rocksdb_options_set_read_io_executor_threads(obj, 0);
|
||||
CheckCondition(rocksdb_options_get_read_io_executor_threads(obj) == 0);
|
||||
rocksdb_options_set_remote_compaction_manifest_floor(obj, 1);
|
||||
CheckCondition(rocksdb_options_get_remote_compaction_manifest_floor(obj) ==
|
||||
1);
|
||||
rocksdb_options_set_remote_compaction_manifest_floor(obj, 0);
|
||||
CheckCondition(rocksdb_options_get_remote_compaction_manifest_floor(obj) ==
|
||||
0);
|
||||
rocksdb_options_set_reuse_manifest_on_open(obj, 1);
|
||||
CheckCondition(rocksdb_options_get_reuse_manifest_on_open(obj) == 1);
|
||||
rocksdb_options_set_reuse_manifest_on_open(obj, 0);
|
||||
@@ -887,6 +913,12 @@ static void rocksdb_roundtrip_test_rocksdb_options(void) {
|
||||
CheckCondition(rocksdb_options_get_track_and_verify_wals(obj) == 1);
|
||||
rocksdb_options_set_track_and_verify_wals(obj, 0);
|
||||
CheckCondition(rocksdb_options_get_track_and_verify_wals(obj) == 0);
|
||||
rocksdb_options_set_track_options_file_number_in_manifest(obj, 1);
|
||||
CheckCondition(
|
||||
rocksdb_options_get_track_options_file_number_in_manifest(obj) == 1);
|
||||
rocksdb_options_set_track_options_file_number_in_manifest(obj, 0);
|
||||
CheckCondition(
|
||||
rocksdb_options_get_track_options_file_number_in_manifest(obj) == 0);
|
||||
rocksdb_options_set_two_write_queues(obj, 1);
|
||||
CheckCondition(rocksdb_options_get_two_write_queues(obj) == 1);
|
||||
rocksdb_options_set_two_write_queues(obj, 0);
|
||||
@@ -905,6 +937,12 @@ static void rocksdb_roundtrip_test_rocksdb_options(void) {
|
||||
CheckCondition(rocksdb_options_get_use_fsync(obj) == 1);
|
||||
rocksdb_options_set_use_fsync(obj, 0);
|
||||
CheckCondition(rocksdb_options_get_use_fsync(obj) == 0);
|
||||
rocksdb_options_set_use_session_tmp_dir_for_remote_compaction(obj, 1);
|
||||
CheckCondition(
|
||||
rocksdb_options_get_use_session_tmp_dir_for_remote_compaction(obj) == 1);
|
||||
rocksdb_options_set_use_session_tmp_dir_for_remote_compaction(obj, 0);
|
||||
CheckCondition(
|
||||
rocksdb_options_get_use_session_tmp_dir_for_remote_compaction(obj) == 0);
|
||||
rocksdb_options_set_verify_manifest_content_on_close(obj, 1);
|
||||
CheckCondition(rocksdb_options_get_verify_manifest_content_on_close(obj) ==
|
||||
1);
|
||||
@@ -1363,6 +1401,7 @@ static void rocksdb_run_generated_roundtrip_tests(void) {
|
||||
rocksdb_roundtrip_test_rocksdb_flushwaloptions();
|
||||
rocksdb_roundtrip_test_rocksdb_ingestexternalfileoptions();
|
||||
rocksdb_roundtrip_test_rocksdb_livefiles_storage_info_options();
|
||||
rocksdb_roundtrip_test_rocksdb_open_and_compact_options();
|
||||
rocksdb_roundtrip_test_rocksdb_optimistictransactiondb_options();
|
||||
rocksdb_roundtrip_test_rocksdb_options();
|
||||
rocksdb_roundtrip_test_rocksdb_readoptions();
|
||||
|
||||
Vendored
+2
-1
@@ -276,7 +276,8 @@ struct KeyGen {
|
||||
// location, so we fix hash bits starting at the bottom of that word.
|
||||
auto hi_hash = uint64_t{0x9e3779b97f4a7c13U} ^
|
||||
(key_hash << 1 << (FLAGS_degenerate_hash_bits - 1));
|
||||
uint64_t un_hi, un_lo;
|
||||
uint64_t un_hi;
|
||||
uint64_t un_lo;
|
||||
BijectiveUnhash2x64(hi_hash, key_hash, &un_hi, &un_lo);
|
||||
un_lo ^= BitwiseAnd(FLAGS_seed, INT32_MAX);
|
||||
EncodeFixed64(key_data, un_lo);
|
||||
|
||||
Vendored
+7
-2
@@ -32,9 +32,9 @@ class CacheReservationManager {
|
||||
// for an incremental amount of memory used (i.e, incremental_memory_used)
|
||||
class CacheReservationHandle {
|
||||
public:
|
||||
virtual ~CacheReservationHandle() {}
|
||||
virtual ~CacheReservationHandle() = default;
|
||||
};
|
||||
virtual ~CacheReservationManager() {}
|
||||
virtual ~CacheReservationManager() = default;
|
||||
virtual Status UpdateCacheReservation(std::size_t new_memory_used) = 0;
|
||||
// TODO(hx235): replace the usage of
|
||||
// `UpdateCacheReservation(memory_used_delta, increase)` with
|
||||
@@ -235,6 +235,11 @@ class ConcurrentCacheReservationManager
|
||||
cache_res_mgr_ = cache_res_mgr;
|
||||
cache_res_handle_ = std::move(cache_res_handle);
|
||||
}
|
||||
// No copying allowed
|
||||
CacheReservationHandle(const CacheReservationHandle&) = delete;
|
||||
CacheReservationHandle& operator=(const CacheReservationHandle&) = delete;
|
||||
CacheReservationHandle(CacheReservationHandle&&) = delete;
|
||||
CacheReservationHandle& operator=(CacheReservationHandle&&) = delete;
|
||||
|
||||
~CacheReservationHandle() override {
|
||||
std::lock_guard<std::mutex> lock(cache_res_mgr_->cache_res_mgr_mu_);
|
||||
|
||||
+2
-2
@@ -407,8 +407,8 @@ TEST(CacheReservationHandleTest, HandleTest) {
|
||||
std::size_t mem_used = 0;
|
||||
const std::size_t incremental_mem_used_handle_1 = 1 * kSizeDummyEntry;
|
||||
const std::size_t incremental_mem_used_handle_2 = 2 * kSizeDummyEntry;
|
||||
std::unique_ptr<CacheReservationManager::CacheReservationHandle> handle_1,
|
||||
handle_2;
|
||||
std::unique_ptr<CacheReservationManager::CacheReservationHandle> handle_1;
|
||||
std::unique_ptr<CacheReservationManager::CacheReservationHandle> handle_2;
|
||||
|
||||
// To test consecutive CacheReservationManager::MakeCacheReservation works
|
||||
// correctly in terms of returning the handle as well as updating cache
|
||||
|
||||
Vendored
+12
-3
@@ -975,7 +975,8 @@ void FixedHyperClockTable::Erase(const UniqueId64x2& hashed_key) {
|
||||
// Could be multiple entries in rare cases. Erase them all.
|
||||
// Optimistically increment acquire counter
|
||||
auto add_acquire = AcquireCounter::PlusTransformPromiseNoOverflow(1);
|
||||
SlotMeta old_meta, meta;
|
||||
SlotMeta old_meta;
|
||||
SlotMeta meta;
|
||||
h->meta.Apply(add_acquire, &old_meta, &meta);
|
||||
// Check if it's an entry visible to lookups
|
||||
if (meta.IsVisible()) {
|
||||
@@ -1738,7 +1739,8 @@ inline bool MatchAndRef(const UniqueId64x2* hashed_key, const ClockHandle& h,
|
||||
// Must be at least something to match
|
||||
assert(hashed_key || shift > 0);
|
||||
|
||||
SlotMeta old_meta, new_meta;
|
||||
SlotMeta old_meta;
|
||||
SlotMeta new_meta;
|
||||
// (Optimistically) increment acquire counter.
|
||||
auto add_acquire = AcquireCounter::PlusTransformPromiseNoOverflow(1);
|
||||
h.meta.Apply(add_acquire, &old_meta, &new_meta);
|
||||
@@ -1856,6 +1858,12 @@ class AutoHyperClockTable::ChainRewriteLock {
|
||||
assert(saved_head_.IsLocked());
|
||||
}
|
||||
|
||||
// No copying allowed
|
||||
ChainRewriteLock(const ChainRewriteLock&) = delete;
|
||||
ChainRewriteLock& operator=(const ChainRewriteLock&) = delete;
|
||||
ChainRewriteLock(ChainRewriteLock&&) = delete;
|
||||
ChainRewriteLock& operator=(ChainRewriteLock&&) = delete;
|
||||
|
||||
~ChainRewriteLock() {
|
||||
if (!IsEnd()) {
|
||||
// Release lock
|
||||
@@ -3122,7 +3130,8 @@ AutoHyperClockTable::HandleImpl* AutoHyperClockTable::Lookup(
|
||||
if (probably_equal) {
|
||||
// Increment acquire counter for definitive check
|
||||
auto add_acquire = AcquireCounter::PlusTransformPromiseNoOverflow(1);
|
||||
SlotMeta old_meta, new_meta;
|
||||
SlotMeta old_meta;
|
||||
SlotMeta new_meta;
|
||||
h->meta.Apply(add_acquire, &old_meta, &new_meta);
|
||||
// Check if it's a referencable (sharable) entry
|
||||
if (LIKELY(old_meta.IsShareable())) {
|
||||
|
||||
Vendored
+4
@@ -29,6 +29,10 @@ class CompressedSecondaryCacheResultHandle : public SecondaryCacheResultHandle {
|
||||
const CompressedSecondaryCacheResultHandle&) = delete;
|
||||
CompressedSecondaryCacheResultHandle& operator=(
|
||||
const CompressedSecondaryCacheResultHandle&) = delete;
|
||||
CompressedSecondaryCacheResultHandle(CompressedSecondaryCacheResultHandle&&) =
|
||||
delete;
|
||||
CompressedSecondaryCacheResultHandle& operator=(
|
||||
CompressedSecondaryCacheResultHandle&&) = delete;
|
||||
|
||||
bool IsReady() override { return true; }
|
||||
|
||||
|
||||
+2
-1
@@ -508,7 +508,8 @@ class CompressedSecondaryCacheTestBase : public testing::Test,
|
||||
std::string str1 = rnd.RandomString(1001);
|
||||
auto item1 = std::make_unique<TestItem>(str1.data(), str1.length());
|
||||
ASSERT_OK(cache->Insert(key1, item1.get(), GetHelper(), str1.length()));
|
||||
item1.release(); // Appease clang-analyze "potential memory leak"
|
||||
// Appease clang-analyze "potential memory leak"
|
||||
[[maybe_unused]] TestItem* released_item1 = item1.release();
|
||||
|
||||
Cache::Handle* handle;
|
||||
handle = cache->Lookup(key2, nullptr, this, Cache::Priority::LOW);
|
||||
|
||||
Vendored
+2
-1
@@ -1366,7 +1366,8 @@ TEST_P(BasicSecondaryCacheTest, BasicFailTest) {
|
||||
// str1.length()).IsInvalidArgument());
|
||||
ASSERT_OK(
|
||||
cache->Insert(k1.AsSlice(), item1.get(), GetHelper(), str1.length()));
|
||||
item1.release(); // Appease clang-analyze "potential memory leak"
|
||||
// Appease clang-analyze "potential memory leak"
|
||||
[[maybe_unused]] TestItem* released_item1 = item1.release();
|
||||
|
||||
Cache::Handle* handle;
|
||||
handle = cache->Lookup(k2.AsSlice(), nullptr, /*context*/ this,
|
||||
|
||||
Vendored
+9
-3
@@ -90,7 +90,7 @@ class CacheShardBase {
|
||||
class ShardedCacheBase : public Cache {
|
||||
public:
|
||||
explicit ShardedCacheBase(const ShardedCacheOptions& opts);
|
||||
virtual ~ShardedCacheBase() = default;
|
||||
~ShardedCacheBase() override = default;
|
||||
|
||||
int GetNumShardBits() const;
|
||||
uint32_t GetNumShards() const;
|
||||
@@ -143,7 +143,13 @@ class ShardedCache : public ShardedCacheBase {
|
||||
sizeof(CacheShard) * GetNumShards()))),
|
||||
destroy_shards_in_dtor_(false) {}
|
||||
|
||||
virtual ~ShardedCache() {
|
||||
// No copying allowed
|
||||
ShardedCache(const ShardedCache&) = delete;
|
||||
ShardedCache& operator=(const ShardedCache&) = delete;
|
||||
ShardedCache(ShardedCache&&) = delete;
|
||||
ShardedCache& operator=(ShardedCache&&) = delete;
|
||||
|
||||
~ShardedCache() override {
|
||||
if (destroy_shards_in_dtor_) {
|
||||
ForEachShard([](CacheShard* cs) { cs->~CacheShard(); });
|
||||
}
|
||||
@@ -245,7 +251,7 @@ class ShardedCache : public ShardedCacheBase {
|
||||
std::unique_ptr<size_t[]> states(new size_t[num_shards]{});
|
||||
|
||||
size_t aepl = opts.average_entries_per_lock;
|
||||
aepl = std::min(aepl, size_t{1});
|
||||
aepl = std::max(aepl, size_t{1});
|
||||
|
||||
bool remaining_work;
|
||||
do {
|
||||
|
||||
Vendored
+2
-2
@@ -36,7 +36,7 @@ class TieredSecondaryCache : public SecondaryCacheWrapper {
|
||||
#endif
|
||||
}
|
||||
|
||||
~TieredSecondaryCache() override {}
|
||||
~TieredSecondaryCache() override = default;
|
||||
|
||||
const char* Name() const override { return "TieredSecondaryCache"; }
|
||||
|
||||
@@ -75,7 +75,7 @@ class TieredSecondaryCache : public SecondaryCacheWrapper {
|
||||
|
||||
class ResultHandle : public SecondaryCacheResultHandle {
|
||||
public:
|
||||
~ResultHandle() override {}
|
||||
~ResultHandle() override = default;
|
||||
|
||||
bool IsReady() override {
|
||||
if (inner_handle_ && inner_handle_->IsReady()) {
|
||||
|
||||
Vendored
+1
-1
@@ -241,7 +241,7 @@ class FullTypedCacheHelperFns : public BasicTypedCacheHelperFns<TValue> {
|
||||
if (source != CacheTier::kVolatileTier) {
|
||||
return Status::InvalidArgument();
|
||||
}
|
||||
if constexpr (sizeof(TCreateContext) > 0) {
|
||||
if constexpr (!std::is_empty_v<TCreateContext>) {
|
||||
TCreateContext* tcontext = static_cast<TCreateContext*>(context);
|
||||
tcontext->Create(&value, out_charge, data, type, allocator);
|
||||
} else {
|
||||
|
||||
@@ -4,6 +4,122 @@
|
||||
Thorough multi-agent code review for RocksDB commits following CLAUDE.md guidelines,
|
||||
with structured codebase exploration, inter-agent debate, and verification.
|
||||
|
||||
## Behavioral Compatibility Review Perspective
|
||||
|
||||
Behavior changes are human judgment calls. They must be intentional,
|
||||
well-documented (including release notes), and researched for potential
|
||||
downstream production impact. Agents and reviewers must call out concrete gaps,
|
||||
ambiguity, or adverse impacts in the change's intent, documentation, and
|
||||
production-impact analysis.
|
||||
|
||||
When a diff changes how an existing accepted public or serialized input is
|
||||
interpreted—including an undocumented, discouraged, or historically ignored
|
||||
value—reviewers must:
|
||||
- Compare the old and new observable behavior, including performance and
|
||||
resource impact.
|
||||
- Check that the rationale and user-facing documentation clearly describe the
|
||||
change.
|
||||
- Identify known direct callers and stored configurations that may depend on
|
||||
the previous behavior.
|
||||
- Check for appropriate regression coverage, or a documented reason why such
|
||||
coverage is not practical.
|
||||
|
||||
A behavior change is not automatically a defect. Report a finding when the
|
||||
intent, documentation, downstream-impact analysis, or mitigation has a concrete
|
||||
gap or ambiguity that could lead to an unintended compatibility regression.
|
||||
|
||||
## DB Mutex Review Perspective
|
||||
|
||||
For every non-trivial change, determine whether the changed code, or any
|
||||
transitive callee it adds or changes, can run while `DBImpl::mutex_` is held.
|
||||
State the result explicitly, including when the DB mutex is not affected.
|
||||
|
||||
`DBImpl::mutex_` is the DB-wide metadata and coordination mutex. It protects
|
||||
cross-object invariants involving `ColumnFamilyData`, memtable and
|
||||
`SuperVersion` publication, `VersionSet`, snapshots, flush/compaction queues,
|
||||
background-job state, and file lifetime. It is shared by all column families
|
||||
and can be a hot lock. It is not the filesystem `db_lock_`, and it should not
|
||||
replace data-path synchronization already provided by `WriteThread`,
|
||||
`wal_write_mutex_`, atomics, referenced `SuperVersion`s, or component-local
|
||||
synchronization.
|
||||
|
||||
When a DB-mutex critical section is affected:
|
||||
|
||||
### Locking and correctness
|
||||
|
||||
- Trace the full call path and mark the exact lock and unlock boundaries. Do
|
||||
not inspect only the lines that directly acquire the mutex.
|
||||
- State the locking contract. Helpers that require the lock should document it
|
||||
and normally use `mutex_.AssertHeld()`. Check whether each caller expects the
|
||||
lock to be held on return.
|
||||
- Treat calls that can release and re-acquire the mutex as concurrency
|
||||
boundaries. Examples include `VersionSet::LogAndApply()`, condition-variable
|
||||
waits, write-thread coordination, and notification helpers. Revalidate state
|
||||
inspected before such a call.
|
||||
- Keep atomic metadata transitions under the mutex. Examples include moving a
|
||||
memtable between mutable and immutable state, reserving compaction inputs,
|
||||
installing a `Version` or `SuperVersion`, updating scheduling counters and
|
||||
queues, and changing file-lifetime protection.
|
||||
- For expensive work, prefer a prepare/run/install structure: reserve and pin
|
||||
the required state while locked, do CPU or I/O work while unlocked, then
|
||||
re-lock, validate, and publish. Before unlocking, copy mutable options and
|
||||
acquire every required object reference or lifetime reservation.
|
||||
- Check object lifetime across every unlocked region. Do not use a raw
|
||||
`ColumnFamilyData*`, `Version*`, memtable, file metadata pointer, or DB-owned
|
||||
container element after unlocking unless an explicit reference,
|
||||
`SuperVersion`, job object, or pending-output reservation keeps it alive.
|
||||
- Preserve the snapshot/input-selection boundary. Flush and compaction must not
|
||||
miss a snapshot whose visible key versions they may remove. Missing a newly
|
||||
created snapshot is a correctness bug; retaining a recently released
|
||||
snapshot is conservative but safe.
|
||||
- Never invoke untrusted or re-entrant code while holding the mutex. The mutex
|
||||
is non-reentrant, so a callback into RocksDB can self-deadlock.
|
||||
- Check lock ordering. `options_mutex_` is acquired before the DB mutex. When
|
||||
both the DB mutex and `wal_write_mutex_` are needed, the DB mutex is acquired
|
||||
first. Establish and document the order for other locks.
|
||||
- Keep cleanup outside the critical section when possible. File deletion,
|
||||
`JobContext::Clean()`, expensive `SuperVersion` destruction, logging, and
|
||||
listener callbacks should normally run after protected state is detached or
|
||||
snapshotted.
|
||||
- Do not add the DB mutex to steady-state point reads or the normal write data
|
||||
phase without strong justification. Reads normally use a referenced
|
||||
`SuperVersion`; writes use `WriteThread` and component-specific
|
||||
synchronization.
|
||||
|
||||
### Critical-section performance
|
||||
|
||||
- Compare the CPU and I/O performed under the mutex before and after the
|
||||
change. Include call frequency, loop bounds, input-size scaling, hidden work
|
||||
in callees, and worst-case cost.
|
||||
- Account for filesystem or network I/O, waits, callbacks, logging and
|
||||
formatting, allocation, container growth, copies, sorting, scans, reference
|
||||
release and destructors, cache misses, and repeated metadata lookups.
|
||||
- Do not allow a significant regression in mutex-held work without clear
|
||||
justification, hold-time or contention measurements, and representative
|
||||
benchmarks.
|
||||
- Treat unavoidable work under the mutex as hot-path code and keep it highly
|
||||
optimized. Precompute, allocate, copy, and perform I/O outside the lock;
|
||||
remove repeated scans and lookups; cache or batch work where practical; and
|
||||
keep locked operations O(1) or tightly bounded when possible.
|
||||
- Treat new blocking I/O, waits, callbacks into user or plugin code, unbounded
|
||||
loops, or work that scales with DB size, files, column families, snapshots,
|
||||
or pending jobs as a blocker unless it is unavoidable and supported by strong
|
||||
measurements and explicit design justification.
|
||||
|
||||
### Validation and evidence
|
||||
|
||||
- Test unlock windows with sync points rather than sleeps. Cover relevant races
|
||||
with memtable switches, snapshot create/release, column-family drop,
|
||||
flush/compaction install, shutdown, and background errors. Run TSAN where
|
||||
appropriate.
|
||||
- Distinguish wait time from hold time. `PerfContext::db_mutex_lock_nanos` and
|
||||
`DB_MUTEX_WAIT_MICROS` primarily expose acquisition wait; use contention
|
||||
profiles or scoped instrumentation to find long critical sections.
|
||||
- Report affected critical sections, before/after cost, workload assumptions,
|
||||
optimization choices, and remaining risk. If exact measurement is not
|
||||
practical, provide a conservative static bound and do not claim no regression
|
||||
without evidence.
|
||||
|
||||
IMPORTANT — Where the review goes: Your FINAL RESPONSE TEXT is the review.
|
||||
The PR comment is generated verbatim from your final response, so the
|
||||
complete, polished review MUST appear there in full. Do NOT end your turn with
|
||||
@@ -283,6 +399,9 @@ in isolation may be a critical bug when you consider the full call chain.
|
||||
- **Callee side effects**: Does the change call functions that mutate shared
|
||||
state (counters, seqnos, metadata)? Are the mutations correct for the new
|
||||
calling context? (Use the callee-chain analysis from context.md)
|
||||
- Apply the DB Mutex Review Perspective above to locking contracts, unlocked
|
||||
object lifetimes, stale-state validation, lock ordering, and snapshot
|
||||
boundaries.
|
||||
|
||||
#### Agent: Cross-Component & Adversarial Reviewer
|
||||
|
||||
@@ -386,6 +505,9 @@ Write findings to `findings-caller-audit.md`.
|
||||
- Loop optimization opportunities
|
||||
- Branch prediction (LIKELY/UNLIKELY)
|
||||
- Zero-overhead design verification
|
||||
- Apply the DB Mutex Review Perspective above, with special focus on transitive
|
||||
CPU and I/O, before/after cost, critical-section hold time, and benchmark
|
||||
evidence
|
||||
|
||||
#### Agent: API & Compatibility Reviewer
|
||||
- Public API backwards compatibility
|
||||
@@ -393,8 +515,8 @@ Write findings to `findings-caller-audit.md`.
|
||||
- Documentation completeness
|
||||
- Deprecation policy compliance
|
||||
- Forward compatibility (struct extensibility)
|
||||
- **Behavioral compatibility**: Do changed semantics (e.g., kOutOfBound vs
|
||||
kUnknown) break implicit contracts with callers?
|
||||
- Apply the Behavioral Compatibility Review Perspective above to changes in
|
||||
observable semantics and implicit caller contracts.
|
||||
|
||||
#### Agent: Serialization & Deserialization Reviewer
|
||||
- Format correctness and versioning
|
||||
@@ -569,6 +691,8 @@ Rules for this structure:
|
||||
- [ ] Multi-component interactions (compaction, recovery, snapshots, iterators)
|
||||
- [ ] Configuration dependencies and unexpected option combinations
|
||||
- [ ] Potential pitfalls from caller-chain analysis
|
||||
- [ ] DB-mutex reachability checked through transitive callees; affected lock
|
||||
and unlock boundaries documented
|
||||
|
||||
### Review Phase (verified by agents)
|
||||
- [ ] Database semantics preserved (snapshot isolation, key ordering)
|
||||
@@ -582,6 +706,10 @@ Rules for this structure:
|
||||
- [ ] Code follows RocksDB style conventions
|
||||
- [ ] Behavioral contracts with upstream callers preserved
|
||||
- [ ] All callers enumerated with parameter range table
|
||||
- [ ] Before/after CPU and I/O under the DB mutex compared, with call frequency,
|
||||
scaling, hidden callee work, and worst-case cost included
|
||||
- [ ] No significant DB-mutex-held work regression; unavoidable work is highly
|
||||
optimized, bounded, and supported by measurements when material
|
||||
|
||||
## Output Structure
|
||||
Your **final response text is the primary output** — it becomes the PR comment
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# RocksDB Diff and Pull Request Summary Guidance
|
||||
|
||||
This guidance applies to both internal Phabricator diff summaries and external
|
||||
GitHub pull request summaries for changes under `fbcode/internal_repo_rocksdb/`.
|
||||
|
||||
## Goal
|
||||
|
||||
Write each diff or pull request summary for a reviewer who has not read the
|
||||
patch. The summary must explain the behavioral story and the evidence for it,
|
||||
not merely inventory the edited files or symbols.
|
||||
|
||||
Every new or updated diff or pull request summary must clearly cover:
|
||||
|
||||
1. Why the change is required.
|
||||
2. What the previous behavior was, including the failure mode or limitation.
|
||||
3. What the new behavior is.
|
||||
4. What changed in the implementation.
|
||||
5. Safety boundaries, compatibility, and unsupported or intentionally deferred
|
||||
cases.
|
||||
6. Tests performed and known limitations of the validation.
|
||||
|
||||
Use explicit headings for a broad, subtle, or high-risk change. A small,
|
||||
focused change may combine these points into a few paragraphs, but none may be
|
||||
left implicit when it affects a review decision.
|
||||
|
||||
## What Good RocksDB Summaries Make Concrete
|
||||
|
||||
- Name the exact conditions that activate a bug or behavior: relevant options,
|
||||
call ordering, storage mode, concurrency mode, file format, platform, or
|
||||
workload shape. Also state important cases that are not affected.
|
||||
- Explain the causal chain from the old behavior to its externally observable
|
||||
result. For correctness fixes, distinguish a symptom from the underlying
|
||||
invariant violation.
|
||||
- Describe the new contract before implementation mechanics. Then identify the
|
||||
essential implementation choices and why they are sufficient.
|
||||
- Call out API, ABI, serialized-format, behavioral, and performance
|
||||
compatibility where relevant. For additive persisted fields, state how older
|
||||
data is interpreted and whether older readers remain supported.
|
||||
- State safety boundaries precisely: defaults, feature gates, fallback paths,
|
||||
ownership and lifetime constraints, memory-ordering assumptions, and cases
|
||||
deliberately postponed to a follow-up.
|
||||
- Support performance claims with a baseline, workload and configuration,
|
||||
measurement method, and results. Report meaningful tradeoffs, not only the
|
||||
winning metric.
|
||||
- Make the test plan evidence-based. Name the tests and configurations, say
|
||||
what behavior each proves, and report failures, timeouts, untested platforms,
|
||||
or stale pre-rebase results honestly. For a regression test, say how it fails
|
||||
without the fix when that was verified.
|
||||
- Separate incidental cleanup or a pre-existing test fix from the primary
|
||||
change, and explain why it belongs in the diff.
|
||||
|
||||
Avoid vague claims such as "improves performance," "fixes an issue," or "tests
|
||||
pass." Avoid copying the patch into prose, exhaustive file-by-file narration,
|
||||
raw command output, and claims broader than the evidence.
|
||||
|
||||
## Suggested Structure
|
||||
|
||||
Adapt this structure to the change; do not keep empty or irrelevant sections.
|
||||
|
||||
```text
|
||||
Summary:
|
||||
|
||||
Why the change is required:
|
||||
<User or system impact, and why the current behavior is inadequate.>
|
||||
|
||||
Previous behavior:
|
||||
<Old contract or mechanism, triggering conditions, and observable result.>
|
||||
|
||||
New behavior:
|
||||
<New contract and externally visible effect.>
|
||||
|
||||
Implementation:
|
||||
<Essential design and code changes; omit routine edit-by-edit narration.>
|
||||
|
||||
Safety, compatibility, and limitations:
|
||||
<Defaults, unaffected paths, compatibility, unsupported cases, and follow-ups.>
|
||||
|
||||
Test Plan:
|
||||
<Exact tests/benchmarks, configurations, what they demonstrate, and gaps.>
|
||||
```
|
||||
|
||||
For a focused bug fix, a cohesive narrative can be clearer than six headings:
|
||||
problem and root cause, trigger and unaffected cases, fix, then quantified or
|
||||
counterfactual validation. For a multi-part change, labeled bullets such as
|
||||
`Fix`, `Add`, and `Change` can make the implementation and review surface easy
|
||||
to scan.
|
||||
|
||||
## Reference Pull Request Summaries
|
||||
|
||||
- [PR #15216](https://github.com/facebook/rocksdb/pull/15216) is a strong
|
||||
focused bug-fix example. It traces stale iterator state to readahead behavior
|
||||
and dropped keys, identifies the exact prerequisites and unaffected mmap
|
||||
case, gives before/after measurements, and explains how the regression test
|
||||
fails when the fix is reverted.
|
||||
- [PR #15218](https://github.com/facebook/rocksdb/pull/15218) is a concise
|
||||
example of explicit `Why`, `Previous behavior`, `New behavior`,
|
||||
`Implementation`, and `Safety boundaries and unsupported cases` sections.
|
||||
- Peter Dillinger's [PR #15231](https://github.com/facebook/rocksdb/pull/15231)
|
||||
is a strong recent multi-part example. Its `Fix`/`Add`/`Change` bullets
|
||||
separate the behaviors, its compatibility sentence explains how old files
|
||||
read back, and its test plan maps each new persisted property to concrete
|
||||
coverage while explaining an adjacent test adjustment.
|
||||
|
||||
Use these pull requests as models for reasoning, specificity, and evidence. Do
|
||||
not copy their length or headings mechanically; scale the summary to the risk
|
||||
and complexity of the change.
|
||||
@@ -25,6 +25,8 @@ CRASHTEST_PY=$(PYTHON) -u tools/db_crashtest.py --stress_cmd=$(DB_STRESS_CMD) --
|
||||
crash_test_with_multiops_wup_txn \
|
||||
crash_test_with_optimistic_txn \
|
||||
crash_test_with_tiered_storage \
|
||||
crash_test_with_full_external_table \
|
||||
liveness_crash_test \
|
||||
blackbox_crash_test blackbox_crash_test_with_atomic_flush \
|
||||
blackbox_crash_test_with_wc_txn blackbox_crash_test_with_wp_txn \
|
||||
blackbox_crash_test_with_wup_txn \
|
||||
@@ -35,6 +37,7 @@ CRASHTEST_PY=$(PYTHON) -u tools/db_crashtest.py --stress_cmd=$(DB_STRESS_CMD) --
|
||||
blackbox_crash_test_with_multiops_wup_txn \
|
||||
blackbox_crash_test_with_optimistic_txn \
|
||||
blackbox_crash_test_with_tiered_storage \
|
||||
blackbox_crash_test_with_full_external_table \
|
||||
whitebox_crash_test whitebox_crash_test_with_atomic_flush \
|
||||
whitebox_crash_test_with_wc_txn whitebox_crash_test_with_wp_txn \
|
||||
whitebox_crash_test_with_wup_txn \
|
||||
@@ -85,6 +88,8 @@ crash_test_with_tiered_storage: $(DB_STRESS_CMD)
|
||||
$(CRASHTEST_MAKE) whitebox_crash_test_with_tiered_storage
|
||||
$(CRASHTEST_MAKE) blackbox_crash_test_with_tiered_storage
|
||||
|
||||
crash_test_with_full_external_table: blackbox_crash_test_with_full_external_table
|
||||
|
||||
crash_test_with_multiops_wc_txn: $(DB_STRESS_CMD)
|
||||
$(CRASHTEST_MAKE) blackbox_crash_test_with_multiops_wc_txn
|
||||
|
||||
@@ -94,6 +99,9 @@ crash_test_with_multiops_wp_txn: $(DB_STRESS_CMD)
|
||||
crash_test_with_multiops_wup_txn: $(DB_STRESS_CMD)
|
||||
$(CRASHTEST_MAKE) blackbox_crash_test_with_multiops_wup_txn
|
||||
|
||||
liveness_crash_test: $(DB_STRESS_CMD)
|
||||
$(CRASHTEST_PY) liveness $(CRASH_TEST_EXT_ARGS)
|
||||
|
||||
blackbox_crash_test: $(DB_STRESS_CMD)
|
||||
$(CRASHTEST_PY) --simple blackbox $(CRASH_TEST_EXT_ARGS)
|
||||
$(CRASHTEST_PY) blackbox $(CRASH_TEST_EXT_ARGS)
|
||||
@@ -128,6 +136,9 @@ blackbox_crash_test_with_multiops_wup_txn: $(DB_STRESS_CMD)
|
||||
blackbox_crash_test_with_tiered_storage: $(DB_STRESS_CMD)
|
||||
$(CRASHTEST_PY) --test_tiered_storage blackbox $(CRASH_TEST_EXT_ARGS)
|
||||
|
||||
blackbox_crash_test_with_full_external_table: $(DB_STRESS_CMD)
|
||||
$(CRASHTEST_PY) --test_full_external_table blackbox $(CRASH_TEST_EXT_ARGS)
|
||||
|
||||
blackbox_crash_test_with_optimistic_txn: $(DB_STRESS_CMD)
|
||||
$(CRASHTEST_PY) --optimistic_txn blackbox $(CRASH_TEST_EXT_ARGS)
|
||||
|
||||
|
||||
@@ -42,6 +42,13 @@ class ArenaWrappedDBIter : public Iterator {
|
||||
std::unique_ptr<ColumnFamilyData, ColumnFamilyDataUnrefDeleter>;
|
||||
|
||||
public:
|
||||
ArenaWrappedDBIter() = default;
|
||||
// No copying allowed
|
||||
ArenaWrappedDBIter(const ArenaWrappedDBIter&) = delete;
|
||||
ArenaWrappedDBIter& operator=(const ArenaWrappedDBIter&) = delete;
|
||||
ArenaWrappedDBIter(ArenaWrappedDBIter&&) = delete;
|
||||
ArenaWrappedDBIter& operator=(ArenaWrappedDBIter&&) = delete;
|
||||
|
||||
~ArenaWrappedDBIter() override {
|
||||
if (db_iter_ != nullptr) {
|
||||
DestroyDBIter();
|
||||
|
||||
@@ -24,6 +24,8 @@ class AttributeGroupIteratorImpl : public AttributeGroupIterator {
|
||||
AttributeGroupIteratorImpl(const AttributeGroupIteratorImpl&) = delete;
|
||||
AttributeGroupIteratorImpl& operator=(const AttributeGroupIteratorImpl&) =
|
||||
delete;
|
||||
AttributeGroupIteratorImpl(AttributeGroupIteratorImpl&&) = delete;
|
||||
AttributeGroupIteratorImpl& operator=(AttributeGroupIteratorImpl&&) = delete;
|
||||
|
||||
bool Valid() const override { return impl_.Valid(); }
|
||||
void SeekToFirst() override { impl_.SeekToFirst(); }
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
|
||||
@@ -31,4 +33,43 @@ constexpr uint64_t kInvalidBlobFileNumber = 0;
|
||||
constexpr uint64_t kCurrentFileBlobIndexFileNumber = kInvalidBlobFileNumber;
|
||||
static_assert(kCurrentFileBlobIndexFileNumber == kInvalidBlobFileNumber);
|
||||
|
||||
// Sentinel range length meaning "the whole blob value" for the unified
|
||||
// blob-read primitives, which take a range descriptor {range_offset,
|
||||
// range_length}; range_length == kWholeBlobLength selects the whole value and
|
||||
// any other value selects the strict sub-range
|
||||
// [range_offset, range_offset + range_length).
|
||||
//
|
||||
// This is the internal counterpart of the public kLazyWholeColumn
|
||||
// (include/rocksdb/lazy_wide_columns.h); both are the maximum size_t. The lazy
|
||||
// resolver does not forward the public value as a range descriptor -- it
|
||||
// normalizes a whole-column request to kWholeBlobLength -- so the two are not
|
||||
// required to be equal, but both rely on the sentinel exceeding any real
|
||||
// blob/column size so a "whole" request is never mistaken for a strict
|
||||
// sub-range. Keep them in sync.
|
||||
inline constexpr size_t kWholeBlobLength = std::numeric_limits<size_t>::max();
|
||||
|
||||
// How aggressively a blob read verifies the record's checksum. Derived once at
|
||||
// the lazy read-path boundary (ReadPathBlobResolver) from
|
||||
// (ReadOptions::verify_checksums, LazyColumnReadRequest::force_verify) and
|
||||
// threaded down through the blob-read primitives, replacing the earlier mix of
|
||||
// a verify_checksums bool and a separate force-verify method. (Not literally a
|
||||
// constant, but kept here as a small shared blob-read enum rather than its own
|
||||
// header.)
|
||||
//
|
||||
// - kVerifyIfPresent: verify the whole-record checksum whenever the format
|
||||
// provides one, even if ReadOptions::verify_checksums is off. A byte-range
|
||||
// request under this policy is escalated to a whole-record read (a strict
|
||||
// sub-range cannot cover the checksum). This is the public force_verify.
|
||||
// - kVerifyIfNoAmplification: verify when the checksummable unit is already
|
||||
// being read -- a whole-value read verifies (the check is "free"), while a
|
||||
// strict sub-range read skips verification rather than amplify the read to
|
||||
// the whole record. Corresponds to verify_checksums on, force_verify off.
|
||||
// - kSkip: never verify. Corresponds to verify_checksums off, force_verify
|
||||
// off.
|
||||
enum class BlobVerifyPolicy : uint8_t {
|
||||
kVerifyIfPresent,
|
||||
kVerifyIfNoAmplification,
|
||||
kSkip,
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
+41
-36
@@ -44,43 +44,41 @@ Status VersionBlobFetcherBase::FetchBlob(const Slice& user_key,
|
||||
prefetch_buffer, blob_value, bytes_read);
|
||||
}
|
||||
|
||||
Status VersionBlobFetcherBase::FetchBlobRange(const Slice& user_key,
|
||||
const BlobIndex& blob_index,
|
||||
uint64_t range_offset,
|
||||
size_t range_length,
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const {
|
||||
// Only ever called when SupportsRangeRead() holds (a Version to read through,
|
||||
// no direct-write fallback); the direct-write path resolves whole records and
|
||||
// has no range variant.
|
||||
assert(SupportsRangeRead());
|
||||
assert(version_);
|
||||
return version_->GetBlobRange(read_options(), user_key, blob_index,
|
||||
range_offset, range_length, blob_value,
|
||||
bytes_read);
|
||||
}
|
||||
|
||||
Status VersionBlobFetcherBase::FetchBlobForceVerify(
|
||||
const Slice& user_key, const BlobIndex& blob_index,
|
||||
FilePrefetchBuffer* prefetch_buffer, PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const {
|
||||
if (read_options().verify_checksums) {
|
||||
// Verification already happens on the normal path.
|
||||
Status VersionBlobFetcherBase::FetchBlobRange(
|
||||
const Slice& user_key, const BlobIndex& blob_index, uint64_t range_offset,
|
||||
size_t range_length, BlobVerifyPolicy verify_policy,
|
||||
PinnableSlice* blob_value, uint64_t* bytes_read) const {
|
||||
if (range_length == kWholeBlobLength) {
|
||||
// Whole-value read. Delegate to FetchBlob (which handles the direct-write
|
||||
// fallback). For kVerifyIfPresent, force whole-record checksum verification
|
||||
// even when ReadOptions::verify_checksums is off, via a transient fetcher
|
||||
// over a verify-enabled copy of the read options (both locals outlive the
|
||||
// borrowed-options VersionBlobFetcher).
|
||||
constexpr FilePrefetchBuffer* prefetch_buffer = nullptr;
|
||||
if (verify_policy == BlobVerifyPolicy::kVerifyIfPresent &&
|
||||
!read_options().verify_checksums) {
|
||||
ReadOptions verify_read_options = read_options();
|
||||
verify_read_options.verify_checksums = true;
|
||||
VersionBlobFetcher verifying_fetcher(version_, verify_read_options,
|
||||
blob_file_cache_,
|
||||
allow_write_path_fallback_);
|
||||
return verifying_fetcher.FetchBlob(user_key, blob_index, prefetch_buffer,
|
||||
blob_value, bytes_read);
|
||||
}
|
||||
return FetchBlob(user_key, blob_index, prefetch_buffer, blob_value,
|
||||
bytes_read);
|
||||
}
|
||||
|
||||
// Force verification via a transient fetcher over a verify-enabled copy of
|
||||
// the read options. `verify_read_options` outlives `verifying_fetcher` (both
|
||||
// are local to this call), so the borrowed-options VersionBlobFetcher is
|
||||
// safe.
|
||||
ReadOptions verify_read_options = read_options();
|
||||
verify_read_options.verify_checksums = true;
|
||||
VersionBlobFetcher verifying_fetcher(version_, verify_read_options,
|
||||
blob_file_cache_,
|
||||
allow_write_path_fallback_);
|
||||
return verifying_fetcher.FetchBlob(user_key, blob_index, prefetch_buffer,
|
||||
blob_value, bytes_read);
|
||||
// Strict sub-range read: value bytes only, no whole-record checksum, no cache
|
||||
// fill. Only ever chosen when SupportsRangeRead() holds (a Version to read
|
||||
// through, no direct-write fallback) and verification is not forced (a strict
|
||||
// sub-range cannot cover the whole-record checksum).
|
||||
assert(SupportsRangeRead());
|
||||
assert(version_);
|
||||
assert(verify_policy != BlobVerifyPolicy::kVerifyIfPresent);
|
||||
return version_->GetBlobRange(read_options(), user_key, blob_index,
|
||||
range_offset, range_length, blob_value,
|
||||
bytes_read);
|
||||
}
|
||||
|
||||
Status EmbeddedAwareBlobFetcher::FetchBlob(const Slice& user_key,
|
||||
@@ -94,9 +92,16 @@ Status EmbeddedAwareBlobFetcher::FetchBlob(const Slice& user_key,
|
||||
// Same-file ("embedded") blob records live in the current SST and are read
|
||||
// by the SameFileBlobReader (the BlockBasedTable). A separate blob-file
|
||||
// prefetch buffer and bytes_read counter do not apply here (embedded reads
|
||||
// account their own BLOB_DB_* stats via BlobSource).
|
||||
return same_file_reader_->GetSameFileBlob(read_options(), blob_index,
|
||||
blob_value);
|
||||
// account their own BLOB_DB_* stats via BlobSource). This is a whole-value
|
||||
// read; the verify policy follows read_options().verify_checksums (the
|
||||
// decorator carries no separate force-verify signal).
|
||||
const BlobVerifyPolicy verify_policy =
|
||||
read_options().verify_checksums
|
||||
? BlobVerifyPolicy::kVerifyIfNoAmplification
|
||||
: BlobVerifyPolicy::kSkip;
|
||||
return same_file_reader_->GetSameFileBlob(
|
||||
read_options(), blob_index,
|
||||
/*range_offset=*/0, kWholeBlobLength, verify_policy, blob_value);
|
||||
}
|
||||
|
||||
if (base_ == nullptr) {
|
||||
|
||||
+23
-27
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "db/blob/blob_constants.h"
|
||||
#include "rocksdb/options.h"
|
||||
#include "rocksdb/status.h"
|
||||
|
||||
@@ -63,37 +64,26 @@ class VersionBlobFetcherBase : public BlobFetcher {
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const override;
|
||||
|
||||
// Fetches a byte sub-range [range_offset, range_offset + range_length) of an
|
||||
// *uncompressed*, separate-file blob's value into *blob_value, reading only
|
||||
// those bytes on a cache miss (see Version::GetBlobRange /
|
||||
// BlobSource::GetBlobRange). Only valid when SupportsRangeRead() is true (a
|
||||
// Version to read through and no direct-write fallback); callers check that
|
||||
// first and take the whole-value FetchBlob path otherwise. Not routed through
|
||||
// the same-file (embedded) decorator -- same-file range reads are a later
|
||||
// phase.
|
||||
// Fetches the value of a separate-file blob reference into *blob_value,
|
||||
// applying `verify_policy` (see BlobVerifyPolicy). range_length ==
|
||||
// kWholeBlobLength reads the whole value (delegating to FetchBlob, which also
|
||||
// handles the direct-write fallback, and forcing whole-record checksum
|
||||
// verification for kVerifyIfPresent even when ReadOptions::verify_checksums
|
||||
// is off). Any other length reads only the sub-range [range_offset,
|
||||
// range_offset + range_length) directly from the blob file, reading just
|
||||
// those bytes on a cache miss -- no record-header/key read, no whole-record
|
||||
// checksum, no cache fill (see Version::GetBlobRange /
|
||||
// BlobSource::GetBlobRange). A strict sub-range is only valid when
|
||||
// SupportsRangeRead() is true and verify_policy != kVerifyIfPresent
|
||||
// (verifying a strict sub-range would have to amplify the read to the whole
|
||||
// record); callers check that first and take the whole-value path otherwise.
|
||||
// Not routed through the same-file (embedded) decorator -- same-file
|
||||
// references are resolved by the caller through a SameFileBlobReader.
|
||||
Status FetchBlobRange(const Slice& user_key, const BlobIndex& blob_index,
|
||||
uint64_t range_offset, size_t range_length,
|
||||
BlobVerifyPolicy verify_policy,
|
||||
PinnableSlice* blob_value, uint64_t* bytes_read) const;
|
||||
|
||||
// Fetches the whole blob with checksum verification forced on, regardless of
|
||||
// this fetcher's ReadOptions::verify_checksums. Used to honor a lazy read's
|
||||
// force_verify when verify_checksums is off: the caller wants the record's
|
||||
// checksum checked even though the global option would skip it. When
|
||||
// verify_checksums is already on this is just FetchBlob.
|
||||
//
|
||||
// TODO(lazy-blob-resolution-cleanup): fold FetchBlobRange /
|
||||
// FetchBlobForceVerify (and the whole-vs-range GetBlob twins throughout the
|
||||
// blob stack) into one parameterized primitive taking a BlobReadRequest -- a
|
||||
// range descriptor ({offset, len}; whole == [0, npos)) plus a 3-valued verify
|
||||
// policy (must-verify-if-present / verify-if-no-amplification / ignore). That
|
||||
// retires this method and the range/whole method pairs. See the lazy blob
|
||||
// resolution plan (deferred for feature velocity).
|
||||
Status FetchBlobForceVerify(const Slice& user_key,
|
||||
const BlobIndex& blob_index,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const;
|
||||
|
||||
// True if this fetcher has enough context to resolve a (non-inlined,
|
||||
// non-same-file) blob reference: either a Version to read through, or the
|
||||
// direct-write fallback (a blob file cache) enabled. Callers that may hold a
|
||||
@@ -111,6 +101,12 @@ class VersionBlobFetcherBase : public BlobFetcher {
|
||||
return version_ != nullptr && !allow_write_path_fallback_;
|
||||
}
|
||||
|
||||
// The Version this fetcher reads separate-file blob references through (may
|
||||
// be null for a direct-write-fallback-only fetcher). Used by the cross-key
|
||||
// lazy coalescing path to group and dispatch reads via
|
||||
// Version::MultiGetBlobLazy.
|
||||
const Version* version() const { return version_; }
|
||||
|
||||
protected:
|
||||
VersionBlobFetcherBase(const Version* version, BlobFileCache* blob_file_cache,
|
||||
bool allow_write_path_fallback)
|
||||
|
||||
@@ -68,6 +68,8 @@ class BlobFileBuilder {
|
||||
|
||||
BlobFileBuilder(const BlobFileBuilder&) = delete;
|
||||
BlobFileBuilder& operator=(const BlobFileBuilder&) = delete;
|
||||
BlobFileBuilder(BlobFileBuilder&&) = delete;
|
||||
BlobFileBuilder& operator=(BlobFileBuilder&&) = delete;
|
||||
|
||||
~BlobFileBuilder();
|
||||
|
||||
|
||||
+88
-24
@@ -38,12 +38,14 @@ BlobFileCache::BlobFileCache(Cache* cache,
|
||||
}
|
||||
|
||||
Status BlobFileCache::GetBlobFileReader(
|
||||
const ReadOptions& read_options, uint64_t blob_file_number,
|
||||
const ReadOptions& read_options, const BlobFileOpenInfo& blob_file,
|
||||
CacheHandleGuard<BlobFileReader>* blob_file_reader,
|
||||
bool allow_footer_skip_retry) {
|
||||
assert(blob_file_reader);
|
||||
assert(blob_file_reader->IsEmpty());
|
||||
|
||||
const uint64_t blob_file_number = blob_file.file_number;
|
||||
|
||||
// NOTE: sharing same Cache with table_cache
|
||||
const Slice key = GetSliceForKey(&blob_file_number);
|
||||
|
||||
@@ -74,18 +76,8 @@ Status BlobFileCache::GetBlobFileReader(
|
||||
std::unique_ptr<BlobFileReader> reader;
|
||||
|
||||
{
|
||||
assert(file_options_);
|
||||
Status s = BlobFileReader::Create(
|
||||
*immutable_options_, read_options, *file_options_, column_family_id_,
|
||||
blob_file_read_hist_, blob_file_number, io_tracer_,
|
||||
/*skip_footer_validation=*/false, &reader);
|
||||
if (!s.ok() && s.IsCorruption() && allow_footer_skip_retry) {
|
||||
reader.reset();
|
||||
s = BlobFileReader::Create(
|
||||
*immutable_options_, read_options, *file_options_, column_family_id_,
|
||||
blob_file_read_hist_, blob_file_number, io_tracer_,
|
||||
/*skip_footer_validation=*/true, &reader);
|
||||
}
|
||||
Status s = OpenBlobFileReader(read_options, blob_file,
|
||||
allow_footer_skip_retry, &reader);
|
||||
if (!s.ok()) {
|
||||
ROCKS_LOG_WARN(immutable_options_->logger,
|
||||
"BlobFileCache open failed for blob file %" PRIu64
|
||||
@@ -106,7 +98,7 @@ Status BlobFileCache::GetBlobFileReader(
|
||||
}
|
||||
}
|
||||
|
||||
reader.release();
|
||||
[[maybe_unused]] BlobFileReader* released_reader = reader.release();
|
||||
|
||||
*blob_file_reader = cache_.Guard(handle);
|
||||
|
||||
@@ -114,7 +106,7 @@ Status BlobFileCache::GetBlobFileReader(
|
||||
}
|
||||
|
||||
Status BlobFileCache::OpenBlobFileReaderUncached(
|
||||
const ReadOptions& read_options, uint64_t blob_file_number,
|
||||
const ReadOptions& read_options, const BlobFileOpenInfo& blob_file,
|
||||
std::unique_ptr<BlobFileReader>* blob_file_reader,
|
||||
bool allow_footer_skip_retry) {
|
||||
assert(blob_file_reader);
|
||||
@@ -123,21 +115,77 @@ Status BlobFileCache::OpenBlobFileReaderUncached(
|
||||
Statistics* const statistics = immutable_options_->stats;
|
||||
RecordTick(statistics, NO_FILE_OPENS);
|
||||
|
||||
Status s = OpenBlobFileReader(read_options, blob_file,
|
||||
allow_footer_skip_retry, blob_file_reader);
|
||||
if (!s.ok()) {
|
||||
RecordTick(statistics, NO_FILE_ERRORS);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
void BlobFileCache::RegisterBlobFileChecksum(
|
||||
const BlobFileOpenInfo& blob_file) {
|
||||
assert(blob_file.file_checksum.empty() ==
|
||||
blob_file.file_checksum_func_name.empty());
|
||||
if (blob_file.file_checksum.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Slice key = GetSliceForKey(&blob_file.file_number);
|
||||
MutexLock cache_lock(&mutex_.Get(key));
|
||||
{
|
||||
MutexLock checksum_lock(&blob_file_checksums_mutex_);
|
||||
BlobFileChecksum& checksum = blob_file_checksums_[blob_file.file_number];
|
||||
checksum.value = blob_file.file_checksum.ToString();
|
||||
checksum.function_name = blob_file.file_checksum_func_name.ToString();
|
||||
}
|
||||
|
||||
// Readers opened while the file was active did not carry finalized metadata
|
||||
// and may also have observed a footer-less file size.
|
||||
cache_.get()->Erase(key);
|
||||
}
|
||||
|
||||
void BlobFileCache::UnregisterBlobFileChecksum(uint64_t blob_file_number) {
|
||||
MutexLock checksum_lock(&blob_file_checksums_mutex_);
|
||||
blob_file_checksums_.erase(blob_file_number);
|
||||
}
|
||||
|
||||
Status BlobFileCache::OpenBlobFileReader(
|
||||
const ReadOptions& read_options, const BlobFileOpenInfo& blob_file,
|
||||
bool allow_footer_skip_retry,
|
||||
std::unique_ptr<BlobFileReader>* blob_file_reader) {
|
||||
assert(file_options_);
|
||||
assert(blob_file.file_checksum.empty() ==
|
||||
blob_file.file_checksum_func_name.empty());
|
||||
|
||||
FileOptions file_options = *file_options_;
|
||||
if (!blob_file.file_checksum.empty()) {
|
||||
file_options.file_checksum = blob_file.file_checksum.ToString();
|
||||
file_options.file_checksum_func_name =
|
||||
blob_file.file_checksum_func_name.ToString();
|
||||
} else {
|
||||
MutexLock checksum_lock(&blob_file_checksums_mutex_);
|
||||
const auto it = blob_file_checksums_.find(blob_file.file_number);
|
||||
if (it != blob_file_checksums_.end()) {
|
||||
file_options.file_checksum = it->second.value;
|
||||
file_options.file_checksum_func_name = it->second.function_name;
|
||||
} else {
|
||||
file_options.file_checksum.clear();
|
||||
file_options.file_checksum_func_name.clear();
|
||||
}
|
||||
}
|
||||
|
||||
Status s = BlobFileReader::Create(
|
||||
*immutable_options_, read_options, *file_options_, column_family_id_,
|
||||
blob_file_read_hist_, blob_file_number, io_tracer_,
|
||||
*immutable_options_, read_options, file_options, column_family_id_,
|
||||
blob_file_read_hist_, blob_file.file_number, io_tracer_,
|
||||
/*skip_footer_validation=*/false, blob_file_reader);
|
||||
if (!s.ok() && s.IsCorruption() && allow_footer_skip_retry) {
|
||||
blob_file_reader->reset();
|
||||
s = BlobFileReader::Create(
|
||||
*immutable_options_, read_options, *file_options_, column_family_id_,
|
||||
blob_file_read_hist_, blob_file_number, io_tracer_,
|
||||
*immutable_options_, read_options, file_options, column_family_id_,
|
||||
blob_file_read_hist_, blob_file.file_number, io_tracer_,
|
||||
/*skip_footer_validation=*/true, blob_file_reader);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
RecordTick(statistics, NO_FILE_ERRORS);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -178,7 +226,7 @@ Status BlobFileCache::InsertBlobFileReader(
|
||||
}
|
||||
|
||||
Status BlobFileCache::RefreshBlobFileReader(
|
||||
uint64_t blob_file_number,
|
||||
const BlobFileOpenInfo& blob_file,
|
||||
std::unique_ptr<BlobFileReader>* blob_file_reader,
|
||||
CacheHandleGuard<BlobFileReader>* cached_blob_file_reader) {
|
||||
assert(blob_file_reader);
|
||||
@@ -186,10 +234,26 @@ Status BlobFileCache::RefreshBlobFileReader(
|
||||
assert(cached_blob_file_reader);
|
||||
assert(cached_blob_file_reader->IsEmpty());
|
||||
|
||||
const Slice key = GetSliceForKey(&blob_file_number);
|
||||
const Slice key = GetSliceForKey(&blob_file.file_number);
|
||||
MutexLock lock(&mutex_.Get(key));
|
||||
|
||||
TypedHandle* handle = cache_.Lookup(key);
|
||||
|
||||
if (blob_file.file_checksum.empty()) {
|
||||
MutexLock checksum_lock(&blob_file_checksums_mutex_);
|
||||
if (blob_file_checksums_.find(blob_file.file_number) !=
|
||||
blob_file_checksums_.end()) {
|
||||
// This reader can have been opened just before finalization registered
|
||||
// checksum metadata. Never let such a racing refresh repopulate the
|
||||
// cache.
|
||||
if (handle) {
|
||||
*cached_blob_file_reader = cache_.Guard(handle);
|
||||
}
|
||||
blob_file_reader->reset();
|
||||
return Status::OK();
|
||||
}
|
||||
}
|
||||
|
||||
if (handle) {
|
||||
BlobFileReader* const cached_reader = cache_.Value(handle);
|
||||
assert(cached_reader != nullptr);
|
||||
|
||||
@@ -6,9 +6,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <cinttypes>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "cache/typed_cache.h"
|
||||
#include "db/blob/blob_file_reader.h"
|
||||
#include "db/blob/blob_read_request.h"
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
#include "util/mutexlock.h"
|
||||
|
||||
@@ -31,21 +34,35 @@ class BlobFileCache {
|
||||
|
||||
BlobFileCache(const BlobFileCache&) = delete;
|
||||
BlobFileCache& operator=(const BlobFileCache&) = delete;
|
||||
BlobFileCache(BlobFileCache&&) = delete;
|
||||
BlobFileCache& operator=(BlobFileCache&&) = delete;
|
||||
|
||||
// Returns a cached reader for `blob_file_number`, opening and caching it on
|
||||
// miss. If `allow_footer_skip_retry` is true, a footer-validation corruption
|
||||
// retries once without requiring a footer.
|
||||
~BlobFileCache() = default;
|
||||
|
||||
// Returns a cached reader for `blob_file`, opening and caching it on miss.
|
||||
// Checksum metadata is forwarded to the FileSystem through FileOptions when
|
||||
// the file is opened. If `allow_footer_skip_retry` is true, a
|
||||
// footer-validation corruption retries once without requiring a footer.
|
||||
Status GetBlobFileReader(const ReadOptions& read_options,
|
||||
uint64_t blob_file_number,
|
||||
const BlobFileOpenInfo& blob_file,
|
||||
CacheHandleGuard<BlobFileReader>* blob_file_reader,
|
||||
bool allow_footer_skip_retry = false);
|
||||
|
||||
// Opens a blob file reader without inserting it into the cache.
|
||||
Status OpenBlobFileReaderUncached(
|
||||
const ReadOptions& read_options, uint64_t blob_file_number,
|
||||
const ReadOptions& read_options, const BlobFileOpenInfo& blob_file,
|
||||
std::unique_ptr<BlobFileReader>* blob_file_reader,
|
||||
bool allow_footer_skip_retry = false);
|
||||
|
||||
// Makes finalized checksum metadata available to direct-write fallback reads
|
||||
// whose pinned Version predates the blob-file addition. Any cached
|
||||
// footer-less reader is evicted as part of the same per-file transition.
|
||||
void RegisterBlobFileChecksum(const BlobFileOpenInfo& blob_file);
|
||||
|
||||
// Stops retaining checksum metadata once no direct-write fallback can refer
|
||||
// to the file. Manifest-backed callers continue to provide it directly.
|
||||
void UnregisterBlobFileChecksum(uint64_t blob_file_number);
|
||||
|
||||
// Inserts a freshly opened uncached reader unless another thread already
|
||||
// cached the same blob file.
|
||||
Status InsertBlobFileReader(
|
||||
@@ -58,7 +75,7 @@ class BlobFileCache {
|
||||
// reader instead so a racing refresh cannot reintroduce an older active-file
|
||||
// view after the blob file grows.
|
||||
Status RefreshBlobFileReader(
|
||||
uint64_t blob_file_number,
|
||||
const BlobFileOpenInfo& blob_file,
|
||||
std::unique_ptr<BlobFileReader>* blob_file_reader,
|
||||
CacheHandleGuard<BlobFileReader>* cached_blob_file_reader);
|
||||
|
||||
@@ -72,6 +89,11 @@ class BlobFileCache {
|
||||
}
|
||||
|
||||
private:
|
||||
Status OpenBlobFileReader(const ReadOptions& read_options,
|
||||
const BlobFileOpenInfo& blob_file,
|
||||
bool allow_footer_skip_retry,
|
||||
std::unique_ptr<BlobFileReader>* blob_file_reader);
|
||||
|
||||
using CacheInterface =
|
||||
BasicTypedCacheInterface<BlobFileReader, CacheEntryRole::kMisc>;
|
||||
using TypedHandle = CacheInterface::TypedHandle;
|
||||
@@ -85,6 +107,13 @@ class BlobFileCache {
|
||||
HistogramImpl* blob_file_read_hist_;
|
||||
std::shared_ptr<IOTracer> io_tracer_;
|
||||
|
||||
struct BlobFileChecksum {
|
||||
std::string value;
|
||||
std::string function_name;
|
||||
};
|
||||
std::unordered_map<uint64_t, BlobFileChecksum> blob_file_checksums_;
|
||||
mutable port::Mutex blob_file_checksums_mutex_;
|
||||
|
||||
static constexpr size_t kNumberOfMutexStripes = 1 << 7;
|
||||
};
|
||||
|
||||
|
||||
@@ -115,13 +115,13 @@ TEST_F(BlobFileCacheTest, GetBlobFileReader) {
|
||||
BlobFileCache blob_file_cache(backing_cache.get(), &immutable_options,
|
||||
&file_options, column_family_id,
|
||||
blob_file_read_hist, nullptr /*IOTracer*/);
|
||||
const BlobFileOpenInfo blob_file{blob_file_number, Slice(), Slice()};
|
||||
|
||||
// First try: reader should be opened and put in cache
|
||||
CacheHandleGuard<BlobFileReader> first;
|
||||
|
||||
const ReadOptions read_options;
|
||||
ASSERT_OK(blob_file_cache.GetBlobFileReader(read_options, blob_file_number,
|
||||
&first));
|
||||
ASSERT_OK(blob_file_cache.GetBlobFileReader(read_options, blob_file, &first));
|
||||
ASSERT_NE(first.GetValue(), nullptr);
|
||||
ASSERT_EQ(options.statistics->getTickerCount(NO_FILE_OPENS), 1);
|
||||
ASSERT_EQ(options.statistics->getTickerCount(NO_FILE_ERRORS), 0);
|
||||
@@ -129,8 +129,8 @@ TEST_F(BlobFileCacheTest, GetBlobFileReader) {
|
||||
// Second try: reader should be served from cache
|
||||
CacheHandleGuard<BlobFileReader> second;
|
||||
|
||||
ASSERT_OK(blob_file_cache.GetBlobFileReader(read_options, blob_file_number,
|
||||
&second));
|
||||
ASSERT_OK(
|
||||
blob_file_cache.GetBlobFileReader(read_options, blob_file, &second));
|
||||
ASSERT_NE(second.GetValue(), nullptr);
|
||||
ASSERT_EQ(options.statistics->getTickerCount(NO_FILE_OPENS), 1);
|
||||
ASSERT_EQ(options.statistics->getTickerCount(NO_FILE_ERRORS), 0);
|
||||
@@ -163,6 +163,7 @@ TEST_F(BlobFileCacheTest, GetBlobFileReader_Race) {
|
||||
BlobFileCache blob_file_cache(backing_cache.get(), &immutable_options,
|
||||
&file_options, column_family_id,
|
||||
blob_file_read_hist, nullptr /*IOTracer*/);
|
||||
const BlobFileOpenInfo blob_file{blob_file_number, Slice(), Slice()};
|
||||
|
||||
CacheHandleGuard<BlobFileReader> first;
|
||||
CacheHandleGuard<BlobFileReader> second;
|
||||
@@ -172,16 +173,15 @@ TEST_F(BlobFileCacheTest, GetBlobFileReader_Race) {
|
||||
"BlobFileCache::GetBlobFileReader:DoubleCheck", [&](void* /* arg */) {
|
||||
// Disabling sync points to prevent infinite recursion
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
ASSERT_OK(blob_file_cache.GetBlobFileReader(read_options,
|
||||
blob_file_number, &second));
|
||||
ASSERT_OK(blob_file_cache.GetBlobFileReader(read_options, blob_file,
|
||||
&second));
|
||||
ASSERT_NE(second.GetValue(), nullptr);
|
||||
ASSERT_EQ(options.statistics->getTickerCount(NO_FILE_OPENS), 1);
|
||||
ASSERT_EQ(options.statistics->getTickerCount(NO_FILE_ERRORS), 0);
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ASSERT_OK(blob_file_cache.GetBlobFileReader(read_options, blob_file_number,
|
||||
&first));
|
||||
ASSERT_OK(blob_file_cache.GetBlobFileReader(read_options, blob_file, &first));
|
||||
ASSERT_NE(first.GetValue(), nullptr);
|
||||
ASSERT_EQ(options.statistics->getTickerCount(NO_FILE_OPENS), 1);
|
||||
ASSERT_EQ(options.statistics->getTickerCount(NO_FILE_ERRORS), 0);
|
||||
@@ -245,21 +245,22 @@ TEST_F(BlobFileCacheTest, RefreshBlobFileReaderPrefersLargestObservedFileSize) {
|
||||
BlobFileCache blob_file_cache(backing_cache.get(), &immutable_options,
|
||||
&file_options, column_family_id,
|
||||
blob_file_read_hist, nullptr /*IOTracer*/);
|
||||
const BlobFileOpenInfo blob_file{blob_file_number, Slice(), Slice()};
|
||||
|
||||
const ReadOptions read_options;
|
||||
std::unique_ptr<BlobFileReader> stale_reader;
|
||||
ASSERT_OK(blob_file_cache.OpenBlobFileReaderUncached(
|
||||
read_options, blob_file_number, &stale_reader,
|
||||
read_options, blob_file, &stale_reader,
|
||||
/*allow_footer_skip_retry=*/true));
|
||||
|
||||
std::unique_ptr<BlobFileReader> initial_cached_reader;
|
||||
ASSERT_OK(blob_file_cache.OpenBlobFileReaderUncached(
|
||||
read_options, blob_file_number, &initial_cached_reader,
|
||||
read_options, blob_file, &initial_cached_reader,
|
||||
/*allow_footer_skip_retry=*/true));
|
||||
|
||||
CacheHandleGuard<BlobFileReader> cached_reader;
|
||||
ASSERT_OK(blob_file_cache.RefreshBlobFileReader(
|
||||
blob_file_number, &initial_cached_reader, &cached_reader));
|
||||
blob_file, &initial_cached_reader, &cached_reader));
|
||||
ASSERT_NE(cached_reader.GetValue(), nullptr);
|
||||
const uint64_t initial_file_size = cached_reader.GetValue()->GetFileSize();
|
||||
|
||||
@@ -269,20 +270,20 @@ TEST_F(BlobFileCacheTest, RefreshBlobFileReaderPrefersLargestObservedFileSize) {
|
||||
|
||||
std::unique_ptr<BlobFileReader> fresh_reader;
|
||||
ASSERT_OK(blob_file_cache.OpenBlobFileReaderUncached(
|
||||
read_options, blob_file_number, &fresh_reader,
|
||||
read_options, blob_file, &fresh_reader,
|
||||
/*allow_footer_skip_retry=*/true));
|
||||
ASSERT_GT(fresh_reader->GetFileSize(), initial_file_size);
|
||||
|
||||
CacheHandleGuard<BlobFileReader> refreshed_reader;
|
||||
ASSERT_OK(blob_file_cache.RefreshBlobFileReader(
|
||||
blob_file_number, &fresh_reader, &refreshed_reader));
|
||||
ASSERT_OK(blob_file_cache.RefreshBlobFileReader(blob_file, &fresh_reader,
|
||||
&refreshed_reader));
|
||||
ASSERT_NE(refreshed_reader.GetValue(), nullptr);
|
||||
ASSERT_GT(refreshed_reader.GetValue()->GetFileSize(), initial_file_size);
|
||||
BlobFileReader* const largest_reader = refreshed_reader.GetValue();
|
||||
|
||||
CacheHandleGuard<BlobFileReader> preserved_reader;
|
||||
ASSERT_OK(blob_file_cache.RefreshBlobFileReader(
|
||||
blob_file_number, &stale_reader, &preserved_reader));
|
||||
ASSERT_OK(blob_file_cache.RefreshBlobFileReader(blob_file, &stale_reader,
|
||||
&preserved_reader));
|
||||
ASSERT_NE(preserved_reader.GetValue(), nullptr);
|
||||
ASSERT_EQ(preserved_reader.GetValue(), largest_reader);
|
||||
ASSERT_EQ(stale_reader.get(), nullptr);
|
||||
@@ -312,12 +313,13 @@ TEST_F(BlobFileCacheTest, GetBlobFileReader_IOError) {
|
||||
|
||||
// Note: there is no blob file with the below number
|
||||
constexpr uint64_t blob_file_number = 123;
|
||||
const BlobFileOpenInfo blob_file{blob_file_number, Slice(), Slice()};
|
||||
|
||||
CacheHandleGuard<BlobFileReader> reader;
|
||||
|
||||
const ReadOptions read_options;
|
||||
ASSERT_TRUE(
|
||||
blob_file_cache.GetBlobFileReader(read_options, blob_file_number, &reader)
|
||||
blob_file_cache.GetBlobFileReader(read_options, blob_file, &reader)
|
||||
.IsIOError());
|
||||
ASSERT_EQ(reader.GetValue(), nullptr);
|
||||
ASSERT_EQ(options.statistics->getTickerCount(NO_FILE_OPENS), 1);
|
||||
@@ -352,6 +354,7 @@ TEST_F(BlobFileCacheTest, GetBlobFileReader_CacheFull) {
|
||||
BlobFileCache blob_file_cache(backing_cache.get(), &immutable_options,
|
||||
&file_options, column_family_id,
|
||||
blob_file_read_hist, nullptr /*IOTracer*/);
|
||||
const BlobFileOpenInfo blob_file{blob_file_number, Slice(), Slice()};
|
||||
|
||||
// Insert into cache should fail since it has zero capacity and
|
||||
// strict_capacity_limit is set
|
||||
@@ -359,7 +362,7 @@ TEST_F(BlobFileCacheTest, GetBlobFileReader_CacheFull) {
|
||||
|
||||
const ReadOptions read_options;
|
||||
ASSERT_TRUE(
|
||||
blob_file_cache.GetBlobFileReader(read_options, blob_file_number, &reader)
|
||||
blob_file_cache.GetBlobFileReader(read_options, blob_file, &reader)
|
||||
.IsMemoryLimit());
|
||||
ASSERT_EQ(reader.GetValue(), nullptr);
|
||||
ASSERT_EQ(options.statistics->getTickerCount(NO_FILE_OPENS), 1);
|
||||
|
||||
@@ -52,6 +52,8 @@ class SharedBlobFileMetaData {
|
||||
SharedBlobFileMetaData(SharedBlobFileMetaData&&) = delete;
|
||||
SharedBlobFileMetaData& operator=(SharedBlobFileMetaData&&) = delete;
|
||||
|
||||
~SharedBlobFileMetaData() = default;
|
||||
|
||||
uint64_t GetBlobFileSize() const;
|
||||
uint64_t GetBlobFileNumber() const { return blob_file_number_; }
|
||||
uint64_t GetTotalBlobCount() const { return total_blob_count_; }
|
||||
@@ -110,6 +112,8 @@ class BlobFileMetaData {
|
||||
BlobFileMetaData(BlobFileMetaData&&) = delete;
|
||||
BlobFileMetaData& operator=(BlobFileMetaData&&) = delete;
|
||||
|
||||
~BlobFileMetaData() = default;
|
||||
|
||||
const std::shared_ptr<SharedBlobFileMetaData>& GetSharedMeta() const {
|
||||
return shared_meta_;
|
||||
}
|
||||
|
||||
@@ -330,6 +330,11 @@ Status BlobFilePartitionManager::FinalizeBlobFile(
|
||||
*addition =
|
||||
BlobFileAddition(file_number, blob_count, total_blob_bytes,
|
||||
std::move(checksum_method), std::move(checksum_value));
|
||||
if (blob_file_cache_ != nullptr) {
|
||||
const BlobFileOpenInfo blob_file{file_number, addition->GetChecksumValue(),
|
||||
addition->GetChecksumMethod()};
|
||||
blob_file_cache_->RegisterBlobFileChecksum(blob_file);
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
@@ -538,10 +543,11 @@ Status BlobFilePartitionManager::WriteBlob(
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
void BlobFilePartitionManager::RotateCurrentGeneration() {
|
||||
void BlobFilePartitionManager::RotateCurrentGeneration(uint64_t memtable_id) {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
GenerationBatch batch;
|
||||
batch.memtable_id = memtable_id;
|
||||
batch.sealed_files = std::move(current_generation_sealed_files_);
|
||||
current_generation_sealed_files_.clear();
|
||||
|
||||
@@ -564,8 +570,19 @@ void BlobFilePartitionManager::RotateCurrentGeneration() {
|
||||
pending_generations_.emplace_back(std::move(batch));
|
||||
}
|
||||
|
||||
BlobFilePartitionManager::GenerationBatch*
|
||||
BlobFilePartitionManager::FindPendingGeneration(uint64_t memtable_id) {
|
||||
for (auto& batch : pending_generations_) {
|
||||
if (batch.memtable_id == memtable_id) {
|
||||
return &batch;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Status BlobFilePartitionManager::PrepareFlushAdditions(
|
||||
const WriteOptions& write_options, size_t num_generations,
|
||||
const WriteOptions& write_options,
|
||||
const std::vector<uint64_t>& memtable_ids,
|
||||
std::vector<BlobFileAddition>* additions,
|
||||
std::vector<BlobFileGarbage>* garbages,
|
||||
std::vector<std::vector<uint64_t>>* generation_blob_file_numbers) {
|
||||
@@ -578,13 +595,13 @@ Status BlobFilePartitionManager::PrepareFlushAdditions(
|
||||
}
|
||||
|
||||
MutexLock lock(&mutex_);
|
||||
if (num_generations > pending_generations_.size()) {
|
||||
return Status::Corruption(
|
||||
"Missing blob direct write generation metadata for flush");
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < num_generations; ++i) {
|
||||
GenerationBatch& batch = pending_generations_[i];
|
||||
for (const uint64_t memtable_id : memtable_ids) {
|
||||
GenerationBatch* const batch_ptr = FindPendingGeneration(memtable_id);
|
||||
if (batch_ptr == nullptr) {
|
||||
return Status::Corruption(
|
||||
"Missing blob direct write generation metadata for flush");
|
||||
}
|
||||
GenerationBatch& batch = *batch_ptr;
|
||||
while (!batch.deferred_files.empty()) {
|
||||
DeferredFile deferred = std::move(batch.deferred_files.front());
|
||||
batch.deferred_files.pop_front();
|
||||
@@ -663,12 +680,22 @@ Status BlobFilePartitionManager::MarkBlobWriteAsGarbage(uint64_t file_number,
|
||||
return Status::Corruption(message);
|
||||
}
|
||||
|
||||
void BlobFilePartitionManager::CommitPreparedGenerations(
|
||||
size_t num_generations) {
|
||||
MutexLock lock(&mutex_);
|
||||
while (num_generations-- > 0 && !pending_generations_.empty()) {
|
||||
pending_generations_.pop_front();
|
||||
void BlobFilePartitionManager::CommitFlushedGenerations(
|
||||
uint64_t earliest_unflushed_memtable_id) {
|
||||
std::vector<uint64_t> sealed_file_numbers;
|
||||
{
|
||||
MutexLock lock(&mutex_);
|
||||
while (!pending_generations_.empty() &&
|
||||
pending_generations_.front().memtable_id <
|
||||
earliest_unflushed_memtable_id) {
|
||||
for (const auto& sealed_file :
|
||||
pending_generations_.front().sealed_files) {
|
||||
sealed_file_numbers.push_back(sealed_file.addition.GetBlobFileNumber());
|
||||
}
|
||||
pending_generations_.pop_front();
|
||||
}
|
||||
}
|
||||
RemoveFilePartitionMappings(sealed_file_numbers);
|
||||
}
|
||||
|
||||
Status BlobFilePartitionManager::SyncAllOpenFiles(
|
||||
@@ -753,6 +780,9 @@ void BlobFilePartitionManager::UnprotectSealedBlobFileNumbers(
|
||||
// manifest-visible path. Evict it here so delayed protection does not
|
||||
// leave an obsolete blob reader behind until DB close.
|
||||
blob_file_cache_->Evict(file_number);
|
||||
if (file_to_partition_.find(file_number) == file_to_partition_.end()) {
|
||||
blob_file_cache_->UnregisterBlobFileChecksum(file_number);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -772,6 +802,10 @@ void BlobFilePartitionManager::RemoveFilePartitionMappings(
|
||||
// this file before it was sealed. Drop it now so future manifest-visible
|
||||
// reads reopen against the finalized on-disk size and footer state.
|
||||
blob_file_cache_->Evict(file_number);
|
||||
if (protected_blob_file_refs_.find(file_number) ==
|
||||
protected_blob_file_refs_.end()) {
|
||||
blob_file_cache_->UnregisterBlobFileChecksum(file_number);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -809,9 +843,9 @@ Status BlobFilePartitionManager::ResolveBlobDirectWriteIndex(
|
||||
}
|
||||
|
||||
Status s;
|
||||
const BlobFileOpenInfo blob_file{blob_idx.file_number(), Slice(), Slice()};
|
||||
CacheHandleGuard<BlobFileReader> reader;
|
||||
s = blob_file_cache->GetBlobFileReader(read_options, blob_idx.file_number(),
|
||||
&reader,
|
||||
s = blob_file_cache->GetBlobFileReader(read_options, blob_file, &reader,
|
||||
/*allow_footer_skip_retry=*/true);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
@@ -836,7 +870,7 @@ Status BlobFilePartitionManager::ResolveBlobDirectWriteIndex(
|
||||
|
||||
std::unique_ptr<BlobFileReader> fresh_reader;
|
||||
s = blob_file_cache->OpenBlobFileReaderUncached(
|
||||
read_options, blob_idx.file_number(), &fresh_reader,
|
||||
read_options, blob_file, &fresh_reader,
|
||||
/*allow_footer_skip_retry=*/true);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
@@ -850,8 +884,7 @@ Status BlobFilePartitionManager::ResolveBlobDirectWriteIndex(
|
||||
if (s.ok()) {
|
||||
blob_value->PinSelf(fresh_contents->data());
|
||||
CacheHandleGuard<BlobFileReader> ignored;
|
||||
blob_file_cache
|
||||
->RefreshBlobFileReader(blob_idx.file_number(), &fresh_reader, &ignored)
|
||||
blob_file_cache->RefreshBlobFileReader(blob_file, &fresh_reader, &ignored)
|
||||
.PermitUncheckedError();
|
||||
}
|
||||
|
||||
|
||||
@@ -88,27 +88,31 @@ class BlobFilePartitionManager {
|
||||
const Slice& key,
|
||||
const WideColumns& columns) const;
|
||||
|
||||
// Move the current active partition files into the next immutable
|
||||
// memtable-generation batch. Called from SwitchMemtable() while DB mutex is
|
||||
// held. No I/O is performed here.
|
||||
void RotateCurrentGeneration();
|
||||
// Move the current active partition files into the generation of the
|
||||
// memtable `memtable_id`, which just became immutable. Called from
|
||||
// SwitchMemtable() while DB mutex is held. No I/O is performed here.
|
||||
void RotateCurrentGeneration(uint64_t memtable_id);
|
||||
|
||||
// Seal the first `num_generations` queued immutable generations and return
|
||||
// all blob additions and initial-garbage updates that must be registered
|
||||
// with the matching flush. Generations stay queued until
|
||||
// CommitPreparedGenerations() is called. If `generation_blob_file_numbers`
|
||||
// is non-null, it receives the sealed blob file numbers for each prepared
|
||||
// generation in the same FIFO order.
|
||||
// Seal the queued generations of the memtables `memtable_ids` and return all
|
||||
// blob additions and initial-garbage updates that must be registered with
|
||||
// the flush of those memtables. Concurrent flushes pick different
|
||||
// memtables, so generations are matched by memtable ID, not by queue
|
||||
// position. Generations stay queued until CommitFlushedGenerations() removes
|
||||
// them. If `generation_blob_file_numbers` is non-null, it receives the
|
||||
// sealed blob file numbers for each memtable, in `memtable_ids` order.
|
||||
Status PrepareFlushAdditions(const WriteOptions& write_options,
|
||||
size_t num_generations,
|
||||
const std::vector<uint64_t>& memtable_ids,
|
||||
std::vector<BlobFileAddition>* additions,
|
||||
std::vector<BlobFileGarbage>* garbages,
|
||||
std::vector<std::vector<uint64_t>>*
|
||||
generation_blob_file_numbers = nullptr);
|
||||
|
||||
// Remove the first `num_generations` prepared immutable generations after
|
||||
// their blob-file additions and garbage edits were committed to MANIFEST.
|
||||
void CommitPreparedGenerations(size_t num_generations);
|
||||
// Remove the generations of the memtables older than
|
||||
// `earliest_unflushed_memtable_id`, whose flush results are installed, and
|
||||
// stop tracking their sealed files as manager-owned. A flush that finishes
|
||||
// before an older one is installed with it, or rolled back if it fails, so
|
||||
// the flush that prepared a generation cannot remove it on its own.
|
||||
void CommitFlushedGenerations(uint64_t earliest_unflushed_memtable_id);
|
||||
|
||||
// In v1 flush-on-write mode each AddRecord flushes to the OS immediately, so
|
||||
// there is nothing extra to do for the non-sync write path.
|
||||
@@ -200,6 +204,8 @@ class BlobFilePartitionManager {
|
||||
};
|
||||
|
||||
struct GenerationBatch {
|
||||
// The memtable that became immutable when this generation was rotated.
|
||||
uint64_t memtable_id = 0;
|
||||
// Files that were still open when the memtable became immutable.
|
||||
std::deque<DeferredFile> deferred_files;
|
||||
// Files already sealed for this memtable generation.
|
||||
@@ -208,6 +214,8 @@ class BlobFilePartitionManager {
|
||||
|
||||
// Clears all active-file bookkeeping from a partition after sealing.
|
||||
void ResetPartitionState(Partition* partition);
|
||||
// Returns the queued generation of `memtable_id`, or null.
|
||||
GenerationBatch* FindPendingGeneration(uint64_t memtable_id);
|
||||
// Marks a blob file as manager-owned until it is committed or abandoned.
|
||||
void AddFilePartitionMapping(uint64_t file_number, uint32_t partition_idx);
|
||||
// Stops tracking a blob file after open/seal failure.
|
||||
|
||||
+211
-14
@@ -278,25 +278,31 @@ Status BlobFileReader::ReadFromFile(const RandomAccessFileReader* file_reader,
|
||||
return s;
|
||||
}
|
||||
|
||||
FSReadRequest read_req;
|
||||
read_req.offset = read_offset;
|
||||
read_req.len = read_size;
|
||||
if (file_reader->use_direct_io()) {
|
||||
constexpr char* scratch = nullptr;
|
||||
read_req.scratch = nullptr;
|
||||
AlignedBufferAllocationContext direct_io_context{direct_io_buffer};
|
||||
|
||||
s = file_reader->Read(io_options, read_offset, read_size, slice, scratch,
|
||||
&direct_io_context, &dbg);
|
||||
file_reader->Read(io_options, &read_req, &direct_io_context, &dbg);
|
||||
} else {
|
||||
buf->reset(new char[read_size]);
|
||||
|
||||
s = file_reader->Read(io_options, read_offset, read_size, slice, buf->get(),
|
||||
nullptr, &dbg);
|
||||
read_req.scratch = buf->get();
|
||||
file_reader->Read(io_options, &read_req, nullptr, &dbg);
|
||||
}
|
||||
s = std::move(read_req.status);
|
||||
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
*slice = read_req.result;
|
||||
|
||||
if (slice->size() != read_size) {
|
||||
return Status::Corruption("Failed to read data from blob file");
|
||||
return Status::Corruption("Incomplete blob read from " +
|
||||
file_reader->file_name() + " at offset " +
|
||||
std::to_string(read_offset) + ": expected " +
|
||||
std::to_string(read_size) + " bytes, got " +
|
||||
std::to_string(slice->size()));
|
||||
}
|
||||
|
||||
return Status::OK();
|
||||
@@ -330,7 +336,10 @@ Status BlobFileReader::GetBlob(
|
||||
|
||||
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_,
|
||||
has_footer_)) {
|
||||
return Status::Corruption("Invalid blob offset");
|
||||
return Status::Corruption(
|
||||
"Invalid blob offset " + std::to_string(offset) + " for value size " +
|
||||
std::to_string(value_size) + " in " + file_reader_->file_name() +
|
||||
" (file size " + std::to_string(file_size_) + ")");
|
||||
}
|
||||
|
||||
if (compression_type != compression_type_) {
|
||||
@@ -438,12 +447,18 @@ Status BlobFileReader::GetBlobRange(const ReadOptions& read_options,
|
||||
// performs); this guards the sub-range read below.
|
||||
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_,
|
||||
has_footer_)) {
|
||||
return Status::Corruption("Invalid blob offset");
|
||||
return Status::Corruption(
|
||||
"Invalid blob offset " + std::to_string(offset) + " for value size " +
|
||||
std::to_string(value_size) + " in " + file_reader_->file_name() +
|
||||
" (file size " + std::to_string(file_size_) + ")");
|
||||
}
|
||||
|
||||
// The requested sub-range must lie within the value.
|
||||
if (range_offset > value_size || range_length > value_size - range_offset) {
|
||||
return Status::InvalidArgument("Blob range out of bounds");
|
||||
return Status::InvalidArgument(
|
||||
"Blob range [" + std::to_string(range_offset) + ", +" +
|
||||
std::to_string(range_length) + ") out of bounds for value size " +
|
||||
std::to_string(value_size) + " in " + file_reader_->file_name());
|
||||
}
|
||||
|
||||
// Read only the requested bytes, at the value's file position plus the range
|
||||
@@ -518,7 +533,10 @@ void BlobFileReader::MultiGetBlob(
|
||||
|
||||
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_,
|
||||
has_footer_)) {
|
||||
*req->status = Status::Corruption("Invalid blob offset");
|
||||
*req->status = Status::Corruption(
|
||||
"Invalid blob offset " + std::to_string(offset) + " for value size " +
|
||||
std::to_string(value_size) + " in " + file_reader_->file_name() +
|
||||
" (file size " + std::to_string(file_size_) + ")");
|
||||
continue;
|
||||
}
|
||||
if (req->compression != compression_type_) {
|
||||
@@ -612,8 +630,11 @@ void BlobFileReader::MultiGetBlob(
|
||||
auto& read_req = read_reqs[j++];
|
||||
const auto& record_slice = read_req.result;
|
||||
if (read_req.status.ok() && record_slice.size() != read_req.len) {
|
||||
read_req.status =
|
||||
IOStatus::Corruption("Failed to read data from blob file");
|
||||
read_req.status = IOStatus::Corruption(
|
||||
"Incomplete blob read from " + file_reader_->file_name() +
|
||||
" at offset " + std::to_string(read_req.offset) + ": expected " +
|
||||
std::to_string(read_req.len) + " bytes, got " +
|
||||
std::to_string(record_slice.size()));
|
||||
}
|
||||
|
||||
*req->status = read_req.status;
|
||||
@@ -644,6 +665,182 @@ void BlobFileReader::MultiGetBlob(
|
||||
}
|
||||
}
|
||||
|
||||
void BlobFileReader::MultiGetBlobRange(
|
||||
const ReadOptions& read_options,
|
||||
autovector<std::pair<BlobRangeReadRequest*, std::unique_ptr<BlobContents>>>&
|
||||
blob_reqs,
|
||||
uint64_t* bytes_read) const {
|
||||
const size_t num_blobs = blob_reqs.size();
|
||||
assert(num_blobs > 0);
|
||||
assert(num_blobs <= MultiGetContext::MAX_BATCH_SIZE);
|
||||
|
||||
// Range reads are only issued for uncompressed blob references (a strict
|
||||
// sub-range of a compressed record cannot be decompressed in isolation).
|
||||
// Version::MultiGetBlobLazy rejects a compressed blob index before it reaches
|
||||
// here, so every request's blob index is uncompressed by contract; a
|
||||
// compressed blob file therefore means the blob index disagrees with the
|
||||
// file. Report that as the compression-type mismatch it is -- exactly like
|
||||
// the whole-value MultiGetBlob path -- rather than as a usage error. (Unlike
|
||||
// the single-read path, BlobSource has no per-request compression to check
|
||||
// for a range read, so this reader-level check is the range multi path's
|
||||
// compression backstop.)
|
||||
if (compression_type_ != kNoCompression) {
|
||||
for (auto& blob_req : blob_reqs) {
|
||||
assert(blob_req.first);
|
||||
assert(blob_req.first->status);
|
||||
*blob_req.first->status =
|
||||
Status::Corruption("Compression type mismatch when reading a blob");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
for (size_t i = 0; i < num_blobs - 1; ++i) {
|
||||
assert(blob_reqs[i].first->offset + blob_reqs[i].first->range_offset <=
|
||||
blob_reqs[i + 1].first->offset +
|
||||
blob_reqs[i + 1].first->range_offset);
|
||||
}
|
||||
#endif // !NDEBUG
|
||||
|
||||
std::vector<FSReadRequest> read_reqs;
|
||||
uint64_t total_len = 0;
|
||||
read_reqs.reserve(num_blobs);
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
BlobRangeReadRequest* const req = blob_reqs[i].first;
|
||||
assert(req);
|
||||
assert(req->status);
|
||||
|
||||
const size_t key_size = req->user_key.size();
|
||||
// Validate that the full value region is within the file (same check
|
||||
// GetBlob performs); this guards the sub-range read below.
|
||||
if (!IsValidBlobOffset(req->offset, key_size, req->value_size, file_size_,
|
||||
has_footer_)) {
|
||||
*req->status = Status::Corruption(
|
||||
"Invalid blob offset " + std::to_string(req->offset) +
|
||||
" for value size " + std::to_string(req->value_size) + " in " +
|
||||
file_reader_->file_name() + " (file size " +
|
||||
std::to_string(file_size_) + ")");
|
||||
continue;
|
||||
}
|
||||
// The requested sub-range must lie within the value.
|
||||
if (req->range_offset > req->value_size ||
|
||||
req->range_length > req->value_size - req->range_offset) {
|
||||
*req->status = Status::InvalidArgument(
|
||||
"Blob range [" + std::to_string(req->range_offset) + ", +" +
|
||||
std::to_string(req->range_length) +
|
||||
") out of bounds for value size " + std::to_string(req->value_size) +
|
||||
" in " + file_reader_->file_name());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read only the requested bytes, at the value's file position plus the
|
||||
// range offset -- no record-header adjustment, no whole-record checksum.
|
||||
FSReadRequest read_req;
|
||||
read_req.offset = req->offset + req->range_offset;
|
||||
read_req.len = req->range_length;
|
||||
total_len += read_req.len;
|
||||
read_reqs.emplace_back(std::move(read_req));
|
||||
}
|
||||
|
||||
RecordTick(statistics_, BLOB_DB_BLOB_FILE_BYTES_READ, total_len);
|
||||
|
||||
if (read_reqs.empty()) {
|
||||
if (bytes_read) {
|
||||
*bytes_read = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Buffer buf;
|
||||
AlignedBuffer direct_io_buffer;
|
||||
|
||||
Status s;
|
||||
bool direct_io = file_reader_->use_direct_io();
|
||||
if (direct_io) {
|
||||
for (size_t i = 0; i < read_reqs.size(); ++i) {
|
||||
read_reqs[i].scratch = nullptr;
|
||||
}
|
||||
} else {
|
||||
buf.reset(new char[total_len]);
|
||||
std::ptrdiff_t pos = 0;
|
||||
for (size_t i = 0; i < read_reqs.size(); ++i) {
|
||||
read_reqs[i].scratch = buf.get() + pos;
|
||||
pos += read_reqs[i].len;
|
||||
}
|
||||
}
|
||||
TEST_SYNC_POINT("BlobFileReader::MultiGetBlobRange:ReadFromFile");
|
||||
PERF_COUNTER_ADD(blob_read_count, num_blobs);
|
||||
PERF_COUNTER_ADD(blob_read_byte, total_len);
|
||||
IOOptions opts;
|
||||
IODebugContext dbg;
|
||||
s = file_reader_->PrepareIOOptions(read_options, opts, &dbg);
|
||||
if (s.ok()) {
|
||||
AlignedBufferAllocationContext direct_io_context{&direct_io_buffer};
|
||||
s = file_reader_->MultiRead(opts, read_reqs.data(), read_reqs.size(),
|
||||
&direct_io_context, &dbg);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
// Batch read failed; s is written to every request's status below, so these
|
||||
// per-request FS statuses carry nothing extra.
|
||||
for (auto& req : read_reqs) {
|
||||
req.status.PermitUncheckedError();
|
||||
}
|
||||
for (auto& blob_req : blob_reqs) {
|
||||
BlobRangeReadRequest* const req = blob_req.first;
|
||||
assert(req);
|
||||
assert(req->status);
|
||||
if (!req->status->IsCorruption() && !req->status->IsInvalidArgument()) {
|
||||
// Avoid overwriting per-request validation errors.
|
||||
*req->status = s;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
assert(s.ok());
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK("BlobFileReader::MultiGetBlobRange:TamperWithResult",
|
||||
&read_reqs);
|
||||
|
||||
uint64_t total_bytes = 0;
|
||||
for (size_t i = 0, j = 0; i < num_blobs; ++i) {
|
||||
BlobRangeReadRequest* const req = blob_reqs[i].first;
|
||||
assert(req);
|
||||
assert(req->status);
|
||||
|
||||
if (!req->status->ok()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
assert(j < read_reqs.size());
|
||||
auto& read_req = read_reqs[j++];
|
||||
const Slice& range_slice = read_req.result;
|
||||
if (read_req.status.ok() && range_slice.size() != read_req.len) {
|
||||
read_req.status = IOStatus::Corruption(
|
||||
"Incomplete blob range read from " + file_reader_->file_name() +
|
||||
" at offset " + std::to_string(read_req.offset) + ": expected " +
|
||||
std::to_string(read_req.len) + " bytes, got " +
|
||||
std::to_string(range_slice.size()));
|
||||
}
|
||||
|
||||
*req->status = read_req.status;
|
||||
if (!req->status->ok()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The blob is uncompressed, so the bytes read are exactly the requested
|
||||
// value bytes; copy them into an owned BlobContents (no decompression).
|
||||
BlobContentsCreator::Create(&blob_reqs[i].second, /*out_charge=*/nullptr,
|
||||
range_slice, kNoCompression,
|
||||
/*allocator=*/nullptr);
|
||||
total_bytes += range_slice.size();
|
||||
}
|
||||
|
||||
if (bytes_read) {
|
||||
*bytes_read = total_bytes;
|
||||
}
|
||||
}
|
||||
|
||||
Status BlobFileReader::VerifyBlob(const Slice& record_slice,
|
||||
const Slice& user_key, uint64_t value_size) {
|
||||
PERF_TIMER_GUARD(blob_checksum_time);
|
||||
|
||||
@@ -53,6 +53,8 @@ class BlobFileReader {
|
||||
|
||||
BlobFileReader(const BlobFileReader&) = delete;
|
||||
BlobFileReader& operator=(const BlobFileReader&) = delete;
|
||||
BlobFileReader(BlobFileReader&&) = delete;
|
||||
BlobFileReader& operator=(BlobFileReader&&) = delete;
|
||||
|
||||
~BlobFileReader();
|
||||
|
||||
@@ -88,6 +90,21 @@ class BlobFileReader {
|
||||
blob_reqs,
|
||||
uint64_t* bytes_read) const;
|
||||
|
||||
// Byte-range (partial) multi-read counterpart of MultiGetBlob: for each
|
||||
// request reads only its sub-range [offset + range_offset, +range_length) of
|
||||
// an *uncompressed* blob value, coalescing all requests into a single
|
||||
// MultiRead. Unlike MultiGetBlob it reads no record header/key, verifies no
|
||||
// whole-record checksum (a strict sub-range cannot cover it), and never
|
||||
// decompresses -- so it is only valid when this reader's blob file is
|
||||
// uncompressed (checked). Each result owns a copy of its requested bytes.
|
||||
// Requests must be sorted ascending by effective read offset (offset +
|
||||
// range_offset) by the caller.
|
||||
void MultiGetBlobRange(
|
||||
const ReadOptions& read_options,
|
||||
autovector<std::pair<BlobRangeReadRequest*,
|
||||
std::unique_ptr<BlobContents>>>& blob_reqs,
|
||||
uint64_t* bytes_read) const;
|
||||
|
||||
CompressionType GetCompressionType() const { return compression_type_; }
|
||||
|
||||
uint64_t GetFileSize() const { return file_size_; }
|
||||
|
||||
+296
-21
@@ -5,9 +5,11 @@
|
||||
|
||||
#include "db/blob/blob_gen2_format.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "file/random_access_file_reader.h"
|
||||
#include "file/writable_file_writer.h"
|
||||
@@ -16,9 +18,56 @@
|
||||
#include "table/format.h"
|
||||
#include "util/cast_util.h"
|
||||
#include "util/coding.h"
|
||||
#include "util/crc32c.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
namespace {
|
||||
|
||||
// Verifies a SimpleGen2Blob record's builtin checksum: the stored 4-byte
|
||||
// checksum (at record[payload_size + 1 ..], context-modified by the record's
|
||||
// file offset) against the checksum computed over the payload plus its 1-byte
|
||||
// compression marker. Returns OK, or a Corruption carrying debugging
|
||||
// information -- stored (with the per-offset context modifier removed),
|
||||
// computed, and checksum type, with CRC values unmasked so they can be compared
|
||||
// against a reference. Mirrors VerifyBlockChecksum() for the block-based
|
||||
// format, since a SimpleGen2Blob record's trailer uses the same
|
||||
// builtin-checksum scheme; shared by the scalar and batched readers so both
|
||||
// report the same diagnostics.
|
||||
Status VerifySimpleGen2BlobChecksum(ChecksumType checksum_type,
|
||||
uint32_t base_context_checksum,
|
||||
const char* record, size_t payload_size,
|
||||
uint64_t record_offset,
|
||||
const std::string& file_name) {
|
||||
uint32_t stored = DecodeFixed32(record + payload_size + 1);
|
||||
const uint32_t modifier =
|
||||
ChecksumModifierForContext(base_context_checksum, record_offset);
|
||||
stored -= modifier;
|
||||
const uint32_t computed = ComputeBuiltinChecksumWithLastByte(
|
||||
checksum_type, record, payload_size, record[payload_size]);
|
||||
if (stored == computed) {
|
||||
return Status::OK();
|
||||
}
|
||||
// Unmask CRC values (as VerifyBlockChecksum does) so a reader of the error
|
||||
// can compare against a reference checksum.
|
||||
uint32_t stored_for_msg = stored;
|
||||
uint32_t computed_for_msg = computed;
|
||||
if (checksum_type == kCRC32c) {
|
||||
stored_for_msg = crc32c::Unmask(stored);
|
||||
computed_for_msg = crc32c::Unmask(computed);
|
||||
}
|
||||
return Status::Corruption("SimpleGen2 blob record checksum mismatch: stored" +
|
||||
std::string(modifier ? "(context removed)" : "") +
|
||||
" = " + std::to_string(stored_for_msg) +
|
||||
", computed = " + std::to_string(computed_for_msg) +
|
||||
", type = " + std::to_string(checksum_type) +
|
||||
" in " + file_name + " offset " +
|
||||
std::to_string(record_offset) + " size " +
|
||||
std::to_string(payload_size));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Status ReadAndVerifySimpleGen2BlobRecord(
|
||||
const ReadOptions& read_options, RandomAccessFileReader* file,
|
||||
uint64_t record_offset, size_t payload_size, size_t record_size,
|
||||
@@ -33,14 +82,17 @@ Status ReadAndVerifySimpleGen2BlobRecord(
|
||||
IODebugContext dbg;
|
||||
Status s = file->PrepareIOOptions(read_options, opts, &dbg);
|
||||
if (s.ok()) {
|
||||
s = file->Read(opts, record_offset, record_size, &result, buf, nullptr,
|
||||
&dbg);
|
||||
s = file->Read(record_offset, record_size, opts, &result, buf, &dbg);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
if (result.size() != record_size) {
|
||||
return Status::Corruption("Could not read complete blob record");
|
||||
return Status::Corruption("Incomplete blob record read from " +
|
||||
file->file_name() + " at offset " +
|
||||
std::to_string(record_offset) + ": expected " +
|
||||
std::to_string(record_size) + " bytes, got " +
|
||||
std::to_string(result.size()));
|
||||
}
|
||||
// With mmap reads the data lands outside `buf`; copy it in so the caller can
|
||||
// rely on `buf` owning the bytes (this is the only copy on the mmap path).
|
||||
@@ -54,24 +106,32 @@ Status ReadAndVerifySimpleGen2BlobRecord(
|
||||
static_cast<CompressionType>(record[payload_size]);
|
||||
if (compression != expected_compression) {
|
||||
return Status::Corruption(
|
||||
"Blob record compression does not match blob index");
|
||||
"Blob record compression mismatch in " + file->file_name() +
|
||||
" at offset " + std::to_string(record_offset) + ": index expects " +
|
||||
std::to_string(static_cast<int>(expected_compression)) +
|
||||
", record has " + std::to_string(static_cast<int>(compression)));
|
||||
}
|
||||
|
||||
if (read_options.verify_checksums) {
|
||||
uint32_t stored = DecodeFixed32(record + payload_size + 1);
|
||||
stored -= ChecksumModifierForContext(base_context_checksum, record_offset);
|
||||
const uint32_t computed = ComputeBuiltinChecksumWithLastByte(
|
||||
checksum_type, record, payload_size, record[payload_size]);
|
||||
if (stored != computed) {
|
||||
return Status::Corruption("Blob record checksum mismatch in " +
|
||||
file->file_name() + " offset " +
|
||||
std::to_string(record_offset) + " size " +
|
||||
std::to_string(payload_size));
|
||||
const Status checksum_status = VerifySimpleGen2BlobChecksum(
|
||||
checksum_type, base_context_checksum, record, payload_size,
|
||||
record_offset, file->file_name());
|
||||
if (!checksum_status.ok()) {
|
||||
return checksum_status;
|
||||
}
|
||||
}
|
||||
|
||||
// The compression marker matched the blob index above, but SimpleGen2Blob
|
||||
// payloads are currently written uncompressed only (see
|
||||
// WriteSimpleGen2BlobRecord), so a record that actually carries a compression
|
||||
// type is not readable here. This guards a corrupt or forward-incompatible
|
||||
// record whose (compressed) marker happened to agree with a (compressed)
|
||||
// index.
|
||||
if (compression != kNoCompression) {
|
||||
return Status::Corruption("Blob record compression is not supported");
|
||||
return Status::Corruption("Blob record compression " +
|
||||
std::to_string(static_cast<int>(compression)) +
|
||||
" is not supported in " + file->file_name() +
|
||||
" at offset " + std::to_string(record_offset));
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
@@ -88,14 +148,21 @@ Status ReadSimpleGen2BlobRange(const ReadOptions& read_options,
|
||||
// A strict sub-range of a compressed payload cannot be decompressed in
|
||||
// isolation; callers resolve such payloads whole and slice instead.
|
||||
if (expected_compression != kNoCompression) {
|
||||
return Status::Corruption("Cannot range-read a compressed blob");
|
||||
return Status::Corruption(
|
||||
"Cannot range-read a compressed blob (compression " +
|
||||
std::to_string(static_cast<int>(expected_compression)) + ") in " +
|
||||
file->file_name() + " at offset " + std::to_string(record_offset));
|
||||
}
|
||||
|
||||
// The requested sub-range must lie within the payload (the trailer that
|
||||
// follows the payload is never part of a range read).
|
||||
if (range_offset > payload_size ||
|
||||
range_length > payload_size - range_offset) {
|
||||
return Status::InvalidArgument("Blob range out of bounds");
|
||||
return Status::InvalidArgument(
|
||||
"Blob range [" + std::to_string(range_offset) + ", +" +
|
||||
std::to_string(range_length) + ") out of bounds for payload size " +
|
||||
std::to_string(payload_size) + " in " + file->file_name() +
|
||||
" at offset " + std::to_string(record_offset));
|
||||
}
|
||||
|
||||
Slice result;
|
||||
@@ -103,14 +170,18 @@ Status ReadSimpleGen2BlobRange(const ReadOptions& read_options,
|
||||
IODebugContext dbg;
|
||||
Status s = file->PrepareIOOptions(read_options, opts, &dbg);
|
||||
if (s.ok()) {
|
||||
s = file->Read(opts, record_offset + range_offset, range_length, &result,
|
||||
buf, nullptr, &dbg);
|
||||
s = file->Read(record_offset + range_offset, range_length, opts, &result,
|
||||
buf, &dbg);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
if (result.size() != range_length) {
|
||||
return Status::Corruption("Could not read complete blob range");
|
||||
return Status::Corruption("Incomplete blob range read from " +
|
||||
file->file_name() + " at offset " +
|
||||
std::to_string(record_offset + range_offset) +
|
||||
": expected " + std::to_string(range_length) +
|
||||
" bytes, got " + std::to_string(result.size()));
|
||||
}
|
||||
// With mmap reads the data lands outside `buf`; copy it in so the caller can
|
||||
// rely on `buf` owning the bytes (the only copy on the mmap path).
|
||||
@@ -124,6 +195,210 @@ Status ReadSimpleGen2BlobRange(const ReadOptions& read_options,
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// One record's read description for the batched SimpleGen2Blob readers below:
|
||||
// where/how much to read, where to put it, and where to report the outcome.
|
||||
struct Gen2ReadSlot {
|
||||
uint64_t offset = 0;
|
||||
size_t len = 0;
|
||||
char* buf = nullptr;
|
||||
Status* status = nullptr;
|
||||
};
|
||||
|
||||
// Issues a single MultiRead for the records described by `slots` and copies
|
||||
// each result into its buf, setting *slots[i].status. Shared by the
|
||||
// whole-record and range batch readers; per-record format checks (compression
|
||||
// marker / checksum) are done by the whole-record caller afterwards from the
|
||||
// (now populated) buf.
|
||||
void MultiReadGen2(const ReadOptions& read_options,
|
||||
RandomAccessFileReader* file,
|
||||
std::vector<Gen2ReadSlot>& slots) {
|
||||
assert(file != nullptr);
|
||||
assert(!slots.empty());
|
||||
|
||||
// RandomAccessFileReader::MultiRead requires the requests sorted ascending by
|
||||
// file offset -- it asserts this, and in direct-I/O mode it aligns and merges
|
||||
// only consecutive requests, so unsorted input reads the wrong bytes. The
|
||||
// callers group records by SST but not by offset, so sort here. Each slot
|
||||
// carries its own output buf/status pointer, so sorting does not disturb
|
||||
// result routing.
|
||||
std::sort(slots.begin(), slots.end(),
|
||||
[](const Gen2ReadSlot& a, const Gen2ReadSlot& b) {
|
||||
return a.offset < b.offset;
|
||||
});
|
||||
|
||||
const size_t num = slots.size();
|
||||
std::vector<FSReadRequest> read_reqs;
|
||||
read_reqs.reserve(num);
|
||||
for (size_t i = 0; i < num; ++i) {
|
||||
FSReadRequest read_req;
|
||||
read_req.offset = slots[i].offset;
|
||||
read_req.len = slots[i].len;
|
||||
read_reqs.emplace_back(std::move(read_req));
|
||||
}
|
||||
|
||||
AlignedBuffer direct_io_buffer;
|
||||
const bool direct_io = file->use_direct_io();
|
||||
if (direct_io) {
|
||||
for (size_t i = 0; i < num; ++i) {
|
||||
read_reqs[i].scratch = nullptr;
|
||||
}
|
||||
} else {
|
||||
for (size_t i = 0; i < num; ++i) {
|
||||
read_reqs[i].scratch = slots[i].buf;
|
||||
}
|
||||
}
|
||||
|
||||
IOOptions opts;
|
||||
IODebugContext dbg;
|
||||
Status s = file->PrepareIOOptions(read_options, opts, &dbg);
|
||||
if (s.ok()) {
|
||||
AlignedBufferAllocationContext direct_io_context{&direct_io_buffer};
|
||||
s = file->MultiRead(opts, read_reqs.data(), read_reqs.size(),
|
||||
&direct_io_context, &dbg);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
// Batch read failed; s is written to every slot's status below, so these
|
||||
// per-request FS statuses carry nothing extra.
|
||||
for (auto& read_req : read_reqs) {
|
||||
read_req.status.PermitUncheckedError();
|
||||
}
|
||||
for (size_t i = 0; i < num; ++i) {
|
||||
*slots[i].status = s;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < num; ++i) {
|
||||
FSReadRequest& read_req = read_reqs[i];
|
||||
if (read_req.status.ok() && read_req.result.size() != read_req.len) {
|
||||
read_req.status = IOStatus::Corruption(
|
||||
"Incomplete blob record read from " + file->file_name() +
|
||||
" at offset " + std::to_string(read_req.offset) + ": expected " +
|
||||
std::to_string(read_req.len) + " bytes, got " +
|
||||
std::to_string(read_req.result.size()));
|
||||
}
|
||||
*slots[i].status = read_req.status;
|
||||
if (!slots[i].status->ok()) {
|
||||
continue;
|
||||
}
|
||||
// With mmap/direct reads the data lands outside the caller's buf; copy it
|
||||
// in so the caller can rely on buf owning the bytes.
|
||||
if (read_req.result.data() != slots[i].buf) {
|
||||
memcpy(slots[i].buf, read_req.result.data(), read_req.len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ReadAndVerifySimpleGen2BlobRecords(const ReadOptions& read_options,
|
||||
RandomAccessFileReader* file,
|
||||
ChecksumType checksum_type,
|
||||
uint32_t base_context_checksum,
|
||||
size_t num_records,
|
||||
SimpleGen2RecordReadRequest* reqs) {
|
||||
assert(file != nullptr);
|
||||
assert(num_records > 0);
|
||||
|
||||
std::vector<Gen2ReadSlot> slots;
|
||||
slots.reserve(num_records);
|
||||
for (size_t i = 0; i < num_records; ++i) {
|
||||
assert(reqs[i].buf != nullptr);
|
||||
assert(reqs[i].status != nullptr);
|
||||
assert(reqs[i].record_size ==
|
||||
reqs[i].payload_size + kSimpleGen2BlobTrailerSize);
|
||||
slots.push_back({reqs[i].record_offset, reqs[i].record_size, reqs[i].buf,
|
||||
reqs[i].status});
|
||||
}
|
||||
|
||||
MultiReadGen2(read_options, file, slots);
|
||||
|
||||
for (size_t i = 0; i < num_records; ++i) {
|
||||
if (!reqs[i].status->ok()) {
|
||||
continue;
|
||||
}
|
||||
const char* record = reqs[i].buf;
|
||||
const size_t payload_size = reqs[i].payload_size;
|
||||
const CompressionType compression =
|
||||
static_cast<CompressionType>(record[payload_size]);
|
||||
if (compression != reqs[i].expected_compression) {
|
||||
*reqs[i].status = Status::Corruption(
|
||||
"Blob record compression mismatch in " + file->file_name() +
|
||||
" at offset " + std::to_string(reqs[i].record_offset) +
|
||||
": index expects " +
|
||||
std::to_string(static_cast<int>(reqs[i].expected_compression)) +
|
||||
", record has " + std::to_string(static_cast<int>(compression)));
|
||||
continue;
|
||||
}
|
||||
if (read_options.verify_checksums) {
|
||||
*reqs[i].status = VerifySimpleGen2BlobChecksum(
|
||||
checksum_type, base_context_checksum, record, payload_size,
|
||||
reqs[i].record_offset, file->file_name());
|
||||
if (!reqs[i].status->ok()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// See ReadAndVerifySimpleGen2BlobRecord: the marker matched the index, but
|
||||
// only uncompressed SimpleGen2Blob payloads are currently supported.
|
||||
if (compression != kNoCompression) {
|
||||
*reqs[i].status = Status::Corruption(
|
||||
"Blob record compression " +
|
||||
std::to_string(static_cast<int>(compression)) +
|
||||
" is not supported in " + file->file_name() + " at offset " +
|
||||
std::to_string(reqs[i].record_offset));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ReadSimpleGen2BlobRanges(const ReadOptions& read_options,
|
||||
RandomAccessFileReader* file, size_t num_records,
|
||||
SimpleGen2RangeReadRequest* reqs) {
|
||||
assert(file != nullptr);
|
||||
assert(num_records > 0);
|
||||
|
||||
std::vector<Gen2ReadSlot> slots;
|
||||
slots.reserve(num_records);
|
||||
for (size_t i = 0; i < num_records; ++i) {
|
||||
assert(reqs[i].buf != nullptr);
|
||||
assert(reqs[i].status != nullptr);
|
||||
// A strict sub-range of a compressed payload cannot be decompressed in
|
||||
// isolation; callers resolve such payloads whole and slice instead.
|
||||
if (reqs[i].expected_compression != kNoCompression) {
|
||||
*reqs[i].status = Status::Corruption(
|
||||
"Cannot range-read a compressed blob (compression " +
|
||||
std::to_string(static_cast<int>(reqs[i].expected_compression)) +
|
||||
") in " + file->file_name() + " at offset " +
|
||||
std::to_string(reqs[i].record_offset));
|
||||
continue;
|
||||
}
|
||||
// The requested sub-range must lie within the payload (the trailer that
|
||||
// follows the payload is never part of a range read).
|
||||
if (reqs[i].range_offset > reqs[i].payload_size ||
|
||||
reqs[i].range_length > reqs[i].payload_size - reqs[i].range_offset) {
|
||||
*reqs[i].status = Status::InvalidArgument(
|
||||
"Blob range [" + std::to_string(reqs[i].range_offset) + ", +" +
|
||||
std::to_string(reqs[i].range_length) +
|
||||
") out of bounds for payload size " +
|
||||
std::to_string(reqs[i].payload_size) + " in " + file->file_name() +
|
||||
" at offset " + std::to_string(reqs[i].record_offset));
|
||||
continue;
|
||||
}
|
||||
slots.push_back({reqs[i].record_offset + reqs[i].range_offset,
|
||||
reqs[i].range_length, reqs[i].buf, reqs[i].status});
|
||||
}
|
||||
|
||||
if (slots.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// slots only covers requests that passed validation above; a failed
|
||||
// validation left *reqs[i].status non-OK and is skipped here.
|
||||
MultiReadGen2(read_options, file, slots);
|
||||
}
|
||||
|
||||
IOStatus WriteSimpleGen2BlobRecord(WritableFileWriter* file,
|
||||
const WriteOptions& write_options,
|
||||
ChecksumType checksum_type,
|
||||
@@ -148,12 +423,12 @@ IOStatus WriteSimpleGen2BlobRecord(WritableFileWriter* file,
|
||||
return io_s;
|
||||
}
|
||||
if (!payload.empty()) {
|
||||
io_s = file->Append(opts, payload);
|
||||
io_s = file->Append(payload, opts);
|
||||
if (!io_s.ok()) {
|
||||
return io_s;
|
||||
}
|
||||
}
|
||||
return file->Append(opts, Slice(trailer.data(), trailer.size()));
|
||||
return file->Append(Slice(trailer.data(), trailer.size()), opts);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -91,6 +91,57 @@ Status ReadSimpleGen2BlobRange(const ReadOptions& read_options,
|
||||
uint64_t range_offset, size_t range_length,
|
||||
CompressionType expected_compression, char* buf);
|
||||
|
||||
// One whole-record request in a batched SimpleGen2Blob read (see
|
||||
// ReadAndVerifySimpleGen2BlobRecords). Mirrors the scalar
|
||||
// ReadAndVerifySimpleGen2BlobRecord inputs, with the read bytes going into
|
||||
// caller-provided `buf` (capacity >= record_size) and the per-record outcome in
|
||||
// `*status`.
|
||||
struct SimpleGen2RecordReadRequest {
|
||||
uint64_t record_offset = 0;
|
||||
size_t payload_size = 0;
|
||||
size_t record_size = 0; // == payload_size + kSimpleGen2BlobTrailerSize
|
||||
CompressionType expected_compression = kNoCompression;
|
||||
char* buf = nullptr;
|
||||
Status* status = nullptr;
|
||||
};
|
||||
|
||||
// One sub-range request in a batched SimpleGen2Blob range read (see
|
||||
// ReadSimpleGen2BlobRanges). Mirrors the scalar ReadSimpleGen2BlobRange inputs.
|
||||
struct SimpleGen2RangeReadRequest {
|
||||
uint64_t record_offset = 0;
|
||||
size_t payload_size = 0;
|
||||
uint64_t range_offset = 0;
|
||||
size_t range_length = 0;
|
||||
CompressionType expected_compression = kNoCompression;
|
||||
char* buf = nullptr;
|
||||
Status* status = nullptr;
|
||||
};
|
||||
|
||||
// Batch counterpart of ReadAndVerifySimpleGen2BlobRecord: reads `num_records`
|
||||
// whole records from `file` in a single MultiRead, then per record verifies the
|
||||
// compression marker and (when read_options.verify_checksums) the
|
||||
// context-modified checksum, copying the record into reqs[i].buf. Per-record
|
||||
// outcome is written to *reqs[i].status; on success reqs[i].buf[0,
|
||||
// payload_size) holds the verified payload (the trailer sits unused at the
|
||||
// tail). Requests may be given in any order; they are sorted internally, as
|
||||
// RandomAccessFileReader::MultiRead requires ascending offsets.
|
||||
void ReadAndVerifySimpleGen2BlobRecords(const ReadOptions& read_options,
|
||||
RandomAccessFileReader* file,
|
||||
ChecksumType checksum_type,
|
||||
uint32_t base_context_checksum,
|
||||
size_t num_records,
|
||||
SimpleGen2RecordReadRequest* reqs);
|
||||
|
||||
// Batch counterpart of ReadSimpleGen2BlobRange: reads the requested sub-range
|
||||
// of `num_records` *uncompressed* payloads from `file` in a single MultiRead
|
||||
// (no trailer read, no checksum). Per-record outcome is in *reqs[i].status; on
|
||||
// success reqs[i].buf[0, range_length) holds the requested bytes. Requests may
|
||||
// be given in any order; they are sorted internally, as
|
||||
// RandomAccessFileReader::MultiRead requires ascending offsets.
|
||||
void ReadSimpleGen2BlobRanges(const ReadOptions& read_options,
|
||||
RandomAccessFileReader* file, size_t num_records,
|
||||
SimpleGen2RangeReadRequest* reqs);
|
||||
|
||||
// Writes a SimpleGen2Blob record for `payload` at the current end of `file`,
|
||||
// which the caller asserts is byte offset `record_offset`. Appends the payload
|
||||
// bytes followed by the 5-byte trailer (compression marker + context-modified
|
||||
|
||||
@@ -55,6 +55,10 @@ class BlobIndex {
|
||||
|
||||
BlobIndex(const BlobIndex&) = default;
|
||||
BlobIndex& operator=(const BlobIndex&) = default;
|
||||
BlobIndex(BlobIndex&&) = default;
|
||||
BlobIndex& operator=(BlobIndex&&) = default;
|
||||
|
||||
~BlobIndex() = default;
|
||||
|
||||
bool IsInlined() const { return type_ == Type::kInlinedTTL; }
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ Status BlobLogSequentialReader::ReadSlice(uint64_t size, Slice* slice,
|
||||
|
||||
StopWatch read_sw(clock_, statistics_, BLOB_DB_BLOB_FILE_READ_MICROS);
|
||||
// TODO: rate limit `BlobLogSequentialReader` reads (it appears unused?)
|
||||
Status s = file_->Read(IOOptions(), next_byte_, static_cast<size_t>(size),
|
||||
Status s = file_->Read(next_byte_, static_cast<size_t>(size), IOOptions(),
|
||||
slice, buf, nullptr);
|
||||
next_byte_ += size;
|
||||
if (!s.ok()) {
|
||||
|
||||
@@ -43,6 +43,8 @@ class BlobLogSequentialReader {
|
||||
// No copying allowed
|
||||
BlobLogSequentialReader(const BlobLogSequentialReader&) = delete;
|
||||
BlobLogSequentialReader& operator=(const BlobLogSequentialReader&) = delete;
|
||||
BlobLogSequentialReader(BlobLogSequentialReader&&) = delete;
|
||||
BlobLogSequentialReader& operator=(BlobLogSequentialReader&&) = delete;
|
||||
|
||||
~BlobLogSequentialReader();
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ Status BlobLogWriter::Sync(const WriteOptions& write_options) {
|
||||
IOOptions opts;
|
||||
Status s = WritableFileWriter::PrepareIOOptions(write_options, opts);
|
||||
if (s.ok()) {
|
||||
s = dest_->Sync(opts, use_fsync_);
|
||||
s = use_fsync_ ? dest_->Fsync(opts) : dest_->Sync(opts);
|
||||
}
|
||||
if (s.ok()) {
|
||||
RecordTick(statistics_, BLOB_DB_BLOB_FILE_SYNCED);
|
||||
@@ -58,7 +58,7 @@ Status BlobLogWriter::WriteHeader(const WriteOptions& write_options,
|
||||
IOOptions opts;
|
||||
Status s = WritableFileWriter::PrepareIOOptions(write_options, opts);
|
||||
if (s.ok()) {
|
||||
s = dest_->Append(opts, Slice(str));
|
||||
s = dest_->Append(Slice(str), opts);
|
||||
}
|
||||
if (s.ok()) {
|
||||
block_offset_ += str.size();
|
||||
@@ -92,7 +92,7 @@ Status BlobLogWriter::AppendFooter(const WriteOptions& write_options,
|
||||
IOOptions opts;
|
||||
s = WritableFileWriter::PrepareIOOptions(write_options, opts);
|
||||
if (s.ok()) {
|
||||
s = dest_->Append(opts, Slice(str));
|
||||
s = dest_->Append(Slice(str), opts);
|
||||
}
|
||||
if (s.ok()) {
|
||||
block_offset_ += str.size();
|
||||
@@ -181,13 +181,13 @@ Status BlobLogWriter::EmitPhysicalRecord(const WriteOptions& write_options,
|
||||
IOOptions opts;
|
||||
Status s = WritableFileWriter::PrepareIOOptions(write_options, opts);
|
||||
if (s.ok()) {
|
||||
s = dest_->Append(opts, Slice(headerbuf));
|
||||
s = dest_->Append(Slice(headerbuf), opts);
|
||||
}
|
||||
if (s.ok()) {
|
||||
s = dest_->Append(opts, key);
|
||||
s = dest_->Append(key, opts);
|
||||
}
|
||||
if (s.ok()) {
|
||||
s = dest_->Append(opts, val);
|
||||
s = dest_->Append(val, opts);
|
||||
}
|
||||
if (do_flush_ && s.ok()) {
|
||||
s = dest_->Flush(opts);
|
||||
|
||||
@@ -37,6 +37,8 @@ class BlobLogWriter {
|
||||
// No copying allowed
|
||||
BlobLogWriter(const BlobLogWriter&) = delete;
|
||||
BlobLogWriter& operator=(const BlobLogWriter&) = delete;
|
||||
BlobLogWriter(BlobLogWriter&&) = delete;
|
||||
BlobLogWriter& operator=(BlobLogWriter&&) = delete;
|
||||
|
||||
~BlobLogWriter();
|
||||
|
||||
|
||||
@@ -14,10 +14,21 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
// Non-owning metadata needed when opening a blob file for reading. The
|
||||
// checksum slices must remain valid while this object is in use. A file without
|
||||
// finalized checksum metadata, such as an active direct-write blob file, uses
|
||||
// empty slices for both checksum fields.
|
||||
struct BlobFileOpenInfo {
|
||||
uint64_t file_number = 0;
|
||||
Slice file_checksum;
|
||||
Slice file_checksum_func_name;
|
||||
};
|
||||
|
||||
// A read Blob request structure for use in BlobSource::MultiGetBlob and
|
||||
// BlobFileReader::MultiGetBlob.
|
||||
struct BlobReadRequest {
|
||||
// User key to lookup the paired blob
|
||||
// TODO: consider simplifying to copied Slice?
|
||||
const Slice* user_key = nullptr;
|
||||
|
||||
// File offset in bytes
|
||||
@@ -49,10 +60,57 @@ struct BlobReadRequest {
|
||||
BlobReadRequest() = default;
|
||||
BlobReadRequest(const BlobReadRequest& other) = default;
|
||||
BlobReadRequest& operator=(const BlobReadRequest& other) = default;
|
||||
BlobReadRequest(BlobReadRequest&& other) = default;
|
||||
BlobReadRequest& operator=(BlobReadRequest&& other) = default;
|
||||
~BlobReadRequest() = default;
|
||||
};
|
||||
|
||||
using BlobFileReadRequests =
|
||||
std::tuple<uint64_t /* file_number */, uint64_t /* file_size */,
|
||||
std::tuple<BlobFileOpenInfo, uint64_t /* file_size */,
|
||||
autovector<BlobReadRequest>>;
|
||||
|
||||
// A byte-range (partial) blob read request for the lazy blob-read multi
|
||||
// primitives (BlobFileReader::MultiGetBlobRange /
|
||||
// BlobSource::MultiGetBlobRange). Unlike BlobReadRequest (whole value), it
|
||||
// reads only the sub-range [range_offset, range_offset + range_length) of an
|
||||
// *uncompressed* blob value, skipping whole-record checksum verification and
|
||||
// blob-cache population -- the multi-read counterpart of
|
||||
// BlobSource::GetBlobRange / BlobFileReader::GetBlobRange.
|
||||
struct BlobRangeReadRequest {
|
||||
// User key that pairs with the blob value (used for offset validation).
|
||||
Slice user_key;
|
||||
|
||||
// The blob value's file offset and full (logical == on-disk, uncompressed)
|
||||
// size, from the BlobIndex.
|
||||
uint64_t offset = 0;
|
||||
uint64_t value_size = 0;
|
||||
|
||||
// The requested sub-range within the value. `range_length` is the number of
|
||||
// bytes to read; the caller must ensure range_offset + range_length <=
|
||||
// value_size (it clamps before building the request).
|
||||
uint64_t range_offset = 0;
|
||||
size_t range_length = 0;
|
||||
|
||||
// Output parameter set to point to the requested bytes, and the read status.
|
||||
PinnableSlice* result = nullptr;
|
||||
Status* status = nullptr;
|
||||
|
||||
BlobRangeReadRequest(Slice _user_key, uint64_t _offset, uint64_t _value_size,
|
||||
uint64_t _range_offset, size_t _range_length,
|
||||
PinnableSlice* _result, Status* _status)
|
||||
: user_key(_user_key),
|
||||
offset(_offset),
|
||||
value_size(_value_size),
|
||||
range_offset(_range_offset),
|
||||
range_length(_range_length),
|
||||
result(_result),
|
||||
status(_status) {}
|
||||
|
||||
BlobRangeReadRequest() = default;
|
||||
};
|
||||
|
||||
using BlobFileRangeReadRequests =
|
||||
std::tuple<BlobFileOpenInfo, uint64_t /* file_size */,
|
||||
autovector<BlobRangeReadRequest>>;
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
+365
-25
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "cache/cache_reservation_manager.h"
|
||||
#include "cache/charged_cache.h"
|
||||
@@ -49,9 +50,10 @@ Status AppendBlobRefreshRetryFailure(const Status& stale_status,
|
||||
template <typename ReadFn>
|
||||
Status ReadBlobWithReaderRetry(BlobFileCache* blob_file_cache,
|
||||
const ReadOptions& read_options,
|
||||
uint64_t file_number, const ReadFn& read) {
|
||||
const BlobFileOpenInfo& blob_file,
|
||||
const ReadFn& read) {
|
||||
CacheHandleGuard<BlobFileReader> blob_file_reader;
|
||||
Status s = blob_file_cache->GetBlobFileReader(read_options, file_number,
|
||||
Status s = blob_file_cache->GetBlobFileReader(read_options, blob_file,
|
||||
&blob_file_reader);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
@@ -65,11 +67,11 @@ Status ReadBlobWithReaderRetry(BlobFileCache* blob_file_cache,
|
||||
|
||||
const Status stale_status = s;
|
||||
blob_file_reader.Reset();
|
||||
blob_file_cache->Evict(file_number);
|
||||
blob_file_cache->Evict(blob_file.file_number);
|
||||
|
||||
std::unique_ptr<BlobFileReader> fresh_reader;
|
||||
s = blob_file_cache->OpenBlobFileReaderUncached(
|
||||
read_options, file_number, &fresh_reader,
|
||||
read_options, blob_file, &fresh_reader,
|
||||
/*allow_footer_skip_retry=*/false);
|
||||
if (!s.ok()) {
|
||||
return AppendBlobRefreshRetryFailure(stale_status, s);
|
||||
@@ -82,7 +84,7 @@ Status ReadBlobWithReaderRetry(BlobFileCache* blob_file_cache,
|
||||
|
||||
CacheHandleGuard<BlobFileReader> ignored_reader;
|
||||
blob_file_cache
|
||||
->RefreshBlobFileReader(file_number, &fresh_reader, &ignored_reader)
|
||||
->RefreshBlobFileReader(blob_file, &fresh_reader, &ignored_reader)
|
||||
.PermitUncheckedError();
|
||||
return s;
|
||||
}
|
||||
@@ -200,7 +202,7 @@ Status BlobSource::PutBlobIntoCache(
|
||||
const Status s = InsertEntryIntoCache(cache_key, blob->get(), &cache_handle,
|
||||
Cache::Priority::BOTTOM);
|
||||
if (s.ok()) {
|
||||
blob->release();
|
||||
[[maybe_unused]] BlobContents* released_blob = blob->release();
|
||||
|
||||
assert(cache_handle != nullptr);
|
||||
*cached_blob =
|
||||
@@ -271,9 +273,9 @@ Status BlobSource::InsertEntryIntoCache(const Slice& key, BlobContents* value,
|
||||
}
|
||||
|
||||
Status BlobSource::GetBlob(const ReadOptions& read_options,
|
||||
const Slice& user_key, uint64_t file_number,
|
||||
uint64_t offset, uint64_t file_size,
|
||||
uint64_t value_size,
|
||||
const Slice& user_key,
|
||||
const BlobFileOpenInfo& blob_file, uint64_t offset,
|
||||
uint64_t file_size, uint64_t value_size,
|
||||
CompressionType compression_type,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* value, uint64_t* bytes_read) {
|
||||
@@ -281,7 +283,8 @@ Status BlobSource::GetBlob(const ReadOptions& read_options,
|
||||
|
||||
Status s;
|
||||
|
||||
const CacheKey cache_key = GetCacheKey(file_number, file_size, offset);
|
||||
const CacheKey cache_key =
|
||||
GetCacheKey(blob_file.file_number, file_size, offset);
|
||||
|
||||
CacheHandleGuard<BlobContents> blob_handle;
|
||||
|
||||
@@ -331,8 +334,7 @@ Status BlobSource::GetBlob(const ReadOptions& read_options,
|
||||
|
||||
uint64_t read_size = 0;
|
||||
s = ReadBlobWithReaderRetry(
|
||||
blob_file_cache_, read_options, file_number,
|
||||
[&](BlobFileReader* reader) {
|
||||
blob_file_cache_, read_options, blob_file, [&](BlobFileReader* reader) {
|
||||
if (compression_type != reader->GetCompressionType()) {
|
||||
return Status::Corruption(
|
||||
"Compression type mismatch when reading blob");
|
||||
@@ -376,7 +378,8 @@ Status BlobSource::GetBlob(const ReadOptions& read_options,
|
||||
}
|
||||
|
||||
Status BlobSource::GetBlobRange(const ReadOptions& read_options,
|
||||
const Slice& user_key, uint64_t file_number,
|
||||
const Slice& user_key,
|
||||
const BlobFileOpenInfo& blob_file,
|
||||
uint64_t offset, uint64_t file_size,
|
||||
uint64_t value_size,
|
||||
CompressionType compression_type,
|
||||
@@ -393,7 +396,8 @@ Status BlobSource::GetBlobRange(const ReadOptions& read_options,
|
||||
|
||||
Status s;
|
||||
|
||||
const CacheKey cache_key = GetCacheKey(file_number, file_size, offset);
|
||||
const CacheKey cache_key =
|
||||
GetCacheKey(blob_file.file_number, file_size, offset);
|
||||
|
||||
CacheHandleGuard<BlobContents> blob_handle;
|
||||
|
||||
@@ -416,7 +420,8 @@ Status BlobSource::GetBlobRange(const ReadOptions& read_options,
|
||||
|
||||
const bool no_io = read_options.read_tier == kBlockCacheTier;
|
||||
if (no_io) {
|
||||
return Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
s = Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
return s;
|
||||
}
|
||||
|
||||
// Cache miss: read only the requested sub-range from the file. The result is
|
||||
@@ -433,8 +438,7 @@ Status BlobSource::GetBlobRange(const ReadOptions& read_options,
|
||||
// case (offset/size mismatch surfacing as Corruption), not a payload
|
||||
// checksum failure.
|
||||
s = ReadBlobWithReaderRetry(
|
||||
blob_file_cache_, read_options, file_number,
|
||||
[&](BlobFileReader* reader) {
|
||||
blob_file_cache_, read_options, blob_file, [&](BlobFileReader* reader) {
|
||||
if (compression_type != reader->GetCompressionType()) {
|
||||
return Status::Corruption(
|
||||
"Compression type mismatch when reading blob");
|
||||
@@ -501,7 +505,8 @@ Status BlobSource::GetSimpleGen2Blob(
|
||||
|
||||
const bool no_io = read_options.read_tier == kBlockCacheTier;
|
||||
if (no_io) {
|
||||
return Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
s = Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
return s;
|
||||
}
|
||||
|
||||
// Cache miss (or no cache configured). Read the record into a buffer
|
||||
@@ -601,7 +606,8 @@ Status BlobSource::GetSimpleGen2BlobRange(
|
||||
|
||||
const bool no_io = read_options.read_tier == kBlockCacheTier;
|
||||
if (no_io) {
|
||||
return Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
s = Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
return s;
|
||||
}
|
||||
|
||||
// Cache miss: read only the requested sub-range from the file. The result is
|
||||
@@ -633,6 +639,188 @@ Status BlobSource::GetSimpleGen2BlobRange(
|
||||
return s;
|
||||
}
|
||||
|
||||
void BlobSource::MultiGetSimpleGen2Blob(
|
||||
const ReadOptions& read_options, const OffsetableCacheKey& base_cache_key,
|
||||
RandomAccessFileReader* file, ChecksumType checksum_type,
|
||||
uint32_t base_context_checksum, size_t num_records,
|
||||
SimpleGen2BlobReadRequest* reqs) {
|
||||
assert(file);
|
||||
if (num_records == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool no_io = read_options.read_tier == kBlockCacheTier;
|
||||
MemoryAllocator* const allocator = (blob_cache_ && read_options.fill_cache)
|
||||
? blob_cache_.get()->memory_allocator()
|
||||
: nullptr;
|
||||
|
||||
// Records that missed the cache and must be read from disk, in read order;
|
||||
// each owns its buffer until we build a BlobContents from it below.
|
||||
struct Pending {
|
||||
size_t idx;
|
||||
CacheAllocationPtr buf;
|
||||
uint64_t record_size;
|
||||
};
|
||||
std::vector<Pending> pending;
|
||||
std::vector<SimpleGen2RecordReadRequest> read_reqs;
|
||||
|
||||
for (size_t i = 0; i < num_records; ++i) {
|
||||
SimpleGen2BlobReadRequest& req = reqs[i];
|
||||
assert(req.result);
|
||||
assert(req.status);
|
||||
|
||||
const uint64_t record_size = req.payload_size + kSimpleGen2BlobTrailerSize;
|
||||
const CacheKey cache_key =
|
||||
GetSimpleGen2BlobCacheKey(base_cache_key, req.record_offset);
|
||||
|
||||
if (blob_cache_) {
|
||||
CacheHandleGuard<BlobContents> blob_handle;
|
||||
const Status cs = GetBlobFromCache(cache_key.AsSlice(), &blob_handle);
|
||||
if (cs.ok()) {
|
||||
*req.status = cs;
|
||||
PinCachedBlob(&blob_handle, req.result);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (no_io) {
|
||||
*req.status =
|
||||
Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
continue;
|
||||
}
|
||||
|
||||
CacheAllocationPtr buf =
|
||||
AllocateBlock(static_cast<size_t>(record_size), allocator);
|
||||
read_reqs.push_back(SimpleGen2RecordReadRequest{
|
||||
req.record_offset, static_cast<size_t>(req.payload_size),
|
||||
static_cast<size_t>(record_size), req.expected_compression, buf.get(),
|
||||
req.status});
|
||||
pending.push_back(Pending{i, std::move(buf), record_size});
|
||||
}
|
||||
|
||||
if (read_reqs.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ReadAndVerifySimpleGen2BlobRecords(read_options, file, checksum_type,
|
||||
base_context_checksum, read_reqs.size(),
|
||||
read_reqs.data());
|
||||
|
||||
for (size_t k = 0; k < pending.size(); ++k) {
|
||||
SimpleGen2BlobReadRequest& req = reqs[pending[k].idx];
|
||||
if (!req.status->ok()) {
|
||||
continue;
|
||||
}
|
||||
const uint64_t record_size = pending[k].record_size;
|
||||
std::unique_ptr<BlobContents> blob_contents(new BlobContents(
|
||||
std::move(pending[k].buf), static_cast<size_t>(req.payload_size)));
|
||||
|
||||
RecordTick(statistics_, BLOB_DB_BLOB_FILE_BYTES_READ, record_size);
|
||||
PERF_COUNTER_ADD(blob_read_count, 1);
|
||||
PERF_COUNTER_ADD(blob_read_byte, record_size);
|
||||
if (read_options.io_activity == Env::IOActivity::kLazyResolve) {
|
||||
RecordLazyRead(statistics_, record_size);
|
||||
}
|
||||
|
||||
if (blob_cache_ && read_options.fill_cache) {
|
||||
const CacheKey cache_key =
|
||||
GetSimpleGen2BlobCacheKey(base_cache_key, req.record_offset);
|
||||
CacheHandleGuard<BlobContents> blob_handle;
|
||||
const Status ps =
|
||||
PutBlobIntoCache(cache_key.AsSlice(), &blob_contents, &blob_handle);
|
||||
if (!ps.ok()) {
|
||||
*req.status = ps;
|
||||
} else {
|
||||
PinCachedBlob(&blob_handle, req.result);
|
||||
}
|
||||
} else {
|
||||
PinOwnedBlob(&blob_contents, req.result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BlobSource::MultiGetSimpleGen2BlobRange(
|
||||
const ReadOptions& read_options, const OffsetableCacheKey& base_cache_key,
|
||||
RandomAccessFileReader* file, size_t num_records,
|
||||
SimpleGen2BlobRangeReadRequest* reqs) {
|
||||
assert(file);
|
||||
// Range reads are only issued while resolving a lazy result (partial reads
|
||||
// are lazy-only), so the lazy read stats below are recorded unconditionally.
|
||||
assert(read_options.io_activity == Env::IOActivity::kLazyResolve);
|
||||
if (num_records == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool no_io = read_options.read_tier == kBlockCacheTier;
|
||||
|
||||
struct Pending {
|
||||
size_t idx;
|
||||
CacheAllocationPtr buf;
|
||||
};
|
||||
std::vector<Pending> pending;
|
||||
std::vector<SimpleGen2RangeReadRequest> read_reqs;
|
||||
|
||||
for (size_t i = 0; i < num_records; ++i) {
|
||||
SimpleGen2BlobRangeReadRequest& req = reqs[i];
|
||||
assert(req.result);
|
||||
assert(req.status);
|
||||
|
||||
const CacheKey cache_key =
|
||||
GetSimpleGen2BlobCacheKey(base_cache_key, req.record_offset);
|
||||
|
||||
// Probe the whole-payload cache; on a hit slice the requested sub-range.
|
||||
if (blob_cache_) {
|
||||
CacheHandleGuard<BlobContents> blob_handle;
|
||||
const Status cs = GetBlobFromCache(cache_key.AsSlice(), &blob_handle);
|
||||
if (cs.ok()) {
|
||||
*req.status = cs;
|
||||
PinCacheHitSubRange(&blob_handle, req.range_offset, req.range_length,
|
||||
req.result);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (no_io) {
|
||||
*req.status =
|
||||
Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
continue;
|
||||
}
|
||||
|
||||
// No cache-fill allocator: a partial payload is never inserted.
|
||||
CacheAllocationPtr buf =
|
||||
AllocateBlock(req.range_length, /*allocator=*/nullptr);
|
||||
read_reqs.push_back(SimpleGen2RangeReadRequest{
|
||||
req.record_offset, static_cast<size_t>(req.payload_size),
|
||||
req.range_offset, req.range_length, req.expected_compression, buf.get(),
|
||||
req.status});
|
||||
pending.push_back(Pending{i, std::move(buf)});
|
||||
}
|
||||
|
||||
if (read_reqs.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ReadSimpleGen2BlobRanges(read_options, file, read_reqs.size(),
|
||||
read_reqs.data());
|
||||
|
||||
for (size_t k = 0; k < pending.size(); ++k) {
|
||||
SimpleGen2BlobRangeReadRequest& req = reqs[pending[k].idx];
|
||||
if (!req.status->ok()) {
|
||||
continue;
|
||||
}
|
||||
std::unique_ptr<BlobContents> blob_contents(
|
||||
new BlobContents(std::move(pending[k].buf), req.range_length));
|
||||
|
||||
RecordTick(statistics_, BLOB_DB_BLOB_FILE_BYTES_READ, req.range_length);
|
||||
PERF_COUNTER_ADD(blob_read_count, 1);
|
||||
PERF_COUNTER_ADD(blob_read_byte, req.range_length);
|
||||
RecordLazyRead(statistics_, req.range_length);
|
||||
RecordLazyPartialRead(statistics_, req.range_length, req.payload_size);
|
||||
|
||||
PinOwnedBlob(&blob_contents, req.result);
|
||||
}
|
||||
}
|
||||
|
||||
void BlobSource::MultiGetBlob(const ReadOptions& read_options,
|
||||
autovector<BlobFileReadRequests>& blob_reqs,
|
||||
uint64_t* bytes_read) {
|
||||
@@ -641,7 +829,7 @@ void BlobSource::MultiGetBlob(const ReadOptions& read_options,
|
||||
uint64_t total_bytes_read = 0;
|
||||
uint64_t bytes_read_in_file = 0;
|
||||
|
||||
for (auto& [file_number, file_size, blob_reqs_in_file] : blob_reqs) {
|
||||
for (auto& [blob_file, file_size, blob_reqs_in_file] : blob_reqs) {
|
||||
// sort blob_reqs_in_file by file offset.
|
||||
std::sort(
|
||||
blob_reqs_in_file.begin(), blob_reqs_in_file.end(),
|
||||
@@ -649,7 +837,7 @@ void BlobSource::MultiGetBlob(const ReadOptions& read_options,
|
||||
return lhs.offset < rhs.offset;
|
||||
});
|
||||
|
||||
MultiGetBlobFromOneFile(read_options, file_number, file_size,
|
||||
MultiGetBlobFromOneFile(read_options, blob_file, file_size,
|
||||
blob_reqs_in_file, &bytes_read_in_file);
|
||||
|
||||
total_bytes_read += bytes_read_in_file;
|
||||
@@ -661,7 +849,7 @@ void BlobSource::MultiGetBlob(const ReadOptions& read_options,
|
||||
}
|
||||
|
||||
void BlobSource::MultiGetBlobFromOneFile(const ReadOptions& read_options,
|
||||
uint64_t file_number,
|
||||
const BlobFileOpenInfo& blob_file,
|
||||
uint64_t /*file_size*/,
|
||||
autovector<BlobReadRequest>& blob_reqs,
|
||||
uint64_t* bytes_read) {
|
||||
@@ -679,6 +867,7 @@ void BlobSource::MultiGetBlobFromOneFile(const ReadOptions& read_options,
|
||||
Mask cache_hit_mask = 0;
|
||||
|
||||
uint64_t total_bytes = 0;
|
||||
const uint64_t file_number = blob_file.file_number;
|
||||
const OffsetableCacheKey base_cache_key(db_id_, db_session_id_, file_number);
|
||||
|
||||
if (blob_cache_) {
|
||||
@@ -750,7 +939,7 @@ void BlobSource::MultiGetBlobFromOneFile(const ReadOptions& read_options,
|
||||
}
|
||||
|
||||
CacheHandleGuard<BlobFileReader> blob_file_reader;
|
||||
Status s = blob_file_cache_->GetBlobFileReader(read_options, file_number,
|
||||
Status s = blob_file_cache_->GetBlobFileReader(read_options, blob_file,
|
||||
&blob_file_reader);
|
||||
if (!s.ok()) {
|
||||
for (size_t i = 0; i < _blob_reqs.size(); ++i) {
|
||||
@@ -790,7 +979,7 @@ void BlobSource::MultiGetBlobFromOneFile(const ReadOptions& read_options,
|
||||
|
||||
std::unique_ptr<BlobFileReader> fresh_reader;
|
||||
s = blob_file_cache_->OpenBlobFileReaderUncached(
|
||||
read_options, file_number, &fresh_reader,
|
||||
read_options, blob_file, &fresh_reader,
|
||||
/*allow_footer_skip_retry=*/false);
|
||||
if (!s.ok()) {
|
||||
for (const auto& blob_req : _blob_reqs) {
|
||||
@@ -851,7 +1040,7 @@ void BlobSource::MultiGetBlobFromOneFile(const ReadOptions& read_options,
|
||||
if (install_fresh_reader) {
|
||||
CacheHandleGuard<BlobFileReader> ignored_reader;
|
||||
blob_file_cache_
|
||||
->RefreshBlobFileReader(file_number, &fresh_reader, &ignored_reader)
|
||||
->RefreshBlobFileReader(blob_file, &fresh_reader, &ignored_reader)
|
||||
.PermitUncheckedError();
|
||||
}
|
||||
}
|
||||
@@ -884,6 +1073,24 @@ void BlobSource::MultiGetBlobFromOneFile(const ReadOptions& read_options,
|
||||
}
|
||||
}
|
||||
|
||||
// Whole-column reads on the lazy resolve path count as lazy reads (mirrors
|
||||
// BlobSource::GetBlob, which records only on a disk read, not a cache hit).
|
||||
// Gated on the activity so regular MultiGet is unaffected; the misses read
|
||||
// above are exactly the disk reads for these blob columns.
|
||||
if (read_options.io_activity == Env::IOActivity::kLazyResolve) {
|
||||
for (auto& [req, blob_contents] : _blob_reqs) {
|
||||
assert(req);
|
||||
if (req->status->ok()) {
|
||||
const uint64_t adjustment =
|
||||
read_options.verify_checksums
|
||||
? BlobLogRecord::CalculateAdjustmentForRecordHeader(
|
||||
req->user_key->size())
|
||||
: 0;
|
||||
RecordLazyRead(statistics_, req->len + adjustment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
total_bytes += _bytes_read;
|
||||
if (bytes_read) {
|
||||
*bytes_read = total_bytes;
|
||||
@@ -891,6 +1098,139 @@ void BlobSource::MultiGetBlobFromOneFile(const ReadOptions& read_options,
|
||||
}
|
||||
}
|
||||
|
||||
void BlobSource::MultiGetBlobRange(
|
||||
const ReadOptions& read_options,
|
||||
autovector<BlobFileRangeReadRequests>& blob_reqs, uint64_t* bytes_read) {
|
||||
assert(blob_reqs.size() > 0);
|
||||
|
||||
uint64_t total_bytes_read = 0;
|
||||
uint64_t bytes_read_in_file = 0;
|
||||
|
||||
for (auto& [blob_file, file_size, blob_reqs_in_file] : blob_reqs) {
|
||||
// Sort by effective read offset so the file system layer can coalesce
|
||||
// adjacent sub-range reads within one MultiRead.
|
||||
std::sort(blob_reqs_in_file.begin(), blob_reqs_in_file.end(),
|
||||
[](const BlobRangeReadRequest& lhs,
|
||||
const BlobRangeReadRequest& rhs) -> bool {
|
||||
return lhs.offset + lhs.range_offset <
|
||||
rhs.offset + rhs.range_offset;
|
||||
});
|
||||
|
||||
MultiGetBlobRangeFromOneFile(read_options, blob_file, file_size,
|
||||
blob_reqs_in_file, &bytes_read_in_file);
|
||||
|
||||
total_bytes_read += bytes_read_in_file;
|
||||
bytes_read_in_file = 0; // reset: some early-return paths do not write it
|
||||
}
|
||||
|
||||
if (bytes_read) {
|
||||
*bytes_read = total_bytes_read;
|
||||
}
|
||||
}
|
||||
|
||||
void BlobSource::MultiGetBlobRangeFromOneFile(
|
||||
const ReadOptions& read_options, const BlobFileOpenInfo& blob_file,
|
||||
uint64_t /*file_size*/, autovector<BlobRangeReadRequest>& blob_reqs,
|
||||
uint64_t* bytes_read) {
|
||||
const size_t num_blobs = blob_reqs.size();
|
||||
assert(num_blobs > 0);
|
||||
assert(num_blobs <= MultiGetContext::MAX_BATCH_SIZE);
|
||||
// Range reads are only issued while resolving a lazy result (partial reads
|
||||
// are lazy-only), so the lazy read stats below are recorded unconditionally.
|
||||
assert(read_options.io_activity == Env::IOActivity::kLazyResolve);
|
||||
|
||||
using Mask = uint64_t;
|
||||
Mask cache_hit_mask = 0;
|
||||
|
||||
uint64_t total_bytes = 0;
|
||||
const uint64_t file_number = blob_file.file_number;
|
||||
const OffsetableCacheKey base_cache_key(db_id_, db_session_id_, file_number);
|
||||
|
||||
// Probe the whole-value cache per request; on a hit, slice the requested
|
||||
// sub-range (zero-copy, no disk read). A partial read is never cache-filled.
|
||||
if (blob_cache_) {
|
||||
size_t cached_blob_count = 0;
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
BlobRangeReadRequest& req = blob_reqs[i];
|
||||
assert(req.status);
|
||||
|
||||
CacheHandleGuard<BlobContents> blob_handle;
|
||||
const CacheKey cache_key = base_cache_key.WithOffset(req.offset);
|
||||
const Status s = GetBlobFromCache(cache_key.AsSlice(), &blob_handle);
|
||||
if (s.ok()) {
|
||||
*req.status = s;
|
||||
PinCacheHitSubRange(&blob_handle, req.range_offset, req.range_length,
|
||||
req.result);
|
||||
++cached_blob_count;
|
||||
cache_hit_mask |= (Mask{1} << i); // hit -> no disk read
|
||||
}
|
||||
}
|
||||
|
||||
if (cached_blob_count == num_blobs) {
|
||||
if (bytes_read) {
|
||||
*bytes_read = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const bool no_io = read_options.read_tier == kBlockCacheTier;
|
||||
if (no_io) {
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
if (!(cache_hit_mask & (Mask{1} << i))) {
|
||||
assert(blob_reqs[i].status);
|
||||
*blob_reqs[i].status =
|
||||
Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
autovector<std::pair<BlobRangeReadRequest*, std::unique_ptr<BlobContents>>>
|
||||
_blob_reqs;
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
if (!(cache_hit_mask & (Mask{1} << i))) {
|
||||
_blob_reqs.emplace_back(&blob_reqs[i], std::unique_ptr<BlobContents>());
|
||||
}
|
||||
}
|
||||
|
||||
// Lazy range reads run against a pinned SuperVersion (max_open_files == -1),
|
||||
// and blob file numbers are never reused, so the stale cached-reader race
|
||||
// that MultiGetBlobFromOneFile guards against cannot occur here; a Corruption
|
||||
// is simply surfaced per request. Hence no reader-refresh retry and no
|
||||
// cache-fill (a partial value cannot represent the whole-record cache entry).
|
||||
CacheHandleGuard<BlobFileReader> blob_file_reader;
|
||||
Status s = blob_file_cache_->GetBlobFileReader(read_options, blob_file,
|
||||
&blob_file_reader);
|
||||
if (!s.ok()) {
|
||||
for (auto& blob_req : _blob_reqs) {
|
||||
assert(blob_req.first);
|
||||
assert(blob_req.first->status);
|
||||
*blob_req.first->status = s;
|
||||
}
|
||||
return;
|
||||
}
|
||||
assert(blob_file_reader.GetValue());
|
||||
|
||||
uint64_t _bytes_read = 0;
|
||||
blob_file_reader.GetValue()->MultiGetBlobRange(read_options, _blob_reqs,
|
||||
&_bytes_read);
|
||||
|
||||
for (auto& [req, blob_contents] : _blob_reqs) {
|
||||
assert(req);
|
||||
if (req->status->ok()) {
|
||||
PinOwnedBlob(&blob_contents, req->result);
|
||||
RecordLazyRead(statistics_, req->range_length);
|
||||
RecordLazyPartialRead(statistics_, req->range_length, req->value_size);
|
||||
}
|
||||
}
|
||||
|
||||
total_bytes += _bytes_read;
|
||||
if (bytes_read) {
|
||||
*bytes_read = total_bytes;
|
||||
}
|
||||
}
|
||||
|
||||
bool BlobSource::TEST_BlobInCache(uint64_t file_number, uint64_t file_size,
|
||||
uint64_t offset, size_t* charge) const {
|
||||
const CacheKey cache_key = GetCacheKey(file_number, file_size, offset);
|
||||
|
||||
+84
-9
@@ -28,6 +28,30 @@ class Slice;
|
||||
class RandomAccessFileReader;
|
||||
enum ChecksumType : char;
|
||||
|
||||
// One whole-record request for BlobSource::MultiGetSimpleGen2Blob (the batched
|
||||
// counterpart of GetSimpleGen2Blob). The per-file context (base cache key,
|
||||
// file, checksum context) is passed once to the batch call; these are the
|
||||
// per-record inputs and outputs.
|
||||
struct SimpleGen2BlobReadRequest {
|
||||
uint64_t record_offset = 0;
|
||||
uint64_t payload_size = 0;
|
||||
CompressionType expected_compression = kNoCompression;
|
||||
PinnableSlice* result = nullptr;
|
||||
Status* status = nullptr;
|
||||
};
|
||||
|
||||
// One sub-range request for BlobSource::MultiGetSimpleGen2BlobRange (the
|
||||
// batched counterpart of GetSimpleGen2BlobRange).
|
||||
struct SimpleGen2BlobRangeReadRequest {
|
||||
uint64_t record_offset = 0;
|
||||
uint64_t payload_size = 0;
|
||||
uint64_t range_offset = 0;
|
||||
size_t range_length = 0;
|
||||
CompressionType expected_compression = kNoCompression;
|
||||
PinnableSlice* result = nullptr;
|
||||
Status* status = nullptr;
|
||||
};
|
||||
|
||||
// BlobSource is a class that provides universal access to blobs, regardless of
|
||||
// whether they are in the blob cache, secondary cache, or (remote) storage.
|
||||
// Depending on user settings, it always fetch blobs from multi-tier cache and
|
||||
@@ -43,6 +67,8 @@ class BlobSource {
|
||||
|
||||
BlobSource(const BlobSource&) = delete;
|
||||
BlobSource& operator=(const BlobSource&) = delete;
|
||||
BlobSource(BlobSource&&) = delete;
|
||||
BlobSource& operator=(BlobSource&&) = delete;
|
||||
|
||||
~BlobSource();
|
||||
|
||||
@@ -56,8 +82,9 @@ class BlobSource {
|
||||
// sets "*bytes_read" to the size of on-disk (possibly compressed) blob
|
||||
// record.
|
||||
Status GetBlob(const ReadOptions& read_options, const Slice& user_key,
|
||||
uint64_t file_number, uint64_t offset, uint64_t file_size,
|
||||
uint64_t value_size, CompressionType compression_type,
|
||||
const BlobFileOpenInfo& blob_file, uint64_t offset,
|
||||
uint64_t file_size, uint64_t value_size,
|
||||
CompressionType compression_type,
|
||||
FilePrefetchBuffer* prefetch_buffer, PinnableSlice* value,
|
||||
uint64_t* bytes_read);
|
||||
|
||||
@@ -81,10 +108,11 @@ class BlobSource {
|
||||
// *bytes_read (when non-null) is the number of bytes read from the file; on a
|
||||
// hit it is 0.
|
||||
Status GetBlobRange(const ReadOptions& read_options, const Slice& user_key,
|
||||
uint64_t file_number, uint64_t offset, uint64_t file_size,
|
||||
uint64_t value_size, CompressionType compression_type,
|
||||
uint64_t range_offset, size_t range_length,
|
||||
PinnableSlice* value, uint64_t* bytes_read);
|
||||
const BlobFileOpenInfo& blob_file, uint64_t offset,
|
||||
uint64_t file_size, uint64_t value_size,
|
||||
CompressionType compression_type, uint64_t range_offset,
|
||||
size_t range_length, PinnableSlice* value,
|
||||
uint64_t* bytes_read);
|
||||
|
||||
// Reads a SimpleGen2Blob payload (see db/blob/blob_gen2_format.h) through the
|
||||
// blob value cache and BLOB_DB_* statistics. This is the counterpart to
|
||||
@@ -146,6 +174,33 @@ class BlobSource {
|
||||
uint64_t range_offset, size_t range_length,
|
||||
PinnableSlice* value, uint64_t* bytes_read);
|
||||
|
||||
// Batched counterpart of GetSimpleGen2Blob: resolves `num_records` whole
|
||||
// embedded (SimpleGen2Blob) records from one file, coalescing all cache-miss
|
||||
// reads into a single MultiRead. The per-file context (base cache key, file,
|
||||
// checksum type, base context checksum) is shared by all records; per-record
|
||||
// inputs/outputs are in `reqs`. Each record independently probes the blob
|
||||
// cache and, on a miss, is read + verified from the file; whole records are
|
||||
// cache-filled (when configured and fill_cache) exactly as GetSimpleGen2Blob.
|
||||
// Per-record outcome is written to *reqs[i].status.
|
||||
void MultiGetSimpleGen2Blob(const ReadOptions& read_options,
|
||||
const OffsetableCacheKey& base_cache_key,
|
||||
RandomAccessFileReader* file,
|
||||
ChecksumType checksum_type,
|
||||
uint32_t base_context_checksum,
|
||||
size_t num_records,
|
||||
SimpleGen2BlobReadRequest* reqs);
|
||||
|
||||
// Batched counterpart of GetSimpleGen2BlobRange: resolves `num_records`
|
||||
// uncompressed embedded sub-ranges from one file, coalescing cache-miss reads
|
||||
// into a single MultiRead. Like GetSimpleGen2BlobRange, each record probes
|
||||
// the whole-payload cache (slicing on a hit) and a partial read is never
|
||||
// inserted into the cache. Per-record outcome is in *reqs[i].status.
|
||||
void MultiGetSimpleGen2BlobRange(const ReadOptions& read_options,
|
||||
const OffsetableCacheKey& base_cache_key,
|
||||
RandomAccessFileReader* file,
|
||||
size_t num_records,
|
||||
SimpleGen2BlobRangeReadRequest* reqs);
|
||||
|
||||
// Read multiple blobs from the underlying cache or blob file(s).
|
||||
//
|
||||
// If successful, returns ok and sets "result" in the elements of "blob_reqs"
|
||||
@@ -181,14 +236,34 @@ class BlobSource {
|
||||
// "*bytes_read" to the total size of on-disk (possibly compressed) blob
|
||||
// records.
|
||||
void MultiGetBlobFromOneFile(const ReadOptions& read_options,
|
||||
uint64_t file_number, uint64_t file_size,
|
||||
const BlobFileOpenInfo& blob_file,
|
||||
uint64_t file_size,
|
||||
autovector<BlobReadRequest>& blob_reqs,
|
||||
uint64_t* bytes_read);
|
||||
|
||||
// Byte-range (partial) multi-read counterpart of MultiGetBlob: for each
|
||||
// request reads only its sub-range of an *uncompressed* blob value, from the
|
||||
// blob cache (whole-value hit, sliced) or coalesced disk reads (one MultiRead
|
||||
// per file). Like GetBlobRange, a partial read never fills the blob cache.
|
||||
// Groups requests by file and dispatches each file via
|
||||
// MultiGetBlobRangeFromOneFile. Per-request outcome is in
|
||||
// *blob_reqs[..].status.
|
||||
void MultiGetBlobRange(const ReadOptions& read_options,
|
||||
autovector<BlobFileRangeReadRequests>& blob_reqs,
|
||||
uint64_t* bytes_read);
|
||||
|
||||
// Byte-range (partial) multi-read counterpart of MultiGetBlobFromOneFile for
|
||||
// a single blob file. See MultiGetBlobRange.
|
||||
void MultiGetBlobRangeFromOneFile(const ReadOptions& read_options,
|
||||
const BlobFileOpenInfo& blob_file,
|
||||
uint64_t file_size,
|
||||
autovector<BlobRangeReadRequest>& blob_reqs,
|
||||
uint64_t* bytes_read);
|
||||
|
||||
inline Status GetBlobFileReader(
|
||||
const ReadOptions& read_options, uint64_t blob_file_number,
|
||||
const ReadOptions& read_options, const BlobFileOpenInfo& blob_file,
|
||||
CacheHandleGuard<BlobFileReader>* blob_file_reader) {
|
||||
return blob_file_cache_->GetBlobFileReader(read_options, blob_file_number,
|
||||
return blob_file_cache_->GetBlobFileReader(read_options, blob_file,
|
||||
blob_file_reader);
|
||||
}
|
||||
|
||||
|
||||
+93
-75
@@ -104,6 +104,10 @@ void WriteBlobFile(const ImmutableOptions& immutable_options,
|
||||
&checksum_method, &checksum_value));
|
||||
}
|
||||
|
||||
BlobFileOpenInfo OpenInfoWithoutChecksum(uint64_t file_number) {
|
||||
return BlobFileOpenInfo{file_number, Slice(), Slice()};
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
class BlobSourceTest : public DBTestBase {
|
||||
@@ -212,10 +216,10 @@ TEST_F(BlobSourceTest, GetBlobsFromCache) {
|
||||
ASSERT_FALSE(blob_source.TEST_BlobInCache(blob_file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
|
||||
ASSERT_OK(blob_source.GetBlob(read_options, keys[i], blob_file_number,
|
||||
blob_offsets[i], file_size, blob_sizes[i],
|
||||
kNoCompression, prefetch_buffer, &values[i],
|
||||
&bytes_read));
|
||||
ASSERT_OK(blob_source.GetBlob(
|
||||
read_options, keys[i], OpenInfoWithoutChecksum(blob_file_number),
|
||||
blob_offsets[i], file_size, blob_sizes[i], kNoCompression,
|
||||
prefetch_buffer, &values[i], &bytes_read));
|
||||
ASSERT_EQ(values[i], blobs[i]);
|
||||
ASSERT_TRUE(values[i].IsPinned());
|
||||
ASSERT_EQ(bytes_read,
|
||||
@@ -251,10 +255,10 @@ TEST_F(BlobSourceTest, GetBlobsFromCache) {
|
||||
ASSERT_FALSE(blob_source.TEST_BlobInCache(blob_file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
|
||||
ASSERT_OK(blob_source.GetBlob(read_options, keys[i], blob_file_number,
|
||||
blob_offsets[i], file_size, blob_sizes[i],
|
||||
kNoCompression, prefetch_buffer, &values[i],
|
||||
&bytes_read));
|
||||
ASSERT_OK(blob_source.GetBlob(
|
||||
read_options, keys[i], OpenInfoWithoutChecksum(blob_file_number),
|
||||
blob_offsets[i], file_size, blob_sizes[i], kNoCompression,
|
||||
prefetch_buffer, &values[i], &bytes_read));
|
||||
ASSERT_EQ(values[i], blobs[i]);
|
||||
ASSERT_TRUE(values[i].IsPinned());
|
||||
ASSERT_EQ(bytes_read,
|
||||
@@ -299,10 +303,10 @@ TEST_F(BlobSourceTest, GetBlobsFromCache) {
|
||||
ASSERT_TRUE(blob_source.TEST_BlobInCache(blob_file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
|
||||
ASSERT_OK(blob_source.GetBlob(read_options, keys[i], blob_file_number,
|
||||
blob_offsets[i], file_size, blob_sizes[i],
|
||||
kNoCompression, prefetch_buffer, &values[i],
|
||||
&bytes_read));
|
||||
ASSERT_OK(blob_source.GetBlob(
|
||||
read_options, keys[i], OpenInfoWithoutChecksum(blob_file_number),
|
||||
blob_offsets[i], file_size, blob_sizes[i], kNoCompression,
|
||||
prefetch_buffer, &values[i], &bytes_read));
|
||||
ASSERT_EQ(values[i], blobs[i]);
|
||||
ASSERT_TRUE(values[i].IsPinned());
|
||||
ASSERT_EQ(bytes_read,
|
||||
@@ -339,10 +343,10 @@ TEST_F(BlobSourceTest, GetBlobsFromCache) {
|
||||
ASSERT_TRUE(blob_source.TEST_BlobInCache(blob_file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
|
||||
ASSERT_OK(blob_source.GetBlob(read_options, keys[i], blob_file_number,
|
||||
blob_offsets[i], file_size, blob_sizes[i],
|
||||
kNoCompression, prefetch_buffer, &values[i],
|
||||
&bytes_read));
|
||||
ASSERT_OK(blob_source.GetBlob(
|
||||
read_options, keys[i], OpenInfoWithoutChecksum(blob_file_number),
|
||||
blob_offsets[i], file_size, blob_sizes[i], kNoCompression,
|
||||
prefetch_buffer, &values[i], &bytes_read));
|
||||
ASSERT_EQ(values[i], blobs[i]);
|
||||
ASSERT_TRUE(values[i].IsPinned());
|
||||
ASSERT_EQ(bytes_read,
|
||||
@@ -385,7 +389,8 @@ TEST_F(BlobSourceTest, GetBlobsFromCache) {
|
||||
blob_offsets[i]));
|
||||
|
||||
ASSERT_TRUE(blob_source
|
||||
.GetBlob(read_options, keys[i], blob_file_number,
|
||||
.GetBlob(read_options, keys[i],
|
||||
OpenInfoWithoutChecksum(blob_file_number),
|
||||
blob_offsets[i], file_size, blob_sizes[i],
|
||||
kNoCompression, prefetch_buffer, &values[i],
|
||||
&bytes_read)
|
||||
@@ -427,7 +432,8 @@ TEST_F(BlobSourceTest, GetBlobsFromCache) {
|
||||
blob_offsets[i]));
|
||||
|
||||
ASSERT_TRUE(blob_source
|
||||
.GetBlob(read_options, keys[i], file_number,
|
||||
.GetBlob(read_options, keys[i],
|
||||
OpenInfoWithoutChecksum(file_number),
|
||||
blob_offsets[i], file_size, blob_sizes[i],
|
||||
kNoCompression, prefetch_buffer, &values[i],
|
||||
&bytes_read)
|
||||
@@ -522,8 +528,8 @@ TEST_F(BlobSourceTest, GetCompressedBlobs) {
|
||||
compression, blob_offsets, blob_sizes);
|
||||
|
||||
CacheHandleGuard<BlobFileReader> blob_file_reader;
|
||||
ASSERT_OK(blob_source.GetBlobFileReader(read_options, file_number,
|
||||
&blob_file_reader));
|
||||
ASSERT_OK(blob_source.GetBlobFileReader(
|
||||
read_options, OpenInfoWithoutChecksum(file_number), &blob_file_reader));
|
||||
ASSERT_NE(blob_file_reader.GetValue(), nullptr);
|
||||
|
||||
const uint64_t file_size = blob_file_reader.GetValue()->GetFileSize();
|
||||
@@ -541,10 +547,10 @@ TEST_F(BlobSourceTest, GetCompressedBlobs) {
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
ASSERT_FALSE(blob_source.TEST_BlobInCache(file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
ASSERT_OK(blob_source.GetBlob(read_options, keys[i], file_number,
|
||||
blob_offsets[i], file_size, blob_sizes[i],
|
||||
compression, nullptr /*prefetch_buffer*/,
|
||||
&values[i], &bytes_read));
|
||||
ASSERT_OK(blob_source.GetBlob(
|
||||
read_options, keys[i], OpenInfoWithoutChecksum(file_number),
|
||||
blob_offsets[i], file_size, blob_sizes[i], compression,
|
||||
nullptr /*prefetch_buffer*/, &values[i], &bytes_read));
|
||||
ASSERT_EQ(values[i], blobs[i] /*uncompressed blob*/);
|
||||
ASSERT_NE(values[i].size(), blob_sizes[i] /*compressed size*/);
|
||||
ASSERT_EQ(bytes_read,
|
||||
@@ -564,10 +570,10 @@ TEST_F(BlobSourceTest, GetCompressedBlobs) {
|
||||
blob_offsets[i]));
|
||||
|
||||
// Compressed blob size is passed in GetBlob
|
||||
ASSERT_OK(blob_source.GetBlob(read_options, keys[i], file_number,
|
||||
blob_offsets[i], file_size, blob_sizes[i],
|
||||
compression, nullptr /*prefetch_buffer*/,
|
||||
&values[i], &bytes_read));
|
||||
ASSERT_OK(blob_source.GetBlob(
|
||||
read_options, keys[i], OpenInfoWithoutChecksum(file_number),
|
||||
blob_offsets[i], file_size, blob_sizes[i], compression,
|
||||
nullptr /*prefetch_buffer*/, &values[i], &bytes_read));
|
||||
ASSERT_EQ(values[i], blobs[i] /*uncompressed blob*/);
|
||||
ASSERT_NE(values[i].size(), blob_sizes[i] /*compressed size*/);
|
||||
ASSERT_EQ(bytes_read,
|
||||
@@ -674,7 +680,8 @@ TEST_F(BlobSourceTest, MultiGetBlobsFromMultiFiles) {
|
||||
keys[j], blob_offsets[j], blob_sizes[j], kNoCompression,
|
||||
&value_buf[i * num_blobs + j], &statuses_buf[i * num_blobs + j]);
|
||||
}
|
||||
blob_reqs.emplace_back(file_number, file_size, blob_reqs_in_file[i]);
|
||||
blob_reqs.emplace_back(OpenInfoWithoutChecksum(file_number), file_size,
|
||||
blob_reqs_in_file[i]);
|
||||
}
|
||||
|
||||
get_perf_context()->Reset();
|
||||
@@ -731,7 +738,8 @@ TEST_F(BlobSourceTest, MultiGetBlobsFromMultiFiles) {
|
||||
}
|
||||
|
||||
// Add a fake multi-get blob request.
|
||||
blob_reqs.emplace_back(fake_file_number, file_size, fake_blob_reqs_in_file);
|
||||
blob_reqs.emplace_back(OpenInfoWithoutChecksum(fake_file_number), file_size,
|
||||
fake_blob_reqs_in_file);
|
||||
|
||||
blob_source.MultiGetBlob(read_options, blob_reqs, &bytes_read);
|
||||
|
||||
@@ -865,8 +873,9 @@ TEST_F(BlobSourceTest, MultiGetBlobsFromCache) {
|
||||
statistics->Reset().PermitUncheckedError();
|
||||
|
||||
// Get half of blobs
|
||||
blob_source.MultiGetBlobFromOneFile(read_options, blob_file_number,
|
||||
file_size, blob_reqs, &bytes_read);
|
||||
blob_source.MultiGetBlobFromOneFile(
|
||||
read_options, OpenInfoWithoutChecksum(blob_file_number), file_size,
|
||||
blob_reqs, &bytes_read);
|
||||
|
||||
uint64_t fs_read_bytes = 0;
|
||||
uint64_t ca_read_bytes = 0;
|
||||
@@ -911,10 +920,10 @@ TEST_F(BlobSourceTest, MultiGetBlobsFromCache) {
|
||||
ASSERT_FALSE(blob_source.TEST_BlobInCache(blob_file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
|
||||
ASSERT_OK(blob_source.GetBlob(read_options, keys[i], blob_file_number,
|
||||
blob_offsets[i], file_size, blob_sizes[i],
|
||||
kNoCompression, prefetch_buffer,
|
||||
&value_buf[i], &bytes_read));
|
||||
ASSERT_OK(blob_source.GetBlob(
|
||||
read_options, keys[i], OpenInfoWithoutChecksum(blob_file_number),
|
||||
blob_offsets[i], file_size, blob_sizes[i], kNoCompression,
|
||||
prefetch_buffer, &value_buf[i], &bytes_read));
|
||||
ASSERT_EQ(value_buf[i], blobs[i]);
|
||||
ASSERT_TRUE(value_buf[i].IsPinned());
|
||||
ASSERT_EQ(bytes_read,
|
||||
@@ -935,8 +944,9 @@ TEST_F(BlobSourceTest, MultiGetBlobsFromCache) {
|
||||
kNoCompression, &value_buf[i], &statuses_buf[i]);
|
||||
}
|
||||
|
||||
blob_source.MultiGetBlobFromOneFile(read_options, blob_file_number,
|
||||
file_size, blob_reqs, &bytes_read);
|
||||
blob_source.MultiGetBlobFromOneFile(
|
||||
read_options, OpenInfoWithoutChecksum(blob_file_number), file_size,
|
||||
blob_reqs, &bytes_read);
|
||||
|
||||
uint64_t blob_bytes = 0;
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
@@ -985,8 +995,9 @@ TEST_F(BlobSourceTest, MultiGetBlobsFromCache) {
|
||||
get_perf_context()->Reset();
|
||||
statistics->Reset().PermitUncheckedError();
|
||||
|
||||
blob_source.MultiGetBlobFromOneFile(read_options, blob_file_number,
|
||||
file_size, blob_reqs, &bytes_read);
|
||||
blob_source.MultiGetBlobFromOneFile(
|
||||
read_options, OpenInfoWithoutChecksum(blob_file_number), file_size,
|
||||
blob_reqs, &bytes_read);
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
ASSERT_TRUE(statuses_buf[i].IsIncomplete());
|
||||
@@ -1029,8 +1040,9 @@ TEST_F(BlobSourceTest, MultiGetBlobsFromCache) {
|
||||
get_perf_context()->Reset();
|
||||
statistics->Reset().PermitUncheckedError();
|
||||
|
||||
blob_source.MultiGetBlobFromOneFile(read_options, non_existing_file_number,
|
||||
file_size, blob_reqs, &bytes_read);
|
||||
blob_source.MultiGetBlobFromOneFile(
|
||||
read_options, OpenInfoWithoutChecksum(non_existing_file_number),
|
||||
file_size, blob_reqs, &bytes_read);
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
ASSERT_TRUE(statuses_buf[i].IsIOError());
|
||||
@@ -1104,8 +1116,9 @@ TEST_F(BlobSourceTest, GetBlobPreservesCorruptionDetailsWhenRefreshOpenFails) {
|
||||
PinnableSlice value;
|
||||
uint64_t bytes_read = 0;
|
||||
ASSERT_OK(blob_source.GetBlob(
|
||||
read_options, keys[0], blob_file_number, blob_offsets[0], file_size,
|
||||
blob_sizes[0], kNoCompression, prefetch_buffer, &value, &bytes_read));
|
||||
read_options, keys[0], OpenInfoWithoutChecksum(blob_file_number),
|
||||
blob_offsets[0], file_size, blob_sizes[0], kNoCompression,
|
||||
prefetch_buffer, &value, &bytes_read));
|
||||
ASSERT_EQ(value, blobs[0]);
|
||||
|
||||
const std::string blob_file_path =
|
||||
@@ -1114,10 +1127,10 @@ TEST_F(BlobSourceTest, GetBlobPreservesCorruptionDetailsWhenRefreshOpenFails) {
|
||||
|
||||
PinnableSlice invalid_value;
|
||||
bytes_read = 0;
|
||||
Status s =
|
||||
blob_source.GetBlob(read_options, keys[0], blob_file_number, file_size,
|
||||
file_size, blob_sizes[0], kNoCompression,
|
||||
prefetch_buffer, &invalid_value, &bytes_read);
|
||||
Status s = blob_source.GetBlob(
|
||||
read_options, keys[0], OpenInfoWithoutChecksum(blob_file_number),
|
||||
file_size, file_size, blob_sizes[0], kNoCompression, prefetch_buffer,
|
||||
&invalid_value, &bytes_read);
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
ASSERT_EQ(bytes_read, 0);
|
||||
|
||||
@@ -1178,8 +1191,9 @@ TEST_F(BlobSourceTest,
|
||||
PinnableSlice value;
|
||||
uint64_t bytes_read = 0;
|
||||
ASSERT_OK(blob_source.GetBlob(
|
||||
read_options, keys[0], blob_file_number, blob_offsets[0], file_size,
|
||||
blob_sizes[0], kNoCompression, prefetch_buffer, &value, &bytes_read));
|
||||
read_options, keys[0], OpenInfoWithoutChecksum(blob_file_number),
|
||||
blob_offsets[0], file_size, blob_sizes[0], kNoCompression,
|
||||
prefetch_buffer, &value, &bytes_read));
|
||||
ASSERT_EQ(value, blobs[0]);
|
||||
|
||||
const std::string blob_file_path =
|
||||
@@ -1193,8 +1207,9 @@ TEST_F(BlobSourceTest,
|
||||
&values[0], &statuses[0]);
|
||||
|
||||
bytes_read = 0;
|
||||
blob_source.MultiGetBlobFromOneFile(read_options, blob_file_number, file_size,
|
||||
blob_reqs, &bytes_read);
|
||||
blob_source.MultiGetBlobFromOneFile(read_options,
|
||||
OpenInfoWithoutChecksum(blob_file_number),
|
||||
file_size, blob_reqs, &bytes_read);
|
||||
ASSERT_TRUE(statuses[0].IsCorruption());
|
||||
ASSERT_EQ(bytes_read, 0);
|
||||
|
||||
@@ -1303,8 +1318,8 @@ TEST_F(BlobSecondaryCacheTest, GetBlobsFromSecondaryCache) {
|
||||
|
||||
CacheHandleGuard<BlobFileReader> file_reader;
|
||||
ReadOptions read_options;
|
||||
ASSERT_OK(
|
||||
blob_source.GetBlobFileReader(read_options, file_number, &file_reader));
|
||||
ASSERT_OK(blob_source.GetBlobFileReader(
|
||||
read_options, OpenInfoWithoutChecksum(file_number), &file_reader));
|
||||
ASSERT_NE(file_reader.GetValue(), nullptr);
|
||||
const uint64_t file_size = file_reader.GetValue()->GetFileSize();
|
||||
ASSERT_EQ(file_reader.GetValue()->GetCompressionType(), kNoCompression);
|
||||
@@ -1322,7 +1337,8 @@ TEST_F(BlobSecondaryCacheTest, GetBlobsFromSecondaryCache) {
|
||||
get_perf_context()->Reset();
|
||||
|
||||
// key0 should be filled to the primary cache from the blob file.
|
||||
ASSERT_OK(blob_source.GetBlob(read_options, keys[0], file_number,
|
||||
ASSERT_OK(blob_source.GetBlob(read_options, keys[0],
|
||||
OpenInfoWithoutChecksum(file_number),
|
||||
blob_offsets[0], file_size, blob_sizes[0],
|
||||
kNoCompression, nullptr /* prefetch_buffer */,
|
||||
values.data(), nullptr /* bytes_read */));
|
||||
@@ -1331,17 +1347,18 @@ TEST_F(BlobSecondaryCacheTest, GetBlobsFromSecondaryCache) {
|
||||
|
||||
// key0 should be evicted and key0's dummy item is inserted into secondary
|
||||
// cache. key1 should be filled to the primary cache from the blob file.
|
||||
ASSERT_OK(blob_source.GetBlob(read_options, keys[1], file_number,
|
||||
blob_offsets[1], file_size, blob_sizes[1],
|
||||
kNoCompression, nullptr /* prefetch_buffer */,
|
||||
&values[1], nullptr /* bytes_read */));
|
||||
ASSERT_OK(blob_source.GetBlob(
|
||||
read_options, keys[1], OpenInfoWithoutChecksum(file_number),
|
||||
blob_offsets[1], file_size, blob_sizes[1], kNoCompression,
|
||||
nullptr /* prefetch_buffer */, &values[1], nullptr /* bytes_read */));
|
||||
|
||||
// Release cache handle
|
||||
values[1].Reset();
|
||||
|
||||
// key0 should be filled to the primary cache from the blob file. key1
|
||||
// should be evicted and key1's dummy item is inserted into secondary cache.
|
||||
ASSERT_OK(blob_source.GetBlob(read_options, keys[0], file_number,
|
||||
ASSERT_OK(blob_source.GetBlob(read_options, keys[0],
|
||||
OpenInfoWithoutChecksum(file_number),
|
||||
blob_offsets[0], file_size, blob_sizes[0],
|
||||
kNoCompression, nullptr /* prefetch_buffer */,
|
||||
values.data(), nullptr /* bytes_read */));
|
||||
@@ -1354,10 +1371,10 @@ TEST_F(BlobSecondaryCacheTest, GetBlobsFromSecondaryCache) {
|
||||
|
||||
// key0 should be evicted and is inserted into secondary cache.
|
||||
// key1 should be filled to the primary cache from the blob file.
|
||||
ASSERT_OK(blob_source.GetBlob(read_options, keys[1], file_number,
|
||||
blob_offsets[1], file_size, blob_sizes[1],
|
||||
kNoCompression, nullptr /* prefetch_buffer */,
|
||||
&values[1], nullptr /* bytes_read */));
|
||||
ASSERT_OK(blob_source.GetBlob(
|
||||
read_options, keys[1], OpenInfoWithoutChecksum(file_number),
|
||||
blob_offsets[1], file_size, blob_sizes[1], kNoCompression,
|
||||
nullptr /* prefetch_buffer */, &values[1], nullptr /* bytes_read */));
|
||||
ASSERT_EQ(values[1], blobs[1]);
|
||||
ASSERT_TRUE(
|
||||
blob_source.TEST_BlobInCache(file_number, file_size, blob_offsets[1]));
|
||||
@@ -1422,9 +1439,10 @@ TEST_F(BlobSecondaryCacheTest, GetBlobsFromSecondaryCache) {
|
||||
// fetch key0 from the blob file to the primary cache.
|
||||
// key1 is evicted and inserted into the secondary cache.
|
||||
ASSERT_OK(blob_source.GetBlob(
|
||||
read_options, keys[0], file_number, blob_offsets[0], file_size,
|
||||
blob_sizes[0], kNoCompression, nullptr /* prefetch_buffer */,
|
||||
values.data(), nullptr /* bytes_read */));
|
||||
read_options, keys[0], OpenInfoWithoutChecksum(file_number),
|
||||
blob_offsets[0], file_size, blob_sizes[0], kNoCompression,
|
||||
nullptr /* prefetch_buffer */, values.data(),
|
||||
nullptr /* bytes_read */));
|
||||
ASSERT_EQ(values[0], blobs[0]);
|
||||
|
||||
// Release cache handle
|
||||
@@ -1606,8 +1624,8 @@ TEST_F(BlobSourceCacheReservationTest, SimpleCacheReservation) {
|
||||
|
||||
for (size_t i = 0; i < kNumBlobs; ++i) {
|
||||
ASSERT_OK(blob_source.GetBlob(
|
||||
read_options, keys_[i], kBlobFileNumber, blob_offsets[i],
|
||||
blob_file_size_, blob_sizes[i], kNoCompression,
|
||||
read_options, keys_[i], OpenInfoWithoutChecksum(kBlobFileNumber),
|
||||
blob_offsets[i], blob_file_size_, blob_sizes[i], kNoCompression,
|
||||
nullptr /* prefetch_buffer */, &values[i], nullptr /* bytes_read */));
|
||||
ASSERT_EQ(cache_res_mgr->GetTotalReservedCacheSize(), 0);
|
||||
ASSERT_EQ(cache_res_mgr->GetTotalMemoryUsed(), 0);
|
||||
@@ -1625,8 +1643,8 @@ TEST_F(BlobSourceCacheReservationTest, SimpleCacheReservation) {
|
||||
uint64_t blob_bytes = 0;
|
||||
for (size_t i = 0; i < kNumBlobs; ++i) {
|
||||
ASSERT_OK(blob_source.GetBlob(
|
||||
read_options, keys_[i], kBlobFileNumber, blob_offsets[i],
|
||||
blob_file_size_, blob_sizes[i], kNoCompression,
|
||||
read_options, keys_[i], OpenInfoWithoutChecksum(kBlobFileNumber),
|
||||
blob_offsets[i], blob_file_size_, blob_sizes[i], kNoCompression,
|
||||
nullptr /* prefetch_buffer */, &values[i], nullptr /* bytes_read */));
|
||||
|
||||
size_t charge = 0;
|
||||
@@ -1728,8 +1746,8 @@ TEST_F(BlobSourceCacheReservationTest, IncreaseCacheReservation) {
|
||||
|
||||
for (size_t i = 0; i < kNumBlobs; ++i) {
|
||||
ASSERT_OK(blob_source.GetBlob(
|
||||
read_options, keys_[i], kBlobFileNumber, blob_offsets[i],
|
||||
blob_file_size_, blob_sizes[i], kNoCompression,
|
||||
read_options, keys_[i], OpenInfoWithoutChecksum(kBlobFileNumber),
|
||||
blob_offsets[i], blob_file_size_, blob_sizes[i], kNoCompression,
|
||||
nullptr /* prefetch_buffer */, &values[i], nullptr /* bytes_read */));
|
||||
ASSERT_EQ(cache_res_mgr->GetTotalReservedCacheSize(), 0);
|
||||
ASSERT_EQ(cache_res_mgr->GetTotalMemoryUsed(), 0);
|
||||
@@ -1744,8 +1762,8 @@ TEST_F(BlobSourceCacheReservationTest, IncreaseCacheReservation) {
|
||||
uint64_t blob_bytes = 0;
|
||||
for (size_t i = 0; i < kNumBlobs; ++i) {
|
||||
ASSERT_OK(blob_source.GetBlob(
|
||||
read_options, keys_[i], kBlobFileNumber, blob_offsets[i],
|
||||
blob_file_size_, blob_sizes[i], kNoCompression,
|
||||
read_options, keys_[i], OpenInfoWithoutChecksum(kBlobFileNumber),
|
||||
blob_offsets[i], blob_file_size_, blob_sizes[i], kNoCompression,
|
||||
nullptr /* prefetch_buffer */, &values[i], nullptr /* bytes_read */));
|
||||
|
||||
// Release cache handle
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
@@ -19,15 +20,19 @@
|
||||
#include "db/column_family.h"
|
||||
#include "db/db_test_util.h"
|
||||
#include "db/db_with_timestamp_test_util.h"
|
||||
#include "env/composite_env_wrapper.h"
|
||||
#include "file/filename.h"
|
||||
#include "file/random_access_file_reader.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/convenience.h"
|
||||
#include "rocksdb/file_system.h"
|
||||
#include "rocksdb/trace_reader_writer.h"
|
||||
#include "rocksdb/trace_record.h"
|
||||
#include "rocksdb/utilities/replayer.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "util/compression.h"
|
||||
#include "util/defer.h"
|
||||
#include "util/file_checksum_helper.h"
|
||||
#include "utilities/fault_injection_env.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -38,6 +43,49 @@ class DBBlobBasicTest : public DBTestBase {
|
||||
: DBTestBase("db_blob_basic_test", /* env_do_fsync */ false) {}
|
||||
};
|
||||
|
||||
class BlobFileChecksumCapturingFS : public FileSystemWrapper {
|
||||
public:
|
||||
explicit BlobFileChecksumCapturingFS(const std::shared_ptr<FileSystem>& base)
|
||||
: FileSystemWrapper(base) {}
|
||||
|
||||
static const char* kClassName() { return "BlobFileChecksumCapturingFS"; }
|
||||
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(".blob") != std::string::npos) {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
file_checksum_ = opts.file_checksum;
|
||||
file_checksum_func_name_ = opts.file_checksum_func_name;
|
||||
++capture_count_;
|
||||
}
|
||||
return target()->NewRandomAccessFile(fname, opts, result, dbg);
|
||||
}
|
||||
|
||||
std::string GetFileChecksum() {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return file_checksum_;
|
||||
}
|
||||
|
||||
std::string GetFileChecksumFuncName() {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return file_checksum_func_name_;
|
||||
}
|
||||
|
||||
int GetCaptureCount() {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return capture_count_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex mu_;
|
||||
std::string file_checksum_;
|
||||
std::string file_checksum_func_name_;
|
||||
int capture_count_ = 0;
|
||||
};
|
||||
|
||||
TEST_F(DBBlobBasicTest, GetBlob) {
|
||||
Options options = GetDefaultOptions();
|
||||
options.enable_blob_files = true;
|
||||
@@ -65,6 +113,41 @@ TEST_F(DBBlobBasicTest, GetBlob) {
|
||||
.IsIncomplete());
|
||||
}
|
||||
|
||||
TEST_F(DBBlobBasicTest, BlobFileChecksumInFileOptions) {
|
||||
auto capturing_fs =
|
||||
std::make_shared<BlobFileChecksumCapturingFS>(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.enable_blob_files = true;
|
||||
options.min_blob_size = 0;
|
||||
options.file_checksum_gen_factory = GetFileChecksumGenCrc32cFactory();
|
||||
Defer close_db([this]() { Close(); });
|
||||
Reopen(options);
|
||||
|
||||
constexpr char key[] = "key";
|
||||
constexpr char blob_value[] = "blob_value";
|
||||
ASSERT_OK(Put(key, blob_value));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
std::vector<ColumnFamilyMetaData> column_family_metadata;
|
||||
db_->GetAllColumnFamilyMetaData(&column_family_metadata);
|
||||
ASSERT_EQ(column_family_metadata.size(), 1);
|
||||
ASSERT_EQ(column_family_metadata[0].blob_files.size(), 1);
|
||||
const BlobMetaData& blob_metadata = column_family_metadata[0].blob_files[0];
|
||||
ASSERT_FALSE(blob_metadata.checksum_value.empty());
|
||||
ASSERT_EQ(blob_metadata.checksum_method, "FileChecksumCrc32c");
|
||||
|
||||
ASSERT_EQ(Get(key), blob_value);
|
||||
|
||||
ASSERT_GT(capturing_fs->GetCaptureCount(), 0);
|
||||
ASSERT_EQ(capturing_fs->GetFileChecksum(), blob_metadata.checksum_value);
|
||||
ASSERT_EQ(capturing_fs->GetFileChecksumFuncName(),
|
||||
blob_metadata.checksum_method);
|
||||
}
|
||||
|
||||
TEST_F(DBBlobBasicTest, BlobFileWritableFileMaxBufferSize) {
|
||||
constexpr uint64_t kBlobWriterBufferSize = 128 * 1024;
|
||||
|
||||
@@ -754,6 +837,15 @@ TEST_F(DBBlobBasicTest, MultiGetBlobsFromCache) {
|
||||
ASSERT_EQ(values[2], third_value);
|
||||
}
|
||||
|
||||
// Load the SST blocks into the block cache. The inlined key does not read
|
||||
// the blob file, so the blob cache stays empty.
|
||||
{
|
||||
ReadOptions fill_block_cache_options;
|
||||
std::string value;
|
||||
ASSERT_OK(db_->Get(fill_block_cache_options, first_key, &value));
|
||||
ASSERT_EQ(value, first_value);
|
||||
}
|
||||
|
||||
// Try again with no I/O allowed. The first (inlined) value should be
|
||||
// successfully read; however, the two blob values could only be read from the
|
||||
// blob file, so for those the read should return Incomplete.
|
||||
@@ -1637,11 +1729,13 @@ TEST_F(DBBlobBasicTest, PropertiesMultiVersion) {
|
||||
|
||||
class DBBlobBasicIOErrorTest : public DBBlobBasicTest,
|
||||
public testing::WithParamInterface<std::string> {
|
||||
public:
|
||||
~DBBlobBasicIOErrorTest() override { Close(); }
|
||||
|
||||
protected:
|
||||
DBBlobBasicIOErrorTest() : sync_point_(GetParam()) {
|
||||
fault_injection_env_.reset(new FaultInjectionTestEnv(env_));
|
||||
}
|
||||
~DBBlobBasicIOErrorTest() { Close(); }
|
||||
|
||||
std::unique_ptr<FaultInjectionTestEnv> fault_injection_env_;
|
||||
std::string sync_point_;
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
@@ -40,6 +42,8 @@
|
||||
#include "rocksdb/utilities/replayer.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "util/compression.h"
|
||||
#include "util/defer.h"
|
||||
#include "util/file_checksum_helper.h"
|
||||
#include "utilities/fault_injection_env.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -121,6 +125,59 @@ bool IsBlobFilePath(const std::string& fname) {
|
||||
file_type == kBlobFile;
|
||||
}
|
||||
|
||||
class BlobFileChecksumCapturingFileSystem : public FileSystemWrapper {
|
||||
public:
|
||||
explicit BlobFileChecksumCapturingFileSystem(
|
||||
const std::shared_ptr<FileSystem>& fs)
|
||||
: FileSystemWrapper(fs) {}
|
||||
|
||||
static const char* kClassName() {
|
||||
return "BlobFileChecksumCapturingFileSystem";
|
||||
}
|
||||
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 (IsBlobFilePath(fname)) {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
file_checksum_ = opts.file_checksum;
|
||||
file_checksum_func_name_ = opts.file_checksum_func_name;
|
||||
++capture_count_;
|
||||
}
|
||||
return target()->NewRandomAccessFile(fname, opts, result, dbg);
|
||||
}
|
||||
|
||||
void Reset() {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
file_checksum_.clear();
|
||||
file_checksum_func_name_.clear();
|
||||
capture_count_ = 0;
|
||||
}
|
||||
|
||||
std::string GetFileChecksum() const {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return file_checksum_;
|
||||
}
|
||||
|
||||
std::string GetFileChecksumFuncName() const {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return file_checksum_func_name_;
|
||||
}
|
||||
|
||||
int GetCaptureCount() const {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return capture_count_;
|
||||
}
|
||||
|
||||
private:
|
||||
mutable std::mutex mu_;
|
||||
std::string file_checksum_;
|
||||
std::string file_checksum_func_name_;
|
||||
int capture_count_ = 0;
|
||||
};
|
||||
|
||||
// Keeps track of when an active blob file stops being writer-visible so the
|
||||
// test filesystem can model current-size visibility only while the file remains
|
||||
// active.
|
||||
@@ -1405,7 +1462,9 @@ TEST_F(DBBlobDirectWriteTest,
|
||||
std::vector<BlobFileGarbage> garbages1;
|
||||
// Simulate a flush retry: prepare once, skip commit, then prepare the same
|
||||
// generation again and verify it reuses the already sealed blob file.
|
||||
ASSERT_OK(mgr->PrepareFlushAdditions(WriteOptions(), /*num_generations=*/1,
|
||||
const std::vector<uint64_t> memtable_ids = {
|
||||
cfh->cfd()->imm()->GetEarliestMemTableID()};
|
||||
ASSERT_OK(mgr->PrepareFlushAdditions(WriteOptions(), memtable_ids,
|
||||
&additions1, &garbages1));
|
||||
ASSERT_EQ(additions1.size(), 1U);
|
||||
ASSERT_TRUE(garbages1.empty());
|
||||
@@ -1414,7 +1473,7 @@ TEST_F(DBBlobDirectWriteTest,
|
||||
|
||||
std::vector<BlobFileAddition> additions2;
|
||||
std::vector<BlobFileGarbage> garbages2;
|
||||
ASSERT_OK(mgr->PrepareFlushAdditions(WriteOptions(), /*num_generations=*/1,
|
||||
ASSERT_OK(mgr->PrepareFlushAdditions(WriteOptions(), memtable_ids,
|
||||
&additions2, &garbages2));
|
||||
ASSERT_EQ(additions2.size(), 1U);
|
||||
ASSERT_TRUE(garbages2.empty());
|
||||
@@ -1755,6 +1814,54 @@ TEST_F(
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->value().ToString(), values[target_idx]);
|
||||
}
|
||||
|
||||
TEST_F(DBBlobDirectWriteTest, DirectWriteDelayedReadUsesFinalizedFileChecksum) {
|
||||
auto capturing_fs = std::make_shared<BlobFileChecksumCapturingFileSystem>(
|
||||
env_->GetFileSystem());
|
||||
std::unique_ptr<Env> env(new CompositeEnvWrapper(env_, capturing_fs));
|
||||
|
||||
Options options = GetDirectWriteOptions();
|
||||
options.env = env.get();
|
||||
options.file_checksum_gen_factory = GetFileChecksumGenCrc32cFactory();
|
||||
Defer close_db([this]() { Close(); });
|
||||
Reopen(options);
|
||||
|
||||
const std::string key = "delayed-read-key";
|
||||
const std::string value(96, 'v');
|
||||
ASSERT_OK(Put(key, value));
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.allow_unprepared_value = true;
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
iter->Seek(key);
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key(), key);
|
||||
ASSERT_TRUE(iter->value().empty());
|
||||
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
std::vector<ColumnFamilyMetaData> column_family_metadata;
|
||||
db_->GetAllColumnFamilyMetaData(&column_family_metadata);
|
||||
ASSERT_EQ(column_family_metadata.size(), 1);
|
||||
ASSERT_EQ(column_family_metadata[0].blob_files.size(), 1);
|
||||
const BlobMetaData& blob_metadata = column_family_metadata[0].blob_files[0];
|
||||
ASSERT_FALSE(blob_metadata.checksum_value.empty());
|
||||
ASSERT_EQ(blob_metadata.checksum_method, "FileChecksumCrc32c");
|
||||
|
||||
capturing_fs->Reset();
|
||||
ASSERT_TRUE(iter->PrepareValue());
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->value().ToString(), value);
|
||||
ASSERT_GT(capturing_fs->GetCaptureCount(), 0);
|
||||
ASSERT_EQ(capturing_fs->GetFileChecksum(), blob_metadata.checksum_value);
|
||||
ASSERT_EQ(capturing_fs->GetFileChecksumFuncName(),
|
||||
blob_metadata.checksum_method);
|
||||
|
||||
const int capture_count = capturing_fs->GetCaptureCount();
|
||||
ASSERT_EQ(Get(key), value);
|
||||
ASSERT_EQ(capturing_fs->GetCaptureCount(), capture_count);
|
||||
}
|
||||
|
||||
TEST_F(DBBlobDirectWriteTest, OrderedTraceUsesLogicalBatchForBlobDirectWrite) {
|
||||
AssertOrderedTraceStoresLogicalPut(GetDirectWriteOptions(),
|
||||
/*expect_blob_files=*/true);
|
||||
@@ -1772,6 +1879,131 @@ TEST_F(DBBlobDirectWriteTest,
|
||||
AssertOrderedTraceStoresLogicalPut(options, /*expect_blob_files=*/false);
|
||||
}
|
||||
|
||||
TEST_F(DBBlobDirectWriteTest, OverlappingFlushesPublishTheirOwnBlobFiles) {
|
||||
for (bool atomic_flush : {false, true}) {
|
||||
SCOPED_TRACE("atomic_flush=" + std::to_string(atomic_flush));
|
||||
Options options = GetDirectWriteOptions();
|
||||
options.atomic_flush = atomic_flush;
|
||||
options.max_background_jobs = 8;
|
||||
options.max_write_buffer_number = 4;
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
const std::string value(1024, 'v');
|
||||
|
||||
// The flush of the memtable holding "key1" waits while it writes its
|
||||
// table file, until a second flush, of the memtable holding "key2", has
|
||||
// finished its own table file.
|
||||
std::atomic<int> table_writes{0};
|
||||
std::atomic<bool> released{false};
|
||||
std::promise<void> first_flush_writing;
|
||||
std::promise<void> release_first_flush;
|
||||
std::shared_future<void> release_first_flush_future =
|
||||
release_first_flush.get_future().share();
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"FlushJob::WriteLevel0Table:num_memtables", [&](void*) {
|
||||
if (table_writes.fetch_add(1) == 0) {
|
||||
first_flush_writing.set_value();
|
||||
release_first_flush_future.wait();
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
atomic_flush ? "DBImpl::AtomicFlushMemTablesToOutputFiles:WaitCV"
|
||||
: "DBImpl::FlushMemTableToOutputFile:Finish",
|
||||
[&](void*) {
|
||||
if (!released.exchange(true)) {
|
||||
release_first_flush.set_value();
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
FlushOptions no_wait;
|
||||
no_wait.wait = false;
|
||||
no_wait.allow_write_stall = true;
|
||||
ASSERT_OK(Put("key1", value));
|
||||
ASSERT_OK(db_->Flush(no_wait));
|
||||
first_flush_writing.get_future().wait();
|
||||
ASSERT_OK(Put("key2", value));
|
||||
ASSERT_OK(db_->Flush(no_wait));
|
||||
|
||||
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
ASSERT_OK(dbfull()->TEST_GetBGError());
|
||||
ASSERT_EQ(2, NumTableFilesAtLevel(0));
|
||||
|
||||
Reopen(options);
|
||||
ASSERT_EQ(value, Get("key1"));
|
||||
ASSERT_EQ(value, Get("key2"));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBBlobDirectWriteTest, RetryAfterOlderOverlappingFlushFails) {
|
||||
Options options = GetDirectWriteOptions();
|
||||
options.max_background_jobs = 8;
|
||||
options.max_write_buffer_number = 4;
|
||||
options.disable_auto_compactions = true;
|
||||
options.max_bgerror_resume_count = 0;
|
||||
DestroyAndReopen(options);
|
||||
const std::string value(1024, 'v');
|
||||
|
||||
// The flush of the memtable holding "key2" finishes while the older flush,
|
||||
// of the memtable holding "key1", is still writing its table file. The
|
||||
// newer flush's results wait to be installed after the older one's, but the
|
||||
// older flush fails, which rolls the newer memtable back too.
|
||||
std::atomic<int> table_writes{0};
|
||||
std::atomic<bool> released{false};
|
||||
std::atomic<bool> failed{false};
|
||||
std::promise<void> first_flush_writing;
|
||||
std::promise<void> release_first_flush;
|
||||
std::shared_future<void> release_first_flush_future =
|
||||
release_first_flush.get_future().share();
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"FlushJob::WriteLevel0Table:num_memtables", [&](void*) {
|
||||
if (table_writes.fetch_add(1) == 0) {
|
||||
first_flush_writing.set_value();
|
||||
release_first_flush_future.wait();
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::FlushMemTableToOutputFile:Finish", [&](void*) {
|
||||
if (!released.exchange(true)) {
|
||||
release_first_flush.set_value();
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"FlushJob::WriteLevel0Table:s", [&](void* arg) {
|
||||
if (released.load() && !failed.exchange(true)) {
|
||||
IOStatus injected = IOStatus::IOError("injected");
|
||||
injected.SetRetryable(true);
|
||||
*static_cast<Status*>(arg) = injected;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
FlushOptions no_wait;
|
||||
no_wait.wait = false;
|
||||
no_wait.allow_write_stall = true;
|
||||
ASSERT_OK(Put("key1", value));
|
||||
ASSERT_OK(db_->Flush(no_wait));
|
||||
first_flush_writing.get_future().wait();
|
||||
ASSERT_OK(Put("key2", value));
|
||||
ASSERT_OK(db_->Flush(no_wait));
|
||||
|
||||
ASSERT_NOK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
ASSERT_TRUE(failed.load());
|
||||
|
||||
// Retrying the flush must still find the blob files of both memtables.
|
||||
ASSERT_OK(dbfull()->Resume());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
ASSERT_OK(dbfull()->TEST_GetBGError());
|
||||
|
||||
Reopen(options);
|
||||
ASSERT_EQ(value, Get("key1"));
|
||||
ASSERT_EQ(value, Get("key2"));
|
||||
}
|
||||
|
||||
class DBBlobDirectWriteWithTimestampTest : public DBBasicTestWithTimestampBase {
|
||||
protected:
|
||||
DBBlobDirectWriteWithTimestampTest()
|
||||
|
||||
@@ -1032,6 +1032,53 @@ TEST_F(DBBlobIndexTest, EmbeddedBlobWideColumnGetEntityBlockCacheTier) {
|
||||
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
|
||||
}
|
||||
|
||||
// Companion to the test above (genuine miss -> Incomplete): with a blob cache
|
||||
// configured, a warm read populates the embedded record into the blob cache, so
|
||||
// a subsequent kBlockCacheTier GetEntity serves it from cache instead of
|
||||
// returning Incomplete. This exercises the non-lazy Get/GetEntity embedded path
|
||||
// (get_context -> EmbeddedAwareBlobFetcher -> GetSameFileBlob whole ->
|
||||
// ResolveEmbeddedBlobCached), which shares the block-cache-tier behavior with
|
||||
// the lazy resolver (a same-file column already in the blob cache is served).
|
||||
TEST_F(DBBlobIndexTest,
|
||||
EmbeddedBlobWideColumnGetEntityBlockCacheTierServesCached) {
|
||||
Options options = GetTestOptions();
|
||||
options.create_if_missing = true;
|
||||
LRUCacheOptions co;
|
||||
co.capacity = 8 << 20;
|
||||
options.blob_cache = NewLRUCache(co);
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string sst_path = dbname_ + "/embedded_wc_blockcache_hit.sst";
|
||||
SstFileWriterEmbeddedBlobOptions embedded_blob_options;
|
||||
embedded_blob_options.min_blob_size = 8;
|
||||
|
||||
const std::string big1(1024, 'a');
|
||||
const WideColumns columns{{kDefaultWideColumnName, "d"}, {"big", big1}};
|
||||
|
||||
SstFileWriter writer(EnvOptions(), options);
|
||||
ASSERT_OK(writer.OpenWithEmbeddedBlobs(sst_path, embedded_blob_options));
|
||||
ASSERT_OK(writer.PutEntity("k", columns));
|
||||
ASSERT_OK(writer.Finish());
|
||||
ASSERT_OK(db_->IngestExternalFile({sst_path}, IngestExternalFileOptions()));
|
||||
|
||||
// Warm both the block cache (data block) and the blob cache (embedded record)
|
||||
// with a full read.
|
||||
{
|
||||
PinnableWideColumns result;
|
||||
ASSERT_OK(db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), "k",
|
||||
&result));
|
||||
ASSERT_EQ(result.columns(), columns);
|
||||
}
|
||||
|
||||
// The embedded record is now cached, so a block-cache-only read serves it.
|
||||
ReadOptions read_options;
|
||||
read_options.read_tier = kBlockCacheTier;
|
||||
PinnableWideColumns result;
|
||||
ASSERT_OK(
|
||||
db_->GetEntity(read_options, db_->DefaultColumnFamily(), "k", &result));
|
||||
ASSERT_EQ(result.columns(), columns);
|
||||
}
|
||||
|
||||
// A corrupt embedded blob record backing a wide-column column must surface an
|
||||
// error on GetEntity, not expose the raw same-file BlobIndex.
|
||||
// A failure resolving an embedded blob record backing a wide-column column
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "db/blob/blob_constants.h"
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
#include "rocksdb/status.h"
|
||||
|
||||
@@ -17,6 +18,18 @@ class BlobIndex;
|
||||
class PinnableSlice;
|
||||
struct ReadOptions;
|
||||
|
||||
// One request for SameFileBlobReader::MultiGetSameFileBlob (the batched
|
||||
// counterpart of GetSameFileBlob). See GetSameFileBlob for the per-field
|
||||
// semantics (range_length == kWholeBlobLength selects the whole value).
|
||||
struct SameFileBlobReadRequest {
|
||||
const BlobIndex* blob_index = nullptr;
|
||||
uint64_t range_offset = 0;
|
||||
size_t range_length = kWholeBlobLength;
|
||||
BlobVerifyPolicy verify_policy = BlobVerifyPolicy::kVerifyIfNoAmplification;
|
||||
PinnableSlice* result = nullptr;
|
||||
Status* status = nullptr;
|
||||
};
|
||||
|
||||
// Narrow interface for resolving a same-file ("embedded") blob reference
|
||||
// against the physical file that currently holds the table entry. Implemented
|
||||
// by BlockBasedTable, which owns the reader context (file, footer, blob cache)
|
||||
@@ -30,28 +43,48 @@ class SameFileBlobReader {
|
||||
public:
|
||||
virtual ~SameFileBlobReader() = default;
|
||||
|
||||
// Resolves a same-file blob reference (one for which blob_index.IsSameFile()
|
||||
// is true) into `value`, pinning the payload zero-copy (from the blob cache
|
||||
// or an owned buffer). read_tier == kBlockCacheTier without a cached record
|
||||
// yields Status::Incomplete; a bad record yields Status::Corruption.
|
||||
// Resolves a same-file ("embedded") blob reference (one for which
|
||||
// blob_index.IsSameFile() is true) into `value`, pinning the payload
|
||||
// zero-copy (from the blob cache or an owned buffer).
|
||||
//
|
||||
// range_length == kWholeBlobLength resolves the whole value. Any other length
|
||||
// resolves only the sub-range [range_offset, range_offset + range_length),
|
||||
// reading just those bytes on a cache miss -- skipping whole-record checksum
|
||||
// verification (a strict sub-range cannot cover it) and never populating the
|
||||
// blob cache; valid only for an uncompressed record and a verify_policy other
|
||||
// than kVerifyIfPresent, with range_offset + range_length <=
|
||||
// blob_index.size().
|
||||
//
|
||||
// verify_policy controls whole-record checksum verification (see
|
||||
// BlobVerifyPolicy); a whole read under kVerifyIfPresent verifies even when
|
||||
// ReadOptions::verify_checksums is off. read_tier == kBlockCacheTier without
|
||||
// a cached record yields Status::Incomplete; a bad record yields
|
||||
// Status::Corruption.
|
||||
virtual Status GetSameFileBlob(const ReadOptions& read_options,
|
||||
const BlobIndex& blob_index,
|
||||
uint64_t range_offset, size_t range_length,
|
||||
BlobVerifyPolicy verify_policy,
|
||||
PinnableSlice* value) const = 0;
|
||||
|
||||
// Resolves a byte sub-range [range_offset, range_offset + range_length) of an
|
||||
// *uncompressed* same-file blob reference into `value`, reading only those
|
||||
// bytes on a cache miss (the embedded counterpart of a separate-file blob
|
||||
// range read). Pins the payload zero-copy (a sliced cached record, or an
|
||||
// owned sub-range buffer). Unlike GetSameFileBlob it skips whole-record
|
||||
// checksum verification (a strict sub-range cannot cover it) and never
|
||||
// populates the blob cache. The caller must ensure range_offset +
|
||||
// range_length <= blob_index.size(). read_tier == kBlockCacheTier without a
|
||||
// cached record yields Status::Incomplete.
|
||||
virtual Status GetSameFileBlobRange(const ReadOptions& read_options,
|
||||
const BlobIndex& blob_index,
|
||||
uint64_t range_offset,
|
||||
size_t range_length,
|
||||
PinnableSlice* value) const = 0;
|
||||
// Batched counterpart of GetSameFileBlob: resolves `num_reads` same-file blob
|
||||
// references (whole or sub-range, mixed) against this reader, writing each
|
||||
// outcome to *reqs[i].status. Implementations may coalesce the underlying
|
||||
// disk reads into a single MultiRead. The default implementation simply loops
|
||||
// over GetSameFileBlob (correct, but not coalesced); BlockBasedTable
|
||||
// overrides it to coalesce reads of embedded records in the same SST.
|
||||
virtual void MultiGetSameFileBlob(const ReadOptions& read_options,
|
||||
size_t num_reads,
|
||||
SameFileBlobReadRequest* reqs) const {
|
||||
for (size_t i = 0; i < num_reads; ++i) {
|
||||
SameFileBlobReadRequest& req = reqs[i];
|
||||
const Status s =
|
||||
GetSameFileBlob(read_options, *req.blob_index, req.range_offset,
|
||||
req.range_length, req.verify_policy, req.result);
|
||||
if (req.status) {
|
||||
*req.status = s;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
+5
-3
@@ -207,7 +207,8 @@ Status BuildTable(
|
||||
std::move(file), fname, file_options, ioptions.clock, io_tracer,
|
||||
ioptions.stats, Histograms::SST_WRITE_MICROS, ioptions.listeners,
|
||||
ioptions.file_checksum_gen_factory.get(),
|
||||
tmp_set.Contains(FileType::kTableFile), false));
|
||||
tmp_set.Contains(FileType::kTableFile),
|
||||
tmp_set.Contains(FileType::kTableFile)));
|
||||
|
||||
builder = NewTableBuilder(tboptions, file_writer.get());
|
||||
}
|
||||
@@ -395,7 +396,7 @@ Status BuildTable(
|
||||
relevant_mapping,
|
||||
ioptions.compaction_style == CompactionStyle::kCompactionStyleFIFO
|
||||
? meta->file_creation_time
|
||||
: meta->oldest_ancester_time);
|
||||
: meta->oldest_ancestor_time);
|
||||
s = builder->Finish();
|
||||
}
|
||||
if (io_status->ok()) {
|
||||
@@ -451,7 +452,8 @@ Status BuildTable(
|
||||
WritableFileWriter::PrepareIOOptions(tboptions.write_options, opts);
|
||||
if (s.ok() && io_status->ok() && !empty) {
|
||||
StopWatch sw(ioptions.clock, ioptions.stats, TABLE_SYNC_MICROS);
|
||||
*io_status = file_writer->Sync(opts, ioptions.use_fsync);
|
||||
*io_status = ioptions.use_fsync ? file_writer->Fsync(opts)
|
||||
: file_writer->Sync(opts);
|
||||
}
|
||||
TEST_SYNC_POINT("BuildTable:BeforeCloseTableFile");
|
||||
if (s.ok() && io_status->ok() && !empty) {
|
||||
|
||||
@@ -195,6 +195,7 @@ using ROCKSDB_NAMESPACE::WALRecoveryMode;
|
||||
using ROCKSDB_NAMESPACE::WritableFile;
|
||||
using ROCKSDB_NAMESPACE::WriteBatch;
|
||||
using ROCKSDB_NAMESPACE::WriteBatchWithIndex;
|
||||
using ROCKSDB_NAMESPACE::WriteBufferFlushPolicy;
|
||||
using ROCKSDB_NAMESPACE::WriteBufferManager;
|
||||
using ROCKSDB_NAMESPACE::WriteOptions;
|
||||
using ROCKSDB_NAMESPACE::WriteStallCondition;
|
||||
@@ -418,6 +419,13 @@ struct rocksdb_readoptions_t {
|
||||
std::function<bool(const TableProperties&)> table_filter_func;
|
||||
std::shared_ptr<UserDefinedIndexFactory> table_index_factory;
|
||||
|
||||
rocksdb_readoptions_t() = default;
|
||||
// No copying allowed
|
||||
rocksdb_readoptions_t(const rocksdb_readoptions_t&) = delete;
|
||||
rocksdb_readoptions_t& operator=(const rocksdb_readoptions_t&) = delete;
|
||||
rocksdb_readoptions_t(rocksdb_readoptions_t&&) = delete;
|
||||
rocksdb_readoptions_t& operator=(rocksdb_readoptions_t&&) = delete;
|
||||
|
||||
~rocksdb_readoptions_t() { ClearTableFilter(); }
|
||||
|
||||
void ClearTableFilter() {
|
||||
@@ -804,14 +812,22 @@ struct rocksdb_open_and_compact_options_t {
|
||||
};
|
||||
|
||||
struct rocksdb_compactionfilter_t : public CompactionFilter {
|
||||
void* state_;
|
||||
void (*destructor_)(void*);
|
||||
void* state_ = nullptr;
|
||||
void (*destructor_)(void*) = nullptr;
|
||||
unsigned char (*filter_)(void*, int level, const char* key, size_t key_length,
|
||||
const char* existing_value, size_t value_length,
|
||||
char** new_value, size_t* new_value_length,
|
||||
unsigned char* value_changed);
|
||||
const char* (*name_)(void*);
|
||||
unsigned char ignore_snapshots_;
|
||||
unsigned char* value_changed) = nullptr;
|
||||
const char* (*name_)(void*) = nullptr;
|
||||
unsigned char ignore_snapshots_ = true;
|
||||
|
||||
rocksdb_compactionfilter_t() = default;
|
||||
// No copying allowed
|
||||
rocksdb_compactionfilter_t(const rocksdb_compactionfilter_t&) = delete;
|
||||
rocksdb_compactionfilter_t& operator=(const rocksdb_compactionfilter_t&) =
|
||||
delete;
|
||||
rocksdb_compactionfilter_t(rocksdb_compactionfilter_t&&) = delete;
|
||||
rocksdb_compactionfilter_t& operator=(rocksdb_compactionfilter_t&&) = delete;
|
||||
|
||||
~rocksdb_compactionfilter_t() override { (*destructor_)(state_); }
|
||||
|
||||
@@ -837,11 +853,22 @@ struct rocksdb_compactionfilter_t : public CompactionFilter {
|
||||
};
|
||||
|
||||
struct rocksdb_compactionfilterfactory_t : public CompactionFilterFactory {
|
||||
void* state_;
|
||||
void (*destructor_)(void*);
|
||||
void* state_ = nullptr;
|
||||
void (*destructor_)(void*) = nullptr;
|
||||
rocksdb_compactionfilter_t* (*create_compaction_filter_)(
|
||||
void*, rocksdb_compactionfiltercontext_t* context);
|
||||
const char* (*name_)(void*);
|
||||
void*, rocksdb_compactionfiltercontext_t* context) = nullptr;
|
||||
const char* (*name_)(void*) = nullptr;
|
||||
|
||||
rocksdb_compactionfilterfactory_t() = default;
|
||||
// No copying allowed
|
||||
rocksdb_compactionfilterfactory_t(const rocksdb_compactionfilterfactory_t&) =
|
||||
delete;
|
||||
rocksdb_compactionfilterfactory_t& operator=(
|
||||
const rocksdb_compactionfilterfactory_t&) = delete;
|
||||
rocksdb_compactionfilterfactory_t(rocksdb_compactionfilterfactory_t&&) =
|
||||
delete;
|
||||
rocksdb_compactionfilterfactory_t& operator=(
|
||||
rocksdb_compactionfilterfactory_t&&) = delete;
|
||||
|
||||
~rocksdb_compactionfilterfactory_t() override { (*destructor_)(state_); }
|
||||
|
||||
@@ -872,6 +899,12 @@ struct rocksdb_comparator_t : public Comparator {
|
||||
|
||||
rocksdb_comparator_t(size_t ts_size) : Comparator(ts_size) {}
|
||||
|
||||
// No copying allowed
|
||||
rocksdb_comparator_t(const rocksdb_comparator_t&) = delete;
|
||||
rocksdb_comparator_t& operator=(const rocksdb_comparator_t&) = delete;
|
||||
rocksdb_comparator_t(rocksdb_comparator_t&&) = delete;
|
||||
rocksdb_comparator_t& operator=(rocksdb_comparator_t&&) = delete;
|
||||
|
||||
~rocksdb_comparator_t() override { (*destructor_)(state_); }
|
||||
|
||||
int Compare(const Slice& a, const Slice& b) const override {
|
||||
@@ -903,9 +936,16 @@ struct rocksdb_comparator_t : public Comparator {
|
||||
};
|
||||
|
||||
struct rocksdb_filterpolicy_t : public FilterPolicy {
|
||||
void* state_;
|
||||
void (*destructor_)(void*);
|
||||
const char* (*name_)(void*);
|
||||
void* state_ = nullptr;
|
||||
void (*destructor_)(void*) = nullptr;
|
||||
const char* (*name_)(void*) = nullptr;
|
||||
|
||||
rocksdb_filterpolicy_t() = default;
|
||||
// No copying allowed
|
||||
rocksdb_filterpolicy_t(const rocksdb_filterpolicy_t&) = delete;
|
||||
rocksdb_filterpolicy_t& operator=(const rocksdb_filterpolicy_t&) = delete;
|
||||
rocksdb_filterpolicy_t(rocksdb_filterpolicy_t&&) = delete;
|
||||
rocksdb_filterpolicy_t& operator=(rocksdb_filterpolicy_t&&) = delete;
|
||||
|
||||
~rocksdb_filterpolicy_t() override { (*destructor_)(state_); }
|
||||
|
||||
@@ -913,19 +953,29 @@ struct rocksdb_filterpolicy_t : public FilterPolicy {
|
||||
};
|
||||
|
||||
struct rocksdb_mergeoperator_t : public MergeOperator {
|
||||
void* state_;
|
||||
void (*destructor_)(void*);
|
||||
const char* (*name_)(void*);
|
||||
void* state_ = nullptr;
|
||||
void (*destructor_)(void*) = nullptr;
|
||||
const char* (*name_)(void*) = nullptr;
|
||||
char* (*full_merge_)(void*, const char* key, size_t key_length,
|
||||
const char* existing_value, size_t existing_value_length,
|
||||
const char* const* operands_list,
|
||||
const size_t* operands_list_length, int num_operands,
|
||||
unsigned char* success, size_t* new_value_length);
|
||||
unsigned char* success,
|
||||
size_t* new_value_length) = nullptr;
|
||||
char* (*partial_merge_)(void*, const char* key, size_t key_length,
|
||||
const char* const* operands_list,
|
||||
const size_t* operands_list_length, int num_operands,
|
||||
unsigned char* success, size_t* new_value_length);
|
||||
void (*delete_value_)(void*, const char* value, size_t value_length);
|
||||
unsigned char* success,
|
||||
size_t* new_value_length) = nullptr;
|
||||
void (*delete_value_)(void*, const char* value,
|
||||
size_t value_length) = nullptr;
|
||||
|
||||
rocksdb_mergeoperator_t() = default;
|
||||
// No copying allowed
|
||||
rocksdb_mergeoperator_t(const rocksdb_mergeoperator_t&) = delete;
|
||||
rocksdb_mergeoperator_t& operator=(const rocksdb_mergeoperator_t&) = delete;
|
||||
rocksdb_mergeoperator_t(rocksdb_mergeoperator_t&&) = delete;
|
||||
rocksdb_mergeoperator_t& operator=(rocksdb_mergeoperator_t&&) = delete;
|
||||
|
||||
~rocksdb_mergeoperator_t() override { (*destructor_)(state_); }
|
||||
|
||||
@@ -1007,12 +1057,19 @@ struct rocksdb_env_t {
|
||||
};
|
||||
|
||||
struct rocksdb_slicetransform_t : public SliceTransform {
|
||||
void* state_;
|
||||
void (*destructor_)(void*);
|
||||
const char* (*name_)(void*);
|
||||
void* state_ = nullptr;
|
||||
void (*destructor_)(void*) = nullptr;
|
||||
const char* (*name_)(void*) = nullptr;
|
||||
char* (*transform_)(void*, const char* key, size_t length,
|
||||
size_t* dst_length);
|
||||
unsigned char (*in_domain_)(void*, const char* key, size_t length);
|
||||
size_t* dst_length) = nullptr;
|
||||
unsigned char (*in_domain_)(void*, const char* key, size_t length) = nullptr;
|
||||
|
||||
rocksdb_slicetransform_t() = default;
|
||||
// No copying allowed
|
||||
rocksdb_slicetransform_t(const rocksdb_slicetransform_t&) = delete;
|
||||
rocksdb_slicetransform_t& operator=(const rocksdb_slicetransform_t&) = delete;
|
||||
rocksdb_slicetransform_t(rocksdb_slicetransform_t&&) = delete;
|
||||
rocksdb_slicetransform_t& operator=(rocksdb_slicetransform_t&&) = delete;
|
||||
|
||||
~rocksdb_slicetransform_t() override { (*destructor_)(state_); }
|
||||
|
||||
@@ -1336,6 +1393,14 @@ struct rocksdb_compactionservice_t : public CompactionService {
|
||||
cancel_awaiting_jobs_(cancel_awaiting_jobs),
|
||||
on_installation_(on_installation) {}
|
||||
|
||||
// No copying allowed
|
||||
rocksdb_compactionservice_t(const rocksdb_compactionservice_t&) = delete;
|
||||
rocksdb_compactionservice_t& operator=(const rocksdb_compactionservice_t&) =
|
||||
delete;
|
||||
rocksdb_compactionservice_t(rocksdb_compactionservice_t&&) = delete;
|
||||
rocksdb_compactionservice_t& operator=(rocksdb_compactionservice_t&&) =
|
||||
delete;
|
||||
|
||||
~rocksdb_compactionservice_t() override {
|
||||
if (destructor_) {
|
||||
(*destructor_)(state_);
|
||||
@@ -2623,6 +2688,59 @@ char* rocksdb_get(rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
return result;
|
||||
}
|
||||
|
||||
static char* rocksdb_get_with_metadata_impl(
|
||||
rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
ColumnFamilyHandle* column_family, const char* key, size_t keylen,
|
||||
size_t* vallen, char** timestamp, size_t* timestamp_len,
|
||||
unsigned char* newer_version_present, char** errptr) {
|
||||
ROCKSDB_NAMESPACE::OutputMetadata output_metadata;
|
||||
if (timestamp != nullptr && timestamp_len != nullptr) {
|
||||
output_metadata.WantTimestamp();
|
||||
}
|
||||
if (newer_version_present != nullptr) {
|
||||
output_metadata.WantNewerVersionPresent();
|
||||
}
|
||||
|
||||
PinnableSlice pinnable_val;
|
||||
const Status s =
|
||||
db->rep->GetWithMetadata(options->rep, column_family, Slice(key, keylen),
|
||||
&pinnable_val, &output_metadata);
|
||||
if (newer_version_present != nullptr) {
|
||||
*newer_version_present = *output_metadata.newer_version_present ? 1 : 0;
|
||||
}
|
||||
|
||||
char* result = nullptr;
|
||||
if (s.ok()) {
|
||||
*vallen = pinnable_val.size();
|
||||
result = CopyString(pinnable_val);
|
||||
if (output_metadata.timestamp.has_value()) {
|
||||
*timestamp_len = output_metadata.timestamp->size();
|
||||
*timestamp = CopyString(*output_metadata.timestamp);
|
||||
}
|
||||
} else {
|
||||
*vallen = 0;
|
||||
if (output_metadata.timestamp.has_value()) {
|
||||
*timestamp = nullptr;
|
||||
*timestamp_len = 0;
|
||||
}
|
||||
if (!s.IsNotFound()) {
|
||||
SaveError(errptr, s);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
char* rocksdb_get_with_metadata(rocksdb_t* db,
|
||||
const rocksdb_readoptions_t* options,
|
||||
const char* key, size_t keylen, size_t* vallen,
|
||||
char** timestamp, size_t* timestamp_len,
|
||||
unsigned char* newer_version_present,
|
||||
char** errptr) {
|
||||
return rocksdb_get_with_metadata_impl(
|
||||
db, options, db->rep->DefaultColumnFamily(), key, keylen, vallen,
|
||||
timestamp, timestamp_len, newer_version_present, errptr);
|
||||
}
|
||||
|
||||
char* rocksdb_get_cf(rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
rocksdb_column_family_handle_t* column_family,
|
||||
const char* key, size_t keylen, size_t* vallen,
|
||||
@@ -2645,6 +2763,16 @@ char* rocksdb_get_cf(rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
return result;
|
||||
}
|
||||
|
||||
char* rocksdb_get_cf_with_metadata(
|
||||
rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
rocksdb_column_family_handle_t* column_family, const char* key,
|
||||
size_t keylen, size_t* vallen, char** timestamp, size_t* timestamp_len,
|
||||
unsigned char* newer_version_present, char** errptr) {
|
||||
return rocksdb_get_with_metadata_impl(
|
||||
db, options, column_family->rep, key, keylen, vallen, timestamp,
|
||||
timestamp_len, newer_version_present, errptr);
|
||||
}
|
||||
|
||||
char* rocksdb_get_with_ts(rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
const char* key, size_t keylen, size_t* vallen,
|
||||
char** ts, size_t* tslen, char** errptr) {
|
||||
@@ -2736,6 +2864,72 @@ void rocksdb_multi_get(rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
}
|
||||
}
|
||||
|
||||
static void rocksdb_multi_get_with_metadata_impl(
|
||||
rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
ColumnFamilyHandle* const* column_families, size_t num_keys,
|
||||
const char* const* keys_list, const size_t* keys_list_sizes,
|
||||
char** values_list, size_t* values_list_sizes, char** timestamp_list,
|
||||
size_t* timestamp_list_sizes, unsigned char* newer_version_present,
|
||||
char** errs) {
|
||||
std::unique_ptr<Slice[]> keys(new Slice[num_keys]);
|
||||
std::vector<PinnableSlice> values(num_keys);
|
||||
std::vector<Status> statuses(num_keys);
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
keys[i] = Slice(keys_list[i], keys_list_sizes[i]);
|
||||
}
|
||||
|
||||
ROCKSDB_NAMESPACE::MultiGetOutputMetadata output_metadata;
|
||||
if (timestamp_list != nullptr && timestamp_list_sizes != nullptr) {
|
||||
output_metadata.WantTimestamps();
|
||||
}
|
||||
if (newer_version_present != nullptr) {
|
||||
output_metadata.WantNewerVersionPresent();
|
||||
}
|
||||
db->rep->MultiGetWithMetadata(options->rep, num_keys, column_families,
|
||||
keys.get(), values.data(), statuses.data(),
|
||||
&output_metadata);
|
||||
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
if (newer_version_present != nullptr) {
|
||||
newer_version_present[i] =
|
||||
(*output_metadata.newer_version_present)[i] ? 1 : 0;
|
||||
}
|
||||
if (statuses[i].ok()) {
|
||||
values_list[i] = CopyString(values[i]);
|
||||
values_list_sizes[i] = values[i].size();
|
||||
if (output_metadata.timestamps.has_value()) {
|
||||
timestamp_list[i] = CopyString((*output_metadata.timestamps)[i]);
|
||||
timestamp_list_sizes[i] = (*output_metadata.timestamps)[i].size();
|
||||
}
|
||||
errs[i] = nullptr;
|
||||
} else {
|
||||
values_list[i] = nullptr;
|
||||
values_list_sizes[i] = 0;
|
||||
if (output_metadata.timestamps.has_value()) {
|
||||
timestamp_list[i] = nullptr;
|
||||
timestamp_list_sizes[i] = 0;
|
||||
}
|
||||
errs[i] = statuses[i].IsNotFound()
|
||||
? nullptr
|
||||
: strdup(statuses[i].ToString().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_multi_get_with_metadata(
|
||||
rocksdb_t* db, const rocksdb_readoptions_t* options, size_t num_keys,
|
||||
const char* const* keys_list, const size_t* keys_list_sizes,
|
||||
char** values_list, size_t* values_list_sizes, char** timestamp_list,
|
||||
size_t* timestamp_list_sizes, unsigned char* newer_version_present,
|
||||
char** errs) {
|
||||
std::vector<ColumnFamilyHandle*> column_families(
|
||||
num_keys, db->rep->DefaultColumnFamily());
|
||||
rocksdb_multi_get_with_metadata_impl(
|
||||
db, options, column_families.data(), num_keys, keys_list, keys_list_sizes,
|
||||
values_list, values_list_sizes, timestamp_list, timestamp_list_sizes,
|
||||
newer_version_present, errs);
|
||||
}
|
||||
|
||||
void rocksdb_multi_get_with_ts(rocksdb_t* db,
|
||||
const rocksdb_readoptions_t* options,
|
||||
size_t num_keys, const char* const* keys_list,
|
||||
@@ -2811,6 +3005,24 @@ void rocksdb_multi_get_cf(
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_multi_get_cf_with_metadata(
|
||||
rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
const rocksdb_column_family_handle_t* const* column_families,
|
||||
size_t num_keys, const char* const* keys_list,
|
||||
const size_t* keys_list_sizes, char** values_list,
|
||||
size_t* values_list_sizes, char** timestamp_list,
|
||||
size_t* timestamp_list_sizes, unsigned char* newer_version_present,
|
||||
char** errs) {
|
||||
std::vector<ColumnFamilyHandle*> cfs(num_keys);
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
cfs[i] = column_families[i]->rep;
|
||||
}
|
||||
rocksdb_multi_get_with_metadata_impl(
|
||||
db, options, cfs.data(), num_keys, keys_list, keys_list_sizes,
|
||||
values_list, values_list_sizes, timestamp_list, timestamp_list_sizes,
|
||||
newer_version_present, errs);
|
||||
}
|
||||
|
||||
void rocksdb_multi_get_cf_with_ts(
|
||||
rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
const rocksdb_column_family_handle_t* const* column_families,
|
||||
@@ -5050,6 +5262,11 @@ uint64_t rocksdb_table_properties_column_family_id(
|
||||
return props->rep.column_family_id;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_table_properties_oldest_ancestor_time(
|
||||
const rocksdb_table_properties_t* props) {
|
||||
return props->rep.oldest_ancestor_time;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_table_properties_creation_time(
|
||||
const rocksdb_table_properties_t* props) {
|
||||
return props->rep.creation_time;
|
||||
@@ -5080,11 +5297,6 @@ uint64_t rocksdb_table_properties_fast_compression_estimated_data_size(
|
||||
return props->rep.fast_compression_estimated_data_size;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_table_properties_external_sst_file_global_seqno_offset(
|
||||
const rocksdb_table_properties_t* props) {
|
||||
return props->rep.external_sst_file_global_seqno_offset;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_table_properties_tail_start_offset(
|
||||
const rocksdb_table_properties_t* props) {
|
||||
return props->rep.tail_start_offset;
|
||||
@@ -5120,6 +5332,11 @@ uint64_t rocksdb_table_properties_separate_key_value_in_data_block(
|
||||
return props->rep.separate_key_value_in_data_block;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_table_properties_lsm_info_at_creation(
|
||||
const rocksdb_table_properties_t* props) {
|
||||
return props->rep.lsm_info_at_creation;
|
||||
}
|
||||
|
||||
const char* rocksdb_table_properties_db_id(
|
||||
const rocksdb_table_properties_t* props, size_t* size) {
|
||||
*size = props->rep.db_id.size();
|
||||
@@ -6810,7 +7027,12 @@ rocksdb_filterpolicy_t* rocksdb_filterpolicy_create_bloom_format(
|
||||
// they delegate to a NewBloomFilterPolicy() instead of user
|
||||
// supplied C functions.
|
||||
struct Wrapper : public rocksdb_filterpolicy_t {
|
||||
const FilterPolicy* rep_;
|
||||
const FilterPolicy* rep_ = nullptr;
|
||||
Wrapper() = default;
|
||||
Wrapper(const Wrapper&) = delete;
|
||||
Wrapper& operator=(const Wrapper&) = delete;
|
||||
Wrapper(Wrapper&&) = delete;
|
||||
Wrapper& operator=(Wrapper&&) = delete;
|
||||
~Wrapper() override { delete rep_; }
|
||||
const char* Name() const override { return rep_->Name(); }
|
||||
const char* CompatibilityName() const override {
|
||||
@@ -6850,7 +7072,12 @@ rocksdb_filterpolicy_t* rocksdb_filterpolicy_create_ribbon_format(
|
||||
// they delegate to a NewRibbonFilterPolicy() instead of user
|
||||
// supplied C functions.
|
||||
struct Wrapper : public rocksdb_filterpolicy_t {
|
||||
const FilterPolicy* rep_;
|
||||
const FilterPolicy* rep_ = nullptr;
|
||||
Wrapper() = default;
|
||||
Wrapper(const Wrapper&) = delete;
|
||||
Wrapper& operator=(const Wrapper&) = delete;
|
||||
Wrapper(Wrapper&&) = delete;
|
||||
Wrapper& operator=(Wrapper&&) = delete;
|
||||
~Wrapper() override { delete rep_; }
|
||||
const char* Name() const override { return rep_->Name(); }
|
||||
const char* CompatibilityName() const override {
|
||||
@@ -7609,6 +7836,35 @@ rocksdb_write_buffer_manager_t* rocksdb_write_buffer_manager_create(
|
||||
return wbm;
|
||||
}
|
||||
|
||||
static bool rocksdb_write_buffer_manager_parse_flush_policy(
|
||||
int value, WriteBufferFlushPolicy* policy) {
|
||||
switch (value) {
|
||||
case rocksdb_write_buffer_manager_flush_policy_oldest:
|
||||
*policy = WriteBufferFlushPolicy::kFlushOldest;
|
||||
return true;
|
||||
case rocksdb_write_buffer_manager_flush_policy_largest:
|
||||
*policy = WriteBufferFlushPolicy::kFlushLargest;
|
||||
return true;
|
||||
case rocksdb_write_buffer_manager_flush_policy_largest_across_dbs:
|
||||
*policy = WriteBufferFlushPolicy::kFlushLargestAcrossDBs;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
rocksdb_write_buffer_manager_t* rocksdb_write_buffer_manager_create_with_policy(
|
||||
size_t buffer_size, bool allow_stall, int flush_policy) {
|
||||
WriteBufferFlushPolicy policy;
|
||||
if (!rocksdb_write_buffer_manager_parse_flush_policy(flush_policy, &policy)) {
|
||||
return nullptr;
|
||||
}
|
||||
rocksdb_write_buffer_manager_t* wbm = new rocksdb_write_buffer_manager_t;
|
||||
wbm->rep = std::make_shared<WriteBufferManager>(
|
||||
buffer_size, std::shared_ptr<Cache>{}, allow_stall, policy);
|
||||
return wbm;
|
||||
}
|
||||
|
||||
rocksdb_write_buffer_manager_t* rocksdb_write_buffer_manager_create_with_cache(
|
||||
size_t buffer_size, const rocksdb_cache_t* cache, bool allow_stall) {
|
||||
rocksdb_write_buffer_manager_t* wbm = new rocksdb_write_buffer_manager_t;
|
||||
@@ -7617,6 +7873,20 @@ rocksdb_write_buffer_manager_t* rocksdb_write_buffer_manager_create_with_cache(
|
||||
return wbm;
|
||||
}
|
||||
|
||||
rocksdb_write_buffer_manager_t*
|
||||
rocksdb_write_buffer_manager_create_with_cache_and_policy(
|
||||
size_t buffer_size, const rocksdb_cache_t* cache, bool allow_stall,
|
||||
int flush_policy) {
|
||||
WriteBufferFlushPolicy policy;
|
||||
if (!rocksdb_write_buffer_manager_parse_flush_policy(flush_policy, &policy)) {
|
||||
return nullptr;
|
||||
}
|
||||
rocksdb_write_buffer_manager_t* wbm = new rocksdb_write_buffer_manager_t;
|
||||
wbm->rep = std::make_shared<WriteBufferManager>(buffer_size, cache->rep,
|
||||
allow_stall, policy);
|
||||
return wbm;
|
||||
}
|
||||
|
||||
void rocksdb_write_buffer_manager_destroy(rocksdb_write_buffer_manager_t* wbm) {
|
||||
delete wbm;
|
||||
}
|
||||
@@ -7657,6 +7927,19 @@ ROCKSDB_LIBRARY_API void rocksdb_write_buffer_manager_set_allow_stall(
|
||||
wbm->rep->SetAllowStall(new_allow_stall);
|
||||
}
|
||||
|
||||
int rocksdb_write_buffer_manager_flush_policy(
|
||||
rocksdb_write_buffer_manager_t* wbm) {
|
||||
return static_cast<int>(wbm->rep->flush_policy());
|
||||
}
|
||||
|
||||
void rocksdb_write_buffer_manager_set_flush_policy(
|
||||
rocksdb_write_buffer_manager_t* wbm, int flush_policy) {
|
||||
WriteBufferFlushPolicy policy;
|
||||
if (rocksdb_write_buffer_manager_parse_flush_policy(flush_policy, &policy)) {
|
||||
wbm->rep->SetFlushPolicy(policy);
|
||||
}
|
||||
}
|
||||
|
||||
rocksdb_sst_file_manager_t* rocksdb_sst_file_manager_create(
|
||||
rocksdb_env_t* env) {
|
||||
rocksdb_sst_file_manager_t* sfm = new rocksdb_sst_file_manager_t;
|
||||
@@ -9808,6 +10091,16 @@ unsigned char rocksdb_options_get_write_dbid_to_manifest(
|
||||
return opt->rep.write_dbid_to_manifest;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_track_options_file_number_in_manifest(
|
||||
rocksdb_options_t* opt, unsigned char v) {
|
||||
opt->rep.track_options_file_number_in_manifest = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_options_get_track_options_file_number_in_manifest(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.track_options_file_number_in_manifest;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_write_identity_file(rocksdb_options_t* opt,
|
||||
unsigned char v) {
|
||||
opt->rep.write_identity_file = v;
|
||||
@@ -9883,6 +10176,26 @@ const char* rocksdb_options_get_db_host_id(rocksdb_options_t* opt,
|
||||
return opt->rep.db_host_id.data();
|
||||
}
|
||||
|
||||
void rocksdb_options_set_remote_compaction_manifest_floor(
|
||||
rocksdb_options_t* opt, unsigned char v) {
|
||||
opt->rep.remote_compaction_manifest_floor = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_options_get_remote_compaction_manifest_floor(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.remote_compaction_manifest_floor;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_use_session_tmp_dir_for_remote_compaction(
|
||||
rocksdb_options_t* opt, unsigned char v) {
|
||||
opt->rep.use_session_tmp_dir_for_remote_compaction = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_options_get_use_session_tmp_dir_for_remote_compaction(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.use_session_tmp_dir_for_remote_compaction;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_lowest_used_cache_tier(rocksdb_options_t* opt, int v) {
|
||||
opt->rep.lowest_used_cache_tier =
|
||||
static_cast<decltype(opt->rep.lowest_used_cache_tier)>(v);
|
||||
@@ -9923,6 +10236,16 @@ uint64_t rocksdb_options_get_max_compaction_trigger_wakeup_seconds(
|
||||
return opt->rep.max_compaction_trigger_wakeup_seconds;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_periodic_compaction_phase_recovery_percent(
|
||||
rocksdb_options_t* opt, int v) {
|
||||
opt->rep.periodic_compaction_phase_recovery_percent = v;
|
||||
}
|
||||
|
||||
int rocksdb_options_get_periodic_compaction_phase_recovery_percent(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.periodic_compaction_phase_recovery_percent;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_follower_refresh_catchup_period_ms(
|
||||
rocksdb_options_t* opt, uint64_t v) {
|
||||
opt->rep.follower_refresh_catchup_period_ms = v;
|
||||
@@ -11094,16 +11417,6 @@ unsigned char rocksdb_ingestexternalfileoptions_get_ingest_behind(
|
||||
return opt->rep.ingest_behind;
|
||||
}
|
||||
|
||||
void rocksdb_ingestexternalfileoptions_set_write_global_seqno(
|
||||
rocksdb_ingestexternalfileoptions_t* opt, unsigned char v) {
|
||||
opt->rep.write_global_seqno = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_ingestexternalfileoptions_get_write_global_seqno(
|
||||
rocksdb_ingestexternalfileoptions_t* opt) {
|
||||
return opt->rep.write_global_seqno;
|
||||
}
|
||||
|
||||
void rocksdb_ingestexternalfileoptions_set_verify_checksums_before_ingest(
|
||||
rocksdb_ingestexternalfileoptions_t* opt, unsigned char v) {
|
||||
opt->rep.verify_checksums_before_ingest = v;
|
||||
@@ -11184,6 +11497,16 @@ int rocksdb_ingestexternalfileoptions_get_file_opening_threads(
|
||||
|
||||
/* OpenAndCompactOptions */
|
||||
|
||||
void rocksdb_open_and_compact_options_set_max_secondary_open_retries(
|
||||
rocksdb_open_and_compact_options_t* opt, uint32_t v) {
|
||||
opt->rep.max_secondary_open_retries = v;
|
||||
}
|
||||
|
||||
uint32_t rocksdb_open_and_compact_options_get_max_secondary_open_retries(
|
||||
rocksdb_open_and_compact_options_t* opt) {
|
||||
return opt->rep.max_secondary_open_retries;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_open_and_compact_options_get_allow_resumption(
|
||||
rocksdb_open_and_compact_options_t* opt) {
|
||||
return opt->rep.allow_resumption;
|
||||
@@ -11581,6 +11904,17 @@ double rocksdb_block_based_options_get_uniform_cv_threshold(
|
||||
return opt->rep.uniform_cv_threshold;
|
||||
}
|
||||
|
||||
void rocksdb_block_based_options_set_optimize_key_common_prefix(
|
||||
rocksdb_block_based_table_options_t* opt, int v) {
|
||||
opt->rep.optimize_key_common_prefix =
|
||||
static_cast<decltype(opt->rep.optimize_key_common_prefix)>(v);
|
||||
}
|
||||
|
||||
int rocksdb_block_based_options_get_optimize_key_common_prefix(
|
||||
rocksdb_block_based_table_options_t* opt) {
|
||||
return static_cast<int>(opt->rep.optimize_key_common_prefix);
|
||||
}
|
||||
|
||||
void rocksdb_block_based_options_set_enable_index_compression(
|
||||
rocksdb_block_based_table_options_t* opt, unsigned char v) {
|
||||
opt->rep.enable_index_compression = v;
|
||||
|
||||
+59
@@ -2474,6 +2474,35 @@ int main(int argc, char** argv) {
|
||||
CheckNoError(err);
|
||||
rocksdb_readoptions_set_snapshot(roptions, snap);
|
||||
CheckGet(db, roptions, "foo", "hello");
|
||||
|
||||
size_t metadata_value_len = 0;
|
||||
unsigned char newer_version_present = 0;
|
||||
char* metadata_value =
|
||||
rocksdb_get_with_metadata(db, roptions, "foo", 3, &metadata_value_len,
|
||||
NULL, NULL, &newer_version_present, &err);
|
||||
// NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling)
|
||||
CheckNoError(err);
|
||||
CheckEqual("hello", metadata_value, metadata_value_len);
|
||||
// NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling)
|
||||
CheckCondition(newer_version_present == 1);
|
||||
Free(&metadata_value);
|
||||
|
||||
const char* metadata_keys[] = {"foo"};
|
||||
const size_t metadata_key_sizes[] = {3};
|
||||
char* metadata_values[1];
|
||||
size_t metadata_value_sizes[1];
|
||||
unsigned char newer_versions[1];
|
||||
char* metadata_errors[1];
|
||||
rocksdb_multi_get_with_metadata(
|
||||
db, roptions, 1, metadata_keys, metadata_key_sizes, metadata_values,
|
||||
metadata_value_sizes, NULL, NULL, newer_versions, metadata_errors);
|
||||
// NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling)
|
||||
CheckCondition(metadata_errors[0] == NULL);
|
||||
CheckEqual("hello", metadata_values[0], metadata_value_sizes[0]);
|
||||
// NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling)
|
||||
CheckCondition(newer_versions[0] == 1);
|
||||
Free(&metadata_values[0]);
|
||||
|
||||
rocksdb_readoptions_set_snapshot(roptions, NULL);
|
||||
CheckGet(db, roptions, "foo", NULL);
|
||||
rocksdb_release_snapshot(db, snap);
|
||||
@@ -6687,6 +6716,36 @@ int main(int argc, char** argv) {
|
||||
300 == rocksdb_write_buffer_manager_buffer_size(write_buffer_manager));
|
||||
|
||||
rocksdb_write_buffer_manager_destroy(write_buffer_manager);
|
||||
|
||||
write_buffer_manager = rocksdb_write_buffer_manager_create_with_policy(
|
||||
200, true,
|
||||
rocksdb_write_buffer_manager_flush_policy_largest_across_dbs);
|
||||
CheckCondition(write_buffer_manager != NULL);
|
||||
CheckCondition(
|
||||
rocksdb_write_buffer_manager_flush_policy_largest_across_dbs ==
|
||||
rocksdb_write_buffer_manager_flush_policy(write_buffer_manager));
|
||||
rocksdb_write_buffer_manager_set_flush_policy(
|
||||
write_buffer_manager,
|
||||
rocksdb_write_buffer_manager_flush_policy_largest);
|
||||
CheckCondition(
|
||||
rocksdb_write_buffer_manager_flush_policy_largest ==
|
||||
rocksdb_write_buffer_manager_flush_policy(write_buffer_manager));
|
||||
rocksdb_write_buffer_manager_set_flush_policy(write_buffer_manager, 999);
|
||||
CheckCondition(
|
||||
rocksdb_write_buffer_manager_flush_policy_largest ==
|
||||
rocksdb_write_buffer_manager_flush_policy(write_buffer_manager));
|
||||
rocksdb_write_buffer_manager_destroy(write_buffer_manager);
|
||||
|
||||
write_buffer_manager =
|
||||
rocksdb_write_buffer_manager_create_with_cache_and_policy(
|
||||
200, lru, true,
|
||||
rocksdb_write_buffer_manager_flush_policy_largest_across_dbs);
|
||||
CheckCondition(write_buffer_manager != NULL);
|
||||
rocksdb_write_buffer_manager_destroy(write_buffer_manager);
|
||||
|
||||
write_buffer_manager =
|
||||
rocksdb_write_buffer_manager_create_with_policy(200, true, 999);
|
||||
CheckCondition(write_buffer_manager == NULL);
|
||||
rocksdb_cache_destroy(lru);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ class CoalescingIterator : public Iterator {
|
||||
// No copy allowed
|
||||
CoalescingIterator(const CoalescingIterator&) = delete;
|
||||
CoalescingIterator& operator=(const CoalescingIterator&) = delete;
|
||||
CoalescingIterator(CoalescingIterator&&) = delete;
|
||||
CoalescingIterator& operator=(CoalescingIterator&&) = delete;
|
||||
|
||||
bool Valid() const override { return impl_.Valid(); }
|
||||
void SeekToFirst() override { impl_.SeekToFirst(); }
|
||||
|
||||
+137
-15
@@ -33,6 +33,7 @@
|
||||
#include "db/write_controller.h"
|
||||
#include "file/sst_file_manager_impl.h"
|
||||
#include "logging/logging.h"
|
||||
#include "memtable/flush_initiator.h"
|
||||
#include "monitoring/thread_status_util.h"
|
||||
#include "options/options_helper.h"
|
||||
#include "port/port.h"
|
||||
@@ -612,7 +613,8 @@ ColumnFamilyData::ColumnFamilyData(
|
||||
const FileOptions* file_options, ColumnFamilySet* column_family_set,
|
||||
BlockCacheTracer* const block_cache_tracer,
|
||||
const std::shared_ptr<IOTracer>& io_tracer, const std::string& db_id,
|
||||
const std::string& db_session_id, bool read_only, bool fast_sst_open)
|
||||
const std::string& db_session_id, bool read_only,
|
||||
bool options_are_sanitized, bool fast_sst_open)
|
||||
: id_(id),
|
||||
name_(name),
|
||||
dummy_versions_(_dummy_versions),
|
||||
@@ -622,7 +624,10 @@ ColumnFamilyData::ColumnFamilyData(
|
||||
dropped_(false),
|
||||
flush_skip_reschedule_(false),
|
||||
internal_comparator_(cf_options.comparator),
|
||||
initial_cf_options_(SanitizeCfOptions(db_options, read_only, cf_options)),
|
||||
initial_cf_options_(
|
||||
options_are_sanitized
|
||||
? cf_options
|
||||
: SanitizeCfOptions(db_options, read_only, cf_options)),
|
||||
ioptions_(db_options, initial_cf_options_),
|
||||
mutable_cf_options_(initial_cf_options_),
|
||||
is_delete_range_supported_(
|
||||
@@ -672,7 +677,7 @@ ColumnFamilyData::ColumnFamilyData(
|
||||
new InternalStats(ioptions_.num_levels, ioptions_.clock, this));
|
||||
table_cache_.reset(new TableCache(ioptions_, file_options, _table_cache,
|
||||
block_cache_tracer, io_tracer,
|
||||
db_session_id, fast_sst_open));
|
||||
db_session_id, fast_sst_open, name_));
|
||||
blob_file_cache_.reset(
|
||||
new BlobFileCache(_table_cache, &ioptions(), soptions(), id_,
|
||||
internal_stats_->GetBlobFileReadHist(), io_tracer));
|
||||
@@ -834,6 +839,11 @@ void ColumnFamilyData::SetDropped() {
|
||||
// can't drop default CF
|
||||
assert(id_ != 0);
|
||||
dropped_ = true;
|
||||
// A tracker can still contain accounting published while a different WBM
|
||||
// policy was active. Cleanup therefore cannot depend on the current policy.
|
||||
if (mem_ != nullptr) {
|
||||
mem_->StopWBMTracking();
|
||||
}
|
||||
write_controller_token_.reset();
|
||||
|
||||
// remove from column_family_set
|
||||
@@ -1246,7 +1256,10 @@ uint64_t ColumnFamilyData::GetLiveSstFilesSize() const {
|
||||
MemTable* ColumnFamilyData::ConstructNewMemtable(
|
||||
const MutableCFOptions& mutable_cf_options, SequenceNumber earliest_seq) {
|
||||
return new MemTable(internal_comparator_, ioptions_, mutable_cf_options,
|
||||
write_buffer_manager_, earliest_seq, id_);
|
||||
write_buffer_manager_, earliest_seq, id_,
|
||||
column_family_set_ != nullptr
|
||||
? column_family_set_->flush_initiator()
|
||||
: nullptr);
|
||||
}
|
||||
|
||||
void ColumnFamilyData::CreateNewMemtable(SequenceNumber earliest_seq) {
|
||||
@@ -1281,15 +1294,15 @@ Compaction* ColumnFamilyData::PickCompaction(
|
||||
}
|
||||
|
||||
bool ColumnFamilyData::RangeOverlapWithCompaction(
|
||||
const Slice& smallest_user_key, const Slice& largest_user_key,
|
||||
int level) const {
|
||||
const Slice& smallest_user_key, const Slice& largest_user_key, int level,
|
||||
bool range_limit_exclusive) const {
|
||||
return compaction_picker_->RangeOverlapWithCompaction(
|
||||
smallest_user_key, largest_user_key, level);
|
||||
smallest_user_key, largest_user_key, level, range_limit_exclusive);
|
||||
}
|
||||
|
||||
Status ColumnFamilyData::RangesOverlapWithMemtables(
|
||||
const autovector<UserKeyRange>& ranges, SuperVersion* super_version,
|
||||
bool allow_data_in_errors, bool* overlap) {
|
||||
bool allow_data_in_errors, bool* overlap, bool range_limit_exclusive) {
|
||||
assert(overlap != nullptr);
|
||||
*overlap = false;
|
||||
// Create an InternalIterator over all unflushed memtables
|
||||
@@ -1334,12 +1347,15 @@ Status ColumnFamilyData::RangesOverlapWithMemtables(
|
||||
}
|
||||
|
||||
if (status.ok()) {
|
||||
const int limit_cmp = memtable_iter->Valid()
|
||||
? ucmp->CompareWithoutTimestamp(
|
||||
seek_result.user_key, ranges[i].limit)
|
||||
: 1;
|
||||
if (memtable_iter->Valid() &&
|
||||
ucmp->CompareWithoutTimestamp(seek_result.user_key,
|
||||
ranges[i].limit) <= 0) {
|
||||
(range_limit_exclusive ? limit_cmp < 0 : limit_cmp <= 0)) {
|
||||
*overlap = true;
|
||||
} else if (range_del_agg.IsRangeOverlapped(ranges[i].start,
|
||||
ranges[i].limit)) {
|
||||
} else if (range_del_agg.IsRangeOverlapped(
|
||||
ranges[i].start, ranges[i].limit, range_limit_exclusive)) {
|
||||
*overlap = true;
|
||||
}
|
||||
}
|
||||
@@ -1826,7 +1842,7 @@ ColumnFamilySet::ColumnFamilySet(
|
||||
ColumnFamilyData::kDummyColumnFamilyDataId, "", nullptr, nullptr,
|
||||
nullptr, ColumnFamilyOptions(), *db_options, &file_options_, nullptr,
|
||||
block_cache_tracer, io_tracer, db_id, db_session_id,
|
||||
/*read_only*/ true)),
|
||||
/*read_only=*/true, /*options_are_sanitized=*/false)),
|
||||
default_cfd_cache_(nullptr),
|
||||
db_name_(dbname),
|
||||
db_options_(db_options),
|
||||
@@ -1896,15 +1912,112 @@ size_t ColumnFamilySet::NumberOfColumnFamilies() const {
|
||||
return column_families_.size();
|
||||
}
|
||||
|
||||
void ColumnFamilySet::RefreshFlushableMemAccounting() {
|
||||
if (flush_initiator_ == nullptr || write_buffer_manager_ == nullptr ||
|
||||
!write_buffer_manager_->ShouldTrackFlushInitiator()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (bulk_flushable_mem_accounting_depth_ > 0) {
|
||||
bulk_flushable_mem_accounting_dirty_ = true;
|
||||
flush_initiator_->InvalidateLargestFlushableCFMem();
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep candidate readers from consuming a mixture of the previous snapshot
|
||||
// and the values published by this rebuild.
|
||||
flush_initiator_->InvalidateLargestFlushableCFMem();
|
||||
constexpr int kMaxRefreshAttempts = 3;
|
||||
for (int attempt = 0; attempt < kMaxRefreshAttempts; ++attempt) {
|
||||
const uint64_t update_seq =
|
||||
flush_initiator_->GetFlushableMemUpdateSequence();
|
||||
bool has_mutable_mem = false;
|
||||
bool has_ineligible_cf = false;
|
||||
size_t largest = 0;
|
||||
size_t waiting_immutable_mem = 0;
|
||||
for (auto cfd : *this) {
|
||||
if (cfd->IsDropped() || !cfd->initialized()) {
|
||||
continue;
|
||||
}
|
||||
const bool mutable_not_empty = !cfd->mem()->IsEmpty();
|
||||
if (mutable_not_empty) {
|
||||
cfd->mem()->RefreshWBMTracking();
|
||||
}
|
||||
const bool has_mutable =
|
||||
mutable_not_empty && cfd->mem()->IsWBMTrackingActive();
|
||||
has_mutable_mem = has_mutable_mem || has_mutable;
|
||||
if (flush_initiator_->UsesTotalMutableMem()) {
|
||||
waiting_immutable_mem +=
|
||||
cfd->imm()->WBMTrackedUnstartedMemTablesMemoryUsage();
|
||||
continue;
|
||||
}
|
||||
if (cfd->imm()->IsFlushPendingOrRunning()) {
|
||||
has_ineligible_cf = true;
|
||||
continue;
|
||||
}
|
||||
const size_t mutable_mem =
|
||||
has_mutable ? cfd->mem()->WBMTrackedMemoryUsage() : 0;
|
||||
const size_t immutable_mem =
|
||||
cfd->imm()->WBMTrackedUnflushedMemTablesMemoryUsage();
|
||||
waiting_immutable_mem += immutable_mem;
|
||||
largest = std::max(largest, mutable_mem + immutable_mem);
|
||||
}
|
||||
if (flush_initiator_->UsesTotalMutableMem()) {
|
||||
flush_initiator_->SetHasIneligibleCF(false);
|
||||
flush_initiator_->SetHasFlushableCF(has_mutable_mem ||
|
||||
waiting_immutable_mem > 0);
|
||||
if (flush_initiator_->TrySetLargestFlushableCFMem(
|
||||
0, waiting_immutable_mem, update_seq)) {
|
||||
write_buffer_manager_->NotifyFlushInitiatorChanged();
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
flush_initiator_->SetHasIneligibleCF(has_ineligible_cf);
|
||||
flush_initiator_->SetHasFlushableCF(largest > 0);
|
||||
if (flush_initiator_->TrySetLargestFlushableCFMem(
|
||||
largest, waiting_immutable_mem, update_seq)) {
|
||||
write_buffer_manager_->NotifyFlushInitiatorChanged();
|
||||
return;
|
||||
}
|
||||
}
|
||||
flush_initiator_->InvalidateLargestFlushableCFMem();
|
||||
}
|
||||
|
||||
void ColumnFamilySet::BeginBulkFlushableMemAccountingUpdate() {
|
||||
++bulk_flushable_mem_accounting_depth_;
|
||||
if (bulk_flushable_mem_accounting_depth_ == 1) {
|
||||
bulk_flushable_mem_accounting_dirty_ = true;
|
||||
if (flush_initiator_ != nullptr && write_buffer_manager_ != nullptr &&
|
||||
write_buffer_manager_->ShouldTrackFlushInitiator()) {
|
||||
flush_initiator_->InvalidateLargestFlushableCFMem();
|
||||
write_buffer_manager_->NotifyFlushInitiatorChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ColumnFamilySet::EndBulkFlushableMemAccountingUpdate() {
|
||||
assert(bulk_flushable_mem_accounting_depth_ > 0);
|
||||
--bulk_flushable_mem_accounting_depth_;
|
||||
if (bulk_flushable_mem_accounting_depth_ == 0 &&
|
||||
bulk_flushable_mem_accounting_dirty_) {
|
||||
bulk_flushable_mem_accounting_dirty_ = false;
|
||||
TEST_SYNC_POINT(
|
||||
"ColumnFamilySet::EndBulkFlushableMemAccountingUpdate:Refresh");
|
||||
RefreshFlushableMemAccounting();
|
||||
}
|
||||
}
|
||||
|
||||
// under a DB mutex AND write thread
|
||||
ColumnFamilyData* ColumnFamilySet::CreateColumnFamily(
|
||||
const std::string& name, uint32_t id, Version* dummy_versions,
|
||||
const ColumnFamilyOptions& options, bool read_only) {
|
||||
const ColumnFamilyOptions& options, bool read_only,
|
||||
bool options_are_sanitized) {
|
||||
assert(column_families_.find(name) == column_families_.end());
|
||||
ColumnFamilyData* new_cfd = new ColumnFamilyData(
|
||||
id, name, dummy_versions, table_cache_, write_buffer_manager_, options,
|
||||
*db_options_, &file_options_, this, block_cache_tracer_, io_tracer_,
|
||||
db_id_, db_session_id_, read_only, fast_sst_open_);
|
||||
db_id_, db_session_id_, read_only, options_are_sanitized, fast_sst_open_);
|
||||
column_families_.insert({name, id});
|
||||
column_family_data_.insert({id, new_cfd});
|
||||
auto ucmp = new_cfd->user_comparator();
|
||||
@@ -1936,6 +2049,15 @@ void ColumnFamilySet::RemoveColumnFamily(ColumnFamilyData* cfd) {
|
||||
column_families_.erase(cfd->GetName());
|
||||
running_ts_sz_.erase(cf_id);
|
||||
ts_sz_for_record_.erase(cf_id);
|
||||
if (bulk_flushable_mem_accounting_depth_ > 0) {
|
||||
bulk_flushable_mem_accounting_dirty_ = true;
|
||||
if (flush_initiator_ != nullptr) {
|
||||
flush_initiator_->InvalidateLargestFlushableCFMem();
|
||||
}
|
||||
return;
|
||||
}
|
||||
TEST_SYNC_POINT("ColumnFamilySet::RemoveColumnFamily:Refresh");
|
||||
RefreshFlushableMemAccounting();
|
||||
}
|
||||
|
||||
// under a DB mutex OR from a write thread
|
||||
|
||||
+39
-16
@@ -52,6 +52,7 @@ struct SuperVersionContext;
|
||||
class BlobFileCache;
|
||||
class BlobFilePartitionManager;
|
||||
class BlobSource;
|
||||
class FlushInitiator;
|
||||
|
||||
extern const double kIncSlowdownRatio;
|
||||
// This file contains a list of data structures for managing column family
|
||||
@@ -170,7 +171,7 @@ class ColumnFamilyHandleImpl : public ColumnFamilyHandle {
|
||||
ColumnFamilyHandleImpl(ColumnFamilyData* cfd, DBImpl* db,
|
||||
InstrumentedMutex* mutex);
|
||||
// destroy without mutex
|
||||
virtual ~ColumnFamilyHandleImpl();
|
||||
~ColumnFamilyHandleImpl() override;
|
||||
virtual ColumnFamilyData* cfd() const { return cfd_; }
|
||||
virtual DBImpl* db() const { return db_; }
|
||||
|
||||
@@ -445,8 +446,8 @@ class ColumnFamilyData {
|
||||
// Check if the passed range overlap with any running compactions.
|
||||
// REQUIRES: DB mutex held
|
||||
bool RangeOverlapWithCompaction(const Slice& smallest_user_key,
|
||||
const Slice& largest_user_key,
|
||||
int level) const;
|
||||
const Slice& largest_user_key, int level,
|
||||
bool range_limit_exclusive) const;
|
||||
|
||||
// Check if the passed ranges overlap with any unflushed memtables
|
||||
// (immutable or mutable).
|
||||
@@ -457,7 +458,8 @@ class ColumnFamilyData {
|
||||
// Thread-safe
|
||||
Status RangesOverlapWithMemtables(const autovector<UserKeyRange>& ranges,
|
||||
SuperVersion* super_version,
|
||||
bool allow_data_in_errors, bool* overlap);
|
||||
bool allow_data_in_errors, bool* overlap,
|
||||
bool range_limit_exclusive);
|
||||
|
||||
// A flag to tell a manual compaction is to compact all levels together
|
||||
// instead of a specific level.
|
||||
@@ -652,17 +654,15 @@ class ColumnFamilyData {
|
||||
|
||||
private:
|
||||
friend class ColumnFamilySet;
|
||||
ColumnFamilyData(uint32_t id, const std::string& name,
|
||||
Version* dummy_versions, Cache* table_cache,
|
||||
WriteBufferManager* write_buffer_manager,
|
||||
const ColumnFamilyOptions& options,
|
||||
const ImmutableDBOptions& db_options,
|
||||
const FileOptions* file_options,
|
||||
ColumnFamilySet* column_family_set,
|
||||
BlockCacheTracer* const block_cache_tracer,
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const std::string& db_id, const std::string& db_session_id,
|
||||
bool read_only, bool fast_sst_open = false);
|
||||
ColumnFamilyData(
|
||||
uint32_t id, const std::string& name, Version* dummy_versions,
|
||||
Cache* table_cache, WriteBufferManager* write_buffer_manager,
|
||||
const ColumnFamilyOptions& options, const ImmutableDBOptions& db_options,
|
||||
const FileOptions* file_options, ColumnFamilySet* column_family_set,
|
||||
BlockCacheTracer* const block_cache_tracer,
|
||||
const std::shared_ptr<IOTracer>& io_tracer, const std::string& db_id,
|
||||
const std::string& db_session_id, bool read_only,
|
||||
bool options_are_sanitized, bool fast_sst_open = false);
|
||||
|
||||
std::vector<std::string> GetDbPaths() const;
|
||||
|
||||
@@ -848,7 +848,8 @@ class ColumnFamilySet {
|
||||
ColumnFamilyData* CreateColumnFamily(const std::string& name, uint32_t id,
|
||||
Version* dummy_version,
|
||||
const ColumnFamilyOptions& options,
|
||||
bool read_only);
|
||||
bool read_only,
|
||||
bool options_are_sanitized);
|
||||
|
||||
const UnorderedMap<uint32_t, size_t>& GetRunningColumnFamiliesTimestampSize()
|
||||
const {
|
||||
@@ -870,6 +871,19 @@ class ColumnFamilySet {
|
||||
|
||||
WriteBufferManager* write_buffer_manager() { return write_buffer_manager_; }
|
||||
|
||||
void SetFlushInitiator(FlushInitiator* flush_initiator) {
|
||||
flush_initiator_ = flush_initiator;
|
||||
}
|
||||
|
||||
FlushInitiator* flush_initiator() const { return flush_initiator_; }
|
||||
|
||||
void RefreshFlushableMemAccounting();
|
||||
|
||||
// Coalesces accounting rebuilds while a caller removes multiple column
|
||||
// families. Both methods require the DB mutex.
|
||||
void BeginBulkFlushableMemAccountingUpdate();
|
||||
void EndBulkFlushableMemAccountingUpdate();
|
||||
|
||||
WriteController* write_controller() { return write_controller_; }
|
||||
|
||||
private:
|
||||
@@ -897,6 +911,9 @@ class ColumnFamilySet {
|
||||
// column families with non-zero user-defined timestamp size.
|
||||
UnorderedMap<uint32_t, size_t> ts_sz_for_record_;
|
||||
|
||||
int bulk_flushable_mem_accounting_depth_ = 0;
|
||||
bool bulk_flushable_mem_accounting_dirty_ = false;
|
||||
|
||||
uint32_t max_column_family_;
|
||||
const FileOptions file_options_;
|
||||
|
||||
@@ -911,6 +928,7 @@ class ColumnFamilySet {
|
||||
const ImmutableDBOptions* const db_options_;
|
||||
Cache* table_cache_;
|
||||
WriteBufferManager* write_buffer_manager_;
|
||||
FlushInitiator* flush_initiator_ = nullptr;
|
||||
WriteController* write_controller_;
|
||||
BlockCacheTracer* const block_cache_tracer_;
|
||||
std::shared_ptr<IOTracer> io_tracer_;
|
||||
@@ -932,6 +950,11 @@ class RefedColumnFamilySet {
|
||||
explicit iterator(ColumnFamilySet::iterator wrapped) : wrapped_(wrapped) {
|
||||
MaybeRef(*wrapped_);
|
||||
}
|
||||
// No copying allowed
|
||||
iterator(const iterator&) = delete;
|
||||
iterator& operator=(const iterator&) = delete;
|
||||
iterator(iterator&&) = delete;
|
||||
iterator& operator=(iterator&&) = delete;
|
||||
~iterator() { MaybeUnref(*wrapped_); }
|
||||
inline void MaybeRef(ColumnFamilyData* cfd) {
|
||||
if (cfd->GetID() != ColumnFamilyData::kDummyColumnFamilyDataId) {
|
||||
|
||||
+545
-38
@@ -25,6 +25,7 @@
|
||||
#include "rocksdb/iterator.h"
|
||||
#include "rocksdb/listener.h"
|
||||
#include "rocksdb/utilities/object_registry.h"
|
||||
#include "rocksdb/utilities/options_util.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "test_util/testharness.h"
|
||||
#include "test_util/testutil.h"
|
||||
@@ -32,6 +33,7 @@
|
||||
#include "util/defer.h"
|
||||
#include "util/string_util.h"
|
||||
#include "utilities/fault_injection_env.h"
|
||||
#include "utilities/fault_injection_fs.h"
|
||||
#include "utilities/merge_operators.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -113,7 +115,8 @@ class ColumnFamilyTestBase : public testing::Test {
|
||||
}
|
||||
|
||||
void Build(int base, int n, int flush_every = 0) {
|
||||
std::string key_space, value_space;
|
||||
std::string key_space;
|
||||
std::string value_space;
|
||||
WriteBatch batch;
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
@@ -179,6 +182,35 @@ class ColumnFamilyTestBase : public testing::Test {
|
||||
db_.reset();
|
||||
}
|
||||
|
||||
void AssertColumnFamilyNames(const std::vector<std::string>& expected) {
|
||||
std::vector<std::string> column_families;
|
||||
ASSERT_OK(DB::ListColumnFamilies(db_options_, dbname_, &column_families));
|
||||
ASSERT_EQ(expected, column_families);
|
||||
}
|
||||
|
||||
void LoadLatestColumnFamilyDescriptors(
|
||||
std::vector<ColumnFamilyDescriptor>* descriptors) {
|
||||
ConfigOptions config_options;
|
||||
config_options.env = db_options_.env;
|
||||
DBOptions loaded_db_options;
|
||||
ASSERT_OK(LoadLatestOptions(config_options, dbname_, &loaded_db_options,
|
||||
descriptors));
|
||||
}
|
||||
|
||||
size_t CountOptionsFiles() {
|
||||
std::vector<std::string> children;
|
||||
EXPECT_OK(db_options_.env->GetChildren(dbname_, &children));
|
||||
size_t count = 0;
|
||||
for (const auto& child : children) {
|
||||
uint64_t number = 0;
|
||||
FileType type;
|
||||
if (ParseFileName(child, &number, &type) && type == kOptionsFile) {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
Status TryOpen(std::vector<std::string> cf,
|
||||
std::vector<ColumnFamilyOptions> options = {}) {
|
||||
std::vector<ColumnFamilyDescriptor> column_families;
|
||||
@@ -514,8 +546,9 @@ class ColumnFamilyTest
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(FormatDef, ColumnFamilyTest,
|
||||
testing::Values(test::kDefaultFormatVersion));
|
||||
INSTANTIATE_TEST_CASE_P(FormatLatest, ColumnFamilyTest,
|
||||
testing::Values(kLatestBbtFormatVersion));
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
FormatLatest, ColumnFamilyTest,
|
||||
testing::Values(BlockBasedTableOptions::kLatestSupportedFormatVersion));
|
||||
|
||||
TEST_P(ColumnFamilyTest, DontReuseColumnFamilyID) {
|
||||
for (int iter = 0; iter < 3; ++iter) {
|
||||
@@ -705,8 +738,11 @@ INSTANTIATE_TEST_CASE_P(
|
||||
std::make_tuple(test::kDefaultFormatVersion, false)));
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
FormatLatest, FlushEmptyCFTestWithParam,
|
||||
testing::Values(std::make_tuple(kLatestBbtFormatVersion, true),
|
||||
std::make_tuple(kLatestBbtFormatVersion, false)));
|
||||
testing::Values(
|
||||
std::make_tuple(BlockBasedTableOptions::kLatestSupportedFormatVersion,
|
||||
true),
|
||||
std::make_tuple(BlockBasedTableOptions::kLatestSupportedFormatVersion,
|
||||
false)));
|
||||
|
||||
TEST_P(ColumnFamilyTest, AddDrop) {
|
||||
Open();
|
||||
@@ -774,6 +810,8 @@ TEST_P(ColumnFamilyTest, BulkAddDrop) {
|
||||
constexpr int kNumCF = 1000;
|
||||
ColumnFamilyOptions cf_options;
|
||||
WriteOptions write_options;
|
||||
db_options_.write_buffer_manager = std::make_shared<WriteBufferManager>(
|
||||
1 << 30, nullptr, false, WriteBufferFlushPolicy::kFlushLargestAcrossDBs);
|
||||
Open();
|
||||
std::vector<std::string> cf_names;
|
||||
std::vector<ColumnFamilyHandle*> cf_handles;
|
||||
@@ -784,7 +822,22 @@ TEST_P(ColumnFamilyTest, BulkAddDrop) {
|
||||
for (int i = 1; i <= kNumCF; i++) {
|
||||
ASSERT_OK(db_->Put(write_options, cf_handles[i - 1], "foo", "bar"));
|
||||
}
|
||||
std::atomic<int> per_removal_refreshes{0};
|
||||
std::atomic<int> bulk_refreshes{0};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"ColumnFamilySet::RemoveColumnFamily:Refresh",
|
||||
[&](void*) { ++per_removal_refreshes; });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"ColumnFamilySet::EndBulkFlushableMemAccountingUpdate:Refresh",
|
||||
[&](void*) { ++bulk_refreshes; });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
Defer cleanup_sync_points([] {
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
});
|
||||
ASSERT_OK(db_->DropColumnFamilies(cf_handles));
|
||||
EXPECT_EQ(0, per_removal_refreshes.load());
|
||||
EXPECT_EQ(1, bulk_refreshes.load());
|
||||
std::vector<ColumnFamilyDescriptor> cf_descriptors;
|
||||
for (auto* handle : cf_handles) {
|
||||
delete handle;
|
||||
@@ -798,7 +851,10 @@ TEST_P(ColumnFamilyTest, BulkAddDrop) {
|
||||
for (int i = 1; i <= kNumCF; i++) {
|
||||
ASSERT_OK(db_->Put(write_options, cf_handles[i - 1], "foo", "bar"));
|
||||
}
|
||||
bulk_refreshes.store(0);
|
||||
ASSERT_OK(db_->DropColumnFamilies(cf_handles));
|
||||
EXPECT_EQ(0, per_removal_refreshes.load());
|
||||
EXPECT_EQ(1, bulk_refreshes.load());
|
||||
for (auto* handle : cf_handles) {
|
||||
delete handle;
|
||||
}
|
||||
@@ -897,7 +953,9 @@ TEST_P(ColumnFamilyTest, IgnoreRecoveredLog) {
|
||||
CreateColumnFamilies({"cf1", "cf2"});
|
||||
|
||||
// fill up the DB
|
||||
std::string one, two, three;
|
||||
std::string one;
|
||||
std::string two;
|
||||
std::string three;
|
||||
PutFixed64(&one, 1);
|
||||
PutFixed64(&two, 2);
|
||||
PutFixed64(&three, 3);
|
||||
@@ -1105,7 +1163,10 @@ TEST_P(ColumnFamilyTest, DifferentWriteBufferSizes) {
|
||||
db_options_.max_total_wal_size = std::numeric_limits<uint64_t>::max();
|
||||
Open();
|
||||
CreateColumnFamilies({"one", "two", "three"});
|
||||
ColumnFamilyOptions default_cf, one, two, three;
|
||||
ColumnFamilyOptions default_cf;
|
||||
ColumnFamilyOptions one;
|
||||
ColumnFamilyOptions two;
|
||||
ColumnFamilyOptions three;
|
||||
// setup options. all column families have max_write_buffer_number setup to 10
|
||||
// "default" -> 100KB memtable, start flushing immediately
|
||||
// "one" -> 200KB memtable, start flushing with two immutable memtables
|
||||
@@ -1257,7 +1318,9 @@ TEST_P(ColumnFamilyTest, GetComparator) {
|
||||
|
||||
// Add three column families - one with no comparator and two
|
||||
// with comparators specified
|
||||
ColumnFamilyOptions second, third, fourth;
|
||||
ColumnFamilyOptions second;
|
||||
ColumnFamilyOptions third;
|
||||
ColumnFamilyOptions fourth;
|
||||
second.comparator = &third_comparator;
|
||||
third.comparator = &fourth_comparator;
|
||||
CreateColumnFamilies({"second", "third", "fourth"}, {second, third, fourth});
|
||||
@@ -1270,12 +1333,16 @@ TEST_P(ColumnFamilyTest, GetComparator) {
|
||||
TEST_P(ColumnFamilyTest, DifferentMergeOperators) {
|
||||
Open();
|
||||
CreateColumnFamilies({"first", "second"});
|
||||
ColumnFamilyOptions default_cf, first, second;
|
||||
ColumnFamilyOptions default_cf;
|
||||
ColumnFamilyOptions first;
|
||||
ColumnFamilyOptions second;
|
||||
first.merge_operator = MergeOperators::CreateUInt64AddOperator();
|
||||
second.merge_operator = MergeOperators::CreateStringAppendOperator();
|
||||
Reopen({default_cf, first, second});
|
||||
|
||||
std::string one, two, three;
|
||||
std::string one;
|
||||
std::string two;
|
||||
std::string three;
|
||||
PutFixed64(&one, 1);
|
||||
PutFixed64(&two, 2);
|
||||
PutFixed64(&three, 3);
|
||||
@@ -1300,7 +1367,9 @@ TEST_P(ColumnFamilyTest, DifferentMergeOperators) {
|
||||
TEST_P(ColumnFamilyTest, DifferentCompactionStyles) {
|
||||
Open();
|
||||
CreateColumnFamilies({"one", "two"});
|
||||
ColumnFamilyOptions default_cf, one, two;
|
||||
ColumnFamilyOptions default_cf;
|
||||
ColumnFamilyOptions one;
|
||||
ColumnFamilyOptions two;
|
||||
db_options_.max_open_files = 20; // only 10 files in file cache
|
||||
|
||||
default_cf.level_compaction_dynamic_level_bytes = false;
|
||||
@@ -1372,7 +1441,9 @@ TEST_P(ColumnFamilyTest, DifferentCompactionStyles) {
|
||||
TEST_P(ColumnFamilyTest, MultipleManualCompactions) {
|
||||
Open();
|
||||
CreateColumnFamilies({"one", "two"});
|
||||
ColumnFamilyOptions default_cf, one, two;
|
||||
ColumnFamilyOptions default_cf;
|
||||
ColumnFamilyOptions one;
|
||||
ColumnFamilyOptions two;
|
||||
db_options_.max_open_files = 20; // only 10 files in file cache
|
||||
db_options_.max_background_compactions = 3;
|
||||
|
||||
@@ -1472,7 +1543,9 @@ TEST_P(ColumnFamilyTest, MultipleManualCompactions) {
|
||||
TEST_P(ColumnFamilyTest, AutomaticAndManualCompactions) {
|
||||
Open();
|
||||
CreateColumnFamilies({"one", "two"});
|
||||
ColumnFamilyOptions default_cf, one, two;
|
||||
ColumnFamilyOptions default_cf;
|
||||
ColumnFamilyOptions one;
|
||||
ColumnFamilyOptions two;
|
||||
db_options_.max_open_files = 20; // only 10 files in file cache
|
||||
db_options_.max_background_compactions = 3;
|
||||
|
||||
@@ -1569,7 +1642,9 @@ TEST_P(ColumnFamilyTest, AutomaticAndManualCompactions) {
|
||||
TEST_P(ColumnFamilyTest, ManualAndAutomaticCompactions) {
|
||||
Open();
|
||||
CreateColumnFamilies({"one", "two"});
|
||||
ColumnFamilyOptions default_cf, one, two;
|
||||
ColumnFamilyOptions default_cf;
|
||||
ColumnFamilyOptions one;
|
||||
ColumnFamilyOptions two;
|
||||
db_options_.max_open_files = 20; // only 10 files in file cache
|
||||
db_options_.max_background_compactions = 3;
|
||||
|
||||
@@ -1668,7 +1743,8 @@ TEST_P(ColumnFamilyTest, ManualAndAutomaticCompactions) {
|
||||
TEST_P(ColumnFamilyTest, SameCFManualManualCompactions) {
|
||||
Open();
|
||||
CreateColumnFamilies({"one"});
|
||||
ColumnFamilyOptions default_cf, one;
|
||||
ColumnFamilyOptions default_cf;
|
||||
ColumnFamilyOptions one;
|
||||
db_options_.max_open_files = 20; // only 10 files in file cache
|
||||
db_options_.max_background_compactions = 3;
|
||||
|
||||
@@ -1768,7 +1844,8 @@ TEST_P(ColumnFamilyTest, SameCFManualManualCompactions) {
|
||||
TEST_P(ColumnFamilyTest, SameCFManualAutomaticCompactions) {
|
||||
Open();
|
||||
CreateColumnFamilies({"one"});
|
||||
ColumnFamilyOptions default_cf, one;
|
||||
ColumnFamilyOptions default_cf;
|
||||
ColumnFamilyOptions one;
|
||||
db_options_.max_open_files = 20; // only 10 files in file cache
|
||||
db_options_.max_background_compactions = 3;
|
||||
|
||||
@@ -1859,7 +1936,8 @@ TEST_P(ColumnFamilyTest, SameCFManualAutomaticCompactions) {
|
||||
TEST_P(ColumnFamilyTest, SameCFManualAutomaticCompactionsLevel) {
|
||||
Open();
|
||||
CreateColumnFamilies({"one"});
|
||||
ColumnFamilyOptions default_cf, one;
|
||||
ColumnFamilyOptions default_cf;
|
||||
ColumnFamilyOptions one;
|
||||
db_options_.max_open_files = 20; // only 10 files in file cache
|
||||
db_options_.max_background_compactions = 3;
|
||||
|
||||
@@ -1956,7 +2034,8 @@ TEST_P(ColumnFamilyTest, SameCFManualAutomaticCompactionsLevel) {
|
||||
TEST_P(ColumnFamilyTest, SameCFAutomaticManualCompactions) {
|
||||
Open();
|
||||
CreateColumnFamilies({"one"});
|
||||
ColumnFamilyOptions default_cf, one;
|
||||
ColumnFamilyOptions default_cf;
|
||||
ColumnFamilyOptions one;
|
||||
db_options_.max_open_files = 20; // only 10 files in file cache
|
||||
db_options_.max_background_compactions = 3;
|
||||
|
||||
@@ -2169,7 +2248,9 @@ TEST_P(ColumnFamilyTest, DontRollEmptyLogs) {
|
||||
TEST_P(ColumnFamilyTest, FlushStaleColumnFamilies) {
|
||||
Open();
|
||||
CreateColumnFamilies({"one", "two"});
|
||||
ColumnFamilyOptions default_cf, one, two;
|
||||
ColumnFamilyOptions default_cf;
|
||||
ColumnFamilyOptions one;
|
||||
ColumnFamilyOptions two;
|
||||
default_cf.write_buffer_size = 100000; // small write buffer size
|
||||
default_cf.arena_block_size = 4096;
|
||||
default_cf.disable_auto_compactions = true;
|
||||
@@ -2230,8 +2311,378 @@ struct CountOptionsFilesFs : public FileSystemWrapper {
|
||||
|
||||
std::atomic<int> options_files_created{};
|
||||
};
|
||||
|
||||
struct FailOptionsFilesFs : public FileSystemWrapper {
|
||||
explicit FailOptionsFilesFs(const std::shared_ptr<FileSystem>& t)
|
||||
: FileSystemWrapper(t) {}
|
||||
const char* Name() const override { return "FailOptionsFilesFs"; }
|
||||
|
||||
IOStatus NewWritableFile(const std::string& f, const FileOptions& file_opts,
|
||||
std::unique_ptr<FSWritableFile>* r,
|
||||
IODebugContext* dbg) override {
|
||||
if (fail_options_writes.load(std::memory_order_relaxed) &&
|
||||
f.find("OPTIONS-") != std::string::npos) {
|
||||
return IOStatus::IOError("injected OPTIONS-file creation failure");
|
||||
}
|
||||
return FileSystemWrapper::NewWritableFile(f, file_opts, r, dbg);
|
||||
}
|
||||
|
||||
std::atomic<bool> fail_options_writes{false};
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST_P(ColumnFamilyTest, TrackedOptionsWriteFailureDoesNotChangeManifest) {
|
||||
db_options_.track_options_file_number_in_manifest = true;
|
||||
db_options_.max_manifest_file_size = 1;
|
||||
db_options_.max_manifest_space_amp_pct = 0;
|
||||
auto fail_fs =
|
||||
std::make_shared<FailOptionsFilesFs>(db_options_.env->GetFileSystem());
|
||||
auto fail_env =
|
||||
std::make_unique<CompositeEnvWrapper>(db_options_.env, fail_fs);
|
||||
SaveAndRestore<Env*> save_restore_env(&db_options_.env, fail_env.get());
|
||||
|
||||
Open();
|
||||
OptionsFileManifestState before_failure;
|
||||
ASSERT_OK(VersionSet::GetOptionsFileManifestState(
|
||||
dbname_, db_options_.env->GetFileSystem().get(), &before_failure));
|
||||
fail_fs->fail_options_writes.store(true, std::memory_order_relaxed);
|
||||
ColumnFamilyHandle* handle = nullptr;
|
||||
ASSERT_NOK(db_->CreateColumnFamily(ColumnFamilyOptions(), "one", &handle));
|
||||
ASSERT_EQ(nullptr, handle);
|
||||
fail_fs->fail_options_writes.store(false, std::memory_order_relaxed);
|
||||
|
||||
OptionsFileManifestState after_failure;
|
||||
ASSERT_OK(VersionSet::GetOptionsFileManifestState(
|
||||
dbname_, db_options_.env->GetFileSystem().get(), &after_failure));
|
||||
ASSERT_EQ(before_failure.has_committed_options_file_number,
|
||||
after_failure.has_committed_options_file_number);
|
||||
ASSERT_EQ(before_failure.committed_options_file_number,
|
||||
after_failure.committed_options_file_number);
|
||||
|
||||
std::vector<ColumnFamilyDescriptor> loaded_cfs;
|
||||
LoadLatestColumnFamilyDescriptors(&loaded_cfs);
|
||||
ASSERT_EQ(1U, loaded_cfs.size());
|
||||
ASSERT_EQ(kDefaultColumnFamilyName, loaded_cfs.front().name);
|
||||
|
||||
const uint64_t old_manifest_number =
|
||||
dbfull()->GetVersionSet()->manifest_file_number();
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "key", "value"));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
ASSERT_NE(old_manifest_number,
|
||||
dbfull()->GetVersionSet()->manifest_file_number());
|
||||
|
||||
OptionsFileManifestState after_rotation;
|
||||
ASSERT_OK(VersionSet::GetOptionsFileManifestState(
|
||||
dbname_, db_options_.env->GetFileSystem().get(), &after_rotation));
|
||||
ASSERT_EQ(before_failure.has_committed_options_file_number,
|
||||
after_rotation.has_committed_options_file_number);
|
||||
ASSERT_EQ(before_failure.committed_options_file_number,
|
||||
after_rotation.committed_options_file_number);
|
||||
Close();
|
||||
|
||||
AssertColumnFamilyNames({kDefaultColumnFamilyName});
|
||||
}
|
||||
|
||||
TEST_P(ColumnFamilyTest, TrackedDropFailsBeforeManifestOnOptionsError) {
|
||||
db_options_.track_options_file_number_in_manifest = true;
|
||||
auto fail_fs =
|
||||
std::make_shared<FailOptionsFilesFs>(db_options_.env->GetFileSystem());
|
||||
auto fail_env =
|
||||
std::make_unique<CompositeEnvWrapper>(db_options_.env, fail_fs);
|
||||
SaveAndRestore<Env*> save_restore_env(&db_options_.env, fail_env.get());
|
||||
|
||||
Open();
|
||||
ColumnFamilyHandle* handle = nullptr;
|
||||
ASSERT_OK(db_->CreateColumnFamily(ColumnFamilyOptions(), "one", &handle));
|
||||
ASSERT_NE(nullptr, handle);
|
||||
|
||||
fail_fs->fail_options_writes.store(true, std::memory_order_relaxed);
|
||||
ASSERT_NOK(db_->DropColumnFamily(handle));
|
||||
fail_fs->fail_options_writes.store(false, std::memory_order_relaxed);
|
||||
ASSERT_OK(db_->DestroyColumnFamilyHandle(handle));
|
||||
Close();
|
||||
|
||||
AssertColumnFamilyNames({kDefaultColumnFamilyName, "one"});
|
||||
}
|
||||
|
||||
TEST_P(ColumnFamilyTest, TrackedManifestFailureKeepsPreviousOptions) {
|
||||
db_options_.track_options_file_number_in_manifest = true;
|
||||
auto fault_fs =
|
||||
std::make_shared<FaultInjectionTestFS>(db_options_.env->GetFileSystem());
|
||||
std::unique_ptr<Env> fault_env(NewCompositeEnv(fault_fs));
|
||||
SaveAndRestore<Env*> save_restore_env(&db_options_.env, fault_env.get());
|
||||
|
||||
Open();
|
||||
std::string selected_options_before;
|
||||
ASSERT_OK(GetLatestOptionsFileName(dbname_, db_options_.env,
|
||||
&selected_options_before));
|
||||
|
||||
std::atomic<bool> inject_error{true};
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::LogAndApply:WriteManifest", [&](void*) {
|
||||
if (inject_error.exchange(false)) {
|
||||
fault_fs->SetThreadLocalErrorContext(
|
||||
FaultInjectionIOType::kWrite, /*seed=*/0, /*one_in=*/1,
|
||||
/*retryable=*/false, /*has_data_loss=*/false);
|
||||
fault_fs->EnableThreadLocalErrorInjection(
|
||||
FaultInjectionIOType::kWrite);
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ColumnFamilyHandle* handle = nullptr;
|
||||
ASSERT_NOK(db_->CreateColumnFamily(ColumnFamilyOptions(), "one", &handle));
|
||||
ASSERT_EQ(nullptr, handle);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
fault_fs->DisableThreadLocalErrorInjection(FaultInjectionIOType::kWrite);
|
||||
|
||||
std::string selected_options_after;
|
||||
ASSERT_OK(GetLatestOptionsFileName(dbname_, db_options_.env,
|
||||
&selected_options_after));
|
||||
ASSERT_EQ(selected_options_before, selected_options_after);
|
||||
Close();
|
||||
|
||||
AssertColumnFamilyNames({kDefaultColumnFamilyName});
|
||||
}
|
||||
|
||||
TEST_P(ColumnFamilyTest, FirstTrackedCreateFailureIgnoresUncommittedOptions) {
|
||||
// Create a legacy DB without a committed number, then enable tracking while
|
||||
// Open also
|
||||
// needs to create a missing CF. The current topology must be bound before
|
||||
// the proposed topology OPTIONS file can become a durable orphan.
|
||||
Open();
|
||||
Close();
|
||||
db_options_.track_options_file_number_in_manifest = true;
|
||||
db_options_.create_missing_column_families = true;
|
||||
|
||||
auto fault_fs =
|
||||
std::make_shared<FaultInjectionTestFS>(db_options_.env->GetFileSystem());
|
||||
std::unique_ptr<Env> fault_env(NewCompositeEnv(fault_fs));
|
||||
SaveAndRestore<Env*> save_restore_env(&db_options_.env, fault_env.get());
|
||||
|
||||
bool injected = false;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::CreateColumnFamilyImpl:AfterPersistOptions", [&](void*) {
|
||||
if (!injected) {
|
||||
injected = true;
|
||||
fault_fs->SetThreadLocalErrorContext(
|
||||
FaultInjectionIOType::kWrite, /*seed=*/0, /*one_in=*/1,
|
||||
/*retryable=*/false, /*has_data_loss=*/false);
|
||||
fault_fs->EnableThreadLocalErrorInjection(
|
||||
FaultInjectionIOType::kWrite);
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ASSERT_NOK(TryOpen({kDefaultColumnFamilyName, "one"}));
|
||||
ASSERT_TRUE(injected);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
fault_fs->DisableThreadLocalErrorInjection(FaultInjectionIOType::kWrite);
|
||||
|
||||
std::vector<ColumnFamilyDescriptor> loaded_cfs;
|
||||
LoadLatestColumnFamilyDescriptors(&loaded_cfs);
|
||||
ASSERT_EQ(1U, loaded_cfs.size());
|
||||
ASSERT_EQ(kDefaultColumnFamilyName, loaded_cfs.front().name);
|
||||
|
||||
OptionsFileManifestState protocol_state;
|
||||
ASSERT_OK(VersionSet::GetOptionsFileManifestState(
|
||||
dbname_, db_options_.env->GetFileSystem().get(), &protocol_state));
|
||||
ASSERT_TRUE(protocol_state.has_committed_options_file_number);
|
||||
std::string selected_options;
|
||||
ASSERT_OK(
|
||||
GetLatestOptionsFileName(dbname_, db_options_.env, &selected_options));
|
||||
ASSERT_EQ(OptionsFileName(protocol_state.committed_options_file_number),
|
||||
selected_options);
|
||||
|
||||
AssertColumnFamilyNames({kDefaultColumnFamilyName});
|
||||
}
|
||||
|
||||
TEST_P(ColumnFamilyTest, TrackedCreatePersistsCommittedOptions) {
|
||||
db_options_.track_options_file_number_in_manifest = true;
|
||||
Open();
|
||||
|
||||
ColumnFamilyOptions requested;
|
||||
requested.arena_block_size = 0;
|
||||
ColumnFamilyHandle* handle = nullptr;
|
||||
ASSERT_OK(db_->CreateColumnFamily(requested, "one", &handle));
|
||||
const ColumnFamilyOptions effective = db_->GetOptions(handle);
|
||||
ASSERT_GT(effective.arena_block_size, 0U);
|
||||
|
||||
std::vector<ColumnFamilyDescriptor> loaded_cfs;
|
||||
LoadLatestColumnFamilyDescriptors(&loaded_cfs);
|
||||
ASSERT_EQ(2U, loaded_cfs.size());
|
||||
ASSERT_EQ("one", loaded_cfs[1].name);
|
||||
ASSERT_EQ(effective.arena_block_size, loaded_cfs[1].options.arena_block_size);
|
||||
|
||||
ASSERT_OK(db_->DestroyColumnFamilyHandle(handle));
|
||||
}
|
||||
|
||||
TEST_P(ColumnFamilyTest, TrackedBatchTopologyWritesOneOptionsSnapshot) {
|
||||
db_options_.track_options_file_number_in_manifest = true;
|
||||
auto count_fs =
|
||||
std::make_shared<CountOptionsFilesFs>(db_options_.env->GetFileSystem());
|
||||
auto count_env =
|
||||
std::make_unique<CompositeEnvWrapper>(db_options_.env, count_fs);
|
||||
SaveAndRestore<Env*> save_restore_env(&db_options_.env, count_env.get());
|
||||
|
||||
Open();
|
||||
count_fs->options_files_created.store(0, std::memory_order_relaxed);
|
||||
std::vector<ColumnFamilyHandle*> handles;
|
||||
ASSERT_OK(db_->CreateColumnFamilies(ColumnFamilyOptions(),
|
||||
{"one", "two", "three"}, &handles));
|
||||
ASSERT_EQ(3U, handles.size());
|
||||
ASSERT_EQ(1, count_fs->options_files_created.load(std::memory_order_relaxed));
|
||||
|
||||
count_fs->options_files_created.store(0, std::memory_order_relaxed);
|
||||
ASSERT_OK(db_->DropColumnFamilies(handles));
|
||||
ASSERT_EQ(1, count_fs->options_files_created.load(std::memory_order_relaxed));
|
||||
for (auto* handle : handles) {
|
||||
ASSERT_OK(db_->DestroyColumnFamilyHandle(handle));
|
||||
}
|
||||
|
||||
std::vector<ColumnFamilyDescriptor> loaded_cfs;
|
||||
LoadLatestColumnFamilyDescriptors(&loaded_cfs);
|
||||
ASSERT_EQ(1U, loaded_cfs.size());
|
||||
ASSERT_EQ(kDefaultColumnFamilyName, loaded_cfs.front().name);
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_P(ColumnFamilyTest, TrackedPartialBatchFiltersOptionsSuperset) {
|
||||
db_options_.track_options_file_number_in_manifest = true;
|
||||
auto fault_fs =
|
||||
std::make_shared<FaultInjectionTestFS>(db_options_.env->GetFileSystem());
|
||||
std::unique_ptr<Env> fault_env(NewCompositeEnv(fault_fs));
|
||||
SaveAndRestore<Env*> save_restore_env(&db_options_.env, fault_env.get());
|
||||
|
||||
Open();
|
||||
std::atomic<int> manifest_writes{0};
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::LogAndApply:WriteManifest", [&](void*) {
|
||||
if (manifest_writes.fetch_add(1, std::memory_order_relaxed) == 1) {
|
||||
fault_fs->SetThreadLocalErrorContext(
|
||||
FaultInjectionIOType::kWrite, /*seed=*/0, /*one_in=*/1,
|
||||
/*retryable=*/false, /*has_data_loss=*/false);
|
||||
fault_fs->EnableThreadLocalErrorInjection(
|
||||
FaultInjectionIOType::kWrite);
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
std::vector<ColumnFamilyHandle*> handles;
|
||||
ASSERT_NOK(db_->CreateColumnFamilies(ColumnFamilyOptions(), {"one", "two"},
|
||||
&handles));
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
fault_fs->DisableThreadLocalErrorInjection(FaultInjectionIOType::kWrite);
|
||||
ASSERT_EQ(1U, handles.size());
|
||||
|
||||
std::vector<ColumnFamilyDescriptor> loaded_cfs;
|
||||
LoadLatestColumnFamilyDescriptors(&loaded_cfs);
|
||||
ASSERT_EQ(2U, loaded_cfs.size());
|
||||
ASSERT_EQ(kDefaultColumnFamilyName, loaded_cfs[0].name);
|
||||
ASSERT_EQ("one", loaded_cfs[1].name);
|
||||
|
||||
ConfigOptions config_options;
|
||||
config_options.env = db_options_.env;
|
||||
DBOptions loaded_db_options;
|
||||
ASSERT_OK(LoadLatestOptions(config_options, dbname_, &loaded_db_options,
|
||||
&loaded_cfs));
|
||||
ASSERT_OK(CheckOptionsCompatibility(config_options, dbname_,
|
||||
loaded_db_options, loaded_cfs));
|
||||
ASSERT_OK(db_->DestroyColumnFamilyHandle(handles.front()));
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_P(ColumnFamilyTest, TrackedPartialBatchDropFiltersOptionsSuperset) {
|
||||
db_options_.track_options_file_number_in_manifest = true;
|
||||
auto fault_fs =
|
||||
std::make_shared<FaultInjectionTestFS>(db_options_.env->GetFileSystem());
|
||||
std::unique_ptr<Env> fault_env(NewCompositeEnv(fault_fs));
|
||||
SaveAndRestore<Env*> save_restore_env(&db_options_.env, fault_env.get());
|
||||
|
||||
Open();
|
||||
std::vector<ColumnFamilyHandle*> handles;
|
||||
ASSERT_OK(db_->CreateColumnFamilies(ColumnFamilyOptions(), {"one", "two"},
|
||||
&handles));
|
||||
ASSERT_EQ(2U, handles.size());
|
||||
|
||||
std::atomic<int> manifest_writes{0};
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::LogAndApply:WriteManifest", [&](void*) {
|
||||
if (manifest_writes.fetch_add(1, std::memory_order_relaxed) == 1) {
|
||||
fault_fs->SetThreadLocalErrorContext(
|
||||
FaultInjectionIOType::kWrite, /*seed=*/0, /*one_in=*/1,
|
||||
/*retryable=*/false, /*has_data_loss=*/false);
|
||||
fault_fs->EnableThreadLocalErrorInjection(
|
||||
FaultInjectionIOType::kWrite);
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ASSERT_NOK(db_->DropColumnFamilies(handles));
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
fault_fs->DisableThreadLocalErrorInjection(FaultInjectionIOType::kWrite);
|
||||
|
||||
std::vector<ColumnFamilyDescriptor> loaded_cfs;
|
||||
LoadLatestColumnFamilyDescriptors(&loaded_cfs);
|
||||
ASSERT_EQ(2U, loaded_cfs.size());
|
||||
ASSERT_EQ(kDefaultColumnFamilyName, loaded_cfs[0].name);
|
||||
ASSERT_EQ("two", loaded_cfs[1].name);
|
||||
for (auto* handle : handles) {
|
||||
ASSERT_OK(db_->DestroyColumnFamilyHandle(handle));
|
||||
}
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_P(ColumnFamilyTest, InvalidTrackedTopologyChangeCreatesNoOptionsOrphan) {
|
||||
db_options_.track_options_file_number_in_manifest = true;
|
||||
Open();
|
||||
|
||||
ColumnFamilyHandle* handle = nullptr;
|
||||
ASSERT_OK(db_->CreateColumnFamily(ColumnFamilyOptions(), "one", &handle));
|
||||
const size_t before_duplicate_create = CountOptionsFiles();
|
||||
ColumnFamilyHandle* duplicate = nullptr;
|
||||
ASSERT_TRUE(db_->CreateColumnFamily(ColumnFamilyOptions(), "one", &duplicate)
|
||||
.IsInvalidArgument());
|
||||
ASSERT_EQ(nullptr, duplicate);
|
||||
ASSERT_EQ(before_duplicate_create, CountOptionsFiles());
|
||||
|
||||
ASSERT_OK(db_->DropColumnFamily(handle));
|
||||
const size_t before_duplicate_drop = CountOptionsFiles();
|
||||
ASSERT_TRUE(db_->DropColumnFamily(handle).IsInvalidArgument());
|
||||
ASSERT_EQ(before_duplicate_drop, CountOptionsFiles());
|
||||
ASSERT_OK(db_->DestroyColumnFamilyHandle(handle));
|
||||
}
|
||||
|
||||
TEST_P(ColumnFamilyTest,
|
||||
PartialMultiDropConsumesLegacyOptionsPersistenceFailure) {
|
||||
auto fail_fs =
|
||||
std::make_shared<FailOptionsFilesFs>(db_options_.env->GetFileSystem());
|
||||
auto fail_env =
|
||||
std::make_unique<CompositeEnvWrapper>(db_options_.env, fail_fs);
|
||||
SaveAndRestore<Env*> save_restore_env(&db_options_.env, fail_env.get());
|
||||
|
||||
Open();
|
||||
ColumnFamilyHandle* handle = nullptr;
|
||||
ASSERT_OK(db_->CreateColumnFamily(ColumnFamilyOptions(), "one", &handle));
|
||||
|
||||
fail_fs->fail_options_writes.store(true, std::memory_order_relaxed);
|
||||
Status s = db_->DropColumnFamilies({handle, db_->DefaultColumnFamily()});
|
||||
ASSERT_TRUE(s.IsInvalidArgument());
|
||||
fail_fs->fail_options_writes.store(false, std::memory_order_relaxed);
|
||||
|
||||
ASSERT_OK(db_->DestroyColumnFamilyHandle(handle));
|
||||
Close();
|
||||
AssertColumnFamilyNames({kDefaultColumnFamilyName});
|
||||
}
|
||||
|
||||
TEST_P(ColumnFamilyTest, CreateMissingColumnFamilies) {
|
||||
// Can't accidentally add CFs to an existing DB
|
||||
Open();
|
||||
@@ -2264,6 +2715,16 @@ TEST_P(ColumnFamilyTest, CreateMissingColumnFamilies) {
|
||||
ASSERT_OK(TryOpen({"default", "one", "two", "three", "four"}));
|
||||
Close();
|
||||
ASSERT_EQ(my_fs->options_files_created.load(), 2);
|
||||
|
||||
Destroy();
|
||||
db_options_.track_options_file_number_in_manifest = true;
|
||||
my_fs->options_files_created.store(0, std::memory_order_relaxed);
|
||||
ASSERT_OK(TryOpen({"default", "one", "two", "three", "four"}));
|
||||
Close();
|
||||
// A new tracked DB writes its initial snapshot, one proposed batch snapshot,
|
||||
// and the normal final DB::Open snapshot, independent of the number of
|
||||
// missing CFs.
|
||||
ASSERT_EQ(my_fs->options_files_created.load(), 3);
|
||||
}
|
||||
|
||||
TEST_P(ColumnFamilyTest, SanitizeCfOptions) {
|
||||
@@ -3167,6 +3628,32 @@ TEST_P(ColumnFamilyTest, CreateDropAndDestroy) {
|
||||
ASSERT_OK(db_->DestroyColumnFamilyHandle(cfh));
|
||||
}
|
||||
|
||||
TEST_P(ColumnFamilyTest, DropWaitsForUnorderedMemtableWrite) {
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
|
||||
{{"DBImpl::WriteImpl:UnorderedWriteAfterWriteWAL",
|
||||
"ColumnFamilyTest::DropWaitsForUnorderedMemtableWrite:AfterWAL"},
|
||||
{"DBImpl::WaitForPendingWrites:BeforeBlock",
|
||||
"DBImpl::WriteImpl:BeforeUnorderedWriteMemtable"}});
|
||||
|
||||
db_options_.unordered_write = true;
|
||||
Open();
|
||||
ColumnFamilyHandle* cfh;
|
||||
ASSERT_OK(db_->CreateColumnFamily(ColumnFamilyOptions(), "yoyo", &cfh));
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
port::Thread writer(
|
||||
[&] { ASSERT_OK(db_->Put(WriteOptions(), cfh, "foo", "bar")); });
|
||||
|
||||
TEST_SYNC_POINT(
|
||||
"ColumnFamilyTest::DropWaitsForUnorderedMemtableWrite:AfterWAL");
|
||||
ASSERT_OK(db_->DropColumnFamily(cfh));
|
||||
writer.join();
|
||||
ASSERT_OK(db_->DestroyColumnFamilyHandle(cfh));
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_P(ColumnFamilyTest, CreateDropAndDestroyWithoutFileDeletion) {
|
||||
ColumnFamilyHandle* cfh;
|
||||
Open();
|
||||
@@ -3225,6 +3712,12 @@ TEST_P(ColumnFamilyTest, FlushCloseWALFiles) {
|
||||
TEST_P(ColumnFamilyTest, IteratorCloseWALFile1) {
|
||||
SpecialEnv env(Env::Default());
|
||||
db_options_.env = &env;
|
||||
// The DB keeps its Env pointer until Close(). Close it before the local Env
|
||||
// is destroyed, including when an ASSERT below exits the test early.
|
||||
Defer close_db_on_exit([this] {
|
||||
Close();
|
||||
db_options_.env = env_;
|
||||
});
|
||||
db_options_.max_background_flushes = 1;
|
||||
// When this option is removed, the test will need re-engineering
|
||||
db_options_.background_close_inactive_wals = true;
|
||||
@@ -3234,25 +3727,38 @@ TEST_P(ColumnFamilyTest, IteratorCloseWALFile1) {
|
||||
CreateColumnFamilies({"one"});
|
||||
ASSERT_OK(Put(1, "fodor", "mirko"));
|
||||
// Create an iterator holding the current super version.
|
||||
Iterator* it = db_->NewIterator(ReadOptions(), handles_[1]);
|
||||
std::unique_ptr<Iterator> it(db_->NewIterator(ReadOptions(), handles_[1]));
|
||||
ASSERT_OK(it->status());
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
|
||||
{"DBImpl::BGWorkFlush:done",
|
||||
"ColumnFamilyTest::IteratorCloseWALFile1:SetupFlushDone"},
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
Defer disable_sync_points(
|
||||
[] { ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing(); });
|
||||
|
||||
// A flush will make `it` hold the last reference of its super version.
|
||||
ASSERT_OK(Flush(1));
|
||||
// Flush() can return before the background function releases all temporary
|
||||
// references to the old super version.
|
||||
TEST_SYNC_POINT("ColumnFamilyTest::IteratorCloseWALFile1:SetupFlushDone");
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
|
||||
{"ColumnFamilyTest::IteratorCloseWALFile1:ReleaseFlush",
|
||||
"DBImpl::BackgroundCallFlush:start"},
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ASSERT_OK(Put(1, "fodor", "mirko"));
|
||||
ASSERT_OK(Put(0, "fodor", "mirko"));
|
||||
ASSERT_OK(Put(1, "fodor", "mirko"));
|
||||
|
||||
// Flush jobs will close previous WAL files after finishing. By
|
||||
// block flush jobs from running, we trigger a condition where
|
||||
// the iterator destructor should close the WAL files.
|
||||
test::SleepingBackgroundTask sleeping_task;
|
||||
env_->Schedule(&test::SleepingBackgroundTask::DoSleepTask, &sleeping_task,
|
||||
Env::Priority::HIGH);
|
||||
// Make sure the task is sleeping. Otherwise, it might start to execute
|
||||
// after sleeping_task.WaitUntilDone() and cause TSAN warning.
|
||||
sleeping_task.WaitUntilSleeping();
|
||||
|
||||
// Flush jobs close previous WAL files after finishing. Keep the next flush
|
||||
// from starting so the iterator destructor is responsible for closing the
|
||||
// WAL. Occupying one Env worker is insufficient because the shared Env can
|
||||
// have multiple HIGH-priority workers.
|
||||
WriteOptions wo;
|
||||
wo.sync = true;
|
||||
ASSERT_OK(db_->Put(wo, handles_[1], "fodor", "mirko"));
|
||||
@@ -3260,18 +3766,15 @@ TEST_P(ColumnFamilyTest, IteratorCloseWALFile1) {
|
||||
ASSERT_EQ(2, env.num_open_wal_file_.load());
|
||||
// Deleting the iterator will clear its super version, triggering
|
||||
// closing all files
|
||||
delete it;
|
||||
it.reset();
|
||||
ASSERT_EQ(1, env.num_open_wal_file_.load());
|
||||
|
||||
sleeping_task.WakeUp();
|
||||
sleeping_task.WaitUntilDone();
|
||||
TEST_SYNC_POINT("ColumnFamilyTest::IteratorCloseWALFile1:ReleaseFlush");
|
||||
WaitForFlush(1);
|
||||
|
||||
Reopen();
|
||||
ASSERT_EQ("mirko", Get(0, "fodor"));
|
||||
ASSERT_EQ("mirko", Get(1, "fodor"));
|
||||
db_options_.env = env_;
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_P(ColumnFamilyTest, IteratorCloseWALFile2) {
|
||||
@@ -3528,7 +4031,8 @@ TEST_P(ColumnFamilyTest, DefaultCfPathsTest) {
|
||||
// Leave cf_paths for one column families to be empty.
|
||||
// Files should be generated according to db_paths for that
|
||||
// column family.
|
||||
ColumnFamilyOptions cf_opt1, cf_opt2;
|
||||
ColumnFamilyOptions cf_opt1;
|
||||
ColumnFamilyOptions cf_opt2;
|
||||
cf_opt1.cf_paths.emplace_back(dbname_ + "_one_1",
|
||||
std::numeric_limits<uint64_t>::max());
|
||||
CreateColumnFamilies({"one", "two"}, {cf_opt1, cf_opt2});
|
||||
@@ -3553,7 +4057,8 @@ TEST_P(ColumnFamilyTest, DefaultCfPathsTest) {
|
||||
TEST_P(ColumnFamilyTest, MultipleCFPathsTest) {
|
||||
Open();
|
||||
// Configure Column family specific paths.
|
||||
ColumnFamilyOptions cf_opt1, cf_opt2;
|
||||
ColumnFamilyOptions cf_opt1;
|
||||
ColumnFamilyOptions cf_opt2;
|
||||
cf_opt1.cf_paths.emplace_back(dbname_ + "_one_1",
|
||||
std::numeric_limits<uint64_t>::max());
|
||||
cf_opt2.cf_paths.emplace_back(dbname_ + "_two_1",
|
||||
@@ -3669,7 +4174,9 @@ TEST(ColumnFamilyTest, ValidateMemtableKVChecksumOption) {
|
||||
// the behavior of manual flush is that it skips retaining UDTs.
|
||||
class ColumnFamilyRetainUDTTest : public ColumnFamilyTestBase {
|
||||
public:
|
||||
ColumnFamilyRetainUDTTest() : ColumnFamilyTestBase(kLatestBbtFormatVersion) {}
|
||||
ColumnFamilyRetainUDTTest()
|
||||
: ColumnFamilyTestBase(
|
||||
BlockBasedTableOptions::kLatestSupportedFormatVersion) {}
|
||||
|
||||
void SetUp() override {
|
||||
db_options_.allow_concurrent_memtable_write = false;
|
||||
|
||||
+14
-11
@@ -214,7 +214,8 @@ bool Compaction::IsBottommostLevel(
|
||||
} else {
|
||||
output_l0_idx = -1;
|
||||
}
|
||||
Slice smallest_key, largest_key;
|
||||
Slice smallest_key;
|
||||
Slice largest_key;
|
||||
GetBoundaryKeys(vstorage, inputs, &smallest_key, &largest_key);
|
||||
return !vstorage->RangeMightExistAfterSortedRun(smallest_key, largest_key,
|
||||
output_level, output_l0_idx);
|
||||
@@ -491,7 +492,8 @@ int Compaction::GetProximalLevel() const { return proximal_level_; }
|
||||
// smallest_key and largest_key include timestamps if user-defined timestamp is
|
||||
// enabled.
|
||||
bool Compaction::OverlapProximalLevelOutputRange(
|
||||
const Slice& smallest_key, const Slice& largest_key) const {
|
||||
const Slice& smallest_key, const Slice& largest_key,
|
||||
bool range_limit_exclusive) const {
|
||||
if (!SupportsPerKeyPlacement()) {
|
||||
return false;
|
||||
}
|
||||
@@ -506,10 +508,11 @@ bool Compaction::OverlapProximalLevelOutputRange(
|
||||
const Comparator* ucmp =
|
||||
input_vstorage_->InternalComparator()->user_comparator();
|
||||
|
||||
const int largest_cmp = ucmp->CompareWithoutTimestamp(
|
||||
largest_key, proximal_level_smallest_.user_key());
|
||||
return ucmp->CompareWithoutTimestamp(
|
||||
smallest_key, proximal_level_largest_.user_key()) <= 0 &&
|
||||
ucmp->CompareWithoutTimestamp(
|
||||
largest_key, proximal_level_smallest_.user_key()) >= 0;
|
||||
(range_limit_exclusive ? largest_cmp > 0 : largest_cmp >= 0);
|
||||
}
|
||||
|
||||
// key includes timestamp if user-defined timestamp is enabled.
|
||||
@@ -984,9 +987,9 @@ uint64_t Compaction::MaxInputFileNewestKeyTime(const InternalKey* start,
|
||||
return newest_key_time;
|
||||
}
|
||||
|
||||
uint64_t Compaction::MinInputFileOldestAncesterTime(
|
||||
uint64_t Compaction::MinInputFileOldestAncestorTime(
|
||||
const InternalKey* start, const InternalKey* end) const {
|
||||
uint64_t min_oldest_ancester_time = std::numeric_limits<uint64_t>::max();
|
||||
uint64_t min_oldest_ancestor_time = std::numeric_limits<uint64_t>::max();
|
||||
const InternalKeyComparator& icmp =
|
||||
column_family_data()->internal_comparator();
|
||||
for (const auto& level_files : inputs_) {
|
||||
@@ -997,14 +1000,14 @@ uint64_t Compaction::MinInputFileOldestAncesterTime(
|
||||
if (end != nullptr && icmp.Compare(file->smallest, *end) > 0) {
|
||||
continue;
|
||||
}
|
||||
uint64_t oldest_ancester_time = file->TryGetOldestAncesterTime();
|
||||
if (oldest_ancester_time != 0) {
|
||||
min_oldest_ancester_time =
|
||||
std::min(min_oldest_ancester_time, oldest_ancester_time);
|
||||
uint64_t oldest_ancestor_time = file->TryGetOldestAncestorTime();
|
||||
if (oldest_ancestor_time != 0) {
|
||||
min_oldest_ancestor_time =
|
||||
std::min(min_oldest_ancestor_time, oldest_ancestor_time);
|
||||
}
|
||||
}
|
||||
}
|
||||
return min_oldest_ancester_time;
|
||||
return min_oldest_ancestor_time;
|
||||
}
|
||||
|
||||
uint64_t Compaction::MinInputFileEpochNumber() const {
|
||||
|
||||
@@ -114,6 +114,8 @@ class Compaction {
|
||||
// No copying allowed
|
||||
Compaction(const Compaction&) = delete;
|
||||
void operator=(const Compaction&) = delete;
|
||||
Compaction(Compaction&&) = delete;
|
||||
void operator=(Compaction&&) = delete;
|
||||
|
||||
~Compaction();
|
||||
|
||||
@@ -397,7 +399,8 @@ class Compaction {
|
||||
// Both smallest_key and largest_key include timestamps if user-defined
|
||||
// timestamp is enabled.
|
||||
bool OverlapProximalLevelOutputRange(const Slice& smallest_key,
|
||||
const Slice& largest_key) const;
|
||||
const Slice& largest_key,
|
||||
bool range_limit_exclusive) const;
|
||||
|
||||
// For testing purposes, check that a key is within proximal level
|
||||
// output range for per_key_placement feature, which is safe to place the key
|
||||
@@ -437,8 +440,8 @@ class Compaction {
|
||||
const InternalKey* end) const;
|
||||
|
||||
// start and end are sub compact range. Null if no boundary.
|
||||
// This is used to filter out some input files' ancester's time range.
|
||||
uint64_t MinInputFileOldestAncesterTime(const InternalKey* start,
|
||||
// This is used to filter out some input files' ancestor's time range.
|
||||
uint64_t MinInputFileOldestAncestorTime(const InternalKey* start,
|
||||
const InternalKey* end) const;
|
||||
// Return the minimum epoch number among
|
||||
// input files' associated with this compaction
|
||||
|
||||
@@ -1909,8 +1909,7 @@ void CompactionIterator::PrepareOutput() {
|
||||
// KeyNotExistsBeyondOutputLevel() return true?
|
||||
if (Valid() && bottommost_level_ &&
|
||||
DefinitelyInSnapshot(ikey_.sequence, earliest_snapshot_) &&
|
||||
ikey_.type != kTypeMerge && current_key_committed_ &&
|
||||
ikey_.sequence <= preserve_seqno_after_ && !is_range_del_) {
|
||||
ikey_.type != kTypeMerge && current_key_committed_ && !is_range_del_) {
|
||||
assert(compaction_ != nullptr && !compaction_->allow_ingest_behind());
|
||||
if (ikey_.type == kTypeDeletion ||
|
||||
(ikey_.type == kTypeSingleDeletion && timestamp_size_ == 0)) {
|
||||
@@ -1932,19 +1931,29 @@ void CompactionIterator::PrepareOutput() {
|
||||
}
|
||||
|
||||
bool zeroed_seqno = false;
|
||||
if (!timestamp_size_) {
|
||||
current_key_.UpdateInternalKey(0, ikey_.type);
|
||||
zeroed_seqno = true;
|
||||
} else if (full_history_ts_low_ && cmp_with_history_ts_low_ < 0) {
|
||||
// For UDT, the seqno and timestamp could only be zeroed out after the
|
||||
// key is below history_ts_low_.
|
||||
// For the same user key (excluding timestamp), the timestamp-based
|
||||
// history can be collapsed to save some space if the timestamp is
|
||||
// older than *full_history_ts_low_.
|
||||
const std::string kTsMin(timestamp_size_, static_cast<char>(0));
|
||||
const Slice ts_slice = kTsMin;
|
||||
ikey_.SetTimestamp(ts_slice);
|
||||
current_key_.UpdateInternalKey(0, ikey_.type, &ts_slice);
|
||||
// Whether the sequence number can actually be zeroed out here is decided
|
||||
// by the shared BottommostSeqnoCanBeZeroed() predicate, which the
|
||||
// bottommost-file marking logic
|
||||
// (VersionStorageInfo::ComputeBottommostFilesMarkedForCompaction) also
|
||||
// consults. Keeping both on one predicate ensures a file is only marked
|
||||
// for a compaction that can make progress; otherwise the file would be
|
||||
// re-marked forever (infinite compaction loop).
|
||||
if (BottommostSeqnoCanBeZeroed(
|
||||
ikey_.sequence, preserve_seqno_after_, timestamp_size_,
|
||||
full_history_ts_low_ != nullptr, cmp_with_history_ts_low_ < 0)) {
|
||||
if (!timestamp_size_) {
|
||||
current_key_.UpdateInternalKey(0, ikey_.type);
|
||||
} else {
|
||||
// For UDT, the seqno and timestamp could only be zeroed out after the
|
||||
// key is below history_ts_low_.
|
||||
// For the same user key (excluding timestamp), the timestamp-based
|
||||
// history can be collapsed to save some space if the timestamp is
|
||||
// older than *full_history_ts_low_.
|
||||
const std::string kTsMin(timestamp_size_, static_cast<char>(0));
|
||||
const Slice ts_slice = kTsMin;
|
||||
ikey_.SetTimestamp(ts_slice);
|
||||
current_key_.UpdateInternalKey(0, ikey_.type, &ts_slice);
|
||||
}
|
||||
zeroed_seqno = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1593,8 +1593,8 @@ TEST_P(CompactionIteratorTsGcTest, DropTombstones) {
|
||||
std::string full_history_ts_low;
|
||||
PutFixed64(&full_history_ts_low, 102);
|
||||
RunTest(input_keys, input_values, expected_keys, expected_values,
|
||||
/*last_committed_sequence=*/kMaxSequenceNumber,
|
||||
/*merge_op=*/nullptr, /*compaction_filter=*/nullptr,
|
||||
/*last_committed_seq=*/kMaxSequenceNumber,
|
||||
/*merge_operator=*/nullptr, /*compaction_filter=*/nullptr,
|
||||
/*bottommost_level=*/false,
|
||||
/*earliest_write_conflict_snapshot=*/kMaxSequenceNumber,
|
||||
/*key_not_exists_beyond_output_level=*/true, &full_history_ts_low);
|
||||
|
||||
@@ -395,6 +395,24 @@ void CompactionJob::Prepare(
|
||||
std::min(preclude_last_level_min_seqno, preserve_time_min_seqno);
|
||||
#endif
|
||||
|
||||
// For a bottommost-file compaction, which exists only because the marker
|
||||
// (VersionStorageInfo::ComputeBottommostFilesMarkedForCompaction) decided the
|
||||
// file's largest sequence number could be zeroed, honor that same
|
||||
// version-carried preserve boundary here. The marker reads DBImpl's live,
|
||||
// DB-wide seqno->time mapping, whereas the value computed above is derived
|
||||
// only from the selected file's persisted mapping, which can be sparse or
|
||||
// empty (e.g. a file written before preserve/preclude was enabled). Taking
|
||||
// the max ensures this compaction can zero at least the sequence numbers the
|
||||
// marker considered zeroable, so a marked file makes progress instead of
|
||||
// being re-marked forever (infinite compaction loop). Other compaction
|
||||
// reasons keep the per-input-file boundary so their seqno-zeroing behavior is
|
||||
// unchanged. When preserve/preclude is inactive the version value is
|
||||
// kMaxSequenceNumber, making this a no-op.
|
||||
if (c->compaction_reason() == CompactionReason::kBottommostFiles) {
|
||||
preserve_time_min_seqno = std::max(preserve_time_min_seqno,
|
||||
storage_info->GetPreserveTimeMinSeqno());
|
||||
}
|
||||
|
||||
// Preserve sequence numbers for preserved write times and snapshots, though
|
||||
// the specific sequence number of the earliest snapshot can be zeroed.
|
||||
preserve_seqno_after_ =
|
||||
@@ -423,6 +441,17 @@ void CompactionJob::Prepare(
|
||||
c->GetKeepInLastLevelThroughSeqno());
|
||||
|
||||
options_file_number_ = versions_->options_file_number();
|
||||
|
||||
// For remote compaction hardening: capture the current MANIFEST position as a
|
||||
// floor for the worker's recovery. manifest_file_size() is a physical byte
|
||||
// offset that VersionSet::ProcessManifestWrites records under the DB mutex
|
||||
// only after the whole version-edit batch is written and sync'd, so it is a
|
||||
// durable, complete-batch offset -- never mid-record or mid-atomic-group --
|
||||
// at or past the compaction's (already committed) input version.
|
||||
if (mutable_db_options_copy_.remote_compaction_manifest_floor) {
|
||||
min_manifest_file_number_ = versions_->manifest_file_number();
|
||||
min_manifest_file_size_ = versions_->manifest_file_size();
|
||||
}
|
||||
}
|
||||
|
||||
void CompactionJob::MaybeAssignCompactionProgressAndWriter(
|
||||
@@ -2131,20 +2160,20 @@ Status CompactionJob::FinishCompactionOutputFile(
|
||||
|
||||
if (s.ok()) {
|
||||
// With accurate smallest and largest key, we can get a slightly more
|
||||
// accurate oldest ancester time.
|
||||
// This makes oldest ancester time in manifest more accurate than in
|
||||
// accurate oldest ancestor time.
|
||||
// This makes oldest ancestor time in manifest more accurate than in
|
||||
// table properties. Not sure how to resolve it.
|
||||
if (meta->smallest.size() > 0 && meta->largest.size() > 0) {
|
||||
uint64_t refined_oldest_ancester_time;
|
||||
uint64_t refined_oldest_ancestor_time;
|
||||
Slice new_smallest = meta->smallest.user_key();
|
||||
Slice new_largest = meta->largest.user_key();
|
||||
if (!new_largest.empty() && !new_smallest.empty()) {
|
||||
refined_oldest_ancester_time =
|
||||
sub_compact->compaction->MinInputFileOldestAncesterTime(
|
||||
refined_oldest_ancestor_time =
|
||||
sub_compact->compaction->MinInputFileOldestAncestorTime(
|
||||
&(meta->smallest), &(meta->largest));
|
||||
if (refined_oldest_ancester_time !=
|
||||
if (refined_oldest_ancestor_time !=
|
||||
std::numeric_limits<uint64_t>::max()) {
|
||||
meta->oldest_ancester_time = refined_oldest_ancester_time;
|
||||
meta->oldest_ancestor_time = refined_oldest_ancestor_time;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2526,7 +2555,7 @@ Status CompactionJob::OpenCompactionOutputFile(SubcompactionState* sub_compact,
|
||||
return s;
|
||||
}
|
||||
|
||||
// Try to figure out the output file's oldest ancester time.
|
||||
// Try to figure out the output file's oldest ancestor time.
|
||||
int64_t temp_current_time = 0;
|
||||
auto get_time_status = db_options_.clock->GetCurrentTime(&temp_current_time);
|
||||
// Safe to proceed even if GetCurrentTime fails. So, log and proceed.
|
||||
@@ -2536,21 +2565,22 @@ Status CompactionJob::OpenCompactionOutputFile(SubcompactionState* sub_compact,
|
||||
get_time_status.ToString().c_str());
|
||||
}
|
||||
uint64_t current_time = static_cast<uint64_t>(temp_current_time);
|
||||
InternalKey tmp_start, tmp_end;
|
||||
InternalKey tmp_start;
|
||||
InternalKey tmp_end;
|
||||
if (sub_compact->start.has_value()) {
|
||||
tmp_start.SetMinPossibleForUserKey(*(sub_compact->start));
|
||||
}
|
||||
if (sub_compact->end.has_value()) {
|
||||
tmp_end.SetMinPossibleForUserKey(*(sub_compact->end));
|
||||
}
|
||||
uint64_t oldest_ancester_time =
|
||||
sub_compact->compaction->MinInputFileOldestAncesterTime(
|
||||
uint64_t oldest_ancestor_time =
|
||||
sub_compact->compaction->MinInputFileOldestAncestorTime(
|
||||
sub_compact->start.has_value() ? &tmp_start : nullptr,
|
||||
sub_compact->end.has_value() ? &tmp_end : nullptr);
|
||||
if (oldest_ancester_time == std::numeric_limits<uint64_t>::max()) {
|
||||
if (oldest_ancestor_time == std::numeric_limits<uint64_t>::max()) {
|
||||
// TODO: fix DBSSTTest.GetTotalSstFilesSize and use
|
||||
// kUnknownOldestAncesterTime
|
||||
oldest_ancester_time = current_time;
|
||||
// kUnknownOldestAncestorTime
|
||||
oldest_ancestor_time = current_time;
|
||||
}
|
||||
|
||||
uint64_t newest_key_time = sub_compact->compaction->MaxInputFileNewestKeyTime(
|
||||
@@ -2563,7 +2593,7 @@ Status CompactionJob::OpenCompactionOutputFile(SubcompactionState* sub_compact,
|
||||
FileMetaData meta;
|
||||
meta.fd = FileDescriptor(file_number,
|
||||
sub_compact->compaction->output_path_id(), 0);
|
||||
meta.oldest_ancester_time = oldest_ancester_time;
|
||||
meta.oldest_ancestor_time = oldest_ancestor_time;
|
||||
meta.file_creation_time = current_time;
|
||||
meta.epoch_number = epoch_number;
|
||||
meta.temperature = temperature;
|
||||
@@ -2605,7 +2635,8 @@ Status CompactionJob::OpenCompactionOutputFile(SubcompactionState* sub_compact,
|
||||
std::move(writable_file), fname, fo_copy, db_options_.clock, io_tracer_,
|
||||
db_options_.stats, Histograms::SST_WRITE_MICROS, listeners,
|
||||
db_options_.file_checksum_gen_factory.get(),
|
||||
tmp_set.Contains(FileType::kTableFile), false));
|
||||
tmp_set.Contains(FileType::kTableFile),
|
||||
tmp_set.Contains(FileType::kTableFile)));
|
||||
|
||||
// TODO(hx235): pass in the correct `oldest_key_time` instead of `0`
|
||||
const ReadOptions read_options(Env::IOActivity::kCompaction);
|
||||
@@ -3238,7 +3269,8 @@ Status CompactionJob::PersistSubcompactionProgress(
|
||||
s = WritableFileWriter::PrepareIOOptions(write_options, opts);
|
||||
}
|
||||
if (s.ok()) {
|
||||
s = compaction_progress_writer_->file()->Sync(opts, db_options_.use_fsync);
|
||||
s = db_options_.use_fsync ? compaction_progress_writer_->file()->Fsync(opts)
|
||||
: compaction_progress_writer_->file()->Sync(opts);
|
||||
}
|
||||
|
||||
if (!s.ok()) {
|
||||
|
||||
@@ -175,6 +175,7 @@ class CompactionJob {
|
||||
CompactionJob(CompactionJob&& job) = delete;
|
||||
CompactionJob(const CompactionJob& job) = delete;
|
||||
CompactionJob& operator=(const CompactionJob& job) = delete;
|
||||
CompactionJob& operator=(CompactionJob&& job) = delete;
|
||||
|
||||
// REQUIRED: mutex held
|
||||
// Prepare for the compaction by setting up boundaries for each subcompaction
|
||||
@@ -508,7 +509,7 @@ class CompactionJob {
|
||||
std::atomic<int>* num_running_remote_compactions_;
|
||||
|
||||
// Stores the sequence number to time mapping gathered from all input files
|
||||
// it also collects the smallest_seqno -> oldest_ancester_time from the SST.
|
||||
// it also collects the smallest_seqno -> oldest_ancestor_time from the SST.
|
||||
SeqnoToTimeMapping seqno_to_time_mapping_;
|
||||
|
||||
// Max seqno that can be zeroed out in last level, including for preserving
|
||||
@@ -524,6 +525,13 @@ class CompactionJob {
|
||||
// Setting this requires DBMutex.
|
||||
uint64_t options_file_number_ = 0;
|
||||
|
||||
// MANIFEST position (file number and size, in bytes) captured in Prepare()
|
||||
// (mutex held) to send to the remote worker as a floor. Zero when
|
||||
// DBOptions::remote_compaction_manifest_floor is off (or this is not a remote
|
||||
// compaction), meaning no floor is sent. Setting these requires DBMutex.
|
||||
uint64_t min_manifest_file_number_ = 0;
|
||||
uint64_t min_manifest_file_size_ = 0;
|
||||
|
||||
// Writer for persisting compaction progress during compaction
|
||||
log::Writer* compaction_progress_writer_ = nullptr;
|
||||
|
||||
@@ -584,6 +592,7 @@ class CompactionJob {
|
||||
struct CompactionServiceInput {
|
||||
std::string cf_name;
|
||||
|
||||
// Snapshot sequence numbers in strictly increasing order.
|
||||
std::vector<SequenceNumber> snapshots;
|
||||
|
||||
// SST files for compaction, it should already be expended to include all the
|
||||
@@ -603,6 +612,18 @@ struct CompactionServiceInput {
|
||||
|
||||
uint64_t options_file_number = 0;
|
||||
|
||||
// MANIFEST position (file number and size, in bytes) the primary scheduled
|
||||
// this compaction from, used as a floor by the remote worker: it refuses to
|
||||
// reconstruct the compaction against an older MANIFEST view (an older file
|
||||
// number, or the same file number read to a smaller size) and falls back to
|
||||
// local compaction. A non-zero min_manifest_file_number also signals the
|
||||
// worker to use "trust the MANIFEST" recovery. Zero means the primary did not
|
||||
// provide a floor (kill switch off, or an older primary); the worker then
|
||||
// uses the original recovery with no floor check.
|
||||
// See DBOptions::remote_compaction_manifest_floor.
|
||||
uint64_t min_manifest_file_number = 0;
|
||||
uint64_t min_manifest_file_size = 0;
|
||||
|
||||
// serialization interface to read and write the object
|
||||
static Status Read(const std::string& data_str, CompactionServiceInput* obj);
|
||||
Status Write(std::string* output);
|
||||
@@ -621,7 +642,7 @@ struct CompactionServiceOutputFile {
|
||||
SequenceNumber largest_seqno{};
|
||||
std::string smallest_internal_key;
|
||||
std::string largest_internal_key;
|
||||
uint64_t oldest_ancester_time = kUnknownOldestAncesterTime;
|
||||
uint64_t oldest_ancestor_time = kUnknownOldestAncestorTime;
|
||||
uint64_t file_creation_time = kUnknownFileCreationTime;
|
||||
uint64_t epoch_number = kUnknownEpochNumber;
|
||||
std::string file_checksum = kUnknownFileChecksum;
|
||||
@@ -637,7 +658,7 @@ struct CompactionServiceOutputFile {
|
||||
CompactionServiceOutputFile(
|
||||
const std::string& name, uint64_t size, SequenceNumber smallest,
|
||||
SequenceNumber largest, std::string _smallest_internal_key,
|
||||
std::string _largest_internal_key, uint64_t _oldest_ancester_time,
|
||||
std::string _largest_internal_key, uint64_t _oldest_ancestor_time,
|
||||
uint64_t _file_creation_time, uint64_t _epoch_number,
|
||||
const std::string& _file_checksum,
|
||||
const std::string& _file_checksum_func_name, uint64_t _paranoid_hash,
|
||||
@@ -650,7 +671,7 @@ struct CompactionServiceOutputFile {
|
||||
largest_seqno(largest),
|
||||
smallest_internal_key(std::move(_smallest_internal_key)),
|
||||
largest_internal_key(std::move(_largest_internal_key)),
|
||||
oldest_ancester_time(_oldest_ancester_time),
|
||||
oldest_ancestor_time(_oldest_ancestor_time),
|
||||
file_creation_time(_file_creation_time),
|
||||
epoch_number(_epoch_number),
|
||||
file_checksum(_file_checksum),
|
||||
|
||||
@@ -322,8 +322,10 @@ class CompactionJobTestBase : public testing::Test {
|
||||
assert(contents.size() > 0);
|
||||
|
||||
bool first_key = true;
|
||||
std::string smallest, largest;
|
||||
InternalKey smallest_key, largest_key;
|
||||
std::string smallest;
|
||||
std::string largest;
|
||||
InternalKey smallest_key;
|
||||
InternalKey largest_key;
|
||||
SequenceNumber smallest_seqno = kMaxSequenceNumber;
|
||||
SequenceNumber largest_seqno = 0;
|
||||
uint64_t oldest_blob_file_number = kInvalidBlobFileNumber;
|
||||
@@ -387,7 +389,7 @@ class CompactionJobTestBase : public testing::Test {
|
||||
edit.AddFile(
|
||||
level, file_number, 0, file_size, smallest_key, largest_key,
|
||||
smallest_seqno, largest_seqno, false, Temperature::kUnknown,
|
||||
oldest_blob_file_number, kUnknownOldestAncesterTime,
|
||||
oldest_blob_file_number, kUnknownOldestAncestorTime,
|
||||
kUnknownFileCreationTime,
|
||||
versions_->GetColumnFamilySet()->GetDefault()->NewEpochNumber(),
|
||||
kUnknownFileChecksum, kUnknownFileChecksumFuncName, kNullUniqueId64x2,
|
||||
@@ -1572,6 +1574,10 @@ TEST_F(CompactionJobTest, InputSerialization) {
|
||||
while (!rnd.OneIn(10)) {
|
||||
input.snapshots.emplace_back(rnd64.Uniform(UINT64_MAX));
|
||||
}
|
||||
std::sort(input.snapshots.begin(), input.snapshots.end());
|
||||
input.snapshots.erase(
|
||||
std::unique(input.snapshots.begin(), input.snapshots.end()),
|
||||
input.snapshots.end());
|
||||
while (!rnd.OneIn(10)) {
|
||||
input.input_files.emplace_back(rnd.RandomString(
|
||||
rnd.Uniform(kStrMaxLen - 1) +
|
||||
@@ -1641,6 +1647,25 @@ TEST_F(CompactionJobTest, InputSerialization) {
|
||||
ASSERT_TRUE(s.IsNotSupported());
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobTest, OpenAndCompactRejectsUnorderedSnapshots) {
|
||||
for (const std::vector<SequenceNumber>& snapshots :
|
||||
{std::vector<SequenceNumber>{2, 1}, std::vector<SequenceNumber>{1, 1}}) {
|
||||
CompactionServiceInput input;
|
||||
input.snapshots = snapshots;
|
||||
|
||||
std::string serialized_input;
|
||||
ASSERT_OK(input.Write(&serialized_input));
|
||||
|
||||
std::string serialized_result;
|
||||
Status s =
|
||||
DB::OpenAndCompact("", "output", serialized_input, &serialized_result,
|
||||
CompactionServiceOptionsOverride());
|
||||
ASSERT_TRUE(s.IsInvalidArgument());
|
||||
ASSERT_NE(s.ToString().find("snapshots must be strictly increasing"),
|
||||
std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobTest, ResultSerialization) {
|
||||
// Setup a random CompactionServiceResult
|
||||
CompactionServiceResult result;
|
||||
@@ -1679,7 +1704,7 @@ TEST_F(CompactionJobTest, ResultSerialization) {
|
||||
rnd.Uniform(kStrMaxLen)) /* smallest_internal_key */,
|
||||
rnd.RandomBinaryString(
|
||||
rnd.Uniform(kStrMaxLen)) /* largest_internal_key */,
|
||||
rnd64.Uniform(UINT64_MAX) /* oldest_ancester_time */,
|
||||
rnd64.Uniform(UINT64_MAX) /* oldest_ancestor_time */,
|
||||
rnd64.Uniform(UINT64_MAX) /* file_creation_time */,
|
||||
rnd64.Uniform(UINT64_MAX) /* epoch_number */,
|
||||
file_checksum /* file_checksum */,
|
||||
|
||||
@@ -10,7 +10,10 @@
|
||||
|
||||
#include "db/compaction/compaction_outputs.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
|
||||
#include "db/builder.h"
|
||||
#include "logging/logging.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
@@ -32,7 +35,7 @@ Status CompactionOutputs::Finish(
|
||||
meta->fd.largest_seqno);
|
||||
relevant_mapping.SetCapacity(kMaxSeqnoTimePairsPerSST);
|
||||
builder_->SetSeqnoTimeTableProperties(relevant_mapping,
|
||||
meta->oldest_ancester_time);
|
||||
meta->oldest_ancestor_time);
|
||||
s = builder_->Finish();
|
||||
|
||||
} else {
|
||||
@@ -73,7 +76,7 @@ IOStatus CompactionOutputs::WriterSyncClose(const Status& input_status,
|
||||
WriteOptions(Env::IOActivity::kCompaction), opts);
|
||||
if (input_status.ok() && io_s.ok()) {
|
||||
StopWatch sw(clock, statistics, COMPACTION_OUTFILE_SYNC_MICROS);
|
||||
io_s = file_writer_->Sync(opts, use_fsync);
|
||||
io_s = use_fsync ? file_writer_->Fsync(opts) : file_writer_->Sync(opts);
|
||||
}
|
||||
if (input_status.ok() && io_s.ok()) {
|
||||
io_s = file_writer_->Close(opts);
|
||||
@@ -267,11 +270,22 @@ bool CompactionOutputs::ShouldStopBefore(const CompactionIterator& c_iter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If there's user defined partitioner, check that first
|
||||
if (partitioner_ && partitioner_->ShouldPartition(PartitionerRequest(
|
||||
last_key_for_partitioner_, c_iter.user_key(),
|
||||
current_output_file_size_)) == kRequired) {
|
||||
return true;
|
||||
if (partitioner_) {
|
||||
if (partitioner_prefix_valid_) {
|
||||
// Fast path: no per-key copy or virtual call. A partition is requested at
|
||||
// the first key that no longer shares the established prefix. An empty
|
||||
// prefix means no further partitions (every key "starts with" it).
|
||||
if (UNLIKELY(!c_iter.user_key().starts_with(partitioner_prefix_))) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// Slow path: per-key callback (and copy elsewhere)
|
||||
if (UNLIKELY(partitioner_->ShouldPartition(PartitionerRequest(
|
||||
last_key_for_partitioner_, c_iter.user_key(),
|
||||
current_output_file_size_)) == kRequired)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// files output to Level 0 won't be split
|
||||
@@ -362,6 +376,24 @@ bool CompactionOutputs::ShouldStopBefore(const CompactionIterator& c_iter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void CompactionOutputs::PrepareNewOutputForFirstKey(
|
||||
const CompactionIterator& c_iter) {
|
||||
// A new output file starts a fresh partition-tracking window. Establish the
|
||||
// partitioner fast path from this file's first key: if the partitioner can
|
||||
// express its next boundary as "first key not sharing this prefix", cache
|
||||
// that prefix; otherwise fall back to per-key ShouldPartition(). This is the
|
||||
// only place the prefix needs to be (re)derived, since a partition boundary
|
||||
// always forces a file cut, and re-deriving on non-partition cuts just
|
||||
// recomputes the same still-valid prefix.
|
||||
if (partitioner_) {
|
||||
OptSlice prefix = partitioner_->ShouldPartitionByPrefix(c_iter.user_key());
|
||||
partitioner_prefix_valid_ = prefix.has_value();
|
||||
if (partitioner_prefix_valid_) {
|
||||
partitioner_prefix_.assign(prefix->data(), prefix->size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Status CompactionOutputs::AddToOutput(
|
||||
const CompactionIterator& c_iter,
|
||||
const CompactionFileOpenFunc& open_file_func,
|
||||
@@ -401,26 +433,117 @@ Status CompactionOutputs::AddToOutput(
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
// c_iter may emit range deletion keys, so update `last_key_for_partitioner_`
|
||||
// here before returning below when `is_range_del` is true
|
||||
if (partitioner_) {
|
||||
last_key_for_partitioner_.assign(c_iter.user_key().data_,
|
||||
c_iter.user_key().size_);
|
||||
PrepareNewOutputForFirstKey(c_iter);
|
||||
}
|
||||
|
||||
if (UNLIKELY(is_range_del)) {
|
||||
// c_iter may emit range deletion keys, so update
|
||||
// `last_key_for_partitioner_` before returning. Only needed in the fallback
|
||||
// mode; the prefix fast path does not use it.
|
||||
if (partitioner_ && !partitioner_prefix_valid_) {
|
||||
last_key_for_partitioner_.assign(c_iter.user_key().data_,
|
||||
c_iter.user_key().size_);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
assert(builder_ != nullptr);
|
||||
const Slice& value = c_iter.value();
|
||||
s = current_output().validator.Add(key, value);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
bool retried_after_new_table = false;
|
||||
while (true) {
|
||||
s = current_output().validator.CanAdd(key);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
TableBuilderAddContext add_context;
|
||||
TableBuilderAddResult add_result =
|
||||
builder_->TryAdd(key, value, &add_context);
|
||||
|
||||
if (add_result == TableBuilderAddResult::kError) {
|
||||
s = builder_->status();
|
||||
if (s.ok()) {
|
||||
return Status::Corruption(
|
||||
"TableBuilder returned kError without a non-OK status");
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
if (add_result == TableBuilderAddResult::kAdded) {
|
||||
current_output().validator.AddValidated(key, value);
|
||||
break;
|
||||
}
|
||||
|
||||
assert(add_result == TableBuilderAddResult::kRequiresNewTable);
|
||||
s = builder_->status();
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
const Compaction* compaction = compaction_;
|
||||
if (compaction == nullptr) {
|
||||
return Status::Corruption("Compaction output is missing compaction");
|
||||
}
|
||||
ColumnFamilyData* cfd = compaction->column_family_data();
|
||||
if (cfd == nullptr) {
|
||||
return Status::Corruption(
|
||||
"Compaction output is missing column family data");
|
||||
}
|
||||
const bool allow_data_in_errors =
|
||||
compaction->immutable_options().allow_data_in_errors;
|
||||
const std::string key_for_diagnostics =
|
||||
allow_data_in_errors ? key.ToString(/* hex */ true) : "hidden";
|
||||
if (compaction->output_level() == 0) {
|
||||
const std::string detail =
|
||||
"cf: " + cfd->GetName() + ", file_number: " +
|
||||
std::to_string(current_output().meta.fd.GetNumber()) + ", reason: " +
|
||||
TableBuilderNewTableReasonToString(add_context.new_table_reason) +
|
||||
", key: " + key_for_diagnostics;
|
||||
return Status::Corruption(
|
||||
"TableBuilder requested a new table while producing level 0 output",
|
||||
detail);
|
||||
}
|
||||
if (retried_after_new_table || builder_->IsEmpty()) {
|
||||
const std::string detail =
|
||||
"cf: " + cfd->GetName() + ", file_number: " +
|
||||
std::to_string(current_output().meta.fd.GetNumber()) + ", reason: " +
|
||||
TableBuilderNewTableReasonToString(add_context.new_table_reason) +
|
||||
", key: " + key_for_diagnostics;
|
||||
return Status::Corruption(
|
||||
"TableBuilder requested a new table without accepting any rows",
|
||||
detail);
|
||||
}
|
||||
|
||||
ROCKS_LOG_INFO(
|
||||
compaction->immutable_options().info_log,
|
||||
"[%s] Compaction output table builder requested a new table before "
|
||||
"key %s for file #%" PRIu64 ". reason: %s",
|
||||
cfd->GetName().c_str(), key_for_diagnostics.c_str(),
|
||||
current_output().meta.fd.GetNumber(),
|
||||
TableBuilderNewTableReasonToString(add_context.new_table_reason));
|
||||
|
||||
s = close_file_func(c_iter.InputStatus(), prev_iter_output_internal_key,
|
||||
key, &c_iter, *this);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
grandparent_boundary_switched_num_ = 0;
|
||||
grandparent_overlapped_bytes_ =
|
||||
GetCurrentKeyGrandparentOverlappedBytes(key);
|
||||
range_tombstone_lower_bound_.Clear();
|
||||
|
||||
s = open_file_func(*this);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
PrepareNewOutputForFirstKey(c_iter);
|
||||
retried_after_new_table = true;
|
||||
}
|
||||
|
||||
if (partitioner_ && !partitioner_prefix_valid_) {
|
||||
last_key_for_partitioner_.assign(c_iter.user_key().data_,
|
||||
c_iter.user_key().size_);
|
||||
}
|
||||
builder_->Add(key, value);
|
||||
|
||||
stats_.num_output_records++;
|
||||
current_output_file_size_ = builder_->EstimatedFileSize();
|
||||
@@ -485,10 +608,13 @@ Status CompactionOutputs::AddRangeDels(
|
||||
assert(!range_del_agg.IsEmpty());
|
||||
FileMetaData& meta = current_output().meta;
|
||||
const Comparator* ucmp = icmp.user_comparator();
|
||||
InternalKey lower_bound_buf, upper_bound_buf;
|
||||
Slice lower_bound_guard, upper_bound_guard;
|
||||
InternalKey lower_bound_buf;
|
||||
InternalKey upper_bound_buf;
|
||||
Slice lower_bound_guard;
|
||||
Slice upper_bound_guard;
|
||||
std::string smallest_user_key;
|
||||
const Slice *lower_bound, *upper_bound;
|
||||
const Slice* lower_bound;
|
||||
const Slice* upper_bound;
|
||||
|
||||
// We first determine the internal key lower_bound and upper_bound for
|
||||
// this output file. All and only range tombstones that overlap with
|
||||
@@ -790,10 +916,10 @@ void CompactionOutputs::FillFilesToCutForTtl() {
|
||||
*(compaction_->inputs(compaction_->num_input_levels() - 1));
|
||||
for (FileMetaData* file : olevel) {
|
||||
// Worth filtering out by start and end?
|
||||
uint64_t oldest_ancester_time = file->TryGetOldestAncesterTime();
|
||||
uint64_t oldest_ancestor_time = file->TryGetOldestAncestorTime();
|
||||
// We put old files if they are not too small to prevent a flood
|
||||
// of small files.
|
||||
if (oldest_ancester_time < old_age_thres &&
|
||||
if (oldest_ancestor_time < old_age_thres &&
|
||||
file->fd.GetFileSize() >
|
||||
compaction_->mutable_cf_options().target_file_size_base / 2) {
|
||||
files_to_cut_for_ttl_.push_back(file);
|
||||
|
||||
@@ -245,6 +245,11 @@ class CompactionOutputs {
|
||||
// before processing the current key in compaction iterator.
|
||||
bool ShouldStopBefore(const CompactionIterator& c_iter);
|
||||
|
||||
// Re-establishes per-file state that depends on the first key accepted by a
|
||||
// freshly opened builder. This keeps caller-requested file cuts equivalent to
|
||||
// RocksDB's built-in file-boundary decisions for partition tracking.
|
||||
void PrepareNewOutputForFirstKey(const CompactionIterator& c_iter);
|
||||
|
||||
void Cleanup() {
|
||||
if (builder_ != nullptr) {
|
||||
// May happen if we get a shutdown call in the middle of compaction
|
||||
@@ -349,8 +354,19 @@ class CompactionOutputs {
|
||||
const bool is_proximal_level_;
|
||||
|
||||
// partitioner information
|
||||
std::string last_key_for_partitioner_;
|
||||
std::unique_ptr<SstPartitioner> partitioner_;
|
||||
// Previous user key, only maintained (copied per key) in the fallback mode
|
||||
// (see below).
|
||||
std::string last_key_for_partitioner_;
|
||||
// Fast path for partitioners that support ShouldPartitionByPrefix(): when
|
||||
// partitioner_prefix_valid_ is true, all keys in the current output file
|
||||
// share partitioner_prefix_, and a partition boundary is requested at the
|
||||
// first future key that does not start with that prefix. This is (re)derived
|
||||
// once per output file from its first key, avoiding a per-key copy and
|
||||
// virtual ShouldPartition() call. When false, we fall back to comparing
|
||||
// against last_key_for_partitioner_ via ShouldPartition() on every key.
|
||||
std::string partitioner_prefix_;
|
||||
bool partitioner_prefix_valid_ = false;
|
||||
|
||||
// A flag determines if this subcompaction has been split by the cursor
|
||||
// for RoundRobin compaction
|
||||
@@ -360,7 +376,7 @@ class CompactionOutputs {
|
||||
// repetitive comparison in ShouldStopBefore()
|
||||
const InternalKey* local_output_split_key_ = nullptr;
|
||||
|
||||
// Some identified files with old oldest ancester time and the range should be
|
||||
// Some identified files with old oldest ancestor time and the range should be
|
||||
// isolated out so that the output file(s) in that range can be merged down
|
||||
// for TTL and clear the timestamps for the range.
|
||||
std::vector<FileMetaData*> files_to_cut_for_ttl_;
|
||||
|
||||
@@ -236,7 +236,10 @@ void CompactionPicker::GetRange(const CompactionInputFiles& inputs1,
|
||||
} else if (inputs2.empty()) {
|
||||
GetRange(inputs1, smallest, largest);
|
||||
} else {
|
||||
InternalKey smallest1, smallest2, largest1, largest2;
|
||||
InternalKey smallest1;
|
||||
InternalKey smallest2;
|
||||
InternalKey largest1;
|
||||
InternalKey largest2;
|
||||
GetRange(inputs1, &smallest1, &largest1);
|
||||
GetRange(inputs2, &smallest2, &largest2);
|
||||
*smallest =
|
||||
@@ -286,7 +289,8 @@ bool CompactionPicker::ExpandInputsToCleanCut(const std::string& /*cf_name*/,
|
||||
return true;
|
||||
}
|
||||
|
||||
InternalKey smallest, largest;
|
||||
InternalKey smallest;
|
||||
InternalKey largest;
|
||||
|
||||
// Keep expanding inputs until we are sure that there is a "clean cut"
|
||||
// boundary between the files in input and the surrounding files.
|
||||
@@ -319,21 +323,22 @@ bool CompactionPicker::ExpandInputsToCleanCut(const std::string& /*cf_name*/,
|
||||
}
|
||||
|
||||
bool CompactionPicker::RangeOverlapWithCompaction(
|
||||
const Slice& smallest_user_key, const Slice& largest_user_key,
|
||||
int level) const {
|
||||
const Slice& smallest_user_key, const Slice& largest_user_key, int level,
|
||||
bool range_limit_exclusive) const {
|
||||
const Comparator* ucmp = icmp_->user_comparator();
|
||||
for (Compaction* c : compactions_in_progress_) {
|
||||
const int largest_cmp = ucmp->CompareWithoutTimestamp(
|
||||
largest_user_key, c->GetSmallestUserKey());
|
||||
if (c->output_level() == level &&
|
||||
ucmp->CompareWithoutTimestamp(smallest_user_key,
|
||||
c->GetLargestUserKey()) <= 0 &&
|
||||
ucmp->CompareWithoutTimestamp(largest_user_key,
|
||||
c->GetSmallestUserKey()) >= 0) {
|
||||
(range_limit_exclusive ? largest_cmp > 0 : largest_cmp >= 0)) {
|
||||
// Overlap
|
||||
return true;
|
||||
}
|
||||
if (c->SupportsPerKeyPlacement()) {
|
||||
if (c->OverlapProximalLevelOutputRange(smallest_user_key,
|
||||
largest_user_key)) {
|
||||
if (c->OverlapProximalLevelOutputRange(
|
||||
smallest_user_key, largest_user_key, range_limit_exclusive)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -359,27 +364,32 @@ bool CompactionPicker::FilesRangeOverlapWithCompaction(
|
||||
|
||||
// TODO: Intra L0 compactions can have the ranges overlapped, but the input
|
||||
// files cannot be overlapped in the order of L0 files.
|
||||
InternalKey smallest, largest;
|
||||
InternalKey smallest;
|
||||
InternalKey largest;
|
||||
GetRange(inputs, &smallest, &largest, Compaction::kInvalidLevel);
|
||||
if (proximal_level != Compaction::kInvalidLevel) {
|
||||
if (ioptions_.compaction_style == kCompactionStyleUniversal) {
|
||||
if (RangeOverlapWithCompaction(smallest.user_key(), largest.user_key(),
|
||||
proximal_level)) {
|
||||
proximal_level,
|
||||
/*range_limit_exclusive=*/false)) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
InternalKey proximal_smallest, proximal_largest;
|
||||
InternalKey proximal_smallest;
|
||||
InternalKey proximal_largest;
|
||||
GetRange(inputs, &proximal_smallest, &proximal_largest, level);
|
||||
if (RangeOverlapWithCompaction(proximal_smallest.user_key(),
|
||||
proximal_largest.user_key(),
|
||||
proximal_level)) {
|
||||
proximal_level,
|
||||
/*range_limit_exclusive=*/false)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return RangeOverlapWithCompaction(smallest.user_key(), largest.user_key(),
|
||||
level);
|
||||
level,
|
||||
/*range_limit_exclusive=*/false);
|
||||
}
|
||||
|
||||
// Returns true if any one of specified files are being compacted
|
||||
@@ -542,7 +552,8 @@ bool CompactionPicker::SetupOtherInputs(
|
||||
assert(vstorage->NumLevelFiles(l) == 0);
|
||||
}
|
||||
|
||||
InternalKey smallest, largest;
|
||||
InternalKey smallest;
|
||||
InternalKey largest;
|
||||
|
||||
// Get the range one last time.
|
||||
GetRange(*inputs, &smallest, &largest);
|
||||
@@ -575,7 +586,8 @@ bool CompactionPicker::SetupOtherInputs(
|
||||
CompactionInputFiles expanded_inputs;
|
||||
expanded_inputs.level = input_level;
|
||||
// Get closed interval of output level
|
||||
InternalKey all_start, all_limit;
|
||||
InternalKey all_start;
|
||||
InternalKey all_limit;
|
||||
GetRange(*inputs, *output_level_inputs, &all_start, &all_limit);
|
||||
bool try_overlapping_inputs = true;
|
||||
if (only_expand_towards_right) {
|
||||
@@ -601,7 +613,8 @@ bool CompactionPicker::SetupOtherInputs(
|
||||
if (try_overlapping_inputs && expanded_inputs.size() > inputs->size() &&
|
||||
!AreFilesInCompaction(expanded_inputs.files) &&
|
||||
output_level_inputs_size + expanded_inputs_size < limit) {
|
||||
InternalKey new_start, new_limit;
|
||||
InternalKey new_start;
|
||||
InternalKey new_limit;
|
||||
GetRange(expanded_inputs, &new_start, &new_limit);
|
||||
CompactionInputFiles expanded_output_level_inputs;
|
||||
expanded_output_level_inputs.level = output_level;
|
||||
@@ -651,7 +664,8 @@ void CompactionPicker::GetGrandparents(
|
||||
VersionStorageInfo* vstorage, const CompactionInputFiles& inputs,
|
||||
const CompactionInputFiles& output_level_inputs,
|
||||
std::vector<FileMetaData*>* grandparents) {
|
||||
InternalKey start, limit;
|
||||
InternalKey start;
|
||||
InternalKey limit;
|
||||
GetRange(inputs, output_level_inputs, &start, &limit);
|
||||
// Compute the set of grandparent files that overlap this compaction
|
||||
// (parent == level+1; grandparent == level+2 or the first
|
||||
@@ -1299,7 +1313,8 @@ bool CompactionPicker::GetOverlappingL0Files(
|
||||
// Two level 0 compaction won't run at the same time, so don't need to worry
|
||||
// about files on level 0 being compacted.
|
||||
assert(level0_compactions_in_progress()->empty());
|
||||
InternalKey smallest, largest;
|
||||
InternalKey smallest;
|
||||
InternalKey largest;
|
||||
GetRange(*start_level_inputs, &smallest, &largest);
|
||||
// Note that the next call will discard the file we placed in
|
||||
// c->inputs_[0] earlier and replace it with an overlapping set
|
||||
|
||||
@@ -154,8 +154,8 @@ class CompactionPicker {
|
||||
// Return true if the passed key range overlap with a compaction output
|
||||
// that is currently running.
|
||||
bool RangeOverlapWithCompaction(const Slice& smallest_user_key,
|
||||
const Slice& largest_user_key,
|
||||
int level) const;
|
||||
const Slice& largest_user_key, int level,
|
||||
bool range_limit_exclusive) const;
|
||||
|
||||
// Stores the minimal range that covers all entries in inputs in
|
||||
// *smallest, *largest.
|
||||
@@ -276,7 +276,7 @@ class NullCompactionPicker : public CompactionPicker {
|
||||
NullCompactionPicker(const ImmutableOptions& ioptions,
|
||||
const InternalKeyComparator* icmp)
|
||||
: CompactionPicker(ioptions, icmp) {}
|
||||
virtual ~NullCompactionPicker() {}
|
||||
~NullCompactionPicker() override = default;
|
||||
|
||||
// Always return "nullptr"
|
||||
Compaction* PickCompaction(
|
||||
|
||||
@@ -105,9 +105,10 @@ Compaction* FIFOCompactionPicker::PickTTLCompaction(
|
||||
TableReader* reader = f->fd.pinned_reader.Get();
|
||||
if (reader != nullptr && reader->GetTableProperties() != nullptr) {
|
||||
uint64_t newest_key_time = f->TryGetNewestKeyTime();
|
||||
uint64_t creation_time = reader->GetTableProperties()->creation_time;
|
||||
uint64_t oldest_ancestor_time =
|
||||
reader->GetTableProperties()->oldest_ancestor_time;
|
||||
uint64_t est_newest_key_time = newest_key_time == kUnknownNewestKeyTime
|
||||
? creation_time
|
||||
? oldest_ancestor_time
|
||||
: newest_key_time;
|
||||
if (est_newest_key_time == kUnknownNewestKeyTime ||
|
||||
est_newest_key_time >= (current_time - mutable_cf_options.ttl)) {
|
||||
@@ -165,13 +166,13 @@ Compaction* FIFOCompactionPicker::PickTTLCompaction(
|
||||
for (const auto& f : inputs[0].files) {
|
||||
assert(f);
|
||||
uint64_t newest_key_time = f->TryGetNewestKeyTime();
|
||||
uint64_t creation_time = 0;
|
||||
uint64_t oldest_ancestor_time = 0;
|
||||
TableReader* reader = f->fd.pinned_reader.Get();
|
||||
if (reader != nullptr && reader->GetTableProperties() != nullptr) {
|
||||
creation_time = reader->GetTableProperties()->creation_time;
|
||||
oldest_ancestor_time = reader->GetTableProperties()->oldest_ancestor_time;
|
||||
}
|
||||
uint64_t est_newest_key_time = newest_key_time == kUnknownNewestKeyTime
|
||||
? creation_time
|
||||
? oldest_ancestor_time
|
||||
: newest_key_time;
|
||||
ROCKS_LOG_BUFFER(log_buffer,
|
||||
"[%s] FIFO compaction: picking file %" PRIu64
|
||||
@@ -225,7 +226,8 @@ Compaction* FIFOCompactionPicker::PickSizeCompaction(
|
||||
vstorage->LevelFiles(last_level);
|
||||
|
||||
// Compute effective size and limit for comparison.
|
||||
uint64_t effective_size, effective_max;
|
||||
uint64_t effective_size;
|
||||
uint64_t effective_max;
|
||||
GetEffectiveSizeAndLimit(fifo_opts, total_size,
|
||||
vstorage->GetBlobStats().total_file_size,
|
||||
&effective_size, &effective_max);
|
||||
|
||||
@@ -393,7 +393,8 @@ void LevelCompactionBuilder::SetupOtherFilesWithRoundRobinExpansion() {
|
||||
}
|
||||
|
||||
size_t start_index = vstorage_->FilesByCompactionPri(start_level_)[0];
|
||||
InternalKey smallest, largest;
|
||||
InternalKey smallest;
|
||||
InternalKey largest;
|
||||
// Constraint 4 (No need to check again later)
|
||||
compaction_picker_->GetRange(start_level_inputs_, &smallest, &largest);
|
||||
CompactionInputFiles output_level_inputs;
|
||||
@@ -675,7 +676,8 @@ bool LevelCompactionBuilder::TryPickL0TrivialMove() {
|
||||
const std::vector<FileMetaData*>& level_files =
|
||||
vstorage_->LevelFiles(start_level_);
|
||||
|
||||
InternalKey my_smallest, my_largest;
|
||||
InternalKey my_smallest;
|
||||
InternalKey my_largest;
|
||||
for (auto it = level_files.rbegin(); it != level_files.rend(); ++it) {
|
||||
CompactionInputFiles output_level_inputs;
|
||||
output_level_inputs.level = output_level_;
|
||||
@@ -886,7 +888,8 @@ bool LevelCompactionBuilder::PickFileToCompact() {
|
||||
// Note we rely on ExpandInputsToCleanCut() to tell us whether any output-
|
||||
// level files are locked, not just the extra ones pulled in for user-key
|
||||
// overlap.
|
||||
InternalKey smallest, largest;
|
||||
InternalKey smallest;
|
||||
InternalKey largest;
|
||||
compaction_picker_->GetRange(start_level_inputs_, &smallest, &largest);
|
||||
CompactionInputFiles output_level_inputs;
|
||||
output_level_inputs.level = output_level_;
|
||||
|
||||
@@ -90,7 +90,8 @@ class CompactionPickerTestBase : public testing::Test {
|
||||
&icmp_, ucmp_, options_.num_levels, style, nullptr, false,
|
||||
EpochNumberRequirement::kMustPresent, ioptions_.clock,
|
||||
options_.bottommost_file_compaction_delay,
|
||||
OffpeakTimeOption(mutable_db_options_.daily_offpeak_time_utc)));
|
||||
OffpeakTimeOption(mutable_db_options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{}));
|
||||
vstorage_->PrepareForVersionAppend(ioptions_, mutable_cf_options_);
|
||||
}
|
||||
|
||||
@@ -101,7 +102,8 @@ class CompactionPickerTestBase : public testing::Test {
|
||||
&icmp_, ucmp_, options_.num_levels, ioptions_.compaction_style,
|
||||
vstorage_.get(), false, EpochNumberRequirement::kMustPresent,
|
||||
ioptions_.clock, options_.bottommost_file_compaction_delay,
|
||||
OffpeakTimeOption(mutable_db_options_.daily_offpeak_time_utc)));
|
||||
OffpeakTimeOption(mutable_db_options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{}));
|
||||
}
|
||||
|
||||
void DeleteVersionStorage() {
|
||||
@@ -118,7 +120,7 @@ class CompactionPickerTestBase : public testing::Test {
|
||||
SequenceNumber smallest_seq = 100, SequenceNumber largest_seq = 100,
|
||||
size_t compensated_file_size = 0, bool marked_for_compact = false,
|
||||
Temperature temperature = Temperature::kUnknown,
|
||||
uint64_t oldest_ancestor_time = kUnknownOldestAncesterTime,
|
||||
uint64_t oldest_ancestor_time = kUnknownOldestAncestorTime,
|
||||
uint64_t newest_key_time = kUnknownNewestKeyTime,
|
||||
Slice ts_of_smallest = Slice(), Slice ts_of_largest = Slice(),
|
||||
uint64_t epoch_number = kUnknownEpochNumber) {
|
||||
@@ -160,15 +162,15 @@ class CompactionPickerTestBase : public testing::Test {
|
||||
FileMetaData* f = new FileMetaData(
|
||||
file_number, path_id, file_size, smallest_ikey, largest_ikey,
|
||||
smallest_seq, largest_seq, marked_for_compact, temperature,
|
||||
kInvalidBlobFileNumber, kUnknownOldestAncesterTime,
|
||||
kInvalidBlobFileNumber, kUnknownOldestAncestorTime,
|
||||
kUnknownFileCreationTime, epoch_number, kUnknownFileChecksum,
|
||||
kUnknownFileChecksumFuncName, kNullUniqueId64x2, 0, 0,
|
||||
true /* user_defined_timestamps_persisted */, "" /* min timestamp */,
|
||||
"" /* max timestamp */);
|
||||
f->compensated_file_size =
|
||||
(compensated_file_size != 0) ? compensated_file_size : file_size;
|
||||
// oldest_ancester_time is only used if newest_key_time is not available
|
||||
f->oldest_ancester_time = oldest_ancestor_time;
|
||||
// oldest_ancestor_time is only used if newest_key_time is not available
|
||||
f->oldest_ancestor_time = oldest_ancestor_time;
|
||||
// Set min/max timestamps for UDT support
|
||||
if (!ts_of_smallest.empty()) {
|
||||
f->min_timestamp = ts_of_smallest.ToString();
|
||||
@@ -323,7 +325,7 @@ class CompactionPickerU64TsTest : public CompactionPickerTestBase {
|
||||
/*smallest_seq=*/10, /*largest_seq=*/40,
|
||||
/*compensated_file_size=*/1000,
|
||||
/*marked_for_compact=*/false, Temperature::kUnknown,
|
||||
kUnknownOldestAncesterTime, kUnknownNewestKeyTime, ts_small, ts_large);
|
||||
kUnknownOldestAncestorTime, kUnknownNewestKeyTime, ts_small, ts_large);
|
||||
|
||||
std::string full_history_ts_low = MakeU64Timestamp(full_history_ts_low_val);
|
||||
|
||||
@@ -348,12 +350,12 @@ class CompactionPickerU64TsTest : public CompactionPickerTestBase {
|
||||
/*smallest_seq=*/100, /*largest_seq=*/100,
|
||||
/*compensated_file_size=*/file_size,
|
||||
/*marked_for_compact=*/false, Temperature::kUnknown,
|
||||
kUnknownOldestAncesterTime, kUnknownNewestKeyTime, ts1, ts2);
|
||||
kUnknownOldestAncestorTime, kUnknownNewestKeyTime, ts1, ts2);
|
||||
Add(0, 2U, "150", "250", file_size, /*path_id=*/0,
|
||||
/*smallest_seq=*/200, /*largest_seq=*/200,
|
||||
/*compensated_file_size=*/file_size,
|
||||
/*marked_for_compact=*/false, Temperature::kUnknown,
|
||||
kUnknownOldestAncesterTime, kUnknownNewestKeyTime, ts1, ts2);
|
||||
kUnknownOldestAncestorTime, kUnknownNewestKeyTime, ts1, ts2);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1450,14 +1452,14 @@ TEST_F(CompactionPickerTest, FIFOToCold1) {
|
||||
static_cast<uint64_t>(current_time) - kColdThreshold;
|
||||
Add(0 /* level */, 4U /* file_number */, "260", "300", 1 * kFileSize, 0,
|
||||
2500, 2600, 0, true, Temperature::kUnknown,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncesterTime
|
||||
newestKeyTimeKnown ? kUnknownOldestAncestorTime
|
||||
: threshold_time - 2000 /* oldest_ancestor_time */,
|
||||
newestKeyTimeKnown ? threshold_time - 2000
|
||||
: kUnknownNewestKeyTime /* newest_key_time */);
|
||||
// Qualifies for compaction to kCold.
|
||||
Add(0, 3U, "200", "300", 4 * kFileSize, 0, 2300, 2400, 0, true,
|
||||
Temperature::kUnknown,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncesterTime : threshold_time - 3000,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncestorTime : threshold_time - 3000,
|
||||
newestKeyTimeKnown ? threshold_time - 3000
|
||||
: kUnknownNewestKeyTime /* newest_key_time */);
|
||||
UpdateVersionStorageInfo();
|
||||
@@ -1502,32 +1504,32 @@ TEST_F(CompactionPickerTest, FIFOToColdMaxCompactionSize) {
|
||||
static_cast<uint64_t>(current_time) - kColdThreshold;
|
||||
Add(0, 6U, "240", "290", 2 * kFileSize, 0, 2900, 3000, 0, true,
|
||||
Temperature::kUnknown,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncesterTime
|
||||
newestKeyTimeKnown ? kUnknownOldestAncestorTime
|
||||
: static_cast<uint64_t>(current_time) - 100,
|
||||
newestKeyTimeKnown ? static_cast<uint64_t>(current_time) - 100
|
||||
: kUnknownNewestKeyTime);
|
||||
Add(0, 5U, "240", "290", 2 * kFileSize, 0, 2700, 2800, 0, true,
|
||||
Temperature::kUnknown,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncesterTime : threshold_time + 100,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncestorTime : threshold_time + 100,
|
||||
newestKeyTimeKnown ? threshold_time + 100 : kUnknownNewestKeyTime);
|
||||
Add(0, 4U, "260", "300", 1 * kFileSize, 0, 2500, 2600, 0, true,
|
||||
Temperature::kUnknown,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncesterTime : threshold_time - 2000,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncestorTime : threshold_time - 2000,
|
||||
newestKeyTimeKnown ? threshold_time - 2000 : kUnknownNewestKeyTime);
|
||||
// The following two files qualify for compaction to kCold.
|
||||
// But only the last two should be included to respect
|
||||
// `max_compaction_bytes`.
|
||||
Add(0, 3U, "200", "300", 4 * kFileSize, 0, 2300, 2400, 0, true,
|
||||
Temperature::kUnknown,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncesterTime : threshold_time - 3000,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncestorTime : threshold_time - 3000,
|
||||
newestKeyTimeKnown ? threshold_time - 3000 : kUnknownNewestKeyTime);
|
||||
Add(0, 2U, "200", "300", 4 * kFileSize, 0, 2100, 2200, 0, true,
|
||||
Temperature::kUnknown,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncesterTime : threshold_time - 4000,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncestorTime : threshold_time - 4000,
|
||||
newestKeyTimeKnown ? threshold_time - 4000 : kUnknownNewestKeyTime);
|
||||
Add(0, 1U, "200", "300", 4 * kFileSize, 0, 2000, 2100, 0, true,
|
||||
Temperature::kUnknown,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncesterTime : threshold_time - 5000,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncestorTime : threshold_time - 5000,
|
||||
newestKeyTimeKnown ? threshold_time - 5000 : kUnknownNewestKeyTime);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
@@ -1572,30 +1574,30 @@ TEST_F(CompactionPickerTest, FIFOToColdWithExistingCold) {
|
||||
static_cast<uint64_t>(current_time) - kColdThreshold;
|
||||
Add(0, 6U, "240", "290", 2 * kFileSize, 0, 2900, 3000, 0, true,
|
||||
Temperature::kUnknown,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncesterTime
|
||||
newestKeyTimeKnown ? kUnknownOldestAncestorTime
|
||||
: static_cast<uint64_t>(current_time) - 100,
|
||||
newestKeyTimeKnown ? static_cast<uint64_t>(current_time) - 100
|
||||
: kUnknownNewestKeyTime);
|
||||
Add(0, 5U, "240", "290", 2 * kFileSize, 0, 2700, 2800, 0, true,
|
||||
Temperature::kUnknown,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncesterTime : threshold_time + 100,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncestorTime : threshold_time + 100,
|
||||
newestKeyTimeKnown ? threshold_time + 100 : kUnknownNewestKeyTime);
|
||||
Add(0, 4U, "260", "300", 1 * kFileSize, 0, 2500, 2600, 0, true,
|
||||
Temperature::kUnknown,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncesterTime : threshold_time - 2000,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncestorTime : threshold_time - 2000,
|
||||
newestKeyTimeKnown ? threshold_time - 2000 : kUnknownNewestKeyTime);
|
||||
// The following two files qualify for compaction to kCold.
|
||||
Add(0, 3U, "200", "300", 4 * kFileSize, 0, 2300, 2400, 0, true,
|
||||
Temperature::kUnknown,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncesterTime : threshold_time - 3000,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncestorTime : threshold_time - 3000,
|
||||
newestKeyTimeKnown ? threshold_time - 3000 : kUnknownNewestKeyTime);
|
||||
Add(0, 2U, "200", "300", 4 * kFileSize, 0, 2100, 2200, 0, true,
|
||||
Temperature::kUnknown,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncesterTime : threshold_time - 4000,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncestorTime : threshold_time - 4000,
|
||||
newestKeyTimeKnown ? threshold_time - 4000 : kUnknownNewestKeyTime);
|
||||
Add(0, 1U, "200", "300", 4 * kFileSize, 0, 2000, 2100, 0, true,
|
||||
Temperature::kCold,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncesterTime : threshold_time - 5000,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncestorTime : threshold_time - 5000,
|
||||
newestKeyTimeKnown ? threshold_time - 5000 : kUnknownNewestKeyTime);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
@@ -1640,30 +1642,30 @@ TEST_F(CompactionPickerTest, FIFOToColdWithHotBetweenCold) {
|
||||
static_cast<uint64_t>(current_time) - kColdThreshold;
|
||||
Add(0, 6U, "240", "290", 2 * kFileSize, 0, 2900, 3000, 0, true,
|
||||
Temperature::kUnknown,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncesterTime
|
||||
newestKeyTimeKnown ? kUnknownOldestAncestorTime
|
||||
: static_cast<uint64_t>(current_time) - 100,
|
||||
newestKeyTimeKnown ? static_cast<uint64_t>(current_time) - 100
|
||||
: kUnknownNewestKeyTime);
|
||||
Add(0, 5U, "240", "290", 2 * kFileSize, 0, 2700, 2800, 0, true,
|
||||
Temperature::kUnknown,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncesterTime : threshold_time + 100,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncestorTime : threshold_time + 100,
|
||||
newestKeyTimeKnown ? threshold_time + 100 : kUnknownNewestKeyTime);
|
||||
Add(0, 4U, "260", "300", 1 * kFileSize, 0, 2500, 2600, 0, true,
|
||||
Temperature::kUnknown,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncesterTime : threshold_time - 2000,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncestorTime : threshold_time - 2000,
|
||||
newestKeyTimeKnown ? threshold_time - 2000 : kUnknownNewestKeyTime);
|
||||
Add(0, 3U, "200", "300", 4 * kFileSize, 0, 2300, 2400, 0, true,
|
||||
Temperature::kCold,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncesterTime : threshold_time - 3000,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncestorTime : threshold_time - 3000,
|
||||
newestKeyTimeKnown ? threshold_time - 3000 : kUnknownNewestKeyTime);
|
||||
// Qualifies for compaction to kCold.
|
||||
Add(0, 2U, "200", "300", 4 * kFileSize, 0, 2100, 2200, 0, true,
|
||||
Temperature::kUnknown,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncesterTime : threshold_time - 4000,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncestorTime : threshold_time - 4000,
|
||||
newestKeyTimeKnown ? threshold_time - 4000 : kUnknownNewestKeyTime);
|
||||
Add(0, 1U, "200", "300", 4 * kFileSize, 0, 2000, 2100, 0, true,
|
||||
Temperature::kCold,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncesterTime : threshold_time - 5000,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncestorTime : threshold_time - 5000,
|
||||
newestKeyTimeKnown ? threshold_time - 5000 : kUnknownNewestKeyTime);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
@@ -1712,36 +1714,36 @@ TEST_F(CompactionPickerTest, FIFOToHotAndWarm) {
|
||||
static_cast<uint64_t>(current_time) - kWarmThreshold;
|
||||
Add(0, 6U, "240", "290", 2 * kFileSize, 0, 2900, 3000, 0, true,
|
||||
Temperature::kUnknown,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncesterTime
|
||||
newestKeyTimeKnown ? kUnknownOldestAncestorTime
|
||||
: static_cast<uint64_t>(current_time) - 100,
|
||||
newestKeyTimeKnown ? static_cast<uint64_t>(current_time) - 100
|
||||
: kUnknownNewestKeyTime);
|
||||
Add(0, 5U, "240", "290", 2 * kFileSize, 0, 2700, 2800, 0, true,
|
||||
Temperature::kUnknown,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncesterTime
|
||||
newestKeyTimeKnown ? kUnknownOldestAncestorTime
|
||||
: hot_threshold_time + 100,
|
||||
newestKeyTimeKnown ? hot_threshold_time + 100 : kUnknownNewestKeyTime);
|
||||
Add(0, 4U, "260", "300", 1 * kFileSize, 0, 2500, 2600, 0, true,
|
||||
Temperature::kUnknown,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncesterTime
|
||||
newestKeyTimeKnown ? kUnknownOldestAncestorTime
|
||||
: hot_threshold_time - 200,
|
||||
newestKeyTimeKnown ? hot_threshold_time - 200 : kUnknownNewestKeyTime);
|
||||
// Qualifies for Hot
|
||||
Add(0, 3U, "200", "300", 4 * kFileSize, 0, 2300, 2400, 0, true,
|
||||
Temperature::kUnknown,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncesterTime
|
||||
newestKeyTimeKnown ? kUnknownOldestAncestorTime
|
||||
: warm_threshold_time - 100,
|
||||
newestKeyTimeKnown ? warm_threshold_time - 100 : kUnknownNewestKeyTime);
|
||||
// Qualifies for Warm
|
||||
Add(0, 2U, "200", "300", 4 * kFileSize, 0, 2100, 2200, 0, true,
|
||||
Temperature::kUnknown,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncesterTime
|
||||
newestKeyTimeKnown ? kUnknownOldestAncestorTime
|
||||
: warm_threshold_time - 4000,
|
||||
newestKeyTimeKnown ? warm_threshold_time - 4000
|
||||
: kUnknownNewestKeyTime);
|
||||
Add(0, 1U, "200", "300", 4 * kFileSize, 0, 2000, 2100, 0, true,
|
||||
Temperature::kUnknown,
|
||||
newestKeyTimeKnown ? kUnknownOldestAncesterTime
|
||||
newestKeyTimeKnown ? kUnknownOldestAncestorTime
|
||||
: warm_threshold_time - 5000,
|
||||
newestKeyTimeKnown ? warm_threshold_time - 5000
|
||||
: kUnknownNewestKeyTime);
|
||||
@@ -2488,7 +2490,7 @@ TEST_F(CompactionPickerTest, FileTtlBoosterLargeNumLevels) {
|
||||
FileTtlBooster booster(kCurrentTime, /*ttl=*/2048,
|
||||
/*num_non_empty_levels=*/100, /*level=*/1);
|
||||
FileMetaData meta;
|
||||
meta.oldest_ancester_time = kCurrentTime - 1023;
|
||||
meta.oldest_ancestor_time = kCurrentTime - 1023;
|
||||
ASSERT_EQ(1, booster.GetBoostScore(&meta));
|
||||
}
|
||||
|
||||
@@ -2507,47 +2509,47 @@ TEST_F(CompactionPickerTest, FileTtlBooster) {
|
||||
FileTtlBooster booster(kCurrentTime, 2048, 7, 3);
|
||||
|
||||
// Not triggering if the file is younger than ttl/2
|
||||
meta.oldest_ancester_time = kCurrentTime - 1023;
|
||||
meta.oldest_ancestor_time = kCurrentTime - 1023;
|
||||
ASSERT_EQ(1, booster.GetBoostScore(&meta));
|
||||
meta.oldest_ancester_time = kCurrentTime - 1024;
|
||||
meta.oldest_ancestor_time = kCurrentTime - 1024;
|
||||
ASSERT_EQ(1, booster.GetBoostScore(&meta));
|
||||
meta.oldest_ancester_time = kCurrentTime + 10;
|
||||
meta.oldest_ancestor_time = kCurrentTime + 10;
|
||||
ASSERT_EQ(1, booster.GetBoostScore(&meta));
|
||||
|
||||
// Within one boosting step
|
||||
meta.oldest_ancester_time = kCurrentTime - (1024 + 120 + 6);
|
||||
meta.oldest_ancestor_time = kCurrentTime - (1024 + 120 + 6);
|
||||
ASSERT_EQ(1, booster.GetBoostScore(&meta));
|
||||
|
||||
// One boosting step
|
||||
meta.oldest_ancester_time = kCurrentTime - (1024 + 120 + 7);
|
||||
meta.oldest_ancestor_time = kCurrentTime - (1024 + 120 + 7);
|
||||
ASSERT_EQ(2, booster.GetBoostScore(&meta));
|
||||
meta.oldest_ancester_time = kCurrentTime - (1024 + 120 + 8);
|
||||
meta.oldest_ancestor_time = kCurrentTime - (1024 + 120 + 8);
|
||||
ASSERT_EQ(2, booster.GetBoostScore(&meta));
|
||||
|
||||
// Multiple boosting steps
|
||||
meta.oldest_ancester_time = kCurrentTime - (1024 + 120 + 30);
|
||||
meta.oldest_ancestor_time = kCurrentTime - (1024 + 120 + 30);
|
||||
ASSERT_EQ(5, booster.GetBoostScore(&meta));
|
||||
|
||||
// Very high boosting steps
|
||||
meta.oldest_ancester_time = kCurrentTime - (1024 + 120 + 700);
|
||||
meta.oldest_ancestor_time = kCurrentTime - (1024 + 120 + 700);
|
||||
ASSERT_EQ(101, booster.GetBoostScore(&meta));
|
||||
}
|
||||
{
|
||||
// Test second last level
|
||||
FileTtlBooster booster(kCurrentTime, 2048, 7, 5);
|
||||
meta.oldest_ancester_time = kCurrentTime - (1024 + 480);
|
||||
meta.oldest_ancestor_time = kCurrentTime - (1024 + 480);
|
||||
ASSERT_EQ(1, booster.GetBoostScore(&meta));
|
||||
meta.oldest_ancester_time = kCurrentTime - (1024 + 480 + 60);
|
||||
meta.oldest_ancestor_time = kCurrentTime - (1024 + 480 + 60);
|
||||
ASSERT_EQ(3, booster.GetBoostScore(&meta));
|
||||
}
|
||||
{
|
||||
// Test last level
|
||||
FileTtlBooster booster(kCurrentTime, 2048, 7, 6);
|
||||
meta.oldest_ancester_time = kCurrentTime - (1024 + 480);
|
||||
meta.oldest_ancestor_time = kCurrentTime - (1024 + 480);
|
||||
ASSERT_EQ(1, booster.GetBoostScore(&meta));
|
||||
meta.oldest_ancester_time = kCurrentTime - (1024 + 480 + 60);
|
||||
meta.oldest_ancestor_time = kCurrentTime - (1024 + 480 + 60);
|
||||
ASSERT_EQ(1, booster.GetBoostScore(&meta));
|
||||
meta.oldest_ancester_time = kCurrentTime - 3000;
|
||||
meta.oldest_ancestor_time = kCurrentTime - 3000;
|
||||
ASSERT_EQ(1, booster.GetBoostScore(&meta));
|
||||
}
|
||||
}
|
||||
@@ -3575,21 +3577,21 @@ TEST_F(CompactionPickerTest, UniversalMarkedCompactionFullOverlap) {
|
||||
|
||||
Add(0, 1U, "150", "200", kFileSize, 0, 500, 550, /*compensated_file_size*/ 0,
|
||||
/*marked_for_compact*/ false, /* temperature*/ Temperature::kUnknown,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncesterTime,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncestorTime,
|
||||
/*newest_key_time*/ kUnknownNewestKeyTime,
|
||||
/*ts_of_smallest*/ Slice(), /*ts_of_largest*/ Slice(),
|
||||
/*epoch_number*/ 3);
|
||||
Add(0, 2U, "201", "250", 2 * kFileSize, 0, 401, 450,
|
||||
/*compensated_file_size*/ 0, /*marked_for_compact*/ false,
|
||||
/* temperature*/ Temperature::kUnknown,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncesterTime,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncestorTime,
|
||||
/*newest_key_time*/ kUnknownNewestKeyTime,
|
||||
/*ts_of_smallest*/ Slice(), /*ts_of_largest*/ Slice(),
|
||||
/*epoch_number*/ 2);
|
||||
Add(0, 4U, "260", "300", 4 * kFileSize, 0, 260, 300,
|
||||
/*compensated_file_size*/ 0, /*marked_for_compact*/ false,
|
||||
/* temperature*/ Temperature::kUnknown,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncesterTime,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncestorTime,
|
||||
/*newest_key_time*/ kUnknownNewestKeyTime,
|
||||
/*ts_of_smallest*/ Slice(), /*ts_of_largest*/ Slice(),
|
||||
/*epoch_number*/ 1);
|
||||
@@ -3617,7 +3619,7 @@ TEST_F(CompactionPickerTest, UniversalMarkedCompactionFullOverlap) {
|
||||
// Simulate a flush and mark the file for compaction
|
||||
Add(0, 7U, "150", "200", kFileSize, 0, 551, 600, 0, true,
|
||||
/* temperature*/ Temperature::kUnknown,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncesterTime,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncestorTime,
|
||||
/*newest_key_time*/ kUnknownNewestKeyTime,
|
||||
/*ts_of_smallest*/ Slice(), /*ts_of_largest*/ Slice(),
|
||||
/*epoch_number*/ 4);
|
||||
@@ -3645,7 +3647,7 @@ TEST_F(CompactionPickerTest, UniversalMarkedCompactionFullOverlap2) {
|
||||
// Mark file number 4 for compaction
|
||||
Add(0, 4U, "260", "300", 4 * kFileSize, 0, 260, 300, 0, true,
|
||||
/* temperature*/ Temperature::kUnknown,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncesterTime,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncestorTime,
|
||||
/*newest_key_time*/ kUnknownNewestKeyTime,
|
||||
/*ts_of_smallest*/ Slice(), /*ts_of_largest*/ Slice(),
|
||||
/*epoch_number*/ 1);
|
||||
@@ -3672,14 +3674,14 @@ TEST_F(CompactionPickerTest, UniversalMarkedCompactionFullOverlap2) {
|
||||
AddVersionStorage();
|
||||
Add(0, 1U, "150", "200", kFileSize, 0, 500, 550, /*compensated_file_size*/ 0,
|
||||
/*marked_for_compact*/ false, /* temperature*/ Temperature::kUnknown,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncesterTime,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncestorTime,
|
||||
/*newest_key_time*/ kUnknownNewestKeyTime,
|
||||
/*ts_of_smallest*/ Slice(), /*ts_of_largest*/ Slice(),
|
||||
/*epoch_number*/ 3);
|
||||
Add(0, 2U, "201", "250", 2 * kFileSize, 0, 401, 450,
|
||||
/*compensated_file_size*/ 0, /*marked_for_compact*/ false,
|
||||
/* temperature*/ Temperature::kUnknown,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncesterTime,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncestorTime,
|
||||
/*newest_key_time*/ kUnknownNewestKeyTime,
|
||||
/*ts_of_smallest*/ Slice(), /*ts_of_largest*/ Slice(),
|
||||
/*epoch_number*/ 2);
|
||||
@@ -3857,27 +3859,27 @@ TEST_F(CompactionPickerTest, UniversalMarkedL0Overlap2) {
|
||||
Add(0, 4U, "260", "300", 1 * kFileSize, 0, 260, 300,
|
||||
/*compensated_file_size*/ 0, /*marked_for_compact*/ false,
|
||||
/* temperature*/ Temperature::kUnknown,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncesterTime,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncestorTime,
|
||||
/*newest_key_time*/ kUnknownNewestKeyTime,
|
||||
/*ts_of_smallest*/ Slice(), /*ts_of_largest*/ Slice(),
|
||||
/*epoch_number*/ 4);
|
||||
Add(0, 5U, "240", "290", 2 * kFileSize, 0, 201, 250, 0, true,
|
||||
/* temperature*/ Temperature::kUnknown,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncesterTime,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncestorTime,
|
||||
/*newest_key_time*/ kUnknownNewestKeyTime,
|
||||
/*ts_of_smallest*/ Slice(), /*ts_of_largest*/ Slice(),
|
||||
/*epoch_number*/ 3);
|
||||
Add(0, 3U, "301", "350", 4 * kFileSize, 0, 101, 150,
|
||||
/*compensated_file_size*/ 0, /*marked_for_compact*/ false,
|
||||
/* temperature*/ Temperature::kUnknown,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncesterTime,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncestorTime,
|
||||
/*newest_key_time*/ kUnknownNewestKeyTime,
|
||||
/*ts_of_smallest*/ Slice(), /*ts_of_largest*/ Slice(),
|
||||
/*epoch_number*/ 2);
|
||||
Add(0, 6U, "501", "750", 8 * kFileSize, 0, 50, 100,
|
||||
/*compensated_file_size*/ 0, /*marked_for_compact*/ false,
|
||||
/* temperature*/ Temperature::kUnknown,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncesterTime,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncestorTime,
|
||||
/*newest_key_time*/ kUnknownNewestKeyTime,
|
||||
/*ts_of_smallest*/ Slice(), /*ts_of_largest*/ Slice(),
|
||||
/*epoch_number*/ 1);
|
||||
@@ -3904,14 +3906,14 @@ TEST_F(CompactionPickerTest, UniversalMarkedL0Overlap2) {
|
||||
Add(0, 1U, "150", "200", kFileSize, 0, 500, 550, /*compensated_file_size*/ 0,
|
||||
/*marked_for_compact*/ false,
|
||||
/* temperature*/ Temperature::kUnknown,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncesterTime,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncestorTime,
|
||||
/*newest_key_time*/ kUnknownNewestKeyTime,
|
||||
/*ts_of_smallest*/ Slice(), /*ts_of_largest*/ Slice(),
|
||||
/*epoch_number*/ 6);
|
||||
Add(0, 2U, "201", "250", kFileSize, 0, 401, 450, /*compensated_file_size*/ 0,
|
||||
/*marked_for_compact*/ false,
|
||||
/* temperature*/ Temperature::kUnknown,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncesterTime,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncestorTime,
|
||||
/*newest_key_time*/ kUnknownNewestKeyTime,
|
||||
/*ts_of_smallest*/ Slice(), /*ts_of_largest*/ Slice(),
|
||||
/*epoch_number*/ 5);
|
||||
@@ -4147,7 +4149,7 @@ TEST_F(CompactionPickerU64TsTest, Overlap) {
|
||||
/*file_size=*/1U, /*path_id=*/0,
|
||||
/*smallest_seq=*/100, /*largest_seq=*/100, /*compensated_file_size=*/0,
|
||||
/*marked_for_compact=*/false, /*temperature=*/Temperature::kUnknown,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncesterTime,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncestorTime,
|
||||
/*newest_key_time*/ kUnknownNewestKeyTime, ts1, ts2);
|
||||
UpdateVersionStorageInfo();
|
||||
}
|
||||
@@ -4162,6 +4164,18 @@ TEST_F(CompactionPickerU64TsTest, Overlap) {
|
||||
CompactionOptions(), input_files, level, vstorage_.get(),
|
||||
mutable_cf_options_, mutable_db_options_, /*output_path_id=*/0));
|
||||
|
||||
{
|
||||
std::string range_start("400");
|
||||
PutFixed64(&range_start, ts_of_smallest);
|
||||
std::string range_limit(smallest);
|
||||
PutFixed64(&range_limit, ts_of_smallest);
|
||||
ASSERT_FALSE(level_compaction_picker.RangeOverlapWithCompaction(
|
||||
range_start, range_limit, level, /*range_limit_exclusive=*/true));
|
||||
ASSERT_TRUE(level_compaction_picker.RangeOverlapWithCompaction(
|
||||
range_start, range_limit, level,
|
||||
/*range_limit_exclusive=*/false));
|
||||
}
|
||||
|
||||
{
|
||||
// [600, ts=50000] to [600, ts=50000] is the range to check.
|
||||
// ucmp->Compare(smallest_user_key, c->GetLargestUserKey()) > 0, but
|
||||
@@ -4173,7 +4187,8 @@ TEST_F(CompactionPickerU64TsTest, Overlap) {
|
||||
std::string user_key_with_ts2(largest);
|
||||
PutFixed64(&user_key_with_ts2, ts_of_largest - 1);
|
||||
ASSERT_TRUE(level_compaction_picker.RangeOverlapWithCompaction(
|
||||
user_key_with_ts1, user_key_with_ts2, level));
|
||||
user_key_with_ts1, user_key_with_ts2, level,
|
||||
/*range_limit_exclusive=*/false));
|
||||
}
|
||||
{
|
||||
// [500, ts=60000] to [500, ts=60000] is the range to check.
|
||||
@@ -4186,7 +4201,8 @@ TEST_F(CompactionPickerU64TsTest, Overlap) {
|
||||
std::string user_key_with_ts2(smallest);
|
||||
PutFixed64(&user_key_with_ts2, ts_of_smallest + 1);
|
||||
ASSERT_TRUE(level_compaction_picker.RangeOverlapWithCompaction(
|
||||
user_key_with_ts1, user_key_with_ts2, level));
|
||||
user_key_with_ts1, user_key_with_ts2, level,
|
||||
/*range_limit_exclusive=*/false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4215,12 +4231,12 @@ TEST_F(CompactionPickerU64TsTest, CannotTrivialMoveUniversal) {
|
||||
Add(1, 1U, "150", "150", kFileSize, /*path_id=*/0, /*smallest_seq=*/100,
|
||||
/*largest_seq=*/100, /*compensated_file_size=*/kFileSize,
|
||||
/*marked_for_compact=*/false, Temperature::kUnknown,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncesterTime,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncestorTime,
|
||||
/*newest_key_time*/ kUnknownNewestKeyTime, ts1, ts2);
|
||||
Add(2, 2U, "150", "150", kFileSize, /*path_id=*/0, /*smallest_seq=*/100,
|
||||
/*largest_seq=*/100, /*compensated_file_size=*/kFileSize,
|
||||
/*marked_for_compact=*/false, Temperature::kUnknown,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncesterTime,
|
||||
/*oldest_ancestor_time*/ kUnknownOldestAncestorTime,
|
||||
/*newest_key_time*/ kUnknownNewestKeyTime, ts3, ts4);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
@@ -5017,12 +5033,12 @@ TEST_F(CompactionPickerTest, StandaloneRangeDeletionOnlyPicksOlderFiles) {
|
||||
// Create L0 files with overlapping ranges
|
||||
// File 1: newest regular file (epoch 5), keys [100, 200]
|
||||
Add(0, 1U, "100", "200", 1U, 0, 100, 100, 0, false, Temperature::kUnknown,
|
||||
kUnknownOldestAncesterTime, kUnknownNewestKeyTime, Slice(), Slice(), 5);
|
||||
kUnknownOldestAncestorTime, kUnknownNewestKeyTime, Slice(), Slice(), 5);
|
||||
|
||||
// File 2: standalone range deletion (epoch 4), keys [150, 250]
|
||||
// This file should be marked as having only range deletions
|
||||
Add(0, 2U, "150", "250", 1U, 0, 200, 200, 0, true, Temperature::kUnknown,
|
||||
kUnknownOldestAncesterTime, kUnknownNewestKeyTime, Slice(), Slice(), 4);
|
||||
kUnknownOldestAncestorTime, kUnknownNewestKeyTime, Slice(), Slice(), 4);
|
||||
|
||||
// Manually set file 2 as standalone range deletion
|
||||
FileMetaData* range_del_file = file_map_[2U].first;
|
||||
@@ -5058,6 +5074,41 @@ TEST_F(CompactionPickerTest, StandaloneRangeDeletionOnlyPicksOlderFiles) {
|
||||
ASSERT_EQ(10U, compaction->input(1, 0)->fd.GetNumber());
|
||||
}
|
||||
|
||||
// Tests for the seqno->time preserve window controlling bottommost file
|
||||
// marking. A bottommost file whose largest seqno is still within the preserve
|
||||
// window cannot have its seqno zeroed, so it must not be marked (otherwise the
|
||||
// rewrite makes no progress and the file is re-marked forever -- an infinite
|
||||
// loop).
|
||||
TEST_F(CompactionPickerTest, BottommostFileNotMarkedWithinPreserveWindow) {
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
Add(5, 1U, "100", "200", /*file_size=*/1000, /*path_id=*/0,
|
||||
/*smallest_seq=*/90, /*largest_seq=*/100);
|
||||
// preserve_time_min_seqno = 100 => preserve_seqno_after = 99; largest_seqno
|
||||
// 100 is within the window (> 99) and cannot be zeroed.
|
||||
vstorage_->SetPreserveTimeMinSeqno(100);
|
||||
UpdateVersionStorageInfo();
|
||||
vstorage_->UpdateOldestSnapshot(/*oldest_snapshot_seqnum=*/200,
|
||||
/*allow_ingest_behind=*/false, /*ucmp=*/ucmp_,
|
||||
/*full_history_ts_low=*/"");
|
||||
ASSERT_EQ(0U, vstorage_->BottommostFilesMarkedForCompaction().size());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, BottommostFileMarkedBelowPreserveWindow) {
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
Add(5, 1U, "100", "200", /*file_size=*/1000, /*path_id=*/0,
|
||||
/*smallest_seq=*/90, /*largest_seq=*/100);
|
||||
// preserve_time_min_seqno = 150 => preserve_seqno_after = 149; largest_seqno
|
||||
// 100 <= 149 so it can be zeroed and the file is marked as usual.
|
||||
vstorage_->SetPreserveTimeMinSeqno(150);
|
||||
UpdateVersionStorageInfo();
|
||||
vstorage_->UpdateOldestSnapshot(/*oldest_snapshot_seqnum=*/200,
|
||||
/*allow_ingest_behind=*/false, /*ucmp=*/ucmp_,
|
||||
/*full_history_ts_low=*/"");
|
||||
ASSERT_EQ(1U, vstorage_->BottommostFilesMarkedForCompaction().size());
|
||||
ASSERT_EQ(1U, vstorage_->BottommostFilesMarkedForCompaction()[0]
|
||||
.second->fd.GetNumber());
|
||||
}
|
||||
|
||||
// Tests for full_history_ts_low parameter in compaction picker.
|
||||
// The full_history_ts_low parameter is used to control bottommost file marking
|
||||
// for compaction when user-defined timestamps (UDT) are enabled.
|
||||
@@ -5118,7 +5169,7 @@ TEST_F(CompactionPickerU64TsTest,
|
||||
/*smallest_seq=*/10, /*largest_seq=*/40,
|
||||
/*compensated_file_size=*/1000,
|
||||
/*marked_for_compact=*/false, Temperature::kUnknown,
|
||||
kUnknownOldestAncesterTime, kUnknownNewestKeyTime, ts_small, ts_large);
|
||||
kUnknownOldestAncestorTime, kUnknownNewestKeyTime, ts_small, ts_large);
|
||||
|
||||
// Update version storage with empty full_history_ts_low
|
||||
UpdateVersionStorageInfo();
|
||||
@@ -5137,6 +5188,90 @@ TEST_F(CompactionPickerU64TsTest,
|
||||
ASSERT_EQ(0U, vstorage_->BottommostFilesMarkedForCompaction().size());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerU64TsTest,
|
||||
BottommostNotMarkedWithEmptyFullHistoryTsLowAndUnknownMaxTs) {
|
||||
// Regression test for an infinite bottommost compaction loop. A bottommost
|
||||
// UDT file whose max_timestamp was not recorded in its FileMetaData (e.g. a
|
||||
// file written by an older version, or one whose timestamp property is
|
||||
// otherwise absent) must NOT be marked for compaction when
|
||||
// full_history_ts_low is unset. Under UDT the only progress a bottommost
|
||||
// compaction can make here is zeroing sequence numbers, which
|
||||
// CompactionIterator does only when full_history_ts_low is set. With it unset
|
||||
// the compaction cannot advance the file, so the rewritten output
|
||||
// re-qualifies forever.
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
|
||||
std::string ts_small = MakeU64Timestamp(50);
|
||||
std::string ts_large = MakeU64Timestamp(100);
|
||||
|
||||
// Add a bottommost file with seqno < oldest_snapshot.
|
||||
Add(5, 1U, "100", "200", /*file_size=*/1000, /*path_id=*/0,
|
||||
/*smallest_seq=*/10, /*largest_seq=*/40,
|
||||
/*compensated_file_size=*/1000,
|
||||
/*marked_for_compact=*/false, Temperature::kUnknown,
|
||||
kUnknownOldestAncestorTime, kUnknownNewestKeyTime, ts_small, ts_large);
|
||||
|
||||
// Simulate a file whose min/max timestamp was never recorded.
|
||||
ASSERT_EQ(1U, vstorage_->LevelFiles(5).size());
|
||||
FileMetaData* f = vstorage_->LevelFiles(5)[0];
|
||||
f->min_timestamp.clear();
|
||||
f->max_timestamp.clear();
|
||||
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
vstorage_->UpdateOldestSnapshot(
|
||||
/*oldest_snapshot_seqnum=*/50,
|
||||
/*allow_ingest_behind=*/false,
|
||||
/*ucmp=*/ucmp_,
|
||||
/*full_history_ts_low=*/"");
|
||||
|
||||
// No bottommost file can make progress with empty full_history_ts_low, so
|
||||
// nothing is marked even though max_timestamp is unknown. Before the fix this
|
||||
// file was marked, producing an infinite compaction loop.
|
||||
ASSERT_EQ(0U, vstorage_->BottommostFilesMarkedForCompaction().size());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerU64TsTest,
|
||||
BottommostMarkedWithSetFullHistoryTsLowAndUnknownMaxTs) {
|
||||
// A bottommost UDT file whose max_timestamp was not recorded (e.g. written by
|
||||
// an older version) must still be marked for a one-time backfill compaction
|
||||
// when full_history_ts_low IS set: that compaction can collapse timestamp
|
||||
// history / zero seqnos and record max_timestamp, so it makes progress
|
||||
// (bounded, not a loop). This complements
|
||||
// BottommostNotMarkedWithEmptyFullHistoryTsLowAndUnknownMaxTs, which covers
|
||||
// the unset case where no progress is possible.
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
|
||||
std::string ts_small = MakeU64Timestamp(50);
|
||||
std::string ts_large = MakeU64Timestamp(100);
|
||||
|
||||
Add(5, 1U, "100", "200", /*file_size=*/1000, /*path_id=*/0,
|
||||
/*smallest_seq=*/10, /*largest_seq=*/40,
|
||||
/*compensated_file_size=*/1000,
|
||||
/*marked_for_compact=*/false, Temperature::kUnknown,
|
||||
kUnknownOldestAncestorTime, kUnknownNewestKeyTime, ts_small, ts_large);
|
||||
|
||||
// Simulate a file whose min/max timestamp was never recorded.
|
||||
ASSERT_EQ(1U, vstorage_->LevelFiles(5).size());
|
||||
FileMetaData* f = vstorage_->LevelFiles(5)[0];
|
||||
f->min_timestamp.clear();
|
||||
f->max_timestamp.clear();
|
||||
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
std::string full_history_ts_low = MakeU64Timestamp(500);
|
||||
vstorage_->UpdateOldestSnapshot(
|
||||
/*oldest_snapshot_seqnum=*/50,
|
||||
/*allow_ingest_behind=*/false,
|
||||
/*ucmp=*/ucmp_, full_history_ts_low);
|
||||
|
||||
// With full_history_ts_low set, the unknown-max_timestamp file is marked so a
|
||||
// bounded compaction can backfill its metadata and reclaim obsolete data.
|
||||
ASSERT_EQ(1U, vstorage_->BottommostFilesMarkedForCompaction().size());
|
||||
ASSERT_EQ(1U, vstorage_->BottommostFilesMarkedForCompaction()[0]
|
||||
.second->fd.GetNumber());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerU64TsTest, LevelPickCompactionWithFullHistoryTsLow) {
|
||||
// Test that level compaction correctly passes full_history_ts_low
|
||||
// and picks compaction appropriately
|
||||
@@ -5580,13 +5715,13 @@ TEST_F(CompactionPickerTest, FIFOTTLBlobEstimationSingleLevel) {
|
||||
|
||||
uint64_t recent_time = static_cast<uint64_t>(time(nullptr));
|
||||
Add(0, 1U, "100", "200", 50 * 1024, 0, 100, 100, 0, false,
|
||||
Temperature::kUnknown, kUnknownOldestAncesterTime, recent_time);
|
||||
Temperature::kUnknown, kUnknownOldestAncestorTime, recent_time);
|
||||
Add(0, 2U, "200", "300", 50 * 1024, 0, 100, 100, 0, false,
|
||||
Temperature::kUnknown, kUnknownOldestAncesterTime, recent_time);
|
||||
Temperature::kUnknown, kUnknownOldestAncestorTime, recent_time);
|
||||
Add(0, 3U, "300", "400", 50 * 1024, 0, 100, 100, 0, false,
|
||||
Temperature::kUnknown, kUnknownOldestAncesterTime, 1);
|
||||
Temperature::kUnknown, kUnknownOldestAncestorTime, 1);
|
||||
Add(0, 4U, "400", "500", 50 * 1024, 0, 100, 100, 0, false,
|
||||
Temperature::kUnknown, kUnknownOldestAncesterTime, 1);
|
||||
Temperature::kUnknown, kUnknownOldestAncestorTime, 1);
|
||||
if (blob_total > 0) {
|
||||
AddBlobFile(100, blob_total / 2);
|
||||
AddBlobFile(101, blob_total / 2);
|
||||
@@ -5659,13 +5794,13 @@ TEST_F(CompactionPickerTest, FIFOTTLBlobEstimationMultiLevel) {
|
||||
uint64_t recent_time = static_cast<uint64_t>(time(nullptr));
|
||||
// L0 files: 2 recent, 2 expired
|
||||
Add(0, 1U, "100", "200", 50 * 1024, 0, 100, 100, 0, false,
|
||||
Temperature::kUnknown, kUnknownOldestAncesterTime, recent_time);
|
||||
Temperature::kUnknown, kUnknownOldestAncestorTime, recent_time);
|
||||
Add(0, 2U, "200", "300", 50 * 1024, 0, 100, 100, 0, false,
|
||||
Temperature::kUnknown, kUnknownOldestAncesterTime, recent_time);
|
||||
Temperature::kUnknown, kUnknownOldestAncestorTime, recent_time);
|
||||
Add(0, 3U, "300", "400", 50 * 1024, 0, 100, 100, 0, false,
|
||||
Temperature::kUnknown, kUnknownOldestAncesterTime, 1);
|
||||
Temperature::kUnknown, kUnknownOldestAncestorTime, 1);
|
||||
Add(0, 4U, "400", "500", 50 * 1024, 0, 100, 100, 0, false,
|
||||
Temperature::kUnknown, kUnknownOldestAncesterTime, 1);
|
||||
Temperature::kUnknown, kUnknownOldestAncestorTime, 1);
|
||||
// L2 legacy migration file
|
||||
Add(2, 10U, "100", "600", 200 * 1024);
|
||||
// Blob files (associated with ALL levels)
|
||||
@@ -5690,8 +5825,8 @@ TEST_F(CompactionPickerTest, FIFOBlobAwareSizeDropping) {
|
||||
// Sub-case 1: Single-level. SST = 200KB, blob = 500MB, limit = 200MB.
|
||||
// effective_size ~ 500MB >> 200MB -> drops from L0.
|
||||
{
|
||||
SetupFIFORatioBased(/*max_table=*/200ULL * 1024 * 1024,
|
||||
/*max_data=*/200ULL * 1024 * 1024,
|
||||
SetupFIFORatioBased(/*max_table_files_size=*/200ULL * 1024 * 1024,
|
||||
/*max_data_files_size=*/200ULL * 1024 * 1024,
|
||||
/*trigger=*/4,
|
||||
/*allow_compaction=*/true,
|
||||
/*use_kv_ratio=*/false);
|
||||
@@ -5745,8 +5880,8 @@ TEST_F(CompactionPickerTest, FIFOBlobAwareSizeDropping) {
|
||||
// Sub-case 3: Under limit. SST = 256KB, blob = 200MB, limit = 1GB.
|
||||
// effective_size ~ 200MB < 1GB -> no dropping.
|
||||
{
|
||||
SetupFIFORatioBased(/*max_table=*/1ULL * 1024 * 1024 * 1024,
|
||||
/*max_data=*/1ULL * 1024 * 1024 * 1024,
|
||||
SetupFIFORatioBased(/*max_table_files_size=*/1ULL * 1024 * 1024 * 1024,
|
||||
/*max_data_files_size=*/1ULL * 1024 * 1024 * 1024,
|
||||
/*trigger=*/4,
|
||||
/*allow_compaction=*/true,
|
||||
/*use_kv_ratio=*/true);
|
||||
@@ -5899,7 +6034,9 @@ class FIFORatioBasedCompactionPickingTest : public CompactionPickerTest {
|
||||
/*existing_snapshots=*/{}, /*snapshot_checker=*/nullptr,
|
||||
vstorage_.get(), &log_buffer_, /*full_history_ts_low=*/""));
|
||||
|
||||
if (!compaction) return {};
|
||||
if (!compaction) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (out_reason) {
|
||||
*out_reason = compaction->compaction_reason();
|
||||
@@ -5945,7 +6082,9 @@ class FIFORatioBasedCompactionPickingTest : public CompactionPickerTest {
|
||||
|
||||
size_t insert_pos = oldest_input_pos;
|
||||
for (size_t idx : sorted_indices) {
|
||||
if (idx < oldest_input_pos) insert_pos--;
|
||||
if (idx < oldest_input_pos) {
|
||||
insert_pos--;
|
||||
}
|
||||
}
|
||||
insert_pos = std::min(insert_pos, files.size());
|
||||
files.insert(files.begin() + insert_pos,
|
||||
@@ -5969,10 +6108,13 @@ class FIFORatioBasedCompactionPickingTest : public CompactionPickerTest {
|
||||
sizes.push_back(f.size);
|
||||
}
|
||||
}
|
||||
if (sizes.empty()) return {0, 0, 0, 0.0, 0.0};
|
||||
if (sizes.empty()) {
|
||||
return {0, 0, 0, 0.0, 0.0};
|
||||
}
|
||||
|
||||
uint64_t sum = 0;
|
||||
uint64_t min_s = UINT64_MAX, max_s = 0;
|
||||
uint64_t min_s = UINT64_MAX;
|
||||
uint64_t max_s = 0;
|
||||
for (uint64_t s : sizes) {
|
||||
sum += s;
|
||||
min_s = std::min(min_s, s);
|
||||
@@ -6109,7 +6251,8 @@ class FIFORatioBasedCompactionPickingTest : public CompactionPickerTest {
|
||||
if (!inputs.empty() && reason == CompactionReason::kFIFOReduceNumFiles) {
|
||||
// Compute target from the picker's perspective: we need to estimate
|
||||
// it the same way the picker does.
|
||||
uint64_t total_sst = 0, total_blob = 0;
|
||||
uint64_t total_sst = 0;
|
||||
uint64_t total_blob = 0;
|
||||
for (const auto& f : files) {
|
||||
total_sst += f.size;
|
||||
total_blob += f.blob_size;
|
||||
|
||||
@@ -573,7 +573,8 @@ bool UniversalCompactionBuilder::IsInputFilesNonOverlapping(Compaction* c) {
|
||||
auto comparator = icmp_->user_comparator();
|
||||
int first_iter = 1;
|
||||
|
||||
InputFileInfo prev, curr;
|
||||
InputFileInfo prev;
|
||||
InputFileInfo curr;
|
||||
|
||||
SmallestKeyHeap smallest_key_priority_q =
|
||||
create_level_heap(c, icmp_->user_comparator());
|
||||
@@ -1421,7 +1422,8 @@ Compaction* UniversalCompactionBuilder::PickIncrementalForReduceSizeAmp(
|
||||
// Since we need to go from lower level up and this is in the reverse
|
||||
// order, compared to level order, we first write to an reversed
|
||||
// data structure and finally copy them to compaction inputs.
|
||||
InternalKey smallest, largest;
|
||||
InternalKey smallest;
|
||||
InternalKey largest;
|
||||
picker_->GetRange(second_last_level_inputs, &smallest, &largest);
|
||||
std::vector<CompactionInputFiles> inputs_reverse;
|
||||
for (auto it = ++(++sorted_runs_.rbegin()); it != sorted_runs_.rend(); it++) {
|
||||
|
||||
@@ -50,6 +50,8 @@ CompactionJob::ProcessKeyValueCompactionWithCompactionService(
|
||||
compaction_input.end =
|
||||
compaction_input.has_end ? sub_compact->end->ToString() : "";
|
||||
compaction_input.options_file_number = options_file_number_;
|
||||
compaction_input.min_manifest_file_number = min_manifest_file_number_;
|
||||
compaction_input.min_manifest_file_size = min_manifest_file_size_;
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"CompactionServiceJob::ProcessKeyValueCompactionWithCompactionService",
|
||||
@@ -82,7 +84,6 @@ CompactionJob::ProcessKeyValueCompactionWithCompactionService(
|
||||
compaction->is_full_compaction(), compaction->is_manual_compaction(),
|
||||
compaction->bottommost_level(), compaction->start_level(),
|
||||
compaction->output_level());
|
||||
|
||||
CompactionServiceScheduleResponse response =
|
||||
db_options_.compaction_service->Schedule(info, compaction_input_binary);
|
||||
switch (response.status) {
|
||||
@@ -270,7 +271,7 @@ CompactionJob::ProcessKeyValueCompactionWithCompactionService(
|
||||
file.smallest_seqno, file.largest_seqno);
|
||||
meta.smallest.DecodeFrom(file.smallest_internal_key);
|
||||
meta.largest.DecodeFrom(file.largest_internal_key);
|
||||
meta.oldest_ancester_time = file.oldest_ancester_time;
|
||||
meta.oldest_ancestor_time = file.oldest_ancestor_time;
|
||||
meta.file_creation_time = file.file_creation_time;
|
||||
meta.epoch_number = file.epoch_number;
|
||||
meta.file_checksum = file.file_checksum;
|
||||
@@ -468,7 +469,7 @@ Status CompactionServiceCompactionJob::Run() {
|
||||
MakeTableFileName(meta.fd.GetNumber()), meta.fd.GetFileSize(),
|
||||
meta.fd.smallest_seqno, meta.fd.largest_seqno,
|
||||
meta.smallest.Encode().ToString(), meta.largest.Encode().ToString(),
|
||||
meta.oldest_ancester_time, meta.file_creation_time, meta.epoch_number,
|
||||
meta.oldest_ancestor_time, meta.file_creation_time, meta.epoch_number,
|
||||
meta.file_checksum, meta.file_checksum_func_name,
|
||||
output_file.validator.GetHash(), meta.marked_for_compaction,
|
||||
meta.unique_id, *output_file.table_properties,
|
||||
@@ -564,6 +565,14 @@ static std::unordered_map<std::string, OptionTypeInfo> cs_input_type_info = {
|
||||
{offsetof(struct CompactionServiceInput, options_file_number),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"min_manifest_file_number",
|
||||
{offsetof(struct CompactionServiceInput, min_manifest_file_number),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"min_manifest_file_size",
|
||||
{offsetof(struct CompactionServiceInput, min_manifest_file_size),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
};
|
||||
|
||||
static std::unordered_map<std::string, OptionTypeInfo>
|
||||
@@ -592,8 +601,9 @@ static std::unordered_map<std::string, OptionTypeInfo>
|
||||
{offsetof(struct CompactionServiceOutputFile, largest_internal_key),
|
||||
OptionType::kEncodedString, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
// Misspelled key kept for compatibility with other versions
|
||||
{"oldest_ancester_time",
|
||||
{offsetof(struct CompactionServiceOutputFile, oldest_ancester_time),
|
||||
{offsetof(struct CompactionServiceOutputFile, oldest_ancestor_time),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"file_creation_time",
|
||||
@@ -911,24 +921,11 @@ static std::unordered_map<std::string, OptionTypeInfo>
|
||||
{offsetof(struct InternalStats::CompactionStats, count),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
// The remote worker may serialize a different number of
|
||||
// CompactionReason counts than this primary expects (RocksDB versions
|
||||
// can disagree on kNumOfReasons). Deserialization tolerates the
|
||||
// mismatch: extra trailing elements are dropped, missing ones are
|
||||
// zero-filled.
|
||||
//
|
||||
// The serialized width is intentionally kept at kNumOfReasons - 1 (the
|
||||
// reduced width) so this change does NOT alter what is written -- only
|
||||
// the read path becomes tolerant. Widening the serialized width to the
|
||||
// full kNumOfReasons is a separate, later change that must not land
|
||||
// until this tolerant read has been deployed to every primary,
|
||||
// including rollback targets.
|
||||
{"counts",
|
||||
SizeTolerantDeserializeArray<
|
||||
int, static_cast<int>(CompactionReason::kNumOfReasons) - 1>(
|
||||
offsetof(struct InternalStats::CompactionStats, counts),
|
||||
OptionVerificationType::kNormal, OptionTypeFlags::kNone,
|
||||
{0, OptionType::kInt})},
|
||||
{"counts", SizeTolerantDeserializeArray<
|
||||
int, static_cast<int>(CompactionReason::kNumOfReasons)>(
|
||||
offsetof(struct InternalStats::CompactionStats, counts),
|
||||
OptionVerificationType::kNormal, OptionTypeFlags::kNone,
|
||||
{0, OptionType::kInt})},
|
||||
};
|
||||
|
||||
static std::unordered_map<std::string, OptionTypeInfo>
|
||||
@@ -1075,9 +1072,19 @@ Status CompactionServiceInput::Read(const std::string& data_str,
|
||||
ConfigOptions cf;
|
||||
cf.invoke_prepare_options = false;
|
||||
cf.ignore_unknown_options = true;
|
||||
return OptionTypeInfo::ParseType(
|
||||
Status s = OptionTypeInfo::ParseType(
|
||||
cf, data_str.substr(sizeof(BinaryFormatVersion)), cs_input_type_info,
|
||||
obj);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
for (size_t i = 1; i < obj->snapshots.size(); ++i) {
|
||||
if (obj->snapshots[i - 1] >= obj->snapshots[i]) {
|
||||
return Status::InvalidArgument(
|
||||
"CompactionServiceInput snapshots must be strictly increasing");
|
||||
}
|
||||
}
|
||||
return Status::OK();
|
||||
} else {
|
||||
return Status::NotSupported(
|
||||
"Compaction Service Input data version not supported: " +
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "db/db_test_util.h"
|
||||
#include "file/file_util.h"
|
||||
#include "file/filename.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/advanced_compression.h"
|
||||
#include "rocksdb/filter_policy.h"
|
||||
@@ -81,6 +84,82 @@ size_t CountFirstCountsFieldElements(const std::string& s) {
|
||||
return count;
|
||||
}
|
||||
|
||||
// Simulates a filesystem without POSIX read-after-unlink semantics (a remote
|
||||
// or object store): once the primary renames a freshly written CURRENT over
|
||||
// the old one, a reader that already opened the previous CURRENT object can no
|
||||
// longer read it and the filesystem reports the path as gone rather than
|
||||
// serving the unlinked object. Fails the first `fail_times` reads of a CURRENT
|
||||
// file that way; later reads succeed, as they do once the reader picks up the
|
||||
// replacement.
|
||||
class VanishingCurrentFileFS : public FileSystemWrapper {
|
||||
public:
|
||||
explicit VanishingCurrentFileFS(const std::shared_ptr<FileSystem>& base)
|
||||
: FileSystemWrapper(base) {}
|
||||
|
||||
static const char* kClassName() { return "VanishingCurrentFileFS"; }
|
||||
const char* Name() const override { return kClassName(); }
|
||||
|
||||
IOStatus NewSequentialFile(const std::string& fname, const FileOptions& opts,
|
||||
std::unique_ptr<FSSequentialFile>* result,
|
||||
IODebugContext* dbg) override {
|
||||
IOStatus io_s = target()->NewSequentialFile(fname, opts, result, dbg);
|
||||
if (io_s.ok() && IsCurrentFileName(fname)) {
|
||||
*result = std::make_unique<MaybeVanishedFile>(std::move(*result), this);
|
||||
}
|
||||
return io_s;
|
||||
}
|
||||
|
||||
void SetFailuresToInject(int failures) {
|
||||
failures_to_inject_.store(failures);
|
||||
}
|
||||
|
||||
int injected_failures() const { return injected_failures_.load(); }
|
||||
|
||||
private:
|
||||
static bool IsCurrentFileName(const std::string& fname) {
|
||||
const std::string suffix = "/" + kCurrentFileName;
|
||||
return fname.size() > suffix.size() &&
|
||||
fname.compare(fname.size() - suffix.size(), suffix.size(), suffix) ==
|
||||
0;
|
||||
}
|
||||
|
||||
// Returns true (consuming one injection) while failures remain.
|
||||
bool ConsumeFailure() {
|
||||
int remaining = failures_to_inject_.load();
|
||||
while (remaining > 0) {
|
||||
if (failures_to_inject_.compare_exchange_weak(remaining, remaining - 1)) {
|
||||
injected_failures_.fetch_add(1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
class MaybeVanishedFile : public FSSequentialFileOwnerWrapper {
|
||||
public:
|
||||
MaybeVanishedFile(std::unique_ptr<FSSequentialFile>&& target,
|
||||
VanishingCurrentFileFS* fs)
|
||||
: FSSequentialFileOwnerWrapper(std::move(target)), fs_(fs) {}
|
||||
|
||||
IOStatus Read(size_t n, const IOOptions& options, Slice* result,
|
||||
char* scratch, IODebugContext* dbg) override {
|
||||
if (fs_->ConsumeFailure()) {
|
||||
return IOStatus::PathNotFound(
|
||||
"Failed to read CURRENT: the object was replaced and is no longer "
|
||||
"readable");
|
||||
}
|
||||
return FSSequentialFileOwnerWrapper::Read(n, options, result, scratch,
|
||||
dbg);
|
||||
}
|
||||
|
||||
private:
|
||||
VanishingCurrentFileFS* fs_;
|
||||
};
|
||||
|
||||
std::atomic<int> failures_to_inject_{0};
|
||||
std::atomic<int> injected_failures_{0};
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
class MyTestCompactionService : public CompactionService {
|
||||
@@ -113,6 +192,7 @@ class MyTestCompactionService : public CompactionService {
|
||||
start_info_ = info;
|
||||
assert(info.db_name == db_path_);
|
||||
std::string unique_id = Env::Default()->GenerateUniqueId();
|
||||
last_output_directory_name_ = unique_id;
|
||||
jobs_.emplace(unique_id, compaction_service_input);
|
||||
infos_.emplace(unique_id, info);
|
||||
CompactionServiceScheduleResponse response(
|
||||
@@ -149,10 +229,10 @@ class MyTestCompactionService : public CompactionService {
|
||||
|
||||
OpenAndCompactOptions options;
|
||||
options.canceled = &canceled_;
|
||||
options.max_secondary_open_retries = max_secondary_open_retries_;
|
||||
|
||||
Status s =
|
||||
DB::OpenAndCompact(options, db_path_, GetOutputPath(scheduled_job_id),
|
||||
compaction_input, result, options_override);
|
||||
Status s = DB::OpenAndCompact(options, db_path_, scheduled_job_id,
|
||||
compaction_input, result, options_override);
|
||||
{
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
if (is_override_wait_result_) {
|
||||
@@ -163,6 +243,14 @@ class MyTestCompactionService : public CompactionService {
|
||||
compaction_num_.fetch_add(1);
|
||||
if (s.ok()) {
|
||||
return CompactionServiceJobStatus::kSuccess;
|
||||
} else if (s.IsIncomplete() && !s.IsManualCompactionPaused()) {
|
||||
// The remote worker declined the job (e.g. the MANIFEST floor check
|
||||
// rejected an older/stale filesystem view). Mapping that decline to
|
||||
// kUseLocal (retry locally) vs. kFailure (surface it) is the integrator's
|
||||
// choice; this example picks kUseLocal to exercise the fallback path,
|
||||
// while DbStressCompactionService deliberately picks kFailure. Real
|
||||
// cancellation (kManualCompactionPaused) is handled as a failure.
|
||||
return CompactionServiceJobStatus::kUseLocal;
|
||||
} else {
|
||||
return CompactionServiceJobStatus::kFailure;
|
||||
}
|
||||
@@ -204,6 +292,11 @@ class MyTestCompactionService : public CompactionService {
|
||||
|
||||
int GetCompactionNum() { return compaction_num_.load(); }
|
||||
|
||||
std::string GetLastOutputDirectoryName() {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
return last_output_directory_name_;
|
||||
}
|
||||
|
||||
CompactionServiceJobInfo GetCompactionInfoForStart() { return start_info_; }
|
||||
CompactionServiceJobInfo GetCompactionInfoForWait() { return wait_info_; }
|
||||
|
||||
@@ -231,6 +324,10 @@ class MyTestCompactionService : public CompactionService {
|
||||
void SetCanceled(bool canceled) { canceled_ = canceled; }
|
||||
bool GetCanceled() { return canceled_; }
|
||||
|
||||
void SetMaxSecondaryOpenRetries(uint32_t retries) {
|
||||
max_secondary_open_retries_ = retries;
|
||||
}
|
||||
|
||||
void GetResult(CompactionServiceResult* deserialized) {
|
||||
CompactionServiceResult::Read(result_, deserialized).PermitUncheckedError();
|
||||
}
|
||||
@@ -248,9 +345,12 @@ class MyTestCompactionService : public CompactionService {
|
||||
std::map<std::string, std::string> jobs_;
|
||||
std::map<std::string, CompactionServiceJobInfo> infos_;
|
||||
std::string result_;
|
||||
std::string last_output_directory_name_;
|
||||
|
||||
std::string GetOutputPath(const std::string& scheduled_job_id) {
|
||||
return db_path_ + "/" + scheduled_job_id;
|
||||
return RemoteCompactionJobDir(
|
||||
db_path_, options_.use_session_tmp_dir_for_remote_compaction,
|
||||
scheduled_job_id);
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -272,6 +372,8 @@ class MyTestCompactionService : public CompactionService {
|
||||
std::atomic_int installation_callback_count_{0};
|
||||
std::atomic<CompactionServiceJobStatus> final_updated_status_{
|
||||
CompactionServiceJobStatus::kUseLocal};
|
||||
uint32_t max_secondary_open_retries_ =
|
||||
OpenAndCompactOptions().max_secondary_open_retries;
|
||||
|
||||
protected:
|
||||
std::atomic_bool canceled_{false};
|
||||
@@ -399,8 +501,12 @@ TEST_F(CompactionServiceTest, BasicCompactions) {
|
||||
ASSERT_EQ(primary_statistics->getTickerCount(REMOTE_COMPACT_WRITE_BYTES),
|
||||
compactor_statistics->getTickerCount(COMPACT_WRITE_BYTES));
|
||||
ASSERT_GE(primary_statistics->getTickerCount(REMOTE_COMPACT_READ_BYTES), 1);
|
||||
ASSERT_GT(primary_statistics->getTickerCount(COMPACT_READ_BYTES),
|
||||
primary_statistics->getTickerCount(REMOTE_COMPACT_READ_BYTES));
|
||||
// The remote worker reads the full compaction inputs on demand, while the
|
||||
// primary only re-reads the (smaller) output files for verify_table(), so
|
||||
// remote read bytes dominate. (The worker no longer eagerly opens every file
|
||||
// during secondary recovery, so its input reads are attributed accurately.)
|
||||
ASSERT_GT(primary_statistics->getTickerCount(REMOTE_COMPACT_READ_BYTES),
|
||||
primary_statistics->getTickerCount(COMPACT_READ_BYTES));
|
||||
// compactor is already the remote side, which doesn't have remote
|
||||
ASSERT_EQ(compactor_statistics->getTickerCount(REMOTE_COMPACT_READ_BYTES), 0);
|
||||
ASSERT_EQ(compactor_statistics->getTickerCount(REMOTE_COMPACT_WRITE_BYTES),
|
||||
@@ -438,6 +544,27 @@ TEST_F(CompactionServiceTest, BasicCompactions) {
|
||||
}
|
||||
ASSERT_TRUE(s.IsAborted());
|
||||
|
||||
CompactionServiceResult result;
|
||||
my_cs->GetResult(&result);
|
||||
ASSERT_NOK(result.status);
|
||||
|
||||
// The aborted job's output files were never installed, so they are still in
|
||||
// the job's staging directory. Size them before reopening: the reopen starts
|
||||
// a new DB session, and DB::Open() deletes staging directories belonging to
|
||||
// any other session.
|
||||
uint64_t total_size = 0;
|
||||
Env* const env = options.env;
|
||||
ASSERT_TRUE(options.env != nullptr);
|
||||
for (const auto& output_file : result.output_files) {
|
||||
std::string file_name = result.output_path + "/" + output_file.file_name;
|
||||
|
||||
uint64_t file_size = 0;
|
||||
ASSERT_OK(env->GetFileSize(file_name, &file_size));
|
||||
ASSERT_GT(file_size, 0);
|
||||
total_size += file_size;
|
||||
}
|
||||
ASSERT_EQ(total_size, result.internal_stats.TotalBytesWritten());
|
||||
|
||||
// Test re-open and successful unique id verification
|
||||
std::atomic_int verify_passed{0};
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
@@ -452,13 +579,6 @@ TEST_F(CompactionServiceTest, BasicCompactions) {
|
||||
ReopenWithColumnFamilies({kDefaultColumnFamilyName, "cf_1", "cf_2", "cf_3"},
|
||||
options);
|
||||
ASSERT_GT(verify_passed, 0);
|
||||
CompactionServiceResult result;
|
||||
my_cs->GetResult(&result);
|
||||
if (s.IsAborted()) {
|
||||
ASSERT_NOK(result.status);
|
||||
} else {
|
||||
ASSERT_OK(result.status);
|
||||
}
|
||||
ASSERT_GE(result.internal_stats.output_level_stats.micros, 1);
|
||||
ASSERT_GE(result.internal_stats.output_level_stats.cpu_micros, 1);
|
||||
|
||||
@@ -466,17 +586,6 @@ TEST_F(CompactionServiceTest, BasicCompactions) {
|
||||
ASSERT_EQ(result.output_files.size(),
|
||||
result.internal_stats.output_level_stats.num_output_files);
|
||||
|
||||
uint64_t total_size = 0;
|
||||
for (auto output_file : result.output_files) {
|
||||
std::string file_name = result.output_path + "/" + output_file.file_name;
|
||||
|
||||
uint64_t file_size = 0;
|
||||
ASSERT_OK(options.env->GetFileSize(file_name, &file_size));
|
||||
ASSERT_GT(file_size, 0);
|
||||
total_size += file_size;
|
||||
}
|
||||
ASSERT_EQ(total_size, result.internal_stats.TotalBytesWritten());
|
||||
|
||||
ASSERT_TRUE(result.stats.is_remote_compaction);
|
||||
ASSERT_TRUE(result.stats.is_manual_compaction);
|
||||
ASSERT_FALSE(result.stats.is_full_compaction);
|
||||
@@ -1791,7 +1900,7 @@ TEST_F(CompactionServiceTest, InvalidResultFallsBackToLocal) {
|
||||
TEST_F(CompactionServiceTest, CompatCheckCountsWidthTolerance) {
|
||||
// Number of per-CompactionReason counts this binary serializes and expects.
|
||||
constexpr size_t kExpected =
|
||||
static_cast<size_t>(CompactionReason::kNumOfReasons) - 1;
|
||||
static_cast<size_t>(CompactionReason::kNumOfReasons);
|
||||
constexpr size_t kLastIndex = kExpected - 1;
|
||||
constexpr int kFirstCount = 3;
|
||||
constexpr int kLastCount = 5;
|
||||
@@ -2169,6 +2278,498 @@ TEST_F(CompactionServiceTest, ConcurrentCompaction) {
|
||||
ASSERT_EQ(FilesPerLevel(), "0,0,10");
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, RemoteCompactionMissingLowerFileTrustsManifest) {
|
||||
// A remote compaction reconstructs its view of the LSM (which files sit below
|
||||
// the output level) from the secondary's recovered MANIFEST version. That
|
||||
// view drives correctness-critical decisions like whether a deletion
|
||||
// tombstone can be dropped (Compaction::KeyNotExistsBeyondOutputLevel /
|
||||
// IsBottommostLevel, which read input_version_'s VersionStorageInfo).
|
||||
//
|
||||
// If a non-input file that is still referenced by the MANIFEST is momentarily
|
||||
// missing (as on a live primary), the OLD point-in-time recovery rolls back
|
||||
// to an earlier version. Here that rollback drops the (later, whole-DB)
|
||||
// manifest record entirely, so the remote worker can no longer find the
|
||||
// compaction input and the job fails ("cannot find matched SST files") --
|
||||
// i.e. it fails safe rather than mis-compacting. The fix makes recovery TRUST
|
||||
// the manifest: it keeps the current version, the job completes, and the
|
||||
// deleted key correctly stays deleted (the below-output tombstone is
|
||||
// retained).
|
||||
Options options = CurrentOptions();
|
||||
options.num_levels = 7;
|
||||
// -1 keeps the primary's descriptor for the (unlinked) lower file valid, so
|
||||
// the final read below is served from the primary regardless of the deletion.
|
||||
options.max_open_files = -1;
|
||||
options.level0_file_num_compaction_trigger = 100; // no auto compaction
|
||||
ReopenWithCompactionService(&options);
|
||||
|
||||
// Old value for "k", pushed down to L2 (below the compaction output level).
|
||||
ASSERT_OK(Put("k", "OLD"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(2); // L2 file contains k=OLD
|
||||
|
||||
// Newer tombstone for "k" at L0 (the compaction input).
|
||||
ASSERT_OK(Delete("k"));
|
||||
ASSERT_OK(Flush()); // L0 file contains Delete(k)
|
||||
|
||||
// Reopen so the MANIFEST is rewritten as current state before we perturb it.
|
||||
ReopenWithColumnFamilies({kDefaultColumnFamilyName, "cf_1", "cf_2", "cf_3"},
|
||||
options);
|
||||
// Closing on reopen cancels pending remote jobs; re-arm the service.
|
||||
GetCompactionService()->SetCanceled(false);
|
||||
|
||||
ColumnFamilyMetaData meta;
|
||||
db_->GetColumnFamilyMetaData(&meta); // default CF
|
||||
ASSERT_EQ(meta.levels[0].files.size(), 1);
|
||||
ASSERT_EQ(meta.levels[2].files.size(), 1);
|
||||
const auto& f_del = meta.levels[0].files[0];
|
||||
const auto& f_old = meta.levels[2].files[0];
|
||||
|
||||
// Make the lower (L2) file momentarily unavailable to a fresh open, as a live
|
||||
// primary would if it had compacted it away. It is NOT a compaction input.
|
||||
ASSERT_OK(env_->DeleteFile(f_old.db_path + "/" + f_old.name));
|
||||
|
||||
// Remote-compact the L0 tombstone into L1. L2 (with k=OLD) is below the
|
||||
// output level, so a correct compaction must retain the tombstone. Without
|
||||
// the fix, recovery rolls back and this job fails; with the fix it completes.
|
||||
std::string fname = f_del.db_path + "/" + f_del.name;
|
||||
ASSERT_OK(db_->CompactFiles(CompactionOptions(), {fname}, 1));
|
||||
|
||||
// Deleted key stays deleted (tombstone retained above the still-present L2
|
||||
// value, which the primary can still read via its open descriptor).
|
||||
ASSERT_EQ("NOT_FOUND", Get("k"));
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, RemoteCompactionManifestFloorSendsPosition) {
|
||||
// By default (remote_compaction_manifest_floor=true), the primary attaches a
|
||||
// non-zero MANIFEST floor to every remote compaction request, and the worker
|
||||
// runs its floor check. Here the worker recovers the same-or-later MANIFEST
|
||||
// position the primary scheduled from, so the check accepts (the
|
||||
// later-manifest positive case).
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
ReopenWithCompactionService(&options);
|
||||
ASSERT_TRUE(db_->GetDBOptions().remote_compaction_manifest_floor);
|
||||
GenerateTestData();
|
||||
|
||||
uint64_t floor_number = 0;
|
||||
uint64_t floor_size = 0;
|
||||
int floor_check_count = 0;
|
||||
bool floor_check_rejected = false;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionServiceJob::ProcessKeyValueCompactionWithCompactionService",
|
||||
[&](void* arg) {
|
||||
auto* input = static_cast<CompactionServiceInput*>(arg);
|
||||
floor_number = input->min_manifest_file_number;
|
||||
floor_size = input->min_manifest_file_size;
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::OpenAndCompact::ManifestFloorCheck", [&](void* arg) {
|
||||
floor_check_count++;
|
||||
if (*static_cast<bool*>(arg)) {
|
||||
floor_check_rejected = true;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
std::string start_str = Key(15);
|
||||
std::string end_str = Key(45);
|
||||
Slice start(start_str);
|
||||
Slice end(end_str);
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), &start, &end));
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
// A floor was populated and the worker accepted its (later-or-equal) view.
|
||||
ASSERT_GT(floor_number, 0U);
|
||||
ASSERT_GT(floor_size, 0U);
|
||||
ASSERT_GE(floor_check_count, 1);
|
||||
ASSERT_FALSE(floor_check_rejected);
|
||||
VerifyTestData();
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, RemoteCompactionManifestFloorRejectsStaleView) {
|
||||
// If the worker recovers an OLDER MANIFEST view than the primary scheduled
|
||||
// from (simulated here by inflating the floor above the real position), the
|
||||
// floor check rejects the job and the primary falls back to a correct local
|
||||
// compaction rather than compacting against a stale/older LSM shape.
|
||||
Options options = CurrentOptions();
|
||||
options.num_levels = 7;
|
||||
options.level0_file_num_compaction_trigger = 100; // no auto compaction
|
||||
ReopenWithCompactionService(&options);
|
||||
|
||||
// Old value for "k", pushed down to L2 (below the compaction output level).
|
||||
ASSERT_OK(Put("k", "OLD"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(2); // L2: k=OLD
|
||||
|
||||
// Newer tombstone for "k" at L0 (the compaction input).
|
||||
ASSERT_OK(Delete("k"));
|
||||
ASSERT_OK(Flush()); // L0: Delete(k)
|
||||
|
||||
int floor_check_rejected = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionServiceJob::ProcessKeyValueCompactionWithCompactionService",
|
||||
[&](void* arg) {
|
||||
auto* input = static_cast<CompactionServiceInput*>(arg);
|
||||
// Simulate the worker seeing an older MANIFEST than the primary
|
||||
// scheduled from by raising the floor above the real position.
|
||||
input->min_manifest_file_number += 1000000;
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::OpenAndCompact::ManifestFloorCheck", [&](void* arg) {
|
||||
if (*static_cast<bool*>(arg)) {
|
||||
floor_check_rejected++;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ColumnFamilyMetaData meta;
|
||||
db_->GetColumnFamilyMetaData(&meta); // default CF
|
||||
ASSERT_EQ(meta.levels[0].files.size(), 1);
|
||||
const auto& f_del = meta.levels[0].files[0];
|
||||
std::string fname = f_del.db_path + "/" + f_del.name;
|
||||
|
||||
// The worker rejects the stale view -> local fallback -> the compaction still
|
||||
// completes correctly (CompactFiles returns OK).
|
||||
ASSERT_OK(db_->CompactFiles(CompactionOptions(), {fname}, 1));
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ASSERT_GE(floor_check_rejected, 1);
|
||||
// Deleted key stays deleted: the local fallback retained the below-output
|
||||
// tombstone. A silent mis-compaction against the older view would resurrect
|
||||
// "OLD".
|
||||
ASSERT_EQ("NOT_FOUND", Get("k"));
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, RemoteCompactionManifestFloorKillSwitchOff) {
|
||||
// With the kill switch off, the primary sends no floor, so the worker uses
|
||||
// the original point-in-time recovery (no trust-manifest, no floor check). A
|
||||
// transiently-missing non-input file then rolls recovery back and fails the
|
||||
// job -- confirming the escape hatch reverts to the pre-fix behavior.
|
||||
Options options = CurrentOptions();
|
||||
options.num_levels = 7;
|
||||
options.max_open_files = -1;
|
||||
options.open_files_async = true;
|
||||
options.skip_stats_update_on_db_open = true;
|
||||
options.level0_file_num_compaction_trigger = 100; // no auto compaction
|
||||
options.remote_compaction_manifest_floor = false; // kill switch OFF
|
||||
ReopenWithCompactionService(&options);
|
||||
|
||||
ASSERT_OK(Put("k", "OLD"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(2); // L2: k=OLD
|
||||
|
||||
ASSERT_OK(Delete("k"));
|
||||
ASSERT_OK(Flush()); // L0: Delete(k)
|
||||
|
||||
// Reopen so the MANIFEST is rewritten as current state before we perturb it.
|
||||
SyncPoint::GetInstance()->LoadDependency(
|
||||
{{"DBImpl::BGWorkAsyncFileOpen:Done",
|
||||
"CompactionServiceTest::ManifestFloorKillSwitchOff:OpenDone"}});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
ReopenWithColumnFamilies({kDefaultColumnFamilyName, "cf_1", "cf_2", "cf_3"},
|
||||
options);
|
||||
TEST_SYNC_POINT("CompactionServiceTest::ManifestFloorKillSwitchOff:OpenDone");
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
GetCompactionService()->SetCanceled(false);
|
||||
|
||||
bool floor_provided_seen = true;
|
||||
int floor_check_count = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionServiceJob::ProcessKeyValueCompactionWithCompactionService",
|
||||
[&](void* arg) {
|
||||
auto* input = static_cast<CompactionServiceInput*>(arg);
|
||||
floor_provided_seen = (input->min_manifest_file_number != 0);
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::OpenAndCompact::ManifestFloorCheck",
|
||||
[&](void* /*arg*/) { floor_check_count++; });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ColumnFamilyMetaData meta;
|
||||
db_->GetColumnFamilyMetaData(&meta);
|
||||
ASSERT_EQ(meta.levels[0].files.size(), 1);
|
||||
ASSERT_EQ(meta.levels[2].files.size(), 1);
|
||||
const auto& f_del = meta.levels[0].files[0];
|
||||
const auto& f_old = meta.levels[2].files[0];
|
||||
|
||||
// Make the lower (L2) non-input file momentarily unavailable, as a live
|
||||
// primary would if it had compacted it away.
|
||||
ASSERT_OK(env_->DeleteFile(f_old.db_path + "/" + f_old.name));
|
||||
|
||||
std::string fname = f_del.db_path + "/" + f_del.name;
|
||||
Status s = db_->CompactFiles(CompactionOptions(), {fname}, 1);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
// No floor was sent and the worker skipped the floor check; the original
|
||||
// recovery rolled back and failed the job (reverted behavior).
|
||||
ASSERT_FALSE(floor_provided_seen);
|
||||
ASSERT_EQ(0, floor_check_count);
|
||||
ASSERT_NOK(s);
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, RemoteCompactionManifestFloorMutable) {
|
||||
// remote_compaction_manifest_floor is a mutable DB option: flipping it via
|
||||
// SetDBOptions changes whether the primary attaches a floor to subsequent
|
||||
// remote compaction requests (and thus whether the worker runs the floor
|
||||
// check).
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
ReopenWithCompactionService(&options);
|
||||
GenerateTestData();
|
||||
|
||||
bool floor_provided = false;
|
||||
int floor_check_count = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionServiceJob::ProcessKeyValueCompactionWithCompactionService",
|
||||
[&](void* arg) {
|
||||
auto* input = static_cast<CompactionServiceInput*>(arg);
|
||||
floor_provided = (input->min_manifest_file_number != 0);
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::OpenAndCompact::ManifestFloorCheck",
|
||||
[&](void* /*arg*/) { floor_check_count++; });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
std::string start_str = Key(15);
|
||||
std::string end_str = Key(45);
|
||||
Slice start(start_str);
|
||||
Slice end(end_str);
|
||||
|
||||
// Default on: floor attached, floor check runs.
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), &start, &end));
|
||||
ASSERT_TRUE(floor_provided);
|
||||
ASSERT_GE(floor_check_count, 1);
|
||||
|
||||
// Turn the kill switch off dynamically.
|
||||
ASSERT_OK(db_->SetDBOptions({{"remote_compaction_manifest_floor", "false"}}));
|
||||
ASSERT_FALSE(db_->GetDBOptions().remote_compaction_manifest_floor);
|
||||
|
||||
floor_provided = true;
|
||||
floor_check_count = 0;
|
||||
|
||||
// Create a fresh compaction target overlapping the range and re-compact.
|
||||
ASSERT_OK(Put(Key(20), "v"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), &start, &end));
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
// Now no floor is attached and the worker skips the floor check.
|
||||
ASSERT_FALSE(floor_provided);
|
||||
ASSERT_EQ(0, floor_check_count);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// A remote compaction service whose output directory already contains an SST
|
||||
// left behind by a previously interrupted (canceled) fresh attempt, then runs
|
||||
// OpenAndCompact with allow_resumption=true. Used to verify that when
|
||||
// resumption is internally disabled, the output directory is cleaned before the
|
||||
// fresh compaction starts.
|
||||
class LeftoverOutputCompactionService : public MyTestCompactionService {
|
||||
public:
|
||||
using MyTestCompactionService::MyTestCompactionService;
|
||||
|
||||
CompactionServiceJobStatus Wait(const std::string& scheduled_job_id,
|
||||
std::string* result) override {
|
||||
std::string compaction_input;
|
||||
{
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
auto it = jobs_.find(scheduled_job_id);
|
||||
if (it == jobs_.end()) {
|
||||
return CompactionServiceJobStatus::kFailure;
|
||||
}
|
||||
compaction_input = it->second;
|
||||
jobs_.erase(it);
|
||||
}
|
||||
|
||||
CompactionServiceOptionsOverride options_override = GetOptionsOverride();
|
||||
Env* env = options_override.env;
|
||||
const std::string output_dir = GetOutputPath(scheduled_job_id);
|
||||
EXPECT_OK(env->CreateDirIfMissing(output_dir));
|
||||
|
||||
// An output SST left behind by a previously interrupted fresh attempt. Uses
|
||||
// a high file number the fresh compaction will not reuse, so whether it is
|
||||
// cleaned up is directly observable.
|
||||
leftover_file_ = MakeTableFileName(output_dir, 999999);
|
||||
{
|
||||
std::unique_ptr<WritableFile> f;
|
||||
EXPECT_OK(env->NewWritableFile(leftover_file_, &f, EnvOptions()));
|
||||
EXPECT_OK(f->Append("leftover"));
|
||||
EXPECT_OK(f->Close());
|
||||
}
|
||||
|
||||
OpenAndCompactOptions open_options;
|
||||
open_options.allow_resumption = true;
|
||||
Status s = DB::OpenAndCompact(open_options, db_path_, scheduled_job_id,
|
||||
compaction_input, result, options_override);
|
||||
{
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
result_ = *result;
|
||||
}
|
||||
if (!s.ok()) {
|
||||
return CompactionServiceJobStatus::kFailure;
|
||||
}
|
||||
leftover_cleaned_ = env->FileExists(leftover_file_).IsNotFound();
|
||||
return CompactionServiceJobStatus::kSuccess;
|
||||
}
|
||||
|
||||
bool leftover_cleaned() const { return leftover_cleaned_; }
|
||||
|
||||
private:
|
||||
std::string leftover_file_;
|
||||
bool leftover_cleaned_ = false;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST_F(CompactionServiceTest, CleansOutputDirWhenResumptionDisabled) {
|
||||
// Regression test: allow_resumption=true is requested, but resumption is
|
||||
// internally disabled because paranoid_file_checks=true is incompatible with
|
||||
// the resumption path's output-hash verification. OpenAndCompact then starts
|
||||
// a fresh compaction, which must clean the output directory first (honoring
|
||||
// the documented OpenAndCompactOptions::allow_resumption fallback contract),
|
||||
// so an output file left by a previously interrupted attempt does not collide
|
||||
// with the file numbers the fresh compaction reuses. Before the fix the
|
||||
// leftover file was kept, which under a no-reopen filesystem (as in the crash
|
||||
// test) fails the fresh compaction's NewWritableFile with a
|
||||
// "no-reopen-for-write contract" violation.
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
options.paranoid_file_checks = true; // disables resumption on the worker
|
||||
options.use_session_tmp_dir_for_remote_compaction = true;
|
||||
options.env = env_;
|
||||
auto statistics = CreateDBStatistics();
|
||||
|
||||
auto cs = std::make_shared<LeftoverOutputCompactionService>(
|
||||
dbname_, options, statistics,
|
||||
std::vector<std::shared_ptr<EventListener>>{},
|
||||
std::vector<std::shared_ptr<TablePropertiesCollectorFactory>>{});
|
||||
options.compaction_service = cs;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Two overlapping flushed files so the manual compaction has real work.
|
||||
ASSERT_OK(Put(Key(1), "v1"));
|
||||
ASSERT_OK(Put(Key(3), "v3"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put(Key(2), "v2"));
|
||||
ASSERT_OK(Put(Key(3), "v3new"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
|
||||
|
||||
// The fresh compaction cleaned the leftover output before starting.
|
||||
ASSERT_TRUE(cs->leftover_cleaned());
|
||||
ASSERT_EQ(Get(Key(1)), "v1");
|
||||
ASSERT_EQ(Get(Key(2)), "v2");
|
||||
ASSERT_EQ(Get(Key(3)), "v3new");
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, RetriesSecondaryOpenWhenCurrentFileVanishes) {
|
||||
// The remote compaction worker opens the live primary's DB directory
|
||||
// read-only inside OpenAndCompact. The primary replaces CURRENT on every
|
||||
// MANIFEST rotation, and on a filesystem without POSIX read-after-unlink
|
||||
// semantics the CURRENT object the worker already opened becomes unreadable.
|
||||
// That is a transient race, so the secondary open must be retried; otherwise
|
||||
// a CompactionService that does not fall back to a local compaction
|
||||
// (db_stress with remote_compaction_failure_fall_back_to_local=false) turns
|
||||
// it into a primary background error that fails every later flush.
|
||||
auto fs = std::make_shared<VanishingCurrentFileFS>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> vanishing_env(NewCompositeEnv(fs));
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
options.env = vanishing_env.get();
|
||||
auto statistics = CreateDBStatistics();
|
||||
|
||||
auto cs = std::make_shared<MyTestCompactionService>(
|
||||
dbname_, options, statistics,
|
||||
std::vector<std::shared_ptr<EventListener>>{},
|
||||
std::vector<std::shared_ptr<TablePropertiesCollectorFactory>>{});
|
||||
cs->SetMaxSecondaryOpenRetries(3);
|
||||
options.compaction_service = cs;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Two overlapping flushed files so the manual compaction has real work.
|
||||
ASSERT_OK(Put(Key(1), "v1"));
|
||||
ASSERT_OK(Put(Key(3), "v3"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put(Key(2), "v2"));
|
||||
ASSERT_OK(Put(Key(3), "v3new"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Arm the injection only now: the primary is done opening, so the failed
|
||||
// CURRENT reads below happen in the worker's secondary open. Three failures
|
||||
// exercise a non-default retry limit and require the fourth attempt.
|
||||
fs->SetFailuresToInject(3);
|
||||
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
const Status compact_s = db_->CompactRange(cro, nullptr, nullptr);
|
||||
|
||||
// Collect everything before closing: the DB must be closed while the
|
||||
// injecting Env is still alive, so no assertion may return early above it.
|
||||
const int injected_failures = fs->injected_failures();
|
||||
const int compactions = cs->GetCompactionNum();
|
||||
const std::string v1 = Get(Key(1));
|
||||
const std::string v2 = Get(Key(2));
|
||||
const std::string v3 = Get(Key(3));
|
||||
Close();
|
||||
|
||||
ASSERT_OK(compact_s);
|
||||
ASSERT_EQ(3, injected_failures);
|
||||
ASSERT_GE(compactions, 1);
|
||||
ASSERT_EQ(v1, "v1");
|
||||
ASSERT_EQ(v2, "v2");
|
||||
ASSERT_EQ(v3, "v3new");
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, CanDisableSecondaryOpenRetry) {
|
||||
auto fs = std::make_shared<VanishingCurrentFileFS>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> vanishing_env(NewCompositeEnv(fs));
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
options.env = vanishing_env.get();
|
||||
auto statistics = CreateDBStatistics();
|
||||
|
||||
auto cs = std::make_shared<MyTestCompactionService>(
|
||||
dbname_, options, statistics,
|
||||
std::vector<std::shared_ptr<EventListener>>{},
|
||||
std::vector<std::shared_ptr<TablePropertiesCollectorFactory>>{});
|
||||
cs->SetMaxSecondaryOpenRetries(0);
|
||||
options.compaction_service = cs;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put(Key(1), "v1"));
|
||||
ASSERT_OK(Put(Key(3), "v3"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put(Key(2), "v2"));
|
||||
ASSERT_OK(Put(Key(3), "v3new"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
fs->SetFailuresToInject(1);
|
||||
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
const Status compact_s = db_->CompactRange(cro, nullptr, nullptr);
|
||||
const int injected_failures = fs->injected_failures();
|
||||
const int compactions = cs->GetCompactionNum();
|
||||
Close();
|
||||
|
||||
ASSERT_TRUE(compact_s.IsIncomplete());
|
||||
ASSERT_EQ(1, injected_failures);
|
||||
ASSERT_EQ(1, compactions);
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, CompactionInfo) {
|
||||
Options options = CurrentOptions();
|
||||
ReopenWithCompactionService(&options);
|
||||
@@ -2182,7 +2783,8 @@ TEST_F(CompactionServiceTest, CompactionInfo) {
|
||||
|
||||
CompactionServiceJobInfo info = my_cs->GetCompactionInfoForStart();
|
||||
ASSERT_EQ(dbname_, info.db_name);
|
||||
std::string db_id, db_session_id;
|
||||
std::string db_id;
|
||||
std::string db_session_id;
|
||||
ASSERT_OK(db_->GetDbIdentity(db_id));
|
||||
ASSERT_EQ(db_id, info.db_id);
|
||||
ASSERT_OK(db_->GetDbSessionId(db_session_id));
|
||||
@@ -2854,7 +3456,7 @@ class ResumableCompactionService : public MyTestCompactionService {
|
||||
EXPECT_OK(statistics_->Reset());
|
||||
|
||||
Status s =
|
||||
DB::OpenAndCompact(options, db_path_, GetOutputPath(scheduled_job_id),
|
||||
DB::OpenAndCompact(options, db_path_, scheduled_job_id,
|
||||
compaction_input, &temp_result, override_options);
|
||||
|
||||
EXPECT_TRUE(s.IsManualCompactionPaused());
|
||||
@@ -2871,9 +3473,8 @@ class ResumableCompactionService : public MyTestCompactionService {
|
||||
std::string* result) {
|
||||
EXPECT_OK(statistics_->Reset());
|
||||
|
||||
Status s =
|
||||
DB::OpenAndCompact(options, db_path_, GetOutputPath(scheduled_job_id),
|
||||
compaction_input, result, override_options);
|
||||
Status s = DB::OpenAndCompact(options, db_path_, scheduled_job_id,
|
||||
compaction_input, result, override_options);
|
||||
|
||||
EXPECT_TRUE(s.ok());
|
||||
|
||||
@@ -3378,6 +3979,182 @@ TEST_F(ResumableCompactionKeyTypeTest, CancelAndResumeWithTimedPut) {
|
||||
|
||||
VerifyResumeBytes();
|
||||
}
|
||||
|
||||
// Covers the DB session temporary directory lifecycle when remote compaction
|
||||
// opts in to using it.
|
||||
class SessionTmpDirTest : public DBTestBase {
|
||||
public:
|
||||
SessionTmpDirTest()
|
||||
: DBTestBase("session_tmp_dir_test",
|
||||
/*env_do_fsync=*/true) {}
|
||||
|
||||
protected:
|
||||
// Opens with a CompactionService opted in to the session temporary directory.
|
||||
Options OptionsWithCompactionService() {
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.use_session_tmp_dir_for_remote_compaction = true;
|
||||
worker_statistics_ = CreateDBStatistics();
|
||||
compaction_service_ = std::make_shared<MyTestCompactionService>(
|
||||
dbname_, options, worker_statistics_,
|
||||
std::vector<std::shared_ptr<EventListener>>{},
|
||||
std::vector<std::shared_ptr<TablePropertiesCollectorFactory>>{});
|
||||
options.compaction_service = compaction_service_;
|
||||
return options;
|
||||
}
|
||||
|
||||
std::string SessionTmpPath() { return SessionTmpDir(dbname_); }
|
||||
|
||||
// Creates and closes a DB session before planting temporary output for the
|
||||
// next session to delete.
|
||||
void CreateDbAndQuiesce(const Options& options) {
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_TRUE(Exists(SessionTmpPath()));
|
||||
Close();
|
||||
}
|
||||
|
||||
// Creates `parent/name/` holding one file, mimicking a leaked job directory.
|
||||
void PlantDir(const std::string& parent, const std::string& name) {
|
||||
SpecialEnv* const env = env_;
|
||||
if (env == nullptr) {
|
||||
ADD_FAILURE() << "Test environment is not initialized";
|
||||
return;
|
||||
}
|
||||
ASSERT_OK(env->CreateDirIfMissing(parent));
|
||||
const std::string dir = parent + "/" + name;
|
||||
ASSERT_OK(env->CreateDirIfMissing(dir));
|
||||
ASSERT_OK(WriteStringToFile(env, "leftover", dir + "/000123.sst"));
|
||||
}
|
||||
|
||||
bool Exists(const std::string& path) {
|
||||
return env_ != nullptr && env_->FileExists(path).ok();
|
||||
}
|
||||
|
||||
// Reopens after planting stale output. Cleanup completes before DB::Open()
|
||||
// schedules compactions or returns.
|
||||
void ReopenAndDeleteStaleOutput(const Options& options) { Reopen(options); }
|
||||
|
||||
std::shared_ptr<Statistics> worker_statistics_;
|
||||
std::shared_ptr<CompactionService> compaction_service_;
|
||||
};
|
||||
|
||||
TEST_F(SessionTmpDirTest, ResolvesJobDirectory) {
|
||||
const std::string job_dir_name = "SESSION01_4294967296";
|
||||
const std::string legacy_output_path = dbname_ + "/" + job_dir_name;
|
||||
|
||||
ASSERT_EQ(RemoteCompactionJobDir(dbname_, false, job_dir_name),
|
||||
legacy_output_path);
|
||||
ASSERT_EQ(RemoteCompactionJobDir(dbname_, false, legacy_output_path),
|
||||
legacy_output_path);
|
||||
ASSERT_EQ(RemoteCompactionJobDir(dbname_, true, job_dir_name),
|
||||
SessionTmpPath() + "/" + job_dir_name);
|
||||
}
|
||||
|
||||
TEST_F(SessionTmpDirTest, DeletesDirsFromPreviousSessions) {
|
||||
Options options = OptionsWithCompactionService();
|
||||
CreateDbAndQuiesce(options);
|
||||
|
||||
// Two leaked job directories, one of them with a nested subdirectory to prove
|
||||
// the removal is recursive.
|
||||
PlantDir(SessionTmpPath(), "SESSIONFROMDEADHOST01_4294967296");
|
||||
PlantDir(SessionTmpPath() + "/SESSIONFROMDEADHOST01_4294967296", "nested");
|
||||
PlantDir(SessionTmpPath(), "ANOTHERDEADSESSION02_8589934592");
|
||||
|
||||
ReopenAndDeleteStaleOutput(options);
|
||||
|
||||
ASSERT_TRUE(Exists(SessionTmpPath()));
|
||||
ASSERT_FALSE(Exists(SessionTmpPath() + "/SESSIONFROMDEADHOST01_4294967296"));
|
||||
ASSERT_FALSE(Exists(SessionTmpPath() + "/ANOTHERDEADSESSION02_8589934592"));
|
||||
}
|
||||
|
||||
TEST_F(SessionTmpDirTest, IgnoresDirsOutsideSessionTmpDir) {
|
||||
Options options = OptionsWithCompactionService();
|
||||
CreateDbAndQuiesce(options);
|
||||
|
||||
// A CompactionService that stages output outside the configured root owns
|
||||
// cleaning it up; the DB must not touch it.
|
||||
const std::string elsewhere = "service_owned_staging_dir";
|
||||
PlantDir(dbname_, elsewhere);
|
||||
PlantDir(SessionTmpPath(), "SESSIONFROMDEADHOST01_4294967296");
|
||||
|
||||
ReopenAndDeleteStaleOutput(options);
|
||||
|
||||
ASSERT_TRUE(Exists(dbname_ + "/" + elsewhere));
|
||||
ASSERT_FALSE(Exists(SessionTmpPath() + "/SESSIONFROMDEADHOST01_4294967296"));
|
||||
|
||||
// Nothing in RocksDB owns this directory, so nothing else will remove it --
|
||||
// including the DestroyDB that the next test's setup runs against the shared
|
||||
// per-thread DB path.
|
||||
ASSERT_OK(DestroyDir(env_, dbname_ + "/" + elsewhere));
|
||||
}
|
||||
|
||||
TEST_F(SessionTmpDirTest, DisabledOptionSkipsCleanup) {
|
||||
if (Exists(SessionTmpPath())) {
|
||||
ASSERT_OK(DestroyDir(env_, SessionTmpPath()));
|
||||
}
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_FALSE(Exists(SessionTmpPath()));
|
||||
|
||||
PlantDir(SessionTmpPath(), "SESSIONFROMDEADHOST01_4294967296");
|
||||
|
||||
Reopen(options);
|
||||
Close();
|
||||
|
||||
ASSERT_TRUE(Exists(SessionTmpPath() + "/SESSIONFROMDEADHOST01_4294967296"));
|
||||
ASSERT_OK(DestroyDir(env_, SessionTmpPath()));
|
||||
}
|
||||
|
||||
TEST_F(SessionTmpDirTest, EnabledSessionTmpDirIsCreated) {
|
||||
Options options = CurrentOptions();
|
||||
options.use_session_tmp_dir_for_remote_compaction = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_TRUE(Exists(SessionTmpPath()));
|
||||
}
|
||||
|
||||
TEST_F(SessionTmpDirTest, DestroyDBRemovesSessionTmpDir) {
|
||||
Options options = OptionsWithCompactionService();
|
||||
CreateDbAndQuiesce(options);
|
||||
PlantDir(SessionTmpPath(), "SESSIONFROMDEADHOST01_4294967296");
|
||||
|
||||
// DestroyDB's final DeleteDir(dbname) is non-recursive, so it can only remove
|
||||
// the DB directory if the session temporary directory is gone first.
|
||||
ASSERT_OK(DestroyDB(dbname_, options));
|
||||
ASSERT_FALSE(Exists(dbname_));
|
||||
}
|
||||
|
||||
TEST_F(SessionTmpDirTest, CompactionUsesSessionTmpDir) {
|
||||
Options options = OptionsWithCompactionService();
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
for (int i = 0; i < 20; i++) {
|
||||
ASSERT_OK(Put(Key(i), "value" + std::to_string(i)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
for (int i = 0; i < 20; i += 2) {
|
||||
ASSERT_OK(Put(Key(i), "value_new" + std::to_string(i)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
auto* my_cs = static_cast_with_check<MyTestCompactionService>(
|
||||
compaction_service_.get());
|
||||
ASSERT_GE(my_cs->GetCompactionNum(), 1);
|
||||
CompactionServiceResult result;
|
||||
my_cs->GetResult(&result);
|
||||
ASSERT_OK(result.status);
|
||||
ASSERT_EQ(result.output_path,
|
||||
RemoteCompactionJobDir(dbname_, true,
|
||||
my_cs->GetLastOutputDirectoryName()));
|
||||
|
||||
for (int i = 0; i < 20; i++) {
|
||||
ASSERT_EQ(Get(Key(i)), (i % 2 ? "value" : "value_new") + std::to_string(i));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -68,11 +68,11 @@ class FileTtlBooster {
|
||||
if (!enabled_) {
|
||||
return 1;
|
||||
}
|
||||
uint64_t oldest_ancester_time = f->TryGetOldestAncesterTime();
|
||||
if (oldest_ancester_time >= current_time_) {
|
||||
uint64_t oldest_ancestor_time = f->TryGetOldestAncestorTime();
|
||||
if (oldest_ancestor_time >= current_time_) {
|
||||
return 1;
|
||||
}
|
||||
uint64_t age = current_time_ - oldest_ancester_time;
|
||||
uint64_t age = current_time_ - oldest_ancestor_time;
|
||||
if (age > boost_age_start_) {
|
||||
// Use integer just for convenience.
|
||||
// We could make all file_to_order double if we want.
|
||||
|
||||
@@ -25,6 +25,18 @@ SstPartitionerFixedPrefixFactory::SstPartitionerFixedPrefixFactory(size_t len)
|
||||
RegisterOptions("Length", &len_, &sst_fixed_prefix_type_info);
|
||||
}
|
||||
|
||||
OptSlice SstPartitionerFixedPrefix::ShouldPartitionByPrefix(
|
||||
const Slice& user_key) {
|
||||
if (user_key.size() >= len_) {
|
||||
return {user_key.data(), len_};
|
||||
} else {
|
||||
// ShouldPartition() cuts a partition for EVERY UNIQUE KEY outside the
|
||||
// prefix length domain, for better or worse. We can't directly represent
|
||||
// that so we defer to ShouldPartition().
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
PartitionerResult SstPartitionerFixedPrefix::ShouldPartition(
|
||||
const PartitionerRequest& request) {
|
||||
Slice last_key_fixed(*request.prev_user_key);
|
||||
|
||||
@@ -168,6 +168,9 @@ class SubcompactionState {
|
||||
? &proximal_level_outputs_
|
||||
: &compaction_outputs_;
|
||||
}
|
||||
SubcompactionState& operator=(SubcompactionState&&) = delete;
|
||||
|
||||
~SubcompactionState() = default;
|
||||
|
||||
// Add all the new files from this compaction to version_edit
|
||||
void AddOutputsEdit(VersionEdit* out_edit) const {
|
||||
|
||||
@@ -389,7 +389,8 @@ TEST_F(TieredCompactionTest, SequenceBasedTieredStorageUniversal) {
|
||||
// the range dels in both cold and hot SSTs
|
||||
auto snap = db_->GetSnapshot();
|
||||
latest_cold_seq = seq_history[2];
|
||||
std::string start = Key(25), end = Key(35);
|
||||
std::string start = Key(25);
|
||||
std::string end = Key(35);
|
||||
ASSERT_OK(
|
||||
db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), start, end));
|
||||
ASSERT_OK(Flush());
|
||||
@@ -620,7 +621,8 @@ TEST_F(TieredCompactionTest, DISABLED_RangeBasedTieredStorageUniversal) {
|
||||
hot_start = Key(50);
|
||||
hot_end = Key(100);
|
||||
}
|
||||
std::string start = Key(1), end = Key(70);
|
||||
std::string start = Key(1);
|
||||
std::string end = Key(70);
|
||||
ASSERT_OK(
|
||||
db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), start, end));
|
||||
ASSERT_OK(Flush());
|
||||
@@ -1268,7 +1270,8 @@ TEST_F(TieredCompactionTest, SequenceBasedTieredStorageLevel) {
|
||||
// the range dels in bottommost
|
||||
auto snap = db_->GetSnapshot();
|
||||
|
||||
std::string start = Key(25), end = Key(35);
|
||||
std::string start = Key(25);
|
||||
std::string end = Key(35);
|
||||
ASSERT_OK(
|
||||
db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), start, end));
|
||||
// add one small key and large key in the input level, to make sure it's able
|
||||
@@ -1339,9 +1342,13 @@ TEST_F(TieredCompactionTest, SequenceBasedTieredStorageLevel) {
|
||||
// This test was essentially for a hacked-up version on future functionality.
|
||||
// It can be resurrected if/when a form of range-based tiering is properly
|
||||
// implemented.
|
||||
// FIXME: aside from that, this test reproduces a near-endless compaction
|
||||
// cycle that needs to be reproduced independently and fixed before
|
||||
// leveled compaction can be used with the preclude feature in production.
|
||||
// The near-endless compaction cycle this used to reproduce (a bottommost file
|
||||
// whose seqno could not be zeroed within the preserve window being re-marked
|
||||
// forever) is now prevented by sharing the seqno-zeroability decision between
|
||||
// the compaction output path and bottommost-file marking (see
|
||||
// BottommostSeqnoCanBeZeroed and CompactionPickerTest.BottommostFile*
|
||||
// PreserveWindow). This test remains disabled only because range-based tiering
|
||||
// is not implemented.
|
||||
TEST_F(TieredCompactionTest, DISABLED_RangeBasedTieredStorageLevel) {
|
||||
const int kNumTrigger = 4;
|
||||
const int kNumLevels = 7;
|
||||
@@ -1558,6 +1565,188 @@ class PrecludeLastLevelTest : public PrecludeLastLevelTestBase,
|
||||
}
|
||||
};
|
||||
|
||||
// Regression test for an infinite kBottommostFiles compaction loop on a
|
||||
// leveled column family that has the seqno->time preserve feature enabled. A
|
||||
// bottommost file whose largest sequence number is still within the preserve
|
||||
// window cannot have its seqno zeroed by compaction, so it must not be marked
|
||||
// for bottommost compaction; otherwise each rewrite makes no progress and the
|
||||
// file is re-marked forever. This exercises the DBImpl plumbing that feeds
|
||||
// VersionStorageInfo::preserve_time_min_seqno_ (DBImpl::
|
||||
// MaybeUpdatePreserveTimeMinSeqno) end to end, complementing the isolated
|
||||
// marking-predicate coverage in compaction_picker_test.cc.
|
||||
class PrecludeBottommostLoopTest : public PrecludeLastLevelTestBase {};
|
||||
|
||||
TEST_F(PrecludeBottommostLoopTest, LeveledPreserveTimeNoBottommostLoop) {
|
||||
const int kNumLevels = 7;
|
||||
Options options = CurrentOptions();
|
||||
options.compaction_style = kCompactionStyleLevel;
|
||||
options.preserve_internal_time_seconds = 10000;
|
||||
options.env = mock_env_.get();
|
||||
options.num_levels = kNumLevels;
|
||||
// Prevent the extra L0 file below from auto-triggering an L0 compaction.
|
||||
options.level0_file_num_compaction_trigger = 4;
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Populate the seqno->time mapping with recent samples so the preserve window
|
||||
// maps the file's (recent) largest seqno as non-zeroable. Total elapsed mock
|
||||
// time stays far below preserve_internal_time_seconds, so nothing ages out.
|
||||
for (int i = 0; i < 10; i++) {
|
||||
ASSERT_OK(Put("infinite", "v" + std::to_string(i)));
|
||||
dbfull()->TEST_WaitForPeriodicTaskRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(10); });
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
// Push the file with the hot key down to the bottommost level.
|
||||
MoveFilesToLevel(kNumLevels - 1);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(kNumLevels - 1));
|
||||
|
||||
// Confirm the bottommost file's largest seqno was not zeroed (it is within
|
||||
// the preserve window), so it is a genuine marking candidate that only the
|
||||
// preserve-window guard should protect -- this keeps the test from passing
|
||||
// vacuously.
|
||||
{
|
||||
std::vector<LiveFileMetaData> files;
|
||||
db_->GetLiveFilesMetaData(&files);
|
||||
bool found_bottommost = false;
|
||||
for (const auto& f : files) {
|
||||
if (f.level == kNumLevels - 1) {
|
||||
ASSERT_GT(f.largest_seqno, 0U);
|
||||
found_bottommost = true;
|
||||
}
|
||||
}
|
||||
ASSERT_TRUE(found_bottommost);
|
||||
}
|
||||
|
||||
// Bump the sequence number above the bottommost file so that releasing a
|
||||
// snapshot advances oldest_snapshot_seqnum_ past the file's largest seqno,
|
||||
// which makes the file a candidate for bottommost marking (the snapshot gate
|
||||
// passes). Only the preserve-window guard should keep it from being marked.
|
||||
ASSERT_OK(Put("bumpseqnum", ""));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
std::atomic<int> bottommost_compactions{0};
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"LevelCompactionPicker::PickCompaction:Return", [&](void* arg) {
|
||||
auto* c = static_cast<Compaction*>(arg);
|
||||
if (c != nullptr &&
|
||||
c->compaction_reason() == CompactionReason::kBottommostFiles) {
|
||||
bottommost_compactions.fetch_add(1);
|
||||
}
|
||||
});
|
||||
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
|
||||
// The bottommost file's largest seqno is within the preserve window, so it is
|
||||
// not marked and the DB quiesces. Before the fix the file was marked and the
|
||||
// rewrite re-qualified it forever (this wait would hang).
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
ASSERT_EQ(0, bottommost_compactions.load());
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
// Close explicitly because mock_env_ is destroyed before the DB otherwise.
|
||||
Close();
|
||||
}
|
||||
|
||||
// Regression test for the empty-mapping variant of the same loop: a bottommost
|
||||
// file that predates a preserve/preclude rollout has an EMPTY persisted
|
||||
// seqno->time mapping. Once the live, DB-wide mapping ages the file out of the
|
||||
// preserve window, the marker (which reads the live mapping) marks it. The
|
||||
// compaction that rewrites it must use the same version-carried preserve
|
||||
// boundary rather than the file's own empty mapping; otherwise it computes
|
||||
// preserve_seqno_after_ = 0, zeroes nothing, and re-marks the file forever.
|
||||
// With the fix the file's seqno is zeroed in one bounded compaction.
|
||||
TEST_F(PrecludeBottommostLoopTest, EmptyMappingAgedOutNoBottommostLoop) {
|
||||
const int kNumLevels = 7;
|
||||
Options options = CurrentOptions();
|
||||
options.compaction_style = kCompactionStyleLevel;
|
||||
options.env = mock_env_.get();
|
||||
options.num_levels = kNumLevels;
|
||||
options.level0_file_num_compaction_trigger = 4;
|
||||
// Start with preserve/preclude disabled so the bottommost file below is
|
||||
// written with an EMPTY persisted seqno->time mapping, mimicking a file that
|
||||
// predates a preserve/preclude rollout.
|
||||
options.preserve_internal_time_seconds = 0;
|
||||
options.preclude_last_level_data_seconds = 0;
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Hold a snapshot below the "aged" key so moving it to the bottommost level
|
||||
// does not zero its seqno (there is no preserve window yet to protect it).
|
||||
ASSERT_OK(Put("keeplow", "v"));
|
||||
const Snapshot* protect = db_->GetSnapshot();
|
||||
ASSERT_OK(Put("aged", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(kNumLevels - 1);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(kNumLevels - 1));
|
||||
|
||||
SequenceNumber aged_seqno = 0;
|
||||
{
|
||||
std::vector<LiveFileMetaData> files;
|
||||
db_->GetLiveFilesMetaData(&files);
|
||||
for (const auto& f : files) {
|
||||
if (f.level == kNumLevels - 1) {
|
||||
aged_seqno = f.largest_seqno;
|
||||
}
|
||||
}
|
||||
}
|
||||
ASSERT_GT(aged_seqno, 0U);
|
||||
|
||||
// Roll out the preserve feature dynamically and let the live, DB-wide
|
||||
// seqno->time mapping accumulate samples while mock time advances well past
|
||||
// preserve_internal_time_seconds, so the pre-existing file's seqno ages out
|
||||
// of the preserve window per the live mapping (even though the file's own
|
||||
// persisted mapping is empty). The protecting snapshot keeps the file from
|
||||
// being marked during this loop (oldest_snapshot_seqnum_ stays below it).
|
||||
ASSERT_OK(db_->SetOptions({{"preserve_internal_time_seconds", "10000"}}));
|
||||
for (int i = 0; i < 100; i++) {
|
||||
ASSERT_OK(Put("hot", "v" + std::to_string(i)));
|
||||
dbfull()->TEST_WaitForPeriodicTaskRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(200); });
|
||||
}
|
||||
|
||||
std::atomic<int> bottommost_compactions{0};
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"LevelCompactionPicker::PickCompaction:Return", [&](void* arg) {
|
||||
auto* c = static_cast<Compaction*>(arg);
|
||||
if (c != nullptr &&
|
||||
c->compaction_reason() == CompactionReason::kBottommostFiles) {
|
||||
bottommost_compactions.fetch_add(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Release the protecting snapshot so oldest_snapshot_seqnum_ advances past
|
||||
// the file's largest seqno, making it a bottommost marking candidate. The
|
||||
// marker (using the live mapping) marks it; the compaction that rewrites it
|
||||
// must use the same version-carried preserve boundary (not the file's empty
|
||||
// mapping) to actually zero the seqno, otherwise it makes no progress and
|
||||
// loops.
|
||||
db_->ReleaseSnapshot(protect);
|
||||
|
||||
// With the fix the file's seqno is zeroed in a bounded number of bottommost
|
||||
// compactions and the DB quiesces. Before the fix this wait would hang.
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
{
|
||||
std::vector<LiveFileMetaData> files;
|
||||
db_->GetLiveFilesMetaData(&files);
|
||||
for (const auto& f : files) {
|
||||
if (f.level == kNumLevels - 1) {
|
||||
ASSERT_EQ(0U, f.largest_seqno);
|
||||
}
|
||||
}
|
||||
}
|
||||
// The file was genuinely marked and rewritten (not vacuously skipped), and
|
||||
// the rewrite made progress instead of looping.
|
||||
ASSERT_GE(bottommost_compactions.load(), 1);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_P(PrecludeLastLevelTest, MigrationFromPreserveTimeManualCompaction) {
|
||||
const int kNumTrigger = 4;
|
||||
const int kNumLevels = 7;
|
||||
@@ -2859,9 +3048,10 @@ TEST_P(PrecludeLastLevelTest, RangeDelsCauseFileEndpointsToOverlap) {
|
||||
verify_db();
|
||||
|
||||
// Rewrite the middle file only. File endpoints should not change.
|
||||
std::string begin_key_buf = Key(kNumKeysPerFile + 1),
|
||||
end_key_buf = Key(kNumKeysPerFile + 2);
|
||||
Slice begin_key(begin_key_buf), end_key(end_key_buf);
|
||||
std::string begin_key_buf = Key(kNumKeysPerFile + 1);
|
||||
std::string end_key_buf = Key(kNumKeysPerFile + 2);
|
||||
Slice begin_key(begin_key_buf);
|
||||
Slice end_key(end_key_buf);
|
||||
ASSERT_OK(db_->SuggestCompactRange(db_->DefaultColumnFamily(), &begin_key,
|
||||
&end_key));
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
@@ -315,8 +315,9 @@ class ComparatorDBTest
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(FormatDef, ComparatorDBTest,
|
||||
testing::Values(test::kDefaultFormatVersion));
|
||||
INSTANTIATE_TEST_CASE_P(FormatLatest, ComparatorDBTest,
|
||||
testing::Values(kLatestBbtFormatVersion));
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
FormatLatest, ComparatorDBTest,
|
||||
testing::Values(BlockBasedTableOptions::kLatestSupportedFormatVersion));
|
||||
|
||||
TEST_P(ComparatorDBTest, Bytewise) {
|
||||
for (int rand_seed = 301; rand_seed < 306; rand_seed++) {
|
||||
|
||||
+212
-6
@@ -10,6 +10,7 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "folly/Executor.h"
|
||||
@@ -18,7 +19,10 @@
|
||||
#include "folly/io/async/EventBase.h"
|
||||
#include "rocksdb/coro_db.h"
|
||||
#include "rocksdb/file_system.h"
|
||||
#include "rocksdb/sst_file_reader.h"
|
||||
#include "table/multiget_context.h"
|
||||
#include "table/sst_file_reader_impl.h"
|
||||
#include "util/coro_stats_util.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
@@ -33,14 +37,28 @@ folly::coro::Task<Status> CoroDB::CoGet(DB* db, const ReadOptions& options,
|
||||
file_system == nullptr ? nullptr : file_system->GetReadExecutor();
|
||||
auto* read_event_base =
|
||||
read_executor == nullptr ? nullptr : read_executor->getEventBase();
|
||||
CoroutineStatsConfig stats_config = CaptureAndDisableCoroutineStatsConfig();
|
||||
|
||||
Status status;
|
||||
if (coro_db == nullptr || read_event_base == nullptr) {
|
||||
CoroutineStatsContextScope stats_scope(std::move(stats_config),
|
||||
db->GetEnv());
|
||||
status = db->Get(options, column_family, key, value, timestamp);
|
||||
} else {
|
||||
auto task = [](CoroutineStatsConfig task_stats_config, Env* task_env,
|
||||
CoroDB* task_db, const ReadOptions& task_options,
|
||||
ColumnFamilyHandle* task_column_family, Slice task_key,
|
||||
PinnableSlice* task_value,
|
||||
std::string* task_timestamp) -> folly::coro::Task<Status> {
|
||||
CoroutineStatsContextScope stats_scope(std::move(task_stats_config),
|
||||
task_env);
|
||||
co_return co_await folly::coro::co_nothrow(
|
||||
task_db->GetCoroutine(task_options, task_column_family, task_key,
|
||||
task_value, task_timestamp));
|
||||
}(std::move(stats_config), db->GetEnv(), coro_db, options, column_family,
|
||||
key, value, timestamp);
|
||||
status = co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base),
|
||||
coro_db->GetCoroutine(options, column_family, key, value, timestamp)));
|
||||
folly::Executor::getKeepAliveToken(read_event_base), std::move(task)));
|
||||
}
|
||||
co_return status;
|
||||
}
|
||||
@@ -73,6 +91,131 @@ folly::coro::Task<Status> CoroDB::CoGet(DB* db, const ReadOptions& options,
|
||||
return CoGet(db, options, db->DefaultColumnFamily(), key, value, timestamp);
|
||||
}
|
||||
|
||||
folly::coro::Task<Status> CoroDB::CoGet(SstFileReader* reader,
|
||||
const ReadOptions& options,
|
||||
const Slice& key,
|
||||
PinnableSlice* value) {
|
||||
assert(reader != nullptr);
|
||||
SstFileReader::Rep* rep = reader->rep_.get();
|
||||
auto* read_executor = rep->read_executor;
|
||||
auto* read_event_base =
|
||||
read_executor == nullptr ? nullptr : read_executor->getEventBase();
|
||||
CoroutineStatsConfig stats_config = CaptureAndDisableCoroutineStatsConfig();
|
||||
|
||||
Status status;
|
||||
if (read_event_base == nullptr) {
|
||||
CoroutineStatsContextScope stats_scope(std::move(stats_config),
|
||||
rep->options.env);
|
||||
status = reader->Get(options, key, value);
|
||||
} else {
|
||||
auto task = [](CoroutineStatsConfig task_stats_config, Env* task_env,
|
||||
SstFileReader* task_reader, const ReadOptions& task_options,
|
||||
Slice task_key,
|
||||
PinnableSlice* task_value) -> folly::coro::Task<Status> {
|
||||
CoroutineStatsContextScope stats_scope(std::move(task_stats_config),
|
||||
task_env);
|
||||
co_return co_await folly::coro::co_nothrow(
|
||||
task_reader->rep_->GetCoroutine(task_options, task_key, task_value));
|
||||
}(std::move(stats_config), rep->options.env, reader, options, key, value);
|
||||
status = co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base), std::move(task)));
|
||||
}
|
||||
co_return status;
|
||||
}
|
||||
|
||||
folly::coro::Task<Status> CoroDB::CoGet(SstFileReader* reader,
|
||||
const ReadOptions& options,
|
||||
const Slice& key, std::string* value) {
|
||||
assert(value != nullptr);
|
||||
PinnableSlice pinnable_value(value);
|
||||
assert(!pinnable_value.IsPinned());
|
||||
Status status = co_await folly::coro::co_nothrow(
|
||||
CoGet(reader, options, key, &pinnable_value));
|
||||
if (status.ok() && pinnable_value.IsPinned()) {
|
||||
value->assign(pinnable_value.data(), pinnable_value.size());
|
||||
}
|
||||
co_return status;
|
||||
}
|
||||
|
||||
folly::coro::Task<Status> CoroDB::CoGetEntity(DB* db,
|
||||
const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const Slice& key,
|
||||
PinnableWideColumns* columns) {
|
||||
assert(db != nullptr);
|
||||
CoroDB* coro_db = db->GetCoroDB();
|
||||
auto* file_system = db->GetFileSystem();
|
||||
auto* read_executor =
|
||||
file_system == nullptr ? nullptr : file_system->GetReadExecutor();
|
||||
auto* read_event_base =
|
||||
read_executor == nullptr ? nullptr : read_executor->getEventBase();
|
||||
CoroutineStatsConfig stats_config = CaptureAndDisableCoroutineStatsConfig();
|
||||
|
||||
Status status;
|
||||
if (coro_db == nullptr || read_event_base == nullptr) {
|
||||
CoroutineStatsContextScope stats_scope(std::move(stats_config),
|
||||
db->GetEnv());
|
||||
status = db->GetEntity(options, column_family, key, columns);
|
||||
} else {
|
||||
auto task =
|
||||
[](CoroutineStatsConfig task_stats_config, Env* task_env,
|
||||
CoroDB* task_db, const ReadOptions& task_options,
|
||||
ColumnFamilyHandle* task_column_family, Slice task_key,
|
||||
PinnableWideColumns* task_columns) -> folly::coro::Task<Status> {
|
||||
CoroutineStatsContextScope stats_scope(std::move(task_stats_config),
|
||||
task_env);
|
||||
co_return co_await folly::coro::co_nothrow(task_db->GetEntityCoroutine(
|
||||
task_options, task_column_family, task_key, task_columns));
|
||||
}(std::move(stats_config), db->GetEnv(), coro_db, options, column_family,
|
||||
key, columns);
|
||||
status = co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base), std::move(task)));
|
||||
}
|
||||
co_return status;
|
||||
}
|
||||
|
||||
folly::coro::Task<Status> CoroDB::CoGetEntity(DB* db,
|
||||
const ReadOptions& options,
|
||||
const Slice& key,
|
||||
PinnableWideColumns* columns) {
|
||||
assert(db != nullptr);
|
||||
return CoGetEntity(db, options, db->DefaultColumnFamily(), key, columns);
|
||||
}
|
||||
|
||||
folly::coro::Task<Status> CoroDB::CoGetEntity(DB* db,
|
||||
const ReadOptions& options,
|
||||
const Slice& key,
|
||||
PinnableAttributeGroups* result) {
|
||||
assert(db != nullptr);
|
||||
CoroDB* coro_db = db->GetCoroDB();
|
||||
auto* file_system = db->GetFileSystem();
|
||||
auto* read_executor =
|
||||
file_system == nullptr ? nullptr : file_system->GetReadExecutor();
|
||||
auto* read_event_base =
|
||||
read_executor == nullptr ? nullptr : read_executor->getEventBase();
|
||||
CoroutineStatsConfig stats_config = CaptureAndDisableCoroutineStatsConfig();
|
||||
|
||||
Status status;
|
||||
if (coro_db == nullptr || read_event_base == nullptr) {
|
||||
CoroutineStatsContextScope stats_scope(std::move(stats_config),
|
||||
db->GetEnv());
|
||||
status = db->GetEntity(options, key, result);
|
||||
} else {
|
||||
auto task =
|
||||
[](CoroutineStatsConfig task_stats_config, Env* task_env,
|
||||
CoroDB* task_db, const ReadOptions& task_options, Slice task_key,
|
||||
PinnableAttributeGroups* task_result) -> folly::coro::Task<Status> {
|
||||
CoroutineStatsContextScope stats_scope(std::move(task_stats_config),
|
||||
task_env);
|
||||
co_return co_await folly::coro::co_nothrow(
|
||||
task_db->GetEntityCoroutine(task_options, task_key, task_result));
|
||||
}(std::move(stats_config), db->GetEnv(), coro_db, options, key, result);
|
||||
status = co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base), std::move(task)));
|
||||
}
|
||||
co_return status;
|
||||
}
|
||||
|
||||
folly::coro::Task<std::vector<Status>> CoroDB::CoMultiGet(
|
||||
DB* db, const ReadOptions& options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
@@ -110,6 +253,54 @@ folly::coro::Task<std::vector<Status>> CoroDB::CoMultiGet(
|
||||
CoMultiGet(db, options, column_families, keys, values, timestamps));
|
||||
}
|
||||
|
||||
folly::coro::Task<std::vector<Status>> CoroDB::CoMultiGet(
|
||||
SstFileReader* reader, const ReadOptions& options,
|
||||
const std::vector<Slice>& keys, std::vector<PinnableSlice>* values) {
|
||||
assert(reader != nullptr);
|
||||
SstFileReader::Rep* rep = reader->rep_.get();
|
||||
auto* read_executor = rep->read_executor;
|
||||
auto* read_event_base =
|
||||
read_executor == nullptr ? nullptr : read_executor->getEventBase();
|
||||
CoroutineStatsConfig stats_config = CaptureAndDisableCoroutineStatsConfig();
|
||||
|
||||
std::vector<Status> statuses;
|
||||
if (read_event_base == nullptr) {
|
||||
CoroutineStatsContextScope stats_scope(std::move(stats_config),
|
||||
rep->options.env);
|
||||
statuses = reader->MultiGet(options, keys, values);
|
||||
} else {
|
||||
auto task = [](CoroutineStatsConfig task_stats_config, Env* task_env,
|
||||
SstFileReader* task_reader, const ReadOptions& task_options,
|
||||
const std::vector<Slice>& task_keys,
|
||||
std::vector<PinnableSlice>* task_values)
|
||||
-> folly::coro::Task<std::vector<Status>> {
|
||||
CoroutineStatsContextScope stats_scope(std::move(task_stats_config),
|
||||
task_env);
|
||||
co_return co_await folly::coro::co_nothrow(
|
||||
task_reader->rep_->MultiGetCoroutine(task_options, task_keys,
|
||||
task_values));
|
||||
}(std::move(stats_config), rep->options.env, reader, options, keys, values);
|
||||
statuses = co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base), std::move(task)));
|
||||
}
|
||||
co_return statuses;
|
||||
}
|
||||
|
||||
folly::coro::Task<std::vector<Status>> CoroDB::CoMultiGet(
|
||||
SstFileReader* reader, const ReadOptions& options,
|
||||
const std::vector<Slice>& keys, std::vector<std::string>* values) {
|
||||
std::vector<PinnableSlice> pinnable_values;
|
||||
std::vector<Status> statuses = co_await folly::coro::co_nothrow(
|
||||
CoMultiGet(reader, options, keys, &pinnable_values));
|
||||
values->resize(keys.size());
|
||||
for (size_t i = 0; i < keys.size(); ++i) {
|
||||
if (statuses[i].ok()) {
|
||||
(*values)[i].assign(pinnable_values[i].data(), pinnable_values[i].size());
|
||||
}
|
||||
}
|
||||
co_return statuses;
|
||||
}
|
||||
|
||||
folly::coro::Task<void> CoroDB::CoMultiGet(
|
||||
DB* db, const ReadOptions& options, size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys,
|
||||
@@ -122,16 +313,31 @@ folly::coro::Task<void> CoroDB::CoMultiGet(
|
||||
file_system == nullptr ? nullptr : file_system->GetReadExecutor();
|
||||
auto* read_event_base =
|
||||
read_executor == nullptr ? nullptr : read_executor->getEventBase();
|
||||
CoroutineStatsConfig stats_config = CaptureAndDisableCoroutineStatsConfig();
|
||||
|
||||
if (coro_db == nullptr || read_event_base == nullptr) {
|
||||
CoroutineStatsContextScope stats_scope(std::move(stats_config),
|
||||
db->GetEnv());
|
||||
db->MultiGet(options, num_keys, column_families, keys, values, timestamps,
|
||||
statuses, sorted_input);
|
||||
} else {
|
||||
auto task = [](CoroutineStatsConfig task_stats_config, Env* task_env,
|
||||
CoroDB* task_db, const ReadOptions& task_options,
|
||||
size_t task_num_keys,
|
||||
ColumnFamilyHandle** task_column_families,
|
||||
const Slice* task_keys, PinnableSlice* task_values,
|
||||
std::string* task_timestamps, Status* task_statuses,
|
||||
bool task_sorted_input) -> folly::coro::Task<void> {
|
||||
CoroutineStatsContextScope stats_scope(std::move(task_stats_config),
|
||||
task_env);
|
||||
co_await folly::coro::co_nothrow(task_db->MultiGetCoroutine(
|
||||
task_options, task_num_keys, task_column_families, task_keys,
|
||||
task_values, task_timestamps, task_statuses, task_sorted_input));
|
||||
}(std::move(stats_config), db->GetEnv(), coro_db, options, num_keys,
|
||||
column_families, keys, values,
|
||||
timestamps, statuses, sorted_input);
|
||||
co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base),
|
||||
coro_db->MultiGetCoroutine(options, num_keys, column_families, keys,
|
||||
values, timestamps, statuses,
|
||||
sorted_input)));
|
||||
folly::Executor::getKeepAliveToken(read_event_base), std::move(task)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
-6
@@ -143,7 +143,8 @@ class CorruptionTest : public testing::Test {
|
||||
}
|
||||
|
||||
void Build(int n, int start, int flush_every) {
|
||||
std::string key_space, value_space;
|
||||
std::string key_space;
|
||||
std::string value_space;
|
||||
WriteBatch batch;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (flush_every != 0 && i != 0 && i % flush_every == 0) {
|
||||
@@ -773,7 +774,8 @@ TEST_F(CorruptionTest, CompactionInputErrorParanoid) {
|
||||
|
||||
// Write must eventually fail because of corrupted table
|
||||
Status s;
|
||||
std::string tmp1, tmp2;
|
||||
std::string tmp1;
|
||||
std::string tmp2;
|
||||
bool failed = false;
|
||||
for (int i = 0; i < 10000; i++) {
|
||||
s = db_->Put(WriteOptions(), Key(i, &tmp1), Value(i, &tmp2));
|
||||
@@ -792,7 +794,8 @@ TEST_F(CorruptionTest, UnrelatedKeys) {
|
||||
Corrupt(kTableFile, 100, 1);
|
||||
ASSERT_NOK(db_->VerifyChecksum());
|
||||
|
||||
std::string tmp1, tmp2;
|
||||
std::string tmp1;
|
||||
std::string tmp2;
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(1000, &tmp1), Value(1000, &tmp2)));
|
||||
std::string v;
|
||||
ASSERT_OK(db_->Get(ReadOptions(), Key(1000, &tmp1), &v));
|
||||
@@ -940,7 +943,8 @@ TEST_F(CorruptionTest, ParanoidFileChecksWithDeleteRangeFirst) {
|
||||
db_.reset();
|
||||
ASSERT_OK(DestroyDB(dbname_, options));
|
||||
ASSERT_OK(DB::Open(options, dbname_, &db_));
|
||||
std::string start, end;
|
||||
std::string start;
|
||||
std::string end;
|
||||
assert(db_ != nullptr); // suppress false clang-analyze report
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(),
|
||||
Key(3, &start), Key(7, &end)));
|
||||
@@ -976,7 +980,8 @@ TEST_F(CorruptionTest, ParanoidFileChecksWithDeleteRange) {
|
||||
ASSERT_OK(DB::Open(options, dbname_, &db_));
|
||||
assert(db_ != nullptr); // suppress false clang-analyze report
|
||||
Build(10, 0, 0);
|
||||
std::string start, end;
|
||||
std::string start;
|
||||
std::string end;
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(),
|
||||
Key(5, &start), Key(15, &end)));
|
||||
auto snap = db_->GetSnapshot();
|
||||
@@ -1012,7 +1017,8 @@ TEST_F(CorruptionTest, ParanoidFileChecksWithDeleteRangeLast) {
|
||||
ASSERT_OK(DestroyDB(dbname_, options));
|
||||
ASSERT_OK(DB::Open(options, dbname_, &db_));
|
||||
assert(db_ != nullptr); // suppress false clang-analyze report
|
||||
std::string start, end;
|
||||
std::string start;
|
||||
std::string end;
|
||||
Build(10);
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(),
|
||||
Key(3, &start), Key(7, &end)));
|
||||
|
||||
+945
-44
File diff suppressed because it is too large.
Load diff
@@ -1921,7 +1921,10 @@ TEST_F(CacheKeyTest, DBImplSessionIdStructure) {
|
||||
// counter."
|
||||
std::string session_id1 = DBImpl::GenerateDbSessionId(/*env*/ nullptr);
|
||||
std::string session_id2 = DBImpl::GenerateDbSessionId(/*env*/ nullptr);
|
||||
uint64_t upper1, upper2, lower1, lower2;
|
||||
uint64_t upper1;
|
||||
uint64_t upper2;
|
||||
uint64_t lower1;
|
||||
uint64_t lower2;
|
||||
ASSERT_OK(DecodeSessionId(session_id1, &upper1, &lower1));
|
||||
ASSERT_OK(DecodeSessionId(session_id2, &upper2, &lower2));
|
||||
// Because generated in same process
|
||||
@@ -1981,7 +1984,8 @@ struct CacheKeyDecoder {
|
||||
}
|
||||
|
||||
void Decode(const CacheKey& key) {
|
||||
uint64_t file_num_etc64, offset_etc64;
|
||||
uint64_t file_num_etc64;
|
||||
uint64_t offset_etc64;
|
||||
DeconstructNonemptyCacheKey(key, &file_num_etc64, &offset_etc64);
|
||||
|
||||
// First decode session counter
|
||||
|
||||
@@ -917,14 +917,15 @@ INSTANTIATE_TEST_CASE_P(
|
||||
::testing::Values(
|
||||
std::make_tuple(kAutoBloom,
|
||||
FilterPartitioning::kCoupledPartitionedFilter,
|
||||
kLatestBbtFormatVersion),
|
||||
BlockBasedTableOptions::kLatestSupportedFormatVersion),
|
||||
std::make_tuple(kAutoBloom,
|
||||
FilterPartitioning::kDecoupledPartitionedFilter,
|
||||
kLatestBbtFormatVersion),
|
||||
BlockBasedTableOptions::kLatestSupportedFormatVersion),
|
||||
std::make_tuple(kAutoBloom, FilterPartitioning::kUnpartitionedFilter,
|
||||
kLatestBbtFormatVersion),
|
||||
std::make_tuple(kAutoRibbon, FilterPartitioning::kUnpartitionedFilter,
|
||||
kLatestBbtFormatVersion)));
|
||||
BlockBasedTableOptions::kLatestSupportedFormatVersion),
|
||||
std::make_tuple(
|
||||
kAutoRibbon, FilterPartitioning::kUnpartitionedFilter,
|
||||
BlockBasedTableOptions::kLatestSupportedFormatVersion)));
|
||||
#endif // !defined(ROCKSDB_VALGRIND_RUN) || defined(ROCKSDB_FULL_VALGRIND_RUN)
|
||||
|
||||
TEST_F(DBBloomFilterTest, BloomFilterRate) {
|
||||
@@ -2542,9 +2543,13 @@ TEST_P(DBBloomFilterTestVaryPrefixAndFormatVer, PartitionedMultiGet) {
|
||||
values[i] = PinnableSlice();
|
||||
}
|
||||
|
||||
get_perf_context()->Reset();
|
||||
const PerfLevel previous_perf_level = GetPerfLevel();
|
||||
SetPerfLevel(PerfLevel::kEnableTime);
|
||||
db_->MultiGet(ropts, Q, column_families.data(), key_slices.data(),
|
||||
values.data(),
|
||||
/*timestamps=*/nullptr, statuses.data(), true);
|
||||
SetPerfLevel(previous_perf_level);
|
||||
|
||||
// Confirm correct status results
|
||||
uint32_t number_not_found = 0;
|
||||
@@ -2574,6 +2579,8 @@ TEST_P(DBBloomFilterTestVaryPrefixAndFormatVer, PartitionedMultiGet) {
|
||||
// Confirm no duplicate loading same filter partition
|
||||
uint64_t filter_accesses = PopTicker(options, BLOCK_CACHE_FILTER_HIT) +
|
||||
PopTicker(options, BLOCK_CACHE_FILTER_MISS);
|
||||
// Partition reads must contribute to the filter-block timing metric.
|
||||
EXPECT_GT(get_perf_context()->read_filter_block_nanos, 0);
|
||||
if (stride == 1) {
|
||||
EXPECT_EQ(filter_accesses, 1);
|
||||
} else {
|
||||
|
||||
+85
-2
@@ -3,8 +3,11 @@
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include <atomic>
|
||||
|
||||
#include "db/db_test_util.h"
|
||||
#include "port/port.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "util/random.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -14,6 +17,62 @@ class DBClipTest : public DBTestBase {
|
||||
DBClipTest() : DBTestBase("db_clip_test", /*env_do_fsync=*/true) {}
|
||||
};
|
||||
|
||||
// ClipColumnFamily reads the CF's boundary keys under the DB mutex, then drops
|
||||
// it and issues several writes with those keys. Those keys must not point into
|
||||
// Version-owned FileMetaData, which a background job can replace and free in
|
||||
// that window. Here a full compaction runs inside the window and obsoletes the
|
||||
// Version the keys were read from.
|
||||
TEST_F(DBClipTest, BoundaryKeysSurviveConcurrentVersionChange) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
options.num_levels = 3;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
Random rnd(301);
|
||||
std::map<int32_t, std::string> values;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
for (int j = 0; j < 100; j++) {
|
||||
int k = i * 100 + j;
|
||||
values[k] = rnd.RandomString(1024);
|
||||
ASSERT_OK(Put(Key(k), values[k]));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
|
||||
std::atomic<bool> fired{false};
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::ClipColumnFamily:PostBoundaryKeys", [&](void*) {
|
||||
if (fired.exchange(true)) {
|
||||
return;
|
||||
}
|
||||
// Replace and free the Version the boundary keys were read from, on
|
||||
// another thread so this does not re-enter Clip's own call stack.
|
||||
port::Thread compactor([&] {
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
ASSERT_OK(dbfull()->TEST_WaitForPurge());
|
||||
});
|
||||
compactor.join();
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
const auto begin_key = Key(251);
|
||||
const auto end_key = Key(751);
|
||||
ASSERT_OK(
|
||||
db_->ClipColumnFamily(db_->DefaultColumnFamily(), begin_key, end_key));
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
ASSERT_TRUE(fired.load());
|
||||
|
||||
for (int i = 251; i < 751; i++) {
|
||||
ASSERT_EQ(Get(Key(i)), values[i]);
|
||||
}
|
||||
for (int i : {0, 250, 751, 999}) {
|
||||
std::string result;
|
||||
ASSERT_TRUE(db_->Get(ReadOptions(), Key(i), &result).IsNotFound());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBClipTest, TestClipRange) {
|
||||
Options options = CurrentOptions();
|
||||
options.write_buffer_size = 10 * 1024 * 1024;
|
||||
@@ -39,7 +98,8 @@ TEST_F(DBClipTest, TestClipRange) {
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
ASSERT_EQ("10", FilesPerLevel(0));
|
||||
auto begin_key = Key(251), end_key = Key(751);
|
||||
auto begin_key = Key(251);
|
||||
auto end_key = Key(751);
|
||||
ASSERT_OK(
|
||||
db_->ClipColumnFamily(db_->DefaultColumnFamily(), begin_key, end_key));
|
||||
|
||||
@@ -99,7 +159,8 @@ TEST_F(DBClipTest, TestClipRange) {
|
||||
}
|
||||
ASSERT_EQ("5,5,3", FilesPerLevel(0));
|
||||
|
||||
auto begin_key_2 = Key(222), end_key_2 = Key(888);
|
||||
auto begin_key_2 = Key(222);
|
||||
auto end_key_2 = Key(888);
|
||||
|
||||
ASSERT_OK(db_->ClipColumnFamily(db_->DefaultColumnFamily(), begin_key_2,
|
||||
end_key_2));
|
||||
@@ -133,6 +194,28 @@ TEST_F(DBClipTest, TestClipRange) {
|
||||
ASSERT_TRUE(in_range);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBClipTest, KeepFileEndingAtBeginKey) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put("a", "va"));
|
||||
ASSERT_OK(Put("b", "vb"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(1);
|
||||
ASSERT_OK(Put("c", "vc"));
|
||||
ASSERT_OK(Put("d", "vd"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(1);
|
||||
ASSERT_EQ("0,2", FilesPerLevel(0));
|
||||
|
||||
ASSERT_OK(db_->ClipColumnFamily(db_->DefaultColumnFamily(), "b", "d"));
|
||||
ASSERT_EQ("NOT_FOUND", Get("a"));
|
||||
ASSERT_EQ("vb", Get("b"));
|
||||
ASSERT_EQ("vc", Get("c"));
|
||||
ASSERT_EQ("NOT_FOUND", Get("d"));
|
||||
}
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -601,7 +601,10 @@ TEST_P(DBTestCompactionFilterWithCompactParam,
|
||||
}
|
||||
|
||||
TEST_F(DBTestCompactionFilter, CompactionFilterWithMergeOperator) {
|
||||
std::string one, two, three, four;
|
||||
std::string one;
|
||||
std::string two;
|
||||
std::string three;
|
||||
std::string four;
|
||||
PutFixed64(&one, 1);
|
||||
PutFixed64(&two, 2);
|
||||
PutFixed64(&three, 3);
|
||||
|
||||
+603
-31
@@ -7,6 +7,9 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file. See the AUTHORS file for names of contributors.
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
|
||||
@@ -14,6 +17,7 @@
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/db_test_util.h"
|
||||
#include "db/dbformat.h"
|
||||
#include "db/periodic_compaction_phaser.h"
|
||||
#include "db/table_cache.h"
|
||||
#include "env/mock_env.h"
|
||||
#include "file/filename.h"
|
||||
@@ -25,10 +29,14 @@
|
||||
#include "rocksdb/file_checksum.h"
|
||||
#include "rocksdb/iostats_context.h"
|
||||
#include "rocksdb/sst_file_writer.h"
|
||||
#include "table/block_based/block_based_table_factory.h"
|
||||
#include "table/table_builder.h"
|
||||
#include "test_util/mock_time_env.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "test_util/testutil.h"
|
||||
#include "util/concurrent_task_limiter_impl.h"
|
||||
#include "util/fastrange.h"
|
||||
#include "util/hash.h"
|
||||
#include "util/random.h"
|
||||
#include "utilities/fault_injection_env.h"
|
||||
#include "utilities/fault_injection_fs.h"
|
||||
@@ -170,6 +178,264 @@ class DBCompactionTest : public DBTestBase {
|
||||
}
|
||||
};
|
||||
|
||||
struct FileCuttingTableFactoryState {
|
||||
std::atomic<int> cut_requests{0};
|
||||
int max_cut_requests = 1;
|
||||
bool allow_empty_builder_cut = false;
|
||||
};
|
||||
|
||||
class FileCuttingTableBuilder : public TableBuilder {
|
||||
public:
|
||||
FileCuttingTableBuilder(std::unique_ptr<TableBuilder> base_builder,
|
||||
std::string cut_before_user_key,
|
||||
std::shared_ptr<FileCuttingTableFactoryState> state)
|
||||
: base_builder_(std::move(base_builder)),
|
||||
cut_before_user_key_(std::move(cut_before_user_key)),
|
||||
state_(std::move(state)) {}
|
||||
|
||||
void Add(const Slice& key, const Slice& value) override {
|
||||
base_builder_->Add(key, value);
|
||||
}
|
||||
|
||||
TableBuilderAddResult TryAdd(const Slice& key, const Slice& value,
|
||||
TableBuilderAddContext* context) override {
|
||||
const Slice user_key = ExtractUserKey(key);
|
||||
const bool cut_request_limit_not_reached =
|
||||
state_->cut_requests.load() < state_->max_cut_requests;
|
||||
const bool should_cut =
|
||||
cut_request_limit_not_reached &&
|
||||
(state_->allow_empty_builder_cut || !base_builder_->IsEmpty()) &&
|
||||
user_key.compare(Slice(cut_before_user_key_)) == 0;
|
||||
if (should_cut) {
|
||||
state_->cut_requests++;
|
||||
context->new_table_reason = TableBuilderNewTableReason::kBuilderPolicy;
|
||||
return TableBuilderAddResult::kRequiresNewTable;
|
||||
}
|
||||
return base_builder_->TryAdd(key, value, context);
|
||||
}
|
||||
|
||||
Status status() const override { return base_builder_->status(); }
|
||||
|
||||
IOStatus io_status() const override { return base_builder_->io_status(); }
|
||||
|
||||
Status Finish() override { return base_builder_->Finish(); }
|
||||
|
||||
void Abandon() override { base_builder_->Abandon(); }
|
||||
|
||||
uint64_t NumEntries() const override { return base_builder_->NumEntries(); }
|
||||
|
||||
bool IsEmpty() const override { return base_builder_->IsEmpty(); }
|
||||
|
||||
uint64_t PreCompressionSize() const override {
|
||||
return base_builder_->PreCompressionSize();
|
||||
}
|
||||
|
||||
uint64_t FileSize() const override { return base_builder_->FileSize(); }
|
||||
|
||||
uint64_t EstimatedFileSize() const override {
|
||||
return base_builder_->EstimatedFileSize();
|
||||
}
|
||||
|
||||
uint64_t EstimatedTailSize() const override {
|
||||
return base_builder_->EstimatedTailSize();
|
||||
}
|
||||
|
||||
uint64_t GetTailSize() const override { return base_builder_->GetTailSize(); }
|
||||
|
||||
bool NeedCompact() const override { return base_builder_->NeedCompact(); }
|
||||
|
||||
TableProperties GetTableProperties() const override {
|
||||
return base_builder_->GetTableProperties();
|
||||
}
|
||||
|
||||
std::string GetFileChecksum() const override {
|
||||
return base_builder_->GetFileChecksum();
|
||||
}
|
||||
|
||||
const char* GetFileChecksumFuncName() const override {
|
||||
return base_builder_->GetFileChecksumFuncName();
|
||||
}
|
||||
|
||||
void SetSeqnoTimeTableProperties(const SeqnoToTimeMapping& relevant_mapping,
|
||||
uint64_t oldest_ancestor_time) override {
|
||||
base_builder_->SetSeqnoTimeTableProperties(relevant_mapping,
|
||||
oldest_ancestor_time);
|
||||
}
|
||||
|
||||
uint64_t GetWorkerCPUMicros() const override {
|
||||
return base_builder_->GetWorkerCPUMicros();
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<TableBuilder> base_builder_;
|
||||
std::string cut_before_user_key_;
|
||||
std::shared_ptr<FileCuttingTableFactoryState> state_;
|
||||
};
|
||||
|
||||
class FileCuttingTableFactory : public TableFactory {
|
||||
public:
|
||||
FileCuttingTableFactory(std::string cut_before_user_key,
|
||||
std::shared_ptr<FileCuttingTableFactoryState> state)
|
||||
: cut_before_user_key_(std::move(cut_before_user_key)),
|
||||
state_(std::move(state)) {}
|
||||
|
||||
const char* Name() const override { return "FileCuttingTableFactory"; }
|
||||
|
||||
using TableFactory::NewTableReader;
|
||||
|
||||
Status NewTableReader(
|
||||
const ReadOptions& read_options,
|
||||
const TableReaderOptions& table_reader_options,
|
||||
std::unique_ptr<RandomAccessFileReader>&& file, uint64_t file_size,
|
||||
std::unique_ptr<TableReader>* table_reader,
|
||||
bool prefetch_index_and_filter_in_cache) const override {
|
||||
return base_factory_.NewTableReader(
|
||||
read_options, table_reader_options, std::move(file), file_size,
|
||||
table_reader, prefetch_index_and_filter_in_cache);
|
||||
}
|
||||
|
||||
TableBuilder* NewTableBuilder(
|
||||
const TableBuilderOptions& table_builder_options,
|
||||
WritableFileWriter* file) const override {
|
||||
return new FileCuttingTableBuilder(
|
||||
std::unique_ptr<TableBuilder>(
|
||||
base_factory_.NewTableBuilder(table_builder_options, file)),
|
||||
cut_before_user_key_, state_);
|
||||
}
|
||||
|
||||
std::unique_ptr<TableFactory> Clone() const override {
|
||||
return std::make_unique<FileCuttingTableFactory>(cut_before_user_key_,
|
||||
state_);
|
||||
}
|
||||
|
||||
bool IsDeleteRangeSupported() const override {
|
||||
return base_factory_.IsDeleteRangeSupported();
|
||||
}
|
||||
|
||||
std::string GetPrintableOptions() const override {
|
||||
return base_factory_.GetPrintableOptions();
|
||||
}
|
||||
|
||||
private:
|
||||
std::string cut_before_user_key_;
|
||||
std::shared_ptr<FileCuttingTableFactoryState> state_;
|
||||
BlockBasedTableFactory base_factory_;
|
||||
};
|
||||
|
||||
TEST_F(DBCompactionTest, TableBuilderCanRequestNewTableDuringCompaction) {
|
||||
auto factory_state = std::make_shared<FileCuttingTableFactoryState>();
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
options.level0_file_num_compaction_trigger = 10;
|
||||
options.target_file_size_base = 1 << 30;
|
||||
options.table_factory =
|
||||
std::make_shared<FileCuttingTableFactory>(Key(5), factory_state);
|
||||
Reopen(options);
|
||||
|
||||
for (int key : {0, 2, 4, 6, 8}) {
|
||||
ASSERT_OK(Put(Key(key), "value" + std::to_string(key)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
for (int key : {1, 3, 5, 7, 9}) {
|
||||
ASSERT_OK(Put(Key(key), "value" + std::to_string(key)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_EQ(2, NumTableFilesAtLevel(0));
|
||||
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
ASSERT_EQ(1, factory_state->cut_requests.load());
|
||||
ASSERT_EQ(0, NumTableFilesAtLevel(0));
|
||||
ASSERT_EQ(2, NumTableFilesAtLevel(1));
|
||||
for (int key = 0; key < 10; ++key) {
|
||||
ASSERT_EQ("value" + std::to_string(key), Get(Key(key)));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, TableBuilderCutRequestForLevelZeroFailsCompaction) {
|
||||
auto factory_state = std::make_shared<FileCuttingTableFactoryState>();
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.compaction_style = kCompactionStyleUniversal;
|
||||
options.disable_auto_compactions = true;
|
||||
options.num_levels = 1;
|
||||
options.table_factory =
|
||||
std::make_shared<FileCuttingTableFactory>(Key(5), factory_state);
|
||||
Reopen(options);
|
||||
|
||||
for (int key : {0, 2, 4, 6, 8}) {
|
||||
ASSERT_OK(Put(Key(key), "value" + std::to_string(key)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
for (int key : {1, 3, 5, 7, 9}) {
|
||||
ASSERT_OK(Put(Key(key), "value" + std::to_string(key)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
Status status = db_->CompactRange(CompactRangeOptions(), nullptr, nullptr);
|
||||
ASSERT_TRUE(status.IsCorruption()) << status.ToString();
|
||||
ASSERT_NE(std::string::npos, status.ToString().find("level 0 output"))
|
||||
<< status.ToString();
|
||||
ASSERT_EQ(1, factory_state->cut_requests.load());
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, TableBuilderCutOnEmptyBuilderFailsCompaction) {
|
||||
auto factory_state = std::make_shared<FileCuttingTableFactoryState>();
|
||||
factory_state->allow_empty_builder_cut = true;
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
options.allow_data_in_errors = false;
|
||||
options.level0_file_num_compaction_trigger = 10;
|
||||
options.target_file_size_base = 1 << 30;
|
||||
options.table_factory =
|
||||
std::make_shared<FileCuttingTableFactory>(Key(0), factory_state);
|
||||
Reopen(options);
|
||||
|
||||
for (int key : {0, 2, 4, 6, 8}) {
|
||||
ASSERT_OK(Put(Key(key), "value" + std::to_string(key)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
for (int key : {1, 3, 5, 7, 9}) {
|
||||
ASSERT_OK(Put(Key(key), "value" + std::to_string(key)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
Status status = db_->CompactRange(CompactRangeOptions(), nullptr, nullptr);
|
||||
ASSERT_TRUE(status.IsCorruption()) << status.ToString();
|
||||
ASSERT_NE(std::string::npos, status.ToString().find("key: hidden"))
|
||||
<< status.ToString();
|
||||
ASSERT_EQ(1, factory_state->cut_requests.load());
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, TableBuilderRepeatedCutRequestFailsCompaction) {
|
||||
auto factory_state = std::make_shared<FileCuttingTableFactoryState>();
|
||||
factory_state->max_cut_requests = 2;
|
||||
factory_state->allow_empty_builder_cut = true;
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
options.level0_file_num_compaction_trigger = 10;
|
||||
options.target_file_size_base = 1 << 30;
|
||||
options.table_factory =
|
||||
std::make_shared<FileCuttingTableFactory>(Key(5), factory_state);
|
||||
Reopen(options);
|
||||
|
||||
for (int key : {0, 2, 4, 6, 8}) {
|
||||
ASSERT_OK(Put(Key(key), "value" + std::to_string(key)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
for (int key : {1, 3, 5, 7, 9}) {
|
||||
ASSERT_OK(Put(Key(key), "value" + std::to_string(key)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
Status status = db_->CompactRange(CompactRangeOptions(), nullptr, nullptr);
|
||||
ASSERT_TRUE(status.IsCorruption()) << status.ToString();
|
||||
ASSERT_EQ(2, factory_state->cut_requests.load());
|
||||
}
|
||||
|
||||
class DBCompactionTestWithParam
|
||||
: public DBTestBase,
|
||||
public testing::WithParamInterface<std::tuple<uint32_t, bool>> {
|
||||
@@ -1334,6 +1600,118 @@ TEST_F(DBCompactionTest, CompactionSstPartitionerNonTrivial) {
|
||||
ASSERT_EQ("B", Get("bbbb1"));
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, CompactionSstPartitionerPrefixFastPath) {
|
||||
// Exercises the ShouldPartitionByPrefix() fast path in CompactionOutputs:
|
||||
// multiple keys sharing a fixed prefix must stay together in one output file
|
||||
// (no spurious cuts), with boundaries landing exactly at prefix changes.
|
||||
Options options = CurrentOptions();
|
||||
options.compaction_style = kCompactionStyleLevel;
|
||||
options.level0_file_num_compaction_trigger = 3;
|
||||
options.sst_partitioner_factory = NewSstPartitionerFixedPrefixFactory(4);
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Three prefixes with multiple keys each, spread across two flushed files so
|
||||
// that a non-trivial compaction merges them.
|
||||
ASSERT_OK(Put("aaaa1", "v"));
|
||||
ASSERT_OK(Put("aaaa3", "v"));
|
||||
ASSERT_OK(Put("bbbb1", "v"));
|
||||
ASSERT_OK(Put("cccc2", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
|
||||
ASSERT_OK(Put("aaaa2", "v"));
|
||||
ASSERT_OK(Put("bbbb2", "v"));
|
||||
ASSERT_OK(Put("cccc1", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
|
||||
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
// Exactly one output file per distinct 4-byte prefix.
|
||||
std::vector<LiveFileMetaData> files;
|
||||
dbfull()->GetLiveFilesMetaData(&files);
|
||||
ASSERT_EQ(3, files.size());
|
||||
// Each file holds a whole prefix group (multiple keys => the fast path
|
||||
// correctly did NOT cut within a shared prefix).
|
||||
for (const auto& f : files) {
|
||||
ASSERT_EQ(f.smallestkey.substr(0, 4), f.largestkey.substr(0, 4));
|
||||
}
|
||||
for (const std::string k :
|
||||
{"aaaa1", "aaaa2", "aaaa3", "bbbb1", "bbbb2", "cccc1", "cccc2"}) {
|
||||
ASSERT_EQ("v", Get(k));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, CompactionSstPartitionerPrefixShortKeyFallback) {
|
||||
// Keys shorter than the fixed prefix length cannot be represented as a
|
||||
// prefix, so ShouldPartitionByPrefix() returns no value and CompactionOutputs
|
||||
// falls back to per-key ShouldPartition(). This also exercises short->long
|
||||
// and long->short mode transitions across output file boundaries.
|
||||
Options options = CurrentOptions();
|
||||
options.compaction_style = kCompactionStyleLevel;
|
||||
options.level0_file_num_compaction_trigger = 3;
|
||||
options.sst_partitioner_factory = NewSstPartitionerFixedPrefixFactory(4);
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// "a" and "bb" are shorter than len 4 (fallback, each in its own file);
|
||||
// "cccc1"/"cccc2" share prefix "cccc" (fast path, one file); "dd" is short
|
||||
// again (fallback, own file).
|
||||
ASSERT_OK(Put("a", "v"));
|
||||
ASSERT_OK(Put("bb", "v"));
|
||||
ASSERT_OK(Put("cccc1", "v"));
|
||||
ASSERT_OK(Put("dd", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
|
||||
// Second file overlaps to force a non-trivial (rewriting) compaction.
|
||||
ASSERT_OK(Put("cccc2", "v"));
|
||||
ASSERT_OK(Put("a", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
|
||||
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
// "a" | "bb" | {"cccc1","cccc2"} | "dd" => 4 files.
|
||||
std::vector<LiveFileMetaData> files;
|
||||
dbfull()->GetLiveFilesMetaData(&files);
|
||||
ASSERT_EQ(4, files.size());
|
||||
ASSERT_EQ("v2", Get("a"));
|
||||
for (const std::string k : {"bb", "cccc1", "cccc2", "dd"}) {
|
||||
ASSERT_EQ("v", Get(k));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, CompactionSstPartitionerZeroLenNoSplit) {
|
||||
// A zero-length fixed prefix yields an empty (but present) prefix from
|
||||
// ShouldPartitionByPrefix(), which means "no further partitions": every key
|
||||
// "starts with" the empty prefix, so the fast path never cuts.
|
||||
Options options = CurrentOptions();
|
||||
options.compaction_style = kCompactionStyleLevel;
|
||||
options.level0_file_num_compaction_trigger = 3;
|
||||
options.sst_partitioner_factory = NewSstPartitionerFixedPrefixFactory(0);
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put("aaaa1", "v1"));
|
||||
ASSERT_OK(Put("bbbb1", "v1"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
|
||||
// Overlapping key forces a non-trivial compaction (not a trivial move).
|
||||
ASSERT_OK(Put("aaaa1", "v2"));
|
||||
ASSERT_OK(Put("cccc1", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
|
||||
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
std::vector<LiveFileMetaData> files;
|
||||
dbfull()->GetLiveFilesMetaData(&files);
|
||||
ASSERT_EQ(1, files.size());
|
||||
ASSERT_EQ("v2", Get("aaaa1"));
|
||||
ASSERT_EQ("v1", Get("bbbb1"));
|
||||
ASSERT_EQ("v2", Get("cccc1"));
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, ZeroSeqIdCompaction) {
|
||||
Options options = CurrentOptions();
|
||||
options.compaction_style = kCompactionStyleLevel;
|
||||
@@ -2343,9 +2721,12 @@ TEST_P(DBDeleteFileRangeTest, DeleteFilesInRanges) {
|
||||
|
||||
// Delete files in range [0, 299] (inclusive)
|
||||
{
|
||||
auto begin_str1 = Key(0), end_str1 = Key(100);
|
||||
auto begin_str2 = Key(100), end_str2 = Key(200);
|
||||
auto begin_str3 = Key(200), end_str3 = Key(299);
|
||||
auto begin_str1 = Key(0);
|
||||
auto end_str1 = Key(100);
|
||||
auto begin_str2 = Key(100);
|
||||
auto end_str2 = Key(200);
|
||||
auto begin_str3 = Key(200);
|
||||
auto end_str3 = Key(299);
|
||||
std::vector<RangeOpt> ranges;
|
||||
ranges.emplace_back(begin_str1, end_str1);
|
||||
ranges.emplace_back(begin_str2, end_str2);
|
||||
@@ -2367,12 +2748,18 @@ TEST_P(DBDeleteFileRangeTest, DeleteFilesInRanges) {
|
||||
|
||||
// Delete files in range [600, 999) (exclusive)
|
||||
{
|
||||
auto begin_str1 = Key(600), end_str1 = Key(800);
|
||||
auto begin_str2 = Key(700), end_str2 = Key(900);
|
||||
auto begin_str3 = Key(800), end_str3 = Key(999);
|
||||
Slice begin1(begin_str1), end1(end_str1);
|
||||
Slice begin2(begin_str2), end2(end_str2);
|
||||
Slice begin3(begin_str3), end3(end_str3);
|
||||
auto begin_str1 = Key(600);
|
||||
auto end_str1 = Key(800);
|
||||
auto begin_str2 = Key(700);
|
||||
auto end_str2 = Key(900);
|
||||
auto begin_str3 = Key(800);
|
||||
auto end_str3 = Key(999);
|
||||
Slice begin1(begin_str1);
|
||||
Slice end1(end_str1);
|
||||
Slice begin2(begin_str2);
|
||||
Slice end2(end_str2);
|
||||
Slice begin3(begin_str3);
|
||||
Slice end3(end_str3);
|
||||
std::vector<RangePtr> ranges;
|
||||
ranges.emplace_back(&begin1, &end1);
|
||||
ranges.emplace_back(&begin2, &end2);
|
||||
@@ -2459,8 +2846,10 @@ TEST_P(DBDeleteFileRangeTest, DeleteFileRangeFileEndpointsOverlapBug) {
|
||||
|
||||
// Verify `DeleteFilesInRange` can't drop only file 0 which would cause
|
||||
// "1 -> vals[0]" to reappear.
|
||||
std::string begin_str = Key(0), end_str = Key(1);
|
||||
Slice begin = begin_str, end = end_str;
|
||||
std::string begin_str = Key(0);
|
||||
std::string end_str = Key(1);
|
||||
Slice begin = begin_str;
|
||||
Slice end = end_str;
|
||||
ASSERT_OK(
|
||||
DeleteFilesInRange(db_.get(), db_->DefaultColumnFamily(), &begin, &end));
|
||||
ASSERT_EQ(vals[1], GetValue(Key(1)));
|
||||
@@ -2768,7 +3157,8 @@ TEST_P(DBCompactionTestWithParam, LevelCompactionCFPathUse) {
|
||||
|
||||
std::vector<Options> option_vector;
|
||||
option_vector.emplace_back(options);
|
||||
ColumnFamilyOptions cf_opt1(options), cf_opt2(options);
|
||||
ColumnFamilyOptions cf_opt1(options);
|
||||
ColumnFamilyOptions cf_opt2(options);
|
||||
// Configure CF1 specific paths.
|
||||
cf_opt1.cf_paths.emplace_back(dbname_ + "cf1", 500 * 1024);
|
||||
cf_opt1.cf_paths.emplace_back(dbname_ + "cf1_2", 4 * 1024 * 1024);
|
||||
@@ -3544,7 +3934,10 @@ TEST_P(DBCompactionTestWithParam, CompressLevelCompaction) {
|
||||
// move to level 3 will not be allowed
|
||||
options.compression_per_level = {kNoCompression, kNoCompression,
|
||||
kZlibCompression};
|
||||
int matches = 0, didnt_match = 0, trivial_move = 0, non_trivial = 0;
|
||||
int matches = 0;
|
||||
int didnt_match = 0;
|
||||
int trivial_move = 0;
|
||||
int non_trivial = 0;
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"Compaction::InputCompressionMatchesOutput:Matches",
|
||||
@@ -4620,7 +5013,8 @@ TEST_F(DBCompactionTest, CompactBottomLevelFilesWithDeletions) {
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
ASSERT_EQ(kNumLevelFiles, NumTableFilesAtLevel(1));
|
||||
|
||||
std::vector<LiveFileMetaData> pre_release_metadata, post_release_metadata;
|
||||
std::vector<LiveFileMetaData> pre_release_metadata;
|
||||
std::vector<LiveFileMetaData> post_release_metadata;
|
||||
db_->GetLiveFilesMetaData(&pre_release_metadata);
|
||||
// just need to bump seqnum so ReleaseSnapshot knows the newest key in the SST
|
||||
// files does not need to be preserved in case of a future snapshot.
|
||||
@@ -4767,7 +5161,8 @@ TEST_F(DBCompactionTest, NoCompactBottomLevelFilesWithDeletions) {
|
||||
ASSERT_OK(dbfull()->TEST_CompactRange(0, nullptr, nullptr, nullptr));
|
||||
ASSERT_EQ(kNumLevelFiles, NumTableFilesAtLevel(1));
|
||||
|
||||
std::vector<LiveFileMetaData> pre_release_metadata, post_release_metadata;
|
||||
std::vector<LiveFileMetaData> pre_release_metadata;
|
||||
std::vector<LiveFileMetaData> post_release_metadata;
|
||||
db_->GetLiveFilesMetaData(&pre_release_metadata);
|
||||
// just need to bump seqnum so ReleaseSnapshot knows the newest key in the SST
|
||||
// files does not need to be preserved in case of a future snapshot.
|
||||
@@ -5235,10 +5630,10 @@ TEST_F(DBCompactionTest, LevelTtlCascadingCompactions) {
|
||||
*max_open_files = 2;
|
||||
});
|
||||
// In the case where all files are opened and doing DB restart
|
||||
// forcing the oldest ancester time in manifest file to be 0 to
|
||||
// forcing the oldest ancestor time in manifest file to be 0 to
|
||||
// simulate the case of reading from an old version.
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionEdit::EncodeTo:VarintOldestAncesterTime", [&](void* arg) {
|
||||
"VersionEdit::EncodeTo:VarintOldestAncestorTime", [&](void* arg) {
|
||||
if (if_restart && if_open_all_files) {
|
||||
std::string* encoded_field = static_cast<std::string*>(arg);
|
||||
*encoded_field = "";
|
||||
@@ -5275,7 +5670,7 @@ TEST_F(DBCompactionTest, LevelTtlCascadingCompactions) {
|
||||
std::vector<std::vector<FileMetaData>> level_to_files;
|
||||
dbfull()->TEST_GetFilesMetaData(dbfull()->DefaultColumnFamily(),
|
||||
&level_to_files);
|
||||
uint64_t oldest_time = level_to_files[0][0].oldest_ancester_time;
|
||||
uint64_t oldest_time = level_to_files[0][0].oldest_ancestor_time;
|
||||
// Add 1 hour and do another flush.
|
||||
env_->MockSleepForSeconds(1 * 60 * 60);
|
||||
for (int i = 101; i <= 200; ++i) {
|
||||
@@ -5343,7 +5738,7 @@ TEST_F(DBCompactionTest, LevelTtlCascadingCompactions) {
|
||||
|
||||
dbfull()->TEST_GetFilesMetaData(dbfull()->DefaultColumnFamily(),
|
||||
&level_to_files);
|
||||
ASSERT_EQ(oldest_time, level_to_files[6][0].oldest_ancester_time);
|
||||
ASSERT_EQ(oldest_time, level_to_files[6][0].oldest_ancestor_time);
|
||||
|
||||
env_->MockSleepForSeconds(25 * 60 * 60);
|
||||
ASSERT_OK(Put(Key(2), "1"));
|
||||
@@ -5371,6 +5766,7 @@ TEST_F(DBCompactionTest, LevelPeriodicCompaction) {
|
||||
for (bool if_open_all_files : {false, true}) {
|
||||
Options options = CurrentOptions();
|
||||
options.periodic_compaction_seconds = 48 * 60 * 60; // 2 days
|
||||
options.periodic_compaction_phase_recovery_percent = 0;
|
||||
if (if_open_all_files) {
|
||||
options.max_open_files = -1; // needed for ttl compaction
|
||||
} else {
|
||||
@@ -5466,6 +5862,169 @@ TEST_F(DBCompactionTest, LevelPeriodicCompaction) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, PeriodicCompactionPhaseTriggerTime) {
|
||||
// Unit-tests PeriodicCompactionPhaser::TriggerTime, the core of
|
||||
// preferred-phase periodic compaction (see
|
||||
// DBOptions::periodic_compaction_phase_recovery_percent).
|
||||
using Params = PeriodicCompactionPhaseParams;
|
||||
const uint64_t kN = 1000; // periodic_compaction_seconds
|
||||
|
||||
// The trigger-time helper treats seed_hash as an opaque value; use a fixed
|
||||
// constant and derive the resulting preferred-phase target the same way
|
||||
// production does (FastRange64 of the seed over the interval).
|
||||
const uint64_t kSeed = 0x9e3779b97f4a7c15ULL;
|
||||
const uint64_t target = FastRange64(kSeed, kN);
|
||||
ASSERT_LT(target, kN);
|
||||
|
||||
// recovery_percent == 0 => classic behavior: trigger at file_mod + N.
|
||||
{
|
||||
Params p;
|
||||
p.seed_hash = kSeed;
|
||||
p.recovery_percent = 0;
|
||||
for (uint64_t fm : {uint64_t{1}, uint64_t{7}, uint64_t{12345}}) {
|
||||
ASSERT_EQ(PeriodicCompactionPhaser::TriggerTime(fm, kN, p), fm + kN);
|
||||
}
|
||||
}
|
||||
|
||||
// Steady state (anchor in the distant past): trigger is pulled earlier by
|
||||
// recovery_percent% of the phase offset, and never later than the deadline.
|
||||
{
|
||||
Params p;
|
||||
p.seed_hash = kSeed;
|
||||
p.recovery_percent = 50;
|
||||
p.anchor_time = 0;
|
||||
const uint64_t fm = 500;
|
||||
const uint64_t natural = fm + kN;
|
||||
const uint64_t offset = (fm % kN + kN - target) % kN;
|
||||
const uint64_t expected = natural - offset * 50 / 100;
|
||||
ASSERT_EQ(PeriodicCompactionPhaser::TriggerTime(fm, kN, p), expected);
|
||||
ASSERT_LE(expected, natural);
|
||||
}
|
||||
|
||||
// Geometric convergence: repeatedly re-stamp the file at its trigger time and
|
||||
// confirm the phase error is non-increasing and converges to (near) zero.
|
||||
for (int recovery_percent : {50, 100}) {
|
||||
Params p;
|
||||
p.seed_hash = kSeed;
|
||||
p.recovery_percent = recovery_percent;
|
||||
p.anchor_time = 0;
|
||||
uint64_t fm = 123;
|
||||
uint64_t prev_error = kN;
|
||||
for (int cycle = 0; cycle < 64; ++cycle) {
|
||||
const uint64_t trig = PeriodicCompactionPhaser::TriggerTime(fm, kN, p);
|
||||
ASSERT_LE(trig, fm + kN); // never later than the deadline
|
||||
const uint64_t error = (trig % kN + kN - target) % kN;
|
||||
ASSERT_LE(error, prev_error);
|
||||
prev_error = error;
|
||||
fm = trig;
|
||||
}
|
||||
// rp=100 pulls the whole gap so it reaches the phase exactly; smaller rates
|
||||
// decay geometrically and leave a tiny integer-rounding residue.
|
||||
if (recovery_percent == 100) {
|
||||
ASSERT_EQ(prev_error, uint64_t{0});
|
||||
} else {
|
||||
ASSERT_LE(prev_error, uint64_t{1});
|
||||
}
|
||||
}
|
||||
|
||||
// Already past the hard deadline when phasing took effect (e.g. the DB was
|
||||
// down, or periodic_compaction_seconds was turned down): instead of firing
|
||||
// the whole cohort immediately, spread it over the first quarter-period after
|
||||
// the anchor on an absolute (epoch-aligned) grid of period N/4.
|
||||
{
|
||||
Params p;
|
||||
p.seed_hash = kSeed;
|
||||
p.recovery_percent = 50;
|
||||
p.anchor_time = 200;
|
||||
const uint64_t n = 100;
|
||||
const uint64_t grid = n / 4; // 25
|
||||
const uint64_t phase = FastRange64(kSeed, grid);
|
||||
// natural deadline (10 + 100 = 110) is before the anchor (200).
|
||||
const uint64_t expected = 200 + (phase + grid - 200 % grid) % grid;
|
||||
const uint64_t trig = PeriodicCompactionPhaser::TriggerTime(10, n, p);
|
||||
ASSERT_EQ(trig, expected);
|
||||
ASSERT_GE(trig, uint64_t{200}); // never before the anchor
|
||||
ASSERT_LT(trig, 200 + grid); // within N/4 of the anchor
|
||||
}
|
||||
|
||||
// Past due against the recovery target but not yet at the deadline => spread
|
||||
// across [anchor, deadline] using the phase; never later than the deadline.
|
||||
{
|
||||
const uint64_t n = 100;
|
||||
const uint64_t small_target = FastRange64(kSeed, n);
|
||||
const uint64_t fm = small_target + 50 + 3 * n; // phase offset == 50
|
||||
const uint64_t natural = fm + n;
|
||||
const uint64_t anchor =
|
||||
natural - 25; // recovery target (natural-50) < anchor
|
||||
Params p;
|
||||
p.seed_hash = kSeed;
|
||||
p.recovery_percent = 100;
|
||||
p.anchor_time = anchor;
|
||||
ASSERT_EQ((fm % n + n - small_target) % n, uint64_t{50});
|
||||
const uint64_t expected = anchor + FastRange64(kSeed, natural - anchor);
|
||||
const uint64_t trig = PeriodicCompactionPhaser::TriggerTime(fm, n, p);
|
||||
ASSERT_EQ(trig, expected);
|
||||
ASSERT_GE(trig, anchor);
|
||||
ASSERT_LT(trig, natural);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, PeriodicCompactionPhaseCfSpread) {
|
||||
// Unit-tests PeriodicCompactionPhaser::CfPhaseSeedHash: a DB's column
|
||||
// families are spread quasi-uniformly around the DB base phase via the
|
||||
// golden-ratio recurrence, for any base and any number of CFs.
|
||||
using Phaser = PeriodicCompactionPhaser;
|
||||
|
||||
// cf_id 0 leaves the base phase unchanged; distinct cf_ids -> distinct
|
||||
// phases.
|
||||
for (uint64_t base : {uint64_t{0}, uint64_t{0x9e3779b97f4a7c15ULL},
|
||||
uint64_t{12345678901234567ULL}, ~uint64_t{0}}) {
|
||||
ASSERT_EQ(Phaser::CfPhaseSeedHash(base, 0), base);
|
||||
std::vector<uint64_t> hs;
|
||||
for (uint32_t id = 0; id < 64; ++id) {
|
||||
hs.push_back(Phaser::CfPhaseSeedHash(base, id));
|
||||
}
|
||||
std::sort(hs.begin(), hs.end());
|
||||
for (size_t i = 1; i < hs.size(); ++i) {
|
||||
ASSERT_NE(hs[i], hs[i - 1]) << "duplicate CF phase seed at base=" << base;
|
||||
}
|
||||
}
|
||||
|
||||
// Low-discrepancy: for N CFs the phases over an interval are well spread --
|
||||
// no gap larger than ~2x the average (a hash would allow chance clustering).
|
||||
for (uint64_t base : {uint64_t{0}, uint64_t{0xdeadbeefcafef00dULL}}) {
|
||||
for (uint32_t N : {uint32_t{3}, uint32_t{8}, uint32_t{16}, uint32_t{40}}) {
|
||||
const uint64_t n = 100000;
|
||||
std::vector<uint64_t> phases;
|
||||
for (uint32_t id = 0; id < N; ++id) {
|
||||
phases.push_back(FastRange64(Phaser::CfPhaseSeedHash(base, id), n));
|
||||
}
|
||||
std::sort(phases.begin(), phases.end());
|
||||
uint64_t max_gap = n - phases.back() + phases.front(); // wrap-around gap
|
||||
for (size_t i = 1; i < phases.size(); ++i) {
|
||||
max_gap = std::max(max_gap, phases[i] - phases[i - 1]);
|
||||
}
|
||||
ASSERT_LE(max_gap, 2 * (n / N))
|
||||
<< "base=" << base << " N=" << N << " max_gap=" << max_gap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, PeriodicCompactionPhaseOptions) {
|
||||
// Verifies periodic_compaction_phase_recovery_percent is plumbed and
|
||||
// dynamically settable.
|
||||
Options options = CurrentOptions();
|
||||
options.periodic_compaction_phase_recovery_percent = 25;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_EQ(dbfull()->GetDBOptions().periodic_compaction_phase_recovery_percent,
|
||||
25);
|
||||
|
||||
ASSERT_OK(dbfull()->SetDBOptions(
|
||||
{{"periodic_compaction_phase_recovery_percent", "0"}}));
|
||||
ASSERT_EQ(dbfull()->GetDBOptions().periodic_compaction_phase_recovery_percent,
|
||||
0);
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, LevelPeriodicCompactionOffpeak) {
|
||||
// This test simply checks if offpeak adjustment works in Leveled
|
||||
// Compactions. For testing offpeak periodic compactions in various
|
||||
@@ -5483,6 +6042,7 @@ TEST_F(DBCompactionTest, LevelPeriodicCompactionOffpeak) {
|
||||
Options options = CurrentOptions();
|
||||
options.ttl = 0;
|
||||
options.periodic_compaction_seconds = 5 * kSecondsPerDay; // 5 days
|
||||
options.periodic_compaction_phase_recovery_percent = 0;
|
||||
// In the case where all files are opened and doing DB restart
|
||||
// forcing the file creation time in manifest file to be 0 to
|
||||
// simulate the case of reading from an old version.
|
||||
@@ -5601,7 +6161,7 @@ TEST_F(DBCompactionTest, LevelPeriodicCompactionWithOldDB) {
|
||||
props->file_creation_time = 0;
|
||||
}
|
||||
if (set_creation_time_to_zero) {
|
||||
props->creation_time = 0;
|
||||
props->oldest_ancestor_time = 0;
|
||||
}
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
@@ -5630,6 +6190,7 @@ TEST_F(DBCompactionTest, LevelPeriodicCompactionWithOldDB) {
|
||||
// Forward the clock by 2 days.
|
||||
env_->MockSleepForSeconds(2 * 24 * 60 * 60);
|
||||
options.periodic_compaction_seconds = 1 * 24 * 60 * 60; // 1 day
|
||||
options.periodic_compaction_phase_recovery_percent = 0;
|
||||
|
||||
Reopen(options);
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
@@ -5648,6 +6209,7 @@ TEST_F(DBCompactionTest, LevelPeriodicAndTtlCompaction) {
|
||||
Options options = CurrentOptions();
|
||||
options.ttl = 10 * 60 * 60; // 10 hours
|
||||
options.periodic_compaction_seconds = 48 * 60 * 60; // 2 days
|
||||
options.periodic_compaction_phase_recovery_percent = 0;
|
||||
options.max_open_files = -1; // needed for both periodic and ttl compactions
|
||||
env_->SetMockSleep();
|
||||
options.env = env_;
|
||||
@@ -5730,6 +6292,7 @@ TEST_F(DBCompactionTest, LevelTtlBooster) {
|
||||
Options options = CurrentOptions();
|
||||
options.ttl = 10 * 60 * 60; // 10 hours
|
||||
options.periodic_compaction_seconds = 480 * 60 * 60; // very long
|
||||
options.periodic_compaction_phase_recovery_percent = 0;
|
||||
options.level0_file_num_compaction_trigger = 2;
|
||||
options.max_bytes_for_level_base = 5 * uint64_t{kNumKeysPerFile * kValueSize};
|
||||
options.max_open_files = -1; // needed for both periodic and ttl compactions
|
||||
@@ -6149,7 +6712,8 @@ TEST_F(DBCompactionTest, CompactRangeFlushOverlappingMemtable) {
|
||||
CompactRangeOptions compact_range_opts;
|
||||
ASSERT_OK(db_->CompactRange(compact_range_opts, begin_ptr, end_ptr));
|
||||
|
||||
uint64_t get_prop_tmp, num_memtable_entries = 0;
|
||||
uint64_t get_prop_tmp;
|
||||
uint64_t num_memtable_entries = 0;
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumEntriesImmMemTables,
|
||||
&get_prop_tmp));
|
||||
num_memtable_entries += get_prop_tmp;
|
||||
@@ -7918,7 +8482,8 @@ TEST_F(DBCompactionTest, ManualCompactionBottomLevelOptimized) {
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, ManualCompactionMax) {
|
||||
uint64_t l1_avg_size = 0, l2_avg_size = 0;
|
||||
uint64_t l1_avg_size = 0;
|
||||
uint64_t l2_avg_size = 0;
|
||||
auto generate_sst_func = [&]() {
|
||||
Random rnd(301);
|
||||
for (auto i = 0; i < 100; i++) {
|
||||
@@ -9106,7 +9671,8 @@ TEST_P(DBCompactionTestL0FilesMisorderCorruptionWithParam,
|
||||
// `start` and `end` is carefully chosen so that compact range:
|
||||
// (1) doesn't overlap with memtable therefore the memtable won't be flushed
|
||||
// (2) should target at compacting s0 with s1 and s2
|
||||
Slice start("k3"), end("k5");
|
||||
Slice start("k3");
|
||||
Slice end("k5");
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), &start, &end));
|
||||
// After compaction, we have LSM tree:
|
||||
//
|
||||
@@ -10621,8 +11187,12 @@ TEST_F(DBCompactionTest, FIFOMultiTierTemperatureAging) {
|
||||
env_->SetMockSleep();
|
||||
|
||||
// Track all temperature file creations
|
||||
int total_hot = 0, total_warm = 0, total_cool = 0, total_cold = 0,
|
||||
total_ice = 0, total_unknown = 0;
|
||||
int total_hot = 0;
|
||||
int total_warm = 0;
|
||||
int total_cool = 0;
|
||||
int total_cold = 0;
|
||||
int total_ice = 0;
|
||||
int total_unknown = 0;
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"NewWritableFile::FileOptions.temperature", [&](void* arg) {
|
||||
Temperature temperature = *(static_cast<Temperature*>(arg));
|
||||
@@ -11458,7 +12028,8 @@ TEST_F(DBCompactionTest, DrainUnnecessaryLevelsAfterMultiplierChanged) {
|
||||
// necessary, unnecessary levels should to be drained.
|
||||
const int kBaseLevelBytes = 256 << 10; // 256KB
|
||||
const int kFileBytes = 64 << 10; // 64KB
|
||||
const int kInitMultiplier = 2, kChangedMultiplier = 10;
|
||||
const int kInitMultiplier = 2;
|
||||
const int kChangedMultiplier = 10;
|
||||
const int kNumFiles = 32;
|
||||
const int kNumLevels = 5;
|
||||
const int kValueBytes = 1 << 10; // 1KB
|
||||
@@ -12134,7 +12705,7 @@ TEST_F(DBCompactionTest, RecordNewestKeyTimeForTtlCompaction) {
|
||||
ASSERT_EQ(newest_key_time, file_metadatas[idx]
|
||||
->fd.pinned_reader.Get()
|
||||
->GetTableProperties()
|
||||
->creation_time);
|
||||
->oldest_ancestor_time);
|
||||
}
|
||||
// The delta between the first and last newest_key_times is 15s
|
||||
uint64_t last_newest_key_time = prev_newest_key_time;
|
||||
@@ -12153,14 +12724,14 @@ TEST_F(DBCompactionTest, RecordNewestKeyTimeForTtlCompaction) {
|
||||
->GetTableProperties()
|
||||
->newest_key_time,
|
||||
first_newest_key_time);
|
||||
// Contrast newest_key_time with creation_time, which records the oldest
|
||||
// ancestor time (15s older than newest_key_time)
|
||||
// Contrast newest_key_time with oldest_ancestor_time (15s older than
|
||||
// newest_key_time)
|
||||
ASSERT_EQ(file_metadatas[0]
|
||||
->fd.pinned_reader.Get()
|
||||
->GetTableProperties()
|
||||
->creation_time,
|
||||
->oldest_ancestor_time,
|
||||
last_newest_key_time);
|
||||
ASSERT_EQ(file_metadatas[0]->oldest_ancester_time, last_newest_key_time);
|
||||
ASSERT_EQ(file_metadatas[0]->oldest_ancestor_time, last_newest_key_time);
|
||||
|
||||
// Make sure TTL of 5s causes compaction
|
||||
env_->MockSleepForSeconds(6);
|
||||
@@ -12279,6 +12850,7 @@ TEST_F(DBCompactionTest, PeriodicTask) {
|
||||
options.statistics = CreateDBStatistics();
|
||||
int kPeriodicCompactionSeconds = 7 * 24 * 60 * 60; // 1 week
|
||||
options.periodic_compaction_seconds = kPeriodicCompactionSeconds;
|
||||
options.periodic_compaction_phase_recovery_percent = 0;
|
||||
options.num_levels = 50;
|
||||
auto listener = std::make_shared<PeriodicCompactionListener>();
|
||||
options.listeners.push_back(listener);
|
||||
|
||||
+223
-2
@@ -1357,7 +1357,8 @@ TEST_F(DBTest2, DuplicateSnapshot) {
|
||||
options = CurrentOptions(options);
|
||||
std::vector<const Snapshot*> snapshots;
|
||||
DBImpl* dbi = dbfull();
|
||||
SequenceNumber oldest_ww_snap, first_ww_snap;
|
||||
SequenceNumber oldest_ww_snap;
|
||||
SequenceNumber first_ww_snap;
|
||||
|
||||
ASSERT_OK(Put("k", "v")); // inc seq
|
||||
snapshots.push_back(db_->GetSnapshot());
|
||||
@@ -2380,10 +2381,12 @@ TEST_F(DBTest2, PausingManualCompaction1) {
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
std::vector<std::string> files_before_compact, files_after_compact;
|
||||
std::vector<std::string> files_before_compact;
|
||||
std::vector<std::string> files_after_compact;
|
||||
// Remember file name before compaction is triggered
|
||||
std::vector<LiveFileMetaData> files_meta;
|
||||
dbfull()->GetLiveFilesMetaData(&files_meta);
|
||||
files_before_compact.reserve(files_meta.size());
|
||||
for (const auto& file : files_meta) {
|
||||
files_before_compact.push_back(file.name);
|
||||
}
|
||||
@@ -2399,6 +2402,7 @@ TEST_F(DBTest2, PausingManualCompaction1) {
|
||||
// Get file names after compaction is stopped
|
||||
files_meta.clear();
|
||||
dbfull()->GetLiveFilesMetaData(&files_meta);
|
||||
files_after_compact.reserve(files_meta.size());
|
||||
for (const auto& file : files_meta) {
|
||||
files_after_compact.push_back(file.name);
|
||||
}
|
||||
@@ -3513,6 +3517,40 @@ TEST_F(DBTest2, LiveFilesOmitObsoleteFiles) {
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
}
|
||||
|
||||
TEST_F(DBTest2, LiveFilesStorageInfoWithConcurrentSetOptions) {
|
||||
// SetOptions() writes a new OPTIONS file and deletes all but the newest
|
||||
// two. Without file deletions disabled by the caller, the OPTIONS file
|
||||
// chosen by GetLiveFilesStorageInfo must still survive until its size is
|
||||
// read.
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::GetLiveFilesStorageInfo:BeforeOptionsFileSize",
|
||||
[&](void* /*arg*/) {
|
||||
ASSERT_OK(db_->SetOptions({{"disable_auto_compactions", "true"}}));
|
||||
ASSERT_OK(db_->SetOptions({{"disable_auto_compactions", "false"}}));
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
std::vector<LiveFileStorageInfo> infos;
|
||||
ASSERT_OK(
|
||||
db_->GetLiveFilesStorageInfo(LiveFilesStorageInfoOptions(), &infos));
|
||||
ASSERT_TRUE(dbfull()->IsFileDeletionsEnabled());
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_F(DBTest2, LiveFilesStorageInfoReadOnlyKeepsDeletionsEnabled) {
|
||||
ASSERT_OK(Put("key", "value"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(ReadOnlyReopen(CurrentOptions()));
|
||||
|
||||
LiveFilesStorageInfoOptions opts;
|
||||
opts.wal_size_for_flush = std::numeric_limits<uint64_t>::max();
|
||||
std::vector<LiveFileStorageInfo> infos;
|
||||
ASSERT_OK(db_->GetLiveFilesStorageInfo(opts, &infos));
|
||||
ASSERT_TRUE(dbfull()->IsFileDeletionsEnabled());
|
||||
}
|
||||
|
||||
TEST_F(DBTest2, TestNumPread) {
|
||||
Options options = CurrentOptions();
|
||||
bool prefetch_supported =
|
||||
@@ -4271,6 +4309,147 @@ TEST_F(DBTest2, TraceWithLimit) {
|
||||
ASSERT_OK(DestroyDB(dbname2, options));
|
||||
}
|
||||
|
||||
namespace {
|
||||
Status ReplayTraceFile(Env* env, const std::string& trace_filename, DB* db,
|
||||
const std::vector<ColumnFamilyHandle*>& handles,
|
||||
uint32_t num_threads,
|
||||
TraceExecutionResultHandler* res_handler = nullptr) {
|
||||
std::unique_ptr<TraceReader> trace_reader;
|
||||
Status s =
|
||||
NewFileTraceReader(env, EnvOptions(), trace_filename, &trace_reader);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
std::unique_ptr<Replayer> replayer;
|
||||
s = db->NewDefaultReplayer(handles, std::move(trace_reader), &replayer);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
s = replayer->Prepare();
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
// Count the replayed records by type in `res_handler`, if given.
|
||||
std::function<void(Status, std::unique_ptr<TraceRecordResult>&&)> res_cb;
|
||||
if (res_handler != nullptr) {
|
||||
res_cb = [res_handler](const Status& exec_s,
|
||||
std::unique_ptr<TraceRecordResult>&& res) {
|
||||
exec_s.PermitUncheckedError();
|
||||
if (res != nullptr) {
|
||||
EXPECT_OK(res->Accept(res_handler));
|
||||
}
|
||||
};
|
||||
}
|
||||
return replayer->Replay(ReplayOptions(num_threads, 1.0), res_cb);
|
||||
}
|
||||
} // anonymous namespace
|
||||
|
||||
TEST_F(DBTest2, TraceReplayDefaultColumnFamilyWithoutHandles) {
|
||||
Options options = CurrentOptions();
|
||||
std::string trace_filename = dbname_ + "/rocksdb.trace";
|
||||
std::unique_ptr<TraceWriter> trace_writer;
|
||||
ASSERT_OK(
|
||||
NewFileTraceWriter(env_, EnvOptions(), trace_filename, &trace_writer));
|
||||
ASSERT_OK(db_->StartTrace(TraceOptions(), std::move(trace_writer)));
|
||||
ASSERT_OK(Put("a", "1"));
|
||||
ASSERT_OK(Put("b", "2"));
|
||||
ASSERT_EQ("1", Get("a"));
|
||||
std::vector<std::string> values;
|
||||
for (const Status& s :
|
||||
db_->MultiGet(ReadOptions(), {Slice("a"), Slice("b")}, &values)) {
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(ReadOptions()));
|
||||
iter->Seek("a");
|
||||
iter->SeekForPrev("b");
|
||||
ASSERT_OK(iter->status());
|
||||
iter.reset();
|
||||
ASSERT_OK(db_->EndTrace());
|
||||
|
||||
std::string dbname2 = test::PerThreadDBPath(env_, "/db_replay2");
|
||||
options.create_if_missing = true;
|
||||
for (uint32_t num_threads : {1U, 2U}) {
|
||||
SCOPED_TRACE("num_threads=" + std::to_string(num_threads));
|
||||
ASSERT_OK(DestroyDB(dbname2, options));
|
||||
std::unique_ptr<DB> db2;
|
||||
ASSERT_OK(DB::Open(options, dbname2, &db2));
|
||||
// No handles: every read record on the default column family must still
|
||||
// replay.
|
||||
TraceExecutionResultHandler res_handler;
|
||||
ASSERT_OK(ReplayTraceFile(env_, trace_filename, db2.get(),
|
||||
/*handles=*/{}, num_threads, &res_handler));
|
||||
// Each record replayed and produced a result.
|
||||
ASSERT_EQ(2, res_handler.GetNumWrites());
|
||||
ASSERT_EQ(1, res_handler.GetNumGets());
|
||||
ASSERT_EQ(1, res_handler.GetNumMultiGets());
|
||||
ASSERT_EQ(2, res_handler.GetNumIterSeeks());
|
||||
std::string value;
|
||||
ASSERT_OK(db2->Get(ReadOptions(), "a", &value));
|
||||
ASSERT_EQ("1", value);
|
||||
ASSERT_OK(db2->Get(ReadOptions(), "b", &value));
|
||||
ASSERT_EQ("2", value);
|
||||
}
|
||||
ASSERT_OK(DestroyDB(dbname2, options));
|
||||
}
|
||||
|
||||
TEST_F(DBTest2, TraceReplayReturnsExecutionError) {
|
||||
Options options = CurrentOptions();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
std::string trace_filename = dbname_ + "/rocksdb.trace";
|
||||
std::unique_ptr<TraceWriter> trace_writer;
|
||||
ASSERT_OK(
|
||||
NewFileTraceWriter(env_, EnvOptions(), trace_filename, &trace_writer));
|
||||
ASSERT_OK(db_->StartTrace(TraceOptions(), std::move(trace_writer)));
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "a"));
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "b"));
|
||||
ASSERT_OK(db_->EndTrace());
|
||||
|
||||
// The replay DB has no "pikachu" column family, so both Get records fail.
|
||||
std::string dbname2 = test::PerThreadDBPath(env_, "/db_replay2");
|
||||
options.create_if_missing = true;
|
||||
ASSERT_OK(DestroyDB(dbname2, options));
|
||||
std::unique_ptr<DB> db2;
|
||||
ASSERT_OK(DB::Open(options, dbname2, &db2));
|
||||
for (uint32_t num_threads : {1U, 2U}) {
|
||||
SCOPED_TRACE("num_threads=" + std::to_string(num_threads));
|
||||
Status s = ReplayTraceFile(env_, trace_filename, db2.get(),
|
||||
{db2->DefaultColumnFamily()}, num_threads);
|
||||
ASSERT_TRUE(s.IsCorruption()) << s.ToString();
|
||||
}
|
||||
db2.reset();
|
||||
ASSERT_OK(DestroyDB(dbname2, options));
|
||||
}
|
||||
|
||||
TEST_F(DBTest2, TraceReplayReturnsNotSupportedExecutionError) {
|
||||
Options options = CurrentOptions();
|
||||
std::string trace_filename = dbname_ + "/rocksdb.trace";
|
||||
std::unique_ptr<TraceWriter> trace_writer;
|
||||
ASSERT_OK(
|
||||
NewFileTraceWriter(env_, EnvOptions(), trace_filename, &trace_writer));
|
||||
ASSERT_OK(db_->StartTrace(TraceOptions(), std::move(trace_writer)));
|
||||
ASSERT_OK(Put("a", "1"));
|
||||
ASSERT_OK(
|
||||
db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), "a", "z"));
|
||||
ASSERT_OK(db_->EndTrace());
|
||||
|
||||
// DeleteRange is not compatible with row_cache, so replaying the DeleteRange
|
||||
// write fails with NotSupported. Unlike an unsupported trace type, this must
|
||||
// not be skipped.
|
||||
std::string dbname2 = test::PerThreadDBPath(env_, "/db_replay2");
|
||||
options.create_if_missing = true;
|
||||
options.row_cache = NewLRUCache(1 << 20);
|
||||
for (uint32_t num_threads : {1U, 2U}) {
|
||||
SCOPED_TRACE("num_threads=" + std::to_string(num_threads));
|
||||
ASSERT_OK(DestroyDB(dbname2, options));
|
||||
std::unique_ptr<DB> db2;
|
||||
ASSERT_OK(DB::Open(options, dbname2, &db2));
|
||||
Status s = ReplayTraceFile(env_, trace_filename, db2.get(),
|
||||
/*handles=*/{}, num_threads);
|
||||
ASSERT_TRUE(s.IsNotSupported()) << s.ToString();
|
||||
}
|
||||
ASSERT_OK(DestroyDB(dbname2, options));
|
||||
}
|
||||
|
||||
TEST_F(DBTest2, TraceWithSampling) {
|
||||
Options options = CurrentOptions();
|
||||
ReadOptions ro;
|
||||
@@ -7651,6 +7830,48 @@ TEST_F(DBTest2, TableCacheMissDuringReadFromBlockCacheTier) {
|
||||
ASSERT_EQ(orig_num_file_opens, TestGetTickerCount(options, NO_FILE_OPENS));
|
||||
}
|
||||
|
||||
TEST_F(DBTest2, MultiGetBlockCacheTierSkipsColdSstData) {
|
||||
Options options = CurrentOptions();
|
||||
options.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics();
|
||||
// Use buffered reads to reach the non-mmap RetrieveMultipleBlocks path.
|
||||
options.allow_mmap_reads = false;
|
||||
BlockBasedTableOptions table_options;
|
||||
// Data blocks use the block cache. Index and filter blocks stay in the table
|
||||
// reader (cache_index_and_filter_blocks is false by default), so a
|
||||
// kBlockCacheTier read gets to the data block without metadata I/O.
|
||||
table_options.block_cache = NewLRUCache(1 << 20);
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put("k1", "v1"));
|
||||
ASSERT_OK(Put("k2", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Open the table reader so that kBlockCacheTier does not fail at table
|
||||
// open. Do not add the data block to the block cache.
|
||||
ReadOptions warm_ro;
|
||||
warm_ro.fill_cache = false;
|
||||
std::string value;
|
||||
ASSERT_OK(db_->Get(warm_ro, "k1", &value));
|
||||
ASSERT_EQ(value, "v1");
|
||||
// Make sure the data block is not in the block cache.
|
||||
table_options.block_cache->EraseUnRefEntries();
|
||||
|
||||
ReadOptions no_io_ro;
|
||||
no_io_ro.read_tier = kBlockCacheTier;
|
||||
|
||||
ASSERT_TRUE(db_->Get(no_io_ro, "k1", &value).IsIncomplete());
|
||||
ASSERT_TRUE(db_->Get(no_io_ro, "k2", &value).IsIncomplete());
|
||||
|
||||
Slice keys[2] = {Slice("k1"), Slice("k2")};
|
||||
PinnableSlice values[2];
|
||||
Status statuses[2];
|
||||
db_->MultiGet(no_io_ro, db_->DefaultColumnFamily(), 2, keys, values,
|
||||
statuses);
|
||||
ASSERT_TRUE(statuses[0].IsIncomplete());
|
||||
ASSERT_TRUE(statuses[1].IsIncomplete());
|
||||
}
|
||||
|
||||
TEST_F(DBTest2, GetFileChecksumsFromCurrentManifest_CRC32) {
|
||||
Options opts = CurrentOptions();
|
||||
opts.create_if_missing = true;
|
||||
|
||||
+2
-1
@@ -61,7 +61,8 @@ TEST_F(DBEtc3Test, AutoTuneManifestSize) {
|
||||
// auto-tuning phases continue to test the unrelaxed background threshold
|
||||
// even though CF manipulation itself is foreground. Phase-specific foreground
|
||||
// checks disable that piggy-backed background write.
|
||||
uint64_t prev_manifest_num = 0, cur_manifest_num = 0;
|
||||
uint64_t prev_manifest_num = 0;
|
||||
uint64_t cur_manifest_num = 0;
|
||||
std::deque<ColumnFamilyHandle*> handles;
|
||||
int counter = 5;
|
||||
auto UpdateManifestNumsFrom = [&](uint64_t before_manifest_num) {
|
||||
|
||||
+90
-8
@@ -17,6 +17,7 @@
|
||||
#include "file/file_util.h"
|
||||
#include "file/filename.h"
|
||||
#include "logging/logging.h"
|
||||
#include "options/options_parser.h"
|
||||
#include "port/port.h"
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/env.h"
|
||||
@@ -198,7 +199,8 @@ Status DBImpl::GetCurrentWalFile(std::unique_ptr<WalFile>* current_wal_file) {
|
||||
|
||||
Status DBImpl::AppendColumnFamilyDropsToManifest(
|
||||
const std::string& manifest_path, uint64_t manifest_size,
|
||||
const std::vector<uint32_t>& cf_ids) {
|
||||
const std::vector<uint32_t>& cf_ids,
|
||||
uint64_t committed_options_file_number) {
|
||||
const WriteOptions write_options;
|
||||
// Snapshot manifest_preallocation_size_ under the DB mutex per VersionSet's
|
||||
// contract (the field is mutated by SetDBOptions under this mutex).
|
||||
@@ -208,21 +210,44 @@ Status DBImpl::AppendColumnFamilyDropsToManifest(
|
||||
preallocation_size = versions_->manifest_preallocation_size_;
|
||||
}
|
||||
return versions_->AppendColumnFamilyDropsToManifest(
|
||||
manifest_path, manifest_size, cf_ids, write_options, preallocation_size);
|
||||
manifest_path, manifest_size, cf_ids, write_options, preallocation_size,
|
||||
committed_options_file_number);
|
||||
}
|
||||
|
||||
Status DBImpl::CreateOptionsFileForSubsetCheckpoint(
|
||||
const std::string& checkpoint_dir,
|
||||
const SubsetCheckpointOptionsSnapshot& options_snapshot) {
|
||||
if (options_snapshot.options_file_number == 0) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
// The snapshot's CF options are shallow copies that share live pointer
|
||||
// sub-options (e.g. RibbonFilterPolicy, whose bloom_before_level is mutated
|
||||
// in place by SetOptions). Hold options_mutex_, as WriteOptionsFile does, so
|
||||
// SetOptions cannot change them between writing the file and verifying it.
|
||||
InstrumentedMutexLock ol(&options_mutex_);
|
||||
|
||||
const std::string options_path =
|
||||
OptionsFileName(checkpoint_dir, options_snapshot.options_file_number);
|
||||
return PersistRocksDBOptions(
|
||||
WriteOptions(), options_snapshot.db_options, options_snapshot.cf_names,
|
||||
options_snapshot.cf_options, options_path, fs_.get());
|
||||
}
|
||||
|
||||
Status DBImpl::GetLiveFilesStorageInfo(
|
||||
const LiveFilesStorageInfoOptions& opts,
|
||||
std::vector<LiveFileStorageInfo>* files) {
|
||||
return GetLiveFilesStorageInfoImpl(opts, /*include_cf_ids=*/{}, files,
|
||||
/*excluded_cf_ids=*/nullptr);
|
||||
/*excluded_cf_ids=*/nullptr,
|
||||
/*options_snapshot=*/nullptr);
|
||||
}
|
||||
|
||||
Status DBImpl::GetLiveFilesStorageInfoForSubsetCheckpoint(
|
||||
const LiveFilesStorageInfoOptions& opts,
|
||||
const std::vector<uint32_t>& include_cf_ids,
|
||||
std::vector<LiveFileStorageInfo>* files,
|
||||
std::vector<uint32_t>* excluded_cf_ids) {
|
||||
std::vector<uint32_t>* excluded_cf_ids,
|
||||
SubsetCheckpointOptionsSnapshot* options_snapshot) {
|
||||
// Internal contract: caller must coalesce ids and always include the default
|
||||
// CF (it cannot be dropped, so a subset checkpoint without it would produce
|
||||
// an unopenable DB). CheckpointImpl enforces this before calling here.
|
||||
@@ -230,15 +255,17 @@ Status DBImpl::GetLiveFilesStorageInfoForSubsetCheckpoint(
|
||||
assert(std::find(include_cf_ids.begin(), include_cf_ids.end(),
|
||||
default_cf_handle_->GetID()) != include_cf_ids.end());
|
||||
assert(excluded_cf_ids != nullptr);
|
||||
assert(options_snapshot != nullptr);
|
||||
return GetLiveFilesStorageInfoImpl(opts, include_cf_ids, files,
|
||||
excluded_cf_ids);
|
||||
excluded_cf_ids, options_snapshot);
|
||||
}
|
||||
|
||||
Status DBImpl::GetLiveFilesStorageInfoImpl(
|
||||
const LiveFilesStorageInfoOptions& opts,
|
||||
const std::vector<uint32_t>& include_cf_ids,
|
||||
std::vector<LiveFileStorageInfo>* files,
|
||||
std::vector<uint32_t>* excluded_cf_ids) {
|
||||
std::vector<uint32_t>* excluded_cf_ids,
|
||||
SubsetCheckpointOptionsSnapshot* options_snapshot) {
|
||||
// To avoid returning partial results, only move results to files on success.
|
||||
assert(files);
|
||||
files->clear();
|
||||
@@ -246,6 +273,7 @@ Status DBImpl::GetLiveFilesStorageInfoImpl(
|
||||
|
||||
const bool cf_subset = !include_cf_ids.empty();
|
||||
assert(!cf_subset || excluded_cf_ids != nullptr);
|
||||
assert(!cf_subset || options_snapshot != nullptr);
|
||||
const std::unordered_set<uint32_t> include_cf_id_set(include_cf_ids.begin(),
|
||||
include_cf_ids.end());
|
||||
// Tracks which requested ids remain to be observed as live under the DB
|
||||
@@ -413,17 +441,71 @@ Status DBImpl::GetLiveFilesStorageInfoImpl(
|
||||
"Requested column family id was not live at checkpoint time");
|
||||
}
|
||||
|
||||
if (cf_subset) {
|
||||
assert(options_snapshot != nullptr);
|
||||
assert(excluded_cf_ids != nullptr);
|
||||
*options_snapshot = SubsetCheckpointOptionsSnapshot();
|
||||
if (!excluded_cf_ids->empty()) {
|
||||
options_snapshot->options_file_number = versions_->NewFileNumber();
|
||||
options_snapshot->commit_options_file_number =
|
||||
versions_->has_committed_options_file_number();
|
||||
options_snapshot->db_options =
|
||||
BuildDBOptions(immutable_db_options_, mutable_db_options_);
|
||||
ColumnFamilySet* column_family_set = versions_->GetColumnFamilySet();
|
||||
assert(column_family_set != nullptr);
|
||||
for (auto* cfd : *column_family_set) {
|
||||
assert(cfd != nullptr);
|
||||
if (!cfd->IsDropped() &&
|
||||
include_cf_id_set.find(cfd->GetID()) != include_cf_id_set.end()) {
|
||||
options_snapshot->cf_names.push_back(cfd->GetName());
|
||||
options_snapshot->cf_options.push_back(cfd->GetLatestCFOptions());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Capture some final info before releasing mutex
|
||||
const uint64_t manifest_number = versions_->manifest_file_number();
|
||||
const uint64_t manifest_size = versions_->manifest_file_size();
|
||||
uint64_t manifest_size = versions_->manifest_file_size();
|
||||
if (cf_subset && !excluded_cf_ids->empty()) {
|
||||
Status manifest_s = versions_->GetManifestAppendBoundary(&manifest_size);
|
||||
if (!manifest_s.ok()) {
|
||||
mutex_.Unlock();
|
||||
return manifest_s;
|
||||
}
|
||||
}
|
||||
const uint64_t options_number = versions_->options_file_number();
|
||||
const uint64_t options_size = versions_->options_file_size_;
|
||||
const uint64_t min_log_num = MinLogNumberToKeep();
|
||||
// Ensure consistency with manifest for track_and_verify_wals_in_manifest
|
||||
const uint64_t max_log_num = cur_wal_number_;
|
||||
|
||||
if (options_number != 0) {
|
||||
// A concurrent SetOptions() may otherwise delete this OPTIONS file before
|
||||
// its size is read below, even when the caller has not disabled deletions.
|
||||
DisableFileDeletionsWithLock().PermitUncheckedError();
|
||||
}
|
||||
mutex_.Unlock();
|
||||
|
||||
uint64_t options_size = 0;
|
||||
if (options_number != 0) {
|
||||
// File deletions must be disabled by callers while they copy this snapshot.
|
||||
// The number belongs to the MANIFEST/CF snapshot captured above, while the
|
||||
// immutable file's size can be read without holding the DB mutex.
|
||||
TEST_SYNC_POINT("DBImpl::GetLiveFilesStorageInfo:BeforeOptionsFileSize");
|
||||
Status options_s = GetEnv()->GetFileSize(
|
||||
OptionsFileName(GetName(), options_number), &options_size);
|
||||
{
|
||||
// Not EnableFileDeletions(): read-only DB implementations override it to
|
||||
// return NotSupported without undoing DisableFileDeletionsWithLock().
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
assert(disable_delete_obsolete_files_ > 0);
|
||||
--disable_delete_obsolete_files_;
|
||||
}
|
||||
if (!options_s.ok()) {
|
||||
return options_s;
|
||||
}
|
||||
}
|
||||
|
||||
std::string manifest_fname = DescriptorFileName(manifest_number);
|
||||
{ // MANIFEST
|
||||
results.emplace_back();
|
||||
|
||||
Loaded 100 of 731 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user