diff --git a/cmd/flags.go b/cmd/flags.go index 2550f7c70cf39..1f7729a4aa550 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -214,6 +214,10 @@ func clientFlags() []cli.Flag { Name: "subdir", Usage: "mount a sub-directory as root", }, + &cli.StringFlag{ + Name: "atime-mode", + Usage: "when to update atime, supported mode includes: noatime (default), relatime, strictatime", + }, } } diff --git a/cmd/mount.go b/cmd/mount.go index d4c7eba93018e..df944b954d305 100644 --- a/cmd/mount.go +++ b/cmd/mount.go @@ -358,6 +358,13 @@ func getMetaConf(c *cli.Context, mp string, readOnly bool) *meta.Config { conf.Heartbeat = duration(c.String("heartbeat")) conf.MountPoint = mp conf.Subdir = c.String("subdir") + + atimeMode := c.String("atime-mode") + if atimeMode != meta.RelAtime && atimeMode != meta.StrictAtime { + logger.Warnf("unknown atime-mode \"%s\", changed to %s", atimeMode, meta.NoAtime) + atimeMode = meta.NoAtime + } + conf.AtimeMode = atimeMode return conf } diff --git a/docs/en/development/internals.md b/docs/en/development/internals.md index 18fd3d34ed275..680c4be27e284 100644 --- a/docs/en/development/internals.md +++ b/docs/en/development/internals.md @@ -173,7 +173,10 @@ type Attr struct { There are a few fields that need clarification. -- Atime/Atimensec: set only when the file is created and when `SetAttr` is actively called, while accessing and modifying the file usually does not affect the Atime value +- Atime/Atimensec: currently support three modes + - noatime: set only when the file is created and when `SetAttr` is actively called, while accessing and modifying the file usually does not affect the Atime value, this is the default behavior + - relatime: update inode access times relative to modify or change time. Access time is only updated if the previous access time was earlier than the current modify or change time, or the file's last access time is always updated if it is more than 1 day old + - strictatime: always update atime on access - Nlink - Directory file: initial value is 2 ('.' and '..'), add 1 for each subdirectory - Other files: initial value is 1, add 1 for each hard link created diff --git a/docs/en/reference/command_reference.md b/docs/en/reference/command_reference.md index 669e300cfa0a1..c27ffb07536ee 100644 --- a/docs/en/reference/command_reference.md +++ b/docs/en/reference/command_reference.md @@ -318,6 +318,9 @@ interval (in seconds) to send heartbeat; it's recommended that all clients use t `--no-bgjob`
disable background jobs (clean-up, backup, etc.) (default: false) +`--atime-mode value`
+control atime behavior, support 3 modes, `noatime`, `relatime`, `strictatime`, default to `noatime` + #### Examples ```bash diff --git a/docs/zh_cn/development/internals.md b/docs/zh_cn/development/internals.md index 34f2dc5a03b04..8c75a3327a16c 100644 --- a/docs/zh_cn/development/internals.md +++ b/docs/zh_cn/development/internals.md @@ -176,7 +176,10 @@ type Attr struct { 其中几个需要说明的字段: -- Atime/Atimensec:仅在文件创建和主动调用 `SetAttr` 时设置,平时访问与修改文件不影响 Atime 值 +- Atime/Atimensec:目前支持三种模式 + - noatime: 仅在文件创建和主动调用 `SetAttr` 时设置,平时访问与修改文件不影响 Atime 值,这是默认行为 + - relatime: Mtime 或者 Ctime 比 Atime 新时,或者 Atime 超过 24 小时没有更新时更新 Atime + - strictatime: 一直更新 Atime - Nlink: - 目录文件:初始值为 2('.' 和 '..'),每有一个子目录 Nlink 值加 1 - 其他文件:初始值为 1,每创建一个硬链接 Nlink 值加 1 diff --git a/docs/zh_cn/reference/command_reference.md b/docs/zh_cn/reference/command_reference.md index 5617767a412b4..3098abedaa507 100644 --- a/docs/zh_cn/reference/command_reference.md +++ b/docs/zh_cn/reference/command_reference.md @@ -318,6 +318,9 @@ Consul 注册中心地址 (默认:"127.0.0.1:8500") `--no-bgjob`
禁用后台作业(清理、备份等)(默认:false) +`--atime-mode value`
+控制如何更新 atime,支持 3 种模式,`noatime`,`relatime`,`strictatime`,默认使用 `noatime` + #### 示例 ```shell diff --git a/pkg/meta/base.go b/pkg/meta/base.go index 498d190617135..113a1c2a30be2 100644 --- a/pkg/meta/base.go +++ b/pkg/meta/base.go @@ -107,6 +107,7 @@ type engine interface { scanPendingFiles(Context, pendingFileScan) error GetSession(sid uint64, detail bool) (*Session, error) + touchAtime(ctx Context, inode Ino) syscall.Errno } type trashSliceScan func(ss []Slice, ts int64) (clean bool, err error) @@ -1561,7 +1562,14 @@ func (m *baseMeta) Close(ctx Context, inode Ino) syscall.Errno { return 0 } -func (m *baseMeta) Readdir(ctx Context, inode Ino, plus uint8, entries *[]*Entry) syscall.Errno { +func (m *baseMeta) Readdir(ctx Context, inode Ino, plus uint8, entries *[]*Entry) (rerr syscall.Errno) { + defer func() { + if rerr == 0 { + if err := m.en.touchAtime(ctx, inode); err != 0 { + logger.Warnf("readdir %v update atime: %s", inode, err) + } + } + }() inode = m.checkRoot(inode) var attr Attr if err := m.GetAttr(ctx, inode, &attr); err != 0 { diff --git a/pkg/meta/config.go b/pkg/meta/config.go index f1ddfbe13d284..71b9713040813 100644 --- a/pkg/meta/config.go +++ b/pkg/meta/config.go @@ -44,6 +44,7 @@ type Config struct { Heartbeat time.Duration MountPoint string Subdir string + AtimeMode string } func DefaultConf() *Config { diff --git a/pkg/meta/redis.go b/pkg/meta/redis.go index c210cda855795..2b613a9587d9a 100644 --- a/pkg/meta/redis.go +++ b/pkg/meta/redis.go @@ -2113,7 +2113,14 @@ func (m *redisMeta) doDeleteSustainedInode(sid uint64, inode Ino) error { return err } -func (m *redisMeta) Read(ctx Context, inode Ino, indx uint32, slices *[]Slice) syscall.Errno { +func (m *redisMeta) Read(ctx Context, inode Ino, indx uint32, slices *[]Slice) (rerr syscall.Errno) { + defer func() { + if rerr == 0 { + if err := m.touchAtime(ctx, inode); err != 0 { + logger.Warnf("read %v update atime: %s", inode, err) + } + } + }() f := m.of.find(inode) if f != nil { f.RLock() @@ -4240,3 +4247,31 @@ func (m *redisMeta) doAttachDirNode(ctx Context, parent Ino, dstIno Ino, name st return err }, m.inodeKey(parent), m.entryKey(parent))) } + +// caller makes sure inode is not special inode. +func (m *redisMeta) touchAtime(ctx Context, ino Ino) syscall.Errno { + if (m.conf.AtimeMode != StrictAtime && m.conf.AtimeMode != RelAtime) || m.conf.ReadOnly { + return 0 + } + + return errno(m.txn(ctx, func(tx *redis.Tx) error { + var attr Attr + a, err := tx.Get(ctx, m.inodeKey(ino)).Bytes() + if err != nil { + return err + } + m.parseAttr(a, &attr) + + now := time.Now() + if !m.atimeNeedsUpdate(&attr, now) { + return nil + } + attr.Atime = now.Unix() + attr.Atimensec = uint32(now.Nanosecond()) + _, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error { + pipe.Set(ctx, m.inodeKey(ino), m.marshal(&attr), 0) + return nil + }) + return err + }, m.inodeKey(ino))) +} diff --git a/pkg/meta/sql.go b/pkg/meta/sql.go index 0b26ada51e512..c64e6ee5d3691 100644 --- a/pkg/meta/sql.go +++ b/pkg/meta/sql.go @@ -2062,7 +2062,14 @@ func (m *dbMeta) doDeleteSustainedInode(sid uint64, inode Ino) error { return err } -func (m *dbMeta) Read(ctx Context, inode Ino, indx uint32, slices *[]Slice) syscall.Errno { +func (m *dbMeta) Read(ctx Context, inode Ino, indx uint32, slices *[]Slice) (rerr syscall.Errno) { + defer func() { + if rerr == 0 { + if err := m.touchAtime(ctx, inode); err != 0 { + logger.Warnf("read %v update atime: %s", inode, err) + } + } + }() f := m.of.find(inode) if f != nil { f.RLock() @@ -3900,3 +3907,30 @@ func (m *dbMeta) doAttachDirNode(ctx Context, parent Ino, inode Ino, name string return err }, parent)) } + +func (m *dbMeta) touchAtime(ctx Context, ino Ino) syscall.Errno { + if (m.conf.AtimeMode != StrictAtime && m.conf.AtimeMode != RelAtime) || m.conf.ReadOnly { + return 0 + } + + return errno(m.txn(func(s *xorm.Session) error { + var cur = node{Inode: ino} + var attr = Attr{} + ok, err := s.ForUpdate().Get(&cur) + if err != nil { + return err + } + if !ok { + return syscall.ENOENT + } + + now := time.Now() + m.parseAttr(&cur, &attr) + if !m.atimeNeedsUpdate(&attr, now) { + return nil + } + cur.Atime = now.Unix()*1e6 + int64(now.Nanosecond())/1e3 + _, err = s.Cols("flags", "mode", "uid", "gid", "atime", "mtime", "ctime").Update(&cur, &node{Inode: ino}) + return err + })) +} diff --git a/pkg/meta/tkv.go b/pkg/meta/tkv.go index 02b75047d4cb9..bd7b3915bfa4f 100644 --- a/pkg/meta/tkv.go +++ b/pkg/meta/tkv.go @@ -1816,7 +1816,14 @@ func (m *kvMeta) doDeleteSustainedInode(sid uint64, inode Ino) error { return err } -func (m *kvMeta) Read(ctx Context, inode Ino, indx uint32, slices *[]Slice) syscall.Errno { +func (m *kvMeta) Read(ctx Context, inode Ino, indx uint32, slices *[]Slice) (rerr syscall.Errno) { + defer func() { + if rerr == 0 { + if err := m.touchAtime(ctx, inode); err != 0 { + logger.Warnf("read %v update atime: %s", inode, err) + } + } + }() f := m.of.find(inode) if f != nil { f.RLock() @@ -3380,3 +3387,27 @@ func (m *kvMeta) doAttachDirNode(ctx Context, parent Ino, inode Ino, name string return nil }, parent)) } + +func (m *kvMeta) touchAtime(_ctx Context, ino Ino) syscall.Errno { + if (m.conf.AtimeMode != StrictAtime && m.conf.AtimeMode != RelAtime) || m.conf.ReadOnly { + return 0 + } + + return errno(m.txn(func(tx *kvTxn) error { + var attr Attr + a := tx.get(m.inodeKey(ino)) + if a == nil { + return syscall.ENOENT + } + m.parseAttr(a, &attr) + + now := time.Now() + if !m.atimeNeedsUpdate(&attr, now) { + return nil + } + attr.Atime = now.Unix() + attr.Atimensec = uint32(now.Nanosecond()) + tx.set(m.inodeKey(ino), m.marshal(&attr)) + return nil + })) +} diff --git a/pkg/meta/utils.go b/pkg/meta/utils.go index 401a26c7c5d4b..7114991bd70cc 100644 --- a/pkg/meta/utils.go +++ b/pkg/meta/utils.go @@ -53,6 +53,11 @@ const ( CLONE_MODE_CAN_OVERWRITE = 0x01 CLONE_MODE_PRESERVE_ATTR = 0x02 CLONE_MODE_PRESERVE_HARDLINKS = 0x08 + + // atime mode + NoAtime = "noatime" + RelAtime = "relatime" + StrictAtime = "strictatime" ) type msgCallbacks struct { @@ -553,3 +558,34 @@ func (m *baseMeta) getTreeSummary(ctx Context, tree *TreeSummary, depth, topN ui } return 0 } + +func (m *baseMeta) atimeNeedsUpdate(attr *Attr, now time.Time) bool { + if m.conf.AtimeMode == RelAtime && relatimeNeedUpdate(attr, now) { + return true + } + + return m.conf.AtimeMode == StrictAtime && !now.Equal(time.Unix(attr.Atime, int64(attr.Atimensec))) +} + +// With relative atime, only update atime if the previous atime is earlier than either the ctime or +// mtime or if at least a day has passed since the last atime update. +func relatimeNeedUpdate(attr *Attr, now time.Time) bool { + atime := time.Unix(attr.Atime, int64(attr.Atimensec)) + + // Is mtime younger than atime? If yes, update atime + if time.Unix(attr.Mtime, int64(attr.Mtimensec)).After(atime) { + return true + } + + // Is ctime younger than atime? If yes, update atime + if time.Unix(attr.Ctime, int64(attr.Ctimensec)).After(atime) { + return true + } + + // Is the previous atime value older than a day? If yes, update atime + if now.Sub(atime) > 24*time.Hour { + return true + } + + return false +} diff --git a/pkg/meta/utils_test.go b/pkg/meta/utils_test.go new file mode 100644 index 0000000000000..b9a4fbf28d4b2 --- /dev/null +++ b/pkg/meta/utils_test.go @@ -0,0 +1,81 @@ +/* + * JuiceFS, Copyright 2023 Juicedata, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package meta + +import ( + "testing" + "time" +) + +func TestRelatimeNeedUpdate(t *testing.T) { + attr := &Attr{ + Atime: 1000, + } + if !relatimeNeedUpdate(attr, time.Now()) { + t.Fatal("atime not updated for 24 hours") + } + + now := time.Now() + attr.Atime = now.Unix() + attr.Ctime = now.Unix() + 10 + if !relatimeNeedUpdate(attr, time.Now()) { + t.Fatal("atime not updated for ctime") + } + + now = time.Now() + attr.Atime = now.Unix() + attr.Mtime = now.Unix() + 10 + if !relatimeNeedUpdate(attr, time.Now()) { + t.Fatal("atime not updated for mtime") + } + + now = time.Now() + attr.Atime = now.Unix() + attr.Mtime = now.Unix() + attr.Ctime = now.Unix() + if relatimeNeedUpdate(attr, now) { + t.Fatal("atime should not be updated") + } +} + +func TestAtimeNeedsUpdate(t *testing.T) { + m := &baseMeta{ + conf: &Config{ + AtimeMode: NoAtime, + }, + } + attr := &Attr{ + Atime: 1000, + } + if m.atimeNeedsUpdate(attr, time.Now()) { + t.Fatal("atime updated for noatime") + } + + m.conf.AtimeMode = RelAtime + if !m.atimeNeedsUpdate(attr, time.Now()) { + t.Fatal("atime not updated for relatime") + } + attr.Atime = time.Now().Unix() + if m.atimeNeedsUpdate(attr, time.Now()) { + t.Fatal("atime updated for relatime") + } + + m.conf.AtimeMode = StrictAtime + if !m.atimeNeedsUpdate(attr, time.Now()) { + t.Fatal("atime not updated for strictatime") + } +} diff --git a/pkg/vfs/vfs_test.go b/pkg/vfs/vfs_test.go index 8f64bd5c5c488..e0686aa71d159 100644 --- a/pkg/vfs/vfs_test.go +++ b/pkg/vfs/vfs_test.go @@ -830,3 +830,168 @@ func TestInternalFile(t *testing.T) { t.Fatalf("result: %s", string(resp[:n])) } } + +func TestAtime(t *testing.T) { + v, _ := createTestVFS() + ctx := NewLogContext(meta.NewContext(10, 1, []uint32{2})) + + // noatime by default + fe, fh, e := v.Create(ctx, 1, "f1", 0755, 0, syscall.O_RDWR) + if e != 0 { + t.Fatalf("create file: %s", e) + } + if e = v.Write(ctx, fe.Inode, []byte("hello"), 0, fh); e != 0 { + t.Fatalf("write file: %s", e) + } + if fe2, e := v.SetAttr(ctx, fe.Inode, meta.SetAttrAtime, 0, 0, 0, 0, 1234, 0, 5678, 0, 0); e != 0 { + t.Fatalf("setattr f1: %s", e) + } else if fe2.Attr.Atime != 1234 || fe2.Attr.Atimensec != 5678 { + t.Fatalf("setattr f1: %+v", fe2.Attr) + } + var buf = make([]byte, 4096) + if _, e := v.Read(ctx, fe.Inode, buf, 0, fh); e != 0 { + t.Fatalf("read file: %s", e) + } + if fe, e := v.GetAttr(ctx, fe.Inode, 0); e != 0 || fe.Attr.Atime != 1234 || fe.Attr.Atimensec != 5678 { + t.Fatalf("getattr after read f1: %s atime: %v, atimensec %v", e, fe.Attr.Atime, fe.Attr.Atimensec) + } + de, e := v.Mkdir(ctx, 1, "d1", 0755, 0) + if e != 0 { + t.Fatalf("mkdir d1: %s", e) + } + if de2, e := v.SetAttr(ctx, de.Inode, meta.SetAttrAtime, 0, 0, 0, 0, 1234, 0, 5678, 0, 0); e != 0 { + t.Fatalf("setattr d1: %s", e) + } else if de2.Attr.Atime != 1234 || de2.Attr.Atimensec != 5678 { + t.Fatalf("setattr d1: %+v", de2.Attr) + } + fh, e = v.Opendir(ctx, de.Inode) + if e != 0 { + t.Fatalf("opendir d1: %s", e) + } + _, _, e = v.Readdir(ctx, de.Inode, 1024, 0, fh, false) + if e != 0 { + t.Fatalf("readdir d1: %s", e) + } + if fe, e := v.GetAttr(ctx, de.Inode, 0); e != 0 || fe.Attr.Atime != 1234 || fe.Attr.Atimensec != 5678 { + t.Fatalf("getattr after readdir d1: %s atime: %v, atimensec %v", e, fe.Attr.Atime, fe.Attr.Atimensec) + } + + // set relatime + v.Conf.Meta.AtimeMode = meta.RelAtime + fe, fh, e = v.Create(ctx, 1, "f2", 0755, 0, syscall.O_RDWR) + if e != 0 { + t.Fatalf("create file: %s", e) + } + if e = v.Write(ctx, fe.Inode, []byte("hello"), 0, fh); e != 0 { + t.Fatalf("write file: %s", e) + } + if fe2, e := v.SetAttr(ctx, fe.Inode, meta.SetAttrAtime, 0, 0, 0, 0, 1234, 0, 5678, 0, 0); e != 0 { + t.Fatalf("setattr f2: %s", e) + } else if fe2.Attr.Atime != 1234 || fe2.Attr.Atimensec != 5678 { + t.Fatalf("setattr f2: %+v", fe2.Attr) + } + if _, e := v.Read(ctx, fe.Inode, buf, 0, fh); e != 0 { + t.Fatalf("read file: %s", e) + } + // older than 24h, update atime on read + if fe, e := v.GetAttr(ctx, fe.Inode, 0); e != 0 || fe.Attr.Atime == 1234 { + t.Fatalf("getattr after read f2: %s atime: %v, atimensec %v", e, fe.Attr.Atime, fe.Attr.Atimensec) + } + + de, e = v.Mkdir(ctx, 1, "d2", 0755, 0) + if e != 0 { + t.Fatalf("mkdir d2: %s", e) + } + if de2, e := v.SetAttr(ctx, de.Inode, meta.SetAttrAtime, 0, 0, 0, 0, 1234, 0, 5678, 0, 0); e != 0 { + t.Fatalf("setattr d2: %s", e) + } else if de2.Attr.Atime != 1234 || de2.Attr.Atimensec != 5678 { + t.Fatalf("setattr d2: %+v", de2.Attr) + } + dfh, e := v.Opendir(ctx, de.Inode) + if e != 0 { + t.Fatalf("opendir d2: %s", e) + } + _, _, e = v.Readdir(ctx, de.Inode, 1024, 0, dfh, false) + if e != 0 { + t.Fatalf("readdir d2: %s", e) + } + // update atime on readdir + if fe, e := v.GetAttr(ctx, de.Inode, 0); e != 0 || fe.Attr.Atime == 1234 { + t.Fatalf("getattr after readdir d2: %s atime: %v, atimensec %v", e, fe.Attr.Atime, fe.Attr.Atimensec) + } + + now := time.Now() + if fe2, e := v.SetAttr(ctx, fe.Inode, meta.SetAttrMtime, 0, 0, 0, 0, 0, now.Unix(), 0, uint32(now.Nanosecond()), 0); e != 0 { + t.Fatalf("setattr f2: %s", e) + } else if fe2.Attr.Mtime != now.Unix() || fe2.Attr.Mtimensec != uint32(now.Nanosecond()) { + t.Fatalf("setattr f2: %+v", fe2.Attr) + } + time.Sleep(time.Second) + if _, e := v.Read(ctx, fe.Inode, buf, 0, fh); e != 0 { + t.Fatalf("read file: %s", e) + } + // mtime > atime, update atime on read + if fe, e := v.GetAttr(ctx, fe.Inode, 0); e != 0 || fe.Attr.Atime < now.Unix() { + t.Fatalf("getattr after read f2: %s atime: %v, now %v", e, fe.Attr.Atime, now.Unix()) + } + + now = time.Now() + if de2, e := v.SetAttr(ctx, de.Inode, meta.SetAttrMtime, 0, 0, 0, 0, 0, now.Unix(), 0, uint32(now.Nanosecond()), 0); e != 0 { + t.Fatalf("setattr d2: %s", e) + } else if de2.Attr.Mtime != now.Unix() || de2.Attr.Mtimensec != uint32(now.Nanosecond()) { + t.Fatalf("setattr d2: %+v", de2.Attr) + } + time.Sleep(time.Second) + _, _, e = v.Readdir(ctx, de.Inode, 1024, 0, dfh, false) + if e != 0 { + t.Fatalf("readdir d2: %s", e) + } + // mtime > atime, update atime on readdir + if fe, e := v.GetAttr(ctx, de.Inode, 0); e != 0 || fe.Attr.Atime < now.Unix() { + t.Fatalf("getattr after readdir d2: %s atime: %v, now %v", e, fe.Attr.Atime, now.Unix()) + } + + // set strictatime + v.Conf.Meta.AtimeMode = meta.StrictAtime + fe, fh, e = v.Create(ctx, 1, "f3", 0755, 0, syscall.O_RDWR) + if e != 0 { + t.Fatalf("create file: %s", e) + } + if e = v.Write(ctx, fe.Inode, []byte("hello"), 0, fh); e != 0 { + t.Fatalf("write file: %s", e) + } + if fe2, e := v.SetAttr(ctx, fe.Inode, meta.SetAttrAtime, 0, 0, 0, 0, 1234, 0, 5678, 0, 0); e != 0 { + t.Fatalf("setattr f3: %s", e) + } else if fe2.Attr.Atime != 1234 || fe2.Attr.Atimensec != 5678 { + t.Fatalf("setattr f3: %+v", fe2.Attr) + } + if _, e := v.Read(ctx, fe.Inode, buf, 0, fh); e != 0 { + t.Fatalf("read file: %s", e) + } + // always update atime on read + if fe, e := v.GetAttr(ctx, fe.Inode, 0); e != 0 || fe.Attr.Atime == 1234 { + t.Fatalf("getattr after read f3: %s atime: %v, atimensec %v", e, fe.Attr.Atime, fe.Attr.Atimensec) + } + _, _, e = v.Readdir(ctx, de.Inode, 1024, 0, dfh, false) + if e != 0 { + t.Fatalf("readdir d2: %s", e) + } + if fe, e := v.GetAttr(ctx, de.Inode, 0); e != 0 || fe.Attr.Atime == 1234 { + t.Fatalf("getattr after readdir d2: %s atime: %v, now %v", e, fe.Attr.Atime, now.Unix()) + } + now = time.Now() + time.Sleep(time.Second) + if _, e := v.Read(ctx, fe.Inode, buf, 0, fh); e != 0 { + t.Fatalf("read file: %s", e) + } + if fe, e := v.GetAttr(ctx, fe.Inode, 0); e != 0 || fe.Attr.Atime < now.Unix() { + t.Fatalf("getattr after access f3: %s atime: %v, now %v", e, fe.Attr.Atime, now.Unix()) + } + _, _, e = v.Readdir(ctx, de.Inode, 1024, 0, dfh, false) + if e != 0 { + t.Fatalf("readdir d2: %s", e) + } + if fe, e := v.GetAttr(ctx, de.Inode, 0); e != 0 || fe.Attr.Atime < now.Unix() { + t.Fatalf("getattr after readdir d2: %s atime: %v, now %v", e, fe.Attr.Atime, now.Unix()) + } +}