diff --git a/conversions.go b/conversions.go index 6ba2f18..3c30d71 100644 --- a/conversions.go +++ b/conversions.go @@ -512,6 +512,10 @@ func (c *Connection) kernelResponseForOp( out := (*fusekernel.OpenOut)(m.Grow(unsafe.Sizeof(fusekernel.OpenOut{}))) out.Fh = uint64(o.Handle) + if o.KeepPageCache { + out.OpenFlags |= uint32(fusekernel.OpenKeepCache) + } + case *fuseops.ReadFileOp: // convertInMessage already set up the destination buffer to be at the end // of the out message. We need only shrink to the right size based on how diff --git a/fuseops/ops.go b/fuseops/ops.go index 6e6b412..f786474 100644 --- a/fuseops/ops.go +++ b/fuseops/ops.go @@ -446,6 +446,22 @@ type OpenFileOp struct { // file handle. The file system must ensure this ID remains valid until a // later call to ReleaseFileHandle. Handle HandleID + + // By default, fuse invalidates the kernel's page cache for an inode when a + // new file handle is opened for that inode (cf. https://goo.gl/2rZ9uk). The + // intent appears to be to allow users to "see" content that has changed + // remotely on a networked file system by re-opening the file. + // + // For file systems where this is not a concern because all modifications for + // a particular inode go through the kernel, set this field to true to + // disable this behavior. + // + // (More discussion: http://goo.gl/cafzWF) + // + // Note that on OS X it appears that the behavior is always as if this field + // is set to true, regardless of its value, at least for files opened in the + // same mode. (Cf. https://github.com/osxfuse/osxfuse/issues/223) + KeepPageCache bool } // Read data from a file previously opened with CreateFile or OpenFile. diff --git a/samples/cachingfs/caching_fs.go b/samples/cachingfs/caching_fs.go index 2565719..ebc7b7c 100644 --- a/samples/cachingfs/caching_fs.go +++ b/samples/cachingfs/caching_fs.go @@ -15,7 +15,9 @@ package cachingfs import ( + "crypto/rand" "fmt" + "io" "os" "time" @@ -43,6 +45,10 @@ const ( // inode entries and attributes to be cached, used when responding to fuse // requests. It also exposes methods for renumbering inodes and updating mtimes // that are useful in testing that these durations are honored. +// +// Each file responds to reads with random contents. SetKeepCache can be used +// to control whether the response to OpenFileOp tells the kernel to keep the +// file's data in the page cache or not. type CachingFS interface { fuseutil.FileSystem @@ -57,6 +63,10 @@ type CachingFS interface { // Cause further queries for the attributes of inodes to use the supplied // time as the inode's mtime. SetMtime(mtime time.Time) + + // Instruct the file system whether or not to reply to OpenFileOp with + // FOPEN_KEEP_CACHE set. + SetKeepCache(keep bool) } // Create a file system that issues cacheable responses according to the @@ -116,6 +126,9 @@ type cachingFS struct { mu syncutil.InvariantMutex + // GUARDED_BY(mu) + keepPageCache bool + // The current ID of the lowest numbered non-root inode. // // INVARIANT: baseID > fuseops.RootInodeID @@ -236,6 +249,14 @@ func (fs *cachingFS) SetMtime(mtime time.Time) { fs.mtime = mtime } +// LOCKS_EXCLUDED(fs.mu) +func (fs *cachingFS) SetKeepCache(keep bool) { + fs.mu.Lock() + defer fs.mu.Unlock() + + fs.keepPageCache = keep +} + //////////////////////////////////////////////////////////////////////// // FileSystem methods //////////////////////////////////////////////////////////////////////// @@ -335,5 +356,17 @@ func (fs *cachingFS) OpenDir( func (fs *cachingFS) OpenFile( ctx context.Context, op *fuseops.OpenFileOp) (err error) { + fs.mu.Lock() + defer fs.mu.Unlock() + + op.KeepPageCache = fs.keepPageCache + + return +} + +func (fs *cachingFS) ReadFile( + ctx context.Context, + op *fuseops.ReadFileOp) (err error) { + op.BytesRead, err = io.ReadFull(rand.Reader, op.Dst) return } diff --git a/samples/cachingfs/caching_fs_test.go b/samples/cachingfs/caching_fs_test.go index ea9f306..19cdf39 100644 --- a/samples/cachingfs/caching_fs_test.go +++ b/samples/cachingfs/caching_fs_test.go @@ -15,6 +15,8 @@ package cachingfs_test import ( + "bytes" + "io/ioutil" "os" "path" "runtime" @@ -531,3 +533,189 @@ func (t *AttributeCachingTest) StatRenumberMtimeStat_ViaFileDescriptor() { ExpectThat(dirAfter.ModTime(), timeutil.TimeEq(newMtime)) ExpectThat(barAfter.ModTime(), timeutil.TimeEq(newMtime)) } + +//////////////////////////////////////////////////////////////////////// +// Page cache +//////////////////////////////////////////////////////////////////////// + +type PageCacheTest struct { + cachingFSTest +} + +var _ SetUpInterface = &PageCacheTest{} + +func init() { RegisterTestSuite(&PageCacheTest{}) } + +func (t *PageCacheTest) SetUp(ti *TestInfo) { + const ( + lookupEntryTimeout = 0 + getattrTimeout = 0 + ) + + t.cachingFSTest.setUp(ti, lookupEntryTimeout, getattrTimeout) +} + +func (t *PageCacheTest) SingleFileHandle_NoKeepCache() { + t.fs.SetKeepCache(false) + + // Open the file. + f, err := os.Open(path.Join(t.Dir, "foo")) + AssertEq(nil, err) + + defer f.Close() + + // Read its contents once. + f.Seek(0, 0) + AssertEq(nil, err) + + c1, err := ioutil.ReadAll(f) + AssertEq(nil, err) + AssertEq(cachingfs.FooSize, len(c1)) + + // And again. + f.Seek(0, 0) + AssertEq(nil, err) + + c2, err := ioutil.ReadAll(f) + AssertEq(nil, err) + AssertEq(cachingfs.FooSize, len(c2)) + + // We should have seen the same contents each time. + ExpectTrue(bytes.Equal(c1, c2)) +} + +func (t *PageCacheTest) SingleFileHandle_KeepCache() { + t.fs.SetKeepCache(true) + + // Open the file. + f, err := os.Open(path.Join(t.Dir, "foo")) + AssertEq(nil, err) + + defer f.Close() + + // Read its contents once. + f.Seek(0, 0) + AssertEq(nil, err) + + c1, err := ioutil.ReadAll(f) + AssertEq(nil, err) + AssertEq(cachingfs.FooSize, len(c1)) + + // And again. + f.Seek(0, 0) + AssertEq(nil, err) + + c2, err := ioutil.ReadAll(f) + AssertEq(nil, err) + AssertEq(cachingfs.FooSize, len(c2)) + + // We should have seen the same contents each time. + ExpectTrue(bytes.Equal(c1, c2)) +} + +func (t *PageCacheTest) TwoFileHandles_NoKeepCache() { + t.fs.SetKeepCache(false) + + // SetKeepCache(false) doesn't work on OS X. See the notes on + // OpenFileOp.KeepPageCache. + if runtime.GOOS == "darwin" { + return + } + + // Open the file. + f1, err := os.Open(path.Join(t.Dir, "foo")) + AssertEq(nil, err) + + defer f1.Close() + + // Read its contents once. + f1.Seek(0, 0) + AssertEq(nil, err) + + c1, err := ioutil.ReadAll(f1) + AssertEq(nil, err) + AssertEq(cachingfs.FooSize, len(c1)) + + // Open a second handle. + f2, err := os.Open(path.Join(t.Dir, "foo")) + AssertEq(nil, err) + + defer f2.Close() + + // We should see different contents if we read from that handle, due to the + // cache being invalidated at the time of opening. + f2.Seek(0, 0) + AssertEq(nil, err) + + c2, err := ioutil.ReadAll(f2) + AssertEq(nil, err) + AssertEq(cachingfs.FooSize, len(c2)) + + ExpectFalse(bytes.Equal(c1, c2)) + + // Another read from the second handle should give the same result as the + // first one from that handle. + f2.Seek(0, 0) + AssertEq(nil, err) + + c3, err := ioutil.ReadAll(f2) + AssertEq(nil, err) + AssertEq(cachingfs.FooSize, len(c3)) + + ExpectTrue(bytes.Equal(c2, c3)) + + // And another read from the first handle should give the same result yet + // again. + f1.Seek(0, 0) + AssertEq(nil, err) + + c4, err := ioutil.ReadAll(f1) + AssertEq(nil, err) + AssertEq(cachingfs.FooSize, len(c4)) + + ExpectTrue(bytes.Equal(c2, c4)) +} + +func (t *PageCacheTest) TwoFileHandles_KeepCache() { + t.fs.SetKeepCache(true) + + // Open the file. + f1, err := os.Open(path.Join(t.Dir, "foo")) + AssertEq(nil, err) + + defer f1.Close() + + // Read its contents once. + f1.Seek(0, 0) + AssertEq(nil, err) + + c1, err := ioutil.ReadAll(f1) + AssertEq(nil, err) + AssertEq(cachingfs.FooSize, len(c1)) + + // Open a second handle. + f2, err := os.Open(path.Join(t.Dir, "foo")) + AssertEq(nil, err) + + defer f2.Close() + + // We should see the same contents when we read via the second handle. + f2.Seek(0, 0) + AssertEq(nil, err) + + c2, err := ioutil.ReadAll(f2) + AssertEq(nil, err) + AssertEq(cachingfs.FooSize, len(c2)) + + ExpectTrue(bytes.Equal(c1, c2)) + + // Ditto if we read again from the first. + f1.Seek(0, 0) + AssertEq(nil, err) + + c3, err := ioutil.ReadAll(f1) + AssertEq(nil, err) + AssertEq(cachingfs.FooSize, len(c3)) + + ExpectTrue(bytes.Equal(c1, c3)) +}