mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-11 00:50:43 +02:00
* s3: dial the object lock's primary filer directly The S3 object write lock builds a fresh short-lived lock per write, each starting at the seed filer. When the seed isn't the key's hash-ring primary the filer forwards the request to the primary, and in multi-cluster setups that forward crosses clusters on every write. Give the lock client a view of the filer lock ring, fed by the master's LockRingUpdate broadcasts the gateway already receives, so it dials the primary directly. The view tracks filer membership by version; a stale view stays correct because the filer still forwards as a fallback. Also send the initial ring snapshot to S3 clients, not just filers. * s3: subscribe to lock-ring updates before starting the master loop The master delivers the initial LockRingUpdate once, on connect. Registering the callback after KeepConnectedToMaster started left a window where that first update could arrive before the handler was set and be dropped, delaying the ring view until the next membership change. Build the lock client and register the callback in the masters block before launching the loop; the filers block reuses that client (or creates a plain one when no masters are configured). * lock_manager: build the hash ring in a deterministic server order rebuildRing ranged over the server set (a map), whose iteration order is randomized per process. On a vnode hash collision the last writer into vnodeToServer wins, so two nodes holding the same server set could resolve the collision to different servers and disagree on the primary for keys near that slot. Now that the S3 gateway also computes PrimaryForKey, such a disagreement would route the same key to different filers and defeat per-path serialization. Iterate the servers in sorted order so the ring is identical on every node with the same set, regardless of discovery order. * lock_manager: skip redundant ring rebuilds, trim comments SetRing now ignores a non-zero version at or below the current one once a ring exists, so repeated LockRingUpdate broadcasts on reconnect no longer rebuild the ring. * s3: hold the lock-ring client on the server for route-by-key Store the object-write lock client on S3ApiServer so handlers can resolve a key's owner filer via PrimaryForKey.
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)
|
|
}
|
|
}
|
|
}
|