diff --git a/Cargo.lock b/Cargo.lock
index 651b58ac..e0309640 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3431,6 +3431,7 @@ name = "probing-memtable"
version = "0.2.5"
dependencies = [
"libc",
+ "log",
"memmap2",
"pco",
"xxhash-rust",
@@ -3500,10 +3501,13 @@ dependencies = [
"pyo3-build-config",
"regex",
"rustc-demangle",
+ "serde",
"serde_json",
+ "sha2",
"signal-hook-registry",
"tempfile",
"tokio",
+ "uuid",
]
[[package]]
@@ -4061,6 +4065,17 @@ dependencies = [
"digest",
]
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
[[package]]
name = "shlex"
version = "2.0.1"
diff --git a/Makefile b/Makefile
index dcfb840c..283cc0f1 100644
--- a/Makefile
+++ b/Makefile
@@ -47,6 +47,9 @@ CLIPPY_WORKSPACE := cargo clippy --workspace --all-targets --no-default-features
CLIPPY_CORE := cargo clippy -p probing-core --all-targets --no-default-features $(CLIPPY_DENY)
CLIPPY_WEB := cd web && cargo clippy --all-targets $(CLIPPY_DENY)
+FMT_WORKSPACE := cargo fmt --all
+FMT_WEB := cd web && cargo fmt --all
+
# ==============================================================================
.PHONY: help
help:
@@ -60,6 +63,7 @@ help:
@echo " install-wheel pip install dist/probing-*.whl"
@echo " venv create/refresh project .venv (used by develop and CI)"
@echo " test / lint Full test and lint suites"
+ @echo " fmt rustfmt workspace + web (same paths as CI)"
@echo " check-dev Quick env sanity check"
@echo " clean Remove build artifacts"
@echo ""
@@ -203,7 +207,7 @@ PYTEST_WHEEL_FLAGS := --import-mode=importlib -o pythonpath= -o "addopts=--verbo
PYTEST_WHEEL_EXTRA ?=
.PHONY: test test-rust test-rust-unit test-rust-regression test-python test-python-unit test-python-regression test-doctest test-python-wheel coverage-python-wheel
-.PHONY: lint lint-python lint-rust lint-core clippy clippy-fix coverage coverage-rust coverage-python bootstrap clean docs-install docs docs-serve docs-clean
+.PHONY: fmt fmt-check lint lint-python lint-rust lint-core clippy clippy-fix coverage coverage-rust coverage-python bootstrap clean docs-install docs docs-serve docs-clean
test: test-rust test-python
test-rust: test-rust-unit test-rust-regression
@@ -241,6 +245,12 @@ coverage-python-wheel:
$(MAKE) test-python-wheel PYTEST_WHEEL_EXTRA="--cov=probing --cov=tests --cov-report=xml:coverage.xml"
lint: lint-python lint-rust
+fmt:
+ $(FMT_WORKSPACE)
+ $(FMT_WEB)
+fmt-check:
+ $(FMT_WORKSPACE) -- --check
+ $(FMT_WEB) -- --check
lint-core:
$(CLIPPY_CORE)
lint-python:
diff --git a/examples/crash_demo.py b/examples/crash_demo.py
new file mode 100644
index 00000000..8d111cdf
--- /dev/null
+++ b/examples/crash_demo.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python3
+"""单进程 crash demo — ``PROBING=1 python examples/crash_demo.py [--mode exception]``.
+
+Probing 由 site hook 自动加载;本脚本不 import probing。
+``record``:后台线程异常,写入 memtable 后正常退出;``exception``:主线程崩溃。
+"""
+
+from __future__ import annotations
+
+import argparse
+import os
+import threading
+
+
+def _crash(*, record_only: bool) -> None:
+ def boom() -> None:
+ def inner() -> None:
+ raise ValueError("demo crash from crash_demo.py")
+
+ inner()
+
+ if record_only:
+ t = threading.Thread(target=boom, name="crash-demo", daemon=True)
+ t.start()
+ t.join()
+ else:
+ boom()
+
+
+def main() -> None:
+ p = argparse.ArgumentParser()
+ p.add_argument("--mode", choices=("record", "exception"), default="record")
+ args = p.parse_args()
+
+ os.environ.setdefault("RANK", "0")
+ os.environ.setdefault("LOCAL_RANK", "0")
+
+ _crash(record_only=args.mode == "record")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/crash_torchrun_demo.py b/examples/crash_torchrun_demo.py
new file mode 100644
index 00000000..57e953e2
--- /dev/null
+++ b/examples/crash_torchrun_demo.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python3
+"""torchrun crash demo — run via ``./examples/run_crash_torchrun.sh``.
+
+Probing loads from ``PROBING=…`` (site hook); crash capture needs no imports here.
+Modes: ``record`` (thread exception, process stays up), ``exception`` / ``all`` (fatal).
+"""
+
+from __future__ import annotations
+
+import argparse
+import os
+import threading
+
+import torch.distributed as dist
+
+
+def _crash(rank: int, *, record_only: bool) -> None:
+ def boom() -> None:
+ raise RuntimeError(f"demo crash on rank {rank}")
+
+ if record_only:
+ t = threading.Thread(target=boom, name=f"crash-r{rank}", daemon=True)
+ t.start()
+ t.join()
+ else:
+ boom()
+
+
+def main() -> None:
+ p = argparse.ArgumentParser()
+ p.add_argument("--mode", choices=("record", "exception", "all"), default="record")
+ p.add_argument("--crash-rank", type=int, default=1)
+ p.add_argument("--backend", default=os.environ.get("PROBING_TORCH_BACKEND", "gloo"))
+ args = p.parse_args()
+
+ dist.init_process_group(backend=args.backend)
+ rank, world = dist.get_rank(), dist.get_world_size()
+ print(f"[rank {rank}/{world}] ready", flush=True)
+
+ dist.barrier()
+ if args.mode == "all" or rank == args.crash_rank:
+ _crash(rank, record_only=args.mode == "record")
+
+ if args.mode == "record":
+ dist.barrier()
+
+ dist.destroy_process_group()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/run_crash_torchrun.sh b/examples/run_crash_torchrun.sh
new file mode 100755
index 00000000..1d4dd167
--- /dev/null
+++ b/examples/run_crash_torchrun.sh
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+# torchrun crash demo — run from repo root: ./examples/run_crash_torchrun.sh [mode] [crash-rank]
+# mode: record (default) | exception | all
+set -euo pipefail
+
+cd "$(dirname "$0")/.."
+[[ -f .venv/bin/activate ]] && source .venv/bin/activate
+
+MODE="${1:-record}"
+CRASH_RANK="${2:-1}"
+NPROC="${NPROC:-4}"
+MASTER_PORT="${MASTER_PORT:-29571}"
+
+export PROBING="${PROBING:-regex:crash_torchrun_demo.py}"
+export MASTER_ADDR=127.0.0.1
+export MASTER_PORT
+[[ "$(uname -s)" == Darwin ]] && export GLOO_SOCKET_IFNAME="${GLOO_SOCKET_IFNAME:-lo0}"
+
+python -c "import torch" 2>/dev/null || {
+ echo "need torch: uv pip install torch" >&2
+ exit 1
+}
+
+exec torchrun --standalone --nnodes=1 --nproc_per_node="$NPROC" \
+ --master_addr=127.0.0.1 --master_port="$MASTER_PORT" --local_addr=127.0.0.1 \
+ examples/crash_torchrun_demo.py --mode "$MODE" --crash-rank "$CRASH_RANK"
diff --git a/probing/core/src/core/memtable_sql.rs b/probing/core/src/core/memtable_sql.rs
index db3d2a58..73821b5f 100644
--- a/probing/core/src/core/memtable_sql.rs
+++ b/probing/core/src/core/memtable_sql.rs
@@ -29,7 +29,7 @@
use std::collections::{BTreeSet, HashSet};
use std::panic::AssertUnwindSafe;
use std::sync::atomic::{AtomicBool, Ordering};
-use std::sync::{Arc, Mutex};
+use std::sync::{Arc, Mutex, MutexGuard};
use std::thread::JoinHandle;
use std::time::Duration;
@@ -1104,6 +1104,18 @@ pub struct ColdCompactor {
handle: Mutex