mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-19 12:34:29 +02:00
* ec: compare live idx NeedleId sets before encode Refuse encode when replicas' live .idx NeedleId sets differ. Walk only live rows (skip deleted and Size<=0) and compare NeedleId, ignoring Size so MIME/flags drift does not abort a matching id set. Mark the volume read-only for encode and reject writes after that. * fix(ec): compare all replicas then pick capacity source Log matching live idx sets with "replicas are consistent" for the shell CI check. After readonly, compare every replica's live NeedleId set, then require the encode source to have FreeVolumeCount>=2. Empty live-idx vs non-empty refuses encode instead of skipping superblock stubs. Drop the FileCount rebind after the idx gate. * volume: mark readonly under the write lock so checked appends finish first * ec: fix readonly deadlock and restore worker-host source preference MarkVolumeReadonly held dataFileAccessLock while calling reopenIdxForWrite, which re-acquires the same mutex and deadlocks TestMarkVolumeReadonlyCanDelete. Split out reopenIdxForWriteLocked for callers already holding the lock, matching the existing Locked pattern. The worker task lost its worker-host source preference when the idx gate replaced the replica union. Late-bind the source to a replica on the worker host using only the checked location set, with no FileCount rebinding. Drop the now-unused helpers, trim comments added by the earlier commits, and rename tests whose names still claimed convergence while asserting refusal. --------- Co-authored-by: Chepurnov Simon <195125934+CHEPURNOV-S-S@users.noreply.github.com> Co-authored-by: Devin <devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Chris Lu <chris.lu@gmail.com>
367 lines
12 KiB
Go
367 lines
12 KiB
Go
// Package volume_replica reconciles regular (non-EC) volume replicas: it reads
|
|
// per-replica status, builds the union of all live entries onto the most-complete
|
|
// replica, and returns that replica. It is shared by the shell (volume.tier.move,
|
|
// ec.encode, replica check) and the EC encode worker so a stale replica is never
|
|
// used as the basis of an operation.
|
|
package volume_replica
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"sync"
|
|
"time"
|
|
|
|
"google.golang.org/grpc"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/operation"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle_map"
|
|
"github.com/seaweedfs/seaweedfs/weed/wdclient"
|
|
)
|
|
|
|
// ReplicaStatus is the observed state of one volume replica.
|
|
type ReplicaStatus struct {
|
|
Location wdclient.Location
|
|
FileCount uint64
|
|
FileDeletedCount uint64
|
|
VolumeSize uint64
|
|
IsReadOnly bool
|
|
Error error
|
|
}
|
|
|
|
// GetReplicaStatus retrieves the current status of a single volume replica.
|
|
func GetReplicaStatus(grpcDialOption grpc.DialOption, vid needle.VolumeId, location wdclient.Location) ReplicaStatus {
|
|
status := ReplicaStatus{Location: location}
|
|
err := operation.WithVolumeServerClient(false, location.ServerAddress(), grpcDialOption, func(volumeServerClient volume_server_pb.VolumeServerClient) error {
|
|
resp, reqErr := volumeServerClient.VolumeStatus(context.Background(), &volume_server_pb.VolumeStatusRequest{
|
|
VolumeId: uint32(vid),
|
|
})
|
|
if reqErr != nil {
|
|
return reqErr
|
|
}
|
|
if resp != nil {
|
|
status.FileCount = resp.FileCount
|
|
status.FileDeletedCount = resp.FileDeletedCount
|
|
status.VolumeSize = resp.VolumeSize
|
|
status.IsReadOnly = resp.IsReadOnly
|
|
}
|
|
return nil
|
|
})
|
|
status.Error = err
|
|
return status
|
|
}
|
|
|
|
// GetReplicaStatuses retrieves status for all replicas of a volume in parallel.
|
|
func GetReplicaStatuses(grpcDialOption grpc.DialOption, vid needle.VolumeId, locations []wdclient.Location) []ReplicaStatus {
|
|
statuses := make([]ReplicaStatus, len(locations))
|
|
var wg sync.WaitGroup
|
|
for i, location := range locations {
|
|
wg.Add(1)
|
|
go func(i int, location wdclient.Location) {
|
|
defer wg.Done()
|
|
statuses[i] = GetReplicaStatus(grpcDialOption, vid, location)
|
|
}(i, location)
|
|
}
|
|
wg.Wait()
|
|
return statuses
|
|
}
|
|
|
|
// ReadNeedleMeta reads a needle's metadata (e.g. AppendAtNs) from a volume server.
|
|
func ReadNeedleMeta(grpcDialOption grpc.DialOption, volumeServer pb.ServerAddress, volumeId uint32, needleValue needle_map.NeedleValue) (resp *volume_server_pb.ReadNeedleMetaResponse, err error) {
|
|
err = operation.WithVolumeServerClient(false, volumeServer, grpcDialOption,
|
|
func(client volume_server_pb.VolumeServerClient) error {
|
|
if resp, err = client.ReadNeedleMeta(context.Background(), &volume_server_pb.ReadNeedleMetaRequest{
|
|
VolumeId: volumeId,
|
|
NeedleId: uint64(needleValue.Key),
|
|
Offset: needleValue.Offset.ToActualOffset(),
|
|
Size: int32(needleValue.Size),
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
)
|
|
return
|
|
}
|
|
|
|
// unionBuilder builds a union replica by copying missing entries from other replicas.
|
|
type unionBuilder struct {
|
|
grpcDialOption grpc.DialOption
|
|
writer io.Writer
|
|
vid needle.VolumeId
|
|
collection string
|
|
}
|
|
|
|
// buildUnionReplica finds the largest replica and copies missing entries from other
|
|
// replicas into it. If excludeFromSelection is non-empty, that server won't be
|
|
// selected as the target but will still be used as a source for missing entries.
|
|
// Returns the location of the union replica (the one that now has all entries).
|
|
func (rub *unionBuilder) buildUnionReplica(locations []wdclient.Location, statuses []ReplicaStatus, excludeFromSelection string) (wdclient.Location, int, error) {
|
|
if len(locations) == 0 {
|
|
return wdclient.Location{}, 0, fmt.Errorf("no replicas available")
|
|
}
|
|
if len(locations) == 1 {
|
|
if locations[0].Url == excludeFromSelection {
|
|
return wdclient.Location{}, 0, fmt.Errorf("only replica is excluded")
|
|
}
|
|
return locations[0], 0, nil
|
|
}
|
|
|
|
// Step 1: Find the largest replica (highest file count) that's not excluded.
|
|
// statuses are supplied by the caller to avoid a redundant status RPC sweep.
|
|
bestIdx := -1
|
|
var bestFileCount uint64
|
|
for i, s := range statuses {
|
|
if s.Error == nil && locations[i].Url != excludeFromSelection {
|
|
if bestIdx == -1 || s.FileCount > bestFileCount {
|
|
bestIdx = i
|
|
bestFileCount = s.FileCount
|
|
}
|
|
}
|
|
}
|
|
|
|
if bestIdx == -1 {
|
|
return wdclient.Location{}, 0, fmt.Errorf("could not find valid replica (all excluded or errored)")
|
|
}
|
|
|
|
bestLocation := locations[bestIdx]
|
|
fmt.Fprintf(rub.writer, "volume %d: selected %s as best replica (file count: %d)\n",
|
|
rub.vid, bestLocation.Url, bestFileCount)
|
|
|
|
// Step 2: Read index database from the best replica
|
|
bestDB := needle_map.NewMemDb()
|
|
if bestDB == nil {
|
|
return wdclient.Location{}, 0, fmt.Errorf("failed to allocate in-memory needle DB")
|
|
}
|
|
defer bestDB.Close()
|
|
|
|
if err := rub.readIndexDatabase(bestDB, bestLocation.ServerAddress()); err != nil {
|
|
return wdclient.Location{}, 0, fmt.Errorf("read index from best replica %s: %w", bestLocation.Url, err)
|
|
}
|
|
|
|
// Step 3: For each other replica (including excluded), find entries missing from best and copy them
|
|
totalSynced := 0
|
|
cutoffFromAtNs := uint64(time.Now().UnixNano())
|
|
|
|
for i, loc := range locations {
|
|
if i == bestIdx {
|
|
continue
|
|
}
|
|
if statuses[i].Error != nil {
|
|
fmt.Fprintf(rub.writer, " skipping %s: %v\n", loc.Url, statuses[i].Error)
|
|
continue
|
|
}
|
|
|
|
otherDB := needle_map.NewMemDb()
|
|
if otherDB == nil {
|
|
fmt.Fprintf(rub.writer, " skipping %s: failed to allocate DB\n", loc.Url)
|
|
continue
|
|
}
|
|
|
|
if err := rub.readIndexDatabase(otherDB, loc.ServerAddress()); err != nil {
|
|
otherDB.Close()
|
|
fmt.Fprintf(rub.writer, " skipping %s: %v\n", loc.Url, err)
|
|
continue
|
|
}
|
|
|
|
var missingNeedles []needle_map.NeedleValue
|
|
otherDB.AscendingVisit(func(nv needle_map.NeedleValue) error {
|
|
if nv.Size.IsDeleted() {
|
|
return nil
|
|
}
|
|
if _, found := bestDB.Get(nv.Key); !found {
|
|
// Skip entries written after sync started to avoid copying in-flight
|
|
// writes. A read-only replica accepts no new writes, so the per-needle
|
|
// metadata RPC is unnecessary then (ec.encode/tier.move mark readonly
|
|
// before syncing).
|
|
if !statuses[i].IsReadOnly {
|
|
if needleMeta, err := ReadNeedleMeta(rub.grpcDialOption, loc.ServerAddress(), uint32(rub.vid), nv); err == nil {
|
|
if needleMeta.AppendAtNs > cutoffFromAtNs {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
missingNeedles = append(missingNeedles, nv)
|
|
}
|
|
return nil
|
|
})
|
|
otherDB.Close()
|
|
|
|
if len(missingNeedles) == 0 {
|
|
continue
|
|
}
|
|
|
|
syncedFromThis := 0
|
|
for _, nv := range missingNeedles {
|
|
needleBlob, err := rub.readNeedleBlob(loc.ServerAddress(), nv)
|
|
if err != nil {
|
|
fmt.Fprintf(rub.writer, " warning: read needle %d from %s: %v\n", nv.Key, loc.Url, err)
|
|
continue
|
|
}
|
|
|
|
if err := rub.writeNeedleBlob(bestLocation.ServerAddress(), nv, needleBlob); err != nil {
|
|
fmt.Fprintf(rub.writer, " warning: write needle %d to %s: %v\n", nv.Key, bestLocation.Url, err)
|
|
continue
|
|
}
|
|
|
|
bestDB.Set(nv.Key, nv.Offset, nv.Size)
|
|
syncedFromThis++
|
|
}
|
|
|
|
if syncedFromThis > 0 {
|
|
fmt.Fprintf(rub.writer, " copied %d entries from %s to %s\n",
|
|
syncedFromThis, loc.Url, bestLocation.Url)
|
|
totalSynced += syncedFromThis
|
|
}
|
|
}
|
|
|
|
return bestLocation, totalSynced, nil
|
|
}
|
|
|
|
func (rub *unionBuilder) readIndexDatabase(db *needle_map.MemDb, server pb.ServerAddress) error {
|
|
return loadReplicaIndex(db, rub.grpcDialOption, rub.vid, rub.collection, server)
|
|
}
|
|
|
|
func loadReplicaIndex(db *needle_map.MemDb, grpcDialOption grpc.DialOption, vid needle.VolumeId, collection string, server pb.ServerAddress) error {
|
|
var buf bytes.Buffer
|
|
|
|
err := operation.WithVolumeServerClient(true, server, grpcDialOption, func(volumeServerClient volume_server_pb.VolumeServerClient) error {
|
|
copyFileClient, err := volumeServerClient.CopyFile(context.Background(), &volume_server_pb.CopyFileRequest{
|
|
VolumeId: uint32(vid),
|
|
Ext: ".idx",
|
|
CompactionRevision: math.MaxUint32,
|
|
StopOffset: math.MaxInt64,
|
|
Collection: collection,
|
|
IsEcVolume: false,
|
|
IgnoreSourceFileNotFound: false,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("start copy: %w", err)
|
|
}
|
|
|
|
for {
|
|
resp, recvErr := copyFileClient.Recv()
|
|
if recvErr == io.EOF {
|
|
break
|
|
}
|
|
if recvErr != nil {
|
|
return fmt.Errorf("receive: %w", recvErr)
|
|
}
|
|
buf.Write(resp.FileContent)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return db.LoadFilterFromReaderAt(bytes.NewReader(buf.Bytes()), true, false)
|
|
}
|
|
|
|
func (rub *unionBuilder) readNeedleBlob(server pb.ServerAddress, nv needle_map.NeedleValue) ([]byte, error) {
|
|
var needleBlob []byte
|
|
err := operation.WithVolumeServerClient(false, server, rub.grpcDialOption, func(client volume_server_pb.VolumeServerClient) error {
|
|
resp, err := client.ReadNeedleBlob(context.Background(), &volume_server_pb.ReadNeedleBlobRequest{
|
|
VolumeId: uint32(rub.vid),
|
|
Offset: nv.Offset.ToActualOffset(),
|
|
Size: int32(nv.Size),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
needleBlob = resp.NeedleBlob
|
|
return nil
|
|
})
|
|
return needleBlob, err
|
|
}
|
|
|
|
func (rub *unionBuilder) writeNeedleBlob(server pb.ServerAddress, nv needle_map.NeedleValue, needleBlob []byte) error {
|
|
return operation.WithVolumeServerClient(false, server, rub.grpcDialOption, func(client volume_server_pb.VolumeServerClient) error {
|
|
_, err := client.WriteNeedleBlob(context.Background(), &volume_server_pb.WriteNeedleBlobRequest{
|
|
VolumeId: uint32(rub.vid),
|
|
NeedleId: uint64(nv.Key),
|
|
Size: int32(nv.Size),
|
|
NeedleBlob: needleBlob,
|
|
})
|
|
return err
|
|
})
|
|
}
|
|
|
|
// SyncAndSelectBestReplica finds the largest replica, copies missing entries from
|
|
// other replicas into it to create a union, then returns this union replica for the
|
|
// operation. If excludeFromSelection is non-empty, that server won't be selected
|
|
// but will still contribute entries. Already-consistent replica sets skip the sync.
|
|
func SyncAndSelectBestReplica(grpcDialOption grpc.DialOption, vid needle.VolumeId, collection string, locations []wdclient.Location, excludeFromSelection string, writer io.Writer) (wdclient.Location, error) {
|
|
if len(locations) == 0 {
|
|
return wdclient.Location{}, fmt.Errorf("no replicas available for volume %d", vid)
|
|
}
|
|
|
|
var checkLocations []wdclient.Location
|
|
for _, loc := range locations {
|
|
if loc.Url != excludeFromSelection {
|
|
checkLocations = append(checkLocations, loc)
|
|
}
|
|
}
|
|
|
|
if len(checkLocations) == 0 {
|
|
return wdclient.Location{}, fmt.Errorf("no replicas available for volume %d after exclusion", vid)
|
|
}
|
|
|
|
if len(checkLocations) == 1 && len(locations) == 1 {
|
|
return checkLocations[0], nil
|
|
}
|
|
|
|
// Check if replicas are already consistent (skip sync if so).
|
|
statuses := GetReplicaStatuses(grpcDialOption, vid, locations)
|
|
var validStatuses []ReplicaStatus
|
|
for _, s := range statuses {
|
|
if s.Error == nil {
|
|
validStatuses = append(validStatuses, s)
|
|
}
|
|
}
|
|
|
|
if len(validStatuses) > 1 {
|
|
allSame := true
|
|
for _, s := range validStatuses[1:] {
|
|
if s.FileCount != validStatuses[0].FileCount {
|
|
allSame = false
|
|
break
|
|
}
|
|
}
|
|
if allSame {
|
|
for _, s := range validStatuses {
|
|
if s.Location.Url != excludeFromSelection {
|
|
fmt.Fprintf(writer, "volume %d: all %d replicas are consistent (file count: %d)\n",
|
|
vid, len(validStatuses), s.FileCount)
|
|
return s.Location, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fmt.Fprintf(writer, "volume %d: replicas are inconsistent, building union...\n", vid)
|
|
|
|
builder := &unionBuilder{
|
|
grpcDialOption: grpcDialOption,
|
|
writer: writer,
|
|
vid: vid,
|
|
collection: collection,
|
|
}
|
|
|
|
unionLocation, totalSynced, err := builder.buildUnionReplica(locations, statuses, excludeFromSelection)
|
|
if err != nil {
|
|
return wdclient.Location{}, fmt.Errorf("failed to build union replica: %w", err)
|
|
}
|
|
|
|
if totalSynced > 0 {
|
|
fmt.Fprintf(writer, "volume %d: added %d entries to union replica %s\n", vid, totalSynced, unionLocation.Url)
|
|
}
|
|
|
|
return unionLocation, nil
|
|
}
|