Files
upstream-seaweedfs/weed/storage/store_ec_multidisk_lifecycle_test.go
upstream-mirror 89f5be005d
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
snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 03:23:42 +05:00

315 lines
11 KiB
Go

package storage
import (
"os"
"path/filepath"
"reflect"
"sort"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
"github.com/seaweedfs/seaweedfs/weed/stats"
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
"github.com/seaweedfs/seaweedfs/weed/storage/volume_info"
"github.com/seaweedfs/seaweedfs/weed/util"
)
// These tests walk one EC volume through the life a multi-disk production node
// gives it: shards spread across disks with the .ecx / .ecj / .vif sidecars on
// only one of them, then a balance moving the sidecar disk's shards away, then
// a restart, then a fresh shard arriving. Each transition is exercised against
// the real Store so the invariant under test is the one production depends on:
// every shard file on disk is either registered (visible to heartbeat and
// reads) or still on disk untouched — never silently dropped, never deleted.
//
// The layout mirrors a support case: a 10+4 volume on a node with a dozen
// disks, sidecars on the disk that ran the encode, shards scattered by
// ec.balance. The volume id and collection are kept from the case.
const (
lifecycleCollection = "pm-itatiaiucu-01"
lifecycleVolumeId = needle.VolumeId(42561)
lifecycleDatSize = int64(10 * 1024 * 1024)
lifecycleData = 10
lifecycleParity = 4
)
// plantLifecycleShard creates a shard file of the exact size a real encode of
// lifecycleDatSize would produce, so size-based validation sees a consistent
// volume rather than an artifact of the fixture.
func plantLifecycleShard(t *testing.T, dir string, shardId int) {
t.Helper()
base := erasure_coding.EcShardFileName(lifecycleCollection, dir, int(lifecycleVolumeId))
f, err := os.Create(base + erasure_coding.ToExt(shardId))
if err != nil {
t.Fatalf("create shard %d in %s: %v", shardId, dir, err)
}
defer f.Close()
if err := f.Truncate(calculateExpectedShardSize(lifecycleDatSize, lifecycleData)); err != nil {
t.Fatalf("size shard %d: %v", shardId, err)
}
}
// plantLifecycleSidecars writes the .ecx / .ecj / .vif set on one disk. No
// .dat is planted: on a production EC node the .dat is gone after encode, and
// its absence is what marks the volume as distributed rather than as an
// interrupted local encode.
func plantLifecycleSidecars(t *testing.T, dir string) {
t.Helper()
base := erasure_coding.EcShardFileName(lifecycleCollection, dir, int(lifecycleVolumeId))
if err := os.WriteFile(base+".ecx", make([]byte, 20), 0o644); err != nil {
t.Fatalf("write .ecx: %v", err)
}
if err := os.WriteFile(base+".ecj", nil, 0o644); err != nil {
t.Fatalf("write .ecj: %v", err)
}
if err := volume_info.SaveVolumeInfo(base+".vif", &volume_server_pb.VolumeInfo{
Version: uint32(needle.Version3),
DatFileSize: lifecycleDatSize,
EcShardConfig: &volume_server_pb.EcShardConfig{
DataShards: lifecycleData,
ParityShards: lifecycleParity,
},
}); err != nil {
t.Fatalf("save .vif: %v", err)
}
}
// newLifecycleStore starts a Store over dirs and returns it with a closer that
// also stops the notification drainer, so a test can restart the store over
// the same directories.
func newLifecycleStore(t *testing.T, dirs []string) (*Store, func()) {
t.Helper()
maxCounts := make([]int32, len(dirs))
freeSpaces := make([]util.MinFreeSpace, len(dirs))
diskTypes := make([]types.DiskType, len(dirs))
for i := range dirs {
maxCounts[i] = 100
diskTypes[i] = types.HardDriveType
}
store := NewStore(nil, "localhost", 8080, 18080, "http://localhost:8080", "store-id",
dirs, maxCounts, freeSpaces, "", NeedleMapInMemory, diskTypes, nil, 3,
stats.DefaultDiskIOProbeConfig())
done := make(chan struct{})
go func() {
for {
select {
case <-store.NewEcShardsChan:
case <-store.NewVolumesChan:
case <-store.DeletedVolumesChan:
case <-store.DeletedEcShardsChan:
case <-store.StateUpdateChan:
case <-done:
return
}
}
}()
return store, func() {
store.Close()
close(done)
}
}
// registeredShards reports which shard ids are registered on each disk, in
// the order of store.Locations — the store's answer to "what would heartbeat
// report", independent of what sits on disk.
func registeredShards(store *Store) map[int][]int {
byDisk := make(map[int][]int)
for diskId, loc := range store.Locations {
if ecv, ok := loc.FindEcVolume(lifecycleVolumeId); ok {
for _, sid := range ecv.ShardIdList() {
byDisk[diskId] = append(byDisk[diskId], int(sid))
}
}
}
return byDisk
}
// assertRegistered fails unless the registered shard set matches want exactly,
// disk by disk. Counts alone can pass while a reconcile associates a shard with
// the wrong disk or loads a different shard than the file on disk; comparing the
// actual ids per disk is what catches that. want maps disk index -> shard ids;
// a disk absent from want must have no registered shards.
func assertRegistered(t *testing.T, store *Store, phase string, want map[int][]int) {
t.Helper()
norm := func(m map[int][]int) map[int][]int {
out := map[int][]int{}
for d, ids := range m {
if len(ids) == 0 {
continue
}
cp := append([]int(nil), ids...)
sort.Ints(cp)
out[d] = cp
}
return out
}
got := norm(registeredShards(store))
exp := norm(want)
if !reflect.DeepEqual(got, exp) {
t.Fatalf("%s: registered shard set = %v, want %v; "+
"a shard on the wrong disk or the wrong id loaded keeps the counts right while corrupting the mapping",
phase, got, exp)
}
}
func countRegistered(store *Store) int {
n := 0
for _, ids := range registeredShards(store) {
n += len(ids)
}
return n
}
// shardFilesOnDisk counts this volume's shard files under dir, the ground
// truth the registration view must never silently diverge from.
func shardFilesOnDisk(t *testing.T, dir string) int {
t.Helper()
base := erasure_coding.EcShardFileName(lifecycleCollection, dir, int(lifecycleVolumeId))
n := 0
for i := 0; i < erasure_coding.MaxShardCount; i++ {
if _, err := os.Stat(base + erasure_coding.ToExt(i)); err == nil {
n++
}
}
return n
}
func TestMultiDiskLifecycle_SpreadLayout(t *testing.T) {
tempDir := t.TempDir()
dirs := []string{
filepath.Join(tempDir, "disk0"),
filepath.Join(tempDir, "disk1"),
filepath.Join(tempDir, "disk2"),
}
for _, d := range dirs {
if err := os.MkdirAll(d, 0o755); err != nil {
t.Fatal(err)
}
}
// The production layout: sidecars live with the first six shards on disk0,
// the rest of the shards sit alone on disks 1 and 2.
plantLifecycleSidecars(t, dirs[0])
for sid := 0; sid <= 5; sid++ {
plantLifecycleShard(t, dirs[0], sid)
}
for sid := 6; sid <= 10; sid++ {
plantLifecycleShard(t, dirs[1], sid)
}
for sid := 11; sid <= 13; sid++ {
plantLifecycleShard(t, dirs[2], sid)
}
// ── Phase A: cold start over the spread layout ──
// Register the first store's closer immediately, reading the closeStore
// variable at defer time so it also covers the second store after the
// restart below. Without this a Fatalf in phase A/B would leak the store
// and its notification-drainer goroutine.
store, closeStore := newLifecycleStore(t, dirs)
defer func() { closeStore() }()
assertRegistered(t, store, "cold start over spread layout", map[int][]int{
0: {0, 1, 2, 3, 4, 5},
1: {6, 7, 8, 9, 10},
2: {11, 12, 13},
})
// ── Phase B: a balance moves the sidecar disk's shards away ──
// Unmount and remove shards 0..5 from disk0. The sidecars must stay: disks
// 1 and 2 still hold eight shards that read and delete through this .ecx.
for sid := 0; sid <= 5; sid++ {
if err := store.UnmountEcShards(lifecycleVolumeId, erasure_coding.ShardId(sid), 0); err != nil {
t.Fatalf("unmount shard %d: %v", sid, err)
}
base := erasure_coding.EcShardFileName(lifecycleCollection, dirs[0], int(lifecycleVolumeId))
if err := os.Remove(base + erasure_coding.ToExt(sid)); err != nil {
t.Fatalf("remove shard %d: %v", sid, err)
}
}
assertRegistered(t, store, "after balancing away the sidecar disk's shards", map[int][]int{
1: {6, 7, 8, 9, 10},
2: {11, 12, 13},
})
// ── Phase C: restart in that state ──
// disk0 now holds only sidecars; every shard is on a disk with no local
// .ecx. This is the steady state a balanced multi-disk node reboots in.
closeStore()
store, closeStore = newLifecycleStore(t, dirs)
assertRegistered(t, store, "restart with sidecars on a shard-less disk", map[int][]int{
1: {6, 7, 8, 9, 10},
2: {11, 12, 13},
})
if n := shardFilesOnDisk(t, dirs[1]) + shardFilesOnDisk(t, dirs[2]); n != 8 {
t.Fatalf("restart changed what is on disk: %d shard files, want 8 — startup must never delete distributed EC shards", n)
}
// ── Phase D: a fresh shard lands on a shard-only disk ──
// A later balance copies shard 0 to disk2. The mount must find the .ecx on
// disk0; failing that, the copy is unusable and the move that sent it
// deletes the source after a copy nothing can read.
plantLifecycleShard(t, dirs[2], 0)
if err := store.MountEcShards(lifecycleCollection, lifecycleVolumeId, 0, ""); err != nil {
t.Fatalf("mounting a shard delivered to a disk without local sidecars: %v", err)
}
// Shard 0 now lives on disk2 alongside 11/12/13; the mapping, not just the
// count, must reflect that.
assertRegistered(t, store, "cross-disk mount of a fresh shard", map[int][]int{
1: {6, 7, 8, 9, 10},
2: {0, 11, 12, 13},
})
}
// A dead disk that held the sidecars must degrade loudly but safely: the other
// disks' shards cannot serve reads without the .ecx, so they may drop out of
// the registered view, but their files must survive so restoring the sidecars
// (or the disk) restores the volume. Losing one disk of fourteen shards' worth
// of sidecars must never cost the other thirteen shards their data.
func TestMultiDiskLifecycle_SidecarDiskLost(t *testing.T) {
tempDir := t.TempDir()
dirs := []string{
filepath.Join(tempDir, "disk0"),
filepath.Join(tempDir, "disk1"),
filepath.Join(tempDir, "disk2"),
}
for _, d := range dirs {
if err := os.MkdirAll(d, 0o755); err != nil {
t.Fatal(err)
}
}
plantLifecycleSidecars(t, dirs[0])
for sid := 0; sid <= 5; sid++ {
plantLifecycleShard(t, dirs[0], sid)
}
for sid := 6; sid <= 10; sid++ {
plantLifecycleShard(t, dirs[1], sid)
}
for sid := 11; sid <= 13; sid++ {
plantLifecycleShard(t, dirs[2], sid)
}
// The node comes back without disk0 — the disk with the sidecars died.
store, closeStore := newLifecycleStore(t, dirs[1:])
defer closeStore()
// Degraded visibility is acceptable and expected; silent file loss is not.
if n := shardFilesOnDisk(t, dirs[1]); n != 5 {
t.Fatalf("disk1 shard files after sidecar-disk loss: %d, want 5 untouched", n)
}
if n := shardFilesOnDisk(t, dirs[2]); n != 3 {
t.Fatalf("disk2 shard files after sidecar-disk loss: %d, want 3 untouched", n)
}
// The sidecar disk is gone, so no shard has a reachable .ecx: none may be
// registered. If a change ever registers shards without usable sidecars,
// this must fail rather than pass silently — a registered-but-unreadable
// shard is worse than an unregistered one, because the master advertises it.
if got := countRegistered(store); got != 0 {
t.Fatalf("registered view without the sidecar disk: %v, want empty "+
"(no shard can load without a reachable .ecx)", registeredShards(store))
}
t.Log("files intact; shards without a reachable .ecx stay unloaded, as required")
}