From fef464546c387300a1f24f0109750d797a01f3b9 Mon Sep 17 00:00:00 2001 From: Alan Somers Date: Wed, 26 Jun 2019 02:09:22 +0000 Subject: [PATCH] fusefs: implement the "time_gran" feature. If a server supports a timestamp granularity other than 1ns, it can tell the client this as of protocol 7.23. The client will use that granularity when updating its cached timestamps during write. This way the timestamps won't appear to change following flush. Sponsored by: The FreeBSD Foundation --- sys/fs/fuse/fuse_internal.c | 10 ++++++-- sys/fs/fuse/fuse_ipc.h | 1 + sys/fs/fuse/fuse_node.c | 4 ++++ tests/sys/fs/fusefs/mockfs.cc | 4 +++- tests/sys/fs/fusefs/mockfs.hh | 5 +++- tests/sys/fs/fusefs/utils.cc | 2 +- tests/sys/fs/fusefs/utils.hh | 4 +++- tests/sys/fs/fusefs/write.cc | 45 +++++++++++++++++++++++++++++++++++ 8 files changed, 69 insertions(+), 6 deletions(-) diff --git a/sys/fs/fuse/fuse_internal.c b/sys/fs/fuse/fuse_internal.c index 5df6e18444ca..68ab4c5b0794 100644 --- a/sys/fs/fuse/fuse_internal.c +++ b/sys/fs/fuse/fuse_internal.c @@ -934,8 +934,8 @@ fuse_internal_init_callback(struct fuse_ticket *tick, struct uio *uio) * redundant with max_write */ /* - * max_background, congestion_threshold, and time_gran - * are not implemented + * max_background and congestion_threshold are not + * implemented */ } else { err = EINVAL; @@ -956,6 +956,12 @@ fuse_internal_init_callback(struct fuse_ticket *tick, struct uio *uio) fsess_set_notimpl(data->mp, FUSE_DESTROY); } + if (fuse_libabi_geq(data, 7, 23) && fiio->time_gran >= 1 && + fiio->time_gran <= 1000000000) + data->time_gran = fiio->time_gran; + else + data->time_gran = 1; + out: if (err) { fdata_set_dead(data); diff --git a/sys/fs/fuse/fuse_ipc.h b/sys/fs/fuse/fuse_ipc.h index 0f9b1180f362..810792fc9c8e 100644 --- a/sys/fs/fuse/fuse_ipc.h +++ b/sys/fs/fuse/fuse_ipc.h @@ -206,6 +206,7 @@ struct fuse_data { struct selinfo ks_rsel; int daemon_timeout; + unsigned time_gran; uint64_t notimpl; uint64_t mnt_flag; }; diff --git a/sys/fs/fuse/fuse_node.c b/sys/fs/fuse/fuse_node.c index d5429e905f0c..7a0774a19778 100644 --- a/sys/fs/fuse/fuse_node.c +++ b/sys/fs/fuse/fuse_node.c @@ -455,10 +455,14 @@ void fuse_vnode_update(struct vnode *vp, int flags) { struct fuse_vnode_data *fvdat = VTOFUD(vp); + struct fuse_data *data = fuse_get_mpdata(vnode_mount(vp)); struct timespec ts; vfs_timestamp(&ts); + if (data->time_gran > 1) + ts.tv_nsec = rounddown(ts.tv_nsec, data->time_gran); + if (flags & FN_MTIMECHANGE) fvdat->cached_attrs.va_mtime = ts; if (flags & FN_CTIMECHANGE) diff --git a/tests/sys/fs/fusefs/mockfs.cc b/tests/sys/fs/fusefs/mockfs.cc index 0584b43db482..e4f28b9563d0 100644 --- a/tests/sys/fs/fusefs/mockfs.cc +++ b/tests/sys/fs/fusefs/mockfs.cc @@ -348,7 +348,7 @@ void MockFS::debug_response(const mockfs_buf_out &out) { MockFS::MockFS(int max_readahead, bool allow_other, bool default_permissions, bool push_symlinks_in, bool ro, enum poll_method pm, uint32_t flags, uint32_t kernel_minor_version, uint32_t max_write, bool async, - bool noclusterr) + bool noclusterr, unsigned time_gran) { struct sigaction sa; struct iovec *iov = NULL; @@ -362,6 +362,7 @@ MockFS::MockFS(int max_readahead, bool allow_other, bool default_permissions, m_maxwrite = max_write; m_nready = -1; m_pm = pm; + m_time_gran = time_gran; m_quit = false; if (m_pm == KQ) m_kq = kqueue(); @@ -475,6 +476,7 @@ void MockFS::init(uint32_t flags) { if (m_kernel_minor_version < 23) { SET_OUT_HEADER_LEN(*out, init_7_22); } else { + out->body.init.time_gran = m_time_gran; SET_OUT_HEADER_LEN(*out, init); } diff --git a/tests/sys/fs/fusefs/mockfs.hh b/tests/sys/fs/fusefs/mockfs.hh index e8bd09df1c8d..91bb248c6f80 100644 --- a/tests/sys/fs/fusefs/mockfs.hh +++ b/tests/sys/fs/fusefs/mockfs.hh @@ -277,6 +277,9 @@ class MockFS { /* Method the daemon should use for I/O to and from /dev/fuse */ enum poll_method m_pm; + /* Timestamp granularity in nanoseconds */ + unsigned m_time_gran; + void debug_request(const mockfs_buf_in&); void debug_response(const mockfs_buf_out&); @@ -320,7 +323,7 @@ class MockFS { bool default_permissions, bool push_symlinks_in, bool ro, enum poll_method pm, uint32_t flags, uint32_t kernel_minor_version, uint32_t max_write, bool async, - bool no_clusterr); + bool no_clusterr, unsigned time_gran); virtual ~MockFS(); diff --git a/tests/sys/fs/fusefs/utils.cc b/tests/sys/fs/fusefs/utils.cc index eda43a5dcbf5..cca5d3582c2c 100644 --- a/tests/sys/fs/fusefs/utils.cc +++ b/tests/sys/fs/fusefs/utils.cc @@ -117,7 +117,7 @@ void FuseTest::SetUp() { m_mock = new MockFS(m_maxreadahead, m_allow_other, m_default_permissions, m_push_symlinks_in, m_ro, m_pm, m_init_flags, m_kernel_minor_version, - m_maxwrite, m_async, m_noclusterr); + m_maxwrite, m_async, m_noclusterr, m_time_gran); /* * FUSE_ACCESS is called almost universally. Expecting it in * each test case would be super-annoying. Instead, set a diff --git a/tests/sys/fs/fusefs/utils.hh b/tests/sys/fs/fusefs/utils.hh index 174ed0e71f7c..2dbca6808b07 100644 --- a/tests/sys/fs/fusefs/utils.hh +++ b/tests/sys/fs/fusefs/utils.hh @@ -55,6 +55,7 @@ class FuseTest : public ::testing::Test { bool m_ro; bool m_async; bool m_noclusterr; + unsigned m_time_gran; MockFS *m_mock = NULL; const static uint64_t FH = 0xdeadbeef1a7ebabe; @@ -73,7 +74,8 @@ class FuseTest : public ::testing::Test { m_push_symlinks_in(false), m_ro(false), m_async(false), - m_noclusterr(false) + m_noclusterr(false), + m_time_gran(1) {} virtual void SetUp(); diff --git a/tests/sys/fs/fusefs/write.cc b/tests/sys/fs/fusefs/write.cc index 147027d1b0ae..ae46a742af40 100644 --- a/tests/sys/fs/fusefs/write.cc +++ b/tests/sys/fs/fusefs/write.cc @@ -229,6 +229,14 @@ virtual void SetUp() { } }; +class TimeGran: public WriteBackAsync, public WithParamInterface { +public: +virtual void SetUp() { + m_time_gran = 1 << GetParam(); + WriteBackAsync::SetUp(); +} +}; + /* Tests for clustered writes with WriteBack cacheing */ class WriteCluster: public WriteBack { public: @@ -1135,6 +1143,43 @@ TEST_F(WriteBackAsync, timestamps_during_setattr) ASSERT_EQ(0, fchmod(fd, newmode)) << strerror(errno); } +/* fuse_init_out.time_gran controls the granularity of timestamps */ +TEST_P(TimeGran, timestamps_during_setattr) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + ssize_t bufsize = strlen(CONTENTS); + uint64_t ino = 42; + const mode_t newmode = 0755; + int fd; + + expect_lookup(RELPATH, ino, 0); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + uint32_t valid = FATTR_MODE | FATTR_MTIME | FATTR_CTIME; + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino && + in.body.setattr.valid == valid && + in.body.setattr.mtimensec % m_time_gran == 0 && + in.body.setattr.ctimensec % m_time_gran == 0); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; + out.body.attr.attr.mode = S_IFREG | newmode; + }))); + + fd = open(FULLPATH, O_RDWR); + EXPECT_LE(0, fd) << strerror(errno); + ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); + ASSERT_EQ(0, fchmod(fd, newmode)) << strerror(errno); +} + +INSTANTIATE_TEST_CASE_P(RA, TimeGran, Range(0u, 10u)); + /* * Without direct_io, writes should be committed to cache */