Fix gofer rename ctime and cleanup stat_times test

There is a lot of redundancy that we can simplify in the stat_times
test. This will make it easier to add new tests. However, the
simplification reveals that cached uattrs on goferfs don't properly
update ctime on rename.

PiperOrigin-RevId: 248773425
Change-Id: I52662728e1e9920981555881f9a85f9ce04041cf
This commit is contained in:
Michael Pratt
2019-05-17 13:05:47 -07:00
committed by Shentubot
parent 114bb3a234
commit 04105781ad
12 changed files with 221 additions and 158 deletions
+2 -2
View File
@@ -341,7 +341,7 @@ func (InodeNotDirectory) RemoveDirectory(context.Context, *fs.Inode, string) err
}
// Rename implements fs.FileOperations.Rename.
func (InodeNotDirectory) Rename(context.Context, *fs.Inode, string, *fs.Inode, string, bool) error {
func (InodeNotDirectory) Rename(context.Context, *fs.Inode, *fs.Inode, string, *fs.Inode, string, bool) error {
return syserror.EINVAL
}
@@ -381,7 +381,7 @@ func (InodeNoopTruncate) Truncate(context.Context, *fs.Inode, int64) error {
type InodeNotRenameable struct{}
// Rename implements fs.InodeOperations.Rename.
func (InodeNotRenameable) Rename(context.Context, *fs.Inode, string, *fs.Inode, string, bool) error {
func (InodeNotRenameable) Rename(context.Context, *fs.Inode, *fs.Inode, string, *fs.Inode, string, bool) error {
return syserror.EINVAL
}
+8
View File
@@ -451,6 +451,14 @@ func (c *CachingInodeOperations) touchModificationTimeLocked(now time.Time) {
c.dirtyAttr.StatusChangeTime = true
}
// TouchStatusChangeTime updates the cached status change time in-place to the
// current time.
func (c *CachingInodeOperations) TouchStatusChangeTime(ctx context.Context) {
c.attrMu.Lock()
c.touchStatusChangeTimeLocked(ktime.NowFromContext(ctx))
c.attrMu.Unlock()
}
// touchStatusChangeTimeLocked updates the cached status change time
// in-place to the current time.
//
+6 -1
View File
@@ -344,7 +344,7 @@ func (i *inodeOperations) RemoveDirectory(ctx context.Context, dir *fs.Inode, na
}
// Rename renames this node.
func (i *inodeOperations) Rename(ctx context.Context, oldParent *fs.Inode, oldName string, newParent *fs.Inode, newName string, replacement bool) error {
func (i *inodeOperations) Rename(ctx context.Context, inode *fs.Inode, oldParent *fs.Inode, oldName string, newParent *fs.Inode, newName string, replacement bool) error {
if len(newName) > maxFilenameLen {
return syserror.ENAMETOOLONG
}
@@ -389,6 +389,11 @@ func (i *inodeOperations) Rename(ctx context.Context, oldParent *fs.Inode, oldNa
newParentInodeOperations.markDirectoryDirty()
}
}
// Rename always updates ctime.
if i.session().cachePolicy.cacheUAttrs(inode) {
i.cachingInodeOps.TouchStatusChangeTime(ctx)
}
return nil
}
+1 -1
View File
@@ -301,7 +301,7 @@ func (i *inodeOperations) RemoveDirectory(ctx context.Context, dir *fs.Inode, na
}
// Rename implements fs.InodeOperations.Rename.
func (i *inodeOperations) Rename(ctx context.Context, oldParent *fs.Inode, oldName string, newParent *fs.Inode, newName string, replacement bool) error {
func (i *inodeOperations) Rename(ctx context.Context, inode *fs.Inode, oldParent *fs.Inode, oldName string, newParent *fs.Inode, newName string, replacement bool) error {
op, ok := oldParent.InodeOperations.(*inodeOperations)
if !ok {
return syscall.EXDEV
+1 -1
View File
@@ -216,7 +216,7 @@ func (i *Inode) Rename(ctx context.Context, oldParent *Dirent, renamed *Dirent,
if i.overlay != nil {
return overlayRename(ctx, i.overlay, oldParent, renamed, newParent, newName, replacement)
}
return i.InodeOperations.Rename(ctx, oldParent.Inode, renamed.name, newParent.Inode, newName, replacement)
return i.InodeOperations.Rename(ctx, renamed.Inode, oldParent.Inode, renamed.name, newParent.Inode, newName, replacement)
}
// Bind calls i.InodeOperations.Bind with i as the directory.
+3 -2
View File
@@ -131,14 +131,15 @@ type InodeOperations interface {
RemoveDirectory(ctx context.Context, dir *Inode, name string) error
// Rename atomically renames oldName under oldParent to newName under
// newParent where oldParent and newParent are directories.
// newParent where oldParent and newParent are directories. inode is
// the Inode of this InodeOperations.
//
// If replacement is true, then newName already exists and this call
// will replace it with oldName.
//
// Implementations are responsible for rejecting renames that replace
// non-empty directories.
Rename(ctx context.Context, oldParent *Inode, oldName string, newParent *Inode, newName string, replacement bool) error
Rename(ctx context.Context, inode *Inode, oldParent *Inode, oldName string, newParent *Inode, newName string, replacement bool) error
// Bind binds a new socket under dir at the given name.
//
+1 -1
View File
@@ -389,7 +389,7 @@ func overlayRename(ctx context.Context, o *overlayEntry, oldParent *Dirent, rena
return err
}
oldName := renamed.name
if err := o.upper.InodeOperations.Rename(ctx, oldParent.Inode.overlay.upper, oldName, newParent.Inode.overlay.upper, newName, replacement); err != nil {
if err := o.upper.InodeOperations.Rename(ctx, renamed.Inode.overlay.upper, oldParent.Inode.overlay.upper, oldName, newParent.Inode.overlay.upper, newName, replacement); err != nil {
return err
}
if renamed.Inode.overlay.lowerExists {
+1 -1
View File
@@ -132,7 +132,7 @@ func (n *MockInodeOperations) CreateDirectory(context.Context, *Inode, string, F
}
// Rename implements fs.InodeOperations.Rename.
func (n *MockInodeOperations) Rename(ctx context.Context, oldParent *Inode, oldName string, newParent *Inode, newName string, replacement bool) error {
func (n *MockInodeOperations) Rename(ctx context.Context, inode *Inode, oldParent *Inode, oldName string, newParent *Inode, newName string, replacement bool) error {
n.renameCalled = true
return nil
}
+1 -1
View File
@@ -401,7 +401,7 @@ func (d *Dir) GetFile(ctx context.Context, dirent *fs.Dirent, flags fs.FileFlags
}
// Rename implements fs.InodeOperations.Rename.
func (*Dir) Rename(ctx context.Context, oldParent *fs.Inode, oldName string, newParent *fs.Inode, newName string, replacement bool) error {
func (*Dir) Rename(ctx context.Context, inode *fs.Inode, oldParent *fs.Inode, oldName string, newParent *fs.Inode, newName string, replacement bool) error {
return Rename(ctx, oldParent.InodeOperations, oldName, newParent.InodeOperations, newName, replacement)
}
+1 -1
View File
@@ -149,7 +149,7 @@ func (f *fileInodeOperations) Mappable(*fs.Inode) memmap.Mappable {
}
// Rename implements fs.InodeOperations.Rename.
func (*fileInodeOperations) Rename(ctx context.Context, oldParent *fs.Inode, oldName string, newParent *fs.Inode, newName string, replacement bool) error {
func (*fileInodeOperations) Rename(ctx context.Context, inode *fs.Inode, oldParent *fs.Inode, oldName string, newParent *fs.Inode, newName string, replacement bool) error {
return rename(ctx, oldParent, oldName, newParent, newName, replacement)
}
+4 -4
View File
@@ -238,7 +238,7 @@ func (d *Dir) newCreateOps() *ramfs.CreateOps {
}
// Rename implements fs.InodeOperations.Rename.
func (d *Dir) Rename(ctx context.Context, oldParent *fs.Inode, oldName string, newParent *fs.Inode, newName string, replacement bool) error {
func (d *Dir) Rename(ctx context.Context, inode *fs.Inode, oldParent *fs.Inode, oldName string, newParent *fs.Inode, newName string, replacement bool) error {
return rename(ctx, oldParent, oldName, newParent, newName, replacement)
}
@@ -271,7 +271,7 @@ func NewSymlink(ctx context.Context, target string, owner fs.FileOwner, msrc *fs
}
// Rename implements fs.InodeOperations.Rename.
func (s *Symlink) Rename(ctx context.Context, oldParent *fs.Inode, oldName string, newParent *fs.Inode, newName string, replacement bool) error {
func (s *Symlink) Rename(ctx context.Context, inode *fs.Inode, oldParent *fs.Inode, oldName string, newParent *fs.Inode, newName string, replacement bool) error {
return rename(ctx, oldParent, oldName, newParent, newName, replacement)
}
@@ -301,7 +301,7 @@ func NewSocket(ctx context.Context, socket transport.BoundEndpoint, owner fs.Fil
}
// Rename implements fs.InodeOperations.Rename.
func (s *Socket) Rename(ctx context.Context, oldParent *fs.Inode, oldName string, newParent *fs.Inode, newName string, replacement bool) error {
func (s *Socket) Rename(ctx context.Context, inode *fs.Inode, oldParent *fs.Inode, oldName string, newParent *fs.Inode, newName string, replacement bool) error {
return rename(ctx, oldParent, oldName, newParent, newName, replacement)
}
@@ -338,7 +338,7 @@ func NewFifo(ctx context.Context, owner fs.FileOwner, perms fs.FilePermissions,
}
// Rename implements fs.InodeOperations.Rename.
func (f *Fifo) Rename(ctx context.Context, oldParent *fs.Inode, oldName string, newParent *fs.Inode, newName string, replacement bool) error {
func (f *Fifo) Rename(ctx context.Context, inode *fs.Inode, oldParent *fs.Inode, oldName string, newParent *fs.Inode, newName string, replacement bool) error {
return rename(ctx, oldParent, oldName, newParent, newName, replacement)
}
+192 -143
View File
@@ -33,23 +33,88 @@ namespace {
using ::testing::IsEmpty;
using ::testing::Not;
class StatTimesTest : public ::testing::Test {
protected:
std::tuple<absl::Time, absl::Time, absl::Time> GetTime(const TempPath& file) {
struct stat statbuf = {};
EXPECT_THAT(stat(file.path().c_str(), &statbuf), SyscallSucceeds());
std::tuple<absl::Time, absl::Time, absl::Time> GetTime(const TempPath& file) {
struct stat statbuf = {};
EXPECT_THAT(stat(file.path().c_str(), &statbuf), SyscallSucceeds());
const auto atime = absl::TimeFromTimespec(statbuf.st_atim);
const auto mtime = absl::TimeFromTimespec(statbuf.st_mtim);
const auto ctime = absl::TimeFromTimespec(statbuf.st_ctim);
return std::make_tuple(atime, mtime, ctime);
}
const auto atime = absl::TimeFromTimespec(statbuf.st_atim);
const auto mtime = absl::TimeFromTimespec(statbuf.st_mtim);
const auto ctime = absl::TimeFromTimespec(statbuf.st_ctim);
return std::make_tuple(atime, mtime, ctime);
}
enum class AtimeEffect {
Unchanged,
Changed,
};
TEST_F(StatTimesTest, FileCreationTimes) {
enum class MtimeEffect {
Unchanged,
Changed,
};
enum class CtimeEffect {
Unchanged,
Changed,
};
// Tests that fn modifies the atime/mtime/ctime of path as specified.
void CheckTimes(const TempPath& path, std::function<void()> fn,
AtimeEffect atime_effect, MtimeEffect mtime_effect,
CtimeEffect ctime_effect) {
absl::Time atime, mtime, ctime;
std::tie(atime, mtime, ctime) = GetTime(path);
// FIXME(b/132819225): gVisor filesystem timestamps inconsistently use the
// internal or host clock, which may diverge slightly. Allow some slack on
// times to account for the difference.
//
// Here we sleep for 1s so that initial creation of path doesn't fall within
// the before slack window.
absl::SleepFor(absl::Seconds(1));
const absl::Time before = absl::Now() - absl::Seconds(1);
// Perform the op.
fn();
const absl::Time after = absl::Now() + absl::Seconds(1);
absl::Time atime2, mtime2, ctime2;
std::tie(atime2, mtime2, ctime2) = GetTime(path);
if (atime_effect == AtimeEffect::Changed) {
EXPECT_LE(before, atime2);
EXPECT_GE(after, atime2);
EXPECT_GT(atime2, atime);
} else {
EXPECT_EQ(atime2, atime);
}
if (mtime_effect == MtimeEffect::Changed) {
EXPECT_LE(before, mtime2);
EXPECT_GE(after, mtime2);
EXPECT_GT(mtime2, mtime);
} else {
EXPECT_EQ(mtime2, mtime);
}
if (ctime_effect == CtimeEffect::Changed) {
EXPECT_LE(before, ctime2);
EXPECT_GE(after, ctime2);
EXPECT_GT(ctime2, ctime);
} else {
EXPECT_EQ(ctime2, ctime);
}
}
// File creation time is reflected in atime, mtime, and ctime.
TEST(StatTimesTest, FileCreation) {
const DisableSave ds; // Timing-related test.
// Get a time for when the file is created.
//
// FIXME(b/132819225): See above.
const absl::Time before = absl::Now() - absl::Seconds(1);
const TempPath file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
const absl::Time after = absl::Now() + absl::Seconds(1);
@@ -65,153 +130,137 @@ TEST_F(StatTimesTest, FileCreationTimes) {
EXPECT_GE(after, ctime);
}
TEST_F(StatTimesTest, FileCtimeChanges) {
auto file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
// Calling chmod on a file changes ctime.
TEST(StatTimesTest, FileChmod) {
TempPath file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
MaybeSave(); // FIXME(b/69865927): ctime is inconsistent.
absl::Time atime, mtime, ctime;
std::tie(atime, mtime, ctime) = GetTime(file);
absl::SleepFor(absl::Seconds(1));
// Chmod should only change ctime.
EXPECT_THAT(chmod(file.path().c_str(), 0666), SyscallSucceeds());
absl::Time atime2, mtime2, ctime2;
std::tie(atime2, mtime2, ctime2) = GetTime(file);
EXPECT_EQ(atime2, atime);
EXPECT_EQ(mtime2, mtime);
EXPECT_GT(ctime2, ctime);
absl::SleepFor(absl::Seconds(1));
// Rename should only change ctime.
const auto newpath = NewTempAbsPath();
EXPECT_THAT(rename(file.path().c_str(), newpath.c_str()), SyscallSucceeds());
file.reset(newpath);
std::tie(atime, mtime, ctime) = GetTime(file);
EXPECT_EQ(atime, atime2);
EXPECT_EQ(mtime, mtime2);
EXPECT_GT(ctime, ctime2);
absl::SleepFor(absl::Seconds(1));
// Utimes should only change ctime and the time that we ask to change (atime
// to now in this case).
const absl::Time before = absl::Now() - absl::Seconds(1);
const struct timespec ts[2] = {{0, UTIME_NOW}, {0, UTIME_OMIT}};
ASSERT_THAT(utimensat(AT_FDCWD, file.path().c_str(), ts, 0),
SyscallSucceeds());
const absl::Time after = absl::Now() + absl::Seconds(1);
std::tie(atime2, mtime2, ctime2) = GetTime(file);
EXPECT_LE(before, atime2);
EXPECT_GE(after, atime2);
EXPECT_EQ(mtime2, mtime);
EXPECT_GT(ctime2, ctime);
auto fn = [&] {
EXPECT_THAT(chmod(file.path().c_str(), 0666), SyscallSucceeds());
};
CheckTimes(file, fn, AtimeEffect::Unchanged, MtimeEffect::Unchanged,
CtimeEffect::Changed);
}
TEST_F(StatTimesTest, FileMtimeChanges) {
const auto file = ASSERT_NO_ERRNO_AND_VALUE(
// Renaming a file changes ctime.
TEST(StatTimesTest, FileRename) {
TempPath file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
const std::string newpath = NewTempAbsPath();
auto fn = [&] {
ASSERT_THAT(rename(file.release().c_str(), newpath.c_str()),
SyscallSucceeds());
file.reset(newpath);
};
CheckTimes(file, fn, AtimeEffect::Unchanged, MtimeEffect::Unchanged,
CtimeEffect::Changed);
}
// Renaming a file changes ctime, even with an open FD.
//
// NOTE(b/132732387): This is a regression test for fs/gofer failing to update
// cached ctime.
TEST(StatTimesTest, FileRenameOpenFD) {
TempPath file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
// Holding an FD shouldn't affect behavior.
const FileDescriptor fd =
ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_RDONLY));
const std::string newpath = NewTempAbsPath();
// FIXME(b/132814682): Restore fails with an uncached gofer and an open FD
// across rename.
//
// N.B. The logic here looks backwards because it isn't possible to
// conditionally disable save, only conditionally re-enable it.
DisableSave ds;
if (!getenv("GVISOR_GOFER_UNCACHED")) {
ds.reset();
}
auto fn = [&] {
ASSERT_THAT(rename(file.release().c_str(), newpath.c_str()),
SyscallSucceeds());
file.reset(newpath);
};
CheckTimes(file, fn, AtimeEffect::Unchanged, MtimeEffect::Unchanged,
CtimeEffect::Changed);
}
// Calling utimes on a file changes ctime and the time that we ask to change
// (atime to now in this case).
TEST(StatTimesTest, FileUtimes) {
TempPath file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
auto fn = [&] {
const struct timespec ts[2] = {{0, UTIME_NOW}, {0, UTIME_OMIT}};
ASSERT_THAT(utimensat(AT_FDCWD, file.path().c_str(), ts, 0),
SyscallSucceeds());
};
CheckTimes(file, fn, AtimeEffect::Changed, MtimeEffect::Unchanged,
CtimeEffect::Changed);
}
// Truncating a file changes mtime and ctime.
TEST(StatTimesTest, FileTruncate) {
const TempPath file = ASSERT_NO_ERRNO_AND_VALUE(
TempPath::CreateFileWith(GetAbsoluteTestTmpdir(), "yaaass", 0666));
absl::Time atime, mtime, ctime;
std::tie(atime, mtime, ctime) = GetTime(file);
absl::SleepFor(absl::Seconds(1));
// Truncate should only change mtime and ctime.
EXPECT_THAT(truncate(file.path().c_str(), 0), SyscallSucceeds());
absl::Time atime2, mtime2, ctime2;
std::tie(atime2, mtime2, ctime2) = GetTime(file);
EXPECT_EQ(atime2, atime);
EXPECT_GT(mtime2, mtime);
EXPECT_GT(ctime2, ctime);
absl::SleepFor(absl::Seconds(1));
// Write should only change mtime and ctime.
const auto fd = ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_RDWR, 0));
const std::string contents = "all the single dollars";
EXPECT_THAT(write(fd.get(), contents.data(), contents.size()),
SyscallSucceeds());
std::tie(atime, mtime, ctime) = GetTime(file);
EXPECT_EQ(atime, atime2);
EXPECT_GT(mtime, mtime2);
EXPECT_GT(ctime, ctime2);
auto fn = [&] {
EXPECT_THAT(truncate(file.path().c_str(), 0), SyscallSucceeds());
};
CheckTimes(file, fn, AtimeEffect::Unchanged, MtimeEffect::Changed,
CtimeEffect::Changed);
}
TEST_F(StatTimesTest, FileAtimeChanges) {
// Writing a file changes mtime and ctime.
TEST(StatTimesTest, FileWrite) {
const TempPath file = ASSERT_NO_ERRNO_AND_VALUE(
TempPath::CreateFileWith(GetAbsoluteTestTmpdir(), "yaaass", 0666));
const FileDescriptor fd =
ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_RDWR, 0));
auto fn = [&] {
const std::string contents = "all the single dollars";
EXPECT_THAT(WriteFd(fd.get(), contents.data(), contents.size()),
SyscallSucceeds());
};
CheckTimes(file, fn, AtimeEffect::Unchanged, MtimeEffect::Changed,
CtimeEffect::Changed);
}
// Reading a file changes atime.
TEST(StatTimesTest, FileRead) {
const std::string contents = "bills bills bills";
const auto file = ASSERT_NO_ERRNO_AND_VALUE(
const TempPath file = ASSERT_NO_ERRNO_AND_VALUE(
TempPath::CreateFileWith(GetAbsoluteTestTmpdir(), contents, 0666));
MaybeSave(); // FIXME(b/69865927): ctime is inconsistent.
const FileDescriptor fd =
ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_RDONLY, 0));
absl::Time atime, mtime, ctime;
std::tie(atime, mtime, ctime) = GetTime(file);
absl::SleepFor(absl::Seconds(1));
const auto fd = ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_RDONLY, 0));
// Read should only change atime.
char buf[20];
const absl::Time before = absl::Now() - absl::Seconds(1);
int read_result;
ASSERT_THAT(read_result = read(fd.get(), buf, sizeof(buf)),
SyscallSucceeds());
const absl::Time after = absl::Now() + absl::Seconds(1);
EXPECT_EQ(std::string(buf, read_result), contents);
absl::Time atime2, mtime2, ctime2;
std::tie(atime2, mtime2, ctime2) = GetTime(file);
EXPECT_LE(before, atime2);
EXPECT_GE(after, atime2);
EXPECT_GT(atime2, atime);
EXPECT_EQ(mtime2, mtime);
EXPECT_EQ(ctime2, ctime);
auto fn = [&] {
char buf[20];
ASSERT_THAT(ReadFd(fd.get(), buf, sizeof(buf)),
SyscallSucceedsWithValue(contents.size()));
};
CheckTimes(file, fn, AtimeEffect::Changed, MtimeEffect::Unchanged,
CtimeEffect::Unchanged);
}
TEST_F(StatTimesTest, DirAtimeChanges) {
const auto dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
const auto file =
// Listing files in a directory changes atime.
TEST(StatTimesTest, DirList) {
const TempPath dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
const TempPath file =
ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileIn(dir.path()));
MaybeSave(); // FIXME(b/69865927): ctime is inconsistent.
absl::Time atime, mtime, ctime;
std::tie(atime, mtime, ctime) = GetTime(dir);
absl::SleepFor(absl::Seconds(1));
const absl::Time before = absl::Now() - absl::Seconds(1);
// NOTE(b/37756234): Keep an fd open. This ensures that the inode backing the
// directory won't be destroyed before the final GetTime to avoid writing out
// timestamps and causing side effects.
const auto fd = ASSERT_NO_ERRNO_AND_VALUE(Open(dir.path(), O_RDONLY, 0));
// Listing the directory contents should only change atime.
auto contents = ASSERT_NO_ERRNO_AND_VALUE(ListDir(dir.path(), false));
EXPECT_THAT(contents, Not(IsEmpty()));
const absl::Time after = absl::Now() + absl::Seconds(1);
absl::Time atime2, mtime2, ctime2;
std::tie(atime2, mtime2, ctime2) = GetTime(dir);
EXPECT_LE(before, atime2);
EXPECT_GE(after, atime2);
EXPECT_GT(atime2, atime);
EXPECT_EQ(mtime2, mtime);
EXPECT_EQ(ctime2, ctime);
auto fn = [&] {
const auto contents = ASSERT_NO_ERRNO_AND_VALUE(ListDir(dir.path(), false));
EXPECT_THAT(contents, Not(IsEmpty()));
};
CheckTimes(dir, fn, AtimeEffect::Changed, MtimeEffect::Unchanged,
CtimeEffect::Unchanged);
}
} // namespace