13111: Avoid multi-page API reqs when looking up entries by name.
authorTom Clegg <tclegg@veritasgenetics.com>
Thu, 12 Apr 2018 20:30:17 +0000 (16:30 -0400)
committerTom Clegg <tclegg@veritasgenetics.com>
Fri, 13 Apr 2018 19:31:49 +0000 (15:31 -0400)
Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg@veritasgenetics.com>

sdk/go/arvados/fs_lookup.go [new file with mode: 0644]
sdk/go/arvados/fs_project.go
sdk/go/arvados/fs_project_test.go
sdk/go/arvados/fs_site.go
sdk/go/arvados/fs_users.go

diff --git a/sdk/go/arvados/fs_lookup.go b/sdk/go/arvados/fs_lookup.go
new file mode 100644 (file)
index 0000000..42322a1
--- /dev/null
@@ -0,0 +1,73 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package arvados
+
+import (
+       "os"
+       "sync"
+       "time"
+)
+
+// lookupnode is a caching tree node that is initially empty and calls
+// loadOne and loadAll to load/update child nodes as needed.
+//
+// See (*customFileSystem)MountUsers for example usage.
+type lookupnode struct {
+       inode
+       loadOne func(parent inode, name string) (inode, error)
+       loadAll func(parent inode) ([]inode, error)
+       stale   func(time.Time) bool
+
+       // internal fields
+       staleLock sync.Mutex
+       staleAll  time.Time
+       staleOne  map[string]time.Time
+}
+
+func (ln *lookupnode) Readdir() ([]os.FileInfo, error) {
+       ln.staleLock.Lock()
+       defer ln.staleLock.Unlock()
+       checkTime := time.Now()
+       if ln.stale(ln.staleAll) {
+               all, err := ln.loadAll(ln)
+               if err != nil {
+                       return nil, err
+               }
+               for _, child := range all {
+                       _, err = ln.inode.Child(child.FileInfo().Name(), func(inode) (inode, error) {
+                               return child, nil
+                       })
+                       if err != nil {
+                               return nil, err
+                       }
+               }
+               ln.staleAll = checkTime
+               // No value in ln.staleOne can make a difference to an
+               // "entry is stale?" test now, because no value is
+               // newer than ln.staleAll. Reclaim memory.
+               ln.staleOne = nil
+       }
+       return ln.inode.Readdir()
+}
+
+func (ln *lookupnode) Child(name string, replace func(inode) (inode, error)) (inode, error) {
+       ln.staleLock.Lock()
+       defer ln.staleLock.Unlock()
+       checkTime := time.Now()
+       if ln.stale(ln.staleAll) && ln.stale(ln.staleOne[name]) {
+               _, err := ln.inode.Child(name, func(inode) (inode, error) {
+                       return ln.loadOne(ln, name)
+               })
+               if err != nil {
+                       return nil, err
+               }
+               if ln.staleOne == nil {
+                       ln.staleOne = map[string]time.Time{name: checkTime}
+               } else {
+                       ln.staleOne[name] = checkTime
+               }
+       }
+       return ln.inode.Child(name, replace)
+}
index 827a44b82224b84af67eb92329115d2c4256b1b5..92995510c7eaefaa4de69ea42f6c5f050e6828bd 100644 (file)
@@ -5,58 +5,81 @@
 package arvados
 
 import (
+       "log"
        "os"
-       "sync"
-       "time"
+       "strings"
 )
 
-type staleChecker struct {
-       mtx  sync.Mutex
-       last time.Time
+func (fs *customFileSystem) defaultUUID(uuid string) (string, error) {
+       if uuid != "" {
+               return uuid, nil
+       }
+       var resp User
+       err := fs.RequestAndDecode(&resp, "GET", "arvados/v1/users/current", nil, nil)
+       if err != nil {
+               return "", err
+       }
+       return resp.UUID, nil
 }
 
-func (sc *staleChecker) DoIfStale(fn func(), staleFunc func(time.Time) bool) {
-       sc.mtx.Lock()
-       defer sc.mtx.Unlock()
-       if !staleFunc(sc.last) {
-               return
+// loadOneChild loads only the named child, if it exists.
+func (fs *customFileSystem) projectsLoadOne(parent inode, uuid, name string) (inode, error) {
+       uuid, err := fs.defaultUUID(uuid)
+       if err != nil {
+               return nil, err
        }
-       sc.last = time.Now()
-       fn()
-}
 
-// projectnode exposes an Arvados project as a filesystem directory.
-type projectnode struct {
-       inode
-       staleChecker
-       uuid string
-       err  error
-}
+       var contents CollectionList
+       err = fs.RequestAndDecode(&contents, "GET", "arvados/v1/groups/"+uuid+"/contents", nil, ResourceListParams{
+               Count: "none",
+               Filters: []Filter{
+                       {"name", "=", name},
+                       {"uuid", "is_a", []string{"arvados#collection", "arvados#group"}},
+                       {"groups.group_class", "=", "project"},
+               },
+       })
+       if err != nil {
+               return nil, err
+       }
+       if len(contents.Items) == 0 {
+               return nil, os.ErrNotExist
+       }
+       coll := contents.Items[0]
 
-func (pn *projectnode) load() {
-       fs := pn.FS().(*customFileSystem)
+       if strings.Contains(coll.UUID, "-j7d0g-") {
+               // Group item was loaded into a Collection var -- but
+               // we only need the Name and UUID anyway, so it's OK.
+               return fs.newProjectNode(parent, coll.Name, coll.UUID), nil
+       } else if strings.Contains(coll.UUID, "-4zz18-") {
+               return deferredCollectionFS(fs, parent, coll), nil
+       } else {
+               log.Printf("projectnode: unrecognized UUID in response: %q", coll.UUID)
+               return nil, ErrInvalidArgument
+       }
+}
 
-       if pn.uuid == "" {
-               var resp User
-               pn.err = fs.RequestAndDecode(&resp, "GET", "arvados/v1/users/current", nil, nil)
-               if pn.err != nil {
-                       return
-               }
-               pn.uuid = resp.UUID
+func (fs *customFileSystem) projectsLoadAll(parent inode, uuid string) ([]inode, error) {
+       uuid, err := fs.defaultUUID(uuid)
+       if err != nil {
+               return nil, err
        }
+
+       var inodes []inode
+
        // Note: the "filters" slice's backing array might be reused
        // by append(filters,...) below. This isn't goroutine safe,
        // but all accesses are in the same goroutine, so it's OK.
-       filters := []Filter{{"owner_uuid", "=", pn.uuid}}
+       filters := []Filter{{"owner_uuid", "=", uuid}}
        params := ResourceListParams{
+               Count:   "none",
                Filters: filters,
                Order:   "uuid",
        }
        for {
                var resp CollectionList
-               pn.err = fs.RequestAndDecode(&resp, "GET", "arvados/v1/collections", nil, params)
-               if pn.err != nil {
-                       return
+               err = fs.RequestAndDecode(&resp, "GET", "arvados/v1/collections", nil, params)
+               if err != nil {
+                       return nil, err
                }
                if len(resp.Items) == 0 {
                        break
@@ -66,9 +89,7 @@ func (pn *projectnode) load() {
                        if !permittedName(coll.Name) {
                                continue
                        }
-                       pn.inode.Child(coll.Name, func(inode) (inode, error) {
-                               return deferredCollectionFS(fs, pn, coll), nil
-                       })
+                       inodes = append(inodes, deferredCollectionFS(fs, parent, coll))
                }
                params.Filters = append(filters, Filter{"uuid", ">", resp.Items[len(resp.Items)-1].UUID})
        }
@@ -77,9 +98,9 @@ func (pn *projectnode) load() {
        params.Filters = filters
        for {
                var resp GroupList
-               pn.err = fs.RequestAndDecode(&resp, "GET", "arvados/v1/groups", nil, params)
-               if pn.err != nil {
-                       return
+               err = fs.RequestAndDecode(&resp, "GET", "arvados/v1/groups", nil, params)
+               if err != nil {
+                       return nil, err
                }
                if len(resp.Items) == 0 {
                        break
@@ -88,52 +109,9 @@ func (pn *projectnode) load() {
                        if !permittedName(group.Name) {
                                continue
                        }
-                       pn.inode.Child(group.Name, func(inode) (inode, error) {
-                               return fs.newProjectNode(pn, group.Name, group.UUID), nil
-                       })
+                       inodes = append(inodes, fs.newProjectNode(parent, group.Name, group.UUID))
                }
                params.Filters = append(filters, Filter{"uuid", ">", resp.Items[len(resp.Items)-1].UUID})
        }
-       pn.err = nil
-}
-
-func (pn *projectnode) Readdir() ([]os.FileInfo, error) {
-       pn.staleChecker.DoIfStale(pn.load, pn.FS().(*customFileSystem).Stale)
-       if pn.err != nil {
-               return nil, pn.err
-       }
-       return pn.inode.Readdir()
-}
-
-func (pn *projectnode) Child(name string, replace func(inode) (inode, error)) (inode, error) {
-       pn.staleChecker.DoIfStale(pn.load, pn.FS().(*customFileSystem).Stale)
-       if pn.err != nil {
-               return nil, pn.err
-       }
-       if replace == nil {
-               // lookup
-               return pn.inode.Child(name, nil)
-       }
-       return pn.inode.Child(name, func(existing inode) (inode, error) {
-               if repl, err := replace(existing); err != nil {
-                       return existing, err
-               } else if repl == nil {
-                       if existing == nil {
-                               return nil, nil
-                       }
-                       // rmdir
-                       // (TODO)
-                       return existing, ErrInvalidArgument
-               } else if existing != nil {
-                       // clobber
-                       return existing, ErrInvalidArgument
-               } else if repl.FileInfo().IsDir() {
-                       // mkdir
-                       // TODO: repl.SetParent(pn, name), etc.
-                       return existing, ErrInvalidArgument
-               } else {
-                       // create file
-                       return existing, ErrInvalidArgument
-               }
-       })
+       return inodes, nil
 }
index 058eb408610db365fa2ff49932c3b643cf9634a2..1a06ce14632af5e1dc7f219208307291009d8da9 100644 (file)
@@ -67,15 +67,15 @@ func (s *SiteFSSuite) testHomeProject(c *check.C, path string) {
        f, err = s.fs.Open(path + "/A Project/..")
        c.Assert(err, check.IsNil)
        fi, err := f.Stat()
-       c.Check(err, check.IsNil)
+       c.Assert(err, check.IsNil)
        c.Check(fi.IsDir(), check.Equals, true)
        _, basename := filepath.Split(path)
        c.Check(fi.Name(), check.Equals, basename)
 
        f, err = s.fs.Open(path + "/A Project/A Subproject")
-       c.Check(err, check.IsNil)
+       c.Assert(err, check.IsNil)
        fi, err = f.Stat()
-       c.Check(err, check.IsNil)
+       c.Assert(err, check.IsNil)
        c.Check(fi.IsDir(), check.Equals, true)
 
        for _, nx := range []string{
@@ -90,6 +90,34 @@ func (s *SiteFSSuite) testHomeProject(c *check.C, path string) {
        }
 }
 
+func (s *SiteFSSuite) TestProjectReaddirAfterLoadOne(c *check.C) {
+       f, err := s.fs.Open("/users/active/A Project/A Subproject")
+       c.Assert(err, check.IsNil)
+       defer f.Close()
+       f, err = s.fs.Open("/users/active/A Project/Project does not exist")
+       c.Assert(err, check.NotNil)
+       f, err = s.fs.Open("/users/active/A Project/A Subproject")
+       c.Assert(err, check.IsNil)
+       defer f.Close()
+       f, err = s.fs.Open("/users/active/A Project")
+       c.Assert(err, check.IsNil)
+       defer f.Close()
+       fis, err := f.Readdir(-1)
+       c.Assert(err, check.IsNil)
+       c.Logf("%#v", fis)
+       var foundSubproject, foundCollection bool
+       for _, fi := range fis {
+               switch fi.Name() {
+               case "A Subproject":
+                       foundSubproject = true
+               case "collection_to_move_around":
+                       foundCollection = true
+               }
+       }
+       c.Check(foundSubproject, check.Equals, true)
+       c.Check(foundCollection, check.Equals, true)
+}
+
 func (s *SiteFSSuite) TestSlashInName(c *check.C) {
        badCollection := Collection{
                Name:      "bad/collection",
@@ -109,7 +137,7 @@ func (s *SiteFSSuite) TestSlashInName(c *check.C) {
        defer s.client.RequestAndDecode(nil, "DELETE", "arvados/v1/groups/"+badProject.UUID, nil, nil)
 
        dir, err := s.fs.Open("/users/active/A Project")
-       c.Check(err, check.IsNil)
+       c.Assert(err, check.IsNil)
        fis, err := dir.Readdir(-1)
        c.Check(err, check.IsNil)
        for _, fi := range fis {
@@ -122,7 +150,7 @@ func (s *SiteFSSuite) TestProjectUpdatedByOther(c *check.C) {
        s.fs.MountProject("home", "")
 
        project, err := s.fs.OpenFile("/home/A Project", 0, 0)
-       c.Check(err, check.IsNil)
+       c.Assert(err, check.IsNil)
 
        _, err = s.fs.Open("/home/A Project/oob")
        c.Check(err, check.NotNil)
@@ -140,6 +168,7 @@ func (s *SiteFSSuite) TestProjectUpdatedByOther(c *check.C) {
        f, err := s.fs.Open("/home/A Project/oob")
        c.Assert(err, check.IsNil)
        fi, err := f.Stat()
+       c.Assert(err, check.IsNil)
        c.Check(fi.IsDir(), check.Equals, true)
        f.Close()
 
index e9c83870d1ae3869c5b6c615514cf4ea1e5a3df8..b5daf7b88381f952caaffd4de186624e19dbeb73 100644 (file)
@@ -73,7 +73,10 @@ func (fs *customFileSystem) MountProject(mount, uuid string) {
 
 func (fs *customFileSystem) MountUsers(mount string) {
        fs.root.inode.Child(mount, func(inode) (inode, error) {
-               return &usersnode{
+               return &lookupnode{
+                       stale:   fs.Stale,
+                       loadOne: fs.usersLoadOne,
+                       loadAll: fs.usersLoadAll,
                        inode: &treenode{
                                fs:     fs,
                                parent: fs.root,
@@ -136,24 +139,10 @@ func (fs *customFileSystem) mountCollection(parent inode, id string) inode {
 }
 
 func (fs *customFileSystem) newProjectNode(root inode, name, uuid string) inode {
-       return &projectnode{
-               uuid: uuid,
-               inode: &treenode{
-                       fs:     fs,
-                       parent: root,
-                       inodes: make(map[string]inode),
-                       fileinfo: fileinfo{
-                               name:    name,
-                               modTime: time.Now(),
-                               mode:    0755 | os.ModeDir,
-                       },
-               },
-       }
-}
-
-func (fs *customFileSystem) newUserNode(root inode, name, uuid string) inode {
-       return &projectnode{
-               uuid: uuid,
+       return &lookupnode{
+               stale:   fs.Stale,
+               loadOne: func(parent inode, name string) (inode, error) { return fs.projectsLoadOne(parent, uuid, name) },
+               loadAll: func(parent inode) ([]inode, error) { return fs.projectsLoadAll(parent, uuid) },
                inode: &treenode{
                        fs:     fs,
                        parent: root,
index ccfe2c5022824f972b60f1d2da12cfd3bf1922b7..00f70369694430f70f0cca185270ceb905e34c01 100644 (file)
@@ -8,55 +8,41 @@ import (
        "os"
 )
 
-// usersnode is a virtual directory with an entry for each visible
-// Arvados username, each showing the respective user's "home
-// projects".
-type usersnode struct {
-       inode
-       staleChecker
-       err error
+func (fs *customFileSystem) usersLoadOne(parent inode, name string) (inode, error) {
+       var resp UserList
+       err := fs.RequestAndDecode(&resp, "GET", "arvados/v1/users", nil, ResourceListParams{
+               Count:   "none",
+               Filters: []Filter{{"username", "=", name}},
+       })
+       if err != nil {
+               return nil, err
+       } else if len(resp.Items) == 0 {
+               return nil, os.ErrNotExist
+       }
+       user := resp.Items[0]
+       return fs.newProjectNode(parent, user.Username, user.UUID), nil
 }
 
-func (un *usersnode) load() {
-       fs := un.FS().(*customFileSystem)
-
+func (fs *customFileSystem) usersLoadAll(parent inode) ([]inode, error) {
        params := ResourceListParams{
+               Count: "none",
                Order: "uuid",
        }
+       var inodes []inode
        for {
                var resp UserList
-               un.err = fs.RequestAndDecode(&resp, "GET", "arvados/v1/users", nil, params)
-               if un.err != nil {
-                       return
-               }
-               if len(resp.Items) == 0 {
-                       break
+               err := fs.RequestAndDecode(&resp, "GET", "arvados/v1/users", nil, params)
+               if err != nil {
+                       return nil, err
+               } else if len(resp.Items) == 0 {
+                       return inodes, nil
                }
                for _, user := range resp.Items {
                        if user.Username == "" {
                                continue
                        }
-                       un.inode.Child(user.Username, func(inode) (inode, error) {
-                               return fs.newProjectNode(un, user.Username, user.UUID), nil
-                       })
+                       inodes = append(inodes, fs.newProjectNode(parent, user.Username, user.UUID))
                }
                params.Filters = []Filter{{"uuid", ">", resp.Items[len(resp.Items)-1].UUID}}
        }
-       un.err = nil
-}
-
-func (un *usersnode) Readdir() ([]os.FileInfo, error) {
-       un.staleChecker.DoIfStale(un.load, un.FS().(*customFileSystem).Stale)
-       if un.err != nil {
-               return nil, un.err
-       }
-       return un.inode.Readdir()
-}
-
-func (un *usersnode) Child(name string, _ func(inode) (inode, error)) (inode, error) {
-       un.staleChecker.DoIfStale(un.load, un.FS().(*customFileSystem).Stale)
-       if un.err != nil {
-               return nil, un.err
-       }
-       return un.inode.Child(name, nil)
 }