snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-16 03:23:42 +05:00
commit 89f5be005d
3997 changed files with 961970 additions and 0 deletions

View File

@@ -0,0 +1,520 @@
package ec
import (
"fmt"
"sort"
"strings"
"testing"
)
// Bounded-exhaustive model check of the EC volume lifecycle.
//
// The randomized chaos harness samples the state space; this test enumerates
// it. The lifecycle of one volume is modeled as a state machine whose
// transitions mirror, step by step, the real pipelines in this package, and
// the checker explores EVERY schedule within the bound: every operation, a
// crash (kill, no cleanup) at every step boundary, an error return (which
// runs the rollback, itself crashable at every step) at every step boundary,
// a volume-server restart (running the startup reconciliation rules) in every
// quiescent state, and every recovery re-run from every crashed state.
//
// Two safety invariants are asserted in every reachable state, including
// mid-operation and post-crash:
//
// I1 durability: the volume's data is always recoverable — the regular
// replica exists, or some single generation holds at least dataShards
// shard files.
// I2 single serving generation: at most one generation is ever mounted.
// I3 single generation on disk: the sweep-before-generate discipline keeps
// at most one generation's files on disk through every schedule. (An
// externally planted file breaks this by construction; that path is the
// chaos harness's, not the orchestration's.)
//
// And one convergence property: from every quiescent state, the prescribed
// recovery — re-running the interrupted operation, per the restart-not-resume
// model — terminates in a clean state (a writable regular volume with no EC
// leftovers, or exactly one complete mounted generation with no source
// replica and no surplus copies).
//
// The model's fidelity contract: each step below names the code it stands
// for. When the pipeline order changes, this file must change with it.
//
// encode = ProcessEcEncodeBatch/doEcEncode: markReadonly
// (markVolumeReplicaWritable false) → sweep
// (clearPreexistingEcShards, removes every prior generation) →
// generate (VolumeEcShardsGenerate, a new generation's files) →
// mount (VolumeEcShardsMount, registers it) → balance
// (EcBalance; a crash mid-move leaves a surplus copy, never a
// loss, because moves copy before they delete) → verify
// (verifyEcShardsBeforeDelete) → deleteSource
// (doDeleteVolumesWithLocations, the commit point).
// rollback = rollbackFailedEcEncode, run only on an error return before
// the commit: rollbackSweep (clearPreexistingEcShards again) →
// restoreWritable. A kill runs nothing.
// decode = DoEcDecode: collect (collectEcShards) → generateVolume
// (VolumeEcShardsToVolume) → mountVolume (VolumeMount) →
// verifyDecoded (verifyDecodedVolumeBeforeDelete) → three
// per-location shard deletions (unmountAndDeleteEcShards).
// Forward-only: no rollback.
// reconcile = the volume server startup rules (weed/storage
// disk_location_ec.go): with the source .dat present, a
// partial generation fails validation and is deleted
// (handleFoundEcxFile/validateEcVolume); surviving shard files
// with their sidecars re-register (the #9212 adoption).
// dedup = the balance's dedup phase removing surplus copies
// (ecbalancer "dedup" moves + verifyEcShardOnKeepNode).
//
// Deliberately outside the model: multi-replica sources (the pipeline syncs
// and reduces to one best replica before encoding), per-node shard placement
// (I1/I2 do not depend on it), and externally planted files (the chaos
// harness covers adoption of foreign shards).
const (
modelDataShards = 10
modelTotalShards = 14
)
type modelGen struct {
files int // shard files on disk, whether or not registered
mounted bool // registered and serving
surplus int // duplicate copies left by an interrupted balance move
}
type modelState struct {
replica bool
replicaReadonly bool
gens []modelGen // oldest first; a new encode appends
}
func (s modelState) clone() modelState {
c := s
c.gens = append([]modelGen(nil), s.gens...)
return c
}
func (s modelState) key() string {
var b strings.Builder
fmt.Fprintf(&b, "r%v,ro%v", s.replica, s.replicaReadonly)
for _, g := range s.gens {
fmt.Fprintf(&b, "|f%d,m%v,s%d", g.files, g.mounted, g.surplus)
}
return b.String()
}
// dropEmptyGens removes generations with no files left.
func (s *modelState) dropEmptyGens() {
kept := s.gens[:0]
for _, g := range s.gens {
if g.files > 0 {
kept = append(kept, g)
}
}
s.gens = kept
}
// ── invariants ──────────────────────────────────────────────────────────────
func checkInvariants(s modelState) error {
// I1: durability.
recoverable := s.replica
for _, g := range s.gens {
if g.files >= modelDataShards {
recoverable = true
}
}
if !recoverable {
return fmt.Errorf("I1 violated: no replica and no generation with >= %d shard files", modelDataShards)
}
// I2: at most one mounted generation.
mounted := 0
for _, g := range s.gens {
if g.mounted {
mounted++
}
}
if mounted > 1 {
return fmt.Errorf("I2 violated: %d generations mounted", mounted)
}
// I3: at most one generation's files on disk.
withFiles := 0
for _, g := range s.gens {
if g.files > 0 {
withFiles++
}
}
if withFiles > 1 {
return fmt.Errorf("I3 violated: %d generations have files on disk", withFiles)
}
return nil
}
func isCleanEc(s modelState) bool {
if s.replica || len(s.gens) != 1 {
return false
}
g := s.gens[0]
return g.files == modelTotalShards && g.mounted && g.surplus == 0
}
// ── operations as step lists ────────────────────────────────────────────────
// A step mutates the state; ok=false means the step's precondition failed
// (the real command would return an error at this point).
type modelStep struct {
name string
apply func(*modelState) (ok bool)
}
func encodeSteps() []modelStep {
return []modelStep{
{"markReadonly", func(s *modelState) bool {
if !s.replica {
return false
}
s.replicaReadonly = true
return true
}},
// The orphan sweep tears down per (node, volume); a crash can land
// between nodes, leaving a partially removed generation.
{"sweepLoc0", func(s *modelState) bool { sweepLocation(s, 0); return true }},
{"sweepLoc1", func(s *modelState) bool { sweepLocation(s, 1); return true }},
{"sweepLoc2", func(s *modelState) bool { sweepLocation(s, 2); return true }},
// Generation writes the shard files on the generation host; a crash
// mid-write leaves a partial, unregistered set beside the .dat, which
// the startup validation removes and the next sweep also covers.
{"generateHalf", func(s *modelState) bool {
if !s.replica {
return false
}
s.gens = append(s.gens, modelGen{files: modelTotalShards / 2})
return true
}},
{"generateRest", func(s *modelState) bool {
s.gens[len(s.gens)-1].files = modelTotalShards
return true
}},
{"mount", func(s *modelState) bool {
s.gens[len(s.gens)-1].mounted = true
return true
}},
// balance: a completed run changes placement only. Its crash variant
// is modeled by crashing between "balanceCopy" and "balanceDelete":
// the copy landed, the source deletion did not.
{"balanceCopy", func(s *modelState) bool {
s.gens[len(s.gens)-1].surplus++
return true
}},
{"balanceDelete", func(s *modelState) bool {
s.gens[len(s.gens)-1].surplus--
return true
}},
{"verify", func(s *modelState) bool {
return s.gens[len(s.gens)-1].files >= modelDataShards
}},
{"deleteSource", func(s *modelState) bool {
s.replica = false
s.replicaReadonly = false
return true
}},
}
}
// encodeCommitIndex is the index of deleteSource: an error return at or after
// it must not roll back (the shards are the only copy once it runs).
func encodeCommitIndex() int { return len(encodeSteps()) - 1 }
// sweepLocation removes one location's share of every generation's files —
// the per-(node, volume) teardown of clearPreexistingEcShards. The last
// location clears the remainder, sidecars included.
func sweepLocation(s *modelState, loc int) {
for gi := range s.gens {
g := &s.gens[gi]
chunk := modelTotalShards / 3
if loc == 2 {
g.files, g.mounted, g.surplus = 0, false, 0
} else if g.files > chunk {
g.files -= chunk
} else {
g.files = 0
}
}
s.dropEmptyGens()
}
func rollbackSteps() []modelStep {
return []modelStep{
{"rollbackSweep", func(s *modelState) bool {
s.gens = nil
return true
}},
{"restoreWritable", func(s *modelState) bool {
if s.replica {
s.replicaReadonly = false
}
return true
}},
}
}
func decodeSteps() []modelStep {
steps := []modelStep{
{"collect", func(s *modelState) bool {
for _, g := range s.gens {
if g.mounted && g.files >= modelDataShards {
return true
}
}
return false
}},
{"generateVolume", func(s *modelState) bool {
s.replica = true
return true
}},
{"mountVolume", func(s *modelState) bool { return true }},
{"verifyDecoded", func(s *modelState) bool { return s.replica }},
}
// Shard deletion is per holder location; three locations model a spread
// volume, so a crash can land between any two of them.
for i := 0; i < 3; i++ {
i := i
steps = append(steps, modelStep{fmt.Sprintf("deleteShardsLoc%d", i), func(s *modelState) bool {
for gi := range s.gens {
g := &s.gens[gi]
if !g.mounted && g.files == 0 {
continue
}
// Each location holds roughly a third of the shards; the last
// pass clears the remainder and the generation with it.
chunk := modelTotalShards / 3
if i == 2 {
g.files, g.surplus, g.mounted = 0, 0, false
} else if g.files > chunk {
g.files -= chunk
}
}
s.dropEmptyGens()
return true
}})
}
return steps
}
// reconcile applies the volume-server startup rules after a crash-restart.
func reconcile(s *modelState) {
for gi := range s.gens {
g := &s.gens[gi]
if s.replica && g.files < modelTotalShards {
// .dat present and the local generation is partial: validation
// fails and the files are removed (disk_location_ec.go).
g.files, g.mounted, g.surplus = 0, false, 0
continue
}
if g.files > 0 {
// Surviving files re-register through their sidecars (#9212).
g.mounted = true
}
}
s.dropEmptyGens()
}
// dedupSurplus is the balance dedup phase.
func dedupSurplus(s *modelState) {
for gi := range s.gens {
s.gens[gi].surplus = 0
}
}
// ── recovery convergence ────────────────────────────────────────────────────
// runToCompletion applies steps without interruption; false when a
// precondition fails (the real re-run would error out).
func runToCompletion(s *modelState, steps []modelStep) bool {
for _, st := range steps {
if !st.apply(s) {
return false
}
}
return true
}
// recover applies the prescribed recovery for a quiescent state and reports
// whether it converges to a clean state. Per the restart-not-resume model:
// a surviving replica means the encode restarts from scratch; no replica
// means the encode had committed and only surplus cleanup can remain; a
// mounted generation beside a replica can also be decoded away.
func recoverModel(s modelState) (modelState, error) {
// A crash-restart happens before any operator action; its reconciliation
// must itself keep the invariants.
reconcile(&s)
if err := checkInvariants(s); err != nil {
return s, fmt.Errorf("after reconcile: %w", err)
}
if !s.replica {
// The encode committed: shards are the volume. Dedup cleans surplus.
dedupSurplus(&s)
if !isCleanEc(s) {
return s, fmt.Errorf("no replica and not clean EC")
}
return s, nil
}
// Replica survives: re-run the encode from scratch (sweep + regenerate),
// then the volume is EC; this is the restart path every interrupted
// encode and every leftover-orphan state takes.
if !runToCompletion(&s, encodeSteps()) {
return s, fmt.Errorf("recovery encode did not complete")
}
dedupSurplus(&s)
if err := checkInvariants(s); err != nil {
return s, fmt.Errorf("after recovery encode: %w", err)
}
if !isCleanEc(s) {
return s, fmt.Errorf("recovery encode did not converge to clean EC")
}
// And a decode must be able to bring it back to a regular volume.
if !runToCompletion(&s, decodeSteps()) {
return s, fmt.Errorf("decode after recovery did not complete")
}
if err := checkInvariants(s); err != nil {
return s, fmt.Errorf("after decode: %w", err)
}
if !s.replica || len(s.gens) != 0 {
return s, fmt.Errorf("decode after recovery did not converge to a bare regular volume")
}
return s, nil
}
// ── exhaustive exploration ──────────────────────────────────────────────────
type trace []string
// explore runs every schedule of one operation from state s: after every
// step, the schedule may continue, crash (kill: stop, nothing else runs), or
// error out (rollback runs, itself crashable at every step). Every resulting
// quiescent state is handed to onQuiescent together with the trace that
// produced it.
func exploreOp(t *testing.T, s modelState, opName string, steps []modelStep, commitIndex int, withRollback bool, tr trace, onQuiescent func(modelState, trace)) {
t.Helper()
// Crash before the first step is the same as never starting.
for prefix := 1; prefix <= len(steps); prefix++ {
run := s.clone()
ok := true
for i := 0; i < prefix; i++ {
if !steps[i].apply(&run) {
ok = false
break
}
if err := checkInvariants(run); err != nil {
t.Fatalf("%s after %s: %v\ntrace: %v", opName, steps[i].name, err, append(tr, opName+"/"+steps[i].name))
}
}
if !ok {
continue // precondition stopped the op; same as an early error
}
stepTrace := append(append(trace{}, tr...), fmt.Sprintf("%s..%s", opName, steps[prefix-1].name))
// Kill here: nothing else runs.
onQuiescent(run.clone(), append(append(trace{}, stepTrace...), "KILL"))
// Error return here: the rollback runs, unless the op has committed;
// the rollback itself can be killed at each of its step boundaries.
if withRollback && prefix <= commitIndex {
rb := rollbackSteps()
for rbPrefix := 0; rbPrefix <= len(rb); rbPrefix++ {
rbRun := run.clone()
for i := 0; i < rbPrefix; i++ {
rb[i].apply(&rbRun)
if err := checkInvariants(rbRun); err != nil {
t.Fatalf("%s rollback after %s: %v\ntrace: %v", opName, rb[i].name, err, stepTrace)
}
}
suffix := "FAIL+rollback-killed"
if rbPrefix == len(rb) {
suffix = "FAIL+rollback-complete"
}
onQuiescent(rbRun.clone(), append(append(trace{}, stepTrace...), fmt.Sprintf("%s@%d", suffix, rbPrefix)))
}
}
}
}
// TestECLifecycleModelExhaustive enumerates every crash and failure schedule
// of encode and decode up to two chained interrupted operations, checks the
// safety invariants in every intermediate state, and requires the prescribed
// recovery to converge from every quiescent state.
func TestECLifecycleModelExhaustive(t *testing.T) {
start := modelState{replica: true}
visited := map[string]trace{}
quiescent := 0
schedules := 0
var enqueue func(s modelState, depth int, tr trace)
checkRecovery := func(s modelState, tr trace) {
schedules++
if _, seen := visited["Q"+s.key()]; seen {
return
}
visited["Q"+s.key()] = tr
quiescent++
if _, err := recoverModel(s.clone()); err != nil {
t.Fatalf("recovery does not converge: %v\nstate: %s\ntrace: %v", err, s.key(), tr)
}
}
enqueue = func(s modelState, depth int, tr trace) {
checkRecovery(s, tr)
if depth == 0 {
return
}
// A crash-restart may happen in any quiescent state before the next
// operation; explore both with and without it.
restarted := s.clone()
reconcile(&restarted)
if err := checkInvariants(restarted); err != nil {
t.Fatalf("reconcile violated invariants: %v\nstate: %s\ntrace: %v", err, s.key(), tr)
}
for _, variant := range []struct {
st modelState
tag string
}{{s, ""}, {restarted, "+restart"}} {
st, tag := variant.st, variant.tag
if st.replica {
exploreOp(t, st, "encode"+tag, encodeSteps(), encodeCommitIndex(), true, tr, func(q modelState, qtr trace) {
enqueue(q, depth-1, qtr)
})
}
canDecode := false
for _, g := range st.gens {
if g.mounted && g.files >= modelDataShards {
canDecode = true
}
}
if canDecode {
exploreOp(t, st, "decode"+tag, decodeSteps(), len(decodeSteps()), false, tr, func(q modelState, qtr trace) {
enqueue(q, depth-1, qtr)
})
}
}
}
// Depth 2: an interrupted operation followed by another interrupted
// operation, recovery checked at every quiescent point in between and
// after. Deeper chains only revisit already-explored states (the state
// space is finite and memoized), which the visited count makes apparent.
enqueue(start, 2, trace{"start"})
// Not spec constants — collapse guards: if a refactor of the model or the
// explorer accidentally prunes schedules, these counts crater and this
// catches it. Update deliberately when the model changes fidelity.
if quiescent < 15 || schedules < 500 {
t.Fatalf("suspiciously small exploration: %d distinct quiescent states from %d schedules", quiescent, schedules)
}
keys := make([]string, 0, len(visited))
for k := range visited {
keys = append(keys, k)
}
sort.Strings(keys)
t.Logf("explored %d schedules reaching %d distinct quiescent states, all recoverable", schedules, quiescent)
}