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
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:
520
weed/ec/ec_lifecycle_model_test.go
Normal file
520
weed/ec/ec_lifecycle_model_test.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user