1
0
mirror of https://github.com/facebook/rocksdb.git synced 2026-10-23 08:09:02 +03:00
Files
rocksdb/tools/fault_injection_log_parser.py
Xingbo Wang 214869aacd Persist fault injection logs and fail fast on expected-state trace writes (#14651)
Summary:
- switch fault injection error recording from an in-memory ring buffer to per-run fixed-record binary logs under `TEST_TMPDIR/fault_injection_logs` (or `/tmp/fault_injection_logs`) so crash paths survive DB reopen cleanup
- keep the raw and decoded fault logs for external artifact collection/cleanup, and make `db_crashtest` print consistent blackbox/whitebox summaries after decoding
- make expected-state tracing fail fast on trace write failures and document offline trace inspection via `trace_analyzer`
- add coverage for binary log persistence/decoding/truncated-tail handling and keep info logs excluded from fault injection

Reviewed By: hx235

Differential Revision: D101973626

fbshipit-source-id: fdcb5b6370cf92a046e09b8d3391e80eecb66c23
2026-06-20 17:20:14 -07:00

257 lines
8.6 KiB
Python

#!/usr/bin/env python3
# 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).
import argparse
import struct
TRACE_FILE_MAGIC = b"FINJLOG1"
LEGACY_TRACE_FILE_VERSION = 1
RING_TRACE_FILE_VERSION = 2
TRACE_FILE_VERSION = 3
DETAIL_KIND_NONE = 0
DETAIL_KIND_TWO_FILES = 1
DETAIL_KIND_SIZE_AND_HEAD = 2
DETAIL_KIND_OFFSET_SIZE_AND_HEAD = 3
DETAIL_KIND_OFFSET_AND_SIZE = 4
DETAIL_KIND_SIZE = 5
DETAIL_KIND_COUNT = 6
DETAIL_KIND_REQ_OFFSET_AND_SIZE = 7
HEADER_STRUCT = struct.Struct("<8sQIIIIII")
LEGACY_ENTRY_STRUCT = struct.Struct("<QQ256s")
ENTRY_STRUCT = struct.Struct("<QQQQII48sBBBB32s72s56s4x")
def _read_exact(infile, size):
data = infile.read(size)
if len(data) != size:
raise ValueError("truncated injected error log")
return data
def _decode_c_string(raw):
return raw.split(b"\0", 1)[0].decode("utf-8", "replace")
def _format_hex_head(payload, total_size):
head = " ".join(f"{byte:02x}" for byte in payload)
if total_size > len(payload):
return f"{head} ..." if head else "..."
return head
def _format_detail(detail_kind, offset, size, count, req_idx, payload):
if detail_kind == DETAIL_KIND_NONE:
return ""
if detail_kind == DETAIL_KIND_TWO_FILES:
suffix = "..." if size > len(payload) else ""
return f"\"{payload.decode('utf-8', 'replace')}{suffix}\""
if detail_kind == DETAIL_KIND_SIZE_AND_HEAD:
return f"size={size}, head=[{_format_hex_head(payload, size)}]"
if detail_kind == DETAIL_KIND_OFFSET_SIZE_AND_HEAD:
return (
f"offset={offset}, size={size}, "
f"head=[{_format_hex_head(payload, size)}]"
)
if detail_kind == DETAIL_KIND_OFFSET_AND_SIZE:
return f"offset={offset}, size={size}"
if detail_kind == DETAIL_KIND_SIZE:
return f"size={size}"
if detail_kind == DETAIL_KIND_COUNT:
return f"num_reqs={count}"
if detail_kind == DETAIL_KIND_REQ_OFFSET_AND_SIZE:
return f"req[{req_idx}], offset={offset}, size={size}"
return f"detail_kind={detail_kind}"
def _decode_legacy_entries(infile, outfile, dumped_entries):
printed = 0
for _ in range(dumped_entries):
timestamp_us, thread_id, context = LEGACY_ENTRY_STRUCT.unpack(
_read_exact(infile, LEGACY_ENTRY_STRUCT.size)
)
if timestamp_us == 0:
continue
secs = timestamp_us // 1000000
usecs = timestamp_us % 1000000
context_str = _decode_c_string(context)
outfile.write(f"[{secs}.{usecs:06d}] thread={thread_id}: {context_str}\n")
printed += 1
return printed
def _decode_entry_bytes(entry_bytes, outfile):
(
timestamp_us,
thread_id,
offset,
size,
count,
req_idx,
detail_payload,
detail_kind,
payload_size,
retryable,
data_loss,
op_name,
file_name,
status_message,
) = ENTRY_STRUCT.unpack(entry_bytes)
if timestamp_us == 0:
return 0
if payload_size > len(detail_payload):
raise ValueError(f"invalid payload size in trace entry: {payload_size}")
secs = timestamp_us // 1000000
usecs = timestamp_us % 1000000
op_name = _decode_c_string(op_name)
file_name = _decode_c_string(file_name)
status_message = _decode_c_string(status_message)
payload = detail_payload[:payload_size]
detail = _format_detail(detail_kind, offset, size, count, req_idx, payload)
line = f"[{secs}.{usecs:06d}] thread={thread_id}: {op_name}(\"{file_name}\""
if detail:
line += f", {detail}"
line += f") -> IO error: {status_message}"
flags = []
if retryable:
flags.append("retryable")
if data_loss:
flags.append("data_loss")
if flags:
line += " [" + ",".join(flags) + "]"
outfile.write(line + "\n")
return 1
def _decode_ring_entries(infile, outfile, dumped_entries):
printed = 0
for _ in range(dumped_entries):
printed += _decode_entry_bytes(_read_exact(infile, ENTRY_STRUCT.size), outfile)
return printed
def _decode_stream_entries(infile, outfile):
data = infile.read()
full_entries = len(data) // ENTRY_STRUCT.size
printed = 0
for index in range(full_entries):
start = index * ENTRY_STRUCT.size
end = start + ENTRY_STRUCT.size
try:
printed += _decode_entry_bytes(data[start:end], outfile)
except ValueError:
if index + 1 == full_entries:
break
raise
return printed, full_entries
def decode_fault_injection_log(raw_path, output_path=None):
if output_path is None:
output_path = raw_path + ".txt"
with open(raw_path, "rb") as infile, open(output_path, "w") as outfile:
outfile.write(
"=== Recently Injected Fault Injection Errors (most recent last) ===\n"
)
header = infile.read(HEADER_STRUCT.size)
footer_total_entries = 0
footer_max_entries = 0
printed = 0
if len(header) >= len(TRACE_FILE_MAGIC) and header.startswith(TRACE_FILE_MAGIC):
if len(header) != HEADER_STRUCT.size:
raise ValueError("truncated injected error log header")
(
magic,
total_entries,
version,
header_size,
entry_size,
max_entries,
dumped_entries,
reserved,
) = HEADER_STRUCT.unpack(header)
if magic != TRACE_FILE_MAGIC:
raise ValueError(f"unexpected trace magic: {magic!r}")
if version not in (
LEGACY_TRACE_FILE_VERSION,
RING_TRACE_FILE_VERSION,
TRACE_FILE_VERSION,
):
raise ValueError(f"unsupported trace version: {version}")
if header_size != HEADER_STRUCT.size:
raise ValueError(
f"unexpected trace header size: {header_size} != {HEADER_STRUCT.size}"
)
footer_total_entries = total_entries
footer_max_entries = max_entries
if version == LEGACY_TRACE_FILE_VERSION:
if entry_size != LEGACY_ENTRY_STRUCT.size:
raise ValueError(
"unexpected legacy trace entry size: "
f"{entry_size} != {LEGACY_ENTRY_STRUCT.size}"
)
if reserved != 256:
raise ValueError(f"unexpected legacy max message len: {reserved}")
printed = _decode_legacy_entries(infile, outfile, dumped_entries)
elif version == RING_TRACE_FILE_VERSION:
if entry_size != ENTRY_STRUCT.size:
raise ValueError(
f"unexpected trace entry size: {entry_size} != {ENTRY_STRUCT.size}"
)
printed = _decode_ring_entries(infile, outfile, dumped_entries)
else:
if entry_size != ENTRY_STRUCT.size:
raise ValueError(
f"unexpected trace entry size: {entry_size} != {ENTRY_STRUCT.size}"
)
printed, full_entries = _decode_stream_entries(infile, outfile)
if footer_total_entries == 0:
footer_total_entries = full_entries
else:
infile.seek(0)
printed, footer_total_entries = _decode_stream_entries(infile, outfile)
if printed == 0:
outfile.write("(none)\n")
if footer_max_entries == 0:
outfile.write(
"=== End of injected error log (%d entries, total=%d, max=unbounded) ===\n"
% (printed, footer_total_entries)
)
else:
outfile.write(
"=== End of injected error log (%d entries, total=%d, max=%d) ===\n"
% (printed, footer_total_entries, footer_max_entries)
)
return printed
def _main():
parser = argparse.ArgumentParser(
description="Decode raw fault injection logs emitted by db_stress."
)
parser.add_argument("raw_log", help="Path to the raw .bin fault injection log")
parser.add_argument(
"--output",
help="Path for decoded text output. Defaults to <raw_log>.txt",
)
args = parser.parse_args()
output_path = args.output if args.output is not None else args.raw_log + ".txt"
decode_fault_injection_log(args.raw_log, output_path)
print(output_path)
if __name__ == "__main__":
_main()