mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-19 04:50:54 +02:00
* volume: start a volume's batch write worker on first use Mounting a volume started a goroutine parked on a 128-slot channel, plus the 128-entry batch slice it had already allocated. That is around 6.7KB per volume the server pays whether or not the volume ever takes a write: 7231 bytes per mounted volume, of which 4101 is goroutine stack. Only a write that asks for fsync ever reaches the worker, and a remote-tiered or read-only volume never can. Create the channel and its goroutine on the first such request instead, and let a write arriving after Destroy fall back to the inline path rather than queue onto a worker that has gone. Measured over 20000 mounted volumes: 7231 -> 1269 bytes each. * volume: update the heartbeat report state in place Every heartbeat built a second map of what it was about to tell the master, holding a freshly allocated short information message per volume, then swapped it in over the old one -- and computed departures through a third map of the live volume ids. A server holding 2M volumes rebuilt all three every VolumePulsePeriod for a report that usually says nothing. Number the heartbeats instead and mark the entry already held with the pass that found the copy, so a quiet volume costs a map lookup and no allocation. Departures are the entries a pass did not mark; the live-id map is now built only when there are some, sized to them. Measured over 10000 mounted volumes: 436 -> 196 bytes allocated per volume per heartbeat. * volume: fill one volume information message per heartbeat, not per volume The heartbeat built a message for every volume held so it could hash it, then dropped all but the few it had something to say about. At 2M volumes that is 2M messages allocated every VolumePulsePeriod to send almost none of them. Fill a message the caller supplies instead, and replace it only when the heartbeat keeps it, so a server with nothing to report fills the same one all the way through. Measured over 10000 mounted volumes: 196 -> 4 bytes allocated per volume per heartbeat, and a heartbeat runs a third faster. * volume: drop the per-volume trace from the heartbeat's status read glog.V(4).Infof evaluates its arguments whether or not the verbosity is on, so every volume boxed its id into a fresh interface slice on every heartbeat: 759 of the 773 allocations a 1000-volume heartbeat made, for a line that at this scale would print millions of unreadable rows. Measured over 1000 mounted volumes: 4776 -> 1792 bytes and 759 -> 14 allocations per heartbeat, which no longer grows with the volume count. * seaweed-volume: mirror the in-place heartbeat report state Same change as the Go volume server: number the heartbeats and mark the entry already held with the pass that found the copy, instead of building a second map of hashes and swapping it in. The volume snapshot must leave the reporting state as it found it, so it keeps asking through changed() while a real heartbeat marks through record(). * volume: refuse writes to a closed volume instead of dereferencing nil Close and Destroy leave the needle map and data backend nil, but a caller that already holds the volume can still reach the write path, where both are used unguarded: a write racing a volume deletion took the server down. syncDelete has always checked; syncWrite and the batch worker had not. Reachable before this series and now also from the inline fallback a durable write takes when the worker has gone. * seaweed-volume: guard the report state with one mutex, as Go does The full-list flag and the generation that answers it have to move together. Split across separate atomics they cannot: a request landing between begin's two reads returns full == false with the generation it just raised, and one landing between commit's read and its clear is marked answered by a heartbeat that carried no list. Either way the resend is dropped. Neither is reachable today -- every caller reaches this through the store's RwLock, the flag setters under a read lock and the heartbeat build under a write lock, so they cannot interleave. The type should not depend on that being true two files away, and Go holds a single mutex over exactly these fields. * test: build the servers under test to match the harness's offset size The mixed Go/Rust suites run both servers against one dataset, so both have to agree on the offset width. They did not: the harness built Go with no tags, 4-byte offsets, while the Rust crate defaults to its 5bytes feature, and the Rust server then refused the .vif the Go server had just written -- "bytes_offset mismatch: found 4, expected 5". Build each side to match the offset size the test binary itself was compiled with, so a plain `go test` and one with -tags 5BytesOffset both get a matched pair.
486 lines
18 KiB
Go
486 lines
18 KiB
Go
package topology
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
"google.golang.org/protobuf/reflect/protoreflect"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/stats"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/types"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
func digestTestNode(t *testing.T) (*Topology, *DataNode) {
|
|
t.Helper()
|
|
topo := NewTopology("digest", nil, 32*1024*1024*1024, 5, false)
|
|
dn := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1").
|
|
GetOrCreateDataNode("127.0.0.1", 8080, 18080, "", "", map[string]uint32{"": 1000})
|
|
return topo, dn
|
|
}
|
|
|
|
func digestTestVolume(id uint32) *master_pb.VolumeInformationMessage {
|
|
return &master_pb.VolumeInformationMessage{
|
|
Id: id,
|
|
Size: 1024 * 1024,
|
|
Collection: "c",
|
|
FileCount: 10,
|
|
DeleteCount: 1,
|
|
DeletedByteCount: 128,
|
|
ReplicaPlacement: 0,
|
|
Version: 3,
|
|
CompactRevision: 1,
|
|
ModifiedAtSecond: 1700000000,
|
|
}
|
|
}
|
|
|
|
func TestVolumeDigestIsStableAcrossRepeatedHeartbeats(t *testing.T) {
|
|
topo, dn := digestTestNode(t)
|
|
volumes := []*master_pb.VolumeInformationMessage{digestTestVolume(1), digestTestVolume(2), digestTestVolume(3)}
|
|
|
|
topo.SyncDataNodeRegistration(volumes, dn)
|
|
first := dn.VolumeDigest()
|
|
if first == 0 {
|
|
t.Fatal("expected a non-zero digest for a node holding volumes")
|
|
}
|
|
for i := 0; i < 3; i++ {
|
|
topo.SyncDataNodeRegistration(volumes, dn)
|
|
if got := dn.VolumeDigest(); got != first {
|
|
t.Fatalf("heartbeat %d changed the digest with no change to report: %d != %d", i, got, first)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestVolumeDigestIsIndependentOfReportOrder(t *testing.T) {
|
|
topoA, dnA := digestTestNode(t)
|
|
topoA.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{
|
|
digestTestVolume(1), digestTestVolume(2), digestTestVolume(3),
|
|
}, dnA)
|
|
|
|
topoB, dnB := digestTestNode(t)
|
|
topoB.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{
|
|
digestTestVolume(3), digestTestVolume(1), digestTestVolume(2),
|
|
}, dnB)
|
|
|
|
if dnA.VolumeDigest() != dnB.VolumeDigest() {
|
|
t.Errorf("digest depends on report order: %d != %d", dnA.VolumeDigest(), dnB.VolumeDigest())
|
|
}
|
|
}
|
|
|
|
// Every field of VolumeInformationMessage has to reach the digest: one the hash
|
|
// skips is a change the master would never be told about. Enumerated from the
|
|
// message rather than listed here, so a field added later cannot quietly fall
|
|
// outside the digest while this still passes.
|
|
// Fields a server reports that the master deliberately does not keep, so a
|
|
// change to one alters nothing it holds and the digest is right not to move.
|
|
var notRetainedByTheMaster = map[string]string{
|
|
"remote_storage_key": "unique per volume, and the master decides nothing from it",
|
|
}
|
|
|
|
func TestVolumeDigestTracksEveryReportedField(t *testing.T) {
|
|
base := digestTestVolume(1)
|
|
baseInfo, err := storage.NewVolumeInfo(base)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
fields := base.ProtoReflect().Descriptor().Fields()
|
|
for i := 0; i < fields.Len(); i++ {
|
|
fd := fields.Get(i)
|
|
t.Run(string(fd.Name()), func(t *testing.T) {
|
|
why, skipped := notRetainedByTheMaster[string(fd.Name())]
|
|
candidates := distinctValuesFor(t, fd, base.ProtoReflect().Get(fd))
|
|
for _, candidate := range candidates {
|
|
changed := proto.Clone(base).(*master_pb.VolumeInformationMessage)
|
|
changed.ProtoReflect().Set(fd, candidate)
|
|
changedInfo, err := storage.NewVolumeInfo(changed)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if baseInfo.ReportHash() != changedInfo.ReportHash() {
|
|
if skipped {
|
|
t.Errorf("%s moves the digest, but is listed as not retained by the master (%s)", fd.Name(), why)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
if skipped {
|
|
return
|
|
}
|
|
t.Errorf("no change to %s moves the digest, so the master would never be told about one", fd.Name())
|
|
})
|
|
}
|
|
}
|
|
|
|
// distinctValuesFor offers values that differ from current. Several, because
|
|
// some fields are narrowed or normalised on the way into VolumeInfo and the
|
|
// smallest change to the wire value can land back on the stored one.
|
|
func distinctValuesFor(t *testing.T, fd protoreflect.FieldDescriptor, current protoreflect.Value) []protoreflect.Value {
|
|
t.Helper()
|
|
switch fd.Kind() {
|
|
case protoreflect.BoolKind:
|
|
return []protoreflect.Value{protoreflect.ValueOfBool(!current.Bool())}
|
|
case protoreflect.Uint32Kind:
|
|
return []protoreflect.Value{
|
|
protoreflect.ValueOfUint32(uint32(current.Uint()) + 1),
|
|
protoreflect.ValueOfUint32(uint32(current.Uint()) + 1<<8),
|
|
protoreflect.ValueOfUint32(uint32(current.Uint()) + 1<<16),
|
|
}
|
|
case protoreflect.Uint64Kind:
|
|
return []protoreflect.Value{protoreflect.ValueOfUint64(current.Uint() + 1)}
|
|
case protoreflect.Int64Kind:
|
|
return []protoreflect.Value{protoreflect.ValueOfInt64(current.Int() + 1)}
|
|
case protoreflect.StringKind:
|
|
return []protoreflect.Value{protoreflect.ValueOfString(current.String() + "x")}
|
|
}
|
|
t.Fatalf("field %s has kind %s, which this test does not know how to vary", fd.Name(), fd.Kind())
|
|
return nil
|
|
}
|
|
|
|
func TestVolumeDigestFollowsVolumeChanges(t *testing.T) {
|
|
topo, dn := digestTestNode(t)
|
|
volumes := []*master_pb.VolumeInformationMessage{digestTestVolume(1), digestTestVolume(2)}
|
|
topo.SyncDataNodeRegistration(volumes, dn)
|
|
original := dn.VolumeDigest()
|
|
|
|
grown := []*master_pb.VolumeInformationMessage{digestTestVolume(1), digestTestVolume(2)}
|
|
grown[1].Size += 4096
|
|
topo.SyncDataNodeRegistration(grown, dn)
|
|
if dn.VolumeDigest() == original {
|
|
t.Error("a volume that grew left the digest unchanged")
|
|
}
|
|
|
|
topo.SyncDataNodeRegistration(volumes, dn)
|
|
if dn.VolumeDigest() != original {
|
|
t.Error("reverting a volume did not restore the digest")
|
|
}
|
|
|
|
topo.SyncDataNodeRegistration(volumes[:1], dn)
|
|
if dn.VolumeDigest() == original {
|
|
t.Error("dropping a volume left the digest unchanged")
|
|
}
|
|
|
|
topo.SyncDataNodeRegistration(volumes, dn)
|
|
if dn.VolumeDigest() != original {
|
|
t.Error("restoring a dropped volume did not restore the digest")
|
|
}
|
|
}
|
|
|
|
func TestVolumeDigestEmptiesWithTheNode(t *testing.T) {
|
|
topo, dn := digestTestNode(t)
|
|
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{
|
|
digestTestVolume(1), digestTestVolume(2),
|
|
}, dn)
|
|
|
|
topo.SyncDataNodeRegistration(nil, dn)
|
|
if got := dn.VolumeDigest(); got != 0 {
|
|
t.Errorf("expected an empty node to digest to 0, got %d", got)
|
|
}
|
|
}
|
|
|
|
func TestVolumeDigestFollowsDeltaRegistration(t *testing.T) {
|
|
topo, dn := digestTestNode(t)
|
|
full := []*master_pb.VolumeInformationMessage{digestTestVolume(1), digestTestVolume(2)}
|
|
topo.SyncDataNodeRegistration(full, dn)
|
|
both := dn.VolumeDigest()
|
|
|
|
topo.IncrementalSyncDataNodeRegistration(nil, []*master_pb.VolumeShortInformationMessage{{Id: 2}}, dn)
|
|
if dn.VolumeDigest() == both {
|
|
t.Error("unmounting a volume left the digest unchanged")
|
|
}
|
|
|
|
topo.SyncDataNodeRegistration(full, dn)
|
|
if dn.VolumeDigest() != both {
|
|
t.Error("a full heartbeat did not restore the digest after an unmount")
|
|
}
|
|
}
|
|
|
|
// The point of the digest is not to detect that volumes changed -- in any live
|
|
// cluster some always have. It is to confirm that after applying the changes a
|
|
// heartbeat did carry, the master holds what the volume server holds. So a
|
|
// heartbeat reporting only the volumes that moved must still reconcile.
|
|
func TestVolumeDigestMatchesAfterApplyingOnlyChangedVolumes(t *testing.T) {
|
|
const total = 50
|
|
full := make([]*master_pb.VolumeInformationMessage, 0, total)
|
|
for i := 1; i <= total; i++ {
|
|
v := digestTestVolume(uint32(i))
|
|
v.ReadOnly = i > 5 // only the first few are writable, as in a tiered cluster
|
|
full = append(full, v)
|
|
}
|
|
|
|
topo, dn := digestTestNode(t)
|
|
topo.SyncDataNodeRegistration(full, dn)
|
|
|
|
// Three writable volumes take writes between two heartbeats.
|
|
changed := make([]storage.VolumeInfo, 0, 3)
|
|
for _, v := range full[:3] {
|
|
v.Size += 4096
|
|
v.FileCount++
|
|
v.ModifiedAtSecond += 5
|
|
vi, err := storage.NewVolumeInfo(v)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
changed = append(changed, vi)
|
|
}
|
|
|
|
// What the volume server would now report for its whole set.
|
|
reference, referenceNode := digestTestNode(t)
|
|
reference.SyncDataNodeRegistration(full, referenceNode)
|
|
want := referenceNode.VolumeDigest()
|
|
|
|
if dn.VolumeDigest() == want {
|
|
t.Fatal("expected the master to be behind before the changes are applied")
|
|
}
|
|
|
|
// The heartbeat carries three volumes, not fifty.
|
|
dn.DeltaUpdateVolumes(changed, nil)
|
|
|
|
if got := dn.VolumeDigest(); got != want {
|
|
t.Errorf("digest still disagrees after applying the reported changes: %d != %d", got, want)
|
|
}
|
|
}
|
|
|
|
// A volume that disappears without a delta is exactly what the full list exists
|
|
// to catch, and is the case the digest has to keep catching.
|
|
func TestVolumeDigestCatchesASilentlyLostVolume(t *testing.T) {
|
|
full := []*master_pb.VolumeInformationMessage{
|
|
digestTestVolume(1), digestTestVolume(2), digestTestVolume(3),
|
|
}
|
|
|
|
topo, dn := digestTestNode(t)
|
|
topo.SyncDataNodeRegistration(full, dn)
|
|
|
|
// The volume server no longer has volume 2 and never got to say so.
|
|
reference, referenceNode := digestTestNode(t)
|
|
reference.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{full[0], full[2]}, referenceNode)
|
|
|
|
if dn.VolumeDigest() == referenceNode.VolumeDigest() {
|
|
t.Error("a volume lost without a delta went undetected, which is what the full list is for")
|
|
}
|
|
}
|
|
|
|
// The disk map and the lookup index are maintained separately, and a disconnect
|
|
// racing a reconnect has been seen to drop a volume from the lookup index while
|
|
// leaving it on the node. The volume server's report is identical either way, so
|
|
// the heartbeat digest cannot see it and the master has to notice on its own.
|
|
func TestVolumeIndexDigestSeesLookupDivergence(t *testing.T) {
|
|
topo, dn := digestTestNode(t)
|
|
v := digestTestVolume(1)
|
|
v.Collection = "drr"
|
|
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{v}, dn)
|
|
|
|
if !dn.HasConsistentVolumeIndex() {
|
|
t.Fatal("a freshly registered node should have a consistent index")
|
|
}
|
|
reported := dn.VolumeDigest()
|
|
|
|
rp, _ := super_block.NewReplicaPlacementFromString("000")
|
|
vl := topo.GetVolumeLayout("drr", rp, needle.EMPTY_TTL, types.HardDriveType)
|
|
vl.SetVolumeUnavailable(dn, needle.VolumeId(1))
|
|
|
|
if got := topo.Lookup("drr", needle.VolumeId(1)); got != nil {
|
|
t.Fatalf("expected the volume to have become unservable, got %v", got)
|
|
}
|
|
if _, err := dn.GetVolumesById(needle.VolumeId(1)); err != nil {
|
|
t.Fatalf("the volume should still be on the node: %v", err)
|
|
}
|
|
if dn.VolumeDigest() != reported {
|
|
t.Error("the reported digest should not move: the volume server sees no change")
|
|
}
|
|
if dn.HasConsistentVolumeIndex() {
|
|
t.Error("a volume held but not servable left the index digests agreeing, so nothing would repair it")
|
|
}
|
|
|
|
// The full heartbeat self-heal puts it back.
|
|
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{v}, dn)
|
|
if !dn.HasConsistentVolumeIndex() {
|
|
t.Error("the self-heal did not restore index consistency")
|
|
}
|
|
}
|
|
|
|
func TestVolumeIndexDigestFollowsNodeLifecycle(t *testing.T) {
|
|
topo, dn := digestTestNode(t)
|
|
full := []*master_pb.VolumeInformationMessage{digestTestVolume(1), digestTestVolume(2), digestTestVolume(3)}
|
|
topo.SyncDataNodeRegistration(full, dn)
|
|
if !dn.HasConsistentVolumeIndex() {
|
|
t.Fatal("registration left the indexes disagreeing")
|
|
}
|
|
|
|
topo.SyncDataNodeRegistration(full[:2], dn)
|
|
if !dn.HasConsistentVolumeIndex() {
|
|
t.Error("dropping a volume left the indexes disagreeing")
|
|
}
|
|
|
|
topo.IncrementalSyncDataNodeRegistration(
|
|
[]*master_pb.VolumeShortInformationMessage{{Id: 9}}, nil, dn)
|
|
if !dn.HasConsistentVolumeIndex() {
|
|
t.Error("a mount delta left the indexes disagreeing")
|
|
}
|
|
|
|
topo.IncrementalSyncDataNodeRegistration(
|
|
nil, []*master_pb.VolumeShortInformationMessage{{Id: 9}}, dn)
|
|
if !dn.HasConsistentVolumeIndex() {
|
|
t.Error("an unmount delta left the indexes disagreeing")
|
|
}
|
|
|
|
topo.UnRegisterDataNode(dn)
|
|
held, servable := dn.VolumeIndexDigests()
|
|
if held != 0 || servable != 0 {
|
|
t.Errorf("an unregistered node should hold nothing: held=%d servable=%d", held, servable)
|
|
}
|
|
}
|
|
|
|
// A volume id mounted on two disks of one server is reported twice with
|
|
// different disk ids, but the master keys volumes by id alone, so it keeps only
|
|
// one copy and its digest can never equal the server's. Resending the full list
|
|
// cannot fix that, so the node has to be excluded from digest comparison
|
|
// entirely rather than resend forever.
|
|
func TestVolumeDigestRefusesDuplicateVolumeIds(t *testing.T) {
|
|
topo, dn := digestTestNode(t)
|
|
|
|
first := digestTestVolume(1)
|
|
second := digestTestVolume(1)
|
|
second.DiskId = 1
|
|
second.Size = first.Size * 2
|
|
|
|
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{first, second}, dn)
|
|
if !dn.HasDuplicateVolumeIds() {
|
|
t.Fatal("a volume id reported twice went unnoticed, so the digest would be trusted and never reconcile")
|
|
}
|
|
|
|
firstInfo, err := storage.NewVolumeInfo(first)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
secondInfo, err := storage.NewVolumeInfo(second)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if dn.VolumeDigest() == firstInfo.ReportHash()^secondInfo.ReportHash() {
|
|
t.Error("expected the master to be unable to represent both copies; if it now can, the guard is no longer needed")
|
|
}
|
|
|
|
// Once the stale twin is gone the node is comparable again.
|
|
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{first}, dn)
|
|
if dn.HasDuplicateVolumeIds() {
|
|
t.Error("the node stayed marked as duplicated after reporting a clean list")
|
|
}
|
|
if dn.VolumeDigest() != firstInfo.ReportHash() {
|
|
t.Error("digest did not settle on the surviving copy")
|
|
}
|
|
}
|
|
|
|
// Two volume servers can hold one address. GetOrCreateDataNode keys on the id a
|
|
// server reports and deliberately refuses to merge a new id onto an address an
|
|
// older node still claims, while the lookup list keys on address alone -- so
|
|
// registering the second server displaces the first from the lookup entry
|
|
// without either node being told. The digest has to follow the entry, not the
|
|
// node that was passed in.
|
|
func addressSharingNodes(t *testing.T) (*Topology, *DataNode, *DataNode) {
|
|
t.Helper()
|
|
topo := NewTopology("digest", nil, 32*1024*1024*1024, 5, false)
|
|
rack := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1")
|
|
counts := map[string]uint32{"": 1000}
|
|
old := rack.GetOrCreateDataNode("10.1.2.3", 8080, 18080, "", "n1", counts)
|
|
fresh := rack.GetOrCreateDataNode("10.1.2.3", 8080, 18080, "", "n2", counts)
|
|
if old == fresh {
|
|
t.Skip("address reuse no longer produces two nodes")
|
|
}
|
|
return topo, old, fresh
|
|
}
|
|
|
|
func TestVolumeIndexDigestFollowsDisplacedLookupEntry(t *testing.T) {
|
|
topo, old, fresh := addressSharingNodes(t)
|
|
v := digestTestVolume(1)
|
|
|
|
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{v}, old)
|
|
if !old.HasConsistentVolumeIndex() {
|
|
t.Fatal("the first node should be consistent before it is displaced")
|
|
}
|
|
|
|
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{v}, fresh)
|
|
|
|
servable := map[*DataNode]bool{}
|
|
for _, dn := range topo.Lookup("", needle.VolumeId(1)) {
|
|
servable[dn] = true
|
|
}
|
|
if servable[old] || !servable[fresh] {
|
|
t.Fatalf("expected the lookup entry to move to the new node, got old=%v fresh=%v",
|
|
servable[old], servable[fresh])
|
|
}
|
|
|
|
if old.HasConsistentVolumeIndex() {
|
|
t.Error("the displaced node still holds the volume and can no longer serve it, so its index must read as inconsistent")
|
|
}
|
|
if !fresh.HasConsistentVolumeIndex() {
|
|
t.Error("the node the lookup entry now names reads as inconsistent")
|
|
}
|
|
}
|
|
|
|
func TestVolumeIndexDigestFollowsRemovedLookupEntry(t *testing.T) {
|
|
topo, old, fresh := addressSharingNodes(t)
|
|
v := digestTestVolume(1)
|
|
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{v}, old)
|
|
|
|
// fresh shares old's address, so unregistering through it removes old's
|
|
// lookup entry. The digest must come off the node that was actually removed.
|
|
rp, _ := super_block.NewReplicaPlacementFromString("000")
|
|
vl := topo.GetVolumeLayout("c", rp, needle.EMPTY_TTL, types.HardDriveType)
|
|
vl.SetVolumeUnavailable(fresh, needle.VolumeId(1))
|
|
|
|
if got := topo.Lookup("c", needle.VolumeId(1)); got != nil {
|
|
t.Fatalf("expected the lookup entry to be gone, got %v", got)
|
|
}
|
|
if old.HasConsistentVolumeIndex() {
|
|
t.Error("the removed node still holds the volume and cannot serve it, so its index must read as inconsistent")
|
|
}
|
|
if _, servable := fresh.VolumeIndexDigests(); servable != 0 {
|
|
t.Error("the node that was merely passed in should never have gained the entry")
|
|
}
|
|
}
|
|
|
|
// The two ends must agree on real heartbeat data, not just on hand-built
|
|
// messages: the volume server hashes what it is about to send, the master
|
|
// hashes what it stored from it.
|
|
func TestMasterDigestMatchesWhatAVolumeServerReports(t *testing.T) {
|
|
dir := t.TempDir()
|
|
loc := storage.NewDiskLocation(dir, 100, util.MinFreeSpace{}, "", types.HardDriveType, nil,
|
|
stats.DefaultDiskIOProbeConfig())
|
|
for _, vid := range []needle.VolumeId{1, 2, 3} {
|
|
v, err := storage.NewVolume(loc.Directory, loc.IdxDirectory, "", vid, storage.NeedleMapInMemory,
|
|
&super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
loc.SetVolume(vid, v)
|
|
}
|
|
|
|
reported := make([]*master_pb.VolumeInformationMessage, 0, 3)
|
|
var serverDigest uint64
|
|
for _, vid := range []needle.VolumeId{1, 2, 3} {
|
|
v, _ := loc.FindVolume(vid)
|
|
_, m := v.ToVolumeInformationMessage(nil)
|
|
if m == nil {
|
|
t.Fatalf("volume %d reported nothing", vid)
|
|
}
|
|
vi, err := storage.NewVolumeInfo(m)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
serverDigest ^= vi.ReportHash()
|
|
reported = append(reported, m)
|
|
}
|
|
|
|
topo, dn := digestTestNode(t)
|
|
topo.SyncDataNodeRegistration(reported, dn)
|
|
|
|
if got := dn.VolumeDigest(); got != serverDigest {
|
|
t.Errorf("master digest %d does not match the reporting server's %d", got, serverDigest)
|
|
}
|
|
}
|