mirror of
https://github.com/facebook/rocksdb.git
synced 2026-11-07 03:22:34 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b44608914 | ||
|
|
83a5b52eda | ||
|
|
6cdeb9d9d0 | ||
|
|
ea72ee89da | ||
|
|
37c3a6de09 | ||
|
|
ede2cbd67b | ||
|
|
b21eaa91c2 | ||
|
|
405e1a4ac6 | ||
|
|
a201a3c61a | ||
|
|
77a0e347bb | ||
|
|
89e384ca6f | ||
|
|
3d99378791 | ||
|
|
f455ab7bd6 | ||
|
|
63c86160cd | ||
|
|
89322fdd9e | ||
|
|
b23fc77aca | ||
|
|
b6f498b2c9 | ||
|
|
bcaf2794dc | ||
|
|
1cb7594b87 | ||
|
|
166b4a4487 | ||
|
|
03bded03b4 | ||
|
|
ec22903914 | ||
|
|
0c6f741422 | ||
|
|
c66c14258f | ||
|
|
e43171d6ca | ||
|
|
1e1097d8d3 | ||
|
|
e44a99f560 | ||
|
|
5494bc1d76 | ||
|
|
91f227d9be | ||
|
|
cb8bc56d14 | ||
|
|
1ed5052486 | ||
|
|
db9449a154 | ||
|
|
26aff32db3 | ||
|
|
2afb387917 | ||
|
|
a066318a31 | ||
|
|
42eff8b632 | ||
|
|
cb43abb1f1 | ||
|
|
e27114b135 | ||
|
|
3ad23b2d94 | ||
|
|
8a86620653 | ||
|
|
071b5eff7a | ||
|
|
3aa706c2bf | ||
|
|
2dc6d6f765 | ||
|
|
ef5b145140 | ||
|
|
3b5cb114e3 | ||
|
|
16c81a27e9 | ||
|
|
3b66de3fac | ||
|
|
9a9cc1e2ef | ||
|
|
ea2f33c81a | ||
|
|
15f2f2bf6f | ||
|
|
9d3d3c4b61 | ||
|
|
cc19bd7555 | ||
|
|
c2580e2b7a | ||
|
|
f25fb41da6 | ||
|
|
b48d6f2c12 | ||
|
|
aa9160e8d7 | ||
|
|
da2c3c0ee6 | ||
|
|
8707abfc11 | ||
|
|
ce0fff9f41 | ||
|
|
372995470a | ||
|
|
1a8471b17e | ||
|
|
c311362ab6 | ||
|
|
5cb88bae30 | ||
|
|
300fb8cace | ||
|
|
5db2eb0f3c | ||
|
|
1ba60ee9ee | ||
|
|
e95f3759e9 | ||
|
|
de3616dc3b | ||
|
|
4213f9e14a | ||
|
|
49a4165e3a | ||
|
|
bd5b299b92 | ||
|
|
e7c3391640 | ||
|
|
e704352e5d | ||
|
|
901c88e37b | ||
|
|
eb5d12e744 | ||
|
|
4e11dd79e1 | ||
|
|
aa7571c3ad | ||
|
|
29819f37e1 | ||
|
|
6d6f7d825b | ||
|
|
4c89ff1102 | ||
|
|
f1a6759b1f | ||
|
|
520c3ecbf1 | ||
|
|
407f02da19 | ||
|
|
98002215d0 | ||
|
|
cfc2a523a3 | ||
|
|
3556c22059 | ||
|
|
61b4edd15e | ||
|
|
d3817f058d | ||
|
|
1f9d8ee302 | ||
|
|
641f4703ac | ||
|
|
d693d5ae26 | ||
|
|
653fd9c65b | ||
|
|
f065e1c95d | ||
|
|
49695ef868 | ||
|
|
09bda51c50 | ||
|
|
5f692d747c | ||
|
|
ebd1000008 | ||
|
|
821bd37d09 | ||
|
|
88ff4f6b12 | ||
|
|
b040ab83e1 | ||
|
|
9f47518676 | ||
|
|
871f79d6ef | ||
|
|
672389fd8c | ||
|
|
c33a4989ad | ||
|
|
7ecc12110c | ||
|
|
d72a471749 | ||
|
|
c3184220b8 | ||
|
|
d8b1893c9d | ||
|
|
3148c6cad4 | ||
|
|
56cb88ec79 | ||
|
|
51feb25567 | ||
|
|
6284a79847 | ||
|
|
8f9cb1a708 | ||
|
|
3695cb6767 | ||
|
|
a668dcbe8c | ||
|
|
6ac0da313e | ||
|
|
47344a0feb | ||
|
|
48ec45d7bb | ||
|
|
c2fab4629b | ||
|
|
82ff0678d4 | ||
|
|
92378eb3b8 | ||
|
|
d1b63738e0 | ||
|
|
feffb67303 | ||
|
|
27d70ecd74 | ||
|
|
656b734a5f | ||
|
|
21a8b5f77f | ||
|
|
e94df3db52 | ||
|
|
83d24db3d5 | ||
|
|
374c8dd635 | ||
|
|
053b0d54dc | ||
|
|
56d40be243 | ||
|
|
148f6c9845 | ||
|
|
6b5ccbbec6 | ||
|
|
80f3d86f21 | ||
|
|
2366f63e4f | ||
|
|
acfea34c91 | ||
|
|
de06ce37db | ||
|
|
a3fe685cdc | ||
|
|
cc691126e5 | ||
|
|
ad218cacf6 | ||
|
|
6a79e02ebd | ||
|
|
a9906f0dd0 | ||
|
|
b89d290c20 | ||
|
|
f84351de98 | ||
|
|
f312633aff | ||
|
|
a6af317476 | ||
|
|
eb5e1a2d1f | ||
|
|
ea5e649225 | ||
|
|
2a7a6a6d72 | ||
|
|
88aff40c97 | ||
|
|
c6d08d3efe | ||
|
|
a1af6f9f64 | ||
|
|
57036b68d9 | ||
|
|
256838180e | ||
|
|
2893c25ca2 | ||
|
|
a6325e9564 | ||
|
|
4bcec5ae89 | ||
|
|
4bf2bcdbb3 | ||
|
|
2b28885c80 | ||
|
|
429b36c22d | ||
|
|
6f03c3dfea | ||
|
|
a7c1acbe9f | ||
|
|
4e10e0bcac | ||
|
|
387cb4aae7 | ||
|
|
8b6f98cdb4 | ||
|
|
1cc1df8dab | ||
|
|
3818cc1aca | ||
|
|
276721cd10 | ||
|
|
45690d0f6a | ||
|
|
e77ba4bc95 | ||
|
|
7e9f54d56b | ||
|
|
41beb1422f | ||
|
|
9065ace05a | ||
|
|
5a06787a26 | ||
|
|
a1d8318563 | ||
|
|
eedf1fe068 | ||
|
|
80c4a67d6a | ||
|
|
5d0cf98e6c | ||
|
|
e3b5464785 | ||
|
|
7c48905ecd | ||
|
|
707e405492 | ||
|
|
340ac7ea6b | ||
|
|
d2fe0ee389 | ||
|
|
4951494a27 | ||
|
|
ac412b1095 | ||
|
|
9e14d06143 | ||
|
|
9c2c8f54fa | ||
|
|
42ba71fbbf | ||
|
|
35148aca91 | ||
|
|
8c7c8b8dab | ||
|
|
2f583aed8f | ||
|
|
c4bbad4dfe | ||
|
|
dc33c1adaf | ||
|
|
c76cacc696 | ||
|
|
8c8586aa23 | ||
|
|
678690274d | ||
|
|
0762586067 | ||
|
|
57a6fb9e3a | ||
|
|
b9951ded37 | ||
|
|
f6c9c3bf1c | ||
|
|
2cf81e0a20 | ||
|
|
c757f5b4e3 | ||
|
|
9fbb68be17 | ||
|
|
b33c547b06 | ||
|
|
b897c3789b | ||
|
|
5879f8b62b | ||
|
|
ea75cdc493 | ||
|
|
2bee29729a | ||
|
|
37176a4a44 | ||
|
|
7603712a88 | ||
|
|
d56da8c112 | ||
|
|
befa6b8050 | ||
|
|
9577b92b55 | ||
|
|
94d91daddb | ||
|
|
0eb5b43b4f | ||
|
|
1bb704b6e0 | ||
|
|
64817ae604 | ||
|
|
fd0b4e0cf0 | ||
|
|
12b85c8ce9 | ||
|
|
a3aa44a716 | ||
|
|
32f66712c8 | ||
|
|
10478b98a5 | ||
|
|
e687ca79b4 | ||
|
|
2edc660e28 | ||
|
|
dce33f9443 | ||
|
|
fac8222bfe | ||
|
|
144e9f1e42 | ||
|
|
e691965558 |
No files matched your search
+86
@@ -0,0 +1,86 @@
|
||||
# When making changes, verify the output of:
|
||||
# clang-tidy -list-checks
|
||||
---
|
||||
Checks: "-*,\
|
||||
bugprone-argument-comment,\
|
||||
bugprone-dangling-handle,\
|
||||
bugprone-fold-init-type,\
|
||||
bugprone-forward-declaration-namespace,\
|
||||
bugprone-forwarding-reference-overload,\
|
||||
bugprone-shadow,\
|
||||
bugprone-sizeof-*,\
|
||||
bugprone-string-constructor,\
|
||||
bugprone-undefined-memory-manipulation,\
|
||||
bugprone-unused-return-value,\
|
||||
bugprone-use-after-move,\
|
||||
cert-env33-c,\
|
||||
cert-err58-cpp,\
|
||||
cert-msc30-c,\
|
||||
cert-msc50-cpp,\
|
||||
clang-analyzer-*,\
|
||||
clang-diagnostic-*,\
|
||||
-clang-diagnostic-missing-designated-field-initializers,\
|
||||
concurrency-mt-unsafe,\
|
||||
cppcoreguidelines-avoid-non-const-global-variables,\
|
||||
cppcoreguidelines-missing-std-forward,\
|
||||
cppcoreguidelines-pro-type-member-init,\
|
||||
cppcoreguidelines-special-member-functions,\
|
||||
cppcoreguidelines-virtual-class-destructor,\
|
||||
google-build-using-namespace,\
|
||||
google-explicit-constructor,\
|
||||
google-readability-avoid-underscore-in-googletest-name,\
|
||||
misc-definitions-in-headers,\
|
||||
misc-redundant-expression,\
|
||||
modernize-make-shared,\
|
||||
modernize-use-emplace,\
|
||||
modernize-use-noexcept,\
|
||||
modernize-use-override,\
|
||||
modernize-use-using,\
|
||||
performance-faster-string-find,\
|
||||
performance-for-range-copy,\
|
||||
performance-implicit-conversion-in-loop,\
|
||||
performance-inefficient-algorithm,\
|
||||
performance-inefficient-string-concatenation,\
|
||||
performance-inefficient-vector-operation,\
|
||||
performance-move-const-arg,\
|
||||
performance-move-constructor-init,\
|
||||
performance-no-automatic-move,\
|
||||
performance-no-int-to-ptr,\
|
||||
performance-noexcept-move-constructor,\
|
||||
performance-noexcept-swap,\
|
||||
performance-trivially-destructible,\
|
||||
performance-type-promotion-in-math-fn,\
|
||||
performance-unnecessary-copy-initialization,\
|
||||
performance-unnecessary-value-param,\
|
||||
readability-braces-around-statements,\
|
||||
readability-duplicate-include,\
|
||||
readability-isolate-declaration,\
|
||||
readability-operators-representation,\
|
||||
readability-redundant-string-init"
|
||||
|
||||
WarningsAsErrors: "bugprone-use-after-move"
|
||||
|
||||
CheckOptions:
|
||||
- key: bugprone-easily-swappable-parameters.MinimumLength
|
||||
value: 4
|
||||
- key: cppcoreguidelines-avoid-non-const-global-variables.AllowThreadLocal
|
||||
value: true
|
||||
- key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor
|
||||
value: true
|
||||
- key: cppcoreguidelines-special-member-functions.AllowImplicitlyDeletedCopyOrMove
|
||||
value: true
|
||||
- key: modernize-use-using.IgnoreExternC
|
||||
value: true
|
||||
- key: performance-move-const-arg.CheckTriviallyCopyableMove
|
||||
value: false
|
||||
- key: performance-unnecessary-value-param.AllowedTypes
|
||||
value: '[Pp]ointer$;[Pp]tr$;[Rr]ef(erence)?$'
|
||||
- key: performance-unnecessary-copy-initialization.AllowedTypes
|
||||
value: '[Pp]ointer$;[Pp]tr$;[Rr]ef(erence)?$'
|
||||
- key: readability-operators-representation.BinaryOperators
|
||||
value: '&&;&=;&;|;~;!;!=;||;|=;^;^='
|
||||
- key: readability-redundant-string-init.StringNames
|
||||
value: '::std::basic_string'
|
||||
- key: readability-named-parameter.InsertPlainNamesInForwardDecls
|
||||
value: true
|
||||
...
|
||||
@@ -7,20 +7,27 @@ outputs:
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Extract FOLLY_COMMIT_HASH from Makefile
|
||||
- name: Extract FOLLY_MK_HASH
|
||||
id: extract-folly-hash
|
||||
shell: bash
|
||||
run: |
|
||||
FOLLY_COMMIT_HASH=$(grep '^FOLLY_COMMIT_HASH' Makefile | awk '{print $3}')
|
||||
echo "hash=$FOLLY_COMMIT_HASH" >> $GITHUB_OUTPUT
|
||||
FOLLY_MK_HASH=$(md5sum folly.mk | cut -d' ' -f1)
|
||||
echo "hash=$FOLLY_MK_HASH" >> $GITHUB_OUTPUT
|
||||
- name: Extract FOLLY_INSTALL_DIR
|
||||
id: extract-folly-install-dir
|
||||
shell: bash
|
||||
run: |
|
||||
FOLLY_INSTALL_DIR=$(cd third-party/folly && python3 build/fbcode_builder/getdeps.py show-inst-dir)
|
||||
echo "dir=$(echo $FOLLY_INSTALL_DIR | sed 's|installed/folly|installed|')" >> $GITHUB_OUTPUT
|
||||
- name: Cache folly build
|
||||
id: cache-folly-build
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
# Cache the folly build directory
|
||||
path: /tmp/fbcode_builder_getdeps-Z__wZrocksdbZrocksdbZthird-partyZfollyZbuildZfbcode_builder-root/installed
|
||||
path: ${{ steps.extract-folly-install-dir.outputs.dir }}
|
||||
# Key is based on:
|
||||
# - OS and architecture
|
||||
# - The specific folly commit hash from Makefile
|
||||
# - The container image version to account for different compiler/library versions
|
||||
key: folly-build-${{ runner.os }}-${{ runner.arch }}-${{ steps.extract-folly-hash.outputs.hash }}-ubuntu22.1-v1
|
||||
# - The docker image, which may not always be specified/known
|
||||
# - Hash of folly.mk, which includes the folly repository commit hash
|
||||
# NOTE: this is still only intended for DEBUG folly builds
|
||||
key: folly-build-${{ runner.os }}-${{ runner.arch }}-${{ github.job_container.image }}-${{ steps.extract-folly-hash.outputs.hash }}-${{ env.PORTABLE == '1' && 'portable' || 'native' }}
|
||||
@@ -0,0 +1,21 @@
|
||||
name: cache-getdeps-downloads
|
||||
description: Cache getdeps downloads to avoid unreliable mirrors and speed up builds
|
||||
outputs:
|
||||
cache-hit:
|
||||
description: Whether the cache was hit
|
||||
value: ${{ steps.cache-downloads.outputs.cache-hit }}
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Cache getdeps downloads
|
||||
id: cache-downloads
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
# Use a fixed path that we control - folly.mk will sync with getdeps downloads dir
|
||||
path: /tmp/rocksdb-getdeps-cache
|
||||
# Use a rolling cache key - the cache accumulates downloads over time
|
||||
# The key includes a weekly timestamp to ensure periodic refresh
|
||||
key: getdeps-downloads-${{ runner.os }}-${{ runner.arch }}-week-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
getdeps-downloads-${{ runner.os }}-${{ runner.arch }}-week-
|
||||
getdeps-downloads-${{ runner.os }}-${{ runner.arch }}-
|
||||
@@ -0,0 +1,49 @@
|
||||
name: setup-ccache
|
||||
description: Setup ccache for faster C++ compilation caching
|
||||
inputs:
|
||||
cache-key-prefix:
|
||||
description: Unique prefix for the cache key (e.g., 'build-linux')
|
||||
required: true
|
||||
portable:
|
||||
description: Set PORTABLE=1 to disable -march=native (set to "false" for jobs linking pre-built Folly)
|
||||
required: false
|
||||
default: "true"
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Set ccache environment variables
|
||||
run: |
|
||||
echo "CCACHE_DIR=${{ github.workspace }}/.ccache" >> $GITHUB_ENV
|
||||
echo "CCACHE_BASEDIR=${{ github.workspace }}" >> $GITHUB_ENV
|
||||
echo "CCACHE_NOHASHDIR=true" >> $GITHUB_ENV
|
||||
if [ "${{ inputs.portable }}" = "true" ]; then
|
||||
echo "PORTABLE=1" >> $GITHUB_ENV
|
||||
fi
|
||||
shell: bash
|
||||
- name: Restore ccache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ github.workspace }}/.ccache
|
||||
key: ccache-${{ inputs.cache-key-prefix }}-${{ github.head_ref || github.ref }}-${{ github.sha }}
|
||||
restore-keys: |-
|
||||
ccache-${{ inputs.cache-key-prefix }}-${{ github.head_ref || github.ref }}-
|
||||
ccache-${{ inputs.cache-key-prefix }}-refs/heads/main-
|
||||
- name: Install ccache
|
||||
run: |
|
||||
if [ "$RUNNER_OS" = "macOS" ]; then
|
||||
which ccache || brew install ccache
|
||||
else
|
||||
which ccache || (apt-get update && apt-get install -y ccache)
|
||||
fi
|
||||
shell: bash
|
||||
- name: Add ccache to PATH
|
||||
run: |
|
||||
if [ "$RUNNER_OS" = "macOS" ]; then
|
||||
echo "$(brew --prefix ccache)/libexec" >> $GITHUB_PATH
|
||||
else
|
||||
echo "/usr/lib/ccache" >> $GITHUB_PATH
|
||||
fi
|
||||
shell: bash
|
||||
- name: Zero ccache stats
|
||||
run: ccache -z
|
||||
shell: bash
|
||||
@@ -5,5 +5,7 @@ runs:
|
||||
- name: Checkout folly sources
|
||||
run: |
|
||||
make checkout_folly
|
||||
echo "GETDEPS_USE_WGET=1" >> "$GITHUB_ENV"
|
||||
shell: bash
|
||||
- name: Install patchelf and libaio
|
||||
run: apt-get update -y && apt-get install -y patchelf libaio-dev
|
||||
shell: bash
|
||||
@@ -4,16 +4,15 @@ runs:
|
||||
steps:
|
||||
- name: Add msbuild to PATH
|
||||
uses: microsoft/setup-msbuild@v1.3.1
|
||||
- name: Cache ccache directory
|
||||
id: ccache-cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: C:\a\rocksdb\rocksdb\.ccache
|
||||
key: rocksdb-build-${{ runner.os }}-${{ runner.arch }}-ccache-${{ hashFiles('CMakeLists.txt', 'cmake/**/*.cmake') }}-v1
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
with:
|
||||
max-size: "10GB"
|
||||
- name: Configure ccache
|
||||
shell: pwsh
|
||||
run: |
|
||||
ccache --set-config=base_dir=C:\a\rocksdb\rocksdb
|
||||
ccache --set-config=hash_dir=false
|
||||
- name: Custom steps
|
||||
env:
|
||||
THIRDPARTY_HOME: ${{ github.workspace }}/thirdparty
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// Parse Claude Code execution log and produce a markdown review comment.
|
||||
//
|
||||
// Usage from actions/github-script:
|
||||
// const parse = require('./.github/scripts/parse-claude-review.js');
|
||||
// const markdown = parse({ executionFile, conclusion, meta });
|
||||
//
|
||||
// Parameters:
|
||||
// executionFile - path to the JSON execution log from claude-code-base-action
|
||||
// conclusion - 'success' or 'failure' from the action output
|
||||
// meta - { trigger, headSha, reviewer, isQuery }
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
module.exports = function parseClaude({executionFile, conclusion, meta}) {
|
||||
let responseBody = '';
|
||||
|
||||
try {
|
||||
const executionLog = JSON.parse(fs.readFileSync(executionFile, 'utf8'));
|
||||
|
||||
if (!Array.isArray(executionLog)) {
|
||||
throw new Error('Expected array format from claude-code-base-action');
|
||||
}
|
||||
|
||||
const resultMessage = executionLog.find(m => m.type === 'result');
|
||||
|
||||
if (!resultMessage) {
|
||||
responseBody = '⚠️ No result message found in execution log.';
|
||||
} else if (resultMessage.subtype === 'success' && resultMessage.result) {
|
||||
responseBody = resultMessage.result;
|
||||
} else if (resultMessage.is_error || resultMessage.subtype === 'error') {
|
||||
const errorInfo =
|
||||
resultMessage.result || resultMessage.error || 'Unknown error';
|
||||
responseBody = `❌ **Claude encountered an error:**\n\n${errorInfo}`;
|
||||
} else if (resultMessage.result) {
|
||||
responseBody = `⚠️ **Completed with status: ${
|
||||
resultMessage.subtype}**\n\n${resultMessage.result}`;
|
||||
} else {
|
||||
responseBody = '⚠️ Claude completed but produced no output.';
|
||||
}
|
||||
} catch (error) {
|
||||
responseBody = `❌ Error parsing Claude response: ${error.message}`;
|
||||
}
|
||||
|
||||
const icon = conclusion === 'success' ? '✅' : '⚠️';
|
||||
const headerTitle = meta.isQuery ? 'Claude Response' : 'Claude Code Review';
|
||||
const triggerLine = meta.trigger === 'auto' ?
|
||||
`*Auto-triggered after CI passed — reviewing commit ${
|
||||
meta.headSha.substring(0, 7)}*` :
|
||||
`*Requested by @${meta.reviewer}*`;
|
||||
|
||||
return [
|
||||
`## ${icon} ${headerTitle}`,
|
||||
'',
|
||||
triggerLine,
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
responseBody,
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'<details>',
|
||||
'<summary>ℹ️ About this response</summary>',
|
||||
'',
|
||||
'Generated by [Claude Code](https://github.com/anthropics/claude-code-action).',
|
||||
'Review methodology: `claude_md/code_review.md`',
|
||||
'',
|
||||
'**Limitations:**',
|
||||
'- Claude may miss context from files not in the diff',
|
||||
'- Large PRs may be truncated',
|
||||
'- Always apply human judgment to AI suggestions',
|
||||
'',
|
||||
'**Commands:**',
|
||||
'- `/claude-review [context]` — Request a code review',
|
||||
'- `/claude-query <question>` — Ask about the PR or codebase',
|
||||
'</details>',
|
||||
].join('\n');
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
// Shared PR comment posting utility.
|
||||
// Used by both clang-tidy-comment.yml and claude-review-comment.yml.
|
||||
//
|
||||
// Usage from actions/github-script:
|
||||
// const post = require('./.github/scripts/post-pr-comment.js');
|
||||
// await post({ github, context, core, prNumber, body, marker });
|
||||
//
|
||||
// Parameters:
|
||||
// github - octokit instance from actions/github-script
|
||||
// context - GitHub Actions context
|
||||
// core - @actions/core for logging
|
||||
// prNumber - PR number to comment on
|
||||
// body - comment body (markdown string)
|
||||
// marker - HTML comment marker for dedup (e.g. '<!-- claude-review -->')
|
||||
// If an existing comment with this marker is found, it is updated.
|
||||
// If not found, a new comment is created.
|
||||
|
||||
module.exports = async function postPrComment(
|
||||
{github, context, core, prNumber, body, marker}) {
|
||||
if (!prNumber || !body) {
|
||||
core.warning('Missing prNumber or body; skipping comment.');
|
||||
return;
|
||||
}
|
||||
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
// Ensure marker is embedded in the body
|
||||
const markedBody = body.includes(marker) ? body : `${marker}\n${body}`;
|
||||
|
||||
// Search for existing comment with this marker
|
||||
const {data: comments} = await github.rest.issues.listComments({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
per_page: 100,
|
||||
});
|
||||
const existing = comments.find(c => c.body.includes(marker));
|
||||
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: existing.id,
|
||||
body: markedBody,
|
||||
});
|
||||
core.info(`Updated existing comment ${existing.id}`);
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
body: markedBody,
|
||||
});
|
||||
core.info('Created new PR comment');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
name: Post clang-tidy PR comment
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["clang-tidy"]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
if: github.event.workflow_run.event == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout scripts
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github/scripts
|
||||
sparse-checkout-cone-mode: true
|
||||
|
||||
- name: Download clang-tidy results
|
||||
id: download
|
||||
uses: actions/download-artifact@v4.0.0
|
||||
with:
|
||||
name: clang-tidy-result
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
continue-on-error: true
|
||||
|
||||
- name: Post or update PR comment
|
||||
if: steps.download.outcome == 'success'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
if (!fs.existsSync('clang-tidy-comment.md') || !fs.existsSync('pr_number.txt')) {
|
||||
core.info('No clang-tidy results found; skipping.');
|
||||
return;
|
||||
}
|
||||
const body = fs.readFileSync('clang-tidy-comment.md', 'utf8');
|
||||
const prNumber = parseInt(fs.readFileSync('pr_number.txt', 'utf8').trim());
|
||||
const post = require('./.github/scripts/post-pr-comment.js');
|
||||
|
||||
await post({
|
||||
github,
|
||||
context,
|
||||
core,
|
||||
prNumber,
|
||||
body,
|
||||
marker: '<!-- clang-tidy-bot -->',
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
name: clang-tidy
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
clang-tidy:
|
||||
if: github.repository_owner == 'facebook'
|
||||
runs-on:
|
||||
labels: 4-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- name: Mark workspace as safe for git
|
||||
run: git config --global --add safe.directory $GITHUB_WORKSPACE
|
||||
- name: Determine diff base
|
||||
id: diff-base
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||
BASE="${{ github.event.pull_request.base.sha }}"
|
||||
git fetch --depth=1 origin "$BASE"
|
||||
else
|
||||
BASE="${{ github.event.before }}"
|
||||
if echo "$BASE" | grep -q '^0\{40\}$'; then
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
echo "New branch push; skipping clang-tidy."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
echo "ref=$BASE" >> "$GITHUB_OUTPUT"
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
- name: Install clang-tidy
|
||||
if: steps.diff-base.outputs.skip != 'true'
|
||||
run: apt-get update && apt-get install -y clang-tidy
|
||||
- name: Generate compile_commands.json
|
||||
if: steps.diff-base.outputs.skip != 'true'
|
||||
run: |
|
||||
mkdir build && cd build
|
||||
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
|
||||
-DCMAKE_C_COMPILER=clang-18 \
|
||||
-DCMAKE_CXX_COMPILER=clang++-18 ..
|
||||
cd ..
|
||||
ln -sf build/compile_commands.json compile_commands.json
|
||||
- name: Run clang-tidy on changed files
|
||||
id: clang-tidy
|
||||
if: steps.diff-base.outputs.skip != 'true'
|
||||
run: |
|
||||
python3 tools/run_clang_tidy.py \
|
||||
-j 4 \
|
||||
--diff-base ${{ steps.diff-base.outputs.ref }} \
|
||||
--github-annotations \
|
||||
--github-step-summary \
|
||||
--comment-output clang-tidy-comment.md
|
||||
continue-on-error: true
|
||||
- name: Save PR number
|
||||
if: github.event_name == 'pull_request' && always()
|
||||
run: echo "${{ github.event.pull_request.number }}" > pr_number.txt
|
||||
- name: Upload clang-tidy results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4.0.0
|
||||
with:
|
||||
name: clang-tidy-result
|
||||
path: |
|
||||
clang-tidy-comment.md
|
||||
pr_number.txt
|
||||
if-no-files-found: ignore
|
||||
- name: Fail if clang-tidy found issues
|
||||
if: steps.clang-tidy.outcome == 'failure'
|
||||
run: exit 1
|
||||
@@ -0,0 +1,146 @@
|
||||
# Claude Code Review — Comment Posting Workflow
|
||||
#
|
||||
# Companion to claude-review.yml. Downloads the review artifact and posts
|
||||
# it as a PR comment. This workflow has write permissions but never runs
|
||||
# AI or processes untrusted code.
|
||||
#
|
||||
# Security: Separating analysis (has ANTHROPIC_API_KEY, no write perms)
|
||||
# from posting (has GITHUB_TOKEN write perms, no AI) prevents a crafted
|
||||
# PR from tricking Claude into exfiltrating tokens.
|
||||
|
||||
name: Post Claude Review Comment
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Claude Code Review"]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout scripts
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github/scripts
|
||||
sparse-checkout-cone-mode: true
|
||||
|
||||
- name: Download review artifact
|
||||
id: download
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: claude-review-result
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
continue-on-error: true
|
||||
|
||||
- name: Post or update PR comment
|
||||
if: steps.download.outcome == 'success'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
if (!fs.existsSync('claude-review-comment.md') || !fs.existsSync('pr_number.txt')) {
|
||||
core.info('No review results found; skipping.');
|
||||
return;
|
||||
}
|
||||
|
||||
const body = fs.readFileSync('claude-review-comment.md', 'utf8');
|
||||
const prNumber = parseInt(fs.readFileSync('pr_number.txt', 'utf8').trim());
|
||||
const trigger = fs.existsSync('trigger_type.txt')
|
||||
? fs.readFileSync('trigger_type.txt', 'utf8').trim()
|
||||
: 'auto';
|
||||
|
||||
// Use different markers for auto vs manual so they don't
|
||||
// overwrite each other. Auto-reviews update in place (one
|
||||
// per PR). Manual reviews always create new comments.
|
||||
const marker = trigger === 'auto'
|
||||
? '<!-- claude-review-auto -->'
|
||||
: `<!-- claude-review-manual-${Date.now()} -->`;
|
||||
|
||||
const post = require('./.github/scripts/post-pr-comment.js');
|
||||
|
||||
await post({
|
||||
github,
|
||||
context,
|
||||
core,
|
||||
prNumber,
|
||||
body,
|
||||
marker,
|
||||
});
|
||||
|
||||
- name: Add reaction to trigger comment
|
||||
if: steps.download.outcome == 'success'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
if (!fs.existsSync('trigger_type.txt')) return;
|
||||
const trigger = fs.readFileSync('trigger_type.txt', 'utf8').trim();
|
||||
if (trigger !== 'manual') return;
|
||||
if (!fs.existsSync('comment_id.txt')) return;
|
||||
const commentId = parseInt(fs.readFileSync('comment_id.txt', 'utf8').trim());
|
||||
if (!commentId) return;
|
||||
|
||||
await github.rest.reactions.createForIssueComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: commentId,
|
||||
content: 'rocket'
|
||||
});
|
||||
|
||||
# Notify on failure (for manual triggers)
|
||||
failure-notice:
|
||||
if: github.event.workflow_run.conclusion == 'failure'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download artifact for PR number
|
||||
id: download
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: claude-review-result
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
continue-on-error: true
|
||||
|
||||
- name: React with failure
|
||||
if: steps.download.outcome == 'success'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
if (!fs.existsSync('comment_id.txt')) return;
|
||||
const commentId = parseInt(fs.readFileSync('comment_id.txt', 'utf8').trim());
|
||||
if (!commentId) return;
|
||||
|
||||
await github.rest.reactions.createForIssueComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: commentId,
|
||||
content: 'confused'
|
||||
});
|
||||
|
||||
# Unauthorized user notice
|
||||
unauthorized-notice:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
if: >-
|
||||
github.event.workflow_run.event == 'issue_comment' &&
|
||||
github.event.workflow_run.conclusion == 'skipped'
|
||||
|
||||
steps:
|
||||
- name: Check if unauthorized
|
||||
id: check
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
// This job only runs when the analysis workflow was skipped,
|
||||
// which happens when the user is not in the authorized list.
|
||||
// We cannot easily get the original comment from workflow_run,
|
||||
// so this is a best-effort notice via workflow logs.
|
||||
core.info('Analysis workflow was skipped — likely unauthorized user.');
|
||||
@@ -0,0 +1,476 @@
|
||||
# Claude Code Review — Analysis Workflow
|
||||
#
|
||||
# This workflow runs Claude to review PRs. It produces a markdown review
|
||||
# as an artifact but does NOT post comments (no write permissions).
|
||||
# The companion workflow (claude-review-comment.yml) handles posting.
|
||||
#
|
||||
# Triggers:
|
||||
# 1. workflow_run: Auto-review after pr-jobs passes
|
||||
# 2. issue_comment: Manual /claude-review or /claude-query by maintainers
|
||||
# 3. workflow_dispatch: Manual testing
|
||||
#
|
||||
# Security:
|
||||
# - This job has contents:read ONLY (no PR write, no issue write)
|
||||
# - ANTHROPIC_API_KEY is used here but never coexists with write tokens
|
||||
# - Claude uses read-only tools (View, GlobTool, GrepTool)
|
||||
# - Review methodology: claude_md/code_review.md
|
||||
|
||||
name: Claude Code Review
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["facebook/rocksdb/pr-jobs"]
|
||||
types: [completed]
|
||||
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: PR number to review
|
||||
required: true
|
||||
type: number
|
||||
model:
|
||||
description: Claude model to use
|
||||
required: false
|
||||
type: choice
|
||||
options:
|
||||
- claude-opus-4-6
|
||||
- claude-sonnet-4-20250514
|
||||
default: claude-opus-4-6
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
# ======================== Auto Review ======================== #
|
||||
auto-review:
|
||||
name: Auto Claude Review
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
if: >-
|
||||
github.event_name == 'workflow_run' &&
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event == 'pull_request'
|
||||
|
||||
steps:
|
||||
- name: Get PR info
|
||||
id: pr_info
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const headSha = context.payload.workflow_run.head_sha;
|
||||
let prNumber = null;
|
||||
|
||||
const prs = context.payload.workflow_run.pull_requests;
|
||||
if (prs && prs.length > 0) {
|
||||
prNumber = prs[0].number;
|
||||
} else {
|
||||
const { data: pulls } = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
sort: 'updated',
|
||||
direction: 'desc',
|
||||
per_page: 30
|
||||
});
|
||||
const match = pulls.find(p => p.head.sha === headSha);
|
||||
if (match) prNumber = match.number;
|
||||
}
|
||||
|
||||
if (!prNumber) {
|
||||
core.setFailed(`Could not find PR for SHA ${headSha}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const pr = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber
|
||||
});
|
||||
|
||||
core.setOutput('pr_number', prNumber.toString());
|
||||
core.setOutput('head_repo', pr.data.head.repo.clone_url);
|
||||
core.setOutput('head_ref', pr.data.head.ref);
|
||||
core.setOutput('head_sha', pr.data.head.sha);
|
||||
core.setOutput('base_sha', pr.data.base.sha);
|
||||
core.setOutput('base_ref', pr.data.base.ref);
|
||||
core.setOutput('pr_title', pr.data.title);
|
||||
core.setOutput('pr_body', pr.data.body || '');
|
||||
core.setOutput('pr_author', pr.data.user.login);
|
||||
|
||||
- name: Check for existing review
|
||||
id: check_existing
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PR_NUMBER: ${{ steps.pr_info.outputs.pr_number }}
|
||||
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
|
||||
with:
|
||||
script: |
|
||||
const comments = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: parseInt(process.env.PR_NUMBER),
|
||||
per_page: 100
|
||||
});
|
||||
const shortSha = process.env.HEAD_SHA.substring(0, 7);
|
||||
const existing = comments.data.find(c =>
|
||||
c.body.includes('<!-- claude-review -->') &&
|
||||
c.body.includes(shortSha)
|
||||
);
|
||||
core.setOutput('skip', existing ? 'true' : 'false');
|
||||
|
||||
- name: Checkout base repository
|
||||
if: steps.check_existing.outputs.skip != 'true'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.pr_info.outputs.base_ref }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Generate diff and prompt
|
||||
if: steps.check_existing.outputs.skip != 'true'
|
||||
env:
|
||||
HEAD_REPO: ${{ steps.pr_info.outputs.head_repo }}
|
||||
HEAD_REF: ${{ steps.pr_info.outputs.head_ref }}
|
||||
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
|
||||
BASE_SHA: ${{ steps.pr_info.outputs.base_sha }}
|
||||
PR_TITLE: ${{ steps.pr_info.outputs.pr_title }}
|
||||
PR_BODY: ${{ steps.pr_info.outputs.pr_body }}
|
||||
PR_AUTHOR: ${{ steps.pr_info.outputs.pr_author }}
|
||||
run: |
|
||||
git remote add fork "${HEAD_REPO}" || true
|
||||
git fetch fork "${HEAD_REF}" --depth=100
|
||||
|
||||
git diff "${BASE_SHA}...${HEAD_SHA}" > /tmp/pr.diff
|
||||
DIFF_STATS=$(git diff --stat "${BASE_SHA}...${HEAD_SHA}" | tail -1)
|
||||
|
||||
DIFF_SIZE=$(wc -c < /tmp/pr.diff)
|
||||
IS_TRUNCATED=false
|
||||
if [ "$DIFF_SIZE" -gt 100000 ]; then
|
||||
head -c 100000 /tmp/pr.diff > /tmp/pr_truncated.diff
|
||||
mv /tmp/pr_truncated.diff /tmp/pr.diff
|
||||
IS_TRUNCATED=true
|
||||
fi
|
||||
|
||||
cat > /tmp/prompt.txt << 'PROMPT_EOF'
|
||||
Read claude_md/code_review.md for the full review methodology, then
|
||||
review the pull request described below. Follow all perspectives and
|
||||
the output format defined in that document.
|
||||
PROMPT_EOF
|
||||
|
||||
cat >> /tmp/prompt.txt << PROMPT_EOF
|
||||
|
||||
## Pull Request Information
|
||||
- **Title:** ${PR_TITLE}
|
||||
- **Author:** ${PR_AUTHOR}
|
||||
- **Changes:** ${DIFF_STATS}
|
||||
PROMPT_EOF
|
||||
|
||||
if [ "${IS_TRUNCATED}" = "true" ]; then
|
||||
echo "- **Note:** Diff truncated due to size." >> /tmp/prompt.txt
|
||||
fi
|
||||
|
||||
printf '\n## PR Description\n' >> /tmp/prompt.txt
|
||||
printf '%s\n' "${PR_BODY}" >> /tmp/prompt.txt
|
||||
printf '\n---\n\n## Diff to Review\n\n' >> /tmp/prompt.txt
|
||||
cat /tmp/pr.diff >> /tmp/prompt.txt
|
||||
|
||||
- name: Run Claude
|
||||
if: steps.check_existing.outputs.skip != 'true'
|
||||
id: claude
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
with:
|
||||
prompt_file: /tmp/prompt.txt
|
||||
model: claude-opus-4-6
|
||||
max_turns: "30"
|
||||
allowed_tools: "View,GlobTool,GrepTool"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
|
||||
- name: Build review comment
|
||||
if: steps.check_existing.outputs.skip != 'true'
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }}
|
||||
CONCLUSION: ${{ steps.claude.outputs.conclusion }}
|
||||
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
|
||||
with:
|
||||
script: |
|
||||
const parse = require('./.github/scripts/parse-claude-review.js');
|
||||
const body = parse({
|
||||
executionFile: process.env.EXECUTION_FILE,
|
||||
conclusion: process.env.CONCLUSION,
|
||||
meta: {
|
||||
trigger: 'auto',
|
||||
headSha: process.env.HEAD_SHA,
|
||||
reviewer: '',
|
||||
}
|
||||
});
|
||||
const fs = require('fs');
|
||||
fs.writeFileSync('claude-review-comment.md', body);
|
||||
|
||||
- name: Save PR number
|
||||
if: steps.check_existing.outputs.skip != 'true'
|
||||
run: echo "${{ steps.pr_info.outputs.pr_number }}" > pr_number.txt
|
||||
|
||||
- name: Save trigger metadata
|
||||
if: steps.check_existing.outputs.skip != 'true'
|
||||
run: echo "auto" > trigger_type.txt
|
||||
|
||||
- name: Upload review artifact
|
||||
if: always() && steps.check_existing.outputs.skip != 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: claude-review-result
|
||||
path: |
|
||||
claude-review-comment.md
|
||||
pr_number.txt
|
||||
trigger_type.txt
|
||||
if-no-files-found: ignore
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload execution log
|
||||
if: always() && steps.check_existing.outputs.skip != 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: claude-review-execution-log
|
||||
path: ${{ steps.claude.outputs.execution_file }}
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
|
||||
# ======================== Manual Review ======================== #
|
||||
manual-review:
|
||||
name: Manual Claude Review
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
github.event_name == 'issue_comment' &&
|
||||
github.event.issue.pull_request &&
|
||||
(contains(github.event.comment.body, '/claude-review') || contains(github.event.comment.body, '/claude-query')) &&
|
||||
contains(fromJSON('["pdillinger", "jaykorean", "hx235", "dannyhchen", "joshkang97", "zaidoon1", "omkarhgawde", "xingbowang", "anand1976", "virajthakur", "nmk70", "archang19", "mszeszko-meta", "yoshinorim", "cbi42"]'), github.event.comment.user.login)
|
||||
)
|
||||
|
||||
steps:
|
||||
- name: Detect command type
|
||||
id: command
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
if (context.eventName === 'workflow_dispatch') {
|
||||
core.setOutput('type', 'review');
|
||||
core.setOutput('query', '');
|
||||
return;
|
||||
}
|
||||
const body = context.payload.comment.body;
|
||||
if (body.includes('/claude-review')) {
|
||||
core.setOutput('type', 'review');
|
||||
const match = body.match(/\/claude-review\s+([\s\S]*)/);
|
||||
core.setOutput('query', match ? match[1].trim() : '');
|
||||
} else if (body.includes('/claude-query')) {
|
||||
const match = body.match(/\/claude-query\s+([\s\S]*)/);
|
||||
const query = match ? match[1].trim() : '';
|
||||
if (!query) {
|
||||
core.setFailed('No query provided. Usage: /claude-query <your question>');
|
||||
return;
|
||||
}
|
||||
core.setOutput('type', 'query');
|
||||
core.setOutput('query', query);
|
||||
} else {
|
||||
core.setFailed('Unknown command');
|
||||
}
|
||||
|
||||
- name: Get PR details
|
||||
id: pr_info
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
INPUT_PR_NUMBER: ${{ inputs.pr_number }}
|
||||
with:
|
||||
script: |
|
||||
const prNumber = context.eventName === 'workflow_dispatch'
|
||||
? parseInt(process.env.INPUT_PR_NUMBER, 10)
|
||||
: context.issue.number;
|
||||
|
||||
const pr = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber
|
||||
});
|
||||
|
||||
core.setOutput('pr_number', prNumber.toString());
|
||||
core.setOutput('head_repo', pr.data.head.repo.clone_url);
|
||||
core.setOutput('head_ref', pr.data.head.ref);
|
||||
core.setOutput('head_sha', pr.data.head.sha);
|
||||
core.setOutput('base_sha', pr.data.base.sha);
|
||||
core.setOutput('base_ref', pr.data.base.ref);
|
||||
core.setOutput('pr_title', pr.data.title);
|
||||
core.setOutput('pr_body', pr.data.body || '');
|
||||
core.setOutput('pr_author', pr.data.user.login);
|
||||
|
||||
- name: Checkout base repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.pr_info.outputs.base_ref }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Generate diff and prompt
|
||||
env:
|
||||
HEAD_REPO: ${{ steps.pr_info.outputs.head_repo }}
|
||||
HEAD_REF: ${{ steps.pr_info.outputs.head_ref }}
|
||||
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
|
||||
BASE_SHA: ${{ steps.pr_info.outputs.base_sha }}
|
||||
PR_TITLE: ${{ steps.pr_info.outputs.pr_title }}
|
||||
PR_BODY: ${{ steps.pr_info.outputs.pr_body }}
|
||||
PR_AUTHOR: ${{ steps.pr_info.outputs.pr_author }}
|
||||
DIFF_STATS: ""
|
||||
COMMAND_TYPE: ${{ steps.command.outputs.type }}
|
||||
ADDITIONAL_CONTEXT: ${{ steps.command.outputs.query }}
|
||||
USER_QUERY: ${{ steps.command.outputs.query }}
|
||||
run: |
|
||||
git remote add fork "${HEAD_REPO}" || true
|
||||
git fetch fork "${HEAD_REF}" --depth=100
|
||||
|
||||
git diff "${BASE_SHA}...${HEAD_SHA}" > /tmp/pr.diff
|
||||
DIFF_STATS=$(git diff --stat "${BASE_SHA}...${HEAD_SHA}" | tail -1)
|
||||
|
||||
DIFF_SIZE=$(wc -c < /tmp/pr.diff)
|
||||
IS_TRUNCATED=false
|
||||
if [ "$DIFF_SIZE" -gt 100000 ]; then
|
||||
head -c 100000 /tmp/pr.diff > /tmp/pr_truncated.diff
|
||||
mv /tmp/pr_truncated.diff /tmp/pr.diff
|
||||
IS_TRUNCATED=true
|
||||
fi
|
||||
|
||||
if [ "${COMMAND_TYPE}" = "query" ]; then
|
||||
# Query prompt
|
||||
cat > /tmp/prompt.txt << 'PROMPT_EOF'
|
||||
You are an expert C++ engineer for RocksDB.
|
||||
Read CLAUDE.md in the repo root and claude_md/ files for project context.
|
||||
Answer thoroughly with exact file paths and line numbers.
|
||||
|
||||
---
|
||||
|
||||
PROMPT_EOF
|
||||
|
||||
cat >> /tmp/prompt.txt << PROMPT_EOF
|
||||
## Pull Request Context
|
||||
- **Title:** ${PR_TITLE}
|
||||
- **Author:** ${PR_AUTHOR}
|
||||
- **Changes:** ${DIFF_STATS}
|
||||
|
||||
## PR Description
|
||||
PROMPT_EOF
|
||||
printf '%s\n' "${PR_BODY}" >> /tmp/prompt.txt
|
||||
printf '\n## Question\n' >> /tmp/prompt.txt
|
||||
printf '%s\n' "${USER_QUERY}" >> /tmp/prompt.txt
|
||||
printf '\n---\n## PR Diff (for reference)\n' >> /tmp/prompt.txt
|
||||
cat /tmp/pr.diff >> /tmp/prompt.txt
|
||||
else
|
||||
# Review prompt
|
||||
cat > /tmp/prompt.txt << 'PROMPT_EOF'
|
||||
Read claude_md/code_review.md for the full review methodology, then
|
||||
review the pull request described below. Follow all perspectives and
|
||||
the output format defined in that document.
|
||||
PROMPT_EOF
|
||||
|
||||
cat >> /tmp/prompt.txt << PROMPT_EOF
|
||||
|
||||
## Pull Request Information
|
||||
- **Title:** ${PR_TITLE}
|
||||
- **Author:** ${PR_AUTHOR}
|
||||
- **Changes:** ${DIFF_STATS}
|
||||
PROMPT_EOF
|
||||
|
||||
if [ "${IS_TRUNCATED}" = "true" ]; then
|
||||
echo "- **Note:** Diff truncated due to size." >> /tmp/prompt.txt
|
||||
fi
|
||||
|
||||
printf '\n## PR Description\n' >> /tmp/prompt.txt
|
||||
printf '%s\n' "${PR_BODY}" >> /tmp/prompt.txt
|
||||
|
||||
if [ -n "${ADDITIONAL_CONTEXT}" ]; then
|
||||
printf '\n## Additional Instructions from Reviewer\n' >> /tmp/prompt.txt
|
||||
printf '%s\n' "${ADDITIONAL_CONTEXT}" >> /tmp/prompt.txt
|
||||
fi
|
||||
|
||||
printf '\n---\n\n## Diff to Review\n\n' >> /tmp/prompt.txt
|
||||
cat /tmp/pr.diff >> /tmp/prompt.txt
|
||||
fi
|
||||
|
||||
- name: Run Claude
|
||||
id: claude
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
with:
|
||||
prompt_file: /tmp/prompt.txt
|
||||
model: ${{ github.event_name == 'workflow_dispatch' && inputs.model || 'claude-opus-4-6' }}
|
||||
max_turns: "30"
|
||||
allowed_tools: "View,GlobTool,GrepTool"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
|
||||
- name: Build review comment
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }}
|
||||
CONCLUSION: ${{ steps.claude.outputs.conclusion }}
|
||||
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
|
||||
REVIEWER: ${{ github.event_name == 'workflow_dispatch' && github.actor || github.event.comment.user.login }}
|
||||
COMMAND_TYPE: ${{ steps.command.outputs.type }}
|
||||
with:
|
||||
script: |
|
||||
const parse = require('./.github/scripts/parse-claude-review.js');
|
||||
const isReview = process.env.COMMAND_TYPE === 'review';
|
||||
const body = parse({
|
||||
executionFile: process.env.EXECUTION_FILE,
|
||||
conclusion: process.env.CONCLUSION,
|
||||
meta: {
|
||||
trigger: 'manual',
|
||||
headSha: process.env.HEAD_SHA,
|
||||
reviewer: process.env.REVIEWER,
|
||||
isQuery: !isReview,
|
||||
}
|
||||
});
|
||||
const fs = require('fs');
|
||||
fs.writeFileSync('claude-review-comment.md', body);
|
||||
|
||||
- name: Save PR number
|
||||
run: echo "${{ steps.pr_info.outputs.pr_number }}" > pr_number.txt
|
||||
|
||||
- name: Save trigger metadata
|
||||
env:
|
||||
REVIEWER: ${{ github.event_name == 'workflow_dispatch' && github.actor || github.event.comment.user.login }}
|
||||
COMMENT_ID: ${{ github.event.comment.id }}
|
||||
run: |
|
||||
echo "manual" > trigger_type.txt
|
||||
echo "${REVIEWER}" > reviewer.txt
|
||||
echo "${COMMENT_ID}" > comment_id.txt
|
||||
|
||||
- name: Upload review artifact
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: claude-review-result
|
||||
path: |
|
||||
claude-review-comment.md
|
||||
pr_number.txt
|
||||
trigger_type.txt
|
||||
reviewer.txt
|
||||
comment_id.txt
|
||||
if-no-files-found: ignore
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload execution log
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: claude-review-execution-log
|
||||
path: ${{ steps.claude.outputs.execution_file }}
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
@@ -54,11 +54,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/cache-getdeps-downloads"
|
||||
- uses: "./.github/actions/setup-folly"
|
||||
- uses: "./.github/actions/build-folly"
|
||||
- run: LIB_MODE=static USE_CLANG=1 USE_FOLLY=1 COMPILE_WITH_UBSAN=1 COMPILE_WITH_ASAN=1 make -j32 check
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-valgrind:
|
||||
build-linux-cmake-with-folly:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
@@ -68,7 +69,30 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- run: make V=1 -j32 valgrind_test
|
||||
- uses: "./.github/actions/cache-getdeps-downloads"
|
||||
- uses: "./.github/actions/setup-folly"
|
||||
- uses: "./.github/actions/cache-folly"
|
||||
id: cache-folly
|
||||
- uses: "./.github/actions/build-folly"
|
||||
with:
|
||||
cache-hit: ${{ steps.cache-folly.outputs.cache-hit }}
|
||||
- run: "(mkdir build && cd build && cmake -DUSE_FOLLY=1 -DWITH_GFLAGS=1 -DROCKSDB_BUILD_SHARED=0 .. && make VERBOSE=1 -j20 && ctest -j20)"
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-release-with-folly:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/cache-getdeps-downloads"
|
||||
- uses: "./.github/actions/setup-folly"
|
||||
- run: "DEBUG_LEVEL=0 make -j20 build_folly"
|
||||
- run: "USE_FOLLY=1 LIB_MODE=static DEBUG_LEVEL=0 V=1 make -j20 release"
|
||||
- run: "(mkdir build && cd build && cmake -DUSE_FOLLY=1 -DWITH_GFLAGS=1 -DROCKSDB_BUILD_SHARED=0 -DCMAKE_BUILD_TYPE=Release .. && make VERBOSE=1 -j20 && ctest -j20)"
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-windows-vs2022-avx2:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
@@ -89,6 +113,20 @@ jobs:
|
||||
- run: sudo apt-get update && sudo apt-get install -y build-essential libgflags-dev
|
||||
- run: make V=1 J=4 -j4 check
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-arm-crashtest:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
runs-on:
|
||||
labels: 4-core-ubuntu-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- run: sudo apt-get update && sudo apt-get install -y build-essential libgflags-dev libsnappy-dev zlib1g-dev libbz2-dev liblz4-dev libzstd-dev
|
||||
- run: sudo mount -o remount,size=16G /dev/shm
|
||||
- run: sudo dd bs=1048576 count=4096 if=/dev/zero of=/swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile
|
||||
- run: ulimit -S -n `ulimit -H -n` && make V=1 -j8 CRASH_TEST_EXT_ARGS='--duration=1800 --max_key=2500000' blackbox_crash_test_with_atomic_flush
|
||||
- run: rm -rf /dev/shm/rocksdb.*
|
||||
- run: ulimit -S -n `ulimit -H -n` && make V=1 -j8 CRASH_TEST_EXT_ARGS='--duration=1800 --max_key=2500000' blackbox_crash_test_with_multiops_wc_txn
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-examples:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
runs-on:
|
||||
@@ -117,7 +155,7 @@ jobs:
|
||||
- name: Build fuzzers
|
||||
run: cd fuzz && make sst_file_writer_fuzzer db_fuzzer db_map_fuzzer
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-cmake-with-folly-lite-no-test:
|
||||
build-linux-cmake-with-folly-lite:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
@@ -127,6 +165,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/cache-getdeps-downloads"
|
||||
- uses: "./.github/actions/setup-folly"
|
||||
- run: "(mkdir build && cd build && cmake -DUSE_FOLLY_LITE=1 -DWITH_GFLAGS=1 -DCMAKE_CXX_FLAGS=-DGLOG_USE_GLOG_EXPORT .. && make V=1 -j20)"
|
||||
- run: "(mkdir build && cd build && cmake -DUSE_FOLLY_LITE=1 -DWITH_GFLAGS=1 -DCMAKE_CXX_FLAGS=-DGLOG_USE_GLOG_EXPORT .. && make VERBOSE=1 -j20 && ctest -j20)"
|
||||
- uses: "./.github/actions/post-steps"
|
||||
+255
-102
@@ -1,7 +1,18 @@
|
||||
name: facebook/rocksdb/pr-jobs
|
||||
on: [push, pull_request]
|
||||
permissions: {}
|
||||
env:
|
||||
# Set to a job name to run only that job (on any repo), or leave empty for
|
||||
# normal behavior (all jobs on facebook repo only).
|
||||
ONLY_JOB: ''
|
||||
jobs:
|
||||
config:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
only_job: ${{ steps.set.outputs.only_job }}
|
||||
steps:
|
||||
- id: set
|
||||
run: echo "only_job=$ONLY_JOB" >> "$GITHUB_OUTPUT"
|
||||
# NOTE: multiple workflows would be recommended, but the current GHA UI in
|
||||
# PRs doesn't make it clear when there's an overall error with a workflow,
|
||||
# making it easy to overlook something broken. Grouping everything into one
|
||||
@@ -19,6 +30,10 @@ jobs:
|
||||
# increasing the risk of misconfiguration, especially on forks that might
|
||||
# want to run with this GHA setup.
|
||||
#
|
||||
# SELECTIVE JOB EXECUTION: Set the ONLY_JOB env var at the top of this file
|
||||
# to a job name (e.g. "build-linux-clang-tidy") to run only that job,
|
||||
# bypassing the repository owner check. Leave it empty for normal behavior.
|
||||
#
|
||||
# DEBUGGING WITH SSH: Temporarily add this as a job step, either before the
|
||||
# step of interest without the "if:" line or after the failing step with the
|
||||
# "if:" line. Then use ssh command printed in CI output.
|
||||
@@ -30,7 +45,8 @@ jobs:
|
||||
|
||||
# ======================== Fast Initial Checks ====================== #
|
||||
check-format-and-targets:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
if: needs.config.outputs.only_job == 'check-format-and-targets' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
@@ -44,6 +60,10 @@ jobs:
|
||||
run: python -m pip install --upgrade pip
|
||||
- name: Install argparse
|
||||
run: pip install argparse
|
||||
- name: Install clang-format
|
||||
run: |
|
||||
pip install https://files.pythonhosted.org/packages/fb/ac/3c04772acc0257f5730e83adb542b2603c1a62d1315010ab593a980af404/clang_format-21.1.2-py2.py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
|
||||
clang-format --version
|
||||
- name: Download clang-format-diff.py
|
||||
run: wget https://rocksdb-deps.s3.us-west-2.amazonaws.com/llvm/llvm-project/release/12.x/clang/tools/clang-format/clang-format-diff.py
|
||||
- name: Check format
|
||||
@@ -62,7 +82,8 @@ jobs:
|
||||
SANITY_CHECK=1 LONG_TEST=1 tools/check_format_compatible.sh
|
||||
# ========================= Linux With Tests ======================== #
|
||||
build-linux:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
if: needs.config.outputs.only_job == 'build-linux' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
@@ -71,10 +92,18 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: build-linux
|
||||
- run: make V=1 J=32 -j32 check
|
||||
- name: Print ccache stats
|
||||
run: ccache -s
|
||||
if: always()
|
||||
shell: bash
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-cmake-mingw:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
if: needs.config.outputs.only_job == 'build-linux-cmake-mingw' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on:
|
||||
labels: 4-core-ubuntu
|
||||
container:
|
||||
@@ -83,6 +112,9 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: cmake-mingw
|
||||
- run: update-alternatives --set x86_64-w64-mingw32-g++ /usr/bin/x86_64-w64-mingw32-g++-posix
|
||||
- name: Build cmake-mingw
|
||||
run: |-
|
||||
@@ -91,45 +123,41 @@ jobs:
|
||||
which java && java -version
|
||||
which javac && javac -version
|
||||
mkdir build && cd build && cmake -DJNI=1 -DWITH_GFLAGS=OFF .. -DCMAKE_C_COMPILER=x86_64-w64-mingw32-gcc -DCMAKE_CXX_COMPILER=x86_64-w64-mingw32-g++ -DCMAKE_SYSTEM_NAME=Windows && make -j4 rocksdb rocksdbjni
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-cmake-with-folly:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/setup-folly"
|
||||
- uses: "./.github/actions/cache-folly"
|
||||
id: cache-folly
|
||||
- uses: "./.github/actions/build-folly"
|
||||
with:
|
||||
cache-hit: ${{ steps.cache-folly.outputs.cache-hit }}
|
||||
- run: "(mkdir build && cd build && cmake -DUSE_FOLLY=1 -DWITH_GFLAGS=1 -DROCKSDB_BUILD_SHARED=0 .. && make V=1 -j20 && ctest -j20)"
|
||||
- name: Print ccache stats
|
||||
run: ccache -s
|
||||
if: always()
|
||||
shell: bash
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-make-with-folly:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
if: needs.config.outputs.only_job == 'build-linux-make-with-folly' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
labels: 32-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/cache-getdeps-downloads"
|
||||
- uses: "./.github/actions/setup-folly"
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: make-with-folly
|
||||
- uses: "./.github/actions/cache-folly"
|
||||
id: cache-folly
|
||||
- uses: "./.github/actions/build-folly"
|
||||
with:
|
||||
cache-hit: ${{ steps.cache-folly.outputs.cache-hit }}
|
||||
- run: USE_FOLLY=1 LIB_MODE=static V=1 make -j32 check
|
||||
- run: USE_FOLLY=1 LIB_MODE=static V=1 make -j64 check
|
||||
- name: Print ccache stats
|
||||
run: ccache -s
|
||||
if: always()
|
||||
shell: bash
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-make-with-folly-lite-no-test:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
if: needs.config.outputs.only_job == 'build-linux-make-with-folly-lite-no-test' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
@@ -138,29 +166,47 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/cache-getdeps-downloads"
|
||||
- uses: "./.github/actions/setup-folly"
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: make-folly-lite
|
||||
- run: USE_FOLLY_LITE=1 EXTRA_CXXFLAGS=-DGLOG_USE_GLOG_EXPORT V=1 make -j32 all
|
||||
- name: Print ccache stats
|
||||
run: ccache -s
|
||||
if: always()
|
||||
shell: bash
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-cmake-with-folly-coroutines:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
if: needs.config.outputs.only_job == 'build-linux-cmake-with-folly-coroutines' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
labels: 32-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/cache-getdeps-downloads"
|
||||
- uses: "./.github/actions/setup-folly"
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: cmake-folly-coroutines
|
||||
- uses: "./.github/actions/cache-folly"
|
||||
id: cache-folly
|
||||
- uses: "./.github/actions/build-folly"
|
||||
with:
|
||||
cache-hit: ${{ steps.cache-folly.outputs.cache-hit }}
|
||||
- run: "(mkdir build && cd build && cmake -DUSE_COROUTINES=1 -DWITH_GFLAGS=1 -DROCKSDB_BUILD_SHARED=0 .. && make V=1 -j20 && ctest -j20)"
|
||||
- run: "(mkdir build && cd build && cmake -DUSE_COROUTINES=1 -DWITH_GFLAGS=1 -DROCKSDB_BUILD_SHARED=0 -DPORTABLE=ON .. && make VERBOSE=1 -j64 && ctest -j64)"
|
||||
- name: Print ccache stats
|
||||
run: ccache -s
|
||||
if: always()
|
||||
shell: bash
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-cmake-with-benchmark:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
build-linux-cmake-with-benchmark-no-thread-status:
|
||||
if: needs.config.outputs.only_job == 'build-linux-cmake-with-benchmark-no-thread-status' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
@@ -169,10 +215,18 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- run: mkdir build && cd build && cmake -DWITH_GFLAGS=1 -DWITH_BENCHMARK=1 .. && make V=1 -j20 && ctest -j20
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: cmake-benchmark
|
||||
- run: mkdir build && cd build && cmake -DWITH_GFLAGS=1 -DWITH_BENCHMARK=1 -DPORTABLE=ON -DCMAKE_CXX_FLAGS=-DNROCKSDB_THREAD_STATUS .. && make VERBOSE=1 -j20 && ctest -j20
|
||||
- name: Print ccache stats
|
||||
run: ccache -s
|
||||
if: always()
|
||||
shell: bash
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-encrypted_env-no_compression:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
if: needs.config.outputs.only_job == 'build-linux-encrypted_env-no_compression' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
@@ -181,12 +235,20 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: encrypted-env-no-compression
|
||||
- run: ENCRYPTED_ENV=1 ROCKSDB_DISABLE_SNAPPY=1 ROCKSDB_DISABLE_ZLIB=1 ROCKSDB_DISABLE_BZIP=1 ROCKSDB_DISABLE_LZ4=1 ROCKSDB_DISABLE_ZSTD=1 make V=1 J=32 -j32 check
|
||||
- run: "./sst_dump --help | grep -E -q 'Supported built-in compression types: kNoCompression$' # Verify no compiled in compression\n"
|
||||
- name: Print ccache stats
|
||||
run: ccache -s
|
||||
if: always()
|
||||
shell: bash
|
||||
- uses: "./.github/actions/post-steps"
|
||||
# ======================== Linux No Test Runs ======================= #
|
||||
build-linux-release:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
if: needs.config.outputs.only_job == 'build-linux-release' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
@@ -194,6 +256,9 @@ jobs:
|
||||
options: --shm-size=16gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: release
|
||||
- run: make V=1 -j32 LIB_MODE=shared release
|
||||
- run: ls librocksdb.so
|
||||
- run: "./trace_analyzer --version" # A tool dependent on gflags that can run in release build
|
||||
@@ -210,9 +275,14 @@ jobs:
|
||||
- run: USE_RTTI=1 make V=1 -j32 release
|
||||
- run: ls librocksdb.a
|
||||
- run: if ./trace_analyzer --version; then false; else true; fi
|
||||
- name: Print ccache stats
|
||||
run: ccache -s
|
||||
if: always()
|
||||
shell: bash
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-clang-13-no_test_run:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
if: needs.config.outputs.only_job == 'build-linux-clang-13-no_test_run' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on:
|
||||
labels: 8-core-ubuntu
|
||||
container:
|
||||
@@ -221,14 +291,22 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: clang-13
|
||||
# FIXME: get back to "all microbench" targets
|
||||
- run: CC=clang-13 CXX=clang++-13 USE_CLANG=1 EXTRA_CXXFLAGS=-stdlib=libc++ EXTRA_LDFLAGS=-stdlib=libc++ make -j32 shared_lib
|
||||
- run: make clean
|
||||
# FIXME: get back to "release" target
|
||||
- run: CC=clang-13 CXX=clang++-13 USE_CLANG=1 EXTRA_CXXFLAGS=-stdlib=libc++ EXTRA_LDFLAGS=-stdlib=libc++ DEBUG_LEVEL=0 make -j32 shared_lib
|
||||
- name: Print ccache stats
|
||||
run: ccache -s
|
||||
if: always()
|
||||
shell: bash
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-clang-18-no_test_run:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
if: needs.config.outputs.only_job == 'build-linux-clang-18-no_test_run' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
@@ -237,12 +315,20 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: clang-18
|
||||
- run: CC=clang-18 CXX=clang++-18 USE_CLANG=1 make -j32 all microbench
|
||||
- run: make clean
|
||||
- run: CC=clang-18 CXX=clang++-18 USE_CLANG=1 DEBUG_LEVEL=0 make -j32 release
|
||||
- name: Print ccache stats
|
||||
run: ccache -s
|
||||
if: always()
|
||||
shell: bash
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-gcc-14-no_test_run:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
if: needs.config.outputs.only_job == 'build-linux-gcc-14-no_test_run' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
@@ -251,31 +337,21 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: gcc-14
|
||||
- run: CC=gcc-14 CXX=g++-14 V=1 make -j32 all microbench
|
||||
- name: Print ccache stats
|
||||
run: ccache -s
|
||||
if: always()
|
||||
shell: bash
|
||||
- uses: "./.github/actions/post-steps"
|
||||
|
||||
# ======================== Linux Other Checks ======================= #
|
||||
build-linux-clang18-clang-analyze:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
|
||||
options: --shm-size=16gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- run: CC=clang-18 CXX=clang++-18 ROCKSDB_DISABLE_ALIGNED_NEW=1 CLANG_ANALYZER="/usr/bin/clang++-18" CLANG_SCAN_BUILD=scan-build-18 USE_CLANG=1 make V=1 -j32 analyze
|
||||
- uses: "./.github/actions/post-steps"
|
||||
- name: compress test report
|
||||
run: tar -cvzf scan_build_report.tar.gz scan_build_report
|
||||
if: failure()
|
||||
- uses: actions/upload-artifact@v4.0.0
|
||||
with:
|
||||
name: scan-build-report
|
||||
path: scan_build_report.tar.gz
|
||||
|
||||
build-linux-unity-and-headers:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
if: needs.config.outputs.only_job == 'build-linux-unity-and-headers' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on:
|
||||
labels: 4-core-ubuntu
|
||||
container:
|
||||
@@ -284,12 +360,20 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- run: apt-get update -y && apt-get install -y libgflags-dev
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: unity-headers
|
||||
- name: Unity build
|
||||
run: make V=1 -j8 unity_test
|
||||
- run: make V=1 -j8 -k check-headers
|
||||
- name: Print ccache stats
|
||||
run: ccache -s
|
||||
if: always()
|
||||
shell: bash
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-mini-crashtest:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
if: needs.config.outputs.only_job == 'build-linux-mini-crashtest' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on:
|
||||
labels: 4-core-ubuntu
|
||||
container:
|
||||
@@ -298,11 +382,19 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: mini-crashtest
|
||||
- run: ulimit -S -n `ulimit -H -n` && make V=1 -j8 CRASH_TEST_EXT_ARGS='--duration=960 --max_key=2500000' blackbox_crash_test_with_atomic_flush
|
||||
- name: Print ccache stats
|
||||
run: ccache -s
|
||||
if: always()
|
||||
shell: bash
|
||||
- uses: "./.github/actions/post-steps"
|
||||
# ======================= Linux with Sanitizers ===================== #
|
||||
build-linux-clang18-asan-ubsan:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
if: needs.config.outputs.only_job == 'build-linux-clang18-asan-ubsan' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on:
|
||||
labels: 32-core-ubuntu
|
||||
container:
|
||||
@@ -311,10 +403,18 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: clang18-asan-ubsan
|
||||
- run: COMPILE_WITH_ASAN=1 COMPILE_WITH_UBSAN=1 CC=clang-18 CXX=clang++-18 ROCKSDB_DISABLE_ALIGNED_NEW=1 USE_CLANG=1 make V=1 -j40 check
|
||||
- name: Print ccache stats
|
||||
run: ccache -s
|
||||
if: always()
|
||||
shell: bash
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-clang18-mini-tsan:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
if: needs.config.outputs.only_job == 'build-linux-clang18-mini-tsan' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on:
|
||||
labels: 32-core-ubuntu
|
||||
container:
|
||||
@@ -323,10 +423,18 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: clang18-tsan
|
||||
- run: COMPILE_WITH_TSAN=1 CC=clang-18 CXX=clang++-18 ROCKSDB_DISABLE_ALIGNED_NEW=1 USE_CLANG=1 make V=1 -j32 check
|
||||
- name: Print ccache stats
|
||||
run: ccache -s
|
||||
if: always()
|
||||
shell: bash
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-static_lib-alt_namespace-status_checked:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
if: needs.config.outputs.only_job == 'build-linux-static_lib-alt_namespace-status_checked' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
@@ -335,11 +443,19 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: static-alt-namespace
|
||||
- run: ASSERT_STATUS_CHECKED=1 TEST_UINT128_COMPAT=1 ROCKSDB_MODIFY_NPHASH=1 LIB_MODE=static OPT="-DROCKSDB_USE_STD_SEMAPHORES -DROCKSDB_NAMESPACE=alternative_rocksdb_ns" make V=1 -j24 check
|
||||
- name: Print ccache stats
|
||||
run: ccache -s
|
||||
if: always()
|
||||
shell: bash
|
||||
- uses: "./.github/actions/post-steps"
|
||||
# ========================= MacOS build only ======================== #
|
||||
build-macos:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
if: needs.config.outputs.only_job == 'build-macos' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on: macos-15-xlarge
|
||||
env:
|
||||
ROCKSDB_DISABLE_JEMALLOC: 1
|
||||
@@ -348,15 +464,23 @@ jobs:
|
||||
- uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
with:
|
||||
xcode-version: 16.4.0
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: macos
|
||||
- uses: "./.github/actions/increase-max-open-files-on-macos"
|
||||
- uses: "./.github/actions/install-gflags-on-macos"
|
||||
- uses: "./.github/actions/pre-steps-macos"
|
||||
- name: Build
|
||||
run: ulimit -S -n `ulimit -H -n` && make V=1 J=16 -j8 all
|
||||
- name: Print ccache stats
|
||||
run: ccache -s
|
||||
if: always()
|
||||
shell: bash
|
||||
- uses: "./.github/actions/post-steps"
|
||||
# ========================= MacOS with Tests ======================== #
|
||||
build-macos-cmake:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
if: needs.config.outputs.only_job == 'build-macos-cmake' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on: macos-15-xlarge
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -366,13 +490,16 @@ jobs:
|
||||
- uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
with:
|
||||
xcode-version: 16.4.0
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: macos-cmake
|
||||
- uses: "./.github/actions/increase-max-open-files-on-macos"
|
||||
- uses: "./.github/actions/install-gflags-on-macos"
|
||||
- uses: "./.github/actions/pre-steps-macos"
|
||||
- name: cmake generate project file
|
||||
run: ulimit -S -n `ulimit -H -n` && mkdir build && cd build && cmake -DWITH_GFLAGS=1 ..
|
||||
- name: Build tests
|
||||
run: cd build && make V=1 -j8
|
||||
run: cd build && make VERBOSE=1 -j8
|
||||
- name: Run shard 0 out of 4 test shards
|
||||
run: ulimit -S -n `ulimit -H -n` && cd build && ctest -j8 -I 0,,4
|
||||
if: ${{ matrix.run_sharded_tests == 0 }}
|
||||
@@ -385,11 +512,16 @@ jobs:
|
||||
- name: Run shard 3 out of 4 test shards
|
||||
run: ulimit -S -n `ulimit -H -n` && cd build && ctest -j8 -I 3,,4
|
||||
if: ${{ matrix.run_sharded_tests == 3 }}
|
||||
- name: Print ccache stats
|
||||
run: ccache -s
|
||||
if: always()
|
||||
shell: bash
|
||||
- uses: "./.github/actions/post-steps"
|
||||
# ======================== Windows with Tests ======================= #
|
||||
# NOTE: some windows jobs are in "nightly" to save resources
|
||||
build-windows-vs2022:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
if: needs.config.outputs.only_job == 'build-windows-vs2022' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on: windows-8-core
|
||||
env:
|
||||
CMAKE_GENERATOR: Visual Studio 17 2022
|
||||
@@ -399,69 +531,58 @@ jobs:
|
||||
- uses: "./.github/actions/windows-build-steps"
|
||||
# ============================ Java Jobs ============================ #
|
||||
build-linux-java:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
if: needs.config.outputs.only_job == 'build-linux-java' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on:
|
||||
labels: 4-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
steps:
|
||||
# The docker image is intentionally based on an OS that has an older GLIBC version.
|
||||
# That GLIBC is incompatibile with GitHub's actions/checkout. Thus we implement a manual checkout step.
|
||||
# NOTE: replaced evolvedbinary/rocksjava:centos7_x64-be with ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
# until a more appropriate docker image with C++20 support is made.
|
||||
- name: Checkout
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
chown `whoami` . || true
|
||||
git clone --no-checkout https://oath2:$GH_TOKEN@github.com/${{ github.repository }}.git .
|
||||
git -c protocol.version=2 fetch --update-head-ok --no-tags --prune --no-recurse-submodules --depth=1 origin +${{ github.sha }}:${{ github.ref }}
|
||||
git checkout --progress --force ${{ github.ref }}
|
||||
git log -1 --format='%H'
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: java
|
||||
- name: Set Java Environment
|
||||
run: |-
|
||||
echo "JAVA_HOME=${JAVA_HOME}"
|
||||
which java && java -version
|
||||
which javac && javac -version
|
||||
- name: Test RocksDBJava
|
||||
# NOTE: replaced scl enable devtoolset-7 'make V=1 J=8 -j8 jtest'
|
||||
run: make V=1 J=8 -j8 jtest
|
||||
# post-steps skipped because of compatibility issues with docker image
|
||||
- name: Print ccache stats
|
||||
run: ccache -s
|
||||
if: always()
|
||||
shell: bash
|
||||
build-linux-java-static:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
if: needs.config.outputs.only_job == 'build-linux-java-static' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on:
|
||||
labels: 4-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
steps:
|
||||
# The docker image is intentionally based on an OS that has an older GLIBC version.
|
||||
# That GLIBC is incompatibile with GitHub's actions/checkout. Thus we implement a manual checkout step.
|
||||
# NOTE: replaced evolvedbinary/rocksjava:centos7_x64-be with ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
# until a more appropriate docker image with C++20 support is made.
|
||||
- name: Checkout
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
chown `whoami` . || true
|
||||
git clone --no-checkout https://oath2:$GH_TOKEN@github.com/${{ github.repository }}.git .
|
||||
git -c protocol.version=2 fetch --update-head-ok --no-tags --prune --no-recurse-submodules --depth=1 origin +${{ github.sha }}:${{ github.ref }}
|
||||
git checkout --progress --force ${{ github.ref }}
|
||||
git log -1 --format='%H'
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: java-static
|
||||
- name: Set Java Environment
|
||||
run: |-
|
||||
echo "JAVA_HOME=${JAVA_HOME}"
|
||||
which java && java -version
|
||||
which javac && javac -version
|
||||
- name: Build RocksDBJava Static Library
|
||||
# NOTE: replaced scl enable devtoolset-7 'make V=1 J=8 -j8 rocksdbjavastatic'
|
||||
run: make V=1 J=8 -j8 rocksdbjavastatic
|
||||
# post-steps skipped because of compatibility issues with docker image
|
||||
- name: Print ccache stats
|
||||
run: ccache -s
|
||||
if: always()
|
||||
shell: bash
|
||||
build-macos-java:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
if: needs.config.outputs.only_job == 'build-macos-java' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on: macos-15-xlarge
|
||||
env:
|
||||
JAVA_HOME: "/Library/Java/JavaVirtualMachines/liberica-jdk-8.jdk/Contents/Home"
|
||||
@@ -471,6 +592,9 @@ jobs:
|
||||
- uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
with:
|
||||
xcode-version: 16.4.0
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: macos-java
|
||||
- uses: "./.github/actions/increase-max-open-files-on-macos"
|
||||
- uses: "./.github/actions/install-gflags-on-macos"
|
||||
- uses: "./.github/actions/install-jdk8-on-macos"
|
||||
@@ -482,9 +606,14 @@ jobs:
|
||||
which javac && javac -version
|
||||
- name: Test RocksDBJava
|
||||
run: make V=1 J=16 -j16 jtest
|
||||
- name: Print ccache stats
|
||||
run: ccache -s
|
||||
if: always()
|
||||
shell: bash
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-macos-java-static:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
if: needs.config.outputs.only_job == 'build-macos-java-static' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on: macos-15-xlarge
|
||||
env:
|
||||
JAVA_HOME: "/Library/Java/JavaVirtualMachines/liberica-jdk-8.jdk/Contents/Home"
|
||||
@@ -493,6 +622,9 @@ jobs:
|
||||
- uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
with:
|
||||
xcode-version: 16.4.0
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: macos-java-static
|
||||
- uses: "./.github/actions/increase-max-open-files-on-macos"
|
||||
- uses: "./.github/actions/install-gflags-on-macos"
|
||||
- uses: "./.github/actions/install-jdk8-on-macos"
|
||||
@@ -504,9 +636,14 @@ jobs:
|
||||
which javac && javac -version
|
||||
- name: Build RocksDBJava x86 and ARM Static Libraries
|
||||
run: make V=1 J=16 -j16 rocksdbjavastaticosx
|
||||
- name: Print ccache stats
|
||||
run: ccache -s
|
||||
if: always()
|
||||
shell: bash
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-macos-java-static-universal:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
if: needs.config.outputs.only_job == 'build-macos-java-static-universal' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on: macos-15-xlarge
|
||||
env:
|
||||
JAVA_HOME: "/Library/Java/JavaVirtualMachines/liberica-jdk-8.jdk/Contents/Home"
|
||||
@@ -515,6 +652,9 @@ jobs:
|
||||
- uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
with:
|
||||
xcode-version: 16.4.0
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: macos-java-static-universal
|
||||
- uses: "./.github/actions/increase-max-open-files-on-macos"
|
||||
- uses: "./.github/actions/install-gflags-on-macos"
|
||||
- uses: "./.github/actions/install-jdk8-on-macos"
|
||||
@@ -526,9 +666,14 @@ jobs:
|
||||
which javac && javac -version
|
||||
- name: Build RocksDBJava Universal Binary Static Library
|
||||
run: make V=1 J=16 -j16 rocksdbjavastaticosx_ub
|
||||
- name: Print ccache stats
|
||||
run: ccache -s
|
||||
if: always()
|
||||
shell: bash
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-java-pmd:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
if: needs.config.outputs.only_job == 'build-linux-java-pmd' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on:
|
||||
labels: 4-core-ubuntu
|
||||
container:
|
||||
@@ -554,12 +699,20 @@ jobs:
|
||||
name: maven-site
|
||||
path: "${{ github.workspace }}/java/target/site"
|
||||
build-linux-arm:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
if: needs.config.outputs.only_job == 'build-linux-arm' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on:
|
||||
labels: 4-core-ubuntu-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- run: sudo apt-get update && sudo apt-get install -y build-essential
|
||||
- run: sudo apt-get update && sudo apt-get install -y build-essential ccache
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: arm
|
||||
- run: ROCKSDBTESTS_PLATFORM_DEPENDENT=only make V=1 J=4 -j4 all_but_some_tests check_some
|
||||
- name: Print ccache stats
|
||||
run: ccache -s
|
||||
if: always()
|
||||
shell: bash
|
||||
- uses: "./.github/actions/post-steps"
|
||||
@@ -0,0 +1,20 @@
|
||||
name: facebook/rocksdb/weekly
|
||||
on:
|
||||
schedule:
|
||||
- cron: 0 9 * * 0
|
||||
workflow_dispatch:
|
||||
permissions: {}
|
||||
jobs:
|
||||
build-linux-valgrind:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
timeout-minutes: 840
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- run: make V=1 -j20 valgrind_test
|
||||
- uses: "./.github/actions/post-steps"
|
||||
@@ -1,12 +1,14 @@
|
||||
# This file @generated by:
|
||||
#$ python3 buckifier/buckify_rocksdb.py
|
||||
# --> DO NOT EDIT MANUALLY <--
|
||||
# This file is a Facebook-specific integration for buck builds, so can
|
||||
# only be validated by Facebook employees.
|
||||
# This file is a Meta-specific integration for buck builds, so can
|
||||
# only be validated by Meta employees.
|
||||
load("//rocks/buckifier:defs.bzl", "cpp_library_wrapper","rocks_cpp_library_wrapper","cpp_binary_wrapper","cpp_unittest_wrapper","fancy_bench_wrapper","add_c_test_wrapper")
|
||||
load("@fbcode_macros//build_defs:export_files.bzl", "export_file")
|
||||
|
||||
|
||||
oncall("rocksdb_point_of_contact")
|
||||
|
||||
cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"cache/cache.cc",
|
||||
"cache/cache_entry_roles.cc",
|
||||
@@ -104,6 +106,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"db/version_edit.cc",
|
||||
"db/version_edit_handler.cc",
|
||||
"db/version_set.cc",
|
||||
"db/version_util.cc",
|
||||
"db/wal_edit.cc",
|
||||
"db/wal_manager.cc",
|
||||
"db/wide/wide_column_serialization.cc",
|
||||
@@ -205,6 +208,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"table/block_based/hash_index_reader.cc",
|
||||
"table/block_based/index_builder.cc",
|
||||
"table/block_based/index_reader_common.cc",
|
||||
"table/block_based/multi_scan_index_iterator.cc",
|
||||
"table/block_based/parsed_full_filter_block.cc",
|
||||
"table/block_based/partitioned_filter_block.cc",
|
||||
"table/block_based/partitioned_index_iterator.cc",
|
||||
@@ -266,6 +270,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"util/dynamic_bloom.cc",
|
||||
"util/file_checksum_helper.cc",
|
||||
"util/hash.cc",
|
||||
"util/io_dispatcher_imp.cc",
|
||||
"util/murmurhash.cc",
|
||||
"util/random.cc",
|
||||
"util/rate_limiter.cc",
|
||||
@@ -359,6 +364,9 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"utilities/transactions/write_prepared_txn_db.cc",
|
||||
"utilities/transactions/write_unprepared_txn.cc",
|
||||
"utilities/transactions/write_unprepared_txn_db.cc",
|
||||
"utilities/trie_index/bitvector.cc",
|
||||
"utilities/trie_index/louds_trie.cc",
|
||||
"utilities/trie_index/trie_index_factory.cc",
|
||||
"utilities/ttl/db_ttl_impl.cc",
|
||||
"utilities/types_util.cc",
|
||||
"utilities/wal_filter.cc",
|
||||
@@ -4820,6 +4828,12 @@ cpp_unittest_wrapper(name="db_clip_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="db_compaction_abort_test",
|
||||
srcs=["db/db_compaction_abort_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="db_compaction_filter_test",
|
||||
srcs=["db/db_compaction_filter_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
@@ -4844,6 +4858,12 @@ cpp_unittest_wrapper(name="db_encryption_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="db_etc3_test",
|
||||
srcs=["db/db_etc3_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="db_flush_test",
|
||||
srcs=["db/db_flush_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
@@ -5122,6 +5142,12 @@ cpp_unittest_wrapper(name="faiss_ivf_index_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="fault_injection_fs_test",
|
||||
srcs=["utilities/fault_injection_fs_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="fault_injection_test",
|
||||
srcs=["db/fault_injection_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
@@ -5194,12 +5220,6 @@ cpp_unittest_wrapper(name="import_column_family_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="index_builder_test",
|
||||
srcs=["table/block_based/index_builder_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="inlineskiplist_test",
|
||||
srcs=["memtable/inlineskiplist_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
@@ -5212,6 +5232,12 @@ cpp_unittest_wrapper(name="interval_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="io_dispatcher_test",
|
||||
srcs=["util/io_dispatcher_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="io_posix_test",
|
||||
srcs=["env/io_posix_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
@@ -5608,6 +5634,18 @@ cpp_unittest_wrapper(name="transaction_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="trie_index_db_test",
|
||||
srcs=["utilities/trie_index/trie_index_db_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="trie_index_test",
|
||||
srcs=["utilities/trie_index/trie_index_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="ttl_test",
|
||||
srcs=["utilities/ttl/ttl_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
@@ -5710,6 +5748,12 @@ cpp_unittest_wrapper(name="write_controller_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="write_prepared_transaction_seqno_test",
|
||||
srcs=["utilities/transactions/write_prepared_transaction_seqno_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="write_prepared_transaction_test",
|
||||
srcs=["utilities/transactions/write_prepared_transaction_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
# RocksDB Code Generation and Review Guidance
|
||||
|
||||
This document provides guidance for generating and reviewing code in the RocksDB project, derived from analysis of code review feedback across hundreds of complex merged Pull Requests. Use this as a reference when writing code with AI assistants or conducting code reviews.
|
||||
|
||||
---
|
||||
|
||||
## General Best Practices
|
||||
|
||||
### Code Quality and Maintainability
|
||||
|
||||
**Clarity and Readability:** Write clear, self-documenting code. Use meaningful variable names, add comments for complex logic, and structure code to minimize cognitive load. Avoid clever tricks that sacrifice readability for marginal performance gains unless absolutely necessary.
|
||||
|
||||
**Consistent Style:** Follow existing code style conventions. RocksDB uses `.clang-format` for formatting, specific naming conventions, and structural patterns. Deviations from these patterns are frequently flagged in reviews.
|
||||
|
||||
**Error Handling:** Ensure robust error handling throughout the codebase. Use RocksDB's `Status` type consistently, propagate errors appropriately, and avoid silently ignoring failures. Reviewers pay close attention to edge cases and failure modes.
|
||||
|
||||
### Testing Philosophy
|
||||
|
||||
**Comprehensive Coverage:** Every change should include appropriate test coverage. This includes unit tests for isolated functionality, integration tests for component interactions, and stress tests for concurrency and performance validation. Reviewers will ask for additional tests if coverage is insufficient.
|
||||
|
||||
**Edge Cases and Failure Modes:** Tests should explicitly cover edge cases, boundary conditions, and potential failure scenarios. This is especially important for changes affecting core database operations, compaction, or recovery logic.
|
||||
|
||||
**Platform-Specific Testing:** RocksDB supports multiple platforms (Linux, Windows, macOS) and compilers (GCC, Clang, MSVC). Changes should be tested across relevant platforms, particularly when touching platform-specific code or using compiler-specific features.
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
**⚠️ PERFORMANCE IS CRITICAL:** RocksDB is a high-performance storage engine where every CPU cycle and memory access matters. When writing code, always evaluate from a performance perspective. This is not optional—performance-aware coding is a fundamental requirement for all contributions.
|
||||
|
||||
**Benchmarking and Profiling:** Performance claims should be backed by empirical evidence. Use RocksDB's benchmarking tools (e.g., `db_bench`) to validate improvements. Reviewers will request benchmark results for changes that could impact performance.
|
||||
|
||||
**Memory Allocation:** Minimize dynamic memory allocations, especially in hot paths. Prefer stack allocation over heap allocation. Reuse buffers when possible. Consider using arena allocators or memory pools for frequent small allocations. Every `new`, `malloc`, or container resize has a cost.
|
||||
|
||||
**Memory Copy:** Avoid unnecessary memory copies. Use move semantics, `std::string_view`, `Slice`, and pass-by-reference where appropriate. Be aware of implicit copies in STL containers and function returns. Prefer in-place operations over copy-and-modify patterns.
|
||||
|
||||
**CPU Cache Efficiency:** Design data structures and access patterns to be cache-friendly. Keep frequently accessed data together (data locality). Prefer sequential memory access over random access. Be mindful of cache line sizes (typically 64 bytes) and avoid false sharing in concurrent code. Consider struct packing and field ordering to improve cache utilization.
|
||||
|
||||
**Loop Optimization:** Look for opportunities to collapse nested loops, reduce loop overhead, and minimize branch mispredictions. Hoist invariant computations out of loops. Consider loop unrolling for tight inner loops. Batch operations when possible to amortize per-operation overhead.
|
||||
|
||||
**SIMD and Vectorization:** Leverage SIMD instructions (SSE, AVX) for data-parallel operations when appropriate. Structure data to enable auto-vectorization by the compiler. Consider explicit SIMD intrinsics for critical hot paths like checksum computation, encoding/decoding, and bulk data processing.
|
||||
|
||||
**Branch Prediction:** Minimize unpredictable branches in hot paths. Use `LIKELY`/`UNLIKELY` macros to hint branch prediction. Consider branchless alternatives for simple conditionals. Order switch cases and if-else chains by frequency.
|
||||
|
||||
**Memory and Resource Management:** Be mindful of memory allocations, especially in hot paths. Use RAII patterns, smart pointers, and RocksDB's memory management utilities appropriately.
|
||||
|
||||
**Hot Path Analysis:** When deciding how aggressively to optimize code, consider whether it's on a hot path:
|
||||
- **Hot path** (executed thousands+ times, e.g., data access, iteration, compaction loops): Performance is paramount. Apply all optimization techniques—loop collapsing, SIMD, cache optimization, pre-allocation, etc. The cost of each operation is multiplied by execution frequency.
|
||||
- **Cold path** (executed rarely, e.g., DB open, configuration parsing, error handling): Maintainability and clarity are more important. Prefer readable code over micro-optimizations. Complex optimizations here add maintenance burden with negligible performance benefit.
|
||||
- **Warm path** (moderate frequency): Balance both concerns. Use profiling data to guide optimization decisions.
|
||||
|
||||
**Avoid Premature Optimization:** While performance is critical, focus on correctness first, then optimize based on profiling data. However, be performance-aware from the start—choosing the right algorithm and data structure upfront is not premature optimization. Use the hot path analysis above to decide how much optimization effort is warranted.
|
||||
|
||||
### API Design and Compatibility
|
||||
|
||||
**Backwards Compatibility:** RocksDB maintains strong backwards compatibility guarantees. Breaking changes are rare and require extensive justification. When deprecating features, follow the project's deprecation policy (typically spanning multiple releases).
|
||||
|
||||
**API Consistency:** New APIs should be consistent with existing patterns. Use similar naming conventions, parameter ordering, and return types. Reviewers will suggest changes to improve consistency with the broader codebase.
|
||||
|
||||
**Documentation:** Public APIs must be thoroughly documented. Include usage examples, parameter descriptions, and notes on thread safety, performance characteristics, and compatibility considerations.
|
||||
|
||||
---
|
||||
|
||||
## Component-Specific Guidance
|
||||
|
||||
### Database Core (`db`)
|
||||
|
||||
The database core handles write-ahead logging (WAL), memtables, compaction, and recovery. This component receives the most scrutiny in code reviews.
|
||||
|
||||
**Concurrency and Thread Safety:** Database operations are highly concurrent. Reviewers carefully examine locking strategies, atomic operations, and memory ordering. Document synchronization assumptions clearly. Use appropriate memory ordering semantics (`acquire`/`release` vs. `seq_cst`).
|
||||
|
||||
**Compaction Logic:** Changes to compaction are complex and high-risk. Ensure that compaction logic respects configured parameters, handles edge cases (empty databases, single-file compactions), and maintains correctness under concurrent operations.
|
||||
|
||||
**Error Propagation:** Database operations can fail in many ways (I/O errors, corruption, resource exhaustion). Ensure that errors are properly propagated, logged, and handled. Avoid assertions in production code paths.
|
||||
|
||||
**Testing:** Database core changes require extensive testing, including unit tests, integration tests, and stress tests. Test with various configurations, compaction styles, and concurrent workloads.
|
||||
|
||||
### Public Headers (`include`)
|
||||
|
||||
Public headers define RocksDB's API surface. Changes here have the highest compatibility impact.
|
||||
|
||||
**API Design:** New APIs should be intuitive, consistent with existing patterns, and well-documented. Consider how the API will be used in practice and avoid adding unnecessary complexity.
|
||||
|
||||
**Backwards Compatibility:** Breaking changes to public APIs require extensive justification and a deprecation plan. Maintain ABI compatibility for bug fixes and patch releases.
|
||||
|
||||
**Documentation:** Every public API must be thoroughly documented with usage examples, parameter descriptions, and notes on thread safety and performance characteristics.
|
||||
|
||||
**Deprecation:** When deprecating APIs, follow the project's policy. Mark deprecated APIs clearly, provide migration guidance, and maintain support for at least one major release.
|
||||
|
||||
### Internal Utilities (`util`)
|
||||
|
||||
Internal utilities provide common functionality used throughout the codebase.
|
||||
|
||||
**Code Reuse:** Utilities should be general-purpose and reusable. Avoid duplicating functionality that already exists elsewhere in the codebase.
|
||||
|
||||
**Error Handling:** Utility functions should handle errors robustly and propagate them appropriately. Consider edge cases like overflow, underflow, and invalid inputs.
|
||||
|
||||
**Testing:** Utility functions should have comprehensive test coverage, including edge cases and failure modes. Consider adding death tests for assertions.
|
||||
|
||||
**Performance:** Utilities are often used in hot paths. Ensure that implementations are efficient and avoid unnecessary allocations or copies.
|
||||
|
||||
### Table Management (`table`)
|
||||
|
||||
Table management handles SST file format, block-based tables, and table readers/writers.
|
||||
|
||||
**Block Format and Checksums:** Changes to block format require extreme care. Ensure that checksums are computed and verified correctly. Test with various compression algorithms and block sizes.
|
||||
|
||||
**Iterator Correctness:** Table iterators are used throughout the codebase. Ensure that iterator semantics (Seek, Next, Prev) are correct, especially at boundaries and with deletions.
|
||||
|
||||
**Caching and Prefetching:** Table readers interact with the block cache and prefetching logic. Ensure that cache keys are unique and that prefetching respects configured limits.
|
||||
|
||||
**Performance:** Table operations are performance-critical. Benchmark changes that could impact read or write performance.
|
||||
|
||||
### Utilities (`utilities`)
|
||||
|
||||
Utilities include optional features like transactions, backup engine, and checkpoint.
|
||||
|
||||
**Feature Isolation:** Utilities should be self-contained and not introduce unnecessary dependencies on core database internals.
|
||||
|
||||
**Deprecation and Cleanup:** Legacy features are being phased out. When removing deprecated code, ensure that migration paths are documented and that users have sufficient warning.
|
||||
|
||||
**Cross-Platform Compatibility:** Utilities often interact with OS-specific APIs. Ensure that code works on all supported platforms.
|
||||
|
||||
### Options and Configuration (`options`)
|
||||
|
||||
Options define RocksDB's configuration system.
|
||||
|
||||
**Type Safety:** Use appropriate types for options (e.g., `uint32_t` for flags, scoped enums for enumerated values).
|
||||
|
||||
**Deprecation Policy:** When deprecating options, follow the project's policy. Document the deprecation, provide migration guidance, and maintain support for at least one major release.
|
||||
|
||||
**Dynamic Configuration:** Some options can be changed dynamically. Ensure that dynamic changes are thread-safe and take effect correctly.
|
||||
|
||||
**Validation:** Validate option values and provide clear error messages for invalid configurations.
|
||||
|
||||
### Cache (`cache`)
|
||||
|
||||
Cache management is critical for RocksDB's performance.
|
||||
|
||||
**Concurrency:** Cache operations are highly concurrent. Ensure that implementations are thread-safe and use appropriate synchronization primitives.
|
||||
|
||||
**Performance:** Cache operations are in the hot path. Optimize for low latency and high throughput. Benchmark changes carefully.
|
||||
|
||||
**Memory Management:** Cache implementations must manage memory carefully to avoid leaks and excessive allocations.
|
||||
|
||||
**Eviction Policies:** Changes to eviction policies should be well-tested and benchmarked to ensure they improve overall performance.
|
||||
|
||||
---
|
||||
|
||||
## Code Review Checklist
|
||||
|
||||
When reviewing RocksDB code (or preparing code for review), use this checklist:
|
||||
|
||||
### Correctness
|
||||
- [ ] Does the change preserve database semantics (e.g., snapshot isolation, key ordering)?
|
||||
- [ ] Are all error cases handled appropriately?
|
||||
- [ ] Is the change thread-safe? Are synchronization primitives used correctly?
|
||||
- [ ] Are there any potential data races or deadlocks?
|
||||
|
||||
### Testing
|
||||
- [ ] Does the change include appropriate test coverage?
|
||||
- [ ] Are edge cases and failure modes tested?
|
||||
- [ ] Have the tests been run on all supported platforms?
|
||||
- [ ] Are stress tests passing?
|
||||
|
||||
### Performance
|
||||
- [ ] Are there benchmark results for performance-sensitive changes?
|
||||
- [ ] Does the change avoid unnecessary allocations or copies?
|
||||
- [ ] Are hot paths optimized appropriately?
|
||||
|
||||
### API and Compatibility
|
||||
- [ ] Is the change backwards compatible?
|
||||
- [ ] Are new APIs consistent with existing patterns?
|
||||
- [ ] Is the public API documented?
|
||||
- [ ] Are deprecated features handled according to policy?
|
||||
|
||||
### Code Quality
|
||||
- [ ] Does the code follow RocksDB's style conventions?
|
||||
- [ ] Is the code clear and maintainable?
|
||||
- [ ] Are comments and documentation sufficient?
|
||||
- [ ] Are there any code smells or anti-patterns?
|
||||
|
||||
---
|
||||
|
||||
## Common Review Feedback Patterns
|
||||
|
||||
The following patterns emerged as frequent sources of review feedback:
|
||||
|
||||
1. **Test Coverage:** Reviewers frequently request additional tests for edge cases, platform-specific behavior, and failure modes. Complex changes require comprehensive test coverage including unit tests, integration tests, and stress tests.
|
||||
|
||||
2. **Error Handling:** Ensure proper error propagation using RocksDB's `Status` type. Avoid silent failures and provide clear error messages that include context about what failed and why.
|
||||
|
||||
3. **API Design:** New APIs should be consistent with existing patterns. Use descriptive names that follow established conventions. Avoid breaking changes without strong justification and a clear deprecation plan.
|
||||
|
||||
4. **Documentation:** Public APIs must be documented with usage examples and notes on thread safety, performance characteristics, and compatibility considerations. Complex internal logic should also be well-commented.
|
||||
|
||||
5. **Performance:** Performance-sensitive changes require benchmark results to validate improvements. Use `db_bench` and other profiling tools to measure impact. Avoid premature optimization that adds complexity without measurable benefit.
|
||||
|
||||
6. **Concurrency:** Thread safety is critical in RocksDB. Document synchronization assumptions clearly. Use appropriate memory ordering semantics. Consider potential race conditions and deadlocks.
|
||||
|
||||
7. **Code Style:** Follow existing conventions for naming, formatting, and structure. Use `.clang-format` for consistent formatting. Prefer scoped enums (`enum class`) over unscoped enums.
|
||||
|
||||
8. **Backwards Compatibility:** RocksDB maintains strong compatibility guarantees. Breaking changes require extensive justification. When deprecating features, provide migration guidance and maintain support across multiple releases.
|
||||
|
||||
9. **Refactoring:** Reviewers appreciate refactoring that improves code readability and maintainability. Look for opportunities to deduplicate code and simplify complex logic.
|
||||
|
||||
10. **Platform Compatibility:** Ensure changes work correctly on all supported platforms (Linux, Windows, macOS) and with all supported compilers (GCC, Clang, MSVC).
|
||||
|
||||
---
|
||||
|
||||
## Important tips
|
||||
|
||||
### Build system
|
||||
* There are 3 build system. Make, CMake, BUCK(meta internal).
|
||||
* When a new .cc file is added, update Makefile, CMakeLists.txt, src.mk, BUCK.
|
||||
* Don't manually edit BUCK file, after updating src.mk, run
|
||||
/usr/local/bin/python3 buckifier/buckify_rocksdb.py to update it
|
||||
* Use make to build and run the test. CMake and BUCK are not used locally.
|
||||
* Use `make dbg` command to build all of the unit test in debug mode.
|
||||
* For -j in make command, use the number of CPU cores to decide it.
|
||||
|
||||
### 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
|
||||
unit tests. This should be done every time unit test is updated.
|
||||
* Don't use sleep to wait for certain events to happen. This will cause test to
|
||||
be flaky. Instead, use sync point to synchronize thread progress.
|
||||
* Cap unit test execution with 60 seconds timeout.
|
||||
* When there are multiple unit tests need to be executed, try to use
|
||||
gtest_parallel.py if available. E.g.
|
||||
python3 ${GTEST_PARALLEL}/gtest_parallel.py ./table_test
|
||||
|
||||
### Unit test dedup guidelines
|
||||
* Extract helper functions for repeated patterns such as object
|
||||
construction, round-trip (encode → decode → verify), and common
|
||||
assertion sequences.
|
||||
* Use table-driven tests (struct array + loop) when multiple test cases
|
||||
share the same logic but differ only in input/expected data.
|
||||
* Prefer randomized tests over exhaustive parameter permutations. Use
|
||||
`Random` from `util/random.h` (not `std::mt19937`). Use a time-based
|
||||
seed with `SCOPED_TRACE("seed=" + std::to_string(seed))` so failures
|
||||
are reproducible.
|
||||
* Keep deterministic edge-case tests separate from randomized tests
|
||||
(error paths, boundary conditions, format verification).
|
||||
* Methods only used in tests should be private with `friend class` +
|
||||
`TEST_F` fixture wrappers. In wrappers, always fully qualify the
|
||||
target method to avoid infinite recursion.
|
||||
|
||||
### Adding new public API
|
||||
Refer to claude_md/add_public_api.md
|
||||
|
||||
### Adding new option
|
||||
Refer to claude_md/add_option.md
|
||||
|
||||
### Removing deprecated option
|
||||
Refer to claude_md/remove_option.md
|
||||
|
||||
### Metrics
|
||||
* When adding a new feature, evaluate whether there is opportunity to add
|
||||
metrics. Try to avoid causing performance regression on hot path when adding
|
||||
metrics.
|
||||
|
||||
### Stress test
|
||||
* When adding a new feature, make sure stress test covers the new option.
|
||||
|
||||
### DB bench update
|
||||
* When adding a performance related feature, support it in db_bench
|
||||
|
||||
### Adding release note
|
||||
* Release note should be kept short at high level for external user consumption.
|
||||
|
||||
### Blog posts (docs/_posts)
|
||||
* Blog post authors must be defined in `docs/_data/authors.yml` to be displayed
|
||||
|
||||
### Final verification of the change
|
||||
* Execute make clean to clean all of the changes.
|
||||
* Execute make check to build all of the changes and execute all of the tests.
|
||||
Note that executing all of the tests could take multiple minutes.
|
||||
|
||||
### Monitoring make check progress
|
||||
* Use `make check-progress` to get machine-parseable JSON progress while
|
||||
`make check` is running. This is useful for Claude Code to monitor long
|
||||
builds without timeout issues.
|
||||
* Run `make check` in background, then poll progress:
|
||||
```bash
|
||||
make check &
|
||||
# Poll periodically:
|
||||
make check-progress
|
||||
```
|
||||
* The output shows current phase and progress:
|
||||
```json
|
||||
{"status":"running","phase":"compiling","completed":300,"total":919,...}
|
||||
{"status":"running","phase":"testing","completed":1500,"total":29962,"failed":0,"percent":5,...}
|
||||
{"status":"completed","phase":"testing","completed":29962,"total":29962,"failed":0,"percent":100,...}
|
||||
```
|
||||
* Phases: `compiling` -> `linking` -> `generating` -> `testing` -> `completed`
|
||||
* Key fields: `status`, `phase`, `completed`, `total`, `failed`, `percent`
|
||||
* When tests fail, `failed_tests` array shows details (up to 10 failures):
|
||||
```json
|
||||
{"status":"running",...,"failed":3,"failed_tests":[
|
||||
{"test":"cache_test-CacheTest.Usage","exit_code":1,"signal":0,"output":"...test log..."},
|
||||
{"test":"env_test-EnvTest.Open","exit_code":0,"signal":11,"output":"...Segmentation fault..."}
|
||||
]}
|
||||
```
|
||||
* `exit_code`: non-zero means test assertion failed
|
||||
* `signal`: non-zero means test was killed (e.g., 9=SIGKILL, 6=SIGABRT, 11=SIGSEGV)
|
||||
* `output`: last 50 lines of test log including error messages and stack traces
|
||||
|
||||
### Executing benchmark using db_bench
|
||||
* Since the goal is to measure performance, we need to build a release binary
|
||||
using `make clean && DEBUG_LEVEL=0 make db_bench`. If there is an engine
|
||||
crash due to bug, we need to switch back to debug build. Make sure to run
|
||||
`make clean` before running `make dbg`.
|
||||
|
||||
### Formatting code
|
||||
* After making change, use `make format-auto` to auto-apply formatting without
|
||||
interactive prompts (Claude Code friendly).
|
||||
+38
-2
@@ -80,6 +80,7 @@ if(NOT CMAKE_BUILD_TYPE)
|
||||
set(CMAKE_BUILD_TYPE "${default_build_type}" CACHE STRING
|
||||
"Default BUILD_TYPE is ${default_build_type}" FORCE)
|
||||
endif()
|
||||
message(STATUS "CMAKE_BUILD_TYPE is set to ${CMAKE_BUILD_TYPE}")
|
||||
|
||||
find_program(CCACHE_FOUND ccache)
|
||||
if(CCACHE_FOUND)
|
||||
@@ -132,7 +133,9 @@ else()
|
||||
option(WITH_GFLAGS "build with GFlags" ON)
|
||||
endif()
|
||||
set(GFLAGS_LIB)
|
||||
if(WITH_GFLAGS)
|
||||
# Skip all gflags detection and setup when USE_FOLLY or USE_COROUTINES is enabled
|
||||
# since Folly provides its own gflags (USE_COROUTINES automatically sets USE_FOLLY)
|
||||
if(WITH_GFLAGS AND NOT USE_FOLLY AND NOT USE_COROUTINES)
|
||||
# Config with namespace available since gflags 2.2.2
|
||||
option(GFLAGS_USE_TARGET_NAMESPACE "Use gflags import target with namespace." ON)
|
||||
find_package(gflags CONFIG)
|
||||
@@ -151,6 +154,9 @@ else()
|
||||
include_directories(${GFLAGS_INCLUDE_DIR})
|
||||
list(APPEND THIRDPARTY_LIBS ${GFLAGS_LIB})
|
||||
add_definitions(-DGFLAGS=1)
|
||||
elseif(WITH_GFLAGS AND (USE_FOLLY OR USE_COROUTINES))
|
||||
# Still set the DGFLAGS=1 define when using Folly since Folly provides gflags
|
||||
add_definitions(-DGFLAGS=1)
|
||||
endif()
|
||||
|
||||
if(WITH_SNAPPY)
|
||||
@@ -213,6 +219,10 @@ if(MSVC)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zi /nologo /EHsc /GS /Gd /GR /GF /fp:precise /Zc:wchar_t /Zc:forScope /errorReport:queue")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /FC /d2Zi+ /W4 /wd4127 /wd4996 /wd4100 /wd4324")
|
||||
endif()
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /DNDEBUG")
|
||||
message(STATUS "Setting /DNDEBUG as CMAKE_BUILD_TYPE is set to ${CMAKE_BUILD_TYPE}")
|
||||
endif()
|
||||
else()
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -W -Wextra -Wall -pthread")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wsign-compare -Wshadow -Wno-unused-parameter -Wno-unused-variable -Woverloaded-virtual -Wnon-virtual-dtor -Wno-missing-field-initializers -Wno-strict-aliasing -Wno-invalid-offsetof")
|
||||
@@ -642,6 +652,12 @@ if(USE_FOLLY)
|
||||
${FOLLY_INST_PATH}/lib/cmake/folly/folly-targets.cmake)
|
||||
|
||||
include(${FOLLY_INST_PATH}/lib/cmake/folly/folly-config.cmake)
|
||||
|
||||
# Fix gflags library name for debug builds
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-rpath=${GFLAGS_INST_PATH}/lib")
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${GFLAGS_INST_PATH}/lib/libgflags_debug.so.2.2")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_compile_definitions(USE_FOLLY FOLLY_NO_CONFIG HAVE_CXX11_ATOMIC)
|
||||
@@ -750,6 +766,7 @@ set(SOURCES
|
||||
db/version_edit.cc
|
||||
db/version_edit_handler.cc
|
||||
db/version_set.cc
|
||||
db/version_util.cc
|
||||
db/wal_edit.cc
|
||||
db/wal_manager.cc
|
||||
db/wide/wide_column_serialization.cc
|
||||
@@ -826,6 +843,7 @@ set(SOURCES
|
||||
table/block_based/block_based_table_builder.cc
|
||||
table/block_based/block_based_table_factory.cc
|
||||
table/block_based/block_based_table_iterator.cc
|
||||
table/block_based/multi_scan_index_iterator.cc
|
||||
table/block_based/block_based_table_reader.cc
|
||||
table/block_based/block_builder.cc
|
||||
table/block_based/block_cache.cc
|
||||
@@ -902,6 +920,7 @@ set(SOURCES
|
||||
util/data_structure.cc
|
||||
util/dynamic_bloom.cc
|
||||
util/hash.cc
|
||||
util/io_dispatcher_imp.cc
|
||||
util/murmurhash.cc
|
||||
util/random.cc
|
||||
util/rate_limiter.cc
|
||||
@@ -982,6 +1001,9 @@ set(SOURCES
|
||||
utilities/transactions/write_prepared_txn_db.cc
|
||||
utilities/transactions/write_unprepared_txn.cc
|
||||
utilities/transactions/write_unprepared_txn_db.cc
|
||||
utilities/trie_index/bitvector.cc
|
||||
utilities/trie_index/louds_trie.cc
|
||||
utilities/trie_index/trie_index_factory.cc
|
||||
utilities/types_util.cc
|
||||
utilities/ttl/db_ttl_impl.cc
|
||||
utilities/wal_filter.cc
|
||||
@@ -1082,12 +1104,21 @@ if(USE_FOLLY_LITE)
|
||||
third-party/folly/folly/synchronization/DistributedMutex.cpp
|
||||
third-party/folly/folly/synchronization/ParkingLot.cpp)
|
||||
include_directories(${PROJECT_SOURCE_DIR}/third-party/folly)
|
||||
# Add boost to the include path
|
||||
exec_program(python3 ${PROJECT_SOURCE_DIR}/third-party/folly ARGS
|
||||
build/fbcode_builder/getdeps.py show-source-dir boost OUTPUT_VARIABLE
|
||||
BOOST_SOURCE_PATH)
|
||||
exec_program(ls ARGS -d ${BOOST_SOURCE_PATH}/boost* OUTPUT_VARIABLE
|
||||
BOOST_INCLUDE_DIR)
|
||||
include_directories(${BOOST_INCLUDE_DIR})
|
||||
# Add fmt to the include path
|
||||
exec_program(python3 ${PROJECT_SOURCE_DIR}/third-party/folly ARGS
|
||||
build/fbcode_builder/getdeps.py show-source-dir fmt OUTPUT_VARIABLE
|
||||
FMT_SOURCE_PATH)
|
||||
exec_program(ls ARGS -d ${FMT_SOURCE_PATH}/fmt*/include OUTPUT_VARIABLE
|
||||
FMT_INCLUDE_DIR)
|
||||
include_directories(${FMT_INCLUDE_DIR})
|
||||
|
||||
add_definitions(-DUSE_FOLLY -DFOLLY_NO_CONFIG)
|
||||
list(APPEND THIRDPARTY_LIBS glog)
|
||||
endif()
|
||||
@@ -1356,9 +1387,11 @@ if(WITH_TESTS)
|
||||
db/db_bloom_filter_test.cc
|
||||
db/db_compaction_filter_test.cc
|
||||
db/db_compaction_test.cc
|
||||
db/db_compaction_abort_test.cc
|
||||
db/db_clip_test.cc
|
||||
db/db_dynamic_level_test.cc
|
||||
db/db_encryption_test.cc
|
||||
db/db_etc3_test.cc
|
||||
db/db_flush_test.cc
|
||||
db/db_inplace_update_test.cc
|
||||
db/db_io_failure_test.cc
|
||||
@@ -1452,7 +1485,6 @@ if(WITH_TESTS)
|
||||
table/block_based/block_based_table_reader_test.cc
|
||||
table/block_based/block_test.cc
|
||||
table/block_based/data_block_hash_index_test.cc
|
||||
table/block_based/index_builder_test.cc
|
||||
table/block_based/full_filter_block_test.cc
|
||||
table/block_based/partitioned_filter_block_test.cc
|
||||
table/cleanable_test.cc
|
||||
@@ -1504,6 +1536,7 @@ if(WITH_TESTS)
|
||||
utilities/cassandra/cassandra_serialize_test.cc
|
||||
utilities/checkpoint/checkpoint_test.cc
|
||||
utilities/env_timed_test.cc
|
||||
utilities/fault_injection_fs_test.cc
|
||||
utilities/memory/memory_test.cc
|
||||
utilities/merge_operators/string_append/stringappend_test.cc
|
||||
utilities/object_registry_test.cc
|
||||
@@ -1521,9 +1554,12 @@ if(WITH_TESTS)
|
||||
utilities/transactions/lock/point/point_lock_manager_stress_test.cc
|
||||
utilities/transactions/write_committed_transaction_ts_test.cc
|
||||
utilities/transactions/write_prepared_transaction_test.cc
|
||||
utilities/transactions/write_prepared_transaction_seqno_test.cc
|
||||
utilities/transactions/write_unprepared_transaction_test.cc
|
||||
utilities/transactions/lock/range/range_locking_test.cc
|
||||
utilities/transactions/timestamped_snapshot_test.cc
|
||||
utilities/trie_index/trie_index_db_test.cc
|
||||
utilities/trie_index/trie_index_test.cc
|
||||
utilities/ttl/ttl_test.cc
|
||||
utilities/types_util_test.cc
|
||||
utilities/util_merge_operators_test.cc
|
||||
|
||||
+119
@@ -1,6 +1,125 @@
|
||||
# Rocksdb Change Log
|
||||
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
|
||||
|
||||
## 11.1.2 (06/24/2026)
|
||||
### Bug Fixes
|
||||
* Fixed a bug where closing a read-only DB instance could delete live SST files created by a concurrent read-write DB sharing the same directory.
|
||||
|
||||
## 11.1.1 (04/10/2026)
|
||||
### Public API Changes
|
||||
* Changed experimental feature `ExternalTableReader::Get` and `ExternalTableReader::MultiGet` to use `PinnableSlice` instead of `std::string` for output values, enabling zero-copy pinning. This will break existing implementations.
|
||||
* Added `SstFileReader::Get` and `SstFileReader::MultiGet` overloads that accept `PinnableSlice`/`std::vector<PinnableSlice>*`, enabling zero-copy reads when the underlying `TableReader` supports pinning.
|
||||
* Added block_decompress_count to PerfContext
|
||||
|
||||
### Bug Fixes
|
||||
* Fix a memory accounting leak in IODispatcher where ReadIndex() moved block values out of ReadSet without releasing the associated prefetch memory, causing subsequent prefetches to be blocked when max_prefetch_memory_bytes was set.
|
||||
|
||||
## 11.1.0 (03/23/2026)
|
||||
### New Features
|
||||
* Add a new option `open_files_async`. The existing behavior is on DB open, we open all sst files and do basic validations. For very large DBs on remote filesystems with many ssts, this may take very long. This option performs these validations instead in the background. Open errors found by this async background task are surfaced as a new background error kAsyncFileOpen.
|
||||
* Added `BlockBasedTableOptions::kAuto` index block search type that automatically selects between binary and interpolation search on a per-index-block basis. During SST construction, each index block's key distribution uniformity is analyzed using the coefficient of variation of key gaps, and index blocks with uniform keys use interpolation search while others fall back to binary search. The uniformity threshold is configurable via `BlockBasedTableOptions::uniform_cv_threshold` (default: 0.2).
|
||||
* Introduced enforce_write_buffer_manager_during_recovery option to allow WriteBufferManager to be enforced during WAL recovery. (Default: true)
|
||||
* Add `memtable_batch_lookup_optimization` option to use batch lookup optimization for memtable MultiGet. For skip list memtables, after each key lookup, the search path is cached and reused for the next key, reducing per-key cost from O(log N) to O(log d) where d is the distance between consecutive keys. Benchmarks show ~7% improvement in memtable-resident MultiGet throughput.
|
||||
* Added `BlockBasedTableOptions::PrepopulateBlockCache::kFlushAndCompaction` to prepopulate the block cache during both flush and compaction. Compaction-warmed blocks are inserted at `BOTTOM` priority (vs `LOW` for flush) so they are evicted first under cache pressure. Recommended only for use cases where most or all of the database is expected to reside in cache (e.g., tiered or remote storage where the working set fits in cache).
|
||||
* Added new mutable DB option `verify_manifest_content_on_close` (default: false). When enabled, on DB close the MANIFEST file is read back and all records are validated (CRC checksums and logical content). If corruption is detected, a fresh MANIFEST is written from in-memory state.
|
||||
|
||||
### Behavior Changes
|
||||
* num_reads_sampled now factors in re-seeks and next/prev() on file iterators for files in L1+. next/prev() is discounted by 64x compared to seek due to being a much cheaper call.
|
||||
* Remote compaction workers now skip WAL recovery when opening the secondary DB instance, since only the LSM state from MANIFEST is needed for compaction. This reduces I/O and speeds up remote compaction startup.
|
||||
|
||||
### Bug Fixes
|
||||
* Fix a bug in round-robin compaction that missed selecting input files that are needed to guarantee data correctness and cause crashing in debug builds or silent data corruption in release builds for Get().
|
||||
|
||||
## 11.0.0 (02/23/2026)
|
||||
### New Features
|
||||
* Added support for storing wide-column entity column values in blob files. When `min_blob_size` is configured, large column values in wide-column entities will be stored in blob files, reducing SST file size and improving read performance.
|
||||
* Added `CompactionOptionsFIFO::max_data_files_size` to support FIFO compaction trimming based on combined SST and blob file sizes. Added `CompactionOptionsFIFO::use_kv_ratio_compaction` to enable a capacity-derived intra-L0 compaction strategy optimized for BlobDB workloads, producing uniform-sized compacted files for predictable FIFO trimming.
|
||||
* Include interpolation search as an alternative to binary search, which typically performs better when keys are uniformly distributed. This is exposed as a new table option `index_block_search_type`. The default is `binary_search`.
|
||||
|
||||
### Public API Changes
|
||||
* Added new virtual methods `AbortAllCompactions()` and `ResumeAllCompactions()` to the `DB` class. Added new `Status::SubCode::kCompactionAborted` to indicate a compaction was aborted. Added `Status::IsCompactionAborted()` helper method to check if a status represents an aborted compaction.
|
||||
* Drop support for reading (and writing) SST files using `BlockBasedTableOptions.format_version` < 2, which hasn't been the default format for about 10 years. An upgrade path is still possible with full compaction using a RocksDB version >= 4.6.0 and < 11.0.0 and then using the newer version.
|
||||
* Remove deprecated raw `DB*` variants of `DB::Open` and related functions. Some other minor public APIs were updated as a result
|
||||
* Remove deprecated `DB::MaxMemCompactionLevel()`
|
||||
* Remove useless option `CompressedSecondaryCacheOptions::compress_format_version`
|
||||
* Remove deprecated DB option `skip_checking_sst_file_sizes_on_db_open`. The option was deprecated in 10.5.0 and has been a no-op since then. File size validation is now always performed in parallel during DB open.
|
||||
* Remove deprecated `SliceTransform::InRange()` virtual method and the `in_range` callback parameter from `rocksdb_slicetransform_create()` in the C API. `InRange()` was never called by RocksDB and existed only for backward compatibility.
|
||||
* Remove deprecated, unused APIs and options: `ReadOptions::managed` and `ColumnFamilyOptions::snap_refresh_nanos`. Corresponding C and Java APIs are also removed.
|
||||
* Remove deprecated `SstFileWriter::Add()` method (use `Put()` instead) and the deprecated `skip_filters` parameter from `SstFileWriter` constructors (use `BlockBasedTableOptions::filter_policy` set to `nullptr` to skip filter generation instead).
|
||||
|
||||
### Behavior Changes
|
||||
* Change the default value of `CompactionOptionsUniversal::reduce_file_locking` from `false` to `true` to improve write stall and reduce read regression
|
||||
|
||||
### Bug Fixes
|
||||
* Fix longstanding failures that can arise from reading and/or compacting old DB dirs with range deletions (likely from version < 5.19.0) in many newer versions.
|
||||
* Fix a bug where WritePrepared/WriteUnprepared TransactionDB with two_write_queues=true could experience "sequence number going backwards" corruption during recovery from a background error, due to allocated-but-not-published sequence numbers not being synced before creating new WAL files.
|
||||
|
||||
### Performance Improvements
|
||||
* Add a new table option `separate_key_value_in_data_block`. When set to true keys and values will be stored separately in the data block, which can result in higher cpu cache hit rate and better compression. Works best with data blocks with sufficient restart intervals and large values. Previous versions of RocksDB will reject files written using this option.
|
||||
|
||||
## 10.11.0 (01/23/2026)
|
||||
### Public API Changes
|
||||
* New SetOptions API that allows setting options for multiple CFs, avoiding the need to reserialize OPTIONS file for each CF
|
||||
* Remove remaining pieces of Lua integration
|
||||
|
||||
### Behavior Changes
|
||||
* The new default for `BlockBasedTableOptions::format_version` is 7, which has been supported since RocksDB 10.4.0 and is required in order to use CompressionManagers supporting custom compression types.
|
||||
|
||||
### Bug Fixes
|
||||
* Fixed a small performance bug with `format_version=7` when decompressing formats other than Snappy and ZSTD.
|
||||
* Fixed an infinite compaction loop bug with User-Defined Timestamps (UDT) where bottommost files were repeatedly marked for compaction even though their timestamp could not be collapsed.
|
||||
* Bugfix for persisted UDT record sequence number zeroing logic.
|
||||
|
||||
## 10.10.0 (12/16/2025)
|
||||
### Bug Fixes
|
||||
* Fixed a bug in best-efforts recovery that causes use-after-free crashes when accessing SST files that were cached during the recovery.
|
||||
* Fix resumable compaction incorrectly allowing resumption from a truncated range deletion that is not well handled currently.
|
||||
* Fixed a bug in `PosixRandomFileAccess` IO uring submission queue ownership & management. Fix eliminates the false positive 'Bad cqe data' IO errors in `PosixRandomFileAccess::MultiRead` when interleaved with `PosixRandomFileAccess::ReadAsync` on the same thread.
|
||||
|
||||
## 10.9.0 (11/21/2025)
|
||||
### New Features
|
||||
* Added an auto-tuning feature for DB manifest file size that also (by default) improves the safety of existing configurations in case `max_manifest_file_size` is repeatedly exceeded. The new recommendation is to set `max_manifest_file_size` to something small like 1MB and tune `max_manifest_space_amp_pct` as needed to balance write amp and space amp in the manifest. Refer to comments on those options in `DBOptions` for details. Both options are (now) mutable.
|
||||
* Added a new API to support option migration for multiple column families
|
||||
* Added new option target_file_size_is_upper_bound that makes most compaction output SST files come close to the target file size without exceeding it, rather than commonly exceeding it by some fraction (current behavior). For now the new behavior is off by default, but we expect to enable it by default in the future.
|
||||
* Add a new option allow_trivial_move in CompactionOptions to allow CompactFiles to perform trivial move if possible. By default the flag of allow_trivial_move is false, so it preserve the original behavior.
|
||||
|
||||
### Public API Changes
|
||||
* To reduce risk of ODR violations or similar, `ROCKSDB_USING_THREAD_STATUS` has been removed from public headers and replaced with static `const bool ThreadStatus::kEnabled`. Some other uses of conditional compilation have been removed from public API headers to reduce risk of ODR violations or other issues.
|
||||
|
||||
### Behavior Changes
|
||||
* PosixWritableFile now repositions the seek pointer to the new end of file after a call to Truncate.
|
||||
* Updated standalone range deletion L0 file compaction behavior to avoid compacting with any newer L0 files (which is expensive and not useful).
|
||||
|
||||
### Bug Fixes
|
||||
* Fix a bug where compaction with range deletion can persist kTypeMaxValid in MANIFEST as file metadata. kTypeMaxValid is not supposed to be persisted and can change as new value types are introduced. This can cause a forward compatibility issue where older versions of RocksDB don't recognize kTypeMaxValid from newer versions. A new placeholder value type kTypeTruncatedRangeDeletionSentinel is also introduced to replace kTypeMaxValid when reading existing SST files' metadata from MANIFEST. This allows us to strengthen some checks to avoid using kTypeMaxValid in the future.
|
||||
* Fixed a bug where `DB::GetSortedWalFiles()` could hang when waiting for a purge operation that found nothing to do (potentially triggered by iterator release, flush, compaction, etc.).
|
||||
* Fixed a bug in MultiScan where `max_sequential_skip_in_iterations` could cause the iterator to seek backward to already-unpinned blocks when the same user key spans multiple data blocks, leading to assertion failures or seg fault.
|
||||
* Fixed a bug for `WAL_ttl_seconds > 0` use cases where the newest archived WAL files could be incorrectly deleted when the system clock moved backwards.
|
||||
|
||||
### Performance Improvements
|
||||
* Added optimization that allowed for the asynchronous prefetching of all data outlined in a multiscan iterator. This optimization was applied to the level iterator, which prefetches all data through each of the block-based iterators.
|
||||
|
||||
## 10.8.0 (10/21/2025)
|
||||
### New Features
|
||||
* Add kFSPrefetch to FSSupportedOps enum to allow file systems to indicate prefetch support capability, avoiding unnecessary prefetch system calls on file systems that don't support them.
|
||||
* Added experimental support `OpenAndCompactOptions::allow_resumption` for resumable compaction that persists progress during `OpenAndCompact()`, allowing interrupted compactions to resume from the last progress persitence. The default behavior is to not persist progress.
|
||||
|
||||
### Public API Changes
|
||||
* Allow specifying output temperature in CompactionOptions
|
||||
* Added `DB::FlushWAL(const FlushWALOptions&)` as an alternative to `DB::FlushWAL(bool sync)`, where `FlushWALOptions` includes a new `rate_limiter_priority` field (default `Env::IO_TOTAL`) that allows rate limiting and priority passing of manual WAL flush's IO operations.
|
||||
* The MultiScan API contract is updated. After a multi scan range got prepared with Prepare API call, the following seeks must seek the start of each prepared scan range in order. In addition, when limit is set, upper bound must be set to the same value of limit before each seek
|
||||
|
||||
### Behavior Changes
|
||||
* `kChangeTemperature` FIFO compaction will now honor `compaction_target_temp` to all levels regardless of `cf_options::last_level_temperature`
|
||||
* Allow UDIs with a non BytewiseComparator
|
||||
|
||||
### Bug Fixes
|
||||
* Fix incorrect MultiScan seek error status due to bugs in handling range limit falling between adjacent SST files key range.
|
||||
* Fix a bug in Page unpinning in MultiScan
|
||||
|
||||
### Performance Improvements
|
||||
* Fixed a performance regression in LZ4 compression that started in version 10.6.0
|
||||
|
||||
## 10.7.0 (09/19/2025)
|
||||
### New Features
|
||||
* Add the fail_if_no_udi_on_open flag in BlockBasedTableOption to control whether a missing user defined index block in a SST is a hard error or not.
|
||||
|
||||
@@ -296,6 +296,28 @@ $(info $(shell $(CC) --version))
|
||||
$(info $(shell $(CXX) --version))
|
||||
endif
|
||||
|
||||
# ccache support
|
||||
# Set USE_CCACHE=1 to enable ccache, or let it auto-detect
|
||||
ifndef USE_CCACHE
|
||||
CCACHE := $(shell which ccache 2>/dev/null)
|
||||
ifneq ($(CCACHE),)
|
||||
USE_CCACHE := 1
|
||||
else
|
||||
USE_CCACHE := 0
|
||||
endif
|
||||
endif
|
||||
|
||||
ifeq ($(USE_CCACHE), 1)
|
||||
CCACHE := $(shell which ccache 2>/dev/null)
|
||||
ifneq ($(CCACHE),)
|
||||
$(info Using ccache: $(CCACHE))
|
||||
CC := $(CCACHE) $(CC)
|
||||
CXX := $(CCACHE) $(CXX)
|
||||
else
|
||||
$(warning ccache requested but not found in PATH)
|
||||
endif
|
||||
endif
|
||||
|
||||
missing_make_config_paths := $(shell \
|
||||
grep "\./\S*\|/\S*" -o $(CURDIR)/make_config.mk | \
|
||||
while read path; \
|
||||
@@ -368,8 +390,6 @@ ifdef COMPILE_WITH_TSAN
|
||||
# Turn off -pg when enabling TSAN testing, because that induces
|
||||
# a link failure. TODO: find the root cause
|
||||
PROFILING_FLAGS =
|
||||
# LUA is not supported under TSAN
|
||||
LUA_PATH =
|
||||
# Limit keys for crash test under TSAN to avoid error:
|
||||
# "ThreadSanitizer: DenseSlabAllocator overflow. Dying."
|
||||
CRASH_TEST_EXT_ARGS += --max_key=1000000
|
||||
@@ -446,83 +466,7 @@ else
|
||||
PLATFORM_CXXFLAGS += -isystem $(GTEST_DIR)
|
||||
endif
|
||||
|
||||
# This provides a Makefile simulation of a Meta-internal folly integration.
|
||||
# It is not validated for general use.
|
||||
#
|
||||
# USE_FOLLY links the build targets with libfolly.a. The latter could be
|
||||
# built using 'make build_folly', or built externally and specified in
|
||||
# the CXXFLAGS and EXTRA_LDFLAGS env variables. The build_detect_platform
|
||||
# script tries to detect if an external folly dependency has been specified.
|
||||
# If not, it exports FOLLY_PATH to the path of the installed Folly and
|
||||
# dependency libraries.
|
||||
#
|
||||
# USE_FOLLY_LITE cherry picks source files from Folly to include in the
|
||||
# RocksDB library. Its faster and has fewer dependencies on 3rd party
|
||||
# libraries, but with limited functionality. For example, coroutine
|
||||
# functionality is not available.
|
||||
ifeq ($(USE_FOLLY),1)
|
||||
ifeq ($(USE_FOLLY_LITE),1)
|
||||
$(error Please specify only one of USE_FOLLY and USE_FOLLY_LITE)
|
||||
endif
|
||||
ifneq ($(strip $(FOLLY_PATH)),)
|
||||
BOOST_PATH = $(shell (ls -d $(FOLLY_PATH)/../boost*))
|
||||
DBL_CONV_PATH = $(shell (ls -d $(FOLLY_PATH)/../double-conversion*))
|
||||
GFLAGS_PATH = $(shell (ls -d $(FOLLY_PATH)/../gflags*))
|
||||
GLOG_PATH = $(shell (ls -d $(FOLLY_PATH)/../glog*))
|
||||
LIBEVENT_PATH = $(shell (ls -d $(FOLLY_PATH)/../libevent*))
|
||||
XZ_PATH = $(shell (ls -d $(FOLLY_PATH)/../xz*))
|
||||
LIBSODIUM_PATH = $(shell (ls -d $(FOLLY_PATH)/../libsodium*))
|
||||
FMT_PATH = $(shell (ls -d $(FOLLY_PATH)/../fmt*))
|
||||
|
||||
# For some reason, glog and fmt libraries are under either lib or lib64
|
||||
GLOG_LIB_PATH = $(shell (ls -d $(GLOG_PATH)/lib*))
|
||||
FMT_LIB_PATH = $(shell (ls -d $(FMT_PATH)/lib*))
|
||||
|
||||
# AIX: pre-defined system headers are surrounded by an extern "C" block
|
||||
ifeq ($(PLATFORM), OS_AIX)
|
||||
PLATFORM_CCFLAGS += -I$(BOOST_PATH)/include -I$(DBL_CONV_PATH)/include -I$(GLOG_PATH)/include -I$(LIBEVENT_PATH)/include -I$(XZ_PATH)/include -I$(LIBSODIUM_PATH)/include -I$(FOLLY_PATH)/include -I$(FMT_PATH)/include
|
||||
PLATFORM_CXXFLAGS += -I$(BOOST_PATH)/include -I$(DBL_CONV_PATH)/include -I$(GLOG_PATH)/include -I$(LIBEVENT_PATH)/include -I$(XZ_PATH)/include -I$(LIBSODIUM_PATH)/include -I$(FOLLY_PATH)/include -I$(FMT_PATH)/include
|
||||
else
|
||||
PLATFORM_CCFLAGS += -isystem $(BOOST_PATH)/include -isystem $(DBL_CONV_PATH)/include -isystem $(GLOG_PATH)/include -isystem $(LIBEVENT_PATH)/include -isystem $(XZ_PATH)/include -isystem $(LIBSODIUM_PATH)/include -isystem $(FOLLY_PATH)/include -isystem $(FMT_PATH)/include
|
||||
PLATFORM_CXXFLAGS += -isystem $(BOOST_PATH)/include -isystem $(DBL_CONV_PATH)/include -isystem $(GLOG_PATH)/include -isystem $(LIBEVENT_PATH)/include -isystem $(XZ_PATH)/include -isystem $(LIBSODIUM_PATH)/include -isystem $(FOLLY_PATH)/include -isystem $(FMT_PATH)/include
|
||||
endif
|
||||
|
||||
# Add -ldl at the end as gcc resolves a symbol in a library by searching only in libraries specified later
|
||||
# in the command line
|
||||
PLATFORM_LDFLAGS += $(FOLLY_PATH)/lib/libfolly.a $(BOOST_PATH)/lib/libboost_context.a $(BOOST_PATH)/lib/libboost_filesystem.a $(BOOST_PATH)/lib/libboost_atomic.a $(BOOST_PATH)/lib/libboost_program_options.a $(BOOST_PATH)/lib/libboost_regex.a $(BOOST_PATH)/lib/libboost_system.a $(BOOST_PATH)/lib/libboost_thread.a $(DBL_CONV_PATH)/lib/libdouble-conversion.a $(FMT_LIB_PATH)/libfmt.a $(GLOG_LIB_PATH)/libglog.so $(GFLAGS_PATH)/lib/libgflags.so.2.2 $(LIBEVENT_PATH)/lib/libevent-2.1.so -ldl
|
||||
PLATFORM_LDFLAGS += -Wl,-rpath=$(GFLAGS_PATH)/lib -Wl,-rpath=$(GLOG_LIB_PATH) -Wl,-rpath=$(LIBEVENT_PATH)/lib -Wl,-rpath=$(LIBSODIUM_PATH)/lib -Wl,-rpath=$(LIBEVENT_PATH)/lib
|
||||
endif
|
||||
PLATFORM_CCFLAGS += -DUSE_FOLLY -DFOLLY_NO_CONFIG
|
||||
PLATFORM_CXXFLAGS += -DUSE_FOLLY -DFOLLY_NO_CONFIG
|
||||
endif
|
||||
|
||||
ifeq ($(USE_FOLLY_LITE),1)
|
||||
# Path to the Folly source code and include files
|
||||
FOLLY_DIR = ./third-party/folly
|
||||
ifneq ($(strip $(BOOST_SOURCE_PATH)),)
|
||||
BOOST_INCLUDE = $(shell (ls -d $(BOOST_SOURCE_PATH)/boost*/))
|
||||
# AIX: pre-defined system headers are surrounded by an extern "C" block
|
||||
ifeq ($(PLATFORM), OS_AIX)
|
||||
PLATFORM_CCFLAGS += -I$(BOOST_INCLUDE)
|
||||
PLATFORM_CXXFLAGS += -I$(BOOST_INCLUDE)
|
||||
else
|
||||
PLATFORM_CCFLAGS += -isystem $(BOOST_INCLUDE)
|
||||
PLATFORM_CXXFLAGS += -isystem $(BOOST_INCLUDE)
|
||||
endif
|
||||
endif # BOOST_SOURCE_PATH
|
||||
# AIX: pre-defined system headers are surrounded by an extern "C" block
|
||||
ifeq ($(PLATFORM), OS_AIX)
|
||||
PLATFORM_CCFLAGS += -I$(FOLLY_DIR)
|
||||
PLATFORM_CXXFLAGS += -I$(FOLLY_DIR)
|
||||
else
|
||||
PLATFORM_CCFLAGS += -isystem $(FOLLY_DIR)
|
||||
PLATFORM_CXXFLAGS += -isystem $(FOLLY_DIR)
|
||||
endif
|
||||
PLATFORM_CCFLAGS += -DUSE_FOLLY -DFOLLY_NO_CONFIG
|
||||
PLATFORM_CXXFLAGS += -DUSE_FOLLY -DFOLLY_NO_CONFIG
|
||||
# TODO: fix linking with fbcode compiler config
|
||||
PLATFORM_LDFLAGS += -lglog
|
||||
endif
|
||||
include folly.mk
|
||||
|
||||
ifdef TEST_CACHE_LINE_SIZE
|
||||
PLATFORM_CCFLAGS += -DTEST_CACHE_LINE_SIZE=$(TEST_CACHE_LINE_SIZE)
|
||||
@@ -562,32 +506,6 @@ ifndef DISABLE_WARNING_AS_ERROR
|
||||
endif
|
||||
|
||||
|
||||
ifdef LUA_PATH
|
||||
|
||||
ifndef LUA_INCLUDE
|
||||
LUA_INCLUDE=$(LUA_PATH)/include
|
||||
endif
|
||||
|
||||
LUA_INCLUDE_FILE=$(LUA_INCLUDE)/lualib.h
|
||||
|
||||
ifeq ("$(wildcard $(LUA_INCLUDE_FILE))", "")
|
||||
# LUA_INCLUDE_FILE does not exist
|
||||
$(error Cannot find lualib.h under $(LUA_INCLUDE). Try to specify both LUA_PATH and LUA_INCLUDE manually)
|
||||
endif
|
||||
LUA_FLAGS = -I$(LUA_INCLUDE) -DLUA -DLUA_COMPAT_ALL
|
||||
CFLAGS += $(LUA_FLAGS)
|
||||
CXXFLAGS += $(LUA_FLAGS)
|
||||
|
||||
ifndef LUA_LIB
|
||||
LUA_LIB = $(LUA_PATH)/lib/liblua.a
|
||||
endif
|
||||
ifeq ("$(wildcard $(LUA_LIB))", "") # LUA_LIB does not exist
|
||||
$(error $(LUA_LIB) does not exist. Try to specify both LUA_PATH and LUA_LIB manually)
|
||||
endif
|
||||
EXEC_LDFLAGS += $(LUA_LIB)
|
||||
|
||||
endif
|
||||
|
||||
ifeq ($(NO_THREEWAY_CRC32C), 1)
|
||||
CXXFLAGS += -DNO_THREEWAY_CRC32C
|
||||
endif
|
||||
@@ -658,8 +576,8 @@ ifneq ($(filter check-headers, $(MAKECMDGOALS)),)
|
||||
# TODO: add/support JNI headers
|
||||
DEV_HEADER_DIRS := $(sort include/ $(dir $(ALL_SOURCES)))
|
||||
# Some headers like in port/ are platform-specific
|
||||
DEV_HEADERS_TO_CHECK := $(shell $(FIND) $(DEV_HEADER_DIRS) -type f -name '*.h' | grep -E -v 'port/|plugin/|lua/|range_tree/|secondary_index/')
|
||||
PUBLIC_HEADERS_TO_CHECK := $(shell $(FIND) include/ -type f -name '*.h' | grep -E -v 'lua/')
|
||||
DEV_HEADERS_TO_CHECK := $(shell $(FIND) $(DEV_HEADER_DIRS) -type f -name '*.h' | grep -E -v 'port/|plugin/|range_tree/|secondary_index/')
|
||||
PUBLIC_HEADERS_TO_CHECK := $(shell $(FIND) include/ -type f -name '*.h')
|
||||
else
|
||||
DEV_HEADERS_TO_CHECK :=
|
||||
PUBLIC_HEADERS_TO_CHECK :=
|
||||
@@ -683,6 +601,7 @@ am__v_CCH_1 =
|
||||
%.h.pub: %.h # .h.pub not actually created, so re-checked on each invocation
|
||||
$(AM_V_CCH) cd include/ && echo '#include "$(patsubst include/%,%,$<)"' | \
|
||||
$(CXX) -std=$(or $(ROCKSDB_CXX_STANDARD),c++20) -I. -DROCKSDB_NAMESPACE=42 -x c++ -c - -o /dev/null
|
||||
build_tools/check-public-header.sh $<
|
||||
|
||||
check-headers: $(HEADER_OK_FILES)
|
||||
|
||||
@@ -708,11 +627,22 @@ ROCKSDBTESTS_SUBSET ?= $(TESTS)
|
||||
# its various tests. Parallel can fill up your /dev/shm
|
||||
# db_bloom_filter_test - serial because excessive space usage by instances
|
||||
# of DBFilterConstructionReserveMemoryTestWithParam can fill up /dev/shm
|
||||
# perf_context_test - 1GB write_buffer_size default, parallel can fill /dev/shm
|
||||
# obsolete_files_test - ~1GB write_buffer_size/target_file_size_base
|
||||
# backup_engine_test - 1GB write_buffer_size in FailOverwritingBackups
|
||||
# prefetch_test - 1GB write_buffer_size in PrefetchWhenReseek parameterized test
|
||||
# db_io_failure_test - 256MB write_buffer_size in FlushSstRangeSyncError and
|
||||
# CompactionSstRangeSyncError
|
||||
NON_PARALLEL_TEST = \
|
||||
c_test \
|
||||
env_test \
|
||||
deletefile_test \
|
||||
db_bloom_filter_test \
|
||||
perf_context_test \
|
||||
obsolete_files_test \
|
||||
backup_engine_test \
|
||||
prefetch_test \
|
||||
db_io_failure_test \
|
||||
$(PLUGIN_TESTS) \
|
||||
|
||||
PARALLEL_TEST = $(filter-out $(NON_PARALLEL_TEST), $(TESTS))
|
||||
@@ -886,7 +816,7 @@ endif # PLATFORM_SHARED_EXT
|
||||
.PHONY: check clean coverage ldb_tests package dbg gen-pc build_size \
|
||||
release tags tags0 valgrind_check format static_lib shared_lib all \
|
||||
rocksdbjavastatic rocksdbjava install install-static install-shared \
|
||||
uninstall analyze tools tools_lib check-headers checkout_folly
|
||||
uninstall analyze tools tools_lib check-headers checkout_folly clang-tidy
|
||||
|
||||
all: $(LIBRARY) $(BENCHMARKS) tools tools_lib test_libs $(TESTS)
|
||||
|
||||
@@ -965,6 +895,9 @@ $(parallel_tests):
|
||||
"d=\$(TEST_TMPDIR)$$TEST_SCRIPT" \
|
||||
'mkdir -p $$d' \
|
||||
"TEST_TMPDIR=\$$d $(DRIVER) ./$$TEST_BINARY --gtest_filter=$$TEST_NAME" \
|
||||
'test_retcode=$$?' \
|
||||
'[ $$test_retcode -eq 0 ] && rm -rf $$d' \
|
||||
'exit $$test_retcode' \
|
||||
> $$TEST_SCRIPT; \
|
||||
chmod a=rx $$TEST_SCRIPT; \
|
||||
done
|
||||
@@ -1074,9 +1007,19 @@ watch-log:
|
||||
dump-log:
|
||||
bash -c '$(quoted_perl_command)' < LOG
|
||||
|
||||
# Machine-parseable progress output for automated monitoring (e.g., Claude Code)
|
||||
# Outputs JSON: {"status":"running","completed":45,"total":100,"failed":0,"percent":45,"eta_seconds":120}
|
||||
check-progress:
|
||||
@build_tools/check_progress.sh
|
||||
|
||||
# If J != 1 and GNU parallel is installed, run the tests in parallel,
|
||||
# via the check_0 rule above. Otherwise, run them sequentially.
|
||||
check: all
|
||||
$(AM_V_at)echo "Cleaning up stale test directories older than 3 hours..."; \
|
||||
test_tmpdir_parent=$$(dirname $(TEST_TMPDIR)); \
|
||||
find $$test_tmpdir_parent -maxdepth 1 -name 'rocksdb.*' -type d \
|
||||
-mmin +180 -exec rm -rf {} + 2>/dev/null; \
|
||||
true
|
||||
$(MAKE) gen_parallel_tests
|
||||
$(AM_V_GEN)if test "$(J)" != 1 \
|
||||
&& (build_tools/gnu_parallel --gnu --help 2>/dev/null) | \
|
||||
@@ -1285,6 +1228,10 @@ tags0:
|
||||
format:
|
||||
build_tools/format-diff.sh
|
||||
|
||||
# Non-interactive format (auto-apply without prompts, for CI/automation/Claude Code)
|
||||
format-auto:
|
||||
build_tools/format-diff.sh -y
|
||||
|
||||
check-format:
|
||||
build_tools/format-diff.sh -c
|
||||
|
||||
@@ -1294,6 +1241,15 @@ check-buck-targets:
|
||||
check-sources:
|
||||
build_tools/check-sources.sh
|
||||
|
||||
# Run clang-tidy on locally changed files, filtered to changed lines only.
|
||||
# Requires compile_commands.json (generate with cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON).
|
||||
# Override CLANG_TIDY_BINARY and CLANG_TIDY_JOBS as needed:
|
||||
# make clang-tidy CLANG_TIDY_BINARY=/usr/bin/clang-tidy CLANG_TIDY_JOBS=8
|
||||
CLANG_TIDY_BINARY ?= /opt/homebrew/opt/llvm/bin/clang-tidy
|
||||
CLANG_TIDY_JOBS ?= $(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)
|
||||
clang-tidy:
|
||||
python3 tools/run_clang_tidy.py --clang-tidy-binary $(CLANG_TIDY_BINARY) -j $(CLANG_TIDY_JOBS)
|
||||
|
||||
package:
|
||||
bash build_tools/make_package.sh $(SHARED_MAJOR).$(SHARED_MINOR)
|
||||
|
||||
@@ -1496,6 +1452,9 @@ db_test: $(OBJ_DIR)/db/db_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
db_test2: $(OBJ_DIR)/db/db_test2.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
db_etc3_test: $(OBJ_DIR)/db/db_etc3_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
compression_test: $(OBJ_DIR)/util/compression_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
@@ -1520,6 +1479,9 @@ db_compaction_filter_test: $(OBJ_DIR)/db/db_compaction_filter_test.o $(TEST_LIBR
|
||||
db_compaction_test: $(OBJ_DIR)/db/db_compaction_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
db_compaction_abort_test: $(OBJ_DIR)/db/db_compaction_abort_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
db_clip_test: $(OBJ_DIR)/db/db_clip_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
@@ -1646,6 +1608,12 @@ object_registry_test: $(OBJ_DIR)/utilities/object_registry_test.o $(TEST_LIBRARY
|
||||
ttl_test: $(OBJ_DIR)/utilities/ttl/ttl_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
trie_index_db_test: $(OBJ_DIR)/utilities/trie_index/trie_index_db_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
trie_index_test: $(OBJ_DIR)/utilities/trie_index/trie_index_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
types_util_test: $(OBJ_DIR)/utilities/types_util_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
@@ -1697,6 +1665,9 @@ io_posix_test: $(OBJ_DIR)/env/io_posix_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
fault_injection_test: $(OBJ_DIR)/db/fault_injection_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
fault_injection_fs_test: $(OBJ_DIR)/utilities/fault_injection_fs_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
rate_limiter_test: $(OBJ_DIR)/util/rate_limiter_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
@@ -1739,9 +1710,6 @@ block_test: $(OBJ_DIR)/table/block_based/block_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
data_block_hash_index_test: $(OBJ_DIR)/table/block_based/data_block_hash_index_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
index_builder_test: $(OBJ_DIR)/table/block_based/index_builder_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
inlineskiplist_test: $(OBJ_DIR)/memtable/inlineskiplist_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
@@ -1898,6 +1866,9 @@ write_committed_transaction_ts_test: $(OBJ_DIR)/utilities/transactions/write_com
|
||||
write_prepared_transaction_test: $(OBJ_DIR)/utilities/transactions/write_prepared_transaction_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
write_prepared_transaction_seqno_test: $(OBJ_DIR)/utilities/transactions/write_prepared_transaction_seqno_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
write_unprepared_transaction_test: $(OBJ_DIR)/utilities/transactions/write_unprepared_transaction_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
@@ -2003,6 +1974,9 @@ blob_source_test: $(OBJ_DIR)/db/blob/blob_source_test.o $(TEST_LIBRARY) $(LIBRAR
|
||||
blob_garbage_meter_test: $(OBJ_DIR)/db/blob/blob_garbage_meter_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
io_dispatcher_test: $(OBJ_DIR)/util/io_dispatcher_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
timer_test: $(OBJ_DIR)/util/timer_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
@@ -2495,42 +2469,6 @@ commit_prereq:
|
||||
false # J=$(J) build_tools/precommit_checker.py unit clang_unit release clang_release tsan asan ubsan lite unit_non_shm
|
||||
# $(MAKE) clean && $(MAKE) jclean && $(MAKE) rocksdbjava;
|
||||
|
||||
FOLLY_COMMIT_HASH = e95383b7c8b5b1e46cf47acf2f317d54f93c8268
|
||||
|
||||
# For public CI runs, checkout folly in a way that can build with RocksDB.
|
||||
# This is mostly intended as a test-only simulation of Meta-internal folly
|
||||
# integration.
|
||||
checkout_folly:
|
||||
if [ -e third-party/folly ]; then \
|
||||
cd third-party/folly && ${GIT_COMMAND} fetch origin; \
|
||||
else \
|
||||
cd third-party && ${GIT_COMMAND} clone https://github.com/facebook/folly.git; \
|
||||
fi
|
||||
@# Pin to a particular version for public CI, so that PR authors don't
|
||||
@# need to worry about folly breaking our integration. Update periodically
|
||||
cd third-party/folly && git reset --hard $(FOLLY_COMMIT_HASH)
|
||||
@# Apparently missing include
|
||||
perl -pi -e 's/(#include <atomic>)/$$1\n#include <cstring>/' third-party/folly/folly/lang/Exception.h
|
||||
@# Warning-as-error on memcpy
|
||||
perl -pi -e 's/memcpy.&ptr/memcpy((void*)&ptr/' third-party/folly/folly/lang/Exception.cpp
|
||||
@# const mismatch
|
||||
perl -pi -e 's/: environ/: (const char**)(environ)/' third-party/folly/folly/Subprocess.cpp
|
||||
@# NOTE: boost source will be needed for any build including `USE_FOLLY_LITE` builds as those depend on boost headers
|
||||
cd third-party/folly && $(PYTHON) build/fbcode_builder/getdeps.py fetch boost
|
||||
|
||||
CXX_M_FLAGS = $(filter -m%, $(CXXFLAGS))
|
||||
|
||||
build_folly:
|
||||
FOLLY_INST_PATH=`cd third-party/folly; $(PYTHON) build/fbcode_builder/getdeps.py show-inst-dir`; \
|
||||
if [ "$$FOLLY_INST_PATH" ]; then \
|
||||
rm -rf $${FOLLY_INST_PATH}/../../*; \
|
||||
else \
|
||||
echo "Please run checkout_folly first"; \
|
||||
false; \
|
||||
fi
|
||||
cd third-party/folly && \
|
||||
CXXFLAGS=" $(CXX_M_FLAGS) -DHAVE_CXX11_ATOMIC " $(PYTHON) build/fbcode_builder/getdeps.py build --no-tests
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Build size testing
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -2651,7 +2589,7 @@ list_all_tests:
|
||||
|
||||
# Remove the rules for which dependencies should not be generated and see if any are left.
|
||||
#If so, include the dependencies; if not, do not include the dependency files
|
||||
ROCKS_DEP_RULES=$(filter-out clean format check-format check-buck-targets check-headers check-sources jclean jtest package analyze tags rocksdbjavastatic% unity.% unity_test checkout_folly, $(MAKECMDGOALS))
|
||||
ROCKS_DEP_RULES=$(filter-out clean format check-format check-buck-targets check-headers check-sources clang-tidy jclean jtest package analyze tags rocksdbjavastatic% unity.% unity_test checkout_folly, $(MAKECMDGOALS))
|
||||
ifneq ("$(ROCKS_DEP_RULES)", "")
|
||||
-include $(DEPFILES)
|
||||
endif
|
||||
@@ -135,6 +135,9 @@ def generate_buck(repo_path, deps_map):
|
||||
|
||||
BUCK = TARGETSBuilder("%s/BUCK" % repo_path, extra_argv)
|
||||
|
||||
# Add oncall("rocksdb_point_of_contact") at the top
|
||||
BUCK.add_oncall("rocksdb_point_of_contact")
|
||||
|
||||
# rocksdb_lib
|
||||
BUCK.add_library(
|
||||
"rocksdb_lib",
|
||||
|
||||
@@ -45,6 +45,11 @@ class TARGETSBuilder:
|
||||
self.total_bin = 0
|
||||
self.total_test = 0
|
||||
self.tests_cfg = ""
|
||||
|
||||
def add_oncall(self, oncall):
|
||||
with open(self.path, "ab") as targets_file:
|
||||
targets_file.write(targets_cfg.oncall_template.format(name=oncall).encode("utf-8"))
|
||||
|
||||
|
||||
def add_library(
|
||||
self,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# This source code is licensed under both the GPLv2 (found in the COPYING file in the root directory)
|
||||
# and the Apache 2.0 License (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
rocksdb_target_header_template = """# This file \100generated by:
|
||||
#$ python3 buckifier/buckify_rocksdb.py{extra_argv}
|
||||
# --> DO NOT EDIT MANUALLY <--
|
||||
# This file is a Facebook-specific integration for buck builds, so can
|
||||
# only be validated by Facebook employees.
|
||||
# This file is a Meta-specific integration for buck builds, so can
|
||||
# only be validated by Meta employees.
|
||||
load("//rocks/buckifier:defs.bzl", "cpp_library_wrapper","rocks_cpp_library_wrapper","cpp_binary_wrapper","cpp_unittest_wrapper","fancy_bench_wrapper","add_c_test_wrapper")
|
||||
load("@fbcode_macros//build_defs:export_files.bzl", "export_file")
|
||||
|
||||
@@ -41,3 +43,8 @@ fancy_bench_wrapper(suite_name="{name}", binary_to_bench_to_metric_list_map={ben
|
||||
export_file_template = """
|
||||
export_file(name = "{name}")
|
||||
"""
|
||||
|
||||
|
||||
oncall_template = """
|
||||
oncall("{name}")
|
||||
"""
|
||||
@@ -311,7 +311,8 @@ EOF
|
||||
EOF
|
||||
then
|
||||
COMMON_FLAGS="$COMMON_FLAGS -DGFLAGS=1"
|
||||
PLATFORM_LDFLAGS="$PLATFORM_LDFLAGS -lgflags"
|
||||
# Hack: don't link extra gflags assuming it comes with folly
|
||||
[ "$USE_FOLLY" ] || PLATFORM_LDFLAGS="$PLATFORM_LDFLAGS -lgflags"
|
||||
# check if namespace is gflags
|
||||
elif $CXX $PLATFORM_CXXFLAGS -x c++ - -o test.o 2>/dev/null << EOF
|
||||
#include <gflags/gflags.h>
|
||||
@@ -320,7 +321,8 @@ EOF
|
||||
EOF
|
||||
then
|
||||
COMMON_FLAGS="$COMMON_FLAGS -DGFLAGS=1 -DGFLAGS_NAMESPACE=gflags"
|
||||
PLATFORM_LDFLAGS="$PLATFORM_LDFLAGS -lgflags"
|
||||
# Hack: don't link extra gflags assuming it comes with folly
|
||||
[ "$USE_FOLLY" ] || PLATFORM_LDFLAGS="$PLATFORM_LDFLAGS -lgflags"
|
||||
# check if namespace is google
|
||||
elif $CXX $PLATFORM_CXXFLAGS -x c++ - -o test.o 2>/dev/null << EOF
|
||||
#include <gflags/gflags.h>
|
||||
@@ -772,6 +774,7 @@ fi
|
||||
if [ "$USE_FOLLY_LITE" ]; then
|
||||
if [ "$FOLLY_DIR" ]; then
|
||||
BOOST_SOURCE_PATH=`cd $FOLLY_DIR && $PYTHON build/fbcode_builder/getdeps.py show-source-dir boost`
|
||||
FMT_SOURCE_PATH=`cd $FOLLY_DIR && $PYTHON build/fbcode_builder/getdeps.py show-source-dir fmt`
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -816,6 +819,7 @@ echo "FIND=$FIND" >> "$OUTPUT"
|
||||
echo "WATCH=$WATCH" >> "$OUTPUT"
|
||||
echo "FOLLY_PATH=$FOLLY_PATH" >> "$OUTPUT"
|
||||
echo "BOOST_SOURCE_PATH=$BOOST_SOURCE_PATH" >> "$OUTPUT"
|
||||
echo "FMT_SOURCE_PATH=$FMT_SOURCE_PATH" >> "$OUTPUT"
|
||||
|
||||
# This will enable some related identifiers for the preprocessor
|
||||
if test -n "$JEMALLOC"; then
|
||||
@@ -827,7 +831,6 @@ fi
|
||||
if test -n "$WITH_JEMALLOC_FLAG"; then
|
||||
echo "WITH_JEMALLOC_FLAG=$WITH_JEMALLOC_FLAG" >> "$OUTPUT"
|
||||
fi
|
||||
echo "LUA_PATH=$LUA_PATH" >> "$OUTPUT"
|
||||
if test -n "$USE_FOLLY"; then
|
||||
echo "USE_FOLLY=$USE_FOLLY" >> "$OUTPUT"
|
||||
fi
|
||||
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved.
|
||||
#
|
||||
# Check for some simple mistakes in public headers (on the command line)
|
||||
# that should prevent commit or push
|
||||
|
||||
BAD=""
|
||||
|
||||
# Look for potential for ODR violations caused by public headers depending on
|
||||
# build parameters that could vary between RocksDB build and application build.
|
||||
# * Cases like ROCKSDB_NAMESPACE, and ROCKSDB_ASSERT_STATUS_CHECKED are
|
||||
# intentional, hard to avoid. (We expect definitions to change and the user
|
||||
# should also.)
|
||||
# * Cases like _WIN32, OS_WIN, and __cplusplus are essentially ODR-safe.
|
||||
# * Cases like
|
||||
# #ifdef BLAH // ODR-SAFE
|
||||
# #undef BLAH
|
||||
# #endif
|
||||
# that should not cause ODR violations can be exempted with the ODR-SAFE
|
||||
# marker recognized here.
|
||||
|
||||
grep -nHE '^#if' -- "$@" | grep -vE 'ROCKSDB_NAMESPACE|ROCKSDB_ASSERT_STATUS_CHECKED|_WIN32|OS_WIN|ODR-SAFE|__cplusplus|ROCKSDB_DLL|ROCKSDB_LIBRARY_EXPORTS'
|
||||
if [ "$?" != "1" ]; then
|
||||
echo "^^^^^ #if in public API could cause an ODR violation."
|
||||
echo " Add // ODR-SAFE if verified safe."
|
||||
BAD=1
|
||||
fi
|
||||
|
||||
if [ "$BAD" ]; then
|
||||
exit 1
|
||||
fi
|
||||
Executable
+231
@@ -0,0 +1,231 @@
|
||||
#!/bin/bash
|
||||
# Output test progress in JSON format for machine parsing
|
||||
# Usage: build_tools/check_progress.sh
|
||||
|
||||
LOG_FILE="LOG"
|
||||
T_DIR="t"
|
||||
SRC_MK="src.mk"
|
||||
|
||||
# Maximum lines of test output to include per failed test
|
||||
MAX_OUTPUT_LINES=50
|
||||
|
||||
# Helper to escape string for JSON (handles newlines, quotes, backslashes, tabs)
|
||||
json_escape() {
|
||||
local str="$1"
|
||||
# Use python for reliable JSON escaping if available, otherwise use sed
|
||||
if command -v python3 &>/dev/null; then
|
||||
printf '%s' "$str" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read())[1:-1], end="")'
|
||||
else
|
||||
printf '%s' "$str" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g; s/\r/\\r/g' | awk '{printf "%s\\n", $0}' | sed 's/\\n$//'
|
||||
fi
|
||||
}
|
||||
|
||||
# Helper to output JSON and exit
|
||||
output_json() {
|
||||
local status="$1"
|
||||
local completed="${2:-0}"
|
||||
local total="${3:-0}"
|
||||
local failed="${4:-0}"
|
||||
local percent="${5:-0}"
|
||||
local eta="${6:-0}"
|
||||
local avg_time="${7:-0}"
|
||||
local last_item="${8:-}"
|
||||
local phase="${9:-}"
|
||||
local failed_tests="${10:-}"
|
||||
|
||||
# Build JSON output
|
||||
local json="{\"status\":\"$status\""
|
||||
|
||||
if [[ -n "$phase" ]]; then
|
||||
json="$json,\"phase\":\"$phase\""
|
||||
fi
|
||||
|
||||
json="$json,\"completed\":$completed,\"total\":$total,\"failed\":$failed,\"percent\":$percent"
|
||||
json="$json,\"eta_seconds\":$eta,\"avg_time\":\"$avg_time\",\"last_item\":\"$(json_escape "$last_item")\""
|
||||
|
||||
if [[ -n "$failed_tests" ]]; then
|
||||
json="$json,\"failed_tests\":[$failed_tests]"
|
||||
fi
|
||||
|
||||
json="$json}"
|
||||
echo "$json"
|
||||
}
|
||||
|
||||
# Get failed test info with log output
|
||||
get_failed_tests_json() {
|
||||
local log_file="$1"
|
||||
local t_dir="$2"
|
||||
local max_failures=10
|
||||
local count=0
|
||||
local first=true
|
||||
|
||||
# Get failed tests from LOG file
|
||||
while IFS=$'\t' read -r seq host starttime runtime send recv exitval signal cmd; do
|
||||
# Skip header line
|
||||
[[ "$seq" == "Seq" ]] && continue
|
||||
|
||||
# Check if failed (exitval != 0 or signal != 0)
|
||||
if [[ "$exitval" != "0" || "$signal" != "0" ]]; then
|
||||
# Extract test name from command
|
||||
test_name=$(echo "$cmd" | sed 's,.*/run-,,;s, .*,,')
|
||||
|
||||
# Get log file path
|
||||
log_path="$t_dir/log-run-$test_name"
|
||||
|
||||
# Read test output (last N lines)
|
||||
if [[ -f "$log_path" ]]; then
|
||||
output=$(tail -n "$MAX_OUTPUT_LINES" "$log_path" 2>/dev/null)
|
||||
else
|
||||
output="(log file not found: $log_path)"
|
||||
fi
|
||||
|
||||
# Escape output for JSON
|
||||
escaped_output=$(json_escape "$output")
|
||||
|
||||
# Build JSON object for this failure
|
||||
if [[ "$first" == "true" ]]; then
|
||||
first=false
|
||||
else
|
||||
printf ","
|
||||
fi
|
||||
printf '{"test":"%s","exit_code":%d,"signal":%d,"output":"%s"}' \
|
||||
"$test_name" "$exitval" "$signal" "$escaped_output"
|
||||
|
||||
((count++))
|
||||
if [[ $count -ge $max_failures ]]; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done < "$log_file"
|
||||
}
|
||||
|
||||
# Check if tests are running (LOG file exists)
|
||||
if [[ -f "$LOG_FILE" ]]; then
|
||||
# Count total tests from t/run-* files
|
||||
if [[ -d "$T_DIR" ]]; then
|
||||
total=$(find "$T_DIR" -name 'run-*' -type f 2>/dev/null | wc -l)
|
||||
else
|
||||
total=0
|
||||
fi
|
||||
|
||||
# If no parallel tests generated yet
|
||||
if [[ "$total" -eq 0 ]]; then
|
||||
output_json "running" 0 0 0 0 0 "0" "" "generating"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Parse LOG file (skip header line)
|
||||
# LOG format: Seq Host Starttime JobRuntime Send Receive Exitval Signal Command
|
||||
completed=$(tail -n +2 "$LOG_FILE" 2>/dev/null | wc -l)
|
||||
|
||||
# Count failures
|
||||
failed=$(awk -F'\t' 'NR>1 && ($7 != 0 || $8 != 0) {count++} END {print count+0}' "$LOG_FILE" 2>/dev/null)
|
||||
|
||||
# Get failed tests JSON with output (only if there are failures)
|
||||
if [[ "$failed" -gt 0 ]]; then
|
||||
failed_tests=$(get_failed_tests_json "$LOG_FILE" "$T_DIR")
|
||||
else
|
||||
failed_tests=""
|
||||
fi
|
||||
|
||||
# Calculate percentage
|
||||
if [[ "$total" -gt 0 ]]; then
|
||||
percent=$((completed * 100 / total))
|
||||
else
|
||||
percent=0
|
||||
fi
|
||||
|
||||
# Get last completed test name (extract from command column)
|
||||
last_test=$(tail -1 "$LOG_FILE" 2>/dev/null | awk -F'\t' '{print $9}' | sed 's,.*/run-,,;s, .*,,;s,^./,,')
|
||||
|
||||
# Calculate ETA based on average time
|
||||
if [[ "$completed" -gt 0 ]]; then
|
||||
avg_time=$(awk -F'\t' 'NR>1 {sum+=$4; count++} END {if(count>0) printf "%.1f", sum/count; else print "0"}' "$LOG_FILE")
|
||||
remaining=$((total - completed))
|
||||
eta=$(awk "BEGIN {printf \"%.0f\", $avg_time * $remaining}")
|
||||
else
|
||||
avg_time="0"
|
||||
eta="0"
|
||||
fi
|
||||
|
||||
# Determine status
|
||||
if [[ "$completed" -ge "$total" ]]; then
|
||||
status="completed"
|
||||
elif [[ "$completed" -gt 0 ]]; then
|
||||
status="running"
|
||||
else
|
||||
status="starting"
|
||||
fi
|
||||
|
||||
output_json "$status" "$completed" "$total" "$failed" "$percent" "$eta" "$avg_time" "$last_test" "testing" "$failed_tests"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# No LOG file - check if we're in compilation/linking phase
|
||||
# Count expected source files from src.mk
|
||||
if [[ -f "$SRC_MK" ]]; then
|
||||
# Count LIB_SOURCES (library object files to compile)
|
||||
expected_lib_objects=$(grep -E '\.cc\s*\\?$' "$SRC_MK" | grep -v '^#' | wc -l)
|
||||
|
||||
# Count TEST_MAIN_SOURCES (test binaries to link)
|
||||
expected_test_binaries=$(sed -n '/^TEST_MAIN_SOURCES =/,/^[^ ]/p' "$SRC_MK" | grep -cE '\.cc\s*\\?$' 2>/dev/null || echo 0)
|
||||
else
|
||||
expected_lib_objects=0
|
||||
expected_test_binaries=0
|
||||
fi
|
||||
|
||||
# Check for test generation phase (t/ directory being created)
|
||||
if [[ -d "$T_DIR" ]]; then
|
||||
total=$(find "$T_DIR" -name 'run-*' -type f 2>/dev/null | wc -l)
|
||||
if [[ "$total" -gt 0 ]]; then
|
||||
output_json "running" 0 "$total" 0 0 0 "0" "" "generating"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Count compiled object files (in subdirectories matching source structure)
|
||||
# Object files are created as dir/file.o (e.g., cache/cache.o, db/db_impl.o)
|
||||
compiled_objects=0
|
||||
if [[ "$expected_lib_objects" -gt 0 ]]; then
|
||||
# Count .o files in source directories
|
||||
compiled_objects=$(find cache db env file logging memory memtable monitoring options port table test_util trace_replay util utilities -name '*.o' -type f 2>/dev/null | wc -l)
|
||||
fi
|
||||
|
||||
# Count linked test binaries (test binaries are in current directory with _test suffix)
|
||||
linked_tests=0
|
||||
if [[ "$expected_test_binaries" -gt 0 ]]; then
|
||||
linked_tests=$(find . -maxdepth 1 -name '*_test' -type f -executable 2>/dev/null | wc -l)
|
||||
fi
|
||||
|
||||
# Determine phase based on what exists
|
||||
if [[ "$compiled_objects" -eq 0 && "$linked_tests" -eq 0 ]]; then
|
||||
# Nothing compiled yet - not started or just beginning
|
||||
output_json "not_started" 0 0 0 0 0 "0" ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Calculate total work units: compiling + linking
|
||||
total_work=$((expected_lib_objects + expected_test_binaries))
|
||||
completed_work=$((compiled_objects + linked_tests))
|
||||
|
||||
if [[ "$total_work" -gt 0 ]]; then
|
||||
percent=$((completed_work * 100 / total_work))
|
||||
else
|
||||
percent=0
|
||||
fi
|
||||
|
||||
# Determine phase
|
||||
if [[ "$compiled_objects" -lt "$expected_lib_objects" ]]; then
|
||||
phase="compiling"
|
||||
# Get most recently modified .o file as last_item
|
||||
last_item=$(find cache db env file logging memory memtable monitoring options port table test_util trace_replay util utilities -name '*.o' -type f -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2- | sed 's,^\./,,;s,\.o$,,')
|
||||
elif [[ "$linked_tests" -lt "$expected_test_binaries" ]]; then
|
||||
phase="linking"
|
||||
# Get most recently modified test binary as last_item
|
||||
last_item=$(find . -maxdepth 1 -name '*_test' -type f -executable -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2- | sed 's,^\./,,')
|
||||
else
|
||||
phase="generating"
|
||||
last_item=""
|
||||
fi
|
||||
|
||||
output_json "running" "$completed_work" "$total_work" 0 "$percent" 0 "0" "$last_item" "$phase"
|
||||
@@ -19,4 +19,3 @@ BENCHMARK_BASE=/mnt/gvfs/third-party2/benchmark/780c7a0f9cf0967961e69ad08e61cddd
|
||||
KERNEL_HEADERS_BASE=/mnt/gvfs/third-party2/kernel-headers/624a2f8f6c93c3c1df8aa4a6255d8202631a6c80/fb/platform010/da39a3e
|
||||
BINUTILS_BASE=/mnt/gvfs/third-party2/binutils/39579e8603b48b3540f8b0633f43adf29acccb8b/2.37/centos8-native/da39a3e
|
||||
VALGRIND_BASE=/mnt/gvfs/third-party2/valgrind/cd9cc656d49ecb53797ce4d055e49fde29fd57ff/3.19.0/platform010/76ebdda
|
||||
LUA_BASE=/mnt/gvfs/third-party2/lua/363787fa5cac2a8aa20638909210443278fa138e/5.3.4/platform010/9079c97
|
||||
@@ -164,12 +164,4 @@ EXEC_LDFLAGS_SHARED="$SNAPPY_LIBS $ZLIB_LIBS $BZIP_LIBS $LZ4_LIBS $ZSTD_LIBS $GF
|
||||
|
||||
VALGRIND_VER="$VALGRIND_BASE/bin/"
|
||||
|
||||
LUA_PATH="$LUA_BASE"
|
||||
|
||||
if test -z $PIC_BUILD; then
|
||||
LUA_LIB=" $LUA_PATH/lib/liblua.a"
|
||||
else
|
||||
LUA_LIB=" $LUA_PATH/lib/liblua_pic.a"
|
||||
fi
|
||||
|
||||
export CC CXX AR CFLAGS CXXFLAGS EXEC_LDFLAGS EXEC_LDFLAGS_SHARED VALGRIND_VER JEMALLOC_LIB JEMALLOC_INCLUDE CLANG_ANALYZER CLANG_SCAN_BUILD LUA_PATH LUA_LIB
|
||||
export CC CXX AR CFLAGS CXXFLAGS EXEC_LDFLAGS EXEC_LDFLAGS_SHARED VALGRIND_VER JEMALLOC_LIB JEMALLOC_INCLUDE CLANG_ANALYZER CLANG_SCAN_BUILD
|
||||
@@ -172,4 +172,4 @@ EXEC_LDFLAGS_SHARED="$SNAPPY_LIBS $ZLIB_LIBS $BZIP_LIBS $LZ4_LIBS $ZSTD_LIBS $GF
|
||||
|
||||
VALGRIND_VER="$VALGRIND_BASE/bin/"
|
||||
|
||||
export CC CXX AR AS CFLAGS CXXFLAGS EXEC_LDFLAGS EXEC_LDFLAGS_SHARED VALGRIND_VER JEMALLOC_LIB JEMALLOC_INCLUDE CLANG_ANALYZER CLANG_SCAN_BUILD LUA_PATH LUA_LIB
|
||||
export CC CXX AR AS CFLAGS CXXFLAGS EXEC_LDFLAGS EXEC_LDFLAGS_SHARED VALGRIND_VER JEMALLOC_LIB JEMALLOC_INCLUDE CLANG_ANALYZER CLANG_SCAN_BUILD
|
||||
@@ -7,14 +7,18 @@ print_usage () {
|
||||
echo "Usage:"
|
||||
echo "format-diff.sh [OPTIONS]"
|
||||
echo "-c: check only."
|
||||
echo "-y: auto-apply formatting without prompts (non-interactive mode)."
|
||||
echo "-h: print this message."
|
||||
}
|
||||
|
||||
while getopts ':ch' OPTION; do
|
||||
while getopts ':cyh' OPTION; do
|
||||
case "$OPTION" in
|
||||
c)
|
||||
CHECK_ONLY=1
|
||||
;;
|
||||
y)
|
||||
AUTO_APPLY=1
|
||||
;;
|
||||
h)
|
||||
print_usage
|
||||
exit 1
|
||||
@@ -148,6 +152,70 @@ else
|
||||
echo "Checking format of uncommitted changes..."
|
||||
fi
|
||||
|
||||
# Check for missing copyright in new files
|
||||
echo "Checking for copyright headers in new files..."
|
||||
|
||||
# Get list of new files (added, not just modified)
|
||||
if [ -z "$uncommitted_code" ]; then
|
||||
# Post-commit: check files added since merge base
|
||||
new_files=$(git diff --name-only --diff-filter=A "$FORMAT_UPSTREAM_MERGE_BASE" -- '*.h' '*.cc' '*.py' $EXCLUDE)
|
||||
else
|
||||
# Pre-commit: check staged new files
|
||||
new_files=$(git diff --name-only --diff-filter=A --cached HEAD -- '*.h' '*.cc' '*.py' $EXCLUDE)
|
||||
fi
|
||||
|
||||
if [ -n "$new_files" ]; then
|
||||
files_missing_copyright=""
|
||||
|
||||
for file in $new_files; do
|
||||
if [ -f "$file" ]; then
|
||||
# Check if file is missing copyright
|
||||
# For .py files, check for Python-style comment
|
||||
# For .h and .cc files, check for C++-style comment
|
||||
if [[ "$file" == *.py ]]; then
|
||||
if ! grep -q "Copyright (c) Meta Platforms, Inc. and affiliates" "$file"; then
|
||||
files_missing_copyright="$files_missing_copyright $file"
|
||||
# Add copyright header to Python file
|
||||
temp_file=$(mktemp)
|
||||
{
|
||||
echo "# Copyright (c) Meta Platforms, Inc. and affiliates."
|
||||
echo "# This source code is licensed under both the GPLv2 (found in the COPYING file in the root directory)"
|
||||
echo "# and the Apache 2.0 License (found in the LICENSE.Apache file in the root directory)."
|
||||
echo
|
||||
cat "$file"
|
||||
} > "$temp_file"
|
||||
mv "$temp_file" "$file"
|
||||
echo "Added copyright header to $file"
|
||||
fi
|
||||
elif [[ "$file" == *.h ]] || [[ "$file" == *.cc ]]; then
|
||||
if ! grep -q "Copyright (c) Meta Platforms, Inc. and affiliates" "$file"; then
|
||||
files_missing_copyright="$files_missing_copyright $file"
|
||||
# Add copyright header to C++ file
|
||||
temp_file=$(mktemp)
|
||||
{
|
||||
echo "// Copyright (c) Meta Platforms, Inc. and affiliates. "
|
||||
echo "// This source code is licensed under both the GPLv2 (found in the "
|
||||
echo "// COPYING file in the root directory) and Apache 2.0 License "
|
||||
echo "// (found in the LICENSE.Apache file in the root directory)."
|
||||
echo
|
||||
cat "$file"
|
||||
} > "$temp_file"
|
||||
mv "$temp_file" "$file"
|
||||
echo "Added copyright header to $file"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$files_missing_copyright" ]; then
|
||||
echo "Copyright headers were added to new files."
|
||||
else
|
||||
echo "All new files have copyright headers."
|
||||
fi
|
||||
else
|
||||
echo "No new files to check for copyright headers."
|
||||
fi
|
||||
|
||||
if [ -z "$diffs" ]
|
||||
then
|
||||
echo "Nothing needs to be reformatted!"
|
||||
@@ -176,11 +244,16 @@ echo "$diffs" |
|
||||
sed -e "s/\(^-.*$\)/`echo -e \"$COLOR_RED\1$COLOR_END\"`/" |
|
||||
sed -e "s/\(^+.*$\)/`echo -e \"$COLOR_GREEN\1$COLOR_END\"`/"
|
||||
|
||||
echo -e "Would you like to fix the format automatically (y/n): \c"
|
||||
# Handle auto-apply mode (non-interactive)
|
||||
if [ "$AUTO_APPLY" ]; then
|
||||
to_fix="y"
|
||||
else
|
||||
echo -e "Would you like to fix the format automatically (y/n): \c"
|
||||
|
||||
# Make sure under any mode, we can read user input.
|
||||
exec < /dev/tty
|
||||
read to_fix
|
||||
# Make sure under any mode, we can read user input.
|
||||
exec < /dev/tty
|
||||
read to_fix
|
||||
fi
|
||||
|
||||
if [ "$to_fix" != "y" ]
|
||||
then
|
||||
@@ -197,7 +270,8 @@ fi
|
||||
echo "Files reformatted!"
|
||||
|
||||
# Amend to last commit if user do the post-commit format check
|
||||
if [ -z "$uncommitted_code" ]; then
|
||||
# Skip amend prompt in auto-apply mode (user can amend manually if desired)
|
||||
if [ -z "$uncommitted_code" ] && [ -z "$AUTO_APPLY" ]; then
|
||||
echo -e "Would you like to amend the changes to last commit (`git log HEAD --oneline | head -1`)? (y/n): \c"
|
||||
read to_amend
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Pre-download packages with unreliable mirrors using fallback mirrors.
|
||||
Reads package info from folly's getdeps manifest files.
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import hashlib
|
||||
import subprocess
|
||||
import configparser
|
||||
|
||||
def sha256_file(path):
|
||||
"""Calculate SHA256 hash of a file."""
|
||||
h = hashlib.sha256()
|
||||
try:
|
||||
with open(path, 'rb') as f:
|
||||
for chunk in iter(lambda: f.read(65536), b''):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def parse_manifest(manifest_path):
|
||||
"""Parse a getdeps manifest file to extract download info."""
|
||||
config = configparser.ConfigParser()
|
||||
try:
|
||||
config.read(manifest_path)
|
||||
if 'download' in config:
|
||||
return {
|
||||
'url': config['download'].get('url', ''),
|
||||
'sha256': config['download'].get('sha256', ''),
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def get_fallback_mirrors(url):
|
||||
"""Get fallback mirror URLs for a given URL."""
|
||||
# Fallback mirror patterns for known unreliable hosts
|
||||
mirror_fallbacks = {
|
||||
"ftp.gnu.org/gnu/": [
|
||||
"https://mirrors.kernel.org/gnu/",
|
||||
"https://ftpmirror.gnu.org/gnu/",
|
||||
"https://ftp.gnu.org/gnu/",
|
||||
],
|
||||
"ftpmirror.gnu.org/gnu/": [
|
||||
"https://mirrors.kernel.org/gnu/",
|
||||
"https://ftpmirror.gnu.org/gnu/",
|
||||
"https://ftp.gnu.org/gnu/",
|
||||
],
|
||||
}
|
||||
|
||||
for pattern, mirrors in mirror_fallbacks.items():
|
||||
if pattern in url:
|
||||
# Extract the path after the pattern
|
||||
path_start = url.find(pattern) + len(pattern)
|
||||
path = url[path_start:]
|
||||
return [mirror + path for mirror in mirrors]
|
||||
return [url] # No fallback, use original
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 4:
|
||||
print(f"Usage: {sys.argv[0]} <download_dir> <cache_dir> <manifests_dir>")
|
||||
sys.exit(1)
|
||||
|
||||
download_dir, cache_dir, manifests_dir = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
|
||||
# Packages known to have unreliable mirrors
|
||||
packages_to_check = ["autoconf", "automake", "libtool"]
|
||||
|
||||
for package in packages_to_check:
|
||||
manifest_path = os.path.join(manifests_dir, package)
|
||||
if not os.path.exists(manifest_path):
|
||||
continue
|
||||
|
||||
info = parse_manifest(manifest_path)
|
||||
if not info or not info['url'] or not info['sha256']:
|
||||
continue
|
||||
|
||||
# Determine filename from URL
|
||||
url = info['url']
|
||||
expected_sha256 = info['sha256']
|
||||
url_filename = os.path.basename(url)
|
||||
|
||||
# getdeps uses format: {package}-{filename}
|
||||
filename = f"{package}-{url_filename}"
|
||||
filepath = os.path.join(download_dir, filename)
|
||||
cache_path = os.path.join(cache_dir, filename)
|
||||
|
||||
# Check if already valid
|
||||
if os.path.exists(filepath) and sha256_file(filepath) == expected_sha256:
|
||||
print(f" {filename}: OK (already downloaded)")
|
||||
continue
|
||||
|
||||
# Check cache
|
||||
if os.path.exists(cache_path) and sha256_file(cache_path) == expected_sha256:
|
||||
print(f" {filename}: OK (from cache)")
|
||||
subprocess.run(['cp', cache_path, filepath], check=True)
|
||||
continue
|
||||
|
||||
# Try fallback mirrors
|
||||
mirrors = get_fallback_mirrors(url)
|
||||
downloaded = False
|
||||
for mirror_url in mirrors:
|
||||
print(f" {filename}: trying {mirror_url}...")
|
||||
try:
|
||||
subprocess.run(['wget', '-q', '-O', filepath, mirror_url], check=True, timeout=120)
|
||||
if sha256_file(filepath) == expected_sha256:
|
||||
print(f" {filename}: OK (downloaded)")
|
||||
subprocess.run(['cp', filepath, cache_path], check=False)
|
||||
downloaded = True
|
||||
break
|
||||
else:
|
||||
os.remove(filepath)
|
||||
except Exception:
|
||||
if os.path.exists(filepath):
|
||||
os.remove(filepath)
|
||||
|
||||
if not downloaded:
|
||||
print(f" {filename}: WARNING - all mirrors failed")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -101,6 +101,5 @@ get_lib_base benchmark LATEST platform010
|
||||
get_lib_base kernel-headers fb platform010
|
||||
get_lib_base binutils LATEST centos8-native
|
||||
get_lib_base valgrind LATEST platform010
|
||||
get_lib_base lua 5.3.4 platform010
|
||||
|
||||
git diff $OUTPUT
|
||||
Vendored
-5
@@ -54,11 +54,6 @@ static std::unordered_map<std::string, OptionTypeInfo>
|
||||
{offsetof(struct CompressedSecondaryCacheOptions, compression_type),
|
||||
OptionType::kCompressionType, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kMutable}},
|
||||
{"compress_format_version",
|
||||
{offsetof(struct CompressedSecondaryCacheOptions,
|
||||
compress_format_version),
|
||||
OptionType::kUInt32T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kMutable}},
|
||||
{"enable_custom_split_merge",
|
||||
{offsetof(struct CompressedSecondaryCacheOptions,
|
||||
enable_custom_split_merge),
|
||||
|
||||
Vendored
+1
-1
@@ -119,7 +119,7 @@ DEFINE_uint32(seed, 0, "Hashing/random seed to use. 0 = choose at random");
|
||||
DEFINE_string(secondary_cache_uri, "",
|
||||
"Full URI for creating a custom secondary cache object");
|
||||
|
||||
DEFINE_string(cache_type, "lru_cache", "Type of block cache.");
|
||||
DEFINE_string(cache_type, "hyper_clock_cache", "Type of block cache.");
|
||||
|
||||
DEFINE_bool(use_jemalloc_no_dump_allocator, false,
|
||||
"Whether to use JemallocNoDumpAllocator");
|
||||
|
||||
Vendored
+9
-9
@@ -101,23 +101,23 @@ class CacheEntryStatsCollector {
|
||||
}
|
||||
|
||||
// Gets saved stats, regardless of age
|
||||
void GetStats(Stats *stats) {
|
||||
void GetStats(Stats* stats) {
|
||||
std::lock_guard<std::mutex> lock(saved_mutex_);
|
||||
*stats = saved_stats_;
|
||||
}
|
||||
|
||||
Cache *GetCache() const { return cache_; }
|
||||
Cache* GetCache() const { return cache_; }
|
||||
|
||||
// Gets or creates a shared instance of CacheEntryStatsCollector in the
|
||||
// cache itself, and saves into `ptr`. This shared_ptr will hold the
|
||||
// entry in cache until all refs are destroyed.
|
||||
static Status GetShared(Cache *raw_cache, SystemClock *clock,
|
||||
std::shared_ptr<CacheEntryStatsCollector> *ptr) {
|
||||
static Status GetShared(Cache* raw_cache, SystemClock* clock,
|
||||
std::shared_ptr<CacheEntryStatsCollector>* ptr) {
|
||||
assert(raw_cache);
|
||||
BasicTypedCacheInterface<CacheEntryStatsCollector, CacheEntryRole::kMisc>
|
||||
cache{raw_cache};
|
||||
|
||||
const Slice &cache_key = GetCacheKey();
|
||||
const Slice& cache_key = GetCacheKey();
|
||||
auto h = cache.Lookup(cache_key);
|
||||
if (h == nullptr) {
|
||||
// Not yet in cache, but Cache doesn't provide a built-in way to
|
||||
@@ -152,7 +152,7 @@ class CacheEntryStatsCollector {
|
||||
}
|
||||
|
||||
private:
|
||||
explicit CacheEntryStatsCollector(Cache *cache, SystemClock *clock)
|
||||
explicit CacheEntryStatsCollector(Cache* cache, SystemClock* clock)
|
||||
: saved_stats_(),
|
||||
working_stats_(),
|
||||
last_start_time_micros_(0),
|
||||
@@ -160,7 +160,7 @@ class CacheEntryStatsCollector {
|
||||
cache_(cache),
|
||||
clock_(clock) {}
|
||||
|
||||
static const Slice &GetCacheKey() {
|
||||
static const Slice& GetCacheKey() {
|
||||
// For each template instantiation
|
||||
static CacheKey ckey = CacheKey::CreateUniqueForProcessLifetime();
|
||||
static Slice ckey_slice = ckey.AsSlice();
|
||||
@@ -175,8 +175,8 @@ class CacheEntryStatsCollector {
|
||||
uint64_t last_start_time_micros_;
|
||||
uint64_t last_end_time_micros_;
|
||||
|
||||
Cache *const cache_;
|
||||
SystemClock *const clock_;
|
||||
Cache* const cache_;
|
||||
SystemClock* const clock_;
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
Vendored
+3
-3
@@ -24,7 +24,7 @@ namespace ROCKSDB_NAMESPACE {
|
||||
// 0 | >= 1<<63 | CreateUniqueForProcessLifetime
|
||||
// > 0 | any | OffsetableCacheKey.WithOffset
|
||||
|
||||
CacheKey CacheKey::CreateUniqueForCacheLifetime(Cache *cache) {
|
||||
CacheKey CacheKey::CreateUniqueForCacheLifetime(Cache* cache) {
|
||||
// +1 so that we can reserve all zeros for "unset" cache key
|
||||
uint64_t id = cache->NewId() + 1;
|
||||
// Ensure we don't collide with CreateUniqueForProcessLifetime
|
||||
@@ -297,8 +297,8 @@ CacheKey CacheKey::CreateUniqueForProcessLifetime() {
|
||||
//
|
||||
// TODO: Nevertheless / regardless, an efficient way to detect (and thus
|
||||
// quantify) block cache corruptions, including collisions, should be added.
|
||||
OffsetableCacheKey::OffsetableCacheKey(const std::string &db_id,
|
||||
const std::string &db_session_id,
|
||||
OffsetableCacheKey::OffsetableCacheKey(const std::string& db_id,
|
||||
const std::string& db_session_id,
|
||||
uint64_t file_number) {
|
||||
UniqueId64x2 internal_id;
|
||||
Status s = GetSstInternalUniqueId(db_id, db_session_id, file_number,
|
||||
|
||||
Vendored
+5
-5
@@ -44,13 +44,13 @@ class CacheKey {
|
||||
inline Slice AsSlice() const {
|
||||
static_assert(sizeof(*this) == 16, "Standardized on 16-byte cache key");
|
||||
assert(!IsEmpty());
|
||||
return Slice(reinterpret_cast<const char *>(this), sizeof(*this));
|
||||
return Slice(reinterpret_cast<const char*>(this), sizeof(*this));
|
||||
}
|
||||
|
||||
// Create a CacheKey that is unique among others associated with this Cache
|
||||
// instance. Depends on Cache::NewId. This is useful for block cache
|
||||
// "reservations".
|
||||
static CacheKey CreateUniqueForCacheLifetime(Cache *cache);
|
||||
static CacheKey CreateUniqueForCacheLifetime(Cache* cache);
|
||||
|
||||
// Create a CacheKey that is unique among others for the lifetime of this
|
||||
// process. This is useful for saving in a static data member so that
|
||||
@@ -87,7 +87,7 @@ class OffsetableCacheKey : private CacheKey {
|
||||
|
||||
// Constructs an OffsetableCacheKey with the given information about a file.
|
||||
// This constructor never generates an "empty" base key.
|
||||
OffsetableCacheKey(const std::string &db_id, const std::string &db_session_id,
|
||||
OffsetableCacheKey(const std::string& db_id, const std::string& db_session_id,
|
||||
uint64_t file_number);
|
||||
|
||||
// Creates an OffsetableCacheKey from an SST unique ID, so that cache keys
|
||||
@@ -134,9 +134,9 @@ class OffsetableCacheKey : private CacheKey {
|
||||
static_assert(sizeof(file_num_etc64_) == kCommonPrefixSize,
|
||||
"8 byte common prefix expected");
|
||||
assert(!IsEmpty());
|
||||
assert(&this->file_num_etc64_ == static_cast<const void *>(this));
|
||||
assert(&this->file_num_etc64_ == static_cast<const void*>(this));
|
||||
|
||||
return Slice(reinterpret_cast<const char *>(this), kCommonPrefixSize);
|
||||
return Slice(reinterpret_cast<const char*>(this), kCommonPrefixSize);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Vendored
+16
-16
@@ -44,8 +44,8 @@ class CacheReservationManager {
|
||||
bool increase) = 0;
|
||||
virtual Status MakeCacheReservation(
|
||||
std::size_t incremental_memory_used,
|
||||
std::unique_ptr<CacheReservationManager::CacheReservationHandle>
|
||||
*handle) = 0;
|
||||
std::unique_ptr<CacheReservationManager::CacheReservationHandle>*
|
||||
handle) = 0;
|
||||
virtual std::size_t GetTotalReservedCacheSize() = 0;
|
||||
virtual std::size_t GetTotalMemoryUsed() = 0;
|
||||
};
|
||||
@@ -90,11 +90,11 @@ class CacheReservationManagerImpl
|
||||
bool delayed_decrease = false);
|
||||
|
||||
// no copy constructor, copy assignment, move constructor, move assignment
|
||||
CacheReservationManagerImpl(const CacheReservationManagerImpl &) = delete;
|
||||
CacheReservationManagerImpl &operator=(const CacheReservationManagerImpl &) =
|
||||
CacheReservationManagerImpl(const CacheReservationManagerImpl&) = delete;
|
||||
CacheReservationManagerImpl& operator=(const CacheReservationManagerImpl&) =
|
||||
delete;
|
||||
CacheReservationManagerImpl(CacheReservationManagerImpl &&) = delete;
|
||||
CacheReservationManagerImpl &operator=(CacheReservationManagerImpl &&) =
|
||||
CacheReservationManagerImpl(CacheReservationManagerImpl&&) = delete;
|
||||
CacheReservationManagerImpl& operator=(CacheReservationManagerImpl&&) =
|
||||
delete;
|
||||
|
||||
~CacheReservationManagerImpl() override;
|
||||
@@ -178,7 +178,7 @@ class CacheReservationManagerImpl
|
||||
// REQUIRES: handle != nullptr
|
||||
Status MakeCacheReservation(
|
||||
std::size_t incremental_memory_used,
|
||||
std::unique_ptr<CacheReservationManager::CacheReservationHandle> *handle)
|
||||
std::unique_ptr<CacheReservationManager::CacheReservationHandle>* handle)
|
||||
override;
|
||||
|
||||
// Return the size of the cache (which is a multiple of kSizeDummyEntry)
|
||||
@@ -200,7 +200,7 @@ class CacheReservationManagerImpl
|
||||
// For testing only - it is to help ensure the CacheItemHelperForRole<R>
|
||||
// accessed from CacheReservationManagerImpl and the one accessed from the
|
||||
// test are from the same translation units
|
||||
static const Cache::CacheItemHelper *TEST_GetCacheItemHelperForRole();
|
||||
static const Cache::CacheItemHelper* TEST_GetCacheItemHelperForRole();
|
||||
|
||||
private:
|
||||
static constexpr std::size_t kSizeDummyEntry = 256 * 1024;
|
||||
@@ -216,7 +216,7 @@ class CacheReservationManagerImpl
|
||||
bool delayed_decrease_;
|
||||
std::atomic<std::size_t> cache_allocated_size_;
|
||||
std::size_t memory_used_;
|
||||
std::vector<Cache::Handle *> dummy_handles_;
|
||||
std::vector<Cache::Handle*> dummy_handles_;
|
||||
CacheKey cache_key_;
|
||||
};
|
||||
|
||||
@@ -251,14 +251,14 @@ class ConcurrentCacheReservationManager
|
||||
std::shared_ptr<CacheReservationManager> cache_res_mgr) {
|
||||
cache_res_mgr_ = std::move(cache_res_mgr);
|
||||
}
|
||||
ConcurrentCacheReservationManager(const ConcurrentCacheReservationManager &) =
|
||||
ConcurrentCacheReservationManager(const ConcurrentCacheReservationManager&) =
|
||||
delete;
|
||||
ConcurrentCacheReservationManager &operator=(
|
||||
const ConcurrentCacheReservationManager &) = delete;
|
||||
ConcurrentCacheReservationManager(ConcurrentCacheReservationManager &&) =
|
||||
ConcurrentCacheReservationManager& operator=(
|
||||
const ConcurrentCacheReservationManager&) = delete;
|
||||
ConcurrentCacheReservationManager(ConcurrentCacheReservationManager&&) =
|
||||
delete;
|
||||
ConcurrentCacheReservationManager &operator=(
|
||||
ConcurrentCacheReservationManager &&) = delete;
|
||||
ConcurrentCacheReservationManager& operator=(
|
||||
ConcurrentCacheReservationManager&&) = delete;
|
||||
|
||||
~ConcurrentCacheReservationManager() override {}
|
||||
|
||||
@@ -286,7 +286,7 @@ class ConcurrentCacheReservationManager
|
||||
|
||||
inline Status MakeCacheReservation(
|
||||
std::size_t incremental_memory_used,
|
||||
std::unique_ptr<CacheReservationManager::CacheReservationHandle> *handle)
|
||||
std::unique_ptr<CacheReservationManager::CacheReservationHandle>* handle)
|
||||
override {
|
||||
std::unique_ptr<CacheReservationManager::CacheReservationHandle>
|
||||
wrapped_handle;
|
||||
|
||||
Vendored
+1
-1
@@ -644,7 +644,7 @@ using TypedHandle = SharedCache::TypedHandle;
|
||||
|
||||
TEST_P(CacheTest, SetCapacity) {
|
||||
if (IsHyperClock()) {
|
||||
// TODO: update test & code for limited supoort
|
||||
// TODO: update test & code for limited support
|
||||
ROCKSDB_GTEST_BYPASS(
|
||||
"HyperClockCache doesn't support arbitrary capacity "
|
||||
"adjustments.");
|
||||
|
||||
Vendored
+355
-414
File diff suppressed because it is too large.
Load diff
Vendored
+135
-69
@@ -317,40 +317,89 @@ struct ClockHandle : public ClockHandleBasicData {
|
||||
// | acquire counter | release counter | hit bit | state marker |
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
// For reading or updating counters in meta word.
|
||||
static constexpr uint8_t kCounterNumBits = 30;
|
||||
static constexpr uint64_t kCounterMask = (uint64_t{1} << kCounterNumBits) - 1;
|
||||
struct SlotMeta : public BitFields<uint64_t, SlotMeta> {
|
||||
// For reading or updating counters in meta word.
|
||||
static constexpr uint8_t kCounterNumBits = 30;
|
||||
// Number of times the a reference has been acquired (or attempted)
|
||||
// since last reset by eviction processing
|
||||
using AcquireCounter =
|
||||
UnsignedBitField<SlotMeta, kCounterNumBits, NoPrevBitField>;
|
||||
// Number of times the a reference has been released (or attempted)
|
||||
// since last reset by eviction processing
|
||||
using ReleaseCounter =
|
||||
UnsignedBitField<SlotMeta, kCounterNumBits, AcquireCounter>;
|
||||
// Metadata bit in support of secondary cache
|
||||
using HitFlag = BoolBitField<SlotMeta, ReleaseCounter>;
|
||||
// Occupied means any state other than empty
|
||||
using OccupiedFlag = BoolBitField<SlotMeta, HitFlag>;
|
||||
// Shareable means the entry is reference counted (visible or invisible)
|
||||
// (only set if also occupied)
|
||||
using ShareableFlag = BoolBitField<SlotMeta, OccupiedFlag>;
|
||||
// Visible is only set if also shareable (invisible can't be found by
|
||||
// Lookup)
|
||||
using VisibleFlag = BoolBitField<SlotMeta, ShareableFlag>;
|
||||
|
||||
static constexpr uint8_t kAcquireCounterShift = 0;
|
||||
static constexpr uint64_t kAcquireIncrement = uint64_t{1}
|
||||
<< kAcquireCounterShift;
|
||||
static constexpr uint8_t kReleaseCounterShift = kCounterNumBits;
|
||||
static constexpr uint64_t kReleaseIncrement = uint64_t{1}
|
||||
<< kReleaseCounterShift;
|
||||
// Convenience functions
|
||||
uint32_t GetAcquireCounter() const { return Get<AcquireCounter>(); }
|
||||
void SetAcquireCounter(uint32_t val) { Set<AcquireCounter>(val); }
|
||||
uint32_t GetReleaseCounter() const { return Get<ReleaseCounter>(); }
|
||||
void SetReleaseCounter(uint32_t val) { Set<ReleaseCounter>(val); }
|
||||
uint32_t GetRefcount() const {
|
||||
return Get<AcquireCounter>() - Get<ReleaseCounter>();
|
||||
}
|
||||
bool GetHit() const { return Get<HitFlag>(); }
|
||||
void SetHit(bool val) { Set<HitFlag>(val); }
|
||||
|
||||
// For setting the hit bit
|
||||
static constexpr uint8_t kHitBitShift = 2U * kCounterNumBits;
|
||||
static constexpr uint64_t kHitBitMask = uint64_t{1} << kHitBitShift;
|
||||
// Some distinct states for the various state flags
|
||||
bool IsEmpty() const {
|
||||
bool rv = !Get<OccupiedFlag>();
|
||||
if (rv) {
|
||||
assert(!Get<ShareableFlag>());
|
||||
assert(!Get<VisibleFlag>());
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
// For reading or updating the state marker in meta word
|
||||
static constexpr uint8_t kStateShift = kHitBitShift + 1;
|
||||
bool IsUnderConstruction() const {
|
||||
bool rv = Get<OccupiedFlag>() && !Get<ShareableFlag>();
|
||||
if (rv) {
|
||||
assert(!Get<VisibleFlag>());
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
void SetUnderConstruction() {
|
||||
Set<OccupiedFlag>(true);
|
||||
Set<ShareableFlag>(false);
|
||||
Set<VisibleFlag>(false);
|
||||
}
|
||||
|
||||
// Bits contribution to state marker.
|
||||
// Occupied means any state other than empty
|
||||
static constexpr uint8_t kStateOccupiedBit = 0b100;
|
||||
// Shareable means the entry is reference counted (visible or invisible)
|
||||
// (only set if also occupied)
|
||||
static constexpr uint8_t kStateShareableBit = 0b010;
|
||||
// Visible is only set if also shareable
|
||||
static constexpr uint8_t kStateVisibleBit = 0b001;
|
||||
bool IsShareable() const { return Get<ShareableFlag>(); }
|
||||
bool IsInvisible() const {
|
||||
bool rv = Get<ShareableFlag>() && !Get<VisibleFlag>();
|
||||
if (rv) {
|
||||
assert(Get<OccupiedFlag>());
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
void SetInvisible() {
|
||||
Set<OccupiedFlag>(true);
|
||||
Set<ShareableFlag>(true);
|
||||
Set<VisibleFlag>(false);
|
||||
}
|
||||
|
||||
// Complete state markers (not shifted into full word)
|
||||
static constexpr uint8_t kStateEmpty = 0b000;
|
||||
static constexpr uint8_t kStateConstruction = kStateOccupiedBit;
|
||||
static constexpr uint8_t kStateInvisible =
|
||||
kStateOccupiedBit | kStateShareableBit;
|
||||
static constexpr uint8_t kStateVisible =
|
||||
kStateOccupiedBit | kStateShareableBit | kStateVisibleBit;
|
||||
bool IsVisible() const {
|
||||
bool rv = Get<ShareableFlag>() && Get<VisibleFlag>();
|
||||
if (rv) {
|
||||
assert(Get<OccupiedFlag>());
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
void SetVisible() {
|
||||
Set<OccupiedFlag>(true);
|
||||
Set<ShareableFlag>(true);
|
||||
Set<VisibleFlag>(true);
|
||||
}
|
||||
};
|
||||
|
||||
// Constants for initializing the countdown clock. (Countdown clock is only
|
||||
// in effect with zero refs, acquire counter == release counter, and in that
|
||||
@@ -364,7 +413,7 @@ struct ClockHandle : public ClockHandleBasicData {
|
||||
// TODO: make these coundown values tuning parameters for eviction?
|
||||
|
||||
// See above. Mutable for read reference counting.
|
||||
mutable AcqRelAtomic<uint64_t> meta{};
|
||||
mutable BitFieldsAtomic<SlotMeta> meta{};
|
||||
}; // struct ClockHandle
|
||||
|
||||
class BaseClockTable {
|
||||
@@ -431,9 +480,9 @@ class BaseClockTable {
|
||||
bool IsEvictionEffortExceeded(const BaseClockTable::EvictionData& data) const;
|
||||
#ifndef NDEBUG
|
||||
// Acquire N references
|
||||
void TEST_RefN(ClockHandle& handle, size_t n);
|
||||
void TEST_RefN(ClockHandle& handle, uint32_t n);
|
||||
// Helper for TEST_ReleaseN
|
||||
void TEST_ReleaseNMinus1(ClockHandle* handle, size_t n);
|
||||
void TEST_ReleaseNMinus1(ClockHandle* handle, uint32_t n);
|
||||
#endif
|
||||
|
||||
private: // fns
|
||||
@@ -489,13 +538,13 @@ class BaseClockTable {
|
||||
// TODO: is this separation needed if we don't do background evictions?
|
||||
ALIGN_AS(CACHE_LINE_SIZE)
|
||||
// Number of elements in the table.
|
||||
AcqRelAtomic<size_t> occupancy_{};
|
||||
Atomic<size_t> occupancy_{};
|
||||
|
||||
// Memory usage by entries tracked by the cache (including standalone)
|
||||
AcqRelAtomic<size_t> usage_{};
|
||||
Atomic<size_t> usage_{};
|
||||
|
||||
// Part of usage by standalone entries (not in table)
|
||||
AcqRelAtomic<size_t> standalone_usage_{};
|
||||
Atomic<size_t> standalone_usage_{};
|
||||
|
||||
// Maximum total charge of all elements stored in the table.
|
||||
// (Relaxed: eventual consistency/update is OK)
|
||||
@@ -586,7 +635,7 @@ class FixedHyperClockTable : public BaseClockTable {
|
||||
bool GrowIfNeeded(size_t new_occupancy, InsertState& state);
|
||||
|
||||
HandleImpl* DoInsert(const ClockHandleBasicData& proto,
|
||||
uint64_t initial_countdown, bool take_ref,
|
||||
uint32_t initial_countdown, bool take_ref,
|
||||
InsertState& state);
|
||||
|
||||
// Runs the clock eviction algorithm trying to reclaim at least
|
||||
@@ -614,7 +663,7 @@ class FixedHyperClockTable : public BaseClockTable {
|
||||
}
|
||||
|
||||
// Release N references
|
||||
void TEST_ReleaseN(HandleImpl* handle, size_t n);
|
||||
void TEST_ReleaseN(HandleImpl* handle, uint32_t n);
|
||||
#endif
|
||||
|
||||
// The load factor p is a real number in (0, 1) such that at all
|
||||
@@ -775,6 +824,7 @@ class AutoHyperClockTable : public BaseClockTable {
|
||||
// chain--specifically the next entry in the chain.
|
||||
// * The end of a chain is given a special "end" marker and refers back
|
||||
// to the head of the chain.
|
||||
// These decorated pointers use the NextWithShift bit field struct below.
|
||||
//
|
||||
// Why do we need shift on each pointer? To make Lookup wait-free, we need
|
||||
// to be able to query a chain without missing anything, and preferably
|
||||
@@ -794,47 +844,63 @@ class AutoHyperClockTable : public BaseClockTable {
|
||||
// it is normal to see "under construction" entries on the chain, and it
|
||||
// is not safe to read their hashed key without either a read reference
|
||||
// on the entry or a rewrite lock on the chain.
|
||||
struct NextWithShift : public BitFields<uint64_t, NextWithShift> {
|
||||
// The "shift" associated with this decorated pointer (see description
|
||||
// above).
|
||||
using Shift = UnsignedBitField<NextWithShift, 6, NoPrevBitField>;
|
||||
// Marker for the end of a chain. Must also (a) point back to the head of
|
||||
// the chain (with end marker removed), and (b) set the LockedFlag
|
||||
// (below), so that attempting to lock an empty chain has no effect (not
|
||||
// needed, as the lock is only needed for removals).
|
||||
using EndFlag = BoolBitField<NextWithShift, Shift>;
|
||||
// Marker that some thread owning writes to the chain structure (except
|
||||
// for inserts), but only if not an "end" pointer. Also called the
|
||||
// "rewrite lock."
|
||||
using LockedFlag = BoolBitField<NextWithShift, EndFlag>;
|
||||
// The "next" associated with this decorated pointer, which is an index
|
||||
// into the table's array_ (see description above).
|
||||
using Next = UnsignedBitField<NextWithShift, 56, LockedFlag>;
|
||||
|
||||
// Marker in a "with_shift" head pointer for some thread owning writes
|
||||
// to the chain structure (except for inserts), but only if not an
|
||||
// "end" pointer. Also called the "rewrite lock."
|
||||
static constexpr uint64_t kHeadLocked = uint64_t{1} << 7;
|
||||
bool IsLocked() const { return Get<LockedFlag>(); }
|
||||
bool IsEnd() const {
|
||||
// End flag should imply locked flag
|
||||
assert(!Get<EndFlag>() || Get<LockedFlag>());
|
||||
return Get<EndFlag>();
|
||||
}
|
||||
bool IsLockedNotEnd() const {
|
||||
// NOTE: helping GCC to optimize this simpler code:
|
||||
// return IsLocked() && !IsEnd();
|
||||
constexpr U kEndFlag = U{1} << EndFlag::kBitOffset;
|
||||
constexpr U kLockedFlag = U{1} << LockedFlag::kBitOffset;
|
||||
return (underlying & (kEndFlag | kLockedFlag)) == kLockedFlag;
|
||||
}
|
||||
auto GetNext() const { return Get<Next>(); }
|
||||
auto GetShift() const { return Get<Shift>(); }
|
||||
|
||||
// Marker in a "with_shift" pointer for the end of a chain. Must also
|
||||
// point back to the head of the chain (with end marker removed).
|
||||
// Also includes the "locked" bit so that attempting to lock an empty
|
||||
// chain has no effect (not needed, as the lock is only needed for
|
||||
// removals).
|
||||
static constexpr uint64_t kNextEndFlags = (uint64_t{1} << 6) | kHeadLocked;
|
||||
static NextWithShift Make(size_t next, int shift) {
|
||||
return NextWithShift{}.With<Next>(next).With<Shift>(
|
||||
static_cast<uint8_t>(shift));
|
||||
}
|
||||
|
||||
static inline bool IsEnd(uint64_t next_with_shift) {
|
||||
// Assuming certain values never used, suffices to check this one bit
|
||||
constexpr auto kCheckBit = kNextEndFlags ^ kHeadLocked;
|
||||
return next_with_shift & kCheckBit;
|
||||
}
|
||||
|
||||
// Bottom bits to right shift away to get an array index from a
|
||||
// "with_shift" pointer.
|
||||
static constexpr int kNextShift = 8;
|
||||
|
||||
// A bit mask for the "shift" associated with each "with_shift" pointer.
|
||||
// Always bottommost bits.
|
||||
static constexpr int kShiftMask = 63;
|
||||
static NextWithShift MakeEnd(size_t next, int shift) {
|
||||
return Make(next, shift).With<EndFlag>(true).With<LockedFlag>(true);
|
||||
}
|
||||
};
|
||||
|
||||
// A marker for head_next_with_shift that indicates this HandleImpl is
|
||||
// heap allocated (standalone) rather than in the table.
|
||||
static constexpr uint64_t kStandaloneMarker = UINT64_MAX;
|
||||
static constexpr NextWithShift kStandaloneMarker{UINT64_MAX};
|
||||
|
||||
// A marker for head_next_with_shift indicating the head is not yet part
|
||||
// of the usable table, or for chain_next_with_shift indicating that the
|
||||
// entry is not present or is not yet part of a chain (must not be
|
||||
// "shareable" state).
|
||||
static constexpr uint64_t kUnusedMarker = 0;
|
||||
static constexpr NextWithShift kUnusedMarker{0};
|
||||
|
||||
// See above. The head pointer is logically independent of the rest of
|
||||
// the entry, including the chain next pointer.
|
||||
AcqRelAtomic<uint64_t> head_next_with_shift{kUnusedMarker};
|
||||
AcqRelAtomic<uint64_t> chain_next_with_shift{kUnusedMarker};
|
||||
BitFieldsAtomic<NextWithShift> head_next_with_shift{kUnusedMarker};
|
||||
BitFieldsAtomic<NextWithShift> chain_next_with_shift{kUnusedMarker};
|
||||
|
||||
// For supporting CreateStandalone and some fallback cases.
|
||||
inline bool IsStandalone() const {
|
||||
@@ -880,7 +946,7 @@ class AutoHyperClockTable : public BaseClockTable {
|
||||
bool GrowIfNeeded(size_t new_occupancy, InsertState& state);
|
||||
|
||||
HandleImpl* DoInsert(const ClockHandleBasicData& proto,
|
||||
uint64_t initial_countdown, bool take_ref,
|
||||
uint32_t initial_countdown, bool take_ref,
|
||||
InsertState& state);
|
||||
|
||||
// Runs the clock eviction algorithm trying to reclaim at least
|
||||
@@ -908,7 +974,7 @@ class AutoHyperClockTable : public BaseClockTable {
|
||||
}
|
||||
|
||||
// Release N references
|
||||
void TEST_ReleaseN(HandleImpl* handle, size_t n);
|
||||
void TEST_ReleaseN(HandleImpl* handle, uint32_t n);
|
||||
#endif
|
||||
|
||||
// Maximum ratio of number of occupied slots to number of usable slots. The
|
||||
@@ -990,7 +1056,7 @@ class AutoHyperClockTable : public BaseClockTable {
|
||||
// To maximize parallelization of Grow() operations, this field is only
|
||||
// updated opportunistically after Grow() operations and in DoInsert() where
|
||||
// it is found to be out-of-date. See CatchUpLengthInfoNoWait().
|
||||
AcqRelAtomic<uint64_t> length_info_;
|
||||
Atomic<uint64_t> length_info_;
|
||||
|
||||
// An already-computed version of the usable length times the max load
|
||||
// factor. Could be slightly out of date but GrowIfNeeded()/Grow() handle
|
||||
@@ -1113,8 +1179,8 @@ class ALIGN_AS(CACHE_LINE_SIZE) ClockCacheShard final : public CacheShardBase {
|
||||
return table_.TEST_MutableOccupancyLimit();
|
||||
}
|
||||
// Acquire/release N references
|
||||
void TEST_RefN(HandleImpl* handle, size_t n);
|
||||
void TEST_ReleaseN(HandleImpl* handle, size_t n);
|
||||
void TEST_RefN(HandleImpl* handle, uint32_t n);
|
||||
void TEST_ReleaseN(HandleImpl* handle, uint32_t n);
|
||||
#endif
|
||||
|
||||
private: // data
|
||||
|
||||
Vendored
+1
-5
@@ -50,8 +50,7 @@ CompressedSecondaryCache::CompressedSecondaryCache(
|
||||
std::make_shared<CacheReservationManagerImpl<CacheEntryRole::kMisc>>(
|
||||
cache_))),
|
||||
disable_cache_(opts.capacity == 0) {
|
||||
auto mgr =
|
||||
GetBuiltinCompressionManager(cache_options_.compress_format_version);
|
||||
auto mgr = GetBuiltinV2CompressionManager();
|
||||
compressor_ = mgr->GetCompressor(cache_options_.compression_opts,
|
||||
cache_options_.compression_type);
|
||||
decompressor_ =
|
||||
@@ -356,9 +355,6 @@ std::string CompressedSecondaryCache::GetPrintableOptions() const {
|
||||
const_cast<CompressionOptions&>(cache_options_.compression_opts))
|
||||
.c_str());
|
||||
ret.append(buffer);
|
||||
snprintf(buffer, kBufferSize, " compress_format_version : %d\n",
|
||||
cache_options_.compress_format_version);
|
||||
ret.append(buffer);
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
+2
-3
@@ -856,8 +856,7 @@ TEST_P(CompressedSecondaryCacheTestWithCompressionParam, BasicTestFromString) {
|
||||
if (LZ4_Supported()) {
|
||||
sec_cache_uri =
|
||||
"compressed_secondary_cache://"
|
||||
"capacity=2048;num_shard_bits=0;compression_type=kLZ4Compression;"
|
||||
"compress_format_version=2";
|
||||
"capacity=2048;num_shard_bits=0;compression_type=kLZ4Compression";
|
||||
} else {
|
||||
ROCKSDB_GTEST_SKIP("This test requires LZ4 support.");
|
||||
sec_cache_uri =
|
||||
@@ -888,7 +887,7 @@ TEST_P(CompressedSecondaryCacheTestWithCompressionParam,
|
||||
sec_cache_uri =
|
||||
"compressed_secondary_cache://"
|
||||
"capacity=2048;num_shard_bits=0;compression_type=kLZ4Compression;"
|
||||
"compress_format_version=2;enable_custom_split_merge=true";
|
||||
"enable_custom_split_merge=true";
|
||||
} else {
|
||||
ROCKSDB_GTEST_SKIP("This test requires LZ4 support.");
|
||||
sec_cache_uri =
|
||||
|
||||
Vendored
+18
-18
@@ -1503,7 +1503,7 @@ TEST_P(BasicSecondaryCacheTest, FullCapacityTest) {
|
||||
/*context*/ this, Cache::Priority::LOW);
|
||||
ASSERT_EQ(handle1, nullptr);
|
||||
|
||||
// k1 promotion can fail with strict_capacit_limit=true, but Lookup still
|
||||
// k1 promotion can fail with strict_capacity_limit=true, but Lookup still
|
||||
// succeeds using a standalone handle
|
||||
handle1 = cache->Lookup(k1.AsSlice(), GetHelper(),
|
||||
/*context*/ this, Cache::Priority::LOW);
|
||||
@@ -1680,7 +1680,7 @@ TEST_P(DBSecondaryCacheTest, TestSecondaryCacheCorrectness2) {
|
||||
// After Flush is successful, RocksDB will do the paranoid check for the new
|
||||
// SST file. Meta blocks are always cached in the block cache and they
|
||||
// will not be evicted. When block_2 is cache miss and read out, it is
|
||||
// inserted to the block cache. Thefore, block_1 is evicted from block
|
||||
// inserted to the block cache. Therefore, block_1 is evicted from block
|
||||
// cache and successfully inserted to the secondary cache. Here are 2
|
||||
// lookups in the secondary cache for block_1 and block_2.
|
||||
ASSERT_EQ(secondary_cache->num_inserts(), 1u);
|
||||
@@ -1721,7 +1721,7 @@ TEST_P(DBSecondaryCacheTest, TestSecondaryCacheCorrectness2) {
|
||||
v = Get(Key(0));
|
||||
ASSERT_EQ(1007, v.size());
|
||||
// This Get needs to access block_1, since block_1 is not in block cache
|
||||
// there is one econdary cache lookup. Then, block_1 is cached in the
|
||||
// there is one secondary cache lookup. Then, block_1 is cached in the
|
||||
// block cache.
|
||||
ASSERT_EQ(secondary_cache->num_inserts(), 2u);
|
||||
ASSERT_EQ(secondary_cache->num_lookups(), 5u);
|
||||
@@ -1785,7 +1785,7 @@ TEST_P(DBSecondaryCacheTest, NoSecondaryCacheInsertion) {
|
||||
std::string v = Get(Key(0));
|
||||
ASSERT_EQ(1000, v.size());
|
||||
// Since the block cache is large enough, all the blocks are cached. we
|
||||
// do not need to lookup the seondary cache.
|
||||
// do not need to lookup the secondary cache.
|
||||
ASSERT_EQ(secondary_cache->num_inserts(), 0u);
|
||||
ASSERT_EQ(secondary_cache->num_lookups(), 2u);
|
||||
|
||||
@@ -2150,7 +2150,7 @@ TEST_P(DBSecondaryCacheTest, LRUCacheDumpLoadBasic) {
|
||||
ASSERT_OK(Flush());
|
||||
Compact("a", "z");
|
||||
|
||||
// do th eread for all the key value pairs, so all the blocks should be in
|
||||
// do the read for all the key value pairs, so all the blocks should be in
|
||||
// cache
|
||||
uint32_t start_insert = cache->GetInsertCount();
|
||||
uint32_t start_lookup = cache->GetLookupcount();
|
||||
@@ -2179,7 +2179,7 @@ TEST_P(DBSecondaryCacheTest, LRUCacheDumpLoadBasic) {
|
||||
&cache_dumper);
|
||||
ASSERT_OK(s);
|
||||
std::vector<DB*> db_list;
|
||||
db_list.push_back(db_);
|
||||
db_list.push_back(db_.get());
|
||||
s = cache_dumper->SetDumpFilter(db_list);
|
||||
ASSERT_OK(s);
|
||||
s = cache_dumper->DumpCacheEntriesToWriter();
|
||||
@@ -2263,11 +2263,11 @@ TEST_P(DBSecondaryCacheTest, LRUCacheDumpLoadWithFilter) {
|
||||
options.env = fault_env_.get();
|
||||
std::string dbname1 = test::PerThreadDBPath("db_1");
|
||||
ASSERT_OK(DestroyDB(dbname1, options));
|
||||
DB* db1 = nullptr;
|
||||
std::unique_ptr<DB> db1;
|
||||
ASSERT_OK(DB::Open(options, dbname1, &db1));
|
||||
std::string dbname2 = test::PerThreadDBPath("db_2");
|
||||
ASSERT_OK(DestroyDB(dbname2, options));
|
||||
DB* db2 = nullptr;
|
||||
std::unique_ptr<DB> db2;
|
||||
ASSERT_OK(DB::Open(options, dbname2, &db2));
|
||||
fault_fs_->SetFailGetUniqueId(true);
|
||||
|
||||
@@ -2335,7 +2335,7 @@ TEST_P(DBSecondaryCacheTest, LRUCacheDumpLoadWithFilter) {
|
||||
&cache_dumper);
|
||||
ASSERT_OK(s);
|
||||
std::vector<DB*> db_list;
|
||||
db_list.push_back(db1);
|
||||
db_list.push_back(db1.get());
|
||||
s = cache_dumper->SetDumpFilter(db_list);
|
||||
ASSERT_OK(s);
|
||||
s = cache_dumper->DumpCacheEntriesToWriter();
|
||||
@@ -2377,7 +2377,7 @@ TEST_P(DBSecondaryCacheTest, LRUCacheDumpLoadWithFilter) {
|
||||
ASSERT_OK(s);
|
||||
|
||||
ASSERT_OK(db1->Close());
|
||||
delete db1;
|
||||
db1.reset();
|
||||
ASSERT_OK(DB::Open(options, dbname1, &db1));
|
||||
|
||||
// After load, we do the Get again. To validate the cache, we do not allow any
|
||||
@@ -2406,8 +2406,8 @@ TEST_P(DBSecondaryCacheTest, LRUCacheDumpLoadWithFilter) {
|
||||
ASSERT_EQ(256, static_cast<int>(block_lookup));
|
||||
fault_fs_->SetFailGetUniqueId(false);
|
||||
fault_fs_->SetFilesystemActive(true);
|
||||
delete db1;
|
||||
delete db2;
|
||||
db1.reset();
|
||||
db2.reset();
|
||||
ASSERT_OK(DestroyDB(dbname1, options));
|
||||
ASSERT_OK(DestroyDB(dbname2, options));
|
||||
}
|
||||
@@ -2464,7 +2464,7 @@ TEST_P(DBSecondaryCacheTest, TestSecondaryCacheOptionBasic) {
|
||||
std::string v = Get(Key(0));
|
||||
ASSERT_EQ(1007, v.size());
|
||||
|
||||
// Check the data in first block. Cache miss, direclty read from SST file.
|
||||
// Check the data in first block. Cache miss, directly read from SST file.
|
||||
ASSERT_EQ(secondary_cache->num_inserts(), 0u);
|
||||
ASSERT_EQ(secondary_cache->num_lookups(), 0u);
|
||||
|
||||
@@ -2598,7 +2598,7 @@ TEST_P(DBSecondaryCacheTest, TestSecondaryCacheOptionChange) {
|
||||
}
|
||||
|
||||
// Two DB test. We create 2 DBs sharing the same block cache and secondary
|
||||
// cache. We diable the secondary cache option for DB2.
|
||||
// cache. We disable the secondary cache option for DB2.
|
||||
TEST_P(DBSecondaryCacheTest, TestSecondaryCacheOptionTwoDB) {
|
||||
if (IsHyperClock()) {
|
||||
ROCKSDB_GTEST_BYPASS("Test depends on LRUCache-specific behaviors");
|
||||
@@ -2619,11 +2619,11 @@ TEST_P(DBSecondaryCacheTest, TestSecondaryCacheOptionTwoDB) {
|
||||
options.paranoid_file_checks = true;
|
||||
std::string dbname1 = test::PerThreadDBPath("db_t_1");
|
||||
ASSERT_OK(DestroyDB(dbname1, options));
|
||||
DB* db1 = nullptr;
|
||||
std::unique_ptr<DB> db1;
|
||||
ASSERT_OK(DB::Open(options, dbname1, &db1));
|
||||
std::string dbname2 = test::PerThreadDBPath("db_t_2");
|
||||
ASSERT_OK(DestroyDB(dbname2, options));
|
||||
DB* db2 = nullptr;
|
||||
std::unique_ptr<DB> db2;
|
||||
Options options2 = options;
|
||||
options2.lowest_used_cache_tier = CacheTier::kVolatileTier;
|
||||
ASSERT_OK(DB::Open(options2, dbname2, &db2));
|
||||
@@ -2700,8 +2700,8 @@ TEST_P(DBSecondaryCacheTest, TestSecondaryCacheOptionTwoDB) {
|
||||
|
||||
fault_fs_->SetFailGetUniqueId(false);
|
||||
fault_fs_->SetFilesystemActive(true);
|
||||
delete db1;
|
||||
delete db2;
|
||||
db1.reset();
|
||||
db2.reset();
|
||||
ASSERT_OK(DestroyDB(dbname1, options));
|
||||
ASSERT_OK(DestroyDB(dbname2, options));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,512 @@
|
||||
# Adding New Options to RocksDB Public API
|
||||
|
||||
This document provides guidance on how to add new options to RocksDB's public API. There are two main categories of options:
|
||||
|
||||
1. **Standard Column Family Options** (Options/DBOptions/AdvancedColumnFamilyOptions)
|
||||
2. **BlockBasedTableOptions** (options specific to block-based table format)
|
||||
|
||||
## Overview of Files to Modify
|
||||
|
||||
### For Standard Column Family Options
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `include/rocksdb/advanced_options.h` | Define the option with documentation |
|
||||
| `include/rocksdb/options.h` | Add reference in related option groups if needed |
|
||||
| `options/cf_options.h` | Add to `MutableCFOptions` or `ImmutableCFOptions` struct |
|
||||
| `options/cf_options.cc` | Register option for serialization/deserialization and logging |
|
||||
| `options/options_helper.cc` | Add to `UpdateColumnFamilyOptions()` for mutable options |
|
||||
| `options/options_settable_test.cc` | Add to test string for option parsing |
|
||||
| `db_stress_tool/db_stress_common.h` | Declare gflag |
|
||||
| `db_stress_tool/db_stress_gflags.cc` | Define gflag with default value |
|
||||
| `db_stress_tool/db_stress_test_base.cc` | Apply flag to options |
|
||||
| `tools/db_bench_tool.cc` | Add flag definition and apply to options |
|
||||
| `tools/db_crashtest.py` | Add randomized values for stress testing |
|
||||
| `unreleased_history/new_features/` | Add release note markdown file |
|
||||
|
||||
### For BlockBasedTableOptions
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `include/rocksdb/table.h` | Define the option in `BlockBasedTableOptions` struct |
|
||||
| `table/block_based/block_based_table_factory.cc` | Register for serialization, validation, and printing |
|
||||
| `options/options_settable_test.cc` | Add to `BlockBasedTableOptionsAllFieldsSettable` test |
|
||||
| `options/options_test.cc` | Add to `MutableCFOptions` test if applicable |
|
||||
| `db_stress_tool/db_stress_common.h` | Declare gflag |
|
||||
| `db_stress_tool/db_stress_gflags.cc` | Define gflag |
|
||||
| `db_stress_tool/db_stress_test_base.cc` | Apply flag to `block_based_options` |
|
||||
| `tools/db_bench_tool.cc` | Add flag definition and apply to `block_based_options` |
|
||||
| `tools/db_crashtest.py` | Add randomized values |
|
||||
| `java/src/main/java/org/rocksdb/BlockBasedTableConfig.java` | Java API |
|
||||
| `java/rocksjni/portal.h` | JNI portal for Java bindings |
|
||||
| `java/rocksjni/table.cc` | JNI implementation |
|
||||
| `java/src/test/java/org/rocksdb/BlockBasedTableConfigTest.java` | Java unit test |
|
||||
|
||||
---
|
||||
|
||||
## Pattern 1: Adding a Standard Column Family Option
|
||||
|
||||
Example reference: commit `94e65a2e0b4f817aa4bfa4c96cdf867e7980d7bc` (memtable_veirfy_per_key_checksum_on_seek)
|
||||
|
||||
### Step 1: Define the Option in Public Header
|
||||
|
||||
**File: `include/rocksdb/advanced_options.h`**
|
||||
|
||||
Add the option with documentation in `AdvancedColumnFamilyOptions` struct:
|
||||
|
||||
```cpp
|
||||
// Enables additional integrity checks during seek.
|
||||
// Specifically, for skiplist-based memtables, key checksum validation could
|
||||
// be enabled during seek optionally. This is helpful to detect corrupted
|
||||
// memtable keys during reads. Enabling this feature incurs a performance
|
||||
// overhead due to additional key checksum validation during memtable seek
|
||||
// operation.
|
||||
// This option depends on memtable_protection_bytes_per_key to be non zero.
|
||||
// If memtable_protection_bytes_per_key is zero, no validation is performed.
|
||||
bool memtable_veirfy_per_key_checksum_on_seek = false;
|
||||
```
|
||||
|
||||
### Step 2: Add to Internal Options Structs
|
||||
|
||||
**File: `options/cf_options.h`**
|
||||
|
||||
Add to `MutableCFOptions` struct (or `ImmutableCFOptions` for immutable options):
|
||||
|
||||
```cpp
|
||||
// In MutableCFOptions constructor from Options:
|
||||
memtable_veirfy_per_key_checksum_on_seek(
|
||||
options.memtable_veirfy_per_key_checksum_on_seek),
|
||||
|
||||
// In MutableCFOptions default constructor:
|
||||
memtable_veirfy_per_key_checksum_on_seek(false),
|
||||
|
||||
// In MutableCFOptions struct member declarations:
|
||||
bool memtable_veirfy_per_key_checksum_on_seek;
|
||||
```
|
||||
|
||||
### Step 3: Register for Serialization/Deserialization
|
||||
|
||||
**File: `options/cf_options.cc`**
|
||||
|
||||
Add to the options type info map for serialization:
|
||||
|
||||
```cpp
|
||||
{"memtable_veirfy_per_key_checksum_on_seek",
|
||||
{offsetof(struct MutableCFOptions,
|
||||
memtable_veirfy_per_key_checksum_on_seek),
|
||||
OptionType::kBoolean, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kMutable}},
|
||||
```
|
||||
|
||||
Add logging in `MutableCFOptions::Dump()`:
|
||||
|
||||
```cpp
|
||||
ROCKS_LOG_INFO(log, "memtable_veirfy_per_key_checksum_on_seek: %d",
|
||||
memtable_veirfy_per_key_checksum_on_seek);
|
||||
```
|
||||
|
||||
### Step 4: Update Options Helper
|
||||
|
||||
**File: `options/options_helper.cc`**
|
||||
|
||||
Add to `UpdateColumnFamilyOptions()`:
|
||||
|
||||
```cpp
|
||||
cf_opts->memtable_veirfy_per_key_checksum_on_seek =
|
||||
moptions.memtable_veirfy_per_key_checksum_on_seek;
|
||||
```
|
||||
|
||||
### Step 5: Add to Options Settable Test
|
||||
|
||||
**File: `options/options_settable_test.cc`**
|
||||
|
||||
Add to the test string in `ColumnFamilyOptionsAllFieldsSettable`:
|
||||
|
||||
```cpp
|
||||
"memtable_veirfy_per_key_checksum_on_seek=1;"
|
||||
```
|
||||
|
||||
### Step 6: Add db_stress Support
|
||||
|
||||
**File: `db_stress_tool/db_stress_common.h`**
|
||||
|
||||
```cpp
|
||||
DECLARE_bool(memtable_veirfy_per_key_checksum_on_seek);
|
||||
```
|
||||
|
||||
**File: `db_stress_tool/db_stress_gflags.cc`**
|
||||
|
||||
```cpp
|
||||
DEFINE_bool(
|
||||
memtable_veirfy_per_key_checksum_on_seek,
|
||||
ROCKSDB_NAMESPACE::Options().memtable_veirfy_per_key_checksum_on_seek,
|
||||
"Sets CF option memtable_veirfy_per_key_checksum_on_seek.");
|
||||
```
|
||||
|
||||
**File: `db_stress_tool/db_stress_test_base.cc`**
|
||||
|
||||
```cpp
|
||||
options.memtable_veirfy_per_key_checksum_on_seek =
|
||||
FLAGS_memtable_veirfy_per_key_checksum_on_seek;
|
||||
```
|
||||
|
||||
### Step 7: Add db_bench Support
|
||||
|
||||
**File: `tools/db_bench_tool.cc`**
|
||||
|
||||
```cpp
|
||||
// Flag definition (near related flags):
|
||||
DEFINE_bool(memtable_veirfy_per_key_checksum_on_seek, false,
|
||||
"Sets CF option memtable_veirfy_per_key_checksum_on_seek");
|
||||
|
||||
// Apply flag to options (in InitializeOptionsFromFlags or similar):
|
||||
options.memtable_veirfy_per_key_checksum_on_seek =
|
||||
FLAGS_memtable_veirfy_per_key_checksum_on_seek;
|
||||
```
|
||||
|
||||
### Step 8: Add Crash Test Support
|
||||
|
||||
**File: `tools/db_crashtest.py`**
|
||||
|
||||
```python
|
||||
"memtable_veirfy_per_key_checksum_on_seek": lambda: random.choice([0] * 7 + [1]),
|
||||
```
|
||||
|
||||
Also add constraint handling in `finalize_and_sanitize()` if needed:
|
||||
|
||||
```python
|
||||
# only skip list memtable representation supports paranoid memory checks
|
||||
if dest_params.get("memtablerep") != "skip_list":
|
||||
dest_params["memtable_veirfy_per_key_checksum_on_seek"] = 0
|
||||
```
|
||||
|
||||
### Step 9: Add Release Note
|
||||
|
||||
**File: `unreleased_history/new_features/<descriptive_name>.md`**
|
||||
|
||||
```markdown
|
||||
A new flag memtable_veirfy_per_key_checksum_on_seek is added to AdvancedColumnFamilyOptions. When it is enabled, it will validate key checksum along the binary search path on skiplist based memtable during seek operation.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 2: Adding a BlockBasedTableOptions Option
|
||||
|
||||
Example reference: commit `742741b175c5f238374c1714f9db3340d49de569` (super_block_alignment_size)
|
||||
|
||||
### Step 1: Define the Option in Public Header
|
||||
|
||||
**File: `include/rocksdb/table.h`**
|
||||
|
||||
Add to `BlockBasedTableOptions` struct with documentation:
|
||||
|
||||
```cpp
|
||||
// Align data blocks on super block alignment. Avoid a data block split across
|
||||
// super block boundaries. Works with/without compression.
|
||||
//
|
||||
// Here a "super block" refers to an aligned unit of underlying Filesystem
|
||||
// storage for which there is an extra cost when a random read involves two
|
||||
// such super blocks instead of just one. Configuring that size here suggests
|
||||
// inserting padding in the SST file to avoid a single SST block splitting
|
||||
// across two super blocks. Only power-of-two sizes are supported. See also
|
||||
// super_block_alignment_space_overhead_ratio. Default to 0, which means super
|
||||
// block alignment is disabled.
|
||||
size_t super_block_alignment_size = 0;
|
||||
|
||||
// This option controls the storage space overhead of super block alignment.
|
||||
// It is used to calculate the max padding size allowed for super block
|
||||
// alignment. It is calculated in this way. If super_block_alignment_size is
|
||||
// 2MB, and super_block_alignment_overhead_ratio is 128, then the max padding
|
||||
// size allowed for super block alignment is 2MB / 128 = 16KB.
|
||||
// Note that, when it is set to 0, super block alignment is disabled.
|
||||
size_t super_block_alignment_space_overhead_ratio = 128;
|
||||
```
|
||||
|
||||
### Step 2: Register for Serialization in Table Factory
|
||||
|
||||
**File: `table/block_based/block_based_table_factory.cc`**
|
||||
|
||||
Add to the type info map:
|
||||
|
||||
```cpp
|
||||
{"super_block_alignment_size",
|
||||
{offsetof(struct BlockBasedTableOptions, super_block_alignment_size),
|
||||
OptionType::kSizeT, OptionVerificationType::kNormal}},
|
||||
{"super_block_alignment_space_overhead_ratio",
|
||||
{offsetof(struct BlockBasedTableOptions,
|
||||
super_block_alignment_space_overhead_ratio),
|
||||
OptionType::kSizeT, OptionVerificationType::kNormal}},
|
||||
```
|
||||
|
||||
Add validation in `ValidateOptions()`:
|
||||
|
||||
```cpp
|
||||
if ((table_options_.super_block_alignment_size &
|
||||
(table_options_.super_block_alignment_size - 1))) {
|
||||
return Status::InvalidArgument(
|
||||
"Super Block alignment requested but super block alignment size is not "
|
||||
"a power of 2");
|
||||
}
|
||||
if (table_options_.super_block_alignment_size >
|
||||
std::numeric_limits<uint32_t>::max()) {
|
||||
return Status::InvalidArgument(
|
||||
"Super block alignment size exceeds maximum number (4GiB) allowed");
|
||||
}
|
||||
```
|
||||
|
||||
Add printing in `GetPrintableOptions()`:
|
||||
|
||||
```cpp
|
||||
snprintf(buffer, kBufferSize,
|
||||
" super_block_alignment_size: %" ROCKSDB_PRIszt "\n",
|
||||
table_options_.super_block_alignment_size);
|
||||
ret.append(buffer);
|
||||
```
|
||||
|
||||
### Step 3: Add to Options Settable Test
|
||||
|
||||
**File: `options/options_settable_test.cc`**
|
||||
|
||||
Add to `BlockBasedTableOptionsAllFieldsSettable` test:
|
||||
|
||||
```cpp
|
||||
"super_block_alignment_size=65536;"
|
||||
"super_block_alignment_space_overhead_ratio=4096;"
|
||||
```
|
||||
|
||||
### Step 4: Add to Options Test
|
||||
|
||||
**File: `options/options_test.cc`**
|
||||
|
||||
```cpp
|
||||
ASSERT_OK(GetColumnFamilyOptionsFromString(
|
||||
config_options, cf_opts,
|
||||
"block_based_table_factory.super_block_alignment_size=65536; "
|
||||
"block_based_table_factory.super_block_alignment_space_overhead_ratio=4096;",
|
||||
&cf_opts));
|
||||
ASSERT_EQ(bbto->super_block_alignment_size, 65536);
|
||||
ASSERT_EQ(bbto->super_block_alignment_space_overhead_ratio, 4096);
|
||||
```
|
||||
|
||||
### Step 5: Add db_stress Support
|
||||
|
||||
**File: `db_stress_tool/db_stress_common.h`**
|
||||
|
||||
```cpp
|
||||
DECLARE_uint64(super_block_alignment_size);
|
||||
DECLARE_uint64(super_block_alignment_space_overhead_ratio);
|
||||
```
|
||||
|
||||
**File: `db_stress_tool/db_stress_gflags.cc`**
|
||||
|
||||
```cpp
|
||||
DEFINE_uint64(
|
||||
super_block_alignment_size,
|
||||
ROCKSDB_NAMESPACE::BlockBasedTableOptions().super_block_alignment_size,
|
||||
"BlockBasedTableOptions.super_block_alignment_size");
|
||||
|
||||
DEFINE_uint64(
|
||||
super_block_alignment_space_overhead_ratio,
|
||||
ROCKSDB_NAMESPACE::BlockBasedTableOptions()
|
||||
.super_block_alignment_space_overhead_ratio,
|
||||
"BlockBasedTableOptions.super_block_alignment_space_overhead_ratio");
|
||||
```
|
||||
|
||||
**File: `db_stress_tool/db_stress_test_base.cc`**
|
||||
|
||||
```cpp
|
||||
block_based_options.super_block_alignment_size =
|
||||
fLU64::FLAGS_super_block_alignment_size;
|
||||
block_based_options.super_block_alignment_space_overhead_ratio =
|
||||
fLU64::FLAGS_super_block_alignment_space_overhead_ratio;
|
||||
```
|
||||
|
||||
### Step 6: Add db_bench Support
|
||||
|
||||
**File: `tools/db_bench_tool.cc`**
|
||||
|
||||
```cpp
|
||||
// Flag definitions:
|
||||
DEFINE_uint64(
|
||||
super_block_alignment_size,
|
||||
ROCKSDB_NAMESPACE::BlockBasedTableOptions().super_block_alignment_size,
|
||||
"Configure super block size");
|
||||
|
||||
DEFINE_uint64(super_block_alignment_space_overhead_ratio,
|
||||
ROCKSDB_NAMESPACE::BlockBasedTableOptions()
|
||||
.super_block_alignment_space_overhead_ratio,
|
||||
"Configure space overhead for super block alignment");
|
||||
|
||||
// Apply to block_based_options (in the block where other options are set):
|
||||
block_based_options.super_block_alignment_size = FLAGS_super_block_alignment_size;
|
||||
block_based_options.super_block_alignment_space_overhead_ratio =
|
||||
FLAGS_super_block_alignment_space_overhead_ratio;
|
||||
```
|
||||
|
||||
### Step 7: Add Crash Test Support
|
||||
|
||||
**File: `tools/db_crashtest.py`**
|
||||
|
||||
```python
|
||||
"super_block_alignment_size": lambda: random.choice(
|
||||
[0, 128 * 1024, 512 * 1024, 2 * 1024 * 1024]
|
||||
),
|
||||
"super_block_alignment_space_overhead_ratio": lambda: random.choice([0, 32, 4096]),
|
||||
```
|
||||
|
||||
### Step 8: Add Java API Support
|
||||
|
||||
**File: `java/src/main/java/org/rocksdb/BlockBasedTableConfig.java`**
|
||||
|
||||
Add getter and setter methods:
|
||||
|
||||
```java
|
||||
/**
|
||||
* Get the super block alignment size.
|
||||
*
|
||||
* @return the super block alignment size.
|
||||
*/
|
||||
public long superBlockAlignmentSize() {
|
||||
return superBlockAlignmentSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the super block alignment size.
|
||||
* When set to 0, super block alignment is disabled.
|
||||
*
|
||||
* @param superBlockAlignmentSize the super block alignment size.
|
||||
*
|
||||
* @return the reference to the current option.
|
||||
*/
|
||||
public BlockBasedTableConfig setSuperBlockAlignmentSize(final long superBlockAlignmentSize) {
|
||||
this.superBlockAlignmentSize = superBlockAlignmentSize;
|
||||
return this;
|
||||
}
|
||||
```
|
||||
|
||||
Add member variable:
|
||||
|
||||
```java
|
||||
private long superBlockAlignmentSize;
|
||||
```
|
||||
|
||||
Update constructor and native method signature.
|
||||
|
||||
**File: `java/rocksjni/portal.h`**
|
||||
|
||||
Update `GetMethodID` signature and add fields to Java object construction.
|
||||
|
||||
**File: `java/rocksjni/table.cc`**
|
||||
|
||||
Add parameters to JNI function and apply to options.
|
||||
|
||||
**File: `java/src/test/java/org/rocksdb/BlockBasedTableConfigTest.java`**
|
||||
|
||||
Add unit tests:
|
||||
|
||||
```java
|
||||
@Test
|
||||
public void superBlockAlignmentSize() {
|
||||
final BlockBasedTableConfig blockBasedTableConfig = new BlockBasedTableConfig();
|
||||
blockBasedTableConfig.setSuperBlockAlignmentSize(1024 * 1024);
|
||||
assertThat(blockBasedTableConfig.superBlockAlignmentSize()).isEqualTo(1024 * 1024);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 3: Adding C API for Existing Option
|
||||
|
||||
Example reference: commit `429b36c22d76403d275dd0e6877b08d4cea2bc90` (block_align C API)
|
||||
|
||||
If an option already exists but needs C API support:
|
||||
|
||||
**File: `db/c.cc`**
|
||||
|
||||
```cpp
|
||||
void rocksdb_block_based_options_set_block_align(
|
||||
rocksdb_block_based_table_options_t* options, unsigned char v) {
|
||||
options->rep.block_align = v;
|
||||
}
|
||||
```
|
||||
|
||||
**File: `include/rocksdb/c.h`**
|
||||
|
||||
```cpp
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_block_based_options_set_block_align(
|
||||
rocksdb_block_based_table_options_t*, unsigned char);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Unit Testing Guidelines
|
||||
|
||||
### For Standard Options
|
||||
|
||||
Add tests in appropriate test files (e.g., `db/db_memtable_test.cc`, `db/db_options_test.cc`):
|
||||
|
||||
```cpp
|
||||
TEST_F(DBMemTableTest, YourOptionTest) {
|
||||
Options options;
|
||||
options.your_new_option = true;
|
||||
Reopen(options);
|
||||
// Test the behavior
|
||||
}
|
||||
```
|
||||
|
||||
### For BlockBasedTableOptions
|
||||
|
||||
Add tests in `db/db_flush_test.cc`, `table/block_based/block_based_table_reader_test.cc`, or `table/table_test.cc`:
|
||||
|
||||
```cpp
|
||||
TEST_P(DBFlushYourFeatureTest, YourFeature) {
|
||||
Options options;
|
||||
BlockBasedTableOptions block_options;
|
||||
block_options.your_new_option = some_value;
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(block_options));
|
||||
|
||||
ASSERT_OK(options.table_factory->ValidateOptions(
|
||||
DBOptions(options), ColumnFamilyOptions(options)));
|
||||
|
||||
Reopen(options);
|
||||
// Test the behavior
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Option Type Reference
|
||||
|
||||
Common option types used in serialization:
|
||||
|
||||
| OptionType | C++ Type | Example |
|
||||
|------------|----------|---------|
|
||||
| `kBoolean` | `bool` | `paranoid_memory_checks` |
|
||||
| `kInt` | `int` | `max_write_buffer_number` |
|
||||
| `kInt32T` | `int32_t` | `level0_file_num_compaction_trigger` |
|
||||
| `kUInt32T` | `uint32_t` | `memtable_protection_bytes_per_key` |
|
||||
| `kUInt64T` | `uint64_t` | `target_file_size_base` |
|
||||
| `kSizeT` | `size_t` | `block_size` |
|
||||
| `kDouble` | `double` | `compression_ratio` |
|
||||
| `kString` | `std::string` | `db_log_dir` |
|
||||
|
||||
---
|
||||
|
||||
## Checklist Summary
|
||||
|
||||
- [ ] Public header file with option definition and documentation
|
||||
- [ ] Internal options struct (MutableCFOptions or ImmutableCFOptions)
|
||||
- [ ] Options serialization/deserialization registration
|
||||
- [ ] Options logging in Dump() method
|
||||
- [ ] UpdateColumnFamilyOptions() for mutable options
|
||||
- [ ] options_settable_test.cc
|
||||
- [ ] db_stress_common.h (DECLARE)
|
||||
- [ ] db_stress_gflags.cc (DEFINE)
|
||||
- [ ] db_stress_test_base.cc (apply flag)
|
||||
- [ ] db_bench_tool.cc (DEFINE and apply)
|
||||
- [ ] db_crashtest.py (randomized values)
|
||||
- [ ] Unit tests
|
||||
- [ ] unreleased_history markdown file
|
||||
- [ ] Java API (for BlockBasedTableOptions)
|
||||
- [ ] C API (if needed)
|
||||
|
||||
@@ -0,0 +1,504 @@
|
||||
# RocksDB API Development Guide
|
||||
|
||||
This document provides guidance for adding new public APIs to RocksDB, following the established patterns used by existing APIs like `CompactRange`.
|
||||
|
||||
## API Layer Architecture
|
||||
|
||||
RocksDB exposes public APIs through multiple layers. Users can access RocksDB through any of the three public APIs: C++ headers, C headers, or Java bindings.
|
||||
|
||||
Here is an example for public header db.h:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Level 1: Public APIs (User Entry Points) │
|
||||
├───────────────────────┬─────────────────────────┬───────────────────────────┤
|
||||
│ C++ Public API │ C API Bindings │ Java/JNI API │
|
||||
│ include/rocksdb/db.h │ include/rocksdb/c.h │ java/src/.../RocksDB.java │
|
||||
│ include/rocksdb/*.h │ │ java/src/.../*.java │
|
||||
└───────────────────────┴────────────┬────────────┴───────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Level 2: C++ Implementation (Internal Core) │
|
||||
│ db/db_impl/db_impl*.cc, db/c.cc, java/rocksjni/*.cc │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Step-by-Step Guide: Adding a New Public API
|
||||
|
||||
### Step 1: Define the C++ Public Interface
|
||||
|
||||
**File:** `include/rocksdb/db.h`
|
||||
|
||||
Add the virtual method declaration in the `DB` class:
|
||||
|
||||
\`\`\`cpp
|
||||
// Pure virtual - must be implemented by DBImpl
|
||||
virtual Status YourNewAPI(const YourAPIOptions& options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
/* other params */) = 0;
|
||||
|
||||
// Convenience overload for default column family
|
||||
virtual Status YourNewAPI(const YourAPIOptions& options,
|
||||
/* other params */) {
|
||||
return YourNewAPI(options, DefaultColumnFamily(), /* other params */);
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
**Key Patterns:**
|
||||
- Use `Status` return type for error handling
|
||||
- Use `OptSlice` to avoid unnecessary levels of indirection and use of raw pointers.
|
||||
- Use `ColumnFamilyHandle*` for column family support
|
||||
- Provide convenience overloads for the default column family
|
||||
|
||||
### Step 2: Define Options Struct (If Needed)
|
||||
|
||||
**File:** `include/rocksdb/options.h`
|
||||
|
||||
If your API has multiple configuration options, define an options struct:
|
||||
|
||||
\`\`\`cpp
|
||||
struct YourAPIOptions {
|
||||
// Document each option with clear comments
|
||||
bool some_boolean_option = false;
|
||||
|
||||
// Default value explanation
|
||||
int some_int_option = -1;
|
||||
|
||||
// Pointer options require careful lifetime management
|
||||
std::atomic<bool>* canceled = nullptr;
|
||||
|
||||
// Enum options for multi-choice settings
|
||||
YourEnumType some_enum = YourEnumType::kDefault;
|
||||
};
|
||||
\`\`\`
|
||||
|
||||
**Key Patterns:**
|
||||
- Use sensible default values specified inline (e.g., `= false`, `= -1`)
|
||||
- Do NOT redundantly document the default value in comments; instead, document the rationale (why this default), historical context, and how different values are interpreted
|
||||
- Group related options logically
|
||||
- Consider thread-safety for pointer options
|
||||
|
||||
### Step 3: Implement in DBImpl
|
||||
|
||||
**Header:** `db/db_impl/db_impl.h`
|
||||
|
||||
\`\`\`cpp
|
||||
using DB::YourNewAPI;
|
||||
Status YourNewAPI(const YourAPIOptions& options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
/* other params */) override;
|
||||
|
||||
// Private internal implementation if needed
|
||||
Status YourNewAPIInternal(const YourAPIOptions& options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
/* other params */);
|
||||
\`\`\`
|
||||
|
||||
**Implementation:** `db/db_impl/db_impl_<category>.cc`
|
||||
|
||||
Choose the appropriate implementation file based on functionality:
|
||||
- `db_impl_compaction_flush.cc` - Compaction and flush operations
|
||||
- `db_impl_write.cc` - Write operations
|
||||
- `db_impl_open.cc` - DB opening/closing
|
||||
- `db_impl_files.cc` - File operations
|
||||
- `db_impl.cc` - General operations
|
||||
|
||||
\`\`\`cpp
|
||||
Status DBImpl::YourNewAPI(const YourAPIOptions& options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
/* other params */) {
|
||||
// 1. Input validation
|
||||
if (/* invalid input */) {
|
||||
return Status::InvalidArgument("Error message");
|
||||
}
|
||||
|
||||
// 2. Check for cancellation/abort conditions
|
||||
if (options.canceled && options.canceled->load(std::memory_order_acquire)) {
|
||||
return Status::Incomplete(Status::SubCode::kManualCompactionPaused);
|
||||
}
|
||||
|
||||
// 3. Get column family data
|
||||
auto cfh = static_cast<ColumnFamilyHandleImpl*>(column_family);
|
||||
auto cfd = cfh->cfd();
|
||||
|
||||
// 4. Core implementation logic
|
||||
// ...
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
### Step 4: Handle Special DB Types
|
||||
|
||||
**StackableDB (Wrapper DBs):**
|
||||
**File:** `include/rocksdb/utilities/stackable_db.h`
|
||||
|
||||
\`\`\`cpp
|
||||
using DB::YourNewAPI;
|
||||
Status YourNewAPI(const YourAPIOptions& options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
/* other params */) override {
|
||||
return db_->YourNewAPI(options, column_family, /* other params */);
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
**Secondary DB (Read-Only):**
|
||||
**File:** `db/db_impl/db_impl_secondary.h`
|
||||
|
||||
\`\`\`cpp
|
||||
using DBImpl::YourNewAPI;
|
||||
Status YourNewAPI(const YourAPIOptions& /*options*/,
|
||||
ColumnFamilyHandle* /*column_family*/,
|
||||
/* other params */) override {
|
||||
return Status::NotSupported("Not supported in secondary DB");
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
**CompactedDB (Read-Only):**
|
||||
**File:** `db/db_impl/compacted_db_impl.h`
|
||||
|
||||
\`\`\`cpp
|
||||
using DBImpl::YourNewAPI;
|
||||
Status YourNewAPI(const YourAPIOptions& /*options*/,
|
||||
ColumnFamilyHandle* /*column_family*/,
|
||||
/* other params */) override {
|
||||
return Status::NotSupported("Not supported for read-only DB");
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
### Step 5: Add C API Bindings
|
||||
|
||||
**Header:** `include/rocksdb/c.h`
|
||||
|
||||
\`\`\`c
|
||||
// Basic version
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_your_new_api(
|
||||
rocksdb_t* db,
|
||||
const char* start_key, size_t start_key_len,
|
||||
const char* limit_key, size_t limit_key_len);
|
||||
|
||||
// Column family version
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_your_new_api_cf(
|
||||
rocksdb_t* db, rocksdb_column_family_handle_t* column_family,
|
||||
const char* start_key, size_t start_key_len,
|
||||
const char* limit_key, size_t limit_key_len);
|
||||
|
||||
// With options and error handling
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_your_new_api_opt(
|
||||
rocksdb_t* db, rocksdb_your_api_options_t* opt,
|
||||
const char* start_key, size_t start_key_len,
|
||||
const char* limit_key, size_t limit_key_len,
|
||||
char** errptr);
|
||||
\`\`\`
|
||||
|
||||
**Implementation:** `db/c.cc`
|
||||
|
||||
\`\`\`cpp
|
||||
void rocksdb_your_new_api(rocksdb_t* db, const char* start_key,
|
||||
size_t start_key_len, const char* limit_key,
|
||||
size_t limit_key_len) {
|
||||
Slice a, b;
|
||||
db->rep->YourNewAPI(
|
||||
YourAPIOptions(), // Default options
|
||||
(start_key ? (a = Slice(start_key, start_key_len), &a) : nullptr),
|
||||
(limit_key ? (b = Slice(limit_key, limit_key_len), &b) : nullptr));
|
||||
}
|
||||
|
||||
void rocksdb_your_new_api_cf(rocksdb_t* db,
|
||||
rocksdb_column_family_handle_t* column_family,
|
||||
const char* start_key, size_t start_key_len,
|
||||
const char* limit_key, size_t limit_key_len) {
|
||||
Slice a, b;
|
||||
db->rep->YourNewAPI(
|
||||
YourAPIOptions(),
|
||||
column_family->rep,
|
||||
(start_key ? (a = Slice(start_key, start_key_len), &a) : nullptr),
|
||||
(limit_key ? (b = Slice(limit_key, limit_key_len), &b) : nullptr));
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
**If you have options, also add:**
|
||||
|
||||
\`\`\`cpp
|
||||
// Options struct wrapper
|
||||
struct rocksdb_your_api_options_t {
|
||||
YourAPIOptions rep;
|
||||
};
|
||||
|
||||
rocksdb_your_api_options_t* rocksdb_your_api_options_create() {
|
||||
return new rocksdb_your_api_options_t;
|
||||
}
|
||||
|
||||
void rocksdb_your_api_options_destroy(rocksdb_your_api_options_t* opt) {
|
||||
delete opt;
|
||||
}
|
||||
|
||||
void rocksdb_your_api_options_set_some_option(
|
||||
rocksdb_your_api_options_t* opt, unsigned char value) {
|
||||
opt->rep.some_boolean_option = value;
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
### Step 6: Add Java Bindings
|
||||
|
||||
**Java API:** `java/src/main/java/org/rocksdb/RocksDB.java`
|
||||
|
||||
\`\`\`java
|
||||
// Basic version
|
||||
public void yourNewAPI() throws RocksDBException {
|
||||
yourNewAPI(null);
|
||||
}
|
||||
|
||||
// Column family version
|
||||
public void yourNewAPI(ColumnFamilyHandle columnFamilyHandle)
|
||||
throws RocksDBException {
|
||||
yourNewAPI(nativeHandle_, null, -1, null, -1, 0,
|
||||
columnFamilyHandle == null ? 0 : columnFamilyHandle.nativeHandle_);
|
||||
}
|
||||
|
||||
// Range version
|
||||
public void yourNewAPI(final byte[] begin, final byte[] end)
|
||||
throws RocksDBException {
|
||||
yourNewAPI(null, begin, end);
|
||||
}
|
||||
|
||||
// Full-featured version with options
|
||||
public void yourNewAPI(ColumnFamilyHandle columnFamilyHandle,
|
||||
final byte[] begin, final byte[] end,
|
||||
final YourAPIOptions options)
|
||||
throws RocksDBException {
|
||||
yourNewAPI(nativeHandle_,
|
||||
begin, begin == null ? -1 : begin.length,
|
||||
end, end == null ? -1 : end.length,
|
||||
options.nativeHandle_,
|
||||
columnFamilyHandle == null ? 0 : columnFamilyHandle.nativeHandle_);
|
||||
}
|
||||
|
||||
// Native method declaration
|
||||
private static native void yourNewAPI(final long handle,
|
||||
/* @Nullable */ final byte[] begin, final int beginLen,
|
||||
/* @Nullable */ final byte[] end, final int endLen,
|
||||
final long optionsHandle,
|
||||
final long cfHandle);
|
||||
\`\`\`
|
||||
|
||||
**Options Class:** `java/src/main/java/org/rocksdb/YourAPIOptions.java`
|
||||
|
||||
\`\`\`java
|
||||
public class YourAPIOptions extends RocksObject {
|
||||
|
||||
public YourAPIOptions() {
|
||||
super(newYourAPIOptions());
|
||||
}
|
||||
|
||||
// Builder pattern setters
|
||||
public YourAPIOptions setSomeBooleanOption(boolean value) {
|
||||
setSomeBooleanOption(nativeHandle_, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Getters
|
||||
public boolean someBooleanOption() {
|
||||
return someBooleanOption(nativeHandle_);
|
||||
}
|
||||
|
||||
// Native method declarations
|
||||
private static native long newYourAPIOptions();
|
||||
private static native void disposeInternalJni(long handle);
|
||||
private static native void setSomeBooleanOption(long handle, boolean value);
|
||||
private static native boolean someBooleanOption(long handle);
|
||||
|
||||
@Override
|
||||
protected final void disposeInternal(final long handle) {
|
||||
disposeInternalJni(handle);
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
**JNI Implementation:** `java/rocksjni/rocksjni.cc`
|
||||
|
||||
\`\`\`cpp
|
||||
void Java_org_rocksdb_RocksDB_yourNewAPI(
|
||||
JNIEnv* env, jclass,
|
||||
jlong jdb_handle, jbyteArray jbegin, jint jbegin_len,
|
||||
jbyteArray jend, jint jend_len,
|
||||
jlong joptions_handle, jlong jcf_handle) {
|
||||
|
||||
// 1. Convert Java byte arrays to C++ strings
|
||||
jboolean has_exception = JNI_FALSE;
|
||||
std::string str_begin;
|
||||
if (jbegin_len > 0) {
|
||||
str_begin = ROCKSDB_NAMESPACE::JniUtil::byteString<std::string>(
|
||||
env, jbegin, jbegin_len,
|
||||
[](const char* str, const size_t len) { return std::string(str, len); },
|
||||
&has_exception);
|
||||
if (has_exception == JNI_TRUE) return;
|
||||
}
|
||||
|
||||
std::string str_end;
|
||||
if (jend_len > 0) {
|
||||
str_end = ROCKSDB_NAMESPACE::JniUtil::byteString<std::string>(
|
||||
env, jend, jend_len,
|
||||
[](const char* str, const size_t len) { return std::string(str, len); },
|
||||
&has_exception);
|
||||
if (has_exception == JNI_TRUE) return;
|
||||
}
|
||||
|
||||
// 2. Get or create options
|
||||
ROCKSDB_NAMESPACE::YourAPIOptions* options = nullptr;
|
||||
if (joptions_handle == 0) {
|
||||
options = new ROCKSDB_NAMESPACE::YourAPIOptions();
|
||||
} else {
|
||||
options = reinterpret_cast<ROCKSDB_NAMESPACE::YourAPIOptions*>(joptions_handle);
|
||||
}
|
||||
|
||||
// 3. Unwrap handles
|
||||
auto* db = reinterpret_cast<ROCKSDB_NAMESPACE::DB*>(jdb_handle);
|
||||
ROCKSDB_NAMESPACE::ColumnFamilyHandle* cf_handle =
|
||||
jcf_handle == 0 ? db->DefaultColumnFamily()
|
||||
: reinterpret_cast<ROCKSDB_NAMESPACE::ColumnFamilyHandle*>(jcf_handle);
|
||||
|
||||
// 4. Create Slices
|
||||
std::unique_ptr<ROCKSDB_NAMESPACE::Slice> begin;
|
||||
std::unique_ptr<ROCKSDB_NAMESPACE::Slice> end;
|
||||
if (jbegin_len > 0) begin.reset(new ROCKSDB_NAMESPACE::Slice(str_begin));
|
||||
if (jend_len > 0) end.reset(new ROCKSDB_NAMESPACE::Slice(str_end));
|
||||
|
||||
// 5. Call C++ API
|
||||
ROCKSDB_NAMESPACE::Status s = db->YourNewAPI(*options, cf_handle, begin.get(), end.get());
|
||||
|
||||
// 6. Cleanup if we created options
|
||||
if (joptions_handle == 0) delete options;
|
||||
|
||||
// 7. Throw Java exception on error
|
||||
ROCKSDB_NAMESPACE::RocksDBExceptionJni::ThrowNew(env, s);
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
**Options JNI:** `java/rocksjni/your_api_options.cc`
|
||||
|
||||
\`\`\`cpp
|
||||
jlong Java_org_rocksdb_YourAPIOptions_newYourAPIOptions(JNIEnv*, jclass) {
|
||||
auto* options = new ROCKSDB_NAMESPACE::YourAPIOptions();
|
||||
return GET_CPLUSPLUS_POINTER(options);
|
||||
}
|
||||
|
||||
void Java_org_rocksdb_YourAPIOptions_disposeInternalJni(JNIEnv*, jclass, jlong jhandle) {
|
||||
auto* options = reinterpret_cast<ROCKSDB_NAMESPACE::YourAPIOptions*>(jhandle);
|
||||
delete options;
|
||||
}
|
||||
|
||||
void Java_org_rocksdb_YourAPIOptions_setSomeBooleanOption(
|
||||
JNIEnv*, jclass, jlong jhandle, jboolean value) {
|
||||
auto* options = reinterpret_cast<ROCKSDB_NAMESPACE::YourAPIOptions*>(jhandle);
|
||||
options->some_boolean_option = static_cast<bool>(value);
|
||||
}
|
||||
|
||||
jboolean Java_org_rocksdb_YourAPIOptions_someBooleanOption(JNIEnv*, jclass, jlong jhandle) {
|
||||
auto* options = reinterpret_cast<ROCKSDB_NAMESPACE::YourAPIOptions*>(jhandle);
|
||||
return static_cast<jboolean>(options->some_boolean_option);
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
### Step 7: Update Build Files
|
||||
|
||||
**Java CMakeLists.txt:** `java/CMakeLists.txt`
|
||||
|
||||
Add your new Java source files:
|
||||
\`\`\`cmake
|
||||
src/main/java/org/rocksdb/YourAPIOptions.java
|
||||
src/test/java/org/rocksdb/YourAPIOptionsTest.java
|
||||
\`\`\`
|
||||
|
||||
### Step 8: Add Release Notes
|
||||
|
||||
**Directory:** `unreleased_history/`
|
||||
|
||||
RocksDB uses individual files in the `unreleased_history/` directory rather than directly editing `HISTORY.md`. This avoids merge conflicts and ensures changes are attributed to the correct release version.
|
||||
|
||||
Add a file to the appropriate subdirectory:
|
||||
- `unreleased_history/new_features/` - For new functionality
|
||||
- `unreleased_history/public_api_changes/` - For API changes
|
||||
- `unreleased_history/behavior_changes/` - For behavior modifications
|
||||
- `unreleased_history/bug_fixes/` - For bug fixes
|
||||
|
||||
**Example:** `unreleased_history/new_features/your_new_api.md`
|
||||
|
||||
\`\`\`markdown
|
||||
Added `YourNewAPI()` to support [describe functionality]. See `YourAPIOptions` for configuration.
|
||||
\`\`\`
|
||||
|
||||
**Example:** `unreleased_history/public_api_changes/your_api_options.md`
|
||||
|
||||
**Note:** Files should contain one line of markdown. The "* " prefix is automatically added if not included. These files are compiled into `HISTORY.md` during the release process.
|
||||
|
||||
### Step 9: Add Tests
|
||||
|
||||
**C++ Unit Tests:** `db/db_your_api_test.cc` or add to existing test file
|
||||
|
||||
\`\`\`cpp
|
||||
TEST_F(DBTest, YourNewAPIBasic) {
|
||||
Options options = CurrentOptions();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
// Setup test data
|
||||
ASSERT_OK(Put(1, "key1", "value1"));
|
||||
ASSERT_OK(Put(1, "key2", "value2"));
|
||||
|
||||
// Test your API
|
||||
YourAPIOptions api_options;
|
||||
api_options.some_boolean_option = true;
|
||||
ASSERT_OK(db_->YourNewAPI(api_options, handles_[1], nullptr, nullptr));
|
||||
|
||||
// Verify results
|
||||
// ...
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
**Java Tests:** `java/src/test/java/org/rocksdb/YourAPIOptionsTest.java`
|
||||
|
||||
\`\`\`java
|
||||
public class YourAPIOptionsTest {
|
||||
@Test
|
||||
public void yourAPIOptions() {
|
||||
try (final YourAPIOptions options = new YourAPIOptions()) {
|
||||
assertFalse(options.someBooleanOption());
|
||||
options.setSomeBooleanOption(true);
|
||||
assertTrue(options.someBooleanOption());
|
||||
}
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## File Summary Checklist
|
||||
|
||||
|
||||
| Component | File(s) | Required |
|
||||
|-----------|---------|----------|
|
||||
| C++ Public Interface | `include/rocksdb/db.h` | ✓ |
|
||||
| Options Struct | `include/rocksdb/options.h` | If needed |
|
||||
| DBImpl Declaration | `db/db_impl/db_impl.h` | ✓ |
|
||||
| DBImpl Implementation | `db/db_impl/db_impl_*.cc` | ✓ |
|
||||
| StackableDB | `include/rocksdb/utilities/stackable_db.h` | ✓ |
|
||||
| Secondary DB | `db/db_impl/db_impl_secondary.h` | If not supported |
|
||||
| Compacted DB | `db/db_impl/compacted_db_impl.h` | If not supported |
|
||||
| C API Header | `include/rocksdb/c.h` | ✓ |
|
||||
| C API Implementation | `db/c.cc` | ✓ |
|
||||
| Java API | `java/src/main/java/org/rocksdb/RocksDB.java` | ✓ |
|
||||
| Java Options | `java/src/main/java/org/rocksdb/YourAPIOptions.java` | If needed |
|
||||
| JNI Implementation | `java/rocksjni/rocksjni.cc` | ✓ |
|
||||
| JNI Options | `java/rocksjni/your_api_options.cc` | If needed |
|
||||
| Java CMake | `java/CMakeLists.txt` | If new files |
|
||||
| Changelog | `unreleased_history/*.md` | ✓ |
|
||||
| C++ Tests | `db/db_*_test.cc` | ✓ |
|
||||
| Java Tests | `java/src/test/java/org/rocksdb/*Test.java` | ✓ |
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Error Handling**: Always return `Status` objects in C++, throw exceptions in Java
|
||||
2. **Default Values**: Provide sensible defaults for all options
|
||||
3. **Documentation**: Add clear comments for all public methods and options
|
||||
4. **Column Family Support**: Always support column family operations
|
||||
5. **Thread Safety**: Document thread-safety guarantees
|
||||
6. **Backward Compatibility**: Avoid breaking existing API contracts
|
||||
7. **Testing**: Add comprehensive unit tests for all code paths
|
||||
@@ -0,0 +1,104 @@
|
||||
# Code Review Skill
|
||||
|
||||
This document defines the methodology for performing thorough, multi-perspective
|
||||
code reviews on RocksDB changes. It is used both by CI (GitHub Actions) and
|
||||
local review workflows.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting a review:
|
||||
- Read `CLAUDE.md` in the repository root for project-specific guidelines
|
||||
- Read any files in `claude_md/` for architecture context
|
||||
- Identify the commit/diff/PR to review and parse the changed files
|
||||
|
||||
## Review Methodology
|
||||
|
||||
Conduct the review from multiple perspectives, then synthesize findings into a
|
||||
single report. Each perspective catches different classes of bugs.
|
||||
|
||||
### Perspective 1: Codebase Context & Call-Chain Analysis
|
||||
|
||||
Before reviewing the diff itself, build deep context:
|
||||
- For each changed function/method, trace the call chain UPWARD 3-5 levels.
|
||||
Who consumes the changed behavior? What invariants do callers rely on?
|
||||
- Trace DOWNWARD into callees. What side effects do they have? What shared
|
||||
state do they mutate (counters, sequence numbers, flags, metadata)?
|
||||
- Identify the "sibling" or "reference" implementation that handles the same
|
||||
scenario the standard way. Compare behaviors.
|
||||
- For data written to shared structures (memtable, SST block, cache entry),
|
||||
find ALL readers and verify their visibility rules match the writer.
|
||||
|
||||
### Perspective 2: Correctness & Edge Cases
|
||||
|
||||
- Thread safety and concurrency (lock ordering, data races)
|
||||
- Error handling and Status propagation
|
||||
- Edge cases: empty inputs, overflow, boundary conditions
|
||||
- Data corruption scenarios
|
||||
- Behavioral contract changes: do return value semantics match callers?
|
||||
|
||||
### Perspective 3: Cross-Component & Adversarial Analysis
|
||||
|
||||
Check the change in ALL execution contexts:
|
||||
|
||||
| Context | Key difference |
|
||||
|---------|---------------|
|
||||
| WritePreparedTxnDB | read_callback_ controls visibility |
|
||||
| ReadOnly DB / SecondaryInstance | No mutable memtable |
|
||||
| CompactionService / Remote compaction | Different process |
|
||||
| User-defined timestamps | Extra key comparison dimension |
|
||||
| MemPurge | Memtable-to-memtable path |
|
||||
| BlobDB | Values in blob files, not inline |
|
||||
| Old snapshots | Seqno far behind current |
|
||||
| Concurrent writers | Lock-free vs locked paths |
|
||||
| FIFO / Universal compaction | Different invariants |
|
||||
| Prefix seek / total order seek | Different iterator behavior |
|
||||
|
||||
When the change claims a property ("logically redundant", "safe because X"),
|
||||
systematically try to break it: list preconditions, construct counterexamples.
|
||||
|
||||
### Perspective 4: Performance
|
||||
|
||||
RocksDB is a high-performance storage engine.
|
||||
- Memory allocation on hot paths
|
||||
- Unnecessary copies (prefer move semantics, Slice, string_view)
|
||||
- Cache efficiency and data locality
|
||||
- Loop optimization, branch prediction (LIKELY/UNLIKELY)
|
||||
|
||||
### Perspective 5: API, Compatibility & Testing
|
||||
|
||||
- Public API backwards compatibility
|
||||
- Serialization format correctness and versioning
|
||||
- Test coverage for edge cases, failure modes, and system-level interactions
|
||||
|
||||
## How to Explore the Codebase
|
||||
|
||||
Use available tools to explore deeply — do NOT just review the diff in isolation.
|
||||
The most critical bugs hide at component boundaries.
|
||||
|
||||
- Read the header files for changed implementations
|
||||
- Search for callers of changed functions (3-5 levels up)
|
||||
- Read existing tests to understand guaranteed behaviors
|
||||
- Check related implementations for conventions and patterns
|
||||
|
||||
## Output Format
|
||||
|
||||
### Summary
|
||||
Brief overall assessment (1-2 sentences).
|
||||
|
||||
### Issues Found
|
||||
Categorize by severity:
|
||||
- :red_circle: **Critical**: Must fix (correctness, data corruption, security)
|
||||
- :yellow_circle: **Suggestion**: Should consider (performance, edge cases)
|
||||
- :green_circle: **Nitpick**: Minor style issues
|
||||
|
||||
For each issue include:
|
||||
- File and line reference
|
||||
- Which review perspective found it
|
||||
- Description with root cause analysis
|
||||
- Suggested fix
|
||||
|
||||
### Cross-Component Analysis
|
||||
Execution context checks and assumption stress-test results.
|
||||
|
||||
### Positive Observations
|
||||
Good patterns, clever optimizations, or improvements.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Removing Deprecated Options from RocksDB
|
||||
### Keep in these files:
|
||||
- [ ] **KEEP** entry in type_info (`options/cf_options.cc` or `options/db_options.cc`) with `OptionVerificationType::kDeprecated` for loading old option files
|
||||
- [ ] **KEEP** or add test in `options/options_test.cc` `OptionsOldApiTest::GetOptionsFromMapTest` for loading old option files
|
||||
|
||||
### Documentation:
|
||||
- [ ] Add release note to `HISTORY.md` and `unreleased_history/`
|
||||
+5
-1
@@ -8,7 +8,7 @@ DB_STRESS_CMD?=./db_stress
|
||||
include common.mk
|
||||
|
||||
CRASHTEST_MAKE=$(MAKE) -f crash_test.mk
|
||||
CRASHTEST_PY=$(PYTHON) -u tools/db_crashtest.py --stress_cmd=$(DB_STRESS_CMD) --cleanup_cmd='$(DB_CLEANUP_CMD)'
|
||||
CRASHTEST_PY=$(PYTHON) -u tools/db_crashtest.py --stress_cmd=$(DB_STRESS_CMD) --cleanup_cmd='$(DB_CLEANUP_CMD)' --destroy_db_initially=1
|
||||
|
||||
.PHONY: crash_test crash_test_with_atomic_flush crash_test_with_txn \
|
||||
crash_test_with_wc_txn crash_test_with_wp_txn crash_test_with_wup_txn \
|
||||
@@ -34,6 +34,7 @@ CRASHTEST_PY=$(PYTHON) -u tools/db_crashtest.py --stress_cmd=$(DB_STRESS_CMD) --
|
||||
whitebox_crash_test_with_txn whitebox_crash_test_with_ts \
|
||||
whitebox_crash_test_with_optimistic_txn \
|
||||
whitebox_crash_test_with_tiered_storage \
|
||||
crash_test_db_cleanup \
|
||||
|
||||
crash_test: $(DB_STRESS_CMD)
|
||||
# Do not parallelize
|
||||
@@ -161,6 +162,9 @@ whitebox_crash_test_with_optimistic_txn: $(DB_STRESS_CMD)
|
||||
$(CRASHTEST_PY) --optimistic_txn whitebox --random_kill_odd \
|
||||
$(CRASH_TEST_KILL_ODD) $(CRASH_TEST_EXT_ARGS)
|
||||
|
||||
crash_test_db_cleanup: $(DB_STRESS_CMD)
|
||||
$(DB_STRESS_CMD) --delete_dir_and_exit=$(TEST_TMPDIR)
|
||||
|
||||
# Old names DEPRECATED
|
||||
crash_test_with_txn: crash_test_with_wc_txn
|
||||
whitebox_crash_test_with_txn: whitebox_crash_test_with_wc_txn
|
||||
|
||||
@@ -67,6 +67,16 @@ BlobFileBuilder::BlobFileBuilder(
|
||||
min_blob_size_(mutable_cf_options->min_blob_size),
|
||||
blob_file_size_(mutable_cf_options->blob_file_size),
|
||||
blob_compression_type_(mutable_cf_options->blob_compression_type),
|
||||
// TODO: support most CompressionOptions with a new CF option
|
||||
// blob_compression_opts
|
||||
// TODO with schema change: support custom compression manager and options
|
||||
// such as max_compressed_bytes_per_kb
|
||||
// NOTE: returns nullptr for kNoCompression
|
||||
blob_compressor_(GetBuiltinV2CompressionManager()->GetCompressor(
|
||||
CompressionOptions{}, blob_compression_type_)),
|
||||
blob_compressor_wa_(blob_compressor_
|
||||
? blob_compressor_->ObtainWorkingArea()
|
||||
: Compressor::ManagedWorkingArea{}),
|
||||
prepopulate_blob_cache_(mutable_cf_options->prepopulate_blob_cache),
|
||||
file_options_(file_options),
|
||||
write_options_(write_options),
|
||||
@@ -113,7 +123,7 @@ Status BlobFileBuilder::Add(const Slice& key, const Slice& value,
|
||||
}
|
||||
|
||||
Slice blob = value;
|
||||
std::string compressed_blob;
|
||||
GrowableBuffer compressed_blob;
|
||||
|
||||
{
|
||||
const Status s = CompressBlobIfNeeded(&blob, &compressed_blob);
|
||||
@@ -254,36 +264,27 @@ Status BlobFileBuilder::OpenBlobFileIfNeeded() {
|
||||
}
|
||||
|
||||
Status BlobFileBuilder::CompressBlobIfNeeded(
|
||||
Slice* blob, std::string* compressed_blob) const {
|
||||
Slice* blob, GrowableBuffer* compressed_blob) const {
|
||||
assert(blob);
|
||||
assert(compressed_blob);
|
||||
assert(compressed_blob->empty());
|
||||
assert(immutable_options_);
|
||||
|
||||
if (blob_compression_type_ == kNoCompression) {
|
||||
if (!blob_compressor_) {
|
||||
assert(blob_compression_type_ == kNoCompression);
|
||||
return Status::OK();
|
||||
}
|
||||
assert(blob_compression_type_ != kNoCompression);
|
||||
|
||||
// TODO: allow user CompressionOptions, including max_compressed_bytes_per_kb
|
||||
CompressionOptions opts;
|
||||
CompressionContext context(blob_compression_type_, opts);
|
||||
// WART: always stored as compressed even when that increases the size.
|
||||
|
||||
CompressionInfo info(opts, context, CompressionDict::GetEmptyDict(),
|
||||
blob_compression_type_);
|
||||
|
||||
constexpr uint32_t compression_format_version = 2;
|
||||
|
||||
bool success = false;
|
||||
|
||||
{
|
||||
StopWatch stop_watch(immutable_options_->clock, immutable_options_->stats,
|
||||
BLOB_DB_COMPRESSION_MICROS);
|
||||
success = OLD_CompressData(*blob, info, compression_format_version,
|
||||
compressed_blob);
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
return Status::Corruption("Error compressing blob");
|
||||
Status s;
|
||||
StopWatch stop_watch(immutable_options_->clock, immutable_options_->stats,
|
||||
BLOB_DB_COMPRESSION_MICROS);
|
||||
s = LegacyForceBuiltinCompression(*blob_compressor_, &blob_compressor_wa_,
|
||||
*blob, compressed_blob);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
*blob = Slice(*compressed_blob);
|
||||
|
||||
@@ -10,12 +10,14 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "rocksdb/advanced_compression.h"
|
||||
#include "rocksdb/advanced_options.h"
|
||||
#include "rocksdb/compression_type.h"
|
||||
#include "rocksdb/env.h"
|
||||
#include "rocksdb/options.h"
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
#include "rocksdb/types.h"
|
||||
#include "util/aligned_buffer.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
@@ -76,7 +78,8 @@ class BlobFileBuilder {
|
||||
private:
|
||||
bool IsBlobFileOpen() const;
|
||||
Status OpenBlobFileIfNeeded();
|
||||
Status CompressBlobIfNeeded(Slice* blob, std::string* compressed_blob) const;
|
||||
Status CompressBlobIfNeeded(Slice* blob,
|
||||
GrowableBuffer* compressed_blob) const;
|
||||
Status WriteBlobToFile(const Slice& key, const Slice& blob,
|
||||
uint64_t* blob_file_number, uint64_t* blob_offset);
|
||||
Status CloseBlobFile();
|
||||
@@ -91,6 +94,8 @@ class BlobFileBuilder {
|
||||
uint64_t min_blob_size_;
|
||||
uint64_t blob_file_size_;
|
||||
CompressionType blob_compression_type_;
|
||||
std::unique_ptr<Compressor> blob_compressor_;
|
||||
mutable Compressor::ManagedWorkingArea blob_compressor_wa_;
|
||||
PrepopulateBlobCache prepopulate_blob_cache_;
|
||||
const FileOptions* file_options_;
|
||||
const WriteOptions* write_options_;
|
||||
|
||||
@@ -403,22 +403,19 @@ TEST_F(BlobFileBuilderTest, Compression) {
|
||||
ASSERT_EQ(blob_file_addition.GetBlobFileNumber(), blob_file_number);
|
||||
ASSERT_EQ(blob_file_addition.GetTotalBlobCount(), 1);
|
||||
|
||||
CompressionOptions opts;
|
||||
CompressionContext context(kSnappyCompression, opts);
|
||||
|
||||
CompressionInfo info(opts, context, CompressionDict::GetEmptyDict(),
|
||||
kSnappyCompression);
|
||||
|
||||
std::string compressed_value;
|
||||
ASSERT_TRUE(Snappy_Compress(info, uncompressed_value.data(),
|
||||
uncompressed_value.size(), &compressed_value));
|
||||
auto compressor =
|
||||
GetBuiltinV2CompressionManager()->GetCompressor({}, kSnappyCompression);
|
||||
GrowableBuffer compressed_value;
|
||||
ASSERT_OK(LegacyForceBuiltinCompression(*compressor, /*working_area=*/nullptr,
|
||||
uncompressed_value,
|
||||
&compressed_value));
|
||||
|
||||
ASSERT_EQ(blob_file_addition.GetTotalBlobBytes(),
|
||||
BlobLogRecord::kHeaderSize + key_size + compressed_value.size());
|
||||
|
||||
// Verify the contents of the new blob file as well as the blob reference
|
||||
std::vector<std::pair<std::string, std::string>> expected_key_value_pairs{
|
||||
{key, compressed_value}};
|
||||
{key, compressed_value.AsSlice().ToString()}};
|
||||
std::vector<std::string> blob_indexes{blob_index};
|
||||
|
||||
VerifyBlobFile(blob_file_number, blob_file_path, column_family_id,
|
||||
@@ -457,11 +454,12 @@ TEST_F(BlobFileBuilderTest, CompressionError) {
|
||||
nullptr /*IOTracer*/, nullptr /*BlobFileCompletionCallback*/,
|
||||
BlobFileCreationReason::kFlush, &blob_file_paths, &blob_file_additions);
|
||||
|
||||
SyncPoint::GetInstance()->SetCallBack("CompressData:TamperWithReturnValue",
|
||||
[](void* arg) {
|
||||
bool* ret = static_cast<bool*>(arg);
|
||||
*ret = false;
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"LegacyForceBuiltinCompression:TamperWithStatus", [](void* arg) {
|
||||
Status* ret = static_cast<Status*>(arg);
|
||||
ASSERT_OK(*ret);
|
||||
*ret = Status::Corruption("Tampered result");
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
constexpr char key[] = "1";
|
||||
@@ -469,7 +467,7 @@ TEST_F(BlobFileBuilderTest, CompressionError) {
|
||||
|
||||
std::string blob_index;
|
||||
|
||||
ASSERT_TRUE(builder.Add(key, value, &blob_index).IsCorruption());
|
||||
ASSERT_EQ(builder.Add(key, value, &blob_index).code(), Status::kCorruption);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
+38
-26
@@ -17,10 +17,10 @@
|
||||
#include "rocksdb/file_system.h"
|
||||
#include "rocksdb/slice.h"
|
||||
#include "rocksdb/status.h"
|
||||
#include "table/format.h"
|
||||
#include "table/multiget_context.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "util/compression.h"
|
||||
#include "util/crc32c.h"
|
||||
#include "util/stop_watch.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -69,9 +69,16 @@ Status BlobFileReader::Create(
|
||||
}
|
||||
}
|
||||
|
||||
blob_file_reader->reset(
|
||||
new BlobFileReader(std::move(file_reader), file_size, compression_type,
|
||||
immutable_options.clock, statistics));
|
||||
std::shared_ptr<Decompressor> decompressor;
|
||||
if (compression_type != kNoCompression) {
|
||||
// The blob format has always used compression format 2
|
||||
decompressor = GetBuiltinV2CompressionManager()->GetDecompressorOptimizeFor(
|
||||
compression_type);
|
||||
}
|
||||
|
||||
blob_file_reader->reset(new BlobFileReader(
|
||||
std::move(file_reader), file_size, compression_type,
|
||||
std::move(decompressor), immutable_options.clock, statistics));
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
@@ -282,11 +289,13 @@ Status BlobFileReader::ReadFromFile(const RandomAccessFileReader* file_reader,
|
||||
|
||||
BlobFileReader::BlobFileReader(
|
||||
std::unique_ptr<RandomAccessFileReader>&& file_reader, uint64_t file_size,
|
||||
CompressionType compression_type, SystemClock* clock,
|
||||
CompressionType compression_type,
|
||||
std::shared_ptr<Decompressor> decompressor, SystemClock* clock,
|
||||
Statistics* statistics)
|
||||
: file_reader_(std::move(file_reader)),
|
||||
file_size_(file_size),
|
||||
compression_type_(compression_type),
|
||||
decompressor_(std::move(decompressor)),
|
||||
clock_(clock),
|
||||
statistics_(statistics) {
|
||||
assert(file_reader_);
|
||||
@@ -375,8 +384,9 @@ Status BlobFileReader::GetBlob(
|
||||
const Slice value_slice(record_slice.data() + adjustment, value_size);
|
||||
|
||||
{
|
||||
const Status s = UncompressBlobIfNeeded(
|
||||
value_slice, compression_type, allocator, clock_, statistics_, result);
|
||||
const Status s = UncompressBlobIfNeeded(value_slice, compression_type,
|
||||
decompressor_.get(), allocator,
|
||||
clock_, statistics_, result);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -523,10 +533,10 @@ void BlobFileReader::MultiGetBlob(
|
||||
}
|
||||
|
||||
// Uncompress blob if needed
|
||||
Slice value_slice(record_slice.data() + adjustments[i], req->len);
|
||||
*req->status =
|
||||
UncompressBlobIfNeeded(value_slice, compression_type_, allocator,
|
||||
clock_, statistics_, &blob_reqs[i].second);
|
||||
Slice value_slice(record_slice.data() + adjustments[j - 1], req->len);
|
||||
*req->status = UncompressBlobIfNeeded(
|
||||
value_slice, compression_type_, decompressor_.get(), allocator, clock_,
|
||||
statistics_, &blob_reqs[i].second);
|
||||
if (req->status->ok()) {
|
||||
total_bytes += record_slice.size();
|
||||
}
|
||||
@@ -583,8 +593,8 @@ Status BlobFileReader::VerifyBlob(const Slice& record_slice,
|
||||
|
||||
Status BlobFileReader::UncompressBlobIfNeeded(
|
||||
const Slice& value_slice, CompressionType compression_type,
|
||||
MemoryAllocator* allocator, SystemClock* clock, Statistics* statistics,
|
||||
std::unique_ptr<BlobContents>* result) {
|
||||
Decompressor* decompressor, MemoryAllocator* allocator, SystemClock* clock,
|
||||
Statistics* statistics, std::unique_ptr<BlobContents>* result) {
|
||||
assert(result);
|
||||
|
||||
if (compression_type == kNoCompression) {
|
||||
@@ -593,31 +603,33 @@ Status BlobFileReader::UncompressBlobIfNeeded(
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
UncompressionContext context(compression_type);
|
||||
UncompressionInfo info(context, UncompressionDict::GetEmptyDict(),
|
||||
compression_type);
|
||||
assert(decompressor);
|
||||
|
||||
size_t uncompressed_size = 0;
|
||||
constexpr uint32_t compression_format_version = 2;
|
||||
Decompressor::Args args;
|
||||
args.compression_type = compression_type;
|
||||
args.compressed_data = value_slice;
|
||||
|
||||
CacheAllocationPtr output;
|
||||
Status s = decompressor->ExtractUncompressedSize(args);
|
||||
if (!s.ok()) {
|
||||
return Status::Corruption(s.ToString());
|
||||
}
|
||||
|
||||
CacheAllocationPtr output = AllocateBlock(args.uncompressed_size, allocator);
|
||||
|
||||
{
|
||||
PERF_TIMER_GUARD(blob_decompress_time);
|
||||
StopWatch stop_watch(clock, statistics, BLOB_DB_DECOMPRESSION_MICROS);
|
||||
output = OLD_UncompressData(info, value_slice.data(), value_slice.size(),
|
||||
&uncompressed_size, compression_format_version,
|
||||
allocator);
|
||||
s = decompressor->DecompressBlock(args, output.get());
|
||||
}
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"BlobFileReader::UncompressBlobIfNeeded:TamperWithResult", &output);
|
||||
"BlobFileReader::UncompressBlobIfNeeded:TamperWithResult", &s);
|
||||
|
||||
if (!output) {
|
||||
return Status::Corruption("Unable to uncompress blob");
|
||||
if (!s.ok()) {
|
||||
return Status::Corruption(s.ToString());
|
||||
}
|
||||
|
||||
result->reset(new BlobContents(std::move(output), uncompressed_size));
|
||||
result->reset(new BlobContents(std::move(output), args.uncompressed_size));
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include "db/blob/blob_read_request.h"
|
||||
#include "file/random_access_file_reader.h"
|
||||
#include "rocksdb/advanced_compression.h"
|
||||
#include "rocksdb/compression_type.h"
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
#include "util/autovector.h"
|
||||
@@ -64,7 +65,8 @@ class BlobFileReader {
|
||||
private:
|
||||
BlobFileReader(std::unique_ptr<RandomAccessFileReader>&& file_reader,
|
||||
uint64_t file_size, CompressionType compression_type,
|
||||
SystemClock* clock, Statistics* statistics);
|
||||
std::shared_ptr<Decompressor> decompressor, SystemClock* clock,
|
||||
Statistics* statistics);
|
||||
|
||||
static Status OpenFile(const ImmutableOptions& immutable_options,
|
||||
const FileOptions& file_opts,
|
||||
@@ -96,6 +98,7 @@ class BlobFileReader {
|
||||
|
||||
static Status UncompressBlobIfNeeded(const Slice& value_slice,
|
||||
CompressionType compression_type,
|
||||
Decompressor* decompressor,
|
||||
MemoryAllocator* allocator,
|
||||
SystemClock* clock,
|
||||
Statistics* statistics,
|
||||
@@ -104,6 +107,7 @@ class BlobFileReader {
|
||||
std::unique_ptr<RandomAccessFileReader> file_reader_;
|
||||
uint64_t file_size_;
|
||||
CompressionType compression_type_;
|
||||
std::shared_ptr<Decompressor> decompressor_;
|
||||
SystemClock* clock_;
|
||||
Statistics* statistics_;
|
||||
};
|
||||
|
||||
@@ -65,7 +65,7 @@ void WriteBlobFile(const ImmutableOptions& immutable_options,
|
||||
|
||||
ASSERT_OK(blob_log_writer.WriteHeader(WriteOptions(), header));
|
||||
|
||||
std::vector<std::string> compressed_blobs(num);
|
||||
std::vector<GrowableBuffer> compressed_blobs(num);
|
||||
std::vector<Slice> blobs_to_write(num);
|
||||
if (kNoCompression == compression) {
|
||||
for (size_t i = 0; i < num; ++i) {
|
||||
@@ -73,16 +73,13 @@ void WriteBlobFile(const ImmutableOptions& immutable_options,
|
||||
blob_sizes[i] = blobs[i].size();
|
||||
}
|
||||
} else {
|
||||
CompressionOptions opts;
|
||||
CompressionContext context(compression, opts);
|
||||
CompressionInfo info(opts, context, CompressionDict::GetEmptyDict(),
|
||||
compression);
|
||||
|
||||
constexpr uint32_t compression_format_version = 2;
|
||||
auto compressor =
|
||||
GetBuiltinV2CompressionManager()->GetCompressor({}, compression);
|
||||
|
||||
for (size_t i = 0; i < num; ++i) {
|
||||
ASSERT_TRUE(OLD_CompressData(blobs[i], info, compression_format_version,
|
||||
&compressed_blobs[i]));
|
||||
ASSERT_OK(LegacyForceBuiltinCompression(*compressor,
|
||||
/*working_area=*/nullptr,
|
||||
blobs[i], &compressed_blobs[i]));
|
||||
blobs_to_write[i] = compressed_blobs[i];
|
||||
blob_sizes[i] = compressed_blobs[i].size();
|
||||
}
|
||||
@@ -809,11 +806,10 @@ TEST_F(BlobFileReaderTest, UncompressionError) {
|
||||
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"BlobFileReader::UncompressBlobIfNeeded:TamperWithResult", [](void* arg) {
|
||||
CacheAllocationPtr* const output =
|
||||
static_cast<CacheAllocationPtr*>(arg);
|
||||
assert(output);
|
||||
auto* result = static_cast<Status*>(arg);
|
||||
assert(result);
|
||||
|
||||
output->reset();
|
||||
*result = Status::Corruption("Injected result");
|
||||
});
|
||||
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
@@ -824,11 +820,12 @@ TEST_F(BlobFileReaderTest, UncompressionError) {
|
||||
std::unique_ptr<BlobContents> value;
|
||||
uint64_t bytes_read = 0;
|
||||
|
||||
ASSERT_TRUE(reader
|
||||
->GetBlob(ReadOptions(), key, blob_offset, blob_size,
|
||||
kSnappyCompression, prefetch_buffer, allocator,
|
||||
&value, &bytes_read)
|
||||
.IsCorruption());
|
||||
ASSERT_EQ(reader
|
||||
->GetBlob(ReadOptions(), key, blob_offset, blob_size,
|
||||
kSnappyCompression, prefetch_buffer, allocator,
|
||||
&value, &bytes_read)
|
||||
.code(),
|
||||
Status::Code::kCorruption);
|
||||
ASSERT_EQ(value, nullptr);
|
||||
ASSERT_EQ(bytes_read, 0);
|
||||
|
||||
@@ -1014,6 +1011,92 @@ TEST_P(BlobFileReaderDecodingErrorTest, DecodingError) {
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_F(BlobFileReaderTest, MultiGetBlobWithFailedValidation) {
|
||||
// Test that MultiGetBlob correctly handles the case where some requests
|
||||
// fail validation. The adjustments vector is only populated for requests
|
||||
// that pass validation, so the indices must track correctly.
|
||||
Options options;
|
||||
options.env = mock_env_.get();
|
||||
options.cf_paths.emplace_back(
|
||||
test::PerThreadDBPath(
|
||||
mock_env_.get(),
|
||||
"BlobFileReaderTest_MultiGetBlobWithFailedValidation"),
|
||||
0);
|
||||
options.enable_blob_files = true;
|
||||
|
||||
ImmutableOptions immutable_options(options);
|
||||
|
||||
constexpr uint32_t column_family_id = 1;
|
||||
constexpr bool has_ttl = false;
|
||||
constexpr ExpirationRange expiration_range;
|
||||
constexpr uint64_t blob_file_number = 1;
|
||||
constexpr size_t num_blobs = 3;
|
||||
const std::vector<std::string> key_strs = {"key1", "key2", "key3"};
|
||||
const std::vector<std::string> blob_strs = {"blob1", "blob2", "blob3"};
|
||||
|
||||
const std::vector<Slice> keys = {key_strs[0], key_strs[1], key_strs[2]};
|
||||
const std::vector<Slice> blobs = {blob_strs[0], blob_strs[1], blob_strs[2]};
|
||||
|
||||
std::vector<uint64_t> blob_offsets(keys.size());
|
||||
std::vector<uint64_t> blob_sizes(keys.size());
|
||||
|
||||
WriteBlobFile(immutable_options, column_family_id, has_ttl, expiration_range,
|
||||
expiration_range, blob_file_number, keys, blobs, kNoCompression,
|
||||
blob_offsets, blob_sizes);
|
||||
|
||||
constexpr HistogramImpl* blob_file_read_hist = nullptr;
|
||||
|
||||
std::unique_ptr<BlobFileReader> reader;
|
||||
|
||||
ReadOptions read_options;
|
||||
ASSERT_OK(BlobFileReader::Create(
|
||||
immutable_options, read_options, FileOptions(), column_family_id,
|
||||
blob_file_read_hist, blob_file_number, nullptr /*IOTracer*/, &reader));
|
||||
|
||||
// Enable checksum verification so adjustments are non-zero
|
||||
read_options.verify_checksums = true;
|
||||
constexpr MemoryAllocator* allocator = nullptr;
|
||||
|
||||
// Fail the first request by giving it an invalid offset (too small).
|
||||
// This causes the first request to be skipped in the adjustments vector,
|
||||
// so adjustments has only 2 entries (for blobs 1 and 2), while blob_reqs
|
||||
// has 3 entries. Without the fix, adjustments[i] would use the wrong
|
||||
// index for the remaining valid requests.
|
||||
std::array<Status, num_blobs> statuses_buf;
|
||||
std::array<BlobReadRequest, num_blobs> requests_buf;
|
||||
autovector<std::pair<BlobReadRequest*, std::unique_ptr<BlobContents>>>
|
||||
blob_reqs;
|
||||
|
||||
// First request: invalid offset (too small, will fail validation)
|
||||
requests_buf[0] = BlobReadRequest(keys[0], /*offset=*/0, blob_sizes[0],
|
||||
kNoCompression, nullptr, &statuses_buf[0]);
|
||||
// Second and third requests: valid
|
||||
requests_buf[1] = BlobReadRequest(keys[1], blob_offsets[1], blob_sizes[1],
|
||||
kNoCompression, nullptr, &statuses_buf[1]);
|
||||
requests_buf[2] = BlobReadRequest(keys[2], blob_offsets[2], blob_sizes[2],
|
||||
kNoCompression, nullptr, &statuses_buf[2]);
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
blob_reqs.emplace_back(&requests_buf[i], std::unique_ptr<BlobContents>());
|
||||
}
|
||||
|
||||
uint64_t bytes_read = 0;
|
||||
reader->MultiGetBlob(read_options, allocator, blob_reqs, &bytes_read);
|
||||
|
||||
// First request should fail validation
|
||||
ASSERT_TRUE(statuses_buf[0].IsCorruption());
|
||||
ASSERT_EQ(blob_reqs[0].second, nullptr);
|
||||
|
||||
// Second and third requests should succeed with correct data
|
||||
ASSERT_OK(statuses_buf[1]);
|
||||
ASSERT_NE(blob_reqs[1].second, nullptr);
|
||||
ASSERT_EQ(blob_reqs[1].second->data(), blobs[1]);
|
||||
|
||||
ASSERT_OK(statuses_buf[2]);
|
||||
ASSERT_NE(blob_reqs[2].second, nullptr);
|
||||
ASSERT_EQ(blob_reqs[2].second->data(), blobs[2]);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -137,6 +137,18 @@ class BlobIndex {
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
// Encode this blob index into dst based on its type.
|
||||
void EncodeTo(std::string* dst) const {
|
||||
if (IsInlined()) {
|
||||
EncodeInlinedTTL(dst, expiration_, value_);
|
||||
} else if (HasTTL()) {
|
||||
EncodeBlobTTL(dst, expiration_, file_number_, offset_, size_,
|
||||
compression_);
|
||||
} else {
|
||||
EncodeBlob(dst, file_number_, offset_, size_, compression_);
|
||||
}
|
||||
}
|
||||
|
||||
static void EncodeInlinedTTL(std::string* dst, uint64_t expiration,
|
||||
const Slice& value) {
|
||||
assert(dst != nullptr);
|
||||
|
||||
@@ -67,7 +67,7 @@ void WriteBlobFile(const ImmutableOptions& immutable_options,
|
||||
|
||||
ASSERT_OK(blob_log_writer.WriteHeader(WriteOptions(), header));
|
||||
|
||||
std::vector<std::string> compressed_blobs(num);
|
||||
std::vector<GrowableBuffer> compressed_blobs(num);
|
||||
std::vector<Slice> blobs_to_write(num);
|
||||
if (kNoCompression == compression) {
|
||||
for (size_t i = 0; i < num; ++i) {
|
||||
@@ -75,16 +75,13 @@ void WriteBlobFile(const ImmutableOptions& immutable_options,
|
||||
blob_sizes[i] = blobs[i].size();
|
||||
}
|
||||
} else {
|
||||
CompressionOptions opts;
|
||||
CompressionContext context(compression, opts);
|
||||
CompressionInfo info(opts, context, CompressionDict::GetEmptyDict(),
|
||||
compression);
|
||||
|
||||
constexpr uint32_t compression_format_version = 2;
|
||||
auto compressor =
|
||||
GetBuiltinV2CompressionManager()->GetCompressor({}, compression);
|
||||
|
||||
for (size_t i = 0; i < num; ++i) {
|
||||
ASSERT_TRUE(OLD_CompressData(blobs[i], info, compression_format_version,
|
||||
&compressed_blobs[i]));
|
||||
ASSERT_OK(LegacyForceBuiltinCompression(*compressor,
|
||||
/*working_area=*/nullptr,
|
||||
blobs[i], &compressed_blobs[i]));
|
||||
blobs_to_write[i] = compressed_blobs[i];
|
||||
blob_sizes[i] = compressed_blobs[i].size();
|
||||
}
|
||||
|
||||
@@ -56,6 +56,18 @@ TableBuilder* NewTableBuilder(const TableBuilderOptions& tboptions,
|
||||
return tboptions.moptions.table_factory->NewTableBuilder(tboptions, file);
|
||||
}
|
||||
|
||||
void ExtractTimestampFromTableProperties(const TableProperties& tp,
|
||||
FileMetaData* meta) {
|
||||
auto min_ts_iter = tp.user_collected_properties.find("rocksdb.timestamp_min");
|
||||
if (min_ts_iter != tp.user_collected_properties.end()) {
|
||||
meta->min_timestamp = min_ts_iter->second;
|
||||
}
|
||||
auto max_ts_iter = tp.user_collected_properties.find("rocksdb.timestamp_max");
|
||||
if (max_ts_iter != tp.user_collected_properties.end()) {
|
||||
meta->max_timestamp = max_ts_iter->second;
|
||||
}
|
||||
}
|
||||
|
||||
Status BuildTable(
|
||||
const std::string& dbname, VersionSet* versions,
|
||||
const ImmutableDBOptions& db_options, const TableBuilderOptions& tboptions,
|
||||
@@ -310,6 +322,8 @@ Status BuildTable(
|
||||
}
|
||||
|
||||
TEST_SYNC_POINT("BuildTable:BeforeFinishBuildTable");
|
||||
TEST_SYNC_POINT_CALLBACK("BuildTable:BeforeCheckEmpty",
|
||||
static_cast<TableBuilder*>(builder));
|
||||
const bool empty = builder->IsEmpty();
|
||||
if (flush_stats) {
|
||||
assert(c_iter.HasNumInputEntryScanned());
|
||||
@@ -318,6 +332,14 @@ Status BuildTable(
|
||||
}
|
||||
if (!s.ok() || empty) {
|
||||
builder->Abandon();
|
||||
// Propagate the builder's error when the builder is empty due to an
|
||||
// internal error (e.g., write fault injection causing all Add() calls to
|
||||
// return early). Without this, `s` would remain OK and the downstream
|
||||
// key count validation in flush_job.cc would produce a misleading
|
||||
// Corruption error instead of the actual builder error.
|
||||
if (s.ok() && !builder->status().ok()) {
|
||||
s = builder->status();
|
||||
}
|
||||
} else {
|
||||
SeqnoToTimeMapping relevant_mapping;
|
||||
if (seqno_to_time_mapping) {
|
||||
@@ -355,6 +377,7 @@ Status BuildTable(
|
||||
assert(meta->fd.GetFileSize() > 0);
|
||||
tp = builder
|
||||
->GetTableProperties(); // refresh now that builder is finished
|
||||
ExtractTimestampFromTableProperties(tp, meta);
|
||||
if (memtable_payload_bytes != nullptr &&
|
||||
memtable_garbage_bytes != nullptr) {
|
||||
const CompactionIterationStats& ci_stats = c_iter.iter_stats();
|
||||
|
||||
@@ -41,6 +41,13 @@ class BlobFileCompletionCallback;
|
||||
TableBuilder* NewTableBuilder(const TableBuilderOptions& tboptions,
|
||||
WritableFileWriter* file);
|
||||
|
||||
// Extract min/max timestamps from table properties and populate FileMetaData.
|
||||
// This is used by both flush (BuildTable) and compaction (CompactionOutputs)
|
||||
// to populate timestamp range in FileMetaData from the TimestampTableProperties
|
||||
// collector output.
|
||||
void ExtractTimestampFromTableProperties(const TableProperties& tp,
|
||||
FileMetaData* meta);
|
||||
|
||||
// Build a Table file from the contents of *iter. The generated file
|
||||
// will be named according to number specified in meta. On success, the rest of
|
||||
// *meta will be filled with metadata about the generated table.
|
||||
|
||||
+672
-1
@@ -722,6 +722,88 @@ static void LoadAndCheckLatestOptions(const char* db_name, rocksdb_env_t* env,
|
||||
num_column_families);
|
||||
}
|
||||
|
||||
// Global state for tracking remote compaction calls
|
||||
typedef struct {
|
||||
int schedule_called;
|
||||
int wait_called;
|
||||
int cancel_called;
|
||||
char last_scheduled_job_id[256];
|
||||
char last_db_name[256];
|
||||
} RemoteCompactionState;
|
||||
|
||||
// Schedule callback - gets called when compaction is scheduled
|
||||
static rocksdb_compactionservice_scheduleresponse_t* RemoteCompactionSchedule(
|
||||
void* state, const rocksdb_compactionservice_jobinfo_t* info,
|
||||
const char* input, size_t input_len) {
|
||||
(void)input;
|
||||
(void)input_len;
|
||||
RemoteCompactionState* rcs = (RemoteCompactionState*)state;
|
||||
rcs->schedule_called++;
|
||||
|
||||
// Extract job info
|
||||
size_t db_name_len;
|
||||
const char* db_name =
|
||||
rocksdb_compactionservice_jobinfo_t_get_db_name(info, &db_name_len);
|
||||
memcpy(rcs->last_db_name, db_name, db_name_len);
|
||||
rcs->last_db_name[db_name_len] = '\0';
|
||||
|
||||
// Generate a job ID
|
||||
snprintf(rcs->last_scheduled_job_id, sizeof(rcs->last_scheduled_job_id),
|
||||
"job-%d", rcs->schedule_called);
|
||||
|
||||
// Create response with success status
|
||||
char* err = NULL;
|
||||
rocksdb_compactionservice_scheduleresponse_t* response =
|
||||
rocksdb_compactionservice_scheduleresponse_create(
|
||||
rcs->last_scheduled_job_id,
|
||||
rocksdb_compactionservice_jobstatus_success, &err);
|
||||
if (err) {
|
||||
free(err);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// Wait callback - simulates waiting for remote compaction to complete
|
||||
static int RemoteCompactionWait(void* state, const char* scheduled_job_id,
|
||||
char** result, size_t* result_len) {
|
||||
RemoteCompactionState* rcs = (RemoteCompactionState*)state;
|
||||
rcs->wait_called++;
|
||||
|
||||
if (strcmp(scheduled_job_id, rcs->last_scheduled_job_id) != 0) {
|
||||
return rocksdb_compactionservice_jobstatus_failure;
|
||||
}
|
||||
|
||||
// For testing purposes, return kUseLocal to cause RocksDB to fall back to
|
||||
// local compaction. This tests the callback mechanism without needing a fully
|
||||
// serialized result. In a real scenario, this would communicate with a remote
|
||||
// worker that calls rocksdb_open_and_compact() and returns a properly
|
||||
// serialized CompactionServiceResult
|
||||
*result = NULL;
|
||||
*result_len = 0;
|
||||
|
||||
return rocksdb_compactionservice_jobstatus_use_local;
|
||||
}
|
||||
|
||||
// Cancel callback - cancels pending jobs
|
||||
static void RemoteCompactionCancel(void* state) {
|
||||
RemoteCompactionState* rcs = (RemoteCompactionState*)state;
|
||||
rcs->cancel_called++;
|
||||
}
|
||||
|
||||
// Destructor callback
|
||||
static void RemoteCompactionDestroy(void* state) { (void)state; }
|
||||
|
||||
// NULL schedule callback for testing failure handling
|
||||
static rocksdb_compactionservice_scheduleresponse_t* NullSchedule(
|
||||
void* state, const rocksdb_compactionservice_jobinfo_t* info,
|
||||
const char* input, size_t input_len) {
|
||||
(void)state;
|
||||
(void)info;
|
||||
(void)input;
|
||||
(void)input_len;
|
||||
return NULL; // Return NULL to simulate failure
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
@@ -1255,6 +1337,70 @@ int main(int argc, char** argv) {
|
||||
rocksdb_writebatch_destroy(wb);
|
||||
}
|
||||
|
||||
StartPhase("writebatch_vectors_cf");
|
||||
{
|
||||
const char* cf_name = "wb_vectors_cf";
|
||||
rocksdb_column_family_handle_t* wb_cf =
|
||||
rocksdb_create_column_family(db, options, cf_name, &err);
|
||||
CheckNoError(err);
|
||||
|
||||
rocksdb_writebatch_t* wb = rocksdb_writebatch_create();
|
||||
|
||||
// Test putv_cf: concatenates multiple slices into a single key/value
|
||||
const char* put_keys[2] = {"k", "ey"};
|
||||
const size_t put_key_sizes[2] = {1, 2};
|
||||
const char* put_vals[3] = {"v", "a", "l"};
|
||||
const size_t put_val_sizes[3] = {1, 1, 1};
|
||||
rocksdb_writebatch_putv_cf(wb, wb_cf, 2, put_keys, put_key_sizes, 3,
|
||||
put_vals, put_val_sizes);
|
||||
rocksdb_write(db, woptions, wb, &err);
|
||||
CheckNoError(err);
|
||||
// putv_cf concatenates: key="k"+"ey"="key", value="v"+"a"+"l"="val"
|
||||
CheckGetCF(db, roptions, wb_cf, "key", "val");
|
||||
CheckGetCF(db, roptions, wb_cf, "k", NULL);
|
||||
CheckGetCF(db, roptions, wb_cf, "ey", NULL);
|
||||
|
||||
// Test deletev_cf: concatenates multiple slices for key
|
||||
rocksdb_writebatch_clear(wb);
|
||||
const char* del_keys[2] = {"k", "ey"};
|
||||
const size_t del_key_sizes[2] = {1, 2};
|
||||
rocksdb_writebatch_deletev_cf(wb, wb_cf, 2, del_keys, del_key_sizes);
|
||||
rocksdb_write(db, woptions, wb, &err);
|
||||
CheckNoError(err);
|
||||
CheckGetCF(db, roptions, wb_cf, "key", NULL);
|
||||
|
||||
// Test delete_rangev_cf: concatenates slices for range deletion
|
||||
rocksdb_writebatch_clear(wb);
|
||||
rocksdb_writebatch_put_cf(wb, wb_cf, "a", 1, "1", 1);
|
||||
rocksdb_writebatch_put_cf(wb, wb_cf, "b", 1, "2", 1);
|
||||
rocksdb_writebatch_put_cf(wb, wb_cf, "c", 1, "3", 1);
|
||||
rocksdb_write(db, woptions, wb, &err);
|
||||
CheckNoError(err);
|
||||
CheckGetCF(db, roptions, wb_cf, "a", "1");
|
||||
CheckGetCF(db, roptions, wb_cf, "b", "2");
|
||||
CheckGetCF(db, roptions, wb_cf, "c", "3");
|
||||
|
||||
rocksdb_writebatch_clear(wb);
|
||||
const char* range_start[2] = {"a", ""}; // "a" + "" = "a"
|
||||
const size_t range_start_sizes[2] = {1, 0};
|
||||
const char* range_end[2] = {"c", ""};
|
||||
const size_t range_end_sizes[2] = {1, 0};
|
||||
rocksdb_writebatch_delete_rangev_cf(wb, wb_cf, 2, range_start,
|
||||
range_start_sizes, range_end,
|
||||
range_end_sizes);
|
||||
rocksdb_write(db, woptions, wb, &err);
|
||||
CheckNoError(err);
|
||||
// Range [a, c) should delete "a" and "b", but not "c"
|
||||
CheckGetCF(db, roptions, wb_cf, "a", NULL);
|
||||
CheckGetCF(db, roptions, wb_cf, "b", NULL);
|
||||
CheckGetCF(db, roptions, wb_cf, "c", "3");
|
||||
|
||||
rocksdb_writebatch_destroy(wb);
|
||||
rocksdb_drop_column_family(db, wb_cf, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_column_family_handle_destroy(wb_cf);
|
||||
}
|
||||
|
||||
StartPhase("writebatch_vectors");
|
||||
{
|
||||
rocksdb_writebatch_t* wb = rocksdb_writebatch_create();
|
||||
@@ -1420,6 +1566,43 @@ int main(int argc, char** argv) {
|
||||
rocksdb_iter_destroy(iter);
|
||||
}
|
||||
|
||||
StartPhase("iter_slice");
|
||||
{
|
||||
// Test the new slice-based iterator API for better performance
|
||||
rocksdb_iterator_t* iter = rocksdb_create_iterator(db, roptions);
|
||||
CheckCondition(!rocksdb_iter_valid(iter));
|
||||
rocksdb_iter_seek_to_first(iter);
|
||||
CheckCondition(rocksdb_iter_valid(iter));
|
||||
|
||||
// Test rocksdb_iter_key_slice
|
||||
rocksdb_slice_t key_slice = rocksdb_iter_key_slice(iter);
|
||||
CheckEqual("box", key_slice.data, key_slice.size);
|
||||
|
||||
// Test rocksdb_iter_value_slice
|
||||
rocksdb_slice_t value_slice = rocksdb_iter_value_slice(iter);
|
||||
CheckEqual("c", value_slice.data, value_slice.size);
|
||||
|
||||
// Move to next entry and test again
|
||||
rocksdb_iter_next(iter);
|
||||
CheckCondition(rocksdb_iter_valid(iter));
|
||||
key_slice = rocksdb_iter_key_slice(iter);
|
||||
value_slice = rocksdb_iter_value_slice(iter);
|
||||
CheckEqual("foo", key_slice.data, key_slice.size);
|
||||
CheckEqual("hello", value_slice.data, value_slice.size);
|
||||
|
||||
// Test seeking with slice API
|
||||
rocksdb_iter_seek(iter, "b", 1);
|
||||
CheckCondition(rocksdb_iter_valid(iter));
|
||||
key_slice = rocksdb_iter_key_slice(iter);
|
||||
value_slice = rocksdb_iter_value_slice(iter);
|
||||
CheckEqual("box", key_slice.data, key_slice.size);
|
||||
CheckEqual("c", value_slice.data, value_slice.size);
|
||||
|
||||
rocksdb_iter_get_error(iter, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_iter_destroy(iter);
|
||||
}
|
||||
|
||||
StartPhase("wbwi_iter");
|
||||
{
|
||||
rocksdb_iterator_t* base_iter = rocksdb_create_iterator(db, roptions);
|
||||
@@ -1505,6 +1688,53 @@ int main(int argc, char** argv) {
|
||||
CheckMultiGetValues(3, vals, vals_sizes, errs, expected);
|
||||
}
|
||||
|
||||
StartPhase("zero_copy_get_pinned_v2");
|
||||
{
|
||||
// Test new zero-copy get functions
|
||||
|
||||
// Test rocksdb_get_pinned_v2
|
||||
rocksdb_pinnable_handle_t* handle =
|
||||
rocksdb_get_pinned_v2(db, roptions, "foo", 3, &err);
|
||||
CheckNoError(err);
|
||||
CheckCondition(handle != NULL);
|
||||
size_t val_len;
|
||||
const char* val = rocksdb_pinnable_handle_get_value(handle, &val_len);
|
||||
CheckEqual("hello", val, val_len);
|
||||
rocksdb_pinnable_handle_destroy(handle);
|
||||
|
||||
// Test with non-existent key
|
||||
handle = rocksdb_get_pinned_v2(db, roptions, "notfound", 8, &err);
|
||||
CheckNoError(err);
|
||||
CheckCondition(handle == NULL);
|
||||
|
||||
// Test rocksdb_get_into_buffer
|
||||
char buffer[100];
|
||||
unsigned char found;
|
||||
unsigned char success = rocksdb_get_into_buffer(
|
||||
db, roptions, "foo", 3, buffer, sizeof(buffer), &val_len, &found, &err);
|
||||
CheckNoError(err);
|
||||
CheckCondition(success == 1);
|
||||
CheckCondition(found == 1);
|
||||
CheckCondition(val_len == 5);
|
||||
CheckCondition(memcmp(buffer, "hello", 5) == 0);
|
||||
|
||||
// Test with buffer too small
|
||||
success = rocksdb_get_into_buffer(db, roptions, "foo", 3, buffer,
|
||||
2, // Buffer too small
|
||||
&val_len, &found, &err);
|
||||
CheckNoError(err);
|
||||
CheckCondition(success == 0); // Should fail due to small buffer
|
||||
CheckCondition(found == 1);
|
||||
CheckCondition(val_len == 5); // Should still report actual size
|
||||
|
||||
// Test with non-existent key
|
||||
success = rocksdb_get_into_buffer(db, roptions, "notfound", 8, buffer,
|
||||
sizeof(buffer), &val_len, &found, &err);
|
||||
CheckNoError(err);
|
||||
CheckCondition(success == 0);
|
||||
CheckCondition(found == 0);
|
||||
}
|
||||
|
||||
StartPhase("pin_get");
|
||||
{
|
||||
CheckPinGet(db, roptions, "box", "c");
|
||||
@@ -1922,6 +2152,55 @@ int main(int argc, char** argv) {
|
||||
rocksdb_flush_wal(db, 1, &err);
|
||||
CheckNoError(err);
|
||||
|
||||
// Test column family handle get name
|
||||
{
|
||||
size_t name_len;
|
||||
char* cf_name =
|
||||
rocksdb_column_family_handle_get_name(handles[1], &name_len);
|
||||
CheckCondition(name_len == 3);
|
||||
CheckCondition(memcmp(cf_name, "cf1", 3) == 0);
|
||||
rocksdb_free(cf_name);
|
||||
}
|
||||
|
||||
// Test zero-copy get with column families
|
||||
{
|
||||
rocksdb_pinnable_handle_t* handle =
|
||||
rocksdb_get_pinned_cf_v2(db, roptions, handles[1], "box", 3, &err);
|
||||
CheckNoError(err);
|
||||
CheckCondition(handle != NULL);
|
||||
size_t val_len;
|
||||
const char* val = rocksdb_pinnable_handle_get_value(handle, &val_len);
|
||||
CheckEqual("c", val, val_len);
|
||||
rocksdb_pinnable_handle_destroy(handle);
|
||||
|
||||
// Test with non-existent key
|
||||
handle = rocksdb_get_pinned_cf_v2(db, roptions, handles[1], "notfound", 8,
|
||||
&err);
|
||||
CheckNoError(err);
|
||||
CheckCondition(handle == NULL);
|
||||
|
||||
// Test rocksdb_get_into_buffer_cf
|
||||
char buffer[100];
|
||||
unsigned char found;
|
||||
unsigned char success = rocksdb_get_into_buffer_cf(
|
||||
db, roptions, handles[1], "buff", 4, buffer, sizeof(buffer), &val_len,
|
||||
&found, &err);
|
||||
CheckNoError(err);
|
||||
CheckCondition(success == 1);
|
||||
CheckCondition(found == 1);
|
||||
CheckCondition(val_len == 7);
|
||||
CheckCondition(memcmp(buffer, "rocksdb", 7) == 0);
|
||||
|
||||
// Test with buffer too small
|
||||
success = rocksdb_get_into_buffer_cf(db, roptions, handles[1], "buff", 4,
|
||||
buffer, 3, // Buffer too small
|
||||
&val_len, &found, &err);
|
||||
CheckNoError(err);
|
||||
CheckCondition(success == 0); // Should fail due to small buffer
|
||||
CheckCondition(found == 1);
|
||||
CheckCondition(val_len == 7); // Should still report actual size
|
||||
}
|
||||
|
||||
// Test WriteBatchWithIndex iteration with Column Family
|
||||
rocksdb_writebatch_wi_t* wbwi = rocksdb_writebatch_wi_create(0, true);
|
||||
rocksdb_writebatch_wi_put_cf(wbwi, handles[1], "boat", 4, "row",
|
||||
@@ -1998,6 +2277,74 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// Test rocksdb_batched_multi_get_cf_slice for better performance
|
||||
// Build rocksdb_slice_t array directly to avoid conversion overhead
|
||||
rocksdb_slice_t batched_key_slices[4];
|
||||
batched_key_slices[0].data = "box";
|
||||
batched_key_slices[0].size = 3;
|
||||
batched_key_slices[1].data = "buff";
|
||||
batched_key_slices[1].size = 4;
|
||||
batched_key_slices[2].data = "barfooxx";
|
||||
batched_key_slices[2].size = 8;
|
||||
batched_key_slices[3].data = "box";
|
||||
batched_key_slices[3].size = 3;
|
||||
|
||||
const char* expected_value[4] = {"c", "rocksdb", NULL, "c"};
|
||||
char* batched_errs[4];
|
||||
rocksdb_pinnableslice_t* pvals[4];
|
||||
|
||||
rocksdb_batched_multi_get_cf_slice(db, roptions, handles[1], 4,
|
||||
batched_key_slices, pvals,
|
||||
batched_errs, false);
|
||||
|
||||
const char* val;
|
||||
size_t val_len;
|
||||
for (i = 0; i < 4; ++i) {
|
||||
CheckNoError(batched_errs[i]);
|
||||
if (pvals[i] != NULL) {
|
||||
val = rocksdb_pinnableslice_value(pvals[i], &val_len);
|
||||
CheckEqual(expected_value[i], val, val_len);
|
||||
rocksdb_pinnableslice_destroy(pvals[i]);
|
||||
} else {
|
||||
CheckEqual(expected_value[i], NULL, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// Test rocksdb_batched_multi_get_cf_slice with sorted_input=true
|
||||
// Keys must be in sorted order for this optimization
|
||||
rocksdb_slice_t sorted_key_slices[3];
|
||||
sorted_key_slices[0].data = "box";
|
||||
sorted_key_slices[0].size = 3;
|
||||
sorted_key_slices[1].data = "buff";
|
||||
sorted_key_slices[1].size = 4;
|
||||
sorted_key_slices[2].data = "notfound";
|
||||
sorted_key_slices[2].size = 8;
|
||||
|
||||
const char* expected_value[3] = {"c", "rocksdb", NULL};
|
||||
char* batched_errs[3];
|
||||
rocksdb_pinnableslice_t* pvals[3];
|
||||
|
||||
rocksdb_batched_multi_get_cf_slice(db, roptions, handles[1], 3,
|
||||
sorted_key_slices, pvals, batched_errs,
|
||||
true);
|
||||
|
||||
const char* val;
|
||||
size_t val_len;
|
||||
for (i = 0; i < 3; ++i) {
|
||||
CheckNoError(batched_errs[i]);
|
||||
if (pvals[i] != NULL) {
|
||||
val = rocksdb_pinnableslice_value(pvals[i], &val_len);
|
||||
CheckEqual(expected_value[i], val, val_len);
|
||||
rocksdb_pinnableslice_destroy(pvals[i]);
|
||||
} else {
|
||||
CheckEqual(expected_value[i], NULL, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
unsigned char value_found = 0;
|
||||
|
||||
@@ -3249,6 +3596,14 @@ int main(int argc, char** argv) {
|
||||
100000 ==
|
||||
rocksdb_fifo_compaction_options_get_max_table_files_size(fco));
|
||||
|
||||
rocksdb_fifo_compaction_options_set_max_data_files_size(fco, 200000);
|
||||
CheckCondition(
|
||||
200000 == rocksdb_fifo_compaction_options_get_max_data_files_size(fco));
|
||||
|
||||
rocksdb_fifo_compaction_options_set_use_kv_ratio_compaction(fco, 1);
|
||||
CheckCondition(
|
||||
1 == rocksdb_fifo_compaction_options_get_use_kv_ratio_compaction(fco));
|
||||
|
||||
rocksdb_fifo_compaction_options_destroy(fco);
|
||||
}
|
||||
|
||||
@@ -3469,6 +3824,17 @@ int main(int argc, char** argv) {
|
||||
rocksdb_transaction_put(txn, "foo", 3, "hello", 5, &err);
|
||||
CheckNoError(err);
|
||||
|
||||
// test transaction get/set name (before commit)
|
||||
{
|
||||
rocksdb_transaction_set_name(txn, "test_txn", 8, &err);
|
||||
CheckNoError(err);
|
||||
size_t name_len;
|
||||
char* txn_name = rocksdb_transaction_get_name(txn, &name_len);
|
||||
CheckCondition(name_len == 8);
|
||||
CheckCondition(memcmp(txn_name, "test_txn", 8) == 0);
|
||||
rocksdb_free(txn_name);
|
||||
}
|
||||
|
||||
// read from outside transaction, before commit
|
||||
CheckTxnDBGet(txn_db, roptions, "foo", NULL);
|
||||
CheckTxnDBPinGet(txn_db, roptions, "foo", NULL);
|
||||
@@ -4089,7 +4455,7 @@ int main(int argc, char** argv) {
|
||||
|
||||
StartPhase("statistics");
|
||||
{
|
||||
const uint32_t BYTES_WRITTEN_TICKER = 60;
|
||||
const uint32_t BYTES_WRITTEN_TICKER = 61;
|
||||
const uint32_t DB_WRITE_HIST = 1;
|
||||
|
||||
rocksdb_statistics_histogram_data_t* hist =
|
||||
@@ -4207,6 +4573,296 @@ int main(int argc, char** argv) {
|
||||
rocksdb_cache_destroy(lru);
|
||||
}
|
||||
|
||||
StartPhase("remote_compaction_service");
|
||||
{
|
||||
RemoteCompactionState remote_state = {0, 0, 0, "", ""};
|
||||
|
||||
// Create compaction service
|
||||
rocksdb_compactionservice_t* service = rocksdb_compactionservice_create(
|
||||
&remote_state, // state
|
||||
RemoteCompactionDestroy, // destructor
|
||||
RemoteCompactionSchedule, // schedule callback
|
||||
"TestRemoteCompaction", // name
|
||||
RemoteCompactionWait, // wait callback
|
||||
RemoteCompactionCancel, // cancel_awaiting_jobs
|
||||
NULL); // on_installation
|
||||
|
||||
// Create options with remote compaction
|
||||
rocksdb_options_t* remote_options = rocksdb_options_create();
|
||||
rocksdb_options_set_create_if_missing(remote_options, 1);
|
||||
rocksdb_options_set_level0_file_num_compaction_trigger(remote_options, 2);
|
||||
rocksdb_options_set_write_buffer_size(remote_options,
|
||||
64 * 1024); // 64KB buffer
|
||||
rocksdb_options_set_max_bytes_for_level_base(remote_options,
|
||||
256 * 1024); // 256KB
|
||||
rocksdb_options_set_target_file_size_base(
|
||||
remote_options, 64 * 1024); // 64KB target file size
|
||||
// Disable automatic compactions to test manual compaction only
|
||||
rocksdb_options_set_disable_auto_compactions(remote_options, 1);
|
||||
rocksdb_options_set_compaction_service(remote_options, service);
|
||||
|
||||
// Destroy old DB and create new one
|
||||
rocksdb_close(db);
|
||||
rocksdb_destroy_db(remote_options, dbname, &err);
|
||||
CheckNoError(err);
|
||||
|
||||
db = rocksdb_open(remote_options, dbname, &err);
|
||||
CheckNoError(err);
|
||||
|
||||
// Create multiple SST files to trigger compaction
|
||||
rocksdb_flushoptions_t* flush_opts = rocksdb_flushoptions_create();
|
||||
rocksdb_flushoptions_set_wait(flush_opts, 1);
|
||||
|
||||
// Write and flush multiple times to create multiple L0 files
|
||||
// Write more data with larger values to ensure files are substantial
|
||||
for (int batch = 0; batch < 5; batch++) {
|
||||
for (int i = 0; i < 200; i++) {
|
||||
char key[20], val[1000];
|
||||
snprintf(key, sizeof(key), "key%d_%d", batch, i);
|
||||
// Fill value with repeated data to make it larger
|
||||
memset(val, 'a' + (batch % 26), sizeof(val) - 1);
|
||||
val[sizeof(val) - 1] = '\0';
|
||||
rocksdb_put(db, woptions, key, strlen(key), val, strlen(val), &err);
|
||||
CheckNoError(err);
|
||||
}
|
||||
rocksdb_flush(db, flush_opts, &err);
|
||||
CheckNoError(err);
|
||||
}
|
||||
rocksdb_flushoptions_destroy(flush_opts);
|
||||
|
||||
// Trigger manual compaction to invoke remote compaction service
|
||||
rocksdb_compact_range(db, NULL, 0, NULL, 0);
|
||||
|
||||
rocksdb_wait_for_compact_options_t* wco =
|
||||
rocksdb_wait_for_compact_options_create();
|
||||
rocksdb_wait_for_compact(db, wco, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_wait_for_compact_options_destroy(wco);
|
||||
|
||||
// Verify that callbacks were actually called
|
||||
CheckCondition(remote_state.schedule_called > 0);
|
||||
CheckCondition(remote_state.wait_called > 0);
|
||||
CheckCondition(strlen(remote_state.last_db_name) > 0);
|
||||
CheckCondition(strstr(remote_state.last_db_name, "rocksdb_c_test") != NULL);
|
||||
|
||||
// Verify data is still accessible after remote compaction
|
||||
// Just check a few keys to verify data integrity
|
||||
for (int batch = 0; batch < 5; batch++) {
|
||||
char key[20];
|
||||
snprintf(key, sizeof(key), "key%d_0", batch);
|
||||
size_t vallen;
|
||||
char* val = rocksdb_get(db, roptions, key, strlen(key), &vallen, &err);
|
||||
CheckNoError(err);
|
||||
CheckCondition(val != NULL);
|
||||
CheckCondition(vallen == 999); // strlen of 1000-byte string
|
||||
free(val);
|
||||
}
|
||||
|
||||
// Test cancellation API directly
|
||||
RemoteCompactionCancel(&remote_state);
|
||||
CheckCondition(remote_state.cancel_called > 0);
|
||||
|
||||
// Cleanup
|
||||
rocksdb_close(db);
|
||||
rocksdb_destroy_db(remote_options, dbname, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_options_destroy(remote_options);
|
||||
|
||||
// Reopen DB with original options for subsequent tests
|
||||
db = rocksdb_open(options, dbname, &err);
|
||||
CheckNoError(err);
|
||||
}
|
||||
|
||||
StartPhase("remote_compaction_scheduleresponse");
|
||||
{
|
||||
// Test scheduleresponse creation and getters
|
||||
rocksdb_compactionservice_scheduleresponse_t* response;
|
||||
|
||||
// Test success response
|
||||
err = NULL;
|
||||
response = rocksdb_compactionservice_scheduleresponse_create(
|
||||
"test-job-123", rocksdb_compactionservice_jobstatus_success, &err);
|
||||
CheckNoError(err);
|
||||
CheckCondition(response != NULL);
|
||||
CheckCondition(
|
||||
rocksdb_compactionservice_scheduleresponse_getstatus(response) ==
|
||||
rocksdb_compactionservice_jobstatus_success);
|
||||
|
||||
size_t job_id_len;
|
||||
const char* job_id =
|
||||
rocksdb_compactionservice_scheduleresponse_get_scheduled_job_id(
|
||||
response, &job_id_len);
|
||||
CheckCondition(job_id_len == strlen("test-job-123"));
|
||||
CheckCondition(memcmp(job_id, "test-job-123", job_id_len) == 0);
|
||||
rocksdb_compactionservice_scheduleresponse_t_destroy(response);
|
||||
|
||||
// Test failure response
|
||||
response = rocksdb_compactionservice_scheduleresponse_create_with_status(
|
||||
rocksdb_compactionservice_jobstatus_failure, &err);
|
||||
CheckCondition(response != NULL);
|
||||
CheckCondition(
|
||||
rocksdb_compactionservice_scheduleresponse_getstatus(response) ==
|
||||
rocksdb_compactionservice_jobstatus_failure);
|
||||
rocksdb_compactionservice_scheduleresponse_t_destroy(response);
|
||||
|
||||
response = rocksdb_compactionservice_scheduleresponse_create_with_status(
|
||||
999, &err);
|
||||
CheckCondition(response == NULL); // Invalid status
|
||||
if (err) {
|
||||
Free(&err);
|
||||
}
|
||||
}
|
||||
|
||||
StartPhase("remote_compaction_options_override");
|
||||
{
|
||||
// Test CompactionServiceOptionsOverride API
|
||||
rocksdb_compaction_service_options_override_t* override_opts =
|
||||
rocksdb_compaction_service_options_override_create();
|
||||
CheckCondition(override_opts != NULL);
|
||||
|
||||
// Set up override options
|
||||
rocksdb_compaction_service_options_override_set_env(override_opts, env);
|
||||
rocksdb_compaction_service_options_override_set_comparator(override_opts,
|
||||
cmp);
|
||||
|
||||
// Test file checksum gen factory
|
||||
rocksdb_file_checksum_gen_factory_t* checksum_factory =
|
||||
rocksdb_file_checksum_gen_crc32c_factory_create();
|
||||
CheckCondition(checksum_factory != NULL);
|
||||
rocksdb_compaction_service_options_override_set_file_checksum_gen_factory(
|
||||
override_opts, checksum_factory);
|
||||
|
||||
// Test SST partitioner factory
|
||||
rocksdb_sst_partitioner_factory_t* partitioner_factory =
|
||||
rocksdb_sst_partitioner_fixed_prefix_factory_create(4);
|
||||
CheckCondition(partitioner_factory != NULL);
|
||||
rocksdb_compaction_service_options_override_set_sst_partitioner_factory(
|
||||
override_opts, partitioner_factory);
|
||||
|
||||
// Test merge operator
|
||||
rocksdb_compaction_service_options_override_set_merge_operator(
|
||||
override_opts, NULL);
|
||||
|
||||
// Test compaction filter
|
||||
rocksdb_compaction_service_options_override_set_compaction_filter(
|
||||
override_opts, NULL);
|
||||
|
||||
// Test prefix extractor
|
||||
rocksdb_compaction_service_options_override_set_prefix_extractor(
|
||||
override_opts, NULL);
|
||||
|
||||
// Test table factory - block based
|
||||
rocksdb_block_based_table_options_t* table_opts =
|
||||
rocksdb_block_based_options_create();
|
||||
rocksdb_compaction_service_options_override_set_block_based_table_factory(
|
||||
override_opts, table_opts);
|
||||
rocksdb_block_based_options_destroy(table_opts);
|
||||
|
||||
// Test statistics via options
|
||||
rocksdb_options_t* stats_opts = rocksdb_options_create();
|
||||
rocksdb_options_enable_statistics(stats_opts);
|
||||
rocksdb_compaction_service_options_override_set_statistics(override_opts,
|
||||
stats_opts);
|
||||
rocksdb_options_destroy(stats_opts);
|
||||
|
||||
// Test info log
|
||||
rocksdb_logger_t* logger =
|
||||
rocksdb_logger_create_stderr_logger(1, "test_prefix");
|
||||
rocksdb_compaction_service_options_override_set_info_log(override_opts,
|
||||
logger);
|
||||
rocksdb_logger_destroy(logger);
|
||||
|
||||
// Test options map
|
||||
rocksdb_compaction_service_options_override_set_option(
|
||||
override_opts, "max_bytes_for_level_base", "67108864");
|
||||
|
||||
// Cleanup
|
||||
rocksdb_file_checksum_gen_factory_destroy(checksum_factory);
|
||||
rocksdb_sst_partitioner_factory_destroy(partitioner_factory);
|
||||
rocksdb_compaction_service_options_override_destroy(override_opts);
|
||||
}
|
||||
|
||||
StartPhase("factory_options_on_regular_options");
|
||||
{
|
||||
// Test that the new factory types work with regular rocksdb_options_t
|
||||
rocksdb_options_t* test_opts = rocksdb_options_create();
|
||||
|
||||
// Test file checksum gen factory on regular options
|
||||
rocksdb_file_checksum_gen_factory_t* checksum_factory =
|
||||
rocksdb_file_checksum_gen_crc32c_factory_create();
|
||||
CheckCondition(checksum_factory != NULL);
|
||||
rocksdb_options_set_file_checksum_gen_factory(test_opts, checksum_factory);
|
||||
|
||||
// Test SST partitioner factory on regular options
|
||||
rocksdb_sst_partitioner_factory_t* partitioner_factory =
|
||||
rocksdb_sst_partitioner_fixed_prefix_factory_create(8);
|
||||
CheckCondition(partitioner_factory != NULL);
|
||||
rocksdb_options_set_sst_partitioner_factory(test_opts, partitioner_factory);
|
||||
|
||||
// Cleanup
|
||||
rocksdb_file_checksum_gen_factory_destroy(checksum_factory);
|
||||
rocksdb_sst_partitioner_factory_destroy(partitioner_factory);
|
||||
rocksdb_options_destroy(test_opts);
|
||||
}
|
||||
|
||||
StartPhase("remote_compaction_null_callback_handling");
|
||||
{
|
||||
// Test that NULL callback returns are handled gracefully
|
||||
// This simulates a failure in the remote compaction service
|
||||
rocksdb_compactionservice_t* null_service =
|
||||
rocksdb_compactionservice_create(NULL, NULL, NullSchedule,
|
||||
"NullTestService", NULL, NULL, NULL);
|
||||
|
||||
rocksdb_options_t* null_opts = rocksdb_options_create();
|
||||
rocksdb_options_set_create_if_missing(null_opts, 1);
|
||||
rocksdb_options_set_compaction_service(null_opts, null_service);
|
||||
|
||||
const char* null_db = "rocksdb_c_test_null_service";
|
||||
|
||||
rocksdb_t* null_db_handle = rocksdb_open(null_opts, null_db, &err);
|
||||
CheckNoError(err);
|
||||
|
||||
// Write data and trigger compaction
|
||||
for (int i = 0; i < 100; i++) {
|
||||
char key[20], val[50];
|
||||
snprintf(key, sizeof(key), "key%d", i);
|
||||
snprintf(val, sizeof(val), "val%d", i);
|
||||
rocksdb_put(null_db_handle, woptions, key, strlen(key), val, strlen(val),
|
||||
&err);
|
||||
CheckNoError(err);
|
||||
}
|
||||
|
||||
// This should fall back to local compaction (not crash)
|
||||
rocksdb_compact_range(null_db_handle, NULL, 0, NULL, 0);
|
||||
|
||||
// Data should still be readable
|
||||
CheckGet(null_db_handle, roptions, "key50", "val50");
|
||||
|
||||
rocksdb_close(null_db_handle);
|
||||
rocksdb_destroy_db(null_opts, null_db, &err);
|
||||
rocksdb_options_destroy(null_opts);
|
||||
}
|
||||
|
||||
StartPhase("remote_compaction_canceled_flag");
|
||||
{
|
||||
// Test atomic cancellation flag API
|
||||
unsigned char* canceled = rocksdb_open_and_compact_canceled_create();
|
||||
CheckCondition(canceled != NULL);
|
||||
|
||||
// Set cancellation
|
||||
rocksdb_open_and_compact_canceled_set(canceled, 1);
|
||||
|
||||
// Use with OpenAndCompactOptions
|
||||
rocksdb_open_and_compact_options_t* oac_opts =
|
||||
rocksdb_open_and_compact_options_create();
|
||||
rocksdb_open_and_compact_options_set_canceled(oac_opts, canceled);
|
||||
rocksdb_open_and_compact_options_set_allow_resumption(oac_opts, 1);
|
||||
|
||||
// Cleanup
|
||||
rocksdb_open_and_compact_options_destroy(oac_opts);
|
||||
rocksdb_open_and_compact_canceled_destroy(canceled);
|
||||
}
|
||||
|
||||
StartPhase("sst_file_manager");
|
||||
{
|
||||
rocksdb_sst_file_manager_t* sst_file_manager;
|
||||
@@ -4224,6 +4880,21 @@ int main(int argc, char** argv) {
|
||||
rocksdb_sst_file_manager_destroy(sst_file_manager);
|
||||
}
|
||||
|
||||
StartPhase("create_column_family_error_returns_null");
|
||||
{
|
||||
// Creating a column family with a name that already exists should fail
|
||||
// and return NULL. Without the fix, the handle is leaked and a non-NULL
|
||||
// pointer with an indeterminate rep field is returned.
|
||||
char* cf_err = NULL;
|
||||
rocksdb_column_family_handle_t* cf_handle =
|
||||
rocksdb_create_column_family(db, options, "default", &cf_err);
|
||||
// Should have an error since "default" already exists
|
||||
CheckCondition(cf_err != NULL);
|
||||
// The handle should be NULL on error (this is the bug fix)
|
||||
CheckCondition(cf_handle == NULL);
|
||||
free(cf_err);
|
||||
}
|
||||
|
||||
StartPhase("cancel_all_background_work");
|
||||
rocksdb_cancel_all_background_work(db, 1);
|
||||
|
||||
|
||||
+31
-3
@@ -401,7 +401,13 @@ ColumnFamilyOptions SanitizeCfOptions(const ImmutableDBOptions& db_options,
|
||||
}
|
||||
|
||||
if (result.max_compaction_bytes == 0) {
|
||||
result.max_compaction_bytes = result.target_file_size_base * 25;
|
||||
// For FIFO with use_kv_ratio_compaction, leave max_compaction_bytes as 0
|
||||
// to signal "auto-calculate target from capacity and SST/blob ratio."
|
||||
// When explicitly set by the user, it overrides the auto-calculated target.
|
||||
if (result.compaction_style != kCompactionStyleFIFO ||
|
||||
!result.compaction_options_fifo.use_kv_ratio_compaction) {
|
||||
result.max_compaction_bytes = result.target_file_size_base * 25;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_block_based_table = (result.table_factory->IsInstanceOf(
|
||||
@@ -1248,7 +1254,7 @@ Compaction* ColumnFamilyData::PickCompaction(
|
||||
auto* result = compaction_picker_->PickCompaction(
|
||||
GetName(), mutable_options, mutable_db_options, existing_snapshots,
|
||||
snapshot_checker, current_->storage_info(), log_buffer,
|
||||
require_max_output_level);
|
||||
GetFullHistoryTsLow(), require_max_output_level);
|
||||
if (result != nullptr) {
|
||||
result->FinalizeInputInfo(current_);
|
||||
}
|
||||
@@ -1336,7 +1342,7 @@ Compaction* ColumnFamilyData::CompactRange(
|
||||
GetName(), mutable_cf_options, mutable_db_options,
|
||||
current_->storage_info(), input_level, output_level,
|
||||
compact_range_options, begin, end, compaction_end, conflict,
|
||||
max_file_num_to_ignore, trim_ts);
|
||||
max_file_num_to_ignore, trim_ts, GetFullHistoryTsLow());
|
||||
if (result != nullptr) {
|
||||
result->FinalizeInputInfo(current_);
|
||||
}
|
||||
@@ -1563,6 +1569,28 @@ Status ColumnFamilyData::ValidateOptions(
|
||||
"FIFO compaction only supported with max_open_files = -1.");
|
||||
}
|
||||
|
||||
if (db_options.open_files_async) {
|
||||
// FIFO TTL picker relies on reading the files table properties inside a
|
||||
// DB mutex. This can be slow if files are opened asynchronously, so we
|
||||
// disable it.
|
||||
//
|
||||
// TODO: consider blocking fifo compaction until async file open task
|
||||
// completes.
|
||||
if (cf_options.compaction_style == kCompactionStyleFIFO) {
|
||||
return Status::NotSupported(
|
||||
"FIFO compaction is not supported with open_files_async = true.");
|
||||
}
|
||||
// Open files async is not useful if skip_stats_update_on_db_open=true
|
||||
// because DB open will still block on IO.
|
||||
//
|
||||
// TODO: consider moving stats update inside async file open background
|
||||
// task.
|
||||
if (!db_options.skip_stats_update_on_db_open) {
|
||||
return Status::InvalidArgument(
|
||||
"open_files_async requires skip_stats_update_on_db_open = true.");
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<uint32_t> supported{0, 1, 2, 4, 8};
|
||||
if (std::find(supported.begin(), supported.end(),
|
||||
cf_options.memtable_protection_bytes_per_key) ==
|
||||
|
||||
+10
-13
@@ -118,8 +118,7 @@ class ColumnFamilyTestBase : public testing::Test {
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (flush_every != 0 && i != 0 && i % flush_every == 0) {
|
||||
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
|
||||
dbi->TEST_FlushMemTable();
|
||||
dbfull()->TEST_FlushMemTable();
|
||||
}
|
||||
|
||||
int keyi = base + i;
|
||||
@@ -177,8 +176,7 @@ class ColumnFamilyTestBase : public testing::Test {
|
||||
}
|
||||
handles_.clear();
|
||||
names_.clear();
|
||||
delete db_;
|
||||
db_ = nullptr;
|
||||
db_.reset();
|
||||
}
|
||||
|
||||
Status TryOpen(std::vector<std::string> cf,
|
||||
@@ -218,7 +216,7 @@ class ColumnFamilyTestBase : public testing::Test {
|
||||
|
||||
void Open() { Open({"default"}); }
|
||||
|
||||
DBImpl* dbfull() { return static_cast_with_check<DBImpl>(db_); }
|
||||
DBImpl* dbfull() { return static_cast_with_check<DBImpl>(db_.get()); }
|
||||
|
||||
int GetProperty(int cf, std::string property) {
|
||||
std::string value;
|
||||
@@ -500,7 +498,7 @@ class ColumnFamilyTestBase : public testing::Test {
|
||||
ColumnFamilyOptions column_family_options_;
|
||||
DBOptions db_options_;
|
||||
std::string dbname_;
|
||||
DB* db_ = nullptr;
|
||||
std::unique_ptr<DB> db_;
|
||||
EnvCounter* env_;
|
||||
std::shared_ptr<Env> env_guard_;
|
||||
Random rnd_;
|
||||
@@ -517,7 +515,7 @@ class ColumnFamilyTest
|
||||
INSTANTIATE_TEST_CASE_P(FormatDef, ColumnFamilyTest,
|
||||
testing::Values(test::kDefaultFormatVersion));
|
||||
INSTANTIATE_TEST_CASE_P(FormatLatest, ColumnFamilyTest,
|
||||
testing::Values(kLatestFormatVersion));
|
||||
testing::Values(kLatestBbtFormatVersion));
|
||||
|
||||
TEST_P(ColumnFamilyTest, DontReuseColumnFamilyID) {
|
||||
for (int iter = 0; iter < 3; ++iter) {
|
||||
@@ -707,8 +705,8 @@ INSTANTIATE_TEST_CASE_P(
|
||||
std::make_tuple(test::kDefaultFormatVersion, false)));
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
FormatLatest, FlushEmptyCFTestWithParam,
|
||||
testing::Values(std::make_tuple(kLatestFormatVersion, true),
|
||||
std::make_tuple(kLatestFormatVersion, false)));
|
||||
testing::Values(std::make_tuple(kLatestBbtFormatVersion, true),
|
||||
std::make_tuple(kLatestBbtFormatVersion, false)));
|
||||
|
||||
TEST_P(ColumnFamilyTest, AddDrop) {
|
||||
Open();
|
||||
@@ -3542,11 +3540,10 @@ TEST_P(ColumnFamilyTest, MultipleCFPathsTest) {
|
||||
|
||||
// Re-open and verify the keys.
|
||||
Reopen({ColumnFamilyOptions(), cf_opt1, cf_opt2});
|
||||
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
|
||||
for (int cf = 1; cf != 3; ++cf) {
|
||||
ReadOptions read_options;
|
||||
read_options.readahead_size = 0;
|
||||
auto it = dbi->NewIterator(read_options, handles_[cf]);
|
||||
auto it = db_->NewIterator(read_options, handles_[cf]);
|
||||
for (it->SeekToFirst(); it->Valid(); it->Next()) {
|
||||
ASSERT_OK(it->status());
|
||||
Slice key(it->key());
|
||||
@@ -3636,7 +3633,7 @@ TEST(ColumnFamilyTest, ValidateMemtableKVChecksumOption) {
|
||||
// the behavior of manual flush is that it skips retaining UDTs.
|
||||
class ColumnFamilyRetainUDTTest : public ColumnFamilyTestBase {
|
||||
public:
|
||||
ColumnFamilyRetainUDTTest() : ColumnFamilyTestBase(kLatestFormatVersion) {}
|
||||
ColumnFamilyRetainUDTTest() : ColumnFamilyTestBase(kLatestBbtFormatVersion) {}
|
||||
|
||||
void SetUp() override {
|
||||
db_options_.allow_concurrent_memtable_write = false;
|
||||
@@ -3886,7 +3883,7 @@ TEST_F(ManualFlushSkipRetainUDTTest, FlushRemovesStaleEntries) {
|
||||
static_cast_with_check<ColumnFamilyHandleImpl>(cfh)->cfd();
|
||||
for (int version = 0; version < 100; version++) {
|
||||
if (version == 50) {
|
||||
ASSERT_OK(static_cast_with_check<DBImpl>(db_)->TEST_SwitchMemtable(cfd));
|
||||
ASSERT_OK(dbfull()->TEST_SwitchMemtable(cfd));
|
||||
}
|
||||
ASSERT_OK(
|
||||
Put(0, "foo", EncodeAsUint64(version), "v" + std::to_string(version)));
|
||||
|
||||
+253
-40
@@ -75,10 +75,9 @@ TEST_F(CompactFilesTest, L0ConflictsFiles) {
|
||||
options.level0_file_num_compaction_trigger = kLevel0Trigger;
|
||||
options.compression = kNoCompression;
|
||||
|
||||
DB* db = nullptr;
|
||||
std::unique_ptr<DB> db;
|
||||
ASSERT_OK(DestroyDB(db_name_, options));
|
||||
Status s = DB::Open(options, db_name_, &db);
|
||||
assert(s.ok());
|
||||
ASSERT_OK(DB::Open(options, db_name_, &db));
|
||||
assert(db);
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
|
||||
@@ -114,7 +113,6 @@ TEST_F(CompactFilesTest, L0ConflictsFiles) {
|
||||
}
|
||||
}
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
delete db;
|
||||
}
|
||||
|
||||
TEST_F(CompactFilesTest, MultipleLevel) {
|
||||
@@ -128,11 +126,11 @@ TEST_F(CompactFilesTest, MultipleLevel) {
|
||||
FlushedFileCollector* collector = new FlushedFileCollector();
|
||||
options.listeners.emplace_back(collector);
|
||||
|
||||
DB* db = nullptr;
|
||||
std::unique_ptr<DB> db;
|
||||
ASSERT_OK(DestroyDB(db_name_, options));
|
||||
Status s = DB::Open(options, db_name_, &db);
|
||||
ASSERT_OK(s);
|
||||
ASSERT_NE(db, nullptr);
|
||||
ASSERT_NE(db.get(), nullptr);
|
||||
|
||||
// create couple files in L0, L3, L4 and L5
|
||||
for (int i = 5; i > 2; --i) {
|
||||
@@ -141,7 +139,8 @@ TEST_F(CompactFilesTest, MultipleLevel) {
|
||||
ASSERT_OK(db->Flush(FlushOptions()));
|
||||
// Ensure background work is fully finished including listener callbacks
|
||||
// before accessing listener state.
|
||||
ASSERT_OK(static_cast_with_check<DBImpl>(db)->TEST_WaitForBackgroundWork());
|
||||
ASSERT_OK(
|
||||
static_cast_with_check<DBImpl>(db.get())->TEST_WaitForBackgroundWork());
|
||||
auto l0_files = collector->GetFlushedFiles();
|
||||
ASSERT_OK(db->CompactFiles(CompactionOptions(), l0_files, i));
|
||||
|
||||
@@ -191,8 +190,6 @@ TEST_F(CompactFilesTest, MultipleLevel) {
|
||||
ASSERT_OK(db->CompactFiles(CompactionOptions(), files, 5));
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
thread.join();
|
||||
|
||||
delete db;
|
||||
}
|
||||
|
||||
TEST_F(CompactFilesTest, ObsoleteFiles) {
|
||||
@@ -212,11 +209,11 @@ TEST_F(CompactFilesTest, ObsoleteFiles) {
|
||||
FlushedFileCollector* collector = new FlushedFileCollector();
|
||||
options.listeners.emplace_back(collector);
|
||||
|
||||
DB* db = nullptr;
|
||||
std::unique_ptr<DB> db;
|
||||
ASSERT_OK(DestroyDB(db_name_, options));
|
||||
Status s = DB::Open(options, db_name_, &db);
|
||||
ASSERT_OK(s);
|
||||
ASSERT_NE(db, nullptr);
|
||||
ASSERT_NE(db.get(), nullptr);
|
||||
|
||||
// create couple files
|
||||
for (int i = 1000; i < 2000; ++i) {
|
||||
@@ -226,13 +223,12 @@ TEST_F(CompactFilesTest, ObsoleteFiles) {
|
||||
|
||||
auto l0_files = collector->GetFlushedFiles();
|
||||
ASSERT_OK(db->CompactFiles(CompactionOptions(), l0_files, 1));
|
||||
ASSERT_OK(static_cast_with_check<DBImpl>(db)->TEST_WaitForCompact());
|
||||
ASSERT_OK(static_cast_with_check<DBImpl>(db.get())->TEST_WaitForCompact());
|
||||
|
||||
// verify all compaction input files are deleted
|
||||
for (const auto& fname : l0_files) {
|
||||
ASSERT_EQ(Status::NotFound(), env_->FileExists(fname));
|
||||
}
|
||||
delete db;
|
||||
}
|
||||
|
||||
TEST_F(CompactFilesTest, NotCutOutputOnLevel0) {
|
||||
@@ -251,10 +247,9 @@ TEST_F(CompactFilesTest, NotCutOutputOnLevel0) {
|
||||
FlushedFileCollector* collector = new FlushedFileCollector();
|
||||
options.listeners.emplace_back(collector);
|
||||
|
||||
DB* db = nullptr;
|
||||
std::unique_ptr<DB> db;
|
||||
ASSERT_OK(DestroyDB(db_name_, options));
|
||||
Status s = DB::Open(options, db_name_, &db);
|
||||
assert(s.ok());
|
||||
ASSERT_OK(DB::Open(options, db_name_, &db));
|
||||
assert(db);
|
||||
|
||||
// create couple files
|
||||
@@ -262,19 +257,20 @@ TEST_F(CompactFilesTest, NotCutOutputOnLevel0) {
|
||||
ASSERT_OK(db->Put(WriteOptions(), std::to_string(i),
|
||||
std::string(1000, 'a' + (i % 26))));
|
||||
}
|
||||
ASSERT_OK(static_cast_with_check<DBImpl>(db)->TEST_WaitForFlushMemTable());
|
||||
ASSERT_OK(
|
||||
static_cast_with_check<DBImpl>(db.get())->TEST_WaitForFlushMemTable());
|
||||
auto l0_files_1 = collector->GetFlushedFiles();
|
||||
collector->ClearFlushedFiles();
|
||||
for (int i = 0; i < 500; ++i) {
|
||||
ASSERT_OK(db->Put(WriteOptions(), std::to_string(i),
|
||||
std::string(1000, 'a' + (i % 26))));
|
||||
}
|
||||
ASSERT_OK(static_cast_with_check<DBImpl>(db)->TEST_WaitForFlushMemTable());
|
||||
ASSERT_OK(
|
||||
static_cast_with_check<DBImpl>(db.get())->TEST_WaitForFlushMemTable());
|
||||
auto l0_files_2 = collector->GetFlushedFiles();
|
||||
ASSERT_OK(db->CompactFiles(CompactionOptions(), l0_files_1, 0));
|
||||
ASSERT_OK(db->CompactFiles(CompactionOptions(), l0_files_2, 0));
|
||||
// no assertion failure
|
||||
delete db;
|
||||
}
|
||||
|
||||
TEST_F(CompactFilesTest, CapturingPendingFiles) {
|
||||
@@ -289,7 +285,7 @@ TEST_F(CompactFilesTest, CapturingPendingFiles) {
|
||||
FlushedFileCollector* collector = new FlushedFileCollector();
|
||||
options.listeners.emplace_back(collector);
|
||||
|
||||
DB* db = nullptr;
|
||||
std::unique_ptr<DB> db;
|
||||
ASSERT_OK(DestroyDB(db_name_, options));
|
||||
Status s = DB::Open(options, db_name_, &db);
|
||||
ASSERT_OK(s);
|
||||
@@ -303,7 +299,8 @@ TEST_F(CompactFilesTest, CapturingPendingFiles) {
|
||||
|
||||
// Ensure background work is fully finished including listener callbacks
|
||||
// before accessing listener state.
|
||||
ASSERT_OK(static_cast_with_check<DBImpl>(db)->TEST_WaitForBackgroundWork());
|
||||
ASSERT_OK(
|
||||
static_cast_with_check<DBImpl>(db.get())->TEST_WaitForBackgroundWork());
|
||||
auto l0_files = collector->GetFlushedFiles();
|
||||
EXPECT_EQ(5, l0_files.size());
|
||||
|
||||
@@ -327,13 +324,12 @@ TEST_F(CompactFilesTest, CapturingPendingFiles) {
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
|
||||
delete db;
|
||||
db.reset();
|
||||
|
||||
// Make sure we can reopen the DB.
|
||||
s = DB::Open(options, db_name_, &db);
|
||||
ASSERT_OK(s);
|
||||
assert(db);
|
||||
delete db;
|
||||
}
|
||||
|
||||
TEST_F(CompactFilesTest, CompactionFilterWithGetSv) {
|
||||
@@ -365,12 +361,12 @@ TEST_F(CompactFilesTest, CompactionFilterWithGetSv) {
|
||||
options.create_if_missing = true;
|
||||
options.compaction_filter = cf.get();
|
||||
|
||||
DB* db = nullptr;
|
||||
std::unique_ptr<DB> db;
|
||||
ASSERT_OK(DestroyDB(db_name_, options));
|
||||
Status s = DB::Open(options, db_name_, &db);
|
||||
ASSERT_OK(s);
|
||||
|
||||
cf->SetDB(db);
|
||||
cf->SetDB(db.get());
|
||||
|
||||
// Write one L0 file
|
||||
ASSERT_OK(db->Put(WriteOptions(), "K1", "V1"));
|
||||
@@ -384,8 +380,6 @@ TEST_F(CompactFilesTest, CompactionFilterWithGetSv) {
|
||||
ASSERT_OK(
|
||||
db->CompactFiles(ROCKSDB_NAMESPACE::CompactionOptions(), {fname}, 0));
|
||||
}
|
||||
|
||||
delete db;
|
||||
}
|
||||
|
||||
TEST_F(CompactFilesTest, SentinelCompressionType) {
|
||||
@@ -413,7 +407,7 @@ TEST_F(CompactFilesTest, SentinelCompressionType) {
|
||||
options.create_if_missing = true;
|
||||
FlushedFileCollector* collector = new FlushedFileCollector();
|
||||
options.listeners.emplace_back(collector);
|
||||
DB* db = nullptr;
|
||||
std::unique_ptr<DB> db;
|
||||
ASSERT_OK(DB::Open(options, db_name_, &db));
|
||||
|
||||
ASSERT_OK(db->Put(WriteOptions(), "key", "val"));
|
||||
@@ -421,7 +415,8 @@ TEST_F(CompactFilesTest, SentinelCompressionType) {
|
||||
|
||||
// Ensure background work is fully finished including listener callbacks
|
||||
// before accessing listener state.
|
||||
ASSERT_OK(static_cast_with_check<DBImpl>(db)->TEST_WaitForBackgroundWork());
|
||||
ASSERT_OK(
|
||||
static_cast_with_check<DBImpl>(db.get())->TEST_WaitForBackgroundWork());
|
||||
auto l0_files = collector->GetFlushedFiles();
|
||||
ASSERT_EQ(1, l0_files.size());
|
||||
|
||||
@@ -433,10 +428,10 @@ TEST_F(CompactFilesTest, SentinelCompressionType) {
|
||||
ROCKSDB_NAMESPACE::TablePropertiesCollection all_tables_props;
|
||||
ASSERT_OK(db->GetPropertiesOfAllTables(&all_tables_props));
|
||||
for (const auto& name_and_table_props : all_tables_props) {
|
||||
ASSERT_EQ(CompressionTypeToString(CompressionType::kZlibCompression),
|
||||
name_and_table_props.second->compression_name);
|
||||
// As of format_version 7, more elaborate information is encoded into the
|
||||
// compression_name property
|
||||
ASSERT_EQ("BuiltinV2;02;", name_and_table_props.second->compression_name);
|
||||
}
|
||||
delete db;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,11 +456,7 @@ TEST_F(CompactFilesTest, CompressionWithBlockAlign) {
|
||||
}
|
||||
|
||||
std::unique_ptr<DB> db;
|
||||
{
|
||||
DB* _db = nullptr;
|
||||
ASSERT_OK(DB::Open(options, db_name_, &_db));
|
||||
db.reset(_db);
|
||||
}
|
||||
ASSERT_OK(DB::Open(options, db_name_, &db));
|
||||
|
||||
ASSERT_OK(db->Put(WriteOptions(), "key", "val"));
|
||||
ASSERT_OK(db->Flush(FlushOptions()));
|
||||
@@ -504,7 +495,7 @@ TEST_F(CompactFilesTest, GetCompactionJobInfo) {
|
||||
FlushedFileCollector* collector = new FlushedFileCollector();
|
||||
options.listeners.emplace_back(collector);
|
||||
|
||||
DB* db = nullptr;
|
||||
std::unique_ptr<DB> db;
|
||||
ASSERT_OK(DestroyDB(db_name_, options));
|
||||
Status s = DB::Open(options, db_name_, &db);
|
||||
ASSERT_OK(s);
|
||||
@@ -515,7 +506,8 @@ TEST_F(CompactFilesTest, GetCompactionJobInfo) {
|
||||
ASSERT_OK(db->Put(WriteOptions(), std::to_string(i),
|
||||
std::string(1000, 'a' + (i % 26))));
|
||||
}
|
||||
ASSERT_OK(static_cast_with_check<DBImpl>(db)->TEST_WaitForFlushMemTable());
|
||||
ASSERT_OK(
|
||||
static_cast_with_check<DBImpl>(db.get())->TEST_WaitForFlushMemTable());
|
||||
auto l0_files_1 = collector->GetFlushedFiles();
|
||||
CompactionOptions co;
|
||||
co.compression = CompressionType::kLZ4Compression;
|
||||
@@ -531,7 +523,228 @@ TEST_F(CompactFilesTest, GetCompactionJobInfo) {
|
||||
ASSERT_EQ(compaction_job_info.output_level, 0);
|
||||
ASSERT_OK(compaction_job_info.status);
|
||||
// no assertion failure
|
||||
delete db;
|
||||
}
|
||||
|
||||
// Helper function to generate zero-padded keys
|
||||
// e.g., MakeKey("a", 5) -> "a05", MakeKey("b", 42) -> "b42"
|
||||
static std::string MakeKey(const std::string& prefix, int index) {
|
||||
return prefix + (index < 10 ? "0" : "") + std::to_string(index);
|
||||
}
|
||||
|
||||
TEST_F(CompactFilesTest, TrivialMoveNonOverlappingFiles) {
|
||||
Options options;
|
||||
options.create_if_missing = true;
|
||||
options.disable_auto_compactions = true;
|
||||
options.compression = kNoCompression;
|
||||
options.level_compaction_dynamic_level_bytes = false;
|
||||
|
||||
std::unique_ptr<DB> db;
|
||||
ASSERT_OK(DestroyDB(db_name_, options));
|
||||
Status s = DB::Open(options, db_name_, &db);
|
||||
ASSERT_OK(s);
|
||||
ASSERT_NE(db.get(), nullptr);
|
||||
|
||||
// Create 3 non-overlapping files in L0
|
||||
// File 1: keys [a00-a99]
|
||||
for (int i = 0; i < 100; i++) {
|
||||
std::string key = MakeKey("a", i);
|
||||
ASSERT_OK(db->Put(WriteOptions(), key, "value_" + key));
|
||||
}
|
||||
ASSERT_OK(db->Flush(FlushOptions()));
|
||||
|
||||
// File 2: keys [b00-b99]
|
||||
for (int i = 0; i < 100; i++) {
|
||||
std::string key = MakeKey("b", i);
|
||||
ASSERT_OK(db->Put(WriteOptions(), key, "value_" + key));
|
||||
}
|
||||
ASSERT_OK(db->Flush(FlushOptions()));
|
||||
|
||||
// File 3: keys [c00-c99]
|
||||
for (int i = 0; i < 100; i++) {
|
||||
std::string key = MakeKey("c", i);
|
||||
ASSERT_OK(db->Put(WriteOptions(), key, "value_" + key));
|
||||
}
|
||||
ASSERT_OK(db->Flush(FlushOptions()));
|
||||
|
||||
// Verify files are in L0
|
||||
ColumnFamilyMetaData meta;
|
||||
db->GetColumnFamilyMetaData(&meta);
|
||||
ASSERT_EQ(meta.levels[0].files.size(), 3);
|
||||
ASSERT_EQ(meta.levels[1].files.size(), 0);
|
||||
|
||||
// Get L0 files
|
||||
std::vector<std::string> l0_files;
|
||||
for (const auto& file : meta.levels[0].files) {
|
||||
l0_files.push_back(file.db_path + "/" + file.name);
|
||||
}
|
||||
|
||||
CompactionOptions compact_option;
|
||||
compact_option.allow_trivial_move = true;
|
||||
// Compact all L0 files to L1 (non-overlapping in L1)
|
||||
ASSERT_OK(db->CompactFiles(compact_option, l0_files, 1));
|
||||
|
||||
// Verify files are now in L1
|
||||
db->GetColumnFamilyMetaData(&meta);
|
||||
ASSERT_EQ(meta.levels[0].files.size(), 0);
|
||||
ASSERT_EQ(meta.levels[1].files.size(), 3);
|
||||
|
||||
// Get the first file from L1 (should be the one with keys a00-a99)
|
||||
std::string l1_file_to_move;
|
||||
std::vector<std::string> l1_files_to_move_later;
|
||||
uint64_t l1_file_number = 0;
|
||||
for (const auto& file : meta.levels[1].files) {
|
||||
if (file.smallestkey[0] == 'a') {
|
||||
l1_file_to_move = file.db_path + "/" + file.name;
|
||||
l1_file_number = file.file_number;
|
||||
} else {
|
||||
l1_files_to_move_later.push_back(file.db_path + "/" + file.name);
|
||||
}
|
||||
}
|
||||
ASSERT_FALSE(l1_file_to_move.empty());
|
||||
|
||||
// Set up sync point to verify trivial move path is taken
|
||||
bool trivial_move_executed = false;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::CompactFilesImpl:TrivialMove",
|
||||
[&](void* /*arg*/) { trivial_move_executed = true; });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Move the file from L1 to L6 - this should be a trivial move
|
||||
// because the file doesn't overlap with anything in L6
|
||||
std::vector<std::string> files_to_move = {l1_file_to_move};
|
||||
ASSERT_OK(db->CompactFiles(compact_option, files_to_move, 6));
|
||||
|
||||
// Verify trivial move was executed
|
||||
ASSERT_TRUE(trivial_move_executed);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
// Verify the file is now in L6
|
||||
db->GetColumnFamilyMetaData(&meta);
|
||||
ASSERT_EQ(meta.levels[1].files.size(), 2); // Two files remain in L1
|
||||
ASSERT_EQ(meta.levels[6].files.size(), 1); // One file in L6
|
||||
|
||||
// Verify it's the correct file in L6
|
||||
bool found_file_in_l6 = false;
|
||||
for (const auto& file : meta.levels[6].files) {
|
||||
if (file.file_number == l1_file_number) {
|
||||
found_file_in_l6 = true;
|
||||
// Verify key range hasn't changed
|
||||
ASSERT_EQ(file.smallestkey[0], 'a');
|
||||
ASSERT_EQ(file.largestkey[0], 'a');
|
||||
break;
|
||||
}
|
||||
}
|
||||
ASSERT_TRUE(found_file_in_l6);
|
||||
|
||||
// Move the other 2 files from L1 to L6, with allow_trivial_move set to false.
|
||||
// This will trigger a normal compaction, so the 2 files will be compacted
|
||||
// into a single file in L6.
|
||||
ASSERT_OK(db->CompactFiles(CompactionOptions(), l1_files_to_move_later, 6));
|
||||
|
||||
// Verify files in L6
|
||||
db->GetColumnFamilyMetaData(&meta);
|
||||
ASSERT_EQ(meta.levels[1].files.size(), 0); // Zero files remain in L1
|
||||
ASSERT_EQ(meta.levels[6].files.size(), 2); // Two file in L6
|
||||
|
||||
// Verify data integrity - all keys should still be readable
|
||||
for (int i = 0; i < 100; i++) {
|
||||
std::string key = MakeKey("a", i);
|
||||
std::string value;
|
||||
ASSERT_OK(db->Get(ReadOptions(), key, &value));
|
||||
ASSERT_EQ(value, "value_" + key);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(CompactFilesTest, TrivialMoveBlockedByOverlap) {
|
||||
Options options;
|
||||
options.create_if_missing = true;
|
||||
options.disable_auto_compactions = true;
|
||||
options.compression = kNoCompression;
|
||||
options.level_compaction_dynamic_level_bytes = false;
|
||||
options.num_levels = 7;
|
||||
|
||||
std::unique_ptr<DB> db;
|
||||
ASSERT_OK(DestroyDB(db_name_, options));
|
||||
Status s = DB::Open(options, db_name_, &db);
|
||||
ASSERT_OK(s);
|
||||
ASSERT_NE(db.get(), nullptr);
|
||||
|
||||
// Create a file in L6 with keys [m00-m99] (wide range)
|
||||
for (int i = 0; i < 100; i++) {
|
||||
std::string key = MakeKey("m", i);
|
||||
ASSERT_OK(db->Put(WriteOptions(), key, "value_" + key));
|
||||
}
|
||||
ASSERT_OK(db->Flush(FlushOptions()));
|
||||
|
||||
// Get L0 file
|
||||
ColumnFamilyMetaData meta;
|
||||
db->GetColumnFamilyMetaData(&meta);
|
||||
std::vector<std::string> l0_files;
|
||||
for (const auto& file : meta.levels[0].files) {
|
||||
l0_files.push_back(file.db_path + "/" + file.name);
|
||||
}
|
||||
|
||||
CompactionOptions compact_option;
|
||||
compact_option.allow_trivial_move = true;
|
||||
|
||||
// Move to L6
|
||||
ASSERT_OK(db->CompactFiles(compact_option, l0_files, 6));
|
||||
|
||||
// Now create a file in L1 with overlapping keys [m50-m60]
|
||||
for (int i = 50; i <= 60; i++) {
|
||||
std::string key = "m" + std::to_string(i);
|
||||
ASSERT_OK(db->Put(WriteOptions(), key, "updated_value_" + key));
|
||||
}
|
||||
ASSERT_OK(db->Flush(FlushOptions()));
|
||||
|
||||
// Get the L0 file
|
||||
db->GetColumnFamilyMetaData(&meta);
|
||||
std::vector<std::string> l0_files_2;
|
||||
for (const auto& file : meta.levels[0].files) {
|
||||
l0_files_2.push_back(file.db_path + "/" + file.name);
|
||||
}
|
||||
|
||||
// Move to L1
|
||||
ASSERT_OK(db->CompactFiles(compact_option, l0_files_2, 1));
|
||||
|
||||
// Get the L1 file
|
||||
db->GetColumnFamilyMetaData(&meta);
|
||||
ASSERT_EQ(meta.levels[1].files.size(), 1);
|
||||
std::string l1_file =
|
||||
meta.levels[1].files[0].db_path + "/" + meta.levels[1].files[0].name;
|
||||
|
||||
// Set up sync point to verify full compaction path is taken
|
||||
bool trivial_move_executed = false;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::CompactFilesImpl:TrivialMove",
|
||||
[&](void* /*arg*/) { trivial_move_executed = true; });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Try to move from L1 to L6 - this should NOT be a trivial move
|
||||
// because the file overlaps with the existing file in L6
|
||||
ASSERT_OK(db->CompactFiles(compact_option, {l1_file}, 6));
|
||||
|
||||
// Verify trivial move was NOT executed (full compaction happened)
|
||||
ASSERT_FALSE(trivial_move_executed);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
// Verify the result - should have merged data in L6
|
||||
db->GetColumnFamilyMetaData(&meta);
|
||||
ASSERT_EQ(meta.levels[1].files.size(), 0); // L1 should be empty
|
||||
// L6 should have the merged file (may be 1 file if merged, or 2 if not)
|
||||
ASSERT_GE(meta.levels[6].files.size(), 1);
|
||||
|
||||
// Verify updated values are present
|
||||
for (int i = 50; i <= 60; i++) {
|
||||
std::string key = "m" + std::to_string(i);
|
||||
std::string value;
|
||||
ASSERT_OK(db->Get(ReadOptions(), key, &value));
|
||||
ASSERT_EQ(value, "updated_value_" + key);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
@@ -180,6 +180,10 @@ class Compaction {
|
||||
const std::vector<CompactionInputFiles>* inputs() { return &inputs_; }
|
||||
|
||||
// Returns the LevelFilesBrief of the specified compaction input level.
|
||||
// Note that if the compaction includes standalone range deletion file,
|
||||
// this function returns the result after filtering out input files covered
|
||||
// by the range deletion file.
|
||||
// Use inputs() if you want to get the original input files.
|
||||
const LevelFilesBrief* input_levels(size_t compaction_input_level) const {
|
||||
return &input_levels_[compaction_input_level];
|
||||
}
|
||||
|
||||
@@ -1310,14 +1310,14 @@ void CompactionIterator::PrepareOutput() {
|
||||
validity_info_.rep);
|
||||
assert(false);
|
||||
}
|
||||
ikey_.sequence = 0;
|
||||
last_key_seq_zeroed_ = true;
|
||||
TEST_SYNC_POINT_CALLBACK("CompactionIterator::PrepareOutput:ZeroingSeq",
|
||||
&ikey_);
|
||||
|
||||
bool zeroed_seqno = false;
|
||||
if (!timestamp_size_) {
|
||||
current_key_.UpdateInternalKey(0, ikey_.type);
|
||||
zeroed_seqno = true;
|
||||
} else if (full_history_ts_low_ && cmp_with_history_ts_low_ < 0) {
|
||||
// We can also zero out timestamp for better compression.
|
||||
// For UDT, the seqno and timestamp could only be zeroed out after the
|
||||
// key is below history_ts_low_.
|
||||
// For the same user key (excluding timestamp), the timestamp-based
|
||||
// history can be collapsed to save some space if the timestamp is
|
||||
// older than *full_history_ts_low_.
|
||||
@@ -1325,6 +1325,14 @@ void CompactionIterator::PrepareOutput() {
|
||||
const Slice ts_slice = kTsMin;
|
||||
ikey_.SetTimestamp(ts_slice);
|
||||
current_key_.UpdateInternalKey(0, ikey_.type, &ts_slice);
|
||||
zeroed_seqno = true;
|
||||
}
|
||||
|
||||
if (zeroed_seqno) {
|
||||
ikey_.sequence = 0;
|
||||
last_key_seq_zeroed_ = true;
|
||||
TEST_SYNC_POINT_CALLBACK("CompactionIterator::PrepareOutput:ZeroingSeq",
|
||||
&ikey_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+291
-93
@@ -33,6 +33,7 @@
|
||||
#include "db/range_del_aggregator.h"
|
||||
#include "db/version_edit.h"
|
||||
#include "db/version_set.h"
|
||||
#include "file/file_util.h"
|
||||
#include "file/filename.h"
|
||||
#include "file/read_write_util.h"
|
||||
#include "file/sst_file_manager_impl.h"
|
||||
@@ -128,6 +129,10 @@ const char* GetCompactionProximalOutputRangeTypeString(
|
||||
}
|
||||
}
|
||||
|
||||
// Static constant for compaction abort flag - always false, used for
|
||||
// compaction service jobs that don't support abort signaling
|
||||
const std::atomic<int> CompactionJob::kCompactionAbortedFalse{0};
|
||||
|
||||
CompactionJob::CompactionJob(
|
||||
int job_id, Compaction* compaction, const ImmutableDBOptions& db_options,
|
||||
const MutableDBOptions& mutable_db_options, const FileOptions& file_options,
|
||||
@@ -141,10 +146,10 @@ CompactionJob::CompactionJob(
|
||||
CompactionJobStats* compaction_job_stats, Env::Priority thread_pri,
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const std::atomic<bool>& manual_compaction_canceled,
|
||||
const std::string& db_id, const std::string& db_session_id,
|
||||
std::string full_history_ts_low, std::string trim_ts,
|
||||
BlobFileCompletionCallback* blob_callback, int* bg_compaction_scheduled,
|
||||
int* bg_bottom_compaction_scheduled)
|
||||
const std::atomic<int>& compaction_aborted, const std::string& db_id,
|
||||
const std::string& db_session_id, std::string full_history_ts_low,
|
||||
std::string trim_ts, BlobFileCompletionCallback* blob_callback,
|
||||
int* bg_compaction_scheduled, int* bg_bottom_compaction_scheduled)
|
||||
: compact_(new CompactionState(compaction)),
|
||||
internal_stats_(compaction->compaction_reason(), 1),
|
||||
db_options_(db_options),
|
||||
@@ -168,6 +173,7 @@ CompactionJob::CompactionJob(
|
||||
versions_(versions),
|
||||
shutting_down_(shutting_down),
|
||||
manual_compaction_canceled_(manual_compaction_canceled),
|
||||
compaction_aborted_(compaction_aborted),
|
||||
db_directory_(db_directory),
|
||||
blob_output_directory_(blob_output_directory),
|
||||
db_mutex_(db_mutex),
|
||||
@@ -708,6 +714,7 @@ void CompactionJob::InitializeCompactionRun() {
|
||||
}
|
||||
|
||||
void CompactionJob::RunSubcompactions() {
|
||||
TEST_SYNC_POINT("CompactionJob::RunSubcompactions:BeforeStart");
|
||||
const size_t num_threads = compact_->sub_compact_states.size();
|
||||
assert(num_threads > 0);
|
||||
compact_->compaction->GetOrInitInputTableProperties();
|
||||
@@ -753,6 +760,71 @@ void CompactionJob::RemoveEmptyOutputs() {
|
||||
}
|
||||
}
|
||||
|
||||
void CompactionJob::CleanupAbortedSubcompactions() {
|
||||
ColumnFamilyData* cfd = compact_->compaction->column_family_data();
|
||||
|
||||
uint64_t total_sst_files_deleted = 0;
|
||||
uint64_t total_blob_files_deleted = 0;
|
||||
|
||||
// Track the first file deletion error to report at the end
|
||||
Status first_error;
|
||||
int deletion_errors = 0;
|
||||
|
||||
// Mark all subcompactions as aborted and delete their output files
|
||||
for (auto& sub_compact : compact_->sub_compact_states) {
|
||||
// Mark this subcompaction as aborted
|
||||
sub_compact.status =
|
||||
Status::Incomplete(Status::SubCode::kCompactionAborted);
|
||||
|
||||
// Delete all files (SST and blob) tracked during compaction.
|
||||
// GetOutputFilePaths() contains ALL file paths created, including
|
||||
// in-progress files that may have been removed from outputs_ or
|
||||
// blob_file_additions_.
|
||||
for (const bool is_proximal_level : {false, true}) {
|
||||
if (is_proximal_level &&
|
||||
!compact_->compaction->SupportsPerKeyPlacement()) {
|
||||
continue;
|
||||
}
|
||||
for (const std::string& file_path :
|
||||
sub_compact.Outputs(is_proximal_level)->GetOutputFilePaths()) {
|
||||
Status s = env_->DeleteFile(file_path);
|
||||
if (s.ok()) {
|
||||
// Count SST vs blob files by checking extension
|
||||
if (file_path.find(".sst") != std::string::npos) {
|
||||
total_sst_files_deleted++;
|
||||
} else if (file_path.find(".blob") != std::string::npos) {
|
||||
total_blob_files_deleted++;
|
||||
}
|
||||
} else if (!s.IsNotFound()) {
|
||||
if (first_error.ok()) {
|
||||
first_error = s;
|
||||
}
|
||||
deletion_errors++;
|
||||
}
|
||||
}
|
||||
}
|
||||
sub_compact.CleanupOutputs();
|
||||
}
|
||||
|
||||
if (stats_) {
|
||||
RecordTick(stats_, COMPACTION_ABORTED);
|
||||
}
|
||||
|
||||
ROCKS_LOG_INFO(db_options_.info_log,
|
||||
"[%s] [JOB %d] Compaction aborted: deleted %" PRIu64
|
||||
" SST files and %" PRIu64 " blob files",
|
||||
cfd->GetName().c_str(), job_id_, total_sst_files_deleted,
|
||||
total_blob_files_deleted);
|
||||
|
||||
if (!first_error.ok()) {
|
||||
ROCKS_LOG_ERROR(db_options_.info_log,
|
||||
"[%s] [JOB %d] Cleanup completed with %d file deletion "
|
||||
"errors. First error: %s",
|
||||
cfd->GetName().c_str(), job_id_, deletion_errors,
|
||||
first_error.ToString().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
bool CompactionJob::HasNewBlobFiles() const {
|
||||
for (const auto& state : compact_->sub_compact_states) {
|
||||
if (state.Current().HasBlobFileAdditions()) {
|
||||
@@ -812,20 +884,19 @@ Status CompactionJob::SyncOutputDirectories() {
|
||||
Status CompactionJob::VerifyOutputFiles() {
|
||||
Status status;
|
||||
std::vector<port::Thread> thread_pool;
|
||||
std::vector<const CompactionOutputs::Output*> files_output;
|
||||
for (const auto& state : compact_->sub_compact_states) {
|
||||
for (const auto& output : state.GetOutputs()) {
|
||||
files_output.emplace_back(&output);
|
||||
}
|
||||
}
|
||||
ColumnFamilyData* cfd = compact_->compaction->column_family_data();
|
||||
std::atomic<size_t> next_file_idx(0);
|
||||
auto verify_table = [&](Status& output_status) {
|
||||
while (true) {
|
||||
size_t file_idx = next_file_idx.fetch_add(1);
|
||||
if (file_idx >= files_output.size()) {
|
||||
break;
|
||||
}
|
||||
VerifyOutputFlags verify_output_flags =
|
||||
compact_->compaction->mutable_cf_options().verify_output_flags;
|
||||
|
||||
// For backward compatibility
|
||||
if (paranoid_file_checks_) {
|
||||
verify_output_flags |= VerifyOutputFlags::kVerifyIteration;
|
||||
verify_output_flags |= VerifyOutputFlags::kEnableForLocalCompaction;
|
||||
verify_output_flags |= VerifyOutputFlags::kEnableForRemoteCompaction;
|
||||
}
|
||||
|
||||
auto verify_table = [&](SubcompactionState& subcompaction_state) {
|
||||
for (const auto& output_file : subcompaction_state.GetOutputs()) {
|
||||
// Verify that the table is usable
|
||||
// We set for_compaction to false and don't
|
||||
// OptimizeForCompactionTableRead here because this is a special case
|
||||
@@ -833,14 +904,20 @@ Status CompactionJob::VerifyOutputFiles() {
|
||||
// use_direct_io_for_flush_and_compaction is true, we will regard this
|
||||
// verification as user reads since the goal is to cache it here for
|
||||
// further user reads
|
||||
ReadOptions verify_table_read_options(Env::IOActivity::kCompaction);
|
||||
verify_table_read_options.rate_limiter_priority =
|
||||
ReadOptions verification_read_options(Env::IOActivity::kCompaction);
|
||||
verification_read_options.verify_checksums = true;
|
||||
verification_read_options.readahead_size =
|
||||
file_options_for_read_.compaction_readahead_size;
|
||||
|
||||
std::unique_ptr<TableReader> table_reader_guard;
|
||||
TableReader* table_reader_ptr = table_reader_guard.get();
|
||||
verification_read_options.rate_limiter_priority =
|
||||
GetRateLimiterPriority();
|
||||
InternalIterator* iter = cfd->table_cache()->NewIterator(
|
||||
verify_table_read_options, file_options_, cfd->internal_comparator(),
|
||||
files_output[file_idx]->meta,
|
||||
verification_read_options, file_options_, cfd->internal_comparator(),
|
||||
output_file.meta,
|
||||
/*range_del_agg=*/nullptr, compact_->compaction->mutable_cf_options(),
|
||||
/*table_reader_ptr=*/nullptr,
|
||||
/*table_reader_ptr=*/&table_reader_ptr,
|
||||
cfd->internal_stats()->GetFileReadHist(
|
||||
compact_->compaction->output_level()),
|
||||
TableReaderCaller::kCompactionRefill, /*arena=*/nullptr,
|
||||
@@ -850,38 +927,86 @@ Status CompactionJob::VerifyOutputFiles() {
|
||||
/*largest_compaction_key=*/nullptr,
|
||||
/*allow_unprepared_value=*/false);
|
||||
auto s = iter->status();
|
||||
if (s.ok()) {
|
||||
// Check for remote/local compaction and verify_output_flags flags
|
||||
const bool should_verify =
|
||||
(subcompaction_state.compaction_job_stats.is_remote_compaction &&
|
||||
!!(verify_output_flags &
|
||||
VerifyOutputFlags::kEnableForRemoteCompaction)) ||
|
||||
(!subcompaction_state.compaction_job_stats.is_remote_compaction &&
|
||||
!!(verify_output_flags &
|
||||
VerifyOutputFlags::kEnableForLocalCompaction));
|
||||
|
||||
if (s.ok() && paranoid_file_checks_) {
|
||||
OutputValidator validator(cfd->internal_comparator(),
|
||||
/*_enable_hash=*/true);
|
||||
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
|
||||
s = validator.Add(iter->key(), iter->value());
|
||||
if (!s.ok()) {
|
||||
break;
|
||||
if (should_verify) {
|
||||
const bool should_verify_block_checksum =
|
||||
!!(verify_output_flags & VerifyOutputFlags::kVerifyBlockChecksum);
|
||||
const bool should_verify_iteration =
|
||||
!!(verify_output_flags & VerifyOutputFlags::kVerifyIteration);
|
||||
const bool should_verify_file_checksum =
|
||||
!!(verify_output_flags &
|
||||
VerifyOutputFlags::kVerifyFileChecksum) &&
|
||||
db_options_.file_checksum_gen_factory != nullptr &&
|
||||
output_file.meta.file_checksum != kUnknownFileChecksum;
|
||||
if (should_verify_block_checksum) {
|
||||
assert(table_reader_ptr != nullptr);
|
||||
// If verifying iteration as well, verify meta blocks here only to
|
||||
// avoid redundant checks on data blocks
|
||||
s = table_reader_ptr->VerifyChecksum(
|
||||
verification_read_options, TableReaderCaller::kCompaction,
|
||||
/*meta_blocks_only=*/should_verify_iteration);
|
||||
}
|
||||
if (s.ok() && should_verify_iteration) {
|
||||
OutputValidator validator(cfd->internal_comparator(),
|
||||
/*_enable_hash=*/true);
|
||||
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
|
||||
s = validator.Add(iter->key(), iter->value());
|
||||
if (!s.ok()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (s.ok()) {
|
||||
s = iter->status();
|
||||
}
|
||||
if (s.ok() && !validator.CompareValidator(output_file.validator)) {
|
||||
s = Status::Corruption(
|
||||
"Key-value checksum of compaction output doesn't match what "
|
||||
"was computed when written");
|
||||
}
|
||||
}
|
||||
if (s.ok() && should_verify_file_checksum) {
|
||||
std::string file_checksum;
|
||||
std::string file_checksum_func_name;
|
||||
std::string fname =
|
||||
GetTableFileName(output_file.meta.fd.GetNumber());
|
||||
s = GenerateOneFileChecksum(
|
||||
fs_.get(), fname, db_options_.file_checksum_gen_factory.get(),
|
||||
output_file.meta.file_checksum_func_name, &file_checksum,
|
||||
&file_checksum_func_name,
|
||||
verification_read_options.readahead_size,
|
||||
db_options_.allow_mmap_reads, io_tracer_,
|
||||
db_options_.rate_limiter.get(), verification_read_options,
|
||||
stats_, db_options_.clock, file_options_for_read_);
|
||||
if (s.ok() && file_checksum != output_file.meta.file_checksum) {
|
||||
s = Status::Corruption(
|
||||
"File checksum mismatch for compaction output file " + fname);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (s.ok()) {
|
||||
s = iter->status();
|
||||
}
|
||||
if (s.ok() &&
|
||||
!validator.CompareValidator(files_output[file_idx]->validator)) {
|
||||
s = Status::Corruption("Paranoid checksums do not match");
|
||||
}
|
||||
}
|
||||
|
||||
delete iter;
|
||||
|
||||
if (!s.ok()) {
|
||||
output_status = s;
|
||||
subcompaction_state.status = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
for (size_t i = 1; i < compact_->sub_compact_states.size(); i++) {
|
||||
thread_pool.emplace_back(verify_table,
|
||||
std::ref(compact_->sub_compact_states[i].status));
|
||||
std::ref(compact_->sub_compact_states[i]));
|
||||
}
|
||||
verify_table(compact_->sub_compact_states[0].status);
|
||||
verify_table(compact_->sub_compact_states[0]);
|
||||
for (auto& thread : thread_pool) {
|
||||
thread.join();
|
||||
}
|
||||
@@ -974,6 +1099,15 @@ Status CompactionJob::Run() {
|
||||
|
||||
Status status = CollectSubcompactionErrors();
|
||||
|
||||
// If compaction was aborted or manually paused, clean up any output files
|
||||
// from completed subcompactions to prevent orphaned files on disk.
|
||||
// Skip cleanup for resumable compaction (when progress writer is set)
|
||||
// because the output files are needed for resumption.
|
||||
if ((status.IsCompactionAborted() || status.IsManualCompactionPaused()) &&
|
||||
compaction_progress_writer_ == nullptr) {
|
||||
CleanupAbortedSubcompactions();
|
||||
}
|
||||
|
||||
if (status.ok()) {
|
||||
status = SyncOutputDirectories();
|
||||
}
|
||||
@@ -982,6 +1116,9 @@ Status CompactionJob::Run() {
|
||||
status = VerifyOutputFiles();
|
||||
}
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK("CompactionJob::Run():AfterVerifyOutputFiles",
|
||||
&status);
|
||||
|
||||
if (status.ok()) {
|
||||
SetOutputTableProperties();
|
||||
}
|
||||
@@ -1385,10 +1522,10 @@ InternalIterator* CompactionJob::CreateInputIterator(
|
||||
return input;
|
||||
}
|
||||
|
||||
void CompactionJob::CreateBlobFileBuilder(SubcompactionState* sub_compact,
|
||||
ColumnFamilyData* cfd,
|
||||
BlobFileResources& blob_resources,
|
||||
const WriteOptions& write_options) {
|
||||
void CompactionJob::CreateBlobFileBuilder(
|
||||
SubcompactionState* sub_compact, ColumnFamilyData* cfd,
|
||||
std::unique_ptr<BlobFileBuilder>& blob_file_builder,
|
||||
const WriteOptions& write_options) {
|
||||
const auto& mutable_cf_options =
|
||||
sub_compact->compaction->mutable_cf_options();
|
||||
|
||||
@@ -1397,24 +1534,24 @@ void CompactionJob::CreateBlobFileBuilder(SubcompactionState* sub_compact,
|
||||
if (mutable_cf_options.enable_blob_files &&
|
||||
sub_compact->compaction->output_level() >=
|
||||
mutable_cf_options.blob_file_starting_level) {
|
||||
blob_resources.blob_file_builder = std::make_unique<BlobFileBuilder>(
|
||||
blob_file_builder = std::make_unique<BlobFileBuilder>(
|
||||
versions_, fs_.get(), &sub_compact->compaction->immutable_options(),
|
||||
&mutable_cf_options, &file_options_, &write_options, db_id_,
|
||||
db_session_id_, job_id_, cfd->GetID(), cfd->GetName(), write_hint_,
|
||||
io_tracer_, blob_callback_, BlobFileCreationReason::kCompaction,
|
||||
&blob_resources.blob_file_paths,
|
||||
sub_compact->Current().GetOutputFilePathsPtr(),
|
||||
sub_compact->Current().GetBlobFileAdditionsPtr());
|
||||
} else {
|
||||
blob_resources.blob_file_builder = nullptr;
|
||||
blob_file_builder = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<CompactionIterator> CompactionJob::CreateCompactionIterator(
|
||||
SubcompactionState* sub_compact, ColumnFamilyData* cfd,
|
||||
InternalIterator* input, const CompactionFilter* compaction_filter,
|
||||
MergeHelper& merge, BlobFileResources& blob_resources,
|
||||
MergeHelper& merge, std::unique_ptr<BlobFileBuilder>& blob_file_builder,
|
||||
const WriteOptions& write_options) {
|
||||
CreateBlobFileBuilder(sub_compact, cfd, blob_resources, write_options);
|
||||
CreateBlobFileBuilder(sub_compact, cfd, blob_file_builder, write_options);
|
||||
|
||||
const std::string* const full_history_ts_low =
|
||||
full_history_ts_low_.empty() ? nullptr : &full_history_ts_low_;
|
||||
@@ -1426,7 +1563,7 @@ std::unique_ptr<CompactionIterator> CompactionJob::CreateCompactionIterator(
|
||||
job_context_->earliest_write_conflict_snapshot,
|
||||
job_context_->GetJobSnapshotSequence(), job_context_->snapshot_checker,
|
||||
env_, ShouldReportDetailedTime(env_, stats_), sub_compact->RangeDelAgg(),
|
||||
blob_resources.blob_file_builder.get(), db_options_.allow_data_in_errors,
|
||||
blob_file_builder.get(), db_options_.allow_data_in_errors,
|
||||
db_options_.enforce_single_del_contracts, manual_compaction_canceled_,
|
||||
sub_compact->compaction
|
||||
->DoesInputReferenceBlobFiles() /* must_count_input_entries */,
|
||||
@@ -1450,11 +1587,11 @@ CompactionJob::CreateFileHandlers(SubcompactionState* sub_compact,
|
||||
const CompactionFileCloseFunc close_file_func =
|
||||
[this, sub_compact, start_user_key, end_user_key](
|
||||
const Status& status,
|
||||
const ParsedInternalKey& prev_table_last_internal_key,
|
||||
const ParsedInternalKey& prev_iter_output_internal_key,
|
||||
const Slice& next_table_min_key, const CompactionIterator* c_iter,
|
||||
CompactionOutputs& outputs) {
|
||||
return this->FinishCompactionOutputFile(
|
||||
status, prev_table_last_internal_key, next_table_min_key,
|
||||
status, prev_iter_output_internal_key, next_table_min_key,
|
||||
start_user_key, end_user_key, c_iter, sub_compact, outputs);
|
||||
};
|
||||
|
||||
@@ -1465,12 +1602,19 @@ Status CompactionJob::ProcessKeyValue(
|
||||
SubcompactionState* sub_compact, ColumnFamilyData* cfd,
|
||||
CompactionIterator* c_iter, const CompactionFileOpenFunc& open_file_func,
|
||||
const CompactionFileCloseFunc& close_file_func, uint64_t& prev_cpu_micros) {
|
||||
Status status;
|
||||
const uint64_t kRecordStatsEvery = 1000;
|
||||
// Cron interval for periodic operations: stats update, abort check,
|
||||
// and sync points. Uses 1024 (power of 2) for efficient bitwise check.
|
||||
const uint64_t kCronEveryMask = (1 << 10) - 1;
|
||||
[[maybe_unused]] const std::optional<const Slice> end = sub_compact->end;
|
||||
|
||||
IterKey last_output_key;
|
||||
ParsedInternalKey last_output_ikey;
|
||||
// Check for abort signal before starting key processing
|
||||
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
|
||||
return Status::Incomplete(Status::SubCode::kCompactionAborted);
|
||||
}
|
||||
|
||||
Status status;
|
||||
IterKey prev_iter_output_key;
|
||||
ParsedInternalKey prev_iter_output_internal_key;
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"CompactionJob::ProcessKeyValueCompaction()::Processing",
|
||||
@@ -1481,8 +1625,16 @@ Status CompactionJob::ProcessKeyValue(
|
||||
assert(!end.has_value() ||
|
||||
cfd->user_comparator()->Compare(c_iter->user_key(), *end) < 0);
|
||||
|
||||
if (c_iter->iter_stats().num_input_records % kRecordStatsEvery ==
|
||||
kRecordStatsEvery - 1) {
|
||||
const uint64_t num_records = c_iter->iter_stats().num_input_records;
|
||||
|
||||
// Periodic cron operations: stats update, abort check.
|
||||
if ((num_records & kCronEveryMask) == kCronEveryMask) {
|
||||
// Check for abort signal periodically
|
||||
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
|
||||
status = Status::Incomplete(Status::SubCode::kCompactionAborted);
|
||||
break;
|
||||
}
|
||||
|
||||
UpdateSubcompactionJobStatsIncrementally(
|
||||
c_iter, &sub_compact->compaction_job_stats,
|
||||
db_options_.clock->CPUMicros(), prev_cpu_micros);
|
||||
@@ -1521,9 +1673,9 @@ Status CompactionJob::ProcessKeyValue(
|
||||
// and `close_file_func`.
|
||||
// TODO: it would be better to have the compaction file open/close moved
|
||||
// into `CompactionOutputs` which has the output file information.
|
||||
status =
|
||||
sub_compact->AddToOutput(*c_iter, use_proximal_output, open_file_func,
|
||||
close_file_func, last_output_ikey);
|
||||
status = sub_compact->AddToOutput(*c_iter, use_proximal_output,
|
||||
open_file_func, close_file_func,
|
||||
prev_iter_output_internal_key);
|
||||
if (!status.ok()) {
|
||||
break;
|
||||
}
|
||||
@@ -1532,9 +1684,10 @@ Status CompactionJob::ProcessKeyValue(
|
||||
static_cast<void*>(const_cast<std::atomic<bool>*>(
|
||||
&manual_compaction_canceled_)));
|
||||
|
||||
last_output_key.SetInternalKey(c_iter->key(), &last_output_ikey);
|
||||
last_output_ikey.sequence = ikey.sequence;
|
||||
last_output_ikey.type = ikey.type;
|
||||
prev_iter_output_key.SetInternalKey(c_iter->key(),
|
||||
&prev_iter_output_internal_key);
|
||||
prev_iter_output_internal_key.sequence = ikey.sequence;
|
||||
prev_iter_output_internal_key.type = ikey.type;
|
||||
c_iter->Next();
|
||||
|
||||
#ifndef NDEBUG
|
||||
@@ -1688,6 +1841,7 @@ Status CompactionJob::FinalizeBlobFiles(SubcompactionState* sub_compact,
|
||||
}
|
||||
|
||||
void CompactionJob::ProcessKeyValueCompaction(SubcompactionState* sub_compact) {
|
||||
TEST_SYNC_POINT("CompactionJob::ProcessKeyValueCompaction:Start");
|
||||
assert(sub_compact);
|
||||
assert(sub_compact->compaction);
|
||||
|
||||
@@ -1741,11 +1895,11 @@ void CompactionJob::ProcessKeyValueCompaction(SubcompactionState* sub_compact) {
|
||||
false /* internal key corruption is expected */,
|
||||
job_context_->GetLatestSnapshotSequence(), job_context_->snapshot_checker,
|
||||
compact_->compaction->level(), db_options_.stats);
|
||||
BlobFileResources blob_resources;
|
||||
std::unique_ptr<BlobFileBuilder> blob_file_builder;
|
||||
|
||||
auto c_iter =
|
||||
CreateCompactionIterator(sub_compact, cfd, input_iter, compaction_filter,
|
||||
merge, blob_resources, write_options);
|
||||
merge, blob_file_builder, write_options);
|
||||
assert(c_iter);
|
||||
c_iter->SeekToFirst();
|
||||
|
||||
@@ -1763,9 +1917,8 @@ void CompactionJob::ProcessKeyValueCompaction(SubcompactionState* sub_compact) {
|
||||
status = FinalizeProcessKeyValueStatus(cfd, input_iter, c_iter.get(), status);
|
||||
|
||||
FinalizeSubcompaction(sub_compact, status, open_file_func, close_file_func,
|
||||
blob_resources.blob_file_builder.get(), c_iter.get(),
|
||||
input_iter, start_cpu_micros, prev_cpu_micros,
|
||||
io_stats);
|
||||
blob_file_builder.get(), c_iter.get(), input_iter,
|
||||
start_cpu_micros, prev_cpu_micros, io_stats);
|
||||
|
||||
NotifyOnSubcompactionCompleted(sub_compact);
|
||||
}
|
||||
@@ -1841,7 +1994,7 @@ void CompactionJob::RecordDroppedKeys(
|
||||
|
||||
Status CompactionJob::FinishCompactionOutputFile(
|
||||
const Status& input_status,
|
||||
const ParsedInternalKey& prev_table_last_internal_key,
|
||||
const ParsedInternalKey& prev_iter_output_internal_key,
|
||||
const Slice& next_table_min_key, const Slice* comp_start_user_key,
|
||||
const Slice* comp_end_user_key, const CompactionIterator* c_iter,
|
||||
SubcompactionState* sub_compact, CompactionOutputs& outputs) {
|
||||
@@ -2019,7 +2172,7 @@ Status CompactionJob::FinishCompactionOutputFile(
|
||||
}
|
||||
|
||||
if (s.ok() && ShouldUpdateSubcompactionProgress(sub_compact, c_iter,
|
||||
prev_table_last_internal_key,
|
||||
prev_iter_output_internal_key,
|
||||
next_table_min_key, meta)) {
|
||||
UpdateSubcompactionProgress(c_iter, next_table_min_key, sub_compact);
|
||||
s = PersistSubcompactionProgress(sub_compact);
|
||||
@@ -2030,10 +2183,10 @@ Status CompactionJob::FinishCompactionOutputFile(
|
||||
|
||||
bool CompactionJob::ShouldUpdateSubcompactionProgress(
|
||||
const SubcompactionState* sub_compact, const CompactionIterator* c_iter,
|
||||
const ParsedInternalKey& prev_table_last_internal_key,
|
||||
const ParsedInternalKey& prev_iter_output_internal_key,
|
||||
const Slice& next_table_min_internal_key, const FileMetaData* meta) const {
|
||||
const auto* cfd = sub_compact->compaction->column_family_data();
|
||||
// No need to update when the output will not get persisted
|
||||
// No need to update when the progress will not get persisted
|
||||
if (compaction_progress_writer_ == nullptr) {
|
||||
return false;
|
||||
}
|
||||
@@ -2057,19 +2210,24 @@ bool CompactionJob::ShouldUpdateSubcompactionProgress(
|
||||
}
|
||||
|
||||
// LIMITATION: Compaction progress persistence disabled for file boundaries
|
||||
// contaning range deletions. Range deletions can span file boundaries, making
|
||||
// it difficult (but possible) to ensure adjacent output tables have different
|
||||
// user keys. See the last check for why different users keys of adjacent
|
||||
// output tables are needed
|
||||
// containing range deletions. Range deletions can span file boundaries,
|
||||
// making it difficult to ensure adjacent output tables have different user
|
||||
// keys. See the last check for why different users keys of adjacent output
|
||||
// tables are needed
|
||||
const ValueType next_table_min_internal_key_type =
|
||||
ExtractValueType(next_table_min_internal_key);
|
||||
const ValueType prev_table_last_internal_key_type =
|
||||
prev_table_last_internal_key.user_key.empty()
|
||||
const ValueType prev_iter_output_internal_key_type =
|
||||
prev_iter_output_internal_key.user_key.empty()
|
||||
? ValueType::kTypeValue
|
||||
: prev_table_last_internal_key.type;
|
||||
: prev_iter_output_internal_key.type;
|
||||
|
||||
if (next_table_min_internal_key_type == ValueType::kTypeRangeDeletion ||
|
||||
prev_table_last_internal_key_type == ValueType::kTypeRangeDeletion) {
|
||||
// Range deletes truncated to align with file boundaries may be output by the
|
||||
// compaction iterator with `ValueType::kTypeMaxValid` instead of the original
|
||||
// type.
|
||||
if ((next_table_min_internal_key_type == ValueType::kTypeRangeDeletion ||
|
||||
next_table_min_internal_key_type == ValueType::kTypeMaxValid) ||
|
||||
(prev_iter_output_internal_key_type == ValueType::kTypeRangeDeletion ||
|
||||
prev_iter_output_internal_key_type == ValueType::kTypeMaxValid)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2079,9 +2237,9 @@ bool CompactionJob::ShouldUpdateSubcompactionProgress(
|
||||
const Slice next_table_min_user_key =
|
||||
ExtractUserKey(next_table_min_internal_key);
|
||||
const Slice prev_table_last_user_key =
|
||||
prev_table_last_internal_key.user_key.empty()
|
||||
prev_iter_output_internal_key.user_key.empty()
|
||||
? Slice()
|
||||
: prev_table_last_internal_key.user_key;
|
||||
: prev_iter_output_internal_key.user_key;
|
||||
|
||||
if (cfd->user_comparator()->EqualWithoutTimestamp(next_table_min_user_key,
|
||||
prev_table_last_user_key)) {
|
||||
@@ -2259,6 +2417,10 @@ Status CompactionJob::OpenCompactionOutputFile(SubcompactionState* sub_compact,
|
||||
Status s;
|
||||
IOStatus io_s = NewWritableFile(fs_.get(), fname, &writable_file, fo_copy);
|
||||
s = io_s;
|
||||
if (io_s.ok()) {
|
||||
// Track the SST file path for cleanup on abort.
|
||||
outputs.AddOutputFilePath(fname);
|
||||
}
|
||||
if (sub_compact->io_status.ok()) {
|
||||
sub_compact->io_status = io_s;
|
||||
// Since this error is really a copy of the io_s that is checked below as s,
|
||||
@@ -2335,8 +2497,16 @@ Status CompactionJob::OpenCompactionOutputFile(SubcompactionState* sub_compact,
|
||||
return s;
|
||||
}
|
||||
|
||||
// Enable hash computation if paranoid_file_checks is on or if
|
||||
// verify_output_flags includes kVerifyIteration, so that
|
||||
// VerifyOutputFiles() can compare the hash of the written data
|
||||
// against a re-read of the output file.
|
||||
bool enable_output_hash =
|
||||
paranoid_file_checks_ ||
|
||||
!!(sub_compact->compaction->mutable_cf_options().verify_output_flags &
|
||||
VerifyOutputFlags::kVerifyIteration);
|
||||
outputs.AddOutput(std::move(meta), cfd->internal_comparator(),
|
||||
paranoid_file_checks_);
|
||||
enable_output_hash);
|
||||
}
|
||||
|
||||
writable_file->SetIOPriority(GetRateLimiterPriority());
|
||||
@@ -2377,7 +2547,7 @@ Status CompactionJob::OpenCompactionOutputFile(SubcompactionState* sub_compact,
|
||||
|
||||
void CompactionJob::CleanupCompaction() {
|
||||
for (SubcompactionState& sub_compact : compact_->sub_compact_states) {
|
||||
sub_compact.Cleanup(table_cache_.get());
|
||||
sub_compact.Cleanup(table_cache_.get(), compact_->status);
|
||||
}
|
||||
delete compact_;
|
||||
compact_ = nullptr;
|
||||
@@ -2402,6 +2572,32 @@ bool CompactionJob::UpdateInternalStatsFromInputFiles(
|
||||
bool has_error = false;
|
||||
const ReadOptions read_options(Env::IOActivity::kCompaction);
|
||||
const auto& input_table_properties = compaction->GetInputTableProperties();
|
||||
|
||||
// Check all input files for old block-based SST format_version. Why? Old
|
||||
// block-based SST files from roughly version 5.0 to 5.18 could produce
|
||||
// inaccurate num_entries counts due to the evolution of its handling along
|
||||
// with num_range_deletions. We have to disable some paranoid checks when
|
||||
// compacting files from such an old release. However, we don't have great
|
||||
// information to identify those files, so we heuristically over-approximate
|
||||
// that set of files using
|
||||
// (a) format_version < 5, which will be true for any files from RocksDB <
|
||||
// 6.6.0 and should not be true for any recent production files
|
||||
// (b) to avoid including non-block-based SST files (which still use older
|
||||
// format_version markers, and do not support DeleteRange), we also require
|
||||
// the presence of the user property "rocksdb.block.based.table.index.type",
|
||||
// which was added in RocksDB 2.8 and is always present in block-based tables.
|
||||
for (const auto& tp_pair : input_table_properties) {
|
||||
if (tp_pair.second && tp_pair.second->format_version < 5) {
|
||||
// Check for block-based table by looking for its index type property
|
||||
const auto& user_props = tp_pair.second->user_collected_properties;
|
||||
if (user_props.find(BlockBasedTablePropertyNames::kIndexType) !=
|
||||
user_props.end()) {
|
||||
job_stats_->has_accurate_num_input_records = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int input_level = 0;
|
||||
input_level < static_cast<int>(compaction->num_input_levels());
|
||||
++input_level) {
|
||||
@@ -2646,7 +2842,10 @@ Status CompactionJob::ReadTablePropertiesDirectly(
|
||||
std::shared_ptr<const TableProperties>* tp) {
|
||||
std::unique_ptr<FSRandomAccessFile> file;
|
||||
std::string file_name = GetTableFileName(file_meta->fd.GetNumber());
|
||||
Status s = ioptions.fs->NewRandomAccessFile(file_name, file_options_, &file,
|
||||
FileOptions fopts = file_options_;
|
||||
fopts.file_checksum = file_meta->file_checksum;
|
||||
fopts.file_checksum_func_name = file_meta->file_checksum_func_name;
|
||||
Status s = ioptions.fs->NewRandomAccessFile(file_name, fopts, &file,
|
||||
nullptr /* dbg */);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
@@ -2740,12 +2939,17 @@ void CompactionJob::RestoreCompactionOutputs(
|
||||
|
||||
const auto& output_files = subcompaction_progress_per_level.GetOutputFiles();
|
||||
|
||||
const bool enable_output_hash =
|
||||
paranoid_file_checks_ ||
|
||||
!!(compact_->compaction->mutable_cf_options().verify_output_flags &
|
||||
VerifyOutputFlags::kVerifyIteration);
|
||||
|
||||
for (size_t i = 0; i < output_files.size(); i++) {
|
||||
FileMetaData file_copy = output_files[i];
|
||||
|
||||
outputs_to_restore->AddOutput(std::move(file_copy),
|
||||
cfd->internal_comparator(),
|
||||
paranoid_file_checks_, true /* finished */);
|
||||
enable_output_hash, true /* finished */);
|
||||
|
||||
outputs_to_restore->UpdateTableProperties(
|
||||
*output_files_table_properties[i]);
|
||||
@@ -2768,12 +2972,6 @@ void CompactionJob::RestoreCompactionOutputs(
|
||||
// - Status::NotFound(): No valid progress to resume from
|
||||
// - Status::Corruption(): Resume key is invalid, beyond input range, or output
|
||||
// restoration failed
|
||||
// - Other non-OK status: Iterator errors or file system issues during
|
||||
// restoration
|
||||
//
|
||||
// The caller must check for Status::IsIncomplete() to distinguish between
|
||||
// "no resume needed" (proceed with `InternalIterator::SeekToFirst()`) vs
|
||||
// "resume failed" scenarios.
|
||||
Status CompactionJob::MaybeResumeSubcompactionProgressOnInputIterator(
|
||||
SubcompactionState* sub_compact, InternalIterator* input_iter) {
|
||||
const ReadOptions read_options(Env::IOActivity::kCompaction);
|
||||
|
||||
@@ -142,6 +142,9 @@ class SubcompactionState;
|
||||
|
||||
class CompactionJob {
|
||||
public:
|
||||
// Constant false aborted flag, used for compaction service jobs
|
||||
static const std::atomic<int> kCompactionAbortedFalse;
|
||||
|
||||
CompactionJob(int job_id, Compaction* compaction,
|
||||
const ImmutableDBOptions& db_options,
|
||||
const MutableDBOptions& mutable_db_options,
|
||||
@@ -157,6 +160,7 @@ class CompactionJob {
|
||||
Env::Priority thread_pri,
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const std::atomic<bool>& manual_compaction_canceled,
|
||||
const std::atomic<int>& compaction_aborted,
|
||||
const std::string& db_id = "",
|
||||
const std::string& db_session_id = "",
|
||||
std::string full_history_ts_low = "", std::string trim_ts = "",
|
||||
@@ -250,6 +254,10 @@ class CompactionJob {
|
||||
// @param num_input_range_del if non-null, will be set to the number of range
|
||||
// deletion entries in this compaction input.
|
||||
//
|
||||
// If any input file has potentially unreliable num_entries count (old SST
|
||||
// files - details in implementation),
|
||||
// job_stats_->has_accurate_num_input_records is set to false.
|
||||
//
|
||||
// Returns true iff internal_stats_.output_level_stats.num_input_records and
|
||||
// num_input_range_del are calculated successfully.
|
||||
//
|
||||
@@ -299,6 +307,7 @@ class CompactionJob {
|
||||
void RunSubcompactions();
|
||||
void UpdateTimingStats(uint64_t start_micros);
|
||||
void RemoveEmptyOutputs();
|
||||
void CleanupAbortedSubcompactions();
|
||||
bool HasNewBlobFiles() const;
|
||||
Status CollectSubcompactionErrors();
|
||||
Status SyncOutputDirectories();
|
||||
@@ -363,11 +372,6 @@ class CompactionJob {
|
||||
std::unique_ptr<InternalIterator> trim_history_iter;
|
||||
};
|
||||
|
||||
struct BlobFileResources {
|
||||
std::vector<std::string> blob_file_paths;
|
||||
std::unique_ptr<BlobFileBuilder> blob_file_builder;
|
||||
};
|
||||
|
||||
bool ShouldUseLocalCompaction(SubcompactionState* sub_compact);
|
||||
CompactionIOStatsSnapshot InitializeIOStats();
|
||||
Status SetupAndValidateCompactionFilter(
|
||||
@@ -382,14 +386,14 @@ class CompactionJob {
|
||||
SubcompactionState* sub_compact, ColumnFamilyData* cfd,
|
||||
SubcompactionInternalIterators& iterators,
|
||||
SubcompactionKeyBoundaries& boundaries, ReadOptions& read_options);
|
||||
void CreateBlobFileBuilder(SubcompactionState* sub_compact,
|
||||
ColumnFamilyData* cfd,
|
||||
BlobFileResources& blob_resources,
|
||||
const WriteOptions& write_options);
|
||||
void CreateBlobFileBuilder(
|
||||
SubcompactionState* sub_compact, ColumnFamilyData* cfd,
|
||||
std::unique_ptr<BlobFileBuilder>& blob_file_builder,
|
||||
const WriteOptions& write_options);
|
||||
std::unique_ptr<CompactionIterator> CreateCompactionIterator(
|
||||
SubcompactionState* sub_compact, ColumnFamilyData* cfd,
|
||||
InternalIterator* input_iter, const CompactionFilter* compaction_filter,
|
||||
MergeHelper& merge, BlobFileResources& blob_resources,
|
||||
MergeHelper& merge, std::unique_ptr<BlobFileBuilder>& blob_file_builder,
|
||||
const WriteOptions& write_options);
|
||||
std::pair<CompactionFileOpenFunc, CompactionFileCloseFunc> CreateFileHandlers(
|
||||
SubcompactionState* sub_compact, SubcompactionKeyBoundaries& boundaries);
|
||||
@@ -430,7 +434,7 @@ class CompactionJob {
|
||||
|
||||
Status FinishCompactionOutputFile(
|
||||
const Status& input_status,
|
||||
const ParsedInternalKey& prev_table_last_internal_key,
|
||||
const ParsedInternalKey& prev_iter_output_internal_key,
|
||||
const Slice& next_table_min_key, const Slice* comp_start_user_key,
|
||||
const Slice* comp_end_user_key, const CompactionIterator* c_iter,
|
||||
SubcompactionState* sub_compact, CompactionOutputs& outputs);
|
||||
@@ -461,6 +465,7 @@ class CompactionJob {
|
||||
VersionSet* versions_;
|
||||
const std::atomic<bool>* shutting_down_;
|
||||
const std::atomic<bool>& manual_compaction_canceled_;
|
||||
const std::atomic<int>& compaction_aborted_;
|
||||
FSDirectory* db_directory_;
|
||||
FSDirectory* blob_output_directory_;
|
||||
InstrumentedMutex* db_mutex_;
|
||||
@@ -545,7 +550,7 @@ class CompactionJob {
|
||||
|
||||
bool ShouldUpdateSubcompactionProgress(
|
||||
const SubcompactionState* sub_compact, const CompactionIterator* c_iter,
|
||||
const ParsedInternalKey& prev_table_last_internal_key,
|
||||
const ParsedInternalKey& prev_iter_output_internal_key,
|
||||
const Slice& next_table_min_internal_key, const FileMetaData* meta) const;
|
||||
|
||||
void UpdateSubcompactionProgress(const CompactionIterator* c_iter,
|
||||
|
||||
@@ -82,7 +82,7 @@ class CompactionJobStatsTest : public testing::Test,
|
||||
std::string dbname_;
|
||||
std::string alternative_wal_dir_;
|
||||
Env* env_;
|
||||
DB* db_;
|
||||
std::unique_ptr<DB> db_;
|
||||
std::vector<ColumnFamilyHandle*> handles_;
|
||||
uint32_t max_subcompactions_;
|
||||
|
||||
@@ -123,7 +123,7 @@ class CompactionJobStatsTest : public testing::Test,
|
||||
static void SetUpTestCase() {}
|
||||
static void TearDownTestCase() {}
|
||||
|
||||
DBImpl* dbfull() { return static_cast_with_check<DBImpl>(db_); }
|
||||
DBImpl* dbfull() { return static_cast_with_check<DBImpl>(db_.get()); }
|
||||
|
||||
void CreateColumnFamilies(const std::vector<std::string>& cfs,
|
||||
const Options& options) {
|
||||
@@ -162,7 +162,8 @@ class CompactionJobStatsTest : public testing::Test,
|
||||
column_families.emplace_back(cfs[i], options[i]);
|
||||
}
|
||||
DBOptions db_opts = DBOptions(options[0]);
|
||||
return DB::Open(db_opts, dbname_, column_families, &handles_, &db_);
|
||||
auto s = DB::Open(db_opts, dbname_, column_families, &handles_, &db_);
|
||||
return s;
|
||||
}
|
||||
|
||||
Status TryReopenWithColumnFamilies(const std::vector<std::string>& cfs,
|
||||
@@ -179,8 +180,7 @@ class CompactionJobStatsTest : public testing::Test,
|
||||
delete h;
|
||||
}
|
||||
handles_.clear();
|
||||
delete db_;
|
||||
db_ = nullptr;
|
||||
db_.reset();
|
||||
}
|
||||
|
||||
void DestroyAndReopen(const Options& options) {
|
||||
@@ -743,7 +743,7 @@ TEST_P(CompactionJobStatsTest, CompactionJobStatsTest) {
|
||||
}
|
||||
|
||||
ASSERT_OK(Flush(1));
|
||||
ASSERT_OK(static_cast_with_check<DBImpl>(db_)->TEST_WaitForCompact());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
stats_checker->set_verify_next_comp_io_stats(true);
|
||||
std::atomic<bool> first_prepare_write(true);
|
||||
@@ -944,7 +944,7 @@ TEST_P(CompactionJobStatsTest, UniversalCompactionTest) {
|
||||
start_key += key_base) {
|
||||
MakeTableWithKeyValues(&rnd, start_key, start_key + key_base - 1, kKeySize,
|
||||
kValueSize, key_interval, compression_ratio, 1);
|
||||
ASSERT_OK(static_cast_with_check<DBImpl>(db_)->TEST_WaitForCompact());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
}
|
||||
ASSERT_EQ(stats_checker->NumberOfUnverifiedStats(), 0U);
|
||||
}
|
||||
|
||||
@@ -211,12 +211,12 @@ class CompactionJobTestBase : public testing::Test {
|
||||
table_cache_(NewLRUCache(50000, 16)),
|
||||
write_buffer_manager_(db_options_.db_write_buffer_size),
|
||||
versions_(new VersionSet(
|
||||
dbname_, &db_options_, env_options_, table_cache_.get(),
|
||||
&write_buffer_manager_, &write_controller_,
|
||||
dbname_, &db_options_, mutable_db_options_, env_options_,
|
||||
table_cache_.get(), &write_buffer_manager_, &write_controller_,
|
||||
/*block_cache_tracer=*/nullptr,
|
||||
/*io_tracer=*/nullptr, /*db_id=*/"", /*db_session_id=*/"",
|
||||
/*daily_offpeak_time_utc=*/"",
|
||||
/*error_handler=*/nullptr, /*read_only=*/false)),
|
||||
/*error_handler=*/nullptr, /*unchanging=*/false)),
|
||||
shutting_down_(false),
|
||||
mock_table_factory_(new mock::MockTableFactory()),
|
||||
error_handler_(nullptr, db_options_, &mutex_),
|
||||
@@ -546,13 +546,13 @@ class CompactionJobTestBase : public testing::Test {
|
||||
ASSERT_OK(s);
|
||||
db_options_.info_log = info_log;
|
||||
|
||||
versions_.reset(
|
||||
new VersionSet(dbname_, &db_options_, env_options_, table_cache_.get(),
|
||||
&write_buffer_manager_, &write_controller_,
|
||||
/*block_cache_tracer=*/nullptr, /*io_tracer=*/nullptr,
|
||||
test::kUnitTestDbId, /*db_session_id=*/"",
|
||||
/*daily_offpeak_time_utc=*/"",
|
||||
/*error_handler=*/nullptr, /*read_only=*/false));
|
||||
versions_.reset(new VersionSet(
|
||||
dbname_, &db_options_, mutable_db_options_, env_options_,
|
||||
table_cache_.get(), &write_buffer_manager_, &write_controller_,
|
||||
/*block_cache_tracer=*/nullptr, /*io_tracer=*/nullptr,
|
||||
test::kUnitTestDbId, /*db_session_id=*/"",
|
||||
/*daily_offpeak_time_utc=*/"",
|
||||
/*error_handler=*/nullptr, /*unchanging=*/false));
|
||||
compaction_job_stats_.Reset();
|
||||
|
||||
VersionEdit new_db;
|
||||
@@ -676,8 +676,8 @@ class CompactionJobTestBase : public testing::Test {
|
||||
&event_logger, false, false, dbname_, &compaction_job_stats_,
|
||||
Env::Priority::USER, nullptr /* IOTracer */,
|
||||
/*manual_compaction_canceled=*/kManualCompactionCanceledFalse,
|
||||
env_->GenerateUniqueId(), DBImpl::GenerateDbSessionId(nullptr),
|
||||
full_history_ts_low_);
|
||||
CompactionJob::kCompactionAbortedFalse, env_->GenerateUniqueId(),
|
||||
DBImpl::GenerateDbSessionId(nullptr), full_history_ts_low_);
|
||||
VerifyInitializationOfCompactionJobStats(compaction_job_stats_);
|
||||
|
||||
compaction_job.Prepare(std::nullopt /*subcompact to be computed*/);
|
||||
@@ -2409,449 +2409,6 @@ TEST_F(CompactionJobIOPriorityTest, GetRateLimiterPriority) {
|
||||
kMaxSequenceNumber, 1, false, {kInvalidBlobFileNumber}, true,
|
||||
Env::IO_LOW, Env::IO_LOW);
|
||||
}
|
||||
|
||||
class ResumableCompactionJobTest : public CompactionJobTestBase {
|
||||
public:
|
||||
ResumableCompactionJobTest()
|
||||
: CompactionJobTestBase(
|
||||
test::PerThreadDBPath("allow_resumption_job_test"),
|
||||
BytewiseComparator(), [](uint64_t /*ts*/) { return ""; },
|
||||
/*test_io_priority=*/false, TableTypeForTest::kBlockBasedTable) {}
|
||||
|
||||
protected:
|
||||
static constexpr const char* kCancelBeforeThisKey = "cancel_before_this_key";
|
||||
std::string progress_dir_ = "";
|
||||
bool enable_cancel_ = false;
|
||||
std::atomic<int> stop_count_{0};
|
||||
std::atomic<bool> cancel_{false};
|
||||
|
||||
void SetUp() override {
|
||||
CompactionJobTestBase::SetUp();
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionOutputs::ShouldStopBefore::manual_decision",
|
||||
[this](void* p) {
|
||||
auto* pair = static_cast<std::pair<bool*, const Slice>*>(p);
|
||||
*(pair->first) = true;
|
||||
|
||||
// Cancel after outputting a specific key
|
||||
if (enable_cancel_) {
|
||||
ParsedInternalKey parsed_key;
|
||||
if (ParseInternalKey(pair->second, &parsed_key, true).ok()) {
|
||||
if (parsed_key.user_key == kCancelBeforeThisKey) {
|
||||
cancel_.store(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
if (env_->FileExists(progress_dir_).ok()) {
|
||||
std::vector<std::string> files;
|
||||
EXPECT_OK(env_->GetChildren(progress_dir_, &files));
|
||||
for (const auto& file : files) {
|
||||
if (file != "." && file != "..") {
|
||||
EXPECT_OK(env_->DeleteFile(progress_dir_ + "/" + file));
|
||||
}
|
||||
}
|
||||
EXPECT_OK(env_->DeleteDir(progress_dir_));
|
||||
}
|
||||
|
||||
CompactionJobTestBase::TearDown();
|
||||
}
|
||||
|
||||
void NewDB() {
|
||||
if (env_->FileExists(progress_dir_).ok()) {
|
||||
std::vector<std::string> files;
|
||||
EXPECT_OK(env_->GetChildren(progress_dir_, &files));
|
||||
for (const auto& file : files) {
|
||||
if (file != "." && file != "..") {
|
||||
EXPECT_OK(env_->DeleteFile(progress_dir_ + "/" + file));
|
||||
}
|
||||
}
|
||||
EXPECT_OK(env_->DeleteDir(progress_dir_));
|
||||
}
|
||||
|
||||
CompactionJobTestBase::NewDB();
|
||||
|
||||
progress_dir_ = test::PerThreadDBPath("compaction_progress");
|
||||
ASSERT_OK(env_->CreateDirIfMissing(progress_dir_));
|
||||
}
|
||||
|
||||
void EnableCompactionCancel() { enable_cancel_ = true; }
|
||||
|
||||
void DisableCompactionCancel() {
|
||||
enable_cancel_ = false;
|
||||
cancel_.store(false);
|
||||
}
|
||||
|
||||
std::unique_ptr<log::Writer> CreateCompactionProgressWriter(
|
||||
const std::string& compaction_progress_file) {
|
||||
std::unique_ptr<FSWritableFile> file;
|
||||
EXPECT_OK(fs_->NewWritableFile(compaction_progress_file, FileOptions(),
|
||||
&file, nullptr));
|
||||
auto file_writer = std::make_unique<WritableFileWriter>(
|
||||
std::move(file), compaction_progress_file, FileOptions());
|
||||
auto compaction_progress_writer =
|
||||
std::make_unique<log::Writer>(std::move(file_writer), 0, false);
|
||||
return compaction_progress_writer;
|
||||
}
|
||||
|
||||
Status RunCompactionWithProgressTracking(
|
||||
const CompactionProgress& compaction_progress,
|
||||
log::Writer* compaction_progress_writer,
|
||||
std::vector<SequenceNumber> snapshots = {},
|
||||
std::shared_ptr<Statistics> stats = nullptr) {
|
||||
mutex_.Lock();
|
||||
|
||||
auto cfd = versions_->GetColumnFamilySet()->GetDefault();
|
||||
auto files = cfd->current()->storage_info()->LevelFiles(0);
|
||||
|
||||
db_options_.statistics = stats;
|
||||
db_options_.stats = db_options_.statistics.get();
|
||||
|
||||
std::vector<CompactionInputFiles> compaction_input_files;
|
||||
CompactionInputFiles level;
|
||||
level.level = 0;
|
||||
level.files = files;
|
||||
compaction_input_files.push_back(level);
|
||||
|
||||
Compaction compaction(
|
||||
cfd->current()->storage_info(), cfd->ioptions(),
|
||||
cfd->GetLatestMutableCFOptions(), mutable_db_options_,
|
||||
compaction_input_files, 1, mutable_cf_options_.target_file_size_base,
|
||||
mutable_cf_options_.max_compaction_bytes, 0, kNoCompression,
|
||||
cfd->GetLatestMutableCFOptions().compression_opts,
|
||||
Temperature::kUnknown, 0, {}, std::nullopt, nullptr,
|
||||
CompactionReason::kManualCompaction);
|
||||
compaction.FinalizeInputInfo(cfd->current());
|
||||
|
||||
LogBuffer log_buffer(InfoLogLevel::INFO_LEVEL, db_options_.info_log.get());
|
||||
EventLogger event_logger(db_options_.info_log.get());
|
||||
JobContext job_context(1, false);
|
||||
job_context.InitSnapshotContext(nullptr, nullptr, kMaxSequenceNumber,
|
||||
std::move(snapshots));
|
||||
CompactionJobStats job_stats;
|
||||
|
||||
CompactionJob compaction_job(
|
||||
0, &compaction, db_options_, mutable_db_options_, env_options_,
|
||||
versions_.get(), &shutting_down_, &log_buffer, nullptr, nullptr,
|
||||
nullptr, stats.get(), &mutex_, &error_handler_, &job_context,
|
||||
table_cache_, &event_logger, false, false, dbname_, &job_stats,
|
||||
Env::Priority::USER, nullptr, cancel_, env_->GenerateUniqueId(),
|
||||
DBImpl::GenerateDbSessionId(nullptr), "");
|
||||
|
||||
compaction_job.Prepare(std::nullopt, compaction_progress,
|
||||
compaction_progress_writer);
|
||||
mutex_.Unlock();
|
||||
|
||||
compaction_job.Run().PermitUncheckedError();
|
||||
EXPECT_OK(compaction_job.io_status());
|
||||
|
||||
mutex_.Lock();
|
||||
|
||||
bool compaction_released = false;
|
||||
Status s = compaction_job.Install(&compaction_released);
|
||||
|
||||
mutex_.Unlock();
|
||||
if (!compaction_released) {
|
||||
compaction.ReleaseCompactionFiles(s);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
SubcompactionProgress ReadAndParseProgress(
|
||||
const std::string& compaction_progress_file) {
|
||||
std::unique_ptr<FSSequentialFile> seq_file;
|
||||
EXPECT_OK(fs_->NewSequentialFile(compaction_progress_file, FileOptions(),
|
||||
&seq_file, nullptr));
|
||||
auto file_reader = std::make_unique<SequentialFileReader>(
|
||||
std::move(seq_file), compaction_progress_file, 0, nullptr);
|
||||
log::Reader reader(nullptr, std::move(file_reader), nullptr, true, 0);
|
||||
|
||||
SubcompactionProgressBuilder builder;
|
||||
std::string record;
|
||||
Slice slice;
|
||||
|
||||
while (reader.ReadRecord(&slice, &record)) {
|
||||
VersionEdit edit;
|
||||
if (!edit.DecodeFrom(slice).ok()) continue;
|
||||
builder.ProcessVersionEdit(edit);
|
||||
}
|
||||
|
||||
EXPECT_TRUE(builder.HasAccumulatedSubcompactionProgress());
|
||||
|
||||
return builder.GetAccumulatedSubcompactionProgress();
|
||||
}
|
||||
|
||||
// Test utility function to verify that compaction progress was correctly
|
||||
// persisted to the progress file after compaction interruption.
|
||||
//
|
||||
// VERIFIES:
|
||||
// - Progress file exists and has expected size (empty if no progress
|
||||
// expected)
|
||||
// - Next internal key to compact matches expected user key with proper format
|
||||
// - Number of processed input records matches position in ordered input keys
|
||||
// - Number of processed output records equals number of processed input
|
||||
// records (by test design to simplify verification)
|
||||
// - Each output file contains exactly one user key (by test design to
|
||||
// simplify verification)
|
||||
void VerifyCompactionProgressPersisted(
|
||||
const std::string& compaction_progress_file,
|
||||
const std::string& next_user_key_to_compact,
|
||||
const std::vector<std::string>& ordered_intput_keys) {
|
||||
ASSERT_OK(env_->FileExists(compaction_progress_file));
|
||||
|
||||
uint64_t file_size;
|
||||
ASSERT_OK(env_->GetFileSize(compaction_progress_file, &file_size));
|
||||
|
||||
if (next_user_key_to_compact.empty()) {
|
||||
ASSERT_EQ(file_size, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& subcompaction_progress =
|
||||
ReadAndParseProgress(compaction_progress_file);
|
||||
|
||||
ASSERT_FALSE(subcompaction_progress.next_internal_key_to_compact.empty());
|
||||
ParsedInternalKey parsed_next_key;
|
||||
ASSERT_OK(
|
||||
ParseInternalKey(subcompaction_progress.next_internal_key_to_compact,
|
||||
&parsed_next_key, true /* log_err_key */));
|
||||
ASSERT_EQ(parsed_next_key.user_key, next_user_key_to_compact);
|
||||
ASSERT_EQ(parsed_next_key.sequence, kMaxSequenceNumber);
|
||||
ASSERT_EQ(parsed_next_key.type, kValueTypeForSeek);
|
||||
|
||||
auto it = std::find(ordered_intput_keys.begin(), ordered_intput_keys.end(),
|
||||
next_user_key_to_compact);
|
||||
ASSERT_TRUE(it != ordered_intput_keys.end());
|
||||
|
||||
auto next_key_index = std::distance(ordered_intput_keys.begin(), it);
|
||||
|
||||
ASSERT_EQ(subcompaction_progress.num_processed_input_records,
|
||||
next_key_index);
|
||||
|
||||
ASSERT_EQ(subcompaction_progress.output_level_progress
|
||||
.GetNumProcessedOutputRecords(),
|
||||
next_key_index);
|
||||
|
||||
ASSERT_EQ(
|
||||
subcompaction_progress.output_level_progress.GetOutputFiles().size(),
|
||||
|
||||
next_key_index);
|
||||
|
||||
for (size_t i = 0;
|
||||
i <
|
||||
subcompaction_progress.output_level_progress.GetOutputFiles().size();
|
||||
++i) {
|
||||
const auto& output_file =
|
||||
subcompaction_progress.output_level_progress.GetOutputFiles()[i];
|
||||
ASSERT_EQ(output_file.smallest.user_key().ToString(),
|
||||
output_file.largest.user_key().ToString());
|
||||
ASSERT_EQ(output_file.largest.user_key().ToString(),
|
||||
ordered_intput_keys[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void RunCancelAndResumeTest(
|
||||
const std::initializer_list<mock::KVPair>& input_file_1,
|
||||
const std::initializer_list<mock::KVPair>& input_file_2,
|
||||
uint64_t last_sequence, const std::vector<uint64_t>& snapshots,
|
||||
const std::string& expected_next_key_to_compact,
|
||||
const std::vector<std::string>& expected_input_keys, bool exists_progress,
|
||||
bool cancelled_past_mid_point = false) {
|
||||
std::shared_ptr<Statistics> stats = ROCKSDB_NAMESPACE::CreateDBStatistics();
|
||||
|
||||
auto file1 = mock::MakeMockFile(input_file_1);
|
||||
AddMockFile(file1);
|
||||
auto file2 = mock::MakeMockFile(input_file_2);
|
||||
AddMockFile(file2);
|
||||
SetLastSequence(last_sequence);
|
||||
|
||||
// First compaction (will be cancelled)
|
||||
std::string compaction_progress_file =
|
||||
CompactionProgressFileName(progress_dir_, 123);
|
||||
std::unique_ptr<log::Writer> compaction_progress_writer =
|
||||
CreateCompactionProgressWriter(compaction_progress_file);
|
||||
|
||||
ASSERT_OK(stats->Reset());
|
||||
EnableCompactionCancel();
|
||||
|
||||
Status status = RunCompactionWithProgressTracking(
|
||||
CompactionProgress{}, compaction_progress_writer.get(), snapshots,
|
||||
stats);
|
||||
|
||||
ASSERT_TRUE(status.IsManualCompactionPaused());
|
||||
DisableCompactionCancel();
|
||||
|
||||
HistogramData cancelled_compaction_stats;
|
||||
stats->histogramData(FILE_WRITE_COMPACTION_MICROS,
|
||||
&cancelled_compaction_stats);
|
||||
|
||||
VerifyCompactionProgressPersisted(compaction_progress_file,
|
||||
expected_next_key_to_compact,
|
||||
expected_input_keys);
|
||||
|
||||
// Resume compaction
|
||||
CompactionProgress compaction_progress;
|
||||
if (exists_progress) {
|
||||
compaction_progress.push_back(
|
||||
ReadAndParseProgress(compaction_progress_file));
|
||||
}
|
||||
|
||||
std::string compaction_progress_file_2 =
|
||||
CompactionProgressFileName(progress_dir_, 234);
|
||||
std::unique_ptr<log::Writer> compaction_progress_writer_2 =
|
||||
CreateCompactionProgressWriter(compaction_progress_file_2);
|
||||
|
||||
ASSERT_OK(stats->Reset());
|
||||
|
||||
status = RunCompactionWithProgressTracking(
|
||||
compaction_progress, compaction_progress_writer_2.get(),
|
||||
{} /* snapshots */, stats);
|
||||
|
||||
ASSERT_OK(status);
|
||||
|
||||
if (cancelled_past_mid_point) {
|
||||
HistogramData resumed_compaction_stats;
|
||||
stats->histogramData(FILE_WRITE_COMPACTION_MICROS,
|
||||
&resumed_compaction_stats);
|
||||
ASSERT_GT(cancelled_compaction_stats.count,
|
||||
resumed_compaction_stats.count);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(ResumableCompactionJobTest, BasicProgressPersistence) {
|
||||
NewDB();
|
||||
|
||||
auto file1 = mock::MakeMockFile({
|
||||
{KeyStr("a", 1U, kTypeValue), "val1"},
|
||||
{KeyStr("b", 2U, kTypeValue), "val2"},
|
||||
});
|
||||
AddMockFile(file1);
|
||||
|
||||
auto file2 = mock::MakeMockFile({
|
||||
{KeyStr("c", 3U, kTypeValue), "val3"},
|
||||
{KeyStr("d", 4U, kTypeValue), "val4"},
|
||||
});
|
||||
AddMockFile(file2);
|
||||
|
||||
SetLastSequence(4U);
|
||||
|
||||
std::string compaction_progress_file =
|
||||
CompactionProgressFileName(progress_dir_, 123);
|
||||
|
||||
std::unique_ptr<log::Writer> compaction_progress_writer =
|
||||
CreateCompactionProgressWriter(compaction_progress_file);
|
||||
|
||||
Status status = RunCompactionWithProgressTracking(
|
||||
CompactionProgress(), compaction_progress_writer.get());
|
||||
|
||||
ASSERT_OK(status);
|
||||
|
||||
VerifyCompactionProgressPersisted(
|
||||
compaction_progress_file, "d" /* next_user_key_to_compact */,
|
||||
{"a", "b", "c", "d"} /* ordered_intput_keys */);
|
||||
}
|
||||
|
||||
TEST_F(ResumableCompactionJobTest, BasicProgressResume) {
|
||||
NewDB();
|
||||
|
||||
RunCancelAndResumeTest(
|
||||
{{KeyStr("a", 1U, kTypeValue), "val1"},
|
||||
{KeyStr("b", 2U, kTypeValue), "val2"}} /* input_file_1 */,
|
||||
{{KeyStr("bb", 3U, kTypeValue), "val3"},
|
||||
{KeyStr(kCancelBeforeThisKey, 4U, kTypeValue),
|
||||
"val4"}} /* input_file_2 */,
|
||||
4U /* last_sequence */, {} /* snapshots */,
|
||||
kCancelBeforeThisKey /* expected_next_key_to_compact */,
|
||||
{"a", "b", "bb", kCancelBeforeThisKey} /* expected_input_keys */,
|
||||
true /* exists_progress */, true /* cancelled_past_mid_point*/);
|
||||
}
|
||||
|
||||
TEST_F(ResumableCompactionJobTest, NoProgressResumeOnSameKey) {
|
||||
NewDB();
|
||||
|
||||
RunCancelAndResumeTest(
|
||||
{{KeyStr(kCancelBeforeThisKey, 1U, kTypeValue),
|
||||
"val1"}} /* input_file_1 */,
|
||||
{{KeyStr(kCancelBeforeThisKey, 2U, kTypeValue),
|
||||
"val2"}} /* input_file_2 */,
|
||||
2U /* last_sequence */, {1U} /* snapshots */,
|
||||
"" /* expected_next_key_to_compact */,
|
||||
{kCancelBeforeThisKey, kCancelBeforeThisKey} /* expected_input_keys */,
|
||||
false /* exists_progress */);
|
||||
}
|
||||
|
||||
TEST_F(ResumableCompactionJobTest, NoProgressResumeOnDeleteRange) {
|
||||
NewDB();
|
||||
|
||||
RunCancelAndResumeTest(
|
||||
{{KeyStr(kCancelBeforeThisKey, 1U, kTypeValue),
|
||||
"val1"}} /* input_file_1 */,
|
||||
{{KeyStr(kCancelBeforeThisKey, 2U, kTypeRangeDeletion),
|
||||
"val2"}} /* input_file_2 */,
|
||||
2U /* last_sequence */, {1U} /* snapshots */,
|
||||
"" /* expected_next_key_to_compact */,
|
||||
{kCancelBeforeThisKey, kCancelBeforeThisKey} /* expected_input_keys */,
|
||||
false /* exists_progress */);
|
||||
}
|
||||
|
||||
TEST_F(ResumableCompactionJobTest, NoProgressResumeOnMerge) {
|
||||
merge_op_ = MergeOperators::CreateStringAppendOperator();
|
||||
NewDB();
|
||||
|
||||
RunCancelAndResumeTest(
|
||||
{{KeyStr("a", 1U, kTypeValue), "val1"},
|
||||
{KeyStr("b", 2U, kTypeValue), "val2"}} /* input_file_1 */,
|
||||
{{KeyStr("bb", 3U, kTypeValue), "val3"},
|
||||
{KeyStr(kCancelBeforeThisKey, 4U, kTypeMerge),
|
||||
"val4"}} /* input_file_2 */,
|
||||
4U /* last_sequence */, {} /* snapshots */,
|
||||
"bb" /* expected_next_key_to_compact */,
|
||||
{"a", "b", "bb", kCancelBeforeThisKey} /* expected_input_keys */,
|
||||
true /* exists_progress */);
|
||||
}
|
||||
|
||||
TEST_F(ResumableCompactionJobTest, NoProgressResumeOnSingleDelete) {
|
||||
NewDB();
|
||||
|
||||
RunCancelAndResumeTest(
|
||||
{{KeyStr("a", 1U, kTypeValue), "val1"},
|
||||
{KeyStr("b", 2U, kTypeValue), "val2"},
|
||||
{KeyStr(kCancelBeforeThisKey, 3U, kTypeValue),
|
||||
"val3"}} /* input_file_1 */,
|
||||
{{KeyStr(kCancelBeforeThisKey, 4U, kTypeSingleDeletion), ""},
|
||||
{KeyStr("d", 5U, kTypeValue), "val4"}} /* input_file_2 */,
|
||||
5U /* last_sequence */, {3U} /* snapshots */,
|
||||
"b" /* expected_next_key_to_compact */,
|
||||
{"a", "b", kCancelBeforeThisKey, kCancelBeforeThisKey,
|
||||
"d"} /* expected_input_keys */,
|
||||
true /* exists_progress */);
|
||||
}
|
||||
|
||||
TEST_F(ResumableCompactionJobTest, NoProgressResumeOnDeletionAtBottom) {
|
||||
NewDB();
|
||||
|
||||
RunCancelAndResumeTest(
|
||||
{{KeyStr("a", 1U, kTypeValue), "val1"},
|
||||
{KeyStr("b", 2U, kTypeValue), "val2"},
|
||||
{KeyStr(kCancelBeforeThisKey, 3U, kTypeValue),
|
||||
"val3"}} /* input_file_1 */,
|
||||
{{KeyStr(kCancelBeforeThisKey, 4U, kTypeDeletion), ""},
|
||||
{KeyStr("d", 5U, kTypeValue), "val4"}} /* input_file_2 */,
|
||||
5U /* last_sequence */, {3U} /* snapshots */,
|
||||
"b" /* expected_next_key_to_compact */,
|
||||
{"a", "b", kCancelBeforeThisKey, kCancelBeforeThisKey,
|
||||
"d"} /* expected_input_keys */,
|
||||
true /* exists_progress */);
|
||||
}
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -49,8 +49,10 @@ Status CompactionOutputs::Finish(
|
||||
meta->fd.file_size = current_bytes;
|
||||
meta->tail_size = builder_->GetTailSize();
|
||||
meta->marked_for_compaction = builder_->NeedCompact();
|
||||
meta->user_defined_timestamps_persisted = static_cast<bool>(
|
||||
builder_->GetTableProperties().user_defined_timestamps_persisted);
|
||||
const TableProperties& tp = builder_->GetTableProperties();
|
||||
meta->user_defined_timestamps_persisted =
|
||||
static_cast<bool>(tp.user_defined_timestamps_persisted);
|
||||
ExtractTimestampFromTableProperties(tp, meta);
|
||||
}
|
||||
current_output().finished = true;
|
||||
stats_.bytes_written += current_bytes;
|
||||
@@ -278,7 +280,11 @@ bool CompactionOutputs::ShouldStopBefore(const CompactionIterator& c_iter) {
|
||||
}
|
||||
|
||||
// reach the max file size
|
||||
if (current_output_file_size_ >= compaction_->max_output_file_size()) {
|
||||
uint64_t estimated_file_size = current_output_file_size_;
|
||||
if (compaction_->mutable_cf_options().target_file_size_is_upper_bound) {
|
||||
estimated_file_size += builder_->EstimatedTailSize();
|
||||
}
|
||||
if (estimated_file_size >= compaction_->max_output_file_size()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -360,7 +366,7 @@ Status CompactionOutputs::AddToOutput(
|
||||
const CompactionIterator& c_iter,
|
||||
const CompactionFileOpenFunc& open_file_func,
|
||||
const CompactionFileCloseFunc& close_file_func,
|
||||
const ParsedInternalKey& prev_table_last_internal_key) {
|
||||
const ParsedInternalKey& prev_iter_output_internal_key) {
|
||||
Status s;
|
||||
bool is_range_del = c_iter.IsDeleteRangeSentinelKey();
|
||||
if (is_range_del && compaction_->bottommost_level()) {
|
||||
@@ -371,8 +377,8 @@ Status CompactionOutputs::AddToOutput(
|
||||
}
|
||||
const Slice& key = c_iter.key();
|
||||
if (ShouldStopBefore(c_iter) && HasBuilder()) {
|
||||
s = close_file_func(c_iter.InputStatus(), prev_table_last_internal_key, key,
|
||||
&c_iter, *this);
|
||||
s = close_file_func(c_iter.InputStatus(), prev_iter_output_internal_key,
|
||||
key, &c_iter, *this);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -84,6 +84,19 @@ class CompactionOutputs {
|
||||
|
||||
bool HasBlobFileAdditions() const { return !blob_file_additions_.empty(); }
|
||||
|
||||
// Get all file paths (SST and blob) created during compaction.
|
||||
const std::vector<std::string>& GetOutputFilePaths() const {
|
||||
return output_file_paths_;
|
||||
}
|
||||
|
||||
std::vector<std::string>* GetOutputFilePathsPtr() {
|
||||
return &output_file_paths_;
|
||||
}
|
||||
|
||||
void AddOutputFilePath(const std::string& path) {
|
||||
output_file_paths_.push_back(path);
|
||||
}
|
||||
|
||||
BlobGarbageMeter* CreateBlobGarbageMeter() {
|
||||
assert(!is_proximal_level_);
|
||||
blob_garbage_meter_ = std::make_unique<BlobGarbageMeter>();
|
||||
@@ -262,7 +275,7 @@ class CompactionOutputs {
|
||||
Status AddToOutput(const CompactionIterator& c_iter,
|
||||
const CompactionFileOpenFunc& open_file_func,
|
||||
const CompactionFileCloseFunc& close_file_func,
|
||||
const ParsedInternalKey& prev_table_last_internal_key);
|
||||
const ParsedInternalKey& prev_iter_output_internal_key);
|
||||
|
||||
// Close the current output. `open_file_func` is needed for creating new file
|
||||
// for range-dels only output file.
|
||||
@@ -321,6 +334,12 @@ class CompactionOutputs {
|
||||
std::vector<BlobFileAddition> blob_file_additions_;
|
||||
std::unique_ptr<BlobGarbageMeter> blob_garbage_meter_;
|
||||
|
||||
// All file paths (SST and blob) created during compaction.
|
||||
// Used for cleanup on abort - ensures orphan files are deleted even if
|
||||
// they were removed from outputs_ or blob_file_additions_ (e.g., by
|
||||
// RemoveLastEmptyOutput when file_size is 0 because builder was abandoned).
|
||||
std::vector<std::string> output_file_paths_;
|
||||
|
||||
// Per level's output stat
|
||||
InternalStats::CompactionStats stats_;
|
||||
|
||||
|
||||
@@ -27,12 +27,68 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
bool FindIntraL0Compaction(const std::vector<FileMetaData*>& level_files,
|
||||
size_t min_files_to_compact,
|
||||
uint64_t max_compact_bytes_per_del_file,
|
||||
uint64_t max_compaction_bytes,
|
||||
CompactionInputFiles* comp_inputs) {
|
||||
TEST_SYNC_POINT("FindIntraL0Compaction");
|
||||
#ifndef NDEBUG
|
||||
static void AssertCleanCut(const InternalKeyComparator* icmp,
|
||||
VersionStorageInfo* vstorage,
|
||||
CompactionInputFiles* inputs, int level,
|
||||
Logger* logger) {
|
||||
const std::vector<FileMetaData*>& level_files = vstorage->LevelFiles(level);
|
||||
if (inputs->files.empty() || level_files.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Comparator* ucmp = icmp->user_comparator();
|
||||
|
||||
// Find first and last input file indices in level
|
||||
int first_input_idx = -1;
|
||||
int last_input_idx = -1;
|
||||
for (size_t i = 0; i < level_files.size(); i++) {
|
||||
if (level_files[i] == inputs->files.front()) {
|
||||
first_input_idx = static_cast<int>(i);
|
||||
}
|
||||
if (level_files[i] == inputs->files.back()) {
|
||||
last_input_idx = static_cast<int>(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Check file before first input
|
||||
if (first_input_idx > 0) {
|
||||
const FileMetaData* prev_file = level_files[first_input_idx - 1];
|
||||
const FileMetaData* first_file = inputs->files.front();
|
||||
int cmp = sstableKeyCompare(ucmp, prev_file->largest, first_file->smallest);
|
||||
if (cmp == 0) {
|
||||
ROCKS_LOG_ERROR(logger,
|
||||
"Clean cut violated: L%d unselected file %" PRIu64
|
||||
" adjacent to first selected file %" PRIu64,
|
||||
level, prev_file->fd.GetNumber(),
|
||||
first_file->fd.GetNumber());
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Check file after last input
|
||||
if (last_input_idx >= 0 &&
|
||||
static_cast<size_t>(last_input_idx) < level_files.size() - 1) {
|
||||
const FileMetaData* last_file = inputs->files.back();
|
||||
const FileMetaData* next_file = level_files[last_input_idx + 1];
|
||||
int cmp = sstableKeyCompare(ucmp, last_file->largest, next_file->smallest);
|
||||
if (cmp == 0) {
|
||||
ROCKS_LOG_ERROR(logger,
|
||||
"Clean cut violated: L%d unselected file %" PRIu64
|
||||
" adjacent to last selected file %" PRIu64,
|
||||
level, next_file->fd.GetNumber(),
|
||||
last_file->fd.GetNumber());
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // NDEBUG
|
||||
|
||||
bool PickCostBasedIntraL0Compaction(
|
||||
const std::vector<FileMetaData*>& level_files, size_t min_files_to_compact,
|
||||
uint64_t max_compact_bytes_per_del_file, uint64_t max_compaction_bytes,
|
||||
CompactionInputFiles* comp_inputs) {
|
||||
TEST_SYNC_POINT("PickCostBasedIntraL0Compaction");
|
||||
|
||||
size_t start = 0;
|
||||
|
||||
@@ -242,7 +298,7 @@ bool CompactionPicker::ExpandInputsToCleanCut(const std::string& /*cf_name*/,
|
||||
GetRange(*inputs, &smallest, &largest);
|
||||
inputs->clear();
|
||||
vstorage->GetOverlappingInputs(level, &smallest, &largest, &inputs->files,
|
||||
hint_index, &hint_index, true,
|
||||
hint_index, &hint_index, true, nullptr,
|
||||
next_smallest);
|
||||
} while (inputs->size() > old_size);
|
||||
|
||||
@@ -250,6 +306,10 @@ bool CompactionPicker::ExpandInputsToCleanCut(const std::string& /*cf_name*/,
|
||||
// inputs. thus, inputs should be non-empty here
|
||||
assert(!inputs->empty());
|
||||
|
||||
#ifndef NDEBUG
|
||||
AssertCleanCut(icmp_, vstorage, inputs, level, ioptions_.logger);
|
||||
#endif // NDEBUG
|
||||
|
||||
// If, after the expansion, there are files that are already under
|
||||
// compaction, then we must drop/cancel this compaction.
|
||||
if (AreFilesInCompaction(inputs->files)) {
|
||||
@@ -465,7 +525,8 @@ bool CompactionPicker::SetupOtherInputs(
|
||||
const std::string& cf_name, const MutableCFOptions& mutable_cf_options,
|
||||
VersionStorageInfo* vstorage, CompactionInputFiles* inputs,
|
||||
CompactionInputFiles* output_level_inputs, int* parent_index,
|
||||
int base_index, bool only_expand_towards_right) {
|
||||
int base_index, bool only_expand_towards_right,
|
||||
const FileMetaData* starting_l0_file) {
|
||||
assert(!inputs->empty());
|
||||
assert(output_level_inputs->empty());
|
||||
const int input_level = inputs->level;
|
||||
@@ -521,11 +582,11 @@ bool CompactionPicker::SetupOtherInputs(
|
||||
// Round-robin compaction only allows expansion towards the larger side.
|
||||
vstorage->GetOverlappingInputs(input_level, &smallest, &all_limit,
|
||||
&expanded_inputs.files, base_index,
|
||||
nullptr);
|
||||
nullptr, true, starting_l0_file);
|
||||
} else {
|
||||
vstorage->GetOverlappingInputs(input_level, &all_start, &all_limit,
|
||||
&expanded_inputs.files, base_index,
|
||||
nullptr);
|
||||
nullptr, true, starting_l0_file);
|
||||
}
|
||||
uint64_t expanded_inputs_size = TotalFileSize(expanded_inputs.files);
|
||||
if (!ExpandInputsToCleanCut(cf_name, vstorage, &expanded_inputs)) {
|
||||
@@ -610,7 +671,8 @@ Compaction* CompactionPicker::PickCompactionForCompactRange(
|
||||
int input_level, int output_level,
|
||||
const CompactRangeOptions& compact_range_options, const InternalKey* begin,
|
||||
const InternalKey* end, InternalKey** compaction_end, bool* manual_conflict,
|
||||
uint64_t max_file_num_to_ignore, const std::string& trim_ts) {
|
||||
uint64_t max_file_num_to_ignore, const std::string& trim_ts,
|
||||
const std::string& full_history_ts_low) {
|
||||
// CompactionPickerFIFO has its own implementation of compact range
|
||||
assert(ioptions_.compaction_style != kCompactionStyleFIFO);
|
||||
|
||||
@@ -689,7 +751,8 @@ Compaction* CompactionPicker::PickCompactionForCompactRange(
|
||||
compact_range_options.blob_garbage_collection_age_cutoff);
|
||||
|
||||
RegisterCompaction(c);
|
||||
vstorage->ComputeCompactionScore(ioptions_, mutable_cf_options);
|
||||
vstorage->ComputeCompactionScore(ioptions_, mutable_cf_options,
|
||||
full_history_ts_low);
|
||||
return c;
|
||||
}
|
||||
|
||||
@@ -886,7 +949,8 @@ Compaction* CompactionPicker::PickCompactionForCompactRange(
|
||||
// takes running compactions into account (by skipping files that are already
|
||||
// being compacted). Since we just changed compaction score, we recalculate it
|
||||
// here
|
||||
vstorage->ComputeCompactionScore(ioptions_, mutable_cf_options);
|
||||
vstorage->ComputeCompactionScore(ioptions_, mutable_cf_options,
|
||||
full_history_ts_low);
|
||||
|
||||
return compaction;
|
||||
}
|
||||
@@ -1231,7 +1295,7 @@ void CompactionPicker::PickFilesMarkedForCompaction(
|
||||
|
||||
bool CompactionPicker::GetOverlappingL0Files(
|
||||
VersionStorageInfo* vstorage, CompactionInputFiles* start_level_inputs,
|
||||
int output_level, int* parent_index) {
|
||||
int output_level, int* parent_index, const FileMetaData* starting_l0_file) {
|
||||
// Two level 0 compaction won't run at the same time, so don't need to worry
|
||||
// about files on level 0 being compacted.
|
||||
assert(level0_compactions_in_progress()->empty());
|
||||
@@ -1242,7 +1306,11 @@ bool CompactionPicker::GetOverlappingL0Files(
|
||||
// which will include the picked file.
|
||||
start_level_inputs->files.clear();
|
||||
vstorage->GetOverlappingInputs(0, &smallest, &largest,
|
||||
&(start_level_inputs->files));
|
||||
&(start_level_inputs->files),
|
||||
/*hint_index=*/-1,
|
||||
/*file_index=*/nullptr,
|
||||
/*expand_range=*/true,
|
||||
/*starting_l0_file=*/starting_l0_file);
|
||||
|
||||
// If we include more L0 files in the same compaction run it can
|
||||
// cause the 'smallest' and 'largest' key to get extended to a
|
||||
|
||||
@@ -65,7 +65,8 @@ class CompactionPicker {
|
||||
const MutableDBOptions& mutable_db_options,
|
||||
const std::vector<SequenceNumber>& existing_snapshots,
|
||||
const SnapshotChecker* snapshot_checker, VersionStorageInfo* vstorage,
|
||||
LogBuffer* log_buffer, bool require_max_output_level) = 0;
|
||||
LogBuffer* log_buffer, const std::string& full_history_ts_low,
|
||||
bool require_max_output_level = false) = 0;
|
||||
|
||||
// The returned Compaction might not include the whole requested range.
|
||||
// In that case, compaction_end will be set to the next key that needs
|
||||
@@ -82,7 +83,8 @@ class CompactionPicker {
|
||||
const CompactRangeOptions& compact_range_options,
|
||||
const InternalKey* begin, const InternalKey* end,
|
||||
InternalKey** compaction_end, bool* manual_conflict,
|
||||
uint64_t max_file_num_to_ignore, const std::string& trim_ts);
|
||||
uint64_t max_file_num_to_ignore, const std::string& trim_ts,
|
||||
const std::string& full_history_ts_low);
|
||||
|
||||
// The maximum allowed output level. Default value is NumberLevels() - 1.
|
||||
virtual int MaxOutputLevel() const { return NumberLevels() - 1; }
|
||||
@@ -203,13 +205,16 @@ class CompactionPicker {
|
||||
const std::vector<CompactionInputFiles>& inputs, int level,
|
||||
int proximal_level) const;
|
||||
|
||||
// @param starting_l0_file If not null, restricts L0 file selection to only
|
||||
// include files at or older than starting_l0_file.
|
||||
bool SetupOtherInputs(const std::string& cf_name,
|
||||
const MutableCFOptions& mutable_cf_options,
|
||||
VersionStorageInfo* vstorage,
|
||||
CompactionInputFiles* inputs,
|
||||
CompactionInputFiles* output_level_inputs,
|
||||
int* parent_index, int base_index,
|
||||
bool only_expand_towards_right = false);
|
||||
bool only_expand_towards_right = false,
|
||||
const FileMetaData* starting_l0_file = nullptr);
|
||||
|
||||
void GetGrandparents(VersionStorageInfo* vstorage,
|
||||
const CompactionInputFiles& inputs,
|
||||
@@ -222,9 +227,12 @@ class CompactionPicker {
|
||||
CompactionInputFiles* start_level_inputs,
|
||||
std::function<bool(const FileMetaData*)> skip_marked_file);
|
||||
|
||||
// @param starting_l0_file If not null, restricts L0 file selection to only
|
||||
// include files at or older than starting_l0_file.
|
||||
bool GetOverlappingL0Files(VersionStorageInfo* vstorage,
|
||||
CompactionInputFiles* start_level_inputs,
|
||||
int output_level, int* parent_index);
|
||||
int output_level, int* parent_index,
|
||||
const FileMetaData* starting_l0_file = nullptr);
|
||||
|
||||
// Register this compaction in the set of running compactions
|
||||
void RegisterCompaction(Compaction* c);
|
||||
@@ -278,7 +286,8 @@ class NullCompactionPicker : public CompactionPicker {
|
||||
const std::vector<SequenceNumber>& /*existing_snapshots*/,
|
||||
const SnapshotChecker* /*snapshot_checker*/,
|
||||
VersionStorageInfo* /*vstorage*/, LogBuffer* /* log_buffer */,
|
||||
bool /*require_max_output_level*/ = false) override {
|
||||
const std::string& /*full_history_ts_low*/,
|
||||
bool /*require_max_output_level*/) override {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -292,8 +301,8 @@ class NullCompactionPicker : public CompactionPicker {
|
||||
const CompactRangeOptions& /*compact_range_options*/,
|
||||
const InternalKey* /*begin*/, const InternalKey* /*end*/,
|
||||
InternalKey** /*compaction_end*/, bool* /*manual_conflict*/,
|
||||
uint64_t /*max_file_num_to_ignore*/,
|
||||
const std::string& /*trim_ts*/) override {
|
||||
uint64_t /*max_file_num_to_ignore*/, const std::string& /*trim_ts*/,
|
||||
const std::string& /*full_history_ts_low*/) override {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -319,11 +328,10 @@ class NullCompactionPicker : public CompactionPicker {
|
||||
// files. Cannot be nullptr.
|
||||
//
|
||||
// @return true iff compaction was found.
|
||||
bool FindIntraL0Compaction(const std::vector<FileMetaData*>& level_files,
|
||||
size_t min_files_to_compact,
|
||||
uint64_t max_compact_bytes_per_del_file,
|
||||
uint64_t max_compaction_bytes,
|
||||
CompactionInputFiles* comp_inputs);
|
||||
bool PickCostBasedIntraL0Compaction(
|
||||
const std::vector<FileMetaData*>& level_files, size_t min_files_to_compact,
|
||||
uint64_t max_compact_bytes_per_del_file, uint64_t max_compaction_bytes,
|
||||
CompactionInputFiles* comp_inputs);
|
||||
|
||||
CompressionType GetCompressionType(const VersionStorageInfo* vstorage,
|
||||
const MutableCFOptions& mutable_cf_options,
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include "db/compaction/compaction_picker_fifo.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -31,6 +32,29 @@ uint64_t GetTotalFilesSize(const std::vector<FileMetaData*>& files) {
|
||||
}
|
||||
return total_size;
|
||||
}
|
||||
|
||||
// Compute effective data size and capacity limit for FIFO compaction.
|
||||
// When max_data_files_size > 0 (blob-aware mode), the effective size includes
|
||||
// both SST and blob file sizes, and the limit is max_data_files_size.
|
||||
// Otherwise, only SST sizes are used with max_table_files_size as the limit.
|
||||
void GetEffectiveSizeAndLimit(const CompactionOptionsFIFO& fifo_opts,
|
||||
uint64_t total_sst_size, uint64_t total_blob_size,
|
||||
uint64_t* effective_size,
|
||||
uint64_t* effective_max) {
|
||||
*effective_size = total_sst_size;
|
||||
*effective_max = fifo_opts.max_table_files_size;
|
||||
if (fifo_opts.max_data_files_size > 0) {
|
||||
*effective_size += total_blob_size;
|
||||
*effective_max = fifo_opts.max_data_files_size;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the effective capacity limit for FIFO compaction.
|
||||
// Convenience wrapper when only the limit is needed (e.g., PickTTLCompaction).
|
||||
uint64_t GetEffectiveMax(const CompactionOptionsFIFO& fifo_opts) {
|
||||
return fifo_opts.max_data_files_size > 0 ? fifo_opts.max_data_files_size
|
||||
: fifo_opts.max_table_files_size;
|
||||
}
|
||||
} // anonymous namespace
|
||||
|
||||
bool FIFOCompactionPicker::NeedsCompaction(
|
||||
@@ -78,10 +102,10 @@ Compaction* FIFOCompactionPicker::PickTTLCompaction(
|
||||
for (auto ritr = level_files.rbegin(); ritr != level_files.rend(); ++ritr) {
|
||||
FileMetaData* f = *ritr;
|
||||
assert(f);
|
||||
if (f->fd.table_reader && f->fd.table_reader->GetTableProperties()) {
|
||||
TableReader* reader = f->fd.pinned_reader.Get();
|
||||
if (reader != nullptr && reader->GetTableProperties() != nullptr) {
|
||||
uint64_t newest_key_time = f->TryGetNewestKeyTime();
|
||||
uint64_t creation_time =
|
||||
f->fd.table_reader->GetTableProperties()->creation_time;
|
||||
uint64_t creation_time = reader->GetTableProperties()->creation_time;
|
||||
uint64_t est_newest_key_time = newest_key_time == kUnknownNewestKeyTime
|
||||
? creation_time
|
||||
: newest_key_time;
|
||||
@@ -98,10 +122,43 @@ Compaction* FIFOCompactionPicker::PickTTLCompaction(
|
||||
// Return a nullptr and proceed to size-based FIFO compaction if:
|
||||
// 1. there are no files older than ttl OR
|
||||
// 2. there are a few files older than ttl, but deleting them will not bring
|
||||
// the total size to be less than max_table_files_size threshold.
|
||||
if (inputs[0].files.empty() ||
|
||||
total_size >
|
||||
mutable_cf_options.compaction_options_fifo.max_table_files_size) {
|
||||
// the total size to be less than the size threshold.
|
||||
uint64_t effective_max =
|
||||
GetEffectiveMax(mutable_cf_options.compaction_options_fifo);
|
||||
// Estimate the effective remaining data after dropping TTL-expired SSTs.
|
||||
// Each dropped SST also frees a proportional share of blob data.
|
||||
//
|
||||
// In multi-level FIFO (migration), we must use total SST across ALL levels
|
||||
// as the reference, because total_blob covers all levels. Using only L0
|
||||
// SST would inflate the blob estimate.
|
||||
uint64_t effective_remaining = total_size;
|
||||
if (mutable_cf_options.compaction_options_fifo.max_data_files_size > 0) {
|
||||
uint64_t total_blob = vstorage->GetBlobStats().total_file_size;
|
||||
// Compute total SST across all levels so the reference scope matches
|
||||
// total_blob's scope (all levels).
|
||||
uint64_t total_sst_all_levels = GetTotalFilesSize(level_files);
|
||||
for (int level = 1; level < vstorage->num_levels(); ++level) {
|
||||
total_sst_all_levels += GetTotalFilesSize(vstorage->LevelFiles(level));
|
||||
}
|
||||
// remaining_sst_all = total_sst_all - dropped_l0_sst
|
||||
// total_size is the remaining L0 SST after removing expired files;
|
||||
// original L0 SST minus remaining L0 SST = dropped.
|
||||
uint64_t original_l0_sst = GetTotalFilesSize(level_files);
|
||||
uint64_t dropped_sst = original_l0_sst - total_size;
|
||||
uint64_t remaining_sst_all = total_sst_all_levels - dropped_sst;
|
||||
// Proportional blob estimate: each SST byte "owns" a proportional
|
||||
// share of blob bytes. Both reference sizes must come from the same
|
||||
// scope (all levels) to avoid inflated estimates.
|
||||
if (total_sst_all_levels > 0 && total_blob > 0) {
|
||||
effective_remaining =
|
||||
remaining_sst_all +
|
||||
static_cast<uint64_t>(static_cast<double>(remaining_sst_all) /
|
||||
total_sst_all_levels * total_blob);
|
||||
} else {
|
||||
effective_remaining = remaining_sst_all;
|
||||
}
|
||||
}
|
||||
if (inputs[0].files.empty() || effective_remaining > effective_max) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -109,8 +166,9 @@ Compaction* FIFOCompactionPicker::PickTTLCompaction(
|
||||
assert(f);
|
||||
uint64_t newest_key_time = f->TryGetNewestKeyTime();
|
||||
uint64_t creation_time = 0;
|
||||
if (f->fd.table_reader && f->fd.table_reader->GetTableProperties()) {
|
||||
creation_time = f->fd.table_reader->GetTableProperties()->creation_time;
|
||||
TableReader* reader = f->fd.pinned_reader.Get();
|
||||
if (reader != nullptr && reader->GetTableProperties() != nullptr) {
|
||||
creation_time = reader->GetTableProperties()->creation_time;
|
||||
}
|
||||
uint64_t est_newest_key_time = newest_key_time == kUnknownNewestKeyTime
|
||||
? creation_time
|
||||
@@ -151,7 +209,9 @@ Compaction* FIFOCompactionPicker::PickSizeCompaction(
|
||||
const std::string& cf_name, const MutableCFOptions& mutable_cf_options,
|
||||
const MutableDBOptions& mutable_db_options, VersionStorageInfo* vstorage,
|
||||
LogBuffer* log_buffer) {
|
||||
// compute the total size and identify the last non-empty level
|
||||
const auto& fifo_opts = mutable_cf_options.compaction_options_fifo;
|
||||
|
||||
// compute the total SST size and identify the last non-empty level
|
||||
int last_level = 0;
|
||||
uint64_t total_size = 0;
|
||||
for (int level = 0; level < vstorage->num_levels(); ++level) {
|
||||
@@ -164,52 +224,13 @@ Compaction* FIFOCompactionPicker::PickSizeCompaction(
|
||||
const std::vector<FileMetaData*>& last_level_files =
|
||||
vstorage->LevelFiles(last_level);
|
||||
|
||||
if (last_level == 0 &&
|
||||
total_size <=
|
||||
mutable_cf_options.compaction_options_fifo.max_table_files_size) {
|
||||
// total size not exceeded, try to find intra level 0 compaction if enabled
|
||||
const std::vector<FileMetaData*>& level0_files = vstorage->LevelFiles(0);
|
||||
if (mutable_cf_options.compaction_options_fifo.allow_compaction &&
|
||||
level0_files.size() > 0) {
|
||||
CompactionInputFiles comp_inputs;
|
||||
// try to prevent same files from being compacted multiple times, which
|
||||
// could produce large files that may never TTL-expire. Achieve this by
|
||||
// disallowing compactions with files larger than memtable (inflate its
|
||||
// size by 10% to account for uncompressed L0 files that may have size
|
||||
// slightly greater than memtable size limit).
|
||||
size_t max_compact_bytes_per_del_file =
|
||||
static_cast<size_t>(MultiplyCheckOverflow(
|
||||
static_cast<uint64_t>(mutable_cf_options.write_buffer_size),
|
||||
1.1));
|
||||
if (FindIntraL0Compaction(
|
||||
level0_files,
|
||||
mutable_cf_options
|
||||
.level0_file_num_compaction_trigger /* min_files_to_compact */
|
||||
,
|
||||
max_compact_bytes_per_del_file,
|
||||
mutable_cf_options.max_compaction_bytes, &comp_inputs)) {
|
||||
Compaction* c = new Compaction(
|
||||
vstorage, ioptions_, mutable_cf_options, mutable_db_options,
|
||||
{comp_inputs}, 0, 16 * 1024 * 1024 /* output file size limit */,
|
||||
0 /* max compaction bytes, not applicable */,
|
||||
0 /* output path ID */, mutable_cf_options.compression,
|
||||
mutable_cf_options.compression_opts, Temperature::kUnknown,
|
||||
0 /* max_subcompactions */, {},
|
||||
/* earliest_snapshot */ std::nullopt,
|
||||
/* snapshot_checker */ nullptr,
|
||||
CompactionReason::kFIFOReduceNumFiles,
|
||||
/* trim_ts */ "", vstorage->CompactionScore(0),
|
||||
/* l0_files_might_overlap */ true);
|
||||
return c;
|
||||
}
|
||||
}
|
||||
// Compute effective size and limit for comparison.
|
||||
uint64_t effective_size, effective_max;
|
||||
GetEffectiveSizeAndLimit(fifo_opts, total_size,
|
||||
vstorage->GetBlobStats().total_file_size,
|
||||
&effective_size, &effective_max);
|
||||
|
||||
ROCKS_LOG_BUFFER(
|
||||
log_buffer,
|
||||
"[%s] FIFO compaction: nothing to do. Total size %" PRIu64
|
||||
", max size %" PRIu64 "\n",
|
||||
cf_name.c_str(), total_size,
|
||||
mutable_cf_options.compaction_options_fifo.max_table_files_size);
|
||||
if (last_level == 0 && effective_size <= effective_max) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -227,11 +248,29 @@ Compaction* FIFOCompactionPicker::PickSizeCompaction(
|
||||
inputs[0].level = last_level;
|
||||
|
||||
if (last_level == 0) {
|
||||
// When using blob-aware sizing, use proportional estimation (same
|
||||
// principle as EstimateTotalDataForSST): each SST "owns"
|
||||
// effective_size / num_files of total data. This is an approximation
|
||||
// — individual SSTs may reference different amounts of blob data,
|
||||
// but uniform distribution is a reasonable estimate for FIFO dropping.
|
||||
uint64_t remaining_size = effective_size;
|
||||
const uint64_t num_files = last_level_files.size();
|
||||
// Proportional estimate of data per file (SST + blob).
|
||||
// Use max(1) to prevent stalling when effective_size < num_files.
|
||||
const uint64_t data_per_file =
|
||||
(fifo_opts.max_data_files_size > 0 && num_files > 0)
|
||||
? std::max(effective_size / num_files, uint64_t{1})
|
||||
: 0;
|
||||
|
||||
// In L0, right-most files are the oldest files.
|
||||
for (auto ritr = last_level_files.rbegin(); ritr != last_level_files.rend();
|
||||
++ritr) {
|
||||
auto f = *ritr;
|
||||
total_size -= f->fd.file_size;
|
||||
if (fifo_opts.max_data_files_size > 0) {
|
||||
remaining_size -= std::min(remaining_size, data_per_file);
|
||||
} else {
|
||||
remaining_size -= std::min(remaining_size, f->fd.file_size);
|
||||
}
|
||||
inputs[0].files.push_back(f);
|
||||
char tmp_fsize[16];
|
||||
AppendHumanBytes(f->fd.GetFileSize(), tmp_fsize, sizeof(tmp_fsize));
|
||||
@@ -239,13 +278,11 @@ Compaction* FIFOCompactionPicker::PickSizeCompaction(
|
||||
"[%s] FIFO compaction: picking file %" PRIu64
|
||||
" with size %s for deletion",
|
||||
cf_name.c_str(), f->fd.GetNumber(), tmp_fsize);
|
||||
if (total_size <=
|
||||
mutable_cf_options.compaction_options_fifo.max_table_files_size) {
|
||||
if (remaining_size <= effective_max) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (total_size >
|
||||
mutable_cf_options.compaction_options_fifo.max_table_files_size) {
|
||||
} else if (effective_size > effective_max) {
|
||||
// If the last level is non-L0, we actually don't know which file is
|
||||
// logically the oldest since the file creation time only represents
|
||||
// when this file was compacted to this level, which is independent
|
||||
@@ -255,34 +292,36 @@ Compaction* FIFOCompactionPicker::PickSizeCompaction(
|
||||
// file with the smallest key will be deleted first. This design decision
|
||||
// better serves a major type of FIFO use cases where smaller keys are
|
||||
// associated with older data.
|
||||
const uint64_t num_files = last_level_files.size();
|
||||
// Proportional estimate of data per file (SST + blob), same as L0 path.
|
||||
const uint64_t data_per_file =
|
||||
(fifo_opts.max_data_files_size > 0 && num_files > 0)
|
||||
? std::max(effective_size / num_files, uint64_t{1})
|
||||
: 0;
|
||||
for (const auto& f : last_level_files) {
|
||||
if (f->being_compacted) {
|
||||
continue;
|
||||
}
|
||||
total_size -= f->fd.file_size;
|
||||
if (fifo_opts.max_data_files_size > 0) {
|
||||
effective_size -= std::min(effective_size, data_per_file);
|
||||
} else {
|
||||
effective_size -= std::min(effective_size, f->fd.file_size);
|
||||
}
|
||||
inputs[0].files.push_back(f);
|
||||
char tmp_fsize[16];
|
||||
AppendHumanBytes(f->fd.GetFileSize(), tmp_fsize, sizeof(tmp_fsize));
|
||||
ROCKS_LOG_BUFFER(
|
||||
log_buffer,
|
||||
"[%s] FIFO compaction: picking file %" PRIu64
|
||||
" with size %s for deletion under total size %" PRIu64
|
||||
" vs max table files size %" PRIu64,
|
||||
cf_name.c_str(), f->fd.GetNumber(), tmp_fsize, total_size,
|
||||
mutable_cf_options.compaction_options_fifo.max_table_files_size);
|
||||
ROCKS_LOG_BUFFER(log_buffer,
|
||||
"[%s] FIFO compaction: picking file %" PRIu64
|
||||
" with size %s for deletion under total size %" PRIu64
|
||||
" vs max size %" PRIu64,
|
||||
cf_name.c_str(), f->fd.GetNumber(), tmp_fsize,
|
||||
effective_size, effective_max);
|
||||
|
||||
if (total_size <=
|
||||
mutable_cf_options.compaction_options_fifo.max_table_files_size) {
|
||||
if (effective_size <= effective_max) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ROCKS_LOG_BUFFER(
|
||||
log_buffer,
|
||||
"[%s] FIFO compaction: nothing to do. Total size %" PRIu64
|
||||
", max size %" PRIu64 "\n",
|
||||
cf_name.c_str(), total_size,
|
||||
mutable_cf_options.compaction_options_fifo.max_table_files_size);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -419,12 +458,282 @@ Compaction* FIFOCompactionPicker::PickTemperatureChangeCompaction(
|
||||
return c;
|
||||
}
|
||||
|
||||
Compaction* FIFOCompactionPicker::PickIntraL0Compaction(
|
||||
const std::string& cf_name, const MutableCFOptions& mutable_cf_options,
|
||||
const MutableDBOptions& mutable_db_options, VersionStorageInfo* vstorage,
|
||||
LogBuffer* log_buffer) {
|
||||
const auto& fifo_opts = mutable_cf_options.compaction_options_fifo;
|
||||
|
||||
if (!fifo_opts.allow_compaction) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const std::vector<FileMetaData*>& level0_files = vstorage->LevelFiles(0);
|
||||
if (level0_files.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (fifo_opts.use_kv_ratio_compaction) {
|
||||
if (fifo_opts.max_data_files_size == 0) {
|
||||
ROCKS_LOG_BUFFER(
|
||||
log_buffer,
|
||||
"[%s] FIFO kv-ratio compaction: skipping — "
|
||||
"max_data_files_size is 0, cannot compute target file size. ",
|
||||
cf_name.c_str());
|
||||
} else if (fifo_opts.max_data_files_size < fifo_opts.max_table_files_size) {
|
||||
ROCKS_LOG_BUFFER(log_buffer,
|
||||
"[%s] FIFO kv-ratio compaction: skipping — "
|
||||
"max_data_files_size (%" PRIu64
|
||||
") < max_table_files_size "
|
||||
"(%" PRIu64 ").",
|
||||
cf_name.c_str(), fifo_opts.max_data_files_size,
|
||||
fifo_opts.max_table_files_size);
|
||||
} else {
|
||||
return PickRatioBasedIntraL0Compaction(cf_name, mutable_cf_options,
|
||||
mutable_db_options, vstorage,
|
||||
log_buffer);
|
||||
}
|
||||
ROCKS_LOG_BUFFER(
|
||||
log_buffer, "[%s] FIFO: falling back to cost-based intra-L0 compaction",
|
||||
cf_name.c_str());
|
||||
}
|
||||
|
||||
// Old intra-L0 path: merge small files using PickCostBasedIntraL0Compaction.
|
||||
// Minimum files to compact follows level0_file_num_compaction_trigger.
|
||||
// Try to prevent same files from being compacted multiple times, which
|
||||
// could produce large files that may never TTL-expire. Achieve this by
|
||||
// disallowing compactions with files larger than memtable (inflate its
|
||||
// size by 10% to account for uncompressed L0 files that may have size
|
||||
// slightly greater than memtable size limit).
|
||||
|
||||
CompactionInputFiles comp_inputs;
|
||||
size_t max_compact_bytes_per_del_file =
|
||||
static_cast<size_t>(MultiplyCheckOverflow(
|
||||
static_cast<uint64_t>(mutable_cf_options.write_buffer_size), 1.1));
|
||||
if (PickCostBasedIntraL0Compaction(
|
||||
level0_files,
|
||||
mutable_cf_options
|
||||
.level0_file_num_compaction_trigger /* min_files_to_compact */,
|
||||
max_compact_bytes_per_del_file,
|
||||
mutable_cf_options.max_compaction_bytes, &comp_inputs)) {
|
||||
Compaction* c = new Compaction(
|
||||
vstorage, ioptions_, mutable_cf_options, mutable_db_options,
|
||||
{comp_inputs}, 0, 16 * 1024 * 1024 /* output file size limit */,
|
||||
0 /* max compaction bytes, not applicable */, 0 /* output path ID */,
|
||||
mutable_cf_options.compression, mutable_cf_options.compression_opts,
|
||||
Temperature::kUnknown, 0 /* max_subcompactions */, {},
|
||||
/* earliest_snapshot */ std::nullopt,
|
||||
/* snapshot_checker */ nullptr, CompactionReason::kFIFOReduceNumFiles,
|
||||
/* trim_ts */ "", vstorage->CompactionScore(0),
|
||||
/* l0_files_might_overlap */ true);
|
||||
return c;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
|
||||
const std::string& cf_name, const MutableCFOptions& mutable_cf_options,
|
||||
const MutableDBOptions& mutable_db_options, VersionStorageInfo* vstorage,
|
||||
LogBuffer* log_buffer) {
|
||||
const auto& fifo_opts = mutable_cf_options.compaction_options_fifo;
|
||||
assert(fifo_opts.use_kv_ratio_compaction);
|
||||
assert(fifo_opts.max_data_files_size > 0);
|
||||
|
||||
// During migration from level/universal compaction to FIFO, non-L0 levels
|
||||
// may still contain files. The ratio-based algorithm only operates on L0,
|
||||
// so skip it until PickSizeCompaction has drained all non-L0 levels.
|
||||
// Once levels collapse to L0-only, this algorithm will kick in.
|
||||
for (int level = 1; level < vstorage->num_levels(); ++level) {
|
||||
if (!vstorage->LevelFiles(level).empty()) {
|
||||
ROCKS_LOG_BUFFER(log_buffer,
|
||||
"[%s] FIFO kv-ratio compaction: skipping — non-L0 "
|
||||
"level %d still has %" ROCKSDB_PRIszt
|
||||
" files (migration in progress)",
|
||||
cf_name.c_str(), level,
|
||||
vstorage->LevelFiles(level).size());
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
if (!level0_compactions_in_progress_.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const std::vector<FileMetaData*>& level0_files = vstorage->LevelFiles(0);
|
||||
if (mutable_cf_options.level0_file_num_compaction_trigger <= 1) {
|
||||
// trigger <= 0 is invalid; trigger == 1 means compact after every flush,
|
||||
// which doesn't make sense for tiered merging (the tier boundary loop
|
||||
// divides by trigger, so trigger == 1 would cause an infinite loop).
|
||||
return nullptr;
|
||||
}
|
||||
const size_t trigger = static_cast<size_t>(
|
||||
mutable_cf_options.level0_file_num_compaction_trigger);
|
||||
if (level0_files.size() < trigger) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Determine the target compacted file size.
|
||||
//
|
||||
// When max_compaction_bytes > 0 (explicitly set by user), use it directly
|
||||
// as the target. This allows users to override the auto-calculated value.
|
||||
//
|
||||
// When max_compaction_bytes == 0 (default), auto-calculate from the data
|
||||
// capacity and observed SST/blob ratio:
|
||||
// target = max_data_files_size * sst_ratio / trigger
|
||||
//
|
||||
// This is recomputed on every PickCompaction call. The computation is
|
||||
// trivial (sum file sizes + arithmetic) and PickCompaction is only called
|
||||
// once per flush or compaction completion, so no caching is needed.
|
||||
uint64_t target = 0;
|
||||
if (mutable_cf_options.max_compaction_bytes > 0) {
|
||||
// User explicitly set max_compaction_bytes — use it as target
|
||||
target = mutable_cf_options.max_compaction_bytes;
|
||||
} else {
|
||||
// Auto-calculate from capacity and observed SST/blob ratio
|
||||
uint64_t total_sst = GetTotalFilesSize(level0_files);
|
||||
uint64_t total_blob = vstorage->GetBlobStats().total_file_size;
|
||||
uint64_t total_data = total_sst + total_blob;
|
||||
|
||||
if (total_data == 0 || total_sst == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Compute sst_ratio (inverse of EstimateTotalDataForSST's proportion):
|
||||
// when no blob files exist, sst_ratio is 1.0 and the target becomes
|
||||
// max_data_files_size / trigger, which is large. The algorithm will
|
||||
// naturally not find small enough files to compact.
|
||||
double sst_ratio =
|
||||
(total_blob > 0) ? static_cast<double>(total_sst) / total_data : 1.0;
|
||||
|
||||
uint64_t total_sst_at_cap =
|
||||
static_cast<uint64_t>(fifo_opts.max_data_files_size * sst_ratio);
|
||||
target = total_sst_at_cap / trigger;
|
||||
|
||||
ROCKS_LOG_BUFFER(log_buffer,
|
||||
"[%s] FIFO ratio-based compaction: sst_ratio=%.4f, "
|
||||
"target_file_size=%" PRIu64,
|
||||
cf_name.c_str(), sst_ratio, target);
|
||||
}
|
||||
if (target == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Tiered size-based file selection.
|
||||
//
|
||||
// Tier boundaries form a geometric sequence descending from target:
|
||||
// ..., target/trigger^2, target/trigger, target
|
||||
// For each boundary (smallest first), find contiguous L0 files with
|
||||
// size < boundary. If their accumulated bytes >= boundary, merge them.
|
||||
// The output (~boundary bytes) advances to the next tier. Files that
|
||||
// reach target are "graduated" and never compacted again.
|
||||
//
|
||||
// Trade-off: write amplification vs L0 file count.
|
||||
//
|
||||
// Write amp: O(log(target/flush) / log(trigger)) per byte, instead of
|
||||
// O(target / (trigger * flush)) from flat merging. Each byte is
|
||||
// rewritten once per tier crossing.
|
||||
//
|
||||
// L0 file count: trigger + k * (trigger - 1) at steady state, where
|
||||
// k = ceil(log(target/flush) / log(trigger)). This is higher than
|
||||
// the original trigger target because intermediate tier files
|
||||
// accumulate while waiting for the next tier merge. The trade-off
|
||||
// is explicit: more L0 files in exchange for logarithmic (instead
|
||||
// of linear) write amplification.
|
||||
|
||||
// Build tier boundaries from smallest to largest.
|
||||
// Stop at 10KB minimum — SST files of most workloads are larger than
|
||||
// this, so lower boundaries would only waste CPU scanning L0 files.
|
||||
// Files smaller than the lowest boundary simply merge at that boundary.
|
||||
static constexpr uint64_t kMinTierBoundary = 10 * 1024; // 10KB
|
||||
std::vector<uint64_t> boundaries;
|
||||
for (uint64_t b = target; b >= kMinTierBoundary; b /= trigger) {
|
||||
boundaries.push_back(b);
|
||||
}
|
||||
if (boundaries.empty()) {
|
||||
// target itself is below kMinTierBoundary — use target as the
|
||||
// sole boundary so we can still compact at the target size.
|
||||
boundaries.push_back(target);
|
||||
}
|
||||
std::reverse(boundaries.begin(), boundaries.end());
|
||||
|
||||
// For each tier boundary (smallest first), scan L0 for mergeable batches.
|
||||
// L0 files are stored newest-first; oldest is at the end.
|
||||
for (const uint64_t boundary : boundaries) {
|
||||
for (size_t scan = level0_files.size(); scan > 0;) {
|
||||
// Skip files >= boundary (they belong to higher tiers) or in-progress
|
||||
if (level0_files[scan - 1]->fd.file_size >= boundary ||
|
||||
level0_files[scan - 1]->being_compacted) {
|
||||
--scan;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Found a file < boundary — collect contiguous batch
|
||||
std::vector<FileMetaData*> batch;
|
||||
uint64_t accumulated = 0;
|
||||
size_t pos = scan;
|
||||
while (pos > 0 && level0_files[pos - 1]->fd.file_size < boundary &&
|
||||
!level0_files[pos - 1]->being_compacted) {
|
||||
// Don't let output exceed 2x boundary (prevent tier-skipping)
|
||||
if (accumulated >= boundary &&
|
||||
accumulated + level0_files[pos - 1]->fd.file_size > boundary * 2) {
|
||||
break;
|
||||
}
|
||||
batch.push_back(level0_files[pos - 1]);
|
||||
accumulated += level0_files[pos - 1]->fd.file_size;
|
||||
--pos;
|
||||
}
|
||||
|
||||
// Viable: >= 2 files and accumulated >= boundary
|
||||
if (batch.size() >= 2 && accumulated >= boundary) {
|
||||
CompactionInputFiles comp_inputs;
|
||||
comp_inputs.level = 0;
|
||||
comp_inputs.files = std::move(batch);
|
||||
|
||||
ROCKS_LOG_BUFFER(
|
||||
log_buffer,
|
||||
"[%s] FIFO kv-ratio compaction: picking %" ROCKSDB_PRIszt
|
||||
" files (%" PRIu64 " bytes) at tier boundary %" PRIu64
|
||||
" for intra-L0 compaction, target=%" PRIu64,
|
||||
cf_name.c_str(), comp_inputs.files.size(), accumulated, boundary,
|
||||
target);
|
||||
|
||||
Compaction* c = new Compaction(
|
||||
vstorage, ioptions_, mutable_cf_options, mutable_db_options,
|
||||
{comp_inputs}, 0, boundary /* output file size limit */,
|
||||
0 /* max compaction bytes, not applicable */,
|
||||
0 /* output path ID */, mutable_cf_options.compression,
|
||||
mutable_cf_options.compression_opts, Temperature::kUnknown,
|
||||
0 /* max_subcompactions */, {},
|
||||
/* earliest_snapshot */ std::nullopt,
|
||||
/* snapshot_checker */ nullptr,
|
||||
CompactionReason::kFIFOReduceNumFiles,
|
||||
/* trim_ts */ "", vstorage->CompactionScore(0),
|
||||
/* l0_files_might_overlap */ true);
|
||||
return c;
|
||||
}
|
||||
|
||||
// This batch wasn't enough — advance past it
|
||||
scan = pos;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// The full_history_ts_low parameter is used to control bottommost file marking
|
||||
// for compaction when user-defined timestamps (UDT) are enabled.
|
||||
|
||||
// TODO leverage full_history_ts_low for FIFO compaction, by trigggerring
|
||||
// compaction early for data that has already expired to achieve the goal of TTL
|
||||
// enforced compliance.
|
||||
Compaction* FIFOCompactionPicker::PickCompaction(
|
||||
const std::string& cf_name, const MutableCFOptions& mutable_cf_options,
|
||||
const MutableDBOptions& mutable_db_options,
|
||||
const std::vector<SequenceNumber>& /* existing_snapshots */,
|
||||
const SnapshotChecker* /* snapshot_checker */, VersionStorageInfo* vstorage,
|
||||
LogBuffer* log_buffer, bool /* require_max_output_level*/) {
|
||||
LogBuffer* log_buffer, const std::string& /* full_history_ts_low */,
|
||||
bool /* require_max_output_level*/) {
|
||||
Compaction* c = nullptr;
|
||||
if (mutable_cf_options.ttl > 0) {
|
||||
c = PickTTLCompaction(cf_name, mutable_cf_options, mutable_db_options,
|
||||
@@ -434,10 +743,22 @@ Compaction* FIFOCompactionPicker::PickCompaction(
|
||||
c = PickSizeCompaction(cf_name, mutable_cf_options, mutable_db_options,
|
||||
vstorage, log_buffer);
|
||||
}
|
||||
// Intra-L0 compaction merges small files to reduce file count.
|
||||
// It runs after size-based dropping: if PickSizeCompaction dropped files,
|
||||
// it returned non-null and we skip this. Otherwise, we try to reduce
|
||||
// L0 file count by merging small files together.
|
||||
if (c == nullptr) {
|
||||
c = PickIntraL0Compaction(cf_name, mutable_cf_options, mutable_db_options,
|
||||
vstorage, log_buffer);
|
||||
}
|
||||
if (c == nullptr) {
|
||||
c = PickTemperatureChangeCompaction(
|
||||
cf_name, mutable_cf_options, mutable_db_options, vstorage, log_buffer);
|
||||
}
|
||||
if (c == nullptr) {
|
||||
ROCKS_LOG_BUFFER(log_buffer, "[%s] FIFO compaction: no compaction picked",
|
||||
cf_name.c_str());
|
||||
}
|
||||
RegisterCompaction(c);
|
||||
return c;
|
||||
}
|
||||
@@ -449,7 +770,8 @@ Compaction* FIFOCompactionPicker::PickCompactionForCompactRange(
|
||||
const CompactRangeOptions& /*compact_range_options*/,
|
||||
const InternalKey* /*begin*/, const InternalKey* /*end*/,
|
||||
InternalKey** compaction_end, bool* /*manual_conflict*/,
|
||||
uint64_t /*max_file_num_to_ignore*/, const std::string& /*trim_ts*/) {
|
||||
uint64_t /*max_file_num_to_ignore*/, const std::string& /*trim_ts*/,
|
||||
const std::string& full_history_ts_low) {
|
||||
#ifdef NDEBUG
|
||||
(void)input_level;
|
||||
(void)output_level;
|
||||
@@ -458,10 +780,10 @@ Compaction* FIFOCompactionPicker::PickCompactionForCompactRange(
|
||||
assert(output_level == 0);
|
||||
*compaction_end = nullptr;
|
||||
LogBuffer log_buffer(InfoLogLevel::INFO_LEVEL, ioptions_.logger);
|
||||
Compaction* c =
|
||||
PickCompaction(cf_name, mutable_cf_options, mutable_db_options,
|
||||
/*existing_snapshots*/ {}, /*snapshot_checker*/ nullptr,
|
||||
vstorage, &log_buffer);
|
||||
Compaction* c = PickCompaction(
|
||||
cf_name, mutable_cf_options, mutable_db_options,
|
||||
/*existing_snapshots*/ {}, /*snapshot_checker*/ nullptr, vstorage,
|
||||
&log_buffer, full_history_ts_low, /* require_max_output_level */ false);
|
||||
log_buffer.FlushBufferToLog();
|
||||
return c;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ class FIFOCompactionPicker : public CompactionPicker {
|
||||
const std::vector<SequenceNumber>& /* existing_snapshots */,
|
||||
const SnapshotChecker* /* snapshot_checker */,
|
||||
VersionStorageInfo* version, LogBuffer* log_buffer,
|
||||
const std::string& /* full_history_ts_low */,
|
||||
bool /* require_max_output_level*/ = false) override;
|
||||
|
||||
Compaction* PickCompactionForCompactRange(
|
||||
@@ -33,7 +34,8 @@ class FIFOCompactionPicker : public CompactionPicker {
|
||||
const CompactRangeOptions& compact_range_options,
|
||||
const InternalKey* begin, const InternalKey* end,
|
||||
InternalKey** compaction_end, bool* manual_conflict,
|
||||
uint64_t max_file_num_to_ignore, const std::string& trim_ts) override;
|
||||
uint64_t max_file_num_to_ignore, const std::string& trim_ts,
|
||||
const std::string& full_history_ts_low) override;
|
||||
|
||||
// The maximum allowed output level. Always returns 0.
|
||||
int MaxOutputLevel() const override { return 0; }
|
||||
@@ -53,6 +55,28 @@ class FIFOCompactionPicker : public CompactionPicker {
|
||||
VersionStorageInfo* version,
|
||||
LogBuffer* log_buffer);
|
||||
|
||||
// Intra-L0 compaction: merges small L0 files to reduce file count.
|
||||
// Dispatches between two strategies based on configuration:
|
||||
// - use_kv_ratio_compaction = true: PickRatioBasedIntraL0Compaction
|
||||
// (BlobDB-optimized)
|
||||
// - use_kv_ratio_compaction = false: PickCostBasedIntraL0Compaction
|
||||
// (original)
|
||||
// Only active when allow_compaction = true.
|
||||
Compaction* PickIntraL0Compaction(const std::string& cf_name,
|
||||
const MutableCFOptions& mutable_cf_options,
|
||||
const MutableDBOptions& mutable_db_options,
|
||||
VersionStorageInfo* vstorage,
|
||||
LogBuffer* log_buffer);
|
||||
|
||||
// Capacity-derived intra-L0 compaction for BlobDB workloads.
|
||||
// Uses the observed SST/blob ratio to compute a target file size,
|
||||
// producing uniform files for predictable FIFO trimming.
|
||||
// Called from PickIntraL0Compaction when use_kv_ratio_compaction = true.
|
||||
Compaction* PickRatioBasedIntraL0Compaction(
|
||||
const std::string& cf_name, const MutableCFOptions& mutable_cf_options,
|
||||
const MutableDBOptions& mutable_db_options, VersionStorageInfo* vstorage,
|
||||
LogBuffer* log_buffer);
|
||||
|
||||
// Will pick one file to compact at a time, starting from the oldest file.
|
||||
Compaction* PickTemperatureChangeCompaction(
|
||||
const std::string& cf_name, const MutableCFOptions& mutable_cf_options,
|
||||
|
||||
@@ -61,14 +61,16 @@ class LevelCompactionBuilder {
|
||||
LogBuffer* log_buffer,
|
||||
const MutableCFOptions& mutable_cf_options,
|
||||
const ImmutableOptions& ioptions,
|
||||
const MutableDBOptions& mutable_db_options)
|
||||
const MutableDBOptions& mutable_db_options,
|
||||
const std::string& full_history_ts_low)
|
||||
: cf_name_(cf_name),
|
||||
vstorage_(vstorage),
|
||||
compaction_picker_(compaction_picker),
|
||||
log_buffer_(log_buffer),
|
||||
mutable_cf_options_(mutable_cf_options),
|
||||
ioptions_(ioptions),
|
||||
mutable_db_options_(mutable_db_options) {}
|
||||
mutable_db_options_(mutable_db_options),
|
||||
full_history_ts_low_(full_history_ts_low) {}
|
||||
|
||||
// Pick and return a compaction.
|
||||
Compaction* PickCompaction();
|
||||
@@ -155,6 +157,7 @@ class LevelCompactionBuilder {
|
||||
const MutableCFOptions& mutable_cf_options_;
|
||||
const ImmutableOptions& ioptions_;
|
||||
const MutableDBOptions& mutable_db_options_;
|
||||
const std::string& full_history_ts_low_;
|
||||
// Pick a path ID to place a newly generated file, with its level
|
||||
static uint32_t GetPathId(const ImmutableCFOptions& ioptions,
|
||||
const MutableCFOptions& mutable_cf_options,
|
||||
@@ -401,7 +404,15 @@ void LevelCompactionBuilder::SetupOtherFilesWithRoundRobinExpansion() {
|
||||
tmp_start_level_inputs = start_level_inputs_;
|
||||
// TODO (zichen): Future parallel round-robin may also need to update this
|
||||
// Constraint 1b (only expand till the end)
|
||||
for (size_t i = start_index + 1; i < level_files.size(); i++) {
|
||||
size_t next_index = start_index + 1;
|
||||
const FileMetaData* last_file = start_level_inputs_.files.back();
|
||||
for (size_t j = next_index; j < level_files.size(); j++) {
|
||||
if (level_files[j] == last_file) {
|
||||
next_index = j + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (size_t i = next_index; i < level_files.size(); i++) {
|
||||
auto* f = level_files[i];
|
||||
if (f->being_compacted) {
|
||||
// Constraint 1a
|
||||
@@ -571,7 +582,8 @@ Compaction* LevelCompactionBuilder::GetCompaction() {
|
||||
// takes running compactions into account (by skipping files that are already
|
||||
// being compacted). Since we just changed compaction score, we recalculate it
|
||||
// here
|
||||
vstorage_->ComputeCompactionScore(ioptions_, mutable_cf_options_);
|
||||
vstorage_->ComputeCompactionScore(ioptions_, mutable_cf_options_,
|
||||
full_history_ts_low_);
|
||||
return c;
|
||||
}
|
||||
|
||||
@@ -910,10 +922,10 @@ bool LevelCompactionBuilder::PickIntraL0Compaction() {
|
||||
// resort to L0->L0 compaction yet.
|
||||
return false;
|
||||
}
|
||||
return FindIntraL0Compaction(level_files, kMinFilesForIntraL0Compaction,
|
||||
std::numeric_limits<uint64_t>::max(),
|
||||
mutable_cf_options_.max_compaction_bytes,
|
||||
&start_level_inputs_);
|
||||
return PickCostBasedIntraL0Compaction(
|
||||
level_files, kMinFilesForIntraL0Compaction,
|
||||
std::numeric_limits<uint64_t>::max(),
|
||||
mutable_cf_options_.max_compaction_bytes, &start_level_inputs_);
|
||||
}
|
||||
|
||||
bool LevelCompactionBuilder::PickSizeBasedIntraL0Compaction() {
|
||||
@@ -976,10 +988,11 @@ Compaction* LevelCompactionPicker::PickCompaction(
|
||||
const MutableDBOptions& mutable_db_options,
|
||||
const std::vector<SequenceNumber>& /*existing_snapshots */,
|
||||
const SnapshotChecker* /*snapshot_checker*/, VersionStorageInfo* vstorage,
|
||||
LogBuffer* log_buffer, bool /* require_max_output_level*/) {
|
||||
LogBuffer* log_buffer, const std::string& full_history_ts_low,
|
||||
bool /* require_max_output_level*/) {
|
||||
LevelCompactionBuilder builder(cf_name, vstorage, this, log_buffer,
|
||||
mutable_cf_options, ioptions_,
|
||||
mutable_db_options);
|
||||
mutable_db_options, full_history_ts_low);
|
||||
return builder.PickCompaction();
|
||||
}
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -26,6 +26,7 @@ class LevelCompactionPicker : public CompactionPicker {
|
||||
const std::vector<SequenceNumber>& /* existing_snapshots */,
|
||||
const SnapshotChecker* /* snapshot_checker */,
|
||||
VersionStorageInfo* vstorage, LogBuffer* log_buffer,
|
||||
const std::string& full_history_ts_low,
|
||||
bool /*require_max_output_level*/ = false) override;
|
||||
|
||||
bool NeedsCompaction(const VersionStorageInfo* vstorage) const override;
|
||||
|
||||
+1456
-105
File diff suppressed because it is too large.
Load diff
@@ -39,7 +39,7 @@ class UniversalCompactionBuilder {
|
||||
const std::vector<SequenceNumber>& existing_snapshots,
|
||||
const SnapshotChecker* snapshot_checker, VersionStorageInfo* vstorage,
|
||||
UniversalCompactionPicker* picker, LogBuffer* log_buffer,
|
||||
bool require_max_output_level)
|
||||
bool require_max_output_level, const std::string& full_history_ts_low)
|
||||
: ioptions_(ioptions),
|
||||
icmp_(icmp),
|
||||
cf_name_(cf_name),
|
||||
@@ -50,7 +50,8 @@ class UniversalCompactionBuilder {
|
||||
log_buffer_(log_buffer),
|
||||
require_max_output_level_(require_max_output_level),
|
||||
allow_ingest_behind_(ioptions.cf_allow_ingest_behind ||
|
||||
ioptions.allow_ingest_behind) {
|
||||
ioptions.allow_ingest_behind),
|
||||
full_history_ts_low_(full_history_ts_low) {
|
||||
assert(icmp_);
|
||||
const auto* ucmp = icmp_->user_comparator();
|
||||
assert(ucmp);
|
||||
@@ -450,6 +451,7 @@ class UniversalCompactionBuilder {
|
||||
std::map<uint64_t, size_t> file_marked_for_compaction_to_sorted_run_index_;
|
||||
bool require_max_output_level_;
|
||||
bool allow_ingest_behind_;
|
||||
const std::string& full_history_ts_low_;
|
||||
|
||||
std::vector<UniversalCompactionBuilder::SortedRun> CalculateSortedRuns(
|
||||
const VersionStorageInfo& vstorage, int last_level,
|
||||
@@ -595,16 +597,20 @@ bool UniversalCompactionPicker::NeedsCompaction(
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO leverage full_history_ts_low in universal compaction picking. It could
|
||||
// help reduce the same infinite compaction loop issue found in level
|
||||
// compaction.
|
||||
Compaction* UniversalCompactionPicker::PickCompaction(
|
||||
const std::string& cf_name, const MutableCFOptions& mutable_cf_options,
|
||||
const MutableDBOptions& mutable_db_options,
|
||||
const std::vector<SequenceNumber>& existing_snapshots,
|
||||
const SnapshotChecker* snapshot_checker, VersionStorageInfo* vstorage,
|
||||
LogBuffer* log_buffer, bool require_max_output_level) {
|
||||
LogBuffer* log_buffer, const std::string& full_history_ts_low,
|
||||
bool require_max_output_level) {
|
||||
UniversalCompactionBuilder builder(
|
||||
ioptions_, icmp_, cf_name, mutable_cf_options, mutable_db_options,
|
||||
existing_snapshots, snapshot_checker, vstorage, this, log_buffer,
|
||||
require_max_output_level);
|
||||
require_max_output_level, full_history_ts_low);
|
||||
return builder.PickCompaction();
|
||||
}
|
||||
|
||||
@@ -825,7 +831,8 @@ Compaction* UniversalCompactionBuilder::PickCompaction() {
|
||||
RecordInHistogram(ioptions_.stats, NUM_FILES_IN_SINGLE_COMPACTION, num_files);
|
||||
|
||||
picker_->RegisterCompaction(c);
|
||||
vstorage_->ComputeCompactionScore(ioptions_, mutable_cf_options_);
|
||||
vstorage_->ComputeCompactionScore(ioptions_, mutable_cf_options_,
|
||||
full_history_ts_low_);
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK("UniversalCompactionBuilder::PickCompaction:Return",
|
||||
c);
|
||||
@@ -1544,9 +1551,18 @@ Compaction* UniversalCompactionBuilder::PickDeleteTriggeredCompaction() {
|
||||
}
|
||||
|
||||
if (output_level != 0) {
|
||||
// For standalone range deletion, we don't want to compact it with newer
|
||||
// L0 files that it doesn't cover.
|
||||
const FileMetaData* starting_l0_file =
|
||||
(start_level == 0 && start_level_inputs.size() == 1 &&
|
||||
start_level_inputs.files[0]->FileIsStandAloneRangeTombstone())
|
||||
? start_level_inputs.files[0]
|
||||
: nullptr;
|
||||
|
||||
if (start_level == 0) {
|
||||
if (!picker_->GetOverlappingL0Files(vstorage_, &start_level_inputs,
|
||||
output_level, nullptr)) {
|
||||
output_level, nullptr,
|
||||
starting_l0_file)) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
@@ -1557,7 +1573,8 @@ Compaction* UniversalCompactionBuilder::PickDeleteTriggeredCompaction() {
|
||||
output_level_inputs.level = output_level;
|
||||
if (!picker_->SetupOtherInputs(cf_name_, mutable_cf_options_, vstorage_,
|
||||
&start_level_inputs, &output_level_inputs,
|
||||
&parent_index, -1)) {
|
||||
&parent_index, -1, false,
|
||||
starting_l0_file)) {
|
||||
return nullptr;
|
||||
}
|
||||
inputs.push_back(start_level_inputs);
|
||||
|
||||
@@ -26,7 +26,8 @@ class UniversalCompactionPicker : public CompactionPicker {
|
||||
const MutableDBOptions& mutable_db_options,
|
||||
const std::vector<SequenceNumber>& existing_snapshots,
|
||||
const SnapshotChecker* snapshot_checker, VersionStorageInfo* vstorage,
|
||||
LogBuffer* log_buffer, bool require_max_output_level = false) override;
|
||||
LogBuffer* log_buffer, const std::string& full_history_ts_low,
|
||||
bool require_max_output_level = false) override;
|
||||
int MaxOutputLevel() const override { return NumberLevels() - 1; }
|
||||
|
||||
bool NeedsCompaction(const VersionStorageInfo* vstorage) const override;
|
||||
|
||||
@@ -117,6 +117,14 @@ CompactionJob::ProcessKeyValueCompactionWithCompactionService(
|
||||
std::string debug_str_before_wait =
|
||||
compaction->input_version()->DebugString(/*hex=*/true);
|
||||
|
||||
// TODO: Update CompactionService API to support abort and resume
|
||||
// functionality. Currently, remote compaction jobs cannot be aborted via
|
||||
// AbortAllCompactions() because the CompactionService interface lacks methods
|
||||
// to signal abort to remote workers and to properly resume after an abort.
|
||||
// The API needs to be extended with:
|
||||
// - A method to signal abort to running remote compaction jobs
|
||||
// - A method to resume/re-enable scheduling after an abort is lifted
|
||||
|
||||
ROCKS_LOG_INFO(db_options_.info_log,
|
||||
"[%s] [JOB %d] Waiting for remote compaction...",
|
||||
compaction->column_family_data()->GetName().c_str(), job_id_);
|
||||
@@ -312,16 +320,17 @@ CompactionServiceCompactionJob::CompactionServiceCompactionJob(
|
||||
std::string output_path,
|
||||
const CompactionServiceInput& compaction_service_input,
|
||||
CompactionServiceResult* compaction_service_result)
|
||||
: CompactionJob(job_id, compaction, db_options, mutable_db_options,
|
||||
file_options, versions, shutting_down, log_buffer, nullptr,
|
||||
output_directory, nullptr, stats, db_mutex,
|
||||
db_error_handler, job_context, std::move(table_cache),
|
||||
event_logger,
|
||||
compaction->mutable_cf_options().paranoid_file_checks,
|
||||
compaction->mutable_cf_options().report_bg_io_stats, dbname,
|
||||
&(compaction_service_result->stats), Env::Priority::USER,
|
||||
io_tracer, manual_compaction_canceled, db_id, db_session_id,
|
||||
compaction->column_family_data()->GetFullHistoryTsLow()),
|
||||
: CompactionJob(
|
||||
job_id, compaction, db_options, mutable_db_options, file_options,
|
||||
versions, shutting_down, log_buffer, nullptr, output_directory,
|
||||
nullptr, stats, db_mutex, db_error_handler, job_context,
|
||||
std::move(table_cache), event_logger,
|
||||
compaction->mutable_cf_options().paranoid_file_checks,
|
||||
compaction->mutable_cf_options().report_bg_io_stats, dbname,
|
||||
&(compaction_service_result->stats), Env::Priority::USER, io_tracer,
|
||||
manual_compaction_canceled, CompactionJob::kCompactionAbortedFalse,
|
||||
db_id, db_session_id,
|
||||
compaction->column_family_data()->GetFullHistoryTsLow()),
|
||||
output_path_(std::move(output_path)),
|
||||
compaction_input_(compaction_service_input),
|
||||
compaction_result_(compaction_service_result) {}
|
||||
|
||||
@@ -195,9 +195,11 @@ class MyTestCompactionService : public CompactionService {
|
||||
std::vector<std::shared_ptr<EventListener>> listeners_;
|
||||
std::vector<std::shared_ptr<TablePropertiesCollectorFactory>>
|
||||
table_properties_collector_factories_;
|
||||
std::atomic_bool canceled_{false};
|
||||
std::atomic<CompactionServiceJobStatus> final_updated_status_{
|
||||
CompactionServiceJobStatus::kUseLocal};
|
||||
|
||||
protected:
|
||||
std::atomic_bool canceled_{false};
|
||||
};
|
||||
|
||||
class CompactionServiceTest : public DBTestBase {
|
||||
@@ -407,6 +409,34 @@ TEST_F(CompactionServiceTest, BasicCompactions) {
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, SkipWALRecoveryInOpenAndCompact) {
|
||||
// Verify that OpenAndCompact skips WAL recovery when opening the secondary
|
||||
// instance. WAL replay is unnecessary for remote compaction since it only
|
||||
// needs the LSM state from MANIFEST.
|
||||
Options options = CurrentOptions();
|
||||
ReopenWithCompactionService(&options);
|
||||
|
||||
// Track whether FindAndRecoverLogFiles is called during compaction
|
||||
std::atomic_bool wal_recovery_called{false};
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::FindAndRecoverLogFiles:Begin",
|
||||
[&](void* /* arg */) { wal_recovery_called.store(true); });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Generate data and trigger compaction (which uses OpenAndCompact)
|
||||
GenerateTestData();
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
// WAL recovery should NOT have been called during OpenAndCompact
|
||||
ASSERT_FALSE(wal_recovery_called.load());
|
||||
|
||||
// Data should still be correct (compaction worked without WAL recovery)
|
||||
VerifyTestData();
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, ManualCompaction) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
@@ -1047,6 +1077,7 @@ TEST_F(CompactionServiceTest, CorruptedOutputParanoidFileCheck) {
|
||||
Destroy(options);
|
||||
options.disable_auto_compactions = true;
|
||||
options.paranoid_file_checks = paranoid_file_check_enabled;
|
||||
options.verify_output_flags = VerifyOutputFlags::kVerifyNone;
|
||||
ReopenWithCompactionService(&options);
|
||||
GenerateTestData();
|
||||
|
||||
@@ -1101,6 +1132,93 @@ TEST_F(CompactionServiceTest, CorruptedOutputParanoidFileCheck) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, CorruptedOutputVerifyOutputFlags) {
|
||||
for (VerifyOutputFlags verify_output_flags :
|
||||
{VerifyOutputFlags::kVerifyNone,
|
||||
VerifyOutputFlags::kEnableForLocalCompaction |
|
||||
VerifyOutputFlags::kVerifyBlockChecksum,
|
||||
VerifyOutputFlags::kEnableForRemoteCompaction |
|
||||
VerifyOutputFlags::kVerifyBlockChecksum,
|
||||
VerifyOutputFlags::kEnableForRemoteCompaction |
|
||||
VerifyOutputFlags::kVerifyIteration,
|
||||
VerifyOutputFlags::kEnableForRemoteCompaction |
|
||||
VerifyOutputFlags::kVerifyFileChecksum,
|
||||
VerifyOutputFlags::kVerifyAll}) {
|
||||
SCOPED_TRACE(
|
||||
"verify_output_flags=" +
|
||||
std::to_string(static_cast<std::underlying_type_t<VerifyOutputFlags>>(
|
||||
verify_output_flags)));
|
||||
|
||||
Options options = CurrentOptions();
|
||||
Destroy(options);
|
||||
options.disable_auto_compactions = true;
|
||||
options.paranoid_file_checks = false;
|
||||
options.verify_output_flags = verify_output_flags;
|
||||
options.file_checksum_gen_factory = GetFileChecksumGenCrc32cFactory();
|
||||
ReopenWithCompactionService(&options);
|
||||
GenerateTestData();
|
||||
|
||||
auto my_cs = GetCompactionService();
|
||||
|
||||
std::string start_str = Key(15);
|
||||
std::string end_str = Key(45);
|
||||
Slice start(start_str);
|
||||
Slice end(end_str);
|
||||
uint64_t comp_num = my_cs->GetCompactionNum();
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionServiceCompactionJob::Run:0", [&](void* arg) {
|
||||
CompactionServiceResult* compaction_result =
|
||||
*(static_cast<CompactionServiceResult**>(arg));
|
||||
ASSERT_TRUE(compaction_result != nullptr &&
|
||||
!compaction_result->output_files.empty());
|
||||
// Corrupt files here
|
||||
for (const auto& output_file : compaction_result->output_files) {
|
||||
std::string file_name =
|
||||
compaction_result->output_path + "/" + output_file.file_name;
|
||||
|
||||
// Corrupt very small range of bytes. This corruption is so small
|
||||
// that this isn't caught by default light-weight check
|
||||
ASSERT_OK(test::CorruptFile(env_, file_name, 0, 1,
|
||||
false /* verifyChecksum */));
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
const bool is_enabled_for_remote_compaction =
|
||||
!!(verify_output_flags & VerifyOutputFlags::kEnableForRemoteCompaction);
|
||||
const bool should_verify_block_checksum =
|
||||
!!(verify_output_flags & VerifyOutputFlags::kVerifyBlockChecksum);
|
||||
const bool should_verify_iteration =
|
||||
!!(verify_output_flags & VerifyOutputFlags::kVerifyIteration);
|
||||
const bool should_verify_file_checksum =
|
||||
!!(verify_output_flags & VerifyOutputFlags::kVerifyFileChecksum);
|
||||
|
||||
Status s = db_->CompactRange(CompactRangeOptions(), &start, &end);
|
||||
if (is_enabled_for_remote_compaction &&
|
||||
(should_verify_block_checksum || should_verify_iteration ||
|
||||
should_verify_file_checksum)) {
|
||||
ASSERT_NOK(s);
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
} else {
|
||||
// CompactRange() goes through if block checksum wasn't verified
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
|
||||
ASSERT_GE(my_cs->GetCompactionNum(), comp_num + 1);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
// On the worker side, the compaction is considered success
|
||||
// Verification is done on the primary side
|
||||
CompactionServiceResult result;
|
||||
my_cs->GetResult(&result);
|
||||
ASSERT_OK(result.status);
|
||||
ASSERT_TRUE(result.stats.is_manual_compaction);
|
||||
ASSERT_TRUE(result.stats.is_remote_compaction);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, TruncatedOutput) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
@@ -1296,8 +1414,9 @@ TEST_F(CompactionServiceTest, CancelCompactionOnPrimarySide) {
|
||||
|
||||
// Primary DB calls CancelAllBackgroundWork() while the compaction is running
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionJob::Run():Inprogress",
|
||||
[&](void* /*arg*/) { CancelAllBackgroundWork(db_, false /*wait*/); });
|
||||
"CompactionJob::Run():Inprogress", [&](void* /*arg*/) {
|
||||
CancelAllBackgroundWork(db_.get(), false /*wait*/);
|
||||
});
|
||||
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
@@ -2055,6 +2174,12 @@ class ResumableCompactionService : public MyTestCompactionService {
|
||||
{} /* table_properties_collector_factories */),
|
||||
scenario_(scenario) {}
|
||||
|
||||
// Set the user key where cancellation should happen.
|
||||
void SetCancelAtKey(const std::string& key, SequenceNumber seqno) {
|
||||
cancel_at_key_ = key;
|
||||
cancel_at_seqno_ = seqno;
|
||||
}
|
||||
|
||||
CompactionServiceJobStatus Wait(const std::string& scheduled_job_id,
|
||||
std::string* result) override {
|
||||
std::string compaction_input = ExtractCompactionInput(scheduled_job_id);
|
||||
@@ -2067,25 +2192,50 @@ class ResumableCompactionService : public MyTestCompactionService {
|
||||
// ASSUMPTION: This makes stats.count directly proportional to keys
|
||||
// processed.
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionOutputs::ShouldStopBefore::manual_decision", [](void* p) {
|
||||
"CompactionOutputs::ShouldStopBefore::manual_decision",
|
||||
[this](void* p) {
|
||||
auto* pair = static_cast<std::pair<bool*, const Slice>*>(p);
|
||||
*(pair->first) = true;
|
||||
*(pair->first) = true; // Force file cut at every key
|
||||
|
||||
// If cancel_at_key_ is set, cancel when we encounter that key
|
||||
if (!cancel_at_key_.empty() && !already_canceled_) {
|
||||
ParsedInternalKey parsed_key;
|
||||
if (ParseInternalKey(pair->second, &parsed_key, true).ok()) {
|
||||
if (parsed_key.user_key.ToString() == cancel_at_key_) {
|
||||
// Check sequence number if specified
|
||||
if (cancel_at_seqno_ == kMaxSequenceNumber ||
|
||||
parsed_key.sequence == cancel_at_seqno_) {
|
||||
canceled_ = true;
|
||||
already_canceled_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// If no cancel_at_key_ is set, use the original behavior:
|
||||
// Simulate cancelled compaction by overriding status at completion. So
|
||||
// compaction processes all keys before this point to make stats.count
|
||||
// comparison straightforward.
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::CompactWithoutInstallation::End", [&](void* status) {
|
||||
auto s = static_cast<Status*>(status);
|
||||
*s = Status::Incomplete(Status::SubCode::kManualCompactionPaused);
|
||||
});
|
||||
if (cancel_at_key_.empty()) {
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::CompactWithoutInstallation::End",
|
||||
[&](void* status) {
|
||||
auto s = static_cast<Status*>(status);
|
||||
*s = Status::Incomplete(Status::SubCode::kManualCompactionPaused);
|
||||
});
|
||||
}
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Phase 1: Run compaction with resumption enabled and cancel it
|
||||
// - Processes all input keys
|
||||
// - Processes input keys until cancellation point
|
||||
// - Creates output files and saves progress
|
||||
// - Status overridden to "paused"
|
||||
open_and_compaction_options.allow_resumption = true;
|
||||
open_and_compaction_options.canceled = &canceled_;
|
||||
already_canceled_ = false;
|
||||
canceled_ = false;
|
||||
|
||||
auto phase1_stats =
|
||||
RunCancelledCompaction(open_and_compaction_options, scheduled_job_id,
|
||||
compaction_input, override_options);
|
||||
@@ -2106,6 +2256,9 @@ class ResumableCompactionService : public MyTestCompactionService {
|
||||
EXPECT_TRUE(cleanup_status.ok());
|
||||
EXPECT_OK(override_options.env->CreateDir(output_dir));
|
||||
|
||||
already_canceled_ = false;
|
||||
canceled_ = false;
|
||||
|
||||
phase2_stats =
|
||||
RunCancelledCompaction(open_and_compaction_options, scheduled_job_id,
|
||||
compaction_input, override_options);
|
||||
@@ -2117,9 +2270,6 @@ class ResumableCompactionService : public MyTestCompactionService {
|
||||
EXPECT_EQ(phase2_stats.count, phase1_stats.count);
|
||||
}
|
||||
|
||||
SyncPoint::GetInstance()->ClearCallBack(
|
||||
"DBImplSecondary::CompactWithoutInstallation::End");
|
||||
|
||||
// Final phase: Run compaction to completion (no cancellation)
|
||||
if (scenario_ == TestScenario::kMultipleCancelToggleResumption) {
|
||||
// Attempt to resume but it ends up starting fresh
|
||||
@@ -2138,6 +2288,12 @@ class ResumableCompactionService : public MyTestCompactionService {
|
||||
EXPECT_OK(override_options.env->CreateDir(output_dir));
|
||||
}
|
||||
|
||||
// Prevent triggering of cancellation
|
||||
SyncPoint::GetInstance()->ClearCallBack(
|
||||
"DBImplSecondary::CompactWithoutInstallation::End");
|
||||
already_canceled_ = true;
|
||||
canceled_ = false;
|
||||
|
||||
auto final_phase_stats =
|
||||
RunCompaction(open_and_compaction_options, scheduled_job_id,
|
||||
compaction_input, override_options, result);
|
||||
@@ -2145,36 +2301,38 @@ class ResumableCompactionService : public MyTestCompactionService {
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
// Validate statistics based on scenario
|
||||
if (scenario_ == TestScenario::kMultipleCancelToggleResumption) {
|
||||
// ASSUMPTION: Phase 1 processes all keys before cancellation
|
||||
EXPECT_GT(phase1_stats.count, 0);
|
||||
// Validate statistics based on scenario (only when cancelling at end)
|
||||
if (cancel_at_key_.empty()) {
|
||||
if (scenario_ == TestScenario::kMultipleCancelToggleResumption) {
|
||||
// ASSUMPTION: Phase 1 processes all keys before cancellation
|
||||
EXPECT_GT(phase1_stats.count, 0);
|
||||
|
||||
// ASSUMPTION: Phase 2 runs with allow_resumption=false and an empty
|
||||
// folder. Phase 2 then creates its own output files (but doesn't save
|
||||
// progress). When Phase 3 starts with allow_resumption=true, it finds no
|
||||
// progress file exists, so it cannot resume and must start from scratch,
|
||||
// processing all input keys again.
|
||||
// Result: Phase 3 does the same amount of work as Phase 1.
|
||||
EXPECT_EQ(final_phase_stats.count, phase1_stats.count);
|
||||
// ASSUMPTION: Phase 2 runs with allow_resumption=false and an empty
|
||||
// folder. Phase 2 then creates its own output files (but doesn't save
|
||||
// progress). When Phase 3 starts with allow_resumption=true, it finds
|
||||
// no progress file exists, so it cannot resume and must start from
|
||||
// scratch, processing all input keys again. Result: Phase 3 does the
|
||||
// same amount of work as Phase 1.
|
||||
EXPECT_EQ(final_phase_stats.count, phase1_stats.count);
|
||||
|
||||
} else if (scenario_ == TestScenario::kCancelThenResume) {
|
||||
// ASSUMPTION: Phase 1 processes all keys before cancellation
|
||||
EXPECT_GT(phase1_stats.count, 0);
|
||||
} else if (scenario_ == TestScenario::kCancelThenResume) {
|
||||
// ASSUMPTION: Phase 1 processes all keys before cancellation
|
||||
EXPECT_GT(phase1_stats.count, 0);
|
||||
|
||||
// ASSUMPTION: Phase 1 processes all keys and saves progress before
|
||||
// cancellation. Final phase resumes from Phase 1's saved progress.
|
||||
// Since Phase 1 completed all processing before being cancelled, the
|
||||
// final phase should do less work than Phase 1.
|
||||
EXPECT_LT(final_phase_stats.count, phase1_stats.count);
|
||||
// ASSUMPTION: Phase 1 processes all keys and saves progress before
|
||||
// cancellation. Final phase resumes from Phase 1's saved progress.
|
||||
// Since Phase 1 completed all processing before being cancelled, the
|
||||
// final phase should do less work than Phase 1.
|
||||
EXPECT_LT(final_phase_stats.count, phase1_stats.count);
|
||||
|
||||
} else { // kCancelThenFreshStart
|
||||
// ASSUMPTION: Phase 1 processes all keys before cancellation
|
||||
EXPECT_GT(phase1_stats.count, 0);
|
||||
} else { // kCancelThenFreshStart
|
||||
// ASSUMPTION: Phase 1 processes all keys before cancellation
|
||||
EXPECT_GT(phase1_stats.count, 0);
|
||||
|
||||
// ASSUMPTION: Final phase starts fresh without resumption, so it
|
||||
// processes all input keys again and creates the same number of files
|
||||
EXPECT_EQ(final_phase_stats.count, phase1_stats.count);
|
||||
// ASSUMPTION: Final phase starts fresh without resumption, so it
|
||||
// processes all input keys again and creates the same number of files
|
||||
EXPECT_EQ(final_phase_stats.count, phase1_stats.count);
|
||||
}
|
||||
}
|
||||
|
||||
StoreResult(*result);
|
||||
@@ -2244,6 +2402,9 @@ class ResumableCompactionService : public MyTestCompactionService {
|
||||
}
|
||||
|
||||
TestScenario scenario_;
|
||||
std::string cancel_at_key_;
|
||||
SequenceNumber cancel_at_seqno_ = kMaxSequenceNumber;
|
||||
std::atomic<bool> already_canceled_{false};
|
||||
};
|
||||
|
||||
class ResumableCompactionServiceTest : public CompactionServiceTest {
|
||||
@@ -2350,6 +2511,387 @@ TEST_F(ResumableCompactionServiceTest,
|
||||
RunCompactionCancelTest(ResumableCompactionService::TestScenario::
|
||||
kMultipleCancelToggleResumption);
|
||||
}
|
||||
|
||||
class ResumableCompactionKeyTypeTest : public CompactionServiceTest {
|
||||
public:
|
||||
explicit ResumableCompactionKeyTypeTest() : CompactionServiceTest() {}
|
||||
|
||||
protected:
|
||||
void SetupResumableCompactionService(
|
||||
Options& options, const std::string& cancel_at_key = "",
|
||||
SequenceNumber cancel_at_seqno = kMaxSequenceNumber) {
|
||||
options.disable_auto_compactions = true;
|
||||
statistics_ = CreateDBStatistics();
|
||||
|
||||
resume_cs_ = std::make_shared<ResumableCompactionService>(
|
||||
dbname_, options, statistics_,
|
||||
ResumableCompactionService::TestScenario::kCancelThenResume);
|
||||
|
||||
if (!cancel_at_key.empty()) {
|
||||
resume_cs_->SetCancelAtKey(cancel_at_key, cancel_at_seqno);
|
||||
}
|
||||
|
||||
options.compaction_service = resume_cs_;
|
||||
DestroyAndReopen(options);
|
||||
}
|
||||
|
||||
void ResetStatistics() { ASSERT_OK(statistics_->Reset()); }
|
||||
|
||||
void VerifyResumeBytes() {
|
||||
uint64_t resumed_bytes =
|
||||
statistics_->getTickerCount(REMOTE_COMPACT_RESUMED_BYTES);
|
||||
ASSERT_GT(resumed_bytes, 0);
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<ResumableCompactionService> resume_cs_;
|
||||
std::shared_ptr<Statistics> statistics_;
|
||||
};
|
||||
|
||||
// Cancel compaction right before processing key "c" to test resumption at a
|
||||
// deletion at the non-bottom level. When resumed, compaction will continue
|
||||
// from this deletion.
|
||||
TEST_F(ResumableCompactionKeyTypeTest,
|
||||
CancelAndResumeWithDeleteAtNonBottomLevel) {
|
||||
Options options = CurrentOptions();
|
||||
|
||||
SetupResumableCompactionService(options, "c");
|
||||
|
||||
ASSERT_OK(Put("c", "old_value"));
|
||||
ASSERT_OK(Put("c_placeholder", "placeholder"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(options.num_levels - 1);
|
||||
|
||||
ASSERT_OK(Put("a", "val1"));
|
||||
ASSERT_OK(Put("b", "val2"));
|
||||
ASSERT_OK(Put("d", "val4"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ASSERT_OK(Delete("c"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
std::vector<std::string> input_files;
|
||||
ColumnFamilyMetaData cf_meta;
|
||||
db_->GetColumnFamilyMetaData(&cf_meta);
|
||||
|
||||
for (const auto& file : cf_meta.levels[0].files) {
|
||||
input_files.push_back(file.name);
|
||||
}
|
||||
|
||||
ASSERT_EQ(input_files.size(), 2);
|
||||
|
||||
ResetStatistics();
|
||||
|
||||
CompactionOptions compact_options;
|
||||
ASSERT_OK(
|
||||
db_->CompactFiles(compact_options, input_files, 1 /* output_level*/));
|
||||
|
||||
ASSERT_EQ(Get("a"), "val1");
|
||||
ASSERT_EQ(Get("b"), "val2");
|
||||
ASSERT_EQ(Get("c"), "NOT_FOUND");
|
||||
ASSERT_EQ(Get("d"), "val4");
|
||||
|
||||
VerifyResumeBytes();
|
||||
}
|
||||
|
||||
// Cancel compaction right before processing key "c" to test resumption at a
|
||||
// deletion at the ottom level. When resumed, compaction will continue from
|
||||
// the last saved progress point before the delete.
|
||||
TEST_F(ResumableCompactionKeyTypeTest, CancelAndResumeWithDeleteAtBottomLevel) {
|
||||
Options options = CurrentOptions();
|
||||
|
||||
SetupResumableCompactionService(options, "c");
|
||||
|
||||
ASSERT_OK(Put("c", "old_value"));
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
ASSERT_OK(Delete("c"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(options.num_levels - 1);
|
||||
|
||||
ASSERT_OK(Put("a", "val1"));
|
||||
ASSERT_OK(Put("b", "val2"));
|
||||
ASSERT_OK(Put("d", "val4"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ResetStatistics();
|
||||
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
ASSERT_EQ(Get("a"), "val1");
|
||||
ASSERT_EQ(Get("b"), "val2");
|
||||
ASSERT_EQ(Get("c"), "NOT_FOUND");
|
||||
ASSERT_EQ(Get("c", snapshot), "old_value");
|
||||
ASSERT_EQ(Get("d"), "val4");
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
|
||||
VerifyResumeBytes();
|
||||
}
|
||||
|
||||
// Cancel compaction right before processing key "c" to test resumption at a
|
||||
// merge operand. When resumed, compaction will continue from the last saved
|
||||
// progress point before the merge operand.
|
||||
TEST_F(ResumableCompactionKeyTypeTest, CancelAndResumeWithMerge) {
|
||||
Options options = CurrentOptions();
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator();
|
||||
|
||||
SetupResumableCompactionService(options, "c");
|
||||
|
||||
ASSERT_OK(Put("c", "old_value"));
|
||||
ASSERT_OK(Put("c_placeholder", "placeholder"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(options.num_levels - 1);
|
||||
|
||||
ASSERT_OK(Put("a", "val1"));
|
||||
ASSERT_OK(Put("b", "val2"));
|
||||
ASSERT_OK(Put("d", "val4"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ASSERT_OK(Merge("c", "new_value"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
std::vector<std::string> input_files;
|
||||
ColumnFamilyMetaData cf_meta;
|
||||
db_->GetColumnFamilyMetaData(&cf_meta);
|
||||
|
||||
for (const auto& file : cf_meta.levels[0].files) {
|
||||
input_files.push_back(file.name);
|
||||
}
|
||||
|
||||
ASSERT_EQ(input_files.size(), 2);
|
||||
|
||||
ResetStatistics();
|
||||
|
||||
CompactionOptions compact_options;
|
||||
ASSERT_OK(
|
||||
db_->CompactFiles(compact_options, input_files, 1 /* output_level*/));
|
||||
|
||||
ASSERT_EQ(Get("a"), "val1");
|
||||
ASSERT_EQ(Get("b"), "val2");
|
||||
ASSERT_EQ(Get("c"), "old_value,new_value");
|
||||
ASSERT_EQ(Get("d"), "val4");
|
||||
|
||||
VerifyResumeBytes();
|
||||
}
|
||||
|
||||
// Cancel compaction right before processing key "c" to test resumption at a
|
||||
// single delete. When resumed, compaction will continue from the last saved
|
||||
// progress point before the single delete.
|
||||
TEST_F(ResumableCompactionKeyTypeTest, CancelAndResumeWithSingleDelete) {
|
||||
Options options = CurrentOptions();
|
||||
|
||||
SetupResumableCompactionService(options, "c");
|
||||
|
||||
ASSERT_OK(Put("c", "old_value"));
|
||||
ASSERT_OK(Put("c_placeholder", "placeholder"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(options.num_levels - 1);
|
||||
|
||||
ASSERT_OK(Put("a", "val1"));
|
||||
ASSERT_OK(Put("b", "val2"));
|
||||
ASSERT_OK(Put("d", "val4"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ASSERT_OK(SingleDelete("c"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
std::vector<std::string> input_files;
|
||||
ColumnFamilyMetaData cf_meta;
|
||||
db_->GetColumnFamilyMetaData(&cf_meta);
|
||||
|
||||
for (const auto& file : cf_meta.levels[0].files) {
|
||||
input_files.push_back(file.name);
|
||||
}
|
||||
|
||||
ASSERT_EQ(input_files.size(), 2);
|
||||
|
||||
ResetStatistics();
|
||||
|
||||
CompactionOptions compact_options;
|
||||
ASSERT_OK(
|
||||
db_->CompactFiles(compact_options, input_files, 1 /* output_level*/));
|
||||
|
||||
ASSERT_EQ(Get("a"), "val1");
|
||||
ASSERT_EQ(Get("b"), "val2");
|
||||
ASSERT_EQ(Get("c"), "NOT_FOUND");
|
||||
ASSERT_EQ(Get("d"), "val4");
|
||||
|
||||
VerifyResumeBytes();
|
||||
}
|
||||
|
||||
// Cancel compaction right before processing key "c" to test resumption at a
|
||||
// range delete. When resumed, compaction will continue from the last saved
|
||||
// progress point before the range delete.
|
||||
TEST_F(ResumableCompactionKeyTypeTest, CancelAndResumeWithRangeDelete) {
|
||||
Options options = CurrentOptions();
|
||||
|
||||
SetupResumableCompactionService(options, "c");
|
||||
|
||||
ASSERT_OK(Put("c", "old_value"));
|
||||
ASSERT_OK(Put("c_placeholder", "placeholder"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(options.num_levels - 1);
|
||||
|
||||
ASSERT_OK(Put("a", "val1"));
|
||||
ASSERT_OK(Put("b", "val2"));
|
||||
ASSERT_OK(Put("d", "val4"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ASSERT_OK(
|
||||
db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), "c", "c_"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
std::vector<std::string> input_files;
|
||||
ColumnFamilyMetaData cf_meta;
|
||||
db_->GetColumnFamilyMetaData(&cf_meta);
|
||||
|
||||
for (const auto& file : cf_meta.levels[0].files) {
|
||||
input_files.push_back(file.name);
|
||||
}
|
||||
|
||||
ASSERT_EQ(input_files.size(), 2);
|
||||
|
||||
ResetStatistics();
|
||||
|
||||
CompactionOptions compact_options;
|
||||
ASSERT_OK(
|
||||
db_->CompactFiles(compact_options, input_files, 1 /* output_level*/));
|
||||
|
||||
ASSERT_EQ(Get("a"), "val1");
|
||||
ASSERT_EQ(Get("b"), "val2");
|
||||
ASSERT_EQ(Get("c"), "NOT_FOUND");
|
||||
ASSERT_EQ(Get("d"), "val4");
|
||||
|
||||
VerifyResumeBytes();
|
||||
}
|
||||
|
||||
// Test resumption when a key has multiple versions spanning across file
|
||||
// boundaries (i.e., the same key exists in multiple SST files).
|
||||
//
|
||||
// Scenario:
|
||||
// File 1 largest key: key "b"
|
||||
// File 2 smallest key: key "c" with seqno=4 (older version)
|
||||
// File 3 largest key: key "c" with seqno=5 (newer version)
|
||||
//
|
||||
// Cancel compaction right before processing the older version of key "c".
|
||||
// Upon resumption, compaction continues from the saved progress point "b" and
|
||||
// correctly processes both versions
|
||||
TEST_F(ResumableCompactionKeyTypeTest,
|
||||
CancelAndResumeWithKeySpanningFileBoundaries) {
|
||||
Options options = CurrentOptions();
|
||||
|
||||
// Set up cancellation at the older version of the key which will have
|
||||
// sequence number zero-ed out
|
||||
SetupResumableCompactionService(options, "c" /*cancel_at_key*/, 0 /*seqno*/);
|
||||
|
||||
ASSERT_OK(Put("a", "val1"));
|
||||
ASSERT_OK(Put("b", "val2"));
|
||||
ASSERT_OK(Put("d", "val4"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ASSERT_OK(Put("c", "old_value"));
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
ASSERT_OK(Put("c", "new_value"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ResetStatistics();
|
||||
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
ASSERT_EQ(Get("a"), "val1");
|
||||
ASSERT_EQ(Get("b"), "val2");
|
||||
ASSERT_EQ(Get("c"), "new_value");
|
||||
ASSERT_EQ(Get("c", snapshot), "old_value");
|
||||
ASSERT_EQ(Get("d"), "val4");
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
|
||||
VerifyResumeBytes();
|
||||
}
|
||||
|
||||
// Cancel compaction right before processing key "c" to test resumption at a
|
||||
// wide column. When resumed, compaction will continue
|
||||
// from the wide column.
|
||||
TEST_F(ResumableCompactionKeyTypeTest, CancelAndResumeWithWideColumn) {
|
||||
Options options = CurrentOptions();
|
||||
|
||||
SetupResumableCompactionService(options, "c" /*cancel_at_key*/);
|
||||
|
||||
ASSERT_OK(Put("a", "val1"));
|
||||
ASSERT_OK(Put("b", "val2"));
|
||||
ASSERT_OK(Put("d", "val4"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
WideColumns columns{{"col1", "value1"}, {"col2", "value2"}};
|
||||
ASSERT_OK(
|
||||
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), "c", columns));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ResetStatistics();
|
||||
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
ASSERT_EQ(Get("a"), "val1");
|
||||
ASSERT_EQ(Get("b"), "val2");
|
||||
|
||||
PinnableWideColumns result;
|
||||
ASSERT_OK(
|
||||
db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), "c", &result));
|
||||
WideColumns expected{{"col1", "value1"}, {"col2", "value2"}};
|
||||
ASSERT_EQ(result.columns(), expected);
|
||||
|
||||
ASSERT_EQ(Get("d"), "val4");
|
||||
|
||||
VerifyResumeBytes();
|
||||
}
|
||||
|
||||
// Cancel compaction right before processing key "c" to test resumption at a
|
||||
// timed put. When resumed, compaction will continue
|
||||
// from the timed put.
|
||||
TEST_F(ResumableCompactionKeyTypeTest, CancelAndResumeWithTimedPut) {
|
||||
Options options = CurrentOptions();
|
||||
options.preclude_last_level_data_seconds = 86400; // Enable TimedPut feature
|
||||
options.preserve_internal_time_seconds = 86400; // Preserve write time
|
||||
|
||||
SetupResumableCompactionService(options, "c" /*cancel_at_key*/);
|
||||
|
||||
ASSERT_OK(Put("c", "old_value"));
|
||||
ASSERT_OK(Put("c_placeholder", "placeholder"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(options.num_levels - 1);
|
||||
|
||||
ASSERT_OK(Put("a", "val1"));
|
||||
ASSERT_OK(Put("b", "val2"));
|
||||
ASSERT_OK(Put("d", "val4"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Use TimedPut for key "c" with current write time
|
||||
uint64_t write_time = env_->NowMicros() / 1000000;
|
||||
ASSERT_OK(TimedPut("c", "val3", write_time /*write_unix_time*/));
|
||||
ASSERT_OK(Put("d", "val4"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
std::vector<std::string> input_files;
|
||||
ColumnFamilyMetaData cf_meta;
|
||||
db_->GetColumnFamilyMetaData(&cf_meta);
|
||||
|
||||
for (const auto& file : cf_meta.levels[0].files) {
|
||||
input_files.push_back(file.name);
|
||||
}
|
||||
|
||||
ASSERT_EQ(input_files.size(), 2);
|
||||
|
||||
ResetStatistics();
|
||||
|
||||
CompactionOptions compact_options;
|
||||
ASSERT_OK(
|
||||
db_->CompactFiles(compact_options, input_files, 1 /* output_level*/));
|
||||
|
||||
ASSERT_EQ(Get("a"), "val1");
|
||||
ASSERT_EQ(Get("b"), "val2");
|
||||
ASSERT_EQ(Get("c"), "val3");
|
||||
ASSERT_EQ(Get("d"), "val4");
|
||||
|
||||
VerifyResumeBytes();
|
||||
}
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -38,11 +38,11 @@ OutputIterator SubcompactionState::GetOutputs() const {
|
||||
compaction_outputs_.outputs_);
|
||||
}
|
||||
|
||||
void SubcompactionState::Cleanup(Cache* cache) {
|
||||
void SubcompactionState::Cleanup(Cache* cache, const Status& overall_status) {
|
||||
proximal_level_outputs_.Cleanup();
|
||||
compaction_outputs_.Cleanup();
|
||||
|
||||
if (!status.ok()) {
|
||||
if (!status.ok() || !overall_status.ok()) {
|
||||
for (const auto& out : GetOutputs()) {
|
||||
// If this file was inserted into the table cache then remove it here
|
||||
// because this compaction was not committed. This is not strictly
|
||||
@@ -109,12 +109,12 @@ Status SubcompactionState::AddToOutput(
|
||||
const CompactionIterator& iter, bool use_proximal_output,
|
||||
const CompactionFileOpenFunc& open_file_func,
|
||||
const CompactionFileCloseFunc& close_file_func,
|
||||
const ParsedInternalKey& prev_table_last_internal_key) {
|
||||
const ParsedInternalKey& prev_iter_output_internal_key) {
|
||||
// update target output
|
||||
current_outputs_ =
|
||||
use_proximal_output ? &proximal_level_outputs_ : &compaction_outputs_;
|
||||
return current_outputs_->AddToOutput(iter, open_file_func, close_file_func,
|
||||
prev_table_last_internal_key);
|
||||
prev_iter_output_internal_key);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -95,6 +95,14 @@ class SubcompactionState {
|
||||
proximal_level_outputs_.RemoveLastEmptyOutput();
|
||||
}
|
||||
|
||||
// Cleanup output builders for abandoning in-progress files.
|
||||
void CleanupOutputs() {
|
||||
compaction_outputs_.Cleanup();
|
||||
if (compaction->SupportsPerKeyPlacement()) {
|
||||
proximal_level_outputs_.Cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
void BuildSubcompactionJobInfo(
|
||||
SubcompactionJobInfo& subcompaction_job_info) const {
|
||||
const Compaction* c = compaction;
|
||||
@@ -162,7 +170,7 @@ class SubcompactionState {
|
||||
}
|
||||
}
|
||||
|
||||
void Cleanup(Cache* cache);
|
||||
void Cleanup(Cache* cache, const Status& overall_status);
|
||||
|
||||
void AggregateCompactionOutputStats(
|
||||
InternalStats::CompactionStatsFull& internal_stats) const;
|
||||
@@ -221,7 +229,7 @@ class SubcompactionState {
|
||||
Status AddToOutput(const CompactionIterator& iter, bool use_proximal_output,
|
||||
const CompactionFileOpenFunc& open_file_func,
|
||||
const CompactionFileCloseFunc& close_file_func,
|
||||
const ParsedInternalKey& prev_table_last_internal_key);
|
||||
const ParsedInternalKey& prev_iter_output_internal_key);
|
||||
|
||||
// Close all compaction output files, both output_to_proximal_level outputs
|
||||
// and normal outputs.
|
||||
|
||||
@@ -1717,8 +1717,8 @@ TEST_P(PrecludeLastLevelTest, MigrationFromPreserveTimePartial) {
|
||||
ASSERT_EQ("0,0,0,0,0,0,1", FilesPerLevel());
|
||||
|
||||
std::vector<KeyVersion> key_versions;
|
||||
ASSERT_OK(GetAllKeyVersions(db_, {}, {}, std::numeric_limits<size_t>::max(),
|
||||
&key_versions));
|
||||
ASSERT_OK(GetAllKeyVersions(
|
||||
db_.get(), {}, {}, std::numeric_limits<size_t>::max(), &key_versions));
|
||||
|
||||
// make sure there're more than 300 keys and first 100 keys are having seqno
|
||||
// zeroed out, the last 100 key seqno not zeroed out
|
||||
@@ -2319,8 +2319,8 @@ TEST_P(PrecludeLastLevelTest, LastLevelOnlyCompactionPartial) {
|
||||
ASSERT_GT(GetSstSizeHelper(Temperature::kUnknown), 0);
|
||||
|
||||
std::vector<KeyVersion> key_versions;
|
||||
ASSERT_OK(GetAllKeyVersions(db_, {}, {}, std::numeric_limits<size_t>::max(),
|
||||
&key_versions));
|
||||
ASSERT_OK(GetAllKeyVersions(
|
||||
db_.get(), {}, {}, std::numeric_limits<size_t>::max(), &key_versions));
|
||||
|
||||
// make sure there're more than 300 keys and first 100 keys are having seqno
|
||||
// zeroed out, the last 100 key seqno not zeroed out
|
||||
|
||||
@@ -258,12 +258,12 @@ class ComparatorDBTest
|
||||
private:
|
||||
std::string dbname_;
|
||||
Env* env_;
|
||||
DB* db_;
|
||||
std::unique_ptr<DB> db_;
|
||||
Options last_options_;
|
||||
std::unique_ptr<const Comparator> comparator_guard;
|
||||
|
||||
public:
|
||||
ComparatorDBTest() : env_(Env::Default()), db_(nullptr) {
|
||||
ComparatorDBTest() : env_(Env::Default()) {
|
||||
kTestComparator = BytewiseComparator();
|
||||
dbname_ = test::PerThreadDBPath("comparator_db_test");
|
||||
BlockBasedTableOptions toptions;
|
||||
@@ -274,12 +274,12 @@ class ComparatorDBTest
|
||||
}
|
||||
|
||||
~ComparatorDBTest() override {
|
||||
delete db_;
|
||||
db_.reset();
|
||||
EXPECT_OK(DestroyDB(dbname_, last_options_));
|
||||
kTestComparator = BytewiseComparator();
|
||||
}
|
||||
|
||||
DB* GetDB() { return db_; }
|
||||
DB* GetDB() { return db_.get(); }
|
||||
|
||||
void SetOwnedComparator(const Comparator* cmp, bool owner = true) {
|
||||
if (owner) {
|
||||
@@ -301,14 +301,12 @@ class ComparatorDBTest
|
||||
}
|
||||
|
||||
void Destroy() {
|
||||
delete db_;
|
||||
db_ = nullptr;
|
||||
db_.reset();
|
||||
ASSERT_OK(DestroyDB(dbname_, last_options_));
|
||||
}
|
||||
|
||||
Status TryReopen() {
|
||||
delete db_;
|
||||
db_ = nullptr;
|
||||
db_.reset();
|
||||
last_options_.create_if_missing = true;
|
||||
|
||||
return DB::Open(last_options_, dbname_, &db_);
|
||||
@@ -318,7 +316,7 @@ class ComparatorDBTest
|
||||
INSTANTIATE_TEST_CASE_P(FormatDef, ComparatorDBTest,
|
||||
testing::Values(test::kDefaultFormatVersion));
|
||||
INSTANTIATE_TEST_CASE_P(FormatLatest, ComparatorDBTest,
|
||||
testing::Values(kLatestFormatVersion));
|
||||
testing::Values(kLatestBbtFormatVersion));
|
||||
|
||||
TEST_P(ComparatorDBTest, Bytewise) {
|
||||
for (int rand_seed = 301; rand_seed < 306; rand_seed++) {
|
||||
|
||||
+4
-4
@@ -65,7 +65,7 @@ Status VerifySstFileChecksum(const Options& options,
|
||||
}
|
||||
|
||||
Status VerifySstFileChecksumInternal(const Options& options,
|
||||
const EnvOptions& env_options,
|
||||
const FileOptions& file_options,
|
||||
const ReadOptions& read_options,
|
||||
const std::string& file_path,
|
||||
const SequenceNumber& largest_seqno) {
|
||||
@@ -74,8 +74,8 @@ Status VerifySstFileChecksumInternal(const Options& options,
|
||||
InternalKeyComparator internal_comparator(options.comparator);
|
||||
ImmutableOptions ioptions(options);
|
||||
|
||||
Status s = ioptions.fs->NewRandomAccessFile(
|
||||
file_path, FileOptions(env_options), &file, nullptr);
|
||||
Status s =
|
||||
ioptions.fs->NewRandomAccessFile(file_path, file_options, &file, nullptr);
|
||||
if (s.ok()) {
|
||||
s = ioptions.fs->GetFileSize(file_path, IOOptions(), &file_size, nullptr);
|
||||
} else {
|
||||
@@ -94,7 +94,7 @@ Status VerifySstFileChecksumInternal(const Options& options,
|
||||
const bool kImmortal = true;
|
||||
auto reader_options = TableReaderOptions(
|
||||
ioptions, options.prefix_extractor, options.compression_manager.get(),
|
||||
env_options, internal_comparator, options.block_protection_bytes_per_key,
|
||||
file_options, internal_comparator, options.block_protection_bytes_per_key,
|
||||
false /* skip_filters */, !kImmortal, false /* force_direct_prefetch */,
|
||||
-1 /* level */);
|
||||
reader_options.largest_seqno = largest_seqno;
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
#pragma once
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/file_system.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
Status VerifySstFileChecksumInternal(const Options& options,
|
||||
const EnvOptions& env_options,
|
||||
const FileOptions& file_options,
|
||||
const ReadOptions& read_options,
|
||||
const std::string& file_path,
|
||||
const SequenceNumber& largest_seqno = 0);
|
||||
|
||||
+60
-88
@@ -73,7 +73,7 @@ class CorruptionTest : public testing::Test {
|
||||
std::string dbname_;
|
||||
std::shared_ptr<Cache> tiny_cache_;
|
||||
Options options_;
|
||||
DB* db_;
|
||||
std::unique_ptr<DB> db_;
|
||||
|
||||
CorruptionTest() {
|
||||
// If LRU cache shard bit is smaller than 2 (or -1 which will automatically
|
||||
@@ -105,8 +105,7 @@ class CorruptionTest : public testing::Test {
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->LoadDependency({});
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
delete db_;
|
||||
db_ = nullptr;
|
||||
db_.reset();
|
||||
if (getenv("KEEP_DB")) {
|
||||
fprintf(stdout, "db is still at %s\n", dbname_.c_str());
|
||||
} else {
|
||||
@@ -116,14 +115,12 @@ class CorruptionTest : public testing::Test {
|
||||
}
|
||||
}
|
||||
|
||||
void CloseDb() {
|
||||
delete db_;
|
||||
db_ = nullptr;
|
||||
}
|
||||
void CloseDb() { db_.reset(); }
|
||||
|
||||
DBImpl* dbfull() { return static_cast_with_check<DBImpl>(db_.get()); }
|
||||
|
||||
Status TryReopen(Options* options = nullptr) {
|
||||
delete db_;
|
||||
db_ = nullptr;
|
||||
db_.reset();
|
||||
Options opt = (options ? *options : options_);
|
||||
if (opt.env == Options().env) {
|
||||
// If env is not overridden, replace it with ErrorEnv.
|
||||
@@ -141,8 +138,7 @@ class CorruptionTest : public testing::Test {
|
||||
void Reopen(Options* options = nullptr) { ASSERT_OK(TryReopen(options)); }
|
||||
|
||||
void RepairDB() {
|
||||
delete db_;
|
||||
db_ = nullptr;
|
||||
db_.reset();
|
||||
ASSERT_OK(::ROCKSDB_NAMESPACE::RepairDB(dbname_, options_));
|
||||
}
|
||||
|
||||
@@ -151,8 +147,7 @@ class CorruptionTest : public testing::Test {
|
||||
WriteBatch batch;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (flush_every != 0 && i != 0 && i % flush_every == 0) {
|
||||
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
|
||||
ASSERT_OK(dbi->TEST_FlushMemTable());
|
||||
ASSERT_OK(dbfull()->TEST_FlushMemTable());
|
||||
}
|
||||
// if ((i % 100) == 0) fprintf(stderr, "@ %d of %d\n", i, n);
|
||||
Slice key = Key(i + start, &key_space);
|
||||
@@ -436,14 +431,14 @@ TEST_F(CorruptionTest, NewFileErrorDuringWrite) {
|
||||
|
||||
TEST_F(CorruptionTest, TableFile) {
|
||||
Build(100);
|
||||
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
|
||||
DBImpl* dbi = dbfull();
|
||||
ASSERT_OK(dbi->TEST_FlushMemTable());
|
||||
ASSERT_OK(dbi->TEST_CompactRange(0, nullptr, nullptr));
|
||||
ASSERT_OK(dbi->TEST_CompactRange(1, nullptr, nullptr));
|
||||
|
||||
Corrupt(kTableFile, 100, 1);
|
||||
Check(99, 99);
|
||||
ASSERT_NOK(dbi->VerifyChecksum());
|
||||
ASSERT_NOK(db_->VerifyChecksum());
|
||||
}
|
||||
|
||||
TEST_F(CorruptionTest, VerifyChecksumReadahead) {
|
||||
@@ -460,14 +455,14 @@ TEST_F(CorruptionTest, VerifyChecksumReadahead) {
|
||||
Reopen(&options);
|
||||
|
||||
Build(10000);
|
||||
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
|
||||
DBImpl* dbi = dbfull();
|
||||
ASSERT_OK(dbi->TEST_FlushMemTable());
|
||||
ASSERT_OK(dbi->TEST_CompactRange(0, nullptr, nullptr));
|
||||
ASSERT_OK(dbi->TEST_CompactRange(1, nullptr, nullptr));
|
||||
|
||||
senv.count_random_reads_ = true;
|
||||
senv.random_read_counter_.Reset();
|
||||
ASSERT_OK(dbi->VerifyChecksum());
|
||||
ASSERT_OK(db_->VerifyChecksum());
|
||||
|
||||
// Make sure the counter is enabled.
|
||||
ASSERT_GT(senv.random_read_counter_.Read(), 0);
|
||||
@@ -480,7 +475,7 @@ TEST_F(CorruptionTest, VerifyChecksumReadahead) {
|
||||
senv.random_read_bytes_counter_ = 0;
|
||||
ReadOptions ro;
|
||||
ro.readahead_size = size_t{32 * 1024};
|
||||
ASSERT_OK(dbi->VerifyChecksum(ro));
|
||||
ASSERT_OK(db_->VerifyChecksum(ro));
|
||||
// The SST file is about 10MB. We set readahead size to 32KB.
|
||||
// Give 0 to 20 reads for metadata blocks, and allow real read
|
||||
// to range from 24KB to 48KB. The lower bound would be:
|
||||
@@ -494,8 +489,7 @@ TEST_F(CorruptionTest, VerifyChecksumReadahead) {
|
||||
// disabled).
|
||||
options.allow_mmap_reads = true;
|
||||
Reopen(&options);
|
||||
dbi = static_cast<DBImpl*>(db_);
|
||||
ASSERT_OK(dbi->VerifyChecksum(ro));
|
||||
ASSERT_OK(db_->VerifyChecksum(ro));
|
||||
|
||||
CloseDb();
|
||||
}
|
||||
@@ -508,18 +502,16 @@ TEST_F(CorruptionTest, TableFileIndexData) {
|
||||
Reopen(&options);
|
||||
// build 2 tables, flush at 5000
|
||||
Build(10000, 5000);
|
||||
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
|
||||
ASSERT_OK(dbi->TEST_FlushMemTable());
|
||||
ASSERT_OK(dbfull()->TEST_FlushMemTable());
|
||||
|
||||
// corrupt an index block of an entire file
|
||||
Corrupt(kTableFile, -2000, 500);
|
||||
options.paranoid_checks = false;
|
||||
Reopen(&options);
|
||||
dbi = static_cast_with_check<DBImpl>(db_);
|
||||
// one full file may be readable, since only one was corrupted
|
||||
// the other file should be fully non-readable, since index was corrupted
|
||||
Check(0, 5000, ReadOptions(true, true));
|
||||
ASSERT_NOK(dbi->VerifyChecksum());
|
||||
ASSERT_NOK(db_->VerifyChecksum());
|
||||
|
||||
// In paranoid mode, the db cannot be opened due to the corrupted file.
|
||||
ASSERT_TRUE(TryReopen().IsCorruption());
|
||||
@@ -527,8 +519,7 @@ TEST_F(CorruptionTest, TableFileIndexData) {
|
||||
|
||||
TEST_F(CorruptionTest, TableFileFooterMagic) {
|
||||
Build(100);
|
||||
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
|
||||
ASSERT_OK(dbi->TEST_FlushMemTable());
|
||||
ASSERT_OK(dbfull()->TEST_FlushMemTable());
|
||||
Check(100, 100);
|
||||
// Corrupt the whole footer
|
||||
Corrupt(kTableFile, -100, 100);
|
||||
@@ -543,8 +534,7 @@ TEST_F(CorruptionTest, TableFileFooterMagic) {
|
||||
|
||||
TEST_F(CorruptionTest, TableFileFooterNotMagic) {
|
||||
Build(100);
|
||||
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
|
||||
ASSERT_OK(dbi->TEST_FlushMemTable());
|
||||
ASSERT_OK(dbfull()->TEST_FlushMemTable());
|
||||
Check(100, 100);
|
||||
// Corrupt footer except magic number
|
||||
Corrupt(kTableFile, -100, 92);
|
||||
@@ -579,8 +569,7 @@ TEST_F(CorruptionTest, DBOpenWithWrongFileSize) {
|
||||
for (auto* cfh : cfhs) {
|
||||
delete cfh;
|
||||
}
|
||||
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
|
||||
ASSERT_OK(dbi->TEST_FlushMemTable());
|
||||
ASSERT_OK(dbfull()->TEST_FlushMemTable());
|
||||
|
||||
// ********************************************
|
||||
// Corrupt the file by making the file bigger
|
||||
@@ -601,7 +590,8 @@ TEST_F(CorruptionTest, DBOpenWithWrongFileSize) {
|
||||
// true
|
||||
options_.paranoid_checks = true;
|
||||
std::vector<ColumnFamilyHandle*> cfhs;
|
||||
auto s = DB::Open(options_, dbname_, cf_descs, &cfhs, &db_);
|
||||
Status s;
|
||||
s = DB::Open(options_, dbname_, cf_descs, &cfhs, &db_);
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
ASSERT_TRUE(s.ToString().find("file size mismatch") != std::string::npos);
|
||||
|
||||
@@ -626,8 +616,7 @@ TEST_F(CorruptionTest, DBOpenWithWrongFileSize) {
|
||||
|
||||
TEST_F(CorruptionTest, TableFileWrongSize) {
|
||||
Build(100);
|
||||
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
|
||||
ASSERT_OK(dbi->TEST_FlushMemTable());
|
||||
ASSERT_OK(dbfull()->TEST_FlushMemTable());
|
||||
Check(100, 100);
|
||||
|
||||
// ********************************************
|
||||
@@ -710,12 +699,11 @@ TEST_F(CorruptionTest, SequenceNumberRecovery) {
|
||||
|
||||
TEST_F(CorruptionTest, CorruptedDescriptor) {
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "foo", "hello"));
|
||||
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
|
||||
ASSERT_OK(dbi->TEST_FlushMemTable());
|
||||
ASSERT_OK(dbfull()->TEST_FlushMemTable());
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
ASSERT_OK(
|
||||
dbi->CompactRange(cro, dbi->DefaultColumnFamily(), nullptr, nullptr));
|
||||
db_->CompactRange(cro, db_->DefaultColumnFamily(), nullptr, nullptr));
|
||||
|
||||
Corrupt(kDescriptorFile, 0, 1000);
|
||||
Status s = TryReopen();
|
||||
@@ -734,7 +722,7 @@ TEST_F(CorruptionTest, CompactionInputError) {
|
||||
options.env = env_.get();
|
||||
Reopen(&options);
|
||||
Build(10);
|
||||
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
|
||||
DBImpl* dbi = dbfull();
|
||||
ASSERT_OK(dbi->TEST_FlushMemTable());
|
||||
ASSERT_OK(dbi->TEST_CompactRange(0, nullptr, nullptr));
|
||||
ASSERT_OK(dbi->TEST_CompactRange(1, nullptr, nullptr));
|
||||
@@ -742,12 +730,12 @@ TEST_F(CorruptionTest, CompactionInputError) {
|
||||
|
||||
Corrupt(kTableFile, 100, 1);
|
||||
Check(9, 9);
|
||||
ASSERT_NOK(dbi->VerifyChecksum());
|
||||
ASSERT_NOK(db_->VerifyChecksum());
|
||||
|
||||
// Force compactions by writing lots of values
|
||||
Build(10000);
|
||||
Check(10000, 10000);
|
||||
ASSERT_NOK(dbi->VerifyChecksum());
|
||||
ASSERT_NOK(db_->VerifyChecksum());
|
||||
}
|
||||
|
||||
TEST_F(CorruptionTest, CompactionInputErrorParanoid) {
|
||||
@@ -758,14 +746,14 @@ TEST_F(CorruptionTest, CompactionInputErrorParanoid) {
|
||||
options.write_buffer_size = 131072;
|
||||
options.max_write_buffer_number = 2;
|
||||
Reopen(&options);
|
||||
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
|
||||
DBImpl* dbi = dbfull();
|
||||
|
||||
// Fill levels >= 1
|
||||
for (int level = 1; level < dbi->NumberLevels(); level++) {
|
||||
ASSERT_OK(dbi->Put(WriteOptions(), "", "begin"));
|
||||
ASSERT_OK(dbi->Put(WriteOptions(), "~", "end"));
|
||||
for (int level = 1; level < db_->NumberLevels(); level++) {
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "", "begin"));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "~", "end"));
|
||||
ASSERT_OK(dbi->TEST_FlushMemTable());
|
||||
for (int comp_level = 0; comp_level < dbi->NumberLevels() - level;
|
||||
for (int comp_level = 0; comp_level < db_->NumberLevels() - level;
|
||||
++comp_level) {
|
||||
ASSERT_OK(dbi->TEST_CompactRange(comp_level, nullptr, nullptr));
|
||||
}
|
||||
@@ -773,7 +761,7 @@ TEST_F(CorruptionTest, CompactionInputErrorParanoid) {
|
||||
|
||||
Reopen(&options);
|
||||
|
||||
dbi = static_cast_with_check<DBImpl>(db_);
|
||||
dbi = dbfull();
|
||||
Build(10);
|
||||
ASSERT_OK(dbi->TEST_FlushMemTable());
|
||||
ASSERT_OK(dbi->TEST_WaitForCompact());
|
||||
@@ -781,7 +769,7 @@ TEST_F(CorruptionTest, CompactionInputErrorParanoid) {
|
||||
|
||||
CorruptTableFileAtLevel(0, 100, 1);
|
||||
Check(9, 9);
|
||||
ASSERT_NOK(dbi->VerifyChecksum());
|
||||
ASSERT_NOK(db_->VerifyChecksum());
|
||||
|
||||
// Write must eventually fail because of corrupted table
|
||||
Status s;
|
||||
@@ -800,17 +788,16 @@ TEST_F(CorruptionTest, CompactionInputErrorParanoid) {
|
||||
|
||||
TEST_F(CorruptionTest, UnrelatedKeys) {
|
||||
Build(10);
|
||||
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
|
||||
ASSERT_OK(dbi->TEST_FlushMemTable());
|
||||
ASSERT_OK(dbfull()->TEST_FlushMemTable());
|
||||
Corrupt(kTableFile, 100, 1);
|
||||
ASSERT_NOK(dbi->VerifyChecksum());
|
||||
ASSERT_NOK(db_->VerifyChecksum());
|
||||
|
||||
std::string tmp1, tmp2;
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(1000, &tmp1), Value(1000, &tmp2)));
|
||||
std::string v;
|
||||
ASSERT_OK(db_->Get(ReadOptions(), Key(1000, &tmp1), &v));
|
||||
ASSERT_EQ(Value(1000, &tmp2).ToString(), v);
|
||||
ASSERT_OK(dbi->TEST_FlushMemTable());
|
||||
ASSERT_OK(dbfull()->TEST_FlushMemTable());
|
||||
ASSERT_OK(db_->Get(ReadOptions(), Key(1000, &tmp1), &v));
|
||||
ASSERT_EQ(Value(1000, &tmp2).ToString(), v);
|
||||
}
|
||||
@@ -857,14 +844,12 @@ TEST_F(CorruptionTest, FileSystemStateCorrupted) {
|
||||
Reopen(&options);
|
||||
Build(10);
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
|
||||
std::vector<LiveFileMetaData> metadata;
|
||||
dbi->GetLiveFilesMetaData(&metadata);
|
||||
db_->GetLiveFilesMetaData(&metadata);
|
||||
ASSERT_GT(metadata.size(), 0);
|
||||
std::string filename = dbname_ + metadata[0].name;
|
||||
|
||||
delete db_;
|
||||
db_ = nullptr;
|
||||
db_.reset();
|
||||
|
||||
if (iter == 0) { // corrupt file size
|
||||
std::unique_ptr<WritableFile> file;
|
||||
@@ -896,8 +881,7 @@ TEST_F(CorruptionTest, ParanoidFileChecksOnFlush) {
|
||||
options.create_if_missing = true;
|
||||
Status s;
|
||||
for (const auto& mode : corruption_modes) {
|
||||
delete db_;
|
||||
db_ = nullptr;
|
||||
db_.reset();
|
||||
s = DestroyDB(dbname_, options);
|
||||
ASSERT_OK(s);
|
||||
std::shared_ptr<mock::MockTableFactory> mock =
|
||||
@@ -924,8 +908,7 @@ TEST_F(CorruptionTest, ParanoidFileChecksOnCompact) {
|
||||
options.create_if_missing = true;
|
||||
Status s;
|
||||
for (const auto& mode : corruption_modes) {
|
||||
delete db_;
|
||||
db_ = nullptr;
|
||||
db_.reset();
|
||||
s = DestroyDB(dbname_, options);
|
||||
ASSERT_OK(s);
|
||||
std::shared_ptr<mock::MockTableFactory> mock =
|
||||
@@ -934,12 +917,11 @@ TEST_F(CorruptionTest, ParanoidFileChecksOnCompact) {
|
||||
ASSERT_OK(DB::Open(options, dbname_, &db_));
|
||||
assert(db_ != nullptr); // suppress false clang-analyze report
|
||||
Build(100, 2);
|
||||
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
|
||||
ASSERT_OK(dbi->TEST_FlushMemTable());
|
||||
ASSERT_OK(dbfull()->TEST_FlushMemTable());
|
||||
mock->SetCorruptionMode(mode);
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
s = dbi->CompactRange(cro, dbi->DefaultColumnFamily(), nullptr, nullptr);
|
||||
s = db_->CompactRange(cro, db_->DefaultColumnFamily(), nullptr, nullptr);
|
||||
if (mode == mock::MockTableFactory::kCorruptNone) {
|
||||
ASSERT_OK(s);
|
||||
} else {
|
||||
@@ -955,8 +937,7 @@ TEST_F(CorruptionTest, ParanoidFileChecksWithDeleteRangeFirst) {
|
||||
options.paranoid_file_checks = true;
|
||||
options.create_if_missing = true;
|
||||
for (bool do_flush : {true, false}) {
|
||||
delete db_;
|
||||
db_ = nullptr;
|
||||
db_.reset();
|
||||
ASSERT_OK(DestroyDB(dbname_, options));
|
||||
ASSERT_OK(DB::Open(options, dbname_, &db_));
|
||||
std::string start, end;
|
||||
@@ -973,12 +954,11 @@ TEST_F(CorruptionTest, ParanoidFileChecksWithDeleteRangeFirst) {
|
||||
if (do_flush) {
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
} else {
|
||||
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
|
||||
ASSERT_OK(dbi->TEST_FlushMemTable());
|
||||
ASSERT_OK(dbfull()->TEST_FlushMemTable());
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
ASSERT_OK(
|
||||
dbi->CompactRange(cro, dbi->DefaultColumnFamily(), nullptr, nullptr));
|
||||
db_->CompactRange(cro, db_->DefaultColumnFamily(), nullptr, nullptr));
|
||||
}
|
||||
db_->ReleaseSnapshot(snap);
|
||||
}
|
||||
@@ -991,8 +971,7 @@ TEST_F(CorruptionTest, ParanoidFileChecksWithDeleteRange) {
|
||||
options.paranoid_file_checks = true;
|
||||
options.create_if_missing = true;
|
||||
for (bool do_flush : {true, false}) {
|
||||
delete db_;
|
||||
db_ = nullptr;
|
||||
db_.reset();
|
||||
ASSERT_OK(DestroyDB(dbname_, options));
|
||||
ASSERT_OK(DB::Open(options, dbname_, &db_));
|
||||
assert(db_ != nullptr); // suppress false clang-analyze report
|
||||
@@ -1012,12 +991,11 @@ TEST_F(CorruptionTest, ParanoidFileChecksWithDeleteRange) {
|
||||
if (do_flush) {
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
} else {
|
||||
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
|
||||
ASSERT_OK(dbi->TEST_FlushMemTable());
|
||||
ASSERT_OK(dbfull()->TEST_FlushMemTable());
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
ASSERT_OK(
|
||||
dbi->CompactRange(cro, dbi->DefaultColumnFamily(), nullptr, nullptr));
|
||||
db_->CompactRange(cro, db_->DefaultColumnFamily(), nullptr, nullptr));
|
||||
}
|
||||
db_->ReleaseSnapshot(snap);
|
||||
}
|
||||
@@ -1030,8 +1008,7 @@ TEST_F(CorruptionTest, ParanoidFileChecksWithDeleteRangeLast) {
|
||||
options.paranoid_file_checks = true;
|
||||
options.create_if_missing = true;
|
||||
for (bool do_flush : {true, false}) {
|
||||
delete db_;
|
||||
db_ = nullptr;
|
||||
db_.reset();
|
||||
ASSERT_OK(DestroyDB(dbname_, options));
|
||||
ASSERT_OK(DB::Open(options, dbname_, &db_));
|
||||
assert(db_ != nullptr); // suppress false clang-analyze report
|
||||
@@ -1048,12 +1025,11 @@ TEST_F(CorruptionTest, ParanoidFileChecksWithDeleteRangeLast) {
|
||||
if (do_flush) {
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
} else {
|
||||
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
|
||||
ASSERT_OK(dbi->TEST_FlushMemTable());
|
||||
ASSERT_OK(dbfull()->TEST_FlushMemTable());
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
ASSERT_OK(
|
||||
dbi->CompactRange(cro, dbi->DefaultColumnFamily(), nullptr, nullptr));
|
||||
db_->CompactRange(cro, db_->DefaultColumnFamily(), nullptr, nullptr));
|
||||
}
|
||||
db_->ReleaseSnapshot(snap);
|
||||
}
|
||||
@@ -1066,8 +1042,7 @@ TEST_F(CorruptionTest, LogCorruptionErrorsInCompactionIterator) {
|
||||
options.create_if_missing = true;
|
||||
options.allow_data_in_errors = true;
|
||||
auto mode = mock::MockTableFactory::kCorruptKey;
|
||||
delete db_;
|
||||
db_ = nullptr;
|
||||
db_.reset();
|
||||
ASSERT_OK(DestroyDB(dbname_, options));
|
||||
|
||||
std::shared_ptr<mock::MockTableFactory> mock =
|
||||
@@ -1079,12 +1054,11 @@ TEST_F(CorruptionTest, LogCorruptionErrorsInCompactionIterator) {
|
||||
assert(db_ != nullptr); // suppress false clang-analyze report
|
||||
Build(100, 2);
|
||||
|
||||
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
|
||||
ASSERT_OK(dbi->TEST_FlushMemTable());
|
||||
ASSERT_OK(dbfull()->TEST_FlushMemTable());
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
Status s =
|
||||
dbi->CompactRange(cro, dbi->DefaultColumnFamily(), nullptr, nullptr);
|
||||
db_->CompactRange(cro, db_->DefaultColumnFamily(), nullptr, nullptr);
|
||||
ASSERT_NOK(s);
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
}
|
||||
@@ -1095,8 +1069,7 @@ TEST_F(CorruptionTest, CompactionKeyOrderCheck) {
|
||||
options.env = env_.get();
|
||||
options.paranoid_file_checks = false;
|
||||
options.create_if_missing = true;
|
||||
delete db_;
|
||||
db_ = nullptr;
|
||||
db_.reset();
|
||||
ASSERT_OK(DestroyDB(dbname_, options));
|
||||
std::shared_ptr<mock::MockTableFactory> mock =
|
||||
std::make_shared<mock::MockTableFactory>();
|
||||
@@ -1105,14 +1078,13 @@ TEST_F(CorruptionTest, CompactionKeyOrderCheck) {
|
||||
assert(db_ != nullptr); // suppress false clang-analyze report
|
||||
mock->SetCorruptionMode(mock::MockTableFactory::kCorruptReorderKey);
|
||||
Build(100, 2);
|
||||
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
|
||||
ASSERT_OK(dbi->TEST_FlushMemTable());
|
||||
ASSERT_OK(dbfull()->TEST_FlushMemTable());
|
||||
|
||||
mock->SetCorruptionMode(mock::MockTableFactory::kCorruptNone);
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
ASSERT_NOK(
|
||||
dbi->CompactRange(cro, dbi->DefaultColumnFamily(), nullptr, nullptr));
|
||||
db_->CompactRange(cro, db_->DefaultColumnFamily(), nullptr, nullptr));
|
||||
}
|
||||
|
||||
TEST_F(CorruptionTest, FlushKeyOrderCheck) {
|
||||
@@ -1139,7 +1111,7 @@ TEST_F(CorruptionTest, FlushKeyOrderCheck) {
|
||||
}
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
Status s = static_cast_with_check<DBImpl>(db_)->TEST_FlushMemTable();
|
||||
Status s = dbfull()->TEST_FlushMemTable();
|
||||
ASSERT_NOK(s);
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
@@ -1263,7 +1235,7 @@ TEST_P(CrashDuringRecoveryWithCorruptionTest, CrashDuringRecovery) {
|
||||
// while other don't.
|
||||
{
|
||||
ASSERT_OK(DB::Open(options, dbname_, cf_descs, &handles, &db_));
|
||||
auto* dbimpl = static_cast_with_check<DBImpl>(db_);
|
||||
auto* dbimpl = dbfull();
|
||||
assert(dbimpl);
|
||||
|
||||
// Write one key to test_cf.
|
||||
|
||||
+10
-12
@@ -21,18 +21,18 @@ class CuckooTableDBTest : public testing::Test {
|
||||
private:
|
||||
std::string dbname_;
|
||||
Env* env_;
|
||||
DB* db_;
|
||||
std::unique_ptr<DB> db_;
|
||||
|
||||
public:
|
||||
CuckooTableDBTest() : env_(Env::Default()) {
|
||||
dbname_ = test::PerThreadDBPath("cuckoo_table_db_test");
|
||||
EXPECT_OK(DestroyDB(dbname_, Options()));
|
||||
db_ = nullptr;
|
||||
db_.reset();
|
||||
Reopen();
|
||||
}
|
||||
|
||||
~CuckooTableDBTest() override {
|
||||
delete db_;
|
||||
db_.reset();
|
||||
EXPECT_OK(DestroyDB(dbname_, Options()));
|
||||
}
|
||||
|
||||
@@ -47,12 +47,11 @@ class CuckooTableDBTest : public testing::Test {
|
||||
return options;
|
||||
}
|
||||
|
||||
DBImpl* dbfull() { return static_cast_with_check<DBImpl>(db_); }
|
||||
DBImpl* dbfull() { return static_cast_with_check<DBImpl>(db_.get()); }
|
||||
|
||||
// The following util methods are copied from plain_table_db_test.
|
||||
void Reopen(Options* options = nullptr) {
|
||||
delete db_;
|
||||
db_ = nullptr;
|
||||
db_.reset();
|
||||
Options opts;
|
||||
if (options != nullptr) {
|
||||
opts = *options;
|
||||
@@ -66,8 +65,7 @@ class CuckooTableDBTest : public testing::Test {
|
||||
void DestroyAndReopen(Options* options) {
|
||||
assert(options);
|
||||
ASSERT_OK(db_->Close());
|
||||
delete db_;
|
||||
db_ = nullptr;
|
||||
db_.reset();
|
||||
ASSERT_OK(DestroyDB(dbname_, *options));
|
||||
Reopen(options);
|
||||
}
|
||||
@@ -130,7 +128,7 @@ TEST_F(CuckooTableDBTest, Flush) {
|
||||
ASSERT_OK(dbfull()->TEST_FlushMemTable());
|
||||
|
||||
TablePropertiesCollection ptc;
|
||||
ASSERT_OK(static_cast<DB*>(dbfull())->GetPropertiesOfAllTables(&ptc));
|
||||
ASSERT_OK(dbfull()->GetPropertiesOfAllTables(&ptc));
|
||||
VerifySstUniqueIds(ptc);
|
||||
ASSERT_EQ(1U, ptc.size());
|
||||
ASSERT_EQ(3U, ptc.begin()->second->num_entries);
|
||||
@@ -147,7 +145,7 @@ TEST_F(CuckooTableDBTest, Flush) {
|
||||
ASSERT_OK(Put("key6", "v6"));
|
||||
ASSERT_OK(dbfull()->TEST_FlushMemTable());
|
||||
|
||||
ASSERT_OK(static_cast<DB*>(dbfull())->GetPropertiesOfAllTables(&ptc));
|
||||
ASSERT_OK(dbfull()->GetPropertiesOfAllTables(&ptc));
|
||||
VerifySstUniqueIds(ptc);
|
||||
ASSERT_EQ(2U, ptc.size());
|
||||
auto row = ptc.begin();
|
||||
@@ -165,7 +163,7 @@ TEST_F(CuckooTableDBTest, Flush) {
|
||||
ASSERT_OK(Delete("key5"));
|
||||
ASSERT_OK(Delete("key4"));
|
||||
ASSERT_OK(dbfull()->TEST_FlushMemTable());
|
||||
ASSERT_OK(static_cast<DB*>(dbfull())->GetPropertiesOfAllTables(&ptc));
|
||||
ASSERT_OK(dbfull()->GetPropertiesOfAllTables(&ptc));
|
||||
VerifySstUniqueIds(ptc);
|
||||
ASSERT_EQ(3U, ptc.size());
|
||||
row = ptc.begin();
|
||||
@@ -190,7 +188,7 @@ TEST_F(CuckooTableDBTest, FlushWithDuplicateKeys) {
|
||||
ASSERT_OK(dbfull()->TEST_FlushMemTable());
|
||||
|
||||
TablePropertiesCollection ptc;
|
||||
ASSERT_OK(static_cast<DB*>(dbfull())->GetPropertiesOfAllTables(&ptc));
|
||||
ASSERT_OK(dbfull()->GetPropertiesOfAllTables(&ptc));
|
||||
VerifySstUniqueIds(ptc);
|
||||
ASSERT_EQ(1U, ptc.size());
|
||||
ASSERT_EQ(2U, ptc.begin()->second->num_entries);
|
||||
|
||||
+540
-64
@@ -91,17 +91,15 @@ class DBBasicTest : public DBTestBase {
|
||||
TEST_F(DBBasicTest, OpenWhenOpen) {
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
DB* db2 = nullptr;
|
||||
std::unique_ptr<DB> db2;
|
||||
Status s = DB::Open(options, dbname_, &db2);
|
||||
ASSERT_NOK(s) << [db2]() {
|
||||
delete db2;
|
||||
ASSERT_NOK(s) << [&db2]() {
|
||||
db2.reset();
|
||||
return "db2 open: ok";
|
||||
}();
|
||||
ASSERT_EQ(Status::Code::kIOError, s.code());
|
||||
ASSERT_EQ(Status::SubCode::kNone, s.subcode());
|
||||
ASSERT_TRUE(strstr(s.getState(), "lock ") != nullptr);
|
||||
|
||||
delete db2;
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, EnableDirectIOWithZeroBuf) {
|
||||
@@ -242,6 +240,43 @@ TEST_F(DBBasicTest, ReadOnlyDB) {
|
||||
Status::Code::kNotSupported);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, ReadOnlyDBFlushWAL) {
|
||||
// Test that FlushWAL returns NotSupported on read-only DB, and that
|
||||
// GetLiveFilesStorageInfo works correctly even with manual_wal_flush=true.
|
||||
// This is a regression test for a bug where GetLiveFilesStorageInfo would
|
||||
// crash on read-only DBs with manual_wal_flush=true because FlushWAL
|
||||
// accessed logs_.back() on an empty deque.
|
||||
auto options = CurrentOptions();
|
||||
options.manual_wal_flush = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("foo", "v1"));
|
||||
ASSERT_OK(Put("bar", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put("baz", "v3")); // Unflushed data in WAL
|
||||
Close();
|
||||
|
||||
// Reopen as read-only
|
||||
ASSERT_OK(ReadOnlyReopen(options));
|
||||
ASSERT_EQ("v1", Get("foo"));
|
||||
ASSERT_EQ("v2", Get("bar"));
|
||||
ASSERT_EQ("v3", Get("baz"));
|
||||
|
||||
// FlushWAL should return NotSupported (not crash)
|
||||
ASSERT_EQ(db_->FlushWAL(/*sync=*/false).code(), Status::Code::kNotSupported);
|
||||
ASSERT_EQ(db_->FlushWAL(/*sync=*/true).code(), Status::Code::kNotSupported);
|
||||
|
||||
// GetLiveFilesStorageInfo should succeed (previously crashed with
|
||||
// manual_wal_flush=true because it called FlushWAL which accessed
|
||||
// logs_.back() on empty deque)
|
||||
LiveFilesStorageInfoOptions lfsi_opts;
|
||||
lfsi_opts.wal_size_for_flush = 0;
|
||||
std::vector<LiveFileStorageInfo> files;
|
||||
ASSERT_OK(db_->GetLiveFilesStorageInfo(lfsi_opts, &files));
|
||||
ASSERT_GT(files.size(), 0);
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, ReadOnlyDBWithWriteDBIdToManifestSet) {
|
||||
auto options = CurrentOptions();
|
||||
options.write_dbid_to_manifest = false;
|
||||
@@ -549,14 +584,14 @@ TEST_F(DBBasicTest, GetSnapshot) {
|
||||
|
||||
TEST_F(DBBasicTest, CheckLock) {
|
||||
do {
|
||||
DB* localdb = nullptr;
|
||||
std::unique_ptr<DB> localdb;
|
||||
Options options = CurrentOptions();
|
||||
ASSERT_OK(TryReopen(options));
|
||||
|
||||
// second open should fail
|
||||
Status s = DB::Open(options, dbname_, &localdb);
|
||||
ASSERT_NOK(s) << [localdb]() {
|
||||
delete localdb;
|
||||
ASSERT_NOK(s) << [&localdb]() {
|
||||
localdb.reset();
|
||||
return "localdb open: ok";
|
||||
}();
|
||||
#ifdef OS_LINUX
|
||||
@@ -675,30 +710,6 @@ TEST_F(DBBasicTest, Flush) {
|
||||
} while (ChangeCompactOptions());
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, ManifestRollOver) {
|
||||
do {
|
||||
Options options;
|
||||
options.max_manifest_file_size = 10; // 10 bytes
|
||||
options = CurrentOptions(options);
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
{
|
||||
ASSERT_OK(Put(1, "manifest_key1", std::string(1000, '1')));
|
||||
ASSERT_OK(Put(1, "manifest_key2", std::string(1000, '2')));
|
||||
ASSERT_OK(Put(1, "manifest_key3", std::string(1000, '3')));
|
||||
uint64_t manifest_before_flush = dbfull()->TEST_Current_Manifest_FileNo();
|
||||
ASSERT_OK(Flush(1)); // This should trigger LogAndApply.
|
||||
uint64_t manifest_after_flush = dbfull()->TEST_Current_Manifest_FileNo();
|
||||
ASSERT_GT(manifest_after_flush, manifest_before_flush);
|
||||
ReopenWithColumnFamilies({"default", "pikachu"}, options);
|
||||
ASSERT_GT(dbfull()->TEST_Current_Manifest_FileNo(), manifest_after_flush);
|
||||
// check if a new manifest file got inserted or not.
|
||||
ASSERT_EQ(std::string(1000, '1'), Get(1, "manifest_key1"));
|
||||
ASSERT_EQ(std::string(1000, '2'), Get(1, "manifest_key2"));
|
||||
ASSERT_EQ(std::string(1000, '3'), Get(1, "manifest_key3"));
|
||||
}
|
||||
} while (ChangeCompactOptions());
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, IdentityAcrossRestarts) {
|
||||
constexpr size_t kMinIdSize = 10;
|
||||
do {
|
||||
@@ -849,7 +860,7 @@ TEST_F(DBBasicTest, Snapshot) {
|
||||
ASSERT_OK(Put(1, "foo", "1v3"));
|
||||
|
||||
{
|
||||
ManagedSnapshot s3(db_);
|
||||
ManagedSnapshot s3(db_.get());
|
||||
ASSERT_EQ(3U, GetNumSnapshots());
|
||||
ASSERT_EQ(time_snap1, GetTimeOldestSnapshots());
|
||||
ASSERT_EQ(GetSequenceOldestSnapshots(), s1->GetSequenceNumber());
|
||||
@@ -972,7 +983,7 @@ TEST_F(DBBasicTest, DBOpen_Options) {
|
||||
Destroy(options);
|
||||
|
||||
// Does not exist, and create_if_missing == false: error
|
||||
DB* db = nullptr;
|
||||
std::unique_ptr<DB> db;
|
||||
options.create_if_missing = false;
|
||||
Status s = DB::Open(options, dbname_, &db);
|
||||
ASSERT_TRUE(strstr(s.ToString().c_str(), "does not exist") != nullptr);
|
||||
@@ -984,8 +995,7 @@ TEST_F(DBBasicTest, DBOpen_Options) {
|
||||
ASSERT_OK(s);
|
||||
ASSERT_TRUE(db != nullptr);
|
||||
|
||||
delete db;
|
||||
db = nullptr;
|
||||
db.reset();
|
||||
|
||||
// Does exist, and error_if_exists == true: error
|
||||
options.create_if_missing = false;
|
||||
@@ -1001,8 +1011,7 @@ TEST_F(DBBasicTest, DBOpen_Options) {
|
||||
ASSERT_OK(s);
|
||||
ASSERT_TRUE(db != nullptr);
|
||||
|
||||
delete db;
|
||||
db = nullptr;
|
||||
db.reset();
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, CompactOnFlush) {
|
||||
@@ -1307,7 +1316,7 @@ TEST_F(DBBasicTest, DBClose) {
|
||||
std::string dbname = test::PerThreadDBPath("db_close_test");
|
||||
ASSERT_OK(DestroyDB(dbname, options));
|
||||
|
||||
DB* db = nullptr;
|
||||
std::unique_ptr<DB> db;
|
||||
TestEnv* env = new TestEnv(env_);
|
||||
std::unique_ptr<TestEnv> local_env_guard(env);
|
||||
options.create_if_missing = true;
|
||||
@@ -1320,14 +1329,14 @@ TEST_F(DBBasicTest, DBClose) {
|
||||
ASSERT_EQ(env->GetCloseCount(), 1);
|
||||
ASSERT_EQ(s, Status::IOError());
|
||||
|
||||
delete db;
|
||||
db.reset();
|
||||
ASSERT_EQ(env->GetCloseCount(), 1);
|
||||
|
||||
// Do not call DB::Close() and ensure our logger Close() still gets called
|
||||
s = DB::Open(options, dbname, &db);
|
||||
ASSERT_OK(s);
|
||||
ASSERT_TRUE(db != nullptr);
|
||||
delete db;
|
||||
db.reset();
|
||||
ASSERT_EQ(env->GetCloseCount(), 2);
|
||||
|
||||
// close by WaitForCompact() with close_db option
|
||||
@@ -1342,7 +1351,7 @@ TEST_F(DBBasicTest, DBClose) {
|
||||
// see TestLogger::CloseHelper()
|
||||
ASSERT_EQ(s, Status::IOError());
|
||||
|
||||
delete db;
|
||||
db.reset();
|
||||
ASSERT_EQ(env->GetCloseCount(), 3);
|
||||
|
||||
// Provide our own logger and ensure DB::Close() does not close it
|
||||
@@ -1353,7 +1362,7 @@ TEST_F(DBBasicTest, DBClose) {
|
||||
|
||||
s = db->Close();
|
||||
ASSERT_EQ(s, Status::OK());
|
||||
delete db;
|
||||
db.reset();
|
||||
ASSERT_EQ(env->GetCloseCount(), 3);
|
||||
options.info_log.reset();
|
||||
ASSERT_EQ(env->GetCloseCount(), 4);
|
||||
@@ -1371,7 +1380,7 @@ TEST_F(DBBasicTest, DBCloseAllDirectoryFDs) {
|
||||
|
||||
ASSERT_OK(DestroyDB(dbname, options));
|
||||
|
||||
DB* db = nullptr;
|
||||
std::unique_ptr<DB> db;
|
||||
std::unique_ptr<Env> env = NewCompositeEnv(
|
||||
std::make_shared<CountedFileSystem>(FileSystem::Default()));
|
||||
options.create_if_missing = true;
|
||||
@@ -1389,7 +1398,7 @@ TEST_F(DBBasicTest, DBCloseAllDirectoryFDs) {
|
||||
ASSERT_EQ(counted_fs->counters()->dir_opens,
|
||||
counted_fs->counters()->dir_closes);
|
||||
ASSERT_OK(s);
|
||||
delete db;
|
||||
db.reset();
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, DBCloseFlushError) {
|
||||
@@ -1451,7 +1460,7 @@ TEST_P(DBMultiGetTestWithParam, MultiGetMultiCF) {
|
||||
}
|
||||
|
||||
int get_sv_count = 0;
|
||||
ROCKSDB_NAMESPACE::DBImpl* db = static_cast_with_check<DBImpl>(db_);
|
||||
ROCKSDB_NAMESPACE::DBImpl* db = dbfull();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::MultiCFSnapshot::AfterRefSV", [&](void* /*arg*/) {
|
||||
if (++get_sv_count == 2) {
|
||||
@@ -1523,10 +1532,9 @@ TEST_P(DBMultiGetTestWithParam, MultiGetMultiCF) {
|
||||
ASSERT_EQ(values[2], std::get<2>(cf_kv_vec[1]) + "_2");
|
||||
|
||||
for (int cf = 0; cf < 8; ++cf) {
|
||||
auto* cfd =
|
||||
static_cast_with_check<ColumnFamilyHandleImpl>(
|
||||
static_cast_with_check<DBImpl>(db_)->GetColumnFamilyHandle(cf))
|
||||
->cfd();
|
||||
auto* cfd = static_cast_with_check<ColumnFamilyHandleImpl>(
|
||||
dbfull()->GetColumnFamilyHandle(cf))
|
||||
->cfd();
|
||||
ASSERT_NE(cfd->TEST_GetLocalSV()->Get(), SuperVersion::kSVInUse);
|
||||
ASSERT_NE(cfd->TEST_GetLocalSV()->Get(), SuperVersion::kSVObsolete);
|
||||
}
|
||||
@@ -1612,10 +1620,9 @@ TEST_P(DBMultiGetTestWithParam, MultiGetMultiCFMutex) {
|
||||
"cf" + std::to_string(j) + "_val" + std::to_string(retries));
|
||||
}
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
auto* cfd =
|
||||
static_cast_with_check<ColumnFamilyHandleImpl>(
|
||||
static_cast_with_check<DBImpl>(db_)->GetColumnFamilyHandle(i))
|
||||
->cfd();
|
||||
auto* cfd = static_cast_with_check<ColumnFamilyHandleImpl>(
|
||||
dbfull()->GetColumnFamilyHandle(i))
|
||||
->cfd();
|
||||
ASSERT_NE(cfd->TEST_GetLocalSV()->Get(), SuperVersion::kSVInUse);
|
||||
}
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
@@ -1639,7 +1646,7 @@ TEST_P(DBMultiGetTestWithParam, MultiGetMultiCFSnapshot) {
|
||||
}
|
||||
|
||||
int get_sv_count = 0;
|
||||
ROCKSDB_NAMESPACE::DBImpl* db = static_cast_with_check<DBImpl>(db_);
|
||||
ROCKSDB_NAMESPACE::DBImpl* db = dbfull();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::MultiCFSnapshot::AfterRefSV", [&](void* /*arg*/) {
|
||||
if (++get_sv_count == 2) {
|
||||
@@ -1680,10 +1687,9 @@ TEST_P(DBMultiGetTestWithParam, MultiGetMultiCFSnapshot) {
|
||||
ASSERT_EQ(values[j], "cf" + std::to_string(j) + "_val");
|
||||
}
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
auto* cfd =
|
||||
static_cast_with_check<ColumnFamilyHandleImpl>(
|
||||
static_cast_with_check<DBImpl>(db_)->GetColumnFamilyHandle(i))
|
||||
->cfd();
|
||||
auto* cfd = static_cast_with_check<ColumnFamilyHandleImpl>(
|
||||
dbfull()->GetColumnFamilyHandle(i))
|
||||
->cfd();
|
||||
ASSERT_NE(cfd->TEST_GetLocalSV()->Get(), SuperVersion::kSVInUse);
|
||||
}
|
||||
}
|
||||
@@ -2529,6 +2535,319 @@ TEST_P(DBMultiGetTestWithParam, MultiGetBatchedValueSizeMultiLevelMerge) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetTestWithParam, MultiGetMemtableBatchLookup) {
|
||||
#ifndef USE_COROUTINES
|
||||
if (std::get<1>(GetParam())) {
|
||||
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
|
||||
return;
|
||||
}
|
||||
#endif // USE_COROUTINES
|
||||
// Skip for unbatched MultiGet
|
||||
if (!std::get<0>(GetParam())) {
|
||||
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
|
||||
return;
|
||||
}
|
||||
Options options = CurrentOptions();
|
||||
options.memtable_batch_lookup_optimization = true;
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
// Insert sorted keys into memtable
|
||||
for (int i = 0; i < 100; i++) {
|
||||
ASSERT_OK(Put(1, Key(i), "val" + std::to_string(i)));
|
||||
}
|
||||
// Delete some keys
|
||||
ASSERT_OK(Delete(1, Key(25)));
|
||||
ASSERT_OK(Delete(1, Key(75)));
|
||||
|
||||
// MultiGet a batch of keys - mix of existing, deleted, and missing
|
||||
// Store key strings to keep Slice data alive
|
||||
std::vector<std::string> key_strs = {Key(0), Key(10), Key(25), Key(50),
|
||||
Key(75), Key(99), Key(200)};
|
||||
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
|
||||
|
||||
std::vector<PinnableSlice> values(keys.size());
|
||||
std::vector<Status> statuses(keys.size());
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = std::get<1>(GetParam());
|
||||
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
|
||||
statuses.data(), true);
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(values[0].ToString(), "val0");
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(values[1].ToString(), "val10");
|
||||
ASSERT_TRUE(statuses[2].IsNotFound()); // deleted
|
||||
ASSERT_OK(statuses[3]);
|
||||
ASSERT_EQ(values[3].ToString(), "val50");
|
||||
ASSERT_TRUE(statuses[4].IsNotFound()); // deleted
|
||||
ASSERT_OK(statuses[5]);
|
||||
ASSERT_EQ(values[5].ToString(), "val99");
|
||||
ASSERT_TRUE(statuses[6].IsNotFound()); // never inserted
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetTestWithParam, MultiGetBatchLookupOverwrite) {
|
||||
#ifndef USE_COROUTINES
|
||||
if (std::get<1>(GetParam())) {
|
||||
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
|
||||
return;
|
||||
}
|
||||
#endif // USE_COROUTINES
|
||||
if (!std::get<0>(GetParam())) {
|
||||
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
|
||||
return;
|
||||
}
|
||||
Options options = CurrentOptions();
|
||||
options.memtable_batch_lookup_optimization = true;
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
// Insert, then overwrite some keys
|
||||
for (int i = 0; i < 50; i++) {
|
||||
ASSERT_OK(Put(1, Key(i), "old" + std::to_string(i)));
|
||||
}
|
||||
for (int i = 0; i < 50; i += 5) {
|
||||
ASSERT_OK(Put(1, Key(i), "new" + std::to_string(i)));
|
||||
}
|
||||
|
||||
std::vector<std::string> key_strs;
|
||||
for (int i = 0; i < 50; i += 5) {
|
||||
key_strs.push_back(Key(i));
|
||||
}
|
||||
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
|
||||
std::vector<PinnableSlice> values(keys.size());
|
||||
std::vector<Status> statuses(keys.size());
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = std::get<1>(GetParam());
|
||||
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
|
||||
statuses.data(), true);
|
||||
|
||||
for (size_t i = 0; i < keys.size(); i++) {
|
||||
ASSERT_OK(statuses[i]);
|
||||
ASSERT_EQ(values[i].ToString(), "new" + std::to_string(i * 5));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetTestWithParam, MultiGetBatchLookupWithFlush) {
|
||||
#ifndef USE_COROUTINES
|
||||
if (std::get<1>(GetParam())) {
|
||||
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
|
||||
return;
|
||||
}
|
||||
#endif // USE_COROUTINES
|
||||
if (!std::get<0>(GetParam())) {
|
||||
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
|
||||
return;
|
||||
}
|
||||
Options options = CurrentOptions();
|
||||
options.memtable_batch_lookup_optimization = true;
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
// Put data into SST
|
||||
for (int i = 0; i < 50; i++) {
|
||||
ASSERT_OK(Put(1, Key(i), "sst" + std::to_string(i)));
|
||||
}
|
||||
ASSERT_OK(Flush(1));
|
||||
|
||||
// Put different data into memtable (overlapping some keys)
|
||||
for (int i = 25; i < 75; i++) {
|
||||
ASSERT_OK(Put(1, Key(i), "mem" + std::to_string(i)));
|
||||
}
|
||||
|
||||
// MultiGet keys spanning both SST and memtable
|
||||
std::vector<std::string> key_strs = {Key(10), Key(30), Key(60), Key(80)};
|
||||
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
|
||||
|
||||
std::vector<PinnableSlice> values(keys.size());
|
||||
std::vector<Status> statuses(keys.size());
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = std::get<1>(GetParam());
|
||||
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
|
||||
statuses.data(), true);
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(values[0].ToString(), "sst10");
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(values[1].ToString(), "mem30");
|
||||
ASSERT_OK(statuses[2]);
|
||||
ASSERT_EQ(values[2].ToString(), "mem60");
|
||||
ASSERT_TRUE(statuses[3].IsNotFound());
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetTestWithParam, MultiGetBatchLookupWithMerge) {
|
||||
#ifndef USE_COROUTINES
|
||||
if (std::get<1>(GetParam())) {
|
||||
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
|
||||
return;
|
||||
}
|
||||
#endif // USE_COROUTINES
|
||||
if (!std::get<0>(GetParam())) {
|
||||
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
|
||||
return;
|
||||
}
|
||||
Options options = CurrentOptions();
|
||||
options.memtable_batch_lookup_optimization = true;
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
// Put base values
|
||||
ASSERT_OK(Put(1, Key(1), "a"));
|
||||
ASSERT_OK(Put(1, Key(2), "x"));
|
||||
// Merge on top
|
||||
ASSERT_OK(Merge(1, Key(1), "b"));
|
||||
ASSERT_OK(Merge(1, Key(1), "c"));
|
||||
ASSERT_OK(Merge(1, Key(2), "y"));
|
||||
|
||||
std::vector<std::string> key_strs = {Key(1), Key(2), Key(3)};
|
||||
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
|
||||
|
||||
std::vector<PinnableSlice> values(keys.size());
|
||||
std::vector<Status> statuses(keys.size());
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = std::get<1>(GetParam());
|
||||
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
|
||||
statuses.data(), true);
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(values[0].ToString(), "a,b,c");
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(values[1].ToString(), "x,y");
|
||||
ASSERT_TRUE(statuses[2].IsNotFound());
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, MultiGetBatchLookupDisabledByDefault) {
|
||||
// Verify that finger search is off by default and MultiGet still works
|
||||
Options options = CurrentOptions();
|
||||
ASSERT_FALSE(options.memtable_batch_lookup_optimization);
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "k1", "v1"));
|
||||
ASSERT_OK(Put(1, "k2", "v2"));
|
||||
|
||||
std::vector<Slice> keys = {"k1", "k2", "k3"};
|
||||
std::vector<PinnableSlice> values(3);
|
||||
std::vector<Status> statuses(3);
|
||||
|
||||
db_->MultiGet(ReadOptions(), handles_[1], 3, keys.data(), values.data(),
|
||||
statuses.data(), true);
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(values[0].ToString(), "v1");
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(values[1].ToString(), "v2");
|
||||
ASSERT_TRUE(statuses[2].IsNotFound());
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetTestWithParam, MultiGetBatchLookupWithParanoid) {
|
||||
#ifndef USE_COROUTINES
|
||||
if (std::get<1>(GetParam())) {
|
||||
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
|
||||
return;
|
||||
}
|
||||
#endif // USE_COROUTINES
|
||||
if (!std::get<0>(GetParam())) {
|
||||
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
|
||||
return;
|
||||
}
|
||||
Options options = CurrentOptions();
|
||||
options.memtable_batch_lookup_optimization = true;
|
||||
options.paranoid_memory_checks = true;
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
// Insert sorted keys into memtable
|
||||
for (int i = 0; i < 100; i++) {
|
||||
ASSERT_OK(Put(1, Key(i), "val" + std::to_string(i)));
|
||||
}
|
||||
ASSERT_OK(Delete(1, Key(25)));
|
||||
|
||||
// MultiGet with both batch optimization and paranoid checks enabled
|
||||
std::vector<std::string> key_strs = {Key(0), Key(10), Key(25),
|
||||
Key(50), Key(99), Key(200)};
|
||||
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
|
||||
|
||||
std::vector<PinnableSlice> values(keys.size());
|
||||
std::vector<Status> statuses(keys.size());
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = std::get<1>(GetParam());
|
||||
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
|
||||
statuses.data(), true);
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(values[0].ToString(), "val0");
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(values[1].ToString(), "val10");
|
||||
ASSERT_TRUE(statuses[2].IsNotFound()); // deleted
|
||||
ASSERT_OK(statuses[3]);
|
||||
ASSERT_EQ(values[3].ToString(), "val50");
|
||||
ASSERT_OK(statuses[4]);
|
||||
ASSERT_EQ(values[4].ToString(), "val99");
|
||||
ASSERT_TRUE(statuses[5].IsNotFound()); // never inserted
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetTestWithParam, MultiGetBatchLookupSnapshot) {
|
||||
#ifndef USE_COROUTINES
|
||||
if (std::get<1>(GetParam())) {
|
||||
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
|
||||
return;
|
||||
}
|
||||
#endif // USE_COROUTINES
|
||||
if (!std::get<0>(GetParam())) {
|
||||
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
|
||||
return;
|
||||
}
|
||||
Options options = CurrentOptions();
|
||||
options.memtable_batch_lookup_optimization = true;
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, Key(1), "v1_old"));
|
||||
ASSERT_OK(Put(1, Key(2), "v2_old"));
|
||||
|
||||
const Snapshot* snap = db_->GetSnapshot();
|
||||
|
||||
// Write new values after snapshot
|
||||
ASSERT_OK(Put(1, Key(1), "v1_new"));
|
||||
ASSERT_OK(Put(1, Key(2), "v2_new"));
|
||||
ASSERT_OK(Put(1, Key(3), "v3_new"));
|
||||
|
||||
// MultiGet with snapshot should see old values
|
||||
std::vector<std::string> key_strs = {Key(1), Key(2), Key(3)};
|
||||
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
|
||||
|
||||
std::vector<PinnableSlice> values(keys.size());
|
||||
std::vector<Status> statuses(keys.size());
|
||||
|
||||
ReadOptions ro;
|
||||
ro.snapshot = snap;
|
||||
ro.async_io = std::get<1>(GetParam());
|
||||
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
|
||||
statuses.data(), true);
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(values[0].ToString(), "v1_old");
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(values[1].ToString(), "v2_old");
|
||||
ASSERT_TRUE(statuses[2].IsNotFound()); // didn't exist at snapshot
|
||||
|
||||
db_->ReleaseSnapshot(snap);
|
||||
|
||||
// MultiGet without snapshot should see new values
|
||||
for (auto& v : values) {
|
||||
v.Reset();
|
||||
}
|
||||
db_->MultiGet(ReadOptions(), handles_[1], keys.size(), keys.data(),
|
||||
values.data(), statuses.data(), true);
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(values[0].ToString(), "v1_new");
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(values[1].ToString(), "v2_new");
|
||||
ASSERT_OK(statuses[2]);
|
||||
ASSERT_EQ(values[2].ToString(), "v3_new");
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(DBMultiGetTestWithParam, DBMultiGetTestWithParam,
|
||||
testing::Combine(testing::Bool(), testing::Bool()));
|
||||
|
||||
@@ -3288,8 +3607,8 @@ TEST_F(DBBasicTest, GetAllKeyVersions) {
|
||||
ASSERT_OK(Delete(std::to_string(i)));
|
||||
}
|
||||
std::vector<KeyVersion> key_versions;
|
||||
ASSERT_OK(GetAllKeyVersions(db_, {}, {}, std::numeric_limits<size_t>::max(),
|
||||
&key_versions));
|
||||
ASSERT_OK(GetAllKeyVersions(
|
||||
db_.get(), {}, {}, std::numeric_limits<size_t>::max(), &key_versions));
|
||||
ASSERT_EQ(kNumInserts + kNumDeletes + kNumUpdates, key_versions.size());
|
||||
for (size_t i = 0; i < kNumInserts + kNumDeletes + kNumUpdates; i++) {
|
||||
if (i % 3 == 0) {
|
||||
@@ -3298,7 +3617,7 @@ TEST_F(DBBasicTest, GetAllKeyVersions) {
|
||||
ASSERT_EQ(key_versions[i].GetTypeName(), "TypeValue");
|
||||
}
|
||||
}
|
||||
ASSERT_OK(GetAllKeyVersions(db_, handles_[0], {}, {},
|
||||
ASSERT_OK(GetAllKeyVersions(db_.get(), handles_[0], {}, {},
|
||||
std::numeric_limits<size_t>::max(),
|
||||
&key_versions));
|
||||
ASSERT_EQ(kNumInserts + kNumDeletes + kNumUpdates, key_versions.size());
|
||||
@@ -3313,14 +3632,14 @@ TEST_F(DBBasicTest, GetAllKeyVersions) {
|
||||
for (size_t i = 0; i + 1 != kNumDeletes; ++i) {
|
||||
ASSERT_OK(Delete(1, std::to_string(i)));
|
||||
}
|
||||
ASSERT_OK(GetAllKeyVersions(db_, handles_[1], {}, {},
|
||||
ASSERT_OK(GetAllKeyVersions(db_.get(), handles_[1], {}, {},
|
||||
std::numeric_limits<size_t>::max(),
|
||||
&key_versions));
|
||||
ASSERT_EQ(kNumInserts + kNumDeletes + kNumUpdates - 3, key_versions.size());
|
||||
|
||||
// Change from historical behavior: empty key is now interpreted literally as
|
||||
// a legal key (rather than as a "not present" key)
|
||||
ASSERT_OK(GetAllKeyVersions(db_, handles_[1], Slice(), Slice(),
|
||||
ASSERT_OK(GetAllKeyVersions(db_.get(), handles_[1], Slice(), Slice(),
|
||||
std::numeric_limits<size_t>::max(),
|
||||
&key_versions));
|
||||
ASSERT_EQ(key_versions.size(), 0);
|
||||
@@ -3892,6 +4211,75 @@ TEST_F(DBBasicTest, SkipWALIfMissingTableFiles) {
|
||||
ASSERT_OK(iter->status());
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, BestEffortRecoveryFailureWithTableCacheUseAfterFree) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.env = env_;
|
||||
// Force multiple manifest files
|
||||
options.max_manifest_file_size = 1;
|
||||
options.max_manifest_space_amp_pct = 0;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Disable file deletions to preserve old manifest files for
|
||||
// best-efforts recovery to succeed
|
||||
ASSERT_OK(db_->DisableFileDeletions());
|
||||
|
||||
// Create multiple SST files to populate TableCache during
|
||||
// best-efforts recovery
|
||||
for (int i = 0; i < 10; i++) {
|
||||
ASSERT_OK(Put("key" + std::to_string(i),
|
||||
std::string(1000, static_cast<char>('a' + i))));
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
|
||||
// Verify we have multiple manifest files
|
||||
std::vector<std::string> files;
|
||||
ASSERT_OK(env_->GetChildren(dbname_, &files));
|
||||
int manifest_count = 0;
|
||||
for (const auto& file : files) {
|
||||
if (file.find("MANIFEST") != std::string::npos) {
|
||||
manifest_count++;
|
||||
}
|
||||
}
|
||||
ASSERT_GE(manifest_count, 2);
|
||||
|
||||
// Inject corruption after TableCache is populated (count > 3), but only once
|
||||
// (injected flag) to allow best-effort recovery to trigger retry and succeed.
|
||||
// This coerce the bug: first recovery caches SSTs with reference to column
|
||||
// family's options in table cache and retry deletes column family so the
|
||||
// reference becomes dangling.
|
||||
int count = 0;
|
||||
bool injected = false;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionBuilder::CheckConsistencyBeforeReturn", [&](void* arg) {
|
||||
count++;
|
||||
if (count > 3 && !injected) {
|
||||
ASSERT_NE(nullptr, arg);
|
||||
*(static_cast<Status*>(arg)) =
|
||||
Status::Corruption("Injected corruption");
|
||||
injected = true;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
options.best_efforts_recovery = true;
|
||||
|
||||
Status s = TryReopen(options);
|
||||
ASSERT_OK(s);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
std::string value;
|
||||
// Without the fix, ASAN detects use-after-free when accessing cached SST
|
||||
// files that hold dangling references to deleted ioptions.
|
||||
s = db_->Get(ReadOptions(), "key" + std::to_string(i), &value);
|
||||
ASSERT_TRUE(s.ok() || s.IsNotFound());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, DisableTrackWal) {
|
||||
// If WAL tracking was enabled, and then disabled during reopen,
|
||||
// the previously tracked WALs should be removed from MANIFEST.
|
||||
@@ -5392,6 +5780,94 @@ INSTANTIATE_TEST_CASE_P(DBBasicTestDeadline, DBBasicTestDeadline,
|
||||
::testing::Values(std::make_tuple(true, false),
|
||||
std::make_tuple(false, true),
|
||||
std::make_tuple(true, true)));
|
||||
|
||||
// FileSystemWrapper that captures FileOptions passed to NewRandomAccessFile
|
||||
// for .sst files, so we can verify file_checksum fields are populated.
|
||||
class ChecksumCapturingFS : public FileSystemWrapper {
|
||||
public:
|
||||
explicit ChecksumCapturingFS(const std::shared_ptr<FileSystem>& base)
|
||||
: FileSystemWrapper(base) {}
|
||||
|
||||
static const char* kClassName() { return "ChecksumCapturingFS"; }
|
||||
const char* Name() const override { return kClassName(); }
|
||||
|
||||
IOStatus NewRandomAccessFile(const std::string& fname,
|
||||
const FileOptions& opts,
|
||||
std::unique_ptr<FSRandomAccessFile>* result,
|
||||
IODebugContext* dbg) override {
|
||||
if (fname.find(".sst") != std::string::npos) {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
captured_file_checksum_ = opts.file_checksum;
|
||||
captured_file_checksum_func_name_ = opts.file_checksum_func_name;
|
||||
capture_count_++;
|
||||
}
|
||||
return target()->NewRandomAccessFile(fname, opts, result, dbg);
|
||||
}
|
||||
|
||||
std::string GetCapturedFileChecksum() {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return captured_file_checksum_;
|
||||
}
|
||||
|
||||
std::string GetCapturedFileChecksumFuncName() {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return captured_file_checksum_func_name_;
|
||||
}
|
||||
|
||||
int GetCaptureCount() {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return capture_count_;
|
||||
}
|
||||
|
||||
void Reset() {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
captured_file_checksum_.clear();
|
||||
captured_file_checksum_func_name_.clear();
|
||||
capture_count_ = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex mu_;
|
||||
std::string captured_file_checksum_;
|
||||
std::string captured_file_checksum_func_name_;
|
||||
int capture_count_ = 0;
|
||||
};
|
||||
|
||||
TEST_F(DBBasicTest, FileChecksumInFileOptions) {
|
||||
// Verify that file_checksum and file_checksum_func_name from FileMetaData
|
||||
// are propagated through FileOptions when opening SST files.
|
||||
auto capturing_fs =
|
||||
std::make_shared<ChecksumCapturingFS>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> env(new CompositeEnvWrapper(env_, capturing_fs));
|
||||
|
||||
Options options = GetDefaultOptions();
|
||||
options.create_if_missing = true;
|
||||
options.env = env.get();
|
||||
options.file_checksum_gen_factory = GetFileChecksumGenCrc32cFactory();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Write data and flush to create an SST with a file checksum.
|
||||
ASSERT_OK(Put("key1", "value1"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Reset captures, then reopen to trigger TableCache SST open.
|
||||
capturing_fs->Reset();
|
||||
Reopen(options);
|
||||
|
||||
// Read to trigger SST open through TableCache::GetTableReader.
|
||||
ASSERT_EQ("value1", Get("key1"));
|
||||
|
||||
// Verify that checksum fields were populated.
|
||||
ASSERT_GT(capturing_fs->GetCaptureCount(), 0);
|
||||
ASSERT_FALSE(capturing_fs->GetCapturedFileChecksum().empty());
|
||||
ASSERT_NE(capturing_fs->GetCapturedFileChecksumFuncName(),
|
||||
capturing_fs->GetCapturedFileChecksum());
|
||||
ASSERT_EQ(capturing_fs->GetCapturedFileChecksumFuncName(),
|
||||
"FileChecksumCrc32c");
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
+116
-2
@@ -466,6 +466,90 @@ TEST_F(DBBlockCacheTest, WarmCacheWithDataBlocksDuringFlush) {
|
||||
options.statistics->getTickerCount(BLOCK_CACHE_DATA_ADD));
|
||||
}
|
||||
|
||||
// Cache wrapper that tracks the priority of each Insert call.
|
||||
namespace {
|
||||
class PriorityTrackingCache : public CacheWrapper {
|
||||
public:
|
||||
explicit PriorityTrackingCache(std::shared_ptr<Cache> target)
|
||||
: CacheWrapper(std::move(target)) {}
|
||||
|
||||
const char* Name() const override { return "PriorityTrackingCache"; }
|
||||
|
||||
Status Insert(const Slice& key, ObjectPtr value,
|
||||
const CacheItemHelper* helper, size_t charge,
|
||||
Handle** handle = nullptr, Priority priority = Priority::LOW,
|
||||
const Slice& compressed_value = Slice(),
|
||||
CompressionType type = kNoCompression) override {
|
||||
insert_priorities_.push_back(priority);
|
||||
return CacheWrapper::Insert(key, value, helper, charge, handle, priority,
|
||||
compressed_value, type);
|
||||
}
|
||||
|
||||
void ResetPriorities() { insert_priorities_.clear(); }
|
||||
|
||||
bool HasPriority(Priority p) const {
|
||||
return std::find(insert_priorities_.begin(), insert_priorities_.end(), p) !=
|
||||
insert_priorities_.end();
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<Priority> insert_priorities_;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST_F(DBBlockCacheTest, WarmCacheWithDataBlocksDuringCompaction) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics();
|
||||
options.disable_auto_compactions = true;
|
||||
|
||||
auto tracking_cache =
|
||||
std::make_shared<PriorityTrackingCache>(NewLRUCache(1 << 25, 0, false));
|
||||
|
||||
BlockBasedTableOptions table_options;
|
||||
table_options.block_cache = tracking_cache;
|
||||
table_options.cache_index_and_filter_blocks = false;
|
||||
table_options.prepopulate_block_cache =
|
||||
BlockBasedTableOptions::PrepopulateBlockCache::kFlushAndCompaction;
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
|
||||
DestroyAndReopen(options);
|
||||
|
||||
std::string value(kValueSize, 'a');
|
||||
|
||||
// Flush warming: inserts should use LOW priority.
|
||||
tracking_cache->ResetPriorities();
|
||||
ASSERT_OK(Put("key", value));
|
||||
ASSERT_OK(Flush());
|
||||
EXPECT_TRUE(tracking_cache->HasPriority(Cache::Priority::LOW));
|
||||
EXPECT_FALSE(tracking_cache->HasPriority(Cache::Priority::BOTTOM));
|
||||
|
||||
// Write overlapping key to force a real merge compaction (not trivial move).
|
||||
ASSERT_OK(Put("key", value + "2"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
auto data_add_before =
|
||||
options.statistics->getTickerCount(BLOCK_CACHE_DATA_ADD);
|
||||
|
||||
// Compaction warming: data block inserts should use BOTTOM priority.
|
||||
// Internal cache bookkeeping (e.g., cache entry stats) may insert at HIGH,
|
||||
// so we check for BOTTOM presence and LOW absence.
|
||||
tracking_cache->ResetPriorities();
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForceOptimized;
|
||||
ASSERT_OK(db_->CompactRange(cro, /*begin=*/nullptr, /*end=*/nullptr));
|
||||
EXPECT_GT(options.statistics->getTickerCount(BLOCK_CACHE_DATA_ADD),
|
||||
data_add_before);
|
||||
EXPECT_TRUE(tracking_cache->HasPriority(Cache::Priority::BOTTOM));
|
||||
EXPECT_FALSE(tracking_cache->HasPriority(Cache::Priority::LOW));
|
||||
|
||||
// Compaction output is in cache — reads should have zero misses.
|
||||
auto data_miss_before =
|
||||
options.statistics->getTickerCount(BLOCK_CACHE_DATA_MISS);
|
||||
ASSERT_EQ(value + "2", Get("key"));
|
||||
EXPECT_EQ(data_miss_before,
|
||||
options.statistics->getTickerCount(BLOCK_CACHE_DATA_MISS));
|
||||
}
|
||||
|
||||
// This test cache data, index and filter blocks during flush.
|
||||
class DBBlockCacheTest1 : public DBTestBase,
|
||||
public ::testing::WithParamInterface<uint32_t> {
|
||||
@@ -620,6 +704,36 @@ TEST_F(DBBlockCacheTest, DynamicOptions) {
|
||||
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_MISS));
|
||||
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_HIT));
|
||||
|
||||
// Switch to kFlushAndCompaction
|
||||
++i;
|
||||
ASSERT_OK(dbfull()->SetOptions(
|
||||
{{"block_based_table_factory",
|
||||
"{prepopulate_block_cache=kFlushAndCompaction;}"}}));
|
||||
|
||||
ASSERT_OK(Put(std::to_string(i), value));
|
||||
ASSERT_OK(Flush());
|
||||
// Flush warming still works
|
||||
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_ADD));
|
||||
|
||||
ASSERT_EQ(value, Get(std::to_string(i)));
|
||||
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_ADD));
|
||||
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_MISS));
|
||||
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_HIT));
|
||||
|
||||
// Switch back to kDisable
|
||||
++i;
|
||||
ASSERT_OK(dbfull()->SetOptions(
|
||||
{{"block_based_table_factory", "{prepopulate_block_cache=kDisable;}"}}));
|
||||
|
||||
ASSERT_OK(Put(std::to_string(i), value));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_ADD));
|
||||
|
||||
ASSERT_EQ(value, Get(std::to_string(i)));
|
||||
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_ADD));
|
||||
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_MISS));
|
||||
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_HIT));
|
||||
|
||||
++i;
|
||||
// NOT YET SUPPORTED
|
||||
// FIXME: find a way to make this fail again (until well supported)
|
||||
@@ -1661,7 +1775,7 @@ TEST_P(DBBlockCacheKeyTest, StableCacheKeys) {
|
||||
std::string export_files_dir = dbname_ + "/exported";
|
||||
ExportImportFilesMetaData* metadata_ptr_ = nullptr;
|
||||
Checkpoint* checkpoint;
|
||||
ASSERT_OK(Checkpoint::Create(db_, &checkpoint));
|
||||
ASSERT_OK(Checkpoint::Create(db_.get(), &checkpoint));
|
||||
ASSERT_OK(checkpoint->ExportColumnFamily(handles_[1], export_files_dir,
|
||||
&metadata_ptr_));
|
||||
ASSERT_NE(metadata_ptr_, nullptr);
|
||||
@@ -1698,7 +1812,7 @@ TEST_P(DBBlockCacheKeyTest, StableCacheKeys) {
|
||||
// StableCacheKeyTestFS, Checkpoint will resort to full copy not hard link.
|
||||
// (Checkpoint not available in LITE mode to test this.)
|
||||
auto db_copy_name = dbname_ + "-copy";
|
||||
ASSERT_OK(Checkpoint::Create(db_, &checkpoint));
|
||||
ASSERT_OK(Checkpoint::Create(db_.get(), &checkpoint));
|
||||
ASSERT_OK(checkpoint->CreateCheckpoint(db_copy_name));
|
||||
delete checkpoint;
|
||||
|
||||
|
||||
+14
-16
@@ -137,11 +137,6 @@ class SliceTransformLimitedDomainGeneric : public SliceTransform {
|
||||
// prefix will be x????
|
||||
return src.size() >= 5;
|
||||
}
|
||||
|
||||
bool InRange(const Slice& dst) const override {
|
||||
// prefix will be x????
|
||||
return dst.size() == 5;
|
||||
}
|
||||
};
|
||||
|
||||
// KeyMayExist can lead to a few false positives, but not false negatives.
|
||||
@@ -710,12 +705,20 @@ class AlwaysTrueBitsBuilder : public FilterBitsBuilder {
|
||||
count_ = 0;
|
||||
// Interpreted as "always true" filter (0 probes over 1 byte of
|
||||
// payload, 5 bytes metadata)
|
||||
return Slice("\0\0\0\0\0\0", 6);
|
||||
return Slice("\0\0\0\0\0\0", kAlwaysTrueFilterBytes);
|
||||
}
|
||||
using FilterBitsBuilder::Finish;
|
||||
size_t ApproximateNumEntries(size_t) override { return SIZE_MAX; }
|
||||
size_t CalculateSpace(size_t /* num_entries */) override {
|
||||
return kAlwaysTrueFilterBytes;
|
||||
}
|
||||
double EstimatedFpRate(size_t /* num_entries */,
|
||||
size_t /* bytes */) override {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr size_t kAlwaysTrueFilterBytes = 6;
|
||||
size_t count_ = 0;
|
||||
};
|
||||
|
||||
@@ -914,14 +917,14 @@ INSTANTIATE_TEST_CASE_P(
|
||||
::testing::Values(
|
||||
std::make_tuple(kAutoBloom,
|
||||
FilterPartitioning::kCoupledPartitionedFilter,
|
||||
kLatestFormatVersion),
|
||||
kLatestBbtFormatVersion),
|
||||
std::make_tuple(kAutoBloom,
|
||||
FilterPartitioning::kDecoupledPartitionedFilter,
|
||||
kLatestFormatVersion),
|
||||
kLatestBbtFormatVersion),
|
||||
std::make_tuple(kAutoBloom, FilterPartitioning::kUnpartitionedFilter,
|
||||
kLatestFormatVersion),
|
||||
kLatestBbtFormatVersion),
|
||||
std::make_tuple(kAutoRibbon, FilterPartitioning::kUnpartitionedFilter,
|
||||
kLatestFormatVersion)));
|
||||
kLatestBbtFormatVersion)));
|
||||
#endif // !defined(ROCKSDB_VALGRIND_RUN) || defined(ROCKSDB_FULL_VALGRIND_RUN)
|
||||
|
||||
TEST_F(DBBloomFilterTest, BloomFilterRate) {
|
||||
@@ -2069,11 +2072,6 @@ class SliceTransformLimitedDomain : public SliceTransform {
|
||||
// prefix will be x????
|
||||
return src.size() >= 5 && src[0] == 'x';
|
||||
}
|
||||
|
||||
bool InRange(const Slice& dst) const override {
|
||||
// prefix will be x????
|
||||
return dst.size() == 5 && dst[0] == 'x';
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(DBBloomFilterTest, PrefixExtractorWithFilter1) {
|
||||
@@ -4137,7 +4135,7 @@ TEST_F(DBBloomFilterTest, SstQueryFilter) {
|
||||
|
||||
using Keys = std::vector<std::string>;
|
||||
auto RangeQuery =
|
||||
[factory, db = db_](
|
||||
[factory, db = db_.get()](
|
||||
std::string lb, std::string ub,
|
||||
std::shared_ptr<SstQueryFilterConfigsManager::Factory> alt_factory =
|
||||
nullptr) {
|
||||
|
||||
@@ -0,0 +1,993 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include <atomic>
|
||||
#include <set>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "db/compaction/compaction_job.h"
|
||||
#include "db/db_impl/db_impl_secondary.h"
|
||||
#include "db/db_test_util.h"
|
||||
#include "options/options_helper.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/sst_file_writer.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "test_util/testharness.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
// Helper class to manage abort synchronization in tests.
|
||||
//
|
||||
// Compaction abort could happen at various stage of compaction.
|
||||
// To test this, we need to trigger abort at different stage. This requires
|
||||
// precise control on the timing of abort API invocation. To achieve this in a
|
||||
// consistent way across various tests, we invoke AbortAllCompactions() within
|
||||
// the sync point callback, that is added at various stages of compaction.
|
||||
// However as the abort API is a blocking call, calling it within the sync point
|
||||
// callback on the compaction thread would cause deadlock. This test helper
|
||||
// class is designed to solve this challenge.
|
||||
//
|
||||
// 1. Abort must happen from a different thread:
|
||||
// AbortAllCompactions() is typically called from the compaction thread
|
||||
// via a sync point callback, so that we could precisely control the time of
|
||||
// API invocation to simulate abort at different stage of compaction.
|
||||
// However, we can't block the compaction thread waiting for the abort to
|
||||
// complete - the compaction needs to continue executing to actually check
|
||||
// the abort flag and exit. So we spawn a separate thread to call
|
||||
// AbortAllCompactions().
|
||||
//
|
||||
// 2. We need to know when abort completes:
|
||||
// After compaction returns (with aborted status), we often need to:
|
||||
// - Verify state (e.g., no output files created)
|
||||
// - Call ResumeAllCompactions()
|
||||
// - Run compaction again to verify it succeeds
|
||||
// We must wait for the abort thread to finish before proceeding, otherwise
|
||||
// we might call Resume before Abort completes, causing race conditions.
|
||||
//
|
||||
// 3. Sync point callbacks may fire multiple times:
|
||||
// With multiple subcompactions, a callback like
|
||||
// "CompactionJob::ProcessKeyValueCompaction:Start" fires once per
|
||||
// subcompaction. We only want to trigger abort once, so we use
|
||||
// abort_triggered_ as a guard.
|
||||
//
|
||||
// 4. Tests may need multiple abort cycles:
|
||||
// Some tests (e.g., MultipleAbortResumeSequence) do abort->resume->abort
|
||||
// multiple times. The class supports this by auto-resetting when a
|
||||
// previous abort has completed.
|
||||
class AbortSynchronizer {
|
||||
public:
|
||||
AbortSynchronizer() : abort_cv_(&abort_mutex_) {}
|
||||
|
||||
~AbortSynchronizer() {
|
||||
// Join the thread if it was started - ensures clean shutdown
|
||||
if (abort_thread_.joinable()) {
|
||||
abort_thread_.join();
|
||||
}
|
||||
}
|
||||
|
||||
// Non-copyable, non-movable due to thread member
|
||||
AbortSynchronizer(const AbortSynchronizer&) = delete;
|
||||
AbortSynchronizer& operator=(const AbortSynchronizer&) = delete;
|
||||
|
||||
// Trigger abort from a separate thread.
|
||||
// - Safe to call multiple times; only first call in each cycle spawns thread
|
||||
// - If a previous abort has completed, automatically resets state first
|
||||
// - The spawned thread calls AbortAllCompactions() and signals completion
|
||||
void TriggerAbort(DBImpl* db) {
|
||||
// If previous abort completed, reset state to allow new abort
|
||||
if (abort_triggered_.load() && abort_completed_.load()) {
|
||||
Reset();
|
||||
}
|
||||
|
||||
if (!abort_triggered_.exchange(true)) {
|
||||
abort_thread_ = std::thread([this, db]() {
|
||||
db->AbortAllCompactions();
|
||||
SignalAbortCompleted();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the abort thread to complete.
|
||||
// Call this AFTER compaction returns to ensure the abort thread has finished
|
||||
// before proceeding with Resume or other operations.
|
||||
void WaitForAbortCompletion() {
|
||||
MutexLock l(&abort_mutex_);
|
||||
while (!abort_completed_.load()) {
|
||||
abort_cv_.Wait();
|
||||
}
|
||||
}
|
||||
|
||||
// Reset state for reuse. Joins any previous thread first.
|
||||
// Called automatically by TriggerAbort() if previous abort completed,
|
||||
// but can also be called explicitly for clarity.
|
||||
void Reset() {
|
||||
if (abort_thread_.joinable()) {
|
||||
abort_thread_.join();
|
||||
}
|
||||
abort_triggered_.store(false);
|
||||
abort_completed_.store(false);
|
||||
}
|
||||
|
||||
bool IsAbortTriggered() const { return abort_triggered_.load(); }
|
||||
|
||||
private:
|
||||
void SignalAbortCompleted() {
|
||||
MutexLock l(&abort_mutex_);
|
||||
abort_completed_.store(true);
|
||||
abort_cv_.SignalAll();
|
||||
}
|
||||
|
||||
std::atomic<bool> abort_triggered_{false}; // Guards against multiple spawns
|
||||
std::atomic<bool> abort_completed_{false}; // Signals thread completion
|
||||
port::Mutex abort_mutex_;
|
||||
port::CondVar abort_cv_;
|
||||
std::thread abort_thread_; // The thread that calls AbortAllCompactions()
|
||||
};
|
||||
|
||||
// Helper to clean up SyncPoint state after tests
|
||||
inline void CleanupSyncPoints() {
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
// Helper class that combines AbortSynchronizer with sync point setup for
|
||||
// deterministic abort triggering. This adds sync point coordination on top
|
||||
// of AbortSynchronizer:
|
||||
//
|
||||
// This is useful when you need deterministic timing - the callback won't
|
||||
// return until AbortAllCompactions() has actually set the abort flag,
|
||||
// guaranteeing the compaction will see it on the next check.
|
||||
class SyncPointAbortHelper {
|
||||
public:
|
||||
explicit SyncPointAbortHelper(const std::string& trigger_point)
|
||||
: trigger_point_(trigger_point) {}
|
||||
|
||||
// Set up sync points and callbacks. Call this before starting compaction.
|
||||
void Setup(DBImpl* db_impl) {
|
||||
db_impl_ = db_impl;
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
|
||||
{"DBImpl::AbortAllCompactions:FlagSet", kWaitPointName},
|
||||
});
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
trigger_point_, [this](void* /*arg*/) {
|
||||
// Use AbortSynchronizer to handle the abort in a separate thread
|
||||
abort_sync_.TriggerAbort(db_impl_);
|
||||
|
||||
// Wait for abort flag to be set via sync point dependency
|
||||
// This ensures deterministic timing - compaction will see the flag
|
||||
TEST_SYNC_POINT_CALLBACK(kWaitPointName, nullptr);
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
}
|
||||
|
||||
// Wait for the abort to complete. Call this after compaction returns.
|
||||
void WaitForAbortCompletion() { abort_sync_.WaitForAbortCompletion(); }
|
||||
|
||||
// Clean up sync points and wait for abort completion in one call
|
||||
void CleanupAndWait() {
|
||||
CleanupSyncPoints();
|
||||
WaitForAbortCompletion();
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr const char* kWaitPointName =
|
||||
"SyncPointAbortHelper::WaitForAbort";
|
||||
std::string trigger_point_;
|
||||
DBImpl* db_impl_{nullptr};
|
||||
AbortSynchronizer abort_sync_;
|
||||
};
|
||||
|
||||
class DBCompactionAbortTest : public DBTestBase {
|
||||
public:
|
||||
DBCompactionAbortTest()
|
||||
: DBTestBase("db_compaction_abort_test", /*env_do_fsync=*/false) {}
|
||||
|
||||
protected:
|
||||
// Map to track the latest value of each key for verification
|
||||
std::unordered_map<std::string, std::string> expected_values_;
|
||||
|
||||
// Statistics object for verifying compaction metrics
|
||||
std::shared_ptr<Statistics> stats_;
|
||||
|
||||
// Get current options with statistics enabled
|
||||
Options GetOptionsWithStats() {
|
||||
Options options = CurrentOptions();
|
||||
stats_ = CreateDBStatistics();
|
||||
options.statistics = stats_;
|
||||
return options;
|
||||
}
|
||||
|
||||
// Populate database with test data.
|
||||
// If overlapping=true, uses the same key range (0 to keys_per_file-1) in each
|
||||
// file to ensure compaction has work to do.
|
||||
// If overlapping=false, uses non-overlapping keys across files.
|
||||
void PopulateData(int num_files, int keys_per_file, int value_size,
|
||||
bool overlapping = true, int seed = 301) {
|
||||
Random rnd(seed);
|
||||
for (int i = 0; i < num_files; ++i) {
|
||||
for (int j = 0; j < keys_per_file; ++j) {
|
||||
int key_index = overlapping ? j : (j + i * keys_per_file);
|
||||
std::string key = Key(key_index);
|
||||
std::string value = rnd.RandomString(value_size);
|
||||
ASSERT_OK(Put(key, value));
|
||||
expected_values_[key] = value;
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
}
|
||||
|
||||
// Verify data integrity by reading all keys and comparing with expected
|
||||
// values
|
||||
void VerifyDataIntegrity(int num_keys, int start_key = 0) {
|
||||
std::string val;
|
||||
for (int j = start_key; j < start_key + num_keys; ++j) {
|
||||
std::string key = Key(j);
|
||||
ASSERT_OK(dbfull()->Get(ReadOptions(), key, &val));
|
||||
auto it = expected_values_.find(key);
|
||||
if (it != expected_values_.end()) {
|
||||
ASSERT_EQ(it->second, val) << "Value mismatch for key: " << key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear expected values (useful when reopening DB or between tests)
|
||||
void ClearExpectedValues() { expected_values_.clear(); }
|
||||
|
||||
// Run the common abort test pattern with SyncPointAbortHelper:
|
||||
// 1. Set up sync point abort helper
|
||||
// 2. Run compaction and verify it's aborted
|
||||
// 3. Verify COMPACTION_ABORTED stat increased (if stats enabled)
|
||||
// 4. Clean up, resume, and verify compaction succeeds
|
||||
// 5. Verify COMPACT_WRITE_BYTES increased (if stats enabled)
|
||||
void RunSyncPointAbortTest(const std::string& trigger_point,
|
||||
CompactRangeOptions cro = CompactRangeOptions()) {
|
||||
// Capture stats and file counts before abort
|
||||
uint64_t aborted_before = 0;
|
||||
uint64_t write_bytes_before = 0;
|
||||
if (stats_) {
|
||||
aborted_before = stats_->getTickerCount(COMPACTION_ABORTED);
|
||||
write_bytes_before = stats_->getTickerCount(COMPACT_WRITE_BYTES);
|
||||
}
|
||||
|
||||
SyncPointAbortHelper helper(trigger_point);
|
||||
helper.Setup(dbfull());
|
||||
|
||||
Status s = dbfull()->CompactRange(cro, nullptr, nullptr);
|
||||
ASSERT_TRUE(s.IsIncomplete());
|
||||
ASSERT_TRUE(s.IsCompactionAborted());
|
||||
|
||||
// Verify abort was counted
|
||||
if (stats_) {
|
||||
uint64_t aborted_after = stats_->getTickerCount(COMPACTION_ABORTED);
|
||||
ASSERT_GT(aborted_after, aborted_before)
|
||||
<< "COMPACTION_ABORTED stat should increase after abort";
|
||||
}
|
||||
|
||||
helper.CleanupAndWait();
|
||||
dbfull()->ResumeAllCompactions();
|
||||
|
||||
ASSERT_OK(dbfull()->CompactRange(cro, nullptr, nullptr));
|
||||
|
||||
// Verify compaction completed and wrote bytes
|
||||
if (stats_) {
|
||||
uint64_t write_bytes_after = stats_->getTickerCount(COMPACT_WRITE_BYTES);
|
||||
ASSERT_GT(write_bytes_after, write_bytes_before)
|
||||
<< "COMPACT_WRITE_BYTES should increase after successful compaction";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Parameterized test for abort with different number of max subcompactions.
|
||||
// This consolidates tests that were essentially duplicates with different
|
||||
// max_subcompactions values
|
||||
class DBCompactionAbortSubcompactionTest
|
||||
: public DBCompactionAbortTest,
|
||||
public ::testing::WithParamInterface<int> {};
|
||||
|
||||
TEST_P(DBCompactionAbortSubcompactionTest, AbortWithVaryingSubcompactions) {
|
||||
int max_subcompactions = GetParam();
|
||||
|
||||
Options options = GetOptionsWithStats();
|
||||
options.level0_file_num_compaction_trigger = 4;
|
||||
options.max_subcompactions = max_subcompactions;
|
||||
options.disable_auto_compactions = true;
|
||||
Reopen(options);
|
||||
|
||||
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/100);
|
||||
|
||||
RunSyncPointAbortTest("CompactionJob::RunSubcompactions:BeforeStart");
|
||||
|
||||
VerifyDataIntegrity(/*num_keys=*/100);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(SubcompactionVariants,
|
||||
DBCompactionAbortSubcompactionTest,
|
||||
::testing::Values(1, 2, 4),
|
||||
[](const ::testing::TestParamInfo<int>& param_info) {
|
||||
return "MaxSubcompactionCount_" +
|
||||
std::to_string(param_info.param);
|
||||
});
|
||||
|
||||
// Parameterized test for abort with different compaction styles
|
||||
// This consolidates tests for Level, Universal, and FIFO compaction styles
|
||||
class DBCompactionAbortStyleTest
|
||||
: public DBCompactionAbortTest,
|
||||
public ::testing::WithParamInterface<CompactionStyle> {
|
||||
protected:
|
||||
// Configure options based on compaction style
|
||||
void ConfigureOptionsForStyle(Options& options, CompactionStyle style) {
|
||||
options.compaction_style = style;
|
||||
options.level0_file_num_compaction_trigger = 4;
|
||||
options.disable_auto_compactions = true;
|
||||
|
||||
switch (style) {
|
||||
case kCompactionStyleLevel:
|
||||
// Level compaction uses default settings
|
||||
break;
|
||||
case kCompactionStyleUniversal:
|
||||
options.compaction_options_universal.size_ratio = 10;
|
||||
break;
|
||||
case kCompactionStyleFIFO:
|
||||
// Set a large max_table_files_size to avoid deletion compaction
|
||||
options.compaction_options_fifo.max_table_files_size =
|
||||
100 * 1024 * 1024;
|
||||
// Enable intra-L0 compaction which goes through normal compaction path
|
||||
options.compaction_options_fifo.allow_compaction = true;
|
||||
options.max_open_files = -1; // Required for FIFO compaction
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TEST_P(DBCompactionAbortStyleTest, AbortCompaction) {
|
||||
CompactionStyle style = GetParam();
|
||||
|
||||
Options options = GetOptionsWithStats();
|
||||
options.max_subcompactions = 1;
|
||||
ConfigureOptionsForStyle(options, style);
|
||||
Reopen(options);
|
||||
|
||||
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/100);
|
||||
|
||||
RunSyncPointAbortTest("CompactionJob::RunSubcompactions:BeforeStart");
|
||||
|
||||
VerifyDataIntegrity(/*num_keys=*/100);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
CompactionStyleVariants, DBCompactionAbortStyleTest,
|
||||
::testing::Values(kCompactionStyleLevel, kCompactionStyleUniversal,
|
||||
kCompactionStyleFIFO),
|
||||
[](const ::testing::TestParamInfo<CompactionStyle>& param_info) {
|
||||
return OptionsHelper::compaction_style_to_string.at(param_info.param);
|
||||
});
|
||||
|
||||
TEST_F(DBCompactionAbortTest, AbortManualCompaction) {
|
||||
Options options = GetOptionsWithStats();
|
||||
options.level0_file_num_compaction_trigger = 10;
|
||||
options.disable_auto_compactions = true;
|
||||
Reopen(options);
|
||||
|
||||
PopulateData(/*num_files=*/5, /*keys_per_file=*/100, /*value_size=*/1000);
|
||||
|
||||
CompactRangeOptions cro;
|
||||
cro.exclusive_manual_compaction = true;
|
||||
RunSyncPointAbortTest("CompactionJob::ProcessKeyValueCompaction:Start", cro);
|
||||
|
||||
VerifyDataIntegrity(/*num_keys=*/100);
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, AbortAutomaticCompaction) {
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = 4;
|
||||
options.max_subcompactions = 2;
|
||||
options.disable_auto_compactions = false;
|
||||
Reopen(options);
|
||||
|
||||
Random rnd(301);
|
||||
AbortSynchronizer abort_sync;
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionJob::ProcessKeyValueCompaction:Start",
|
||||
[&](void* /*arg*/) { abort_sync.TriggerAbort(dbfull()); });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
for (int j = 0; j < 100; ++j) {
|
||||
ASSERT_OK(Put(Key(j), rnd.RandomString(1000)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
CleanupSyncPoints();
|
||||
|
||||
abort_sync.WaitForAbortCompletion();
|
||||
dbfull()->ResumeAllCompactions();
|
||||
|
||||
for (int j = 0; j < 100; ++j) {
|
||||
ASSERT_OK(Put(Key(j), rnd.RandomString(1000)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
std::string val;
|
||||
for (int j = 0; j < 100; ++j) {
|
||||
ASSERT_OK(dbfull()->Get(ReadOptions(), Key(j), &val));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, AbortAndVerifyNoOutputFiles) {
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = 4;
|
||||
options.max_subcompactions = 2;
|
||||
options.disable_auto_compactions = true;
|
||||
Reopen(options);
|
||||
|
||||
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/1000);
|
||||
|
||||
int num_l0_files_before = NumTableFilesAtLevel(0);
|
||||
int num_l1_files_before = NumTableFilesAtLevel(1);
|
||||
|
||||
SyncPointAbortHelper helper("CompactionJob::ProcessKeyValueCompaction:Start");
|
||||
helper.Setup(dbfull());
|
||||
|
||||
CompactRangeOptions cro;
|
||||
Status s = dbfull()->CompactRange(cro, nullptr, nullptr);
|
||||
ASSERT_TRUE(s.IsIncomplete());
|
||||
ASSERT_TRUE(s.IsCompactionAborted());
|
||||
|
||||
CleanupSyncPoints();
|
||||
|
||||
int num_l0_files_after = NumTableFilesAtLevel(0);
|
||||
int num_l1_files_after = NumTableFilesAtLevel(1);
|
||||
|
||||
ASSERT_EQ(num_l0_files_before, num_l0_files_after);
|
||||
ASSERT_EQ(num_l1_files_before, num_l1_files_after);
|
||||
|
||||
helper.WaitForAbortCompletion();
|
||||
dbfull()->ResumeAllCompactions();
|
||||
|
||||
ASSERT_OK(dbfull()->CompactRange(cro, nullptr, nullptr));
|
||||
|
||||
int num_l0_files_final = NumTableFilesAtLevel(0);
|
||||
int num_l1_files_final = NumTableFilesAtLevel(1);
|
||||
|
||||
ASSERT_EQ(0, num_l0_files_final);
|
||||
ASSERT_GT(num_l1_files_final, 0);
|
||||
|
||||
VerifyDataIntegrity(/*num_keys=*/100);
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, MultipleAbortResumeSequence) {
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = 4;
|
||||
options.max_subcompactions = 2;
|
||||
options.disable_auto_compactions = true;
|
||||
Reopen(options);
|
||||
|
||||
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/1000);
|
||||
|
||||
for (int round = 0; round < 3; ++round) {
|
||||
// Use SyncPointAbortHelper for deterministic abort timing - it waits
|
||||
// for the abort flag to be set via sync point dependency
|
||||
SyncPointAbortHelper helper(
|
||||
"CompactionJob::ProcessKeyValueCompaction:Start");
|
||||
helper.Setup(dbfull());
|
||||
|
||||
CompactRangeOptions cro;
|
||||
Status s = dbfull()->CompactRange(cro, nullptr, nullptr);
|
||||
ASSERT_TRUE(s.IsIncomplete());
|
||||
ASSERT_TRUE(s.IsCompactionAborted());
|
||||
|
||||
helper.CleanupAndWait();
|
||||
dbfull()->ResumeAllCompactions();
|
||||
}
|
||||
|
||||
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
VerifyDataIntegrity(/*num_keys=*/100);
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, AbortWithOutputFilesCleanup) {
|
||||
Options options = CurrentOptions();
|
||||
options.num_levels = 2; // Ensure compaction output goes to L1
|
||||
options.level0_file_num_compaction_trigger = 4;
|
||||
options.max_subcompactions = 2;
|
||||
options.disable_auto_compactions = true;
|
||||
options.target_file_size_base = 50 * 1024;
|
||||
Reopen(options);
|
||||
|
||||
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/100);
|
||||
|
||||
SyncPointAbortHelper helper("CompactionJob::RunSubcompactions:BeforeStart");
|
||||
helper.Setup(dbfull());
|
||||
|
||||
CompactRangeOptions cro;
|
||||
Status s = dbfull()->CompactRange(cro, nullptr, nullptr);
|
||||
ASSERT_TRUE(s.IsIncomplete());
|
||||
ASSERT_TRUE(s.IsCompactionAborted());
|
||||
|
||||
CleanupSyncPoints();
|
||||
|
||||
int num_l1_files_after_abort = NumTableFilesAtLevel(1);
|
||||
ASSERT_EQ(0, num_l1_files_after_abort);
|
||||
|
||||
helper.WaitForAbortCompletion();
|
||||
dbfull()->ResumeAllCompactions();
|
||||
|
||||
ASSERT_OK(dbfull()->CompactRange(cro, nullptr, nullptr));
|
||||
|
||||
// Verify L0 files are compacted and L1 has output files
|
||||
int num_l0_files_final = NumTableFilesAtLevel(0);
|
||||
int num_l1_files_final = NumTableFilesAtLevel(1);
|
||||
ASSERT_EQ(0, num_l0_files_final)
|
||||
<< "L0 should be empty after successful compaction";
|
||||
ASSERT_GT(num_l1_files_final, 0)
|
||||
<< "L1 should have files after successful compaction";
|
||||
|
||||
VerifyDataIntegrity(/*num_keys=*/100);
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, NestedAbortResumeCalls) {
|
||||
// Test that nested AbortAllCompactions() calls work correctly with the
|
||||
// counter
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = 4;
|
||||
options.max_subcompactions = 2;
|
||||
options.disable_auto_compactions = true;
|
||||
Reopen(options);
|
||||
|
||||
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/1000);
|
||||
|
||||
// First abort call
|
||||
dbfull()->AbortAllCompactions();
|
||||
|
||||
// Nested abort call (counter should be 2)
|
||||
dbfull()->AbortAllCompactions();
|
||||
|
||||
// Compaction should still be blocked after one resume
|
||||
dbfull()->ResumeAllCompactions();
|
||||
|
||||
// Compaction should still return aborted because counter is still 1
|
||||
CompactRangeOptions cro;
|
||||
Status s = dbfull()->CompactRange(cro, nullptr, nullptr);
|
||||
ASSERT_TRUE(s.IsIncomplete());
|
||||
ASSERT_TRUE(s.IsCompactionAborted());
|
||||
|
||||
// Second resume - counter should be 0 now
|
||||
dbfull()->ResumeAllCompactions();
|
||||
|
||||
// Compaction should succeed now
|
||||
ASSERT_OK(dbfull()->CompactRange(cro, nullptr, nullptr));
|
||||
|
||||
VerifyDataIntegrity(/*num_keys=*/100);
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, AbortCompactFilesAPI) {
|
||||
// Test that AbortAllCompactions works with CompactFiles API
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = 100; // Disable auto compaction
|
||||
options.disable_auto_compactions = true;
|
||||
Reopen(options);
|
||||
|
||||
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/1000);
|
||||
|
||||
// Get the L0 file names
|
||||
std::vector<std::string> files_to_compact;
|
||||
ColumnFamilyMetaData cf_meta;
|
||||
dbfull()->GetColumnFamilyMetaData(dbfull()->DefaultColumnFamily(), &cf_meta);
|
||||
for (const auto& file : cf_meta.levels[0].files) {
|
||||
files_to_compact.push_back(file.name);
|
||||
}
|
||||
ASSERT_GE(files_to_compact.size(), 2);
|
||||
|
||||
SyncPointAbortHelper helper("CompactionJob::ProcessKeyValueCompaction:Start");
|
||||
helper.Setup(dbfull());
|
||||
|
||||
CompactionOptions compact_options;
|
||||
Status s = dbfull()->CompactFiles(compact_options, files_to_compact, 1);
|
||||
ASSERT_TRUE(s.IsIncomplete());
|
||||
ASSERT_TRUE(s.IsCompactionAborted());
|
||||
|
||||
helper.CleanupAndWait();
|
||||
dbfull()->ResumeAllCompactions();
|
||||
|
||||
// CompactFiles should work after resume
|
||||
ASSERT_OK(dbfull()->CompactFiles(compact_options, files_to_compact, 1));
|
||||
|
||||
VerifyDataIntegrity(/*num_keys=*/100);
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, AbortDoesNotAffectFlush) {
|
||||
// Test that AbortAllCompactions does not affect flush operations
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = 100;
|
||||
options.disable_auto_compactions = true;
|
||||
Reopen(options);
|
||||
|
||||
Random rnd(301);
|
||||
for (int j = 0; j < 100; ++j) {
|
||||
ASSERT_OK(Put(Key(j), rnd.RandomString(1000)));
|
||||
}
|
||||
|
||||
// Abort compactions
|
||||
dbfull()->AbortAllCompactions();
|
||||
|
||||
// Flush should still work
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Write more data
|
||||
for (int j = 100; j < 200; ++j) {
|
||||
ASSERT_OK(Put(Key(j), rnd.RandomString(1000)));
|
||||
}
|
||||
|
||||
// Flush should still work
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Resume compactions
|
||||
dbfull()->ResumeAllCompactions();
|
||||
|
||||
VerifyDataIntegrity(/*num_keys=*/200);
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, AbortBeforeCompactionStarts) {
|
||||
// Test aborting before any compaction has started
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = 4;
|
||||
options.disable_auto_compactions = true;
|
||||
Reopen(options);
|
||||
|
||||
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/1000);
|
||||
|
||||
// Abort before starting compaction
|
||||
dbfull()->AbortAllCompactions();
|
||||
|
||||
// Compaction should immediately return aborted
|
||||
CompactRangeOptions cro;
|
||||
Status s = dbfull()->CompactRange(cro, nullptr, nullptr);
|
||||
ASSERT_TRUE(s.IsIncomplete());
|
||||
ASSERT_TRUE(s.IsCompactionAborted());
|
||||
|
||||
// Resume
|
||||
dbfull()->ResumeAllCompactions();
|
||||
|
||||
// Now compaction should work
|
||||
ASSERT_OK(dbfull()->CompactRange(cro, nullptr, nullptr));
|
||||
|
||||
// Verify L0 files are compacted
|
||||
ASSERT_EQ(0, NumTableFilesAtLevel(0));
|
||||
}
|
||||
|
||||
// Test that in-progress blob and SST files are properly cleaned up when
|
||||
// compaction is aborted. This specifically tests the case where abort happens
|
||||
// while files are being written (opened but not yet completed/closed).
|
||||
// This catches the bug where files exist on disk but are removed from the
|
||||
// outputs_ vector (e.g., by RemoveLastEmptyOutput when file_size is 0 because
|
||||
// the builder was abandoned), leaving orphan files.
|
||||
TEST_F(DBCompactionAbortTest, AbortWithInProgressFileCleanup) {
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = 4;
|
||||
options.max_subcompactions =
|
||||
1; // Single subcompaction for deterministic behavior
|
||||
options.disable_auto_compactions = true;
|
||||
options.target_file_size_base = 32 * 1024; // 32KB
|
||||
|
||||
// Enable BlobDB with garbage collection to force blob rewriting during
|
||||
// compaction
|
||||
options.enable_blob_files = true;
|
||||
options.min_blob_size = 0; // All values go to blob files
|
||||
options.blob_file_size =
|
||||
1024 * 1024; // 1MB - large enough to not close during test
|
||||
// Enable blob garbage collection - this forces blob data to be rewritten
|
||||
// during compaction, creating new blob files
|
||||
options.enable_blob_garbage_collection = true;
|
||||
options.blob_garbage_collection_age_cutoff = 1.0; // Include all blob files
|
||||
options.blob_garbage_collection_force_threshold = 0.0; // Always force GC
|
||||
|
||||
Reopen(options);
|
||||
|
||||
// Write enough data to trigger the periodic abort check (every 1000 records).
|
||||
// 4 files * 2000 keys = 2000 unique overlapping keys processed during
|
||||
// compaction. The sync point triggers at 999, 1999, etc.
|
||||
PopulateData(/*num_files=*/4, /*keys_per_file=*/2000, /*value_size=*/500);
|
||||
|
||||
// Helper function to get blob files on disk with their names
|
||||
auto GetBlobFilesOnDisk = [this]() -> std::vector<std::string> {
|
||||
std::vector<std::string> blob_files;
|
||||
std::vector<std::string> files;
|
||||
EXPECT_OK(env_->GetChildren(dbname_, &files));
|
||||
for (const auto& f : files) {
|
||||
if (f.find(".blob") != std::string::npos) {
|
||||
blob_files.push_back(f);
|
||||
}
|
||||
}
|
||||
std::sort(blob_files.begin(), blob_files.end());
|
||||
return blob_files;
|
||||
};
|
||||
|
||||
// Helper function to get blob file count in metadata
|
||||
auto GetBlobFilesInMetadata = [this]() -> std::vector<uint64_t> {
|
||||
std::vector<uint64_t> blob_file_numbers;
|
||||
ColumnFamilyMetaData cf_meta;
|
||||
dbfull()->GetColumnFamilyMetaData(db_->DefaultColumnFamily(), &cf_meta);
|
||||
for (const auto& blob_meta : cf_meta.blob_files) {
|
||||
blob_file_numbers.push_back(blob_meta.blob_file_number);
|
||||
}
|
||||
std::sort(blob_file_numbers.begin(), blob_file_numbers.end());
|
||||
return blob_file_numbers;
|
||||
};
|
||||
|
||||
// Helper function to get SST files on disk
|
||||
auto GetSstFilesOnDisk = [this]() -> std::vector<std::string> {
|
||||
std::vector<std::string> sst_files;
|
||||
std::vector<std::string> files;
|
||||
EXPECT_OK(env_->GetChildren(dbname_, &files));
|
||||
for (const auto& f : files) {
|
||||
if (f.find(".sst") != std::string::npos) {
|
||||
sst_files.push_back(f);
|
||||
}
|
||||
}
|
||||
std::sort(sst_files.begin(), sst_files.end());
|
||||
return sst_files;
|
||||
};
|
||||
|
||||
// Helper function to get SST file numbers in metadata
|
||||
auto GetSstFilesInMetadata = [this]() -> std::vector<uint64_t> {
|
||||
std::vector<uint64_t> sst_file_numbers;
|
||||
ColumnFamilyMetaData cf_meta;
|
||||
dbfull()->GetColumnFamilyMetaData(db_->DefaultColumnFamily(), &cf_meta);
|
||||
for (const auto& level : cf_meta.levels) {
|
||||
for (const auto& file : level.files) {
|
||||
// Extract file number from the file name (e.g., "000010.sst" -> 10)
|
||||
uint64_t file_num = 0;
|
||||
std::string fname = file.name;
|
||||
// Remove leading path separators if present
|
||||
size_t pos = fname.rfind('/');
|
||||
if (pos != std::string::npos) {
|
||||
fname = fname.substr(pos + 1);
|
||||
}
|
||||
if (sscanf(fname.c_str(), "%" PRIu64, &file_num) == 1) {
|
||||
sst_file_numbers.push_back(file_num);
|
||||
}
|
||||
}
|
||||
}
|
||||
std::sort(sst_file_numbers.begin(), sst_file_numbers.end());
|
||||
return sst_file_numbers;
|
||||
};
|
||||
|
||||
std::vector<std::string> initial_blob_files = GetBlobFilesOnDisk();
|
||||
std::vector<uint64_t> initial_meta_blobs = GetBlobFilesInMetadata();
|
||||
std::vector<std::string> initial_sst_files = GetSstFilesOnDisk();
|
||||
std::vector<uint64_t> initial_meta_ssts = GetSstFilesInMetadata();
|
||||
|
||||
ASSERT_GT(initial_blob_files.size(), 0u) << "Expected initial blob files";
|
||||
ASSERT_EQ(initial_blob_files.size(), initial_meta_blobs.size())
|
||||
<< "Initial blob files should match between disk and metadata";
|
||||
ASSERT_GT(initial_sst_files.size(), 0u) << "Expected initial SST files";
|
||||
ASSERT_EQ(initial_sst_files.size(), initial_meta_ssts.size())
|
||||
<< "Initial SST files should match between disk and metadata";
|
||||
|
||||
// Tracking variables for blob file lifecycle
|
||||
std::atomic<int> blob_writes{0};
|
||||
std::atomic<bool> abort_triggered{false};
|
||||
AbortSynchronizer abort_sync;
|
||||
|
||||
// Set up dependency: the wait point will block until FlagSet is hit
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
|
||||
{"DBImpl::AbortAllCompactions:FlagSet",
|
||||
"DBCompactionAbortTest::InProgressBlob:WaitForAbort"},
|
||||
});
|
||||
|
||||
// Trigger abort after some blob writes during compaction output.
|
||||
// This ensures we have an in-progress blob file when abort happens.
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"BlobFileBuilder::WriteBlobToFile:AddRecord", [&](void* /*arg*/) {
|
||||
int count = blob_writes.fetch_add(1) + 1;
|
||||
|
||||
// Trigger abort after 100 blob writes - this ensures:
|
||||
// 1. A blob file has been opened (for writing)
|
||||
// 2. Some data has been written to it
|
||||
// 3. But it's not yet completed (blob_file_size is 1MB)
|
||||
if (count == 100 && !abort_triggered.exchange(true)) {
|
||||
abort_sync.TriggerAbort(dbfull());
|
||||
// Wait for abort flag to be set - this sync point blocks until
|
||||
// FlagSet is processed
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"DBCompactionAbortTest::InProgressBlob:WaitForAbort", nullptr);
|
||||
}
|
||||
});
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Run compaction - it should be aborted while blob file is in-progress
|
||||
CompactRangeOptions cro;
|
||||
Status s = dbfull()->CompactRange(cro, nullptr, nullptr);
|
||||
|
||||
ASSERT_TRUE(s.IsIncomplete())
|
||||
<< "Expected compaction to be aborted, got: " << s.ToString();
|
||||
|
||||
CleanupSyncPoints();
|
||||
abort_sync.WaitForAbortCompletion();
|
||||
|
||||
// Check state after abort
|
||||
std::vector<std::string> post_abort_disk_blobs = GetBlobFilesOnDisk();
|
||||
std::vector<uint64_t> post_abort_meta_blobs = GetBlobFilesInMetadata();
|
||||
std::vector<std::string> post_abort_disk_ssts = GetSstFilesOnDisk();
|
||||
std::vector<uint64_t> post_abort_meta_ssts = GetSstFilesInMetadata();
|
||||
|
||||
// This is the key assertion for blob files: files on disk should match
|
||||
// metadata. If the in-progress blob file was NOT cleaned up, there will be an
|
||||
// extra file on disk that's not in metadata (orphan).
|
||||
ASSERT_EQ(post_abort_disk_blobs.size(), post_abort_meta_blobs.size())
|
||||
<< "Orphan blob file detected! In-progress blob file was not cleaned up "
|
||||
"after abort. Files on disk: "
|
||||
<< post_abort_disk_blobs.size()
|
||||
<< ", Files in metadata: " << post_abort_meta_blobs.size()
|
||||
<< ". The difference indicates orphaned in-progress blob file(s).";
|
||||
|
||||
// This is the key assertion for SST files: files on disk should match
|
||||
// metadata. If the in-progress SST file was NOT cleaned up, there will be an
|
||||
// extra file on disk that's not in metadata (orphan).
|
||||
ASSERT_EQ(post_abort_disk_ssts.size(), post_abort_meta_ssts.size())
|
||||
<< "Orphan SST file detected! In-progress SST file was not cleaned up "
|
||||
"after abort. Files on disk: "
|
||||
<< post_abort_disk_ssts.size()
|
||||
<< ", Files in metadata: " << post_abort_meta_ssts.size()
|
||||
<< ". The difference indicates orphaned in-progress SST file(s).";
|
||||
|
||||
// Resume and complete compaction to verify DB is still functional
|
||||
dbfull()->ResumeAllCompactions();
|
||||
|
||||
ASSERT_OK(dbfull()->CompactRange(cro, nullptr, nullptr));
|
||||
|
||||
// Verify data integrity - we wrote 4 files * 2000 keys with overlapping keys
|
||||
VerifyDataIntegrity(/*num_keys=*/2000);
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, AbortBottommostLevelCompaction) {
|
||||
Options options = CurrentOptions();
|
||||
options.num_levels = 4;
|
||||
options.level0_file_num_compaction_trigger = 2;
|
||||
options.max_bytes_for_level_base = 1024 * 10; // 10KB
|
||||
options.max_bytes_for_level_multiplier = 2;
|
||||
options.disable_auto_compactions = true;
|
||||
Reopen(options);
|
||||
|
||||
// Write data to fill multiple levels (non-overlapping keys)
|
||||
PopulateData(/*num_files=*/6, /*keys_per_file=*/100,
|
||||
/*value_size=*/500, /*overlapping=*/false);
|
||||
|
||||
// First compact to push data to lower levels
|
||||
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
// Write more data to L0 (overlapping keys)
|
||||
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/500);
|
||||
|
||||
SyncPointAbortHelper helper("CompactionJob::ProcessKeyValueCompaction:Start");
|
||||
helper.Setup(dbfull());
|
||||
|
||||
// Trigger bottommost level compaction
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
Status s = dbfull()->CompactRange(cro, nullptr, nullptr);
|
||||
ASSERT_TRUE(s.IsIncomplete());
|
||||
ASSERT_TRUE(s.IsCompactionAborted());
|
||||
|
||||
helper.CleanupAndWait();
|
||||
dbfull()->ResumeAllCompactions();
|
||||
|
||||
ASSERT_OK(dbfull()->CompactRange(cro, nullptr, nullptr));
|
||||
|
||||
VerifyDataIntegrity(/*num_keys=*/600);
|
||||
}
|
||||
|
||||
// Test that while compactions are aborted, atomic range replace
|
||||
// (IngestExternalFiles with atomic_replace_range) works correctly.
|
||||
// This verifies that the abort state doesn't block other write operations
|
||||
// like atomic range replace.
|
||||
TEST_F(DBCompactionAbortTest, AbortThenAtomicRangeReplace) {
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = 4;
|
||||
options.max_subcompactions = 2;
|
||||
options.disable_auto_compactions = true;
|
||||
Reopen(options);
|
||||
|
||||
// Create a directory for SST files
|
||||
std::string sst_files_dir = dbname_ + "_sst_files/";
|
||||
ASSERT_OK(env_->CreateDirIfMissing(sst_files_dir));
|
||||
|
||||
// Populate initial data with overlapping keys
|
||||
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/500);
|
||||
|
||||
// Verify initial data
|
||||
VerifyDataIntegrity(/*num_keys=*/100);
|
||||
|
||||
// Trigger compaction and abort it
|
||||
SyncPointAbortHelper helper("CompactionJob::ProcessKeyValueCompaction:Start");
|
||||
helper.Setup(dbfull());
|
||||
|
||||
CompactRangeOptions cro;
|
||||
Status s = dbfull()->CompactRange(cro, nullptr, nullptr);
|
||||
ASSERT_TRUE(s.IsIncomplete());
|
||||
ASSERT_TRUE(s.IsCompactionAborted());
|
||||
|
||||
helper.CleanupAndWait();
|
||||
|
||||
// While compaction is still aborted, perform atomic range replace using
|
||||
// IngestExternalFiles with atomic_replace_range. This verifies that the
|
||||
// abort state doesn't block other write operations.
|
||||
// Using RangeOpt() (empty range) means replace everything in the CF.
|
||||
|
||||
// Create an SST file with new data for keys 0-49 (replacing keys 0-99)
|
||||
std::string sst_file_path = sst_files_dir + "atomic_replace_1.sst";
|
||||
SstFileWriter sst_file_writer(EnvOptions(), options);
|
||||
ASSERT_OK(sst_file_writer.Open(sst_file_path));
|
||||
|
||||
// Write new values for keys 0-49
|
||||
Random rnd(42);
|
||||
std::unordered_map<std::string, std::string> new_values;
|
||||
for (int j = 0; j < 50; ++j) {
|
||||
std::string key = Key(j);
|
||||
std::string value = "replaced_" + rnd.RandomString(100);
|
||||
ASSERT_OK(sst_file_writer.Put(key, value));
|
||||
new_values[key] = value;
|
||||
}
|
||||
ASSERT_OK(sst_file_writer.Finish());
|
||||
|
||||
// Perform atomic range replace for the entire column family.
|
||||
// Using RangeOpt() (default constructor) means replace everything in the CF.
|
||||
IngestExternalFileArg arg;
|
||||
arg.column_family = db_->DefaultColumnFamily();
|
||||
arg.external_files = {sst_file_path};
|
||||
arg.atomic_replace_range = RangeOpt();
|
||||
// snapshot_consistency must be false when using atomic_replace_range
|
||||
arg.options.snapshot_consistency = false;
|
||||
|
||||
// Atomic range replace should work even while compactions are aborted
|
||||
ASSERT_OK(db_->IngestExternalFiles({arg}));
|
||||
|
||||
// Now resume compactions after the atomic range replace
|
||||
dbfull()->ResumeAllCompactions();
|
||||
|
||||
// Verify that the atomic range replace worked correctly:
|
||||
// 1. Keys 0-49 should have new replaced values
|
||||
std::string val;
|
||||
for (int j = 0; j < 50; ++j) {
|
||||
std::string key = Key(j);
|
||||
ASSERT_OK(db_->Get(ReadOptions(), key, &val));
|
||||
auto it = new_values.find(key);
|
||||
ASSERT_NE(it, new_values.end());
|
||||
ASSERT_EQ(it->second, val) << "Value mismatch for replaced key: " << key;
|
||||
}
|
||||
|
||||
// 2. Keys 50-99 should not exist (they were replaced/deleted by atomic
|
||||
// replace)
|
||||
for (int j = 50; j < 100; ++j) {
|
||||
std::string key = Key(j);
|
||||
Status get_status = db_->Get(ReadOptions(), key, &val);
|
||||
ASSERT_TRUE(get_status.IsNotFound())
|
||||
<< "Key " << key << " should not exist after full CF replace";
|
||||
}
|
||||
|
||||
// Clean up SST files directory
|
||||
ASSERT_OK(DestroyDir(env_, sst_files_dir));
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
ROCKSDB_NAMESPACE::port::InstallStackTraceHandler();
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
+394
-29
@@ -14,11 +14,13 @@
|
||||
#include "db/db_test_util.h"
|
||||
#include "db/dbformat.h"
|
||||
#include "env/mock_env.h"
|
||||
#include "file/filename.h"
|
||||
#include "port/port.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/advanced_options.h"
|
||||
#include "rocksdb/concurrent_task_limiter.h"
|
||||
#include "rocksdb/experimental.h"
|
||||
#include "rocksdb/file_checksum.h"
|
||||
#include "rocksdb/iostats_context.h"
|
||||
#include "rocksdb/sst_file_writer.h"
|
||||
#include "test_util/mock_time_env.h"
|
||||
@@ -797,6 +799,13 @@ TEST_F(DBCompactionTest, CompactRangeBottomPri) {
|
||||
// and one compact to L2 in bottom pri pool.
|
||||
int low_pri_count = 0;
|
||||
int bottom_pri_count = 0;
|
||||
bool bottom_running_seen = false;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"BackgroundCallCompaction:1", [&](void*) {
|
||||
if (dbfull()->TEST_NumRunningBottomCompactions() > 0) {
|
||||
bottom_running_seen = true;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"ThreadPoolImpl::Impl::BGThread:BeforeRun", [&](void* arg) {
|
||||
Env::Priority* pri = static_cast<Env::Priority*>(arg);
|
||||
@@ -815,6 +824,7 @@ TEST_F(DBCompactionTest, CompactRangeBottomPri) {
|
||||
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
ASSERT_EQ(1, low_pri_count);
|
||||
ASSERT_EQ(1, bottom_pri_count);
|
||||
ASSERT_TRUE(bottom_running_seen);
|
||||
ASSERT_EQ("0,0,2", FilesPerLevel(0));
|
||||
|
||||
// Recompact bottom most level uses bottom pool
|
||||
@@ -2208,7 +2218,8 @@ TEST_P(DBDeleteFileRangeTest, DeleteFileRange) {
|
||||
std::string end_string = Key(2000);
|
||||
Slice begin(begin_string);
|
||||
Slice end(end_string);
|
||||
ASSERT_OK(DeleteFilesInRange(db_, db_->DefaultColumnFamily(), &begin, &end));
|
||||
ASSERT_OK(
|
||||
DeleteFilesInRange(db_.get(), db_->DefaultColumnFamily(), &begin, &end));
|
||||
|
||||
int32_t deleted_count = 0;
|
||||
for (int32_t i = 0; i < 4300; i++) {
|
||||
@@ -2229,8 +2240,8 @@ TEST_P(DBDeleteFileRangeTest, DeleteFileRange) {
|
||||
Slice begin1(begin_string);
|
||||
Slice end1(end_string);
|
||||
// Try deleting files in range which contain no keys
|
||||
ASSERT_OK(
|
||||
DeleteFilesInRange(db_, db_->DefaultColumnFamily(), &begin1, &end1));
|
||||
ASSERT_OK(DeleteFilesInRange(db_.get(), db_->DefaultColumnFamily(), &begin1,
|
||||
&end1));
|
||||
|
||||
// Push data from level 0 to level 1 to force all data to be deleted
|
||||
// Note that we don't delete level 0 files
|
||||
@@ -2239,8 +2250,8 @@ TEST_P(DBDeleteFileRangeTest, DeleteFileRange) {
|
||||
ASSERT_OK(db_->CompactRange(compact_options, nullptr, nullptr));
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
ASSERT_OK(
|
||||
DeleteFilesInRange(db_, db_->DefaultColumnFamily(), nullptr, nullptr));
|
||||
ASSERT_OK(DeleteFilesInRange(db_.get(), db_->DefaultColumnFamily(), nullptr,
|
||||
nullptr));
|
||||
|
||||
int32_t deleted_count2 = 0;
|
||||
for (int32_t i = 0; i < 4300; i++) {
|
||||
@@ -2308,7 +2319,7 @@ TEST_P(DBDeleteFileRangeTest, DeleteFilesInRanges) {
|
||||
ranges.emplace_back(begin_str1, end_str1);
|
||||
ranges.emplace_back(begin_str2, end_str2);
|
||||
ranges.emplace_back(begin_str3, end_str3);
|
||||
ASSERT_OK(DeleteFilesInRanges(db_, db_->DefaultColumnFamily(),
|
||||
ASSERT_OK(DeleteFilesInRanges(db_.get(), db_->DefaultColumnFamily(),
|
||||
ranges.data(), ranges.size()));
|
||||
ASSERT_EQ("0,3,7", FilesPerLevel(0));
|
||||
|
||||
@@ -2335,7 +2346,7 @@ TEST_P(DBDeleteFileRangeTest, DeleteFilesInRanges) {
|
||||
ranges.emplace_back(&begin1, &end1);
|
||||
ranges.emplace_back(&begin2, &end2);
|
||||
ranges.emplace_back(&begin3, &end3);
|
||||
ASSERT_OK(DeleteFilesInRanges(db_, db_->DefaultColumnFamily(),
|
||||
ASSERT_OK(DeleteFilesInRanges(db_.get(), db_->DefaultColumnFamily(),
|
||||
ranges.data(), ranges.size(), false));
|
||||
ASSERT_EQ("0,1,4", FilesPerLevel(0));
|
||||
|
||||
@@ -2356,7 +2367,8 @@ TEST_P(DBDeleteFileRangeTest, DeleteFilesInRanges) {
|
||||
// Delete all files.
|
||||
{
|
||||
RangeOpt range;
|
||||
ASSERT_OK(DeleteFilesInRanges(db_, db_->DefaultColumnFamily(), &range, 1));
|
||||
ASSERT_OK(
|
||||
DeleteFilesInRanges(db_.get(), db_->DefaultColumnFamily(), &range, 1));
|
||||
ASSERT_EQ("", FilesPerLevel(0));
|
||||
|
||||
for (auto i = 0; i < 1000; i++) {
|
||||
@@ -2418,7 +2430,8 @@ TEST_P(DBDeleteFileRangeTest, DeleteFileRangeFileEndpointsOverlapBug) {
|
||||
// "1 -> vals[0]" to reappear.
|
||||
std::string begin_str = Key(0), end_str = Key(1);
|
||||
Slice begin = begin_str, end = end_str;
|
||||
ASSERT_OK(DeleteFilesInRange(db_, db_->DefaultColumnFamily(), &begin, &end));
|
||||
ASSERT_OK(
|
||||
DeleteFilesInRange(db_.get(), db_->DefaultColumnFamily(), &begin, &end));
|
||||
ASSERT_EQ(vals[1], GetValue(Key(1)));
|
||||
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
@@ -3657,7 +3670,7 @@ TEST_F(DBCompactionTest, SuggestCompactRangeNoTwoLevel0Compactions) {
|
||||
|
||||
GenerateNewRandomFile(&rnd, /* nowait */ true);
|
||||
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
ASSERT_OK(experimental::SuggestCompactRange(db_, nullptr, nullptr));
|
||||
ASSERT_OK(experimental::SuggestCompactRange(db_.get(), nullptr, nullptr));
|
||||
for (int num = 0; num < options.level0_file_num_compaction_trigger + 1;
|
||||
num++) {
|
||||
GenerateNewRandomFile(&rnd, /* nowait */ true);
|
||||
@@ -4533,7 +4546,8 @@ TEST_F(DBCompactionTest, DeleteFilesInRangeConflictWithCompaction) {
|
||||
std::string end_string = Key(kMaxKey + 1);
|
||||
Slice begin(begin_string);
|
||||
Slice end(end_string);
|
||||
ASSERT_OK(DeleteFilesInRange(db_, db_->DefaultColumnFamily(), &begin, &end));
|
||||
ASSERT_OK(
|
||||
DeleteFilesInRange(db_.get(), db_->DefaultColumnFamily(), &begin, &end));
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
}
|
||||
|
||||
@@ -7840,7 +7854,7 @@ class DBCompactionTestL0FilesMisorderCorruption : public DBCompactionTest {
|
||||
options_.level0_file_num_compaction_trigger = 3;
|
||||
|
||||
CompactionOptionsFIFO fifo_options;
|
||||
if (compaction_path_to_test == "FindIntraL0Compaction" ||
|
||||
if (compaction_path_to_test == "PickCostBasedIntraL0Compaction" ||
|
||||
compaction_path_to_test == "CompactRange") {
|
||||
fifo_options.allow_compaction = true;
|
||||
} else if (compaction_path_to_test == "CompactFile") {
|
||||
@@ -7940,7 +7954,7 @@ class DBCompactionTestL0FilesMisorderCorruption : public DBCompactionTest {
|
||||
|
||||
void SetupSyncPoints(const std::string& compaction_path_to_test) {
|
||||
compaction_path_sync_point_called_.store(false);
|
||||
if (compaction_path_to_test == "FindIntraL0Compaction" &&
|
||||
if (compaction_path_to_test == "PickCostBasedIntraL0Compaction" &&
|
||||
options_.compaction_style == CompactionStyle::kCompactionStyleLevel) {
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"PostPickFileToCompact", [&](void* arg) {
|
||||
@@ -7950,7 +7964,7 @@ class DBCompactionTestL0FilesMisorderCorruption : public DBCompactionTest {
|
||||
*picked_file_to_compact = false;
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"FindIntraL0Compaction", [&](void* /*arg*/) {
|
||||
"PickCostBasedIntraL0Compaction", [&](void* /*arg*/) {
|
||||
compaction_path_sync_point_called_.store(true);
|
||||
});
|
||||
|
||||
@@ -7986,12 +8000,12 @@ class DBCompactionTestL0FilesMisorderCorruption : public DBCompactionTest {
|
||||
"PickDeleteTriggeredCompactionReturnNonnullptr", [&](void* /*arg*/) {
|
||||
compaction_path_sync_point_called_.store(true);
|
||||
});
|
||||
} else if ((compaction_path_to_test == "FindIntraL0Compaction" ||
|
||||
} else if ((compaction_path_to_test == "PickCostBasedIntraL0Compaction" ||
|
||||
compaction_path_to_test == "CompactRange") &&
|
||||
options_.compaction_style ==
|
||||
CompactionStyle::kCompactionStyleFIFO) {
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"FindIntraL0Compaction", [&](void* /*arg*/) {
|
||||
"PickCostBasedIntraL0Compaction", [&](void* /*arg*/) {
|
||||
compaction_path_sync_point_called_.store(true);
|
||||
});
|
||||
}
|
||||
@@ -8063,7 +8077,7 @@ TEST_F(DBCompactionTest, CompactFilesSupportKeyPlacementRangeConflict) {
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put("k4", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(experimental::PromoteL0(db_, db_->DefaultColumnFamily(), 1));
|
||||
ASSERT_OK(experimental::PromoteL0(db_.get(), db_->DefaultColumnFamily(), 1));
|
||||
ASSERT_EQ("0,2,1", FilesPerLevel());
|
||||
|
||||
ASSERT_OK(Put("k2", "v"));
|
||||
@@ -8151,7 +8165,7 @@ TEST_F(DBCompactionTestL0FilesMisorderCorruption,
|
||||
IngestOneKeyValue(dbfull(), Key(i), "new", options_);
|
||||
}
|
||||
|
||||
SetupSyncPoints("FindIntraL0Compaction");
|
||||
SetupSyncPoints("PickCostBasedIntraL0Compaction");
|
||||
ResumeCompactionThread();
|
||||
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
@@ -8284,7 +8298,8 @@ TEST_F(DBCompactionTestL0FilesMisorderCorruption,
|
||||
|
||||
TEST_F(DBCompactionTestL0FilesMisorderCorruption,
|
||||
FlushAfterIntraL0FIFOCompactionWithIngestedFile) {
|
||||
for (const std::string compaction_path_to_test : {"FindIntraL0Compaction"}) {
|
||||
for (const std::string compaction_path_to_test :
|
||||
{"PickCostBasedIntraL0Compaction"}) {
|
||||
SetupOptions(CompactionStyle::kCompactionStyleFIFO,
|
||||
compaction_path_to_test);
|
||||
DestroyAndReopen(options_);
|
||||
@@ -11483,20 +11498,24 @@ TEST_F(DBCompactionTest, RecordNewestKeyTimeForTtlCompaction) {
|
||||
// Check that we are populating newest_key_time on flush
|
||||
std::vector<FileMetaData*> file_metadatas = GetLevelFileMetadatas(0);
|
||||
ASSERT_EQ(file_metadatas.size(), 4);
|
||||
uint64_t first_newest_key_time =
|
||||
file_metadatas[0]->fd.table_reader->GetTableProperties()->newest_key_time;
|
||||
uint64_t first_newest_key_time = file_metadatas[0]
|
||||
->fd.pinned_reader.Get()
|
||||
->GetTableProperties()
|
||||
->newest_key_time;
|
||||
ASSERT_NE(first_newest_key_time, kUnknownNewestKeyTime);
|
||||
// Check that the newest_key_times are in expected ordering
|
||||
uint64_t prev_newest_key_time = first_newest_key_time;
|
||||
for (size_t idx = 1; idx < file_metadatas.size(); idx++) {
|
||||
uint64_t newest_key_time = file_metadatas[idx]
|
||||
->fd.table_reader->GetTableProperties()
|
||||
->fd.pinned_reader.Get()
|
||||
->GetTableProperties()
|
||||
->newest_key_time;
|
||||
|
||||
ASSERT_LT(newest_key_time, prev_newest_key_time);
|
||||
prev_newest_key_time = newest_key_time;
|
||||
ASSERT_EQ(newest_key_time, file_metadatas[idx]
|
||||
->fd.table_reader->GetTableProperties()
|
||||
->fd.pinned_reader.Get()
|
||||
->GetTableProperties()
|
||||
->creation_time);
|
||||
}
|
||||
// The delta between the first and last newest_key_times is 15s
|
||||
@@ -11511,14 +11530,18 @@ TEST_F(DBCompactionTest, RecordNewestKeyTimeForTtlCompaction) {
|
||||
ASSERT_EQ(NumTableFilesAtLevel(0), 1);
|
||||
file_metadatas = GetLevelFileMetadatas(0);
|
||||
ASSERT_EQ(file_metadatas.size(), 1);
|
||||
ASSERT_EQ(
|
||||
file_metadatas[0]->fd.table_reader->GetTableProperties()->newest_key_time,
|
||||
first_newest_key_time);
|
||||
ASSERT_EQ(file_metadatas[0]
|
||||
->fd.pinned_reader.Get()
|
||||
->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)
|
||||
ASSERT_EQ(
|
||||
file_metadatas[0]->fd.table_reader->GetTableProperties()->creation_time,
|
||||
last_newest_key_time);
|
||||
ASSERT_EQ(file_metadatas[0]
|
||||
->fd.pinned_reader.Get()
|
||||
->GetTableProperties()
|
||||
->creation_time,
|
||||
last_newest_key_time);
|
||||
ASSERT_EQ(file_metadatas[0]->oldest_ancester_time, last_newest_key_time);
|
||||
|
||||
// Make sure TTL of 5s causes compaction
|
||||
@@ -11541,6 +11564,71 @@ TEST_F(DBCompactionTest, RecordNewestKeyTimeForTtlCompaction) {
|
||||
ASSERT_EQ(NumTableFilesAtLevel(0), 0);
|
||||
}
|
||||
|
||||
// Test verifies compaction file cutting logic when using tail size estimation
|
||||
// maintains output files at or below the target file size.
|
||||
TEST_F(DBCompactionTest, CompactionRespectsTargetSizeWithTailEstimation) {
|
||||
const int kInitialKeyCount = 10000; // 10k keys
|
||||
const int kValueSize = 100; // 100 bytes per key
|
||||
const int kSeed = 301;
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.target_file_size_is_upper_bound = true;
|
||||
options.target_file_size_base = 256 * 1024;
|
||||
options.write_buffer_size = 2 * 1024 * 1024;
|
||||
options.level0_file_num_compaction_trigger = 100; // Never trigger L0->L1
|
||||
options.compression = kNoCompression;
|
||||
|
||||
BlockBasedTableOptions table_options;
|
||||
table_options.partition_filters = true;
|
||||
table_options.metadata_block_size = 4 * 1024;
|
||||
table_options.index_type = BlockBasedTableOptions::kBinarySearch;
|
||||
table_options.filter_policy.reset(NewBloomFilterPolicy(10));
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Generate 2 L0 files
|
||||
// Generate first file with 10k keys (each ~100 bytes) approx 1.2MB total
|
||||
Random rnd(kSeed);
|
||||
for (int i = 0; i < kInitialKeyCount; i++) {
|
||||
ASSERT_OK(Put(Key(i), rnd.RandomString(kValueSize)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Generate second file with overlapping keys to force compaction (prevent
|
||||
// trivial move)
|
||||
for (int i = kInitialKeyCount / 2; i < kInitialKeyCount * 1.5; i++) {
|
||||
ASSERT_OK(Put(Key(i), rnd.RandomString(kValueSize)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Capture file metadata and assert two L0 files
|
||||
std::vector<LiveFileMetaData> file_metadata;
|
||||
db_->GetLiveFilesMetaData(&file_metadata);
|
||||
ASSERT_EQ(file_metadata.size(), 2);
|
||||
for (const auto& file : file_metadata) {
|
||||
ASSERT_EQ(file.level, 0);
|
||||
};
|
||||
|
||||
// Manually compact LO files to L1
|
||||
CompactRangeOptions cro;
|
||||
cro.change_level = true;
|
||||
cro.target_level = 1;
|
||||
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
|
||||
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
// Verify that compacted output files are under target file size
|
||||
for (const auto& file : file_metadata) {
|
||||
if (file.level > 0) {
|
||||
EXPECT_LE(file.size, options.target_file_size_base)
|
||||
<< "Output file size exceeds target size: " << " File: " << file.name
|
||||
<< " level: " << file.level << " File size: " << file.size
|
||||
<< " Target size: " << options.target_file_size_base;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PeriodicCompactionListener : public EventListener {
|
||||
public:
|
||||
explicit PeriodicCompactionListener() {}
|
||||
@@ -11593,6 +11681,283 @@ TEST_F(DBCompactionTest, PeriodicTask) {
|
||||
ASSERT_EQ(listener->num_periodic_compactions, 1);
|
||||
Close();
|
||||
}
|
||||
|
||||
// Regression test for a bug in SetupOtherFilesWithRoundRobinExpansion where
|
||||
// duplicate files are added to the compaction input, corrupting
|
||||
// ExpandInputsToCleanCut and violating the clean-cut invariant. The bug
|
||||
// requires: (1) kRoundRobin compaction priority, (2) files at a non-L0 level
|
||||
// with shared user key boundaries (adjacent files whose boundary keys share
|
||||
// the same user key), and (3) ExpandInputsToCleanCut expanding the initially
|
||||
// picked file to include multiple adjacent files in PickFileToCompact.
|
||||
TEST_F(DBCompactionTest, RoundRobinCleanCutWithSharedBoundary) {
|
||||
Options options = CurrentOptions();
|
||||
options.compaction_style = kCompactionStyleLevel;
|
||||
options.compaction_pri = kRoundRobin;
|
||||
options.level_compaction_dynamic_level_bytes = false;
|
||||
options.max_bytes_for_level_base = 100;
|
||||
options.disable_auto_compactions = true;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
std::vector<const Snapshot*> snapshots;
|
||||
for (int v = 0; v < 5; v++) {
|
||||
for (int k = 0; k < 3; k++) {
|
||||
ASSERT_OK(Put("key" + std::to_string(k), "v" + std::to_string(v)));
|
||||
}
|
||||
snapshots.push_back(db_->GetSnapshot());
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
|
||||
// Force L0->L1 compaction output to split every 3 keys. With 3 keys x 5
|
||||
// versions (15 KVs) sorted by (user_key asc, seq desc), splitting every 3
|
||||
// creates 5 files where adjacent files share boundaries across different
|
||||
// user keys (e.g., File0 ends with key0, File1 starts with key0 and ends
|
||||
// with key1, etc.). This chain of 4+ shared boundaries across 3 different
|
||||
// user keys is needed so that ExpandInputsToCleanCut expands the picked
|
||||
// file to multiple files, and the duplicate in the round-robin loop causes
|
||||
// GetRange to return a truncated range that drops files from the set.
|
||||
std::atomic<int> key_count{0};
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionOutputs::ShouldStopBefore::manual_decision", [&](void* arg) {
|
||||
auto* p = static_cast<std::pair<bool*, const Slice>*>(arg);
|
||||
int n = key_count.fetch_add(1);
|
||||
if (n > 0 && n % 3 == 0) {
|
||||
*(p->first) = true;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
ASSERT_OK(dbfull()->TEST_CompactRange(0, nullptr, nullptr));
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ColumnFamilyMetaData cf_meta;
|
||||
db_->GetColumnFamilyMetaData(&cf_meta);
|
||||
ASSERT_EQ(cf_meta.levels[1].files.size(), 5U);
|
||||
|
||||
for (auto s : snapshots) {
|
||||
db_->ReleaseSnapshot(s);
|
||||
}
|
||||
|
||||
ASSERT_OK(dbfull()->SetOptions({{"disable_auto_compactions", "false"}}));
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
for (int k = 0; k < 3; k++) {
|
||||
ASSERT_EQ(Get("key" + std::to_string(k)), "v4");
|
||||
}
|
||||
}
|
||||
|
||||
// Regression test:
|
||||
// 1. Compaction succeeds at subcompaction level, VerifyOutputFiles adds cache
|
||||
// entries for output files via table_cache()->NewIterator().
|
||||
// 2. A post-verification step fails (injected here via sync point), setting
|
||||
// compact_->status to error while each subcompaction's status stays OK.
|
||||
// 3. SubcompactionState::Cleanup checks individual status (OK) and skips
|
||||
// ReleaseObsolete — the cache entries leak.
|
||||
// 4. FaultInjectionTestFS injects metadata read errors, causing GetChildren
|
||||
// to fail in FindObsoleteFiles.
|
||||
// 5. Close()'s FindObsoleteFiles also fails to find the orphan for the same
|
||||
// reason. TEST_VerifyNoObsoleteFilesCached finds the leaked entry.
|
||||
TEST_F(DBCompactionTest, LeakedTableCacheEntryOnCompactionFailure) {
|
||||
auto fault_fs = std::make_shared<FaultInjectionTestFS>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> fault_env(NewCompositeEnv(fault_fs));
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.env = fault_env.get();
|
||||
options.paranoid_file_checks = true;
|
||||
options.level0_file_num_compaction_trigger = 2;
|
||||
options.disable_auto_compactions = true;
|
||||
options.num_levels = 3;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Write overlapping data to force a real (non-trivial) compaction.
|
||||
ASSERT_OK(Put("a", std::string(1024, 'x')));
|
||||
ASSERT_OK(Put("z", std::string(1024, 'x')));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put("a", std::string(1024, 'y')));
|
||||
ASSERT_OK(Put("z", std::string(1024, 'y')));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_EQ(NumTableFilesAtLevel(0), 2);
|
||||
|
||||
// After VerifyOutputFiles succeeds (cache entries created), inject error
|
||||
// and deactivate the filesystem. The error makes the overall compaction
|
||||
// fail while individual subcompaction statuses stay OK (so Cleanup skips
|
||||
// ReleaseObsolete). The filesystem deactivation makes GetChildren fail
|
||||
// in FindObsoleteFiles, preventing the backstop from evicting the leaked
|
||||
// cache entries — matching the crash test's metadata read fault injection.
|
||||
std::atomic<bool> inject_error{true};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionJob::Run():AfterVerifyOutputFiles", [&](void* arg) {
|
||||
if (inject_error.exchange(false)) {
|
||||
*static_cast<Status*>(arg) = Status::Corruption("injected");
|
||||
}
|
||||
});
|
||||
|
||||
// Enable metadata read fault injection on the bg compaction thread after
|
||||
// the compaction job finishes but before FindObsoleteFiles runs. This
|
||||
// makes GetChildren fail (metadata read), matching crash test's
|
||||
// --open_metadata_read_fault_one_in=8. Only metadata reads fail —
|
||||
// logging and other IO operations continue normally.
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"BackgroundCallCompaction:1", [&](void*) {
|
||||
fault_fs->SetThreadLocalErrorContext(
|
||||
FaultInjectionIOType::kMetadataRead, /*seed=*/0, /*one_in=*/1,
|
||||
/*retryable=*/false, /*has_data_loss=*/false);
|
||||
fault_fs->EnableThreadLocalErrorInjection(
|
||||
FaultInjectionIOType::kMetadataRead);
|
||||
});
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Trigger compaction — fails after VerifyOutputFiles.
|
||||
Status s = dbfull()->TEST_CompactRange(0, nullptr, nullptr);
|
||||
ASSERT_NOK(s);
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
// Enable metadata read fault injection on the main thread too, so
|
||||
// Close()'s FindObsoleteFiles also fails to find the orphan file.
|
||||
fault_fs->SetThreadLocalErrorContext(
|
||||
FaultInjectionIOType::kMetadataRead, /*seed=*/0, /*one_in=*/1,
|
||||
/*retryable=*/false, /*has_data_loss=*/false);
|
||||
fault_fs->EnableThreadLocalErrorInjection(
|
||||
FaultInjectionIOType::kMetadataRead);
|
||||
|
||||
// TEST_VerifyNoObsoleteFilesCached asserted within Close on ASAN builds
|
||||
s = db_->Close();
|
||||
ASSERT_OK(s);
|
||||
// Release DB before fault_env goes out of scope to avoid use-after-free.
|
||||
db_ = nullptr;
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, VerifyFileChecksumOnCompactionOutput) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
options.file_checksum_gen_factory = GetFileChecksumGenCrc32cFactory();
|
||||
options.verify_output_flags = VerifyOutputFlags::kVerifyFileChecksum |
|
||||
VerifyOutputFlags::kEnableForLocalCompaction;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Create 2 L0 files to trigger compaction
|
||||
for (int i = 0; i < 10; i++) {
|
||||
ASSERT_OK(Put(Key(i), "value" + std::to_string(i)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
for (int i = 5; i < 15; i++) {
|
||||
ASSERT_OK(Put(Key(i), "value2_" + std::to_string(i)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Corrupt output files right before verification
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionJob::Run:BeforeVerify", [&](void* /*arg*/) {
|
||||
// Find and corrupt the newest SST file (compaction output)
|
||||
std::vector<std::string> filenames;
|
||||
ASSERT_OK(env_->GetChildren(dbname_, &filenames));
|
||||
uint64_t max_number = 0;
|
||||
std::string target_fname;
|
||||
for (const auto& f : filenames) {
|
||||
uint64_t number;
|
||||
FileType type;
|
||||
if (ParseFileName(f, &number, &type) && type == kTableFile &&
|
||||
number > max_number) {
|
||||
max_number = number;
|
||||
target_fname = dbname_ + "/" + f;
|
||||
}
|
||||
}
|
||||
ASSERT_FALSE(target_fname.empty());
|
||||
ASSERT_OK(test::CorruptFile(env_, target_fname, 0, 1,
|
||||
false /* verifyChecksum */));
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Status s = db_->CompactRange(CompactRangeOptions(), nullptr, nullptr);
|
||||
ASSERT_TRUE(s.IsCorruption()) << s.ToString();
|
||||
ASSERT_TRUE(
|
||||
std::strstr(s.getState(), "File checksum mismatch for compaction output"))
|
||||
<< s.ToString();
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
// Regression test: verify_output_flags with kVerifyIteration should work
|
||||
// correctly even when paranoid_file_checks is false. Before the fix, the
|
||||
// OutputValidator hash was only computed during writing when
|
||||
// paranoid_file_checks was true, but the verification always computed the
|
||||
// hash, leading to a false positive "Key-value checksum of compaction output
|
||||
// doesn't match" error.
|
||||
TEST_F(DBCompactionTest, VerifyIterationWithoutParanoidFileChecks) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
options.paranoid_file_checks = false;
|
||||
options.verify_output_flags = VerifyOutputFlags::kVerifyIteration |
|
||||
VerifyOutputFlags::kEnableForLocalCompaction;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Create 2 L0 files to trigger compaction
|
||||
for (int i = 0; i < 10; i++) {
|
||||
ASSERT_OK(Put(Key(i), "value" + std::to_string(i)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
for (int i = 5; i < 15; i++) {
|
||||
ASSERT_OK(Put(Key(i), "value2_" + std::to_string(i)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Compaction should succeed without false corruption errors
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
// Verify data is intact
|
||||
for (int i = 0; i < 15; i++) {
|
||||
std::string expected;
|
||||
if (i >= 5) {
|
||||
expected = "value2_" + std::to_string(i);
|
||||
} else {
|
||||
expected = "value" + std::to_string(i);
|
||||
}
|
||||
ASSERT_EQ(Get(Key(i)), expected);
|
||||
}
|
||||
}
|
||||
|
||||
// Also test all verification types combined without paranoid_file_checks
|
||||
TEST_F(DBCompactionTest, VerifyAllOutputFlagsWithoutParanoidFileChecks) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
options.paranoid_file_checks = false;
|
||||
options.file_checksum_gen_factory = GetFileChecksumGenCrc32cFactory();
|
||||
options.verify_output_flags = VerifyOutputFlags::kVerifyBlockChecksum |
|
||||
VerifyOutputFlags::kVerifyIteration |
|
||||
VerifyOutputFlags::kVerifyFileChecksum |
|
||||
VerifyOutputFlags::kEnableForLocalCompaction;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
ASSERT_OK(Put(Key(i), "value" + std::to_string(i)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
for (int i = 5; i < 15; i++) {
|
||||
ASSERT_OK(Put(Key(i), "value2_" + std::to_string(i)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Compaction should succeed with all verification types enabled
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
for (int i = 0; i < 15; i++) {
|
||||
std::string expected;
|
||||
if (i >= 5) {
|
||||
expected = "value2_" + std::to_string(i);
|
||||
} else {
|
||||
expected = "value" + std::to_string(i);
|
||||
}
|
||||
ASSERT_EQ(Get(Key(i)), expected);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include "db/db_test_util.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class DBEtc3Test : public DBTestBase {
|
||||
public:
|
||||
DBEtc3Test() : DBTestBase("db_etc3_test", /*env_do_fsync=*/true) {}
|
||||
};
|
||||
|
||||
TEST_F(DBEtc3Test, ManifestRollOver) {
|
||||
do {
|
||||
Options options;
|
||||
// Force new manifest on each manifest write
|
||||
options.max_manifest_file_size = 0;
|
||||
options.max_manifest_space_amp_pct = 0;
|
||||
options = CurrentOptions(options);
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
{
|
||||
ASSERT_OK(Put(1, "key1", std::string(1000, '1')));
|
||||
ASSERT_OK(Put(1, "key2", std::string(1000, '2')));
|
||||
ASSERT_OK(Put(1, "key3", std::string(1000, '3')));
|
||||
uint64_t manifest_before_flush = dbfull()->TEST_Current_Manifest_FileNo();
|
||||
ASSERT_OK(Flush(1)); // This should trigger LogAndApply.
|
||||
uint64_t manifest_after_flush = dbfull()->TEST_Current_Manifest_FileNo();
|
||||
ASSERT_GT(manifest_after_flush, manifest_before_flush);
|
||||
// Re-open should always re-create manifest file
|
||||
ReopenWithColumnFamilies({"default", "pikachu"}, options);
|
||||
ASSERT_GT(dbfull()->TEST_Current_Manifest_FileNo(), manifest_after_flush);
|
||||
ASSERT_EQ(std::string(1000, '1'), Get(1, "key1"));
|
||||
ASSERT_EQ(std::string(1000, '2'), Get(1, "key2"));
|
||||
ASSERT_EQ(std::string(1000, '3'), Get(1, "key3"));
|
||||
}
|
||||
} while (ChangeCompactOptions());
|
||||
}
|
||||
|
||||
TEST_F(DBEtc3Test, AutoTuneManifestSize) {
|
||||
// Ensure we have auto-tuning beyond max_manifest_file_size by default
|
||||
ASSERT_EQ(DBOptions{}.max_manifest_space_amp_pct, 500);
|
||||
|
||||
Options options = CurrentOptions();
|
||||
ASSERT_OK(db_->SetOptions({{"level0_file_num_compaction_trigger", "20"}}));
|
||||
|
||||
// Use large column family names to essentially control the amount of payload
|
||||
// data needed for the manifest file. Drop manifest entries don't include the
|
||||
// CF name so are small.
|
||||
uint64_t prev_manifest_num = 0, cur_manifest_num = 0;
|
||||
std::deque<ColumnFamilyHandle*> handles;
|
||||
int counter = 5;
|
||||
auto AddCfFn = [&]() {
|
||||
std::string name = "cf" + std::to_string(counter++);
|
||||
name.resize(1000, 'a');
|
||||
ASSERT_OK(db_->CreateColumnFamily(options, name, &handles.emplace_back()));
|
||||
prev_manifest_num = cur_manifest_num;
|
||||
cur_manifest_num = dbfull()->TEST_Current_Manifest_FileNo();
|
||||
};
|
||||
auto DropCfFn = [&]() {
|
||||
ASSERT_OK(db_->DropColumnFamily(handles.front()));
|
||||
ASSERT_OK(db_->DestroyColumnFamilyHandle(handles.front()));
|
||||
handles.pop_front();
|
||||
prev_manifest_num = cur_manifest_num;
|
||||
cur_manifest_num = dbfull()->TEST_Current_Manifest_FileNo();
|
||||
};
|
||||
auto TrivialManifestWriteFn = [&]() {
|
||||
ASSERT_OK(Put("x", std::to_string(counter++)));
|
||||
ASSERT_OK(Flush());
|
||||
prev_manifest_num = cur_manifest_num;
|
||||
cur_manifest_num = dbfull()->TEST_Current_Manifest_FileNo();
|
||||
};
|
||||
|
||||
options.max_manifest_file_size = 1000000;
|
||||
options.max_manifest_space_amp_pct = 0; // no auto-tuning yet
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// With the generous (minimum) maximum manifest size, should not be rotated
|
||||
AddCfFn();
|
||||
AddCfFn();
|
||||
AddCfFn();
|
||||
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
|
||||
|
||||
// Change options for small max and (still) no auto-tuning
|
||||
ASSERT_OK(db_->SetDBOptions({{"max_manifest_file_size", "3000"}}));
|
||||
|
||||
// Takes effect on the next manifest write
|
||||
TrivialManifestWriteFn();
|
||||
ASSERT_LT(prev_manifest_num, cur_manifest_num);
|
||||
|
||||
// Now we have to rewrite the whole manifest on each write because the
|
||||
// compacted size exceeds the "max" size.
|
||||
AddCfFn();
|
||||
ASSERT_LT(prev_manifest_num, cur_manifest_num);
|
||||
DropCfFn();
|
||||
ASSERT_LT(prev_manifest_num, cur_manifest_num);
|
||||
AddCfFn();
|
||||
ASSERT_LT(prev_manifest_num, cur_manifest_num);
|
||||
TrivialManifestWriteFn();
|
||||
ASSERT_LT(prev_manifest_num, cur_manifest_num);
|
||||
|
||||
// Enabling auto-tuning should fix this, immediately for next manifest writes.
|
||||
// This will allow up to double-ish the size of the compacted manifest,
|
||||
// which last should have been 4000 + some bytes.
|
||||
ASSERT_EQ(handles.size(), 4U);
|
||||
ASSERT_OK(db_->SetDBOptions({{"max_manifest_space_amp_pct", "105"}}));
|
||||
|
||||
// After 9 CF names should be enough to rotate the manifest
|
||||
for (int i = 1; i <= 5; ++i) {
|
||||
if ((i % 2) == 1) {
|
||||
DropCfFn();
|
||||
}
|
||||
AddCfFn();
|
||||
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
|
||||
}
|
||||
TrivialManifestWriteFn();
|
||||
ASSERT_LT(prev_manifest_num, cur_manifest_num);
|
||||
|
||||
// We now have a different last compacted manifest size, should be
|
||||
// able to go beyond 9 CFs named in manifest this time.
|
||||
ASSERT_EQ(handles.size(), 6U);
|
||||
|
||||
DropCfFn();
|
||||
DropCfFn();
|
||||
for (int i = 1; i <= 4; ++i) {
|
||||
DropCfFn();
|
||||
AddCfFn();
|
||||
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
|
||||
}
|
||||
// We've written 10 named CFs to the manifest. We should be able to
|
||||
// dynamically change the auto-tuning still based on the last "compacted"
|
||||
// manifest size of 7000 + some bytes.
|
||||
ASSERT_OK(db_->SetDBOptions({{"max_manifest_space_amp_pct", "51"}}));
|
||||
TrivialManifestWriteFn();
|
||||
ASSERT_LT(prev_manifest_num, cur_manifest_num);
|
||||
// And the "compacted" manifest size has reset again, so should be changed
|
||||
// again sooner.
|
||||
ASSERT_EQ(handles.size(), 4U);
|
||||
for (int i = 1; i <= 2; ++i) {
|
||||
AddCfFn();
|
||||
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
|
||||
}
|
||||
// Enough for manifest change
|
||||
AddCfFn();
|
||||
ASSERT_LT(prev_manifest_num, cur_manifest_num);
|
||||
|
||||
// Wrap up
|
||||
while (!handles.empty()) {
|
||||
DropCfFn();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
ROCKSDB_NAMESPACE::port::InstallStackTraceHandler();
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
RegisterCustomObjects(argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
@@ -109,6 +109,7 @@ Status DBImpl::GetSortedWalFilesImpl(VectorWalPtr& files, bool need_seqnos) {
|
||||
{
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
while (pending_purge_obsolete_files_ > 0 || bg_purge_scheduled_ > 0) {
|
||||
TEST_SYNC_POINT("DBImpl::GetSortedWalFilesImpl:WaitPurge");
|
||||
bg_cv_.Wait();
|
||||
}
|
||||
|
||||
|
||||
+155
-13
@@ -17,6 +17,7 @@
|
||||
#include "port/port.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/utilities/transaction_db.h"
|
||||
#include "table/block_based/block_based_table_builder.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "test_util/testutil.h"
|
||||
#include "util/cast_util.h"
|
||||
@@ -33,6 +34,17 @@ static std::string NEW_VALUE = "NewValue";
|
||||
class DBFlushTest : public DBTestBase {
|
||||
public:
|
||||
DBFlushTest() : DBTestBase("db_flush_test", /*env_do_fsync=*/true) {}
|
||||
|
||||
// Wait for all background flush callbacks to complete before Close().
|
||||
// Close() calls db_.reset() which nulls the unique_ptr before the destructor
|
||||
// runs, so a concurrent OnFlushCompleted reading test_->db_ would race.
|
||||
// WaitForCompact() waits for bg_flush_scheduled_==0 (which is decremented
|
||||
// after listener callbacks return) without setting shutting_down_ (so
|
||||
// callbacks are not skipped) and without requiring NumNotFlushed()==0 (which
|
||||
// would hang when mempurge leaves a memtable in imm()).
|
||||
Status WaitForFlushCallbacks() {
|
||||
return db_->WaitForCompact(WaitForCompactOptions());
|
||||
}
|
||||
};
|
||||
|
||||
class DBFlushDirectIOTest : public DBFlushTest,
|
||||
@@ -101,7 +113,7 @@ TEST_F(DBFlushTest, SyncFail) {
|
||||
TEST_SYNC_POINT("DBFlushTest::SyncFail:2");
|
||||
fault_injection_env->SetFilesystemActive(true);
|
||||
// Now the background job will do the flush; wait for it.
|
||||
// Returns the IO error happend during flush.
|
||||
// Returns the IO error happened during flush.
|
||||
ASSERT_NOK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
ASSERT_EQ("", FilesPerLevel()); // flush failed.
|
||||
Destroy(options);
|
||||
@@ -518,11 +530,11 @@ TEST_F(DBFlushTest, StatisticsGarbageInsertAndDeletes) {
|
||||
// Note : one set of delete for KEY1, KEY2, KEY3 is written to
|
||||
// SSTable to propagate the delete operations to K-V pairs
|
||||
// that could have been inserted into the database during past Flush
|
||||
// opeartions.
|
||||
// operations.
|
||||
EXPECTED_MEMTABLE_GARBAGE_BYTES_AT_FLUSH -=
|
||||
KEY1.size() + KEY2.size() + KEY3.size() + 3 * sizeof(uint64_t);
|
||||
|
||||
// Additional useful paylaod.
|
||||
// Additional useful payload.
|
||||
ASSERT_OK(Delete(KEY4));
|
||||
ASSERT_OK(Delete(KEY5));
|
||||
ASSERT_OK(Delete(KEY6));
|
||||
@@ -614,7 +626,7 @@ TEST_F(DBFlushTest, StatisticsGarbageRangeDeletes) {
|
||||
|
||||
// Note : one set of deleteRange for (KEY1, KEY2) and (KEY2, KEY3) is written
|
||||
// to SSTable to propagate the deleteRange operations to K-V pairs that could
|
||||
// have been inserted into the database during past Flush opeartions.
|
||||
// have been inserted into the database during past Flush operations.
|
||||
EXPECTED_MEMTABLE_GARBAGE_BYTES_AT_FLUSH -=
|
||||
(KEY1.size() + KEY2.size() + sizeof(uint64_t)) +
|
||||
(KEY2.size() + KEY3.size() + sizeof(uint64_t));
|
||||
@@ -709,7 +721,7 @@ class TestFlushListener : public EventListener {
|
||||
// that assumption does not hold (see the test case MultiDBMultiListeners
|
||||
// below).
|
||||
ASSERT_TRUE(test_);
|
||||
if (db == test_->db_) {
|
||||
if (db == test_->db_.get()) {
|
||||
std::vector<std::vector<FileMetaData>> files_by_level;
|
||||
test_->dbfull()->TEST_GetFilesMetaData(db->DefaultColumnFamily(),
|
||||
&files_by_level);
|
||||
@@ -842,7 +854,7 @@ TEST_F(DBFlushTest, FixFlushReasonRaceFromConcurrentFlushes) {
|
||||
ASSERT_OK(Put(1, Key(idx), std::string(1, 'v')));
|
||||
}
|
||||
|
||||
// To coerce a manual flush happenning in the middle of GetLiveFiles's flush,
|
||||
// To coerce a manual flush happening in the middle of GetLiveFiles's flush,
|
||||
// we need to pause background flush thread and enable it later.
|
||||
std::shared_ptr<test::SleepingBackgroundTask> sleeping_task =
|
||||
std::make_shared<test::SleepingBackgroundTask>();
|
||||
@@ -851,7 +863,7 @@ TEST_F(DBFlushTest, FixFlushReasonRaceFromConcurrentFlushes) {
|
||||
sleeping_task.get(), Env::Priority::HIGH);
|
||||
sleeping_task->WaitUntilSleeping();
|
||||
|
||||
// Coerce a manual flush happenning in the middle of GetLiveFiles's flush
|
||||
// Coerce a manual flush happening in the middle of GetLiveFiles's flush
|
||||
bool get_live_files_paused_at_sync_point = false;
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::AtomicFlushMemTables:AfterScheduleFlush", [&](void* /* arg */) {
|
||||
@@ -1055,6 +1067,7 @@ TEST_F(DBFlushTest, MemPurgeBasic) {
|
||||
ASSERT_EQ(Get(RNDKEY2), p_rv2);
|
||||
ASSERT_EQ(Get(RNDKEY3), p_rv3);
|
||||
|
||||
ASSERT_OK(WaitForFlushCallbacks());
|
||||
Close();
|
||||
}
|
||||
|
||||
@@ -1169,6 +1182,7 @@ TEST_F(DBFlushTest, MemPurgeBasicToggle) {
|
||||
// We expect no mempurge at all.
|
||||
EXPECT_EQ(mempurge_count.exchange(0), ZERO);
|
||||
|
||||
ASSERT_OK(WaitForFlushCallbacks());
|
||||
Close();
|
||||
}
|
||||
// End of MemPurgeBasicToggle, which is not
|
||||
@@ -1425,10 +1439,11 @@ TEST_F(DBFlushTest, MemPurgeDeleteAndDeleteRange) {
|
||||
delete iter;
|
||||
}
|
||||
|
||||
ASSERT_OK(WaitForFlushCallbacks());
|
||||
Close();
|
||||
}
|
||||
|
||||
// Create a Compaction Fitler that will be invoked
|
||||
// Create a Compaction Filter that will be invoked
|
||||
// at flush time and will update the value of a KV pair
|
||||
// if the key string is "lower" than the filter_key_ string.
|
||||
class ConditionalUpdateFilter : public CompactionFilter {
|
||||
@@ -1879,6 +1894,85 @@ TEST_F(DBFlushTest, MemPurgeCorrectLogNumberAndSSTFileCreation) {
|
||||
Close();
|
||||
}
|
||||
|
||||
// Reproduction for MemPurge memtable ID ordering bug.
|
||||
// When MemPurge runs, it releases db_mutex_. During that window, new
|
||||
// memtables can be switched to the immutable list with higher IDs. When
|
||||
// MemPurge re-acquires the mutex and adds its output memtable using the
|
||||
// stale ID from mems_.back(), the ordering assertion fires.
|
||||
TEST_F(DBFlushTest, MemPurgeIdOrdering) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.compression = kNoCompression;
|
||||
options.inplace_update_support = false;
|
||||
options.allow_concurrent_memtable_write = true;
|
||||
options.write_buffer_size = 1 << 20; // 1MB
|
||||
// Allow enough immutable memtables so writes don't stall while the
|
||||
// flush thread is paused in the sync point.
|
||||
options.max_write_buffer_number = 8;
|
||||
// Always attempt MemPurge on flush.
|
||||
options.experimental_mempurge_threshold = 1.0;
|
||||
ASSERT_OK(TryReopen(options));
|
||||
|
||||
// Coordinate via LoadDependency:
|
||||
// 1. Flush thread hits BeforeReacquireMutex -> foreground unblocks
|
||||
// 2. Foreground writes data, triggers memtable switch
|
||||
// 3. Foreground hits SwitchDone -> flush thread unblocks at
|
||||
// AfterWaitForTest
|
||||
// 4. Flush thread re-acquires mutex, tries AddMemTable with stale ID
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
|
||||
{{"FlushJob::MemPurge:BeforeReacquireMutex",
|
||||
"DBFlushTest::MemPurgeIdOrdering:StartWriting"},
|
||||
{"DBFlushTest::MemPurgeIdOrdering:SwitchDone",
|
||||
"FlushJob::MemPurge:AfterWaitForTest"}});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
const int kValueSize = 10240; // 10KB values like MemPurgeBasic
|
||||
const int kNumKeys = 30;
|
||||
Random rnd(301);
|
||||
|
||||
// Fill the memtable with overwrites of the same keys so MemPurge
|
||||
// can compact them down (high garbage ratio). Each round is ~300KB,
|
||||
// need ~3 rounds to approach the 1MB write_buffer_size.
|
||||
for (int round = 0; round < 3; round++) {
|
||||
for (int i = 0; i < kNumKeys; i++) {
|
||||
ASSERT_OK(Put("key" + std::to_string(i), rnd.RandomString(kValueSize)));
|
||||
}
|
||||
}
|
||||
|
||||
// One more round should trigger a flush with MemPurge in the background.
|
||||
for (int i = 0; i < kNumKeys; i++) {
|
||||
ASSERT_OK(Put("key" + std::to_string(i), rnd.RandomString(kValueSize)));
|
||||
}
|
||||
|
||||
// Block until MemPurge reaches the sync point (db_mutex_ released).
|
||||
TEST_SYNC_POINT("DBFlushTest::MemPurgeIdOrdering:StartWriting");
|
||||
|
||||
// Now MemPurge is paused with db_mutex_ released. Write enough data
|
||||
// to fill another memtable and trigger a switch. This creates an
|
||||
// immutable memtable with a higher ID than the one being mempurged.
|
||||
for (int i = 0; i < kNumKeys * 4; i++) {
|
||||
ASSERT_OK(Put("newkey" + std::to_string(i), rnd.RandomString(kValueSize)));
|
||||
}
|
||||
|
||||
// Let MemPurge continue -- it will re-acquire the mutex and try to
|
||||
// add the output memtable with the stale ID.
|
||||
TEST_SYNC_POINT("DBFlushTest::MemPurgeIdOrdering:SwitchDone");
|
||||
|
||||
// Allow background work to finish.
|
||||
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
|
||||
// If the bug is present, the assertion in AddMemTable fires before
|
||||
// we reach here.
|
||||
for (int i = 0; i < kNumKeys; i++) {
|
||||
ASSERT_NE(Get("key" + std::to_string(i)), "NOT_FOUND");
|
||||
}
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_P(DBFlushDirectIOTest, DirectIO) {
|
||||
Options options;
|
||||
options.create_if_missing = true;
|
||||
@@ -2533,7 +2627,7 @@ TEST_F(DBFlushTest, TombstoneVisibleInSnapshot) {
|
||||
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "foo", "value0"));
|
||||
|
||||
ManagedSnapshot snapshot_guard(db_);
|
||||
ManagedSnapshot snapshot_guard(db_.get());
|
||||
|
||||
ColumnFamilyHandle* default_cf = db_->DefaultColumnFamily();
|
||||
ASSERT_OK(db_->Flush(FlushOptions(), default_cf));
|
||||
@@ -2574,7 +2668,7 @@ TEST_P(DBAtomicFlushTest, ManualFlushUnder2PC) {
|
||||
txn_db_opts.write_policy = TxnDBWritePolicy::WRITE_COMMITTED;
|
||||
ASSERT_OK(TransactionDB::Open(options, txn_db_opts, dbname_, &txn_db));
|
||||
ASSERT_NE(txn_db, nullptr);
|
||||
db_ = txn_db;
|
||||
db_.reset(txn_db);
|
||||
|
||||
// Create two more columns other than default CF.
|
||||
std::vector<std::string> cfs = {"puppy", "kitty"};
|
||||
@@ -2638,9 +2732,8 @@ TEST_P(DBAtomicFlushTest, ManualFlushUnder2PC) {
|
||||
// it means atomic flush didn't write the min_log_number_to_keep to MANIFEST.
|
||||
cfs.push_back(kDefaultColumnFamilyName);
|
||||
ASSERT_OK(TryReopenWithColumnFamilies(cfs, options));
|
||||
DBImpl* db_impl = static_cast<DBImpl*>(db_);
|
||||
ASSERT_TRUE(db_impl->allow_2pc());
|
||||
ASSERT_NE(db_impl->MinLogNumberToKeep(), 0);
|
||||
ASSERT_TRUE(dbfull()->allow_2pc());
|
||||
ASSERT_NE(dbfull()->MinLogNumberToKeep(), 0);
|
||||
}
|
||||
|
||||
TEST_P(DBAtomicFlushTest, ManualAtomicFlush) {
|
||||
@@ -3707,6 +3800,55 @@ INSTANTIATE_TEST_CASE_P(
|
||||
// larger than the max allowed padding size
|
||||
testing::Values(4, kLowSpaceOverheadRatio)));
|
||||
|
||||
// Test that when the table builder's io_status becomes bad during flush
|
||||
// (simulating write fault injection), BuildTable properly propagates the
|
||||
// builder's IO error instead of producing a misleading Corruption from the
|
||||
// num_entries mismatch check.
|
||||
TEST_F(DBFlushTest, BuilderWriteFaultPropagationDuringFlush) {
|
||||
Options options = CurrentOptions();
|
||||
options.flush_verify_memtable_count = true;
|
||||
options.table_factory.reset(
|
||||
NewBlockBasedTableFactory(BlockBasedTableOptions()));
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
for (int i = 0; i < 100; i++) {
|
||||
ASSERT_OK(Put("key" + std::to_string(i),
|
||||
std::string(100, 'v') + std::to_string(i)));
|
||||
}
|
||||
|
||||
// Skip all Add() calls to simulate entries not being committed (builder
|
||||
// stays empty), as happens when fault injection causes early returns.
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"BlockBasedTableBuilder::Add::skip",
|
||||
[&](void* skip) { *(bool*)skip = true; });
|
||||
|
||||
// Inject an IOError into the builder's status before the empty check.
|
||||
// This simulates the scenario where write fault injection puts the builder
|
||||
// in an error state, causing all Add() calls to return early (ok() is false)
|
||||
// and leaving the builder empty.
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"BuildTable:BeforeCheckEmpty", [&](void* arg) {
|
||||
auto* builder = static_cast<BlockBasedTableBuilder*>(
|
||||
static_cast<TableBuilder*>(arg));
|
||||
builder->TEST_InjectIOError();
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Status s = Flush();
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
// With the fix, the builder's IOError is propagated instead of the
|
||||
// misleading Corruption from the key count mismatch check.
|
||||
ASSERT_NOK(s);
|
||||
ASSERT_TRUE(s.IsIOError())
|
||||
<< "Expected IOError from builder error propagation, got: "
|
||||
<< s.ToString();
|
||||
|
||||
Close();
|
||||
}
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -91,8 +91,21 @@ Status CompactedDBImpl::Get(const ReadOptions& _read_options,
|
||||
/*b_has_ts=*/false) < 0) {
|
||||
return Status::NotFound();
|
||||
}
|
||||
Status s = f.fd.table_reader->Get(read_options, lkey.internal_key(),
|
||||
&get_context, nullptr);
|
||||
TableReader* t = nullptr;
|
||||
TableCache::TypedHandle* handle = nullptr;
|
||||
Status s = cfd_->table_cache()->FindTable(
|
||||
read_options, cfd_->table_cache()->file_options(),
|
||||
cfd_->internal_comparator(), *f.file_metadata, &handle,
|
||||
version_->GetMutableCFOptions(), &t, false /* no_io */,
|
||||
nullptr /* file_read_hist */, false /* skip_filters */, files_level_,
|
||||
true /* prefetch_index_and_filter_in_cache */,
|
||||
0 /* max_file_size_for_l0_meta_pin */, f.file_metadata->temperature,
|
||||
true /* pin_table_handle */);
|
||||
if (s.ok()) {
|
||||
assert(handle == nullptr);
|
||||
// Use TableReader directly to avoid extra table_cache->Get() overheads
|
||||
s = t->Get(read_options, lkey.internal_key(), &get_context, nullptr);
|
||||
}
|
||||
if (!s.ok() && !s.IsNotFound()) {
|
||||
return s;
|
||||
}
|
||||
@@ -152,7 +165,6 @@ void CompactedDBImpl::MultiGet(const ReadOptions& _read_options,
|
||||
}
|
||||
|
||||
GetWithTimestampReadCallback read_cb(kMaxSequenceNumber);
|
||||
autovector<TableReader*, 16> reader_list;
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
const Slice& key = keys[i];
|
||||
LookupKey lkey(key, kMaxSequenceNumber, read_options.timestamp);
|
||||
@@ -162,38 +174,40 @@ void CompactedDBImpl::MultiGet(const ReadOptions& _read_options,
|
||||
ExtractUserKeyAndStripTimestamp(f.smallest_key,
|
||||
user_comparator_->timestamp_size()),
|
||||
/*b_has_ts=*/false) < 0) {
|
||||
reader_list.push_back(nullptr);
|
||||
} else {
|
||||
f.fd.table_reader->Prepare(lkey.internal_key());
|
||||
reader_list.push_back(f.fd.table_reader);
|
||||
statuses[i] = Status::NotFound();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
statuses[i] = Status::NotFound();
|
||||
}
|
||||
int idx = 0;
|
||||
for (auto* r : reader_list) {
|
||||
if (r != nullptr) {
|
||||
PinnableSlice& pinnable_val = values[idx];
|
||||
LookupKey lkey(keys[idx], kMaxSequenceNumber, read_options.timestamp);
|
||||
std::string* timestamp = timestamps ? ×tamps[idx] : nullptr;
|
||||
GetContext get_context(
|
||||
user_comparator_, nullptr, nullptr, nullptr, GetContext::kNotFound,
|
||||
lkey.user_key(), &pinnable_val, /*columns=*/nullptr,
|
||||
user_comparator_->timestamp_size() > 0 ? timestamp : nullptr, nullptr,
|
||||
nullptr, true, nullptr, nullptr, nullptr, nullptr, &read_cb);
|
||||
Status status =
|
||||
r->Get(read_options, lkey.internal_key(), &get_context, nullptr);
|
||||
assert(static_cast<size_t>(idx) < num_keys);
|
||||
if (!status.ok() && !status.IsNotFound()) {
|
||||
statuses[idx] = status;
|
||||
PinnableSlice& pinnable_val = values[i];
|
||||
std::string* timestamp = timestamps ? ×tamps[i] : nullptr;
|
||||
GetContext get_context(
|
||||
user_comparator_, nullptr, nullptr, nullptr, GetContext::kNotFound,
|
||||
lkey.user_key(), &pinnable_val, /*columns=*/nullptr,
|
||||
user_comparator_->timestamp_size() > 0 ? timestamp : nullptr, nullptr,
|
||||
nullptr, true, nullptr, nullptr, nullptr, nullptr, &read_cb);
|
||||
TableReader* t = nullptr;
|
||||
TableCache::TypedHandle* handle = nullptr;
|
||||
Status status = cfd_->table_cache()->FindTable(
|
||||
read_options, cfd_->table_cache()->file_options(),
|
||||
cfd_->internal_comparator(), *f.file_metadata, &handle,
|
||||
version_->GetMutableCFOptions(), &t, false /* no_io */,
|
||||
nullptr /* file_read_hist */, false /* skip_filters */, files_level_,
|
||||
true /* prefetch_index_and_filter_in_cache */,
|
||||
0 /* max_file_size_for_l0_meta_pin */, f.file_metadata->temperature,
|
||||
true /* pin_table_handle */);
|
||||
if (status.ok()) {
|
||||
assert(handle == nullptr);
|
||||
// Use TableReader directly to avoid extra table_cache->Get() overheads
|
||||
status = t->Get(read_options, lkey.internal_key(), &get_context, nullptr);
|
||||
}
|
||||
if (!status.ok() && !status.IsNotFound()) {
|
||||
statuses[i] = status;
|
||||
} else {
|
||||
if (get_context.State() == GetContext::kFound) {
|
||||
statuses[i] = Status::OK();
|
||||
} else {
|
||||
if (get_context.State() == GetContext::kFound) {
|
||||
statuses[idx] = Status::OK();
|
||||
}
|
||||
statuses[i] = Status::NotFound();
|
||||
}
|
||||
}
|
||||
++idx;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,6 +244,7 @@ Status CompactedDBImpl::Init(const Options& options) {
|
||||
return Status::NotSupported("Both L0 and other level contain files");
|
||||
}
|
||||
files_ = l0;
|
||||
files_level_ = 0;
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
@@ -242,6 +257,7 @@ Status CompactedDBImpl::Init(const Options& options) {
|
||||
int level = vstorage->num_non_empty_levels() - 1;
|
||||
if (vstorage->LevelFilesBrief(level).num_files > 0) {
|
||||
files_ = vstorage->LevelFilesBrief(level);
|
||||
files_level_ = level;
|
||||
return Status::OK();
|
||||
}
|
||||
return Status::NotSupported("no file exists");
|
||||
@@ -261,6 +277,13 @@ Status CompactedDBImpl::Open(const Options& options, const std::string& dbname,
|
||||
std::unique_ptr<CompactedDBImpl> db(new CompactedDBImpl(db_options, dbname));
|
||||
Status s = db->Init(options);
|
||||
if (s.ok()) {
|
||||
{
|
||||
InstrumentedMutexLock l(&db->mutex_);
|
||||
db->opened_successfully_ = true;
|
||||
if (db->immutable_db_options_.open_files_async) {
|
||||
db->ScheduleAsyncFileOpening();
|
||||
}
|
||||
}
|
||||
s = db->StartPeriodicTaskScheduler();
|
||||
}
|
||||
if (s.ok()) {
|
||||
|
||||
Loaded 100 of 538 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user