mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-12 23:09:33 +01:00
* 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>
258 lines
8.1 KiB
Go
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")
|
|
}
|
|
}
|