mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-21 14:46:58 +00:00
* mount: discard a path-cache insert that raced a purge The Windows adapter's walk resolves a component with a Lookup RPC and inserts the result holding no lock, so a purge can land in between - and what the walk just resolved is then the very name the purge removed. Anything opening the old path concurrently with a rename repopulates the cache with the vacated name, which the next stat is served from for up to a second. The release path already guards its equivalent insert; the walk had nothing. The cache counts purges now. A resolve snapshots the generation before its lookups and insert discards the entry when any purge ran in between, parking the reference in the graveyard so the in-flight caller keeps a valid inode either way. Seen once in CI as TestRenameOverExisting failing with 'source survived the rename': every SeaweedFS layer is synchronous with the rename, but a background open of the source - an antivirus scan of the just-written file fits - can requalify the stale name through this window. The assertion also reports what stat returned now, and whether it persisted, so a recurrence indicts a specific layer instead of reading as a mystery. * mount: cover the path-cache discard by key, and let a discard rest Review follow-ups. The generation was global, so any purge between a walk's snapshot and its insert discarded the entry whatever its name - and an open retries resolve-then-steal only four times before failing with EIO, so sustained unrelated churn could fail opens of untouched paths. Purges are remembered by key now and only one that covers the inserted name discards it; past the remembered window the insert is discarded without a check, which only costs a retry. A discard that itself tripped the sweep also handed its own reference straight to forget while the walker was still using the inode. The graveyard holds two generations now, so an appended reference always survives the sweep of the call that appended it - which the displaced-entry and purge paths needed too. Also restores the original path-cache test suite this branch had overwritten instead of extended, and rewords the semantics-test failure so it no longer claims the source survived when stat returned a transient error. * mount: take an open's reference directly instead of stealing it back resolveAndSteal cached the final component only to steal it back, so an open depended on that insert surviving whatever purges raced it - four attempts and then EIO. The keyed purge window narrowed how often an insert is discarded, but past the window the discard is blind again, so the cliff had only moved. A cached entry is still stolen; anything else is now looked up directly, with the caller owning the reference from the start. No retry loop, and no way for churn - covered, unrelated or overflowing the window - to fail an open. Also covers the whole-cache purge: purge of the root with prefix set clears every entry, but the covers check tested for a '/'-prefixed key that a normalised key never has, so it covered no in-flight insert at all.
530 lines
14 KiB
Go
530 lines
14 KiB
Go
package winfsp
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestTruncate(t *testing.T) {
|
|
dir := testRoot(t)
|
|
path := filepath.Join(dir, "resize")
|
|
original := bytes.Repeat([]byte("abcd"), 4096) // 16 KiB
|
|
|
|
if err := os.WriteFile(path, original, 0644); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
|
|
// Shrink.
|
|
if err := os.Truncate(path, 100); err != nil {
|
|
t.Fatalf("truncate down: %v", err)
|
|
}
|
|
got, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read after shrink: %v", err)
|
|
}
|
|
if len(got) != 100 {
|
|
t.Fatalf("size after shrink = %d, want 100", len(got))
|
|
}
|
|
if !bytes.Equal(got, original[:100]) {
|
|
t.Fatal("the surviving prefix does not match the original")
|
|
}
|
|
|
|
// Grow: the new space has to read back as zeros, not stale bytes.
|
|
if err := os.Truncate(path, 8192); err != nil {
|
|
t.Fatalf("truncate up: %v", err)
|
|
}
|
|
got, err = os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read after grow: %v", err)
|
|
}
|
|
if len(got) != 8192 {
|
|
t.Fatalf("size after grow = %d, want 8192", len(got))
|
|
}
|
|
if !bytes.Equal(got[100:], make([]byte, 8192-100)) {
|
|
t.Fatal("extension is not zero-filled")
|
|
}
|
|
|
|
// Truncate to empty.
|
|
if err := os.Truncate(path, 0); err != nil {
|
|
t.Fatalf("truncate to zero: %v", err)
|
|
}
|
|
if info, err := os.Stat(path); err != nil || info.Size() != 0 {
|
|
t.Fatalf("stat after zero truncate: size=%v err=%v", info, err)
|
|
}
|
|
}
|
|
|
|
func TestAppend(t *testing.T) {
|
|
dir := testRoot(t)
|
|
path := filepath.Join(dir, "log")
|
|
|
|
for i := 0; i < 5; i++ {
|
|
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
|
if err != nil {
|
|
t.Fatalf("open for append: %v", err)
|
|
}
|
|
if _, err := fmt.Fprintf(f, "line %d\n", i); err != nil {
|
|
t.Fatalf("append: %v", err)
|
|
}
|
|
if err := f.Close(); err != nil {
|
|
t.Fatalf("close: %v", err)
|
|
}
|
|
}
|
|
|
|
got, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read: %v", err)
|
|
}
|
|
want := "line 0\nline 1\nline 2\nline 3\nline 4\n"
|
|
if string(got) != want {
|
|
t.Fatalf("appended content = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestReadPastEndOfFile(t *testing.T) {
|
|
dir := testRoot(t)
|
|
path := filepath.Join(dir, "short")
|
|
if err := os.WriteFile(path, []byte("12345"), 0644); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
t.Fatalf("open: %v", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
buf := make([]byte, 16)
|
|
n, err := f.ReadAt(buf, 100)
|
|
if n != 0 {
|
|
t.Fatalf("read %d bytes past EOF, want 0", n)
|
|
}
|
|
if err == nil {
|
|
t.Fatal("reading past EOF returned no error, want io.EOF")
|
|
}
|
|
}
|
|
|
|
func TestErrorPaths(t *testing.T) {
|
|
dir := testRoot(t)
|
|
|
|
t.Run("open missing file", func(t *testing.T) {
|
|
_, err := os.Open(filepath.Join(dir, "does-not-exist"))
|
|
if !os.IsNotExist(err) {
|
|
t.Fatalf("got %v, want a not-exist error", err)
|
|
}
|
|
})
|
|
|
|
t.Run("exclusive create over existing", func(t *testing.T) {
|
|
path := filepath.Join(dir, "taken")
|
|
if err := os.WriteFile(path, []byte("x"), 0644); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
_, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
|
|
if !os.IsExist(err) {
|
|
t.Fatalf("got %v, want an already-exists error", err)
|
|
}
|
|
})
|
|
|
|
t.Run("remove missing file", func(t *testing.T) {
|
|
if err := os.Remove(filepath.Join(dir, "never-there")); !os.IsNotExist(err) {
|
|
t.Fatalf("got %v, want a not-exist error", err)
|
|
}
|
|
})
|
|
|
|
t.Run("mkdir under a file", func(t *testing.T) {
|
|
path := filepath.Join(dir, "regular")
|
|
if err := os.WriteFile(path, []byte("x"), 0644); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
if err := os.Mkdir(filepath.Join(path, "child"), 0755); err == nil {
|
|
t.Fatal("created a directory underneath a regular file")
|
|
}
|
|
})
|
|
|
|
t.Run("readdir of a missing directory", func(t *testing.T) {
|
|
if _, err := os.ReadDir(filepath.Join(dir, "no-such-dir")); err == nil {
|
|
t.Fatal("listed a directory that does not exist")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRenameOverExisting(t *testing.T) {
|
|
dir := testRoot(t)
|
|
src := filepath.Join(dir, "src")
|
|
dst := filepath.Join(dir, "dst")
|
|
|
|
if err := os.WriteFile(src, []byte("new"), 0644); err != nil {
|
|
t.Fatalf("write src: %v", err)
|
|
}
|
|
if err := os.WriteFile(dst, []byte("old"), 0644); err != nil {
|
|
t.Fatalf("write dst: %v", err)
|
|
}
|
|
if err := os.Rename(src, dst); err != nil {
|
|
t.Fatalf("rename over existing: %v", err)
|
|
}
|
|
got, err := os.ReadFile(dst)
|
|
if err != nil {
|
|
t.Fatalf("read: %v", err)
|
|
}
|
|
if string(got) != "new" {
|
|
t.Fatalf("target holds %q, want the source content", got)
|
|
}
|
|
if fi, err := os.Stat(src); !os.IsNotExist(err) {
|
|
// Both a stale positive and a transient non-ENOENT error land here, and
|
|
// they indict different layers; a second look says whether it persists.
|
|
time.Sleep(200 * time.Millisecond)
|
|
fi2, err2 := os.Stat(src)
|
|
t.Fatalf("stat of the renamed-away source did not return not-exist: stat=%v err=%v; 200ms later stat=%v err=%v",
|
|
describeFileInfo(fi), err, describeFileInfo(fi2), err2)
|
|
}
|
|
}
|
|
|
|
func TestRenameAcrossDirectories(t *testing.T) {
|
|
dir := testRoot(t)
|
|
from := filepath.Join(dir, "from")
|
|
to := filepath.Join(dir, "to")
|
|
for _, d := range []string{from, to} {
|
|
if err := os.Mkdir(d, 0755); err != nil {
|
|
t.Fatalf("mkdir %s: %v", d, err)
|
|
}
|
|
}
|
|
|
|
src := filepath.Join(from, "file")
|
|
dst := filepath.Join(to, "file")
|
|
if err := os.WriteFile(src, []byte("moved"), 0644); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
if err := os.Rename(src, dst); err != nil {
|
|
t.Fatalf("rename across directories: %v", err)
|
|
}
|
|
got, err := os.ReadFile(dst)
|
|
if err != nil {
|
|
t.Fatalf("read: %v", err)
|
|
}
|
|
if string(got) != "moved" {
|
|
t.Fatalf("content after move = %q", got)
|
|
}
|
|
|
|
// A directory should move too, children and all.
|
|
nested := filepath.Join(from, "sub")
|
|
if err := os.Mkdir(nested, 0755); err != nil {
|
|
t.Fatalf("mkdir sub: %v", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(nested, "child"), []byte("c"), 0644); err != nil {
|
|
t.Fatalf("write child: %v", err)
|
|
}
|
|
if err := os.Rename(nested, filepath.Join(to, "sub")); err != nil {
|
|
t.Fatalf("rename directory: %v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(to, "sub", "child")); err != nil {
|
|
t.Fatalf("child did not come along: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestAwkwardNames covers what has to survive the UTF-16 boundary and the
|
|
// shapes Windows software tends to produce.
|
|
func TestAwkwardNames(t *testing.T) {
|
|
dir := testRoot(t)
|
|
names := []string{
|
|
"café.txt",
|
|
"日本語のファイル.dat",
|
|
"emoji-🐟.bin",
|
|
"with space.txt",
|
|
"with.many.dots.txt",
|
|
"UPPER and lower.TXT",
|
|
"dash-and_underscore.txt",
|
|
"'quoted'.txt",
|
|
"(parens).txt",
|
|
"#hash&.txt",
|
|
strings.Repeat("x", 200) + ".txt",
|
|
}
|
|
for _, name := range names {
|
|
t.Run(name, func(t *testing.T) {
|
|
path := filepath.Join(dir, name)
|
|
want := []byte(name)
|
|
if err := os.WriteFile(path, want, 0644); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
got, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read: %v", err)
|
|
}
|
|
if !bytes.Equal(got, want) {
|
|
t.Fatalf("content = %q, want %q", got, want)
|
|
}
|
|
})
|
|
}
|
|
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
t.Fatalf("readdir: %v", err)
|
|
}
|
|
found := make(map[string]bool, len(entries))
|
|
for _, e := range entries {
|
|
found[e.Name()] = true
|
|
}
|
|
for _, name := range names {
|
|
if !found[name] {
|
|
t.Errorf("%q did not come back from readdir", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDeepDirectoryNesting(t *testing.T) {
|
|
dir := testRoot(t)
|
|
deep := dir
|
|
for i := 0; i < 24; i++ {
|
|
deep = filepath.Join(deep, fmt.Sprintf("level%02d", i))
|
|
}
|
|
if err := os.MkdirAll(deep, 0755); err != nil {
|
|
t.Fatalf("mkdirall depth 24: %v", err)
|
|
}
|
|
leaf := filepath.Join(deep, "leaf.txt")
|
|
if err := os.WriteFile(leaf, []byte("bottom"), 0644); err != nil {
|
|
t.Fatalf("write leaf: %v", err)
|
|
}
|
|
got, err := os.ReadFile(leaf)
|
|
if err != nil {
|
|
t.Fatalf("read leaf: %v", err)
|
|
}
|
|
if string(got) != "bottom" {
|
|
t.Fatalf("leaf content = %q", got)
|
|
}
|
|
}
|
|
|
|
// TestConcurrentSameFile is the interleaving that matters: several handles on
|
|
// one file at once, rather than one writer each on separate files.
|
|
func TestConcurrentSameFile(t *testing.T) {
|
|
dir := testRoot(t)
|
|
path := filepath.Join(dir, "shared")
|
|
|
|
const writers = 4
|
|
const blockSize = 4096
|
|
if err := os.WriteFile(path, make([]byte, writers*blockSize), 0644); err != nil {
|
|
t.Fatalf("preallocate: %v", err)
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
errs := make(chan error, writers)
|
|
for w := 0; w < writers; w++ {
|
|
wg.Add(1)
|
|
go func(w int) {
|
|
defer wg.Done()
|
|
f, err := os.OpenFile(path, os.O_WRONLY, 0644)
|
|
if err != nil {
|
|
errs <- fmt.Errorf("writer %d open: %w", w, err)
|
|
return
|
|
}
|
|
defer f.Close()
|
|
block := bytes.Repeat([]byte{byte('A' + w)}, blockSize)
|
|
if _, err := f.WriteAt(block, int64(w*blockSize)); err != nil {
|
|
errs <- fmt.Errorf("writer %d write: %w", w, err)
|
|
}
|
|
}(w)
|
|
}
|
|
wg.Wait()
|
|
close(errs)
|
|
for err := range errs {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
|
|
got, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read: %v", err)
|
|
}
|
|
if len(got) != writers*blockSize {
|
|
t.Fatalf("size = %d, want %d", len(got), writers*blockSize)
|
|
}
|
|
for w := 0; w < writers; w++ {
|
|
want := bytes.Repeat([]byte{byte('A' + w)}, blockSize)
|
|
if !bytes.Equal(got[w*blockSize:(w+1)*blockSize], want) {
|
|
t.Fatalf("block %d was not written whole", w)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestConcurrentReaders(t *testing.T) {
|
|
dir := testRoot(t)
|
|
path := filepath.Join(dir, "read-me")
|
|
want := contentFor("concurrent-readers", 1<<20)
|
|
if err := os.WriteFile(path, want, 0644); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
|
|
// WriteFile's own existence probe ran while the file did not exist, and
|
|
// WinFsp may serve that answer from its metadata cache for up to the
|
|
// mount's FileInfoTimeout. Establish visibility once before racing the
|
|
// readers, so the race exercises concurrent reading rather than the
|
|
// cache window.
|
|
deadline := time.Now().Add(3 * time.Second)
|
|
for {
|
|
if _, err := os.Stat(path); err == nil {
|
|
break
|
|
} else if time.Now().After(deadline) {
|
|
t.Fatalf("file never became visible: %v", err)
|
|
}
|
|
time.Sleep(50 * time.Millisecond)
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
errs := make(chan error, 8)
|
|
for r := 0; r < 8; r++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
got, err := os.ReadFile(path)
|
|
if err != nil {
|
|
errs <- err
|
|
return
|
|
}
|
|
if !bytes.Equal(got, want) {
|
|
errs <- fmt.Errorf("reader saw %d bytes, want %d", len(got), len(want))
|
|
}
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
close(errs)
|
|
for err := range errs {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
}
|
|
|
|
// Hard links are not something WinFsp offers, so the mount has to refuse them
|
|
// rather than appear to succeed.
|
|
func TestHardLinkUnsupported(t *testing.T) {
|
|
dir := testRoot(t)
|
|
target := filepath.Join(dir, "original")
|
|
if err := os.WriteFile(target, []byte("x"), 0644); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
if err := os.Link(target, filepath.Join(dir, "hardlink")); err == nil {
|
|
t.Fatal("hard link creation reported success")
|
|
}
|
|
}
|
|
|
|
// Symlinks are refused rather than half-supported: WinFsp needs a reparse
|
|
// point to follow one, and an entry it cannot follow reads back empty.
|
|
func TestSymlinkUnsupported(t *testing.T) {
|
|
dir := testRoot(t)
|
|
target := filepath.Join(dir, "target.txt")
|
|
if err := os.WriteFile(target, []byte("pointed at"), 0644); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
if err := os.Symlink(target, filepath.Join(dir, "link.txt")); err == nil {
|
|
t.Fatal("symlink creation reported success")
|
|
}
|
|
}
|
|
|
|
func TestModTimeAdvances(t *testing.T) {
|
|
dir := testRoot(t)
|
|
path := filepath.Join(dir, "touched")
|
|
if err := os.WriteFile(path, []byte("one"), 0644); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
first, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatalf("stat: %v", err)
|
|
}
|
|
|
|
time.Sleep(1100 * time.Millisecond)
|
|
if err := os.WriteFile(path, []byte("two"), 0644); err != nil {
|
|
t.Fatalf("rewrite: %v", err)
|
|
}
|
|
second, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatalf("stat again: %v", err)
|
|
}
|
|
if !second.ModTime().After(first.ModTime()) {
|
|
t.Fatalf("mtime did not advance: %v then %v", first.ModTime(), second.ModTime())
|
|
}
|
|
}
|
|
|
|
func TestChtimes(t *testing.T) {
|
|
dir := testRoot(t)
|
|
path := filepath.Join(dir, "dated")
|
|
if err := os.WriteFile(path, []byte("x"), 0644); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
want := time.Date(2020, 3, 4, 5, 6, 7, 0, time.UTC)
|
|
if err := os.Chtimes(path, want, want); err != nil {
|
|
t.Fatalf("chtimes: %v", err)
|
|
}
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatalf("stat: %v", err)
|
|
}
|
|
if diff := info.ModTime().UTC().Sub(want); diff > 2*time.Second || diff < -2*time.Second {
|
|
t.Fatalf("mtime = %v, want about %v", info.ModTime().UTC(), want)
|
|
}
|
|
}
|
|
|
|
func TestStatfsIsSelfConsistent(t *testing.T) {
|
|
if *mountPoint == "" {
|
|
t.Skip("no -mountpoint given; this test needs a live WinFsp mount")
|
|
}
|
|
var free, total, totalFree uint64
|
|
if err := getDiskFreeSpace(*mountPoint, &free, &total, &totalFree); err != nil {
|
|
t.Fatalf("GetDiskFreeSpaceEx: %v", err)
|
|
}
|
|
if total == 0 {
|
|
t.Fatal("drive reports zero total bytes")
|
|
}
|
|
if free > total {
|
|
t.Fatalf("free (%d) exceeds total (%d)", free, total)
|
|
}
|
|
if totalFree > total {
|
|
t.Fatalf("total free (%d) exceeds total (%d)", totalFree, total)
|
|
}
|
|
}
|
|
|
|
// TestDeleteOnClose covers the pattern Windows software uses for temporaries:
|
|
// the file goes away when the last handle closes, with no explicit delete. A
|
|
// filesystem that ignores the flag leaves the name behind, and the next
|
|
// program to create it gets a collision.
|
|
func TestDeleteOnClose(t *testing.T) {
|
|
dir := testRoot(t)
|
|
path := filepath.Join(dir, "ephemeral.tmp")
|
|
|
|
h, err := createDeleteOnClose(path)
|
|
if err != nil {
|
|
if errors.Is(err, errWindowsOnly) {
|
|
t.Skip("delete-on-close needs the Win32 create call")
|
|
}
|
|
t.Fatalf("open with delete-on-close: %v", err)
|
|
}
|
|
if _, err := os.Stat(path); err != nil {
|
|
closeHandle(h)
|
|
t.Fatalf("file is not visible while its handle is open: %v", err)
|
|
}
|
|
if err := closeHandle(h); err != nil {
|
|
t.Fatalf("close: %v", err)
|
|
}
|
|
if _, err := os.Stat(path); err == nil {
|
|
t.Fatal("file outlived its last handle")
|
|
} else if !os.IsNotExist(err) {
|
|
t.Fatalf("stat after close: %v", err)
|
|
}
|
|
// The name has to be free again, which is what the conformance suite
|
|
// tripped over when an aborted test left its file behind.
|
|
h, err = createDeleteOnClose(path)
|
|
if err != nil {
|
|
t.Fatalf("recreating the same name failed: %v", err)
|
|
}
|
|
if err := closeHandle(h); err != nil {
|
|
t.Fatalf("close after recreate: %v", err)
|
|
}
|
|
}
|
|
|
|
func describeFileInfo(fi os.FileInfo) string {
|
|
if fi == nil {
|
|
return "<nil>"
|
|
}
|
|
return fmt.Sprintf("{size=%d mode=%v mtime=%s}", fi.Size(), fi.Mode(), fi.ModTime().Format(time.RFC3339Nano))
|
|
}
|