Skip to content

Commit 91f227d

Browse files
xingbowangmeta-codesync[bot]
authored andcommitted
Add injected error log ring buffer for fault injection diagnostics (facebook#14431)
Summary: Add a circular ring buffer (InjectedErrorLog) to FaultInjectionTestFS that records the last 1000 injected errors. Each entry captures the timestamp, thread ID, FS API name with all arguments (file path, offset, buffer size, first 8 bytes of data in hex), and the injected error status. For example: Append("/path/035354.sst", size=4096, head=[1a 2b 3c ...]) -> IOError (Retryable): injected write error RenameFile("/path/tmp.sst", "/path/035355.sst") -> IOError: injected metadata write error Read(offset=16384, size=4096) -> IOError (Retryable): injected read error The ring buffer is printed to a file automatically: - On any fatal signal (SIGABRT, SIGSEGV, SIGTERM, SIGINT, SIGHUP, SIGFPE, SIGBUS, SIGILL, SIGQUIT, SIGXCPU, SIGXFSZ, SIGSYS) via a registered crash callback - At the end of db_stress main(), for diagnostic visibility even when the test completes normally This addresses a key debugging gap: when write fault injection causes secondary failures (e.g., the builder error propagation issue in T257612259), the injected errors were previously completely silent with no logging trail. The ring buffer provides the missing diagnostic context to correlate fault injection with downstream failures. Changes: - port/stack_trace.h/.cc: Add RegisterCrashCallback() API; extend InstallStackTraceHandler() to catch all catchable termination signals - utilities/fault_injection_fs.h: Add InjectedErrorLog class with printf-style Record(), HexHead() for data bytes, and signal-safe PrintAll() - utilities/fault_injection_fs.cc: Record full API arguments and error status at all 31 fault injection call sites - db_stress_tool/db_stress_tool.cc: Register crash callback and print ring buffer at end of main() Pull Request resolved: facebook#14431 Reviewed By: hx235 Differential Revision: D95435430 Pulled By: xingbowang fbshipit-source-id: 6c18e1b072044575d6c8c3f198070127b0f80608
1 parent cb8bc56 commit 91f227d

6 files changed

Lines changed: 492 additions & 73 deletions

File tree

db_stress_tool/db_stress_tool.cc

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include "db_stress_tool/db_stress_common.h"
2525
#include "db_stress_tool/db_stress_driver.h"
2626
#include "db_stress_tool/db_stress_shared_state.h"
27+
#include "port/stack_trace.h"
2728
#include "rocksdb/convenience.h"
2829
#include "utilities/fault_injection_fs.h"
2930

@@ -92,6 +93,15 @@ int db_stress_tool(int argc, char** argv) {
9293
fault_env_guard =
9394
std::make_shared<CompositeEnvWrapper>(raw_env, fault_fs_guard);
9495
raw_env = fault_env_guard.get();
96+
97+
// Register a crash callback so that recently injected errors are
98+
// printed to stderr when the process crashes (SIGABRT, SIGSEGV, etc.).
99+
// This helps diagnose stress test failures caused by fault injection.
100+
port::RegisterCrashCallback([]() {
101+
if (fault_fs_guard) {
102+
fault_fs_guard->PrintRecentInjectedErrors();
103+
}
104+
});
95105
}
96106

97107
auto db_stress_fs =
@@ -301,6 +311,25 @@ int db_stress_tool(int argc, char** argv) {
301311
FLAGS_db = default_db_path;
302312
}
303313

314+
// Now that FLAGS_db is resolved, set the fault injection log file path
315+
// so that PrintAll() writes to a file instead of stderr (signal-safe).
316+
// Store the log in TEST_TMPDIR (outside the DB directory) so it survives
317+
// DB reopen (which cleans untracked files) and gets included in the
318+
// sandcastle db.tar.gz artifact for post-failure analysis.
319+
if (fault_fs_guard) {
320+
std::string log_dir;
321+
const char* test_tmpdir = getenv("TEST_TMPDIR");
322+
if (test_tmpdir && test_tmpdir[0] != '\0') {
323+
log_dir = test_tmpdir;
324+
} else {
325+
log_dir = "/tmp";
326+
}
327+
std::string log_path = log_dir + "/fault_injection_" +
328+
std::to_string(getpid()) + "_" +
329+
std::to_string(time(nullptr)) + ".log";
330+
fault_fs_guard->SetInjectedErrorLogPath(log_path);
331+
}
332+
304333
if ((FLAGS_test_secondary || FLAGS_continuous_verification_interval > 0) &&
305334
FLAGS_secondaries_base.empty()) {
306335
std::string default_secondaries_path;

port/stack_trace.cc

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ void PrintAndFreeStack(void* /*callstack*/, int /*num_frames*/) {}
1818
void* SaveStack(int* /*num_frames*/, int /*first_frames_to_skip*/) {
1919
return nullptr;
2020
}
21+
void RegisterCrashCallback(CrashCallback /*callback*/) {}
2122
} // namespace port
2223
} // namespace ROCKSDB_NAMESPACE
2324

@@ -320,6 +321,7 @@ void* SaveStack(int* num_frames, int first_frames_to_skip) {
320321
static std::atomic<uint64_t> g_thread_handling_stack_trace{0};
321322
static int g_recursion_count = 0;
322323
static std::atomic<bool> g_at_exit_called{false};
324+
static std::atomic<CrashCallback> g_crash_callback{nullptr};
323325

324326
static void StackTraceHandler(int sig) {
325327
fprintf(stderr, "Received signal %d (%s)\n", sig, strsignal(sig));
@@ -360,6 +362,12 @@ static void StackTraceHandler(int sig) {
360362
fprintf(stderr, "In a race with process already exiting...\n");
361363
}
362364

365+
// Invoke registered crash callback before printing stack trace
366+
auto callback = g_crash_callback.load(std::memory_order_acquire);
367+
if (callback) {
368+
callback();
369+
}
370+
363371
// skip the top three signal handler related frames
364372
PrintStack(3);
365373

@@ -398,13 +406,41 @@ static void AtExit() {
398406
g_at_exit_called.store(true, std::memory_order_release);
399407
}
400408

409+
// Lightweight handler for graceful termination signals (SIGTERM, SIGINT).
410+
// Prints the crash callback (e.g., ring buffer) but skips the
411+
// expensive GDB/LLDB stack trace, since these are intentional terminations.
412+
static void TerminationHandler(int sig) {
413+
char buf[64];
414+
int len = snprintf(buf, sizeof(buf), "Received signal %d (%s)\n", sig,
415+
strsignal(sig));
416+
if (len > 0) {
417+
auto unused __attribute__((unused)) = write(STDOUT_FILENO, buf, len);
418+
}
419+
auto callback = g_crash_callback.load(std::memory_order_acquire);
420+
if (callback) {
421+
callback();
422+
}
423+
signal(sig, SIG_DFL);
424+
raise(sig);
425+
}
426+
427+
void RegisterCrashCallback(CrashCallback callback) {
428+
g_crash_callback.store(callback, std::memory_order_release);
429+
}
430+
401431
void InstallStackTraceHandler() {
402432
// just use the plain old signal as it's simple and sufficient
403433
// for this use case
434+
// Crash signals — invoke full stack trace + ring buffer
404435
signal(SIGILL, StackTraceHandler);
405436
signal(SIGSEGV, StackTraceHandler);
406437
signal(SIGBUS, StackTraceHandler);
407438
signal(SIGABRT, StackTraceHandler);
439+
signal(SIGFPE, StackTraceHandler);
440+
signal(SIGQUIT, StackTraceHandler);
441+
// Termination signals — print ring buffer only, no stack trace
442+
signal(SIGTERM, TerminationHandler);
443+
signal(SIGINT, TerminationHandler);
408444
atexit(AtExit);
409445
// Allow ouside debugger to attach, even with Yama security restrictions.
410446
// This is needed even outside of PrintStack() so that external mechanisms

port/stack_trace.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,14 @@ void PrintAndFreeStack(void* callstack, int num_frames);
2727
// Save the current callstack
2828
void* SaveStack(int* num_frame, int first_frames_to_skip = 0);
2929

30+
// Register a callback to be invoked when a fatal signal is received,
31+
// before the stack trace is printed. This is useful for printing diagnostic
32+
// information (e.g., recently injected errors) when a crash occurs.
33+
// The callback must only call async-signal-safe functions (write, snprintf,
34+
// etc.) or functions that are safe enough in practice (fprintf to stderr).
35+
// Only one callback is supported; subsequent calls overwrite the previous one.
36+
using CrashCallback = void (*)();
37+
void RegisterCrashCallback(CrashCallback callback);
38+
3039
} // namespace port
3140
} // namespace ROCKSDB_NAMESPACE

tools/db_crashtest.py

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
33

44
import argparse
5+
import glob
56
import math
67
import os
78
import random
@@ -1418,11 +1419,16 @@ def execute_cmd(cmd, timeout=None, timeout_pstack=False):
14181419
hit_timeout = True
14191420
if timeout_pstack:
14201421
os.system("pstack %d" % pid)
1421-
child.kill()
1422-
print("KILLED %d\n" % child.pid)
1423-
outs, errs = child.communicate()
1422+
child.terminate() # SIGTERM — triggers TerminationHandler
1423+
try:
1424+
outs, errs = child.communicate(timeout=3)
1425+
print("TERMINATED %d\n" % child.pid)
1426+
except subprocess.TimeoutExpired:
1427+
child.kill()
1428+
print("KILLED %d (SIGTERM did not work)\n" % child.pid)
1429+
outs, errs = child.communicate()
14241430

1425-
return hit_timeout, child.returncode, outs.decode("utf-8"), errs.decode("utf-8")
1431+
return hit_timeout, child.returncode, outs.decode("utf-8"), errs.decode("utf-8"), pid
14261432

14271433

14281434
def print_output_and_exit_on_error(stdout, stderr, print_stderr_separately=False):
@@ -1453,6 +1459,54 @@ def cleanup_after_success(dbname):
14531459
sys.exit(2)
14541460

14551461

1462+
def print_and_cleanup_fault_injection_log(pid):
1463+
# Fault injection logs are stored in TEST_TMPDIR (or /tmp) to survive
1464+
# DB reopen cleanup, and to be included in sandcastle's db.tar.gz artifact.
1465+
# Filter by pid to only print the log from the current run.
1466+
max_tail_entries = 32
1467+
log_dir = os.environ.get(_TEST_DIR_ENV_VAR) or "/tmp"
1468+
pattern = os.path.join(log_dir, "fault_injection_%d_*.log" % pid)
1469+
for log in glob.glob(pattern):
1470+
print("=== Fault injection log: %s ===" % log)
1471+
try:
1472+
with open(log) as f:
1473+
lines = f.readlines()
1474+
# Log format: header line(s), entry lines, footer line.
1475+
# The footer starts with "=== End of".
1476+
# Print header and footer always, truncate entries in the middle.
1477+
header = []
1478+
footer = []
1479+
entries = []
1480+
for line in lines:
1481+
stripped = line.strip()
1482+
if stripped.startswith("=== End of"):
1483+
footer.append(line)
1484+
elif stripped.startswith("===") or stripped == "(none)":
1485+
header.append(line)
1486+
else:
1487+
entries.append(line)
1488+
total_entries = len(entries)
1489+
print("".join(header), end="")
1490+
if total_entries <= max_tail_entries:
1491+
print("".join(entries), end="")
1492+
print("".join(footer), end="")
1493+
else:
1494+
skipped = total_entries - max_tail_entries
1495+
print(
1496+
"... (%d entries omitted, showing last %d. "
1497+
"Full log: %s)\n" % (skipped, max_tail_entries, log),
1498+
end="",
1499+
)
1500+
print("".join(entries[-max_tail_entries:]), end="")
1501+
print(
1502+
"=== Showed %d of %d injected error entries ===\n"
1503+
% (max_tail_entries, total_entries),
1504+
end="",
1505+
)
1506+
except OSError:
1507+
pass
1508+
1509+
14561510
# This script runs and kills db_stress multiple times. It checks consistency
14571511
# in case of unsafe crashes in RocksDB.
14581512
def blackbox_crash_main(args, unknown_args):
@@ -1476,7 +1530,9 @@ def blackbox_crash_main(args, unknown_args):
14761530
dict(list(cmd_params.items()) + list({"db": dbname}.items())), unknown_args
14771531
)
14781532

1479-
hit_timeout, retcode, outs, errs = execute_cmd(cmd, cmd_params["interval"])
1533+
hit_timeout, retcode, outs, errs, pid = execute_cmd(cmd, cmd_params["interval"])
1534+
1535+
print_and_cleanup_fault_injection_log(pid)
14801536

14811537
# Reset destroy_db_initially after each run (it may have been set by
14821538
# command line for first run only)
@@ -1502,10 +1558,12 @@ def blackbox_crash_main(args, unknown_args):
15021558
cmd = gen_cmd(
15031559
dict(list(cmd_params.items()) + list({"db": dbname}.items())), unknown_args
15041560
)
1505-
hit_timeout, retcode, outs, errs = execute_cmd(
1561+
hit_timeout, retcode, outs, errs, pid = execute_cmd(
15061562
cmd, cmd_params["verify_timeout"], True
15071563
)
15081564

1565+
print_and_cleanup_fault_injection_log(pid)
1566+
15091567
# For the final run
15101568
print_output_and_exit_on_error(outs, errs, args.print_stderr_separately)
15111569

@@ -1646,7 +1704,7 @@ def whitebox_crash_main(args, unknown_args):
16461704
# for job scheduling or execution.
16471705
# TODO detect a hanging condition. The job might run too long as RocksDB
16481706
# hits a hanging bug.
1649-
hit_timeout, retncode, stdoutdata, stderrdata = execute_cmd(
1707+
hit_timeout, retncode, stdoutdata, stderrdata, pid = execute_cmd(
16501708
cmd, exit_time - time.time() + 900
16511709
)
16521710

0 commit comments

Comments
 (0)