19362: Use hardlinks to a singleton for each project/collection.
[arvados.git] / sdk / go / arvados / fs_site.go
index 531d7968abab360df333619ce5f300f1a809209d..716ef34e26098771c44f99c46aeea593ce3e87fb 100644 (file)
@@ -29,6 +29,10 @@ type customFileSystem struct {
        staleLock      sync.Mutex
 
        forwardSlashNameSubstitution string
+
+       byID     map[string]inode
+       byIDLock sync.Mutex
+       byIDRoot *treenode
 }
 
 func (c *Client) CustomFileSystem(kc keepClient) CustomFileSystem {
@@ -51,6 +55,17 @@ func (c *Client) CustomFileSystem(kc keepClient) CustomFileSystem {
                },
                inodes: make(map[string]inode),
        }
+       fs.byID = map[string]inode{}
+       fs.byIDRoot = &treenode{
+               fs:     fs,
+               parent: root,
+               inodes: make(map[string]inode),
+               fileinfo: fileinfo{
+                       name:    "_internal_by_id",
+                       modTime: time.Now(),
+                       mode:    0755 | os.ModeDir,
+               },
+       }
        return fs
 }
 
@@ -69,7 +84,7 @@ func (fs *customFileSystem) MountByID(mount string) {
                                        mode:    0755 | os.ModeDir,
                                },
                        },
-                       create: fs.mountByID,
+                       create: fs.newCollectionOrProjectHardlink,
                }, nil
        })
 }
@@ -78,7 +93,7 @@ func (fs *customFileSystem) MountProject(mount, uuid string) {
        fs.root.treenode.Lock()
        defer fs.root.treenode.Unlock()
        fs.root.treenode.Child(mount, func(inode) (inode, error) {
-               return fs.newProjectNode(fs.root, mount, uuid, nil), nil
+               return fs.newProjectDir(fs.root, mount, uuid, nil), nil
        })
 }
 
@@ -122,7 +137,7 @@ func (c *Client) SiteFileSystem(kc keepClient) CustomFileSystem {
 }
 
 func (fs *customFileSystem) Sync() error {
-       return fs.root.Sync()
+       return fs.byIDRoot.Sync()
 }
 
 // Stale returns true if information obtained at time t should be
@@ -137,54 +152,59 @@ func (fs *customFileSystem) newNode(name string, perm os.FileMode, modTime time.
        return nil, ErrInvalidOperation
 }
 
-func (fs *customFileSystem) mountByID(parent inode, id string) (inode, error) {
+func (fs *customFileSystem) newCollectionOrProjectHardlink(parent inode, id string) (inode, error) {
+       fs.byIDLock.Lock()
+       n := fs.byID[id]
+       if n != nil {
+               // Avoid the extra remote API lookup if we already
+               // have a singleton for this ID.
+               fs.byIDLock.Unlock()
+               return &hardlink{inode: n, parent: parent, name: id}, nil
+       }
+       fs.byIDLock.Unlock()
+
        if strings.Contains(id, "-4zz18-") || pdhRegexp.MatchString(id) {
-               return fs.mountCollection(parent, id)
-       } else if strings.Contains(id, "-j7d0g-") {
-               return fs.newProjectNode(fs.root, id, id, nil), nil
+               node, err := fs.collectionSingleton(id)
+               if os.IsNotExist(err) {
+                       return nil, nil
+               } else if err != nil {
+                       return nil, err
+               }
+               return &hardlink{inode: node, parent: parent, name: id}, nil
+       } else if strings.Contains(id, "-j7d0g-") || strings.Contains(id, "-tpzed-") {
+               proj, err := fs.getProject(id)
+               if os.IsNotExist(err) {
+                       return nil, nil
+               } else if err != nil {
+                       return nil, err
+               }
+               node := fs.projectSingleton(id, proj)
+               return &hardlink{inode: node, parent: parent, name: id}, nil
        } else {
                return nil, nil
        }
 }
 
-func (fs *customFileSystem) mountCollection(parent inode, id string) (inode, error) {
-       var coll Collection
-       err := fs.RequestAndDecode(&coll, "GET", "arvados/v1/collections/"+id, nil, nil)
-       if statusErr, ok := err.(interface{ HTTPStatus() int }); ok && statusErr.HTTPStatus() == http.StatusNotFound {
-               return nil, nil
-       } else if err != nil {
-               return nil, err
-       }
-       if len(id) != 27 {
-               // This means id is a PDH, and controller/railsapi
-               // returned one of (possibly) many collections with
-               // that PDH. Even if controller returns more fields
-               // besides PDH and manifest text (which are equal for
-               // all matching collections), we don't want to expose
-               // them (e.g., through Sys()).
-               coll = Collection{
-                       PortableDataHash: coll.PortableDataHash,
-                       ManifestText:     coll.ManifestText,
-               }
+func (fs *customFileSystem) projectSingleton(uuid string, proj *Group) inode {
+       fs.byIDLock.Lock()
+       defer fs.byIDLock.Unlock()
+       if n := fs.byID[uuid]; n != nil {
+               return n
        }
-       newfs, err := coll.FileSystem(fs, fs)
-       if err != nil {
-               return nil, err
+       name := uuid
+       if name == "" {
+               // special case uuid=="" implements the "home project"
+               // (owner_uuid == current user uuid)
+               name = "home"
        }
-       cfs := newfs.(*collectionFileSystem)
-       cfs.SetParent(parent, id)
-       return cfs, nil
-}
-
-func (fs *customFileSystem) newProjectNode(root inode, name, uuid string, proj *Group) inode {
        var projLoading sync.Mutex
-       return &lookupnode{
+       n := &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) },
                treenode: treenode{
                        fs:     fs,
-                       parent: root,
+                       parent: fs.byIDRoot,
                        inodes: make(map[string]inode),
                        fileinfo: fileinfo{
                                name:    name,
@@ -196,17 +216,81 @@ func (fs *customFileSystem) newProjectNode(root inode, name, uuid string, proj *
                                        if proj != nil {
                                                return proj
                                        }
-                                       var g Group
-                                       err := fs.RequestAndDecode(&g, "GET", "arvados/v1/groups/"+uuid, nil, nil)
+                                       g, err := fs.getProject(uuid)
                                        if err != nil {
                                                return err
                                        }
-                                       proj = &g
+                                       proj = g
                                        return proj
                                },
                        },
                },
        }
+       fs.byID[uuid] = n
+       return n
+}
+
+func (fs *customFileSystem) getProject(uuid string) (*Group, error) {
+       var g Group
+       err := fs.RequestAndDecode(&g, "GET", "arvados/v1/groups/"+uuid, nil, nil)
+       if statusErr, ok := err.(interface{ HTTPStatus() int }); ok && statusErr.HTTPStatus() == http.StatusNotFound {
+               return nil, os.ErrNotExist
+       } else if err != nil {
+               return nil, err
+       }
+       return &g, err
+}
+
+func (fs *customFileSystem) collectionSingleton(id string) (inode, error) {
+       fs.byIDLock.Lock()
+       if n := fs.byID[id]; n != nil {
+               fs.byIDLock.Unlock()
+               return n, nil
+       }
+       fs.byIDLock.Unlock()
+
+       coll, err := fs.getCollection(id)
+       if err != nil {
+               return nil, err
+       }
+       newfs, err := coll.FileSystem(fs, fs)
+       if err != nil {
+               return nil, err
+       }
+       cfs := newfs.(*collectionFileSystem)
+       cfs.SetParent(fs.byIDRoot, id)
+
+       fs.byIDLock.Lock()
+       defer fs.byIDLock.Unlock()
+       if n := fs.byID[id]; n != nil {
+               return n, nil
+       }
+       fs.byID[id] = cfs
+       fs.byIDRoot.Child(id, func(inode) (inode, error) { return cfs, nil })
+       return cfs, nil
+}
+
+func (fs *customFileSystem) getCollection(id string) (*Collection, error) {
+       var coll Collection
+       err := fs.RequestAndDecode(&coll, "GET", "arvados/v1/collections/"+id, nil, nil)
+       if statusErr, ok := err.(interface{ HTTPStatus() int }); ok && statusErr.HTTPStatus() == http.StatusNotFound {
+               return nil, os.ErrNotExist
+       } else if err != nil {
+               return nil, err
+       }
+       if len(id) != 27 {
+               // This means id is a PDH, and controller/railsapi
+               // returned one of (possibly) many collections with
+               // that PDH. Even if controller returns more fields
+               // besides PDH and manifest text (which are equal for
+               // all matching collections), we don't want to expose
+               // them (e.g., through Sys()).
+               coll = Collection{
+                       PortableDataHash: coll.PortableDataHash,
+                       ManifestText:     coll.ManifestText,
+               }
+       }
+       return &coll, nil
 }
 
 // vdirnode wraps an inode by rejecting (with ErrInvalidOperation)
@@ -244,3 +328,41 @@ func (vn *vdirnode) Child(name string, replace func(inode) (inode, error)) (inod
                }
        })
 }
+
+// A hardlink can be used to mount an existing node at an additional
+// point in the same filesystem.
+type hardlink struct {
+       inode
+       parent inode
+       name   string
+}
+
+func (hl *hardlink) Sync() error {
+       if node, ok := hl.inode.(syncer); ok {
+               return node.Sync()
+       } else {
+               return ErrInvalidOperation
+       }
+}
+
+func (hl *hardlink) SetParent(parent inode, name string) {
+       hl.Lock()
+       defer hl.Unlock()
+       hl.parent = parent
+       hl.name = name
+}
+
+func (hl *hardlink) Parent() inode {
+       hl.RLock()
+       defer hl.RUnlock()
+       return hl.parent
+}
+
+func (hl *hardlink) FileInfo() os.FileInfo {
+       fi := hl.inode.FileInfo()
+       if fi, ok := fi.(fileinfo); ok {
+               fi.name = hl.name
+               return fi
+       }
+       return fi
+}