mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-08 15:41:15 +02:00
* 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.
145 lines
4.8 KiB
Go
145 lines
4.8 KiB
Go
package winfsp
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
cgofuse "github.com/winfsp/cgofuse/fuse"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/mount"
|
|
)
|
|
|
|
// Options are the WinFsp-specific knobs the mount command passes through.
|
|
type Options struct {
|
|
// VolumeName labels the drive in Explorer.
|
|
VolumeName string
|
|
|
|
// Uid and Gid are reported for every entry. WinFsp overrides what is
|
|
// reported anyway (see the uid=-1 option below), so these are what gets
|
|
// written to the filer and read by every other client.
|
|
Uid uint32
|
|
Gid uint32
|
|
|
|
// CacheTimeout bounds how long resolved paths and attributes may be
|
|
// served from the adapter's cache, and how long WinFsp may serve cached
|
|
// directory listings. Metadata events shorten it by purging; nothing
|
|
// else invalidates these caches.
|
|
CacheTimeout time.Duration
|
|
|
|
// ReadOnly rejects every modification. WinFsp has no "ro" option — it
|
|
// discards the flag and leaves the volume writable — so the refusal has
|
|
// to happen in the operations themselves.
|
|
ReadOnly bool
|
|
|
|
// Debug turns on cgofuse's operation trace.
|
|
Debug bool
|
|
|
|
// ExtraOptions are passed through to WinFsp as -o arguments, after the
|
|
// defaults, so they win when they name the same option.
|
|
ExtraOptions []string
|
|
}
|
|
|
|
// Host is a WinFsp mount that has not been started yet.
|
|
type Host struct {
|
|
host *cgofuse.FileSystemHost
|
|
fs *WinFS
|
|
options Options
|
|
}
|
|
|
|
// New wires wfs up to WinFsp. Nothing is mounted until Serve.
|
|
func New(wfs *mount.WFS, options Options) *Host {
|
|
fs := NewWinFS(wfs, options.Uid, options.Gid, options.ReadOnly, options.CacheTimeout)
|
|
host := cgofuse.NewFileSystemHost(fs)
|
|
host.SetCapReaddirPlus(true)
|
|
host.SetUseIno(true)
|
|
return &Host{host: host, fs: fs, options: options}
|
|
}
|
|
|
|
// Notify wires the mount's metadata events to Windows. Called once before
|
|
// Serve; the host has to exist first, which is why it is not done in New.
|
|
func (h *Host) Notify(wfs *mount.WFS) {
|
|
n := ¬ifier{host: h.host, fs: h.fs, mountRoot: wfs.MountRoot()}
|
|
wfs.SetEntryChangeListener(n.notify)
|
|
}
|
|
|
|
// Serve attaches the filesystem at mountPoint, which is a drive letter ("S:"),
|
|
// a directory that does not yet exist, or a UNC path. It blocks until the
|
|
// filesystem is unmounted.
|
|
func (h *Host) Serve(mountPoint string) error {
|
|
opts := []string{
|
|
"-o", "volname=" + h.volumeName(),
|
|
"-o", "uid=-1",
|
|
"-o", "gid=-1",
|
|
// Only an infinite FileInfoTimeout lets the Windows cache manager
|
|
// cache file data; at any finite value every application read and
|
|
// write is a synchronous trip into this process at whatever size the
|
|
// application issued. Remote changes stay visible because every
|
|
// applied metadata event goes through Notify, which purges the file's
|
|
// cached pages along with its attributes.
|
|
//
|
|
// KeepFileCache is deliberately absent: it would keep the cache alive
|
|
// past cleanup, deferring the close — and with it the flush that
|
|
// persists a written file — until Windows reclaims the memory.
|
|
"-o", "FileInfoTimeout=-1",
|
|
// FlushOnCleanup is absent for the same reason: it makes every
|
|
// handle's cleanup flush, and those flushes race the unlinks of
|
|
// delete-on-close. The flush stays at close, which WinFsp runs after
|
|
// CloseHandle has returned; the mount persists entries eagerly at
|
|
// create instead, so nothing that reads through the filer depends on
|
|
// when the flush runs.
|
|
}
|
|
if h.options.CacheTimeout > 0 {
|
|
ms := strconv.FormatInt(h.options.CacheTimeout.Milliseconds(), 10)
|
|
// These would silently inherit the infinite FileInfoTimeout.
|
|
opts = append(opts,
|
|
"-o", "DirInfoTimeout="+ms,
|
|
"-o", "VolumeInfoTimeout="+ms,
|
|
"-o", "EaTimeout="+ms,
|
|
)
|
|
}
|
|
if h.options.Debug {
|
|
opts = append(opts, "-d")
|
|
}
|
|
for _, extra := range h.options.ExtraOptions {
|
|
opts = append(opts, "-o", extra)
|
|
}
|
|
|
|
if err := h.mount(mountPoint, opts); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// mount turns a refusal into an error. cgofuse panics rather than returning
|
|
// when winfsp-x64.dll is missing, which is the most likely reason for a
|
|
// failure here and the one worth naming.
|
|
func (h *Host) mount(mountPoint string, opts []string) (err error) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
err = fmt.Errorf("mounting %s failed (%v); is WinFsp installed?", mountPoint, r)
|
|
}
|
|
}()
|
|
if !h.host.Mount(mountPoint, opts) {
|
|
return fmt.Errorf("WinFsp refused to mount %s; check that WinFsp is installed and the mount point is free", mountPoint)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Unmount detaches the filesystem, releasing a blocked Serve.
|
|
func (h *Host) Unmount() bool {
|
|
return h.host.Unmount()
|
|
}
|
|
|
|
// volumeName keeps the label parseable: WinFsp splits options on commas, so a
|
|
// label carrying one would be cut short and take the rest of the option string
|
|
// with it.
|
|
func (h *Host) volumeName() string {
|
|
name := strings.ReplaceAll(h.options.VolumeName, ",", "+")
|
|
if name == "" {
|
|
return "SeaweedFS"
|
|
}
|
|
return name
|
|
}
|