Skip to content

Commit

Permalink
vfs: introduce --atime-mode option to control atime update behavior
Browse files Browse the repository at this point in the history
Currently juicefs doesn't update atime on access, so introduce
--atime-mode option to select how to update atime. And support three
modes:

- noatime: don't update atime
- relatime: update atime relative to motify or change time
- strictatime: always update atime

Fixes: #3240
Signed-off-by: Eryu Guan <[email protected]>
  • Loading branch information
eryugey committed Apr 26, 2023
1 parent 39cc73d commit 1fe64e8
Show file tree
Hide file tree
Showing 14 changed files with 420 additions and 6 deletions.
4 changes: 4 additions & 0 deletions cmd/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
}
}

Expand Down
7 changes: 7 additions & 0 deletions cmd/mount.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
5 changes: 4 additions & 1 deletion docs/en/development/internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions docs/en/reference/command_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,9 @@ interval (in seconds) to send heartbeat; it's recommended that all clients use t
`--no-bgjob`<br />
disable background jobs (clean-up, backup, etc.) (default: false)

`--atime-mode value`<br />
control atime behavior, support 3 modes, `noatime`, `relatime`, `strictatime`, default to `noatime`

#### Examples

```bash
Expand Down
5 changes: 4 additions & 1 deletion docs/zh_cn/development/internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions docs/zh_cn/reference/command_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,9 @@ Consul 注册中心地址 (默认:"127.0.0.1:8500")
`--no-bgjob`<br />
禁用后台作业(清理、备份等)(默认:false)

`--atime-mode value`<br />
控制如何更新 atime,支持3种模式,`noatime``relatime``strictatime`,默认使用 `noatime`

#### 示例

```shell
Expand Down
10 changes: 9 additions & 1 deletion pkg/meta/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions pkg/meta/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type Config struct {
Heartbeat time.Duration
MountPoint string
Subdir string
AtimeMode string
}

func DefaultConf() *Config {
Expand Down
37 changes: 36 additions & 1 deletion pkg/meta/redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)))
}
36 changes: 35 additions & 1 deletion pkg/meta/sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
}))
}
33 changes: 32 additions & 1 deletion pkg/meta/tkv.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
}))
}
36 changes: 36 additions & 0 deletions pkg/meta/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
81 changes: 81 additions & 0 deletions pkg/meta/utils_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
Loading

0 comments on commit 1fe64e8

Please sign in to comment.