20318: Sync cache state after 1% churn instead of 5 minute timer.
[arvados.git] / sdk / go / arvados / fs_lookup.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: Apache-2.0
4
5 package arvados
6
7 import (
8         "os"
9         "time"
10 )
11
12 // lookupnode is a caching tree node that is initially empty and calls
13 // loadOne and loadAll to load/update child nodes as needed.
14 //
15 // See (*customFileSystem)MountUsers for example usage.
16 type lookupnode struct {
17         treenode
18         loadOne func(parent inode, name string) (inode, error)
19         loadAll func(parent inode) ([]inode, error)
20         stale   func(time.Time) bool
21
22         // internal fields
23         staleAll time.Time
24         staleOne map[string]time.Time
25 }
26
27 // Sync flushes pending writes for loaded children and, if successful,
28 // triggers a reload on next lookup.
29 func (ln *lookupnode) Sync() error {
30         err := ln.treenode.Sync()
31         if err != nil {
32                 return err
33         }
34         ln.Lock()
35         ln.staleAll = time.Time{}
36         ln.staleOne = nil
37         ln.Unlock()
38         return nil
39 }
40
41 func (ln *lookupnode) Readdir() ([]os.FileInfo, error) {
42         ln.Lock()
43         checkTime := time.Now()
44         if ln.stale(ln.staleAll) {
45                 all, err := ln.loadAll(ln)
46                 if err != nil {
47                         ln.Unlock()
48                         return nil, err
49                 }
50                 for _, child := range all {
51                         var name string
52                         if hl, ok := child.(*hardlink); ok && hl.inode == ln {
53                                 // If child is a hardlink to its
54                                 // parent, FileInfo()->RLock() will
55                                 // deadlock, because we already have
56                                 // the write lock. In this situation
57                                 // we can safely access the hardlink's
58                                 // name directly.
59                                 name = hl.name
60                         } else {
61                                 name = child.FileInfo().Name()
62                         }
63                         _, err = ln.treenode.Child(name, func(inode) (inode, error) {
64                                 return child, nil
65                         })
66                         if err != nil {
67                                 ln.Unlock()
68                                 return nil, err
69                         }
70                 }
71                 ln.staleAll = checkTime
72                 // No value in ln.staleOne can make a difference to an
73                 // "entry is stale?" test now, because no value is
74                 // newer than ln.staleAll. Reclaim memory.
75                 ln.staleOne = nil
76         }
77         ln.Unlock()
78         return ln.treenode.Readdir()
79 }
80
81 // Child rejects (with ErrInvalidOperation) calls to add/replace
82 // children, instead calling loadOne when a non-existing child is
83 // looked up.
84 func (ln *lookupnode) Child(name string, replace func(inode) (inode, error)) (inode, error) {
85         checkTime := time.Now()
86         var existing inode
87         var err error
88         if ln.stale(ln.staleAll) && ln.stale(ln.staleOne[name]) {
89                 existing, err = ln.treenode.Child(name, func(inode) (inode, error) {
90                         return ln.loadOne(ln, name)
91                 })
92                 if err == nil && existing != nil {
93                         if ln.staleOne == nil {
94                                 ln.staleOne = map[string]time.Time{name: checkTime}
95                         } else {
96                                 ln.staleOne[name] = checkTime
97                         }
98                 }
99         } else {
100                 existing, err = ln.treenode.Child(name, nil)
101                 if err != nil && !os.IsNotExist(err) {
102                         return existing, err
103                 }
104         }
105         if replace != nil {
106                 // Let the callback try to delete or replace the
107                 // existing node; if it does, return
108                 // ErrInvalidOperation.
109                 if tryRepl, err := replace(existing); err != nil {
110                         // Propagate error from callback
111                         return existing, err
112                 } else if tryRepl != existing {
113                         return existing, ErrInvalidOperation
114                 }
115         }
116         // Return original error from ln.treenode.Child() (it might be
117         // ErrNotExist).
118         return existing, err
119 }