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
This commit is contained in:
Alan Somers 2019-06-26 02:09:22 +00:00
parent 6efc53b9f3
commit fef464546c
Notes: svn2git 2020-12-20 02:59:44 +00:00
svn path=/projects/fuse2/; revision=349403
8 changed files with 69 additions and 6 deletions

View File

@ -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);

View File

@ -206,6 +206,7 @@ struct fuse_data {
struct selinfo ks_rsel;
int daemon_timeout;
unsigned time_gran;
uint64_t notimpl;
uint64_t mnt_flag;
};

View File

@ -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)

View File

@ -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);
}

View File

@ -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();

View File

@ -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

View File

@ -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();

View File

@ -229,6 +229,14 @@ virtual void SetUp() {
}
};
class TimeGran: public WriteBackAsync, public WithParamInterface<unsigned> {
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
*/