Files
seaweedfs/weed/shell/command_nfs_disable.go
T
Chris Lu 44235b5def shell: serialize inode index row updates during nfs.enable/disable
TraverseBfs runs its callback on five workers, and hard-linked entries share an inode: concurrent KvGet/AddPath/KvPut cycles on the same row could overwrite each other and drop a path. Row updates now take a per-inode striped lock, and progress output takes a mutex.
2026-10-22 02:27:05 -07:00

182 lines
5.2 KiB
Go

package shell
import (
"bytes"
"context"
"errors"
"flag"
"fmt"
"io"
"strings"
"sync/atomic"
"github.com/seaweedfs/seaweedfs/weed/filer"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/util"
"google.golang.org/protobuf/proto"
)
func init() {
Commands = append(Commands, &commandNfsDisable{})
}
type commandNfsDisable struct {
}
func (c *commandNfsDisable) Name() string {
return "nfs.disable"
}
func (c *commandNfsDisable) Help() string {
return `disable NFS export support for a filer path
# preview the filer.conf change
nfs.disable -path=/exports
# persist it and delete the inode index rows under the path
nfs.disable -path=/exports -apply
Clears the inode_index mark that nfs.enable set on the path, then
walks the entries under it and removes their inode->path index rows,
so disabling leaves no index data behind. Pass -keepIndex to skip
the cleanup walk.
`
}
func (c *commandNfsDisable) HasTag(CommandTag) bool {
return false
}
func (c *commandNfsDisable) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
nfsDisableCommand := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
path := nfsDisableCommand.String("path", "", "filer path that nfs.enable was applied to")
apply := nfsDisableCommand.Bool("apply", false, "update filer.conf and delete the inode index rows")
keepIndex := nfsDisableCommand.Bool("keepIndex", false, "keep existing inode index rows")
if err = nfsDisableCommand.Parse(args); err != nil {
return nil
}
if !strings.HasPrefix(*path, "/") {
return fmt.Errorf("-path must be an absolute filer path")
}
fc, err := filer.ReadFilerConf(commandEnv.option.FilerAddress, commandEnv.option.GrpcDialOption, commandEnv.MasterClient)
if err != nil {
return err
}
if locConf, found := fc.GetLocationConf(*path); found && locConf.InodeIndex {
locConf = filer.ClonePathConf(locConf)
locConf.InodeIndex = false
if proto.Equal(locConf, &filer_pb.FilerConf_PathConf{LocationPrefix: *path}) {
fc.DeleteLocationConf(*path)
} else if err = fc.SetLocationConf(locConf); err != nil {
return err
}
} else {
fmt.Fprintf(writer, "inode_index is not set on %s in filer.conf\n", *path)
}
var buf bytes.Buffer
fc.ToText(&buf)
fmt.Fprint(writer, buf.String())
fmt.Fprintln(writer)
if !*apply {
infoAboutSimulationMode(writer, *apply, "-apply")
return nil
}
if err = commandEnv.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
return filer.SaveInsideFiler(context.Background(), client, filer.DirectoryEtcSeaweedFS, filer.FilerConfName, buf.Bytes())
}); err != nil && err != filer_pb.ErrNotFound {
return err
}
if *keepIndex {
return nil
}
return cleanupInodeIndex(commandEnv, writer, util.FullPath(*path))
}
func cleanupInodeIndex(commandEnv *CommandEnv, writer io.Writer, exportPath util.FullPath) error {
var cleared atomic.Int64
var locks inodeRowLocks
if exportPath != "/" {
rootEntry, err := lookupPathEntry(commandEnv, exportPath)
if err == filer_pb.ErrNotFound {
fmt.Fprintf(writer, "%s does not exist; nothing to clean up\n", exportPath)
return nil
}
if err != nil {
return err
}
if err := removeInodeIndexRow(commandEnv, exportPath, rootEntry.GetAttributes().GetInode(), &cleared, &locks); err != nil {
return err
}
}
err := filer_pb.TraverseBfs(context.Background(), commandEnv, exportPath, func(parentPath util.FullPath, entry *filer_pb.Entry) error {
fullPath := util.NewFullPath(string(parentPath), entry.Name)
if err := removeInodeIndexRow(commandEnv, fullPath, entry.GetAttributes().GetInode(), &cleared, &locks); err != nil {
return err
}
return nil
})
if err != nil {
return fmt.Errorf("clean up inode index under %s: %v", exportPath, err)
}
fmt.Fprintf(writer, "cleared %d inode index rows\n", cleared.Load())
return nil
}
func removeInodeIndexRow(commandEnv *CommandEnv, fullPath util.FullPath, inode uint64, cleared *atomic.Int64, locks *inodeRowLocks) error {
if inode == 0 {
return nil
}
key := filer.InodeIndexKey(inode)
lock := locks.forInode(inode)
lock.Lock()
defer lock.Unlock()
err := commandEnv.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
resp, kvErr := client.KvGet(context.Background(), &filer_pb.KvGetRequest{Key: key})
if kvErr != nil {
return kvErr
}
if resp.GetError() != "" {
return errors.New(resp.GetError())
}
if len(resp.GetValue()) == 0 {
return nil
}
record, decodeErr := filer.DecodeInodeIndexRecord(resp.GetValue())
if decodeErr != nil || !record.RemovePath(fullPath) {
return nil
}
// A hard link can keep paths outside the export; only drop the row
// once no path remains. An empty KvPut value deletes the key.
var value []byte
if len(record.Paths) > 0 {
var encodeErr error
if value, encodeErr = record.Encode(); encodeErr != nil {
return encodeErr
}
}
putResp, putErr := client.KvPut(context.Background(), &filer_pb.KvPutRequest{Key: key, Value: value})
if putErr != nil {
return putErr
}
if putResp.GetError() != "" {
return errors.New(putResp.GetError())
}
cleared.Add(1)
return nil
})
if err != nil {
return fmt.Errorf("clear inode %d for %s: %v", inode, fullPath, err)
}
return nil
}