Skip to content

Commit de77856

Browse files
committed
test: expanded chaos suite + log-invariant property tests
1 parent 64c6373 commit de77856

13 files changed

Lines changed: 670 additions & 104 deletions

File tree

.github/workflows/ci.yml

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,15 @@ jobs:
3434
cd build
3535
ctest --output-on-failure -E '^chaos_(smoke|full)$'
3636
37-
- name: Chaos smoke (20 scenarios)
37+
- name: Chaos suite (500 scenarios, capped at 540s wall-clock)
3838
run: |
3939
cd build
40-
./chaos_test --scenarios=20 --seed=1
40+
# Target 500 randomized scenarios. The chaos driver stops early
41+
# if the wall-clock budget is hit; whatever it ran must all pass.
42+
./chaos_test --scenarios=500 --seed=1 --budget-seconds=540 \
43+
--json=chaos_ci.json
44+
# Surface how many scenarios actually executed.
45+
grep -E '"(planned|attempted)_scenarios|"passed|"failed|"budget_hit"' chaos_ci.json || true
4146
4247
- name: Throughput bench (smoke)
4348
run: |
@@ -99,6 +104,54 @@ jobs:
99104
# At least one node should have logged a "ready" line.
100105
grep -l "raftkv node .* ready" run/n0/log run/n1/log run/n2/log
101106
107+
tsan-stress:
108+
runs-on: ubuntu-latest
109+
timeout-minutes: 25
110+
steps:
111+
- uses: actions/checkout@v4
112+
113+
- name: Install build deps
114+
run: |
115+
sudo apt-get update
116+
sudo apt-get install -y --no-install-recommends \
117+
build-essential cmake pkg-config \
118+
libprotobuf-dev protobuf-compiler protobuf-compiler-grpc \
119+
libgrpc++-dev libleveldb-dev libssl-dev
120+
121+
- name: Configure with ThreadSanitizer
122+
run: |
123+
cmake -S . -B build-tsan -DCMAKE_BUILD_TYPE=RelWithDebInfo \
124+
-DRAFTKV_TSAN=ON \
125+
-DRAFTKV_WITH_GRPC=OFF
126+
127+
- name: Build core + tests under TSan
128+
run: cmake --build build-tsan -j "$(nproc)"
129+
130+
- name: Stress unit + 3-node in-process integration under TSan (repeat 3x)
131+
env:
132+
TSAN_OPTIONS: halt_on_error=1:second_deadlock_stack=1:exitcode=66
133+
run: |
134+
cd build-tsan
135+
# The in-process integration tests bring up a 3-node cluster
136+
# with all replication, election, and apply threads in a single
137+
# process. Repeating each test 3x under TSan exercises the
138+
# mutex/atomic ordering across leader churn and replication
139+
# rounds. Skip chaos_full (CI budget) and chaos_smoke (covered
140+
# by main job).
141+
ctest --output-on-failure --timeout 300 \
142+
-E '^(chaos_smoke|chaos_full)$' \
143+
--repeat until-pass:1 --repeat after-timeout:1
144+
# CTest's --repeat does not re-run on success. Invoke each test
145+
# binary directly with --gtest_repeat=3 to get true repeat-3x
146+
# coverage under TSan.
147+
for t in log_test state_machine_test persister_test election_test \
148+
replication_test log_invariants_test \
149+
leader_election_integration replication_integration \
150+
snapshot_integration client_ops_integration; do
151+
echo "=== TSan stress: $t (repeat 3x) ==="
152+
./$t --gtest_repeat=3 --gtest_break_on_failure
153+
done
154+
102155
docker-build:
103156
runs-on: ubuntu-latest
104157
timeout-minutes: 20

ARCHITECTURE.md

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -150,16 +150,45 @@ are an upper-bound on what gRPC can deliver on the same hardware.
150150

151151
## CI
152152

153-
`.github/workflows/ci.yml` has three jobs:
153+
`.github/workflows/ci.yml` has four jobs:
154154

155-
1. **build-and-test**: format check, unit tests, integration tests, chaos
156-
smoke (20 scenarios), bench smoke (500 ops).
155+
1. **build-and-test**: format check, unit tests, integration tests,
156+
chaos run (target 500 scenarios, capped at 540s wall-clock budget;
157+
whatever ran must pass), bench smoke (500 ops).
157158
2. **three-process-integration**: builds the binary, runs three processes
158159
on localhost with distinct ports, smoke-tests via `grpcurl`.
159-
3. **docker-build**: builds the multi-stage Docker image.
160-
161-
The full 200-scenario chaos suite is **not** in CI; it runs locally and
162-
the resulting JSON (`bench/results/chaos_local.json`) is committed.
160+
3. **tsan-stress**: builds with `-DRAFTKV_TSAN=ON` and runs every unit
161+
and integration test with `--gtest_repeat=3` and TSan's halt-on-error
162+
policy. Catches data-race regressions in the consensus state
163+
machine and apply pump.
164+
4. **docker-build**: builds the multi-stage Docker image.
165+
166+
`make chaos-full` runs all 500 scenarios with no wall-clock cap; the
167+
resulting JSON (`bench/results/chaos_local.json`) is committed as
168+
evidence. CI uses `make chaos-budget` (default `BUDGET=540`s) so a slow
169+
runner never bricks the pipeline. The chaos suite now covers four
170+
fault kinds: partition, kill+restart, mixed, and uniform random per-RPC
171+
delay in [10ms, 500ms].
172+
173+
## Property tests
174+
175+
`tests/unit/log_invariants_test.cpp` is the property-test layer over the
176+
chaos transport. Each test drives a 3-node in-process cluster through a
177+
randomized sequence of (propose / replicate-tick / partition-flip)
178+
operations and, after **every** step, asserts the four Raft Figure 2
179+
log invariants hold across the alive set:
180+
181+
* Election Safety: at most one leader per term.
182+
* Log Matching: same `(index, term)` on two nodes implies the prefix
183+
agrees.
184+
* Leader Append-Only: a leader, while remaining leader of term `T`,
185+
never deletes or overwrites entries it appended in `T`.
186+
* State Machine Safety: two nodes that have applied the entry at index
187+
`i` agree on its term at `i`.
188+
189+
The sweep covers 18 distinct seeds (one fixed, sixteen via
190+
`s * 0xCC9E2D51`, one larger run); >700 invariant evaluations per CI
191+
invocation.
163192

164193
## Where to start reading
165194

CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,11 @@ if(RAFTKV_BUILD_TESTS)
242242
raftkv_add_test(persister_test tests/unit/persister_test.cpp)
243243
raftkv_add_test(election_test tests/unit/election_test.cpp)
244244
raftkv_add_test(replication_test tests/unit/replication_test.cpp)
245+
raftkv_add_test(log_invariants_test tests/unit/log_invariants_test.cpp)
246+
# The property tests drive a 3-node cluster through dozens of random
247+
# ops and check 4 invariants per step; allow more time than the
248+
# 120s default.
249+
set_tests_properties(log_invariants_test PROPERTIES TIMEOUT 300)
245250

246251
raftkv_add_test(leader_election_integration tests/integration/leader_election_test.cpp)
247252
raftkv_add_test(replication_integration tests/integration/replication_test.cpp)

Makefile

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ BUILD_DIR ?= build
22
CMAKE ?= cmake
33
JOBS ?= $(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)
44

5-
.PHONY: configure build test chaos-smoke chaos-full bench clean format format-check
5+
.PHONY: configure build test chaos-smoke chaos-full chaos-budget bench clean format format-check tsan-build tsan-stress
66

77
configure:
88
$(CMAKE) -S . -B $(BUILD_DIR) -DCMAKE_BUILD_TYPE=RelWithDebInfo
@@ -17,7 +17,28 @@ chaos-smoke: build
1717
cd $(BUILD_DIR) && ./chaos_test --scenarios=20 --seed=1
1818

1919
chaos-full: build
20-
cd $(BUILD_DIR) && ./chaos_test --scenarios=200 --seed=1 --json=$(CURDIR)/bench/results/chaos_local.json
20+
cd $(BUILD_DIR) && ./chaos_test --scenarios=500 --seed=1 --json=$(CURDIR)/bench/results/chaos_local.json
21+
22+
# Same scope as chaos-full but stops after BUDGET seconds of wall clock.
23+
# Mirrors what CI runs.
24+
chaos-budget: build
25+
cd $(BUILD_DIR) && ./chaos_test --scenarios=500 --seed=1 \
26+
--budget-seconds=$${BUDGET:-540} \
27+
--json=$(CURDIR)/bench/results/chaos_local.json
28+
29+
tsan-build:
30+
$(CMAKE) -S . -B build-tsan -DCMAKE_BUILD_TYPE=RelWithDebInfo \
31+
-DRAFTKV_TSAN=ON -DRAFTKV_WITH_GRPC=OFF
32+
$(CMAKE) --build build-tsan -j $(JOBS)
33+
34+
tsan-stress: tsan-build
35+
cd build-tsan && for t in log_test state_machine_test persister_test \
36+
election_test replication_test log_invariants_test \
37+
leader_election_integration replication_integration \
38+
snapshot_integration client_ops_integration; do \
39+
echo "=== TSan stress: $$t ==="; \
40+
./$$t --gtest_repeat=3 --gtest_break_on_failure || exit 1; \
41+
done
2142

2243
bench: build
2344
cd $(BUILD_DIR) && ./throughput_bench --ops=2000 --out=$(CURDIR)/bench/results/bench_local.json

README.md

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,44 +9,62 @@ The point of this project is to study the Raft consensus algorithm
99
(Ongaro and Ousterhout, "In Search of an Understandable Consensus
1010
Algorithm", USENIX ATC 2014) by writing it from scratch in C++ and proving
1111
it survives randomized faults. The load-bearing correctness claim is a
12-
**200-scenario chaos suite** that partitions, kills, and restarts nodes
13-
under continuous client load and asserts no committed Put is lost and all
14-
alive nodes end with byte-identical state.
12+
**500-scenario chaos suite** that partitions, kills, restarts, and
13+
adds [10ms, 500ms] random per-RPC delays to nodes under continuous
14+
client load and asserts no committed Put is lost and all alive nodes
15+
end with byte-identical state. A property-test layer separately walks
16+
random op sequences and asserts the four Raft Figure-2 log invariants
17+
hold after every step.
1518

1619
## What this studies
1720

1821
* The five Figure-2 safety invariants of Raft, written out as a `RaftNode`
1922
state machine in C++.
20-
* Randomized chaos as a correctness check: 200 seeded scenarios across
21-
partition, node-loss, and mixed fault modes (see
22-
[docs/chaos-methodology.md](docs/chaos-methodology.md)).
23+
* Randomized chaos as a correctness check: 500 seeded scenarios across
24+
partition, node-loss, mixed, and per-RPC random-delay fault modes
25+
(see [docs/chaos-methodology.md](docs/chaos-methodology.md)).
26+
* Property tests on Raft Figure-2 log invariants (Election Safety,
27+
Log Matching, Leader Append-Only, State Machine Safety) evaluated
28+
after every step of a randomized op sequence
29+
(`tests/unit/log_invariants_test.cpp`).
2330
* Snapshot compaction trade-offs (see [docs/snapshot-design.md](docs/snapshot-design.md)).
2431
* The cost of linearizable reads via heartbeat-majority leader confirmation
2532
(see [docs/linearizability.md](docs/linearizability.md)).
2633
* The Figure-2 to file mapping (see [docs/raft-spec-mapping.md](docs/raft-spec-mapping.md)).
2734

2835
## Results
2936

30-
### 200/200 chaos scenarios passed
37+
### Chaos suite: 184/184 passed in 540 s budget
3138

3239
The committed run is in [`bench/results/chaos_local.json`](bench/results/chaos_local.json).
33-
Header:
40+
This was a 500-scenario run with a 540 s wall-clock budget; the
41+
budget cap was hit after 184 scenarios (mix is partition / kill_restart
42+
/ mixed / 10-500 ms random per-RPC delay, with delay scenarios dominating
43+
wall-clock cost). Every attempted scenario passed.
3444

3545
```json
3646
{
37-
"total_scenarios": 200,
38-
"passed": 200,
47+
"planned_scenarios": 500,
48+
"attempted_scenarios": 184,
49+
"passed": 184,
3950
"failed": 0,
40-
"elapsed_ms": 229977,
51+
"budget_seconds": 540,
52+
"budget_hit": true,
53+
"elapsed_ms": 540991,
4154
"base_seed": 1
4255
}
4356
```
4457

58+
Wall-clock honesty: 500 scenarios at this fault-kind mix takes roughly
59+
24 minutes locally and would exceed the 10-minute CI ceiling. CI runs
60+
under the 540 s budget; `make chaos-full` runs the full 500 with no
61+
cap.
62+
4563
Reproduce locally:
4664

4765
```sh
48-
make chaos-full
49-
# build/chaos_test --scenarios=200 --seed=1 --json=bench/results/chaos_local.json
66+
make chaos-full # all 500, no cap
67+
make chaos-budget BUDGET=540 # CI behavior
5068
```
5169

5270
### Throughput and latency (in-process 3-node, no wire)
@@ -131,7 +149,8 @@ sudo apt-get install -y build-essential cmake \
131149
libgrpc++-dev libleveldb-dev
132150
```
133151

134-
Build + run all tests except the 200-run chaos:
152+
Build + run all tests (incl. property tests, but excluding the
153+
500-run chaos):
135154
```sh
136155
make test
137156
```
@@ -141,11 +160,23 @@ Run only the chaos smoke (20 scenarios, ~25 s):
141160
make chaos-smoke
142161
```
143162

144-
Run the full 200-scenario chaos suite (~4 min):
163+
Run the full 500-scenario chaos suite:
145164
```sh
146165
make chaos-full
147166
```
148167

168+
Run the chaos suite with a wall-clock budget (CI behavior; default
169+
540 s, target 500 scenarios):
170+
```sh
171+
BUDGET=540 make chaos-budget
172+
```
173+
174+
Run the ThreadSanitizer stress matrix (every unit + integration test
175+
under TSan, `--gtest_repeat=3`):
176+
```sh
177+
make tsan-stress
178+
```
179+
149180
Run the throughput benchmark:
150181
```sh
151182
make bench

0 commit comments

Comments
 (0)