mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-08 15:41:15 +02:00
* master: keep new volumes and writes off servers in maintenance mode The master recorded a volume server's maintenance flag from the heartbeat but never consulted it. A server in maintenance (#7977) is being drained, yet the master kept creating volumes on it whenever it had free slots and kept handing out its volumes for writes. Nothing on the volume server blocks plain HTTP uploads either, so "read-only mode" was only a name. Volume growth: a data node in maintenance mode reports zero free slots through AvailableSpaceFor, which takes it out of every candidate list, feasibility count and capacity reservation. Its slots still roll up into its rack and data center, so the random offset drawn from those totals for an other-rack or other-DC replica could land in space the walk then skips and fail with "No free volume slot found!" while siblings had room; the walk now folds the offset into the space that is actually eligible. This also covers the pre-existing case of an over-committed sibling. Assignment: a replica on a server in maintenance mode is treated like a read-only replica in isAllWritable, so its volume leaves the writable list and returns when the flag clears. Topology.SetDataNodeMaintenanceMode re-evaluates the node's volumes on every change, since heartbeats are digest-based and a full volume list may not follow for a long time. Reads and lookups are untouched. The flag moves to an atomic so the assign and growth paths can read it without the node lock. Heartbeat: the Go volume server sent its state only when it changed, so a master elected while a server sat in maintenance never learned about it. The state now rides along on every heartbeat, as the Rust server already does; the master's compare is an atomic swap, and only a change does work. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * master: hold maintenance mode through vacuum commit and mark-writable SetVolumeAvailable and SetVolumeWritable put a volume back on the writable list on the replica count alone. A vacuum that started before the server entered maintenance, or a vacuum worker's mark-writable arriving after it, handed the volume back to assignment with a replica on the draining server. Heartbeats carry only changed volumes, so nothing re-evaluated it until the volume itself changed. Apply isAllWritable on both paths, the same test EnsureCorrectWritables uses. Also pin that re-evaluating a volume a concurrent disconnect already removed from its layout is a no-op. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * master: record a server's read-only notification on its node before judging the volume A volume server notifies the master the moment it flips a volume between read-only and writable, ahead of the heartbeat that repeats the flag. The layout only set its per-location flag, so isAllWritable, which reads the node's heartbeat copy, still saw the old value: a mark-writable was withheld until the next heartbeat, and a re-evaluation landing between a mark-readonly and its heartbeat put the volume back on the writable list. Record the flag on the node's volume first. AddOrUpdateVolume keeps the digest and the active volume count in step, so the heartbeat that follows finds nothing to change. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * master: a read-only mark does not confirm a provisional volume DataNode.SetVolumeReadOnly went through Disk.AddOrUpdateVolume, which treats its input as a server report and so ended the grace period that keeps a just-grown volume safe from a full report collected before the grow. A volume marked read-only before its first report could then be removed by that stale report. Give Disk a SetVolumeReadOnly that flips the flag and keeps the digest and active volume count in step without touching volumeAddedAt. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
106 lines
3.4 KiB
Go
106 lines
3.4 KiB
Go
package storage
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
|
|
)
|
|
|
|
func mountTestVolume(t testing.TB, loc *DiskLocation, vid needle.VolumeId, collection string) *Volume {
|
|
t.Helper()
|
|
v, err := NewVolume(loc.Directory, loc.IdxDirectory, collection, vid, NeedleMapInMemory,
|
|
&super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
loc.SetVolume(vid, v)
|
|
return v
|
|
}
|
|
|
|
// The digest has to cover exactly the volumes the heartbeat carries. A volume
|
|
// reported but left out of the digest, or the reverse, makes the master's
|
|
// comparison disagree forever.
|
|
func TestCollectHeartbeatDigestsExactlyWhatItReports(t *testing.T) {
|
|
store := newTestStore(t, 2)
|
|
mountTestVolume(t, store.Locations[0], 1, "")
|
|
mountTestVolume(t, store.Locations[0], 2, "")
|
|
mountTestVolume(t, store.Locations[1], 3, "")
|
|
|
|
heartbeat := store.CollectHeartbeat()
|
|
if heartbeat.VolumeDigest == nil {
|
|
t.Fatal("heartbeat carried no digest")
|
|
}
|
|
if len(heartbeat.Volumes) != 3 {
|
|
t.Fatalf("expected 3 volumes reported, got %d", len(heartbeat.Volumes))
|
|
}
|
|
|
|
var want uint64
|
|
for _, m := range heartbeat.Volumes {
|
|
vi, err := NewVolumeInfo(m)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want ^= vi.ReportHash()
|
|
}
|
|
if got := heartbeat.GetVolumeDigest(); got != want {
|
|
t.Errorf("digest %d does not cover the reported volumes (%d)", got, want)
|
|
}
|
|
}
|
|
|
|
// A server holding nothing reports a digest of 0, which is why the field needs
|
|
// explicit presence: it must stay distinguishable from a server that computes
|
|
// no digest at all.
|
|
func TestCollectHeartbeatDigestsAnEmptyStore(t *testing.T) {
|
|
store := newTestStore(t, 1)
|
|
|
|
heartbeat := store.CollectHeartbeat()
|
|
if heartbeat.VolumeDigest == nil {
|
|
t.Fatal("an empty store still has to report a digest, or the master cannot tell it from an old server")
|
|
}
|
|
if got := heartbeat.GetVolumeDigest(); got != 0 {
|
|
t.Errorf("expected an empty store to digest to 0, got %d", got)
|
|
}
|
|
if !heartbeat.HasNoVolumes {
|
|
t.Error("expected has_no_volumes on an empty store")
|
|
}
|
|
}
|
|
|
|
// The maintenance flag has to reach a master that never saw the change: a
|
|
// leader elected while a server sits in maintenance only hears from it through
|
|
// the regular heartbeats, so each one carries the state.
|
|
func TestCollectHeartbeatCarriesState(t *testing.T) {
|
|
store := newTestStore(t, 1)
|
|
|
|
heartbeat := store.CollectHeartbeat()
|
|
if heartbeat.State == nil {
|
|
t.Fatal("heartbeat carried no state")
|
|
}
|
|
if heartbeat.State.GetMaintenance() {
|
|
t.Fatal("a fresh store must not report maintenance mode")
|
|
}
|
|
|
|
if err := store.State.Update(&volume_server_pb.VolumeServerState{Maintenance: true}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !store.CollectHeartbeat().GetState().GetMaintenance() {
|
|
t.Error("heartbeat did not report maintenance mode after it was switched on")
|
|
}
|
|
}
|
|
|
|
func TestCollectHeartbeatDigestFollowsVolumeChanges(t *testing.T) {
|
|
store := newTestStore(t, 1)
|
|
mountTestVolume(t, store.Locations[0], 1, "")
|
|
first := store.CollectHeartbeat().GetVolumeDigest()
|
|
|
|
if second := store.CollectHeartbeat().GetVolumeDigest(); second != first {
|
|
t.Errorf("an unchanged store reported a different digest: %d then %d", first, second)
|
|
}
|
|
|
|
mountTestVolume(t, store.Locations[0], 2, "")
|
|
if grown := store.CollectHeartbeat().GetVolumeDigest(); grown == first {
|
|
t.Error("mounting a volume left the digest unchanged")
|
|
}
|
|
}
|