X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/4dda5d7b0ea74103c07617cc2ab9e5c97682c85d..555c67a6126b88b592110b82bd96fed5cff5da31:/sdk/go/arvados/fs_base.go diff --git a/sdk/go/arvados/fs_base.go b/sdk/go/arvados/fs_base.go index 3058a7609c..4dd8b53e1d 100644 --- a/sdk/go/arvados/fs_base.go +++ b/sdk/go/arvados/fs_base.go @@ -29,8 +29,42 @@ var ( ErrIsDirectory = errors.New("cannot rename file to overwrite existing directory") ErrNotADirectory = errors.New("not a directory") ErrPermission = os.ErrPermission + DebugLocksPanicMode = false ) +type syncer interface { + Sync() error +} + +func debugPanicIfNotLocked(l sync.Locker, writing bool) { + if !DebugLocksPanicMode { + return + } + race := false + if rl, ok := l.(interface { + RLock() + RUnlock() + }); ok && writing { + go func() { + // Fail if we can grab the read lock during an + // operation that purportedly has write lock. + rl.RLock() + race = true + rl.RUnlock() + }() + } else { + go func() { + l.Lock() + race = true + l.Unlock() + }() + } + time.Sleep(100) + if race { + panic("bug: caller-must-have-lock func called, but nobody has lock") + } +} + // A File is an *os.File-like interface for reading and writing files // in a FileSystem. type File interface { @@ -58,6 +92,9 @@ type FileSystem interface { // while locking multiple inodes. locker() sync.Locker + // throttle for limiting concurrent background writers + throttle() *throttle + // create a new node with nil parent. newNode(name string, perm os.FileMode, modTime time.Time) (node inode, err error) @@ -86,7 +123,22 @@ type FileSystem interface { Remove(name string) error RemoveAll(name string) error Rename(oldname, newname string) error + + // Write buffered data from memory to storage, returning when + // all updates have been saved to persistent storage. Sync() error + + // Write buffered data from memory to storage, but don't wait + // for all writes to finish before returning. If shortBlocks + // is true, flush everything; otherwise, if there's less than + // a full block of buffered data at the end of a stream, leave + // it buffered in memory in case more data can be appended. If + // path is "", flush all dirs/streams; otherwise, flush only + // the specified dir/stream. + Flush(path string, shortBlocks bool) error + + // Estimate current memory usage. + MemorySize() int64 } type inode interface { @@ -137,6 +189,7 @@ type inode interface { sync.Locker RLock() RUnlock() + MemorySize() int64 } type fileinfo struct { @@ -210,6 +263,13 @@ func (*nullnode) Child(name string, replace func(inode) (inode, error)) (inode, return nil, ErrNotADirectory } +func (*nullnode) MemorySize() int64 { + // Types that embed nullnode should report their own size, but + // if they don't, we at least report a non-zero size to ensure + // a large tree doesn't get reported as 0 bytes. + return 64 +} + type treenode struct { fs FileSystem parent inode @@ -241,6 +301,7 @@ func (n *treenode) IsDir() bool { } func (n *treenode) Child(name string, replace func(inode) (inode, error)) (child inode, err error) { + debugPanicIfNotLocked(n, false) child = n.inodes[name] if name == "" || name == "." || name == ".." { err = ErrInvalidArgument @@ -254,8 +315,10 @@ func (n *treenode) Child(name string, replace func(inode) (inode, error)) (child return } if newchild == nil { + debugPanicIfNotLocked(n, true) delete(n.inodes, name) } else if newchild != child { + debugPanicIfNotLocked(n, true) n.inodes[name] = newchild n.fileinfo.modTime = time.Now() child = newchild @@ -284,16 +347,47 @@ func (n *treenode) Readdir() (fi []os.FileInfo, err error) { return } +func (n *treenode) Sync() error { + n.RLock() + defer n.RUnlock() + for _, inode := range n.inodes { + syncer, ok := inode.(syncer) + if !ok { + return ErrInvalidOperation + } + err := syncer.Sync() + if err != nil { + return err + } + } + return nil +} + +func (n *treenode) MemorySize() (size int64) { + n.RLock() + defer n.RUnlock() + debugPanicIfNotLocked(n, false) + for _, inode := range n.inodes { + size += inode.MemorySize() + } + return +} + type fileSystem struct { root inode fsBackend mutex sync.Mutex + thr *throttle } func (fs *fileSystem) rootnode() inode { return fs.root } +func (fs *fileSystem) throttle() *throttle { + return fs.thr +} + func (fs *fileSystem) locker() sync.Locker { return &fs.mutex } @@ -335,13 +429,12 @@ func (fs *fileSystem) openFile(name string, flag int, perm os.FileMode) (*fileha } } createMode := flag&os.O_CREATE != 0 - if createMode { - parent.Lock() - defer parent.Unlock() - } else { - parent.RLock() - defer parent.RUnlock() - } + // We always need to take Lock() here, not just RLock(). Even + // if we know we won't be creating a file, parent might be a + // lookupnode, which sometimes populates its inodes map during + // a Child() call. + parent.Lock() + defer parent.Unlock() n, err := parent.Child(name, nil) if err != nil { return nil, err @@ -556,10 +649,21 @@ func (fs *fileSystem) remove(name string, recursive bool) error { } func (fs *fileSystem) Sync() error { - log.Printf("TODO: sync fileSystem") + if syncer, ok := fs.root.(syncer); ok { + return syncer.Sync() + } return ErrInvalidOperation } +func (fs *fileSystem) Flush(string, bool) error { + log.Printf("TODO: flush fileSystem") + return ErrInvalidOperation +} + +func (fs *fileSystem) MemorySize() int64 { + return fs.root.MemorySize() +} + // rlookup (recursive lookup) returns the inode for the file/directory // with the given name (which may contain "/" separators). If no such // file/directory exists, the returned node is nil.