From 42fca1bb390655147cbdbd4e395ec60f06efdeaf Mon Sep 17 00:00:00 2001 From: Xingbo Wang Date: Thu, 20 Aug 2026 04:59:40 -0700 Subject: [PATCH] Enable core dumps for sanitizer crash tests (#15131) Summary: Pull Request resolved: https://github.com/facebook/rocksdb/pull/15131 ASAN and other sanitizer runtimes can disable coredumps or terminate without a core after the test driver sets its shell core limit. This leaves sanitizer stress-test failures with an LLDB stack but no core for the DB artifact. Configure ASAN_OPTIONS, TSAN_OPTIONS, and UBSAN_OPTIONS during sanitizer crash-test steps so reports terminate with a core-generating abort and core dumping remains enabled. Preserve caller-provided options and the existing Make fallback semantics. Keep the behavior scoped to test steps and crash targets. Enabling sanitizer cores raises a concern that the generated core-dump artifacts can be large because they include the process working set. Reduce RocksDB's cache and memtable contribution to the core size without replacing the normal randomized test configuration. Add a db_crashtest cache/write-buffer size multiplier that is applied after each iteration chooses its values, and pass 0.5 from ASAN, TSAN, and UBSAN crash lanes. For the simple profile, cache_size remains randomized with 8/32 MiB becoming 4/16 MiB, while the 32 MiB write buffer becomes 16 MiB. Preserve RocksDB's 64 KiB minimum write-buffer size and leave non-sanitizer lanes unchanged. Reviewed By: rban1 Differential Revision: D116666171 fbshipit-source-id: aa157cfa255bb637d4a68e54ea2039db779fdc53 --- tools/db_crashtest.py | 31 +++++++++++++++++++++++++++ tools/db_crashtest_test.py | 43 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/tools/db_crashtest.py b/tools/db_crashtest.py index e98ca742da..d9855bd041 100644 --- a/tools/db_crashtest.py +++ b/tools/db_crashtest.py @@ -48,6 +48,7 @@ DEFAULT_LIVENESS_NO_PROGRESS_TIMEOUT_SEC = 300 REMOTE_DB_LIVENESS_TIMEOUT_MULTIPLIER = 2 _FAULT_INJECTION_LOG_DIR_NAME = "fault_injection_logs" _REMOTE_DB_URI_FLAGS = ("--env_uri", "--fs_uri") +_MIN_WRITE_BUFFER_SIZE = 64 * 1024 def get_random_seed(override): @@ -76,6 +77,25 @@ def stress_cmd_env(): return env +def apply_cache_and_write_buffer_size_multiplier(params): + """Scale selected per-iteration memory sizes while preserving randomization.""" + multiplier = params.pop("cache_and_write_buffer_size_multiplier", None) + if multiplier is None: + return + if not math.isfinite(multiplier) or multiplier <= 0: + raise ValueError( + "cache_and_write_buffer_size_multiplier must be finite and greater than zero" + ) + + for name, minimum in ( + ("cache_size", 1), + ("write_buffer_size", _MIN_WRITE_BUFFER_SIZE), + ): + value = params.get(name) + if value is not None and value > 0: + params[name] = max(int(value * multiplier), minimum) + + def normalize_remote_db_args(args): """Normalize supported remote URI flags to --flag=value form.""" normalized_args = [] @@ -1053,6 +1073,7 @@ multiops_txn_params = { def finalize_and_sanitize(src_params): dest_params = {k: v() if callable(v) else v for (k, v) in src_params.items()} + apply_cache_and_write_buffer_size_multiplier(dest_params) if is_release_mode(): dest_params["read_fault_one_in"] = 0 if dest_params.get("compression_max_dict_bytes") == 0: @@ -2586,6 +2607,11 @@ def main(): parser.add_argument("--test_tiered_storage", action="store_true") parser.add_argument("--cleanup_cmd") # ignore old option for now parser.add_argument("--print_stderr_separately", action="store_true", default=False) + parser.add_argument( + "--cache_and_write_buffer_size_multiplier", + type=float, + help="Scale each iteration's cache_size and write_buffer_size", + ) all_params = dict( list(default_params.items()) @@ -2613,6 +2639,11 @@ def main(): # unknown_args are passed directly to db_stress args, unknown_args = parser.parse_known_args(remain_args) + multiplier = args.cache_and_write_buffer_size_multiplier + if multiplier is not None and (not math.isfinite(multiplier) or multiplier <= 0): + parser.error( + "--cache_and_write_buffer_size_multiplier must be finite and greater than zero" + ) test_tmpdir = os.environ.get(_TEST_DIR_ENV_VAR) if test_tmpdir is not None and not is_remote_db: isdir = False diff --git a/tools/db_crashtest_test.py b/tools/db_crashtest_test.py index d6d581ba21..2a3cc21f8e 100644 --- a/tools/db_crashtest_test.py +++ b/tools/db_crashtest_test.py @@ -153,6 +153,49 @@ class DBCrashTestTest(unittest.TestCase): any(arg.startswith("--use_coro_db_api=") for arg in command) ) + def test_cache_and_write_buffer_size_multiplier_preserves_randomization(self): + db_crashtest = self.load_db_crashtest() + cache_sizes = iter([8 * 1024 * 1024, 32 * 1024 * 1024]) + params = self.build_params( + db_crashtest.default_params, + { + "cache_size": lambda: next(cache_sizes), + "write_buffer_size": 32 * 1024 * 1024, + "cache_and_write_buffer_size_multiplier": 0.5, + }, + ) + + first_command, _ = db_crashtest.gen_cmd(params, []) + second_command, _ = db_crashtest.gen_cmd(params, []) + + self.assertIn("--cache_size=4194304", first_command) + self.assertIn("--cache_size=16777216", second_command) + self.assertIn("--write_buffer_size=16777216", first_command) + self.assertIn("--write_buffer_size=16777216", second_command) + self.assertFalse( + any( + arg.startswith("--cache_and_write_buffer_size_multiplier=") + for arg in first_command + ) + ) + + def test_cache_and_write_buffer_size_multiplier_respects_write_buffer_minimum( + self, + ): + db_crashtest = self.load_db_crashtest() + params = self.build_params( + db_crashtest.default_params, + { + "write_buffer_size": 64 * 1024, + "cache_and_write_buffer_size_multiplier": 0.5, + }, + ) + + finalized = db_crashtest.finalize_and_sanitize(params) + + self.assertEqual(64 * 1024, finalized["write_buffer_size"]) + self.assertNotIn("cache_and_write_buffer_size_multiplier", finalized) + def test_get_ev_parent_dir_preserves_existing_contents(self): os.makedirs(self.expected_dir) marker = os.path.join(self.expected_dir, "marker")