Files
seaweedfs/weed/command/mount_common_test.go
T
Chris LuandClaude Opus 5.5 b11af30cb1 mount: remove the local control socket when the mount exits (#12560)
* mount: remove the local control socket when the mount exits

weed mount listened on /tmp/seaweedfs-mount-<hash>.sock and removed it only
when a later mount of the same directory started, so every unmount or
SIGTERM left one socket file behind. Remove it on the normal return path and
in the interrupt hook, unless a later mount of the same directory has
already replaced it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* mount: remove the control socket before a failed mount exits

glog.Fatalf and glog.Exitf call os.Exit, which skips the deferred removal,
so a mount that failed after creating its socket (mount point not
available, fusermount failure) still left it behind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* mount: register the socket cleanup hook before mount setup

A SIGTERM that arrived while the mount was still setting up, for example
waiting on the filer to create the mount root, exited through grace without
running the deferred cleanup. Registering the hook right after the listener
is created makes it run last, after the unmount and filesystem hooks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 10:11:58 -08:00

258 lines
8.1 KiB
Go

//go:build linux || darwin || freebsd || windows
package command
import (
"bytes"
"context"
"net"
"os"
"path/filepath"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
)
func Test_volumeName(t *testing.T) {
tests := []struct {
name string
filer string
filerMountRootPath string
dir string
override string
expected string
}{
{
name: "an override outranks the mounted path and the mount point",
filer: "127.0.0.1:8888",
filerMountRootPath: "/buckets/videos",
dir: `\\seaweedfs\Images`,
override: "MyDisk",
expected: "MyDisk",
},
{
name: "an override still gets commas replaced",
filer: "127.0.0.1:8888",
filerMountRootPath: "/",
override: "My, Disk",
expected: "My+ Disk",
},
{
name: "a drive letter leaves only the filer to fall back on",
filer: "127.0.0.1:8888",
filerMountRootPath: "/",
dir: "S:",
expected: "127.0.0.1:8888",
},
{
name: "empty path falls back to the filer",
filer: "127.0.0.1:8888",
filerMountRootPath: "",
expected: "127.0.0.1:8888",
},
{
name: "several filers stay parseable as one option",
filer: "127.0.0.1:8888,127.0.0.1:8889",
filerMountRootPath: "/",
expected: "127.0.0.1:8888+127.0.0.1:8889",
},
{
name: "a whole-tree mount takes the name of its network share",
filer: "127.0.0.1:8888",
filerMountRootPath: "/",
dir: `\\seaweedfs\Images`,
expected: "Images",
},
{
name: "a whole-tree mount takes the name of its directory",
filer: "127.0.0.1:8888",
filerMountRootPath: "/",
dir: "/mnt/seaweedfs",
expected: "seaweedfs",
},
{
name: "the mounted path outranks the mount point",
filer: "127.0.0.1:8888",
filerMountRootPath: "/buckets/videos",
dir: "/mnt/seaweedfs",
expected: "videos",
},
{
name: "a relative mount point is not a name",
filer: "127.0.0.1:8888",
filerMountRootPath: "/",
dir: ".",
expected: "127.0.0.1:8888",
},
{
name: "mounted directory names the disk",
filer: "127.0.0.1:8888",
filerMountRootPath: "/buckets/images",
expected: "images",
},
{
name: "trailing slash is not a name",
filer: "127.0.0.1:8888",
filerMountRootPath: "/buckets/videos/",
expected: "videos",
},
{
name: "spaces are kept, commas are not",
filer: "127.0.0.1:8888",
filerMountRootPath: "/Image, Disk",
expected: "Image+ Disk",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := volumeName(tt.filer, tt.filerMountRootPath, tt.dir, tt.override); got != tt.expected {
t.Errorf("volumeName(%q, %q, %q, %q) = %q, want %q", tt.filer, tt.filerMountRootPath, tt.dir, tt.override, got, tt.expected)
}
})
}
}
type allowEmptyFoldersInnerClient struct {
filer_pb.SeaweedFilerClient
entry *filer_pb.Entry
beforeUpdate func()
lookups int
updateAttempts int
updates []*filer_pb.Entry
}
func (c *allowEmptyFoldersInnerClient) LookupDirectoryEntry(_ context.Context, _ *filer_pb.LookupDirectoryEntryRequest, _ ...grpc.CallOption) (*filer_pb.LookupDirectoryEntryResponse, error) {
c.lookups++
return &filer_pb.LookupDirectoryEntryResponse{Entry: proto.Clone(c.entry).(*filer_pb.Entry)}, nil
}
func (c *allowEmptyFoldersInnerClient) UpdateEntry(_ context.Context, in *filer_pb.UpdateEntryRequest, _ ...grpc.CallOption) (*filer_pb.UpdateEntryResponse, error) {
c.updateAttempts++
if c.beforeUpdate != nil {
c.beforeUpdate()
}
for key, expected := range in.ExpectedExtended {
actual, found := c.entry.Extended[key]
if found && !bytes.Equal(actual, expected) || !found && len(expected) > 0 {
return nil, status.Error(codes.FailedPrecondition, "extended attribute changed")
}
}
c.entry = proto.Clone(in.Entry).(*filer_pb.Entry)
c.updates = append(c.updates, in.Entry)
return &filer_pb.UpdateEntryResponse{}, nil
}
type allowEmptyFoldersFilerClient struct {
inner *allowEmptyFoldersInnerClient
}
func (c *allowEmptyFoldersFilerClient) WithFilerClient(_ bool, fn func(filer_pb.SeaweedFilerClient) error) error {
return fn(c.inner)
}
func (c *allowEmptyFoldersFilerClient) AdjustedUrl(_ *filer_pb.Location) string { return "" }
func (c *allowEmptyFoldersFilerClient) GetDataCenter() string { return "" }
func Test_ensureBucketAllowEmptyFolders(t *testing.T) {
bucketEntry := func(attrValue string) *filer_pb.Entry {
entry := &filer_pb.Entry{Name: "b1", IsDirectory: true, Extended: map[string][]byte{}}
if attrValue != "" {
entry.Extended[s3_constants.ExtAllowEmptyFolders] = []byte(attrValue)
}
return entry
}
tests := []struct {
name string
mountRoot string
attrValue string
beforeUpdate func(inner *allowEmptyFoldersInnerClient)
wantLookups int
wantUpdates int
wantAttrValue string
}{
{name: "no policy allows empty folders", mountRoot: "/buckets/b1", wantLookups: 1, wantUpdates: 1, wantAttrValue: "true"},
{name: "explicit true is kept", mountRoot: "/buckets/b1", attrValue: "true", wantLookups: 1, wantUpdates: 0},
{name: "explicit false is kept", mountRoot: "/buckets/b1", attrValue: "false", wantLookups: 1, wantUpdates: 0},
{name: "subdirectory mount leaves the bucket alone", mountRoot: "/buckets/b1/sub", wantLookups: 0, wantUpdates: 0},
{
name: "a concurrent false wins over the mount default",
mountRoot: "/buckets/b1",
beforeUpdate: func(inner *allowEmptyFoldersInnerClient) {
inner.entry.Extended[s3_constants.ExtAllowEmptyFolders] = []byte("false")
},
wantLookups: 2,
wantUpdates: 0,
wantAttrValue: "false",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
inner := &allowEmptyFoldersInnerClient{entry: bucketEntry(tt.attrValue)}
if tt.beforeUpdate != nil {
inner.beforeUpdate = func() { tt.beforeUpdate(inner) }
}
filerClient := &allowEmptyFoldersFilerClient{inner: inner}
if err := ensureBucketAllowEmptyFolders(context.Background(), filerClient, tt.mountRoot, "/buckets"); err != nil {
t.Fatalf("ensureBucketAllowEmptyFolders: %v", err)
}
if inner.lookups != tt.wantLookups {
t.Errorf("lookups = %d, want %d", inner.lookups, tt.wantLookups)
}
if len(inner.updates) != tt.wantUpdates {
t.Fatalf("updates = %d, want %d", len(inner.updates), tt.wantUpdates)
}
if tt.wantAttrValue != "" {
got := string(inner.entry.Extended[s3_constants.ExtAllowEmptyFolders])
if got != tt.wantAttrValue {
t.Errorf("%s = %q, want %q", s3_constants.ExtAllowEmptyFolders, got, tt.wantAttrValue)
}
}
})
}
}
func Test_mountSocketCleanup(t *testing.T) {
dir, err := os.MkdirTemp("", "swmnt")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { os.RemoveAll(dir) })
socketPath := filepath.Join(dir, "mount.sock")
listen := func() {
l, err := net.Listen("unix", socketPath)
if err != nil {
t.Skipf("unix sockets unavailable: %v", err)
}
t.Cleanup(func() { l.Close() })
}
exists := func() bool {
_, err := os.Lstat(socketPath)
return err == nil
}
listen()
removeMountSocket := mountSocketCleanup(socketPath)
removeMountSocket()
removeMountSocket()
if exists() {
t.Fatalf("%s is still there after the mount exited", socketPath)
}
listen()
removeMountSocket = mountSocketCleanup(socketPath)
if err := os.Remove(socketPath); err != nil {
t.Fatal(err)
}
listen()
removeMountSocket()
if !exists() {
t.Fatalf("exiting mount removed the socket of a later mount of the same directory")
}
}