Files
seaweedfs/weed/command/mount_windows.go
T
Chris LuandGitHub 214d3599d3 windows mount: cache file data, resolved paths and attributes (#10703)
* benchmark tool for mounted filesystems

* ci: on-demand mount benchmark, native WinFsp vs rclone plus a Linux reference

* windows mount: let the Windows cache manager cache file data

WinFsp only turns the cache manager on for a file when FileInfoTimeout
is infinite; at any finite value every application read and write is a
synchronous trip into the mount process at whatever size the application
issued. Metadata events already reach FspFileSystemNotify, which purges
a changed file's cached pages and attributes, so an infinite timeout
stays coherent. The dir listing, volume info and EA timeouts are pinned
to one second so they do not silently inherit the infinity.

* windows mount: cache resolved paths and attributes in the adapter

WinFsp addresses every operation by path and has no FORGET, so the
adapter walked the whole path through Lookup on each one, and in a
directory the filer has not listed yet every walk was a filer round
trip; nothing played the part of the kernel's dentry and attribute
caches. The path cache owns one lookup reference per entry the way the
kernel holds one until FORGET, serves attribute reads for files without
an open handle, and is purged by the mount's own mutations and by
metadata events, with the timeout as backstop.

* windows mount: keep a closed file's attributes cached

Open steals the path's cache entry for its handle and Release returned
the reference with a purge, so the stat that follows every copied file
walked to the filer again. Reading the handle's final attributes before
it goes away and moving the reference back into the cache serves that
stat locally, the way the kernel's attribute cache does after a close.

Only if the path still names that inode, though: WinFsp reports the
path the handle opened with, and after a delete-on-close or a rename
caching it would resurrect an entry that is gone.

* windows mount: persist entries at create, and let the flush stay at close

WinFsp posts the cleanup and close that carry the flush after
CloseHandle has returned, so deferring the filer entry to the flush let
everything that reads through the filer race an unflushed close: a
listing missed just-written files, and a directory rename moved a
directory on the filer before its newest child existed there, leaving
the straggler flush to recreate the child under the dead path.

Flush-at-cleanup is not the answer either: it makes every handle's
cleanup flush, and those flushes race the unlinks of delete-on-close,
re-inserting the entry the unlink just removed. Persisting the entry at
create takes the ordering question away.

* mount: flush written pages before a truncate shrinks past them

The shrink trims chunks, but written pages that have not become chunks
yet are invisible to it, so the next flush wrote them back and the file
grew again, resurrecting the truncated bytes. Windows hits this on
every write-then-shrink because its flush runs after CloseHandle, but
the gap is platform-neutral.

* mount: order a file's unlink against its in-flight flush

Unlink set the handle's deleted flag bare, so a flush already past its
own check of that flag wrote the entry back right after the delete
removed it, and a delete-on-close file outlived its last handle. The
flag is now set under the handle's flush lock and re-checked under it,
so a flush either completes before the delete or sees the flag and
skips. An eagerly created handle also starts clean: the dirty mark
existed to make the deferred filer create happen at flush, and eager
creates have nothing to flush.
2026-08-10 18:46:18 -07:00

198 lines
6.7 KiB
Go

package command
import (
"fmt"
"net"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/mount/meta_cache"
"github.com/seaweedfs/seaweedfs/weed/mount/winfsp"
"github.com/seaweedfs/seaweedfs/weed/util"
"github.com/seaweedfs/seaweedfs/weed/util/grace"
"github.com/seaweedfs/seaweedfs/weed/util/version"
)
// ownedByMounter reports the mount root as belonging to whoever started it,
// matching the uid=-1 option handed to WinFsp. It is a display value only:
// WinFsp substitutes the calling user, and it must never be persisted, since
// every other client would read the entry as owned by uid 4294967295.
const ownedByMounter = ^uint32(0)
// windowsCacheTimeout bounds how long the adapter serves cached paths and
// attributes and WinFsp serves cached directory listings, matching what the
// kernel caches would be allowed on a unix mount. Metadata events purge
// entries earlier; this is the backstop for anything an event misses.
const windowsCacheTimeout = time.Second
func RunMount(option *MountOptions, umask os.FileMode) bool {
chunkSizeLimitMB := *mountOptions.chunkSizeLimitMB
if chunkSizeLimitMB <= 0 {
fmt.Printf("Please specify a reasonable buffer size.\n")
return false
}
dir := *option.dir
if dir == "" {
fmt.Printf("Please specify the mount point via \"-dir\", for example -dir=S:\n")
return false
}
// A drive letter or a not-yet-existing directory is what WinFsp wants, so
// the mount point deliberately goes through none of the unix preparation:
// no ResolvePath, no auto-create, no stat.
if err := checkWindowsMountPoint(dir); err != nil {
fmt.Printf("%v\n", err)
return false
}
filerAddresses, grpcDialOption, cipher, bucketRootPath, ok := connectToFiler(option)
if !ok {
return true
}
if *option.localSocket == "" {
mountDirHash := util.HashToInt32([]byte(dir))
if mountDirHash < 0 {
mountDirHash = -mountDirHash
}
*option.localSocket = filepath.Join(os.TempDir(), fmt.Sprintf("seaweedfs-mount-%d.sock", mountDirHash))
}
if err := os.Remove(*option.localSocket); err != nil && !os.IsNotExist(err) {
glog.Fatalf("Failed to remove %s, error: %s", *option.localSocket, err.Error())
}
mountSocketListener, err := net.Listen("unix", *option.localSocket)
if err != nil {
glog.Fatalf("Failed to listen on %s: %v", *option.localSocket, err)
}
uidGidMapper, err := meta_cache.NewUidGidMapper(*option.uidMap, *option.gidMap)
if err != nil {
fmt.Printf("failed to parse %s %s: %v\n", *option.uidMap, *option.gidMap, err)
return false
}
mountRoot := resolveMountRoot(*option.filerMountRootPath)
cacheDirForRead, cacheDirForWrite := resolveCacheDirs(option)
seaweedFileSystem := buildSeaweedFileSystem(option, fileSystemParams{
dir: dir,
mountRoot: mountRoot,
filerAddresses: filerAddresses,
grpcDialOption: grpcDialOption,
cipher: cipher,
uidGidMapper: uidGidMapper,
uid: uint32(*option.windowsUid),
gid: uint32(*option.windowsGid),
mountMode: os.ModeDir | 0777,
mountCtime: time.Now(),
umask: umask,
chunkSizeLimitMB: chunkSizeLimitMB,
cacheDirForRead: cacheDirForRead,
cacheDirForWrite: cacheDirForWrite,
// WinFsp posts the cleanup and close that carry the flush after
// CloseHandle has already returned, so entry creation cannot wait
// for the flush the way it does when close(2) runs it synchronously.
eagerFilerCreate: true,
})
if !createMountRoot(seaweedFileSystem, mountRoot, bucketRootPath, filerAddresses) {
return false
}
host := winfsp.New(seaweedFileSystem, winfsp.Options{
VolumeName: strings.ReplaceAll(*option.filer, ",", "+"),
Uid: ownedByMounter,
Gid: ownedByMounter,
CacheTimeout: windowsCacheTimeout,
ReadOnly: *option.readOnly,
Debug: *option.debugFuse,
ExtraOptions: option.extraOptions,
})
// Windows caches entries on its own side and the mount cannot invalidate
// that cache directly, so changes made elsewhere have to be pushed out.
host.Notify(seaweedFileSystem)
grace.OnInterrupt(func() {
// The signal handler exits the process as soon as the hooks return, so
// anything still queued has to be flushed here rather than after Serve.
// WaitForAsyncFlush is idempotent, so the post-Serve call below is
// harmless if both run.
host.Unmount()
seaweedFileSystem.WaitForAsyncFlush()
})
serveMountGrpc(seaweedFileSystem, mountSocketListener)
if err := seaweedFileSystem.StartBackgroundTasks(); err != nil {
fmt.Printf("failed to start background tasks: %v\n", err)
return false
}
glog.V(0).Infof("mounting %s%s to %v", *option.filer, mountRoot, dir)
glog.V(0).Infof("This is SeaweedFS version %s %s %s", version.Version(), runtime.GOOS, runtime.GOARCH)
glog.V(0).Infof("Windows mount is beta: hard links are unavailable and byte-range locks are not shared across mounts")
if err := host.Serve(windowsMountPoint(dir)); err != nil {
glog.Errorf("%v", err)
return false
}
seaweedFileSystem.WaitForAsyncFlush()
seaweedFileSystem.ClearCacheDir()
return true
}
// checkWindowsMountPoint rejects the mount points WinFsp cannot take, which is
// worth doing up front because its own failure is a bare false.
func checkWindowsMountPoint(dir string) error {
if isDriveLetter(dir) {
if _, err := os.Stat(strings.TrimRight(dir, `\/`) + `\`); err == nil {
return fmt.Errorf("drive %s is already in use", dir)
}
return nil
}
if strings.HasPrefix(dir, `\\`) {
return nil
}
// WinFsp creates the directory itself, with FILE_CREATE, and deletes it
// again when the filesystem goes away. An existing one — empty or not —
// fails with "mount point in use".
if _, err := os.Stat(dir); err == nil {
return fmt.Errorf("mount point %s already exists; WinFsp creates the directory itself, so give it a path that does not exist yet", dir)
} else if !os.IsNotExist(err) {
return err
}
if _, err := os.Stat(filepath.Dir(dir)); err != nil {
return fmt.Errorf("parent of mount point %s does not exist", dir)
}
return nil
}
// windowsMountPoint drops a trailing separator from a drive letter. WinFsp
// recognises a drive only as exactly two characters; "S:\\" falls through to
// its directory handling and the mount fails.
func windowsMountPoint(dir string) string {
if isDriveLetter(dir) {
return strings.TrimRight(dir, `\/`)
}
return dir
}
// isDriveLetter accepts both "S:" and "S:\\"; the trailing separator is how
// the drive is usually written, and windowsMountPoint normalises it.
func isDriveLetter(dir string) bool {
trimmed := strings.TrimRight(dir, `\/`)
if len(trimmed) != 2 || trimmed[1] != ':' {
return false
}
c := trimmed[0]
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
}