mirror of
https://github.com/hanzoai/s3.git
synced 2026-11-03 03:07:41 +00:00
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.
343 lines
11 KiB
Go
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)
|
|
}
|