mirror of
https://github.com/hanzoai/s3.git
synced 2026-11-02 10:33:22 +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.
209 lines
6.0 KiB
Go
209 lines
6.0 KiB
Go
package lock_manager
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestHashRing_BasicOperations(t *testing.T) {
|
|
hr := NewHashRing(50)
|
|
|
|
// Empty ring
|
|
p, b := hr.GetPrimaryAndBackup("key1")
|
|
assert.Equal(t, pb.ServerAddress(""), p)
|
|
assert.Equal(t, pb.ServerAddress(""), b)
|
|
|
|
// Single server — no backup
|
|
hr.AddServer("filer1:8888")
|
|
p, b = hr.GetPrimaryAndBackup("key1")
|
|
assert.Equal(t, pb.ServerAddress("filer1:8888"), p)
|
|
assert.Equal(t, pb.ServerAddress(""), b)
|
|
|
|
// Two servers — backup is the other server
|
|
hr.AddServer("filer2:8888")
|
|
p, b = hr.GetPrimaryAndBackup("key1")
|
|
assert.NotEqual(t, p, b)
|
|
assert.NotEmpty(t, b)
|
|
|
|
// Three servers
|
|
hr.AddServer("filer3:8888")
|
|
p, b = hr.GetPrimaryAndBackup("key1")
|
|
assert.NotEqual(t, p, b)
|
|
assert.NotEmpty(t, b)
|
|
|
|
// Remove server
|
|
hr.RemoveServer("filer2:8888")
|
|
assert.Equal(t, 2, hr.ServerCount())
|
|
}
|
|
|
|
func TestHashRing_DuplicateAddRemove(t *testing.T) {
|
|
hr := NewHashRing(50)
|
|
|
|
hr.AddServer("filer1:8888")
|
|
hr.AddServer("filer1:8888") // duplicate
|
|
assert.Equal(t, 1, hr.ServerCount())
|
|
|
|
hr.RemoveServer("filer1:8888")
|
|
assert.Equal(t, 0, hr.ServerCount())
|
|
|
|
hr.RemoveServer("filer1:8888") // remove non-existent
|
|
assert.Equal(t, 0, hr.ServerCount())
|
|
}
|
|
|
|
func TestHashRing_SetServers(t *testing.T) {
|
|
hr := NewHashRing(50)
|
|
|
|
hr.SetServers([]pb.ServerAddress{"a:1", "b:2", "c:3"})
|
|
assert.Equal(t, 3, hr.ServerCount())
|
|
|
|
servers := hr.GetServers()
|
|
assert.Equal(t, 3, len(servers))
|
|
|
|
// SetServers replaces
|
|
hr.SetServers([]pb.ServerAddress{"x:1", "y:2"})
|
|
assert.Equal(t, 2, hr.ServerCount())
|
|
}
|
|
|
|
func TestHashRing_ConsistencyOnRemoval(t *testing.T) {
|
|
// The key property of consistent hashing: when a server is removed,
|
|
// only keys that mapped to the removed server change.
|
|
hr := NewHashRing(50)
|
|
servers := []pb.ServerAddress{"filer1:8888", "filer2:8888", "filer3:8888"}
|
|
hr.SetServers(servers)
|
|
|
|
numKeys := 1000
|
|
// Record where each key maps before removal
|
|
before := make(map[string]pb.ServerAddress, numKeys)
|
|
for i := 0; i < numKeys; i++ {
|
|
key := fmt.Sprintf("lock-key-%d", i)
|
|
before[key] = hr.GetPrimary(key)
|
|
}
|
|
|
|
// Remove filer2
|
|
hr.RemoveServer("filer2:8888")
|
|
|
|
moved := 0
|
|
for i := 0; i < numKeys; i++ {
|
|
key := fmt.Sprintf("lock-key-%d", i)
|
|
after := hr.GetPrimary(key)
|
|
if before[key] != after {
|
|
// Only keys from filer2 should move
|
|
assert.Equal(t, pb.ServerAddress("filer2:8888"), before[key],
|
|
"key %s moved from %s to %s, but it wasn't on the removed server", key, before[key], after)
|
|
moved++
|
|
}
|
|
}
|
|
// Roughly 1/3 of keys should move (those that were on filer2)
|
|
t.Logf("Keys that moved: %d / %d", moved, numKeys)
|
|
assert.Greater(t, moved, 0, "some keys should have moved")
|
|
assert.Less(t, moved, numKeys, "not all keys should move")
|
|
}
|
|
|
|
func TestHashRing_BackupIsSuccessor(t *testing.T) {
|
|
// After removing primary, the backup should become the new primary
|
|
hr := NewHashRing(50)
|
|
servers := []pb.ServerAddress{"filer1:8888", "filer2:8888", "filer3:8888"}
|
|
hr.SetServers(servers)
|
|
|
|
// For each key, verify that removing the primary makes the backup the new primary
|
|
promoted := 0
|
|
total := 500
|
|
for i := 0; i < total; i++ {
|
|
key := fmt.Sprintf("test-lock-%d", i)
|
|
primary, backup := hr.GetPrimaryAndBackup(key)
|
|
assert.NotEqual(t, primary, backup)
|
|
|
|
// Temporarily remove primary
|
|
hr.RemoveServer(primary)
|
|
newPrimary := hr.GetPrimary(key)
|
|
if newPrimary == backup {
|
|
promoted++
|
|
}
|
|
// Restore
|
|
hr.AddServer(primary)
|
|
}
|
|
// The backup should become new primary for all keys
|
|
assert.Equal(t, total, promoted,
|
|
"backup should become new primary for all keys when primary is removed")
|
|
}
|
|
|
|
func TestHashRing_Distribution(t *testing.T) {
|
|
hr := NewHashRing(50)
|
|
servers := []pb.ServerAddress{"filer1:8888", "filer2:8888", "filer3:8888"}
|
|
hr.SetServers(servers)
|
|
|
|
counts := make(map[pb.ServerAddress]int)
|
|
numKeys := 3000
|
|
for i := 0; i < numKeys; i++ {
|
|
key := fmt.Sprintf("dist-key-%d", i)
|
|
p := hr.GetPrimary(key)
|
|
counts[p]++
|
|
}
|
|
|
|
expected := float64(numKeys) / float64(len(servers))
|
|
for server, count := range counts {
|
|
deviation := math.Abs(float64(count)-expected) / expected
|
|
t.Logf("Server %s: %d keys (%.1f%% deviation)", server, count, deviation*100)
|
|
// Allow up to 40% deviation with 50 vnodes and 3 servers
|
|
assert.Less(t, deviation, 0.40,
|
|
"server %s has too many or too few keys: %d (expected ~%d)", server, count, int(expected))
|
|
}
|
|
}
|
|
|
|
func TestHashRing_GetPrimary(t *testing.T) {
|
|
hr := NewHashRing(50)
|
|
|
|
// Empty ring
|
|
assert.Equal(t, pb.ServerAddress(""), hr.GetPrimary("key"))
|
|
|
|
hr.SetServers([]pb.ServerAddress{"a:1", "b:2"})
|
|
|
|
// Deterministic: same key always maps to same server
|
|
p1 := hr.GetPrimary("mykey")
|
|
p2 := hr.GetPrimary("mykey")
|
|
assert.Equal(t, p1, p2)
|
|
|
|
// GetPrimary matches the primary from GetPrimaryAndBackup
|
|
primary, _ := hr.GetPrimaryAndBackup("mykey")
|
|
assert.Equal(t, primary, hr.GetPrimary("mykey"))
|
|
}
|
|
|
|
// The ring must be identical on every node holding the same server set,
|
|
// regardless of the order servers were added or supplied. Build rings several
|
|
// ways and assert they agree on the primary for a wide range of keys.
|
|
func TestHashRing_OrderIndependent(t *testing.T) {
|
|
servers := []pb.ServerAddress{
|
|
"filer-a:8888", "filer-b:8888", "filer-c:8888", "filer-d:8888", "filer-e:8888",
|
|
}
|
|
|
|
bySet := NewHashRing(50)
|
|
bySet.SetServers(servers)
|
|
|
|
byReverse := NewHashRing(50)
|
|
rev := append([]pb.ServerAddress(nil), servers...)
|
|
for i, j := 0, len(rev)-1; i < j; i, j = i+1, j-1 {
|
|
rev[i], rev[j] = rev[j], rev[i]
|
|
}
|
|
byReverse.SetServers(rev)
|
|
|
|
byAdd := NewHashRing(50)
|
|
for _, s := range []pb.ServerAddress{"filer-c:8888", "filer-e:8888", "filer-a:8888", "filer-d:8888", "filer-b:8888"} {
|
|
byAdd.AddServer(s)
|
|
}
|
|
|
|
for i := 0; i < 5000; i++ {
|
|
key := fmt.Sprintf("s3.object.write:/buckets/b/obj-%d", i)
|
|
p := bySet.GetPrimary(key)
|
|
if got := byReverse.GetPrimary(key); got != p {
|
|
t.Fatalf("reverse-order ring disagrees on %q: %s vs %s", key, got, p)
|
|
}
|
|
if got := byAdd.GetPrimary(key); got != p {
|
|
t.Fatalf("add-order ring disagrees on %q: %s vs %s", key, got, p)
|
|
}
|
|
}
|
|
}
|