mirror of
https://github.com/facebook/rocksdb.git
synced 2026-11-07 11:32:34 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef3dd12e9a | ||
|
|
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 |
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"
|
||||
|
||||
@@ -206,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",
|
||||
@@ -240,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",
|
||||
@@ -414,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",
|
||||
@@ -428,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",
|
||||
@@ -443,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)
|
||||
|
||||
@@ -467,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)
|
||||
@@ -4614,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"],
|
||||
@@ -4754,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=[])
|
||||
|
||||
@@ -5458,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=[])
|
||||
|
||||
@@ -5476,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=[])
|
||||
|
||||
@@ -5686,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=[])
|
||||
|
||||
|
||||
@@ -59,6 +59,8 @@ Specifically:
|
||||
|
||||
**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.
|
||||
@@ -242,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.
|
||||
@@ -308,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 |
|
||||
@@ -322,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
|
||||
@@ -372,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
|
||||
|
||||
@@ -401,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
|
||||
|
||||
@@ -938,6 +938,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
|
||||
@@ -971,6 +972,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
|
||||
@@ -1595,6 +1597,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
|
||||
@@ -1686,6 +1689,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
|
||||
)
|
||||
|
||||
+88
@@ -1,6 +1,94 @@
|
||||
# Rocksdb Change Log
|
||||
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -826,6 +826,16 @@ unsigned char rocksdb_options_get_remote_compaction_manifest_floor(
|
||||
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);
|
||||
@@ -2047,16 +2057,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;
|
||||
@@ -2137,6 +2137,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;
|
||||
|
||||
@@ -254,6 +254,14 @@ rocksdb_options_set_remote_compaction_manifest_floor(rocksdb_options_t* opt,
|
||||
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);
|
||||
|
||||
@@ -808,14 +816,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);
|
||||
@@ -878,6 +878,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);
|
||||
|
||||
@@ -556,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);
|
||||
}
|
||||
|
||||
@@ -592,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 =
|
||||
@@ -923,6 +931,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);
|
||||
@@ -1381,6 +1395,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
+1
-1
@@ -245,7 +245,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 {
|
||||
|
||||
@@ -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,7 @@ CRASHTEST_PY=$(PYTHON) -u tools/db_crashtest.py --stress_cmd=$(DB_STRESS_CMD) --
|
||||
crash_test_with_multiops_wup_txn \
|
||||
crash_test_with_optimistic_txn \
|
||||
crash_test_with_tiered_storage \
|
||||
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 \
|
||||
@@ -36,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 \
|
||||
@@ -86,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
|
||||
|
||||
@@ -132,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)
|
||||
|
||||
|
||||
@@ -101,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)
|
||||
|
||||
+87
-23
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -32,20 +35,30 @@ class BlobFileCache {
|
||||
BlobFileCache(const BlobFileCache&) = delete;
|
||||
BlobFileCache& operator=(const 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.
|
||||
// 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 +71,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 +85,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 +103,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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -753,6 +758,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 +780,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 +821,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 +848,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 +862,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();
|
||||
}
|
||||
|
||||
|
||||
+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);
|
||||
|
||||
@@ -88,6 +88,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
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -52,7 +63,51 @@ struct BlobReadRequest {
|
||||
};
|
||||
|
||||
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
|
||||
+358
-21
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -434,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");
|
||||
@@ -636,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) {
|
||||
@@ -644,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(),
|
||||
@@ -652,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;
|
||||
@@ -664,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) {
|
||||
@@ -682,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_) {
|
||||
@@ -753,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) {
|
||||
@@ -793,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) {
|
||||
@@ -854,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();
|
||||
}
|
||||
}
|
||||
@@ -887,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;
|
||||
@@ -894,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);
|
||||
|
||||
+82
-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
|
||||
@@ -56,8 +80,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 +106,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 +172,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 +234,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.
|
||||
|
||||
@@ -40,6 +40,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 +123,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.
|
||||
@@ -1755,6 +1810,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);
|
||||
|
||||
@@ -18,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)
|
||||
@@ -53,6 +65,26 @@ class SameFileBlobReader {
|
||||
uint64_t range_offset, size_t range_length,
|
||||
BlobVerifyPolicy verify_policy,
|
||||
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;
|
||||
@@ -2623,6 +2624,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 +2699,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 +2800,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 +2941,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 +5198,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 +5233,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 +5268,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();
|
||||
@@ -7609,6 +7762,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 +7799,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 +7853,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;
|
||||
@@ -9893,6 +10102,16 @@ unsigned char rocksdb_options_get_remote_compaction_manifest_floor(
|
||||
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);
|
||||
@@ -11114,16 +11333,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;
|
||||
@@ -11204,6 +11413,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;
|
||||
|
||||
+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);
|
||||
}
|
||||
|
||||
|
||||
+116
-2
@@ -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"
|
||||
@@ -672,7 +673,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 +835,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 +1252,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) {
|
||||
@@ -1899,6 +1908,102 @@ 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,
|
||||
@@ -1939,6 +2044,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
|
||||
|
||||
@@ -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
|
||||
@@ -871,6 +872,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:
|
||||
@@ -898,6 +912,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_;
|
||||
|
||||
@@ -912,6 +929,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_;
|
||||
|
||||
@@ -774,6 +774,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 +786,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 +815,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;
|
||||
}
|
||||
@@ -3167,6 +3187,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();
|
||||
|
||||
@@ -986,9 +986,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_) {
|
||||
@@ -999,14 +999,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 {
|
||||
|
||||
@@ -438,8 +438,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
|
||||
|
||||
@@ -2160,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2555,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.
|
||||
@@ -2572,14 +2572,14 @@ Status CompactionJob::OpenCompactionOutputFile(SubcompactionState* sub_compact,
|
||||
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(
|
||||
@@ -2592,7 +2592,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;
|
||||
@@ -2634,7 +2634,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);
|
||||
@@ -3267,7 +3268,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()) {
|
||||
|
||||
@@ -508,7 +508,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
|
||||
@@ -591,6 +591,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
|
||||
@@ -640,7 +641,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;
|
||||
@@ -656,7 +657,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,
|
||||
@@ -669,7 +670,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),
|
||||
|
||||
@@ -387,7 +387,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 +1572,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 +1645,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 +1702,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 */,
|
||||
|
||||
@@ -32,7 +32,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 +73,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);
|
||||
@@ -817,10 +817,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);
|
||||
|
||||
@@ -371,7 +371,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_;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -120,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) {
|
||||
@@ -162,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();
|
||||
@@ -325,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);
|
||||
|
||||
@@ -350,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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1452,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();
|
||||
@@ -1504,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();
|
||||
|
||||
@@ -1574,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();
|
||||
|
||||
@@ -1642,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();
|
||||
|
||||
@@ -1714,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);
|
||||
@@ -2490,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));
|
||||
}
|
||||
|
||||
@@ -2509,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));
|
||||
}
|
||||
}
|
||||
@@ -3577,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);
|
||||
@@ -3619,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);
|
||||
@@ -3647,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);
|
||||
@@ -3674,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);
|
||||
@@ -3859,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);
|
||||
@@ -3906,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);
|
||||
@@ -4149,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();
|
||||
}
|
||||
@@ -4231,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();
|
||||
|
||||
@@ -5033,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;
|
||||
@@ -5169,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();
|
||||
@@ -5209,7 +5209,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);
|
||||
|
||||
// Simulate a file whose min/max timestamp was never recorded.
|
||||
ASSERT_EQ(1U, vstorage_->LevelFiles(5).size());
|
||||
@@ -5249,7 +5249,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);
|
||||
|
||||
// Simulate a file whose min/max timestamp was never recorded.
|
||||
ASSERT_EQ(1U, vstorage_->LevelFiles(5).size());
|
||||
@@ -5715,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);
|
||||
@@ -5794,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)
|
||||
|
||||
@@ -84,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) {
|
||||
@@ -272,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;
|
||||
@@ -470,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,
|
||||
@@ -602,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",
|
||||
@@ -921,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>
|
||||
@@ -1085,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_) {
|
||||
@@ -212,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_; }
|
||||
|
||||
@@ -239,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();
|
||||
}
|
||||
@@ -256,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:
|
||||
@@ -280,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};
|
||||
@@ -450,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(
|
||||
@@ -464,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);
|
||||
|
||||
@@ -478,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);
|
||||
@@ -1803,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;
|
||||
@@ -2355,6 +2452,8 @@ TEST_F(CompactionServiceTest, RemoteCompactionManifestFloorKillSwitchOff) {
|
||||
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);
|
||||
@@ -2367,8 +2466,15 @@ TEST_F(CompactionServiceTest, RemoteCompactionManifestFloorKillSwitchOff) {
|
||||
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;
|
||||
@@ -2461,6 +2567,209 @@ TEST_F(CompactionServiceTest, RemoteCompactionManifestFloorMutable) {
|
||||
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);
|
||||
@@ -3146,7 +3455,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());
|
||||
@@ -3163,9 +3472,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());
|
||||
|
||||
@@ -3670,6 +3978,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.
|
||||
|
||||
+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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+691
-8
@@ -21,7 +21,9 @@
|
||||
#include "rocksdb/merge_operator.h"
|
||||
#include "rocksdb/perf_context.h"
|
||||
#include "rocksdb/table.h"
|
||||
#include "rocksdb/utilities/checkpoint.h"
|
||||
#include "rocksdb/utilities/debug.h"
|
||||
#include "rocksdb/write_buffer_manager.h"
|
||||
#include "table/block_based/block_based_table_reader.h"
|
||||
#include "table/block_based/block_builder.h"
|
||||
#include "table/format.h"
|
||||
@@ -37,6 +39,19 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
namespace {
|
||||
|
||||
OutputMetadata NewerVersionOutputMetadata() {
|
||||
OutputMetadata output_metadata;
|
||||
output_metadata.WantNewerVersionPresent();
|
||||
return output_metadata;
|
||||
}
|
||||
|
||||
MultiGetOutputMetadata NewerVersionMultiGetOutputMetadata() {
|
||||
MultiGetOutputMetadata output_metadata;
|
||||
output_metadata.WantNewerVersionPresent();
|
||||
return output_metadata;
|
||||
}
|
||||
|
||||
class MyFlushBlockPolicy : public FlushBlockPolicy {
|
||||
public:
|
||||
explicit MyFlushBlockPolicy(const int num_keys_in_block,
|
||||
@@ -356,7 +371,53 @@ TEST_F(DBBasicTest,
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest,
|
||||
OptimizeManifestForRecoveryIgnoresOptionsFilesForNextFileNumber) {
|
||||
OptimizeManifestForRecoveryKeepsCurrentOptionsFileWithStaleOptions) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
options.avoid_unnecessary_blocking_io = false;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
const uint64_t next_before_close =
|
||||
dbfull()->GetVersionSet()->current_next_file_number();
|
||||
Close();
|
||||
|
||||
const uint64_t stale_options_number = next_before_close + 100;
|
||||
ASSERT_OK(WriteStringToFile(
|
||||
env_, "" /*data*/, dbname_ + "/" + OptionsFileName(stale_options_number),
|
||||
/*should_sync=*/true));
|
||||
ASSERT_OK(WriteStringToFile(
|
||||
env_, "" /*data*/,
|
||||
dbname_ + "/" + OptionsFileName(stale_options_number + 1),
|
||||
/*should_sync=*/true));
|
||||
|
||||
RecoveryOptimizationCounters counters;
|
||||
counters.Install();
|
||||
Reopen(options);
|
||||
counters.Uninstall();
|
||||
|
||||
const uint64_t current_options_number =
|
||||
dbfull()->GetVersionSet()->options_file_number();
|
||||
ASSERT_EQ(1, counters.next_file_number.load());
|
||||
ASSERT_GT(dbfull()->GetVersionSet()->current_next_file_number(),
|
||||
stale_options_number + 1);
|
||||
ASSERT_GT(current_options_number, 0u);
|
||||
ASSERT_OK(env_->FileExists(OptionsFileName(dbname_, current_options_number)));
|
||||
|
||||
const std::string checkpoint_dir = dbname_ + "_checkpoint";
|
||||
ASSERT_OK(DestroyDir(env_, checkpoint_dir));
|
||||
Checkpoint* checkpoint = nullptr;
|
||||
ASSERT_OK(Checkpoint::Create(db_.get(), &checkpoint));
|
||||
std::unique_ptr<Checkpoint> checkpoint_guard(checkpoint);
|
||||
ASSERT_OK(checkpoint_guard->CreateCheckpoint(checkpoint_dir));
|
||||
ASSERT_OK(DestroyDir(env_, checkpoint_dir));
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest,
|
||||
OptimizeManifestForRecoveryReservesTempOptionsFileNumbersInMemory) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
@@ -368,13 +429,11 @@ TEST_F(DBBasicTest,
|
||||
dbfull()->GetVersionSet()->current_next_file_number();
|
||||
Close();
|
||||
|
||||
const uint64_t options_number = next_before_close + 100;
|
||||
ASSERT_OK(WriteStringToFile(env_, "" /*data*/,
|
||||
dbname_ + "/" + OptionsFileName(options_number),
|
||||
/*should_sync=*/true));
|
||||
ASSERT_OK(WriteStringToFile(env_, "" /*data*/,
|
||||
TempOptionsFileName(dbname_, options_number + 1),
|
||||
/*should_sync=*/true));
|
||||
const uint64_t stale_temp_options_number = next_before_close + 100;
|
||||
ASSERT_OK(
|
||||
WriteStringToFile(env_, "" /*data*/,
|
||||
TempOptionsFileName(dbname_, stale_temp_options_number),
|
||||
/*should_sync=*/true));
|
||||
|
||||
RecoveryOptimizationCounters counters;
|
||||
counters.Install();
|
||||
@@ -382,6 +441,8 @@ TEST_F(DBBasicTest,
|
||||
counters.Uninstall();
|
||||
|
||||
ASSERT_EQ(1, counters.next_file_number.load());
|
||||
ASSERT_GT(dbfull()->GetVersionSet()->current_next_file_number(),
|
||||
stale_temp_options_number);
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
}
|
||||
|
||||
@@ -1274,6 +1335,24 @@ TEST_F(DBBasicTest, ReadOnlyDB) {
|
||||
ASSERT_OK(EnforcedReadOnlyReopen(options));
|
||||
ASSERT_EQ("v3", Get("foo"));
|
||||
ASSERT_EQ("v2", Get("bar"));
|
||||
std::string metadata_value;
|
||||
ReadOptions metadata_read_options;
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_OK(db_->GetWithMetadata(metadata_read_options, "foo", &metadata_value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("v3", metadata_value);
|
||||
ASSERT_FALSE(*output_metadata.newer_version_present);
|
||||
std::vector<Slice> metadata_keys{Slice("foo")};
|
||||
std::vector<std::string> metadata_values;
|
||||
MultiGetOutputMetadata multiget_output_metadata =
|
||||
NewerVersionMultiGetOutputMetadata();
|
||||
std::vector<Status> metadata_statuses =
|
||||
db_->MultiGetWithMetadata(metadata_read_options, metadata_keys,
|
||||
&metadata_values, &multiget_output_metadata);
|
||||
ASSERT_EQ(1, metadata_statuses.size());
|
||||
ASSERT_OK(metadata_statuses[0]);
|
||||
ASSERT_EQ("v3", metadata_values[0]);
|
||||
ASSERT_FALSE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
verify_all_iters();
|
||||
ASSERT_EQ(db_->SyncWAL().code(), Status::Code::kNotSupported);
|
||||
|
||||
@@ -1455,6 +1534,35 @@ TEST_F(DBBasicTest, CompactedDB) {
|
||||
ASSERT_EQ(DummyString(kFileSize / 2, 'j'), Get("jjj"));
|
||||
ASSERT_EQ("NOT_FOUND", Get("kkk"));
|
||||
|
||||
std::string metadata_value;
|
||||
ReadOptions metadata_read_options;
|
||||
const Snapshot* metadata_snapshot = db_->GetSnapshot();
|
||||
metadata_read_options.snapshot = metadata_snapshot;
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_OK(db_->GetWithMetadata(metadata_read_options, "aaa", &metadata_value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ(DummyString(kFileSize / 2, 'a'), metadata_value);
|
||||
ASSERT_FALSE(*output_metadata.newer_version_present);
|
||||
std::vector<Slice> metadata_keys{Slice("aaa"), Slice("ccc")};
|
||||
std::vector<std::string> metadata_values;
|
||||
MultiGetOutputMetadata multiget_output_metadata =
|
||||
NewerVersionMultiGetOutputMetadata();
|
||||
std::vector<Status> metadata_statuses =
|
||||
db_->MultiGetWithMetadata(metadata_read_options, metadata_keys,
|
||||
&metadata_values, &multiget_output_metadata);
|
||||
ASSERT_EQ(metadata_keys.size(), metadata_statuses.size());
|
||||
ASSERT_OK(metadata_statuses[0]);
|
||||
ASSERT_EQ(DummyString(kFileSize / 2, 'a'), metadata_values[0]);
|
||||
ASSERT_FALSE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
ASSERT_TRUE(metadata_statuses[1].IsNotFound());
|
||||
ASSERT_FALSE((*multiget_output_metadata.newer_version_present)[1]);
|
||||
db_->ReleaseSnapshot(metadata_snapshot);
|
||||
|
||||
ASSERT_TRUE(db_->GetWithMetadata(ReadOptions(), db_->DefaultColumnFamily(),
|
||||
"aaa", static_cast<PinnableSlice*>(nullptr),
|
||||
&output_metadata)
|
||||
.IsInvalidArgument());
|
||||
|
||||
// TODO: validate that other write ops return NotImplemented
|
||||
// (CompactedDB is missing some overrides)
|
||||
|
||||
@@ -1642,6 +1750,430 @@ TEST_P(DBBasicGetWithParam, GetSnapshot) {
|
||||
} while (ChangeOptions());
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, GetNewerVersionPresent) {
|
||||
// Snapshot reads report later point and range-delete writes without changing
|
||||
// the snapshot-visible value.
|
||||
Options options = CurrentOptions();
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "key", "v1"));
|
||||
ASSERT_OK(Put(1, "merge_key", "base"));
|
||||
ASSERT_OK(Put(1, "stable", "s1"));
|
||||
ASSERT_OK(Put(1, "range_key", "r1"));
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
|
||||
ReadOptions snapshot_read;
|
||||
snapshot_read.snapshot = snapshot;
|
||||
|
||||
std::string value;
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_OK(db_->GetWithMetadata(snapshot_read, handles_[1], "key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("v1", value);
|
||||
ASSERT_FALSE(*output_metadata.newer_version_present);
|
||||
|
||||
ASSERT_OK(Put(1, "other", "v2"));
|
||||
ASSERT_OK(Put(1, "created_after_put", "new"));
|
||||
ASSERT_OK(Merge(1, "created_after_merge", "new"));
|
||||
ASSERT_OK(
|
||||
db_->DeleteRange(WriteOptions(), handles_[1], "range_key", "range_key~"));
|
||||
ASSERT_OK(Merge(1, "merge_key", "new"));
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = true;
|
||||
ASSERT_OK(db_->GetWithMetadata(snapshot_read, handles_[1], "stable", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("s1", value);
|
||||
ASSERT_FALSE(*output_metadata.newer_version_present);
|
||||
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = true;
|
||||
ASSERT_TRUE(db_->GetWithMetadata(snapshot_read, handles_[1],
|
||||
"created_after_put", &value,
|
||||
&output_metadata)
|
||||
.IsNotFound());
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = false;
|
||||
ASSERT_TRUE(db_->GetWithMetadata(snapshot_read, handles_[1],
|
||||
"created_after_merge", &value,
|
||||
&output_metadata)
|
||||
.IsNotFound());
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = false;
|
||||
ASSERT_OK(db_->GetWithMetadata(snapshot_read, handles_[1], "range_key",
|
||||
&value, &output_metadata));
|
||||
ASSERT_EQ("r1", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = false;
|
||||
ASSERT_OK(db_->GetWithMetadata(snapshot_read, handles_[1], "merge_key",
|
||||
&value, &output_metadata));
|
||||
ASSERT_EQ("base", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
ASSERT_OK(Put(1, "key", "v2"));
|
||||
value.clear();
|
||||
ASSERT_OK(db_->GetWithMetadata(snapshot_read, handles_[1], "key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("v1", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
ASSERT_OK(Flush(1));
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = false;
|
||||
ASSERT_OK(db_->GetWithMetadata(snapshot_read, handles_[1], "key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("v1", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = false;
|
||||
ASSERT_OK(db_->GetWithMetadata(snapshot_read, handles_[1], "range_key",
|
||||
&value, &output_metadata));
|
||||
ASSERT_EQ("r1", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
ReadOptions latest_read;
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = true;
|
||||
ASSERT_OK(db_->GetWithMetadata(latest_read, handles_[1], "key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("v2", value);
|
||||
ASSERT_FALSE(*output_metadata.newer_version_present);
|
||||
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, NewerVersionPresentRequestedByOutputMetadata) {
|
||||
// Newer-version tracking is opt-in through OutputMetadata and
|
||||
// MultiGetOutputMetadata.
|
||||
Options options = CurrentOptions();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "key", "old"));
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
ASSERT_OK(Put(1, "key", "new"));
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot;
|
||||
|
||||
std::string value;
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_OK(db_->GetWithMetadata(read_options, handles_[1], "key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("old", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
std::vector<Slice> keys{"key"};
|
||||
std::vector<ColumnFamilyHandle*> cfs(keys.size(), handles_[1]);
|
||||
std::vector<std::string> values;
|
||||
MultiGetOutputMetadata multiget_output_metadata =
|
||||
NewerVersionMultiGetOutputMetadata();
|
||||
std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
read_options, cfs, keys, &values, &multiget_output_metadata);
|
||||
ASSERT_EQ(1, statuses.size());
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("old", values[0]);
|
||||
ASSERT_EQ(1, multiget_output_metadata.newer_version_present->size());
|
||||
ASSERT_TRUE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, NewerVersionPresentIgnoreRangeDeletions) {
|
||||
// ignore_range_deletions affects the returned value, not newer-version
|
||||
// metadata about covering range tombstones.
|
||||
Options options = CurrentOptions();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "range_key", "old"));
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
ASSERT_OK(
|
||||
db_->DeleteRange(WriteOptions(), handles_[1], "range_key", "range_key~"));
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot;
|
||||
read_options.ignore_range_deletions = true;
|
||||
|
||||
std::string value;
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_OK(db_->GetWithMetadata(read_options, handles_[1], "range_key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("old", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
std::vector<Slice> keys{"range_key"};
|
||||
std::vector<ColumnFamilyHandle*> cfs(keys.size(), handles_[1]);
|
||||
std::vector<std::string> values;
|
||||
MultiGetOutputMetadata multiget_output_metadata =
|
||||
NewerVersionMultiGetOutputMetadata();
|
||||
std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
read_options, cfs, keys, &values, &multiget_output_metadata);
|
||||
ASSERT_EQ(1, statuses.size());
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("old", values[0]);
|
||||
ASSERT_TRUE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
|
||||
ASSERT_OK(Flush(1));
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = false;
|
||||
ASSERT_OK(db_->GetWithMetadata(read_options, handles_[1], "range_key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("old", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
values.clear();
|
||||
multiget_output_metadata.newer_version_present->clear();
|
||||
statuses = db_->MultiGetWithMetadata(read_options, cfs, keys, &values,
|
||||
&multiget_output_metadata);
|
||||
ASSERT_EQ(1, statuses.size());
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("old", values[0]);
|
||||
ASSERT_TRUE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, NewerVersionPresentFromImmutableMemTable) {
|
||||
Options options = CurrentOptions();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "point_key", "old_point"));
|
||||
ASSERT_OK(Put(1, "range_key", "old_range"));
|
||||
ManagedSnapshot snapshot(db_.get());
|
||||
|
||||
ASSERT_OK(Put(1, "point_key", "new_point"));
|
||||
ASSERT_OK(
|
||||
db_->DeleteRange(WriteOptions(), handles_[1], "range_key", "range_key~"));
|
||||
|
||||
auto* cfd =
|
||||
static_cast_with_check<ColumnFamilyHandleImpl>(handles_[1])->cfd();
|
||||
Status resume_status;
|
||||
{
|
||||
ASSERT_OK(db_->PauseBackgroundWork());
|
||||
Defer resume_background(
|
||||
[&] { resume_status = db_->ContinueBackgroundWork(); });
|
||||
ASSERT_OK(dbfull()->TEST_SwitchMemtable(cfd));
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot.snapshot();
|
||||
|
||||
std::string value;
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_OK(db_->GetWithMetadata(read_options, handles_[1], "point_key",
|
||||
&value, &output_metadata));
|
||||
ASSERT_EQ("old_point", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = false;
|
||||
ASSERT_OK(db_->GetWithMetadata(read_options, handles_[1], "range_key",
|
||||
&value, &output_metadata));
|
||||
ASSERT_EQ("old_range", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
std::vector<Slice> keys{"point_key", "range_key"};
|
||||
std::vector<ColumnFamilyHandle*> cfs(keys.size(), handles_[1]);
|
||||
std::vector<std::string> values;
|
||||
MultiGetOutputMetadata multiget_output_metadata =
|
||||
NewerVersionMultiGetOutputMetadata();
|
||||
const std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
read_options, cfs, keys, &values, &multiget_output_metadata);
|
||||
ASSERT_EQ(2, statuses.size());
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(std::vector<std::string>({"old_point", "old_range"}), values);
|
||||
ASSERT_TRUE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
ASSERT_TRUE((*multiget_output_metadata.newer_version_present)[1]);
|
||||
}
|
||||
ASSERT_OK(resume_status);
|
||||
ASSERT_OK(Flush(1));
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, NewerVersionPresentBypassesRowCacheWhileTracking) {
|
||||
// Prime row-cache entries for the snapshot values, then verify metadata
|
||||
// reads still inspect SST sequence numbers instead of replaying those
|
||||
// entries.
|
||||
option_config_ = kRowCache;
|
||||
Options options = CurrentOptions();
|
||||
options.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "get_key", "old_get"));
|
||||
ASSERT_OK(Put(1, "multiget_key", "old_multiget"));
|
||||
ASSERT_OK(Flush(1));
|
||||
ManagedSnapshot snapshot(db_.get());
|
||||
ASSERT_OK(Put(1, "get_key", "new_get"));
|
||||
ASSERT_OK(Put(1, "multiget_key", "new_multiget"));
|
||||
ASSERT_OK(Flush(1));
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot.snapshot();
|
||||
|
||||
std::string primed_value;
|
||||
ASSERT_OK(db_->Get(read_options, handles_[1], "get_key", &primed_value));
|
||||
ASSERT_EQ("old_get", primed_value);
|
||||
std::vector<Slice> primed_keys{"multiget_key"};
|
||||
std::vector<ColumnFamilyHandle*> primed_cfs(primed_keys.size(), handles_[1]);
|
||||
std::vector<std::string> primed_values;
|
||||
const std::vector<Status> primed_statuses =
|
||||
db_->MultiGet(read_options, primed_cfs, primed_keys, &primed_values);
|
||||
ASSERT_OK(primed_statuses[0]);
|
||||
ASSERT_EQ("old_multiget", primed_values[0]);
|
||||
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
SCOPED_TRACE(i);
|
||||
std::string value;
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_OK(db_->GetWithMetadata(read_options, handles_[1], "get_key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("old_get", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
}
|
||||
|
||||
std::vector<Slice> keys{"multiget_key"};
|
||||
std::vector<ColumnFamilyHandle*> cfs(keys.size(), handles_[1]);
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
SCOPED_TRACE(i);
|
||||
std::vector<std::string> values;
|
||||
MultiGetOutputMetadata multiget_output_metadata =
|
||||
NewerVersionMultiGetOutputMetadata();
|
||||
std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
read_options, cfs, keys, &values, &multiget_output_metadata);
|
||||
ASSERT_EQ(1, statuses.size());
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("old_multiget", values[0]);
|
||||
ASSERT_TRUE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, NewerVersionPresentUsesRowCacheAfterObservation) {
|
||||
option_config_ = kRowCache;
|
||||
Options options = CurrentOptions();
|
||||
options.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "key", "old"));
|
||||
ASSERT_OK(Flush(1));
|
||||
ManagedSnapshot snapshot(db_.get());
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot.snapshot();
|
||||
std::vector<Slice> keys{"key"};
|
||||
std::vector<ColumnFamilyHandle*> cfs(keys.size(), handles_[1]);
|
||||
std::vector<std::string> values;
|
||||
std::vector<Status> statuses =
|
||||
db_->MultiGet(read_options, cfs, keys, &values);
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("old", values[0]);
|
||||
|
||||
ASSERT_OK(Put(1, "key", "new"));
|
||||
const uint64_t row_cache_hits = TestGetTickerCount(options, ROW_CACHE_HIT);
|
||||
|
||||
values.clear();
|
||||
MultiGetOutputMetadata output_metadata = NewerVersionMultiGetOutputMetadata();
|
||||
statuses = db_->MultiGetWithMetadata(read_options, cfs, keys, &values,
|
||||
&output_metadata);
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("old", values[0]);
|
||||
ASSERT_TRUE((*output_metadata.newer_version_present)[0]);
|
||||
ASSERT_EQ(row_cache_hits + 1, TestGetTickerCount(options, ROW_CACHE_HIT));
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, NewerVersionPresentPersistedTierNotSupported) {
|
||||
// kPersistedTier is incompatible with snapshot newer-version tracking but not
|
||||
// with untracked latest-version reads.
|
||||
Options options = CurrentOptions();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "key", "old"));
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
ASSERT_OK(Put(1, "key", "new"));
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot;
|
||||
read_options.read_tier = kPersistedTier;
|
||||
|
||||
std::string value;
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_TRUE(db_->GetWithMetadata(read_options, handles_[1], "key", &value,
|
||||
&output_metadata)
|
||||
.IsNotSupported());
|
||||
|
||||
std::vector<Slice> keys{"key"};
|
||||
std::vector<ColumnFamilyHandle*> cfs(keys.size(), handles_[1]);
|
||||
std::vector<std::string> values;
|
||||
MultiGetOutputMetadata multiget_output_metadata =
|
||||
NewerVersionMultiGetOutputMetadata();
|
||||
std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
read_options, cfs, keys, &values, &multiget_output_metadata);
|
||||
ASSERT_EQ(1, statuses.size());
|
||||
ASSERT_TRUE(statuses[0].IsNotSupported());
|
||||
ASSERT_FALSE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
|
||||
read_options.snapshot = nullptr;
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = true;
|
||||
ASSERT_OK(db_->GetWithMetadata(read_options, handles_[1], "key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("new", value);
|
||||
ASSERT_FALSE(*output_metadata.newer_version_present);
|
||||
|
||||
values.clear();
|
||||
multiget_output_metadata.newer_version_present->clear();
|
||||
statuses = db_->MultiGetWithMetadata(read_options, cfs, keys, &values,
|
||||
&multiget_output_metadata);
|
||||
ASSERT_EQ(1, statuses.size());
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("new", values[0]);
|
||||
ASSERT_FALSE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, ReadOnlyNewerVersionPresentReturnsFalse) {
|
||||
// Read-only DBs do not widen snapshot reads, so requested metadata remains
|
||||
// false.
|
||||
Options options = CurrentOptions();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "key", "value"));
|
||||
ASSERT_OK(Flush(1));
|
||||
ASSERT_OK(
|
||||
TryReopenReadOnlyWithColumnFamilies({"default", "pikachu"}, options));
|
||||
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot;
|
||||
|
||||
std::string value;
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_OK(db_->GetWithMetadata(read_options, handles_[1], "key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("value", value);
|
||||
ASSERT_FALSE(*output_metadata.newer_version_present);
|
||||
|
||||
std::vector<Slice> keys{"key"};
|
||||
std::vector<ColumnFamilyHandle*> cfs(keys.size(), handles_[1]);
|
||||
std::vector<std::string> values;
|
||||
MultiGetOutputMetadata multiget_output_metadata =
|
||||
NewerVersionMultiGetOutputMetadata();
|
||||
std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
read_options, cfs, keys, &values, &multiget_output_metadata);
|
||||
ASSERT_EQ(1, statuses.size());
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("value", values[0]);
|
||||
ASSERT_EQ(1, multiget_output_metadata.newer_version_present->size());
|
||||
ASSERT_FALSE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, CheckLock) {
|
||||
do {
|
||||
std::unique_ptr<DB> localdb;
|
||||
@@ -2234,6 +2766,151 @@ TEST_F(DBBasicTest, MultiGetSimple) {
|
||||
} while (ChangeCompactOptions());
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, MultiGetNewerVersionPresent) {
|
||||
// A mixed MultiGet batch reports newer writes independently for each key in
|
||||
// input order.
|
||||
Options options = CurrentOptions();
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "updated", "old"));
|
||||
ASSERT_OK(Put(1, "unchanged", "same"));
|
||||
ASSERT_OK(Put(1, "deleted", "old"));
|
||||
ASSERT_OK(Put(1, "merged", "old"));
|
||||
ASSERT_OK(Put(1, "range_deleted", "old"));
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
|
||||
ASSERT_OK(Put(1, "updated", "new"));
|
||||
ASSERT_OK(Put(1, "created_after_put", "new"));
|
||||
ASSERT_OK(Merge(1, "created_after_merge", "new"));
|
||||
ASSERT_OK(Delete(1, "deleted"));
|
||||
ASSERT_OK(Merge(1, "merged", "new"));
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), handles_[1], "range_deleted",
|
||||
"range_deleted~"));
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot;
|
||||
std::vector<Slice> keys({"updated", "unchanged", "deleted", "merged",
|
||||
"range_deleted", "created_after_put",
|
||||
"created_after_merge", "missing"});
|
||||
std::vector<ColumnFamilyHandle*> cfs(keys.size(), handles_[1]);
|
||||
std::vector<std::string> values;
|
||||
MultiGetOutputMetadata output_metadata = NewerVersionMultiGetOutputMetadata();
|
||||
|
||||
std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
read_options, cfs, keys, &values, &output_metadata);
|
||||
|
||||
ASSERT_EQ(keys.size(), statuses.size());
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("old", values[0]);
|
||||
ASSERT_TRUE((*output_metadata.newer_version_present)[0]);
|
||||
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ("same", values[1]);
|
||||
ASSERT_FALSE((*output_metadata.newer_version_present)[1]);
|
||||
|
||||
ASSERT_OK(statuses[2]);
|
||||
ASSERT_EQ("old", values[2]);
|
||||
ASSERT_TRUE((*output_metadata.newer_version_present)[2]);
|
||||
|
||||
ASSERT_OK(statuses[3]);
|
||||
ASSERT_EQ("old", values[3]);
|
||||
ASSERT_TRUE((*output_metadata.newer_version_present)[3]);
|
||||
|
||||
ASSERT_OK(statuses[4]);
|
||||
ASSERT_EQ("old", values[4]);
|
||||
ASSERT_TRUE((*output_metadata.newer_version_present)[4]);
|
||||
|
||||
ASSERT_TRUE(statuses[5].IsNotFound());
|
||||
ASSERT_TRUE((*output_metadata.newer_version_present)[5]);
|
||||
|
||||
ASSERT_TRUE(statuses[6].IsNotFound());
|
||||
ASSERT_TRUE((*output_metadata.newer_version_present)[6]);
|
||||
|
||||
ASSERT_TRUE(statuses[7].IsNotFound());
|
||||
ASSERT_FALSE((*output_metadata.newer_version_present)[7]);
|
||||
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, MultiGetNewerVersionPresentAcrossColumnFamilies) {
|
||||
// Unsorted mixed-CF reads keep metadata aligned with input order after each
|
||||
// CF is processed independently.
|
||||
Options options = CurrentOptions();
|
||||
CreateAndReopenWithCF({"one", "two"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "updated_one", "old_one"));
|
||||
ASSERT_OK(Put(2, "updated_two", "old_two"));
|
||||
ASSERT_OK(Put(2, "stable_two", "stable"));
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
ASSERT_OK(Put(1, "updated_one", "new_one"));
|
||||
ASSERT_OK(Put(2, "updated_two", "new_two"));
|
||||
ASSERT_OK(Flush(1));
|
||||
ASSERT_OK(Flush(2));
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot;
|
||||
std::vector<Slice> keys{"updated_two", "updated_one", "stable_two"};
|
||||
std::vector<ColumnFamilyHandle*> column_families{handles_[2], handles_[1],
|
||||
handles_[2]};
|
||||
std::vector<std::string> values;
|
||||
MultiGetOutputMetadata output_metadata = NewerVersionMultiGetOutputMetadata();
|
||||
const std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
read_options, column_families, keys, &values, &output_metadata);
|
||||
|
||||
for (const Status& status : statuses) {
|
||||
ASSERT_OK(status);
|
||||
}
|
||||
ASSERT_EQ(std::vector<std::string>({"old_two", "old_one", "stable"}), values);
|
||||
ASSERT_TRUE((*output_metadata.newer_version_present)[0]);
|
||||
ASSERT_TRUE((*output_metadata.newer_version_present)[1]);
|
||||
ASSERT_FALSE((*output_metadata.newer_version_present)[2]);
|
||||
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, MultiGetWithMetadataColumnFamiliesKeysSizeMismatch) {
|
||||
CreateAndReopenWithCF({"pikachu"}, CurrentOptions());
|
||||
|
||||
std::vector<Slice> keys({"k1", "k2"});
|
||||
std::vector<ColumnFamilyHandle*> cfs({handles_[1]});
|
||||
std::vector<std::string> values({"stale"});
|
||||
MultiGetOutputMetadata output_metadata;
|
||||
output_metadata.timestamps.emplace();
|
||||
output_metadata.newer_version_present = std::vector<uint8_t>({1});
|
||||
|
||||
std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
ReadOptions(), cfs, keys, &values, &output_metadata);
|
||||
|
||||
ASSERT_EQ(keys.size(), statuses.size());
|
||||
ASSERT_EQ(keys.size(), values.size());
|
||||
for (const Status& status : statuses) {
|
||||
ASSERT_TRUE(status.IsInvalidArgument());
|
||||
}
|
||||
ASSERT_EQ(keys.size(), output_metadata.timestamps->size());
|
||||
ASSERT_EQ(keys.size(), output_metadata.newer_version_present->size());
|
||||
ASSERT_FALSE((*output_metadata.newer_version_present)[0]);
|
||||
ASSERT_FALSE((*output_metadata.newer_version_present)[1]);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, GetWithMetadataNullValueInvalidArgument) {
|
||||
CreateAndReopenWithCF({"pikachu"}, CurrentOptions());
|
||||
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_TRUE(db_->GetWithMetadata(ReadOptions(), handles_[1], "key",
|
||||
static_cast<PinnableSlice*>(nullptr),
|
||||
&output_metadata)
|
||||
.IsInvalidArgument());
|
||||
ASSERT_FALSE(*output_metadata.newer_version_present);
|
||||
|
||||
*output_metadata.newer_version_present = true;
|
||||
ASSERT_TRUE(db_->GetWithMetadata(ReadOptions(), "key",
|
||||
static_cast<std::string*>(nullptr),
|
||||
&output_metadata)
|
||||
.IsInvalidArgument());
|
||||
ASSERT_FALSE(*output_metadata.newer_version_present);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, MultiGetEmpty) {
|
||||
do {
|
||||
CreateAndReopenWithCF({"pikachu"}, CurrentOptions());
|
||||
@@ -5091,6 +5768,9 @@ INSTANTIATE_TEST_CASE_P(BestEffortsRecoverIncompleteVersionTest,
|
||||
TEST_F(DBBasicTest, BestEffortsRecoveryTryMultipleManifests) {
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.write_buffer_manager = std::make_shared<WriteBufferManager>(
|
||||
64 << 20, nullptr /* cache */, false /* allow_stall */,
|
||||
WriteBufferFlushPolicy::kFlushLargestAcrossDBs);
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("foo", "value0"));
|
||||
ASSERT_OK(Flush());
|
||||
@@ -5112,6 +5792,9 @@ TEST_F(DBBasicTest, BestEffortsRecoveryTryMultipleManifests) {
|
||||
|
||||
Reopen(options);
|
||||
ASSERT_OK(Put("bar", "value"));
|
||||
options.write_buffer_manager->TEST_RefreshFlushInitiatorCandidate();
|
||||
ASSERT_TRUE(
|
||||
options.write_buffer_manager->TEST_ScheduleFlushOnLargestDB(nullptr));
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, RecoverWithNoCurrentFile) {
|
||||
|
||||
@@ -2542,9 +2542,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 +2578,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 {
|
||||
|
||||
@@ -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;
|
||||
@@ -133,6 +192,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) {
|
||||
|
||||
+10
-10
@@ -5350,10 +5350,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 = "";
|
||||
@@ -5390,7 +5390,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) {
|
||||
@@ -5458,7 +5458,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"));
|
||||
@@ -5881,7 +5881,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();
|
||||
@@ -12417,7 +12417,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;
|
||||
@@ -12436,14 +12436,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);
|
||||
|
||||
@@ -4271,6 +4271,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 +7792,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;
|
||||
|
||||
@@ -2982,6 +2982,34 @@ TEST_P(DBAtomicFlushTest, ManualAtomicFlush) {
|
||||
}
|
||||
}
|
||||
|
||||
// Two handles naming the same column family make the selected-cfd list longer
|
||||
// than the per-CF memtable-id list, which is keyed by CF. The wait loop must
|
||||
// not read the id list past its own length.
|
||||
TEST_P(DBAtomicFlushTest, ManualAtomicFlushDuplicateColumnFamilyHandles) {
|
||||
if (!GetParam()) {
|
||||
// Only the atomic path reaches the shared wait list; without atomic_flush
|
||||
// each handle is flushed independently.
|
||||
return;
|
||||
}
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.atomic_flush = true;
|
||||
options.write_buffer_size = (static_cast<size_t>(64) << 20);
|
||||
DestroyAndReopen(options);
|
||||
|
||||
WriteOptions wopts;
|
||||
wopts.disableWAL = true;
|
||||
ASSERT_OK(Put("key", "value", wopts));
|
||||
|
||||
FlushOptions flush_options; // wait == true
|
||||
ASSERT_OK(db_->Flush(
|
||||
flush_options, {db_->DefaultColumnFamily(), db_->DefaultColumnFamily()}));
|
||||
|
||||
auto cfh = static_cast<ColumnFamilyHandleImpl*>(db_->DefaultColumnFamily());
|
||||
ASSERT_EQ(0, cfh->cfd()->imm()->NumNotFlushed());
|
||||
ASSERT_TRUE(cfh->cfd()->mem()->IsEmpty());
|
||||
}
|
||||
|
||||
TEST_P(DBAtomicFlushTest, PrecomputeMinLogNumberToKeepNon2PC) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
|
||||
@@ -204,6 +204,19 @@ class DBFollowerTest : public DBTestBase {
|
||||
return result;
|
||||
}
|
||||
|
||||
void VerifyFollowerBatchValue(const std::string& key,
|
||||
const std::string& expected) {
|
||||
std::vector<Iterator*> iterators;
|
||||
ASSERT_OK(follower()->NewIterators(
|
||||
ReadOptions(), {follower()->DefaultColumnFamily()}, &iterators));
|
||||
ASSERT_EQ(1U, iterators.size());
|
||||
std::unique_ptr<Iterator> iterator(iterators[0]);
|
||||
iterator->Seek(key);
|
||||
ASSERT_OK(iterator->status());
|
||||
ASSERT_TRUE(iterator->Valid());
|
||||
ASSERT_EQ(expected, iterator->value());
|
||||
}
|
||||
|
||||
DB* follower() { return follower_.get(); }
|
||||
DBFollowerTestFS* follower_fs() {
|
||||
return static_cast<DBFollowerTestFS*>(follower_env_->GetFileSystem().get());
|
||||
@@ -253,6 +266,32 @@ TEST_F(DBFollowerTest, Basic) {
|
||||
CheckDirs();
|
||||
}
|
||||
|
||||
TEST_F(DBFollowerTest, NewIteratorsRefreshesAfterCatchUp) {
|
||||
ASSERT_OK(Put("key", "v1"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
SyncPoint::GetInstance()->LoadDependency({
|
||||
{"DBImplFollower::TryCatchupWithLeader:Begin1", "Leader::Start"},
|
||||
{"Leader::Done", "DBImplFollower::TryCatchupWithLeader:Begin2"},
|
||||
{"DBImplFollower::TryCatchupWithLeader:End", "Follower::WaitForCatchup"},
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ASSERT_OK(OpenAsFollower());
|
||||
VerifyFollowerBatchValue("key", "v1");
|
||||
|
||||
TEST_SYNC_POINT("Leader::Start");
|
||||
ASSERT_OK(Put("key", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
TEST_SYNC_POINT("Leader::Done");
|
||||
TEST_SYNC_POINT("Follower::WaitForCatchup");
|
||||
|
||||
VerifyFollowerBatchValue("key", "v2");
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearTrace();
|
||||
}
|
||||
|
||||
TEST_F(DBFollowerTest, Flush) {
|
||||
SyncPoint::GetInstance()->LoadDependency({
|
||||
{"DBImplFollower::TryCatchupWithLeader:Begin1", "Leader::Start"},
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include "db/blob/blob_fetcher.h"
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "db/db_impl/db_impl_metadata.h"
|
||||
#include "db/version_set.h"
|
||||
#include "logging/logging.h"
|
||||
#include "table/get_context.h"
|
||||
@@ -34,6 +35,46 @@ size_t CompactedDBImpl::FindFile(const Slice& key) {
|
||||
files_.files);
|
||||
}
|
||||
|
||||
Status CompactedDBImpl::GetWithMetadata(const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value,
|
||||
OutputMetadata* output_metadata) {
|
||||
std::string* timestamp = GetOutputTimestamp(output_metadata);
|
||||
bool* newer_version_present = GetOutputNewerVersionPresent(output_metadata);
|
||||
if (newer_version_present != nullptr) {
|
||||
*newer_version_present = false;
|
||||
}
|
||||
if (value == nullptr) {
|
||||
return Status::InvalidArgument(
|
||||
"Cannot call GetWithMetadata with a null value");
|
||||
}
|
||||
return Get(options, column_family, key, value, timestamp);
|
||||
}
|
||||
|
||||
void CompactedDBImpl::MultiGetWithMetadata(
|
||||
const ReadOptions& options, size_t num_keys,
|
||||
ColumnFamilyHandle* const* column_families, const Slice* keys,
|
||||
PinnableSlice* values, Status* statuses,
|
||||
MultiGetOutputMetadata* output_metadata, const bool sorted_input) {
|
||||
std::vector<std::string>* timestamps = GetOutputTimestamps(output_metadata);
|
||||
if (timestamps != nullptr) {
|
||||
timestamps->resize(num_keys);
|
||||
}
|
||||
std::vector<uint8_t>* newer_version_present =
|
||||
GetOutputNewerVersionPresent(output_metadata);
|
||||
if (newer_version_present != nullptr) {
|
||||
newer_version_present->assign(num_keys, false);
|
||||
}
|
||||
autovector<ColumnFamilyHandle*, MultiGetContext::MAX_BATCH_SIZE>
|
||||
stack_column_families;
|
||||
std::vector<ColumnFamilyHandle*> heap_column_families;
|
||||
ColumnFamilyHandle** mutable_column_families = MakeMutableCfHandles(
|
||||
column_families, num_keys, &stack_column_families, &heap_column_families);
|
||||
MultiGet(options, num_keys, mutable_column_families, keys, values,
|
||||
timestamps != nullptr ? timestamps->data() : nullptr, statuses,
|
||||
sorted_input);
|
||||
}
|
||||
|
||||
Status CompactedDBImpl::Init(const Options& options) {
|
||||
SuperVersionContext sv_context(/* create_superversion */ true);
|
||||
mutex_.Lock();
|
||||
|
||||
@@ -31,6 +31,12 @@ class CompactedDBImpl : public DBImpl {
|
||||
const Slice& key, PinnableSlice* value,
|
||||
std::string* timestamp);
|
||||
|
||||
using DB::GetWithMetadata;
|
||||
Status GetWithMetadata(const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value,
|
||||
OutputMetadata* output_metadata) override;
|
||||
|
||||
using DB::MultiGet;
|
||||
// Note that CompactedDBImpl::MultiGet is not the optimized version of
|
||||
// MultiGet to use.
|
||||
@@ -42,6 +48,14 @@ class CompactedDBImpl : public DBImpl {
|
||||
std::string* timestamps, Status* statuses,
|
||||
const bool sorted_input);
|
||||
|
||||
using DB::MultiGetWithMetadata;
|
||||
void MultiGetWithMetadata(const ReadOptions& options, size_t num_keys,
|
||||
ColumnFamilyHandle* const* column_families,
|
||||
const Slice* keys, PinnableSlice* values,
|
||||
Status* statuses,
|
||||
MultiGetOutputMetadata* output_metadata,
|
||||
const bool sorted_input) override;
|
||||
|
||||
using DBImpl::Put;
|
||||
Status Put(const WriteOptions& /*options*/,
|
||||
ColumnFamilyHandle* /*column_family*/, const Slice& /*key*/,
|
||||
|
||||
@@ -6,10 +6,6 @@
|
||||
|
||||
#include "util/coro_utils.h"
|
||||
|
||||
#if defined(USE_COROUTINES) && defined(WITH_COROUTINES)
|
||||
#include "util/coro_stats_util.h"
|
||||
#endif // USE_COROUTINES && WITH_COROUTINES
|
||||
|
||||
#if defined(WITHOUT_COROUTINES) || \
|
||||
(defined(USE_COROUTINES) && defined(WITH_COROUTINES))
|
||||
|
||||
@@ -18,10 +14,6 @@ namespace ROCKSDB_NAMESPACE {
|
||||
DEFINE_SYNC_AND_ASYNC(Status, CompactedDBImpl::Get)
|
||||
(const ReadOptions& _read_options, ColumnFamilyHandle*, const Slice& key,
|
||||
PinnableSlice* value, std::string* timestamp) {
|
||||
#ifdef WITH_COROUTINES
|
||||
INSTALL_COROUTINE_STATS_CONTEXT_SCOPE(
|
||||
immutable_db_options_.fs->GetReadExecutor(), immutable_db_options_.env);
|
||||
#endif
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kGet) {
|
||||
CO_RETURN Status::InvalidArgument(
|
||||
@@ -110,10 +102,6 @@ DEFINE_SYNC_AND_ASYNC(void, CompactedDBImpl::MultiGet)
|
||||
ColumnFamilyHandle** /*column_families*/, const Slice* keys,
|
||||
PinnableSlice* values, std::string* timestamps, Status* statuses,
|
||||
const bool /*sorted_input*/) {
|
||||
#ifdef WITH_COROUTINES
|
||||
INSTALL_COROUTINE_STATS_CONTEXT_SCOPE(
|
||||
immutable_db_options_.fs->GetReadExecutor(), immutable_db_options_.env);
|
||||
#endif
|
||||
assert(user_comparator_);
|
||||
Status s;
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
|
||||
+977
-326
File diff suppressed because it is too large.
Load diff
+229
-9
@@ -9,6 +9,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
@@ -51,6 +52,7 @@
|
||||
#include "db/write_controller.h"
|
||||
#include "db/write_thread.h"
|
||||
#include "logging/event_logger.h"
|
||||
#include "memtable/flush_initiator.h"
|
||||
#include "memtable/wbwi_memtable.h"
|
||||
#include "monitoring/instrumented_mutex.h"
|
||||
#include "options/db_options.h"
|
||||
@@ -290,12 +292,22 @@ class DBImpl : public DB
|
||||
const Slice& key, PinnableSlice* value,
|
||||
std::string* timestamp);
|
||||
|
||||
using DB::GetWithMetadata;
|
||||
Status GetWithMetadata(const ReadOptions& _read_options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value,
|
||||
OutputMetadata* output_metadata) override;
|
||||
|
||||
using DB::GetEntity;
|
||||
Status GetEntity(const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableWideColumns* columns) override;
|
||||
Status GetEntity(const ReadOptions& options, const Slice& key,
|
||||
PinnableAttributeGroups* result) override;
|
||||
DECLARE_SYNC_AND_ASYNC_OVERRIDE(Status, GetEntity,
|
||||
const ReadOptions& _read_options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const Slice& key,
|
||||
PinnableWideColumns* columns);
|
||||
DECLARE_SYNC_AND_ASYNC_OVERRIDE(Status, GetEntity,
|
||||
const ReadOptions& _read_options,
|
||||
const Slice& key,
|
||||
PinnableAttributeGroups* result);
|
||||
|
||||
Status GetEntityLazy(const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
@@ -332,6 +344,15 @@ class DBImpl : public DB
|
||||
std::string* timestamps, Status* statuses,
|
||||
const bool sorted_input = false);
|
||||
|
||||
using DB::MultiGetWithMetadata;
|
||||
void MultiGetWithMetadata(const ReadOptions& _read_options,
|
||||
const size_t num_keys,
|
||||
ColumnFamilyHandle* const* column_families,
|
||||
const Slice* keys, PinnableSlice* values,
|
||||
Status* statuses,
|
||||
MultiGetOutputMetadata* output_metadata,
|
||||
const bool sorted_input = false) override;
|
||||
|
||||
void MultiGetWithCallback(
|
||||
const ReadOptions& _read_options, ColumnFamilyHandle* column_family,
|
||||
ReadCallback* callback,
|
||||
@@ -419,6 +440,9 @@ class DBImpl : public DB
|
||||
Status NewIterators(const ReadOptions& _read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) override;
|
||||
Status NewIterators(const std::vector<ReadOptions>& read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) override;
|
||||
|
||||
using DB::NewMultiScan;
|
||||
std::unique_ptr<MultiScan> NewMultiScan(
|
||||
@@ -765,6 +789,7 @@ class DBImpl : public DB
|
||||
PinnableSlice* value = nullptr;
|
||||
PinnableWideColumns* columns = nullptr;
|
||||
std::string* timestamp = nullptr;
|
||||
bool* newer_version_present = nullptr;
|
||||
bool* value_found = nullptr;
|
||||
ReadCallback* callback = nullptr;
|
||||
bool* is_blob_index = nullptr;
|
||||
@@ -795,6 +820,18 @@ class DBImpl : public DB
|
||||
Cleanable* lazy_columns_pin = nullptr;
|
||||
const Version** lazy_columns_version = nullptr;
|
||||
const SameFileBlobReader** lazy_columns_same_file_reader = nullptr;
|
||||
|
||||
// Batched lazy read (MultiGetEntityLazy). When `lazy_columns_shared_sv` is
|
||||
// non-null, GetImpl reads against this externally-owned SuperVersion at
|
||||
// `lazy_columns_snapshot_seq` (the consistent per-batch sequence number)
|
||||
// instead of acquiring its own SuperVersion and deriving its own sequence
|
||||
// number, and it does NOT transfer a per-key pin -- the batch holds one
|
||||
// shared SuperVersion pin per column family. `lazy_columns_pin` must then
|
||||
// be null; `lazy_columns_version` / `lazy_columns_same_file_reader` are
|
||||
// still filled. (Single-key GetEntityLazy leaves this null and uses
|
||||
// `lazy_columns_pin`.)
|
||||
SuperVersion* lazy_columns_shared_sv = nullptr;
|
||||
SequenceNumber lazy_columns_snapshot_seq = kMaxSequenceNumber;
|
||||
};
|
||||
|
||||
DECLARE_SYNC_AND_ASYNC(Status, GetImpl, const ReadOptions& read_options,
|
||||
@@ -805,6 +842,11 @@ class DBImpl : public DB
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value, std::string* timestamp);
|
||||
|
||||
Status GetImpl(const ReadOptions& read_options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value, std::string* timestamp,
|
||||
bool* newer_version_present);
|
||||
|
||||
// Function that Get and KeyMayExist call with no_io true or false
|
||||
// Note: 'value_found' from KeyMayExist propagates here
|
||||
// This function is also called by GetMergeOperands
|
||||
@@ -827,6 +869,18 @@ class DBImpl : public DB
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
LazyWideColumns* result);
|
||||
|
||||
// Batched-read counterpart of GetEntityLazyImpl for one key of
|
||||
// MultiGetEntityLazy: performs the lazy point lookup against the batch's
|
||||
// shared `super_version` at the batch's consistent `snapshot_seq` (rather
|
||||
// than acquiring its own), and binds the entity's resolver to that Version
|
||||
// without taking a per-key pin (the batch owns one shared pin per column
|
||||
// family).
|
||||
Status GetEntityLazyForBatch(const ReadOptions& read_options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
SuperVersion* super_version,
|
||||
SequenceNumber snapshot_seq, const Slice& key,
|
||||
LazyWideColumns* result);
|
||||
|
||||
// If `snapshot` == kMaxSequenceNumber, set a recent one inside the file.
|
||||
ArenaWrappedDBIter* NewIteratorImpl(const ReadOptions& options,
|
||||
ColumnFamilyHandleImpl* cfh,
|
||||
@@ -1290,6 +1344,9 @@ class DBImpl : public DB
|
||||
|
||||
Status TEST_SwitchMemtable(ColumnFamilyData* cfd = nullptr);
|
||||
|
||||
size_t TEST_GetWBMFlushableMemUsage();
|
||||
bool TEST_IsWBMFlushInitiatorFlushable() const;
|
||||
|
||||
// Force current memtable contents to be flushed.
|
||||
Status TEST_FlushMemTable(bool wait = true, bool allow_write_stall = false,
|
||||
ColumnFamilyHandle* cfh = nullptr);
|
||||
@@ -1310,7 +1367,10 @@ class DBImpl : public DB
|
||||
// finishes. For example in CompactRange.
|
||||
Status TEST_AtomicFlushMemTables(
|
||||
const autovector<ColumnFamilyData*>& provided_candidate_cfds,
|
||||
const FlushOptions& flush_opts);
|
||||
const FlushOptions& flush_opts, bool non_blocking_write_thread = false);
|
||||
|
||||
void TEST_BeginWriteStall();
|
||||
void TEST_EndWriteStall();
|
||||
|
||||
// Wait for background threads to complete scheduled work.
|
||||
Status TEST_WaitForBackgroundWork();
|
||||
@@ -1503,6 +1563,21 @@ class DBImpl : public DB
|
||||
}
|
||||
}
|
||||
|
||||
// Returns false when the wait times out while the DB is still blocked.
|
||||
bool BlockFor(uint64_t timeout_micros) {
|
||||
MutexLock lock(&state_mutex_);
|
||||
const std::chrono::microseconds deadline(
|
||||
SystemClock::Default()->NowMicros() + timeout_micros);
|
||||
while (state_ == State::BLOCKED) {
|
||||
TEST_SYNC_POINT("WBMStallInterface::BlockDB");
|
||||
if (SystemClock::Default()->TimedWait(&state_cv_, deadline) &&
|
||||
state_ == State::BLOCKED) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Called from WriteBufferManager. This function changes the state_
|
||||
// to State::RUNNING indicating the stall is cleared and DB can proceed.
|
||||
void Signal() override {
|
||||
@@ -1523,6 +1598,24 @@ class DBImpl : public DB
|
||||
State state_;
|
||||
};
|
||||
|
||||
// Adapts DBImpl to the shared WriteBufferManager flush registry.
|
||||
class WBMFlushInitiator : public FlushInitiator {
|
||||
public:
|
||||
WBMFlushInitiator(DBImpl* db, bool atomic_flush)
|
||||
: FlushInitiator(atomic_flush), db_(db) {}
|
||||
|
||||
bool ScheduleFlush() override {
|
||||
return db_->ScheduleWriteBufferManagerFlush();
|
||||
}
|
||||
|
||||
bool TryRefreshMemoryAccounting() override {
|
||||
return db_->TryRefreshFlushableMemAccounting();
|
||||
}
|
||||
|
||||
private:
|
||||
DBImpl* const db_;
|
||||
};
|
||||
|
||||
static void TEST_ResetDbSessionIdGen();
|
||||
static std::string GenerateDbSessionId(Env* env);
|
||||
|
||||
@@ -1540,6 +1633,9 @@ class DBImpl : public DB
|
||||
// db_session_id_ is an identifier that gets reset
|
||||
// every time the DB is opened
|
||||
std::string db_session_id_;
|
||||
// Declared before versions_ so memtables cannot outlive their allocation
|
||||
// accounting target during destruction.
|
||||
std::unique_ptr<FlushInitiator> wbm_flush_initiator_;
|
||||
std::unique_ptr<VersionSet> versions_;
|
||||
// Flag to check whether we allocated and own the info log file
|
||||
bool own_info_log_;
|
||||
@@ -2579,7 +2675,7 @@ class DBImpl : public DB
|
||||
|
||||
// Begin stalling of writes when memory usage increases beyond a certain
|
||||
// threshold.
|
||||
void WriteBufferManagerStallWrites();
|
||||
Status WriteBufferManagerStallWrites(WriteContext* write_context);
|
||||
|
||||
Status ThrottleLowPriWritesIfNeeded(const WriteOptions& write_options,
|
||||
WriteBatch* my_batch);
|
||||
@@ -2607,6 +2703,16 @@ class DBImpl : public DB
|
||||
ReadOnlyMemTable* new_imm = nullptr,
|
||||
SequenceNumber last_seqno = 0);
|
||||
|
||||
// `refresh_wbm_accounting` can be false when a caller switches a batch of
|
||||
// memtables and refreshes the DB-wide accounting once after the batch.
|
||||
Status SwitchMemtableImpl(ColumnFamilyData* cfd, WriteContext* context,
|
||||
ReadOnlyMemTable* new_imm,
|
||||
SequenceNumber last_seqno,
|
||||
bool refresh_wbm_accounting);
|
||||
|
||||
// REQUIRES: mutex_ is held
|
||||
void UpdateWBMAccountingAfterMemtableSwitch(bool refresh_now);
|
||||
|
||||
// Select and output column families qualified for atomic flush in
|
||||
// `selected_cfds`. If `provided_candidate_cfds` is non-empty, it will be used
|
||||
// as candidate CFs to select qualified ones from. Otherwise, all column
|
||||
@@ -2631,6 +2737,18 @@ class DBImpl : public DB
|
||||
const autovector<ColumnFamilyData*>& provided_candidate_cfds = {},
|
||||
bool entered_write_thread = false);
|
||||
|
||||
enum class WriteThreadJoinMode { kBlocking, kNonBlocking };
|
||||
|
||||
Status FlushMemTableImpl(ColumnFamilyData* cfd, const FlushOptions& options,
|
||||
FlushReason flush_reason, bool entered_write_thread,
|
||||
WriteThreadJoinMode join_mode, bool* made_progress);
|
||||
|
||||
Status AtomicFlushMemTablesImpl(
|
||||
const FlushOptions& options, FlushReason flush_reason,
|
||||
const autovector<ColumnFamilyData*>& provided_candidate_cfds,
|
||||
bool entered_write_thread, WriteThreadJoinMode join_mode,
|
||||
bool* made_progress);
|
||||
|
||||
// REQUIRES: mutex locked and write queues drained up to the recovery flush
|
||||
// fence that is about to switch memtables.
|
||||
void MaybeSyncLastSequenceWithAllocatedForRecovery(FlushReason flush_reason);
|
||||
@@ -2717,6 +2835,18 @@ class DBImpl : public DB
|
||||
return static_cast<uint8_t*>(static_cast<void*>(this)) + type;
|
||||
}
|
||||
|
||||
// Checks DB-local write queues; manager-wide stall state would reject healthy
|
||||
// DBs. REQUIRES: mutex_ held.
|
||||
bool WouldBlockJoiningWriteThread() {
|
||||
mutex_.AssertHeld();
|
||||
return write_thread_.GetBegunCountOfOutstandingStall() != 0 ||
|
||||
(two_write_queues_ &&
|
||||
nonmem_write_thread_.GetBegunCountOfOutstandingStall() != 0);
|
||||
}
|
||||
|
||||
Status EnterWriteThreadForNonBlockingFlush(WriteThread::Writer* w,
|
||||
WriteThread::Writer* nonmem_w);
|
||||
|
||||
// REQUIRES: mutex locked and in write thread.
|
||||
void AssignAtomicFlushSeq(const autovector<ColumnFamilyData*>& cfds);
|
||||
|
||||
@@ -2726,6 +2856,31 @@ class DBImpl : public DB
|
||||
// REQUIRES: mutex locked and in write thread.
|
||||
Status HandleWriteBufferManagerFlush(WriteContext* write_context);
|
||||
|
||||
// Column families and memory eligible for a WriteBufferManager flush.
|
||||
struct FlushableCFs {
|
||||
ColumnFamilyData* largest = nullptr; // most reclaimable memtable memory
|
||||
ColumnFamilyData* oldest = nullptr; // smallest memtable creation seq
|
||||
size_t largest_mem = 0;
|
||||
size_t total_mem = 0;
|
||||
};
|
||||
|
||||
// Shared selection for bidding and flushing. REQUIRES: mutex_ held.
|
||||
FlushableCFs CollectFlushableCFs(bool include_waiting_immutable);
|
||||
|
||||
// Registers an opened read-write DB without holding mutex_.
|
||||
void MaybeRegisterFlushInitiator();
|
||||
|
||||
// Refreshes the published mutable-memory counters after memtables change.
|
||||
// REQUIRES: mutex_ held.
|
||||
void RefreshFlushableMemAccounting();
|
||||
|
||||
bool TryRefreshFlushableMemAccounting();
|
||||
|
||||
// Queues one WBM flush; false lets the coordinator try another DB.
|
||||
bool ScheduleWriteBufferManagerFlush();
|
||||
|
||||
void BackgroundCallWBMFlush();
|
||||
|
||||
// REQUIRES: mutex locked
|
||||
Status PreprocessWrite(const WriteOptions& write_options,
|
||||
WalContext* log_context, WriteContext* write_context);
|
||||
@@ -2764,7 +2919,13 @@ class DBImpl : public DB
|
||||
|
||||
// Used by WriteImpl to update bg_error_ when IO error happens, e.g., write
|
||||
// WAL, sync WAL fails, if paranoid check is enabled.
|
||||
void WALIOStatusCheck(const IOStatus& status);
|
||||
void WALIOStatusCheck(const IOStatus& status, uint64_t failed_wal_number = 0,
|
||||
SequenceNumber failed_wal_sequence = 0,
|
||||
SequenceNumber failed_wal_sequence_start = 0);
|
||||
|
||||
// Extends the inclusive WAL-number cutoff that must not receive more I/O.
|
||||
// This is lock-free because a WAL error can race with public SyncWAL().
|
||||
void ExtendWALRecoveryCutoff(uint64_t failed_wal_number);
|
||||
|
||||
// Used by WriteImpl to update bg_error_ in case of memtable insert error.
|
||||
void HandleMemTableInsertFailure(const Status& nonok_memtable_insert_status);
|
||||
@@ -2865,6 +3026,9 @@ class DBImpl : public DB
|
||||
Env::Priority thread_pri);
|
||||
void BackgroundCallFlush(Env::Priority thread_pri);
|
||||
void BackgroundCallPurge();
|
||||
// Recursively removes all children of the DB session temporary directory.
|
||||
// Best effort: errors are logged, not returned. REQUIRES: mutex_ not held.
|
||||
void CleanupSessionTmpDir();
|
||||
Status BackgroundCompaction(bool* madeProgress, JobContext* job_context,
|
||||
LogBuffer* log_buffer,
|
||||
PrepickedCompaction* prepicked_compaction,
|
||||
@@ -3130,7 +3294,9 @@ class DBImpl : public DB
|
||||
ColumnFamilyHandle** column_families,
|
||||
const Slice* keys, PinnableSlice* values,
|
||||
PinnableWideColumns* columns, std::string* timestamps,
|
||||
Status* statuses, bool sorted_input);
|
||||
Status* statuses,
|
||||
std::vector<uint8_t>* newer_version_present,
|
||||
bool sorted_input);
|
||||
|
||||
// A structure to hold the information required to process MultiGet of keys
|
||||
// belonging to one column family. For a multi column family MultiGet, there
|
||||
@@ -3345,6 +3511,16 @@ class DBImpl : public DB
|
||||
// * whenever a column family is dropped.
|
||||
InstrumentedCondVar bg_cv_;
|
||||
|
||||
// Set only while ResumeImpl performs a file-scoped WAL recovery. Protected
|
||||
// by mutex_; SwitchMemtable uses it to avoid touching the failed writer.
|
||||
bool recovering_from_wal_write_error_ = false;
|
||||
uint64_t recover_wal_through_number_ = 0;
|
||||
|
||||
// Once a file-scoped WAL error is reported, all writers through this
|
||||
// inclusive cutoff are quarantined until recovery detaches them. A
|
||||
// concurrent SyncWAL() reads this without taking the DB mutex.
|
||||
std::atomic<uint64_t> wal_recovery_cutoff_{0};
|
||||
|
||||
ColumnFamilyHandleImpl* persist_stats_cf_handle_ = nullptr;
|
||||
|
||||
bool persistent_stats_cfd_exists_ = true;
|
||||
@@ -3608,6 +3784,12 @@ class DBImpl : public DB
|
||||
// number of background memtable flush jobs, submitted to the HIGH pool
|
||||
int bg_flush_scheduled_ = 0;
|
||||
|
||||
// LOW avoids consuming a flush worker while joining the target write thread.
|
||||
static constexpr Env::Priority kWBMFlushPriority = Env::Priority::LOW;
|
||||
|
||||
// Outstanding cross-DB WBM flush jobs, drained during shutdown.
|
||||
int bg_wbm_flush_scheduled_ = 0;
|
||||
|
||||
// stores the number of flushes are currently running
|
||||
int num_running_flushes_ = 0;
|
||||
|
||||
@@ -3814,9 +3996,47 @@ class GetWithTimestampReadCallback : public ReadCallback {
|
||||
public:
|
||||
explicit GetWithTimestampReadCallback(SequenceNumber seq)
|
||||
: ReadCallback(seq) {}
|
||||
// A null result is used by MultiGet, whose per-key results live in
|
||||
// KeyContext/GetContext.
|
||||
void EnableNewerVersionTracking(SequenceNumber read_snapshot_seq,
|
||||
SequenceNumber upper_bound_seq,
|
||||
bool* single_key_result) {
|
||||
metadata_read_bounds_.emplace(
|
||||
MetadataReadBounds{read_snapshot_seq, upper_bound_seq});
|
||||
newer_version_present_ = single_key_result;
|
||||
}
|
||||
const MetadataReadBounds* GetMetadataReadBounds() const override {
|
||||
return metadata_read_bounds_.has_value() ? &*metadata_read_bounds_
|
||||
: nullptr;
|
||||
}
|
||||
bool NeedToTrackNewerVersions(
|
||||
const bool* per_key_result = nullptr) const override {
|
||||
const bool* result =
|
||||
per_key_result != nullptr ? per_key_result : newer_version_present_;
|
||||
return metadata_read_bounds_.has_value() && result != nullptr && !*result;
|
||||
}
|
||||
bool IsVisibleFullCheck(SequenceNumber seq) override {
|
||||
return seq <= max_visible_seq_;
|
||||
}
|
||||
bool IsNewerVisibleForMetadataRead(SequenceNumber seq) override {
|
||||
const MetadataReadBounds* bounds = GetMetadataReadBounds();
|
||||
return bounds != nullptr && bounds->read_snapshot_seq < seq &&
|
||||
seq <= bounds->newer_version_upper_bound_seq;
|
||||
}
|
||||
void MaybeRecordNewerVersion(SequenceNumber seq, ValueType type,
|
||||
bool* per_key_result = nullptr) override {
|
||||
bool* result =
|
||||
per_key_result != nullptr ? per_key_result : newer_version_present_;
|
||||
if (result != nullptr && !*result &&
|
||||
(IsValueType(type) || type == kTypeRangeDeletion) &&
|
||||
IsNewerVisibleForMetadataRead(seq)) {
|
||||
*result = true;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::optional<MetadataReadBounds> metadata_read_bounds_;
|
||||
bool* newer_version_present_ = nullptr;
|
||||
};
|
||||
|
||||
Options SanitizeOptions(const std::string& db, const Options& src,
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "db/blob/blob_file_partition_manager.h"
|
||||
@@ -1712,7 +1713,7 @@ void DBImpl::PrepareTrivialMoveEdit(Compaction& c, LogBuffer* log_buffer,
|
||||
c.output_level(), f->fd.GetNumber(), f->fd.GetPathId(),
|
||||
f->fd.GetFileSize(), f->smallest, f->largest, f->fd.smallest_seqno,
|
||||
f->fd.largest_seqno, f->marked_for_compaction, f->temperature,
|
||||
f->oldest_blob_file_number, f->oldest_ancester_time,
|
||||
f->oldest_blob_file_number, f->oldest_ancestor_time,
|
||||
f->file_creation_time, f->epoch_number, f->file_checksum,
|
||||
f->file_checksum_func_name, f->unique_id,
|
||||
f->compensated_range_deletion_size, f->tail_size,
|
||||
@@ -2033,7 +2034,7 @@ Status DBImpl::PauseBackgroundWork() {
|
||||
InstrumentedMutexLock guard_lock(&mutex_);
|
||||
bg_compaction_paused_++;
|
||||
while (bg_bottom_compaction_scheduled_ > 0 || bg_compaction_scheduled_ > 0 ||
|
||||
bg_flush_scheduled_ > 0) {
|
||||
bg_flush_scheduled_ > 0 || bg_wbm_flush_scheduled_ > 0) {
|
||||
bg_cv_.Wait();
|
||||
}
|
||||
bg_work_paused_++;
|
||||
@@ -2289,7 +2290,7 @@ Status DBImpl::ReFitLevel(ColumnFamilyData* cfd, int level, int target_level) {
|
||||
to_level, f->fd.GetNumber(), f->fd.GetPathId(), f->fd.GetFileSize(),
|
||||
f->smallest, f->largest, f->fd.smallest_seqno, f->fd.largest_seqno,
|
||||
f->marked_for_compaction, f->temperature, f->oldest_blob_file_number,
|
||||
f->oldest_ancester_time, f->file_creation_time, f->epoch_number,
|
||||
f->oldest_ancestor_time, f->file_creation_time, f->epoch_number,
|
||||
f->file_checksum, f->file_checksum_func_name, f->unique_id,
|
||||
f->compensated_range_deletion_size, f->tail_size,
|
||||
f->user_defined_timestamps_persisted, f->min_timestamp,
|
||||
@@ -2724,12 +2725,54 @@ void DBImpl::MaybeSyncLastSequenceWithAllocatedForRecovery(
|
||||
}
|
||||
}
|
||||
|
||||
Status DBImpl::EnterWriteThreadForNonBlockingFlush(
|
||||
WriteThread::Writer* w, WriteThread::Writer* nonmem_w) {
|
||||
mutex_.AssertHeld();
|
||||
if (!write_thread_.EnterUnbatchedNonBlocking(w, &mutex_)) {
|
||||
return Status::Incomplete(
|
||||
"Write stall in progress, unable to join the write thread to switch "
|
||||
"memtables");
|
||||
}
|
||||
if (two_write_queues_ &&
|
||||
!nonmem_write_thread_.EnterUnbatchedNonBlocking(nonmem_w, &mutex_)) {
|
||||
write_thread_.ExitUnbatched(w);
|
||||
return Status::Incomplete(
|
||||
"Write stall in progress, unable to join the write thread to switch "
|
||||
"memtables");
|
||||
}
|
||||
if (!error_handler_.IsBGWorkStopped() && !write_controller_.IsStopped() &&
|
||||
!WouldBlockJoiningWriteThread()) {
|
||||
return Status::OK();
|
||||
}
|
||||
write_thread_.ExitUnbatched(w);
|
||||
if (two_write_queues_) {
|
||||
nonmem_write_thread_.ExitUnbatched(nonmem_w);
|
||||
}
|
||||
return Status::Incomplete(
|
||||
"Write stop started while joining the write thread to switch memtables");
|
||||
}
|
||||
|
||||
Status DBImpl::FlushMemTable(ColumnFamilyData* cfd,
|
||||
const FlushOptions& flush_options,
|
||||
FlushReason flush_reason,
|
||||
bool entered_write_thread) {
|
||||
return FlushMemTableImpl(cfd, flush_options, flush_reason,
|
||||
entered_write_thread, WriteThreadJoinMode::kBlocking,
|
||||
nullptr /* made_progress */);
|
||||
}
|
||||
|
||||
Status DBImpl::FlushMemTableImpl(ColumnFamilyData* cfd,
|
||||
const FlushOptions& flush_options,
|
||||
FlushReason flush_reason,
|
||||
bool entered_write_thread,
|
||||
WriteThreadJoinMode join_mode,
|
||||
bool* made_progress) {
|
||||
if (made_progress != nullptr) {
|
||||
*made_progress = false;
|
||||
}
|
||||
// This method should not be called if atomic_flush is true.
|
||||
assert(!immutable_db_options_.atomic_flush);
|
||||
assert(!IsRecoveryFlush(flush_reason));
|
||||
if (!flush_options.wait && write_controller_.IsStopped()) {
|
||||
std::ostringstream oss;
|
||||
oss << "Writes have been stopped, thus unable to perform manual flush. "
|
||||
@@ -2763,13 +2806,45 @@ Status DBImpl::FlushMemTable(ColumnFamilyData* cfd,
|
||||
WriteThread::Writer w;
|
||||
WriteThread::Writer nonmem_w;
|
||||
if (needs_to_join_write_thread) {
|
||||
write_thread_.EnterUnbatched(&w, &mutex_);
|
||||
if (two_write_queues_) {
|
||||
nonmem_write_thread_.EnterUnbatched(&nonmem_w, &mutex_);
|
||||
if (join_mode == WriteThreadJoinMode::kNonBlocking) {
|
||||
s = EnterWriteThreadForNonBlockingFlush(&w, &nonmem_w);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
} else {
|
||||
write_thread_.EnterUnbatched(&w, &mutex_);
|
||||
if (two_write_queues_) {
|
||||
nonmem_write_thread_.EnterUnbatched(&nonmem_w, &mutex_);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (join_mode == WriteThreadJoinMode::kNonBlocking && cfd->IsDropped()) {
|
||||
TEST_SYNC_POINT("DBImpl::FlushMemTableImpl:Dropped");
|
||||
if (needs_to_join_write_thread) {
|
||||
write_thread_.ExitUnbatched(&w);
|
||||
if (two_write_queues_) {
|
||||
nonmem_write_thread_.ExitUnbatched(&nonmem_w);
|
||||
}
|
||||
}
|
||||
s.PermitUncheckedOk();
|
||||
return Status::ColumnFamilyDropped();
|
||||
}
|
||||
WaitForPendingWrites();
|
||||
|
||||
if (join_mode == WriteThreadJoinMode::kNonBlocking &&
|
||||
flush_reason == FlushReason::kWriteBufferManager &&
|
||||
cfd->imm()->IsFlushPendingOrRunning()) {
|
||||
TEST_SYNC_POINT("DBImpl::FlushMemTableImpl:AlreadyFlushing");
|
||||
if (needs_to_join_write_thread) {
|
||||
write_thread_.ExitUnbatched(&w);
|
||||
if (two_write_queues_) {
|
||||
nonmem_write_thread_.ExitUnbatched(&nonmem_w);
|
||||
}
|
||||
}
|
||||
return Status::Incomplete(
|
||||
"Column family started flushing before the WBM handoff completed");
|
||||
}
|
||||
|
||||
// Recovery may have released `mutex_` after the earlier `ResumeImpl()`
|
||||
// sync. Refresh sequence state at the actual memtable-switch fence, after
|
||||
// both write queues are drained and before `SwitchMemtable()` consumes
|
||||
@@ -2779,6 +2854,9 @@ Status DBImpl::FlushMemTable(ColumnFamilyData* cfd,
|
||||
if (!cfd->mem()->IsEmpty() || !cached_recoverable_state_empty_.load() ||
|
||||
IsRecoveryFlush(flush_reason)) {
|
||||
s = SwitchMemtable(cfd, &context);
|
||||
if (s.ok() && made_progress != nullptr) {
|
||||
*made_progress = true;
|
||||
}
|
||||
}
|
||||
const uint64_t flush_memtable_id = std::numeric_limits<uint64_t>::max();
|
||||
if (s.ok()) {
|
||||
@@ -2851,7 +2929,11 @@ Status DBImpl::FlushMemTable(ColumnFamilyData* cfd,
|
||||
req.cfd_to_max_mem_id_to_persist.begin()->first;
|
||||
bool already_queued_for_flush = loop_cfd->queued_for_flush();
|
||||
bool flush_req_enqueued = EnqueuePendingFlush(req);
|
||||
if (already_queued_for_flush || flush_req_enqueued) {
|
||||
if (flush_req_enqueued && made_progress != nullptr) {
|
||||
*made_progress = true;
|
||||
}
|
||||
if (flush_reason != FlushReason::kWriteBufferManager &&
|
||||
(already_queued_for_flush || flush_req_enqueued)) {
|
||||
loop_cfd->SetFlushSkipReschedule();
|
||||
}
|
||||
}
|
||||
@@ -2866,7 +2948,9 @@ Status DBImpl::FlushMemTable(ColumnFamilyData* cfd,
|
||||
}
|
||||
}
|
||||
|
||||
NotifyOnManualFlushScheduled({cfd}, flush_reason);
|
||||
if (flush_reason != FlushReason::kWriteBufferManager) {
|
||||
NotifyOnManualFlushScheduled({cfd}, flush_reason);
|
||||
}
|
||||
TEST_SYNC_POINT("DBImpl::FlushMemTable:AfterScheduleFlush");
|
||||
TEST_SYNC_POINT("DBImpl::FlushMemTable:BeforeWaitForBgFlush");
|
||||
if (s.ok() && flush_options.wait) {
|
||||
@@ -2895,6 +2979,20 @@ Status DBImpl::AtomicFlushMemTables(
|
||||
const FlushOptions& flush_options, FlushReason flush_reason,
|
||||
const autovector<ColumnFamilyData*>& provided_candidate_cfds,
|
||||
bool entered_write_thread) {
|
||||
return AtomicFlushMemTablesImpl(flush_options, flush_reason,
|
||||
provided_candidate_cfds, entered_write_thread,
|
||||
WriteThreadJoinMode::kBlocking,
|
||||
nullptr /* made_progress */);
|
||||
}
|
||||
|
||||
Status DBImpl::AtomicFlushMemTablesImpl(
|
||||
const FlushOptions& flush_options, FlushReason flush_reason,
|
||||
const autovector<ColumnFamilyData*>& provided_candidate_cfds,
|
||||
bool entered_write_thread, WriteThreadJoinMode join_mode,
|
||||
bool* made_progress) {
|
||||
if (made_progress != nullptr) {
|
||||
*made_progress = false;
|
||||
}
|
||||
if (!flush_options.wait && write_controller_.IsStopped()) {
|
||||
std::ostringstream oss;
|
||||
oss << "Writes have been stopped, thus unable to perform manual flush. "
|
||||
@@ -2925,32 +3023,30 @@ Status DBImpl::AtomicFlushMemTables(
|
||||
candidate_cfds = provided_candidate_cfds;
|
||||
}
|
||||
|
||||
const auto unref_generated_candidates = [&]() {
|
||||
if (!provided_candidate_cfds.empty()) {
|
||||
return;
|
||||
}
|
||||
for (auto candidate_cfd : candidate_cfds) {
|
||||
candidate_cfd->UnrefAndTryDelete();
|
||||
}
|
||||
candidate_cfds.clear();
|
||||
};
|
||||
|
||||
if (!flush_options.allow_write_stall) {
|
||||
int num_cfs_to_flush = 0;
|
||||
for (auto cfd : candidate_cfds) {
|
||||
bool flush_needed = true;
|
||||
s = WaitUntilFlushWouldNotStallWrites(cfd, &flush_needed);
|
||||
if (!s.ok()) {
|
||||
// Unref the newly generated candidate cfds (when not provided) in
|
||||
// `candidate_cfds`
|
||||
if (provided_candidate_cfds.empty()) {
|
||||
for (auto candidate_cfd : candidate_cfds) {
|
||||
candidate_cfd->UnrefAndTryDelete();
|
||||
}
|
||||
}
|
||||
unref_generated_candidates();
|
||||
return s;
|
||||
} else if (flush_needed) {
|
||||
++num_cfs_to_flush;
|
||||
}
|
||||
}
|
||||
if (0 == num_cfs_to_flush) {
|
||||
// Unref the newly generated candidate cfds (when not provided) in
|
||||
// `candidate_cfds`
|
||||
if (provided_candidate_cfds.empty()) {
|
||||
for (auto candidate_cfd : candidate_cfds) {
|
||||
candidate_cfd->UnrefAndTryDelete();
|
||||
}
|
||||
}
|
||||
unref_generated_candidates();
|
||||
return s;
|
||||
}
|
||||
}
|
||||
@@ -2965,39 +3061,54 @@ Status DBImpl::AtomicFlushMemTables(
|
||||
WriteThread::Writer w;
|
||||
WriteThread::Writer nonmem_w;
|
||||
if (needs_to_join_write_thread) {
|
||||
write_thread_.EnterUnbatched(&w, &mutex_);
|
||||
if (two_write_queues_) {
|
||||
nonmem_write_thread_.EnterUnbatched(&nonmem_w, &mutex_);
|
||||
if (join_mode == WriteThreadJoinMode::kNonBlocking) {
|
||||
s = EnterWriteThreadForNonBlockingFlush(&w, &nonmem_w);
|
||||
if (!s.ok()) {
|
||||
unref_generated_candidates();
|
||||
return s;
|
||||
}
|
||||
} else {
|
||||
write_thread_.EnterUnbatched(&w, &mutex_);
|
||||
if (two_write_queues_) {
|
||||
nonmem_write_thread_.EnterUnbatched(&nonmem_w, &mutex_);
|
||||
}
|
||||
}
|
||||
}
|
||||
WaitForPendingWrites();
|
||||
|
||||
// Keep atomic recovery flushes consistent with the single-CF path: the
|
||||
// sequence sync must happen after both write queues are drained and before
|
||||
// any recovery memtable switch reads `LastSequence()`.
|
||||
// Recovery may have released `mutex_` after the earlier `ResumeImpl()`
|
||||
// sync. Refresh sequence state at the atomic memtable-switch fence, after
|
||||
// both write queues are drained and before `SwitchMemtable()` consumes
|
||||
// `LastSequence()`.
|
||||
MaybeSyncLastSequenceWithAllocatedForRecovery(flush_reason);
|
||||
|
||||
SelectColumnFamiliesForAtomicFlush(&cfds, candidate_cfds, flush_reason);
|
||||
|
||||
// Unref the newly generated candidate cfds (when not provided) in
|
||||
// `candidate_cfds`
|
||||
if (provided_candidate_cfds.empty()) {
|
||||
for (auto candidate_cfd : candidate_cfds) {
|
||||
candidate_cfd->UnrefAndTryDelete();
|
||||
}
|
||||
}
|
||||
unref_generated_candidates();
|
||||
|
||||
const bool defer_wbm_accounting_refresh = cfds.size() > 1;
|
||||
bool switch_attempted = false;
|
||||
for (auto cfd : cfds) {
|
||||
if (cfd->mem()->IsEmpty() && cached_recoverable_state_empty_.load() &&
|
||||
!IsRecoveryFlush(flush_reason)) {
|
||||
continue;
|
||||
}
|
||||
cfd->Ref();
|
||||
s = SwitchMemtable(cfd, &context);
|
||||
switch_attempted = true;
|
||||
s = defer_wbm_accounting_refresh
|
||||
? SwitchMemtableImpl(cfd, &context, nullptr, 0,
|
||||
false /* refresh_wbm_accounting */)
|
||||
: SwitchMemtable(cfd, &context);
|
||||
cfd->UnrefAndTryDelete();
|
||||
if (!s.ok()) {
|
||||
break;
|
||||
}
|
||||
if (made_progress != nullptr) {
|
||||
*made_progress = true;
|
||||
}
|
||||
}
|
||||
if (defer_wbm_accounting_refresh && switch_attempted) {
|
||||
UpdateWBMAccountingAfterMemtableSwitch(true /* refresh_now */);
|
||||
}
|
||||
if (s.ok()) {
|
||||
AssignAtomicFlushSeq(cfds);
|
||||
@@ -3014,7 +3125,9 @@ Status DBImpl::AtomicFlushMemTables(
|
||||
}
|
||||
}
|
||||
GenerateFlushRequest(cfds, flush_reason, &flush_req);
|
||||
EnqueuePendingFlush(flush_req);
|
||||
if (EnqueuePendingFlush(flush_req) && made_progress != nullptr) {
|
||||
*made_progress = true;
|
||||
}
|
||||
MaybeScheduleFlushOrCompaction();
|
||||
}
|
||||
|
||||
@@ -3025,14 +3138,39 @@ Status DBImpl::AtomicFlushMemTables(
|
||||
}
|
||||
}
|
||||
}
|
||||
NotifyOnManualFlushScheduled(cfds, flush_reason);
|
||||
if (flush_reason != FlushReason::kWriteBufferManager) {
|
||||
NotifyOnManualFlushScheduled(cfds, flush_reason);
|
||||
}
|
||||
TEST_SYNC_POINT("DBImpl::AtomicFlushMemTables:AfterScheduleFlush");
|
||||
TEST_SYNC_POINT("DBImpl::AtomicFlushMemTables:BeforeWaitForBgFlush");
|
||||
if (s.ok() && flush_options.wait) {
|
||||
// Walk cfds and look each one up, so the ids line up with cfds by
|
||||
// construction. Iterating the map instead pairs ids with the wrong CF
|
||||
// (its order is unrelated to cfds), and a CF that dropped out of the map
|
||||
// (an idle CF de-selected during the switch loop, or a duplicate handle
|
||||
// collapsed by the map) would make the two vectors different lengths.
|
||||
autovector<const uint64_t*> flush_memtable_ids;
|
||||
for (auto& iter : flush_req.cfd_to_max_mem_id_to_persist) {
|
||||
flush_memtable_ids.push_back(&(iter.second));
|
||||
flush_memtable_ids.reserve(cfds.size());
|
||||
for (auto* cfd : cfds) {
|
||||
auto iter = flush_req.cfd_to_max_mem_id_to_persist.find(cfd);
|
||||
flush_memtable_ids.push_back(
|
||||
iter == flush_req.cfd_to_max_mem_id_to_persist.end()
|
||||
? nullptr
|
||||
: &(iter->second));
|
||||
}
|
||||
#ifndef NDEBUG
|
||||
// WaitForFlushMemTables pairs flush_memtable_ids[i] with cfds[i], so each
|
||||
// entry must point at this cfd's own map id (or be null when absent). Fires
|
||||
// if the ids are ever rebuilt in the request map's order.
|
||||
assert(flush_memtable_ids.size() == cfds.size());
|
||||
for (size_t i = 0; i < cfds.size(); ++i) {
|
||||
auto iter = flush_req.cfd_to_max_mem_id_to_persist.find(cfds[i]);
|
||||
assert(flush_memtable_ids[i] ==
|
||||
(iter == flush_req.cfd_to_max_mem_id_to_persist.end()
|
||||
? nullptr
|
||||
: &(iter->second)));
|
||||
}
|
||||
#endif // NDEBUG
|
||||
s = WaitForFlushMemTables(
|
||||
cfds, flush_memtable_ids,
|
||||
flush_reason == FlushReason::kErrorRecovery /* resuming_from_bg_err */,
|
||||
@@ -3072,9 +3210,29 @@ Status DBImpl::RetryFlushesForErrorRecovery(FlushReason flush_reason,
|
||||
flush_req.atomic_flush = true;
|
||||
GenerateFlushRequest(cfds, flush_reason, &flush_req);
|
||||
EnqueuePendingFlush(flush_req);
|
||||
for (auto& iter : flush_req.cfd_to_max_mem_id_to_persist) {
|
||||
flush_memtable_ids.push_back(iter.second);
|
||||
// Keep flush_memtable_ids aligned with cfds by walking cfds, since the
|
||||
// request map's order is unrelated to cfds. A CF not in the map waits on
|
||||
// its whole imm list, matching the max-bound the non-atomic branch uses.
|
||||
for (auto* cfd : cfds) {
|
||||
auto iter = flush_req.cfd_to_max_mem_id_to_persist.find(cfd);
|
||||
flush_memtable_ids.push_back(
|
||||
iter == flush_req.cfd_to_max_mem_id_to_persist.end()
|
||||
? std::numeric_limits<uint64_t>::max()
|
||||
: iter->second);
|
||||
}
|
||||
#ifndef NDEBUG
|
||||
// WaitForFlushMemTables pairs flush_memtable_ids[i] with cfds[i], so each
|
||||
// entry must be this cfd's own max id. Fires if the ids are ever rebuilt in
|
||||
// the request map's order, which is unrelated to cfds order.
|
||||
assert(flush_memtable_ids.size() == cfds.size());
|
||||
for (size_t i = 0; i < cfds.size(); ++i) {
|
||||
auto iter = flush_req.cfd_to_max_mem_id_to_persist.find(cfds[i]);
|
||||
assert(flush_memtable_ids[i] ==
|
||||
(iter == flush_req.cfd_to_max_mem_id_to_persist.end()
|
||||
? std::numeric_limits<uint64_t>::max()
|
||||
: iter->second));
|
||||
}
|
||||
#endif // NDEBUG
|
||||
} else {
|
||||
for (auto cfd : cfds) {
|
||||
flush_memtable_ids.push_back(
|
||||
@@ -3835,11 +3993,34 @@ bool DBImpl::EnqueuePendingFlush(const FlushRequest& flush_req) {
|
||||
enqueued = true;
|
||||
}
|
||||
} else {
|
||||
// Atomic flush requests bypass the queued_for_flush() deduplication guard.
|
||||
// This means an atomic request and a non-atomic request for the same CF
|
||||
// can coexist in the flush queue. This is safe because PickMemtablesToFlush
|
||||
// uses flush_in_progress_ to prevent double-picking of memtables, and
|
||||
// BackgroundFlush filters out CFs where !IsFlushPending().
|
||||
if (flush_req.flush_reason == FlushReason::kWriteBufferManager &&
|
||||
write_buffer_manager_->ShouldTrackFlushInitiator()) {
|
||||
for (const FlushRequest& queued : flush_queue_) {
|
||||
if (!queued.atomic_flush) {
|
||||
continue;
|
||||
}
|
||||
bool covered = true;
|
||||
for (const auto& requested : flush_req.cfd_to_max_mem_id_to_persist) {
|
||||
const auto queued_entry =
|
||||
queued.cfd_to_max_mem_id_to_persist.find(requested.first);
|
||||
if (queued_entry == queued.cfd_to_max_mem_id_to_persist.end() ||
|
||||
queued_entry->second < requested.second) {
|
||||
covered = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (covered) {
|
||||
TEST_SYNC_POINT(
|
||||
"DBImpl::EnqueuePendingFlush:AtomicWBMRequestDeduplicated");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Atomic flush requests normally bypass the per-CF queued_for_flush()
|
||||
// guard. Cross-DB WBM requests are deduplicated above because its
|
||||
// autonomous retry loop must not repeatedly enqueue the same atomic cut
|
||||
// while HIGH workers are unavailable. Other atomic requests retain their
|
||||
// existing behavior.
|
||||
for (auto& iter : flush_req.cfd_to_max_mem_id_to_persist) {
|
||||
ColumnFamilyData* cfd = iter.first;
|
||||
cfd->Ref();
|
||||
@@ -3964,6 +4145,178 @@ void DBImpl::UnscheduleFlushCallback(void* arg) {
|
||||
TEST_SYNC_POINT("DBImpl::UnscheduleFlushCallback");
|
||||
}
|
||||
|
||||
void DBImpl::RefreshFlushableMemAccounting() {
|
||||
mutex_.AssertHeld();
|
||||
versions_->GetColumnFamilySet()->RefreshFlushableMemAccounting();
|
||||
}
|
||||
|
||||
bool DBImpl::TryRefreshFlushableMemAccounting() {
|
||||
if (!mutex_.TryLock()) {
|
||||
return false;
|
||||
}
|
||||
Defer unlock_mutex([this] { mutex_.Unlock(); });
|
||||
RefreshFlushableMemAccounting();
|
||||
return wbm_flush_initiator_ != nullptr &&
|
||||
wbm_flush_initiator_->HasAccurateFlushableMemUsage();
|
||||
}
|
||||
|
||||
DBImpl::FlushableCFs DBImpl::CollectFlushableCFs(
|
||||
bool include_waiting_immutable) {
|
||||
mutex_.AssertHeld();
|
||||
FlushableCFs result;
|
||||
SequenceNumber oldest_seq = kMaxSequenceNumber;
|
||||
for (auto cfd : *versions_->GetColumnFamilySet()) {
|
||||
if (cfd->IsDropped() || !cfd->initialized()) {
|
||||
continue;
|
||||
}
|
||||
const size_t mutable_mem =
|
||||
cfd->mem()->IsEmpty() ? 0 : cfd->mem()->WBMTrackedMemoryUsage();
|
||||
size_t waiting_immutable_mem = 0;
|
||||
if (include_waiting_immutable) {
|
||||
if (immutable_db_options_.atomic_flush) {
|
||||
waiting_immutable_mem =
|
||||
cfd->imm()->WBMTrackedUnstartedMemTablesMemoryUsage();
|
||||
} else if (!cfd->imm()->IsFlushPendingOrRunning()) {
|
||||
waiting_immutable_mem =
|
||||
cfd->imm()->WBMTrackedUnflushedMemTablesMemoryUsage();
|
||||
}
|
||||
}
|
||||
result.total_mem += mutable_mem + waiting_immutable_mem;
|
||||
// Non-atomic flushes skip CFs already flushing.
|
||||
if (cfd->imm()->IsFlushPendingOrRunning()) {
|
||||
continue;
|
||||
}
|
||||
size_t mem = mutable_mem;
|
||||
if (include_waiting_immutable) {
|
||||
mem += waiting_immutable_mem;
|
||||
}
|
||||
if (mem == 0) {
|
||||
continue;
|
||||
}
|
||||
if (result.largest == nullptr || mem > result.largest_mem) {
|
||||
result.largest = cfd;
|
||||
result.largest_mem = mem;
|
||||
}
|
||||
const SequenceNumber seq = cfd->mem()->GetCreationSeq();
|
||||
if (result.oldest == nullptr || seq < oldest_seq) {
|
||||
result.oldest = cfd;
|
||||
oldest_seq = seq;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool DBImpl::ScheduleWriteBufferManagerFlush() {
|
||||
{
|
||||
if (!mutex_.TryLock()) {
|
||||
return false;
|
||||
}
|
||||
Defer unlock_mutex([this] { mutex_.Unlock(); });
|
||||
// Recheck bid eligibility because DB state may have changed.
|
||||
if (!write_buffer_manager_->ShouldTrackFlushInitiator() ||
|
||||
shutdown_initiated_.load(std::memory_order_acquire) ||
|
||||
shutting_down_.load(std::memory_order_acquire) ||
|
||||
reject_new_background_jobs_ || !opened_successfully_ || read_only_ ||
|
||||
error_handler_.IsBGWorkStopped() || write_controller_.IsStopped() ||
|
||||
// Reject both the pause drain and fully paused states.
|
||||
bg_work_paused_ > 0 || bg_compaction_paused_ > 0 ||
|
||||
// Never queue work that would wait behind this DB's stall.
|
||||
WouldBlockJoiningWriteThread()) {
|
||||
return false;
|
||||
}
|
||||
// A queued or running job does not count as new progress. Returning false
|
||||
// lets the coordinator try another DB instead of queuing more work behind
|
||||
// a busy LOW pool.
|
||||
if (bg_wbm_flush_scheduled_ > 0) {
|
||||
return false;
|
||||
}
|
||||
const FlushableCFs flushable =
|
||||
CollectFlushableCFs(true /* include_waiting_immutable */);
|
||||
const bool has_flushable_cf = immutable_db_options_.atomic_flush
|
||||
? flushable.total_mem > 0
|
||||
: flushable.largest != nullptr;
|
||||
if (!has_flushable_cf) {
|
||||
wbm_flush_initiator_->SetHasFlushableCF(false);
|
||||
write_buffer_manager_->NotifyFlushInitiatorChanged();
|
||||
return false;
|
||||
}
|
||||
wbm_flush_initiator_->SetHasFlushableCF(false);
|
||||
write_buffer_manager_->NotifyFlushInitiatorChanged();
|
||||
++bg_wbm_flush_scheduled_;
|
||||
}
|
||||
|
||||
// The WBM-owned executor provides bounded capacity independent of Env::LOW.
|
||||
// Run outside mutex_ so DB-local work cannot block other DB mutex users.
|
||||
BackgroundCallWBMFlush();
|
||||
return true;
|
||||
}
|
||||
|
||||
void DBImpl::BackgroundCallWBMFlush() {
|
||||
const bool atomic_flush = immutable_db_options_.atomic_flush;
|
||||
ColumnFamilyData* cfd_to_flush = nullptr;
|
||||
bool has_flushable_cf = false;
|
||||
{
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
if (!shutdown_initiated_.load(std::memory_order_acquire) &&
|
||||
!shutting_down_.load(std::memory_order_acquire) &&
|
||||
!reject_new_background_jobs_ && opened_successfully_ && !read_only_ &&
|
||||
!error_handler_.IsBGWorkStopped() && !write_controller_.IsStopped() &&
|
||||
bg_work_paused_ == 0 && bg_compaction_paused_ == 0 &&
|
||||
!WouldBlockJoiningWriteThread()) {
|
||||
const FlushableCFs flushable =
|
||||
CollectFlushableCFs(true /* include_waiting_immutable */);
|
||||
has_flushable_cf =
|
||||
atomic_flush ? flushable.total_mem > 0 : flushable.largest != nullptr;
|
||||
if (!atomic_flush) {
|
||||
cfd_to_flush = flushable.largest;
|
||||
if (cfd_to_flush != nullptr) {
|
||||
cfd_to_flush->Ref();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
TEST_SYNC_POINT("DBImpl::BackgroundCallWBMFlush:AfterPick");
|
||||
|
||||
FlushOptions flush_options;
|
||||
flush_options.wait = false;
|
||||
flush_options.allow_write_stall = true;
|
||||
bool made_progress = false;
|
||||
if (has_flushable_cf) {
|
||||
Status s;
|
||||
if (atomic_flush) {
|
||||
s = AtomicFlushMemTablesImpl(
|
||||
flush_options, FlushReason::kWriteBufferManager,
|
||||
{} /* provided_candidate_cfds */, false /* entered_write_thread */,
|
||||
WriteThreadJoinMode::kNonBlocking, &made_progress);
|
||||
} else {
|
||||
s = FlushMemTableImpl(cfd_to_flush, flush_options,
|
||||
FlushReason::kWriteBufferManager,
|
||||
false /* entered_write_thread */,
|
||||
WriteThreadJoinMode::kNonBlocking, &made_progress);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
ROCKS_LOG_WARN(
|
||||
immutable_db_options_.info_log,
|
||||
"Flush on behalf of a shared WriteBufferManager failed: %s",
|
||||
s.ToString().c_str());
|
||||
}
|
||||
s.PermitUncheckedError();
|
||||
}
|
||||
TEST_SYNC_POINT_CALLBACK("DBImpl::BackgroundCallWBMFlush:MadeProgress",
|
||||
&made_progress);
|
||||
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
if (cfd_to_flush != nullptr) {
|
||||
cfd_to_flush->UnrefAndTryDelete();
|
||||
}
|
||||
RefreshFlushableMemAccounting();
|
||||
--bg_wbm_flush_scheduled_;
|
||||
write_buffer_manager_->NotifyFlushInitiatorFlushCompleted(
|
||||
wbm_flush_initiator_.get(), made_progress);
|
||||
TEST_SYNC_POINT_CALLBACK("DBImpl::BGWorkWBMFlush:done", this);
|
||||
bg_cv_.SignalAll();
|
||||
}
|
||||
|
||||
Status DBImpl::BackgroundFlush(bool* made_progress, JobContext* job_context,
|
||||
LogBuffer* log_buffer, FlushReason* reason,
|
||||
bool* flush_rescheduled_to_retain_udt,
|
||||
@@ -4194,6 +4547,9 @@ void DBImpl::BackgroundCallFlush(Env::Priority thread_pri) {
|
||||
assert(num_running_flushes_ > 0);
|
||||
num_running_flushes_--;
|
||||
bg_flush_scheduled_--;
|
||||
if (write_buffer_manager_->ShouldTrackFlushInitiator()) {
|
||||
RefreshFlushableMemAccounting();
|
||||
}
|
||||
// See if there's more work to be done
|
||||
MaybeScheduleFlushOrCompaction();
|
||||
|
||||
@@ -4760,7 +5116,8 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
|
||||
immutable_db_options_.stats, Histograms::SST_WRITE_MICROS,
|
||||
c->immutable_options().listeners,
|
||||
immutable_db_options_.file_checksum_gen_factory.get(),
|
||||
tmp_set.Contains(FileType::kTableFile), false));
|
||||
tmp_set.Contains(FileType::kTableFile),
|
||||
tmp_set.Contains(FileType::kTableFile)));
|
||||
}
|
||||
|
||||
ROCKS_LOG_BUFFER(
|
||||
@@ -4851,7 +5208,7 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
|
||||
false /* marked_for_compact */,
|
||||
c->GetOutputTemperature() /* temperature */,
|
||||
in_file->oldest_blob_file_number,
|
||||
in_file->oldest_ancester_time,
|
||||
in_file->oldest_ancestor_time,
|
||||
out_file_creation_time,
|
||||
c->MinInputFileEpochNumber(),
|
||||
dest_writer->GetFileChecksum(),
|
||||
@@ -5657,15 +6014,47 @@ Status DBImpl::WaitForCompact(
|
||||
const auto deadline = immutable_db_options_.clock->NowMicros() +
|
||||
wait_for_compact_options.timeout.count();
|
||||
for (;;) {
|
||||
// The coordinator reserves this DB before invoking its non-blocking
|
||||
// scheduling callback. Drain that DB-specific reservation without holding
|
||||
// mutex_; successful callbacks publish bg_wbm_flush_scheduled_ before
|
||||
// releasing the reservation.
|
||||
if (write_buffer_manager_ != nullptr &&
|
||||
write_buffer_manager_->ShouldTrackFlushInitiator() &&
|
||||
wbm_flush_initiator_ != nullptr) {
|
||||
// Fence all coordinator requests visible before this local quiescence
|
||||
// check. Candidate retrieval either observes this generation and skips
|
||||
// the DB, or atomically publishes a handoff reservation first.
|
||||
write_buffer_manager_->FenceFlushInitiator(wbm_flush_initiator_.get());
|
||||
}
|
||||
if (wbm_flush_initiator_ != nullptr &&
|
||||
wbm_flush_initiator_->HasPendingFlushHandoff()) {
|
||||
std::chrono::microseconds handoff_timeout{0};
|
||||
if (wait_for_compact_options.timeout.count() != 0) {
|
||||
const uint64_t now = immutable_db_options_.clock->NowMicros();
|
||||
if (now >= deadline) {
|
||||
return Status::TimedOut();
|
||||
}
|
||||
handoff_timeout = std::chrono::microseconds(deadline - now);
|
||||
}
|
||||
mutex_.Unlock();
|
||||
const bool handoff_finished =
|
||||
wbm_flush_initiator_->WaitForPendingFlushHandoff(handoff_timeout);
|
||||
mutex_.Lock();
|
||||
if (!handoff_finished) {
|
||||
return Status::TimedOut();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (shutting_down_.load(std::memory_order_acquire)) {
|
||||
return Status::ShutdownInProgress();
|
||||
}
|
||||
if (bg_work_paused_ && wait_for_compact_options.abort_on_pause) {
|
||||
return Status::Aborted();
|
||||
}
|
||||
// A WBM job can enqueue a regular flush, so wait for both to quiesce.
|
||||
if ((bg_bottom_compaction_scheduled_ || bg_compaction_scheduled_ ||
|
||||
bg_flush_scheduled_ || unscheduled_compactions_ ||
|
||||
!parked_compaction_cfds_.empty() ||
|
||||
bg_flush_scheduled_ || bg_wbm_flush_scheduled_ ||
|
||||
unscheduled_compactions_ || !parked_compaction_cfds_.empty() ||
|
||||
(wait_for_compact_options.wait_for_purge && bg_purge_scheduled_) ||
|
||||
unscheduled_flushes_ || error_handler_.IsRecoveryInProgress()) &&
|
||||
(error_handler_.GetBGError().ok())) {
|
||||
|
||||
@@ -144,6 +144,18 @@ Status DBImpl::TEST_SwitchMemtable(ColumnFamilyData* cfd) {
|
||||
return s;
|
||||
}
|
||||
|
||||
size_t DBImpl::TEST_GetWBMFlushableMemUsage() {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
assert(wbm_flush_initiator_ != nullptr);
|
||||
RefreshFlushableMemAccounting();
|
||||
return wbm_flush_initiator_->GetFlushableMemUsage();
|
||||
}
|
||||
|
||||
bool DBImpl::TEST_IsWBMFlushInitiatorFlushable() const {
|
||||
assert(wbm_flush_initiator_ != nullptr);
|
||||
return wbm_flush_initiator_->IsFlushable();
|
||||
}
|
||||
|
||||
Status DBImpl::TEST_FlushMemTable(bool wait, bool allow_write_stall,
|
||||
ColumnFamilyHandle* cfh) {
|
||||
FlushOptions fo;
|
||||
@@ -182,9 +194,31 @@ Status DBImpl::TEST_FlushMemTableWithListenerWait(bool allow_write_stall,
|
||||
|
||||
Status DBImpl::TEST_AtomicFlushMemTables(
|
||||
const autovector<ColumnFamilyData*>& provided_candidate_cfds,
|
||||
const FlushOptions& flush_opts) {
|
||||
return AtomicFlushMemTables(flush_opts, FlushReason::kTest,
|
||||
provided_candidate_cfds);
|
||||
const FlushOptions& flush_opts, bool non_blocking_write_thread) {
|
||||
const auto join_mode = non_blocking_write_thread
|
||||
? WriteThreadJoinMode::kNonBlocking
|
||||
: WriteThreadJoinMode::kBlocking;
|
||||
return AtomicFlushMemTablesImpl(
|
||||
flush_opts, FlushReason::kTest, provided_candidate_cfds,
|
||||
false /* entered_write_thread */, join_mode, nullptr /* made_progress */);
|
||||
}
|
||||
|
||||
void DBImpl::TEST_BeginWriteStall() {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
write_thread_.BeginWriteStall();
|
||||
if (wbm_flush_initiator_ != nullptr &&
|
||||
write_buffer_manager_->ShouldTrackFlushInitiator()) {
|
||||
wbm_flush_initiator_->SetFlushable(false);
|
||||
}
|
||||
}
|
||||
|
||||
void DBImpl::TEST_EndWriteStall() {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
write_thread_.EndWriteStall();
|
||||
if (wbm_flush_initiator_ != nullptr &&
|
||||
write_buffer_manager_->ShouldTrackFlushInitiator()) {
|
||||
wbm_flush_initiator_->SetFlushable(true);
|
||||
}
|
||||
}
|
||||
|
||||
Status DBImpl::TEST_WaitForBackgroundWork() {
|
||||
|
||||
@@ -140,7 +140,7 @@ Status DBImpl::PromoteL0(ColumnFamilyHandle* column_family, int target_level) {
|
||||
f->fd.GetFileSize(), f->smallest, f->largest,
|
||||
f->fd.smallest_seqno, f->fd.largest_seqno,
|
||||
f->marked_for_compaction, f->temperature,
|
||||
f->oldest_blob_file_number, f->oldest_ancester_time,
|
||||
f->oldest_blob_file_number, f->oldest_ancestor_time,
|
||||
f->file_creation_time, f->epoch_number, f->file_checksum,
|
||||
f->file_checksum_func_name, f->unique_id,
|
||||
f->compensated_range_deletion_size, f->tail_size,
|
||||
|
||||
+21
-13
@@ -489,31 +489,33 @@ void DBImpl::DeleteObsoleteFileImpl(int job_id, const std::string& fname,
|
||||
&file_deletion_status);
|
||||
if (file_deletion_status.ok()) {
|
||||
ROCKS_LOG_DEBUG(immutable_db_options_.info_log,
|
||||
"[JOB %d] Delete %s type=%d #%" PRIu64 " -- %s\n", job_id,
|
||||
fname.c_str(), type, number,
|
||||
"[JOB %d] [db_session_id=%s] Delete %s type=%d #%" PRIu64
|
||||
" -- %s\n",
|
||||
job_id, db_session_id_.c_str(), fname.c_str(), type, number,
|
||||
file_deletion_status.ToString().c_str());
|
||||
} else if (env_->FileExists(fname).IsNotFound()) {
|
||||
ROCKS_LOG_INFO(
|
||||
immutable_db_options_.info_log,
|
||||
"[JOB %d] Tried to delete a non-existing file %s type=%d #%" PRIu64
|
||||
" -- %s\n",
|
||||
job_id, fname.c_str(), type, number,
|
||||
"[JOB %d] [db_session_id=%s] Tried to delete a non-existing file %s"
|
||||
" type=%d #%" PRIu64 " -- %s\n",
|
||||
job_id, db_session_id_.c_str(), fname.c_str(), type, number,
|
||||
file_deletion_status.ToString().c_str());
|
||||
} else {
|
||||
ROCKS_LOG_ERROR(immutable_db_options_.info_log,
|
||||
"[JOB %d] Failed to delete %s type=%d #%" PRIu64 " -- %s\n",
|
||||
job_id, fname.c_str(), type, number,
|
||||
"[JOB %d] [db_session_id=%s] Failed to delete %s type=%d"
|
||||
" #%" PRIu64 " -- %s\n",
|
||||
job_id, db_session_id_.c_str(), fname.c_str(), type, number,
|
||||
file_deletion_status.ToString().c_str());
|
||||
}
|
||||
if (type == kTableFile) {
|
||||
EventHelpers::LogAndNotifyTableFileDeletion(
|
||||
&event_logger_, job_id, number, fname, file_deletion_status, GetName(),
|
||||
immutable_db_options_.listeners);
|
||||
db_session_id_, immutable_db_options_.listeners);
|
||||
}
|
||||
if (type == kBlobFile) {
|
||||
EventHelpers::LogAndNotifyBlobFileDeletion(
|
||||
&event_logger_, immutable_db_options_.listeners, job_id, number, fname,
|
||||
file_deletion_status, GetName());
|
||||
file_deletion_status, GetName(), db_session_id_);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1192,6 +1194,7 @@ Status DBImpl::MaybeUpdateNextFileNumber(RecoveryContext* recovery_ctx) {
|
||||
const uint64_t prev_next_file_number = versions_->current_next_file_number();
|
||||
uint64_t largest_file_number = prev_next_file_number;
|
||||
bool on_disk_file_advanced_counter = false;
|
||||
bool manifest_relevant_file_advanced_counter = false;
|
||||
Status s;
|
||||
for (const auto& path : CollectAllDBPaths()) {
|
||||
std::vector<std::string> files;
|
||||
@@ -1211,10 +1214,11 @@ Status DBImpl::MaybeUpdateNextFileNumber(RecoveryContext* recovery_ctx) {
|
||||
// Use >= so a crashed-mid-allocation file at exactly
|
||||
// prev_next_file_number triggers the advance -- otherwise the next
|
||||
// NewFileNumber() would collide with it.
|
||||
const bool must_reserve_file_number =
|
||||
type != kOptionsFile && type != kTempFile;
|
||||
if (must_reserve_file_number && number >= prev_next_file_number) {
|
||||
if (number >= prev_next_file_number) {
|
||||
on_disk_file_advanced_counter = true;
|
||||
if (type != kOptionsFile && type != kTempFile) {
|
||||
manifest_relevant_file_advanced_counter = true;
|
||||
}
|
||||
}
|
||||
largest_file_number = std::max(largest_file_number, number);
|
||||
if ((type == kTableFile || type == kBlobFile)) {
|
||||
@@ -1236,7 +1240,8 @@ Status DBImpl::MaybeUpdateNextFileNumber(RecoveryContext* recovery_ctx) {
|
||||
mutable_db_options_.optimize_manifest_for_recovery &&
|
||||
!immutable_db_options_.best_efforts_recovery;
|
||||
|
||||
if (!optimize_manifest_for_recovery || on_disk_file_advanced_counter) {
|
||||
if (!optimize_manifest_for_recovery ||
|
||||
manifest_relevant_file_advanced_counter) {
|
||||
if (largest_file_number >= prev_next_file_number) {
|
||||
versions_->next_file_number_.store(largest_file_number + 1);
|
||||
}
|
||||
@@ -1249,6 +1254,9 @@ Status DBImpl::MaybeUpdateNextFileNumber(RecoveryContext* recovery_ctx) {
|
||||
assert(default_cfd);
|
||||
recovery_ctx->UpdateVersionEdits(default_cfd, edit);
|
||||
} else {
|
||||
if (on_disk_file_advanced_counter) {
|
||||
versions_->next_file_number_.store(largest_file_number + 1);
|
||||
}
|
||||
TEST_SYNC_POINT("DBImpl::Recovery:SkippedNoopEdit:NextFileNumber");
|
||||
}
|
||||
return s;
|
||||
|
||||
@@ -153,6 +153,7 @@ Status DBImplFollower::TryCatchUpWithLeader() {
|
||||
cfd->InstallSuperVersion(&sv_context, &mutex_);
|
||||
sv_context.NewSuperVersion();
|
||||
}
|
||||
PublishSecondaryReadView();
|
||||
}
|
||||
|
||||
for (auto& file : files_to_delete) {
|
||||
@@ -164,6 +165,7 @@ Status DBImplFollower::TryCatchUpWithLeader() {
|
||||
}
|
||||
}
|
||||
}
|
||||
CleanupRetiredSecondaryReadViews();
|
||||
job_context.Clean();
|
||||
|
||||
// Cleanup unused, obsolete files.
|
||||
@@ -296,7 +298,8 @@ Status DB::OpenAsFollower(
|
||||
dbname, &impl->immutable_db_options_, impl->mutable_db_options_,
|
||||
impl->file_options_, impl->table_cache_.get(),
|
||||
impl->write_buffer_manager_, &impl->write_controller_, impl->io_tracer_,
|
||||
impl->db_id_, impl->db_session_id_));
|
||||
impl->db_id_, impl->db_session_id_,
|
||||
/*defer_sst_file_opening=*/false));
|
||||
impl->column_family_memtables_.reset(
|
||||
new ColumnFamilyMemTablesImpl(impl->versions_->GetColumnFamilySet()));
|
||||
impl->wal_in_db_path_ = impl->immutable_db_options_.IsWalDirSameAsDBPath();
|
||||
@@ -322,9 +325,11 @@ Status DB::OpenAsFollower(
|
||||
sv_context.NewSuperVersion();
|
||||
cfd->InstallSuperVersion(&sv_context, &impl->mutex_);
|
||||
}
|
||||
impl->PublishSecondaryReadView();
|
||||
impl->MarkAsyncFileOpenNotNeeded();
|
||||
}
|
||||
impl->mutex_.Unlock();
|
||||
impl->CleanupRetiredSecondaryReadViews();
|
||||
sv_context.Clean();
|
||||
if (s.ok()) {
|
||||
dbptr->reset(impl);
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "rocksdb/db.h"
|
||||
#include "table/multiget_context.h"
|
||||
#include "util/autovector.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
inline std::string* GetOutputTimestamp(OutputMetadata* output_metadata) {
|
||||
return output_metadata != nullptr && output_metadata->timestamp.has_value()
|
||||
? &*output_metadata->timestamp
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
inline std::vector<std::string>* GetOutputTimestamps(
|
||||
MultiGetOutputMetadata* output_metadata) {
|
||||
return output_metadata != nullptr && output_metadata->timestamps.has_value()
|
||||
? &*output_metadata->timestamps
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
inline bool* GetOutputNewerVersionPresent(OutputMetadata* output_metadata) {
|
||||
return output_metadata != nullptr &&
|
||||
output_metadata->newer_version_present.has_value()
|
||||
? &*output_metadata->newer_version_present
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
inline std::vector<uint8_t>* GetOutputNewerVersionPresent(
|
||||
MultiGetOutputMetadata* output_metadata) {
|
||||
return output_metadata != nullptr &&
|
||||
output_metadata->newer_version_present.has_value()
|
||||
? &*output_metadata->newer_version_present
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
inline ColumnFamilyHandle** MakeMutableCfHandles(
|
||||
ColumnFamilyHandle* const* column_families, size_t num_keys,
|
||||
autovector<ColumnFamilyHandle*, MultiGetContext::MAX_BATCH_SIZE>*
|
||||
stack_column_families,
|
||||
std::vector<ColumnFamilyHandle*>* heap_column_families) {
|
||||
ColumnFamilyHandle** mutable_column_families;
|
||||
if (num_keys <= MultiGetContext::MAX_BATCH_SIZE) {
|
||||
stack_column_families->resize(num_keys);
|
||||
mutable_column_families =
|
||||
num_keys == 0 ? nullptr : &(*stack_column_families)[0];
|
||||
} else {
|
||||
heap_column_families->resize(num_keys);
|
||||
mutable_column_families = heap_column_families->data();
|
||||
}
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
mutable_column_families[i] = column_families[i];
|
||||
}
|
||||
return mutable_column_families;
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
+45
-13
@@ -683,7 +683,7 @@ Status DBImpl::Recover(
|
||||
f->marked_for_compaction,
|
||||
f->temperature, // this can be different from
|
||||
// `last_level_temperature`
|
||||
f->oldest_blob_file_number, f->oldest_ancester_time,
|
||||
f->oldest_blob_file_number, f->oldest_ancestor_time,
|
||||
f->file_creation_time, f->epoch_number,
|
||||
f->file_checksum, f->file_checksum_func_name,
|
||||
f->unique_id, f->compensated_range_deletion_size,
|
||||
@@ -2143,7 +2143,7 @@ Status DBImpl::WriteLevel0TableForRecovery(int job_id, ColumnFamilyData* cfd,
|
||||
immutable_db_options_.clock->GetCurrentTime(&_current_time)
|
||||
.PermitUncheckedError(); // ignore error
|
||||
const uint64_t current_time = static_cast<uint64_t>(_current_time);
|
||||
meta.oldest_ancester_time = current_time;
|
||||
meta.oldest_ancestor_time = current_time;
|
||||
meta.epoch_number = cfd->NewEpochNumber();
|
||||
{
|
||||
auto write_hint = cfd->current()->storage_info()->CalculateSSTWriteHint(
|
||||
@@ -2277,7 +2277,7 @@ Status DBImpl::WriteLevel0TableForRecovery(int job_id, ColumnFamilyData* cfd,
|
||||
meta.fd.GetFileSize(), meta.smallest, meta.largest,
|
||||
meta.fd.smallest_seqno, meta.fd.largest_seqno,
|
||||
meta.marked_for_compaction, meta.temperature,
|
||||
meta.oldest_blob_file_number, meta.oldest_ancester_time,
|
||||
meta.oldest_blob_file_number, meta.oldest_ancestor_time,
|
||||
meta.file_creation_time, meta.epoch_number,
|
||||
meta.file_checksum, meta.file_checksum_func_name,
|
||||
meta.unique_id, meta.compensated_range_deletion_size,
|
||||
@@ -2711,6 +2711,12 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
|
||||
if (s.ok()) {
|
||||
s = impl->CreateArchivalDirectory();
|
||||
}
|
||||
if (s.ok() &&
|
||||
impl->immutable_db_options_.use_session_tmp_dir_for_remote_compaction) {
|
||||
// Create the session-scoped temporary directory before a CompactionService
|
||||
// can create any per-job directory under it.
|
||||
s = impl->env_->CreateDirIfMissing(SessionTmpDir(dbname));
|
||||
}
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -2785,8 +2791,9 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
|
||||
s = WritableFileWriter::PrepareIOOptions(write_options, opts);
|
||||
}
|
||||
if (s.ok()) {
|
||||
s = log_writer->file()->Sync(opts,
|
||||
impl->immutable_db_options_.use_fsync);
|
||||
s = impl->immutable_db_options_.use_fsync
|
||||
? log_writer->file()->Fsync(opts)
|
||||
: log_writer->file()->Sync(opts);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2925,6 +2932,13 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
|
||||
impl->immutable_db_options_.db_paths[0].path);
|
||||
}
|
||||
|
||||
if (s.ok() &&
|
||||
impl->immutable_db_options_.use_session_tmp_dir_for_remote_compaction) {
|
||||
// Finish deleting data from the previous DB session before this open can
|
||||
// schedule a new compaction in the directory.
|
||||
impl->CleanupSessionTmpDir();
|
||||
}
|
||||
|
||||
if (s.ok()) {
|
||||
// When the DB is stopped, it's possible that there are some .trash files
|
||||
// that were not deleted yet, when we open the DB we will find these .trash
|
||||
@@ -2966,8 +2980,9 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
|
||||
IOOptions opts;
|
||||
s = WritableFileWriter::PrepareIOOptions(write_options, opts);
|
||||
if (s.ok()) {
|
||||
s = log_writer->file()->Sync(opts,
|
||||
impl->immutable_db_options_.use_fsync);
|
||||
s = impl->immutable_db_options_.use_fsync
|
||||
? log_writer->file()->Fsync(opts)
|
||||
: log_writer->file()->Sync(opts);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3000,6 +3015,8 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
|
||||
}
|
||||
}
|
||||
if (s.ok()) {
|
||||
// Register only after open succeeds and without holding the DB mutex.
|
||||
impl->MaybeRegisterFlushInitiator();
|
||||
*dbptr = std::move(impl);
|
||||
} else {
|
||||
for (auto* h : *handles) {
|
||||
@@ -3057,8 +3074,18 @@ void DBImpl::ScheduleAsyncFileOpening() {
|
||||
bg_async_file_open_state_ = AsyncFileOpenState::kScheduled;
|
||||
|
||||
// since this is a one time job, best to schedule it with high priority
|
||||
auto unschedule_callback = [](void* arg) {
|
||||
auto* unscheduled_ctx = static_cast<AsyncFileOpenContext*>(arg);
|
||||
DBImpl* db = unscheduled_ctx->db;
|
||||
db->mutex_.AssertHeld();
|
||||
delete unscheduled_ctx;
|
||||
db->bg_async_file_open_state_ = AsyncFileOpenState::kComplete;
|
||||
db->bg_cv_.SignalAll();
|
||||
TEST_SYNC_POINT("DBImpl::UnscheduleAsyncFileOpenCallback");
|
||||
TEST_SYNC_POINT("DBImpl::BGWorkAsyncFileOpen:Done");
|
||||
};
|
||||
env_->Schedule(&DBImpl::BGWorkAsyncFileOpen, ctx, Env::Priority::HIGH,
|
||||
nullptr);
|
||||
&bg_async_file_open_state_, unschedule_callback);
|
||||
}
|
||||
|
||||
void DBImpl::MarkAsyncFileOpenNotNeeded() {
|
||||
@@ -3088,8 +3115,9 @@ void DBImpl::BGWorkAsyncFileOpen(void* arg) {
|
||||
auto* version = ctx->versions[i];
|
||||
ColumnFamilyData* cfd = version->cfd();
|
||||
|
||||
// Skip column families that were dropped after scheduling
|
||||
if (cfd->IsDropped()) {
|
||||
// Retained secondary handles remain readable after the primary drops the
|
||||
// column family, so their captured Versions still need pinned readers.
|
||||
if (cfd->IsDropped() && !db->read_only_) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -3119,11 +3147,15 @@ void DBImpl::BGWorkAsyncFileOpen(void* arg) {
|
||||
if (!s.ok()) {
|
||||
ROCKS_LOG_ERROR(
|
||||
db->immutable_db_options_.info_log,
|
||||
"BGWorkAsyncFileOpen: LoadTableHandlers failed for CF %s: "
|
||||
"%s",
|
||||
"BGWorkAsyncFileOpen: file validation/opening failed for CF %s: %s",
|
||||
cfd->GetName().c_str(), s.ToString().c_str());
|
||||
InstrumentedMutexLock l(&db->mutex_);
|
||||
db->error_handler_.SetBGError(s, BackgroundErrorReason::kAsyncFileOpen);
|
||||
if (db->read_only_) {
|
||||
db->error_handler_.SetBGErrorNoAutoRecovery(
|
||||
s, BackgroundErrorReason::kAsyncFileOpen);
|
||||
} else {
|
||||
db->error_handler_.SetBGError(s, BackgroundErrorReason::kAsyncFileOpen);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
+114
-43
@@ -11,6 +11,7 @@
|
||||
#include "db/blob/blob_fetcher.h"
|
||||
#include "db/db_impl/compacted_db_impl.h"
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "db/db_impl/db_impl_metadata.h"
|
||||
#include "db/manifest_ops.h"
|
||||
#include "db/merge_context.h"
|
||||
#include "logging/logging.h"
|
||||
@@ -30,6 +31,30 @@ DBImplReadOnly::DBImplReadOnly(const DBOptions& db_options,
|
||||
|
||||
DBImplReadOnly::~DBImplReadOnly() = default;
|
||||
|
||||
void DBImplReadOnly::MultiGetWithMetadata(
|
||||
const ReadOptions& options, const size_t num_keys,
|
||||
ColumnFamilyHandle* const* column_families, const Slice* keys,
|
||||
PinnableSlice* values, Status* statuses,
|
||||
MultiGetOutputMetadata* output_metadata, const bool sorted_input) {
|
||||
std::vector<std::string>* timestamps = GetOutputTimestamps(output_metadata);
|
||||
if (timestamps != nullptr) {
|
||||
timestamps->resize(num_keys);
|
||||
}
|
||||
std::vector<uint8_t>* newer_version_present =
|
||||
GetOutputNewerVersionPresent(output_metadata);
|
||||
if (newer_version_present != nullptr) {
|
||||
newer_version_present->assign(num_keys, false);
|
||||
}
|
||||
autovector<ColumnFamilyHandle*, MultiGetContext::MAX_BATCH_SIZE>
|
||||
stack_column_families;
|
||||
std::vector<ColumnFamilyHandle*> heap_column_families;
|
||||
ColumnFamilyHandle** mutable_column_families = MakeMutableCfHandles(
|
||||
column_families, num_keys, &stack_column_families, &heap_column_families);
|
||||
DBImpl::MultiGet(options, num_keys, mutable_column_families, keys, values,
|
||||
timestamps != nullptr ? timestamps->data() : nullptr,
|
||||
statuses, sorted_input);
|
||||
}
|
||||
|
||||
Iterator* DBImplReadOnly::NewIterator(const ReadOptions& _read_options,
|
||||
ColumnFamilyHandle* column_family) {
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
@@ -82,65 +107,111 @@ Status DBImplReadOnly::NewIterators(
|
||||
const ReadOptions& read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) {
|
||||
if (read_options.timestamp) {
|
||||
for (auto* cf : column_families) {
|
||||
assert(cf);
|
||||
const Status s = FailIfTsMismatchCf(cf, *(read_options.timestamp));
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (auto* cf : column_families) {
|
||||
assert(cf);
|
||||
const Status s = FailIfCfHasTs(cf);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
return NewIterators(
|
||||
std::vector<ReadOptions>(column_families.size(), read_options),
|
||||
column_families, iterators);
|
||||
}
|
||||
|
||||
ReadCallback* read_callback = nullptr; // No read callback provided.
|
||||
Status DBImplReadOnly::NewIterators(
|
||||
const std::vector<ReadOptions>& read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) {
|
||||
if (read_options.size() != column_families.size()) {
|
||||
return Status::InvalidArgument(
|
||||
"read_options and column_families must have the same size");
|
||||
}
|
||||
if (iterators == nullptr) {
|
||||
return Status::InvalidArgument("iterators not allowed to be nullptr");
|
||||
}
|
||||
|
||||
if (column_families.empty()) {
|
||||
iterators->clear();
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
const Snapshot* const snapshot = read_options.front().snapshot;
|
||||
const bool tailing = read_options.front().tailing;
|
||||
std::vector<ReadOptions> normalized_read_options;
|
||||
normalized_read_options.reserve(read_options.size());
|
||||
for (size_t i = 0; i < read_options.size(); ++i) {
|
||||
const ReadOptions& options = read_options[i];
|
||||
if (options.io_activity != Env::IOActivity::kUnknown &&
|
||||
options.io_activity != Env::IOActivity::kDBIterator) {
|
||||
return Status::InvalidArgument(
|
||||
"Can only call NewIterators with `ReadOptions::io_activity` is "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kDBIterator`");
|
||||
}
|
||||
if (options.read_tier == kPersistedTier) {
|
||||
return Status::NotSupported(
|
||||
"ReadTier::kPersistedData is not yet supported in iterators.");
|
||||
}
|
||||
if (options.snapshot != snapshot) {
|
||||
return Status::InvalidArgument(
|
||||
"All ReadOptions must use the same snapshot");
|
||||
}
|
||||
if (options.tailing != tailing) {
|
||||
return Status::InvalidArgument(
|
||||
"All ReadOptions must use the same tailing setting");
|
||||
}
|
||||
|
||||
auto* cf = column_families[i];
|
||||
assert(cf);
|
||||
Status s;
|
||||
if (options.timestamp) {
|
||||
s = FailIfTsMismatchCf(cf, *(options.timestamp));
|
||||
} else {
|
||||
s = FailIfCfHasTs(cf);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
normalized_read_options.emplace_back(options);
|
||||
if (normalized_read_options.back().io_activity ==
|
||||
Env::IOActivity::kUnknown) {
|
||||
normalized_read_options.back().io_activity = Env::IOActivity::kDBIterator;
|
||||
}
|
||||
}
|
||||
|
||||
iterators->clear();
|
||||
iterators->reserve(column_families.size());
|
||||
SequenceNumber latest_snapshot = versions_->LastSequence();
|
||||
|
||||
SequenceNumber read_seq =
|
||||
read_options.snapshot != nullptr
|
||||
? static_cast<const SnapshotImpl*>(read_options.snapshot)->number_
|
||||
: latest_snapshot;
|
||||
|
||||
snapshot != nullptr ? static_cast<const SnapshotImpl*>(snapshot)->number_
|
||||
: versions_->LastSequence();
|
||||
autovector<std::tuple<ColumnFamilyHandleImpl*, SuperVersion*>> cfh_to_sv;
|
||||
for (auto* cf : column_families) {
|
||||
auto* cfh = static_cast_with_check<ColumnFamilyHandleImpl>(cf);
|
||||
cfh_to_sv.emplace_back(cfh, cfh->cfd()->GetSuperVersion()->Ref());
|
||||
}
|
||||
|
||||
const bool check_read_ts =
|
||||
read_options.timestamp && read_options.timestamp->size() > 0;
|
||||
for (auto cfh : column_families) {
|
||||
auto* cfd = static_cast_with_check<ColumnFamilyHandleImpl>(cfh)->cfd();
|
||||
auto* sv = cfd->GetSuperVersion()->Ref();
|
||||
cfh_to_sv.emplace_back(static_cast_with_check<ColumnFamilyHandleImpl>(cfh),
|
||||
sv);
|
||||
if (check_read_ts) {
|
||||
const auto unref_super_versions = [&]() {
|
||||
for (const auto& entry : cfh_to_sv) {
|
||||
std::get<1>(entry)->Unref();
|
||||
}
|
||||
};
|
||||
for (size_t i = 0; i < cfh_to_sv.size(); ++i) {
|
||||
const ReadOptions& options = normalized_read_options[i];
|
||||
if (options.timestamp && !options.timestamp->empty()) {
|
||||
auto* cfd = std::get<0>(cfh_to_sv[i])->cfd();
|
||||
auto* sv = std::get<1>(cfh_to_sv[i]);
|
||||
const Status s =
|
||||
FailIfReadCollapsedHistory(cfd, sv, *(read_options.timestamp));
|
||||
FailIfReadCollapsedHistory(cfd, sv, *(options.timestamp));
|
||||
if (!s.ok()) {
|
||||
for (auto prev_entry : cfh_to_sv) {
|
||||
std::get<1>(prev_entry)->Unref();
|
||||
}
|
||||
unref_super_versions();
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert(cfh_to_sv.size() == column_families.size());
|
||||
for (auto [cfh, sv] : cfh_to_sv) {
|
||||
auto* db_iter = NewArenaWrappedDbIterator(
|
||||
env_, read_options, cfh, sv, read_seq, read_callback, this,
|
||||
/*expose_blob_index=*/false, /*allow_refresh=*/false,
|
||||
/*allow_mark_memtable_for_flush=*/false);
|
||||
iterators->push_back(db_iter);
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < cfh_to_sv.size(); ++i) {
|
||||
auto* cfh = std::get<0>(cfh_to_sv[i]);
|
||||
auto* sv = std::get<1>(cfh_to_sv[i]);
|
||||
iterators->push_back(NewArenaWrappedDbIterator(
|
||||
env_, normalized_read_options[i], cfh, sv, read_seq,
|
||||
nullptr /* read_callback */, this, /* expose_blob_index */ false,
|
||||
/* allow_refresh */ false, /* allow_mark_memtable_for_flush */ false));
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,13 @@ class DBImplReadOnly : public DBImpl {
|
||||
const Slice& key,
|
||||
GetImplOptions& get_impl_options);
|
||||
|
||||
// TODO: Implement ReadOnly MultiGet?
|
||||
using DBImpl::MultiGetWithMetadata;
|
||||
void MultiGetWithMetadata(const ReadOptions& options, const size_t num_keys,
|
||||
ColumnFamilyHandle* const* column_families,
|
||||
const Slice* keys, PinnableSlice* values,
|
||||
Status* statuses,
|
||||
MultiGetOutputMetadata* output_metadata,
|
||||
const bool sorted_input = false) override;
|
||||
|
||||
using DBImpl::NewIterator;
|
||||
Iterator* NewIterator(const ReadOptions& _read_options,
|
||||
@@ -37,6 +43,9 @@ class DBImplReadOnly : public DBImpl {
|
||||
Status NewIterators(const ReadOptions& options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) override;
|
||||
Status NewIterators(const std::vector<ReadOptions>& read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) override;
|
||||
|
||||
using DBImpl::Put;
|
||||
Status Put(const WriteOptions& /*options*/,
|
||||
|
||||
@@ -20,6 +20,10 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplReadOnly::GetImpl)
|
||||
get_impl_options.merge_operands != nullptr);
|
||||
assert(get_impl_options.column_family);
|
||||
|
||||
// This instance cannot observe writes after one of its snapshots, so the
|
||||
// false result initialized by GetWithMetadata is definitive.
|
||||
get_impl_options.newer_version_present = nullptr;
|
||||
|
||||
Status s;
|
||||
|
||||
if (read_options.timestamp) {
|
||||
@@ -49,7 +53,13 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplReadOnly::GetImpl)
|
||||
|
||||
const Comparator* ucmp = get_impl_options.column_family->GetComparator();
|
||||
assert(ucmp);
|
||||
SequenceNumber snapshot = versions_->LastSequence();
|
||||
// For a batched lazy read (MultiGetEntityLazy), use the SuperVersion and
|
||||
// consistent sequence number the batch already fixed (see
|
||||
// GetImplOptions::lazy_columns_shared_sv); otherwise this read-only
|
||||
// instance's stable current SuperVersion and last sequence.
|
||||
SequenceNumber snapshot = get_impl_options.lazy_columns_shared_sv != nullptr
|
||||
? get_impl_options.lazy_columns_snapshot_seq
|
||||
: versions_->LastSequence();
|
||||
GetWithTimestampReadCallback read_cb(snapshot);
|
||||
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(
|
||||
get_impl_options.column_family);
|
||||
@@ -77,7 +87,10 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplReadOnly::GetImpl)
|
||||
|
||||
// In read-only mode Get(), no super version operation is needed (i.e.
|
||||
// GetAndRefSuperVersion and ReturnAndCleanupSuperVersion)
|
||||
SuperVersion* super_version = cfd->GetSuperVersion();
|
||||
SuperVersion* super_version =
|
||||
get_impl_options.lazy_columns_shared_sv != nullptr
|
||||
? get_impl_options.lazy_columns_shared_sv
|
||||
: cfd->GetSuperVersion();
|
||||
if (read_options.timestamp && read_options.timestamp->size() > 0) {
|
||||
s = FailIfReadCollapsedHistory(cfd, super_version,
|
||||
*(read_options.timestamp));
|
||||
@@ -157,15 +170,19 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplReadOnly::GetImpl)
|
||||
PERF_COUNTER_ADD(get_read_bytes, size);
|
||||
}
|
||||
}
|
||||
if (get_impl_options.lazy_columns_version != nullptr && s.ok()) {
|
||||
// Lazy result (GetEntityLazy / MultiGetEntityLazy): hand back the Version
|
||||
// the entity's blob references must be resolved against (both the
|
||||
// single-key and batched paths).
|
||||
*get_impl_options.lazy_columns_version = super_version->current;
|
||||
}
|
||||
if (get_impl_options.lazy_columns_pin != nullptr && s.ok()) {
|
||||
// Lazy result (GetEntityLazy): hand back the Version the entity's blob
|
||||
// references must be resolved against, and transfer a SuperVersion pin into
|
||||
// the result so it -- and deferred blob reads -- stay valid after this call
|
||||
// (as an iterator's pin does). Read-only borrows the current SuperVersion
|
||||
// without a reference, so the pin takes its own.
|
||||
if (get_impl_options.lazy_columns_version != nullptr) {
|
||||
*get_impl_options.lazy_columns_version = super_version->current;
|
||||
}
|
||||
// Single-key GetEntityLazy: transfer a SuperVersion pin into the result so
|
||||
// it -- and deferred blob reads -- stay valid after this call (as an
|
||||
// iterator's pin does). Read-only borrows the current SuperVersion without
|
||||
// a reference, so the pin takes its own. The batched path passes no pin (it
|
||||
// uses lazy_columns_shared_sv); the batch holds one shared pin per column
|
||||
// family instead.
|
||||
TransferSuperVersionPin(super_version, get_impl_options.lazy_columns_pin);
|
||||
}
|
||||
CO_RETURN s;
|
||||
|
||||
+532
-95
@@ -11,6 +11,7 @@
|
||||
|
||||
#include "db/arena_wrapped_db_iter.h"
|
||||
#include "db/blob/blob_fetcher.h"
|
||||
#include "db/db_impl/db_impl_metadata.h"
|
||||
#include "db/log_reader.h"
|
||||
#include "db/log_writer.h"
|
||||
#include "db/merge_context.h"
|
||||
@@ -22,6 +23,7 @@
|
||||
#include "monitoring/perf_context_imp.h"
|
||||
#include "rocksdb/convenience.h"
|
||||
#include "rocksdb/utilities/options_util.h"
|
||||
#include "util/atomic.h"
|
||||
#include "util/cast_util.h"
|
||||
#include "util/write_batch_util.h"
|
||||
|
||||
@@ -32,12 +34,229 @@ DBImplSecondary::DBImplSecondary(const DBOptions& db_options,
|
||||
std::string secondary_path)
|
||||
: DBImpl(db_options, dbname, false, true, true),
|
||||
secondary_path_(std::move(secondary_path)) {
|
||||
secondary_read_view_reclaimer_.reset(new port::Thread(
|
||||
&DBImplSecondary::ReclaimRetiredSecondaryReadViewsLoop, this));
|
||||
ROCKS_LOG_INFO(immutable_db_options_.info_log,
|
||||
"Opening the db in secondary mode");
|
||||
LogFlush(immutable_db_options_.info_log);
|
||||
}
|
||||
|
||||
DBImplSecondary::~DBImplSecondary() = default;
|
||||
DBImplSecondary::~DBImplSecondary() {
|
||||
ResetSecondaryReadView();
|
||||
StopSecondaryReadViewReclaimer();
|
||||
}
|
||||
|
||||
// REQUIRES: mutex_ not held. CleanupSuperVersion() locks mutex_ to release a
|
||||
// SuperVersion's last reference, so dropping the last reference to a view while
|
||||
// mutex_ is held self-deadlocks on the non-reentrant mutex.
|
||||
DBImplSecondary::SecondaryReadView::~SecondaryReadView() {
|
||||
for (const SuperVersions::value_type& entry : super_versions) {
|
||||
db->CleanupSuperVersion(entry.second);
|
||||
}
|
||||
}
|
||||
|
||||
void DBImplSecondary::PublishSecondaryReadView() {
|
||||
mutex_.AssertHeld();
|
||||
|
||||
if (secondary_read_view_closed_) {
|
||||
// A catch-up that raced ResetSecondaryReadView() must not republish, or the
|
||||
// new view would outlive the drain that close waits on.
|
||||
return;
|
||||
}
|
||||
|
||||
std::shared_ptr<SecondaryReadView> new_view =
|
||||
std::make_shared<SecondaryReadView>(this, versions_->LastSequence());
|
||||
for (ColumnFamilyData* cfd : *versions_->GetColumnFamilySet()) {
|
||||
if (cfd->IsDropped() || !cfd->initialized()) {
|
||||
continue;
|
||||
}
|
||||
SuperVersion* super_version = cfd->GetSuperVersion();
|
||||
if (super_version == nullptr) {
|
||||
continue;
|
||||
}
|
||||
new_view->super_versions.emplace(cfd->GetID(), super_version->Ref());
|
||||
}
|
||||
|
||||
// Load the outgoing view before the store, and keep that reference until it
|
||||
// reaches the retirement queue. The store drops the published reference, so
|
||||
// without this local it could be the last one and ~SecondaryReadView() would
|
||||
// run here, under mutex_. Retiring before the store would leave the view
|
||||
// reachable by readers after it was queued for reclamation.
|
||||
std::shared_ptr<const SecondaryReadView> previous_view =
|
||||
AtomicSharedPtrLoad(&secondary_read_view_, std::memory_order_acquire);
|
||||
AtomicSharedPtrStore(
|
||||
&secondary_read_view_,
|
||||
std::shared_ptr<const SecondaryReadView>(std::move(new_view)),
|
||||
std::memory_order_release);
|
||||
RetireSecondaryReadView(std::move(previous_view));
|
||||
}
|
||||
|
||||
void DBImplSecondary::RetireSecondaryReadView(
|
||||
std::shared_ptr<const SecondaryReadView> read_view) {
|
||||
if (read_view == nullptr) {
|
||||
return;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(retired_secondary_read_views_mutex_);
|
||||
retired_secondary_read_views_.emplace_back(std::move(read_view));
|
||||
}
|
||||
// Publish and close synchronously reclaim after retiring a view. Do not wake
|
||||
// the background reclaimer here: it could take a reader-free view first and
|
||||
// let catch-up scan for obsolete files before releasing the view's
|
||||
// SuperVersions. A reader that makes a retained view reclaimable later wakes
|
||||
// the reclaimer in ReleaseSecondaryReadView().
|
||||
}
|
||||
|
||||
bool DBImplSecondary::HasReclaimableSecondaryReadView() const {
|
||||
for (const auto& read_view : retired_secondary_read_views_) {
|
||||
if (read_view.use_count() == 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DBImplSecondary::AllSecondaryReadViewsReclaimed() const {
|
||||
return retired_secondary_read_views_.empty() &&
|
||||
secondary_read_view_reclaims_in_flight_ == 0;
|
||||
}
|
||||
|
||||
bool DBImplSecondary::ReclaimRetiredSecondaryReadViews() {
|
||||
// Keep moved views alive until after the retirement lock is released. Their
|
||||
// destructors call CleanupSuperVersion(), which can lock mutex_.
|
||||
std::vector<std::shared_ptr<const SecondaryReadView>> reclaimable_views;
|
||||
size_t num_reclaiming;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(retired_secondary_read_views_mutex_);
|
||||
for (auto iter = retired_secondary_read_views_.begin();
|
||||
iter != retired_secondary_read_views_.end();) {
|
||||
// The view is no longer published, so its reference count can only
|
||||
// decrease. A count of one means the retirement queue is the sole owner.
|
||||
if (iter->use_count() == 1) {
|
||||
reclaimable_views.emplace_back(std::move(*iter));
|
||||
iter = retired_secondary_read_views_.erase(iter);
|
||||
} else {
|
||||
++iter;
|
||||
}
|
||||
}
|
||||
num_reclaiming = reclaimable_views.size();
|
||||
secondary_read_view_reclaims_in_flight_ += num_reclaiming;
|
||||
if (num_reclaiming == 0) {
|
||||
return AllSecondaryReadViewsReclaimed();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SYNC_POINT("DBImplSecondary::ReclaimRetiredReadViews:BeforeReclaim");
|
||||
reclaimable_views.clear();
|
||||
bool all_reclaimed;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(retired_secondary_read_views_mutex_);
|
||||
assert(secondary_read_view_reclaims_in_flight_ >= num_reclaiming);
|
||||
secondary_read_view_reclaims_in_flight_ -= num_reclaiming;
|
||||
all_reclaimed = AllSecondaryReadViewsReclaimed();
|
||||
}
|
||||
retired_secondary_read_views_cv_.notify_all();
|
||||
TEST_SYNC_POINT("DBImplSecondary::ReclaimRetiredReadViews:AfterReclaim");
|
||||
return all_reclaimed;
|
||||
}
|
||||
|
||||
void DBImplSecondary::ReclaimRetiredSecondaryReadViewsLoop() {
|
||||
while (true) {
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(retired_secondary_read_views_mutex_);
|
||||
retired_secondary_read_views_cv_.wait(lock, [this]() {
|
||||
if (secondary_read_view_reclaimer_stopping_ &&
|
||||
AllSecondaryReadViewsReclaimed()) {
|
||||
return true;
|
||||
}
|
||||
return HasReclaimableSecondaryReadView();
|
||||
});
|
||||
if (secondary_read_view_reclaimer_stopping_ &&
|
||||
AllSecondaryReadViewsReclaimed()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
TEST_SYNC_POINT(
|
||||
"DBImplSecondary::ReclaimRetiredReadViewsLoop:BeforeReclaim");
|
||||
ReclaimRetiredSecondaryReadViews();
|
||||
}
|
||||
}
|
||||
|
||||
void DBImplSecondary::CleanupRetiredSecondaryReadViews() {
|
||||
ReclaimRetiredSecondaryReadViews();
|
||||
std::unique_lock<std::mutex> lock(retired_secondary_read_views_mutex_);
|
||||
retired_secondary_read_views_cv_.wait(
|
||||
lock, [this]() { return secondary_read_view_reclaims_in_flight_ == 0; });
|
||||
}
|
||||
|
||||
void DBImplSecondary::DrainRetiredSecondaryReadViews() {
|
||||
while (!ReclaimRetiredSecondaryReadViews()) {
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(retired_secondary_read_views_mutex_);
|
||||
TEST_SYNC_POINT("DBImplSecondary::DrainRetiredReadViews:Wait");
|
||||
retired_secondary_read_views_cv_.wait(lock, [this]() {
|
||||
if (AllSecondaryReadViewsReclaimed()) {
|
||||
return true;
|
||||
}
|
||||
return HasReclaimableSecondaryReadView();
|
||||
});
|
||||
}
|
||||
TEST_SYNC_POINT("DBImplSecondary::DrainRetiredReadViews:AfterWait");
|
||||
}
|
||||
}
|
||||
|
||||
void DBImplSecondary::ReleaseSecondaryReadView(
|
||||
std::shared_ptr<const SecondaryReadView>* read_view) {
|
||||
read_view->reset();
|
||||
{
|
||||
// Synchronize the reference-count change with the wait predicate so its
|
||||
// notification cannot be lost.
|
||||
std::lock_guard<std::mutex> lock(retired_secondary_read_views_mutex_);
|
||||
retired_secondary_read_views_cv_.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
void DBImplSecondary::StopSecondaryReadViewReclaimer() {
|
||||
if (secondary_read_view_reclaimer_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(retired_secondary_read_views_mutex_);
|
||||
secondary_read_view_reclaimer_stopping_ = true;
|
||||
}
|
||||
retired_secondary_read_views_cv_.notify_all();
|
||||
TEST_SYNC_POINT("DBImplSecondary::StopSecondaryReadViewReclaimer:BeforeJoin");
|
||||
secondary_read_view_reclaimer_->join();
|
||||
secondary_read_view_reclaimer_.reset();
|
||||
}
|
||||
|
||||
void DBImplSecondary::ResetSecondaryReadView() {
|
||||
std::shared_ptr<const SecondaryReadView> read_view;
|
||||
{
|
||||
// mutex_ makes this read-modify-write atomic against
|
||||
// PublishSecondaryReadView(), which holds it too. Without that, a catch-up
|
||||
// racing close can read the same outgoing view and retire it twice, leaving
|
||||
// two queue entries that each see use_count() == 2 so the drain below never
|
||||
// finishes.
|
||||
InstrumentedMutexLock lock_guard(&mutex_);
|
||||
secondary_read_view_closed_ = true;
|
||||
read_view =
|
||||
AtomicSharedPtrLoad(&secondary_read_view_, std::memory_order_acquire);
|
||||
AtomicSharedPtrStore(&secondary_read_view_,
|
||||
std::shared_ptr<const SecondaryReadView>(),
|
||||
std::memory_order_release);
|
||||
}
|
||||
RetireSecondaryReadView(std::move(read_view));
|
||||
DrainRetiredSecondaryReadViews();
|
||||
}
|
||||
|
||||
Status DBImplSecondary::CloseImpl() {
|
||||
// The view's SuperVersions pin their column families, so release them before
|
||||
// DBImpl::CloseImpl() destroys the VersionSet.
|
||||
ResetSecondaryReadView();
|
||||
StopSecondaryReadViewReclaimer();
|
||||
return DBImpl::CloseImpl();
|
||||
}
|
||||
|
||||
Status DBImplSecondary::Recover(
|
||||
const std::vector<ColumnFamilyDescriptor>& column_families,
|
||||
@@ -521,6 +740,42 @@ void DBImplSecondary::DeleteResolvedRecoveredTransactions() {
|
||||
}
|
||||
}
|
||||
|
||||
void DBImplSecondary::MultiGetWithMetadata(
|
||||
const ReadOptions& options, const size_t num_keys,
|
||||
ColumnFamilyHandle* const* column_families, const Slice* keys,
|
||||
PinnableSlice* values, Status* statuses,
|
||||
MultiGetOutputMetadata* output_metadata, const bool sorted_input) {
|
||||
// The only secondary-specific policy here is rejecting newer-version
|
||||
// tracking for explicit snapshots: catch-up can advance a secondary's view.
|
||||
// The remaining code initializes requested metadata and forwards to the
|
||||
// existing MultiGet implementation.
|
||||
std::vector<std::string>* timestamps = GetOutputTimestamps(output_metadata);
|
||||
if (timestamps != nullptr) {
|
||||
timestamps->resize(num_keys);
|
||||
}
|
||||
std::vector<uint8_t>* newer_version_present =
|
||||
GetOutputNewerVersionPresent(output_metadata);
|
||||
if (newer_version_present != nullptr) {
|
||||
newer_version_present->assign(num_keys, false);
|
||||
if (options.snapshot != nullptr) {
|
||||
const Status s = Status::NotSupported(
|
||||
"MultiGetWithMetadata is not supported in secondary DB mode");
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
statuses[i] = s;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
autovector<ColumnFamilyHandle*, MultiGetContext::MAX_BATCH_SIZE>
|
||||
stack_column_families;
|
||||
std::vector<ColumnFamilyHandle*> heap_column_families;
|
||||
ColumnFamilyHandle** mutable_column_families = MakeMutableCfHandles(
|
||||
column_families, num_keys, &stack_column_families, &heap_column_families);
|
||||
DBImpl::MultiGet(options, num_keys, mutable_column_families, keys, values,
|
||||
timestamps != nullptr ? timestamps->data() : nullptr,
|
||||
statuses, sorted_input);
|
||||
}
|
||||
|
||||
Iterator* DBImplSecondary::NewIterator(const ReadOptions& _read_options,
|
||||
ColumnFamilyHandle* column_family) {
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
@@ -585,8 +840,9 @@ ArenaWrappedDBIter* DBImplSecondary::NewIteratorImpl(
|
||||
SuperVersion* super_version, SequenceNumber snapshot,
|
||||
ReadCallback* read_callback, bool expose_blob_index, bool allow_refresh) {
|
||||
assert(nullptr != cfh);
|
||||
assert(snapshot == kMaxSequenceNumber);
|
||||
snapshot = versions_->LastSequence();
|
||||
if (snapshot == kMaxSequenceNumber) {
|
||||
snapshot = versions_->LastSequence();
|
||||
}
|
||||
assert(snapshot != kMaxSequenceNumber);
|
||||
return NewArenaWrappedDbIterator(env_, read_options, cfh, super_version,
|
||||
snapshot, read_callback, this,
|
||||
@@ -598,76 +854,165 @@ Status DBImplSecondary::NewIterators(
|
||||
const ReadOptions& _read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) {
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kDBIterator) {
|
||||
if (column_families.empty()) {
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kDBIterator) {
|
||||
return Status::InvalidArgument(
|
||||
"Can only call NewIterators with `ReadOptions::io_activity` is "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kDBIterator`");
|
||||
}
|
||||
if (_read_options.read_tier == kPersistedTier) {
|
||||
return Status::NotSupported(
|
||||
"ReadTier::kPersistedData is not yet supported in iterators.");
|
||||
}
|
||||
if (_read_options.tailing) {
|
||||
return Status::NotSupported(
|
||||
"tailing iterator not supported in secondary mode");
|
||||
}
|
||||
if (_read_options.snapshot != nullptr) {
|
||||
return Status::NotSupported("snapshot not supported in secondary mode");
|
||||
}
|
||||
}
|
||||
return NewIteratorsImpl(
|
||||
std::vector<ReadOptions>(column_families.size(), _read_options),
|
||||
column_families, iterators);
|
||||
}
|
||||
|
||||
Status DBImplSecondary::NewIterators(
|
||||
const std::vector<ReadOptions>& read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) {
|
||||
return NewIteratorsImpl(read_options, column_families, iterators);
|
||||
}
|
||||
|
||||
Status DBImplSecondary::NewIteratorsImpl(
|
||||
const std::vector<ReadOptions>& read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) {
|
||||
if (read_options.size() != column_families.size()) {
|
||||
return Status::InvalidArgument(
|
||||
"Can only call NewIterators with `ReadOptions::io_activity` is "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kDBIterator`");
|
||||
"read_options and column_families must have the same size");
|
||||
}
|
||||
ReadOptions read_options(_read_options);
|
||||
if (read_options.io_activity == Env::IOActivity::kUnknown) {
|
||||
read_options.io_activity = Env::IOActivity::kDBIterator;
|
||||
}
|
||||
if (read_options.read_tier == kPersistedTier) {
|
||||
return Status::NotSupported(
|
||||
"ReadTier::kPersistedData is not yet supported in iterators.");
|
||||
}
|
||||
ReadCallback* read_callback = nullptr; // No read callback provided.
|
||||
if (iterators == nullptr) {
|
||||
return Status::InvalidArgument("iterators not allowed to be nullptr");
|
||||
}
|
||||
|
||||
if (read_options.timestamp) {
|
||||
for (auto* cf : column_families) {
|
||||
assert(cf);
|
||||
const Status s = FailIfTsMismatchCf(cf, *(read_options.timestamp));
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
if (column_families.empty()) {
|
||||
iterators->clear();
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
const Snapshot* const snapshot = read_options.front().snapshot;
|
||||
const bool tailing = read_options.front().tailing;
|
||||
std::vector<ReadOptions> normalized_read_options;
|
||||
normalized_read_options.reserve(read_options.size());
|
||||
for (size_t i = 0; i < read_options.size(); ++i) {
|
||||
const ReadOptions& options = read_options[i];
|
||||
if (options.io_activity != Env::IOActivity::kUnknown &&
|
||||
options.io_activity != Env::IOActivity::kDBIterator) {
|
||||
return Status::InvalidArgument(
|
||||
"Can only call NewIterators with `ReadOptions::io_activity` is "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kDBIterator`");
|
||||
}
|
||||
} else {
|
||||
for (auto* cf : column_families) {
|
||||
assert(cf);
|
||||
const Status s = FailIfCfHasTs(cf);
|
||||
if (options.read_tier == kPersistedTier) {
|
||||
return Status::NotSupported(
|
||||
"ReadTier::kPersistedData is not yet supported in iterators.");
|
||||
}
|
||||
if (options.snapshot != snapshot) {
|
||||
return Status::InvalidArgument(
|
||||
"All ReadOptions must use the same snapshot");
|
||||
}
|
||||
if (options.tailing != tailing) {
|
||||
return Status::InvalidArgument(
|
||||
"All ReadOptions must use the same tailing setting");
|
||||
}
|
||||
if (options.tailing) {
|
||||
return Status::NotSupported(
|
||||
"tailing iterator not supported in secondary mode");
|
||||
}
|
||||
if (options.snapshot != nullptr) {
|
||||
return Status::NotSupported("snapshot not supported in secondary mode");
|
||||
}
|
||||
|
||||
auto* cf = column_families[i];
|
||||
assert(cf);
|
||||
Status s;
|
||||
if (options.timestamp) {
|
||||
s = FailIfTsMismatchCf(cf, *(options.timestamp));
|
||||
} else {
|
||||
s = FailIfCfHasTs(cf);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
normalized_read_options.emplace_back(options);
|
||||
if (normalized_read_options.back().io_activity ==
|
||||
Env::IOActivity::kUnknown) {
|
||||
normalized_read_options.back().io_activity = Env::IOActivity::kDBIterator;
|
||||
}
|
||||
}
|
||||
|
||||
iterators->clear();
|
||||
iterators->reserve(column_families.size());
|
||||
|
||||
std::shared_ptr<const SecondaryReadView> read_view =
|
||||
AtomicSharedPtrLoad(&secondary_read_view_, std::memory_order_acquire);
|
||||
if (read_view == nullptr) {
|
||||
return Status::InvalidArgument("Secondary read view is not initialized");
|
||||
}
|
||||
TEST_SYNC_POINT("DBImplSecondary::NewIterators:AfterLoadReadView");
|
||||
|
||||
const SequenceNumber read_seq = read_view->sequence;
|
||||
autovector<ColumnFamilySuperVersionPair, MultiGetContext::MAX_BATCH_SIZE>
|
||||
cf_sv_pairs;
|
||||
const auto cleanup_super_versions = [&]() {
|
||||
for (const auto& cf_sv_pair : cf_sv_pairs) {
|
||||
CleanupSuperVersion(cf_sv_pair.super_version);
|
||||
}
|
||||
};
|
||||
for (ColumnFamilyHandle* cf : column_families) {
|
||||
ColumnFamilyHandleImpl* cfh =
|
||||
static_cast_with_check<ColumnFamilyHandleImpl>(cf);
|
||||
const SecondaryReadView::SuperVersions::const_iterator sv_iter =
|
||||
read_view->super_versions.find(cfh->GetID());
|
||||
if (sv_iter == read_view->super_versions.end()) {
|
||||
if (!cfh->cfd()->IsDropped()) {
|
||||
cleanup_super_versions();
|
||||
ReleaseSecondaryReadView(&read_view);
|
||||
return Status::InvalidArgument(
|
||||
"Column family is not part of the secondary read view");
|
||||
}
|
||||
// A retained handle remains readable after its column family is dropped.
|
||||
// Its immutable final SuperVersion is intentionally not published, since
|
||||
// doing so would keep the dropped column family alive indefinitely.
|
||||
cf_sv_pairs.emplace_back(cfh,
|
||||
cfh->cfd()->GetReferencedSuperVersion(this));
|
||||
} else {
|
||||
cf_sv_pairs.emplace_back(cfh, sv_iter->second->Ref());
|
||||
}
|
||||
}
|
||||
ReleaseSecondaryReadView(&read_view);
|
||||
TEST_SYNC_POINT("DBImplSecondary::NewIterators:AfterReleaseReadView");
|
||||
for (size_t i = 0; i < cf_sv_pairs.size(); ++i) {
|
||||
const auto& cf_sv_pair = cf_sv_pairs[i];
|
||||
const ReadOptions& options = normalized_read_options[i];
|
||||
if (options.timestamp && !options.timestamp->empty()) {
|
||||
const Status s = FailIfReadCollapsedHistory(
|
||||
cf_sv_pair.cfd, cf_sv_pair.super_version, *(options.timestamp));
|
||||
if (!s.ok()) {
|
||||
cleanup_super_versions();
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
iterators->clear();
|
||||
iterators->reserve(column_families.size());
|
||||
if (read_options.tailing) {
|
||||
return Status::NotSupported(
|
||||
"tailing iterator not supported in secondary mode");
|
||||
} else if (read_options.snapshot != nullptr) {
|
||||
// TODO (yanqin) support snapshot.
|
||||
return Status::NotSupported("snapshot not supported in secondary mode");
|
||||
} else {
|
||||
SequenceNumber read_seq(kMaxSequenceNumber);
|
||||
autovector<std::tuple<ColumnFamilyHandleImpl*, SuperVersion*>> cfh_to_sv;
|
||||
const bool check_read_ts =
|
||||
read_options.timestamp && read_options.timestamp->size() > 0;
|
||||
for (auto cf : column_families) {
|
||||
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(cf);
|
||||
auto cfd = cfh->cfd();
|
||||
SuperVersion* sv = cfd->GetReferencedSuperVersion(this);
|
||||
cfh_to_sv.emplace_back(cfh, sv);
|
||||
if (check_read_ts) {
|
||||
const Status s =
|
||||
FailIfReadCollapsedHistory(cfd, sv, *(read_options.timestamp));
|
||||
if (!s.ok()) {
|
||||
for (auto prev_entry : cfh_to_sv) {
|
||||
CleanupSuperVersion(std::get<1>(prev_entry));
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert(cfh_to_sv.size() == column_families.size());
|
||||
for (auto [cfh, sv] : cfh_to_sv) {
|
||||
iterators->push_back(
|
||||
NewIteratorImpl(read_options, cfh, sv, read_seq, read_callback));
|
||||
}
|
||||
for (size_t i = 0; i < cf_sv_pairs.size(); ++i) {
|
||||
const auto& cf_sv_pair = cf_sv_pairs[i];
|
||||
iterators->push_back(NewIteratorImpl(
|
||||
normalized_read_options[i], cf_sv_pair.cfh, cf_sv_pair.super_version,
|
||||
read_seq, nullptr /* read_callback */, false /* expose_blob_index */,
|
||||
false /* allow_refresh */));
|
||||
TEST_SYNC_POINT("DBImplSecondary::NewIterators:AfterCreateIterator");
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
@@ -776,8 +1121,14 @@ Status DBImplSecondary::TryCatchUpWithPrimary() {
|
||||
// completions; an outstanding prepare stops the scan.
|
||||
(void)logs_with_prep_tracker_.FindMinLogContainingOutstandingPrep();
|
||||
}
|
||||
TEST_SYNC_POINT(
|
||||
"DBImplSecondary::TryCatchUpWithPrimary:BeforePublishReadView");
|
||||
TEST_SYNC_POINT(
|
||||
"DBImplSecondary::TryCatchUpWithPrimary:AllowPublishReadView");
|
||||
PublishSecondaryReadView();
|
||||
}
|
||||
}
|
||||
CleanupRetiredSecondaryReadViews();
|
||||
job_context.Clean();
|
||||
|
||||
// Cleanup unused, obsolete files.
|
||||
@@ -832,6 +1183,24 @@ Status DBImplSecondary::OpenAsSecondaryImpl(
|
||||
std::vector<ColumnFamilyHandle*>* handles, std::unique_ptr<DB>* dbptr,
|
||||
bool recover_wal, bool trust_manifest_recovery) {
|
||||
*dbptr = nullptr;
|
||||
// Only normal DB::OpenAsSecondary recovery can defer SST opening.
|
||||
// `recover_wal` excludes OpenAndCompact even when no snapshot floor is
|
||||
// supplied, `open_files_async` is the explicit opt-in, and trust-MANIFEST
|
||||
// recovery must only open its protected compaction inputs.
|
||||
const bool defer_sst_file_opening =
|
||||
recover_wal && db_options.open_files_async && !trust_manifest_recovery;
|
||||
if (defer_sst_file_opening && !db_options.skip_stats_update_on_db_open) {
|
||||
return Status::InvalidArgument(
|
||||
"open_files_async requires skip_stats_update_on_db_open = true.");
|
||||
}
|
||||
if (defer_sst_file_opening) {
|
||||
for (const ColumnFamilyDescriptor& column_family : column_families) {
|
||||
if (column_family.options.compaction_style == kCompactionStyleFIFO) {
|
||||
return Status::NotSupported(
|
||||
"FIFO compaction is not supported with open_files_async = true.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DBOptions tmp_opts(db_options);
|
||||
Status s;
|
||||
@@ -844,7 +1213,7 @@ Status DBImplSecondary::OpenAsSecondaryImpl(
|
||||
}
|
||||
|
||||
assert(tmp_opts.info_log != nullptr);
|
||||
if (db_options.max_open_files != -1) {
|
||||
if (db_options.max_open_files != -1 || defer_sst_file_opening) {
|
||||
std::ostringstream oss;
|
||||
oss << "The primary instance may delete all types of files after they "
|
||||
"become obsolete. The application can coordinate the primary and "
|
||||
@@ -852,15 +1221,24 @@ Status DBImplSecondary::OpenAsSecondaryImpl(
|
||||
"currently being used by the secondary. Alternatively, a custom "
|
||||
"Env/FS can be provided such that files become inaccessible only "
|
||||
"after all primary and secondaries indicate that they are obsolete "
|
||||
"and deleted. If the above two are not possible, you can open the "
|
||||
"secondary instance with `max_open_files==-1` so that secondary "
|
||||
"will eagerly keep all table files open. Even if a file is deleted, "
|
||||
"its content can still be accessed via a prior open file "
|
||||
"descriptor. This is a hacky workaround for only table files. If "
|
||||
"none of the above is done, then point lookup or "
|
||||
"and deleted. ";
|
||||
if (defer_sst_file_opening) {
|
||||
oss << "With `open_files_async=true`, `max_open_files==-1` does not pin "
|
||||
"all table files before OpenAsSecondary returns. ";
|
||||
} else {
|
||||
oss << "If the above two are not possible, you can open the secondary "
|
||||
"instance with `max_open_files==-1` so that secondary will "
|
||||
"eagerly keep all table files open. Even if a file is deleted, "
|
||||
"its content can still be accessed via a prior open file "
|
||||
"descriptor. This is a hacky workaround for only table files. ";
|
||||
}
|
||||
oss << "If none of the above is done, then point lookup or "
|
||||
"range scan via the secondary instance can result in IOError: file "
|
||||
"not found. This can be resolved by retrying "
|
||||
"TryCatchUpWithPrimary().";
|
||||
"not found. TryCatchUpWithPrimary() can resolve a transient, "
|
||||
"non-latched error if a newer primary version no longer references "
|
||||
"the missing file. A latched asynchronous-open background error "
|
||||
"requires closing and reopening the secondary after fixing the "
|
||||
"underlying problem.";
|
||||
ROCKS_LOG_WARN(tmp_opts.info_log, "%s", oss.str().c_str());
|
||||
}
|
||||
|
||||
@@ -870,9 +1248,10 @@ Status DBImplSecondary::OpenAsSecondaryImpl(
|
||||
dbname, &impl->immutable_db_options_, impl->mutable_db_options_,
|
||||
impl->file_options_, impl->table_cache_.get(),
|
||||
impl->write_buffer_manager_, &impl->write_controller_, impl->io_tracer_,
|
||||
impl->db_id_, impl->db_session_id_));
|
||||
static_cast_with_check<ReactiveVersionSet>(impl->versions_.get())
|
||||
->SetTrustManifestRecovery(trust_manifest_recovery);
|
||||
impl->db_id_, impl->db_session_id_, defer_sst_file_opening));
|
||||
ReactiveVersionSet* reactive_versions =
|
||||
static_cast_with_check<ReactiveVersionSet>(impl->versions_.get());
|
||||
reactive_versions->SetTrustManifestRecovery(trust_manifest_recovery);
|
||||
impl->column_family_memtables_.reset(
|
||||
new ColumnFamilyMemTablesImpl(impl->versions_->GetColumnFamilySet()));
|
||||
impl->wal_in_db_path_ = impl->immutable_db_options_.IsWalDirSameAsDBPath();
|
||||
@@ -910,9 +1289,15 @@ Status DBImplSecondary::OpenAsSecondaryImpl(
|
||||
sv_context.NewSuperVersion();
|
||||
cfd->InstallSuperVersion(&sv_context, &impl->mutex_);
|
||||
}
|
||||
impl->MarkAsyncFileOpenNotNeeded();
|
||||
impl->PublishSecondaryReadView();
|
||||
if (defer_sst_file_opening) {
|
||||
impl->ScheduleAsyncFileOpening();
|
||||
} else {
|
||||
impl->MarkAsyncFileOpenNotNeeded();
|
||||
}
|
||||
}
|
||||
impl->mutex_.Unlock();
|
||||
impl->CleanupRetiredSecondaryReadViews();
|
||||
sv_context.Clean();
|
||||
job_context.Clean();
|
||||
if (s.ok()) {
|
||||
@@ -1186,27 +1571,48 @@ Status DBImplSecondary::CleanupPhysicalCompactionOutputFiles(
|
||||
}
|
||||
|
||||
Status DBImplSecondary::InitializeCompactionWorkspace(
|
||||
bool allow_resumption, std::unique_ptr<FSDirectory>* output_dir,
|
||||
bool allow_resumption, bool resumption_requested,
|
||||
std::unique_ptr<FSDirectory>* output_dir,
|
||||
std::unique_ptr<log::Writer>* compaction_progress_writer) {
|
||||
// Create output directory if it doest exist yet
|
||||
Status s = CreateAndNewDirectory(fs_.get(), secondary_path_, output_dir);
|
||||
if (!s.ok() || !allow_resumption) {
|
||||
return s;
|
||||
}
|
||||
|
||||
s = PrepareCompactionProgressState();
|
||||
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
s = FinalizeCompactionProgressWriter(compaction_progress_writer);
|
||||
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
if (allow_resumption) {
|
||||
s = PrepareCompactionProgressState();
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
return FinalizeCompactionProgressWriter(compaction_progress_writer);
|
||||
}
|
||||
|
||||
return Status::OK();
|
||||
if (resumption_requested) {
|
||||
// Resumption was requested by the caller but has been disabled internally
|
||||
// (see CompactWithoutInstallation: incompatible with output hash
|
||||
// verification). The caller therefore did not empty output_directory (it
|
||||
// expects the resumption path to own that state), so honor the
|
||||
// OpenAndCompactOptions::allow_resumption=true fallback contract by
|
||||
// cleaning any leftover progress and output files here, starting the fresh
|
||||
// compaction from a clean directory. Without this, output files left by a
|
||||
// previously interrupted attempt collide with the file numbers the fresh
|
||||
// compaction reuses.
|
||||
CompactionProgressFilesScan scan_result;
|
||||
s = ScanCompactionProgressFiles(&scan_result);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
s = CleanupOldAndTemporaryCompactionProgressFiles(
|
||||
/*preserve_latest=*/false, scan_result);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
s = HandleInvalidOrNoCompactionProgress(
|
||||
/*compaction_progress_file_path=*/std::nullopt, scan_result);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
// PrepareCompactionProgressState() manages compaction progress files and output
|
||||
@@ -1434,8 +1840,9 @@ Status DBImplSecondary::CompactWithoutInstallation(
|
||||
|
||||
mutex_.Unlock();
|
||||
|
||||
s = InitializeCompactionWorkspace(allow_resumption, &output_dir,
|
||||
&compaction_progress_writer);
|
||||
s = InitializeCompactionWorkspace(
|
||||
allow_resumption, /*resumption_requested=*/options.allow_resumption,
|
||||
&output_dir, &compaction_progress_writer);
|
||||
|
||||
mutex_.Lock();
|
||||
|
||||
@@ -1623,6 +2030,10 @@ Status DB::OpenAndCompact(
|
||||
db_options.compaction_service = nullptr;
|
||||
db_options.info_log = override_options.info_log;
|
||||
|
||||
const std::string output_path = RemoteCompactionJobDir(
|
||||
name, db_options.use_session_tmp_dir_for_remote_compaction,
|
||||
output_directory);
|
||||
|
||||
// 4. Filter CFs that are needed for OpenAndCompact()
|
||||
// We do not need to open all column families for the remote compaction.
|
||||
// Only open default CF + target CF. If target CF == default CF, we will open
|
||||
@@ -1674,10 +2085,35 @@ Status DB::OpenAndCompact(
|
||||
std::unique_ptr<DB> db;
|
||||
std::vector<ColumnFamilyHandle*> handles;
|
||||
const uint64_t db_open_start_micros = db_options.env->NowMicros();
|
||||
s = DBImplSecondary::OpenAsSecondaryImpl(
|
||||
db_options, name, output_directory, column_families, &handles, &db,
|
||||
/*recover_wal=*/false,
|
||||
/*trust_manifest_recovery=*/floor_provided);
|
||||
// This opens the *live* primary's directory read-only, and the primary keeps
|
||||
// rotating its MANIFEST underneath us: each rotation writes a new MANIFEST
|
||||
// and renames a freshly written CURRENT over the old one. POSIX keeps an
|
||||
// already-opened CURRENT/MANIFEST readable after such a replacement, but
|
||||
// filesystems without read-after-unlink semantics (remote or object stores)
|
||||
// report the replaced object as gone instead (Status::PathNotFound /
|
||||
// NotFound, or Status::TryAgain from
|
||||
// ReactiveVersionSet::MaybeSwitchManifest for the MANIFEST equivalent). That
|
||||
// is a transient race, not a broken DB: the next attempt reads the
|
||||
// replacement. Retry a bounded number of times so a concurrent rotation does
|
||||
// not fail the compaction -- a CompactionService that does not fall back to a
|
||||
// local compaction turns such a failure into a primary background error.
|
||||
for (uint32_t retry_count = 0;; ++retry_count) {
|
||||
s = DBImplSecondary::OpenAsSecondaryImpl(
|
||||
db_options, name, output_path, column_families, &handles, &db,
|
||||
/*recover_wal=*/false,
|
||||
/*trust_manifest_recovery=*/floor_provided);
|
||||
if (s.ok() || !(s.IsTryAgain() || s.IsPathNotFound() || s.IsNotFound()) ||
|
||||
retry_count == options.max_secondary_open_retries) {
|
||||
break;
|
||||
}
|
||||
ROCKS_LOG_WARN(db_options.info_log,
|
||||
"OpenAndCompact: secondary open of %s failed with %s "
|
||||
"(retry %" PRIu32 " of %" PRIu32
|
||||
"); the primary may have replaced "
|
||||
"CURRENT/MANIFEST concurrently, retrying",
|
||||
name.c_str(), s.ToString().c_str(), retry_count + 1,
|
||||
options.max_secondary_open_retries);
|
||||
}
|
||||
RecordTimeToHistogram(db_options.statistics.get(),
|
||||
OPEN_AND_COMPACT_DB_OPEN_MICROS,
|
||||
db_options.env->NowMicros() - db_open_start_micros);
|
||||
@@ -1751,7 +2187,7 @@ Status DB::OpenAndCompact(
|
||||
assert(cfh);
|
||||
|
||||
// 7. Run the compaction without installation.
|
||||
// Output will be stored in the directory specified by output_directory
|
||||
// Output will be stored under the DB-owned staging directory.
|
||||
CompactionServiceResult compaction_result;
|
||||
DBImplSecondary* db_secondary =
|
||||
static_cast_with_check<DBImplSecondary>(db.get());
|
||||
@@ -1830,8 +2266,9 @@ Status DBImplSecondary::PersistInitialCompactionProgress(
|
||||
return s;
|
||||
}
|
||||
|
||||
s = compaction_progress_writer->file()->Sync(opts,
|
||||
immutable_db_options_.use_fsync);
|
||||
s = immutable_db_options_.use_fsync
|
||||
? compaction_progress_writer->file()->Fsync(opts)
|
||||
: compaction_progress_writer->file()->Sync(opts);
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -5,14 +5,19 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <condition_variable>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "logging/logging.h"
|
||||
#include "port/port.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
@@ -104,6 +109,14 @@ class DBImplSecondary : public DBImpl {
|
||||
const Slice& key,
|
||||
GetImplOptions& get_impl_options);
|
||||
|
||||
using DBImpl::MultiGetWithMetadata;
|
||||
void MultiGetWithMetadata(const ReadOptions& options, const size_t num_keys,
|
||||
ColumnFamilyHandle* const* column_families,
|
||||
const Slice* keys, PinnableSlice* values,
|
||||
Status* statuses,
|
||||
MultiGetOutputMetadata* output_metadata,
|
||||
const bool sorted_input = false) override;
|
||||
|
||||
using DBImpl::NewIterator;
|
||||
// Operations on the created iterators can return IOError due to files being
|
||||
// deleted by the primary. To avoid IOError in this case, application can
|
||||
@@ -127,6 +140,9 @@ class DBImplSecondary : public DBImpl {
|
||||
Status NewIterators(const ReadOptions& _read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) override;
|
||||
Status NewIterators(const std::vector<ReadOptions>& read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) override;
|
||||
|
||||
using DBImpl::Put;
|
||||
Status Put(const WriteOptions& /*options*/,
|
||||
@@ -277,6 +293,8 @@ class DBImplSecondary : public DBImpl {
|
||||
#endif // NDEBUG
|
||||
|
||||
protected:
|
||||
Status CloseImpl() override;
|
||||
|
||||
Status FlushForGetLiveFiles(bool /*force_atomic_flush*/) override {
|
||||
// No-op for read-only DB
|
||||
return Status::OK();
|
||||
@@ -311,9 +329,55 @@ class DBImplSecondary : public DBImpl {
|
||||
std::unique_ptr<log::Reader::Reporter> manifest_reporter_;
|
||||
std::unique_ptr<Status> manifest_reader_status_;
|
||||
|
||||
// Publishes a view of every initialized, non-dropped column family while
|
||||
// mutex_ protects their SuperVersions. The previous view is retained for
|
||||
// cleanup outside mutex_ and outside reader threads. No-op once the view has
|
||||
// been torn down for close.
|
||||
// REQUIRES: mutex_ held
|
||||
void PublishSecondaryReadView();
|
||||
|
||||
// Reclaims retired views that have no readers. This must be called without
|
||||
// mutex_ because releasing a view's final SuperVersion can lock it.
|
||||
void CleanupRetiredSecondaryReadViews();
|
||||
|
||||
private:
|
||||
friend class DB;
|
||||
|
||||
struct SecondaryReadView {
|
||||
using SuperVersions = std::unordered_map<uint32_t, SuperVersion*>;
|
||||
|
||||
SecondaryReadView(DBImplSecondary* db_arg, SequenceNumber sequence_arg)
|
||||
: db(db_arg), sequence(sequence_arg) {}
|
||||
~SecondaryReadView();
|
||||
SecondaryReadView(const SecondaryReadView&) = delete;
|
||||
SecondaryReadView& operator=(const SecondaryReadView&) = delete;
|
||||
SecondaryReadView(SecondaryReadView&&) = delete;
|
||||
SecondaryReadView& operator=(SecondaryReadView&&) = delete;
|
||||
|
||||
DBImplSecondary* db;
|
||||
SequenceNumber sequence;
|
||||
SuperVersions super_versions;
|
||||
};
|
||||
|
||||
void RetireSecondaryReadView(
|
||||
std::shared_ptr<const SecondaryReadView> read_view);
|
||||
// REQUIRES: retired_secondary_read_views_mutex_ held
|
||||
bool HasReclaimableSecondaryReadView() const;
|
||||
// REQUIRES: retired_secondary_read_views_mutex_ held
|
||||
bool AllSecondaryReadViewsReclaimed() const;
|
||||
bool ReclaimRetiredSecondaryReadViews();
|
||||
void ReclaimRetiredSecondaryReadViewsLoop();
|
||||
void DrainRetiredSecondaryReadViews();
|
||||
void StopSecondaryReadViewReclaimer();
|
||||
void ReleaseSecondaryReadView(
|
||||
std::shared_ptr<const SecondaryReadView>* read_view);
|
||||
void ResetSecondaryReadView();
|
||||
|
||||
Status NewIteratorsImpl(
|
||||
const std::vector<ReadOptions>& read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators);
|
||||
|
||||
// No copying allowed
|
||||
DBImplSecondary(const DBImplSecondary&);
|
||||
void operator=(const DBImplSecondary&);
|
||||
@@ -420,7 +484,8 @@ class DBImplSecondary : public DBImpl {
|
||||
};
|
||||
|
||||
Status InitializeCompactionWorkspace(
|
||||
bool allow_resumption, std::unique_ptr<FSDirectory>* output_dir,
|
||||
bool allow_resumption, bool resumption_requested,
|
||||
std::unique_ptr<FSDirectory>* output_dir,
|
||||
std::unique_ptr<log::Writer>* compaction_progress_writer);
|
||||
|
||||
Status PrepareCompactionProgressState();
|
||||
@@ -502,6 +567,22 @@ class DBImplSecondary : public DBImpl {
|
||||
std::unordered_map<uint32_t, std::pair<uint64_t, int>>
|
||||
cf_id_to_retention_warning_;
|
||||
|
||||
// Mutated only under mutex_, so that load-then-store stays atomic between
|
||||
// publication and teardown. Readers load it without mutex_, so every access
|
||||
// must still go through AtomicSharedPtrLoad/Store.
|
||||
std::shared_ptr<const SecondaryReadView> secondary_read_view_;
|
||||
// Set by ResetSecondaryReadView() so a later catch-up cannot publish a view
|
||||
// that would outlive the drain close waits on.
|
||||
// Guarded by mutex_, which every mutation of secondary_read_view_ holds.
|
||||
bool secondary_read_view_closed_ = false;
|
||||
std::mutex retired_secondary_read_views_mutex_;
|
||||
std::condition_variable retired_secondary_read_views_cv_;
|
||||
std::vector<std::shared_ptr<const SecondaryReadView>>
|
||||
retired_secondary_read_views_;
|
||||
size_t secondary_read_view_reclaims_in_flight_ = 0;
|
||||
bool secondary_read_view_reclaimer_stopping_ = false;
|
||||
std::unique_ptr<port::Thread> secondary_read_view_reclaimer_;
|
||||
|
||||
const std::string secondary_path_;
|
||||
|
||||
CompactionProgress compaction_progress_;
|
||||
|
||||
@@ -19,6 +19,14 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplSecondary::GetImpl)
|
||||
get_impl_options.merge_operands != nullptr);
|
||||
assert(get_impl_options.column_family);
|
||||
|
||||
if (get_impl_options.newer_version_present != nullptr) {
|
||||
if (read_options.snapshot != nullptr) {
|
||||
CO_RETURN Status::NotSupported(
|
||||
"GetWithMetadata is not supported in secondary DB mode");
|
||||
}
|
||||
get_impl_options.newer_version_present = nullptr;
|
||||
}
|
||||
|
||||
Status s;
|
||||
|
||||
if (read_options.timestamp) {
|
||||
@@ -48,7 +56,12 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplSecondary::GetImpl)
|
||||
|
||||
const Comparator* ucmp = get_impl_options.column_family->GetComparator();
|
||||
assert(ucmp);
|
||||
SequenceNumber snapshot = versions_->LastSequence();
|
||||
// For a batched lazy read (MultiGetEntityLazy) use the sequence number the
|
||||
// batch fixed (see GetImplOptions::lazy_columns_shared_sv); otherwise this
|
||||
// instance's last sequence.
|
||||
SequenceNumber snapshot = get_impl_options.lazy_columns_shared_sv != nullptr
|
||||
? get_impl_options.lazy_columns_snapshot_seq
|
||||
: versions_->LastSequence();
|
||||
GetWithTimestampReadCallback read_cb(snapshot);
|
||||
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(
|
||||
get_impl_options.column_family);
|
||||
@@ -74,21 +87,32 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplSecondary::GetImpl)
|
||||
}
|
||||
}
|
||||
|
||||
// Acquire SuperVersion
|
||||
// Acquire SuperVersion, or use the batch's shared one for a batched lazy read
|
||||
// (see GetImplOptions::lazy_columns_shared_sv), skipping the per-key
|
||||
// reference and its release.
|
||||
const bool own_super_version =
|
||||
get_impl_options.lazy_columns_shared_sv == nullptr;
|
||||
SuperVersion* super_version;
|
||||
if (own_super_version) {
|
||||
#if defined(WITH_COROUTINES)
|
||||
SuperVersion* super_version = cfd->GetReferencedSuperVersion(this);
|
||||
super_version = cfd->GetReferencedSuperVersion(this);
|
||||
#else
|
||||
SuperVersion* super_version = GetAndRefSuperVersion(cfd);
|
||||
super_version = GetAndRefSuperVersion(cfd);
|
||||
#endif
|
||||
} else {
|
||||
super_version = get_impl_options.lazy_columns_shared_sv;
|
||||
}
|
||||
if (read_options.timestamp && read_options.timestamp->size() > 0) {
|
||||
s = FailIfReadCollapsedHistory(cfd, super_version,
|
||||
*(read_options.timestamp));
|
||||
if (!s.ok()) {
|
||||
if (own_super_version) {
|
||||
#if defined(WITH_COROUTINES)
|
||||
CleanupSuperVersion(super_version);
|
||||
CleanupSuperVersion(super_version);
|
||||
#else
|
||||
ReturnAndCleanupSuperVersion(cfd, super_version);
|
||||
ReturnAndCleanupSuperVersion(cfd, super_version);
|
||||
#endif
|
||||
}
|
||||
CO_RETURN s;
|
||||
}
|
||||
}
|
||||
@@ -169,11 +193,13 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplSecondary::GetImpl)
|
||||
}
|
||||
if (!s.ok() && !s.IsMergeInProgress() && !s.IsNotFound()) {
|
||||
assert(done);
|
||||
if (own_super_version) {
|
||||
#if defined(WITH_COROUTINES)
|
||||
CleanupSuperVersion(super_version);
|
||||
CleanupSuperVersion(super_version);
|
||||
#else
|
||||
ReturnAndCleanupSuperVersion(cfd, super_version);
|
||||
ReturnAndCleanupSuperVersion(cfd, super_version);
|
||||
#endif
|
||||
}
|
||||
CO_RETURN s;
|
||||
}
|
||||
if (!done) {
|
||||
@@ -191,21 +217,27 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplSecondary::GetImpl)
|
||||
}
|
||||
{
|
||||
PERF_TIMER_GUARD(get_post_process_time);
|
||||
if (get_impl_options.lazy_columns_version != nullptr && s.ok()) {
|
||||
// Lazy result (GetEntityLazy / MultiGetEntityLazy): hand back the Version
|
||||
// the entity's blob references must be resolved against (both the
|
||||
// single-key and batched paths).
|
||||
*get_impl_options.lazy_columns_version = super_version->current;
|
||||
}
|
||||
if (get_impl_options.lazy_columns_pin != nullptr && s.ok()) {
|
||||
// Lazy result (GetEntityLazy): hand back the resolution Version and take
|
||||
// a SuperVersion pin for the result before the borrowed reference below
|
||||
// is released, so the result -- and deferred blob reads -- stay valid
|
||||
// after this call (as an iterator's pin does).
|
||||
if (get_impl_options.lazy_columns_version != nullptr) {
|
||||
*get_impl_options.lazy_columns_version = super_version->current;
|
||||
}
|
||||
// Single-key GetEntityLazy: take a SuperVersion pin for the result before
|
||||
// the borrowed reference below is released, so the result -- and deferred
|
||||
// blob reads -- stay valid after this call (as an iterator's pin does).
|
||||
// The batched path passes no pin (it uses lazy_columns_shared_sv); the
|
||||
// batch holds one shared pin per column family instead.
|
||||
TransferSuperVersionPin(super_version, get_impl_options.lazy_columns_pin);
|
||||
}
|
||||
if (own_super_version) {
|
||||
#if defined(WITH_COROUTINES)
|
||||
CleanupSuperVersion(super_version);
|
||||
CleanupSuperVersion(super_version);
|
||||
#else
|
||||
ReturnAndCleanupSuperVersion(cfd, super_version);
|
||||
ReturnAndCleanupSuperVersion(cfd, super_version);
|
||||
#endif
|
||||
}
|
||||
RecordTick(stats_, NUMBER_KEYS_READ);
|
||||
size_t size = 0;
|
||||
// Mirror DBImpl::GetImpl: only produce merge-operand output and count bytes
|
||||
|
||||
@@ -6,10 +6,6 @@
|
||||
|
||||
#include "util/coro_utils.h"
|
||||
|
||||
#if defined(USE_COROUTINES) && defined(WITH_COROUTINES)
|
||||
#include "util/coro_stats_util.h"
|
||||
#endif // USE_COROUTINES && WITH_COROUTINES
|
||||
|
||||
#if defined(WITHOUT_COROUTINES) || \
|
||||
(defined(USE_COROUTINES) && defined(WITH_COROUTINES))
|
||||
|
||||
@@ -25,10 +21,6 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
DEFINE_SYNC_AND_ASYNC(Status, DBImpl::Get)
|
||||
(const ReadOptions& _read_options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value, std::string* timestamp) {
|
||||
#ifdef WITH_COROUTINES
|
||||
INSTALL_COROUTINE_STATS_CONTEXT_SCOPE(
|
||||
immutable_db_options_.fs->GetReadExecutor(), immutable_db_options_.env);
|
||||
#endif
|
||||
assert(value != nullptr);
|
||||
value->Reset();
|
||||
|
||||
@@ -48,6 +40,106 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::Get)
|
||||
timestamp);
|
||||
}
|
||||
|
||||
DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetEntity)
|
||||
(const ReadOptions& _read_options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableWideColumns* columns) {
|
||||
if (!column_family) {
|
||||
CO_RETURN Status::InvalidArgument(
|
||||
"Cannot call GetEntity without a column family handle");
|
||||
}
|
||||
if (!columns) {
|
||||
CO_RETURN Status::InvalidArgument(
|
||||
"Cannot call GetEntity without a PinnableWideColumns object");
|
||||
}
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kGetEntity) {
|
||||
CO_RETURN Status::InvalidArgument(
|
||||
"Can only call GetEntity with `ReadOptions::io_activity` set to "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kGetEntity`");
|
||||
}
|
||||
ReadOptions read_options(_read_options);
|
||||
if (read_options.io_activity == Env::IOActivity::kUnknown) {
|
||||
read_options.io_activity = Env::IOActivity::kGetEntity;
|
||||
}
|
||||
columns->Reset();
|
||||
|
||||
GetImplOptions get_impl_options;
|
||||
get_impl_options.column_family = column_family;
|
||||
get_impl_options.columns = columns;
|
||||
|
||||
CO_RETURN CO_AWAIT(GetImpl, read_options, key, get_impl_options);
|
||||
}
|
||||
|
||||
DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetEntity)
|
||||
(const ReadOptions& _read_options, const Slice& key,
|
||||
PinnableAttributeGroups* result) {
|
||||
if (!result) {
|
||||
CO_RETURN Status::InvalidArgument(
|
||||
"Cannot call GetEntity without PinnableAttributeGroups object");
|
||||
}
|
||||
Status s;
|
||||
const size_t num_column_families = result->size();
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kGetEntity) {
|
||||
s = Status::InvalidArgument(
|
||||
"Can only call GetEntity with `ReadOptions::io_activity` set to "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kGetEntity`");
|
||||
for (size_t i = 0; i < num_column_families; ++i) {
|
||||
(*result)[i].SetStatus(s);
|
||||
}
|
||||
CO_RETURN s;
|
||||
}
|
||||
// return early if no CF was passed in
|
||||
if (num_column_families == 0) {
|
||||
CO_RETURN s;
|
||||
}
|
||||
ReadOptions read_options(_read_options);
|
||||
if (read_options.io_activity == Env::IOActivity::kUnknown) {
|
||||
read_options.io_activity = Env::IOActivity::kGetEntity;
|
||||
}
|
||||
std::vector<Slice> keys;
|
||||
std::vector<ColumnFamilyHandle*> column_families;
|
||||
for (size_t i = 0; i < num_column_families; ++i) {
|
||||
// If any of the CFH is null, break early since the entire query will fail
|
||||
if (!(*result)[i].column_family()) {
|
||||
s = Status::InvalidArgument(
|
||||
"DB failed to query because one or more group(s) have null column "
|
||||
"family handle");
|
||||
(*result)[i].SetStatus(
|
||||
Status::InvalidArgument("Column family handle cannot be null"));
|
||||
break;
|
||||
}
|
||||
// Adding the same key slice for different CFs
|
||||
keys.emplace_back(key);
|
||||
column_families.emplace_back((*result)[i].column_family());
|
||||
}
|
||||
if (!s.ok()) {
|
||||
for (size_t i = 0; i < num_column_families; ++i) {
|
||||
if ((*result)[i].status().ok()) {
|
||||
(*result)[i].SetStatus(
|
||||
Status::Incomplete("DB not queried due to invalid argument(s) in "
|
||||
"one or more of the attribute groups"));
|
||||
}
|
||||
}
|
||||
CO_RETURN s;
|
||||
}
|
||||
std::vector<PinnableWideColumns> columns(num_column_families);
|
||||
std::vector<Status> statuses(num_column_families);
|
||||
CO_AWAIT(MultiGetCommon, read_options, num_column_families,
|
||||
column_families.data(), keys.data(),
|
||||
/* values */ nullptr, columns.data(),
|
||||
/* timestamps */ nullptr, statuses.data(),
|
||||
/* newer_version_present */ nullptr,
|
||||
/* sorted_input */ false);
|
||||
// Set results
|
||||
for (size_t i = 0; i < num_column_families; ++i) {
|
||||
(*result)[i].Reset();
|
||||
(*result)[i].SetStatus(statuses[i]);
|
||||
(*result)[i].SetColumns(std::move(columns[i]));
|
||||
}
|
||||
CO_RETURN s;
|
||||
}
|
||||
|
||||
DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
(const ReadOptions& read_options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value, std::string* timestamp) {
|
||||
@@ -69,6 +161,13 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
|
||||
assert(get_impl_options.column_family);
|
||||
|
||||
if (get_impl_options.newer_version_present != nullptr &&
|
||||
read_options.snapshot != nullptr &&
|
||||
get_impl_options.callback != nullptr) {
|
||||
CO_RETURN Status::NotSupported(
|
||||
"Newer-version metadata is not supported with a read callback");
|
||||
}
|
||||
|
||||
if (read_options.timestamp) {
|
||||
const Status s = FailIfTsMismatchCf(get_impl_options.column_family,
|
||||
*(read_options.timestamp));
|
||||
@@ -118,13 +217,33 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
}
|
||||
}
|
||||
|
||||
// Acquire SuperVersion
|
||||
// Acquire SuperVersion (or, for a batched lazy read, use the shared one the
|
||||
// batch already referenced -- see GetImplOptions::lazy_columns_shared_sv).
|
||||
const bool own_super_version =
|
||||
get_impl_options.lazy_columns_shared_sv == nullptr;
|
||||
SuperVersion* sv;
|
||||
#if defined(WITH_COROUTINES)
|
||||
SuperVersion* sv = cfd->GetReferencedSuperVersion(this);
|
||||
Defer sv_cleanup([&] { CleanupSuperVersion(sv); });
|
||||
if (own_super_version) {
|
||||
sv = cfd->GetReferencedSuperVersion(this);
|
||||
} else {
|
||||
sv = get_impl_options.lazy_columns_shared_sv;
|
||||
}
|
||||
Defer sv_cleanup([&] {
|
||||
if (own_super_version) {
|
||||
CleanupSuperVersion(sv);
|
||||
}
|
||||
});
|
||||
#else
|
||||
SuperVersion* sv = GetAndRefSuperVersion(cfd);
|
||||
Defer sv_cleanup([&] { ReturnAndCleanupSuperVersion(cfd, sv); });
|
||||
if (own_super_version) {
|
||||
sv = GetAndRefSuperVersion(cfd);
|
||||
} else {
|
||||
sv = get_impl_options.lazy_columns_shared_sv;
|
||||
}
|
||||
Defer sv_cleanup([&] {
|
||||
if (own_super_version) {
|
||||
ReturnAndCleanupSuperVersion(cfd, sv);
|
||||
}
|
||||
});
|
||||
#endif
|
||||
if (read_options.timestamp && read_options.timestamp->size() > 0) {
|
||||
const Status s =
|
||||
@@ -139,7 +258,11 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
TEST_SYNC_POINT("DBImpl::GetImpl:2");
|
||||
|
||||
SequenceNumber snapshot;
|
||||
if (read_options.snapshot != nullptr) {
|
||||
if (!own_super_version) {
|
||||
// Batched lazy read: use the batch's consistent sequence number (derived
|
||||
// once, with the shared SuperVersion, by MultiCFSnapshot).
|
||||
snapshot = get_impl_options.lazy_columns_snapshot_seq;
|
||||
} else if (read_options.snapshot != nullptr) {
|
||||
if (get_impl_options.callback) {
|
||||
// Already calculated based on read_options.snapshot
|
||||
snapshot = get_impl_options.callback->max_visible_seq();
|
||||
@@ -173,6 +296,17 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
snapshot = get_impl_options.callback->max_visible_seq();
|
||||
}
|
||||
}
|
||||
SequenceNumber lookup_snapshot = snapshot;
|
||||
const bool newer_version_present_requested =
|
||||
get_impl_options.newer_version_present != nullptr &&
|
||||
read_options.snapshot != nullptr;
|
||||
const SequenceNumber newer_version_upper_bound_seq =
|
||||
newer_version_present_requested ? GetLastPublishedSequence() : snapshot;
|
||||
const bool track_newer_versions = newer_version_present_requested &&
|
||||
snapshot < newer_version_upper_bound_seq;
|
||||
if (track_newer_versions) {
|
||||
lookup_snapshot = newer_version_upper_bound_seq;
|
||||
}
|
||||
// If timestamp is used, we use read callback to ensure <key,t,s> is returned
|
||||
// only if t <= read_opts.timestamp and s <= snapshot.
|
||||
// HACK: temporarily overwrite input struct field but restore
|
||||
@@ -184,6 +318,13 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
.callback); // timestamp with callback is not supported
|
||||
read_cb.Refresh(snapshot);
|
||||
get_impl_options.callback = &read_cb;
|
||||
} else if (track_newer_versions && get_impl_options.callback == nullptr) {
|
||||
read_cb.Refresh(snapshot);
|
||||
get_impl_options.callback = &read_cb;
|
||||
}
|
||||
if (track_newer_versions) {
|
||||
read_cb.EnableNewerVersionTracking(snapshot, newer_version_upper_bound_seq,
|
||||
get_impl_options.newer_version_present);
|
||||
}
|
||||
TEST_SYNC_POINT("DBImpl::GetImpl:3");
|
||||
TEST_SYNC_POINT("DBImpl::GetImpl:4");
|
||||
@@ -198,7 +339,7 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
// First look in the memtable, then in the immutable memtable (if any).
|
||||
// s is both in/out. When in, s could either be OK or MergeInProgress.
|
||||
// merge_operands will contain the sequence of merges in the latter case.
|
||||
LookupKey lkey(key, snapshot, read_options.timestamp);
|
||||
LookupKey lkey(key, lookup_snapshot, read_options.timestamp);
|
||||
PERF_TIMER_STOP(get_snapshot_time);
|
||||
|
||||
bool skip_memtable = (read_options.read_tier == kPersistedTier &&
|
||||
@@ -403,17 +544,21 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
PERF_COUNTER_ADD(get_read_bytes, size);
|
||||
}
|
||||
|
||||
if (get_impl_options.lazy_columns_version != nullptr && s.ok()) {
|
||||
// Lazy result (GetEntityLazy / MultiGetEntityLazy): the entity's blob
|
||||
// references were left unresolved. Hand back the Version they must be
|
||||
// resolved against (for both the single-key and batched paths).
|
||||
*get_impl_options.lazy_columns_version = sv->current;
|
||||
}
|
||||
if (get_impl_options.lazy_columns_pin != nullptr && s.ok()) {
|
||||
// Lazy result (GetEntityLazy): the entity's blob references were left
|
||||
// unresolved. Hand back the Version they must be resolved against and
|
||||
// transfer an extra SuperVersion reference into the result's pin, so the
|
||||
// result -- and thus deferred blob reads -- stay valid after this call
|
||||
// returns (as an iterator's SuperVersion pin does). The reference is
|
||||
// released when the pin's cleanup runs (result destruction); the
|
||||
// borrowed reference for this call is dropped by the sv_cleanup Defer.
|
||||
if (get_impl_options.lazy_columns_version != nullptr) {
|
||||
*get_impl_options.lazy_columns_version = sv->current;
|
||||
}
|
||||
// Single-key GetEntityLazy: transfer an extra SuperVersion reference into
|
||||
// the result's pin, so the result -- and thus deferred blob reads -- stay
|
||||
// valid after this call returns (as an iterator's SuperVersion pin does).
|
||||
// The reference is released when the pin's cleanup runs (result
|
||||
// destruction); the borrowed reference for this call is dropped by the
|
||||
// sv_cleanup Defer. The batched path passes no pin here (it uses
|
||||
// lazy_columns_shared_sv) because the batch holds one shared pin per
|
||||
// column family instead.
|
||||
TransferSuperVersionPin(sv, get_impl_options.lazy_columns_pin);
|
||||
}
|
||||
RecordInHistogram(stats_, BYTES_PER_READ, size);
|
||||
@@ -635,7 +780,7 @@ DEFINE_SYNC_AND_ASYNC(void, DBImpl::MultiGetCommon)
|
||||
(const ReadOptions& read_options, const size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys, PinnableSlice* values,
|
||||
PinnableWideColumns* columns, std::string* timestamps, Status* statuses,
|
||||
const bool sorted_input) {
|
||||
std::vector<uint8_t>* newer_version_present, const bool sorted_input) {
|
||||
if (num_keys == 0) {
|
||||
CO_RETURN;
|
||||
}
|
||||
@@ -754,8 +899,26 @@ DEFINE_SYNC_AND_ASYNC(void, DBImpl::MultiGetCommon)
|
||||
|
||||
GetWithTimestampReadCallback timestamp_read_callback(0);
|
||||
ReadCallback* read_callback = nullptr;
|
||||
if (read_options.timestamp && read_options.timestamp->size() > 0) {
|
||||
SequenceNumber lookup_seqnum = consistent_seqnum;
|
||||
const bool newer_version_present_requested =
|
||||
newer_version_present != nullptr && read_options.snapshot != nullptr;
|
||||
const SequenceNumber newer_version_upper_bound_seq =
|
||||
newer_version_present_requested ? GetLastPublishedSequence()
|
||||
: consistent_seqnum;
|
||||
const bool track_newer_versions =
|
||||
newer_version_present_requested &&
|
||||
consistent_seqnum < newer_version_upper_bound_seq;
|
||||
if (track_newer_versions) {
|
||||
lookup_seqnum = newer_version_upper_bound_seq;
|
||||
}
|
||||
if ((read_options.timestamp && read_options.timestamp->size() > 0) ||
|
||||
track_newer_versions) {
|
||||
timestamp_read_callback.Refresh(consistent_seqnum);
|
||||
if (track_newer_versions) {
|
||||
timestamp_read_callback.EnableNewerVersionTracking(
|
||||
consistent_seqnum, newer_version_upper_bound_seq,
|
||||
/*single_key_result=*/nullptr);
|
||||
}
|
||||
read_callback = ×tamp_read_callback;
|
||||
}
|
||||
|
||||
@@ -766,8 +929,7 @@ DEFINE_SYNC_AND_ASYNC(void, DBImpl::MultiGetCommon)
|
||||
cf_sv_pair_iter != cf_sv_pairs.end()) {
|
||||
s = CO_AWAIT(MultiGetImpl, read_options, key_range_per_cf_iter->start,
|
||||
key_range_per_cf_iter->num_keys, &sorted_keys,
|
||||
cf_sv_pair_iter->super_version, consistent_seqnum,
|
||||
read_callback);
|
||||
cf_sv_pair_iter->super_version, lookup_seqnum, read_callback);
|
||||
if (!s.ok()) {
|
||||
break;
|
||||
}
|
||||
@@ -795,16 +957,13 @@ DEFINE_SYNC_AND_ASYNC(void, DBImpl::MultiGetCommon)
|
||||
CleanupSuperVersion(cf_sv_pair.super_version);
|
||||
}
|
||||
}
|
||||
CopyNewerVersionPresent(key_context, newer_version_present);
|
||||
}
|
||||
|
||||
DEFINE_SYNC_AND_ASYNC(void, DBImpl::MultiGet)
|
||||
(const ReadOptions& _read_options, const size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys, PinnableSlice* values,
|
||||
std::string* timestamps, Status* statuses, const bool sorted_input) {
|
||||
#ifdef WITH_COROUTINES
|
||||
INSTALL_COROUTINE_STATS_CONTEXT_SCOPE(
|
||||
immutable_db_options_.fs->GetReadExecutor(), immutable_db_options_.env);
|
||||
#endif
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kMultiGet) {
|
||||
Status s = Status::InvalidArgument(
|
||||
@@ -823,7 +982,8 @@ DEFINE_SYNC_AND_ASYNC(void, DBImpl::MultiGet)
|
||||
}
|
||||
CO_AWAIT(MultiGetCommon, read_options, num_keys, column_families, keys,
|
||||
values,
|
||||
/* columns */ nullptr, timestamps, statuses, sorted_input);
|
||||
/* columns */ nullptr, timestamps, statuses,
|
||||
/* newer_version_present */ nullptr, sorted_input);
|
||||
CO_RETURN;
|
||||
}
|
||||
|
||||
|
||||
+244
-63
@@ -380,15 +380,26 @@ Status DBImpl::IngestWBWIAsMemtable(
|
||||
|
||||
// Switch memtable and add WBWIMemTables
|
||||
Status s;
|
||||
const bool defer_wbm_accounting_refresh =
|
||||
immutable_db_options_.atomic_flush && cfds.size() > 1;
|
||||
bool switch_attempted = false;
|
||||
for (size_t i = 0; i < cfds.size(); ++i) {
|
||||
WriteContext write_context;
|
||||
switch_attempted = true;
|
||||
// TODO: not switch on empty memtable, may need to update metadata
|
||||
// like NextLogNumber(), earliest_seqno and memtable id.
|
||||
if (i < memtables.size()) {
|
||||
s = SwitchMemtable(cfds[i], &write_context, memtables[i],
|
||||
last_seqno_after_ingest);
|
||||
s = defer_wbm_accounting_refresh
|
||||
? SwitchMemtableImpl(cfds[i], &write_context, memtables[i],
|
||||
last_seqno_after_ingest,
|
||||
false /* refresh_wbm_accounting */)
|
||||
: SwitchMemtable(cfds[i], &write_context, memtables[i],
|
||||
last_seqno_after_ingest);
|
||||
} else {
|
||||
s = SwitchMemtable(cfds[i], &write_context);
|
||||
s = defer_wbm_accounting_refresh
|
||||
? SwitchMemtableImpl(cfds[i], &write_context, nullptr, 0,
|
||||
false /* refresh_wbm_accounting */)
|
||||
: SwitchMemtable(cfds[i], &write_context);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
// SwitchMemtable() can only fail if a new WAL is to be created, this
|
||||
@@ -420,6 +431,9 @@ Status DBImpl::IngestWBWIAsMemtable(
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (defer_wbm_accounting_refresh && switch_attempted) {
|
||||
UpdateWBMAccountingAfterMemtableSwitch(true /* refresh_now */);
|
||||
}
|
||||
for (size_t i = 0; i < cfds.size(); ++i) {
|
||||
if (cfds[i]->UnrefAndTryDelete()) {
|
||||
cfds[i] = nullptr;
|
||||
@@ -1201,6 +1215,7 @@ Status DBImpl::WriteImpl(
|
||||
IOStatus io_s;
|
||||
Status pre_release_cb_status;
|
||||
size_t seq_inc = 0;
|
||||
uint64_t wal_write_number = 0;
|
||||
if (status.ok()) {
|
||||
if (deferred_put_entities != nullptr) {
|
||||
my_batch->Clear();
|
||||
@@ -1362,6 +1377,7 @@ Status DBImpl::WriteImpl(
|
||||
if (!two_write_queues_) {
|
||||
if (status.ok() && !write_options.disableWAL) {
|
||||
assert(wal_context.wal_file_number_size);
|
||||
wal_write_number = wal_context.wal_file_number_size->number;
|
||||
wal_context.prev_size = wal_context.writer->file()->GetFileSize();
|
||||
PERF_TIMER_GUARD(write_wal_time);
|
||||
io_s = WriteGroupToWAL(write_group, wal_context.writer, wal_used,
|
||||
@@ -1486,7 +1502,9 @@ Status DBImpl::WriteImpl(
|
||||
|
||||
if (!io_s.ok()) {
|
||||
// Check WriteToWAL status
|
||||
WALIOStatusCheck(io_s);
|
||||
TEST_SYNC_POINT_CALLBACK("DBImpl::WriteImpl:BeforeWALIOStatusCheck", &io_s);
|
||||
WALIOStatusCheck(io_s, wal_write_number, last_sequence,
|
||||
last_sequence + 1 - seq_inc);
|
||||
}
|
||||
if (!w.CallbackFailed()) {
|
||||
if (!io_s.ok()) {
|
||||
@@ -2069,7 +2087,10 @@ void DBImpl::WriteStatusCheck(const Status& status) {
|
||||
}
|
||||
}
|
||||
|
||||
void DBImpl::WALIOStatusCheck(const IOStatus& io_status) {
|
||||
void DBImpl::WALIOStatusCheck(const IOStatus& io_status,
|
||||
uint64_t failed_wal_number,
|
||||
SequenceNumber failed_wal_sequence,
|
||||
SequenceNumber failed_wal_sequence_start) {
|
||||
// Is setting bg_error_ enough here? This will at least stop
|
||||
// compaction and fail any further writes.
|
||||
if ((immutable_db_options_.paranoid_checks && !io_status.ok() &&
|
||||
@@ -2077,8 +2098,33 @@ void DBImpl::WALIOStatusCheck(const IOStatus& io_status) {
|
||||
io_status.IsIOFenced()) {
|
||||
mutex_.Lock();
|
||||
// Maybe change the return status to void?
|
||||
error_handler_.SetBGError(io_status, BackgroundErrorReason::kWriteCallback,
|
||||
/*wal_related=*/true);
|
||||
if (io_status.IsIOError() && !io_status.IsIOFenced()) {
|
||||
DBRecoverContext context;
|
||||
context.failed_wal_sequence = failed_wal_sequence;
|
||||
context.failed_wal_sequence_start = failed_wal_sequence_start;
|
||||
const uint64_t resolved_failed_wal_number =
|
||||
failed_wal_number != 0 ? failed_wal_number : cur_wal_number_;
|
||||
if (io_status.GetScope() == IOStatus::IOErrorScope::kIOErrorScopeFile) {
|
||||
context.failed_wal_number = resolved_failed_wal_number;
|
||||
if (failed_wal_sequence_start != 0 &&
|
||||
failed_wal_sequence_start <= failed_wal_sequence) {
|
||||
// Successful live resumption deliberately omits this write from the
|
||||
// database even if the physical WAL record is complete. Keep a
|
||||
// DB-lifetime fence so TransactionLogIterator does not expose it.
|
||||
wal_manager_.RecordIndeterminateWriteSequence(
|
||||
resolved_failed_wal_number, failed_wal_sequence_start,
|
||||
failed_wal_sequence);
|
||||
}
|
||||
ExtendWALRecoveryCutoff(context.failed_wal_number);
|
||||
}
|
||||
error_handler_.SetBGError(io_status,
|
||||
BackgroundErrorReason::kWriteCallback,
|
||||
/*wal_related=*/true, context);
|
||||
} else {
|
||||
error_handler_.SetBGError(io_status,
|
||||
BackgroundErrorReason::kWriteCallback,
|
||||
/*wal_related=*/true);
|
||||
}
|
||||
mutex_.Unlock();
|
||||
} else {
|
||||
// Force writable file to be continue writable.
|
||||
@@ -2086,6 +2132,16 @@ void DBImpl::WALIOStatusCheck(const IOStatus& io_status) {
|
||||
}
|
||||
}
|
||||
|
||||
void DBImpl::ExtendWALRecoveryCutoff(uint64_t failed_wal_number) {
|
||||
assert(failed_wal_number != 0);
|
||||
uint64_t current = wal_recovery_cutoff_.load(std::memory_order_relaxed);
|
||||
while (current < failed_wal_number &&
|
||||
!wal_recovery_cutoff_.compare_exchange_weak(
|
||||
current, failed_wal_number, std::memory_order_release,
|
||||
std::memory_order_relaxed)) {
|
||||
}
|
||||
}
|
||||
|
||||
void DBImpl::HandleMemTableInsertFailure(const Status& status) {
|
||||
assert(!status.ok());
|
||||
// A non-OK status on memtable insert indicates that the state implied by the
|
||||
@@ -2127,14 +2183,11 @@ Status DBImpl::PreprocessWrite(const WriteOptions& write_options,
|
||||
}
|
||||
|
||||
if (UNLIKELY(status.ok() && write_buffer_manager_->ShouldFlush())) {
|
||||
// Before a new memtable is added in SwitchMemtable(),
|
||||
// write_buffer_manager_->ShouldFlush() will keep returning true. If another
|
||||
// thread is writing to another DB with the same write buffer, they may also
|
||||
// be flushed. We may end up with flushing much more DBs than needed. It's
|
||||
// suboptimal but still correct.
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
WaitForPendingWrites();
|
||||
status = HandleWriteBufferManagerFlush(write_context);
|
||||
if (write_buffer_manager_->TryAcquireLocalFlush()) {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
WaitForPendingWrites();
|
||||
status = HandleWriteBufferManagerFlush(write_context);
|
||||
}
|
||||
}
|
||||
|
||||
if (UNLIKELY(status.ok() && !trim_history_scheduler_.Empty())) {
|
||||
@@ -2178,7 +2231,7 @@ Status DBImpl::PreprocessWrite(const WriteOptions& write_options,
|
||||
status = Status::Incomplete("Write stall");
|
||||
} else {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
WriteBufferManagerStallWrites();
|
||||
status = WriteBufferManagerStallWrites(write_context);
|
||||
}
|
||||
}
|
||||
InstrumentedMutexLock l(&wal_write_mutex_);
|
||||
@@ -2287,10 +2340,13 @@ IOStatus DBImpl::WriteToWAL(const WriteBatch& merged_batch,
|
||||
}
|
||||
IOStatus io_s = log_writer->MaybeAddUserDefinedTimestampSizeRecord(
|
||||
write_options, versions_->GetColumnFamiliesTimestampSizeForRecord());
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"DBImpl::WriteToWAL:AfterMaybeAddUserDefinedTimestampSizeRecord", &io_s);
|
||||
if (!io_s.ok()) {
|
||||
return io_s;
|
||||
}
|
||||
io_s = log_writer->AddRecord(write_options, log_entry, sequence);
|
||||
TEST_SYNC_POINT_CALLBACK("DBImpl::WriteToWAL:AfterAddRecord", &io_s);
|
||||
|
||||
if (UNLIKELY(needs_locking)) {
|
||||
wal_write_mutex_.Unlock();
|
||||
@@ -2379,7 +2435,8 @@ IOStatus DBImpl::WriteGroupToWAL(const WriteThread::WriteGroup& write_group,
|
||||
// and closed WAL that just needs to be recorded as synced in the
|
||||
// manifest.
|
||||
if (auto* f = log.writer->file()) {
|
||||
io_s = f->Sync(opts, immutable_db_options_.use_fsync);
|
||||
io_s =
|
||||
immutable_db_options_.use_fsync ? f->Fsync(opts) : f->Sync(opts);
|
||||
if (!io_s.ok()) {
|
||||
break;
|
||||
}
|
||||
@@ -2671,14 +2728,24 @@ Status DBImpl::SwitchWAL(WriteContext* write_context) {
|
||||
nonmem_write_thread_.EnterUnbatched(&nonmem_w, &mutex_);
|
||||
}
|
||||
|
||||
const bool defer_wbm_accounting_refresh =
|
||||
immutable_db_options_.atomic_flush && cfds.size() > 1;
|
||||
bool switch_attempted = false;
|
||||
for (const auto cfd : cfds) {
|
||||
cfd->Ref();
|
||||
status = SwitchMemtable(cfd, write_context);
|
||||
switch_attempted = true;
|
||||
status = defer_wbm_accounting_refresh
|
||||
? SwitchMemtableImpl(cfd, write_context, nullptr, 0,
|
||||
false /* refresh_wbm_accounting */)
|
||||
: SwitchMemtable(cfd, write_context);
|
||||
cfd->UnrefAndTryDelete();
|
||||
if (!status.ok()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (defer_wbm_accounting_refresh && switch_attempted) {
|
||||
UpdateWBMAccountingAfterMemtableSwitch(true /* refresh_now */);
|
||||
}
|
||||
if (two_write_queues_) {
|
||||
nonmem_write_thread_.ExitUnbatched(&nonmem_w);
|
||||
}
|
||||
@@ -2722,25 +2789,17 @@ Status DBImpl::HandleWriteBufferManagerFlush(WriteContext* write_context) {
|
||||
if (immutable_db_options_.atomic_flush) {
|
||||
SelectColumnFamiliesForAtomicFlush(&cfds);
|
||||
} else {
|
||||
ColumnFamilyData* cfd_picked = nullptr;
|
||||
SequenceNumber seq_num_for_cf_picked = kMaxSequenceNumber;
|
||||
|
||||
for (auto cfd : *versions_->GetColumnFamilySet()) {
|
||||
if (cfd->IsDropped()) {
|
||||
continue;
|
||||
}
|
||||
if (!cfd->mem()->IsEmpty() && !cfd->imm()->IsFlushPendingOrRunning()) {
|
||||
// We only consider flush on CFs with bytes in the mutable memtable,
|
||||
// and no immutable memtables for which flush has yet to finish. If
|
||||
// we triggered flush on CFs already trying to flush, we would risk
|
||||
// creating too many immutable memtables leading to write stalls.
|
||||
uint64_t seq = cfd->mem()->GetCreationSeq();
|
||||
if (cfd_picked == nullptr || seq < seq_num_for_cf_picked) {
|
||||
cfd_picked = cfd;
|
||||
seq_num_for_cf_picked = seq;
|
||||
}
|
||||
}
|
||||
}
|
||||
const WriteBufferFlushPolicy policy = write_buffer_manager_->flush_policy();
|
||||
// Cross-DB selection still flushes the largest CF within the chosen DB.
|
||||
const bool flush_largest =
|
||||
policy == WriteBufferFlushPolicy::kFlushLargest ||
|
||||
policy == WriteBufferFlushPolicy::kFlushLargestAcrossDBs;
|
||||
const bool include_waiting_immutable =
|
||||
policy == WriteBufferFlushPolicy::kFlushLargestAcrossDBs;
|
||||
const FlushableCFs flushable =
|
||||
CollectFlushableCFs(include_waiting_immutable);
|
||||
ColumnFamilyData* cfd_picked =
|
||||
flush_largest ? flushable.largest : flushable.oldest;
|
||||
if (cfd_picked != nullptr) {
|
||||
cfds.push_back(cfd_picked);
|
||||
}
|
||||
@@ -2760,12 +2819,17 @@ Status DBImpl::HandleWriteBufferManagerFlush(WriteContext* write_context) {
|
||||
if (two_write_queues_) {
|
||||
nonmem_write_thread_.EnterUnbatched(&nonmem_w, &mutex_);
|
||||
}
|
||||
const bool defer_wbm_accounting_refresh =
|
||||
immutable_db_options_.atomic_flush && cfds.size() > 1;
|
||||
for (const auto cfd : cfds) {
|
||||
if (cfd->mem()->IsEmpty()) {
|
||||
continue;
|
||||
}
|
||||
cfd->Ref();
|
||||
status = SwitchMemtable(cfd, write_context);
|
||||
status = defer_wbm_accounting_refresh
|
||||
? SwitchMemtableImpl(cfd, write_context, nullptr, 0,
|
||||
false /* refresh_wbm_accounting */)
|
||||
: SwitchMemtable(cfd, write_context);
|
||||
cfd->UnrefAndTryDelete();
|
||||
if (!status.ok()) {
|
||||
break;
|
||||
@@ -2794,6 +2858,11 @@ Status DBImpl::HandleWriteBufferManagerFlush(WriteContext* write_context) {
|
||||
GenerateFlushRequest(cfds, FlushReason::kWriteBufferManager, &flush_req);
|
||||
EnqueuePendingFlush(flush_req);
|
||||
}
|
||||
}
|
||||
if (write_buffer_manager_->ShouldTrackFlushInitiator()) {
|
||||
RefreshFlushableMemAccounting();
|
||||
}
|
||||
if (status.ok()) {
|
||||
MaybeScheduleFlushOrCompaction();
|
||||
}
|
||||
return status;
|
||||
@@ -2914,25 +2983,63 @@ Status DBImpl::DelayWrite(uint64_t num_bytes, WriteThread& write_thread,
|
||||
|
||||
// REQUIRES: mutex_ is held
|
||||
// REQUIRES: this thread is currently at the front of the writer queue
|
||||
void DBImpl::WriteBufferManagerStallWrites() {
|
||||
Status DBImpl::WriteBufferManagerStallWrites(WriteContext* write_context) {
|
||||
mutex_.AssertHeld();
|
||||
// First block future writer threads who want to add themselves to the queue
|
||||
// of WriteThread.
|
||||
write_thread_.BeginWriteStall();
|
||||
mutex_.Unlock();
|
||||
for (;;) {
|
||||
// First block future writer threads who want to add themselves to the
|
||||
// queue of WriteThread.
|
||||
write_thread_.BeginWriteStall();
|
||||
const bool hid_flush_initiator =
|
||||
wbm_flush_initiator_ != nullptr &&
|
||||
write_buffer_manager_->ShouldTrackFlushInitiator();
|
||||
if (hid_flush_initiator) {
|
||||
wbm_flush_initiator_->SetFlushable(false);
|
||||
}
|
||||
mutex_.Unlock();
|
||||
|
||||
// Change the state to State::Blocked.
|
||||
static_cast<WBMStallInterface*>(wbm_stall_.get())
|
||||
->SetState(WBMStallInterface::State::BLOCKED);
|
||||
// Then WriteBufferManager will add DB instance to its queue
|
||||
// and block this thread by calling WBMStallInterface::Block().
|
||||
write_buffer_manager_->BeginWriteStall(wbm_stall_.get());
|
||||
wbm_stall_->Block();
|
||||
WBMStallInterface* const stall =
|
||||
static_cast<WBMStallInterface*>(wbm_stall_.get());
|
||||
stall->SetState(WBMStallInterface::State::BLOCKED);
|
||||
write_buffer_manager_->BeginWriteStall(wbm_stall_.get());
|
||||
bool retry_local_flush = false;
|
||||
if (write_buffer_manager_->ShouldTrackFlushInitiator()) {
|
||||
for (;;) {
|
||||
if (stall->BlockFor(
|
||||
write_buffer_manager_->GetLocalFlushRetryMicros())) {
|
||||
break;
|
||||
}
|
||||
if (write_buffer_manager_->TryAcquireLocalFlush(wbm_stall_.get())) {
|
||||
write_buffer_manager_->RemoveDBFromQueue(wbm_stall_.get());
|
||||
retry_local_flush = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stall->Block();
|
||||
}
|
||||
|
||||
mutex_.Lock();
|
||||
// Stall has ended. Signal writer threads so that they can add
|
||||
// themselves to the WriteThread queue for writes.
|
||||
write_thread_.EndWriteStall();
|
||||
mutex_.Lock();
|
||||
// Allow this front writer to retry the manager-wide emergency lease. It
|
||||
// must not let its write through while the hard limit is still exceeded.
|
||||
write_thread_.EndWriteStall();
|
||||
// Restore the state hidden by this stall even if the runtime policy
|
||||
// changed while the writer was blocked. Otherwise switching back to the
|
||||
// across-DB policy can leave this DB permanently ineligible.
|
||||
if (hid_flush_initiator) {
|
||||
wbm_flush_initiator_->SetFlushable(true);
|
||||
if (write_buffer_manager_->ShouldTrackFlushInitiator()) {
|
||||
write_buffer_manager_->NotifyFlushInitiatorChanged();
|
||||
}
|
||||
}
|
||||
if (!retry_local_flush || !write_buffer_manager_->ShouldStall()) {
|
||||
return Status::OK();
|
||||
}
|
||||
WaitForPendingWrites();
|
||||
Status status = HandleWriteBufferManagerFlush(write_context);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Status DBImpl::ThrottleLowPriWritesIfNeeded(const WriteOptions& write_options,
|
||||
@@ -3063,17 +3170,27 @@ Status DBImpl::ScheduleFlushes(WriteContext* context) {
|
||||
nullptr);
|
||||
autovector<FlushReason> flush_reasons;
|
||||
flush_reasons.reserve(cfds.size());
|
||||
const bool defer_wbm_accounting_refresh =
|
||||
immutable_db_options_.atomic_flush && cfds.size() > 1;
|
||||
bool switch_attempted = false;
|
||||
for (auto& cfd : cfds) {
|
||||
FlushReason flush_reason = FlushReason::kWriteBufferFull;
|
||||
if (status.ok() && !cfd->mem()->IsEmpty()) {
|
||||
flush_reason = cfd->mem()->GetFlushReason();
|
||||
status = SwitchMemtable(cfd, context);
|
||||
switch_attempted = true;
|
||||
status = defer_wbm_accounting_refresh
|
||||
? SwitchMemtableImpl(cfd, context, nullptr, 0,
|
||||
false /* refresh_wbm_accounting */)
|
||||
: SwitchMemtable(cfd, context);
|
||||
}
|
||||
flush_reasons.push_back(flush_reason);
|
||||
if (cfd->UnrefAndTryDelete()) {
|
||||
cfd = nullptr;
|
||||
}
|
||||
}
|
||||
if (defer_wbm_accounting_refresh && switch_attempted) {
|
||||
UpdateWBMAccountingAfterMemtableSwitch(true /* refresh_now */);
|
||||
}
|
||||
|
||||
if (two_write_queues_) {
|
||||
nonmem_write_thread_.ExitUnbatched(&nonmem_w);
|
||||
@@ -3118,6 +3235,31 @@ void DBImpl::NotifyOnMemTableSealed(ColumnFamilyData* /*cfd*/,
|
||||
Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context,
|
||||
ReadOnlyMemTable* new_imm,
|
||||
SequenceNumber last_seqno) {
|
||||
return SwitchMemtableImpl(cfd, context, new_imm, last_seqno,
|
||||
true /* refresh_wbm_accounting */);
|
||||
}
|
||||
|
||||
void DBImpl::UpdateWBMAccountingAfterMemtableSwitch(bool refresh_now) {
|
||||
mutex_.AssertHeld();
|
||||
if (wbm_flush_initiator_ == nullptr ||
|
||||
!write_buffer_manager_->ShouldTrackFlushInitiator()) {
|
||||
return;
|
||||
}
|
||||
if (refresh_now) {
|
||||
TEST_SYNC_POINT("DBImpl::UpdateWBMAccountingAfterMemtableSwitch:Refresh");
|
||||
RefreshFlushableMemAccounting();
|
||||
} else {
|
||||
// Do not let the sorter consume an intermediate snapshot while a caller is
|
||||
// switching a batch of memtables. The caller refreshes once after the
|
||||
// complete batch, including a partial-error exit.
|
||||
wbm_flush_initiator_->InvalidateLargestFlushableCFMem();
|
||||
}
|
||||
}
|
||||
|
||||
Status DBImpl::SwitchMemtableImpl(ColumnFamilyData* cfd, WriteContext* context,
|
||||
ReadOnlyMemTable* new_imm,
|
||||
SequenceNumber last_seqno,
|
||||
bool refresh_wbm_accounting) {
|
||||
mutex_.AssertHeld();
|
||||
assert(lock_wal_owner_thread_id_counts_.empty());
|
||||
|
||||
@@ -3128,6 +3270,7 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context,
|
||||
UnpublishedWAL prepared_wal;
|
||||
MemTable* new_mem = nullptr;
|
||||
IOStatus io_s;
|
||||
bool current_wal_write_buffer_failed = false;
|
||||
|
||||
// Recoverable state is persisted in WAL. After memtable switch, WAL might
|
||||
// be deleted, so we write the state to memtable to be persisted as well.
|
||||
@@ -3142,7 +3285,9 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context,
|
||||
if (two_write_queues_) {
|
||||
wal_write_mutex_.Lock();
|
||||
}
|
||||
bool creating_new_log = !wal_empty_;
|
||||
bool creating_new_log =
|
||||
!wal_empty_ || (recovering_from_wal_write_error_ &&
|
||||
cur_wal_number_ <= recover_wal_through_number_);
|
||||
if (two_write_queues_) {
|
||||
wal_write_mutex_.Unlock();
|
||||
}
|
||||
@@ -3273,13 +3418,28 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context,
|
||||
InstrumentedMutexLock l(&wal_write_mutex_);
|
||||
assert(new_log != nullptr);
|
||||
if (!logs_.empty()) {
|
||||
// Alway flush the buffer of the last log before switching to a new one
|
||||
// Always flush the buffer of the last log before switching to a new one,
|
||||
// except when recovery is replacing a file-scoped failed WAL. Its
|
||||
// accepted contents are persisted by the atomic recovery flush.
|
||||
log::Writer* cur_log_writer = logs_.back().writer;
|
||||
if (error_handler_.IsRecoveryInProgress()) {
|
||||
const bool abandon_current_wal =
|
||||
recovering_from_wal_write_error_ &&
|
||||
cur_log_writer->get_log_number() <= recover_wal_through_number_;
|
||||
if (abandon_current_wal) {
|
||||
TEST_SYNC_POINT("DBImpl::SwitchMemtable:AbandonWAL");
|
||||
ROCKS_LOG_WARN(immutable_db_options_.info_log,
|
||||
"[WAL recovery] [%s] Replacing failed WAL #%" PRIu64
|
||||
" with WAL #%" PRIu64,
|
||||
cfd->GetName().c_str(), cur_log_writer->get_log_number(),
|
||||
new_log_number);
|
||||
} else if (error_handler_.IsRecoveryInProgress()) {
|
||||
// In recovery path, we force another try of writing WAL buffer.
|
||||
cur_log_writer->file()->reset_seen_error();
|
||||
}
|
||||
io_s = cur_log_writer->WriteBuffer(write_options);
|
||||
if (!abandon_current_wal) {
|
||||
io_s = cur_log_writer->WriteBuffer(write_options);
|
||||
current_wal_write_buffer_failed = !io_s.ok();
|
||||
}
|
||||
if (s.ok()) {
|
||||
s = io_s;
|
||||
}
|
||||
@@ -3306,16 +3466,35 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context,
|
||||
delete new_mem;
|
||||
delete new_log;
|
||||
context->superversion_context.new_superversion.reset();
|
||||
// We may have lost data from the WritableFileBuffer in-memory buffer for
|
||||
// the current log, so treat it as a fatal error and set bg_error
|
||||
// Only a WriteBuffer() failure belongs to the current WAL. CreateWAL() and
|
||||
// StartWALFile() operate on an unpublished replacement, so a failure there
|
||||
// must not quarantine an otherwise healthy current WAL.
|
||||
if (!io_s.ok()) {
|
||||
error_handler_.SetBGError(io_s, BackgroundErrorReason::kMemTable,
|
||||
/*wal_related=*/true);
|
||||
if (!current_wal_write_buffer_failed) {
|
||||
ROCKS_LOG_WARN(immutable_db_options_.info_log,
|
||||
"Failed to create or start replacement WAL #%" PRIu64
|
||||
"; current WAL #%" PRIu64 " remains active: %s",
|
||||
new_log_number, cur_wal_number_,
|
||||
io_s.ToString().c_str());
|
||||
}
|
||||
if (current_wal_write_buffer_failed && io_s.IsIOError() &&
|
||||
!io_s.IsIOFenced() &&
|
||||
io_s.GetScope() == IOStatus::IOErrorScope::kIOErrorScopeFile) {
|
||||
DBRecoverContext recovery_context;
|
||||
recovery_context.failed_wal_number = cur_wal_number_;
|
||||
ExtendWALRecoveryCutoff(recovery_context.failed_wal_number);
|
||||
error_handler_.SetBGError(io_s, BackgroundErrorReason::kMemTable,
|
||||
/*wal_related=*/true, recovery_context);
|
||||
} else {
|
||||
error_handler_.SetBGError(io_s, BackgroundErrorReason::kMemTable,
|
||||
/*wal_related=*/true);
|
||||
}
|
||||
} else {
|
||||
error_handler_.SetBGError(s, BackgroundErrorReason::kMemTable);
|
||||
}
|
||||
// Read back bg_error in order to get the right severity
|
||||
s = error_handler_.GetBGError();
|
||||
UpdateWBMAccountingAfterMemtableSwitch(refresh_wbm_accounting);
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -3357,6 +3536,7 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context,
|
||||
BackgroundErrorReason::kManifestWrite);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
UpdateWBMAccountingAfterMemtableSwitch(refresh_wbm_accounting);
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -3409,6 +3589,7 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context,
|
||||
}
|
||||
new_mem->Ref();
|
||||
cfd->SetMemtable(new_mem);
|
||||
UpdateWBMAccountingAfterMemtableSwitch(refresh_wbm_accounting);
|
||||
InstallSuperVersionAndScheduleWork(cfd, &context->superversion_context);
|
||||
MaybeScheduleAsyncWALPrecreate(preallocate_block_size);
|
||||
|
||||
|
||||
@@ -4308,6 +4308,109 @@ TEST_F(DBIteratorTest, IteratorsConsistentViewExplicitSnapshot) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBIteratorTest, NewIteratorsWithPerColumnFamilyReadOptions) {
|
||||
Options options = GetDefaultOptions();
|
||||
options.prefix_extractor.reset(NewFixedPrefixTransform(1));
|
||||
BlockBasedTableOptions table_options;
|
||||
table_options.filter_policy.reset(NewBloomFilterPolicy(10));
|
||||
table_options.whole_key_filtering = false;
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
|
||||
CreateAndReopenWithCF({"cf_1"}, options);
|
||||
|
||||
ASSERT_OK(Put(0, "b1", "default_value"));
|
||||
ASSERT_OK(Put(1, "b1", "cf_1_value"));
|
||||
ASSERT_OK(Flush(0));
|
||||
ASSERT_OK(Flush(1));
|
||||
|
||||
ReadOptions prefix_read_options;
|
||||
prefix_read_options.total_order_seek = false;
|
||||
ReadOptions total_order_read_options;
|
||||
total_order_read_options.total_order_seek = true;
|
||||
std::vector<ReadOptions> read_options = {prefix_read_options,
|
||||
total_order_read_options};
|
||||
std::vector<Iterator*> iterators;
|
||||
ASSERT_OK(db_->NewIterators(read_options, handles_, &iterators));
|
||||
ASSERT_EQ(2U, iterators.size());
|
||||
|
||||
iterators[0]->Seek("a9");
|
||||
ASSERT_FALSE(iterators[0]->Valid());
|
||||
ASSERT_OK(iterators[0]->status());
|
||||
iterators[1]->Seek("a9");
|
||||
ASSERT_TRUE(iterators[1]->Valid());
|
||||
ASSERT_EQ("b1", iterators[1]->key());
|
||||
ASSERT_OK(iterators[1]->status());
|
||||
|
||||
for (auto* iterator : iterators) {
|
||||
delete iterator;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBIteratorTest, NewIteratorsPerColumnFamilyOptionsValidation) {
|
||||
Options options = GetDefaultOptions();
|
||||
CreateAndReopenWithCF({"cf_1"}, options);
|
||||
|
||||
std::vector<Iterator*> iterators;
|
||||
ASSERT_TRUE(
|
||||
db_->NewIterators(std::vector<ReadOptions>(1), handles_, &iterators)
|
||||
.IsInvalidArgument());
|
||||
|
||||
std::vector<ReadOptions> read_options(2);
|
||||
read_options[1].tailing = true;
|
||||
ASSERT_TRUE(db_->NewIterators(read_options, handles_, &iterators)
|
||||
.IsInvalidArgument());
|
||||
|
||||
read_options[1].tailing = false;
|
||||
const Snapshot* first_snapshot = db_->GetSnapshot();
|
||||
ASSERT_OK(Put(0, "key", "value"));
|
||||
const Snapshot* second_snapshot = db_->GetSnapshot();
|
||||
read_options[0].snapshot = second_snapshot;
|
||||
read_options[1].snapshot = second_snapshot;
|
||||
ASSERT_OK(db_->NewIterators(read_options, handles_, &iterators));
|
||||
for (auto* iterator : iterators) {
|
||||
delete iterator;
|
||||
}
|
||||
|
||||
read_options[0].snapshot = first_snapshot;
|
||||
read_options[1].snapshot = second_snapshot;
|
||||
ASSERT_TRUE(db_->NewIterators(read_options, handles_, &iterators)
|
||||
.IsInvalidArgument());
|
||||
db_->ReleaseSnapshot(first_snapshot);
|
||||
db_->ReleaseSnapshot(second_snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBIteratorTest,
|
||||
ReadOnlyNewIteratorsAllowsTableFilterWithRangeConversion) {
|
||||
Options options = CurrentOptions();
|
||||
options.min_tombstones_for_range_conversion = 2;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("key", "value"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(ReadOnlyReopen(options));
|
||||
|
||||
std::function<bool(const TableProperties&)> table_filter =
|
||||
[](const TableProperties&) { return true; };
|
||||
ReadOptions read_options;
|
||||
read_options.table_filter = &table_filter;
|
||||
const std::vector<ReadOptions> per_cf_read_options = {read_options};
|
||||
const std::vector<ColumnFamilyHandle*> column_families = {
|
||||
db_->DefaultColumnFamily()};
|
||||
for (bool use_per_cf_read_options : {false, true}) {
|
||||
std::vector<Iterator*> iterators;
|
||||
const Status s =
|
||||
use_per_cf_read_options
|
||||
? db_->NewIterators(per_cf_read_options, column_families,
|
||||
&iterators)
|
||||
: db_->NewIterators(read_options, column_families, &iterators);
|
||||
ASSERT_OK(s);
|
||||
ASSERT_EQ(1U, iterators.size());
|
||||
iterators[0]->SeekToFirst();
|
||||
ASSERT_OK(iterators[0]->status());
|
||||
ASSERT_TRUE(iterators[0]->Valid());
|
||||
ASSERT_EQ("key", iterators[0]->key());
|
||||
delete iterators[0];
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(DBIteratorTest, MemtableOpsScanFlushTriggerWithSeek) {
|
||||
// Tests that option memtable_op_scan_flush_trigger works when the limit
|
||||
// is reached during a Seek() operation.
|
||||
|
||||
@@ -140,7 +140,8 @@ TEST_F(DBMemTableTest, DuplicateSeq) {
|
||||
ImmutableOptions ioptions(options);
|
||||
WriteBufferManager wb(options.db_write_buffer_size);
|
||||
MemTable* mem = new MemTable(cmp, ioptions, MutableCFOptions(options), &wb,
|
||||
kMaxSequenceNumber, 0 /* column_family_id */);
|
||||
kMaxSequenceNumber, 0 /* column_family_id */,
|
||||
nullptr /* flush_initiator */);
|
||||
|
||||
// Write some keys and make sure it returns false on duplicates
|
||||
ASSERT_OK(
|
||||
@@ -188,7 +189,8 @@ TEST_F(DBMemTableTest, DuplicateSeq) {
|
||||
new TestPrefixExtractor()); // which uses _ to extract the prefix
|
||||
ioptions = ImmutableOptions(options);
|
||||
mem = new MemTable(cmp, ioptions, MutableCFOptions(options), &wb,
|
||||
kMaxSequenceNumber, 0 /* column_family_id */);
|
||||
kMaxSequenceNumber, 0 /* column_family_id */,
|
||||
nullptr /* flush_initiator */);
|
||||
// Insert a duplicate key with _ in it
|
||||
ASSERT_OK(
|
||||
mem->Add(seq, kTypeValue, "key_1", "value", nullptr /* kv_prot_info */));
|
||||
@@ -201,7 +203,8 @@ TEST_F(DBMemTableTest, DuplicateSeq) {
|
||||
options.allow_concurrent_memtable_write = true;
|
||||
ioptions = ImmutableOptions(options);
|
||||
mem = new MemTable(cmp, ioptions, MutableCFOptions(options), &wb,
|
||||
kMaxSequenceNumber, 0 /* column_family_id */);
|
||||
kMaxSequenceNumber, 0 /* column_family_id */,
|
||||
nullptr /* flush_initiator */);
|
||||
MemTablePostProcessInfo post_process_info;
|
||||
ASSERT_OK(mem->Add(seq, kTypeValue, "key", "value",
|
||||
nullptr /* kv_prot_info */, true, &post_process_info));
|
||||
@@ -234,7 +237,8 @@ TEST_F(DBMemTableTest, ConcurrentMergeWrite) {
|
||||
ImmutableOptions ioptions(options);
|
||||
WriteBufferManager wb(options.db_write_buffer_size);
|
||||
MemTable* mem = new MemTable(cmp, ioptions, MutableCFOptions(options), &wb,
|
||||
kMaxSequenceNumber, 0 /* column_family_id */);
|
||||
kMaxSequenceNumber, 0 /* column_family_id */,
|
||||
nullptr /* flush_initiator */);
|
||||
|
||||
// Put 0 as the base
|
||||
PutFixed64(&value, static_cast<uint64_t>(0));
|
||||
|
||||
+973
-3
File diff suppressed because it is too large.
Load diff
+81
-5
@@ -1183,10 +1183,43 @@ TEST_F(DBSSTTest, DeleteSchedulerMultipleDBPaths) {
|
||||
}
|
||||
|
||||
TEST_F(DBSSTTest, DestroyDBWithRateLimitedDelete) {
|
||||
Destroy(last_options_);
|
||||
|
||||
int bg_delete_file = 0;
|
||||
int preexisting_trash_files_scheduled = 0;
|
||||
uint64_t preexisting_trash_penalty = 0;
|
||||
std::string file_being_deleted;
|
||||
std::string preexisting_trash_file;
|
||||
std::vector<uint64_t> penalties;
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DeleteScheduler::DeleteTrashFile:DeleteFile",
|
||||
[&](void* /*arg*/) { bg_delete_file++; });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DeleteScheduler::DeleteTrashFile::cb",
|
||||
[&](void* arg) { file_being_deleted = *static_cast<std::string*>(arg); });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DeleteScheduler::BackgroundEmptyTrash:Wait", [&](void* arg) {
|
||||
uint64_t penalty = *(static_cast<uint64_t*>(arg));
|
||||
penalties.push_back(penalty);
|
||||
if (file_being_deleted == preexisting_trash_file) {
|
||||
preexisting_trash_penalty = penalty;
|
||||
}
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"SstFileManagerImpl::ScheduleExistingTrashFileDeletion", [&](void* arg) {
|
||||
const std::string& file_path = *static_cast<std::string*>(arg);
|
||||
if (EndsWith(file_path, ".trash")) {
|
||||
preexisting_trash_files_scheduled++;
|
||||
}
|
||||
});
|
||||
std::atomic<bool> fail_preexisting_trash_delete{true};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DeleteScheduler::DeleteTrashFile:BeforeDeleteFile", [&](void* arg) {
|
||||
if (file_being_deleted == preexisting_trash_file &&
|
||||
fail_preexisting_trash_delete.exchange(false)) {
|
||||
*static_cast<Status*>(arg) = Status::IOError("injected delete error");
|
||||
}
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Status s;
|
||||
@@ -1196,7 +1229,7 @@ TEST_F(DBSSTTest, DestroyDBWithRateLimitedDelete) {
|
||||
options.sst_file_manager.reset(
|
||||
NewSstFileManager(env_, nullptr, "", 0, false, &s, 0));
|
||||
ASSERT_OK(s);
|
||||
DestroyAndReopen(options);
|
||||
Reopen(options);
|
||||
|
||||
// Create 4 files in L0
|
||||
for (int i = 0; i < 4; i++) {
|
||||
@@ -1211,27 +1244,70 @@ TEST_F(DBSSTTest, DestroyDBWithRateLimitedDelete) {
|
||||
|
||||
int num_sst_files = 0;
|
||||
int num_wal_files = 0;
|
||||
std::string sst_file_to_mark_trash;
|
||||
std::vector<std::string> db_files;
|
||||
ASSERT_OK(env_->GetChildren(dbname_, &db_files));
|
||||
for (const std::string& f : db_files) {
|
||||
if (f.substr(f.find_last_of('.') + 1) == "sst") {
|
||||
num_sst_files++;
|
||||
if (sst_file_to_mark_trash.empty()) {
|
||||
sst_file_to_mark_trash = dbname_;
|
||||
sst_file_to_mark_trash.append("/").append(f);
|
||||
}
|
||||
} else if (f.substr(f.find_last_of('.') + 1) == "log") {
|
||||
num_wal_files++;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
ASSERT_GT(num_sst_files, 0);
|
||||
ASSERT_GT(num_wal_files, 0);
|
||||
ASSERT_FALSE(sst_file_to_mark_trash.empty());
|
||||
uint64_t preexisting_trash_file_size = 0;
|
||||
ASSERT_OK(
|
||||
env_->GetFileSize(sst_file_to_mark_trash, &preexisting_trash_file_size));
|
||||
preexisting_trash_file = sst_file_to_mark_trash;
|
||||
preexisting_trash_file.append(".trash");
|
||||
|
||||
auto sfm = static_cast<SstFileManagerImpl*>(options.sst_file_manager.get());
|
||||
|
||||
sfm->SetDeleteRateBytesPerSecond(1024 * 1024);
|
||||
const int64_t rate_bytes_per_sec = 1024 * 1024;
|
||||
sfm->SetDeleteRateBytesPerSecond(rate_bytes_per_sec);
|
||||
// Set an extra high trash ratio to prevent immediate/non-rate limited
|
||||
// deletions
|
||||
sfm->delete_scheduler()->SetMaxTrashDBRatio(1000.0);
|
||||
ASSERT_OK(DestroyDB(dbname_, options));
|
||||
ASSERT_OK(
|
||||
sfm->OnAddFile(sst_file_to_mark_trash, preexisting_trash_file_size));
|
||||
ASSERT_OK(sfm->ScheduleFileDeletion(sst_file_to_mark_trash, dbname_,
|
||||
/*force_bg=*/true));
|
||||
sfm->WaitForEmptyTrashInDirectory(dbname_);
|
||||
ASSERT_FALSE(fail_preexisting_trash_delete);
|
||||
ASSERT_OK(env_->FileExists(preexisting_trash_file));
|
||||
ASSERT_EQ(sfm->GetTotalSize(), preexisting_trash_file_size);
|
||||
ASSERT_EQ(sfm->GetTotalTrashSize(), preexisting_trash_file_size);
|
||||
|
||||
const std::string unaccounted_trash_file = dbname_ + "/unaccounted.sst.trash";
|
||||
ASSERT_OK(WriteStringToFile(env_, "trash", unaccounted_trash_file,
|
||||
/*should_sync=*/false));
|
||||
|
||||
std::string dbname_alias = dbname_;
|
||||
dbname_alias.insert(dbname_alias.find_last_of('/'), "/");
|
||||
ASSERT_OK(DestroyDB(dbname_alias, options));
|
||||
sfm->WaitForEmptyTrash();
|
||||
ASSERT_EQ(bg_delete_file, num_sst_files + num_wal_files);
|
||||
ASSERT_EQ(bg_delete_file, num_sst_files + num_wal_files + 2);
|
||||
ASSERT_EQ(preexisting_trash_files_scheduled, 2);
|
||||
ASSERT_GE(preexisting_trash_penalty,
|
||||
preexisting_trash_file_size * 1000000 / rate_bytes_per_sec);
|
||||
ASSERT_EQ(penalties.size(), num_sst_files + num_wal_files + 2);
|
||||
ASSERT_TRUE(sfm->GetTrackedFiles().empty());
|
||||
ASSERT_EQ(sfm->GetTotalSize(), 0);
|
||||
ASSERT_EQ(sfm->GetTotalTrashSize(), 0);
|
||||
auto bg_errors = sfm->delete_scheduler()->GetBackgroundErrors();
|
||||
ASSERT_EQ(bg_errors.size(), 1);
|
||||
for (auto& error : bg_errors) {
|
||||
ASSERT_NOK(error.second);
|
||||
}
|
||||
ASSERT_TRUE(env_->FileExists(dbname_).IsNotFound());
|
||||
}
|
||||
|
||||
TEST_F(DBSSTTest, DBWithMaxSpaceAllowed) {
|
||||
@@ -1672,7 +1748,7 @@ TEST_F(DBSSTTest, GetTotalSstFilesSize) {
|
||||
// it. Should remove the workaround after we propagate the property on
|
||||
// compaction.
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"FlushJob::WriteLevel0Table:oldest_ancester_time", [&](void* arg) {
|
||||
"FlushJob::WriteLevel0Table:oldest_ancestor_time", [&](void* arg) {
|
||||
uint64_t* current_time = static_cast<uint64_t*>(arg);
|
||||
*current_time = 0;
|
||||
});
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/advanced_compression.h"
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/sst_file_reader.h"
|
||||
#include "rocksdb/sst_file_writer.h"
|
||||
#include "rocksdb/types.h"
|
||||
#include "rocksdb/utilities/object_registry.h"
|
||||
#include "rocksdb/utilities/table_properties_collectors.h"
|
||||
@@ -851,6 +853,173 @@ TEST_F(DBTablePropertiesTest, KeyLargestSmallestSeqno) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBTablePropertiesTest, LsmInfoAtCreationPacking) {
|
||||
using Applicability = LsmInfoAtCreation::Applicability;
|
||||
auto round_trip = [](const LsmInfoAtCreation& info) {
|
||||
return LsmInfoAtCreation::DecodeFrom(info.Encode());
|
||||
};
|
||||
|
||||
// Default is "unknown", which encodes to 0 and is distinct from every other
|
||||
// state.
|
||||
LsmInfoAtCreation unknown;
|
||||
EXPECT_EQ(unknown.applicability, Applicability::kUnknown);
|
||||
EXPECT_EQ(unknown.Encode(), 0U);
|
||||
EXPECT_EQ(round_trip(unknown), unknown);
|
||||
|
||||
// "not applicable" is non-zero (so it persists) and distinct from "unknown".
|
||||
LsmInfoAtCreation na = LsmInfoAtCreation::NotApplicable();
|
||||
EXPECT_EQ(na.applicability, Applicability::kNotApplicable);
|
||||
EXPECT_NE(na.Encode(), 0U);
|
||||
EXPECT_NE(na.Encode(), unknown.Encode());
|
||||
EXPECT_EQ(round_trip(na), na);
|
||||
|
||||
// "applicable" round-trips natural level values and bottommost. Level 0 / not
|
||||
// bottommost is still non-zero and thus distinct from "unknown"/"N/A".
|
||||
for (int level : {0, 1, 5, 63, 200}) {
|
||||
for (bool bottommost : {false, true}) {
|
||||
LsmInfoAtCreation info = LsmInfoAtCreation::Applicable(level, bottommost);
|
||||
EXPECT_NE(info.Encode(), 0U);
|
||||
LsmInfoAtCreation decoded = round_trip(info);
|
||||
EXPECT_EQ(decoded.applicability, Applicability::kApplicable);
|
||||
EXPECT_EQ(decoded.level_at_creation, level);
|
||||
EXPECT_EQ(decoded.is_bottommost, bottommost);
|
||||
EXPECT_EQ(decoded, info);
|
||||
}
|
||||
}
|
||||
|
||||
// Applicable but with an unknown level (e.g. the DB repairer): still
|
||||
// applicable, bottommost preserved, level decodes back to kUnknownLevel.
|
||||
LsmInfoAtCreation unknown_level = LsmInfoAtCreation::Applicable(
|
||||
LsmInfoAtCreation::kUnknownLevel, /*is_bottommost=*/true);
|
||||
LsmInfoAtCreation decoded = round_trip(unknown_level);
|
||||
EXPECT_EQ(decoded.applicability, Applicability::kApplicable);
|
||||
EXPECT_EQ(decoded.level_at_creation, LsmInfoAtCreation::kUnknownLevel);
|
||||
EXPECT_TRUE(decoded.is_bottommost);
|
||||
}
|
||||
|
||||
TEST_F(DBTablePropertiesTest, CreationTimeAliasesOldestAncestorTime) {
|
||||
TableProperties props;
|
||||
ASSERT_EQ(&props.creation_time, &props.oldest_ancestor_time);
|
||||
props.creation_time = 12345;
|
||||
ASSERT_EQ(props.oldest_ancestor_time, 12345U);
|
||||
ASSERT_EQ(&TablePropertiesNames::kCreationTime,
|
||||
&TablePropertiesNames::kOldestAncestorTime);
|
||||
}
|
||||
|
||||
TEST_F(DBTablePropertiesTest, SstFileMetaDataOldestAncesterTimeAlias) {
|
||||
// Intentionally uses the deprecated misspelled alias
|
||||
LiveFileMetaData meta;
|
||||
ASSERT_EQ(&meta.oldest_ancester_time, &meta.oldest_ancestor_time);
|
||||
meta.oldest_ancester_time = 12345;
|
||||
ASSERT_EQ(meta.oldest_ancestor_time, 12345U);
|
||||
}
|
||||
|
||||
TEST_F(DBTablePropertiesTest, UncompressedDataSizeAndLsmInfoPersisted) {
|
||||
Options options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "key1", "value1"));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "key2", "value2"));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "key3", "value3"));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
|
||||
// Freshly flushed L0 file. uncompressed_data_size is now persisted (it used
|
||||
// to always read back as 0), and LSM-at-creation info is populated.
|
||||
{
|
||||
TablePropertiesCollection props;
|
||||
ASSERT_OK(db_->GetPropertiesOfAllTables(&props));
|
||||
ASSERT_EQ(1U, props.size());
|
||||
auto table_props = props.begin()->second;
|
||||
|
||||
EXPECT_GT(table_props->uncompressed_data_size, 0U);
|
||||
LsmInfoAtCreation info =
|
||||
LsmInfoAtCreation::DecodeFrom(table_props->lsm_info_at_creation);
|
||||
EXPECT_EQ(info.applicability,
|
||||
LsmInfoAtCreation::Applicability::kApplicable);
|
||||
EXPECT_EQ(info.level_at_creation, 0);
|
||||
EXPECT_FALSE(info.is_bottommost);
|
||||
}
|
||||
|
||||
// After forcing a bottommost compaction, the (re)created file records
|
||||
// is_bottommost = true and a deeper creation level.
|
||||
{
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
|
||||
|
||||
TablePropertiesCollection props;
|
||||
ASSERT_OK(db_->GetPropertiesOfAllTables(&props));
|
||||
ASSERT_EQ(1U, props.size());
|
||||
auto table_props = props.begin()->second;
|
||||
|
||||
EXPECT_GT(table_props->uncompressed_data_size, 0U);
|
||||
LsmInfoAtCreation info =
|
||||
LsmInfoAtCreation::DecodeFrom(table_props->lsm_info_at_creation);
|
||||
EXPECT_EQ(info.applicability,
|
||||
LsmInfoAtCreation::Applicability::kApplicable);
|
||||
EXPECT_TRUE(info.is_bottommost);
|
||||
EXPECT_GT(info.level_at_creation, 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBTablePropertiesTest, LsmInfoNotApplicableForSstFileWriter) {
|
||||
// A file written by SstFileWriter has no LSM position, so its
|
||||
// lsm_info_at_creation records "not applicable" (distinct from "unknown").
|
||||
Options options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
std::string file = dbname_ + "/for_ingestion.sst";
|
||||
{
|
||||
SstFileWriter writer(EnvOptions(), options);
|
||||
ASSERT_OK(writer.Open(file));
|
||||
ASSERT_OK(writer.Put("a", "1"));
|
||||
ASSERT_OK(writer.Put("b", "2"));
|
||||
ASSERT_OK(writer.Finish());
|
||||
}
|
||||
|
||||
SstFileReader reader(options);
|
||||
ASSERT_OK(reader.Open(file));
|
||||
ASSERT_OK(reader.VerifyChecksum());
|
||||
std::shared_ptr<const TableProperties> table_props =
|
||||
reader.GetTableProperties();
|
||||
ASSERT_NE(table_props, nullptr);
|
||||
|
||||
LsmInfoAtCreation info =
|
||||
LsmInfoAtCreation::DecodeFrom(table_props->lsm_info_at_creation);
|
||||
EXPECT_EQ(info.applicability,
|
||||
LsmInfoAtCreation::Applicability::kNotApplicable);
|
||||
EXPECT_EQ(info.level_at_creation, LsmInfoAtCreation::kUnknownLevel);
|
||||
EXPECT_FALSE(info.is_bottommost);
|
||||
// uncompressed_data_size is persisted for SstFileWriter files too.
|
||||
EXPECT_GT(table_props->uncompressed_data_size, 0U);
|
||||
}
|
||||
|
||||
TEST_F(DBTablePropertiesTest, CompressionManagerFullDetailsPersisted) {
|
||||
// With an explicit CompressionManager, compression_options records the
|
||||
// manager's full details via ToString() (id plus any configured options),
|
||||
// not just GetId(). This manager has no options, so ToString() is its id.
|
||||
Options options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
options.compression_manager =
|
||||
std::make_shared<ParseCompressionDisplayNameManager>();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "k", "v"));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
|
||||
TablePropertiesCollection props;
|
||||
ASSERT_OK(db_->GetPropertiesOfAllTables(&props));
|
||||
ASSERT_EQ(1U, props.size());
|
||||
auto table_props = props.begin()->second;
|
||||
|
||||
EXPECT_NE(
|
||||
table_props->compression_options.find(
|
||||
"_compression_manager=" +
|
||||
std::string(ParseCompressionDisplayNameManager::kCompatibilityName)),
|
||||
std::string::npos)
|
||||
<< table_props->compression_options;
|
||||
}
|
||||
|
||||
TEST_F(DBTablePropertiesTest, ParseCompressionNameForDisplay) {
|
||||
auto custom_mgr = std::make_shared<ParseCompressionDisplayNameManager>();
|
||||
|
||||
|
||||
+9
-4
@@ -1334,16 +1334,16 @@ void CheckColumnFamilyMeta(
|
||||
file_meta_from_files.largest.user_key().ToString());
|
||||
ASSERT_EQ(file_meta_from_cf.oldest_blob_file_number,
|
||||
file_meta_from_files.oldest_blob_file_number);
|
||||
ASSERT_EQ(file_meta_from_cf.oldest_ancester_time,
|
||||
file_meta_from_files.oldest_ancester_time);
|
||||
ASSERT_EQ(file_meta_from_cf.oldest_ancestor_time,
|
||||
file_meta_from_files.oldest_ancestor_time);
|
||||
ASSERT_EQ(file_meta_from_cf.file_creation_time,
|
||||
file_meta_from_files.file_creation_time);
|
||||
ASSERT_GE(file_meta_from_cf.file_creation_time, start_time);
|
||||
ASSERT_LE(file_meta_from_cf.file_creation_time, end_time);
|
||||
ASSERT_EQ(file_meta_from_cf.epoch_number,
|
||||
file_meta_from_files.epoch_number);
|
||||
ASSERT_GE(file_meta_from_cf.oldest_ancester_time, start_time);
|
||||
ASSERT_LE(file_meta_from_cf.oldest_ancester_time, end_time);
|
||||
ASSERT_GE(file_meta_from_cf.oldest_ancestor_time, start_time);
|
||||
ASSERT_LE(file_meta_from_cf.oldest_ancestor_time, end_time);
|
||||
// More from FileStorageInfo
|
||||
ASSERT_EQ(file_meta_from_cf.file_type, kTableFile);
|
||||
ASSERT_EQ(file_meta_from_cf.name,
|
||||
@@ -3600,6 +3600,11 @@ class ModelDB : public DB {
|
||||
std::vector<Iterator*>* /*iterators*/) override {
|
||||
return Status::NotSupported("Not supported yet");
|
||||
}
|
||||
Status NewIterators(const std::vector<ReadOptions>& /*options*/,
|
||||
const std::vector<ColumnFamilyHandle*>& /*column_family*/,
|
||||
std::vector<Iterator*>* /*iterators*/) override {
|
||||
return Status::NotSupported("Not supported yet");
|
||||
}
|
||||
|
||||
std::unique_ptr<Iterator> NewCoalescingIterator(
|
||||
const ReadOptions& /*options*/,
|
||||
|
||||
+41
-28
@@ -991,35 +991,12 @@ std::vector<std::string> DBTestBase::MultiGet(const std::vector<std::string>& k,
|
||||
bool async,
|
||||
bool optimize_multiget_for_io,
|
||||
bool use_coroutine) {
|
||||
ReadOptions options;
|
||||
options.verify_checksums = true;
|
||||
options.snapshot = snapshot;
|
||||
options.async_io = async;
|
||||
options.optimize_multiget_for_io = optimize_multiget_for_io;
|
||||
std::vector<Slice> keys;
|
||||
std::vector<std::string> result(k.size());
|
||||
std::vector<Status> statuses(k.size());
|
||||
std::vector<PinnableSlice> pin_values(k.size());
|
||||
std::vector<Status> statuses;
|
||||
std::vector<PinnableSlice> pin_values;
|
||||
MultiGet(k, snapshot, async, optimize_multiget_for_io, use_coroutine,
|
||||
pin_values, statuses);
|
||||
|
||||
for (size_t i = 0; i < k.size(); ++i) {
|
||||
keys.emplace_back(k[i]);
|
||||
}
|
||||
#if USE_COROUTINES
|
||||
if (use_coroutine) {
|
||||
std::vector<ColumnFamilyHandle*> cfs(keys.size(),
|
||||
dbfull()->DefaultColumnFamily());
|
||||
folly::coro::blockingWait(CoroDB::CoMultiGet(
|
||||
dbfull(), options, keys.size(), cfs.data(), keys.data(),
|
||||
pin_values.data(), /*timestamps=*/nullptr, statuses.data(),
|
||||
/*sorted_input=*/false));
|
||||
} else
|
||||
#else
|
||||
(void)use_coroutine;
|
||||
#endif // USE_COROUTINES
|
||||
{
|
||||
db_->MultiGet(options, dbfull()->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), pin_values.data(), statuses.data());
|
||||
}
|
||||
std::vector<std::string> result(k.size());
|
||||
for (size_t i = 0; i < statuses.size(); ++i) {
|
||||
if (statuses[i].IsNotFound()) {
|
||||
result[i] = "NOT_FOUND";
|
||||
@@ -1035,6 +1012,42 @@ std::vector<std::string> DBTestBase::MultiGet(const std::vector<std::string>& k,
|
||||
return result;
|
||||
}
|
||||
|
||||
void DBTestBase::MultiGet(const std::vector<std::string>& k,
|
||||
const Snapshot* snapshot, bool async,
|
||||
bool optimize_multiget_for_io, bool use_coroutine,
|
||||
std::vector<PinnableSlice>& values,
|
||||
std::vector<Status>& statuses) {
|
||||
ReadOptions options;
|
||||
options.verify_checksums = true;
|
||||
options.snapshot = snapshot;
|
||||
options.async_io = async;
|
||||
options.optimize_multiget_for_io = optimize_multiget_for_io;
|
||||
std::vector<Slice> keys;
|
||||
values.clear();
|
||||
statuses.resize(k.size());
|
||||
values.resize(k.size());
|
||||
|
||||
for (size_t i = 0; i < k.size(); ++i) {
|
||||
keys.emplace_back(k[i]);
|
||||
}
|
||||
#if USE_COROUTINES
|
||||
if (use_coroutine) {
|
||||
std::vector<ColumnFamilyHandle*> cfs(keys.size(),
|
||||
dbfull()->DefaultColumnFamily());
|
||||
folly::coro::blockingWait(CoroDB::CoMultiGet(
|
||||
dbfull(), options, keys.size(), cfs.data(), keys.data(), values.data(),
|
||||
/*timestamps=*/nullptr, statuses.data(),
|
||||
/*sorted_input=*/false));
|
||||
} else
|
||||
#else
|
||||
(void)use_coroutine;
|
||||
#endif // USE_COROUTINES
|
||||
{
|
||||
db_->MultiGet(options, dbfull()->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), values.data(), statuses.data());
|
||||
}
|
||||
}
|
||||
|
||||
Status DBTestBase::Get(const std::string& k, PinnableSlice* v,
|
||||
bool use_coroutine) {
|
||||
ReadOptions options;
|
||||
|
||||
@@ -1253,6 +1253,11 @@ class DBTestBase : public testing::Test {
|
||||
bool optimize_multiget_for_io = true,
|
||||
bool use_coroutine = false);
|
||||
|
||||
void MultiGet(const std::vector<std::string>& k, const Snapshot* snapshot,
|
||||
bool async, bool optimize_multiget_for_io, bool use_coroutine,
|
||||
std::vector<PinnableSlice>& values,
|
||||
std::vector<Status>& statuses);
|
||||
|
||||
Status CompactRange(const CompactRangeOptions& options,
|
||||
std::optional<Slice> begin, std::optional<Slice> end);
|
||||
|
||||
|
||||
@@ -32,6 +32,46 @@ class DBBasicTestWithTimestamp : public DBBasicTestWithTimestampBase {
|
||||
: DBBasicTestWithTimestampBase("db_basic_test_with_timestamp") {}
|
||||
};
|
||||
|
||||
TEST_F(DBBasicTestWithTimestamp, GetWithMetadataTimestampAndNewerVersion) {
|
||||
Options options = CurrentOptions();
|
||||
options.comparator = test::BytewiseComparatorWithU64TsWrapper();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string old_timestamp = EncodeAsUint64(1);
|
||||
const std::string new_timestamp = EncodeAsUint64(2);
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "key", old_timestamp, "old"));
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "key", new_timestamp, "new"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
Slice read_timestamp(new_timestamp);
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot;
|
||||
read_options.timestamp = &read_timestamp;
|
||||
|
||||
OutputMetadata output_metadata;
|
||||
output_metadata.WantTimestamp().WantNewerVersionPresent();
|
||||
std::string value;
|
||||
ASSERT_OK(
|
||||
db_->GetWithMetadata(read_options, "key", &value, &output_metadata));
|
||||
ASSERT_EQ("old", value);
|
||||
ASSERT_EQ(old_timestamp, *output_metadata.timestamp);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
MultiGetOutputMetadata multiget_output_metadata;
|
||||
multiget_output_metadata.WantTimestamps().WantNewerVersionPresent();
|
||||
std::vector<Slice> keys{"key"};
|
||||
std::vector<std::string> values;
|
||||
const std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
read_options, keys, &values, &multiget_output_metadata);
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("old", values[0]);
|
||||
ASSERT_EQ(old_timestamp, (*multiget_output_metadata.timestamps)[0]);
|
||||
ASSERT_TRUE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTestWithTimestamp, SanityChecks) {
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1075,6 +1075,161 @@ INSTANTIATE_TEST_CASE_P(DBWriteTestInstance, DBWriteTest,
|
||||
DBTestBase::kConcurrentWALWrites,
|
||||
DBTestBase::kPipelinedWrite));
|
||||
|
||||
// A non-blocking unbatched join must refuse an active write stall.
|
||||
TEST(WriteThreadTest, EnterUnbatchedNonBlockingRefusesDuringStall) {
|
||||
Options options;
|
||||
ImmutableDBOptions db_options(options);
|
||||
WriteThread write_thread(db_options);
|
||||
InstrumentedMutex mu;
|
||||
|
||||
// No stall: joining succeeds and must be paired with ExitUnbatched().
|
||||
{
|
||||
WriteThread::Writer w;
|
||||
InstrumentedMutexLock l(&mu);
|
||||
ASSERT_TRUE(write_thread.EnterUnbatchedNonBlocking(&w, &mu));
|
||||
ASSERT_OK(w.status);
|
||||
EXPECT_FALSE(w.no_slowdown);
|
||||
EXPECT_FALSE(w.non_blocking_join);
|
||||
write_thread.ExitUnbatched(&w);
|
||||
}
|
||||
|
||||
// Stalled: refuses rather than parking on stall_cv_. Reaching the assertion
|
||||
// at all is the point -- EnterUnbatched() would never have returned.
|
||||
{
|
||||
InstrumentedMutexLock l(&mu);
|
||||
write_thread.BeginWriteStall();
|
||||
ASSERT_NE(0, write_thread.GetBegunCountOfOutstandingStall());
|
||||
|
||||
WriteThread::Writer w;
|
||||
ASSERT_FALSE(write_thread.EnterUnbatchedNonBlocking(&w, &mu));
|
||||
ASSERT_TRUE(w.status.IsIncomplete());
|
||||
EXPECT_FALSE(w.no_slowdown);
|
||||
EXPECT_FALSE(w.non_blocking_join);
|
||||
|
||||
// Not linked, so ExitUnbatched() must not be called; ending the stall is
|
||||
// enough to leave the queue clean.
|
||||
write_thread.EndWriteStall();
|
||||
ASSERT_EQ(0, write_thread.GetBegunCountOfOutstandingStall());
|
||||
}
|
||||
|
||||
{
|
||||
WriteThread::Writer w;
|
||||
InstrumentedMutexLock l(&mu);
|
||||
ASSERT_TRUE(write_thread.EnterUnbatchedNonBlocking(&w, &mu));
|
||||
write_thread.ExitUnbatched(&w);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(WriteThreadTest, EnterUnbatchedNonBlockingFinishesAfterLeadershipStall) {
|
||||
Options options;
|
||||
ImmutableDBOptions db_options(options);
|
||||
WriteThread write_thread(db_options);
|
||||
InstrumentedMutex mu;
|
||||
InstrumentedMutex signal_mu;
|
||||
InstrumentedCondVar signal_cv(&signal_mu);
|
||||
bool before_lock = false;
|
||||
bool release_joiner = false;
|
||||
bool entered = true;
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"WriteThread::EnterUnbatchedNonBlocking:BeforeLock", [&](void*) {
|
||||
InstrumentedMutexLock l(&signal_mu);
|
||||
before_lock = true;
|
||||
signal_cv.SignalAll();
|
||||
while (!release_joiner) {
|
||||
signal_cv.Wait();
|
||||
}
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
Defer cleanup_sync_points([] {
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
});
|
||||
|
||||
port::Thread joiner([&] {
|
||||
WriteThread::Writer w;
|
||||
InstrumentedMutexLock l(&mu);
|
||||
entered = write_thread.EnterUnbatchedNonBlocking(&w, &mu);
|
||||
if (entered) {
|
||||
write_thread.ExitUnbatched(&w);
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
InstrumentedMutexLock l(&signal_mu);
|
||||
while (!before_lock) {
|
||||
signal_cv.Wait();
|
||||
}
|
||||
}
|
||||
{
|
||||
InstrumentedMutexLock l(&mu);
|
||||
write_thread.BeginWriteStall();
|
||||
}
|
||||
{
|
||||
InstrumentedMutexLock l(&signal_mu);
|
||||
release_joiner = true;
|
||||
signal_cv.SignalAll();
|
||||
}
|
||||
|
||||
joiner.join();
|
||||
EXPECT_TRUE(entered);
|
||||
{
|
||||
InstrumentedMutexLock l(&mu);
|
||||
write_thread.EndWriteStall();
|
||||
}
|
||||
}
|
||||
|
||||
// A WBM join queues behind an ordinary leader so sustained write traffic
|
||||
// cannot starve the memtable switch.
|
||||
TEST(WriteThreadTest, EnterUnbatchedNonBlockingQueuesBehindOrdinaryLeader) {
|
||||
Options options;
|
||||
ImmutableDBOptions db_options(options);
|
||||
WriteThread write_thread(db_options);
|
||||
InstrumentedMutex mu;
|
||||
InstrumentedMutex signal_mu;
|
||||
InstrumentedCondVar signal_cv(&signal_mu);
|
||||
bool queued = false;
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"WriteThread::EnterUnbatched:Wait", [&](void*) {
|
||||
InstrumentedMutexLock l(&signal_mu);
|
||||
queued = true;
|
||||
signal_cv.SignalAll();
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
Defer cleanup_sync_points([] {
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
});
|
||||
|
||||
WriteThread::Writer leader;
|
||||
mu.Lock();
|
||||
write_thread.EnterUnbatched(&leader, &mu);
|
||||
mu.Unlock();
|
||||
|
||||
bool entered = false;
|
||||
port::Thread joiner([&] {
|
||||
WriteThread::Writer follower;
|
||||
InstrumentedMutexLock l(&mu);
|
||||
entered = write_thread.EnterUnbatchedNonBlocking(&follower, &mu);
|
||||
if (entered) {
|
||||
write_thread.ExitUnbatched(&follower);
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
InstrumentedMutexLock l(&signal_mu);
|
||||
while (!queued) {
|
||||
signal_cv.Wait();
|
||||
}
|
||||
}
|
||||
mu.Lock();
|
||||
write_thread.ExitUnbatched(&leader);
|
||||
mu.Unlock();
|
||||
joiner.join();
|
||||
EXPECT_TRUE(entered);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
+113
-15
@@ -5,6 +5,8 @@
|
||||
//
|
||||
#include "db/error_handler.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "db/event_helpers.h"
|
||||
#include "file/sst_file_manager_impl.h"
|
||||
@@ -293,7 +295,9 @@ void ErrorHandler::CancelErrorRecoveryForShutDown() {
|
||||
// also track the error separately in recovery_error_ so we can tell in the
|
||||
// end whether recovery succeeded or not
|
||||
void ErrorHandler::HandleKnownErrors(const Status& bg_err,
|
||||
BackgroundErrorReason reason) {
|
||||
BackgroundErrorReason reason,
|
||||
const DBRecoverContext& context,
|
||||
bool allow_auto_recovery) {
|
||||
db_mutex_->AssertHeld();
|
||||
if (bg_err.ok()) {
|
||||
return;
|
||||
@@ -302,7 +306,6 @@ void ErrorHandler::HandleKnownErrors(const Status& bg_err,
|
||||
bool paranoid = db_options_.paranoid_checks;
|
||||
Status::Severity sev = Status::Severity::kFatalError;
|
||||
Status new_bg_err;
|
||||
DBRecoverContext context;
|
||||
bool found = false;
|
||||
|
||||
{
|
||||
@@ -338,7 +341,7 @@ void ErrorHandler::HandleKnownErrors(const Status& bg_err,
|
||||
recovery_error_ = status_to_io_status(Status(new_bg_err));
|
||||
}
|
||||
|
||||
bool auto_recovery = auto_recovery_;
|
||||
bool auto_recovery = allow_auto_recovery && auto_recovery_;
|
||||
if (new_bg_err.severity() >= Status::Severity::kFatalError && auto_recovery) {
|
||||
auto_recovery = false;
|
||||
}
|
||||
@@ -353,9 +356,13 @@ void ErrorHandler::HandleKnownErrors(const Status& bg_err,
|
||||
Status s = new_bg_err;
|
||||
EventHelpers::NotifyOnBackgroundError(db_options_.listeners, reason, &s,
|
||||
db_mutex_, &auto_recovery);
|
||||
if (!s.ok() && (s.severity() > bg_error_.severity())) {
|
||||
if (!s.ok() && s.severity() > bg_error_.severity()) {
|
||||
bg_error_ = s;
|
||||
UpdateRecoveryContext(context, /*replace_existing_context=*/true);
|
||||
} else {
|
||||
if (!s.ok()) {
|
||||
UpdateRecoveryContext(context, /*replace_existing_context=*/false);
|
||||
}
|
||||
ROCKS_LOG_INFO(db_options_.info_log,
|
||||
"ErrorHandler: Hit less severe background error\n");
|
||||
|
||||
@@ -371,7 +378,6 @@ void ErrorHandler::HandleKnownErrors(const Status& bg_err,
|
||||
"ErrorHandler: Set regular background error, auto_recovery=%d, stop=%d\n",
|
||||
int{auto_recovery}, int{stop});
|
||||
|
||||
recover_context_ = context;
|
||||
if (auto_recovery) {
|
||||
recovery_in_prog_ = true;
|
||||
|
||||
@@ -412,6 +418,27 @@ void ErrorHandler::HandleKnownErrors(const Status& bg_err,
|
||||
// such as delegating to SstFileManager to handle no space error.
|
||||
void ErrorHandler::SetBGError(const Status& bg_status,
|
||||
BackgroundErrorReason reason, bool wal_related) {
|
||||
SetBGErrorImpl(bg_status, reason, wal_related, DBRecoverContext(),
|
||||
/*allow_auto_recovery=*/true);
|
||||
}
|
||||
|
||||
void ErrorHandler::SetBGError(const Status& bg_status,
|
||||
BackgroundErrorReason reason, bool wal_related,
|
||||
DBRecoverContext context) {
|
||||
SetBGErrorImpl(bg_status, reason, wal_related, std::move(context),
|
||||
/*allow_auto_recovery=*/true);
|
||||
}
|
||||
|
||||
void ErrorHandler::SetBGErrorNoAutoRecovery(const Status& bg_status,
|
||||
BackgroundErrorReason reason) {
|
||||
SetBGErrorImpl(bg_status, reason, /*wal_related=*/false, DBRecoverContext(),
|
||||
/*allow_auto_recovery=*/false);
|
||||
}
|
||||
|
||||
void ErrorHandler::SetBGErrorImpl(const Status& bg_status,
|
||||
BackgroundErrorReason reason,
|
||||
bool wal_related, DBRecoverContext context,
|
||||
bool allow_auto_recovery) {
|
||||
db_mutex_->AssertHeld();
|
||||
Status tmp_status = bg_status;
|
||||
IOStatus bg_io_err = status_to_io_status(std::move(tmp_status));
|
||||
@@ -421,18 +448,34 @@ void ErrorHandler::SetBGError(const Status& bg_status,
|
||||
}
|
||||
ROCKS_LOG_WARN(db_options_.info_log, "Background IO error %s, reason %d",
|
||||
bg_io_err.ToString().c_str(), static_cast<int>(reason));
|
||||
if (context.failed_wal_number != 0) {
|
||||
ROCKS_LOG_WARN(
|
||||
db_options_.info_log,
|
||||
"[WAL recovery] Quarantined WALs through #%" PRIu64
|
||||
" after a file-scoped error; indeterminate sequence range=[%" PRIu64
|
||||
", %" PRIu64 "], status=%s",
|
||||
context.failed_wal_number, context.failed_wal_sequence_start,
|
||||
context.failed_wal_sequence, bg_io_err.ToString().c_str());
|
||||
}
|
||||
|
||||
RecordStats({ERROR_HANDLER_BG_ERROR_COUNT, ERROR_HANDLER_BG_IO_ERROR_COUNT},
|
||||
{} /* int_histograms */);
|
||||
|
||||
Status new_bg_io_err = bg_io_err;
|
||||
DBRecoverContext context;
|
||||
if (bg_io_err.GetScope() != IOStatus::IOErrorScope::kIOErrorScopeFile &&
|
||||
bg_io_err.GetDataLoss()) {
|
||||
// First, data loss (non file scope) is treated as unrecoverable error. So
|
||||
// it can directly overwrite any existing bg_error_.
|
||||
bool auto_recovery = false;
|
||||
Status bg_err(new_bg_io_err, Status::Severity::kUnrecoverableError);
|
||||
if (!allow_auto_recovery) {
|
||||
EventHelpers::NotifyOnBackgroundError(db_options_.listeners, reason,
|
||||
&bg_err, db_mutex_, &auto_recovery);
|
||||
if (!bg_err.ok()) {
|
||||
CheckAndSetRecoveryAndBGError(bg_err, context);
|
||||
}
|
||||
return;
|
||||
}
|
||||
CheckAndSetRecoveryAndBGError(bg_err, context);
|
||||
ROCKS_LOG_INFO(
|
||||
db_options_.info_log,
|
||||
@@ -448,12 +491,11 @@ void ErrorHandler::SetBGError(const Status& bg_status,
|
||||
}
|
||||
if (db_options_.manual_wal_flush && wal_related && bg_io_err.IsIOError()) {
|
||||
// With manual_wal_flush, a WAL write failure can drop buffered WAL writes.
|
||||
// Memtables and WAL then become inconsistent. A successful memtable flush
|
||||
// on one CF can cause CFs to be inconsistent upon restart. Before we fix
|
||||
// the bug in auto recovery from WAL write failures that can flush one CF
|
||||
// at a time, we set the error severity to fatal to disallow auto recovery.
|
||||
// TODO: remove parameter `wal_related` once we can automatically recover
|
||||
// from WAL write failures.
|
||||
// Memtables and WAL can then become inconsistent. Full error recovery now
|
||||
// flushes all CFs atomically, but automatically recovering this case still
|
||||
// needs dedicated analysis and testing. Keep the error fatal until then.
|
||||
// TODO: Re-evaluate `wal_related` and automatic recovery for
|
||||
// manual_wal_flush WAL failures.
|
||||
bool auto_recovery = false;
|
||||
Status bg_err(new_bg_io_err, Status::Severity::kFatalError);
|
||||
CheckAndSetRecoveryAndBGError(bg_err, context);
|
||||
@@ -517,6 +559,15 @@ void ErrorHandler::SetBGError(const Status& bg_status,
|
||||
severity = Status::Severity::kHardError;
|
||||
}
|
||||
Status bg_err(new_bg_io_err, severity);
|
||||
if (!allow_auto_recovery) {
|
||||
bool auto_recovery = false;
|
||||
EventHelpers::NotifyOnBackgroundError(db_options_.listeners, reason,
|
||||
&bg_err, db_mutex_, &auto_recovery);
|
||||
if (!bg_err.ok()) {
|
||||
CheckAndSetRecoveryAndBGError(bg_err, context);
|
||||
}
|
||||
return;
|
||||
}
|
||||
CheckAndSetRecoveryAndBGError(bg_err, context);
|
||||
bool auto_recovery = db_options_.max_bgerror_resume_count > 0;
|
||||
EventHelpers::NotifyOnBackgroundError(db_options_.listeners, reason,
|
||||
@@ -525,7 +576,7 @@ void ErrorHandler::SetBGError(const Status& bg_status,
|
||||
StartRecoverFromRetryableBGIOError(bg_io_err);
|
||||
return;
|
||||
}
|
||||
HandleKnownErrors(new_bg_io_err, reason);
|
||||
HandleKnownErrors(new_bg_io_err, reason, context, allow_auto_recovery);
|
||||
}
|
||||
|
||||
void ErrorHandler::AddFilesToQuarantine(
|
||||
@@ -610,6 +661,10 @@ Status ErrorHandler::ClearBGError() {
|
||||
is_db_stopped_.store(false, std::memory_order_release);
|
||||
bg_error_ = Status::OK();
|
||||
recovery_error_ = IOStatus::OK();
|
||||
// Recovery facts belong to one background-error episode. Keeping a
|
||||
// retired WAL cutoff or reserved sequence here would attach it to a later,
|
||||
// unrelated error when UpdateRecoveryContext() merges the next context.
|
||||
recover_context_ = DBRecoverContext();
|
||||
bg_error_.PermitUncheckedError();
|
||||
recovery_error_.PermitUncheckedError();
|
||||
recovery_in_prog_ = false;
|
||||
@@ -657,6 +712,15 @@ Status ErrorHandler::RecoverFromBGError(bool is_manual) {
|
||||
recovery_error_.PermitUncheckedError();
|
||||
DBRecoverContext context = recover_context_;
|
||||
context.flush_after_recovery = true;
|
||||
TEST_SYNC_POINT_CALLBACK("ErrorHandler::RecoverFromBGError:Context",
|
||||
&context);
|
||||
if (context.failed_wal_number != 0 || context.failed_wal_sequence != 0) {
|
||||
ROCKS_LOG_INFO(db_options_.info_log,
|
||||
"[WAL recovery] Starting requested recovery: cutoff=%" PRIu64
|
||||
", indeterminate sequence range=[%" PRIu64 ", %" PRIu64 "]",
|
||||
context.failed_wal_number, context.failed_wal_sequence_start,
|
||||
context.failed_wal_sequence);
|
||||
}
|
||||
Status s = db_->ResumeImpl(context, Env::IOActivity::kFlush);
|
||||
if (s.ok()) {
|
||||
soft_error_no_bg_work_ = false;
|
||||
@@ -754,6 +818,15 @@ void ErrorHandler::RecoverFromRetryableBGIOError() {
|
||||
DBRecoverContext context = recover_context_;
|
||||
context.flush_after_recovery = true;
|
||||
retry_count++;
|
||||
if (context.failed_wal_number != 0 || context.failed_wal_sequence != 0) {
|
||||
ROCKS_LOG_INFO(
|
||||
db_options_.info_log,
|
||||
"[WAL recovery] Starting automatic recovery attempt #%" PRIu64
|
||||
": cutoff=%" PRIu64 ", indeterminate sequence range=[%" PRIu64
|
||||
", %" PRIu64 "]",
|
||||
retry_count, context.failed_wal_number,
|
||||
context.failed_wal_sequence_start, context.failed_wal_sequence);
|
||||
}
|
||||
Status s = db_->ResumeImpl(context, Env::IOActivity::kFlush);
|
||||
RecordStats({ERROR_HANDLER_AUTORESUME_RETRY_TOTAL_COUNT},
|
||||
{} /* int_histograms */);
|
||||
@@ -819,15 +892,40 @@ void ErrorHandler::CheckAndSetRecoveryAndBGError(
|
||||
if (recovery_in_prog_ && recovery_error_.ok()) {
|
||||
recovery_error_ = status_to_io_status(Status(bg_err));
|
||||
}
|
||||
if (bg_err.severity() > bg_error_.severity()) {
|
||||
if (!bg_err.ok() && bg_err.severity() > bg_error_.severity()) {
|
||||
bg_error_ = bg_err;
|
||||
recover_context_ = context;
|
||||
UpdateRecoveryContext(context, /*replace_existing_context=*/true);
|
||||
} else {
|
||||
UpdateRecoveryContext(context, /*replace_existing_context=*/false);
|
||||
}
|
||||
if (bg_error_.severity() >= Status::Severity::kHardError) {
|
||||
is_db_stopped_.store(true, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
void ErrorHandler::UpdateRecoveryContext(const DBRecoverContext& context,
|
||||
bool replace_existing_context) {
|
||||
db_mutex_->AssertHeld();
|
||||
const uint64_t failed_wal_number =
|
||||
std::max(recover_context_.failed_wal_number, context.failed_wal_number);
|
||||
uint64_t failed_wal_sequence = recover_context_.failed_wal_sequence;
|
||||
uint64_t failed_wal_sequence_start =
|
||||
recover_context_.failed_wal_sequence_start;
|
||||
if (context.failed_wal_sequence > failed_wal_sequence) {
|
||||
failed_wal_sequence = context.failed_wal_sequence;
|
||||
failed_wal_sequence_start = context.failed_wal_sequence_start;
|
||||
}
|
||||
if (replace_existing_context) {
|
||||
recover_context_ = context;
|
||||
}
|
||||
// WAL failures can race or be classified through different severity paths.
|
||||
// Preserve the broadest failed-file cutoff and highest sequence assigned to
|
||||
// an indeterminate write group independently of the winning Status.
|
||||
recover_context_.failed_wal_number = failed_wal_number;
|
||||
recover_context_.failed_wal_sequence = failed_wal_sequence;
|
||||
recover_context_.failed_wal_sequence_start = failed_wal_sequence_start;
|
||||
}
|
||||
|
||||
void ErrorHandler::EndAutoRecovery() {
|
||||
db_mutex_->AssertHeld();
|
||||
if (!end_recovery_) {
|
||||
|
||||
+39
-4
@@ -23,12 +23,33 @@ class DBImpl;
|
||||
struct DBRecoverContext {
|
||||
FlushReason flush_reason;
|
||||
bool flush_after_recovery;
|
||||
// A non-zero value is an inclusive upper bound on WAL file handles that
|
||||
// recovery must not reuse. Recovery must not declare success until every
|
||||
// column family has atomically advanced beyond it in the MANIFEST.
|
||||
uint64_t failed_wal_number;
|
||||
// Highest sequence assigned to the write group whose WAL outcome is
|
||||
// indeterminate. Reserving it prevents a later acknowledged write from
|
||||
// reusing a sequence that might exist in the abandoned WAL.
|
||||
uint64_t failed_wal_sequence;
|
||||
// First sequence in the same indeterminate write group. Generic errors keep
|
||||
// this evidence without reserving it, in case a retry later establishes that
|
||||
// the WAL handle is file-scoped and activates the stronger recovery path.
|
||||
uint64_t failed_wal_sequence_start;
|
||||
|
||||
DBRecoverContext()
|
||||
: flush_reason(FlushReason::kErrorRecovery),
|
||||
flush_after_recovery(false) {}
|
||||
flush_after_recovery(false),
|
||||
failed_wal_number(0),
|
||||
failed_wal_sequence(0),
|
||||
failed_wal_sequence_start(0) {}
|
||||
DBRecoverContext(FlushReason reason)
|
||||
: flush_reason(reason), flush_after_recovery(false) {}
|
||||
: flush_reason(reason),
|
||||
flush_after_recovery(false),
|
||||
failed_wal_number(0),
|
||||
failed_wal_sequence(0),
|
||||
failed_wal_sequence_start(0) {}
|
||||
|
||||
bool IsWALWriteErrorRecovery() const { return failed_wal_number != 0; }
|
||||
};
|
||||
|
||||
class ErrorHandler {
|
||||
@@ -59,6 +80,11 @@ class ErrorHandler {
|
||||
|
||||
void SetBGError(const Status& bg_err, BackgroundErrorReason reason,
|
||||
bool wal_related = false);
|
||||
void SetBGError(const Status& bg_err, BackgroundErrorReason reason,
|
||||
bool wal_related, DBRecoverContext context);
|
||||
|
||||
void SetBGErrorNoAutoRecovery(const Status& bg_err,
|
||||
BackgroundErrorReason reason);
|
||||
|
||||
Status GetBGError() const { return bg_error_; }
|
||||
|
||||
@@ -151,13 +177,22 @@ class ErrorHandler {
|
||||
// unsorted.
|
||||
autovector<uint64_t> files_to_quarantine_;
|
||||
|
||||
void HandleKnownErrors(const Status& bg_err, BackgroundErrorReason reason);
|
||||
void SetBGErrorImpl(const Status& bg_err, BackgroundErrorReason reason,
|
||||
bool wal_related, DBRecoverContext context,
|
||||
bool allow_auto_recovery);
|
||||
void HandleKnownErrors(const Status& bg_err, BackgroundErrorReason reason,
|
||||
const DBRecoverContext& context,
|
||||
bool allow_auto_recovery);
|
||||
// REQUIRES: db_mutex_ held. When replacing the general recovery context,
|
||||
// retain WAL facts accumulated from concurrent errors independently.
|
||||
void UpdateRecoveryContext(const DBRecoverContext& context,
|
||||
bool replace_existing_context);
|
||||
Status OverrideNoSpaceError(const Status& bg_error, bool* auto_recovery);
|
||||
void RecoverFromNoSpace();
|
||||
void StartRecoverFromRetryableBGIOError(const IOStatus& io_error);
|
||||
void RecoverFromRetryableBGIOError();
|
||||
// If recovery is in progress, record the first new error for the current
|
||||
// attempt. If this error is more severe, retain it and its recovery context.
|
||||
// attempt. A more severe error replaces the current background error.
|
||||
void CheckAndSetRecoveryAndBGError(const Status& bg_err,
|
||||
const DBRecoverContext& context);
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Loaded 100 of 378 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user