package erasure_coding import ( "context" "errors" "fmt" "os" "path/filepath" "strconv" "strings" "github.com/seaweedfs/seaweedfs/weed/operation" "github.com/seaweedfs/seaweedfs/weed/pb" "github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb" "google.golang.org/grpc" ) // ErrFullTeardownNotAcked marks a reachable server that completed the delete // RPC but did not report a full teardown (e.g. a pre-upgrade volume server), so // a stale EC generation may remain on it. Callers distinguish this from an // unreachable node (which may recover and be re-swept) with errors.Is. var ErrFullTeardownNotAcked = errors.New("delete did not perform full teardown (pre-upgrade volume server?); a stale EC generation may remain") // UnmountAndDeleteEcShards unmounts then tears down the named EC shards for a // volume on one server. Unmount must precede delete (delete requires the shard // be unmounted); both RPCs are idempotent against missing shards. // // encodeTsNs fences both RPCs: // - 0 selects the server's blanket, generation-independent teardown. This is // the correct choice for a pre-encode or rollback wipe: it clears same- // generation shards (a retried encode's prior attempt shares the job's // generation) and shards whose .vif generation is unreadable (an // interrupted distribute never landed the sidecar) — both of which a fenced // teardown preserves. The blanket path aborts rather than clobber a live // newer mount, and the caller must guarantee no concurrent newer encode of // this volume (e.g. the admin dedupe key, or an operator lock). // - a non-zero value fences the teardown to strictly-older generations, // preserving same-or-newer, generation 0, and an unreadable .vif — for a // stale-worker cleanup that must never wipe a newer run's live shards. // // Returns ErrFullTeardownNotAcked (wrapped, so errors.Is matches) when a // reachable server does not ack the full teardown. // // This is the single teardown primitive shared by the plugin-worker EC task // and the shell ec.encode pre-cleanup, so the fence semantics cannot drift // between the two paths. func UnmountAndDeleteEcShards( ctx context.Context, dialOption grpc.DialOption, server pb.ServerAddress, collection string, volumeID uint32, shardIds []uint32, encodeTsNs int64, ) error { return operation.WithVolumeServerClient(false, server, dialOption, func(client volume_server_pb.VolumeServerClient) error { if _, err := client.VolumeEcShardsUnmount(ctx, &volume_server_pb.VolumeEcShardsUnmountRequest{ VolumeId: volumeID, ShardIds: shardIds, EncodeTsNs: encodeTsNs, }); err != nil { return fmt.Errorf("unmount: %w", err) } resp, err := client.VolumeEcShardsDelete(ctx, &volume_server_pb.VolumeEcShardsDeleteRequest{ VolumeId: volumeID, Collection: collection, ShardIds: shardIds, FullTeardown: true, EncodeTsNs: encodeTsNs, }) if err != nil { return fmt.Errorf("delete: %w", err) } if !resp.GetFullTeardownDone() { return fmt.Errorf("delete on %s: %w", server, ErrFullTeardownNotAcked) } return nil }) } // EcFileGeneration parses the generation of a 2PC-staged .v file: // -1 means the name is not a generation file of base. func EcFileGeneration(name, base string) int64 { suffix, ok := strings.CutPrefix(name, base+".v") if !ok { return -1 } generation, err := strconv.ParseInt(suffix, 10, 64) if err != nil || generation <= 0 { return -1 } return generation } // RemoveEcGenerationFiles removes 2PC generation files staged under base: // .ecNN.v, .ecx.v, .ecj.v, .ecsum.v and // .vif.v. generationsOlderThan == 0 removes every generation; // otherwise only generations strictly below it. Returns the first real // removal failure. func RemoveEcGenerationFiles(baseFileName string, generationsOlderThan uint32) error { var firstErr error record := func(err error) { if err != nil && firstErr == nil { firstErr = err } } dir, fileName := filepath.Dir(baseFileName), filepath.Base(baseFileName) ecPrefix, vifName := fileName+".ec", fileName+".vif" entries, err := os.ReadDir(dir) if err != nil { if os.IsNotExist(err) { return nil } return err } for _, entry := range entries { name := entry.Name() // A generation file is .v; the last dot separates the // staged-generation suffix from the artifact name. artifact := name[:max(strings.LastIndexByte(name, '.'), 0)] if artifact != vifName && !strings.HasPrefix(artifact, ecPrefix) { continue } generation := EcFileGeneration(name, artifact) if generation < 0 || (generationsOlderThan > 0 && generation >= int64(generationsOlderThan)) { continue } if err := os.Remove(filepath.Join(dir, name)); err != nil && !os.IsNotExist(err) { record(err) } } return firstErr }