Enabled writeback caching by default.

This may bring significant performance improvements, but also comes with
some caveats. See the notes on MountConfig.DisableWritebackCaching for
details.
geesefs-0-30-9
Aaron Jacobs 2015-08-12 12:42:00 +10:00
commit 962e8e26d5
12 changed files with 202 additions and 141 deletions

View File

@ -57,6 +57,7 @@ const maxReadahead = 1 << 20
// A connection to the fuse kernel process. // A connection to the fuse kernel process.
type Connection struct { type Connection struct {
cfg MountConfig
debugLogger *log.Logger debugLogger *log.Logger
errorLogger *log.Logger errorLogger *log.Logger
@ -65,9 +66,6 @@ type Connection struct {
dev *os.File dev *os.File
protocol fusekernel.Protocol protocol fusekernel.Protocol
// The context from which all op contexts inherit.
parentCtx context.Context
mu sync.Mutex mu sync.Mutex
// A map from fuse "unique" request ID (*not* the op ID for logging used // A map from fuse "unique" request ID (*not* the op ID for logging used
@ -94,15 +92,15 @@ type opState struct {
// //
// The loggers may be nil. // The loggers may be nil.
func newConnection( func newConnection(
parentCtx context.Context, cfg MountConfig,
debugLogger *log.Logger, debugLogger *log.Logger,
errorLogger *log.Logger, errorLogger *log.Logger,
dev *os.File) (c *Connection, err error) { dev *os.File) (c *Connection, err error) {
c = &Connection{ c = &Connection{
cfg: cfg,
debugLogger: debugLogger, debugLogger: debugLogger,
errorLogger: errorLogger, errorLogger: errorLogger,
dev: dev, dev: dev,
parentCtx: parentCtx,
cancelFuncs: make(map[uint64]func()), cancelFuncs: make(map[uint64]func()),
} }
@ -165,6 +163,11 @@ func (c *Connection) Init() (err error) {
// Tell the kernel not to use pitifully small 4 KiB writes. // Tell the kernel not to use pitifully small 4 KiB writes.
initOp.Flags |= fusekernel.InitBigWrites initOp.Flags |= fusekernel.InitBigWrites
// Enable writeback caching if the user hasn't asked us not to.
if !c.cfg.DisableWritebackCaching {
initOp.Flags |= fusekernel.InitWritebackCache
}
c.Reply(ctx, nil) c.Reply(ctx, nil)
return return
} }
@ -227,7 +230,7 @@ func (c *Connection) beginOp(
opCode uint32, opCode uint32,
fuseID uint64) (ctx context.Context) { fuseID uint64) (ctx context.Context) {
// Start with the parent context. // Start with the parent context.
ctx = c.parentCtx ctx = c.cfg.OpContext
// Set up a cancellation function. // Set up a cancellation function.
// //

View File

@ -57,6 +57,23 @@ func describeRequest(op interface{}) (s string) {
addComponent("parent %d", typed.Parent) addComponent("parent %d", typed.Parent)
addComponent("name %q", typed.Name) addComponent("name %q", typed.Name)
case *fuseops.SetInodeAttributesOp:
if typed.Size != nil {
addComponent("size %d", *typed.Size)
}
if typed.Mode != nil {
addComponent("mode %v", *typed.Mode)
}
if typed.Atime != nil {
addComponent("atime %v", *typed.Atime)
}
if typed.Mtime != nil {
addComponent("mtime %v", *typed.Mtime)
}
case *fuseops.ReadFileOp: case *fuseops.ReadFileOp:
addComponent("handle %d", typed.Handle) addComponent("handle %d", typed.Handle)
addComponent("offset %d", typed.Offset) addComponent("offset %d", typed.Offset)

View File

@ -509,17 +509,8 @@ type ReadFileOp struct {
// //
// Note that the kernel *will* ensure that writes are received and acknowledged // Note that the kernel *will* ensure that writes are received and acknowledged
// by the file system before sending a FlushFileOp when closing the file // by the file system before sending a FlushFileOp when closing the file
// descriptor to which they were written: // descriptor to which they were written. Cf. the notes on
// // fuse.MountConfig.DisableWritebackCaching.
// * (http://goo.gl/PheZjf) fuse_flush calls write_inode_now, which appears
// to start a writeback in the background (it talks about a "flusher
// thread").
//
// * (http://goo.gl/1IiepM) fuse_flush then calls fuse_sync_writes, which
// "[waits] for all pending writepages on the inode to finish".
//
// * (http://goo.gl/zzvxWv) Only then does fuse_flush finally send the
// flush request.
// //
// (See also http://goo.gl/ocdTdM, fuse-devel thread "Fuse guarantees on // (See also http://goo.gl/ocdTdM, fuse-devel thread "Fuse guarantees on
// concurrent requests".) // concurrent requests".)

View File

@ -28,28 +28,32 @@ import (
// also that it matches. // also that it matches.
func MtimeIs(expected time.Time) oglematchers.Matcher { func MtimeIs(expected time.Time) oglematchers.Matcher {
return oglematchers.NewMatcher( return oglematchers.NewMatcher(
func(c interface{}) error { return mtimeIs(c, expected) }, func(c interface{}) error { return mtimeIsWithin(c, expected, 0) },
fmt.Sprintf("mtime is %v", expected)) fmt.Sprintf("mtime is %v", expected))
} }
func mtimeIs(c interface{}, expected time.Time) error { // Like MtimeIs, but allows for a tolerance.
func MtimeIsWithin(expected time.Time, d time.Duration) oglematchers.Matcher {
return oglematchers.NewMatcher(
func(c interface{}) error { return mtimeIsWithin(c, expected, d) },
fmt.Sprintf("mtime is within %v of %v", d, expected))
}
func mtimeIsWithin(c interface{}, expected time.Time, d time.Duration) error {
fi, ok := c.(os.FileInfo) fi, ok := c.(os.FileInfo)
if !ok { if !ok {
return fmt.Errorf("which is of type %v", reflect.TypeOf(c)) return fmt.Errorf("which is of type %v", reflect.TypeOf(c))
} }
// Check ModTime(). // Check ModTime().
if fi.ModTime() != expected { diff := fi.ModTime().Sub(expected)
d := fi.ModTime().Sub(expected) absDiff := diff
return fmt.Errorf("which has mtime %v, off by %v", fi.ModTime(), d) if absDiff < 0 {
absDiff = -absDiff
} }
// Check Sys(). if !(absDiff < d) {
if sysMtime, ok := extractMtime(fi.Sys()); ok { return fmt.Errorf("which has mtime %v, off by %v", fi.ModTime(), diff)
if sysMtime != expected {
d := sysMtime.Sub(expected)
return fmt.Errorf("which has Sys() mtime %v, off by %v", sysMtime, d)
}
} }
return nil return nil

View File

@ -67,14 +67,14 @@ func Mount(
} }
// Choose a parent context for ops. // Choose a parent context for ops.
opContext := config.OpContext cfgCopy := *config
if opContext == nil { if cfgCopy.OpContext == nil {
opContext = context.Background() cfgCopy.OpContext = context.Background()
} }
// Create a Connection object wrapping the device. // Create a Connection object wrapping the device.
connection, err := newConnection( connection, err := newConnection(
opContext, cfgCopy,
config.DebugLogger, config.DebugLogger,
config.ErrorLogger, config.ErrorLogger,
dev) dev)

View File

@ -48,6 +48,71 @@ type MountConfig struct {
// performed. // performed.
DebugLogger *log.Logger DebugLogger *log.Logger
// Linux only. OS X always behaves as if writeback caching is disabled.
//
// By default on Linux we allow the kernel to perform writeback caching
// (cf. http://goo.gl/LdZzo1):
//
// * When the user calls write(2), the kernel sticks the user's data into
// its page cache. Only later does it call through to the file system,
// potentially after coalescing multiple small user writes.
//
// * The file system may receive multiple write ops from the kernel
// concurrently if there is a lot of page cache data to flush.
//
// * Write performance may be significantly improved due to the user and
// the kernel not waiting for serial round trips to the file system. This
// is especially true if the user makes tiny writes.
//
// * close(2) (and anything else calling f_op->flush) causes all dirty
// pages to be written out before it proceeds to send a FlushFileOp
// (cf. https://goo.gl/TMrY6X).
//
// * Similarly, close(2) causes the kernel to send a setattr request
// filling in the mtime if any dirty pages were flushed, since the time
// at which the pages were written to the file system can't be trusted.
//
// * close(2) (and anything else calling f_op->flush) writes out all dirty
// pages, then sends a setattr request with an appropriate mtime for
// those writes if there were any, and only then proceeds to send a flush
//
// Code walk:
//
// * (https://goo.gl/zTIZQ9) fuse_flush calls write_inode_now before
// calling the file system. The latter eventually calls into
// __writeback_single_inode.
//
// * (https://goo.gl/L7Z2w5) __writeback_single_inode calls
// do_writepages, which writes out any dirty pages.
//
// * (https://goo.gl/DOPgla) __writeback_single_inode later calls
// write_inode, which calls into the superblock op struct's write_inode
// member. For fuse, this is fuse_write_inode
// (cf. https://goo.gl/eDSKOX).
//
// * (https://goo.gl/PbkGA1) fuse_write_inode calls fuse_flush_times.
//
// * (https://goo.gl/ig8x9V) fuse_flush_times sends a setttr request
// for setting the inode's mtime.
//
// However, this brings along some caveats:
//
// * The file system must handle SetInodeAttributesOp or close(2) will fail,
// due to the call chain into fuse_flush_times listed above.
//
// * The kernel caches mtime and ctime regardless of whether the file
// system tells it to do so, disregarding the result of further getattr
// requests (cf. https://goo.gl/3ZZMUw, https://goo.gl/7WtQUp). It
// appears this may be true of the file size, too. Writeback caching may
// therefore not be suitable for file systems where these attributes can
// spontaneously change for reasons the kernel doesn't observe. See
// http://goo.gl/V5WQCN for more discussion.
//
// Setting DisableWritebackCaching disables this behavior. Instead the file
// system is called one or more times for each write(2), and the user's
// syscall doesn't return until the file system returns.
DisableWritebackCaching bool
// OS X only. // OS X only.
// //
// Normally on OS X we mount with the novncache option // Normally on OS X we mount with the novncache option

View File

@ -53,6 +53,10 @@ func (t *cachingFSTest) setUp(
getattrTimeout time.Duration) { getattrTimeout time.Duration) {
var err error var err error
// We assert things about whether or not mtimes are cached, but writeback
// caching causes them to always be cached. Turn it off.
t.MountConfig.DisableWritebackCaching = true
// Create the file system. // Create the file system.
t.fs, err = cachingfs.NewCachingFS(lookupEntryTimeout, getattrTimeout) t.fs, err = cachingfs.NewCachingFS(lookupEntryTimeout, getattrTimeout)
AssertEq(nil, err) AssertEq(nil, err)

View File

@ -90,6 +90,29 @@ func (fs *flushFS) barAttributes() fuseops.InodeAttributes {
} }
} }
// LOCKS_REQUIRED(fs.mu)
func (fs *flushFS) getAttributes(id fuseops.InodeID) (
attrs fuseops.InodeAttributes,
err error) {
switch id {
case fuseops.RootInodeID:
attrs = fs.rootAttributes()
return
case fooID:
attrs = fs.fooAttributes()
return
case barID:
attrs = fs.barAttributes()
return
default:
err = fuse.ENOENT
return
}
}
//////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////
// FileSystem methods // FileSystem methods
//////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////
@ -134,23 +157,20 @@ func (fs *flushFS) GetInodeAttributes(
fs.mu.Lock() fs.mu.Lock()
defer fs.mu.Unlock() defer fs.mu.Unlock()
switch op.Inode { op.Attributes, err = fs.getAttributes(op.Inode)
case fuseops.RootInodeID: return
op.Attributes = fs.rootAttributes() }
return
case fooID: func (fs *flushFS) SetInodeAttributes(
op.Attributes = fs.fooAttributes() ctx context.Context,
return op *fuseops.SetInodeAttributesOp) (err error) {
fs.mu.Lock()
defer fs.mu.Unlock()
case barID: // Ignore any changes and simply return existing attributes.
op.Attributes = fs.barAttributes() op.Attributes, err = fs.getAttributes(op.Inode)
return
default: return
err = fuse.ENOENT
return
}
} }
func (fs *flushFS) OpenFile( func (fs *flushFS) OpenFile(

View File

@ -590,6 +590,8 @@ func (t *NoErrorsTest) Mmap_NoMsync_MunmapBeforeClose() {
syscall.MAP_SHARED) syscall.MAP_SHARED)
AssertEq(nil, err) AssertEq(nil, err)
defer syscall.Munmap(data)
AssertEq("taco", string(data)) AssertEq("taco", string(data))
// Modify the contents. // Modify the contents.
@ -638,6 +640,8 @@ func (t *NoErrorsTest) Mmap_NoMsync_CloseBeforeMunmap() {
syscall.MAP_SHARED) syscall.MAP_SHARED)
AssertEq(nil, err) AssertEq(nil, err)
defer syscall.Munmap(data)
AssertEq("taco", string(data)) AssertEq("taco", string(data))
// Close the file. We should see a flush. // Close the file. We should see a flush.
@ -682,6 +686,8 @@ func (t *NoErrorsTest) Mmap_WithMsync_MunmapBeforeClose() {
syscall.MAP_SHARED) syscall.MAP_SHARED)
AssertEq(nil, err) AssertEq(nil, err)
defer syscall.Munmap(data)
AssertEq("taco", string(data)) AssertEq("taco", string(data))
// Modify the contents. // Modify the contents.
@ -738,6 +744,8 @@ func (t *NoErrorsTest) Mmap_WithMsync_CloseBeforeMunmap() {
syscall.MAP_SHARED) syscall.MAP_SHARED)
AssertEq(nil, err) AssertEq(nil, err)
defer syscall.Munmap(data)
AssertEq("taco", string(data)) AssertEq("taco", string(data))
// Close the file. We should see a flush. // Close the file. We should see a flush.
@ -941,6 +949,7 @@ func (t *FsyncErrorTest) Msync() {
syscall.MAP_SHARED) syscall.MAP_SHARED)
AssertEq(nil, err) AssertEq(nil, err)
defer syscall.Munmap(data)
// msync the mapping. // msync the mapping.
err = msync(data) err = msync(data)

View File

@ -22,19 +22,12 @@ import (
"github.com/jacobsa/fuse/fuseops" "github.com/jacobsa/fuse/fuseops"
"github.com/jacobsa/fuse/fuseutil" "github.com/jacobsa/fuse/fuseutil"
"github.com/jacobsa/timeutil"
) )
// Common attributes for files and directories. // Common attributes for files and directories.
// //
// External synchronization is required. // External synchronization is required.
type inode struct { type inode struct {
/////////////////////////
// Dependencies
/////////////////////////
clock timeutil.Clock
///////////////////////// /////////////////////////
// Mutable state // Mutable state
///////////////////////// /////////////////////////
@ -79,16 +72,14 @@ type inode struct {
// Create a new inode with the supplied attributes, which need not contain // Create a new inode with the supplied attributes, which need not contain
// time-related information (the inode object will take care of that). // time-related information (the inode object will take care of that).
func newInode( func newInode(
clock timeutil.Clock,
attrs fuseops.InodeAttributes) (in *inode) { attrs fuseops.InodeAttributes) (in *inode) {
// Update time info. // Update time info.
now := clock.Now() now := time.Now()
attrs.Mtime = now attrs.Mtime = now
attrs.Crtime = now attrs.Crtime = now
// Create the object. // Create the object.
in = &inode{ in = &inode{
clock: clock,
attrs: attrs, attrs: attrs,
} }
@ -226,7 +217,7 @@ func (in *inode) AddChild(
var index int var index int
// Update the modification time. // Update the modification time.
in.attrs.Mtime = in.clock.Now() in.attrs.Mtime = time.Now()
// No matter where we place the entry, make sure it has the correct Offset // No matter where we place the entry, make sure it has the correct Offset
// field. // field.
@ -260,7 +251,7 @@ func (in *inode) AddChild(
// REQUIRES: An entry for the given name exists. // REQUIRES: An entry for the given name exists.
func (in *inode) RemoveChild(name string) { func (in *inode) RemoveChild(name string) {
// Update the modification time. // Update the modification time.
in.attrs.Mtime = in.clock.Now() in.attrs.Mtime = time.Now()
// Find the entry. // Find the entry.
i, ok := in.findChild(name) i, ok := in.findChild(name)
@ -334,7 +325,7 @@ func (in *inode) WriteAt(p []byte, off int64) (n int, err error) {
} }
// Update the modification time. // Update the modification time.
in.attrs.Mtime = in.clock.Now() in.attrs.Mtime = time.Now()
// Ensure that the contents slice is long enough. // Ensure that the contents slice is long enough.
newLen := int(off) + len(p) newLen := int(off) + len(p)
@ -361,7 +352,7 @@ func (in *inode) SetAttributes(
mode *os.FileMode, mode *os.FileMode,
mtime *time.Time) { mtime *time.Time) {
// Update the modification time. // Update the modification time.
in.attrs.Mtime = in.clock.Now() in.attrs.Mtime = time.Now()
// Truncate? // Truncate?
if size != nil { if size != nil {

View File

@ -26,7 +26,6 @@ import (
"github.com/jacobsa/fuse/fuseops" "github.com/jacobsa/fuse/fuseops"
"github.com/jacobsa/fuse/fuseutil" "github.com/jacobsa/fuse/fuseutil"
"github.com/jacobsa/syncutil" "github.com/jacobsa/syncutil"
"github.com/jacobsa/timeutil"
) )
type memFS struct { type memFS struct {
@ -36,12 +35,6 @@ type memFS struct {
uid uint32 uid uint32
gid uint32 gid uint32
/////////////////////////
// Dependencies
/////////////////////////
clock timeutil.Clock
///////////////////////// /////////////////////////
// Mutable state // Mutable state
///////////////////////// /////////////////////////
@ -74,11 +67,9 @@ type memFS struct {
// default_permissions option. // default_permissions option.
func NewMemFS( func NewMemFS(
uid uint32, uid uint32,
gid uint32, gid uint32) fuse.Server {
clock timeutil.Clock) fuse.Server {
// Set up the basic struct. // Set up the basic struct.
fs := &memFS{ fs := &memFS{
clock: clock,
inodes: make([]*inode, fuseops.RootInodeID+1), inodes: make([]*inode, fuseops.RootInodeID+1),
uid: uid, uid: uid,
gid: gid, gid: gid,
@ -91,7 +82,7 @@ func NewMemFS(
Gid: gid, Gid: gid,
} }
fs.inodes[fuseops.RootInodeID] = newInode(clock, rootAttrs) fs.inodes[fuseops.RootInodeID] = newInode(rootAttrs)
// Set up invariant checking. // Set up invariant checking.
fs.mu = syncutil.NewInvariantMutex(fs.checkInvariants) fs.mu = syncutil.NewInvariantMutex(fs.checkInvariants)
@ -165,7 +156,7 @@ func (fs *memFS) getInodeOrDie(id fuseops.InodeID) (inode *inode) {
func (fs *memFS) allocateInode( func (fs *memFS) allocateInode(
attrs fuseops.InodeAttributes) (id fuseops.InodeID, inode *inode) { attrs fuseops.InodeAttributes) (id fuseops.InodeID, inode *inode) {
// Create the inode. // Create the inode.
inode = newInode(fs.clock, attrs) inode = newInode(attrs)
// Re-use a free ID if possible. Otherwise mint a new one. // Re-use a free ID if possible. Otherwise mint a new one.
numFree := len(fs.freeInodes) numFree := len(fs.freeInodes)
@ -216,7 +207,7 @@ func (fs *memFS) LookUpInode(
// We don't spontaneously mutate, so the kernel can cache as long as it wants // We don't spontaneously mutate, so the kernel can cache as long as it wants
// (since it also handles invalidation). // (since it also handles invalidation).
op.Entry.AttributesExpiration = fs.clock.Now().Add(365 * 24 * time.Hour) op.Entry.AttributesExpiration = time.Now().Add(365 * 24 * time.Hour)
op.Entry.EntryExpiration = op.Entry.EntryExpiration op.Entry.EntryExpiration = op.Entry.EntryExpiration
return return
@ -236,7 +227,7 @@ func (fs *memFS) GetInodeAttributes(
// We don't spontaneously mutate, so the kernel can cache as long as it wants // We don't spontaneously mutate, so the kernel can cache as long as it wants
// (since it also handles invalidation). // (since it also handles invalidation).
op.AttributesExpiration = fs.clock.Now().Add(365 * 24 * time.Hour) op.AttributesExpiration = time.Now().Add(365 * 24 * time.Hour)
return return
} }
@ -258,7 +249,7 @@ func (fs *memFS) SetInodeAttributes(
// We don't spontaneously mutate, so the kernel can cache as long as it wants // We don't spontaneously mutate, so the kernel can cache as long as it wants
// (since it also handles invalidation). // (since it also handles invalidation).
op.AttributesExpiration = fs.clock.Now().Add(365 * 24 * time.Hour) op.AttributesExpiration = time.Now().Add(365 * 24 * time.Hour)
return return
} }
@ -300,7 +291,7 @@ func (fs *memFS) MkDir(
// We don't spontaneously mutate, so the kernel can cache as long as it wants // We don't spontaneously mutate, so the kernel can cache as long as it wants
// (since it also handles invalidation). // (since it also handles invalidation).
op.Entry.AttributesExpiration = fs.clock.Now().Add(365 * 24 * time.Hour) op.Entry.AttributesExpiration = time.Now().Add(365 * 24 * time.Hour)
op.Entry.EntryExpiration = op.Entry.EntryExpiration op.Entry.EntryExpiration = op.Entry.EntryExpiration
return return
@ -324,7 +315,7 @@ func (fs *memFS) CreateFile(
} }
// Set up attributes from the child. // Set up attributes from the child.
now := fs.clock.Now() now := time.Now()
childAttrs := fuseops.InodeAttributes{ childAttrs := fuseops.InodeAttributes{
Nlink: 1, Nlink: 1,
Mode: op.Mode, Mode: op.Mode,
@ -348,7 +339,7 @@ func (fs *memFS) CreateFile(
// We don't spontaneously mutate, so the kernel can cache as long as it wants // We don't spontaneously mutate, so the kernel can cache as long as it wants
// (since it also handles invalidation). // (since it also handles invalidation).
op.Entry.AttributesExpiration = fs.clock.Now().Add(365 * 24 * time.Hour) op.Entry.AttributesExpiration = time.Now().Add(365 * 24 * time.Hour)
op.Entry.EntryExpiration = op.Entry.EntryExpiration op.Entry.EntryExpiration = op.Entry.EntryExpiration
// We have nothing interesting to put in the Handle field. // We have nothing interesting to put in the Handle field.
@ -374,7 +365,7 @@ func (fs *memFS) CreateSymlink(
} }
// Set up attributes from the child. // Set up attributes from the child.
now := fs.clock.Now() now := time.Now()
childAttrs := fuseops.InodeAttributes{ childAttrs := fuseops.InodeAttributes{
Nlink: 1, Nlink: 1,
Mode: 0444 | os.ModeSymlink, Mode: 0444 | os.ModeSymlink,
@ -401,7 +392,7 @@ func (fs *memFS) CreateSymlink(
// We don't spontaneously mutate, so the kernel can cache as long as it wants // We don't spontaneously mutate, so the kernel can cache as long as it wants
// (since it also handles invalidation). // (since it also handles invalidation).
op.Entry.AttributesExpiration = fs.clock.Now().Add(365 * 24 * time.Hour) op.Entry.AttributesExpiration = time.Now().Add(365 * 24 * time.Hour)
op.Entry.EntryExpiration = op.Entry.EntryExpiration op.Entry.EntryExpiration = op.Entry.EntryExpiration
return return

View File

@ -35,6 +35,11 @@ import (
func TestMemFS(t *testing.T) { RunTests(t) } func TestMemFS(t *testing.T) { RunTests(t) }
// The radius we use for "expect mtime is within"-style assertions. We can't
// share a synchronized clock with the ultimate source of mtimes because with
// writeback caching enabled the kernel manufactures them based on wall time.
const timeSlop = 25 * time.Millisecond
//////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////
// Helpers // Helpers
//////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////
@ -89,7 +94,7 @@ type MemFSTest struct {
func init() { RegisterTestSuite(&MemFSTest{}) } func init() { RegisterTestSuite(&MemFSTest{}) }
func (t *MemFSTest) SetUp(ti *TestInfo) { func (t *MemFSTest) SetUp(ti *TestInfo) {
t.Server = memfs.NewMemFS(currentUid(), currentGid(), &t.Clock) t.Server = memfs.NewMemFS(currentUid(), currentGid())
t.SampleTest.SetUp(ti) t.SampleTest.SetUp(ti)
} }
@ -112,17 +117,11 @@ func (t *MemFSTest) Mkdir_OneLevel() {
dirName := path.Join(t.Dir, "dir") dirName := path.Join(t.Dir, "dir")
// Simulate time advancing.
t.Clock.AdvanceTime(time.Second)
// Create a directory within the root. // Create a directory within the root.
createTime := t.Clock.Now() createTime := time.Now()
err = os.Mkdir(dirName, 0754) err = os.Mkdir(dirName, 0754)
AssertEq(nil, err) AssertEq(nil, err)
// Simulate time advancing.
t.Clock.AdvanceTime(time.Second)
// Stat the directory. // Stat the directory.
fi, err = os.Stat(dirName) fi, err = os.Stat(dirName)
stat = fi.Sys().(*syscall.Stat_t) stat = fi.Sys().(*syscall.Stat_t)
@ -131,7 +130,7 @@ func (t *MemFSTest) Mkdir_OneLevel() {
ExpectEq("dir", fi.Name()) ExpectEq("dir", fi.Name())
ExpectEq(0, fi.Size()) ExpectEq(0, fi.Size())
ExpectEq(os.ModeDir|applyUmask(0754), fi.Mode()) ExpectEq(os.ModeDir|applyUmask(0754), fi.Mode())
ExpectThat(fi, fusetesting.MtimeIs(createTime)) ExpectThat(fi, fusetesting.MtimeIsWithin(createTime, timeSlop))
ExpectThat(fi, fusetesting.BirthtimeIs(createTime)) ExpectThat(fi, fusetesting.BirthtimeIs(createTime))
ExpectTrue(fi.IsDir()) ExpectTrue(fi.IsDir())
@ -145,7 +144,7 @@ func (t *MemFSTest) Mkdir_OneLevel() {
fi, err = os.Stat(t.Dir) fi, err = os.Stat(t.Dir)
AssertEq(nil, err) AssertEq(nil, err)
ExpectEq(0, fi.ModTime().Sub(createTime)) ExpectThat(fi, fusetesting.MtimeIsWithin(createTime, timeSlop))
// Read the directory. // Read the directory.
entries, err = fusetesting.ReadDirPicky(dirName) entries, err = fusetesting.ReadDirPicky(dirName)
@ -174,17 +173,11 @@ func (t *MemFSTest) Mkdir_TwoLevels() {
err = os.Mkdir(path.Join(t.Dir, "parent"), 0700) err = os.Mkdir(path.Join(t.Dir, "parent"), 0700)
AssertEq(nil, err) AssertEq(nil, err)
// Simulate time advancing.
t.Clock.AdvanceTime(time.Second)
// Create a child of that directory. // Create a child of that directory.
createTime := t.Clock.Now() createTime := time.Now()
err = os.Mkdir(path.Join(t.Dir, "parent/dir"), 0754) err = os.Mkdir(path.Join(t.Dir, "parent/dir"), 0754)
AssertEq(nil, err) AssertEq(nil, err)
// Simulate time advancing.
t.Clock.AdvanceTime(time.Second)
// Stat the directory. // Stat the directory.
fi, err = os.Stat(path.Join(t.Dir, "parent/dir")) fi, err = os.Stat(path.Join(t.Dir, "parent/dir"))
stat = fi.Sys().(*syscall.Stat_t) stat = fi.Sys().(*syscall.Stat_t)
@ -193,7 +186,7 @@ func (t *MemFSTest) Mkdir_TwoLevels() {
ExpectEq("dir", fi.Name()) ExpectEq("dir", fi.Name())
ExpectEq(0, fi.Size()) ExpectEq(0, fi.Size())
ExpectEq(os.ModeDir|applyUmask(0754), fi.Mode()) ExpectEq(os.ModeDir|applyUmask(0754), fi.Mode())
ExpectThat(fi, fusetesting.MtimeIs(createTime)) ExpectThat(fi, fusetesting.MtimeIsWithin(createTime, timeSlop))
ExpectThat(fi, fusetesting.BirthtimeIs(createTime)) ExpectThat(fi, fusetesting.BirthtimeIs(createTime))
ExpectTrue(fi.IsDir()) ExpectTrue(fi.IsDir())
@ -206,7 +199,7 @@ func (t *MemFSTest) Mkdir_TwoLevels() {
// Check the parent's mtime. // Check the parent's mtime.
fi, err = os.Stat(path.Join(t.Dir, "parent")) fi, err = os.Stat(path.Join(t.Dir, "parent"))
AssertEq(nil, err) AssertEq(nil, err)
ExpectEq(0, fi.ModTime().Sub(createTime)) ExpectThat(fi, fusetesting.MtimeIsWithin(createTime, timeSlop))
// Read the directory. // Read the directory.
entries, err = fusetesting.ReadDirPicky(path.Join(t.Dir, "parent/dir")) entries, err = fusetesting.ReadDirPicky(path.Join(t.Dir, "parent/dir"))
@ -290,13 +283,10 @@ func (t *MemFSTest) CreateNewFile_InRoot() {
fileName := path.Join(t.Dir, "foo") fileName := path.Join(t.Dir, "foo")
const contents = "Hello\x00world" const contents = "Hello\x00world"
createTime := t.Clock.Now() createTime := time.Now()
err = ioutil.WriteFile(fileName, []byte(contents), 0400) err = ioutil.WriteFile(fileName, []byte(contents), 0400)
AssertEq(nil, err) AssertEq(nil, err)
// Simulate time advancing.
t.Clock.AdvanceTime(time.Second)
// Stat it. // Stat it.
fi, err = os.Stat(fileName) fi, err = os.Stat(fileName)
stat = fi.Sys().(*syscall.Stat_t) stat = fi.Sys().(*syscall.Stat_t)
@ -305,7 +295,7 @@ func (t *MemFSTest) CreateNewFile_InRoot() {
ExpectEq("foo", fi.Name()) ExpectEq("foo", fi.Name())
ExpectEq(len(contents), fi.Size()) ExpectEq(len(contents), fi.Size())
ExpectEq(applyUmask(0400), fi.Mode()) ExpectEq(applyUmask(0400), fi.Mode())
ExpectThat(fi, fusetesting.MtimeIs(createTime)) ExpectThat(fi, fusetesting.MtimeIsWithin(createTime, timeSlop))
ExpectThat(fi, fusetesting.BirthtimeIs(createTime)) ExpectThat(fi, fusetesting.BirthtimeIs(createTime))
ExpectFalse(fi.IsDir()) ExpectFalse(fi.IsDir())
@ -335,13 +325,10 @@ func (t *MemFSTest) CreateNewFile_InSubDir() {
fileName := path.Join(dirName, "foo") fileName := path.Join(dirName, "foo")
const contents = "Hello\x00world" const contents = "Hello\x00world"
createTime := t.Clock.Now() createTime := time.Now()
err = ioutil.WriteFile(fileName, []byte(contents), 0400) err = ioutil.WriteFile(fileName, []byte(contents), 0400)
AssertEq(nil, err) AssertEq(nil, err)
// Simulate time advancing.
t.Clock.AdvanceTime(time.Second)
// Stat it. // Stat it.
fi, err = os.Stat(fileName) fi, err = os.Stat(fileName)
stat = fi.Sys().(*syscall.Stat_t) stat = fi.Sys().(*syscall.Stat_t)
@ -350,7 +337,7 @@ func (t *MemFSTest) CreateNewFile_InSubDir() {
ExpectEq("foo", fi.Name()) ExpectEq("foo", fi.Name())
ExpectEq(len(contents), fi.Size()) ExpectEq(len(contents), fi.Size())
ExpectEq(applyUmask(0400), fi.Mode()) ExpectEq(applyUmask(0400), fi.Mode())
ExpectThat(fi, fusetesting.MtimeIs(createTime)) ExpectThat(fi, fusetesting.MtimeIsWithin(createTime, timeSlop))
ExpectThat(fi, fusetesting.BirthtimeIs(createTime)) ExpectThat(fi, fusetesting.BirthtimeIs(createTime))
ExpectFalse(fi.IsDir()) ExpectFalse(fi.IsDir())
@ -375,26 +362,20 @@ func (t *MemFSTest) ModifyExistingFile_InRoot() {
// Write a file. // Write a file.
fileName := path.Join(t.Dir, "foo") fileName := path.Join(t.Dir, "foo")
createTime := t.Clock.Now() createTime := time.Now()
err = ioutil.WriteFile(fileName, []byte("Hello, world!"), 0600) err = ioutil.WriteFile(fileName, []byte("Hello, world!"), 0600)
AssertEq(nil, err) AssertEq(nil, err)
// Simulate time advancing.
t.Clock.AdvanceTime(time.Second)
// Open the file and modify it. // Open the file and modify it.
f, err := os.OpenFile(fileName, os.O_WRONLY, 0400) f, err := os.OpenFile(fileName, os.O_WRONLY, 0400)
t.ToClose = append(t.ToClose, f) t.ToClose = append(t.ToClose, f)
AssertEq(nil, err) AssertEq(nil, err)
modifyTime := t.Clock.Now() modifyTime := time.Now()
n, err = f.WriteAt([]byte("H"), 0) n, err = f.WriteAt([]byte("H"), 0)
AssertEq(nil, err) AssertEq(nil, err)
AssertEq(1, n) AssertEq(1, n)
// Simulate time advancing.
t.Clock.AdvanceTime(time.Second)
// Stat the file. // Stat the file.
fi, err = os.Stat(fileName) fi, err = os.Stat(fileName)
stat = fi.Sys().(*syscall.Stat_t) stat = fi.Sys().(*syscall.Stat_t)
@ -403,7 +384,7 @@ func (t *MemFSTest) ModifyExistingFile_InRoot() {
ExpectEq("foo", fi.Name()) ExpectEq("foo", fi.Name())
ExpectEq(len("Hello, world!"), fi.Size()) ExpectEq(len("Hello, world!"), fi.Size())
ExpectEq(applyUmask(0600), fi.Mode()) ExpectEq(applyUmask(0600), fi.Mode())
ExpectThat(fi, fusetesting.MtimeIs(modifyTime)) ExpectThat(fi, fusetesting.MtimeIsWithin(modifyTime, timeSlop))
ExpectThat(fi, fusetesting.BirthtimeIs(createTime)) ExpectThat(fi, fusetesting.BirthtimeIs(createTime))
ExpectFalse(fi.IsDir()) ExpectFalse(fi.IsDir())
@ -433,26 +414,20 @@ func (t *MemFSTest) ModifyExistingFile_InSubDir() {
// Write a file. // Write a file.
fileName := path.Join(dirName, "foo") fileName := path.Join(dirName, "foo")
createTime := t.Clock.Now() createTime := time.Now()
err = ioutil.WriteFile(fileName, []byte("Hello, world!"), 0600) err = ioutil.WriteFile(fileName, []byte("Hello, world!"), 0600)
AssertEq(nil, err) AssertEq(nil, err)
// Simulate time advancing.
t.Clock.AdvanceTime(time.Second)
// Open the file and modify it. // Open the file and modify it.
f, err := os.OpenFile(fileName, os.O_WRONLY, 0400) f, err := os.OpenFile(fileName, os.O_WRONLY, 0400)
t.ToClose = append(t.ToClose, f) t.ToClose = append(t.ToClose, f)
AssertEq(nil, err) AssertEq(nil, err)
modifyTime := t.Clock.Now() modifyTime := time.Now()
n, err = f.WriteAt([]byte("H"), 0) n, err = f.WriteAt([]byte("H"), 0)
AssertEq(nil, err) AssertEq(nil, err)
AssertEq(1, n) AssertEq(1, n)
// Simulate time advancing.
t.Clock.AdvanceTime(time.Second)
// Stat the file. // Stat the file.
fi, err = os.Stat(fileName) fi, err = os.Stat(fileName)
stat = fi.Sys().(*syscall.Stat_t) stat = fi.Sys().(*syscall.Stat_t)
@ -461,7 +436,7 @@ func (t *MemFSTest) ModifyExistingFile_InSubDir() {
ExpectEq("foo", fi.Name()) ExpectEq("foo", fi.Name())
ExpectEq(len("Hello, world!"), fi.Size()) ExpectEq(len("Hello, world!"), fi.Size())
ExpectEq(applyUmask(0600), fi.Mode()) ExpectEq(applyUmask(0600), fi.Mode())
ExpectThat(fi, fusetesting.MtimeIs(modifyTime)) ExpectThat(fi, fusetesting.MtimeIsWithin(modifyTime, timeSlop))
ExpectThat(fi, fusetesting.BirthtimeIs(createTime)) ExpectThat(fi, fusetesting.BirthtimeIs(createTime))
ExpectFalse(fi.IsDir()) ExpectFalse(fi.IsDir())
@ -574,17 +549,11 @@ func (t *MemFSTest) Rmdir_Empty() {
err = os.MkdirAll(path.Join(t.Dir, "foo/bar"), 0754) err = os.MkdirAll(path.Join(t.Dir, "foo/bar"), 0754)
AssertEq(nil, err) AssertEq(nil, err)
// Simulate time advancing.
t.Clock.AdvanceTime(time.Second)
// Remove the leaf. // Remove the leaf.
rmTime := t.Clock.Now() rmTime := time.Now()
err = os.Remove(path.Join(t.Dir, "foo/bar")) err = os.Remove(path.Join(t.Dir, "foo/bar"))
AssertEq(nil, err) AssertEq(nil, err)
// Simulate time advancing.
t.Clock.AdvanceTime(time.Second)
// There should be nothing left in the parent. // There should be nothing left in the parent.
entries, err = fusetesting.ReadDirPicky(path.Join(t.Dir, "foo")) entries, err = fusetesting.ReadDirPicky(path.Join(t.Dir, "foo"))
@ -594,7 +563,7 @@ func (t *MemFSTest) Rmdir_Empty() {
// Check the parent's mtime. // Check the parent's mtime.
fi, err := os.Stat(path.Join(t.Dir, "foo")) fi, err := os.Stat(path.Join(t.Dir, "foo"))
AssertEq(nil, err) AssertEq(nil, err)
ExpectEq(0, fi.ModTime().Sub(rmTime)) ExpectThat(fi, fusetesting.MtimeIsWithin(rmTime, timeSlop))
// Remove the parent. // Remove the parent.
err = os.Remove(path.Join(t.Dir, "foo")) err = os.Remove(path.Join(t.Dir, "foo"))
@ -618,13 +587,10 @@ func (t *MemFSTest) Rmdir_OpenedForReading() {
var err error var err error
// Create a directory. // Create a directory.
createTime := t.Clock.Now() createTime := time.Now()
err = os.Mkdir(path.Join(t.Dir, "dir"), 0700) err = os.Mkdir(path.Join(t.Dir, "dir"), 0700)
AssertEq(nil, err) AssertEq(nil, err)
// Simulate time advancing.
t.Clock.AdvanceTime(time.Second)
// Open the directory for reading. // Open the directory for reading.
f, err := os.Open(path.Join(t.Dir, "dir")) f, err := os.Open(path.Join(t.Dir, "dir"))
defer func() { defer func() {
@ -655,7 +621,7 @@ func (t *MemFSTest) Rmdir_OpenedForReading() {
fi, err := f.Stat() fi, err := f.Stat()
ExpectEq("dir", fi.Name()) ExpectEq("dir", fi.Name())
ExpectEq(0, fi.ModTime().Sub(createTime)) ExpectThat(fi, fusetesting.MtimeIsWithin(createTime, timeSlop))
ExpectEq(0, fi.Sys().(*syscall.Stat_t).Nlink) ExpectEq(0, fi.Sys().(*syscall.Stat_t).Nlink)
// Attempt to read from the directory. This shouldn't see any junk from the // Attempt to read from the directory. This shouldn't see any junk from the
@ -1054,7 +1020,7 @@ func (t *MemFSTest) Chtimes() {
// Stat it. // Stat it.
fi, err := os.Stat(fileName) fi, err := os.Stat(fileName)
AssertEq(nil, err) AssertEq(nil, err)
ExpectEq(0, fi.ModTime().Sub(expectedMtime)) ExpectThat(fi, fusetesting.MtimeIsWithin(expectedMtime, timeSlop))
} }
func (t *MemFSTest) ReadDirWhileModifying() { func (t *MemFSTest) ReadDirWhileModifying() {