mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-30 17:00:48 +01:00
* volume: remount ec shards the rebuild replaced under a mounted handle Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * ec.rebuild: keep copied shards the rebuild regenerated in place Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * volume: reopen regenerated ec shards in place instead of remounting UnmountEcShards and MountEcShards send deltas through separate buffered channels, so a remount can reach the master as add-then-delete and drop the repaired shard from lookups until the next full EC heartbeat. ReopenEcShard swaps the mounted shard's descriptor under the volume lock instead: registration, disk type, and heartbeat state never flip, and readers holding the old shard degrade to a clean error the parity path already recovers from. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * rust volume: reopen rebuilt ec shards in place, mirroring Go volume_ec_shards_rebuild recreates a shard file under a mounted handle whose descriptor still serves the detached inode (a mounted shard whose file was deleted under it). DiskLocation::reopen_ec_shard swaps the mounted descriptor for a fresh open, matching Go's ReopenEcShard — the registration and heartbeat view never flip. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * volume: fail ec rebuild when a regenerated shard cannot be reopened * rust volume: fail ec shards rebuild when a rebuilt shard cannot be reopened * volume: repoint mounted ec shards after rebuild across disks A mounted shard whose file was deleted under the mount is "missing" to the rebuild: the regenerated file lands on the rebuild location while the handle keeps serving the detached inode on another disk. Reopening the mounted disk then failed the whole RPC, so the caller never saw the rebuilt ids and its deferred cleanup deleted the regenerated copies. Repoint every mounted handle of the rebuilt volume instead of only this run's outputs: reopen in place when the rebuilt file is on the mounted disk, otherwise drop the stale handle and remount the shard from whichever local disk holds the file via a delta-free load split out of MountEcShards — a same-server move, so the master's registration never flips. A shard that can be neither reopened nor remounted is left unmounted rather than serving replaced bytes, and the repoint step no longer fails the RPC, so RebuiltShardIds always reaches the caller. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * rust volume: repoint mounted ec shards after rebuild across disks Mirror the Go repoint loop: iterate every mounted shard of the rebuilt volume, reopen in place when the rebuilt file is on the mounted disk, and otherwise drop the stale handle and remount the shard from whichever local disk holds it. The repoint no longer fails the RPC, so rebuilt_shard_ids still reaches the caller and its copy cleanup keeps the regenerated shards. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * rust volume: still rebuild ecx and repoint mounts when no shard is missing * master: keep a node in an ec shard's lookup while a disk still holds it A rebuild that relocates a mounted shard between a node's disks reports the move as a delete on the source disk plus an add on the destination. Registering first is a no-op because shard lookups key on the node, then the unregister dropped the node outright — and steady-state heartbeats never restore it, so readers and later rebuilds lose the repaired copy. Skip the delete while another disk on the node still holds the shard. UnregisterDataNode now clears the node's own ec records before dropping its registrations, so the retained-shard check cannot skip the teardown, and each unregister gets a ShardsInfo copy since DeleteEcShard drains the shared one. * rust volume: fold the journal on a mounted index instead of rewriting it The rebuild handler regenerated .ecx through File::create even while the volume was mounted, truncating the index under readers that search the open descriptor with its old size — a live lookup could hit EOF mid-scan. Go folds the deletion journal in place on a mounted volume; do the same through rebuild_ecx_from_journal and reserve the full regen for the unmounted path, where nothing holds the file open to interrupt. * volume, rust volume: report rebuilt shards on late failures, fix rust corrupt discovery Two review findings the repoint rework left open: - A RebuildEcxFile failure still returns a plain error after shards were regenerated, so the caller discards the response holding the rebuilt ids and the deferred cleanup deletes copies the rebuild regenerated in place. The Go handler now packs the ids into the gRPC status details and the shell pulls them back out on error; the Rust handler carries them in grpc-status-details-bin the same way. - The Rust rebuild still counted a sidecar-corrupt shard as present, so it fed bad bytes into reconstruction and never regenerated the shard. Discovery now loads the generation-0 .ecsum the way Go does, verifies every present shard, and reclassifies mismatches (and zero-size residue) as rebuild targets written to a .rebuilding temp file, verified, and atomically renamed over the original only after every output passes the sidecar check. Honors unsafe_ignore_sidecar, added to VolumeEcShardsRebuildRequest to match the Go proto. * volume: repoint sibling journals sharing a folded .ecj after rebuild RebuildEcxFile unlinks the deletion journal and the mounted path only reopens its own handle — a sibling mount journaling into the same file keeps appending to the detached inode, and those deletes never reach a path-based reader again. Reopen every mounted volume sharing the index base before leaving the rebuild. * rust volume: repoint sibling journals sharing a folded .ecj after rebuild rebuild_ecx_from_journal replaces the journal and reopens only its own handle; mounts on sibling disks sharing that .ecj keep the detached handle, so their next appends are lost at restart. Adopt the folded journal on every mounted runtime sharing the path before releasing the store lock, clearing their in-memory set — every journaled id is now an .ecx tombstone. * ec: swap the deletion journal handle only after the new file opens A failed reopen left ecjFile nil, so every later delete returned "ec volume closed". Open first, then close and swap — mirroring the Rust adopt_folded_journal ordering. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * ec.rebuild: fail the rebuild when a sibling journal cannot be repointed A warning left the sibling mount journaling to a detached inode while the RPC reported success. Propagate the error like the Rust handler's adopt_folded_journal map_err. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * ec: keep deletes loud when the journal reopen cannot open the replacement Reverts the swap-on-success ordering: a detached handle kept on failure lets deletes append and sync to the removed inode, then vanish at restart. Closing first also frees the descriptor before the open. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * rust volume: drop the folded journal handle before reopening it adopt_folded_journal and the post-fold recreate now clear ecj_file before opening the replacement, so a failed reopen leaves deletes erroring instead of appending to the detached inode. Mirrors Go's ReopenDeletionJournal. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * ec: drop the cached journal size when its reopen fails 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>
223 lines
6.8 KiB
Go
223 lines
6.8 KiB
Go
package topology
|
|
|
|
import (
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/types"
|
|
)
|
|
|
|
func (dn *DataNode) GetEcShards() (ret []*erasure_coding.EcVolumeInfo) {
|
|
dn.RLock()
|
|
for _, c := range dn.children {
|
|
disk := c.(*Disk)
|
|
ret = append(ret, disk.GetEcShards()...)
|
|
}
|
|
dn.RUnlock()
|
|
return ret
|
|
}
|
|
|
|
// ecShardKey identifies a per-physical-disk EC shard entry on a DataNode.
|
|
// A single volume's EC shards can live on multiple physical disks of one
|
|
// DataNode (e.g. a 10+4 volume spread across 4 mount points), so entries
|
|
// must be tracked per (volume, physical disk) rather than per volume alone.
|
|
// See issue #9212.
|
|
type ecShardKey struct {
|
|
vid needle.VolumeId
|
|
diskId types.DiskId
|
|
}
|
|
|
|
func (dn *DataNode) UpdateEcShards(actualShards []*erasure_coding.EcVolumeInfo) (newShards, deletedShards []*erasure_coding.EcVolumeInfo) {
|
|
// Aggregate incoming messages by (volume, physical disk). Duplicates for
|
|
// the same (vid, diskId) are merged; entries for the same vid on
|
|
// different disks stay separate so per-physical-disk attribution is
|
|
// preserved all the way to DiskInfo.EcShardInfos — which the admin
|
|
// shell's ec.balance / ec.rebuild planners read via eci.DiskId.
|
|
actualByKey := make(map[ecShardKey]*erasure_coding.EcVolumeInfo, len(actualShards))
|
|
for _, ecShards := range actualShards {
|
|
key := ecShardKey{vid: ecShards.VolumeId, diskId: types.DiskId(ecShards.DiskId)}
|
|
if existing, ok := actualByKey[key]; ok {
|
|
existing.ShardsInfo.Add(ecShards.ShardsInfo)
|
|
continue
|
|
}
|
|
// Clone so subsequent merges for the same key don't mutate the
|
|
// caller's EcVolumeInfo, and so the diff below is stable.
|
|
clone := *ecShards
|
|
clone.ShardsInfo = ecShards.ShardsInfo.Copy()
|
|
actualByKey[key] = &clone
|
|
}
|
|
|
|
// Hold dn.Lock for the full read-diff-write cycle so concurrent heartbeats,
|
|
// getOrCreateDisk calls, and UpAdjustDiskUsageDelta updates on this data
|
|
// node are serialized with us — matching UpdateVolumes' locking model.
|
|
// Internal helpers below assume the caller holds dn.Lock.
|
|
dn.Lock()
|
|
defer dn.Unlock()
|
|
|
|
existingEcShards := dn.getEcShardsLocked()
|
|
|
|
// find out the newShards and deletedShards
|
|
for _, ecShards := range existingEcShards {
|
|
|
|
var newShardCount, deletedShardCount int
|
|
disk := dn.getOrCreateDisk(ecShards.DiskType)
|
|
|
|
key := ecShardKey{vid: ecShards.VolumeId, diskId: types.DiskId(ecShards.DiskId)}
|
|
if actualEcShards, ok := actualByKey[key]; !ok {
|
|
// dn registered ec shards not found in the new set of ec shards
|
|
deletedShards = append(deletedShards, ecShards)
|
|
deletedShardCount += ecShards.ShardsInfo.Count()
|
|
} else {
|
|
// found, but maybe the actual shard could be missing
|
|
a := actualEcShards.Minus(ecShards)
|
|
if a.ShardsInfo.Count() > 0 {
|
|
newShards = append(newShards, a)
|
|
newShardCount += a.ShardsInfo.Count()
|
|
}
|
|
d := ecShards.Minus(actualEcShards)
|
|
if d.ShardsInfo.Count() > 0 {
|
|
deletedShards = append(deletedShards, d)
|
|
deletedShardCount += d.ShardsInfo.Count()
|
|
}
|
|
}
|
|
|
|
if (newShardCount - deletedShardCount) != 0 {
|
|
disk.UpAdjustDiskUsageDelta(types.ToDiskType(ecShards.DiskType), &DiskUsageCounts{
|
|
ecShardCount: int64(newShardCount - deletedShardCount),
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
existingKeys := make(map[ecShardKey]struct{}, len(existingEcShards))
|
|
for _, ev := range existingEcShards {
|
|
existingKeys[ecShardKey{vid: ev.VolumeId, diskId: types.DiskId(ev.DiskId)}] = struct{}{}
|
|
}
|
|
for key, ecShards := range actualByKey {
|
|
if _, found := existingKeys[key]; found {
|
|
continue
|
|
}
|
|
|
|
newShards = append(newShards, ecShards)
|
|
|
|
disk := dn.getOrCreateDisk(ecShards.DiskType)
|
|
disk.UpAdjustDiskUsageDelta(types.ToDiskType(ecShards.DiskType), &DiskUsageCounts{
|
|
ecShardCount: int64(ecShards.ShardsInfo.Count()),
|
|
})
|
|
}
|
|
|
|
if len(newShards) > 0 || len(deletedShards) > 0 {
|
|
// if changed, set to the new ec shard map
|
|
dn.doUpdateEcShardsLocked(actualByKey)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// getEcShardsLocked returns the flat list of per-physical-disk EC shard
|
|
// entries across all children. Caller MUST hold dn.Lock (or RLock); each
|
|
// disk's ecShardsLock is taken internally via Disk.GetEcShards.
|
|
func (dn *DataNode) getEcShardsLocked() (ret []*erasure_coding.EcVolumeInfo) {
|
|
for _, c := range dn.children {
|
|
disk := c.(*Disk)
|
|
ret = append(ret, disk.GetEcShards()...)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func (dn *DataNode) HasEcShards(volumeId needle.VolumeId) (found bool) {
|
|
dn.RLock()
|
|
defer dn.RUnlock()
|
|
for _, c := range dn.children {
|
|
disk := c.(*Disk)
|
|
disk.ecShardsLock.RLock()
|
|
byDisk, ok := disk.ecShards[volumeId]
|
|
has := ok && len(byDisk) > 0
|
|
disk.ecShardsLock.RUnlock()
|
|
if has {
|
|
return true
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// HasEcShard reports whether any disk on this node still holds the shard.
|
|
// Registration lookups key on the node alone, so a shard moving between its
|
|
// disks must not drop the node from the lookup while a copy remains.
|
|
func (dn *DataNode) HasEcShard(volumeId needle.VolumeId, shardId erasure_coding.ShardId) (found bool) {
|
|
dn.RLock()
|
|
defer dn.RUnlock()
|
|
for _, c := range dn.children {
|
|
disk := c.(*Disk)
|
|
disk.ecShardsLock.RLock()
|
|
for _, ev := range disk.ecShards[volumeId] {
|
|
if ev.ShardsInfo.Has(shardId) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
disk.ecShardsLock.RUnlock()
|
|
if found {
|
|
return true
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// doUpdateEcShardsLocked rewrites disk.ecShards from actualByKey. Caller
|
|
// MUST hold dn.Lock; each disk's ecShardsLock is taken internally around
|
|
// the read-modify-write of its ecShards map.
|
|
func (dn *DataNode) doUpdateEcShardsLocked(actualByKey map[ecShardKey]*erasure_coding.EcVolumeInfo) {
|
|
for _, c := range dn.children {
|
|
disk := c.(*Disk)
|
|
disk.ecShardsLock.Lock()
|
|
disk.ecShards = make(map[needle.VolumeId]map[types.DiskId]*erasure_coding.EcVolumeInfo)
|
|
disk.ecShardsLock.Unlock()
|
|
}
|
|
for _, shard := range actualByKey {
|
|
disk := dn.getOrCreateDisk(shard.DiskType)
|
|
disk.ecShardsLock.Lock()
|
|
byDisk, ok := disk.ecShards[shard.VolumeId]
|
|
if !ok {
|
|
byDisk = make(map[types.DiskId]*erasure_coding.EcVolumeInfo, 1)
|
|
disk.ecShards[shard.VolumeId] = byDisk
|
|
}
|
|
byDisk[types.DiskId(shard.DiskId)] = shard
|
|
disk.ecShardsLock.Unlock()
|
|
}
|
|
}
|
|
|
|
func (dn *DataNode) DeltaUpdateEcShards(newShards, deletedShards []*erasure_coding.EcVolumeInfo) {
|
|
for _, newShard := range newShards {
|
|
dn.AddOrUpdateEcShard(newShard)
|
|
}
|
|
|
|
for _, deletedShard := range deletedShards {
|
|
dn.DeleteEcShard(deletedShard)
|
|
}
|
|
|
|
}
|
|
|
|
func (dn *DataNode) AddOrUpdateEcShard(s *erasure_coding.EcVolumeInfo) {
|
|
disk := dn.getOrCreateDisk(s.DiskType)
|
|
disk.AddOrUpdateEcShard(s)
|
|
}
|
|
|
|
func (dn *DataNode) DeleteEcShard(s *erasure_coding.EcVolumeInfo) {
|
|
disk := dn.getOrCreateDisk(s.DiskType)
|
|
disk.DeleteEcShard(s)
|
|
}
|
|
|
|
func (dn *DataNode) HasVolumesById(volumeId needle.VolumeId) (hasVolumeId bool) {
|
|
|
|
dn.RLock()
|
|
defer dn.RUnlock()
|
|
for _, c := range dn.children {
|
|
disk := c.(*Disk)
|
|
if disk.HasVolumesById(volumeId) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
|
|
}
|