Files
s3/weed/storage/volume_vacuum_crash_safe_test.go
zooqueen dd74d3e6d6 Replace AGPL MinIO fork with Apache-2.0 SeaweedFS fork; rebrand CLI weed->s3
hanzoai/s3 was an AGPL-3.0 MinIO fork whose network-copyleft blocks embedding
and reselling as a managed service. Replace it wholesale with a fork of
SeaweedFS (github.com/seaweedfs/seaweedfs, Apache-2.0, Go) which ships a
built-in S3 gateway + Filer.

- License: AGPL-3.0 -> Apache-2.0 (upstream LICENSE kept verbatim, incl.
  "Copyright 2025 Chris Lu"); NOTICE added crediting SeaweedFS. No AGPL/BUSL/
  SSPL/GPL or enterprise ("ee") files remain.
- Module: go.mod path set to github.com/hanzoai/s3 with a self-replace
  (replace github.com/seaweedfs/seaweedfs => ./) so the ~1800 internal Go files
  keep their upstream import paths with zero churn. Internal identifiers and
  import paths are intentionally NOT renamed.
- CLI rebrand: the binary is built as `s3` (was `weed`); root usage, per-command
  help/usage/flag strings, shell autocomplete program name, and the product name
  in CLI output now read "Hanzo S3" / `s3`. Users never see `weed`.
  (e.g. `s3 server`, `s3 filer`, `s3 master`, `s3 volume`, `s3 s3`, `s3 mount`.)
- Build: `make` (or `go build -o s3 ./weed`) builds the `s3` binary green;
  `cmd/weed-db` and `cmd/weed-sql` also build; `go vet ./weed/command/` clean.
- README/LLM.md rewritten for Hanzo S3; AGENTS.md/CLAUDE.md symlinks preserved.
- Dropped a 33MB prebuilt Mach-O `stress` binary upstream had committed; `.gitignore`
  updated to ignore the `s3` build output.
2026-06-18 23:41:25 -07:00

343 lines
11 KiB
Go

package storage
import (
"os"
"path/filepath"
"testing"
"github.com/seaweedfs/seaweedfs/weed/stats"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
"github.com/seaweedfs/seaweedfs/weed/util"
)
// fileExists is a tiny test helper for asserting file presence.
func mustNotExist(t *testing.T, path string) {
t.Helper()
if _, err := os.Stat(path); !os.IsNotExist(err) {
t.Fatalf("expected %s to be absent, got err=%v", path, err)
}
}
func mustExist(t *testing.T, path string) {
t.Helper()
if _, err := os.Stat(path); err != nil {
t.Fatalf("expected %s to exist: %v", path, err)
}
}
// A crash after the .idx was already renamed away but before the .cpx->.idx
// rename committed leaves only .cpd + .cpx + .cpc on disk. The directory
// pre-pass must roll the swap FORWARD: finish the renames and produce a
// loadable, writable volume holding the compacted needle count. Before the
// fix there was no marker and the loader simply discarded the orphan temp
// files, losing the entire compacted generation.
func TestReconcileRollForwardMarkerOnly(t *testing.T) {
dir := t.TempDir()
v, err := NewVolume(dir, dir, "", 1, NeedleMapLevelDb, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0)
if err != nil {
t.Fatalf("volume creation: %v", err)
}
const liveCount = 6
for i := uint64(1); i <= liveCount; i++ {
if _, _, _, err := v.writeNeedle2(newRandomNeedle(i), true, false); err != nil {
t.Fatalf("write %d: %v", i, err)
}
}
// Delete two so the compacted generation is strictly smaller than the source.
for _, id := range []uint64{2, 5} {
if _, err := v.deleteNeedle2(newEmptyNeedle(id)); err != nil {
t.Fatalf("delete %d: %v", id, err)
}
}
expectedLive := uint64(liveCount - 2)
// Generate a valid .cpd/.cpx pair, then close the volume.
if err := v.CompactByIndex(nil); err != nil {
t.Fatalf("compact by index: %v", err)
}
v.Close()
cpd := filepath.Join(dir, "1.cpd")
cpx := filepath.Join(dir, "1.cpx")
cpc := filepath.Join(dir, "1.cpc")
datPath := filepath.Join(dir, "1.dat")
idxPath := filepath.Join(dir, "1.idx")
mustExist(t, cpd)
mustExist(t, cpx)
// Simulate the mid-commit crash: the original .dat/.idx are gone and only
// the compacted temp files plus the commit marker survive.
if err := os.Remove(datPath); err != nil {
t.Fatalf("remove dat: %v", err)
}
if err := os.Remove(idxPath); err != nil {
t.Fatalf("remove idx: %v", err)
}
os.RemoveAll(filepath.Join(dir, "1.ldb"))
if err := os.WriteFile(cpc, nil, 0644); err != nil {
t.Fatalf("write marker: %v", err)
}
// The directory pre-pass should roll the commit forward.
location := NewDiskLocation(dir, 10, util.MinFreeSpace{}, dir, "", nil, stats.DefaultDiskIOProbeConfig())
defer location.Close()
location.loadExistingVolumes(NeedleMapLevelDb, 0)
mustNotExist(t, cpd)
mustNotExist(t, cpx)
mustNotExist(t, cpc)
mustExist(t, datPath)
mustExist(t, idxPath)
reloaded, found := location.FindVolume(needle.VolumeId(1))
if !found {
t.Fatalf("volume not loaded after roll-forward")
}
if reloaded.IsReadOnly() {
t.Fatalf("rolled-forward volume is read-only")
}
if got := reloaded.FileCount(); got != expectedLive {
t.Fatalf("rolled-forward volume has %d needles, want compacted count %d", got, expectedLive)
}
}
// A crash AFTER .cpd->.dat but BEFORE .cpx->.idx leaves a compacted .dat, a
// stale .idx, and only .cpx + .cpc. The roll-forward must finish the pending
// .cpx->.idx rename, not abandon it -- abandoning pairs the fresh .dat with the
// stale .idx, which is index corruption.
func TestReconcileRollForwardPartialRename(t *testing.T) {
dir := t.TempDir()
v, err := NewVolume(dir, dir, "", 1, NeedleMapLevelDb, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0)
if err != nil {
t.Fatalf("volume creation: %v", err)
}
const liveCount = 6
for i := uint64(1); i <= liveCount; i++ {
if _, _, _, err := v.writeNeedle2(newRandomNeedle(i), true, false); err != nil {
t.Fatalf("write %d: %v", i, err)
}
}
for _, id := range []uint64{2, 5} {
if _, err := v.deleteNeedle2(newEmptyNeedle(id)); err != nil {
t.Fatalf("delete %d: %v", id, err)
}
}
expectedLive := uint64(liveCount - 2)
if err := v.CompactByIndex(nil); err != nil {
t.Fatalf("compact by index: %v", err)
}
v.Close()
cpd := filepath.Join(dir, "1.cpd")
cpx := filepath.Join(dir, "1.cpx")
cpc := filepath.Join(dir, "1.cpc")
datPath := filepath.Join(dir, "1.dat")
idxPath := filepath.Join(dir, "1.idx")
// Crash after the .dat rename committed but before the .idx one: .cpd has
// become .dat, the stale pre-compact .idx survives, .cpx and the marker remain.
os.RemoveAll(filepath.Join(dir, "1.ldb"))
if err := os.Remove(datPath); err != nil {
t.Fatalf("remove stale dat: %v", err)
}
if err := os.Rename(cpd, datPath); err != nil {
t.Fatalf("rename cpd->dat: %v", err)
}
if err := os.WriteFile(cpc, nil, 0644); err != nil {
t.Fatalf("write marker: %v", err)
}
mustNotExist(t, cpd)
mustExist(t, cpx)
mustExist(t, idxPath)
location := NewDiskLocation(dir, 10, util.MinFreeSpace{}, dir, "", nil, stats.DefaultDiskIOProbeConfig())
defer location.Close()
location.loadExistingVolumes(NeedleMapLevelDb, 0)
mustNotExist(t, cpx)
mustNotExist(t, cpc)
mustExist(t, datPath)
mustExist(t, idxPath)
reloaded, found := location.FindVolume(needle.VolumeId(1))
if !found {
t.Fatalf("volume not loaded after partial roll-forward")
}
if got := reloaded.FileCount(); got != expectedLive {
t.Fatalf("volume has %d needles, want compacted count %d (pending .cpx->.idx was not rolled forward)", got, expectedLive)
}
}
// With no .cpc marker, leftover .cpd/.cpx are an abandoned generation. The
// pre-pass must roll BACK by deleting them, leaving the live .dat/.idx intact.
func TestReconcileRollBackNoMarker(t *testing.T) {
dir := t.TempDir()
v, err := NewVolume(dir, dir, "", 1, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0)
if err != nil {
t.Fatalf("volume creation: %v", err)
}
for i := uint64(1); i <= 4; i++ {
if _, _, _, err := v.writeNeedle2(newRandomNeedle(i), true, false); err != nil {
t.Fatalf("write %d: %v", i, err)
}
}
if err := v.CompactByIndex(nil); err != nil {
t.Fatalf("compact by index: %v", err)
}
v.Close()
cpd := filepath.Join(dir, "1.cpd")
cpx := filepath.Join(dir, "1.cpx")
datPath := filepath.Join(dir, "1.dat")
idxPath := filepath.Join(dir, "1.idx")
mustExist(t, cpd)
mustExist(t, cpx)
// No .cpc marker exists.
location := NewDiskLocation(dir, 10, util.MinFreeSpace{}, dir, "", nil, stats.DefaultDiskIOProbeConfig())
defer location.Close()
location.loadExistingVolumes(NeedleMapInMemory, 0)
mustNotExist(t, cpd)
mustNotExist(t, cpx)
mustExist(t, datPath)
mustExist(t, idxPath)
reloaded, found := location.FindVolume(needle.VolumeId(1))
if !found {
t.Fatalf("volume not loaded after roll-back")
}
if got := reloaded.FileCount(); got != 4 {
t.Fatalf("rolled-back volume has %d needles, want original 4", got)
}
}
// A runtime reload (SIGHUP -> LoadNewVolumes -> reconcileCompactStates) must
// NOT roll back an in-flight vacuum of an already-loaded volume: its .cpd/.cpx
// are live, not crash leftovers. The pre-pass only reconciles vids not loaded.
func TestReconcileSkipsLoadedVolumeMidVacuum(t *testing.T) {
dir := t.TempDir()
v, err := NewVolume(dir, dir, "", 1, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0)
if err != nil {
t.Fatalf("volume creation: %v", err)
}
for i := uint64(1); i <= 4; i++ {
if _, _, _, err := v.writeNeedle2(newRandomNeedle(i), true, false); err != nil {
t.Fatalf("write %d: %v", i, err)
}
}
v.Close()
// Load the volume as a running server would (populates l.volumes).
location := NewDiskLocation(dir, 10, util.MinFreeSpace{}, dir, "", nil, stats.DefaultDiskIOProbeConfig())
defer location.Close()
location.loadExistingVolumes(NeedleMapInMemory, 0)
loaded, found := location.FindVolume(needle.VolumeId(1))
if !found {
t.Fatalf("volume not loaded")
}
// In-flight vacuum: .cpd/.cpx written, commit (.cpc) not yet reached.
if err := loaded.CompactByIndex(nil); err != nil {
t.Fatalf("compact by index: %v", err)
}
cpd := filepath.Join(dir, "1.cpd")
cpx := filepath.Join(dir, "1.cpx")
mustExist(t, cpd)
mustExist(t, cpx)
// The reload pre-pass must leave the loaded volume's in-flight temp files alone.
location.reconcileCompactStates()
mustExist(t, cpd)
mustExist(t, cpx)
}
// applyCompactSwap must abort without touching the live .dat/.idx when the
// .cpd/.cpx temp files are missing (a stale or duplicate commit). Before the
// fix a late commit on Windows would RemoveAll the .dat/.idx first and then
// fail the rename, destroying the volume.
func TestApplyCompactSwapMissingTempFilesPreservesLive(t *testing.T) {
dir := t.TempDir()
v, err := NewVolume(dir, dir, "", 1, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0)
if err != nil {
t.Fatalf("volume creation: %v", err)
}
for i := uint64(1); i <= 3; i++ {
if _, _, _, err := v.writeNeedle2(newRandomNeedle(i), true, false); err != nil {
t.Fatalf("write %d: %v", i, err)
}
}
v.Close()
datPath := filepath.Join(dir, "1.dat")
idxPath := filepath.Join(dir, "1.idx")
mustExist(t, datPath)
mustExist(t, idxPath)
datBefore, _ := os.Stat(datPath)
// No .cpd/.cpx present: the swap must refuse.
if err := v.applyCompactSwap(); err == nil {
t.Fatalf("applyCompactSwap should error when .cpd/.cpx are missing")
}
mustExist(t, datPath)
mustExist(t, idxPath)
if datAfter, _ := os.Stat(datPath); datAfter.Size() != datBefore.Size() {
t.Fatalf("live .dat changed size: before=%d after=%d", datBefore.Size(), datAfter.Size())
}
}
// Deleting a volume and recreating the same id must not leave a stale .cpc
// behind for the next load to roll forward against. removeVolumeFiles must
// sweep the marker along with the rest.
func TestDestroyRemovesCommitMarker(t *testing.T) {
dir := t.TempDir()
v, err := NewVolume(dir, dir, "", 1, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0)
if err != nil {
t.Fatalf("volume creation: %v", err)
}
for i := uint64(1); i <= 3; i++ {
if _, _, _, err := v.writeNeedle2(newRandomNeedle(i), true, false); err != nil {
t.Fatalf("write %d: %v", i, err)
}
}
// Strand a commit marker and its temp files as a crashed commit would.
cpc := filepath.Join(dir, "1.cpc")
cpd := filepath.Join(dir, "1.cpd")
cpx := filepath.Join(dir, "1.cpx")
for _, p := range []string{cpc, cpd, cpx} {
if err := os.WriteFile(p, []byte("x"), 0644); err != nil {
t.Fatalf("write %s: %v", p, err)
}
}
if err := v.Destroy(false, false); err != nil {
t.Fatalf("destroy: %v", err)
}
mustNotExist(t, cpc)
mustNotExist(t, cpd)
mustNotExist(t, cpx)
// Recreate the same id and load: there must be no marker to roll forward.
v2, err := NewVolume(dir, dir, "", 1, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0)
if err != nil {
t.Fatalf("recreate volume: %v", err)
}
defer v2.Close()
location := NewDiskLocation(dir, 10, util.MinFreeSpace{}, dir, "", nil, stats.DefaultDiskIOProbeConfig())
defer location.Close()
location.reconcileCompactStates()
mustNotExist(t, cpc)
}