mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-19 14:34:15 +00:00
* fix(storage): prune partial EC shards when sibling disk has healthy .dat (#9478) handleFoundEcxFile only checks for .dat in the same disk location as the EC shards. In a multi-disk volume server an interrupted encode can leave .ec?? + .ecx on disk B while the source .dat still lives on disk A: the per-disk loader sees no .dat next to .ecx, mistakes the leftover for a distributed-EC layout, and mounts the partial shards. The volume server then heartbeats both a regular replica and an EC shard for the same vid and the master keeps both. Sweep the store after per-disk loading and before the cross-disk reconcile to delete partial EC files when a healthy .dat for the same (collection, vid) exists on a sibling disk. Push DeletedEcShardsChan for every pruned shard so master forgets the new-shard message the per-disk pass already emitted, instead of waiting for the next periodic heartbeat. * fix(seaweed-volume): mirror prune of partial EC with sibling .dat (#9478) Rust port of the same Store-level prune added to weed/storage. The per-disk EC loader in disk_location.rs only checks for .dat in the same disk as the EC shards, so an interrupted encode that leaves .ec?? + .ecx on disk B while the source .dat sits on disk A is mounted as if it were a distributed-EC layout. The volume server then heartbeats both a regular replica and an EC shard for the same vid. Sweep the store after per-disk loading and before the cross-disk reconcile, dropping in-memory EcVolumes with fewer than DATA_SHARDS_COUNT shards when a .dat for the same (collection, vid) exists on a sibling disk, and remove all on-disk EC artefacts for them. The Rust heartbeat path already diff-emits deletes from the next ec_volumes snapshot, so no explicit delete-channel push is needed here. Tests cover both the issue 9478 layout and a distributed-EC layout with no .dat anywhere on the store, which must be left alone. * fix(storage): validate sibling .dat size before deleting partial EC (#9478) The earlier prune deleted partial EC files whenever any .dat for the same vid existed on a sibling disk — including a zero-byte shell. A shell is no more useful than the partial shard it would replace, and the partial shard might still combine with shards on other servers in a recoverable distributed-EC layout. Wiping it based on a corrupt sibling .dat is data loss masquerading as cleanup. Tighten the check: when the EC's .vif recorded a non-zero source size in datFileSize, require the sibling .dat to be at least that many bytes; otherwise fall back to "at least a superblock". The .vif value is what the encoder wrote at the moment the source was sealed, so a sibling .dat smaller than that is provably truncated. Carry the size through indexDatOwners alongside the location. The Rust port had the same gap and an additional bug behind it: EcVolume::new wasn't reading datFileSize from .vif, so the safety check always fell back to the superblock floor. Wire datFileSize through. The existing shard-size calculation in LocateEcShardNeedleInterval already uses dat_file_size when non-zero, so populating it also matches Go's behaviour there. Tests cover the truncated-sibling case in both ports.
This commit is contained in:
@@ -288,6 +288,15 @@ func (ev *EcVolume) ShardSize() uint64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// DatFileSize returns the source .dat file size as recorded in .vif at
|
||||
// EC encoding time. Zero for old EC volumes whose .vif predates the
|
||||
// field, or for .vif files we failed to parse. Used by the Store-level
|
||||
// prune in store_ec_reconcile.go to validate that a sibling-disk .dat
|
||||
// is plausibly the encoding source before deleting the partial EC.
|
||||
func (ev *EcVolume) DatFileSize() int64 {
|
||||
return ev.datFileSize
|
||||
}
|
||||
|
||||
func (ev *EcVolume) Size() (size uint64) {
|
||||
for _, shard := range ev.Shards {
|
||||
if shardSize := shard.Size(); shardSize > 0 {
|
||||
|
||||
@@ -154,6 +154,16 @@ func NewStore(
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// First, scrub partial EC artefacts left on one disk by an interrupted
|
||||
// encode while the source .dat still lives on a sibling disk of the
|
||||
// same store. The per-disk loader cannot see the sibling .dat and so
|
||||
// loads the partial shards as if they were a distributed-EC layout,
|
||||
// which makes the volume server heartbeat both a regular replica and
|
||||
// an EC shard set for the same vid (issue #9478). Running before the
|
||||
// cross-disk reconcile keeps that pass from later re-loading shards
|
||||
// we just cleaned up.
|
||||
s.pruneIncompleteEcWithSiblingDat()
|
||||
|
||||
// After every DiskLocation has finished its per-disk EC scan, sweep the
|
||||
// store for shards that live on a disk without local index files and
|
||||
// load them by reaching across to a sibling disk's .ecx / .ecj / .vif.
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/types"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
)
|
||||
|
||||
// TestIssue9478_PartialEcOnSiblingDiskOfHealthyDat reproduces
|
||||
// https://github.com/seaweedfs/seaweedfs/issues/9478:
|
||||
//
|
||||
// - A single volume server has two disks. Disk A holds a healthy
|
||||
// volume_x.dat (with .idx + .vif). Disk B holds the leftovers of an
|
||||
// interrupted EC encode for the SAME volume: a single .ec?? shard plus
|
||||
// .ecx + .ecj + .vif, with no .dat next to them.
|
||||
//
|
||||
// The current per-disk EC loader (handleFoundEcxFile / validateEcVolume) only
|
||||
// checks for .dat in the same DiskLocation as the EC shards. Because Disk B
|
||||
// has no .dat, it concludes "this must be a distributed EC volume" and
|
||||
// happily loads the lone partial shard. The .dat on Disk A also loads as a
|
||||
// regular replica. The volume server then heartbeats BOTH a regular volume
|
||||
// and an EC shard for the same vid, which is the inconsistent state the
|
||||
// issue is describing.
|
||||
//
|
||||
// The expected behaviour is to recognise that a healthy .dat exists on a
|
||||
// sibling disk on the same store and clean up the partial EC files on
|
||||
// Disk B, exactly as we already do today when .dat and the partial EC files
|
||||
// sit on the same disk.
|
||||
func TestIssue9478_PartialEcOnSiblingDiskOfHealthyDat(t *testing.T) {
|
||||
collection := ""
|
||||
vid := needle.VolumeId(122)
|
||||
|
||||
root := t.TempDir()
|
||||
datDir := root + "/sdd"
|
||||
ecDir := root + "/sdf"
|
||||
if err := os.MkdirAll(datDir, 0o755); err != nil {
|
||||
t.Fatalf("mkdir datDir: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(ecDir, 0o755); err != nil {
|
||||
t.Fatalf("mkdir ecDir: %v", err)
|
||||
}
|
||||
|
||||
// Disk A (sdd): a healthy-looking volume_x.dat + .idx + .vif. We don't
|
||||
// run NewVolume here, only check what loadAllEcShards on Disk B does in
|
||||
// the presence of a same-server .dat. A truncated 10 MiB .dat is enough
|
||||
// for the EC validator to compute an expected shard size from.
|
||||
datFileSize := int64(10 * 1024 * 1024)
|
||||
datBase := erasure_coding.EcShardFileName(collection, datDir, int(vid))
|
||||
if f, err := os.Create(datBase + ".dat"); err == nil {
|
||||
if err := f.Truncate(datFileSize); err != nil {
|
||||
t.Fatalf("truncate dat: %v", err)
|
||||
}
|
||||
f.Close()
|
||||
} else {
|
||||
t.Fatalf("create dat: %v", err)
|
||||
}
|
||||
if f, err := os.Create(datBase + ".idx"); err == nil {
|
||||
f.Close()
|
||||
}
|
||||
if f, err := os.Create(datBase + ".vif"); err == nil {
|
||||
f.Close()
|
||||
}
|
||||
|
||||
// Disk B (sdf): only one EC shard plus .ecx + .ecj + .vif, no .dat.
|
||||
// This mirrors the issue 9478 listing for volume_server_4 / sdf.
|
||||
ecBase := erasure_coding.EcShardFileName(collection, ecDir, int(vid))
|
||||
expectedShardSize := calculateExpectedShardSize(datFileSize, erasure_coding.DataShardsCount)
|
||||
if f, err := os.Create(ecBase + erasure_coding.ToExt(1)); err == nil {
|
||||
if err := f.Truncate(expectedShardSize); err != nil {
|
||||
t.Fatalf("truncate shard: %v", err)
|
||||
}
|
||||
f.Close()
|
||||
} else {
|
||||
t.Fatalf("create shard: %v", err)
|
||||
}
|
||||
if f, err := os.Create(ecBase + ".ecx"); err == nil {
|
||||
f.WriteString("dummy ecx")
|
||||
f.Close()
|
||||
}
|
||||
if f, err := os.Create(ecBase + ".ecj"); err == nil {
|
||||
f.Close()
|
||||
}
|
||||
if f, err := os.Create(ecBase + ".vif"); err == nil {
|
||||
f.Close()
|
||||
}
|
||||
|
||||
minFreeSpace := util.MinFreeSpace{Type: util.AsPercent, Percent: 1, Raw: "1"}
|
||||
makeDisk := func(dir string) *DiskLocation {
|
||||
dl := &DiskLocation{
|
||||
Directory: dir,
|
||||
DirectoryUuid: "test-uuid-" + dir,
|
||||
IdxDirectory: dir,
|
||||
DiskType: types.HddType,
|
||||
MinFreeSpace: minFreeSpace,
|
||||
}
|
||||
dl.volumes = make(map[needle.VolumeId]*Volume)
|
||||
dl.ecVolumes = make(map[needle.VolumeId]*erasure_coding.EcVolume)
|
||||
return dl
|
||||
}
|
||||
datLoc := makeDisk(datDir)
|
||||
ecLoc := makeDisk(ecDir)
|
||||
|
||||
// Stand the disks up the same way NewStore does: per-disk EC scan
|
||||
// first, then the Store-level passes. The per-disk pass alone cannot
|
||||
// see the .dat on the sibling disk, so it mounts the partial shards;
|
||||
// the Store-level prune is what brings the cluster back to a
|
||||
// consistent state.
|
||||
store := &Store{
|
||||
Locations: []*DiskLocation{datLoc, ecLoc},
|
||||
NewEcShardsChan: make(chan master_pb.VolumeEcShardInformationMessage, 16),
|
||||
DeletedEcShardsChan: make(chan master_pb.VolumeEcShardInformationMessage, 16),
|
||||
}
|
||||
ecLoc.ecShardNotifyHandler = func(collection string, vid needle.VolumeId, shardId erasure_coding.ShardId, ecVolume *erasure_coding.EcVolume) {
|
||||
store.NewEcShardsChan <- master_pb.VolumeEcShardInformationMessage{
|
||||
Id: uint32(vid),
|
||||
Collection: collection,
|
||||
}
|
||||
}
|
||||
|
||||
if err := ecLoc.loadAllEcShards(ecLoc.ecShardNotifyHandler); err != nil {
|
||||
t.Logf("loadAllEcShards on ecDir returned: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { closeEcVolumes(ecLoc) })
|
||||
|
||||
preShardCount := ecLoc.EcShardCount()
|
||||
preNewMessages := len(store.NewEcShardsChan)
|
||||
t.Logf("after per-disk EC load on sdf: inMemoryShards=%d newEcShardMessages=%d", preShardCount, preNewMessages)
|
||||
if preShardCount == 0 {
|
||||
t.Fatalf("test setup is no longer reproducing the bug: per-disk EC load did not mount the partial shard")
|
||||
}
|
||||
|
||||
store.pruneIncompleteEcWithSiblingDat()
|
||||
store.reconcileEcShardsAcrossDisks()
|
||||
|
||||
leftoverShard := util.FileExists(ecBase + erasure_coding.ToExt(1))
|
||||
leftoverEcx := util.FileExists(ecBase + ".ecx")
|
||||
leftoverEcj := util.FileExists(ecBase + ".ecj")
|
||||
loaded := ecLoc.EcShardCount()
|
||||
deleteMessages := len(store.DeletedEcShardsChan)
|
||||
|
||||
t.Logf("after Store-level cleanup: shardFileLeft=%v ecxLeft=%v ecjLeft=%v inMemoryShards=%d deletedEcShardMessages=%d",
|
||||
leftoverShard, leftoverEcx, leftoverEcj, loaded, deleteMessages)
|
||||
|
||||
if loaded != 0 {
|
||||
t.Fatalf("partial EC shard was still mounted (%d shards) after the Store-level prune; a healthy .dat exists on a sibling disk and the leftover should have been cleaned up", loaded)
|
||||
}
|
||||
if leftoverShard || leftoverEcx || leftoverEcj {
|
||||
t.Fatalf("partial EC files survived the Store-level prune (shard=%v ecx=%v ecj=%v); a healthy .dat exists on a sibling disk and the leftover should have been removed",
|
||||
leftoverShard, leftoverEcx, leftoverEcj)
|
||||
}
|
||||
if deleteMessages == 0 {
|
||||
t.Errorf("prune did not push DeletedEcShardsChan; master would have to wait for the next periodic heartbeat to forget the partial shard")
|
||||
}
|
||||
|
||||
// The .dat on the sibling disk must survive — pruning the partial EC
|
||||
// must never touch the healthy replica we are falling back to.
|
||||
if !util.FileExists(datBase + ".dat") {
|
||||
t.Fatalf("healthy .dat on sibling disk was removed by the prune; only the partial EC files should be cleaned up")
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssue9478_ZeroByteSiblingDatKeepsPartialEc is the safety guard for
|
||||
// pruneIncompleteEcWithSiblingDat: when the sibling .dat is zero bytes
|
||||
// (or smaller than what .vif recorded as the encoding source), we'd
|
||||
// rather keep the partial EC than delete it based on garbage. The
|
||||
// partial shard may still combine with shards on other servers in a
|
||||
// recoverable distributed-EC layout.
|
||||
func TestIssue9478_ZeroByteSiblingDatKeepsPartialEc(t *testing.T) {
|
||||
collection := ""
|
||||
vid := needle.VolumeId(122)
|
||||
|
||||
root := t.TempDir()
|
||||
datDir := root + "/sdd"
|
||||
ecDir := root + "/sdf"
|
||||
if err := os.MkdirAll(datDir, 0o755); err != nil {
|
||||
t.Fatalf("mkdir datDir: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(ecDir, 0o755); err != nil {
|
||||
t.Fatalf("mkdir ecDir: %v", err)
|
||||
}
|
||||
|
||||
// Sibling .dat is a zero-byte shell — the kind volume servers 3
|
||||
// and 5 in the issue 9478 report have.
|
||||
datBase := erasure_coding.EcShardFileName(collection, datDir, int(vid))
|
||||
if f, err := os.Create(datBase + ".dat"); err == nil {
|
||||
f.Close()
|
||||
} else {
|
||||
t.Fatalf("create zero-byte dat: %v", err)
|
||||
}
|
||||
|
||||
ecBase := erasure_coding.EcShardFileName(collection, ecDir, int(vid))
|
||||
datFileSizeForShard := int64(10 * 1024 * 1024)
|
||||
expectedShardSize := calculateExpectedShardSize(datFileSizeForShard, erasure_coding.DataShardsCount)
|
||||
if f, err := os.Create(ecBase + erasure_coding.ToExt(1)); err == nil {
|
||||
if err := f.Truncate(expectedShardSize); err != nil {
|
||||
t.Fatalf("truncate shard: %v", err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
if f, err := os.Create(ecBase + ".ecx"); err == nil {
|
||||
f.WriteString("dummy ecx")
|
||||
f.Close()
|
||||
}
|
||||
if f, err := os.Create(ecBase + ".ecj"); err == nil {
|
||||
f.Close()
|
||||
}
|
||||
if f, err := os.Create(ecBase + ".vif"); err == nil {
|
||||
f.Close()
|
||||
}
|
||||
|
||||
minFreeSpace := util.MinFreeSpace{Type: util.AsPercent, Percent: 1, Raw: "1"}
|
||||
makeDisk := func(dir string) *DiskLocation {
|
||||
dl := &DiskLocation{
|
||||
Directory: dir,
|
||||
DirectoryUuid: "test-uuid-" + dir,
|
||||
IdxDirectory: dir,
|
||||
DiskType: types.HddType,
|
||||
MinFreeSpace: minFreeSpace,
|
||||
}
|
||||
dl.volumes = make(map[needle.VolumeId]*Volume)
|
||||
dl.ecVolumes = make(map[needle.VolumeId]*erasure_coding.EcVolume)
|
||||
return dl
|
||||
}
|
||||
datLoc := makeDisk(datDir)
|
||||
ecLoc := makeDisk(ecDir)
|
||||
|
||||
store := &Store{
|
||||
Locations: []*DiskLocation{datLoc, ecLoc},
|
||||
NewEcShardsChan: make(chan master_pb.VolumeEcShardInformationMessage, 16),
|
||||
DeletedEcShardsChan: make(chan master_pb.VolumeEcShardInformationMessage, 16),
|
||||
}
|
||||
|
||||
if err := ecLoc.loadAllEcShards(nil); err != nil {
|
||||
t.Logf("loadAllEcShards on ecDir returned: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { closeEcVolumes(ecLoc) })
|
||||
if ecLoc.EcShardCount() == 0 {
|
||||
t.Fatalf("test setup is not reproducing the layout: per-disk EC load did not mount the partial shard")
|
||||
}
|
||||
|
||||
store.pruneIncompleteEcWithSiblingDat()
|
||||
|
||||
if ecLoc.EcShardCount() == 0 {
|
||||
t.Fatalf("prune deleted partial EC based on a zero-byte sibling .dat; the shard should have been left in place so distributed reconstruction is still possible")
|
||||
}
|
||||
if !util.FileExists(ecBase + erasure_coding.ToExt(1)) {
|
||||
t.Fatalf("prune deleted the partial shard file based on a zero-byte sibling .dat")
|
||||
}
|
||||
if len(store.DeletedEcShardsChan) != 0 {
|
||||
t.Errorf("prune pushed a DeletedEcShardsChan message despite skipping cleanup; nothing was actually deleted")
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,22 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
|
||||
)
|
||||
|
||||
// datOwnerInfo records both the disk that holds a .dat for a given
|
||||
// (collection, vid) and the size on disk. The size is consulted by
|
||||
// pruneIncompleteEcWithSiblingDat before deleting any EC artefacts:
|
||||
// a zero-byte or truncated .dat is not a credible fallback, and we'd
|
||||
// rather leave the partial EC in place than wipe it based on garbage.
|
||||
type datOwnerInfo struct {
|
||||
location *DiskLocation
|
||||
size int64
|
||||
}
|
||||
|
||||
// ecKeyForReconcile keys orphan-shard reconciliation by collection + volume
|
||||
// id. Per-collection grouping matters because two collections can re-use the
|
||||
// same volume id, and we must only pair shards with their own .ecx file.
|
||||
@@ -125,6 +137,160 @@ func (s *Store) indexEcxOwners() map[ecKeyForReconcile]ecxOwnerInfo {
|
||||
return owners
|
||||
}
|
||||
|
||||
// pruneIncompleteEcWithSiblingDat removes leftover EC artefacts on one
|
||||
// disk when a healthy .dat for the same (collection, vid) lives on a
|
||||
// sibling disk of the same store. This is the cross-disk analogue of the
|
||||
// validateEcVolume cleanup in handleFoundEcxFile: a same-disk .dat next
|
||||
// to partial shards is already taken as proof that an EC encode was
|
||||
// interrupted, and the partial shards get removed so the .dat keeps
|
||||
// serving the volume. Per-disk loaders cannot see sibling disks, so when
|
||||
// the .dat ends up on disk A and the partial shards on disk B the per-disk
|
||||
// pass mistakes the leftover for a normal distributed-EC layout (no .dat
|
||||
// next to .ecx) and mounts the partial shards. The volume server then
|
||||
// heartbeats both a regular replica and an EC shard for the same vid, the
|
||||
// master keeps both entries, and reads route through either path
|
||||
// depending on the client. Issue 9478.
|
||||
//
|
||||
// Cleanup is gated on shardCount < DataShardsCount so that a deliberate
|
||||
// "full local EC, .dat retained" layout split across two disks (.dat on
|
||||
// disk A, all 10+ shards on disk B) is left alone — the per-disk loader
|
||||
// already keeps that configuration when everything is on a single disk,
|
||||
// and pruning it here would be a behaviour regression for operators who
|
||||
// rely on it. Distributed EC volumes (no .dat on any disk of this server)
|
||||
// also fall through unchanged because the lookup in the .dat index below
|
||||
// will simply not find a match.
|
||||
//
|
||||
// Before deleting any EC files we also check that the sibling .dat is
|
||||
// plausibly the encoding source: at least super_block.SuperBlockSize
|
||||
// bytes long, and — when the EC's .vif recorded a non-zero source size
|
||||
// in datFileSize — at least that many bytes. A zero-byte shell or a
|
||||
// truncated .dat does not justify wiping the partial EC, because that
|
||||
// EC shard may still combine usefully with shards on other servers in
|
||||
// a recoverable distributed-EC layout.
|
||||
//
|
||||
// We push DeletedEcShardsChan for every pruned shard so the master is told
|
||||
// to forget the registrations the per-disk pass already emitted on
|
||||
// NewEcShardsChan during startup, instead of waiting for the first
|
||||
// periodic heartbeat to reconcile.
|
||||
func (s *Store) pruneIncompleteEcWithSiblingDat() {
|
||||
if len(s.Locations) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
datOwners := s.indexDatOwners()
|
||||
if len(datOwners) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for diskId, loc := range s.Locations {
|
||||
// Snapshot under the read lock so we are not iterating
|
||||
// ecVolumes while the cleanup below takes the write lock.
|
||||
type victim struct {
|
||||
collection string
|
||||
vid needle.VolumeId
|
||||
messages []*master_pb.VolumeEcShardInformationMessage
|
||||
datDir string
|
||||
shardCount int
|
||||
}
|
||||
var victims []victim
|
||||
loc.ecVolumesLock.RLock()
|
||||
for vid, ev := range loc.ecVolumes {
|
||||
shardCount := len(ev.Shards)
|
||||
if shardCount >= erasure_coding.DataShardsCount {
|
||||
continue
|
||||
}
|
||||
key := ecKeyForReconcile{collection: ev.Collection, vid: vid}
|
||||
owner, hasDat := datOwners[key]
|
||||
if !hasDat || owner.location == loc {
|
||||
continue
|
||||
}
|
||||
// Decide whether the sibling .dat is a credible source.
|
||||
// Prefer the size baked into .vif at encode time; fall
|
||||
// back to "at least a superblock" for old EC volumes
|
||||
// whose .vif predates the field.
|
||||
requiredDatSize := ev.DatFileSize()
|
||||
if requiredDatSize <= 0 {
|
||||
requiredDatSize = int64(super_block.SuperBlockSize)
|
||||
}
|
||||
if owner.size < requiredDatSize {
|
||||
glog.Warningf("ec volume %d (collection=%q) on %s has only %d shards but sibling .dat on %s is %d bytes (need >= %d); leaving partial EC in place so distributed reconstruction is still possible",
|
||||
vid, ev.Collection, loc.Directory, shardCount, owner.location.Directory, owner.size, requiredDatSize)
|
||||
continue
|
||||
}
|
||||
victims = append(victims, victim{
|
||||
collection: ev.Collection,
|
||||
vid: vid,
|
||||
messages: ev.ToVolumeEcShardInformationMessage(uint32(diskId)),
|
||||
datDir: owner.location.Directory,
|
||||
shardCount: shardCount,
|
||||
})
|
||||
}
|
||||
loc.ecVolumesLock.RUnlock()
|
||||
|
||||
for _, v := range victims {
|
||||
glog.Warningf("ec volume %d (collection=%q) on %s has only %d shards (need %d) while a healthy .dat exists on sibling disk %s; cleaning up leftover EC files (issue 9478)",
|
||||
v.vid, v.collection, loc.Directory, v.shardCount, erasure_coding.DataShardsCount, v.datDir)
|
||||
loc.unloadEcVolume(v.vid)
|
||||
loc.removeEcVolumeFiles(v.collection, v.vid)
|
||||
for _, msg := range v.messages {
|
||||
select {
|
||||
case s.DeletedEcShardsChan <- *msg:
|
||||
default:
|
||||
// Channel full during startup is fine — the next
|
||||
// periodic heartbeat reports the full ecVolumes
|
||||
// state, which no longer contains these shards.
|
||||
glog.V(2).Infof("DeletedEcShardsChan full while pruning ec volume %d; relying on periodic heartbeat", v.vid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// indexDatOwners returns, for every (collection, vid), the first disk on
|
||||
// this store that holds a .dat file for it plus the file's size. Used by
|
||||
// pruneIncompleteEcWithSiblingDat so it can decide whether partial EC
|
||||
// artefacts on another disk are leftovers of an interrupted encode AND
|
||||
// whether the sibling .dat is large enough to be a credible fallback.
|
||||
//
|
||||
// We record any .dat os.ReadDir can see — including zero-byte shells.
|
||||
// The mere presence of a .dat means this volume was a regular volume on
|
||||
// this server at some point, which rules out the "distributed EC, no
|
||||
// .dat anywhere" reading. Whether that .dat is actually usable is the
|
||||
// caller's call, made by comparing this size to the EC's recorded
|
||||
// source size in .vif.
|
||||
func (s *Store) indexDatOwners() map[ecKeyForReconcile]datOwnerInfo {
|
||||
owners := make(map[ecKeyForReconcile]datOwnerInfo)
|
||||
for _, loc := range s.Locations {
|
||||
entries, err := os.ReadDir(loc.Directory)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := entry.Name()
|
||||
if !strings.HasSuffix(name, ".dat") {
|
||||
continue
|
||||
}
|
||||
base := name[:len(name)-len(".dat")]
|
||||
collection, vid, err := parseCollectionVolumeId(base)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
key := ecKeyForReconcile{collection: collection, vid: vid}
|
||||
if _, exists := owners[key]; !exists {
|
||||
owners[key] = datOwnerInfo{location: loc, size: info.Size()}
|
||||
}
|
||||
}
|
||||
}
|
||||
return owners
|
||||
}
|
||||
|
||||
// collectOrphanEcShards walks the disk's data directory and returns the
|
||||
// .ec?? shard files that are present on disk but not yet registered to an
|
||||
// EcVolume in memory. The map is keyed by (collection, vid) so callers can
|
||||
|
||||
Reference in New Issue
Block a user