Files
seaweedfs/weed/storage/volume_replica/idx_set.go
T
d04d838ebd ec: compare live idx NeedleId sets before encode (#11812)
* 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>
2026-10-19 00:56:17 -07:00

146 lines
4.8 KiB
Go

package volume_replica
import (
"fmt"
"io"
"google.golang.org/grpc"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/needle_map"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
"github.com/seaweedfs/seaweedfs/weed/wdclient"
)
type replicaLiveIdx struct {
loc wdclient.Location
live []types.NeedleId
}
// LiveNeedlesFromIndex walks a compact needle map and returns live NeedleIds
// in key order. Deleted, invalid-size, and zero-offset rows are skipped; Size
// is not part of the comparison since replicas can differ by MIME/flags while
// holding the same ids.
func LiveNeedlesFromIndex(db *needle_map.MemDb) ([]types.NeedleId, error) {
if db == nil {
return nil, fmt.Errorf("nil needle index")
}
var live []types.NeedleId
err := db.AscendingVisit(func(nv needle_map.NeedleValue) error {
if nv.Offset.IsZero() || nv.Size.IsDeleted() || !nv.Size.IsValid() {
return nil
}
live = append(live, nv.Key)
return nil
})
return live, err
}
// EqualLiveNeedleSets reports whether two live NeedleId sets match.
func EqualLiveNeedleSets(a, b []types.NeedleId) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
// RequireCapacitySource returns loc if it is in capacityLocs (FreeVolumeCount>=2).
func RequireCapacitySource(vid needle.VolumeId, loc wdclient.Location, capacityLocs []wdclient.Location) (wdclient.Location, error) {
if loc.Url == "" {
return wdclient.Location{}, fmt.Errorf("volume %d: no encode source after idx compare", vid)
}
for _, c := range capacityLocs {
if c.Url == loc.Url {
return loc, nil
}
}
return wdclient.Location{}, fmt.Errorf("volume %d: selected source %s is not in the FreeVolumeCount>=2 replica set; refusing encode", vid, loc.Url)
}
// SelectSourceIfIndexesMatch copies each replica's live .idx and compares
// NeedleId sets. A read error or a mismatch aborts. One replica skips the
// comparison. On match, the source is prefer when listed, else the first.
func SelectSourceIfIndexesMatch(grpcDialOption grpc.DialOption, vid needle.VolumeId, collection string, locations []wdclient.Location, prefer string, writer io.Writer) (wdclient.Location, error) {
if len(locations) == 0 {
return wdclient.Location{}, fmt.Errorf("no replicas available for volume %d", vid)
}
if len(locations) == 1 {
if writer != nil {
fmt.Fprintf(writer, "volume %d: single replica %s, skipping idx set compare\n", vid, locations[0].Url)
}
return locations[0], nil
}
views := make([]replicaLiveIdx, 0, len(locations))
for _, loc := range locations {
status := GetReplicaStatus(grpcDialOption, vid, loc)
if status.Error != nil {
return wdclient.Location{}, fmt.Errorf("volume %d: replica status %s: %w", vid, loc.Url, status.Error)
}
db, err := ReadReplicaIndex(grpcDialOption, vid, collection, loc.ServerAddress())
if err != nil {
return wdclient.Location{}, fmt.Errorf("volume %d: read .idx from %s: %w", vid, loc.Url, err)
}
live, err := LiveNeedlesFromIndex(db)
db.Close()
if err != nil {
return wdclient.Location{}, fmt.Errorf("volume %d: walk .idx from %s: %w", vid, loc.Url, err)
}
views = append(views, replicaLiveIdx{loc: loc, live: live})
}
return chooseSourceFromLiveNeedles(vid, views, prefer, writer)
}
func chooseSourceFromLiveNeedles(vid needle.VolumeId, views []replicaLiveIdx, prefer string, writer io.Writer) (wdclient.Location, error) {
if len(views) == 0 {
return wdclient.Location{}, fmt.Errorf("volume %d: no data-bearing replicas to encode", vid)
}
reference := views[0].live
referenceUrl := views[0].loc.Url
for _, view := range views[1:] {
if !EqualLiveNeedleSets(reference, view.live) {
return wdclient.Location{}, fmt.Errorf("volume %d: replica %s live NeedleId set does not match %s (%d vs %d entries); refusing encode (no union)",
vid, view.loc.Url, referenceUrl, len(view.live), len(reference))
}
}
if len(reference) == 0 {
return wdclient.Location{}, fmt.Errorf("volume %d: no data-bearing replicas to encode", vid)
}
if writer != nil {
fmt.Fprintf(writer, "volume %d: all %d data replicas are consistent (matching live idx sets, %d needles)\n",
vid, len(views), len(reference))
}
if prefer != "" {
for _, view := range views {
if view.loc.Url == prefer {
return view.loc, nil
}
}
}
return views[0].loc, nil
}
// ReadReplicaIndex streams a replica's .idx into an in-memory compact map.
func ReadReplicaIndex(grpcDialOption grpc.DialOption, vid needle.VolumeId, collection string, server pb.ServerAddress) (*needle_map.MemDb, error) {
db := needle_map.NewMemDb()
if db == nil {
return nil, fmt.Errorf("failed to allocate in-memory needle DB")
}
if err := loadReplicaIndex(db, grpcDialOption, vid, collection, server); err != nil {
db.Close()
return nil, err
}
return db, nil
}